Repository navigation
Commit baedcec
feat(spec)!: defineStack refuses a declared webhook whose requires lacks webhooks or realtime (STACK_WEBHOOK_CAPABILITY_REQUIRED) (#25057)
Fixes #24997
Refs #24910
Clause-②: no (narrowing: a stack declaring a webhook without the
capability tokens is refused; ships now under the amended 「甲」, #6015
`6303865025`)
## What the ruling binds
Ruling A on #24910 (record `6303779439`, amended by `6303859748`: 「暂时不考虑
v19,协议的调整原则上也是尽快,不考虑退役版本。」), as the card states it:
> - `defineStack` refuses a stack that declares a webhook while
`requires` lacks `'webhooks'` or `'realtime'`, with ONE code for every
arm, `STACK_WEBHOOK_CAPABILITY_REQUIRED`, in the `STACK_*` family beside
`STACK_TRIGGER_CAPABILITY_REQUIRED`
(`packages/spec/src/stack.zod.ts:3263`, #20332): which token is missing
is in the finding, not in the code. Registered in
`packages/spec/src/api/error-code-ledger.zod.ts`.
> - Every door that loads a stack refuses with the same sentence: `os
validate`, `os build`, `os serve` / `os dev`, an app's own `bootStack`
tests, the cloud runtime. The boot `error` of PR #24946 (`19e5338cc8`)
stays for a host that wires a stack by hand.
> - **Measure first, at claim:** cloud's `resolveCapabilityDependencies`
for `webhooks` (the trigger rule measured it for #20332); the dogfood
fixture
`packages/qa/dogfood/test/fixtures/webhook-materialization-fixture.ts`
(boots without realtime on purpose) is converted or declared, not
deleted; the example apps' `requires` re-checked
(`examples/app-showcase` already carries both tokens).
> - ⛔ No token implied or auto-added; ⛔ the activation door is unchanged
(A′ was not taken on #24910).
## What changed
- `packages/spec/src/stack.zod.ts`: `validateWebhookCapability`, the
third declared-capability refusal, follows the
`validateTriggerCapability` precedent exactly. It runs right after the
trigger check and throws `StackWebhookCapabilityRequiredError` (`code:
'STACK_WEBHOOK_CAPABILITY_REQUIRED'`, `status: 422`, the
`StackRefusalError` envelope) with the header `defineStack webhook
capability validation failed (N issues):` and one `issues` entry per
declared webhook. Each entry names the webhook, the missing token or
tokens, and a prescription that is the whole fix for the `requires` it
was given:
- `realtime` present, `webhooks` missing: add `'webhooks'`;
- `webhooks` present, `realtime` missing: add `'realtime'`;
- neither (an absent `requires` included): add both, `['webhooks',
'realtime']`.
- Every declared webhook counts, `isActive: false` included. Unlike a
retired flow, an inactive declaration is one activation-door write away
from being switched on (the ledger row overrides the declared
`isActive`, ADR-0131 D6), and the switch would then deliver nothing.
Only the top-level `webhooks:` collection is read, the one the
declared-webhook reader dispatches. Nothing is implied or added;
`strict: false` skips the judgement by contract, as for every sibling.
- `packages/spec/src/api/error-code-ledger.zod.ts`: the new code is
registered under `@objectstack/spec` beside the other `STACK_*` rows,
with `door: 'none'` on their reading. The generated
`content/docs/references/api/error-code-ledger.mdx` and `contract.mdx`
are regenerated (`check:generated --fix`, which proved only those
stale).
- `content/docs/automation/webhooks.mdx` §3.0 now says that such a stack
is **refused**, names `'webhooks'` and `'realtime'`, says that every
door stops with the same sentence, and says that the boot `error`
remains for a host that wires a stack by hand.
- Changeset `.changeset/24997-webhook-capability-required.md`:
`@objectstack/spec` minor, BREAKING banner, `Clause-②: no (narrowing)`,
the `adr-0087: not-required (no-migration-prescription)` marker. Same
shape as the 24813 changeset.
## Producers measured and fixed
| producer (tree `3b3830ca22`) | declares a webhook | `requires` |
action |
|:--|:--|:--|:--|
| `examples/app-showcase` | yes (`allWebhooks`) | carries `webhooks` and
`realtime` | none; still loads |
| `examples/app-crm`, `app-todo`, `app-multi-package`, `embed-objectql`
| no (`app-crm` names webhooks in a comment only) | n/a | none |
| `create-objectstack` blank template | no | `['automation',
'triggers']` | none; refused once a webhook is added, which is the point
|
| `packages/qa/dogfood/test/fixtures/webhook-materialization-fixture.ts`
| yes | absent | **declared**, see below |
| `packages/spec/src/stack-duplicate-entry.test.ts` webhook row | yes |
absent | row context now carries `requires: ['webhooks', 'realtime']` |
|
`packages/qa/dogfood/test/webhook-meta-door-credential-redaction.dogfood.test.ts`
(landed on `main` after the branch point) | yes | absent | **declared
via `strict: false`**, because its "without plugin-webhooks" composition
is what the redactor pins need. Same choice as the materialization
fixture. Commit `779ed238e5` |
Search used to find producers: every file that calls `defineStack` /
`bootStack` / `composeStacks` and names `webhooks`, plus `defineWebhook`
and `webhooks: [` repo-wide. The plugin-webhooks tests build
declarations through `DeclaredWebhookSource`, never through
`defineStack`, so they are unaffected.
**The dogfood fixture is declared, not converted.** It is now built with
`defineStack(..., { strict: false })`, and a docblock says why. The
proof exists to show that the declared-webhook reader binds on the data
engine alone, with NO realtime service. If the fixture declared the
pair, `bootStack` would mount realtime the way `serve` does, and a
reader re-gated behind the dispatch prerequisites would then pass
unseen. So the fixture models the one composition `defineStack` cannot
reach: a hand-wired host. That is the population the boot `error` of
#24946 serves. The precedent for a dogfood fixture built `strict: false`
on purpose is `flow-node-config-values-at-registration.dogfood.test.ts`.
Its second consumer,
`wall-declared-webhook-deployment-wide.dogfood.test.ts`, hand-wires
realtime and is unaffected. Both suites pass.
**Cloud's `resolveCapabilityDependencies`: NOT MEASURED.** Reason: the
`cloud` repository is not in this container. The refusal sits in
`defineStack`, so the cloud runtime refuses with the same sentence on
any stack it loads through the producer. Whether its resolver implies
`realtime` from `webhooks` changes nothing here, because no token is
implied by ruling.
## The doors, measured (scaffold-shaped project, `requires:
['automation', 'triggers']` plus one declared webhook; CLI from source,
`bin/run-dev.js`)
| `requires` | `os validate` | `os build --json` | `os dev --no-watch
--fresh` |
|:--|:--|:--|:--|
| `['automation', 'triggers']` | exit 1, `--json` `"code":
"STACK_WEBHOOK_CAPABILITY_REQUIRED"`, finding: `webhook
'blank_note_changed' is declared but `requires` does not include
'webhooks' or 'realtime'` | exit 1, `"success": false`, `"code":
"STACK_WEBHOOK_CAPABILITY_REQUIRED"` | exit 1 at `Loading
objectstack.config.ts...`, same header and finding |
| `[..., 'webhooks']` | exit 1, finding names `'realtime'` | not run |
not run |
| `[..., 'realtime']` | exit 1, finding names `'webhooks'` | not run |
not run |
| `['automation', 'triggers', 'webhooks', 'realtime']` (CONTROL) | exit
0 | exit 0, `"success": true` | `Server is ready`, `/api/v1/health` 200,
`WebhookOutboxPlugin` and `RealtimeServicePlugin` in the plugin list;
stopped by its recorded PID |
An app's `bootStack` tests hit the same refusal at import, because the
throw is in `defineStack` itself.
## Tests (pinned arms, control, ablation)
- New `packages/spec/src/stack-webhook-capability.test.ts`. It pins: the
three arms (no `webhooks`, no `realtime`, both missing; for both
missing, `requires` of `['automation', 'triggers']`, `[]` and absent),
each with `code`, `status: 422`, the header, one `issues` entry and the
finding text naming the webhook and the token(s); the CONTROL (the
scaffold tokens plus the pair, the pair in either order, among other
tokens); each prescription applied once is accepted; no token is implied
(an accepted stack keeps the `requires` it declared); a stack with no
webhook owes nothing; an inactive declaration still owes the pair; every
webhook reported, one entry each; the map form; the trigger sibling
reports first; `strict: false` skips.
- `stack-refusal-envelopes.test.ts`: a webhook row (refused + accepted
control); the census is now nine sites and nine distinct codes.
`stack-conversions-record.test.ts`: a webhook row; its census is now
nine codes.
- **Ablation, red:** `node scripts/ablation-replace.mjs` replaced the
call `const webhookErrors = validateWebhookCapability(data);` with an
empty list. The tool's own on-disk evidence: anchor 1 to 0, blob
`3d8624dc4fa1` to `5898d7c8a60e`. With the call gone, the three spec
files gave **18 failed / 76 passed**. Restore: blob equals HEAD
`3d8624dc4fa1` and `git diff HEAD` is empty. No dist rebuild was needed,
because these tests import `./stack.zod` from source.
## Verification
- **Resumed after a container restart:** merged `origin/main`
`63085c3806`. Regenerated
`content/docs/references/api/automation-api.mdx` (a page added on `main`
that lists every error code), commit `e69078f0d1`. At `779ed238e5`: spec
`check:generated` up to date, spec tests 21446 passed, spec `test:repo`
1237 passed, the three webhook-declaring dogfood files 24 of 24 passed,
and `dispatch-gates --ran` shows 116 derived and 116 run. Report comment
`6309741302` on #24997.
Run at `5107a88d13` (before the `origin/main` merge) unless stated
otherwise:
- `pnpm --filter @objectstack/spec test`: 703/704 files passed on the
first run. The one failure was the conversions-record census (`8` to
`9`), fixed; the four affected files then gave 232/232. `test:repo`: 72
files / 1236 tests passed. `typecheck`: passed (`check:test-typecheck:
OK`). `check:generated`: all 14 artifacts up to date after `--fix`
regenerated the two reference pages.
- `@objectstack/dogfood`: `webhook-materialization.dogfood.test.ts` and
`wall-declared-webhook-deployment-wide.dogfood.test.ts`, 9/9 passed.
- `@objectstack/cli` `--project unit`: 313/316 files passed. The 3
failures (`published-subpath-console.pin`,
`published-subpath-hook-body.pin`, `scaffold-emission-typechecks`) read
packed `.d.ts`, and an `OS_SKIP_DTS` closure build had removed them
(TS7016 "Could not find a declaration file"). After a full declaration
build of `@objectstack/cli...`, the three files gave 42/42. Integration
layer: declared to CI (this diff touches no cli file).
- Lint: `eslint --no-inline-config --format json` over the 7 touched
`.ts` files gave 7 files, 0 errors, 0 warnings. Population: all 7
resolve a config (`--print-config`). Invariance: `eslint.config.mjs`
enables no type-aware linting (no `parserOptions.project`), so this diff
cannot move the verdict on any untouched file. Repo-wide `pnpm lint`
belongs to CI.
- `node scripts/pm/dispatch-gates.mjs --commands`: 116 families derived
at `5107a88d13`, all 116 run, all exit 0. Seven of them first answered
PREREQUISITE NOT MET because of the same skip-DTS build, and gave exit 0
on re-run after the declaration build: `check:api-surface`,
`check:dual-source-exports`, `check:entry-nameability`,
`check:exported-any`, `check:skill-examples`, `check:dts-closure`,
`check:dual-build-cjs-loads`.
- After merging `origin/main` (14 commits, file-disjoint from this
diff): spec build + `check:generated` + spec `test` were re-run on the
merge head; see the report comment for that reading.
## Acceptance notes
- Text-mode `os validate` and `os dev` print the refusal's message,
header and finding, but not the `code`. That is how every `STACK_*`
refusal surfaces today, the trigger sibling included; the code is on
`--json` (`os validate --json`, `os build --json`). Carrier: none. The
CLI's text renderer is not this card's surface.
- `skills/objectstack-automation/SKILL.md`, the capability table, still
reads `webhooks`, absent, "nothing delivered outbound", and does not
name `realtime`. Since this change, a stack in that state is refused
with the fix named, so this is no silent trap. `skills/**` is governed
(Tier H) and outside the claim's file surface, so it is not edited here.
Carrier: none.
- Contract review at `CONTRACT_REVIEW_TIER` is owed on this PR (the
claim and the ruling both say so).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 2b1382a commit baedcec
13 files changed
Lines changed: 376 additions & 17 deletions
File tree
- .changeset
- content/docs
- automation
- references/api
- packages
- qa/dogfood/test
- fixtures
- spec/src
- api
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
104 | | - | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
105 | 111 | | |
106 | 112 | | |
107 | 113 | | |
| |||
113 | 119 | | |
114 | 120 | | |
115 | 121 | | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
120 | 128 | | |
121 | 129 | | |
122 | 130 | | |
123 | | - | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
124 | 134 | | |
125 | 135 | | |
126 | 136 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
352 | 352 | | |
353 | 353 | | |
354 | 354 | | |
355 | | - | |
| 355 | + | |
356 | 356 | | |
357 | 357 | | |
358 | 358 | | |
| |||
673 | 673 | | |
674 | 674 | | |
675 | 675 | | |
| 676 | + | |
676 | 677 | | |
677 | 678 | | |
678 | 679 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| |||
356 | 356 | | |
357 | 357 | | |
358 | 358 | | |
| 359 | + | |
359 | 360 | | |
360 | 361 | | |
361 | 362 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
523 | 523 | | |
524 | 524 | | |
525 | 525 | | |
| 526 | + | |
526 | 527 | | |
527 | 528 | | |
528 | 529 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
49 | 63 | | |
50 | 64 | | |
51 | 65 | | |
| |||
57 | 71 | | |
58 | 72 | | |
59 | 73 | | |
60 | | - | |
| 74 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
60 | 73 | | |
61 | 74 | | |
62 | 75 | | |
| |||
79 | 92 | | |
80 | 93 | | |
81 | 94 | | |
82 | | - | |
| 95 | + | |
83 | 96 | | |
84 | 97 | | |
85 | 98 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1554 | 1554 | | |
1555 | 1555 | | |
1556 | 1556 | | |
| 1557 | + | |
| 1558 | + | |
| 1559 | + | |
| 1560 | + | |
| 1561 | + | |
| 1562 | + | |
| 1563 | + | |
1557 | 1564 | | |
1558 | 1565 | | |
1559 | 1566 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
533 | 533 | | |
534 | 534 | | |
535 | 535 | | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
536 | 547 | | |
537 | 548 | | |
538 | 549 | | |
| |||
553 | 564 | | |
554 | 565 | | |
555 | 566 | | |
556 | | - | |
557 | | - | |
| 567 | + | |
| 568 | + | |
558 | 569 | | |
559 | 570 | | |
560 | 571 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
133 | 133 | | |
134 | 134 | | |
135 | 135 | | |
136 | | - | |
| 136 | + | |
| 137 | + | |
137 | 138 | | |
138 | 139 | | |
139 | 140 | | |
| |||
0 commit comments