Repository navigation
Commit 7f368f5
feat(lint)!: os lint refuses a flow region the engine refuses to register (flow-region-shape-invalid) (#29495)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi94SgyoaFCXpCbitt9ZQSDDL)_
Fixes #29049
Clause-②: no (narrowing)
Metadata-protocol: none
## 维护者速读
- 改了什么:流程区域形状检查(ADR-0031,loop / parallel / try_catch
区域须单入口单出口、无环,parallel 至少两个分支)从 CLI 专用关卡搬进 `@objectstack/lint`
的作者期规则表,新规则 `flow-region-shape-invalid`,直接调用 spec 的
`validateControlFlow`;`os lint` 从此也拒收这类流程。
- 为什么改:此前只有 `os validate` / `os build` 拒收,`os lint` 照样放行;作者跑 lint
看到绿灯,部署后引擎注册时拒收该流程,触发器永不触发。
- 风险与回滚:`os lint` 收窄(BREAKING 横幅);validate / build
的通过与拒收结论不变,只是错误改由规则表报出、`--json` 行的形状随之变化(该形状尚未发布)。回滚即还原本 PR。
-
先看:`packages/lint/src/validate-flow-region-shape.ts`(规则本体)、`packages/lint/src/authoring-rules.ts`
新条目、`packages/cli/test/validate-flow-region-shape.test.ts`(三命令同一夹具)。
- 两分钟验证:`pnpm --filter @objectstack/cli exec vitest run --project unit
test/validate-flow-region-shape.test.ts`,预期 5/5 通过,其中三条用例对
validate、build、lint 同一夹具均退出 1 并给出同一规则 id。
- 需要你做的:无,席位落地(新规则 id 由席位在 ACCEPT 中裁定;两条待发 changeset 的句子被本 PR 改正,Check
Changeset 按设计变红,由席位复核确认)。
## What changes
- **New authoring rule** `flow-region-shape-invalid`
(`packages/lint/src/validate-flow-region-shape.ts`;
`validateFlowRegionShape` and `FLOW_REGION_SHAPE_INVALID` exported from
the root barrel). Each `flows[]` record goes through the two calls the
engine's `registerFlow` makes: `FlowSchema`, then `validateControlFlow`
from `@objectstack/spec/automation`. No copy of the region logic.
Message: `Flow 'NAME' would be refused at registration: ` plus the
engine's sentence, verbatim. Gating, `input: 'parsed'`, all three
commands.
- **Fail-closed kept (#29243).** A flow record `FlowSchema` refuses is a
finding ("could not be judged", with the schema's issues), never a skip.
A non-record entry (null, a string) is skipped like every registry
reader skips it (`non-record-object-entry.test.ts` reds otherwise); the
flows after it keep their own index.
- **Runtime publish gate: not wired** (`surfaces: CLI_ONLY`, with a
`surfaceReason`). Whether a metadata publish refuses a malformed region
is sibling card #29233 (domain:server); this PR does not decide it.
- **CLI-only gate retired.**
`packages/cli/src/utils/flow-region-shape.ts` and its unit test are
deleted; `validate.ts` step 3a-vi and `compile.ts` step 3b-iii-ter are
reduced to a pointer comment. validate and build now get the check from
the registry, so the defect is reported once (measured below).
- **`os explain flow-region-shape-invalid`**: new entry in
`rule-explanations.ts`.
- **Docs**: the four CLI transcripts now print `Running author-time
rules (61)` (recounted from the merged registry: validate 61, build 61).
- **Changesets**: new `.changeset/29049-flow-region-shape-lint-rule.md`
(`@objectstack/lint` + `@objectstack/cli` patch, BREAKING banner,
`adr-0087: not-required (no-migration-prescription)`).
### Forced test edits
- `packages/cli/test/authoring-rule-command-parity.test.ts`: the `flow
region shape (#28893)` row left `OUTSIDE_THE_TABLE` and is now a `CASES`
row (`rule: 'flow-region-shape-invalid'`, `blindTo: ['lint']`), as that
table's docblock prescribed. `OUTSIDE_THE_TABLE` held only that row, so
the table, its docblock and its `it.each` are removed (an empty
`it.each` would register no test).
- `packages/cli/test/validate-build-gate-parity.test.ts`: the
`findFlowRegionRefusals` row is removed from
`SHARED_NON_REGISTRY_GATES`; neither command calls it now, and the
ledger's own stale-row check requires that.
- `packages/cli/test/validate-flow-region-shape.test.ts`: still green,
with three edits. (1) A refusal is now a rule finding (`rule`, `where`,
`path`, `message`, `hint`), under `errors` for `os validate --json` and
under `issues` for `os build --json` (the rule table's envelope), not
the old gate's `VALIDATION_ERROR` row with `flow`. (2) The #29059
assembled-artifact case located the row at
`packages[1].manifest.flows[0]`; the rule table judges the artifact's
folded union first (as it does every collection), so the finding is at
`flows[1]` with `where: flow 'rgn_ops_lead_time'`. Still exactly one
row, from the second package. (3) Each case now also runs `os lint` in
process on the same fixture.
- `packages/cli/src/utils/flow-region-shape.test.ts` (deleted with its
subject): its three #29059 refused-parse cases moved to
`packages/lint/src/validate-flow-region-shape.test.ts`. The `packages[]`
location case has no rule-level counterpart (the rule reads the stack it
is handed); its door-level half is the assembled case above.
## Tests
One test per command on the same fixture:
`packages/cli/test/validate-flow-region-shape.test.ts` runs `os validate
--json`, `os build --json` and `os lint --json` in process on each of
`duly`, `nested`, `singleBranch` and `assembled`; all three exit 1 with
`rule: flow-region-shape-invalid` and the engine's sentence, build
writes no `dist/`, and the joined control passes validate and build with
no finding of the rule under lint. The validate leg also asserts the
refusal appears exactly once (no echo from a second gate).
Readings on head `52b05b8e` (after merging `origin/main` at `7b1ae338`,
closure rebuilt):
<details><summary>Consumer suites</summary>
- `pnpm --filter @objectstack/lint test`: Test Files 190 passed (190),
Tests 7843 passed (7843); `typecheck` clean.
- `pnpm --filter @objectstack/cli exec vitest run --project unit`: Test
Files 373 passed (373), Tests 5263 passed (5263) (includes
`validate-flow-region-shape`, `validate-build-gate-parity`,
`explain-rule-id`, the `published-subpath-*` pins after `pnpm --filter
@objectstack/cli build`); `typecheck` clean.
- `pnpm --filter @objectstack/cli exec vitest run --project integration
test/authoring-rule-command-parity.test.ts`: Tests 12 passed (12),
including `flow-region-shape-invalid gates on all three commands`.
- `pnpm --filter @objectstack/spec exec vitest run src/automation`: Test
Files 45 passed (45), Tests 1745 passed (1745).
- Rule count recounted from the merged registry's dist: validate 61,
build 61 (lint 59), matching the four transcripts.
</details>
<details><summary>Ablation (one, on the line the tests pin)</summary>
Mutation: `validateControlFlow(result.data);` in
`packages/lint/src/validate-flow-region-shape.ts` guarded behind an
always-false `globalThis.ABLATION_29049_MARKER` read, via
`scripts/ablation-replace.mjs` (anchor 1 -> 0, blob `46bdaf4b` ->
changed), lint rebuilt, `ablation-dist-preflight` found the marker in 4
dist files.
- lint `validate-flow-region-shape.test.ts`: 4 failed, 3 passed (red as
expected; the 3 green are the refused-parse and control cases, which do
not reach the call).
- cli `validate-flow-region-shape.test.ts`: 4 failed, 1 passed:
validate, build and lint all PASS the malformed flow with the rule
disabled, which also proves no second gate refuses it any more (the
retired walk is gone).
- cli `authoring-rule-command-parity.test.ts`:
`flow-region-shape-invalid gates on all three commands` failed, 11
passed.
- Restore: blob back to `46bdaf4b` == HEAD, `git diff HEAD` empty, lint
rebuilt, preflight `--absent` clean on 20 files and a clean tree; both
CLI files green again (5/5, 12/12).
A first attempt replaced the call with a bare expression; that build
failed, so the marker never reached `dist/` and its readings are
discarded (not counted).
</details>
<details><summary>Measured out of scope</summary>
A flow whose top-level graph cycles (start -> a -> b -> a, b -> end)
draws no finding; the same cycle inside a `loop` body draws `loop 'L'
body: ... region contains a cycle`. So this rule does not cover #29051.
</details>
## Acceptance notes
- `os lint` now also reports a flow record the flow schema refuses (the
fail-closed finding). `os validate` and `os build` already refuse such a
flow at their schema step, so no command accepts what it refused before
beyond the region case; `os lint` reaches it because it never parses.
- Top-level graph cycles stay out of scope (#29051): the engine's
`detectCycles` is not exported, and `validateControlFlow` checks cycles
inside regions only. Measured: this rule does not cover the top-level
cycle.
- The lane line in `lanes/server.md` reported in triage 6579387723 is
not touched.
## Verification
Gates on head `52b05b8e`: `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` derived 92 commands; all 92 run and
reconciled with `--ran` (`92 derived famil(ies) accounted for — 88 run,
4 NOT-MEASURED`).
<details><summary>Non-zero exits</summary>
- `node scripts/check-empty-changeset.mjs --base origin/main` exit 1, by
design: this PR corrects two PENDING changesets whose sentences it makes
false (DELIBERATE CORRECTION class).
`.changeset/28893-flow-region-shape-at-validate-build.md` no longer says
the doors call `findFlowRegionRefusals` and print a `VALIDATION_ERROR`
row; it now names the rule `flow-region-shape-invalid` and that `os
lint` reports it too. `.changeset/29059-flow-region-gate-hardening.md`
now says the unjudgeable flow is refused with a
`flow-region-shape-invalid` finding instead of a `VALIDATION_ERROR` row.
Restoring them would republish false sentences; this needs confirming on
the PR. `Check Changeset` is not a required context.
- NOT MEASURED (exit 3, prerequisite): `check:dual-build-cjs-loads` and
`check:skill-examples` (packages outside this closure have no `dist/`,
e.g. `client-react`), `check:i18n-coverage` (the showcase example needs
an unbuilt connector `dist/`), `check-plugin-teardown-shape --self-test`
(shallow clone cannot reach its pinned fixture commit). None reads a
file this PR changes for its verdict; CI runs them on a full build.
- Offline: `check-changeset-no-major --event` exit 0 (LEVEL AXIS: `no
(narrowing)` + `none`, patch); `check-adr-0087-registration` exit 0
(`not-required (no-migration-prescription)`).
</details>
Dogfood (required by the `(narrowing)` arm) on head `52b05b8e`, `pnpm
--filter @objectstack/dogfood test` under the lock after building
`@objectstack/dogfood^...`: `VERDICT command-exit 1` · Test Files 2
failed | 393 passed | 3 skipped (398) · Tests 4 failed | 3255 passed |
28 skipped (3287).
<details><summary>The 4 failures are not this PR's (control
measured)</summary>
- `test/flow-runas.dogfood.test.ts`: `runAs:'system' ELEVATES` (trigger
answers 404 `RECORD_NOT_FOUND` on `runas_note`), `runAs:'user'
DE-ELEVATES`, `runAs:'system' READS`.
- `test/declarative-endpoint-anonymous-guest.dogfood.test.ts`: `carries
the guest position …` (`flowContexts` length 0, expected 1).
- Re-run alone on this head: same 4 failed, 8 passed. Control:
`packages/lint/src`, `packages/cli/src` and `packages/cli/test` restored
to `origin/main` `7b1ae338` (this PR's diff removed from both packages),
lint and cli rebuilt, the same two files: the same 4 failed, 8 passed.
So they fail on `main`'s code without this diff; reported to the seat as
a main-red candidate. Tree restored to HEAD and rebuilt (clean `git
status`).
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmQjqkXDjc5jhjFfHt3)_
<!-- signed: domain:spec#4 · os-steve · session_01ADHkmQNap7ryQcyQXocWct
-->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 728c73e commit 7f368f5
19 files changed
Lines changed: 371 additions & 270 deletions
File tree
- .changeset
- content/docs
- deployment
- getting-started
- ui
- packages
- cli
- src
- commands
- utils
- test
- lint/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | | - | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
| 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 | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
11 | 11 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
771 | 771 | | |
772 | 772 | | |
773 | 773 | | |
774 | | - | |
| 774 | + | |
775 | 775 | | |
776 | 776 | | |
777 | 777 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
760 | 760 | | |
761 | 761 | | |
762 | 762 | | |
763 | | - | |
| 763 | + | |
764 | 764 | | |
765 | 765 | | |
766 | 766 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
310 | 310 | | |
311 | 311 | | |
312 | 312 | | |
313 | | - | |
| 313 | + | |
314 | 314 | | |
315 | 315 | | |
316 | 316 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
404 | 404 | | |
405 | 405 | | |
406 | 406 | | |
407 | | - | |
| 407 | + | |
408 | 408 | | |
409 | 409 | | |
410 | 410 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | 86 | | |
90 | 87 | | |
91 | 88 | | |
| |||
840 | 837 | | |
841 | 838 | | |
842 | 839 | | |
843 | | - | |
844 | | - | |
845 | | - | |
846 | | - | |
847 | | - | |
848 | | - | |
849 | | - | |
850 | | - | |
851 | | - | |
852 | | - | |
853 | | - | |
854 | | - | |
855 | | - | |
856 | | - | |
857 | | - | |
858 | | - | |
859 | | - | |
860 | | - | |
861 | | - | |
862 | | - | |
863 | | - | |
864 | | - | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
865 | 843 | | |
866 | 844 | | |
867 | 845 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
84 | | - | |
85 | | - | |
86 | | - | |
87 | 84 | | |
88 | 85 | | |
89 | 86 | | |
| |||
754 | 751 | | |
755 | 752 | | |
756 | 753 | | |
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 | | - | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
| 757 | + | |
786 | 758 | | |
787 | 759 | | |
788 | 760 | | |
| |||
This file was deleted.
0 commit comments