Skip to content

feat(docs-audit): anchor a changed @docs-rule block on the expressions it states - #9394

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-9282-docs-drift-anchorless-files
Aug 17, 2026
Merged

feat(docs-audit): anchor a changed @docs-rule block on the expressions it states#9394
os-steve merged 1 commit into
mainfrom
claude/issue-9282-docs-drift-anchorless-files

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes#9282

The gap

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:

routepages listedof the 3 restating pages found
today0 ("no opinion")0 / 3
package-mention fallback140 / 3
this PR (rule anchors)73 / 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:

  1. An untagged anchorless file is unchanged: still "no opinion", still named in
    anchorlessChanges, never buried under a package-wide fallback.
  2. A tagged block that changed and yielded nothing is published in a new
    unanchoredRuleBlocks field plus a summary note — modelled directly on [finding][devx] The docs-drift anchor derivation cannot see a CLI command name — os meta resync is a lowercase word, so the shape guard drops it #9230's
    unmappedCommandFiles, and reported besideanchorlessChanges, not instead of it,
    because such a file may still have anchored through symbol.
  3. Every listed page says why it is listed: 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:

7 docs name something this change touched (16 anchor(s) - 0 symbol, 0 route, 0 sdk,
0 literal, 0 command, 16 rule) across 1 changed package(s)
anchorlessChanges: []
* content/docs/automation/flows.mdx <- has(record.x) && record.x != null (rule)
content/docs/automation/hooks.mdx <- record.done == true (rule)
content/docs/data-modeling/formulas.mdx <- record.x != null (rule)
content/docs/getting-started/quick-start.mdx <- record.x != null (rule)
* content/docs/protocol/objectui/actions.mdx <- has(record.x) && record.x != null (rule)
content/docs/releases/v17.mdx <- record.done == true (rule)
* content/docs/ui/actions.mdx <- record.x != null (rule)

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:

gateresult
pnpm check:docs-audit-scopegreen — self-test 197 cases (155 before)
node scripts/docs-audit/check-affected-docs.mjsgreen — 197 cases
pnpm check:nul-bytesgreen — 6103 files, no raw control bytes
pnpm check:durability-log-levelgreen
node scripts/check-engine-split-ratio.mjsgreen
pnpm --filter @objectstack/objectql typecheckgreen
pnpm --filter @objectstack/objectql testgreen — 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.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 16 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx(via has(record.x) && record.x != null (rule), record.x != null (rule))
  • content/docs/automation/hooks.mdx(via record.done == true (rule))
  • content/docs/data-modeling/formulas.mdx(via record.x != null (rule))
  • content/docs/getting-started/quick-start.mdx(via record.x != null (rule))
  • content/docs/protocol/objectui/actions.mdx(via has(record.x) && record.x != null (rule), record.x != null (rule))
  • content/docs/ui/actions.mdx(via record.x != null (rule))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via record.done == true (rule))

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/mainpackageMentionDocs.

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.

@os-steveos-steve added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 17, 2026 — with Claude
@os-steve
os-steve marked this pull request as ready for review August 17, 2026 22:50
@os-steve
os-steve added this pull request to the merge queueAug 17, 2026
Merged via the queue into main with commit a0f65a8Aug 17, 2026
29 checks passed
@os-steve
os-steve deleted the claude/issue-9282-docs-drift-anchorless-files branch August 17, 2026 23:05
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-steve@claude