feat(lint): webhook without triggers fails at author time (ADR-0078 Phase 3) - #4565
Merged
Conversation
…hase 3)
A webhook that declares no `triggers` never fires on any path. It still
materializes into `sys_webhook` and still renders in Setup looking armed, so the
omission is invisible on every surface an author can see — the ADR-0078 shape.
## The first source argued AGAINST this rule
The runtime skip site reads:
if (triggers.size === 0) {
// No dispatchable triggers (or a manual-only webhook with none) —
// skip auto-enqueue.
return null;
That parenthetical blesses the empty case as a deliberate mode. It is
structurally identical to the `multiselect`-without-options NON-rule, where
`record-validator.ts`'s `// free-form (tags without options)` is precisely why
we do NOT flag it. On this evidence alone the candidate stays unenforced.
The mode it names does not exist. `webhook.zod.ts`'s #3196 note records that the
`api` (manual/programmatic fire) trigger was REMOVED because "no manual fire
path exists — the only webhook HTTP surface re-queues already-failed
deliveries". Nothing can fire a webhook the auto-enqueuer dropped. Inert on
every path, so: error, not a NON-rule.
The generalization is now in the module doc and pinned by a test: a runtime
comment records what its author BELIEVED, and beliefs go stale when a sibling
feature is deleted. A blessing needs corroboration that the blessed mode is
still reachable, or it is a comment about a mode that no longer exists. The test
asserts the finding carries both citations so nobody demotes the rule on the
comment alone.
`triggers: []` is flagged identically to an omitted `triggers`: unlike an
action's `locations: []` (the documented headless spelling), an empty array here
carries no "I meant it" signal, because a webhook's off switch is `isActive`.
The repo's one real webhook confirms the premise — `showcase_task_changed`
ships inactive via `isActive: false` with a full trigger list, and is clean.
## Phase 3's scope was mostly already closed
Verifying each Tier-B candidate before writing it — the discipline that caught
four false prescriptions in #4001 — found most of the audit's list already
handled or misfiled:
A2 action without `locations` already shipped (validate-action-locations,
which already exempts `locations: []`)
approval empty approvers already shipped (validate-approval-approvers)
select/multiselect options shipped in Phase 1
write-side FK integrity not an authoring-lint item (runtime gap)
`unique` on memory driver not an authoring-lint item (driver gap)
composite sub-field constraints not an authoring-lint item (runtime gap)
nav page/report/url/component genuine gap, but reference resolvability
(ADR-0072), not completeness — wrong module
dataset with zero measures UNVERIFIED, not shipped: no runtime consumer
in this repo
schedule trigger invalid cron UNVERIFIED, not shipped: normalizeSchedule
takes any non-empty string, scheduler
behaviour not traced
webhook without triggers this change
Two candidates are left unshipped rather than written on the audit's stated
confidence, and one is left to the module that owns it.
Verification: 24 predicate tests (19 -> 24) and 8 walk tests (7 -> 8); the one
real webhook in the repo produces no finding; full suite 132/132.
Tracked in #4544.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnqGjQFQMqd5k81LYV8SCY
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
os-zhuang
marked this pull request as ready for review
August 2, 2026 03:23
os-zhuang
enabled auto-merge
August 2, 2026 03:23
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 3 of #4544. Builds on the Phase 1 predicate (#4547).
The rule
webhook/without-triggers, error, in the shared@objectstack/spec/kernelpredicate beside the Phase 1 rules, walked byvalidate-functional-completenessoverstack.webhooksin both collection spellings.A webhook with no
triggersnever fires on any path. It still materializes intosys_webhookand still renders in Setup looking armed — the omission is invisible on every surface an author can see.The first source argued against this rule
The runtime skip site reads:
That parenthetical blesses the empty case as a deliberate mode. It is structurally identical to the
multiselect-without-options NON-rule, whererecord-validator.ts's// free-form (tags without options)is precisely why we don't flag it. On this evidence alone, the candidate stays unenforced.The mode it names does not exist.
webhook.zod.ts's #3196 note records that theapi(manual/programmatic fire) trigger was removed because "no manual fire path exists — the only webhook HTTP surface re-queues already-failed deliveries". Nothing can fire a webhook the auto-enqueuer dropped. Inert on every path →error, not a NON-rule.The test asserts the finding carries both citations, so nobody demotes this rule on the strength of the comment alone.
triggers: []is flagged identically to an omittedtriggers— unlike an action'slocations: [](the documented headless spelling), an empty array here carries no "I meant it" signal, because a webhook's off switch isisActive. The repo's one real webhook confirms the premise:showcase_task_changedships inactive viaisActive: falsewith a full trigger list, and produces no finding.Phase 3's scope was mostly already closed
Verifying each Tier-B candidate before writing it — the discipline that caught four false prescriptions in #4001 — found most of the audit's list already handled or misfiled:
locationsvalidate-action-locations.ts, which already exempts the documentedlocations: []validate-approval-approvers.tsunique: trueno-op on memory drivernormalizeScheduletakes any non-empty string; scheduler behaviour not tracedTwo candidates are deliberately left unshipped rather than written on the audit's stated confidence, and one is left to the module that owns it. The audit's own lesson stands: it produces candidates, not confirmed bugs.
Verification
Generated by Claude Code