Review execution semantic-major changes without storage migration planning
This commit is contained in:
@@ -7,7 +7,12 @@ import type { convergeAuthoring } from "./authoring-converge.js";
|
|||||||
import { execFile as callback } from "node:child_process";
|
import { execFile as callback } from "node:child_process";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
import { buildImmutableCandidate, checkerIdentity } from "./checked-build.js";
|
import { buildImmutableCandidate, checkerIdentity } 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";
|
||||||
|
|
||||||
export const checkRecordName = (directory: string) => createHash("sha256").update(directory).digest("hex") + ".json";
|
export const checkRecordName = (directory: string) => createHash("sha256").update(directory).digest("hex") + ".json";
|
||||||
export async function checkAuthoring(
|
export async function checkAuthoring(
|
||||||
@@ -105,7 +110,20 @@ export async function checkAuthoring(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||||
}
|
}
|
||||||
if (resource.kind === "workspace" && !options.contractOnly && report.runtime !== "execution") {
|
if (resource.kind === "workspace" && !options.contractOnly && report.runtime === "execution") {
|
||||||
|
report.phase = "semantic-review";
|
||||||
|
if (options.reviews && !options.baseline)
|
||||||
|
throw new Error("Execution semantic reviews require an explicit --baseline candidate");
|
||||||
|
const baseline = options.baseline
|
||||||
|
? (JSON.parse(await fs.readFile(options.baseline, "utf8")) as WorkspaceRevision)
|
||||||
|
: null;
|
||||||
|
const receipts = options.reviews
|
||||||
|
? (JSON.parse(await fs.readFile(options.reviews, "utf8")) as EvolutionReview[])
|
||||||
|
: [];
|
||||||
|
const review = reviewSemanticMajors(baseline, JSON.parse(candidateText), receipts);
|
||||||
|
await fs.writeFile(path.join(output, "semantic-review.json"), JSON.stringify(review, null, 2));
|
||||||
|
report.blockers.push(...review.blockers);
|
||||||
|
} else if (resource.kind === "workspace" && !options.contractOnly) {
|
||||||
report.phase = "evolution";
|
report.phase = "evolution";
|
||||||
await progress();
|
await progress();
|
||||||
let baseline = options.baseline;
|
let baseline = options.baseline;
|
||||||
@@ -154,7 +172,7 @@ export async function checkAuthoring(
|
|||||||
await fs.writeFile(temporary, JSON.stringify(value, null, 2), { flag: "wx", mode: 0o600 });
|
await fs.writeFile(temporary, JSON.stringify(value, null, 2), { flag: "wx", mode: 0o600 });
|
||||||
await fs.rename(temporary, filename);
|
await fs.rename(temporary, filename);
|
||||||
};
|
};
|
||||||
if (report.artifactPath) {
|
if (report.artifactPath && report.compilation === "passed" && !report.blockers.length) {
|
||||||
const graph = JSON.parse(await fs.readFile(path.join(report.artifactPath, "graph.json"), "utf8"));
|
const graph = JSON.parse(await fs.readFile(path.join(report.artifactPath, "graph.json"), "utf8"));
|
||||||
for (const checked of graph.resources) {
|
for (const checked of graph.resources) {
|
||||||
const managed = context.resources.find(
|
const managed = context.resources.find(
|
||||||
|
|||||||
@@ -367,6 +367,36 @@ export const planEvolution = (
|
|||||||
...(next ? { candidate: next } : {}),
|
...(next ? { candidate: next } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const semanticReview = assessSemanticReviews(baseline, candidate, options.reviews ?? [], checkerVersion, [
|
||||||
|
...nextRuntimes.values(),
|
||||||
|
]);
|
||||||
|
const reviews = semanticReview.reviews;
|
||||||
|
blockers.push(...semanticReview.blockers);
|
||||||
|
return {
|
||||||
|
schemaVersion: 1,
|
||||||
|
baselineDigest: baseline ? contentDigest(baseline) : null,
|
||||||
|
candidateDigest,
|
||||||
|
checkerVersion,
|
||||||
|
runtimeActions,
|
||||||
|
storageChanges,
|
||||||
|
migrationRequired: storageChanges.filter((entry) => entry.requiresMigration).map((entry) => entry.id),
|
||||||
|
reviews,
|
||||||
|
packageChecks: runtimeActions
|
||||||
|
.filter((entry) => entry.candidate && entry.action !== "keep")
|
||||||
|
.map((entry) => ({ groupId: entry.groupId, contractDigest: entry.candidate!.digest })),
|
||||||
|
blockers,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Review dependency contracts, without making any storage or process-placement decision. */
|
||||||
|
function assessSemanticReviews(
|
||||||
|
baseline: WorkspaceRevision | null,
|
||||||
|
candidate: WorkspaceRevision,
|
||||||
|
receipts: readonly EvolutionReview[],
|
||||||
|
checkerVersion: string,
|
||||||
|
consumers: readonly RuntimeContract[],
|
||||||
|
): Pick<EvolutionReport, "reviews" | "blockers"> {
|
||||||
|
const blockers: string[] = [];
|
||||||
const providers = (workspace: WorkspaceRevision) => [
|
const providers = (workspace: WorkspaceRevision) => [
|
||||||
...workspace.packageImports.map((entry) => ({
|
...workspace.packageImports.map((entry) => ({
|
||||||
id: entry.packageId as string,
|
id: entry.packageId as string,
|
||||||
@@ -393,7 +423,7 @@ export const planEvolution = (
|
|||||||
}
|
}
|
||||||
if (!old[0] || old[0].major === provider.major) continue;
|
if (!old[0] || old[0].major === provider.major) continue;
|
||||||
if (provider.major < old[0].major) blockers.push(`Semantic major decreases for ${provider.id}`);
|
if (provider.major < old[0].major) blockers.push(`Semantic major decreases for ${provider.id}`);
|
||||||
for (const consumer of nextRuntimes.values()) {
|
for (const consumer of consumers) {
|
||||||
if (consumer.packageId === provider.id || !consumer.reviewProviders.includes(provider.id)) continue;
|
if (consumer.packageId === provider.id || !consumer.reviewProviders.includes(provider.id)) continue;
|
||||||
const requirement = {
|
const requirement = {
|
||||||
consumerId: consumer.groupId,
|
consumerId: consumer.groupId,
|
||||||
@@ -408,7 +438,7 @@ export const planEvolution = (
|
|||||||
consumer: consumer.digest,
|
consumer: consumer.digest,
|
||||||
checkerVersion,
|
checkerVersion,
|
||||||
});
|
});
|
||||||
const accepted = (options.reviews ?? []).some(
|
const accepted = receipts.some(
|
||||||
(entry) =>
|
(entry) =>
|
||||||
entry.requirementDigest === requirementDigest &&
|
entry.requirementDigest === requirementDigest &&
|
||||||
["changed", "accepted-unchanged"].includes(entry.decision) &&
|
["changed", "accepted-unchanged"].includes(entry.decision) &&
|
||||||
@@ -419,18 +449,27 @@ export const planEvolution = (
|
|||||||
if (!accepted) blockers.push(`Semantic-major review required: ${consumer.groupId} consumes ${provider.id}`);
|
if (!accepted) blockers.push(`Semantic-major review required: ${consumer.groupId} consumes ${provider.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return { reviews, blockers };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reviewSemanticMajors(
|
||||||
|
baseline: WorkspaceRevision | null,
|
||||||
|
candidate: WorkspaceRevision,
|
||||||
|
receipts: readonly EvolutionReview[] = [],
|
||||||
|
) {
|
||||||
|
for (const workspace of baseline ? [baseline, candidate] : [candidate]) {
|
||||||
|
const issues = validateWorkspaceRevision(workspace);
|
||||||
|
if (issues.length)
|
||||||
|
throw new Error("Invalid semantic-review workspace: " + issues.map((issue) => issue.message).join("; "));
|
||||||
|
}
|
||||||
|
if (baseline && baseline.workspaceId !== candidate.workspaceId)
|
||||||
|
throw new Error("Cannot review a different workspace");
|
||||||
|
const checkerVersion = "quixos-semantic-review-v1";
|
||||||
|
const report = assessSemanticReviews(baseline, candidate, receipts, checkerVersion, runtimeContracts(candidate));
|
||||||
return {
|
return {
|
||||||
schemaVersion: 1,
|
|
||||||
baselineDigest: baseline ? contentDigest(baseline) : null,
|
|
||||||
candidateDigest,
|
|
||||||
checkerVersion,
|
checkerVersion,
|
||||||
runtimeActions,
|
baselineDigest: baseline ? contentDigest(baseline) : null,
|
||||||
storageChanges,
|
candidateDigest: contentDigest(candidate),
|
||||||
migrationRequired: storageChanges.filter((entry) => entry.requiresMigration).map((entry) => entry.id),
|
...report,
|
||||||
reviews,
|
|
||||||
packageChecks: runtimeActions
|
|
||||||
.filter((entry) => entry.candidate && entry.action !== "keep")
|
|
||||||
.map((entry) => ({ groupId: entry.groupId, contractDigest: entry.candidate!.digest })),
|
|
||||||
blockers,
|
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { capabilityId as id, valueType, validateWorkspaceRevision } from "../src
|
|||||||
import {
|
import {
|
||||||
contentDigest,
|
contentDigest,
|
||||||
planEvolution,
|
planEvolution,
|
||||||
|
reviewSemanticMajors,
|
||||||
runtimeContracts,
|
runtimeContracts,
|
||||||
storageContracts,
|
storageContracts,
|
||||||
storageChangeRequiresMigration,
|
storageChangeRequiresMigration,
|
||||||
@@ -126,6 +127,42 @@ test("semantic review receipts cover exact consumer/provider contracts", () => {
|
|||||||
assert.equal(planEvolution(before, after, { allowLegacy: true, reviews: [receipt] }).reviews[0]!.accepted, false);
|
assert.equal(planEvolution(before, after, { allowLegacy: true, reviews: [receipt] }).reviews[0]!.accepted, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("execution semantic review is independent of storage migration and process action planning", () => {
|
||||||
|
const before = makeValidCapabilityWorkspace();
|
||||||
|
before.conformances[0]!.id = id.conformance("conformance:project:named");
|
||||||
|
const after = structuredClone(before);
|
||||||
|
after.conformances[0]!.semanticMajor = 2;
|
||||||
|
const report = reviewSemanticMajors(before, after);
|
||||||
|
assert.equal(report.checkerVersion, "quixos-semantic-review-v1");
|
||||||
|
assert.equal(report.reviews.length, 1);
|
||||||
|
assert.equal(report.reviews[0]!.accepted, false);
|
||||||
|
assert.equal("migrationRequired" in report, false);
|
||||||
|
assert.equal("runtimeActions" in report, false);
|
||||||
|
assert.equal(report.blockers.length, 1);
|
||||||
|
const receipt = {
|
||||||
|
requirementDigest: report.reviews[0]!.requirementDigest,
|
||||||
|
decision: "accepted-unchanged" as const,
|
||||||
|
rationale: "The consumer only displays this provider's unchanged label contract",
|
||||||
|
agentId: "fixture-agent",
|
||||||
|
};
|
||||||
|
assert.deepEqual(reviewSemanticMajors(before, after, [receipt]).blockers, []);
|
||||||
|
const oldReceipt = {
|
||||||
|
...receipt,
|
||||||
|
requirementDigest: planEvolution(before, after, { allowLegacy: true }).reviews[0]!.requirementDigest,
|
||||||
|
};
|
||||||
|
assert.equal(reviewSemanticMajors(before, after, [oldReceipt]).reviews[0]!.accepted, false);
|
||||||
|
after.packageImports[0]!.source.commit = "f".repeat(40);
|
||||||
|
assert.equal(reviewSemanticMajors(before, after, [receipt]).reviews[0]!.accepted, false);
|
||||||
|
assert.deepEqual(reviewSemanticMajors(null, after).blockers, []);
|
||||||
|
assert.throws(
|
||||||
|
() => reviewSemanticMajors(before, { ...after, workspaceId: id.workspace("different") }),
|
||||||
|
/different workspace/,
|
||||||
|
);
|
||||||
|
const earlier = structuredClone(after);
|
||||||
|
earlier.conformances[0]!.semanticMajor = 1;
|
||||||
|
assert.ok(reviewSemanticMajors(after, earlier).blockers.some((message) => message.includes("decreases")));
|
||||||
|
});
|
||||||
|
|
||||||
test("evolution enrollment is explicit and never erases legacy ownership", () => {
|
test("evolution enrollment is explicit and never erases legacy ownership", () => {
|
||||||
const workspace = makeValidCapabilityWorkspace();
|
const workspace = makeValidCapabilityWorkspace();
|
||||||
const report = planEvolution(workspace, workspace);
|
const report = planEvolution(workspace, workspace);
|
||||||
|
|||||||
Reference in New Issue
Block a user