@mknrt/autotests-overkill 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +117 -0
  2. package/assets/overkill-logo.svg +8 -0
  3. package/assets/overkill-small.svg +8 -0
  4. package/dist/connectors/index.d.ts +4 -0
  5. package/dist/connectors/index.js +4 -0
  6. package/dist/indexer/index.d.ts +4 -0
  7. package/dist/indexer/index.js +4 -0
  8. package/dist/mcp/index.d.ts +2 -0
  9. package/dist/mcp/index.js +2 -0
  10. package/dist/src/appContext.d.ts +21 -0
  11. package/dist/src/appContext.js +40 -0
  12. package/dist/src/cli/indexKnowledge.d.ts +1 -0
  13. package/dist/src/cli/indexKnowledge.js +11 -0
  14. package/dist/src/cli/main.d.ts +19 -0
  15. package/dist/src/cli/main.js +59 -0
  16. package/dist/src/cli/runAcceptance.d.ts +21 -0
  17. package/dist/src/cli/runAcceptance.js +131 -0
  18. package/dist/src/cli/runMcpServer.d.ts +1 -0
  19. package/dist/src/cli/runMcpServer.js +10 -0
  20. package/dist/src/cli/shared.d.ts +1 -0
  21. package/dist/src/cli/shared.js +7 -0
  22. package/dist/src/config/consumerConfig.d.ts +126 -0
  23. package/dist/src/config/consumerConfig.js +63 -0
  24. package/dist/src/connectors/autotests2Connector.d.ts +41 -0
  25. package/dist/src/connectors/autotests2Connector.js +94 -0
  26. package/dist/src/connectors/caseplatformConnector.d.ts +35 -0
  27. package/dist/src/connectors/caseplatformConnector.js +251 -0
  28. package/dist/src/connectors/ciArtifactsConnector.d.ts +23 -0
  29. package/dist/src/connectors/ciArtifactsConnector.js +14 -0
  30. package/dist/src/connectors/diffConnector.d.ts +1 -0
  31. package/dist/src/connectors/diffConnector.js +4 -0
  32. package/dist/src/connectors/filesystemConnector.d.ts +4 -0
  33. package/dist/src/connectors/filesystemConnector.js +15 -0
  34. package/dist/src/connectors/gitlabConnector.d.ts +14 -0
  35. package/dist/src/connectors/gitlabConnector.js +64 -0
  36. package/dist/src/connectors/reportPortalConnector.d.ts +14 -0
  37. package/dist/src/connectors/reportPortalConnector.js +84 -0
  38. package/dist/src/contracts/confidence.d.ts +8 -0
  39. package/dist/src/contracts/confidence.js +33 -0
  40. package/dist/src/contracts/errors.d.ts +6 -0
  41. package/dist/src/contracts/errors.js +18 -0
  42. package/dist/src/contracts/toolOutput.d.ts +113 -0
  43. package/dist/src/contracts/toolOutput.js +35 -0
  44. package/dist/src/domain/analyzeDiffImpact.d.ts +22 -0
  45. package/dist/src/domain/analyzeDiffImpact.js +52 -0
  46. package/dist/src/domain/analyzeTestGaps.d.ts +22 -0
  47. package/dist/src/domain/analyzeTestGaps.js +41 -0
  48. package/dist/src/domain/buildRerunScope.d.ts +23 -0
  49. package/dist/src/domain/buildRerunScope.js +17 -0
  50. package/dist/src/domain/buildSetupTeardownPlan.d.ts +23 -0
  51. package/dist/src/domain/buildSetupTeardownPlan.js +13 -0
  52. package/dist/src/domain/findExistingTestAssets.d.ts +22 -0
  53. package/dist/src/domain/findExistingTestAssets.js +50 -0
  54. package/dist/src/domain/findFrontendContract.d.ts +22 -0
  55. package/dist/src/domain/findFrontendContract.js +13 -0
  56. package/dist/src/domain/findFrontendRuntimeLogic.d.ts +22 -0
  57. package/dist/src/domain/findFrontendRuntimeLogic.js +28 -0
  58. package/dist/src/domain/findTestAreaContext.d.ts +23 -0
  59. package/dist/src/domain/findTestAreaContext.js +30 -0
  60. package/dist/src/domain/generateSpecBlueprint.d.ts +23 -0
  61. package/dist/src/domain/generateSpecBlueprint.js +213 -0
  62. package/dist/src/domain/mapFeatureToExistingCoverage.d.ts +22 -0
  63. package/dist/src/domain/mapFeatureToExistingCoverage.js +56 -0
  64. package/dist/src/domain/recommendSelectorStrategy.d.ts +22 -0
  65. package/dist/src/domain/recommendSelectorStrategy.js +13 -0
  66. package/dist/src/domain/shared.d.ts +5 -0
  67. package/dist/src/domain/shared.js +36 -0
  68. package/dist/src/domain/summarizeCiContext.d.ts +22 -0
  69. package/dist/src/domain/summarizeCiContext.js +35 -0
  70. package/dist/src/domain/triageFailedRun.d.ts +25 -0
  71. package/dist/src/domain/triageFailedRun.js +46 -0
  72. package/dist/src/index.d.ts +4 -0
  73. package/dist/src/index.js +4 -0
  74. package/dist/src/indexer/extractors/frontendContractExtractor.d.ts +17 -0
  75. package/dist/src/indexer/extractors/frontendContractExtractor.js +109 -0
  76. package/dist/src/indexer/extractors/frontendRuntimeExtractor.d.ts +14 -0
  77. package/dist/src/indexer/extractors/frontendRuntimeExtractor.js +110 -0
  78. package/dist/src/indexer/extractors/testAssetExtractor.d.ts +13 -0
  79. package/dist/src/indexer/extractors/testAssetExtractor.js +17 -0
  80. package/dist/src/indexer/refreshPipeline.d.ts +12 -0
  81. package/dist/src/indexer/refreshPipeline.js +156 -0
  82. package/dist/src/indexer/snapshotStore.d.ts +10 -0
  83. package/dist/src/indexer/snapshotStore.js +45 -0
  84. package/dist/src/knowledge/database.d.ts +3 -0
  85. package/dist/src/knowledge/database.js +8 -0
  86. package/dist/src/knowledge/migrations.d.ts +1 -0
  87. package/dist/src/knowledge/migrations.js +28 -0
  88. package/dist/src/knowledge/repositories.d.ts +28 -0
  89. package/dist/src/knowledge/repositories.js +78 -0
  90. package/dist/src/mcp/registerTools.d.ts +11 -0
  91. package/dist/src/mcp/registerTools.js +118 -0
  92. package/dist/src/mcp/server.d.ts +4 -0
  93. package/dist/src/mcp/server.js +29 -0
  94. package/dist/src/utils/fileUtils.d.ts +4 -0
  95. package/dist/src/utils/fileUtils.js +16 -0
  96. package/dist/src/utils/pathUtils.d.ts +7 -0
  97. package/dist/src/utils/pathUtils.js +59 -0
  98. package/dist/src/utils/textUtils.d.ts +3 -0
  99. package/dist/src/utils/textUtils.js +26 -0
  100. package/docs/architecture/overview.md +31 -0
  101. package/docs/consumer-integration.md +64 -0
  102. package/docs/operator-cookbook.md +39 -0
  103. package/docs/plugin-packaging.md +33 -0
  104. package/docs/superpowers/plans/2026-04-19-autotests-overkill-v1.md +600 -0
  105. package/docs/tool-catalog.md +34 -0
  106. package/overkill.config.example.json +16 -0
  107. package/package.json +73 -0
  108. package/skills/overkill-debug-failure/SKILL.md +13 -0
  109. package/skills/overkill-find-gaps/SKILL.md +13 -0
  110. package/skills/overkill-generate-setup/SKILL.md +12 -0
  111. package/skills/overkill-generate-test/SKILL.md +15 -0
  112. package/skills/overkill-impact-analysis/SKILL.md +12 -0
  113. package/skills/overkill-onboard/SKILL.md +16 -0
  114. package/skills/overkill-prompt-builder/SKILL.md +13 -0
  115. package/skills/overkill-reuse-project-patterns/SKILL.md +13 -0
  116. package/skills/overkill-review-test-draft/SKILL.md +10 -0
  117. package/templates/prompts/debug-failure.md +3 -0
  118. package/templates/prompts/find-gaps.md +3 -0
  119. package/templates/prompts/generate-test.md +3 -0
  120. package/templates/prompts/impact-analysis.md +3 -0
  121. package/templates/spec-blueprints/default.md +39 -0
@@ -0,0 +1,28 @@
1
+ import { extractFrontendRuntimeLogic } from '../indexer/extractors/frontendRuntimeExtractor.js';
2
+ import { evidenceFile, output } from './shared.js';
3
+ export async function findFrontendRuntimeLogic(input, context) {
4
+ const runtime = await extractFrontendRuntimeLogic({
5
+ root: context.config.repos.caseplatformWeb,
6
+ widgetHint: input.widget,
7
+ });
8
+ return output(`Found ${runtime.runtimeSignals.length} runtime signals for ${input.widget}, including methods, branches, and event triggers.`, selectDiverseSignals(runtime.runtimeSignals, 10).map((signal) => evidenceFile(`${signal.kind} ${signal.name}`, signal.path, signal.detail, { kind: signal.kind })), [
9
+ 'Use branch evidence to decide where current specs miss state transitions.',
10
+ 'Treat event binding changes as candidate rerun triggers.',
11
+ ], runtime.warnings, runtime.runtimeSignals.map((signal) => signal.path));
12
+ }
13
+ function selectDiverseSignals(signals, limit) {
14
+ const perPath = new Map();
15
+ const selected = [];
16
+ for (const signal of signals) {
17
+ const count = perPath.get(signal.path) ?? 0;
18
+ if (count >= 2) {
19
+ continue;
20
+ }
21
+ selected.push(signal);
22
+ perPath.set(signal.path, count + 1);
23
+ if (selected.length >= limit) {
24
+ break;
25
+ }
26
+ }
27
+ return selected;
28
+ }
@@ -0,0 +1,23 @@
1
+ import type { AppContext } from '../appContext.js';
2
+ export declare function findTestAreaContext(input: {
3
+ area?: string;
4
+ feature?: string;
5
+ }, context: AppContext): Promise<{
6
+ warnings: string[];
7
+ summary: string;
8
+ evidence: {
9
+ type: "symbol" | "config" | "file" | "test" | "artifact" | "report" | "diff";
10
+ label: string;
11
+ path?: string | undefined;
12
+ locator?: string | undefined;
13
+ snippet?: string | undefined;
14
+ metadata?: Record<string, unknown> | undefined;
15
+ }[];
16
+ recommended_actions: string[];
17
+ repo_paths: string[];
18
+ confidence: {
19
+ score: number;
20
+ level: "low" | "medium" | "high";
21
+ rationale: string[];
22
+ };
23
+ }>;
@@ -0,0 +1,30 @@
1
+ import { queryDocuments } from '../knowledge/repositories.js';
2
+ import { ensureIndexed, evidenceFile, output } from './shared.js';
3
+ export async function findTestAreaContext(input, context) {
4
+ const docs = context.autotests2.getAreaDocs();
5
+ const area = input.area ?? inferArea(input.feature ?? '');
6
+ const initPattern = area === 'constructor' ? 'initConf()' : 'init()';
7
+ const selectPattern = area === 'constructor' ? 'cy.selectAppInConstructor(...)' : 'cy.selectForm(...)';
8
+ await ensureIndexed(context);
9
+ const indexedDocs = queryDocuments(context.database, area === 'constructor' ? 'initConf constructor' : 'init selectForm formRunner', {
10
+ repoKind: 'autotests2',
11
+ sourceKinds: ['doc'],
12
+ limit: 4,
13
+ });
14
+ const directEvidence = [
15
+ docs.contextText ? evidenceFile('project context', docs.contextPath, docs.contextText.slice(0, 220), { retrieval: 'direct-scan' }) : null,
16
+ docs.testingText ? evidenceFile('testing rules', docs.testingPath, docs.testingText.slice(0, 220), { retrieval: 'direct-scan' }) : null,
17
+ ].filter((item) => item !== null);
18
+ const indexedEvidence = indexedDocs.map((item) => evidenceFile(item.title, item.path, item.body.slice(0, 220), { retrieval: 'knowledge-store' }));
19
+ const evidence = directEvidence.length > 0 ? directEvidence : indexedEvidence;
20
+ return output(`Area '${area}' uses ${initPattern} as the primary bootstrap pattern and keeps selectors stable via project helpers.`, evidence, [
21
+ `Bootstrap the draft with ${initPattern}.`,
22
+ `Use ${selectPattern} when navigating to the feature under test.`,
23
+ ]);
24
+ }
25
+ function inferArea(feature) {
26
+ if (/constructor|arm|repository|application/i.test(feature)) {
27
+ return 'constructor';
28
+ }
29
+ return 'formRunner';
30
+ }
@@ -0,0 +1,23 @@
1
+ import type { AppContext } from '../appContext.js';
2
+ export declare function generateSpecBlueprint(input: {
3
+ feature: string;
4
+ area?: string;
5
+ }, context: AppContext): Promise<{
6
+ warnings: string[];
7
+ summary: string;
8
+ evidence: {
9
+ type: "symbol" | "config" | "file" | "test" | "artifact" | "report" | "diff";
10
+ label: string;
11
+ path?: string | undefined;
12
+ locator?: string | undefined;
13
+ snippet?: string | undefined;
14
+ metadata?: Record<string, unknown> | undefined;
15
+ }[];
16
+ recommended_actions: string[];
17
+ repo_paths: string[];
18
+ confidence: {
19
+ score: number;
20
+ level: "low" | "medium" | "high";
21
+ rationale: string[];
22
+ };
23
+ }>;
@@ -0,0 +1,213 @@
1
+ import path from 'node:path';
2
+ import { extractTestAssets } from '../indexer/extractors/testAssetExtractor.js';
3
+ import { safeRelative, toPosixPath } from '../utils/pathUtils.js';
4
+ import { evidenceFile, output } from './shared.js';
5
+ export async function generateSpecBlueprint(input, context) {
6
+ const assets = await extractTestAssets({
7
+ root: context.config.repos.autotests2,
8
+ featureHint: input.feature,
9
+ });
10
+ const area = input.area ?? inferAreaFromSpecs(assets.specs, input.feature);
11
+ const referenceSpecs = pickReferenceSpecs(assets.specs, area);
12
+ const referenceSpec = referenceSpecs[0];
13
+ const initIdentifier = area === 'constructor' ? 'initConf' : 'init';
14
+ const initExpose = area === 'constructor' ? 'appNameConf' : 'appVcm2Id';
15
+ const importTarget = buildImportTarget(referenceSpec?.path, context.config.repos.autotests2, area);
16
+ const hookPlan = buildHookPlan(referenceSpec, area);
17
+ const reuseCandidates = [
18
+ ...pickNamedCommands(assets.commands, ['getWidget', 'selectForm', 'selectAppInConstructor', 'waitVcmForm', 'waitFormsInFr', 'fixCypressSpec']),
19
+ ...pickHelpers(assets.helpers),
20
+ ];
21
+ const selectorPaths = assets.selectors
22
+ .filter((selector) => isFeatureSpecificSelector(selector.path, input.feature))
23
+ .slice(0, 3)
24
+ .map((selector) => toPosixPath(selector.path));
25
+ const selectorGuidance = selectorPaths.length > 0
26
+ ? `Prefer selectors already defined in ${selectorPaths.join(', ')} before adding new hooks.`
27
+ : 'Prefer cy.getWidget(...) plus the selectors tree from cypress/support/selectors/index.js; do not add new selectors under cypress/support/selectors/elements.';
28
+ const blueprint = buildBlueprint({
29
+ area,
30
+ feature: input.feature,
31
+ initIdentifier,
32
+ initExpose,
33
+ importTarget,
34
+ hookPlan,
35
+ referenceSpecPath: referenceSpec ? toPosixPath(referenceSpec.path) : undefined,
36
+ reuseCandidates,
37
+ selectorGuidance,
38
+ });
39
+ return output(`Generated a ${area}-oriented spec draft for ${input.feature} with reference-spec setup, import targets, reuse candidates, and selector guidance from autotests2.`, [
40
+ ...referenceSpecs.map((spec) => evidenceFile('reference spec', spec.path)),
41
+ ...reuseCandidates.slice(0, 4).map((candidate) => evidenceFile(candidate.label, candidate.path)),
42
+ ...selectorPaths.slice(0, 2).map((selectorPath) => evidenceFile('selector file', selectorPath)),
43
+ {
44
+ type: 'symbol',
45
+ label: 'blueprint',
46
+ snippet: blueprint,
47
+ },
48
+ ], [
49
+ `Import ${initIdentifier} from ${importTarget} and keep bootstrap at the describe level.`,
50
+ `Reuse the ${hookPlan.beforeSummary} before() pattern and the ${hookPlan.beforeEachSummary} beforeEach() pattern from ${referenceSpec ? toPosixPath(referenceSpec.path) : 'the closest area reference spec'}.`,
51
+ `Borrow commands/helpers from ${reuseCandidates.map((candidate) => candidate.name).join(', ')} before introducing new support code.`,
52
+ selectorGuidance,
53
+ ], assets.warnings);
54
+ }
55
+ function buildBlueprint(context) {
56
+ const tags = context.area === 'constructor'
57
+ ? `[ '@constructor', '@regress' ]`
58
+ : `[ '@vcm', '@regress', '@parallel-true' ]`;
59
+ const reuseLines = context.reuseCandidates.length > 0
60
+ ? context.reuseCandidates.map((candidate) => ` * - ${candidate.name}: ${toPosixPath(candidate.path)}`).join('\n')
61
+ : ' * - cy.getWidget / cy.waitVcmForm from cypress/support/commands';
62
+ const referenceComment = context.referenceSpecPath
63
+ ? ` * Reference spec: ${context.referenceSpecPath}`
64
+ : ' * Reference spec: inspect the closest area match in autotests2 before finalizing widget names.';
65
+ return `/**\n * Draft spec blueprint for ${context.feature}\n${referenceComment}\n * Suggested import target: ${context.importTarget}\n * Setup hints:\n * - ${context.hookPlan.beforeSummary}\n * - ${context.hookPlan.beforeEachSummary}\n * Reuse candidates:\n${reuseLines}\n * Selector guidance: ${context.selectorGuidance}\n */\nimport ${context.initIdentifier} from '${context.importTarget}';\n\ndescribe('[Overkill] ${context.feature}', { tags: ${tags} }, () => {\n ${context.initIdentifier}(Cypress.expose('${context.initExpose}'));\n\n${renderHook('before', context.hookPlan.beforeLines)}\n\n${renderHook('beforeEach', context.hookPlan.beforeEachLines)}\n\n const target = '<widget control name from reference spec>';\n\n it('covers the primary ${context.feature} flow', () => {\n cy.getWidget('<action widget from reference spec>').click();\n cy.getWidget(target).should('be.visible');\n cy.getWidget(target)\n .find('<stable inner selector or selector-tree locator>')\n .should('exist');\n });\n});`;
66
+ }
67
+ function renderHook(name, lines) {
68
+ const body = lines.map((line) => ` ${line}`).join('\n');
69
+ return ` ${name}(() => {\n${body}\n });`;
70
+ }
71
+ function buildHookPlan(referenceSpec, area) {
72
+ const beforeLines = sanitizeHookLines(extractHookLines(referenceSpec?.body, 'before'));
73
+ const beforeEachLines = sanitizeHookLines(extractHookLines(referenceSpec?.body, 'beforeEach'));
74
+ const fallbackBeforeLines = defaultBeforeLines(area);
75
+ const fallbackBeforeEachLines = defaultBeforeEachLines(area);
76
+ return {
77
+ beforeLines: beforeLines.length > 0 ? beforeLines : fallbackBeforeLines,
78
+ beforeEachLines: beforeEachLines.length > 0 ? beforeEachLines : fallbackBeforeEachLines,
79
+ beforeSummary: summarizeHook(beforeLines.length > 0 ? beforeLines : fallbackBeforeLines),
80
+ beforeEachSummary: summarizeHook(beforeEachLines.length > 0 ? beforeEachLines : fallbackBeforeEachLines),
81
+ };
82
+ }
83
+ function extractHookLines(body, hookName) {
84
+ if (!body) {
85
+ return [];
86
+ }
87
+ const patterns = [
88
+ new RegExp(`${hookName}\\(\\(\\) => \\{([\\s\\S]*?)\\n\\s*\\}\\);`),
89
+ new RegExp(`${hookName}\\(function \\(\\) \\{([\\s\\S]*?)\\n\\s*\\}\\);`),
90
+ ];
91
+ for (const pattern of patterns) {
92
+ const match = body.match(pattern);
93
+ if (match?.[1]) {
94
+ return match[1]
95
+ .split(/\r?\n/)
96
+ .map((line) => line.trim())
97
+ .filter(Boolean)
98
+ .filter((line) => !line.startsWith('//'));
99
+ }
100
+ }
101
+ return [];
102
+ }
103
+ function sanitizeHookLines(lines) {
104
+ return lines.map((line) => {
105
+ if (line.startsWith('cy.selectForm(')) {
106
+ return "cy.selectForm('<form name from reference spec>');";
107
+ }
108
+ if (line.startsWith('cy.selectAppInConstructor(')) {
109
+ return "cy.selectAppInConstructor('<app or branch from reference spec>');";
110
+ }
111
+ if (line.includes('.contains(')) {
112
+ return line
113
+ .replace(/cy\.getWidget\((?:'|")[^'"]+(?:'|")\)/, "cy.getWidget('<navigation widget from reference spec>')")
114
+ .replace(/\.contains\((?:'|")[^'"]+(?:'|")\)/, ".contains('<navigation text from reference spec>')");
115
+ }
116
+ if (line.startsWith('cy.getWidget(')) {
117
+ return "cy.getWidget('<widget control name from reference spec>').should('be.visible');";
118
+ }
119
+ return line;
120
+ });
121
+ }
122
+ function defaultBeforeLines(area) {
123
+ if (area === 'constructor') {
124
+ return [
125
+ 'cy.waitLoading();',
126
+ ];
127
+ }
128
+ return [
129
+ "cy.waitFormsInFr('<form count if the feature opens nested forms>');",
130
+ ];
131
+ }
132
+ function defaultBeforeEachLines(area) {
133
+ if (area === 'constructor') {
134
+ return [
135
+ "cy.selectAppInConstructor('<app or branch from reference spec>');",
136
+ 'cy.waitLoading();',
137
+ ];
138
+ }
139
+ return [
140
+ 'cy.waitVcmForm();',
141
+ 'cy.fixCypressSpec();',
142
+ "cy.selectForm('<form name from reference spec>');",
143
+ 'cy.waitVcmForm();',
144
+ "cy.getWidget('<widget control name from reference spec>').should('be.visible');",
145
+ ];
146
+ }
147
+ function summarizeHook(lines) {
148
+ return lines
149
+ .slice(0, 2)
150
+ .map((line) => line.replace(/;$/, ''))
151
+ .join(' + ');
152
+ }
153
+ function pickReferenceSpecs(specs, area) {
154
+ const inArea = specs.filter((spec) => toPosixPath(spec.path).includes(`/${area}/`));
155
+ return (inArea.length > 0 ? inArea : specs).slice(0, 3);
156
+ }
157
+ function inferAreaFromSpecs(specs, feature) {
158
+ const scored = specs.slice(0, 8).reduce((acc, spec) => {
159
+ const specPath = toPosixPath(spec.path);
160
+ if (specPath.includes('/constructor/')) {
161
+ acc.constructor += 1;
162
+ }
163
+ if (specPath.includes('/formRunner/')) {
164
+ acc.formRunner += 1;
165
+ }
166
+ return acc;
167
+ }, { constructor: 0, formRunner: 0 });
168
+ if (scored.formRunner > scored.constructor) {
169
+ return 'formRunner';
170
+ }
171
+ if (scored.constructor > scored.formRunner) {
172
+ return 'constructor';
173
+ }
174
+ return /constructor|arm|repository|application/i.test(feature) ? 'constructor' : 'formRunner';
175
+ }
176
+ function buildImportTarget(referenceSpecPath, autotestsRoot, area) {
177
+ const supportFile = area === 'constructor' ? 'baseConf.js' : 'base.js';
178
+ const fromDir = referenceSpecPath
179
+ ? path.dirname(referenceSpecPath)
180
+ : path.join(autotestsRoot, 'cypress', 'e2e', 'tests', area);
181
+ const toFile = path.join(autotestsRoot, 'cypress', 'support', supportFile);
182
+ const relativePath = safeRelative(fromDir, toFile).replace(/\.js$/, '');
183
+ return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
184
+ }
185
+ function pickNamedCommands(matches, preferredNames) {
186
+ const picked = [];
187
+ for (const preferredName of preferredNames) {
188
+ const match = matches.find((candidate) => candidate.name === preferredName);
189
+ if (match) {
190
+ picked.push({
191
+ name: match.name,
192
+ label: `command ${match.name}`,
193
+ path: match.path,
194
+ });
195
+ }
196
+ }
197
+ return picked;
198
+ }
199
+ function pickHelpers(matches) {
200
+ return matches.slice(0, 2).map((match) => ({
201
+ name: match.name,
202
+ label: `helper ${match.name}`,
203
+ path: match.path,
204
+ }));
205
+ }
206
+ function isFeatureSpecificSelector(selectorPath, feature) {
207
+ const normalizedPath = toPosixPath(selectorPath).toLowerCase();
208
+ return feature
209
+ .toLowerCase()
210
+ .split(/[\s_\-/]+/)
211
+ .filter((token) => token.length >= 4)
212
+ .some((token) => normalizedPath.includes(token));
213
+ }
@@ -0,0 +1,22 @@
1
+ import type { AppContext } from '../appContext.js';
2
+ export declare function mapFeatureToExistingCoverage(input: {
3
+ widget: string;
4
+ }, context: AppContext): Promise<{
5
+ warnings: string[];
6
+ summary: string;
7
+ evidence: {
8
+ type: "symbol" | "config" | "file" | "test" | "artifact" | "report" | "diff";
9
+ label: string;
10
+ path?: string | undefined;
11
+ locator?: string | undefined;
12
+ snippet?: string | undefined;
13
+ metadata?: Record<string, unknown> | undefined;
14
+ }[];
15
+ recommended_actions: string[];
16
+ repo_paths: string[];
17
+ confidence: {
18
+ score: number;
19
+ level: "low" | "medium" | "high";
20
+ rationale: string[];
21
+ };
22
+ }>;
@@ -0,0 +1,56 @@
1
+ import { evidenceFile } from './shared.js';
2
+ import { extractTestAssets } from '../indexer/extractors/testAssetExtractor.js';
3
+ import { output } from './shared.js';
4
+ export async function mapFeatureToExistingCoverage(input, context) {
5
+ const assets = await extractTestAssets({
6
+ root: context.config.repos.autotests2,
7
+ featureHint: input.widget,
8
+ });
9
+ const sourceFiles = await context.caseplatform.resolveFeatureFiles(input.widget);
10
+ const evidence = assets.specs.slice(0, 8).map((spec) => evidenceFile('coverage spec', spec.path, spec.body.slice(0, 180), { score: spec.score }));
11
+ const sourceEvidence = selectSourceHints(sourceFiles.primaryFiles, context).slice(0, 4).map((filePath) => evidenceFile('source hint', filePath, undefined, { kind: context.caseplatform.classifyFileKind(filePath) }));
12
+ return output(`Mapped ${assets.specs.length} existing specs to ${input.widget}${sourceEvidence.length > 0 ? ` and found ${sourceEvidence.length} caseplatform source hint(s)` : ''}.`, [...evidence, ...sourceEvidence], [
13
+ 'Prioritize the highest-scoring specs as reuse candidates.',
14
+ sourceEvidence.length > 0
15
+ ? 'Use attached caseplatform source hints to inspect implementation before expanding coverage.'
16
+ : 'If implementation sources are unclear, run frontend contract/runtime tools explicitly.',
17
+ 'Check whether container and non-container coverage are both present.',
18
+ ], assets.warnings);
19
+ }
20
+ function selectSourceHints(filePaths, context) {
21
+ const byKind = new Map();
22
+ for (const filePath of filePaths) {
23
+ const kind = context.caseplatform.classifyFileKind(filePath);
24
+ byKind.set(kind, [...(byKind.get(kind) ?? []), filePath]);
25
+ }
26
+ const rankedViews = rankViewHints(byKind.get('view') ?? []);
27
+ return [
28
+ ...rankedViews.slice(0, 2),
29
+ ...(byKind.get('controller') ?? []).slice(0, 1),
30
+ ...(byKind.get('model') ?? []).slice(0, 1),
31
+ ...filePaths,
32
+ ].filter((filePath, index, items) => items.indexOf(filePath) === index);
33
+ }
34
+ function rankViewHints(filePaths) {
35
+ return filePaths.slice().sort((left, right) => scoreViewHint(right) - scoreViewHint(left));
36
+ }
37
+ function scoreViewHint(filePath) {
38
+ const normalized = filePath.replaceAll('\\', '/').toLowerCase();
39
+ let score = 0;
40
+ if (normalized.endsWith('/tabpanel.js')) {
41
+ score += 6;
42
+ }
43
+ if (normalized.endsWith('/grid.js')) {
44
+ score += 5;
45
+ }
46
+ if (normalized.endsWith('/form.js')) {
47
+ score += 4;
48
+ }
49
+ if (normalized.includes('/apperror/')) {
50
+ score += 3;
51
+ }
52
+ if (normalized.includes('/application/')) {
53
+ score += 2;
54
+ }
55
+ return score;
56
+ }
@@ -0,0 +1,22 @@
1
+ import type { AppContext } from '../appContext.js';
2
+ export declare function recommendSelectorStrategy(input: {
3
+ feature: string;
4
+ }, context: AppContext): Promise<{
5
+ warnings: string[];
6
+ summary: string;
7
+ evidence: {
8
+ type: "symbol" | "config" | "file" | "test" | "artifact" | "report" | "diff";
9
+ label: string;
10
+ path?: string | undefined;
11
+ locator?: string | undefined;
12
+ snippet?: string | undefined;
13
+ metadata?: Record<string, unknown> | undefined;
14
+ }[];
15
+ recommended_actions: string[];
16
+ repo_paths: string[];
17
+ confidence: {
18
+ score: number;
19
+ level: "low" | "medium" | "high";
20
+ rationale: string[];
21
+ };
22
+ }>;
@@ -0,0 +1,13 @@
1
+ import { evidenceFile, output } from './shared.js';
2
+ export async function recommendSelectorStrategy(input, context) {
3
+ const selectors = await context.autotests2.findSelectors(input.feature);
4
+ const commands = await context.autotests2.findCommands();
5
+ const getWidgetCommand = commands.find((command) => command.name === 'getWidget');
6
+ return output(`Selector strategy for ${input.feature}: prefer project widget helpers and stable testguid-backed selectors.`, [
7
+ ...(getWidgetCommand ? [evidenceFile('getWidget command', getWidgetCommand.path)] : []),
8
+ ...selectors.slice(0, 4).map((selector) => evidenceFile('selector file', selector.path)),
9
+ ], [
10
+ 'Start with cy.getWidget(...) for runtime widgets.',
11
+ 'Prefer selectors tree and testguid-based hooks over DOM-order selectors.',
12
+ ], selectors.length === 0 ? [`No dedicated selector file matched '${input.feature}', so fallback to getWidget is recommended.`] : []);
13
+ }
@@ -0,0 +1,5 @@
1
+ import type { AppContext } from '../appContext.js';
2
+ import { type EvidenceItem, type NormalizedToolOutput } from '../contracts/toolOutput.js';
3
+ export declare function ensureIndexed(context: AppContext): Promise<void>;
4
+ export declare function evidenceFile(label: string, filePath: string, snippet?: string, metadata?: Record<string, unknown>): EvidenceItem;
5
+ export declare function output(summary: string, evidence: EvidenceItem[], recommendedActions: string[], warnings?: string[], repoPaths?: string[]): NormalizedToolOutput;
@@ -0,0 +1,36 @@
1
+ import { buildToolOutput } from '../contracts/toolOutput.js';
2
+ import { confidenceFromEvidence } from '../contracts/confidence.js';
3
+ import { refreshKnowledge } from '../indexer/refreshPipeline.js';
4
+ import { countDocuments } from '../knowledge/repositories.js';
5
+ import { unique } from '../utils/pathUtils.js';
6
+ let indexedContexts = new WeakSet();
7
+ export async function ensureIndexed(context) {
8
+ if (indexedContexts.has(context)) {
9
+ return;
10
+ }
11
+ if (countDocuments(context.database) > 0) {
12
+ indexedContexts.add(context);
13
+ return;
14
+ }
15
+ await refreshKnowledge(context);
16
+ indexedContexts.add(context);
17
+ }
18
+ export function evidenceFile(label, filePath, snippet, metadata) {
19
+ return {
20
+ type: 'file',
21
+ label,
22
+ path: filePath,
23
+ snippet,
24
+ metadata,
25
+ };
26
+ }
27
+ export function output(summary, evidence, recommendedActions, warnings = [], repoPaths) {
28
+ return buildToolOutput({
29
+ summary,
30
+ evidence,
31
+ recommended_actions: recommendedActions,
32
+ repo_paths: unique(repoPaths ?? evidence.flatMap((item) => item.path ? [item.path] : [])),
33
+ confidence: confidenceFromEvidence(evidence.length, warnings),
34
+ warnings,
35
+ });
36
+ }
@@ -0,0 +1,22 @@
1
+ import type { AppContext } from '../appContext.js';
2
+ export declare function summarizeCiContext(input: {
3
+ statsFile: string;
4
+ }, context: AppContext): Promise<{
5
+ warnings: string[];
6
+ summary: string;
7
+ evidence: {
8
+ type: "symbol" | "config" | "file" | "test" | "artifact" | "report" | "diff";
9
+ label: string;
10
+ path?: string | undefined;
11
+ locator?: string | undefined;
12
+ snippet?: string | undefined;
13
+ metadata?: Record<string, unknown> | undefined;
14
+ }[];
15
+ recommended_actions: string[];
16
+ repo_paths: string[];
17
+ confidence: {
18
+ score: number;
19
+ level: "low" | "medium" | "high";
20
+ rationale: string[];
21
+ };
22
+ }>;
@@ -0,0 +1,35 @@
1
+ import { evidenceFile, output } from './shared.js';
2
+ export async function summarizeCiContext(input, context) {
3
+ const stats = context.ciArtifacts.readStats(input.statsFile);
4
+ const [reportPortal, gitlab] = await Promise.all([
5
+ context.reportPortal.describeLaunch(stats),
6
+ context.gitlab.describePipeline(),
7
+ ]);
8
+ const warnings = [
9
+ ...reportPortal.warnings,
10
+ ...gitlab.warnings,
11
+ ];
12
+ return output(`CI context: ${stats.total ?? 0} tests, ${stats.passed ?? 0} passed, ${stats.failed ?? 0} failed, ${stats.skipped ?? 0} skipped.`, [
13
+ evidenceFile('stats report', input.statsFile, JSON.stringify(stats).slice(0, 220)),
14
+ ...(reportPortal.launchLink ? [{
15
+ type: 'report',
16
+ label: reportPortal.launchStatus ? `ReportPortal launch (${reportPortal.launchStatus})` : 'ReportPortal launch',
17
+ snippet: reportPortal.launchLink,
18
+ metadata: { source: reportPortal.source, launchId: reportPortal.launchId },
19
+ }] : []),
20
+ ...(gitlab.pipelineUrl ? [{
21
+ type: 'report',
22
+ label: gitlab.pipelineStatus ? `GitLab pipeline (${gitlab.pipelineStatus})` : 'GitLab pipeline',
23
+ snippet: gitlab.pipelineUrl,
24
+ metadata: { source: gitlab.source },
25
+ }] : []),
26
+ ], [
27
+ reportPortal.launchLink
28
+ ? 'Use the ReportPortal launch link when deeper run metadata is needed.'
29
+ : 'Rely on local artifact metadata first; add ReportPortal config only when live launch enrichment is needed.',
30
+ gitlab.pipelineUrl
31
+ ? 'Use the GitLab pipeline link when you need upstream CI context.'
32
+ : 'Rely on local artifacts first; add GitLab projectId/token only when live pipeline lookup is required.',
33
+ 'Combine this summary with artifact triage before changing test code.',
34
+ ], warnings);
35
+ }
@@ -0,0 +1,25 @@
1
+ import type { AppContext } from '../appContext.js';
2
+ export declare function triageFailedRun(input: {
3
+ statsFile: string;
4
+ screenshotsRoot: string;
5
+ videosRoot: string;
6
+ apiCaptureRoot: string;
7
+ }, context: AppContext): Promise<{
8
+ warnings: string[];
9
+ summary: string;
10
+ evidence: {
11
+ type: "symbol" | "config" | "file" | "test" | "artifact" | "report" | "diff";
12
+ label: string;
13
+ path?: string | undefined;
14
+ locator?: string | undefined;
15
+ snippet?: string | undefined;
16
+ metadata?: Record<string, unknown> | undefined;
17
+ }[];
18
+ recommended_actions: string[];
19
+ repo_paths: string[];
20
+ confidence: {
21
+ score: number;
22
+ level: "low" | "medium" | "high";
23
+ rationale: string[];
24
+ };
25
+ }>;
@@ -0,0 +1,46 @@
1
+ import fs from 'node:fs';
2
+ import { evidenceFile, output } from './shared.js';
3
+ export async function triageFailedRun(input, context) {
4
+ const stats = context.ciArtifacts.readStats(input.statsFile);
5
+ const [reportPortal, gitlab] = await Promise.all([
6
+ context.reportPortal.describeLaunch(stats),
7
+ context.gitlab.describePipeline(),
8
+ ]);
9
+ const screenshots = context.ciArtifacts.listArtifacts(input.screenshotsRoot, '**/*.png');
10
+ const videos = context.ciArtifacts.listArtifacts(input.videosRoot, '**/*.mp4');
11
+ const apiCaptures = context.ciArtifacts.listArtifacts(input.apiCaptureRoot, '**/*.json');
12
+ const failedSpecs = stats.failed_specs ?? [];
13
+ const repeatedAttempts = screenshots.filter((filePath) => filePath.includes('attempt 2')).length;
14
+ const warnings = failedSpecs.length === 0
15
+ ? ['Current stats file reports zero failed specs; using persisted screenshots and artifact residue for triage context.']
16
+ : [];
17
+ warnings.push(...reportPortal.warnings, ...gitlab.warnings);
18
+ const actions = [
19
+ failedSpecs.length > 0 ? 'Rerun only the failed specs first.' : 'Start from the specs named in persisted screenshots before broad reruns.',
20
+ repeatedAttempts > 0 ? 'Inspect flaky or environment-sensitive failures because repeated attempts are present.' : 'Inspect first-failure artifacts before assuming flakiness.',
21
+ apiCaptures.length > 0 ? 'Compare API capture payloads for the affected run before editing selectors.' : 'Capture API traffic on the next rerun to reduce ambiguity.',
22
+ ];
23
+ return output(`CI triage summary: stats show ${stats.total ?? 0} tests, ${stats.failed ?? 0} current failures, ${screenshots.length} screenshots, ${videos.length} videos, and ${apiCaptures.length} captured API artifacts.`, [
24
+ evidenceFile('stats report', input.statsFile, JSON.stringify({
25
+ total: stats.total,
26
+ passed: stats.passed,
27
+ failed: stats.failed,
28
+ launch_id: stats.launch_id,
29
+ })),
30
+ ...failedSpecs.slice(0, 5).map((specPath) => ({ type: 'test', label: 'failed spec', path: specPath })),
31
+ ...screenshots.slice(0, 5).map((filePath) => evidenceFile('failure screenshot', filePath, fs.existsSync(filePath) ? undefined : 'missing')),
32
+ ...videos.slice(0, 3).map((filePath) => evidenceFile('failure video', filePath)),
33
+ ...(reportPortal.launchLink ? [{
34
+ type: 'report',
35
+ label: reportPortal.launchStatus ? `ReportPortal launch (${reportPortal.launchStatus})` : 'ReportPortal launch',
36
+ snippet: reportPortal.launchLink,
37
+ metadata: { source: reportPortal.source, launchId: reportPortal.launchId },
38
+ }] : []),
39
+ ...(gitlab.pipelineUrl ? [{
40
+ type: 'report',
41
+ label: gitlab.pipelineStatus ? `GitLab pipeline (${gitlab.pipelineStatus})` : 'GitLab pipeline',
42
+ snippet: gitlab.pipelineUrl,
43
+ metadata: { source: gitlab.source },
44
+ }] : []),
45
+ ], actions, warnings);
46
+ }
@@ -0,0 +1,4 @@
1
+ export * from './appContext.js';
2
+ export * from './config/consumerConfig.js';
3
+ export * from './contracts/toolOutput.js';
4
+ export * from './mcp/server.js';
@@ -0,0 +1,4 @@
1
+ export * from './appContext.js';
2
+ export * from './config/consumerConfig.js';
3
+ export * from './contracts/toolOutput.js';
4
+ export * from './mcp/server.js';
@@ -0,0 +1,17 @@
1
+ export type FrontendContractExtraction = {
2
+ widgetName: string;
3
+ properties: string[];
4
+ methods: string[];
5
+ events: string[];
6
+ files: string[];
7
+ snippets: Array<{
8
+ label: string;
9
+ path: string;
10
+ snippet: string;
11
+ }>;
12
+ warnings: string[];
13
+ };
14
+ export declare function extractFrontendContract(input: {
15
+ root: string;
16
+ widgetHint: string;
17
+ }): Promise<FrontendContractExtraction>;