Skip to content

docs(plugin-webhooks): bootstrapDeclaredWebhooks names only the half it delivers, and a pin test holds the other half open - #19193

Merged
huangyiirene merged 2 commits into
mainfrom
claude/issue-18613-connector-nested-webhooks
Sep 19, 2026
Merged

huangyiirene merged 2 commits into
mainfrom
claude/issue-18613-connector-nested-webhooks

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes #18613

Clause-②: no

What was false, and what this changes

bootstrapDeclaredWebhooks opened with:

materialize stack/connector-declared webhooks into sys_webhook rows so the dispatcher can actually see them (closes #3461).

The connector half was false, and false in exactly the sentence a reader trusts when deciding whether a connector's nested webhooks[] reaches the dispatcher. This PR corrects the prose and pins the corrected claim as behaviour. It does not build the connector bridge — that is #3197's card.

Direction is ruled by packages/spec, which declares the connector surface unenforced in two places, so the docblock is the text out of step:

⛔ Zero packages/spec in this diff.

The premise, measured first-hand (not grepped)

The card's load-bearing premise is "a connector's nested webhooks[] never becomes a webhook metadata item". A token grep answers "does this string appear", which is the instrument that cannot tell prose from behaviour — the exact defect this card is about. So it was measured by running the real ObjectQL boot registration over a manifest declaring both shapes at once:

listItems('webhook')   = ["stack_declared_hook"]
listItems('connector') = ["acme_erp"]
connector.webhooks (still nested inside the connector doc) = ["connector_nested_hook"]
CONTROL  top-level webhook registered : true
SUBJECT  connector-nested registered  : false

Anti-vacuity control, same corpus and same spelling convention: the same manifest's top-level webhooks: entry IS registered. That is what would have made the zero not-zero; without it the reading would also pass on an engine that registered nothing, on a misspelled metadata type, or on a manifest the engine rejected.

Why the zero holds — the registrar census. Every registrar of the webhook metadata type keys on a TOP-LEVEL collection name, and none walks into a connectors: entry:

registrar keyed on
registerMetadataCollections (packages/objectql/src/engine.ts) METADATA_ARRAY_KEYS over the manifest / nested-plugin document itself
artifact loader collection map (packages/metadata/src/plugin.ts) webhooks: 'webhook'
shared spelling table (packages/spec/src/meta-spelling/manifest-collection-spelling.ts) PLURAL_TO_SINGULAR

readDeclared's two sources reduce to the same store: MetadataFacade.list(type) delegates to registry.listItems(canonicalMetadataServiceType(type)).

A1 and A2 from the dispatch also hold on origin/main @ eeaa882459: the docblock still carried the clause, and :141 is still the function's only source.

The #18582 liveness ledger row is the one that was RIGHT

packages/spec/liveness/connector.jsonprops.webhooks reads status: "dead", verifiedAt: 2026-09-17, with a note naming the same mechanism measured above. The ledger and the docblock disagreed because the docblock was wrong. ⇒ nothing to file about the ledger, and after this PR the two agree.

Prose alone would rot the same way, so the claim is pinned

New file bootstrap-declared-webhooks.connector-nested.test.ts asserts, against a real ObjectQL boot, that the connector-nested webhook is neither a webhook metadata item nor a sys_webhook row — with the top-level control asserted green in both legs.

Ablation (one-off, reverted — not a committed file). A scratch copy of the pin simulated the bridge by registering the connector-nested webhook as a webhook metadata item, exactly as a hoist inside registerMetadataCollections would produce it. Both legs went red on the SUBJECT while the CONTROL stayed green:

× registers the top-level collection and leaves the connector-nested one where it was authored
  AssertionError: expected [ 'stack_declared_hook', …(1) ] to not include 'connector_nested_hook'
× seeds `sys_webhook` from the top-level collection only — the connector half is not even skipped
  AssertionError: expected 2 to be 1
Test Files  1 failed (1) · Tests  2 failed (2)

Restore proof: the scratch file was removed and git status --porcelain / git diff HEAD both print nothing. Observed direction was RED, as predicted.

⚠️ Disclosed limit: the mutation was applied to the pin's own copy rather than inside packages/objectql/src/engine.ts, because a write to that path was refused by this container's permission classifier ("Modify Shared Resources") and that path is outside this card's declared file surface. So the ablation proves the pin fires on the observable a hoist would produce; it does not exercise a hoist branch written into the engine.

Also corrected: the test file's copy of the same false clause

bootstrap-declared-webhooks.test.ts's header carried the identical "stack/connector-declared" claim. Measured before editing: the word connector appeared in that file only in that sentence — every fixture seeds declared.webhook directly, so the suite never made the claim its header did.

Gates

Re-derived for the actual diff with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no hand-written path list), exit codes landed to a file before any pipe, then reconciled:

