Repository navigation
fix(objectql): an uninstalled package no longer reads as read-only until a restart - #30280
Conversation
Claude-Session: https://claude.ai/code/session_01Utp6CbfmKbpDmbzwGpfLLp Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CbfmKbpDmbzwGpfLLp Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CbfmKbpDmbzwGpfLLp Co-authored-by: Claude <noreply@anthropic.com>
…ription Claude-Session: https://claude.ai/code/session_01Utp6CbfmKbpDmbzwGpfLLp Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 38 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 21 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 471bfe4d4b9aa1c2e74727960ff4df91ad8e16ac && git checkout 471bfe4d4b9aa1c2e74727960ff4df91ad8e16ac
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 04f36b63f8d2f32b00428929b07268130881e30f f8e6c780f0c46932be761c5f692fc62f047cd460 && git checkout -B drift-repro 04f36b63f8d2f32b00428929b07268130881e30f && git merge --no-ff f8e6c780f0c46932be761c5f692fc62f047cd460
node scripts/docs-audit/affected-docs.mjs --json 04f36b63f8d2f32b00428929b07268130881e30f
|
|
domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC Contract reviewServed-tier: Face: declaration ( ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC 席位核对ACCEPT · head
|
Requested by Bill · project thread
Fixes #30215
Clause-②: yes (widening) —
@objectstack/objectql's exportedSchemaRegistrygains the public methodonPackageUninstalled(listener), which returns its unsubscribe. Nothing is refused that was accepted before.Metadata-protocol: none
维护者速读
uninstallPackage撤下一个包后,向订阅者宣告一次;引擎在构造时订阅,从自己的manifests表里删掉该包 id。于是卸载答 200 之后,同一实例上该 id 不再被当成代码包,ensure-customization答writable: true。registerApp往manifests写,没有任何卸载门删它;ADR-0070 的isWritablePackage先读这张表,所以卸载过的包(例如旧运行时当代码装的.custom配对包,PR fix(cloud-connection,cli): install a .custom export into the target's writable pair #30207 现在叫管理员去卸)一直只读,直到重启,而卸载答复只说「已从运行内核撤下」。manifests项;被拒的卸载(ADR-0029 有扩展者)什么都不动(有 pin);同 id 重装照旧写回。订阅者抛错不会把已完成的卸载变成拒绝,只记一条error日志。回滚即撤 PR。packages/objectql/src/registry.ts的onPackageUninstalled/announcePackageUninstalled与uninstallPackage末尾一行;packages/objectql/src/engine.ts构造函数里的订阅一行;packages/objectql/src/registry-module-mock.ts新增的替身成员。pnpm --filter @objectstack/objectql exec vitest run --project local src/engine-uninstall-forgets-manifest.test.ts,预期 7 条全过。What changes
The gap.
ObjectQL.registerAppwrites the package's manifest into the engine's privatemanifestsmap, and nothing ever deleted an entry. Every uninstall door withdraws a package throughSchemaRegistry.uninstallPackage(the install-local DELETE, the protocol'sdeletePackage, the runtime'sDELETE /packages/:id), and that verb never told the engine. ADR-0070'sisWritablePackage(packages/metadata-protocol/src/package-writability.ts) readsengine.manifestsfirst, so an uninstalled id kept reading as a booted code package, read-only, until a restart.The fix: the state follows the registry, not the reader.
SchemaRegistrygainsonPackageUninstalled(listener), anduninstallPackageannounces each completed uninstall as its last step, after the package record is gone. The engine subscribes in its constructor and drops itsmanifestsentry. One place, so whichever door withdrew the package releases the id, and no door needs to know the engine's map exists.registerApp, unchanged).errorwith the consequence and the remedy (restart), because the listener's state now disagrees with the registry while everything looks healthy.Why not the other route the card offered (the uninstall answer says a restart is needed). The card prefers releasing the id. Releasing it is also the only route that is right for every door: an answer-only fix would cover the install-local DELETE and leave the same stale entry behind every other caller of the registry verb.
Why not teach
isWritablePackageto cross-check the registry. That would be a reader skipping a stale entry, which the install-local door's ownwithdrawFromRunningKernelnote rejects for the same class of defect: withdraw the registration, so every reader is right. The map's other reader (the packagedefaultDatasourcelookup inengine-secrets-and-routing.ts) is keyed by the object's owner, which the registry already withdraws, so it was not wrong in practice; it now reads a map that agrees with the registry too.Clause-② measured. The seat's claim predicted
no; it isyes (widening)becauseSchemaRegistryis exported from@objectstack/objectqland gains a public method. Nothing is narrowed: the only answer that changes is that an uninstalled id is no longer read-only on the same instance.Test double. Every engine now calls
onPackageUninstalledin its constructor, so the sharedvi.mock('./registry')factory (registry-module-mock.ts, used by 19 engine suites) models the member: it records the subscription and answers an unsubscribe. Without it those 19 files failed at construction (391 tests, measured on the first full run).Verification
Unit pins. New
packages/objectql/src/engine-uninstall-forgets-manifest.test.ts, 7 cases, against a real engine and its own registry, asserting through the realisWritablePackage:erroronce.Ablation (twice, through
scripts/ablation-replace.mjs; the test imports source)Under ablation 1 the refusal, other-package and three registry cases stay green: they assert what the fix must not change or the registry's half alone.
End to end, two
os dev --freshshowcase runtimesFixture: a ledger entry for
com.example.showcase.custominexamples/app-showcase/.objectstack/installed-packages/, the shape an older runtime wrote when it installed a pair as code; the boot rehydrates it as code (GET /api/v1/marketplace/install-locallists it). Signed in as the dev admin.Fixed build (head with the fix, full
pnpm build):Control, the same flow with the engine's subscription ablated and
@objectstack/objectqlrebuilt (scripts/ablation-dist-preflight.mjsconfirmed the line absent from all 14 built files):That
writable: falseis the card's defect, reproduced; the restored build answerswritable: true. Both servers were started on random ports and stopped by recorded PID; the fixture directory was removed.Tests and gates on the final head
On head
f8e6c780f.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 62 families; each was run with its exit code recorded to a file before any pipe, then reconciled:Beyond the derivation, each exit 0:
pnpm lintpnpm --filter @objectstack/objectql test: Test Files 549 passed (549), Tests 10870 passed (10870)pnpm --filter @objectstack/objectql typecheck(includescheck:test-typecheck)pnpm --filter @objectstack/metadata-protocol testpnpm --filter @objectstack/cloud-connection test(the install-local uninstall suites included)pnpm --filter @objectstack/runtime test: Test Files 541 passed (541), Tests 8619 passed, 19 skippedThe CLI end-to-end uninstall suite (
package-install-local-uninstall-cleanups.integration.test.ts, integration tier) was not run locally: it is left to CI.Acceptance notes
#30217(same install-local door) stays serial behind this card and is not touched here.registerAppalso indexes the manifest'sdatasources(registerDatasourceDef), and nothing withdraws those definitions on uninstall either. A datasource definition is keyed by its own name, not the package id, and whether a package's datasource should outlive it is a separate decision; nothing in this card measured a wrong answer from it.Human Minimum
packages/specholds no runtime logic. (Not touched.)pnpm lintpasses.main, one pull request.packages/spectouched: not touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code