@yasserkhanorg/e2e-agents 1.8.5 → 1.10.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.
- package/README.md +95 -8
- package/dist/adapters/cypress.d.ts +10 -0
- package/dist/adapters/cypress.d.ts.map +1 -0
- package/dist/adapters/cypress.js +86 -0
- package/dist/adapters/framework_adapter.d.ts +41 -0
- package/dist/adapters/framework_adapter.d.ts.map +1 -0
- package/dist/adapters/framework_adapter.js +152 -0
- package/dist/adapters/playwright.d.ts +10 -0
- package/dist/adapters/playwright.d.ts.map +1 -0
- package/dist/adapters/playwright.js +86 -0
- package/dist/adapters/pytest.d.ts +10 -0
- package/dist/adapters/pytest.d.ts.map +1 -0
- package/dist/adapters/pytest.js +96 -0
- package/dist/adapters/supertest.d.ts +12 -0
- package/dist/adapters/supertest.d.ts.map +1 -0
- package/dist/adapters/supertest.js +85 -0
- package/dist/agent/config.d.ts +1 -1
- package/dist/agent/config.d.ts.map +1 -1
- package/dist/agent/git.d.ts +1 -0
- package/dist/agent/git.d.ts.map +1 -1
- package/dist/agent/git.js +3 -0
- package/dist/agentic/fix_loop.d.ts.map +1 -1
- package/dist/agentic/fix_loop.js +5 -4
- package/dist/agentic/runner.d.ts +2 -0
- package/dist/agentic/runner.d.ts.map +1 -1
- package/dist/agentic/runner.js +15 -12
- package/dist/agents/cross-impact.d.ts.map +1 -1
- package/dist/agents/cross-impact.js +6 -1
- package/dist/agents/executor.d.ts.map +1 -1
- package/dist/agents/executor.js +6 -1
- package/dist/agents/strategist.d.ts.map +1 -1
- package/dist/agents/strategist.js +6 -1
- package/dist/agents/test-designer.d.ts.map +1 -1
- package/dist/agents/test-designer.js +6 -1
- package/dist/anthropic_provider.d.ts.map +1 -1
- package/dist/anthropic_provider.js +1 -0
- package/dist/base_provider.d.ts +56 -0
- package/dist/base_provider.d.ts.map +1 -1
- package/dist/base_provider.js +123 -1
- package/dist/budget_ledger.d.ts +28 -0
- package/dist/budget_ledger.d.ts.map +1 -0
- package/dist/budget_ledger.js +62 -0
- package/dist/cache/cached_provider.d.ts +45 -0
- package/dist/cache/cached_provider.d.ts.map +1 -0
- package/dist/cache/cached_provider.js +88 -0
- package/dist/cache/response_cache.d.ts +79 -0
- package/dist/cache/response_cache.d.ts.map +1 -0
- package/dist/cache/response_cache.js +177 -0
- package/dist/cli/commands/bootstrap.d.ts +3 -0
- package/dist/cli/commands/bootstrap.d.ts.map +1 -0
- package/dist/cli/commands/bootstrap.js +109 -0
- package/dist/cli/commands/cost_report.d.ts +3 -0
- package/dist/cli/commands/cost_report.d.ts.map +1 -0
- package/dist/cli/commands/cost_report.js +115 -0
- package/dist/cli/commands/crew.d.ts.map +1 -1
- package/dist/cli/commands/crew.js +118 -1
- package/dist/cli/commands/gate.d.ts +3 -0
- package/dist/cli/commands/gate.d.ts.map +1 -0
- package/dist/cli/commands/gate.js +86 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +7 -62
- package/dist/cli/commands/train.d.ts.map +1 -1
- package/dist/cli/commands/train.js +16 -21
- package/dist/cli/defaults.d.ts +35 -0
- package/dist/cli/defaults.d.ts.map +1 -0
- package/dist/cli/defaults.js +125 -0
- package/dist/cli/errors.d.ts +27 -0
- package/dist/cli/errors.d.ts.map +1 -0
- package/dist/cli/errors.js +57 -0
- package/dist/cli/parse_args.d.ts.map +1 -1
- package/dist/cli/parse_args.js +24 -2
- package/dist/cli/types.d.ts +7 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli.js +47 -2
- package/dist/crew/context.d.ts +15 -0
- package/dist/crew/context.d.ts.map +1 -1
- package/dist/crew/orchestrator.d.ts +14 -0
- package/dist/crew/orchestrator.d.ts.map +1 -1
- package/dist/crew/orchestrator.js +162 -4
- package/dist/crew/protocol.d.ts +13 -0
- package/dist/crew/protocol.d.ts.map +1 -1
- package/dist/crew/provider.d.ts +15 -1
- package/dist/crew/provider.d.ts.map +1 -1
- package/dist/crew/provider.js +24 -4
- package/dist/custom_provider.d.ts.map +1 -1
- package/dist/custom_provider.js +1 -0
- package/dist/engine/diff_loader.d.ts.map +1 -1
- package/dist/engine/diff_loader.js +3 -14
- package/dist/engine/impact_engine.d.ts.map +1 -1
- package/dist/engine/impact_engine.js +9 -23
- package/dist/esm/adapters/cypress.js +49 -0
- package/dist/esm/adapters/framework_adapter.js +114 -0
- package/dist/esm/adapters/playwright.js +49 -0
- package/dist/esm/adapters/pytest.js +59 -0
- package/dist/esm/adapters/supertest.js +48 -0
- package/dist/esm/agent/git.js +3 -1
- package/dist/esm/agentic/fix_loop.js +5 -4
- package/dist/esm/agentic/runner.js +15 -12
- package/dist/esm/agents/cross-impact.js +6 -1
- package/dist/esm/agents/executor.js +6 -1
- package/dist/esm/agents/strategist.js +6 -1
- package/dist/esm/agents/test-designer.js +6 -1
- package/dist/esm/anthropic_provider.js +1 -0
- package/dist/esm/base_provider.js +121 -0
- package/dist/esm/budget_ledger.js +58 -0
- package/dist/esm/cache/cached_provider.js +82 -0
- package/dist/esm/cache/response_cache.js +140 -0
- package/dist/esm/cli/commands/bootstrap.js +106 -0
- package/dist/esm/cli/commands/cost_report.js +112 -0
- package/dist/esm/cli/commands/crew.js +118 -1
- package/dist/esm/cli/commands/gate.js +83 -0
- package/dist/esm/cli/commands/init.js +3 -58
- package/dist/esm/cli/commands/train.js +16 -21
- package/dist/esm/cli/defaults.js +118 -0
- package/dist/esm/cli/errors.js +52 -0
- package/dist/esm/cli/parse_args.js +24 -2
- package/dist/esm/cli.js +47 -2
- package/dist/esm/crew/orchestrator.js +162 -4
- package/dist/esm/crew/provider.js +24 -4
- package/dist/esm/custom_provider.js +1 -0
- package/dist/esm/engine/diff_loader.js +1 -12
- package/dist/esm/engine/impact_engine.js +9 -23
- package/dist/esm/index.js +21 -0
- package/dist/esm/knowledge/api_surface.js +265 -34
- package/dist/esm/knowledge/cluster_utils.js +60 -0
- package/dist/esm/knowledge/failure_history.js +121 -0
- package/dist/esm/knowledge/kg_bridge.js +381 -0
- package/dist/esm/knowledge/kg_types.js +3 -0
- package/dist/esm/knowledge/route_families.js +119 -0
- package/dist/esm/mcp-server.js +2 -4
- package/dist/esm/metrics/prometheus.js +149 -0
- package/dist/esm/model_router.js +59 -0
- package/dist/esm/ollama_provider.js +1 -0
- package/dist/esm/openai_provider.js +1 -0
- package/dist/esm/pipeline/orchestrator.js +6 -12
- package/dist/esm/pipeline/stage0_preprocess.js +12 -19
- package/dist/esm/pipeline/stage1_impact.js +19 -3
- package/dist/esm/pipeline/stage2_coverage.js +29 -7
- package/dist/esm/pipeline/stage3_generation.js +21 -1
- package/dist/esm/progress.js +112 -0
- package/dist/esm/prompts/coverage.js +17 -24
- package/dist/esm/prompts/cross-impact.js +3 -21
- package/dist/esm/prompts/generation.js +201 -45
- package/dist/esm/prompts/generation_profile.js +147 -0
- package/dist/esm/prompts/heal.js +33 -15
- package/dist/esm/prompts/impact.js +3 -22
- package/dist/esm/prompts/json_extract.js +36 -0
- package/dist/esm/prompts/strategist.js +2 -20
- package/dist/esm/prompts/test-designer.js +6 -21
- package/dist/esm/provider_factory.js +6 -4
- package/dist/esm/reporters/junit.js +86 -0
- package/dist/esm/reporters/reporter.js +3 -0
- package/dist/esm/reporters/sarif.js +131 -0
- package/dist/esm/resilience/circuit_breaker.js +78 -0
- package/dist/esm/resilience/retry.js +56 -0
- package/dist/esm/sanitize.js +66 -0
- package/dist/esm/training/kg_scanner.js +115 -0
- package/dist/esm/training/scanner.js +27 -34
- package/dist/esm/validation/guardrails.js +5 -0
- package/dist/esm/version.js +33 -0
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -1
- package/dist/knowledge/api_surface.d.ts +12 -0
- package/dist/knowledge/api_surface.d.ts.map +1 -1
- package/dist/knowledge/api_surface.js +268 -34
- package/dist/knowledge/cluster_utils.d.ts +28 -0
- package/dist/knowledge/cluster_utils.d.ts.map +1 -0
- package/dist/knowledge/cluster_utils.js +67 -0
- package/dist/knowledge/failure_history.d.ts +39 -0
- package/dist/knowledge/failure_history.d.ts.map +1 -0
- package/dist/knowledge/failure_history.js +128 -0
- package/dist/knowledge/kg_bridge.d.ts +31 -0
- package/dist/knowledge/kg_bridge.d.ts.map +1 -0
- package/dist/knowledge/kg_bridge.js +388 -0
- package/dist/knowledge/kg_types.d.ts +75 -0
- package/dist/knowledge/kg_types.d.ts.map +1 -0
- package/dist/knowledge/kg_types.js +4 -0
- package/dist/knowledge/route_families.d.ts +29 -0
- package/dist/knowledge/route_families.d.ts.map +1 -1
- package/dist/knowledge/route_families.js +122 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +2 -4
- package/dist/metrics/prometheus.d.ts +37 -0
- package/dist/metrics/prometheus.d.ts.map +1 -0
- package/dist/metrics/prometheus.js +153 -0
- package/dist/model_router.d.ts +28 -0
- package/dist/model_router.d.ts.map +1 -0
- package/dist/model_router.js +63 -0
- package/dist/ollama_provider.d.ts.map +1 -1
- package/dist/ollama_provider.js +1 -0
- package/dist/openai_provider.d.ts.map +1 -1
- package/dist/openai_provider.js +1 -0
- package/dist/pipeline/orchestrator.d.ts +2 -0
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +6 -12
- package/dist/pipeline/stage0_preprocess.d.ts.map +1 -1
- package/dist/pipeline/stage0_preprocess.js +11 -18
- package/dist/pipeline/stage1_impact.d.ts +1 -1
- package/dist/pipeline/stage1_impact.d.ts.map +1 -1
- package/dist/pipeline/stage1_impact.js +18 -2
- package/dist/pipeline/stage2_coverage.d.ts +2 -0
- package/dist/pipeline/stage2_coverage.d.ts.map +1 -1
- package/dist/pipeline/stage2_coverage.js +29 -7
- package/dist/pipeline/stage3_generation.d.ts +2 -0
- package/dist/pipeline/stage3_generation.d.ts.map +1 -1
- package/dist/pipeline/stage3_generation.js +21 -1
- package/dist/pipeline/stage4_heal.d.ts +2 -0
- package/dist/pipeline/stage4_heal.d.ts.map +1 -1
- package/dist/progress.d.ts +22 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +116 -0
- package/dist/prompts/coverage.d.ts +2 -0
- package/dist/prompts/coverage.d.ts.map +1 -1
- package/dist/prompts/coverage.js +17 -24
- package/dist/prompts/cross-impact.d.ts +1 -0
- package/dist/prompts/cross-impact.d.ts.map +1 -1
- package/dist/prompts/cross-impact.js +3 -21
- package/dist/prompts/generation.d.ts +4 -2
- package/dist/prompts/generation.d.ts.map +1 -1
- package/dist/prompts/generation.js +201 -45
- package/dist/prompts/generation_profile.d.ts +29 -0
- package/dist/prompts/generation_profile.d.ts.map +1 -0
- package/dist/prompts/generation_profile.js +151 -0
- package/dist/prompts/heal.d.ts +3 -1
- package/dist/prompts/heal.d.ts.map +1 -1
- package/dist/prompts/heal.js +33 -15
- package/dist/prompts/impact.d.ts +1 -0
- package/dist/prompts/impact.d.ts.map +1 -1
- package/dist/prompts/impact.js +3 -22
- package/dist/prompts/json_extract.d.ts +14 -0
- package/dist/prompts/json_extract.d.ts.map +1 -0
- package/dist/prompts/json_extract.js +39 -0
- package/dist/prompts/strategist.d.ts.map +1 -1
- package/dist/prompts/strategist.js +2 -20
- package/dist/prompts/test-designer.d.ts +2 -0
- package/dist/prompts/test-designer.d.ts.map +1 -1
- package/dist/prompts/test-designer.js +6 -21
- package/dist/provider_factory.d.ts.map +1 -1
- package/dist/provider_factory.js +6 -4
- package/dist/reporters/junit.d.ts +6 -0
- package/dist/reporters/junit.d.ts.map +1 -0
- package/dist/reporters/junit.js +89 -0
- package/dist/reporters/reporter.d.ts +42 -0
- package/dist/reporters/reporter.d.ts.map +1 -0
- package/dist/reporters/reporter.js +4 -0
- package/dist/reporters/sarif.d.ts +7 -0
- package/dist/reporters/sarif.d.ts.map +1 -0
- package/dist/reporters/sarif.js +134 -0
- package/dist/resilience/circuit_breaker.d.ts +36 -0
- package/dist/resilience/circuit_breaker.d.ts.map +1 -0
- package/dist/resilience/circuit_breaker.js +82 -0
- package/dist/resilience/retry.d.ts +11 -0
- package/dist/resilience/retry.d.ts.map +1 -0
- package/dist/resilience/retry.js +59 -0
- package/dist/sanitize.d.ts +15 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +71 -0
- package/dist/training/kg_scanner.d.ts +13 -0
- package/dist/training/kg_scanner.d.ts.map +1 -0
- package/dist/training/kg_scanner.js +118 -0
- package/dist/training/scanner.d.ts +7 -2
- package/dist/training/scanner.d.ts.map +1 -1
- package/dist/training/scanner.js +27 -34
- package/dist/validation/guardrails.d.ts +2 -0
- package/dist/validation/guardrails.d.ts.map +1 -1
- package/dist/validation/guardrails.js +5 -0
- package/dist/validation/output_schema.d.ts +3 -0
- package/dist/validation/output_schema.d.ts.map +1 -1
- package/dist/version.d.ts +6 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +36 -0
- package/package.json +7 -2
- package/schemas/route-families.schema.json +31 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
/**
|
|
4
|
+
* Tracks historical test failure correlations: which tests fail when certain files change.
|
|
5
|
+
* Used to boost confidence in impact analysis — if a file change historically breaks a test,
|
|
6
|
+
* future changes to that file should prioritize that test.
|
|
7
|
+
*
|
|
8
|
+
* Data is stored as a JSON file at .e2e-ai-agents/failure-history.json.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
11
|
+
import { join, dirname } from 'path';
|
|
12
|
+
const DEFAULT_HISTORY = {
|
|
13
|
+
correlations: [],
|
|
14
|
+
totalRuns: 0,
|
|
15
|
+
updatedAt: new Date().toISOString(),
|
|
16
|
+
};
|
|
17
|
+
export function loadFailureHistory(testsRoot) {
|
|
18
|
+
const historyPath = join(testsRoot, '.e2e-ai-agents', 'failure-history.json');
|
|
19
|
+
if (!existsSync(historyPath)) {
|
|
20
|
+
return { ...DEFAULT_HISTORY };
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const raw = JSON.parse(readFileSync(historyPath, 'utf-8'));
|
|
24
|
+
if (!Array.isArray(raw.correlations)) {
|
|
25
|
+
return { ...DEFAULT_HISTORY };
|
|
26
|
+
}
|
|
27
|
+
return raw;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { ...DEFAULT_HISTORY };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function saveFailureHistory(testsRoot, history) {
|
|
34
|
+
const historyPath = join(testsRoot, '.e2e-ai-agents', 'failure-history.json');
|
|
35
|
+
try {
|
|
36
|
+
const dir = dirname(historyPath);
|
|
37
|
+
if (!existsSync(dir)) {
|
|
38
|
+
mkdirSync(dir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
history.updatedAt = new Date().toISOString();
|
|
41
|
+
writeFileSync(historyPath, JSON.stringify(history, null, 2), 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Non-fatal — history is advisory, not required
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Record that a set of changed files caused a set of spec failures.
|
|
49
|
+
* Call this after a test run where failures were observed.
|
|
50
|
+
*/
|
|
51
|
+
export function recordFailures(history, changedFiles, failedSpecs) {
|
|
52
|
+
const now = new Date().toISOString();
|
|
53
|
+
const updated = { ...history, totalRuns: history.totalRuns + 1, correlations: [...history.correlations] };
|
|
54
|
+
for (const changedFile of changedFiles) {
|
|
55
|
+
for (const specFile of failedSpecs) {
|
|
56
|
+
const existing = updated.correlations.find((c) => c.changedFile === changedFile && c.specFile === specFile);
|
|
57
|
+
if (existing) {
|
|
58
|
+
existing.count++;
|
|
59
|
+
existing.lastSeen = now;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
updated.correlations.push({
|
|
63
|
+
changedFile,
|
|
64
|
+
specFile,
|
|
65
|
+
count: 1,
|
|
66
|
+
lastSeen: now,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Prune stale correlations (not seen in 90 days)
|
|
72
|
+
const cutoff = new Date();
|
|
73
|
+
cutoff.setDate(cutoff.getDate() - 90);
|
|
74
|
+
const cutoffStr = cutoff.toISOString();
|
|
75
|
+
updated.correlations = updated.correlations.filter((c) => c.lastSeen >= cutoffStr);
|
|
76
|
+
return updated;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get a confidence boost (0-20) for a file based on historical failure patterns.
|
|
80
|
+
* A file that historically causes test failures gets a higher confidence boost
|
|
81
|
+
* when detected as impacted, meaning the system is more confident it needs testing.
|
|
82
|
+
*/
|
|
83
|
+
export function getConfidenceBoost(history, changedFile) {
|
|
84
|
+
const correlations = history.correlations.filter((c) => c.changedFile === changedFile);
|
|
85
|
+
if (correlations.length === 0) {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
// More correlations and higher counts = more confidence
|
|
89
|
+
const totalCount = correlations.reduce((sum, c) => sum + c.count, 0);
|
|
90
|
+
const uniqueSpecs = correlations.length;
|
|
91
|
+
// Scale: 1 correlation = +5, 3+ = +10, 5+ with high counts = +15, max +20
|
|
92
|
+
if (totalCount >= 10 && uniqueSpecs >= 5)
|
|
93
|
+
return 20;
|
|
94
|
+
if (totalCount >= 5 && uniqueSpecs >= 3)
|
|
95
|
+
return 15;
|
|
96
|
+
if (totalCount >= 3)
|
|
97
|
+
return 10;
|
|
98
|
+
return 5;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get the most likely failing specs for a set of changed files, based on history.
|
|
102
|
+
* Returns specs sorted by correlation strength (count * recency).
|
|
103
|
+
*/
|
|
104
|
+
export function getPredictedFailures(history, changedFiles, limit = 10) {
|
|
105
|
+
const specScores = new Map();
|
|
106
|
+
for (const changedFile of changedFiles) {
|
|
107
|
+
for (const c of history.correlations) {
|
|
108
|
+
if (c.changedFile !== changedFile)
|
|
109
|
+
continue;
|
|
110
|
+
// Score: count weighted by recency (days since last seen)
|
|
111
|
+
const daysSince = (Date.now() - new Date(c.lastSeen).getTime()) / (1000 * 60 * 60 * 24);
|
|
112
|
+
const recencyWeight = Math.max(0.1, 1 - daysSince / 90);
|
|
113
|
+
const score = c.count * recencyWeight;
|
|
114
|
+
specScores.set(c.specFile, (specScores.get(c.specFile) || 0) + score);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return Array.from(specScores.entries())
|
|
118
|
+
.map(([specFile, score]) => ({ specFile, score }))
|
|
119
|
+
.sort((a, b) => b.score - a.score)
|
|
120
|
+
.slice(0, limit);
|
|
121
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
/**
|
|
4
|
+
* Bridge between Understand-Anything's knowledge graph and e2e-agents' route families.
|
|
5
|
+
* Transforms KG nodes/edges into RouteFamilyManifest so existing pipeline works unchanged.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { logger } from '../logger.js';
|
|
10
|
+
import { deriveClusterId, deriveClusterIdFromPath } from './cluster_utils.js';
|
|
11
|
+
const UA_DIR = '.understand-anything';
|
|
12
|
+
const KG_FILE = 'knowledge-graph.json';
|
|
13
|
+
const DIFF_FILE = 'diff-overlay.json';
|
|
14
|
+
const FRONTEND_FRAMEWORKS = new Set([
|
|
15
|
+
'react', 'vue', 'angular', 'svelte', 'next', 'nextjs', 'next.js',
|
|
16
|
+
'nuxt', 'nuxtjs', 'gatsby', 'remix', 'astro', 'solid', 'solidjs',
|
|
17
|
+
'preact', 'lit', 'stencil', 'qwik',
|
|
18
|
+
]);
|
|
19
|
+
const BACKEND_FRAMEWORKS = new Set([
|
|
20
|
+
'express', 'fastify', 'koa', 'hapi', 'nest', 'nestjs',
|
|
21
|
+
'django', 'flask', 'fastapi', 'rails', 'spring', 'gin', 'echo', 'fiber',
|
|
22
|
+
'actix', 'axum', 'rocket', 'phoenix', 'laravel',
|
|
23
|
+
]);
|
|
24
|
+
/**
|
|
25
|
+
* Loads the knowledge graph from .understand-anything/knowledge-graph.json
|
|
26
|
+
* or from a custom path when provided.
|
|
27
|
+
*/
|
|
28
|
+
export function loadKnowledgeGraph(projectRoot, customPath) {
|
|
29
|
+
const kgPath = customPath || join(projectRoot, UA_DIR, KG_FILE);
|
|
30
|
+
if (!existsSync(kgPath)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const raw = JSON.parse(readFileSync(kgPath, 'utf-8'));
|
|
35
|
+
if (!raw.nodes || !Array.isArray(raw.nodes) || !raw.edges || !Array.isArray(raw.edges)) {
|
|
36
|
+
logger.warn('Knowledge graph missing required nodes/edges arrays');
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (!raw.project) {
|
|
40
|
+
logger.warn('Knowledge graph missing project metadata');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
// Field-level validation: filter out invalid nodes rather than rejecting the whole graph
|
|
44
|
+
const MAX_STRING_LEN = 1000;
|
|
45
|
+
const validNodes = raw.nodes.filter((node) => {
|
|
46
|
+
if (typeof node.id !== 'string' || typeof node.name !== 'string') {
|
|
47
|
+
logger.warn(`Dropping KG node with missing/invalid id or name: ${JSON.stringify(node).slice(0, 200)}`);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (node.filePath !== undefined) {
|
|
51
|
+
if (typeof node.filePath !== 'string') {
|
|
52
|
+
logger.warn(`Dropping KG node "${node.id}": filePath is not a string`);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (node.filePath.startsWith('/')) {
|
|
56
|
+
logger.warn(`Dropping KG node "${node.id}": absolute filePath rejected`);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (node.filePath.includes('..')) {
|
|
60
|
+
logger.warn(`Dropping KG node "${node.id}": path traversal in filePath rejected`);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (node.filePath.includes('\0')) {
|
|
64
|
+
logger.warn(`Dropping KG node "${node.id}": null byte in filePath rejected`);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
// Truncate excessively long strings
|
|
71
|
+
for (const node of validNodes) {
|
|
72
|
+
if (node.name.length > MAX_STRING_LEN) {
|
|
73
|
+
node.name = node.name.slice(0, MAX_STRING_LEN);
|
|
74
|
+
}
|
|
75
|
+
if (node.description && node.description.length > MAX_STRING_LEN) {
|
|
76
|
+
node.description = node.description.slice(0, MAX_STRING_LEN);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
raw.nodes = validNodes;
|
|
80
|
+
return raw;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
logger.warn(`Failed to load knowledge graph: ${error instanceof Error ? error.message : String(error)}`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Classifies project type based on KG framework metadata.
|
|
89
|
+
*/
|
|
90
|
+
export function classifyProjectType(kg) {
|
|
91
|
+
const frameworks = kg.project.frameworks.map((f) => f.toLowerCase());
|
|
92
|
+
const hasFrontend = frameworks.some((f) => FRONTEND_FRAMEWORKS.has(f));
|
|
93
|
+
const hasBackend = frameworks.some((f) => BACKEND_FRAMEWORKS.has(f));
|
|
94
|
+
if (hasFrontend && hasBackend)
|
|
95
|
+
return 'fullstack';
|
|
96
|
+
if (hasBackend)
|
|
97
|
+
return 'backend';
|
|
98
|
+
return 'frontend';
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Core bridge: transforms KG into a RouteFamilyManifest.
|
|
102
|
+
*
|
|
103
|
+
* Strategy:
|
|
104
|
+
* - Frontend: cluster UI-layer nodes into families by module/component groups
|
|
105
|
+
* - Backend: cluster API-layer nodes, follow calls edges into Service→Data layers
|
|
106
|
+
* - Priority: P0 = high fan-in nodes, P1 = moderate, P2 = leaf/utility
|
|
107
|
+
* - userFlows: derived from KG tour steps referencing family nodes
|
|
108
|
+
*/
|
|
109
|
+
export function transformKGToFamilies(kg) {
|
|
110
|
+
const projectType = classifyProjectType(kg);
|
|
111
|
+
const nodeMap = new Map(kg.nodes.map((n) => [n.id, n]));
|
|
112
|
+
const edgesByTarget = groupEdges(kg.edges, 'target');
|
|
113
|
+
// Build clusters based on project type
|
|
114
|
+
const clusters = buildClusters(kg, projectType, nodeMap);
|
|
115
|
+
// Transform clusters into route families
|
|
116
|
+
const families = [];
|
|
117
|
+
for (const [clusterId, nodeIds] of clusters) {
|
|
118
|
+
const nodes = nodeIds.map((id) => nodeMap.get(id)).filter((n) => n !== undefined);
|
|
119
|
+
if (nodes.length === 0)
|
|
120
|
+
continue;
|
|
121
|
+
const family = buildFamilyFromCluster(clusterId, nodes, projectType, nodeMap, edgesByTarget);
|
|
122
|
+
if (family) {
|
|
123
|
+
families.push(family);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Derive userFlows from KG tour steps
|
|
127
|
+
if (kg.tour && kg.tour.length > 0) {
|
|
128
|
+
assignTourFlows(families, kg.tour, nodeMap);
|
|
129
|
+
}
|
|
130
|
+
// Sort by priority (P0 first) then alphabetically
|
|
131
|
+
families.sort((a, b) => {
|
|
132
|
+
const pOrder = { P0: 0, P1: 1, P2: 2 };
|
|
133
|
+
const pa = pOrder[a.priority || 'P2'];
|
|
134
|
+
const pb = pOrder[b.priority || 'P2'];
|
|
135
|
+
if (pa !== pb)
|
|
136
|
+
return pa - pb;
|
|
137
|
+
return a.id.localeCompare(b.id);
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
families,
|
|
141
|
+
source: 'knowledge-graph',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Loads the diff overlay from .understand-anything/diff-overlay.json
|
|
146
|
+
*/
|
|
147
|
+
export function loadDiffOverlay(projectRoot) {
|
|
148
|
+
const overlayPath = join(projectRoot, UA_DIR, DIFF_FILE);
|
|
149
|
+
if (!existsSync(overlayPath)) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const raw = JSON.parse(readFileSync(overlayPath, 'utf-8'));
|
|
154
|
+
if (!raw.changes || !Array.isArray(raw.changes)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return raw;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Maps diff overlay changes to file paths using KG node resolution.
|
|
165
|
+
*/
|
|
166
|
+
export function diffOverlayToChangedFiles(overlay, kg) {
|
|
167
|
+
const nodeMap = new Map(kg.nodes.map((n) => [n.id, n]));
|
|
168
|
+
const files = new Set();
|
|
169
|
+
for (const change of overlay.changes) {
|
|
170
|
+
// Use filePath from change if available
|
|
171
|
+
if (change.filePath) {
|
|
172
|
+
files.add(change.filePath);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// Fall back to KG node's filePath
|
|
176
|
+
const node = nodeMap.get(change.nodeId);
|
|
177
|
+
if (node?.filePath) {
|
|
178
|
+
files.add(node.filePath);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return [...files];
|
|
182
|
+
}
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Internal helpers
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
function groupEdges(edges, key) {
|
|
187
|
+
const map = new Map();
|
|
188
|
+
for (const edge of edges) {
|
|
189
|
+
const val = edge[key];
|
|
190
|
+
if (!map.has(val))
|
|
191
|
+
map.set(val, []);
|
|
192
|
+
map.get(val).push(edge);
|
|
193
|
+
}
|
|
194
|
+
return map;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Build clusters of related nodes to become families.
|
|
198
|
+
* Frontend: group by route/page/component modules
|
|
199
|
+
* Backend: group by API endpoint handlers + their service dependencies
|
|
200
|
+
*/
|
|
201
|
+
function buildClusters(kg, projectType, nodeMap) {
|
|
202
|
+
const clusters = new Map();
|
|
203
|
+
// Strategy 1: Use KG layers to find anchor nodes
|
|
204
|
+
const layerMap = new Map();
|
|
205
|
+
if (kg.layers) {
|
|
206
|
+
for (const layer of kg.layers) {
|
|
207
|
+
layerMap.set(layer.name, new Set(layer.nodeIds));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Strategy 2: Group by module/component/route nodes
|
|
211
|
+
const anchorKinds = projectType === 'backend'
|
|
212
|
+
? new Set(['module', 'route', 'class', 'function'])
|
|
213
|
+
: new Set(['component', 'route', 'module', 'class']);
|
|
214
|
+
const anchorLayers = projectType === 'backend'
|
|
215
|
+
? new Set(['api', 'service'])
|
|
216
|
+
: new Set(['ui']);
|
|
217
|
+
for (const node of kg.nodes) {
|
|
218
|
+
// Skip test/infra nodes as cluster anchors
|
|
219
|
+
if (node.layer === 'test' || node.layer === 'infra')
|
|
220
|
+
continue;
|
|
221
|
+
const isAnchorKind = anchorKinds.has(node.kind);
|
|
222
|
+
const isAnchorLayer = !node.layer || anchorLayers.has(node.layer);
|
|
223
|
+
if (!isAnchorKind || !isAnchorLayer)
|
|
224
|
+
continue;
|
|
225
|
+
// Derive cluster ID from node path or name
|
|
226
|
+
const clusterId = deriveClusterId(node);
|
|
227
|
+
if (!clusterId)
|
|
228
|
+
continue;
|
|
229
|
+
if (!clusters.has(clusterId)) {
|
|
230
|
+
clusters.set(clusterId, []);
|
|
231
|
+
}
|
|
232
|
+
clusters.get(clusterId).push(node.id);
|
|
233
|
+
}
|
|
234
|
+
// If no clusters found, fall back to file-based grouping
|
|
235
|
+
if (clusters.size === 0) {
|
|
236
|
+
for (const node of kg.nodes) {
|
|
237
|
+
if (!node.filePath || node.layer === 'test' || node.layer === 'infra')
|
|
238
|
+
continue;
|
|
239
|
+
const clusterId = deriveClusterIdFromPath(node.filePath);
|
|
240
|
+
if (!clusterId)
|
|
241
|
+
continue;
|
|
242
|
+
if (!clusters.has(clusterId)) {
|
|
243
|
+
clusters.set(clusterId, []);
|
|
244
|
+
}
|
|
245
|
+
clusters.get(clusterId).push(node.id);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return clusters;
|
|
249
|
+
}
|
|
250
|
+
// deriveClusterId and deriveClusterIdFromPath imported from cluster_utils.ts
|
|
251
|
+
function computePriority(nodes) {
|
|
252
|
+
const maxFanIn = Math.max(...nodes.map((n) => n.fanIn || 0));
|
|
253
|
+
if (maxFanIn >= 10)
|
|
254
|
+
return 'P0';
|
|
255
|
+
if (maxFanIn >= 4)
|
|
256
|
+
return 'P1';
|
|
257
|
+
return 'P2';
|
|
258
|
+
}
|
|
259
|
+
function buildFamilyFromCluster(clusterId, nodes, projectType, nodeMap, edgesByTarget) {
|
|
260
|
+
const webappPaths = new Set();
|
|
261
|
+
const serverPaths = new Set();
|
|
262
|
+
const routes = [];
|
|
263
|
+
const apiEndpoints = [];
|
|
264
|
+
for (const node of nodes) {
|
|
265
|
+
if (!node.filePath)
|
|
266
|
+
continue;
|
|
267
|
+
const layer = node.layer;
|
|
268
|
+
if (layer === 'api' || layer === 'service' || layer === 'data') {
|
|
269
|
+
serverPaths.add(`${node.filePath}*`);
|
|
270
|
+
}
|
|
271
|
+
else if (layer === 'ui') {
|
|
272
|
+
webappPaths.add(`${node.filePath}*`);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// Auto-assign based on project type
|
|
276
|
+
if (projectType === 'backend') {
|
|
277
|
+
serverPaths.add(`${node.filePath}*`);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
webappPaths.add(`${node.filePath}*`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Extract routes from route nodes
|
|
284
|
+
if (node.kind === 'route') {
|
|
285
|
+
const routePath = node.metadata?.path;
|
|
286
|
+
if (routePath) {
|
|
287
|
+
routes.push(routePath);
|
|
288
|
+
// Extract API endpoint info
|
|
289
|
+
const method = node.metadata?.method || 'GET';
|
|
290
|
+
apiEndpoints.push({ method: method.toUpperCase(), path: routePath, description: node.description });
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
routes.push(`/${clusterId}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// If no routes extracted, generate a default
|
|
298
|
+
if (routes.length === 0) {
|
|
299
|
+
routes.push(`/${clusterId}`);
|
|
300
|
+
}
|
|
301
|
+
// Collect related test file paths by following 'tests' edges
|
|
302
|
+
const specDirs = new Set();
|
|
303
|
+
for (const node of nodes) {
|
|
304
|
+
const testEdges = (edgesByTarget.get(node.id) || []).filter((e) => e.type === 'tests');
|
|
305
|
+
for (const edge of testEdges) {
|
|
306
|
+
const testNode = nodeMap.get(edge.source);
|
|
307
|
+
if (testNode?.filePath) {
|
|
308
|
+
// Use directory of test file
|
|
309
|
+
const dir = testNode.filePath.replace(/\\/g, '/').split('/').slice(0, -1).join('/');
|
|
310
|
+
if (dir)
|
|
311
|
+
specDirs.add(`${dir}/*`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const priority = computePriority(nodes);
|
|
316
|
+
// Determine test type based on content
|
|
317
|
+
let testType;
|
|
318
|
+
if (webappPaths.size > 0 && serverPaths.size > 0) {
|
|
319
|
+
testType = 'both';
|
|
320
|
+
}
|
|
321
|
+
else if (serverPaths.size > 0 && apiEndpoints.length > 0) {
|
|
322
|
+
testType = 'api';
|
|
323
|
+
}
|
|
324
|
+
else if (webappPaths.size > 0) {
|
|
325
|
+
testType = 'ui';
|
|
326
|
+
}
|
|
327
|
+
const family = {
|
|
328
|
+
id: clusterId,
|
|
329
|
+
routes: [...new Set(routes)],
|
|
330
|
+
priority,
|
|
331
|
+
};
|
|
332
|
+
if (webappPaths.size > 0)
|
|
333
|
+
family.webappPaths = [...webappPaths];
|
|
334
|
+
if (serverPaths.size > 0)
|
|
335
|
+
family.serverPaths = [...serverPaths];
|
|
336
|
+
if (specDirs.size > 0)
|
|
337
|
+
family.specDirs = [...specDirs];
|
|
338
|
+
if (apiEndpoints.length > 0)
|
|
339
|
+
family.apiEndpoints = apiEndpoints;
|
|
340
|
+
if (testType)
|
|
341
|
+
family.testType = testType;
|
|
342
|
+
return family;
|
|
343
|
+
}
|
|
344
|
+
function assignTourFlows(families, tour, nodeMap) {
|
|
345
|
+
if (!tour)
|
|
346
|
+
return;
|
|
347
|
+
// Build a map of nodeId → family for quick lookup
|
|
348
|
+
const nodeToFamily = new Map();
|
|
349
|
+
for (const family of families) {
|
|
350
|
+
// Resolve family nodes from paths
|
|
351
|
+
for (const [nodeId, node] of nodeMap) {
|
|
352
|
+
if (!node.filePath)
|
|
353
|
+
continue;
|
|
354
|
+
const matchesPaths = [
|
|
355
|
+
...(family.webappPaths || []),
|
|
356
|
+
...(family.serverPaths || []),
|
|
357
|
+
];
|
|
358
|
+
for (const pattern of matchesPaths) {
|
|
359
|
+
const prefix = pattern.replace(/\*$/, '');
|
|
360
|
+
if (node.filePath.startsWith(prefix)) {
|
|
361
|
+
nodeToFamily.set(nodeId, family);
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Assign tour steps as user flows
|
|
368
|
+
for (const step of tour.sort((a, b) => a.order - b.order)) {
|
|
369
|
+
for (const nodeId of step.nodeIds) {
|
|
370
|
+
const family = nodeToFamily.get(nodeId);
|
|
371
|
+
if (family) {
|
|
372
|
+
if (!family.userFlows)
|
|
373
|
+
family.userFlows = [];
|
|
374
|
+
const flowDesc = `${step.title}: ${step.description}`;
|
|
375
|
+
if (!family.userFlows.includes(flowDesc)) {
|
|
376
|
+
family.userFlows.push(flowDesc);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
@@ -30,6 +30,17 @@ export function matchesGlob(filePath, pattern) {
|
|
|
30
30
|
export function matchesAnyPattern(filePath, patterns) {
|
|
31
31
|
return patterns.some((pattern) => matchesGlob(filePath, pattern));
|
|
32
32
|
}
|
|
33
|
+
function validateApiEndpoint(ep) {
|
|
34
|
+
if (!ep || typeof ep !== 'object')
|
|
35
|
+
return null;
|
|
36
|
+
const obj = ep;
|
|
37
|
+
if (typeof obj.method !== 'string' || typeof obj.path !== 'string')
|
|
38
|
+
return null;
|
|
39
|
+
const result = { method: obj.method, path: obj.path };
|
|
40
|
+
if (typeof obj.description === 'string')
|
|
41
|
+
result.description = obj.description;
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
33
44
|
function validateFamily(family) {
|
|
34
45
|
if (!family || typeof family !== 'object') {
|
|
35
46
|
return null;
|
|
@@ -38,6 +49,10 @@ function validateFamily(family) {
|
|
|
38
49
|
if (typeof obj.id !== 'string' || !obj.id.trim()) {
|
|
39
50
|
return null;
|
|
40
51
|
}
|
|
52
|
+
// When testType is 'api', routes may contain API paths like "GET /api/users"
|
|
53
|
+
const testType = (obj.testType === 'ui' || obj.testType === 'api' || obj.testType === 'both')
|
|
54
|
+
? obj.testType
|
|
55
|
+
: undefined;
|
|
41
56
|
if (!Array.isArray(obj.routes) || obj.routes.length === 0) {
|
|
42
57
|
return null;
|
|
43
58
|
}
|
|
@@ -81,8 +96,32 @@ function validateFamily(family) {
|
|
|
81
96
|
.map((f) => validateFeature(f))
|
|
82
97
|
.filter((f) => f !== null);
|
|
83
98
|
}
|
|
99
|
+
if (Array.isArray(obj.apiEndpoints)) {
|
|
100
|
+
const endpoints = obj.apiEndpoints
|
|
101
|
+
.map((ep) => validateApiEndpoint(ep))
|
|
102
|
+
.filter((ep) => ep !== null);
|
|
103
|
+
if (endpoints.length > 0)
|
|
104
|
+
result.apiEndpoints = endpoints;
|
|
105
|
+
}
|
|
106
|
+
if (testType) {
|
|
107
|
+
result.testType = testType;
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(obj.assertionPatterns)) {
|
|
110
|
+
result.assertionPatterns = validateAssertionPatterns(obj.assertionPatterns);
|
|
111
|
+
}
|
|
84
112
|
return result;
|
|
85
113
|
}
|
|
114
|
+
const VALID_ASSERTION_TYPES = [
|
|
115
|
+
'state-change', 'cross-user', 'persistence', 'negative',
|
|
116
|
+
'permission', 'data-integrity', 'error-handling',
|
|
117
|
+
];
|
|
118
|
+
function validateAssertionPatterns(patterns) {
|
|
119
|
+
return patterns
|
|
120
|
+
.filter((p) => p != null && typeof p === 'object')
|
|
121
|
+
.filter((p) => typeof p.type === 'string' && typeof p.pattern === 'string')
|
|
122
|
+
.filter((p) => VALID_ASSERTION_TYPES.includes(p.type))
|
|
123
|
+
.map((p) => ({ type: p.type, pattern: p.pattern }));
|
|
124
|
+
}
|
|
86
125
|
function validateFeature(feature) {
|
|
87
126
|
if (!feature || typeof feature !== 'object') {
|
|
88
127
|
return null;
|
|
@@ -116,6 +155,9 @@ function validateFeature(feature) {
|
|
|
116
155
|
if (Array.isArray(obj.userFlows)) {
|
|
117
156
|
result.userFlows = obj.userFlows.filter((v) => typeof v === 'string');
|
|
118
157
|
}
|
|
158
|
+
if (Array.isArray(obj.assertionPatterns)) {
|
|
159
|
+
result.assertionPatterns = validateAssertionPatterns(obj.assertionPatterns);
|
|
160
|
+
}
|
|
119
161
|
return result;
|
|
120
162
|
}
|
|
121
163
|
export function loadRouteFamilyManifest(testsRoot, config) {
|
|
@@ -269,6 +311,83 @@ export function getRoutesForBinding(manifest, binding) {
|
|
|
269
311
|
}
|
|
270
312
|
return family.routes;
|
|
271
313
|
}
|
|
314
|
+
export function getAssertionPatternsForBinding(manifest, binding) {
|
|
315
|
+
const family = getFamilyById(manifest, binding.family);
|
|
316
|
+
if (!family) {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
if (binding.feature) {
|
|
320
|
+
const feature = getFeatureById(family, binding.feature);
|
|
321
|
+
if (feature?.assertionPatterns && feature.assertionPatterns.length > 0) {
|
|
322
|
+
return feature.assertionPatterns;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return family.assertionPatterns || [];
|
|
326
|
+
}
|
|
272
327
|
export function clearManifestCache() {
|
|
273
328
|
manifestCache.clear();
|
|
274
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Build heuristic route families from changed files when no manifest exists.
|
|
332
|
+
* Groups files by their top-level directory to create rough family groupings.
|
|
333
|
+
* Results are lower confidence but allow analysis to proceed without training.
|
|
334
|
+
*/
|
|
335
|
+
export function buildHeuristicFamilies(changedFiles, testsRoot) {
|
|
336
|
+
const dirGroups = new Map();
|
|
337
|
+
for (const file of changedFiles) {
|
|
338
|
+
const normalized = file.replace(/\\/g, '/');
|
|
339
|
+
const parts = normalized.split('/');
|
|
340
|
+
// Use the first meaningful directory segment as the family ID
|
|
341
|
+
// Skip common prefixes like 'src/', 'app/', 'lib/'
|
|
342
|
+
const skipDirs = new Set(['src', 'app', 'lib', 'packages', 'components']);
|
|
343
|
+
let familyDir = parts[0] || 'root';
|
|
344
|
+
if (skipDirs.has(familyDir) && parts.length > 1) {
|
|
345
|
+
familyDir = parts[1];
|
|
346
|
+
}
|
|
347
|
+
// Normalize to a clean family name
|
|
348
|
+
familyDir = familyDir.replace(/\.[^.]+$/, ''); // strip file extensions for single files
|
|
349
|
+
if (!dirGroups.has(familyDir)) {
|
|
350
|
+
dirGroups.set(familyDir, []);
|
|
351
|
+
}
|
|
352
|
+
dirGroups.get(familyDir).push(normalized);
|
|
353
|
+
}
|
|
354
|
+
const families = [];
|
|
355
|
+
for (const [dir, files] of dirGroups) {
|
|
356
|
+
families.push({
|
|
357
|
+
id: dir,
|
|
358
|
+
routes: [`/${dir}`],
|
|
359
|
+
webappPaths: files.map((f) => `${f}*`),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
logger.info(`Built ${families.length} heuristic families from ${changedFiles.length} changed files (no route-families.json found)`);
|
|
363
|
+
logger.info('Tip: Run `e2e-ai-agents train` to generate a proper route-families manifest for better accuracy.');
|
|
364
|
+
return {
|
|
365
|
+
families,
|
|
366
|
+
source: 'heuristic',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Serialize a RouteFamilyManifest to clean JSON, stripping empty optional fields.
|
|
371
|
+
*/
|
|
372
|
+
export function serializeManifest(manifest) {
|
|
373
|
+
const output = {
|
|
374
|
+
families: manifest.families.map((f) => {
|
|
375
|
+
const cleaned = { ...f };
|
|
376
|
+
const optionalArrays = [
|
|
377
|
+
'pageObjects', 'components', 'webappPaths', 'serverPaths',
|
|
378
|
+
'specDirs', 'cypressSpecDirs', 'tags', 'userFlows', 'features', 'apiEndpoints', 'assertionPatterns',
|
|
379
|
+
];
|
|
380
|
+
for (const key of optionalArrays) {
|
|
381
|
+
if (!cleaned[key] || (Array.isArray(cleaned[key]) && cleaned[key].length === 0)) {
|
|
382
|
+
delete cleaned[key];
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (!cleaned.priority)
|
|
386
|
+
delete cleaned.priority;
|
|
387
|
+
if (!cleaned.testType)
|
|
388
|
+
delete cleaned.testType;
|
|
389
|
+
return cleaned;
|
|
390
|
+
}),
|
|
391
|
+
};
|
|
392
|
+
return JSON.stringify(output, null, 2) + '\n';
|
|
393
|
+
}
|
package/dist/esm/mcp-server.js
CHANGED
|
@@ -370,8 +370,7 @@ export class E2EAgentsMCPServer {
|
|
|
370
370
|
if (!validateGitRef(since)) {
|
|
371
371
|
return JSON.stringify({ error: 'Invalid git reference format' });
|
|
372
372
|
}
|
|
373
|
-
|
|
374
|
-
const result = spawnSync('git', ['diff', '--name-only', '--', `${since}..HEAD`], {
|
|
373
|
+
const result = spawnSync('git', ['diff', '--name-only', `${since}..HEAD`], {
|
|
375
374
|
cwd: this.repoRoot,
|
|
376
375
|
encoding: 'utf-8',
|
|
377
376
|
timeout: 30000,
|
|
@@ -439,8 +438,7 @@ export class E2EAgentsMCPServer {
|
|
|
439
438
|
if (!validateGitRef(since)) {
|
|
440
439
|
return [];
|
|
441
440
|
}
|
|
442
|
-
|
|
443
|
-
const result = spawnSync('git', ['diff', '--name-only', '--', `${since}..HEAD`], {
|
|
441
|
+
const result = spawnSync('git', ['diff', '--name-only', `${since}..HEAD`], {
|
|
444
442
|
cwd: this.repoRoot,
|
|
445
443
|
encoding: 'utf-8',
|
|
446
444
|
timeout: 30000,
|