Skip to content

feat(spec)!: record:json is retired — refused by name at the schema door, the approval request page's raw JSON panel it was declared for was dropped (#24162) - #24170

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-24162-retire-record-json
Oct 17, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-24162-retire-record-json

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24162

Clause-②: no (narrowing)

Retires the page component type record:json and its props def RecordJsonProps, refused by name through RETIRED_PAGE_COMPONENT_TYPES. This follows the maintainer's ruling quoted on the card (「业务上没有这个需求啊,处理审批的人为什么需要看json」) and triage grade 6232511377. record:json was declared spec first (#23745, 97b8e7a51d) for one face, the approval request page's raw JSON panel, and that face was dropped. No renderer was built, no page placed it, and its one planned producer (PR #23837) was closed unmerged. It shipped in 18.0.0-next.3. Under the no-staging rule, this PR removes it in the next prerelease, with its own changeset. The shape is the ai:chat_window retirement (PR #21531, 48eb9c193), surface for surface.

Dev session: session_01LKyb454ATwXXksfRnB8Xiy (cloud dispatch from the domain:spec#2 seat, claim 6232774903).

Accept set, before → after, at each door

door before (eda6f14b7b) after (this head)
PageComponentType (enum alone) accepts record:json refuses: invalid_value with the retirement prescription (the enum error map). A stranger such as record:jsonl keeps zod's own message
PageComponentSchema.type (node) accepts { type: 'record:json' } refuses: code: 'custom', path: ['type'], params.retiredComponentType: 'record:json', message = the prescription. Bare, {}-bag and populated alike
PageSchema (what os validate / save parse) accepts the node refuses at regions[i].components[j].type, one issue per node
ComponentPropsMap['record:json'] (props gate, check-yaml-examples, vocabulary) strict RecordJsonProps: field (req.), label, requiredPermissions (req., min 1) row kept as retiredComponentProps('record:json') (z.never): every bag refused, {} included, invalid_type at [] with the prescription
lint component-type-unknown silent (known type) error at …type, message = the prescription's head, hint = its "Delete the …" sentence
lint component-props-* judged the bag against the strict row component-props-invalid error at …properties for every bag
typo suggester offered record:json for record:jsn no longer offers it (out of KNOWN_COMPONENT_TYPE_CANDIDATES)
print pages PRINT_REFUSED_PAGE_COMPONENT_TYPES row row removed: a retired type is refused at the parse before any print rule reads it (the map's own docblock rule)
exports RecordJsonProps (const + type), JSON Schema ui/RecordJsonProps removed, the JSON Schema unpublished

Everything else is byte-identical, including the record blocks' own fail-open requiredPermissions gate. The open string arm stays open.

The prescription (one string, all doors): "record:json was removed in @objectstack/spec 18 (ADR-0049) — it shipped only in the 18.0.0-next.3 prerelease, declared for one raw-JSON panel on the approval request page, and that panel was dropped: … Delete the record:json component node and put nothing in its place." It names no replacement, by ruling, and carries no tracker number (check:doc-authoring).

The precedent's surfaces, and whether this PR touches each

PR #21531 surface here
value leaves PageComponentType, joins RETIRED_PAGE_COMPONENT_TYPES with a prescription, no tracker number ✅ page.zod.ts
enum docblock + PageComponentSchema.type .describe() name the retirement ✅
props def deleted, row kept as retiredComponentProps ✅ component.zod.ts, plus the factory's docblock list
D3 semantic entry, one STEP18_RATIONALE fragment, RETIRED_DEFS_BY_MAJOR[18] entry ✅ ui-record-json-retired (inserted at its sorted id, order: 100 = max + 1), retired-defs/18.ui__RecordJsonProps.ts; registry.ts generated, not committed
no D2 conversion ✅ none, see below
pins at every door with a control ✅ component-record-json-retirement.test.ts (replaces component-record-json.test.ts)
vocabulary pin ✅ component-type-vocabulary.test.ts: known through the row, not a candidate, record: still reserved
component-props-unknown-members.pin.test.ts line n/a: RecordJsonProps had no z.unknown() member
generated baselines ✅ see Ratchets
content/docs/ui/pages.mdx hand-written list n/a: it never listed record:json (grep 0)
lint comment naming the def n/a: no lint source named RecordJsonProps
differs: print classification row removed (the precedent had none)
differs: a lint pin existed validate-record-json-23745.test.ts is flipped into a refusal pin, not deleted: both lint rules, the shared runAuthoringRules('validate') pipeline, and the typo case, with record:approvals as the live control. Kept rather than deleted because the map-driven flags %s case in validate-component-types.test.ts covers only the type rule, not the props rule or the pipeline
differs: a renderer contract existed its census pins move: component-record-block-field-security.test.ts (the requiredPermissions census loses record:json), component-record-related-list-parent-23134.test.ts (the whole-bag set gains it), type-alias-convention.pin.test.ts (Iso_ui_component__RecordJsonProps removed, 789 → 788 and the prose counts)
tree-scoped absence pin (playbook §4) ✅ new: a walk of the five roots @objectstack/spec already declares in cross-package-test-inputs.mjs, judging the authoring shape (a type key valued record:json), with its exclusions written beside it. Registered in vitest.repo-tests.json, which check:cross-package-test-inputs demanded

D2 decision: none registered

The playbook's §3 conversion is a per-key stripKeys. Here the whole element leaves, so there is no key a walker could strip that leaves a valid node behind. Deleting an authored page node is a layout decision, and the element-filter-removed docblock rules that a conversion must not make it. The precedent registered none for the same reason. The D3 entry carries the delegated deletion. A user on next.3 meets the prescription at os validate.

Because packages/spec/src/conversions/registry.ts is not edited, the carried correction from contract review 6142098559 item A does not ride this PR (per the dispatch: only if that file is edited).

Ratchets: a whole-def removal, so all four move

Each was regenerated by its generator. The two hand deletions are the ones the build gate prescribes.

artifact movement proof
json-schema.manifest/ui.json −1 (ui/RecordJsonProps) build: "1 schema(s) left the published set since eda6f14, each declared (#4725): json-schema/ui/RecordJsonProps.json — RETIRED_DEFS_BY_MAJOR, major 18."
authorable-surface/ui.json −3 (field, label, requiredPermissions) build: "ui/RecordJsonProps:* (3 line(s)) — def no longer emitted by this build; whole-schema removals are adjudicated by json-schema.manifest/"
api-surface/ui.json −2 (RecordJsonProps (const), (type)) gen:api-surface
declaration-map/ui.json, export-origins/ui.json −1 each gen:declaration-map, gen:export-origins
strictness-ledger counts ui.md ui/ sites 214 → 213, component.zod.ts 81 → 80 gen:strictness-ledger
reference docs index.mdx, ui/component.mdx, ui/page.mdx RecordJsonProps section gone, enum list −1 gen:docs
authorable-defaults/, docs-import-surface.baseline.json unchanged: the def was default-free and never in the docs import surface check:generated green
authorable-surface.base.json untouched —

check:generated on the final head: ✓ All 14 generated artifacts are up to date. No skills/** path moved, so check-governed-merges is not owed.

Consumer grep on this head

git grep -e 'record:json' -e RecordJsonProps -- . ':!**/CHANGELOG.md' ':!.changeset/'. **/CHANGELOG.md and .changeset/ are excluded because they are release-owned records of what next.3 shipped and are not edited here (seat ruling). The hits:

  • the retirement kit itself: page.zod.ts, component.zod.ts, the two entry files, registry.ts.template, the five spec test files and the lint pin;
  • content/docs/references/ui/page.mdx: 2 lines, the regenerated .describe() naming it as retired.

The control in the same query shape is record:approvals, which hits in 9 files. The .objectui-sha pin is not touched (objectui main: 0 hits, per the card).

Ablation (fix committed first, scripts/ablation-replace.mjs, restored by blob)

The subject resolves by relative import (./page.zod, ./component.zod) to src, so no dist rebuild was involved.

  • Mutation: the map key renamed to record:json-ABLATED (held), and the row turned into emptyProps('record:json'). Tool output: "anchor 1 -> 0, blob fee472ca872d -> 9927e199cb36".
  • Result: 7 failed, 24 passed. The failures are the map pin, the row pin, PageComponentSchema, PageSchema at the element path, the enum error map, the vocabulary pin and the candidate-list pin.
  • Restore: both files back to their HEAD blobs (e3f3674b323f, fee472ca872d), and git diff HEAD is empty.

Gates (local, final head 26e52c407)

  • spec build exit 0. check:generated ✓ All 14 generated artifacts are up to date.
  • spec vitest --project local --maxWorkers=2: 691 files, 20969 passed, 1 skipped, 1 todo. The new walk runs under --project repo: 10 of 10 passed.
  • spec typecheck exit 0, with check:test-typecheck: OK.
  • lint: closure built, then vitest: 142 files, 6812 passed, 5 skipped. typecheck exit 0.
  • Downstream importers of the changed symbols, with the closure built first: platform-objects, mcp and cloud-connection canonical-expression-envelopes (15, 11 and 16 passed), metadata-protocol protocol.runtime-authoring-gate (145 passed), showcase command-center-metric-filter (3 passed), dogfood expression-conformance (7 passed).
  • check-platform-checklist --changed-from origin/main: OK, 0 anchors lost.
  • dispatch-gates --ran: "✓ 110 derived famil(ies) accounted for — 108 run, 2 NOT-MEASURED". Every family exited 0 except the two below.
  • NOT MEASURED: check:dual-build-cjs-loads. Reason: it needs a whole-repo build, and it exited 3 with PREREQUISITE NOT MET.
  • NOT MEASURED: check:type-check-debt. Reason: repo-wide tsc, killed by the 420s per-gate cap on the shared box. The two packages this PR touches typecheck green. Both are declared to CI.
  • NOT MEASURED: spec test:repo as a whole. It belongs to CI (Test Core); only this PR's new repo-project file was run.
  • Changeset gates, with this body as the --event payload:
    • check-adr-0087-registration: "✓ 1 declared-breaking changeset(s) … [BREAKING+bang+clause-②-narrowing] registered ui-record-json-retired (new here: ui-record-json-retired)".
    • check-changeset-no-major: "✓ This diff introduces no major bump." and "✓ LEVEL AXIS: this PR declares clause-② no (narrowing), and no package whose published source … it moves is graded patch".
    • check-empty-changeset: exit 0.

Deviations, stated

  • Clause-②: no (narrowing), not the claim's Clause-②: no. The dispatch asked for the spelling the gates accept for a narrowing today. check-changeset-no-major.mjs reads (narrowing) as the BREAKING arm and stands the level axis down on a bare no. The newest merged narrowings (24131-rest-public-form-required, 24062-authored-install-scope-project) carry the same line.
  • The spec pin file is renamed component-record-json.test.ts → component-record-json-retirement.test.ts, the *-retirement.test.ts house name. Its accept pins are flipped, not kept.

Acceptance notes

  • .changeset/pre/23745-record-json-block.md and packages/spec/CHANGELOG.md are untouched. They record what next.3 shipped, and this PR's changeset records the retirement (seat ruling). content/docs/releases/ is untouched.
  • objectui#12490 (face 4) stays its domain:ui owner's call, per triage.
  • Fixed container overhead, measured: pnpm install --frozen-lockfile 52s. The first spec build took 252s on the shared box, and later ones 431–462s.

Generated by Claude Code

@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 10 documentable anchor(s). ⚠️ 8 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/authorable-surface/ui.json, packages/spec/declaration-map/ui.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json bd4276a91e94d6edf803110ccea4ca6bcbbf810d.

⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

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

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

Which tree this was computed on

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

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d450005a06e7d1c4794ea0d926567ff6b02f90b8 && git checkout d450005a06e7d1c4794ea0d926567ff6b02f90b8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bd4276a91e94d6edf803110ccea4ca6bcbbf810d a12ff6bd8835bf887ece378b577d235776e1133d && git checkout -B drift-repro bd4276a91e94d6edf803110ccea4ca6bcbbf810d && git merge --no-ff a12ff6bd8835bf887ece378b577d235776e1133d

node scripts/docs-audit/affected-docs.mjs --json bd4276a91e94d6edf803110ccea4ca6bcbbf810d

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 26e52c407a47db174d857825fb12f7221e3596e6
Local-runs: none
Scope: packages/spec/src/migrations/entries/retired-defs/18.ui__RecordJsonProps.ts, packages/spec/src/migrations/entries/semantic/18.ui-record-json-retired.ts, packages/spec/src/migrations/registry.ts.template, packages/spec/src/ui/component.zod.ts, packages/spec/src/ui/page.zod.ts + packages/spec/src/shared/retired-key.ts, packages/spec/src/ui/component-type-vocabulary.ts, packages/spec/src/ui/page-print.test.ts, packages/spec/src/conversions/registry.ts (the element-filter-removed docblock), packages/spec/scripts/build-schemas.ts (checkManifestRemovals, RETIRED_DEFS_BY_MAJOR), packages/spec/src/migrations/entries/README.md, packages/spec/src/migrations/types.ts, packages/spec/scripts/step18-rationale-merge.test.ts, packages/spec/scripts/build-upgrade-guide.ts, packages/cli/src/commands/migrate/meta.ts, packages/lint/src/validate-component-types.ts, packages/lint/src/validate-component-types.test.ts, packages/lint/src/validate-print-page-blocks.ts, scripts/cross-package-test-inputs.mjs, turbo.json, packages/spec/vitest.config.ts, scripts/pm/clause2-line.mjs, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs, scripts/pm/record-recognisers.mjs, .claude/skills/spec-property-retirement/SKILL.md, .github/workflows/lint.yml, .github/workflows/ci.yml, the rest of the diff (.changeset/24162-record-json-retired.md, the five packages/spec/*/ui.json shards, the three content/docs/references pages, docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md, the six spec test files, packages/spec/vitest.repo-tests.json, packages/lint/src/validate-record-json-23745.test.ts), the precedent commit 48eb9c193 and the rationale sweep f623e2f71, and the objectui sibling checkout at the pinned 5e15928b46.

① Derived judgments

Every door record:json was accepted at is now a refusal, each judged right.

  • The enum. record:json leaves PageComponentType.options; the enum's own error map (page.zod.ts: an error callback that looks issue.input up in RETIRED_PAGE_COMPONENT_TYPES) answers the retired name with the prescription as invalid_value, and a stranger (record:jsonl) keeps zod's own message. Right.
  • PageComponentSchema.type, the open string arm. The existing superRefine consults the map, so the node is refused at its own path with code: 'custom', path: ['type'], params.retiredComponentType: 'record:json' — bare, {}-bag and populated alike (the parse does not judge properties against the row; that gate is lint's). Right.
  • ComponentPropsMap['record:json']. The row is kept as retiredComponentProps('record:json'): a z.never whose error callback returns the map's guidance, described [REMOVED] …, where the factory throws at module load if the map has no entry — so the row and the node refusal carry one string and cannot drift. {}, the former bag and a partial bag are all refused with one invalid_type issue at []. Keeping the row is required, not optional: KNOWN_COMPONENT_TYPES derives from the row keys, lint's props gate skips a rowless type as an unregistered custom string, and check-yaml-examples judges only rowed types. Right.
  • PageSchema at the element path. Refused at regions[0].components[1].type, one custom issue, with the same page parsing when record:approvals sits in that slot. Right.
  • The lint door. validate-component-types.ts reads the map: component-type-unknown at …type as error, message = retiredTypeHead(prescription) ("record:json was removed in @objectstack/spec 18 (ADR-0049)") plus the fixed relay clause, hint = retiredTypeRemedy(prescription) = "Delete the record:json component node and put nothing in its place". component-props-invalid at …properties for every bag, {} included, because the row is z.never. The typo suggester drops the name without an edit: KNOWN_COMPONENT_TYPE_CANDIDATES is KNOWN_COMPONENT_TYPES minus RETIRED_PAGE_COMPONENT_TYPES. Right.
  • The prescription against the playbook's §2 conventions. Backticked fully qualified name first (record:json, the spelling the ai:chat_window and user:profile entries use for a component type); "was removed in @objectstack/spec 18 (ADR-0049)" — 18 is the right version, the type was published in 18.0.0-next.3 only and the removal ships on the 18 line; a dash clause stating why (declared for one panel, the panel was dropped, no renderer, no placement, the three props configure nothing); the imperative fix "Delete the record:json component node and put nothing in its place."; no os migrate meta sentence, which is right because no conversion covers the surface (the retired-key-migrate-sentence class pin requires the house sentence only where a prescription names the command); no tracker number, pinned by the test's ISSUE_ID regex. Right.

Nothing else's accept set moves, judged right.

  • PRINT_REFUSED_PAGE_COMPONENT_TYPES loses its record:json row. Required by that map's own docblock ("The retired types are refused by name at the parse already and are not repeated here") and by page-print.test.ts, whose census takes KNOWN_COMPONENT_TYPES minus the retired map and reports a retired key in either print set as stale — leaving the row would have gone red. validate-print-page-blocks.ts already skips retired types. Right.
  • The open string arm stays open: custom.record_json, object-grid, record:details pinned parsing. Right.
  • The four record blocks' fail-open requiredPermissions are byte-identical; the field-security census pin moves from five declarers to four, and the 23134 whole-bag set gains record:json (a z.never row refuses the key as invalid_type, not unrecognized_keys). Right.
  • Strictness-ledger counts ui/ 214 → 213 and component.zod.ts 81 → 80: exactly one strictObject( site left. Right.

RecordJsonProps leaves the public surface completely, and the ratchets move as a whole-def deletion must. The const and the type are deleted from component.zod.ts; the ui barrel is export * only (no named re-export existed at the merge base); api-surface/ui.json −2 (const, type), declaration-map/ui.json −1, export-origins/ui.json −1, json-schema.manifest/ui.json −1 (ui/RecordJsonProps), authorable-surface/ui.json −3 (field, label, requiredPermissions). The playbook's fourth ratchet, api-surface-signatures.json, records define* entry-point signatures only and had no RecordJsonProps row at the merge base, so it is rightly unmoved. authorable-defaults/, docs-import-surface.baseline.json, dropped-refinements, unemitted-schemas, entry-nameability and dual-source-exports baselines had no row at the merge base: rightly untouched. The liveness ledger had no row for the def or the type (props defs are not walked per metadata type), and Spec property liveness is green. type-alias-convention.pin.test.ts 789 → 788, with its prose counts. A grep of the head outside **/CHANGELOG.md and .changeset/ finds RecordJsonProps and record:json only in the kit, the two entries, the template, the six pins and the regenerated references/ui/page.mdx; the control record:approvals hits 9 files. Right.

No D2 conversion — right under the playbook's §3. §3's conversion is a per-key stripKeys. Here the whole element leaves and its row is z.never, so a strip would list three mechanical edits whose result, a bare node, the parse refuses by name anyway — the author must delete the node regardless, and the tool would be listing edits the author then undoes. conversions/registry.ts's element-filter-removed docblock states the rule the PR cites: "The component node itself is NOT removed: deleting authored page nodes is a layout decision a mechanical conversion must not make." The precedent 48eb9c193 registered none for the same reason, and for a type authored nowhere (zero measured in objectstack and objectui) there is nothing for a walker to meet. The D3 entry ui-record-json-retired carries the delegation, and os migrate meta prints every MIGRATIONS_BY_MAJOR[18].semantic entry under "manual change(s) require your judgment" (meta.ts). The entry is honest: replacement says delete the node and put nothing in its place, with no counterpart for the three keys; reason states the ruling, the zero census and why no conversion exists; acceptanceCriteria names the os validate report at the node's own type path with params.retiredComponentType, which is exactly what the superRefine emits. One limit, stated not faulted: a next.3 → next.4 upgrader is inside major 18, so the chain they replay is --from 17, which is what the changeset tells them. Right.

Registration keys are spelled as the gates read them. RETIRED_DEFS_BY_MAJOR[18] gains ui/RecordJsonProps — the category/SchemaName form entries/README.md requires and build-schemas.ts checkManifestRemovals reads by exact set membership (its (b2) mirror refuses an entry naming a still-published def; the manifest deletion gate requires the entry for the removed json-schema/ui/RecordJsonProps.json). File 18.ui__RecordJsonProps.ts follows the / → __ rule; 18.ui-record-json-retired.ts is named for its id. check:spec-changes, check:upgrade-guide and check:generated --reconcile-only ran green in Type Check · source gates. Right.

The STEP18_RATIONALE fragment is placed right and worded wrong. Placement: inserted at its sorted id, between ui-page-action-row-navigation-executor-refused and ui-record-line-items-props-closed; order: 100 is the list's highest (99, permission-set-managed-by-retired) plus one; the text is trimmed. That is the template header's rule, and step18-rationale-merge.test.ts holds it. Wording: the text reads "It also retires the record:json page element (#24162, ADR-0049 enforce-or-remove), …" — a tracker id inside a runtime string. The rationale is printed to the author: packages/cli/src/commands/migrate/meta.ts prints hop.rationale on every os migrate meta run, and build-upgrade-guide.ts prints step.rationale. AGENTS.md: runtime strings an author is shown carry no tracker number. This very list was swept of all 88 of its ids on 2026-10-04 by f623e2f71 (PR #21698, stage 9 of ruling 5902360492, form D), and the ui-ai-chat-window-retired fragment this PR mirrors reads "(ADR-0049 enforce-or-remove)" at the head, its "(#21504, " removed by that sweep — the dev copied the fragment's shape from the precedent commit 48eb9c193 instead of from the template at the head. No gate reaches this position: check:doc-authoring's spec rule enumerates guidance, tombstone, error-map and .describe() positions, not a text: field, and step18-rationale-merge.test.ts judges sort, order and trim only — so Lint & Repo Gates green carries no information about it. The two (#11509, ruling A-narrow) fragments present at the head landed on 2026-10-10 (317cddd40), after the sweep: debt of the same kind, not a precedent. A scan of every added non-comment line in the five in-scope files finds this one token and no other; the D3 entry's four strings, the prescription, the .describe() and the [REMOVED] describe are all id-free. Fix: delete "#24162, " from the fragment so the parenthetical reads "(ADR-0049 enforce-or-remove)"; the id stays in the // comments beside the entries and in the schema comments, where it belongs. One token, one file, on a hits path.

The flipped lint pin asserts the refusal at the codes it names, with a live control. validate-record-json-23745.test.ts pins component-type-unknown (error, pages[0].regions[0].components[0].type, message starting with the prescription head, hint starting with "Delete the record:json component node") for the bare and the populated node; component-props-invalid (error, …properties) for {} and the former bag; the suggester no longer naming record:json for record:jsn; and runAuthoringRules('validate') yielding exactly those two rule codes as errors — with record:approvals drawing no finding at both rules. The generic flags %s case in validate-component-types.test.ts iterates RETIRED_PAGE_COMPONENT_TYPES.keys(), so the type rule is also covered map-wide; keeping this file for the props rule and the pipeline is justified. Right.

The tree-scoped absence walk is a complete pin. It judges the authoring shape (a type key valued record:json, inline code stripped) over packages, examples, skills, content and scripts — the same roots and matcher shape as the landed element-flat-binding-retirement.test.ts; its exclusions (the two pins, content/docs/releases/, the gitignored packages/spec/json-schema/, CHANGELOG.md) carry their reasons beside them; it has a matcher self-test and anti-vacuity floors. The radius is declared: scripts/cross-package-test-inputs.mjs's @objectstack/spec globs cover packages/** by extension, examples/** by extension plus examples/*/src/**/*.ts, content/**, skills/** and scripts/**, mirrored in turbo.json's @objectstack/spec#test:repo inputs; the file is registered in vitest.repo-tests.json, so it runs under --project repo, which is Test Core's test:repo. Right.

② Semver level

  • .changeset/24162-record-json-retired.md: '@objectstack/spec': minor, a feat(spec)!: title, the BREAKING banner, a FROM → TO table with the one-line fix, the marker adr-0087: registered ui-record-json-retired, and the line Clause-②: no (narrowing) — matching what the diff publishes: an accept-set narrowing on a published authoring surface, one export removed, one JSON Schema unpublished, nothing widened. minor not major is the launch-window convention check-changeset-no-major.mjs enforces; Check Changeset is green on the head. Right.
  • .changeset/pre/23745-record-json-block.md, packages/spec/CHANGELOG.md and content/docs/releases/ are untouched (the diff against main names none of them), as triage 6232511377 ruled: the next.3 entry records what shipped. Right.
  • The spelling, and the dev's deviation from the claim's bare no. scripts/pm/clause2-line.mjs defines the closed vocabulary: no (narrowing) is "NOT a widening, but breaking". check-changeset-no-major.mjs keeps the level axis standing only when a no carries the narrowing arm — a bare no stands it down — and check-adr-0087-registration.mjs reads the clause-②-narrowing arm as a breaking signal; record-recognisers.mjs reads the narrowing face off the same arm. So the claim's bare no would have under-declared a breaking change and silenced the level axis, while the precedent's yes (narrowing) would have over-declared (this diff widens nothing). The dev's no (narrowing), at the start of a line in both the PR body and the changeset, is the correct and truthful spelling. Deviation right; the seat aligns the claim line, not the PR.

③ Boundary flags

  • Deviation, Clause-② spelling: answered in ② — right.
  • Deviation, pin file renamed component-record-json.test.ts → component-record-json-retirement.test.ts with its accept pins flipped: the house *-retirement.test.ts name (element-flat-binding-retirement.test.ts, form-field-public-picker-retirement.test.ts); every door the deleted file accepted is pinned inverted in the new one. Right.
  • NOT-MEASURED check:dual-build-cjs-loads: hosted by Build Core (ci.yml, the pnpm check:dual-build-cjs-loads step) — success on the head. Covered and green.
  • NOT-MEASURED check:type-check-debt: hosted by Type Check · debt ledger (lint.yml) — in_progress at read time. Covered, not yet concluded.
  • NOT-MEASURED spec test:repo as a whole: Test Core (1/6) … (6/6) run turbo run test test:repo (ci.yml) — all six in_progress at read time. The new walk sits in the repo project, so it runs there and nowhere else. Covered, not yet concluded.
  • Check-runs on the head at read time, 39: 26 success, 5 skipped (Console Pin Gate, the Dogfood shards, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance — full-battery lanes, not PR lanes), 8 in_progress (Test Core 1–6/6, Type Check · debt ledger, Type Check · workspace). Of the five required contexts: Lint & Repo Gates success, Build Core success, Governed Surface Queue Guard success; the TypeScript Type Check and Test Core aggregators are not yet present because their members are running. Also green: Check Changeset, Spec property liveness, Type Check · source gates (spec check:spec-changes, check:upgrade-guide, check:authorable-surface, check:docs, check:generated --reconcile-only), Type Check · consumer gates (check:api-surface), Lint · CI, release & engine gates (check:cross-package-test-inputs), Lint · repo & tooling gates (check:doc-authoring). No check-run on the head judges the rationale text, so the ① finding stands whatever the remaining runs conclude.
  • Dev-reported local results (spec build, check:generated all 14 current, 20969 spec tests, lint 6812, the 7-of-31 ablation with blob-restore) are recorded as reported, not re-run here; the gate families they cover are the check-runs above.
  • Out-of-scope note, objectui#12490 (face 4): stays its domain:ui owner's call per triage; nothing in this PR depends on it. Acknowledged.
  • Carried correction 6142098559 item A: conversions/registry.ts is not edited, so it does not ride — exactly the claim's conditional. Right.
  • Sibling pin: .objectui-sha is untouched at 5e15928b46, and the objectui checkout at exactly that commit has 0 hits for record:json and RecordJsonProps, so the full battery's Console Pin Gate is not threatened by this removal and no objectui change is owed. Right.
  • Governed surfaces: none in the file list (no .claude/**, skills/**, docs/adr/**, .github/workflows/**, AGENTS.md); Governed Surface Queue Guard success; 878 changed lines, under the 3,000 line class. Nothing to escalate.
  • The one defect is the tracker id in the STEP18_RATIONALE text (① above): a one-token edit on a hits path, with the remedy named. Everything else on this head is judged right, so the re-review of the next head is that fragment, the check-runs that were in_progress here, and nothing else.

Implemented-by: claude/issue-24162-retire-record-json
Reviewed-by: session_01HSDGREvqxQUt7K7pjHXMyC

VERDICT: FAIL


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a12ff6bd8835bf887ece378b577d235776e1133d
Local-runs: none
Scope: packages/spec/src/migrations/entries/retired-defs/18.ui__RecordJsonProps.ts, packages/spec/src/migrations/entries/semantic/18.ui-record-json-retired.ts, packages/spec/src/migrations/registry.ts.template, packages/spec/src/ui/component.zod.ts, packages/spec/src/ui/page.zod.ts + .github/workflows/ci.yml (the build-core job that hosts check:dual-build-cjs-loads), packages/cli/src/commands/migrate/meta.ts and packages/spec/scripts/build-upgrade-guide.ts (the two printers of rationale), packages/spec/scripts/step18-rationale-merge.test.ts, scripts/check-doc-authoring.mjs (its position list), .gitignore (packages/spec/src/migrations/registry.ts), AGENTS.md (the runtime-string rule), .claude/skills/pm-dispatch/SKILL.md and .claude/skills/pm-dispatch/references/filing-gate.md (the carrier rule), the commits 317cddd40, ed1de8c2d and f623e2f71 (provenance of the two #11509 fragments), and the rest of the diff only as the delta 26e52c407..a12ff6bd and the PR file list touch it. This is the re-review the record 6234523454 scoped: that fragment, the check-runs that were in_progress there, and nothing else; the previous record's other judgments stand and are not re-derived here.

① Derived judgments

The delta is exactly the prescribed one-token removal, and nothing else in the PR moved. git diff 26e52c407 a12ff6bd is one file, one line, +1/−1: packages/spec/src/migrations/registry.ts.template, the ui-record-json-retired fragment's first line, (#24162, ADR-0049 enforce-or-remove) → (ADR-0049 enforce-or-remove); the nine characters #24162, are deleted and no other character changes. One commit lies between the heads, a12ff6bd ("fix(spec): the record:json step-18 rationale fragment carries no tracker id"). The PR's file list is the same 23 paths with the same counts (546 insertions, 332 deletions; git diff --stat origin/main...a12ff6bd agrees file for file), the merge base is still eda6f14b7b (no rebase; main is 3 commits ahead at bd4276a91, mergeable_state: clean), and the PR is still draft. Right.

The fragment at the head passes the rule the previous record applied. The full text, read at the head (lines 3165–3178 of the template): "It also retires the record:json page element (ADR-0049 enforce-or-remove), one prerelease after it was declared: it shipped in 18.0.0-next.3 only, for the approval request page's raw JSON panel, and the maintainer dropped that panel — the person deciding an approval has no business need to read the request as JSON — so no renderer was built and no page placed it. The name leaves PageComponentType and is refused by name at the node, its ComponentPropsMap row stays as a whole-bag refusal carrying the same prescription, and the props def RecordJsonProps is unpublished. No conversion is registered: the only edit is deleting the node, a layout decision that is the author's. Its D3 record is the semantic entry ui-record-json-retired." No tracker id anywhere in it; the lesson is in the words. The parenthetical is now the one the ui-ai-chat-window-retired fragment carries at the head (line 2861, "(ADR-0049 enforce-or-remove)"), the shape the 2026-10-04 sweep f623e2f71 left the list in. The string is still printed to the author — packages/cli/src/commands/migrate/meta.ts:933 prints hop.rationale on every os migrate meta run and packages/spec/scripts/build-upgrade-guide.ts:114 prints step.rationale into the guide — so AGENTS.md's rule (runtime strings an author is shown carry no tracker number) applies, and it is now met. Id, order: 100 and the sorted position are untouched by the one-line delta, so what step18-rationale-merge.test.ts judges (a sorted, one-key-each, kebab-case list; order placement; trimmed text) is as it was at 26e52c407; the dev reports it 9/9 at the head and Test Core covers test:repo on the head. Right.

The whole template at the head carries exactly two tracker ids outside comments, and neither is this PR's. A grep of every # followed by digits in the template at a12ff6bd finds them in * and // comments throughout and in two text: strings only: line 1746 (element layer's second data door (#11509, ruling A-narrow)) and line 2336 (object-grid's defaultFilters (#11509, ruling A-narrow, …)), both pre-existing on main and outside this diff (③ below). Right.

A re-scan of every added non-comment line in the five in-scope files finds no other id. The net diff against main adds 14 lines to 18.ui__RecordJsonProps.ts (1 non-comment), 47 to 18.ui-record-json-retired.ts (29 non-comment: the surface, replacement, reason and acceptanceCriteria strings), 14 to the template (all non-comment), 19 to component.zod.ts (1 non-comment) and 30 to page.zod.ts (7 non-comment: the prescription, the .describe() and the map row). Across those 52 non-comment lines the only runs of four or more digits are ADR-0049 (twice: the template fragment and the page.zod.ts prescription); no # id, no bare issue number, no comment id. The ids #24162, #23745 and objectui#12438 appear only in // and * comments beside the entries, the map row and the enum docblock, where the rule allows them. Right.

The fix reaches the artifact it must, and no committed artifact moves. packages/spec/src/migrations/registry.ts is gitignored (.gitignore lines 68–69) and untracked at the head (git ls-tree empty), so the generated registry carries the id-free text at build and no checked-in file changes with it; Type Check · source gates (check:generated --reconcile-only) is success on the head. No gate reaches a text: field — check-doc-authoring's spec rule enumerates the guidance/guidanceSets/history/aliases/retiredForms/surface error-map options, retiredKey() tombstones and .describe() prose — so the dev's check:doc-authoring exit 0 carries no information about the fix either way, as the previous record said; the fix is evidenced by the delta itself, read in full above. Right.

② Semver level

Unchanged. The delta edits one runtime prose string, the 17 → 18 upgrade rationale printed by os migrate meta and the upgrade guide — not a comment strictly, but prose: it moves no accept set, no export, no JSON Schema, no conversion. .changeset/24162-record-json-retired.md is not in the delta and reads as judged on 26e52c407: '@objectstack/spec': minor, a feat(spec)!: title, the BREAKING banner, the FROM → TO table, adr-0087: registered ui-record-json-retired, and Clause-②: no (narrowing) at the start of a line in both the changeset and the PR body. Check Changeset is success on a12ff6bd. The .changeset/pre/23745-record-json-block.md, packages/spec/CHANGELOG.md and content/docs/releases/ remain untouched, as triage 6232511377 ruled. Right.

③ Boundary flags

  • Check-runs on a12ff6bd, read complete: 41, all completed; 36 success, 5 skipped, 0 failed, 0 pending. Every run names this head and no other. The eight that were in_progress on 26e52c407 have concluded success: Test Core (1/6) … (6/6) (completed 13:55Z–14:09Z), Type Check · debt ledger (13:45Z), Type Check · workspace (13:57Z); the two aggregators that were absent are present and success: TypeScript Type Check (13:57Z) and Test Core (14:10Z). The five required contexts are all success: Lint & Repo Gates, Build Core, Governed Surface Queue Guard, TypeScript Type Check, Test Core. The 5 skipped are the full-battery lanes that are not PR lanes — Console Pin Gate, the Dogfood matrix shards, Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance — the same five as before. Everything green on the previous head is green again on this one: a push re-runs the whole PR set, so the one-string delta sits inside a complete green run, not a partial one.
  • NOT-MEASURED check:dual-build-cjs-loads (this round's only one): hosted by Build Core — ci.yml job build-core, name: Build Core, step run: pnpm check:dual-build-cjs-loads — success on the head, completed 13:49Z. Covered and green. The two further families left NOT-MEASURED in round 1 are now concluded too: check:type-check-debt by Type Check · debt ledger, spec test:repo as a whole by Test Core (1/6)–(6/6) with the new absence walk inside the repo project — all success.
  • Deviation 1, the scoped derivation: dispatch-gates --commands was fed the one changed path (72 families: 71 run, 1 NOT-MEASURED) rather than run path-less (110), at the seat's direction. Right for a patch round: the whole-PR derivation already ran on 26e52c407 (108 run, 2 NOT-MEASURED, both since concluded green in CI), a one-string change in a template derives no family the full set did not, and the full set ran again in CI on this head (the 41 check-runs above). Nothing was left unmeasured by the narrowing.
  • Deviation 2, the PR body not rewritten: verified — the body names the fragment by id, order: 100 and sorted placement and never quotes its text (neither "It also retires" nor "spec(ui): retire record:json (#23745, 97b8e7a51d): its only consumer, the approval request page panel, was dropped by the maintainer; main already versions it into 18.0.0-next.3, which is not yet published #24162, " occurs in it), so no statement in the body became false when the token left; pr_body_replacement: not owed is right, and the seat holds the body write anyway. One label for that write: the heading "Gates (local, final head 26e52c407)" is now one commit stale — the gates it lists did run there, and the a12ff6bd round's gates are recorded on the card at 6235442527 — so when the seat next edits the body it drops the word "final" or adds the round. Not a defect of the diff, not a verdict matter.
  • Out-of-scope note, the two (#11509, ruling A-narrow) fragments: real, and the same class as the defect fixed here. They are the text: strings at template lines 1746 and 2336, authored into registry.ts by 317cddd40 (2026-10-10 10:58Z, PR feat(spec)!: an element binds data through dataSource only — retire the element-layer flat binding keys and object-grid.defaultFilters (#11509) #22421) six days after the sweep f623e2f71 (2026-10-04) and carried into the template by ed1de8c2d (build(spec): the migration registry is generated at build and leaves git #22706); the same two printers show them to every 17 → 18 upgrader, and the same gate gap (check-doc-authoring does not reach text:) lets them stand. Not touched here, rightly: the round was "change nothing else" and the previous record's scope held it out. Carrier: a card, filed by the adopting seat — not this PR, and not the record. The dev's "carrier: the seat (record 6234523454 already names it)" names nothing that carries: a review record that mentions a debt files nothing. The filing gate rules this shape in: a follow-up owed by already-landed work is filed as a card and is not pressed down as "not a card"; same-family findings go into one closing card covering every position of the family with enumerated pins, and the family opens on its second occurrence — this PR's own fragment was the first, these two are the second. The card is one, for the family: the two fragments plus the gate position (a check:doc-authoring rule for STEP18_RATIONALE text: fields, since a rule and the gate that enforces it are one change), with reach named as the os migrate meta print and the upgrade guide. Dedupe is the filer's step under the gate and was not run here (outside this review's inputs).
  • Out-of-scope note, objectui#12490 (face 4): unchanged — its domain:ui owner's call per triage; nothing in this PR depends on it. Stands.
  • Governed surfaces: none in the file list (no .claude/**, skills/**, docs/adr/**, .github/workflows/**, AGENTS.md, CLAUDE.md); Governed Surface Queue Guard success; Check PR Size success at 878 changed lines. Nothing to escalate. The record is owed by the schema face on --paths (a published schema in the tree) and by the narrowing, not by a governed path.
  • Everything else the record 6234523454 judged right stands — every door's refusal, the kept z.never row, the prescription's wording, the ratchets and baselines, the no-D2 decision, the registration keys, the flipped lint pin, the tree-scoped absence walk, the no (narrowing) spelling, the pin-file rename, the un-ridden 6142098559 item A correction, the untouched .objectui-sha pin — because the delta touches none of them and no check-run on this head contradicts any of them.
  • Dev-reported local results this round (spec build exit 0, check:generated all 14 current, step18-rationale-merge.test.ts 9/9, 71 gate families exit 0) are recorded as reported, not re-run here; the check-runs above are the gate verdicts.

The one defect of the previous record is closed by the delta, read in full, and nothing else moved.

Implemented-by: claude/issue-24162-retire-record-json
Reviewed-by: session_01HSDGREvqxQUt7K7pjHXMyC

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:spec seat 2 · huangyiirene · session session_01HSDGREvqxQUt7K7pjHXMyC · 2026-10-17T15:16Z · head a12ff6bd8835bf887ece378b577d235776e1133d

The seat's own reading, on top of the CONTRACT_REVIEW_TIER records on this PR: FAIL 6234523454 on 26e52c407, then PASS 6235567508 on this head. Those records cover the published-schema face (page.zod.ts, component.zod.ts, the two migration entries and the registry template). The rest of the diff is read here.

item reading
shape Draft, base main, head repo = base repo, mergeable_state clean. First line Fixes #24162, and that is the body's only closing keyword next to an issue number. Line-start Clause-②: no (narrowing) in the body and the changeset. The claim 6232774903 was aligned to it after record 6234523454 judged that spelling the right one.
file list 23 files, +546 / −332. Outside the contract face: the changeset; the five generated packages/spec/*/ui.json shards; three regenerated content/docs/references pages; the strictness counts shard; six spec pins and vitest.repo-tests.json; the flipped lint pin. No content/docs/releases/. .changeset/pre/23745-record-json-block.md and packages/spec/CHANGELOG.md are untouched, as triage 6232511377 ruled. No skills/**.
premise Triage 6232511377 holds on the merge base: record:json had rows in page.zod.ts and component.zod.ts, and objectui at the console pin had 0 hits. Record 6234523454 re-measured the pin 5e15928b46.
changeset '@objectstack/spec': minor, a feat(spec)!: title, a BREAKING banner, Clause-②: no (narrowing), and adr-0087: registered ui-record-json-retired. Sentences checked against the diff: what is refused (an authored node, at PageComponentSchema.type, plus the enum's error map and the kept ComponentPropsMap row); what is removed (RecordJsonProps and its JSON Schema, and the print-classification row); what stays accepted (every other member, and the open string arm). All are true of the diff.
gates Check-runs on the head: 41, all completed: 36 success, 5 skipped, 0 failures. check-expected-skips.mjs --pr 24170: "5 skipped check-run(s), every one in the roster (exit 0)". The dev's derived families: round 0 had 110 (108 run, 2 NOT-MEASURED, both hosted green by CI); round 1 had 72 (71 run, 1 NOT-MEASURED, check:dual-build-cjs-loads, hosted by Build Core, success).
governed / size check-governed-merges.mjs --pr 24170: 0 of 23 paths governed, and 878 changed lines, under 3000. Ordinary queue landing.
deviations accepted The Clause-② spelling (judged right; the claim was aligned). The pin rename to component-record-json-retirement.test.ts (house name). The lint pin flipped, not deleted (it covers the props rule and the pipeline). Round 1's gate derivation was scoped to the one changed path, on the seat's direction. The PR body was not rewritten, because it never quotes the fragment's text. Its heading "Gates (local, final head 26e52c407)" is one commit stale; the a12ff6bd8 round's gates are on the card at 6235442527.
out of scope The two (#11509, ruling A-narrow) STEP18_RATIONALE fragments (from 317cddd40) are the same kind of debt as the fixed one, a tracker id in a runtime string. Carrier: #24179, filed by this seat in this act. It is not this PR: the round was "change nothing else". objectui#12490 (face 4) stays its domain:ui owner's call, per triage. The carried 6142098559 item A does not ride: conversions/registry.ts is not edited.

Landing: pr_ready, then automerge_enable, through the relay in the write after this comment. The landing record follows the merge.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants