docs(spec): ObjectNavItem.recordId is tolerated with viewName, not mutually exclusive - #17697
Conversation
…tually exclusive The recordId docblock declared an exclusivity the navigation guard does not enforce. recordId + viewName parses clean through NavigationItemSchema; it is the one legacy pair objectNavTargetExclusivity deliberately lets through, and that guard's own docblock says so. The parenthetical gave it away — "viewName is ignored if both are set" describes a precedence, not a refusal, so the sentence's second clause contradicted its first. An author who read "mutually exclusive" would avoid a combination the platform accepts, or report a bug when it parses. Silent in both directions. Rewrite the sentence to say the pair is tolerated, name the guard that tolerates it, and point at the test that pins it. No schema, no guard, no accept set moves, and the .describe() strings that reach the generated reference are untouched. app-nav-target-exclusivity-export.test.ts gains a fifth leg asserting the docblock against the accept set it describes, so the next copy of the false sentence goes red instead of shipping. Its extraction throws when the anchor moves, so it cannot pass vacuously. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…backslashes The measurement paragraph quoted a neighbouring docblock sentence inside a code span that already contained backticks, so the inner pair was escaped and markdown would render the backslashes verbatim. Name the claim instead of quoting it. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 135 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 879d7a4c15afd6fb1ff77bc4b6ef4e86bd9e9ed6 && git checkout 879d7a4c15afd6fb1ff77bc4b6ef4e86bd9e9ed6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 88a933088e93067b4df4b20380ab9a1ceed2ed17 bfd149e080b9bf3044b5aebca972aaa90a6c3bc0 && git checkout -B drift-repro 88a933088e93067b4df4b20380ab9a1ceed2ed17 && git merge --no-ff bfd149e080b9bf3044b5aebca972aaa90a6c3bc0
node scripts/docs-audit/affected-docs.mjs --json 88a933088e93067b4df4b20380ab9a1ceed2ed17 |
Part of #16875
What moves
ObjectNavItem.recordId's docblock inpackages/spec/src/ui/app.zod.tssaid the field was "Mutually exclusive withviewName(viewName is ignored if both are set)". The guard does not refuse that pair. It is the one legacy combinationobjectNavTargetExclusivitydeliberately tolerates, and that guard's own docblock says so in as many words. The parenthetical was the tell: "ignored if both are set" is a precedence, not a refusal, so the sentence's second clause contradicted its first — and the code agrees with the second clause.The sentence is rewritten to describe what the code does: the pair is tolerated, not refused, the guard that tolerates it is named, and the test that pins the tolerance is cited.
This is a PROSE fix, not a guard fix. The #16714 ruling's item 3 explicitly preserves the tolerance, and
app-nav-target-exclusivity-export.test.tsalready pinsrecordId+viewNameas accepted precisely so that making the target fields pairwise exclusive goes red. ⛔ No schema, no guard, no accept set, no authorable key, no export moves. The.describe()strings — the ones that reachcontent/docs/references/— are untouched, andcheck:docsis green with no regeneration.Declared file face — one path beyond the claim, stated rather than slipped in
The claim declared
packages/spec/src/ui/app.zod.ts. This PR also touchespackages/spec/src/ui/app-nav-target-exclusivity-export.test.ts, which the card itself names as the optional half: "⭐ Worth pinning the sentence too, if cheap:app-nav-target-exclusivity-export.test.tsalready assertsrecordId + viewNameaccepts, so a docblock assertion beside it would close the drift rather than just fix today's copy." It was cheap, so it is here. That file is held by no other in-flight round (the claim's face-disjointness list namesui/component.zod.ts, a different file, and four others — none of them this one). ⛔ Drop this half if the seat would rather keep the face at one path; the docblock correction stands on its own.The new fifth leg extracts the
recordIddocblock by text, throws when its anchor moves so it can never pass vacuously, and asserts three things: the block claims no exclusivity (mutually exclusive/not combinable/cannot be combinedall absent), it says the pair is tolerated, andNavigationItemSchemastill accepts the pair. Prose is the only place the tolerated pair is documented — nothing else was watching it, which is how the sentence survived.Prerequisites, re-measured on
origin/main6465cc0a7c— anchors by state, never from the cardThe sentence was still there, located by content. On the file normalised the way a JSDoc block has to be — strip the leading
*per line, THEN flatten whitespace — the target phrase returns 1. Lit control: thefiltersdocblock's phrase, which also wraps a line break, returns 1. Dark control: a deliberately mistyped anchor returns 0. And the same target phrase against the raw unflattened file returns 0, which is what makes the normalisation a requirement rather than a flourish.The tolerance, re-measured through
NavigationItemSchemaitself (tsxoversrc, not a rebuilt bundle):recordId+viewNamefilters+recordIdcustomatfiltersfilters+viewNamecustomatfiltersrunAction+recordIdcustomatrunActionunrecognized_keys(a non-guard refusal, so the probe is not only reading the guard)recordIdalone /viewNamealoneFour of the seven come back refused through the same probe, so the accept on row 1 is a reading and not a probe that accepts everything.
The ruling's item 3 still preserves the tolerance — with one honest gap.
#16714itself returns 404 on both the REST API and the rendered web page (lit control:#16875returns 200 on both, and#16713returns 200 on REST). The record survives elsewhere and is unambiguous: merged PR feat(spec): export objectNavTargetExclusivity; state no precedence order in the filters docblock #16862's body ("the two deliberate asymmetries are preserved and pinned, not unified: (i)recordId+viewNamestays a tolerated legacy combination"), its at-tier review comment ("item 3 explicitly preserves the tolerance"), its fable-tier review comment ("it describes exactly the pair ruling item 3 keeps tolerated"), and in-tree at6465cc0a7c: the guard's docblock ("The legacyrecordId+viewNamecombination stays tolerated") and the pin test's header naming that pair among "the ruling's negative controls". ⇒ premise holds. The 404 is reported because it is a fact about the source of record, not because it changes the answer.Census — the file, then the tree. In this file,
mutually exclusiveoccurs 3 times: therecordIdone (FALSE — the defect), and two onfilters(TRUE, measured by rows 2 and 3 above). One further exclusivity claim,not combinable with recordIdonrunAction, is TRUE (row 4). Bothunrepresentableclaims are TRUE by the same rows. ⇒ exactly one false exclusivity sentence in the file, and it is the one the card names. Dark control on the census instrument: 0. Tree-wide over tracked files (git grep, so no builtdist/is in scope), the only otherviewNameexclusivity prose iscontent/docs/ui/apps.mdx:90, which already states the tolerance correctly; theexplain.zod.tsfamily is a different pair (recordId/recordIds) and is enforced. Instrument lit at 137 totalmutually exclusivehits tree-wide, dark control 0.Verification
Head at every build / test / typecheck / gate run below:
bcfd9b5d46. The second commit on this branch (bfd149e080) edits only the changeset's prose — no source, no test — and the changeset family (check-changeset-no-major,check-empty-changeset,check-adr-0087-registration,check:pm-changeset-deadline-census) pluscheck:nul-bytesandcheck:doc-authoringwere re-run on it, each exit 0. Every exit captured before any pipe; lock verdicts quoted fromos-verify-lock.pnpm --filter @objectstack/spec buildunder the lock →VERDICT command-exit 0.pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 src/ui/app-nav-target-exclusivity-export.test.ts src/ui/app.test.tsunder the lock →VERDICT command-exit 0,Test Files 2 passed (2),Tests 139 passed (139). The pin file itself is37 passed (37), up from 34.pnpm --filter @objectstack/spec typecheckunder the lock →VERDICT command-exit 0(tsc --noEmit,check:scripts-typecheck,check:test-typecheck: OK).pnpm --filter @objectstack/spec check:generated→ exit 0,✓ All 15 generated artifacts are up to date— nothing regenerated, which re-measures the card's claim that this docblock reaches no generated reference.check:api-surface,check:authorable-surface,check:docs,check:export-origins,check:variant-docs,check:exported-any,check:liveness,check:empty-state,check:yaml-examples,check:llms-txt,check:objectui-pin-citations,check:duration-unit-keys,check:dual-source-exports,check:entry-nameability,check:browser-reachable-entries,check:strictness-ledger,check:skill-refs.check:nul-bytes,check-spec-docblock-symbol-anchors,check-changeset-no-major --base origin/main,check-empty-changeset --base origin/main,check-adr-0087-registration --base origin/main,check-closing-keyword-parity,check-comment-mask-adoption,check-comment-mask-corpus,check:cross-package-test-inputs,check:test-source-alias,check:tier-file-adoption,check-keyed-text-bounds,check:doc-authoring,check:published-files,check:merge-driver,check:objectui-changeset,check:changeset-gate-self-tests,check:pm-changeset-deadline-census,docs-audit/check-affected-docs,docs-audit/check-drift-comment,check-undeclared-dep-imports,check:type-check-coverage.check:doc-formula-expressions,check:type-check-debtandcheck:skill-exampleseach exited 3 —PREREQUISITE NOT MET, a workspace closure this worktree has not built. Nothing was measured by any of the three; none is a red. Narrowing declared: of the 82 familiesscripts/pm/dispatch-gates.mjs --commandsderives for these three paths, 29 were run; the rest are CI's farm.Reverse verification — the new pin can fail
Committed first, then mutated, then restored; every leg proved on disk rather than from an editor's exit code.
XXMUTATIONXXmarker. On-disk proof: markergrep -c= 1, restored false sentence = 1, andgit hash-objectdiffered from the HEAD blob (4b25e25d…vsc47671ab…). Run:vitest run src/ui/app-nav-target-exclusivity-export.test.ts→ exit 1,Tests 1 failed | 36 passed (37), the failure named as "recordIddocblock parity with the accept set > claims no exclusivity the guard does not enforce". One test failed, not the file — the extraction still found its anchor, so the red is the assertion's and not a broken probe's.git checkout HEAD -- packages/spec/src/ui/app.zod.ts(⛔ never the bare form, which restores from the index). Proof: marker 0, false sentence 0,git hash-objectbyte-identical to the HEAD blob,git diff HEADempty,git status --porcelainempty. Re-run → exit 0,Tests 37 passed (37).packages/spec's own vitest resolves./app.zodrelatively, so the test readssrc, neverdist.Changeset
@objectstack/spec: patch. Notskip-changeset— the publishedfiles[]carries bothdistandsrc/**/*.zod.ts, and the edited file matches that glob, so it ships as source verbatim. Measured on the built artifact too: the new sentence is present in 18 files underdist/and the old spelling in 0, with two untouched sentences from the same region present in 18 each as lit controls. The declaration files do not carry it (a field-level docblock inside a Zod shape), which is why the reach is stated as the bundles plus the shipped source rather than as.d.ts.Acceptance notes — noted, not filed
ObjectNavItemSchema(the standalone export) carries no object-level check, so the tolerated/refused matrix exists only onNavigationItemSchema'stype: 'object'union branch. That is deliberate, pinned by leg 2 of the same test file, and stated in the guard's docblock — recorded here only because a reader measuring exclusivity through the wrong export will find nothing refused at all. Carrier: any future round touching this guard's mount. Not filed: no defect, and the ruling that fixed the mount is the one this card serves.#16714is unreachable (404, both channels) while its ruling is still cited by four live artifacts in this repo and in PR feat(spec): export objectNavTargetExclusivity; state no precedence order in the filters docblock #16862. Not filed here — it is a board fact, not a code defect, and it belongs to whoever owns card hygiene rather than to this PR. Carrier: the PM seat reading this report.🤖 Generated with Claude Code
https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Generated by Claude Code