Repository navigation
Commit 55c4e95
feat(service-automation)!: the flow index and the activation ledger consult on (package, name) (ADR-0150 E1) (#31738)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9U3goQBvwMsJHNB3R7mZR7M)_
Fixes #31672
Clause-②: no (narrowing)
Metadata-protocol: none
Narrowing: (1) D3 (a), as the card declares: a package flow's bare
`subflow` / `map` reference 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 session
`session_01ADHkmQjmHnuYAuaRd9k252`
## 维护者速读
- 改了什么:流程引擎按(软件包, 名称)解析裸流程名,全部经 R 的共享解析器
`resolvePackageScopedName`。软件包内流程的子流程/映射节点先找本包,再找它声明依赖闭包里唯一的持有者,否则拒绝并列出全部候选。两个软件包同名的流程(包括冷启动封存的
phase 1 之前的旧冲突)在注册和运行时都拒绝,不再按包名字典序挑一个。启停台账按(软件包,
名称)生效:过期行仍然关闭当前持有者,并警告一次。
- 为什么改:专题 #31527 子卡 E1(ADR-0150 D3/D4),以及 #31677 Q1 裁决 A。
- 风险与回滚:收窄三处,第一方实测都是 0 处。单持有者的裸名、REST 门、触发器行为不变。phase 1 的 2 条 dogfood
用例按裁决改为读拒绝。回滚即还原本 PR。
- 先看:`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 条全绿。
- 需要你做的:无。席位在史诗席复核通过且 CI 绿后落地。
## What changes
**Step 0 — every read point that looks a flow up by bare name today**
(on `origin/main` b126c13):
| # | Read point | File / symbol | Caller | This PR |
|:--|:--|:--|:--|:--|
| 1 | arming a name (boot pull, `kernel:ready` bind, reload re-sync,
mutation sync all end here) | `engine.ts` `registerFlow` (via
`plugin.ts` `resolveFlowContenders` → `flow-precedence.ts`
`resolveFlowPrecedence`, which arms the lexicographically-first package
on two package holders) | no package | gated: the resolver is asked for
a caller with no package; a name two packages hold is refused, naming
both, and nothing is armed |
| 2 | running a flow by name | `engine.ts` `execute`
(`this.flows.get(flowName)`) | doors, triggers (no package); `subflow` /
`map` children (the calling flow's package) | gated: the resolver is
asked for the caller's package |
| 3 | `subflow` / `map` child name | `builtin/subflow-node.ts`,
`builtin/map-node.ts` → `engine.execute(flowName, childContext)` | the
parent flow's package (`childContext.flowName`, stamped by
`resolveRunContext`) | resolved through #2 |
| 4 | activation ledger projection | `flow-definitions.ts`
`hydrateFlowActivations`, `toggleFlow`; read by `isFlowEnabled`,
`describeDisabledFlow`, the §7.3 subflow guards (`subflow-guards.ts`) |
— | keyed by `(package, name)` (`FlowActivationProjection`) |
| 5 | which package ships a name | `engine.ts` `packagedFlowOwner` (the
loader's set, `plugin.ts` `packagedFlowReader`) | — | unchanged; it is
now also the caller package of #2 and the holder #4 reads |
| 6 | context-free by-name reads | `getFlow`, `getFlowRegistration`,
`toggleFlow`, `getFlowVersionHistory`, `rollbackFlow`, `unregisterFlow`,
`flowCredentialHoldings` | no package | unchanged: they read the
engine's one armed slot per name, which #1 now fills only for a name
with one holder |
| 7 | stored references by flow name | run store (`flowName`),
credential channel, dispatch keys, approvals `flow:NAME`, history by
checksum | — | not here: S3 #31684 (R7–R10, R14, R20) adds the package
columns |
| 8 | screen node `getFlow(flowName)` | `builtin/screen-nodes.ts` | the
run's own flow | unchanged: reads the running flow's own definition |
**Step 1.** Both gates call one engine seam, `flowNameRefusal`, which
asks the reader the host attaches (`setFlowNameResolver`, new). The
automation plugin attaches `flowNameResolver(ctx)`:
`resolvePackageScopedName` from `@objectstack/objectql` over 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's `refused` answer; `resolved` and `not-found` leave
the existing path (a name the registry knows nothing of has one holder,
the engine's own map).
- **Caller package.** `execute` reads `context.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's
`packagedFlowOwner`, 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).
- **Arming.** `registerFlow` asks 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`).
- **Activation.** `flowLedgerDisabled` becomes a
`FlowActivationProjection` that 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 stay `hydrateFlowActivations` and
`toggleFlow` (the existing pin in `flow-activation-ledger.test.ts`
holds).
- **Dependency.** `@objectstack/objectql` is a runtime dependency of
service-automation. E5 made that move on `main` first, 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-cycles` green. The flow reader asks for
the registry slice through E5's `packageScopedRegistryOf`, so the slice
is checked in one place.
**ADR-0150's four readings.**
- **(a)** Phase 1's one-holder tests stay green: `flow` stays in
`BARE_NAME_ONE_HOLDER_TYPES` and 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 #31677 Q1 rules; every other pin is
unchanged.
- **(b)** Bare-name behaviour is unchanged for every name with one
holder, except the declared narrowing: a door, a trigger, MCP and the
SDK reach `execute` with 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).
- **(c)** No migration in this PR.
- **(d)** `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 real
`SchemaRegistry` (packages through `installPackage`, so the declared
closure is the save door's) and the plugin's own `flowNameResolver`:
- own flow, closure holder, environment caller to a package holder,
engine-only flow: all run;
- red line 1: D3 (a) refused naming the environment; D3 step 3 refused
naming the stranger package; the door control runs the same environment
flow;
- red line 2: a name two packages hold is refused at registration naming
both, nothing armed; a door run of a sealed clash is refused naming
both; a package caller whose closure holds the name twice is refused
naming both; control: one holder arms;
- reading (b): a door runs a packaged flow by its bare name; with no
resolver attached a subflow reaches an environment flow as before;
- D4: the shipping package's row switches its flow off, a row of another
package that still holds the name does not; a stale row still switches
the holder off, is warned once naming both packages and the remedy, and
the remedy works; the toggle door writes and reads under the shipping
package; four `FlowActivationProjection` unit 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
- **D4's "all same-named flows arm" is not done here.** The engine keeps
one armed slot per bare name and refuses a name two packages hold. Per
the epic seat, arming both is a prerequisite of B #31696 (which narrows
phase 1 for flows), not of S3: it becomes reachable only when phase 1
stops refusing a second flow holder.
- **A missing resolver slice refuses nothing.** An engine with no
ObjectQL registry answers as before; accepted while phase 1 refuses for
`flow`. B #31696 must fail closed on a missing slice or prove every
composition carries it (the epic seat's recorded obligation, same as
E5's).
- **The precedence's collision line still says "arming … by package
id".** For two package holders `resolveFlowPrecedence` still logs `Flow
name collision: … arming <first> by package id`, and the refusal line
follows it naming both. The wording is pinned by
`packages/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.
- The §7.3 subflow guards still list callers by bare reference, so a
caller whose reference is now refused still counts as reaching the child
(a stricter guard, unchanged from today).
- A refused child call answers `success: false` with the resolver's
sentence and no `code`: no new error code is minted. The doors' `409
RESOURCE_CONFLICT` for 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
<details><summary>Ablations (each refusal and ledger pin; mutation
through scripts/ablation-replace.mjs on a committed head, restored, blob
== HEAD, git diff HEAD empty)</summary>
- The `registerFlow` gate deleted (head c2f2031): `Tests 1 failed | 19
passed (20)`: the two-holder registration pin goes red.
- The `execute` gate 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
on `main`).
- The projection's "still holds" check replaced by `continue` (head
4cf79d4): `Tests 2 failed | 15 passed (17)`: the stale-row engine pin
and unit pin go red.
- The stale-row report deleted (head 4cf79d4): `2 failed | 15 passed
(17)`: the same two pins go red on the warning.
- The holder's own row no longer deciding (head 4cf79d4): `3 failed |
14 passed (17)`: the foreign-holder row, stale-row remedy and toggle
pins go red.
</details>
<details><summary>First-party narrowing measures</summary>
- (1), (2): every `type: 'subflow'` / `type: 'map'` node in
`examples/**` and `packages/**` outside tests and the spec (`git grep`
on b126c13): 5 nodes, all in `examples/app-showcase`, naming
`showcase_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.
- (3): across every boot of the full dogfood battery (bcc5ba6), the
cold boot's `Bare-name conflict at boot` warning appears 4 times and
names one flow, `qa_shared_flow`, the QA fixture that builds the clash
on purpose. First-party: 0.
</details>
<details><summary>Gates, suite and dogfood (head d7a38bb, merged with
main at c28d8e7)</summary>
- **Derived gates:** `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-census` could not read the receiver of the
projection's `delete()`, so the local is annotated (no
`UNTYPED_RECEIVERS` row added).
- **Package:** service-automation `vitest run` 267 files / 3478 tests
passed; `tsc --noEmit` 0; eslint on the package and the dogfood file 0.
- **Dogfood** (`turbo run build --filter='@objectstack/dogfood^...'`
then `pnpm --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.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmQjmHnuYAuaRd9k252)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01ADHkmQjmHnuYAuaRd9k252
<!-- signed: domain:server#3 · os-steve ·
session_01ADHkmQjmHnuYAuaRd9k252 -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 62cbf9a commit 55c4e95
8 files changed
Lines changed: 676 additions & 17 deletions
File tree
- .changeset
- packages
- qa/dogfood/test
- services/service-automation/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
19 | | - | |
| 18 | + | |
| 19 | + | |
20 | 20 | | |
21 | | - | |
| 21 | + | |
| 22 | + | |
22 | 23 | | |
23 | 24 | | |
24 | | - | |
| 25 | + | |
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
| |||
224 | 225 | | |
225 | 226 | | |
226 | 227 | | |
227 | | - | |
228 | | - | |
229 | | - | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
230 | 233 | | |
231 | 234 | | |
232 | 235 | | |
233 | 236 | | |
234 | 237 | | |
235 | 238 | | |
236 | | - | |
| 239 | + | |
237 | 240 | | |
238 | 241 | | |
239 | 242 | | |
| |||
243 | 246 | | |
244 | 247 | | |
245 | 248 | | |
246 | | - | |
| 249 | + | |
247 | 250 | | |
248 | 251 | | |
249 | 252 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
224 | 224 | | |
225 | 225 | | |
226 | 226 | | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
227 | 232 | | |
228 | 233 | | |
229 | 234 | | |
| |||
2567 | 2572 | | |
2568 | 2573 | | |
2569 | 2574 | | |
| 2575 | + | |
| 2576 | + | |
| 2577 | + | |
| 2578 | + | |
| 2579 | + | |
| 2580 | + | |
| 2581 | + | |
| 2582 | + | |
| 2583 | + | |
| 2584 | + | |
| 2585 | + | |
2570 | 2586 | | |
2571 | 2587 | | |
2572 | 2588 | | |
| |||
2766 | 2782 | | |
2767 | 2783 | | |
2768 | 2784 | | |
2769 | | - | |
| 2785 | + | |
| 2786 | + | |
| 2787 | + | |
| 2788 | + | |
| 2789 | + | |
| 2790 | + | |
| 2791 | + | |
| 2792 | + | |
| 2793 | + | |
| 2794 | + | |
| 2795 | + | |
| 2796 | + | |
| 2797 | + | |
| 2798 | + | |
| 2799 | + | |
| 2800 | + | |
| 2801 | + | |
| 2802 | + | |
| 2803 | + | |
| 2804 | + | |
2770 | 2805 | | |
2771 | 2806 | | |
2772 | 2807 | | |
| |||
2781 | 2816 | | |
2782 | 2817 | | |
2783 | 2818 | | |
| 2819 | + | |
| 2820 | + | |
| 2821 | + | |
| 2822 | + | |
| 2823 | + | |
| 2824 | + | |
| 2825 | + | |
2784 | 2826 | | |
2785 | 2827 | | |
2786 | 2828 | | |
| |||
4538 | 4580 | | |
4539 | 4581 | | |
4540 | 4582 | | |
| 4583 | + | |
| 4584 | + | |
| 4585 | + | |
| 4586 | + | |
| 4587 | + | |
| 4588 | + | |
| 4589 | + | |
4541 | 4590 | | |
4542 | 4591 | | |
4543 | 4592 | | |
| |||
4933 | 4982 | | |
4934 | 4983 | | |
4935 | 4984 | | |
| 4985 | + | |
| 4986 | + | |
| 4987 | + | |
| 4988 | + | |
| 4989 | + | |
| 4990 | + | |
| 4991 | + | |
| 4992 | + | |
| 4993 | + | |
| 4994 | + | |
| 4995 | + | |
| 4996 | + | |
| 4997 | + | |
| 4998 | + | |
| 4999 | + | |
| 5000 | + | |
| 5001 | + | |
| 5002 | + | |
| 5003 | + | |
| 5004 | + | |
| 5005 | + | |
| 5006 | + | |
| 5007 | + | |
| 5008 | + | |
| 5009 | + | |
| 5010 | + | |
| 5011 | + | |
| 5012 | + | |
| 5013 | + | |
| 5014 | + | |
| 5015 | + | |
| 5016 | + | |
| 5017 | + | |
| 5018 | + | |
| 5019 | + | |
| 5020 | + | |
| 5021 | + | |
| 5022 | + | |
| 5023 | + | |
| 5024 | + | |
| 5025 | + | |
| 5026 | + | |
| 5027 | + | |
| 5028 | + | |
| 5029 | + | |
| 5030 | + | |
| 5031 | + | |
| 5032 | + | |
| 5033 | + | |
| 5034 | + | |
| 5035 | + | |
| 5036 | + | |
| 5037 | + | |
| 5038 | + | |
4936 | 5039 | | |
4937 | 5040 | | |
4938 | 5041 | | |
| |||
6150 | 6253 | | |
6151 | 6254 | | |
6152 | 6255 | | |
| 6256 | + | |
| 6257 | + | |
| 6258 | + | |
| 6259 | + | |
| 6260 | + | |
| 6261 | + | |
| 6262 | + | |
6153 | 6263 | | |
6154 | 6264 | | |
6155 | 6265 | | |
| |||
0 commit comments