import { join, relative, resolve } from "node:path"; import { makeScratchDir } from "#scripts/scratch.ts"; import { collectTlaSources } from "#scripts/tla/files.ts"; import { ensureJscpd } from "./jscpd/install.ts"; import { type PreparedSource, prepareSource, translateReport, } from "./tla-cpd/prepare.ts"; export const checkTla = async (inputs: readonly string[]): Promise => { const files = await collectTlaSources(inputs); const work = await makeScratchDir("tla-cpd-"); try { const preparedDirectory = resolve(work, "tokens"); const outputDirectory = resolve(work, "report"); await Deno.mkdir(preparedDirectory); const sources = new Map(); let tokenCount = 0; for (const [index, file] of files.entries()) { const prepared = prepareSource( relative(Deno.cwd(), file), await Deno.readTextFile(file), ); tokenCount += prepared.tokens.length; const path = join(preparedDirectory, `${index}.txt`); await Deno.writeTextFile(path, prepared.encoded); sources.set(path, prepared); } if (tokenCount === 0) throw new Error("No TLA tokens found in prepared inputs"); const config = resolve(work, "jscpd.json"); await Deno.writeTextFile( config, JSON.stringify({ absolute: true, exitCode: 1, format: ["txt"], gitignore: false, minLines: 1, minTokens: 19, noTips: true, output: outputDirectory, reporters: ["json"], threshold: 0, }), ); const result = await new Deno.Command(await ensureJscpd(), { args: ["--config", config, preparedDirectory], stderr: "piped", stdout: "piped", }).output(); const reportPath = join(outputDirectory, "jscpd-report.json"); const report = translateReport( JSON.parse(await Deno.readTextFile(reportPath)), sources, ); const publishedPath = join( await makeScratchDir("tla-cpd-report-"), "jscpd-report.json", ); await Deno.writeTextFile(publishedPath, JSON.stringify(report, null, 2)); console.log(`TLA CPD report: ${publishedPath}`); const duplicates = report.duplicates; if (!Array.isArray(duplicates)) throw new Error("Missing translated clones"); for (const clone of duplicates) { const { firstFile, secondFile } = clone; console.error( `TLA clone: ${firstFile.name}:${firstFile.startLoc.line}:${firstFile.startLoc.column}-${firstFile.endLoc.line}:${firstFile.endLoc.column}`, ); console.error( ` and ${secondFile.name}:${secondFile.startLoc.line}:${secondFile.startLoc.column}-${secondFile.endLoc.line}:${secondFile.endLoc.column}`, ); } if (duplicates.length > 0) { console.error( "TLA duplication exceeds the non-negotiable 0% threshold. Extract a shared rule; do not rename, reorder, exclude sources or add ignore tags to hide duplication.", ); return 1; } if (!result.success) { throw new Error( `jscpd failed (${result.code}): ${new TextDecoder().decode(result.stderr)}${new TextDecoder().decode(result.stdout)}`, ); } console.log( `TLA CPD: ${files.length} sources, ${tokenCount} tokens, no clones`, ); return 0; } finally { await Deno.remove(work, { recursive: true }); } }; if (import.meta.main) { try { Deno.exit( await checkTla(Deno.args.length === 0 ? ["verification"] : Deno.args), ); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); Deno.exit(1); } }