Repository navigation
feat(spec)!: retire ListViewSchema.navigation.view, the detail-view binding nothing ever resolved - #18619
Conversation
WIP: tombstone + ADR-0087 registration. Generated artifacts follow. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Tombstone, ADR-0087 registration, regenerated artifacts and the pins. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 136 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 a693aa4e1bac6118d3280c0d95fc8fa996c4593c && git checkout a693aa4e1bac6118d3280c0d95fc8fa996c4593c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 21b7c12b4f779b86409db2a7bb9235fe80ef8a54 650c7938a962900518041f8aac7279e056166367 && git checkout -B drift-repro 21b7c12b4f779b86409db2a7bb9235fe80ef8a54 && git merge --no-ff 650c7938a962900518041f8aac7279e056166367
node scripts/docs-audit/affected-docs.mjs --json 21b7c12b4f779b86409db2a7bb9235fe80ef8a54
|
… the published README (objectui#9987) The README's record-navigation paragraph told an author to write the member that `@objectstack/spec` retired under ADR-0049 (objectstack-ai/objectstack#18619) — and then, in the same paragraph, explained the mechanism by which an undeclared key rejects the whole config. The first sentence caused what the second diagnosed. This file is listed in `files[]`, so the instruction shipped in the npm tarball. The instruction is replaced by the route the retirement actually leaves standing, carried in from `@object-ui/react`'s `useNavigationOverlay` docblock: assign a `record` page to the object and let `isDefault` pick the one that opens. Deleting the sentence alone was not enough — it answered a real authoring question, and a reader who loses the answer goes looking and finds the retired key elsewhere. The second sentence is kept byte-identical (modulo re-wrapping): it is the only place this README tells an author why a typo costs them the whole block. It just stops being a description of what the paragraph above it told them to do. `readme-navigation-example.test.ts` now measures the PROSE as well as the fence. The fence tests could never have caught this — they extract the ```json block, and the block never carried the key. The pinned `@objectstack/spec@17.4.0` still declares `view`, so nothing schema-derived can say it is retired in this tree; it is named by hand, exactly as `basePath` already is, and the absence carries its own control: the same detector is run over the sentence that used to carry the instruction and must find it there. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
… the published README (objectui#9987) (objectstack-ai#10090) Fixes objectstack-ai#9987 Clause-②: no — a published README and the test that runs its example. It moves no exported symbol and no key on a published payload. ## What the paragraph did `packages/plugin-gantt/README.md` is listed in that package's `files[]`, so it ships inside the npm tarball. Its record-navigation paragraph **instructed** an author to write the member `@objectstack/spec` retired under ADR-0049 (objectstack-ai/objectstack#18619) — and then, in the same paragraph, explained the mechanism by which an undeclared key rejects the whole config. The first sentence caused what the second diagnosed, and a reader who stops at the instruction never reaches the contradiction. ## Premise re-taken on today's `origin/main` before any edit All three legs, each with its control lit in the same command: 1. **The two sentences are still there** — at `packages/plugin-gantt/README.md` lines 54–62 on `0c2eb5eee` (the card cited `1db82373f`; the text, not the line number, is the anchor). Both quoted verbatim in the card matched byte for byte. 2. **`README.md` really is in `files[]`** — `["dist", "README.md", "CHANGELOG.md", "LICENSE"]`. ⭐ Control in the same reading: `ROADMAP.md` sits in the same directory, has the same extension, and is **not** in `files[]` — so the instrument distinguishes shipped from not-shipped rather than saying yes to every `.md` in the folder. `demo/`, `docs/`, `scripts/`, `src/` and the two tsconfigs are likewise absent. 3. **The spec declares a tombstone, not a removal** — objectstack `main` (`cdedfb348`) `packages/spec/src/ui/view.zod.ts:1971` reads `view: retiredKey(NAVIGATION_VIEW_RETIRED)`, and the comment above it says why: the key stays in the shape so `tsc` types it `never` and **a value reaching a parse raises the prescription instead of a bare unrecognized-key report**. That distinction is load-bearing for this repair, and is why the surviving sentence below is not re-pointed at `view`. ## The repair The instruction is gone. In its place is the route the retirement actually leaves standing, carried in from the carrier the card names — `@object-ui/react`'s `useNavigationOverlay` docblock — in the README's own voice, without restating the retirement's reasoning: > *Which* detail layout opens is not authorable here either: assign a `record` page to the object and let `isDefault` pick the one that opens. Page assignment is what resolves a detail layout; this block only decides **how** that detail is surfaced (`mode`, `size`) — `@object-ui/react`'s `useNavigationOverlay` docblock owns that account.⚠️ **Deleting the sentence was not the repair on its own.** It answered a real authoring question, and a reader who loses the answer goes looking and finds the retired key elsewhere. The replacement answers the same question.⚠️ **The second sentence is kept, byte-identical.** «an undeclared key rejects the whole config, so the `mode` beside it never takes effect either» is true and it is the only place this README tells an author why a typo costs them the whole block. Only its line wrapping moved — proved below. It now stands as the generic strictness warning it always was, and stops being a description of what the paragraph above it told the reader to do. ⛔ It is deliberately **not** re-pointed at `view`: after the retirement `view` is a declared tombstone raising a prescription, not an undeclared key, so aiming this sentence at it would describe the failure wrongly. ⛔ **Neither neighbouring carrier is folded in.** `packages/react/src/hooks/useNavigationOverlay.ts` line 31 spells the same shape and the card checked it CLEAN — it is the tombstone's own account of past behaviour, with the replacement stated four lines below it. `packages/types/src/objectql.ts` is objectui#9938's site. Neither is touched by this diff. ## The test is the other half of the deliverable `packages/plugin-gantt/src/readme-navigation-example.test.ts` existed, was green, and would have stayed green through this defect for a structural reason worth stating: it extracts the `json` fence, and **the fence never carried the key**. The instruction lived entirely in prose. So the file now measures the prose as well.⚠️ **Nothing schema-derived can say `view` is retired in this tree.** The pinned `@objectstack/spec@17.4.0` still DECLARES it — measured, not assumed: `NavigationConfigSchema`'s shape reads `["mode","view","preventNavigation","openNewTab","size","width"]` and `safeParse({ mode: 'page', view: 'summary_view' })` succeeds. So `declaredMembers()` reports the key legal, and every schema-derived statement about it would be vacuous here. It is therefore named by hand — exactly as `basePath` already is in this same file — and the empty result carries its own control: - `does not INSTRUCT the retired detail-view key, in prose or fence` — no code span in the section equals `view`, and `summary_view` appears nowhere in it. - `CONTROL: the same detector finds the key in the sentence that carried it` — the same `codeSpans()` is run over the removed sentence, verbatim, and must return `['view']`. If the detector ever stops firing there, the clean section above says nothing at all. - `still answers the question the retired instruction answered` — the section must still name `record` and `isDefault`. These are identifiers, not wording: the sentence may be rewritten freely as long as it still hands the reader the route. ## Evidence **RED before / GREEN after, file state verified in the SAME command as each run:** ``` BEFORE README blob 7461cf5 · vs HEAD: UNMODIFIED instruction grep -c 1 · summary_view 1 · isDefault 0 Test Files 1 failed (1) · Tests 2 failed | 6 passed (8) × does not INSTRUCT the retired detail-view key, in prose or fence expected [ 'view' ] to deeply equal [] × still answers the question the retired instruction answered expected [ 'record', 'isDefault' ] to deeply equal [] ✓ CONTROL: the same detector finds the key in the sentence that carried it AFTER README blob 3695c7b · vs HEAD: MODIFIED-by-this-repair instruction grep -c 0 · summary_view 0 · isDefault 1 Test Files 1 passed (1) · Tests 8 passed (8) ``` The control test passed on **both** runs: the detector was lit before the repair and is still lit after, so the zero is a measurement and not a broken instrument. **The kept sentence is byte-identical, with its own control.** Whitespace-normalised, the substring from `` `navigation` is the spec `` to `owns the member list.` compares EQUAL between `HEAD:packages/plugin-gantt/README.md` and the worktree — while the same instrument applied to the half that *should* have changed reports DIFFERENT. And the same fixed-string grep that finds the removed instruction (`` use the declared `view` member ``) once in the file at HEAD finds it zero times in the repaired file. **No ablation was run, and that is a statement rather than an omission.** Nothing in this diff is a guard whose failure mode needs proving by mutation — the RED-before run already is the "can this fail" leg, taken against the real published text rather than an injected one, and it failed for exactly the two reasons named. ## Gates — re-run as the LAST action before the push Tree verified equal to the pushed commit `9ef577d38`, `git status --porcelain` empty, at the time of the run. Exit codes captured before any pipe. Gates that **do** select a package README (established by reading each script's own surface declaration, ⛔ not assumed): | gate | exit | |---|---| | `check-doc-fence-languages.mjs` | 0 | | `check-doc-snippet-types.mjs` | 0 — 672/672 blocks judged | | `check-doc-component-types.mjs` | 0 | | `check-doc-example-types.mjs` | 0 | | `check-readme-exports.mjs` | 0 — 37 of 40 packages read, 544 self-imports judged, population NOT collapsed | | `check-doc-links.mjs` | 0 — its `SCAN_ROOTS` carries `packages/*/README.md` | | `check-doc-expression-carriage.mjs` | 0 | | `check-doc-example-shared-reader.mjs` | 0 |⚠️ `check-readme-exports.mjs` FAILS by design on an unbuilt tree (`population COLLAPSED -- this run proves nothing`). The green above is from a run after all 39 package `dist/` trees were built; the collapsed run is not being reported as a pass. ⛔ `check-doc-example-ids.mjs` does **not** select package READMEs — its own header rules the population to `content/docs/**` on purpose — so it is not in the list above. Everything else run on the same tree: `check-changeset-presence` 0, `check-changeset-claims` 0, `check-pending-changeset-literals` 0, `check-changeset-fixed` 0, `check-changeset-no-major` 0, `check-control-bytes` 0, `check-new-cross-file-line-citations` 0, `check-test-path-roots` 0, `check-unreferenced-sources` 0, `pnpm --filter @object-ui/plugin-gantt lint` 0 (0 errors, pre-existing warnings only), `pnpm --filter @object-ui/plugin-gantt type-check` 0, and the package suite from the repo root — `pnpm exec vitest run packages/plugin-gantt/` — **69 files, 541 tests, all passing**. Governed-surface guard, asked rather than assumed: `check-governed-queue-guard.mjs --test` over the three changed paths returns **NOT GOVERNED**, with `AGENTS.md` run through the same command as a lit control returning the governed verdict. ## Changeset — decided by running the gate, not from memory `check-changeset-presence.mjs` demanded one, and the file it named is the **test under `src/`**, not the README: that gate subtracts `*.md` from published source on purpose («published-but-not-code sits with the READMEs»), and its header answers a change confined to tests with the empty-frontmatter exemption in one line. So `.changeset/9987-gantt-readme-navigation-view.md` carries an EMPTY frontmatter and says so in prose, including the part that argues the other way — the README genuinely is in the tarball. No runtime behaviour, no export and no published-contract field moved, and every package here is in one fixed group, so the corrected README reaches npm with the next release either way. ⭐ A reviewer who wants this to carry a `patch` can flip it in one line; the reasoning is written down rather than assumed. ## Acceptance notes - **noted, not filed** — `packages/react/src/hooks/useNavigationOverlay.ts` says the key «was removed in `@objectstack/spec` 17.5.0» while objectstack `main` keeps it as a `retiredKey` tombstone. Checked and it is **not** a defect: the spec's own tombstone string opens with those exact words, so the docblock is quoting its source faithfully. Recorded only so nobody re-reads it as drift. Carrier for the retire-vs-remove distinction in this repo: objectui#9684, whose PR is this tree's HEAD commit. - **noted, not filed** — no other published carrier in this repo instructs the retired key. `summary_view` across the tracked tree returns 20 hits and every one of them is a test fixture, a CHANGELOG entry or the retirement's own account (`packages/react`, `packages/types`, `packages/plugin-grid`, `.changeset/9874-…`) — all owned by objectui#9667 / objectstack-ai#9938 / objectstack-ai#10080 / objectstack-ai#9874. Zero hits under `content/docs`, zero in any other `packages/*/README.md`, zero in the root `README.md`; the 20 hits elsewhere are the lit control for that zero. No new card is owed. - Nothing else in the diff's blast radius was touched. The two carriers the card fenced out stay fenced out. --- _Generated by [Claude Code](https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #16885
Clause-②: no
Retires
ListViewSchema.navigation.viewunder ADR-0049 enforce-or-remove, executing themaintainer ruling of 2026-09-13 (director decision batch #126 item 4, comment
5651023571,verbatim 「同意」, option B), with its item 5 superseded by comment
5657440531: thechangeset level is
minor, notmajor.What the key did
navigation.viewwas an unconstrained string whose describe promised "the form view to usefor details". No layer from spec to console ever resolved a view by that name. Its one read in
the shipped console passed the value into the second argument of
onNavigate— the slotthat otherwise carries the navigation-MODE token — so an authored name did not select a view,
it substituted for the mode. A consumer in the same bundle reads that argument against a
closed two-value vocabulary (
edit/view), so any other authored value matched neitherbranch. Declared, consumed, and wrong.
Acceptance criteria — executable, with both controls
PROBE —
navigation: { view: 'summary_view' }is now refused, at the key the author wrote,with the prescription naming page assignment as the route:
LIT CONTROL — the live siblings still parse.
{ mode: 'page' }is accepted, and so are allfive survivors together (
mode,preventNavigation,openNewTab,size,width). A tombstonethat broke its siblings would satisfy the refusal assertion while being a larger bug, and
navigationis one closed shape, so that blast radius is the whole block. Pinned at all threedoors —
ListViewSchema,ObjectListViewSchema, and the flattenedPUT /api/v1/meta/viewoverlay.
DARK CONTROL — the reading used is the regenerated
packages/spec/authorable-surface/ui.jsonui/NavigationConfig:*row set. It returned: 6 rows before, 6 rows after; exactly one linechanged,
ui/NavigationConfig:view->ui/NavigationConfig:view [RETIRED]; the retired keynever appears plain, and no other
ui/NavigationConfig:*row disappeared."ui/NavigationConfig:openNewTab", "ui/NavigationConfig:preventNavigation", "ui/NavigationConfig:size", - "ui/NavigationConfig:view", + "ui/NavigationConfig:view [RETIRED]", "ui/NavigationConfig:width",Gates observed RED before GREEN
A gate never observed failing for this change is not known to be a gate for it.
check:generated(check:docsleg)✗ 1 of 15 artifact(s) stale: content/docs/references/**✓ All 15 generated artifacts are up to date.after--fixregenerated exactly that onecheck:generated(check:api-surfaceleg)✗ ... dist/**/*.d.ts describe DIFFERENT sources than the ones on disk(stale-dist trap, after the test file moved)✓after a real rebuildcheck-adr-0087-registrationdeclares a breaking change (BREAKING) but no adr-0087: disposition marker— proven by committing the changeset with the marker line dropped1 declared-breaking changeset(s), each carrying an ADR-0087 disposition ... registered list-view-navigation-view-retiredThe ablation mutated
packages/spec/src/ui/view.zod.tson disk (tombstone occurrences 1 -> 0,ablation marker 0 -> 1, blob hash moved), ran the pins, then restored under a
trap— restoredblob hash byte-identical to
HEADandgit diff HEADempty. Note the direction: only the threerefusal pins went red; the eight sibling-acceptance assertions stayed green, which is the correct
shape for reverting a tombstone.
Gate results
pnpm --filter @objectstack/spec check:generated— all 15 green, including the four named onthe card:
check:authorable-surface,check:api-surface,check:docs,check:liveness. Workingtree clean afterwards (no regeneration drift).
pnpm --filter @objectstack/spec typecheck— green.pnpm --filter @objectstack/spec test—484 files, 13830 tests, all passed.
pnpm lint(repo-wide,eslint . --no-inline-config) —green, so no narrowing was taken and none is declared.
All 14 source audits
check:generateddeliberately does not run — green.check:skill-examplesfirst reported PREREQUISITE NOT MET (packages/client-react/distunbuiltin a fresh worktree); after building that closure it reads
✅ 258 prose examples type-check across 3 surface(s).node scripts/check-changeset-no-major.mjs— green, nomajorbump.check-widening-tellsdid not recur.node scripts/pm/check-widening-tells.mjs --declaration no --diff <this diff>exits 0:10 changed file(s) — 4 judged against a declared surface (no widening tell), 6 NOT MEASURED. The T1/T2 firing onretiredKey()lines recorded on #17955 and#17300 did not reproduce here, so there is nothing to report as a regression and the
declaration is unchanged, as ruled.
The ADR-0087 disposition is SEMANTIC, deliberately
The ruling asked for a semantic migration entry, and the shape earns it: a mechanical D2 strip
would delete the key without recording which list view lost it, and an author who wrote it
wanted a named detail layout — a want page assignment serves and a stripped key does not record.
So the entry is a D3
SemanticMigration,list-view-navigation-view-retired, and the tombstoneprescription therefore carries no
os migrate metasentence: that sentence is owed only wherea conversion covers the surface (
shared/retired-key.tsmodule docblock; the class pinretired-key-migrate-sentence.test.tsdeliberately judges nothing when the marker is absent).The precedent for registering a retired key with no D2 conversion is
data/AggregationNode:distinctin this same table.Registered as
ui/NavigationConfig:viewinRETIRED_KEYS_BY_MAJOR[18], which also starts itsaging clock. Note for the reviewer: no major-18 semantic entry reaches
spec-changes.jsonordocs/protocol-upgrade-guide.mdyet — measured, not assumed: four sampled major-18 ids(
change-management-family-retired,training-family-retired,scim-provider-object-retired,epoch-instant-keys-renamed) return 0 in both files, while major-17 ids return non-zero and theguide stops at "Protocol 17". This entry behaving the same way is the steady state for the open
window, not a gap.
Liveness ledger — nothing to update, and why
view/list/navigationis oneliverow with no classified children, becausecheck-livenessdrills one level. That is #17424's subject; it is cited here and not fixed, exactly as ruling
item 4 directs. Since the walk never reaches
navigation's children, no ledger row exists forviewto markdead, and none is owed —check:livenessis green with the tombstone in place,with no UNCLASSIFIED row.
packages/spec/liveness/**is untouched (held by #18582).The objectui contract twin is in the SIBLING repo — reported, not touched
Ruling item 3 asked where the contract twins live. They are in
objectui, not here. Thereading:
objectui/packages/types/src/__tests__/view-navigation-config-spec-parity.test.tsat the pinned.objectui-sha(53ded82bf7a494f54e344e19099dbf00854b8694) is the twin. It assertsEqual<ViewNavigationConfig, NavigationConfig>—ViewNavigationConfigis this spec type,re-exported by reference since objectui#4588.
const navigation: ViewNavigationConfig = { view: 'summary_view' };. Once objectui resolves aspec carrying this tombstone, those lines become
tscerrors under that package'stype-check.ViewNavigationConfigoccursonly in
packages/console/CHANGELOG.md(a release-owned file), and the trackedsdui.manifest.jsondeclaresnavigationas a flatobjectinput with no drill intoview,so
check:react-declaration-parityis structurally blind to it. Nothing here to update.The Console Pin Gate is not affected and does not run on this PR. Measured: that job is gated
on the
consolepaths filter (.objectui-shaplus four scripts), none of which this diff touches;and it runs
scripts/build-console.sh— a build, which does not type-check the sibling'stsconfig.test.json. objectui's src at the pinned SHA contains no value write ofnavigation.viewoutside that test file (only docblocks), and reads of.viewstill compileagainst
never | undefined. So this PR does not turnmainred.What is still owed, in the other repository: the twin gains the tombstone pin before objectui
picks up a spec carrying this retirement. Its dependency is
@objectstack/spec: ^17.0.0, so apublished 17.5.0 is in range.
File surface
Every path touched, including the pre-declared OPEN set.
packages/spec/src/ui/view.zod.tsretiredKey()tombstone + its prescription constantpackages/spec/src/migrations/entries/retired-keys/18.ui__NavigationConfig__view.tsRETIRED_KEYS_BY_MAJOR[18]registration (build-schemas gate (b))packages/spec/src/migrations/entries/semantic/18.list-view-navigation-view-retired.tspackages/spec/src/ui/view.test.tspackages/spec/src/migrations/registry.tsgen:migration-registry, never hand-edited.changeset/16885-retire-navigation-view.mdminor,**BREAKING**banner, FROM -> TO, ADR-0087 dispositionpackages/spec/authorable-surface/ui.jsonbuildcontent/docs/references/ui/view.mdxgen:docscontent/docs/references/api/protocol.mdxgen:docs(the inline navigation summary dropsview?: string)content/docs/references/data/object.mdxHeld by other in-flight work and not touched:
packages/spec/scripts/build-schemas.ts(#17969),packages/spec/liveness/**andpackages/spec/scripts/liveness/check-liveness.mts(#18582),packages/spec/src/api/protocol.zod.ts(#18597). Ruling item 2 was already discharged — PR #17796reads
closed, not merged — so the retirement is written fresh.Acceptance notes
Noted, not filed: nothing in the touched files.
One finding to file separately, unrelated to this diff and not fixed here:
pnpm check:cross-package-test-inputsreaches opposite verdicts on an identical source treedepending only on whether
packages/spec/dist/happens to be built — exit 1 with it present(115 entries), exit 0 with the same directory moved aside, same commit, restored afterwards.
The flagged radius is
packages/spec/dist/descended frompackages/cli/test/init-created-files-summary.e2e.test.ts, which this diff does not touch. Nopnpm buildstep precedespnpm check:cross-package-test-inputsinlint.yml'slintjob, soCI always runs it against an unbuilt tree and it passes vacuously — the declaration hole it exists
to catch is structurally invisible on every PR. That is the "a verifier that silently degrades is
worse than no verifier" shape from AGENTS.md Route & surface ownership rule 3.
维护者速读(草稿)
改了什么 ——
ListViewSchema.navigation.view退役:键仍在 shape 里,但变成 ADR-0049 墓碑,写它是
tsc报错、传值进来是带处方的 parse 拒收。处方点名替代路线:给对象发布record页面,用
isDefault决定打开哪一个。同批落地 ADR-0087 的 D3 语义迁移条目、changeset(minor+**BREAKING**)、以及三道门上的拒收 pin。为什么改 —— 这个键承诺"用哪个表单视图打开详情",而从 spec 到 console 没有任何一层按名字解析
视图。它唯一的读取把值塞进了
onNavigate的第二个参数 —— 那个槽位装的是导航模式令牌。所以作者写的视图名不是在选视图,而是在顶替模式;而同一个 bundle 里有消费者拿这个参数去对
edit/view两值词表,任何别的值两个分支都不匹配。仓库内零编写实例,唯一的外部作者已自行删除。维护者 2026-09-13 裁决 B:零拉力即移除。
风险与代价(含回滚) —— 风险低。行为面没有回归可言:这个键从来没选中过任何东西,决定详情怎么
呈现的是
mode和size,两者一字未动,并且有 pin 钉着五个存活键仍然接受。代价是发布面:这是breaking,升级时仍写着该键的元数据会在三道门(含
PUT /api/v1/meta/view)被拒。回滚就是revert 本 PR —— 墓碑、注册条目、changeset 与生成产物都在同一个提交序列里,没有跨仓副作用。
一个待办留在姊妹仓:objectui 的契约孪生 pin 把
{ view: 'summary_view' }写成合法值,需要在objectui 取到带墓碑的 spec 之前补上墓碑 pin。本 PR 不触发 Console Pin Gate,不会让
main变红。席位意见 ——
你要做的 ——
Generated by Claude Code