Repository navigation
Commit 2d91c9a
Fixes #20332
Clause-②: no (narrowing)
**BREAKING** (`@objectstack/spec` `minor`): a published accept set
narrows. No export, no error code and no accepted shape is added. The
refusal reuses `STACK_TRIGGER_CAPABILITY_REQUIRED` (`status: 422`).
## What changes
`validateTriggerCapability` (`packages/spec/src/stack.zod.ts`) refused
an auto-launched flow only when `requires` lacked `'triggers'`. Its
docblock said the trigger "is installed by ONE token, `requires:
['triggers']`". That is false. Every trigger plugin installs its trigger
into the automation service at `kernel:ready`, and without that service
it warns and installs nothing. No runtime's resolver turns `triggers`
into `automation`. So a stack with `requires: ['triggers']` and a
`record_change` flow passed `os validate`, booted, and never fired the
flow.
Triage direction `5861188312`, verbatim: 「**The contract choice, decided
here: refuse, do not imply.**」
The refusal now has three arms, one line per offending flow, on the same
code, header and `issues` shape. Each prescription is the whole fix for
the `requires` it was given:
| `requires` (auto-launched flow present) | Before | After |
|:---|:---|:---|
| `['automation', 'triggers']` | accepted | accepted (any order) |
| `['automation']` | refused: add `'triggers'` | **unchanged, byte for
byte** |
| `['triggers']` | **accepted** (the defect) | refused: add
`'automation'` |
| `[]` or absent | refused: add `['triggers']` | refused: add
`['automation', 'triggers']` |
| any, with no auto-launched flow, or only `obsolete` / `invalid` flows
| accepted | accepted |
### The refusal text, quoted (for review)
New arm, `requires: ['triggers']`:
```text
flow 'task_fanout' declares a 'record_change' trigger but `requires` does not include 'automation' — 'triggers' installs the 'record_change' trigger into the automation service, and without it no 'record_change' trigger would be registered, so the flow would never auto-launch. Add 'automation' to requires: ['automation', 'triggers'] (@objectstack/service-automation runs the flow; @objectstack/trigger-* only fires it).
```
Changed arm, neither token:
```text
flow 'task_fanout' declares a 'record_change' trigger but `requires` does not include 'automation' or 'triggers' — no 'record_change' trigger would be registered, so the flow would never auto-launch. Add requires: ['automation', 'triggers'] (record_change/schedule/time_relative/api ship in @objectstack/trigger-* and install into @objectstack/service-automation — 'triggers' alone installs nothing).
```
Unchanged arm, `requires: ['automation']`:
```text
flow 'task_fanout' declares a 'record_change' trigger but `requires` does not include 'triggers' — no 'record_change' trigger would be registered, so the flow would never auto-launch. Add requires: ['triggers'] (record_change/schedule/time_relative/api ship in @objectstack/trigger-*).
```
### A choice made here: the neither-token message names both tokens
The dispatch left this open and suggested keeping today's message.
Analysed on the four axes:
- **Real business need (measured).** Every real producer that declares
an auto-launched flow declares both tokens: `examples/app-showcase`,
`examples/app-todo`, the `os init` template, and `os g flow`'s own
output. The CLI boot banner already prescribes `requires: ['automation',
'triggers']`. No producer uses `['triggers']` alone.
- **Long-term soundness.** One prescription that satisfies the contract.
The message states the real install fact, the pair.
- **Keeping AI authors from writing it wrong.** Keeping the old message
would make an author who follows it literally land on `['triggers']`,
and the new arm would refuse them a second time. A prescription that
leads to another refusal is a trap. The pin `each prescription, applied
literally once, is ACCEPTED` holds the property.
- **No scope growth.** No new code path, code or export. Only the text
of one arm changes. No pin anywhere parsed the old text for this case.
## Measurements (dispatch Zone 2)
**§1 Reproduce on both runtimes.**
- **Framework CLI**, at this branch with the pre-fix early exit (`if
(hasTriggers) return errors;`) built into `packages/spec/dist`. The
mutation was held by `scripts/ablation-replace.mjs` and confirmed in
`dist/` by `scripts/ablation-dist-preflight.mjs`. On a probe project
with `requires: ['triggers']` and one `record_change` flow:
- `os validate` exited **0**.
- `os serve --dev` reached `Server is ready` and printed `⚠ Flows: 1
flow(s) declared but the automation engine is not enabled — they will
never run. Add requires: ['automation', 'triggers'] to
objectstack.config.ts`.
- It also printed four warns: `RecordChangeTriggerPlugin` /
`ScheduleTriggerPlugin` / `TimeRelativeTriggerPlugin` /
`ApiTriggerPlugin: automation service not available — … trigger NOT
installed`.
- After the restore leg (source equal to HEAD, spec rebuilt, marker
absent from all 222 dist files), `os validate` on the same probe exits
**1** with the new message.
- **cloud** `origin/main` `96eb092`, read only:
- `packages/objectos-runtime/src/capability-loader.ts`
`resolveCapabilityDependencies` pulls `queue`, `job` and `messaging` for
`triggers`, never `automation`. So the resolver does not imply it.
- The hosted hosts force-mount both tokens on every tenant environment,
whatever the artifact declares: `apps/objectos/hosted-slate.ts`
`HOSTED_FORCED_REQUIRES`, and `apps/objectos-ee` `defaultRequires`.
There a `['triggers']` stack runs, but so does a stack with no
`requires` at all. The published arm has refused that second stack since
it landed. The refusal judges the stack's own declaration, which is
portable, and not one host's floor.
- The fork condition ("`triggers` pulls `automation` in by itself")
holds on neither resolver.
**§2 Which kinds need `automation`.** All four. The boot above printed
all four "NOT installed" warns. Each plugin's `kernel:ready` hook
resolves `automation` and returns if it is missing:
`packages/triggers/trigger-record-change/src/plugin.ts:47`,
`trigger-schedule/src/plugin.ts:72`,
`trigger-schedule/src/time-relative-plugin.ts:67`,
`trigger-api/src/plugin.ts:62`. No kind stays accepted.
**§3 Shape and door.**
- One arm beside the existing one, same class and code.
- `os validate` reaches it through `loadConfig`, which evaluates the
author's `defineStack()` call (step 1), and not through its own stack
parse (step 2). It is not a separate door.
- Measured through the real CLI (`tsx bin/run-dev.js validate`) at this
branch: `['triggers']` exits 1, `[]` exits 1, `['automation',
'triggers']` exits 0.
**§4 Producer census** (expected 0 producers; 0 found; three test
stacks):
- `examples/**`:
- `app-showcase` and `app-todo` declare both. `os validate` exits 0 on
each at this branch.
- `app-crm` declares `['ui', 'automation']` and has no auto-launched
flow; `os validate` exits 0.
- `app-multi-package` and `embed-objectql` declare no `requires`.
- `packages/create-objectstack/src/templates/**`: `blank` declares
`['automation']`. It is out of this arm's reach.
- `os init` / `os g` on current `main` (PR #20329 landed as
`c5dcb3ba07`): `init` declares both. `os g flow` into a `['triggers']`
project was the "cannot run" answer and is now a refusal (see below).
- `packages/**` test stacks with `triggers` alone and an auto-launched
flow:
- `packages/cli/test/generate-object-namespace-prefix.test.ts`:
converted to the pair.
- `packages/qa/dogfood/test/fixtures/override-composite-fixture.ts`:
converted. Its boot mounts both explicitly; `verify`'s harness does not
read `requires`.
- `packages/lint/src/authoring-rule-input-tier.test.ts:93`: **not
converted.** `packages/lint` is fenced read-only for this dispatch. See
"Owed, and fenced".
- cloud `main`: six `requires` literals with `triggers` and no
`automation`, all loader and publish-route token-list tests. No
`defineStack`, no flows, so none is affected.
- hotcrm: NOT MEASURED. It is not a repository in this container.
**§5 The one-token sentence**, restated at every site that repeated it:
- the `validateTriggerCapability` and
`StackTriggerCapabilityRequiredError` docblocks;
- `automation/flow-trigger-kind.ts`;
- the error-code ledger comment;
- the two spec test comments;
- `content/docs/automation/flows.mdx` and
`content/docs/permissions/capabilities.mdx`.
`@objectstack/lint` carries no trigger-capability rule of its own; it
shares only `resolveFlowTriggerKind`. There is no asymmetry to report.
## Pins (table-driven: requires × kind × status)
`packages/spec/src/stack-requires.test.ts`, new block. Each refusal is
asserted as its envelope (`code`, `status: 422`, one `issues` entry per
flow) plus the prescription text:
- `['triggers']` refused for `record_change`, `schedule`,
`time_relative` and `api`;
- `['automation', 'triggers']` accepted for all four, and in any order
(the control);
- `[]` and absent: the pair named, and `Add requires: ['triggers']`
asserted absent;
- `['automation']`: today's `issues` entry asserted with `toEqual`;
- no auto-launched flow (none, a `screen` flow, a hand-launched
`autolaunched` flow) unaffected;
- `obsolete` / `invalid` unaffected, while `draft` / `active` are
refused;
- four flows give four issues;
- each prescription, applied once, is accepted.
**Ablation**, committed first and restored by blob hash:
- **Leg A:** the old early exit. `7 failed | 23 passed`.
- **Leg B:** the neither case given the old message. `3 failed | 27
passed`.
- **Restored:** `30 passed`, blob `4c0bfced0f02` equal to HEAD.
- Direction as predicted (turns red).
**`os g` pin flipped**
(`packages/cli/test/generate-stack-reach.test.ts`, landed with PR
#20329). `os g flow order_line` into a `['triggers']` project was pinned
as "cannot run", exit 0. The written flow now stops the config from
loading, so the write is refused. The pin is now: exit 1, the project
tree byte-identical, the flow file absent, stdout names `does not
include 'automation'` and prints `requires: ['automation', 'triggers']`,
and no `Created`. Result: `7 passed`.
## Tests and gates
The branch head is `94e32023d4`: a merge of `origin/main` `6704717188`
made through `scripts/pm/os-regen-merge.sh`. It touched
`error-code-ledger.zod.ts` on both sides, and both edits survive. Every
reading below was taken at that head unless it names `45cfbaafee`, the
last commit before the merge. The merge brought no change to any file
those older readings depend on.
- `@objectstack/spec` at `94e32023d4`:
- `vitest run --project local`: **554 files, 16439 passed, 1 todo**.
- `typecheck` exit 0.
- `check:generated`: every artifact up to date, after a rebuild.
- `@objectstack/cli` at `45cfbaafee`:
- `generate-object-namespace-prefix` and `generate-scaffold-wiring`
(unit): **52 passed**.
- `generate-stack-reach` (integration): **7 passed**.
- `@objectstack/lint`, the consumer suite, at `45cfbaafee`: **1 failed |
4303 passed**. The one failure is the fenced fixture described below.
Its new message, re-read at `94e32023d4`, is the refusal it should be.
This is expected, and owed.
- dogfood: the fixture module loads (`requires =
["automation","triggers"]`). The boot pin itself is left to CI's Dogfood
Regression Gate.
- `os validate` on the examples at `45cfbaafee`: `app-todo`,
`app-showcase` and `app-crm` each exit 0.
- Gates: `dispatch-gates --commands` at `94e32023d4` derives 112. All
112 ran, each with its exit code recorded, and `--ran` reconciles them:
**112 run, 0 NOT-MEASURED, 0 UNRUN**.
- Exit 0: 111. This includes every `@objectstack/spec` `check:*` in the
list (`api-surface`, `authorable-surface`, `docs`,
`error-code-provenance`, `liveness`, `skill-examples`),
`check:type-check-debt`, `check:dual-build-cjs-loads`, and the root
`check:*` and `node scripts/*` set.
- Exit 1, red by design: `check-empty-changeset`. See the next section.
## Deliberate correction of a pending release note
`.changeset/20215-generate-scaffolds-reach-stack.md` (from PR #20329,
unreleased) says two things this PR makes false:
- "Without `automation`, the server loads the flow and never runs it."
- that `os g` reports **cannot run** for a flow in a stack whose
`requires` lacks `automation`.
Both sentences are corrected in place. `content/docs/deployment/cli.mdx`
gets the same correction. `check-empty-changeset` stays red on this, by
design ("DELIBERATE CORRECTION … say so on the PR and get it
confirmed"). **This needs a person's confirmation.** Restoring the file
from base would put the false sentences back into the next release.
## Owed, and fenced (not in this PR)
These are outside the dispatch's fence, so this PR does not touch them:
1. `packages/lint/src/authoring-rule-input-tier.test.ts:93`: `requires:
['triggers']` becomes `['automation', 'triggers']`, and its comment
names the pair. Until then `@objectstack/lint`'s suite is red on that
one test.
2. `packages/cli/src/commands/init.ts` (the emitted config comment,
`:649–652`, and the `SCAFFOLD_WIRED_REQUIRES` docblock) and
`packages/cli/src/commands/generate.ts` (the emitted flow-file header,
`:366–369`, and its docblock). Each says that without `automation` "the
server loads the flow and never runs it". After this change the config
stops loading.
## Acceptance notes
- The `cannot run` branch of `os g`'s reach report has no scaffold that
reaches it now. The flow scaffold's only tokens are the pair, and a
stack missing either is refused. This is dead for today's generators. It
is noted and not filed. Carrier: the next PR to touch
`packages/cli/src/commands/generate.ts`.
- `os validate` on a config that exports a plain object instead of
calling `defineStack()` skips every `defineStack` cross-field refusal,
this one included. Measured: a plain-object probe with `requires:
['triggers']` and a `record_change` flow exits 0 at this head. This is
the whole refusal family's door, not this arm's, so it is reported to
the seat in the dev report and not filed here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent ab94656 commit 2d91c9a
16 files changed
Lines changed: 388 additions & 84 deletions
File tree
- .changeset
- content/docs
- automation
- deployment
- permissions
- packages
- cli
- src/commands
- test
- lint/src
- qa/dogfood/test/fixtures
- spec/src
- api
- automation
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
| 13 | + | |
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
18 | | - | |
| 17 | + | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| |||
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1782 | 1782 | | |
1783 | 1783 | | |
1784 | 1784 | | |
1785 | | - | |
1786 | | - | |
1787 | | - | |
1788 | | - | |
1789 | | - | |
1790 | | - | |
1791 | | - | |
1792 | | - | |
1793 | | - | |
| 1785 | + | |
| 1786 | + | |
| 1787 | + | |
| 1788 | + | |
| 1789 | + | |
| 1790 | + | |
| 1791 | + | |
| 1792 | + | |
| 1793 | + | |
| 1794 | + | |
| 1795 | + | |
| 1796 | + | |
| 1797 | + | |
1794 | 1798 | | |
1795 | 1799 | | |
1796 | 1800 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1431 | 1431 | | |
1432 | 1432 | | |
1433 | 1433 | | |
1434 | | - | |
1435 | | - | |
1436 | | - | |
1437 | 1434 | | |
1438 | 1435 | | |
1439 | 1436 | | |
1440 | | - | |
1441 | | - | |
1442 | | - | |
| 1437 | + | |
| 1438 | + | |
| 1439 | + | |
| 1440 | + | |
| 1441 | + | |
1443 | 1442 | | |
1444 | 1443 | | |
1445 | 1444 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | | - | |
| 29 | + | |
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
348 | 348 | | |
349 | 349 | | |
350 | 350 | | |
351 | | - | |
352 | | - | |
353 | | - | |
354 | | - | |
355 | | - | |
356 | | - | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
357 | 358 | | |
358 | 359 | | |
359 | 360 | | |
| |||
365 | 366 | | |
366 | 367 | | |
367 | 368 | | |
368 | | - | |
369 | | - | |
| 369 | + | |
| 370 | + | |
370 | 371 | | |
371 | 372 | | |
372 | 373 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
605 | 605 | | |
606 | 606 | | |
607 | 607 | | |
608 | | - | |
609 | | - | |
610 | | - | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
611 | 612 | | |
612 | 613 | | |
613 | 614 | | |
| |||
647 | 648 | | |
648 | 649 | | |
649 | 650 | | |
650 | | - | |
651 | | - | |
652 | | - | |
| 651 | + | |
| 652 | + | |
653 | 653 | | |
654 | 654 | | |
655 | 655 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
105 | | - | |
106 | | - | |
107 | | - | |
108 | | - | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
109 | 110 | | |
110 | 111 | | |
111 | 112 | | |
| |||
116 | 117 | | |
117 | 118 | | |
118 | 119 | | |
119 | | - | |
| 120 | + | |
120 | 121 | | |
121 | 122 | | |
122 | 123 | | |
| |||
0 commit comments