Run reconciliation — 54 derived, 54 run, 0 NOT-MEASURED, 0 UNRUN.
  • 50 exit 0 on the first pass; 51 now, with check:engine-double-contract repaired below.
  • pnpm check:engine-double-contract — exit 1 on the first run, a real finding on this PR's new fake, now exit 0. The remedy taken is the in-surface one: the fake's update() was removed, not stubbed-and-asserted. find always answers empty here, so the seeder always takes the INSERT branch and never dispatches an update; a fake that stops where the path stops has nothing to be looser than. ⛔ The gate's other offer — --write to scripts/engine-double-contract.pinned.json — was declined because that path is outside this card's declared file surface.
  • 3 × exit 3 = PREREQUISITE NOT MET, i.e. NOT MEASURED, not green: check:dual-build-cjs-loads, check:i18n, check:type-check-debt. All three read built output of the whole tree; a full pnpm build does not fit this container's foreground cap, so they are declared to CI.

Package legs, both exit 0 through scripts/pm/os-verify-lock.sh:

pnpm --filter @objectstack/plugin-webhooks test        Test Files 13 passed (13) · Tests 150 passed (150)
pnpm --filter @objectstack/plugin-webhooks typecheck   tsc --noEmit + tsconfig.scripts.json + check:test-typecheck → OK

Repo-wide lint was run in full rather than narrowed — pnpm lint (eslint . --no-inline-config) exit 0 at d75d57ae2, the final commit.

Changeset: skip-changeset, measured

The diff is comments plus one test file. @objectstack/plugin-webhooks's files[] is dist, README.md, CHANGELOG.md; after pnpm --filter @objectstack/plugin-webhooks build:

  • SUBJECT — every distinctive phrase of the new prose: 0 hits across dist/, README.md, CHANGELOG.md. The new test file produces no dist/ artifact.
  • POSITIVE CONTROL — bootstrapDeclaredWebhooks, a symbol that IS published: 8 files in dist/. The corpus and the grep are live.

⇒ nothing this PR touches is shipped. ⛔ No label was written by this run — label writes belong to the PM seat on this dispatch, which is where the skip-changeset application is requested.

Acceptance notes

Observations made in passing, ⛔ not filed and not in this PR:

  • noted, not filed: WebhookConfigSchema extends WebhookSchema, so a connector's nested entries and a stack's top-level ones are near-identical documents in two different places with opposite runtime fates. Not a defect under the three filing classes — the spec says so out loud in both files. Successor: whoever takes Audit: several event/subscription/connector enums are schema-only (declared, no runtime consumer) #3197.
  • noted, not filed: the harness-injected co-author trailer spells a model name, which check:commit-card-trailers refuses at pre-push. Rewritten to the model-free pair before pushing; no history was rewritten because nothing was published. Successor: none — it is a per-run collision, reported to the PM seat rather than filed.

Generated by Claude Code

…elivers, and a pin test holds the other half open

`bootstrapDeclaredWebhooks` opened with "materialize stack/connector-declared
`webhooks` into `sys_webhook` rows so the dispatcher can actually see them".
The connector half was false, and false in the one sentence a reader trusts
when deciding whether a connector's nested `webhooks[]` reaches the dispatcher.

Measured against the real registration path, not grepped: `ObjectQL.registerApp`
over a manifest declaring BOTH shapes registers `webhook` items for the
top-level `webhooks:` collection only. The connector's nested `webhooks[]` stays
inside the connector document — every registrar of the `webhook` type
(`registerMetadataCollections`'s `METADATA_ARRAY_KEYS`, the artifact loader's
collection map, the shared `PLURAL_TO_SINGULAR` spelling table) is keyed on a
TOP-LEVEL collection name and none walks into a `connectors:` entry.

The repair is the prose, not the code: `packages/spec` declares the connector
surface unenforced twice (`automation/webhook.zod.ts`, and `WebhookConfigSchema`
in `integration/connector.zod.ts`), so the docblock is the text out of step.

A corrected sentence is still prose, so the claim it makes is now asserted by
`bootstrap-declared-webhooks.connector-nested.test.ts` against a real ObjectQL
boot, with an anti-vacuity control in the same corpus: a top-level `webhooks:`
entry that IS registered and IS seeded.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF
…'s path stops

`check:engine-double-contract` pinned the new fake's `update()`: a double
looser than `ObjectQL.update` is how a dead route ships with a green suite.
The verb is not stubbed-and-asserted but removed — `find` always answers empty
in this file, so the seeder always takes the INSERT branch and never dispatches
an update. A fake that stops where the path stops has nothing to be looser
than, and a future change that DID reach the update branch now fails loudly
here instead of passing against a permissive stub.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/plugins/plugin-webhooks/src/bootstrap-declared-webhooks.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-webhooks/src/bootstrap-declared-webhooks.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 82b32258521f6ceae6573d6cf995015250e81254packageMentionDocs.

@huangyiirene huangyiirene added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 19, 2026 — with Claude
@huangyiirene
huangyiirene marked this pull request as ready for review September 19, 2026 10:20
@huangyiirene
huangyiirene added this pull request to the merge queue Sep 19, 2026
Merged via the queue into main with commit 351a161 Sep 19, 2026
39 of 40 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-18613-connector-nested-webhooks branch September 19, 2026 10:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants