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" | "build"; 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", "moduleMember", "moduleExport", "relationshipMember", "operationMember", "packageOperationExport", "packageFunctionExport", "packageConstructorExport", "conformanceDecl", "stateDecl", "edgeDecl", "constructorBindingDecl", "resourceImportDecl", "sourceImportDecl", "operationBindingDecl", ]); /** 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`) }, ]); }; const select = ( source: string, selector: StructuralSelector, ): { node: SyntaxNode; syntax: ReturnType } => { 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", "build"].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 { // 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; 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; result = applySourceEdits(source, [ { start: wrapper?.start ?? header ?? node.start, end: wrapper?.end ?? node.end, text: edit.operation === "replace" ? edit.source : "", }, ]); } 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; };