Skip to content

docs: consolidate README, GUIDE and USE-CASES against the current implementation - #67

Open
srnnkls wants to merge 15 commits into
mainfrom
docs/overhaul
Open

docs: consolidate README, GUIDE and USE-CASES against the current implementation#67
srnnkls wants to merge 15 commits into
mainfrom
docs/overhaul

Conversation

@srnnkls

@srnnkls srnnkls commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Supersedes #10, #11 and #28, which were each based on a commit from before the source → projection → sync reorganization and could not be rebased independently — they cross-reference each other, and #11 carried scrut changes that conflicted with contracts now on main.

The split

Three documents, three jobs, with content living in exactly one of them:

Doc Job Reader's question
README.md reference and entry "What is this, how do I install it, what's the exact flag?"
GUIDE.md explanation and internals "How does this work, and why is it built this way?"
USE-CASES.md situation-first recipes "I have this problem — show me a config for it."

The model is curl's: an exhaustive precise reference plus a separate book-length explanation. README went from 1003 lines of everything to 830 lines of reference, quickstart and troubleshooting; the deep-dive prose it duplicated now lives once, in the guide.

Corrections, not just prose

Fact-checking every technical claim against the current source turned up more than expected.

The README quickstart was not runnable. It named a repository that does not exist, printed phora list output for a file nothing in the config selected, and pointed a first-time reader's very first sync at their real ~/.config. It is now a disposable directory and a local ./out target, and every console block in the document is captured from an actual run.

phora.example.toml — the config users copy from — could not sync. Its release URL 404s because fzf tags are v-prefixed, its digest was a placeholder that fails the verify-before-extract check, and its pinned rev was a seven-character abbreviation, which Commit::from_str rejects outright. A source path of ~/dev/... also fails, because tilde expansion applies to target paths only.

Four USE-CASES snippets were hard config errors — bindings moved to a keyed [targets.<t>.sources] table, and binding-level root/include/exclude/map/as are rejected at parse time. Two of its "phora does not do this" claims were false: templating and hooks both exist.

The guide's internals had eight false claims. The swap is one atomic rename, not a per-file reflink — reflink is only the cross-device fallback. Projection no longer stages anything. A url source cannot be handed a branch or tag. ExportPolicy has no allow_submodules. The resolution pool's cap has a floor of 50 threads. where --source matches the binding identity, not the underlying source. The module map named files that no longer exist.

Also removed: claims that verification "keeps agents honest" or "proves conformance". It re-hashes deployed bytes against the registry, which cannot prove an agent read a skill file or that a linter reads a config phora deployed.

Tests

The four scrut suites main was missing are added, and the three the abandoned branch rewrote are merged against the contracts main pins — including whole-run abort atomicity, which the docs branch predated. Every suite is hermetic: git remotes redirect through insteadOf onto local fixture repos, and the release tarball is built in-suite and served over loopback. Verified under a sandbox denying all non-loopback network. A transient clone failure on the macOS runner had already demonstrated the alternative. 286 testcases across 13 documents, all green.

tests/doc_invariants.rs now extracts configs written by a heredoc inside a fence, not just bare fences. Moving the quickstart into a heredoc is what made it copy-pasteable, and it silently took the most important config in the documentation out from under the guard. Verified by corrupting the quickstart and confirming the test fails.

