Skip to content

fix(spec,lint): page requires is live — refused at save, reported at load - #21451

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20871-page-requires-live
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20871-page-requires-live

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20871
Clause-②: no

Summary

This is the spec half of #20312 stage ③. The engine half landed first:

This PR makes the spec say what those landings made true. No runtime code changes.

  • packages/spec/liveness/page.json: the requires row moves from planned to live. It carries verifiedAt: 2026-10-02 and evidenceScope: in-repo. Its evidence names the save door, the promotion re-stamp and the load report, each as file#symbol. Its producer names the host that supplies the second input, the deployment's SDUI component manifest. The liveness README's producer table asks for one, because the reader compares the authored value against something a caller supplies.
  • packages/spec/src/ui/page.zod.ts: the requires describe used to say "(validated at save and load)", while the ledger said "declared, not enforced yet". The describe and its TSDoc now state what happens:
    • At save, on a server that has the deployment's SDUI component manifest, a kind: 'html' page's source is compiled (alias 'jsx' too). A written list that disagrees with the source is refused (422 INVALID_METADATA, page-requires-disagrees-with-source). A draft keeps the list until its publish, which refuses it. The derived list is stored.
    • At load, a stored page whose list names a plugin no manifest component carries is reported, and it is still served.
    • A server with no manifest checks neither, and says so once at boot.
  • packages/lint/src/authoring-rules.ts: validateJsxPages no longer shares the RUNTIME_HEAVY_SOURCE_PARSE reason ("parses authored source through typescript/sucrase"). It gets its own reason, RUNTIME_HTML_SOURCE_COMPILED_AT_SAVE. That constant's TSDoc no longer lists jsx page bodies. validateReactPages keeps the old reason, which is true for it (Sucrase).
  • ADR-0087 guide entry: the reason of 18.ui-html-page-div-refused.ts now names the runtime save door. migrations/registry.ts was regenerated with gen:migration-registry, never by hand. The existing entry is amended rather than a new D3 entry added. Step 18 is unreleased (@objectstack/spec is at 17.6.0), the entries README makes an entry file the unit of edit, and ace770d5fc amended this same entry's reason the same way.
  • Docs: the only "validated at save and load" sentence under content/docs/** was the requires row of content/docs/references/ui/page.mdx. That tree is AUTO-GEN, rendered from the describe, so it was regenerated rather than hand-edited. It now matches the describe, and check:docs holds the two equal, so this PR adds no separate grep pin. The hand-written content/docs/ui/pages.mdx has no requires row and no such sentence.
  • Counts: liveness/state-counts/page.md was regenerated. page goes from 22 live and 1 planned to 23 live and 0 planned (24 classified).
  • Changeset: patch for @objectstack/spec and @objectstack/lint, with Clause-②: no. No accept set moves.

Declared deviation from the claimed file surface

packages/spec/liveness/README.md also changed: the page row of the hand-written state table. Its Notes cell said "live + one planned", which this PR makes false. It now records the flip. check:liveness holds the row set and the counts, but never a Notes cell's text.

Premise checks

  • A1, positions at ceb4a939b4, all confirmed:

    • liveness/page.json:9 was planned, with the note "save/load enforcement of plugin presence is deferred (M3b)".
    • page.zod.ts:903 was the requires line.
    • authoring-rules.ts:450-:451 held the "typescript/sucrase" reason. validateJsxPages used it at :1108 and validateReactPages at :1122.
    • The guide entry was migrations/entries/semantic/18.ui-html-page-div-refused.ts.
  • A2, is the authored value read, or only overwritten? It is read, and refused when it disagrees. The two metadata: refuse to save or load a page whose requires names a plugin that is not loaded (1 key) #20312 blocks of packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts (-t 20312) give 17 passed and 39 skipped. They include the case "refuses a hand-written requires that disagrees with the source, naming each namespace". That case pins { code: 'INVALID_METADATA', status: 422 } for three shapes:

    • an unused namespace;
    • a namespace no manifest component carries;
    • a used namespace left unlisted.

    So authoring the key changes runtime behaviour, which is the README's definition of live.

  • A3, what validateJsxPages parses with. packages/lint/src/validate-jsx-pages.ts imports parseJsx and compile from @objectstack/sdui-parser, whose package.json declares no dependencies. @objectstack/metadata-protocol's runtime-authoring-gate.ts imports the same compile statically, so the kernel already loads it. The rule stays off the runtime surface for a different reason: the save door runs the same compile itself (findHtmlPageSourceGaps), under the same jsx-CODE rule ids. The new reason says that.

  • A4, the guide entry's new prose, checked against main:

    • os serve (which dev and start spawn) resolves the manifest from beside the served config, then from the console's copy (registerDeploymentSduiManifest);
    • the save door compiles html source against it on every publish;
    • a draft is judged at its publish;
    • a host with no manifest prints one boot line and stores pages unjudged;
    • rows at rest are not recompiled at load.
  • A5, the docs. See Summary. Studio's round trip of a stale stamp answering 422 is exactly what the new sentence describes (a written list that disagrees is refused), so the docs do not name it. objectui#11357 is closed.

The readers and the producer (A2)

moment role file#symbol
save judges the authored list packages/metadata-protocol/src/runtime-authoring-gate.ts#findHtmlPageSourceGaps
save stores the derived list packages/metadata-protocol/src/runtime-authoring-gate.ts#stampHtmlPageRequires
draft promotion re-stamps the promoted body packages/metadata-protocol/src/protocol.ts#promoteDraftForPublish (deriveActiveBody)
load reports an absent plugin packages/metadata-protocol/src/protocol.ts#reportPageRequiresAbsentAtLoad, called from loadMetaFromDb, judged by runtime-authoring-gate.ts#findPageRequiresAbsentFromManifest
producer supplies the manifest packages/cli/src/utils/sdui-manifest.ts#registerDeploymentSduiManifest, called from packages/cli/src/commands/serve.ts and read per publish and at load through protocol.ts#resolveSduiManifest

The ledger gate reads the row. As a one-shot ablation through scripts/ablation-replace.mjs, the evidence path runtime-authoring-gate.ts#findHtmlPageSourceGaps was rewritten to a file that does not exist.

  • check:liveness went red: "1 'live' / 'planned' / 'experimental' / 'live-elsewhere' entr(ies) cite a file that is missing from THIS repo: page/requires".
  • The same run reports "854 pointer(s) written path#symbol, 854 naming a symbol the cited file contains", so the cited symbols are held as well as the paths.
  • The restore was verified: blob a866b58134 equals HEAD, and git diff HEAD is empty.

Verification at 3e1f0dabff

This run resumed one that was lost to a container restart. Nothing from before the restart is cited. origin/main was merged through scripts/pm/os-regen-merge.sh (merge 3e1f0dabff). registry.ts is not driver-routed, and both sides survived the text merge: this branch's step 18 text, and main's new dashboard-widget-single-series-multi-measure-refused entry. Every reading below is at 3e1f0dabff.

  • Build. turbo run build --filter='./packages/**': 71 of 71 tasks successful. The tree was clean afterwards.
  • @objectstack/spec:
    • build: exit 0.
    • check:generated: exit 0, "All 15 generated artifacts are up to date".
    • check:liveness: exit 0, "packages/spec/liveness/state-counts/ is current".
    • test (vitest run --project local, two shards): 300 files, 9053 passed and 1 todo; then 300 files, 8631 passed. Both exit 0.
    • typecheck: exit 0.
  • @objectstack/lint: test gives 119 files and 5585 passed, exit 0. typecheck exits 0.
  • Derived gates. node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths) derived 110 commands. All 110 ran, each exit code written to disk before any reading, and all exited 0. --ran reconciles them: "110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN".
    • On the first pass, two were infrastructure non-measurements, not reds, and both were re-run green.
    • check-adr-0087-registration --self-test could not write its fixture commits: the container's commit-signing server answered 503. On re-run: "441 assertions".
    • check:query-options-erasure hit the per-command 300s cap on a contended box. On re-run it exited 0 in 491s: "ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new".
  • Named gates, with their own verdict lines:
    • pnpm check:adr-0087-registration: "this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)".
    • pnpm check:empty-changeset: "No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)".
    • check-changeset-no-major --base origin/main: "This diff introduces no major bump". Driven offline against this body (--event): "LEVEL AXIS: this PR declares clause-② no, so no package here is declared to have grown a published surface".
    • check-changeset-fixed: the .changeset/config.json "fixed" group "is in sync with 69 public workspace packages".
    • pnpm check:doc-authoring: "17283 customer-facing string(s) across 1234 spec sources clean".
    • pnpm check:nul-bytes: "OK (scanned 9771 text file(s) ... no raw ASCII control bytes)".
    • Roster gates with a roster under these paths are all exit 0: check:meta-url-spelling, check:authz-resolver, check:error-code-casing, check:filter-alias-parity.
  • Lint, narrowed and declared. pnpm lint is run by CI. Here:
    • Population: eslint.config.mjs lints **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}. Of the 9 changed files, exactly the 4 .ts files are in it.
    • Count: eslint --no-inline-config --format json over those 4 files gives 4 results, 0 errors and 0 warnings.
    • Invariance: the config never enables type-aware linting (no parserOptions.project, no projectService), so this diff cannot move the verdict on any untouched file.
  • Mergeability. main moved after the merge. A local git merge-tree --write-tree HEAD origin/main at 53fd35e3e3 is clean. None of this diff's driver-routed paths changed on main, so GitHub sees the same answer. CI judges the merge ref.

Acceptance notes

  • packages/lint/src/runtime-lazy-deps.test.ts's header says "The two rules that need them stay CLI-only (RUNTIME_HEAVY_SOURCE_PARSE)". After this PR, one registry rule (validateReactPages) carries that constant. This is test prose, not a published surface, and it is not edited here. Carrier: none.
  • The no-manifest boot line in packages/cli/src/utils/sdui-manifest.ts says "Page source and requires not validated at save". That host skips the load report too, so the line could say "at save or load". It is not false, it is in a domain:cli file pinned by the CLI's tests, and it stays out of scope here. Carrier: none.
  • A host with no manifest has its save door judge nothing, while validateJsxPages still checks syntax and structure without a manifest. The new reason's TSDoc records this. The host announces it at boot, so it is not a finding.
  • skills/**: zero hits for a page requires sentence or "validated at save and load". Nothing to list.
  • Review fix round: the reconciliation-ledger root omit row for page / requires (packages/spec/src/system/metadata-form-zod-reconciliation.test.ts) said "declared, not enforced yet", which this PR makes false; it is re-ledgered under "platform-written, never authored" on the schema's own words with the measured truth per page kind, and no form offer, per seat answer 5959584348 (commit 54c73b11ff).

Generated by Claude Code

claude added 5 commits October 2, 2026 14:27
… load

The liveness row flips planned to live, citing the save door and the
load report; the describe states what happens at save and load; the
validateJsxPages surface reason no longer names typescript/sucrase; the
ui-html-page-div-refused guide entry names the runtime save door.

Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM
Co-authored-by: Claude <noreply@anthropic.com>
…ge-requires-live

# Conflicts:
#	packages/spec/liveness/README.md
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation protocol:ui tooling labels Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 3 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/liveness/README.md, packages/spec/liveness/page.json, packages/spec/liveness/state-counts/page.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/protocol/objectui/layout-dsl.mdx (via PageSchema (symbol, a top-level const))
What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/liveness/README.md, packages/spec/liveness/page.json, packages/spec/liveness/state-counts/page.md) — pages documenting those are invisible to this run
  • 2 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 — 138 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 b94a2a7277857d1bad1153e0ce5b34d9d89d50b5 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 b94a2a7277857d1bad1153e0ce5b34d9d89d50b5 → 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: 3e1f0dabffff3c856aea5968391a41bf26e05ea8
Local-runs: none

Inputs read: card #20871 (body; comments 5928431965 unlock, 5954183577 claim, 5959084718 dev report), PR #21451 (body, 9-file list, net diff against main, one bot comment), the 32 check-runs on the head, and the head tree through git show (metadata-protocol, cli, lint, spec liveness scripts, lint.yml, .gitattributes). Nothing built, run or re-run.

① Derived judgments

Accept set — unchanged, judged RIGHT. requires: z.array(z.string()).optional() (page.zod.ts:912) is byte-identical; only its .describe() and TSDoc move. check:authorable-surface and check:docs both sit in Type Check · source gates = success on this head, and the regenerated content/docs/references/ui/page.mdx row is the describe verbatim. No key enters or leaves any schema; the changeset's "No schema accepts or refuses anything it did not before" holds.

Public surface — no export movement, judged RIGHT. RUNTIME_HTML_SOURCE_COMPILED_AT_SAVE is a module-private const. The one published value that changes is AUTHORING_RULES[validateJsxPages].surfaceReason (a string on a shipped registry); surfaces: CLI_ONLY is unchanged, so runtime-lazy-deps.test.ts:403 (the rule stays off the kernel-safe entry) is untouched. check:api-surface sits in Type Check · consumer gates = in_progress: read as not concluded, not as a pass; the diff adds, removes and renames no export, so no movement is expected.

Liveness verdict planned → live, judged RIGHT against the README's definition (liveness/README.md:180: "authoring the property changes runtime behaviour"). At this head:

  • Save judge: runtime-authoring-gate.ts#findHtmlPageSourceGaps (:657) reads body.requires of a kind:'html'/'jsx' page compiled against a usable manifest and, when the written list disagrees (unprovided, unused, missing, or non-string entries), emits an error finding under page-requires-disagrees-with-source (:559) — the refusal. The gate receives the manifest per publish (protocol.ts:5501). So authoring the key changes behaviour: a disagreeing list is refused, not overwritten.
  • Save stamp: #stampHtmlPageRequires (:725) at protocol.ts:17584; a disagreeing or non-compiling body is returned as written so the draft's publish refuses it (:712-724, :17576-17583).
  • Promotion: protocol.ts#promoteDraftForPublish passes deriveActiveBody as a closure that calls stampHtmlPageRequires on the draft body against the manifest read now (:19066).
  • Load report: protocol.ts#reportPageRequiresAbsentAtLoad (:23784) is called at :23667 inside boot hydration, only after hydrated && normalizedType === 'page', judged by #findPageRequiresAbsentFromManifest (:616, kind-agnostic); it warns [page_requires_plugin_absent] and never refuses.
  • Producer: packages/cli/src/utils/sdui-manifest.ts#registerDeploymentSduiManifest, called from serve.ts:3099-3100; resolveSduiManifest (:220-229) looks beside the served config first, then at the console's copy; dev/start spawn serve (dev.ts:22-27, start.ts:148-154); protocol.ts#resolveSduiManifest (:5118) reads SDUI_MANIFEST_SERVICE on each of the three moments. The README's producer table (:227-232) requires a producer exactly here — the consumer compares the authored value against something a caller supplies — and the row cites one.
  • evidenceScope: in-repo is right (every reader and the producer are in this repo); verifiedAt: 2026-10-02 is well-formed; the Spec property liveness check-run (= check:liveness + check:empty-state, spec-liveness-check.yml) is success, so the path#symbol anchors of evidence and producer resolved. The note's boundaries match the code: COMPILED_PAGE_KINDS excludes react, drafts are judged at publish, stored rows are reported and not rewritten (:23662-23665).
  • The describe's clauses each hold: 422 INVALID_METADATA under that rule id (the gate's status; the metadata: refuse to save or load a page whose requires names a plugin that is not loaded (1 key) #20312 tests pin it per the report, and metadata-protocol's CHANGELOG :272 records the same contract); "kept until the draft's publish, which refuses it"; "reported, page and plugin named, and is still served" (:23790-23797); "no manifest checks neither and says so once at boot" (compileHtmlPage and findPageRequiresAbsentFromManifest both bail on an unusable manifest; the boot line is registerDeploymentSduiManifest's return, sdui-manifest.ts:237-245).

Lint reason, judged RIGHT. @objectstack/sdui-parser declares dependencies: null; validate-jsx-pages.ts:62 runs compile(source, manifest) with a manifest and parseJsx(source) without one, which is the TSDoc's "with no manifest this rule still checks syntax and structure"; the save door reports under jsx-${d.code} (:674) and adds the requires check — the stated reason for not wiring the rule twice. validateReactPages (:1152) is now the only carrier of RUNTIME_HEAVY_SOURCE_PARSE, and that reason stays true for it.

ADR-0087 entry text, judged RIGHT sentence by sentence (project manifest then console copy; dev/start run serve; compile on every publish; 422 under the same rule ids; a draft stored as written and refused at publish; one boot line with no manifest; stored pages not rewritten). The registry.ts hunk is the entry's reason string byte-for-byte after indentation (diffed), and id: 'ui-html-page-div-refused' occurs once.

Docs, judged RIGHT. The only "validated at save and load" sentence under content/docs/** was the AUTO-GEN row, regenerated; content/docs/ui/pages.mdx and the drift-flagged content/docs/protocol/objectui/layout-dsl.mdx carry no page requires sentence at this head; skills/** hits are the stack-level requires capability key, a different key. The card's conditional grep pin is not owed: check:docs holds the row equal to the describe.

Counts, judged RIGHT. page 22/1 → 23/0 of 24; state-counts/page.md regenerated; readme-table.mts:72 confirms the Notes cell's content is deliberately not checked, so the README cell edit is a hand edit no gate replaces.

One derived statement the diff makes false and leaves standing — judged WRONG, the FAIL item. packages/spec/src/system/metadata-form-zod-reconciliation.test.ts:402-407, the omit row for type: 'page', key: 'requires', reads: "declared, not enforced yet — liveness verdict planned (ADR-0080: inferred at compile time; save/load enforcement of plugin presence is deferred). No offer until it is enforced; whether to offer it then is a ruling for the enforcement, not for this gate". After this diff the verdict is live and the enforcement is recorded, so both premises are false. The file's own governing comment (:306-309) prescribes the action on exactly this flip: "The not-enforced-yet rows hold only while the verdict does. Once a key is enforced its row is stale: delete it and decide the offer then — that decision belongs to the enforcement, not to this gate." The test checks the row for non-vacuity and resolution, not for truth, so it stays green over the stale row — which is why it has to be read. The dev's own reasoning for the README Notes cell ("the flip makes it false") applies identically here, and this row is in packages/spec/src/**, the surface this review is owed on. Not declared, not in the acceptance notes, not in open_questions. The fix is one row: re-ledger page.requires under a reason that holds (the describe's own words — derived from the source at save, omit it; a control would offer the written list the door refuses when it disagrees — the "platform-written, never authored" family already in that ledger), or delete the row and record the offer decision the comment calls for.

② Semver level

patch for @objectstack/spec and @objectstack/lint, Clause-②: no — judged RIGHT. What the diff publishes: in spec, a liveness row, a .describe()/TSDoc text and the reason text of an unreleased step-18 entry (and its generated mirror); in lint, one rule's surfaceReason string and TSDoc with surfaces unchanged. No accept set moves, no export moves, no published surface grows, so no is the correct arm and minor is not owed; the pair carries no (widening)/(narrowing) arm, so it is well-formed. Non-breaking, so no ADR-0087 disposition marker is due. The changeset body carries no tracker number and no model identifier; Clause-②: no is at a line start in both the changeset and the PR body. Check Changeset = success. skip-changeset is correctly absent: both packages publish.

Step 18 is unreleased, confirmed: @objectstack/spec is at 17.6.0; step18 is the newest of the two steps in registry.ts; ui-html-page-div-refused appears 0 times in packages/spec/spec-changes.json and 0 times in docs/protocol-upgrade-guide.md, so the entry is not yet projected, and check:spec-changes / check:upgrade-guide are green in Type Check · source gates. Amending its reason is a pre-release text edit, a patch.

③ Boundary flags

  1. README ledger-row edit outside the claim surface — declared in the PR body and the report. Accepted: the page Notes cell said "live + one planned", which the flip falsifies; the cell's content is not gate-held, the row's numbers stay generated (state-counts/), and the new text names the date, the card and the mechanism. Right to take it in this PR.
  2. Amended unreleased step-18 entry + regenerated registry.ts — accepted. Unreleased and unprojected (② above); the entries README makes the entry file the unit of edit and gen:migration-registry the only writer; the registry text equals the entry text byte-for-byte; the cited precedent ace770d5fc did amend this same entry's reason with a regenerated registry (verified: 3 files, entry + registry + changeset). Note the CI verdict on the regeneration itself, check:migration-registry, runs in Lint & Repo Gates = in_progress — pending, not passed; the textual identity above is a read, not that gate.
  3. Head opened behind main; migrations/registry.ts moved on main — accepted. Measured: merge-base c2c21f357c; origin/main is now 7 commits ahead (086ad0aa68), and the only file both sides touch is registry.ts. PR feat(spec)!: the analytics row wildcard '*' is admitted only where a count consumes it (#21409) #21431 inserted analytics-row-wildcard-outside-count-refused into step 18's semantic list near registry.ts:7023; this PR amends ui-html-page-div-refused near :18860 — far apart, non-adjacent. GitHub answers mergeable: true. registry.ts is not merge=os-regen-routed (.gitattributes routes only its projections), so no deferral is owed, and the entries README's measured table (spec-changes.json and protocol-upgrade-guide.md are unsharded generated artifacts whose merge safety rests on a LOCAL-only git driver — the merge queue rebuilds server-side, where no custom driver runs #8344) says a driver-less text merge of two non-adjacent registrations is byte-identical to the regeneration, with the --check gates proving it on the queue's rebuilt generation. None of the seven main commits touches authoring-rules.ts, page.zod.ts, page.json or the docs row. No joint-breakage risk visible; the queue re-runs the required set on the merged generation. A git merge origin/main before arming is hygiene, not owed.
  4. Dev deviations — labels (zero writes; the labeler's set), footer form (session-URL form in the PR body, per AGENTS.md), sharded spec test run, resumed run after a container restart: each answered, none bears on the diff.
  5. Acceptance notes — runtime-lazy-deps.test.ts:21 header "two rules" (now one): test prose, agreed, carrier none. CLI no-manifest boot line "not validated at save" (also skips the load report): not false, domain:cli, agreed. No-manifest host judges nothing at save while validateJsxPages still parses: recorded in the new TSDoc, agreed. The reconciliation-ledger row in ① is the one consequence of the flip these notes missed.
  6. open_questions — empty; nothing to answer.
  7. Check-runs on this head, as they stand: success — Spec property liveness, Type Check · source gates, Governed Surface Queue Guard, Check Changeset, Check PR Size, Build Docs, Check Documentation Links, Flag docs affected by code changes, the three card/branch/path guards, Auto Label, filter; skipped — Console Pin Gate (no removal), Packed-tarball smoke; in_progress — Lint & Repo Gates (carries pnpm lint and check:migration-registry), Test Core 1-6, Dogfood Regression Gate 1-3, Dogfood Verify CLI, Build Core, Temporal Conformance (live PG + MySQL), Type Check · workspace/debt ledger/consumer gates; not yet created — the TypeScript Type Check aggregator (needs those four). Five of the seven required contexts are not concluded. An in_progress gate is an honest reading, not a pass; no governed surface is in the file list, so no tier record is owed for landing.

Verdict basis. The derived judgments, the liveness flip, the semver arm and the three named boundaries are all right at this head. One ledgered statement in packages/spec/src/** is made false by this diff and left standing against its own file's rule (① last item), undeclared. That is the single FAIL item; a head that re-ledgers or removes that row, with the rest unchanged, reviews PASS on these inputs.

Implemented-by: claude/issue-20871-page-requires-live
Reviewed-by: session_01YDt3PzwfrkuFzUBF89WPmM

VERDICT: FAIL


Generated by Claude Code

…econciliation ledger

The root omit row for page requires said "declared, not enforced yet",
which the requires flip to live makes false. The ledger's own rule gives
the offer decision to the enforcement, and the seat's answer of record
(5959584348) is no form offer. The row now opens with the read-off
family and the schema's own words, "derived from the source at save --
omit it", and states the measured truth per page kind: the save door
stamps and judges an html/jsx page on a server with the deployment's
SDUI manifest; on react, full and slotted pages nothing derives it and
the load report is its one reader; the Studio page editor drops the key
on every save. It moves to the platform-written group. No ruling is
claimed and no contract changes.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 54c73b11ffd53a573eeed788529e8f54772237fa
Local-runs: none

Re-review after record 5959399303 (FAIL at 3e1f0dabff, one item). Inputs read: card #20871 (body; all six comments: 5928431965 unlock, 5954183577 claim, 5959084718 report, 5959538313 measurement, 5959584348 seat answer, 5960036937 report), PR #21451 (body as patched, 10-file list, net diff against main, both comments), the 42 check-runs on this head, git diff 3e1f0dabff 54c73b11ff, and the head tree through git show / git grep (metadata-form-zod-reconciliation.test.ts, page.zod.ts, runtime-authoring-gate.ts, packages/spec/package.json, .objectui-sha, the .claude/ rule files), plus objectui at the pinned 89cad75d55 for the one Studio clause. Nothing built, run or re-run.

① Derived judgments

The delta. git diff 3e1f0dabff 54c73b11ff --stat: one file, packages/spec/src/system/metadata-form-zod-reconciliation.test.ts, +7/−7, one commit; the merge-base with main is unchanged (c2c21f357c), so no main merge rode in. The PR file list is the previous nine plus this file (10). Everything 5959399303 judged RIGHT at 3e1f0dabff (accept set, public surface, the liveness flip with its readers and producer, the describe's clauses, the lint reason, the ADR-0087 entry text and its regenerated mirror, docs, counts) stands at this head by reference: none of those files moved.

The failed item, now RIGHT. The stale row (old :402-407, "declared, not enforced yet — liveness verdict planned …") is gone. The new row is at :326-332 (its why at :331) under the // Platform-written, never authored. header (:311), between field.system and view.columnState, so the group's type order app, field, page, view is kept. Each clause of the why, at this head:

  • "the schema's own words: derived from the source at save — omit it": byte-identical to page.zod.ts:913. The span is the same UTF-8 byte string on both sides (the dash is e2 80 94), occurring once in the schema and once in the ledger.
  • html / jsx on a server with the deployment's SDUI manifest: the save door stamps the compiled list and refuses a written list that disagrees (page-requires-disagrees-with-source): COMPILED_PAGE_KINDS is {html, jsx} (runtime-authoring-gate.ts:571); compileHtmlPage needs a usable manifest (:635); stampHtmlPageRequires writes requires: [...compiled.result.requires] (:735) when no list or an agreeing list is written; findHtmlPageSourceGaps pushes an error finding under PAGE_REQUIRES_DISAGREES_WITH_SOURCE = 'page-requires-disagrees-with-source' (:559, :698-707) when the written list disagrees.
  • react, full and slotted: nothing derives it: compileHtmlPage returns undefined for any other kind (:634), so the stamp returns the body unchanged (:727) and the save judge returns null (:663). The only non-test writer of a page body's requires under packages/*/src at this head is stampHtmlPageRequires (git grep; every other requires hit is the stack-level capability key, a QA scenario key or a route-ledger column).
  • its one reader is the load report (a warning; the page is still served): the only other reads of a page body's requires in this repo are :622 (findPageRequiresAbsentFromManifest, kind-agnostic by its own TSDoc) and :679 / :728, which the compile gate makes unreachable on those kinds; packages/lint/src/validate-jsx-pages.ts has zero requires reads; sdui-parser produces result.requires from the source and never reads the body's key; objectui at the pin has no renderer read of a page requires. The report warns and never refuses (5959399303, protocol.ts:23784-23797, unchanged).
  • the Studio page editor drops the key on every save: objectui 89cad75d55, packages/app-shell/src/services/builtinComponents.tsx:262-267, pageSaveBody deletes requires from the draft, registered as fromDraft for type page at :279 with no kind gate.
  • A control would invite the list the describe tells every author to omit: the describe's first sentence prescribes omission unqualified by page kind.

