Skip to content

[Decision] the package boot seed set is never updated by enable/disable, so a flag-absent re-install durably reverts an operator later enable once #18752 lands — and the obvious producer fix re-opens #18058 F1 pins #18877

Description

@os-litant

Filed by the domain:spec execution seat, session_01LvwGppdonww4zGLWZo5rho, as the card its ruling (ii) on PR #18752 owes. Measured by the isolated at-tier delta review of that PR (record 5724766809, Finding 2). ⛔ Not a defect in #18752's diff — that PR faithfully implements decision batch #148 item 4 letter A. This card is the producer defect the remedy made durable.

The defect

SchemaRegistry.initialDisabledPackageIds is written by exactly one function, setInitialDisabledPackageIds (packages/objectql/src/registry.ts:1997), and read by the install path at :4172. Neither enablePackage (:4316) nor disablePackage (:4328) updates it.

⇒ once an id is in the boot seed set, every re-install within that boot re-lands it DISABLED, whatever the operator most recently did. An operator's PATCH /packages/:id/enable succeeds, is honoured in the registry, clears the record — and is then silently reverted by the next re-install.

Why it needs a card NOW, rather than having needed one all along

The quirk is pre-existing and, on the released door, memory-only: the install branch at the merge base calls setPackageDisabled 0 times, so the stale row never reached disk and a restart still honoured the operator.

PR #18752 correctly makes the durable write follow the ROW the door returned (the ruled remedy (a), which closes a real defect where the request's intent was persisted instead). The stale-seed row is one of the rows it now faithfully persists. ⇒ the quirk stops being memory-only and becomes durable when #18752 lands.

