Repository navigation
metadata: refuse to save or load a page whose requires names a plugin that is not loaded (1 key) #20312
Description
Activity
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsPath: changing a running app without code | 缺项 (no item saves a page whose
requiresnames an unloaded plugin) | P1Triage: first grade —
enhancement·priority:p3·domain:spec·area:studio·pm:queue. Verdict: ENFORCE, by the maintainer's criterionTriage: the reader lands in
packages/metadata-protocol(the save door) and the runtime load path: comparepage.requiresagainst the loaded plugin namespaces (ADR-0080 M3b) ⇒domain:specparent, with a metadata-protocol sub-issue. Rationale: a page saved against a missing plugin fails only at render time, for the end user. No measured author ⇒ p3.Triage seat (objectstack-wide, seat post #6015) ·
session_01W89enF2dYV7K4N2Fbfj33f· 2026-09-27T22:18Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card (no comments), the criterion on #18900 (5727134555), and the family grades applied on #20273 onward.Verdict. Dependency checks at save or deploy time are mainstream: Salesforce deploy validation fails a Lightning page that references a missing component, and a Power Apps solution import blocks on missing dependencies. ⇒ ENFORCE. One word from the maintainer reverses it before dispatch.
Execution notes.
- Refuse at the save door, and report at load, a page whose
requiresnames a namespace no loaded plugin provides, with a prescription naming the plugin. - Pin one refused save and one load report, with a page whose
requiresis satisfied as the control. - The ledger row flips to
live.
- Refuse at the save door, and report at load, a page whose
- addedarea:studioChanging a running app without code — authoring, publish, docs and the portalChanging a running app without code — authoring, publish, docs and the portalenhancementNew feature or requestNew feature or requestand removed
on Sep 27, 2026 objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_014EJ1ED8X4MMrT18BhVx4tx
Account:os-tesla(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-20312-page-requires-enforced
Worktree:objectstack-issue-20312
Domain:domain:spec(parent; thepackages/metadata-protocolsave-door half is carried in this one PR, as triage5860357340routes it: "domain:specparent, with a metadata-protocol sub-issue")
Seat:domain:spec#2(seat post #18549)
File surface, per the card and triage5860357340(ENFORCE):- the save door in
packages/metadata-protocol/src/**: refuse a page whoserequiresnames a namespace no loaded plugin provides, with a prescription naming the plugin; - the runtime load path: report the same at load (the dev measures where pages are loaded);
packages/spec/src/ui/page.zod.ts(~:903): therequiresdescribe text only, if it still says enforcement is deferred;packages/spec/liveness/page.json: therequiresrow flips tolive, citing the reader;packages/spec/liveness/state-counts.md, regenerated;- their tests and
.changeset/20312-*.md.
(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate). The diff narrows what the save door accepts, so the at-tier review is owed before enqueue.
Clause-②: no
Thread-read: 5860357340
Serial constraints cleared: read at 2026-09-28T16:31Z againstorigin/main75b2169243. No open PR touchespackages/spec/src/ui/page.zod.ts,packages/spec/liveness/page.jsonorpackages/metadata-protocol/src/**(open-PR file lists read at this stamp). This seat's in-flight finding(metadata-protocol): a query carryinggroupBy/aggregationssilently dropssearch— grouped counts under a search are the unsearched counts #20358 editspackages/metadata-protocol/src/protocol.tsinfindData's grouped branch: same-file ordinary concurrency if the save door is in that file too; the later lander merges.liveness/state-counts.mdis the shared hot file (liveness/state-counts.md's shared total row makes every liveness-touching PR dirty whenever another lands, so its CI never starts until a merge-and-regenerate round #20361): expect a merge-and-regenerate round per sibling landing.
Generated by Claude Code
- the save door in
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20312,
"status": "needs_decision",
"branch": "claude/issue-20312-page-requires-enforced",
"pr": null,
"session": "session_014EJ1ED8X4MMrT18BhVx4tx — the dispatching PM session (subagent = parent's); claim 5874283305, verified as the newest Claim: on #20312, names this branch",
"premise_still_valid": false,
"summary": "No code: the dispatch's own stop condition (mechanism hypothesis 2) holds — no registry answering 'is namespace N provided by a loaded plugin' is reachable from the save door or from the load path, so ENFORCE cannot be executed without a public-contract decision (open_questions). Measured at objectstack 75b2169 and the objectui pin dd3f7e1be3: page.requires holds objectui RENDERER namespaces taken from the SDUI manifest (17 in repo-root sdui.manifest.json: ui, record, page, element, action plus 12 plugin-*), not server plugin or package namespaces (manifest.namespace's regex admits no hyphen); the runtime save door saveMetaItem -> assertRuntimeAuthoringRules (metadata-protocol/src/protocol.ts:4972) -> evaluateRuntimeAuthoringGate (runtime-authoring-gate.ts:576) declares an sduiManifest input (:625) that its only caller (protocol.ts:5148) never passes, validateJsxPages is surfaces CLI_ONLY (lint/src/authoring-rules.ts:1049-1057, pinned off the runtime by runtime-lazy-deps.test.ts:403), no serving host resolves a manifest (resolveSduiManifest is CLI-only and its console leg resolves to nothing: @objectstack/console exports only ./package.json), and loadMetaFromDb (diagnose-never-drop) has the same absence. Producers 0 and readers 0: 0 of 25 .page. files and 0 of 41 page-defining source files author requires; sdui-parser/src/index.ts#compile infers it but both callers (lint validateJsxPages at build, objectui PageRenderer at render) discard it; the 3 in-repo html pages infer only ['ui']; every .requires read in packages//src is stack-level, and the objectui pin reads none. Consequence for every ENFORCE shape: a requires INFERRED against the manifest it is judged against is a subset of the provided set by construction (an absent plugin's tag is refused by compile()'s component whitelist and has no namespace to infer), so the save-time refusal the ruling wants is carried by the whitelist, and the one non-vacuous requires reader is the load-time check after the deployment's manifest shrank since save — the only reader that can name the missing plugin.",
"tests": "No suite owed (no diff). Measurements: sdui-parser build under os-verify-lock (VERDICT command-exit 0, waited 8m21s, held 3s). Scratch compile of the showcase html pages against repo-root sdui.manifest.json via sdui-parser dist: command-center-jsx ok=true requires=['ui'] unprovided=[]; start-here ok=true requires=['ui'] unprovided=[]; capability-map uses template interpolation so the regex extraction is invalid for it — its witness is lint validate-jsx-pages.production-witness.test.ts (validateJsxPages with the manifest answers []). Control: provided.has('plugin-kanban')=true, provided.has('plugin-nonexistent')=false. Producer census: git grep -c requires over the 25 .page. files = 0 hits, control git grep -c source lit on 5 files; over 41 page-defining source files 6 hits, each inspected: prose or stack-level. Reader census: .requires / ['requires'] reads in packages//src non-test = 13 hits, all stack-level (config, artifactStack, stack.zod d.requires) or sdui-parser's own return value; objectui at the pin: 0 page .requires reads, control compile(src, getJsxManifest()) lit (1). No ablation (nothing to ablate).",
"mcp_calls": "0",
"api_writes": "1 — POST /repos//issues/20312/comments (this os-dev-report, through scripts/pm/post-stamped.mjs). The empty-branch git push is not a REST write.",
"gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands -> EXIT=2, 'this branch changes nothing against origin/main (merge base 75b2169) — nothing to derive'; no gate owed, none run, --ran reconciliation not applicable. Cost reading only: the same tool over option B's 8 hypothetical paths (metadata-protocol protocol.ts and runtime-authoring-gate.ts, lint validate-jsx-pages.ts and authoring-rules.ts, cli serve.ts, console package.json, spec page.zod.ts and liveness/page.json) -> EXIT=0, 89 gate commands (74 excluding --self-test).",
"line_budget": "not applicable — no diff: 0 files, 0 lines; no governed surface touched",
"deviations": [
"No PR, code, changeset or ledger flip: dispatch hypothesis 2's stop condition met (no provided-namespace registry reachable from the save door or load path) => needs_decision, per the dispatch's instruction not to invent a second registry.",
"Branch pushed empty (the rule-1 write probe, identical to origin/main 75b2169); pr_create and label-write (PR assignee os-tesla) not spent — no PR exists. Worktree removed after this report; an ENFORCE round re-creates it from the pushed branch.",
"Hypothesis 2 falsified in part: requires is inferred by @objectstack/sdui-parser compile(), but NOT at save — nothing at the runtime save door compiles html source; compile's callers are the CLI build gate (os validate/build/lint, only when a manifest resolves) and objectui's render-time PageRenderer. Hypothesis 3 confirmed (0 page.requires reads). Hypothesis 4: save door = saveMetaItem -> assertRuntimeAuthoringRules (protocol.ts:4972); load = loadMetaFromDb in protocol.ts (convention: diagnose, never drop — [metadata_spec_invalid] warn, row still registered), so a load-side requires check would be a report, not a refusal."
],
"files_changed": [],
"open_questions": [
{
"question": "Q1 — Where does the runtime obtain the set of renderer namespaces this deployment provides (the SDUI manifest ADR-0080 §5 calls deployment-scoped), which both the save door and the load path must judge against? Today nothing holds it at runtime: the gate's sduiManifest input is unthreaded and no serving host resolves a manifest.",
"options": [
"(a) The serving host reads the manifest of the console it serves: @objectstack/console publishes ./dist/sdui.manifest.json in exports; os serve and the standalone host pass it through the metadata-protocol assembly into the gate's existing sduiManifest input; a host serving no console states none and prints ONE boot notice that page source and requires are unchecked, never a silent pass. Business: the one artefact that really is 'what this deployment renders'. Long-term: reuses the one registry ADR-0080 names; the deployment fact enters the way orgWallEnforced and judgeFilter do (gathered impurely, passed to a pure gate). Anti-AI: arms full compile at the only door a Studio, MCP or AI author has. Startup: M — cli serve, runtime standalone host, metadata-protocol assembly option, console exports; cloud per-project kernels owe their own threading.",
"(b) os build stamps the resolved manifest's namespace set into the compiled artifact, and the runtime reads it from there. Business: ties the check to the build rather than to the console actually served — the two diverge on a console pin bump without a rebuild. Long-term: a second copy of the manifest per artifact, a drift surface. Anti-AI: as (a) for artifact pages; runtime-authored pages judged against a build-time snapshot. Startup: M plus an artifact-format change.",
"(c) None — take Q2 option C and keep the manifest off the runtime."
],
"recommendation": "(a), because it is the only source that answers 'provided by THIS deployment' without inventing a registry, it follows the gate's established host-fact pattern, and its absence is made loud rather than a silent pass (AGENTS.md route-ownership rule 3)."
},
{
"question": "Q2 — Given a runtime manifest, which ENFORCE shape does page.requires take? Measured constraint: an INFERRED requires is a subset of the manifest it is judged against by construction, so a save-time requires check can only fire on an author-typed entry; the non-vacuous reader is the load-time check after the manifest shrank since save.",
"options": [
"A — keep requires authorable: the save door refuses (422 INVALID_METADATA, a new rule id) an author-typed entry the manifest does not provide, naming the namespace; load reports the same (warn, diagnose-never-drop). Business: fires only on hand-typed entries, and 0 in-repo producers type one. Long-term: an author-writable projection of source that can disagree with it (two sources of truth). Anti-AI: invites an AI to author the value, then polices only half of the disagreement. Startup: M on top of Q1.",
"B — ENFORCE as ADR-0080 §5 specifies: cross validateJsxPages onto the runtime page write door (sdui-parser has zero runtime dependencies, so RUNTIME_HEAVY_SOURCE_PARSE's typescript/sucrase reason is measured not to apply; the runtime-lazy-deps pin moves with it), so the component whitelist refuses an absent plugin's block at save; the save door STAMPS requires from compile() (source is truth; an authored value that disagrees is refused); load (loadMetaFromDb and artifact load) reports a stored page whose requires names a namespace the booted manifest lacks, naming that plugin. Business: the mainstream capability end to end (Salesforce deploy validation, Power Apps import-time dependency check). Long-term: requires becomes a derived cache like compiledTree, one source of truth. Anti-AI: strongest — an unknown or absent-plugin tag is refused at save, and an AI cannot author a requires that disagrees with its source. Startup: L — Q1(a) plus the rule crossing (every active html page save starts paying a full compile and can newly 422, so the precedent that crossed the object rules owes a false-positive measurement first) plus the describe change; 89 derived gate commands on its 8-path surface.",
"C — RETIRE page.requires (the maintainer's one-word reversal under ruling A′ ④): tombstone per the spec-property-retirement playbook, ledger row to its retired disposition, the published 'validated at save and load' claim removed (page.zod.ts:904, content/docs/references/ui/page.mdx:187); the save-time dependency check itself (whitelist against the deployment manifest) filed as its own card on Q1(a). Business: 0 producers, 0 readers, value derivable at any time from source plus the manifest. Long-term: no stored projection to drift; loses only the load-time naming of a vanished plugin. Anti-AI: removes a declared-never-enforced key an AI can fill today. Startup: S-M, immediate.",
"D — build door only, now: judge author-typed requires inside validateJsxPages when a manifest resolves (os validate/build/lint) and narrow the describe to that. Business: rarely fires — most projects resolve no manifest (the console leg is unreachable). Long-term: leaves the save and load halves open. Anti-AI: weak — Studio, MCP and AI authors never reach it. Startup: S, but the ledger row cannot honestly become live for save and load."
],
"recommendation": "B via Q1(a), because it is the only shape whose reader is both non-vacuous and names the missing plugin (load), and whose save-door refusal is the whitelist ADR-0080 §5 already ruled server-side — the 'build the consumer once, correctly' reading of the maintainer's criterion. If the maintainer judges the runtime manifest too wide for this stage, C is the honest alternative; never D, which keeps a published claim the runtime does not honour."
}
],
"out_of_scope_findings": [
"carrier: #20312's own decision (Q2 option B crosses it) · noted, not filed — validateJsxPages is held CLI_ONLY under RUNTIME_HEAVY_SOURCE_PARSE ('parses authored source through typescript/sucrase', lint/src/authoring-rules.ts:423-425), a reason shared with validateReactPages that does not apply to it: @objectstack/sdui-parser declares no runtime dependencies and every src import is type-only or package-local (measured), so the runtime save door runs no html source parse, against ADR-0080 §5 '[ruled] Authoritative parse + sanitize + validate + compile runs server-side at save'. Seam: spec:PageSchema.source (kind html) -> runtime:assertRuntimeAuthoringRules (protocol.ts:4972) | renderer:objectui PageRenderer (compile at render). Dedupe words: validateJsxPages runtime surface, RUNTIME_HEAVY_SOURCE_PARSE, html page save compile, ADR-0080 server-side save"
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsBack to the decision box — premise falsified ·
domain:specseat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-28T16:58ZRelease:
session_014EJ1ED8X4MMrT18BhVx4tx(claim5874283305) · cause: premise falsified — the dev report5874605425measured that no set of provided plugin namespaces reaches the save door or the load path, so the ENFORCE execution note has no reader to compare against · destination:needs-user-decision;pm:dispatchedis replaced and the assigneeos-teslacleared in this act. Nothing is in flight: the pushed branchclaude/issue-20312-page-requires-enforcedcarries no commit of its own.The seat re-ran the report's load-bearing readings on
origin/main8e02859185(commands below) before writing this. The analysis that follows is the seat's own; it does not adopt the dev's lettering.
维护者裁决(一个字母)·
page.requires怎么兑现一句话问题:平台对外说「页面依赖的插件会在保存和加载时校验」,实际上从不校验。在 Studio、MCP 或 AI 保存一个用了本部署没装的插件组件的页面,保存照样成功,等最终用户打开页面才坏。原定的修法是「保存时拿
requires去比对已加载插件」,但运行时根本没有「本部署提供了哪些组件命名空间」这份清单,所以原修法无从下手。背景:本卡是 ruling A′ ④(#18900
5727134555)下的 declared≠enforced 族卡。分诊按维护者判据给了 ENFORCE,原文写明「维护者一个字即可在派发前推翻」(5860357340)。本席派发后,dev 实测证伪了执行前提(5874605425):requires里放的是 objectui 渲染器的命名空间,来自 SDUI manifest(ui、record、plugin-kanban等),不是服务端插件名;而服务端任何宿主都拿不到这份 manifest。Governing text:
- ADR-0080 §5 [ruled](ADR 整体仍是 Proposed,且自注「save-time … pipeline NOT wired」):「Authoritative parse + sanitize + validate + compile runs server-side at save … Store
{ source, compiledTree, requires }— source is truth, tree is derived cache.」 - ADR-0080
:110:「requiresis inferred at parse and validated at save and load (plugin presence …)」;:142:「the manifest andrequiresvalidation are deployment-scoped (a page referencing an absent plugin's block fails the gate, not the render — loud-not-silent)」。 - 维护者判据(本卡正文引):「每族该问的是:主流平台有没有这个能力 —— 有 ⇒ 补消费端(一次做对);没有 ⇒ 退役,而不是看仓里有没有人读」。
- 对外声明:
packages/spec/src/ui/page.zod.ts:904与content/docs/references/ui/page.mdx:187:「validated at save and load」;账本packages/spec/liveness/page.json:9为planned。
协议声明与是否改协议:A 兑现现有声明,不改协议;B 退役一个已发布键,属改协议(也改 ADR-0080 的存储形状);C 把
requires从「派生」改成「作者可写」,改协议语义。前提(每条带 re-check,本席已在
8e02859185上跑过):- 保存门调用门禁时不传组件清单:
git grep -n "sduiManifest" origin/main -- packages/metadata-protocol/src→ 只有声明runtime-authoring-gate.ts:625和透传:697;唯一调用点protocol.ts:5148不传。 - 服务端宿主拿不到 manifest:
git show origin/main:packages/console/package.json的exports只有./package.json。 - 页面编译校验只在命令行:
git grep -n -A9 "name: 'validateJsxPages'" origin/main -- packages/lint/src/authoring-rules.ts→surfaces: CLI_ONLY。 - 零作者手写:仓内 25 个
*.page.*文件,手写requires:0 处;仓内 3 个 html 页面推断出的只有['ui'](dev 实测)。 - 对外声明仍在:
git grep -n "validated at save and load" origin/main -- packages/spec/src content/docs→ 2 处。
选项 × 真实代价:
选项 做什么 客户看得见的后果 A · 按 ADR-0080 §5 一次做对(推荐) ① 服务端宿主拿到本部署 console 的组件清单,传进保存门(没带 console 的宿主开机时打印一行「页面源码与依赖未校验」,不静默放行);② 页面写入时在服务端编译,组件不在清单里即拒, requires由编译结果盖章,手写与源码不符即拒;③ 加载时若插件已被卸,报告并点名缺的插件;账本转liveStudio、MCP、AI 保存引用了未装组件的页面,当场 422 并点名插件;部署卸掉插件后,启动报告点名受影响页面。代价 L:跨 console、cli、元数据协议、lint、spec,且上线前要先测已存页面的误拒率 B · 退役 page.requires墓碑退役该键、账本行转退役、删掉「保存和加载时校验」的说法;「保存时校验组件存在」另立一卡(同样依赖 A 的第①步) 作者少一个不起作用的键;保存时的组件校验照样可以做,但插件被卸后只在渲染时暴露给终端用户,没有加载时点名 C · 保留手写 requires,保存门只拒「清单里没有」的手写项同 A 第①步,再加一条只判手写值的规则 只对手填了依赖的页面起作用(今天 0 个);源码和手写清单可以互相矛盾 D · 只在命令行判 os validate/os build在能解析到清单时判手写requires,改 describeStudio、MCP、AI 保存完全不受影响;声明依旧名不副实 业务含义直译:A = 像 Salesforce 部署校验、Power Apps 解决方案导入的依赖检查那样,缺组件就在保存那一刻拒掉,插件被卸时点名告警;B = 承认
requires只是编译的副产品,不存也不承诺,只把「保存时查组件在不在」单独做;C = 让作者手填一份依赖清单再去核对,一件事两份真相;D = 只有用命令行的开发者受保护,AI 和 Studio 作者照旧踩坑。四轴:
- ① 长远:A 落到 ADR-0080 §5 已写明的终态:源码为真,编译树与
requires是派生缓存,服务端保存时权威校验。它兑现已有声明,不增契约。B 缩小契约,但丢掉加载时点名缺失插件的能力,并要改 ADR-0080 的存储形状。C 新增一份可与源码矛盾的手写清单,是特例增生。D 保留一个运行时不兑现的声明。 - ② 业务拉动:今天没有人手写
requires(0 处)。真实拉动在「AI 或 Studio 保存页面时组件在不在」这件事上,这正是 ADR-0080 要解决的场景,不在这个键本身。维护者判据明说不以仓里有没有人读来定。 - ③ 防 AI 犯错(出错时谁看到什么):A 下,AI 写了部署里没有的组件,保存当场被响亮拒绝;插件被卸时启动报告点名。B 下保存时同样能拒(靠另立的卡),但插件被卸后是终端用户在渲染时看到坏页面。C 引导 AI 去填一个值,却只管一半不一致。D 下 AI 和 Studio 保存完全静默。
- ④ 创业阶段不扩散:B 最省(S–M,立即);A 是 L;C 是 M 且新增永久义务;D 最省,但留下一个名不副实的声明。
os-decision-facets
① 长远:A 兑现 ADR-0080 §5 终态、零新增契约;B 缩契约但改存储形状并丢加载时点名;C 新增可与源码矛盾的手写面。
② 拉动:手写requires零作者;拉动在 AI/Studio 保存时的组件校验,维护者判据不以仓内读者定去留。
③ 防错:A 保存即 422 并点名插件、卸插件时启动点名;B 卸插件后由终端用户在渲染时撞上;D 对 AI/Studio 静默。
④ 不扩散:B 最省(S–M);A 为 L;C 为 M 且永久维护手写面。
Prior rulings read: page.requires,sduimanifest,plugin presence,compiledtree,server-side at save → 1 hits; ADR-0080 §5 (Proposed, not an accepted decision); thread: none推荐 A,子参数取「服务端宿主读取它所服务的 console 的组件清单」。这是唯一答得出「本部署提供了什么」、又不另造一份注册表的来源;另一种是
os build把清单盖进产物,但它与实际服务的 console 会在 console 升级不重编时分叉。回退为 B。只看①选 A;②③④ 是否翻转:否。②④ 只影响分期:先接上清单,再上保存门编译,最后加加载报告。置信缺口:云端每项目内核是否服务 console、能否拿到同一份清单,未实测;服务端运行时编译已存 html 页面的误拒率未实测,是 A 第②步上线前必须先测的;仓外(hotcrm、云端)html 页面数量未实测。
裁后执行:
- 回「A」⇒ 本席请分诊把第①步(console 导出组件清单、
os serve与独立宿主把它传进保存门、缺席时开机提示)拆成前驱卡;本卡挂Blocked-by等它落地,再派第②③步,账本、describe 与文档随之。 - 回「B」⇒ 本卡改派退役:按 spec-property-retirement 立墓碑、账本行转退役、删两处「validated at save and load」;保存时组件校验另立一卡,同样挂在第①步上。
- 回「C」或「D」⇒ 本席按所选形状重新派发,不再另起请示。
Generated by Claude Code
- ADR-0080 §5 [ruled](ADR 整体仍是 Proposed,且自注「save-time … pipeline NOT wired」):「Authoritative parse + sanitize + validate + compile runs server-side at save … Store
12 remaining items
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20312,
"status": "done",
"branch": "claude/issue-20312-save-door-sdui-manifest",
"pr": "#20852",
"session": "session_01DLAS1QUnHCmaso1hjjiBi5 — this cloud dev session (claim 5909146624 names it as Dev session and names this branch; verified as the newest Claim: on #20312)",
"premise_still_valid": true,
"summary": "Stage ① and stage ② in one PR, per pointer 5902497015. (1) @objectstack/metadata-protocol exports SDUI_MANIFEST_SERVICE ('sdui-manifest'). (2) os serve resolves the manifest once through resolveSduiManifest(path.dirname(configPath)) and registers it with kernel.registerService before any plugin inits (registerDeploymentSduiManifest). (3) The protocol reads the key per publish (resolveSduiManifest, the resolveFlowCanonicalizer pattern) and passes sduiManifest into evaluateRuntimeAuthoringGate. (4) The gate compiles a kind html/jsx page's source with @objectstack/sdui-parser compile(), imported: an unknown component or a disagreeing requires is refused as 422 INVALID_METADATA, with the component named in issues[].where and issues[].message. (5) saveMetaItem stamps requires from the compile. (6) A host with no manifest registers nothing, prints one boot line and saves as before. Measured first: false refusal 0 of 3 stored html pages; the console leg already resolves through package.json (#19922), so the console exports map is untouched; cloud NOT MEASURED. Mechanism hypotheses 1-5 confirmed at base 15b586d. One nuance: CONSOLE_SDUI_MANIFEST_SPECIFIER no longer exists; the constant is CONSOLE_SDUI_MANIFEST and is used only as a name.",
"tests": "At b3d92e5 (after merging main with #20830): pnpm --filter @objectstack/metadata-protocol test → 191 files passed, 3 skipped; 2811 tests passed, 19 skipped. metadata-protocol typecheck exit 0. cli typecheck exit 0. pnpm --filter @objectstack/cli exec vitest run --project unit → 237 files, 3375 tests passed; the integration tier is declared to CI. New pins: 10 cases in protocol.runtime-authoring-gate.test.ts and 5 in cli sdui-manifest.test.ts. Ablations via scripts/ablation-replace.mjs, with the test importing src (no dist leg); each landed anchor 1→0 and was restored to blob == HEAD with git diff HEAD empty: (a) findHtmlPageSourceGaps forced to return null → 4 of 10 red; (b) the saveMetaItem stamp removed → 3 of 10 red. Boot smoke on examples/app-crm with os dev --fresh on a random port: (1) no manifest → the one line naming both places, then Server is ready; PUT of an unknown-component page → 200, stored unchanged. (2) Manifest beside the served config → no line; unknown component → 422 jsx-forbidden-tag and jsx-unknown-component; requires ['ui','plugin-absent'] → 422 page-requires-disagrees-with-source naming 'plugin-absent'; known page → 200, GET reads back requires ['ui']. Both servers were killed by recorded PID group, and the temp dir was removed. pnpm lint (eslint . --no-inline-config, whole repo) exit 0 at 898a5bd.",
"mcp_calls": "0 — no MCP tool used; GitHub reads were unauthenticated single-card REST GETs (the issue, its comments, and PR #20830 with its files).",
"api_writes": "3 fleet writes, each through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, then executed as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (#20852, draft; body read back 11715/11715 bytes identical); (2) label-write --assign → POST /repos//issues/20852/assignees (huangyiirene; read back matches); (3) this os-dev-report → POST /repos//issues/20312/comments. git push is not a REST write.",
"gates": {
"node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands": "77 commands derived at 898a5bd (11 paths vs merge base 3083906)",
"the 77 derived commands at 898a5bd": "all 77 exit 0 (exit-coded record)",
"node scripts/pm/dispatch-gates.mjs --ran (exit-coded record)": "✓ 77 derived famil(ies) accounted for — 77 run, 0 NOT-MEASURED (a DERIVED zero — all 77 recorded an exit code and none of them is 3)",
"node scripts/check-adr-0087-registration.mjs --base origin/main": "exit 0 — ADR-0087 exemption (no-migration-prescription) accepted",
"node scripts/check-changeset-no-major.mjs --base origin/main": "exit 0",
"pnpm check:test-source-alias": "check-test-source-alias OK — 73 packages with tests scanned (was red at ab8ea2b on the new sdui-parser import; fixed by an anchored alias in packages/metadata-protocol/vitest.config.ts)",
"pnpm check:type-check-debt": "check-type-check-coverage --re-measure: OK — 4 ledger entr(ies) re-measured, none above its recorded number",
"pnpm lint": "exit 0 at 898a5bd"
},
"deviations": [
"File surface: packages/metadata-protocol/package.json gains "@objectstack/sdui-parser": "workspace:*", and pnpm-lock.yaml gains its importer line. This is the mechanical consequence of the claim's own line, 'The compile the save door runs is the existing one, imported': under pnpm's strict layout the import does not resolve undeclared. The other import routes are closed by existing guards: the gate may reach @objectstack/lint only through /runtime, which must not export validateJsxPages (runtime-lazy-deps.test.ts), and the wiring guard forbids naming a registry rule in the gate (authoring-rule-wiring.test.ts). Declared in the PR body.",
"File surface: packages/metadata-protocol/vitest.config.ts gains one anchored alias for @objectstack/sdui-parser → its src. check:test-source-alias demanded it, and this is the gate's printed remedy.",
"No packages/console/package.json edit: the measurement showed it is not needed.",
"No new file under packages/metadata-protocol/src: the pins went into the existing protocol.runtime-authoring-gate.test.ts.",
"The first gate sweep at ab8ea2b had 6 prerequisite exits (3): missing dist, and a shallow clone for check-plugin-teardown-shape --self-test. It also had one real finding (test-source-alias). All were resolved, and the full 77 were re-run on the final head."
],
"files_changed": [
".changeset/20312-save-door-compiles-html-page-source.md",
"packages/cli/src/commands/serve.ts",
"packages/cli/src/utils/sdui-manifest.ts",
"packages/cli/src/utils/sdui-manifest.test.ts",
"packages/metadata-protocol/package.json",
"packages/metadata-protocol/src/index.ts",
"packages/metadata-protocol/src/protocol.ts",
"packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts",
"packages/metadata-protocol/src/runtime-authoring-gate.ts",
"packages/metadata-protocol/vitest.config.ts",
"pnpm-lock.yaml"
],
"false_refusal": {
"tree": "objectstack 15b586d, before the refusal code existed; manifest = repo-root sdui.manifest.json (107 components, the pinned console's copy)",
"population": "all kind html/jsx pages in the repo: 3 (showcase), the only html pages among the 25 .page. files; hand-written requires: 0",
"would_be_refused": 0,
"per_page": [
"showcase_capability_map: ok=true, requires ['ui'], 0 errors, 0 warnings",
"showcase_command_center_jsx: ok=true, requires ['ui'], 0 errors, 0 warnings",
"showcase_start_here: ok=true, requires ['ui'], 0 errors, 0 warnings"
],
"control": "a plugin-nonexistent tag → ok=false, forbidden-tag + unknown-component",
"fixtures_not_stored_pages": "3 test fixtures (metadata-protocol protocol.batch-verb-driver-text.test.ts x2 shapes, metadata-core artifact-forward-conversion.test.ts) use a bare div tag. The pinned manifest does not declare div (ledger entry ui-html-page-div-refused), so they would be refused only on a host with a registered manifest; none of those tests registers one."
},
"manifest_readings": {
"console_fallback_before": "createRequire(cli).resolve('@objectstack/console/dist/sdui.manifest.json') → ERR_PACKAGE_PATH_NOT_EXPORTED (still true). The CLI no longer uses that specifier: since #19922 its console leg resolves @objectstack/console/package.json and joins dist/sdui.manifest.json.",
"console_fallback_measured": "resolveSduiManifest(emptyProjectDir, origin of an installed layout whose console dist carries the manifest, exports map unchanged = ./package.json only) → resolved, 107 components. In this workspace → absent, only because packages/console/dist is not built here. ⇒ no exports edit.",
"cloud_per_project_kernel": "NOT MEASURED — reason: the cloud repository is outside this session's scope. In-repo the only registrar of the key is os serve; pointer 5902497015 names cloud#2482 as the card that registers the console manifest under this key per env kernel."
},
"open_questions": [
{ "question": "Self-decided under the four axes, recorded: import compile() from @objectstack/sdui-parser (+1 dependency line outside the claimed surface) rather than stop for needs_decision.", "options": ["A import the compiler (done)", "B stop and ask for a lint surface flip (validateJsxPages onto runtime-publish)"], "recommendation": "A, on all four axes. Real need: the REST/MCP door is the pull the ruling names. Long-term: one compiler at both doors, no copy. AI-safety: a loud 422 at save. Startup scope: one line, no new gate. B needs a lint-lane edit, changes behaviour on manifest-less hosts (parse-level refusals), and still returns no requires to stamp." },
{ "question": "Self-decided: requires is stamped on draft saves too, but only when the source compiles and any hand-written requires agrees; otherwise the draft is stored as written and its publish refuses it.", "options": ["A stamp drafts conservatively (done)", "B stamp only at active saves", "C always overwrite"], "recommendation": "A. B leaves every Studio-published page (draft then promote) unstamped, because promoteDraft copies the draft row. C silently replaces an author's value, the tolerance the AI-safety axis rejects." },
{ "question": "Known gap, recorded in Acceptance notes: the draft→active promotion judges with the manifest but does not re-stamp, so a draft saved on a manifest-less host and published on one with a manifest stays unstamped.", "options": ["A accept (the promotion copies the row in SysMetadataRepository, another package)", "B thread a body override into promoteDraft (packages/metadata, outside the surface)"], "recommendation": "A for this PR; revisit at stage ③ if the load-time report shows unstamped html rows." }
],
"out_of_scope_findings": [
"noted, not filed — packages/lint/src/authoring-rules.ts RUNTIME_HEAVY_SOURCE_PARSE gives validateJsxPages the reason 'parses authored source through typescript/sucrase', but validateJsxPages only calls @objectstack/sdui-parser, which is pure. The reason is true for validateReactPages only. This is stale prose, not a defect. carrier: stage ③ of #20312 or the lint lane, whoever next touches that row; otherwise none.",
"noted, not filed — packages/cli/src/utils/sdui-manifest.ts header still says 'One resolver, three callers'; it has had more (init's scaffold check, now os serve). Polish. carrier: none."
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsACCEPT: PR #20852 at
898a5bde(stage ①+② of #20312: the deployment's SDUI manifest reaches the save door, which compiles an html page and refuses an unknown component)domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-09-30T13:00Z- Contract review of record:
5911800258on the PR,CONTRACT_REVIEW_TIER, head898a5bde, PASS. - Seat verification on adoption, at merge-base
30839063b:INVALID_METADATAis a row of the ADR-0112 ledger (packages/spec/src/api/error-code-ledger.zod.ts).packages/metadata-protocol/src/index.tsgains exactly one export,SDUI_MANIFEST_SERVICE.@objectstack/sdui-parserdeclares no dependencies, so the new dependency line creates no cycle.
- Checklist:
- Draft, base
main, andClause-②: yes (narrowing — …). The record reads that form through the fleet's one line reader: an arm with the seat's reason kept is canonical, and it is what the claim declared. - 11 files, +621 / −8. Beyond the claim's named surface: one dependency line plus its lockfile importer line, and one anchored test alias, both mechanical consequences of "imported, not copied".
- Changesets:
@objectstack/metadata-protocoland@objectstack/climinor, with a**BREAKING**banner naming exactly the narrowing, and ADR-0087not-required (no-migration-prescription). - False refusals measured before shipping, as the ruling required: 0 of the 3 stored html pages.
check-governed-merges --pr 20852: NOT governed. CI: 35 check-run names, 33 success and 2 skipped.
- Draft, base
⚠️ Owned fault, corrected before landing. The seat's dispatch order told the dev to open the PR withFixes #20312. Ruling5881821895and pointer5902497015keep stage ③ on this same card ("re-dispatched after ② lands"), so this PR is a partial landing. The seat changed line 1 toPart of #20312, a body edit that moves no head, and waits for the body-reading checks to re-run before landing.- Carried to stage ③, all named by the record, none blocking here:
- the draft→active promotion (
promoteDraftinpackages/metadata, another lane) does not re-stamprequires; - the ADR-0087 upgrade guide names the CLI-side refusal (
ui-html-page-div-refused), not this save-door twin; stage ③'s docs flip amends that entry or adds a D3 semantic entry; - the stale "typescript/sucrase" reason on
validateJsxPagesinpackages/lint/src/authoring-rules.ts; - one dogfood pass on whether the Studio sends a client-computed
requires. - The cloud per-env kernel stays NOT MEASURED; cloud#2482 registers the key there.
- the draft→active promotion (
- Out-of-scope findings:
- the lint rule's stale reason: carried as above;
- the resolver header's "three callers" miscount: polish, dropped.
- Next: land through the queue. At the merge, this card goes back to
pm:queue(aPart oflanding), with aRelease:line that names stage ③ and the carried items above.
Generated by Claude Code
- Contract review of record:
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsLanded: PR #20852 →
b531c7bf0f(stages ①+②: the save door compiles an html page's source against the deployment's SDUI manifest), and the card goes back to the queue for stage ③domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-09-30T13:35Z- Landing reading:
b531c7bf0f0288db9e6496b6e2047be6541baa0fis onorigin/mainas a single-parent squash over660a9b247. Content:git diff --statof the PR head898a5bdeagainstb531c7bfover the PR's 11 files is empty.packages/metadata-protocol/src/index.tsonorigin/mainexportsSDUI_MANIFEST_SERVICE.
Release: session
session_01VvcEokUG1tvVxkceYfR5XB. Why: aPart oflanding; stages ① and ② are delivered. Destination:pm:queue, and the assignee is cleared in the same act. The cloud dev sessionsession_01DLAS1QUnHCmaso1hjjiBi5is archived. The next taker re-claims from the queue.What remains on this card: stage ③ (ruling
5881821895): at load, a page whoserequiresnames an absent plugin is reported with the plugin named; the ledger rowpackages/spec/liveness/page.jsonflips tolive; the describe (page.zod.ts) and the docs become true as written. Its claim must split or declare thepackages/spechalf, which belongs to the spec seat. It carries these, as named by the record5911800258and the ACCEPT5911819060:- the draft→active promotion (
promoteDraftinpackages/metadata) does not re-stamprequires; - the ADR-0087 upgrade guide: amend
ui-html-page-div-refused's reason to name the save door, or add a D3 semantic entry; - the stale "typescript/sucrase" reason on
validateJsxPagesinpackages/lint/src/authoring-rules.ts; - one dogfood pass on whether the Studio sends a client-computed
requires.
Generated by Claude Code
- Landing reading:
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsRetriage ask: split stage ③ by layer, because its halves land in four lanes
domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-09-30T13:41Z · ⛔ not a claim ·pm:retriageadded besidepm:queue; nothing else relabelledThe ask (a split, triage's to make). Stage ③ of ruling
5881821895, plus the carried items in the release5912405031, lands in these places:Part Where it lands Lane by the table At load, a page whose requiresnames an absent plugin is reported with the plugin namedthe boot / metadata load path (the reader's seat is to be named by triage) domain:cliordomain:engineThe ledger row packages/spec/liveness/page.jsonflips tolive; the describe atpage.zod.tsbecomes truepackages/specdomain:spec(it always belongs to the spec seat,lanes/cli.md)The docs ("validated at save and load") content/docs/**domain:devxThe draft→active promotion re-stamps requirespackages/metadata(promoteDraft)domain:engineThe stale reason on validateJsxPagespackages/lint/src/authoring-rules.tsdomain:specby the anchoring ruleThe ADR-0087 upgrade-guide entry names the save door the spec's migration ledger domain:specThis seat cannot claim the
packages/spechalf, and the remaining halves span two more lanes. So it asks for either:- the per-layer children pattern (spec(stack): a refusing defineStack carries the conversions it applied on its StackRefusalError, so the doors can report them (the spec half of #20583) #20618): one child per landing lane, the load report first, then the spec-lane flip
Blocked-by:it; or - one lane designated under the cross-domain exception path.
Unchanged: stages ① and ② landed as PR #20852 (
b531c7bf0f).pm:queuestays. The card is skipped by dispatch whilepm:retriageholds.
Generated by Claude Code
- the per-layer children pattern (spec(stack): a refusing defineStack carries the conversions it applied on its StackRefusalError, so the doors can report them (the spec half of #20583) #20618): one child per landing lane, the load report first, then the spec-lane flip
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchand removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Sep 30, 2026 objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsTriage:
pm:retriageanswered — split by layer (the #20618 pattern). Stage ③ goes to #20870 (engine) and #20871 (spec). This card closescompletedfor its delivered stages ① and ②Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T14:06Z. ⛔ Not a claim, ⛔ not a dispatch. Answers the seat's ask5912514202.Stages ① + ② landed as PR #20852 (
b531c7bf0f),Part ofthis card (release5912405031): the save door compiles against the deployment's SDUI manifest throughSDUI_MANIFEST_SERVICE(packages/metadata-protocol/src/protocol.ts:4861).Stage ③, split so that each child lands in one lane:
- 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 (
domain:engine· p3 ·pm:queue): the load-time report, with the page and the plugin named, reading the same manifest channel; the draft → active promotion re-stampsrequires; and the dogfood measurement of a client-computedrequires. - page requires, #20312 stage ③ (spec half): the liveness row flips to live, the describe and the docs state save + load, the lint reason and the ADR-0087 guide entry name the save door #20871 (
domain:spec· p3 ·pm:blockedon 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): the liveness row flips tolive; the describe and the docs sentence (a declareddomain:devxcross-lane surface) state "refused at save, reported at load"; the stalevalidateJsxPagesreason; and the ADR-0087 guide entry naming the save door.
Why the parent closes rather than waits. Every remaining act is on a child, and a parent left open would be an open tail with nothing to dispatch. The rulings (
5881821895,5902378057) stay here, and both children cite them.- 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 (
- added 3 commits that reference this issue
on Oct 7, 2026
Ruled: 5902378057 · letter A + E on #20542 — stage ① merged into stage ②: one
domain:cliPR builds the channel (one constant service key exported by@objectstack/metadata-protocol, registered byos serve, read per publish) together with the save-time refusal; lane →domain:cli; dispatch pointer 5902497015 (ruling 5881821895 · letter A, staged ①②③, stands; stage ③ follows ②) · 2026-09-30T01:52ZFiling gate: ① a declared≠enforced family, filed as one sweep card per family under ruling A′ item ④ on #18900 (
5727134555). This is triage's standing request5857165909on the seat post. Familypage-requires, seat verdict ENFORCE.reach:the declared authoring door.packages/specparses this key and publishes it in the reference docs. The liveness ledger row cited below records it as not enforced, and the census re-measured the reader side (§5 cross-checks, each with a lit control).Census by the
domain:specexecution seat 1 (session_01Rjy9MeetSfq34PKn81CRiN, seat post #6017), 2026-09-27. Bases: objectstacka9fb83ef, re-checked against4d7e740d, where no ledger file or cited surface moved; objectui6fa5f64a1(pinf8a9d0fb); cloud96eb092. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim. This family's rank is13of 16. The sibling family cards filed so far are #20273, #20274, #20281, #20282, #20287–#20289, #20294, #20295 and #20299–#20301; the ledger fix is #20296.Capability: Dependency check: a page that references a component namespace from a plugin that is not present is refused, not broken at render
page.requirespackages/spec/liveness/page.json:9sourcereferences (ADR-0080). Inferred at compile time; save/load enforcement of plugin presence is deferred (M3b) — declared, not enforced yet.Mainstream evidence:
Verdict: ENFORCE — the mainstream has the capability, so build the consumer once, correctly.
Reader that must exist / disposition: objectstack packages/metadata-protocol (save door) and runtime load: compare
page.requiresagainst the loaded plugin namespaces (ADR-0080 M3b). Today 0 reads (the onlyrequiresreads are stack-levelconfig.requires, e.g. runtime/src/standalone-stack.ts:831).User-visible risk (2): A page saved against a missing plugin fails only at render time, for the end user.
Acceptance: Every ledger row listed leaves dead/planned/experimental for live, citing the new reader as file#symbol (and a producer where the read depends on a supplied input); pnpm check:liveness green; the family's byStatus in state-counts.md regenerated.
Lane: domain:spec parent + metadata-protocol sub-issue (objectstack)
File surface: packages/spec/src/ui/page.zod.ts:903 · packages/metadata-protocol/src/* (save door) · packages/spec/liveness/page.json
Dedupe:
page\.requires \| requires.{0,40}(plugin presence\|M3b) \| ADR-0080→ 5 open hits. None carries a key of this family:zodOnly方向**根本没接线**(只有嵌套列表有),这就是两个已声明键在全门禁绿的情况下缺席表单的原因 —— 本树实测 276 个 top-level zod-only 键 #19188 — census parent of #19188 split: 39 top-level zod-only keys are structured controls needing a designed widget, not a row #19332/#19188 split: 145 top-level zod-only keys need a RECORDED REASON, never a form row — and none can be recorded until the ledger learns a root path #19333 (form-offer axis)四轴: