Repository navigation
Commit 5d3667e
fix(cli)!: os validate and os build refuse a flow whose region the automation engine refuses to register (#29043)
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X8dNy8r76WjFsVdvaSJbXXR)_
Fixes #28893
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no file under `skills/**` teaches what `os validate`
/ `os build` refuse about flow regions; the region rules themselves are
unchanged.
## 维护者速读
- 问题:`os validate` 和 `os build` 放行一个 `try_catch` / `loop` / `parallel`
区域不是单入口单出口(或单分支 parallel)的 flow,build 还照常产出 artifact;引擎在 `registerFlow`
里用 `validateControlFlow` 拒绝它,启动只记一条 `[Automation] failed to register
flow` 警告,然后不带这个 flow 继续运行,触发器从不触发。
- 现在:两条命令对每个 flow 调用引擎注册时的同一判定(`FlowSchema`,再 spec 导出的
`validateControlFlow`),任意深度;拒绝信息就是引擎原文(`<where>: <errors>`)前面加 flow
名,退出码 1,build 不写 `dist/`。规则没有第二份拷贝,未改 `packages/spec/**`。
- 这是作者期的收窄(`Clause-②: no (narrowing)`,标题带 `!`):新拒绝的每个 flow
本来就注册失败、从未运行,运行中的 flow 不受影响。
- 未做(已列入 out_of_scope):`os lint` 仍看不见这个检查(parity 测试里以 `blindTo:
['lint']` 钉住);顶层图的环(`detectCycles` 在 service-automation 内且未导出)validate
仍放行;启动横幅不计"注册失败"数,只在 Boot diagnostics 里逐条出现。
## What changes
- **`packages/cli/src/utils/flow-region-shape.ts`** (new):
`findFlowRegionRefusals(parsed)` walks each `packages[]` entry's flows,
or the stack's own flows when it declares no packages (the walk
`findExtensionFieldSuffixRefusals` takes), and hands each one to the
engine's registration verdict: `FlowSchema.safeParse`, then
`validateControlFlow` from `@objectstack/spec/automation`. A refusal is
a row `{ path, code: 'VALIDATION_ERROR', flow, message }` whose message
is `Flow '<name>' would be refused at registration: ` followed by the
engine's message verbatim. Like the engine it stops at a flow's first
malformed region; every flow is judged. A flow the schema refuses is
left to the schema step.
- **`commands/validate.ts`** step 3a-vi and **`commands/compile.ts`**
(`os build`) step 3b-iii-ter call it beside the `__c` reservation gate,
ahead of every artifact write, with the same two faces as the gates
around them: the command's `--json` failure envelope (`errors` rows),
and the text face (`printError` + one bullet per flow, exit 1).
- **`test/validate-build-gate-parity.test.ts`**:
`findFlowRegionRefusals` joins `SHARED_NON_REGISTRY_GATES`, the ledger
of gates both commands run.
Placement (A), in lane, following #28727: a `cli/src/utils` judge over
the spec's exported function. No `packages/spec/**`, `packages/lint/**`
or `service-automation` edit.
## Tests
- `test/validate-flow-region-shape.test.ts` (new, in-process `Validate`
/ `Compile`, 4 cases):
- the duly shape (`try_catch 'guard'` whose catch region is `d → x, y,
z` with no join): validate `--json` exits 1 with a row carrying
`try_catch 'guard' catch: region must be single-exit but has 3: x, y, z`
and the flow name; build `--json` exits 1, `success: false`, the same
row, and no `dist/`;
- nested (a `loop` whose body is that fan-out, inside a `try_catch` try
region): the same, with `try_catch 'outer' try → loop 'sweep' body: …`;
- single-branch `parallel`: `parallel 'fan': a parallel block needs at
least 2 branches`, both commands;
- control: the joined region (`x, y, z → j`) validates and builds.
- On `origin/main` (`ff881050`): the three refused cases red (validate
exited 0, build succeeded), the control green.
- Ablations, each landed on disk then restored: the call in
`validate.ts` replaced by `[]` → 3 red; the call in `compile.ts`
replaced by `[]` → 3 red (`expected +0 to be 1`).
- `test/authoring-rule-command-parity.test.ts`: per the seat's ruling,
`os lint`'s blindness is recorded as `blindTo: ['lint']`. It sits in a
second table, `OUTSIDE_THE_TABLE`, not in `CASES`: every `CASES` row
asserts the rule gates on all three commands through
`runAuthoringRules`, and this check is not an authoring rule, so a
`CASES` row would be red by construction. The new row ASSERTS the
blindness (`runAuthoringRules('lint', …)` carries no `single-exit`
finding), so closing the gap reds it and the case moves into `CASES`.
- One `os validate` run (real CLI, text face) over the duly fixture:
```
base (ff88105) head
→ Running author-time rules (60)... → Running author-time rules (60)...
→ Checking that every required capability…
→ Checking package docs (ADR-0046)... ✗ The automation engine would refuse to register 1 flow for a malformed region (ADR-0031)
✓ Validation passed (166ms) • flows[0] [VALIDATION_ERROR] Flow 'rgn_lead_time' would be refused at registration:
Logic: 1 Flows try_catch 'guard' catch: region must be single-exit but has 3: x, y, z — add one node
exit 0 with an edge from each of x, y, z to it, so it is the region's only exit; …
exit 1
```
## Acceptance notes
- **Clause-② reading.** The order asked for `yes (narrowing)` if the
gates read it so. `clause2-line.mjs` reads `yes (narrowing)` as "widens
one surface and narrows another"; nothing here widens (no new export,
schema key or accepted input; `flow` on the error row is a new member of
an existing row list, like `rule` / `packageId` on its neighbours). So
the line is `no (narrowing)`, the spelling #28727 used for the same kind
of change; the changeset carries the `**BREAKING**` banner and an
ADR-0087 `not-required` disposition.
- **Boot side (ruling 3), measured, not changed.** The banner's `Flows:`
line (`utils/format.ts` `printAutomationSummary`, ~1718) counts
registered flows (`flowCount` = `getFlowRuntimeStates().length`), bound
flows and drafts. `declaredFlowCount` (`commands/serve.ts` ~5708,
`resolveStackCollection(config, 'flows')`) is read only for the
engine-disabled line. A refused flow is in neither count; it surfaces
only as its `[Automation] failed to register flow` line in Boot
diagnostics. A refused count would be one more `parts.push` beside the
draft count, but no one-line, same-file version is exact:
`declaredFlowCount - flowCount` is moved by runtime overlay rows and
same-named contenders too, and the engine exposes refusals only per name
(`getFlowRegistration(name)`), so an exact count needs a new summary
field collected in `serve.ts`. Left out, reported. Whether boot
continues is untouched.
- **Not closed here, reported**: the lint-rule parity (`os lint` blind,
B, domain:devx), and a cycle in the flow's TOP-LEVEL graph, which `os
validate` still passes (`start → a → b → a → end`, exit 0): the engine
refuses it with `detectCycles`
(`packages/services/service-automation/src/flow-registration-checks.ts`),
which is exported from that file but not from the package index. Region
bodies are cycle-checked, by `validateControlFlow`.
- Seat 3's files (`subflow-guards.ts`, `region-suspension-refusal.ts`,
the arming gate) are untouched.
## Verification
Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 62 commands on the diff; all 62 exit
0, and `--ran` reconciles 62 derived, 62 run, 0 NOT-MEASURED. `pnpm
--filter @objectstack/cli typecheck` (runs `check:test-typecheck`): exit
0. `pnpm --filter @objectstack/spec check:error-code-provenance`: exit
0. Scoped eslint over the six changed `.ts` files: exit 0.
`pnpm --filter @objectstack/cli exec vitest run --project unit`, after
merging `main` (`00d0668d`, nothing under `packages/cli`,
`packages/spec` or `packages/lint`): first run 368/369 files, 1 red:
`validate-build-gate-parity.test.ts` "every call site … is classified"
named `findFlowRegionRefusals`. Fixed by adding it to
`SHARED_NON_REGISTRY_GATES` (a gate both commands run); re-run on the
final head: 369/369 files, 5201/5201 tests, VERDICT command-exit 0. The
`integration` tier (where `authoring-rule-command-parity.test.ts` lives,
for its spawn case) is declared to CI; its in-process rule cases, the
new pin included, ran locally green (10 passed).
Dogfood battery (owed for the `(narrowing)` arm): after building the
`@objectstack/dogfood...` closure (68/68 tasks), `pnpm --filter
@objectstack/dogfood test` under the lock, VERDICT command-exit 0 · Test
Files 383 passed | 3 skipped (386) · Tests 3163 passed | 28 skipped
(3191). It ran on `832bd06d`; the final head adds the ledger row in a
`packages/cli` test file and one more merge of `main` (`ab085a04`: under
`packages/spec` only `shared/region-currency.*` and a save-notice
placement change with a `FlowSchema` comment, nothing the region check
reads).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5kV6ptpTnU6Hjb2mE4)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 636432c commit 5d3667e
7 files changed
Lines changed: 381 additions & 0 deletions
File tree
- .changeset
- packages/cli
- src
- commands
- utils
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
86 | 89 | | |
87 | 90 | | |
88 | 91 | | |
| |||
837 | 840 | | |
838 | 841 | | |
839 | 842 | | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
840 | 866 | | |
841 | 867 | | |
842 | 868 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
84 | 87 | | |
85 | 88 | | |
86 | 89 | | |
| |||
751 | 754 | | |
752 | 755 | | |
753 | 756 | | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
754 | 787 | | |
755 | 788 | | |
756 | 789 | | |
| |||
| 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 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
142 | 142 | | |
143 | 143 | | |
144 | 144 | | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
145 | 192 | | |
146 | 193 | | |
147 | 194 | | |
| |||
160 | 207 | | |
161 | 208 | | |
162 | 209 | | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
163 | 220 | | |
164 | 221 | | |
165 | 222 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
213 | 213 | | |
214 | 214 | | |
215 | 215 | | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
216 | 225 | | |
217 | 226 | | |
218 | 227 | | |
| |||
0 commit comments