@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,63 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { z } from 'zod';
4
+ const reportPortalSchema = z.object({
5
+ endpoint: z.string().min(1),
6
+ project: z.string().min(1).optional(),
7
+ token: z.string().min(1).optional(),
8
+ }).strict();
9
+ const gitlabSchema = z.object({
10
+ baseUrl: z.string().min(1),
11
+ projectId: z.string().min(1).optional(),
12
+ token: z.string().min(1).optional(),
13
+ }).strict();
14
+ export const consumerConfigSchema = z.object({
15
+ repos: z.object({
16
+ autotests2: z.string().min(1),
17
+ caseplatformWeb: z.string().min(1),
18
+ }).strict(),
19
+ ci: z.object({
20
+ artifactsRoot: z.string().min(1),
21
+ reportPortal: reportPortalSchema.optional(),
22
+ gitlab: gitlabSchema.optional(),
23
+ }).strict(),
24
+ cacheDir: z.string().min(1).optional(),
25
+ projectHints: z.object({
26
+ defaultWidget: z.string().min(1).optional(),
27
+ }).strict().optional(),
28
+ }).strict();
29
+ export function resolveRepoPath(value, baseDir = process.cwd()) {
30
+ return path.resolve(baseDir, value);
31
+ }
32
+ export function parseConsumerConfig(input, baseDir = process.cwd()) {
33
+ const parsed = consumerConfigSchema.parse(input);
34
+ return {
35
+ ...parsed,
36
+ repos: {
37
+ autotests2: resolveRepoPath(parsed.repos.autotests2, baseDir),
38
+ caseplatformWeb: resolveRepoPath(parsed.repos.caseplatformWeb, baseDir),
39
+ },
40
+ ci: {
41
+ artifactsRoot: resolveRepoPath(parsed.ci.artifactsRoot, baseDir),
42
+ reportPortal: parsed.ci.reportPortal,
43
+ gitlab: parsed.ci.gitlab,
44
+ },
45
+ cacheDir: resolveRepoPath(parsed.cacheDir ?? '.overkill-cache', baseDir),
46
+ };
47
+ }
48
+ export function loadConsumerConfig(configPath = path.resolve(process.cwd(), 'overkill.config.json')) {
49
+ const raw = fs.readFileSync(configPath, 'utf8');
50
+ return parseConsumerConfig(JSON.parse(raw), path.dirname(configPath));
51
+ }
52
+ export function createDefaultConsumerConfig(baseDir = process.cwd()) {
53
+ return parseConsumerConfig({
54
+ repos: {
55
+ autotests2: '../autotests2',
56
+ caseplatformWeb: '../caseplatform-web',
57
+ },
58
+ ci: {
59
+ artifactsRoot: '../autotests2/cypress',
60
+ },
61
+ cacheDir: '.overkill-cache',
62
+ }, baseDir);
63
+ }
@@ -0,0 +1,41 @@
1
+ export type TestSpecMatch = {
2
+ path: string;
3
+ title: string;
4
+ body: string;
5
+ score: number;
6
+ };
7
+ export type CommandMatch = {
8
+ path: string;
9
+ name: string;
10
+ body: string;
11
+ };
12
+ export type HelperMatch = {
13
+ path: string;
14
+ name: string;
15
+ body: string;
16
+ };
17
+ export declare class Autotests2Connector {
18
+ private readonly root;
19
+ private readonly fs;
20
+ constructor(root: string);
21
+ findSpecs(featureHint?: string): Promise<TestSpecMatch[]>;
22
+ findCommands(): Promise<CommandMatch[]>;
23
+ findHelpers(featureHint?: string): Promise<HelperMatch[]>;
24
+ getAreaDocs(): {
25
+ contextPath: string;
26
+ testingPath: string;
27
+ contextText: string;
28
+ testingText: string;
29
+ supportBaseText: string;
30
+ supportBaseConfText: string;
31
+ };
32
+ findSelectors(featureHint?: string): Promise<{
33
+ path: string;
34
+ body: string;
35
+ }[]>;
36
+ summarizeSpec(spec: TestSpecMatch): {
37
+ path: string;
38
+ describeNames: string[];
39
+ itNames: string[];
40
+ };
41
+ }
@@ -0,0 +1,94 @@
1
+ import path from 'node:path';
2
+ import { FilesystemConnector } from './filesystemConnector.js';
3
+ import { basenameWithoutExt, pathIncludes, toPosixPath } from '../utils/pathUtils.js';
4
+ import { extractMatches, scoreTextMatch } from '../utils/textUtils.js';
5
+ export class Autotests2Connector {
6
+ root;
7
+ fs = new FilesystemConnector();
8
+ constructor(root) {
9
+ this.root = root;
10
+ }
11
+ async findSpecs(featureHint) {
12
+ const files = await this.fs.glob(this.root, ['cypress/e2e/tests/**/*.cy.js']);
13
+ const results = [];
14
+ for (const filePath of files) {
15
+ const body = this.fs.read(filePath);
16
+ const score = featureHint ? scoreTextMatch(filePath, featureHint) + scoreTextMatch(body, featureHint) : 1;
17
+ if (score > 0) {
18
+ results.push({
19
+ path: filePath,
20
+ title: basenameWithoutExt(filePath),
21
+ body,
22
+ score,
23
+ });
24
+ }
25
+ }
26
+ return results.sort((left, right) => right.score - left.score);
27
+ }
28
+ async findCommands() {
29
+ const files = await this.fs.glob(this.root, ['cypress/support/**/*.js']);
30
+ const results = [];
31
+ for (const filePath of files) {
32
+ const body = this.fs.read(filePath);
33
+ const names = extractMatches(body, /Cypress\.Commands\.add\((?:'|")([^'"]+)(?:'|")/g);
34
+ for (const name of names) {
35
+ results.push({ path: filePath, name, body });
36
+ }
37
+ if (pathIncludes(filePath, 'widget') || body.includes('getWidget')) {
38
+ results.push({ path: filePath, name: 'getWidget', body });
39
+ }
40
+ }
41
+ return dedupeByName(results);
42
+ }
43
+ async findHelpers(featureHint) {
44
+ const files = await this.fs.glob(this.root, ['cypress/support/functions/**/*.js']);
45
+ const results = [];
46
+ for (const filePath of files) {
47
+ const body = this.fs.read(filePath);
48
+ const names = [
49
+ ...extractMatches(body, /export function ([A-Za-z0-9_]+)/g),
50
+ ...extractMatches(body, /function ([A-Za-z0-9_]+)/g),
51
+ ];
52
+ for (const name of names) {
53
+ if (!featureHint || scoreTextMatch(`${filePath}\n${body}`, featureHint) > 0) {
54
+ results.push({ path: filePath, name, body });
55
+ }
56
+ }
57
+ }
58
+ return dedupeByName(results);
59
+ }
60
+ getAreaDocs() {
61
+ return {
62
+ contextPath: path.join(this.root, '.codex', 'context.md'),
63
+ testingPath: path.join(this.root, '.codex', 'testing.md'),
64
+ contextText: this.fs.read(path.join(this.root, '.codex', 'context.md')),
65
+ testingText: this.fs.read(path.join(this.root, '.codex', 'testing.md')),
66
+ supportBaseText: this.fs.read(path.join(this.root, 'cypress', 'support', 'base.js')),
67
+ supportBaseConfText: this.fs.read(path.join(this.root, 'cypress', 'support', 'baseConf.js')),
68
+ };
69
+ }
70
+ async findSelectors(featureHint) {
71
+ const files = await this.fs.glob(this.root, ['cypress/support/selectors/**/*.js']);
72
+ return files
73
+ .map((filePath) => ({ path: filePath, body: this.fs.read(filePath) }))
74
+ .filter((file) => !featureHint || scoreTextMatch(`${file.path}\n${file.body}`, featureHint) > 0);
75
+ }
76
+ summarizeSpec(spec) {
77
+ const describeNames = extractMatches(spec.body, /describe\((?:'|")([^'"]+)/g);
78
+ const itNames = extractMatches(spec.body, /it\((?:'|")([^'"]+)/g);
79
+ return {
80
+ path: toPosixPath(spec.path),
81
+ describeNames,
82
+ itNames,
83
+ };
84
+ }
85
+ }
86
+ function dedupeByName(items) {
87
+ const seen = new Map();
88
+ for (const item of items) {
89
+ if (!seen.has(item.name)) {
90
+ seen.set(item.name, item);
91
+ }
92
+ }
93
+ return [...seen.values()];
94
+ }
@@ -0,0 +1,35 @@
1
+ export type WidgetFiles = {
2
+ controlFile?: string;
3
+ singletonFile?: string;
4
+ handlersFile?: string;
5
+ templateFile?: string;
6
+ dataControlFile?: string;
7
+ supportFiles: string[];
8
+ allFiles: string[];
9
+ };
10
+ export type FeatureFiles = WidgetFiles & {
11
+ mode: 'widget' | 'conf' | 'unknown';
12
+ primaryFiles: string[];
13
+ };
14
+ export type IndexableCaseplatformSource = {
15
+ path: string;
16
+ title: string;
17
+ body: string;
18
+ sourceKind: string;
19
+ metadata: Record<string, unknown>;
20
+ };
21
+ export declare class CaseplatformConnector {
22
+ private readonly root;
23
+ private readonly fs;
24
+ constructor(root: string);
25
+ listIndexableSources(): Promise<IndexableCaseplatformSource[]>;
26
+ resolveFeatureFiles(featureHint: string): Promise<FeatureFiles>;
27
+ resolveWidgetFiles(widgetHint: string): Promise<WidgetFiles>;
28
+ read(filePath: string): string;
29
+ classifyFileKind(filePath: string): "widget" | "support" | "controller" | "view" | "model" | "store";
30
+ classifyIndexSourceKind(filePath: string): "frontend-widget" | "frontend-formrunner" | "conf-view" | "conf-controller" | "conf-model" | "conf-store" | "conf-support" | "frontend-support" | "caseplatform-support";
31
+ private resolveImportedSupportFiles;
32
+ private resolveImportPath;
33
+ private isSupportFile;
34
+ private resolveConfFiles;
35
+ }
@@ -0,0 +1,251 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { FilesystemConnector } from './filesystemConnector.js';
4
+ import { featureNameCandidates, pathIncludes, unique, widgetNameCandidates } from '../utils/pathUtils.js';
5
+ import { scoreTextMatch } from '../utils/textUtils.js';
6
+ export class CaseplatformConnector {
7
+ root;
8
+ fs = new FilesystemConnector();
9
+ constructor(root) {
10
+ this.root = root;
11
+ }
12
+ async listIndexableSources() {
13
+ const files = await this.fs.glob(this.root, [
14
+ 'vcm/**/*.{ts,tsx,js}',
15
+ 'FormRunner/**/*.{ts,tsx,js}',
16
+ 'conf/**/*.{ts,tsx,js}',
17
+ ]);
18
+ return unique(files)
19
+ .map((filePath) => ({
20
+ path: filePath,
21
+ title: path.basename(filePath),
22
+ body: this.read(filePath),
23
+ sourceKind: this.classifyIndexSourceKind(filePath),
24
+ metadata: {
25
+ fileKind: this.classifyFileKind(filePath),
26
+ },
27
+ }))
28
+ .filter((source) => source.body.trim().length > 0);
29
+ }
30
+ async resolveFeatureFiles(featureHint) {
31
+ const widgetFiles = await this.resolveWidgetFiles(featureHint);
32
+ if (widgetFiles.allFiles.length > 0) {
33
+ return {
34
+ mode: 'widget',
35
+ primaryFiles: [widgetFiles.controlFile, widgetFiles.singletonFile, widgetFiles.handlersFile, widgetFiles.templateFile].filter(Boolean),
36
+ ...widgetFiles,
37
+ };
38
+ }
39
+ const confFiles = await this.resolveConfFiles(featureHint);
40
+ if (confFiles.allFiles.length > 0) {
41
+ return confFiles;
42
+ }
43
+ return {
44
+ mode: 'unknown',
45
+ primaryFiles: [],
46
+ supportFiles: [],
47
+ allFiles: [],
48
+ };
49
+ }
50
+ async resolveWidgetFiles(widgetHint) {
51
+ const files = await this.fs.glob(this.root, [
52
+ 'vcm/jqVCM/widget/**/*.{ts,tsx,js}',
53
+ 'FormRunner/**/*.{ts,tsx,js}',
54
+ ]);
55
+ const candidates = widgetNameCandidates(widgetHint);
56
+ const matching = files.filter((filePath) => candidates.some((candidate) => pathIncludes(filePath, candidate)));
57
+ const rankFile = (filePath) => candidates.reduce((score, candidate) => score + scoreTextMatch(filePath, candidate), 0);
58
+ const ordered = matching.sort((left, right) => rankFile(right) - rankFile(left));
59
+ const dataControlFile = ordered.find((filePath) => /DataControl\.(ts|js)$/i.test(filePath));
60
+ const controlFile = ordered.find((filePath) => /(?:Control|_control)\.(ts|js)$/i.test(filePath) && filePath !== dataControlFile);
61
+ const singletonFile = ordered.find((filePath) => /(?:Singleton|_singleton)\.(ts|js)$/i.test(filePath));
62
+ const handlersFile = ordered.find((filePath) => /(?:Handlers|_handlers)\.(ts|js)$/i.test(filePath));
63
+ const templateFile = ordered.find((filePath) => /Templates\.(tsx|ts|js)$/i.test(filePath));
64
+ const widgetDirectory = controlFile
65
+ ? controlFile.replace(/[\\/][^\\/]+$/, '')
66
+ : singletonFile
67
+ ? singletonFile.replace(/[\\/][^\\/]+$/, '')
68
+ : ordered[0]?.replace(/[\\/][^\\/]+$/, '');
69
+ const localSupportFiles = widgetDirectory
70
+ ? files
71
+ .filter((filePath) => filePath.startsWith(widgetDirectory))
72
+ .filter((filePath) => ![controlFile, singletonFile, handlersFile, templateFile, dataControlFile].includes(filePath))
73
+ .filter((filePath) => this.isSupportFile(filePath))
74
+ : [];
75
+ const importedSupportFiles = this.resolveImportedSupportFiles([
76
+ controlFile,
77
+ singletonFile,
78
+ handlersFile,
79
+ templateFile,
80
+ dataControlFile,
81
+ ...localSupportFiles,
82
+ ].filter(Boolean));
83
+ const supportFiles = unique([...localSupportFiles, ...importedSupportFiles]);
84
+ const allFiles = unique([...ordered, ...supportFiles]);
85
+ return {
86
+ controlFile,
87
+ singletonFile,
88
+ handlersFile,
89
+ templateFile,
90
+ dataControlFile,
91
+ supportFiles,
92
+ allFiles,
93
+ };
94
+ }
95
+ read(filePath) {
96
+ return this.fs.read(filePath);
97
+ }
98
+ classifyFileKind(filePath) {
99
+ const normalized = filePath.replaceAll('\\', '/').toLowerCase();
100
+ if (normalized.includes('/controller/')) {
101
+ return 'controller';
102
+ }
103
+ if (normalized.includes('/view/')) {
104
+ return 'view';
105
+ }
106
+ if (normalized.includes('/model/')) {
107
+ return 'model';
108
+ }
109
+ if (normalized.includes('/store/')) {
110
+ return 'store';
111
+ }
112
+ if (normalized.includes('/widget/')) {
113
+ return 'widget';
114
+ }
115
+ return 'support';
116
+ }
117
+ classifyIndexSourceKind(filePath) {
118
+ const normalized = filePath.replaceAll('\\', '/').toLowerCase();
119
+ if (normalized.includes('/vcm/jqvcm/widget/')) {
120
+ return 'frontend-widget';
121
+ }
122
+ if (normalized.includes('/formrunner/')) {
123
+ return 'frontend-formrunner';
124
+ }
125
+ if (normalized.includes('/conf/app/view/')) {
126
+ return 'conf-view';
127
+ }
128
+ if (normalized.includes('/conf/app/controller/')) {
129
+ return 'conf-controller';
130
+ }
131
+ if (normalized.includes('/conf/app/model/')) {
132
+ return 'conf-model';
133
+ }
134
+ if (normalized.includes('/conf/app/store/')) {
135
+ return 'conf-store';
136
+ }
137
+ if (normalized.includes('/conf/')) {
138
+ return 'conf-support';
139
+ }
140
+ if (normalized.includes('/vcm/')) {
141
+ return 'frontend-support';
142
+ }
143
+ return 'caseplatform-support';
144
+ }
145
+ resolveImportedSupportFiles(seedFiles) {
146
+ const visited = new Set();
147
+ const supportFiles = [];
148
+ const queue = [...seedFiles];
149
+ while (queue.length > 0) {
150
+ const currentFile = queue.shift();
151
+ if (!currentFile || visited.has(currentFile)) {
152
+ continue;
153
+ }
154
+ visited.add(currentFile);
155
+ const body = this.read(currentFile);
156
+ if (!body) {
157
+ continue;
158
+ }
159
+ const specifiers = [
160
+ ...body.matchAll(/(?:import|export)\s+(?:[^'"]+?\s+from\s+)?['"]([^'"]+)['"]/g),
161
+ ].map((match) => match[1]).filter(Boolean);
162
+ for (const specifier of specifiers) {
163
+ const resolvedFile = this.resolveImportPath(currentFile, specifier);
164
+ if (!resolvedFile || !this.isSupportFile(resolvedFile)) {
165
+ continue;
166
+ }
167
+ supportFiles.push(resolvedFile);
168
+ queue.push(resolvedFile);
169
+ }
170
+ }
171
+ return unique(supportFiles);
172
+ }
173
+ resolveImportPath(fromFile, specifier) {
174
+ let rawPath;
175
+ if (specifier.startsWith('.')) {
176
+ rawPath = path.resolve(path.dirname(fromFile), specifier);
177
+ }
178
+ else if (specifier.startsWith('@VCM/')) {
179
+ rawPath = path.resolve(this.root, 'vcm', specifier.slice('@VCM/'.length));
180
+ }
181
+ else if (specifier.startsWith('@CONF/')) {
182
+ rawPath = path.resolve(this.root, 'conf', specifier.slice('@CONF/'.length));
183
+ }
184
+ if (!rawPath) {
185
+ return undefined;
186
+ }
187
+ const candidates = [
188
+ rawPath,
189
+ `${rawPath}.ts`,
190
+ `${rawPath}.tsx`,
191
+ `${rawPath}.js`,
192
+ path.join(rawPath, 'index.ts'),
193
+ path.join(rawPath, 'index.tsx'),
194
+ path.join(rawPath, 'index.js'),
195
+ ];
196
+ return candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
197
+ }
198
+ isSupportFile(filePath) {
199
+ const normalized = filePath.replaceAll('\\', '/').toLowerCase();
200
+ return normalized.includes('/widget/utils/')
201
+ || /(constants|validation|replace|helper|template|data|config|day)/i.test(path.basename(normalized));
202
+ }
203
+ async resolveConfFiles(featureHint) {
204
+ const files = await this.fs.glob(this.root, [
205
+ 'conf/**/*.{ts,tsx,js}',
206
+ ]);
207
+ const candidates = featureNameCandidates(featureHint);
208
+ const scored = files
209
+ .map((filePath) => {
210
+ const body = this.read(filePath);
211
+ const pathScore = candidates.reduce((score, candidate) => score + scoreTextMatch(filePath, candidate), 0);
212
+ const bodyScore = candidates.reduce((score, candidate) => score + scoreTextMatch(body, candidate), 0);
213
+ const kindBoost = this.classifyFileKind(filePath) === 'view' ? 3 : this.classifyFileKind(filePath) === 'controller' ? 2 : 1;
214
+ return {
215
+ path: filePath,
216
+ body,
217
+ score: pathScore * 2 + bodyScore + kindBoost,
218
+ };
219
+ })
220
+ .filter((item) => item.score > 0)
221
+ .sort((left, right) => right.score - left.score);
222
+ const primaryFiles = pickDiverseConfFiles(scored, this);
223
+ const importedSupportFiles = this.resolveImportedSupportFiles(primaryFiles)
224
+ .filter((filePath) => filePath.replaceAll('\\', '/').includes('/conf/'));
225
+ const supportFiles = unique(importedSupportFiles.filter((filePath) => !primaryFiles.includes(filePath)).slice(0, 6));
226
+ const allFiles = unique([...primaryFiles, ...supportFiles]);
227
+ return {
228
+ mode: allFiles.length > 0 ? 'conf' : 'unknown',
229
+ primaryFiles,
230
+ supportFiles,
231
+ allFiles,
232
+ };
233
+ }
234
+ }
235
+ function pickDiverseConfFiles(scored, connector) {
236
+ const byKind = new Map();
237
+ for (const item of scored) {
238
+ const kind = connector.classifyFileKind(item.path);
239
+ byKind.set(kind, [...(byKind.get(kind) ?? []), item.path]);
240
+ }
241
+ const picked = [
242
+ ...(byKind.get('view') ?? []).slice(0, 5),
243
+ ...(byKind.get('controller') ?? []).slice(0, 2),
244
+ ...(byKind.get('model') ?? []).slice(0, 1),
245
+ ...(byKind.get('store') ?? []).slice(0, 1),
246
+ ];
247
+ if (picked.length < 8) {
248
+ picked.push(...scored.map((item) => item.path));
249
+ }
250
+ return unique(picked).slice(0, 8);
251
+ }
@@ -0,0 +1,23 @@
1
+ export type StatsReport = {
2
+ total?: number;
3
+ passed?: number;
4
+ failed?: number;
5
+ skipped?: number;
6
+ failed_specs?: string[];
7
+ failed_specs_string?: string;
8
+ launch_link?: string;
9
+ launch_id?: string;
10
+ specs?: Array<{
11
+ file: string;
12
+ stats?: Record<string, number>;
13
+ tests?: Array<{
14
+ title: string;
15
+ status: string;
16
+ duration?: number;
17
+ }>;
18
+ }>;
19
+ };
20
+ export declare class CiArtifactsConnector {
21
+ readStats(filePath: string): StatsReport;
22
+ listArtifacts(root: string, pattern: string): string[];
23
+ }
@@ -0,0 +1,14 @@
1
+ import fg from 'fast-glob';
2
+ import { readJson } from '../utils/fileUtils.js';
3
+ export class CiArtifactsConnector {
4
+ readStats(filePath) {
5
+ return readJson(filePath);
6
+ }
7
+ listArtifacts(root, pattern) {
8
+ return fg.sync(pattern, {
9
+ cwd: root,
10
+ absolute: true,
11
+ onlyFiles: true,
12
+ });
13
+ }
14
+ }
@@ -0,0 +1 @@
1
+ export declare function normalizeChangedFiles(changedFiles: string[]): string[];
@@ -0,0 +1,4 @@
1
+ import path from 'node:path';
2
+ export function normalizeChangedFiles(changedFiles) {
3
+ return changedFiles.map((file) => path.resolve(file));
4
+ }
@@ -0,0 +1,4 @@
1
+ export declare class FilesystemConnector {
2
+ glob(root: string, patterns: string[]): Promise<string[]>;
3
+ read(filePath: string): string;
4
+ }
@@ -0,0 +1,15 @@
1
+ import fg from 'fast-glob';
2
+ import { readTextIfExists } from '../utils/fileUtils.js';
3
+ export class FilesystemConnector {
4
+ async glob(root, patterns) {
5
+ return fg(patterns, {
6
+ cwd: root,
7
+ absolute: true,
8
+ onlyFiles: true,
9
+ dot: true,
10
+ });
11
+ }
12
+ read(filePath) {
13
+ return readTextIfExists(filePath);
14
+ }
15
+ }
@@ -0,0 +1,14 @@
1
+ import type { ConsumerConfig } from '../config/consumerConfig.js';
2
+ export type GitLabPipelineContext = {
3
+ pipelineUrl?: string;
4
+ pipelineStatus?: string;
5
+ source: 'config-only' | 'live-api' | 'unavailable';
6
+ warnings: string[];
7
+ };
8
+ export declare class GitLabConnector {
9
+ private readonly config?;
10
+ constructor(config?: ConsumerConfig['ci']['gitlab']);
11
+ describePipeline(input?: {
12
+ sha?: string;
13
+ }): Promise<GitLabPipelineContext>;
14
+ }
@@ -0,0 +1,64 @@
1
+ export class GitLabConnector {
2
+ config;
3
+ constructor(config) {
4
+ this.config = config;
5
+ }
6
+ async describePipeline(input) {
7
+ if (!this.config) {
8
+ return {
9
+ source: 'unavailable',
10
+ warnings: [],
11
+ };
12
+ }
13
+ if (!this.config.projectId || !this.config.token) {
14
+ return {
15
+ source: 'config-only',
16
+ warnings: ['GitLab live lookup is disabled because projectId or token is missing. Using local artifacts only.'],
17
+ };
18
+ }
19
+ const sha = input?.sha ?? process.env.CI_COMMIT_SHA;
20
+ if (!sha) {
21
+ return {
22
+ source: 'config-only',
23
+ warnings: ['GitLab live lookup is disabled because no commit SHA was provided by CI_COMMIT_SHA or tool input.'],
24
+ };
25
+ }
26
+ try {
27
+ const controller = new AbortController();
28
+ const timeout = setTimeout(() => controller.abort(), 1500);
29
+ const response = await fetch(`${this.config.baseUrl.replace(/\/+$/, '')}/api/v4/projects/${encodeURIComponent(this.config.projectId)}/pipelines?sha=${encodeURIComponent(sha)}`, {
30
+ headers: {
31
+ 'PRIVATE-TOKEN': this.config.token,
32
+ },
33
+ signal: controller.signal,
34
+ });
35
+ clearTimeout(timeout);
36
+ if (!response.ok) {
37
+ return {
38
+ source: 'config-only',
39
+ warnings: [`GitLab live lookup failed with status ${response.status}. Using local artifacts only.`],
40
+ };
41
+ }
42
+ const payload = await response.json();
43
+ const pipeline = payload[0];
44
+ if (!pipeline) {
45
+ return {
46
+ source: 'config-only',
47
+ warnings: [`GitLab did not return a pipeline for commit ${sha}. Using local artifacts only.`],
48
+ };
49
+ }
50
+ return {
51
+ pipelineUrl: pipeline.web_url,
52
+ pipelineStatus: pipeline.status,
53
+ source: 'live-api',
54
+ warnings: [],
55
+ };
56
+ }
57
+ catch (error) {
58
+ return {
59
+ source: 'config-only',
60
+ warnings: [`GitLab live lookup failed: ${error instanceof Error ? error.message : 'unknown error'}. Using local artifacts only.`],
61
+ };
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,14 @@
1
+ import type { ConsumerConfig } from '../config/consumerConfig.js';
2
+ import type { StatsReport } from './ciArtifactsConnector.js';
3
+ export type ReportPortalLaunchContext = {
4
+ launchLink?: string;
5
+ launchId?: string;
6
+ launchStatus?: string;
7
+ source: 'local-stats' | 'live-api' | 'unavailable';
8
+ warnings: string[];
9
+ };
10
+ export declare class ReportPortalConnector {
11
+ private readonly config?;
12
+ constructor(config?: ConsumerConfig['ci']['reportPortal']);
13
+ describeLaunch(stats: StatsReport): Promise<ReportPortalLaunchContext>;
14
+ }