From a935273913933969852916d8086e9e548228e213 Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Thu, 8 Oct 2026 07:17:40 -0700 Subject: [PATCH] Use execution workspace review semantics during bottom-up pin upgrades --- src/capability-language/pin-upgrades.ts | 48 +++++++++++++-- test/pin-upgrades.test.ts | 82 ++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 6 deletions(-) diff --git a/src/capability-language/pin-upgrades.ts b/src/capability-language/pin-upgrades.ts index 21b24f9..4f5d9aa 100644 --- a/src/capability-language/pin-upgrades.ts +++ b/src/capability-language/pin-upgrades.ts @@ -15,7 +15,12 @@ import { } from "./assembly.js"; import { createGitCapabilityResolver } from "./git-resolver.js"; import { snapshotCommit, buildImmutableCandidate } from "./checked-build.js"; -import { planEvolution, type WorkspaceRevision, type EvolutionReview } from "../capability-model/index.js"; +import { + planEvolution, + reviewSemanticMajors, + type WorkspaceRevision, + type EvolutionReview, +} from "../capability-model/index.js"; import { withFileLock } from "./file-lock.js"; const execFile = promisify(callback); type Source = { repository: string; commit: string }; @@ -187,6 +192,34 @@ export type UpgradeEffects = { snapshot(root: string): Promise; publish(root: string, commit: string): Promise; }; + +/** Review a successfully built immutable candidate using the same boundary as + * ordinary authoring checks. Execution storage compatibility is checked against + * the current graph at activation; the legacy evolution model is not its schema. + */ +export async function reviewUpgradeCandidate({ + artifact, + baseline, + candidate, + reviews = [], +}: { + artifact: string; + baseline: WorkspaceRevision | null; + candidate: WorkspaceRevision; + reviews?: EvolutionReview[]; +}) { + let execution = false; + try { + await fs.access(path.join(artifact, "execution-plan.json")); + execution = true; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + } + return execution + ? { filename: "semantic-review.json", review: reviewSemanticMajors(baseline, candidate, reviews) } + : { filename: "evolution.json", review: planEvolution(baseline, candidate, { reviews }) }; +} + const effects: UpgradeEffects = { async check(node, root, output, spec) { if (node.kind === "workspace" && !spec.baseline && !spec.bootstrap) @@ -206,10 +239,15 @@ const effects: UpgradeEffects = { ? (JSON.parse(await fs.readFile(spec.baseline, "utf8")) as WorkspaceRevision) : null; const reviews = spec.reviews ? (JSON.parse(await fs.readFile(spec.reviews, "utf8")) as EvolutionReview[]) : []; - const evolution = planEvolution(baseline, JSON.parse(candidate), { reviews }); - await fs.writeFile(path.join(output, "evolution.json"), JSON.stringify(evolution, null, 2)); - if (evolution.blockers.length) - throw new Error(`Refactor required in ${node.directory}: ${evolution.blockers.join("; ")}`); + const { filename, review } = await reviewUpgradeCandidate({ + artifact, + baseline, + candidate: JSON.parse(candidate), + reviews, + }); + await fs.writeFile(path.join(output, filename), JSON.stringify(review, null, 2)); + if (review.blockers.length) + throw new Error(`Refactor required in ${node.directory}: ${review.blockers.join("; ")}`); } }, async snapshot(root) { diff --git a/test/pin-upgrades.test.ts b/test/pin-upgrades.test.ts index 026a733..1dff5f1 100644 --- a/test/pin-upgrades.test.ts +++ b/test/pin-upgrades.test.ts @@ -5,9 +5,89 @@ import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; import { execFile as callback } from "node:child_process"; -import { planPinUpgrades, applyPinUpgrades, type UpgradeEffects } from "../src/capability-language/pin-upgrades.js"; +import { + planPinUpgrades, + applyPinUpgrades, + reviewUpgradeCandidate, + type UpgradeEffects, +} from "../src/capability-language/pin-upgrades.js"; +import { capabilityId } from "../src/capability-model/index.js"; +import { makeValidCapabilityWorkspace } from "./fixtures/capability-model.js"; const execFile = promisify(callback); +test("execution pin upgrades accept unchanged shared attachments without legacy migration planning", async (context) => { + const artifact = await fs.mkdtemp(path.join(os.tmpdir(), "qx-upgrade-execution-review-")); + context.after(() => fs.rm(artifact, { recursive: true, force: true })); + await fs.writeFile(path.join(artifact, "execution-plan.json"), "{}"); + const baseline = makeValidCapabilityWorkspace(); + assert.ok(baseline.sharedAttachments.length); + const candidate = structuredClone(baseline); + candidate.sourceRootCommit = "f".repeat(40); + const { filename, review } = await reviewUpgradeCandidate({ artifact, baseline, candidate }); + assert.equal(filename, "semantic-review.json"); + assert.equal(review.checkerVersion, "quixos-semantic-review-v1"); + assert.deepEqual(review.blockers, []); + assert.equal("migrationRequired" in review, false); + assert.equal("runtimeActions" in review, false); +}); + +test("execution pin upgrades require exact semantic-major receipts and reject foreign workspaces", async (context) => { + const artifact = await fs.mkdtemp(path.join(os.tmpdir(), "qx-upgrade-semantic-review-")); + context.after(() => fs.rm(artifact, { recursive: true, force: true })); + await fs.writeFile(path.join(artifact, "execution-plan.json"), "{}"); + const baseline = makeValidCapabilityWorkspace(); + baseline.conformances[0]!.id = capabilityId.conformance("conformance:project:named"); + const candidate = structuredClone(baseline); + candidate.conformances[0]!.semanticMajor = 2; + const { review } = await reviewUpgradeCandidate({ artifact, baseline, candidate }); + assert.equal(review.reviews.length, 1); + assert.equal(review.reviews[0]!.accepted, false); + assert.match(review.blockers[0]!, /Semantic-major review required/); + const receipt = { + requirementDigest: review.reviews[0]!.requirementDigest, + decision: "accepted-unchanged" as const, + rationale: "Reviewed the consumer against the provider's unchanged label behavior", + agentId: "upgrade-fixture-agent", + }; + assert.deepEqual( + (await reviewUpgradeCandidate({ artifact, baseline, candidate, reviews: [receipt] })).review.blockers, + [], + ); + candidate.packageImports[0]!.source.commit = "f".repeat(40); + assert.equal( + (await reviewUpgradeCandidate({ artifact, baseline, candidate, reviews: [receipt] })).review.reviews[0]!.accepted, + false, + ); + candidate.workspaceId = capabilityId.workspace("workspace:foreign"); + await assert.rejects(() => reviewUpgradeCandidate({ artifact, baseline, candidate }), /different workspace/); +}); + +test("nonexecution pin upgrades retain their evolution checks", async (context) => { + const artifact = await fs.mkdtemp(path.join(os.tmpdir(), "qx-upgrade-evolution-review-")); + context.after(() => fs.rm(artifact, { recursive: true, force: true })); + const baseline = makeValidCapabilityWorkspace(); + const { filename, review } = await reviewUpgradeCandidate({ + artifact, + baseline, + candidate: structuredClone(baseline), + }); + assert.equal(filename, "evolution.json"); + assert.equal(review.checkerVersion, "quixos-evolution-v1"); + assert.ok(review.blockers.some((message) => message.includes("legacy workspace-shared attachments"))); + assert.equal("migrationRequired" in review, true); +}); + +test("unexpected artifact access failures do not select nonexecution checks", async (context) => { + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "qx-upgrade-review-failure-")); + context.after(() => fs.rm(directory, { recursive: true, force: true })); + const artifact = path.join(directory, "not-a-directory"); + await fs.writeFile(artifact, "not an artifact"); + const candidate = makeValidCapabilityWorkspace(); + await assert.rejects(() => reviewUpgradeCandidate({ artifact, baseline: candidate, candidate }), { + code: "ENOTDIR", + }); +}); + test( "real jj snapshots and immutable Git publication propagate a changed interface into the root", { skip: !process.env.QX_CHECK_GENERATOR },