Skip to content

fix(objectql,spec)!: a hook's handler name resolves inside the hook's own package only (#21604) - #21653

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-21604-hook-handler-package-scope
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-21604-hook-handler-package-scope

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21604
Clause-②: yes (narrowing)

Executes the maintainer's ruling on #21604 (comment 5974477722, letter B, 「同意」 2026-10-03T23:11Z): a hook's handler name resolves inside the hook's own package only. The functions the package's own runtime module registers keep resolving; a name the package does not hold is refused at registration, with a refusal that names it; HookSchema.handler's declaration changes in the same PR. #21585's landed install-local refusal (#21615, 045b946256) is untouched: no file of packages/runtime or packages/cloud-connection source changes here.

Census first (the ruling's first step): zero dependents

Every composition that could rely on cross-package resolution by name, read before any refusal was written:

Composition Tree What was read Dependents
objectstack examples/** objectstack 15fe567c9c string handler: values, registerFunction calls, functions: declarations, composeStacks, each app's hook forms 0. The only string handler is a job's (app-showcase sweepProjectHealth), which the job half resolves against its own bundle. app-showcase's 5 hooks all carry body; app-crm and app-todo each have 1 inline-function hook, which os build lowers to the hook's own name inside that app's own runtime module (same owner). app-multi-package and embed-objectql declare no hooks or functions.
this repository's --artifact runtime modules objectstack 15fe567c9c tracked objectstack-runtime*.mjs; tracked non-TS files naming runtimeModule 0 committed. A runtime module is a build output of an app's own config.
hotcrm f24c196588 the same greps 0. No string handler:, no registerFunction. Its 19 hook files are inline functions inside one composeStacks app (one owner), each lowered to its own name.
objectos 7612ffebd1 the same greps 0. No hit for any of them.
cloud none NOT MEASURED. Unreachable from this session: a shallow clone has no credentials, a REST read answers 403 "not enabled for this session", and add_repo answers no access.

No stop condition fired: no real dependent was found, and owner-scoped resolution needed no new authorable spelling (see boundary flag 9).

What changed, and where

  • packages/objectql/src/hook-binder.ts: resolveHandler resolves a string handler against the functions handed to the bind (the package's functions, which an --artifact runtime module supplies), then against the engine entry of that name only if the entry's packageId equals the bind's packageId (ownPackageFunction, reading the owner through the existing resolveFunctionEntry). A string handler that resolves to neither is refused at registration: an Error carrying code: 'INVALID_REFERENCE', status: 400, hook, handler and packageId, recorded on BindHooksResult.errors[] (which gains optional code and status), logged at error with the Error in the logger's error slot, and thrown under strict. The hook is not bound.
  • packages/objectql/src/engine.ts: doc comments only (the registry and registerFunction). The lookup itself is unchanged: the entry already carried its owner.
  • packages/spec/src/data/hook.zod.ts: HookSchema.handler's TSDoc stops declaring the engine-wide fallback ("anything engine.registerFunction(name, fn) added") and states the own-package rule, the refusal, and the route for a runtime-authored hook. The schema and its .describe() are unchanged, so no generated artifact moves (check:generated: all 15 up to date).
  • The landing matches the dispatch's expected surface; no producer elsewhere needed the fix.

① The accept set, before and after

A hook whose handler is a function name and which has no body (a hook with a body binds exactly as before, body first):

Door Before After
Boot of a code package (AppPlugin, a defineStack config, os start --artifact) its own functions (runtime module included), then any function any package registered its own functions (runtime module included), then functions its own package registered earlier; another app's function is refused
Install-local (os package install) handler-only hooks already refused at install and withheld on rehydrate unchanged
Metadata door (PUT /api/v1/meta/hook/NAME, bound under owner metadata-service) any function any package registered none: the owner registers no functions, so every handler-only authored hook is refused when the door binds it
Multi-app composition (several apps on one engine) app Y's hook could bind app X's function refused
Direct bindHooksToEngine with no packageId any engine function only the functions handed to that bind
A name no package holds (typo) skipped, warn, reason unknown function 'NAME' refused: same envelope as above, error

② Semver

minor for @objectstack/objectql and @objectstack/spec, BREAKING, ! in the title, Clause-②: yes (narrowing), under the launch-window convention for narrowings of an accept set. The changeset (.changeset/21604-hook-handler-package-scope.md) carries the ADR-0087 disposition not-required (no-migration-prescription), written from the census facts above: no authorable key, spelling, export of a published release or stored shape moves, so objectstack migrate meta has nothing to rewrite. (The marker sits in the changeset as the gate's comment-form marker; check-adr-0087-registration --base origin/main reads it green.)

③ Boundary flags

  1. Log level and text. An unresolved string handler used to log warn with reason unknown function 'NAME'; it now logs error with the coded refusal's sentence, beside the binder's other coded registration refusal (the stored-metadata body boundary, also logged at error by this binder). The ruling asks for a loud refusal at registration.
  2. Result type. BindHooksResult.errors[] gains optional code and status (additive output). HOOK_HANDLER_NOT_IN_PACKAGE_CODE and HOOK_HANDLER_NOT_IN_PACKAGE_STATUS are exported from hook-binder.ts only; neither index.ts nor core.ts re-exports them, so the package's public entry gains no symbol.
  3. The envelope (H4). No coded refusal existed for this condition (the binder's unresolved branch carried a bare reason string). INVALID_REFERENCE / 400 is the standard catalog's member for a reference that does not resolve where it must. The ledger's admission rule sends a generic condition to the standard catalog, so no code is registered; plugin-auth already answers INVALID_REFERENCE for both a missing and a cross-scope reference. The sibling registration refusal's PERMISSION_DENIED / 403 was not reused: that refusal is about a permission on a table; this one is about a name that does not resolve, and a typo is no permission question.
  4. strict (OBJECTQL_STRICT_HOOKS=1) throws the refusal. A strict runtime whose hook bound across packages now fails that bind, exactly as it already failed an unknown name.
  5. The metadata door (H2). A runtime-authored hook is bound under the synthetic owner metadata-service, which registers no function, so a handler-only authored hook is always refused at bind. The save itself still answers as before: the pin records 200 for that PUT. That is the same posture as the stored-metadata body boundary, which refuses at bind and leaves the save door's answer unchanged. A sys_metadata hook row stamped with a package_id (the Studio package authoring workspace) is still bound under metadata-service, so it does not reach that package's runtime-module functions; before, it reached every function. Measured pull: zero string handlers anywhere in the census. Resolving by a row's own package_id would let any metadata author claim a package's code, which is the channel the ruling closes.
  6. A bind that names no package (H2). With no packageId, a hook resolves only the functions handed to that bind; an engine entry registered without an owner is resolvable by no hook. Measured: every first-party door stamps an owner (app:APPID, metadata-service, sys:audit). After a platform boot (ObjectQL, sqlite-wasm, Hono, one app, platform objects, auth, security, sharing, REST, dispatcher), the engine's function registry holds exactly one entry, the app's own (h3_fn, owner app:com.h3.probe). I read "a name the package does not hold is refused" as covering a bind with no package; the reviewer may weigh that reading.
  7. The platform's own functions (H3). None reach the engine registry. The formula stdlib's registerFunction registers into a cel-js Environment, not the engine (packages/formula/src/stdlib.ts). So no platform function's resolution changes; H3's "formula stdlib" leg is falsified.
  8. One artifact, one owner. A multi-package artifact (packages[], composeStacks) is bound under one owner app:APPID, with its functions flattened, so a hook in one composed package can still name a sibling package's function inside the same artifact. Census: app-multi-package declares no hooks or functions, and hotcrm's composition lowers each hook to its own name. Scoping inside one artifact would need per-package attribution in the bundle collectors, beyond "only as far as owner-scoped resolution needs it".
  9. No new spelling. "Cross-package reuse must name the owning package explicitly" is met by an existing spelling: import the function from the package that owns it and declare it in your own functions. The refusal and the changeset prescribe exactly that, and no pkg/fn or { package, name } form was minted.
  10. Install-local is untouched. Its CLI integration pin (packages/cli/test/package-install-local-hooks.integration.test.ts, whose host hook names its own runtime-module function) is in the CLI integration tier and is declared to CI; this diff touches no CLI file.

Pins

  • packages/objectql/src/hook-binder-package-scope.test.ts. Refusals, each asserting code INVALID_REFERENCE, status 400 and that the hook did not bind (the other package's function never runs): another package's function; the same under strict (thrown, with hook, handler and packageId); a name nobody holds; the metadata-door owner, read off the engine logger's error call; a bind with no package naming an unowned entry. Controls: a function handed to the hook's own bind; a function its own package registered in an earlier bind.
  • packages/runtime/src/hook-handler-package-scope.pin.test.ts, a composed kernel. ① Multi-app composition: app Y's hook naming app X's x_stamp is refused, and Y's insert is not stamped by X. ② Metadata door: PUT /api/v1/meta/hook/scope_authored_cross naming x_stamp is refused when the door binds it, while an authored body hook (the re-sync witness) fires. Controls: X's own hook binds and runs; app Z, loaded through loadArtifactBundle from an artifact whose runtime module exports z_stamp, binds and runs.
  • Re-triaged fixtures in hook-binder.test.ts: the two cases that pinned the text unknown function (the refused branch) now assert the envelope.

Reverse verification (committed first, at 1eb671bac6)

The owner check was ablated through scripts/ablation-replace.mjs in WRAP mode, with an absolute-path git checkout HEAD -- PATH trap. The ablated ownPackageFunction resolves any entry by name, which is the old fallback. On-disk proof: anchor 1 → 0, replacement 0 → 1, blob 9301e0130c → 49bf4c96cc. pnpm --filter @objectstack/objectql build exited 0, and ablation-dist-preflight found the marker in all 4 JS files the runtime suite consumes.

  • objectql pins: 4 red (another package's function, strict, metadata-door owner, unowned bind) and 30 green (the typo refusal, both controls, the existing binder suite).
  • runtime composed pin: 2 red, with the defect itself as the reason: Y's insert came back |x-fn, and the authored row came back |x-fn|authored-body|x-fn. 2 controls green.
  • Restore: blob back to the HEAD blob 9301e0130c, git diff HEAD empty, whole-tree git status --porcelain empty. After the rebuild, the marker is absent from all 14 dist/ files and the pins are green again (34/34 and 4/4).
  • A first ablation run read the same red and green split, but its DTS step failed on the then-unused packageId parameter (the JS bundles still carried the marker). It was rerun with void packageId; so the build leg exits 0, and the figures above are from that clean run.

Tests

Suites at 1eb671bac6; the later merges of origin/main (b43c6fe76f, 308ae946b9) bring only service-analytics and CLI files, with no overlap. Build order: turbo build --filter='@objectstack/runtime^...', then --filter='@objectstack/dogfood^...' --filter=@objectstack/rest --filter=@objectstack/service-automation, after the objectql change.

  • @objectstack/objectql: local project 370 files / 7441 passed; repo 1 / 5 passed; typecheck green (test layer within its pinned debt).
  • @objectstack/runtime (reads objectql's dist/): local 319 files / 4534 passed, 19 skipped; repo 3 / 751 passed; typecheck green.
  • @objectstack/rest: local 260 files / 4897 passed, 326 skipped; repo 5 / 177 passed, 1 skipped.
  • @objectstack/service-automation: 168 files / 2078 passed.
  • dogfood hook files (hook-error-format, hook-refusal-user-facing-marking, hook-runas-fls, webhook-materialization): 4 files / 13 passed.
  • @objectstack/spec: check:generated, all 15 artifacts up to date against a dist/ whose declaration stamp matches.

Direction: these are downstream consumers of objectql (runtime, rest, service-automation, dogfood); the spec edit is TSDoc only.

Gates (at 308ae946b9)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, derived 91 commands. That is the dispatch list plus check-empty-changeset (both), release-rehearsal-clone --self-test, release-pending-publish --self-test, check:engine-double-contract, check:objectql-double-limit, check:objectui-changeset, check:pm-changeset-deadline-census, check:query-options-erasure, check:stack-collection-maps, check:swallow-census-controls, check:type-check-coverage, check:type-check-debt and check:where-matcher. All 91 ran, each exit code captured before any pipe, and all 91 exited 0. --ran reconciliation: 91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN. On the first pass, check:dual-build-cjs-loads answered PREREQUISITE NOT MET: 8 packages unrelated to this diff had no dist/ in this worktree. Those were built, and it measured green.

Lint, narrowed and proven: eslint --no-inline-config --format json over the 6 touched TS files reports 6 files linted, 0 errors and 0 warnings. That covers every TS file in the diff under the config's **/*.ts and packages/** globs. eslint.config.mjs never enables type-aware linting (no parserOptions.project, no typed rules), so the diff cannot move any untouched file's verdict. The repo-wide pnpm lint is CI's.

NOT MEASURED

  • The cloud census: unreachable, as above.
  • CI-only families the derivation names, which have no local invocation: Test Core shards, Dogfood Regression Gate, Dogfood Verify CLI, Build Core, Temporal Conformance, and the workspace type-check lanes.
  • The CLI integration tier: declared to CI.
  • check:objectui-pin-citations: its self-test's live objectui round trip was skipped, because there is no objectui checkout here; the gate itself passed.

Acceptance notes (observed, not filed)

  • Action.target and a flow script node's config.function still resolve through the engine's function registry by bare name (service-automation bridges objectql.resolveFunction). The ruling covers a hook's handler only. This is the same family on other surfaces, recorded from a code-read with no measured reach.
  • The registry stays keyed by bare name: two packages registering one name leave the later one's entry. A hook bound in the same call resolves its own bundle first, so boot binding is unaffected.

Generated by Claude Code

claude added 5 commits October 3, 2026 23:46
…package only

The binder resolved a string `handler` against the bundle's functions and
then against the engine-wide function registry, keyed by bare name, so a
hook could bind to a function another package registered and run that
package's code on its own events. Resolution now stays inside the hook's
own package: the functions handed to its bind (the package's `functions`,
its runtime module's among them), then the entries the same `packageId`
registered. A name the package does not hold is refused at registration
with the ADR-0112 envelope (INVALID_REFERENCE, 400), recorded on the
bind result and logged at error; fatal under strict. HookSchema.handler's
doc stops declaring the engine-wide fallback.

Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
…p composition and the metadata door

Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
… (minor, Clause-② yes)

Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/spec, touching 20 documentable anchor(s).

12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via getBookTree (sdk, the bare tail of client method meta.getBookTree, bound to GET /api/v1/meta/book/:name/tree), meta.getBookTree (sdk, the route ledger binds it to GET /api/v1/meta/book/:name/tree, selected by route anchor /book/:name/tree), meta.publishItem (sdk, the route ledger binds it to POST /api/v1/meta/:type/:name/publish, selected by route anchor /:type/:name/publish), publishItem (sdk, the bare tail of client method meta.publishItem, bound to POST /api/v1/meta/:type/:name/publish))
  • content/docs/api/error-catalog.mdx (via INVALID_REFERENCE (literal, a string literal in HOOK_HANDLER_NOT_IN_PACKAGE_CODE))
  • content/docs/automation/flows.mdx (via my_fn (literal, a string literal in a comment in HookSchema))
  • content/docs/kernel/contracts/metadata-service.mdx (via packageId (symbol, a field of type HookRegistrationRefusal), /:type/:name/publish (route, bridged from symbol packageId — its route source's handler names it), /api/v1/packages/:packageId (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/permissions/authorization.mdx (via packageId (symbol, a field of type HookRegistrationRefusal))
  • content/docs/permissions/capabilities.mdx (via packageId (symbol, a field of type HookRegistrationRefusal))
  • content/docs/permissions/permission-sets.mdx (via packageId (symbol, a field of type HookRegistrationRefusal))
  • content/docs/protocol/kernel/error-handling.mdx (via /:type/:name/publish (route, bridged from symbol packageId — its route source's handler names it), /api/v1/packages/:packageId (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/protocol/kernel/plugin-spec.mdx (via packageId (symbol, a field of type HookRegistrationRefusal))
  • content/docs/ui/apps.mdx (via /book/:name/tree (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/ui/doc-pages.mdx (via packageId (symbol, a field of type HookRegistrationRefusal))
  • content/docs/ui/setup-app.mdx (via packageId (symbol, a field of type HookRegistrationRefusal))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx (via /book/:name/tree (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/releases/v17/17-0.mdx (via HookSchema (symbol, a top-level const), /:type/:name/publish (route, bridged from symbol packageId — its route source's handler names it), /api/v1/packages/:packageId (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/releases/v17/17-1.mdx (via /:type/:name/publish (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/releases/v17/17-2.mdx (via /:type/:name/publish (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/releases/v17/17-4.mdx (via /:type/:name/publish (route, bridged from symbol packageId — its route source's handler names it), /api/v1/packages/:packageId (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/releases/v17/17-5.mdx (via /book/:name/tree (route, bridged from symbol packageId — its route source's handler names it))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 71 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 139 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 eea82af67779f504bd5d53fccda3151066cdeaf6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 55ac8c38061b1d8844063c3cfdf506725bd02b78 — the merge of head b8c4e5edc4ec9b4ac87bdea3608d60a8d07c7bdf into base eea82af67779f504bd5d53fccda3151066cdeaf6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 55ac8c38061b1d8844063c3cfdf506725bd02b78 && git checkout 55ac8c38061b1d8844063c3cfdf506725bd02b78
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eea82af67779f504bd5d53fccda3151066cdeaf6 b8c4e5edc4ec9b4ac87bdea3608d60a8d07c7bdf && git checkout -B drift-repro eea82af67779f504bd5d53fccda3151066cdeaf6 && git merge --no-ff b8c4e5edc4ec9b4ac87bdea3608d60a8d07c7bdf

node scripts/docs-audit/affected-docs.mjs --json eea82af67779f504bd5d53fccda3151066cdeaf6

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs eea82af67779f504bd5d53fccda3151066cdeaf6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT (seat's review) — PR #21653 at head 308ae946b9; contract review owed before it enqueues

domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · read at 2026-10-04T01:04Z. The os-dev report is on #21604. Judged against GitHub and the branch, not against the report.

  • Shape: draft, base main, assignee os-project-manager.
    • The title carries !.
    • The first lines are Fixes #21604 and Clause-②: yes (narrowing), the ruling's release parameter.
    • The closing-keyword scan finds #21604 only.
  • Scope: 7 files, +606/-23. check-governed-merges reports NOT governed.
    • hook-binder.ts carries the change.
    • engine.ts has doc comments only.
    • hook.zod.ts changes the HookSchema.handler doc only; it is the declared cross-lane path (5974628902).
    • Two new pin files, one adjusted test, and the changeset.
  • The diff, read:
    • resolveHandler tries the bind's own functions first. Then ownPackageFunction takes an engine entry only when the entry's packageId equals the bind's packageId, read through the existing resolveFunctionEntry. A bind with no packageId reaches no engine entry. The bare-name fallback to engine.resolveFunction is gone.
    • When a body-less string handler does not resolve, the bind does not register the hook. It records a coded refusal on BindHooksResult.errors (INVALID_REFERENCE / 400), logs at error, and throws under strict.
    • INVALID_REFERENCE is a standard-catalog member, so the error-code ledger is untouched.
  • Public surface:
    • @objectstack/objectql's entry re-exports bindHooksToEngine and the types BindHooksOptions and BindHooksResult (index.ts:369-370). BindHooksResult.errors[] gains optional code and status: a widening of an exported type beside the narrowing of the accept set.
    • The new HOOK_HANDLER_NOT_IN_PACKAGE_* constants are not re-exported from the entry.
    • So the ruled yes (narrowing) reads as the one reader defines it (clause2-line.mjs:93): one surface widens and another narrows.
  • Census (the ruling's first step), checked:
    • 0 dependents in examples/**, where the only string handler is a job's, resolved in its own bundle.
    • 0 in this repository's committed --artifact modules.
    • 0 in hotcrm f24c196588 and objectos 7612ffebd1, both read-only clones, deleted after.
    • cloud is NOT MEASURED: it is unreachable from this session. add_repo (read) answered no access, and the seat's own list_repos does not list it.
    • No dependent was found, so the ruling's stop condition did not fire. The cloud gap goes to the maintainer in the seat's round report.
  • Open question (the hook's own package on the metadata door and on an owner-less bind) — the seat answers A, as implemented.
    • The card names the metadata-authored door as one of the cross-package doors this ruling closes.
    • A runtime-authored hook ships no code package. So under the ruling it holds no functions and gives its body.
    • B would turn a stamped package_id into a key to that package's code, which is the reach the ruling removes.
    • C keeps an unowned channel open.
  • H3 falsified, accepted: the formula stdlib registers into a cel-js Environment, not the engine. Measured after a full platform boot, the engine registry holds only the app's own entry, so no platform function a hook could name is lost.
  • Changeset, checked sentence by sentence:
  • Evidence:
    • Pins: 5 refusals asserting code, status and not bound, plus controls. The composed-kernel pin covers the multi-app refusal, the metadata door through PUT /api/v1/meta/hook, own functions, and an --artifact runtime module.
    • Ablation of the owner check, with a dist preflight: exactly the 4 objectql and 2 runtime refusal pins red, controls green, and the restore proved by blob equality.
    • Downstream suites (objectql, runtime, rest, service-automation and the dogfood hook files) and typecheck are green.
    • dispatch-gates --ran reconciles 91 of 91.
  • CI on 308ae946, at this read: 16 check runs are in progress.

Before it enqueues: an at-tier contract review is owed. The ruling requires it, and both legs hit: Clause-②: yes and packages/spec/src/**. needs:contract-review is hung on this PR in this act. The PR enqueues only with a same-head PASS on record and every check green.

Out-of-scope:

  • Filed by the seat: the metadata save door answers 200 to a body-less handler hook that can never bind (class c).
  • Noted, not filed (code-read only, no measured reach):
    • Action.target and a flow script node's config.function still resolve by bare name;
    • the registry stays keyed by bare name, so the later of two same-named registrations wins.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI red on 308ae946b9, this PR's own: "Lint & Repo Gates", step 119, "Documented HTTP status matches the status the runtime emits" (pnpm check:error-status-conformance). domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · 2026-10-04T01:12Z.

  • Reproduced on the head: ✗ INVALID_REFERENCE: baselined as unpinned, but a producer now declares its status — ratchet the baseline down with --update.
    • This PR adds the first INVALID_REFERENCE producer the deriver can read (400).
    • The documented status matches, so the only finding is the baseline ratchet.
  • The fix, in flight as patch round 1 to the same dev: node scripts/check-error-status-conformance.mjs --update on 308ae946b9, committing only scripts/error-status-unpinned-baseline.json, with no merge of main.
    • It is a pure regeneration, so the contract review being rendered on 308ae946b9 carries forward by a Regen-provenance: line, re-testable on the two committed trees.

Generated by Claude Code

…LID_REFERENCE now has a producer that declares its status

Regenerated with `node scripts/check-error-status-conformance.mjs --update`
and nothing else.

Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 308ae946b95c8637a1ccf97b362336c38edfb6c7
Local-runs: none

Inputs read: card #21604's body and its three comments (ruling 5974477722, claim 5974619950, os-dev-report 5975213073); PR #21653's body and file list (7 files, +606 / −23); the net diff of the head against origin/main (merge-base 759dbe9); the check-runs on the head, polled at 2026-10-04T01:05:57Z, 01:11:59Z and 01:16:50Z; and, to attribute one red, the gate sources at the head (scripts/check-error-status-conformance.mjs, .github/workflows/lint.yml, .github/workflows/ci.yml). No PR comment was read. Nothing was checked out, built or run.

Gate verdicts on this head (final poll 01:16:50Z). Required contexts: Lint & Repo Gates failure (01:10:41Z); TypeScript Type Check success; Build Core success; Dogfood Regression Gate success (all three shards); Temporal Conformance (live PG + MySQL) success; Governed Surface Queue Guard success; Test Core failure (01:16:17Z) — shards 1/6 and 2/6 read cancelled at 01:15:41Z–01:15:48Z with 3/6–6/6 green; the whole CI run 37166593538 is cancelled, displaced by CI run 37167413950 on the branch's newer head b8c4e5e (started 01:14:41Z) under the ci-CI-pull_request-21653 concurrency group. That second red is a cancellation by a later push, not a test result, and is not evidence against the diff. Advisory contexts: both Check Changeset runs success (00:59:56Z and 01:06:08Z; this job carries check-adr-0087-registration and check-changeset-no-major), Dogfood Verify CLI, Spec property liveness, Type Check · source gates (spec check:docs) and Type Check · consumer gates (check:api-surface) all success.

The one red that belongs to this diff. Lint & Repo Gates failed at step 119 of 199, "Documented HTTP status matches the status the runtime emits" (pnpm check:error-status-conformance); steps 1–118 passed and 76 later gate steps never ran — among them the dispatcher error-code vocabulary guard, the error-code provenance guard, the durability-degradation log-level guard, the changeset-family gate self-tests and the paired kernel-hook pin gate — so that tail is unmeasured on this head. The job log is not readable from this session (the log blob is refused through the proxy), so the cause is attributed from the gate's source against the diff: hookHandlerNotInPackageRefusal stamps err.code = HOOK_HANDLER_NOT_IN_PACKAGE_CODE; err.status = HOOK_HANDLER_NOT_IN_PACKAGE_STATUS;, which is the gate's R6 assignment-pair producer shape with both constants resolved by its index, so INVALID_REFERENCE gains a derived producer at 400 while scripts/error-status-unpinned-baseline.json at this head still lists it as unpinned; the gate's nowPinned rule then refuses: "INVALID_REFERENCE: baselined as unpinned, but a producer now declares its status — ratchet the baseline down with --update". The branch's next commit b8c4e5e ("ratchet the unpinned error-status baseline down", one line removed from that file and nothing else) is exactly that remedy, which confirms the reading. The two main commits CI's merge ref adds over this head (f40bb32, 6ec54f0) move no error-status path, so this is not a joint breakage.

① Derived judgments

Doors the binder serves at this head (every bindHooks / bindHooksToEngine call site outside tests): bindAppArtifactHandlers (packages/runtime/src/app-artifact-handlers.ts:227 — owner app:APPID, the bundle's functions handed; this is the AppPlugin boot path, --artifact, and install-local); the objectql plugin's two metadata-service binds (boot loadMetadataFromService at plugin.ts:2895 and resyncAuthoredHooks at plugin.ts:2302 — no functions handed, artifact-shipped hooks filtered out on both); and the built-in audit hooks under sys:audit (plugin.ts:1301, inline function handlers). No first-party door binds without an owner.

  1. Boot / artifact / install-local (app:APPID): a string handler resolves against the functions handed to the bind (runtime module included), then against an engine entry of that name only when entry.packageId equals the bind's owner; another package's entry is refused. Right — ruling B's letter, and the composed pin (apps X, Y, Z) exercises each arm.
  2. Metadata door (metadata-service): every handler-only hook, including a sys_metadata row stamped with a package_id, is refused at bind with the envelope; a body hook binds as before. Right under the ruling (the owner that door binds under holds no code; ③ 5). Not double-refused: isArtifactShippedHook keeps code-package hooks out of both metadata-service binds, so an app's own string-handler hooks are not re-bound and refused there.
  3. sys:audit: inline functions, untouched. Right.
  4. Direct bindHooksToEngine with no packageId (a public export; no first-party caller): only the handed functions resolve; an unowned engine entry is unreachable. Right, and the changeset states it (③ 6).
  5. A name nobody holds: was skip + warn "unknown function", now the same coded refusal at error; strict throws as it threw before. Right — "refused at registration, with the name and a prescription" is the ruling's wording, and the message names the hook, the function, the package and the fix.
  6. body hooks: body-first ordering unchanged; a body hook with no runner keeps its old reason string. Right.
  7. Deliberately not narrowed: Action.target and a flow script node's config.function still resolve engine-wide by bare name (service-automation bridges resolveFunction). The ruling covers Hook.handler only; the dev's acceptance note names the family. Right to leave; carried for triage below.
  8. Composition inside ONE artifact (composeStacks, packages[]): one owner app:APPID, flattened functions, so a hook may still name a sibling stack's function inside the same artifact; STACK_COMPOSE_FUNCTION_CONFLICT already refuses same-name collisions at compose time. Right reading of "own package" as the install unit; finer attribution would need the bundle collectors, beyond the claim's declared surface.

Public surface:

  1. BindHooksResult.errors[] elements gain optional code and status; the type is exported from both entries (index.ts:370, core.ts:82), so this is an additive widening of a published output type. Right, and it is the yes half of the declaration.
  2. HOOK_HANDLER_NOT_IN_PACKAGE_CODE / HOOK_HANDLER_NOT_IN_PACKAGE_STATUS are module exports of hook-binder.ts only; index.ts and core.ts re-export bindHooksToEngine and the two types by name, with no star re-export. Right: no new public symbol.
  3. HookSchema.handler TSDoc: the engine-wide fallback sentence is gone (the ruling's requirement); the string form is relabelled "(build artifact)" with "/ Studio" dropped; a runtime-authored hook is told to use body. Schema and .describe() unchanged, so no generated artifact moves (both spec gate jobs green). Right, with one escalation on the Studio half (③ 5b).
  4. The envelope INVALID_REFERENCE / 400: a StandardErrorCode member of the 400 validation class; the cited precedent is real (plugin-auth's admin-set-user-manager.ts answers deny(400, 'INVALID_REFERENCE', …) for manager_not_found and cross_organization); no ledger code covers this condition (HOOK_UNSCOPED_DATA_ACCESS and STACK_COMPOSE_FUNCTION_CONFLICT are other conditions). The code choice is right; the landing is wrong at this head: declaring a status on a standard code that the shrink-only error-status-unpinned-baseline.json lists as unpinned requires removing that row in the same diff, and this head did not. That is the Lint & Repo Gates red above.
  5. Pre-existing and interacting: the engine registry is keyed by bare name, so a later registerFunction of the same name under another owner replaces the entry; under the new rule the displaced owner's later bind is refused loudly instead of running the other package's code, and boot binds resolve the handed functions first. Noted by the dev; right to note, not fix, here.

Census, re-read on the head tree with git grep (string handler values under examples/; registerFunction( in non-test packages/ sources): the only string handler in examples is app-showcase's job sweepProjectHealth; every registerFunction( outside the engine's own definition is packages/formula/src/stdlib.ts registering into a cel-js Environment, not the engine. This agrees with the dev's rows and with H3. cloud stays NOT MEASURED, as the dev named it.

② Semver level

.changeset/21604-hook-handler-package-scope.md: @objectstack/objectql minor, @objectstack/spec minor; ! in the title and the **BREAKING** banner; exactly one ADR-0087 HTML-comment marker, not-required (no-migration-prescription). Right.

  • The declaration line reads yes (narrowing). It is the first key-initial line of the PR body (line 2) and of the changeset body; the key is spelled Clause-② with an ASCII colon; the value token yes is the first thing after the colon; the arm is a parenthetical opening with narrowing. readClause2Line in scripts/pm/clause2-line.mjs returns declared / yes / narrowing for that shape (not twice-named, not quoted-and-continued; the later mention in the body's ② section is mid-line and is at most an inline-key reporter row, never a reading). yes is truthful because ① 9 widens a published type; (narrowing) is truthful because ① 1–5 narrow a declared accept set. It is also the ruling's release parameter verbatim.
  • Level axis: yes requires at least one package whose published source moves to grade minor or above — both do. (narrowing) is breaking, so check-adr-0087-registration demands a disposition; no-migration-prescription is honest: no authorable key, spelling, export or stored shape moves, HookSchema's shape is unchanged, and the body's "give it a body, or declare the function in your own functions" is a remedy sentence, not a FROM → TO rewrite, so the prescription detector does not refuse it. Both gates ran green twice on this head (Check Changeset).
  • minor on spec for a TSDoc-only edit is at the generous end, but the edit is the published declaration of the accept set in the shipped .d.ts, the ruling fixes minor, and the level axis admits it. Right.

③ Boundary flags

The ten dev flags in the PR body and the os-dev-report's open_questions entry, each answered:

  1. error level. Right. AGENTS.md's one question — after the degradation the system looks normal while something persisted (a hook row) claims a capability that never runs — answers error. The proxy doors discard BindHooksResult, so the log is the only channel there; the first line carries the consequence and the fix.
  2. Result type and exports. Verified; right (① 9–10).
  3. Envelope. Right code; incomplete landing on this head (① 12). The baseline shrink is the gate's own prescribed remedy (--update in the shrink direction; the maintainer-only tag in that script's header governs the baseline-expanding offer, not the shrink).
  4. strict. Right; an unknown name threw before, a cross-package name throws now.
  5. The metadata door and a package_id-stamped row (open question, options A / B / C). A is right. The ruling's "own package" is the owner the binder binds under; metadata-service holds no functions; B would make a stamped package_id a key to a code package's functions — the channel the ruling closes — and would teach objectql the runtime's app: prefix; C keeps an unowned channel open; measured pull is zero. Two escalations to the seat: (a) the dev's out-of-scope finding — PUT /api/v1/meta/hook/NAME with a handler and no body still answers 200, and such a hook can never bind on that door — is the Prime Directive 12 save-door half that this PR's own TSDoc now presupposes ("give it a body"); it belongs to the metadata-protocol save door, where card Two stored view containers of one object still displace each other's views: a container bound elsewhere or unbound under a sibling's expanded name, and a second container's bare list taking the first's <object>.default, are accepted with no diagnostic #21639 is in flight; file it or confirm it is filed. (b) the TSDoc drops "/ Studio" as a writer of the string form; objectui is not readable from this session, so the seat should confirm at the .objectui-sha pin that Studio's hook editor emits body and never a handler string.
  6. A bind with no packageId. A over C; right (① 4); the changeset declares it for library consumers of the public bindHooksToEngine.
  7. Platform functions (H3). Verified: the formula stdlib registers into cel-js; no platform function reaches the engine registry. Right.
  8. One artifact, one owner. Right reading (① 8); a composed customer artifact relying on sibling-stack names is the one shape the census could not see, and it belongs in any follow-up census.
  9. No new spelling. Right: "name the owning package explicitly" is met by importing from the owner and declaring in your own functions; minting a pkg/fn form would be a new door.
  10. Install-local untouched. Verified: app-artifact-handlers.ts and every CLI and runtime source file are outside the diff; the runtime addition is a test.

Carried for triage, not blocking: Action.target and script-node bare-name resolution (same family, other surfaces); the registry's bare-name key (① 13).

On this head's standing. The branch moved to b8c4e5e at 01:14:41Z; its only change over this head is the one-row baseline shrink, which is the right remedy. A ratchet baseline carries no merge=os-regen attribute (and the regen tool lists ratchets as not driver-managed), so no regeneration carry applies: the new head owes its own record on its own check-runs, which were in progress when I stopped reading (01:19:10Z).

Implemented-by: claude/issue-21604-hook-handler-package-scope
Reviewed-by: session_017ErfyP2Rx7XWHJA27QjyUi (isolated at-tier reviewer)

VERDICT: FAIL — the contract judgments in ①, ② and ③ stand, but this head carries a diff-caused red on the required Lint & Repo Gates context (① 12) with 76 gate steps unmeasured behind it, and the remedy is a code change that has already produced a different head.

Adopted by domain:engine#1 (session_017ErfyP2Rx7XWHJA27QjyUi) at 2026-10-04T01:24Z as the record of head 308ae946b9. The head has since moved to b8c4e5edc4 (patch round 1: the one-row baseline ratchet). Per this record, that head owes its own record on its own check-runs. needs:contract-review stays on the PR until a same-head PASS is posted.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b8c4e5edc4ec9b4ac87bdea3608d60a8d07c7bdf
Local-runs: none

Inputs read: card #21604's body and its four comments (ruling 5974477722, claim 5974619950, os-dev-report 5975213073, os-dev-report patch round 1 5975354465); PR #21653's body (unchanged since the previous head; the declaration is its line 2) and file list (8 files, +606 / −24); the net diff of the head against origin/main (merge-base 759dbe9); the check-runs on the head, polled at 2026-10-04T01:49:28Z; the Lint & Repo Gates job's step roster on this head; and, for the two escalations the previous record raised, card #21658's title and state, and objectui at the pin .objectui-sha names (ab187972159583b595facdcae3c73b50f6f312e9), read through git into the scratch directory. No PR comment was read. Nothing was checked out, built, tested or re-run.

Delta over the previous head, verified from git. One commit, b8c4e5e, parent 308ae94 (no merge of main): scripts/error-status-unpinned-baseline.json, one line removed, "INVALID_REFERENCE",; no other path moves. That is exactly the remedy the previous record derived for the Lint & Repo Gates red (the gate's nowPinned rule: a producer now declares a status for a code the shrink-only baseline listed as unpinned). The patch-round report states the file was produced by the gate's own node scripts/check-error-status-conformance.mjs --update and that re-running it on this head leaves the tree clean; the diff is consistent with that. The shrink direction is the author's remedy; the script header's maintainer-only tag covers the baseline-expanding offer.

Gate verdicts on this head (polled 01:49:28Z). 35 check-runs: 32 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), none failed, cancelled or in progress. Required contexts, all success: Lint & Repo Gates (01:46:41Z — 199 steps, 195 success and 4 skipped by condition; steps 119, 120, 121, 130, 135 and 179, which never ran on the previous head, all success); TypeScript Type Check (01:30:57Z; Type Check · source gates, · workspace, · debt ledger, · consumer gates all success); Test Core (01:44:44Z, six shards success); Dogfood Regression Gate (01:27:05Z, three shards success); Build Core (01:21:34Z); Temporal Conformance (live PG + MySQL) (01:28:01Z); Governed Surface Queue Guard (01:15:45Z). Advisory, all success: Check Changeset (01:15:45Z; carries check-adr-0087-registration and check-changeset-no-major), Dogfood Verify CLI, Spec property liveness, Flag docs affected by code changes, the three claim and single-writer guards, Check Documentation Links, Check PR Size, Auto Label. Since the merge-base, main carries four commits (f40bb32, 6ec54f0, eea82af, 1a23054); none touches any of this PR's eight files, the error-status gate, its baseline, the error catalog pages or errors.zod.ts, so no joint breakage is in view; the queue's rebuild is the race-free check.

① Derived judgments

The code diff is byte-identical to the previous head's; the judgments below are re-affirmed on this head, with item 12 now resolved. Doors the binder serves (every bindHooks / bindHooksToEngine call site outside tests): bindAppArtifactHandlers (packages/runtime/src/app-artifact-handlers.ts:227 — owner app:APPID, the bundle's functions handed; the AppPlugin boot path, --artifact, and install-local); the objectql plugin's two metadata-service binds (plugin.ts:2895 at boot and plugin.ts:2302 on re-sync — no functions handed, artifact-shipped hooks filtered out on both); and the built-in audit hooks under sys:audit (plugin.ts:1301, inline function handlers). No first-party door binds without an owner.

  1. Boot / artifact / install-local (app:APPID): a string handler resolves against the functions handed to the bind (runtime module included), then against an engine entry of that name only when entry.packageId equals the bind's owner; another package's entry is refused. Right — ruling B's letter; the composed pin (apps X, Y, Z) exercises each arm.
  2. Metadata door (metadata-service): every handler-only hook, including a sys_metadata row stamped with a package_id, is refused at bind with the envelope; a body hook binds as before. Right (③ 5). Not double-refused: isArtifactShippedHook keeps code-package hooks out of both metadata-service binds.
  3. sys:audit: inline functions, untouched. Right.
  4. Direct bindHooksToEngine with no packageId (a public export, no first-party caller): only the handed functions resolve. Right, and the changeset states it (③ 6).
  5. A name nobody holds: was skip + warn, now the coded refusal at error; strict throws as before. Right — the ruling's "refused at registration, with the name and a prescription"; the message names hook, function, package and the fix.
  6. body hooks: body-first ordering unchanged. Right.
  7. Deliberately not narrowed: Action.target and a flow script node's config.function still resolve engine-wide by bare name. The ruling covers Hook.handler only. Right to leave; carried for triage.
  8. Composition inside one artifact: one owner app:APPID, flattened functions, so a hook may still name a sibling stack's function inside the same artifact; STACK_COMPOSE_FUNCTION_CONFLICT refuses same-name collisions at compose time. Right reading of "own package" as the install unit.

Public surface:

  1. BindHooksResult.errors[] elements gain optional code and status; the type is exported from both entries (index.ts:370, core.ts:82) — an additive widening of a published output type. Right; the yes half of the declaration.
  2. HOOK_HANDLER_NOT_IN_PACKAGE_CODE / HOOK_HANDLER_NOT_IN_PACKAGE_STATUS are module exports of hook-binder.ts only; neither entry re-exports them (named re-exports, no star). Right: no new public symbol.
  3. HookSchema.handler TSDoc: the engine-wide fallback sentence is gone; the string form is relabelled "(build artifact)" with "/ Studio" dropped; a runtime-authored hook is told to use body. Schema and .describe() unchanged, no generated artifact moves. Right — and the Studio half is now verified (③ 5b).
  4. The envelope INVALID_REFERENCE / 400: a StandardErrorCode member of the 400 validation class, with a real precedent (plugin-auth's admin-set-user-manager.ts answers deny(400, 'INVALID_REFERENCE', …)), and no ledger code covering this condition. Declaring a status on a code the shrink-only error-status-unpinned-baseline.json listed as unpinned requires removing that row in the same diff — this head does, and the gate that reads it is green. Right. The baseline is a repo-internal ratchet, not a published surface, so the changeset owes nothing for it.
  5. Pre-existing and interacting: the engine registry is keyed by bare name, so a later registerFunction of the same name under another owner replaces the entry; under the new rule the displaced owner's later bind is refused loudly instead of running another package's code. Noted by the dev; right to note, not fix, here.

Census, re-read on the head tree with git grep: the only string handler under examples/** is app-showcase's job sweepProjectHealth; every registerFunction( in non-test packages/** sources outside the engine's own definition is packages/formula/src/stdlib.ts registering into a cel-js Environment, not the engine. cloud stays NOT MEASURED, as the dev named it.

② Semver level

.changeset/21604-hook-handler-package-scope.md, unchanged on this head: @objectstack/objectql minor, @objectstack/spec minor; ! in the title and the **BREAKING** banner; exactly one ADR-0087 HTML-comment marker, not-required (no-migration-prescription). Right.

  • The declaration line reads yes (narrowing): the first key-initial line of the PR body (line 2) and of the changeset body, key spelled Clause-② with an ASCII colon, value token yes first after the colon, arm a parenthetical opening with narrowing. readClause2Line in scripts/pm/clause2-line.mjs returns declared / yes / narrowing for that shape (not twice-named, not quoted-and-continued; the body's later mid-line mention is at most an inline-key reporter row). yes is truthful because ① 9 widens a published type; (narrowing) because ① 1–5 narrow a declared accept set. It is the ruling's release parameter verbatim.
  • Level axis: yes requires at least one package whose published source moves to grade minor or above — both do. (narrowing) is breaking, so a disposition is owed; no-migration-prescription is honest (no authorable key, spelling, export or stored shape moves; HookSchema's shape is unchanged; the body's remedy sentence is not a FROM → TO rewrite). Check Changeset is green on this head.
  • minor on spec for a TSDoc-only edit is at the generous end, but the edit is the published declaration of the accept set in the shipped .d.ts, the ruling fixes minor, and the level axis admits it. Right. The one new file, the baseline, publishes nothing from any released package and changes no changeset.

③ Boundary flags

The ten dev flags in the PR body, the first report's open_questions entry, the patch-round report (its open_questions is empty), and the two escalations the previous record raised:

  1. error level. Right: after the refusal the system looks normal while a persisted hook row claims a capability that never runs; the proxy doors discard BindHooksResult, so the log is the only channel there, and the first line carries consequence and fix.
  2. Result type and exports. Verified; right (① 9–10).
  3. Envelope. Right code, and on this head a complete landing (① 12). The patch-round report also explains why round one missed the gate: dispatch-gates placed check:error-status-conformance in its artifact-roster block, outside the 91 it ran; on this head the derivation adds it on the baseline path and the dev reports it green. The check-run is the verdict either way.
  4. strict. Right.
  5. The metadata door and a package_id-stamped row (open question A / B / C). A is right: the ruling's "own package" is the owner the binder binds under; metadata-service holds no functions; B would make a stamped package_id a key to a code package's functions and teach objectql the runtime's app: prefix; C keeps an unowned channel open; measured pull is zero. The two escalations are answered: (a) the save door answering 200 to a handler-only hook that can never bind on that door is filed as card The metadata save door answers 200 to a hook with a handler name and no body, which the runtime then refuses at bind: a runtime-authored hook holds no functions, so that form can never run #21658 (open, labelled finding, title naming exactly that), for triage, not fixed here — right, it belongs to the metadata-protocol save door. (b) Studio does not write the string form: at the pin, objectui's packages/app-shell/src/views/studio-design/ObjectHooksPanel.tsx addHook seeds body: { language: 'js', source: 'return;' } and the file contains no handler; the metadata-admin seeds in anchors.ts carry a no-op L2 body for the hook anchor and no handler string. Dropping "/ Studio" from the TSDoc is therefore accurate.
  6. A bind with no packageId. A over C; right (① 4).
  7. Platform functions (H3). Verified; right.
  8. One artifact, one owner. Right reading (① 8); a composed customer artifact relying on sibling-stack names is the one shape the census could not see.
  9. No new spelling. Right: importing from the owner and declaring in your own functions names the owning package explicitly; a pkg/fn form would be a new door.
  10. Install-local untouched. Verified: app-artifact-handlers.ts and every CLI and runtime source file are outside the diff; the runtime addition is a test.

Carried for triage, not blocking: Action.target and script-node bare-name resolution (same family, other surfaces); the registry's bare-name key (① 13).

Implemented-by: claude/issue-21604-hook-handler-package-scope
Reviewed-by: session_017ErfyP2Rx7XWHJA27QjyUi (isolated at-tier reviewer)

VERDICT: PASS

Adopted by domain:engine#1 (session_017ErfyP2Rx7XWHJA27QjyUi) at 2026-10-04T01:55Z as the record of head b8c4e5edc4, the current head. needs:contract-review is removed in this act.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 4, 2026 02:02
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 4, 2026 02:02
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit 98eb3b9 Oct 4, 2026
40 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21604-hook-handler-package-scope branch October 4, 2026 02:38
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 protocol:data size/l tests tooling

Projects

None yet

2 participants