@taodigital/agents 0.0.0-stage → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +99 -2
  2. package/dist/api-test-agent.d.ts +126 -0
  3. package/dist/api-test-agent.js +267 -0
  4. package/dist/artifact-intel-agent.d.ts +11 -0
  5. package/dist/artifact-intel-agent.js +152 -0
  6. package/dist/coverage-gap-agent.d.ts +25 -0
  7. package/dist/coverage-gap-agent.js +269 -0
  8. package/dist/coverage-map-agent.d.ts +15 -0
  9. package/dist/coverage-map-agent.js +255 -0
  10. package/dist/crawl-diff-agent.d.ts +27 -0
  11. package/dist/crawl-diff-agent.js +412 -0
  12. package/dist/data-agent.d.ts +101 -0
  13. package/dist/data-agent.js +2284 -0
  14. package/dist/document-agent.d.ts +10 -0
  15. package/dist/document-agent.js +32 -0
  16. package/dist/enhancement-agent.d.ts +48 -0
  17. package/dist/enhancement-agent.js +342 -0
  18. package/dist/field-detector-agent.d.ts +35 -0
  19. package/dist/field-detector-agent.js +220 -0
  20. package/dist/flow-agent.d.ts +130 -0
  21. package/dist/flow-agent.js +169 -0
  22. package/dist/github-agent.d.ts +18 -0
  23. package/dist/github-agent.js +39 -0
  24. package/dist/github-write-agent.d.ts +67 -0
  25. package/dist/github-write-agent.js +249 -0
  26. package/dist/grouping-agent.d.ts +18 -0
  27. package/dist/grouping-agent.js +338 -0
  28. package/dist/healing-agent.d.ts +73 -0
  29. package/dist/healing-agent.js +137 -0
  30. package/dist/index.d.ts +36 -0
  31. package/dist/index.js +67 -0
  32. package/dist/integration-test-agent.d.ts +118 -0
  33. package/dist/integration-test-agent.js +165 -0
  34. package/dist/label-assignment-agent.d.ts +15 -0
  35. package/dist/label-assignment-agent.js +97 -0
  36. package/dist/llm-config.d.ts +38 -0
  37. package/dist/llm-config.js +287 -0
  38. package/dist/llm-mock.d.ts +77 -0
  39. package/dist/llm-mock.js +192 -0
  40. package/dist/llm.d.ts +58 -0
  41. package/dist/llm.js +438 -0
  42. package/dist/nlp-agent.d.ts +208 -0
  43. package/dist/nlp-agent.js +340 -0
  44. package/dist/orchestrator.d.ts +285 -0
  45. package/dist/orchestrator.js +141 -0
  46. package/dist/plan-agent.d.ts +4 -0
  47. package/dist/plan-agent.js +200 -0
  48. package/dist/repo-scan-agent.d.ts +189 -0
  49. package/dist/repo-scan-agent.js +2032 -0
  50. package/dist/requirements-agent.d.ts +59 -0
  51. package/dist/requirements-agent.js +641 -0
  52. package/dist/scenario-agent.d.ts +20 -0
  53. package/dist/scenario-agent.js +123 -0
  54. package/dist/script-agent.d.ts +447 -0
  55. package/dist/script-agent.js +11813 -0
  56. package/dist/security-test-agent.d.ts +111 -0
  57. package/dist/security-test-agent.js +141 -0
  58. package/dist/skill-file-agent.d.ts +264 -0
  59. package/dist/skill-file-agent.js +3470 -0
  60. package/dist/step-coverage.d.ts +46 -0
  61. package/dist/step-coverage.js +288 -0
  62. package/dist/step-utils.d.ts +2 -0
  63. package/dist/step-utils.js +62 -0
  64. package/dist/test-case-agent.d.ts +206 -0
  65. package/dist/test-case-agent.js +2141 -0
  66. package/dist/test-import-agent.d.ts +148 -0
  67. package/dist/test-import-agent.js +3235 -0
  68. package/dist/test-script-agent.d.ts +16 -0
  69. package/dist/test-script-agent.js +48 -0
  70. package/dist/token-pricing.d.ts +14 -0
  71. package/dist/token-pricing.js +62 -0
  72. package/dist/types.d.ts +232 -0
  73. package/dist/types.js +7 -0
  74. package/dist/visual-agent.d.ts +114 -0
  75. package/dist/visual-agent.js +261 -0
  76. package/package.json +26 -4
