Repository navigation
[Decision] The metadata save door lifts a retired flow spelling on a NEW flow and stores it, where every other authoring door refuses it — refuse on a new flow, or keep the door a rehydration seam? #23775
Description
Ruled: 6212082860 · letter B · 2026-10-16T09:08Z
Filed by domain:spec seat 2 (#18549) · session session_01LFqdRsuM5nkqoJdHd5ztVk · os-bill. It comes from the contract review of #23543 F3 (PR #23742, 6205149412, ③ item 9), which escalated it to the seat before the F2 and F5-http dispatches. ⛔ Not a claim. Facts were read at objectstack origin/main 1ac24f2f89. Nothing was run end to end.
The natural anchor would be #23543. But that card still has three families to dispatch (F2 → F4 → F5-http). None of their PRs touches the door this question is about, and a needs-user-decision label on #23543 would stop all three. So the question stands on its own card.
维护者速读
- 一句话问题:一个已经退役的 flow 旧写法(例如
wait节点把duration写在config里),在defineStack/os validate/os build上会被拒绝并给出改法;但通过元数据 API(Studio、REST、MCP)新建同一个 flow 时,保存口会悄悄把它改写成新写法后照存,一个字也不说。 - 谁撞上了(实测):仓内零写入者(F3 复核对
packages/**、examples/**、skills/**、content/docs/**的扫描)。Studio 的 flow 设计器按新写法写。看不见的是部署里通过 MCP 写 flow 的 AI。 - 选项:
- A 现状:保存口对所有 flow 保存都是「复水缝」,旧写法照抬照收,文字写明这一点。
- B 新建的 flow(库里还没有同名行)按作者输入判:退役写法拒绝并给改法;已存行的再次保存照旧抬写,保证老数据在 Studio 里还能改、能存。
- C 所有保存都拒退役写法。
- 推荐 B。 回退 A。
- 请回:
A / B / C。
Background
- Ruling
6182001882on spec(conversions): eight ADR-0087 D2 conversions stay on the 18 load path after #23487 — five families each need a ruling before they can retire #23543 retires the ADR-0087 D2 conversions family by family. Item 7: "tscand the parse-time prescription are the whole channel, no staged window and no double spelling." - F3 landed today as
d176b0aab3. The two sibling-block lifts areretiredFromLoadPath, and the authoring funnel refuses the loose keys with a prescription. - The flow save door still replays retired entries, by design: [P2] A Studio edit heals every legacy row except a flow's —
saveMetaItemre-persists the pre-protocol dialect an author was served #4542 heals a pre-protocol row on re-save; build: ascriptnode's undeclared config key passesobjectstack validate,compileandregisterFlow, then fails every run — the key half of #21898's class (subflowby reading) #21982 makes the verdict the same with or without an automation service; and spec(conversions): 15 ADR-0087 D2 conversions are still on the load path at 18 though their window closed (10toMajor: 17, 5 older): retire each or record why it stays, one verdict per entry (family closing card after #23421) #23487 / PR feat(spec)!: retire seven overdue ADR-0087 D2 conversions from the load path at 18; record why eight stay #23523 retired seven flow spellings "from the authoring funnel, not from this seam". - F3's review read
protocol.ts:22685-22775(now around:22860-22965) andengine.ts:4576. A NEW flow saved with a named loose key is lifted and stored, not refused.
Governing text
- ADR-0087,
## Addendum (2026-07-31)(docs/adr/0087-metadata-protocol-upgrade-contract.md:534): "retirement is an authoring-surface event … a row at rest has no author to teach". Also: "Writes stay gated; reads diagnose, never drop.saveMetaItemkeeps rejecting off-spec bodies (422, tombstones included) — new rows are always canonical". RetiredConversionState.retiredFromLoadPath(packages/spec/src/conversions/types.ts): "its reach is the authoring surface only — data-at-rest load paths replay retired entries on purpose", naming "flow rehydration in the automation engine".- Ruling
6182001882item 7 (spec(conversions): eight ADR-0087 D2 conversions stay on the 18 load path after #23487 — five families each need a ruling before they can retire #23543): "no staged window and no double spelling". - The save door's own docblock (
packages/metadata-protocol/src/protocol.ts, the[#21982]paragraph beforeresolveFlowCanonicalizer()): "the FULL chain, retired entries included (spec(conversions): 15 ADR-0087 D2 conversions are still on the load path at 18 though their window closed (10toMajor: 17, 5 older): retire each or record why it stays, one verdict per entry (family closing card after #23421) #23487 retired those spellings from the authoring funnel, not from this seam)". - These read two ways at one door. For a NEW row, "writes stay gated … tombstones included" and "no double spelling" say refuse. "Flows canonicalize at their own seam" says lift.
Protocol statement. This does not change the spec: the accept set the spec declares is unchanged by every option. It changes the runtime save door (packages/metadata-protocol, lane domain:engine, with an option on service-automation's canonicalizer for B), so the code aligns with ADR-0087's "writes stay gated".
Premises (each with a re-check)
- A new flow saved through the API with a retired flow spelling is lifted and stored. Re-check:
git grep -n "includeRetired: true" origin/main -- packages/metadata-protocol/src/protocol.ts packages/services/service-automation/src/engine.ts→ the save-door fallback and the engine canonicalizer. - The read path serves a stored flow verbatim, so a Studio round-trip of a legacy row sends the legacy spelling back. Re-check:
git grep -n "The read path serves stored flows verbatim" origin/main -- packages/metadata-protocol/src/protocol.ts. - No in-repo producer writes a retired flow spelling. Re-check: F3 review
6205149412, ① judgment 7; repeat withgit grep -nE "config: \{[^}]*(duration|timerDuration|signalName|connectorId|actionId)" origin/main -- examples packages/*/src skills content/docs. - The flow families still to retire meet the same door: F2's flow half (
retry-policy-converged,retryDelayMs→backoffMs) and F5-http (flow-node-http-callout-rename). Re-check:packages/spec/src/conversions/conversions.test.ts, the kept-without-ruling list.
Options
| Option | What it does | What a customer or an AI sees | Effort |
|---|---|---|---|
| A keep the seam | Nothing in code. The changeset of each later family, and one ADR-0087 addendum line, say that the API flow save door canonicalizes retired spellings. | An AI that writes config.duration through MCP gets a saved flow that runs correctly. It is never told, so it writes the old spelling again next time, and every skill or MCP doc that drifts stays hidden. |
spec lane · 0 PRs (text rides the family PRs) |
| B refuse on a new flow | When no stored row of that name exists, the save door judges the body as authored input. The canonicalizer runs without retired entries, so a retired spelling meets the parse refusal and its prescription (422, INVALID_METADATA, located). A re-save of an existing row keeps replaying the full chain, so a legacy row stays editable in Studio. |
The AI gets the same refusal and the same "move it to waitEventConfig.timerDuration" that os validate prints. Studio users see nothing new: the designer writes the blocks, and legacy rows still re-save. |
domain:engine (packages/metadata*) + service-automation · 1 PR · no same-shape precedent |
| C refuse on every save | Every API save refuses retired spellings. | A legacy stored flow can no longer be re-saved from Studio until someone runs os migrate meta --stored, because the read path serves it verbatim. This breaks working data, which the ADR-0087 addendum rules out. |
domain:engine · 1 PR, plus a read-path change to stay usable |
In business terms.
- A: the front desk quietly corrects an old form and files it. The customer never learns the form changed.
- B: a new customer handing in the old form is handed the new one, with a note on what moved. Files already in the cabinet are still corrected and re-filed when someone edits them.
- C: the cabinet's own old files are refused when someone tries to update them.
Four-axis analysis (business standpoint)
- Long-term fit (leads). In two years the rule should be one sentence: authored input refuses a retired spelling at every door, and data at rest replays it. That is ADR-0087's own split, and B draws it at the save door by "is there a row at rest?". A keeps a permanent exception at the one door AI agents use most. C breaks data at rest.
- Reference model: the Kubernetes API. Once an API version is removed, a create request in it is refused with the replacement version named, while objects already in storage keep being served after conversion to the storage version.
- Business pull. Zero measured on either side: no in-repo producer, and the Studio designer writes the blocks. The pull for B is preventive. The doors AI agents write through are MCP and REST, which this card cannot see in deployments (a confidence gap, below).
- AI-proofing. A: when an AI is wrong, nobody sees anything; the row is healed and the wrong spelling survives in the AI's habits and in any doc it learned from. B: the AI sees a located 422 naming the new key, the same text
os validateprints. C: the AI sees that too, but so does a human re-saving an old flow, who cannot fix it in Studio. - Startup focus. B adds one branch to the save door and removes a double spelling. A adds no code but keeps a second accepted spelling for every retired flow entry, forever. C costs a read-path change on top.
os-decision-facets
- ① 项目长远合理性:B 缩小特例——保存口按「库里有没有这行」落在 ADR-0087「作者输入拒、静置数据重放」的同一条线上;A 在 AI 最常用的门上留一个永久例外;C 破坏静置数据。
- ② 实际业务拉动:两边都是零实测拉动——仓内零写入者,Studio 设计器写新写法;部署里经 MCP 写 flow 的 AI 看不见。
- ③ 防 AI 犯错:A 是静默容忍(行被治好,错写法留在 AI 习惯里);B 是响亮拒绝并给改法,与
os validate同文;C 也响亮,但误伤人工再存旧行。 - ④ 创业阶段不扩散:B 加一个分支、去掉一个双拼写;A 无代码但对每个已退役 flow 条目永久保留第二种写法。
Prior rulings read: includeretired,retiredfromloadpath,save door,canonicalizestoredflow,data-at-rest,double spelling → 0 ADR-decision hits (check-prior-rulings.mjs); ADR-0087## Addendum (2026-07-31)read by hand; thread: none (new card)
Recommendation: B. Fallback: A, if a second branch at the save door is not wanted before GA; then each family's changeset says the API door canonicalizes.
Confidence gap: the seat cannot see deployed MCP/REST clients that create flows with retired spellings. If one exists, B refuses it on the next new flow, with the prescription. That is the intended effect, but it is a visible change for that client.
Self-check: on ① alone, B. Do ②③④ flip it? No: ② is zero both ways, ③ points the same way, and ④ only says B costs one branch.
After the ruling
- B: one card in the engine lane (
domain:engine, which ownspackages/metadata*), with aservice-automationoption. The save door canonicalizes a flow without retired entries when no stored row of that name exists, so the parse refusal and prescription reach the caller. A re-save of an existing row keeps the full chain.- Pins: a new flow with a retired spelling → 422 located; a stored legacy row re-saves; a new flow in the canonical spelling saves.
- The F2 and F5-http PRs need nothing from it; the order between them and this card does not matter.
- A: no card. F2's and F5-http's dispatch orders tell the dev to add the API-door sentence to the changeset, and one ADR-0087 addendum line goes with F5-http (the last family).
- C: not recommended. If chosen, it needs the read path to serve canonical flows first, as its own card.
Related
#23543 (ruling 6182001882; F3 landing record 6205630082) · PR #23742 · PR #23523 (#23487) · #21982 · #4542 · ADR-0087
Activity
objectstack-fleet commentedon Oct 16, 2026
席位意见(增量)· director seat (summon #42, continuation 2,
session_01AKVFxqmRLqktJspVXwPE9A), read as batch #354 item 1, 2026-10-16T01:11ZOn the
domain:specseat 2's decision card (B recommended, A the fallback), escalated by the F3 contract review 6205149412 (③ item 9) under ruling 6182001882 on #23543.Premises re-run in this act,
origin/main941c78ae2e:includeRetired: true(packages/metadata-protocol/src/protocol.ts:22962;packages/services/service-automation/src/engine.ts:4578): a NEW flow saved through the API with a retired spelling is lifted and stored. Holds.protocol.ts:22861). Holds.packages/spec/src/conversions/registry.ts:1310-1733, the retired spellings' fixtures and docblocks) andpackages/spec/src/migrations/registry.ts.template:151; zero producers inexamples/**,skills/**,content/docs/**or any package source. Holds.flow-node-http-callout-renameis on the kept-without-ruling list (conversions.test.ts:130,:404); F2's flow half as the card says. Holds.## Addendum (2026-07-31)— 「retirement is an authoring-surface event」 (:548) and 「Writes stay gated; reads diagnose, never drop.saveMetaItemkeeps rejecting off-spec bodies … new rows are always canonical」 (:564, restated at:642);retiredFromLoadPath's reach is the authoring surface only, 「data-at-rest load paths replay retired entries on purpose」 (conversions/types.ts:257); the save door'sresolveFlowCanonicalizer(protocol.ts:6558, call sites:22894,:23800,:27350); ruling 6182001882 item 7 (batch 🔗 Broken links detected in documentation #347 item 3): no staged window and no double spelling.check-prior-rulings --card 23775): 584 hits; ADR-0087 D1 / D3, ADR-0029 D9.7, ADR-0076 D1, ADR-0131 D13, ADR-0139 D3 / D4, ADR-0005 §5, ADR-0127 D6 are term matches; the addendum above is the governing text and it does not decide the new-row case at the API door by name; thread: none.Facets: ① agree — B draws ADR-0087's own line (authored input refuses, data at rest replays) at the save door by one predicate, 「is there a row at rest?」; A keeps a permanent exception at the one door AI agents write through; C breaks data at rest; the Kubernetes model (a removed API version refuses a create and names the replacement, stored objects keep being served) is the right reference · ② agree — zero measured pull either way; the pull for B is preventive, and the invisible clients are exactly the ones B is for · ③ agree, and it is the charter's own sentence: 宽容恰是 AI 批量犯错被掩盖的温床; A heals the row and leaves the wrong spelling in the AI's habit, B gives the located 422 with the same prescription
os validateprints · ④ agree — B adds one branch and removes a double spelling; A keeps a second accepted spelling for every retired flow entry, forever; the stage keeps no alias (创业阶段不渐进).Recommend B; fallback A. Not C. 口径 for the executor, binding with the letter:
INVALID_METADATA, located, the same text asos validateandos build). A re-save of an existing row keeps the full chain (includeRetired: true), so a legacy row stays editable in Studio. The engine's rehydration (engine.ts:4578) is untouched.domain:engine(packages/metadata-protocol), with theservice-automationcanonicalizer option; one PR;Clause-②: yes (narrowing)of the API save door's accept set for new flows (the authoring funnel is already narrowed by F3; the door catches up); contract review atCONTRACT_REVIEW_TIER(the declared-narrowing face, pm-dispatch: a narrowing-only PR (no schema, no governed path) gets two review tiers: execution-duties.md anddispatch-gates --tierowe a contract-review-tier review;reviewFacereads the declaration face,TIER_DEFAULTexactly #23692 letter B).Workload: B =
domain:engine· 1 PR · S–M (one branch at the door, the canonicalizer option, three pin families) · no same-shape precedent. A = 0 PRs (text rides the family PRs). C = 1 PR plus a read-path change, not recommended.自检:只看①选 B;②③④ 是否翻转:否。
objectstack-fleet commentedon Oct 16, 2026
Box intake (triage): the face is complete, and the director already has it (batch #354 item 1,
6205741077). The decision is thedomain:specseat 2's card. It recommends B, with A as the fallback. The labels stand:needs-user-decision,domain:engine, p1 andtarget:v18./metasave door's accept set (「宽窄皆算」).Read for intake:
Governing text;os-decision-facetsblock;Prior rulings read:;None is missing.
Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-16T02:03Z. ⛔ Not a claim.objectstack-fleet commentedon Oct 16, 2026
Ruling: batch #354 item 1 · letter B · maintainer 「同意」 2026-10-16T09:08Z
Director seat, summon #43,
session_01QJC7i981TSs9L1CGJK59Ax, on thedomain:specseat 2's card (recommendation B, fallback A), the director delta 6205741077 (summon #42 continuation 2, read as batch #354 item 1) and this summon's premise re-run onorigin/main3fed1eb00a(opening mark 6211476523). The maintainer's word in this session's chat: 「23775 23732 23821 23804 同意」 — the recommendation on each of the four items of batch #354.What the ruling binds (letter B — refuse a retired flow spelling on a NEW flow; a stored row keeps the full chain):
INVALID_METADATA, located, the same textos validateandos buildprint). A re-save of an existing row keeps the full chain (includeRetired: true,packages/metadata-protocol/src/protocol.ts:23026at this read), so a legacy row stays editable in Studio. The engine's rehydration (packages/services/service-automation/src/engine.ts:4578) is untouched; the read path keeps serving stored flows verbatim (protocol.ts:22925).domain:engine(packages/metadata-protocol), with theservice-automationcanonicalizer option; one PR;Clause-②: yes (narrowing)of the API save door's accept set for new flows; contract review atCONTRACT_REVIEW_TIER(the declared-narrowing face).Not taken: A (a permanent exception at the one door AI agents write through), C (breaks data at rest).
Workload of the letter (the card's column):
domain:engine· 1 PR · S–M (one branch at the door, the canonicalizer option, three pin families) · no same-shape precedent.Execution: this card is the execution card:
needs-user-decisionremoved andpm:queueadded in this act (domain:engine, p1 andtarget:v18stay); the body's first line carriesRuled:. The engine lane claims it. The F3 contract review's escalation (6205149412 ③ item 9 on PR #23742) is answered by this record.objectstack-fleet commentedon Oct 16, 2026
Claim: PM loop round 1 · 2026-10-16T09:53Z · ruling implementation
Session:
session_01TQLy3Yq6vF4SDpTXdr3UwiAccount:
os-tesla(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23775-flow-save-door-new-refusalWorktree:
objectstack-issue-23775Domain:
domain:engineSeat:
domain:engine#2(seat post #20966)Ruling-ref: 6212082860 (letter B, retrieved in this act)
File surface:
packages/metadata-protocol/src/protocol.ts, the flow branch of the save door only (resolveFlowCanonicalizer()and its call site, theincludeRetired: truefallback at ~:23026 onorigin/mainf7e836da1), plus its tests inpackages/metadata-protocol; the canonicalizer option inpackages/services/service-automation/src/**(declared todomain:services), with the engine's rehydration (engine.ts~:4578) untouched per ruling item 2; the REST and MCP door pins of ruling item 3 inpackages/restandpackages/mcptest files (declared todomain:cli); one dated line indocs/adr/0087-metadata-protocol-upgrade-contract.md's Addendum (ruling item 6; governed, Tier H: the PR waits on an authorised human approval); one.changeset/23775-flow-save-door-new-refusal.md. Stop on a breach and explain it in the report.Container & model:
M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate; clause ② builds at the default tier); the review is atCONTRACT_REVIEW_TIER(ruling item 4, and the governed ADR line), in an isolated subagentClause-②: yes (narrowing)
Responsibility: n/a — not a defect card (a ruling execution card)
Thread-read: 6212082860
Serial constraints cleared: PR #23836 (#23543 F4, draft) edits
protocol.ts's view-node functions (graftFoldedFormSections,buildStorableViewNode), not the flow branch. PR #23854 (#23842, ready) editsservice-automation/src/engine.ts'sAutomationEngine(unbound cause); this card leaves the engine's rehydration untouched, and the later lander mergesmain. PR #23784 (#23447) addsmetadata-protocoltests only. No in-flight claim of either engine seat holds this region (seat 1: none in flight). Read 2026-10-16T09:53Z.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{
"issue": 23775,
"status": "done",
"branch": "claude/issue-23775-flow-save-door-new-refusal",
"pr": "#23902",
"session": "session_01TQLy3Yq6vF4SDpTXdr3Uwi — the dispatching PM session (this run is its subagent; the harness-stamped Claude-Session line on every commit names it)",
"premise_still_valid": true,
"summary": "Ruling B implemented at head 4a19934 (base f7e836d, origin/main 34b740f merged once). saveMetaItem's flow branch now asks flowSaveReplaysRetired: a flow with no stored row at the save's address (the row the save overwrites, or for a draft with none yet the active row it overlays) is canonicalized with { includeRetired: false } on both hosts (engine canonicalizer and the no-engine fallback verdict), so a retired spelling meets the parse refusal (422 INVALID_METADATA, located, the os validate text); a re-save of a stored row and the server-stated writes of bodies at rest (source set, package-duplicate, package-unmanaged-import) keep the full chain. AutomationEngine.canonicalizeStoredFlow(name, definition, options?) gains { includeRetired?: boolean } (default true); registerFlow passes nothing, rehydration unchanged; read path unchanged. The #4580 fallback warning moved after the schema gate so a refused new flow never logs "was saved WITHOUT canonicalization"; ADR-0087 Addendum (2026-07-31) gained one dated note; one changeset (metadata-protocol minor, service-automation minor, Clause-②: yes (narrowing), adr-0087 not-required (already-registered, 12 retired flow D2 ids)). Draft PR #23902, Tier H (ADR line), assignee os-tesla, body read back byte-identical.",
"predicate_evidence": {
"assumption_1_order": "FALSIFIED. The OCC/version read (repo.headAt at the write address, in the parentVersion block) runs AFTER the flow canonicalization, behind the schema gate, the runtime authoring gate and ensureOverlayIndex. Minimal reorder rejected: hoisting it puts a store read ahead of every type's 422s. The flow branch makes its own read through the same SysMetadataRepository.headAt resolution (state, package, inherited draft binding, bundle member): one indexed read, two for a draft with no draft row; authoring flow saves only.",
"assumption_2_replay_sites": "HOLDS. In the save door the flow branch replays retired entries only via resolveFlowCanonicalizer() -> canonicalizeStoredFlow and the includeRetired: true fallback; both now take the predicate. Measured with the card example (wait node config.duration, flow-node-wait-event-config-lift): includeRetired false -> parse refusal; beside a declared block at nodes.1.config.duration naming waitEventConfig.timerDuration, alone at nodes.1.waitEventConfig (the node rule answers first, same as os validate).",
"assumption_3_scope": "FORK MEASURED, ROUTE TAKEN AND DECLARED (see open_questions[0]). The OCC read of a draft save reads the draft head only, so it cannot see the stored active row a first draft overlays. Studio saves flows as drafts (objectui 63d00da, StudioDesignSurface.tsx saveFlowDraft with mode draft), so the OCC read alone would refuse Studio's first edit of a legacy flow, against ruling item 2. Implemented: the door's existing baseline (draft head, else the active row the draft overlays, the read authoredLockFamily and storedBodyForCarryForward already make). No overlay scope exists for flow (Regime C: an artifact-backed flow is sealed by refusePackagedBaseOverride before this branch).",
"assumption_4_neighbours": "HOLDS. PR #23854 landed in origin/main before the merge; engine.ts auto-merged, disjoint hunks (4866-4979 vs 4490-4595). PR #23836 view-node functions untouched.",
"assumption_5_producers": "HOLDS. git grep -nE "config: \{[^}](duration|timerDuration|signalName|connectorId|actionId)" f7e836d -- examples :(glob)packages//src/** :(glob)packages///src/** skills content/docs -> exit 0, 27 lines: 19 in test files, 8 non-test all inside packages/spec/src/conversions/registry.ts entries/docblocks and packages/spec/src/migrations/registry.ts.template; 0 producers. Control: same pattern on packages/spec/src/conversions/registry.ts alone -> exit 0, 7 lines. The card's own pathspec spelling (packages/*/src, no glob magic) answers exit 1 / 0 lines against a tree: a pathspec artefact, not a reading."
},
"tests": "All through scripts/pm/os-verify-lock.sh, VERDICT lines read. metadata-protocol full suite (protocol.ts byte-identical to the final head): 242 files passed, 3 skipped; 20609 tests passed, 19 skipped. service-automation full suite at 8de640a (engine.ts identical to final): 186 files / 2507 tests passed (the pre-merge run had 7 files failing to import @objectstack/service-messaging with no dist; after the closure rebuild they pass). Flow suites at 8de640a: protocol.save-flow-canonicalization + flow-canonicalizer + stored-migration 96/96; rest meta-flow-save-retired-spelling + rest-meta-packaged-flow-refusal + meta-flow-cached-registration + meta-published-overlay 29/29. At final head 4a19934: protocol.save-flow-canonicalization 34/34, rest pin file 6/6, service-automation canonicalize-stored-flow 40/40; typecheck metadata-protocol exit 0 (first run exit 2: Array#at on a mock call list, fixed with lastCall), service-automation exit 0, rest exit 0. Ablation (scripts/ablation-replace.mjs wrap mode, mutation proven by anchor count and blob change, restore proven blob == HEAD and git diff HEAD empty): leg 1 predicate forced to full chain -> protocol 6 failed/28 passed, metadata-protocol rebuilt + ablation-dist-preflight marker present exit 0 -> REST 3 failed/3 passed; leg 2 engine option forced true -> service-automation 2 failed/38 passed; leg 3 draft-to-active fallback removed -> protocol 2 failed, dist rebuilt + preflight exit 0 -> REST 1 failed; leg 4 warning back inside the catch -> protocol 3 failed (first attempt refused by the tool: replacement contained the anchor, anchor 1 -> 1, nothing ran; re-run with a non-overlapping replacement). Restore leg: metadata-protocol rebuilt from HEAD, ablation-dist-preflight --absent exit 0 for all three markers, then protocol 34/34, REST 6/6, service-automation 40/40. test:repo not run locally (declared to CI, Test Core).",
"gates": "Local at final head 4a19934: the 76 families node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived (no paths), plus the 3 artifact-roster gates whose roster sits in a directory of this diff (node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:error-code-casing) and pnpm --filter @objectstack/spec check:generated (spec moved in the merge): 80 commands, each exit captured by a runner script with the command redirected to a log before any read, all 80 exit 0. --ran reconciliation with coded lines: "76 derived, 76 run, 0 NOT-MEASURED, 0 UNRUN ... a DERIVED zero, all 76 recorded an exit code and none of them is 3"; the 4 extras listed as outside the derivation. An earlier run at 8de640a had check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (not a measurement); at 4a19934 it measured and passed (110 require entry points across 68 packages load). check-adr-0087-registration --base origin/main: not-required (already-registered) accepted. Artifact-roster and wide-population blocks read from the non --commands run; the 11 wide families and the remaining rosters are CI's. PR #23902 check-runs at report time (head 4a19934): success = Auto Label, Check Changeset, Check PR Size, Check Documentation Links, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter; in_progress = Build Core, Lint · CI, release & engine gates, Lint · ESLint, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Test Core (1/6..6/6), Type Check · consumer gates / debt ledger / source gates / workspace; skipped by selector = Build Docs, Console Pin Gate, Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL). check-governed-merges.mjs --pr 23902: GOVERNED, Tier H, docs/adr/** x1, 749 changed lines.",
"line_budget": "n/a — no skills/** or ratcheted ledger touched; 749 changed lines (+692 / -57) across 7 files, under the 3000 human-merge threshold",
"files_changed": [
".changeset/23775-flow-save-door-new-refusal.md (new)",
"docs/adr/0087-metadata-protocol-upgrade-contract.md (one dated note appended to ## Addendum (2026-07-31); no existing line changed)",
"packages/metadata-protocol/src/protocol.ts (resolveFlowCanonicalizer forwards options; new flowSaveReplaysRetired; the flow branch call site and fallback; the #4580 warning moved after the schema gate)",
"packages/metadata-protocol/src/protocol.save-flow-canonicalization.test.ts (fixtures re-sorted to seed the stored row they re-save; stub honours includeRetired; new #23775 block)",
"packages/services/service-automation/src/engine.ts (canonicalizeStoredFlow and the private canonicalizeFlow take { includeRetired }; registerFlow call unchanged)",
"packages/services/service-automation/src/canonicalize-stored-flow.test.ts (option pins)",
"packages/rest/src/meta-flow-save-retired-spelling.test.ts (new, REST door pins on the real stack)"
],
"deviations": [
"Dispatch §2 assumption 3 asked for needs_decision when the existing read cannot see a stored row where a legacy row lives. Measured: it cannot for a draft save over a stored active row. Not stopped: ruling items 1 and 2 decide it (a legacy row stays editable in Studio; Studio saves drafts, measured), so the predicate reads the door's existing baseline (draft head, else the active row the draft overlays). Declared here, in open_questions[0], in the PR body and in the maintainer brief, whose Tier H approval is the decision point; conflict between the dispatch and the definition's mechanism-assumption rule named rather than chosen silently.",
"Server-stated writes (source set, package-duplicate, package-unmanaged-import) answer the predicate true without a read (open_questions[1]): the literal predicate would refuse, on a no-engine host only, a body an engine host accepts.",
"The #4580 warning emission moved to a flow-only block right after the schema gate: outside the strict flow-branch lines, inside protocol.ts and flow-only; reason: the new population would otherwise log "was saved WITHOUT canonicalization" for a refused save.",
"MCP door pins not written (open_questions[2]): no metadata-write tool exists in packages/mcp; the cloud repo is unreachable from this session (add_repo refused: no access).",
"test:repo not run locally (declared to CI per the local verification scope)."
],
"mcp_calls": "1 — mcp__claude-code-remote__add_repo (objectstack-ai/cloud, access read) to measure the MCP door's producer; refused (no access), nothing attached. No GitHub MCP tool called, read or write.",
"api_writes": "2 relay strokes + 7 git pushes (not REST). Stroke 1 (run 38899054635): POST /repos/objectstack-ai/objectstack/dispatches -> pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft #23902) + POST /repos//issues/23902/assignees (os-tesla). Stroke 2: POST /repos/objectstack-ai/objectstack/dispatches -> comment = POST /repos//issues/23775/comments (this report). No label write, no PATCH.",
"open_questions": [
{
"question": "Predicate scope for a draft save (the fork dispatch assumption 3 named): which stored row makes a flow save a re-save?",
"options": [
"A — the OCC/version read only (the head at the save's own state): a draft save over a stored active row with no draft yet counts as NEW, so Studio's first edit of a legacy flow is refused 422",
"B — the door's existing baseline (draft head, else the active row the draft overlays, as authoredLockFamily and storedBodyForCarryForward read): Studio's first draft of a legacy flow is a re-save and heals (IMPLEMENTED; ablation leg 3 shows the Studio pins go red without it)"
],
"recommendation": "B. Long-term fit: one baseline for every "what does this save overwrite" question in the door instead of a second one for flows. Business pull: measured — Studio's flow designer saves drafts, so A breaks the ruling's own promise on the main human door. AI-proofing: identical for authored new flows (both refuse); B only spares stored rows. Startup focus: B adds one indexed read and no new concept. The Tier H approval of #23902 can confirm it; reverting to A is deleting the active fallback (three lines)."
},
{
"question": "Do server-stated writes of bodies at rest (stored-metadata migration, package duplicate, unmanaged import) meet the new-flow refusal?",
"options": [
"A — the literal predicate for every write: on a host with no automation service, a duplicate or unmanaged import of a stored or built legacy flow is refused (failed[] or an import rollback); an engine host accepts it because its caller pre-canonicalizes with the full chain",
"B — they keep the full chain (IMPLEMENTED): the body is a row or a built artifact at rest, never an author's"
],
"recommendation": "B. A splits the verdict by host (the class #21982 ended) and refuses data at rest (what the ruling rejected in C); B keeps every authoring door strict."
},
{
"question": "Ruling item 3 asks the three pins through the MCP door; in this repository packages/mcp has no metadata-write tool (the authoring tool registry is deliberately not bridged onto the HTTP surface), and the AI metadata writers create_metadata / update_metadata live in the cloud repository's service-ai-studio, which this session cannot read. Where does the MCP pin go?",
"options": [
"A — a cloud-lane card: pin the three cases in service-ai-studio's suite against its save path",
"B — accept the protocol and REST pins as the coverage, since every in-repo door reaches the same saveMetaItem"
],
"recommendation": "A, if the cloud tool saves through saveMetaItem the pin is cheap and closes the ruling's letter; B leaves the ruling's MCP clause NOT MEASURED."
}
],
"out_of_scope_findings": [
"carrier: PM (cloud lane, open_questions[2]) · MCP door pin for ruling item 3 not constructible in objectstack; noted in PR Acceptance notes",
"carrier: none · IAutomationService.canonicalizeStoredFlow (packages/spec/src/contracts/automation-service.ts) declares (name, definition); the includeRetired option lives on AutomationEngine and the protocol's structural reader only, so an implementation ignoring the third argument would silently heal a new flow; the one in-repo implementation honours it — noted in PR Acceptance notes, not filed (承接者:无)",
"carrier: none · scripts/adr-anchors/packages__metadata-protocol__src__protocol.ts.json does not name the new-flow predicate (outside the claim surface) — noted, not filed (承接者:无)",
"carrier: none · a wait node carrying only the loose config.duration is refused at waitEventConfig (the node rule) and the loose key's own prescription surfaces only in a second round — authoring-funnel behaviour, identical at os validate — noted, not filed (承接者:无)",
"carrier: none · the #4580 fallback warning still fires when a gate after the schema gate refuses (runtime authoring rules, the authoring gate) — pre-existing, narrowed by this PR — noted, not filed (承接者:无)"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
ACCEPT → PR #23902 comment 6215638408 (Tier H: the maintainer brief on top, the seat check below) · contract review 6215619986 PASS ·
needs-user-decisionon the PR; review requested fromos-zhuangandhotlong·domain:engineseat 2 ·session_01TQLy3Yq6vF4SDpTXdr3Uwi· 2026-10-16T12:37Z. The PR stays draft until an authorised approval; the seat lands it after.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
Fork ruled on #23903 (batch #356 item 1, record 6216220018) · director seat (summon #43 continuation 2,
session_01QJC7i981TSs9L1CGJK59Ax) · 2026-10-16T13:11Z#23903 asked this door's question again without this card's ruling in view. Its A and B are settled by 6212082860 (letter B, executed by PR #23902). Its one new sub-question is ruled B′: a retired spelling that a re-save NEWLY INTRODUCES on a stored flow is refused like a new flow's; it follows as its own
domain:enginecard (#23903,pm:blockedon this card) after PR #23902 lands, extendingflowSaveReplaysRetired. Read-time canonicalization (its C) is not taken. Nothing changes for PR #23902.objectstack-fleet commentedon Oct 16, 2026
Landing record: PR #23902 →
cf071687f1·domain:engineseat 2 ·session_01TQLy3Yq6vF4SDpTXdr3Uwi· 2026-10-16T13:13Zmerged: true, merge commitcf071687f1, through the merge queue (queued byos-zhuangafter the approval)os-zhuang(inGOVERNED_APPROVERS), review 5547942423, APPROVED on head4a199348b9, the head CI was green onorigin/maincf071687fis an ancestor oforigin/main(2ef46279c);protocol.tsthere carriesflowSaveReplaysRetired; ADR-0087 carries the2026-10-16 note;.changeset/23775-flow-save-door-new-refusal.mdis presentgh-readonly-queue/main/*pr-23902*head remainsState change: this card closed
completedbyFixes #23775;pm:dispatchedremoved in this act.needs-user-decisionleft the PR at the seat's landing step. The other two cards that closed in the same second, #23879 and #23385, were closed by hand byos-zhuang(no commit on their timelines), not by this PR.What the approval confirmed (the maintainer brief on 6215638408): the predicate reads the door's existing baseline (draft head, else the active row a draft overlays), and the server's own writes of bodies at rest keep the full chain.
Carried: ruling item 3's MCP-door pins are with the
repo:cloud#1seat (6215683940 on #6026); that clause reads NOT MEASURED until it lands. The contract review record is 6215619986.Generated by Claude Code