Repository navigation
Commit dd4a21f
fix(cli)!: os validate and os build refuse a flow with an undeclared top-level cycle, as the engine refuses it at registration (#29595)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9BEQHr4NaK8w2SRWffi5xaT)_
Fixes #29051
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none —
`skills/objectstack-automation/references/state-machines-and-approvals.md`
already teaches that a cycle-closing edge MUST be typed `'back'` and
that an unmarked cycle is refused; that stays true, and the CLI now
refuses the same flow before it ships.
## 维护者速读
- 改了什么:`os validate` 和 `os build` 现在拒绝顶层图里有环、且环上没有一条 `type: 'back'`
边的流程(ADR-0044),退出码 1,build 不写产物;报错定位到该流程,正文是引擎注册时那句 "Flow contains a
cycle: …"。
- 为什么改:这种流程以前本地校验、构建都通过,发布后引擎拒绝注册,流程从不运行。#29226 已让发布门拒绝它,CLI 是最后一扇没关的门。
- 风险与回滚:收窄(BREAKING),以前能 build 的这类流程现在 build 失败,但它们本来就从未运行过。判定直接调用
`@objectstack/metadata-core` 的
`findUndeclaredFlowCycle`,与引擎注册、发布门是同一个函数,没有第二份环检测。回滚即还原本 PR,无数据迁移。
-
先看:`packages/cli/src/utils/flow-undeclared-cycle.ts`(新的遍历,一次调用),`packages/cli/src/commands/validate.ts`
第 3a-vii 步与 `compile.ts` 第 3b-iii-quater
步(新的门禁步骤),`packages/cli/test/validate-flow-region-shape.test.ts` 末尾(用同一份
`FLOW_CYCLE_CORPUS` 驱动 validate 与 build)。
- 两分钟验证:`pnpm --filter @objectstack/cli exec vitest run
src/utils/flow-undeclared-cycle.test.ts
test/validate-flow-region-shape.test.ts`,预期 16 个用例全绿;在 main
上环相关用例全红(validate 答 `valid: true`)。
- 需要你做的:无,席位落地。
## What changes
- **The walk** (new `packages/cli/src/utils/flow-undeclared-cycle.ts`):
`findFlowCycleRefusals` walks each `packages[]` entry's flows (or the
stack's own flows when it declares no packages), parses each with
`FlowSchema`, and asks `findUndeclaredFlowCycle` from
`@objectstack/metadata-core` (exported by #29226), the judge the
engine's registration and the runtime publish gate call. No copy of the
DFS. A refused flow is one `VALIDATION_ERROR` row located at the flow
(`flows[N]` or `packages[K].manifest.flows[N]`), message `Flow 'NAME'
would be refused at registration: ` followed by the judge's sentence
verbatim. `@objectstack/cli` already depended on
`@objectstack/metadata-core`.
- **The doors** (`packages/cli/src/commands/validate.ts` step 3a-vii,
`compile.ts` step 3b-iii-quater): one gate step each, the same shape as
the CLI's other walks (`--json`: `errors` rows and exit 1; text: a
heading naming ADR-0044 and the rows). The build writes no artifact.
- **Why a CLI step and not a rule-table entry.** #29495 moved the
ADR-0031 region check into `@objectstack/lint`
(`flow-region-shape-invalid`). `@objectstack/lint` does not depend on
`@objectstack/metadata-core`, so the rule table cannot reach this judge
without a new package edge, and the ruling forbids a copy (seat claim
amendment 6584349353 declares this file surface). So `os lint` does not
give this verdict; `os validate` and `os build` do.
- **Changeset** `.changeset/29051-cli-flow-undeclared-cycle.md`:
`@objectstack/cli` patch, BREAKING banner with before/now and the
remedy, `adr-0087: not-required (no-migration-prescription)`.
- **Test-only config**: `packages/cli/vitest.config.ts` gains one
anchored alias, `@objectstack/metadata-core/testing` →
`../metadata-core/src/testing.ts`, which `check:test-source-alias` names
for the new corpus import. The bare `@objectstack/metadata-core`
specifier is not aliased; its ledger entry is unchanged.
Not touched: `packages/metadata-protocol/**`, `packages/spec/**`,
`packages/lint/**`, `ui/**`.
## Tests
- `packages/cli/src/utils/flow-undeclared-cycle.test.ts` (new): one case
per `FLOW_CYCLE_CORPUS` entry (`@objectstack/metadata-core/testing`, the
corpus the judge, the engine's registration and the publish gate are
driven from). A refused case gives exactly one row at `flows[0]` whose
message is `Flow 'NAME' would be refused at registration: ` +
`undeclaredFlowCycleMessage(cycle)`. A back-typed or acyclic case gives
none. Plus an assembled artifact: the refused flow in the second
`packages[]` entry is located at `packages[1].manifest.flows[1]`, and
the stack's top level is not walked.
- `packages/cli/test/validate-flow-region-shape.test.ts` (new block at
the end, in-process `Command.run`, unit tier): the whole corpus in one
stack. `os validate --json` and `os build --json` each exit 1, with
exactly one row per refused corpus flow at its own `flows[N]` carrying
the judge's sentence; the build writes no `dist/`. CONTROL: the corpus's
back-typed and acyclic flows alone validate and build (exit 0).
- **Forced test edit:**
`packages/cli/test/validate-build-gate-parity.test.ts`. Its ledger
classifies every call site in `compile.ts` and `validate.ts`, so
`findFlowCycleRefusals` joins `SHARED_NON_REGISTRY_GATES` with the
reason it is not a registry rule.
## Acceptance notes
- **Order differs from the engine for a flow with two faults.** The
engine judges the top-level cycle before the ADR-0031 region shape. The
commands run the rule table (`flow-region-shape-invalid`) first, so a
flow with both a malformed region and an undeclared cycle is refused for
its region first, then for the cycle once the region is fixed. Both are
refusals; only which one an author reads first differs.
- **`os lint` does not give this verdict** (see What changes). Closing
that would need a lint → metadata-core edge, or the judge moving to a
package lint already depends on; a separate decision, not taken here.
- The CLI row is located at the flow, as the card's acceptance asks. The
publish gate (#29226) locates its finding on the closing edge
(`edges.N`). The judge returns `closingEdgeIndex`, so the CLI row could
carry the edge later; not done here, to keep the row shape the CLI's
other flow walks use.
- A flow `FlowSchema` refuses is not judged by this walk:
`flow-region-shape-invalid` refuses it at the rule-table step, which
exits before this one.
## Verification
Head `5b16eb34` (origin/main merged at `a5b8e9bf`, after #29495).
`5b16eb34` is a comment-only commit over `1f72080c` (the
`@objectstack/metadata-core/testing` alias comment in
`packages/cli/vitest.config.ts`); the suites and dogfood below were
measured on `a5b8e9bf` / `1f72080c`, and the gate list was re-run on
`5b16eb34`.
<details><summary>Red first on main, green with the fix,
ablation</summary>
- Red, before the fix (base `aca58cdb`, the tests then in their
pre-#29495 home): `Tests 6 failed | 13 passed (19)`. `os validate
--json` on the corpus stack answered `"valid": true` with exit 0. Every
refused corpus case was `expected [] to deeply equal [ { path:
'flows[0]', … } ]`.
- Green on `a5b8e9bf`: `pnpm --filter @objectstack/cli exec vitest run
--maxWorkers=2 src/utils/flow-undeclared-cycle.test.ts
test/validate-flow-region-shape.test.ts` → `Test Files 2 passed (2) ·
Tests 16 passed (16)`.
- Ablation on `a5b8e9bf`, through `scripts/ablation-replace.mjs`: anchor
`const cycle = findUndeclaredFlowCycle(result.data);` replaced with a
null verdict. Output: `anchor x1 -> x0`, `ok mutation landed: anchor 1
-> 0, blob 5f733ce -> 9d51706e9b08`. Run: 6 red (the 4 refused
corpus cases, the assembled-artifact case, the validate/build door
case). Then `ok restored: blob == HEAD … git diff HEAD empty`. The CLI
tests import source, so no dist leg.
</details>
<details><summary>Suites</summary>
- `pnpm --filter @objectstack/cli typecheck`: VERDICT command-exit 0 (on
`a5b8e9bf`; `1f72080c` adds one ledger row to a test file).
- `pnpm --filter @objectstack/cli exec vitest run --project unit` on
`a5b8e9bf`: `Tests 1 failed | 5281 passed (5282)`. The one failure was
`validate-build-gate-parity` naming the unclassified
`findFlowCycleRefusals`, the forced edit above (`1f72080c`). On
`1f72080c`: `validate-build-gate-parity`,
`authoring-rule-command-parity`, `flow-undeclared-cycle`,
`validate-flow-region-shape` → `Test Files 4 passed (4) · Tests 61
passed (61)`.
- `eslint --no-inline-config` over the 7 touched source and test files:
exit 0.
- Full dogfood on `a5b8e9bf` (`pnpm --filter @objectstack/dogfood test`,
after `pnpm install --frozen-lockfile && pnpm build`): VERDICT
command-exit 0 · `Test Files 397 passed | 3 skipped (400)` · `Tests 3277
passed | 28 skipped (3305)`. `1f72080c` changes one CLI test file only.
</details>
<details><summary>Gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `1f72080c`, and again on `5b16eb34` (62
families, all exit 0, `--ran` the same derived zero): 62 families, each
run with its exit code captured before any pipe, all exit 0. `--ran`
reconciliation: `62 derived famil(ies) accounted for — 62 run, 0
NOT-MEASURED (a DERIVED zero — all 62 recorded an exit code and none of
them is 3)`. `check:plugin-teardown-shape --self-test` needed the
shallow-clone deepen (`git fetch --depth=1 origin 621a487…`) first.
</details>
<!-- signed: domain:server#2 · os-steve ·
session_01ADHkmSGEsjUPQp4S968p2X -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 56076fd commit dd4a21f
8 files changed
Lines changed: 272 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 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
87 | 91 | | |
88 | 92 | | |
89 | 93 | | |
| |||
841 | 845 | | |
842 | 846 | | |
843 | 847 | | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
844 | 869 | | |
845 | 870 | | |
846 | 871 | | |
| |||
| 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 | | |
| |||
756 | 759 | | |
757 | 760 | | |
758 | 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 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
759 | 792 | | |
760 | 793 | | |
761 | 794 | | |
| |||
| 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 | + | |
| 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 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
213 | 213 | | |
214 | 214 | | |
215 | 215 | | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
216 | 224 | | |
217 | 225 | | |
218 | 226 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
| 34 | + | |
| 35 | + | |
34 | 36 | | |
35 | 37 | | |
36 | 38 | | |
| |||
151 | 153 | | |
152 | 154 | | |
153 | 155 | | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
154 | 160 | | |
155 | 161 | | |
156 | 162 | | |
| |||
251 | 257 | | |
252 | 258 | | |
253 | 259 | | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
875 | 875 | | |
876 | 876 | | |
877 | 877 | | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
878 | 891 | | |
879 | 892 | | |
880 | 893 | | |
| |||
0 commit comments