Conversation
…g performs `NavigationConfigSchema.view` described itself as "Name of the form view to use for details". Measured against spec source and the `objectui` tree this repo pins in `.objectui-sha`, no reader resolves an authored view name: - Its only read is `useNavigationOverlay`, which binds `navigation?.view` and passes that string as the SECOND argument of `onNavigate` — the slot whose other producers are navigation MODE tokens. - The hook also re-exports it on `NavigationOverlayState.view`, and no consumer reads that member, while its siblings on the same object (`width`, `isOverlay`, `mode`, `selectedRecord`) are read at ~20 sites. - Every `formViews` read in that tree is `formViews?.default`; none is keyed by an authored view name. - One shipped consumer types that argument `'view' | 'edit'` and branches on both with no fallback arm, so an authored name is a dead row click. The description now says that and carries the repo's existing `[EXPERIMENTAL — not enforced]` marker. No accept set moves: `view` is still `z.string().optional()` and every document that parsed before parses now. The enforce-or-remove decision (ADR-0049) is deliberately NOT taken here. Generated reference docs regenerated from the corrected description. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
📓 Docs Drift CheckThis PR changes 1 package(s): 1 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 d93e47ef990523b6275afdceeb39854ccfdfd9f5 && git checkout d93e47ef990523b6275afdceeb39854ccfdfd9f5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8fa3fe63d9e8dbbc507c0735d918c99db41df31d bec4767cc78de82366224351b85042bf546b27f2 && git checkout -B drift-repro 8fa3fe63d9e8dbbc507c0735d918c99db41df31d && git merge --no-ff bec4767cc78de82366224351b85042bf546b27f2
node scripts/docs-audit/affected-docs.mjs --json 8fa3fe63d9e8dbbc507c0735d918c99db41df31d
|
Third instance, and the first where the re-declared line is a
|
| corpus | view: z.string().optional() |
lit control preventNavigation |
dark control |
|---|---|---|---|
origin/main @ 8fa3fe63d9 |
2 | 1 | 0 |
| the PR's branch head | 2 | — | — |
⇒ same count on both sides. On origin/main the two sites are :1146 (a tab schema) and :1599 (NavigationConfigSchema, under its /** Target View Config */ comment). The PR changed only the .describe() string on that second line, which pushed it to :1615 — and the diff therefore re-emits the whole view: … line as an addition.
⭐ This instance sharpens the shape. The two earlier ones re-declared a key while restructuring around it. Here the diff does nothing but correct the prose inside an existing key's .describe() — arguably the most common edit anyone makes to a Zod schema — and T1 reads it as the accept set gaining a spelling. ⇒ the tell fires on a class of change that can never widen anything, because a .describe() string is not an accept set.
⛔ What this seat will not do
The only sanctioned clear is flipping Clause-②: no → yes. ⛔ Refused, for the third time: it would record on a permanent carrier that this PR widened a published accept set, which is false and measurably so. A declaration is a claim about the tree, not a key that opens a gate.
⇒ PR #17796 stays draft and unlanded until this card is fixed. Same disposition as #17157 / PR #17638.
domain:spec execution seat · session_01MkQhmuuJAVDjmeWNixwDDH · readings taken 2026-09-12T05:31Z on origin/main @ 8fa3fe63d9
Generated by Claude Code
…bject-calendar prescription already names (objectstack-ai#17877) Fixes objectstack-ai#17054 `CalendarConfigSchema` now declares **`allDayField`**, the fifth field binding on a calendar config. ## The two sentences, and which one was wrong The `object-calendar` door refuses a flat `allDayField` and prescribes, verbatim from its own diagnostic: > Write this as a key of the `calendar` config object instead — `calendar: { startDateField, endDateField, titleField, colorField, allDayField }`. `CalendarConfigSchema` was a `strictObject` of exactly four keys and refused that shape by name. **The round measured which half was wrong rather than picking the convenient one, and the answer is (a) — the schema was missing a key that is honoured.** It is not (b): trimming the prescription would leave a shipped, honoured capability with no protocol carrier. The evidence, read at the objectui pin this repo builds against (`.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694`, read with `git show PIN:path`, not at that checkout's HEAD): - `packages/plugin-list/src/ListView.tsx` — `collectViewFields` reads `v.allDayField` off `schema.calendar` and `schema.options.calendar` at two sites, feeding the `$select` projection and the `$expand` set. The authored nested key already changes what the server is asked for. - The same file's `case 'calendar':` branch spreads `...(schema.calendar || {})` onto the `object-calendar` node, so the nested key reaches the block. - `packages/plugin-calendar/src/ObjectCalendar.tsx` — `getCalendarConfig` resolves it into the calendar config. - `packages/app-shell/src/views/ObjectView.tsx` — the dev-mode Spec Compliance warning lists `allDayField` among the flat keys an author must move under `viewDef.calendar`: a third face prescribing the nested spelling. - `packages/types/src/zod/objectql.zod.ts` — the mirror keeps `.passthrough()` and names this key as its reason: *"the renderers grow config knobs ahead of the protocol (calendar's `allDayField`, for one), and stripping them here would silently disable a shipped capability."* - Post-pin, on objectui `main`, the renderer makes it load-bearing: `allDay: allDayField ? Boolean(record[allDayField]) : !endDate`. ⭐ A widening is not made acceptable by the diagnostic having promised it. This one is right because the renderer honours the key — the prescription merely happened to be the accurate half. **The countervailing reading, stated plainly.** objectui `main` carries a comment declaring `allDayField` objectui-LOCAL, in the same class as its sanctioned `defaultView`, and concluding "honouring `allDayField` widens no accept set". That is a true statement about what objectui needed in order to honour it, and it does not bind what the protocol may declare. The two keys are not the same class from this side: `defaultView` is the renderer's initial view mode, a UI preference that already has a declared home as an `object-calendar` component prop; `allDayField` is a field binding, the same kind as its four neighbours, and it had no home at all. `defaultView` stays refused on this config, pinned. ## A correction to the card's framing, measured The card reads as though the prescribed shape is refused at the door that printed the prescription. It is not. `ObjectCalendarPropsSchema.calendar` is `z.unknown()`, so the block accepts `calendar: { …, allDayField }` today. The second refusal lands one door over, on **stored view metadata** — `ListViewShapeSchema.calendar` is `CalendarConfigSchema` — which is how calendars are actually authored in this product. The trap is real; the two doors are just not the same door. Both readings are in the before/after table. ## Measured against the BUILT dist, before and after Build first and confirm both passes finished (`check-dts-emitted: 34/34 declared declaration file(s) present`), then parse through the package's own `./ui` export. | input | door | before | after | |---|---|---|---| | `{ objectName, allDayField }` flat | `object-calendar` | REFUSED `unrecognized_keys` keys=`["allDayField"]` | REFUSED, unchanged | | `calendar: { four, allDayField }` | `object-calendar` | ACCEPTED | ACCEPTED | | `{ four, allDayField }` | `CalendarConfigSchema` | REFUSED `unrecognized_keys` keys=`["allDayField"]` | ACCEPTED | | `calendar: { four, allDayField }` | `ListViewSchema` | REFUSED `unrecognized_keys` at `path: ["calendar"]` | ACCEPTED | | `{ four }` | `CalendarConfigSchema` | ACCEPTED (positive control) | ACCEPTED | | `{ four, bogusKeyXy }` | `CalendarConfigSchema` | REFUSED (negative control) | REFUSED | The exact refusal texts, before: - flat, on `object-calendar`: `Unrecognized key(s) on this \`object-calendar\`: \`allDayField\`.` followed by the prescription quoted above. - nested, on the config: `Unrecognized key(s) on this calendar configuration: \`allDayField\`. Until these shapes were closed an unknown key was dropped silently — the view still rendered, without whatever the key was meant to configure.` After, the first is byte-identical and the second is gone — replaced by acceptance. The bogus-key control still produces that second text verbatim with `keys=["bogusKeyXy"]`, which is what proves the message did not change, only the membership. ## Pins, both directions `packages/spec/src/ui/calendar-config-allday-prescription-17054.test.ts`, 9 cases. They import `./view.zod` and `./component.zod` — **`src/`, not `dist/`**, so no rebuild leg is needed for the ablation, and that is measured rather than assumed. **Accepted:** the prescribed shape at the config schema; the same shape through the stored-view door where the second refusal used to land; the same view without the key as a control. ⭐ The lead pin is written on the defect CLASS, not on one key: it reads the key list out of the `calendar: { … }` shape the runtime's own prescription prints and asks the config schema to accept each name, with a floor on the extracted list so an empty extraction cannot make it vacuously true. Any future diagnostic that names a non-member goes red here, including a key nobody has thought of yet. **Still refused — what the widening did NOT cost:** the flat `allDayField` on `object-calendar` (one key per concept, and the refusal still carries the prescription); `defaultView` on the config, so the opening is exactly one key wide; an unknown key, in the same message shape, at the config and at `path: ["calendar"]`; and `startDateField` is still required, so `allDayField` alone is not a calendar binding. ## Ablation Mutation: rename the declaration to `allDayFieldAblated` in `packages/spec/src/ui/view.zod.ts`. Absolute paths, `trap '…' EXIT INT TERM`. On-disk proof read FIRST, before the run: declaration occurrences `1 → 0`, injected text `0 → 1`, and the file's `git hash-object` moving `3ecc02a254265786fc29c146408479ed072ee462 → 560dc1d3299460e582e04d0a727a89600e6d23ba`. The run then aborts itself if the injected text is not present exactly once. Predicted direction: RED. Observed: `3 failed | 6 passed (9)`, exit 1 — exactly the three acceptance pins, with the lead pin failing on its own sentence: *"the prescription names `allDayField`, which CalendarConfigSchema refuses"*. GREEN after restore: `9 passed (9)`, exit 0. Restore proven by hash, not by an exit code: `git checkout HEAD -- ABSOLUTE_PATH` (never the bare form, which reads the index), restored hash `3ecc02a254265786fc29c146408479ed072ee462` equal to the HEAD blob, with an empty-hash guard treating a missing read as FAILURE, and `git diff HEAD` empty. ## Changeset `.changeset/17054-calendar-config-all-day-field.md`, grade **`minor`** — a published accept set widens, and `minor` is the floor for this class. Not `skip-changeset`, measured rather than assumed, with `npm pack --dry-run --json` after a build and controls in both directions over the packed file list (2012 files): - subject `allDayField` → **52** published files. - positive control `startDateField` (a sibling key that must publish) → **55**. - negative control `bogusKeyXy`, which lives only in the new test file → **0**. - the new test file is **absent** from the packed list, and 0 `*.test.ts` files publish at all. -⚠️ `packages/spec` also ships `src/**/*.zod.ts` **as source** and its tsup does not strip comments, so a source comment is published text: the probe phrase from the new TSDoc block appears in **23** published files. Controls were picked accordingly. Purely additive — nothing that parsed before is refused now, and no key is renamed or removed, so there is no ADR-0087 disposition to declare. ## Verification Head `7803e3d6fd`. `origin/main` merged via `scripts/pm/os-regen-merge.sh` before opening; main brought driver-sql and lint changes only, no `packages/spec`, and no contact with PR objectstack-ai#17796's `ui/view.zod.ts` hunks — that PR is not addressed here and remains open. Every number below is from the final head, after the merge. - `pnpm --filter @objectstack/spec test` (`--project local`) :: exit 0 — 473 files / 13443 tests, 0 skipped. - `pnpm --filter @objectstack/spec test:repo` (`--project repo`, the cross-corpus scanners) :: exit 0 — 30 files / 520 tests. ⭐ Run separately on purpose: `test` is not the whole suite. - `pnpm --filter @objectstack/spec typecheck` :: exit 0 — including `check:test-typecheck` (shrink-only ledger held). - `pnpm --filter @objectstack/spec check:generated` :: exit 0 — all 15 generated artifacts up to date; `authorable-surface/ui.json` gained exactly one line, `ui/CalendarConfig:allDayField`, and `authorable-surface.base.json` was not touched. - `pnpm lint` (`eslint . --no-inline-config`, repo-wide) :: exit 0 — no narrowing claimed. - Derived families via `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack`, re-derived on the merged head and identical to the pre-merge derivation: **106 derived, 104 run green, 2 NOT MEASURED, 0 unrun** (`--ran` reconciliation exits 0). Exit codes captured before any pipe. - ⛔ **NOT MEASURED, declared, not green:** `pnpm check:dual-build-cjs-loads` and `pnpm check:type-check-debt`, both exit **3** — PREREQUISITE NOT MET. Each needs every workspace package built (`turbo run build --filter='./packages/*' --filter='./packages/*/*'`), which is the whole-farm run CI owns; nothing about them is answerable from a spec-only closure. This is a declared narrowing, not a skipped gate, and their verdicts are CI's. - Control bytes: `pnpm check:nul-bytes` :: exit 0, plus a direct scan of all seven changed paths for the wider control-byte class — no match, grep exit 1. Clause-②: yes — this widens a published accept set, so the round's measurement agrees with the value declared at dispatch. `needs:contract-review` rides on both carriers and this does not enqueue without an at-tier verdict on the head that lands. ## 验收备注 Out of scope, noted and not filed — each with its carrier named: - `ObjectCalendarPropsSchema.calendar` is `z.unknown()`, so the component door validates nothing about the config it names in its own `.describe()`. Tightening it to `CalendarConfigSchema` would narrow a published accept set and needs its own ruling; it is not a defect, it is an unbuilt door. Carrier: whoever next converges the component-door configs. - objectui's mirror comment and its `ObjectCalendar` docblock both state that `allDayField` is not a spec key. Once this lands, both are stale. objectui#8831 is already the declared follow-up and triage named it, so this is not a new card. Carrier: objectui#8831. - The objectui-side `list-view-spec-parity` pin lists `defaultView` as the only sanctioned local key on the calendar config; the mirror derives from the spec schema, so it picks up this key without an edit. Nothing to do, recorded so the next reader does not go looking. Carrier: objectui#8831. --- _Generated by [Claude Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… the permission-set route (objectstack-ai#17835) Fixes objectstack-ai#16929 Executes **Ruling A** — director seat, decision batch objectstack-ai#121 item 2, comment `5644017943` (2026-09-12), carrying the maintainer's 「同意」. Nothing here re-opens a question that ruling settled; alternatives B / C / E are not revisited. **Clause-②: no** — this is a removal / narrowing. Nothing is widened, so no `needs:contract-review`. PR objectstack-ai#17401's landed half (the two `guidance` prescriptions stopping naming the key) **stands and is not redone**: both prescriptions on `origin/main` already omit it, and this branch leaves their text alone. --- ## The six ruled items, one by one | # | ruled | done | | --- | --- | --- | | 1 | `page.zod.ts`: `assignedProfiles` removed; `profiles` / `assignedTo` become refusals naming the permission-set route; the two guidance strings rewritten | ✅ — with one **route correction**, below | | 2 | `page.form.ts` helpText and its four locale bundles removed | ✅ | | 3 | the three false records corrected in the same PR | ✅ — with one **relocation**, below | | 4 | `major` changeset + an ADR-0087 semantic migration entry; key stripped on `migrate meta --stored` with a structured TODO | ✅ substance;⚠️ the `major` **grade** is refused by a standing repo-wide gate — see *One ruled item the tree refuses* | | 5 | PR objectstack-ai#17401's landed half stands | ✅ untouched | | 6 | `Clause-②: no` | ✅ declared above in the fixed spelling | ### Item 1 — route correction: a `retiredKey()` tombstone, not a bare shape deletion The retirement playbook offers two routes and keys the choice on whether the schema is strict: `retiredKey()` for a non-strict schema, delete-plus-`guidance` for a strict one. `PageSchema` is a `strictObject`, so the first attempt took the strict route — and the build refused it: ``` ❌ 1 authorable key(s) disappeared from the contract: - ui/Page:assignedProfiles ``` `scripts/build-schemas.ts` check (a) is fatal for **any** key that leaves an emitting def, strictness notwithstanding, and check (c) then ratchets a baseline deletion against the merge base on one of three proofs — aged-out tombstone, def unreachable from the metadata-type roots, or whole def gone. `ui/Page` is reachable from the `page` root and keeps emitting, so none holds. The route the tree actually permits here is the tombstone, which is also what the sibling `view.pageName` retirement took two days ago. This is not a softening of the ruling. The key is **unwritable**: `tsc` types it `never`, and a value reaching a parse raises the prescription. It simply stays in the *walked shape*, which is why its liveness row stays (as `dead`) and why the authorable-surface baseline marks it `[RETIRED]` instead of losing the line. ### Item 3 — one of the three false records is not where the ruling says it is Every `path:line` was re-derived by sentence rather than trusted. Two of the three resolved as written; the second did not. | ruled location | what is actually there | | --- | --- | | `packages/spec/liveness/page.json` — `live` citing a non-existent objectui bridge | ✅ **found**, corrected | | `packages/spec/liveness/view.json:125` — the "page audience gate" justification | ❌ **not there any more.** `git grep` over that file finds zero hits for `audience`, `assignedProfiles` or `page audience` (lit control: `pageName` reads 3 lines in the same file; dark control 0). The objectstack-ai#17063 `pageName` retirement rewrote that row on 2026-09-10 and the justification left with it. The same assertion is live at **`packages/spec/src/api/protocol.zod.ts`** (`SearchAllPageHitSchema`'s TSDoc) — that is the one corrected here | | `packages/metadata-protocol/src/protocol.ts` — "enforced at page render" | ✅ **found**, corrected | So the count is still three, and all three assertions are gone; one of them lives at a different address than the ruling recorded. **Before → after, and what makes the new text true.** 1. `packages/spec/liveness/page.json` — was `status: "live"`, note: *"profile-scoped page audience; objectui bridges it (react/src/spec-bridge/bridges/page.ts) to PageLayout.assignedProfiles."* Now `status: "dead"` with a `verifiedAt` and a note recording that the cited path does not exist in objectui (nor does any `spec-bridge` directory), while two sibling objectui citations in the same file resolve. True because the key is now a tombstone and the ledger's own route table says a tombstoned key keeps its row with a `dead` verdict. 2. `packages/spec/src/api/protocol.zod.ts` — was *"where the page's own audience gate (`assignedProfiles`) applies unchanged"*. Now states that a page has no audience gate of its own, that the key which read as one was removed precisely because nothing enforced it, and that what protects a page is the permission sets on the data it shows. True because the key no longer exists and never had a reader. 3. `packages/metadata-protocol/src/protocol.ts` — was *"is enforced where it is enforced now, at page render"*. Now states the opposite and keeps the delegation posture the sweep rests on, which never depended on the key. True by the cross-repo measurement the card and triage both took.⚠️ `packages/metadata-protocol/**` is `domain:engine`'s lane. It is here **only** because ruling item 3 puts all three records in one PR, and exactly one sentence is touched. --- ## Measurement ### The removal is real, and it reaches the built artifact Probed against the **built** `packages/spec/dist/ui/index.mjs` before and after, same script both times. **Before** (`origin/main` content, built): ``` A assignedProfiles :: ACCEPTED :: keys=assignedProfiles,isDefault,kind,label,name,regions,template,type :: value=["admin"] B profiles(alias) :: REFUSED :: code=unrecognized_keys :: path=[] :: Unrecognized key(s) on this page: `profiles`. Did you mean `profiles` → `assignedProfiles`? … C assignedTo :: REFUSED :: code=unrecognized_keys :: path=[] :: Unrecognized key(s) on this page: `assignedTo`. Did you mean `assignedTo` → `assignedProfiles`? … D LIT-CONTROL icon :: ACCEPTED E DARK-CONTROL :: REFUSED :: code=unrecognized_keys ``` **After** (this branch, rebuilt): ``` A assignedProfiles :: REFUSED :: code=invalid_type :: path=["assignedProfiles"] :: `page.assignedProfiles` was removed in @objectstack/spec 18 (ADR-0090 D2, ADR-0049 enforce-or-remove) — it was named for the Profile concept ADR-0090 D2 deleted, and it gated nothing: no renderer, route or metadata read door ever read the key, so a page that "assigned profiles" stayed open to every caller who could reach it. Delete the key. Page audience is the permission set's: gate the DATA the page shows with the object's permission sets, and bind those sets to people through positions (`sys_position_permission_set`) — those are the checks the runtime actually runs. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. B profiles(alias) :: REFUSED :: code=unrecognized_keys :: path=[] :: Unrecognized key(s) on this page: `profiles`. • `profiles` / `assignedTo` are not page keys (ADR-0090 D2: no Profile concept). Page audience is the permission set's — gate the DATA the page shows with the object's permission sets, and bind those sets to people through positions (`sys_position_permission_set`), never on the page itself. … C assignedTo :: REFUSED :: same sentence D LIT-CONTROL icon :: ACCEPTED (unchanged) E DARK-CONTROL :: REFUSED :: code=unrecognized_keys (unchanged — the generic report, no rename offered) ``` That is the actual refusal text, not a claim that one exists. Note the refusal **moved channel**: `unrecognized_keys` at the page → `invalid_type` located at `["assignedProfiles"]`, which is what a `z.never()` tombstone produces. ### The alias refusals point somewhere true — read, not inherited A previous round on this card asserted that an alias table runs only from the `unrecognized_keys` path. I re-read the source rather than inherit it. `packages/spec/src/shared/strict-object.ts`'s own docblock puts it in terms — *"`aliases` … is consulted BEFORE the distance fallback"* inside `strictUnknownKeyError` — and `shared/alias-integrity.test.ts` states the mechanism as the premise of the gate it implements: *"an alias only ever runs from the `unrecognized_keys` path, so a key the shape declares can never reach it."* **The before-probe is the direct evidence**: rows B and C above are `REFUSED` with `code=unrecognized_keys` **before** any change. So `profiles` and `assignedTo` were never in the accept set, the alias only decorated the rejection, and **deleting or repointing those entries narrows nothing** — same code, same path, different text. My own reading agrees with the earlier round's. A second consequence made the entries impossible to keep: `alias-integrity.test.ts` asserts that an alias's *target* is a key the shape accepts. Once the key is a tombstone, `profiles: 'assignedProfiles'` would point at a key the schema cannot accept — the ledger's finding-7 shape. They had to become guidance. ### The migration entry actually fires Driven over a stored page carrying the key, against the built artifact: ``` === 1. `migrate meta` chain replay (authored source, 17 -> 18) === stack after :: {"pages":[{"name":"deal_desk","label":"Deal Desk","isDefault":false}]} key still present? false applied :: [{"toMajor":18,"conversionId":"page-assigned-profiles-removed","surface":"page.assignedProfiles", "from":"assignedProfiles","to":"(removed)","path":"pages[0].assignedProfiles"}] structured TODO present? true TODO.toMajor = 18 TODO.surface = `page.assignedProfiles` — the per-page audience list (REMOVED) === 2. stored-row rehydration (the `migrate meta --stored` seam) === stored row after :: {"name":"deal_desk","label":"Deal Desk","isDefault":false} key still present? false === 3. CONTROLS === LIT : a page WITHOUT the key is untouched :: {"name":"team_home","label":"Team Home","isDefault":true} DARK: a fabricated key is NOT stripped :: {"name":"p","label":"P","zzqqNeverDeclared":1} ``` The key goes on both seams and the structured TODO appears. The strip is deliberately paired with a D3 semantic entry rather than left to read as "handled": which permission set a given profile name corresponds to is a judgement no walker can derive. ### Ablation — the three new pins, RED before and GREEN after One mutation leg restores both halves of the fix (the live key, and the two alias entries in place of the guidance ones). ``` HEAD blob for packages/spec/src/ui/page.zod.ts = 388c9ee leg 0 GREEN baseline Tests 83 passed (83) leg 1 on-disk proof of the mutation tombstone line 1 -> 0 live-key line 0 -> 1 guidance entries 2 -> 0 blob hash 388c9ee… -> 966b7ef6… (changed, so the write landed) RED × refuses `assignedProfiles` at the tombstone and prescribes the permission-set route × answers `profiles:` with the permission-set route, not the retired key × answers `assignedTo:` with the same sentence as `profiles:` Tests 3 failed | 80 passed (83) ← exactly the three new pins, nothing else leg 2 restore (git checkout HEAD -- <abs path>; trap on EXIT INT TERM) restored blob hash = 388c9ee == HEAD blob git diff HEAD :: '' (empty) git status --porcelain :: '' (empty — index AND tree) GREEN Tests 83 passed (83) ``` Restore is proven by the blob hash against the HEAD blob and by an empty `git diff HEAD` + empty `git status --porcelain`, never by an exit code. No rebuild leg is needed and none is claimed: `page.test.ts` imports `./page.zod` — a relative source path inside the same package — so this ablation never resolves through `dist`.⚠️ One reading in that run is **NOT MEASURED**: the `alias line` occurrence count printed empty because the `grep -c` was mis-quoted inside a double-quoted `$(...)`. The mutation is nonetheless established by the two counts that did fire and by the blob-hash change; a clean re-grep on the shipped file reads **0** alias lines (lit control `aliases: {` = 6, dark control = 0). ### Changeset — it reaches a published `dist`, with both controls Built **first**, then measured (`npm pack --dry-run --json` before a build reads `dist` as empty — that trap is avoided). - **Positive control** — the new prescription is in **18** packed `dist/` files and in `dist/index.d.ts` (3 hits), so a consumer's `tsc` and runtime both see it. `src/ui/page.zod.ts` is itself in the packed list. - **Negative control** — `src/ui/page.test.ts` and `scripts/build-schemas.ts` are **NOT** in the packed list (2012 files packed; 216 `dist/`, 201 `src/`, 38 `liveness/`). -⚠️ A first negative control **failed and is reported rather than swapped out quietly**: a source-only `//` comment in `page.zod.ts` reads 18 hits in `dist/`. `packages/spec`'s tsup build does **not** strip comments, so for this package a comment inside a shipped module is published text. Useful, and worth knowing before writing one. - `packages/metadata-protocol` correctly carries **no** changeset entry: its `files` is `["dist","README.md","CHANGELOG.md"]`, the corrected sentence is an inline body comment, and it reads **0** files in that package's `dist` (lit control `CLOSURE_CONTEXT_KEY_BY_TYPE` = 2, so the instrument fired). --- ##⚠️ One ruled item the tree refuses as written: the `major` grade Ruling item 4 fixes the changeset at `major`. The changeset in this PR **is** `major`, as ruled. A standing repo-wide gate refuses it: ``` $ node scripts/check-changeset-no-major.mjs --base origin/main EXIT=1 ⛔ This PR introduces changeset(s) that declare a `major` bump. .changeset/16929-page-assigned-profiles-removed.md - @objectstack/spec: major Every publishable package is in the Changesets `fixed` (lockstep) group, so a single `major` promotes the ENTIRE monorepo to a new major version. During the launch window ship breaking changes as `minor` instead … If a whole-stack major release is genuinely intended, add the `allow-major` label to the PR to skip this check. ``` So the ruling **can** be executed literally — the gate names its own escape — but the escape asserts *"a whole-stack major release is genuinely intended"*, and that is a release-shaping claim well beyond this card: - every publishable package moves (~70, in lockstep) from `17.x` to `18.0.0`; - `scripts/sync-protocol-version.mjs` keys `PROTOCOL_VERSION` off the spec package major, so the bump also flips the handshake to `18` and activates the **24 other** `toMajor: 18` conversions already waiting in the registry. ⛔ I have therefore **not** applied `allow-major`, and I have **not** silently regraded the changeset to `minor`. The grade stands as ruled and the gate stands red, with its reading recorded here. The remedy is one line and it belongs to the seat or the maintainer: - **keep `major`** ⇒ add the `allow-major` label, and this PR is the one that cuts `18.0.0`; or - **regrade to `minor`** ⇒ the launch-window convention, which the gate's own prose says is carried instead by the **BREAKING** banner and the ADR-0087 disposition — both of which this changeset already has. It is also what the sibling `view-page-mount` retirement (an identical-shape breaking removal, two days ago) did. --- ## The hot registry file — what was taken, and against which tip `packages/spec/src/migrations/registry.ts` is contended by objectstack-ai#17792, objectstack-ai#17638 and objectstack-ai#17635. **No entry number was taken, because there are none.** The contended regions of that file are *generated*: `src/migrations/entries/` holds one file per entry, filename derived from the id, no index, concatenated by `gen:migration-registry` and sorted by id (`entries/README.md` is the authority). This PR adds two such files and **never edits between the markers**: - `entries/retired-keys/18.ui__Page__assignedProfiles.ts` → `RETIRED_KEYS_BY_MAJOR[18]` - `entries/semantic/18.page-assigned-profiles-audience-to-permission-set.ts` → `MIGRATIONS_BY_MAJOR[18].semantic` Two hand-edited lines remain, both appends at the tail and neither renumbering anything: `step18.conversionIds` gains `'page-assigned-profiles-removed'` after `'view-page-mount-removed'`, and `step18.rationale` gains a paragraph. ⛔ No other PR's entry is renumbered or reordered. **The merge, and what it actually collided with.** `origin/main` was merged immediately before opening this PR: merge commit `8774a8c115`, parents `3a1be112ff` (this branch) + **`c1078a5591`** (`origin/main` at that moment). The collision was real but it was **not** a number — it was `step18.rationale`, where objectstack-ai#17260's landed `object-kanban.quickAdd` retirement and this card had each appended a paragraph to the same prose field. Resolved semantically, **both intents kept**, main's paragraph first: - neither paragraph was dropped, shortened or reordered; - the merge also silently dropped main's `"ui/ObjectKanbanProps:quickAdd [RETIRED]"` baseline marker — the os-regen driver's documented exit-0-while-dropping-a-side behaviour. `scripts/pm/os-regen-merge.sh`'s order was followed (merge committed first, regeneration as its own commit, `pre-commit` deferral discharged), and the regeneration put that marker back. That restoration is visible as its own commit; - ⛔ nothing belonging to objectstack-ai#17792, objectstack-ai#17638 or objectstack-ai#17635 was renumbered, reordered or touched. Everything in *Verification* below was re-run **on the merged tree**, at `626ca34833`. --- ## ⭐ T1 of objectstack-ai#17618 — known false positive, recorded in advance This PR declares `Clause-②: no`, which is the only condition under which objectstack-ai#17618's T1 leg fires, and T1 reads a **re-declared** key line as a new key (three false positives to date, most recently objectstack-ai#17796 on a `.describe()` change). This diff re-declares `assignedProfiles` on its existing key line and rewrites alias/guidance entries on existing lines, so it is squarely in T1's blast radius. If T1 reds on a line this PR did not add, **that is the false positive** — the reading goes here, and ⛔ `no` is not flipped to `yes` to clear a gate: the declared value is the criterion, not the diff shape. --- ## Verification Exit codes captured before any pipe. Heavy runs went through `scripts/pm/os-verify-lock.sh` with `OS_VERIFY_LOCK_SLOT=issue-16929`; the verdict quoted is the wrapper's own `VERDICT` line, or a per-part `echo "$?"` marker where parts were sequenced. All of the following at **`626ca34833`**, the merged head. | run | result | | --- | --- | | `pnpm --filter @objectstack/spec build` | `VERDICT command-exit 0` | | `pnpm --filter @objectstack/spec check:generated` | **all 15 artifacts up to date** on the merged tree (an earlier pre-merge run found 2 stale — `content/docs/references/**` and `liveness/state-counts.md` — regenerated with `--fix`) | | spec `test` + `typecheck` + platform-objects `test` + metadata-protocol `typecheck`, joined with `&&` so one verdict covers all four | `VERDICT command-exit 0` — spec: Test Files 471 passed (471), Tests **13375 passed (13375)**; platform-objects: 39 files, 561 tests | | `npx eslint . --no-inline-config --format json` — the **full repo union, no narrowing claimed** | `ESLINT_EXIT=0` — **6647** files received per `--format json`, **0 errors, 0 warnings** | | `node scripts/pm/dispatch-gates.mjs --ran … --repo objectstack-ai/objectstack` | `EXIT=0` — **114 derived, 112 run, 2 NOT-MEASURED, 0 UNRUN** | | `node scripts/check-i18n-bundles.mjs --write` | first run `EXIT=3` = **PREREQUISITE NOT MET = NOT MEASURED** (the built CLI closure was absent); the closure was built (`VERDICT command-exit 0`, 57 tasks) and the re-run is `EXIT=0`, all nine bundles regenerated | | `pnpm check:nul-bytes` | `EXIT=0` — 8463 files scanned, no raw control bytes. Own sweep over the 20 changed paths with `grep -naP` finds none; lit control on an injected byte fires | | `node scripts/check-changeset-no-major.mjs --base origin/main` | `EXIT=1` — **deliberate, see above** | The two NOT-MEASURED families both exited **3 = PREREQUISITE NOT MET**, which is neither a pass nor a finding, and neither is counted green here: - `pnpm check:dual-build-cjs-loads` — reads built output for ten packages this worktree never built (`@objectstack/studio`, `client-react`, four connectors, …); - `pnpm check:type-check-debt` — wants `turbo run build --filter='./packages/*' --filter='./packages/*/*'` first, and its own text says ⛔ no ledger number may be raised on a run that measured nothing. CI's **Build Core** supplies both. Two families that first read red on a stale build were re-run after rebuilding and are green: `check:skill-examples` (`exit 0`, 258 prose examples across 3 surfaces — it had refused on an unbuilt `packages/client-react/dist`, which was then built) and `check:react-declaration-parity`, run **as CI runs it** with `MANIFEST="$PWD/sdui.manifest.json" … --baseline react-declaration-parity.baseline.json --strict` (`exit 0`, *"no new DECLARATION divergence vs accepted baseline"*) — its own refusal text says a complete local run is available from the checked-in root manifest and ⛔ must not be reported as NOT MEASURED. One earlier reading is withdrawn rather than quietly dropped: `pnpm check:query-options-erasure` first read `exit 2`. That run happened while `src/migrations/registry.ts` briefly held a merge-resolution syntax error, and the gate parses that very file. Re-run on the fixed tree it is `exit 0` — *"ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new"*. Regenerated artifacts, each reviewed rather than waved through: - `authorable-surface/ui.json` — `"ui/Page:assignedProfiles"` → `"ui/Page:assignedProfiles [RETIRED]"` - `liveness/state-counts.md` — `page` 23 live → 22 live + 1 dead (total 24 unchanged); repo total 850→849 live, 93→94 dead - `content/docs/references/ui/page.mdx` — the row's type becomes `never` and carries the `[REMOVED]` prescription - the four `*.metadata-forms.generated.ts` bundles lose the `assignedProfiles` block (zh-CN 「指定配置文件 / 此页面对哪些 Profile 可用」, ja-JP「割り当てプロファイル」, es-ES "Perfiles asignados", en) Commit messages were swept for a card relation and a model identifier, each stem counted separately: `fix` / `fixes` / `fixed` / `close` / `closes` / `closed` / `resolve` / `resolves` / `resolved` / `part of` / `refs` and `#`+digits all read **0**; `Opus` / `Sonnet` / `Haiku` read 0. Lit control `Claude` reads 8, so the sweep reaches. The only `claude-` / `anthropic` hits are the mandated trailer pair. ## 验收备注 - `docs/adr/0010-nl-to-flow-authoring.md` mentions `assignedProfiles` in a 2026-era open question. Untouched on purpose: `docs/adr/**` is a governed surface, and one path hit would make this whole diff governed and unmergeable by the queue. Noted, not filed. - `docs/audits/2026-06-pageschema-property-liveness.md` and `docs/audits/2026-06-security-identity-property-liveness.md` both name the key. Untouched: they are dated audit records, and editing them would falsify the record rather than correct it. - `.changeset/page-guidance-stops-prescribing-assignedprofiles.md` (PR objectstack-ai#17401's, still pending) states *"`assignedProfiles` remains an authorable key with its declaration untouched"*. Accurate about that PR; superseded by this one inside the same unreleased window. This changeset says so rather than editing another PR's. Authored by the `domain:spec` execution seat's `os-dev` round, session `session_01MkQhmuuJAVDjmeWNixwDDH`, on branch `claude/issue-16929-assignedprofiles-removal` at `626ca34833`. --- _Generated by [Claude Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…code, and pins the re-declared key (objectstack-ai#18539) Fixes objectstack-ai#17848 Clause-②: no `REFUSAL_SENTENCE` in `scripts/pm/check-widening-tells.mjs` now names the two doors that actually move its exit code, and `--self-test` pins the re-declared-key shape in both directions. One file changed. ⛔ No exit code moves. ## First act: the card's own specimens, re-measured — half one did not reproduce The card's table was taken 2026-09-12T11:5xZ. Re-run today against all three PR diffs it names (fetched as the PR's own diff and fed to `--declaration no --diff`), this matcher reports **no widening tell on any of them**: | specimen | judged files | tells | exit | |:--|--:|--:|--:| | PR objectstack-ai#17638 — `system/cache.zod.ts:197` | 3 of 7 (4 NOT MEASURED) | 0 | 0 | | PR objectstack-ai#17796 — `ui/view.zod.ts:1615` | 1 of 3 (2 NOT MEASURED) | 0 | 0 | | PR objectstack-ai#17846 — seven `filter:` doors | 3 of 5 (2 NOT MEASURED) | 0 | 0 |⚠️ Every zero above is bracketed by controls, because a zero from a dead invocation reads the same as a zero from a decline: - **Lit control** — a genuinely new key added to `packages/spec/src/ui/view.zod.ts` (the same file as objectstack-ai#17796's specimen) fires `T1 … :101` and exits **4**. The surface covers these files and the run is alive. - **Dark control** — the card's sharpest specimen reduced to its hunk, with the removed line deleted and the three added lines byte-identical, fires `T1 packages/spec/src/ui/component.zod.ts:2504` and exits **4**. The silence is bought by the replacement, never by the shape. ⭐ And it did not fire at the card's own filing commit either. Running `check-widening-tells.mjs` **as it stood at `758ac409`** (`origin/main`, 2026-09-12T11:38Z — seventeen minutes before the card was written) against the same five inputs reproduces the same five readings: three specimens exit 0, both controls exit 4. The repair had landed three days earlier, in objectstack-ai#16943's per-change-block replacement budget: a key re-declared in place removes a T1 line and adds one, and the removal pays. ⇒ the card's "nine tells" was carried over from the threads that accumulated on closed card objectstack-ai#17618; it was not a reading of the gate on the day it was filed. Two of the three PRs have since merged (objectstack-ai#17846 on 2026-09-12, objectstack-ai#17638 on 2026-09-13) and objectstack-ai#17796 was closed unmerged, so the "three PRs parked" cost is also spent. ## Why no matcher change was made for half one ⛔ The shapes the card floated — pairing across a HUNK, or diffing the file's key SET instead of the block's added lines — are the silence `changeBlocks`'s own docblock refuses: a hunk carries three context lines each side and routinely holds an unrelated removal at one end and a real addition at the other, so pairing across it pays for a new key with a removal that has nothing to do with it. Buying that would trade a loud failure for a quiet one to repair a defect that is not there. **A fix that silences T1 generally is worse than the bug**, and this PR does not make one. What half one did leave is a gap in the **instrument**, not in the reader: the `{ error: … }` re-declaration had no case of its own, and it is arithmetically distinct from the `.describe()` pair objectstack-ai#16943 pinned — the block removes ONE line and adds THREE, of which exactly one is a key. A budget counting LINES instead of KINDS comes up short right there. It is pinned now, with both controls above and a third: - ⭐ **surplus control** — a genuinely new key (`filterLogic:`) added in the SAME block as the re-declaration still fires at its own file:line. One removal pays for one key; a real widening riding along with a re-declaration is still caught. ## Half two — the remedy with no reader — is what this PR repairs The sentence offered two doors and only one was real: > re-declare `yes` or explain in the claim why this addition does not widen `c5WideningTell()` compares the declaration against the diff's tells and stops there; nothing in either file reads an explanation. So an author who followed the instruction got the identical exit 4 with no way to learn that the remedy was never implemented — and the only door that DID move the number was `Clause-②: no` → `yes`, which on a false tell is the one thing the standing rule forbids outright: 「⛔ 永不把 `no` 翻成 `yes` 去过门」.⚠️ A gate whose only working door is a lie teaches the lie.⚠️ This file's header had already recorded that twice — at objectstack-ai#16822 and again at objectstack-ai#16943 — without ever changing the string a refused author actually reads. So the file knew and the author could not: a declared-but-unenforced remedy, which this repo removes rather than documents. **Three shapes were weighed:** 1. **Delete the second branch.** ⛔ Refused. It leaves `re-declare yes` as the only door, which on a FALSE tell is exactly the forbidden lie — the message would then instruct it. 2. **Give the explanation a reader.** ⛔ Refused. An author-written sentence that clears the author's own gate is 自查放行, and it needs the new claim-line syntax objectstack-ai#16448 forbids. 3. **Name the door that was always the right one.** ⭐ Chosen. objectstack-ai#16822 already ruled where a demonstrated false positive gets repaired — HERE, in the matcher, with a `--self-test` case pinning the shape — and the sentence now says so, names the file to open, allows filing it as its own card when it is out of the PR's scope, and states outright that an explanation moves no exit code so nobody spends a round rediscovering it. Both doors the sentence now names are doors this file can open. That is the pin. ## Exit-code contract — ⛔ UNCHANGED `EXIT_OK` 0 · `EXIT_USAGE` 1 · `EXIT_INCOMPLETE` 2 · `EXIT_REFUSED` 4, all unmoved, and no verdict state changes. Every tell fires exactly where it fired: the only behavioural surface touched is the TEXT a refusal renders. The seat reading `$?` reads the same table it read yesterday. ## Verification `--self-test`: **309 cases pass**, exit 0 — measured against **298** on `origin/main`'s own copy of this file, so 11 cases were added and none removed. Sibling `check-clause2-carriers.mjs --self-test`: **715 cases pass**, exit 0 — it renders `REFUSAL_SENTENCE` unparaphrased and that pin still holds through the constant. All **31** commands derived by `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` for this file surface were run; **every one exited 0**, each exit code captured by redirect-then-`$?`, never through a pipe. **Non-vacuity — three ablation legs**, each proving the mutation reached disk by a `grep -c` (LINES) count on the anchored text before and after, each restored under a `trap … EXIT INT TERM`, and each restore proven by `git hash-object` equalling the HEAD blob `9ecddb0c…` with `git diff HEAD` empty: | leg | mutation | on-disk proof | self-test | which cases red | |:--|:--|:--|--:|:--| | budget | the replacement budget never pays | 1 → 0 | exit 1, 26 fail | the specimen case, the surplus control, and objectstack-ai#16943's own live pairs | | t1-silent | T1 declines unconditionally | 0 → 1 | exit 1, 43 fail | ⭐ the dark control AND the surplus control — the "silencing T1 generally" direction | | old-sentence | the pre-objectstack-ai#17848 wording restored | 0 → 1 | exit 1, 4 fail | all four sentence pins | ⛔ The first attempt at the `t1-silent` leg counted an anchor the mutation does not move (before=1, after=1). It was reported as a void reading and re-run against the injected text rather than quietly retried until something landed. **`skip-changeset`, measured not asserted.** `npm pack --dry-run --json --ignore-scripts` in `packages/spec` packs **271** entries (the lit control) and **0** matching `scripts/pm` or `check-widening-tells`. The root package is `private: true`, and 0 of the 70 publishable workspace packages contain the changed path. Nothing published moves. ## Acceptance notes - **noted, not filed** — the card's own "nine tells, 2026-09-12T11:5xZ" figure was not a reading of the gate on that date; its table's `git grep -oF` counts (which prove the keys are not new) were fresh, the tell count was not. Successor: this PR's body and the report. - **noted, not filed** — `SELF_TEST_BATTERY_FLOOR` is 16 against a roster of 22 declared batteries, so five could be deleted without reddening. That is what a floor IS (AGENTS.md prescribes a minimum, not an equality), and adding batteries must not red — an observation about slack, not a defect. Successor: none. - ⛔ Nothing here attaches, removes or waits on `needs:contract-review`; `scripts/pm/**` is not in `GOVERNED_SURFACES` (`check-governed-merges.mjs`: `docs/adr/**`, `.claude/**`, `skills/**`, `AGENTS.md`, `CLAUDE.md`), so this is an ordinary landing path. - ⛔ The three PRs the card names were not touched. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 --- _Generated by [Claude Code](https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6)_ --- ## Landing note (seat, 2026-09-17) Contract review at `CONTRACT_REVIEW_TIER` on head `f8ca2fdddf`: **PASS** — record is comment `5706908992` on this PR. ⭐ The review did not reason about this gate, it **executed** it: it `git archive`d a runnable subset at the head, at `origin/main` and at `758ac409` (main seventeen minutes before the card was filed), ran `--self-test` at each (309 / 298 / 269, all exit 0), re-fetched the three specimen PR heads and fed their real diffs through all three file versions, and ran three ablations with the blob hash re-verified after each restore. That is what makes its central finding trustworthy. **Central finding, which confirms this PR rather than undermining it:** the card's half one — T1 firing on a re-declared key — **was never real on any reachable version of the file**, with a lit control alive at every one. The card's 「nine tells」 figure reproduces nowhere. Half two (a remedy with no reader) **was** real and is what this PR fixes. ⇒ closing objectstack-ai#17848 on this PR is supportable. **The direction that mattered — does this weaken the gate?** No. Every predicate (`patchLines`, `changeBlocks`, `memberTellKind`, `tellsInFile` incl. the objectstack-ai#16943 budget, `wideningRefusal`, `exitForRefusal`) is byte-for-byte unchanged; the exported symbol list is identical at 46; the exit register stays 0/1/2/4; and the t1-silent ablation reds both the new dark and surplus controls. The only output change is the text of `REFUSAL_SENTENCE`, which no parser outside the file reads (0 hits, control 9).⚠️ **Two self-narration discrepancies in this body, appended rather than rewritten** (this repo squashes, so the body becomes the permanent commit message): the battery roster is **21** at head, not the 22 the body states (20 at main); and the budget-ablation failure count measured **24**, not 26 — the direction is confirmed, the exact figure is not as stated.⚠️ **One pin label overclaims, recorded not fixed:** 「the row it reports is the new key, never the re-declared one」 holds for the fixture's ordering only. With the new key written *before* the re-declaration in the same block the gate still refuses (1 tell) but reports the re-declared line. That is objectstack-ai#16943's pre-existing patch-order budget and it is the loud direction, so nothing is weakened — the word 「never」 is simply too strong. ⛔ Not fixed in-branch: that moves the head and voids an otherwise complete review record, for a word in a test label. **Pre-landing checks:** ① review PASS on record ✅ · ② `check-clause2-carriers --pair 18539` exit 0; ⛔ no carriers were hung on this pair (`Clause-②: no`, no declared surface) so there is nothing to strip ✅ · ③ re-taken at landing time, latest-run-per-check-name ✅. Governed-surface predicate: **0 of 1 path hits the register** ⇒ ordinary queue landing. --- _Generated by [Claude Code](https://claude.ai/code)_ Co-authored-by: Claude <noreply@anthropic.com>
Part of #16885 — this round is measure-and-report. Both repairs triage named (honour
view, or retire it under ADR-0049) are out of bounds for it, so the card stays open.What this changes
One
.describe()string onNavigationConfigSchema.view, plus the three generated reference-doc rows that project from it, plus a changeset.⛔ No accept set moves.
viewis stillz.string().optional(). Every document that parsed before parses now, with identical issues and identical output. Nothing is retired, renamed, constrained, or newly resolved.The measurement
Declaration —
packages/spec/src/ui/view.zod.ts,NavigationConfigSchema(unique in non-built source:preventNavigationoccurs exactly once outsidedist/, at that block).Reads — re-measured against source, not the bundle the card quoted. Repo-local: zero (
git grepover tracked files excluding**/dist/**; lit control —navigation.modeoccurs 5×).objectuiat the sha this repo pins in.objectui-sha(53ded82b, read withgit show SHA:PATH;dist/is untracked at that sha, so the reading is reproducible):navigation?.viewreadpackages/react/src/hooks/useNavigationOverlay.ts:245navigation?.mode/preventNavigation/openNewTab: 13 sites.viewwidth/isOverlay/modeon the same returned object: ~20 sitesformViewsread keyed by an authored view nameformViews?.defaultformViewsreads overall: 23 sitesThe one read binds
const view = navigation?.viewand then passes it as the second argument ofonNavigateat two branches (:287no-config,:320page) — the slot whose other producers in the same function are mode tokens (onNavigate(id, 'new_window')at:278and:301).A consumer with a closed vocabulary on that argument exists.
packages/app-shell/src/views/ObjectView.tsx:2540types it(recordId: string | number, mode: 'view' | 'edit')and branches on both values with no fallback arm — an authored name matches neither and the row click does nothing. The sibling consumer at:1957tolerates anything non-new_window, which is why the effect is invisible on some surfaces.Authored instances in this repo — zero, and the space is not vacuous:
ListViewSchema.navigationis set nowhere in-tree (every object-formnavigation: {in tracked files, 21 sites, is an i18n translation map). The scanner was proven on a synthetic fixture that lights for bothviewandmodebefore that zero was accepted.Which exit triage's two the measurement supports
Undetermined on the repair — and that is the finding. Both exits keep live evidence, neither is discharged:
summary_view,edit_form)", under a/** Target View Config */comment. Downstream,useNavigationOverlaypromotesviewto a first-class member ofNavigationOverlayState, which reads as a slot built for renderers and never wired. And a maintainer comment atObjectView.tsx:1968describes the forwarding path in so many words.lint-view-refsresolves app navigationviewName→listViews, a different key on a different surface). The specCHANGELOGnever mentions it, while the siblingsizework is mentioned 3×.objectui'sview-navigation-config-spec-parity.test.ts:139assertssite.navigation?.view === 'summary_view'. It uses{ view: 'summary_view' }only as a convenient mode-less config to pin type parity — it asserts the key is accepted, never that it resolves.⇒ Choosing between them is the seat's act: honouring
viewis new capability, retiring it narrows a published schema. This PR takes neither.Verification
At
bec4767c(the final commit).pnpm --filter @objectstack/spec build→VERDICT command-exit 0(shared verify lock), thenbuild-docs.ts→✅ Generated 222 files.check:authorable-surface·check:docs·check:generated·check:liveness·check:api-surface·check:skill-refs·check:skill-examples·check:llms-txt·check:variant-docs·check:empty-state·check:strictness-ledger·check:objectui-pin-citations·check:export-origins·check:duration-unit-keys·check:exported-any·check:dual-source-exports·check:entry-nameability·check:yaml-examples·check:browser-reachable-entries; repocheck:doc-authoring·check:doc-anchors·check:docs-single-h1·check:docs-spec-enumerations·check:docs-audit-scope·check:docs-redirects·check:docs-transcript-drift·check:nul-bytes·check:quick-reference-counts·check:corpus-claim-drift·check:published-files·check:org-identifier·check:role-word·check:slot-lookup·check:watch-hint-literal·check:spec-parsed-alias·check:page-declaration-shape·check:pm-widening-tells·check:skill-identifier-liveness·check:published-readme-links·check:type-source-resolution·check:test-source-alias·check:cross-package-test-inputs·check-adr-0087-registration --base origin/main·check-changeset-no-major --base origin/main·check-closing-keyword-parity·check-comment-mask-adoption·check-comment-mask-corpus·check-registry-log-declared·check-section-landing-index·check-plugin-teardown-shape·check-platform-object-tenancy-census·check-reference-carrier-shape·check-ci-filter-parity·check-affected-docs·check-undeclared-dep-imports·check-system-context-census.pnpm --filter @objectstack/spec typecheck→ exit 0. Targeted testssrc/ui/view.test.ts+src/ui/interaction-config-retirement.test.ts→ 373 passed.eslint . --no-inline-configover the whole repo → exit 0, no output. ⭐ Not a narrowing: the repo-level scan actually ran at this head, so no ratchet reading is deferred.check:doc-authoringcaught a real defect mid-round: the first draft of the description carried the card id, which that gate forbids inside.describe()prose because the prose projects into published docs where the citation resolves to nothing. The id was moved to the adjacent TSDoc block and the gate went green.Changeset — measured, not assumed. Owed.
@objectstack/spec's publishedfiles[]ships bothdistandsrc/**/*.zod.ts. On the rebuilt artifact the corrected sentence is present in 22 built bundles and in the published source file; the old sentence is absent from all of them. Positive control: a sibling shippeddescribe()(Disable standard navigation entirely) lit the same probe at 22. Negative control: a test-onlyit()title litsrcat 1 anddistat 0.验收备注
noted, not filed— the liveness ledger classifiesview/list/navigationas oneliverow with nochildren, so its six sub-keys (mode,view,preventNavigation,openNewTab,size,width) are classified nowhere andnavigation.viewinheritslivefrom its container. It is also absent fromundrilled-containers.baseline.json, whose own_containersnote claims to record every such gap. This is not a new card: it is exactly the defectcheck-livenessdrills exactly ONE level, so achildrenmap at depth 2 is silently ignored — no evidence resolved, no unclassified report, no warning that any of it is missing #17424 already describes ("check-livenessdrills exactly ONE level"), whose stated fix includes migrating that baseline. 承接者:check-livenessdrills exactly ONE level, so achildrenmap at depth 2 is silently ignored — no evidence resolved, no unclassified report, no warning that any of it is missing #17424 —navigation.viewis a concrete, measured casualty for that round to cite.noted, not filed—ObjectView.tsx:1968(objectui, pinned sha) tells the next reader that a forwardednavigation.viewname is "resolved by RecordDetailView from its own config". RecordDetailView does resolve from its own config, but the forwarded name is dropped before it: the handler navigates torecord/:idcarrying only anoriginState.from. The sentence is therefore true about RecordDetailView and misleading about the name. 承接者: 无 in this repo — it is a comment in the read-only pinned checkout, and it belongs to whichever exit List viewnavigation.viewis declared in spec but resolves no form view — its only read lands it in theonNavigatenavigation-MODE argument #16885 takes.Generated by Claude Code