Verification

  • printf '%s\n' tests/scrut/*.md | xargs -P0 -n1 scrut test — 282 passed, 0 failed
  • cargo test --release --test doc_invariants — 9 passed
  • cargo fmt --check and cargo clippy --all-targets -- -D warnings — clean
  • The quickstart executed end to end in an isolated environment; its documented output is that run's output
  • Every cross-document link and anchor resolves within the branch

Product defects found, deliberately not fixed here

Three, all reproduced, none belonging in a docs PR:

  1. rebuild-registry corrupts linked artifacts. src/sync/resolve.rs:178 records the checkout's real sha for any link source with a HEAD, so the commit == "link" gate at src/sync/rebuild.rs:234 never fires and rebuild_linked is unreachable. The artifact is re-recorded as a hashed copy with linked = false, then reads modified permanently and sync refuses to redeploy it without --force.
  2. phora where --source <s> matches the binding identity. ArtifactRecord.source is written at two sites and never read anywhere.
  3. phora list reports (nothing deployed) on a transitive import target whose files are on disk.

Two smaller ones: the two-bindings-collide diagnostic prints the contested destination twice and never names the colliding identities, and [hosts] auth is parsed but never consumed.

GUIDE.md and USE-CASES.md plus the four scrut suites main lacks, taken
verbatim from docs/guide and docs/use-cases as the reconciliation base.
Updates the four seeded suites to the output the CLI actually produces:
preview names each target's deploy path, sync reports a version
transition, and the collision diagnostic names destinations relative to
the target root.

Merges the abandoned docs branch's walkthroughs into the suites main
already pins — the on_change change-set environment, check-match
verdicts, the lock's resolved-vs-commit distinction, the link-mode edit
loop, the composition story, and per-project [paths] roots. The
docs-branch fixtures cloned from github.com at runtime; every scenario
is re-expressed against the hermetic local fixtures instead.

282 testcases across 13 documents, all green.
Getting started now opens with a runnable phora.toml and a real sync
rather than prose. The Usage wall of commented bash becomes a command
reference organized so a flag can be found by scanning, and a
Troubleshooting section maps the common questions to the command that
answers them.

The deep-dive prose — bindings, take, collapse, source kinds, link mode,
transitive dependencies, templating — moves to GUIDE.md; README keeps the
complete key reference for each.

Adds flags and keys that were never documented: sync --fast-forward and
-j/--jobs, list --plan and --orphans, target rm --force, target add
--local, [hooks] pre_sync and when, pre_deploy with its on-fail policy,
the shell-free cmd argv form, and the PHORA_TARGETS and PHORA_TARGET_PATH
hook variables. Corrects the target list description, the preview sample
output, and a migration note for a [worktree] table that no longer parses.

1003 to 777 lines.
Four snippets were hard config errors: bindings moved to a keyed
[targets.<t>.sources] table, and binding-level root/include/exclude/map/as
are rejected at parse time. Each is rewritten onto source-owned scope plus
binding take, and executed.

Two "phora does not do this" claims were false. Templating exists — .tmpl
renders through minijinja against [vars] with the suffix stripped on
deploy, strictly, so an undefined variable fails the artifact rather than
rendering empty. Hooks exist, including the fc-cache example the old text
offered as something you must do yourself.

Also corrects a 404 release URL and its placeholder digest, the claim that
artifacts are top-level directories under root, the suggestion to reach for
check-match when preview and explain are the tools that answer the
question, and the description of add --symlink. A source path does not
expand a tilde; the link recipe now says so.

Adds the honest limits the document lacked: phora delivers files but does
not merge them, it has no idea what a skill is, and allow_symlinks defaults
off so a committed symlink fails the first sync until opted in.

Corrects the link-mode note in phora.local.example.toml, which claimed
deploy = "link" in the committed config is a config error. It is honored;
phora warns when the path is absolute, and only a remote URL is an error.
The claim that rebuild-registry skips linked artifacts is dropped — it does
not.
The url source pointed at a release tag that does not exist — fzf tags
are v-prefixed — and carried a placeholder digest that fails the
verify-before-extract check. Both are now real: the URL resolves and the
sha256 is the asset's actual checksum.

The pinned rev was a 7-character abbreviation. Commit ids parse only at
40 or 64 hex chars, so the shipped example could not sync; it is now a
full id, and the comment says abbreviations are not accepted.

A source path does not expand a tilde — only target paths do — so
`~/dev/scratch-configs` failed as a literal directory name. Omitting
branch, tag and rev follows the repo's own default branch rather than
`main`.
Eight claims were false. The swap is one atomic rename of the staged tree,
not a per-file reflink — reflink is only the cross-device fallback.
Projection no longer stages anything; it is a pure calculation, and sync
owns staging and the digest. A url source cannot be handed a branch or tag
at all, and there is no branch = "main" default. ExportPolicy has no
allow_submodules. The resolution pool's cap has a floor of fifty threads
and dispatches per mirror. where --source matches the binding identity, not
the underlying source. The module map named files that no longer exist.

Adds what the guide never covered: the pre_sync and pre_deploy gates and
whole-run abort atomicity, [paths] cache and state overrides, orphan
visibility and physical prune, --frozen's lockless fallback, the network
filesystem lock advisory, --fast-forward for moved pins, target rm refusing
while artifacts are live, the cross-device staging warning, mirror
self-heal, and the template fuel bound.

Source paths are absolute throughout — tilde expansion is a target-path
feature — and a rev must be a full commit id.
The quickstart was not runnable: it named a repository that does not
exist, showed output for a file nothing in the config selected, and
pointed a first-time reader's first sync at their real ~/.config. It is
now a disposable directory, a real public source, and a local ./out
target, with every console block captured from an actual run.

README gains a heading per command so a flag can be found by scanning,
parallel phrasing across the flag tables, and troubleshooting as
questions rather than a paragraph of fragments. The Concepts section
claimed in prose that a binding owns take and collapse while its table
credited the target; a single glossary replaces both. Implementation
vocabulary is gone from user-facing text, and rev is documented as
requiring a full commit id.

USE-CASES moves every decisive limitation ahead of the recipe it governs
rather than after the reader has invested in it, and drops two
overclaims: verification checks deployed bytes against the registry, so
it cannot prove an agent read a skill file or that a linter reads a
config phora deployed.
The guide taught well through Collapse, then began restating README's
flag and key tables. Editing bindings, the hook value schema, the
cache/state location rules, and the closing file inventory are now the
design reason plus a link to the reference that owns the detail. When
something looks wrong becomes a walk back through the pipeline —
check-match, explain, preview, list, verify — rather than a second
troubleshooting section.

Fixes an internal contradiction: one chapter said a URL source's offer is
shaped by include and exclude while another said there is no offer to
subset.

Terms are now defined where they are first used rather than assumed —
registry, mirror, capture digest, drift gate, manifest, seals — and
italics are back to marking a term on introduction or a real contrast.

Deletions kept every fact that lived only in the passage being cut: that
prune removes an orphaned link by deleting the symlink alone, that
where --source matches the binding identity, that each project root has
its own lock and registry partition, that empty hook tables serialize
away, and that confinement protects the projects subtree rather than the
whole state root.

16,675 to 14,709 words.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refreshes and restructures phora’s documentation to match the current “source → projection → sync” implementation, splitting content into README (reference), GUIDE (internals/explanation), and USE-CASES (recipe-style configs). It also expands the documentation test harness so embedded configs (including heredoc-written configs) are validated, and adds/updates scrut suites to keep documented workflows executable.

Changes:

  • Rewrites and reorganizes README.md, and adds new GUIDE.md and USE-CASES.md with updated, implementation-accurate behavior and examples.
  • Fixes and modernizes example configs (phora.example.toml, phora.local.example.toml) to reflect current parsing/validation rules.
  • Extends doc validation (tests/doc_invariants.rs) to extract version = 1 configs written via heredocs inside fenced blocks, and adds/updates scrut suites accordingly.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
USE-CASES.md New situation-first recipes with working configs and documented limits.
GUIDE.md New long-form walkthrough and internals guide aligned to current behavior.
README.md Consolidated reference + runnable quickstart + updated command/config docs.
phora.example.toml Corrected example config (refs/digests/paths) to be runnable/valid.
phora.local.example.toml Updated overlay guidance, especially around link mode semantics.
tests/doc_invariants.rs Extracts heredoc-written configs from fenced blocks for validation; adds a targeted regression test.
tests/scrut/_setup.sh Adds helper to assert hook change-set env behavior in scrut.
tests/scrut/hooks.md Expands hook documentation and adds change-set env assertions.
tests/scrut/showcase.md Adds additional narrated examples (check-match, lock inspection, link-mode loop details).
tests/scrut/transitive.md Reworks transitive dependency narrative and adds coverage for stripped hooks + pinned paths behavior.
tests/scrut/versions.md New scrut suite demonstrating per-binding version overrides and promotion workflow.
tests/scrut/mapped.md New scrut suite demonstrating take renames for fanning out one file under multiple names.
tests/scrut/release-assets.md New scrut suite for URL sources with digest verification and update behavior.
tests/scrut/drift.md New scrut suite for drift detection, force restore, and eject/uneject flow.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/scrut/versions.md Outdated
Comment on lines +8 to +10
State is hermetic — the first command points `HOME` and the XDG cache/state
roots at scrut's per-document tempdir; the clone of github.com/junegunn/fzf is
real. Both refs are release tags, so every hash below is stable.
Comment thread tests/scrut/mapped.md Outdated
Comment on lines +8 to +12
drives the real [github/spec-kit](https://github.com/github/spec-kit) repository,
which carries an `AGENTS.md` at its root, pinned to tag `v0.9.5`.

State is hermetic — the first command points `HOME` and the XDG cache/state
roots at scrut's per-document tempdir; the clone is real. Both the commit and
Comment thread tests/scrut/drift.md Outdated
Comment on lines +10 to +12
State is hermetic — the first command points `HOME` and the XDG cache/state
roots at scrut's per-document tempdir, so nothing touches your real config; the
clone is real, pinned to one commit.
Comment thread tests/scrut/release-assets.md Outdated
Comment on lines +8 to +11
State is hermetic — the first command points `HOME` and the XDG cache/state
roots at scrut's per-document tempdir; the download is real. Release assets are
uploaded bytes, not generated-on-demand tarballs, so the digest and the imported
commit are stable for as long as the asset exists.
Prose only; no factual claim, config fence, console block, or heading
changed. Breaks up same-shape sentence runs, thins the dominant causal
tails, cuts announcing intros and restating closers, drops the surviving
"worth knowing" padding, and varies the recipe openings that all shared
one construction.
drift, mapped, and versions cloned public github repositories at run
time and release-assets downloaded a release tarball; a transient IO
error on the macOS runner failed CI. Each now drives a local fixture —
git remotes redirected through insteadOf, the tarball built in-suite and
served over loopback — with commit ids and digests asserted verbatim
again, since the fixtures pin their dates.

Verified under a sandbox denying all non-loopback network: all four pass
where the old drift.md fails 20/25.

Also corrects the fzf example URL in GUIDE.md, which kept the unprefixed
tag that 404s, and replaces its placeholder digest with the asset's real
checksum.
Three-sentence runs on one syntactic frame — the document maps in both
README and GUIDE, the command walkthrough in Getting started — now merge
or break the frame. Passive stretches where nothing acted for a full
paragraph give phora the verbs back. The About paragraph swaps category
description for the tool's own nouns: phora.lock, the registry, a blake3
digest per deployed file.
"This README is the reference", "This is the long-form companion", "the
closing catalogue only sketches smaller fits" — a document narrating its
own role is the tell, and grounding the sentence didn't fix its skeleton.
The maps are now flat pointers that name what the other file contains:
GUIDE.md explains the machinery, USE-CASES.md has configs that were
actually run.
The plain-description opener undersold the category. The claim comes
back with both terms cashed out in the same sentence: package-manager
treatment for ordinary files, one source fanned out to many directories.
lint-test passes install_args: rust, but mise run auto-installs every
tool in mise.toml, so cold macOS runners downloaded release-plz,
git-cliff, cargo-dist, and hk that no CI step runs — and each download
was a chance to hit the runner fleet's intermittent TLS distrust of
GitHub's own certificates, which failed two runs today on two different
downloads. MISE_AUTO_INSTALL=0 holds the scope, and integration installs
only rust and scrut.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants