Format authored monorepo code with pinned language formatters
This commit is contained in:
@@ -3,50 +3,75 @@ import type { Binding, Conformance, DependencyBinding, PersistentAttachment, Wor
|
||||
import { validateWorkspaceRevision } from "./validation.js";
|
||||
|
||||
/** Content hashing is independent of JSON object insertion order, not array order. */
|
||||
const compareText = (a: string, b: string) => a < b ? -1 : a > b ? 1 : 0;
|
||||
const compareText = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
|
||||
export const canonicalJson = (value: unknown): string => {
|
||||
if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
||||
if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
|
||||
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
||||
if (typeof value === "object" && value !== null) {
|
||||
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new Error("Expected a plain JSON object");
|
||||
return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([a], [b]) => compareText(a, b))
|
||||
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
||||
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
||||
throw new Error("Expected a plain JSON object");
|
||||
return `{${Object.entries(value)
|
||||
.filter(([, entry]) => entry !== undefined)
|
||||
.sort(([a], [b]) => compareText(a, b))
|
||||
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
throw new Error(`Cannot hash non-JSON value: ${typeof value}`);
|
||||
};
|
||||
export const contentDigest = (value: unknown) => `sha256:${createHash("sha256").update(canonicalJson(value)).digest("hex")}`;
|
||||
export const contentDigest = (value: unknown) =>
|
||||
`sha256:${createHash("sha256").update(canonicalJson(value)).digest("hex")}`;
|
||||
const semantic = (value: unknown): unknown => {
|
||||
if (Array.isArray(value)) return value.map(semantic);
|
||||
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value)
|
||||
.filter(([key, entry]) => entry !== undefined && key !== "displayName" && key !== "documentation")
|
||||
// These are authored data/maps, not schema nodes. A user field literally
|
||||
// named displayName or documentation is semantic and must stay in the hash.
|
||||
.map(([key, entry]) => [key, key === "defaultValue" || key === "fields" ? entry : semantic(entry)]));
|
||||
if (value && typeof value === "object")
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.filter(([key, entry]) => entry !== undefined && key !== "displayName" && key !== "documentation")
|
||||
// These are authored data/maps, not schema nodes. A user field literally
|
||||
// named displayName or documentation is semantic and must stay in the hash.
|
||||
.map(([key, entry]) => [key, key === "defaultValue" || key === "fields" ? entry : semantic(entry)]),
|
||||
);
|
||||
return value;
|
||||
};
|
||||
const sorted = <T>(entries: readonly T[], key: (entry: T) => string) => [...entries].sort((a, b) => compareText(key(a), key(b)));
|
||||
export const conformanceIdentity = (entry: Conformance): string => entry.id ?? `legacy:${entry.atomId}:${entry.interfaceRevisionId}`;
|
||||
const sorted = <T>(entries: readonly T[], key: (entry: T) => string) =>
|
||||
[...entries].sort((a, b) => compareText(key(a), key(b)));
|
||||
export const conformanceIdentity = (entry: Conformance): string =>
|
||||
entry.id ?? `legacy:${entry.atomId}:${entry.interfaceRevisionId}`;
|
||||
|
||||
export type StorageContract = { id: string; ownerId: string; kind: "state" | "edge"; digest: string; definition: unknown };
|
||||
export type StorageContract = {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
kind: "state" | "edge";
|
||||
digest: string;
|
||||
definition: unknown;
|
||||
};
|
||||
/** Automatic evolution preserves values; it never interprets migration code or
|
||||
* guesses that a new nominal message descriptor means the same representation. */
|
||||
export const storageChangeRequiresMigration = (previous: StorageContract | undefined, next: StorageContract | undefined,
|
||||
oldAtomIds: ReadonlySet<string>): boolean => {
|
||||
export const storageChangeRequiresMigration = (
|
||||
previous: StorageContract | undefined,
|
||||
next: StorageContract | undefined,
|
||||
oldAtomIds: ReadonlySet<string>,
|
||||
): boolean => {
|
||||
if (!next) return true;
|
||||
const after = next.definition as PersistentAttachment;
|
||||
if (!previous) {
|
||||
if (after.kind === "state") return oldAtomIds.has(after.attachedTo) && after.defaultValue === undefined && after.valueType.kind !== "optional";
|
||||
return after.endpoints.some(endpoint => endpoint.cardinality === "exactly-one" &&
|
||||
(endpoint.constraint.kind !== "atom" || oldAtomIds.has(endpoint.constraint.atomId)));
|
||||
if (after.kind === "state")
|
||||
return (
|
||||
oldAtomIds.has(after.attachedTo) && after.defaultValue === undefined && after.valueType.kind !== "optional"
|
||||
);
|
||||
return after.endpoints.some(
|
||||
(endpoint) =>
|
||||
endpoint.cardinality === "exactly-one" &&
|
||||
(endpoint.constraint.kind !== "atom" || oldAtomIds.has(endpoint.constraint.atomId)),
|
||||
);
|
||||
}
|
||||
if (previous.ownerId !== next.ownerId || previous.kind !== next.kind) return true;
|
||||
const before = previous.definition as PersistentAttachment;
|
||||
if (before.kind === "state" && after.kind === "state") {
|
||||
// Capture materializes old defaults, so changing a default affects only
|
||||
// newly constructed objects, not existing sparse state.
|
||||
const {defaultValue: _beforeDefault, ...beforeStorage} = before;
|
||||
const {defaultValue: _afterDefault, ...afterStorage} = after;
|
||||
const { defaultValue: _beforeDefault, ...beforeStorage } = before;
|
||||
const { defaultValue: _afterDefault, ...afterStorage } = after;
|
||||
return canonicalJson(beforeStorage) !== canonicalJson(afterStorage);
|
||||
}
|
||||
return canonicalJson(before) !== canonicalJson(after);
|
||||
@@ -55,15 +80,29 @@ export const storageContracts = (workspace: WorkspaceRevision): StorageContract[
|
||||
const result: StorageContract[] = [];
|
||||
const add = (attachment: PersistentAttachment, ownerId: string) => {
|
||||
const definition = semantic(attachment);
|
||||
result.push({ id: attachment.id, ownerId, kind: attachment.kind, definition, digest: contentDigest({ ownerId, definition }) });
|
||||
result.push({
|
||||
id: attachment.id,
|
||||
ownerId,
|
||||
kind: attachment.kind,
|
||||
definition,
|
||||
digest: contentDigest({ ownerId, definition }),
|
||||
});
|
||||
};
|
||||
for (const attachment of workspace.sharedAttachments) add(attachment, "legacy:workspace");
|
||||
for (const conformance of workspace.conformances) for (const attachment of conformance.privateAttachments) add(attachment, conformanceIdentity(conformance));
|
||||
for (const conformance of workspace.conformances)
|
||||
for (const attachment of conformance.privateAttachments) add(attachment, conformanceIdentity(conformance));
|
||||
return sorted(result, (entry) => entry.id);
|
||||
};
|
||||
|
||||
type GraphNode = { value: unknown; dependencies: Set<string>; reviewProviders: Set<string> };
|
||||
export type RuntimeContract = { groupId: string; packageId: string; packageRevisionId: string; digest: string; reviewProviders: string[]; dependencies: Array<{ id: string; digest: string }> };
|
||||
export type RuntimeContract = {
|
||||
groupId: string;
|
||||
packageId: string;
|
||||
packageRevisionId: string;
|
||||
digest: string;
|
||||
reviewProviders: string[];
|
||||
dependencies: Array<{ id: string; digest: string }>;
|
||||
};
|
||||
|
||||
/** Build only outbound execution dependencies. Incoming callers never retain or invalidate a provider. */
|
||||
export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[] => {
|
||||
@@ -75,13 +114,23 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
|
||||
};
|
||||
const conformanceKey = (atom: string, iface: string) => `conformance:${atom}:${iface}`;
|
||||
for (const storage of storageContracts(workspace)) node(`attachment:${storage.id}`, storage);
|
||||
for (const iface of workspace.interfaceImports) node(`interface:${iface.revisionId}`, {
|
||||
...iface, members: sorted(iface.members, (entry) => entry.id).map((entry) => ({ ...entry, operations: sorted(entry.operations, (operation) => operation.id) })),
|
||||
});
|
||||
for (const pkg of workspace.packageImports) node(`package:${pkg.revisionId}`, {
|
||||
...pkg, semanticMajor: pkg.semanticMajor ?? 1,
|
||||
exports: sorted(pkg.exports, (entry) => entry.id).map((entry) => ({ ...entry, dependencyPorts: sorted(entry.dependencyPorts, (port) => port.id) })),
|
||||
});
|
||||
for (const iface of workspace.interfaceImports)
|
||||
node(`interface:${iface.revisionId}`, {
|
||||
...iface,
|
||||
members: sorted(iface.members, (entry) => entry.id).map((entry) => ({
|
||||
...entry,
|
||||
operations: sorted(entry.operations, (operation) => operation.id),
|
||||
})),
|
||||
});
|
||||
for (const pkg of workspace.packageImports)
|
||||
node(`package:${pkg.revisionId}`, {
|
||||
...pkg,
|
||||
semanticMajor: pkg.semanticMajor ?? 1,
|
||||
exports: sorted(pkg.exports, (entry) => entry.id).map((entry) => ({
|
||||
...entry,
|
||||
dependencyPorts: sorted(entry.dependencyPorts, (port) => port.id),
|
||||
})),
|
||||
});
|
||||
const dependency = (parent: GraphNode, binding: DependencyBinding, atomId: string, reviews?: Set<string>) => {
|
||||
if (binding.kind === "state") parent.dependencies.add(`attachment:${binding.slotId}`);
|
||||
if (binding.kind === "edge") parent.dependencies.add(`attachment:${binding.edgeTypeId}`);
|
||||
@@ -96,7 +145,10 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
|
||||
parent.dependencies.add(`interface:${binding.interfaceRevisionId}`);
|
||||
// An edge traversal may select any matching target. Conservatively include every possible witness.
|
||||
for (const conformance of workspace.conformances) {
|
||||
if (conformance.interfaceRevisionId === binding.interfaceRevisionId && (binding.via || conformance.atomId === atomId)) {
|
||||
if (
|
||||
conformance.interfaceRevisionId === binding.interfaceRevisionId &&
|
||||
(binding.via || conformance.atomId === atomId)
|
||||
) {
|
||||
parent.dependencies.add(conformanceKey(conformance.atomId, conformance.interfaceRevisionId));
|
||||
reviews?.add(conformanceIdentity(conformance));
|
||||
for (const operation of conformance.operationBindings) {
|
||||
@@ -110,7 +162,10 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
|
||||
}
|
||||
};
|
||||
const binding = (parent: GraphNode, value: Binding, atomId: string, context: unknown) => {
|
||||
if (value.kind !== "package") { dependency(parent, value, atomId); return; }
|
||||
if (value.kind !== "package") {
|
||||
dependency(parent, value, atomId);
|
||||
return;
|
||||
}
|
||||
const packageNode = nodes.get(`package:${value.packageRevisionId}`)!;
|
||||
parent.dependencies.add(`package:${value.packageRevisionId}`);
|
||||
const normalized = { ...value, dependencies: sorted(value.dependencies, (entry) => entry.portId) };
|
||||
@@ -121,16 +176,21 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
|
||||
};
|
||||
for (const conformance of workspace.conformances) {
|
||||
const parent = node(conformanceKey(conformance.atomId, conformance.interfaceRevisionId), {
|
||||
id: conformanceIdentity(conformance), semanticMajor: conformance.semanticMajor ?? 1,
|
||||
atomId: conformance.atomId, interfaceRevisionId: conformance.interfaceRevisionId,
|
||||
id: conformanceIdentity(conformance),
|
||||
semanticMajor: conformance.semanticMajor ?? 1,
|
||||
atomId: conformance.atomId,
|
||||
interfaceRevisionId: conformance.interfaceRevisionId,
|
||||
operations: sorted(conformance.operationBindings, (entry) => entry.operationId),
|
||||
materializations: sorted(conformance.relationshipMaterializations, (entry) => entry.memberId),
|
||||
});
|
||||
parent.dependencies.add(`interface:${conformance.interfaceRevisionId}`);
|
||||
for (const attachment of conformance.privateAttachments) parent.dependencies.add(`attachment:${attachment.id}`);
|
||||
for (const operation of conformance.operationBindings) binding(parent, operation.binding, conformance.atomId, {
|
||||
conformanceId: conformanceIdentity(conformance), semanticMajor: conformance.semanticMajor ?? 1, operationId: operation.operationId,
|
||||
});
|
||||
for (const operation of conformance.operationBindings)
|
||||
binding(parent, operation.binding, conformance.atomId, {
|
||||
conformanceId: conformanceIdentity(conformance),
|
||||
semanticMajor: conformance.semanticMajor ?? 1,
|
||||
operationId: operation.operationId,
|
||||
});
|
||||
for (const materialization of conformance.relationshipMaterializations) {
|
||||
parent.dependencies.add(`constructor:${materialization.constructorAtomId}`);
|
||||
parent.dependencies.add(`attachment:${materialization.edgeTypeId}`);
|
||||
@@ -142,91 +202,195 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
|
||||
}
|
||||
const counts = new Map<string, number>();
|
||||
for (const pkg of workspace.packageImports) counts.set(pkg.packageId, (counts.get(pkg.packageId) ?? 0) + 1);
|
||||
return sorted(workspace.packageImports.map((pkg): RuntimeContract => {
|
||||
const visited = new Set<string>();
|
||||
const walk = (key: string) => {
|
||||
if (visited.has(key)) return;
|
||||
const entry = nodes.get(key);
|
||||
if (!entry) throw new Error(`Unresolved execution dependency ${key}`);
|
||||
visited.add(key);
|
||||
for (const target of entry.dependencies) walk(target);
|
||||
};
|
||||
walk(`package:${pkg.revisionId}`);
|
||||
const dependencies = [...visited].sort().map((id) => ({ id, digest: contentDigest(nodes.get(id)!.value) }));
|
||||
return { groupId: counts.get(pkg.packageId) === 1 ? pkg.packageId : `${pkg.packageId}#${pkg.revisionId}`,
|
||||
packageId: pkg.packageId, packageRevisionId: pkg.revisionId, digest: contentDigest(dependencies),
|
||||
reviewProviders: [...nodes.get(`package:${pkg.revisionId}`)!.reviewProviders].sort(), dependencies };
|
||||
}), (entry) => entry.groupId);
|
||||
return sorted(
|
||||
workspace.packageImports.map((pkg): RuntimeContract => {
|
||||
const visited = new Set<string>();
|
||||
const walk = (key: string) => {
|
||||
if (visited.has(key)) return;
|
||||
const entry = nodes.get(key);
|
||||
if (!entry) throw new Error(`Unresolved execution dependency ${key}`);
|
||||
visited.add(key);
|
||||
for (const target of entry.dependencies) walk(target);
|
||||
};
|
||||
walk(`package:${pkg.revisionId}`);
|
||||
const dependencies = [...visited].sort().map((id) => ({ id, digest: contentDigest(nodes.get(id)!.value) }));
|
||||
return {
|
||||
groupId: counts.get(pkg.packageId) === 1 ? pkg.packageId : `${pkg.packageId}#${pkg.revisionId}`,
|
||||
packageId: pkg.packageId,
|
||||
packageRevisionId: pkg.revisionId,
|
||||
digest: contentDigest(dependencies),
|
||||
reviewProviders: [...nodes.get(`package:${pkg.revisionId}`)!.reviewProviders].sort(),
|
||||
dependencies,
|
||||
};
|
||||
}),
|
||||
(entry) => entry.groupId,
|
||||
);
|
||||
};
|
||||
|
||||
export type EvolutionReview = { requirementDigest: string; decision: "changed" | "accepted-unchanged"; rationale: string; agentId: string };
|
||||
export type ReviewRequirement = { consumerId: string; providerId: string; oldMajor: number; newMajor: number; requirementDigest: string };
|
||||
export type RuntimeAction = { groupId: string; action: "keep" | "start" | "replace" | "retire"; previous?: RuntimeContract; candidate?: RuntimeContract; reasons: string[] };
|
||||
export type EvolutionReview = {
|
||||
requirementDigest: string;
|
||||
decision: "changed" | "accepted-unchanged";
|
||||
rationale: string;
|
||||
agentId: string;
|
||||
};
|
||||
export type ReviewRequirement = {
|
||||
consumerId: string;
|
||||
providerId: string;
|
||||
oldMajor: number;
|
||||
newMajor: number;
|
||||
requirementDigest: string;
|
||||
};
|
||||
export type RuntimeAction = {
|
||||
groupId: string;
|
||||
action: "keep" | "start" | "replace" | "retire";
|
||||
previous?: RuntimeContract;
|
||||
candidate?: RuntimeContract;
|
||||
reasons: string[];
|
||||
};
|
||||
export type EvolutionReport = {
|
||||
schemaVersion: 1; baselineDigest: string | null; candidateDigest: string; checkerVersion: string;
|
||||
schemaVersion: 1;
|
||||
baselineDigest: string | null;
|
||||
candidateDigest: string;
|
||||
checkerVersion: string;
|
||||
runtimeActions: RuntimeAction[];
|
||||
storageChanges: Array<{ id: string; kind: "add" | "remove" | "change"; requiresMigration: boolean; previous?: StorageContract; candidate?: StorageContract }>;
|
||||
storageChanges: Array<{
|
||||
id: string;
|
||||
kind: "add" | "remove" | "change";
|
||||
requiresMigration: boolean;
|
||||
previous?: StorageContract;
|
||||
candidate?: StorageContract;
|
||||
}>;
|
||||
migrationRequired: string[];
|
||||
reviews: Array<ReviewRequirement & { accepted: boolean }>;
|
||||
packageChecks: Array<{ groupId: string; contractDigest: string }>;
|
||||
blockers: string[];
|
||||
};
|
||||
|
||||
export const planEvolution = (baseline: WorkspaceRevision | null, candidate: WorkspaceRevision,
|
||||
options: { reviews?: EvolutionReview[]; allowLegacy?: boolean } = {}): EvolutionReport => {
|
||||
export const planEvolution = (
|
||||
baseline: WorkspaceRevision | null,
|
||||
candidate: WorkspaceRevision,
|
||||
options: { reviews?: EvolutionReview[]; allowLegacy?: boolean } = {},
|
||||
): EvolutionReport => {
|
||||
const issues = validateWorkspaceRevision(candidate);
|
||||
if (issues.length) throw new Error(`Invalid candidate workspace:\n${issues.map((entry) => `${entry.path}: ${entry.message}`).join("\n")}`);
|
||||
if (baseline && baseline.workspaceId !== candidate.workspaceId) throw new Error("Cannot evolve a different workspace");
|
||||
if (issues.length)
|
||||
throw new Error(
|
||||
`Invalid candidate workspace:\n${issues.map((entry) => `${entry.path}: ${entry.message}`).join("\n")}`,
|
||||
);
|
||||
if (baseline && baseline.workspaceId !== candidate.workspaceId)
|
||||
throw new Error("Cannot evolve a different workspace");
|
||||
const candidateDigest = contentDigest(candidate);
|
||||
const checkerVersion = "quixos-evolution-v1";
|
||||
const blockers: string[] = [];
|
||||
if (!options.allowLegacy) {
|
||||
if (candidate.sharedAttachments.length) blockers.push("Assign legacy workspace-shared attachments to explicit conformance owners");
|
||||
for (const entry of candidate.conformances) if (!entry.id) blockers.push(`Conformance ${entry.atomId} as ${entry.interfaceRevisionId} requires an authored ID`);
|
||||
if (candidate.sharedAttachments.length)
|
||||
blockers.push("Assign legacy workspace-shared attachments to explicit conformance owners");
|
||||
for (const entry of candidate.conformances)
|
||||
if (!entry.id)
|
||||
blockers.push(`Conformance ${entry.atomId} as ${entry.interfaceRevisionId} requires an authored ID`);
|
||||
}
|
||||
const previousRuntimes = new Map((baseline ? runtimeContracts(baseline) : []).map((entry) => [entry.groupId, entry]));
|
||||
const nextRuntimes = new Map(runtimeContracts(candidate).map((entry) => [entry.groupId, entry]));
|
||||
const runtimeActions: RuntimeAction[] = [...new Set([...previousRuntimes.keys(), ...nextRuntimes.keys()])].sort().map((groupId) => {
|
||||
const previous = previousRuntimes.get(groupId), next = nextRuntimes.get(groupId);
|
||||
const before = new Map(previous?.dependencies.map((entry) => [entry.id, entry.digest]));
|
||||
const after = new Map(next?.dependencies.map((entry) => [entry.id, entry.digest]));
|
||||
const reasons = [...new Set([...before.keys(), ...after.keys()])].sort().filter((id) => before.get(id) !== after.get(id));
|
||||
return { groupId, action: !previous ? "start" : !next ? "retire" : previous.digest === next.digest ? "keep" : "replace",
|
||||
...(previous ? { previous } : {}), ...(next ? { candidate: next } : {}), reasons };
|
||||
});
|
||||
const runtimeActions: RuntimeAction[] = [...new Set([...previousRuntimes.keys(), ...nextRuntimes.keys()])]
|
||||
.sort()
|
||||
.map((groupId) => {
|
||||
const previous = previousRuntimes.get(groupId),
|
||||
next = nextRuntimes.get(groupId);
|
||||
const before = new Map(previous?.dependencies.map((entry) => [entry.id, entry.digest]));
|
||||
const after = new Map(next?.dependencies.map((entry) => [entry.id, entry.digest]));
|
||||
const reasons = [...new Set([...before.keys(), ...after.keys()])]
|
||||
.sort()
|
||||
.filter((id) => before.get(id) !== after.get(id));
|
||||
return {
|
||||
groupId,
|
||||
action: !previous ? "start" : !next ? "retire" : previous.digest === next.digest ? "keep" : "replace",
|
||||
...(previous ? { previous } : {}),
|
||||
...(next ? { candidate: next } : {}),
|
||||
reasons,
|
||||
};
|
||||
});
|
||||
const beforeStorage = new Map((baseline ? storageContracts(baseline) : []).map((entry) => [entry.id, entry]));
|
||||
const afterStorage = new Map(storageContracts(candidate).map((entry) => [entry.id, entry]));
|
||||
const storageChanges: EvolutionReport["storageChanges"] = [];
|
||||
for (const id of [...new Set([...beforeStorage.keys(), ...afterStorage.keys()])].sort()) {
|
||||
const previous = beforeStorage.get(id), next = afterStorage.get(id);
|
||||
if (previous?.digest !== next?.digest) storageChanges.push({ id, kind: !previous ? "add" : !next ? "remove" : "change",
|
||||
requiresMigration: storageChangeRequiresMigration(previous, next, new Set(baseline?.atoms.map(atom => atom.id) ?? [])),
|
||||
...(previous ? { previous } : {}), ...(next ? { candidate: next } : {}) });
|
||||
const previous = beforeStorage.get(id),
|
||||
next = afterStorage.get(id);
|
||||
if (previous?.digest !== next?.digest)
|
||||
storageChanges.push({
|
||||
id,
|
||||
kind: !previous ? "add" : !next ? "remove" : "change",
|
||||
requiresMigration: storageChangeRequiresMigration(
|
||||
previous,
|
||||
next,
|
||||
new Set(baseline?.atoms.map((atom) => atom.id) ?? []),
|
||||
),
|
||||
...(previous ? { previous } : {}),
|
||||
...(next ? { candidate: next } : {}),
|
||||
});
|
||||
}
|
||||
const providers = (workspace: WorkspaceRevision) => [
|
||||
...workspace.packageImports.map((entry) => ({ id: entry.packageId as string, revision: entry.revisionId as string, major: entry.semanticMajor ?? 1,
|
||||
node: `package:${entry.revisionId}`, digest: contentDigest(entry) })),
|
||||
...workspace.conformances.map((entry) => ({ id: conformanceIdentity(entry), revision: contentDigest(entry), major: entry.semanticMajor ?? 1,
|
||||
node: `conformance:${entry.atomId}:${entry.interfaceRevisionId}`, digest: contentDigest(entry) })),
|
||||
...workspace.packageImports.map((entry) => ({
|
||||
id: entry.packageId as string,
|
||||
revision: entry.revisionId as string,
|
||||
major: entry.semanticMajor ?? 1,
|
||||
node: `package:${entry.revisionId}`,
|
||||
digest: contentDigest(entry),
|
||||
})),
|
||||
...workspace.conformances.map((entry) => ({
|
||||
id: conformanceIdentity(entry),
|
||||
revision: contentDigest(entry),
|
||||
major: entry.semanticMajor ?? 1,
|
||||
node: `conformance:${entry.atomId}:${entry.interfaceRevisionId}`,
|
||||
digest: contentDigest(entry),
|
||||
})),
|
||||
];
|
||||
const oldProviders = baseline ? providers(baseline) : [];
|
||||
const reviews: EvolutionReport["reviews"] = [];
|
||||
for (const provider of providers(candidate)) {
|
||||
const old = oldProviders.filter((entry) => entry.id === provider.id);
|
||||
if (old.length > 1) { blockers.push(`Ambiguous semantic-major lineage for ${provider.id}`); continue; }
|
||||
if (old.length > 1) {
|
||||
blockers.push(`Ambiguous semantic-major lineage for ${provider.id}`);
|
||||
continue;
|
||||
}
|
||||
if (!old[0] || old[0].major === provider.major) continue;
|
||||
if (provider.major < old[0].major) blockers.push(`Semantic major decreases for ${provider.id}`);
|
||||
for (const consumer of nextRuntimes.values()) {
|
||||
if (consumer.packageId === provider.id || !consumer.reviewProviders.includes(provider.id)) continue;
|
||||
const requirement = { consumerId: consumer.groupId, providerId: provider.id, oldMajor: old[0].major, newMajor: provider.major };
|
||||
const requirementDigest = contentDigest({ ...requirement, oldProvider: old[0].digest, newProvider: provider.digest, consumer: consumer.digest, checkerVersion });
|
||||
const accepted = (options.reviews ?? []).some((entry) => entry.requirementDigest === requirementDigest &&
|
||||
["changed", "accepted-unchanged"].includes(entry.decision) && entry.rationale.trim() && entry.agentId.trim());
|
||||
const requirement = {
|
||||
consumerId: consumer.groupId,
|
||||
providerId: provider.id,
|
||||
oldMajor: old[0].major,
|
||||
newMajor: provider.major,
|
||||
};
|
||||
const requirementDigest = contentDigest({
|
||||
...requirement,
|
||||
oldProvider: old[0].digest,
|
||||
newProvider: provider.digest,
|
||||
consumer: consumer.digest,
|
||||
checkerVersion,
|
||||
});
|
||||
const accepted = (options.reviews ?? []).some(
|
||||
(entry) =>
|
||||
entry.requirementDigest === requirementDigest &&
|
||||
["changed", "accepted-unchanged"].includes(entry.decision) &&
|
||||
entry.rationale.trim() &&
|
||||
entry.agentId.trim(),
|
||||
);
|
||||
reviews.push({ ...requirement, requirementDigest, accepted });
|
||||
if (!accepted) blockers.push(`Semantic-major review required: ${consumer.groupId} consumes ${provider.id}`);
|
||||
}
|
||||
}
|
||||
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 };
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user