{"version":3,"file":"assertions.d.ts","sourceRoot":"","sources":["../../../src/core/evaluation/assertions.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,eAAe,EAAa,MAAM,YAAY,CAAC;AAIjH,MAAM,WAAW,gBAAgB;IAChC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC5C,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;CAClB;AA0CD,wBAAsB,iBAAiB,CACtC,IAAI,EAAE,uBAAuB,EAC7B,OAAO,EAAE,gBAAgB,GACvB,OAAO,CAAC,yBAAyB,CAAC,CAiMpC;AAED,wBAAsB,kBAAkB,CACvC,KAAK,EAAE,uBAAuB,EAAE,EAChC,OAAO,EAAE,gBAAgB,GACvB,OAAO,CAAC,yBAAyB,EAAE,CAAC,CActC","sourcesContent":["import { exec } from \"node:child_process\";\nimport { readFile } from \"node:fs/promises\";\nimport { isAbsolute, resolve } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport type { EvaluationAssertionResult, EvaluationAssertionSpec, EvaluationEvent, JsonValue } from \"./types.js\";\n\nconst execAsync = promisify(exec);\n\nexport interface AssertionContext {\n\tworkspaceRoot?: string;\n\tevents: EvaluationEvent[];\n\tmetrics: Record<string, number | undefined>;\n\tevidenceIds: Set<string>;\n\ttimeoutMs: number;\n}\n\nfunction result(\n\tspec: EvaluationAssertionSpec,\n\tstatus: EvaluationAssertionResult[\"status\"],\n\texpected: JsonValue | undefined,\n\tobserved: JsonValue | undefined,\n\treasonCode: string,\n\tevidenceIds: string[] = [],\n): EvaluationAssertionResult {\n\treturn {\n\t\tassertionId: spec.assertionId,\n\t\tstatus,\n\t\texpected,\n\t\tobserved,\n\t\tevidenceIds,\n\t\treasonCode,\n\t\tseverity: spec.severity,\n\t};\n}\n\nfunction workspacePath(root: string | undefined, filePath: string | undefined): string {\n\tif (!root || !filePath || isAbsolute(filePath)) throw new Error(\"workspace-relative path required\");\n\tconst resolved = resolve(root, filePath);\n\tif (\n\t\tresolved !== resolve(root) &&\n\t\t!resolved.startsWith(`${resolve(root)}${process.platform === \"win32\" ? \"\\\\\" : \"/\"}`)\n\t)\n\t\tthrow new Error(\"path escapes evaluation workspace\");\n\treturn resolved;\n}\n\nfunction eventMatches(event: EvaluationEvent, spec: EvaluationAssertionSpec): boolean {\n\tif (event.type !== spec.pattern && spec.pattern !== undefined) return false;\n\tif (spec.expected && typeof spec.expected === \"object\" && !Array.isArray(spec.expected)) {\n\t\tfor (const [key, value] of Object.entries(spec.expected)) {\n\t\t\tif (event.details?.[key] !== value) return false;\n\t\t}\n\t}\n\treturn true;\n}\n\nexport async function evaluateAssertion(\n\tspec: EvaluationAssertionSpec,\n\tcontext: AssertionContext,\n): Promise<EvaluationAssertionResult> {\n\ttry {\n\t\tswitch (spec.kind) {\n\t\t\tcase \"file_exists\": {\n\t\t\t\tconst path = workspacePath(context.workspaceRoot, spec.path);\n\t\t\t\ttry {\n\t\t\t\t\tawait readFile(path);\n\t\t\t\t\treturn result(spec, \"pass\", true, true, \"FILE_EXISTS\", [spec.assertionId]);\n\t\t\t\t} catch {\n\t\t\t\t\treturn result(spec, \"fail\", true, false, \"FILE_MISSING\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tcase \"file_absent\": {\n\t\t\t\tconst path = workspacePath(context.workspaceRoot, spec.path);\n\t\t\t\ttry {\n\t\t\t\t\tawait readFile(path);\n\t\t\t\t\treturn result(spec, \"fail\", false, true, \"FILE_PRESENT\");\n\t\t\t\t} catch {\n\t\t\t\t\treturn result(spec, \"pass\", false, false, \"FILE_ABSENT\", [spec.assertionId]);\n\t\t\t\t}\n\t\t\t}\n\t\t\tcase \"file_content\": {\n\t\t\t\tconst path = workspacePath(context.workspaceRoot, spec.path);\n\t\t\t\tconst content = await readFile(path, \"utf8\");\n\t\t\t\tconst expected = typeof spec.expected === \"string\" ? spec.expected : \"\";\n\t\t\t\tconst matches = spec.pattern ? new RegExp(spec.pattern, \"u\").test(content) : content === expected;\n\t\t\t\treturn result(\n\t\t\t\t\tspec,\n\t\t\t\t\tmatches ? \"pass\" : \"fail\",\n\t\t\t\t\tspec.expected,\n\t\t\t\t\tcontent,\n\t\t\t\t\tmatches ? \"FILE_CONTENT\" : \"FILE_CONTENT_MISMATCH\",\n\t\t\t\t\t[spec.assertionId],\n\t\t\t\t);\n\t\t\t}\n\t\t\tcase \"command\": {\n\t\t\t\tif (!context.workspaceRoot || !spec.command)\n\t\t\t\t\treturn result(spec, \"invalid\", spec.expected, undefined, \"COMMAND_CONTEXT_MISSING\");\n\t\t\t\tconst commandResult = await execAsync(spec.command, {\n\t\t\t\t\tcwd: context.workspaceRoot,\n\t\t\t\t\ttimeout: context.timeoutMs,\n\t\t\t\t\tmaxBuffer: 2 * 1024 * 1024,\n\t\t\t\t\twindowsHide: true,\n\t\t\t\t});\n\t\t\t\tconst observed = commandResult.stdout.trim();\n\t\t\t\tconst expected = spec.expected;\n\t\t\t\tconst pass = expected === undefined || observed === String(expected);\n\t\t\t\treturn result(\n\t\t\t\t\tspec,\n\t\t\t\t\tpass ? \"pass\" : \"fail\",\n\t\t\t\t\texpected,\n\t\t\t\t\tobserved,\n\t\t\t\t\tpass ? \"COMMAND_PASS\" : \"COMMAND_OUTPUT_MISMATCH\",\n\t\t\t\t\t[spec.assertionId],\n\t\t\t\t);\n\t\t\t}\n\t\t\tcase \"git_diff\": {\n\t\t\t\tif (!context.workspaceRoot) return result(spec, \"invalid\", spec.expected, undefined, \"WORKSPACE_MISSING\");\n\t\t\t\tconst { stdout } = await execAsync(\"git diff --no-ext-diff --binary\", {\n\t\t\t\t\tcwd: context.workspaceRoot,\n\t\t\t\t\ttimeout: context.timeoutMs,\n\t\t\t\t\tmaxBuffer: 4 * 1024 * 1024,\n\t\t\t\t});\n\t\t\t\tconst expected = typeof spec.expected === \"string\" ? spec.expected : \"\";\n\t\t\t\tconst pass = spec.pattern ? new RegExp(spec.pattern, \"u\").test(stdout) : stdout === expected;\n\t\t\t\treturn result(\n\t\t\t\t\tspec,\n\t\t\t\t\tpass ? \"pass\" : \"fail\",\n\t\t\t\t\tspec.expected,\n\t\t\t\t\tstdout,\n\t\t\t\t\tpass ? \"GIT_DIFF_MATCH\" : \"GIT_DIFF_MISMATCH\",\n\t\t\t\t\t[spec.assertionId],\n\t\t\t\t);\n\t\t\t}\n\t\t\tcase \"event_present\": {\n\t\t\t\tconst matches = context.events.filter((event) => eventMatches(event, spec));\n\t\t\t\treturn result(\n\t\t\t\t\tspec,\n\t\t\t\t\tmatches.length > 0 ? \"pass\" : \"fail\",\n\t\t\t\t\ttrue,\n\t\t\t\t\tmatches.length > 0,\n\t\t\t\t\tmatches.length > 0 ? \"EVENT_PRESENT\" : \"EVENT_MISSING\",\n\t\t\t\t\tmatches.map((event) => event.eventId),\n\t\t\t\t);\n\t\t\t}\n\t\t\tcase \"event_absent\": {\n\t\t\t\tconst matches = context.events.filter((event) => eventMatches(event, spec));\n\t\t\t\treturn result(\n\t\t\t\t\tspec,\n\t\t\t\t\tmatches.length === 0 ? \"pass\" : \"fail\",\n\t\t\t\t\tfalse,\n\t\t\t\t\tmatches.length > 0,\n\t\t\t\t\tmatches.length === 0 ? \"EVENT_ABSENT\" : \"EVENT_UNEXPECTED\",\n\t\t\t\t\tmatches.map((event) => event.eventId),\n\t\t\t\t);\n\t\t\t}\n\t\t\tcase \"event_order\": {\n\t\t\t\tconst order = Array.isArray(spec.expected) ? spec.expected.map(String) : [];\n\t\t\t\tconst indices = order.map((type) => context.events.findIndex((event) => event.type === type));\n\t\t\t\tconst pass = indices.every(\n\t\t\t\t\t(index, position) => index >= 0 && (position === 0 || index > indices[position - 1]!),\n\t\t\t\t);\n\t\t\t\treturn result(\n\t\t\t\t\tspec,\n\t\t\t\t\tpass ? \"pass\" : \"fail\",\n\t\t\t\t\torder,\n\t\t\t\t\tindices,\n\t\t\t\t\tpass ? \"EVENT_ORDER\" : \"EVENT_ORDER_MISMATCH\",\n\t\t\t\t\tcontext.events.filter((event) => order.includes(event.type)).map((event) => event.eventId),\n\t\t\t\t);\n\t\t\t}\n\t\t\tcase \"tool_effect\":\n\t\t\tcase \"policy_decision\":\n\t\t\tcase \"transaction_state\":\n\t\t\tcase \"rollback_state\":\n\t\t\tcase \"process_cleanup\": {\n\t\t\t\tconst matches = context.events.filter((event) => eventMatches(event, spec));\n\t\t\t\tconst pass = matches.length > 0 && matches.every((event) => event.details?.status !== \"fail\");\n\t\t\t\treturn result(\n\t\t\t\t\tspec,\n\t\t\t\t\tpass ? \"pass\" : \"fail\",\n\t\t\t\t\tspec.expected,\n\t\t\t\t\tmatches.map((event) => event.details ?? {}),\n\t\t\t\t\tpass ? `${spec.kind.toUpperCase()}_PASS` : `${spec.kind.toUpperCase()}_FAIL`,\n\t\t\t\t\tmatches.map((event) => event.eventId),\n\t\t\t\t);\n\t\t\t}\n\t\t\tcase \"workspace_clean\": {\n\t\t\t\tif (!context.workspaceRoot) return result(spec, \"invalid\", true, undefined, \"WORKSPACE_MISSING\");\n\t\t\t\tconst { stdout } = await execAsync(\"git status --porcelain\", {\n\t\t\t\t\tcwd: context.workspaceRoot,\n\t\t\t\t\ttimeout: context.timeoutMs,\n\t\t\t\t});\n\t\t\t\tconst pass = stdout.trim().length === 0;\n\t\t\t\treturn result(spec, pass ? \"pass\" : \"fail\", true, pass, pass ? \"WORKSPACE_CLEAN\" : \"WORKSPACE_DIRTY\", [\n\t\t\t\t\tspec.assertionId,\n\t\t\t\t]);\n\t\t\t}\n\t\t\tcase \"budget_bound\": {\n\t\t\t\tconst metricId = spec.customKey ?? spec.path;\n\t\t\t\tif (!metricId) return result(spec, \"invalid\", spec.expected, undefined, \"METRIC_ID_MISSING\");\n\t\t\t\tconst observed = context.metrics[metricId];\n\t\t\t\tif (observed === undefined || typeof spec.expected !== \"number\")\n\t\t\t\t\treturn result(spec, \"invalid\", spec.expected, observed, \"METRIC_EVIDENCE_MISSING\");\n\t\t\t\tconst pass = observed <= spec.expected;\n\t\t\t\treturn result(\n\t\t\t\t\tspec,\n\t\t\t\t\tpass ? \"pass\" : \"fail\",\n\t\t\t\t\tspec.expected,\n\t\t\t\t\tobserved,\n\t\t\t\t\tpass ? \"BUDGET_WITHIN_BOUND\" : \"BUDGET_EXCEEDED\",\n\t\t\t\t\t[spec.assertionId],\n\t\t\t\t);\n\t\t\t}\n\t\t\tcase \"evidence_linkage\": {\n\t\t\t\tconst evidenceId = typeof spec.expected === \"string\" ? spec.expected : undefined;\n\t\t\t\tconst pass = evidenceId !== undefined && context.evidenceIds.has(evidenceId);\n\t\t\t\treturn result(\n\t\t\t\t\tspec,\n\t\t\t\t\tpass ? \"pass\" : \"fail\",\n\t\t\t\t\tspec.expected,\n\t\t\t\t\tpass ? evidenceId : undefined,\n\t\t\t\t\tpass ? \"EVIDENCE_LINKED\" : \"EVIDENCE_MISSING\",\n\t\t\t\t\tpass ? [evidenceId!] : [],\n\t\t\t\t);\n\t\t\t}\n\t\t\tcase \"retrieval_relevance\": {\n\t\t\t\tconst relevant = context.events.filter(\n\t\t\t\t\t(event) => event.type === \"retrieval.result\" && event.details?.relevant === true,\n\t\t\t\t);\n\t\t\t\tconst expected = typeof spec.expected === \"number\" ? spec.expected : 1;\n\t\t\t\tconst observed = relevant.length;\n\t\t\t\treturn result(\n\t\t\t\t\tspec,\n\t\t\t\t\tobserved >= expected ? \"pass\" : \"fail\",\n\t\t\t\t\texpected,\n\t\t\t\t\tobserved,\n\t\t\t\t\tobserved >= expected ? \"RETRIEVAL_RELEVANT\" : \"RETRIEVAL_MISS\",\n\t\t\t\t\trelevant.map((event) => event.eventId),\n\t\t\t\t);\n\t\t\t}\n\t\t\tcase \"custom\":\n\t\t\t\treturn result(spec, \"not_evaluated\", spec.expected, undefined, \"CUSTOM_ASSERTION_REQUIRES_HANDLER\");\n\t\t}\n\t} catch (error) {\n\t\treturn result(\n\t\t\tspec,\n\t\t\t\"invalid\",\n\t\t\tspec.expected,\n\t\t\tundefined,\n\t\t\terror instanceof Error ? `ASSERTION_ERROR:${error.message}` : \"ASSERTION_ERROR\",\n\t\t);\n\t}\n}\n\nexport async function evaluateAssertions(\n\tspecs: EvaluationAssertionSpec[],\n\tcontext: AssertionContext,\n): Promise<EvaluationAssertionResult[]> {\n\tconst results: EvaluationAssertionResult[] = [];\n\tfor (const spec of specs) {\n\t\tif (\n\t\t\tspec.dependsOn?.some(\n\t\t\t\t(dependency) => results.find((item) => item.assertionId === dependency)?.status !== \"pass\",\n\t\t\t)\n\t\t) {\n\t\t\tresults.push(result(spec, \"not_evaluated\", spec.expected, undefined, \"DEPENDENCY_FAILED\"));\n\t\t\tcontinue;\n\t\t}\n\t\tresults.push(await evaluateAssertion(spec, context));\n\t}\n\treturn results;\n}\n"]}