import { readFile, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { readAcpxSidecarProtocolVersion } from "./acpx-sidecar-contract.mjs"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const schema = JSON.parse( await readFile( resolve(root, "protocol/provider-schemas/acpx-sidecar.schema.json"), "utf8", ), ); const commands = schema.$defs.command.enum; const events = schema.$defs.eventType.enum; // Mutation wins for compound provider kinds so create-target attestation and // the normalized readOnly flag cannot disagree across the sidecar boundary. const toolOperationPrecedence = [ ["edit", ["edit", "write", "patch"]], ["read", ["read"]], ["search", ["search", "grep", "find"]], ["list", ["list", "glob"]], ]; const protocolVersion = readAcpxSidecarProtocolVersion(schema); const quote = (value) => JSON.stringify(value); const rustVariant = (value) => value .split(/[._-]/) .map((part) => part[0].toUpperCase() + part.slice(1)) .join(""); const typescript = `// Generated by scripts/generate-acpx-sidecar-contract.mjs. Do not edit.\n\nexport const GENERATED_ACPX_SIDECAR_PROTOCOL_VERSION = ${protocolVersion} as const;\nexport const GENERATED_ACPX_SIDECAR_COMMANDS = [\n${commands.map((value) => ` ${quote(value)},`).join("\n")}\n] as const;\nexport type GeneratedAcpxSidecarCommand =\n (typeof GENERATED_ACPX_SIDECAR_COMMANDS)[number];\n\nexport const GENERATED_ACPX_SIDECAR_EVENT_TYPES = [\n${events.map((value) => ` ${quote(value)},`).join("\n")}\n] as const;\nexport type GeneratedAcpxSidecarEventType =\n (typeof GENERATED_ACPX_SIDECAR_EVENT_TYPES)[number];\n\nexport type GeneratedAcpxToolOperation =\n | "read"\n | "search"\n | "list"\n | "edit"\n | "execute"\n | "unknown";\n\nexport const GENERATED_ACPX_TOOL_OPERATION_PRECEDENCE = [\n${toolOperationPrecedence.map(([operation, tokens]) => ` { operation: ${quote(operation)}, tokens: [${tokens.map(quote).join(", ")}] },`).join("\n")}\n] as const;\n\nexport function classifyGeneratedAcpxToolOperation(\n toolKind: unknown,\n toolTitle: unknown,\n): GeneratedAcpxToolOperation {\n const candidate =\n typeof toolKind === "string" && toolKind\n ? toolKind\n : typeof toolTitle === "string"\n ? toolTitle\n : "";\n const normalized = candidate.slice(0, 240).toLowerCase();\n for (const { operation, tokens } of GENERATED_ACPX_TOOL_OPERATION_PRECEDENCE) {\n if (tokens.some((token) => normalized.includes(token))) return operation;\n }\n return normalized ? "execute" : "unknown";\n}\n`; const rust = `// Generated by scripts/generate-acpx-sidecar-contract.mjs. Do not edit.\n\npub const GENERATED_ACPX_SIDECAR_PROTOCOL_VERSION: u64 = ${protocolVersion};\n\npub const GENERATED_ACPX_TOOL_OPERATION_PRECEDENCE: &[(&str, &[&str])] = &[\n${toolOperationPrecedence.map(([operation, tokens]) => ` (${quote(operation)}, &[${tokens.map(quote).join(", ")}]),`).join("\n")}\n];\n\npub fn classify_generated_acpx_tool_operation(kind: &str, title: &str) -> &'static str {\n let kind = kind.to_ascii_lowercase();\n let title = title.to_ascii_lowercase();\n let candidate = if kind.is_empty() { &title } else { &kind };\n for (operation, tokens) in GENERATED_ACPX_TOOL_OPERATION_PRECEDENCE {\n if tokens.iter().any(|token| candidate.contains(token)) {\n return operation;\n }\n }\n if candidate.is_empty() {\n "unknown"\n } else {\n "execute"\n }\n}\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum GeneratedAcpxSidecarCommand {\n${commands.map((value) => ` ${rustVariant(value)},`).join("\n")}\n}\n\nimpl GeneratedAcpxSidecarCommand {\n pub const fn as_str(self) -> &'static str {\n match self {\n${commands.map((value) => ` Self::${rustVariant(value)} => ${quote(value)},`).join("\n")}\n }\n }\n}\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Deserialize)]\npub enum GeneratedAcpxSidecarEventType {\n${events.map((value) => ` #[serde(rename = ${quote(value)})]\n ${rustVariant(value)},`).join("\n")}\n}\n`; // Classification must inspect the same complete provider value in both // languages. Display-field truncation happens only after operation selection. const typescriptWithAsciiLowercase = typescript.replace( "export function classifyGeneratedAcpxToolOperation(", `function lowercaseGeneratedAcpxAscii(value: string): string { return value.replace(/[A-Z]/g, (character) => String.fromCharCode(character.charCodeAt(0) + 32), ); } export function classifyGeneratedAcpxToolOperation(`, ); if (typescriptWithAsciiLowercase === typescript) { throw new Error("generated TypeScript ACPX classifier export drifted"); } const typescriptClassifier = typescriptWithAsciiLowercase.replace( "candidate.slice(0, 240).toLowerCase()", "lowercaseGeneratedAcpxAscii(candidate)", ); if (typescriptClassifier === typescriptWithAsciiLowercase) { throw new Error("generated TypeScript ACPX classifier template drifted"); } const outputs = [ [ resolve(root, "src/drivers/acpx/generated-sidecar-contract.ts"), typescriptClassifier, ], [ resolve( root, "runner/crates/runner-core/src/generated_acpx_sidecar_contract.rs", ), rust, ], ]; if (process.argv.includes("--check")) { const stale = []; for (const [path, content] of outputs) if ((await readFile(path, "utf8").catch(() => "")) !== content) stale.push(path); if (stale.length > 0) { process.stderr.write( `Generated ACPX sidecar contracts are stale: ${stale.join(", ")}\n`, ); process.exitCode = 1; } else process.stdout.write( "Generated TypeScript/Rust ACPX sidecar contracts match the schema.\n", ); } else { await Promise.all(outputs.map(([path, content]) => writeFile(path, content))); process.stdout.write("Generated TypeScript/Rust ACPX sidecar contracts.\n"); }