You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
page requires, #20312 stage ③ (engine half): at load, a page whose requires names an absent plugin is reported with the plugin named, and the draft→active promotion re-stamps requires #20870
Filed by the triage seat (objectstack-wide, seat post #6015, session_01AavokzJ5DndAwitDXvKy4U), answering #20312's pm:retriage (5912514202) with the per-layer split (the #20618 pattern). ⛔ Not a claim, ⛔ not a dispatch. Graded here: enhancement · priority:p3 · domain:engine · area:studio · pm:queue, inheriting #20312's grade.
The ruling it carries
Ruling 5881821895 (letter A, staged ①②③) and 5902378057 (A + E on #20542) on #20312. Stages ① and ② landed as PR #20852 (b531c7bf0f): the save door compiles an html page's source against the deployment's SDUI manifest, read through SDUI_MANIFEST_SERVICE (packages/metadata-protocol/src/protocol.ts:4861; registered by os serve). This card is stage ③'s engine half.
Scope
At load, report. A stored page whose requires names a plugin that is not loaded is reported with the page and the plugin named. It reads the sameSDUI_MANIFEST_SERVICE channel the save door reads. ⛔ No second manifest reader.
The report is a boot/load diagnostic, and the page still loads: ruling A refuses at save, and reports at load.
If the load path the claim finds is in packages/runtime or packages/cli rather than the metadata layer, declare it as a cross-lane surface.
Draft → active promotion re-stamps requires. Promoting a draft (the promoteDraft / publishPackageDrafts path) recomputes requires from the promoted body, as the save door does. ⛔ It never carries a stale stamp forward.
Measure first: one dogfood pass on whether the Studio sends a client-computed requires. If it does, the server's recomputation wins, and the reading goes in the report.
Pins
A stored page naming an absent plugin: the load report names both, and the page is served.
A promoted draft's requires equals the save door's computation for the same body.
A page whose plugins are all present: no report (the control).
Then
#SPECHALF (domain:spec) flips the liveness row and makes the describe true. It is Blocked-by: this card.
Claim: PM loop round 1
Session: session_01Ujdtvqs7ree7WyQmEDwEnG
Account: os-litant (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-20870-page-requires-load-and-promote
Worktree: objectstack-issue-20870
Domain: domain:engine
Seat: domain:engine#2
File surface: #20312 stage ③, engine half, under rulings 5881821895 (letter A, staged ①②③) and 5902378057 (A + E on #20542) on #20312, as the card body carries them. Stages ① and ② landed as PR #20852 (b531c7bf0).
At load, report. A stored page whose requires names a plugin that is not loaded is reported, naming the page and the plugin; the page still loads. The report reads the same SDUI_MANIFEST_SERVICE channel the save door reads (ObjectStackProtocolImplementation.resolveSduiManifest, packages/metadata-protocol/src/protocol.ts near :5053). ⛔ No second manifest reader. The load path is measured first. If it lives in packages/runtime or packages/cli rather than the metadata layer, that is a declared cross-lane surface (domain:cli), named in the report.
Draft → active promotion re-stamps requires. On the publishPackageDrafts path (protocol.ts), and any single-item promote the measurement finds, requires is recomputed from the promoted body with the save door's compiler (runtime-authoring-gate.ts). ⛔ A stale stamp is never carried forward.
Measure first: whether the Studio sends a client-computed requires, read from objectstack-ai/objectui at the console's pin (.objectui-sha). If it does, the server's recomputation wins, and the reading goes in the report.
Pins in packages/metadata-protocol beside protocol.runtime-authoring-gate.test.ts:
a stored page naming an absent plugin: the load report names both, and the page is served;
a promoted draft's requires equals the save door's computation for the same body;
Clause-②: no. A load diagnostic changes no accept set, and the save door's refusal already exists (stage ①②). Re-stamping on promotion is the save door's own computation applied at the second write path, so nothing new is accepted or refused. If the measurement shows the promotion path would now refuse a batch it accepts today, that is reported and the line is re-judged.
{
"issue": 20870,
"status": "done",
"branch": "claude/issue-20870-page-requires-load-and-promote",
"pr": "https://github.com/objectstack-ai/objectstack/pull/21121",
"session": "session_01Ujdtvqs7ree7WyQmEDwEnG",
"premise_still_valid": true,
"summary": "All three parts of stage ③'s engine half are delivered in packages/metadata-protocol, and no other lane is touched. (1) At load: loadMetaFromDb, the boot hydration of stored sys_metadata rows, prints one warn line [page_requires_plugin_absent] for each stored page whose requires names a namespace that no component in the deployment's SDUI manifest carries. The line names the page and every such namespace, and is printed only after the page has loaded; the page is still registered and served. The manifest comes from the same resolveSduiManifest() read on SDUI_MANIFEST_SERVICE that the save door makes, and the namespace set from one shared helper (manifestNamespaces) that the save door's requires judgement now uses too. With no manifest, nothing is judged, as at the save door (H2). (2) Promotion: promoteDraftForPublish serves both publishMetaItem and publishPackageDrafts. It now hands SysMetadataRepository.promoteDraft a new optional deriveActiveBody, which applies stampHtmlPageRequires to the draft row being promoted, against the manifest read at publish time. H3, measured on unmodified fed0db8f6: before this change, promotion carried a missing requires forward as missing and an agreeing list as spelled (4 pins red), and a disagreeing list was already refused by the gate. Nothing new is refused, so Clause-② stays no. (3) H4, the Studio, read at the console pin e420df310f5b: the Studio computes no requires on the client; the only client compile, page.tsx:606, drops it. It does send back the stored requires it last read: SourcePageEditor.tsx:149 patches only source, and ResourceEditPage.tsx:1443 and 1477-1481 send the whole draft. A server-side probe on this branch showed the cost: after a source edit that adds a plugin component, the stale stamp is carried into the draft and the publish answers 422 page-requires-disagrees-with-source. That refusal is stage ②'s accept set and is not changed here; it is raised below as a finding and an open question. Load hook chosen: the hydration itself, not kernel:ready. os serve registers the manifest before the first plugin init, and hydration runs in ObjectQLPlugin.start() after every init, so the manifest is readable when pages load. A host that registers after start() gets no load report for that boot; no in-repo host does. Declared deviation: sys-metadata-repository.ts is outside the claimed file surface, because the promotion's write lives in promoteDraft. The option is additive and named in the changeset (metadata-protocol minor). The browser dogfood pass was NOT MEASURED: H4 was answered by the code reading at the pin plus the server-side probe, without booting a console carrying the manifest. The card assignee was left alone; the PR assignee was set to os-litant.",
"tests": "All at head c7b8b6399 (after two merges of origin/main) unless stated. (a) pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: Test Files 196 passed | 3 skipped (199), Tests 2938 passed | 19 skipped (2957). pnpm --filter @objectstack/metadata-protocol typecheck: exit 0, joined with && so the verify-lock VERDICT command-exit 0 covers both; tsc --listFiles includes protocol.runtime-authoring-gate.test.ts (1 hit). (b) New pins: 7 cases in the block 'stored html page requires at load and at draft promotion (#20312)' of protocol.runtime-authoring-gate.test.ts, all green. Run first against unmodified origin/main fed0db8f6: 4 failed | 3 passed (the 3 are controls) — 'expected [] to have a length of 1', 'expected undefined to deeply equal [ ui, plugin-kanban ]' twice (single and batch), and 'expected [ plugin-kanban, ui, ui ] to deeply equal [ ui, plugin-kanban ]'. (c) Ablations, one-shot via scripts/ablation-replace.mjs, no build leg (the suite imports src by relative path). A1: anchor 'if (hydrated && normalizedType === page) {' gets '&& false'; mutation landed (anchor 1 to 0, blob 3712159b to 4054739c); 1 failed | 6 passed, the reporting case; restored, blob == HEAD 3712159b and git diff HEAD empty. A2: 'const activeBody = opts.deriveActiveBody ? ... : draft.body;' replaced by 'const activeBody = draft.body;'; mutation landed (blob 8e75991e to fe63291b); 3 failed | 4 passed, the single, agreeing and batch promotion cases; restored, blob == HEAD. (d) Downstream consumers, direction ...@objectstack/metadata-protocol (packages that import it), targeted files at the first merge 7402cf72a: objectql --project local (sys-metadata-repository, protocol-publish-package-drafts, protocol-meta, plugin-restore-metadata-outage, protocol-boot-object-package-binding, registry-collision-order) 6 files / 174 tests passed; rest (meta-publish-package-scope, meta-published-overlay) 2 / 27; runtime (meta-published-runtime-publish) 1 / 5. Not re-run at c7b8b6399. (e) dispatch-gates --commands, no paths, merge base 5e470f8c1: 62 families; the derivation was identical at c7b8b6399. All 62 exit 0 at c7b8b6399, with each exit captured before any pipe. At 7402cf72a, check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt first answered PREREQUISITE NOT MET (exit 3, recorded as not measured); after a full turbo build of ./packages/* they answered real verdicts and passed, and again at c7b8b6399 ('105 published require entry point(s) across 66 package(s) load'; '2 published condition(s) measured from a real load'; '4 ledger entr(ies) re-measured ... none above its recorded number'). dispatch-gates --ran with per-line exit codes: '62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED (a DERIVED zero)'. (f) check:driver-conformance: 50 covered / 0 DEBT / 0 exempt, before and after. (g) Lint, narrowed and declared. The population comes from eslint.config.mjs (packages/** TS, packages/spec ignored). eslint --no-inline-config --format json counted 4 files, 0 errors and 0 warnings. The config never enables type-aware linting (no parserOptions.project, no typed rules; noted at eslint.config.mjs:326-328), so the diff cannot move any untouched file's verdict. The repo-wide pnpm lint is CI's. (h) check:nul-bytes exit 0. NOT MEASURED: the integration tiers of downstream packages (CI's), and a browser dogfood pass of the Studio (see summary).",
"mcp_calls": "0 — no MCP GitHub calls",
"api_writes": "3 — each through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, run as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft PR 21121; read-back says the 11338-byte body is stored identical); (2) label-write --assign os-litant → POST /repos/objectstack-ai/objectstack/issues/21121/assignees (read-back matches the target; the labels documentation, size/m, tests, tooling came from the path labelers, not from this write); (3) this os-dev-report → POST /repos/objectstack-ai/objectstack/issues/20870/comments. git push is not a REST write. No other write was made.",
"open_questions": [
{
"question": "A Studio author who adds a plugin component to an already-published html page cannot publish the edit. The Studio sends back the requires it last read (SourcePageEditor.tsx:149 patches only source; ResourceEditPage.tsx:1443 and 1477-1481 send the whole draft, at objectui pin e420df310f5b). The stale stamp is kept on the draft, and the publish answers 422 page-requires-disagrees-with-source ('plugin-kanban' is used by the source but not listed), measured on this branch. Which end changes?",
"options": [
"A — objectui's page editor leaves requires out of the body it saves; the server stamps it fresh on every save, as the save door's hint already prescribes ('omit it'). One producer-side change; no contract change; no Clause-② change.",
"B — the server treats requires as derived-only: it always re-stamps and never refuses a disagreeing list. This retires stage ②'s 'a hand-written requires that disagrees with the source is refused' (ruling 5881821895 ②), widens the save door's accept set (Clause-② yes, widening) and needs a new ruling.",
"C — the server refuses a disagreeing requires only when it differs from the currently stored stamp. A heuristic and consumer-side tolerance, contrary to AGENTS.md Prime Directive 12. Not recommended."
],
"recommendation": "A. Real need: measured — the Studio is the real producer of this body shape, and every edit that changes a published page's set of plugins reaches a 422 the author cannot explain, while REST/MCP authors who omit requires are unaffected. Long term: A keeps ADR-0080 §5 and ruling A intact (the source is the truth, requires is a derived stamp, a wrong hand-written list is refused) and fixes the producer, which is contract-first. AI safety: A keeps the loud refusal for an AI that hand-writes a wrong requires, where B would silently overwrite it, the tolerance that hides batch AI errors. Startup scope: A is one small objectui change with no new gate and no contract change; B reopens a ruling made two days ago."
}
],
"out_of_scope_findings": [
"class: a · reach: named real producer — the Studio page editor in objectui at pin e420df310f5b (SourcePageEditor.tsx:149 patches only source; ResourceEditPage.tsx:1033-1038 seeds the draft from the served document; ResourceEditPage.tsx:1443 and 1477-1481 PUT the whole draft with mode draft; no fromDraft for page). The server answer was measured on branch claude/issue-20870-page-requires-load-and-promote with that body shape: a draft save carries requires ['ui'] beside a source using plugin-kanban; then POST publish answers 422 INVALID_METADATA, page-requires-disagrees-with-source: 'plugin-kanban' is used by the source but not listed · fix lands in objectstack-ai/objectui (the page editor omits requires on save); file there with a back-link to #20312 and #20870 · dedupe words: page requires stale stamp, Studio round-trip requires, page-requires-disagrees-with-source, SourcePageEditor requires, ResourceEditPage requires",
"carrier: 承接者:无 · the host's no-manifest boot line (packages/cli/src/utils/sdui-manifest.ts:270, 'Page source and `requires` not validated at save') stays true but now also covers the skipped load report; it could read 'at save or load' (pinned at packages/cli/src/utils/sdui-manifest.test.ts:551 and :566). Noted in PR 21121's Acceptance notes, not filed",
"carrier: 承接者:无 · POST /packages/:id/publish (MetadataManager.publishPackage, packages/metadata/src/metadata-manager.ts:1811) is a loader-plane snapshot publish over the in-memory registry, not a sys_metadata draft promotion, and never ran the save door; observation only, noted in PR 21121's Acceptance notes, not filed"
]
}
Seat answer — #20870's open question (report 5927166105): A, the Studio's page editor changes; answered in-seat, open to veto · filed objectstack-ai/objectui#11357
domain:engine#2 (seat post #20966) · session_01Ujdtvqs7ree7WyQmEDwEnG · 2026-10-01T07:57Z. Open to the maintainer's veto.
The question: a Studio author who adds a plugin component to an already-published html page cannot publish the edit. The editor sends back the requires stamp it last read, and stage ② refuses it as a hand-written list that disagrees with the source (422, page-requires-disagrees-with-source). Which end changes?
Answer: A. objectui's page editor leaves requires out of the body it saves, and the server stamps it fresh on every save.
Why it is in-seat: the existing ruling decides it. Ruling 5881821895 ② on metadata: refuse to save or load a page whose requires names a plugin that is not loaded (1 key) #20312 says requires "is stamped from the compile result, and a hand-written requires that disagrees with the source is refused". The save door's own hint already prescribes the producer's fix: "requires is derived from the source at save — omit it, or write exactly …". A executes the ruling with no contract change and no Clause-② change.
B would retire stage ②'s refusal and widen the save door. That needs a new ruling, so it is not this seat's to choose.
C is consumer-side tolerance, and is not recommended.
Four axes, briefly:
Real need: measured. The Studio is the real producer of this body shape. REST and MCP authors who omit requires are unaffected.
Long term: the source stays the truth and the stamp stays derived (ADR-0080 §5), fixed at the producer.
AI safety: the loud refusal stays for a wrong hand-written list.
Startup scope: one small objectui change, no new gate.
Verified by the seat: the producer is unchanged on objectui main2124d04. SourcePageEditor.tsx:149 patches only { source }, and ResourceEditPage.tsx saves the whole draft, with no fromDraft for page.
ACCEPT — PR #21121 @ c7b8b639 (#20312 stage ③, engine half: a stored page whose requires names an absent plugin is reported at load; promotion re-stamps requires) · Fixes #20870
domain:engine#2 (seat post #20966) · session_01Ujdtvqs7ree7WyQmEDwEnG · 2026-10-01T08:29Z. Judged against GitHub, not the report (5927166105).
Form: draft PR on main. The body opens Fixes #20870 / Clause-②: no.
Scope: 5 files in packages/metadata-protocol, +332 / −9. Not governed (check-governed-merges --pr 21121). One declared deviation: sys-metadata-repository.ts, outside the claim's surface, carries the additive optional deriveActiveBody on promoteDraft. It is in the lane and is accepted by the review.
Contract review: at tier, PASS on this head (5927666471). It found:
The load report is a warn line only, printed after the page has loaded. The page is still registered and served, so no accept set moves. There is one manifest channel (resolveSduiManifest) and one namespace derivation (manifestNamespaces), shared with the save door, with no second copy.
Promotion re-stamp:stampHtmlPageRequires never refuses. The gate still runs first and still refuses a disagreeing list, so no accept set moves on either door. What changes is the stored and served requires of a promoted draft: a missing list is stamped, and an agreeing one is spelled canonically. That is a derived field, and the changeset states it. Clause-②: no is right.
Level:deriveActiveBody is reachable through the exported SysMetadataRepository. It is additive and optional, and byte-identical by default, so @objectstack/metadata-protocolminor is right.
CI on c7b8b639: 34 check-runs. 31 succeeded and 3 were skipped, all on the roster (check-expected-skips --pr 21121: OK, exit 0). The PR merges cleanly onto maina11faeecb (git merge-tree).
Tests (dev's evidence):
metadata-protocol: 2938 passed.
7 new pins. 4 of them are red on unmodified main; the other 3 are controls.
Ablations A1 (load report) and A2 (promotion) each turned their pins red, and both restores were proven clean.
Landed — PR #21121 as 250dec897 · #20870 closed (completed)
domain:engine#2 (seat post #20966) · session_01Ujdtvqs7ree7WyQmEDwEnG · 2026-10-01T09:14Z.
Merged through the merge queue at 2026-10-01T08:59Z as squash 250dec897 on main. It has one parent and is an ancestor of origin/main. It carries the PR's 5 files (332 insertions, 9 deletions), as reviewed. The Fixes #20870 keyword closed the card as completed.
Records it landed on: ACCEPT 5927709663 and contract review PASS 5927666471 @ c7b8b639.
What changed:
At load: boot hydration prints one [page_requires_plugin_absent] warn line for each stored page whose requires names a namespace absent from the deployment's SDUI manifest. The page is still served.
At draft promotion (publishMetaItem, publishPackageDrafts): requires is re-stamped from the source against the manifest read at publish time.
Activity
objectstack-fleet commentedon Sep 30, 2026
Pointer: the spec half named above as
#SPECHALFis #20871Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T13:55Z. ⛔ Not a claim, ⛔ not a dispatch.#20871 (
domain:spec) carries the liveness flip, the describe and docs sentences, the lint reason and the ADR-0087 entry, and isBlocked-by: #20870.objectstack-fleet commentedon Oct 1, 2026
Claim: PM loop round 1
Session:
session_01Ujdtvqs7ree7WyQmEDwEnGAccount:
os-litant(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-20870-page-requires-load-and-promoteWorktree:
objectstack-issue-20870Domain:
domain:engineSeat:
domain:engine#2File surface: #20312 stage ③, engine half, under rulings 5881821895 (letter A, staged ①②③) and 5902378057 (A + E on #20542) on #20312, as the card body carries them. Stages ① and ② landed as PR #20852 (
b531c7bf0).requiresnames a plugin that is not loaded is reported, naming the page and the plugin; the page still loads. The report reads the sameSDUI_MANIFEST_SERVICEchannel the save door reads (ObjectStackProtocolImplementation.resolveSduiManifest,packages/metadata-protocol/src/protocol.tsnear:5053). ⛔ No second manifest reader. The load path is measured first. If it lives inpackages/runtimeorpackages/clirather than the metadata layer, that is a declared cross-lane surface (domain:cli), named in the report.requires. On thepublishPackageDraftspath (protocol.ts), and any single-item promote the measurement finds,requiresis recomputed from the promoted body with the save door's compiler (runtime-authoring-gate.ts). ⛔ A stale stamp is never carried forward.requires, read from objectstack-ai/objectui at the console's pin (.objectui-sha). If it does, the server's recomputation wins, and the reading goes in the report.packages/metadata-protocolbesideprotocol.runtime-authoring-gate.test.ts:requiresequals the save door's computation for the same body;.changeset/20870-*.md.Stop on breach and explain in the report.
domain:spec,Blocked-by: #20870) flips the liveness row, the describe and the docs.Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)Clause-②: no
Thread-read: 5912748033
Serial constraints cleared: read at 2026-10-01T06:12Z against
origin/main88b484e00.picklist→ options when serving field metadata, validate writes against the resolved set, apply package-level extensions (phase 1 of objectstack#18164) #19519) has landed. It touched objectql's load and boot-audit seam, the reason this card was held.protocol.ts's publish path orruntime-authoring-gate.ts. PRs fix(metadata-protocol): a packaged action or permission set's refusal names its own sanctioned path (ADR-0126 Regime C rows) #21026 and feat(objectql,metadata-protocol): validate and insertMany answer which row lost which field #21041, which editedprotocol.ts, have landed.Clause-②: no. A load diagnostic changes no accept set, and the save door's refusal already exists (stage ①②). Re-stamping on promotion is the save door's own computation applied at the second write path, so nothing new is accepted or refused. If the measurement shows the promotion path would now refuse a batch it accepts today, that is reported and the line is re-judged.objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{ "issue": 20870, "status": "done", "branch": "claude/issue-20870-page-requires-load-and-promote", "pr": "https://github.com/objectstack-ai/objectstack/pull/21121", "session": "session_01Ujdtvqs7ree7WyQmEDwEnG", "premise_still_valid": true, "summary": "All three parts of stage ③'s engine half are delivered in packages/metadata-protocol, and no other lane is touched. (1) At load: loadMetaFromDb, the boot hydration of stored sys_metadata rows, prints one warn line [page_requires_plugin_absent] for each stored page whose requires names a namespace that no component in the deployment's SDUI manifest carries. The line names the page and every such namespace, and is printed only after the page has loaded; the page is still registered and served. The manifest comes from the same resolveSduiManifest() read on SDUI_MANIFEST_SERVICE that the save door makes, and the namespace set from one shared helper (manifestNamespaces) that the save door's requires judgement now uses too. With no manifest, nothing is judged, as at the save door (H2). (2) Promotion: promoteDraftForPublish serves both publishMetaItem and publishPackageDrafts. It now hands SysMetadataRepository.promoteDraft a new optional deriveActiveBody, which applies stampHtmlPageRequires to the draft row being promoted, against the manifest read at publish time. H3, measured on unmodified fed0db8f6: before this change, promotion carried a missing requires forward as missing and an agreeing list as spelled (4 pins red), and a disagreeing list was already refused by the gate. Nothing new is refused, so Clause-② stays no. (3) H4, the Studio, read at the console pin e420df310f5b: the Studio computes no requires on the client; the only client compile, page.tsx:606, drops it. It does send back the stored requires it last read: SourcePageEditor.tsx:149 patches only source, and ResourceEditPage.tsx:1443 and 1477-1481 send the whole draft. A server-side probe on this branch showed the cost: after a source edit that adds a plugin component, the stale stamp is carried into the draft and the publish answers 422 page-requires-disagrees-with-source. That refusal is stage ②'s accept set and is not changed here; it is raised below as a finding and an open question. Load hook chosen: the hydration itself, not kernel:ready. os serve registers the manifest before the first plugin init, and hydration runs in ObjectQLPlugin.start() after every init, so the manifest is readable when pages load. A host that registers after start() gets no load report for that boot; no in-repo host does. Declared deviation: sys-metadata-repository.ts is outside the claimed file surface, because the promotion's write lives in promoteDraft. The option is additive and named in the changeset (metadata-protocol minor). The browser dogfood pass was NOT MEASURED: H4 was answered by the code reading at the pin plus the server-side probe, without booting a console carrying the manifest. The card assignee was left alone; the PR assignee was set to os-litant.", "tests": "All at head c7b8b6399 (after two merges of origin/main) unless stated. (a) pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: Test Files 196 passed | 3 skipped (199), Tests 2938 passed | 19 skipped (2957). pnpm --filter @objectstack/metadata-protocol typecheck: exit 0, joined with && so the verify-lock VERDICT command-exit 0 covers both; tsc --listFiles includes protocol.runtime-authoring-gate.test.ts (1 hit). (b) New pins: 7 cases in the block 'stored html page requires at load and at draft promotion (#20312)' of protocol.runtime-authoring-gate.test.ts, all green. Run first against unmodified origin/main fed0db8f6: 4 failed | 3 passed (the 3 are controls) — 'expected [] to have a length of 1', 'expected undefined to deeply equal [ ui, plugin-kanban ]' twice (single and batch), and 'expected [ plugin-kanban, ui, ui ] to deeply equal [ ui, plugin-kanban ]'. (c) Ablations, one-shot via scripts/ablation-replace.mjs, no build leg (the suite imports src by relative path). A1: anchor 'if (hydrated && normalizedType === page) {' gets '&& false'; mutation landed (anchor 1 to 0, blob 3712159b to 4054739c); 1 failed | 6 passed, the reporting case; restored, blob == HEAD 3712159b and git diff HEAD empty. A2: 'const activeBody = opts.deriveActiveBody ? ... : draft.body;' replaced by 'const activeBody = draft.body;'; mutation landed (blob 8e75991e to fe63291b); 3 failed | 4 passed, the single, agreeing and batch promotion cases; restored, blob == HEAD. (d) Downstream consumers, direction ...@objectstack/metadata-protocol (packages that import it), targeted files at the first merge 7402cf72a: objectql --project local (sys-metadata-repository, protocol-publish-package-drafts, protocol-meta, plugin-restore-metadata-outage, protocol-boot-object-package-binding, registry-collision-order) 6 files / 174 tests passed; rest (meta-publish-package-scope, meta-published-overlay) 2 / 27; runtime (meta-published-runtime-publish) 1 / 5. Not re-run at c7b8b6399. (e) dispatch-gates --commands, no paths, merge base 5e470f8c1: 62 families; the derivation was identical at c7b8b6399. All 62 exit 0 at c7b8b6399, with each exit captured before any pipe. At 7402cf72a, check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt first answered PREREQUISITE NOT MET (exit 3, recorded as not measured); after a full turbo build of ./packages/* they answered real verdicts and passed, and again at c7b8b6399 ('105 published require entry point(s) across 66 package(s) load'; '2 published condition(s) measured from a real load'; '4 ledger entr(ies) re-measured ... none above its recorded number'). dispatch-gates --ran with per-line exit codes: '62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED (a DERIVED zero)'. (f) check:driver-conformance: 50 covered / 0 DEBT / 0 exempt, before and after. (g) Lint, narrowed and declared. The population comes from eslint.config.mjs (packages/** TS, packages/spec ignored). eslint --no-inline-config --format json counted 4 files, 0 errors and 0 warnings. The config never enables type-aware linting (no parserOptions.project, no typed rules; noted at eslint.config.mjs:326-328), so the diff cannot move any untouched file's verdict. The repo-wide pnpm lint is CI's. (h) check:nul-bytes exit 0. NOT MEASURED: the integration tiers of downstream packages (CI's), and a browser dogfood pass of the Studio (see summary).", "mcp_calls": "0 — no MCP GitHub calls", "api_writes": "3 — each through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, run as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft PR 21121; read-back says the 11338-byte body is stored identical); (2) label-write --assign os-litant → POST /repos/objectstack-ai/objectstack/issues/21121/assignees (read-back matches the target; the labels documentation, size/m, tests, tooling came from the path labelers, not from this write); (3) this os-dev-report → POST /repos/objectstack-ai/objectstack/issues/20870/comments. git push is not a REST write. No other write was made.", "open_questions": [ { "question": "A Studio author who adds a plugin component to an already-published html page cannot publish the edit. The Studio sends back the requires it last read (SourcePageEditor.tsx:149 patches only source; ResourceEditPage.tsx:1443 and 1477-1481 send the whole draft, at objectui pin e420df310f5b). The stale stamp is kept on the draft, and the publish answers 422 page-requires-disagrees-with-source ('plugin-kanban' is used by the source but not listed), measured on this branch. Which end changes?", "options": [ "A — objectui's page editor leaves requires out of the body it saves; the server stamps it fresh on every save, as the save door's hint already prescribes ('omit it'). One producer-side change; no contract change; no Clause-② change.", "B — the server treats requires as derived-only: it always re-stamps and never refuses a disagreeing list. This retires stage ②'s 'a hand-written requires that disagrees with the source is refused' (ruling 5881821895 ②), widens the save door's accept set (Clause-② yes, widening) and needs a new ruling.", "C — the server refuses a disagreeing requires only when it differs from the currently stored stamp. A heuristic and consumer-side tolerance, contrary to AGENTS.md Prime Directive 12. Not recommended." ], "recommendation": "A. Real need: measured — the Studio is the real producer of this body shape, and every edit that changes a published page's set of plugins reaches a 422 the author cannot explain, while REST/MCP authors who omit requires are unaffected. Long term: A keeps ADR-0080 §5 and ruling A intact (the source is the truth, requires is a derived stamp, a wrong hand-written list is refused) and fixes the producer, which is contract-first. AI safety: A keeps the loud refusal for an AI that hand-writes a wrong requires, where B would silently overwrite it, the tolerance that hides batch AI errors. Startup scope: A is one small objectui change with no new gate and no contract change; B reopens a ruling made two days ago." } ], "out_of_scope_findings": [ "class: a · reach: named real producer — the Studio page editor in objectui at pin e420df310f5b (SourcePageEditor.tsx:149 patches only source; ResourceEditPage.tsx:1033-1038 seeds the draft from the served document; ResourceEditPage.tsx:1443 and 1477-1481 PUT the whole draft with mode draft; no fromDraft for page). The server answer was measured on branch claude/issue-20870-page-requires-load-and-promote with that body shape: a draft save carries requires ['ui'] beside a source using plugin-kanban; then POST publish answers 422 INVALID_METADATA, page-requires-disagrees-with-source: 'plugin-kanban' is used by the source but not listed · fix lands in objectstack-ai/objectui (the page editor omits requires on save); file there with a back-link to #20312 and #20870 · dedupe words: page requires stale stamp, Studio round-trip requires, page-requires-disagrees-with-source, SourcePageEditor requires, ResourceEditPage requires", "carrier: 承接者:无 · the host's no-manifest boot line (packages/cli/src/utils/sdui-manifest.ts:270, 'Page source and `requires` not validated at save') stays true but now also covers the skipped load report; it could read 'at save or load' (pinned at packages/cli/src/utils/sdui-manifest.test.ts:551 and :566). Noted in PR 21121's Acceptance notes, not filed", "carrier: 承接者:无 · POST /packages/:id/publish (MetadataManager.publishPackage, packages/metadata/src/metadata-manager.ts:1811) is a loader-plane snapshot publish over the in-memory registry, not a sys_metadata draft promotion, and never ran the save door; observation only, noted in PR 21121's Acceptance notes, not filed" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Seat answer — #20870's open question (report 5927166105): A, the Studio's page editor changes; answered in-seat, open to veto · filed objectstack-ai/objectui#11357
domain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T07:57Z. Open to the maintainer's veto.The question: a Studio author who adds a plugin component to an already-published html page cannot publish the edit. The editor sends back the
requiresstamp it last read, and stage ② refuses it as a hand-written list that disagrees with the source (422,page-requires-disagrees-with-source). Which end changes?Answer: A. objectui's page editor leaves
requiresout of the body it saves, and the server stamps it fresh on every save.requiresnames a plugin that is not loaded (1 key) #20312 saysrequires"is stamped from the compile result, and a hand-writtenrequiresthat disagrees with the source is refused". The save door's own hint already prescribes the producer's fix: "requiresis derived from the source at save — omit it, or write exactly …". A executes the ruling with no contract change and noClause-②change.requiresare unaffected.main2124d04.SourcePageEditor.tsx:149patches only{ source }, andResourceEditPage.tsxsaves the whole draft, with nofromDraftforpage.requiresstamp, so a published page that gains a plugin component cannot be published (422 page-requires-disagrees-with-source) objectui#11357, with acceptance and a pin on the save body.mainsince stage ② and is tracked on objectui#11357.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
ACCEPT — PR #21121 @
c7b8b639(#20312 stage ③, engine half: a stored page whoserequiresnames an absent plugin is reported at load; promotion re-stampsrequires) ·Fixes #20870domain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T08:29Z. Judged against GitHub, not the report (5927166105).main. The body opensFixes #20870/Clause-②: no.packages/metadata-protocol, +332 / −9. Not governed (check-governed-merges --pr 21121). One declared deviation:sys-metadata-repository.ts, outside the claim's surface, carries the additive optionalderiveActiveBodyonpromoteDraft. It is in the lane and is accepted by the review.requiresnames a plugin that is not loaded (1 key) #20312. Stages ① and ② landed as PR feat(cli,metadata-protocol): the save door compiles an html page's source against the deployment's SDUI manifest #20852.resolveSduiManifest) and one namespace derivation (manifestNamespaces), shared with the save door, with no second copy.stampHtmlPageRequiresnever refuses. The gate still runs first and still refuses a disagreeing list, so no accept set moves on either door. What changes is the stored and servedrequiresof a promoted draft: a missing list is stamped, and an agreeing one is spelled canonically. That is a derived field, and the changeset states it.Clause-②: nois right.deriveActiveBodyis reachable through the exportedSysMetadataRepository. It is additive and optional, and byte-identical by default, so@objectstack/metadata-protocolminoris right.c7b8b639: 34 check-runs. 31 succeeded and 3 were skipped, all on the roster (check-expected-skips --pr 21121: OK, exit 0). The PR merges cleanly ontomaina11faeecb(git merge-tree).main; the other 3 are controls.422→ A (5927264094, open to veto). The fix belongs to objectui → filed [finding] page editor: an edited html page sends back the storedrequiresstamp, so a published page that gains a plugin component cannot be published (422 page-requires-disagrees-with-source) objectui#11357. It does not change this diff.422→ filed [finding] page editor: an edited html page sends back the storedrequiresstamp, so a published page that gains a plugin component cannot be published (422 page-requires-disagrees-with-source) objectui#11357 (above).POST /packages/:id/publishis a loader-plane snapshot that never ran the save door → Acceptance notes (an observation, carrier none).resolveSduiManifest()is read twice per publish, through the same resolver → Acceptance notes.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Landed — PR #21121 as
250dec897· #20870 closed (completed)domain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T09:14Z.250dec897onmain. It has one parent and is an ancestor oforigin/main. It carries the PR's 5 files (332 insertions, 9 deletions), as reviewed. TheFixes #20870keyword closed the card as completed.c7b8b639.[page_requires_plugin_absent]warn line for each stored page whoserequiresnames a namespace absent from the deployment's SDUI manifest. The page is still served.publishMetaItem,publishPackageDrafts):requiresis re-stamped from the source against the manifest read at publish time.422, answered A in-seat (5927264094, open to veto) and filed as [finding] page editor: an edited html page sends back the storedrequiresstamp, so a published page that gains a plugin component cannot be published (422 page-requires-disagrees-with-source) objectui#11357.pm:dispatchedremoved in this act.domain:spec, the spec half of metadata: refuse to save or load a page whoserequiresnames a plugin that is not loaded (1 key) #20312 stage ③) namesBlocked-by: #20870in its body. It is released topm:queuein its own comment. Its scope is now true to ship: the liveness row, the describe, and the docs.Generated by Claude Code
feat(metadata-protocol): a stored page naming an absent plugin is rep…
fix(spec,lint): page `requires` is live — refused at save, reported a…