Skip to content

Auto-organize documentation by zod source files - #91

Merged
hotlong merged 4 commits into
mainfrom
copilot/organize-zod-files-by-folder
Jan 23, 2026
Merged

hotlong merged 4 commits into
mainfrom
copilot/organize-zod-files-by-folder

Conversation

Copilot AI commented Jan 23, 2026 •

Copy link
Copy Markdown
Contributor

Documentation was generated flat within category roots (88 files in /ai/), making navigation difficult. Now automatically organizes into subfolders matching source .zod.ts files.

Implementation

  • Auto-detection: Scans src/{category}/*.zod.ts files to build schema → zodFile mapping
  • Removes hardcoded subcategories: Replaced 74-line SUB_CATEGORIES config with dynamic ZOD_FILE_TITLES lookup
  • Clean rebuild: Purges old flat structure before generation to prevent stale files
  • Complete coverage: All 49 zod files mapped to human-readable titles

Structure Change

Before:

/content/docs/references/ai/
  Agent.mdx
  CostEntry.mdx
  ModelRegistry.mdx
  ... (88 files)

After:

/content/docs/references/ai/
  agent/           (Agents)
    Agent.mdx
    AIKnowledge.mdx
  cost/            (Cost Management)
    CostEntry.mdx
    BudgetLimit.mdx
  model-registry/  (Model Registry)
    ModelRegistry.mdx
  ... (8 folders)

Applies to all categories: data (12 folders), ui (7 folders), system (19 folders), ai (8 folders), api (1 folder).

Files Modified

  • packages/spec/scripts/build-docs.ts - Rewritten to scan zod files instead of using hardcoded mappings
Original prompt

生成官网文档的时候要不要再按每一个zod文件分子文件夹?


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@vercel

vercel Bot commented Jan 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
spec Error Error Jan 23, 2026 4:13pm

Request Review

Copilot AI and others added 3 commits January 23, 2026 16:05
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…y cleanup logic

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Organize Zod files into individual subfolders Auto-organize documentation by zod source files Jan 23, 2026
Copilot AI requested a review from hotlong January 23, 2026 16:11
@hotlong
hotlong marked this pull request as ready for review January 23, 2026 18:03
Copilot AI review requested due to automatic review settings January 23, 2026 18:03
@hotlong
hotlong merged commit f93df51 into main Jan 23, 2026
1 of 2 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR restructures the reference documentation so that content is organized by underlying .zod.ts source files rather than flat category directories, improving navigation and alignment with the metamodel. It updates category meta.json files and per-subfolder meta.json titles to reflect the new structure and removes now-obsolete pages and groupings.

Changes:

  • Reworked meta.json navigation for data, system, ai, and api references to point to subfolders that mirror Zod schema groupings.
  • Added new meta.json files for each new subfolder (e.g., data/object, system/manifest, ai/agent, api/contract) with human-readable titles.
  • Removed obsolete high-level groupings and old MDX pages that no longer correspond 1:1 to the current Zod schema layout.

Reviewed changes

Copilot reviewed 55 out of 393 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
content/docs/references/system/organization/meta.json Adds title metadata for the new organization system subfolder.
content/docs/references/system/meta.json Updates system reference root pages to list schema-aligned subfolders (api, audit, auth, datasource, etc.).
content/docs/references/system/manifest/meta.json Introduces a Manifest section title for the manifest-related docs.
content/docs/references/system/license/meta.json Introduces a Licenses section title for license-related system docs.
content/docs/references/system/job/meta.json Introduces a Jobs section title for job-related system docs.
content/docs/references/system/integration/meta.json Removes the old Integration grouping meta; now handled via schema-aligned folders.
content/docs/references/system/identity/meta.json Renames the identity section title from “Identity & Auth” to “Identity” to separate identity from auth.
content/docs/references/system/identity/AuthProvider.mdx Deletes an obsolete AuthProvider schema doc that no longer matches the new organization.
content/docs/references/system/identity/AuthProtocol.mdx Deletes an obsolete AuthProtocol schema doc replaced by new structure.
content/docs/references/system/i18n/meta.json Removes the old Internationalization grouping in favor of new schema-based layout.
content/docs/references/system/geo/meta.json Removes the old Territory & Geo grouping; territory is now covered via dedicated entries.
content/docs/references/system/events/meta.json Adds an Events section title for event-related system docs.
content/docs/references/system/driver/meta.json Adds a Drivers section title for driver-related system docs.
content/docs/references/system/discovery/meta.json Adds a Service Discovery section title for discovery-related docs.
content/docs/references/system/datasource/meta.json Adds a Datasources section title for datasource-related docs.
content/docs/references/system/config/meta.json Removes the old Configuration grouping meta.
content/docs/references/system/auth/meta.json Adds an Authentication section title separating auth from identity.
content/docs/references/system/audit/meta.json Simplifies the audit section title from “Audit & Compliance” to “Audit”.
content/docs/references/system/api/meta.json Adds an api system subfolder title for API-related system configuration/docs.
content/docs/references/system/AuthenticationProvider.mdx Deletes an obsolete AuthenticationProvider doc consolidated into new structure.
content/docs/references/system/AuthenticationConfig.mdx Deletes an obsolete AuthenticationConfig doc consolidated into new structure.
content/docs/references/meta.json Registers api as a top-level reference category alongside data/ui/system/ai.
content/docs/references/data/workflow/meta.json Adds a Workflows section title for workflow-related data schemas.
content/docs/references/data/validation/meta.json Adds a Validation section title for validation-related data schemas.
content/docs/references/data/types/meta.json Removes the old “Types & Definitions” grouping meta.
content/docs/references/data/types/FieldMapping.mdx Deletes an obsolete FieldMapping doc; mapping docs are now organized under the new mapping structure.
content/docs/references/data/trigger/meta.json Adds a Triggers section title for trigger-related data schemas.
content/docs/references/data/sharing/meta.json Adds a Sharing Rules section title for data sharing schemas.
content/docs/references/data/security/meta.json Removes the old “Security & Access” grouping meta.
content/docs/references/data/query/meta.json Adds a Queries section title for query-related data schemas.
content/docs/references/data/permission/meta.json Adds a Permissions section title for permission-related data schemas.
content/docs/references/data/object/meta.json Adds an Objects section title for object-level data schemas.
content/docs/references/data/meta.json Replaces conceptual data groupings with schema-aligned pages (dataset, field, filter, etc.).
content/docs/references/data/mapping/meta.json Adds a Mappings section title for mapping-related data schemas.
content/docs/references/data/logic/meta.json Removes the old “Logic & Validation” grouping meta.
content/docs/references/data/flow/meta.json Adds a Flows section title for flow-related data schemas.
content/docs/references/data/filter/meta.json Adds a Filters section title for filter-related data schemas.
content/docs/references/data/field/meta.json Adds a Fields section title for field-related data schemas.
content/docs/references/data/dataset/meta.json Adds a Datasets section title for dataset-related data schemas.
content/docs/references/data/core/meta.json Removes the old “Core Entities” grouping meta.
content/docs/references/data/automation/meta.json Removes the old Automation grouping meta.
content/docs/references/data/analytics/meta.json Removes the old Analytics (Data) grouping meta.
content/docs/references/api/requests/meta.json Removes the old Request Payloads grouping meta.
content/docs/references/api/meta.json Updates API reference root to a single contract page reflecting the new structure.
content/docs/references/api/envelopes/meta.json Removes the old Response Envelopes grouping meta.
content/docs/references/api/contract/meta.json Adds an API Contracts section title for contract-related API schemas.
content/docs/references/ai/workflow-automation/meta.json Adds a Workflow Automation section title for AI workflow automation schemas.
content/docs/references/ai/rag-pipeline/meta.json Adds a RAG Pipelines section title for retrieval-augmented generation pipelines.
content/docs/references/ai/predictive/meta.json Adds a Predictive Models section title for predictive AI schemas.
content/docs/references/ai/nlq/meta.json Adds a Natural Language Query section title for NLQ-related schemas.
content/docs/references/ai/model-registry/meta.json Adds a Model Registry section title for model registry schemas.
content/docs/references/ai/meta.json Adds explicit AI subpages (agent, conversation, cost, etc.) for schema-aligned navigation.
content/docs/references/ai/cost/meta.json Adds a Cost Management section title for AI cost-related schemas.
content/docs/references/ai/conversation/meta.json Adds a Conversations section title for conversation-related AI schemas.
content/docs/references/ai/agent/meta.json Adds an Agents section title for agent-related AI schemas.

@@ -0,0 +1,3 @@
{
"title": "Api"

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The title uses the mixed-case form "Api" while other references (for example "title": "API Protocol" and "title": "API Contracts") use the all-caps acronym; for consistency and clarity, this should be updated to "API".

Suggested change
"title": "Api"
"title": "API"

Copilot uses AI. Check for mistakes.
Copilot AI mentioned this pull request Jan 23, 2026
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…ity run (objectstack-ai#17733)

Fixes objectstack-ai#15367

`AGENTS.md` carried a sentence family describing an input this tree no
longer lacks: the
`check:react-declaration-parity` gate was said to have objectui's
browser dump as its only
right-hand side, to be unrunnable here, and to be forbidden from any
workflow. CI has
contradicted that last clause since the manifest landed. Per the
recorded ruling (decision
batch objectstack-ai#91, option A — the wiring is right and the rule is obsolete),
both paragraphs are
corrected, and the correction is taken only after the ruling's
precondition measurement.

## The ruling's precondition, measured first

The ruling conditions the correction on comparing the tracked root
artefact with a real
browser dump input-for-input, "because today the ratcheting half
(`registryOnly`) reads 0,
the configuration in which an input mismatch is least visible".

Produced on `431c757` in a worktree: `bash scripts/build-console.sh`
built objectui at the
pinned `.objectui-sha` `53ded82bf7a494f54e344e19099dbf00854b8694`, then
`bash scripts/gen-sdui-manifest.sh` drove a real Chromium over it and
wrote
`packages/console/dist/sdui.manifest.json` (73,194 bytes). Both ran
under the shared verify
lock: `os-verify-lock: VERDICT command-exit 0 · held the lock 22s` for
the dump step. The
first attempt failed on a Playwright build-revision lookup gap
(installed
`chromium_headless_shell-1194`, playwright 1.62.1 wants `1234`); the
remedy already recorded
in `docs/releases-maintenance.md` "If the dispatch container's
Playwright browser doesn't
match the revision" — a scratchpad symlink tree, never under
`/opt/pw-browsers` — resolved it
unchanged. ⛔ No `playwright install` was run.

Compared over exactly what the gate consumes. `manifestInputs()` in
`packages/spec/scripts/check-react-blocks-declaration-parity.ts` reads
`manifest.components[schemaType]` (bare key, or a key ending
`:schemaType`) and then
`inputs[].name` — nothing else per input. So the basis is: the set of
component keys, and per
component the set of input names.

**Verdict: DIFFERS on the consumed fields.**

| basis | reading |
|:--|:--|
| component keys | 57 vs 57 — **0** only in root, **0** only in dump |
| components whose input-name set differs | **18** of 57 |
| input names only in root | **2** (`type` on `action:button` and
`action:icon`) |
| input names only in dump | **19** (`dataSource` on 15 components;
`align`, `variant` on `text`; `actionType` on the two action blocks) |

**But the gate's verdict set is identical under both inputs.** Running
the CI invocation
against each manifest in turn, `--baseline
react-declaration-parity.baseline.json --strict`:

```
MANIFEST=.../sdui.manifest.json                    -> exit 0
MANIFEST=.../packages/console/dist/sdui.manifest.json -> exit 0
                     blocks  declared-by-both  spec-only  registry-only  node-level
root artefact             9               119         90              5           2
browser dump              9               119         90              5          11
```

Both print `✓ no new DECLARATION divergence vs accepted baseline` and
`Summary: 90 spec-only divergences, 0 blocks missing from the registry`.
Per block, the
`declared by both` / `spec-only` / `registry-only` triples are
byte-identical. The whole
divergence lands in the **node-level** bucket — the nine extra entries
are `dataSource`,
which the gate classifies as accepted at node level and which feeds
neither the ratchet nor
any verdict. The remaining consumed-field differences (`text`,
`action:button`, `action:icon`)
sit in components the gate does not judge at all.

⇒ Nothing here narrows the `--strict` verdict set, and this PR does not
touch it: that file is
`domain:spec`. What the measurement does record is that the root
artefact is systematically
missing `dataSource` and spells `type` where the dump says `actionType`,
so the two producers
are not interchangeable — a future registry change on one of those names
would be invisible in
the artefact CI reads. That belongs to the spec lane as its own card.

## The correction

Two paragraphs, both in `AGENTS.md`:

- the `check:react-declaration-parity` paragraph in *Touched
`packages/spec`? Regenerate its
artifacts BEFORE pushing* — now names the tracked repo-root
`sdui.manifest.json`, its
generator, its provenance record, the freshness gate that holds it
honest in the required
lint job, and the per-PR `--strict` run. Kept: "exits 1 with no usable
manifest — could not
run is a failure, not a skip" and the prohibition on re-adding a skip.
Kept as a measured
fact, not dropped: `check:generated` still files the gate
`EXTERNAL_INPUT_REQUIRED`, because
that aggregate still hands it no manifest. Dropped: "do not wire the
gate into a workflow
  either", which CI contradicts.
- the pin-bump paragraph in *Frontend (Studio UI)* — the same false
claim in the same file
("never a CI job", "the pin bump is the ratchet's trigger, and its only
one"). It now names
  the regenerator the freshness gate actually reds on, in the spelling
`scripts/check-sdui-manifest.mjs` itself prints, and keeps `pnpm
sdui:manifest` as the
separate browser dump it is. Correcting only the CI-job half would have
left this paragraph
  naming a different producer than the paragraph above.

`docs/releases-maintenance.md` carries the same producer confusion and
is **not** touched
here: it is a `domain:devx` page and gets its own card.

## Budget and gates

`AGENTS.md` 1075 lines before, 1075 after (ceiling 1075, headroom 0) —
net zero, paid by the
deleted prohibition and the deleted cross-reference rather than by
re-wrapping.
`✓ check-skill-line-ratchet: AGENTS.md is 1075 lines (ceiling 1075;
headroom 0).`

All 14 gate families derived for this diff by `node
scripts/pm/dispatch-gates.mjs --commands`
ran green (exit 0 each), plus `node
scripts/check-published-list-mirrors.mjs`
(`OK: 1 published list mirror(s) match their constants line for line`).
`node scripts/pm/check-governed-merges.mjs --test AGENTS.md` — exit 3,
`⛔ GOVERNED — a human merge is the review record for this PR`.

## 维护者速读(草稿)

**改了什么** — `AGENTS.md` 两段散文。一段说这个 React 声明一致性检查「在本仓根本跑不了、也不要把它接进
CI」,另一段说它「永远不是 CI 作业,只在 objectui pin 变动时按需跑」。两句都改成实况:它读的是仓库根那份
被跟踪、带防篡改记录、由必需 lint 作业守新鲜度的 `sdui.manifest.json`,并且每个 PR 都以 `--strict`
跑。
「没有可用清单就 exit 1,那是失败不是跳过」以及「⛔ 不要靠重新加 skip 来消红」两条保留。顺带把 pin 变动
那半句改成真正被门禁盯着的那条重新生成命令。

**为什么改** — 一条具约束力的规则正被 `main` 上的 CI 违反,而且已经造成实测伤害:有开发者读了同类的陈述并
把「这个检查跑不了」写进自己的 PR 说明。裁决(决策批次 objectstack-ai#91)选了 A:接线是对的,规则过时了。

**风险与代价(含回滚)** — 纯散文,零代码、零产物、不发布任何东西,行数净增为 0。回滚 = revert 这一个提交。
唯一的判断成本是:裁决要求的前提测量结果是「两份清单在门禁读取的字段上不相等」(18/57 个组件的输入名集合
不同),但门禁的判决集在两份输入下逐块相同,差异全部落在不上棘轮的 node-level 桶里。⇒ 本 PR 不收窄判决集,
把那个不等价作为 spec 车道的独立卡记录下来。

**席位意见** —

**你要做的** — 受管面:请人工合并。⛔ 没有席位会把它翻成 ready、入队或挂 auto-merge。如果你认为「两份清单
不等价」这件事应当先由 spec 车道处理完再合这段散文,请直接说,这段可以等。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…stone, D2 conversion and baselines (objectstack-ai#17792)

Fixes objectstack-ai#17260

Executes the objectui#8285 director-seat ruling (comment 5583979207,
decision batch objectstack-ai#91, 2026-09-08, standing maintainer delegation) — ruled
**option B**: `quickAdd` is retired from the `object-kanban` board and
stays only on the `kanban-ui` block, where a React host can supply the
runtime function the control needs. This PR is the tombstone half that
ruling assigns to this repo. A vs B is not re-opened here.

- **Clause-②: yes** — this PR narrows a published accept set:
`ObjectKanbanProps.quickAdd` is retired from `object-kanban`.

## The premise, re-measured rather than relayed

The card's body said an author writing `quickAdd: true` got "nothing,
with no diagnostic". The filer corrected that themselves, and the
correction is what holds — re-measured here at the objectui sha this
repo pins (`.objectui-sha` = `53ded82bf`), not at that checkout's HEAD:

| probe at the pinned sha | reading | control (same instrument, same
file) |
|:--|--:|:--|
| `onQuickAdd` in `plugin-kanban/src/ObjectKanban.tsx` | **0** |
`onCardClick` — **6** |
| `quickAdd` in the same file | **0** | `objectName` — **38** |
| `object-kanban` registration `inputs` (`index.tsx:421-431`) |
`objectName`, `columns` — `quickAdd` absent | `objectName` present |

The board **forwards** the key — `ObjectKanban.tsx:931` spreads the
authored bag into `KanbanRenderer`, which passes
`quickAdd={schema.quickAdd}` alongside `onQuickAdd={schema.onQuickAdd}`
(`index.tsx:196`) — but `KanbanImpl` gates the affordance on **both**
(`:355`, `:368`), and `onQuickAdd` is a host-supplied function JSON
cannot carry and no producer puts on an `object-kanban` node. So the
gate was permanently false: accepted-and-dropped, exactly as the card
classifies it.

Two readings the card's numbers came from could **not** be reproduced at
the pin, and are reported as such rather than passed on:
`OBJECT_KANBAN_INPUTS` (the 13-key constant) and the `inert-quick-add`
interim diagnostic do not exist at `53ded82bf` at all — both are later
objectui work. `OBJECT_KANBAN_INPUTS` does resolve at that checkout's
HEAD (control lit, 3 files), and the registry-spec ledger records the
key verbatim there as `ESCALATED (object-kanban.quickAdd — measured NOT
honoured)`. The pin's own equivalent reading is the `inputs` row in the
table above, and it says the same thing.

## The retirement kit

| carrier | what changed |
|:--|:--|
| `ObjectKanbanPropsSchema.quickAdd` | `retiredKey()` tombstone — `tsc`
types it `never`, and a value reaching the parse raises the prescription
instead of a bare unknown-key verdict |
| the schema's docblock | it listed `quickAdd` among the keys reached
"via the forwarded schema" — true about the FORWARD, false about the
READ, which is how the key kept re-authorizing itself. Corrected in the
same stroke |
| `src/conversions/registry.ts` | D2 conversion
`object-kanban-quick-add-removed` — a pure lossless delete (the key
never had an effect to preserve), scoped by component `type` so the LIVE
`kanban-ui` spelling stays out of its reach |
|
`migrations/entries/retired-keys/18.ui__ObjectKanbanProps__quickAdd.ts`
| `RETIRED_KEYS_BY_MAJOR[18]` entry `ui/ObjectKanbanProps:quickAdd`,
plus the D3 chain-step wiring and rationale |
| `authorable-surface/ui.json` | the row becomes
`ui/ObjectKanbanProps:quickAdd [RETIRED]` |
| `content/docs/references/ui/component.mdx` | regenerated — the row now
prints the prescription |
| `src/ui/component.test.ts` | four pins (see the ablation below) |
| `.changeset/17260-object-kanban-quick-add-retired.md` | `minor`,
`adr-0087: registered object-kanban-quick-add-removed` |

`packages/spec/src/ui/view.zod.ts` is untouched — it is another round's
declared face this batch. The `kanban-ui` block's `quickAdd` is
untouched by design: it is not a component type this spec declares at
all, which is why the conversion is scoped by `type` rather than by key
name.

## Liveness — measured, with a lit control

Zero stored or example stacks in this repo carry `quickAdd`, because
**no `object-kanban` component is authored anywhere under `examples/` or
`apps/`**. The zero is a reading, not a dark probe: the same instrument
over the same corpora returns `object-grid` 3 and `object-metric` 8.
Repo-wide, `quickAdd` occurred in exactly four places before this PR —
the schema key, the docblock sentence, the ratchet row and the generated
docs row — i.e. only the carriers being retired here. Out-of-repo
authors are unknown and unknowable from here, which is what the D2
conversion and the prescription exist for.

## Verification

| run | result |
|:--|:--|
| `pnpm --filter @objectstack/spec build` | `VERDICT command-exit 0` |
| `pnpm --filter @objectstack/spec test` | `VERDICT command-exit 0` —
Test Files 473 passed (473), Tests 13443 passed (13443) |
| `pnpm --filter @objectstack/spec typecheck` | exit 0 |
| `pnpm --filter @objectstack/spec check:generated` | 15/15 artifacts
current (one lap: `gen:migration-registry`, then the build's
`gen:schema`, then `gen:docs`) |
| `pnpm --filter @objectstack/spec check:migration-registry` | `✓
src/migrations/registry.ts is current (202 semantic, 168 retired-key,
178 retired-def)` |
| `node scripts/check-adr-0087-registration.mjs --base origin/main` | `✓
1 declared-breaking changeset(s), each carrying an ADR-0087 disposition`
— `registered object-kanban-quick-add-removed (new here)` |
| `node scripts/check-changeset-no-major.mjs --base origin/main` | `✓
This diff introduces no major bump` |
| `pnpm check:nul-bytes` | OK — 8453 text files scanned |

The registry was regenerated by the repo's own generator (`pnpm --filter
@objectstack/spec gen:migration-registry` → `✓ wrote
src/migrations/registry.ts`), never by hand; the first build before that
run failed loudly with `1 key(s) were tombstoned with no registered
retirement`, which is the gate doing its job.

**Ablation — the pins can fail.** On `HEAD`: 4 passed. With the
tombstone mutated back to a live `z.boolean().optional()` in source
(mutation proven on disk: tombstone-call count 1 → 0, injected marker 1,
blob hash `106299e7…` → `e657a7e3…`), the same run goes **2 failed / 2
passed**: the two refusal pins are the discriminating half. Restored
from `HEAD` and proven byte-identical (blob back to `106299e7…`, `git
diff HEAD` empty). Reported honestly: the `not.toHaveProperty` pin does
**not** flip under that mutation — an optional key absent from the input
is not materialized either way — so it guards the strip direction and
not the refusal.

## Changeset — measured, not assumed

Owed, at `minor`.

- **Subject**: the tombstone's prescription text reaches the published
`dist` — 2 hits, `dist/ui/index.js` and `dist/ui/index.mjs`, both inside
`packages/spec`'s `files[]`.
- **Positive control**: a pre-existing shipped describe from the same
schema — 2 hits, same files.
- **Negative control**: text that exists only in `component.test.ts` —
**0** hits in `dist`.

Level is `minor`, not `major`: `scripts/check-changeset-no-major.mjs`
forbids `major` during the launch window (lockstep versioning would
promote ~70 packages), and the sibling retirement one entry over
(`ui/ObjectGridProps:defaultSort`, objectstack-ai#11805) is registered under protocol
18 on the same reading. `api-surface/` is unchanged and correctly so —
it ratchets export existence, and `ObjectKanbanProps` still exists, one
key narrower.

## Not flipping this ready

An at-tier contract-review verdict is owed on the head that lands;
`needs:contract-review` rides this PR. Draft, not enqueued.

Co-Authored-By: Claude <noreply@anthropic.com>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---
_Generated by [Claude
Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_

---
_Generated by [Claude Code](https://claude.ai/code)_
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…rget-unknown` is an `error` (objectstack-ai#16310) (objectstack-ai#17777)

Closes objectstack-ai#16310

Clause-②: yes

`validate-translation-references` already found every orphan locale key,
named its
id, named its locale and printed the remedy — and failed nothing. This
makes it
fail. Severity only; the rule's detection logic is untouched.

## The ruling this executes

Decision batch objectstack-ai#91, director seat (comment 5583985173) — **option A**,
verbatim:

> **Ruled.** An orphaned locale key is dead data that actively misleads
— grepping
> it returns a confident hit in every locale — and a rule that reports
it precisely
> while exiting 0 is declared-but-unenforced. The severity goes to
`error` at the
> three hard-coded sites; `os lint` then fails on it like the forward
> `i18n/missing-*` half already does. ⛔ B / D refused … ⛔ C refused (a
195-id
> migration across two producers for one rule); ⛔ E refused …

The card lists two independent causes and does not order them. **Only
cause 1
(severity) is moved.** Cause 2 (the bare, un-namespaced rule id) is
option C, which
the ruling refused: `packages/lint` exports 200 rule-id constants and
195 are bare,
so prefixing this one makes it the sixth exception or forces a
cross-producer
migration — and renaming a published finding id is itself breaking.

### A consumer really can select this rule by its id (asked for, since
only severity moved)

Two readings, both on this tree:

1. **Exact-id selection works and is the published contract.** The
registry finding
   reaches the JSON report as `rule: "translation-target-unknown"`, so
`jq '.issues[] | select(.rule == "translation-target-unknown")'` selects
it
today. Pinned in `validate-translation-references.test.ts` against the
literal
string as well as the exported constant, so a rename cannot pass the pin
by
   moving the constant alone.
2. **After this change no selection is needed at all.** The rule fails
the run by
default, on the plain `os lint` / `os validate` / `os build` exit code,
with no
flag and no config. That is the acceptance criterion, and it is met on
the
   default path rather than on an opt-in one.

What a consumer still cannot do is reach this rule by *family prefix*.
That is
exactly the asymmetry the card named, and it stays — by ruling, not by
oversight.

## Measured on this tree (⛔ the card is not cited as evidence)

`examples/app-crm`, 8 orphan locale keys planted across both locale
bundles
(`apps.crm_app.navigation`, `objects.crm_lead._sections`,
`objects.crm_lead._views`,
`objects.crm_lead.fields`), `objectstack lint --json`. Planted,
measured, restored;
the restore is proven by blob-hash identity against `HEAD`, not by an
exit code.

The card's shape reproduces: baseline clean, +8 findings when planted,
`passed: true`,
exit 0 — and the pipeline was green throughout.

| tree | total | errors | warnings | `passed` | exit |
| :-- | --: | --: | --: | :-- | --: |
| **before this PR** | | | | | |
| baseline | 12 | 0 | 10 | `true` | 0 |
| 8 orphan keys planted | 20 | 0 | 18 | `true` | **0** |
| restored | 12 | 0 | 10 | `true` | 0 |
| **after this PR** | | | | | |
| baseline | 12 | 0 | 10 | `true` | 0 |
| 8 orphan keys planted | 20 | **8** | 10 | `false` | **1** |
| restored | 12 | 0 | 10 | `true` | 0 |

(`--skip-i18n`, which suppresses the coverage walk but not this rule, so
the table
stays readable. Without it the same run reads 113 / 121 / 113 total with
the same
0 → 8 error delta and the same exit-code flip.)

All three authoring commands move together on the planted tree:

| command | before | after |
| :-- | --: | --: |
| `os lint` | exit 0 | exit 1 |
| `os validate` | exit 0 | exit 1 |
| `os build` | exit 0 | exit 1 |

### ⭐ Negative control — a clean tree is unchanged, no new noise

On the pristine tree the `os lint --json` report is **identical field
for field
before and after**, with one exception: `duration` (wall clock). Checked
on three
clean runs (`baseline`, `baseline --skip-i18n`, `restored`): same
`total`, same
`errors`, same `warnings`, same `passed`, same `issues` array, same exit
code 0.

That identity is structural, not lucky: on a clean tree this rule
returns zero
findings, so the severity literal this PR changes is never reached.

### ⛔ Not "promote all warnings" — 1 rule of 13

Measured on the planted tree, which carries findings from **13 distinct
rules**:

- rules whose severity set changed: **1** —
`translation-target-unknown`, `warning` → `error`
- rules unchanged: **12**
- the finding set is **identical modulo that one severity** (same count,
same paths,
  same message and hint text: 121 findings before, 121 after)

`translation-option-key-unknown`, raised by the *same function*,
deliberately stays
`warning`: a mis-keyed option translation names something real and its
remedy is a
rename, not a deletion. `validateTranslatableSections` — the sibling
asking "is there
a key at all?" — is untouched; its comment claiming it is warning-only
*"for the same
reason its sibling is"* was corrected, since that reason no longer
holds.

### The runtime publish gate is unaffected — a measured zero

`validateTranslationReferences` reaches the runtime door on a `flow`
write (default
`runtimeTypes`), so this could have been a refusal widening at the
hottest door.
It is not: the per-write snapshot carries only `objects` / `permissions`
/ `books` /
`datasets`, and `RuntimeStackContext` has no `translations` member for a
host to
fill, so the rule sees no bundle and returns nothing there.

Measured: a `flow` write through `runRuntimeAuthoringRules` returns **0
errors and
0 advisories** from this rule, with `validateReferenceIntegrity`
confirmed in
`rulesRun`, and `buildRuntimeWriteSnapshots(...).baseline` confirmed to
carry no
`translations` key. No publish that used to succeed is refused.

## ⭐ Reverse-read — which existing sentence does this make false?

Six live sentences, all repaired in this PR:

1. `validate-translation-references.ts` module note: *"All findings are
**warnings**.
An orphan key is inert, not broken."* — rewritten; the inertness reading
is what
the card measured wrong, and the new note says why, and why ADR-0072 D1
keeps the
   promotion narrow instead of licensing the neighbours.
2. `TranslationRefFinding.severity` doc: *"Always `warning` …"* —
rewritten to state
   the split.
3. `TranslationRefSeverity = 'warning'` — widened to `'warning' |
'error'`.
4. The nested-screen comment calling a missed screen *"a
warning-severity false
positive"* — that false positive now fails the run; the comment says so.
5. `reference-integrity-suite.ts` on `validateTranslatableSections`:
*"warning-only
for the same reason its sibling is"* — the sibling no longer is.
Re-grounded on
its own reading (the surface is present; only its heading stays in the
source locale).
6. Eight test assertions pinning `severity: 'warning'` — **re-judged in
place, ⛔ not
deleted**, with the reason recorded in a block comment at the head of
the file.
That silence was deliberate and the note says what it encoded and why it
was wrong.

**Reported zeros** — swept and found nothing to change:

- `content/docs/**`: **0** mentions of this rule id.
- `packages/lint/README.md`, `packages/cli/README.md`: **0** mentions.
- `packages/cli/src/**`: **0** sentences about this rule's severity (the
CLI maps
  `f.severity` through generically; `error` passes through untouched).
- `docs/audits/2026-07-…-reference-integrity-assessment.md`: mentions
the rule in a
  historical findings table with no severity claim — **not** falsified.
- `examples/app-showcase/test/seed.test.ts`: already says the rule
*"fails on a bundle
entry no section declares"* — **not** falsified, and now literally true.
- `reference-integrity-suite.test.ts:352`: asserts id membership only —
**not** falsified.
- In-repo stacks that would newly go red: **0**. `examples/app-crm`,
`examples/app-todo`
and `examples/app-multi-package` each report **0** `translation-*`
findings.
(`examples/app-showcase` could not be linted in this container — it
fails to load on
a missing `@objectstack/connector-mcp` dist, a build-ordering condition
unrelated to
  this diff. Declared to CI.)

**One falsified sentence is NOT repaired here, deliberately:**
`skills/objectstack-i18n/SKILL.md:197` says these commands *"report it
as **warnings**
(`translation-target-unknown`, `translation-option-key-unknown`)"* —
half of that is
now false. `skills/**` is out of this PR's landing scope by dispatch,
and it is a
governed surface with its own seat. Flagged for routing rather than
edited.

## Ablation — the new pins can fail

Reverting the severity constant to `'warning'` in source (on-disk proof:
injected
spelling `grep -c` = 1, replaced spelling `grep -c` = 0) turns the
rule's test file
**red — 9 failed / 58 passed, exit 1**. Restoring (blob hash equal to
`HEAD`,
`git diff HEAD` empty, mutant `grep -c` = 0) returns it to **67 passed,
exit 0**.
The tests import the rule by relative path, so no `dist/` is in that
loop; the
`dist/`-mediated statement is the CLI table above, measured across a
real rebuild.

## Changeset

`@objectstack/lint` **`minor`** (⛔ no `skip-changeset` — a `Clause-②:
yes` PR takes
at least `minor`), carrying the `**BREAKING**` banner with its
before/after and the
one-line author remedy, plus the ADR-0087 disposition
(`not-required (no-migration-prescription)`), verified by
`pnpm check:adr-0087-registration` — exit 0, the disposition echoed on
the pass path.

⚠️ **Worth a reviewer's eye.** The first draft framed the exit-code
delta with the
`FROM → TO` prescription template. The gate refused it, correctly: a
body carrying a
migration prescription contradicts `no-migration-prescription`, and none
of the other
four categories is honest here (`unpublished` — lint publishes;
`already-registered` —
no entry covers this; `runtime-interface-only` — inherits the same
refusal;
`type-surface-only` — requires an `any`/`unknown` base-side reading, and
`TranslationRefSeverity` was concrete at base). The changeset now states
the delta as
what it is — a measured before/after of the tool's own verdict — because
it is not a
migration: no authorable key moves, an orphan key resolved to nothing
before this
release and resolves to nothing after it, and the rule has printed each
one with its
remedy in every release that shipped it. ⛔ The `**BREAKING**` token was
not dropped.
If a reviewer reads that as a category gap rather than a mislabel on my
part, it is
the objectstack-ai#13080 shape one axis over (a published *verdict* narrowing) and
wants its own card.

## Verification

- `pnpm --filter @objectstack/lint test` — **101 files, 3749 tests, all
pass**
- `pnpm --filter @objectstack/lint --filter @objectstack/cli typecheck`
— both `Done`
- Derived gate family (`scripts/pm/dispatch-gates.mjs --commands`, 59
commands):
**58 exit 0**. The one non-zero is `check:dual-build-cjs-loads` **exit 3
—
`PREREQUISITE NOT MET`**, which prints *"⛔ This is NOT a pass: nothing
was
measured"*: it reads built output and several unrelated packages have no
`dist/`
  in this container. Not measured, declared to CI.
- `pnpm lint` equivalent run in full, not narrowed: `eslint .
--no-inline-config`
  over **6640 files — 0 errors, 0 warnings**, at `29bbb886`.

## Acceptance notes

- ⛔ Auto-merge not armed; PR is a draft.
- Assignee and the `Claim:` comment were placed by the PM; neither was
written or
  changed here, and no second claim was posted.

---
_Generated by [Claude
Code](https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ecision in words instead of a tracker number (stage 9) (objectstack-ai#21698)

Part of objectstack-ai#20749
Clause-②: no

Stage 9 of the `domain:spec` lane's share of the runtime-string
burn-down (ruling `5902360492`, form D): the author-visible tracker ids
in **step 18's** rationale in
`packages/spec/src/migrations/registry.ts`, per the pointer `5968465093`
and stage 8's landing `5976757373`. Every cited decision is now stated
in words, or the citation is dropped where its sentence already said
what was decided. Text only. The test strings (class (e)) are a later
stage, so this PR closes no card.

## What changed

- **`STEP18_RATIONALE`: 49 of its 79 fragments carried 88 tracker ids.**
72 are objectstack issue numbers, 12 point into objectui (nine issues
spelled `objectui#N`, one issue spelled `ui#N` twice, one PR), and 4 are
director decision-batch numbers that the gate's id pattern also counts.
Exactly those 49 fragments changed, and each now reads id-free. Fragment
ids, orders and the other 30 fragments are byte-identical.
- **Two two-digit decision-batch numbers** (`decision batch objectstack-ai#91`, `objectstack-ai#77`)
sat inside two of the same parentheticals. The gate's pattern (three to
five digits) does not count them. They went with the sentence they
belonged to, and the ruling they named is stated instead.
- **No reader pinned the rewritten words.** Every string and regex
literal in the repository's code was matched against each fragment's old
and new text. No assertion that matched the old text fails on the new
one. See "Readers" below.
- **Nothing to regenerate.** No generated artefact prints step 18 yet:
`docs/protocol-upgrade-guide.md` prints majors up to `PROTOCOL_MAJOR`
(17), and `spec-changes.json` carries no step rationale.
`check:generated` reads all 15 artefacts up to date at this head. `os
migrate meta` prints the text at runtime today, because its chain runs
to the highest registered major.
- One `@objectstack/spec` **patch** changeset, `Clause-②: no`. The
rationale ships in `dist/migrations`.
- No step-17 text, no code comment, no test title, and no schema, order,
id, conversion or behaviour change.

## Census at the base (A1)

Base `7d0781482d`, the worktree before any edit, which is the claim's
stamp. I reused stage 8's instruments byte-identically (`census.cjs`,
md5 `6e42a45a926d375013c32d62f16a296e`; `skeleton.cjs`, md5
`a43a72c63f55c883da7a54b6f600ca08`) and held the census to the same
reference: `check-doc-authoring.mjs --census`, run from a scratch copy
whose `PACKAGES_PROSE_ROOT` / `PACKAGES_PROSE_EXCLUDED` point at
`packages/spec/src` and at nothing. The copy is byte-identical to the
one stages 7 and 8 used (md5 `8edc9a4064f55d2766c1eb0d3e8d889f`; the
live gate differs from it only on those two lines). Result: **88 = 88
ids, 0 differences per (line, id)**.

| region of `migrations/registry.ts` | messages | ids | gate literals |
|:--|--:|--:|--:|
| step 17 (`const step17`) | 0 | 0 | 0 |
| step 18 (`STEP18_RATIONALE`, lines 5025–6408) | **49** | **88** | 81 |
| step 18 entries (`surface` / `replacement` / `reason` /
`acceptanceCriteria`, generated regions) | 0 | 0 | 0 |
| rest of the file | 0 | 0 | 0 |

- **Reconciled with stage 8's 46 / 85.** I re-ran the instrument on the
registry at stage 8's base `eea82af677`: 46 / 85, reproduced. Between
that base and this one, PR objectstack-ai#21673 (stage 5 of objectstack-ai#21464, landed as
`72f3c74d60`) added three step-18 fragments, orders 71–73
(`ui-object-metric-compare-to-typed`,
`ui-object-metric-drill-down-typed`, `ui-object-grid-columns-typed`),
each citing that card once. 46 + 3 = 49 and 85 + 3 = 88. No other
fragment differs between the two bases. Ancestry with a control leg on
this shallow checkout: `72f3c74d60` is an ancestor of HEAD (exit 0) and
not of `eea82af677` (exit 1), while the older stage-7 landing
`36e4647520` is an ancestor of `eea82af677` (exit 0).
- 1859 files scanned, 1355 parsed, 0 parse diagnostics. Test strings at
the base: 1803 messages / 1919 ids in 425 files, stage 8's after-count.
- **Lit controls:** at the base, `:5066` (`objectstack-ai#8681`) and `:5126` (`objectstack-ai#118`)
count. After the rewrite, an id planted into a scratch copy of the new
file (one literal of the `targetVariable` fragment) reads 1 message / 1
id, folded over the whole fragment (lines 5515–5522).
- **Dark controls:** in the `STEP18_RATIONALE` region, 96 raw `#NNN`
hits, 8 of them on comment lines (the step-18 docblock,
`:6421`–`:6440`), which read 0: 96 − 8 = 88. Past `:6444`, 957 raw hits
sit on comment lines in the generated entry regions, and all read 0. The
header docblock (`:19`, `:32`, `:40`, `:62`) and step 17's 18
comment-line hits read 0.

## After

At `9b8c7f38d7`: step 18 reads **0 / 0**, step 17 still reads 0 / 0, and
`packages/spec/src` outside test strings reads 0 messages / 0 ids. The
gate reference agrees (0 sites). Test strings are unchanged at 1803 /
1919.

## Text-only proof

Stage 8's `skeleton.cjs`, base copy against the head file: **SAME —
22335 skeleton tokens, 3508 string groups, 49 changed (each id-bearing
before, id-free after), diagnostics 0/0.** A `+` chain's string run is
one group, so a fragment's whole text is one group; the 49 changed
groups are the 49 fragments, and every other string in the file
(fragment ids, entry ids, surfaces, conversions) is byte-identical.
Controls, each mutation counted on disk on a scratch copy of the head
file (anchor hit 1, mutation landed):

| control | expected | got |
|:--|:--|:--|
| rename the `step18` binding | DIFF, exit 1 | exit 1 |
| a fragment's `order` 43 → 44 | DIFF, exit 1 | exit 1 |
| a fragment's `id` string renamed | VIOLATION, exit 3 | exit 3 |
| a step-18 entry's `surface` edited | VIOLATION, exit 3 | exit 3 |
| a literal re-split inside a `+` chain | SAME, exit 0 | exit 0, 49
changed |
| an id-free fragment's text edited | VIOLATION, exit 3 | exit 3 |
| one fragment reverted to its base text | SAME, 48 changed | exit 0, 48
changed |
| an id planted in a rewritten fragment | VIOLATION, exit 3 | exit 3 |

Lines grew where words replaced a number: the longest new line is 111
characters, inside the region's existing maximum of 117. There is no
`max-len` rule; ESLint reads 0 / 0 on the file.

## Per-record table

Every cited record was read through REST with all its comments (59
objectstack issues and PRs, 11 objectui issues and PRs). The four
three-digit decision-batch numbers are not cards of this repository:
objectstack issues 118, 121, 132 and 210 are unrelated broken-link
reports and a docs PR from 2026-01, which is the control that they are
batch numbers. Each batch's decision was read from where it is recorded
(a ruling comment, or the spec CHANGELOG). Numbers below are written
bare.

| fragment (order) | record | decision as read (where) | in the text now
|
|:--|:--|:--|:--|
| `metadata-plugin-additional-types-retired` (2) | 8586 | remove
`additionalTypes` (ruling 5293094846) | dropped |
| `field-scale-precision-integer-refused` (3) | 8321 | refuse malformed
`scale` / `precision` at authoring (body, ACCEPT 5296942541) | dropped |
| same | 7501 | enforce `scale` by rejecting, never by rounding (ruling
5250623270) | "the write-time `scale` check, which refuses an over-scale
value rather than rounding it" |
| `admin-export-wildcard-removed` (4) | 8681 | remove `allowExport` from
the `'*'` entry of both admin sets (ruling 5299825940) | dropped |
| same | 5491 | remove `member_default`'s `'*'` object grant on create /
read / edit (ruling 5219845380) | "the earlier removal of
`member_default`'s CRUD wildcard" |
| `record-chatter-position-vocabulary-converged` (5) | 8762 | the
renderer's vocabulary, a conversion, the defaults dropped (ruling
5299771841) | dropped; the ruling's date stays |
| `element-input-target-variable-retired` (6) | 9198 | retire the hint,
the retirement route (ACCEPT 5311252358) | dropped |
| `element-filter-retired` (7) | 9220 | retire `element:filter` at
element grain (verdict 5312176877) | dropped |
| same | 9198 | its sweep recorded the element-grain lead and left it |
"the wider finding that the `targetVariable` retirement recorded" |
| `element-form-retired` (8) | 9249 | retire `element:form` at element
grain (cloud reading 5350190376) | dropped |
| same | 9220 | the `element:filter` retirement's own verdict sweep
found it | "the `element:filter` shape … recorded by that retirement's
own verdict sweep" |
| same | 7751 | direction A: the `object-*` blocks' props enter
`ComponentPropsMap`, so the component-props gate judges them (ruling
5261743573) | "its props declared for the component-props gate" |
| `field-inline-and-related-list-columns-closed` (9) | 9227 | a strict
element schema for `inlineColumns`, `relatedListColumns` in the same
pass (ruling 5315735776) | dropped |
| same | objectui 3951 | the published spelling `name` wins, the reader
is fixed, no tolerant dual read (ruling 5236150020) | "objectui aligned
the widget to `name` and retired the `field` spelling with no tolerant
alias" |
| `cube-metric-filters-retired` (10) | 10414 | the remove leg (triage
5363699539) | dropped |
| same | 10298 | the dataset path answered unfiltered aggregates under
the measure's name (body) | "the same defect the dataset path had" |
| same | 10411 (PR) | the strategy compiles a measure's `field` and
`filter` on both doors (ACCEPT 5360094389 on 10298) | "repaired … when
the analytics strategy began compiling each dataset measure's `filter`"
|
| `record-highlights-field-icon-retired` (12) | 10054 | option A, retire
(ruling 5364978909) | dropped; the ruling's date stays |
| same | 8691 | the rail's row declares what the renderer reads; `icon`
is refused, since no render path reads it (ACCEPT 5296436860) | "the
shape that got the reference-rail `icon` refused" |
| same | 5176 | A: declare `readonly`, because the chip's gate reads it
(ruling 5196402876) | "declared because the chip's read-only gate reads
it" |
| `page-component-responsive-retired` (15) | 11027 | B: retire, and
repair the four redirect texts (ruling 5380752244) | dropped |
| same | 4876 | A: retire `widgets[].responsive` (ruling 5169512655) |
dropped; "tombstone" already says it |
| `object-grid-default-sort-retired` (17) | 11805 | retire, the
changeset not major (ruling 5404972152) | dropped |
| same | objectui 5861, objectui 4869 | 2026-08-22 「接受所有」: lower the
legacy pair through the shared sort sink now, and retire
`table.defaultSort` on its own card (ruling 5377367456 on 4869) | "the
producer half of objectui's `table.defaultSort` retirement, which the
maintainer's 2026-08-22 「接受所有」 ruling on objectui's sort sink ordered" |
| `permission-restore-purge-bits-retired` (18) | 12497 | the
implementing card of the ruling below (body) | dropped |
| same | 1883 | B: retire the two bits; the card stays open as the M2
anchor, and the keys return with the feature (ruling 5421209848); gating
operations that do not exist is false compliance (ruling 5163046733) |
"which chose retiring the two bits over gating operations that do not
exist"; "the M2 lifecycle initiative …, which stays open as their
anchor" |
| same | 8106 (PR) | pins the engine's seven-verb dispatch vocabulary
(body) | "which a test pins" |
| same | 3004 | `owner_id` is server-managed for non-privileged writers
(body); `allowTransfer` enforced through that write guard (1883 ruling
5163046733) | "the server guards who may rewrite a record's owner" |
| `field-reference-to-spelling-retired` (21) | 13700, objectui 6837 | C:
the server normalizes the protocol and the renderer only executes it;
half 1 on the server, half 2 deletes objectui's arms (ruling 5475017957;
PASS 5480047935) | "the server half of the maintainer's 2026-08-31
ruling …"; "which the ruling's objectui half deletes" |
| same | 4923 | equal values: the shadowed alias is deleted with a
notice; different values: both kept, the strict door names both (ruling
5173149122) | "the house precedence for a shadowed alias" |
| `compliance-deadline-keys-retired` (23) | 14477 | A: retire per
family; e-signature too unless roadmapped (ruling 5518646938) | dropped;
"the deadline-key ruling" |
| same | 15513 | A: the three families retire whole, not roadmapped
(ruling 5548577921) | dropped |
| `duration-keys-unit-in-key` (24) | 14478 | B: a gate plus a conversion
of every offender, no baseline (ruling 5518649320) | dropped; the
ruling's date stays |
| same | 14519 | folded into that ruling (5546967881) | dropped |
| `metadata-manager-config-inert-cache-keys-retired` (25) | 14478 | the
rename gave `ttl` its `ttlSeconds` spelling | "the duration rename's
respelling of `ttl`" |
| same | 15624 | the outer three keys are read by nothing (triage
5548953183) | dropped |
| `cron-positions-deleted` (26) | 16320, 15954 | retire the seven cron
positions family by family (option A), not mark them experimental
(ruling 5559778263) | "the 2026-09-06 ruling retired each family rather
than marking it experimental" |
| same | 3786 | derive a list from its single source, never a hand copy
(sweep 5131678201) | dropped; the sentence says what the lint does |
| `list-view-page-mount-retired` (27) | 17063 | 「撤」, 2026-09-09 (body) |
dropped |
| `object-kanban-quick-add-retired` (28) | 17260, objectui 8285, batch
91 | B: the board grows no inline record-creation path, and the key
retires (ruling 5583979207) | "the director-seat ruling of 2026-09-08
that the board grows no inline record-creation path and retires the key"
|
| `list-view-sort-string-clause-retired` (29) | 17053, objectui 8221,
batch 77 | B: the legacy string clause is retired, one spelling, the
array (ruling 5567944420) | "ruled 2026-09-07: the legacy string clause
is retired, one spelling, the array" |
| same | objectui 8758 (PR) | `convertSortToQueryParams` refuses a
runtime string; merged 2026-09-09, before the spec half | "whose
consumer half shipped in objectui first" |
| `chart-config-aria-retired` (31) | batch 118 item 2 | recommendation
C: remove rather than enforce, the protocol judged wrong for this one
key (spec CHANGELOG entry `2bf6ef1`) | "maintainer decision of
2026-09-12 — judge the protocol wrong for this one key" |
| `dashboard-widget-chart-config-structure-refused` (32) | batch 121
item 1 | C+D: the dataset owns structure, `chartConfig` appearance, and
the structural keys are refused by name (ruling 5644017639 on card
17385) | dropped; the ruling's date stays, and the sentence states the
split |
| `translation-per-app-settings-platform-only` (33) | 15178, batch 132
item 2 | ②: the bundle type splits, and the per-app bundle refuses
`settings` (ruling 5653315643) | "maintainer ruling 2026-09-13: settings
copy belongs to the platform" |
| same | 19620, batch 210 item 2 | B: `settings` leaves the item door
too, one app metadata type, one shape (ruling 5770445203) | "maintainer
ruling 2026-09-22: one app metadata type, two authoring doors, one
accepted shape" |
| `object-tenancy-organization-field-retired` (34) | 19054 | off the
authorable surface, kept as a platform fact (body) | dropped |
| `page-component-filter-record-to-rule-array` (36) | objectui 6206 | B:
one filter spelling platform-wide, the rule array (ruling 5406409590) |
"ruled 2026-08-25: one filter spelling platform-wide, the rule array" |
| same | 17321 | B: a partial conversion of the lossless subset;
combinators stay as stored (ruling 5644018752) | "ruled 2026-09-12"; the
sentence states the rest |
| `view-item-owner-hidden-retired` (37) | 20085 | retire both keys
(triage 5826969296) | dropped |
| `ui-report-joined-chart-retired` (38) | 20161 | retire (triage
5852548444) | dropped |
| `view-overlay-owner-hidden-retired` (39) | 20230, 20085 | follow the
view item's disposition for the same pair (triage 5856621469) | "the
view item's disposition for the same key pair, followed here as triage
directed" |
| `ui-form-layout-inline-grid-retired` (40) | 20221 | retire `inline` /
`grid` (triage 5855767378) | dropped |
| same | 18900 | A′: a capability mainstream platforms have gets its
consumer once; one they lack retires (ruling 5727134555); triage applied
it: multi-column already exists, as `columns` | "the maintainer's family
criterion (a capability mainstream platforms have is served once, here
by `columns`)" |
| `currency-config-precision-retired` (41) | 19992 | retire (triage
5817146460) | dropped |
| `permission-rls-tags-retired` (42) | 20321 | RETIRE by the criterion
(triage 5860425529) | dropped; the sentence states the criterion |
| `flow-decision-edge-branching-first-match` (45) | 15429 | 「跟主流对齐」:
first match, inclusive only when declared (ruling 5793803317,
2026-09-23) | dropped; the ruling's date added |
| `ui-object-master-detail-form-details-closed` (56) | 20928 | a strict
detail entry (ACCEPT 5936904192) | dropped |
| `ui-record-line-items-props-closed` (57) | 21142 | the row and the
producer fix (ACCEPT 5941249538) | dropped |
| `ui-object-grid-export-options-closed` (59) | 21229 | the object form
only, refused, not lifted (triage 5939380297) | dropped |
| `translation-widget-sub-caption-retired` (60) | 21257, objectui 11389
| C: retire both ends (ruling 5942430353, 2026-10-01) | "maintainer
ruling 2026-10-01" |
| same | 5428 item 4 | the sub-caption got its own key beside
`widget.description` (ruling 5200254075, 2026-08-06), which C reverses |
"which reverses the 2026-08-06 ruling that gave it a translation key of
its own" |
| `object-grid-resizable-columns-retired` (62) | 21445 | retire the
alias (triage 5958164933) | dropped |
| same | objectui 6152 | `resizable` is canonical, and the alias retires
with no window (seat answer 5957903936) | "objectui's ruling that
`resizable` is canonical" |
| `ui-object-grid-row-members-typed` (63) | 21445 | type the members
(triage 5958164933) | dropped |
| `ui-object-map-gantt-tree-navigation-typed` (64), list members (66),
form members (67), metric aggregate / trend (69), compare-to (71),
drill-down (72), grid columns (73) | 21464 | the staged close-out (seat
answer 5963787404) | dropped ×7; each sentence names its stage |
| `ui-ai-chat-window-retired` (65) | 21504 | retire (triage 5963897014)
| dropped |
| `element-text-variant-heading-subheading-retired` (68) | 21015 |
release 2 of objectui's ruled two-release split (body) | dropped; the
sentence names the split |
| `object-master-detail-form-detail-sort-field-retired` (70) | 21589 |
retire (triage 5969870827) | dropped |
| same | objectui 11070 (round 9) | the authored override retires; the
sort field is derived from the child object (seat answer 5928627070,
landed 5930904717) | "the spec half of objectui's own retirement of the
override" |

## Readers

- **Searched by substring, not by guess.** A TypeScript-AST pass over
all 9878 tracked code and text files collected every string literal (8+
characters) and regex literal, and tested each against each fragment's
old and new text, and against the joined rationale. A reader is owed a
move only if it matched the old text and not the new. In the files that
read the registry or the rationale, the only literals that lose a match
are generic words and patterns that match some fragment by coincidence
(`'decision'`, `'objectui'`, `/100/`), plus the tracker-id detectors,
which are absence checks that now pass. **No pin needed moving.**
- **The step-18 readers that exist, and why they hold:**
- `ui/component.test.ts` pins the phrase "retires `object-kanban`'s
`quickAdd`" and pins that the rationale never says `kanban-ui`. That
second pin shaped the rewrite: objectui's ruling kept the control on
`kanban-ui`, a node type objectui has since retired, so the new text
states the ruling without naming it.
- `system/compliance-families-retirement.test.ts` pins `/retires those
three compliance-shaped families WHOLE/`, kept verbatim.
- `data/cube-metric-expression-types-retirement.test.ts` reads a
fragment this stage does not touch.
- `scripts/step18-rationale-merge.test.ts` pins the fragments' sort and
that the rationale equals their joined text. Neither moves.
- **No reverse verification was owed:** no pin moved, so there is no
moved assertion to turn red. The text-only proof's controls and the
census's lit control stand in its place.
- **Parallel copies, not readers:** the spec CHANGELOG, the 17.1 release
notes and three liveness-ledger notes carry their own copies of some old
phrases. They are release-owned or ledger notes, and nothing compares
them with the rationale.

## Tests and gates (at `9b8c7f38d7`)

All builds and tests ran through `scripts/pm/os-verify-lock.sh`, with
each exit code written to a file before reading.

- `pnpm --filter @objectstack/spec build`: exit 0, 38/38 dts.
- `pnpm --filter @objectstack/spec check:generated`: exit 0, all 15
artefacts up to date.
- The step-18 readers, local project: `migrations.test.ts`,
`component.test.ts` and the cube-metric test, 561 / 561. Repo project:
`step18-rationale-merge.test.ts` and the compliance test, 19 / 19. The
other 19 repo-project tests that import the registry: 314 / 314.
`conversions-major18-merge.test.ts`: 12 / 12.
- Full spec suite, local project: 610 files, 18113 passed, 1 todo.
- `pnpm --filter @objectstack/spec run typecheck`: exit 0.
- A turbo build of 71 / 71 packages, then the derived union: **84 / 84
exit 0**, reconciled by `dispatch-gates --ran` (84 run, 0 NOT-MEASURED,
0 UNRUN). Three gates first answered PREREQUISITE NOT MET (exit 3,
nothing measured) before the 71-package build, and passed after it.
- ESLint on `registry.ts`, a proven narrowing: 1 file, 0 errors, 0
warnings. The population comes from ESLint's own config
(`calculateConfigForFile` returns a config, `isPathIgnored` is false).
Invariance: `parserOptions.project` and `projectService` are null, so no
type-aware rule exists and an untouched file's verdict cannot move.
- `dist`: the new phrases are in `dist/migrations/index.js` and
`index.mjs`; the old runtime phrases probed are in no `dist/migrations`
file. The ids still present in `dist` are code comments, which this
stage does not touch.
- NOT MEASURED locally: the spec repo project as a whole (stopped at the
foreground limit with no output; its registry readers ran above), and
`scripts/build-schemas-check-mode.test.ts`, which reads semantic
`surface` strings and never the rationale. Both are CI's, as is the
repo-wide `pnpm lint`.

## Overlap

objectstack-ai#21654's PR objectstack-ai#21687 (draft, this seat) adds one `STEP18_RATIONALE`
fragment (order 74, no tracker id in its text) and one D3 entry. A local
`git merge-tree` of this head with its head `a9d2d5d453` is clean, and
`registry.ts` has no merge driver, so that result is the plain text
merge GitHub runs. Whichever lands later merges `origin/main` through
`scripts/pm/os-regen-merge.sh`. `origin/main` was re-fetched just before
opening this PR (`38bef8cf95`): the four commits since the base touch
neither this file, the conversions registry nor the guide, and a local
`git merge-tree` with it is clean.

## Acceptance notes

- **Out of this stage's reach, noted:** `registry.ts`'s code comments
still carry many ids (the step-18 docblock and the generated entries'
comments), and so does `dist`, which keeps comments. They are the
comment lane's share.
- **Kept on purpose:** `decision-inbox batch 4` and `batch 5` name a
batch without a `#` number and are not ids. Present-tense clauses whose
follow-through has since landed (for example "held up today only by
objectui's fallback arms") were left as written: form D asks for the
decision, and the decision is stated.
- **Ledger notes:** `packages/spec/liveness/dashboard.json`, `page.json`
and `view.json` cite some of the same decision batches in their notes.
They are not runtime strings and sit outside this census; nothing here
touched them.

## Clause-② and changeset

`Clause-②: no`. Nothing authorable, exported or typed is added, removed
or renamed; only the value of `MIGRATIONS_BY_MAJOR[18].rationale`
changes. It ships in `dist` and `os migrate meta` prints it, so one
`@objectstack/spec` patch changeset is owed, and `skip-changeset` does
not apply.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_

---------

Co-authored-by: Claude <noreply@anthropic.com>

This branch had an error being deployed

1 failed deployment
Preview — 40db89fe Deployed Jan 23, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants