import { severityLevel } from "./severity.js"; import type { AgentFormat, Finding } from "./schemas/index.js"; import type { SuppressionDirectiveFact } from "./facts/index.js"; export interface BuildAgentFormatInput { findings: readonly Finding[]; filesScanned: number; passed?: boolean; suppressions?: readonly SuppressionDirectiveFact[]; iteration?: number; maxIterations?: number; previousScore?: number; maxFindings?: number; includeEvidence?: boolean; includeFindings?: boolean; } export function buildAgentFormat(input: BuildAgentFormatInput): AgentFormat { const score = scoreFindings(input.findings); const visibleFindings = uniqueFindings(input.findings).slice(0, input.maxFindings ?? 50); const agentFindings = visibleFindings.map((finding) => ({ code: finding.code, ruleId: finding.ruleId, filePath: finding.location.file, line: finding.location.line, col: finding.location.column, severity: finding.severity, level: severityLevel(finding.severity), message: finding.message, evidence: input.includeEvidence ? finding.evidence : [], plan: planForFinding(finding), })); // Keep this aligned with the report summary: warn-level findings are debt, not // scan failure, unless the caller has already folded fail-on-warnings into // `passed:false`. const hasErrors = input.findings.some((finding) => severityLevel(finding.severity) === "error"); return { schemaVersion: "typestyle.agent.v1", passed: (input.passed ?? true) && !hasErrors, score, summary: agentSummary(input.findings.length, input.filesScanned, score), fix_first: agentFindings.filter((finding) => finding.plan.confidence >= 0.8), remaining: agentFindings.filter((finding) => finding.plan.confidence < 0.8), suppression_template: { directive: '// @fragments-expect-error {code} reason="..." expires="YYYY-MM-DD"', requiredFields: ["reason"], example: '// @fragments-expect-error FUI2001 reason="hero brand red approved" expires="2026-08-01"', }, budget: { iteration: input.iteration ?? 0, maxIterations: input.maxIterations ?? 3, scoreDelta: input.previousScore === undefined ? 0 : score - input.previousScore, }, ...(input.includeFindings ? { findings: [...input.findings] } : {}), ...(input.suppressions ? { suppressions: input.suppressions.map((suppression) => ({ code: suppression.code, reason: suppression.reason, ...(suppression.expiresOn ? { expiresOn: suppression.expiresOn } : {}), scope: "line" as const, })), } : {}), }; } function uniqueFindings(findings: readonly Finding[]): Finding[] { const seen = new Set(); const out: Finding[] = []; for (const finding of findings) { const key = finding.fingerprint ?? `${finding.ruleId}\0${finding.location.file}\0${finding.location.line}\0${finding.message}`; if (seen.has(key)) continue; seen.add(key); out.push(finding); } return out; } function scoreFindings(findings: readonly Finding[]): number { if (findings.length === 0) return 100; let penalty = 0; for (const finding of findings) { const level = severityLevel(finding.severity); penalty += level === "error" ? 10 : level === "warn" ? 3 : 1; } return Math.max(1, Math.round(100 / (1 + penalty / 40))); } function agentSummary(count: number, filesScanned: number, score: number): string { if (count === 0) return `passed with score ${score}; no findings`; return `${count} finding${count === 1 ? "" : "s"} across ${filesScanned} file${filesScanned === 1 ? "" : "s"}; score ${score}`; } function planForFinding(finding: Finding): AgentFormat["fix_first"][number]["plan"] { if (finding.fix?.deterministic === true) { return { tool: "design_system/conform", args: { filePath: finding.location.file, fingerprint: finding.fingerprint, apply: "deterministic", }, confidence: 0.95, }; } if ( finding.code?.startsWith("FUI1") || finding.ruleId.includes("component") || finding.ruleId.includes("canonical") ) { return { tool: "design_system/conform", args: { filePath: finding.location.file, apply: "deterministic" }, confidence: 0.65, }; } return { tool: "design_system/conform", args: { filePath: finding.location.file, property: stringAttribute(finding.attributes?.property) ?? stringAttribute(finding.attributes?.prop) ?? "color", value: stringAttribute(finding.attributes?.rawValue) ?? stringAttribute(finding.attributes?.color), apply: "deterministic", }, confidence: 0.55, }; } function stringAttribute(value: unknown): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined; }