Repository navigation
Commit 0814b89
fix(lint)!: refuse a flow whose loop, parallel or try_catch region is malformed at publish (#29526)
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9SEsy6LaBvU7eiRnCrSgR6K)_
Fixes #29233
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — no published skill teaches that a malformed region
publishes; the automation skill's region rules (one entry, one exit,
acyclic) stay true, and the publish door now refuses a region that
breaks them earlier, in the engine's words.
## 维护者速读
- 改了什么:#29495 新增的 lint 规则
`validateFlowRegionShape`(`flow-region-shape-invalid`)现在也在运行时发布门上跑:循环体、并行分支或
try/catch 区域写坏了的流程,草稿读取显示 `valid: false`,所有发布入口都拒绝发布。
- 为什么改:以前这种流程草稿显示有效、发布成功,可引擎注册时拒收,流程永远不会运行,作者看不到任何错误。
- 风险与回滚:只改了注册表里一行(`surfaces: CLI_AND_RUNTIME`、`runtimeTypes:
['flow']`),判官就是引擎注册时调用的 `FlowSchema` +
`validateControlFlow`,没有第二份判断;回滚即撤销本 PR。
- 先看:`packages/lint/src/authoring-rules.ts` 里 `validateFlowRegionShape`
那一行,再看
`packages/metadata-protocol/src/runtime-authoring-gate.flow-region.test.ts`。
- 两分钟验证:`pnpm --filter @objectstack/metadata-protocol exec vitest run
src/runtime-authoring-gate.flow-region.test.ts`,预期 8 个用例全绿(需先 `pnpm
--filter @objectstack/lint build`)。
- 需要你做的:无,席位落地。
## What changes
- `@objectstack/lint`: the registry row for `validateFlowRegionShape`
(#29495, rule id `flow-region-shape-invalid`) moves from `CLI_ONLY` to
`surfaces: CLI_AND_RUNTIME`, `runtimeTypes: ['flow']`, and its
`surfaceReason` is retired. The runtime authoring gate
(`evaluateRuntimeAuthoringGate`), which the metadata draft read and
every publish door run, picks it up through `runRuntimeAuthoringRules`
and discloses it in `rulesRun`. No gate-local rule and no second call:
`runtime-authoring-gate.ts` is untouched.
- The judge is the one the engine's registration uses
(`canonicalizeFlow`): `FlowSchema`, then `validateControlFlow` from
`@objectstack/spec/automation`. The message is "Flow 'NAME' would be
refused at registration: " followed by the engine's sentence, word for
word.
- Changeset `.changeset/29233-flow-region-shape-publish.md`:
`@objectstack/lint` `minor`, BREAKING banner with before/now, `adr-0087:
not-required (no-migration-prescription)`.
### Accepted differences from a gate-local rule (seat ruling)
1. The finding is anchored on the flow (`flows[i]`, where `flow
'NAME'`), so the draft read's path is the item root `''`, not the
container (`nodes.1`). The sentence names the region. Finer location is
#29558.
2. The message carries the registry rule's prefix "Flow 'NAME' would be
refused at registration: ".
3. One finding per flow (the engine's first refusal), as the engine
itself throws once.
4. The publish doors judge the raw stored draft body; the engine
converts first (`applyConversionsToFlow`). Landed as declared under seat
ruling A; judging the converted body is #29557. Measurement below.
## Tests
-
`packages/metadata-protocol/src/runtime-authoring-gate.flow-region.test.ts`
(new, pure gate, 8 cases): the card's loop body a → b → a (pins the
engine's sentence once; where `flow 'rgnprobe_loop'`, path `flows[0]`,
`rulesRun` contains `validateFlowRegionShape`), a one-branch parallel, a
try region that closes on itself, a region its schema refuses ("invalid
region"), a malformed region nested in another (sentence names both
levels), two malformed containers giving one finding; controls:
well-formed loop / parallel / try_catch (nested too) raise nothing, a
draft is not gated. Every expected message is `validateControlFlow`'s
own answer for the case.
-
`packages/metadata-protocol/src/protocol.draft-publish-verdict.test.ts`
(real protocol doors): a CASES row for the card's loop (draft saves,
draft read `valid:false` with the publish's own rule and message,
per-item publish 422, package publish rolls back); a control that a
well-formed loop, parallel and try_catch each read `valid:true` and
publish (`publishedCount: 3`); a pin that the draft read carries the
registry rule's message at the item root (`''`).
-
`packages/qa/dogfood/test/flow-region-malformed-publish.dogfood.test.ts`
(showcase boot, REST): the card's table (draft PUT 200, draft GET
`valid:false` with `code flow-region-shape-invalid`, `path ""` and the
engine's sentence, publish-drafts `outcome: refused`, no active row);
the per-item `POST /meta/flow/:name/publish` and a live `PUT` each 422
with no active row; control: well-formed loop, parallel and try_catch
each read valid, publish, and reach `registration.state: registered`.
- **Forced test edits** (claim amendment 6585054494):
`packages/lint/src/runtime-gate.test.ts` (`brokenApprovalFlow` /
`cleanApprovalFlow`) and
`packages/lint/src/validate-flow-trigger-readiness.test.ts` (`apiFlow`,
`startSecond`). Their runtime-gate fixtures were flows the flow schema
refuses (no flow label, unlabelled nodes, a top-level `trigger` key, no
edges), which the engine never registers and which this rule now reports
on the gate. The fixtures are made schema-valid; no assertion changed.
## Registration / publish measurement (difference 4)
- A stored body the flow schema refuses both raw and after conversion
(no flow label, a node without a label, a top-level `trigger` key, an
unknown flow type, `edges` missing) is refused by the engine's
`registerFlow` too: the rule refuses nothing the engine would register.
- A legacy D2 spelling (`get_record` `config.filters`) fails
`FlowSchema` raw, passes once converted, and `registerFlow` REGISTERS
it. Seeded as a stored legacy draft, it is refused at `publishMetaItem`
and `publishPackageDrafts` with `[expression-invalid,
flow-region-shape-invalid]`: `expression-invalid` already refuses it on
main, so this PR adds no new refusal on that spelling.
- NOT MEASURED: whether some other legacy spelling is refused by this
rule alone. A new draft cannot carry a retired spelling (the draft save
refuses it), so only rows stored before those retirements can reach
this.
## Acceptance notes
- `os validate`, `os build` and `os lint` already ran this rule
(#29495); the CLI is unchanged.
- The engine throws on the first malformed region of the whole flow, and
so does this rule: one finding per flow.
## Verification
Head `2cb755ef` (origin/main merged at f99a058, which carries #29495's
merge 7f368f5).
<details><summary>Red first (tests on the main code)</summary>
On the earlier round the same door pins went red on main with the gate
answering nothing (8 failed | 38 passed). On this head the ablation
below puts main's registry row back (`surfaces: CLI_ONLY`) and the same
8 metadata-protocol cases fail, which is the red-first reading for the
registry route.
</details>
<details><summary>Ablation (registry row back to CLI_ONLY, through lint
dist)</summary>
`scripts/ablation-replace.mjs` on `packages/lint/src/authoring-rules.ts`
(row back to `surfaces: CLI_ONLY` with a marked `surfaceReason`), `pnpm
--filter @objectstack/lint build`, `ablation-dist-preflight.mjs
@objectstack/lint ABLATION_29233`: marker present in 4 built files.
- `runtime-authoring-gate.flow-region.test.ts` +
`protocol.draft-publish-verdict.test.ts`: `Tests 8 failed | 37 passed
(45)`; the 8 are the six refusal cases of the pure-gate file and the
CASES row plus the location pin; the controls and the draft case pass.
- Dogfood file: `Tests 2 failed | 1 passed (3)`; the card case and the
per-item/live case fail, the control passes.
- Restored: blob == HEAD (488c91a), lint rebuilt, preflight `--absent`
clean on all 20 built files, `git status --porcelain` empty.
</details>
<details><summary>Suites, gates, dogfood</summary>
All on head 2cb755e after `pnpm install --frozen-lockfile` and a full
packages build (78/78 tasks).
- `pnpm --filter @objectstack/lint typecheck && test`: Test Files 192
passed (192), Tests 7885 passed (7885).
- `pnpm --filter @objectstack/metadata-protocol typecheck && test`: Test
Files 308 passed | 4 skipped (312), Tests 22240 passed | 26 skipped
(22266).
- Lint: `pnpm exec eslint --no-inline-config` (the root `pnpm lint`
command) on the six touched TS files, exit 0.
- Derived gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` listed 63 families; all 63 run, exit 0.
`--ran` reconciliation: "63 derived famil(ies) accounted for — 63 run, 0
NOT-MEASURED (a DERIVED zero)".
- Full dogfood battery (`os-verify-lock.sh -c "pnpm --filter
@objectstack/dogfood test"`): VERDICT command-exit 0; Test Files 398
passed | 3 skipped (401), Tests 3280 passed | 28 skipped (3308).
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmSfgX3fNF6rAXudbtz)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01ADHkmSfgX3fNF6rAXudbtz
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d3175d5 commit 0814b89
7 files changed
Lines changed: 479 additions & 12 deletions
File tree
- .changeset
- packages
- lint/src
- metadata-protocol/src
- qa/dogfood/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 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1981 | 1981 | | |
1982 | 1982 | | |
1983 | 1983 | | |
1984 | | - | |
1985 | | - | |
1986 | | - | |
1987 | | - | |
1988 | | - | |
| 1984 | + | |
| 1985 | + | |
| 1986 | + | |
| 1987 | + | |
| 1988 | + | |
| 1989 | + | |
1989 | 1990 | | |
1990 | 1991 | | |
1991 | 1992 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
24 | 28 | | |
25 | 29 | | |
26 | 30 | | |
27 | | - | |
| 31 | + | |
28 | 32 | | |
29 | | - | |
| 33 | + | |
30 | 34 | | |
31 | 35 | | |
32 | 36 | | |
| 37 | + | |
33 | 38 | | |
34 | 39 | | |
35 | 40 | | |
| 41 | + | |
36 | 42 | | |
37 | 43 | | |
38 | 44 | | |
39 | 45 | | |
40 | 46 | | |
41 | 47 | | |
42 | | - | |
| 48 | + | |
43 | 49 | | |
44 | 50 | | |
45 | 51 | | |
| 52 | + | |
46 | 53 | | |
47 | 54 | | |
48 | 55 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1099 | 1099 | | |
1100 | 1100 | | |
1101 | 1101 | | |
| 1102 | + | |
| 1103 | + | |
| 1104 | + | |
1102 | 1105 | | |
1103 | 1106 | | |
| 1107 | + | |
1104 | 1108 | | |
1105 | 1109 | | |
1106 | 1110 | | |
1107 | | - | |
1108 | | - | |
| 1111 | + | |
| 1112 | + | |
1109 | 1113 | | |
1110 | 1114 | | |
1111 | 1115 | | |
| |||
1298 | 1302 | | |
1299 | 1303 | | |
1300 | 1304 | | |
1301 | | - | |
1302 | | - | |
| 1305 | + | |
| 1306 | + | |
1303 | 1307 | | |
1304 | 1308 | | |
1305 | 1309 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
460 | 460 | | |
461 | 461 | | |
462 | 462 | | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
463 | 489 | | |
464 | 490 | | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
465 | 514 | | |
466 | 515 | | |
467 | 516 | | |
| |||
541 | 590 | | |
542 | 591 | | |
543 | 592 | | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
544 | 645 | | |
545 | 646 | | |
546 | 647 | | |
| |||
0 commit comments