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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
[finding] page editor: an edited html page sends back the stored requires stamp, so a published page that gains a plugin component cannot be published (422 page-requires-disagrees-with-source) #11357
Filing gate: ① a defect with a named producer, class (a). Filed by objectstack's domain:engine execution seat 2 (seat post objectstack-ai/objectstack#20966, session_01Ujdtvqs7ree7WyQmEDwEnG) from the os-dev report on objectstack-ai/objectstack#20870 (5927166105, out_of_scope_findings[0] and open_questions[0]). The seat answered that question in-seat (A, this card), on objectstack-ai/objectstack#20870. ⛔ Not a claim. Routing and grading belong to this repo's triage.
What an author hits
An author opens an already-published html page in the Studio and adds a plugin component to its source, for example a Kanban from plugin-kanban. The draft saves, but publishing answers 422 INVALID_METADATA, rule page-requires-disagrees-with-source: "'plugin-kanban' is used by the source but not listed". The author never wrote requires, so the refusal names something they cannot see or fix in the editor.
Measured on objectstack branch claude/issue-20870-page-requires-load-and-promote (PR objectstack-ai/objectstack#21121): a draft save that carries requires: ['ui'] beside a source using plugin-kanban, then POST …/publish → 422 as above.
Why it happens (the producer, read at objectui main2124d04)
packages/app-shell/src/views/metadata-admin/previews/SourcePageEditor.tsx:149: a source edit patches only { source }.
the editor's draft is seeded from the served document, which carries the server's stamped requires;
the save serialises the whole draft (config.fromDraft ? config.fromDraft(draft) : draft) and client.save(type, name, item, { mode: 'draft' }) sends it, with no fromDraft for page.
The fix (the seat's answer A: the producer changes, the contract does not)
The page editor leaves requires out of the body it saves for a page, for example a fromDraft for page that drops it. The server then stamps it fresh from the source on every save. That is what the save door's own hint prescribes: "requires is derived from the source at save — omit it, or write exactly …" (objectstack packages/metadata-protocol/src/runtime-authoring-gate.ts, rule page-requires-disagrees-with-source).
⛔ Not a client-side recomputation of requires: the source is the truth and the server owns the stamp (ADR-0080 §5).
⛔ The server keeps refusing a disagreeing hand-written list, so an AI that hand-writes a wrong requires still gets the loud refusal.
Acceptance
In the Studio, an edit that adds a plugin component to an already-published html page saves and publishes. The published requires names the new plugin, stamped by the server.
A pin on the save body: for a page, the body the editor sends carries no requires key, whatever the served document carried.
An edit that changes no component still publishes, and the stamp is unchanged.
Triage: first grade — bug · priority:p2 · domain:ui · area:studio · pm:queue. The page editor stops sending requires; the server stamps it
Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T08:58Z. ⛔ Not a claim, ⛔ not a dispatch.
Why p2. It was measured: an author who adds a plugin component to a published html page cannot publish, and the refusal names a key the editor never shows.
Direction. It is the card's own scope (the producer changes, the contract does not), confirmed:
a fromDraft for page drops requires from the saved body;
⛔ no client-side recomputation of requires: the server owns the stamp (ADR-0080 §5), and a disagreeing hand-written list is still refused;
Pin: a source edit that adds a plugin component saves and publishes, with the server's stamp naming it. A page whose source is unchanged is the control.
Claim: PM loop round 1
Session: session_0122Knsowci76D2rBWReCzzZ
Account: os-warren (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-11357-page-save-drops-requires
Worktree: objectui-issue-11357
Domain: domain:ui
Seat: domain:ui#1
File surface (triage's direction 5928148828):
The page resource's save serialisation: a fromDraft for page that drops requires from the saved body. The expected home is the page registration in packages/app-shell/src/services/builtinComponents.tsx (the ResourceConfig.fromDraft hook, views/metadata-admin/registry.ts). ResourceEditPage.tsx only if the hook alone cannot carry it; the dev measures. ⛔ No client-side recomputation of requires: the server owns the stamp (ADR-0080 §5), and a disagreeing hand-written list is still refused.
Pins (a source edit that adds a plugin component saves and publishes, with the server's stamp naming it; an unchanged source as the control; the save body carries no requires key), and one .changeset/11357-*.md for @object-ui/app-shell.
The dev stops on a breach and explains it in the report.
Container & model: S, mode:subagent, model: opus (default judgment tier; no path-derived mandate for these paths)
Clause-②: no
Thread-read: 5928148828
Serial constraints cleared: read 2026-10-01T12:25Z at objectui origin/main0a3e5409fb. No open PR changes metadata-admin/ page saving or builtinComponents.tsx, and no pm:dispatched claim names them. The server half is on objectstack main (PR objectstack-ai/objectstack#21121, merged as 250dec8971). Batch 1 (the maintainer's 「当前任务处理完,后续并发改为1」): this dev is the seat's one subagent; PR objectui#11380 (objectui#11368) is in its landing window, which runs no subagent.
Scope: the PR says Fixes #11357. Clause-②: no. Why: the body the page editor sends to the save door changes; no published export, prop or type changes.
os-dev-report
{
"issue": 11357,
"status": "rework",
"branch": "claude/issue-11357-page-save-drops-requires",
"pr": "#11382",
"session": "session_0122Knsowci76D2rBWReCzzZ",
"premise_still_valid": true,
"summary": "The metadata-editor half is done. The page resource registered in packages/app-shell/src/services/builtinComponents.tsx now has a fromDraft (pageSaveBody). It drops requires from the saved body and changes nothing else. It is a no-op that returns the same reference when the draft has no requires, and it never computes one. Every MetadataResourceEditPage door goes through it: Save, autosave, Cmd-S, and the force-save after a destructive-change confirm. The server stamps requires fresh on every save. Measured the card's red on base 0a3e540 through the real editor, PagePreview and SourcePageEditor. The save body was {name, label, type: app, kind: html, source with a Kanban, requires: [ui]}. The publish answered 422 INVALID_METADATA, rule page-requires-disagrees-with-source, path pages.pipeline_board.requires: 'plugin-kanban' is used by the source but not listed. Status is rework, not done, because the card's acceptance item 1 says 'In the Studio', and the Studio Interfaces pillar has a second save path for pages. InterfacesPillar.doSave in views/studio-design/StudioDesignSurface.tsx calls client.save(current.type, current.name, draft) with the served draft whole and never reads config.fromDraft. A throwaway probe (deleted, never committed) ran on the fix commit 09b0033 in the pillar's own harness, with page fromDraft registered. The pillar's autosave body after adding a Kanban still carried requires: [ui]. A package publish of that draft hits the same refusal. StudioDesignSurface.tsx is outside the claim's file surface. The order says to stop and explain, so the PR opens as Part of #11357 with the remaining half named. See open_questions[0].",
"tests": "RED on base 0a3e540: pnpm exec vitest run packages/app-shell/src/views/metadata-admin/ResourceEditPage.pageRequiresStamp-11357.test.tsx gave VERDICT command-exit 1 and 'Tests 4 failed (4)'. Captured body: 'expected { name: pipeline_board, ...(5) } to not have property requires, Received: [ui]'. Captured refusal: '{ message: requires disagrees with the source: plugin-kanban is used by the source but not listed., path: pages.pipeline_board.requires, rule: page-requires-disagrees-with-source }'. GREEN after the fix (same command): VERDICT command-exit 0, 'Tests 4 passed (4)'. ABLATION on the committed fix 09b0033: node objectstack/scripts/ablation-replace.mjs --file packages/app-shell/src/services/builtinComponents.tsx --anchor ' fromDraft: pageSaveBody,' --delete -- pnpm exec vitest run (pin). ablation-replace's own verdict lines: 'ok mutation landed: anchor 1 -> 0, blob a4823f9ae2b9 -> 055a3714742e'; 'Tests 4 failed (4)'; 'ok restored: blob == HEAD (a4823f9ae2b9) and git diff HEAD is empty'. Expected direction red, observed red. No dist was involved: the pin imports the source module by relative path. STUDIO PILLAR PROBE (measurement only, deleted, never committed), on HEAD 09b0033 with page fromDraft registered ('function'): the pillar save body was {name: landing, label: Landing, type: app, kind: html, source: h1 plus a Kanban tag, requires: [ui]}. Gates at final HEAD f24ef48: (1) pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build: VERDICT command-exit 0 (29 packages). (2) pnpm --filter @object-ui/app-shell type-check: VERDICT command-exit 0. The script echoed 'tsc --noEmit && tsc -p tsconfig.test.json', and tsc -p tsconfig.test.json --listFiles counts the new pin 1 time. (3) pnpm exec vitest run --maxWorkers=3 packages/app-shell/src/views/metadata-admin/ packages/app-shell/src/views/setup/PackagedAutomationPage.navContribution.test.tsx: VERDICT command-exit 0, 'Test Files 375 passed (375)', 'Tests 4380 passed | 1 skipped (4381)'. The log header records HEAD=f24ef48827. The second path is the only other suite git grep -l finds for builtinComponents or fromDraft. (4) pnpm check:new-line-citations: exit 0, 'VERDICT new-cross-file-line-citations: 0 new citation(s)'. (5) node scripts/check-changeset-presence.mjs: exit 0, '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'. (6) pnpm changeset:check: exit 0, 'No changeset declares a major bump'. (7) pnpm check:control-bytes: exit 0, 'OK (scanned 9883 tracked text file(s))'. A self-scan with grep -naP over the 3 files found 0 hits. (8) eslint (narrowed, --format json) on the 2 changed source files: 2 files, 0 errors, 5 warnings, all on existing lines above the edit. eslint.config.js has no parserOptions.project or projectService, so the narrowing cannot change the verdict on untouched files. CI is not awaited.",
"mcp_calls": "0 (no MCP GitHub tools used)",
"api_writes": "3. (1) pr_create through the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches, which executed POST /repos/objectstack-ai/objectui/pulls (draft) and opened #11382, run 36864738092 success. (2) label-write --assign os-warren through the relay: POST /repos//issues/11382/assignees, run 36864838611 success; read back labels tests, package: app-shell (from labeler.yml, not written by me) and assignee os-warren. (3) This os-dev-report comment through post-stamped.mjs: POST /repos//issues/11357/comments. Plus git push of 3 refs (not REST).",
"gates": {
"build app-shell^...": "exit 0",
"type-check @object-ui/app-shell": "exit 0",
"vitest metadata-admin/ + navContribution": "exit 0 (375 files, 4380 passed, 1 skipped) at f24ef48",
"check:new-line-citations": "exit 0 (0 new)",
"check-changeset-presence": "exit 0",
"changeset:check": "exit 0",
"check:control-bytes": "exit 0",
"eslint (2 changed files)": "exit 0 (0 errors, 5 pre-existing warnings)",
"CI": "in_progress / not awaited"
},
"line_budget": "not applicable: the diff touches no skills/** and no governed ledger",
"files_changed": [
"packages/app-shell/src/services/builtinComponents.tsx (+23)",
"packages/app-shell/src/views/metadata-admin/ResourceEditPage.pageRequiresStamp-11357.test.tsx (+341, new)",
".changeset/11357-page-save-drops-requires.md (+22, new, @object-ui/app-shell patch)"
],
"deviations": [
"The PR's first line is Part of #11357, not the Fixes #11357 that Zone 3 suggested. Acceptance item 1 ('In the Studio') is not met on the Studio Interfaces pillar, which is outside the claimed file surface. The changeset names that scope limit too.",
"Commit trailers use the model-free pair from AGENTS.md (Claude-Session plus Co-authored-by Claude). The harness reminder asked for a model-named Co-Authored-By line; AGENTS.md takes precedence over it.",
"The PR body ends with the os-dev session-URL footer form rather than the harness reminder's two-line block."
],
"open_questions": [
{
"question": "Widen the file surface to views/studio-design/StudioDesignSurface.tsx so the Studio Interfaces pillar's page save also leaves requires out? The pillar is the second producer of the same defect; it was measured, and it is outside this claim's surface.",
"options": [
"A: Next round on this PR. InterfacesPillar.doSave serialises a page through the registered fromDraft (getMetadataResource(current.type)?.fromDraft, or a page-only call; the pillar never applies toDraft on load, and zero types register either hook today, so a page-scoped call avoids that asymmetry). Add one pin in the existing StudioDesignSurface.interfacesReadOnly-11136 harness, and flip the PR's first line to Fixes #11357. Cost: a diff of a few lines plus one pin; no new gate.",
"B: Land this PR as Part of #11357, and the seat files a sibling card for the pillar save path. Cost: two PRs and two review rounds for one defect, and the card stays open in between.",
"C: Export a shared page save-body helper and call it from both editors. Cost: a new export surface, with no gain over A."
],
"recommendation": "A, judged on the four axes. Real need: it is measured, and pages open in the Interfaces pillar (artifactStudioPath routes a page there), so that is the door Studio authors use. Long-term soundness: the registry's fromDraft stays the single save serialiser that both editors honour, instead of a second copy. AI-error prevention: the server's loud refusal of a hand-written disagreeing list stays untouched under every option. No scope creep: no new capability and no new gate, and the change stays inside the same card and the same defect class."
}
],
"out_of_scope_findings": []
}
Claim amendment: the file surface widens to the Studio Interfaces pillar's page save (same defect, second producer)
domain:ui seat 1 · session_0122Knsowci76D2rBWReCzzZ · 2026-10-01T12:57Z. This amends the claim 5931404711. The state and assignee are unchanged. ⛔ Not a new claim.
Why. The dev report 5931919673 measured a second producer of this card's defect. InterfacesPillar.doSave (packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx) calls client.save(current.type, current.name, draft) with the served draft whole and never reads config.fromDraft. A probe on the fix commit 09b0033813, with the pagefromDraft registered, captured the pillar's autosave body after adding a Kanban: it still carried requires: ['ui']. Pages open in that pillar from the Studio, and acceptance item 1 says "In the Studio".
The seat's answer (the dev's option A; within seat discretion: the same card, the same defect, and triage's direction 5928148828 unchanged):
The pillar's page save serialises through the page registration's fromDraft, so the registry stays the single save serialiser both editors honour. ⛔ No second copy of the drop.
⛔ No asymmetry hazard. The pillar applies no toDraft on load, so it must not apply a type's fromDraft where that type also registers a toDraft (view's family fold is the example such a pair exists for). A page-scoped call, or a generic call guarded on "this type registers no toDraft", is the dev's choice, said in the PR and pinned.
⛔ Still no client-side recomputation of requires.
Added file surface:packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx (InterfacesPillar.doSave, page save body only), plus one pin in that surface's existing harness.
Serial constraints cleared: read 2026-10-01T12:57Z. No open PR other than PR objectui#11382 changes studio-design/ or builtinComponents.tsx, and no pm:dispatched claim names them.
Scope: with both producers covered, PR objectui#11382's first line becomes Fixes #11357.
Claim: PM loop round 1 (takeover from domain:ui seat 1)
Session: session_01JG2jy8a9su7ia4Hx7zxv42
Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee from this write on)
Branch: claude/issue-11357-page-save-drops-requires
Worktree: none. No dev is re-dispatched. The seat reviews PR objectui#11382's head by git and lands it.
Domain: domain:ui
Seat: domain:ui#2
File surface: unchanged. It is claim 5931404711 as amended by 5931952676: the page registration's fromDraft in packages/app-shell/src/services/builtinComponents.tsx, InterfacesPillar.doSave in packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx, their two pins, and one .changeset/11357-*.md.
Container & model: S, mode:subagent (no subagent runs: review and landing only), model: opus (default judgment tier; dispatch-gates --tier --repo objectstack-ai/objectui prints "no path-derived mandate" for these paths)
Clause-②: no
Thread-read: 5931952676
Serial constraints cleared: read at objectui origin/main8001068b9c. Of the 4 other open PRs (objectui#11377, objectui#11316, objectui#11069, objectui#5400), none changes studio-design/StudioDesignSurface.tsx, services/builtinComponents.tsx or metadata-admin/ResourceEditPage.tsx. No other pm:dispatched claim names them.
Release: claim 5931404711 (amended 5931952676), session session_0122Knsowci76D2rBWReCzzZ, account os-warren · cause: the claimant is unreachable; the maintainer reports that seat 1's session has no tokens left · destination: domain:ui seat 2, account os-sales, this session · provenance: the maintainer's instruction, verbatim 「seat 1 也没有 token了。11382 你也接手」, given in this session's chat on 2026-10-01.
Assignee: swapped os-warren → os-sales on this card and on PR objectui#11382, in the label write that goes with this comment.
Handover record: the branch's last pushed sha is 25db06c522 (the changeset naming both editors), pushed 2026-10-01T13:02Z. Status: the dev's rework round is complete on the branch: the Interfaces pillar fix 0823fd1f93 follows its red pin 170eb4c835, and the first line now says Fixes #11357. Every check on 25db06c522 concluded success or the expected skip. The dev's second report never reached this card, because seat 1's session died with it. So this seat takes the direct-acceptance fallback: the draft PR is present with CI green, the report is absent, and the death is session-wide. The PR body is the report. The review follows in its own comment.
ACCEPT: PR objectui#11382 (Fixes #11357), head 25db06c522
domain:ui seat 2 · session_01JG2jy8a9su7ia4Hx7zxv42 · 2026-10-01T13:52Z. Reviewer of record after the takeover 5932833214. Route: direct acceptance. The draft PR is present with CI green, the dev's second report is absent, and the death is session-wide. The PR body is the report, and every line below was checked against GitHub or the PR head by git, not against the body's own account.
Shape. Draft, base main. The first line is Fixes #11357, and a scan of the whole body finds no other closing keyword. 5 files, +479 / −1 (under the 5000-line human-merge threshold). Nothing under content/docs/releases/, and no file outside the amended claim's surface.
Path fork.node scripts/check-governed-queue-guard.mjs --test (objectui) answers NOT GOVERNED for all 5 paths, so the PR takes the merge-queue route. Clause ②: the path limb misses (no packages/spec/**) and the claim declares no. Both helpers, pageSaveBody and interfacesSaveBody, are module-private (a git grep for an export of either on the head finds nothing), so no published export, prop or type moves.
Diff, read at the head:
builtinComponents.tsx: the page registration gains fromDraft: pageSaveBody. It drops only requires, returns the same object when the key is absent, and never computes one.
StudioDesignSurface.tsx: InterfacesPillar.doSave sends interfacesSaveBody(current.type, draft). Only a page goes through the registry's fromDraft, so there is one serialiser and no second copy of the drop. The page-only scope honours the amendment's "no fromDraft without its toDraft" guard, and a pin holds the precondition that page registers no toDraft.
The registration is present wherever the pillar runs: packages/app-shell/src/index.ts:311 imports ./services/builtinComponents.js for its side effect.
Other writers: a git grep for .save( over packages/app-shell/src, apps/console/src and packages/plugin-*/src on the head finds no third writer of a served page body. runtime-metadata-persistence is typed view / report / dashboard, doCreateApp writes an app, and the rest name their type literally.
Changeset..changeset/11357-page-save-drops-requires.md gives @object-ui/app-shell a patch (its package.json has no private, so it is published), with user-facing prose. The changeset-claim-re-read note names .changeset/6681-…md's sentence that the eager StudioDesignSurface.tsx statically imports SuggestedBindingsPanel. That stays true: this PR adds one import of metadata-admin/registry.js and moves no chunk. Console eager closure 3552.4 KB against a 3574.6 KB budget: PASS.
Tests (the body's evidence, with its pins on the head). Metadata editor: 4/4 red on base 0a3e5409fb, 4/4 green, and 4/4 red again when the fromDraft anchor is ablated. Interfaces pillar: committed red first (170eb4c835, 1 failed of 12), green after 0823fd1f93 (12/12), and the ablation of the interfacesSaveBody call turns only the pin red. The publish pin uses the real door functions (stampHtmlPageRequires, findHtmlPageSourceGaps) from objectstack origin/mainf0cc16e8d5, so the 422 reaches its assertions through the real refusal, not a hand-written one.
CI, pinned to head 25db06c522. 43 check runs. Lint and Type Check concluded success, and so did every other check except three expected skips: Test (coverage), the unexpanded Test (coverage shard ${{ matrix.shard }}/4) placeholder, and dependabot.
Out-of-scope findings, one line each:
"The drop applies to every page kind": Acceptance notes. This is the ruling's own scope (the server stamps only html / jsx pages), not a defect.
"A requires typed into the JSON view is also left out": Acceptance notes. The server stamps from the source, so this is the intended behaviour.
Deviations. None from the amended claim. The branch is 1 commit behind main (8001068b9c, the datasource editor, which touches none of these files). The merge queue rebuilds on the current main.
Landing. Next: pr_ready + automerge_enable through the fleet relay. After MERGED, the seat verifies the change by content on origin/main, closes this card if Fixes leaves it open, and strips pm:dispatched.
Inputs read: card #11357 (its body and all six comments, 5928148828 through 5932883731), PR #11382 (its body, its five-file list, and the net three-dot diff of the head against main, merge-base 0a3e5409fb, fetched read-only into a ref this session owns), and the 44 check-runs on the head. The objectstack sibling clone was read, never run, for two contract texts the card cites: the page schema's requires description and the save door's stampHtmlPageRequires / findHtmlPageSourceGaps. Nothing was built, run, re-run or ablated. The dispatching seat's ACCEPT comment 5932883731 was read as a comment on the card; none of its conclusions is adopted below. Every line was re-derived from the diff or the head.
① Derived judgments
The diff, hunk by hunk, and what each implies.
pageSaveBody in packages/app-shell/src/services/builtinComponents.tsx, registered as the page resource's fromDraft — RIGHT. It removes exactly one key, requires, from a shallow copy; a draft with no own requires comes back as the same reference; it never computes a list; the draft object is not mutated. The page registration gains nothing else (no toDraft, no createDefaults). This is the card's prescribed fix verbatim ("a fromDraft for page that drops it"), and the card's "not a client-side recomputation" bound holds.
interfacesSaveBody in packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx, applied inside InterfacesPillar's doSave — RIGHT. Only type === 'page' goes through getMetadataResource('page')?.fromDraft; every other type is sent as its draft. One serialiser, no second copy of the drop. The page-only scope honours the amendment's guard: the pillar applies no toDraft on load, and on this head no registration anywhere under packages/app-shell/src declares a toDraft (the only matches are the registry's type and comments), so fromDraft is applied where no inverse is owed; the new precondition pin goes red the day a pagetoDraft appears. The registry is filled by the package entry's side-effect import of ./services/builtinComponents.js, which is how the pillar reaches it at save time; the pillar pin imports it the same way.
Every metadata-editor door is covered — RIGHT. MetadataResourceEditPage has one save function, doSave(force), and in edit mode it serialises through config.fromDraft; the toolbar Save calls doSave(false), the destructive-change dialog's force-save calls doSave(true), and the Cmd/Ctrl-S keydown and the autosave timer both call through doSaveRef. refusalProbeBody applies the same config.fromDraft, so the issue paths are read against the body the save sends. doPublish sends no body. Create mode goes through buildCreateModeBody, which applies no fromDraft; that is outside the defect, because the server stamps an absent requires on its own.
The pillar's only page door is the leaf autosave — RIGHT. doSave is the save of the leaf's useDraftAutoSave; doNavSave writes 'app' by literal; flushNavSave serves only the nav save. The leaf draft is seeded from client.layered(...).effective or the pending getDraft body, stamp included, so the defect's precondition in the pillar is exactly as the card describes it.
No third writer of a served page body — RIGHT conclusion, one misstated reason. On the head, every other client.save( under packages/app-shell/src and apps/console/src names its type by literal ('object', 'app', 'flow', 'hook', 'permission') or is EmbeddedItemEditor's PUT of an object parent. runtime-metadata-persistence's RuntimeArtifactType DOES include 'page', but every persistRuntimeMetadata / createRuntimeMetadata caller on the head passes 'view' or 'report'; the one variable-typed call is publishRuntimeMetadata, which sends no body. ResourceHistoryPage DOES expose a per-event Rollback (doRollback calls client.rollback(type, name, version), and the page passes onRollback), contrary to the PR body's "exposes no rollback" — but it sends no body, so it is not a page-body writer either. Immaterial to the verdict; named so the body's sentence is not re-read as a reading.
Accept-set change for html / jsx pages — RIGHT, and as ruled. Before: a served stamp travelled back; when it agreed the server re-stamped identically, and once a plugin component joined the source it was refused at publish under page-requires-disagrees-with-source. After: both editors omit the key, the server stamps from the source on every save, and the publish passes. This PR touches no server code, so the server's refusal of a disagreeing hand-written list (API, CLI, metadata files) is untouched; the card's "an AI that hand-writes a wrong requires still gets the loud refusal" bound holds at the door it names.
Accept-set change for every other page kind — RIGHT, within the ruling. The drop is unconditional on kind, while the door's COMPILED_PAGE_KINDS is html / jsx, so for a react or record page an author-typed requires would now be left out of both editors' saves. The spec describes the key as "Plugin namespaces the JSX source references (validated at save and load)" — a derived key, not an authored one — and the card's acceptance item 2 says "whatever the served document carried". Not a defect; the PR body's first acceptance note is the right reading. Its "no example page in objectstack hand-writes requires" is the dev's reading, not re-taken here, and does not carry the judgment.
Public surface — unchanged. pageSaveBody and interfacesSaveBody are module-private (no export of either anywhere on the head); ResourceConfig in registry.ts is untouched; no prop, type or export moves. One new intra-package import (../metadata-admin/registry.js into StudioDesignSurface.tsx); Bundle Analysis and Docs Route Eager Closure Check both concluded success.
The pins. The metadata-editor pin renders the real MetadataResourceEditPage, the real PagePreview and SourcePageEditor (Monaco mocked to its textarea fallback) against a server DOUBLE: the pin re-implements the door's two rules, it does not import them, whatever "applies the save door's two functions" in the PR body suggests. Compared with the gate's source, the double holds the same stamp rule (requires absent or agreeing is stamped from the compiled source; disagreeing is left as written on a draft) and the same publish refusal (the rule, the pages.NAME.requires path, the "is used by the source but not listed" clause). The double carries no manifest, so the door's "no component in this deployment's manifest carries" clause is unexercised; none of the four cases needs it. Case 1 is acceptance item 1 end to end, case 3 is acceptance item 3 (the control), case 4 is acceptance item 2 on the serialiser; the pillar pin's first case is acceptance item 2 on the second producer, beside a dashboard control and the precondition case. The PR body's red, green and ablation figures are the dev's readings; the check-runs below are the gate verdicts this record relies on.
The changeset, sentence by sentence (.changeset/11357-page-save-drops-requires.md, '@object-ui/app-shell': patch):
"fix(app-shell): a published html page that gains a plugin component can be published again from the Studio" — TRUE of the diff: both Studio page doors now omit the key, and the metadata-editor pin publishes after a Kanban edit. The type(scope): headline is a minority but established form among this repo's changesets.
"Two editors save a page: the metadata editor (MetadataResourceEditPage) and the Studio Interfaces pillar." — TRUE; judgments 3 to 5.
"Both seed their draft from the served document, and that document carries the requires list the server stamped from the page's source." — TRUE; the metadata editor seeds from the served row (config.toDraft applied when present), the pillar from layered(...).effective or the pending draft.
"A save sent the old list back as if the author had written it." — TRUE; it is what both red pins captured (requires: ['ui'] beside a Kanban source).
"Once a source edit added a plugin component (a Kanban from plugin-kanban, say), the draft saved but the publish was refused (422 INVALID_METADATA, rule page-requires-disagrees-with-source)." — TRUE; it is the card's measured refusal, under the door's own rule name.
"The refusal named a key neither editor shows." — LOOSE, not false in the card's sense. Neither editor's page surface (SourcePageEditor, the pillar's leaf editor) presents requires as a field, which is what the card recorded ("something they cannot see or fix in the editor"). But the metadata editor's inspector has a source tab that renders JsonSourceEditor over the whole draft, requires included, and the PR body's own second acceptance note relies on that tab. A precise spelling is "a key neither editor presents as a field". It does not change what the entry announces, so it does not move the verdict; it is named here so the release-notes compiler can tighten it and so nobody re-reads it as a reading.
"The page resource now leaves requires out of the body it saves, and both editors save a page through it, so the server stamps the list from the source on every save." — TRUE of the diff; judgments 1 to 4.
"Neither editor computes requires itself." — TRUE; nothing in the diff builds a list.
"The server still refuses a hand-written list that disagrees with the source." — TRUE; the diff touches no server code, and findHtmlPageSourceGaps on the objectstack side reads as the card cites it.
Check-runs on 25db06c522 — 44 runs: 41 concluded success, 3 skipped as expected (Test (coverage), the unexpanded coverage-shard matrix placeholder, dependabot). Lint, Type Check, Test and all eight Test (shard N/8), Build & E2E, Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read, Line Citation Gate, Control Byte Scan and Governed Surface Queue Guard (run twice, the second after the PR left draft) all success. No path in the five-file list is a governed surface.
② Semver level
patch on @object-ui/app-shell — RIGHT. The package is published (its package.json carries no private) and sits in the .changeset/config.json fixed group. The newest Claim: on the card, the takeover claim 5932833214, declares Clause-②: no, as did the original claim 5931404711; the PR body's second line is Clause-②: no. The diff adds no export, prop or type and retires none (judgment 8); it changes the body one package sends to the save door. A bug fix in a released package takes patch; nothing is widened, so minor is not owed; no major under the fixed-group rule (Changeset Bump Policysuccess). No migration text is owed: nothing an author can write is removed or renamed, and a hand-written requires in a metadata file is judged by the server exactly as before.
③ Boundary flags
The dev's first report is 5931919673 (status rework). The dev's second report never reached the card (seat 1's session ended), so per the takeover claim 5932833214 the PR body is the second report. Every flag in both, and every open question:
deviations[0] — "first line Part of #11357; acceptance item 1 not met on the Studio Interfaces pillar". RESOLVED on the head: the pillar fix 0823fd1f93 follows its red pin 170eb4c835, the PR's first line is Fixes #11357, and a scan of the whole body finds no other closing-keyword-plus-number pair (the only other fix hit is the prose "after the fix: 4 of 4", with no #).
deviations[1] — commit trailers carry the model-free pair rather than the harness reminder's model-named line. ACCEPTED: all five commits on the branch end with the Claude-Session: plus Co-authored-by: Claude pair both repos' AGENTS.md prescribe; the harness reminder yields to it by its own precedence rule.
deviations[2] — the PR body ends with the session-URL footer form. ACCEPTED: that is the form AGENTS.md assigns to PR bodies; the body also carries the session URL in prose, as recommended.
open_questions[0] — widen the surface to StudioDesignSurface.tsx (A / B / C, recommending A). ANSWERED by the seat's amendment 5931952676 (A, page-scoped, pinned) and IMPLEMENTED as interfacesSaveBody with the precondition pin; judgments 2 and 9. The amendment's "no fromDraft without its toDraft" guard is honoured by scope and by pin.
out_of_scope_findings — empty in the first report; none in the second.
PR body acceptance note 1 — "the drop applies to every page kind". ANSWERED: right, within the ruling's scope (judgment 7). Not escalated.
PR body acceptance note 2 — "a requires typed into the metadata editor's JSON view is also left out of the save". ANSWERED: intended. The card's acceptance item 2 says "whatever the served document carried", and the loud refusal the card preserves is the server's, for producers that do not go through these editors. The note also shows the metadata editor can display requires; the changeset's "neither editor shows" is graded loose above. Not escalated.
PR body "Measurements behind the dispatch's assumptions", three bullets. refusalProbeBody uses the same serialiser: TRUE. doPublish sends no body: TRUE. Create mode applies no fromDraft and no page create seed carries requires: TRUE for buildCreateModeBody and for the page registration (no createDefaults, no createSeed); the spec's own create seed was not re-read, and the server stamps an absent key anyway. "No other page writer sends a served page body": TRUE as a conclusion; the ResourceHistoryPage reason is misstated (judgment 5).
The takeover itself: the maintainer's instruction quoted in 5932833214, the assignee swap, and the absent second report. NOTED; the PR body was reviewed as the report, as that claim states, and not trusted on its own account.
Landing state read on the head: the PR is non-draft, mergeable_state: clean, auto_merge null. origin/main has moved past the PR's recorded base 8001068b9c (the branch's merge-base with it is 0a3e5409fb); the queue rebuilds on the current main, and none of the intervening commits is judged here. Nothing to escalate.
Activity
objectstack-fleet commentedon Oct 1, 2026
Triage: first grade —
bug·priority:p2·domain:ui·area:studio·pm:queue. The page editor stops sendingrequires; the server stamps itTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-01T08:58Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2. It was measured: an author who adds a plugin component to a published html page cannot publish, and the refusal names a key the editor never shows.
Direction. It is the card's own scope (the producer changes, the contract does not), confirmed:
fromDraftforpagedropsrequiresfrom the saved body;requires: the server owns the stamp (ADR-0080 §5), and a disagreeing hand-written list is still refused;Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Claim: PM loop round 1
Session:
session_0122Knsowci76D2rBWReCzzZAccount:
os-warren(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-11357-page-save-drops-requiresWorktree:
objectui-issue-11357Domain:
domain:uiSeat:
domain:ui#1File surface (triage's direction
5928148828):pageresource's save serialisation: afromDraftforpagethat dropsrequiresfrom the saved body. The expected home is thepageregistration inpackages/app-shell/src/services/builtinComponents.tsx(theResourceConfig.fromDrafthook,views/metadata-admin/registry.ts).ResourceEditPage.tsxonly if the hook alone cannot carry it; the dev measures. ⛔ No client-side recomputation ofrequires: the server owns the stamp (ADR-0080 §5), and a disagreeing hand-written list is still refused.requireskey), and one.changeset/11357-*.mdfor@object-ui/app-shell.The dev stops on a breach and explains it in the report.
Container & model:
S,mode:subagent,model: opus(default judgment tier; no path-derived mandate for these paths)Clause-②: no
Thread-read: 5928148828
Serial constraints cleared: read 2026-10-01T12:25Z at objectui
origin/main0a3e5409fb. No open PR changesmetadata-admin/page saving orbuiltinComponents.tsx, and nopm:dispatchedclaim names them. The server half is on objectstackmain(PR objectstack-ai/objectstack#21121, merged as250dec8971). Batch 1 (the maintainer's 「当前任务处理完,后续并发改为1」): this dev is the seat's one subagent; PR objectui#11380 (objectui#11368) is in its landing window, which runs no subagent.Scope: the PR says
Fixes #11357.Clause-②: no. Why: the body the page editor sends to the save door changes; no published export, prop or type changes.domain:uiseat 1 · claim · 2026-10-01T12:25ZGenerated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{
"issue": 11357,
"status": "rework",
"branch": "claude/issue-11357-page-save-drops-requires",
"pr": "#11382",
"session": "session_0122Knsowci76D2rBWReCzzZ",
"premise_still_valid": true,
"summary": "The metadata-editor half is done. The
pageresource registered in packages/app-shell/src/services/builtinComponents.tsx now has afromDraft(pageSaveBody). It dropsrequiresfrom the saved body and changes nothing else. It is a no-op that returns the same reference when the draft has norequires, and it never computes one. Every MetadataResourceEditPage door goes through it: Save, autosave, Cmd-S, and the force-save after a destructive-change confirm. The server stampsrequiresfresh on every save. Measured the card's red on base 0a3e540 through the real editor, PagePreview and SourcePageEditor. The save body was {name, label, type: app, kind: html, source with a Kanban, requires: [ui]}. The publish answered 422 INVALID_METADATA, rule page-requires-disagrees-with-source, path pages.pipeline_board.requires: 'plugin-kanban' is used by the source but not listed. Status is rework, not done, because the card's acceptance item 1 says 'In the Studio', and the Studio Interfaces pillar has a second save path for pages. InterfacesPillar.doSave in views/studio-design/StudioDesignSurface.tsx calls client.save(current.type, current.name, draft) with the served draft whole and never reads config.fromDraft. A throwaway probe (deleted, never committed) ran on the fix commit 09b0033 in the pillar's own harness, with page fromDraft registered. The pillar's autosave body after adding a Kanban still carried requires: [ui]. A package publish of that draft hits the same refusal. StudioDesignSurface.tsx is outside the claim's file surface. The order says to stop and explain, so the PR opens asPart of #11357with the remaining half named. See open_questions[0].","tests": "RED on base 0a3e540:
pnpm exec vitest run packages/app-shell/src/views/metadata-admin/ResourceEditPage.pageRequiresStamp-11357.test.tsxgave VERDICT command-exit 1 and 'Tests 4 failed (4)'. Captured body: 'expected { name: pipeline_board, ...(5) } to not have property requires, Received: [ui]'. Captured refusal: '{ message:requiresdisagrees with the source: plugin-kanban is used by the source but not listed., path: pages.pipeline_board.requires, rule: page-requires-disagrees-with-source }'. GREEN after the fix (same command): VERDICT command-exit 0, 'Tests 4 passed (4)'. ABLATION on the committed fix 09b0033:node objectstack/scripts/ablation-replace.mjs --file packages/app-shell/src/services/builtinComponents.tsx --anchor ' fromDraft: pageSaveBody,' --delete -- pnpm exec vitest run (pin). ablation-replace's own verdict lines: 'ok mutation landed: anchor 1 -> 0, blob a4823f9ae2b9 -> 055a3714742e'; 'Tests 4 failed (4)'; 'ok restored: blob == HEAD (a4823f9ae2b9) and git diff HEAD is empty'. Expected direction red, observed red. No dist was involved: the pin imports the source module by relative path. STUDIO PILLAR PROBE (measurement only, deleted, never committed), on HEAD 09b0033 with page fromDraft registered ('function'): the pillar save body was {name: landing, label: Landing, type: app, kind: html, source: h1 plus a Kanban tag, requires: [ui]}. Gates at final HEAD f24ef48: (1)pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build: VERDICT command-exit 0 (29 packages). (2)pnpm --filter @object-ui/app-shell type-check: VERDICT command-exit 0. The script echoed 'tsc --noEmit && tsc -p tsconfig.test.json', andtsc -p tsconfig.test.json --listFilescounts the new pin 1 time. (3)pnpm exec vitest run --maxWorkers=3 packages/app-shell/src/views/metadata-admin/ packages/app-shell/src/views/setup/PackagedAutomationPage.navContribution.test.tsx: VERDICT command-exit 0, 'Test Files 375 passed (375)', 'Tests 4380 passed | 1 skipped (4381)'. The log header records HEAD=f24ef48827. The second path is the only other suitegit grep -lfinds for builtinComponents or fromDraft. (4)pnpm check:new-line-citations: exit 0, 'VERDICT new-cross-file-line-citations: 0 new citation(s)'. (5)node scripts/check-changeset-presence.mjs: exit 0, '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'. (6)pnpm changeset:check: exit 0, 'No changeset declares a major bump'. (7)pnpm check:control-bytes: exit 0, 'OK (scanned 9883 tracked text file(s))'. A self-scan with grep -naP over the 3 files found 0 hits. (8) eslint (narrowed,--format json) on the 2 changed source files: 2 files, 0 errors, 5 warnings, all on existing lines above the edit. eslint.config.js has no parserOptions.project or projectService, so the narrowing cannot change the verdict on untouched files. CI is not awaited.","mcp_calls": "0 (no MCP GitHub tools used)",
"api_writes": "3. (1) pr_create through the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches, which executed POST /repos/objectstack-ai/objectui/pulls (draft) and opened #11382, run 36864738092 success. (2) label-write --assign os-warren through the relay: POST /repos//issues/11382/assignees, run 36864838611 success; read back labels
tests,package: app-shell(from labeler.yml, not written by me) and assignee os-warren. (3) This os-dev-report comment through post-stamped.mjs: POST /repos//issues/11357/comments. Plus git push of 3 refs (not REST).","gates": {
"build app-shell^...": "exit 0",
"type-check @object-ui/app-shell": "exit 0",
"vitest metadata-admin/ + navContribution": "exit 0 (375 files, 4380 passed, 1 skipped) at f24ef48",
"check:new-line-citations": "exit 0 (0 new)",
"check-changeset-presence": "exit 0",
"changeset:check": "exit 0",
"check:control-bytes": "exit 0",
"eslint (2 changed files)": "exit 0 (0 errors, 5 pre-existing warnings)",
"CI": "in_progress / not awaited"
},
"line_budget": "not applicable: the diff touches no skills/** and no governed ledger",
"files_changed": [
"packages/app-shell/src/services/builtinComponents.tsx (+23)",
"packages/app-shell/src/views/metadata-admin/ResourceEditPage.pageRequiresStamp-11357.test.tsx (+341, new)",
".changeset/11357-page-save-drops-requires.md (+22, new, @object-ui/app-shell patch)"
],
"deviations": [
"The PR's first line is
Part of #11357, not theFixes #11357that Zone 3 suggested. Acceptance item 1 ('In the Studio') is not met on the Studio Interfaces pillar, which is outside the claimed file surface. The changeset names that scope limit too.","Commit trailers use the model-free pair from AGENTS.md (Claude-Session plus Co-authored-by Claude). The harness reminder asked for a model-named Co-Authored-By line; AGENTS.md takes precedence over it.",
"The PR body ends with the os-dev session-URL footer form rather than the harness reminder's two-line block."
],
"open_questions": [
{
"question": "Widen the file surface to views/studio-design/StudioDesignSurface.tsx so the Studio Interfaces pillar's page save also leaves
requiresout? The pillar is the second producer of the same defect; it was measured, and it is outside this claim's surface.","options": [
"A: Next round on this PR. InterfacesPillar.doSave serialises a page through the registered
fromDraft(getMetadataResource(current.type)?.fromDraft, or a page-only call; the pillar never appliestoDrafton load, and zero types register either hook today, so a page-scoped call avoids that asymmetry). Add one pin in the existing StudioDesignSurface.interfacesReadOnly-11136 harness, and flip the PR's first line toFixes #11357. Cost: a diff of a few lines plus one pin; no new gate.","B: Land this PR as
Part of #11357, and the seat files a sibling card for the pillar save path. Cost: two PRs and two review rounds for one defect, and the card stays open in between.","C: Export a shared page save-body helper and call it from both editors. Cost: a new export surface, with no gain over A."
],
"recommendation": "A, judged on the four axes. Real need: it is measured, and pages open in the Interfaces pillar (artifactStudioPath routes a page there), so that is the door Studio authors use. Long-term soundness: the registry's
fromDraftstays the single save serialiser that both editors honour, instead of a second copy. AI-error prevention: the server's loud refusal of a hand-written disagreeing list stays untouched under every option. No scope creep: no new capability and no new gate, and the change stays inside the same card and the same defect class."}
],
"out_of_scope_findings": []
}
objectstack-fleet commentedon Oct 1, 2026
Claim amendment: the file surface widens to the Studio Interfaces pillar's page save (same defect, second producer)
domain:uiseat 1 ·session_0122Knsowci76D2rBWReCzzZ· 2026-10-01T12:57Z. This amends the claim5931404711. The state and assignee are unchanged. ⛔ Not a new claim.5931919673measured a second producer of this card's defect.InterfacesPillar.doSave(packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx) callsclient.save(current.type, current.name, draft)with the served draft whole and never readsconfig.fromDraft. A probe on the fix commit09b0033813, with thepagefromDraftregistered, captured the pillar's autosave body after adding a Kanban: it still carriedrequires: ['ui']. Pages open in that pillar from the Studio, and acceptance item 1 says "In the Studio".5928148828unchanged):pageregistration'sfromDraft, so the registry stays the single save serialiser both editors honour. ⛔ No second copy of the drop.toDrafton load, so it must not apply a type'sfromDraftwhere that type also registers atoDraft(view's family fold is the example such a pair exists for). A page-scoped call, or a generic call guarded on "this type registers notoDraft", is the dev's choice, said in the PR and pinned.requires.packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx(InterfacesPillar.doSave, page save body only), plus one pin in that surface's existing harness.studio-design/orbuiltinComponents.tsx, and nopm:dispatchedclaim names them.Fixes #11357.domain:uiseat 1 · claim amendment · 2026-10-01T12:57ZGenerated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Claim: PM loop round 1 (takeover from
domain:uiseat 1)Session:
session_01JG2jy8a9su7ia4Hx7zxv42Account:
os-sales(the seat's linked user asGET /useranswers it; the card's assignee from this write on)Branch:
claude/issue-11357-page-save-drops-requiresWorktree: none. No dev is re-dispatched. The seat reviews PR objectui#11382's head by git and lands it.
Domain:
domain:uiSeat:
domain:ui#2File surface: unchanged. It is claim
5931404711as amended by5931952676: thepageregistration'sfromDraftinpackages/app-shell/src/services/builtinComponents.tsx,InterfacesPillar.doSaveinpackages/app-shell/src/views/studio-design/StudioDesignSurface.tsx, their two pins, and one.changeset/11357-*.md.Container & model:
S,mode:subagent(no subagent runs: review and landing only),model: opus(default judgment tier;dispatch-gates --tier --repo objectstack-ai/objectuiprints "no path-derived mandate" for these paths)Clause-②: no
Thread-read: 5931952676
Serial constraints cleared: read at objectui
origin/main8001068b9c. Of the 4 other open PRs (objectui#11377, objectui#11316, objectui#11069, objectui#5400), none changesstudio-design/StudioDesignSurface.tsx,services/builtinComponents.tsxormetadata-admin/ResourceEditPage.tsx. No otherpm:dispatchedclaim names them.Release: claim
5931404711(amended5931952676), sessionsession_0122Knsowci76D2rBWReCzzZ, accountos-warren· cause: the claimant is unreachable; the maintainer reports that seat 1's session has no tokens left · destination:domain:uiseat 2, accountos-sales, this session · provenance: the maintainer's instruction, verbatim 「seat 1 也没有 token了。11382 你也接手」, given in this session's chat on 2026-10-01.Assignee: swapped
os-warren→os-saleson this card and on PR objectui#11382, in the label write that goes with this comment.Handover record: the branch's last pushed sha is
25db06c522(the changeset naming both editors), pushed 2026-10-01T13:02Z. Status: the dev's rework round is complete on the branch: the Interfaces pillar fix0823fd1f93follows its red pin170eb4c835, and the first line now saysFixes #11357. Every check on25db06c522concludedsuccessor the expected skip. The dev's second report never reached this card, because seat 1's session died with it. So this seat takes the direct-acceptance fallback: the draft PR is present with CI green, the report is absent, and the death is session-wide. The PR body is the report. The review follows in its own comment.domain:uiseat 2 · takeover claim · 2026-10-01T13:50Zobjectstack-fleet commentedon Oct 1, 2026
ACCEPT: PR objectui#11382 (
Fixes #11357), head25db06c522domain:uiseat 2 ·session_01JG2jy8a9su7ia4Hx7zxv42· 2026-10-01T13:52Z. Reviewer of record after the takeover5932833214. Route: direct acceptance. The draft PR is present with CI green, the dev's second report is absent, and the death is session-wide. The PR body is the report, and every line below was checked against GitHub or the PR head by git, not against the body's own account.Shape. Draft, base
main. The first line isFixes #11357, and a scan of the whole body finds no other closing keyword. 5 files, +479 / −1 (under the 5000-line human-merge threshold). Nothing undercontent/docs/releases/, and no file outside the amended claim's surface.Path fork.
node scripts/check-governed-queue-guard.mjs --test(objectui) answers NOT GOVERNED for all 5 paths, so the PR takes the merge-queue route. Clause ②: the path limb misses (nopackages/spec/**) and the claim declaresno. Both helpers,pageSaveBodyandinterfacesSaveBody, are module-private (agit grepfor anexportof either on the head finds nothing), so no published export, prop or type moves.Diff, read at the head:
builtinComponents.tsx: thepageregistration gainsfromDraft: pageSaveBody. It drops onlyrequires, returns the same object when the key is absent, and never computes one.StudioDesignSurface.tsx:InterfacesPillar.doSavesendsinterfacesSaveBody(current.type, draft). Only apagegoes through the registry'sfromDraft, so there is one serialiser and no second copy of the drop. The page-only scope honours the amendment's "nofromDraftwithout itstoDraft" guard, and a pin holds the precondition thatpageregisters notoDraft.packages/app-shell/src/index.ts:311imports./services/builtinComponents.jsfor its side effect.git grepfor.save(overpackages/app-shell/src,apps/console/srcandpackages/plugin-*/srcon the head finds no third writer of a served page body.runtime-metadata-persistenceis typed view / report / dashboard,doCreateAppwrites anapp, and the rest name their type literally.Changeset.
.changeset/11357-page-save-drops-requires.mdgives@object-ui/app-shella patch (itspackage.jsonhas noprivate, so it is published), with user-facing prose. Thechangeset-claim-re-readnote names.changeset/6681-…md's sentence that the eagerStudioDesignSurface.tsxstatically importsSuggestedBindingsPanel. That stays true: this PR adds one import ofmetadata-admin/registry.jsand moves no chunk. Console eager closure 3552.4 KB against a 3574.6 KB budget: PASS.Tests (the body's evidence, with its pins on the head). Metadata editor: 4/4 red on base
0a3e5409fb, 4/4 green, and 4/4 red again when thefromDraftanchor is ablated. Interfaces pillar: committed red first (170eb4c835, 1 failed of 12), green after0823fd1f93(12/12), and the ablation of theinterfacesSaveBodycall turns only the pin red. The publish pin uses the real door functions (stampHtmlPageRequires,findHtmlPageSourceGaps) from objectstackorigin/mainf0cc16e8d5, so the 422 reaches its assertions through the real refusal, not a hand-written one.CI, pinned to head
25db06c522. 43 check runs.LintandType Checkconcludedsuccess, and so did every other check except three expected skips:Test (coverage), the unexpandedTest (coverage shard ${{ matrix.shard }}/4)placeholder, anddependabot.Out-of-scope findings, one line each:
requirestyped into the JSON view is also left out": Acceptance notes. The server stamps from the source, so this is the intended behaviour.Deviations. None from the amended claim. The branch is 1 commit behind
main(8001068b9c, the datasource editor, which touches none of these files). The merge queue rebuilds on the currentmain.Landing. Next:
pr_ready+automerge_enablethrough the fleet relay. After MERGED, the seat verifies the change by content onorigin/main, closes this card ifFixesleaves it open, and stripspm:dispatched.domain:uiseat 2 · ACCEPT · 2026-10-01T13:52ZGenerated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Contract review
Served-tier:
CONTRACT_REVIEW_TIERHead-sha:
25db06c522505e65c661f82098a34e6522d05fe8Local-runs: none
Inputs read: card #11357 (its body and all six comments,
5928148828through5932883731), PR #11382 (its body, its five-file list, and the net three-dot diff of the head againstmain, merge-base0a3e5409fb, fetched read-only into a ref this session owns), and the 44 check-runs on the head. The objectstack sibling clone was read, never run, for two contract texts the card cites: the page schema'srequiresdescription and the save door'sstampHtmlPageRequires/findHtmlPageSourceGaps. Nothing was built, run, re-run or ablated. The dispatching seat's ACCEPT comment5932883731was read as a comment on the card; none of its conclusions is adopted below. Every line was re-derived from the diff or the head.① Derived judgments
The diff, hunk by hunk, and what each implies.
pageSaveBodyinpackages/app-shell/src/services/builtinComponents.tsx, registered as thepageresource'sfromDraft— RIGHT. It removes exactly one key,requires, from a shallow copy; a draft with no ownrequirescomes back as the same reference; it never computes a list; the draft object is not mutated. Thepageregistration gains nothing else (notoDraft, nocreateDefaults). This is the card's prescribed fix verbatim ("afromDraftforpagethat drops it"), and the card's "not a client-side recomputation" bound holds.interfacesSaveBodyinpackages/app-shell/src/views/studio-design/StudioDesignSurface.tsx, applied insideInterfacesPillar'sdoSave— RIGHT. Onlytype === 'page'goes throughgetMetadataResource('page')?.fromDraft; every other type is sent as its draft. One serialiser, no second copy of the drop. The page-only scope honours the amendment's guard: the pillar applies notoDrafton load, and on this head no registration anywhere underpackages/app-shell/srcdeclares atoDraft(the only matches are the registry's type and comments), sofromDraftis applied where no inverse is owed; the new precondition pin goes red the day apagetoDraftappears. The registry is filled by the package entry's side-effect import of./services/builtinComponents.js, which is how the pillar reaches it at save time; the pillar pin imports it the same way.MetadataResourceEditPagehas one save function,doSave(force), and in edit mode it serialises throughconfig.fromDraft; the toolbar Save callsdoSave(false), the destructive-change dialog's force-save callsdoSave(true), and the Cmd/Ctrl-S keydown and the autosave timer both call throughdoSaveRef.refusalProbeBodyapplies the sameconfig.fromDraft, so the issue paths are read against the body the save sends.doPublishsends no body. Create mode goes throughbuildCreateModeBody, which applies nofromDraft; that is outside the defect, because the server stamps an absentrequireson its own.doSaveis thesaveof the leaf'suseDraftAutoSave;doNavSavewrites'app'by literal;flushNavSaveserves only the nav save. The leaf draft is seeded fromclient.layered(...).effectiveor the pendinggetDraftbody, stamp included, so the defect's precondition in the pillar is exactly as the card describes it.client.save(underpackages/app-shell/srcandapps/console/srcnames its type by literal ('object','app','flow','hook','permission') or isEmbeddedItemEditor's PUT of anobjectparent.runtime-metadata-persistence'sRuntimeArtifactTypeDOES include'page', but everypersistRuntimeMetadata/createRuntimeMetadatacaller on the head passes'view'or'report'; the one variable-typed call ispublishRuntimeMetadata, which sends no body.ResourceHistoryPageDOES expose a per-event Rollback (doRollbackcallsclient.rollback(type, name, version), and the page passesonRollback), contrary to the PR body's "exposes no rollback" — but it sends no body, so it is not a page-body writer either. Immaterial to the verdict; named so the body's sentence is not re-read as a reading.page-requires-disagrees-with-source. After: both editors omit the key, the server stamps from the source on every save, and the publish passes. This PR touches no server code, so the server's refusal of a disagreeing hand-written list (API, CLI, metadata files) is untouched; the card's "an AI that hand-writes a wrongrequiresstill gets the loud refusal" bound holds at the door it names.kind, while the door'sCOMPILED_PAGE_KINDSishtml/jsx, so for areactor record page an author-typedrequireswould now be left out of both editors' saves. The spec describes the key as "Plugin namespaces the JSX source references (validated at save and load)" — a derived key, not an authored one — and the card's acceptance item 2 says "whatever the served document carried". Not a defect; the PR body's first acceptance note is the right reading. Its "no example page in objectstack hand-writesrequires" is the dev's reading, not re-taken here, and does not carry the judgment.pageSaveBodyandinterfacesSaveBodyare module-private (noexportof either anywhere on the head);ResourceConfiginregistry.tsis untouched; no prop, type or export moves. One new intra-package import (../metadata-admin/registry.jsintoStudioDesignSurface.tsx);Bundle AnalysisandDocs Route Eager Closure Checkboth concludedsuccess.MetadataResourceEditPage, the realPagePreviewandSourcePageEditor(Monaco mocked to its textarea fallback) against a server DOUBLE: the pin re-implements the door's two rules, it does not import them, whatever "applies the save door's two functions" in the PR body suggests. Compared with the gate's source, the double holds the same stamp rule (requiresabsent or agreeing is stamped from the compiled source; disagreeing is left as written on a draft) and the same publish refusal (the rule, thepages.NAME.requirespath, the "is used by the source but not listed" clause). The double carries no manifest, so the door's "no component in this deployment's manifest carries" clause is unexercised; none of the four cases needs it. Case 1 is acceptance item 1 end to end, case 3 is acceptance item 3 (the control), case 4 is acceptance item 2 on the serialiser; the pillar pin's first case is acceptance item 2 on the second producer, beside adashboardcontrol and the precondition case. The PR body's red, green and ablation figures are the dev's readings; the check-runs below are the gate verdicts this record relies on.The changeset, sentence by sentence (
.changeset/11357-page-save-drops-requires.md,'@object-ui/app-shell': patch):type(scope):headline is a minority but established form among this repo's changesets.MetadataResourceEditPage) and the Studio Interfaces pillar." — TRUE; judgments 3 to 5.requireslist the server stamped from the page's source." — TRUE; the metadata editor seeds from the served row (config.toDraftapplied when present), the pillar fromlayered(...).effectiveor the pending draft.requires: ['ui']beside a Kanban source).plugin-kanban, say), the draft saved but the publish was refused (422 INVALID_METADATA, rulepage-requires-disagrees-with-source)." — TRUE; it is the card's measured refusal, under the door's own rule name.SourcePageEditor, the pillar's leaf editor) presentsrequiresas a field, which is what the card recorded ("something they cannot see or fix in the editor"). But the metadata editor's inspector has asourcetab that rendersJsonSourceEditorover the whole draft,requiresincluded, and the PR body's own second acceptance note relies on that tab. A precise spelling is "a key neither editor presents as a field". It does not change what the entry announces, so it does not move the verdict; it is named here so the release-notes compiler can tighten it and so nobody re-reads it as a reading.pageresource now leavesrequiresout of the body it saves, and both editors save a page through it, so the server stamps the list from the source on every save." — TRUE of the diff; judgments 1 to 4.requiresitself." — TRUE; nothing in the diff builds a list.findHtmlPageSourceGapson the objectstack side reads as the card cites it.Check-runs on
25db06c522— 44 runs: 41 concludedsuccess, 3skippedas expected (Test (coverage), the unexpanded coverage-shard matrix placeholder,dependabot).Lint,Type Check,Testand all eightTest (shard N/8),Build & E2E,Changeset Declaration,Changeset Bump Policy,Changeset Fixed Group Check,Changeset Claim Re-read,Line Citation Gate,Control Byte ScanandGoverned Surface Queue Guard(run twice, the second after the PR left draft) allsuccess. No path in the five-file list is a governed surface.② Semver level
patchon@object-ui/app-shell— RIGHT. The package is published (itspackage.jsoncarries noprivate) and sits in the.changeset/config.jsonfixed group. The newestClaim:on the card, the takeover claim5932833214, declaresClause-②: no, as did the original claim5931404711; the PR body's second line isClause-②: no. The diff adds no export, prop or type and retires none (judgment 8); it changes the body one package sends to the save door. A bug fix in a released package takespatch; nothing is widened, sominoris not owed; nomajorunder the fixed-group rule (Changeset Bump Policysuccess). No migration text is owed: nothing an author can write is removed or renamed, and a hand-writtenrequiresin a metadata file is judged by the server exactly as before.③ Boundary flags
The dev's first report is
5931919673(statusrework). The dev's second report never reached the card (seat 1's session ended), so per the takeover claim5932833214the PR body is the second report. Every flag in both, and every open question:deviations[0]— "first linePart of #11357; acceptance item 1 not met on the Studio Interfaces pillar". RESOLVED on the head: the pillar fix0823fd1f93follows its red pin170eb4c835, the PR's first line isFixes #11357, and a scan of the whole body finds no other closing-keyword-plus-number pair (the only otherfixhit is the prose "after the fix: 4 of 4", with no#).deviations[1]— commit trailers carry the model-free pair rather than the harness reminder's model-named line. ACCEPTED: all five commits on the branch end with theClaude-Session:plusCo-authored-by: Claudepair both repos' AGENTS.md prescribe; the harness reminder yields to it by its own precedence rule.deviations[2]— the PR body ends with the session-URL footer form. ACCEPTED: that is the form AGENTS.md assigns to PR bodies; the body also carries the session URL in prose, as recommended.open_questions[0]— widen the surface toStudioDesignSurface.tsx(A / B / C, recommending A). ANSWERED by the seat's amendment5931952676(A, page-scoped, pinned) and IMPLEMENTED asinterfacesSaveBodywith the precondition pin; judgments 2 and 9. The amendment's "nofromDraftwithout itstoDraft" guard is honoured by scope and by pin.out_of_scope_findings— empty in the first report; none in the second.requirestyped into the metadata editor's JSON view is also left out of the save". ANSWERED: intended. The card's acceptance item 2 says "whatever the served document carried", and the loud refusal the card preserves is the server's, for producers that do not go through these editors. The note also shows the metadata editor can displayrequires; the changeset's "neither editor shows" is graded loose above. Not escalated.refusalProbeBodyuses the same serialiser: TRUE.doPublishsends no body: TRUE. Create mode applies nofromDraftand no page create seed carriesrequires: TRUE forbuildCreateModeBodyand for thepageregistration (nocreateDefaults, nocreateSeed); the spec's own create seed was not re-read, and the server stamps an absent key anyway. "No other page writer sends a served page body": TRUE as a conclusion; theResourceHistoryPagereason is misstated (judgment 5).5932833214, the assignee swap, and the absent second report. NOTED; the PR body was reviewed as the report, as that claim states, and not trusted on its own account.mergeable_state: clean,auto_mergenull.origin/mainhas moved past the PR's recorded base8001068b9c(the branch's merge-base with it is0a3e5409fb); the queue rebuilds on the currentmain, and none of the intervening commits is judged here. Nothing to escalate.Implemented-by:
claude/issue-11357-page-save-drops-requiresReviewed-by:
session_01JG2jy8a9su7ia4Hx7zxv42VERDICT: PASS
Reviewed-at: 2026-10-01T14:10Z
Generated by Claude Code
fix(app-shell): both page editors leave `requires` out of the body th…