2026-09-10 18:27:41 -07:00
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" )
2026-09-14 15:24:37 -07:00
// 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 )]));
2026-09-10 18:27:41 -07:00
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 };
2026-09-14 15:24:37 -07:00
/** 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 );
};
2026-09-10 18:27:41 -07:00
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 === "state" ) parent . dependencies . add ( `attachment: ${ binding . slotId } ` );
if ( binding . kind === "edge" ) parent . dependencies . add ( `attachment: ${ binding . edgeTypeId } ` );
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 ) {
if ( operation . binding . kind !== "package" ) continue ;
const revisionId = operation . binding . 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 !== "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 ),
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 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 [];
2026-09-14 15:24:37 -07:00
storageChanges : Array < { id : string ; kind : "add" | "remove" | "change" ; requiresMigration : boolean ; previous? : StorageContract ; candidate? : StorageContract } > ;
migrationRequired : string [];
2026-09-10 18:27:41 -07:00
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" ,
2026-09-14 15:24:37 -07:00
requiresMigration : storageChangeRequiresMigration ( previous , next , new Set ( baseline ? . atoms . map ( atom => atom . id ) ?? [])),
2026-09-10 18:27:41 -07:00
...( 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 ) })),
];
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 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 ());
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 ,
2026-09-14 15:24:37 -07:00
runtimeActions , storageChanges , migrationRequired : storageChanges.filter ( entry => entry . requiresMigration ). map ( entry => entry . id ), reviews , packageChecks : runtimeActions.filter (( entry ) => entry . candidate && entry . action !== "keep" )
2026-09-10 18:27:41 -07:00
. map (( entry ) => ({ groupId : entry.groupId , contractDigest : entry.candidate ! . digest })), blockers };
};