Files
quixos-protocol/src/capability-model/evolution.ts
T
Timothy J. Aveni 1e72713ae2 Implement durable typed journals, subscriptions, and Reader document connections
Add checked native journal/subscription bindings, managed-reference retention,
bounded history and backpressure, atomic delivery receipts, and fenced hosting.
Preserve source publication prediction and replica reconciliation; subscription
attachment, controls, and delivery remain authoritative in this initial version.

Connect independent manual/Gutenberg document producers to Reader bookshelves
through typed adapters. Add connection inspection, shared provider selection,
authoring documentation, and restart/rejection/fan-out integration coverage.

Verified compiler suite (287 passed, two existing skips), Nix runtime and
PostgreSQL qualification, five example assemblies, headless Reader integration,
Chromium Reader integration, resource pins, formatting, and lint.
No pinned workspace runtime was deployed.
2026-09-28 19:35:51 -07:00

485 lines
22 KiB
TypeScript

import { createHash } from "node:crypto";
import type { Binding, Conformance, DependencyBinding, PersistentAttachment, WorkspaceRevision } from "./types.js";
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);
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(",")}}`;
}
throw new Error(`Cannot hash non-JSON value: ${typeof value}`);
};
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)]),
);
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}`;
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 => {
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 (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;
return canonicalJson(beforeStorage) !== canonicalJson(afterStorage);
}
return canonicalJson(before) !== canonicalJson(after);
};
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 }),
});
};
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));
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 }>;
};
/** Build only outbound execution dependencies. Incoming callers never retain or invalidate a provider. */
export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[] => {
const nodes = new Map<string, GraphNode>();
const node = (key: string, value: unknown) => {
const result = { value: semantic(value), dependencies: new Set<string>(), reviewProviders: new Set<string>() };
nodes.set(key, result);
return result;
};
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),
})),
});
const dependency = (parent: GraphNode, binding: DependencyBinding, atomId: string, reviews?: Set<string>) => {
if (binding.kind === "query") {
const pkg = workspace.packageImports.find((pkg) => pkg.revisionId === binding.packageRevisionId);
const query = pkg?.checkedQueries?.find((query) => query.declaration.id === binding.queryId);
if (!query) throw new Error(`Missing checked query contract ${binding.packageRevisionId}:${binding.queryId}`);
const key = `query:${binding.packageRevisionId}:${binding.queryId}`;
const queryNode = nodes.get(key) ?? node(key, query);
parent.dependencies.add(key);
for (const effect of query.effects) {
queryNode.dependencies.add(`interface:${effect.interfaceRevisionId}`);
for (const conformance of workspace.conformances.filter(
(entry) => entry.interfaceRevisionId === effect.interfaceRevisionId,
))
queryNode.dependencies.add(conformanceKey(conformance.atomId, conformance.interfaceRevisionId));
}
}
if (binding.kind === "state") parent.dependencies.add(`attachment:${binding.slotId}`);
if (binding.kind === "edge") parent.dependencies.add(`attachment:${binding.edgeTypeId}`);
if (binding.kind === "creation") {
for (const owner of binding.owners)
parent.dependencies.add(conformanceKey(binding.atomId, owner.interfaceRevisionId));
}
if (binding.kind === "constructor") {
parent.dependencies.add(`constructor:${binding.atomId}`);
const ctor = workspace.constructors.find((entry) => entry.atomId === binding.atomId);
const pkg = workspace.packageImports.find((entry) => entry.revisionId === ctor?.packageRevisionId);
if (pkg) reviews?.add(pkg.packageId);
}
if (binding.kind !== "constructor" && binding.via) parent.dependencies.add(`attachment:${binding.via.edgeTypeId}`);
if (binding.kind === "interface") {
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)
) {
parent.dependencies.add(conformanceKey(conformance.atomId, conformance.interfaceRevisionId));
reviews?.add(conformanceIdentity(conformance));
for (const operation of conformance.operationBindings) {
let provider: Binding | undefined = operation.binding;
const visited = new Set<string>();
while (provider?.kind === "interface") {
const forward: Extract<Binding, { kind: "interface" }> = provider;
const key = `${provider.interfaceRevisionId}\0${provider.operationId}`;
if (visited.has(key)) throw new Error("Forwarding cycle in evolution contract");
visited.add(key);
const target = workspace.conformances.find(
(c) => c.atomId === conformance.atomId && c.interfaceRevisionId === forward.interfaceRevisionId,
);
if (!target) throw new Error("Missing forwarding conformance in evolution contract");
reviews?.add(conformanceIdentity(target));
provider = target.operationBindings.find((entry) => entry.operationId === forward.operationId)?.binding;
}
if (provider?.kind !== "package") continue;
const revisionId = provider.packageRevisionId;
const pkg = workspace.packageImports.find((entry) => entry.revisionId === revisionId);
if (pkg) reviews?.add(pkg.packageId);
}
}
}
}
};
const binding = (parent: GraphNode, value: Binding, atomId: string, context: unknown) => {
if (value.kind === "lifecycle") return;
if (value.kind === "journal" || value.kind === "subscription") {
dependency(parent, { kind: "state", slotId: value.slotId }, 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) };
const key = `binding:${contentDigest({ atomId, context, value: semantic(normalized) })}`;
const bound = node(key, { atomId, context, binding: normalized });
packageNode.dependencies.add(key);
for (const port of value.dependencies) dependency(bound, port.binding, atomId, packageNode.reviewProviders);
};
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,
operations: sorted(conformance.operationBindings, (entry) => entry.operationId),
...(conformance.moduleBindings?.length
? { modules: sorted(conformance.moduleBindings, (entry) => entry.memberId) }
: {}),
...(conformance.initialization
? { initialization: sorted(conformance.initialization, (entry) => entry.input) }
: {}),
materializations: sorted(conformance.relationshipMaterializations, (entry) => entry.memberId),
});
parent.dependencies.add(`interface:${conformance.interfaceRevisionId}`);
for (const module of conformance.moduleBindings ?? [])
parent.dependencies.add(`package:${module.packageRevisionId}`);
const contract = workspace.interfaceImports.find((entry) => entry.revisionId === conformance.interfaceRevisionId);
for (const required of contract?.requiredInterfaces ?? [])
parent.dependencies.add(conformanceKey(conformance.atomId, required));
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 materialization of conformance.relationshipMaterializations) {
parent.dependencies.add(`constructor:${materialization.constructorAtomId}`);
parent.dependencies.add(`attachment:${materialization.edgeTypeId}`);
}
}
for (const constructor of workspace.constructors) {
const parent = node(`constructor:${constructor.atomId}`, constructor);
binding(parent, { kind: "package", ...constructor }, constructor.atomId, { constructor: constructor.atomId });
}
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,
);
};
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;
runtimeActions: RuntimeAction[];
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 => {
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");
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`);
}
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 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 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) => [
...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[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 consumers) {
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 = receipts.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 { 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 {
checkerVersion,
baselineDigest: baseline ? contentDigest(baseline) : null,
candidateDigest: contentDigest(candidate),
...report,
};
}