/** * Tests for Chain Inspector top-N template matches (v0.4.4). */ import { describe, it, expect } from "vitest"; import { inspectChains } from "../../analysis/chainInspector"; import { createVaultAppFromFixtures } from "../helpers/vaultHelper"; import { loadChainRolesFromFixtures, loadChainTemplatesFromFixtures } from "../helpers/configHelper"; import type { SectionContext } from "../../analysis/sectionContext"; import type { ChainTemplatesConfig } from "../../dictionary/types"; describe("Chain Inspector top-N template matches", () => { it("should return top 3 matches sorted by confidence, score, templateName", async () => { const app = createVaultAppFromFixtures(); const chainRoles = loadChainRolesFromFixtures(); const baseTemplates = loadChainTemplatesFromFixtures(); if (!chainRoles || !baseTemplates) { console.warn("Skipping test: real config files not found in fixtures"); return; } // Create templates config with multiple templates that should match const chainTemplates: ChainTemplatesConfig = { ...baseTemplates, defaults: { ...baseTemplates.defaults, profiles: { discovery: { required_links: false, min_slots_filled_for_gap_findings: 1, min_score_for_gap_findings: 0, }, }, }, templates: [ ...baseTemplates.templates, { name: "simple_chain", slots: [ { id: "start", allowed_node_types: ["experience"] }, { id: "end", allowed_node_types: ["decision"] }, ], links: [ { from: "start", to: "end", allowed_edge_roles: ["causal"] }, ], }, { name: "trigger_transformation", slots: [ { id: "trigger", allowed_node_types: ["experience"] }, { id: "transformation", allowed_node_types: ["insight"] }, ], links: [ { from: "trigger", to: "transformation", allowed_edge_roles: ["causal", "influences"] }, ], }, ], }; const context: SectionContext = { file: "Tests/01_experience_trigger.md", heading: "Kontext", zoneKind: "content", sectionIndex: 0, }; const report = await inspectChains( app, context, { includeNoteLinks: true, includeCandidates: false, maxDepth: 3, direction: "both", }, chainRoles, "_system/dictionary/edge_vocabulary.md", chainTemplates, undefined, "discovery" ); // Should have template matches (may be empty if no templates match) if (!report.templateMatches || report.templateMatches.length === 0) { // If no matches, topNUsed should still be set if templates were processed if (report.templatesSource) { expect(report.analysisMeta?.topNUsed).toBe(3); } return; // Skip ordering tests if no matches } // Should have at most 3 matches per template (topN per chain type) const byTemplate = new Map(); for (const m of report.templateMatches) { byTemplate.set(m.templateName, (byTemplate.get(m.templateName) ?? 0) + 1); } for (const count of byTemplate.values()) { expect(count).toBeLessThanOrEqual(3); } // Should have topNUsed in analysisMeta expect(report.analysisMeta?.topNUsed).toBe(3); // Verify deterministic ordering: confidence rank (confirmed > plausible > weak), then score desc, then templateName asc const confidenceRank = (c: "confirmed" | "plausible" | "weak"): number => { if (c === "confirmed") return 3; if (c === "plausible") return 2; return 1; // weak }; for (let i = 0; i < report.templateMatches.length - 1; i++) { const current = report.templateMatches[i]; const next = report.templateMatches[i + 1]; if (!current || !next) continue; const currentRank = confidenceRank(current.confidence); const nextRank = confidenceRank(next.confidence); // If confidence ranks differ, current should be higher if (currentRank !== nextRank) { expect(currentRank).toBeGreaterThan(nextRank); continue; } // If scores differ, current should be higher if (current.score !== next.score) { expect(current.score).toBeGreaterThanOrEqual(next.score); continue; } // If templateNames differ, current should be lexicographically smaller expect(current.templateName.localeCompare(next.templateName)).toBeLessThanOrEqual(0); } // If we have multiple templates, both should appear const templateNames = report.templateMatches.map(m => m.templateName); if (templateNames.length >= 2) { // At least one template should be present expect(templateNames.length).toBeGreaterThan(0); } }); it("should show Why evidence in pretty print format", async () => { const app = createVaultAppFromFixtures(); const chainRoles = loadChainRolesFromFixtures(); const chainTemplates = loadChainTemplatesFromFixtures(); if (!chainRoles || !chainTemplates) { console.warn("Skipping test: real config files not found in fixtures"); return; } const context: SectionContext = { file: "Tests/01_experience_trigger.md", heading: "Kontext", zoneKind: "content", sectionIndex: 0, }; const report = await inspectChains( app, context, { includeNoteLinks: true, includeCandidates: false, maxDepth: 3, direction: "both", }, chainRoles, "_system/dictionary/edge_vocabulary.md", chainTemplates, undefined, "discovery" ); // Check that matches have roleEvidence if they have links if (report.templateMatches && report.templateMatches.length > 0) { for (const match of report.templateMatches) { if (match.requiredLinks > 0 && match.satisfiedLinks > 0) { // Should have roleEvidence expect(match.roleEvidence).toBeDefined(); if (match.roleEvidence && match.roleEvidence.length > 0) { // Each evidence should have from, to, edgeRole, rawEdgeType for (const ev of match.roleEvidence) { expect(ev.from).toBeDefined(); expect(ev.to).toBeDefined(); expect(ev.edgeRole).toBeDefined(); expect(ev.rawEdgeType).toBeDefined(); } } } } } }); });