Repository navigation
Commit 7fd2c34
Fixes #21689
Clause-②: no (narrowing)
This carries out triage's ruling on #21689 (comment 5977077883, unlocked
in 5977495602): **one predicate. The metadata save door refuses any hook
with no `body`**, with a named error and the prescription "give it a
`body`". The handler-only refusal that #21658 landed (PR #21686,
`ced217ca30`) is now one case of it. `HookSchema` is untouched.
## What changes
`runtimeHookWithoutBodyRefusal` in
`packages/metadata-protocol/src/protocol.ts` keeps its name, its one
call site in `saveMetaItem` and its one envelope. Its predicate widens
from "a non-empty `handler` string and no `body` object" to "no `body`
object". It is the same judgement install-local's
`collectHooksWithoutBody` makes on its own door (`hookCarriesBody` in
`packages/runtime/src/app-artifact-handlers.ts`).
- **Envelope:** `VALIDATION_ERROR` / 400, unchanged. No code is added
and the ledger is not edited.
- **When and where:** unchanged from #21658. It runs in draft and in
publish mode, after the type-schema parse (a malformed `body` keeps the
schema's located 422), and before the authoring gate and every write.
- **Message, two readings of one rule:**
- With a `handler` that names a function, the message is byte-identical
to the one #21658 shipped. It names the hook and the function.
- With neither field, or an empty `handler`: "Invalid hook: 'NAME'
carries no `body`, so it has nothing to run. Give it a `body` (sandboxed
JS, `{ language: 'js', source }`, or an expression), which is stored
with the hook. A hook saved through the metadata API ships with no code
package, so its `body` is the only code it can run."
- Length: the handler form is 499 characters with 65-character names, as
before. The neither form is 282 characters plus the hook name, well
under the 500-character REST bound.
- **What still saves:** a hook with a `body`, with or without a
`handler` beside it.
## The PM's mechanism hypotheses, measured (at `1db5322ba2`, then rerun
on the merged head `15402d98f8`)
| # | Hypothesis | Reading |
|:--|:--|:--|
| H1 | Widen the existing predicate, one function, one call site, one
code. | **Holds.** One early return removed, and the message chosen by
whether `handler` names a function. The measured messages read right for
both shapes (quoted above; the sweep's failure output below shows the
neither text verbatim as the door produced it). |
| H2 | Five suites use a bare hook as their schema-valid probe; report
any others. | **Holds, with two more.** With the predicate widened and
every probe untouched, the full `metadata-protocol` suite went 7 failed
/ 3461 passed (4 files), and the full `objectql` suite went 6 failed /
7446 passed (3 files). The five named suites, plus `metadata-protocol`
`protocol.save-receipt-wording` and `objectql`
`metadata-validation-sweep`. A grep of `runtime`, `rest`, `plugins/*`,
`services/*`, `cli`, `verify`, `qa` and `examples/**` for hook saves
through this door (`type: 'hook'` items, `/meta/hook` paths) found only
`runtime`'s two pin tests, which already carry bodies. See the fixture
triage below. |
| H3 | `migrateStoredMetadata` and `duplicatePackage` surface a stored
bare row as their recorded failure; stored rows keep their bytes. |
**Holds.** Measured with a one-off harness that is not committed (the
stub engine of `protocol.stored-residue-resave.test.ts`).
`duplicatePackage` over a package holding one bare hook row and one body
hook row answered `{ success: false, copiedCount: 1, failedCount: 1 }`
with this refusal in `failed[0].error`, and the source rows' bytes were
unchanged. `migrateStoredMetadata({ apply: true })` over a bare row
answered `{ scanned: 1, canonical: 1, rewritten: 0, failed: 0 }` with
the bytes unchanged. **Population of stored bare hooks reachable from
this repository: zero.** `examples/**` and `packages/qa/**` seed no
`sys_metadata` hook rows (zero `type: 'hook'` items). |
| H4 | A built artifact's `handler` hook never passes `saveMetaItem`. |
**Holds at `1db5322ba2`.** Every production call site either saves a
fixed type other than `hook` (`automation.ts` and
`flow-credential-migration.ts` for flow, `packages.ts` for app,
`permission-set-projection.ts` for permission) or forwards an author's
or a stored row's type. The forwarding callers are the REST `PUT
/meta/:type/:name` (`rest-server.ts`), the dispatcher's metadata save
(`domains/meta.ts`), `migrateStoredMetadata` and `duplicatePackage`.
`AppPlugin`, `loadArtifactBundle`, the install-local door and the boot
path make zero `saveMetaItem` calls. The composed pin's X and Z controls
hold it end to end. |
| H5 | No first-party authoring path emits a hook with neither field
through this door. | **Holds for what this repository holds.** `os meta
register` forwards the author's own file and emits no hook of its own.
The `os` create and scaffold templates author `defineStack` sources,
which reach the artifact door and never this one. `packages/mcp` has no
hook tool. Studio is at the objectui pin `ab18797215`, unchanged since
#21658's census, which read its hook skeleton carrying a `body`. **Not
measured:** the cloud AI build agent's metadata tools, which live
outside this repository. |
## Fixture triage: seven probe suites move to a body-carrying hook
Each probe item gains `body: { language: 'js', source: 'return;' }` and
nothing else. No probe is deleted, and no assertion is changed or
loosened.
| Suite | What the probe measures | Still measures it |
|:--|:--|:--|
| `metadata-protocol` `protocol.code-only-types` (2 probes) | The #5086
code-only gate does not catch `hook` (`allowRuntimeCreate` only) on
either kernel; the #5264 matrix shows a hook save answers a repository
receipt. | Yes: `success: true` and one row on both kernels, and the
receipt fields. |
| `metadata-protocol` `protocol.meta-types-mint-door-agreement` | `PUT
/meta/hook` behaves as `/meta/types` advertises (declared, creatable),
read off one fact. | Yes: both cases are green. |
| `metadata-protocol` `protocol.unrecognised-meta-type` | A declared
runtime-create-only type still saves past the unrecognised-type refusal.
| Yes. |
| `objectql` `overlay-precedence` (3 probes: `hook`, plural `hooks`,
single-kernel bypass) | The two-tier verdict: brand-new items of
`allowRuntimeCreate` types pass the overlay whitelist, and a single
kernel bypasses the overlay gate. | Yes. |
| `objectql` `protocol-meta` (3 probes) | The PR-10d.7 two-tier model:
an artifact-backed hook is refused `NOT_OVERRIDABLE` / 403; a brand-new
hook and an edit of a DB-only hook are accepted. | Yes. The
`NOT_OVERRIDABLE` probe was green before the move too, because the
provenance gate runs first. It moved anyway, so that the refusal it
measures can only be the provenance gate's. The registry-seeded items
there are scenery for provenance, not saves through the door, and keep
their bytes. |
| **Beyond the five:** `metadata-protocol`
`protocol.save-receipt-wording` (`OVERLAYLESS_PROBES.hook`) | The
receipt sentence for a brand-new overlay-less type. | Yes. |
| **Beyond the five:** `objectql` `metadata-validation-sweep`
(`FIXTURES.hook`) | Valid gets 200; invalid gets the schema's 422 naming
`events`. | Yes. The invalid fixture carries the body too, so it stays
the valid fixture minus the one field the schema must name. |
## The enumeration pin, on the real door (ADR-0112: each refusal asserts
`code` and `status`)
Composed kernel,
`packages/runtime/src/hook-handler-package-scope.pin.test.ts`, through
`PUT /api/v1/meta/hook/NAME` as the signed-in administrator:
| Pin | Case | Asserts |
|:--|:--|:--|
| 1. A `body` hook saves. | ②b (existing) | 2xx; it binds and runs, and
so does one with both fields. |
| 2. A handler-only hook is refused. | ② (existing) | 400
`VALIDATION_ERROR`, naming the hook and the function and prescribing a
`body`; GET by name answers 404. |
| 3. A hook with neither field is refused. | **②c (new)** | 400
`VALIDATION_ERROR`, naming the hook and prescribing a `body`; GET by
name answers 404. |
| (2 and 3) Nothing bound. | **"② and ②c nothing bound" (widened)** |
After ②b's re-sync, the binder recorded no refusal of either hook and no
skip of the bare one (`skipsOf`, a new reader of the engine logger's
`skipping hook` warns). |
| 4. A built artifact's `handler` hook through its own door is
unchanged. | X and Z controls (existing) | App X's hook naming its own
`functions` entry binds and runs. App Z's hook naming a function its
`--artifact` runtime module exports binds and runs. |
At unit level, section 8 of
`protocol.invalid-metadata-422-face-inventory.test.ts` is new. It covers
publish and draft with neither field, and an empty `handler`. Each case
asserts `code`, `status`, the named hook, the prescription and an empty
store. Section 7 (#21658) is unchanged and still green.
## Reverse verification (the fix committed first, at `0c32c32bb1`)
**Mutation.** `node scripts/ablation-replace.mjs` restored the old
handler-only guard after the body test: `typeof hook.handler !==
'string' || hook.handler === ''` returns `undefined`, behind a marker
constant `ABLATED_21689_NEITHER`. The anchor count went 1 to 0 and the
blob went `3059168d5703` to `237d2559c48b`.
`@objectstack/metadata-protocol` was rebuilt, and `node
scripts/ablation-dist-preflight.mjs @objectstack/metadata-protocol
ABLATED_21689_NEITHER` found the marker in `dist/index.js` and
`dist/index.cjs`. A shell `trap` restore on EXIT, INT and TERM wrapped
the whole run.
The first attempt was a **no-op**, and it is disclosed here. Its
replacement re-contained the anchor line, so `ablation-replace` refused
it ("the anchor count moved 1 to 1"), ran nothing and proved the
restore. The second attempt re-spelled the body test (`typeof hook.body
=== 'object' && hook.body`, the same truth table), so the anchor left
the file.
**Prediction:** pin 3 red, and pins 1, 2 and 4 green. **Observed:**
- **Unit (src), sections 4 to 8:** 3 failed, all three section 8 cases
(publish neither, draft neither, empty `handler`). 15 passed, including
all of section 7 (handler-only refused in both modes, the body CONTROL,
body beside handler, malformed body 422).
- **Composed (dist):**
- ②c failed. It received `{ status: 200 }` with `Saved hook
'scope_authored_bare' (env-wide, state=active)`.
- "② and ②c nothing bound" failed. `skipsOf('scope_authored_bare')` held
3 binder skips, which also proves the new reader fires.
- ① ✓, X control ✓, Z control ✓, ② ✓ and ②b ✓: 2 failed, 5 passed.
**Restore.**
- `ablation-replace` restored the file. The blob equals HEAD
(`3059168d5703`) and `git diff HEAD` is empty. The outer trap's hash
compare agreed.
- Whole-tree `git status --porcelain` is empty.
- After a rebuild, the `--absent` preflight found the marker in none of
the 24 built files, and the tree was clean.
- The reruns are green: 26/26 (face inventory) and 7/7 (composed).
## Tests (at `15402d98f8`, after merging `origin/main` `8843505d91`,
which carries PR #21693)
- `pnpm --filter @objectstack/metadata-protocol exec vitest run
--maxWorkers=2`: 210 files passed and 3 skipped; 3578 tests passed and
19 skipped.
- `pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2
--project local`: 372 files and 7455 tests passed.
- Runtime `hook-handler-package-scope.pin.test.ts` and
`stored-metadata-body-boundary.pin.test.ts`: 14/14.
- Typecheck, exit 0 for all three packages:
- metadata-protocol `tsc --noEmit`. Its program includes all five edited
test files (`--listFiles`, one hit each).
- objectql and runtime: `tsc` plus `check:test-typecheck`, OK, with the
debt ledgers held. Their test layers compile under `tsconfig.test.json`
(`include: src/**/*`).
- Dependency closure: `pnpm turbo run build
--filter='@objectstack/runtime^...' --concurrency=2`, 29/29.
`packages/spec` moved on main's side, so `pnpm --filter
@objectstack/spec check:generated` also ran: all 15 generated artifacts
are up to date.
- The rest of `packages/runtime`'s suite is declared to CI.
## Gates (at `15402d98f8`)
Every family below ran first at `623b4a0b94` and ran again in full on
the merged head `15402d98f8`. The figures are the second run's.
**Derived.** `node scripts/pm/dispatch-gates.mjs --commands` (no paths)
derives 66 families, the same list on both heads, and all 66 ran.
- All 66 exited 0. That includes `check:dual-build-cjs-loads`: 106
published require entry points across 66 packages load. On the first
head it had exited 3, PREREQUISITE NOT MET, for want of a full build.
- `--ran` reconciliation: 66 accounted for, 66 run, 0 NOT-MEASURED (a
derived zero), 0 UNRUN.
**Artifact-roster block** (54 families, outside the derived total). All
54 ran.
- 51 exited 0. These include `check:error-status-conformance`,
`check:error-code-casing`, `check:authz-resolver`,
`check:route-ledger-census`, `check-changeset-fixed` and
`check:engine-double-contract`.
- 3 exited 2, NOT WIRED without PR context:
`check-closing-target-claim`, `check-partof-closing-keyword` and
`check-single-claim-paths`. They are rerun with this PR's context, and
the results go in the os-dev report.
**Symbol-anchor sweeps**, all exit 0:
- `check:adr-symbol-anchors`: 2167 anchors across 140 records.
- `check:scripts-symbol-anchors`: 3760 anchors across 282 scripts.
- `check:spec-docblock-symbol-anchors`: 4867 anchors across 1861 spec
sources.
- `check:adr-anchors`: OK.
**Lint.** CI owns `pnpm lint`. This PR records a proven narrowing
instead:
- **Population:** `eslint.config.mjs` lints `files:
['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`.
- **Count:** `eslint --no-inline-config --format json` over the 10
changed TS files reports 10 files, 0 errors and 0 warnings.
- **Invariance:** the config enables no type-aware linting (no
`parserOptions.project`), so this diff cannot move the verdict of any
untouched file.
## Changeset
`.changeset/21689-hook-no-body-save-door.md`: `minor` for
`@objectstack/metadata-protocol`, `Clause-②: no (narrowing)`, the
BREAKING banner, and the ADR-0087 marker `not-required
(no-migration-prescription)` with the census above.
`check-adr-0087-registration --base origin/main` accepts it.
## Landing point
As the claim predicted: `packages/metadata-protocol/src/protocol.ts`,
`runtimeHookWithoutBodyRefusal` (`saveMetaItem`, type `hook`), plus the
seven probe suites. No producer elsewhere needs a change. No governed
surface is touched. PR #21693, which edits the read region and the
import block of `protocol.ts`, landed on main before this PR opened. It
is merged in here; the merge was clean, and this diff touches neither
region.
## Acceptance notes
- **The draft-promotion and restore doors still do not re-ask this
rule.** #21658's PR recorded this for the `handler` form, and it now
covers the neither form too. `publishMetaItem`, `rollbackMetaItem` and
`revertCommit` can make a draft or a history version stored before this
change into an active bare row, which the runtime skips at re-sync as
before. Carrier: none.
- **`os meta register hook --data FILE`** forwards the author's file
through this door, so a bare hook file now gets this 400 and the CLI
prints its message. It needs no change of its own.
- **The binder's skip line** reads `skipping hook with unresolved
handler` for a hook that has no `handler`. No stored row of that shape
can be minted through this door any more. Noted, not filed.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7e0066a commit 7fd2c34
11 files changed
Lines changed: 202 additions & 54 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- objectql/src
- runtime/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
402 | 402 | | |
403 | 403 | | |
404 | 404 | | |
405 | | - | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
406 | 409 | | |
407 | 410 | | |
408 | 411 | | |
| |||
511 | 514 | | |
512 | 515 | | |
513 | 516 | | |
514 | | - | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
515 | 520 | | |
516 | 521 | | |
517 | 522 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
570 | 570 | | |
571 | 571 | | |
572 | 572 | | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
143 | 143 | | |
144 | 144 | | |
145 | 145 | | |
146 | | - | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
147 | 150 | | |
148 | 151 | | |
149 | 152 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
132 | | - | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
133 | 135 | | |
134 | 136 | | |
135 | 137 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
781 | 781 | | |
782 | 782 | | |
783 | 783 | | |
784 | | - | |
785 | | - | |
786 | | - | |
787 | | - | |
788 | | - | |
789 | | - | |
790 | | - | |
791 | | - | |
792 | | - | |
793 | | - | |
794 | | - | |
795 | | - | |
796 | | - | |
797 | | - | |
798 | | - | |
799 | | - | |
800 | | - | |
801 | | - | |
802 | | - | |
803 | | - | |
804 | | - | |
805 | | - | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
806 | 817 | | |
807 | 818 | | |
808 | 819 | | |
809 | 820 | | |
810 | 821 | | |
811 | | - | |
| 822 | + | |
812 | 823 | | |
813 | 824 | | |
814 | 825 | | |
815 | 826 | | |
816 | 827 | | |
817 | 828 | | |
818 | 829 | | |
819 | | - | |
820 | | - | |
821 | | - | |
822 | | - | |
823 | | - | |
824 | | - | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
825 | 836 | | |
826 | 837 | | |
827 | 838 | | |
| |||
832 | 843 | | |
833 | 844 | | |
834 | 845 | | |
835 | | - | |
| 846 | + | |
| 847 | + | |
836 | 848 | | |
837 | | - | |
838 | | - | |
839 | | - | |
840 | | - | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
841 | 855 | | |
842 | 856 | | |
843 | 857 | | |
| |||
18947 | 18961 | | |
18948 | 18962 | | |
18949 | 18963 | | |
18950 | | - | |
18951 | | - | |
18952 | | - | |
18953 | | - | |
18954 | | - | |
18955 | | - | |
| 18964 | + | |
| 18965 | + | |
| 18966 | + | |
| 18967 | + | |
| 18968 | + | |
| 18969 | + | |
| 18970 | + | |
18956 | 18971 | | |
18957 | 18972 | | |
18958 | 18973 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
215 | 215 | | |
216 | 216 | | |
217 | 217 | | |
218 | | - | |
| 218 | + | |
| 219 | + | |
219 | 220 | | |
220 | 221 | | |
221 | 222 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
143 | 143 | | |
144 | 144 | | |
145 | 145 | | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
146 | 150 | | |
147 | 151 | | |
148 | 152 | | |
149 | 153 | | |
| 154 | + | |
150 | 155 | | |
151 | | - | |
| 156 | + | |
152 | 157 | | |
153 | 158 | | |
154 | 159 | | |
| |||
0 commit comments