@@ -0,0 +1,25 @@
1
+ /**
2
+ * ZeTa-AI Bug Identification Engine — P7
3
+ *
4
+ * Four entry points:
5
+ * computeRequirementCoverage — LLM-based AC coverage check → RequirementCoverage + BugSignal rows
6
+ * analyzeTestRunFailures — cluster failed TestResults by error type → BugSignal rows
7
+ * promoteVisualRegressionToBugSignal — elevate FLAG/BLOCK VisualDiffs to BugSignals
8
+ * promoteBugSignalToJira — push a BugSignal to Jira via stored connector credentials
9
+ *
10
+ * NOTE: BugSignal and RequirementCoverage are new Prisma models (pending schema migration).
11
+ * prismaAdmin is cast to `any` for those two models until the migration is applied.
12
+ */
13
+ export declare function computeRequirementCoverage(tenantId: string, projectId: string): Promise<{
14
+ requirementsChecked: number;
15
+ gapsFound: number;
16
+ }>;
17
+ export declare function analyzeTestRunFailures(tenantId: string, projectId: string, runId: string): Promise<{
18
+ failuresAnalyzed: number;
19
+ signalsCreated: number;
20
+ }>;
21
+ export declare function promoteVisualRegressionToBugSignal(tenantId: string, projectId: string, visualDiffId: string): Promise<any | null>;
22
+ export declare function promoteBugSignalToJira(tenantId: string, projectId: string, signalId: string): Promise<{
23
+ jiraIssueKey: string;
24
+ jiraIssueUrl: string;
25
+ }>;
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ /**
3
+ * ZeTa-AI Bug Identification Engine — P7
4
+ *
5
+ * Four entry points:
6
+ * computeRequirementCoverage — LLM-based AC coverage check → RequirementCoverage + BugSignal rows
7
+ * analyzeTestRunFailures — cluster failed TestResults by error type → BugSignal rows
8
+ * promoteVisualRegressionToBugSignal — elevate FLAG/BLOCK VisualDiffs to BugSignals
9
+ * promoteBugSignalToJira — push a BugSignal to Jira via stored connector credentials
10
+ *
11
+ * NOTE: BugSignal and RequirementCoverage are new Prisma models (pending schema migration).
12
+ * prismaAdmin is cast to `any` for those two models until the migration is applied.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.computeRequirementCoverage = computeRequirementCoverage;
16
+ exports.analyzeTestRunFailures = analyzeTestRunFailures;
17
+ exports.promoteVisualRegressionToBugSignal = promoteVisualRegressionToBugSignal;
18
+ exports.promoteBugSignalToJira = promoteBugSignalToJira;
19
+ const database_1 = require("@taodigital/database");
20
+ const core_1 = require("@taodigital/core");
21
+ const llm_js_1 = require("./llm.js");
22
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
23
+ function classifyError(msg) {
24
+ const m = msg?.toLowerCase() ?? '';
25
+ if (m.includes('locator') || m.includes('selector') || m.includes('element not found') || m.includes('waitfor'))
26
+ return 'LOCATOR_ERROR';
27
+ if (m.includes('expected') || m.includes('assert') || m.includes('to equal') || m.includes('to be'))
28
+ return 'ASSERTION_ERROR';
29
+ if (m.includes('navigation') || m.includes('net::err') || m.includes('timeout'))
30
+ return 'NAVIGATION_ERROR';
31
+ if (m.includes('network') || m.includes('404') || m.includes('500') || m.includes('fetch'))
32
+ return 'NETWORK_ERROR';
33
+ return 'UNKNOWN_ERROR';
34
+ }
35
+ const db = database_1.prismaAdmin;
36
+ // ─── Function 1: computeRequirementCoverage ───────────────────────────────────
37
+ async function computeRequirementCoverage(tenantId, projectId) {
38
+ const requirements = await database_1.prismaAdmin.requirement.findMany({
39
+ where: { tenantId, projectId },
40
+ take: 200,
41
+ });
42
+ let gapsFound = 0;
43
+ for (const req of requirements) {
44
+ const testCases = await database_1.prismaAdmin.testCase.findMany({
45
+ where: {
46
+ tenantId,
47
+ projectId,
48
+ OR: [
49
+ { traceTo: req.id },
50
+ { traceToExtra: { string_contains: req.id } },
51
+ ],
52
+ },
53
+ select: { id: true, steps: true, title: true },
54
+ });
55
+ const acceptanceCriteria = Array.isArray(req.acceptanceCriteria)
56
+ ? req.acceptanceCriteria
57
+ : [];
58
+ if (acceptanceCriteria.length === 0)
59
+ continue;
60
+ const promptText = (await (0, core_1.getAgentPrompt)('requirement-coverage-check').catch(() => undefined)) ??
61
+ 'Given this acceptance criterion and test steps, return JSON: { covered: boolean, coverageScore: number, missingAspects: string[] }';
62
+ const acScores = [];
63
+ for (const ac of acceptanceCriteria) {
64
+ if (testCases.length === 0) {
65
+ acScores.push(0);
66
+ continue;
67
+ }
68
+ const steps = testCases.flatMap((tc) => Array.isArray(tc.steps) ? tc.steps : []);
69
+ let score = 0;
70
+ try {
71
+ const { text: raw } = await (0, llm_js_1.askTracked)(promptText, `Acceptance criterion: ${ac}\n\nTest steps: ${JSON.stringify(steps)}`);
72
+ const parsed = JSON.parse(raw.match(/\{[\s\S]*\}/)?.[0] ?? '{"coverageScore":0}');
73
+ score = typeof parsed.coverageScore === 'number' ? parsed.coverageScore : 0;
74
+ }
75
+ catch {
76
+ score = 0;
77
+ }
78
+ acScores.push(score);
79
+ }
80
+ const avgScore = acScores.length > 0
81
+ ? acScores.reduce((a, b) => a + b, 0) / acScores.length
82
+ : 0;
83
+ await db.requirementCoverage.upsert({
84
+ where: {
85
+ tenantId_requirementId: { tenantId, requirementId: req.id },
86
+ },
87
+ create: {
88
+ tenantId,
89
+ projectId,
90
+ requirementId: req.id,
91
+ testCaseIds: testCases.map((t) => t.id),
92
+ coverageScore: avgScore,
93
+ uncoveredAcIds: [],
94
+ },
95
+ update: {
96
+ testCaseIds: testCases.map((t) => t.id),
97
+ coverageScore: avgScore,
98
+ uncoveredAcIds: [],
99
+ },
100
+ });
101
+ if (avgScore < 0.5) {
102
+ await db.bugSignal.upsert({
103
+ where: {
104
+ tenantId_sourceRefHash: {
105
+ tenantId,
106
+ sourceRefHash: `REQUIREMENT_GAP:${req.id}`,
107
+ },
108
+ },
109
+ create: {
110
+ tenantId,
111
+ projectId,
112
+ signalType: 'REQUIREMENT_GAP',
113
+ severity: avgScore < 0.2 ? 'HIGH' : 'MEDIUM',
114
+ title: `Requirement gap: ${req.title}`,
115
+ sourceRef: { requirementId: req.id },
116
+ sourceRefHash: `REQUIREMENT_GAP:${req.id}`,
117
+ },
118
+ update: {
119
+ severity: avgScore < 0.2 ? 'HIGH' : 'MEDIUM',
120
+ },
121
+ });
122
+ gapsFound += 1;
123
+ }
124
+ }
125
+ return { requirementsChecked: requirements.length, gapsFound };
126
+ }
127
+ // ─── Function 2: analyzeTestRunFailures ──────────────────────────────────────
128
+ async function analyzeTestRunFailures(tenantId, projectId, runId) {
129
+ const failedResults = await database_1.prismaAdmin.testResult.findMany({
130
+ where: { runId, tenantId, status: 'FAILED' },
131
+ include: {
132
+ testCase: { select: { id: true, priority: true, title: true } },
133
+ },
134
+ take: 200,
135
+ });
136
+ // Group by error type
137
+ const groups = new Map();
138
+ for (const result of failedResults) {
139
+ const errorType = classifyError(result.error ?? '');
140
+ if (!groups.has(errorType))
141
+ groups.set(errorType, []);
142
+ groups.get(errorType).push(result);
143
+ }
144
+ const priorityToSeverity = {
145
+ CRITICAL: 'CRITICAL',
146
+ HIGH: 'HIGH',
147
+ };
148
+ let signalsCreated = 0;
149
+ for (const [errorType, group] of groups) {
150
+ // Determine highest priority in the group
151
+ const hasCritical = group.some((r) => r.testCase?.priority === 'CRITICAL');
152
+ const hasHigh = group.some((r) => r.testCase?.priority === 'HIGH');
153
+ const severity = hasCritical
154
+ ? 'CRITICAL'
155
+ : hasHigh
156
+ ? 'HIGH'
157
+ : 'MEDIUM';
158
+ await db.bugSignal.create({
159
+ data: {
160
+ tenantId,
161
+ projectId,
162
+ signalType: 'RUNNER_FAILURE',
163
+ severity,
164
+ title: `${errorType} failures in run ${runId.slice(-8)}`,
165
+ sourceRef: {
166
+ runId,
167
+ testResultIds: group.map((r) => r.id),
168
+ },
169
+ affectedTestCaseIds: group.map((r) => r.testCaseId),
170
+ },
171
+ });
172
+ signalsCreated += 1;
173
+ }
174
+ return { failuresAnalyzed: failedResults.length, signalsCreated };
175
+ }
176
+ // ─── Function 3: promoteVisualRegressionToBugSignal ──────────────────────────
177
+ async function promoteVisualRegressionToBugSignal(tenantId, projectId, visualDiffId) {
178
+ const visualDiff = await database_1.prismaAdmin.visualDiff.findFirst({
179
+ where: { id: visualDiffId, tenantId },
180
+ });
181
+ if (!visualDiff)
182
+ return null;
183
+ if (visualDiff.bugSignalVisualDiffId)
184
+ return null;
185
+ if (visualDiff.recommendation === 'FLAG' ||
186
+ visualDiff.recommendation === 'BLOCK') {
187
+ const bugReportDraft = visualDiff.bugReportDraft;
188
+ const signal = await db.bugSignal.create({
189
+ data: {
190
+ tenantId,
191
+ projectId,
192
+ signalType: 'VISUAL_REGRESSION',
193
+ severity: visualDiff.recommendation === 'BLOCK' ? 'CRITICAL' : 'HIGH',
194
+ title: bugReportDraft?.title ?? 'Visual regression detected',
195
+ description: bugReportDraft?.description ?? null,
196
+ sourceRef: { visualDiffId },
197
+ },
198
+ });
199
+ await database_1.prismaAdmin.visualDiff.update({
200
+ where: { id: visualDiffId },
201
+ data: { bugSignalVisualDiffId: signal.id },
202
+ });
203
+ return signal;
204
+ }
205
+ return null;
206
+ }
207
+ // ─── Function 4: promoteBugSignalToJira ──────────────────────────────────────
208
+ const severityToJiraPriority = {
209
+ CRITICAL: 'Highest',
210
+ HIGH: 'High',
211
+ MEDIUM: 'Medium',
212
+ LOW: 'Low',
213
+ };
214
+ async function promoteBugSignalToJira(tenantId, projectId, signalId) {
215
+ const signal = await db.bugSignal.findFirstOrThrow({
216
+ where: { id: signalId, tenantId },
217
+ });
218
+ const connector = await database_1.prismaAdmin.connector.findFirst({
219
+ where: { projectId, kind: 'JIRA', tenantId },
220
+ });
221
+ if (!connector) {
222
+ throw new Error('No Jira connector configured for this project');
223
+ }
224
+ const cfg = (connector.config ?? {});
225
+ const baseUrl = (cfg.baseUrl ?? '').replace(/\/$/, '');
226
+ const projectKey = cfg.projectKey ?? projectId.slice(0, 8).toUpperCase();
227
+ const payload = {
228
+ fields: {
229
+ project: { key: projectKey },
230
+ summary: signal.title,
231
+ description: {
232
+ version: 1,
233
+ type: 'doc',
234
+ content: [
235
+ {
236
+ type: 'paragraph',
237
+ content: [
238
+ {
239
+ type: 'text',
240
+ text: signal.description ?? signal.title,
241
+ },
242
+ ],
243
+ },
244
+ ],
245
+ },
246
+ issuetype: { name: 'Bug' },
247
+ priority: { name: severityToJiraPriority[signal.severity] ?? 'Medium' },
248
+ },
249
+ };
250
+ const apiToken = cfg.apiToken ?? connector.secretRef;
251
+ const userEmail = cfg.userEmail ?? '';
252
+ const res = await fetch(`${baseUrl}/rest/api/3/issue`, {
253
+ method: 'POST',
254
+ headers: {
255
+ 'Content-Type': 'application/json',
256
+ Authorization: `Basic ${Buffer.from(`${userEmail}:${apiToken}`).toString('base64')}`,
257
+ },
258
+ body: JSON.stringify(payload),
259
+ });
260
+ if (!res.ok) {
261
+ const body = await res.text().catch(() => '');
262
+ throw new Error(`Jira API error ${res.status}: ${body.slice(0, 200)}`);
263
+ }
264
+ const data = (await res.json());
265
+ return {
266
+ jiraIssueKey: data.key,
267
+ jiraIssueUrl: `${baseUrl}/browse/${data.key}`,
268
+ };
269
+ }
@@ -0,0 +1,15 @@
1
+ import type { ExtractedTest } from './repo-scan-agent.js';
2
+ export interface MappingResult {
3
+ testCaseId: string;
4
+ status: 'MAPPED' | 'UNMAPPED' | 'AMBIGUOUS';
5
+ matchedFilePath?: string;
6
+ scriptTestName?: string;
7
+ lineNumber?: number;
8
+ confidence: number;
9
+ matchMethod: 'EXACT' | 'NORMALIZED' | 'FUZZY' | 'AI' | 'UNMAPPED';
10
+ }
11
+ export declare function buildCoverageMap(testCases: Array<{
12
+ id: string;
13
+ title: string;
14
+ ref?: string | null;
15
+ }>, extractedTests: ExtractedTest[], tenantId: string): Promise<MappingResult[]>;
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildCoverageMap = buildCoverageMap;
4
+ const core_1 = require("@taodigital/core");
5
+ const llm_js_1 = require("./llm.js");
6
+ const llm_config_js_1 = require("./llm-config.js");
7
+ const app_brain_1 = require("@taodigital/app-brain");
8
+ function normalize(s) {
9
+ return s.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
10
+ }
11
+ function levenshtein(a, b) {
12
+ const m = a.length;
13
+ const n = b.length;
14
+ const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
15
+ for (let i = 1; i <= m; i++) {
16
+ for (let j = 1; j <= n; j++) {
17
+ if (a[i - 1] === b[j - 1]) {
18
+ dp[i][j] = dp[i - 1][j - 1];
19
+ }
20
+ else {
21
+ dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]);
22
+ }
23
+ }
24
+ }
25
+ return dp[m][n];
26
+ }
27
+ function similarity(a, b) {
28
+ if (a.length === 0 && b.length === 0)
29
+ return 1.0;
30
+ const maxLen = Math.max(a.length, b.length);
31
+ if (maxLen === 0)
32
+ return 1.0;
33
+ return 1 - levenshtein(a, b) / maxLen;
34
+ }
35
+ function assignStatus(confidence) {
36
+ if (confidence >= 0.95)
37
+ return 'MAPPED';
38
+ if (confidence >= 0.70)
39
+ return 'AMBIGUOUS';
40
+ return 'UNMAPPED';
41
+ }
42
+ function makeMapped(testCaseId, et, confidence, matchMethod) {
43
+ return {
44
+ testCaseId,
45
+ status: assignStatus(confidence),
46
+ matchedFilePath: et.filePath,
47
+ scriptTestName: et.name,
48
+ lineNumber: et.lineNumber,
49
+ confidence,
50
+ matchMethod,
51
+ };
52
+ }
53
+ function makeUnmapped(testCaseId) {
54
+ return {
55
+ testCaseId,
56
+ status: 'UNMAPPED',
57
+ confidence: 0,
58
+ matchMethod: 'UNMAPPED',
59
+ };
60
+ }
61
+ async function runAIPass(unmatched, pool, tenantId) {
62
+ const results = new Map();
63
+ const llmCfg = await (0, llm_config_js_1.getTaskAgentLLMConfig)(tenantId, 'coverage_mapping');
64
+ const BATCH_SIZE = 20;
65
+ for (let i = 0; i < unmatched.length; i += BATCH_SIZE) {
66
+ const batch = unmatched.slice(i, i + BATCH_SIZE);
67
+ const testCasesText = batch
68
+ .map((tc) => `- id: "${tc.id}", title: "${tc.title}"`)
69
+ .join('\n');
70
+ const extractedNamesText = pool.map((et) => `- "${et.name}"`).join('\n');
71
+ const SYSTEM_DEFAULT = 'You are a test mapping assistant. Given a list of test case titles and extracted test names from source code, ' +
72
+ 'find the best match for each test case. Return a JSON array only, no explanation.';
73
+ const system = await (0, core_1.getAgentPrompt)('coverage-map', undefined, tenantId) ?? SYSTEM_DEFAULT;
74
+ const user = `Test cases:\n${testCasesText}\n\nExtracted test names:\n${extractedNamesText}\n\n` +
75
+ 'Return a JSON array where each element has: { "testCaseId": string, "matchedName": string | null, "confidence": number }. ' +
76
+ 'confidence is 0.0–1.0. Use null for matchedName if no reasonable match exists.';
77
+ let parsed = [];
78
+ try {
79
+ const seed = (0, llm_js_1.deterministicSeed)(`${tenantId}:cov-map:${batch.map((tc) => tc.id).join('|')}`);
80
+ const res = await (0, llm_js_1.askTracked)(system, user, llmCfg, 2048, undefined, { temperature: 0, seed });
81
+ const raw = res.text.trim();
82
+ const jsonMatch = raw.match(/\[[\s\S]*\]/);
83
+ if (jsonMatch) {
84
+ parsed = JSON.parse(jsonMatch[0]);
85
+ }
86
+ }
87
+ catch {
88
+ // If AI fails, leave batch unmatched
89
+ }
90
+ for (const item of parsed) {
91
+ if (!item.matchedName || item.confidence < 0.70) {
92
+ results.set(item.testCaseId, makeUnmapped(item.testCaseId));
93
+ continue;
94
+ }
95
+ const et = pool.find((e) => e.name === item.matchedName);
96
+ if (!et) {
97
+ results.set(item.testCaseId, makeUnmapped(item.testCaseId));
98
+ continue;
99
+ }
100
+ const confidence = Math.min(item.confidence, 0.94); // AI matches cap at AMBIGUOUS
101
+ results.set(item.testCaseId, {
102
+ testCaseId: item.testCaseId,
103
+ status: 'AMBIGUOUS',
104
+ matchedFilePath: et.filePath,
105
+ scriptTestName: et.name,
106
+ lineNumber: et.lineNumber,
107
+ confidence,
108
+ matchMethod: 'AI',
109
+ });
110
+ }
111
+ // Mark any not returned by AI as UNMAPPED
112
+ for (const tc of batch) {
113
+ if (!results.has(tc.id)) {
114
+ results.set(tc.id, makeUnmapped(tc.id));
115
+ }
116
+ }
117
+ }
118
+ return results;
119
+ }
120
+ async function buildCoverageMap(testCases, extractedTests, tenantId) {
121
+ const finalResults = new Map();
122
+ // Separate dynamic tests — skip all passes
123
+ const dynamic = testCases.filter((tc) => {
124
+ const et = extractedTests.find((e) => e.name.trim() === tc.title.trim());
125
+ return et?.dynamic === true;
126
+ });
127
+ for (const tc of dynamic) {
128
+ finalResults.set(tc.id, makeUnmapped(tc.id));
129
+ }
130
+ let remaining = testCases.filter((tc) => !finalResults.has(tc.id));
131
+ // Pool is mutable — remove used extracted tests after each pass
132
+ const usedExtracted = new Set();
133
+ const availablePool = () => extractedTests.filter((et) => !usedExtracted.has(et.name) && et.dynamic !== true);
134
+ // Ref-based matching (before Pass 1)
135
+ const afterRef = [];
136
+ for (const tc of remaining) {
137
+ if (!tc.ref) {
138
+ afterRef.push(tc);
139
+ continue;
140
+ }
141
+ const pool = availablePool();
142
+ const match = pool.find((et) => et.name.includes(tc.ref));
143
+ if (match) {
144
+ finalResults.set(tc.id, makeMapped(tc.id, match, 1.0, 'EXACT'));
145
+ usedExtracted.add(match.name);
146
+ }
147
+ else {
148
+ afterRef.push(tc);
149
+ }
150
+ }
151
+ remaining = afterRef;
152
+ // Pass 1 — EXACT
153
+ const afterPass1 = [];
154
+ for (const tc of remaining) {
155
+ const pool = availablePool();
156
+ const match = pool.find((et) => et.name.trim() === tc.title.trim());
157
+ if (match) {
158
+ finalResults.set(tc.id, makeMapped(tc.id, match, 1.0, 'EXACT'));
159
+ usedExtracted.add(match.name);
160
+ }
161
+ else {
162
+ afterPass1.push(tc);
163
+ }
164
+ }
165
+ remaining = afterPass1;
166
+ // Pass 2 — NORMALIZED
167
+ const afterPass2 = [];
168
+ for (const tc of remaining) {
169
+ const normTitle = normalize(tc.title);
170
+ const pool = availablePool();
171
+ const match = pool.find((et) => et.normalizedName === normTitle);
172
+ if (match) {
173
+ finalResults.set(tc.id, makeMapped(tc.id, match, 0.90, 'NORMALIZED'));
174
+ usedExtracted.add(match.name);
175
+ }
176
+ else {
177
+ afterPass2.push(tc);
178
+ }
179
+ }
180
+ remaining = afterPass2;
181
+ // Pass 3 — FUZZY (Levenshtein, threshold ≥ 0.85)
182
+ const afterPass3 = [];
183
+ for (const tc of remaining) {
184
+ const normTitle = normalize(tc.title);
185
+ const pool = availablePool();
186
+ let best = null;
187
+ let secondBest = null;
188
+ for (const et of pool) {
189
+ const score = similarity(normTitle, et.normalizedName ?? normalize(et.name));
190
+ if (score >= 0.85) {
191
+ if (!best || score > best.score) {
192
+ secondBest = best;
193
+ best = { et, score };
194
+ }
195
+ else if (!secondBest || score > secondBest.score) {
196
+ secondBest = { et, score };
197
+ }
198
+ }
199
+ }
200
+ if (!best) {
201
+ afterPass3.push(tc);
202
+ continue;
203
+ }
204
+ // Ambiguous if two candidates are within 0.02 of each other
205
+ if (secondBest && best.score - secondBest.score <= 0.02) {
206
+ // Neither matched — leave for AI pass
207
+ afterPass3.push(tc);
208
+ continue;
209
+ }
210
+ finalResults.set(tc.id, makeMapped(tc.id, best.et, best.score, 'FUZZY'));
211
+ usedExtracted.add(best.et.name);
212
+ }
213
+ remaining = afterPass3;
214
+ // Pass 3.5 — SEMANTIC (cross-encoder for paraphrases that Levenshtein misses)
215
+ if (remaining.length > 0) {
216
+ const semanticPool = availablePool();
217
+ if (semanticPool.length > 0) {
218
+ const poolTexts = semanticPool.map((et) => et.normalizedName ?? normalize(et.name));
219
+ const afterSemantic = [];
220
+ for (const tc of remaining) {
221
+ try {
222
+ const ranked = await (0, app_brain_1.rerankStrings)(normalize(tc.title), poolTexts, 1, 0);
223
+ const top = ranked[0];
224
+ if (top && top.score >= 0.65) {
225
+ const et = semanticPool[top.index];
226
+ if (et && !usedExtracted.has(et.name)) {
227
+ finalResults.set(tc.id, makeMapped(tc.id, et, Math.min(top.score, 0.89), 'FUZZY'));
228
+ usedExtracted.add(et.name);
229
+ continue;
230
+ }
231
+ }
232
+ }
233
+ catch {
234
+ // reranker unavailable — fall through to AI pass
235
+ }
236
+ afterSemantic.push(tc);
237
+ }
238
+ remaining = afterSemantic;
239
+ }
240
+ }
241
+ // Pass 4 — AI
242
+ if (remaining.length > 0) {
243
+ const aiResults = await runAIPass(remaining, availablePool(), tenantId);
244
+ for (const [id, result] of aiResults) {
245
+ finalResults.set(id, result);
246
+ }
247
+ }
248
+ // Ensure every test case has a result
249
+ for (const tc of testCases) {
250
+ if (!finalResults.has(tc.id)) {
251
+ finalResults.set(tc.id, makeUnmapped(tc.id));
252
+ }
253
+ }
254
+ return testCases.map((tc) => finalResults.get(tc.id));
255
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * ZeTa-AI Crawl Diff Agent
3
+ *
4
+ * Compares DOM elements and navigation flows across two crawl runs to detect
5
+ * structural UI changes. Drives the approval gate state machine that gates
6
+ * script generation until all diffs are reviewed.
7
+ *
8
+ * Public entry points:
9
+ * computeCrawlDiffs — diff elements + flows between two crawl jobs
10
+ * createCrawlContextEnhancement — create ContextEnhancement record for a crawl job
11
+ * approveCrawlDiff — mark a diff approved, flag affected test cases suspect
12
+ * recomputeApprovalGateSummary — recompute contextPhase based on pending diffs
13
+ * computeCoverageGaps — find UI elements not covered by any test case
14
+ */
15
+ export declare function computeCrawlDiffs(tenantId: string, projectId: string, fromJobId: string, toJobId: string): Promise<{
16
+ diffCount: number;
17
+ criticalCount: number;
18
+ significantCount: number;
19
+ }>;
20
+ export declare function createCrawlContextEnhancement(tenantId: string, projectId: string, toJobId: string, diffs: Array<{
21
+ severity: string;
22
+ }>): Promise<any>;
23
+ export declare function approveCrawlDiff(tenantId: string, projectId: string, diffId: string, reviewedBy?: string): Promise<void>;
24
+ export declare function recomputeApprovalGateSummary(tenantId: string, projectId: string): Promise<object>;
25
+ export declare function computeCoverageGaps(tenantId: string, projectId: string): Promise<{
26
+ gapsFound: number;
27
+ }>;