docs: consolidate README, GUIDE and USE-CASES against the current implementation - #67
Open
srnnkls wants to merge 15 commits into
Open
docs: consolidate README, GUIDE and USE-CASES against the current implementation#67srnnkls wants to merge 15 commits into
srnnkls wants to merge 15 commits into
Conversation
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.
This was referenced Aug 11, 2026
There was a problem hiding this comment.
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 newGUIDE.mdandUSE-CASES.mdwith 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 extractversion = 1configs 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 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 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 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 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
README.mdGUIDE.mdUSE-CASES.mdThe 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 listoutput 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./outtarget, 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 arev-prefixed, its digest was a placeholder that fails the verify-before-extract check, and its pinnedrevwas a seven-character abbreviation, whichCommit::from_strrejects outright. A sourcepathof~/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-levelroot/include/exclude/map/asare 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.
ExportPolicyhas noallow_submodules. The resolution pool's cap has a floor of 50 threads.where --sourcematches 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.rsnow 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 failedcargo test --release --test doc_invariants— 9 passedcargo fmt --checkandcargo clippy --all-targets -- -D warnings— cleanProduct defects found, deliberately not fixed here
Three, all reproduced, none belonging in a docs PR:
rebuild-registrycorrupts linked artifacts.src/sync/resolve.rs:178records the checkout's real sha for any link source with a HEAD, so thecommit == "link"gate atsrc/sync/rebuild.rs:234never fires andrebuild_linkedis unreachable. The artifact is re-recorded as a hashed copy withlinked = false, then readsmodifiedpermanently and sync refuses to redeploy it without--force.phora where --source <s>matches the binding identity.ArtifactRecord.sourceis written at two sites and never read anywhere.phora listreports(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] authis parsed but never consumed.