Repository navigation
feat(spec)!: defineStack refuses a declared webhook whose requires lacks webhooks or realtime (STACK_WEBHOOK_CAPABILITY_REQUIRED) - #25057
Conversation
…nnot dispatch it (STACK_WEBHOOK_CAPABILITY_REQUIRED) Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…d fixture's hand-wired composition; docs and changeset Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…bhook-capability-required
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 86335e0c4488ac16a45415bdd621b16da692c7fa && git checkout 86335e0c4488ac16a45415bdd621b16da692c7fa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4bf079c6935b34b68da6d52acc67cf64cb4a55ac 779ed238e57bd2a678b3c35cbda06cb0690be707 && git checkout -B drift-repro 4bf079c6935b34b68da6d52acc67cf64cb4a55ac && git merge --no-ff 779ed238e57bd2a678b3c35cbda06cb0690be707
node scripts/docs-audit/affected-docs.mjs --json 4bf079c6935b34b68da6d52acc67cf64cb4a55ac
|
…bhook-capability-required
…the FlowInnerRefusal code set now lists STACK_WEBHOOK_CAPABILITY_REQUIRED Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…composition (strict: false) — it declares a webhook without requires on purpose The without-plugin-webhooks composition is what the redactor pins need; declaring requires: ['webhooks', 'realtime'] would have bootStack mount the plugin and remove that composition. Same disposition as webhook-materialization-fixture. Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Seat re-read of the check-runs at landing: all 49 runs on |
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 by6303859748: 「暂时不考虑 v19,协议的调整原则上也是尽快,不考虑退役版本。」), as the card states it:What changed
packages/spec/src/stack.zod.ts:validateWebhookCapability, the third declared-capability refusal, follows thevalidateTriggerCapabilityprecedent exactly. It runs right after the trigger check and throwsStackWebhookCapabilityRequiredError(code: 'STACK_WEBHOOK_CAPABILITY_REQUIRED',status: 422, theStackRefusalErrorenvelope) with the headerdefineStack webhook capability validation failed (N issues):and oneissuesentry per declared webhook. Each entry names the webhook, the missing token or tokens, and a prescription that is the whole fix for therequiresit was given:realtimepresent,webhooksmissing: add'webhooks';webhookspresent,realtimemissing: add'realtime';requiresincluded): add both,['webhooks', 'realtime'].isActive: falseincluded. Unlike a retired flow, an inactive declaration is one activation-door write away from being switched on (the ledger row overrides the declaredisActive, ADR-0131 D6), and the switch would then deliver nothing. Only the top-levelwebhooks:collection is read, the one the declared-webhook reader dispatches. Nothing is implied or added;strict: falseskips the judgement by contract, as for every sibling.packages/spec/src/api/error-code-ledger.zod.ts: the new code is registered under@objectstack/specbeside the otherSTACK_*rows, withdoor: 'none'on their reading. The generatedcontent/docs/references/api/error-code-ledger.mdxandcontract.mdxare 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 booterrorremains for a host that wires a stack by hand..changeset/24997-webhook-capability-required.md:@objectstack/specminor, BREAKING banner,Clause-②: no (narrowing), theadr-0087: not-required (no-migration-prescription)marker. Same shape as the 24813 changeset.Producers measured and fixed
3b3830ca22)requiresexamples/app-showcaseallWebhooks)webhooksandrealtimeexamples/app-crm,app-todo,app-multi-package,embed-objectqlapp-crmnames webhooks in a comment only)create-objectstackblank template['automation', 'triggers']packages/qa/dogfood/test/fixtures/webhook-materialization-fixture.tspackages/spec/src/stack-duplicate-entry.test.tswebhook rowrequires: ['webhooks', 'realtime']packages/qa/dogfood/test/webhook-meta-door-credential-redaction.dogfood.test.ts(landed onmainafter the branch point)strict: false, because its "without plugin-webhooks" composition is what the redactor pins need. Same choice as the materialization fixture. Commit779ed238e5Search used to find producers: every file that calls
defineStack/bootStack/composeStacksand nameswebhooks, plusdefineWebhookandwebhooks: [repo-wide. The plugin-webhooks tests build declarations throughDeclaredWebhookSource, never throughdefineStack, 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,bootStackwould mount realtime the wayservedoes, and a reader re-gated behind the dispatch prerequisites would then pass unseen. So the fixture models the one compositiondefineStackcannot reach: a hand-wired host. That is the population the booterrorof #24946 serves. The precedent for a dogfood fixture builtstrict: falseon purpose isflow-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: thecloudrepository is not in this container. The refusal sits indefineStack, so the cloud runtime refuses with the same sentence on any stack it loads through the producer. Whether its resolver impliesrealtimefromwebhookschanges 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)requiresos validateos build --jsonos dev --no-watch --fresh['automation', 'triggers']--json"code": "STACK_WEBHOOK_CAPABILITY_REQUIRED", finding:webhook 'blank_note_changed' is declared butrequiresdoes not include 'webhooks' or 'realtime'"success": false,"code": "STACK_WEBHOOK_CAPABILITY_REQUIRED"Loading objectstack.config.ts..., same header and finding[..., 'webhooks']'realtime'[..., 'realtime']'webhooks'['automation', 'triggers', 'webhooks', 'realtime'](CONTROL)"success": trueServer is ready,/api/v1/health200,WebhookOutboxPluginandRealtimeServicePluginin the plugin list; stopped by its recorded PIDAn app's
bootStacktests hit the same refusal at import, because the throw is indefineStackitself.Tests (pinned arms, control, ablation)
packages/spec/src/stack-webhook-capability.test.ts. It pins: the three arms (nowebhooks, norealtime, both missing; for both missing,requiresof['automation', 'triggers'],[]and absent), each withcode,status: 422, the header, oneissuesentry 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 therequiresit 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: falseskips.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.node scripts/ablation-replace.mjsreplaced the callconst webhookErrors = validateWebhookCapability(data);with an empty list. The tool's own on-disk evidence: anchor 1 to 0, blob3d8624dc4fa1to5898d7c8a60e. With the call gone, the three spec files gave 18 failed / 76 passed. Restore: blob equals HEAD3d8624dc4fa1andgit diff HEADis empty. No dist rebuild was needed, because these tests import./stack.zodfrom source.Verification
origin/main63085c3806. Regeneratedcontent/docs/references/api/automation-api.mdx(a page added onmainthat lists every error code), commite69078f0d1. At779ed238e5: speccheck:generatedup to date, spec tests 21446 passed, spectest:repo1237 passed, the three webhook-declaring dogfood files 24 of 24 passed, anddispatch-gates --ranshows 116 derived and 116 run. Report comment6309741302on spec: refuse a declared webhook whoserequirescannot dispatch it —STACK_WEBHOOK_CAPABILITY_REQUIREDindefineStack, the third declared-capability refusal (ruling A on #24910; ships in 18.x by the maintainer's re-timing) #24997.Run at
5107a88d13(before theorigin/mainmerge) unless stated otherwise:pnpm --filter @objectstack/spec test: 703/704 files passed on the first run. The one failure was the conversions-record census (8to9), 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--fixregenerated the two reference pages.@objectstack/dogfood:webhook-materialization.dogfood.test.tsandwall-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 anOS_SKIP_DTSclosure 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).eslint --no-inline-config --format jsonover the 7 touched.tsfiles gave 7 files, 0 errors, 0 warnings. Population: all 7 resolve a config (--print-config). Invariance:eslint.config.mjsenables no type-aware linting (noparserOptions.project), so this diff cannot move the verdict on any untouched file. Repo-widepnpm lintbelongs to CI.node scripts/pm/dispatch-gates.mjs --commands: 116 families derived at5107a88d13, 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.origin/main(14 commits, file-disjoint from this diff): spec build +check:generated+ spectestwere re-run on the merge head; see the report comment for that reading.Acceptance notes
os validateandos devprint the refusal's message, header and finding, but not thecode. That is how everySTACK_*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 readswebhooks, absent, "nothing delivered outbound", and does not namerealtime. 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_TIERis owed on this PR (the claim and the ruling both say so).Generated by Claude Code