Skip to content

feat(spec)!: retire the list view's own tabs key; named presets are listViews entries - #20357

Merged
objectstack-fleet[bot] merged 21 commits into
mainfrom
claude/issue-20301-list-tabs-retired
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 21 commits into
mainfrom
claude/issue-20301-list-tabs-retired

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Part of #20301

Clause-②: no (narrowing)

Stage 1 of a staged card: the list view's own tabs key only. #20301 remains open for its second key, the view container's body name, which waits until cloud's writer stops sending it (triage note 2). This PR does not touch the container's name.

What

ListViewSchema.tabs (and the same key on ObjectListViewSchema, a view item record's list config, and the flattened list overlay at PUT /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 by listViews, rendered by the saved-view switcher (ViewTabBar).

  • Tombstone. tabs becomes a retiredKey() tombstone inside ListViewShapeSchema only, beside the pageName tombstone on the same strict shape. Authoring it is a tsc error (input type never) and a parse error that carries the prescription: delete the key, and move each tab to a named listViews entry (tab name becomes the entry key, label the entry label, its filter rules join the view's own filter, copy columns).
  • ADR-0087. D2 conversion view-list-tabs-removed (protocol 18, retired from the load path) strips the key from every list payload in stack.views[], in all three persisted spellings. D2 chosen: the delete is lossless in pixels because nothing ever read or drew the key, so a stored view row 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] gains ui/ListView:tabs and ui/ObjectListView:tabs. Declared boundary (same as the three sibling list-view conversions): an object's own listViews is reached by no conversion, so such an object is refused at its own door.
  • Ledger. The row view.json /props/list/children/tabs stays dead (tombstone discipline), re-verified 2026-09-27, with a REMOVED note. The stale view/list.tabs row leaves undrilled-containers.baseline.json (the tombstone is no longer a container, which check:liveness reported).
  • Kit. The metadata form's tabs repeater 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 published objectstack-ui skill's list-view rules stop teaching the key. layout-dsl.mdx stops pointing ViewTab at list views. Changeset: @objectstack/spec minor, BREAKING, FROM to TO table.
  • Tests that pinned the retired acceptance were flipped or re-pointed: view.test.ts drops "should accept list view with tabs" and the key from the full-parity fixture; two wire-door tests move their tab carrier to the surviving userFilters.tabs.

ViewTabSchema is not retired: the page-only userFilters.tabs preset bar reuses it and renders. Pinned as a boundary.

Measured first (premise holds)

  • What is true, stated first: a list view's own tabs has no reader, and objectui's TabBar, the one component that would draw it, has zero production mounts. userFilters.tabs is a different key with the same element type (z.array(ViewTabSchema)): objectui's UserFilters passes it to TabFilters, which renders it in production as a page list's preset bar, and it stays.
  • objectui at the pin f8a9d0fb (.objectui-sha): git grep -E for a TabBar JSX mount = 10 hits in 2 files, both test files (components/__tests__/TabBar.i18nLabel.test.tsx, __tests__/icon-seam-fallbacks-5935.test.tsx); every TabBar import is a test's. Lit control, same instrument and corpus: a ViewTabBar JSX mount in production source = 1 file, packages/app-shell/src/views/ObjectView.tsx (views={viewTabItems}, built from objectDef.listViews). No production .tabs read of a list view anywhere in packages/*/src at 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, the pageName precedent; no export is removed here, so the Console Pin Gate build is unaffected.
  • This repo at base 4e0f72e8d: zero list-view tabs authorings in examples/ or in platform sources (the 2 tabs: hits in examples/ are a page userFilters preset 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.
  • Readers in this repo: two author-time reference walks read the key off RAW input (packages/lint/src/validate-list-view-field-refs.ts#checkTabs, computeViewReferenceDiagnostics in packages/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 generic rec.tabs walk) does NOT reach a list view: its walk starts at stack.apps[*] and descends only an app node's navigation / 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 behind findReferencesToMeta (the Studio "Used by" panel) walks each type's schema, and listViews.*.tabs[].view (a ViewTab naming a list view) was the ONLY property under object spelled as a view reference. With the key retired the walk finds none, so a view's "Used by" scan no longer reads object rows. Measured by an ablation of the tombstone over the object input schema: 1 view-spelled path now (listViews.*.navigation.view, itself retired), 2 with the tombstone removed (plus listViews.*.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's listViews).
  • hotcrm's inert-key test (lint: liveness-dead-property and liveness-live-elsewhere-property cannot fire on 17.3.0 — 90 dead + 1 live-elsewhere ledger rows and not one sets authorWarn #16094) is inherited, NOT MEASURED here (out of this seat's repo scope). Cloud: NOT MEASURED here; the card's census at cloud 96eb092 names cloud writers for view.name only.

Ablation (the tombstone is what shuts every door)

Run on committed HEAD 74f8ab9727 with node scripts/ablation-replace.mjs (wrap mode, restore armed on EXIT/INT/TERM) against packages/spec/src/ui/view.zod.ts:

  • mutation landed on disk: anchor tabs: retiredKey( x1 to x0, replacement (tabs back to a plain ViewTab array, the tombstone parked on a dummy key) x0 to x1, blob b64e9996f3fc to 90f0a141e429;
  • 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.tabs boundary and absence pins, which do not depend on the tombstone. Direction observed: turned red;
  • restore proven by bytes, not exit code: blob after restore b64e9996f3fc equals the HEAD blob, git diff HEAD empty, tombstone anchor count 1, mutant count 0.

The tsc door is proven by the positive run: check:test-typecheck is green with an @ts-expect-error on the tabs line of a defineView call, 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 a ViewTab[] has zero production mounts" was false, because userFilters.tabs is a ViewTab[] that TabFilters renders. It now reads: a list view's own tabs has no reader, and TabBar, the one component that would draw it, has zero production mounts; userFilters.tabs is read, rendered, and stays. Corrected in the changeset, the tombstone docblock in view.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. main merged at d0003a10c7 with scripts/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 at 8845cb564e, and the prose corrections landed at c9f81fa132. main moved again (the RLS policy tags retirement) and was merged at 606046e96a, again keeping both registry tails, main's first; the migration registry regenerated byte-identically. Checks at c9f81fa132: spec build + check:generated all 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 and check:cross-package-test-inputs all exit 0; the changeset's Clause-②: no (narrowing) line reads back as declared. Targeted spec tests at c9f81fa132: 5 files / 863 tests passed (view-list-tabs-retirement, conversions, migrations, view, retired-key-migrate-sentence). At 606046e96a: the source-reading spec gates (check:migration-registry, check:spec-changes, check:upgrade-guide, check:liveness, check:authorable-surface, check:docs), check-comment-mask-corpus and check:query-options-erasure all exit 0. The shared verify lock was not acquired for a local post-merge spec rebuild. CI on 606046e96a: 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 at 606046e96a: 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-drift and check:lean-entry-closure, all green at a3f8054dcc and covered by CI. The other three are check:i18n, check:dual-build-cjs-loads and check:type-check-debt.

CI repair round, head a3f8054dcc (main merged again at bfa765ca08, reference pages regenerated at c13e610ab5). 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.ts now asserts view:tabs ABSENT beside the two surviving view repeaters; object-lifecycle-panel-echo-decisions.test.ts positive 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.ts keeps its view-filter leg and moves the nested ViewTab.filter leg to the surviving userFilters.tabs carrier; protocol.read-seam-empty-accumulator.test.ts reads the consulted source types from the derived index and pins object absent (see Measured first). Full suites at a3f8054dcc: @objectstack/platform-objects 55 files / 911 tests passed; @objectstack/metadata-protocol 189 files passed, 3 skipped (pre-existing) / 2736 tests passed; @objectstack/lint 112 files / 4640 tests passed; @objectstack/objectql metadata-diagnostics.test.ts 10 passed. Derived gates: 122, 119 run green, 3 NOT MEASURED (check:i18n: its CLI prerequisite build never acquired the shared lock, green at baa383008b on the same bundles; check:dual-build-cjs-loads; check:type-check-debt).

First round, at merged HEAD baa383008b unless stated.

  • pnpm --filter @objectstack/spec build then check: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.
  • Repo project: full test:repo at 74f8ab9727: 34 files, 633 tests passed. After merging main again (the flattened-overlay owner / hidden retirement landed and conflicted only in the two hand-written registry tails; both sides kept), targeted rerun of the two repo-project retirement pins at baa383008b: 2 files, 47 tests passed. Declared narrowing: the other 32 repo-project files were not rerun after that merge; CI runs them.
  • i18n: node scripts/check-i18n-bundles.mjs --write regenerated the four metadata-forms bundles; pnpm check:i18n exit 0.
  • Derived gate families (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.
  • NOT MEASURED: 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-closure exit 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-only dist/ in this worktree after a shared turbo cache replay during an unrelated closure build; the same gate was exit 0 at 74f8ab9727 before that build. CI builds fresh.
  • Lint, narrowed and proven: eslint --no-inline-config --format json over the 15 changed JS/TS files: 15 files, 0 errors, 0 warnings, none ignored. Population is pnpm lint (eslint . --no-inline-config, eslint.config.mjs); invariance: that config enables no type-aware linting (no parserOptions.project, stated in eslint.config.mjs), so this diff cannot move any untouched file's verdict.

Governed surface: Tier H

skills/objectstack-ui/rules/list-views.md is under skills/**, so this PR is Tier H: it lands only by the maintainer's hand or an authorized approval. No generated file under skills/** changed.

Readings for the skills/** edit (lines): the edited file 309 to 302 (net -7, pure deletion: the list-level tabs example and a rule that described a tab bar that never rendered, replaced by a 3-line listViews pointer). Whole pack, all 10 skills/**/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):

  1. The two author-time walks above still read a list view's tabs off 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 pin packages/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.
  2. packages/cli/src/utils/i18n-extract.ts and packages/lint/src/validate-translation-references.ts carry prose that says the list view's own tabs "has no reader"; still true, now for a stronger reason. Carrier: none named.
  3. objectui at its next pin bump: types/src/objectql.ts keeps tabs?: ListViewSchema['tabs'] (becomes undefined-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

…, 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>
…st-tabs-retired

# Conflicts:
#	packages/spec/src/conversions/registry.ts
#	packages/spec/src/migrations/registry.ts
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec, touching 19 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/view.json, packages/spec/src/migrations/entries/retired-keys/18.ui__ListView__tabs.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via isDefault (literal, a string literal in sections))
  • content/docs/permissions/authorization.mdx (via isDefault (literal, a string literal in sections))
  • content/docs/permissions/permission-metadata.mdx (via isDefault (literal, a string literal in sections))
  • content/docs/permissions/permission-sets.mdx (via isDefault (literal, a string literal in sections))
  • content/docs/permissions/positions.mdx (via isDefault (literal, a string literal in sections))
  • content/docs/permissions/profiles.mdx (via isDefault (literal, a string literal in sections))
  • content/docs/protocol/objectui/layout-dsl.mdx (via isDefault (literal, a string literal in sections))
  • content/docs/ui/apps.mdx (via isDefault (literal, a string literal in sections))
  • content/docs/ui/pages.mdx (via isDefault (literal, a string literal in sections))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via isDefault (literal, a string literal in sections))
  • content/docs/releases/v13.mdx (via isDefault (literal, a string literal in sections))
  • content/docs/releases/v15.mdx (via isDefault (literal, a string literal in sections))
  • content/docs/releases/v17/17-0.mdx (via isDefault (literal, a string literal in sections))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/view.json, packages/spec/src/migrations/entries/retired-keys/18.ui__ListView__tabs.ts, …) — pages documenting those are invisible to this run
  • 10 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c745e2b896ba2c9382ecd208a40799a44226cddb → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d564f7e7c4678aa81d86dda17119509af76860fd — the merge of head c1c3ec90ddbc149d1d1515f693a887961b893cc8 into base c745e2b896ba2c9382ecd208a40799a44226cddb, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c745e2b896ba2c9382ecd208a40799a44226cddb → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 80/80 CONTRACT_REVIEW_TIER
Head-sha: a3f8054dccc3ff3814363139c34b0d50ef88ddb8

① Derived judgments

BLOCKING (item 6, changeset truth) — two sentences in .changeset/list-view-tabs-retired.md are false by the PR's own other statements:

  • "the one component that reads a ViewTab[] (objectui's TabBar) has zero production mounts": objectui packages/plugin-list/src/UserFilters.tsx reads config.tabs (:225) typed ListViewSchema['userFilters']['tabs'] (:756), i.e. a ViewTab[], and renders it in production; the changeset itself says the preset bar renders. Fix: "the one component that reads a list view's own tabs". Same phrase in comments (view.zod.ts:2776, migrations/registry.ts x2, both retired-key entries, conversions/registry.ts:10574, test header) — align, non-blocking.
  • "Zero list views in this repo's examples, skills or platform sources authored the key": at base skills/objectstack-ui/rules/list-views.md:127 authored tabs: [ in a list-view snippet (the hunk this PR deletes; ledger note and PR body admit it). Fix: "examples or platform sources; the one published skill example that taught it is corrected here".
  1. Premise holds. objectui f8a9d0fb: <TabBar JSX = 10 hits, all in plugin-list/src/__tests__/icon-seam-fallbacks-5935.test.tsx and components/__tests__/TabBar.i18nLabel.test.tsx; zero production imports. Lit control: <ViewTabBar at app-shell/src/views/ObjectView.tsx:3242, fed from objectDef.listViews (:1787). In-repo: no production read of a list view's tabs except the raw-input author-time walks (below); examples/ tabs: = a record-page slot and task-triage.page.ts:35 userFilters.tabs; control listViews: in 9 example files. No platform writer sends it.
  2. Refusal true at every door. retiredKey() = z.never({error}).optional() (retired-key.ts:113) inside ListViewShapeSchema; ObjectListViewSchema = shape .omit({userFilters}) (:4613) keeps it; flattened overlay is ListViewShapeSchema.extend (:5884). Pin covers 7 doors (ListView, ObjectListView, view.list, listViews.*, ViewItem config, ViewMetadataSchema PUT overlay, ObjectSchema.listViews) asserting invalid_type/expected never/path/prescription, each with a lit control; getMetadataTypeSchema('view') === ViewMetadataSchema pinned; tsc via @ts-expect-error under CI TypeScript Type Check. Page/component carriers: InterfacePageConfigSchema is its own strictObject with userFilters: UserFiltersSchema; page.zod.ts:882 tabs is a record slot — no such door exists. Guidance follows all five house conventions; 17.5.0 matches siblings (pageName :2296, owner :4857) with package at 17.4.0.
  3. D2 lossless and reach declared. mapViewPayloads walks list/listViews/form/formViews, ViewItem config, flattened overlay; stripKeys copy-on-write; fixture 3 notices; idempotence and load-path retirement pinned. objects[].listViews.* unreached — same declared boundary as the three sibling conversions (registry.ts:9728, 9835, 10600). Read seams (database-loader.ts:825, protocol.ts:4782, objectql plugin.ts:2107) replay and do NOT parse, so a stored object row keeps its inert key and is served as before (nothing lost, nothing drawn); the strict write door refuses with the prescription on next save; os migrate meta --from 17 (chain.ts:85, replays by id, no retired filter) lists view edits only. Loud at write, not silent loss; out-of-repo population NOT MEASURED, stated. D3 list-view-tabs-retired: no #\d in surface/replacement/reason/acceptanceCriteria, pinned by the PR's test (the widened engine-guidance pin holds only engine-/ui-/plugin- prefixes, so it does not hold this id); content true to the mechanism.
  4. Skills hunk. 309 → 302, pure deletion plus a 3-line listViews pointer; the replaced "tabs win over dropdowns" rule described a bar that never rendered. Remaining tabs in skills/**: userFilters element (page-only), record-page slot, relatedLayout, navigation.md:61 switcher tabs = listViews — none teaches the retired key. objectstack-i18n/SKILL.md:166 _tabs row describes a surviving translation key (flagged in ③).
  5. Pins moved, none weakened. repeater-row-properties: carrier moved out plus dark assertions (view:tabs absent, no view:tabs.* rows). echo-decisions: 608 → 598 = repeater label + 9 row labels, matching 10 label: lines deleted per bundle. graft-normalized-operators: leg moved to userFilters.tabs plus two ride-through asserts. read-seam-empty-accumulator: remembered list replaced by the derived REFERENCE_SITES population plus explicit object absent. Consequence correct: object's only view-spelled site was listViews.*.tabs[].view; navigation.view is itself a tombstone (:2131); page stays a source via userFilters.tabs[].view. Complete under the index's exact-name rule.
  6. Generated artifacts are generator-shaped (8 reference rows to never + [REMOVED], 2 nested-shape sections gone, 4 bundles −31 lines, 2 [RETIRED] rows, registry tails); CI Lint & Repo Gates, Spec property liveness, Check Changeset green at head. Changeset: minor, BREAKING, FROM → TO, one-line fix, adr-0087 marker — except the two false sentences above.

② Semver level

minor with BREAKING banner is correct: the accept set narrows (a parsed key becomes a refusal), nothing widens. Clause-②: no (narrowing) stands alone in the changeset and PR body; per scripts/pm/clause2-line.mjs that arm reads "not a widening, but breaking", AGENTS.md:1075 makes (narrowing) BREAKING, and check-changeset-no-major refuses major (CI Check Changeset green).

③ Boundary flags

  • Container body name: correctly deferred — no ViewSchema.name edit; waits on cloud's service-ai writer per triage note 2.
  • Dead author-time walks left: lint/src/validate-list-view-field-refs.ts:795 (checkTabs(listView.tabs…)), metadata-protocol/src/metadata-diagnostics.ts:231 (view?.tabs); a third generic rec.tabs walk at lint/src/validate-capability-references.ts:186 was not named by the PR. Their fixtures are the pin's self-expiring RESIDUE.
  • objectui pin bump: packages/types/src/objectql.ts:2538 tabs?: ListViewSchema['tabs'] becomes undefined-typed; also objects.*._tabs (translation.zod.ts:385) now has no authorable object-level subject and no reader in either repo, yet the i18n skill row still teaches it — follow-up, not this card.
  • git merge-tree --write-tree vs origin/main ab946560fd is NOT clean: content conflicts in packages/spec/src/conversions/registry.ts and packages/spec/src/migrations/registry.ts hand-written tails against feat(spec)!: retire currencyConfig.precision — a currency's decimal places are its currency's (ADR-0049) #20251 (currency-config-precision-removed); PR object reads mergeable_state: dirty; needs a main merge plus gen:migration-registry and gen:docs.

Implemented-by: claude/issue-20301-list-tabs-retired
Reviewed-by: session_01QcAS3qiYYZNezaxZxaUdMV

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>
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 12:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 6e3e546 Sep 28, 2026
40 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20301-list-tabs-retired branch September 28, 2026 13:20
This was referenced Sep 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Post-landing delta audit · PR #20357 merged at c1c3ec90dd without a record at that head · director seat (objectstack#12708, session_01AsCNgFBs8HCjwhyHQsFbx3) · 2026-09-28T14:24Z

What happened. The last contract-review record on this PR is the PASS 5866560959 at 606046e96a. os-zhuang approved at 2026-09-28T11:56Z on head b751fc7842. Five commits followed the PASS head (ec79ac04 and b4c624aa conflicted main merges, b751fc78 and 8ca08e04 regens, c1c3ec90 a clean merge; the last three after the approval), and the PR was merged through the relay at 2026-09-28T13:20Z (6e3e5462c6) with no record at c1c3ec90dd. Tier H (skills/**). Logged on the seat ledger as 漏网; this note is the audit the landing owed.

Measured (read-only: git diff <merge-base> <head> per file at both heads, index lines ignored):

  • File list: 29 files at 606046e96a (merge-base df3ba164a5) and the same 29 at c1c3ec90dd (merge-base c745e2b896); no file added or dropped. skills/objectstack-ui/rules/list-views.md is byte-identical between the two.
  • 17 files: PR-own diff byte-identical.
  • 12 files: differ only by hunk offsets (the base moved) or by context lines that are main's own newer content: the three reference pages, the four generated form-label bundles (the same 31-line deletion at a new offset), undrilled-containers.baseline.json, vitest.repo-tests.json, and the two hand-written registry tails, where main's action-aria-removed and flow-decision-mode-inclusive-explicit entries now sit beside this PR's view-list-tabs-removed (both sides kept, main's first, as the earlier records describe).
  • The one hand-resolved conflict, object-lifecycle-panel-echo-decisions.test.ts: the positive control moved from 598 to 623 because main's own count moved from 608 to 633 through other landings; this PR's delta is unchanged at −10, the ten label: leaves the retired repeater took with it.
  • CI at c1c3ec90dd: 35 success / 4 skipped / 0 failure (the Type Check gates, Lint & Repo Gates, Spec property liveness and Test Core 6/6 included).

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 Spec Main Shape Gate went red on every objectui PR when this landed (packages/types/src/__tests__/p1-spec-alignment.test.ts compiles against objectstack main); stop-the-bleed objectui#10987 → PR objectui#10992. That is the objectui residue the PASS record's ③ named, arriving through the shape gate rather than the pin bump.

Reviewed-by: session_01AsCNgFBs8HCjwhyHQsFbx3

os-warren pushed a commit that referenced this pull request Sep 28, 2026
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>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
… 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>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…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>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants