89 lines
5.9 KiB
JavaScript
89 lines
5.9 KiB
JavaScript
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");
|
|
}
|