2026-09-10 18:27:41 -07:00
import { CharStream , CommonTokenStream } from "antlr4ng" ;
import { QuixosLockLexer } from "../resource-lock/generated/QuixosLockLexer.js" ;
import { QuixosLockParser } from "../resource-lock/generated/QuixosLockParser.js" ;
import { parseQuixosLockDocument } from "../resource-lock/parser.js" ;
import { parseQx , walkSyntax , applySourceEdits , type SyntaxNode } from "./source.js" ;
export type StructuralSelector = { kind : string ; id? : string ; name? : string ; names? : string [] };
export type StructuralEdit =
| { operation : "append" ; parent : StructuralSelector ; source : string }
| { operation : "replace" ; target : StructuralSelector ; source : string }
| { operation : "remove" ; target : StructuralSelector }
| { operation : "import" ; kind : "interface" | "package" ; name : string }
| { operation : "semantic-major" ; target : StructuralSelector ; major : number }
| { operation : "conformance-id" ; target : StructuralSelector ; id : string }
| { operation : "quixos-pin" ; source : { repository : string ; commit : string } }
| { operation : "dependency" ; kind : "interface" | "package" ; name : string ; source : { repository : string ; commit : string } | null };
// Deliberately exclude valueType/identifier/stringLiteral: callers operate on
// declaration structure, not arbitrary token offsets or lockfile text patches.
const selectable = new Set ([ "workspaceDecl" , "fragmentDecl" , "interfaceResourceDecl" , "packageResourceDecl" , "atomDecl" ,
"valueMember" , "relationshipMember" , "operationMember" , "packageOperationExport" , "packageFunctionExport" , "packageConstructorExport" ,
"conformanceDecl" , "stateDecl" , "edgeDecl" , "constructorBindingDecl" , "resourceImportDecl" , "sourceImportDecl" , "operationBindingDecl" ]);
2026-09-14 10:26:11 -07:00
/** Bind only the template root identity; schema/atom identities are reusable.
* The revision's real identity is derived from its containing commit at compile time. */
export const instantiateWorkspaceIdentity = ( source : string , workspaceId : string ) : string => {
if ( ! /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/ . test ( workspaceId )) throw new Error ( "Workspace identity must be a UUID" );
const syntax = parseQx ( source );
if ( syntax . diagnostics . length ) throw new Error ( "Cannot instantiate a malformed template workspace" );
const declaration = syntax . root . children . find ( node => node . kind === "workspaceDecl" );
const literals = declaration ? . children . filter ( node => node . kind === "stringLiteral" );
if ( ! literals || literals . length !== 3 ) throw new Error ( "Template must contain a workspace declaration" );
return applySourceEdits ( source , [
{ ... literals [ 0 ], text : JSON.stringify ( workspaceId ) },
{ ... literals [ 1 ], text : JSON.stringify ( `workspace-revision: ${ workspaceId } :source` ) },
]);
};
2026-09-10 18:27:41 -07:00
const select = ( source : string , selector : StructuralSelector ) : { node : SyntaxNode ; syntax : ReturnType < typeof parseQx >} => {
if ( ! selectable . has ( selector . kind )) throw new Error ( `Unsupported structural selector ${ selector . kind } ` );
const syntax = parseQx ( source );
if ( syntax . diagnostics . length ) throw new Error ( "Cannot scaffold syntactically invalid QX" );
const matches = [... walkSyntax ( syntax . root )]. filter (( node ) => {
if ( node . kind !== selector . kind ) return false ;
if ( selector . name && ! node . children . some (( child ) => child . kind === "identifier" && source . slice ( child . start , child . end ) === selector . name )) return false ;
if ( selector . names && JSON . stringify ( node . children . filter (( child ) => child . kind === "identifier" ). map (( child ) => source . slice ( child . start , child . end ))) !== JSON . stringify ( selector . names )) return false ;
if ( selector . id ) {
// Only an explicit ID field counts, not a coincidentally equal revision,
// default value, nested declaration, or comment.
const tokens = syntax . tokens . filter (( token ) => ! token . trivia && token . start >= node . start && token . end <= node . end );
const literal = node . children . find (( child ) => child . kind === "stringLiteral" && tokens . some (( token , index ) => token . start === child . start && tokens [ index - 1 ] ? . kind === "ID" ));
if ( ! literal || JSON . parse ( source . slice ( literal . start , literal . end )) !== selector . id ) return false ;
}
return true ;
});
if ( matches . length !== 1 ) throw new Error ( `Structural selector must resolve exactly once (found ${ matches . length } )` );
return { node : matches [ 0 ], syntax };
};
/** Comment-preserving structural edits; every result is parsed before returning. */
export const editStructure = ( source : string , edit : StructuralEdit ) : string => {
if ( edit . operation === "quixos-pin" ) {
const parsed = parseQuixosLockDocument ( source );
if ( ! parsed . ok || parsed . document . kind !== "root" ) throw new Error ( "Quixos pins belong in a valid root lockfile" );
const parser = new QuixosLockParser ( new CommonTokenStream ( new QuixosLockLexer ( CharStream . fromString ( source ))));
const entries = parser . document (). quixosEntry ();
if ( entries . length !== 1 ) throw new Error ( "Expected one Quixos source declaration" );
const literals = entries [ 0 ]. quixosSourceBlock (). stringLiteral ();
const offsets = [ 0 ];
for ( const character of source ) offsets . push ( offsets [ offsets . length - 1 ] + character . length );
const result = applySourceEdits ( source , [
{ start : offsets [ literals [ 0 ]. start ! . start ], end : offsets [ literals [ 0 ]. stop ! . stop + 1 ], text : JSON.stringify ( edit . source . repository )},
{ start : offsets [ literals [ literals . length - 1 ]. start ! . start ], end : offsets [ literals [ literals . length - 1 ]. stop ! . stop + 1 ], text : JSON.stringify ( edit . source . commit )},
]);
const checked = parseQuixosLockDocument ( result );
if ( ! checked . ok ) throw new Error ( `Invalid Quixos pin: ${ checked . diagnostics . map (( entry ) => entry . message ). join ( "; " ) } ` );
return result ;
}
if ( edit . operation === "import" ) {
if ( ! [ "interface" , "package" ]. includes ( edit . kind ) || ! /^[A-Za-z_][A-Za-z0-9_]*$/ . test ( edit . name )) throw new Error ( "Invalid resource import" );
const syntax = parseQx ( source );
if ( syntax . diagnostics . length ) throw new Error ( "Cannot scaffold invalid QX" );
const text = `import ${ edit . kind } ${ edit . name } ;` ;
if ([... walkSyntax ( syntax . root )]. some (( entry ) => entry . kind === "resourceImportDecl" && source . slice ( entry . start , entry . end ). replace ( /\s+/g , " " ) === text )) return source ;
const root = syntax . root . children [ 0 ];
const position = [ "workspaceDecl" , "fragmentDecl" ]. includes ( root . kind ) ? syntax . tokens . find (( token ) => token . kind === "LBRACE" ) ! . end : root.start ;
const result = applySourceEdits ( source , [{ start : position , end : position , text : ` \ n ${ text } \ n` }]);
if ( parseQx ( result ). diagnostics . length ) throw new Error ( "Invalid resource import position" );
return result ;
}
if ( edit . operation === "dependency" ) {
const parsed = parseQuixosLockDocument ( source );
if ( ! parsed . ok ) throw new Error ( "Cannot scaffold an invalid lockfile" );
if ( ! [ "interface" , "package" ]. includes ( edit . kind ) || ! /^[A-Za-z_][A-Za-z0-9_]*$/ . test ( edit . name )) throw new Error ( "Invalid dependency selector" );
const parser = new QuixosLockParser ( new CommonTokenStream ( new QuixosLockLexer ( CharStream . fromString ( source ))));
const tree = parser . document ();
const offsets = [ 0 ];
for ( const character of source ) offsets . push ( offsets [ offsets . length - 1 ] + character . length );
const entries = tree . resourceEntry (). filter (( entry ) => entry . resourceKind (). getText () === edit . kind && entry . identifier (). getText () === edit . name );
if ( entries . length > 1 ) throw new Error ( "Ambiguous dependency selector" );
const entry = entries [ 0 ];
const replacement = edit . source ? ` ${ edit . kind } ${ edit . name } source { \ n repository ${ JSON . stringify ( edit . source . repository ) } ; \ n commit ${ JSON . stringify ( edit . source . commit ) } ; \ n}` : "" ;
if ( ! entry && ! edit . source ) throw new Error ( "Cannot remove an absent dependency" );
const start = entry ? offsets [ entry . start ! . start ] : offsets [ tree . RBRACE (). symbol . start ];
const end = entry ? offsets [ entry . stop ! . stop + 1 ] : start ;
const result = applySourceEdits ( source , [{ start , end , text : entry ? replacement : ` ${ replacement } \ n` }]);
const checked = parseQuixosLockDocument ( result );
if ( ! checked . ok ) throw new Error ( `Invalid dependency change: ${ checked . diagnostics . map (( entry ) => entry . message ). join ( "; " ) } ` );
return result ;
}
const { node , syntax } = select ( source , edit . operation === "append" ? edit.parent : edit.target );
let result : string ;
if ( edit . operation === "semantic-major" ) {
if ( ! [ "conformanceDecl" , "packageResourceDecl" ]. includes ( node . kind ) || ! Number . isSafeInteger ( edit . major ) || edit . major < 1 ) throw new Error ( "Semantic major requires a package/conformance and positive integer" );
const tokens = syntax . tokens . filter (( token ) => ! token . trivia && token . start >= node . start && token . end <= node . end );
const marker = tokens . findIndex (( token ) => token . kind === "SEMANTIC_MAJOR" );
const value = marker < 0 ? undefined : tokens [ marker + 1 ];
const brace = tokens . find (( token ) => token . kind === "LBRACE" ) ! ;
result = applySourceEdits ( source , [{ start : value?.start ?? brace . start , end : value?.end ?? brace . start , text : value ? String ( edit . major ) : `semantic-major ${ edit . major } ` }]);
} else if ( edit . operation === "conformance-id" ) {
if ( node . kind !== "conformanceDecl" || ! edit . id ) throw new Error ( "Identity enrollment requires a conformance and stable ID" );
const existing = node . children . find (( entry ) => entry . kind === "stringLiteral" );
if ( existing ) {
if ( JSON . parse ( source . slice ( existing . start , existing . end )) !== edit . id ) throw new Error ( "Cannot change an enrolled conformance identity; create a new conformance explicitly" );
return source ;
}
const identifiers = node . children . filter (( entry ) => entry . kind === "identifier" );
const position = identifiers [ identifiers . length - 1 ]. end ;
result = applySourceEdits ( source , [{ start : position , end : position , text : ` id ${ JSON . stringify ( edit . id ) } ` }]);
} else if ( edit . operation === "append" ) {
const closing = syntax . tokens . find (( token ) => token . kind === "RBRACE" && token . end === node . end );
if ( ! closing ) throw new Error ( "Append requires a declaration with a body" );
result = applySourceEdits ( source , [{ start : closing.start , end : closing.start , text : ` \ n ${ edit . source } \ n` }]);
} else {
2026-09-14 10:26:11 -07:00
// A resource parser node includes imports/external declarations preceding
// its header. Replacing the declaration must not delete that preamble.
const identifier = node . children . find ( child => child . kind === "identifier" );
const header = [ "packageResourceDecl" , "interfaceResourceDecl" ]. includes ( node . kind ) && identifier
? syntax . tokens . filter ( token => token . start >= node . start && token . end <= identifier . start &&
token . kind === ( node . kind === "packageResourceDecl" ? "PACKAGE" : "INTERFACE" )). at ( - 1 ) ? . start : undefined ;
2026-09-10 18:27:41 -07:00
const wrapper = edit . operation === "remove" && [ "stateDecl" , "edgeDecl" ]. includes ( node . kind )
? [... walkSyntax ( syntax . root )]. filter (( entry ) => [ "conformanceItem" , "sharedAttachmentDecl" ]. includes ( entry . kind ) && entry . start <= node . start && entry . end >= node . end ). sort (( a , b ) => ( a . end - a . start ) - ( b . end - b . start ))[ 0 ]
: undefined ;
2026-09-14 10:26:11 -07:00
result = applySourceEdits ( source , [{ start : wrapper?.start ?? header ?? node . start , end : wrapper?.end ?? node . end , text : edit.operation === "replace" ? edit . source : "" }]);
2026-09-10 18:27:41 -07:00
}
const checked = parseQx ( result );
if ( checked . diagnostics . length ) throw new Error ( `Invalid structural change: ${ checked . diagnostics . map (( entry ) => entry . message ). join ( "; " ) } ` );
return result ;
};
export const scaffoldResourceSource = ( kind : "interface" | "package" , name : string , id : string , revision : string ) => {
if ( ! /^[A-Z][A-Za-z0-9]*$/ . test ( name ) || ! id || ! revision ) throw new Error ( "Resource scaffold requires a PascalCase name and explicit identities" );
const source = ` ${ kind } ${ name } id ${ JSON . stringify ( id ) } revision ${ JSON . stringify ( revision ) } { \ n} \ n` ;
if ( parseQx ( source ). diagnostics . length ) throw new Error ( "Invalid resource scaffold" );
return source ;
};