Repository navigation
Commit 8e0f3db
fix(cli): the flow-region gate never passes a flow it could not judge, and the boot banner counts refused flows (#29243)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9CRVFsFbBGh67CEn4YUnUGT)_
Fixes #29059
Clause-②: no
Metadata-protocol: none
domain:server#3 · os-steve · session_01ADHkmQBbn1k9FtQeh2nJSs
## 维护者速读
- 改了什么:`os validate` / `os build` 的 flow 区域门禁在 FlowSchema
解析失败时不再静默放行,改为报一条带路径的 VALIDATION_ERROR;`os serve` / `os dev` 启动横幅新增一行「N
flow(s) declared, M refused at registration」。
- 为什么改:卡 #29059 是 #29043 合同复审的四条应修项加 #28893 的启动半边:门禁的静默跳过、空转的盲区断言、未测的
packages[] 主体、过期计数,以及启动时被拒的 flow 只在 Boot diagnostics 里逐条出现。
- 风险与回滚:今天被拒或被收的 flow 都不变(解析失败分支对作者不可达,见下);横幅只在有拒载时多一行,启动是否继续不变。回滚即
revert 本 PR。
-
先看:`packages/cli/src/utils/flow-region-shape.ts`(解析失败分支)、`packages/cli/src/commands/serve.ts`
的 `collectAutomationSummary`(拒载计数读 engine 自己的
`getFlowRegistration`)、`packages/cli/src/utils/format.ts` 的
`printAutomationSummary`(行序与全拒时的早返回)。
- 两分钟验证:`pnpm --filter @objectstack/cli exec vitest run
src/commands/serve-automation-refused.test.ts
src/utils/flow-region-shape.test.ts`,预期 7 个测试全绿。
- 需要你做的:无,席位落地。
## What changes
All five Wanted items of #29059.
1. **No silent pass on a parse failure** (`utils/flow-region-shape.ts`).
`findFlowRegionRefusals` used to `return` with no row when
`FlowSchema.safeParse` refused a flow. It now pushes a
`VALIDATION_ERROR` row at the flow's `path`, named by the flow's `name`
(or its path when it has no string name), carrying the schema's issues.
The door refuses on it like any other row. New unit test
`src/utils/flow-region-shape.test.ts` forces the branch (own flows,
nameless body, `packages[]` entry).
2. **The blindness pin is no longer vacuous**
(`test/authoring-rule-command-parity.test.ts`). Each `OUTSIDE_THE_TABLE`
entry now names its `gate`, and the case first asserts that
`gate(stack)` is non-empty and carries the marker, then that the
registry run as `os lint` runs it reports nothing with the marker. The
docblock now says it measures the registry, not the `os lint` command.
3. **`packages[]` subject tested end to end**
(`test/validate-flow-region-shape.test.ts`). A two-entry assembled
artifact with the malformed flow in the second entry: `os validate
--json` and `os build --json` both exit 1 with exactly one region row `{
path: 'packages[1].manifest.flows[0]', code: 'VALIDATION_ERROR', flow:
'rgn_ops_lead_time' }`, and build writes no `dist/`.
4. **Stale prose**: the parity file's test-budget header says "the ten
rule cases" (nine `CASES` plus one `OUTSIDE_THE_TABLE`).
5. **Boot banner count** (`commands/serve.ts`, `utils/format.ts`).
`collectAutomationSummary` takes the declared flows' names and counts
those for which the engine's `getFlowRegistration(name).state` is
`refused`. That read already exists on the engine and on
`IAutomationService` (optional, the metadata door uses it), and the
record behind it is written by the plugin's `kernel:ready` cold-boot
bind (`recordFlowRefusal`). So no new engine or plugin surface. The
banner prints `⚠ Flows: N flow(s) declared, M refused at registration —
…` after the Flows count line, and alone when every declared flow was
refused (the old early return on `flowCount === 0` would have printed
nothing). No line when nothing is refused. Whether boot continues is
unchanged.
`AutomationReadySummary` gains `refusedCount`. It is not on any
published entry of `@objectstack/cli` (`exports` is `.`, `./console`,
`./hook-body`; `index.ts` re-exports only the `ServeCommand` default),
and the banner is stderr text, so no JSON or published member changes.
**Forced test edits:** `src/utils/format.boot-warning-classes.test.ts`
and `src/utils/format.flow-contender-agreement.test.ts` build
`AutomationReadySummary` literals by hand; each gains `refusedCount: 0`
because the member is required.
## Tests
Red first, by ablation (`scripts/ablation-replace.mjs`, each leg
restored and proven by blob hash equal to `HEAD` and an empty `git diff
HEAD`):
<details><summary>(1) parse failure: put the silent return
back</summary>
On `03cc2f40`, `flow-region-shape.ts` anchor `if (!result.success) {`
replaced by `if (result.success === false) { return; } if (false) {`:
`src/utils/flow-region-shape.test.ts` 3 failed / 3, each `expected [] to
deeply equal [ { path: 'flows[0]', … } ]`. (A first attempt whose
replacement contained the anchor was refused by the tool as a no-op and
restored; it is not counted.)
</details>
<details><summary>(2) vacuous pin: a fixture typo</summary>
Fixture edge `dy` rewired `d → y` to `x → y` (the catch region becomes
single-exit): with this PR's assertion, `known gap` case 1 failed, `[]:
expected [] to not deeply equal []`. Control leg, same typo with the new
assertion deleted (the pin as on `main`): 1 passed. The old pin stayed
green on a fixture nothing refuses.
</details>
<details><summary>(3) packages[] subject</summary>
`const subjects = entries.length > 0` → `false && …` (walk the empty top
level only): the assembled case failed, `os validate` exit `+0`,
expected 1. Path spelling `packages[N].manifest.` → `packages[N].`:
failed with `path: "packages[1].flows[0]"` against the expected
`packages[1].manifest.flows[0]`.
</details>
<details><summary>(5) boot banner count</summary>
`serve.ts` `refusedCount++` → `void 0`:
`serve-automation-refused.test.ts` 2 failed / 4 (`refusedCount` 0). On
`48c3a64c`, the all-refused branch's `printRefused();` → `void
printRefused;`: 1 failed / 4, `expected [] to have a length of 1`. (On
`03cc2f40`, the earlier line placement, gating the line on `flowCount >
0` failed the same case.)
The test drives the REAL `AutomationEngine` (dist): `registerFlow`
refuses a one-branch `parallel`, the test records it the way the host's
sync does, and the collector and the real `printServerReady` read it.
Real boot, one-off measurement (not kept as a test): `os serve` on a
two-flow config with one one-branch `parallel`,
`OS_DATABASE_URL=:memory:`, printed
```text
⚠ Flows: 2 flow(s) declared, 1 refused at registration — refused flows never run; each one's reason is under Boot diagnostics ('failed to register flow')
Flows: 1 flow(s) 1 bound to triggers (record_change, schedule, time_relative, api)
```
with both `[Automation] failed to register flow` and `[Automation]
cold-boot flow bind: failed to register flow` for `bt_fan` under Boot
diagnostics, and boot continued to `Press Ctrl+C to stop`. (That run had
the line before the count line; `48c3a64c` moved it after.)
</details>
## Verification
All on the final head `fc24941128` (this branch merged with
`origin/main` `dedc350519`; clean merge), after rebuilding
`@objectstack/cli` and its dependency closure:
- `pnpm --filter @objectstack/cli typecheck` (tsc plus
`check:test-typecheck`): exit 0.
- `pnpm --filter @objectstack/cli exec vitest run --project unit`: 374
files, 5252 tests passed.
- `test/authoring-rule-command-parity.test.ts` (integration tier, edited
here): 12 / 12 passed.
- `node scripts/pm/dispatch-gates.mjs --commands` derived 63 commands
for this change set; all 63 exit 0; `--ran` reconciliation: 63 run, 0
NOT-MEASURED, 0 UNRUN.
- `pnpm lint`: exit 0.
On the pre-merge head `48c3a64c` the same set was green too. Five gates
first exited 3 (prerequisite not met: no full build, shallow clone) and
were rerun green after `turbo run build` and an unshallow fetch:
`check-plugin-teardown-shape --self-test`, `check:dual-build-cjs-loads`,
`check:i18n`, `check:i18n-coverage`, `check:i18n-walk-parity`.
Dogfood battery: not owed (no `!` in the title, `Clause-②: no` with no
narrowing arm).
<details><summary>The 63 derived commands, each with its exit
code</summary>
```text
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:cli-test-child-env :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:i18n :: exit 0
pnpm check:i18n-coverage :: exit 0
pnpm check:i18n-walk-parity :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:route-envelope :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
```
</details>
## Acceptance notes
- **Clause-② stays `no`.** The parse-failure branch cannot be reached by
an authored stack: every flow the walk reads was parsed by the stack
schema's `flows: z.array(FlowSchema)` (and, under `packages[]`, by
`AssembledPackageBodySchema`, whose collections come from the same stack
shape), and `FlowSchema` accepts its own output. All existing
validate/build/parity cases and the CONTROL case stay green, so nothing
that passes today is refused.
- **What the count does not see.** It counts DECLARED flow names (the
config, packages included). A runtime-authored `sys_metadata` flow the
engine refuses is not declared, so it is not counted; its per-flow line
under Boot diagnostics is unchanged. And the record is written by the
`kernel:ready` bind; the boot pull's own refusals are deliberately not
recorded (AGENTS.md "Startup registry reads"). A host with no `protocol`
service runs no such bind, so its count reads 0 there while the warn
still prints. Every `os serve` boot measured here has the bind.
- Integration tier: `test/authoring-rule-command-parity.test.ts` is
integration (it spawns the CLI) and was run locally in full because it
is edited; the rest of the integration tier is declared to CI.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmQBbn1k9FtQeh2nJSs)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 78f78ac commit 8e0f3db
10 files changed
Lines changed: 333 additions & 14 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 | + | |
| 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 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5704 | 5704 | | |
5705 | 5705 | | |
5706 | 5706 | | |
| 5707 | + | |
5707 | 5708 | | |
5708 | 5709 | | |
5709 | | - | |
| 5710 | + | |
| 5711 | + | |
| 5712 | + | |
| 5713 | + | |
5710 | 5714 | | |
5711 | 5715 | | |
5712 | 5716 | | |
| |||
7240 | 7244 | | |
7241 | 7245 | | |
7242 | 7246 | | |
| 7247 | + | |
| 7248 | + | |
| 7249 | + | |
| 7250 | + | |
| 7251 | + | |
7243 | 7252 | | |
7244 | 7253 | | |
7245 | 7254 | | |
| |||
7249 | 7258 | | |
7250 | 7259 | | |
7251 | 7260 | | |
| 7261 | + | |
7252 | 7262 | | |
7253 | 7263 | | |
7254 | 7264 | | |
| |||
7291 | 7301 | | |
7292 | 7302 | | |
7293 | 7303 | | |
| 7304 | + | |
| 7305 | + | |
| 7306 | + | |
| 7307 | + | |
| 7308 | + | |
| 7309 | + | |
| 7310 | + | |
| 7311 | + | |
| 7312 | + | |
| 7313 | + | |
| 7314 | + | |
| 7315 | + | |
| 7316 | + | |
| 7317 | + | |
| 7318 | + | |
7294 | 7319 | | |
7295 | 7320 | | |
7296 | 7321 | | |
| |||
7327 | 7352 | | |
7328 | 7353 | | |
7329 | 7354 | | |
| 7355 | + | |
7330 | 7356 | | |
7331 | 7357 | | |
7332 | 7358 | | |
| |||
| 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 | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
23 | | - | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
24 | 31 | | |
25 | 32 | | |
26 | 33 | | |
| |||
37 | 44 | | |
38 | 45 | | |
39 | 46 | | |
40 | | - | |
| 47 | + | |
41 | 48 | | |
42 | | - | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
43 | 53 | | |
44 | 54 | | |
45 | 55 | | |
| |||
54 | 64 | | |
55 | 65 | | |
56 | 66 | | |
57 | | - | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
58 | 81 | | |
59 | 82 | | |
60 | 83 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
238 | 238 | | |
239 | 239 | | |
240 | 240 | | |
| 241 | + | |
241 | 242 | | |
242 | 243 | | |
243 | 244 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
77 | 77 | | |
78 | 78 | | |
79 | 79 | | |
| 80 | + | |
80 | 81 | | |
81 | 82 | | |
82 | 83 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1011 | 1011 | | |
1012 | 1012 | | |
1013 | 1013 | | |
| 1014 | + | |
| 1015 | + | |
| 1016 | + | |
| 1017 | + | |
| 1018 | + | |
| 1019 | + | |
| 1020 | + | |
| 1021 | + | |
| 1022 | + | |
1014 | 1023 | | |
1015 | 1024 | | |
1016 | 1025 | | |
| |||
1713 | 1722 | | |
1714 | 1723 | | |
1715 | 1724 | | |
1716 | | - | |
| 1725 | + | |
| 1726 | + | |
| 1727 | + | |
| 1728 | + | |
| 1729 | + | |
| 1730 | + | |
| 1731 | + | |
| 1732 | + | |
| 1733 | + | |
| 1734 | + | |
| 1735 | + | |
| 1736 | + | |
| 1737 | + | |
| 1738 | + | |
| 1739 | + | |
| 1740 | + | |
| 1741 | + | |
| 1742 | + | |
1717 | 1743 | | |
1718 | 1744 | | |
1719 | 1745 | | |
1720 | 1746 | | |
1721 | 1747 | | |
| 1748 | + | |
1722 | 1749 | | |
1723 | 1750 | | |
1724 | 1751 | | |
| |||
0 commit comments