You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(docs-audit): anchor a changed @docs-rule block on the expressions it states - #9394
packages/objectql/src/declared-fields.ts is 191 lines, 179 of them one doc comment that
is the canonical statement of the sparse-face guard rule, and one exported function. A
change confined to that comment derived no anchor at all, so the docs-drift run
reported "this run has no opinion" — which a reader reasonably takes as "nothing to
check" — while three pages restating the rule in prose sat unlisted.
Re-measured on current origin/main (51cd953fb, i.e. after #9230 landed the command
kind), the instance reproduces verbatim:
0 docs name something this change touched (no anchors derived) across 1 changed package(s)
since origin/main; 1 changed file(s) yielded no anchor
anchorlessChanges: ["packages/objectql/src/declared-fields.ts"]
Why the coarse fallback was measured and rejected
Triage's route input was the package-mention fallback for an anchorless file's own package.
I priced it on this specimen before building anything, and it is wrong in both
directions — the exact failure the #9192 rewrite exists to undo:
route
pages listed
of the 3 restating pages found
today
0 ("no opinion")
0 / 3
package-mention fallback
14
0 / 3
this PR (rule anchors)
7
3 / 3
14 pages name @objectstack/objectql or packages/objectql, and not one of them is automation/flows.mdx, protocol/objectui/actions.mdx or ui/actions.mdx. Those three
document the authoring face and never name the implementing package at all. So bounded
width (14, not 140) does not rescue the fallback — it is 0-for-3 on recall on the single
instance it was proposed for. The measured width is reported here because that is what
turns this from a preference into a verdict.
What the restating pages do share with the rule is the expressions the rule is written
in. That is what this PR anchors on.
What landed
A fifth anchor kind, rule, built on the precedent #9230 set for command:
Marker, not registry. A bare @docs-rule tag in a doc comment opts the block in. It
takes no argument and carries no anchor list — it says derive anchors from this block,
never which. The anchors are read off the block's own code spans, so an author cannot
write one, forget to update one, or leave one behind: the marker lives inside the prose
it describes and is deleted with it. That is the whole difference from the hand-curated
registry of canonical-rule sites the card ruled out.
Whole-block scope. The rule is stated across 179 lines of tables and prose, and a
one-line refinement to any of them can stale any page restating any part. Line-scoping
measured this specimen down to zero: the changed line's own span was has(), which is
not an expression.
Admitted as an expression, not a name.isRuleExpression requires a comparison or
logical operator and a dotted-or-called reference. Both halves are load-bearing and
each drops a measured class: the operator half drops the block's caller list
(visibleWhen, requiredWhen, readonlyWhen — admitting them took the specimen from 7
pages to 27); the reference half drops prose a stray backtick paired across two lines.
No new knob.rule joins PHRASE_ANCHOR_KINDS and then faces the existing
corpus-share guard unchanged. The shape guard is untouched for every single-token kind —
the exemption is beside it, never through it, and it is earned by construction: a span
carrying && or != cannot be the bare lowercase word the guard exists to drop.
The honest-failure property is preserved, and that is deliberate
The card is explicit that this run's shape was more honest than most — it said it had no
opinion and named the file it could not anchor, which is the only reason the defect was
filable. A fallback that quietly widened the list without saying it fell back would have
destroyed exactly that. Three disclosure paths, all verified end to end:
An untagged anchorless file is unchanged: still "no opinion", still named in anchorlessChanges, never buried under a package-wide fallback.
All three restating pages found (*), each naming the anchor that put it there.
Leg 2 — silence not traded for noise. Two sub-legs:
A change to the function body below the tagged block fires 0 rule anchors — the
rule's statement did not change, so the restating pages are not dragged in. The file
anchors on materializeDeclaredFields exactly as it always did.
A change to an untagged doc comment elsewhere (record-change-trigger.ts, the
deliberate structural mirror) still reports honestly: no anchors derived, named in anchorlessChanges, 0 docs.
A third check confirms the new disclosure fires: tagging a block with no expressions in it
produces unanchoredRuleBlocks: ["packages/triggers/.../record-change-trigger.ts"] and the
matching summary note.
Harness ablations — three, each red in the predicted direction, then restored green:
neutralising the operator half of isRuleExpression (5 cases red); making the tag
non-optional so every doc comment becomes a rule block (2 cases red); collapsing touched
into the span set, which is what would destroy disclosure path 2 (1 case red).
Gates
Union run at 44225b82b, the final commit on this branch:
gate
result
pnpm check:docs-audit-scope
green — self-test 197 cases (155 before)
node scripts/docs-audit/check-affected-docs.mjs
green — 197 cases
pnpm check:nul-bytes
green — 6103 files, no raw control bytes
pnpm check:durability-log-level
green
node scripts/check-engine-split-ratio.mjs
green
pnpm --filter @objectstack/objectql typecheck
green
pnpm --filter @objectstack/objectql test
green — 215 files, 3811 tests
Rows 4 and 5 were not in the dispatch's gate list. I re-derived the family from my actual
changed paths with node scripts/pm/dispatch-gates.mjs, and both were pulled in by the packages/objectql path once the diff extended there. Named here per the rule that the
dispatch list is a lead, not a spec. Rows 6 and 7 are the affected package's own checks.
Scope
Two files. scripts/docs-audit/affected-docs.mjs carries the derivation; packages/objectql/src/declared-fields.ts gets the 12-line marker and nothing else — an
opt-in mechanism with zero users would not close the ruling. The claim's file surface was
amended on the card in the same round.
skip-changeset: repo tooling plus a comment-only edit. Nothing published changes.
content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.
What this run could not see
Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json origin/main → packageMentionDocs.
Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files: node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.
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
size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate
2 participants
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.
Fixes #9282
The gap
packages/objectql/src/declared-fields.tsis 191 lines, 179 of them one doc comment thatis the canonical statement of the sparse-face guard rule, and one exported function. A
change confined to that comment derived no anchor at all, so the docs-drift run
reported "this run has no opinion" — which a reader reasonably takes as "nothing to
check" — while three pages restating the rule in prose sat unlisted.
Re-measured on current
origin/main(51cd953fb, i.e. after #9230 landed thecommandkind), the instance reproduces verbatim:
Why the coarse fallback was measured and rejected
Triage's route input was the package-mention fallback for an anchorless file's own package.
I priced it on this specimen before building anything, and it is wrong in both
directions — the exact failure the #9192 rewrite exists to undo:
ruleanchors)14 pages name
@objectstack/objectqlorpackages/objectql, and not one of them isautomation/flows.mdx,protocol/objectui/actions.mdxorui/actions.mdx. Those threedocument the authoring face and never name the implementing package at all. So bounded
width (14, not 140) does not rescue the fallback — it is 0-for-3 on recall on the single
instance it was proposed for. The measured width is reported here because that is what
turns this from a preference into a verdict.
What the restating pages do share with the rule is the expressions the rule is written
in. That is what this PR anchors on.
What landed
A fifth anchor kind,
rule, built on the precedent #9230 set forcommand:@docs-ruletag in a doc comment opts the block in. Ittakes no argument and carries no anchor list — it says derive anchors from this block,
never which. The anchors are read off the block's own code spans, so an author cannot
write one, forget to update one, or leave one behind: the marker lives inside the prose
it describes and is deleted with it. That is the whole difference from the hand-curated
registry of canonical-rule sites the card ruled out.
one-line refinement to any of them can stale any page restating any part. Line-scoping
measured this specimen down to zero: the changed line's own span was
has(), which isnot an expression.
isRuleExpressionrequires a comparison orlogical operator and a dotted-or-called reference. Both halves are load-bearing and
each drops a measured class: the operator half drops the block's caller list
(
visibleWhen,requiredWhen,readonlyWhen— admitting them took the specimen from 7pages to 27); the reference half drops prose a stray backtick paired across two lines.
rulejoinsPHRASE_ANCHOR_KINDSand then faces the existingcorpus-share guard unchanged. The shape guard is untouched for every single-token kind —
the exemption is beside it, never through it, and it is earned by construction: a span
carrying
&&or!=cannot be the bare lowercase word the guard exists to drop.The honest-failure property is preserved, and that is deliberate
The card is explicit that this run's shape was more honest than most — it said it had no
opinion and named the file it could not anchor, which is the only reason the defect was
filable. A fallback that quietly widened the list without saying it fell back would have
destroyed exactly that. Three disclosure paths, all verified end to end:
anchorlessChanges, never buried under a package-wide fallback.unanchoredRuleBlocksfield plus a summary note — modelled directly on [finding][devx] The docs-drift anchor derivation cannot see a CLI command name —os meta resyncis a lowercase word, so the shape guard drops it #9230'sunmappedCommandFiles, and reported besideanchorlessChanges, not instead of it,because such a file may still have anchored through
symbol.record.x != null (rule).Reverse verification, both legs predicted red in advance
Run from the committed state, so each leg has a real restore point.
Leg 1 — the measured instance. Replaying the specimen edit:
All three restating pages found (
*), each naming the anchor that put it there.Leg 2 — silence not traded for noise. Two sub-legs:
rule's statement did not change, so the restating pages are not dragged in. The file
anchors on
materializeDeclaredFieldsexactly as it always did.record-change-trigger.ts, thedeliberate structural mirror) still reports honestly:
no anchors derived, named inanchorlessChanges, 0 docs.A third check confirms the new disclosure fires: tagging a block with no expressions in it
produces
unanchoredRuleBlocks: ["packages/triggers/.../record-change-trigger.ts"]and thematching summary note.
Harness ablations — three, each red in the predicted direction, then restored green:
neutralising the operator half of
isRuleExpression(5 cases red); making the tagnon-optional so every doc comment becomes a rule block (2 cases red); collapsing
touchedinto the span set, which is what would destroy disclosure path 2 (1 case red).
Gates
Union run at
44225b82b, the final commit on this branch:pnpm check:docs-audit-scopenode scripts/docs-audit/check-affected-docs.mjspnpm check:nul-bytespnpm check:durability-log-levelnode scripts/check-engine-split-ratio.mjspnpm --filter @objectstack/objectql typecheckpnpm --filter @objectstack/objectql testRows 4 and 5 were not in the dispatch's gate list. I re-derived the family from my actual
changed paths with
node scripts/pm/dispatch-gates.mjs, and both were pulled in by thepackages/objectqlpath once the diff extended there. Named here per the rule that thedispatch list is a lead, not a spec. Rows 6 and 7 are the affected package's own checks.
Scope
Two files.
scripts/docs-audit/affected-docs.mjscarries the derivation;packages/objectql/src/declared-fields.tsgets the 12-line marker and nothing else — anopt-in mechanism with zero users would not close the ruling. The claim's file surface was
amended on the card in the same round.
skip-changeset: repo tooling plus a comment-only edit. Nothing published changes.Generated by Claude Code
Generated by Claude Code