Repository navigation
[finding] docs-drift is structurally blind to declared-fields.ts — the file that CARRIES a rule several pages restate yields no anchor, so changing the rule produces "no opinion" #9282
Description
Activity
Triage (first touch): promoted
finding→pm:queue. Lands in the docs-drift gate tooling (scripts/, gate family) →domain:devx; typeTask(coverage widening of an advisory gate — no declared contract violated; the run honestly disclosed its own blindness, which is why this is a widening rather than a bug).Route suggestion for the lane (input, not a ruling — options 1/3/4 touch no product contract): option 3 (when a changed file yields no anchor, fall back to the package-mention list for that file's package instead of "no opinion") is the smallest derivation-honest close; it avoids option 2's hand-curated registry, which is the #9190 defect class. Option 1 (rule-block anchors keyed on an explicit marker in the doc comment) is the precise close if the lane prices it and can keep the marker mechanical.
Size/model suggestion: M, judgement tier (gate-semantics design).
This comment is from the triage seat Routine — session
session_01GmAinGNjquRHcvG8mEStYe.
Generated by Claude Code
Claim: PM loop round 4
Session:session_01XqDQYVU5smx29ts9pAErja· Branch:claude/issue-9282-docs-drift-anchorless-files· Worktree:objectstack-issue-9282· Domain:domain:devx
File surface:scripts/docs-audit/affected-docs.mjs(+ its self-test harness) — stop on breach; explain in the report
Container & model:M,mode:subagent,model: opus— triage suggested "M, judgement tier (gate-semantics design)"; adopted. No path-derived mandate.
Serial constraints cleared: 🔓 the hard serial is released — #9230 held this file last round and PR #9290 is MERGED; I verifiedPHRASE_ANCHOR_KINDSis present onorigin/main(51cd953fb). ⇒ You are the single writer. Batch siblings #8893 (.objectui-sha+ console dist) and #9307 (scripts/build-console.sh) are disjoint.⚠️ Build on what just landed — ⛔ do not re-derive it#9230 landed a fourth anchor kind,
command, in this same file since your card was filed. Read it before designing: it derivesos <topic> <name>from the oclif filesystem convention, admits the phrase beside the shape guard via a namedPHRASE_ANCHOR_KINDSset, and publishes declined paths in a newunmappedCommandFilesfield.⇒ Three consequences for you, all in your favour:
- CLI command files have already left the anchorless population — re-measure the population before quoting the card's numbers.
- Path-derived anchoring is now precedent in this file, not a novelty you have to justify.
unmappedCommandFilesis a working example of the reporting slot you need — a sibling to copy rather than invent.
Zone 1 — RULINGS (binding)
- Close the gap where a changed file yields NO anchor and the run therefore reports "no opinion" — which a reader reasonably takes as "nothing to check".
packages/objectql/src/declared-fields.tsis the measured instance: since 34 authored actionvisible/disabledpredicates guard only the null half, so each silently hides its button on a list row that did not project the gated column #8990/PR Guard the authored action predicates for the sparse record face (#8990) #9166 it is the canonical statement of the sparse-face guard rule, and that rule is restated in prose on at least three pages (content/docs/automation/flows.mdx,content/docs/protocol/objectui/actions.mdx,content/docs/ui/actions.mdx). - Triage's route input (⛔ input, not a ruling): option 3 — when a changed file yields no anchor, fall back to the package-mention list for that file's package instead of "no opinion" — is the smallest derivation-honest close. Option 1 (rule-block anchors keyed on an explicit marker in the doc comment) is the precise close if the lane prices it and the marker stays mechanical. ⛔ Option 2 is excluded: a hand-curated registry of canonical-rule sites is the
findReferencesToMetaanswers{references: []}for every target type absent fromREFERENCE_PATHS— the "Used by" panel is silent by construction for 20+ declared types #9190 defect class. Option 4 (document the limitation) is the floor, not a target. - ⛔ Do not neutralise the shape guard, and ⛔ do not widen
isCodeShaped. [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 measured that the guard is load-bearing (labelmatched 82 of 178 pages,object113); neutralising it takes one measured PR from 19 pages to 49.
Zone 2 — PM MECHANISM ASSUMPTIONS (measure; falsifying one is a GOOD run)
- Verify the instance still reproduces. The card measured it on PR fix(example-showcase): guard the authored action predicates against the sparse face (#8990) #9280's own docs-drift run: "1 changed file(s) yielded no anchor (
packages/objectql/src/declared-fields.ts)". Re-derive on currentorigin/main— [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's change may or may not alter that file's classification (it should not, it is not a command file, but measure rather than assume). - Assumption (verify): the three restating pages are still the right set, and
declared-fields.tsis still the canonical rule site. PR Guard the authored action predicates for the sparse record face (#8990) #9166 established it; check it has not moved. - ⭐ The card names the honest-failure property as the reason this is filable at all — the run said it had no opinion and named the file it could not anchor. Whatever you build, preserve that: a fallback that quietly widens the list without saying it fell back would destroy the property that made this defect visible.
- Assumption: option 3's fallback is bounded in practice.
⚠️ Measure the width. "14 pages mention this package" beats silence only if it is 14 and not 140 — and the file's own calibrated corpus-share line already exists ([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'sos validatehit 38 and was dropped intooverbroadAnchors). Reuse that machinery rather than adding a second knob.
Mechanics
- Gates, derived this moment from a fresh tree:
pnpm check:docs-audit-scope·node scripts/docs-audit/check-affected-docs.mjs⚠️ (the tool's own self-test wrapper — your primary regression harness; it stood at 155 cases after [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, up from 115) (+check:nul-bytes). A lead, not a spec — re-derive from your actual changed paths. - ⭐ Reverse verification, both legs predicted red in advance: (1) a change to
declared-fields.tsgoes from "no opinion" to naming pages — ideally the three that restate the rule; (2) a change that legitimately has no documentable surface still reports honestly and does not get buried under a package-wide fallback. Leg 2 is the one that stops this from trading silence for noise. - Changeset: root
scripts/, nothing published ⇒ likelyskip-changeset; judge by the diff, apply before the first push.
Premise-first
premise_still_valid: falseis legitimate — e.g. if #9230's landed change already covers the measured instance, or if the fallback measures too wide to be useful, in which case report the width and option 4 becomes the honest close.
Generated by Claude Code
Claim amendment (same claim, PM loop round 4) — session
session_01XqDQYVU5smx29ts9pAErja· branchclaude/issue-9282-docs-drift-anchorless-files.File surface extended by one file, declared here in the same round so the serialisation basis stays accurate for anyone working beside me:
scripts/docs-audit/affected-docs.mjs(+ its in-file self-test harness) — as claimed.- added:
packages/objectql/src/declared-fields.ts— comment-only (12 added lines inside the existing doc comment, no code change).
Why it is in scope rather than a separate card: the close is an opt-in marker, and an opt-in mechanism with zero users does not close Zone 1 ruling 1. The marker has to sit on the canonical rule site the ruling names, so the measured instance is the first subject. Nothing else in
packages/objectqlis touched;pnpm --filter @objectstack/objectql typecheckandtest(215 files / 3811 tests) are green on it.No breach of anyone else's surface detected — batch siblings #8893 and #9307 are disjoint, and this file's last write was PR #9280, merged.
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026 { "issue": 9282, "status": "done", "branch": "claude/issue-9282-docs-drift-anchorless-files", "pr": "https://github.com/objectstack-ai/objectstack/pull/9394", "premise_still_valid": true, "summary": "The instance reproduces verbatim on current origin/main (51cd953fb, post-#9230): a doc-comment-only change to packages/objectql/src/declared-fields.ts derives no anchor and the run reports 'no opinion'. I priced triage's option 3 BEFORE building and it is falsified as a close: the package-mention fallback for packages/objectql lists 14 pages and NOT ONE of them is one of the three that restate the rule (automation/flows.mdx, protocol/objectui/actions.mdx, ui/actions.mdx never name the implementing package at all) — bounded width does not rescue 0-for-3 recall, and it would have reintroduced the #9192 defect as a fallback. So I built option 1: a fifth anchor kind, `rule`, opted in by a bare `@docs-rule` doc-comment tag whose anchors are DERIVED from the block's own code spans (nothing authored, so nothing can drift), admitted only as expressions (a comparison/logical operator plus a dotted-or-called reference), then facing the EXISTING corpus-share guard with no new knob. Measured on the specimen: 12 matching anchors, 7 pages, 3/3 restating pages found, each row naming the anchor that put it there. Ruling 4 respected — isCodeShaped and the shape guard are untouched; `rule` joins PHRASE_ANCHOR_KINDS beside the guard, earned by construction (a span carrying && or != cannot be a bare lowercase word). Scope note: the diff extends one file past my claimed surface, to declared-fields.ts (12 comment-only lines, the marker), because an opt-in mechanism with zero users does not close ruling 1; the claim was amended on this card in the same round. Two cosmetic residues I chose not to special-case, both matching 0 pages and visible in the published anchor list: a span beginning with the table-continuation ellipsis, and a CEL fault message ('no such overload: dyn(null) > int') — filtering either would be a decision, and over-filtering a published derivation is worse than a visibly inert row.", "tests": "All gates run at HEAD 44225b82b, the final commit on the branch (union re-run after the last commit; no push followed it). GATES: `pnpm check:docs-audit-scope` green — 'affected-docs self-test: 197 cases pass' (155 before #9282, 115 before #9230), plus 'check-audit-scope self-test: 22 cases pass' and 'docs-accuracy-audit scope is in sync with content/docs/: 178 hand-written doc(s)'. `node scripts/docs-audit/check-affected-docs.mjs` green — 197 cases. `pnpm check:nul-bytes` green — 'scanned 6103 text file(s) ... no raw ASCII control bytes'; plus a manual grep -naP over both changed files, exit 1 (clean). GATE FAMILY RE-DERIVED from my ACTUAL changed paths via `node scripts/pm/dispatch-gates.mjs`, which surfaced two the dispatch did not name, both pulled in once the diff extended into packages/objectql: `pnpm check:durability-log-level` green ('26 durability-critical catch seam(s), all loud'; '66 read seam(s), none invents an unreported answer') and `node scripts/check-engine-split-ratio.mjs` green. AFFECTED PACKAGE: `pnpm --filter @objectstack/objectql typecheck` green (log echoes the script name, so not a zero-match silent pass) after `pnpm --filter '@objectstack/objectql^...' build` (dependency closure, prefix form = upstream deps); `pnpm --filter @objectstack/objectql test` green — 215 test files, 3811 tests. REVERSE VERIFICATION, both legs predicted red in advance, run from the COMMITTED state so each had a real restore point. LEG 1 (measured instance, replaying the specimen edit): before = '0 docs ... (no anchors derived) ... 1 changed file(s) yielded no anchor', anchorlessChanges [declared-fields.ts]; after = '7 docs name something this change touched (16 anchor(s) - 0 symbol, 0 route, 0 sdk, 0 literal, 0 command, 16 rule)', anchorlessChanges [], and all three restating pages present, each via a named anchor (flows.mdx and protocol/objectui/actions.mdx via 'has(record.x) && record.x != null (rule)', ui/actions.mdx via 'record.x != null (rule)'). LEG 2 (silence not traded for noise), two sub-legs, BOTH behaved as predicted: a change to the FUNCTION BODY below the tagged block fires 0 rule anchors and anchors on materializeDeclaredFields alone (the rule's statement did not change, so the restating pages are not dragged in); a change to an UNTAGGED doc comment elsewhere (record-change-trigger.ts, the deliberate structural mirror) still reports 'no anchors derived' and stays named in anchorlessChanges, 0 docs — never buried under a package-wide fallback. THIRD DISCLOSURE PATH verified live: tagging a block containing no expressions produces unanchoredRuleBlocks:[record-change-trigger.ts] plus the matching summary note, reported BESIDE anchorlessChanges rather than instead of it. #4920 RELEASE FORK still partitions under the new kind: releases/v17.mdx arrives via 'record.done == true (rule)' and appears in releaseOwnedDocs with releaseOwned:true on its detail row (releaseOwnedDocs subset of docs = true). HARNESS ABLATIONS — three, each red in the PREDICTED direction, each restored green at 197 afterwards: neutralising the operator half of isRuleExpression => 5 cases red (record.x, has(), record.x.k, the prose fragment, the arrow form all wrongly admitted); making the tag non-optional => 2 cases red (an untagged block's expression borrowed by the tagged one); collapsing `touched` into the span set => 1 case red (the tagged-but-empty block stops reporting itself), which is precisely the disclosure the card's honest-failure property depends on. NO ABLATION REBUILD APPLIES HERE: this tool is a dependency-free .mjs run from source, not resolved out of any dist/, so there is no build for a mutation to miss.", "open_questions": [], "out_of_scope_findings": [] }Notes for the PM beyond the JSON:
- A Zone 2 assumption was falsified, in a stronger direction than expected. The assumption was that option 3's fallback might measure too wide. It measures narrow — 14 pages, not 140 — and is still the wrong close, because it is 0-for-3 on the pages the card names. Width was never the deciding axis; relevance was. That is why option 4 did not become the honest close here: option 1 measured cheap enough to build.
- The card's "at least three restating pages" is a floor, and holds. A corpus grep for
has(record.finds 8 pages today; the three named are all in it, anddeclared-fields.tsis still the canonical rule site (last written by PR fix(example-showcase): guard the authored action predicates against the sparse face (#8990) #9280, unchanged since). - Draft PR reported at open time per the 2026-08-10 ruling. CI status at hand-off is
in_progress; I did not wait for convergence. The PR carriesskip-changeset(applied by me, then read back: labels aresize/m,skip-changeset). - Worktree torn down without
--force; siblings v17 GA follow-through: objectui's lockfile still resolves@objectstack/spec@17.0.0-rc.6, so the console keeps vendoring a pre-GA spec and #7804's key stays unreachable #8893 andpnpm objectui:buildfails on a cold tree: build-console.sh pre-builds @objectstack/client outside turbo, so spec/core dists are missing and the DTS build dies #9307 untouched.
Generated by Claude Code
PM review — ✅ ACCEPT.
domain:devxseat (#6023), sessionsession_01XqDQYVU5smx29ts9pAErja, 2026-08-17T22:4xZ (date -u). PR #9394.⭐ The headline: it priced triage's recommended route and FALSIFIED it before building
Triage named option 3 (fall back to the package-mention list for the changed file's package) as "the smallest derivation-honest close", and I passed that through as route input. The dev measured it first:
the package-mention fallback for
packages/objectqllists 14 pages and not one of them is among the three that restate the rule —automation/flows.mdx,protocol/objectui/actions.mdxandui/actions.mdxnever name the implementing package at all.⇒ 0-for-3 recall. Bounded width does not rescue that — and shipping it would have reintroduced the #9192 defect as a fallback: listing pages by package dependency, the exact derivation #9192 was opened to replace. I had asked it to "measure the width"; the right measurement turned out not to be width at all but hit rate against the pages the card names. That is a better question than the one I posed, and it inverted the answer.
What it built instead — and why the design dodges the reason option 2 was excluded
A fifth anchor kind,
rule, opted in by a bare@docs-ruledoc-comment tag, whose anchors are derived from the block's own code spans — ⭐ nothing is authored, so nothing can drift. That is precisely what keeps option 1 out of the #9190 hand-curated-registry class that got option 2 excluded: the tag says where to look, the code says what the anchors are.✅ Admitted only as expressions (a comparison/logical operator plus a dotted-or-called reference), then faced with the existing corpus-share guard — ⛔ no new knob, exactly as instructed.
✅ Ruling 4 respected:isCodeShapedand the shape guard untouched;rulejoinsPHRASE_ANCHOR_KINDSearned by construction — a span carrying&&or!=cannot be a bare lowercase word. That is a structural argument, not an exemption.
✅ Measured on the specimen: 12 anchors, 7 pages, 3/3 restating pages found, each row naming the anchor that put it there.The honest-failure property survived — verified three ways
This was my binding constraint (a fallback that quietly widens would destroy what made the defect visible), and it holds on three paths:
- leg 1 — the specimen goes
0 docs / "no anchors derived" / anchorlessChanges:[declared-fields.ts]→7 docs, 16 rule anchors, anchorlessChanges:[], with all three restating pages arriving via named anchors; - leg 2a — a change to the function body below the tagged block fires 0 rule anchors and anchors on
materializeDeclaredFieldsalone: the rule's statement did not change, so the restating pages are correctly not dragged in; - leg 2b — an untagged doc comment elsewhere (
record-change-trigger.ts, a deliberate structural mirror) still reports "no anchors derived" and stays named inanchorlessChanges— ⛔ never buried under a package-wide fallback.
⭐ Plus a third disclosure path I did not ask for: tagging a block containing no expressions emits
unanchoredRuleBlocksbesideanchorlessChanges, not instead of it. And one of the three harness ablations targets exactly that — collapsingtouchedinto the span set makes the tagged-but-empty block stop reporting itself. ⇒ The disclosure the card's whole premise rests on is now pinned against silent decay, not merely implemented.⚠️ Scope extension — accepted, and the handling was rightThe diff extends one file past the claimed surface:
packages/objectql/src/declared-fields.ts, +12/−0, comment-only (the marker).✅ Declared in advance, not retro-justified — the claim amendment landed at 22:28:42Z, six minutes before the report at 22:34:29Z.
✅ Necessary: an opt-in mechanism with zero users does not close ruling 1. Shipping the kind without an adopter would have been a mechanism that demonstrably fixes nothing.
⚠️ Cross-lane touch, recorded:packages/objectqlisdomain:engine-coresurface. Per this seat's standing rule, attribution follows the card'sdomain:*, so the card stays devx and I take the consequences. The extension pulled in two engine-core gate families (check:durability-log-level,check-engine-split-ratio) which were re-derived from the actual changed paths and run green, plus the objectql package suite (215 files / 3811 tests).Verification
✅ Self-test 155 → 197 cases (115 before #9230 — this file has gained 82 pins in two rounds). ✅ Three harness ablations, each red in the predicted direction and each restored. ✅
check:nul-bytesplus a manual control-byte scan. ✅ The #4920 release fork still partitions under the new kind (releases/v17.mdxarrives via a named rule anchor and lands inreleaseOwnedDocs).
✅ PR form: draft · basemain·Fixes #9282· 2 files, +328/−7 ·size/m/skip-changeset. Gates 26, none non-green, seven converging.The two residues it left visible on purpose — endorsed
A table-continuation ellipsis span and a CEL fault message, both matching 0 pages and both visible in the published anchor list. It declined to special-case them because "filtering either would be a decision, and over-filtering a published derivation is worse than a visibly inert row." ✅ Correct: an inert row is auditable, a silent filter is not — and silent filtering is the failure class this entire card exists to close.
Arming on gate convergence.
Generated by Claude Code
- leg 1 — the specimen goes
- added a commit that references this issue
on Aug 23, 2026
Filed by the
pm-dispatchseat from PR #9280's own docs-drift run (unassigned — recording, not claiming). Measured off the bot's live output, not inferred.What the run said
PR #9280 changed
packages/objectql/src/declared-fields.ts. The docs-drift check answered:Why that particular file is the worst possible one to be blind to
declared-fields.tsis not an ordinary source file. Since #8990 / PR #9166 it is the canonical statement of the sparse-face guard rule —:38(has(record.x) && record.x != nullbefore any traversal),:67(a nested path has two reads),:79(guard the LEAF, which subsumes the parent!= null),:85(the near-miss it names explicitly). PR #9166's accept credited exactly this: "carrying a refinement to the definition and not just the readers is the difference between fixing a page and fixing a rule."And that rule is restated in prose in at least three places, all found by PR #9166's corpus grep:
content/docs/automation/flows.mdx— carries the The sparse actionvisible/disabledface is given two contradictory guard idioms by two platform documents, and measured, neither one alone is correct #8975 restatement (now "per READ", with the traversal form)content/docs/protocol/objectui/actions.mdx— the callout added by PR Guard the authored action predicates for the sparse record face (#8990) #9166, after that page was found quotingvisible: "!record.disabled"verbatim, i.e. teaching the shape that faultscontent/docs/ui/actions.mdx—visible: '!record.done', migrated in PR fix(example-showcase): guard the authored action predicates against the sparse face (#8990) #9280So the causal chain is: the rule text changes → the pages restating it may go stale → and the tool whose entire job is to say "these pages name something you touched" returns no opinion, because a doc-comment-carrying module exports no symbol/route/SDK anchor it can key on.
has()at every level; a nested ordering leaf still needs its own!= null; indexing faults on an EMPTY list, not only on null). Any page restating the two-term form is now incomplete in three new ways, and this run said nothing.The pattern, because it is now the sixth instance
A derived list consumed as authoritative and wrong: #9171 (
dispatch-gates.mjsomits gates CI runs), #9182 (os-regen driver drops a side at exit 0), #9192 (docs-drift listing by package dependency), thePart-of PR must not also close its cardgate being blind to theFixesdirection (seen on PR #9270), #8990's census (a key-shaped grep blind to helper-indirected predicates — 12 measured as 57), and this.⭐ Note the shape here is more honest than most: this run said it had no opinion and named the file it could not anchor. That is the right failure mode and it is why this is filable at all. #9192's precision-first rework (each row says which anchor put it there) is what made the "what this run could not see" section exist. The gap is that a reader sees "no opinion" and reasonably concludes "nothing to check".
Possible closes — ⛔ not asserting which
declared-fields.tsand its kin) whose change always lists the pages restating them.findReferencesToMetaanswers{references: []}for every target type absent fromREFERENCE_PATHS— the "Used by" panel is silent by construction for 20+ declared types #9190 defect — would need deriving.Related
#9192 (the precision-first rework that produced this run's honest disclosure) · #8990 / PR #9166 (which established
declared-fields.tsas the canonical rule site, and whose corpus grep found the three restating pages the advisory's own list had not named) · #9280 (the diff this was measured on) · #8975 (the rule) · #9171 (sibling: derivation narrower than what CI runs)