@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
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
2
|
// See LICENSE.txt for license information.
|
|
3
|
-
import {
|
|
3
|
+
import { runGitRaw } from '../agent/git.js';
|
|
4
4
|
const MAX_DIFF_CHARS = 8000;
|
|
5
5
|
const MAX_TOTAL_CHARS = 60000;
|
|
6
6
|
const TRUNCATION_NOTICE = '\n... (diff truncated)';
|
|
7
|
-
function runGitRaw(args, cwd) {
|
|
8
|
-
const result = spawnSync('git', args, {
|
|
9
|
-
cwd,
|
|
10
|
-
encoding: 'utf-8',
|
|
11
|
-
timeout: 30000,
|
|
12
|
-
});
|
|
13
|
-
if (result.error || result.status !== 0) {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
return result.stdout;
|
|
17
|
-
}
|
|
18
7
|
/**
|
|
19
8
|
* Loads git diffs for the given changed files relative to the given since ref.
|
|
20
9
|
* Uses `git merge-base` to find the accurate base ref first.
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// See LICENSE.txt for license information.
|
|
3
3
|
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
4
4
|
import { join } from 'path';
|
|
5
|
-
import { loadRouteFamilyManifest, bindFilesToFamilies, getSpecDirsForBinding, getCypressSpecDirsForBinding, getPriorityForBinding, getUserFlowsForBinding, } from '../knowledge/route_families.js';
|
|
5
|
+
import { loadRouteFamilyManifest, buildHeuristicFamilies, bindFilesToFamilies, getSpecDirsForBinding, getCypressSpecDirsForBinding, getPriorityForBinding, getUserFlowsForBinding, } from '../knowledge/route_families.js';
|
|
6
|
+
import { isTestFile } from '../agent/git.js';
|
|
6
7
|
function scanDirForSpecs(baseDir, specDir, extension) {
|
|
7
8
|
const fullDir = join(baseDir, specDir);
|
|
8
9
|
if (!existsSync(fullDir)) {
|
|
@@ -125,7 +126,8 @@ function groupBindings(fileBindings) {
|
|
|
125
126
|
const key = binding.feature || binding.family;
|
|
126
127
|
const existing = groups.get(key);
|
|
127
128
|
if (existing) {
|
|
128
|
-
if (!existing.
|
|
129
|
+
if (!existing._seen.has(fb.file)) {
|
|
130
|
+
existing._seen.add(fb.file);
|
|
129
131
|
existing.files.push(fb.file);
|
|
130
132
|
}
|
|
131
133
|
}
|
|
@@ -134,23 +136,13 @@ function groupBindings(fileBindings) {
|
|
|
134
136
|
familyId: binding.family,
|
|
135
137
|
featureId: binding.feature,
|
|
136
138
|
files: [fb.file],
|
|
139
|
+
_seen: new Set([fb.file]),
|
|
137
140
|
});
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
144
|
return groups;
|
|
142
145
|
}
|
|
143
|
-
/** Filter out test files that should not be treated as application changes. */
|
|
144
|
-
function isTestFile(file) {
|
|
145
|
-
const normalized = file.replace(/\\/g, '/');
|
|
146
|
-
return /\.(spec|test)\.(ts|tsx|js|jsx)$/.test(normalized) ||
|
|
147
|
-
/\.snap$/.test(normalized) ||
|
|
148
|
-
/_test\.go$/.test(normalized) ||
|
|
149
|
-
normalized.includes('__tests__/') ||
|
|
150
|
-
normalized.includes('__snapshots__/') ||
|
|
151
|
-
normalized.includes('/tests/') ||
|
|
152
|
-
normalized.includes('/test/');
|
|
153
|
-
}
|
|
154
146
|
/** Classify filtered test files by type for downstream decision-making. */
|
|
155
147
|
function classifyPrTestFiles(allFiles, sourceFiles) {
|
|
156
148
|
const sourceSet = new Set(sourceFiles);
|
|
@@ -180,17 +172,11 @@ export function analyzeImpact(changedFiles, options) {
|
|
|
180
172
|
const allOriginalFiles = [...new Set([...changedFiles, ...preFilteredTests])];
|
|
181
173
|
changedFiles = changedFiles.filter((f) => !isTestFile(f));
|
|
182
174
|
const prIncludedTestFiles = classifyPrTestFiles(allOriginalFiles, changedFiles);
|
|
183
|
-
// Load manifest
|
|
184
|
-
|
|
175
|
+
// Load manifest, fall back to heuristic families if not found
|
|
176
|
+
let manifest = loadRouteFamilyManifest(testsRoot, routeFamilies);
|
|
185
177
|
if (!manifest) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
expandedFiles: options.expandedFiles || [],
|
|
189
|
-
impactedFeatures: [],
|
|
190
|
-
unboundFiles: [...changedFiles],
|
|
191
|
-
warnings: ['Route family manifest not found. All files are unbound.'],
|
|
192
|
-
prIncludedTestFiles,
|
|
193
|
-
};
|
|
178
|
+
manifest = buildHeuristicFamilies(changedFiles, testsRoot);
|
|
179
|
+
warnings.push('Route family manifest not found. Using directory-based heuristics (lower accuracy).', 'Tip: Run `e2e-ai-agents train` to generate a proper manifest.');
|
|
194
180
|
}
|
|
195
181
|
// Combine original + expanded files
|
|
196
182
|
const allFiles = [...new Set([...changedFiles, ...(options.expandedFiles || [])])];
|
package/dist/esm/index.js
CHANGED
|
@@ -44,8 +44,29 @@ export { StrategistAgent } from './agents/strategist.js';
|
|
|
44
44
|
export { TestDesignerAgent } from './agents/test-designer.js';
|
|
45
45
|
export { CrossImpactAgent } from './agents/cross-impact.js';
|
|
46
46
|
export { RegressionAdvisorAgent } from './agents/regression-advisor.js';
|
|
47
|
+
// Base provider (for extending with custom providers)
|
|
48
|
+
export { BaseProvider, BudgetExceededError } from './base_provider.js';
|
|
49
|
+
// Budget tracking
|
|
50
|
+
export { BudgetLedger } from './budget_ledger.js';
|
|
51
|
+
// Model routing
|
|
52
|
+
export { ModelRouter } from './model_router.js';
|
|
53
|
+
// Resilience
|
|
54
|
+
export { withRetry } from './resilience/retry.js';
|
|
55
|
+
export { CircuitBreaker } from './resilience/circuit_breaker.js';
|
|
56
|
+
// Metrics
|
|
57
|
+
export { PrometheusMetrics } from './metrics/prometheus.js';
|
|
58
|
+
// Secret scanning
|
|
59
|
+
export { sanitizeSecrets, containsSecrets, sanitizeObject } from './sanitize.js';
|
|
60
|
+
// CLI errors
|
|
61
|
+
export { CliError, classifyError, EXIT_CODES } from './cli/errors.js';
|
|
47
62
|
// Training (route-families bootstrap and maintenance)
|
|
48
63
|
export { scanProject } from './training/scanner.js';
|
|
49
64
|
export { mergeFamilies, detectStaleFamilies } from './training/merger.js';
|
|
50
65
|
export { enrichFamilies } from './training/enricher.js';
|
|
51
66
|
export { getCommitFiles, validateCommit, buildValidationReport, formatValidationReport } from './training/validator.js';
|
|
67
|
+
export { loadKnowledgeGraph, classifyProjectType, transformKGToFamilies, loadDiffOverlay } from './knowledge/kg_bridge.js';
|
|
68
|
+
export { scanFromKnowledgeGraph } from './training/kg_scanner.js';
|
|
69
|
+
export { resolveGenerationProfile, isMattermostProfile } from './prompts/generation_profile.js';
|
|
70
|
+
export { detectFramework, detectTestMode } from './adapters/framework_adapter.js';
|
|
71
|
+
// Route families (additional)
|
|
72
|
+
export { serializeManifest } from './knowledge/route_families.js';
|
|
@@ -2,39 +2,233 @@
|
|
|
2
2
|
// See LICENSE.txt for license information.
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs';
|
|
4
4
|
import { join, extname } from 'path';
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
// ── TypeScript AST-based extraction ────────────────────────
|
|
7
|
+
function extractMethodsFromAST(sourceFile, checker) {
|
|
8
|
+
const surfaces = [];
|
|
9
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
10
|
+
if (!ts.isClassDeclaration(node) || !node.name) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const className = node.name.text;
|
|
14
|
+
const methods = [];
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
// Get base class name if extends
|
|
17
|
+
let extendsName;
|
|
18
|
+
if (node.heritageClauses) {
|
|
19
|
+
for (const clause of node.heritageClauses) {
|
|
20
|
+
if (clause.token === ts.SyntaxKind.ExtendsKeyword && clause.types.length > 0) {
|
|
21
|
+
const expr = clause.types[0].expression;
|
|
22
|
+
if (ts.isIdentifier(expr)) {
|
|
23
|
+
extendsName = expr.text;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const member of node.members) {
|
|
29
|
+
// Skip constructor
|
|
30
|
+
if (ts.isConstructorDeclaration(member)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// Skip private/protected members
|
|
34
|
+
const modifiers = ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined;
|
|
35
|
+
if (modifiers?.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const name = member.name && ts.isIdentifier(member.name) ? member.name.text : null;
|
|
39
|
+
if (!name || name.startsWith('_') || seen.has(name)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
seen.add(name);
|
|
43
|
+
if (ts.isMethodDeclaration(member)) {
|
|
44
|
+
const isAsync = modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
|
|
45
|
+
const params = extractParams(member.parameters, checker);
|
|
46
|
+
const returnType = extractReturnType(member, checker);
|
|
47
|
+
methods.push({
|
|
48
|
+
name,
|
|
49
|
+
kind: 'method',
|
|
50
|
+
async: isAsync ? true : undefined,
|
|
51
|
+
params: params.length > 0 ? params : undefined,
|
|
52
|
+
returnType: returnType || undefined,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else if (ts.isGetAccessorDeclaration(member)) {
|
|
56
|
+
methods.push({ name, kind: 'getter' });
|
|
57
|
+
}
|
|
58
|
+
else if (ts.isPropertyDeclaration(member)) {
|
|
59
|
+
// Check if it's an arrow function property (e.g., name = async () => {})
|
|
60
|
+
if (member.initializer && (ts.isArrowFunction(member.initializer) || ts.isFunctionExpression(member.initializer))) {
|
|
61
|
+
const fn = member.initializer;
|
|
62
|
+
const fnModifiers = ts.canHaveModifiers(fn) ? ts.getModifiers(fn) : undefined;
|
|
63
|
+
const isAsync = fnModifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
|
|
64
|
+
const params = extractParams(fn.parameters, checker);
|
|
65
|
+
methods.push({
|
|
66
|
+
name,
|
|
67
|
+
kind: 'method',
|
|
68
|
+
async: isAsync ? true : undefined,
|
|
69
|
+
params: params.length > 0 ? params : undefined,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
methods.push({ name, kind: 'property' });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (methods.length > 0) {
|
|
78
|
+
surfaces.push({
|
|
79
|
+
className,
|
|
80
|
+
file: sourceFile.fileName,
|
|
81
|
+
methods,
|
|
82
|
+
extends: extendsName,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return surfaces;
|
|
87
|
+
}
|
|
88
|
+
function extractParams(params, checker) {
|
|
89
|
+
return params.map((p) => {
|
|
90
|
+
const name = ts.isIdentifier(p.name) ? p.name.text : p.name.getText();
|
|
91
|
+
const optional = p.questionToken !== undefined || p.initializer !== undefined;
|
|
92
|
+
let type;
|
|
93
|
+
if (p.type) {
|
|
94
|
+
type = p.type.getText();
|
|
95
|
+
}
|
|
96
|
+
else if (checker) {
|
|
97
|
+
try {
|
|
98
|
+
const symbol = checker.getSymbolAtLocation(p.name);
|
|
99
|
+
if (symbol) {
|
|
100
|
+
const t = checker.getTypeOfSymbolAtLocation(symbol, p);
|
|
101
|
+
type = checker.typeToString(t);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Type inference failure is non-fatal
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { name, type, optional: optional || undefined };
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function extractReturnType(method, checker) {
|
|
112
|
+
if (method.type) {
|
|
113
|
+
return method.type.getText();
|
|
114
|
+
}
|
|
115
|
+
if (checker) {
|
|
116
|
+
try {
|
|
117
|
+
const signature = checker.getSignatureFromDeclaration(method);
|
|
118
|
+
if (signature) {
|
|
119
|
+
const returnType = checker.getReturnTypeOfSignature(signature);
|
|
120
|
+
const typeStr = checker.typeToString(returnType);
|
|
121
|
+
// Skip overly verbose inferred types
|
|
122
|
+
if (typeStr.length < 100) {
|
|
123
|
+
return typeStr;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Type inference failure is non-fatal
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Extract page objects using the TypeScript Compiler API.
|
|
135
|
+
* Falls back to regex if compilation fails.
|
|
136
|
+
*/
|
|
137
|
+
function extractWithAST(files) {
|
|
138
|
+
if (files.length === 0) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
// Find the nearest tsconfig.json
|
|
142
|
+
const firstDir = join(files[0], '..');
|
|
143
|
+
const tsconfigPath = ts.findConfigFile(firstDir, ts.sys.fileExists, 'tsconfig.json');
|
|
144
|
+
let program;
|
|
145
|
+
if (tsconfigPath) {
|
|
146
|
+
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
147
|
+
const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, join(tsconfigPath, '..'));
|
|
148
|
+
// Only compile the files we care about, using the config's compiler options
|
|
149
|
+
program = ts.createProgram(files, parsedConfig.options);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
program = ts.createProgram(files, {
|
|
153
|
+
target: ts.ScriptTarget.ES2022,
|
|
154
|
+
module: ts.ModuleKind.ESNext,
|
|
155
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
156
|
+
strict: true,
|
|
157
|
+
allowJs: false,
|
|
158
|
+
noEmit: true,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const checker = program.getTypeChecker();
|
|
162
|
+
const surfaces = [];
|
|
163
|
+
for (const filePath of files) {
|
|
164
|
+
const sourceFile = program.getSourceFile(filePath);
|
|
165
|
+
if (!sourceFile) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
surfaces.push(...extractMethodsFromAST(sourceFile, checker));
|
|
169
|
+
}
|
|
170
|
+
// Resolve inherited methods: if class extends another class in the catalog,
|
|
171
|
+
// merge parent methods that the child doesn't override
|
|
172
|
+
resolveInheritance(surfaces);
|
|
173
|
+
return surfaces;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Merge parent class methods into child classes that extend them.
|
|
177
|
+
*/
|
|
178
|
+
function resolveInheritance(surfaces) {
|
|
179
|
+
const byName = new Map(surfaces.map((s) => [s.className, s]));
|
|
180
|
+
for (const surface of surfaces) {
|
|
181
|
+
if (!surface.extends) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const parent = byName.get(surface.extends);
|
|
185
|
+
if (!parent) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const childMethodNames = new Set(surface.methods.map((m) => m.name));
|
|
189
|
+
for (const parentMethod of parent.methods) {
|
|
190
|
+
if (!childMethodNames.has(parentMethod.name)) {
|
|
191
|
+
surface.methods.push({ ...parentMethod });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// ── Regex-based extraction (fallback) ──────────────────────
|
|
5
197
|
const RESERVED_WORDS = new Set([
|
|
6
198
|
'private', 'protected', 'static', 'abstract', 'override',
|
|
7
199
|
'if', 'for', 'while', 'switch', 'return',
|
|
8
200
|
'const', 'let', 'var', 'import', 'export',
|
|
9
201
|
'class', 'type', 'interface', 'constructor',
|
|
10
202
|
]);
|
|
11
|
-
function
|
|
203
|
+
function extractMethodsFromRegex(content) {
|
|
12
204
|
const methods = [];
|
|
13
205
|
const seen = new Set();
|
|
14
|
-
// Match async method declarations: async methodName(
|
|
15
206
|
const asyncMethodRe = /(?:async\s+)([a-zA-Z_]\w*)\s*\(/g;
|
|
16
207
|
let match;
|
|
17
208
|
while ((match = asyncMethodRe.exec(content)) !== null) {
|
|
18
209
|
const name = match[1];
|
|
19
|
-
if (RESERVED_WORDS.has(name)) {
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
if (!seen.has(name)) {
|
|
210
|
+
if (!RESERVED_WORDS.has(name) && !seen.has(name)) {
|
|
23
211
|
seen.add(name);
|
|
24
|
-
methods.push({ name, kind: 'method' });
|
|
212
|
+
methods.push({ name, kind: 'method', async: true });
|
|
25
213
|
}
|
|
26
214
|
}
|
|
27
|
-
// Match non-async public method patterns
|
|
28
215
|
const methodRe = /^\s+(?:readonly\s+)?([a-zA-Z_]\w*)\s*(?:\(|=\s*(?:async\s*)?\()/gm;
|
|
29
216
|
while ((match = methodRe.exec(content)) !== null) {
|
|
30
217
|
const name = match[1];
|
|
31
|
-
if (RESERVED_WORDS.has(name)
|
|
32
|
-
|
|
218
|
+
if (!RESERVED_WORDS.has(name) && !seen.has(name)) {
|
|
219
|
+
seen.add(name);
|
|
220
|
+
const isAsync = match[0].includes('async');
|
|
221
|
+
methods.push({ name, kind: 'method', async: isAsync ? true : undefined });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const arrowRe = /^\s+([a-zA-Z_]\w*)\s*=\s*(async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>/gm;
|
|
225
|
+
while ((match = arrowRe.exec(content)) !== null) {
|
|
226
|
+
const name = match[1];
|
|
227
|
+
if (!RESERVED_WORDS.has(name) && !seen.has(name)) {
|
|
228
|
+
seen.add(name);
|
|
229
|
+
methods.push({ name, kind: 'method', async: match[2] ? true : undefined });
|
|
33
230
|
}
|
|
34
|
-
seen.add(name);
|
|
35
|
-
methods.push({ name, kind: 'method' });
|
|
36
231
|
}
|
|
37
|
-
// Match getter patterns: get propertyName()
|
|
38
232
|
const getterRe = /\bget\s+([a-zA-Z_]\w*)\s*\(\)/g;
|
|
39
233
|
while ((match = getterRe.exec(content)) !== null) {
|
|
40
234
|
const name = match[1];
|
|
@@ -43,15 +237,13 @@ function extractMethodsFromSource(content) {
|
|
|
43
237
|
methods.push({ name, kind: 'getter' });
|
|
44
238
|
}
|
|
45
239
|
}
|
|
46
|
-
// Match readonly property declarations
|
|
47
240
|
const propRe = /^\s+(?:readonly\s+)?([a-zA-Z_]\w*)\s*[:=]/gm;
|
|
48
241
|
while ((match = propRe.exec(content)) !== null) {
|
|
49
242
|
const name = match[1];
|
|
50
|
-
if (RESERVED_WORDS.has(name)
|
|
51
|
-
|
|
243
|
+
if (!RESERVED_WORDS.has(name) && !seen.has(name)) {
|
|
244
|
+
seen.add(name);
|
|
245
|
+
methods.push({ name, kind: 'property' });
|
|
52
246
|
}
|
|
53
|
-
seen.add(name);
|
|
54
|
-
methods.push({ name, kind: 'property' });
|
|
55
247
|
}
|
|
56
248
|
return methods;
|
|
57
249
|
}
|
|
@@ -59,7 +251,27 @@ function extractClassName(content) {
|
|
|
59
251
|
const match = content.match(/(?:export\s+)?class\s+(\w+)/);
|
|
60
252
|
return match ? match[1] : null;
|
|
61
253
|
}
|
|
62
|
-
|
|
254
|
+
// ── Directory scanning ─────────────────────────────────────
|
|
255
|
+
function collectTypeScriptFiles(dir) {
|
|
256
|
+
const files = [];
|
|
257
|
+
if (!existsSync(dir)) {
|
|
258
|
+
return files;
|
|
259
|
+
}
|
|
260
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
261
|
+
for (const entry of entries) {
|
|
262
|
+
const fullPath = join(dir, entry.name);
|
|
263
|
+
if (entry.isDirectory()) {
|
|
264
|
+
files.push(...collectTypeScriptFiles(fullPath));
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const ext = extname(entry.name);
|
|
268
|
+
if (ext === '.ts' || ext === '.tsx') {
|
|
269
|
+
files.push(fullPath);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return files;
|
|
273
|
+
}
|
|
274
|
+
function scanDirectoryWithRegex(dir) {
|
|
63
275
|
const surfaces = [];
|
|
64
276
|
if (!existsSync(dir)) {
|
|
65
277
|
return surfaces;
|
|
@@ -68,29 +280,22 @@ function scanDirectory(dir) {
|
|
|
68
280
|
for (const entry of entries) {
|
|
69
281
|
const fullPath = join(dir, entry.name);
|
|
70
282
|
if (entry.isDirectory()) {
|
|
71
|
-
surfaces.push(...
|
|
283
|
+
surfaces.push(...scanDirectoryWithRegex(fullPath));
|
|
72
284
|
continue;
|
|
73
285
|
}
|
|
74
286
|
const ext = extname(entry.name);
|
|
75
287
|
if (ext !== '.ts' && ext !== '.tsx') {
|
|
76
288
|
continue;
|
|
77
289
|
}
|
|
78
|
-
if (entry.name === 'index.ts' || entry.name === 'index.tsx') {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
290
|
try {
|
|
82
291
|
const content = readFileSync(fullPath, 'utf-8');
|
|
83
292
|
const className = extractClassName(content);
|
|
84
293
|
if (!className) {
|
|
85
294
|
continue;
|
|
86
295
|
}
|
|
87
|
-
const extractedMethods =
|
|
296
|
+
const extractedMethods = extractMethodsFromRegex(content);
|
|
88
297
|
if (extractedMethods.length > 0) {
|
|
89
|
-
surfaces.push({
|
|
90
|
-
className,
|
|
91
|
-
file: fullPath,
|
|
92
|
-
methods: extractedMethods,
|
|
93
|
-
});
|
|
298
|
+
surfaces.push({ className, file: fullPath, methods: extractedMethods });
|
|
94
299
|
}
|
|
95
300
|
}
|
|
96
301
|
catch {
|
|
@@ -99,6 +304,7 @@ function scanDirectory(dir) {
|
|
|
99
304
|
}
|
|
100
305
|
return surfaces;
|
|
101
306
|
}
|
|
307
|
+
// ── Public API ──────────────────────────────────────────────
|
|
102
308
|
export function buildApiSurface(testsRoot, config) {
|
|
103
309
|
const pageObjectsDir = config?.pageObjectsDir
|
|
104
310
|
? join(testsRoot, config.pageObjectsDir)
|
|
@@ -106,10 +312,30 @@ export function buildApiSurface(testsRoot, config) {
|
|
|
106
312
|
const componentsDir = config?.componentsDir
|
|
107
313
|
? join(testsRoot, config.componentsDir)
|
|
108
314
|
: join(testsRoot, 'lib', 'src', 'ui', 'components');
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
315
|
+
let pageObjects;
|
|
316
|
+
if (config?.useRegexFallback) {
|
|
317
|
+
pageObjects = [
|
|
318
|
+
...scanDirectoryWithRegex(pageObjectsDir),
|
|
319
|
+
...scanDirectoryWithRegex(componentsDir),
|
|
320
|
+
];
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
// Use TypeScript AST — full type info, inheritance, params
|
|
324
|
+
const allFiles = [
|
|
325
|
+
...collectTypeScriptFiles(pageObjectsDir),
|
|
326
|
+
...collectTypeScriptFiles(componentsDir),
|
|
327
|
+
];
|
|
328
|
+
try {
|
|
329
|
+
pageObjects = extractWithAST(allFiles);
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// Fall back to regex if AST extraction fails
|
|
333
|
+
pageObjects = [
|
|
334
|
+
...scanDirectoryWithRegex(pageObjectsDir),
|
|
335
|
+
...scanDirectoryWithRegex(componentsDir),
|
|
336
|
+
];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
113
339
|
return {
|
|
114
340
|
pageObjects,
|
|
115
341
|
generatedAt: new Date().toISOString(),
|
|
@@ -168,7 +394,12 @@ export function formatApiSurfaceForPrompt(catalog, classNames) {
|
|
|
168
394
|
if (m.kind === 'getter') {
|
|
169
395
|
return ` get ${m.name}()`;
|
|
170
396
|
}
|
|
171
|
-
|
|
397
|
+
const prefix = m.async ? 'async ' : '';
|
|
398
|
+
const paramStr = m.params
|
|
399
|
+
? m.params.map((p) => `${p.name}${p.optional ? '?' : ''}${p.type ? `: ${p.type}` : ''}`).join(', ')
|
|
400
|
+
: '';
|
|
401
|
+
const retStr = m.returnType ? `: ${m.returnType}` : '';
|
|
402
|
+
return ` ${prefix}${m.name}(${paramStr})${retStr}`;
|
|
172
403
|
})
|
|
173
404
|
.join('\n');
|
|
174
405
|
sections.push(`${name}:\n${methodList}`);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
/**
|
|
4
|
+
* Shared cluster ID derivation for knowledge graph processing.
|
|
5
|
+
* Used by both kg_bridge.ts and kg_scanner.ts.
|
|
6
|
+
*/
|
|
7
|
+
/** Default directories to skip when deriving cluster IDs from file paths. */
|
|
8
|
+
const DEFAULT_SKIP_DIRS = new Set([
|
|
9
|
+
'src', 'app', 'lib', 'packages', 'server', 'api', 'pages',
|
|
10
|
+
'components', 'features', 'modules',
|
|
11
|
+
]);
|
|
12
|
+
/** Extended skip set that also excludes test directories. */
|
|
13
|
+
const SKIP_DIRS_WITH_TESTS = new Set([
|
|
14
|
+
...DEFAULT_SKIP_DIRS,
|
|
15
|
+
'test', 'tests', 'e2e', 'spec', 'specs',
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* Normalize a name to a snake_case cluster ID.
|
|
19
|
+
* Handles camelCase conversion, then strips non-alphanumeric characters.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeToClusterId(name) {
|
|
22
|
+
return name
|
|
23
|
+
.replace(/[A-Z]/g, (c, idx) => (idx > 0 ? `_${c.toLowerCase()}` : c.toLowerCase()))
|
|
24
|
+
.replace(/[^a-z0-9_]/g, '_')
|
|
25
|
+
.replace(/_+/g, '_')
|
|
26
|
+
.replace(/^_|_$/g, '');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Derive a cluster ID from a node that has an optional filePath and name.
|
|
30
|
+
* Prefers file path grouping for consistency.
|
|
31
|
+
*/
|
|
32
|
+
export function deriveClusterId(node, skipDirs) {
|
|
33
|
+
if (node.filePath) {
|
|
34
|
+
return deriveClusterIdFromPath(node.filePath, skipDirs);
|
|
35
|
+
}
|
|
36
|
+
const name = normalizeToClusterId(node.name);
|
|
37
|
+
return name && name.length > 1 ? name : null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Derive a cluster ID from a file path by finding the first meaningful
|
|
41
|
+
* directory segment after skipping common structural prefixes.
|
|
42
|
+
*/
|
|
43
|
+
export function deriveClusterIdFromPath(filePath, skipDirs = DEFAULT_SKIP_DIRS) {
|
|
44
|
+
const parts = filePath.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
45
|
+
for (const part of parts) {
|
|
46
|
+
if (skipDirs.has(part))
|
|
47
|
+
continue;
|
|
48
|
+
if (part.includes('.'))
|
|
49
|
+
continue; // skip files
|
|
50
|
+
const normalized = part.toLowerCase()
|
|
51
|
+
.replace(/[^a-z0-9_]/g, '_')
|
|
52
|
+
.replace(/_+/g, '_')
|
|
53
|
+
.replace(/^_|_$/g, '');
|
|
54
|
+
if (normalized && normalized.length > 1) {
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
export { DEFAULT_SKIP_DIRS, SKIP_DIRS_WITH_TESTS };
|