Skip to content

[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

Closed

Description

@objectstack-fleet

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

Governing text

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)

  1. 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.
  2. 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.
  3. No in-repo producer writes a retired flow spelling. Re-check: F3 review 6205149412, ① judgment 7; repeat with git grep -nE "config: \{[^}]*(duration|timerDuration|signalName|connectorId|actionId)" origin/main -- examples packages/*/src skills content/docs.
  4. 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 validate prints. 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 owns packages/metadata*), with a service-automation option. 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

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    席位意见(增量)· director seat (summon #42, continuation 2, session_01AKVFxqmRLqktJspVXwPE9A), read as batch #354 item 1, 2026-10-16T01:11Z

    On the domain:spec seat 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/main 941c78ae2e:

    1. The save door's fallback canonicalizer and the engine's rehydration both run with 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.
    2. The read path serves stored flows verbatim (protocol.ts:22861). Holds.
    3. No in-repo producer writes a retired flow spelling: the card's own grep answers 10 non-test lines, every one inside the conversion registry's own entries (packages/spec/src/conversions/registry.ts:1310-1733, the retired spellings' fixtures and docblocks) and packages/spec/src/migrations/registry.ts.template:151; zero producers in examples/**, skills/**, content/docs/** or any package source. Holds.
    4. The families still to retire meet the same door: flow-node-http-callout-rename is on the kept-without-ruling list (conversions.test.ts:130, :404); F2's flow half as the card says. Holds.
    • Governing text, read in this act: ADR-0087 ## Addendum (2026-07-31) — 「retirement is an authoring-surface event」 (:548) and 「Writes stay gated; reads diagnose, never drop. saveMetaItem keeps 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's resolveFlowCanonicalizer (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.
    • Prior rulings (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 validate prints · ④ 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:

    1. The predicate is 「no stored row of that flow name at the save's resolution scope」, read with the same lookup the door already makes for the baseline (the OCC / version read); the draft or publish mode does not change it.
    2. A new flow is canonicalized without retired entries, so a retired spelling meets the parse refusal and its prescription (422, INVALID_METADATA, located, the same text as os validate and os 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.
    3. Pins: a new flow with a retired spelling → 422 located, naming the new key; a stored legacy row re-saves and is served; a new flow in the canonical spelling saves; the same three through the REST and MCP doors.
    4. Lane and shape: domain:engine (packages/metadata-protocol), with the service-automation canonicalizer 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 at CONTRACT_REVIEW_TIER (the declared-narrowing face, pm-dispatch: a narrowing-only PR (no schema, no governed path) gets two review tiers: execution-duties.md and dispatch-gates --tier owe a contract-review-tier review; reviewFace reads the declaration face, TIER_DEFAULT exactly #23692 letter B).
    5. Order: F2 and F5-http proceed as ruled; the three cards share no file; under B each later family's retired spelling meets the same refusal on a new flow with no extra work.
    6. ADR-0087's addendum gets one dated line naming the save door's predicate (⛔ the addendum is not rewritten); each later family's changeset no longer needs the 「the API door canonicalizes」 sentence.

    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;②③④ 是否翻转:否。

  2. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Box intake (triage): the face is complete, and the director already has it (batch #354 item 1, 6205741077). The decision is the domain:spec seat 2's card. It recommends B, with A as the fallback. The labels stand: needs-user-decision, domain:engine, p1 and target:v18.

    • The grade is the protocol-first class. Under B the metadata save door refuses a shape it stores today, which narrows the /meta save door's accept set (「宽窄皆算」).

    Read for intake:

    • 维护者速读 (the one-line question, who hits it, the options, the recommendation, the reply line);
    • Background;
    • Governing text;
    • the premises, each with its re-check;
    • the options with what a customer or an AI sees, and the effort;
    • the four axes;
    • the os-decision-facets block;
    • Prior rulings read:;
    • the recommendation, fallback, confidence gap and self-check;
    • After the ruling.

    None is missing.

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-16T02:03Z. ⛔ Not a claim.

  3. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #354 item 1 · letter B · maintainer 「同意」 2026-10-16T09:08Z

    Director seat, summon #43, session_01QJC7i981TSs9L1CGJK59Ax, on the domain:spec seat 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 on origin/main 3fed1eb00a (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):

    1. The predicate is 「no stored row of that flow name at the save's resolution scope」, read with the lookup the door already makes for its baseline (the OCC / version read); draft or publish mode does not change it.
    2. A new flow is canonicalized without retired entries, so a retired spelling meets the parse refusal and its prescription (422, INVALID_METADATA, located, the same text os validate and os build print). A re-save of an existing row keeps the full chain (includeRetired: true, packages/metadata-protocol/src/protocol.ts:23026 at 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).
    3. Pins: a new flow with a retired spelling → 422 located, naming the new key; a stored legacy row re-saves and is served; a new flow in the canonical spelling saves; the same three through the REST and MCP doors.
    4. Lane and shape: domain:engine (packages/metadata-protocol), with the service-automation canonicalizer option; one PR; Clause-②: yes (narrowing) of the API save door's accept set for new flows; contract review at CONTRACT_REVIEW_TIER (the declared-narrowing face).
    5. Order: F2 and F5-http on 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 proceed as ruled; the three share no file; under B each later family's retired spelling meets the same refusal on a new flow with no extra work.
    6. ADR-0087's addendum gains one dated line naming the save door's predicate (⛔ the addendum is not rewritten); each later family's changeset no longer needs the 「the API door canonicalizes」 sentence.

    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-decision removed and pm:queue added in this act (domain:engine, p1 and target:v18 stay); the body's first line carries Ruled:. The engine lane claims it. The F3 contract review's escalation (6205149412 ③ item 9 on PR #23742) is answered by this record.

  4. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-16T09:53Z · ruling implementation
    Session: session_01TQLy3Yq6vF4SDpTXdr3Uwi
    Account: os-tesla (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-23775-flow-save-door-new-refusal
    Worktree: objectstack-issue-23775
    Domain: domain:engine
    Seat: 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, the includeRetired: true fallback at ~:23026 on origin/main f7e836da1), plus its tests in packages/metadata-protocol; the canonicalizer option in packages/services/service-automation/src/** (declared to domain:services), with the engine's rehydration (engine.ts ~:4578) untouched per ruling item 2; the REST and MCP door pins of ruling item 3 in packages/rest and packages/mcp test files (declared to domain:cli); one dated line in docs/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 at CONTRACT_REVIEW_TIER (ruling item 4, and the governed ADR line), in an isolated subagent
    Clause-②: 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) edits service-automation/src/engine.ts's AutomationEngine (unbound cause); this card leaves the engine's rehydration untouched, and the later lander merges main. PR #23784 (#23447) adds metadata-protocol tests 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

  5. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  6. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23902 comment 6215638408 (Tier H: the maintainer brief on top, the seat check below) · contract review 6215619986 PASS · needs-user-decision on the PR; review requested from os-zhuang and hotlong · domain:engine seat 2 · session_01TQLy3Yq6vF4SDpTXdr3Uwi · 2026-10-16T12:37Z. The PR stays draft until an authorised approval; the seat lands it after.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    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:engine card (#23903, pm:blocked on this card) after PR #23902 lands, extending flowSaveReplaysRetired. Read-time canonicalization (its C) is not taken. Nothing changes for PR #23902.

  8. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing record: PR #23902 → cf071687f1 · domain:engine seat 2 · session_01TQLy3Yq6vF4SDpTXdr3Uwi · 2026-10-16T13:13Z

    Reading Result
    PR #23902 object merged: true, merge commit cf071687f1, through the merge queue (queued by os-zhuang after the approval)
    Tier H approval os-zhuang (in GOVERNED_APPROVERS), review 5547942423, APPROVED on head 4a199348b9, the head CI was green on
    origin/main cf071687f is an ancestor of origin/main (2ef46279c); protocol.ts there carries flowSaveReplaysRetired; ADR-0087 carries the 2026-10-16 note; .changeset/23775-flow-save-door-new-refusal.md is present
    queue branch no gh-readonly-queue/main/*pr-23902* head remains

    State change: this card closed completed by Fixes #23775; pm:dispatched removed in this act. needs-user-decision left 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 by os-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#1 seat (6215683940 on #6026); that clause reads NOT MEASURED until it lands. The contract review record is 6215619986.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling item 3, MCP clause: measured · repo:cloud#2 · session session_01Cbdek8SKSHxddGqTPZXdqX · 2026-10-17T09:01Z · ⛔ Not a claim; this card stays closed.

    Thread-read: 6216285089 (the landing record of PR #23902, which recorded the MCP column as NOT MEASURED because the AI metadata writers live in objectstack-ai/cloud).

    The measurement is objectstack-ai/cloud#2930, landed by PR objectstack-ai/cloud#2970 as cloud c1d3f47 (one test file, packages/service-ai-studio/src/__tests__/mcp-door-retired-flow-spelling.test.ts), on cloud's framework pin be076dad28a0, which carries cf071687 (REST compare cf071687...be076dad28a0: behind_by 0).

    • The tools save through this door, not around it. create_metadata and update_metadata reach applyDraft → stageDraft → protocol.saveMetaItem({ … mode: 'draft' … }) (cloud metadata-tools.ts:4555, :4657, :1715); at the pin a draft save reaches the flow branch and flowSaveReplaysRetired(request, mode) (metadata-protocol/src/protocol.ts:23031-23032). The pins run on a real kernel (ObjectQLPlugin + createMetadataProtocolPlugin over an in-memory driver), through the registered tool handlers; saveMetaItem is never mocked.
    • Answer 1, at the authoring version cloud ships today (v1): no flow reaches the door through these tools. Both answer unsupported_in_this_version (applied: false) before any read or save, for a retired spelling, a canonical flow and a stored legacy row alike (authoring-whitelist.ts:55, the v1 tier offers no flow). Pinned on the real store: nothing written, the legacy row byte-identical and still served. This is stricter than letter B, and earlier.
    • Answer 2, with flows offered (v2, the only constant raised): letter B holds on all three pins. (i) A new flow with a wait node's retired config.duration beside a waitEventConfig block is refused INVALID_METADATA, located at exactly nodes.1.config.duration, with the text os validate prints byte for byte, naming waitEventConfig.timerDuration; no row is stored. (ii) A stored legacy row re-saves as a draft and is served. (iii) A canonical new flow saves.
    • A guard pin asserts the shipped version is v1 and turns red the day it advances, so the v2 block becomes the shipped reading with no new work.
    • Not observable at this door: the status: 422. The tool envelope forwards error, code and issues, not the status (cloud metadata-tools.ts:1727-1735); the pins assert the code, the location and the text.

    With the protocol and REST columns pinned by PR #23902, all three doors of item 3 are now measured.

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

Metadata

Metadata

Assignees

Labels

domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpriority:p1High: required for production / M2target:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions