Skip to content

fix(webhooks): materialize stack-declared webhooks into the dispatcher (#3461) - #3489

Merged
os-zhuang merged 1 commit into
mainfrom
fix/3461-webhook-bridge
Jul 25, 2026
Merged

fix(webhooks): materialize stack-declared webhooks into the dispatcher (#3461)#3489
os-zhuang merged 1 commit into
mainfrom
fix/3461-webhook-bridge

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3461.

Problem

The spec WebhookSchema authoring surface was disconnected from the runtime dispatcher — not merely a naming drift. A webhook authored declaratively (defineStack({ webhooks }) / defineWebhook(), declaring object / isActive) is decomposed into the ObjectQL registry as webhook metadata, but the dispatcher (AutoEnqueuer) fans out off sys_webhook data rows (object_name / active) that until now were only ever written by hand through the object's CRUD UI. No ingestion path turned a declared webhook into a dispatchable row, so authoring webhooks: on a stack was a silent no-op (ADR-0078). The showcase app itself shipped a webhooks: entry that did nothing — and, separately, never required the webhooks capability, so the dispatcher plugin wasn't even mounted.

Per the issue's decision, this is Option A — build the bridge (Option B would retire a public authoring surface the showcase already uses).

What changed

  • bootstrap-declared-webhooks.ts — reads declared webhook metadata from the registry (falling back to the metadata service), validates each through WebhookSchema.parse() (the spec schema finally has a real consumer + default-fill), and materializes a sys_webhook row: object → object_name, isActive → active, full validated envelope → definition_json (whence the enqueuer reads headers/secret/timeout). Modeled on the sibling bootstrapDeclaredSharingRules.
  • Runs on the data engine alone, before the auto-enqueuer's first cache refresh — deliberately not gated behind the realtime/messaging dispatch prerequisites. (An earlier revision gated it inside bootAutoEnqueue, which meant a realtime-less deployment silently failed to materialize — the very no-op class this closes. Caught during the real-boot dogfood.)
  • Seed-not-clobber provenance (mirrors sys_sharing_rule, Audit sibling declared-metadata↔record two-store types (sys_position, sys_sharing_rule, sys_capability) per ADR-0094 addendum #2909): sys_webhook gains managed_by / customized. Declared webhooks re-seed every boot as managed_by: 'package'; a row an admin created (admin) or edited (customized, stamped by a beforeUpdate hook in webhook-provenance.ts) is never overwritten — a deactivated noisy webhook survives redeploys.
  • showcase — require the webhooks + realtime capabilities so the dispatcher actually mounts, and ship the demo webhook inactive (the hooks.example URL is a placeholder; activate it in Setup).
  • specWebhookSchema docstring documents the materialization contract. Connector webhooks remain not-yet-enforced (Audit: several event/subscription/connector enums are schema-only (declared, no runtime consumer) #3197).
  • Fixed a pre-existing dangling SysWebhookDelivery import in the i18n extract config (dead since delivery moved to service-messaging).

Verification

9 new unit tests (bootstrap-declared-webhooks.test.ts) — mapping, idempotency, declared-change propagation, seed-not-clobber, admin-name-collision, invalid-webhook skip, no-op-when-empty, and end-to-end dispatch (declared → materialized → AutoEnqueuer fires with headers/secret from definition_json). Red-proofed (breaking the mapping + the skip guard fails the right tests). Full suite: 23 passing.

Real showcase boot (objectstack dev, SQLite):

  • declared webhook materializes into a sys_webhook row — object_name=showcase_task, active=0, managed_by=package
  • same-DB reboot stays idempotent (1 row, not duplicated) ✅
  • an admin's customized edit (active=1) survives redeploy despite the declared isActive:false

Build + DTS type-check green.

Follow-ups (out of scope)

🤖 Generated with Claude Code

#3461)

The spec `WebhookSchema` authoring surface (`defineStack({ webhooks })` /
`defineWebhook()`, `object` / `isActive`) was disconnected from the runtime
dispatcher, which fans out off `sys_webhook` DATA rows (`object_name` /
`active`) written only by hand through the object's CRUD UI. Nothing turned a
declared webhook into a dispatchable row, so authoring `webhooks:` on a stack
was a silent no-op (ADR-0078) — the showcase itself shipped one that did
nothing.

- `bootstrapDeclaredWebhooks` reads declared `webhook` metadata from the
  ObjectQL registry (where manifest decomposition already parks
  `stack.webhooks`), validates each through `WebhookSchema.parse()` (the spec
  schema finally gets a real consumer), and materializes it into a `sys_webhook`
  row: `object → object_name`, `isActive → active`, full envelope →
  `definition_json`. Runs on the DATA ENGINE alone, before the auto-enqueuer's
  first refresh — NOT gated behind the realtime/messaging dispatch prerequisites
  (else a realtime-less deployment reproduces the silent no-op).
- Seed-not-clobber provenance (mirrors sys_sharing_rule #2909): `sys_webhook`
  gains `managed_by` / `customized`. Declared webhooks re-seed as
  `managed_by: 'package'`; a row an admin created (`admin`) or edited
  (`customized`, stamped by a beforeUpdate hook) is never overwritten.
- showcase: require the `webhooks` + `realtime` capabilities (so the dispatcher
  actually mounts) and ship the demo webhook inactive (placeholder endpoint).
- Fix the stale `SysWebhookDelivery` import in the i18n extract config (dead
  since delivery moved to service-messaging).

Connector `webhooks` remain not-yet-enforced (#3197). Registering `webhook` as a
metadata type + GOVERNED liveness enrollment is a tracked follow-up.

Verified: 9 new unit tests (mapping / idempotency / seed-not-clobber / invalid /
end-to-end dispatch), red-proofed; and a real showcase boot — declared webhook
materializes into a sys_webhook row, same-DB reboot stays idempotent, and an
admin's customized edit survives redeploy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Building Building Preview, Comment Jul 25, 2026 3:07am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Jul 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-webhooks, @objectstack/spec.

104 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 packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via packages/plugins/plugin-webhooks, @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/permissions/authorization.mdx (via @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/plugin-webhooks, @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/plugin-webhooks, @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/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.

@os-zhuang
os-zhuang merged commit 69f1dfd into main Jul 25, 2026
16 of 17 checks passed
@os-zhuang
os-zhuang deleted the fix/3461-webhook-bridge branch July 25, 2026 03:21
xuyushun441-sys pushed a commit that referenced this pull request Jul 26, 2026
`sys_webhook_delivery` was removed from @objectstack/plugin-webhooks when
outbound delivery moved to @objectstack/service-messaging (`sys_http_delivery`,
ADR-0018 M3), but its translation blocks lingered in all four generated locale
bundles — loaded at runtime yet referenced by nothing.

- Removed the `sys_webhook_delivery` node from each *.objects.generated.ts
  bundle (en/zh-CN/ja-JP/es-ES); WebhooksTranslations now carries only
  sys_webhook.
- Corrected the stale ownership comment on SysWebhook that still named
  sys_webhook_delivery as a live sibling.

The dangling SysWebhookDelivery import in scripts/i18n-extract.config.ts was
fixed independently on main by #3489, so it is not part of this change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 26, 2026
#3492)

`sys_webhook_delivery` was removed from @objectstack/plugin-webhooks when
outbound delivery moved to @objectstack/service-messaging (`sys_http_delivery`,
ADR-0018 M3), but its translation blocks lingered in all four generated locale
bundles — loaded at runtime yet referenced by nothing.

- Removed the `sys_webhook_delivery` node from each *.objects.generated.ts
  bundle (en/zh-CN/ja-JP/es-ES); WebhooksTranslations now carries only
  sys_webhook.
- Corrected the stale ownership comment on SysWebhook that still named
  sys_webhook_delivery as a live sibling.

The dangling SysWebhookDelivery import in scripts/i18n-extract.config.ts was
fixed independently on main by #3489, so it is not part of this change.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
xuyushun441-sys pushed a commit that referenced this pull request Jul 27, 2026
…-dead props (#3490)

Second half of the Test Core lockstep the bound-class + ledger flip require. The
bridge (#3489) made `url` live, so the CLI compile lint's per-webhook author
warning must move OFF it onto the props that are STILL dead + misleading:
retryPolicy/body/payloadFields/includeSession now carry `authorWarn` (folded into
definition_json but never read — authored retry/body/field-projection/session do
nothing). `tags` stays unmarked (benign display). `lint-liveness-properties.test.ts`
asserts the new behavior: a webhook that authors only now-live props is silent,
and the showcase-shaped webhook warns exactly once — on its inert `retryPolicy`,
not on `url`. isActive test comment updated (it is live now, not "unmarked dead").

Verified: lint-liveness-properties.test.ts (17) + proof-registry.test.ts (23)
green; liveness gate still green (webhook 17 classified, proofs resolve).
os-zhuang added a commit that referenced this pull request Jul 27, 2026
…ation / Webhook (#3494) (#3516)

* feat(spec)!: prune still-dead aspirational config from Theme / Translation / Webhook (#3494)

Removes the authorable-but-never-consumed props confirmed dead by the
2026-06 liveness audit (follow-up to #1878/#1893; same enforce-or-remove
treatment as #2377/#3464):

- Theme: spacing, breakpoints, logo, density, wcagContrast, rtl,
  touchTarget, keyboardNavigation (+ SpacingSchema, BreakpointsSchema,
  DensityModeSchema, WcagContrastLevelSchema and their aliases/types) —
  the objectui theme engine never emitted or consumed them.
- Translation: fileOrganization, messageFormat, lazyLoad, cache
  (+ MessageFormatSchema, TranslationFileOrganizationSchema) — no ICU
  engine exists; interpolation is always simple substitution.
- Webhook: body, payloadFields, includeSession, authentication (non-HMAC;
  HMAC via secret stays), retryPolicy, tags + the entire inbound
  WebhookReceiverSchema — the delivery path sends a fixed envelope and
  the outbox owns its own retry schedule. Liveness ledger entries for the
  removed props are dropped (dead->live flip for materialized props stays
  with #3490).

Kept deliberately: supportedLocales (live pinyin-search reader), job
retryPolicy/timeout (being built in the follow-up PR), and all webhook
props materialized by the #3489 bridge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(cli): drop lint assertions for the pruned webhook authentication/retryPolicy props

The liveness compile lint no longer warns on authentication — the prop (and
its ledger entry) were removed outright, so the experimental-marker test case
is obsolete; the showcase-shaped fixture loses its dead retryPolicy key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
xuyushun441-sys pushed a commit that referenced this pull request Jul 27, 2026
Resolve conflicts in webhook.json + lint-liveness-properties.test.ts against
#3494 (feat(spec)!: prune still-dead aspirational config from Webhook), which
deleted body/payloadFields/includeSession/retryPolicy/tags/authentication from
WebhookSchema. With those gone and the #3489 bridge making the rest live, the
webhook ledger is 11 props ALL live — no dead surface, so no authorWarn carrier —
and the lint test asserts an authored webhook is silent (the old `url` carrier is
now live). proof-registry (23) / lint (14) / liveness gate (webhook 11 live) /
webhook-materialization dogfood proof all green post-merge.
os-zhuang added a commit that referenced this pull request Jul 27, 2026
…lization proof (#3490) (#3518)

* chore(spec): flip webhook liveness ledger dead→live + webhook-materialization proof (#3490)

Follow-up to #3489. The materializer bridge made stack-authored webhooks reach
the dispatcher, so the webhook liveness ledger (packages/spec/liveness/webhook.json)
was stale — it still classified all 16 authorable props `dead` from the pre-bridge
"nothing materializes an authored webhook" vantage point. Because `webhook` rides
the SPEC_ONLY_SCHEMAS gate path (walk-only), the liveness CI did NOT catch the
dead→live drift; this updates it by hand.

- Flip the props the materializer + dispatcher consume to `live` with evidence
  (materializer bootstrap-declared-webhooks.ts + dispatcher auto-enqueuer.ts):
  object/isActive/url/triggers/method/name/headers/secret/timeoutMs, plus
  display-only label/description. Drop the `url` authorWarn (authoring is live).
- Keep `dead`: body/payloadFields/includeSession/retryPolicy/tags — folded into
  definition_json by the bridge but parseRow never reads them (no consumer, the
  #1878 delivery-layer worklist). `authentication` stays experimental.
- Add a `webhook-materialization` ADR-0054 high-risk proof class (proof-registry.mts,
  bound to webhook.object) + its dogfood proof: bootStack with the messaging +
  webhook plugins but NO realtime, asserting the sys_webhook row materializes with
  object→object_name / isActive→active. Pins the #3461 integration seam (the bridge
  was first gated behind the realtime dispatch guard, silently materializing nothing).
- Refresh the stale "enforce-or-remove pending #3461" prose in check-liveness.mts
  and webhook.json's _note (the disconnect is closed by #3489).

Verified: liveness gate green (webhook 17 classified: live 11 / dead 5 / exp 1;
webhook-materialization proof resolves) and red-proofed (breaking the proof tag →
exit 1); the dogfood proof passes (logs confirm hasRealtime:false yet the row
materializes). Liveness-ledger + gate assets and the private dogfood package only —
no package release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(spec): sync proof-registry test to the webhook-materialization bound class (#3490)

The registry's BOUND_PROOF_PATHS expected-list and the real-proof-wiring
`ledgerFor` map must be updated in lockstep when a bound high-risk class is added
— add `webhook/object` and the `webhook` → webhook.json ledger path. The Test
Core failure on the first push was exactly this lockstep assertion doing its job.

(Validate Package Dependencies is red on OSV-Scanner audit hits for pre-existing
transitive deps — next / react-router / sharp / tar / js-yaml / brace-expansion —
none introduced here; that job is not a required check and is triggered only
because this PR touches pnpm-lock.yaml to add the dogfood workspace deps.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(spec,cli): redirect webhook authorWarn from now-live url to still-dead props (#3490)

Second half of the Test Core lockstep the bound-class + ledger flip require. The
bridge (#3489) made `url` live, so the CLI compile lint's per-webhook author
warning must move OFF it onto the props that are STILL dead + misleading:
retryPolicy/body/payloadFields/includeSession now carry `authorWarn` (folded into
definition_json but never read — authored retry/body/field-projection/session do
nothing). `tags` stays unmarked (benign display). `lint-liveness-properties.test.ts`
asserts the new behavior: a webhook that authors only now-live props is silent,
and the showcase-shaped webhook warns exactly once — on its inert `retryPolicy`,
not on `url`. isActive test comment updated (it is live now, not "unmarked dead").

Verified: lint-liveness-properties.test.ts (17) + proof-registry.test.ts (23)
green; liveness gate still green (webhook 17 classified, proofs resolve).

---------

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 27, 2026
…kupFilters hint (#1878) (#3677)

* docs(audits): recheck the §3 naming-drift list; fix the os doctor lookupFilters hint (#1878)

Re-verified all nine spec-key-vs-consumed-key drifts against current code in
both repos (including the os:check TypeScript blocks inside skills/*/SKILL.md,
which compile against the spec and are real consumers).

Six of nine were already resolved -- the list was stale, not the code:
referenceFilters (-> lookupFilters), maxRating (-> max), page visibility
(-> visibleWhen), dashboard title/label (dual-read + strict widget schema),
flow http, agent knowledge.topics (alias folded #1891), webhook keys (#3489);
action disabled was fixed this month (objectui#2863). skill
requiredPermissions was never a drift -- the original audit described prose
drift in the docs, not a spec-vs-runtime mismatch.

Still real: the coupled page pair type->pageType + label->title, which
mis-renders every non-record page today (fixed in the companion objectui PR),
and two snake-only max_length readers.

- Add 2026-07-naming-drift-recheck.md with per-item file:line evidence, the
  gate-coverage finding (bare z.object silently strips; the dashboard-widget
  .strict() + naming error map is the only mechanism that teaches), and the
  three items left as owner decisions (app badgeVariant/separator decayed when
  NavigationRenderer was replaced by UnifiedSidebar; skill.permissions has no
  gate; agent.knowledge is inert).
- Annotate README §3 with the recheck summary.
- Fix os doctor: the reference_filters rule pointed at the REMOVED
  referenceFilters key instead of the live lookupFilters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(audits): correct the app badge/separator finding — NavigationRenderer exists

My first pass claimed NavigationRenderer had been deleted; it has not. The
component (layout/src/NavigationRenderer.tsx, 1327 lines) is publicly exported
and SDUI-registered as `navigation-renderer`, and fully implements separator
(:906) + badge/badgeVariant (:983-985, :1024-1025). The error came from a
truncated grep (head -3 showed only CHANGELOG/ROADMAP hits).

Corrected classification: badge/badgeVariant/separator are SURFACE-CONDITIONAL,
not dead — they render through the SDUI component but not through
UnifiedSidebar, which is what actually renders app.navigation in the console.
Disposition flips from enforce-or-prune to port-the-three-branches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 2, 2026
…rage complete (objectstack-ai#4488) (objectstack-ai#4510)

Nine new ledgers (app, book, doc, email_template, job, mapping, seed,
translation, validation; ~150 verdicts, all verifiedAt-stamped), emptying
PENDING_GOVERNANCE: every registered metadata type is now governed.

- check-liveness.mts: unwrap() sees through z.preprocess pipes (takes the
  OUT side when IN is a transform) — translation's registered schema was
  unwalkable before this.
- CLI lint TYPE_COLLECTIONS registers the six new types carrying authorWarn
  entries, with pins in the test suite (21 tests).
- liveness/README.md: the count-table method is decided and recorded (mirrors
  check-liveness --json byStatus); all rows regenerated, the stale webhook
  row rewritten to the post-objectstack-ai#3489/objectstack-ai#3494 state.

Key findings, all recorded in ledger notes and tracked in objectstack-ai#4509:
- email_template: the ENTIRE authoring surface is disconnected from
  sendTemplate (false compliance on auth mail).
- app: areas[].visible / areas[].requiredPermissions are fail-open dead
  gates; homePageId dead; selector includeAll deliberately ignored.
- translation.validationMessages: read by nothing while objectstack-ai#3778's own
  migration table steers authors into it.
- job / validation: runtime-authoring doors disconnected from their
  execution points.


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

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 3, 2026
…emplate bridges, job and validation close their doors (objectstack-ai#4558)

* feat(email): materialize declared email_template metadata into sys_email_template (objectstack-ai#4509)

Authoring an `email_template` was a silent no-op. `sendTemplate` resolves
`(name, locale)` against sys_email_template ROWS, whose only writers were the
built-in auth templates plus a code-constructed `EmailServicePluginOptions.
templates` that no bootstrapper passed — while every door an author can use
(stack `emailTemplates:`, `*.email-template.ts`, Studio, PUT /meta) parked items
in a metadata store nothing read back. An admin could "fix" the password-reset
mail in Studio and users kept receiving the built-in copy: ADR-0078 false
compliance on AUTH mail. Webhook objectstack-ai#3461's shape, closed the same way (ADR-0049
enforce-or-remove, route: enforce).

`bootstrapDeclaredEmailTemplates` materializes declared items at boot, validating
each through EmailTemplateDefinitionSchema.parse() and projecting with
`mapTemplateToRow` — the SAME mapping the built-in seeder uses, extracted and
shared so the two doors cannot drift. Because email_template is
allowRuntimeCreate:true (unlike webhook), the plugin also subscribes to metadata
changes and re-materializes the changed item, so a Studio save takes effect
without a restart; a withdrawn template deactivates its rows rather than losing
them.

Three breaks sat on this path, and closing any two would still have shipped a
template that never sent:
- objectql never registered a manifest's `emailTemplates:` into the registry at
  all — the key was missing from the generic ingestion list, so the bridge's own
  source was empty.
- Built-in seeds left `managed_by` at the column's 'admin' default, masquerading
  as admin-authored; since the bridge refuses to overwrite admin rows, a built-in
  would permanently outrank the template an app declared. Built-ins now stamp
  'platform'.
- Nothing materialized declared metadata into rows.

Seed-not-clobber mirrors sys_webhook (objectstack-ai#3489): sys_email_template gains
managed_by/customized, declared rows re-seed as 'package', and an admin-created
or admin-edited row is never overwritten. Separate axis from `is_system`, which
keeps its meaning for built-ins.

Ledger email_template flips 13 dead → fully live, with an ADR-0054 proof bound on
`subject`: it boots a real stack, authors a template overriding a built-in auth
template, and asserts the AUTHORED wording reaches the transport.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5CYr5SDwe85gH2Jr5KSgu

* feat(spec)!: close the job runtime-create gate — a job is a code artifact (objectstack-ai#4509)

A `job` created at runtime could never be scheduled. `JobSchema.handler` names a
function in the compiled bundle's function table — the schema says so, and the
scheduler is built that way: AppPlugin sources jobs from `bundle.jobs` alone and
resolves handlers through `collectBundleFunctions(bundle)`. Yet the type was
registered allowRuntimeCreate:true (and allowOrgOverride:true), so a job authored
in Studio or via PUT /meta parsed, saved, reported success, and never ran.

Unlike the sibling disconnects in this batch, this one cannot be bridged: the
runtime writer does not have the bundle and cannot name a function inside it. The
missing piece is a handler-binding design, not an ingestion path. Per ADR-0049
enforce-or-remove the honest move is to close the door — both flags now false,
with a rationale block modeled on the `agent` entry stating what re-opening would
require.

`job` stays a first-class authorable type: `*.job.ts` and `defineStack({ jobs })`
are the supported doors and are fully enforced, so the kind still passes the
ADR-0088 admission test and stays registered.

Also restamps job.json's evidence lines, which pointed at app-plugin.ts:767-791
and had drifted ~25 lines — the gate resolves paths, not line numbers, which is
exactly the rot `verifiedAt` exists to catch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5CYr5SDwe85gH2Jr5KSgu

* chore(i18n): regenerate object bundles for the sys_email_template provenance columns (objectstack-ai#4509)

Follow-up to the materializer bridge: `managed_by` / `customized` on
sys_email_template are new translatable field labels, so the nine locale bundles
had drifted from the schema. `node scripts/check-i18n-bundles.mjs --write`
(merge mode — no existing translation overwritten).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5CYr5SDwe85gH2Jr5KSgu

* feat(spec)!: retire the standalone validation metadata kind (objectstack-ai#4509, ADR-0088)

A validation rule authored as its own artifact bound to nothing and gated no
write. ValidationRuleSchema carries no object-binding key — no `object`, no
`objectName` — and all six variants are strictObject, so an author could not
supply one either. No merge step existed; the only code expecting such a key was
a reference-tracker row scanning a field the schema would have stripped.
Meanwhile the engine evaluates exactly one shape: the object's own
`validations[]`, on insert and on every matched update row.

So a rule created through the standalone door (*.validation.ts, or Studio's
Validations list) parsed, saved, reported success, and intercepted nothing —
including a state_machine rule, which ADR-0020 routes through this same
vocabulary. An author could believe they had locked down record state
transitions and have changed nothing.

Under ADR-0088 the kind fails the admission test on its first clause: a rule has
no independent lifecycle, because it only means something against an object. And
unlike the sibling disconnects in this batch it could not be bridged into one —
the shape has nowhere to name its object. Enforce-or-remove picks ENFORCE where
the feature exists and only wiring is missing (email_template), REMOVE where the
shape cannot carry the feature.

The rule VOCABULARY is untouched: ValidationRuleSchema and all six variants are
unchanged and fully live, and the engine's evaluation path is not modified. The
ledger keeps governing the schema through the gate's SPEC_ONLY_SCHEMAS override
(alongside webhook and query) — an ungoverned live schema is how the next drift
would hide.

Removed: registry entry (with its file patterns), MetadataTypeSchema member,
metadata-core lockstep enum member, schema-map entry, create seed, Studio nav
item + hand-crafted form + its four locale strings, and the dangling
reference-tracker row. Stored standalone rows are left alone — never evaluated,
so nothing changes behaviorally.

Also restamps validation.json's engine evidence lines (drifted ~220 lines) and
adds the ADR-0088 addendum recording the retirement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5CYr5SDwe85gH2Jr5KSgu

* chore(liveness): record the nav action dispatch gap as closed (objectstack-ai#4509)

app.json's type note carried the one gap the AppSchema navigation walk found by
hand: an `action` item rendered and gated like any other while its click went to
an `onAction` prop no shipped shell passed. objectui @e8bec83 wires it — the
sidebar resolves the action name against metadata and dispatches through the
console action runtime, and a shell that still passes no handler now hides
action items instead of rendering them dead.

Re-pins the objectui evidence (the old pin @940ba24 predates the fix) and dates
the row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5CYr5SDwe85gH2Jr5KSgu

* fix(email): type the metadata slot lookup so the ratchet holds (objectstack-ai#4509)

CI's slot-lookup ratchet caught the bridge's `ctx.getService('metadata')`:
email-plugin.ts is grandfathered for its three existing erasures, and a new
untyped lookup pushed it to four. Pass the slot's contract type
(`IMetadataService`) as objectstack-ai#4251 asks — which also gives the subscribe/get calls a
real type instead of `any`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5CYr5SDwe85gH2Jr5KSgu

---------

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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P2] Webhook: the spec WebhookSchema authoring surface is disconnected from the sys_webhook dispatcher

1 participant