Repository navigation
Commit 045b946
Fixes #21585
Clause-②: yes (narrowing)
Triage's retriage answer `5970803032` (Q1: A), re-claimed in
`5970905860`. This is #21489's ruling C applied to hooks and completed
on rehydrate, with the off-spec job `body` folded in.
## What changes
**The install-local door (`packages/cloud-connection`, `POST
/api/v1/marketplace/install-local`, `os package install`)** refuses two
more kinds of package it used to install with a 200. The refusal sits in
the same step as the enabled-job refusal and gives the same response.
- **A hook with no `body`.** A function-name `handler` names code that
travels only in an artifact's runtime module, never in the JSON this
door installs. So on this door such a hook could never bind to the
package's own code. It used to install and then either never fire, or
bind by name to a function the package does not ship.
- Every hook is judged, since a hook has no on/off switch.
- A hook carrying both `body` and `handler` installs, because its body
wins, as in the binder.
- **An enabled job whose `body` does not bind.** The door used to judge
only that a job `body` was present. It now judges that the body binds,
using the declaration's own parse of `JobSchema.body`.
- An expression (L1) body, or one carrying `body.timeoutMs`, is refused
instead of installing and never being scheduled.
- **One answer:** `422 VALIDATION_ERROR`, the code the job refusal
already used.
- It names everything the door cannot run: each hook and its handler,
each job and its handler, and each refused job body with the key the
declaration refuses.
- The remedies are a `body`, or `os start --artifact`.
- Nothing is registered, persisted, bound or scheduled.
- `os package install` renders it unchanged, with no special case:
`Install failed (422 VALIDATION_ERROR): …`.
- No ledger edit: `VALIDATION_ERROR` fits under the ledger's admission
rule for a generic validation condition. The reasoning is in the
constant's doc.
**The ONE binder (`packages/runtime/src/app-artifact-handlers.ts`)**
owns both judgements, so the door and the binder cannot disagree:
- `collectHooksWithoutBody`: a hook whose `body` is not a body object.
This mirrors the engine binder's own body-first test, so exactly the
hooks whose `handler` the engine would resolve by name are named.
- `bindAppArtifactHandlers` takes `withholdHooksWithoutBody`, which only
install-local sets because it carries no runtime module.
- Under it, a hook with no `body` is warned by name and **not bound**.
- It fires only on the **rehydrate** of an entry an earlier build
installed. The install route refuses such a hook first.
- The result reports `withheldHooks`.
- Install-local warns if the runtime it runs on predates the option.
- `collectJobsWithoutBody` (the landed name, kept) also names an enabled
job whose `body` does not bind: an expression body, or one carrying
`body.timeoutMs`. Its TSDoc says so. It reads `judgeJobBody`
(`sandbox/body-runner.ts`), a parse against `JobSchema.shape.body`.
- The job body factory now binds by that same judgement, replacing its
own parse with a separate `timeoutMs` check.
- Its warn still says `invalid job.body shape — the job is NOT
scheduled`, and now carries the declaration's sentence.
- The export names `collectJobsWithoutBody` and `JobWithoutBody` are
unchanged from `main`, because the shipped job liveness ledger anchors
`job/enabled`'s evidence on that symbol. This round's first head renamed
them, and CI's `@objectstack/spec` repo tier caught it. The landed names
are restored, with no alias and no `packages/spec` edit.
**Docs:** `content/docs/automation/jobs.mdx` now says the install door
also refuses an enabled job whose `body` does not bind. No hand-written
page states how a hook's `handler` behaves on `os package install`, so
there was no hook sentence to correct.
**Unchanged:**
- `os start --artifact` and a `defineStack` config carry their runtime
module: an app's own handler hooks bind to its own functions exactly as
before. This is pinned at the public door and in the binder unit.
- The metadata door is unchanged.
- No `objectql`, `packages/spec` or error-code-ledger edit.
- Owner-scoped function resolution on every door is the maintainer's
decision #21604, which remains open and is not done here.
- `scheduleAppArtifactJobs`'s scheduling identity is untouched. #21602
is held behind this PR.
## Measured before
These readings were taken on `main` `6c5697dffb` through the public
door, plus one unit reading on one engine, in os-dev report
`5970542332`.
- A package with a hook in the deprecated `handler` form and no `body`
installed with exit 0.
- When the name resolved nowhere, the hook never fired.
- When another app in the same runtime had registered a function under
that name, the hook bound to that app's code, hot and after a restart.
- An expression job body and a `body.timeoutMs` job body each installed
with exit 0 and were never scheduled.
- Only a server warn said any of this.
## Pins
| Pin (triage `5970803032`) | Where |
|:---|:---|
| The cross-app shape is refused at install with a non-zero exit |
`packages/cli/test/package-install-local-hooks.integration.test.ts`
(exit 1, `Install failed (422 VALIDATION_ERROR)`, names hook and remedy,
nothing in the ledger); `cloud-connection`
`marketplace-install-local-hooks.test.ts` (422, nothing registered,
persisted or bound) |
| A pre-existing entry rehydrates with the hook unbound and warned | the
CLI integration pin, second boot over a ledger entry in an earlier
build's layout: the record carries only the body hook's stamp, and the
warn names the hook; `cloud-connection` rehydrate unit; `runtime` binder
unit on a real `ObjectQL` engine (another app's function of the same
name never runs) |
| The body-hook control installs and fires | CLI integration (exit 0,
stamp `body`); cloud-connection unit (bound under `app:MANIFEST_ID`) |
| An `--artifact` app's own handler hooks still bind | CLI integration
(the host artifact's own handler hook fires from its runtime module);
runtime binder unit (no option: own function binds) |
| Each off-spec job-body shape is refused; a valid body job installs and
runs | CLI integration (expression body and `body.timeoutMs` each exit 1
naming the key, 0 rows; a valid body job exits 0 and writes rows);
cloud-connection jobs unit; runtime collector unit, including that every
job named is not scheduled and every enabled body job not named is |
## Reverse verification
Each leg went through `scripts/ablation-replace.mjs` in WRAP mode, with
its restore trap held by the tool. Each was rebuilt, then checked with
`scripts/ablation-dist-preflight.mjs`, then measured. Each restore was
proven by blob equals HEAD and an empty `git diff HEAD`. Each was
rebuilt again and checked with the preflight in the opposite mode, and
finished with the whole tree clean. All were taken from committed
`e961c7f5d5`, before the main merge.
| Leg | Anchor → mutation | Blob | dist preflight | Went red | Restore |
|:---|:---|:---|:---|:---|:---|
| (a) the hook refusal (door) | `if (unrunnable.jobs.length > 0 \|\|
unrunnable.hooks.length > 0) {` → drops the hooks term | `83b6de87a2b1`
→ `64f0fb4337e1` | `--absent 'unrunnable.hooks.length > 0'`: absent from
all 6 built files (it was in pristine dist, 1 hit in `index.js`) |
cloud-connection hooks unit 2/6 (the two refusals); CLI integration 1/8
(the refusal; the CLI printed `Package installed`) | blob `83b6de87a2b1`
== HEAD, diff empty; rebuilt: marker back in 2 built files, tree clean |
| (b) the rehydrate withholding (binder) | `if
(options.withholdHooksWithoutBody) {` → `… && String('ABLATED_21585_B')
=== '') {` | `8879aeae73fd` → `ee32244b3f9a` | marker present in
`dist/index.js` and `index.cjs` | runtime binder unit 1/9;
cloud-connection rehydrate unit 1/6; CLI integration 2/8: the rehydrated
record read `hostbody`, so the cross-app binding is back, and no warn
appeared | blob == HEAD, diff empty; rebuilt: marker absent from all 6,
tree clean |
| (c) the job-body bindability check (collector) | `if (judged.binds)
continue;` → `if (judged.binds \|\| String('ABLATED_21585_C') !== '')
continue;` | `8879aeae73fd` → `9e40620a0d48` | marker present in
`dist/index.js` and `index.cjs` | runtime collector unit 3/26;
cloud-connection jobs unit 2/11; CLI integration 2/8 (both off-spec
shapes installed) | blob == HEAD, diff empty; rebuilt: marker absent
from all 6, tree clean |
The direction was as expected on every leg: red.
## Verification (at `c7144b68bc`, after merging `main` `83b3d32020`)
- The test that went red in CI on `1b41b79d2d` is `pnpm --filter
@objectstack/spec exec vitest run --project repo
scripts/liveness/evidence.test.ts`: 42 passed. The whole `pnpm --filter
@objectstack/spec test:repo`: 51 files, 879 passed.
- `pnpm --filter '@objectstack/cli...' build` under `os-verify-lock`:
VERDICT command-exit 0.
- `@objectstack/runtime`: `typecheck` green, including
`check:test-typecheck`. Full `test`: 318 files, 4495 passed, 19 skipped.
- `@objectstack/cloud-connection`: `typecheck` green. Full `test`: 36
files, 437 passed.
- `@objectstack/cli`, `--project unit`: 255 files, 3745 passed.
- The install-local integration pins, on built packages:
- `package-install-local-{hooks,jobs}`: 19 passed.
- `package-install-local-{handlers,boot-steps,uninstall-cleanups}`: 40
passed.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 93 commands, all run, including the docs
families the `jobs.mdx` edit brings in. `--ran` reports 93 derived, 93
run, 0 NOT-MEASURED, 0 UNRUN.
- `check:skill-examples` and `check:dual-build-cjs-loads` first exited 3
(`PREREQUISITE NOT MET`, unrelated packages not yet built). Both exited
0 on rerun once a later gate in the run had built those packages. The
record carries the reruns.
- `main` moved to `5c9138b4b6` after this merge (`objectql` and
`driver-sql`). The derivation did not call the tree stale, so the branch
was not merged again.
- Roster gates whose roster sits in a touched directory, all exit 0 on
the first head: `check:error-code-casing`, `check:route-ledger-census`,
`check:authz-resolver`, `check:filter-alias-parity`,
`check-changeset-fixed`, `check:engine-double-contract`,
`check:error-status-conformance`.
- Full `pnpm lint` at `c7144b68bc`: exit 0.
- The reverse-verification table above was taken on `e961c7f5d5`. The
name restoration changes no logic, and the anchors it used are
unchanged.
## Acceptance notes
- A hook whose `body` IS an object the hook body runner refuses is
warned and not bound on every door. The binder does not fall back to its
`handler`. The door does not refuse that shape: the ruling folded in
job-body bindability only. Observation; carrier: none.
- The rehydrate pin's ledger entry is written in the layout the install
route persists, because no earlier build exists in the tree to write it.
- The contract review of record is owed before enqueue (claim
`5970905860`).
---
_Generated by [Claude
Code](https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent e367002 commit 045b946
11 files changed
Lines changed: 1276 additions & 96 deletions
File tree
- .changeset
- content/docs/automation
- packages
- cli/test
- cloud-connection/src
- runtime/src
- sandbox
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
170 | 170 | | |
171 | 171 | | |
172 | 172 | | |
173 | | - | |
174 | | - | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
175 | 176 | | |
176 | 177 | | |
177 | 178 | | |
| |||
0 commit comments