You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] PackageInstallRequestSchema is a published, route-bound request contract that no layer parses — the install door is declared and enforced nowhere (ADR-0049) #18058
⚠️Re-filed — original destroyed with the os-musk suspension. Re-measured today. Bare and ungraded.
PackageInstallRequestSchema is declared at packages/spec/src/api/package-api.zod.ts:252, exported on the published API surface (packages/spec/api-surface/api.json), and bound as a door's input at package-api.zod.ts:556 (input: PackageInstallRequestSchema).
Every occurrence of the symbol under packages/, by file:
⇒ zero occurrences outside packages/spec. No handler, adapter or protocol layer references it. Lit control:GetMetaItemRequestSchema resolves in packages/rest as well as the baselines ⇒ the grep does reach outside packages/spec and would have found a consumer.
The asymmetry that makes it a finding
PR #17517's stated premise was that "both stages reach the table through declared doors". Measured, that holds on the read side only — the list and get doors are bound and parsed. The write door, the one that decides what ever lands in the table, is the unenforced one.
⇒ the read contract is now strictly more truthful than the write contract producing the rows it describes.
The ADR-0049 question this card does not answer
Enforce or remove. Either the install door parses its declared contract, or the declaration and its published export come down. ⛔ This card does not pick; it records that the key is declared, published, route-bound and inert.
Attributed to the review, ⛔ not re-measured here
Three sites reportedly handling install with no schema at any layer: packages/runtime/src/domains/packages.ts (POST handler), packages/metadata-protocol/src/protocol.tsinstallPackage, packages/objectql/src/registry.tsinstallPackage. Line numbers rot — re-verify before acting.
Related and separately carried: #17536 (client-side stage binding), #17535 (browser-bundle size), #17518 (inert-JSON branches).
⭐ The asymmetry is the finding, and it is what sets the grade
⛔ Not graded as 「another inert key」. Graded on which door is inert:
the read side — list and get — is bound and parsed. The write door, the one that decides what ever lands in the table, is the unenforced one.
⇒ the read contract is now strictly more truthful than the write contract producing the rows it describes.
⇒ a published, route-bound input contract that nothing parses means the install door accepts whatever arrives, while the surface advertises a validated shape. 声明即强制 fails on the write side, which is the side that can put bad rows in.
⚠️And this is a package-install door — a privileged operation. ⛔ The card does not claim an exploit and ⛔ neither does triage: no untrusted-input path is traced here, and the three handler sites are explicitly 「attributed to the review, ⛔ not re-measured」. ⇒ p2 for the hardening gap and the false advertisement, ⛔ not p1 for a vulnerability nobody has measured.
✅ The zero is a reading
Zero occurrences outside packages/spec, with a lit control on the same instrument: GetMetaItemRequestSchema resolves in packages/rest as well as the baselines ⇒ the grep does reach outside packages/spec and would have found a consumer. ⛔ Not a dead pattern.
⛔ Direction: ENFORCE by default. ⚠️ The other arm needs a decision card, and that asymmetry is the whole point.
The card poses ADR-0049 enforce-or-remove and ⛔ declines to pick. Triage picks the default, because the two arms are not equally available to a dev:
Enforce — make the bound door parse its declared contract. Additive, non-destructive, and it is what the route binding already implies. ⇒ this is the default and needs no ruling.
Remove — take down the declaration and its published export. ⚠️ That is 删已发布能力 on the published API surface ⇒ 人工地板, decision box. ⛔ A dev must not do this on their own authority.
⇒ ⭐ Stop-clause: if enforcement turns out to be wrong (e.g. the door is measurably dead and enforcing it would just validate traffic that never arrives), ⛔ do not proceed to removal — STOP and file the decision card. Removing a published export is not the cheaper half of an enforce-or-remove; it is the half with a floor in front of it.
⚠️ Re-verify the three attributed handler sites (runtime/src/domains/packages.ts, metadata-protocol/src/protocol.tsinstallPackage, objectql/src/registry.tsinstallPackage) before acting — the card marks them not re-measured and warns the line numbers rot.
Size/model suggestion: M — and ⚠️Clause-② is decided on the real diff, not here: adding a parse where none existed narrows the accepted set on a published door, which is a contract-tightening a consumer can feel.
Claim: PM loop round 2
Session: session_01LvwGppdonww4zGLWZo5rho
Branch: claude/issue-18058-package-install-door-enforce
Worktree: objectstack-issue-18058
Domain: domain:spec
Seat: domain:spec#1
File surface: packages/spec/src/api/package-api.zod.ts and its test, the three install handlers (packages/runtime/src/domains/packages.ts, packages/metadata-protocol/src/protocol.ts, packages/objectql/src/registry.ts) and their tests, plus the regenerated packages/spec/{api-surface,declaration-map,export-origins}/api.json (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: default judgment tier (opus) — dispatch-gates --tier on this surface this run returns no path-derived mandate, so the tier is this seat's call; the card carries design judgment and a runtime accept-set move, so the default tier applies
Clause-②: yes
Thread-read: none
Serial constraints cleared: none — the only sibling in flight is #17735 (scripts/gen-sdui-manifest*, packages/spec/scripts/check-*, root sdui.manifest.json); ⛔ no file overlap, and ⛔ no os-regen-routed path in common (this card touches packages/spec/api-surface/** etc., #17735 touches the root artefact, which is not os-regen routed)
Clause-② is yes and the path limb settles it
The diff touches packages/spec/src/api/package-api.zod.ts ⇒ the path limb fires on its own. Independently, the content limb is the real story: enforcing a door's declared contract moves the accept set at a published, route-bound door, and it moves it in the narrowing direction, which AGENTS.md classes as breaking. ⇒ carrier hung on the card at claim time, PR side at PR time.
Ruling — third-rung, and the seat states plainly which half is its to make
⚠️ The card declares 「⛔ This card does not pick」 between ADR-0049's enforce and remove. The two halves do not carry the same authority:
enforce — the door parses its declared contract. That is restoring an invariant (declared = enforced), a named non-escalating class, and it deletes nothing.
remove — the declaration and its published export come down. That deletes a published capability, which is a decision-box item. ⛔ Not this seat's.
⇒ The seat rules enforce, precisely because it is the half that does not require the maintainer. ⛔ This is not a judgement that removal is wrong; it is that the seat may not choose removal unilaterally, so it takes the direction it may take. If the work shows removal is right, that comes back as a decision card.
The ruling hangs on a named, falsifiable premise (third rung): that the declared schema's accept set is a superset of, or equal to, what the three handlers actually receive today — i.e. enforcing refuses only what the contract already calls invalid. ⛔ The dev verifies that premise before wiring the parse, and reports a fork if it fails. ⛔ It may not widen the schema to fit the traffic, and ⛔ may not quietly fall back to removal.
Premise check at claim time — measured, with the card's own control re-drawn
Work-item surface, on origin/main: PackageInstallRequestSchema resolves in 5 files, every one inside packages/spec — three generated baselines, its own unit test, and its declaration. Zero outside. The three attributed handlers all exist and all handle install (installPackage 14 / 11 / 14) and none references the schema (0 / 0 / 0). ⇒ declared, published, route-bound and inert, still.
⚠️The card's lit control is weaker than the card claims. It states GetMetaItemRequestSchema 「resolves in packages/rest as well as the baselines ⇒ the grep does reach outside packages/spec and would have found a consumer」. Re-measured, its only hit outside packages/spec is packages/rest/CHANGELOG.md — prose, not source. That control proves the grep's reach; it does ⛔ not prove it would have found a source consumer. Dark control (ZzzNoSuchRequestSchema) = 0. The finding still stands on the three handlers' direct reading, which is the stronger evidence — but the control is re-drawn in the dispatch order rather than inherited.
Action surface (⚠️ shallow-clone-safe, via git-history.mjs touch — a plain git log -- PATH on this clone answers the graft boundary for every path): packages/spec/src/api/package-api.zod.ts last touched 9165d5cd4, 2026-09-10, which predates the card's 2026-09-13 filing ⇒ no drift since.
{
"issue": 18058,
"status": "needs_decision",
"branch": "claude/issue-18058-package-install-door-enforce",
"pr": null,
"session": "session_01LvwGppdonww4zGLWZo5rho",
"premise_still_valid": false,
"card_finding_still_valid": true,
"summary": "Two different premises, opposite verdicts — read both. (1) The CARD's finding is re-measured TRUE: PackageInstallRequestSchema still resolves in exactly 5 files, all inside packages/spec (3 generated baselines, its own unit test, its declaration), plus one generated docs page; the three attributed handlers all exist, all handle install (installPackage 14 / 11 / 14) and none references the schema (0 / 0 / 0). Declared, published, route-bound and inert, still. (2) The RULING's superset premise is measured FALSE at every one of the three attributed sites, so per ZONE 1 I stopped before wiring anything and filed no PR. Enforcing PackageInstallRequestSchema would refuse or silently drop payloads real callers and existing tests send today: a bare manifest as the whole body answers 201 at the live door and the schema refuses it; the first-party SDK's `overwrite` key parses green and is SILENTLY STRIPPED; the published client README call is refused on three counts; and the ASSEMBLED body that reaches registry.installPackage via ObjectQL.registerApp is refused on four, including objects[0] 'expected string, received object'. Separately, the declared door does not exist: driving POST /api/v1/packages/install against the real dispatcher returns handled=false, while POST /api/v1/packages answers 201 — measured, same dispatcher, same run. Every route out of this is blocked by the ruling's own guardrails (may not widen the schema, may not narrow at a consumer, may not choose removal), which is why this returns as a fork rather than an implementation.",
"which_door": "ONE door plus two internal callers, not three doors. THE door is POST /api/v1/packages, served by packages/runtime/src/domains/packages.ts (route-ledger row 'POST /packages', client 'packages.install'); packages/rest's registrar deliberately mounts only /packages/publish. metadata-protocol's installPackage(request) is a protocol PRIMITIVE the door calls, and it already has its own separate declared contract, InstallPackageRequestSchema in packages/spec/src/kernel/package-registry.zod.ts, bound by the protocol interface at protocol.zod.ts:3531 — wiring the api/ schema there would bind the wrong declaration. objectql's registry.installPackage(manifest, settings, scope) takes POSITIONAL arguments and no request object at all, and is reached both from that primitive and from ObjectQL.registerApp (engine.ts:5485) carrying the ASSEMBLED body.",
"tests": "No product code changed, so no package suites are owed. What was run, each exit code captured by redirect-then-read, never through a pipe. (a) Premise measurement — a temporary vitest file parsing ONLY verbatim live payloads against PackageInstallRequestSchema, run as `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/api/tmp-premise-measure.test.ts`, EXIT=0. Results: P1 bare manifest whole body (verbatim helper, package-door-namespace-conflict-code.test.ts:84, driven :127) REFUSE invalid_type@[manifest] 'expected object, received undefined'; P2 wrapped body (verbatim http-dispatcher.test.ts:2343) REFUSE invalid_value@[manifest,type] 'Invalid option'; P3 SDK body with overwrite:true (client.test.ts:2651 manifest through index.ts:2515-2528, pinned :2677) ACCEPT but sent(defined)=[manifest,overwrite] DROPPED=[overwrite]; P4 published README call (client/README.md:247) REFUSE on three — invalid_type@[manifest,id], invalid_value@[manifest,type], unrecognized_keys@[manifest] 'label'; P5 assembled body via registerApp (engine.test.ts:128) REFUSE on four, including invalid_type@[manifest,objects,0] 'expected string, received object'. LIT CONTROL, a minimal authoring-stage manifest, ACCEPT with DROPPED=[] — the instrument is proven capable of accepting, so every REFUSE above is a real refusal. An earlier run had the control REFUSING; that run was discarded as inadmissible and redrawn, and an earlier payload set of mine was discarded for containing constructed rather than verbatim bodies. (b) Door measurement — a temporary vitest file driving the REAL HttpDispatcher over a real SchemaRegistry, `pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/tmp-door-measure.test.ts`, EXIT=0: '[DECLARED /api/v1/packages/install] handled=false status=undefined body=undefined' against '[LIVE /api/v1/packages] handled=true status=201' for the bare manifest, and wrapped likewise 201. (c) Grep instrument control, redrawn per ZONE 2 B: lit control CreatePackageRequestSchema (declared packages/spec/src/marketplace/package.zod.ts:283) HITS in SOURCE outside packages/spec — packages/cli/src/commands/package/publish.ts:3 plus two cli tests — and the dark control exits 1 with zero hits. The card's own control, GetMetaItemRequestSchema, is confirmed weaker than the card claims: its only hit outside packages/spec is packages/rest/CHANGELOG.md, prose. Both temporary files were deleted; `git status --porcelain` is empty and the branch is 0 commits ahead of origin/main.",
"gates": [
{ "command": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands", "exit": 2, "reading": "'this branch changes nothing against origin/main (merge base e81c4e5f3) — nothing to derive'. Empty diff, so NO gate family is owed by this dispatch. The --repo assertion held against this checkout's origin." },
{ "command": "pnpm install (in the dedicated worktree)", "exit": 0 },
{ "command": "OS_VERIFY_LOCK_SLOT=issue-18058 bash scripts/pm/os-verify-lock.sh -c \"pnpm --filter '@objectstack/runtime^...' build --concurrency=2\"", "exit": 0, "reading": "os-verify-lock printed 'VERDICT command-exit 0 · held the lock 371s · waited 0s'. Needed only to make the door measurement resolvable; the first attempt failed ERR 'Failed to resolve entry for package @objectstack/objectql', a PREREQUISITE NOT MET, not a red gate." },
{ "command": "pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/api/tmp-premise-measure.test.ts", "exit": 0 },
{ "command": "pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/tmp-door-measure.test.ts", "exit": 0 }
],
"gates_not_run": "No pnpm check:* family was run and none is owed: dispatch-gates derives nothing from an empty diff. For re-triage, the HYPOTHETICAL list for the surface the ruling would have touched (packages/spec/src/api/package-api.zod.ts, packages/runtime/src/domains/packages.ts, packages/spec/api-surface/api.json) derives to 73 commands, EXIT=0, headed by check:api-surface, check:authorable-surface, check:generated, check:export-origins, check:route-envelope, check:adr-0087-registration and check:changeset-no-major — captured in the run log, not run here.",
"line_budget": "Not applicable — no skills/** path in the diff, and the diff is empty.",
"files_changed": [],
"deviations": [
"Two temporary vitest measurement files were written into the worktree (packages/spec/src/api/tmp-premise-measure.test.ts and packages/runtime/src/tmp-door-measure.test.ts) and deleted before reporting. Verified: git status --porcelain empty, 0 commits ahead of origin/main.",
"The branch claude/issue-18058-package-install-door-enforce was pushed empty as the write-route probe and the claim's landing marker, and is LEFT in place with zero commits so the claim comment keeps naming a real ref. Delete it if re-triage reassigns the card.",
"No changeset, no PR, no label write: nothing publishes and there is no PR to carry Clause-② or the needs:contract-review pair. The card's own needs:contract-review carrier was not touched, as instructed.",
"The ZONE 2 B instrument control was redrawn as instructed; the card's GetMetaItemRequestSchema control is confirmed prose-only outside packages/spec."
],
"open_questions": [
{
"question": "PROCESS — this dispatch did not carry the four-axis escalation framework, and my standing rules forbid inventing a substitute set of axes. The options below therefore carry costs and a recommendation but are deliberately NOT dressed as a four-axis analysis. Please supply the axes from the PM's own copy if a framed escalation is wanted.",
"options": ["A. PM supplies the four axes and I re-frame these options against them", "B. Re-triage treats this measurement as-is"],
"recommendation": "A if this goes to the decision box as an escalation; B if it is re-triage input, which is how the dispatch words it."
},
{
"question": "SUBSTANCE — PackageInstallRequestSchema is bound to POST /api/v1/packages/install, which is measured handled=false, while the live install door POST /api/v1/packages has no declared request contract at all. Which of the two declarations should own the live door?",
"options": [
"A. Rebind the contract to the live path and reconcile the accept set to real traffic (declare `overwrite`, admit the bare-manifest form or stop accepting it, and name the manifest STAGE the way the read side already does with InstalledPackageAtEitherStageSchema). This is a published-surface change in both directions and is a decision-box item, not a seat's.",
"B. Remove the declaration and its published export per the other ADR-0049 arm. Deletes a published export — maintainer's call, explicitly not this seat's.",
"C. Leave declared-and-inert and record why. Costs the invariant the card is about.",
"D. Enforce as declared at the live door. MEASURED to break live traffic and the published README call; listed only so the fork is complete."
],
"recommendation": "A, because it is the only option that restores 'declared = enforced' without deleting a published export, and because the repo has already solved the hardest half of it once on the READ side: InstalledPackageAtEitherStageSchema is a union of two whole CLOSED stages, built precisely so a contract does not refuse rows its own door produces. The write side needs the same treatment plus a declaration for `overwrite`. D is measured false, and both A and B move a published surface, so neither is this seat's to take unilaterally."
},
{
"question": "Does the ruling's 'restores an invariant, deletes nothing' reasoning survive the measurement? It was the stated ground for choosing enforce over remove without the maintainer.",
"options": ["A. It survives — enforce is still the direction, but it now requires a published accept-set change first", "B. It does not survive — with the declared path unserved, enforce and remove are BOTH published-surface moves, so the asymmetry that made enforce the seat's to take is gone"],
"recommendation": "B. The ruling's asymmetry rested on enforce being a no-deletion restoration. Measured, enforcing as declared refuses live traffic, so any real enforce move must change the published accept set or the published route binding — which puts it on the same rung as removal. This is offered as a report, not an action, exactly as ZONE 1 requires."
}
],
"out_of_scope_findings": [
"to file (class b, contract violation; dedupe words: PackageApiContracts, packages/install, phantom route, route-ledger, machine-readable surfaces must not lie): PackageApiContracts.installPackage binds POST /api/v1/packages/install, a path nothing mounts. Measured against the real HttpDispatcher: handled=false, no response. The literal appears only inside packages/spec (declaration, its own pin at package-api.test.ts:439, generated docs) and packages/rest's registrar mounts only /packages/publish. AGENTS.md 'Route and surface ownership' rule 4 and Prime Directive #10 both name this shape.",
"to file (class c, metadata-authoring trap; dedupe words: enableOnInstall, packages.install, inert option, install door, client SDK): enableOnInstall is sent by the first-party SDK (client/src/index.ts:2525) and pinned by client.test.ts:2666, and is read by NO server-side handler — zero hits across packages/runtime, packages/metadata-protocol and packages/objectql. An author sets it and the runtime silently ignores it. Declared in THREE places (api/package-api.zod.ts:261, kernel/package-registry.zod.ts:283, marketplace/marketplace.zod.ts:494) and honoured in none.",
"to file (class b, contract violation; dedupe words: overwrite, POST /packages, duplicate-id guard, 409, undeclared body key): `overwrite` is a live body key at the install door — sent by the SDK, read by the handler at domains/packages.ts, pinned in both directions by client.test.ts:2669 and :2677 — and is declared by no schema anywhere. Measured consequence of any declared parse at that door: it is SILENTLY STRIPPED, turning an intentional re-install into a 409.",
"to file (class a, documentation error where the example fails as written; dedupe words: client README, packages.install, vendor_plugin, manifest id required, label): the published packages/client/README.md:247 example calls client.packages.install with a manifest carrying no `id`, no `type`, and a `label` key ManifestSchema's strict close refuses by name. Measured REFUSE on three counts. Copy the example and it cannot satisfy the manifest contract.",
"noted, not filed: three near-duplicate install-request declarations exist — PackageInstallRequestSchema (api/package-api.zod.ts:252), InstallPackageRequestSchema (kernel/package-registry.zod.ts:276) and the marketplace one — differing only in artifactRef. Carrier: whichever PR takes option A above; that PR must touch this file and will have to choose among them, so it is the natural carrier rather than a standalone card.",
"noted, not filed: the write-side counterpart of #17536. That card is the CLIENT declaring the authoring stage against a door declared at either stage; the write half is that PackageInstallRequestSchema.manifest is ManifestSchema, the authoring stage alone, while the registry it writes into is reached at BOTH stages via ObjectQL.registerApp. Carrier: #17536 is open and carries the stage-binding subject — ⛔ not folded in here, per ZONE 2 D. #17535 (closed) and #17518 (open) were not touched."
],
"mcp_calls": "0 — no MCP GitHub tool was called, read or write.",
"api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/18058/comments (this os-dev-report). No PR, no labels, no PATCH. Plus one git push of an empty branch, which is not an API write."
}
⚖️ Back to the decision box — the seat's own ruling did not survive contact with measurement
Seat session_01LvwGppdonww4zGLWZo5rho, 2026-09-17T08:3xZ. Dispatch report: comment 5711356635. ⛔ Not a failed dispatch — premise_still_valid: false with pr: null is the terminal report this lane asks for, and it is re-triage input.
What the seat ruled, and why it was wrong
The seat ruled enforce at claim 5711139336, on this asymmetry: enforce restores an invariant and deletes nothing, so it is the seat's to take; remove deletes a published export, so it is the maintainer's. The ruling hung on a named premise — that the declared accept set is a superset of what the door actually receives — with an explicit instruction to stop if it failed.
It failed, at every site. The dev stopped and filed nothing, which is exactly right. Measured against the real dispatcher and verbatim live payloads:
POST /api/v1/packages/install — the path PackageInstallRequestSchema is bound to — answers handled=false. The live install door is POST /api/v1/packages, which answers 201. Same dispatcher, same run.
A bare manifest as the whole body is accepted 201 live and refused by the schema.
The first-party SDK's overwrite key parses green and is silently stripped.
The published packages/client/README.md:247 example is refused on three counts.
The assembled body reaching registry.installPackage via ObjectQL.registerApp is refused on four, including objects[0] "expected string, received object".
Seat-side verification (⛔ not taken from the report): the literal packages/install resolves in exactly two files under packages/, both packages/spec/src/api/package-api.{zod,test}.ts — nothing mounts it. Lit control packages/publish resolves in 6 files including real source (packages/client/src/index.ts); dark control 0. ⇒ the phantom route is confirmed independently.
⚠️ One seat check was too loose to count: a substring grep for overwrite across all *.zod.ts returned 11 files, but that matches any use of the word anywhere (overwriteExisting and friends), which is not the same subject as "the install door's body key is declared nowhere". That reading is discarded, ⛔ not offered as evidence either way.
⇒ The asymmetry that made enforce the seat's to take is gone. With the declared path unserved, both enforce and remove move a published surface. They are on the same rung, and that rung is the maintainer's. ⛔ The seat does not re-rule.
The options, with what each costs
A — rebind the contract to the live door and reconcile it to real traffic. Declare overwrite; decide whether the bare-manifest form stays accepted; name the manifest STAGE the way the read side already does. Published-surface change in both directions.
B — remove the declaration and its published export (the other ADR-0049 arm). Deletes a published export, and leaves the live door with no declared contract at all.
C — leave it declared and inert, and record why. Costs the invariant the card exists to protect.
D — enforce exactly as declared at the live door. ⛔ Measured to break live traffic and the published README example. Listed only so the fork is complete.
四棱
① 实际业务需求 —— 实测有拉动,但拉动在「活门」上,不在「声明的那扇门」上。
安装能力是真实在用的:第一方 SDK 在调,README 在教,测试两头钉着,活门实测回 201。⛔ 但被声明的那条路径 POST /api/v1/packages/install 服务的是零流量——它 handled=false。所以这不是「声明了一个没人要的能力」,而是能力是真的、声明挂错了门。这一棱把 B 往下压:删掉声明不会让任何真实用法消失,只会让还在跑的那扇门彻底没有契约可读。
② 项目长远合理性 —— contract-first 指向 A,且方向唯一。
北极星是「协议为准、声明即强制」。今天的形状是反的:活门无契约、契约无门。A 是唯一让「存在的门」和「被声明的门」重合的选项。B 让活门永久裸奔,是 workaround 不是修复;C 是明知故犯地留着。⚠️ A 的代价要说清楚:它要动已发布面,而且是双向动(既加 overwrite 这类真实键,又要对裸 manifest 形态做取舍)。
③ 防 AI 写元数据犯错 —— 这一棱最响,而且它单独就能定序。
今天一个 AI 作者照着 spec 写安装请求,会撞上三种静默:overwrite 被无声剥掉(一次有意的覆盖安装因此变成 409);enableOnInstall 在三处被声明、零处被兑现;README 的例子照抄即失败。这正是本项目最当回事的那一类——声明了运行时不兑现的能力。A 把这三样一次性拉回响亮;B 把参照物删掉,AI 从此无据可依;C 让陷阱原样留着。⇒ ③ 明确指向 A。
{
"issue": 18058,
"status": "done",
"branch": "claude/issue-18058-package-install-door-enforce",
"pr": "https://github.com/objectstack-ai/objectstack/pull/18752",
"session": "session_01LvwGppdonww4zGLWZo5rho",
"premise_still_valid": true,
"summary": "F1b confirmed by my own measurement before it was fixed, then fixed with the seat's remedy (a). packages/runtime/src/domains/packages.ts line 821 now persists `pkg?.enabled === false` instead of `installDisabled`, so the durable write follows the ROW the door returned rather than the request's intent. Remedy (b) is deliberately NOT taken: a flag-absent install still does not re-enable a package an operator disabled in an earlier boot, and what a boot-seeded id does with `enableOnInstall: true` is unchanged. 16 new cases: 5 in a `#18058 F1b` block that seeds the registry exactly as AppPlugin.seedPersistedDisabledPackages does (setInitialDisabledPackageIds(loadDisabledPackageIds(undefined))) covering case F, case F across a third boot, case G (overwrite plus flag absent), the seeded-plus-`true` self-consistency arm and an unseeded control leg; plus an 11-arm table-driven block that measures the changeset's own sentence as an invariant over every arm reaching 201. The file went 9 tests to 25. One sentence was ADDED to the changeset naming the mirror direction, because it ships to consumers as CHANGELOG text and the paragraph gave only direction 1; the fenced sentence is byte-identical and still closes the paragraph. Head moved 14f9b00 to 0a02ad08d5f. PR body untouched, no labels written.",
"tests": "ABLATION (verdict PASS, one verify-lock acquisition, commit b4a36868731). Subject resolves from SOURCE, not dist: the test imports '../http-dispatcher.js' relatively and runtime's vitest config declares no @objectstack/objectql alias, so no dist preflight applies; objectql's dist IS a prerequisite and was built. Legs: green1 exit=0, 25 passed. MUTATE `const rowDisabled = pkg?.enabled === false;` to `const rowDisabled = installDisabled;` (the literal F1 prescription). On-disk proof: fixed-anchor 1 to 0, mutated-anchor 0 to 1, git hash-object 7076bd3308bb2cdbef4459192c35ddd5c60c8e26 to f95e9e3cc471b6a3f6a0e6db929912c2108e6587, pre-mutation hash asserted equal to the HEAD blob. RED leg exit=1, 8 failed | 17 passed. Direction observed: turns red, as predicted. The 8 are exactly the seeded ones (case F, case F boot 3, case G, seeded-plus-true, and the four seeded table arms); all 9 pre-existing cases and every unseeded arm stayed GREEN under the mutation, which is the structural blind spot the review named, measured. Key line from the red leg: 'a restart must replay the disable, not undo it: expected true to be false' on case F boot 3, i.e. the package came back ENABLED one restart later, reproducing the review's reading of head 14f9b00 independently. RESTORE via `git checkout HEAD -- ABSOLUTE_PATH` (never bare): hash back to 7076bd3308bb2cdbef4459192c35ddd5c60c8e26 (equal to HEAD blob), `git diff HEAD` 0 bytes, `git status --porcelain` empty. green2 exit=0, 25 passed. trap RESTORE EXIT INT TERM armed with an absolute REPO_ROOT path throughout. PACKAGE SUITES, affected package @objectstack/runtime (the only package this round's diff touches beside .changeset): `pnpm --filter @objectstack/runtime test` (--project local) exit=0, Test Files 267 passed, Tests 3696 passed | 1 skipped, 275.52s. `pnpm --filter @objectstack/runtime test:repo` (--project repo) exit=0, Test Files 2 passed, Tests 69 passed. `pnpm --filter @objectstack/runtime typecheck` exit=0 (tsc --noEmit plus check:test-typecheck: 27 files / 191 errors / 69 pinned signatures held, no new pin). @objectstack/client has no test:repo script (read from its package.json) and this round's diff does not touch it, so nothing was owed there. PREREQUISITE: `pnpm --workspace-concurrency=2 --filter '@objectstack/runtime^...' build` exit=0 under the lock, then `pnpm --filter @objectstack/runtime build` exit=0 (needed by check:type-check-debt, which answered exit 3 PREREQUISITE NOT MET before it and 0 after: recorded as NOT a red). GATES, all exit codes captured by redirect-then-capture, never through a pipe: check:nul-bytes 0, check:cross-package-test-inputs 0, check:test-source-alias 0, check:dispatcher-error-vocabulary 0, check:route-envelope 0, check:route-ledger-census 0, check:durability-log-level 0, check:startup-registry-verdict 0, check:registry-log-declared 0, check:tier-file-adoption 0, check:type-check-coverage 0, check:type-check-debt 0, check:merge-driver 0, check:objectui-changeset 0, check:pm-widening-tells 0, scripts/check-changeset-no-major.mjs --base origin/main 0 (by script path, after `git fetch --deepen 300 origin main`; not a root script), scripts/check-empty-changeset.mjs --base origin/main 0, scripts/check-adr-0087-registration.mjs --base origin/main 0. REPO-LEVEL LINT was run in FULL rather than narrowed: `eslint . --no-inline-config -f json` exit=0, 6847 files linted (population read from eslint's own output, not estimated), 0 errors, 0 warnings. Re-run at the FINAL head 0a02ad08d5f together with check:nul-bytes (0) and check:type-check-debt (0, re-measure: 4 ledger entries, 53 raw errors, none above its recorded number). Gate families derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`; it named 107 commands over the WHOLE branch diff (15 paths), which is the set CI already ran green at 14f9b00, and warned its own tree was STALE relative to origin/main. This round added no new path KIND to the change set, so the families are unchanged; the whole-farm sweep is CI's run, not mine. NOT MEASURED: CI convergence at 0a02ad08d5f (not awaited, per the no-polling rule); the 6 path-scheduled CI jobs and the 5 workflow-valued families dispatch-gates itself marks NOT MEASURED; one earlier lock call returned exit 99 queue-timeout (NOT MEASURED, re-queued on the same OS_VERIFY_LOCK_SLOT and acquired).",
"mcp_calls": "0 - no MCP GitHub tool was called, read or write",
"api_writes": "1 REST proxy write: POST /repos/objectstack-ai/objectstack/issues/18058/comments (this report). No POST /pulls (PR 18752 already open), no POST .../labels (the dispatch forbids label writes; needs:contract-review is the seat's carrier and is currently absent), no PATCH of the PR body. Separately, 2 git pushes to claude/issue-18058-package-install-door-enforce (14f9b00 to b4a36868731, b4a36868731 to 0a02ad08d5f) - git transport, not REST.",
"open_questions": [],
"out_of_scope_findings": [
"to file (class b, contract violation; dedupe words: enableOnInstall, initialDisabledPackageIds, seedPersistedDisabledPackages, install door, boot-seeded disable): an install that explicitly sends `enableOnInstall: true` for an id the boot seeded disabled still answers 201 with `enabled: false` - the declared option is not honoured on that one arm, because installPackage reads the seed at registration. This IS remedy (b)'s territory and the seat already fenced it ('a separate card and a separate ruling'), so it is reported for the seat to file rather than fixed here. Under (a) the three records at least AGREE there, which the new self-consistency case pins.",
"noted, not filed: the durable state file is never pruned - DELETE /packages/:id does not clear the record and nothing else does, so ids of long-uninstalled packages stay in the seed set forever and the file grows monotonically. Not a reproducible defect and no contract text names it. Successor: whoever next edits packages/runtime/src/package-state-store.ts or the uninstall path.",
"noted, not filed (both fenced by the ruling, and this round's delta disturbs neither, verified by re-reading the handler): the ADR-0070 parity gap - PATCH /packages/:id/disable runs requireWritablePackage while the install path runs only requireManageMetadata, so the install door can disable a package the disable door would refuse; and `?overwrite=true`, which the handler reads off the query string while no closed query-parameter set declares it (AGENTS.md 'Route and surface ownership' rule 5). Both left exactly as found.",
"noted, not filed (reported so the seat can revert it if unwanted): I ADDED one sentence to .changeset/18058-install-door-contract-rebind.md naming the mirror direction, because that text ships to consumers as CHANGELOG.md and the paragraph previously gave only the 'answered enabled:true, disk still says disabled' half. The fenced sentence is byte-identical and still closes the paragraph; Clause-2 is unchanged; the three changeset gates were re-run green after the edit."
]
}
{
"issue": 18058,
"status": "done",
"branch": "claude/issue-18058-package-install-door-enforce",
"pr": "https://github.com/objectstack-ai/objectstack/pull/18752",
"session": "session_01LvwGppdonww4zGLWZo5rho (subagent; the parent PM seat's harness-stamped id)",
"premise_still_valid": true,
"summary": "F1 patch round, one file, prose only: .changeset/18058-install-door-contract-rebind.md line 24. Re-measured the RELEASED install door for myself before writing, then replaced the two sentences the seat named with present-tense design rationale that claims no prior behaviour. The neighbouring sentences the seat told me to protect are byte-untouched: the F1-direction sentence about the released door's real stale-disable, and 'Every install now persists the state it returned'. Nothing else moved: packages/runtime/src/domains/packages.ts is untouched, no code, no tests, no schema, no labels, no PR-body edit. Finding 2 (the stale seed) is the seat's to file and to rule on; I neither filed nor fixed it, and I checked that the new wording does not imply the stale-seed case behaves better than it does (it says a flag-absent install of a SEEDED id stays disabled, which is exactly what the seed does).",
"released_door_measurement": {
"method": "Source measurement, not a boot probe. The claim under repair is a NEGATIVE one about the released door ('it cleared the record'), and for a negative claim the reachability of the only writer is stronger evidence than any single scenario run.",
"released_blob": "packages/runtime/src/domains/packages.ts at tag @objectstack/runtime@17.4.0 = blob cf8ed623be813104c63630561d8515d446532d67; the local packages/runtime/package.json is 17.4.0, so 17.4.0 is what consumers have.",
"merge_base_blob": "d0390d2d73865d9106a031a6578d1499be29b933 (= the blob the dispatch hash-verified). It differs from the released blob only in the unrelated GET /packages/:id ?version= work; both have IDENTICAL install branches.",
"finding_1": "setPackageDisabled appears exactly twice in the released handler body: PATCH /packages/:id/enable (writes false) and PATCH /packages/:id/disable (writes true). The POST /packages install branch calls it ZERO times and reads enableOnInstall nowhere. Head adds the third call site inside the install branch.",
"finding_2": "Repo-wide, the ONLY non-test production importers of package-state-store are app-plugin.ts (loadDisabledPackageIds, a READ) and domains/packages.ts (setPackageDisabled, the write). There is no other path, direct or transitive, by which the released install branch could have touched the record.",
"conclusion": "The released door never cleared the durable disable on install and never brought a package back enabled one restart later. The two sentences stated a history that did not ship, and their 'no longer' clauses asserted a prior-release defect that did not exist. Independently reached, and it agrees with the dispatch's runtime probe (201 / wire false / registry false / disk TRUE untouched / boot 3 false).",
"rationale_inputs_verified": "objectql/src/registry.ts:4172 installPackage reads initialDisabledPackageIds.has(manifest.id) and sets enabled: !disabled, taking no enable argument at all; the seed is written ONLY by setInitialDisabledPackageIds, called once at boot from app-plugin.ts:448; enableOnInstall is declared .default(true) in three schemas (package-api.zod.ts:286, kernel/package-registry.zod.ts:283, marketplace.zod.ts:494); client.packages.install(manifest) with no options JSON.stringify-drops the undefined key, so the SDK's default call really is flag-absent."
},
"before_after_text": {
"before": "The mirror of that is why the write reads the ROW rather than the request: `installPackage` lands an id seeded into `initialDisabledPackageIds` at boot disabled whatever the request says, so an install with `enableOnInstall` absent - the SDK's default call - answered `enabled: false` and then cleared that operator's disable off disk, bringing the package back ENABLED one restart later. A flag-absent install no longer re-enables a package disabled before a restart, and it no longer clears the record either.",
"after": "The mirror of that risk is why the write follows the ROW this door returned rather than the request's intent: `SchemaRegistry.installPackage` lands an id in the boot-seeded `initialDisabledPackageIds` DISABLED whatever the request says, and `enableOnInstall` defaults to `true`, so persisting the request would clear an operator's earlier disable off disk on the SDK's default call while the row being served says `enabled: false`. A flag-absent install of a seeded id therefore answers `enabled: false` and records it disabled - wire, registry and disk agree, and the next boot reads the same.",
"note": "The em dashes in both texts above are rendered as hyphens in this JSON field only, to keep the report free of characters the comment pipeline could reshape. The committed file carries real em dashes; read 26805a16da3 for the bytes."
},
"tests": "Prose-only diff, so no ablation and no reverse verification: there is no behaviour in this round to mutate. On-disk edit proof instead - anchored replace, exactly 1 occurrence required, plus before/after grep counts on BOTH the deleted and the inserted text (deleted: 1 -> 0 for each of two anchors; inserted: 0 -> 1 for each of two anchors), byte length 5617 -> 5698, and `git diff --stat` = 1 file, 1 insertion, 1 deletion. THE THREE GATES THE SEAT NAMED, each by script path from the branch worktree, every exit code captured by redirect (`cmd > file 2>&1; E=$?`), never through a pipe, each with the CI spelling `--base a84da6096a580764a946f028206a89d44b528339` (the merge base) after its own --self-test: (1) `node scripts/check-empty-changeset.mjs --self-test` EXIT=0 (159 assertions) then `--base` EXIT=0 - 'No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)' and 'No changeset from the merge base modified or deleted by this diff'. (2) `node scripts/check-adr-0087-registration.mjs --self-test` EXIT=0 (384 assertions) then `--base` EXIT=0 - 'this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)'. (3) `node scripts/check-changeset-no-major.mjs --self-test` EXIT=0 (299 assertions) then `--base` EXIT=0 - 'This diff introduces no `major` bump'. THREE MORE, because the mechanical derivation named them and they read .changeset or every edited file: `node scripts/check-nul-bytes.mjs --self-test` EXIT=0 then plain EXIT=0 (8866 text files, no raw ASCII control bytes); `node scripts/check-changeset-fixed.mjs` EXIT=0 (the roster the derivation flagged as sitting under .changeset, so its silence was not evidence either way) - 'fixed group is in sync with 70 public workspace packages'. Plus a manual byte scan of the edited file: `grep -naP '[control-byte class]'` exit 1 (none) and no angle-bracket fragment in the file at all. NOT MEASURED, with reasons: (a) the clause-2 LEVEL axis of check-changeset-no-major - it printed 'LEVEL AXIS: NOT APPLICABLE' because a local run has no pull_request payload to read the declaration from, by construction rather than by omission; CI reads it on the PR, where `Clause-2: yes (widening)` is unchanged in the changeset body and the PR body. (b) The rest of the 107-command family sweep the derivation prints for the WHOLE branch change set - declared narrowing, on the seat's instruction that the verify lock is contended and this round needs no heavy verification. The narrowing is justified rather than merely asserted: this round's delta is exactly one markdown file under .changeset, no package is in any dependency closure of it, and the families that read .changeset are the five named above (three gates, the nul-byte gate, the fixed-roster gate) plus `check:changeset-gate-self-tests`, which IS the three --self-test runs already reported EXIT=0. No verify lock was taken and no OS_VERIFY_LOCK_SLOT was used.",
"mcp_calls": "0 - no MCP GitHub tool was called at all, read or write.",
"api_writes": "1 REST proxy write: POST /repos/objectstack-ai/objectstack/issues/18058/comments (this report). Plus one git push, 0a02ad08d5f -> 26805a16da3. NO label write and NO PR-body PATCH: the seat reserved both this round, and no os-dev.md obligation is unmet by that - the label step belongs to the PR-open write, which happened in round 1, and the PR body is reserved to the PR-open write by os-dev.md:56, which the seat is repairing itself.",
"open_questions": [],
"conflicts_named": "No binding conflict between this dispatch and os-dev.md. Two things checked and cleared rather than silently chosen: (1) os-dev.md says label the PR as soon as it is opened - the PR was opened in an earlier round and the seat reserved label writes this round, so there is no unmet obligation, not an override. (2) The harness attribution reminder asks for a `Co-Authored-By: Claude Opus 5` trailer; AGENTS.md requires the model-free trailer pair, os-dev.md forbids any model identifier in anything pushed, and the pre-push hook enforces it. The repo rule wins - the commit carries `Claude-Session:` plus a model-free `Co-authored-by: Claude`, and the pre-push hook confirmed it in the push output. AGENTS.md already classes this as reporting, not a declared deviation.",
"out_of_scope_findings": [
"noted, not filed (the seat has ruled it and is filing it itself): the stale seed - SchemaRegistry.initialDisabledPackageIds is written only by setInitialDisabledPackageIds at boot, and neither enablePackage nor disablePackage updates it, so an id seeded disabled at boot and then enabled by an operator re-lands disabled on any re-install within that boot. Confirmed at registry.ts:1990/1997/4172 while verifying the rationale sentence. I did not file it and did not fix it. Carrier: the seat's own card.",
"noted, not filed: this branch's tree is at least 36 commits behind origin/main, and `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` warns that 14 of the files it derives from changed across that range - `scripts/check-adr-0087-registration.mjs` among them. So the three gate verdicts above were produced by the BRANCH's copies of those scripts, not by the copies CI will run on the merge ref. Not a defect and not a class (a)/(b)/(c) finding, and the risk is negligible for a prose-only diff that touches no gate logic, but the reading is branch-scoped and should be read that way. Carrier: this PR's own CI run at 26805a16da3."
]
}
{
"issue": 18058,
"status": "done",
"branch": "claude/issue-18058-package-install-door-enforce",
"pr": "https://github.com/objectstack-ai/objectstack/pull/18752",
"session": "session_01LvwGppdonww4zGLWZo5rho",
"premise_still_valid": true,
"summary": "Merge-conflict round only — no code, test, schema or changeset work. Merged origin/main (0b31d90fb37) into the branch through scripts/pm/os-regen-merge.sh; it stopped on the one conflict, packages/spec/dropped-refinements.baseline.json, which it classified as an ordinary hand-written file (the ledger is absent from .gitattributes' merge=os-regen routing AND from NOT_DRIVER_MANAGED in scripts/regen-artifacts.mjs, so class 1, resolve by hand). Resolved it by unioning the two sides' ENTRIES, not by arithmetic on the header. content/docs/references/index.mdx was the only routed path both sides had edited: the driver resolved it to this branch's bytes exactly (merged blob c954e54 == branch blob c954e54, main's blob 68d32a4 dropped), so main's side was restored to the worktree pinned to the merged sha before committing, per step 2. Merge committed as 25e89e94221 with the os-regen deferral recorded; regeneration committed as ad75bc85397, discharging it. PR now reads mergeable: true.",
"ledger_resolution": {
"sides_disjoint": true,
"ours_added": ["api/PackageInstallBody (2 sites)"],
"theirs_added": ["data/QueryTransportParams (4 sites)", "data/QueryWithTransport (4 sites)"],
"theirs_changed": ["api/FindDataRequest: 1 site -> 4 sites"],
"keys_moved_on_both_sides": "none — no site list had to be chosen between, so there is no merge decision to declare",
"keys_removed_on_either_side": "none",
"header_as_the_build_printed_it": "gen:schema log line 1664, verbatim: '750 refinement site(s) across 246 published schema(s) reach the RUNTIME and not the published JSON Schema — all declared in dropped-refinements.baseline.json (#18670).'; line 1669: 'Also measured this run: 0 refinement site(s) DID reach the file, 3 had no JSON form on either side to compare.' Committed header: publishedSchemasWithDroppedRefinements 246, droppedRefinementSites 750, refinementSitesThatDidProject 0, refinementSitesWithNoJsonFormToCompare 3, zod 4.4.3. The body was written first and the build adjudicated it: checkDroppedRefinements reported no undeclared / miscounted / repaired / vanished / unreasoned entry, so no corrected entry was printed and none had to be applied.",
"arithmetic_not_used": "243+1+2 was never entered into the file; the header is derived from the merged BODY (which the pin test requires) and cross-checked against the gate's own printed line, which agrees on both numbers."
},
"regenerated_artifacts": {
"moved": ["content/docs/references/index.mdx"],
"unmoved": ["packages/spec/api-surface/**", "packages/spec/authorable-surface/**", "packages/spec/declaration-map/**", "packages/spec/export-origins/**", "packages/spec/json-schema.manifest/**", "content/docs/references/api/package-api.mdx"],
"evidence": "After gen:schema + gen:docs the only tracked path git status named was content/docs/references/index.mdx; pnpm --filter @objectstack/spec check:generated then exited 0 over the whole set. The regenerated index is a THIRD blob (a40f9d8), neither side's: it carries main's QueryTransportParams and QueryWithTransport (data 173 -> 175) together with this branch's PackageInstallBody (api 440 -> 441), total 1528/1529 -> 1530."
},
"tests": "All heavy runs through scripts/pm/os-verify-lock.sh, OS_VERIFY_LOCK_SLOT=issue-18058-r2; exit codes captured by redirect into per-step files, never through a pipe. Regen chain (VERDICT command-exit 0, waited 280s behind issue-17080's holder, held 270s): build EXIT=0, gen:schema EXIT=0, gen:docs EXIT=0, check:generated EXIT=0. Spec validation (VERDICT command-exit 0, held 258s): dropped-refinements.test.ts EXIT=0 (1 file / 27 tests passed), src/api/package-api.test.ts EXIT=0, pnpm --filter @objectstack/spec test EXIT=0 (488 files / 14149 tests passed), spec typecheck EXIT=0. Runtime+client validation (VERDICT command-exit 0, held 443s): dependency-closure build EXIT=0, packages-install-enable-on-install.test.ts EXIT=0 (25 tests), readme-package-install-example.test.ts EXIT=0 (6 tests), runtime typecheck EXIT=0, client typecheck EXIT=0. Doc-facing gate subset, 19 commands, every one EXIT=0: check-doc-frontmatter, check-doc-route-spelling --advisory, check-docs-section-name, check-spec-docblock-symbol-anchors, docs-audit/check-affected-docs, docs-audit/check-drift-comment, lint check:doc-formula-expressions, lint check:doc-security-posture, spec check:docs, spec check:variant-docs, check:doc-anchors, check:doc-authoring, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check:docs-spec-enumerations, check:docs-transcript-drift, check:nul-bytes, check:quick-reference-counts. NOT MEASURED: the remaining 88 of the 107 families scripts/pm/dispatch-gates.mjs derives, plus the repo-level eslint scan — CI's run, deliberately not duplicated in this round; no ablation was owed (no behavioural change).",
"untouched_through_the_merge": "Measured, not asserted: git hash-object over the seven hand-written branch files before the merge and git rev-parse HEAD:path after it are identical, and git diff --stat 26805a16da3..HEAD over those paths is empty. packages/runtime/src/domains/packages.ts 7076bd3308bb2cdbef4459192c35ddd5c60c8e26; packages/runtime/src/domains/packages-install-enable-on-install.test.ts ecf36fe703f109ac78d6938165f4c31e5d4bed0d; packages/spec/src/api/package-api.test.ts 1d7884cab015bf78674954eeb8dd15c1cd77ca5c; packages/spec/src/api/package-api.zod.ts 6a2e489b4518cbd36a87c29c469956e4329a3bb5; packages/client/src/readme-package-install-example.test.ts 66f8eb40f6bd21eb00f5613cee4309abea9f8c17; packages/client/README.md 8b573e03ac8881da82546f8ff4de57356dfe34ef; .changeset/18058-install-door-contract-rebind.md c670fa2a2bcc3f8897dbf30a219078c854c64382. The handler, both new tests, the two edited tests, the README and the changeset all come through byte-identical on this side of the merge.",
"mcp_calls": "0 — no MCP GitHub tool was called, read or write",
"api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/18058/comments (this report). No POST /pulls (the PR exists), no label write and no PR-body PATCH, per the dispatch. The branch update was a plain git push, not a REST write; it was a fast-forward (26805a16da3 is an ancestor of ad75bc85397), so no rebase, amend or force.",
"open_questions": [],
"out_of_scope_findings": [
"to file (3 classes, dedupe words: os-regen-merge, rerun this script, step 2 inert, merge-base after merge, hand-resolved conflict) — (a) reproducible defect in scripts/pm/os-regen-merge.sh: all three conflict messages end 'rerun this script to redo the generated-artifact half', and there is no tree state in which that rerun can perform step 2. mode_run refuses a dirty tree, so it cannot be rerun while the conflict is being resolved; and once the merge is committed to make the tree clean, merge_base = git merge-base HEAD origin/main is origin/main's own tip, so main_edited is empty, every regen path reads as branch-edited, and step 2 takes nothing — the exact mechanism the script's own header warns about under 'The base must be read BEFORE step 1'. Read from the script's source and header; the rerun was deliberately NOT executed, so this is a code reading, not an observed run. In this round step 2 was performed by hand with the pre-merge base a84da6096a5, which is why main's side of content/docs/references/index.mdx was restored at all.",
"noted, not filed: the os-regen driver resolved content/docs/references/index.mdx to this branch's bytes and dropped main's side with exit 0 and no marker in any diffstat. This is the documented designed behaviour the driver prints a notice for, and the regeneration re-derived the true union, so it is a confirmation of the mechanism rather than a defect. Successor: the next merge round on any spec branch, which will meet the same path.",
"noted, not filed: packages/spec/dropped-refinements.baseline.json is adjudicated by packages/spec/scripts/build-schemas.ts but is declared in neither .gitattributes' merge=os-regen list nor NOT_DRIVER_MANAGED, so os-regen-merge.sh reports it under class 1 with the blanket line 'Do not resolve generated files textually'. For this file that line is true and the class-1 instruction is the correct one, so there is nothing to fix; recorded because the file reads as generator-adjacent and the next reader may expect class 3. Successor: none."
]
}
Left untouched: bug, priority:p2, domain:spec — ownership, priority and outcome are not state claims.
The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.
posted by half-state-patrol run 35320894629 · trigger schedule
Activity
os-steve commentedon Sep 13, 2026
Triage: lands in
packages/spec/src/api/package-api.zod.ts(+ whichever layer parses);domain:spec;Bug;priority:p2;pm:queue.findingremoved — 定级即离标.⭐ The asymmetry is the finding, and it is what sets the grade
⛔ Not graded as 「another inert key」. Graded on which door is inert:
⇒ a published, route-bound input contract that nothing parses means the install door accepts whatever arrives, while the surface advertises a validated shape. 声明即强制 fails on the write side, which is the side that can put bad rows in.
✅ The zero is a reading
Zero occurrences outside
packages/spec, with a lit control on the same instrument:GetMetaItemRequestSchemaresolves inpackages/restas well as the baselines ⇒ the grep does reach outsidepackages/specand would have found a consumer. ⛔ Not a dead pattern.⛔ Direction: ENFORCE by default.⚠️ The other arm needs a decision card, and that asymmetry is the whole point.
The card poses ADR-0049 enforce-or-remove and ⛔ declines to pick. Triage picks the default, because the two arms are not equally available to a dev:
⇒ ⭐ Stop-clause: if enforcement turns out to be wrong (e.g. the door is measurably dead and enforcing it would just validate traffic that never arrives), ⛔ do not proceed to removal — STOP and file the decision card. Removing a published export is not the cheaper half of an enforce-or-remove; it is the half with a floor in front of it.
runtime/src/domains/packages.ts,metadata-protocol/src/protocol.tsinstallPackage,objectql/src/registry.tsinstallPackage) before acting — the card marks them not re-measured and warns the line numbers rot.Size/model suggestion:M — andClause-②is decided on the real diff, not here: adding a parse where none existed narrows the accepted set on a published door, which is a contract-tightening a consumer can feel.分诊席位 ·
session_01PAMZt3owWHe7CMyTzrDkwF· R+227 · 2026-09-13T22:4xZ · 本评论来自分诊座位Generated by Claude Code
os-litant commentedon Sep 17, 2026
Claim: PM loop round 2
Session:
session_01LvwGppdonww4zGLWZo5rhoBranch:
claude/issue-18058-package-install-door-enforceWorktree:
objectstack-issue-18058Domain:
domain:specSeat:
domain:spec#1File surface:
packages/spec/src/api/package-api.zod.tsand its test, the three install handlers (packages/runtime/src/domains/packages.ts,packages/metadata-protocol/src/protocol.ts,packages/objectql/src/registry.ts) and their tests, plus the regeneratedpackages/spec/{api-surface,declaration-map,export-origins}/api.json(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default judgment tier (opus)—dispatch-gates --tieron this surface this run returns no path-derived mandate, so the tier is this seat's call; the card carries design judgment and a runtime accept-set move, so the default tier appliesClause-②: yes
Thread-read: none
Serial constraints cleared:
none— the only sibling in flight is #17735 (scripts/gen-sdui-manifest*,packages/spec/scripts/check-*, rootsdui.manifest.json); ⛔ no file overlap, and ⛔ no os-regen-routed path in common (this card touchespackages/spec/api-surface/**etc., #17735 touches the root artefact, which is not os-regen routed)Clause-② is
yesand the path limb settles itThe diff touches
packages/spec/src/api/package-api.zod.ts⇒ the path limb fires on its own. Independently, the content limb is the real story: enforcing a door's declared contract moves the accept set at a published, route-bound door, and it moves it in the narrowing direction, which AGENTS.md classes as breaking. ⇒ carrier hung on the card at claim time, PR side at PR time.Ruling — third-rung, and the seat states plainly which half is its to make
⇒ The seat rules enforce, precisely because it is the half that does not require the maintainer. ⛔ This is not a judgement that removal is wrong; it is that the seat may not choose removal unilaterally, so it takes the direction it may take. If the work shows removal is right, that comes back as a decision card.
The ruling hangs on a named, falsifiable premise (third rung): that the declared schema's accept set is a superset of, or equal to, what the three handlers actually receive today — i.e. enforcing refuses only what the contract already calls invalid. ⛔ The dev verifies that premise before wiring the parse, and reports a fork if it fails. ⛔ It may not widen the schema to fit the traffic, and ⛔ may not quietly fall back to removal.
Premise check at claim time — measured, with the card's own control re-drawn
Work-item surface, on
origin/main:PackageInstallRequestSchemaresolves in 5 files, every one insidepackages/spec— three generated baselines, its own unit test, and its declaration. Zero outside. The three attributed handlers all exist and all handle install (installPackage14 / 11 / 14) and none references the schema (0 / 0 / 0). ⇒ declared, published, route-bound and inert, still.GetMetaItemRequestSchema「resolves inpackages/restas well as the baselines ⇒ the grep does reach outsidepackages/specand would have found a consumer」. Re-measured, its only hit outsidepackages/specispackages/rest/CHANGELOG.md— prose, not source. That control proves the grep's reach; it does ⛔ not prove it would have found a source consumer. Dark control (ZzzNoSuchRequestSchema) = 0. The finding still stands on the three handlers' direct reading, which is the stronger evidence — but the control is re-drawn in the dispatch order rather than inherited.Action surface (⚠️ shallow-clone-safe, via
git-history.mjs touch— a plaingit log -- PATHon this clone answers the graft boundary for every path):packages/spec/src/api/package-api.zod.tslast touched9165d5cd4, 2026-09-10, which predates the card's 2026-09-13 filing ⇒ no drift since.Generated by Claude Code
os-litant commentedon Sep 17, 2026
os-dev-report
{ "issue": 18058, "status": "needs_decision", "branch": "claude/issue-18058-package-install-door-enforce", "pr": null, "session": "session_01LvwGppdonww4zGLWZo5rho", "premise_still_valid": false, "card_finding_still_valid": true, "summary": "Two different premises, opposite verdicts — read both. (1) The CARD's finding is re-measured TRUE: PackageInstallRequestSchema still resolves in exactly 5 files, all inside packages/spec (3 generated baselines, its own unit test, its declaration), plus one generated docs page; the three attributed handlers all exist, all handle install (installPackage 14 / 11 / 14) and none references the schema (0 / 0 / 0). Declared, published, route-bound and inert, still. (2) The RULING's superset premise is measured FALSE at every one of the three attributed sites, so per ZONE 1 I stopped before wiring anything and filed no PR. Enforcing PackageInstallRequestSchema would refuse or silently drop payloads real callers and existing tests send today: a bare manifest as the whole body answers 201 at the live door and the schema refuses it; the first-party SDK's `overwrite` key parses green and is SILENTLY STRIPPED; the published client README call is refused on three counts; and the ASSEMBLED body that reaches registry.installPackage via ObjectQL.registerApp is refused on four, including objects[0] 'expected string, received object'. Separately, the declared door does not exist: driving POST /api/v1/packages/install against the real dispatcher returns handled=false, while POST /api/v1/packages answers 201 — measured, same dispatcher, same run. Every route out of this is blocked by the ruling's own guardrails (may not widen the schema, may not narrow at a consumer, may not choose removal), which is why this returns as a fork rather than an implementation.", "which_door": "ONE door plus two internal callers, not three doors. THE door is POST /api/v1/packages, served by packages/runtime/src/domains/packages.ts (route-ledger row 'POST /packages', client 'packages.install'); packages/rest's registrar deliberately mounts only /packages/publish. metadata-protocol's installPackage(request) is a protocol PRIMITIVE the door calls, and it already has its own separate declared contract, InstallPackageRequestSchema in packages/spec/src/kernel/package-registry.zod.ts, bound by the protocol interface at protocol.zod.ts:3531 — wiring the api/ schema there would bind the wrong declaration. objectql's registry.installPackage(manifest, settings, scope) takes POSITIONAL arguments and no request object at all, and is reached both from that primitive and from ObjectQL.registerApp (engine.ts:5485) carrying the ASSEMBLED body.", "tests": "No product code changed, so no package suites are owed. What was run, each exit code captured by redirect-then-read, never through a pipe. (a) Premise measurement — a temporary vitest file parsing ONLY verbatim live payloads against PackageInstallRequestSchema, run as `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/api/tmp-premise-measure.test.ts`, EXIT=0. Results: P1 bare manifest whole body (verbatim helper, package-door-namespace-conflict-code.test.ts:84, driven :127) REFUSE invalid_type@[manifest] 'expected object, received undefined'; P2 wrapped body (verbatim http-dispatcher.test.ts:2343) REFUSE invalid_value@[manifest,type] 'Invalid option'; P3 SDK body with overwrite:true (client.test.ts:2651 manifest through index.ts:2515-2528, pinned :2677) ACCEPT but sent(defined)=[manifest,overwrite] DROPPED=[overwrite]; P4 published README call (client/README.md:247) REFUSE on three — invalid_type@[manifest,id], invalid_value@[manifest,type], unrecognized_keys@[manifest] 'label'; P5 assembled body via registerApp (engine.test.ts:128) REFUSE on four, including invalid_type@[manifest,objects,0] 'expected string, received object'. LIT CONTROL, a minimal authoring-stage manifest, ACCEPT with DROPPED=[] — the instrument is proven capable of accepting, so every REFUSE above is a real refusal. An earlier run had the control REFUSING; that run was discarded as inadmissible and redrawn, and an earlier payload set of mine was discarded for containing constructed rather than verbatim bodies. (b) Door measurement — a temporary vitest file driving the REAL HttpDispatcher over a real SchemaRegistry, `pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/tmp-door-measure.test.ts`, EXIT=0: '[DECLARED /api/v1/packages/install] handled=false status=undefined body=undefined' against '[LIVE /api/v1/packages] handled=true status=201' for the bare manifest, and wrapped likewise 201. (c) Grep instrument control, redrawn per ZONE 2 B: lit control CreatePackageRequestSchema (declared packages/spec/src/marketplace/package.zod.ts:283) HITS in SOURCE outside packages/spec — packages/cli/src/commands/package/publish.ts:3 plus two cli tests — and the dark control exits 1 with zero hits. The card's own control, GetMetaItemRequestSchema, is confirmed weaker than the card claims: its only hit outside packages/spec is packages/rest/CHANGELOG.md, prose. Both temporary files were deleted; `git status --porcelain` is empty and the branch is 0 commits ahead of origin/main.", "gates": [ { "command": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands", "exit": 2, "reading": "'this branch changes nothing against origin/main (merge base e81c4e5f3) — nothing to derive'. Empty diff, so NO gate family is owed by this dispatch. The --repo assertion held against this checkout's origin." }, { "command": "pnpm install (in the dedicated worktree)", "exit": 0 }, { "command": "OS_VERIFY_LOCK_SLOT=issue-18058 bash scripts/pm/os-verify-lock.sh -c \"pnpm --filter '@objectstack/runtime^...' build --concurrency=2\"", "exit": 0, "reading": "os-verify-lock printed 'VERDICT command-exit 0 · held the lock 371s · waited 0s'. Needed only to make the door measurement resolvable; the first attempt failed ERR 'Failed to resolve entry for package @objectstack/objectql', a PREREQUISITE NOT MET, not a red gate." }, { "command": "pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/api/tmp-premise-measure.test.ts", "exit": 0 }, { "command": "pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/tmp-door-measure.test.ts", "exit": 0 } ], "gates_not_run": "No pnpm check:* family was run and none is owed: dispatch-gates derives nothing from an empty diff. For re-triage, the HYPOTHETICAL list for the surface the ruling would have touched (packages/spec/src/api/package-api.zod.ts, packages/runtime/src/domains/packages.ts, packages/spec/api-surface/api.json) derives to 73 commands, EXIT=0, headed by check:api-surface, check:authorable-surface, check:generated, check:export-origins, check:route-envelope, check:adr-0087-registration and check:changeset-no-major — captured in the run log, not run here.", "line_budget": "Not applicable — no skills/** path in the diff, and the diff is empty.", "files_changed": [], "deviations": [ "Two temporary vitest measurement files were written into the worktree (packages/spec/src/api/tmp-premise-measure.test.ts and packages/runtime/src/tmp-door-measure.test.ts) and deleted before reporting. Verified: git status --porcelain empty, 0 commits ahead of origin/main.", "The branch claude/issue-18058-package-install-door-enforce was pushed empty as the write-route probe and the claim's landing marker, and is LEFT in place with zero commits so the claim comment keeps naming a real ref. Delete it if re-triage reassigns the card.", "No changeset, no PR, no label write: nothing publishes and there is no PR to carry Clause-② or the needs:contract-review pair. The card's own needs:contract-review carrier was not touched, as instructed.", "The ZONE 2 B instrument control was redrawn as instructed; the card's GetMetaItemRequestSchema control is confirmed prose-only outside packages/spec." ], "open_questions": [ { "question": "PROCESS — this dispatch did not carry the four-axis escalation framework, and my standing rules forbid inventing a substitute set of axes. The options below therefore carry costs and a recommendation but are deliberately NOT dressed as a four-axis analysis. Please supply the axes from the PM's own copy if a framed escalation is wanted.", "options": ["A. PM supplies the four axes and I re-frame these options against them", "B. Re-triage treats this measurement as-is"], "recommendation": "A if this goes to the decision box as an escalation; B if it is re-triage input, which is how the dispatch words it." }, { "question": "SUBSTANCE — PackageInstallRequestSchema is bound to POST /api/v1/packages/install, which is measured handled=false, while the live install door POST /api/v1/packages has no declared request contract at all. Which of the two declarations should own the live door?", "options": [ "A. Rebind the contract to the live path and reconcile the accept set to real traffic (declare `overwrite`, admit the bare-manifest form or stop accepting it, and name the manifest STAGE the way the read side already does with InstalledPackageAtEitherStageSchema). This is a published-surface change in both directions and is a decision-box item, not a seat's.", "B. Remove the declaration and its published export per the other ADR-0049 arm. Deletes a published export — maintainer's call, explicitly not this seat's.", "C. Leave declared-and-inert and record why. Costs the invariant the card is about.", "D. Enforce as declared at the live door. MEASURED to break live traffic and the published README call; listed only so the fork is complete." ], "recommendation": "A, because it is the only option that restores 'declared = enforced' without deleting a published export, and because the repo has already solved the hardest half of it once on the READ side: InstalledPackageAtEitherStageSchema is a union of two whole CLOSED stages, built precisely so a contract does not refuse rows its own door produces. The write side needs the same treatment plus a declaration for `overwrite`. D is measured false, and both A and B move a published surface, so neither is this seat's to take unilaterally." }, { "question": "Does the ruling's 'restores an invariant, deletes nothing' reasoning survive the measurement? It was the stated ground for choosing enforce over remove without the maintainer.", "options": ["A. It survives — enforce is still the direction, but it now requires a published accept-set change first", "B. It does not survive — with the declared path unserved, enforce and remove are BOTH published-surface moves, so the asymmetry that made enforce the seat's to take is gone"], "recommendation": "B. The ruling's asymmetry rested on enforce being a no-deletion restoration. Measured, enforcing as declared refuses live traffic, so any real enforce move must change the published accept set or the published route binding — which puts it on the same rung as removal. This is offered as a report, not an action, exactly as ZONE 1 requires." } ], "out_of_scope_findings": [ "to file (class b, contract violation; dedupe words: PackageApiContracts, packages/install, phantom route, route-ledger, machine-readable surfaces must not lie): PackageApiContracts.installPackage binds POST /api/v1/packages/install, a path nothing mounts. Measured against the real HttpDispatcher: handled=false, no response. The literal appears only inside packages/spec (declaration, its own pin at package-api.test.ts:439, generated docs) and packages/rest's registrar mounts only /packages/publish. AGENTS.md 'Route and surface ownership' rule 4 and Prime Directive #10 both name this shape.", "to file (class c, metadata-authoring trap; dedupe words: enableOnInstall, packages.install, inert option, install door, client SDK): enableOnInstall is sent by the first-party SDK (client/src/index.ts:2525) and pinned by client.test.ts:2666, and is read by NO server-side handler — zero hits across packages/runtime, packages/metadata-protocol and packages/objectql. An author sets it and the runtime silently ignores it. Declared in THREE places (api/package-api.zod.ts:261, kernel/package-registry.zod.ts:283, marketplace/marketplace.zod.ts:494) and honoured in none.", "to file (class b, contract violation; dedupe words: overwrite, POST /packages, duplicate-id guard, 409, undeclared body key): `overwrite` is a live body key at the install door — sent by the SDK, read by the handler at domains/packages.ts, pinned in both directions by client.test.ts:2669 and :2677 — and is declared by no schema anywhere. Measured consequence of any declared parse at that door: it is SILENTLY STRIPPED, turning an intentional re-install into a 409.", "to file (class a, documentation error where the example fails as written; dedupe words: client README, packages.install, vendor_plugin, manifest id required, label): the published packages/client/README.md:247 example calls client.packages.install with a manifest carrying no `id`, no `type`, and a `label` key ManifestSchema's strict close refuses by name. Measured REFUSE on three counts. Copy the example and it cannot satisfy the manifest contract.", "noted, not filed: three near-duplicate install-request declarations exist — PackageInstallRequestSchema (api/package-api.zod.ts:252), InstallPackageRequestSchema (kernel/package-registry.zod.ts:276) and the marketplace one — differing only in artifactRef. Carrier: whichever PR takes option A above; that PR must touch this file and will have to choose among them, so it is the natural carrier rather than a standalone card.", "noted, not filed: the write-side counterpart of #17536. That card is the CLIENT declaring the authoring stage against a door declared at either stage; the write half is that PackageInstallRequestSchema.manifest is ManifestSchema, the authoring stage alone, while the registry it writes into is reached at BOTH stages via ObjectQL.registerApp. Carrier: #17536 is open and carries the stage-binding subject — ⛔ not folded in here, per ZONE 2 D. #17535 (closed) and #17518 (open) were not touched." ], "mcp_calls": "0 — no MCP GitHub tool was called, read or write.", "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/18058/comments (this os-dev-report). No PR, no labels, no PATCH. Plus one git push of an empty branch, which is not an API write." }Generated by Claude Code
os-litant commentedon Sep 17, 2026
⚖️ Back to the decision box — the seat's own ruling did not survive contact with measurement
Seat
session_01LvwGppdonww4zGLWZo5rho, 2026-09-17T08:3xZ. Dispatch report: comment5711356635. ⛔ Not a failed dispatch —premise_still_valid: falsewithpr: nullis the terminal report this lane asks for, and it is re-triage input.What the seat ruled, and why it was wrong
The seat ruled enforce at claim
5711139336, on this asymmetry: enforce restores an invariant and deletes nothing, so it is the seat's to take; remove deletes a published export, so it is the maintainer's. The ruling hung on a named premise — that the declared accept set is a superset of what the door actually receives — with an explicit instruction to stop if it failed.It failed, at every site. The dev stopped and filed nothing, which is exactly right. Measured against the real dispatcher and verbatim live payloads:
POST /api/v1/packages/install— the pathPackageInstallRequestSchemais bound to — answershandled=false. The live install door isPOST /api/v1/packages, which answers 201. Same dispatcher, same run.overwritekey parses green and is silently stripped.packages/client/README.md:247example is refused on three counts.registry.installPackageviaObjectQL.registerAppis refused on four, includingobjects[0]"expected string, received object".Seat-side verification (⛔ not taken from the report): the literal
packages/installresolves in exactly two files underpackages/, bothpackages/spec/src/api/package-api.{zod,test}.ts— nothing mounts it. Lit controlpackages/publishresolves in 6 files including real source (packages/client/src/index.ts); dark control 0. ⇒ the phantom route is confirmed independently.overwriteacross all*.zod.tsreturned 11 files, but that matches any use of the word anywhere (overwriteExistingand friends), which is not the same subject as "the install door's body key is declared nowhere". That reading is discarded, ⛔ not offered as evidence either way.⇒ The asymmetry that made enforce the seat's to take is gone. With the declared path unserved, both enforce and remove move a published surface. They are on the same rung, and that rung is the maintainer's. ⛔ The seat does not re-rule.
The options, with what each costs
overwrite; decide whether the bare-manifest form stays accepted; name the manifest STAGE the way the read side already does. Published-surface change in both directions.四棱
① 实际业务需求 —— 实测有拉动,但拉动在「活门」上,不在「声明的那扇门」上。
安装能力是真实在用的:第一方 SDK 在调,README 在教,测试两头钉着,活门实测回 201。⛔ 但被声明的那条路径
POST /api/v1/packages/install服务的是零流量——它handled=false。所以这不是「声明了一个没人要的能力」,而是能力是真的、声明挂错了门。这一棱把 B 往下压:删掉声明不会让任何真实用法消失,只会让还在跑的那扇门彻底没有契约可读。② 项目长远合理性 —— contract-first 指向 A,且方向唯一。⚠️ A 的代价要说清楚:它要动已发布面,而且是双向动(既加
北极星是「协议为准、声明即强制」。今天的形状是反的:活门无契约、契约无门。A 是唯一让「存在的门」和「被声明的门」重合的选项。B 让活门永久裸奔,是 workaround 不是修复;C 是明知故犯地留着。
overwrite这类真实键,又要对裸 manifest 形态做取舍)。③ 防 AI 写元数据犯错 —— 这一棱最响,而且它单独就能定序。
今天一个 AI 作者照着 spec 写安装请求,会撞上三种静默:
overwrite被无声剥掉(一次有意的覆盖安装因此变成 409);enableOnInstall在三处被声明、零处被兑现;README 的例子照抄即失败。这正是本项目最当回事的那一类——声明了运行时不兑现的能力。A 把这三样一次性拉回响亮;B 把参照物删掉,AI 从此无据可依;C 让陷阱原样留着。⇒ ③ 明确指向 A。④ 创业阶段不扩散需求 —— 不反对 A,因为 A 不是扩张。
从紧原则针对的是「无拉动的能力扩张」。A 要声明的
overwrite、要命名的 manifest 阶段,都是已经在跑的东西——把既成事实写进契约不是扩面,是止损。真正的扩张是 D(按一个没人服务的声明去收窄活门)。这一棱唯一的告诫是范围:A 应当只覆盖活门今天实际接受的形状,⛔ 不要顺手设计新的安装选项。四棱同向 A(③ 最强,① 压 B,② 否 B/C,④ 不反对但限范围)。 按分歧推荐序:有实测拉动 ⇒ 荐长远形态一次付清。
维护者速读
改了什么 —— 什么都没改。派了一个 dev 去把「安装门按它自己声明的契约校验」做掉,它测出这事做不了,就停手了,一行代码没动。
为什么改 —— 我们的 spec 里声明了一份「安装请求」的契约,但它绑在一条根本没人挂载的路径上;真正在用的安装门反而一份契约都没有。结果是:SDK 传的
overwrite(控制「覆盖安装」的开关)会被无声丢掉,一次有意的覆盖因此变成报错;enableOnInstall在三个地方被声明、没有一个地方兑现;我们自己发布的 README 里那段安装示例,照抄下来是跑不通的。风险与代价(含回滚) —— 现在这样放着,零风险但陷阱继续在(AI 和人照着我们的文档写,写出来的东西被静默丢弃)。选 A 要动一次已发布的接口面,风险是可能影响现有调用方——但范围只限「把今天已经在跑的东西写进契约」,不新增能力,做错了一次 revert 能回。选 B(删声明)零风险但活门从此没有契约,我认为更糟。
席位意见 —— 荐 A。四条评估轴同向,其中「防 AI 写错」这一条单独就够:今天的形状正在教人写会被丢掉的东西。我原本裁了「enforce」并且以为那是我能裁的,实测证明我错了——enforce 真做起来也要动已发布面,和删除同一个台阶,所以它不是我的。
你要做的(一个动作) —— 选 A / B / C 其中一个:A?
Generated by Claude Code
25 remaining items
os-litant commentedon Sep 18, 2026
os-dev-report
{ "issue": 18058, "status": "done", "branch": "claude/issue-18058-package-install-door-enforce", "pr": "https://github.com/objectstack-ai/objectstack/pull/18752", "session": "session_01LvwGppdonww4zGLWZo5rho", "premise_still_valid": true, "summary": "F1b confirmed by my own measurement before it was fixed, then fixed with the seat's remedy (a). packages/runtime/src/domains/packages.ts line 821 now persists `pkg?.enabled === false` instead of `installDisabled`, so the durable write follows the ROW the door returned rather than the request's intent. Remedy (b) is deliberately NOT taken: a flag-absent install still does not re-enable a package an operator disabled in an earlier boot, and what a boot-seeded id does with `enableOnInstall: true` is unchanged. 16 new cases: 5 in a `#18058 F1b` block that seeds the registry exactly as AppPlugin.seedPersistedDisabledPackages does (setInitialDisabledPackageIds(loadDisabledPackageIds(undefined))) covering case F, case F across a third boot, case G (overwrite plus flag absent), the seeded-plus-`true` self-consistency arm and an unseeded control leg; plus an 11-arm table-driven block that measures the changeset's own sentence as an invariant over every arm reaching 201. The file went 9 tests to 25. One sentence was ADDED to the changeset naming the mirror direction, because it ships to consumers as CHANGELOG text and the paragraph gave only direction 1; the fenced sentence is byte-identical and still closes the paragraph. Head moved 14f9b00 to 0a02ad08d5f. PR body untouched, no labels written.", "tests": "ABLATION (verdict PASS, one verify-lock acquisition, commit b4a36868731). Subject resolves from SOURCE, not dist: the test imports '../http-dispatcher.js' relatively and runtime's vitest config declares no @objectstack/objectql alias, so no dist preflight applies; objectql's dist IS a prerequisite and was built. Legs: green1 exit=0, 25 passed. MUTATE `const rowDisabled = pkg?.enabled === false;` to `const rowDisabled = installDisabled;` (the literal F1 prescription). On-disk proof: fixed-anchor 1 to 0, mutated-anchor 0 to 1, git hash-object 7076bd3308bb2cdbef4459192c35ddd5c60c8e26 to f95e9e3cc471b6a3f6a0e6db929912c2108e6587, pre-mutation hash asserted equal to the HEAD blob. RED leg exit=1, 8 failed | 17 passed. Direction observed: turns red, as predicted. The 8 are exactly the seeded ones (case F, case F boot 3, case G, seeded-plus-true, and the four seeded table arms); all 9 pre-existing cases and every unseeded arm stayed GREEN under the mutation, which is the structural blind spot the review named, measured. Key line from the red leg: 'a restart must replay the disable, not undo it: expected true to be false' on case F boot 3, i.e. the package came back ENABLED one restart later, reproducing the review's reading of head 14f9b00 independently. RESTORE via `git checkout HEAD -- ABSOLUTE_PATH` (never bare): hash back to 7076bd3308bb2cdbef4459192c35ddd5c60c8e26 (equal to HEAD blob), `git diff HEAD` 0 bytes, `git status --porcelain` empty. green2 exit=0, 25 passed. trap RESTORE EXIT INT TERM armed with an absolute REPO_ROOT path throughout. PACKAGE SUITES, affected package @objectstack/runtime (the only package this round's diff touches beside .changeset): `pnpm --filter @objectstack/runtime test` (--project local) exit=0, Test Files 267 passed, Tests 3696 passed | 1 skipped, 275.52s. `pnpm --filter @objectstack/runtime test:repo` (--project repo) exit=0, Test Files 2 passed, Tests 69 passed. `pnpm --filter @objectstack/runtime typecheck` exit=0 (tsc --noEmit plus check:test-typecheck: 27 files / 191 errors / 69 pinned signatures held, no new pin). @objectstack/client has no test:repo script (read from its package.json) and this round's diff does not touch it, so nothing was owed there. PREREQUISITE: `pnpm --workspace-concurrency=2 --filter '@objectstack/runtime^...' build` exit=0 under the lock, then `pnpm --filter @objectstack/runtime build` exit=0 (needed by check:type-check-debt, which answered exit 3 PREREQUISITE NOT MET before it and 0 after: recorded as NOT a red). GATES, all exit codes captured by redirect-then-capture, never through a pipe: check:nul-bytes 0, check:cross-package-test-inputs 0, check:test-source-alias 0, check:dispatcher-error-vocabulary 0, check:route-envelope 0, check:route-ledger-census 0, check:durability-log-level 0, check:startup-registry-verdict 0, check:registry-log-declared 0, check:tier-file-adoption 0, check:type-check-coverage 0, check:type-check-debt 0, check:merge-driver 0, check:objectui-changeset 0, check:pm-widening-tells 0, scripts/check-changeset-no-major.mjs --base origin/main 0 (by script path, after `git fetch --deepen 300 origin main`; not a root script), scripts/check-empty-changeset.mjs --base origin/main 0, scripts/check-adr-0087-registration.mjs --base origin/main 0. REPO-LEVEL LINT was run in FULL rather than narrowed: `eslint . --no-inline-config -f json` exit=0, 6847 files linted (population read from eslint's own output, not estimated), 0 errors, 0 warnings. Re-run at the FINAL head 0a02ad08d5f together with check:nul-bytes (0) and check:type-check-debt (0, re-measure: 4 ledger entries, 53 raw errors, none above its recorded number). Gate families derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`; it named 107 commands over the WHOLE branch diff (15 paths), which is the set CI already ran green at 14f9b00, and warned its own tree was STALE relative to origin/main. This round added no new path KIND to the change set, so the families are unchanged; the whole-farm sweep is CI's run, not mine. NOT MEASURED: CI convergence at 0a02ad08d5f (not awaited, per the no-polling rule); the 6 path-scheduled CI jobs and the 5 workflow-valued families dispatch-gates itself marks NOT MEASURED; one earlier lock call returned exit 99 queue-timeout (NOT MEASURED, re-queued on the same OS_VERIFY_LOCK_SLOT and acquired).", "mcp_calls": "0 - no MCP GitHub tool was called, read or write", "api_writes": "1 REST proxy write: POST /repos/objectstack-ai/objectstack/issues/18058/comments (this report). No POST /pulls (PR 18752 already open), no POST .../labels (the dispatch forbids label writes; needs:contract-review is the seat's carrier and is currently absent), no PATCH of the PR body. Separately, 2 git pushes to claude/issue-18058-package-install-door-enforce (14f9b00 to b4a36868731, b4a36868731 to 0a02ad08d5f) - git transport, not REST.", "open_questions": [], "out_of_scope_findings": [ "to file (class b, contract violation; dedupe words: enableOnInstall, initialDisabledPackageIds, seedPersistedDisabledPackages, install door, boot-seeded disable): an install that explicitly sends `enableOnInstall: true` for an id the boot seeded disabled still answers 201 with `enabled: false` - the declared option is not honoured on that one arm, because installPackage reads the seed at registration. This IS remedy (b)'s territory and the seat already fenced it ('a separate card and a separate ruling'), so it is reported for the seat to file rather than fixed here. Under (a) the three records at least AGREE there, which the new self-consistency case pins.", "noted, not filed: the durable state file is never pruned - DELETE /packages/:id does not clear the record and nothing else does, so ids of long-uninstalled packages stay in the seed set forever and the file grows monotonically. Not a reproducible defect and no contract text names it. Successor: whoever next edits packages/runtime/src/package-state-store.ts or the uninstall path.", "noted, not filed (both fenced by the ruling, and this round's delta disturbs neither, verified by re-reading the handler): the ADR-0070 parity gap - PATCH /packages/:id/disable runs requireWritablePackage while the install path runs only requireManageMetadata, so the install door can disable a package the disable door would refuse; and `?overwrite=true`, which the handler reads off the query string while no closed query-parameter set declares it (AGENTS.md 'Route and surface ownership' rule 5). Both left exactly as found.", "noted, not filed (reported so the seat can revert it if unwanted): I ADDED one sentence to .changeset/18058-install-door-contract-rebind.md naming the mirror direction, because that text ships to consumers as CHANGELOG.md and the paragraph previously gave only the 'answered enabled:true, disk still says disabled' half. The fenced sentence is byte-identical and still closes the paragraph; Clause-2 is unchanged; the three changeset gates were re-run green after the edit." ] }Generated by Claude Code
os-litant commentedon Sep 18, 2026
os-dev-report
{ "issue": 18058, "status": "done", "branch": "claude/issue-18058-package-install-door-enforce", "pr": "https://github.com/objectstack-ai/objectstack/pull/18752", "session": "session_01LvwGppdonww4zGLWZo5rho (subagent; the parent PM seat's harness-stamped id)", "premise_still_valid": true, "summary": "F1 patch round, one file, prose only: .changeset/18058-install-door-contract-rebind.md line 24. Re-measured the RELEASED install door for myself before writing, then replaced the two sentences the seat named with present-tense design rationale that claims no prior behaviour. The neighbouring sentences the seat told me to protect are byte-untouched: the F1-direction sentence about the released door's real stale-disable, and 'Every install now persists the state it returned'. Nothing else moved: packages/runtime/src/domains/packages.ts is untouched, no code, no tests, no schema, no labels, no PR-body edit. Finding 2 (the stale seed) is the seat's to file and to rule on; I neither filed nor fixed it, and I checked that the new wording does not imply the stale-seed case behaves better than it does (it says a flag-absent install of a SEEDED id stays disabled, which is exactly what the seed does).", "released_door_measurement": { "method": "Source measurement, not a boot probe. The claim under repair is a NEGATIVE one about the released door ('it cleared the record'), and for a negative claim the reachability of the only writer is stronger evidence than any single scenario run.", "released_blob": "packages/runtime/src/domains/packages.ts at tag @objectstack/runtime@17.4.0 = blob cf8ed623be813104c63630561d8515d446532d67; the local packages/runtime/package.json is 17.4.0, so 17.4.0 is what consumers have.", "merge_base_blob": "d0390d2d73865d9106a031a6578d1499be29b933 (= the blob the dispatch hash-verified). It differs from the released blob only in the unrelated GET /packages/:id ?version= work; both have IDENTICAL install branches.", "finding_1": "setPackageDisabled appears exactly twice in the released handler body: PATCH /packages/:id/enable (writes false) and PATCH /packages/:id/disable (writes true). The POST /packages install branch calls it ZERO times and reads enableOnInstall nowhere. Head adds the third call site inside the install branch.", "finding_2": "Repo-wide, the ONLY non-test production importers of package-state-store are app-plugin.ts (loadDisabledPackageIds, a READ) and domains/packages.ts (setPackageDisabled, the write). There is no other path, direct or transitive, by which the released install branch could have touched the record.", "conclusion": "The released door never cleared the durable disable on install and never brought a package back enabled one restart later. The two sentences stated a history that did not ship, and their 'no longer' clauses asserted a prior-release defect that did not exist. Independently reached, and it agrees with the dispatch's runtime probe (201 / wire false / registry false / disk TRUE untouched / boot 3 false).", "rationale_inputs_verified": "objectql/src/registry.ts:4172 installPackage reads initialDisabledPackageIds.has(manifest.id) and sets enabled: !disabled, taking no enable argument at all; the seed is written ONLY by setInitialDisabledPackageIds, called once at boot from app-plugin.ts:448; enableOnInstall is declared .default(true) in three schemas (package-api.zod.ts:286, kernel/package-registry.zod.ts:283, marketplace.zod.ts:494); client.packages.install(manifest) with no options JSON.stringify-drops the undefined key, so the SDK's default call really is flag-absent." }, "before_after_text": { "before": "The mirror of that is why the write reads the ROW rather than the request: `installPackage` lands an id seeded into `initialDisabledPackageIds` at boot disabled whatever the request says, so an install with `enableOnInstall` absent - the SDK's default call - answered `enabled: false` and then cleared that operator's disable off disk, bringing the package back ENABLED one restart later. A flag-absent install no longer re-enables a package disabled before a restart, and it no longer clears the record either.", "after": "The mirror of that risk is why the write follows the ROW this door returned rather than the request's intent: `SchemaRegistry.installPackage` lands an id in the boot-seeded `initialDisabledPackageIds` DISABLED whatever the request says, and `enableOnInstall` defaults to `true`, so persisting the request would clear an operator's earlier disable off disk on the SDK's default call while the row being served says `enabled: false`. A flag-absent install of a seeded id therefore answers `enabled: false` and records it disabled - wire, registry and disk agree, and the next boot reads the same.", "note": "The em dashes in both texts above are rendered as hyphens in this JSON field only, to keep the report free of characters the comment pipeline could reshape. The committed file carries real em dashes; read 26805a16da3 for the bytes." }, "tests": "Prose-only diff, so no ablation and no reverse verification: there is no behaviour in this round to mutate. On-disk edit proof instead - anchored replace, exactly 1 occurrence required, plus before/after grep counts on BOTH the deleted and the inserted text (deleted: 1 -> 0 for each of two anchors; inserted: 0 -> 1 for each of two anchors), byte length 5617 -> 5698, and `git diff --stat` = 1 file, 1 insertion, 1 deletion. THE THREE GATES THE SEAT NAMED, each by script path from the branch worktree, every exit code captured by redirect (`cmd > file 2>&1; E=$?`), never through a pipe, each with the CI spelling `--base a84da6096a580764a946f028206a89d44b528339` (the merge base) after its own --self-test: (1) `node scripts/check-empty-changeset.mjs --self-test` EXIT=0 (159 assertions) then `--base` EXIT=0 - 'No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)' and 'No changeset from the merge base modified or deleted by this diff'. (2) `node scripts/check-adr-0087-registration.mjs --self-test` EXIT=0 (384 assertions) then `--base` EXIT=0 - 'this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)'. (3) `node scripts/check-changeset-no-major.mjs --self-test` EXIT=0 (299 assertions) then `--base` EXIT=0 - 'This diff introduces no `major` bump'. THREE MORE, because the mechanical derivation named them and they read .changeset or every edited file: `node scripts/check-nul-bytes.mjs --self-test` EXIT=0 then plain EXIT=0 (8866 text files, no raw ASCII control bytes); `node scripts/check-changeset-fixed.mjs` EXIT=0 (the roster the derivation flagged as sitting under .changeset, so its silence was not evidence either way) - 'fixed group is in sync with 70 public workspace packages'. Plus a manual byte scan of the edited file: `grep -naP '[control-byte class]'` exit 1 (none) and no angle-bracket fragment in the file at all. NOT MEASURED, with reasons: (a) the clause-2 LEVEL axis of check-changeset-no-major - it printed 'LEVEL AXIS: NOT APPLICABLE' because a local run has no pull_request payload to read the declaration from, by construction rather than by omission; CI reads it on the PR, where `Clause-2: yes (widening)` is unchanged in the changeset body and the PR body. (b) The rest of the 107-command family sweep the derivation prints for the WHOLE branch change set - declared narrowing, on the seat's instruction that the verify lock is contended and this round needs no heavy verification. The narrowing is justified rather than merely asserted: this round's delta is exactly one markdown file under .changeset, no package is in any dependency closure of it, and the families that read .changeset are the five named above (three gates, the nul-byte gate, the fixed-roster gate) plus `check:changeset-gate-self-tests`, which IS the three --self-test runs already reported EXIT=0. No verify lock was taken and no OS_VERIFY_LOCK_SLOT was used.", "mcp_calls": "0 - no MCP GitHub tool was called at all, read or write.", "api_writes": "1 REST proxy write: POST /repos/objectstack-ai/objectstack/issues/18058/comments (this report). Plus one git push, 0a02ad08d5f -> 26805a16da3. NO label write and NO PR-body PATCH: the seat reserved both this round, and no os-dev.md obligation is unmet by that - the label step belongs to the PR-open write, which happened in round 1, and the PR body is reserved to the PR-open write by os-dev.md:56, which the seat is repairing itself.", "open_questions": [], "conflicts_named": "No binding conflict between this dispatch and os-dev.md. Two things checked and cleared rather than silently chosen: (1) os-dev.md says label the PR as soon as it is opened - the PR was opened in an earlier round and the seat reserved label writes this round, so there is no unmet obligation, not an override. (2) The harness attribution reminder asks for a `Co-Authored-By: Claude Opus 5` trailer; AGENTS.md requires the model-free trailer pair, os-dev.md forbids any model identifier in anything pushed, and the pre-push hook enforces it. The repo rule wins - the commit carries `Claude-Session:` plus a model-free `Co-authored-by: Claude`, and the pre-push hook confirmed it in the push output. AGENTS.md already classes this as reporting, not a declared deviation.", "out_of_scope_findings": [ "noted, not filed (the seat has ruled it and is filing it itself): the stale seed - SchemaRegistry.initialDisabledPackageIds is written only by setInitialDisabledPackageIds at boot, and neither enablePackage nor disablePackage updates it, so an id seeded disabled at boot and then enabled by an operator re-lands disabled on any re-install within that boot. Confirmed at registry.ts:1990/1997/4172 while verifying the rationale sentence. I did not file it and did not fix it. Carrier: the seat's own card.", "noted, not filed: this branch's tree is at least 36 commits behind origin/main, and `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` warns that 14 of the files it derives from changed across that range - `scripts/check-adr-0087-registration.mjs` among them. So the three gate verdicts above were produced by the BRANCH's copies of those scripts, not by the copies CI will run on the merge ref. Not a defect and not a class (a)/(b)/(c) finding, and the risk is negligible for a prose-only diff that touches no gate logic, but the reading is branch-scoped and should be read that way. Carrier: this PR's own CI run at 26805a16da3." ] }Generated by Claude Code
os-litant commentedon Sep 18, 2026
os-dev-report
{ "issue": 18058, "status": "done", "branch": "claude/issue-18058-package-install-door-enforce", "pr": "https://github.com/objectstack-ai/objectstack/pull/18752", "session": "session_01LvwGppdonww4zGLWZo5rho", "premise_still_valid": true, "summary": "Merge-conflict round only — no code, test, schema or changeset work. Merged origin/main (0b31d90fb37) into the branch through scripts/pm/os-regen-merge.sh; it stopped on the one conflict, packages/spec/dropped-refinements.baseline.json, which it classified as an ordinary hand-written file (the ledger is absent from .gitattributes' merge=os-regen routing AND from NOT_DRIVER_MANAGED in scripts/regen-artifacts.mjs, so class 1, resolve by hand). Resolved it by unioning the two sides' ENTRIES, not by arithmetic on the header. content/docs/references/index.mdx was the only routed path both sides had edited: the driver resolved it to this branch's bytes exactly (merged blob c954e54 == branch blob c954e54, main's blob 68d32a4 dropped), so main's side was restored to the worktree pinned to the merged sha before committing, per step 2. Merge committed as 25e89e94221 with the os-regen deferral recorded; regeneration committed as ad75bc85397, discharging it. PR now reads mergeable: true.", "ledger_resolution": { "sides_disjoint": true, "ours_added": ["api/PackageInstallBody (2 sites)"], "theirs_added": ["data/QueryTransportParams (4 sites)", "data/QueryWithTransport (4 sites)"], "theirs_changed": ["api/FindDataRequest: 1 site -> 4 sites"], "keys_moved_on_both_sides": "none — no site list had to be chosen between, so there is no merge decision to declare", "keys_removed_on_either_side": "none", "header_as_the_build_printed_it": "gen:schema log line 1664, verbatim: '750 refinement site(s) across 246 published schema(s) reach the RUNTIME and not the published JSON Schema — all declared in dropped-refinements.baseline.json (#18670).'; line 1669: 'Also measured this run: 0 refinement site(s) DID reach the file, 3 had no JSON form on either side to compare.' Committed header: publishedSchemasWithDroppedRefinements 246, droppedRefinementSites 750, refinementSitesThatDidProject 0, refinementSitesWithNoJsonFormToCompare 3, zod 4.4.3. The body was written first and the build adjudicated it: checkDroppedRefinements reported no undeclared / miscounted / repaired / vanished / unreasoned entry, so no corrected entry was printed and none had to be applied.", "arithmetic_not_used": "243+1+2 was never entered into the file; the header is derived from the merged BODY (which the pin test requires) and cross-checked against the gate's own printed line, which agrees on both numbers." }, "regenerated_artifacts": { "moved": ["content/docs/references/index.mdx"], "unmoved": ["packages/spec/api-surface/**", "packages/spec/authorable-surface/**", "packages/spec/declaration-map/**", "packages/spec/export-origins/**", "packages/spec/json-schema.manifest/**", "content/docs/references/api/package-api.mdx"], "evidence": "After gen:schema + gen:docs the only tracked path git status named was content/docs/references/index.mdx; pnpm --filter @objectstack/spec check:generated then exited 0 over the whole set. The regenerated index is a THIRD blob (a40f9d8), neither side's: it carries main's QueryTransportParams and QueryWithTransport (data 173 -> 175) together with this branch's PackageInstallBody (api 440 -> 441), total 1528/1529 -> 1530." }, "tests": "All heavy runs through scripts/pm/os-verify-lock.sh, OS_VERIFY_LOCK_SLOT=issue-18058-r2; exit codes captured by redirect into per-step files, never through a pipe. Regen chain (VERDICT command-exit 0, waited 280s behind issue-17080's holder, held 270s): build EXIT=0, gen:schema EXIT=0, gen:docs EXIT=0, check:generated EXIT=0. Spec validation (VERDICT command-exit 0, held 258s): dropped-refinements.test.ts EXIT=0 (1 file / 27 tests passed), src/api/package-api.test.ts EXIT=0, pnpm --filter @objectstack/spec test EXIT=0 (488 files / 14149 tests passed), spec typecheck EXIT=0. Runtime+client validation (VERDICT command-exit 0, held 443s): dependency-closure build EXIT=0, packages-install-enable-on-install.test.ts EXIT=0 (25 tests), readme-package-install-example.test.ts EXIT=0 (6 tests), runtime typecheck EXIT=0, client typecheck EXIT=0. Doc-facing gate subset, 19 commands, every one EXIT=0: check-doc-frontmatter, check-doc-route-spelling --advisory, check-docs-section-name, check-spec-docblock-symbol-anchors, docs-audit/check-affected-docs, docs-audit/check-drift-comment, lint check:doc-formula-expressions, lint check:doc-security-posture, spec check:docs, spec check:variant-docs, check:doc-anchors, check:doc-authoring, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check:docs-spec-enumerations, check:docs-transcript-drift, check:nul-bytes, check:quick-reference-counts. NOT MEASURED: the remaining 88 of the 107 families scripts/pm/dispatch-gates.mjs derives, plus the repo-level eslint scan — CI's run, deliberately not duplicated in this round; no ablation was owed (no behavioural change).", "untouched_through_the_merge": "Measured, not asserted: git hash-object over the seven hand-written branch files before the merge and git rev-parse HEAD:path after it are identical, and git diff --stat 26805a16da3..HEAD over those paths is empty. packages/runtime/src/domains/packages.ts 7076bd3308bb2cdbef4459192c35ddd5c60c8e26; packages/runtime/src/domains/packages-install-enable-on-install.test.ts ecf36fe703f109ac78d6938165f4c31e5d4bed0d; packages/spec/src/api/package-api.test.ts 1d7884cab015bf78674954eeb8dd15c1cd77ca5c; packages/spec/src/api/package-api.zod.ts 6a2e489b4518cbd36a87c29c469956e4329a3bb5; packages/client/src/readme-package-install-example.test.ts 66f8eb40f6bd21eb00f5613cee4309abea9f8c17; packages/client/README.md 8b573e03ac8881da82546f8ff4de57356dfe34ef; .changeset/18058-install-door-contract-rebind.md c670fa2a2bcc3f8897dbf30a219078c854c64382. The handler, both new tests, the two edited tests, the README and the changeset all come through byte-identical on this side of the merge.", "mcp_calls": "0 — no MCP GitHub tool was called, read or write", "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/18058/comments (this report). No POST /pulls (the PR exists), no label write and no PR-body PATCH, per the dispatch. The branch update was a plain git push, not a REST write; it was a fast-forward (26805a16da3 is an ancestor of ad75bc85397), so no rebase, amend or force.", "open_questions": [], "out_of_scope_findings": [ "to file (3 classes, dedupe words: os-regen-merge, rerun this script, step 2 inert, merge-base after merge, hand-resolved conflict) — (a) reproducible defect in scripts/pm/os-regen-merge.sh: all three conflict messages end 'rerun this script to redo the generated-artifact half', and there is no tree state in which that rerun can perform step 2. mode_run refuses a dirty tree, so it cannot be rerun while the conflict is being resolved; and once the merge is committed to make the tree clean, merge_base = git merge-base HEAD origin/main is origin/main's own tip, so main_edited is empty, every regen path reads as branch-edited, and step 2 takes nothing — the exact mechanism the script's own header warns about under 'The base must be read BEFORE step 1'. Read from the script's source and header; the rerun was deliberately NOT executed, so this is a code reading, not an observed run. In this round step 2 was performed by hand with the pre-merge base a84da6096a5, which is why main's side of content/docs/references/index.mdx was restored at all.", "noted, not filed: the os-regen driver resolved content/docs/references/index.mdx to this branch's bytes and dropped main's side with exit 0 and no marker in any diffstat. This is the documented designed behaviour the driver prints a notice for, and the regeneration re-derived the true union, so it is a confirmation of the mechanism rather than a defect. Successor: the next merge round on any spec branch, which will meet the same path.", "noted, not filed: packages/spec/dropped-refinements.baseline.json is adjudicated by packages/spec/scripts/build-schemas.ts but is declared in neither .gitattributes' merge=os-regen list nor NOT_DRIVER_MANAGED, so os-regen-merge.sh reports it under class 1 with the blanket line 'Do not resolve generated files textually'. For this file that line is true and the class-1 instruction is the correct one, so there is nothing to fix; recorded because the file reads as generator-adjacent and the next reader may expect class 3. Successor: none." ] }Generated by Claude Code
github-actions commentedon Sep 18, 2026
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.156792e0a0, merged intomain.bug,priority:p2,domain:spec— ownership, priority and outcome are not state claims.A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.
posted by half-state-patrol run 35320894629 · trigger
scheduleGenerated by Claude Code
fix(spec,runtime): bind the package-install contract to the door that…
fix(objectql,runtime): a flag-absent install preserves the package's …
spec: one authority for enableOnInstall, and a read-out of its other …
docs(changeset): correct four statements in the pending enableOnInsta…
test(spec): the first api/ file group's test titles state each cited …