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

vercelBot commented Jul 25, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specBuildingBuildingPreview, CommentJul 25, 2026 3:07am

Request Review

@github-actionsgithub-actionsBot 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 mainJul 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

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

@os-zhuang