Skip to content

feat(spec)!: retire the type: 'page' list-view mount and its pageName binding - #17298

Open
os-bill wants to merge 4 commits into
mainfrom
claude/issue-17063-retire-list-view-page-member
Open

feat(spec)!: retire the type: 'page' list-view mount and its pageName binding#17298
os-bill wants to merge 4 commits into
mainfrom
claude/issue-17063-retire-list-view-page-member

Conversation

@os-bill

@os-bill os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #17063

Clause-②: no

⚠️ The mechanical reading DISAGREES with that declaration, and I am reporting it rather than moving it — the seat moves a clause-② declaration, never the dev. See Clause ② — measured, three readings below.

This PR is GOVERNED (check-governed-merges --test on every diff path returns exit 3). No seat flips it ready, enqueues it, or arms auto-merge — a human merge is the review record. The dispatch predicted exit 0 / NOT governed; that prediction was false, and the reason is below.


What is retired

A list view could declare type: 'page' and name a published page in pageName, rendering nothing of its own and delegating to the page renderer (#13216 direction 1, spec half PR #13372). Only the spec half was ever built. Measured on objectui at the pinned .objectui-sha 53ded82b and at objectui main 3fbdd4a2d:

  • packages/plugin-list/src/ListView.tsx — the view-type switch shares its default: arm with case 'grid' ("an unrecognized viewType must degrade to a working table"), so 'page' has always drawn an empty grid;
  • @object-ui/core's isListViewVisualization('page') === false (normalize-list-view.test.ts:647), with a residual test named for objectui#8429.

So the three parse refusals that policed the binding policed a mount that never mounted anything. ADR-0049 enforce-or-remove; maintainer ruling 2026-09-09, decision batch #107 item 1, verbatim 「撤」, on 「已发布零消费的能力不因沉没成本获得豁免」.

Route, and why each half took the route it did

retired thing kind route why
pageName authorable KEY retiredKey() tombstone on ListViewSchema + ObjectListViewSchema tsc types it never and the parse raises the prescription instead of a bare unrecognized-key report. strict-object.ts calls a tombstone "strictly stronger than a guidance entry", and this same shape already carries virtualScroll: retiredKey(…).
'page' enum VALUE the type enum's own error map, keyed on issue.input An enum-value narrowing has no tombstone to hang a prescription on — the def survives, one value lighter. The precedent is in this same file: LIST_VIEW_EXPORT_PDF_RETIRED (#8010), and HookBodyCapability / object.managedBy: 'system' before it.

Liveness-ledger disposition follows the route, as the playbook's table demands: the pageName row stays, flipped to status: "dead" with a verifiedAt and a REMOVED note, because retiredKey() keeps the key in the walked shape (the rls.priority precedent). Deleting it would report UNCLASSIFIED.

The four surfaces the dispatch front-loaded

1 — a sys_metadata row already carrying type: 'page' / a pageName. The read path does re-validate: applyConversionsToStoredItem replays the full ADR-0087 chain on every rehydration seam, and the loader skips only 'flow' (packages/metadata/src/loaders/database-loader.ts:824 — the if (singular === 'flow') return payload guard, one line above the call at :825), never view. The new D2 conversion view-page-mount-removed therefore fires, and the row is served canonical. Measured, with a lit control:

IN   {"name":"orders.dash","object":"orders","viewKind":"list","type":"page","pageName":"sales_home","columns":[]}
OUT  {"name":"orders.dash","object":"orders","viewKind":"list","columns":[]}
     2 notices, both view-page-mount-removed (views[0].type, views[0].pageName)
CTRL {"…","type":"grid","columns":["name"]}  ->  unchanged, 0 notices

What an operator sees is a warn, not a 400 and not silence — and there are three emitters, one per rehydration seam, which do not carry the same string. Grep carries a pre-protocol shape:

emitter file:line string
[DatabaseLoader] packages/metadata/src/loaders/database-loader.ts:831 stored view/NAME carries a pre-protocol shape; NOTICE
[ObjectQLPlugin] packages/objectql/src/plugin.ts:2134 stored view/NAME carries a pre-protocol shape; NOTICE
[Protocol] packages/metadata-protocol/src/protocol.ts:4511 the same, plus The row itself is unchanged — re-save it (Studio edit → save, or run "os migrate meta --stored --apply") to persist the canonical shape.

A write through PUT /api/v1/meta/view is where the refusal fires instead: the tombstone and the enum error map both answer at parse, and that door heals the row on the next save.

⚠️ Coverage boundary, stated rather than left to be found: the conversion walks stack.views[] in all three persisted spellings (mapViewPayloads); it does not reach objects[].listViews.*, which no conversion in this registry reaches — the view-export-options-pdf-removed precedent has the identical reach. An object body still carrying a mount is refused at its own door with the prescription rather than converted. Both populations measured at zero (below).

2 — the ADR-0087 disposition. registered view-page-mount-removed, a real D2. not-required (no-migration-prescription) was not available: ADR-0087's own text (docs/adr/0087…md:596) refuses it for a body carrying a migration prescription, and this changeset carries a FROM → TO table because AGENTS.md requires one. A D3 semantic entry was considered and is not what this needed: a D2 is possible here and therefore owed. The conversion strips type rather than rewriting it to 'grid'ListViewSchema.type carries .default('grid'), so deleting the key lands the row on exactly what it already rendered, declared in one place instead of guessed in the registry, and stripKeys-shaped deletion is idempotent by construction. Registered under protocol 18 (RETIRED_KEYS_BY_MAJOR[18], MIGRATIONS_BY_MAJOR[18].conversionIds, step18.rationale extended), matching every in-flight retirement on the 17.x line. check-adr-0087-registration run after the commit — it reads changeset bodies from git, not the working tree:

✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.
    .changeset/retire-list-view-page-mount.md  [BREAKING]  registered view-page-mount-removed (new here: view-page-mount-removed)

3 — the tombstone kit. Per the spec-property-retirement playbook. Both prescriptions carry the pinned os migrate meta --from 17 house sentence, and pageName's is a retiredKey(). Two RETIRED_KEYS_BY_MAJOR[18] entries as new files under entries/retired-keys/ (18.ui__ListView__pageName.ts, 18.ui__ObjectListView__pageName.ts) plus gen:migration-registry; nothing was hand-edited inside the generated markers. The build gate named the two exact key spellings itself before they were registered, which is how they were derived rather than guessed. No RETIRED_DEFS_BY_MAJOR row is owed: no def is deleted.

4 — the three refusal messages. checkListViewPageMount existed only to police this mount, so the check, its three message constants and the export all go with it. They were module-private string constants, not error codes — no ERROR_CODE_LEDGER row exists for any of them (VIEW_PAGE_MOUNT_NEEDS_PAGE_NAME / VIEW_PAGE_NAME_NEEDS_PAGE_TYPE / VIEW_PAGE_MOUNT_HAS_COLUMNS appear nowhere but view.zod.ts), so nothing is owed there. @objectstack/lint's VIEW_PAGE_UNRESOLVED is a lint rule id, not an ADR-0112 code, and leaves with validateViewPageRefs.

What else left with it, and what deliberately did not

  • validateCrossReferences' page branch in stack.zod.ts. The surviving three page references in that function (an app nav item's pageName, a modal action's target at two rungs) and pageNames itself are untouched.
  • @objectstack/lint's validateViewPageRefs / VIEW_PAGE_UNRESOLVED. Its nav twin validateNavTargetRefs is untouched.
  • RuntimeStackContext.pages and the page row of CLOSURE_CONTEXT_KEY_BY_TYPE. validateViewPageRefs was the only member whose runtimeTypes reached stack.pages (every other page-reading rule takes the frozen flow default, and validatePageVisualizationBindings says in its own comment that it is not crossed for exactly this reason). The runtime-gate docblock states the widening rule — "made when a rule that reads the collection actually crosses the wall, never in advance" — so the symmetric retirement is owed, not optional: leaving it would create a carried collection no rule reads, which is the ADR-0049 shape this card exists to remove. A view publish also stops paying a sys_metadata round trip for it.
  • view.form.ts's page section (its one input was pageName) and the four i18n metadata-form bundles it fed — a form input for an unwritable key is the false-compliant UI half of a retirement.
  • Not touched: direction 3 of [feature] Give runtime-published custom pages an end-user entry point: a page view type on objects, or nav registration at publish time #13216 (command-palette page indexing, PR feat(spec,metadata-protocol): a page hit kind on GET /api/v1/search — command palette indexes published pages #14249) and the nav-item type: 'page' / PageNavItem.pageName surface, which is the page mount that has always rendered.

Zero usage, re-measured with a lit control

Query: git grep -n "type: *'VALUE'" over examples/**, packages/platform-objects/**, packages/mcp/**, packages/cloud-connection/**, packages/create-objectstack/**, tests excluded.

value hits verdict
page 30 all NAV items — every one sits in an items: array under navigation, carrying id / label / icon (25 in examples/app-showcase/src/ui/apps/index.ts, plus cloud-connection-ui.ts:69, marketplace-ui.ts:99, connect-ui.ts:72; studio.app.ts:187 is an action params bag). Zero list views.
kanban 4 lit control — the query shape finds authored list-view type values
tree 4 lit control
gallery 1 lit control

A zero whose control was never inspected survives review while saying nothing; each of the four rows above was read, not counted.

Ablation — the old spelling now fails, proven both ways

Each leg: mutate → prove it landed on disk (anchor count + injected-marker count + git hash-object moved) → run → restore → prove the restore by state. The spec unit suite imports ./view.zod as source (no dist hop), so no build leg is owed; the restore is git checkout HEAD -- PATH, never a bare git checkout --.

leg mutation landed result
reinstate 'page' in the type enum anchor 1→0, marker 0→1, hash 34f111fcc3c5d06b 3 pins RED — the three door-parameterised REFUSES type: 'page' cases
reinstate pageName: SnakeCaseIdentifierSchema.optional() anchor 1→0, marker 0→1, hash 34f111fc6447f4e3 6 pins RED
control — unmutated tree 13 passed, 0 failed

Restore, both legs: git hash-object returned 34f111fcef274c7ade5543b195a884fe6abde7c4, byte-equal to the HEAD:packages/spec/src/ui/view.zod.ts blob, and git diff HEAD -- PATH was empty. Not read from an exit code. A trap on EXIT INT TERM with absolute paths carried the crash path.

⚠️ The first attempt at leg 1 was a void reading and is reported as one. Its landing check used grep -cF with a multi-line anchor, which counts lines rather than occurrences; the guard mis-fired, the run was declared VOID and restored without being read. Leg 1 above is the re-run with a single-line anchor. The mutation had in fact landed — that is exactly why a landing check that cannot be trusted has to void the reading rather than be talked past.

Verification

Every exit code captured to disk before any pipe. Scripts under scripts/pm/ were run from an origin/main mirror (git archive origin/main scripts | tar -x, origin/main = a256f189), each byte-verified: check-widening-tells.mjs 949e94fd, check-governed-merges.mjs 081df968, check-clause2-carriers.mjs 60b7562c — all three identical to git rev-parse origin/main:PATH.

run exit
pnpm --filter @objectstack/spec build 0
pnpm --filter @objectstack/spec test (13 185 tests) 0
pnpm --filter @objectstack/spec check:generated (15 artifacts) 0
pnpm --filter @objectstack/lint typecheck && … test (3 688 tests) 0
pnpm --filter @objectstack/metadata-protocol typecheck && … test (2 482) 0
pnpm --filter @objectstack/platform-objects test (561) 0
check:liveness · check:empty-state · check:variant-docs · check:skill-examples · check:skill-refs · check:skill-docs · check:exported-any · check:dual-source-exports · check:duration-unit-keys · check:entry-nameability · check:yaml-examples 0 each
pnpm check:i18n 0 (RED first — 7 drifted bundles — then --write; the diff is 44 pure deletions)
check:doc-authoring · check:nul-bytes · check:cross-package-test-inputs · check:test-source-alias · check:stack-collection-maps · check:quick-reference-counts · check:corpus-claim-drift · check:published-files · check:type-check-coverage · check:i18n-stale-fill · check:objectui-changeset · check:pm-changeset-deadline-census 0 each
check-adr-0087-registration --self-test / --base (post-commit) 0 / 0
check-changeset-no-major --self-test / --base 0 / 0
check-empty-changeset · check-reference-carrier-shape (+self-tests) · check-skills-token-ratchet · check-skill-line-ratchet · check-spec-docblock-symbol-anchors · check-comment-mask-adoption · check-comment-mask-corpus · check-closing-keyword-parity 0 each

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack was re-derived after the i18n commit; it grew six entries (the changeset/i18n/release-rehearsal family), all of which were then run. The remaining families in its 121-line list are declared to CI as a narrowing, not claimed green.

ESLint — a measured narrowing, not a skipped run. ① The population is read from eslint's own config, not guessed: pnpm exec eslint --no-inline-config --format json . enumerates 6 465 files. ② The narrowed run linted 32 files (every surviving .ts/.tsx/.mjs/.mts path in the diff; the three deleted paths cannot be linted), count read from --format json: 0 errors, 0 warnings, exit 0. ③ Invariance: this repo runs one eslint.config.mjs and it never enables type-aware linting for any file — no parserOptions.project, no typed @typescript-eslint rules, stated and positively controlled at eslint.config.mjs:327-335 — so nothing in this diff can move the verdict on a file it does not touch. Measured at 1e937111c, this branch's final commit.

Clause ② — measured, three readings

Run on the finished diff (git diff MERGE_BASE...HEAD), from the origin/main mirror:

probe exit reading
check-widening-tells --declaration no 4 3 × T2 — "a new member of a closed set … the accept set gains a value"
check-widening-tells --declaration yes 0 a yes already routes to contract review, so a tell on top decides nothing
check-clause2-carriers --pair THIS_PR_NUMBER see the report comment on #17063 run once this PR had a number

The three T2 tells, by number and line:

  1. packages/spec/src/migrations/registry.ts:5443+ 'view-page-mount-removed', (the D3 chain step's conversionIds)
  2. packages/spec/src/migrations/registry.ts:12757+ 'ui/ListView:pageName',
  3. packages/spec/src/migrations/registry.ts:12791+ 'ui/ObjectListView:pageName',

⚠️ Correction (seat, 2026-09-10T05:00Z): two of the three are generated rows inside registry.ts's os-generated markers; registry.ts:5443 is NOT — the nearest marker above it is the CLOSING </os-generated semantic:17>, so it is the hand-maintained step18.conversionIds. The argument below is unchanged. Originally written as: all three are generated rows produced by gen:migration-registry from the three new files under src/migrations/entries/. They are the retirement LEDGER growing — the mechanical record that two keys are now unwritable — and every one of them is a consequence of the accept set shrinking. But that argument is the seat's to accept or reject: the dispatch ordered me to measure rather than agree, and to stop and report on disagreement. The declaration in this body stays no, exactly as claimed in comment 5612101866. ⛔ I have not moved it.

Governance — the dispatch's prediction was false

check-governed-merges.mjs --test over all 48 diff paths returns exit 3, GOVERNED: skills/** ×2 hits the register — skills/objectstack-ui/SKILL.md (the retired list-view type row, a surface the retirement playbook requires) and skills/objectstack-ui/references/react-blocks.md (regenerated by gen:react-blocks; the #11705 generated-surface exception did not lift it, because the generator declares no readable output manifest, so it fails closed). The dispatch's pre-flight read packages/spec/** and packages/lint/** only and concluded "⛔ No … ordinary queue landing applies". One hit governs the whole PR. ⛔ Dropping the skills edit to dodge governance is not an option — a published SKILL.md teaching a key that now refuses is the thing being retired.

Changeset

minor for @objectstack/spec, @objectstack/lint, @objectstack/metadata-protocol, with the **BREAKING** banner. Level derived, not inherited: check-changeset-no-major refuses major in the launch window, existing pending changesets already put @objectstack/spec at minor (next release 17.5.0, which the two prescriptions name), and all three packages publish removed surface — @objectstack/lint loses two exports and @objectstack/metadata-protocol narrows a published context type, so neither is a patch and skip-changeset is plainly wrong.

维护者速读(草稿)

改了什么 — 列表视图的 type: 'page' 挂载与它的 pageName 绑定被整体退役:枚举值、键、三条拒绝消息、checkListViewPageMount 导出、defineStack 的页面交叉引用分支、@objectstack/lintvalidateViewPageRefs、以及只为那一条规则而存在的 RuntimeStackContext.pages 实时页面全集。表单区块与四份 i18n 词条随之删除。应用导航项的 type: 'page'另一个键,原样保留。

为什么改 — 2026-09-09 裁决「撤」,依据 ADR-0049 enforce-or-remove 与「已发布零消费的能力不因沉没成本获得豁免」。spec 侧半边落了地,objectui 的渲染半边从未建成:实测 objectui 的 switchdefault:grid 共用分支,所以 page 视图一直画的是空表格,而三条解析拒绝守的是一个从未挂载过任何东西的挂载点。

风险与代价(含回滚) — 已存 sys_metadata 行不会炸:ADR-0087 的 D2 转换在每个再水化接缝上重放,剥掉两个键,行按规范服务;type剥除不是改写,靠 schema 自己的 grid 默认值落到它本来就渲染的样子。运营者看到的是三个接缝各一条 warn(字符串见上表),不是 400。已测编写面用量为零(带亮对照)。⚠️ objects[].listViews.* 不在任何转换的行走范围内 —— 那条路上的旧行在它自己的门上被带处方拒绝,而不是被转换。回滚 = 还原本 PR;墓碑与转换都是新增,不改动任何现存行为。

席位意见 — 达档契约复核返回 PASS WITH FINDINGS,无 must-fix;席位判定可以合。转录保险丝:120 个 harness 打戳的 claude-fable-5-1,零残留,对照词点亮。⚠️ 独立性说实话:复核者是本席位派出的子代理、继承本 session id ⇒ 席位内达档复核,⛔ 不是跨席位独立。⛔ 人工合并即审核记录,但 ⛔ 不替代契约复核 —— 上面那份就是那一道。

两件请你在 diff 里特别看一眼的事。

一、这不是普通的收窄,它是一次真 D2 —— 但覆盖有边界,而边界的代价被测出来了。 存量 view 行会被自动改写(探针:type:'page' 视图 → 2 条 view-page-mount-removed notice;grid 对照 → 0 条;容器视图 → 3 条)。⛔ 但转换只走 stack.views[],够不到 objects[].listViews.*。复核实测了那条边界的实际代价:一条带 page mount 的存量 object 行会原样穿过转换链(0 条 notice),在自己的门被 ObjectSchema 拒绝,却仍然被注册,每个进程打一条 [metadata_spec_invalid] —— 供着,不治;而两处 prescription 都叫作者去跑一个够不到它的 os migrate meta今天两个人群实测都是零生产者,所以不挡落地,但它是 ACCEPT 时要立的卡(view-export-options-pdf-removed 先例有同样的够不到)。

二、clause ② 的机械读数与本 PR 的声明冲突,我裁维持 no 三条 T2 tell 全部落在退役台账的行上 —— 它们存在的原因恰恰是接受集缩小了。改判 yes 会让 clause ② 轴对语义上最明确的收窄报「扩大」,并成为此后每一次退役的先例。工具侧另立 #17300

⚠️ 而我在裁决里把那三条 tell 都说成「生成的行」—— 错了一条。 registry.ts:5443 不在任何 os-generated 区域内(它上面最近的标记是闭合标记 </os-generated semantic:17>),它是手工维护step18.conversionIds。复核逮到,我自己在 head 上重测确认。已在裁决评论和 #17300 上就地更正 —— 因为 #17300 原本提的修法只按生成区域来判,会只覆盖三分之二的 tell,而每一次 D2 退役都会注册一个 conversion。

复核另纠了三处报告口径,都不动字节: runtime 读者的「唯一」少算了一个(validateVisibilityPredicates 也读 stack.pages,但 collectionEntries(undefined) 返回 [],无害,措辞应为「不把引用解析进 stack.pages」);消融计数 3/6/13 实为 4/4/14(方向对、守卫有效,只是记录要写真数);以及 #11705 的生成物例外其实抬得起 react-blocks.md,dev 读到的「fail closed」是未构建闭包导致的环境依赖结果。

受管面归属复核已重跑确认:exit 3,由 skills/objectstack-ui/SKILL.md 命中。 ⛔ 我当初的预检说「走普通队列」是错的 —— 我把检查跑在了卡片点名的路径上,而不是一次退役实际需要碰的路径上,而退役手册本身就要求动已发布的 skills 目录。⛔ 把 skills 拆出去躲开受管面这条路,dev 在我之前就拒了,理由对:一份教着「现在会在 parse 处被拒的键」的 SKILL.md,正是被退役的东西本身。

你要做的 — ① 判定 clause-② 的分歧:机械读数给出三条 T2(全部是退役台账的生成行),语义方向仍是收窄;移动声明是席位的事。② 本 PR 受治理(skills/**),需人工合并。③ ACCEPT 时按规则 3 在 objectui 立跨仓跟进:objectui#8429 的前提被本次退役消灭。


⚠️ Reminder, not an action: objectui#8429 is open and pm:blocked — it is the mount half that was never built, and this retirement kills its premise. ⛔ Nothing in objectui was filed or touched by this PR; that cross-repo follow-up is the seat's at ACCEPT.


Generated by Claude Code

…ame` binding

A list view could declare `type: 'page'` and name a published page in
`pageName`, delegating its whole rendering to the page renderer. Only the
spec half of that was ever built (#13216 direction 1, PR #13372). No
renderer ever routed the member — objectui's list-view switch shares its
`default:` arm with `case 'grid'`, and `isListViewVisualization('page')`
is false — so a page view drew an empty table where the page belonged,
and the three parse refusals policing the binding (`checkListViewPageMount`)
policed a mount that never mounted anything.

ADR-0049 enforce-or-remove; maintainer ruling 2026-09-09, decision batch
 #107 item 1.

- `pageName` becomes a `retiredKey()` tombstone on both list-view doors.
- `'page'` leaves the `type` enum; the enum's own error map carries the
  prescription, keyed on `issue.input` (the `exportOptions` 'pdf'
  precedent — an enum-VALUE narrowing has no tombstone to hang one on).
- `checkListViewPageMount`, its three refusal messages, the
  `validateCrossReferences` page branch, `@objectstack/lint`'s
  `validateViewPageRefs` / `VIEW_PAGE_UNRESOLVED`, and `view.form.ts`'s
  `page` section are removed with the mount.
- `RuntimeStackContext.pages` and the `page` row of
  `CLOSURE_CONTEXT_KEY_BY_TYPE` leave with the only crossed rule that
  read them: a view publish no longer gathers a live page universe.
- ADR-0087: D2 conversion `view-page-mount-removed` (protocol 18) strips
  both keys from stored rows and `os migrate meta --from 17` output;
  `type` is stripped rather than rewritten, since the schema defaults it
  to `grid` — exactly what the row already rendered.

The surviving page mount is the app navigation item
(`PageNavItem.pageName`), untouched.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
…tirement

`view.form.ts`'s `page` section and its `pageName` input are gone (#17063), so
the extracted metadata-form labels drop with them: the `page` section
label/description and the `pageName` label/helpText, across en/es-ES/ja-JP/zh-CN
plus the three source-hash bundles. Pure deletion — merge mode adds and
translates, it does not remove, so this is `check-i18n-bundles.mjs --write`
rewriting the default locale from source and dropping the orphaned keys.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:ui tests tooling labels Sep 10, 2026
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/lint, @objectstack/metadata-protocol, @objectstack/platform-objects, @objectstack/spec, touching 40 documentable anchor(s). ⚠️ 10 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/lint/src/validate-list-view-field-refs.ts, packages/spec/api-surface/ui.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/plugin-endpoints.mdx (via /api/v1/meta/view (route, a path literal in REFERENCE_INTEGRITY_RULES; a path literal in evaluateRuntimeAuthoringGate; a path literal on a changed line))
  • content/docs/concepts/metadata-lifecycle.mdx (via /api/v1/meta/view (route, a path literal in REFERENCE_INTEGRITY_RULES; a path literal in evaluateRuntimeAuthoringGate; a path literal on a changed line))
  • content/docs/kernel/services-checklist.mdx (via /api/v1/meta/view (route, a path literal in REFERENCE_INTEGRITY_RULES; a path literal in evaluateRuntimeAuthoringGate; a path literal on a changed line))
  • content/docs/protocol/objectui/index.mdx (via /api/v1/meta/view (route, a path literal in REFERENCE_INTEGRITY_RULES; a path literal in evaluateRuntimeAuthoringGate; a path literal on a changed line))
  • content/docs/ui/apps.mdx (via pageName (literal, a string literal in apply; a string literal in checkListViewPageMount; a string literal in sections))
  • content/docs/ui/forms.mdx (via /api/v1/meta/view (route, a path literal in REFERENCE_INTEGRITY_RULES; a path literal in evaluateRuntimeAuthoringGate; a path literal on a changed line))

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

  • content/docs/releases/v12.mdx (via ObjectListViewSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-1.mdx (via ListViewSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-3.mdx (via crm_contract (literal, a string literal in fixture))
  • content/docs/releases/v17/17-4.mdx (via checkListViewPageMount (symbol, a top-level function))

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
  • 10 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/lint/src/validate-list-view-field-refs.ts, packages/spec/api-surface/ui.json, …) — pages documenting those are invisible to this run
  • 9 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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.

Coarse fallback — 135 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 357f4992ba5b3cce98629bf75d3a6ccb89212800packageMentionDocs.

Which tree this was computed on

This run read content/docs from c60feb1dc0c3897e49d9d40a5fa6bc47a2cce239 — the merge of head 59b2e5f0d29828619b9ac6fc304d56158610057c into base 357f4992ba5b3cce98629bf75d3a6ccb89212800, 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 c60feb1dc0c3897e49d9d40a5fa6bc47a2cce239 && git checkout c60feb1dc0c3897e49d9d40a5fa6bc47a2cce239
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 357f4992ba5b3cce98629bf75d3a6ccb89212800 59b2e5f0d29828619b9ac6fc304d56158610057c && git checkout -B drift-repro 357f4992ba5b3cce98629bf75d3a6ccb89212800 && git merge --no-ff 59b2e5f0d29828619b9ac6fc304d56158610057c

node scripts/docs-audit/affected-docs.mjs --json 357f4992ba5b3cce98629bf75d3a6ccb89212800

⚠️ 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 357f4992ba5b3cce98629bf75d3a6ccb89212800 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

⛔ Correction — this PR is no longer mergeable as it stands. My "ready for a hand merge" is stale

domain:spec seat, 2026-09-10T05:47Z. Read live just now: mergeable_state: dirty — it conflicts with main.

⚠️ I told you this was ready for your hand merge, and that is no longer true. Do not press merge on it — git would refuse, but the wasted trip is mine to prevent, so here is the correction as soon as I saw it.

What happened, and it is my sequencing error

PR #17257 merged at 04:42:41Z, and it writes the same generated file this PR writes: packages/spec/src/migrations/registry.ts (this PR registers view-page-mount-removed there).

When I released the registry hold and enqueued #17257, I priced that decision against #17267 and #17146 — the two PRs I then knew shared that file — and I posted the sequencing for both. ⛔ I did not price #17298 into it, because it was still under review at that moment and I did not go back and re-check the file set after its diff was final. The hold I reasoned about carefully covered two of the three PRs it should have covered.

⇒ Nothing about the reviewed work changed. The at-tier PASS WITH FINDINGS stands, CI on this head reads 30 success / 4 skipped / 0 red, and the 席位意见 is filled. What changed is only that main moved underneath it.

What I am doing about it

A mechanical merge + regenerate round is dispatched now — the same operation #17267 and #17146 each took after #17257 landed, and it is the seat's, ⛔ not yours:

⛔ The PR stays draft throughout, and it stays yours to merge — the round makes that merge possible again, it does not perform it.

One thing worth keeping from this

⭐ The lesson is not "hold longer". It is that a hold priced against a file set is only as good as the moment it was priced — mine was correct when I wrote it and silently incomplete twenty minutes later, because a third PR's diff finalised in between. A file-set hold has to be re-derived at the moment of release, over every open PR in the lane, ⛔ not over the ones the seat happened to be thinking about.

I will carry that as a standing check rather than as a note, since this is the second time tonight a pre-check of mine was run over the wrong path set — the first being the governed-surface check on this very card, which read only the paths the card named and missed the skills/** hits that a retirement necessarily brings.


Generated by Claude Code

Brings the branch up to the landed main (8c9bd8f) so the governed PR is
mergeable again. Two hand-resolved hunks, both in
`packages/metadata-protocol/src/protocol.runtime-gate-stored-universe.test.ts`:

- the `@objectstack/lint` import — main widened it to
  `DASHBOARD_FILTER_FIELD_UNKNOWN, VIEW_PAGE_UNRESOLVED, WIDGET_MEASURE_UNKNOWN`
  while this branch removed the `VIEW_PAGE_UNRESOLVED` import along with the rule
  itself. Resolved to main's two surviving symbols; `VIEW_PAGE_UNRESOLVED` no
  longer exists in `packages/lint/src` on the merged tree, so keeping it would
  not compile.
- the helper block after `threeWidgetBoard` — main appended `filterBoard`
  (#16224) directly after `pageMountView`, which this branch deleted with the
  mount it modelled. Resolved by keeping `filterBoard` and dropping
  `pageMountView`; nothing references the latter on the merged tree.

Generated artifacts are regenerated in the follow-up commit, from the committed
merge rather than from a tree in MERGE state.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
…ropped

`packages/spec/api-surface/ui.json` and `packages/spec/export-origins/ui.json`
are routed to `merge=os-regen`, and both sides of the merge had edited them, so
the driver merged them with exit 0 while keeping one side. The dropped side was
main's: #17257 added four `*Parsed` type exports on the `ui` entry point
(`ElementDataSourceParsed`, `ObjectCalendarPropsParsed`,
`ObjectKanbanPropsParsed`, `ObjectMetricPropsParsed`), and the merge commit's
copy of both artifacts carried none of them.

Regenerated from the committed merge — `pnpm --filter @objectstack/spec build`
then the gen chain — so the content is re-derived from the merged source rather
than hand-reconciled. Both files gain exactly those four rows and nothing else;
every other generated artifact this branch touches (migration registry,
authorable surface, spec-changes, upgrade guide, liveness counts, react-blocks,
the reference docs) regenerated byte-identical to the merged tree.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>

os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

ACCEPT — merge+regen round, head 59b2e5f0d

Reviewed by the domain:spec execution seat, session_01MkQhmuuJAVDjmeWNixwDDH, readings taken 2026-09-10T06:48Z UTC. ⛔ Governed surface: this is an acceptance record only — no ready-flip, no enqueue, no auto-merge, no approving review. The human merge remains the terminal step and remains the sign-off.

The dirty this round was dispatched for is gone. mergeable: true, mergeable_state: blocked (required checks pending), draft still true, 48 changed files.

⛔ Three false premises in my dispatch order, corrected on the record

The round's most valuable output is that it falsified my own order rather than executing it.

1 — I predicted registry.ts would conflict. It did not. It text-merged cleanly and check:migration-registry reports it current with no regeneration needed.

The files that actually lost a side were the pair routed to merge=os-regen: packages/spec/api-surface/ui.json and packages/spec/export-origins/ui.json. There the driver exited 0 and silently dropped MAIN's side — PR #17257's four new *Parsed ui exports (ElementDataSourceParsed, ObjectCalendarPropsParsed, ObjectKanbanPropsParsed, ObjectMetricPropsParsed) were absent from the merge commit's copy of both files, and are restored by 59b2e5f0d.

⇒ That is exactly the silent-loss shape this round exists to catch — caught in a file my order did not name, and it would have been missed by a round that only checked where I said to look. ⚠️ The lesson for the next merge order on a spec retirement branch: derive the prediction from .gitattributes, not from the last incident. I derived it from the #17146 incident and named the wrong file.

2 — I named five gates in their bare spelling. check:migration-registry, check:spec-changes, check:upgrade-guide and check:generated are not root scripts; they live in packages/spec. Run bare they exit 254 (ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL, "Command not found") — which is NOT MEASURED, not red, and pnpm's own "Did you mean check:query-options-erasure?" hint is actively misleading. Correct spelling: pnpm --filter @objectstack/spec check:…. Only check:i18n is a root script.

⚠️ This one is the more dangerous of the three: an order that teaches a dev to expect a specific non-zero exit is pre-authorisation to wave through an unrelated failure. It is the second time this stretch I have shipped that shape in an order.

3 — I asserted the skills bundle line reading could stay at 13531. It cannot and does not: md-only reads 13530 at HEAD. This PR's own contribution is −1 line; the other −1 is main's own skills/objectstack-ai/SKILL.md going 417→416 inside the same window. ⇒ Any published-bundle ratchet number quoted from before a merge is stale by construction; the per-file attribution is the durable form.

Evidence I checked before accepting

  • Entry-survival assertions ran with both a lit and a dark control. A probe that matches nothing proves nothing, and this round's own earlier legs had already produced one vacuous probe; the controls are what make the survival reading a reading.
  • Gate exits were landed to disk and read back, never taken through a pipe or from a bare exit variable.
  • Suites were run sequentially with each exit echoed, because pnpm --filter A --filter B test --concurrency=2 forwards --concurrency to vitest and dies (first attempt exit 1 — NOT MEASURED, correctly reported as such rather than as red). Green: @objectstack/spec 470 files / 13208 tests; @objectstack/lint 101 / 3688; @objectstack/metadata-protocol 173 passed + 2 skipped / 2486 passed + 10 skipped — that last one carries the hand-resolved file, green with main's whole The runtime authoring gate judges an OVERRIDDEN item's content from the registry copy, so an overlay that removes a measure still accepts widgets bound to it #16224 describe running. Typecheck across all three joined with &&, exit 0.
  • Declared narrowing, accepted: the repo-wide farm was not run locally. dispatch-gates.mjs --commands derives 121 commands for this 48-path change set; the round ran the order's named set plus the families its own mechanics implicate (merge-driver, nul-bytes, skills token ratchet, widening-tells, four spec generated-artifact gates). The rest is CI's, on this head.
  • No ablation, and none owed — nothing here is a new guard, so there is no trap/restore leg to report.

CI, read as newest run per check name

Read 2026-09-10T06:48Z from the check-runs endpoint for 59b2e5f0d, reduced to the newest run per check NAME — ⛔ not the raw run list, ⛔ not the required subset. 34 runs, 34 distinct names, so no name carried a stale duplicate: 31 success, 2 skipped, 1 in progress (Lint & Repo Gates), 0 failure, 0 cancelled, 0 timed out.

⇒ One job from converged. I will post again here when the last one lands, as promised — ⛔ that post will be a fresh reading, not this one restated.

Out-of-scope findings, dispositions

All three of the round's noted-not-filed items are correctly not-filed: two have no carrier (the pnpm 254 is loud and never a false green; the bundle arithmetic is arithmetic, not a defect), and the third — re-deriving a merge prediction from .gitattributes — is now recorded above, where the next order's author reads it.


Generated by Claude Code

os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Head 59b2e5f0d is green — as promised, a fresh reading

domain:spec execution seat, session_01MkQhmuuJAVDjmeWNixwDDH, read 2026-09-10T06:51Z. ⛔ Not a restatement of the acceptance comment above — re-read from the API at this timestamp.

Check runs on 59b2e5f0d, reduced to the newest run per check NAME (⛔ not the raw run list, ⛔ not the required subset): 34 runs, 34 distinct names, so no name carried a stale duplicate.

success 32
skipped 2 (Console Pin Gate, Packed-tarball smoke (opt-in))
failure / cancelled / timed out 0
still running 0

Lint & Repo Gates, the last one outstanding at the acceptance comment, completed success.

⚠️ One thing the check-run reading alone would have got wrong

The PR reads mergeable_state: unstable, not clean — and the check runs do not explain that, because it is not a check run. The combined commit status endpoint does:

combined state: pending | statuses: 1
  pending  Vercel  |  Vercel is deploying your app

⇒ The unstable is a pending Vercel preview deploy, a commit status and not a gate. Every gate on this head is green. ⛔ Recording it because a newest-run-per-name reading over /check-runs alone would have let me report "clean" while the API said otherwise — the two surfaces are different populations and the mergeability field reads both.

State

mergeable: true, mergeable_state: unstable (Vercel only), draft true, 48 changed files, labels unchanged. The dirty this round was dispatched for is gone and has not returned.

⛔ Still governed: no ready-flip, no enqueue, no auto-merge, no approving review by any seat. The hand merge is available again, and it remains yours.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 10, 2026 12:47
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 10, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 10, 2026
os-litant pushed a commit that referenced this pull request Sep 10, 2026
…n rows as an accept-set gain

T2 is "the accept set gains a VALUE", and the ADR-0087 retirement ledger is a
list of values written BECAUSE an accept set shrank. Every retirement adds rows
to `RETIRED_KEYS_BY_MAJOR` / `RETIRED_DEFS_BY_MAJOR` and to a step's
`conversionIds`, so the mechanical clause-② axis read adverse on the one change
class whose direction is unambiguously narrowing: PR #17298 exits 4 with three
T2 rows against a declaration that is correct.

The repair is a LICENCE, minted only by an added line on one of two declared
input surfaces and checked by exact string identity:

  - `export const entry = '<row>';` under `packages/spec/src/migrations/entries/**`
    — the generator's own per-entry input (#7297), the exact string
    `gen:migration-registry` emits into the table; and
  - `id: '<id>',` added to `packages/spec/src/conversions/registry.ts` — the D2
    registration a hand-maintained `conversionIds` row refers to.

Neither a region nor a file is excluded. One of PR #17298's three rows sits in
the hand-maintained `conversionIds` array, outside every `<os-generated …>`
marker, so a position-keyed predicate covers two rows of three. A lookup in the
local tree was measured and is wrong rather than merely expensive: a retirement
registers its conversion in the same PR, so `view-page-mount-removed` reads 0
occurrences in any checkout of main against a positive control reading 1.

The licence is read AFTER #16943's replacement budget, so a block that removes a
member and adds both a tombstone and a genuine member still reports the genuine
one. Self-test 218 -> 245 cases, 27 of them a new battery whose firing half
comes first: the same row with no licence, a licence for a neighbouring row, a
licence spent on the wrong file, a genuine member beside a licensed one, and the
ordering against the budget.

Claude-Session: https://claude.ai/code/session_01YKEjmbYNvYWJvWGSWx26zK
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

Projects

None yet

3 participants