Measured, {row / registry / disk} after: operator disable in an earlier boot → this boot seeded and installed disabled → operator PATCH /packages/:id/enable (200, registry true, disk cleared) → flag-absent overwrite re-install (the SDK's install(m, {overwrite: true})):

handler re-install reading restart
merge base (released) false / false / false enabled — the operator's enable is honoured
#18752 pre-delta false / false / false enabled
#18752 at head false / false / true disabled — the operator's enable is lost

The row is false in all three; only the durable half moves. ⇒ a flag-absent install overrides the operator's most recent persisted lifecycle action — the dual of the behaviour the seat refused as remedy (b) on #18058.

Reachable in the composed runtime without anything exotic: disable → restart → enable in Studio → SDK upgrade with overwrite.

⚠️ Why this is a ruling and not a repair — the mechanical reason

The obvious fix is at the producer: enablePackage deletes from the seed set, disablePackage adds to it. The review measured that this flips PR #18752's F1 arm red. The pins establishing 「re-installing with the flag ABSENT clears the durable disable」 — which are the ruled remedy's own acceptance criteria — would fail.

⇒ the two behaviours are in genuine tension and cannot both be pinned:

  • F1's rule: a flag-absent re-install should clear a disable recorded earlier in this boot.
  • This card's rule: a flag-absent re-install should NOT override an operator's later explicit enable.

⭐ No amount of further measurement decides which wins — they are the same request shape with different histories, and the door cannot see the history the seed set forgot. That is a caliber question.

os-decision-facets

  • ① 项目长远合理性 — a lifecycle record whose most recent explicit write can be silently reverted by an unrelated operation is not a record. The long-term shape is that the seed set is derived state that tracks the live registry, not a boot-time snapshot that outlives the truth. But that shape is exactly what re-opens F1, so 「correct」 here costs a pinned behaviour.
  • ② 实际业务拉动 — ⛔ zero today and that is measured, not assumed: on the released door the quirk never reaches disk. The pull begins only when fix(spec,runtime): bind the package-install contract to the door that serves, and honour enableOnInstall #18752 lands, which is what makes this a sequencing question as much as a design one.
  • ③ 防 AI 犯错 — decisive and cuts one way: the failure is silent and durable. An operator (or an agent) enables a package, sees 200, sees the registry agree, and a later routine upgrade reverts it across a restart with no error anywhere. ⚠️ But note the symmetric risk: whichever rule is chosen, the OTHER one becomes the silent surprise, so this axis argues for choosing explicitly and pinning both directions, ⛔ not for a particular letter.
  • ④ 创业阶段不扩散 — the producer fix is small in lines (two set mutations) but its blast radius is every consumer of the install door's disable semantics plus [finding] PackageInstallRequestSchema is a published, route-bound request contract that no layer parses — the install door is declared and enforced nowhere (ADR-0049) #18058's pins. ⛔ Not a drive-by.

The question, in one line: should the boot seed set become derived state that enablePackage / disablePackage keep current — accepting that #18058's F1 pins are re-opened and must be re-ruled — or should it stay a boot snapshot, accepting that a flag-absent re-install durably reverts an operator's later enable?

⛔ The filing seat does not grade this: it is the seat that ruled remedy (a) on #18058, so the letter that vindicates its own ruling is the one it would be grading. Conflict declared in the open.

Refs and fences

Dedupe words: initialDisabledPackageIds stale seed, enablePackage does not update seed set, re-install reverts operator enable, boot seed snapshot vs derived state, package disable durable override.


Generated by Claude Code

Activity

  1. hotlong commented on Sep 18, 2026

    @hotlong
    Contributor

    Ruling: batch #157 item 5 · letter C (the install contract becomes 「缺省 = 保持,有旗 = 设置」: a flag-absent re-install of an EXISTING row preserves that row's lifecycle state; the boot seed set applies only to ids that have no row yet; enableOnInstall present sets the state in both directions; DELETE /packages/:id clears the seed entry and the durable record; #18058 F1's 「flag-absent re-install clears the durable disable」 pin is re-ruled to 「preserves」, F1b stands) · maintainer 「其他同意」 2026-09-18T08:10Z

    Director seat, summon #24, session_01Wj1HUjzyeiBQ8atRf1ZhaL. Presented in detail with the recommendation C (outside the card's A / B); the maintainer agreed. Facts (this card; at-tier record 5724766809; seat reading on origin/main): initialDisabledPackageIds is seeded once at boot from the durable file (app-plugin.ts:446), read by every installPackage (registry.ts:4172), and never updated by enablePackage / disablePackage (:4316 / :4328). PR #18752 (merged 2026-09-18T05:40Z, #18058 closed) correctly makes the disk follow the row the door returned — which turns a memory-only quirk durable: disable → restart → operator PATCH /packages/:id/enable (200, registry true, disk cleared) → flag-absent install(m, {overwrite: true}) → the seed still lists the id → row lands disabled → disk written disabled → the operator's enable is gone after the next restart, with no error anywhere.

    The root: 「flag-absent re-install」 has three meanings today — F1 pins 「same-boot disable, then flag-absent re-install ⇒ back to the declared default, enabled」; F1b pins 「boot-seeded disable survives a flag-absent install」; this card's path yields 「seeded, then enabled, then flag-absent re-install ⇒ back to disabled」. They are mutually inconsistent, and F1 vs F1b already disagree on what 「absent」 means. Mainstream platforms (a WordPress plugin update, a Jira app upgrade, a Home Assistant integration reload) never touch the operator's enabled/disabled state unless asked.

    Ruling — C: absent = preserve, present = set

    1. SchemaRegistry.installPackage, overwrite of an existing row: enabled / status / statusChangedAt are carried over from the existing row (:4172 reads the existing row first; the seed set is consulted only when no row exists, i.e. boot hydration and a genuinely fresh install). A fresh id not in the seed lands enabled (the declared default).
    2. The install door: enableOnInstall: true ⇒ enablePackage; false ⇒ disablePackage; absent ⇒ no lifecycle call (today only false is handled). The durable write keeps following the row the door returned (fix(spec,runtime): bind the package-install contract to the door that serves, and honour enableOnInstall #18752 unchanged).
    3. DELETE /packages/:id removes the id from the seed set and clears its durable record — a row that no longer exists has no lifecycle state; the next install of that id is a fresh install.
    4. Pins: F1's 「re-installing with the flag ABSENT clears the durable disable」 is re-ruled: with the flag absent the disable is preserved (row, registry and disk all disabled); the enableOnInstall: true case keeps clearing it. F1b stands as is. This card's scenario is pinned: seeded → enable → flag-absent overwrite ⇒ enabled on row, registry, disk and after a restart.
    5. If PackageInstallRequestSchema's enableOnInstall: z.boolean().default(true) erases absence at parse time so the door cannot see it, the declaration becomes optional() with its semantics written on the field (absent = keep the current state; a fresh install lands enabled) ⇒ that half is Clause-②: yes, @objectstack/spec patch changeset, a spec-lane sub-card by charter conflict: does a Clause-②: no PR that touches no contract surface still owe an in-seat review before it can land? #18536 rule 2; the engine half lands with or after it. ⛔ The door does not sniff the raw body around the schema.
    6. ⛔ A (seed set kept in step by enable/disable) leaves three meanings and re-opens F1 for a rule nobody wrote down; ⛔ B (snapshot) keeps the silent durable revert.

    Four-facet reading: ① one rule covers the three scenarios and the seed set stops being a second source of truth; ② package upgrade is the SDK's and Studio's everyday path; ③ a platform user's AI running an upgrade must never flip lifecycle state — 「preserve」 makes the safe outcome the default; ④ one read at :4172, two door arms, one DELETE line; no new mechanism.

    Execution

    needs-user-decision → pm:queue; domain:engine, p2, bug stay. Clause-②: no for the engine half unless item 5 measures true, in which case the claimant files the spec sub-card first and declares it. The filing seat's declared conflict (it ruled #18058's remedy) is noted; the ruling here is the director's on the maintainer's word.


    Generated by Claude Code

  2. self-assigned this
    on Sep 20, 2026
  3. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round 6
    Session: session_01NcPSwnmJHczmTu6FG7NMjE
    Branch: claude/issue-18877-install-preserves-lifecycle
    Worktree: objectstack-issue-18877
    Domain: domain:engine
    Seat: domain:engine#1
    File surface: packages/objectql/src/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgment tier — quoting this act's node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/objectql/src/registry.ts: 「Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s)… The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable).」 ⇒ ruling-implementation card with live design judgment ⇒ default judgment tier, ⛔ not the floor.
    Clause-②: no
    Thread-read: 5727164909
    Serial constraints cleared: lane in flight was 0 at claim; 26 open PRs in the repo enumerated and NONE touches packages/objectql/src/registry.ts; same-batch siblings #18997 (packages/metadata-protocol/src/protocol.ts) and #19082 (packages/objectql/src/engine.ts) are file-disjoint from this card — ⛔ judged by FILE, not by package, so sharing packages/objectql with #19082 is not a serial constraint; H17 hold #12789 declares packages/objectql/src/registry.ts as its trigger file — named below as a declared opportunistic rider, ⛔ not folded in


    ⭐ The claimant's own obligation under this card's ruling is DISCHARGED

    Ruling 5727164909 (batch #157 item 5 · letter C) item 5 makes this card conditional on a measurement and assigns the follow-up to the claimant: 「Clause-②: no for the engine half unless item 5 measures true, in which case the claimant files the spec sub-card first and declares it.」

    Item 5 measures TRUE. Read on origin/main 1739f71879 (== this checkout's HEAD), 2026-09-20T08:45Z:

    reading value instrument
    the declaration enableOnInstall: z.boolean().default(true) at packages/spec/src/api/package-api.zod.ts:286 git grep -n enableOnInstall origin/main -- 'packages/spec/src/**' ':!*.test.ts'
    lit control — absence really is erased parse({manifest:{…}}) ⇒ expect(result.enableOnInstall).toBe(true), packages/spec/src/api/package-api.test.ts:85 git show origin/main:…/package-api.test.ts

    ⇒ Spec sub-card #19273 is filed (domain:spec, pm:blocked, Blocked-by: #18605), carrying the measurement, the instrument radius, a one-line executable criterion, and the collision below. ⇒ this card's engine half keeps Clause-②: no; the yes rides on #19273, exactly as the ruling allocates it.

    ⚠️ Declared collision the taker must NOT discover late

    #18605 (domain:spec, p1, needs-user-decision) carries draft PR #19130, whose option A installs a pin across 缺省 / false / true / 字符串 / null while asserting 「键、默认值、接受集一个字节不动」. Its 缺省 cell asserts the behaviour #19273 must change. Batch #153 (5724940709, 2026-09-18T03:59Z) and batch #157 (5727164909, 2026-09-18T08:10Z) meet here and the younger one moves the elder's ground; ⛔ #157's text names neither #18605 nor PR #19130. ⛔ This seat does not resolve that — declared on both cards, sequencing left to the domain:spec seat and the maintainer.

    ⇒ For THIS card it is a fence, not work: the engine half is fenced ⛔ out of packages/spec/** entirely.

    H17 rider, declared

    Half-state patrol anchor #9857's H17 index declares #12789 (pm:on-hold) with trigger file packages/objectql/src/registry.ts — this card's landing file. Per 「相交 ⇒ 点名该单,顺手活列为申报过的增项」 it is named, and offered as an opportunistic rider only: #12789 is a one-line warning-text defect (「the sibling [Registry] Collision warning also double-quotes the package id」). ⛔ It is NOT a requirement of this card and ⛔ must not grow the diff or the changeset story; take it only if it is genuinely one line in a file already open, and say so in the report if taken or skipped.


    Generated by Claude Code

  4. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    ⛔ Correction by the dispatching seat — MY claim declared this card's file surface too narrowly. The dev is not in breach; I am. domain:engine#1, session_01NcPSwnmJHczmTu6FG7NMjE. Written 2026-09-20T09:35Z.

    What I got wrong

    Claim 5748790383 declared File surface: packages/objectql/src/ and told the dev 「stop on breach」. The branch claude/issue-18877-install-preserves-lifecycle (8481f4f07, read 2026-09-20T09:35Z) also touches packages/runtime/src/domains/packages.ts and two test files beside it.

    ⭐ That is the ruling's own surface, and I should have declared it. Ruling 5727164909 item 2 governs 「The install door: enableOnInstall: true ⇒ enablePackage; false ⇒ disablePackage; absent ⇒ no lifecycle call」. Measured just now, ⛔ not assumed: git log --oneline origin/main -- packages/runtime/src/domains/packages.ts returns 156792e0a — 「fix(spec,runtime): bind the package-install contract to the door that serves, and honour enableOnInstall (#18752)」. ⇒ PR #18752 — the very PR this card was filed against — landed in that file. The install door IS packages/runtime/src/domains/packages.ts; item 2 is unimplementable without it.

    ⇒ ⛔ Not scope creep, ⛔ not a breach to answer for in the report. The dev read the ruling correctly where my claim read it narrowly.

    The true declared surface, and the channel it travels on

    packages/objectql/src/ + packages/runtime/src/domains/.

    packages/runtime belongs to domain:cli by the lane table. This is the second of the three cross-boundary channels — 「认领席跟到底的越界面」 — so ⛔ the card does not move lanes: this seat keeps it and follows it to MERGED, and the cross-lane half gets an isolated at-tier review at delivery.

    Serial-constraint re-check on the widened surface — ⛔ run, not assumed

    All 26 open PRs enumerated and their file lists read (2026-09-20T09:35Z):

    file conflicting open PR
    packages/runtime/src/domains/packages.ts none — PR #18319 is the only open PR anywhere under packages/runtime/src/domains/, and it touches only packages-capability-gate.test.ts and packages-uninstall-envelope.test.ts, ⛔ not packages.ts
    packages/objectql/src/registry.ts none

    ⇒ the widened surface is clear. Same-batch siblings #18997 (packages/metadata-protocol/src/) and #19082 (packages/objectql/src/engine.ts) remain file-disjoint from both halves.

    ⚠️ Still owed by the dev at delivery, ⛔ not waived by this correction: this branch carries no .changeset/ entry as of 8481f4f07, while both sibling branches in this batch already do — and it now changes published behaviour in two packages (@objectstack/objectql and @objectstack/runtime). ⛔ Not judged here: the branch is mid-flight and the PR is not open. It is named so the delivery report answers it rather than the review discovering it.


    Generated by Claude Code

  5. added a commit that references this issue on Sep 20, 2026
  6. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
     "issue": 18877,
     "status": "done",
     "branch": "claude/issue-18877-install-preserves-lifecycle",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/19291",
     "session": "session_01NcPSwnmJHczmTu6FG7NMjE",
     "premise_still_valid": true,
     "summary": "Ruling batch #157 item 5 letter C implemented across its three engine-half items. Item 1: `SchemaRegistry.installPackage` reads the EXISTING row first and carries its `enabled` / `status` / `statusChangedAt` over; the boot seed set is consulted only when no row exists, so a stale boot snapshot can no longer revert an operator's later explicit lifecycle action. Item 2: the install door honours BOTH arms of `enableOnInstall` (true => `enablePackage`, false => `disablePackage`) and makes NO lifecycle call when the key is absent; the `true` arm is newly load-bearing because item 1 removed the re-install restamp that used to carry it. Item 3: `DELETE /packages/:id` clears the boot seed entry (inside `uninstallPackage`, downstream of the ADR-0029 refusal point) and the durable disable record. Item 5 is NOT here: it rides on spec sub-card #19273 and `packages/spec/**` was not touched, so the PR declares `Clause-2: no`. Assignee on the card was already set by the PM (huangyiirene) and was not written by this seat.",
     "premise_check": "Card premise re-verified on origin/main before editing, by symbol not by the card's line numbers. `setInitialDisabledPackageIds` is still the sole writer of `initialDisabledPackageIds`; the install path still read it unconditionally; neither `enablePackage` nor `disablePackage` updated it. Landing sites had moved (registry.ts:4172 -> the `const disabled = ...` line at 4172 still, but `enablePackage`/`disablePackage` at 4316/4328 -> 4316/4328 after lander 2bed4c32 was already in). Premise holds.",
     "pm_assumptions_measured": [
      "Assumption 1 (absence visible to the engine on the SDK path) — CONFIRMED, but by a DIFFERENT mechanism than the PM's reading. It is not that the TS contract `InstallPackageInput.enableOnInstall?: boolean` is optional while the zod `.default(true)` sits at a REST-only door. Measured: NOTHING parses an install request through `PackageInstallRequestSchema` on the serving path at all. `PackageApiContracts.installPackage` declares `input: PackageInstallBodySchema` but `PackageApiContracts` has ZERO runtime consumers (grep across packages/ + apps/ finds only the declaration and spec's own tests), and `handlePackagesRequest` reads the raw body directly (`body?.enableOnInstall`). The SDK sends `enableOnInstall: options?.enableOnInstall`, which JSON.stringify drops when undefined, so the key is absent on the wire and arrives absent at the door. => rule 2 ('absent => no lifecycle call') is implementable today with no spec edit. No fork.",
      "Assumption 2 (landing sites) — CONFIRMED by symbol. All four symbols are where the card said, in `packages/objectql/src/registry.ts`. What the card did NOT say, and what this seat measured, is that items 2 and 3 do not live there at all — see `deviations`."
     ],
     "tests": "All numbers taken at the final commit e40efa41b (origin/main merged in at adf4b1877). 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. pnpm lint (repo-wide `eslint . --no-inline-config`) :: exit 0 — run WHOLE, so there is no narrowing to justify. pnpm build :: 73/73 tasks. ABLATION A (revert item 1) via scripts/ablation-replace.mjs, anchor 'const existing = this.getPackage(manifest.id);' x1 -> x0, blob f75d202b2b98 -> ea1977a1328d; rebuilt @objectstack/objectql; ablation-dist-preflight confirmed marker `ablation18877RevertsPreserve` PRESENT in 4 built files. Predicted RED, observed RED: objectql pins 3 failed / 6 passed (9), runtime pins 6 failed / 28 passed (34). Restore leg: blob == HEAD f75d202b2b98, `git diff HEAD` empty, rebuild, preflight --absent confirmed marker ABSENT from all 14 built files, whole-tree `git status --porcelain` clean. ABLATION B (drop item 2's `true` arm) anchor 'const requestedEnabled = wrapped ? body?.enableOnInstall : undefined;' x1 -> x0, blob 010a542447e8 -> 81265fc1be59; rebuilt @objectstack/runtime; marker `ablation18877DropsTrueArm` PRESENT in 2 built files. Predicted RED, observed RED: exactly 3 pins failed, ALL of them `true`-arm ones, one of which is #18058's own pre-existing pin 're-installing with enableOnInstall: true clears the durable disable' — that is the measurement showing the new arm is load-bearing rather than decoration. Restore leg: blob == HEAD 010a542447e8, `git diff HEAD` empty, rebuild, preflight --absent, tree clean. Both suites green again after both restores at e40efa41b (9 + 34 passed). No permanent ablation artefacts remain.",
     "gates": [
      "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
      "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
      "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
      "node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
      "node scripts/check-ci-filter-parity.mjs :: exit 0",
      "node scripts/check-closing-keyword-parity.mjs :: exit 0",
      "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
      "node scripts/check-comment-mask-adoption.mjs :: exit 0",
      "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
      "node scripts/check-comment-mask-corpus.mjs :: exit 0",
      "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
      "node scripts/check-empty-changeset.mjs --self-test :: exit 0",
      "node scripts/check-engine-split-ratio.mjs --days 90 :: exit 0",
      "node scripts/check-engine-split-ratio.mjs --self-test :: exit 0",
      "node scripts/check-keyed-text-bounds.mjs :: exit 0",
      "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
      "node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
      "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
      "node scripts/check-plugin-teardown-shape.mjs :: exit 0",
      "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
      "node scripts/check-registry-log-declared.mjs :: exit 0",
      "node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
      "node scripts/check-rest-log-declared.mjs :: exit 0",
      "node scripts/check-rest-log-declared.mjs --self-test :: exit 0",
      "node scripts/check-rest-log-spy-declared.mjs :: exit 0",
      "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
      "node scripts/check-system-context-census.mjs :: exit 0",
      "node scripts/check-system-context-census.mjs --self-test :: exit 0",
      "node scripts/check-undeclared-dep-imports.mjs :: exit 0",
      "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
      "node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
      "node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
      "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
      "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
      "pnpm check:changeset-gate-self-tests :: exit 0",
      "pnpm check:cross-package-test-inputs :: exit 0",
      "pnpm check:dispatcher-error-vocabulary :: exit 0",
      "pnpm check:doc-authoring :: exit 0",
      "pnpm check:driver-memory-census :: exit 0",
      "pnpm check:dts-closure :: exit 0",
      "pnpm check:dual-build-cjs-loads :: exit 0",
      "pnpm check:durability-log-level :: exit 0",
      "pnpm check:engine-double-contract :: exit 0",
      "pnpm check:lean-entry-closure :: exit 0",
      "pnpm check:logger-receiver-detach :: exit 0",
      "pnpm check:nul-bytes :: exit 0",
      "pnpm check:objectql-double-limit :: exit 0",
      "pnpm check:objectui-changeset :: exit 0",
      "pnpm check:org-identifier :: exit 0",
      "pnpm check:page-declaration-shape :: exit 0",
      "pnpm check:pm-changeset-deadline-census :: exit 0",
      "pnpm check:published-files :: exit 0",
      "pnpm check:query-options-erasure :: exit 0",
      "pnpm check:refd-timer-probe :: exit 0",
      "pnpm check:route-envelope :: exit 0",
      "pnpm check:slot-lookup :: exit 0",
      "pnpm check:sourcemap-no-sources-content :: exit 0",
      "pnpm check:test-source-alias :: exit 0",
      "pnpm check:tier-file-adoption :: exit 0",
      "pnpm check:type-check-coverage :: exit 0",
      "pnpm check:type-check-debt :: exit 0",
      "pnpm check:watch-hint-literal :: exit 0",
      "pnpm check:where-matcher :: exit 0",
      "pnpm check:authz-resolver :: exit 0  (roster-under-my-paths, run although scored `silent`)",
      "pnpm check:error-code-casing :: exit 0  (roster-under-my-paths, run although scored `silent`)",
      "pnpm check:filter-alias-parity :: exit 0  (roster-under-my-paths, run although scored `silent`)",
      "pnpm check:route-ledger-census :: exit 0  (roster-under-my-paths, run although scored `silent`)"
     ],
     "gates_reconciliation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 63 families from the ACTUAL changed files (re-derived after writing the changeset, which added 9, and again after merging origin/main, which added none). --ran reconciliation with `COMMAND :: exit CODE` records: 63 derived, 63 run, 0 UNRUN, 0 NOT-MEASURED. Exit codes were landed to a TSV before any pipe. `pnpm check:dual-build-cjs-loads` first answered exit 3 (PREREQUISITE NOT MET — 37 packages had no dist/); that was NOT recorded as a pass — a full `pnpm build` (73/73) was run and the gate re-run to exit 0, which is the code in the list above. The first --ran attempt was REJECTED by the tool for carrying bare command lines with no exit codes; redone in the required form. A STALE TREE warning on that attempt (origin/main had moved 4 commits and `scripts/pm/dispatch-gates.mjs` itself was one of the changed files) was honoured: origin/main was fetched, merged, and the derivation re-run — the family list came back byte-identical, so nothing was added and nothing extra needed running.",
     "line_budget": null,
     "api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (draft PR #19291) x1; POST /repos/objectstack-ai/objectstack/issues/18877/comments (this report) x1. ZERO label writes: the dispatch narrowed the write budget to exactly these two endpoints, so `scripts/pm/label-write.mjs` was not run and no label was added or removed on the card or the PR. Reads (unbudgeted) were REST too: the card, its comments, the open-PR list and 23 per-PR file lists for the collision scan below.",
     "mcp_calls": "0 — no MCP GitHub tool was called, read or write.",
     "files_changed": [
      "packages/objectql/src/registry.ts — item 1 (installPackage reads the row first) + item 3's registry half (uninstallPackage forgets the seed entry) + the seed-set docblocks",
      "packages/objectql/src/registry-install-preserves-lifecycle.test.ts — NEW, 9 pins on the registry's own answer",
      "packages/runtime/src/domains/packages.ts — item 2 (both flag arms at the install door) + item 3's durable half at DELETE + the comment blocks the ruling invalidated",
      "packages/runtime/src/domains/packages-install-enable-on-install.test.ts — 3 existing pins flipped per item 4 (expectations changed, none deleted) + 2 docblocks re-ruled",
      "packages/runtime/src/domains/packages-install-preserves-lifecycle.test.ts — NEW, 10 pins: the card's scenario end to end (row/registry/disk/restart), both 「有旗」 arms, both halves of item 3",
      ".changeset/18877-install-preserves-lifecycle.md — patch/patch for @objectstack/objectql + @objectstack/runtime, carrying the behaviour-change note and `Clause-2: no`"
     ],
     "deviations": [
      "DECLARED FILE-SURFACE BREACH. The claim comment named `packages/objectql/src/` as the file surface ('stop on breach; explain in the report'). MEASURED: no arm of this card is implementable inside it. Ruling items 2 and 3 land in `packages/runtime/src/domains/packages.ts`, and the pins item 4 re-rules by name live in `packages/runtime/src/domains/packages-install-enable-on-install.test.ts`. Even a registry-only slice breaches, because it turns that file's `enableOnInstall: true` pin red AND leaves `true` silently unhonoured on an existing row. The dispatch prompt's own Zone 3 routes items 2 and 3 to those files, so this is read as the claim line being narrower than the work the PM assigned, not as a second opinion. De-risked BEFORE editing rather than assumed: all 24 open PRs enumerated and their file lists read — 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 #19130 (card #18605, the declared collision) is spec-side and disjoint. PR #18319 touches two OTHER test files in the same directory (packages-capability-gate.test.ts, packages-uninstall-envelope.test.ts) and is file-disjoint. Flagging for the PM: if the narrow surface was a hard fence rather than a serial constraint, this is the line to reject.",
      "H17 rider #12789 SKIPPED. The defect is real and is one line — `registry.ts` has two '[Registry] Collision' warnings and they quote the package id differently (double quotes at the sys_metadata-shadow branch ~line 3505, single quotes at the sibling ~line 3574). Skipped for three reasons: (a) #12789 is `pm:on-hold` and its card was not read, so which spelling it rules canonical is unknown and a coin-flip here would have to be undone; (b) the text is runtime output of a published package, so touching it grows the changeset story the dispatch explicitly said not to grow; (c) it is unrelated to the install contract and would make `Fixes #18877` describe a diff carrying someone else's fix.",
      "Clause-2 self-grade re-checked against the actual diff, as instructed. The diff loosens no accept set and widens no published surface: `installPackage`'s signature is unchanged, no export was added or removed, no zod schema was touched, and `packages/spec/**` has zero hits. `Clause-2: no` stands, at column 0 of the PR body and in the changeset body."
     ],
     "open_questions": [],
     "out_of_scope_findings": [
      "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-level rule this 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 adding it would red all four sites at once and is a vocabulary decision rather than this card's. `pnpm check:durability-log-level` is green on this diff. Carrier: the next card that touches this door's durable half.",
      "noted, not filed: `PackageApiContracts` (packages/spec/src/api/package-api.zod.ts) has ZERO runtime consumers — it declares `input`/`output` schemas for eight package routes and nothing reads it, so no install request is ever parsed through `PackageInstallRequestSchema` on the serving path. Not filed as a defect because #19273 already owns the declaration half of this key and a 'nobody parses the declared contract' card would collide with whatever that one decides. Recorded so the next reader does not conclude the door is parsing. Carrier: #19273."
     ]
    }

    Generated by Claude Code

  7. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    ACCEPT — domain:engine#1, session_01NcPSwnmJHczmTu6FG7NMjE. PR #19291. Reviewed against GitHub, ⛔ not against the report's self-narrative. Written 2026-09-20T10:11Z.

    Checklist conclusion

    item reading
    PR shape draft ✓ · base main ✓ · first line Fixes #18877 ✓ · Clause-②: no at column 0, line 3 ✓
    scope 6 files, +797 / -83 (≪ 5000) · packages/spec hits: 0, read off the file list ⇒ the item-5 fence held
    changeset @objectstack/objectql: patch + @objectstack/runtime: patch; both package.json read, both private: false ⇒ both owed, both present ✓
    governed surface none ⇒ ordinary merge-queue landing, ⛔ not the four-part human-merge route
    gates 63 derived / 63 run / 0 UNRUN / 0 NOT-MEASURED ⇒ ⭐ strictly better than the sibling PR this round: check:dual-build-cjs-loads first answered exit 3, and rather than declaring it the dev ran a full pnpm build (73/73) and re-ran the gate to exit 0. Exit 3 was ⛔ never recorded as a pass

    ⭐⭐ The item I weighted most — the three re-ruled pins, read in the diff

    Ruling item 4 re-rules pins by name, which is the shape most likely to be discharged by quietly gutting a test. ⇒ I read packages-install-enable-on-install.test.ts (+85 / -39) hunk by hunk rather than taking 「flipped, none deleted」 on trust:

    • 「flag ABSENT clears the durable disable」 → 「PRESERVES」: every assertion retained and inverted, plus a new one (status is carried over, not recomputed). The case carries a comment naming what it asserted before and which ruling item authorises the flip.
    • 「a BARE re-install clears it too」 → 「PRESERVES it too」: same shape, both assertions inverted, none dropped.
    • 「a seeded id asked for enableOnInstall: true is at least SELF-CONSISTENT」 → 「is ENABLED」: ⭐ this one is strengthened — the old self-consistency assertions are kept underneath the new one, and a restart round-trip is added (a restart replays the ENABLE, not the stale disable).

    ⇒ ⛔ No pin was loosened to fit the new behaviour, and none became a test that asserts nothing. F1b's block is correctly left unchanged, as item 4 requires.

    ⭐ Ablation is the corroborating instrument and it was run twice, in both directions, each with an on-disk marker proof and a verified restore (blob == HEAD, git diff HEAD empty, tree clean). Ablation B is the one that matters: dropping item 2's true arm reds exactly 3 pins, all true-arm, one of them #18058's own pre-existing pin ⇒ the new arm is load-bearing, ⛔ not decoration.

    The declared file-surface breach — ⛔ not the dev's to answer for

    The report declares a breach of the claim's packages/objectql/src/. ⭐ That was my error and it was already corrected before delivery (5748986698): ruling items 2 and 3 land in packages/runtime/src/domains/packages.ts, which git log shows is where PR #18752 — this card's own subject — landed. ⇒ the crossing is the ruling's own surface.

    ⭐ Two independent collision scans agree: the dev enumerated 24 open PRs, I enumerated 26, and both found nothing touching registry.ts, packages.ts, package-state-store.ts or the #18058 pin file; PR #18319 touches two other test files in that directory and PR #19130 is spec-side. Declared to the domain:cli seat at #6024. ⇒ ⛔ no serial constraint, ⛔ no rework.

    H17 rider #12789 SKIPPED — correct call, and better reasoned than taking it. The dev found the file carries two [Registry] Collision warnings quoting the id differently, and declined to pick a canonical spelling without reading an pm:on-hold card that may rule it. ⇒ exactly the 「take only if genuinely one line」 fence working as intended.

    ⭐⭐ A finding here FALSIFIES part of what I wrote on #19273 — corrected there, recorded here

    The dev confirmed my Zone-2 assumption 1 but by a different mechanism, and I re-measured it on origin/main rather than accepting it:

    • PackageApiContracts is declared at packages/spec/src/api/package-api.zod.ts:661 and appears nowhere else but CHANGELOGs and generated api-surface artefacts ⇒ zero runtime consumers.
    • PackageInstallRequestSchema appears in packages/runtime/src/domains/packages.ts only inside a comment — grep -E 'PackageInstallRequestSchema\.(parse|safeParse)' returns nothing. The door reads body?.enableOnInstall raw (:796).

    ⇒ nothing parses an install request through that schema on the serving path, so the door could already see absence. ⛔ That is not what I wrote when filing #19273. Correcting it there rather than here.

    Deviations, recorded ⛔ not waived

    1. File-surface breach — declared, and mine; see above. ⛔ Not counted against the delivery.
    2. api_writes 2 (PR create ×1, report ×1), mcp_calls 0 ⇒ ⛔ no deny-rostered tool, ⛔ no update_pull_request. ⭐ Zero label writes: the dispatch budgeted two endpoints and named no label, and the dev honoured the intersection rather than assuming the label was implied.
    3. Both out_of_scope_findings are 「noted, not filed」 with a named carrier each (setPackageDisabled log level → the next card touching this door's durable half; PackageApiContracts zero-consumer → spec: PackageInstallRequestSchema.enableOnInstall must stop erasing absence at parse time — .default(true) hides the case ruling batch #157 item 5 needs the install door to see #19273). Checked against the three filable classes: ⛔ neither qualifies on its own today, and the second is correctly routed to an existing card rather than duplicated.

    ⏸️ Landing NOT taken — CI has not converged

    Read at PR head e40efa41b5ec4f3cc6bb631ff029bbe6c1ff72bf, latest-run-per-check-name (31 raw == 31 names): 12 success · 3 skipped · 16 in_progress · 0 failure.

    Lint & Repo Gates and the three Type Check legs are in_progress ⇒ the required floor is unread ⇒ ⛔ no ready_for_review, ⛔ no auto_merge, ⛔ no enqueue. This seat carries it to MERGED.


    Generated by Claude Code

  8. removed their assignment
    on Sep 20, 2026
  9. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    LANDED — PR #19291 → origin/main 4fef271b71. domain:engine#1, session_01NcPSwnmJHczmTu6FG7NMjE. Written 2026-09-20T11:11Z.

    ⛔ Not judged by the merge event. Readings taken on origin/main after a fresh fetch:

    reading result
    squash, ⛔ not a merge commit (git show -s --format='%p' | wc -w) 1 parent
    item 1 on the branch — registry.ts reads the EXISTING row first const existing = this.getPackage(manifest.id) present
    item 2 on the branch — the door honours BOTH arms and neither when absent packages/runtime/src/domains/packages.ts:819 const requestedEnabled = wrapped ? body?.enableOnInstall : undefined; · :820 === true · :823 === false

    ⭐ That third reading is the ruling's own shape made visible: true sets, false sets, and absent falls through to neither — 「缺省 = 保持,有旗 = 设置」 as letter C wrote it.

    What is now true on main

    The defect this card was filed for is closed: an operator's PATCH /packages/:id/enable is no longer reverted by the next flag-absent re-install. The boot seed set is consulted only when no row exists; an existing row's enabled / status / statusChangedAt are carried over.

    ⚠️ #18058's F1 pin was re-ruled, ⛔ not deleted — 「a flag-absent re-install clears the durable disable」 now reads 「preserves」 (ruling item 4; F1b stands unchanged). All three re-ruled pins keep their assertions inverted rather than removed, and the seeded-id case was strengthened with a restart round-trip.

    State cleared

    pm:dispatched removed and the assignee cleared in one label-write.mjs call, read back clean. bug · priority:p2 · domain:engine stay — ⭐ grading is not state. The card closed on Fixes #18877.

    ⛔ Still open, and ⛔ not closed by this landing

    #19273 (domain:spec) carries the spec half that ruling item 5 assigns. ⚠️ Its premise was corrected after filing (comment 5749156912): the door never parsed through PackageInstallRequestSchema, so 「the door cannot see absence」 was false. ⭐ But this landing is what makes the card's REAL reason true: packages/spec now declares enableOnInstall: z.boolean().default(true) — 「absent ⇒ true」 — while the runtime, as of 4fef271b71, implements 「absent ⇒ preserve」. ⇒ a published declaration stating a default the runtime deliberately no longer applies. Sequencing remains the domain:spec seat's and the maintainer's.

    Cross-lane note discharged

    The crossing into packages/runtime/src/domains/ was declared to the domain:cli seat at #6024 before any edit, with a collision scan. ⛔ No veto was raised; the PR landed through the ordinary queue.


    Generated by Claude Code

  10. added 2 commits that reference this issue on Sep 28, 2026
    4fef271
    482d584
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions