@skyramp/mcp 0.3.0-rc.1 → 0.3.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/build/prompts/test-maintenance/actionsInstructions.js +4 -2
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +9 -0
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +8 -7
- package/build/prompts/test-maintenance/driftAnalysisSections.js +4 -5
- package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +11 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +22 -6
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +23 -31
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +7 -1
- package/build/prompts/test-recommendation/diffExecutionPlan.js +20 -6
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +10 -1
- package/build/prompts/test-recommendation/scopeAssessment.js +27 -2
- package/build/prompts/test-recommendation/scopeAssessment.test.js +35 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +5 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +61 -0
- package/build/prompts/testbot/testbot-prompts.js +7 -5
- package/build/prompts/testbot/testbot-prompts.test.js +1 -1
- package/build/services/AnalyticsService.js +37 -2
- package/build/services/TestDiscoveryService.d.ts +38 -4
- package/build/services/TestDiscoveryService.js +187 -16
- package/build/services/TestDiscoveryService.test.js +232 -10
- package/build/services/TestExecutionService.d.ts +12 -1
- package/build/services/TestExecutionService.js +20 -41
- package/build/services/TestGenerationService.d.ts +8 -0
- package/build/services/TestGenerationService.js +62 -17
- package/build/services/TestGenerationService.test.js +67 -1
- package/build/tools/auth/loginTool.js +1 -1
- package/build/tools/auth/logoutTool.js +1 -1
- package/build/tools/code-refactor/codeReuseTool.js +1 -1
- package/build/tools/code-refactor/modularizationTool.js +1 -1
- package/build/tools/executeSkyrampTestTool.js +30 -1
- package/build/tools/fixErrorTool.js +1 -1
- package/build/tools/one-click/oneClickTool.js +1 -1
- package/build/tools/test-management/actionsTool.js +76 -41
- package/build/tools/test-management/actionsTool.test.js +104 -38
- package/build/tools/test-management/analyzeChangesTool.js +102 -4
- package/build/tools/test-management/analyzeChangesTool.test.js +3 -1
- package/build/tools/test-management/analyzeTestHealthTool.js +9 -1
- package/build/tools/test-management/analyzeTestHealthTool.test.js +1 -1
- package/build/tools/trace/startTraceCollectionTool.js +1 -1
- package/build/tools/trace/stopTraceCollectionTool.js +1 -1
- package/build/tools/workspace/initScanWorkspaceTool.js +1 -1
- package/build/tools/workspace/initializeWorkspaceTool.js +1 -1
- package/build/tools/workspace/initializeWorkspaceTool.test.js +1 -1
- package/build/types/RepositoryAnalysis.d.ts +6 -6
- package/build/types/TestAnalysis.d.ts +13 -0
- package/build/utils/dartRouteExtractor.js +8 -30
- package/build/utils/docker.test.js +1 -1
- package/build/utils/fileWalk.d.ts +71 -0
- package/build/utils/fileWalk.js +79 -0
- package/build/utils/fileWalk.test.d.ts +1 -0
- package/build/utils/fileWalk.test.js +252 -0
- package/build/utils/frontendIntegration.js +20 -36
- package/build/utils/frontendSelectors.d.ts +34 -0
- package/build/utils/frontendSelectors.js +124 -0
- package/build/utils/frontendSelectors.test.d.ts +1 -0
- package/build/utils/frontendSelectors.test.js +118 -0
- package/build/utils/importerHop.d.ts +18 -0
- package/build/utils/importerHop.js +48 -3
- package/build/utils/importerHop.test.js +71 -1
- package/build/utils/pythonMountPrefixes.d.ts +25 -0
- package/build/utils/pythonMountPrefixes.js +347 -0
- package/build/utils/pythonMountPrefixes.test.d.ts +1 -0
- package/build/utils/pythonMountPrefixes.test.js +113 -0
- package/build/utils/repoScanner.js +30 -23
- package/build/utils/repoScanner.test.js +98 -0
- package/build/utils/routeParsers.d.ts +7 -1
- package/build/utils/routeParsers.js +32 -18
- package/build/utils/sourceRouteExtractor.js +17 -40
- package/build/utils/trace-parser.js +7 -19
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/utils/walkerCharacterization.test.d.ts +1 -0
- package/build/utils/walkerCharacterization.test.js +233 -0
- package/node_modules/playwright/lib/common/config.js +1 -1
- package/node_modules/playwright/lib/common/configLoader.js +1 -1
- package/node_modules/playwright/lib/common/fixtures.js +1 -1
- package/node_modules/playwright/lib/common/testType.js +1 -1
- package/node_modules/playwright/lib/index.js +2 -2
- package/node_modules/playwright/lib/isomorphic/testTree.js +1 -1
- package/node_modules/playwright/lib/matchers/expect.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tab.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/navigate.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/tracing.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +7 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +3 -1
- package/node_modules/playwright/lib/reporters/base.js +2 -2
- package/node_modules/playwright/lib/reporters/list.js +1 -1
- package/node_modules/playwright/lib/reporters/teleEmitter.js +1 -1
- package/node_modules/playwright/lib/worker/testInfo.js +2 -2
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-Cqp3cwEJ.js → codeMirrorModule-aszq5EdG.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-CYf9adZh.js → defaultSettingsView-BxS7Jm4s.js} +94 -94
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.d4gkVSou.js → index.D4JTTy4R.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.u4_8VnCV.js → uiMode.DaRMQKOI.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/playwright/._ThirdPartyNotices.txt +0 -0
- package/node_modules/playwright/._cli.js +0 -0
- package/node_modules/playwright/._index.d.ts +0 -0
- package/node_modules/playwright/._index.js +0 -0
- package/node_modules/playwright/._index.mjs +0 -0
- package/node_modules/playwright/._jsx-runtime.js +0 -0
- package/node_modules/playwright/._jsx-runtime.mjs +0 -0
- package/node_modules/playwright/._lib +0 -0
- package/node_modules/playwright/._node_modules +0 -0
- package/node_modules/playwright/._package.json +0 -0
- package/node_modules/playwright/._test.d.ts +0 -0
- package/node_modules/playwright/._test.js +0 -0
- package/node_modules/playwright/._test.mjs +0 -0
- package/node_modules/playwright/._types +0 -0
- package/node_modules/playwright/._vitest.config.ts +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._.DS_Store +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._.npmignore +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._README.md +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._ThirdPartyNotices.txt +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._bin +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._browsers.json +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._bundles +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._cli.js +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._index.d.ts +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._index.js +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._index.mjs +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._lib +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._package.json +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._src +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._types +0 -0
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { logger } from "../../utils/logger.js";
|
|
3
3
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
4
4
|
import { MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, MAX_CRITICAL_TESTS, PATH_PARAM_UUID_GUIDANCE, AUTH_CONFLICT_ERROR_MSG, } from "../test-recommendation/recommendationSections.js";
|
|
5
|
+
import { TASK_ANALYZE_MAINTAIN, TASK_GENERATE, TASK_SUBMIT, taskRef } from "../test-recommendation/recommendationShared.js";
|
|
5
6
|
import { getTraceRecordingPromptText } from "../../playwright/traceRecordingPrompt.js";
|
|
6
7
|
import { isContractConsumerModeEnabled } from "../../utils/featureFlags.js";
|
|
7
8
|
import { resolveServiceDetailsRef } from "../../utils/utils.js";
|
|
@@ -130,7 +131,7 @@ ${hasRelatedRepos ? `
|
|
|
130
131
|
|
|
131
132
|
b. Write \`updateInstructions\` for each UPDATE or REGENERATE test before calling \`skyramp_actions\` — articulating the change first prevents file content from overriding diff-based reasoning.
|
|
132
133
|
|
|
133
|
-
c. Call \`skyramp_execute_test\` with \`phase: "before"\` and \`stateFile\` for every test
|
|
134
|
+
c. Call \`skyramp_execute_test\` with \`phase: "before"\` and \`stateFile\` for every UPDATE/REGENERATE/DELETE test. Exclude tests marked \`[external]\`. Run them sequentially, not in parallel. This captures the pre-edit baseline — do not skip even if you expect the test to fail.
|
|
134
135
|
|
|
135
136
|
d. Call \`skyramp_actions\` with \`stateFile\` (from \`skyramp_analyze_changes\` output) and apply the edits it returns.
|
|
136
137
|
|
|
@@ -355,6 +356,7 @@ ${task1Section}
|
|
|
355
356
|
${userPrompt ? "Generate only the tests that the user requested from the Additional Recommendations. The rules below still apply." : "Drift-based maintenance (Task 1) is complete. This step only processes the GENERATE list. Exception: if a GENERATE item targets a resource with an existing `[skyramp]` contract test, UPDATE that test file (see covered-resource handling below) — a new test case added to an existing file counts toward the budget and is reported in `newTestsCreated`."}
|
|
356
357
|
|
|
357
358
|
- **MANDATORY — use the pre-ranked GENERATE list as-is**: The Execution Plan's GENERATE section governs ADD actions. You MUST generate exactly those scenarios in the exact order listed. Do NOT substitute, rename, or replace a GENERATE item. If parameter grounding uncovers a distinct bug-catching scenario not already in the GENERATE or ADDITIONAL list, generate it after all planned GENERATE items are complete and report it in \`newTestsCreated\` — this is an additional test driven by source-code analysis and does not count against the GENERATE budget.${hasRelatedRepos ? `\n - **Multi-repo exception:** this run has related repositories, so the per-repo GENERATE lists are NOT final — they are candidates re-selected by the cross-repo round-robin described in Task 1's "Cross-repo test generation". Follow that pooled, type-distributed selection instead of any single repo's GENERATE list. (In single-repo runs the GENERATE list IS final — generate it exactly as-is.)` : ""}
|
|
359
|
+
- **Do not fabricate tests outside the GENERATE list.** New test files cover NEW observable surface only — a new endpoint, or a newly-integrated component/route not already covered by an existing test. Changes that only modify, delete, or add fields to an EXISTING covered endpoint or component are maintenance: handle them in ${taskRef(TASK_ANALYZE_MAINTAIN)} by UPDATE/DELETE of the existing test, never by creating a new spec. If the GENERATE list is empty (deletion-only, cosmetic, or modification-of-existing PRs with no new surface), create zero new tests and proceed to ${taskRef(TASK_SUBMIT)} — do not invent a new spec to have something to report.
|
|
358
360
|
- Scenario JSON files are always new files — always generate them for new methods. Every generated scenario JSON must have a corresponding new integration test generated from it via \`skyramp_integration_test_generation\`.
|
|
359
361
|
- Covered-resource handling (aligns with Execution Plan Step 0): When a GENERATE item targets a resource that already has an existing test file covering the same endpoint:
|
|
360
362
|
- If the existing test source is \`[external]\`, skip the resource entirely — the external test already provides coverage. Do NOT UPDATE, REGENERATE, or DELETE external tests.
|
|
@@ -362,9 +364,9 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
|
|
|
362
364
|
- Contract tests: UPDATE the existing Skyramp test file (add the new method's test cases). A new test case is a new test even if the file already exists — report in \`newTestsCreated\` and count toward the budget.
|
|
363
365
|
- Integration/scenario tests: Always generate as a new file via the scenario pipeline (\`skyramp_batch_scenario_test_generation\` → \`skyramp_integration_test_generation\`), even if an existing integration test covers the same resource. A new multi-step scenario (e.g. create → PATCH → verify recalculation) is a distinct test file. Report in \`newTestsCreated\` and count toward the budget.
|
|
364
366
|
- UI tests: Always generate as a new file. Report in \`newTestsCreated\`.
|
|
365
|
-
Keep advancing until you have created exactly
|
|
367
|
+
Keep advancing until you have created exactly as many new test files as your committed Budget Plan's generate count (at most ${maxGenerate}) OR exhausted all candidates. If your Budget Plan is 0 total, ${taskRef(TASK_GENERATE)} produces zero tests.
|
|
366
368
|
- Example: If enrichment reveals that sending \`discount_value\` without \`discount_type\` silently orphans the value (a concrete bug), complete all planned GENERATE items first, then generate this discovered scenario as an extra test and report it in \`newTestsCreated\`.
|
|
367
|
-
- Total generated:
|
|
369
|
+
- Total generated: your committed Budget Plan's generate count (from the Execution Plan's Scope Assessment, at most ${maxGenerate}) is the single source of truth for how many tests to create. Process every GENERATE-tagged item in order, then backfill from ADDITIONAL candidates (highest-ranked first) until \`newTestsCreated\` reaches that generate count or all candidates are exhausted. If your Budget Plan is 0 total (the Execution Plan's zero-classified default or cosmetic-only override applies), skip generation and backfilling entirely and proceed to ${taskRef(TASK_SUBMIT)}'s zero-test report path.
|
|
368
370
|
- **UI test priority**: If the PR scope assessment shows any UI/E2E budget OR \`uiContext.changedFrontendFiles\` is non-empty (the deterministic server signal — populated for all supported frontend file types including \`.tsx\`/\`.jsx\`/\`.vue\`/\`.svelte\`/\`.dart\`), you MUST attempt to generate at least one UI test. Use \`browser_navigate\` to the app's base URL — if the app responds, record a trace and generate the test.
|
|
369
371
|
**Flutter web apps:** Skyramp's Playwright tools automatically enable Flutter's accessibility semantics tree on every \`browser_navigate\` call — you do NOT need to manually click \`flt-semantics-placeholder\` or add any activation step to the trace. Do NOT log an \`issuesFound\` entry about Flutter canvas rendering or accessibility activation — this is handled transparently. **Do NOT skip test generation or abstain from recording based on what you see in the Flutter source code** (e.g. \`SemanticsBinding.ensureSemantics()\` commented out, \`IS_TESTING\` flag absent, or similar) — Skyramp enables accessibility from the browser side regardless of the app's Dart code. Proceed with \`browser_navigate\` and test recording as normal. **Start at the app's root URL** (e.g. \`{baseUrl}/\`) — do NOT \`browser_navigate\` straight to a deep sub-route (e.g. \`/authors\`, \`/orders/13\`). Flutter \`go_router\` SPAs route from the root: deep-linking on a cold page load often fails to render the expected screen (the route's widgets never mount, so the trace captures the wrong page). Load the root, let the app's own routing/auth-redirect render, then reach target screens by interaction. **After the initial login, navigate using in-app controls only** (tab buttons, links, back buttons) — do NOT call \`browser_navigate\` to a different URL after login. Flutter web apps are SPAs: a \`browser_navigate\` to a new URL after login triggers a full page reload which clears the auth session, causing redundant re-login cycles in the generated test. Use button clicks to reach target screens instead.
|
|
370
372
|
**Skip only if one of these conditions is met:**
|
|
@@ -546,7 +548,7 @@ Do NOT use \`page.waitForTimeout()\` with fixed delays. Do NOT retry more than o
|
|
|
546
548
|
4. **Wait**: Do NOT proceed to test execution until steps 1–3 are complete and the verification checklist in the \`skyramp_enhance_assertions\` tool result has been validated for EVERY generated test file.
|
|
547
549
|
Do not make any changes other than the assertion enhancements described above. For example: do not modify auth headers, cookies, tokens, env vars, or imports that the generation tool already set correctly — those are correct by construction and changing them breaks auth or execution.
|
|
548
550
|
|
|
549
|
-
**Final execution (mandatory):** Do NOT call \`skyramp_execute_test\` until ALL maintenance edits AND ALL new test generation/enhancement are complete. Run these calls sequentially, not in parallel.
|
|
551
|
+
**Final execution (mandatory):** Do NOT call \`skyramp_execute_test\` until ALL maintenance edits AND ALL new test generation/enhancement are complete. Run these calls sequentially, not in parallel. Exclude tests marked \`[external]\`.
|
|
550
552
|
- Only report test results for files you actually ran.
|
|
551
553
|
**Auth**: If \`skyramp_analyze_changes\` reports an auth token or \`SKYRAMP_TEST_TOKEN\` is set, pass it in **every** \`skyramp_execute_test\` call from the first attempt — do NOT wait for a 401/403 to discover auth is needed.
|
|
552
554
|
|
|
@@ -568,7 +570,7 @@ In these cases:
|
|
|
568
570
|
- \`businessCaseAnalysis\` must be a one-sentence summary of what the PR actually does (do NOT leave it blank)
|
|
569
571
|
- \`additionalRecommendations\` must be \`[]\` — do NOT recommend tests for a no-surface PR
|
|
570
572
|
|
|
571
|
-
Otherwise: in \`newTestsCreated\`, you must have exactly
|
|
573
|
+
Otherwise (your Budget Plan is non-zero): in \`newTestsCreated\`, you must have exactly as many budget-counting new tests as your committed Budget Plan's generate count (at most ${maxGenerate}). Only new files (ADD) created for the planned GENERATE items count toward this target — GENERATE items converted to UPDATE do not. You may also include at most one additional discovered-scenario file in \`newTestsCreated\` (the bug-catching test generated after all planned items); that extra test does **not** count against the budget. If you have fewer budget-counting new tests than your generate count, backfill from the remaining ADDITIONAL candidates before proceeding. Only proceed with fewer if all candidates failed after retry AND the fallback single-contract test also failed.
|
|
572
574
|
|
|
573
575
|
Call \`skyramp_submit_report\` with \`summaryOutputFile\`: "${summaryOutputFile}" and \`stateFile\` (from \`skyramp_analyze_changes\` output) — the stateFile is required for execution outcome tracking. Field names, types, and formats are defined in the tool's parameter schema — follow them exactly.
|
|
574
576
|
|
|
@@ -3,7 +3,7 @@ jest.unstable_mockModule("../../workspace/workspace.js", () => ({
|
|
|
3
3
|
WorkspaceConfigManager: jest.fn(),
|
|
4
4
|
}));
|
|
5
5
|
jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
|
|
6
|
-
AnalyticsService: { pushMCPToolEvent: jest.fn() },
|
|
6
|
+
AnalyticsService: { pushMCPToolEvent: jest.fn(() => Promise.resolve()) },
|
|
7
7
|
}));
|
|
8
8
|
jest.unstable_mockModule("../../playwright/traceRecordingPrompt.js", () => ({
|
|
9
9
|
getTraceRecordingPromptText: () => "",
|
|
@@ -4,6 +4,41 @@ import * as path from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { getEntryPoint, getCIPlatform, getRepositoryInfo, } from "../utils/telemetry.js";
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
|
+
/**
|
|
8
|
+
* Timeout (ms) for the native FFI analytics call. 5 s is long enough for a
|
|
9
|
+
* single HTTPS POST to the telemetry endpoint (p99 < 1 s in normal conditions)
|
|
10
|
+
* but short enough that a hung DNS lookup or unresponsive backend won't stall
|
|
11
|
+
* the MCP tool response pipeline — the tool result must not be delayed by
|
|
12
|
+
* best-effort telemetry.
|
|
13
|
+
*/
|
|
14
|
+
const ANALYTICS_TIMEOUT_MS = 5000;
|
|
15
|
+
/**
|
|
16
|
+
* Race a promise against a timeout. Resolves/rejects with whichever settles
|
|
17
|
+
* first; the losing branch is left to run (fire-and-forget) but cannot affect
|
|
18
|
+
* the caller.
|
|
19
|
+
*/
|
|
20
|
+
function withTimeout(promise, ms, label) {
|
|
21
|
+
let timer;
|
|
22
|
+
const timeout = new Promise((_, reject) => {
|
|
23
|
+
timer = setTimeout(() => reject(new Error(`${label}: timed out after ${ms}ms`)), ms);
|
|
24
|
+
});
|
|
25
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Safely invoke the native pushToolEvent FFI call. Wraps the call in a timeout
|
|
29
|
+
* so a hung Go network call cannot block forever, and catches any synchronous
|
|
30
|
+
* throw from the native binding (e.g. if the Go runtime panicked on a prior
|
|
31
|
+
* call and the shared library is in a bad state).
|
|
32
|
+
*/
|
|
33
|
+
async function safePushToolEvent(entryPoint, toolName, errorMessage, params) {
|
|
34
|
+
try {
|
|
35
|
+
await withTimeout(pushToolEvent(entryPoint, toolName, errorMessage, params), ANALYTICS_TIMEOUT_MS, "pushToolEvent");
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
logger.error("safePushToolEvent failed", { error: error instanceof Error ? error.message : String(error) });
|
|
39
|
+
// Swallow — analytics must never crash the server.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
7
42
|
export class AnalyticsService {
|
|
8
43
|
static async pushTestGenerationToolEvent(toolName, result, params) {
|
|
9
44
|
const analyticsResult = {};
|
|
@@ -33,7 +68,7 @@ export class AnalyticsService {
|
|
|
33
68
|
// process.cwd() is set by the IDE and may not be the user's repo.
|
|
34
69
|
const repoPath = params.repositoryPath || params.workspacePath;
|
|
35
70
|
Object.assign(params, await getRepositoryInfo(repoPath));
|
|
36
|
-
await
|
|
71
|
+
await safePushToolEvent(getEntryPoint(), toolName, errorMessage, params);
|
|
37
72
|
}
|
|
38
73
|
catch (error) {
|
|
39
74
|
logger.error("Error pushing MCP tool event", { error: error });
|
|
@@ -53,7 +88,7 @@ export class AnalyticsService {
|
|
|
53
88
|
if (ciPlatform) {
|
|
54
89
|
params.ciPlatform = ciPlatform;
|
|
55
90
|
}
|
|
56
|
-
await
|
|
91
|
+
await safePushToolEvent(getEntryPoint(), "mcp_server_crash", errorMessage, params);
|
|
57
92
|
}
|
|
58
93
|
}
|
|
59
94
|
/**
|
|
@@ -12,13 +12,32 @@ export interface TestDiscoveryOptions {
|
|
|
12
12
|
* exist but resource names are unresolvable — most files score 0 (low-relevance)
|
|
13
13
|
* and are excluded; only the few that happen to match the sentinel are kept.
|
|
14
14
|
* - Empty array `[]`: PR mode with no endpoints from diff or scanner — API/integration
|
|
15
|
-
* external tests are excluded entirely; UI tests may still be promoted via
|
|
15
|
+
* external tests are excluded entirely; UI tests may still be promoted via changedFrontendFiles.
|
|
16
16
|
* - `undefined`: full-repo mode — external tests capped at MAX_EXTERNAL_FULL_REPO.
|
|
17
17
|
*/
|
|
18
18
|
changedResources?: string[];
|
|
19
|
-
/**
|
|
20
|
-
*
|
|
21
|
-
|
|
19
|
+
/** Raw changed symbol names (e.g. ["DeploymentCreate"]). Content-grep fallback so an
|
|
20
|
+
* external test that references a changed type by name — but whose path/URL matches no
|
|
21
|
+
* changed resource (e.g. a schema unit test) — is still surfaced (SKYR-3924). */
|
|
22
|
+
changedSymbols?: string[];
|
|
23
|
+
/** True when `changedResources` are precise (derived from specific changed schema/model
|
|
24
|
+
* symbols, not the `["unknown"]` sentinel or a broad set). Lets the URL content-match
|
|
25
|
+
* bucket skip the MAX_CONTENT_PROMOTED cap — a direct HTTP call to the changed endpoint
|
|
26
|
+
* is high-confidence, and the specific resource set naturally bounds the match count. */
|
|
27
|
+
preciseResources?: boolean;
|
|
28
|
+
/** Repo-relative paths of the changed frontend (non-test) files — their presence is the
|
|
29
|
+
* frontend-change signal that drives UI-test promotion (Step 2). When present, promotion
|
|
30
|
+
* is scoped to tests whose name/path/imports are relevant to these files — a frontend PR
|
|
31
|
+
* must not pull in the whole monorepo's UI test suite (SKYR-3941). When absent, Step 2 is
|
|
32
|
+
* skipped (no UI promotion). */
|
|
33
|
+
changedFrontendFiles?: string[];
|
|
34
|
+
/** Selector literals (data-testid values, CSS class tokens) added or removed by the diff.
|
|
35
|
+
* UI-test promotion pass 3 ("selector-edge"): a page object / spec couples to a changed
|
|
36
|
+
* component by SELECTOR, not by filename or import, so passes 1-2 miss it. Any external
|
|
37
|
+
* test-owned file (including page objects, which are not *.test/*.spec) whose body
|
|
38
|
+
* references one of these literals is promoted. Both added and removed values are passed
|
|
39
|
+
* so a page object still holding a renamed selector's OLD value is caught. */
|
|
40
|
+
changedSelectors?: string[];
|
|
22
41
|
}
|
|
23
42
|
export declare class TestDiscoveryService {
|
|
24
43
|
private readonly EXCLUDED_DIRS;
|
|
@@ -47,7 +66,22 @@ export declare class TestDiscoveryService {
|
|
|
47
66
|
* Example: "test_orders_api.py" vs ["orders"] → score 1.
|
|
48
67
|
*/
|
|
49
68
|
private scoreRelevance;
|
|
69
|
+
/**
|
|
70
|
+
* Derive relevance tokens from the changed frontend files, for scoring UI tests in Step 2
|
|
71
|
+
* the same way scoreRelevance scores against changed API resources. For each file we emit:
|
|
72
|
+
* - the basename stem (e.g. "OrderSearch" → "ordersearch") — matches co-located tests like
|
|
73
|
+
* OrderSearch.test.tsx, the dominant convention;
|
|
74
|
+
* - its sub-tokens, split on delimiters AND camelCase boundaries ("OrderSearch" →
|
|
75
|
+
* "order","search") — matches hyphenated/underscored test names like order-search.spec.tsx;
|
|
76
|
+
* - the parent directory name — the signal when the basename is generic (orders/index.tsx).
|
|
77
|
+
* Generic tokens (index, components, utils, …) and tokens shorter than 3 chars are dropped so
|
|
78
|
+
* a shared dir or an index file doesn't match every UI test in the repo.
|
|
79
|
+
*/
|
|
80
|
+
private deriveFrontendResourceTokens;
|
|
50
81
|
private readonly MAX_CONTENT_PROMOTED;
|
|
82
|
+
private readonly MAX_UI_PROMOTED;
|
|
83
|
+
private readonly UI_IMPORT_MATCH_WEIGHT;
|
|
84
|
+
private readonly GENERIC_FRONTEND_TOKENS;
|
|
51
85
|
/**
|
|
52
86
|
* Partition external test files into relevant (score > 0) and low-relevance (score = 0).
|
|
53
87
|
* Two-pass: filename token overlap first (primary); content-based endpoint path scan
|
|
@@ -3,6 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import { logger } from "../utils/logger.js";
|
|
4
4
|
import { TestSource } from "../types/TestAnalysis.js";
|
|
5
5
|
import { TestType } from "../types/TestTypes.js";
|
|
6
|
+
import { buildPathSignatures } from "../utils/pathSignatures.js";
|
|
6
7
|
import fg from "fast-glob";
|
|
7
8
|
export class TestDiscoveryService {
|
|
8
9
|
EXCLUDED_DIRS = [
|
|
@@ -104,7 +105,7 @@ export class TestDiscoveryService {
|
|
|
104
105
|
// Full-repo mode (changedResources is undefined):
|
|
105
106
|
// No diff context — all external files treated as potentially relevant.
|
|
106
107
|
// Cap at MAX_EXTERNAL_FULL_REPO to avoid reading hundreds of files.
|
|
107
|
-
const { changedResources,
|
|
108
|
+
const { changedResources, changedSymbols, preciseResources, changedFrontendFiles, changedSelectors } = options;
|
|
108
109
|
let relevantExternal;
|
|
109
110
|
// Includes .e2e.ts — Playwright/Cypress tests that navigate pages. These get
|
|
110
111
|
// testType="e2e" from path detection but are never named *.test.ts or *.spec.ts
|
|
@@ -117,7 +118,7 @@ export class TestDiscoveryService {
|
|
|
117
118
|
// for tests that are misnamed relative to the endpoint they exercise.
|
|
118
119
|
if (changedResources?.length) {
|
|
119
120
|
({ relevant: relevantExternal } =
|
|
120
|
-
this.partitionByRelevance(classified.external, changedResources, classified.contentCache));
|
|
121
|
+
this.partitionByRelevance(classified.external, changedResources, classified.contentCache, changedSymbols, preciseResources));
|
|
121
122
|
}
|
|
122
123
|
else if (changedResources !== undefined) {
|
|
123
124
|
relevantExternal = [];
|
|
@@ -125,10 +126,93 @@ export class TestDiscoveryService {
|
|
|
125
126
|
else {
|
|
126
127
|
relevantExternal = classified.external.slice(0, this.MAX_EXTERNAL_FULL_REPO);
|
|
127
128
|
}
|
|
128
|
-
// Step 2 — UI test promotion
|
|
129
|
-
|
|
129
|
+
// Step 2 — UI test promotion, driven by the presence of changed frontend files and
|
|
130
|
+
// independent of Step 1. Scoped by relevance to those files, mirroring Step 1's API
|
|
131
|
+
// discipline: a frontend PR must not pull in the whole monorepo's UI test suite (SKYR-3941).
|
|
132
|
+
// Absent a changed-frontend-file list, Step 2 is skipped (no UI promotion).
|
|
133
|
+
if (changedFrontendFiles?.length) {
|
|
130
134
|
const alreadyRelevant = new Set(relevantExternal);
|
|
131
|
-
const
|
|
135
|
+
const uiCandidates = classified.external.filter(f => UI_TEST_EXT.test(f) && !alreadyRelevant.has(f));
|
|
136
|
+
// Pass 1 — filename/path relevance: tokens derived from the changed frontend files.
|
|
137
|
+
const feTokens = this.deriveFrontendResourceTokens(changedFrontendFiles);
|
|
138
|
+
// Pass 2 — import-edge relevance: module-path signatures of the changed files, matched
|
|
139
|
+
// against test bodies so a test that imports a changed component (from './PriceLabel')
|
|
140
|
+
// is caught even when its name shares no token — the recall case a filename-only filter
|
|
141
|
+
// drops (a checkout test that renders a changed child component). Reuses the same
|
|
142
|
+
// buildPathSignatures used by frontendIntegration/importerHop; this is Step 2's analog
|
|
143
|
+
// of Step 1's content-scan fallback. Skip generic basenames (index/app/…): their
|
|
144
|
+
// signature (`/index'`) would match every barrel import — those are already covered by
|
|
145
|
+
// pass 1's parent-directory token.
|
|
146
|
+
const importSignatures = changedFrontendFiles
|
|
147
|
+
.filter(f => {
|
|
148
|
+
const stem = path.basename(f).replace(/\.[^.]+$/, "").toLowerCase();
|
|
149
|
+
return stem.length >= 3 && !this.GENERIC_FRONTEND_TOKENS.has(stem);
|
|
150
|
+
})
|
|
151
|
+
.flatMap(f => buildPathSignatures(f));
|
|
152
|
+
let promoted;
|
|
153
|
+
if (feTokens.length === 0 && importSignatures.length === 0) {
|
|
154
|
+
// No relevance signal is derivable — every changed frontend file is a generic
|
|
155
|
+
// entrypoint (index.tsx / app.tsx / …) whose tokens are filtered out, and none
|
|
156
|
+
// yields an import signature. A root/entrypoint change is broadly impactful, so
|
|
157
|
+
// fall back to promoting all UI candidates rather than degenerate to zero — still
|
|
158
|
+
// bounded by MAX_UI_PROMOTED so it can't re-flood a large monorepo's suite.
|
|
159
|
+
promoted = uiCandidates.slice(0, this.MAX_UI_PROMOTED);
|
|
160
|
+
const dropped = uiCandidates.length - promoted.length;
|
|
161
|
+
logger.info(`UI test promotion: no relevance signal from generic-only frontend changes — capped promote-all fallback (${promoted.length}/${uiCandidates.length} promoted${dropped > 0 ? `, ${dropped} dropped by MAX_UI_PROMOTED` : ""})`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
let scored = uiCandidates
|
|
165
|
+
.map(f => {
|
|
166
|
+
const nameScore = this.scoreRelevance(f, feTokens);
|
|
167
|
+
const content = classified.contentCache.get(f) ?? "";
|
|
168
|
+
const importsChanged = content.length > 0 && importSignatures.some(sig => content.includes(sig));
|
|
169
|
+
return { f, score: nameScore + (importsChanged ? this.UI_IMPORT_MATCH_WEIGHT : 0) };
|
|
170
|
+
})
|
|
171
|
+
.filter(x => x.score > 0)
|
|
172
|
+
.sort((a, b) => b.score - a.score)
|
|
173
|
+
.map(x => x.f);
|
|
174
|
+
if (scored.length > this.MAX_UI_PROMOTED) {
|
|
175
|
+
logger.info(`UI test promotion: ${scored.length} tests matched ${changedFrontendFiles.length} changed frontend file(s); capping to ${this.MAX_UI_PROMOTED} highest-scoring (SKYR-3941 backstop)`);
|
|
176
|
+
scored = scored.slice(0, this.MAX_UI_PROMOTED);
|
|
177
|
+
}
|
|
178
|
+
logger.info(`UI test promotion scoped to ${scored.length}/${uiCandidates.length} UI test(s) relevant to changed frontend files`);
|
|
179
|
+
promoted = scored;
|
|
180
|
+
}
|
|
181
|
+
// Pass 3 — selector-edge: a page object / spec couples to a changed component by
|
|
182
|
+
// SELECTOR (data-testid value or CSS class), not by filename or import, so passes
|
|
183
|
+
// 1-2 miss it. Crucially the candidate pool here is the FULL external set, not just
|
|
184
|
+
// UI_TEST_EXT: a page object like `table.component.ts` is classified external (via
|
|
185
|
+
// the page-objects/ dir pattern) with content cached, but is not a *.test/*.spec
|
|
186
|
+
// file, so it never enters `uiCandidates`. Promote any not-yet-relevant external
|
|
187
|
+
// file whose body references a changed selector literal (matched against both the
|
|
188
|
+
// added and removed diff values, so a page object still holding a renamed selector's
|
|
189
|
+
// OLD value is caught). Bounded by MAX_UI_PROMOTED.
|
|
190
|
+
// Combined cap: MAX_UI_PROMOTED is a single Step-2 ceiling, so pass 3 may only add up
|
|
191
|
+
// to what pass 1/2 left — otherwise the two independent caps could promote 2x and
|
|
192
|
+
// re-flood discovery (the exact failure SKYR-3941 guards against). Compute the headroom
|
|
193
|
+
// up front and skip the O(external × selectors) content scan entirely when pass 1/2
|
|
194
|
+
// already filled the budget.
|
|
195
|
+
const selectorHeadroom = Math.max(0, this.MAX_UI_PROMOTED - promoted.length);
|
|
196
|
+
if (changedSelectors?.length && selectorHeadroom > 0) {
|
|
197
|
+
const alreadyPromoted = new Set([...alreadyRelevant, ...promoted]);
|
|
198
|
+
const selectorMatched = classified.external
|
|
199
|
+
.filter((f) => {
|
|
200
|
+
if (alreadyPromoted.has(f))
|
|
201
|
+
return false;
|
|
202
|
+
const content = classified.contentCache.get(f) ?? "";
|
|
203
|
+
return content.length > 0 && changedSelectors.some((sel) => content.includes(sel));
|
|
204
|
+
})
|
|
205
|
+
// Sort for a deterministic cap — filesystem/glob traversal order is not stable
|
|
206
|
+
// across environments, so which files survive MAX_UI_PROMOTED must not depend on it.
|
|
207
|
+
.sort();
|
|
208
|
+
if (selectorMatched.length > 0) {
|
|
209
|
+
const capped = selectorMatched.slice(0, selectorHeadroom);
|
|
210
|
+
logger.info(`UI test promotion: ${selectorMatched.length} test/page-object file(s) reference a changed selector (pass 3, selector-edge)` +
|
|
211
|
+
(capped.length < selectorMatched.length ? `; added ${capped.length} (MAX_UI_PROMOTED headroom after pass 1/2)` : ""));
|
|
212
|
+
if (capped.length > 0)
|
|
213
|
+
promoted = [...promoted, ...capped];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
132
216
|
const promotedSet = new Set(promoted);
|
|
133
217
|
// Promote snapshot/fixture siblings of scored AND newly promoted UI tests.
|
|
134
218
|
// Include promoted files so siblings are found on UI-only PRs where
|
|
@@ -188,9 +272,59 @@ export class TestDiscoveryService {
|
|
|
188
272
|
return parts.length > 1 && parts.every(p => p.length >= 3 && expanded.has(p));
|
|
189
273
|
}).length;
|
|
190
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Derive relevance tokens from the changed frontend files, for scoring UI tests in Step 2
|
|
277
|
+
* the same way scoreRelevance scores against changed API resources. For each file we emit:
|
|
278
|
+
* - the basename stem (e.g. "OrderSearch" → "ordersearch") — matches co-located tests like
|
|
279
|
+
* OrderSearch.test.tsx, the dominant convention;
|
|
280
|
+
* - its sub-tokens, split on delimiters AND camelCase boundaries ("OrderSearch" →
|
|
281
|
+
* "order","search") — matches hyphenated/underscored test names like order-search.spec.tsx;
|
|
282
|
+
* - the parent directory name — the signal when the basename is generic (orders/index.tsx).
|
|
283
|
+
* Generic tokens (index, components, utils, …) and tokens shorter than 3 chars are dropped so
|
|
284
|
+
* a shared dir or an index file doesn't match every UI test in the repo.
|
|
285
|
+
*/
|
|
286
|
+
deriveFrontendResourceTokens(changedFrontendFiles) {
|
|
287
|
+
const out = new Set();
|
|
288
|
+
const add = (t) => {
|
|
289
|
+
const v = t.toLowerCase();
|
|
290
|
+
if (v.length >= 3 && !this.GENERIC_FRONTEND_TOKENS.has(v))
|
|
291
|
+
out.add(v);
|
|
292
|
+
};
|
|
293
|
+
for (const f of changedFrontendFiles) {
|
|
294
|
+
const segs = f.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
295
|
+
const base = segs[segs.length - 1] ?? "";
|
|
296
|
+
const stem = base.replace(/\.[^.]+$/, ""); // drop extension
|
|
297
|
+
add(stem); // whole stem (e.g. "ordersearch", "order-search")
|
|
298
|
+
for (const t of stem.split(/[-_.]+|(?<=[a-z0-9])(?=[A-Z])/))
|
|
299
|
+
add(t); // sub-tokens
|
|
300
|
+
const parent = segs[segs.length - 2];
|
|
301
|
+
if (parent && !parent.startsWith("__"))
|
|
302
|
+
add(parent);
|
|
303
|
+
}
|
|
304
|
+
return [...out];
|
|
305
|
+
}
|
|
191
306
|
// Max additional tests promoted via content-based scoring on top of filename matches.
|
|
192
307
|
// Content matches are lower-confidence than filename matches — cap to avoid token bloat.
|
|
193
308
|
MAX_CONTENT_PROMOTED = 5;
|
|
309
|
+
// Backstop cap on scoped UI-test promotion (Step 2). The relevance filter against the
|
|
310
|
+
// changed frontend files does the real bounding; this only guards a pathological match
|
|
311
|
+
// (e.g. a change to a widely-shared component). Highest-scoring tests survive truncation.
|
|
312
|
+
MAX_UI_PROMOTED = 50;
|
|
313
|
+
// Weight added to a UI test's relevance score when it imports a changed frontend file by
|
|
314
|
+
// module path (import-edge match, Step 2 pass 2). Set high above any filename-token score
|
|
315
|
+
// so import matches — the strongest "this test exercises the changed code" signal —
|
|
316
|
+
// always survive the MAX_UI_PROMOTED cap ahead of name-only matches.
|
|
317
|
+
UI_IMPORT_MATCH_WEIGHT = 100;
|
|
318
|
+
// Generic path/name tokens that carry no feature signal — excluded when deriving relevance
|
|
319
|
+
// tokens from changed frontend files so a changed index.tsx or a shared "components" dir
|
|
320
|
+
// doesn't match every UI test in the repo.
|
|
321
|
+
GENERIC_FRONTEND_TOKENS = new Set([
|
|
322
|
+
"index", "app", "main", "root", "styles", "style", "css", "scss", "types", "type",
|
|
323
|
+
"constants", "utils", "util", "helpers", "helper", "components", "component", "pages",
|
|
324
|
+
"page", "routes", "route", "src", "common", "shared", "hooks", "hook", "lib", "context",
|
|
325
|
+
"providers", "provider", "store", "stores", "api", "test", "tests", "spec", "specs",
|
|
326
|
+
"mocks", "mock", "fixtures", "fixture", "assets", "config",
|
|
327
|
+
]);
|
|
194
328
|
/**
|
|
195
329
|
* Partition external test files into relevant (score > 0) and low-relevance (score = 0).
|
|
196
330
|
* Two-pass: filename token overlap first (primary); content-based endpoint path scan
|
|
@@ -198,8 +332,9 @@ export class TestDiscoveryService {
|
|
|
198
332
|
* (e.g. test_checkout_flow.py testing /api/orders scores 0 by name but matches in content).
|
|
199
333
|
* Content matches are capped at MAX_CONTENT_PROMOTED to bound token impact.
|
|
200
334
|
*/
|
|
201
|
-
partitionByRelevance(files, changedResources, contentCache) {
|
|
335
|
+
partitionByRelevance(files, changedResources, contentCache, changedSymbols, preciseResources) {
|
|
202
336
|
const nameMatched = [];
|
|
337
|
+
const symbolMatched = [];
|
|
203
338
|
const contentCandidates = [];
|
|
204
339
|
// Precompile content patterns once — avoids 20 resources × 150 files = 3K compilations.
|
|
205
340
|
const contentPatterns = contentCache
|
|
@@ -212,22 +347,58 @@ export class TestDiscoveryService {
|
|
|
212
347
|
return new RegExp(`["'\`][^"'\`]*/${escaped}`, "i");
|
|
213
348
|
})
|
|
214
349
|
: [];
|
|
350
|
+
// Exact, case-sensitive word-boundary match on a changed type name (e.g.
|
|
351
|
+
// `DeploymentCreate`). Catches an external test that references the changed schema/model
|
|
352
|
+
// by name but whose path/URL matches no changed resource — a schema unit test
|
|
353
|
+
// constructing it directly (SKYR-3924). High-confidence, so not subject to the content cap.
|
|
354
|
+
const symbolPatterns = contentCache
|
|
355
|
+
? (changedSymbols ?? []).map(s => new RegExp(`\\b${s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`))
|
|
356
|
+
: [];
|
|
357
|
+
// Endpoint-call patterns: an HTTP-method call to a path containing the changed resource
|
|
358
|
+
// (e.g. `client.post("/deployments/")`). A direct call to the changed endpoint is
|
|
359
|
+
// high-confidence — distinct from a file that merely mentions the resource string.
|
|
360
|
+
const endpointCallPatterns = contentCache
|
|
361
|
+
? changedResources.map(r => {
|
|
362
|
+
const escaped = r.toLowerCase()
|
|
363
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
364
|
+
.replace(/-|\//g, "[-/]");
|
|
365
|
+
return new RegExp(`\\b(?:get|post|put|patch|delete|request|fetch)\\b[^\\n]{0,60}["'\`][^"'\`]*/${escaped}`, "i");
|
|
366
|
+
})
|
|
367
|
+
: [];
|
|
368
|
+
const strongContent = [];
|
|
215
369
|
for (const f of files) {
|
|
216
370
|
if (this.scoreRelevance(f, changedResources) > 0) {
|
|
217
371
|
nameMatched.push(f);
|
|
372
|
+
continue;
|
|
218
373
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
374
|
+
const content = contentCache?.get(f) ?? "";
|
|
375
|
+
if (!content)
|
|
376
|
+
continue;
|
|
377
|
+
if (symbolPatterns.some(p => p.test(content))) {
|
|
378
|
+
symbolMatched.push(f);
|
|
379
|
+
}
|
|
380
|
+
else if (endpointCallPatterns.some(p => p.test(content))) {
|
|
381
|
+
strongContent.push(f); // direct call to the changed endpoint — high-confidence
|
|
382
|
+
}
|
|
383
|
+
else if (contentPatterns.some(p => p.test(content))) {
|
|
384
|
+
contentCandidates.push(f); // loose mention of the resource — low-confidence, capped
|
|
224
385
|
}
|
|
225
386
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
387
|
+
// Precise resource sets (schema-diff preload): keep high-confidence endpoint-call matches
|
|
388
|
+
// uncapped (bounded by the specific resource), still cap loose mentions. Otherwise cap all
|
|
389
|
+
// content matches together (original behavior).
|
|
390
|
+
const contentMatched = preciseResources
|
|
391
|
+
? [...strongContent, ...contentCandidates.slice(0, this.MAX_CONTENT_PROMOTED)]
|
|
392
|
+
: [...strongContent, ...contentCandidates].slice(0, this.MAX_CONTENT_PROMOTED);
|
|
393
|
+
if (symbolMatched.length > 0 || contentMatched.length > 0) {
|
|
394
|
+
// Report what actually landed in contentMatched — the non-precise branch caps
|
|
395
|
+
// strong+loose together, so strongContent may be truncated too. Deriving both
|
|
396
|
+
// counts from contentMatched.length avoids a misleading negative "loose" count.
|
|
397
|
+
const strongIn = Math.min(strongContent.length, contentMatched.length);
|
|
398
|
+
const looseIn = contentMatched.length - strongIn;
|
|
399
|
+
logger.info(`Relevance: ${symbolMatched.length} symbol + ${strongIn} endpoint-call + ${looseIn} loose content`);
|
|
400
|
+
}
|
|
401
|
+
return { relevant: [...nameMatched, ...symbolMatched, ...contentMatched] };
|
|
231
402
|
}
|
|
232
403
|
/**
|
|
233
404
|
* Process test files in parallel batches with concurrency control
|