Repository navigation
Auto-organize documentation by zod source files - #91
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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>
There was a problem hiding this comment.
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.jsonnavigation fordata,system,ai, andapireferences to point to subfolders that mirror Zod schema groupings. - Added new
meta.jsonfiles 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" | |||
There was a problem hiding this comment.
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".
| "title": "Api" | |
| "title": "API" |
…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>
…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)_
…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>
…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>
Documentation was generated flat within category roots (88 files in
/ai/), making navigation difficult. Now automatically organizes into subfolders matching source.zod.tsfiles.Implementation
src/{category}/*.zod.tsfiles to buildschema → zodFilemappingSUB_CATEGORIESconfig with dynamicZOD_FILE_TITLESlookupStructure Change
Before:
After:
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 mappingsOriginal prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.