/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ export type ScriptDiagnosticSeverity = 'error' | 'warning'; export type ScriptDiagnosticSource = 'preflight' | 'runtime' | 'patch'; export type RepairScope = 'local' | 'block' | 'structural' | 'full_rewrite'; export type PreflightDiagnosticCode = | 'unknown_namespace' | 'unknown_method' | 'create_contract' | 'bare_identifier' | 'wall_hosted_opening_pattern' | 'metadata_query_pattern' | 'world_placement_elevation' | 'detached_snippet_scope'; export type RuntimeDiagnosticCode = | 'generic_placement_contract' | 'plate_contract_mismatch' | 'world_placement_elevation' | 'detached_snippet_scope' | 'wall_hosted_opening_alignment'; export type PatchDiagnosticCode = | 'patch_revision_conflict' | 'patch_range_error' | 'patch_semantic_error' | 'unsafe_full_replacement' | 'destructive_partial_rewrite'; export interface ScriptDiagnosticRange { from: number; to: number; } export interface ScriptDiagnosticEvidence { source?: ScriptDiagnosticSource; code?: string; message?: string; methodName?: string; symbol?: string; failureKind?: string; range?: ScriptDiagnosticRange; line?: number; column?: number; snippet?: string; } export interface ScriptDiagnosticBase { source: TSource; code: TCode; severity: ScriptDiagnosticSeverity; message: string; rootCauseKey: string; repairScope: RepairScope; evidence?: ScriptDiagnosticEvidence[]; data?: Record; } export type PreflightScriptDiagnostic = ScriptDiagnosticBase<'preflight', PreflightDiagnosticCode>; export type RuntimeScriptDiagnostic = ScriptDiagnosticBase<'runtime', RuntimeDiagnosticCode>; export type PatchScriptDiagnostic = ScriptDiagnosticBase<'patch', PatchDiagnosticCode>; export type ScriptDiagnostic = | PreflightScriptDiagnostic | RuntimeScriptDiagnostic | PatchScriptDiagnostic; export interface RootCauseDiagnosticGroup { rootCauseKey: string; repairScope: RepairScope; summary: string; diagnostics: ScriptDiagnostic[]; evidence: ScriptDiagnosticEvidence[]; } export function createPreflightDiagnostic( code: PreflightDiagnosticCode, message: string, severity: ScriptDiagnosticSeverity = 'error', data?: Record, ): PreflightScriptDiagnostic { return createDiagnosticBase('preflight', code, message, severity, data); } export function createRuntimeDiagnostic( code: RuntimeDiagnosticCode, message: string, severity: ScriptDiagnosticSeverity = 'error', data?: Record, ): RuntimeScriptDiagnostic { return createDiagnosticBase('runtime', code, message, severity, data); } export function createPatchDiagnostic( code: PatchDiagnosticCode, message: string, severity: ScriptDiagnosticSeverity = 'error', data?: Record, ): PatchScriptDiagnostic { return createDiagnosticBase('patch', code, message, severity, data); } export function formatDiagnosticsForDisplay(diagnostics: ScriptDiagnostic[]): string[] { return diagnostics.map((diagnostic) => diagnostic.message); } export function formatDiagnosticsForPrompt(diagnostics: ScriptDiagnostic[]): string { if (diagnostics.length === 0) return ''; const groups = groupDiagnosticsByRootCause(diagnostics); const rootCauseLines = groups.flatMap((group) => { const lines = [`- [root-cause:${group.rootCauseKey}] ${group.summary} (scope=${group.repairScope})`]; for (const evidence of group.evidence.slice(0, 4)) { lines.push(` supporting evidence: ${formatEvidence(evidence)}`); } return lines; }); const rawLines = diagnostics.map((diagnostic) => { const details = formatDiagnosticDetails(diagnostic.data); const hint = typeof diagnostic.data?.fixHint === 'string' ? ` Hint: ${diagnostic.data.fixHint}` : ''; return `- [${diagnostic.source}:${diagnostic.code}] ${diagnostic.message} (rootCause=${diagnostic.rootCauseKey}, scope=${diagnostic.repairScope})${details}${hint}`; }); return [ 'Root causes:', ...rootCauseLines, 'Raw diagnostics:', ...rawLines, ].join('\n'); } export function groupDiagnosticsByRootCause(diagnostics: ScriptDiagnostic[]): RootCauseDiagnosticGroup[] { const groups = new Map(); for (const diagnostic of diagnostics) { const key = diagnostic.rootCauseKey; const existing = groups.get(key); const evidence = dedupeEvidence(collectDiagnosticEvidence(diagnostic)); if (!existing) { groups.set(key, { rootCauseKey: key, repairScope: diagnostic.repairScope, summary: summarizeRootCause(key, diagnostic), diagnostics: [diagnostic], evidence, }); continue; } existing.diagnostics.push(diagnostic); existing.repairScope = widenRepairScope(existing.repairScope, diagnostic.repairScope); existing.evidence = dedupeEvidence([...existing.evidence, ...evidence]); } return [...groups.values()].sort((left, right) => compareRepairScope(right.repairScope, left.repairScope)); } export function getPrimaryRootCause(diagnostics: ScriptDiagnostic[]): RootCauseDiagnosticGroup | null { return groupDiagnosticsByRootCause(diagnostics)[0] ?? null; } function formatDiagnosticDetails(data?: Record): string { if (!data) return ''; const details: string[] = []; const opId = asString(data.opId); const methodName = asString(data.methodName); const symbol = asString(data.symbol); const failureKind = asString(data.failureKind); const snippet = asString(data.snippet); const range = formatRange(data.range); const selection = formatRange(data.selection); const currentEditorRevision = asNumber(data.currentEditorRevision); const expectedBaseRevision = asNumber(data.expectedBaseRevision); const line = asNumber(data.line); const column = asNumber(data.column); if (opId) details.push(`op=${opId}`); if (methodName) details.push(`method=${methodName}`); if (symbol) details.push(`symbol=${symbol}`); if (failureKind) details.push(`failure=${failureKind}`); if (line !== null) details.push(`line=${line}`); if (column !== null) details.push(`column=${column}`); if (range) details.push(`range=${range}`); if (selection) details.push(`selection=${selection}`); if (expectedBaseRevision !== null) details.push(`expectedRevision=${expectedBaseRevision}`); if (currentEditorRevision !== null) details.push(`currentRevision=${currentEditorRevision}`); if (snippet) details.push(`snippet=${JSON.stringify(truncateSnippet(snippet))}`); return details.length > 0 ? ` (${details.join(', ')})` : ''; } function formatRange(value: unknown): string | null { if (!value || typeof value !== 'object') return null; const from = asNumber((value as Record).from); const to = asNumber((value as Record).to); if (from === null || to === null) return null; return `${from}..${to}`; } function formatEvidence(evidence: ScriptDiagnosticEvidence): string { const parts: string[] = []; if (evidence.code) parts.push(`${evidence.source ?? 'diagnostic'}:${evidence.code}`); if (evidence.methodName) parts.push(`method=${evidence.methodName}`); if (evidence.symbol) parts.push(`symbol=${evidence.symbol}`); if (evidence.failureKind) parts.push(`failure=${evidence.failureKind}`); const range = formatRange(evidence.range); if (range) parts.push(`range=${range}`); if (typeof evidence.line === 'number') parts.push(`line=${evidence.line}`); if (typeof evidence.column === 'number') parts.push(`column=${evidence.column}`); if (evidence.snippet) parts.push(`snippet=${JSON.stringify(truncateSnippet(evidence.snippet))}`); if (evidence.message) parts.push(evidence.message); return parts.join(', '); } function createDiagnosticBase( source: TSource, code: TCode, message: string, severity: ScriptDiagnosticSeverity, data?: Record, ): ScriptDiagnosticBase { const rootCauseKey = asString(data?.rootCauseKey) ?? defaultRootCauseKey(source, code); const repairScope = asRepairScope(data?.repairScope) ?? defaultRepairScope(rootCauseKey, code); const evidence = dedupeEvidence(buildEvidenceFromData(source, code, message, data)); return { source, code, severity, message, rootCauseKey, repairScope, evidence: evidence.length > 0 ? evidence : undefined, data, }; } function buildEvidenceFromData( source: ScriptDiagnosticSource, code: string, message: string, data?: Record, ): ScriptDiagnosticEvidence[] { const explicit = Array.isArray(data?.evidence) ? (data.evidence as unknown[]).map(asEvidence).filter((value): value is ScriptDiagnosticEvidence => Boolean(value)) : []; if (explicit.length > 0) return explicit; const methodName = asString(data?.methodName); const symbol = asString(data?.symbol); const failureKind = asString(data?.failureKind); const snippet = asString(data?.snippet); const range = asRange(data?.range); const line = asNumber(data?.line); const column = asNumber(data?.column); if (!methodName && !symbol && !failureKind && !snippet && !range && line === null && column === null) { return []; } return [{ source, code, message, methodName: methodName ?? undefined, symbol: symbol ?? undefined, failureKind: failureKind ?? undefined, snippet: snippet ?? undefined, range: range ?? undefined, line: line ?? undefined, column: column ?? undefined, }]; } function collectDiagnosticEvidence(diagnostic: ScriptDiagnostic): ScriptDiagnosticEvidence[] { return diagnostic.evidence ?? []; } function summarizeRootCause(rootCauseKey: string, diagnostic: ScriptDiagnostic): string { switch (rootCauseKey) { case 'api_contract_mismatch': return 'The failing code does not match the exact BIM API contract and should be repaired by correcting the payload shape or required keys.'; case 'placement_context_mismatch': return 'The script is mixing placement or host-context assumptions, so related geometry calls should be repaired together instead of as isolated lines.'; case 'detached_fragment_rewrite': return 'The proposed or current script fragment depends on missing surrounding declarations, so the repair should preserve and reconnect the broader script context.'; case 'creator_lifecycle_violation': return 'The script uses a creator or project lifecycle in an invalid order, so the repair must fix the broader creation/finalization flow.'; case 'structural_script_corruption': return 'The repair payload would corrupt, truncate, or replace too much of the script, so context-preserving structural repair is required.'; case 'stale_patch_target': return 'The repair patch no longer matches the current script snapshot, so it must be regenerated against the latest revision and exact text.'; case 'malformed_repair_reply': return 'The repair reply format is invalid or ambiguous, so the next repair turn should reissue a clean patch-only response.'; case 'unknown_api_reference': return 'The script references an unknown BIM namespace or method and should be corrected to match the supported API surface.'; case 'metadata_access_pattern': return 'The script is using a brittle metadata access pattern and should switch to dedicated IFC query helpers.'; default: return diagnostic.message; } } function defaultRootCauseKey(source: ScriptDiagnosticSource, code: string): string { switch (code) { case 'unknown_namespace': case 'unknown_method': return 'unknown_api_reference'; case 'create_contract': case 'bare_identifier': case 'generic_placement_contract': case 'plate_contract_mismatch': return 'api_contract_mismatch'; case 'wall_hosted_opening_pattern': case 'world_placement_elevation': case 'wall_hosted_opening_alignment': return 'placement_context_mismatch'; case 'detached_snippet_scope': return 'detached_fragment_rewrite'; case 'metadata_query_pattern': return 'metadata_access_pattern'; case 'patch_revision_conflict': case 'patch_range_error': return 'stale_patch_target'; case 'patch_semantic_error': return source === 'patch' ? 'malformed_repair_reply' : 'api_contract_mismatch'; case 'unsafe_full_replacement': case 'destructive_partial_rewrite': return 'structural_script_corruption'; default: return code; } } function defaultRepairScope(rootCauseKey: string, code: string): RepairScope { switch (rootCauseKey) { case 'stale_patch_target': case 'malformed_repair_reply': case 'structural_script_corruption': case 'detached_fragment_rewrite': case 'creator_lifecycle_violation': return 'structural'; case 'placement_context_mismatch': return 'block'; case 'api_contract_mismatch': case 'unknown_api_reference': case 'metadata_access_pattern': return code === 'bare_identifier' ? 'block' : 'local'; default: return 'local'; } } function widenRepairScope(left: RepairScope, right: RepairScope): RepairScope { return compareRepairScope(left, right) >= 0 ? left : right; } function compareRepairScope(left: RepairScope, right: RepairScope): number { return repairScopeRank(left) - repairScopeRank(right); } function repairScopeRank(scope: RepairScope): number { switch (scope) { case 'local': return 0; case 'block': return 1; case 'structural': return 2; case 'full_rewrite': return 3; } } function dedupeEvidence(evidence: ScriptDiagnosticEvidence[]): ScriptDiagnosticEvidence[] { const seen = new Set(); const result: ScriptDiagnosticEvidence[] = []; for (const item of evidence) { const key = [ item.source ?? '', item.code ?? '', item.methodName ?? '', item.symbol ?? '', item.failureKind ?? '', item.range?.from ?? '', item.range?.to ?? '', item.snippet ?? '', ].join('|'); if (seen.has(key)) continue; seen.add(key); result.push(item); } return result; } function asRange(value: unknown): ScriptDiagnosticRange | null { if (!value || typeof value !== 'object') return null; const from = asNumber((value as Record).from); const to = asNumber((value as Record).to); if (from === null || to === null) return null; return { from, to }; } function asEvidence(value: unknown): ScriptDiagnosticEvidence | null { if (!value || typeof value !== 'object') return null; const record = value as Record; return { source: asDiagnosticSource(record.source) ?? undefined, code: asString(record.code) ?? undefined, message: asString(record.message) ?? undefined, methodName: asString(record.methodName) ?? undefined, symbol: asString(record.symbol) ?? undefined, failureKind: asString(record.failureKind) ?? undefined, range: asRange(record.range) ?? undefined, line: asNumber(record.line) ?? undefined, column: asNumber(record.column) ?? undefined, snippet: asString(record.snippet) ?? undefined, }; } function asDiagnosticSource(value: unknown): ScriptDiagnosticSource | null { return value === 'preflight' || value === 'runtime' || value === 'patch' ? value : null; } function asRepairScope(value: unknown): RepairScope | null { return value === 'local' || value === 'block' || value === 'structural' || value === 'full_rewrite' ? value : null; } function asString(value: unknown): string | null { return typeof value === 'string' && value.length > 0 ? value : null; } function asNumber(value: unknown): number | null { return typeof value === 'number' && Number.isFinite(value) ? value : null; } function truncateSnippet(value: string): string { return value.length > 80 ? `${value.slice(0, 77)}...` : value; }