Description
Nine documentation statements across CLAUDE.md, README.md, docs/ and one shipped source string describe behaviour the binary does not have. They are grouped here because they share a root cause — doc surfaces were not updated when the discovery model, the health-check set, and the JSON envelopes changed — and because fixing them is one coherent editing pass.
The list is ordered by impact.
-
The schema-discovery boundary model is documented wrong, and it is the upstream cause of the most common runtime failure.
internal/rules/discovery.go:70 (WalkUp) collects .stem files upward and terminates on stem.Root (discovery.go:99) or the filesystem root. It never consults .git. When the walk reaches the filesystem root without a root: true marker, the preflight turns that into a hard error on every governed command.
Four doc sites still teach the old model:
CLAUDE.md:33 — ".stem file loading, walk-up discovery (target → .git root)". Factually wrong.
CLAUDE.md contains zero occurrences of root: true, "root marker", or "boundary" (grep -n -i "root: true\|root marker\|boundary\|\.git root" CLAUDE.md returns nothing). The single concept a new project must know is absent from the file contributors are told to read first.
README.md:114 — "Without an explicit marker, discovery continues to the filesystem root." It does not; the preflight makes it a hard error. README.md:109 frames the marker as one of two equally valid stopping conditions.
docs/validate.md:18 — "The walk continues to the filesystem root if no marker is found." Same false claim.
docs/explain.md:75 — "up to the root: true marker (or the filesystem root)" — stale parenthetical.
internal/infer/schema_coverage.go:48 also ships the old model inside a user-facing inference message: "Directory contains %d markdown file(s) but no .stem schema (checked walk-up to .git root)". (Note: this branch requires WalkUp to fail for a scanned directory and was not reachable in testing — but the stale wording is in the shipped string either way, and the branch being unreachable is itself worth a look.)
Consequence: users place a .stem at the repo root, expect it to work because the docs say discovery reaches the filesystem root, and instead hit a hard error on every command at once. Commit fb79448 began this cleanup; these sites were missed.
Two .git walks do remain in the tree, and neither bounds discovery: internal/rules/discovery.go:174 (projectBoundaryHint, explicitly documented as advisory and used only by ProposeRootDirectory) and internal/migrate/source.go:58 (findGitRoot, used for git show). The docs describe the boundary as if it were projectBoundaryHint's job; it is not.
-
CLAUDE.md:56 "All JSON output carries "version": 1 for contract stability" is false. tree emits version: 2 (cmd/rootline/tree.go:96). A consumer written against this sentence and asserting version == 1 breaks on tree. The identical claim was already corrected in README.md by commit 08ba2f8; CLAUDE.md was not updated in the same pass. docs/tree.md:46 is correct.
-
CLAUDE.md:31 attributes working behaviour to an operation that does not exist. The file states that update_stem "now grows the .stem by creating field nodes for newly-observed fields (field_type/enum_values/required_field/constant_field), closing the analyze --incremental → schema apply loop". grep -rn 'update_stem' --include='*.go' cmd internal finds only a struct comment (cmd/rootline/schema.go:25) and a test fixture. No producer sets it: generateSchemaProposals emits only create_stem. No consumer handles it: cmd/rootline/schema.go:269 branches on create_stem alone, and anything else falls through with no error, no warning, and no skipped entry. The field-growth behaviour is real but lives in internal/infer.ApplySchemaInferences (internal/infer/apply.go:52-97), keyed on inference types. A reader implementing against CLAUDE.md emits update_stem proposals and watches them silently vanish.
-
docs/validate.md:35-46 documents three health checks that do not exist and omits four that do. The doc lists version-deprecated (line 43), domain-type-compat (line 44) and domain-missing-attrs (line 45). None appears in any Go file, test, or roadmap record. The real set from internal/rules/stemhealth.go is twelve: stem-files-exist (:56), yaml-valid (:69/:76), scope-match (:112), type-consistency (:140), enum-values (:161), rule-field-exists (:191), field-override (:221), aggregated-required (:244), aggregate-formula-coverage (:302), monotonic-violations (:355), unknown-check-keys (:377), nested-root-marker (:423). The doc omits four of them.
docs/validate.md:42 additionally inverts the polarity of a real check: it says "aggregated-required — required fields have aggregate expressions", but stemhealth.go:244-252 warns precisely when a field is both required and aggregated ("required is auto-skipped on index files — consider removing required or using excludes"). A reader takes a warning as a requirement.
-
CLAUDE.md:33 says 11 stem-health checks; there are 12. The enumeration omits nested-root-marker (internal/rules/stemhealth.go:423), which is live and user-visible in validate --all output.
-
docs/describe.md documents two result keys the command never emits. docs/describe.md:38 shows a "levels" object, :62 shows a "structural" object, and :85 lists structural in the Sections table. internal/rules/describe.go (DescribeResult) has neither field. A consumer reading .levels or .structural always gets null. The prose at docs/describe.md:72-74 correctly describes layers and provenance, but the JSON example above it omits both — so the example is wrong in both directions.
-
docs/migrate.md:53 states a kind no code path produces, and omits a real key. The doc shows "kind": "rootline/migrate". The three real kinds are rootline/migrate-diff (internal/migrate/diff.go), rootline/migrate-rename (internal/migrate/rename.go) and rootline/migrate-batch (cmd/rootline/migrate.go). A consumer dispatching on kind against the doc never matches. The documented example also omits stem_path, which the real payload does carry.
-
docs/stats.md contradicts itself in the same file. Line 6: "rootline stats shows aggregate counts by estado and tipo frontmatter fields." Line 27: "stats is field-agnostic: it reports the total record count and makes no assumptions about which frontmatter fields exist." Line 52 confirms the two maps "are reserved in the output contract but are currently always empty". The binary settles it in favour of lines 27 and 52; line 6 is the stale one.
-
The JSON contract is not discoverable from the CLI, and the shapes are mutually inconsistent. CLAUDE.md sells the versioned contract as a feature, but no --help output for query, graph, validate, tree, or stats names a single JSON key (the only near-match is --limit's "limit number of results", which is prose). Meanwhile the top-level shapes disagree with each other:
| command |
row collection |
count/summary |
notable keys |
query |
rows |
meta.count |
— |
validate --all |
results |
summary |
drift_warnings |
graph |
nodes / edges |
— |
edge fields are source/target |
stats |
— |
total |
by_lifecycle_state, by_record_type |
tree |
root (nested) |
root.total |
— |
The predictable result is consumers guessing: .to/.from on graph edges (real: source/target), .files[] on validate (real: results), and treating query output as a top-level array (real: an object with rows). A contract that cannot be enumerated from the CLI is a contract in name only.
Steps to Reproduce
Build a self-contained fixture. Requires jq and a rootline on PATH.
rm -rf /tmp/rootline-repro && mkdir -p /tmp/rootline-repro/docs
cd /tmp/rootline-repro && git init -q .
cat > docs/.stem <<'EOF'
version: 2
scope:
match: "*.md"
schema:
estado:
type: enum
values: [Pending, Done]
EOF
printf -- "---\nestado: Pending\n---\n\n# a\n" > docs/a.md
printf -- "---\nestado: Done\n---\n\n# b\n" > docs/b.md
(1) Boundary model — a .stem inside a git repo, with no root: true:
$ rootline validate docs/a.md -o json
Error: Schema discovery reached the filesystem root without finding a declared boundary.
No .stem in this project declares where the project starts.
Fix: add this line to /tmp/rootline-repro/docs/.stem
root: true
Discovery then stops there and never reads .stem files above it.
Run rootline in a terminal to be prompted and have this applied for you.
The git init is deliberate: a .git directory sits directly above docs/, and discovery walks straight past it to the filesystem root. README.md:114 and docs/validate.md:18 both state this cannot happen. Unblock the fixture for the rest of the steps:
printf 'root: true\n' >> docs/.stem
(2) tree emits version: 2:
$ rootline tree docs -o json | jq -c '{version,kind}'
{"version":2,"kind":"rootline/tree"}
(3) update_stem exists nowhere (run from a source checkout):
$ grep -rn 'update_stem' --include='*.go' cmd internal
cmd/rootline/schema.go:25: Operation string `json:"operation"` // "create_stem", "update_stem", etc.
cmd/rootline/render_tables_test.go:70: {ID: "p2", Operation: "update_stem", ...}
A struct comment and a test fixture. No producer, no consumer.
(4 and 5) Health-check names — the twelve real ones, from a source checkout:
$ grep -n 'Name:\s*"' internal/rules/stemhealth.go
56: Name: "stem-files-exist",
69: Name: "yaml-valid",
76: Name: "yaml-valid",
112: Name: "scope-match",
140: Name: "type-consistency",
161: Name: "enum-values",
191: Name: "rule-field-exists",
221: Name: "field-override",
244: Name: "aggregated-required",
302: Name: "aggregate-formula-coverage",
355: Name: "monotonic-violations",
377: Name: "unknown-check-keys",
423: Name: "nested-root-marker",
Twelve distinct names (yaml-valid appears at two call sites). version-deprecated, domain-type-compat, domain-missing-attrs are absent from the whole tree; CLAUDE.md:33 lists eleven and omits nested-root-marker.
(6) describe result keys:
$ rootline describe docs -o json | jq -c 'keys'
["aggregate","applies","derive","kind","layers","links","path","provenance","schema","scope","validate","version"]
No levels, no structural.
(7) migrate kind:
$ rootline migrate docs --dry-run -o json | jq -c '{version,kind,keys:(keys)}'
{"version":1,"kind":"rootline/migrate-diff","keys":["breaking_count","changes","kind","stem_path","total_count","version"]}
Kind is rootline/migrate-diff, not rootline/migrate; stem_path is present and undocumented.
(8) stats is field-agnostic:
$ rootline stats docs -o json | jq -c .
{"version":1,"kind":"rootline/stats","by_lifecycle_state":{},"by_record_type":{},"total":2}
(9) Contract discoverability:
$ for c in query graph validate tree stats; do
echo -n "$c: "; rootline $c --help 2>&1 | grep -ci 'rows\|edges\|"kind"'
done
query: 0
graph: 0
validate: 0
tree: 0
stats: 0
$ rootline graph docs -o json | jq -c '.edges[0]'
{"source":"a.md","target":"b.md","type":"reference","line":3}
Expected Behavior
CLAUDE.md, README.md, docs/validate.md and docs/explain.md describe discovery as terminating at a root: true marker, and state plainly that reaching the filesystem root without one is a hard error. CLAUDE.md names root: true at least once.
CLAUDE.md states that each payload carries its own version (the wording README.md already uses after 08ba2f8), not a blanket version: 1.
CLAUDE.md describes the analyze → schema apply growth path by the mechanism that implements it, and does not name update_stem.
docs/validate.md lists exactly the twelve health checks the binary implements, with correct polarity for aggregated-required.
CLAUDE.md says 12 checks and enumerates nested-root-marker.
docs/describe.md's JSON example matches DescribeResult exactly — including layers and provenance, excluding levels and structural.
docs/migrate.md documents all three real kind values and the stem_path key.
docs/stats.md states the field-agnostic behaviour once, consistently.
- The JSON keys of each command are enumerable from the CLI itself.
Actual Behavior
Every statement above is contradicted by the shipped binary, as captured in Steps to Reproduce. Item 1 is the load-bearing one: it is the documented model for a mechanism that every governed command depends on, and the doc's version of it cannot be made to work.
Environment
- Rootline version:
dev (source build at b3c3d52). The doc text and the source lines cited are unchanged since 4.0.16, where the drift was first established.
- OS: macOS (darwin)
- Go version: 1.26+
Proposed fix
Documentation-only for items 1–8; item 9 needs a design decision.
- Boundary model — rewrite
CLAUDE.md:33 (drop "target → .git root", state the root: true termination and the hard-error case), README.md:109 and README.md:114, docs/validate.md:18, docs/explain.md:75. Fix the shipped string at internal/infer/schema_coverage.go:48. While there, check whether that missing_schema branch is reachable at all — with the preflight in place, WalkUp returning empty for a scanned subdirectory looks unreachable, and if so the branch should be removed rather than reworded.
- Version claim —
CLAUDE.md:56, adopt the README.md wording from 08ba2f8.
update_stem — rewrite CLAUDE.md:31 to describe internal/infer.ApplySchemaInferences. Separately decide whether cmd/rootline/schema.go:269 should reject unknown operations instead of dropping them; today an unhandled operation is neither applied nor reported.
- Health checks — regenerate the
docs/validate.md:35-46 list from internal/rules/stemhealth.go, correct the aggregated-required description, update the count in CLAUDE.md:33 to 12 and add nested-root-marker. A test asserting that the documented names match the Name: literals in stemhealth.go would stop this recurring.
describe — regenerate the docs/describe.md JSON example from rules.DescribeResult.
migrate — document all three kinds and stem_path in docs/migrate.md.
stats — delete or rewrite docs/stats.md:6.
- Contract discoverability — needs a design decision first. Options are a
rootline schema --contract style subcommand that dumps the envelope shape per command, per-command --help sections naming the top-level keys, or generated JSON Schema files checked into docs/. Normalizing the envelopes themselves (rows vs results, meta vs summary) is a breaking change and belongs in its own discussion, not in this issue.
Chained PR series
One work unit per PR.
- Boundary model across all doc surfaces —
CLAUDE.md, README.md, docs/validate.md, docs/explain.md. Single coherent edit; no code.
internal/infer/schema_coverage.go:48 message — code change, and the reachability question above. Separated from PR 1 because it may turn into a deletion rather than a reword.
CLAUDE.md version and update_stem claims — items 2 and 3. Both are single-paragraph rewrites in the same file.
- Health-check documentation —
docs/validate.md:35-46, CLAUDE.md:33, plus the drift test that keeps the list honest.
- Per-command reference docs —
docs/describe.md, docs/migrate.md, docs/stats.md. Mechanical, verified against captured output.
- Contract discoverability — blocked on the design decision. Should not be bundled with 1–5.
PRs 1–5 are independent of each other and can land in any order. PR 6 should not start until the mechanism is agreed.
Description
Nine documentation statements across
CLAUDE.md,README.md,docs/and one shipped source string describe behaviour the binary does not have. They are grouped here because they share a root cause — doc surfaces were not updated when the discovery model, the health-check set, and the JSON envelopes changed — and because fixing them is one coherent editing pass.The list is ordered by impact.
The schema-discovery boundary model is documented wrong, and it is the upstream cause of the most common runtime failure.
internal/rules/discovery.go:70(WalkUp) collects.stemfiles upward and terminates onstem.Root(discovery.go:99) or the filesystem root. It never consults.git. When the walk reaches the filesystem root without aroot: truemarker, the preflight turns that into a hard error on every governed command.Four doc sites still teach the old model:
CLAUDE.md:33— ".stemfile loading, walk-up discovery (target →.gitroot)". Factually wrong.CLAUDE.mdcontains zero occurrences ofroot: true, "root marker", or "boundary" (grep -n -i "root: true\|root marker\|boundary\|\.git root" CLAUDE.mdreturns nothing). The single concept a new project must know is absent from the file contributors are told to read first.README.md:114— "Without an explicit marker, discovery continues to the filesystem root." It does not; the preflight makes it a hard error.README.md:109frames the marker as one of two equally valid stopping conditions.docs/validate.md:18— "The walk continues to the filesystem root if no marker is found." Same false claim.docs/explain.md:75— "up to theroot: truemarker (or the filesystem root)" — stale parenthetical.internal/infer/schema_coverage.go:48also ships the old model inside a user-facing inference message:"Directory contains %d markdown file(s) but no .stem schema (checked walk-up to .git root)". (Note: this branch requiresWalkUpto fail for a scanned directory and was not reachable in testing — but the stale wording is in the shipped string either way, and the branch being unreachable is itself worth a look.)Consequence: users place a
.stemat the repo root, expect it to work because the docs say discovery reaches the filesystem root, and instead hit a hard error on every command at once. Commitfb79448began this cleanup; these sites were missed.Two
.gitwalks do remain in the tree, and neither bounds discovery:internal/rules/discovery.go:174(projectBoundaryHint, explicitly documented as advisory and used only byProposeRootDirectory) andinternal/migrate/source.go:58(findGitRoot, used forgit show). The docs describe the boundary as if it wereprojectBoundaryHint's job; it is not.CLAUDE.md:56"All JSON output carries"version": 1for contract stability" is false.treeemitsversion: 2(cmd/rootline/tree.go:96). A consumer written against this sentence and assertingversion == 1breaks ontree. The identical claim was already corrected inREADME.mdby commit08ba2f8;CLAUDE.mdwas not updated in the same pass.docs/tree.md:46is correct.CLAUDE.md:31attributes working behaviour to an operation that does not exist. The file states thatupdate_stem"now grows the.stemby creating field nodes for newly-observed fields (field_type/enum_values/required_field/constant_field), closing theanalyze --incremental→schema applyloop".grep -rn 'update_stem' --include='*.go' cmd internalfinds only a struct comment (cmd/rootline/schema.go:25) and a test fixture. No producer sets it:generateSchemaProposalsemits onlycreate_stem. No consumer handles it:cmd/rootline/schema.go:269branches oncreate_stemalone, and anything else falls through with no error, no warning, and noskippedentry. The field-growth behaviour is real but lives ininternal/infer.ApplySchemaInferences(internal/infer/apply.go:52-97), keyed on inference types. A reader implementing againstCLAUDE.mdemitsupdate_stemproposals and watches them silently vanish.docs/validate.md:35-46documents three health checks that do not exist and omits four that do. The doc listsversion-deprecated(line 43),domain-type-compat(line 44) anddomain-missing-attrs(line 45). None appears in any Go file, test, or roadmap record. The real set frominternal/rules/stemhealth.gois twelve:stem-files-exist(:56),yaml-valid(:69/:76),scope-match(:112),type-consistency(:140),enum-values(:161),rule-field-exists(:191),field-override(:221),aggregated-required(:244),aggregate-formula-coverage(:302),monotonic-violations(:355),unknown-check-keys(:377),nested-root-marker(:423). The doc omits four of them.docs/validate.md:42additionally inverts the polarity of a real check: it says "aggregated-required— required fields have aggregate expressions", butstemhealth.go:244-252warns precisely when a field is both required and aggregated ("required is auto-skipped on index files — consider removing required or using excludes"). A reader takes a warning as a requirement.CLAUDE.md:33says 11 stem-health checks; there are 12. The enumeration omitsnested-root-marker(internal/rules/stemhealth.go:423), which is live and user-visible invalidate --alloutput.docs/describe.mddocuments two result keys the command never emits.docs/describe.md:38shows a"levels"object,:62shows a"structural"object, and:85listsstructuralin the Sections table.internal/rules/describe.go(DescribeResult) has neither field. A consumer reading.levelsor.structuralalways getsnull. The prose atdocs/describe.md:72-74correctly describeslayersandprovenance, but the JSON example above it omits both — so the example is wrong in both directions.docs/migrate.md:53states akindno code path produces, and omits a real key. The doc shows"kind": "rootline/migrate". The three real kinds arerootline/migrate-diff(internal/migrate/diff.go),rootline/migrate-rename(internal/migrate/rename.go) androotline/migrate-batch(cmd/rootline/migrate.go). A consumer dispatching onkindagainst the doc never matches. The documented example also omitsstem_path, which the real payload does carry.docs/stats.mdcontradicts itself in the same file. Line 6: "rootline statsshows aggregate counts byestadoandtipofrontmatter fields." Line 27: "statsis field-agnostic: it reports the total record count and makes no assumptions about which frontmatter fields exist." Line 52 confirms the two maps "are reserved in the output contract but are currently always empty". The binary settles it in favour of lines 27 and 52; line 6 is the stale one.The JSON contract is not discoverable from the CLI, and the shapes are mutually inconsistent.
CLAUDE.mdsells the versioned contract as a feature, but no--helpoutput forquery,graph,validate,tree, orstatsnames a single JSON key (the only near-match is--limit's "limit number of results", which is prose). Meanwhile the top-level shapes disagree with each other:queryrowsmeta.countvalidate --allresultssummarydrift_warningsgraphnodes/edgessource/targetstatstotalby_lifecycle_state,by_record_typetreeroot(nested)root.totalThe predictable result is consumers guessing:
.to/.fromon graph edges (real:source/target),.files[]on validate (real:results), and treatingqueryoutput as a top-level array (real: an object withrows). A contract that cannot be enumerated from the CLI is a contract in name only.Steps to Reproduce
Build a self-contained fixture. Requires
jqand arootlineonPATH.(1) Boundary model — a
.steminside a git repo, with noroot: true:The
git initis deliberate: a.gitdirectory sits directly abovedocs/, and discovery walks straight past it to the filesystem root.README.md:114anddocs/validate.md:18both state this cannot happen. Unblock the fixture for the rest of the steps:(2)
treeemitsversion: 2:(3)
update_stemexists nowhere (run from a source checkout):A struct comment and a test fixture. No producer, no consumer.
(4 and 5) Health-check names — the twelve real ones, from a source checkout:
Twelve distinct names (
yaml-validappears at two call sites).version-deprecated,domain-type-compat,domain-missing-attrsare absent from the whole tree;CLAUDE.md:33lists eleven and omitsnested-root-marker.(6)
describeresult keys:No
levels, nostructural.(7)
migratekind:Kind is
rootline/migrate-diff, notrootline/migrate;stem_pathis present and undocumented.(8)
statsis field-agnostic:(9) Contract discoverability:
Expected Behavior
CLAUDE.md,README.md,docs/validate.mdanddocs/explain.mddescribe discovery as terminating at aroot: truemarker, and state plainly that reaching the filesystem root without one is a hard error.CLAUDE.mdnamesroot: trueat least once.CLAUDE.mdstates that each payload carries its ownversion(the wordingREADME.mdalready uses after08ba2f8), not a blanketversion: 1.CLAUDE.mddescribes the analyze →schema applygrowth path by the mechanism that implements it, and does not nameupdate_stem.docs/validate.mdlists exactly the twelve health checks the binary implements, with correct polarity foraggregated-required.CLAUDE.mdsays 12 checks and enumeratesnested-root-marker.docs/describe.md's JSON example matchesDescribeResultexactly — includinglayersandprovenance, excludinglevelsandstructural.docs/migrate.mddocuments all three realkindvalues and thestem_pathkey.docs/stats.mdstates the field-agnostic behaviour once, consistently.Actual Behavior
Every statement above is contradicted by the shipped binary, as captured in Steps to Reproduce. Item 1 is the load-bearing one: it is the documented model for a mechanism that every governed command depends on, and the doc's version of it cannot be made to work.
Environment
dev(source build atb3c3d52). The doc text and the source lines cited are unchanged since 4.0.16, where the drift was first established.Proposed fix
Documentation-only for items 1–8; item 9 needs a design decision.
CLAUDE.md:33(drop "target →.gitroot", state theroot: truetermination and the hard-error case),README.md:109andREADME.md:114,docs/validate.md:18,docs/explain.md:75. Fix the shipped string atinternal/infer/schema_coverage.go:48. While there, check whether thatmissing_schemabranch is reachable at all — with the preflight in place,WalkUpreturning empty for a scanned subdirectory looks unreachable, and if so the branch should be removed rather than reworded.CLAUDE.md:56, adopt theREADME.mdwording from08ba2f8.update_stem— rewriteCLAUDE.md:31to describeinternal/infer.ApplySchemaInferences. Separately decide whethercmd/rootline/schema.go:269should reject unknown operations instead of dropping them; today an unhandled operation is neither applied nor reported.docs/validate.md:35-46list frominternal/rules/stemhealth.go, correct theaggregated-requireddescription, update the count inCLAUDE.md:33to 12 and addnested-root-marker. A test asserting that the documented names match theName:literals instemhealth.gowould stop this recurring.describe— regenerate thedocs/describe.mdJSON example fromrules.DescribeResult.migrate— document all three kinds andstem_pathindocs/migrate.md.stats— delete or rewritedocs/stats.md:6.rootline schema --contractstyle subcommand that dumps the envelope shape per command, per-command--helpsections naming the top-level keys, or generated JSON Schema files checked intodocs/. Normalizing the envelopes themselves (rowsvsresults,metavssummary) is a breaking change and belongs in its own discussion, not in this issue.Chained PR series
One work unit per PR.
CLAUDE.md,README.md,docs/validate.md,docs/explain.md. Single coherent edit; no code.internal/infer/schema_coverage.go:48message — code change, and the reachability question above. Separated from PR 1 because it may turn into a deletion rather than a reword.CLAUDE.mdversion andupdate_stemclaims — items 2 and 3. Both are single-paragraph rewrites in the same file.docs/validate.md:35-46,CLAUDE.md:33, plus the drift test that keeps the list honest.docs/describe.md,docs/migrate.md,docs/stats.md. Mechanical, verified against captured output.PRs 1–5 are independent of each other and can land in any order. PR 6 should not start until the mechanism is agreed.