feat(spec)!: page requires only on the compiled kinds — refused at parse on react, full and slotted pages (#21459) - #21547
Conversation
…, D3 entry, ledger rows (#21459) Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…rence for the compiled-kind requires check Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
… conversion of page requires on non-compiled kinds; add the changeset Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…ge-requires-compiled-kinds
…rder the requires rationale fragment after the ai:chat_window one that landed first Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…ge-requires-compiled-kinds
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3441b4074b64caee7cd728c69c3040c759c3e864 && git checkout 3441b4074b64caee7cd728c69c3040c759c3e864
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 41b13331cdf096c8a940e1430bd535c67049cea5 e38149267c16b962c1298b4b76a9b8b7137b7640 && git checkout -B drift-repro 41b13331cdf096c8a940e1430bd535c67049cea5 && git merge --no-ff e38149267c16b962c1298b4b76a9b8b7137b7640
node scripts/docs-audit/affected-docs.mjs --json 41b13331cdf096c8a940e1430bd535c67049cea5
|
Contract reviewServed-tier: Inputs read: card #21459 (body; comments ① Derived judgmentsEach accept-set or public-surface change the diff implies, judged against ruling A and the refs.
② Semver level
③ Boundary flagsDev
Dev
Check-runs on the head, read as they stand at 2026-10-03T05:33Z: 20 completed success, 2 completed skipped (Console Pin Gate and Packed-tarball smoke, both opt-in), 0 failed, 10 in progress: Lint & Repo Gates; Test Core (1/6) through (5/6); Type Check · consumer gates; Type Check · workspace; Dogfood Regression Gate (1/3) and (2/3). Concluded and relied on above: Type Check · source gates, Check Changeset, Spec property liveness, Build Docs, Check Documentation Links, Governed Surface Queue Guard, Build Core, Test Core (6/6), Dogfood Regression Gate (3/3), Dogfood Verify CLI, Temporal Conformance, Type Check · debt ledger, Check PR Size, and the four claim and path guards. An in-progress gate is not a pass: this record judges the contract, and the merge waits on those ten as it would regardless. Reads carrying no verdict: the PR is a draft; its Implemented-by: VERDICT: PASS Generated by Claude Code |
ACCEPT — PR #21547 @
|
Fixes #21459
Clause-②: yes (narrowing)
Summary
This PR executes ruling A (record
5964312254).PageSchemanow acceptsrequiresonly whenkindishtmlorjsx, the two kinds the metadata save door compiles. On areact,fullorslottedpage it is refused at parse, and so is a page that omitskind, which defaults tofull. The refusal names the key, the page's kind and the compiled kinds.checkPageRequiresKind(packages/spec/src/ui/page.zod.ts). It is an exported object-level check chained ontoPageSchemawith.superRefine(...), right aftercheckPageSourceCompleteness. That is the mechanism this file already uses for kind-conditional rules, so no second one is added. Its vocabulary is the new exportCOMPILED_PAGE_KINDS = ['html', 'jsx']. The issue iscode: 'custom'atpath: ['requires']. The message opens with "requiresis refused on akind: 'react'page" (or the page's kind), nameshtmland its deprecated aliasjsx, says "Delete the key.", and ends with the houseos migrate meta --from 17sentence. It carries no tracker number.page-requires-non-compiled-kind-removed(step 18,order: 58,retiredFromLoadPath: true,retiredAfter: '17.6.0'). It strips the key fromreact,full,slottedand kind-less pages.page-requires-non-compiled-kind-refused, which carriesconversionIds: [page-requires-non-compiled-kind-removed].STEP18_RATIONALEfragment (order: 66, placed afterui-ai-chat-window-retired, which landed first at 65). The registry regions were regenerated withgen:migration-registry.RETIRED_KEYS_BY_MAJORrow: the key stays live on html pages.liveness/page.json: therequiresrow stayslive. Its evidence gains the PARSE reader, its note's per-kind clause shrinks to the compiled kinds, andverifiedAtis now 2026-10-03. The state counts do not move.metadata-form-zod-reconciliation.test.ts: its per-kind clause now reads "on every other kind the parse refuses it".api-surface/ui.jsonandexport-origins/ui.jsongain the two exports, andcontent/docs/references/ui/page.mdxre-renders the describe. All three were produced bycheck:generated --fix, never edited by hand.@objectstack/specminorwith the BREAKING banner,Clause-②: yes (narrowing), a FROM → TO table, and the ADR-0087 markerregistered page-requires-non-compiled-kind-removed, page-requires-non-compiled-kind-refused.No runtime code changes. The save door, the load report and objectui are untouched.
Premise checks (at base
c98a72d69e, re-read on the merged tree)requires:hit inexamples/**,packages/apps/**(none there),content/docs/**andskills/**is the stack-level capability list. In examples that isapp-crm,app-showcaseandapp-todo'sobjectstack.config.ts, plus prose comments naming capability tokens. The only page bodies that carryrequiresare the 11htmlPage(...)sites inpackages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, allkind: 'html', so they are still accepted. The proximity scan (eachrequires:hit within 15 lines of a page marker) found them, which is its positive control. No test pinned acceptance on a non-compiled kind. objectui at the.objectui-shapin89cad75d55shows pagerequiresonly on stamped html fixtures, plus one compile-only type fixture (twins-spec-by-reference-9736.test.ts:158). The ruling's cloud and hotcrm census stands.kindparses asfull, and the refinement runs on the parsed value. So{ requires: [...] }with nokindis refused as afullpage, and the message adds "(fullis also the kind of a page that omitskind)". A.shapemirror without the default reaches the check withkindabsent and gets the same issue, which the exports-parity fixture pins.[]:compileHtmlPagereturnsundefined, sofindHtmlPageSourceGapsanswersnullandstampHtmlPageRequiresreturns the body as written. The load report answers[]for it, which reports nothing. So[]had no effect anywhere. The ruling's words refuse the key ("acceptsrequiresonly whenkindishtmlorjsx"), not its contents, so[]is refused too, and the conversion strips it too. Both are pinned.A4: the stored-row disposition is a mechanical drop, so it has a D2 conversion
The drop is lossless. On those kinds nothing derived the list, no renderer read it, and the Studio page editor already drops it on every save. Its one reader was the load report's warning. With the conversion:
metadata_spec_invalidwarning and a_diagnosticsbadge at every boot;os migrate meta --from 17lists the edit.This is pinned at the real seam:
loadMetaFromDbover a seeded stored react page carryingrequiresgivesloaded: 1, errors: 0, invalid: 0, one notice naming the conversion, no[page_requires_plugin_absent]line (the manifest lacks the plugin, so an unconverted list would have been reported) and no[metadata_spec_invalid]line.A6: reader branches the parse boundary now makes unreachable for non-compiled kinds (none changed here)
packages/metadata-protocol/src/runtime-authoring-gate.ts:616-627,findPageRequiresAbsentFromManifest, which is "Kind-agnostic on purpose" (TSDoc at:610). A non-compiled page can no longer reach it: the save door refuses the key, and at load the stored-row conversion strips it beforereportPageRequiresAbsentAtLoad(protocol.ts:24418, called on the converted body) reads it. The kind-agnostic reach now serves only html / jsx rows, and its TSDoc sentence is a follow-up candidate.packages/metadata-protocol/src/protocol.ts:24526, thereportPageRequiresAbsentAtLoadTSDoc "How a stored page gets here". It is still true, but now only of html rows.runtime-authoring-gate.ts:679and:728were already gated to html / jsx bycompileHtmlPage(:634). No change.builtinComponents.tsxpageSaveBody(at the pin) deletesrequireson every kind. On non-compiled kinds that is now redundant, and harmless.No test pinned the old acceptance, so no existing test changed meaning.
A9: merge state
PR #21531 landed as
48eb9c193f. This branch mergedorigin/maintwice throughscripts/pm/os-regen-merge.sh:dafb0f6d1eafter #21531, ande38149267cat49161683fb.ui/page.zod.tsandmigrations/registry.tsmerged textually clean. Both rationale fragments are kept (ui-ai-chat-window-retired65, this one 66), and the regenerated regions matchcheck:migration-registry.os-regenartifacts both sides touched were regenerated on the merged tree, and the delta against main is exactly the two exports and the describe row.'ui-ai-chat-window-retired'(3),'ui/AIChatWindowProps'(2) and'ai:chat_window'(17).ui/page.zod.ts,migrations/registry.ts,conversions/registry.ts,liveness/page.jsonor the reconciliation test.49161683fb, main gained1ac7308d7aand41b13331cd. Neither touches a file in this diff.Tests (all at head
e38149267cunless noted)@objectstack/spectest (vitest run --project local): 604 files, 17888 passed, 1 todo. Run atd7cd797549; the later merge moved nothing underpackages/spec.@objectstack/spectypecheck: exit 0. That coverstsc --noEmit,check:scripts-typecheckandcheck:test-typecheck(52 files / 246 errors held, unchanged).@objectstack/metadata-protocoltest: 205 files passed, 3 skipped (3157 tests passed). Typecheck exit 0, and--listFilesincludesprotocol.runtime-authoring-gate.test.ts.packages/spec/src/ui/page-requires-compiled-kinds.test.ts: the refusal onreact,fullandslotted, checked for code, path and named subjects; the omitted-kind default; the empty array; html / jsx controls; norequireson every kind; the stack door envelope (STACK_SCHEMA_INVALID, 422, atpages.1.requires); and the conversion. The conversion pins cover the stored-row strip with a notice, html / jsx kept, strip-iff-refused over the whole kind vocabulary, unknown kind left alone, artifact replay, idempotence and retired-from-load-path. The file also pins the ledger wiring and that there is no tombstone, withui/Page:assignedProfilesas the control.object-refinement-check-exports.test.ts: the new export is catalogued with 8 fixtures, and the parity, bijection, attachment-by-identifier and barrel-identity legs hold.protocol.runtime-authoring-gate.test.ts: the save door refusesrequireson areact,full,slottedor kind-less page with{ code: 'INVALID_METADATA', status: 422 }, onecustomissue atrequires, nojsx-*compile finding, and nothing persisted. The html control still saves and stamps. The load pin is described above.scripts/ablation-replace.mjs, run under the verify lock. The mutation replaces the kind conditionif ((COMPILED_PAGE_KINDS as readonly string[]).includes(kind)) return;with a barereturn;, so the key is accepted on every kind again. The source-resolved spec suites need no rebuild.6b13a7df8e44→1fd2575e28e3, marker on disk 1.6b13a7df8e44) andgit diff HEADis empty. Green leg: 187 passed.Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 116 commands ate38149267c. All 116 were run and their exit codes recorded.--ranreports: "116 derived famil(ies) accounted for — 115 run, 1 NOT-MEASURED". The NOT-MEASURED one ispnpm check:dual-build-cjs-loads(exit 3, PREREQUISITE NOT MET: it needs every package'sdist/, a repo-wide build), and it is declared to CI.check:skill-examplesandcheck:lean-entry-closurefirst exited 3 for missingclient-react/objectqlbuilds. After building those closures they re-ran to exit 0. Highlights, each exit 0:check:generatedreports "all up to date" for 15 artifacts;check:liveness,check:migration-registry,check:spec-changes,check:upgrade-guide,check:api-surface,check:export-origins,check:docs;check:adr-0087-registration,check-changeset-no-major(also run with this body as the--eventpayload),check-empty-changeset;check:doc-authoring,check:nul-bytes,check:cross-package-test-inputs,check:engine-double-contract,check:type-check-debt.A narrowed eslint run (
--no-inline-config --format json) covered the 8 changed.tsfiles and found 0 errors and 0 warnings. This repo's eslint config enables no type-aware linting, so the diff cannot move a verdict on an untouched file.Acceptance notes
spec-object-refinements-7715.test.tscensus (at the pin) countsPageSchema's object-level checks and listsattached: ['checkPageSourceCompleteness']forPageNodeSchema. It is built to go red when the spec adds a check. At objectui's next@objectstack/specbump, that row will ask forcheckPageRequiresKindto be attached, or declared not attachable. Runtime behaviour needs no objectui change, as the ruling says, but that census will need one row. The Console Pin Gate only builds objectui, and nothing objectui compiles against changed type, so it is unaffected.examples/app-showcaseand every other example author no page-levelrequires, so no example changed.Generated by Claude Code