spec(ui): retire ObjectKanbanProps.quickAdd from object-kanban — tombstone, D2 conversion and baselines - #17792
Conversation
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
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 3632d2b06967d0faf471d7932bdbc030dae66513 && git checkout 3632d2b06967d0faf471d7932bdbc030dae66513
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 482d34d60c1d7bdc808c09a9d4edd152443bb101 10f206359fdf490336571717f8dc033b23080a9f && git checkout -B drift-repro 482d34d60c1d7bdc808c09a9d4edd152443bb101 && git merge --no-ff 10f206359fdf490336571717f8dc033b23080a9f
node scripts/docs-audit/affected-docs.mjs --json 482d34d60c1d7bdc808c09a9d4edd152443bb101
|
At-tier contract review — BLOCK on head
|
| question | verdict | the evidence that settles it |
|---|---|---|
| Is the narrowing exactly ruled option B? | ✅ | ⭐ A direct probe against the head's registry: a stack carrying quickAdd on object-kanban (region / nested / slot), kanban-ui (×3), kanban, kanban-enhanced, object-grid → after apply, only the three object-kanban sites lose the key; all others keep it. 3 notices, all at object-kanban paths |
Is kanban-ui still live? |
✅ | at the pinned sha: KanbanRenderer passes quickAdd/onQuickAdd, KanbanImpl gates on both — exactly what B preserves |
| Registration complete? | ✅ | ADR-0087 disposition, D2 conversion, RETIRED_KEYS_BY_MAJOR[18], D3 chain step, authorable-surface [RETIRED], docs row, 4 pin tests — and regenerating registry.ts produced 0 porcelain difference, so it is generated, not hand-edited |
Is Clause-②: yes correct? |
✅ | the prescription text reaches dist/ui/index.js and .mjs; positive control a shipped describe at the same 2 files, negative control test-only text at 0 |
| Collateral narrowing? | ✅ none | whole 8-file diff read hunk by hunk; neighbouring forwarded keys coverImageField / conditionalFormatting verified still live and still read at the pin |
| Do the pins discriminate? | ✅ | run against the merge-base schema: 2 of 4 fail — the two refusal pins flip, the other two guard the strip direction. ⛔ Not four green pins mistaken for four proofs |
⭐ A correction the review makes to the round's own reasoning
minor is the right level — but ⛔ not for the stated reason. "api-surface/ is unchanged, so no export leaves, therefore minor" conflates two ratchets: that test is right for the export ratchet and is not what licenses a key-level narrowing. What licenses it is the launch-window guard in check-changeset-no-major.mjs plus the three key-level carriers that did move — the authorable-surface [RETIRED] row, the RETIRED_KEYS entry, and the BREAKING banner. Same conclusion, sounder ground.
⛔ What the review could not measure — stated, not omitted
Lint & Repo Gates was still running · check:react-declaration-parity needs an external browser artifact (EXTERNAL_INPUT_REQUIRED) · objectui's interim module (PR #8922) predates both the pin and the sibling checkout, so not counted in either direction · out-of-repo authors of quickAdd, which the in-repo zero cannot speak to · the full 13,443-test suite was not re-run end to end (CI's shards all concluded success at this head).
⇒ Disposition: the fix is one line and has been sent back to the delivering round, which retains its full context. ⛔ The PR stays draft; no flip, no enqueue, no auto-merge. The next head owes a re-run of check:objectui-pin-citations and a re-confirmation that the four pins still discriminate —
domain:spec execution seat · session_01MkQhmuuJAVDjmeWNixwDDH · verdict adopted 2026-09-12T05:32Z
Generated by Claude Code
…and correct the ObjectKanban anchor
Scoped at-tier re-review of head
|
| re-measured because the fix touched it | reading |
|---|---|
| the 4 pin tests still discriminate | ablation on the tombstone: 2 failed / 2 passed — the two refusal pins flip, as before. Mutation proven on disk (quickAdd: retiredKey 1→0, marker 1, blob changed), restored byte-identical |
registry.ts generator-current |
exit 0, regen byte-identical |
| diff scope vs merge base | still exactly 8 files |
| ⭐ the anchor correction, verified at the pin | :930 is <KanbanRenderer schema={{, :931 is ...effectiveSchema, ⇒ :931 is right. All 7 occurrences across 6 files now read :931; ObjectKanban.tsx:930 tree-wide → 0, with the :931 probe as its control |
⇒ the round's decision to widen the fix from the one flagged site to all six was correct, and the widened anchor is right everywhere.
Relied on without re-deriving — and only because the delta is comment-only, which was measured rather than assumed: the type-scoped narrowing being exactly ruled option B, kanban-ui still live, registration completeness, the dist clause-② measurement, and minor as the level. ⛔ Had measure 1–3 come back otherwise, all five would be void.
Two NON-BINDING findings, neither blocking
⚠️ Pre-existing anchor imprecision the fix did not introduce: the entries file,migrations/registry.tsandcomponent.zod.ts:2834citeindex.tsx:196for bothquickAddandonQuickAdd; at the pin:196passes the first and:197the second. The substantive claim (both are forwarded together) holds. Outside the gate's population, so nothing would flag it — worth:196-197on a future touch.--verify-anchorslists the new entries file with 2 unasserted anchors on its coverage worklist; that floor is a ratchet, not a requirement on new records. Informational.
⛔ What the review could not measure — stated, not omitted
CI's own run of the pin gate was still in progress (Lint & Repo Gates, three Test Core shards); every completed check on the head is success, and the gate reading above is a local run at the exact sha with a lit control, ⛔ not CI's. --verify-anchors verified 0 anchor-content assertions because no record in the tree carries a quoted first line, so anchor correctness rests on the direct reads quoted above. The full spec suite was not re-run — out of the bounded scope, the delta being comment-only.
⇒ Disposition: cleared. The landing sequence is armed under its own guards — flip, re-read ④ after the flip (it re-triggers Check Changeset and the queue guard), arm only on a fresh GREEN. ⛔ Nothing is armed on this verdict alone.
domain:spec execution seat · session_01MkQhmuuJAVDjmeWNixwDDH · verdict adopted 2026-09-12T06:05Z
Generated by Claude Code
Carrier cleared —
|
| # | check | reading |
|---|---|---|
| ① | at-tier verdict (clause-② is yes) |
PASS_WITH_FINDINGS on the head that lands, linked above |
| ② | check-clause2-carriers.mjs --pair 17792 |
exit 0 — "the clause-② declaration is readable in the fixed spelling and both carriers agree" |
| ③ | check-governed-merges.mjs --test over all 8 final diff paths |
exit 0 — "0 of 8 path(s) hit the register (5 surfaces)" · ⭐ lit control: the same predicate with AGENTS.md appended → exit 3, GOVERNED. So the 0 is a verdict, not a predicate that answers NOT-governed to anything |
| ④ | every check green, newest run per check NAME | re-read after the strip, below |
⭐ One correction to my own instrument while I was at it: I first ran ③ as --branch <name>, which this script does not accept — it silently fell through to the repo-wide governed-merge sweep and exited 2 for both the target and the control. Two identical answers from a probe that was not aimed at the question. The flag is --test <paths…>, and it was the dead control that said so.
What I am doing now
Stripping needs:contract-review from both carriers — this PR and card #17260 — in an additive-removal write with a read-back diff, then re-arming. ⛔ No second review is being invented and ⛔ no label is being stripped "to get past the check": the verdict predates the enqueue by 14 minutes.
Generated by Claude Code
… 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>
Fixes #17260
Executes the objectui#8285 director-seat ruling (comment 5583979207, decision batch #91, 2026-09-08, standing maintainer delegation) — ruled option B:
quickAddis retired from theobject-kanbanboard and stays only on thekanban-uiblock, where a React host can supply the runtime function the control needs. This PR is the tombstone half that ruling assigns to this repo. A vs B is not re-opened here.ObjectKanbanProps.quickAddis retired fromobject-kanban.The premise, re-measured rather than relayed
The card's body said an author writing
quickAdd: truegot "nothing, with no diagnostic". The filer corrected that themselves, and the correction is what holds — re-measured here at the objectui sha this repo pins (.objectui-sha=53ded82bf), not at that checkout's HEAD:onQuickAddinplugin-kanban/src/ObjectKanban.tsxonCardClick— 6quickAddin the same fileobjectName— 38object-kanbanregistrationinputs(index.tsx:421-431)objectName,columns—quickAddabsentobjectNamepresentThe board forwards the key —
ObjectKanban.tsx:931spreads the authored bag intoKanbanRenderer, which passesquickAdd={schema.quickAdd}alongsideonQuickAdd={schema.onQuickAdd}(index.tsx:196) — butKanbanImplgates the affordance on both (:355,:368), andonQuickAddis a host-supplied function JSON cannot carry and no producer puts on anobject-kanbannode. So the gate was permanently false: accepted-and-dropped, exactly as the card classifies it.Two readings the card's numbers came from could not be reproduced at the pin, and are reported as such rather than passed on:
OBJECT_KANBAN_INPUTS(the 13-key constant) and theinert-quick-addinterim diagnostic do not exist at53ded82bfat all — both are later objectui work.OBJECT_KANBAN_INPUTSdoes resolve at that checkout's HEAD (control lit, 3 files), and the registry-spec ledger records the key verbatim there asESCALATED (object-kanban.quickAdd — measured NOT honoured). The pin's own equivalent reading is theinputsrow in the table above, and it says the same thing.The retirement kit
ObjectKanbanPropsSchema.quickAddretiredKey()tombstone —tsctypes itnever, and a value reaching the parse raises the prescription instead of a bare unknown-key verdictquickAddamong the keys reached "via the forwarded schema" — true about the FORWARD, false about the READ, which is how the key kept re-authorizing itself. Corrected in the same strokesrc/conversions/registry.tsobject-kanban-quick-add-removed— a pure lossless delete (the key never had an effect to preserve), scoped by componenttypeso the LIVEkanban-uispelling stays out of its reachmigrations/entries/retired-keys/18.ui__ObjectKanbanProps__quickAdd.tsRETIRED_KEYS_BY_MAJOR[18]entryui/ObjectKanbanProps:quickAdd, plus the D3 chain-step wiring and rationaleauthorable-surface/ui.jsonui/ObjectKanbanProps:quickAdd [RETIRED]content/docs/references/ui/component.mdxsrc/ui/component.test.ts.changeset/17260-object-kanban-quick-add-retired.mdminor,adr-0087: registered object-kanban-quick-add-removedpackages/spec/src/ui/view.zod.tsis untouched — it is another round's declared face this batch. Thekanban-uiblock'squickAddis untouched by design: it is not a component type this spec declares at all, which is why the conversion is scoped bytyperather than by key name.Liveness — measured, with a lit control
Zero stored or example stacks in this repo carry
quickAdd, because noobject-kanbancomponent is authored anywhere underexamples/orapps/. The zero is a reading, not a dark probe: the same instrument over the same corpora returnsobject-grid3 andobject-metric8. Repo-wide,quickAddoccurred in exactly four places before this PR — the schema key, the docblock sentence, the ratchet row and the generated docs row — i.e. only the carriers being retired here. Out-of-repo authors are unknown and unknowable from here, which is what the D2 conversion and the prescription exist for.Verification
pnpm --filter @objectstack/spec buildVERDICT command-exit 0pnpm --filter @objectstack/spec testVERDICT command-exit 0— Test Files 473 passed (473), Tests 13443 passed (13443)pnpm --filter @objectstack/spec typecheckpnpm --filter @objectstack/spec check:generatedgen:migration-registry, then the build'sgen:schema, thengen:docs)pnpm --filter @objectstack/spec check:migration-registry✓ src/migrations/registry.ts is current (202 semantic, 168 retired-key, 178 retired-def)node scripts/check-adr-0087-registration.mjs --base origin/main✓ 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition—registered object-kanban-quick-add-removed (new here)node scripts/check-changeset-no-major.mjs --base origin/main✓ This diff introduces no major bumppnpm check:nul-bytesThe registry was regenerated by the repo's own generator (
pnpm --filter @objectstack/spec gen:migration-registry→✓ wrote src/migrations/registry.ts), never by hand; the first build before that run failed loudly with1 key(s) were tombstoned with no registered retirement, which is the gate doing its job.Ablation — the pins can fail. On
HEAD: 4 passed. With the tombstone mutated back to a livez.boolean().optional()in source (mutation proven on disk: tombstone-call count 1 → 0, injected marker 1, blob hash106299e7…→e657a7e3…), the same run goes 2 failed / 2 passed: the two refusal pins are the discriminating half. Restored fromHEADand proven byte-identical (blob back to106299e7…,git diff HEADempty). Reported honestly: thenot.toHavePropertypin does not flip under that mutation — an optional key absent from the input is not materialized either way — so it guards the strip direction and not the refusal.Changeset — measured, not assumed
Owed, at
minor.dist— 2 hits,dist/ui/index.jsanddist/ui/index.mjs, both insidepackages/spec'sfiles[].component.test.ts— 0 hits indist.Level is
minor, notmajor:scripts/check-changeset-no-major.mjsforbidsmajorduring the launch window (lockstep versioning would promote ~70 packages), and the sibling retirement one entry over (ui/ObjectGridProps:defaultSort, #11805) is registered under protocol 18 on the same reading.api-surface/is unchanged and correctly so — it ratchets export existence, andObjectKanbanPropsstill exists, one key narrower.Not flipping this ready
An at-tier contract-review verdict is owed on the head that lands;
needs:contract-reviewrides this PR. Draft, not enqueued.Co-Authored-By: Claude noreply@anthropic.com
🤖 Generated with Claude Code
Generated by Claude Code
Generated by Claude Code