Skip to content

docs(spec): ObjectNavItem.recordId is tolerated with viewName, not mutually exclusive - #17697

Merged
os-bill merged 2 commits into
mainfrom
claude/issue-16875-navigation-recordid-docblock
Sep 11, 2026
Merged

docs(spec): ObjectNavItem.recordId is tolerated with viewName, not mutually exclusive#17697
os-bill merged 2 commits into
mainfrom
claude/issue-16875-navigation-recordid-docblock

Conversation

@os-bill

@os-bill os-bill commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Part of #16875

  • Clause-②: no

What moves

ObjectNavItem.recordId's docblock in packages/spec/src/ui/app.zod.ts said the field was "Mutually exclusive with viewName (viewName is ignored if both are set)". The guard does not refuse that pair. It is the one legacy combination objectNavTargetExclusivity deliberately 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.ts already pins recordId + viewName as 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 reach content/docs/references/ — are untouched, and check:docs is 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 touches packages/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.ts already asserts recordId + viewName accepts, 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 names ui/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 recordId docblock 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 combined all absent), it says the pair is tolerated, and NavigationItemSchema still 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/main 6465cc0a7c — anchors by state, never from the card

  1. The 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: the filters docblock'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.

  2. The tolerance, re-measured through NavigationItemSchema itself (tsx over src, not a rebuilt bundle):

case expected measured
recordId + viewName accept accept
filters + recordId refuse refuse — custom at filters
filters + viewName refuse refuse — custom at filters
runAction + recordId refuse refuse — custom at runAction
an undeclared key refuse refuse — unrecognized_keys (a non-guard refusal, so the probe is not only reading the guard)
recordId alone / viewName alone accept accept

Four 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.

  1. The ruling's item 3 still preserves the tolerance — with one honest gap. #16714 itself returns 404 on both the REST API and the rendered web page (lit control: #16875 returns 200 on both, and #16713 returns 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 + viewName stays 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 at 6465cc0a7c: the guard's docblock ("The legacy recordId + viewName combination 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.

  2. Census — the file, then the tree. In this file, mutually exclusive occurs 3 times: the recordId one (FALSE — the defect), and two on filters (TRUE, measured by rows 2 and 3 above). One further exclusivity claim, not combinable with recordId on runAction, is TRUE (row 4). Both unrepresentable claims 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 built dist/ is in scope), the only other viewName exclusivity prose is content/docs/ui/apps.mdx:90, which already states the tolerance correctly; the explain.zod.ts family is a different pair (recordId / recordIds) and is enforced. Instrument lit at 137 total mutually exclusive hits 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) plus check:nul-bytes and check:doc-authoring were re-run on it, each exit 0. Every exit captured before any pipe; lock verdicts quoted from os-verify-lock.

  • pnpm --filter @objectstack/spec build under 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.ts under the lock → VERDICT command-exit 0, Test Files 2 passed (2), Tests 139 passed (139). The pin file itself is 37 passed (37), up from 34.
  • pnpm --filter @objectstack/spec typecheck under 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 datenothing regenerated, which re-measures the card's claim that this docblock reaches no generated reference.
  • Spec source audits, each exit 0: 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.
  • Root gates, each exit 0: 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.
  • NOT MEASURED, declared rather than counted as green: check:doc-formula-expressions, check:type-check-debt and check:skill-examples each exited 3PREREQUISITE 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 families scripts/pm/dispatch-gates.mjs --commands derives 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.

  • Mutation leg. The false sentence was put back verbatim alongside an injected XXMUTATIONXX marker. On-disk proof: marker grep -c = 1, restored false sentence = 1, and git hash-object differed from the HEAD blob (4b25e25d… vs c47671ab…). Run: vitest run src/ui/app-nav-target-exclusivity-export.test.ts → exit 1, Tests 1 failed | 36 passed (37), the failure named as "recordId docblock 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.
  • Restore leg. 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-object byte-identical to the HEAD blob, git diff HEAD empty, git status --porcelain empty. Re-run → exit 0, Tests 37 passed (37).
  • A shell trap on EXIT, INT and TERM, restoring through an absolute path, guarded both legs. No build step is in this ablation's path on purpose: packages/spec's own vitest resolves ./app.zod relatively, so the test reads src, never dist.

Changeset

@objectstack/spec: patch. Not skip-changeset — the published files[] carries both dist and src/**/*.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 under dist/ 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 on NavigationItemSchema's type: '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.
  • #16714 is 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

…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
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui labels Sep 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 135 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 88a933088e93067b4df4b20380ab9a1ceed2ed17packageMentionDocs.

Which tree this was computed on

This run read content/docs from 879d7a4c15afd6fb1ff77bc4b6ef4e86bd9e9ed6 — the merge of head bfd149e080b9bf3044b5aebca972aaa90a6c3bc0 into base 88a933088e93067b4df4b20380ab9a1ceed2ed17, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@os-bill
os-bill marked this pull request as ready for review September 11, 2026 16:24
@os-bill
os-bill added this pull request to the merge queue Sep 11, 2026
Merged via the queue into main with commit d4a1a28 Sep 11, 2026
36 checks passed
@os-bill
os-bill deleted the claude/issue-16875-navigation-recordid-docblock branch September 11, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants