Skip to content

fix(cloud-connection): an install-local uninstall runs the protocol's registered uninstall cleanups - #21512

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-21490-install-local-uninstall-cleanups
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-21490-install-local-uninstall-cleanups

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21490

Clause-②: yes

Status: both halves are in this diff; draft. This PR now carries the install-local door and the protocol runner the door calls. The runner is runUninstallCleanups, a new public method on the exported ObjectStackProtocolImplementation. That widens @objectstack/metadata-protocol's public surface, so the seat ruled Clause-②: yes on the card, and that package takes a minor changeset. An at-tier contract review is owed before this PR enqueues.

The defect, measured at the public door

On main 9ff7428, with an empty os start, I installed a package that declares a permission set through os package install. I granted the set to the operator through the data door, sent DELETE /api/v1/marketplace/install-local/com.example.tasksapp, and probed over REST.

order of events DELETE set row right after user grant right after after restart: object after restart: set row
hot install, DELETE, restart 200 1 (managed_by: package) 1 404 1
install, restart, DELETE, restart 200 1 1 404 1

handleUninstall removed the ledger entry and nothing else. plugin-security registers the revocation (security.package-permissions) with the protocol's uninstall-cleanup registry, and before this change only deletePackage ran that registry.

Why not call an existing protocol door (measured)

  • DELETE /api/v1/packages/:id refuses an install-local package outright: 422 WRITABLE_PACKAGE_REQUIRED. The set survives, and the ledger keeps the package across a restart.
  • The verb itself, deletePackage, measured over an install-local package's shape (registry entry only, no sys_packages row, no sys_metadata rows):
    • Without an organization it refuses (400 TENANT_SCOPE_REQUIRED).
    • With allTenants: true it answers success: false (0 rows deleted).
    • On the way it issues a sys_packages delete and calls registry.uninstallPackage, which withdraws the package from the running kernel. The door never did that, and it would leave the package's bound handlers and flows behind. It would also delete any sys_metadata overlay rows bound to the package, dropping their tables by default.
  • So the cleanup loop is extracted into ONE protocol runner that deletePackage and this door both call (the triage direction: one registration seam, both doors run it).

What this PR changes

@objectstack/metadata-protocol (minor)

  • runUninstallCleanups({ packageId, organizationId?, actor? }) sits right after registerUninstallCleanup. Its request type is DeletePackageRequest picked down to those three keys, and it answers one UninstallCleanupOutcome per registered cleanup.
  • Its body is deletePackage's step-7 loop, moved verbatim. The only change inside the loop is the warn tag, now [protocol.runUninstallCleanups] instead of [protocol.deletePackage].
  • deletePackage calls it in place of the loop: const cleanups = await this.runUninstallCleanups(request);. There is no other change to deletePackage, and its existing suites are the control (see Tests).
  • New test protocol.uninstall-cleanups-runner.test.ts. It pins the arguments each cleanup receives (with and without an organization and actor), failure as an outcome with the driver text withheld while the remaining cleanups still run, and the control: deletePackage calls the runner once with its own request and reports exactly the runner's outcomes.

@objectstack/cloud-connection (patch)

  • handleUninstall runs the protocol's uninstall cleanups once the ledger entry is gone, through protocol.runUninstallCleanups.
    • The request carries the MANIFEST id, because the registry and every row a domain plugin stamped know the package by manifest.id, and a cloud install's ledger packageId is the catalog id. It carries no organization, because an install-local package is installed for the whole runtime. actor is the admitted operator.
    • There is no second revocation path in cloud-connection.
  • The response carries every outcome as data.cleanups, the way deletePackage reports them.
    • A failed cleanup is reported there, never swallowed, and named in a warn with its remedy: install the package again, then uninstall it again. The ledger entry is gone, so retrying the DELETE answers 404.
    • No protocol service means no registry exists, so cleanups: [].
    • A protocol without the runner, or a runner that throws, gives one failed outcome named protocol.runUninstallCleanups, so "nothing to revoke" and "the revocation never ran" read differently.
  • Nothing is revoked when the uninstall does not happen: a refused caller, an id this door never installed (so another package's grants are never this door's to touch), or a ledger write that fails.
  • The protocol slot is uncontracted, so this door narrows it per consumer: the verb name is this file's, and the request and outcome types are the producer's (DeletePackageRequest, UninstallCleanupOutcome). This is the same shape PackagesDomainProtocol uses for deletePackage in packages/runtime. @objectstack/cloud-connection therefore declares @objectstack/metadata-protocol, which it already received through @objectstack/runtime (check:undeclared-dep-imports).
  • The route ledger note for this DELETE, and the file header, now say what the door does.

Tests

All at head cd5cabce34 (this branch after merging origin/main 6dd99b82c3), on real built packages: turbo run build --filter=@objectstack/cli^... built every dependency, @objectstack/metadata-protocol and @objectstack/cloud-connection included, and ablation-dist-preflight read the runner's own warn tag in both of @objectstack/metadata-protocol's runtime bundles. No dist overlay.

  • Integration pin (packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts, --project integration): 10 passed, 2 expected fail (12).
    • Orders 1 and 2 (hot install, DELETE, restart; install, restart, DELETE, restart): 8/8 green. For each order: the precondition (set and grant exist), DELETE 200 with security.package-permissions reported success: true, no set and no grant right after, and no set, no grant and object 404 after a restart. The previous round read this green only through a temporary overlay of the built runner; it now reads green on the real build.
    • On main (ebf0b8e329, test only) the same 8 cases read 6 failed, 2 passed. That is the reproduction.
    • Order 3, the re-seed window, is new in this round; see the next section.
  • Runner test plus the four existing suites that register uninstall cleanups (protocol.uninstall-cleanups-runner, durable-package, protocol.driver-text-disclosure, protocol.marked-refusal-classification, protocol.package-delete-refusal): 5 files, 54/54.
  • pnpm --filter @objectstack/metadata-protocol typecheck green, and the full suite: 207 files passed, 3 skipped; 3192 tests passed, 19 skipped. The new test is in the tsc program (--listFiles, count 1).
  • pnpm --filter @objectstack/cloud-connection typecheck (both programs) green, and the full suite: 33 files, 414/414. The unit pin marketplace-install-local-uninstall-cleanups.test.ts is 8/8 in it.
  • pnpm --filter @objectstack/cli exec vitest run --project unit: 250 files passed. The 2 that failed, published-subpath-console.pin and published-subpath-hook-body.pin, refused at collection because packages/cli itself had no dist/ (packages/cli is not built). That is a prerequisite, not a reading. After pnpm --filter @objectstack/cli build they passed, 29/29. test/vitest-tiers-partition.test.ts is in the unit run.

The re-seed window (dispatch A4): measured RED, pinned as it.fails, not fixed here

Order 3 in the integration pin: hot install of the package, grant its set, DELETE it, then a hot install of a DIFFERENT package (com.example.notesapp), then restart.

reading set row of the uninstalled package its user grant
right after the DELETE 0 0
right after the other package's hot install 1, a fresh row: managed_by: package, package_id: com.example.tasksapp 0
after a restart 1, the same row (an orphan: the package's object answers 404) 0

Why: this DELETE leaves the package registered in the running kernel until the next restart (the response's note says so). The other install announces metadata:reloaded, and plugin-security's subscriber re-runs bootstrapDeclaredPermissions over every package the kernel still holds. That re-projects the uninstalled package's set. After the restart nothing selects it again: the package is gone, the row is not. The grant does not come back, because the cleanup deleted the binding and the seeding writes none.

In the pin, the precondition and the "grant stays revoked, object gone" reading are plain assertions, both green. The two set readings are it.fails: each turns red the day its half is fixed, which is the cue to promote it to a plain assertion. The finding goes to the seat for filing (see the report on the card); this PR does not fix it.

Reverse verification

Both legs ran through scripts/ablation-replace.mjs (WRAP mode, with a planted marker), from the committed head ea93d2754e.

  • Leg A, the door's call. In marketplace-install-local-plugin.ts, the anchor const cleanups = await this.runUninstallCleanups(ctx, manifestId, admission.userId); (1 hit) was replaced with an empty list plus the marker ABLATION-21490-DOOR. The tool reported "anchor 1 → 0, blob f9929f2f700a → 1771b1b9b2f3". ablation-dist-preflight found the marker in both runtime bundles, index.js and index.cjs. The package's DTS step failed on the now-unused private helper (TS6133), but the JS bundles the suites load were emitted and carried the marker.
    • Unit pin: 4 failed, 4 passed. The red cases are every one that needs the call. The 4 green ones assert an absence the ablation also produces: a refused caller, an unknown id, a failed ledger write, no protocol service.
    • Integration pin: 8 failed, 2 passed, 2 expected fail. Every revocation reading went red, and the order-3 precondition went red too, because the DELETE no longer revoked the set.
    • Restore: "blob == HEAD (f9929f2f700a) and git diff HEAD is empty". Rebuilt (exit 0). ablation-dist-preflight --absent: the marker is absent from all 6 built files, and the working tree is clean against HEAD.
  • Leg B, the runner itself. In protocol.ts, the anchor for (const [name, cleanup] of this.uninstallCleanups) { (1 hit, now only inside runUninstallCleanups) was replaced with a loop over an empty Map plus the marker ABLATION-21490-RUNNER. The tool reported "anchor 1 → 0, blob 4308b1f47cce → 721cc1b5c8b8". The build exited 0, and the marker was present in index.js and index.cjs.
    • The five suites above: 11 failed, 43 passed (54), and all 5 files were red. The runner test went 4/4 red. Each of the 4 existing deletePackage suites went red, which shows deletePackage really runs through the runner.
    • Integration pin: 8 failed, 2 passed, 2 expected fail, the same reading as leg A.
    • Restore: "blob == HEAD (4308b1f47cce) and git diff HEAD is empty". Rebuilt (exit 0). --absent: the marker is absent from all 24 built files, and the tree is clean.

Gates

  • At cd5cabce34, node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 77 commands, and all 77 exited 0.
  • --ran reconciles to "77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN".
  • pnpm check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3: 9 packages had no dist/). Later gates in the same sweep built them, and the re-run exited 0.
  • pnpm --filter @objectstack/spec check:generated after the merge: all 15 generated artifacts are up to date.
  • Lint: the full pnpm lint (eslint . --no-inline-config) exited 0 at cd5cabce34.

Acceptance notes

  • The re-seed window is measured red (the section above). It is pinned as it.fails and reported for filing, not fixed here. A fix belongs where the package outlives its uninstall: in the running registry, or in the seeding's selection of packages.
  • Response note. The response's note still says the kernel API "does not support unregistering apps in-place". SchemaRegistry.uninstallPackage exists and deletePackage uses it. The wording is kept here because withdrawing the package from the running kernel is outside this card. That withdrawal is also what would close the re-seed window.
  • Retry after a failed cleanup. It answers 404 on this door, because the ledger entry is gone. The protocol door has the same property once its sys_packages row is gone. The warn names the remedy that works: install the package again, then uninstall it again.

Generated by Claude Code

claude added 4 commits October 3, 2026 00:41
…ission set or grant behind

Reproduces the defect at the public door on both orders of events
(hot install -> DELETE -> restart; install -> restart -> DELETE -> restart).
Red on main until the uninstall runs the registered cleanups.

Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-Authored-By: Claude <noreply@anthropic.com>
… registered uninstall cleanups

DELETE /api/v1/marketplace/install-local/:manifestId removed the ledger
entry and nothing else, so the package's managed_by: package permission
sets and every grant of them outlived the uninstall. The door now calls
the protocol's uninstall-cleanup runner (the registry deletePackage runs)
once the ledger entry is gone, with the manifest id and no organization,
and answers each outcome as cleanups. No second revocation path lives in
cloud-connection.

The runner itself (protocol.runUninstallCleanups) is a metadata-protocol
edit sequenced separately; until it lands this door reports one failed
outcome naming it rather than an empty list.

Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 3, 2026
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cloud-connection, @objectstack/metadata-protocol, touching 11 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/cloud-connection/package.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 6dd99b82c38cd68b51c86241d53c5b7dda670a08.

⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/cloud-connection/package.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6dd99b82c38cd68b51c86241d53c5b7dda670a08 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 292fffcece61c7fa6c02ae8aff7fba06b534fa84 — the merge of head cd5cabce34aff75de8855ca4aa501c18d3a55020 into base 6dd99b82c38cd68b51c86241d53c5b7dda670a08, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 292fffcece61c7fa6c02ae8aff7fba06b534fa84 && git checkout 292fffcece61c7fa6c02ae8aff7fba06b534fa84
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6dd99b82c38cd68b51c86241d53c5b7dda670a08 cd5cabce34aff75de8855ca4aa501c18d3a55020 && git checkout -B drift-repro 6dd99b82c38cd68b51c86241d53c5b7dda670a08 && git merge --no-ff cd5cabce34aff75de8855ca4aa501c18d3a55020

node scripts/docs-audit/affected-docs.mjs --json 6dd99b82c38cd68b51c86241d53c5b7dda670a08

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6dd99b82c38cd68b51c86241d53c5b7dda670a08 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

claude added 4 commits October 3, 2026 09:14
…runUninstallCleanups

deletePackage's step-7 loop over the registerUninstallCleanup registry
moves verbatim into a public runUninstallCleanups method, and
deletePackage calls it. The only change inside the loop is the warn tag,
now [protocol.runUninstallCleanups]. install-local's uninstall door calls
the same runner, so both doors run one registry through one runner.

The new test pins the args each cleanup receives, failure as an outcome
with driver text withheld, and the control: deletePackage reports
exactly the runner's outcomes.

Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-Authored-By: Claude <noreply@anthropic.com>
…anups; Clause-② is yes

The extracted runner is a new public method on the exported
ObjectStackProtocolImplementation, an additive widening of
@objectstack/metadata-protocol's public surface, so that package is
graded minor and the declaration line reads Clause-②: yes.
cloud-connection stays a patch.

Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-Authored-By: Claude <noreply@anthropic.com>
A third order of events in the uninstall pin: hot install, DELETE, then a
hot install of another package, then restart. The DELETE leaves the
package registered until the restart, and the second install's
metadata:reloaded re-runs plugin-security's declared-permission seeding
over it, so the uninstalled package's set is re-projected as a fresh
managed_by: package row and survives the restart as an orphan. Its grant
stays revoked; that half is a plain assertion. The two set readings are
it.fails, measured red and reported for filing, not fixed here.

Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: cd5cabce34aff75de8855ca4aa501c18d3a55020
Local-runs: none

Inputs: card #21490 (body and all 7 comments, triage through ACCEPT), PR #21512 (body, 9-file list, net diff against main at the merge base 6dd99b82c3), and the check-runs on the head (latest run per name, every one completed). Read-only: nothing built, run or re-run. The raw Test Core job logs were not readable from this seat (the log host was refused by the proxy), so per-file readings below are the dev's and the check-run conclusions are the gate verdicts. Security posture: classes, doors and roles only.

① Derived judgments

  1. Public surface widened, @objectstack/metadata-protocol — RIGHT. ObjectStackProtocolImplementation (exported from the package index) gains runUninstallCleanups(request), request Pick of DeletePackageRequest over packageId, organizationId, actor, answering UninstallCleanupOutcome[]. Purely additive; no export removed or renamed. This package carries no api-surface snapshot (only packages/spec does), so no artifact regeneration was owed; TypeScript Type Check and Build Core are success.
  2. The extraction is verbatim — RIGHT. The loop removed from deletePackage and the body added to the runner match hunk for hunk: same request projection, same outcome shape, same [finding] metadata-protocol interpolates raw driver text into client-facing messages — three downstream sanitizers each have a hole because of it (option C of #8086) #8136 withholding (clientFacingFailureText) and [Decision] metadataStoreUnavailableError destroys a producer's userMessage mark AT THE PRODUCER — a metadata app's marked refusal on sys_metadata can never reach any door #12536 userMessage mark; the only change is the warn tag. deletePackage hands the runner its own request and keeps its failed / deleted verdict logic untouched. The four existing cleanup suites are the control and ride in the green Test Core.
  3. The install-local door's accept set is unchanged — RIGHT. DELETE /api/v1/marketplace/install-local/:manifestId still demands the manage_metadata admission, answers 400 on an empty id, 404 (RESOURCE_NOT_FOUND) on an id not in this door's own ledger, and 500 (MARKETPLACE_STORAGE_FAILED) on a failed ledger write; the runner is reached only after the removal succeeded. The ordering (revoke only once the ledger entry is gone) is the right side of the security condition: a package that stays installed keeps its grants, and a failed revocation is reported on the response rather than hidden. The unit pin asserts the runner was never called on each refusal path and asserts the exact request on the preservation path, both halves.
  4. Response payload widened — RIGHT, and named as a Clause-② act in its own right. data.cleanups is a new key on a published payload; the note text changed. The route ledger keeps the door server-only, and at the base no CLI command reads this response, so nothing consumes the shape by contract; the PR-scoped Clause-②: yes covers this act as well as item 1.
  5. The request the door builds — RIGHT. packageId is the manifest id, not the ledger's catalog packageId (the registry and every row a domain plugin stamped key on manifest.id; the unit pin seeds a ledger whose catalog id differs and asserts the manifest id reaches the runner). No organizationId: the registered security.package-permissions cleanup at the merge base reads only packageId and revokes installation-wide, and deletePackage under allTenants hands the runner no organization either, so this door matches the protocol door's own cross-tenant uninstall. The runner carries no tenant-scope guard of its own; deletePackage keeps TENANT_SCOPE_REQUIRED ahead of it, so nothing is relaxed on the protocol door.
  6. Per-consumer narrowing, not a lenient alias — RIGHT. The protocol slot is uncontracted (core-service-contracts.ts: protocol and mcp have no written contract), and UninstallCleanupRunner is the same shape PackagesDomainProtocol uses in packages/runtime/src/domains/packages.ts: the verb name is the consumer's, the request and outcome types are the producer's. A protocol without the runner answers one failed outcome naming the upgrade, never an empty list, so "nothing to revoke" and "the revocation never ran" read differently; absence is loud.
  7. Failure disclosure and log level — RIGHT. Driver text never reaches the wire: the runner withholds it per [finding] metadata-protocol interpolates raw driver text into client-facing messages — three downstream sanitizers each have a hole because of it (option C of #8086) #8136, and the door's own catch emits a fixed sentence and logs the cause. A failed cleanup rides on the response as a per-item outcome, the "told the caller" shape, so warn (matching deletePackage's own level) is the right level; Lint & Repo Gates (which carries check:durability-log-level) is success.
  8. One registry, one runner, no second revocation path in cloud-connection — RIGHT. The door knows no table; a cleanup registered tomorrow fires here with no edit to the file. This is triage's direction (5963310358) exactly.
  9. Dependency edge — RIGHT. @objectstack/cloud-connection declares @objectstack/metadata-protocol for a type-only import it already received transitively; no cycle (metadata-protocol depends on neither cloud-connection nor runtime); the lockfile hunk is the one importer entry; Validate Package Dependencies and Build Core (frozen lockfile) are success.
  10. Pins and their CI population — RIGHT. .integration.test.ts is queue population by the nightly name predicate (scripts/nightly-tiers.mjs selects only .e2e. and .live. names), so the cli integration pin sits in the population Test Core runs under OS_TEST_TIERS=queue; Test Core and all six shards are success on the head. The runner test and the cloud-connection unit pin ride the same job.
  11. The it.fails pair for the re-seed window — RIGHT, with one stated limit. It is a measured red recorded in place, with the repo's precedent (packages/cli/test/commands.test.ts), the plain precondition and revoked-grant cases holding the halves this PR fixes, and the carrier [finding] After an install-local uninstall, a later hot install of ANOTHER package re-projects the uninstalled package's permission set, which survives the restart as an orphan package-managed row #21576 filed and open (bug, finding). The limit: it.fails is green on any failure of its body, so the two readings pin "still broken", not "broken in exactly this way"; the filed carrier is what holds the specific reading. Not a disabled test.
  12. Landing class — RIGHT. No governed surface in the file list (Governed Surface Queue Guard success), head repo equals base repo, 1,085 changed lines. The contract review is owed by the Clause-② path of the lane text, not by governance.

Nothing in the diff was judged wrong.

② Semver level

  • Changeset .changeset/21490-install-local-uninstall-cleanups.md: @objectstack/cloud-connection: patch, @objectstack/metadata-protocol: minor; body carries Clause-②: yes and describes the new method, the never-throws contract, the data.cleanups key, the unchanged deletePackage outcomes, the warn-tag change and the declared dependency, sentence by sentence as the diff reads. Both packages publish (publishConfig.access: public, neither private), so a changeset, not skip-changeset, is the right form. Nothing is removed or renamed, so no BREAKING banner, migration text or ADR-0087 marker is owed.
  • Clause-②: line — yes, line-initial in the PR body under Fixes #21490, and the same in the changeset. RIGHT on two counts: the new public method on an exported class (the seat's ruling at 5964293631, grounded in the lane text 「放宽接受集或扩大公开面的卡,不论多小,即条款②」), and the new key on the published response payload (the gate header's own parenthetical).
  • Level axis — discharged: a declared yes with @objectstack/metadata-protocol: minor. Check Changeset on the head is success (its latest run, after the needs:contract-review label event, completed success).
  • cloud-connection at patch — RIGHT by the letter of the WHICH LEVEL rule (exports and accepted keys; data.cleanups is emitted on a server-only door, not accepted and not an export). minor would also have been defensible, and both packages sit in the 69-member fixed group, so the released version moves minor across the group either way; no publishable consequence turns on the per-package grade.

③ Boundary flags

Open question, round 1 (Clause-② A or B for the protocol edit) — answered by the seat (5964293631): B, yes with minor. Judged RIGHT, for the reasons in ②.

Open questions, round 2 — none declared; none found.

Round-1 deviations, each resolved at the head:

  • protocol.ts kept off the branch behind PR fix(metadata-protocol): refuse an org-scoped public form withdrawal a walled posture cannot honour #21473 — the hold was released at the unlock (5967595208) and the edit is now in the net diff.
  • temporary dist overlay for one integration leg — round 2 reads green on real built packages; CI builds fresh regardless.
  • lint delivered as a narrowing — round 2 ran the full pnpm lint, exit 0; Lint & Repo Gates is success on the head.
  • lockfile hand-trimmed — the net diff holds exactly the one importer hunk; the frozen-lockfile install in Build Core is success.
  • footer and trailer form — the PR body carries the session-URL footer; all seven authored commits carry the model-free trailer pair (the two merge commits carry none, which the hook does not refuse). No model identifier anywhere in the PR.

Round-2 deviations:

  • worktree attached to the existing local branch; no rebase, no force-push — accepted.
  • two clean merges of origin/main (bd70706713, 6dd99b82c3) — the net diff against main is what was judged; accepted.
  • it.fails form for the re-seed readings — judged in ① item 11; accepted.
  • leg A's DTS step exited 1 under ablation (the unused private helper) — a measurement artefact; the head builds; accepted.
  • ablations measured at ea93d2754e, before the second merge — both anchors (const cleanups = await this.runUninstallCleanups(ctx, manifestId, admission.userId); and the runner's for over this.uninstallCleanups) are present once each in the head diff, unchanged; accepted.
  • two cli unit files refused at collection for want of the package's own dist/ — a prerequisite, re-read green after a build; accepted.

Out-of-scope findings:

Check-runs on the head: every latest run per name is completed. The seven required contexts (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard) are success; Check Changeset is success. Skipped: Console Pin Gate and Packed-tarball smoke (opt-in) on a PR that moves no pin, and the label-event reruns of Auto Label and Check PR Size (whose first runs are success); none is a gate verdict.

Escalated: nothing.

Implemented-by: claude/issue-21490-install-local-uninstall-cleanups
Reviewed-by: session_016GiHYRmLSNWTfbX9gVQkpz

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants