Repository navigation
feat(service-automation, metadata-protocol): a flow override declared in BASE.custom is the packaged flow, saved, served and armed in place of the base (#28412) - #28693
Conversation
…override is the packaged flow ADR-0146 D9 item 1 for flow (#28412). A flow saved into the shipping package's paired <base>.custom under the base flow's name, declaring overrides: { package: <base>, basedOn }, is now: - admitted by the D2 override write gate (flow is open; action still waits), at the draft save, the publish, os validate and install; - served by every read in the base's slot, under the base's provenance, judged by isServedOverrideRowOf on the stored package_id; - armed by the boot pull, kernel:ready, metadata:reloaded and the mutation sync in place of the base (new `override` precedence rank, read off the stored binding the registry hydration records with a non-enumerable symbol), keeping the base's ledger identity; - removable: a delete naming the pair is admitted, and every step re-arms the base with one registration. Every other stored row of a shipped flow name stays shadowed; the remedy now names the .custom declaration or a new name, not a clone. Co-Authored-By: Claude <noreply@anthropic.com>
…ow override; a declared action still waits Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin bec0324a9826f761b8a9fd12b55038b7fab4aa59 && git checkout bec0324a9826f761b8a9fd12b55038b7fab4aa59
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fa494d0538fe69e19be4a21ab13b92e8e6ae8d23 56a1fe5968aa39ff344b57455d578514c8bd7e12 && git checkout -B drift-repro fa494d0538fe69e19be4a21ab13b92e8e6ae8d23 && git merge --no-ff 56a1fe5968aa39ff344b57455d578514c8bd7e12
node scripts/docs-audit/affected-docs.mjs --json fa494d0538fe69e19be4a21ab13b92e8e6ae8d23
|
|
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC Contract reviewServed-tier: Inputs: card #28412 (body; the triage record, the claim, the os-dev-report), this PR's body, its 23-file list and the net diff ① Derived judgmentsRight, the widening the card asks for (Wanted 1-6):
Wrong: the /meta pair-row delete widens beyond the card (MUST-FIX).
② Semver level
③ Boundary flags
Must-fix before landing: the non-flow pair-row delete widening in ①. Fix (a) needs no ruling. Implemented-by: VERDICT: FAIL Generated by Claude Code |
The contract review on #28693 found isPairRowRemoval keyed on isOpenOverrideType, so a /meta delete naming the pair also slipped past the sealed-removal refusal for every other open type: a mapping pair-row delete answered success instead of 403 NOT_OVERRIDABLE, and a view pair-row delete reached the store as runtime-only instead of main's override-artifact intent. The removal is now admitted only for a type whose ADR-0146 D9 conditions are delivered (CONDITIONS_DELIVERED, flow today). Two real-engine pins hold main's verdict for the other open types: the mapping delete still refuses on the host-config and env kernels and keeps the row, and the view delete still tells the store override-artifact. Co-Authored-By: Claude <noreply@anthropic.com>
…olumn since #28594 Main's #28671 (#28594) made storedOverlayEntries serve a stored row's _packageId from its package_id column and drop the body's own stamp, so the hydration pin that expected the body stamp on the registry body now reads the pair. The assertion follows main's column-wins rule; the storedRowBindingOf and not-serialized assertions, the #28412 binding contract, are unchanged. The stored-row-binding header no longer says the hydrated body keeps the stored stamp. Co-Authored-By: Claude <noreply@anthropic.com>
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: Taken under Generated by Claude Code |
|
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC To triage#1, on Contract-review-claim The head moves in the next few minutes, away from
Generated by Claude Code |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract reviewServed-tier: This is a delta review. It follows the FAIL record 6559358939 on head Inputs: the compare ① Derived judgmentsMust-fix from 6559358939 is closed (fix a).
Delta commit
The rest of ①, as accepted in 6559358939, is unchanged. Save-door admission, view admission, the boot-pull binding, rank and ledger, the remedy and the public surface ( Non-blocking prose nits. Neither one is a declaration line.
② Semver level
③ Boundary flags
No must-fix remains on this head. Of the two nits in ①, the PR body sentence can be edited without moving the head. The Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC 席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #28412. 23 files (+1423 / −81). @objectstack/metadata-protocol and @objectstack/service-automation are minor. No packages/spec/** and no new gate. The head has not moved since the review. It is mergeable_state: clean against current main, which already carries #28734 (7e6e8a4647). |
| Must-fix closed | isPairRowRemoval is now gated on CONDITIONS_DELIVERED = {'flow'}, so the other 11 open override types get main's delete verdict back. Two new real-engine pins cover this: a mapping pair-row delete answers 403 NOT_OVERRIDABLE, and a view pair-row delete keeps intent override-artifact. CI ran them (protocol-flow-custom-override.test.ts, 7 tests). The delta commit 56a1fe59 aligns one hydration pin with #28671's column rule. |
| Evidence | CI green on head 56a1fe59 (filter=latest: 35 success, 13 skipped, 0 failed). |
| Nits | Triage's PR body nit is fixed in place without moving the head: line 29 now reads "of a flow". Three comment corrections ride the card thread's #28717 PR as its first commit, so this head does not move for prose. They are the protocol.ts receipt-intent comment near L28632 from triage's nit, the stored-row-binding.ts header sentence that #28734 made false, and the #23233 header in hydrateOverlayIntoRegistry, which is #28734's review should-fix S1. |
| Carriers | Out-of-scope (b) is on #28570 (6559375158). Out-of-scope (c) is carried by #28717 (p2, seat 1, starting from main after this lands). |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, with the contract review PASS on this head and every check green or an expected skip. |
Generated by Claude Code
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
Fixes #28412
Clause-②: yes (widening: a flow saved into the shipping package's paired .custom under the base flow's name, declaring overrides, is admitted at the save door, the publish, os validate and install where it was refused with declared_type_not_yet_available, and a /meta delete naming that pair removes its row; nothing that was admitted is refused)
Metadata-protocol: semantic
维护者速读
BASE.custom里用同名并声明overrides: { package: BASE, basedOn }来覆盖。保存、发布、os validate、安装都放行;所有读取都在基础包的位置上返回这份覆盖;自动化引擎在启动拉取、kernel:ready、metadata:reloaded、元数据变更同步这四步都用覆盖替换基础流程。删除覆盖后,基础流程原地重新挂上。overrides、并且由存储的package_id列绑定的那一行才算覆盖,绝不看正文里的_packageId。依赖包的声明、正文冒充配对包的行、配对包未声明的同名行,照旧被遮蔽并告警。action仍是「暂不可用」。不改 spec、不改 ADR。回滚即还原本 PR。packages/services/service-automation/src/flow-precedence.ts(新增override优先级,只认存储绑定);packages/metadata-protocol/src/protocol-lock-state.ts的declinesStoredRow(用 N-10 的isServedOverrideRowOf放行这一行);packages/metadata-protocol/src/stored-row-binding.ts(把package_id列带到注册表正文上的不可枚举 symbol)。pnpm --filter @objectstack/service-automation exec vitest run src/flow-sync-one-precedence.test.ts,预期 16/16 通过(四个挂载步骤、删除复原、开关、N-12 挂起运行)。What changes
The save door (Wanted 5).
isOpenOverrideTypeinprotocol-packaged-base-refusal.tsadmitsflowthrough aCONDITIONS_DELIVEREDset (flowonly), so N-09'sjudgeOverrideWriteadmits the pair's declared flow. There is no second gate.actionkeeps "action customization is not yet available".tenantAuthoredWriteRefusalno longer applies the packaged-base lock to a write the gate admits.The remedy (Wanted 6). The flow's sealed save sentence names the declaration in place of the clone route: "Declare an override from 'BASE.custom' or rename it to customize it, or switch it off (...)". A partner package gets "Rename it to customize it (only 'BASE.custom' may declare an override of 'BASE')". This comes from the new
regimeCDeclaredSaveSentenceinpackaged-base-regime.ts, which keeps the switch route and the ADR-0126 citation, and stays under the REST door's 500-character cap. A delete still names the clone and the switch. The boot pull's collision warning now says "declare it as an override in 'BASE.custom' (overrides: { package: 'BASE', basedOn }), or save it under a new name". It no longer says "clone".The flow view (Wanted 2).
declinesStoredRowtakes the stored row it judges. For a shipped flow name it declines every row except the oneisServedOverrideRowOf(row, owner)admits, judged on thepackage_idcolumn. The list, the execution view, the by-name read, the layered read andlistServedOverridesall pass the row. So every read serves the override once, in the base's slot, under the base's_packageId, which is what N-10 already does for presentation types.The binding the boot pull reads. The boot pull reads
registry.listItems('flow'), and a hydrated body's_packageIdis not trustworthy (withRowBindingkeeps a body's own stamp). SohydrateOverlayIntoRegistryrecords the row'spackage_idon the hydrated flow body as a non-enumerableSymbol.forproperty (bindStoredRow, read withstoredRowBindingOf, both newly exported). That property cannot be authored in JSON,JSON.stringifydrops it, and a spread or copy does not carry it.The precedence (Wanted 1, 3, 4).
classifyContenderasks first, and only inside a name the loader's set holds: is this the owner's declared override, byisServedOverrideRowOf({ type: 'flow', package_id: storedRowBindingOf(item), metadata: item }, owner)? If yes, it is anoverridecontender, ranked 0, abovepackage(1) andruntime(2). An override armed over its own base's body writes no receipt and no warning: that is the customized state, not a collision. The ledger identity is unchanged, because the override keeps the base's name andpackagedFlowOwnerstill answers the base's package. The four arming steps already go throughresolveFlowContenders, andregisterFlowre-binds in one synchronous step, so the syncs needed no code change.Delete restores the base (Wanted 3). A
/metadelete naming the pair, of a flow, is admitted past the sealed-removal refusal and told to the store asruntime-only, as the declared save is (isPairRowRemoval). Without this, the override could be saved but never removed through the door. The real-engine test found it.⛔ No
packages/spec/**and nodocs/adr/**hunk. No new error code.Tests
New cases, and red first on main (34 red across 5 suites)
Production sources restored to main
0b6a52e8(the newstored-row-binding.tshelper kept so the tests compile),metadata-protocolandservice-automationrebuilt:service-automationflow-name-shadowing+flow-precedence-loader-set+flow-sync-one-precedence:Tests 14 failed | 41 passed (55). Red: the real-registry boot pull in both registration orders, the override classification, override armed with no receipt, the remedy sentence on all five not-admitted rows, override beside an undeclared row, the boot pull,kernel:ready,metadata:reloadedand the mutation sync.metadata-protocolprotocol.flow-stored-row-shipped-name+protocol.packaged-base-refusal:Tests 15 failed | 71 passed (86). Red: the execution view, the served list,listServedOverrides, the hydrated binding, delete serving the loader again, the save sentence pins, and the declared save at the save door,publishMetaItemandpublishPackageDrafts.objectqlprotocol-flow-custom-override(new, realObjectQLengine, packages installed through the package door):Tests 3 failed | 1 passed (4). Red: save, publish and every read (by-name with and without the package, layeredcode/effective, served list, execution view); the undeclared remedy; delete serving X's flow again.cloud-connectioninstall gate:Tests 1 failed | 6 passed (7).clioverride-declarations(theos validatejudge):Tests 1 failed | 6 passed (7).Green on main by construction, and kept as pins: the "not admitted" cases (a dependent's declaration, a package-less row stamping the pair, a body that only claims the pair, the pair's undeclared row, another base's pair), the ledger off/on case, and the N-12 case. The ledger identity and version pinning are inherited by the override, not added here.
Restore:
git checkout HEAD -- packages,git status --porcelainempty, both packages rebuilt.Ablations, one per arming step (each red, restored by blob hash)
node scripts/ablation-replace.mjson the committed head, under the verify lock.metadata-protocolwas rebuilt in each leg and checked withablation-dist-preflight.mjs.flow-precedence.ts, theisServedOverrideRowOfbranch gated off):Tests 9 failed | 46 passed (55). Each step is red on its own:× the boot pull arms the override over the loader's body,× the kernel:ready sync arms the override,× the metadata:reloaded re-sync: publishing the override arms it…,× the metadata-mutation sync: publishing the override arms it…, plus the real-registry and pure-precedence cases.protocol-lock-state.ts, every stored row of a shipped flow name declined):metadata-protocolTests 5 failed | 81 passed;objectql× the save door admits… every read serves it(by-name (no package): expected 'SHIPPED' to be 'OVERRIDE') and× deleting the override serves X's flow again.protocol.ts,bindStoredRowskipped):× the hydration records the row's own package_id…;objectqlexpected [ undefined, undefined ] to include 'com.example.showcase.custom'.protocol.ts, the delete told to the store asoverride-artifact):objectql× deleting the override serves X's flow again.After each leg:
ablation-replace: ok restored: blob == HEAD … git diff HEAD is empty. After all four:metadata-protocolrebuilt,ablation-dist-preflight --absentexit 0 for ABL1 to ABL4,git status --porcelainempty.Pins whose meaning changed (updated in the same commit)
metadata-protocolprotocol.packaged-base-refusal.test.ts: the flow save sentence now names the declaration (the literal pin is rewritten); the "declared flow override is refused" case becomes five cases (pair admitted at the save door,publishMetaItem,publishPackageDrafts; partner refused "only BASE.custom may declare"; pair undeclared refused with declare, no clone).metadata-protocolprotocol.sealed-remedy-kind.test.ts: a Regime C type whose override is open names the declaration in place of its clone route.metadata-protocolprotocol.register-item-call-sites.test.ts: the hydrator'sregisterItemarguments are nowtype, registered, 'name'(the body is built one line above so the binding can be recorded). The disposition is unchanged.restrest-meta-packaged-flow-refusal.test.tsandruntimeautomation-packaged-base-lock.test.ts: a save names the declaration and not the clone; a delete still names the clone.clioverride-declarations.test.ts: a declared flow is admitted now; the "not yet available" row moves to a declaredaction.Full suites, typecheck, lint, gates
metadata-protocolfull suite:Test Files 2 failed | 289 passed,Tests 4 failed | 21954 passedbefore the call-site and Regime C pin updates above; after them, both files pass (5/5 and 13/13).service-automationshadowing/precedence/sync:Tests 55 passed (55).objectqlnew file4 passed.cloud-connectioninstall gate7 passed.clioverride-declarations7 passed,validate-override-declarations2 passed.rest4 passed.runtime15 passed.pnpm --filter PKG run typecheck: exit 0 for metadata-protocol, service-automation, objectql, cli, rest, runtime, cloud-connection.pnpm lint's invocation) over those seven packages: exit 0.dispatch-gates.mjs --commands, 68 families) plus the four roster gates under touched directories (check:authz-resolver,check:changeset-fixed,check:error-code-casing,check:route-ledger-census): all exit 0. On the first pass,check:dual-build-cjs-loads,check:i18n,check:i18n-coverageandcheck:i18n-walk-parityrefused with exit 3 (unbuilt workspace packages, nothing measured), and the teardown self-test with exit 3 (its pinned fixture commit was missing from the shallow clone). Each was re-run after the build and the fetch, and exited 0.--ran:68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN.metadata-protocolorservice-automationsources.git merge-treeof each head against this head is clean, and none pins the flow refusal or the clone sentence.Acceptance notes
kernel:readyarms the override from the view a moment later. Rows like that are what the gate now refuses; N-16 migrates the old ones.deletablefor the served override still follows the base's seal. Studio reads it, and N-18 owns that surface.pnpm turbo run build --filter=@objectstack/dogfoodthenpnpm turbo run test --filter=@objectstack/dogfood, unsharded) on head0e67bba0:Test Files 369 passed | 3 skipped (372),Tests 2981 passed | 28 skipped (3009), VERDICT command-exit 0.🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5oTRCUNQ4EA3YV84sZ