Repository navigation
feat(service-automation)!: the flow index and the activation ledger consult on (package, name) (ADR-0150 E1) - #31738
Conversation
…through the package-scoped resolver The flow index and the activation ledger projection consult the package (ADR-0150 D3/D4). A subflow or map child named inside a package flow resolves from that package: its own flow, else the one holder in its declared dependency closure, else a refusal naming every candidate. A name two packages hold is refused at registration instead of arming one of them. The ledger projection switches off the (package, name) a row names. Claude-Session: https://claude.ai/code/session_01ADHkmQjmHnuYAuaRd9k252 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmQjmHnuYAuaRd9k252 Co-authored-by: Claude <noreply@anthropic.com>
…the projection's receiver A flow name two packages hold reaches the engine only from a deployment that held the clash before phase 1, sealed at its cold boot with a warning. Refusing it is a maintainer ruling still open, and phase 1's cold-boot pins read it as armed, so the engine now acts only on the resolver's outside-closure refusal (D3 (a), D3 step 3) and the arming gate in registerFlow is left out. The activation projection's Set receiver is annotated so the tenant-audit census can place its delete(). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADHkmQjmHnuYAuaRd9k252
📓 Docs Drift CheckThis PR changes 1 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 10 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 38fd92b9d1739005a3b3804c80e4e53cdb587141 && git checkout 38fd92b9d1739005a3b3804c80e4e53cdb587141
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f65c36fcca8c33d38c62104c0888c950a315089a 1081c7770bfa860b55635ae45c9348bd4845312e && git checkout -B drift-repro f65c36fcca8c33d38c62104c0888c950a315089a && git merge --no-ff 1081c7770bfa860b55635ae45c9348bd4845312e
node scripts/docs-audit/affected-docs.mjs --json f65c36fcca8c33d38c62104c0888c950a315089a
|
|
epic:#31527#1 · os-steve · session_01ADHkmP7o3uDTti21wLTfPf Epic red-line review: E1 #31672, head e94f303 · 2026-11-09T01:06Z Verdict: PASS once one small change is in. It needs no re-review; send the head.
The change: a held-back refusal is still reported.
Recorded obligations:
The D4 activation projection keyed by (package, name) is accepted: an environment-wide row ( Seat 3 accepts and lands this after the change and green CI. It is not a governed surface. |
…r name and caller Holding back the act on a name two packages hold waits for the maintainer's ruling; the report does not. The engine now warns once per (name, caller) with the resolver's sentence naming every candidate, and says the flow precedence's pick still runs until that ruling. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADHkmQjmHnuYAuaRd9k252
…d is reported A ledger row whose package no longer holds the flow name is stale: it keeps switching the name's current holder off, as before rows were keyed, and is warned once naming both packages and the remedy (switch the flow again, which writes the holder's own row). A row of a package that still holds the name keys only that package's flow, and the holder's own row, in either state, decides for it. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADHkmQjmHnuYAuaRd9k252
…led clash included The maintainer ruled that a name two packages held before phase 1 is refused like any other two-holder name. The engine now refuses arming such a name and refuses running it, naming both holders, in place of the interim warning; the phase-1 dogfood pins on the sealed clash now read neither flow armed and the refusal in the boot log. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADHkmQjmHnuYAuaRd9k252
…ne reader The merge brought the connector reconcile's own registry slice reader and a second import of the same type. The flow-name reader now asks that one reader too, so the slice the package-scoped resolver needs is checked in one place. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADHkmQjmHnuYAuaRd9k252
|
epic:#31527#1 · os-steve · session_01ADHkmP7o3uDTti21wLTfPf Epic red-line review, final: E1 #31672, head d7a38bb · 2026-11-09T02:34Z Verdict: PASS.
Seat 3 accepts and lands this after green CI. It is not a governed surface. |
…-ack2ot # Conflicts: # packages/qa/dogfood/test/bare-name-one-holder-flow-family.dogfood.test.ts
|
domain:server#3 · os-steve · session_01ADHkmQXLfvyB5R53VDm3Ka 席位核对 · ACCEPT · #31738 (Fixes #31672, ADR-0150 E1) · 2026-11-09T03:04ZHead-sha:
|
… the calling flow's package (objectstack-ai#31863) <!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9B9qYj7GTyDGfaMgXwXx7rZ)_ Fixes objectstack-ai#31677 Clause-②: no Metadata-protocol: none Narrowing: a `connector_action` in a packaged flow whose `connectorConfig.connectorId` does not resolve for the flow's package is refused, naming every candidate, instead of dispatching to whatever instance the engine holds under the name. Three sub-cases: the only holder is outside the flow package's declared closure (the closure-outside sub-case of objectstack-ai#31527 comment 6728487836, a sibling package in the same artifact included); the only instance is environment-authored (ADR-0150 D3 (a)); two packages in the closure hold the name. First-party count: 0 (every first-party packaged flow names its own package's instance or a connector a plugin registers in code; HotCRM 6961ab1, ADR-0150's census). This closes card objectstack-ai#31677 (ADR-0150 child E5, epic objectstack-ai#31527): it is the card's last PR. Connectors landed in objectstack-ai#31725, webhooks in objectstack-ai#31744, email templates in objectstack-ai#31825. This part is the remainder those PRs named: `connector_action` resolving `connectorConfig.connectorId` from the calling flow's package (seat answer 6728849524, Q3), now that E1 objectstack-ai#31738 gives a run its flow's package. **Epic review R1 (6735080780), fixed here:** the caller package now follows the body the engine ARMS under the flow's name, not the loader's set by bare name. An environment override of P's flow (a package-less stored row, or P's declared `.custom` override) names its references with no package, per ADR-0150 D3's overlay paragraph. That corrects E1's `subflow`/`map` reader too, back to main's answer before E1 for overrides. It is a loosening, not a new narrowing. Names were accepted by seat 3 (6735158321). Card thread `domain:server#3` · `os-steve` · `session_01ADHkmR4astMzES5UkW47jW`; claim 6728454873, file-surface amendments 6728498905 and 6728849524. ## 维护者速读 - 改了什么:flow 里的 `connector_action` 按名字找连接器实例前,先用 R 的共享解析器 `resolvePackageScopedName`,以「这个 flow 当前生效的定义所属的软件包」为调用方去问;解析器拒绝时节点失败并列出全部候选。引擎在每次注册 flow 时记下生效定义的来源(包本身、`.custom` 覆盖、环境自建),只有包本身的定义才以该包为调用方;覆盖和环境自建一律按无包调用方解析(E1 的 subflow 也一并纠正)。 - 为什么改:ADR-0150 把连接器改为按(软件包, 名称)寻址;以前包里的 flow 会派发到引擎里同名的任何实例,哪怕它属于闭包外的包或只是环境自建。 - 风险与回滚:只影响「包内 flow 指向自己够不着的已声明连接器」的部署:只有闭包外的包持有、只有环境自建、或闭包内两个包同名。第一方计数 0。环境自建的 flow、代码注册的连接器(slack、rest 等)照常派发。回滚即还原本 PR。 - 先看:`engine.ts` 的 `armedFlowPackage` 和 `registerFlow` 的 `armedFrom`,`flow-precedence.ts` 里识别协议视图下发的覆盖文档那一段,再看 `connectorNameRefusal`。 - 两分钟验证:`pnpm --filter @objectstack/service-automation exec vitest run src/flow-package-scoped-resolution.test.ts -t "connector_action|environment override"`,预期 17 条通过:连接器的 8 条,加上三种覆盖形态各自的 connectorId 与 subflow 都按无包解析(6 条)、包本身定义的两条对照、写入口的一条。 - 需要你做的:无,席位落地。本 PR 合并后 objectstack-ai#31677 关闭。 ## Step 0: where the flow's package comes from | What | Where on `main` | Used here as | |:--|:--|:--| | The run's flow name | `AutomationContext.flowName`, set for every run `execute` starts | the key | | Which package ships that flow | `AutomationEngine.packagedFlowOwner(name)`, fed by `setPackagedFlowSource(packagedFlowReader(ctx))` (E1 objectstack-ai#31738; the protocol's `packagedArtifactOwner`) | the caller package | | Both together | private `flowCallerPackage(context)` (E1): `context.flowName` → `packagedFlowOwner`, `null` for an environment-authored flow or a call outside a run | **corrected (R1)**: `context.flowName` → `armedFlowPackage` | | Which body is armed under the name | `resolveFlowPrecedence` → `classifyContender` at the boot pull, the `kernel:ready` bind, the `metadata:reloaded` re-sync and the mutation sync; the class was dropped after the pick, and a single-contender name was never classified | each winner carries its `contender`, recorded at `registerFlow` | Measured on the real composition (`flow-declared-override-first-boot-pull.integration.test.ts`: ObjectKernel, ObjectQLPlugin over driver-sql/better-sqlite3, two cold boots): at the boot pull the declared override arrives as a stored row carrying its binding, so `classifyContender` already answers `override`. At `kernel:ready` the protocol's view serves it with no binding, stamped `_packageId` = the base beside `overrides.package` = the base, and `isCodeArtifactBody` answered **`package`**. That is the stamp trap; the classifier now recognises that shape. ## What changes - `engine.ts`: a `ConnectorNameResolver` type, `setConnectorNameResolver`, and `connectorNameRefusal(connectorId, context)`. It asks the resolver for the flow's package and returns the refusal when there is one. A caller with no package, a name the registry holds nothing under (a connector a plugin registers in code), or a host with no resolver returns `undefined` and dispatches by name as before. - `builtin/connector-nodes.ts`: `connector_action` checks `connectorNameRefusal` before `resolveConnectorAction` and fails the node with the resolver's message, prefixed with the node id. - `connector-package-scope.ts`: `connectorNameResolver(ctx)`, the ONE resolver (`resolvePackageScopedName`, type `connector`) over the registry ObjectQL serves, read at question time like E1's `flowNameResolver`. - `plugin.ts`: attaches it at `init()`, beside `setFlowNameResolver`. R1, the shared reader: - `engine.ts`: `registerFlow(name, definition, armedFrom?: FlowContender)` records the arm in a `WeakMap` keyed by the parsed body it stores. With no `armedFrom`, the arm is `runtime` (a write door's body is the caller's own). The exception is a body that IS the parsed object the engine armed (the `/automation` door putting back `held`), which keeps its arm by identity. `armedFlowPackage(name)` (public, read-only) returns `packagedFlowOwner(name)` only when the armed body's arm is `package`, else `undefined`. `flowCallerPackage` reads it. - `flow-precedence.ts`: every winner carries `contender`, single-contender names included. `classifyContender` also answers `override` for the served shape: `_packageId` equal to the owner AND `isServedOverrideRowOf` holding for the row the pair would store it as (the reading `servedOverrideBaseOf` makes, through the exported predicate). A package-less row that declares the same carries no envelope and stays `runtime`. - `plugin.ts`: the four arming steps pass `entry.contender`. Kept for the connector backstop child (ADR-0150 D9), as the review recorded: dispatch is still `resolveConnectorAction(connectorId)` by bare name, which is right only while phase 1's one-holder refusal keeps one instance per name. Why a refusal is the only answer that needs acting on: the engine holds one instance per name, materialized for the no-package answer (the declared-connector reconcile from objectstack-ai#31725 already refuses a name two packages hold). In the registry an environment item exists only for a name no package holds (a bare registration under a package-held name keeps the package holder), so a package caller whose name resolves at all resolves to the instance the engine serves. No second resolver, no first-registered fallback. | Reader on `main`, by symbol | Pick today | Caller package | After this PR | |:--|:--|:--|:--| | `connector_action` executor (`registerConnectorNodes`) → `engine.resolveConnectorAction(connectorId, actionId)` | the engine's one instance under the name | the flow's package (`flowCallerPackage`, E1) | a refusal for that package fails the node naming every candidate; any other answer dispatches as before | ## ADR-0150's four readings - **(a) phase-1 one-holder tests stay green.** The four `bare-name-one-holder*` dogfood files are in the full run below. - **(b) bare-name behaviour unchanged for every name with one holder the caller can reach.** Pinned: own package, the one closure holder, an environment-authored flow, a code-registered connector. The change is the declared narrowing above, plus R1's loosening for overrides back to main's pre-E1 answer. No URL, MCP or SDK surface moves. - **(c) no migration.** Nothing stored is read or written differently. - **(d)** `Clause-②: no`, `Metadata-protocol: none`; no ADR-0087 entry. ## Tests `service-automation/src/flow-package-scoped-resolution.test.ts`, new describe "ADR-0150 D3 — a connector_action names its connector from the calling flow's package (epic objectstack-ai#31527 child E5)", over a real `SchemaRegistry` with E1's install/ship/author helpers. 8 tests: four dispatch as before (own package, closure holder, environment-authored flow, code-registered connector); red line 1 (holder only outside the closure, refused naming it, nothing dispatched); D3 (a) (environment-only, refused); red line 2 (two closure holders, refused naming both); and a booted `AutomationServicePlugin` on a `LiteKernel` attaching the resolver at init. R1, same file, describe "ADR-0150 D3 — an environment override of a packaged flow names its references with no package". Each body is armed as the plugin arms it (`resolveFlowPrecedence`, then `registerFlow(…, winner.contender)`). There are three override shapes: a package-less row, the `.custom` override as a stored row, and the `.custom` override as the served envelope. Each is pinned for both `connectorId` (an environment-only instance is reached) and `subflow` (an environment-only flow is reached), 6 tests. Two CONTROLs: P's own body (a package arm) still answers P, so the same references are refused. One more pin: a write door's body is `runtime`, and putting back the held body restores its `package` arm. `flow-declared-override-first-boot-pull.integration.test.ts`, new describe on the real composition, 4 cases: the loader's body alone and beside a package-less row answer P; the declared override, alone or beside a row, answers no package. Each is checked at the boot pull and after `kernel:ready`. Changed existing test code: `flow-precedence-loader-set.test.ts` "asks the set once per contested name, and never for an uncontested one" becomes "once per name, contested or not" (see Verification). E1's `ship()` helper now arms with `{ source: 'package', packageId }`, as the plugin does, and the plugin pin arms P's `charge` before asking. Ablations (`scripts/ablation-replace.mjs`, run over both test files, each restored to the HEAD blob, `git diff HEAD` empty after; the suite imports `src`, so no `dist` leg): | Mutation | Result | |:--|:--| | `connectorNameRefusal` returns `undefined` for every answer (engine) | 3 failed: red line 1, D3 (a), red line 2 | | the `connector_action` refusal check removed (node) | 3 failed: the same three | | `setConnectorNameResolver(connectorNameResolver(ctx))` removed (plugin) | 1 failed: the plugin pin, `expected undefined to be 'refused'` | | R1: `flowCallerPackage` back to `packagedFlowOwner(caller)` (main's E1 reader) | 6 failed: every override shape × connectorId and subflow; both CONTROLs stay green | | R1: the served-envelope `override` branch disabled (classifier) | 4 failed: the served-shape connectorId and subflow pins, and the two real-kernel override cases at `kernel:ready` | | R1: the plugin's four `registerFlow(…, entry.contender)` calls without the arm | 2 failed: the real-kernel cases whose loader body must answer P | ## Verification Head c6a0810 (main 2e9121e merged in as 5e37bfd; c6a0810 adds one test-only commit on top): - `@objectstack/service-automation` test exit 0 (267 files, 3518 tests) and typecheck exit 0 at c6a0810, under `os-verify-lock` (one `&&` command, VERDICT command-exit 0). Root `eslint --no-inline-config packages/services/service-automation`: exit 0. - The first local run, at 5e37bfd, had 1 red: `flow-precedence-loader-set.test.ts` pinned "never asks the loader's set for an uncontested name". R1 classifies every name for its arm, so c6a0810 restates that pin as "once per name, an uncontested one included", and adds that the uncontested body's claimed id buys nothing. CI's Test Core 5/8 showed the same 1 red at 5e37bfd. - `turbo run build --filter='@objectstack/dogfood^...'`: 70/70. - Full dogfood run (owed for the declared narrowing), `pnpm --filter @objectstack/dogfood test` under the lock at 5e37bfd: VERDICT command-exit 0, Test Files 441 passed / 3 skipped (444), Tests 3662 passed / 42 skipped (3704). The only change since is that test file. - Derived gates at c6a0810 (the same 73 as at 5e37bfd): 73 derived, all 73 `exit 0`; `--ran`: "73 derived, 73 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero). - `test:repo` not run locally (CI's `Test Core`); this diff touches no repo-project test. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01ADHkmR4astMzES5UkW47jW <!-- signed: domain:server#3 · os-steve · session_01ADHkmR4astMzES5UkW47jW --> --------- Co-authored-by: Claude <noreply@anthropic.com>
Requested by Steve · project thread
Fixes #31672
Clause-②: no (narrowing)
Metadata-protocol: none
Narrowing: (1) D3 (a), as the card declares: a package flow's bare
subflow/mapreference whose only holder is an environment-authored flow is refused (outside-closure, naming the environment). (2) D3 step 3, ruled inside ADR-0150 by the epic seat (#31527 comment 6728487836): a package flow's bare reference whose only holder is a package outside its declared dependency closure is refused (outside-closure, naming that package). Measured first-party for (1) and (2): 0, including the sibling-in-one-artifact sub-case (ADR-0130 co-ownership): 0. (3) A flow name two packages hold, which only a deployment sealed at its cold boot with a clash that predates phase 1 reaches (#31677 Q1, ruled A, comment 6729831159): arming it is refused and a run of it is refused, naming both, so neither flow is armed (it used to arm the lexicographically first package's flow). Measured first-party: 0. Methods under Verification.Skills-docs: follow-up #31741
epic:#31527 child E1 ·
domain:server#3· dev sessionsession_01ADHkmQjmHnuYAuaRd9k252维护者速读
resolvePackageScopedName。软件包内流程的子流程/映射节点先找本包,再找它声明依赖闭包里唯一的持有者,否则拒绝并列出全部候选。两个软件包同名的流程(包括冷启动封存的 phase 1 之前的旧冲突)在注册和运行时都拒绝,不再按包名字典序挑一个。启停台账按(软件包, 名称)生效:过期行仍然关闭当前持有者,并警告一次。engine.ts的registerFlow与execute两处flowNameRefusal闸门;plugin.ts的flowNameResolver;flow-activation-projection.ts。pnpm --filter @objectstack/service-automation exec vitest run src/flow-package-scoped-resolution.test.ts,预期 20 条全绿。What changes
Step 0 — every read point that looks a flow up by bare name today (on
origin/mainb126c13):kernel:readybind, reload re-sync, mutation sync all end here)engine.tsregisterFlow(viaplugin.tsresolveFlowContenders→flow-precedence.tsresolveFlowPrecedence, which arms the lexicographically-first package on two package holders)engine.tsexecute(this.flows.get(flowName))subflow/mapchildren (the calling flow's package)subflow/mapchild namebuiltin/subflow-node.ts,builtin/map-node.ts→engine.execute(flowName, childContext)childContext.flowName, stamped byresolveRunContext)flow-definitions.tshydrateFlowActivations,toggleFlow; read byisFlowEnabled,describeDisabledFlow, the §7.3 subflow guards (subflow-guards.ts)(package, name)(FlowActivationProjection)engine.tspackagedFlowOwner(the loader's set,plugin.tspackagedFlowReader)getFlow,getFlowRegistration,toggleFlow,getFlowVersionHistory,rollbackFlow,unregisterFlow,flowCredentialHoldingsflowName), credential channel, dispatch keys, approvalsflow:NAME, history by checksumgetFlow(flowName)builtin/screen-nodes.tsStep 1. Both gates call one engine seam,
flowNameRefusal, which asks the reader the host attaches (setFlowNameResolver, new). The automation plugin attachesflowNameResolver(ctx):resolvePackageScopedNamefrom@objectstack/objectqlover the ObjectQL registry, read at question time (the startup-registry rule). ⛔ No second resolver and no first-registered fallback: the engine reads only the resolver'srefusedanswer;resolvedandnot-foundleave the existing path (a name the registry knows nothing of has one holder, the engine's own map).executereadscontext.flowName, which the engine stamps on every run context (resolveRunContext), so a child call carries its parent's name; the caller package is that flow'spackagedFlowOwner, or none for a door, a trigger, or an environment-authored flow (ADR-0150 D3: an environment item resolves as a caller with no package).registerFlowasks for a caller with no package, so a name two packages hold (ambiguous-in-environment) throws the resolver's sentence before anything is armed. Each of the four call sites already catches a registration throw and logs it as a warning carrying the flow and the sentence naming both packages (the boot pull's is[Automation] failed to register flow).flowLedgerDisabledbecomes aFlowActivationProjectionthat keeps each row's package and state. The row of the package that ships the name decides for it, in either state. A row of another package that still holds the name keys only that package's flow (D4). A stale row, whose package no longer holds the name, still switches the shipping package's flow off, as before rows were keyed, and is warned once naming both packages and the remedy: switch the flow again, which writes the shipping package's own row (epic seat ruling B on Q3). "Still holds" is the resolver reaching that package's own flow; with no resolver it reads as stale, so the row keeps the flow off. A row naming no package binds to the one holder (D8's backfill reading). The two writers stayhydrateFlowActivationsandtoggleFlow(the existing pin inflow-activation-ledger.test.tsholds).@objectstack/objectqlis a runtime dependency of service-automation. E5 made that move onmainfirst, so after the merge it is no longer in this diff. No cycle: objectql's runtime workspace closure (11 packages) does not contain service-automation;pnpm check:workspace-manifest-cyclesgreen. The flow reader asks for the registry slice through E5'spackageScopedRegistryOf, so the slice is checked in one place.ADR-0150's four readings.
flowstays inBARE_NAME_ONE_HOLDER_TYPESand this PR touches nothing in objectql. Of phase 1's dogfood pins, exactly the two that read the sealed clash's armed flow move to the refusal, as webhooks, connectors, email templates on (package, name) (ADR-0150 child E5) #31677 Q1 rules; every other pin is unchanged.executewith no run context, so they resolve environment-wide, and a name with one holder resolves (pinned:reading (b)block, plus the door control in the red-line-1 block).Clause-②: no (narrowing); no protocol change, so no ADR-0087 migration entry (not-required (no-migration-prescription)in the changeset).Tests
New
src/flow-package-scoped-resolution.test.ts(20 cases) over a realSchemaRegistry(packages throughinstallPackage, so the declared closure is the save door's) and the plugin's ownflowNameResolver:FlowActivationProjectionunit cases.Forced test changes:
packages/qa/dogfood/test/bare-name-one-holder-flow-family.dogfood.test.ts, the two pins that read the sealed clash's flow as armed (armedFlowLabel('qa_shared_flow') === 'alpha flow', in the cold-boot case and the third-package install case) now read it unarmed and find the refusal naming both packages in the boot log. The seat directed this after #31677 Q1 was ruled A. The file's header now states the refusal. No other existing test changed.Acceptance notes
flow. B objectql: narrow the phase-1 one-holder refusal from environment to namespace for flow (ADR-0150 child B, flow family) #31696 must fail closed on a missing slice or prove every composition carries it (the epic seat's recorded obligation, same as E5's).resolveFlowPrecedencestill logsFlow name collision: … arming <first> by package id, and the refusal line follows it naming both. The wording is pinned bypackages/cli/src/utils/format.boot-warning-classes.test.ts, outside this card's file surface, so it is reported as a follow-up and not changed here.success: falsewith the resolver's sentence and nocode: no new error code is minted. The doors'409 RESOURCE_CONFLICTfor an ambiguous bare address is G1's (D5). E2 (actions) and E6 (api endpoints) callers execute with no caller package until their cards land (seat: A).Verification
Ablations (each refusal and ledger pin; mutation through scripts/ablation-replace.mjs on a committed head, restored, blob == HEAD, git diff HEAD empty)
registerFlowgate deleted (head c2f2031):Tests 1 failed | 19 passed (20): the two-holder registration pin goes red.executegate deleted (head c2f2031):4 failed: D3 (a), D3 step 3, the sealed-clash door run and the ambiguous-in-closure caller all go red (the child runs: this is also the base reading, the behaviour onmain).continue(head 4cf79d48):Tests 2 failed | 15 passed (17): the stale-row engine pin and unit pin go red.2 failed | 15 passed (17): the same two pins go red on the warning.3 failed | 14 passed (17): the foreign-holder row, stale-row remedy and toggle pins go red.First-party narrowing measures
type: 'subflow'/type: 'map'node inexamples/**andpackages/**outside tests and the spec (git grepon b126c13): 5 nodes, all inexamples/app-showcase, namingshowcase_notify_owner,showcase_closure_signoff,showcase_one_task_signoff,showcase_recalc_task_estimate, all flows of the same package. Refused by D3 (a): 0; by D3 step 3: 0; sibling-in-one-artifact sub-case: 0.Bare-name conflict at bootwarning appears 4 times and names one flow,qa_shared_flow, the QA fixture that builds the clash on purpose. First-party: 0.Gates, suite and dogfood (head d7a38bb, merged with main at c28d8e7)
dispatch-gates.mjs --ran: 76 derived, 76 run, 0 NOT-MEASURED, 0 UNRUN; every recorded exit is 0 on d7a38bb. An earlier pass (bcc5ba6) caught one real finding, fixed here:check-tenant-audit-censuscould not read the receiver of the projection'sdelete(), so the local is annotated (noUNTYPED_RECEIVERSrow added).vitest run267 files / 3478 tests passed;tsc --noEmit0; eslint on the package and the dogfood file 0.turbo run build --filter='@objectstack/dogfood^...'thenpnpm --filter @objectstack/dogfood test, under the verify lock):Test Files 440 passed | 3 skipped (443),Tests 3652 passed | 41 skipped (3693), VERDICT command-exit 0. That includes the two moved phase-1 pins.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01ADHkmQjmHnuYAuaRd9k252