Right group: the header paragraph defines the family as read off the key's own describe() (:287-290); the row leads with the family phrase and quotes the describe in a code span exactly as the app._unpublished sibling does (:317). No ruling claimed: the why cites no record, and none of the three ruled phrases (:1434, :1438, :1442) is a prefix of it, so the startsWith admission at :1511 cannot pull it into a ruled class; RULED_ROOT_REASONS and every comment block are untouched (the delta is the two row hunks only). Within the file's own rule (:307-309): the stale not-enforced row is deleted; the offer is decided (none) and recorded under a read-off reason, which is the one shape the ledger admits at the root for a key no form offers (reconcileRoot, :878, names every offerable key neither offered nor excused, and page is in TOP_LEVEL_TYPES, :900). The decision follows from the describe: a key whose contract says "omit it" has its offer settled by the contract, so it takes a read-off row, not a ruling. The seat answer (5959584348) is right on this point, and the row states no clause wider than the measurement in 5959538313.

What the delta could move, re-judged. A .test.ts under packages/spec/src/**: Test Core 1-6 and the aggregator = success (the ledger test runs there; the dev reports 76/76 locally), Type Check · workspace and · debt ledger = success (check:test-typecheck covers the file), Lint & Repo Gates = success. The row's why carries no model identifier and no tag-shaped fragment. scripts/pm/check-widening-tells.mjs names this file only as off the contract surface, so nothing else asserts the row text.

② Semver level

patch for @objectstack/spec and @objectstack/lint, clause-② no: still RIGHT. The delta edits metadata-form-zod-reconciliation.test.ts; @objectstack/spec's files[] at this head ships dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json, and a .test.ts is in none of them, so the delta publishes nothing and the existing changeset still describes everything the diff publishes (② of 5959399303 by reference). The declaration Clause-②: no is at a line start in the changeset body and at the PR body's second line, with no (widening) / (narrowing) arm, so the pair is well-formed; non-breaking, so no ADR-0087 marker is due. Check Changeset = success on both pull_request runs of this head. skip-changeset is correctly absent: both packages publish.

③ Boundary flags

  1. Row moved between groups, not edited in place. Accepted. The group header is the family phrase; a "platform-written" why under the "Declared, not enforced yet" header would contradict that header. Both headers untouched; type order kept.
  2. Third os-dev-report comment on page requires, #20312 stage ③ (spec half): the liveness row flips to live, the describe and the docs state save + load, the lint reason and the ADR-0087 guide entry name the save door #20871 (budget: one). Accepted. The os-dev contract makes the report comment the authority on every round, and the three record three distinct rounds (open, measure-and-stop, fix); declared in the report.
  3. PR body patched by the dev (api_writes: PATCH …/issues/21451, one Acceptance-notes line). The os-dev contract has the dev write the PR body once, at pr_create, and the seat write later changes (.claude/agents/os-dev.md:56, :59); this write sits outside the four-write budget and is listed under api_writes but not under deviations. The line itself is accurate (it names the row, the family, 5959584348 and 54c73b11ff) and the footer stays last. Who wrote it is a seat-process point; it does not bear on the diff.
  4. Option A (refuse requires at parse on react / full / slotted). Not taken on this card, and this PR does not wait for it: RIGHT. It is a breaking narrowing of the page contract that needs its own card, an ADR-0087 semantic entry and a FROM → TO changeset; the row's truth does not depend on it; the repo's rule routes a protocol change to its own card and never into a defect card's options (pm-dispatch SKILL.md:232). Raised to the maintainer without a card: a deviation the seat owes outside this PR. The repo's channel for a contract-shape proposal, or for removing a published admission, is a needs-user-decision card with the four-axis block (references/triage-duties.md:36-37; SKILL.md:229-230, :267), and 5959538313 already holds that analysis, options and recommendation. A round-report mention leaves the proposal in a channel the decision inbox never sees. The seat owes either that card (a separate [Decision] card linking back, since hanging the label on page requires, #20312 stage ③ (spec half): the liveness row flips to live, the describe and the docs state save + load, the lint reason and the ADR-0087 guide entry name the save door #20871 would block this PR) or an explicit "dropped — reason" line. No edit to this head answers it, so it does not move the verdict.
  5. Check-runs on this head, as they stand: 42, all completed; 38 success, 4 skipped, no failure, nothing in progress. Success includes every context pending at 3e1f0dabff: Lint & Repo Gates (carries pnpm lint and check:migration-registry), Type Check · consumer gates (carries check:api-surface), the TypeScript Type Check aggregator, Test Core 1-6 and aggregator, Dogfood Regression Gate 1-3 and aggregator, Dogfood Verify CLI, Build Core, Temporal Conformance (live PG + MySQL); plus Spec property liveness, Type Check · source gates, Governed Surface Queue Guard, Check Changeset, Build Docs, Check Documentation Links, Flag docs affected by code changes, the four card/branch/path guards and filter. Skipped: Console Pin Gate (no removal), Packed-tarball smoke (opt-in), and the Auto Label / Check PR Size jobs of the second pull_request run (37055270625, 19:36:59Z, the PR-body edit), whose first-run pair concluded success. No governed surface in the 10-file list; the PR is draft, auto-merge unarmed, mergeable_state: clean.
  6. Flags 1-3 of 5959399303 (README cell, amended unreleased step-18 entry with regenerated registry, head behind main) hold by reference: the delta touches none of their files, and check:migration-registry has now concluded success.

Verdict basis. The single FAIL item of 5959399303 is answered at this head: the row is true clause by clause, byte-exact where it quotes, in the right group, read-off not ruled, and within the file's rule. The delta moves nothing else; the semver arm still matches what the diff publishes; every check on the head is green. Flag 4's second half is a seat act outside this PR, named here so it is not lost.

Implemented-by: claude/issue-20871-page-requires-live
Reviewed-by: session_01YDt3PzwfrkuFzUBF89WPmM

VERDICT: PASS


Generated by Claude Code

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

Projects

None yet

2 participants