feat(spec)!: retire the list view's own tabs key; named presets are listViews entries - #20357
Conversation
…, D2/D3, ledger) A list view's `tabs` parsed and was stored at every list-view door and drew nothing: the one component that reads a ViewTab[] has no production mount, and the tab strip above an object's records is the saved-view switcher, which renders one tab per `listViews` entry. ADR-0049 enforce-or-remove. - `tabs` becomes a retiredKey() tombstone on the list-view shape, with a prescription naming how to move each tab to a `listViews` entry. - The metadata form's `tabs` repeater leaves with the key. - D2 `view-list-tabs-removed` (protocol 18) strips it from list payloads in stack.views[]; D3 `list-view-tabs-retired`; RETIRED_KEYS_BY_MAJOR[18] gains ui/ListView:tabs and ui/ObjectListView:tabs. - The ledger row stays dead with a REMOVED note. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
…viving tab-carrier fixtures to userFilters.tabs Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
…s retirement; add the changeset The view form's `tabs` repeater left with the key it wrote, so the extracted form labels drop with it across en/es-ES/ja-JP/zh-CN. Pure deletion, regenerated by check-i18n-bundles.mjs --write. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
…n the retirement pin in the repo project The tombstone is no longer a container, so check:liveness reports the view/list.tabs undrilled row as a closed gap; the tree-scoped absence walk reads outside the package, so it joins vitest.repo-tests.json. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
…bstone Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
…st-tabs-retired # Conflicts: # packages/spec/src/conversions/registry.ts # packages/spec/src/migrations/registry.ts
📓 Docs Drift CheckThis PR changes 2 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 137 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 d564f7e7c4678aa81d86dda17119509af76860fd && git checkout d564f7e7c4678aa81d86dda17119509af76860fd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c745e2b896ba2c9382ecd208a40799a44226cddb c1c3ec90ddbc149d1d1515f693a887961b893cc8 && git checkout -B drift-repro c745e2b896ba2c9382ecd208a40799a44226cddb && git merge --no-ff c1c3ec90ddbc149d1d1515f693a887961b893cc8
node scripts/docs-audit/affected-docs.mjs --json c745e2b896ba2c9382ecd208a40799a44226cddb
|
…st-tabs-retired # Conflicts: # packages/spec/src/conversions/registry.ts # packages/spec/src/migrations/registry.ts
…rges the os-regen deferral) Main's side of the three pages (the form layout narrowing) taken by os-regen-merge.sh step 2, then gen:docs re-derived the list-view tabs tombstone rows on top. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
…post-retirement truth - platform-objects repeater survey: `view:tabs` is asserted ABSENT (dark leg) beside the two surviving view repeaters, instead of present. - platform-objects zh/ja/es positive control: 608 to 598 — the retired tabs repeater took its own label and nine row labels out of the catalog. - metadata-protocol graft: the nested ViewTab.filter leg rides the surviving userFilters.tabs carrier; the view-filter leg is unchanged. - metadata-protocol findReferencesToMeta control: the consulted source types are read from the derived reference-site index, and `object` is pinned absent — its one view-reference site was listViews.*.tabs[].view. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
Contract reviewServed-tier: 80/80 ① Derived judgmentsBLOCKING (item 6, changeset truth) — two sentences in
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL |
…st-tabs-retired # Conflicts: # packages/spec/src/conversions/registry.ts # packages/spec/src/migrations/registry.ts
…discharges the os-regen deferral) Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
A list view's own `tabs` has no reader, and objectui's TabBar, the one component that would draw it, has zero production mounts. The earlier wording said the one component that reads a ViewTab[] has none, which is false: `userFilters.tabs` is a ViewTab[] too, and TabFilters reads and renders it. The population sentence now says examples or platform sources, and that the one published skill example that taught the key is corrected in this change. Corrected in the changeset, the tombstone docblock, both retired-key entries, the conversion docblock, the step-18 rationale, the ledger note and the pin's header; the migration registry regenerated. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
|
Post-landing delta audit · PR #20357 merged at What happened. The last contract-review record on this PR is the PASS Measured (read-only:
Verdict: the merged head's PR-own change equals the PASS head's change modulo base movement; nothing rode on the missing record. Residue for the fleet, not for this PR: objectui's Reviewed-by: |
The entry landed with #20357 after this branch's last merge. It is in no published tarball, so it carries the package label (17.4.0, also npm latest). Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
… its object, as boot does (objectstack-ai#20393) (objectstack-ai#20459) Fixes objectstack-ai#20393 Clause-②: no `os build` / `os compile` now runs the check `os validate` has run since objectstack-ai#20331: a `views:` container whose own `name` disagrees with the object key it binds to is refused. The build exits 1 with the message the boot registrar prints, and it writes no artifact. Before this change the build exited 0 and wrote `dist/objectstack.json`, and `os serve` then refused that artifact at boot. This follows triage's grade on the card (comment 5865077508): the build calls the same function over the same view set, the `validate-build-gate-parity` row moves from `VALIDATE_ONLY_GATES` to `SHARED_NON_REGISTRY_GATES`, and there is no second rule. ## Premise, measured on `origin/main` `7fa3e3e07` before any edit (H0) The setup: the CLI's dependency closure was built (turbo, 59 tasks). Then `os init my-app -t app --no-install`, `os g object order_line` and `os g view order_line` were run, and the view's `name` was hand-edited to `'order_line'`, bound to `my_app_order_line`. | step | exit | what it did | |---|---|---| | `os validate` | **1** | "The server would refuse this stack at boot (1 view container)" (the objectstack-ai#20331 control) | | `os build` | **0** | `Build complete`, and wrote `dist/objectstack.json` carrying `[{"name":"order_line","object":"my_app_order_line"}]` | | `os serve` in a directory holding only that artifact (no config) | **1** | "Invalid `views:` container from manifest 'com.example.my-app': the container's own `name` is 'order_line', which disagrees with the object key it binds to, 'my_app_order_line' …" | The card's moot condition does not hold: the container body `name` still parses on `main` (objectstack-ai#20357 retired `list.tabs` only). ## What changed 1. **`packages/cli/src/commands/compile.ts`, new step 3a**, right after the schema parse and before the rule table and any artifact write. It makes the same call `validate.ts` step 2c makes, `findViewContainerNameRefusals(result.data)`. That is the walk over `@objectstack/objectql`'s `viewContainerNameRefusal`, the function the boot registrar throws the answer of. There is no second implementation of the check or of the walk. The message is the runtime's own, unchanged. - `--json`: `{ success: false, errors, warnings: warningsSoFar(), conversions }`. This is build's schema-exit envelope, with the refusal rows in `errors` exactly as `os validate --json` carries them: `{ path, code: 'VALIDATION_ERROR', httpStatus: 400, message }`. The exit carries `warnings` and `conversions` like every other exit, so the `build-json-failure-warnings` contract holds. - Text face: `os validate`'s header ("The server would refuse this stack at boot (N view container(s))") and the bullet list. No step line is printed, so a passing build prints what it printed before (the docs transcripts stay true). 2. **`packages/cli/test/validate-build-gate-parity.test.ts`**: `findViewContainerNameRefusals` moves to `SHARED_NON_REGISTRY_GATES`, so `both commands run findViewContainerNameRefusals` now holds both doors to it. `VALIDATE_ONLY_GATES` stays, empty, with a note that empty is its steady state. Its two-way pruning test is unchanged. 3. **`packages/cli/src/commands/validate.ts`**: one comment sentence in step 2c said "`os build` does not run it", which this change makes false. It now points at build's step 3a. Code is unchanged. 4. **New `packages/cli/test/build-view-container-name.test.ts`** (integration tier: it spawns the CLI and constructs `ObjectQL`). It has 6 tests: - a premise case: boot refuses both divergent payloads and accepts both controls; - THE PIN: `build --json` exits 1, `success: false`, and `errors[0]` equals what `ObjectQL.registerApp` throws for the same payload (`message`, `code`, `httpStatus`, `path: 'views[0]'`). No artifact is written; - the text face: exit 1, the same words, no `Build complete`, no artifact; - a `packages[]` stack: the divergent container in the second body is refused as `packages[1].manifest.views[0]`, under that package's id and in the words boot throws for that body. The matching container in the first body is not reported; - two controls, a matching `name` and no `name`: each exits 0. The matching control's written artifact is registered by boot without a throw. 5. **Changesets.** - New `.changeset/20393-build-view-container-name.md`: `@objectstack/cli` `patch`, `Clause-②: no`. - The pending `.changeset/20331-validate-view-container-name.md` was also edited. See the gate note below: this is a deliberate correction and needs your confirmation. ### Which shape build judges, and why the verdict is boot's (H1) Build judges `result.data`, the output of `ObjectStackDefinitionSchema.safeParse(lowerCallables(normalized).lowered)`. That is the same expression, over the same pipeline, as the input to `validate.ts` step 2c. It is also the object build serializes. Step 4 adds only `docs`, `packages[i].manifest.docs` (via `attachPackageDocs`, docs only) and `runtimeModule`, never a view. So every `views:` entry the artifact carries is judged here, at the top level or in each `packages[i].manifest` body. The walker mirrors the load path over that shape: `{ ...manifest, ...stack }` under `artifactPackageId`, or each package body under its own id. Measured after the fix on the repro project: `os build --json`'s `errors[0].message` is **byte-equal** to the line `os serve` printed when booting the pre-fix artifact (`cmp`: identical, 568 bytes). ## After the fix, on the same project (CLI from source) - `os build` exits 1 with the refusal, and no `dist/` directory is created. `os build --json` exits 1 with `success: false`, and `errors[0]` is `views[0]` / `VALIDATION_ERROR` / `400`. - Controls: `name: 'my_app_order_line'` gives exit 0 and an artifact. Deleting `name` gives exit 0 and an artifact. - `examples/`: `os build --json` exits 0 with `success: true` on each of app-crm, app-multi-package, app-showcase and app-todo, with no refusals. The output was written outside the tree. ## Fixture census (H2): no build-door fixture turned red A structural scan read 7,909 tracked `.ts`/`.js`/`.json` files under `packages/`, `examples/`, `apps/` and `scripts/`, including the 260 string and template literals that carry config source (the configs tests write to disk). It found 936 containers. It read each object literal carrying a container arm (`list`/`form`/`listViews`/`formViews`) and no `viewKind`, and derived the key as boot does. It found 21 divergent containers, all outside every build door: `packages/lint` rule unit tests (13), `packages/objectql` (3, including the refusal's own fixtures), `packages/metadata-protocol` (2) and `packages/spec` (2). None of these runs `os build`. `packages/cli`, `packages/qa` and `examples/` have none; objectstack-ai#20331's patch round had already fixed that population. There were 9 non-literal-name containers, none in a build-door suite. The behavioural half agrees: all 15 build-door `.e2e` suites (the nightly tier) and the full unit tier are green at the head. ## Ablation (H3), with the fix committed first The mutation went through `scripts/ablation-replace.mjs` on `packages/cli/src/commands/compile.ts`. It replaced the call with `const containerNameRefusals: ReturnType of typeof findViewContainerNameRefusals = []` plus the marker `__ABLATION_20393_NO_CALL`. The tool reported anchor x1 to x0, replacement x0 to x1, and blob `6b4b8871` to `b7f532cb`. On disk, the marker count was 1 and `= findViewContainerNameRefusals(` counted 0. - Both pins read source: the parity test reads `src/commands/compile.ts` as text, and the build-door test runs `bin/run-dev.js`, which is `src/` through tsx. So no `dist/` leg applies. - The result was 4 of 30 red: `both commands run findViewContainerNameRefusals` (unit), THE PIN, the text face, and the `packages[]` case. The premise, both controls and the rest of the parity file stayed green. The direction is red, as expected. - **Restore:** by the tool's trap, `git checkout HEAD --` on the absolute path. The blob after the restore equals HEAD `6b4b8871`, `git diff HEAD` is empty, `git status --porcelain` is empty, and the marker count is 0. The re-run was 30 of 30 green. ## Tests, at head `0c4e9c3724` (after merging `origin/main` `3cf644938`) - `@objectstack/cli` `vitest run --project unit --maxWorkers=2`, two shards: 117 + 116 files, 1789 + 1547 tests, all passed. - `--project integration`, run locally for the two view-container files only: 11 of 11 passed. The rest of the integration tier is declared to CI. - Nightly `.e2e` build-door suites (`OS_TEST_TIERS=nightly`), 15 files: 297 of 297 passed. - `pnpm --filter @objectstack/cli typecheck`: exit 0. `tsc --listFilesOnly` puts the new test in the `tsconfig.test.json` program. - **Host note (macOS):** three suites compare paths under the default `TMPDIR`, which macOS resolves from `/var` to `/private/var`: `published-subpath-console.pin`, `published-subpath-hook-body.pin` and `config-miss-stdout-purity.e2e`. Under the default `TMPDIR` they fail on that prefix alone, for commands this PR does not touch (`os diff`, `os info`, `os verify`, …). With `TMPDIR` set to its realpath they pass (29 of 29 and 174 of 174), so the counts above were taken that way. See the Acceptance notes. ## Gates, at `0c4e9c3724` - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` gave 63 commands. `--ran` reconciled them as 63 run, 0 NOT-MEASURED and 0 UNRUN. Every command exited 0 except the one below. - `check:dual-build-cjs-loads` and `check:i18n-coverage` first exited 3 (PREREQUISITE NOT MET, unbuilt packages). Each exited 0 after the named packages were built. - `pnpm lint` (full `eslint . --no-inline-config`): exit 0 in 45 s. - `node scripts/check-issue-citations.mjs --base origin/main`, run after merging `origin/main`: exit 0, 3 citations resolve. - Two families take their argv from the workflow and are CI-only: `check-issue-citations.mjs --census` and the dogfood shard attestation. ### ⚠ `check-empty-changeset --base origin/main` exits 1 by design: a pending release note is corrected here `.changeset/20331-validate-view-container-name.md` is objectstack-ai#20331's pending entry. It closes with "Not changed: `os build` does not run this check, so it still writes an artifact carrying such a container …". This PR makes that sentence false. Both entries ship in the same release while that file is pending, and a published `CHANGELOG.md` sentence is corrected only in the entry that carries it. So that paragraph now reads "`os build` runs the same check as well (objectstack-ai#20393, its own entry), so it no longer writes an artifact carrying such a container." The gate classifies this as the DELIBERATE CORRECTION class: it stays red, and its remedy is to confirm the correction on the PR, ⛔ not to restore the file. - **Needs confirmation:** keep the correction. - **The alternative** is to restore the file from base and let this PR's own entry carry the correction. The gate goes green, and the release then carries the false sentence in the objectstack-ai#20331 entry. - If a release consumes `20331-validate-view-container-name.md` before this lands, that file's edit becomes a modify/delete conflict. Drop the edit then: the sentence was true in that release. ## Declared narrowing: verification ran UNLOCKED Every build and test run above went through `scripts/pm/os-verify-lock.sh`, and each run printed this disclosure (quoted verbatim from the first one): ```text **Declared narrowing — verification ran UNLOCKED.** `scripts/pm/os-verify-lock.sh` could not take the shared verify lock on this host: no usable `flock`. The shared verify lock is declared Linux-only (`flock` is util-linux, and a stock macOS does not ship it), so the command below was run directly, without the lock — a declared narrowing, not a silent one. No serialization guarantee held for this run, nor for any sibling agent in this container while it ran. pnpm turbo run build --filter='@objectstack/cli...' --concurrency=2 ``` ## Acceptance notes - **File surface beyond the claim, declared:** the claim named `compile.ts`, the parity test, `packages/cli` tests and one new changeset. Two more files were edited, each because this change made a sentence in it false. One is a comment sentence in `validate.ts` step 2c ("`os build` does not run it"). The other is the closing paragraph of the pending `.changeset/20331-validate-view-container-name.md` (above). - **Headers now incomplete, not false, and left alone:** - `packages/cli/src/utils/view-container-names.ts` opens "`os validate`'s author-time half of …". Both doors call it now. - `packages/objectql/src/view-container-name-refusal.ts` says "called by the boot registrar and by `os validate`". It is read-only for this card, and `os build` reaches it through the walker. - Carrier: none. - **Bound carried over, unchanged:** the walker does not walk a nested `plugins[]` entry's `views`, which boot also registers. Its header states why: the stack schema types `plugins` as `unknown[]`. Build inherits that bound. It does not widen it. - **macOS test portability (not a product defect, not filed):** three suites fail under macOS's default `TMPDIR` on a `/var` vs `/private/var` prefix: `test/published-subpath-console.pin.test.ts`, `test/published-subpath-hook-body.pin.test.ts` and `test/config-miss-stdout-purity.e2e.test.ts`. They pass with a realpath `TMPDIR`, and CI (Linux) is unaffected. There is no public-door reach. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_ --------- Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
…orm (objectstack-ai#19332, flight G2a) (objectstack-ai#20449) Part of objectstack-ai#19332 Flight G2a of ruling 5861442317. Clause-②: no ## Status: draft, no open gap The first round stopped at one red class guard, `packages/spec/src/kernel/repeater-item-titles.test.ts` (objectstack-ai#17232), because its fix sat in a file the claim forbade. The seat amended claim `5869305892` in place (its "Amended 2026-09-28T13:26Z" line). `packages/spec/src/data/object.zod.ts` joined the surface for `.meta({ title })` on the item properties of `IndexSchema` and `ObjectFieldGroupSchema` only. The 12 titles landed in `e8bdb1ad` (**Row titles** below), and the guard is green. ## What Two live keys that `ObjectSchema` declares had no form row, so an author could reach them only through the Source tab. Each is now a `type: 'repeater'` row on the object form with hand-written sub-rows, as the ruling says: 「**G2 (…) — hand-written curated sub-rows**, plus the three nested `omit` rows under `fieldGroups` (the `[DEPRECATED → collapse]` aliases, `object.zod.ts:1206-1210`) … `indexes.unique` offers `global` / `organization` only.」 The four-locale catalogue rows are in this PR. | key | form, section | sub-rows (face) | the row each sub-row copies | |:--|:--|:--|:--| | `object.fieldGroups` | `object.form.ts`, Basics, beside `highlightFields` | `key`, `label` (required text); `icon` (text); `description` (textarea); `collapse` (select: `none` / `expanded` / `collapsed`); `visibleWhen` (`type: 'code'`, `language: 'expression'`) | the repeater face is `object.form.ts` `fields.options` (declared, labelled sub-rows). `key` / `label` / `icon` copy the plain text rows `name` / `label` / `icon` in Basics; `description` copies Basics `description`; `collapse` copies the `lifecycle.class` select (every member is a spellable option value); `visibleWhen` copies `fields.visibleWhen` (`object.form.ts`, same node type, `EvaluatedExpressionInputSchema`) | | `object.indexes` | `object.form.ts`, Advanced, beside `datasource` | `name` (text); `fields` (`widget: 'string-tags'`, required); `unique` (select: `global` / `organization` only) | `fields.options` repeater face; `fields` copies the `highlightFields` row (a chip input over `string[]`); `unique` is a declared select for the reason below | Ledger: three nested `omit` rows at `object` / `fieldGroups` in `metadata-form-zod-reconciliation.test.ts`, one per alias: `defaultExpanded` (line 1206 on `main`), `collapsible` (1208), `collapsed` (1210). Row shape: the `page` / `interfaceConfig` / `sourceView` row at the top of the ledger, which is the existing nested `omit` for a deprecated alias. `indexes` needs no ledger row: its other two keys, `type` and `partial`, are `retiredKey()` tombstones and are excused automatically. Row titles: `object.zod.ts` gives both row schemas a JSON Schema `title` on every property, 12 in all (**Row titles** below). The help text states what the runtime does, and each claim was checked against its reader: - Group `icon` renders on the record detail page only. At the `.objectui-sha` pin `f8a9d0fb`, plugin-detail `DetailSection` draws it; plugin-form `fieldGroups.ts` does not copy it, because `ObjectFormSection` declares no `icon`. - Group `description` and `collapse` render on both the entry form and the detail page (`fieldGroups.ts` and `deriveFieldGroupDetailSections`). - Group `visibleWhen` gates the entry form's whole group (`projectSectionDivider`). The help text claims only the form. - A group `key` that is not snake_case, or is duplicated, is refused by the parse (probe below). A field whose `group` names no declared key is ungrouped (`deriveFieldGroupLayout`). - `indexes`: `SqlDriver.syncTableIndexes` is additive only, so a sync never drops an index. An unset `name` becomes `buildIndexName` (`idx_TABLE_COLUMNS`, `uniq_…` for a unique index). ## Where a misspelt field name is refused, read from the code The ruling's 「a misspelling is refused loudly at parse」 does not hold here, the same as for G1b's lists. - `indexes[].fields`: **no authoring door judges it.** - The Zod parse accepts `{ fields: ['statsu'] }` on an object that declares only `status` (probe below). - `validate-object-field-refs.ts` excludes the list by design, as a storage question for the registration path. No other lint rule, and so neither the publish door nor `os validate`, reads it for existence. - At sync, `syncDeclaredIndexes` skips the whole index and logs `skipping declared index … column(s) not materialized` at `warn`. - The help text says exactly that: "Nothing checks them when you save or publish: a name that is not a stored column makes the SQL driver skip the whole index, with a warning in the server log." - `fieldGroups` has no field-name list. A field joins a group through its own `group` key, so there is no name here for a misspelling to hide in. ## `unique`: how the row treats a stored `true` or `false` - The node is `boolean | 'global' | 'organization'`, default `false`. - A derived face takes the union's first arm, the boolean, and renders a switch that writes the deprecated bare `true`. No option can spell a boolean either: `FormSelectOptionSchema` refuses `value: true` (probe). So the row is a select that declares exactly the two scopes. - **What it can write:** only `global` and `organization`. Both parse today, and neither is the spelling protocol 18 refuses. - **A stored `true` or `false`** is left untouched on save. objectui's `RepeaterField.update` at the pin writes `{ ...row, ...patch }`, so `unique` changes only when the author picks a scope. - **Display:** the select renders `String(value)`, and no option matches `'true'` or `'false'`. Reading the Radix select, the trigger is then blank and does not show the placeholder. This is a code reading only, with no browser run. The help text therefore claims only the merge: "The deprecated bare true (it means global) is not offered; an index that carries it keeps it until you pick a scope." - **The same choice already exists in objectui's own embedded index editor** at the pin (`EmbeddedItemEditor.tsx` `FALLBACK_SCHEMAS.index`): it offers `global` / `organization` for a new index and leaves a legacy boolean alone. ## Row titles (landed by claim amendment) - **The guard.** `repeater-item-titles.test.ts` (objectstack-ai#17232) is the class guard for "a repeater's property-panel table shows raw keys". It derives each repeater's row schema from `z.toJSONSchema(…, { io: 'input' })` and requires a `title` on every authorable row property. That includes the three deprecated aliases, because the guard reads the schema, not the form. Its ledger says 「⛔ Never add an entry to LEDGER to make this file green」. - **The change (`e8bdb1ad`).** 12 `.meta({ title })` calls in `packages/spec/src/data/object.zod.ts`, the ones the first round measured: - `IndexSchema`: `name` → 'Name', `fields` → 'Fields', `unique` → 'Unique'; - `ObjectFieldGroupSchema`: `key` → 'Key', `label` → 'Label', `icon` → 'Icon', `description` → 'Description', `visibleWhen` → 'Visible When', `collapse` → 'Collapse', `defaultExpanded` → 'Default Expanded', `collapsible` → 'Collapsible', `collapsed` → 'Collapsed'. - The titles equal the labels this PR declares, which is the `view.form.ts` row-property convention. - **Nothing else moved.** Stripping those 12 exact calls from the new `object.zod.ts` yields the previous commit's file byte for byte. The guard and the reconciliation test are green together (2 files, 85 tests). `check:generated` reads "All 15 generated artifacts are up to date" on the merged head, so nothing regenerated and no accept set moved. The changeset now names the titles as part of the served JSON Schema and stays `Clause-②: no`. ## Residue of the reconciliation gate (dispatch assumption 1) The gate's own helper block was copied verbatim into a scratch probe that was never committed. At base, that block is lines 1-808, prefix sha256 `91478ba8d05c70b7…`, the same prefix G1b read. The probe ran in a scratch worktree detached at the base `e4d3f2ca`. Residue = offerable root keys − offered − root `omit` rows, per type, with `view` apart. Controls, asserted inside the probe: - lit: `name` is offered by 17 of 17 forms; - dark: `object.zzFabricated19332G2a` and `object.name` are in no residue; - named lit key: `object.activityMilestones` (G2b) is in the residue on both trees. | tree | residue | per type | view | |:--|:--|:--|:--| | base `e4d3f2ca` | **6** | object 5, field 1 | 42 | | this branch's form + ledger (from `a6f19eb2`, `git diff` against it empty) | **4** | object 3, field 1 | 42 | Removed: `object.fieldGroups`, `object.indexes`. Added: none. The four left are the G2b keys: `object.activityMilestones`, `object.publicSharing`, `object.userActions`, `field.inlineColumns`. Nested reading on the same tree, through the gate's own `reconcileNestedLists`: - With the three new rows removed from the ledger, `object.fieldGroups` reads `zodOnly = [collapsed, collapsible, defaultExpanded]`. With them, it reads `[]`. - `object.indexes` reads `zodOnly = []` and `unanchored = false`, with keys `fields, name, partial, type, unique` and retired `partial, type`. So the three rows are exactly what the gate needs. Parse probe on the same tree: - `IndexSchema`: `'global'` and `'organization'` pass; `true` and `false` pass (17.x); `'tenant'` fails with `invalid_union`. - `ObjectSchema`: - a misspelt index column parses (`true`); - a group carrying `collapse: 'collapsed'` beside `collapsible: true, collapsed: false` keeps `'collapsed'`; - an alias-only group (`defaultExpanded: false`) derives `'collapsed'`; - a key `'Bad Key'` and a duplicate key are both refused. ## Pins moved (measured, mechanical) | file | pin | from → to | why | |:--|:--|:--|:--| | `object-collapsed-sections-echo-decisions.test.ts` | collapsed-section leaves / `advanced` | 61 → 69 / 52 → 60 | `indexes` + 3 sub-rows, 8 leaves | | same | open-section leaves | 100 → 114 | `fieldGroups` + 6 sub-rows, 14 leaves | | `object-lifecycle-panel-echo-decisions.test.ts` | translated `.label` control | 633 → 644 on the old base; **634** after the merge | 11 new row labels, per locale. The `origin/main` merge brought objectstack-ai#20357's move of the same pin to 623 (its `view` `tabs` repeater left, 10 labels), and the conflict was resolved by stacking both intents: 633 − 10 + 11 = 634 | | `packages/spec/src/data/field-rows-option-description.test.ts` | `icon` inputs on the object form | 1 → 2 | the `fieldGroups.icon` sub-row. Outside the claim's named surface: a population pin the new row moves mechanically. The pin now names both inputs by where they sit, and the options repeater's no-`icon` assertions are untouched | No lint census pin moved: `packages/lint/src/validate-predicate-path-refs.test.ts` passes unchanged (1 file, 54 tests). ## Verification Test runs went through `scripts/pm/os-verify-lock.sh`. Real lines. The suites ran on the merged head `215603c8`, whose tree equals the final head `c22fc1e2` except the changeset prose: | run | head | result | |:--|:--|:--| | `pnpm --filter @objectstack/spec test` | `215603c8` | `Test Files 564 passed (564)` · `Tests 16641 passed \| 1 todo (16642)` | | `pnpm --filter @objectstack/spec test:repo` | `215603c8` | `Test Files 37 passed (37)` · `Tests 684 passed (684)` | | `pnpm --filter @objectstack/platform-objects test` | `215603c8` | `Test Files 55 passed (55)` · `Tests 911 passed (911)`. The echo pins hold after the merge: 634, 69 / 60 and 114 | | `repeater-item-titles.test.ts` + `metadata-form-zod-reconciliation.test.ts` | `e8bdb1ad` | 2 files, 85 tests passed | | `pnpm --filter @objectstack/spec typecheck` | `215603c8` | exit 0; `check:test-typecheck: OK — … 53 file(s) / 251 error(s) / 138 pinned signature(s) held` | | `pnpm check:i18n` (after the closure build it names, 59 tasks, at `215603c8`) | `215603c8` | `check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys)`. The textually merged catalogues equal a fresh extract | | `pnpm --filter @objectstack/spec check:generated` | `215603c8` | `All 15 generated artifacts are up to date` | | `pnpm --filter @objectstack/platform-objects typecheck` | `92fb68da` | exit 0; `check:test-typecheck: OK — … 1 file(s) / 3 error(s) / 2 pinned signature(s) held` | | lint `src/validate-predicate-path-refs.test.ts` | `92fb68da` | 1 file, 54 tests passed | | cli unit `test/i18n-coverage.test.ts`, `test/i18n-duplicate-demand.test.ts` | `92fb68da` | 2 files, 27 tests passed | | metadata-protocol `src/protocol.meta-types-*.test.ts` | `92fb68da` | 4 files, 58 tests passed | Catalogues: `node scripts/check-i18n-bundles.mjs --write` regenerated the 22 `en` leaves. The 66 translated leaves were then authored in zh-CN, ja-JP and es-ES, with no en echo. A second `--write` kept every translated value and left no source-hash row, net zero. Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 86 commands at the final head `c22fc1e2`. That is the first round's 85 plus `check:skill-identifier-liveness`, which `object.zod.ts` brings in. All 86 ran on that head, each exit code went to disk before it was read, and every first run exited 0. `--ran` reports: `86 derived famil(ies) accounted for — 86 run, 0 NOT-MEASURED`. No ablation. This PR adds rows, three ledger rows and pin moves, and no guard. The nested with/without reading above is the measurement that the ledger rows are load-bearing. ## Acceptance notes - **objectui hides this `fieldGroups` row on the Studio object edit page.** At the pin, `ResourceEditPage.tsx` `CANVAS_OWNED_KEYS.object = ['fields', 'fieldGroups']`, because the form designer owns both. The row still reaches every other consumer of the served form: `getMetaTypes()`, the catalogues, and the reconciliation direction. The ruling chose the row knowing the designer edits groups. Carrier: none. - **objectui's `ObjectGroupInspector` comment is stale.** It says a group's `icon` and `description` have no consumer, but `DetailSection` renders both at the pin. It is only a comment. Carrier: none. - **`fieldGroups.visibleWhen` shares `CodeWidget`'s envelope bound.** A stored ADR-0089 envelope shows as `[object Object]`, and the first edit overwrites it. Carried by objectui#10963, the class fix for every `type: 'code'` expression row. - **The `unique` select cannot be cleared.** Once a scope is picked, making the index non-unique again means removing and re-adding the entry, or editing the source. This is a generic select bound. Carrier: none. - **Stored `true` / `false` display is not browser-run.** That it shows blank is a reading of the Radix select, not a measurement. - **`origin/main` was merged once, with `scripts/pm/os-regen-merge.sh`, at `6e3e5462`**, because objectstack-ai#20357 had moved the catalogues and the lifecycle pin. The only conflict was that pin (resolved above). No os-regen path needed main's side, and the rebuild regenerated nothing. `origin/main` has moved since, but not onto any file in this diff, so it was not merged again. ## Out-of-scope finding (the seat folds it into objectstack-ai#20432's family) - **class c · reach: the save door, measured.** `ObjectSchema` parses an index column that names no field. - The publish door and `os validate` have no rule for it. - The SQL driver skips the whole index at sync and logs it at `warn`. For a `unique` index, that leaves a declared constraint unenforced while the system looks normal. The sibling duplicate-row skip in the same function logs at `error` through `logDurabilityFailure`. - Likely family: objectstack-ai#20432 (field-name reference integrity). - Dedupe words: `indexes fields unknown column`, `declared index skipped column not materialized`, `index field reference integrity`, `unique index not enforced warn`. --- _Generated by [Claude Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…the record key is the member's name (objectstack-ai#20300) (objectstack-ai#20458) Fixes objectstack-ai#20300 Clause-②: no (narrowing) Retires the inner `name` on analytics cube measures and dimensions (`MetricSchema.name`, `DimensionSchema.name`). `measures` and `dimensions` are records, and the record key was always the member's identity: `GET /api/v1/analytics/meta` publishes every member as `CUBE.KEY`, and every consumer resolves a member by indexing the bag with its key. The inner copy was REQUIRED, read by nothing, and silently ignored when it disagreed with its key. ADR-0049 enforce-or-remove, by triage's verdict `5859547666` (RETIRE) under the maintainer's criterion, verbatim: 「每族该问的是:主流平台有没有这个能力 —— 有 ⇒ 补消费端(一次做对);没有 ⇒ 退役,而不是看仓里有没有人读」. Cube.dev and LookML key a member by its declared name, with no second inner name that can disagree. **Tier H.** The diff touches `skills/objectstack-ui/rules/dashboards.md` (a deletion only; see Deviations 1). It lands on the maintainer's word, then the seat lands it. 103 files, +1446 / -423 (1869 changed lines, under the 5,000 line class). ## Patch round after objectstack-ai#20390 (head `f639af5f3`) The sections below describe `c4771e604`. This head adds two commits and nothing else: - **`5ce26b0b2`** merges `origin/main` `75b2169243` through `os-regen-merge.sh`. The one hand conflict, `packages/spec/vitest.repo-tests.json`, was resolved by stacking both entries. Main’s step-18 siblings and this PR’s entries are all present in the four registries. - **`f639af5f3`** stamps `retiredAfter: 17.4.0` on `cube-member-inner-name-removed`. objectstack-ai#20390 (`e956924e1`) made the stamp required on every `retiredFromLoadPath: true` conversion. This entry is unpublished, so it takes the package label, as `view-list-tabs-removed` and `action-aria-removed` do on `main`. The net diff is 103 files, +1447 / −423: the stamp is the one added line. CI is green on this head. At-tier record `5875291969`: PASS. ## What this head carries (`c4771e604`) | surface | change | | --- | --- | | schema | `retiredKey()` tombstones on `MetricSchema.name` and `DimensionSchema.name` (both `strictObject`s, the `action.aria` posture). `tsc` types the key `never`, and the parse raises the prescription at `measures.KEY.name` / `dimensions.KEY.name`. The `measures` / `dimensions` describes now state that the record key IS the member's name. | | D2 | `cube-member-inner-name-removed` (protocol 18, `retiredFromLoadPath`), chained into `step18.conversionIds` with a rationale paragraph. It strips the inner `name` from every member of every `analyticsCubes[]` entry, and its notice names the cube. | | D3 | semantic entry `cube-member-inner-name-retired`: the judgement a DISAGREEING value still owes its author (which spelling was meant). | | registration | `RETIRED_KEYS_BY_MAJOR[18]` gains `data/Metric:name` and `data/Dimension:name` (per-file entries, generated region). | | ledger | both `analytics_cube.json` rows STAY `dead` (the tombstone keeps the key in the walked shape) with a `REMOVED 2026-09-28` note and a re-measured `verifiedAt`. The README row is updated; the counts do not move. | | producers | `dataset-compiler.ts` stops writing it (the triage line), and so do the two untyped internal mints tsc cannot see (`CubeRegistry.inferFromObject`, and `inferCubeFromQuery` / `inferMeasure` in `analytics-service.ts`). | | authors | the showcase cube (8 members), the `service-analytics` README example (3), and the published `objectstack-ui` skill example (6) | | fixtures | about 300 member literals and map-built members across 84 test and fixture files in eight packages; the three existing step-18 cube conversion fixtures are trimmed so the whole-table replay stays disjoint | | pins | `packages/spec/src/data/cube-member-inner-name-retirement.test.ts` covers every door (schema, `/meta` binding, `defineCube`, `defineStack` with its `STACK_SCHEMA_INVALID`/422 envelope, and a `@ts-expect-error` tsc leg), the D2 legs (stored row, boot door with a lit control, a disagreeing value, idempotence, load-path retirement), the registration, and a tree-scoped structural absence pin over the declared five-root radius. The flipped `analytics.test.ts` blocks (the snake_case pins on a value nothing read) are now tombstone pins. | | changeset | `@objectstack/spec` minor (BREAKING banner, FROM → TO, the one-line fix, what an author sees, and the ADR-0087 `registered` marker); `@objectstack/service-analytics` patch | What an author who still writes it sees: `tsc` fails at the authoring site. The parse refuses it with: "`measures.METRIC.name` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the metric's name. … Delete the key. To rename a metric, rename its key in `measures` — and every query, dashboard and report that names `CUBE.KEY`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand." A stored or built cube heals at rehydration and at the artifact door. ## Zone 2, measured - **A1 holds.** Zero reads of a member's inner `name` in non-test source (`analytics-service.ts#getMeta` and `memory-analytics.ts#getMeta` publish `CUBE.KEY`; `native-sql-strategy.ts#lookupMember` and `memory-analytics.ts#resolveMeasure` / `#resolveDimension` index the bag by key). Lit control: four reads of `measure.label` / `dimension.label` in the same two projections. The one `measure.name` hit (`dataset-compiler.ts:383`) is a `DatasetMeasure`, not a cube member. objectui at pin `f8a9d0fb05`: no cube-member authoring. `CubeSchema` is used only in `clientValidation.ts` (control: that hit resolves at the same sha). - **A2 holds, and is wider than the card.** Non-test producers: `dataset-compiler.ts` (2 sites), `CubeRegistry.inferFromObject` (3) and the ad-hoc mint in `analytics-service.ts` (4, plus `inferMeasure`'s 3 returns), the showcase, the README and the skill. Every one wrote the name EQUAL to its key, so no producer writes a disagreeing value. Test fixtures: 21 disagreed, all in `driver-memory` (e.g. `totalAmount: { name: 'total_amount' }`), and every one was queried by its key (`orders.totalAmount`). That is the trap, live in-repo. No `platform-objects` or template authors any cube. Stored rows: `analytics_cube` wraps as `analyticsCubes` at `applyConversionsToStoredItem`, and the pin's stored-row leg replays it. - **A3.** Worked on the merged cube contract (`public` enforced, objectstack-ai#20348 landed). Line numbers are from the merged tree. - **A4.** Merged `origin/main` `6e3e5462c` (which carries objectstack-ai#20357's step-18 appends) with `bash scripts/pm/os-regen-merge.sh`. - The driverless merge-tree (a bare shared clone with no `merge.os-regen.driver` registered) answered exit 0 with no conflicted paths. - The script's step 2 took main's side of `content/docs/references/data/analytics.mdx`, which was regenerated from the merged tree in its own commit (`e19628132`). - After the merge: both sides' ids are present in both step-18 lists (`view-list-tabs-removed` and `cube-member-inner-name-removed`) and in the rationale. - After the merge: `check:generated` reported all 15 artifacts current, measured right after a spec build of the merged tree. ## Deviations 1. **The `skills/**` split was ordered, then withdrawn.** The seat ordered the skill hunk split into a companion PR, and withdrew that on this measurement: - The example is an `os:check` block that `check:skill-examples` type-checks inside `typecheck-consumers`, a member of the required `TypeScript Type Check` aggregator. - Putting one inner `name` back into the example with `ablation-replace` (restored to the HEAD blob, `git diff HEAD` empty) gave exit 1: `dashboards.md:450:15 error TS2322: Type 'string' is not assignable to type 'undefined'`. So this PR without the hunk is red. - A companion PR alone on `main` would be red too: at base `dbddf02c1`, `MetricSchema.name` is a REQUIRED `z.string()`. That half is derived from the schema, not built. - No landing order is green, so the hunk stays here, as a pure deletion. 2. **File surface wider than the claim, same package and same defect class.** `CubeRegistry.inferFromObject` and the `analytics-service.ts` mints are untyped (a `Record` of `any`) producers that tsc cannot see; A2 put every producer in scope. Fixture edits span `service-analytics`, `driver-memory`, `spec`, `client`, `objectql`, `runtime`, `qa/dogfood` and `qa/downstream-contract`. 3. **Route.** The `spec-property-retirement` skill's route table maps `.strict()` to deletion plus a guidance map. I took the triage's `retiredKey()` route instead, which `shared/retired-key.ts` documents for closed shapes (strictly stronger than a guidance entry) and which `action.aria` used this week. As a result the ledger rows stay, per the card's acceptance. The `CubeJoinSchema` docblock line that said cube shapes never take a tombstone is corrected. 4. **D2 strips a disagreeing value too.** Triage: "lossless when it equals the key; a disagreeing value gets a D3 entry". The D3 entry exists. The strip still removes a disagreeing value because the key already won everywhere, so no answer changes, and leaving it would stop the cube loading at the boot door. The notice prints both spellings (`from: name "total_amount"`, `to: (removed; the record key "totalAmount" is the name)`). 5. **`service-analytics` is graded `patch`.** Its members are filed under the same keys, and every `/analytics/*` answer is unchanged. `@objectstack/spec` carries `minor`: a published narrowing ships `minor` in the launch window, and the changeset declares it as `Clause-②: no (narrowing)` under its BREAKING banner. ## Tests (head `c4771e604` unless stated) - **`@objectstack/spec`:** - `test` 564 files / 16641 tests green (merged tree `e19628132`; spec `src/` is unchanged since). - `test:repo` 37 / 675 green (pre-merge `c1cae40df`). Post-merge, its three tree-reading legs this diff owns were re-run green: the retirement pin, `retired-key-migrate-sentence`, and `build-schemas-check-mode` (107 tests together with the pin). - `typecheck` green (src, scripts, and the test layer under its shrink-only ledger). `tsc -p tsconfig.test.json --listFilesOnly` lists the new pin, so its `@ts-expect-error` legs are live. - **`@objectstack/service-analytics`:** typecheck green, 132 files / 3093 tests green (`14cac89f4`). - **`@objectstack/driver-memory`:** typecheck (which reaches the test layer) green, 57 / 1374 green (`14cac89f4`). tsc found the two map-built members there; the same shape was then swept in service-analytics and runtime. - **Touched test files in other packages:** `client` 7/7, `objectql` `protocol-meta` 95/95, `runtime` `cross-field-refusal-operand-withhold` 11/11, `downstream-contract` `contract.test.ts` 13/13 plus typecheck exit 0. - **Reverse verification.** Putting `name: m.name` back in `dataset-compiler.ts` via `ablation-replace` gave `src/dataset-compiler.ts(673,7): error TS2322: Type 'string' is not assignable to type 'undefined'` against the rebuilt spec `.d.ts`. Restored: blob equals HEAD, `git diff HEAD` empty. The direction was the predicted one (red). - **Gates.** `dispatch-gates.mjs --commands` at `c4771e604` derives 126 families. All 126 were run and recorded, and `--ran` reports 0 UNRUN. - 123 exit 0. - 2 exit 3, PREREQUISITE NOT MET: `check:dual-build-cjs-loads` and `check:type-check-debt` (both need the whole-repo build). - 1 exit 1: `check:platform-checklist`, inherited from `main` (see Acceptance notes). Its inputs are byte-identical to `main`, and it is not a per-PR CI gate. - **Lint (a proven narrowing).** - Scope: `eslint --no-inline-config --format json` over exactly the 95 changed code files returned 0 errors and 0 warnings. - Population: read from `eslint.config.mjs` (`files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`); the file count comes from the JSON output. - Invariance: the config itself records that it never enables type-aware linting (no `parserOptions.project`), so no untouched file's verdict can move. **NOT MEASURED** (CI runs these): - `qa/dogfood`: the two touched dogfood tests and `expression-conformance`. The package does not resolve `@objectstack/verify` unbuilt, so no test ran. Static reading: 0 `analytics.zod` references in that ledger, against 11 `.zod.ts` references as the control. - The showcase `typecheck`: 7 TS2307 for unbuilt connectors and plugins, and 0 diagnostics in `showcase.cube.ts`. - `downstream-contract`'s `consumer-specifier-ledger` needs `@objectstack/cli` built. - The two exit-3 gates above. ## Skills readings `skills/objectstack-ui/rules/dashboards.md` goes 468 → 468 lines. The whole package (every `SKILL.md`) goes 4404 → 4404. Against base the hunk is 6 lines modified and nothing added: each change deletes a `name: 'KEY',` fragment. ## Acceptance notes (noted, not filed) - `check:platform-checklist` is red on `main` at `3cf644938`: - The failure: `areas/identity-auth.json` anchors `plugin-auth/src/auth-plugin.ts#twoFactor`, and `7d6308895` (objectstack-ai#20429) turned that line-start key into an inline nested object key (`plugins: { twoFactor: true }`), which the shared resolver reads as absent by design. - It is independent of this diff: both files are byte-identical to `main`. - The gate is run by hand, not per PR. Carrier: the next PR to touch `identity-auth.json` or `auth-plugin.ts`, or the checklist owner. - The inner `name` carried a snake_case regex. The record key never had one, and it legitimately takes camelCase and dotted spellings in-repo (`driver-memory` fixtures; `'owner.amount_sum'` in `dotted-measure-refusal.test.ts`). Nothing is enforced on the key today; this is an observation, not a change here. - The absence pin states its blind spot: members built under computed keys (`Object.fromEntries(… { name: n, … })`). tsc found the typed ones in `driver-memory`, and the rest of that shape was swept by an AST scan (object literals holding `name`, `sql` and `type`). What remains is only this pin's own refusal specimens and the schema shape. ## 维护者速读(草稿) **改了什么**:分析立方体(cube)的度量与维度不再接受内部 `name` 字段;成员的名字就是它在 `measures` / `dimensions` 里的键。写了 `name` 会在编译期和解析期被明确拒绝,并给出迁移提示。已存储的立方体在加载时自动去掉该字段,照常可用。 **为什么改**:这个字段从来没有任何代码读取,系统一律按键识别成员;当 `name` 与键不一致时,作者写的值被静默忽略。Cube.dev、LookML 等主流方案也只有一个名字。按您「主流平台有没有这个能力」的判据,判定退役。 **风险与代价(含回滚)**:对外行为不变 —— `/analytics/meta` 与查询接口的成员名仍是 `立方体.键`。仍写 `name` 的作者源码需要删除该字段(`os migrate meta --from 17` 列出改动)。回滚:还原本 PR 即可,无数据迁移需要撤销。本 PR 同时改了一个对外发布的 skill 示例(仅删除 `name`),因此需要您的批准。 **席位意见**: **你要做的**:审阅后批准(Approve)本 PR。 Line 3 and Deviation 5 were amended by the `domain:spec` seat 1 (`session_01B3TqpoQbTAfG7G74GMDWNW`) before the at-tier review: this diff widens no accept set and adds no export, so `Clause-②` is `no (narrowing)`, as most retirements of this family on `main` declare. `20323-action-aria-removed.md` declared `yes`; the definition in `clause2-line.mjs` decides, not the precedent. The changeset line moved with it in `2252e5728`, and the claim on objectstack-ai#20300 was amended in place. The patch-round section above was added by the same seat after the at-tier record `5875291969`. --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #20301
Clause-②: no (narrowing)
Stage 1 of a staged card: the list view's own
tabskey only. #20301 remains open for its second key, the view container's bodyname, which waits until cloud's writer stops sending it (triage note 2). This PR does not touch the container'sname.What
ListViewSchema.tabs(and the same key onObjectListViewSchema, a view item record's listconfig, and the flattened list overlay atPUT /api/v1/meta/view) is retired under ADR-0049 enforce-or-remove. The triage verdict was RETIRE under the maintainer's family criterion: named-view switching is mainstream and is already delivered here bylistViews, rendered by the saved-view switcher (ViewTabBar).tabsbecomes aretiredKey()tombstone insideListViewShapeSchemaonly, beside thepageNametombstone on the same strict shape. Authoring it is atscerror (input typenever) and a parse error that carries the prescription: delete the key, and move each tab to a namedlistViewsentry (tabnamebecomes the entry key,labelthe entrylabel, itsfilterrules join the view's ownfilter, copycolumns).view-list-tabs-removed(protocol 18, retired from the load path) strips the key from every list payload instack.views[], in all three persisted spellings. D2 chosen: the delete is lossless in pixels because nothing ever read or drew the key, so a storedviewrow replays without it at every rehydration seam and re-saves through the strict write door instead of being refused there over a key that never had an effect. One D3 entry,list-view-tabs-retired, per ruling B on [Decision] 一次退役,要写一条记录还是两条?—— 迁移条目的 D2/D3 约定,两处成文相互矛盾 #17152, with no tracker number in any author-shown field.RETIRED_KEYS_BY_MAJOR[18]gainsui/ListView:tabsandui/ObjectListView:tabs. Declared boundary (same as the three sibling list-view conversions): an object's ownlistViewsis reached by no conversion, so such an object is refused at its own door.view.json/props/list/children/tabsstaysdead(tombstone discipline), re-verified 2026-09-27, with a REMOVED note. The staleview/list.tabsrow leavesundrilled-containers.baseline.json(the tombstone is no longer a container, whichcheck:livenessreported).tabsrepeater leaves with the key; the extracted form-label bundles are regenerated (pure deletion, 4 files). Reference docs,authorable-surface/ui.json(two rows become[RETIRED]) and the migration registry are regenerated by their generators. The publishedobjectstack-uiskill's list-view rules stop teaching the key.layout-dsl.mdxstops pointingViewTabat list views. Changeset:@objectstack/specminor, BREAKING, FROM to TO table.view.test.tsdrops "should accept list view with tabs" and the key from the full-parity fixture; two wire-door tests move their tab carrier to the survivinguserFilters.tabs.ViewTabSchemais not retired: the page-onlyuserFilters.tabspreset bar reuses it and renders. Pinned as a boundary.Measured first (premise holds)
tabshas no reader, and objectui'sTabBar, the one component that would draw it, has zero production mounts.userFilters.tabsis a different key with the same element type (z.array(ViewTabSchema)): objectui'sUserFilterspasses it toTabFilters, which renders it in production as a page list's preset bar, and it stays.f8a9d0fb(.objectui-sha):git grep -Efor aTabBarJSX mount = 10 hits in 2 files, both test files (components/__tests__/TabBar.i18nLabel.test.tsx,__tests__/icon-seam-fallbacks-5935.test.tsx); everyTabBarimport is a test's. Lit control, same instrument and corpus: aViewTabBarJSX mount in production source = 1 file,packages/app-shell/src/views/ObjectView.tsx(views={viewTabItems}, built fromobjectDef.listViews). No production.tabsread of a list view anywhere inpackages/*/srcat the pin. objectui's type mirror (types/src/objectql.ts,tabs?: ListViewSchema['tabs']) and its zod twin import the spec field by reference, so the tombstone flows in on the next pin bump, thepageNameprecedent; no export is removed here, so the Console Pin Gate build is unaffected.4e0f72e8d: zero list-viewtabsauthorings inexamples/or in platform sources (the 2tabs:hits inexamples/are a pageuserFilterspreset bar and a record-page slot); control:listViews:authored in 9 example files. The one published skill example that taught the key (skills/objectstack-ui/rules/list-views.md) is corrected here. No platform writer sends it.packages/lint/src/validate-list-view-field-refs.ts#checkTabs,computeViewReferenceDiagnosticsinpackages/metadata-protocol/src/metadata-diagnostics.ts). Neither draws a tab bar; the ledger already graded them as not delivering the key's effect. Left in place, see Acceptance notes.packages/lint/src/validate-capability-references.ts:186(a genericrec.tabswalk) does NOT reach a list view: its walk starts atstack.apps[*]and descends only an app node'snavigation/areas/tabs/children/items, so it never visits a view. A third reader, found by CI and not by the pre-change census: the derived reference-site index behindfindReferencesToMeta(the Studio "Used by" panel) walks each type's schema, andlistViews.*.tabs[].view(aViewTabnaming a list view) was the ONLY property underobjectspelled as aviewreference. With the key retired the walk finds none, so a view's "Used by" scan no longer readsobjectrows. Measured by an ablation of the tombstone over theobjectinput schema: 1 view-spelled path now (listViews.*.navigation.view, itself retired), 2 with the tombstone removed (pluslistViews.*.tabs[].view). No such reference is authorable any more; a stored object row still carrying one is refused at its own door (no conversion reaches an object'slistViews).liveness-dead-propertyandliveness-live-elsewhere-propertycannot fire on 17.3.0 — 90dead+ 1live-elsewhereledger rows and not one setsauthorWarn#16094) is inherited, NOT MEASURED here (out of this seat's repo scope). Cloud: NOT MEASURED here; the card's census at cloud96eb092names cloud writers forview.nameonly.Ablation (the tombstone is what shuts every door)
Run on committed HEAD
74f8ab9727withnode scripts/ablation-replace.mjs(wrap mode, restore armed on EXIT/INT/TERM) againstpackages/spec/src/ui/view.zod.ts:tabs: retiredKey(x1 to x0, replacement (tabsback to a plainViewTabarray, the tombstone parked on a dummy key) x0 to x1, blobb64e9996f3fcto90f0a141e429;vitest run src/ui/view-list-tabs-retirement.test.ts: 11 failed, 18 passed (29). Red: the refusal at all seven doors, the empty-array pin, the prescription pin, the parse-channel half of the tsc pin, and the object-door boundary. Still green, as expected: the D2 conversion, registration, form,userFilters.tabsboundary and absence pins, which do not depend on the tombstone. Direction observed: turned red;b64e9996f3fcequals the HEAD blob,git diff HEADempty, tombstone anchor count 1, mutant count 0.The tsc door is proven by the positive run:
check:test-typecheckis green with an@ts-expect-erroron thetabsline of adefineViewcall, and an unneeded directive is itself a TS2578 in that same program. It was not separately ablated under tsc.Verification
Rework round 1, head
606046e96a. The at-tier review failed on the truth of two sentences; the retirement itself stood. (1) "The one component that reads aViewTab[]has zero production mounts" was false, becauseuserFilters.tabsis aViewTab[]thatTabFiltersrenders. It now reads: a list view's owntabshas no reader, andTabBar, the one component that would draw it, has zero production mounts;userFilters.tabsis read, rendered, and stays. Corrected in the changeset, the tombstone docblock inview.zod.ts, both retired-key entries, the conversion docblock, the step-18 rationale, the ledger note, the pin's header, and this body. (2) "examples, skills or platform sources" now reads "examples or platform sources", plus the note that the one published skill example that taught it is corrected here.mainmerged atd0003a10c7withscripts/pm/os-regen-merge.sh: the hand-written registry tails conflicted with #20251 and both sides were kept, main's first. That also repaired a join from the previous merge that had dropped a period and space before this PR's rationale sentence. The object reference page was regenerated at8845cb564e, and the prose corrections landed atc9f81fa132.mainmoved again (the RLS policytagsretirement) and was merged at606046e96a, again keeping both registry tails, main's first; the migration registry regenerated byte-identically. Checks atc9f81fa132: spec build +check:generatedall 15 up to date;check:liveness,check:adr-0087-registration,check-changeset-no-major,check-empty-changeset,check:doc-authoring,check:nul-bytes,check:issue-citations, spec docblock anchors, comment-mask adoption andcheck:cross-package-test-inputsall exit 0; the changeset'sClause-②: no (narrowing)line reads back as declared. Targeted spec tests atc9f81fa132: 5 files / 863 tests passed (view-list-tabs-retirement,conversions,migrations,view,retired-key-migrate-sentence). At606046e96a: the source-reading spec gates (check:migration-registry,check:spec-changes,check:upgrade-guide,check:liveness,check:authorable-surface,check:docs),check-comment-mask-corpusandcheck:query-options-erasureall exit 0. The shared verify lock was not acquired for a local post-merge spec rebuild. CI on606046e96a: 35 check runs, 33 success and 2 skipped, including TypeScript Type Check (the generated-artifact gates), Test Core 6/6 and Lint & Repo Gates. Derived gates at606046e96a: 122, 114 run green, 8 NOT MEASURED. Five of those lost their build prerequisites when a local turbo rebuild was OOM-killed on the shared box:check:doc-formula-expressions,check:doc-security-posture,check:skill-examples,check:docs-transcript-driftandcheck:lean-entry-closure, all green ata3f8054dccand covered by CI. The other three arecheck:i18n,check:dual-build-cjs-loadsandcheck:type-check-debt.CI repair round, head
a3f8054dcc(main merged again atbfa765ca08, reference pages regenerated atc13e610ab5). Four consumer pins of the retired key, in packages the first round did not run, moved to the post-retirement truth, none weakened:repeater-row-properties.test.tsnow assertsview:tabsABSENT beside the two surviving view repeaters;object-lifecycle-panel-echo-decisions.test.tspositive control 608 to 598 (the repeater's label plus its nine row labels), with the reason in the pin's comment;protocol.graft-normalized-operators.test.tskeeps its view-filter leg and moves the nestedViewTab.filterleg to the survivinguserFilters.tabscarrier;protocol.read-seam-empty-accumulator.test.tsreads the consulted source types from the derived index and pinsobjectabsent (see Measured first). Full suites ata3f8054dcc:@objectstack/platform-objects55 files / 911 tests passed;@objectstack/metadata-protocol189 files passed, 3 skipped (pre-existing) / 2736 tests passed;@objectstack/lint112 files / 4640 tests passed;@objectstack/objectqlmetadata-diagnostics.test.ts10 passed. Derived gates: 122, 119 run green, 3 NOT MEASURED (check:i18n: its CLI prerequisite build never acquired the shared lock, green atbaa383008bon the same bundles;check:dual-build-cjs-loads;check:type-check-debt).First round, at merged HEAD
baa383008bunless stated.pnpm --filter @objectstack/spec buildthencheck:generated: All 15 generated artifacts are up to date (migration registry, spec-changes, upgrade guide, authorable surface, api-surface, docs, liveness counts, test-typecheck and the rest).pnpm --filter @objectstack/spec typecheck: exit 0 (tsc, scripts typecheck, and test typecheck: 53 files / 255 errors / 142 pinned signatures held, unchanged).pnpm --filter @objectstack/spec test(local project): 554 files passed, 16426 tests passed, 1 todo.test:repoat74f8ab9727: 34 files, 633 tests passed. After mergingmainagain (the flattened-overlayowner/hiddenretirement landed and conflicted only in the two hand-written registry tails; both sides kept), targeted rerun of the two repo-project retirement pins atbaa383008b: 2 files, 47 tests passed. Declared narrowing: the other 32 repo-project files were not rerun after that merge; CI runs them.node scripts/check-i18n-bundles.mjs --writeregenerated the fourmetadata-formsbundles;pnpm check:i18nexit 0.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, reconciled with--ran): 121 derived, 119 run, 2 NOT MEASURED, 0 unrun. Named in the dispatch and green:check:liveness,check:migration-registry,check:generated,check:spec-changes.check:dual-build-cjs-loads(exit 3, needs a full workspace build),check:type-check-debt(repo-wide tsc re-measure of every package exceeded the foreground cap on a shared box).check:dts-closureexit 1 on tree state, not on this diff: five packages this PR does not touch (cloud-connection, organizations, plugin-approvals, plugin-dev, verify) hold JS-onlydist/in this worktree after a shared turbo cache replay during an unrelated closure build; the same gate was exit 0 at74f8ab9727before that build. CI builds fresh.--no-inline-config --format jsonover the 15 changed JS/TS files: 15 files, 0 errors, 0 warnings, none ignored. Population ispnpm lint(eslint . --no-inline-config,eslint.config.mjs); invariance: that config enables no type-aware linting (noparserOptions.project, stated ineslint.config.mjs), so this diff cannot move any untouched file's verdict.Governed surface: Tier H
skills/objectstack-ui/rules/list-views.mdis underskills/**, so this PR is Tier H: it lands only by the maintainer's hand or an authorized approval. No generated file underskills/**changed.Readings for the
skills/**edit (lines): the edited file 309 to 302 (net -7, pure deletion: the list-leveltabsexample and a rule that described a tab bar that never rendered, replaced by a 3-linelistViewspointer). Whole pack, all 10skills/**/SKILL.md: 4402 to 4402 (no SKILL.md edited).Acceptance notes
Out of this card's file surface, noted and not filed (dead code class; carrier named):
tabsoff raw input, and their test fixtures still author it (packages/lint/src/validate-list-view-field-refs.test.ts,packages/objectql/src/metadata-diagnostics.test.ts, plus the CLI's negative i18n pinpackages/cli/test/i18n-tab-coverage.test.ts). After this PR the parse refuses the key first, so those branches are unreachable for valid input. The tree-scoped absence pin declares these three files as self-expiring residue: each is asserted to still hold an offender, so the day a follow-up deletes the walks, the pin forces the entry out. Carrier: none named.packages/cli/src/utils/i18n-extract.tsandpackages/lint/src/validate-translation-references.tscarry prose that says the list view's owntabs"has no reader"; still true, now for a stronger reason. Carrier: none named.types/src/objectql.tskeepstabs?: ListViewSchema['tabs'](becomesundefined-typed) and its parity tests may want the row updated. Carrier: the objectui pin-bump PR.维护者速读(草稿)
改了什么
列表视图上的
tabs字段被正式退役。以后在列表视图里写tabs,类型检查和保存都会直接报错,错误信息会告诉作者改用listViews(每个命名视图就是切换条上的一个页签)。存量数据里残留的这个字段,升级时会被自动删掉。为什么改
这个字段一直能写、能存,但没有任何代码读它,界面上也从来没有画出过任何东西:本来要画它的那个页签组件在正式代码里一次都没被用上;真正显示在记录列表上方的页签条,是按
listViews逐个列出的已存视图。页面筛选栏里的userFilters.tabs是另一个字段,照常显示,不受影响。同一个能力两种写法,其中一种完全无效,AI 和作者都容易写错。按“主流平台已有、本平台已交付 ⇒ 只保留一种写法”的判据退役。风险与代价(含回滚)
属于破坏性变更:外部仓库里若有人写了这个字段,升级后会被拒绝(报错里带修改方法)。本仓示例与平台代码实测零使用,唯一教过这种写法的已发布技能示例已在本 PR 中改正;外部使用面未测量。存量数据自动清理,不影响任何已有画面。回滚:revert 本 PR 即可恢复该字段。
席位意见
你要做的
本 PR 触及已发布技能目录(
skills/**),属于 Tier H,需要你本人批准后才能合并。Generated by Claude Code