Skip to content

feat(lint): webhook without triggers fails at author time (ADR-0078 Phase 3) - #4565

Merged
os-zhuang merged 1 commit into
mainfrom
claude/adr-0078-phase3
Aug 2, 2026
Merged

feat(lint): webhook without triggers fails at author time (ADR-0078 Phase 3)#4565
os-zhuang merged 1 commit into
mainfrom
claude/adr-0078-phase3

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Phase 3 of #4544. Builds on the Phase 1 predicate (#4547).

The rule

webhook/without-triggers, error, in the shared @objectstack/spec/kernel predicate beside the Phase 1 rules, walked by validate-functional-completeness over stack.webhooks in both collection spellings.

A webhook with no triggers never fires on any path. It still materializes into sys_webhook and 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:

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 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 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 → error, not a NON-rule.

The generalization, 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's a comment about a mode that no longer exists.

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 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 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:

candidate disposition
A2 action without locations already shippedvalidate-action-locations.ts, which already exempts the documented locations: []
approval empty/unresolvable approvers already shippedvalidate-approval-approvers.ts
select/multiselect without options shipped in Phase 1
write-side referential integrity not an authoring-lint item — runtime gap, no metadata omission to detect
unique: true no-op on memory driver not an authoring-lint item — driver gap
composite/repeater sub-field constraints not an authoring-lint item — runtime gap
nav targets: page/report/url/component/action genuine gap, different module — key present but dangling ⇒ reference resolvability (ADR-0072), not completeness
dataset with zero measures unverified — not shipped. No runtime consumer in this repo
schedule trigger with invalid cron unverified — not shipped. normalizeSchedule takes any non-empty string; scheduler behaviour not traced
webhook without triggers this PR

Two 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

  • 24 predicate tests (19 → 24), 8 walk tests (7 → 8)
  • The one real webhook in the repo produces no finding
  • API surface: additive only. i18n ratchet: unchanged
  • Full suite: 132/132

Generated by Claude Code

…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
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 2, 2026 3:23am

Request Review

@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 03:23
@os-zhuang
os-zhuang enabled auto-merge August 2, 2026 03:23
@github-actions github-actions Bot added the size/m label Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec.

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 2, 2026
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit e6e9379 Aug 2, 2026
21 checks passed
@os-zhuang
os-zhuang deleted the claude/adr-0078-phase3 branch August 2, 2026 03:44
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 4, 2026
…ectstack-ai#4919)

ADR-0078 的状态行仍写着「Proposed……核心机制未建(2026-07-16 审计)」,
而它所说「未建」的机制早已合并四次:共享谓词
packages/spec/src/kernel/functional-completeness.ts、作者期闸门
packages/lint/src/validate-functional-completeness.ts(在 authoring-rules.ts
里注册为 gating 层,error 级发现会让 os build/validate/lint 直接失败),以及
packages/objectql/src/registry.ts 里的注册期孪生诊断。一份说「没建」的 ADR
本身就是误导源:后来者要么重新立项,要么绕开一道已经承重的闸门。

状态词按仓内既有已落地 ADR 的写法改为
「Accepted — framework implemented; the cloud half of §2/§3 pending (cloud)」,
并且**没有**笼统标成全做完 —— 分阶段的 ADR 标「全完成」比标「未开始」更有害,
因为它会让人不再去查。逐阶段:Phase 1 ✅(objectstack-ai#4547)、Phase 2 🟡 只落地框架侧
(objectstack-ai#4577)、Phase 3 ✅ 在验证支持的范围内(objectstack-ai#4565)、Phase 4 ✅ 已裁决(objectstack-ai#4599);
§2 的 cloud graph-lint 复用共享谓词仍未落地,单独标注为唯一会悄悄腐烂的开口。

正文一并更新:§2/§3/§4 加落地说明;新增决策 §6 —— 「一条规则只有带上让它成立的
那行运行时静默跳过点才能上线」,以及「运行时注释对空值的祝福必须另有证据证明该模式
仍可达」(webhook 跳过点祝福的 manual-only 模式已被 objectstack-ai#3196 删除)。Phase 4 的
生成式规则遍历记为**否决而非推迟**。两条推论(multiselect 无 options 的
NON-rule 等)在 scripts/adr-anchors.json 里锚定回实现文件。

仅文档;不发版。


Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants