Repository navigation
Commit 4fef271
fix(objectql,runtime): a flag-absent install preserves the package's lifecycle state (#19291)
Fixes #18877
Clause-②: no
Maintainer decision batch #157 item 5, letter C (ruling comment
`5727164909`, director seat summon #24): the install contract becomes
「缺省 = 保持,有旗 = 设置」.
## The defect
`SchemaRegistry.initialDisabledPackageIds` is a **boot hydration input**
— filled once, before any registration, from the durable disable file,
and never updated by `enablePackage` / `disablePackage`. It was
nevertheless consulted by **every** `installPackage` call, so once an id
was in the boot seed set, every re-install within that boot re-landed it
disabled whatever the operator had most recently done. Since PR #18752
made the durable write follow the row the door returns, that stopped
being memory-only:
```text
boot 1 operator disables the package → disk lists the id
boot 2 seeded from disk; the package installs disabled
PATCH /packages/:id/enable → 200, registry true, disk CLEARED
install(m, { overwrite: true }) (no flag) → the seed still listed the id
→ row disabled, disk written DISABLED
boot 3 the operator's enable is gone, with no error anywhere
```
Reachable with nothing exotic: disable → restart → enable in Studio → an
SDK upgrade with `overwrite`.
## What changed — the ruling's items 1, 2 and 3
**Item 1 · `packages/objectql/src/registry.ts` · `installPackage`.** The
row is read **first**: an existing row keeps its own `enabled`, `status`
and `statusChangedAt`, and the boot seed decides only for an id that has
no row yet (boot hydration and a genuinely fresh install). A fresh id
the seed never named still lands enabled, the declared default.
`statusChangedAt` is carried, never restamped — a re-install is not a
lifecycle move.
**Item 2 · `packages/runtime/src/domains/packages.ts` · the install
door.** `enableOnInstall: true` ⇒ `enablePackage`, `false` ⇒
`disablePackage`, **absent ⇒ no lifecycle call at all**. Only `false`
was read before; the `true` case was carried by the re-install
restamping every row enabled, so under item 1 it would have silently
stopped working — the same «declared ≠ enforced» defect card #18058 was
about, pointing the other way. The bare (unwrapped) body form still
honours nothing: no schema declares the key there, so it is always 「缺省」.
**Item 3 · `DELETE /packages/:id`.** The id leaves the boot seed set
with its row (inside `uninstallPackage`, downstream of the ADR-0029
refusal point, so a refused uninstall still removes nothing at all) and
its durable disable entry is cleared. Previously that record was
immortal: a delete left behind a disable naming a package that no longer
existed.
**Item 5 is not here.** It measured TRUE and rides on spec sub-card
#19273; this PR does not touch `packages/spec/**` at all, which is why
it declares `Clause-②: no`.
## Item 4 — the pins that were re-ruled
Every flip is an expectation change on an existing pin, ⛔ never a
deletion. In
`packages/runtime/src/domains/packages-install-enable-on-install.test.ts`:
| pin | asserted before | asserts now | authority |
|---|---|---|---|
| `re-installing with the flag ABSENT …` | clears the durable disable —
row / registry / disk all `true` | **preserves** it — row / registry /
disk all `false`, plus `status` carried | ruling item 4, by name |
| `a BARE re-install …` | clears it too, because that form cannot ask
for `false` | **preserves** it — a body that cannot ask for a lifecycle
change is 「缺省」 | ruling items 1 and 2 |
| `a seeded id asked for enableOnInstall: true …` | SELF-CONSISTENT only
— 「the seed wins」, no claim about which state it lands in | **ENABLED**
— the flag outranks the seed; the self-consistency assertions are kept
underneath the new one | ruling item 2 |
**F1b stands unchanged**, exactly as the ruling says: every one of its
cases is a seeded id whose install asks for nothing, which still lands —
and stays — disabled. The `#18058 MEASURED` matrix (`disk.has(id) ===
(row.enabled === false)`) is untouched and still holds on all eleven
arms.
New pins:
`packages/runtime/src/domains/packages-install-preserves-lifecycle.test.ts`
(the card's own scenario end to end — row, registry, disk **and** a
restart — plus both 「有旗」 arms and both halves of item 3) and
`packages/objectql/src/registry-install-preserves-lifecycle.test.ts`
(the registry's own answer, which the door's pins structurally cannot
see: on every door arm 「the registry preserved the row」 and 「the door
re-applied the same state」 produce the same three records).
## Verification
Full local suites of both affected packages, plus the derived gate set,
at the final commit.
| run | result |
|---|---|
| `pnpm --filter @objectstack/objectql test` | 301 files / 5018 tests
passed |
| `pnpm --filter @objectstack/objectql typecheck` | exit 0 |
| `pnpm --filter @objectstack/runtime test` | 269 files / 3714 passed, 1
skipped |
| `pnpm --filter @objectstack/runtime run test:repo` | 2 files / 69
tests passed |
| `pnpm --filter @objectstack/runtime typecheck` | exit 0 |
| `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` | 63 families derived, 63 run, **all exit 0** |
| `--ran` reconciliation | 63 derived, 63 accounted, 0 UNRUN, 0
NOT-MEASURED |
| `pnpm lint` (repo-wide `eslint . --no-inline-config`) | exit 0 — run
whole, so no narrowing to justify |
| four roster gates whose roster sits under these paths |
`check:authz-resolver`, `check:error-code-casing`,
`check:filter-alias-parity`, `check:route-ledger-census` — all exit 0 |
`pnpm check:dual-build-cjs-loads` first answered **exit 3 — PREREQUISITE
NOT MET** (no `dist/` for 37 packages). That is not a pass and was not
recorded as one: a full `pnpm build` (73/73 tasks) was run and the gate
re-run to exit 0.
### Reverse verification — direction predicted before running, both legs
proven on disk
Both ablations went through `scripts/ablation-replace.mjs` (anchor must
hit; the write is verified against the disk and the blob hash, never
against an exit code) and `scripts/ablation-dist-preflight.mjs` (the
mutation really reached the `dist/` the consuming suite reads, and
really left it again).
**A · revert item 1** — plant `ablation18877RevertsPreserve` so
`installPackage` never sees the existing row. Marker present in 4 built
files of `@objectstack/objectql`. Predicted red, observed red: 3 of 9
registry pins and 6 of 34 door pins failed, including the card's own
end-to-end scenario and both flipped ABSENT pins. Restored: blob
`f75d202b2b98` == HEAD, `git diff HEAD` empty, marker absent from all 14
built files, whole-tree `git status --porcelain` clean.
**B · drop item 2's `true` arm** — plant `ablation18877DropsTrueArm` to
restore the pre-ruling door (only `false` moves the registry). Marker
present in 2 built files of `@objectstack/runtime`. Predicted red,
observed red: exactly 3 pins failed, all of them `true`-arm ones — and
one of them is #18058's own pre-existing `re-installing with
enableOnInstall: true clears the durable disable`, which is the
measurement showing the new arm is load-bearing rather than decoration.
Restored: blob `010a542447e8` == HEAD, `git diff HEAD` empty, marker
absent from all 6 built files, tree clean.
Both suites green again afterwards at `e40efa41b` (9 + 34 passed), which
is the commit every number above was taken at.
## Acceptance notes
- **Declared file-surface breach.** The dispatch claim named
`packages/objectql/src/` as the file surface. The ruling's items 2 and 3
land in `packages/runtime/src/domains/packages.ts`, and the pins item 4
re-rules live in
`packages/runtime/src/domains/packages-install-enable-on-install.test.ts`
— so **no arm of this card is implementable inside the declared
surface**, including a registry-only slice, which would have left
`enableOnInstall: true` silently unhonoured on an existing row. The
breach was de-risked first, not assumed: all 24 open PRs were enumerated
and **none** touches `packages/objectql/src/registry.ts`,
`packages/runtime/src/domains/packages.ts`,
`packages/runtime/src/package-state-store.ts` or the `#18058` pin file.
PR #18319 touches two other test files in the same directory and is
file-disjoint.
- **H17 rider #12789 — SKIPPED, deliberately.** The two `[Registry]
Collision` warnings in `registry.ts` do quote the package id
inconsistently (one double-quoted at the `sys_metadata`-shadow branch,
one single-quoted at its sibling), so the defect is real and it is one
line. It is skipped for three reasons: #12789 is `pm:on-hold` and its
card was not read, so which spelling it rules canonical is unknown; the
text is runtime output of a published package, so changing it grows the
changeset story this card was told not to grow; and it is unrelated to
the install contract.
- **noted, not filed:** `setPackageDisabled` failures are logged at
`warn` at all four call sites in this door (three pre-existing, one
added by item 3 in the same words). By the degradation-log rule that is
arguably a durability seam — the write claims to persist, the response
stays a clean 200/201, and the loss surfaces one restart later — but
`setPackageDisabled` is deliberately absent from
`DURABILITY_CRITICAL_CALLEES`, so escalating it would red all four sites
at once and is a vocabulary decision, not this card's. Carrier: the next
card that touches this door's durable half.
- **noted, not filed:** nothing parses an install request through
`PackageInstallRequestSchema` on the serving path —
`PackageApiContracts` has no runtime consumer and the handler reads the
raw body — so the schema's `.default(true)` is inert there today. That
is what makes «absent» visible to the door at all, and it is the
mechanism the PM's own assumption 1 predicted by a different route. It
is not filed as a defect because #19273 already owns the declaration
half; recorded here so the next reader does not conclude the door is
parsing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---
_Generated by [Claude
Code](https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent cdedfb3 commit 4fef271
6 files changed
Lines changed: 797 additions & 83 deletions
File tree
- .changeset
- packages
- objectql/src
- runtime/src/domains
| 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 | + | |
| 27 | + | |
| 28 | + | |
| 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 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1980 | 1980 | | |
1981 | 1981 | | |
1982 | 1982 | | |
1983 | | - | |
1984 | | - | |
1985 | | - | |
1986 | | - | |
1987 | | - | |
1988 | | - | |
| 1983 | + | |
| 1984 | + | |
| 1985 | + | |
| 1986 | + | |
| 1987 | + | |
| 1988 | + | |
| 1989 | + | |
| 1990 | + | |
| 1991 | + | |
| 1992 | + | |
| 1993 | + | |
| 1994 | + | |
| 1995 | + | |
1989 | 1996 | | |
1990 | 1997 | | |
1991 | 1998 | | |
1992 | 1999 | | |
1993 | 2000 | | |
1994 | | - | |
1995 | | - | |
| 2001 | + | |
| 2002 | + | |
| 2003 | + | |
| 2004 | + | |
1996 | 2005 | | |
1997 | 2006 | | |
1998 | 2007 | | |
| |||
4169 | 4178 | | |
4170 | 4179 | | |
4171 | 4180 | | |
4172 | | - | |
| 4181 | + | |
| 4182 | + | |
| 4183 | + | |
| 4184 | + | |
| 4185 | + | |
| 4186 | + | |
| 4187 | + | |
| 4188 | + | |
| 4189 | + | |
| 4190 | + | |
| 4191 | + | |
| 4192 | + | |
| 4193 | + | |
| 4194 | + | |
| 4195 | + | |
| 4196 | + | |
| 4197 | + | |
| 4198 | + | |
| 4199 | + | |
| 4200 | + | |
| 4201 | + | |
| 4202 | + | |
| 4203 | + | |
| 4204 | + | |
| 4205 | + | |
| 4206 | + | |
| 4207 | + | |
| 4208 | + | |
| 4209 | + | |
| 4210 | + | |
| 4211 | + | |
| 4212 | + | |
| 4213 | + | |
| 4214 | + | |
| 4215 | + | |
| 4216 | + | |
| 4217 | + | |
| 4218 | + | |
| 4219 | + | |
| 4220 | + | |
| 4221 | + | |
4173 | 4222 | | |
4174 | 4223 | | |
4175 | 4224 | | |
4176 | 4225 | | |
4177 | 4226 | | |
4178 | 4227 | | |
4179 | | - | |
4180 | | - | |
| 4228 | + | |
4181 | 4229 | | |
4182 | 4230 | | |
4183 | | - | |
4184 | 4231 | | |
4185 | 4232 | | |
4186 | 4233 | | |
| |||
4295 | 4342 | | |
4296 | 4343 | | |
4297 | 4344 | | |
| 4345 | + | |
| 4346 | + | |
| 4347 | + | |
| 4348 | + | |
| 4349 | + | |
| 4350 | + | |
| 4351 | + | |
| 4352 | + | |
| 4353 | + | |
| 4354 | + | |
4298 | 4355 | | |
4299 | 4356 | | |
4300 | 4357 | | |
| |||
0 commit comments