@mknrt/autotests-overkill 1.1.2 → 1.1.4

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 (28) hide show
  1. package/README.md +17 -5
  2. package/bin/autotests-overkill.js +3 -1
  3. package/dist/src/appContext.d.ts +2 -0
  4. package/dist/src/appContext.js +4 -0
  5. package/dist/src/config/consumerConfig.d.ts +5 -0
  6. package/dist/src/config/consumerConfig.js +5 -0
  7. package/dist/src/connectors/autotests2Connector.d.ts +8 -0
  8. package/dist/src/connectors/autotests2Connector.js +7 -0
  9. package/dist/src/connectors/fisPlatformConnector.d.ts +15 -0
  10. package/dist/src/connectors/fisPlatformConnector.js +112 -0
  11. package/dist/src/domain/analyzeDiffImpact.js +66 -0
  12. package/dist/src/domain/findBackendTestContext.d.ts +24 -0
  13. package/dist/src/domain/findBackendTestContext.js +80 -0
  14. package/dist/src/domain/generateSpecBlueprint.js +48 -0
  15. package/dist/src/domain/triageFailedRun.js +97 -0
  16. package/dist/src/indexer/extractors/apiDiscoveryExtractor.d.ts +23 -0
  17. package/dist/src/indexer/extractors/apiDiscoveryExtractor.js +183 -0
  18. package/dist/src/indexer/extractors/apiInteractionExtractor.d.ts +23 -0
  19. package/dist/src/indexer/extractors/apiInteractionExtractor.js +35 -0
  20. package/dist/src/indexer/extractors/backendContractExtractor.d.ts +26 -0
  21. package/dist/src/indexer/extractors/backendContractExtractor.js +66 -0
  22. package/dist/src/indexer/refreshPipeline.js +40 -0
  23. package/dist/src/mcp/registerTools.js +8 -0
  24. package/docs/operator-cookbook.md +14 -3
  25. package/docs/superpowers/plans/2026-04-20-fis-platform-backend-integration.md +980 -0
  26. package/docs/tool-catalog.md +12 -3
  27. package/overkill.config.example.json +2 -1
  28. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import { countDocuments, queryDocuments } from '../knowledge/repositories.js';
2
3
  import { evidenceFile, output } from './shared.js';
3
4
  export async function triageFailedRun(input, context) {
4
5
  const stats = context.ciArtifacts.readStats(input.statsFile);
@@ -11,6 +12,10 @@ export async function triageFailedRun(input, context) {
11
12
  const apiCaptures = context.ciArtifacts.listArtifacts(input.apiCaptureRoot, '**/*.json');
12
13
  const failedSpecs = stats.failed_specs ?? [];
13
14
  const repeatedAttempts = screenshots.filter((filePath) => filePath.includes('attempt 2')).length;
15
+ const captureSignals = readApiCaptureSignals(apiCaptures);
16
+ const apiBackendContext = captureSignals.length > 0
17
+ ? await matchApiCapturesToBackendContext(captureSignals, context)
18
+ : { interactionHits: [], backendHits: [] };
14
19
  const warnings = failedSpecs.length === 0
15
20
  ? ['Current stats file reports zero failed specs; using persisted screenshots and artifact residue for triage context.']
16
21
  : [];
@@ -19,6 +24,9 @@ export async function triageFailedRun(input, context) {
19
24
  failedSpecs.length > 0 ? 'Rerun only the failed specs first.' : 'Start from the specs named in persisted screenshots before broad reruns.',
20
25
  repeatedAttempts > 0 ? 'Inspect flaky or environment-sensitive failures because repeated attempts are present.' : 'Inspect first-failure artifacts before assuming flakiness.',
21
26
  apiCaptures.length > 0 ? 'Compare API capture payloads for the affected run before editing selectors.' : 'Capture API traffic on the next rerun to reduce ambiguity.',
27
+ ...(apiBackendContext.interactionHits.length > 0 || apiBackendContext.backendHits.length > 0
28
+ ? ['Compare the failing API capture with the indexed API interaction flow and backend contract before editing the test flow.']
29
+ : []),
22
30
  ];
23
31
  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
32
  evidenceFile('stats report', input.statsFile, JSON.stringify({
@@ -30,6 +38,16 @@ export async function triageFailedRun(input, context) {
30
38
  ...failedSpecs.slice(0, 5).map((specPath) => ({ type: 'test', label: 'failed spec', path: specPath })),
31
39
  ...screenshots.slice(0, 5).map((filePath) => evidenceFile('failure screenshot', filePath, fs.existsSync(filePath) ? undefined : 'missing')),
32
40
  ...videos.slice(0, 3).map((filePath) => evidenceFile('failure video', filePath)),
41
+ ...captureSignals.slice(0, 5).map((signal) => evidenceFile('matched api capture', signal.sourcePath, `${signal.method ?? 'UNKNOWN'} ${signal.path} -> ${signal.status ?? 'unknown status'}`, {
42
+ requestPath: signal.path,
43
+ status: signal.status,
44
+ })),
45
+ ...apiBackendContext.interactionHits.map((item) => evidenceFile('api interaction', item.path, item.title, {
46
+ confidence: parseMetadata(item.metadata_json).confidence,
47
+ })),
48
+ ...apiBackendContext.backendHits.map((item) => evidenceFile('backend endpoint', item.path, item.title, {
49
+ confidence: parseMetadata(item.metadata_json).confidence,
50
+ })),
33
51
  ...(reportPortal.launchLink ? [{
34
52
  type: 'report',
35
53
  label: reportPortal.launchStatus ? `ReportPortal launch (${reportPortal.launchStatus})` : 'ReportPortal launch',
@@ -44,3 +62,82 @@ export async function triageFailedRun(input, context) {
44
62
  }] : []),
45
63
  ], actions, warnings);
46
64
  }
65
+ function readApiCaptureSignals(apiCaptures) {
66
+ return apiCaptures.flatMap((filePath) => {
67
+ try {
68
+ const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
69
+ const entries = Array.isArray(raw) ? raw : [raw];
70
+ return entries.flatMap((entry) => normalizeCaptureEntry(entry, filePath));
71
+ }
72
+ catch {
73
+ return [];
74
+ }
75
+ });
76
+ }
77
+ function normalizeCaptureEntry(entry, sourcePath) {
78
+ if (!entry || typeof entry !== 'object') {
79
+ return [];
80
+ }
81
+ const record = entry;
82
+ const pathValue = record.path ?? record.url ?? record.requestUrl;
83
+ if (typeof pathValue !== 'string') {
84
+ return [];
85
+ }
86
+ return [{
87
+ sourcePath,
88
+ method: typeof record.method === 'string' ? record.method : undefined,
89
+ path: normalizeRequestPath(pathValue),
90
+ status: typeof record.status === 'number' ? record.status : undefined,
91
+ }];
92
+ }
93
+ async function matchApiCapturesToBackendContext(captureSignals, context) {
94
+ if (countDocuments(context.database) === 0) {
95
+ return { interactionHits: [], backendHits: [] };
96
+ }
97
+ const signalQuery = captureSignals.map((signal) => signal.path).join(' ');
98
+ const interactionHits = queryDocuments(context.database, signalQuery, {
99
+ repoKind: 'autotests2',
100
+ sourceKinds: ['api-interaction'],
101
+ limit: 6,
102
+ }).filter((item) => {
103
+ const metadata = parseMetadata(item.metadata_json);
104
+ return (metadata.confidence ?? 0) >= 0.7
105
+ && (metadata.normalizedPaths ?? []).some((indexedPath) => captureSignals.some((signal) => pathsOverlap(indexedPath, signal.path)));
106
+ });
107
+ const backendHits = interactionHits.length > 0
108
+ ? queryDocuments(context.database, interactionHits.map((item) => item.title).join(' '), {
109
+ repoKind: 'fis-platform',
110
+ sourceKinds: ['backend-endpoint'],
111
+ limit: 6,
112
+ }).filter((item) => (parseMetadata(item.metadata_json).confidence ?? 0) >= 0.7)
113
+ : [];
114
+ return { interactionHits, backendHits };
115
+ }
116
+ function normalizeRequestPath(value) {
117
+ try {
118
+ const parsed = new URL(value);
119
+ return parsed.pathname;
120
+ }
121
+ catch {
122
+ return value.split('?')[0] ?? value;
123
+ }
124
+ }
125
+ function pathsOverlap(indexedPath, capturedPath) {
126
+ return capturedPath.includes(indexedPath)
127
+ || indexedPath.includes(capturedPath)
128
+ || normalizeTokens(indexedPath).filter((token) => normalizeTokens(capturedPath).includes(token)).length >= 2;
129
+ }
130
+ function normalizeTokens(value) {
131
+ return value
132
+ .toLowerCase()
133
+ .split(/[^a-z0-9]+/)
134
+ .filter((token) => token.length > 1 && !/^(platform|rs2|api|rest)$/.test(token));
135
+ }
136
+ function parseMetadata(metadataJson) {
137
+ try {
138
+ return JSON.parse(metadataJson);
139
+ }
140
+ catch {
141
+ return {};
142
+ }
143
+ }
@@ -0,0 +1,23 @@
1
+ export type ApiDiscoveryCatalog = {
2
+ families: ApiDiscoveryFamily[];
3
+ generatedAt: string;
4
+ warnings: string[];
5
+ };
6
+ export type ApiDiscoveryFamily = {
7
+ family: string;
8
+ normalizedPaths: string[];
9
+ consumerSignals: ApiConsumerSignal[];
10
+ score: number;
11
+ whySelected: string[];
12
+ };
13
+ export type ApiConsumerSignal = {
14
+ kind: 'endpoint-constant' | 'request-wrapper' | 'capture-support' | 'spec-reference';
15
+ path: string;
16
+ name?: string;
17
+ snippet: string;
18
+ };
19
+ export declare function buildApiDiscoveryCatalog(input: {
20
+ root: string;
21
+ maxSelectedFamilies?: number;
22
+ }): Promise<ApiDiscoveryCatalog>;
23
+ export declare function scoreFamily(signals: ApiConsumerSignal[]): number;
@@ -0,0 +1,183 @@
1
+ import path from 'node:path';
2
+ import { Autotests2Connector } from '../../connectors/autotests2Connector.js';
3
+ import { toPosixPath } from '../../utils/pathUtils.js';
4
+ export async function buildApiDiscoveryCatalog(input) {
5
+ const connector = new Autotests2Connector(input.root);
6
+ const sources = await connector.findApiSupportSources();
7
+ const drafts = new Map();
8
+ for (const source of sources) {
9
+ collectEndpointSignals(source, drafts);
10
+ }
11
+ for (const source of sources) {
12
+ collectWrapperSignals(source, drafts);
13
+ collectCaptureSignals(source, drafts);
14
+ }
15
+ const candidates = [...drafts.entries()]
16
+ .map(([family, draft]) => {
17
+ const score = scoreFamily(draft.consumerSignals);
18
+ const signalKinds = new Set(draft.consumerSignals.map((signal) => signal.kind));
19
+ return {
20
+ family,
21
+ normalizedPaths: [...draft.normalizedPaths].sort(),
22
+ consumerSignals: dedupeSignals(draft.consumerSignals),
23
+ score,
24
+ whySelected: [
25
+ `Selected from ${signalKinds.size} consumer signal kind(s).`,
26
+ `Deterministic score ${score} from endpoint constants, request wrappers, capture support, and spec references.`,
27
+ ],
28
+ };
29
+ })
30
+ .filter((family) => family.score > 0 && hasEnoughEvidence(family))
31
+ .sort((left, right) => {
32
+ if (right.score !== left.score) {
33
+ return right.score - left.score;
34
+ }
35
+ return left.family.localeCompare(right.family);
36
+ });
37
+ const selected = candidates.slice(0, input.maxSelectedFamilies ?? 3);
38
+ return {
39
+ families: selected,
40
+ generatedAt: new Date().toISOString(),
41
+ warnings: selected.length === 0
42
+ ? [`No API families with at least two signal kinds were discovered under ${input.root}.`]
43
+ : [],
44
+ };
45
+ }
46
+ export function scoreFamily(signals) {
47
+ const count = (kind) => signals.filter((signal) => signal.kind === kind).length;
48
+ return count('endpoint-constant') * 4
49
+ + count('request-wrapper') * 3
50
+ + count('capture-support') * 2
51
+ + count('spec-reference');
52
+ }
53
+ function hasEnoughEvidence(family) {
54
+ return new Set(family.consumerSignals.map((signal) => signal.kind)).size >= 2;
55
+ }
56
+ function collectEndpointSignals(source, drafts) {
57
+ for (const line of source.body.split(/\r?\n/)) {
58
+ const endpointMatches = [...line.matchAll(/(?:const|let|var|export\s+const)?\s*([A-Z0-9_]+)?\s*=?\s*['"`]([^'"`]*(?:\/platform\/|\/rs2\/)[^'"`]*)['"`]/gi)];
59
+ for (const match of endpointMatches) {
60
+ const rawPath = match[2];
61
+ if (!rawPath) {
62
+ continue;
63
+ }
64
+ const normalizedPath = normalizeApiPath(rawPath);
65
+ const family = inferFamily(normalizedPath);
66
+ if (!family) {
67
+ continue;
68
+ }
69
+ const draft = getDraft(drafts, family);
70
+ draft.normalizedPaths.add(normalizedPath);
71
+ draft.consumerSignals.push({
72
+ kind: 'endpoint-constant',
73
+ path: source.path,
74
+ name: match[1],
75
+ snippet: line.trim(),
76
+ });
77
+ }
78
+ }
79
+ }
80
+ function collectWrapperSignals(source, drafts) {
81
+ if (!/cy\.request|fetch\(|XMLHttpRequest|\brequest\s*\(/i.test(source.body)) {
82
+ return;
83
+ }
84
+ const commandNames = [...source.body.matchAll(/Cypress\.Commands\.add\((?:'|")([^'"]+)(?:'|")/g)]
85
+ .map((match) => match[1])
86
+ .filter((value) => Boolean(value));
87
+ const families = familiesReferencedBySource(source, drafts);
88
+ for (const family of families) {
89
+ getDraft(drafts, family).consumerSignals.push({
90
+ kind: 'request-wrapper',
91
+ path: source.path,
92
+ name: commandNames.join(', ') || path.basename(source.path),
93
+ snippet: firstMatchingLine(source.body, /cy\.request|fetch\(|XMLHttpRequest|\brequest\s*\(/i),
94
+ });
95
+ }
96
+ }
97
+ function collectCaptureSignals(source, drafts) {
98
+ if (!/apiCapture/i.test(source.body)) {
99
+ return;
100
+ }
101
+ for (const family of familiesReferencedBySource(source, drafts)) {
102
+ getDraft(drafts, family).consumerSignals.push({
103
+ kind: 'capture-support',
104
+ path: source.path,
105
+ snippet: firstMatchingLine(source.body, /apiCapture/i),
106
+ });
107
+ }
108
+ }
109
+ function familiesReferencedBySource(source, drafts) {
110
+ const directFamilies = new Set();
111
+ for (const endpoint of extractApiPaths(source.body)) {
112
+ const family = inferFamily(endpoint);
113
+ if (family) {
114
+ directFamilies.add(family);
115
+ }
116
+ }
117
+ if (directFamilies.size > 0) {
118
+ return [...directFamilies];
119
+ }
120
+ const importedNames = [...source.body.matchAll(/\b([A-Z][A-Z0-9_]+_ENDPOINT)\b/g)]
121
+ .map((match) => match[1])
122
+ .filter((value) => Boolean(value))
123
+ .map((value) => value.toLowerCase());
124
+ return [...drafts.entries()]
125
+ .filter(([family, draft]) => {
126
+ const familyToken = family.toLowerCase().replace(/[^a-z0-9]+/g, '_');
127
+ return importedNames.some((name) => name.includes(familyToken))
128
+ || [...draft.normalizedPaths].some((endpoint) => source.body.includes(endpoint));
129
+ })
130
+ .map(([family]) => family);
131
+ }
132
+ function extractApiPaths(body) {
133
+ return [...body.matchAll(/['"`]([^'"`]*(?:\/platform\/|\/rs2\/)[^'"`]*)['"`]/gi)]
134
+ .map((match) => match[1])
135
+ .filter((value) => Boolean(value))
136
+ .map((value) => normalizeApiPath(value));
137
+ }
138
+ function normalizeApiPath(value) {
139
+ const withoutHost = value.replace(/^https?:\/\/[^/]+/i, '');
140
+ const withoutQuery = withoutHost.split('?')[0] ?? withoutHost;
141
+ return withoutQuery.startsWith('/') ? withoutQuery : `/${withoutQuery}`;
142
+ }
143
+ function inferFamily(normalizedPath) {
144
+ const segments = normalizedPath.split('/').filter(Boolean);
145
+ const rs2Index = segments.findIndex((segment) => segment.toLowerCase() === 'rs2');
146
+ const rs2Family = segments[rs2Index + 1];
147
+ if (rs2Index >= 0 && rs2Family) {
148
+ return rs2Family.toLowerCase();
149
+ }
150
+ const platformIndex = segments.findIndex((segment) => segment.toLowerCase() === 'platform');
151
+ const platformFamily = segments[platformIndex + 1];
152
+ if (platformIndex >= 0 && platformFamily) {
153
+ return platformFamily.toLowerCase();
154
+ }
155
+ return segments.find((segment) => !/^(api|rest|v\d+)$/i.test(segment))?.toLowerCase();
156
+ }
157
+ function getDraft(drafts, family) {
158
+ const existing = drafts.get(family);
159
+ if (existing) {
160
+ return existing;
161
+ }
162
+ const draft = {
163
+ normalizedPaths: new Set(),
164
+ consumerSignals: [],
165
+ };
166
+ drafts.set(family, draft);
167
+ return draft;
168
+ }
169
+ function dedupeSignals(signals) {
170
+ const seen = new Set();
171
+ const result = [];
172
+ for (const signal of signals) {
173
+ const key = `${signal.kind}:${toPosixPath(signal.path)}:${signal.name ?? ''}:${signal.snippet}`;
174
+ if (!seen.has(key)) {
175
+ seen.add(key);
176
+ result.push(signal);
177
+ }
178
+ }
179
+ return result;
180
+ }
181
+ function firstMatchingLine(body, pattern) {
182
+ return body.split(/\r?\n/).find((line) => pattern.test(line))?.trim() ?? body.slice(0, 160);
183
+ }
@@ -0,0 +1,23 @@
1
+ import type { ApiDiscoveryCatalog } from './apiDiscoveryExtractor.js';
2
+ export type ApiInteractionRecord = {
3
+ id: string;
4
+ title: string;
5
+ path: string;
6
+ body: string;
7
+ sourceKind: 'api-interaction';
8
+ repoKind: 'autotests2';
9
+ metadata: {
10
+ apiFamily: string;
11
+ normalizedPaths: string[];
12
+ consumerSignalIds: string[];
13
+ evidencePaths: string[];
14
+ confidence: number;
15
+ whySelected: string[];
16
+ };
17
+ };
18
+ export declare function extractApiInteractions(input: {
19
+ catalog: ApiDiscoveryCatalog;
20
+ }): {
21
+ documents: ApiInteractionRecord[];
22
+ warnings: string[];
23
+ };
@@ -0,0 +1,35 @@
1
+ export function extractApiInteractions(input) {
2
+ const documents = input.catalog.families
3
+ .map((family) => {
4
+ const consumerSignalIds = family.consumerSignals.map((signal, index) => `${signal.kind}:${signal.path}:${signal.name ?? index}`);
5
+ const evidencePaths = [...new Set(family.consumerSignals.map((signal) => signal.path))].sort();
6
+ const confidence = Math.min(1, family.score / 10);
7
+ const body = [
8
+ `API family: ${family.family}`,
9
+ `Normalized paths: ${family.normalizedPaths.join(', ')}`,
10
+ `Consumer evidence: ${family.consumerSignals.map((signal) => `${signal.kind} ${signal.name ?? ''} ${signal.snippet}`).join('\n')}`,
11
+ `Why selected: ${family.whySelected.join('\n')}`,
12
+ ].join('\n');
13
+ return {
14
+ id: `api-interaction:${family.family}`,
15
+ title: `API interaction: ${family.family}`,
16
+ path: evidencePaths[0] ?? `api-discovery/${family.family}`,
17
+ body,
18
+ sourceKind: 'api-interaction',
19
+ repoKind: 'autotests2',
20
+ metadata: {
21
+ apiFamily: family.family,
22
+ normalizedPaths: family.normalizedPaths,
23
+ consumerSignalIds,
24
+ evidencePaths,
25
+ confidence,
26
+ whySelected: family.whySelected,
27
+ },
28
+ };
29
+ })
30
+ .filter((record) => record.metadata.confidence >= 0.7);
31
+ return {
32
+ documents,
33
+ warnings: documents.length === 0 ? ['No high-confidence API interaction documents were extracted.'] : [],
34
+ };
35
+ }
@@ -0,0 +1,26 @@
1
+ import type { FocusedFisSource } from '../../connectors/fisPlatformConnector.js';
2
+ export type { FocusedFisSource } from '../../connectors/fisPlatformConnector.js';
3
+ export type BackendKnowledgeRecord = {
4
+ id: string;
5
+ title: string;
6
+ path: string;
7
+ body: string;
8
+ sourceKind: 'backend-endpoint';
9
+ repoKind: 'fis-platform';
10
+ metadata: {
11
+ httpMethod?: 'GET' | 'POST' | 'PUT' | 'DELETE';
12
+ endpointPath: string;
13
+ apiFamily: string;
14
+ paramNames: string[];
15
+ ruleHints: string[];
16
+ evidencePaths: string[];
17
+ confidence: number;
18
+ whyMatched: string[];
19
+ };
20
+ };
21
+ export declare function extractBackendContracts(input: {
22
+ sources: FocusedFisSource[];
23
+ }): {
24
+ documents: BackendKnowledgeRecord[];
25
+ warnings: string[];
26
+ };
@@ -0,0 +1,66 @@
1
+ export function extractBackendContracts(input) {
2
+ const documents = input.sources.flatMap((source) => extractRecordsFromSource(source));
3
+ return {
4
+ documents,
5
+ warnings: documents.length === 0 ? ['No backend endpoint contracts were extracted from focused FIS Platform sources.'] : [],
6
+ };
7
+ }
8
+ function extractRecordsFromSource(source) {
9
+ const classPath = firstMatch(source.body, /@Path\(\s*"([^"]+)"\s*\)/);
10
+ const methodBlocks = [...source.body.matchAll(/@(GET|POST|PUT|DELETE)([\s\S]*?)(?=@(?:GET|POST|PUT|DELETE)|\n\s*}\s*$)/g)];
11
+ if (methodBlocks.length === 0 && classPath) {
12
+ return [createRecord(source, undefined, classPath, source.body)];
13
+ }
14
+ return methodBlocks.map((match) => {
15
+ const method = match[1];
16
+ const block = match[0];
17
+ const methodPath = firstMatch(block, /@Path\(\s*"([^"]+)"\s*\)/);
18
+ return createRecord(source, method, joinPaths(classPath, methodPath), block);
19
+ });
20
+ }
21
+ function createRecord(source, httpMethod, endpointPath, body) {
22
+ const resolvedEndpointPath = endpointPath ?? firstMatch(source.body, /@Path\(\s*"([^"]+)"\s*\)/) ?? '/';
23
+ const paramNames = [
24
+ ...extractMatches(body, /@QueryParam\(\s*"([^"]+)"\s*\)/g),
25
+ ...extractMatches(body, /@HeaderParam\(\s*"([^"]+)"\s*\)/g),
26
+ ].filter((value) => Boolean(value));
27
+ const ruleHints = extractRuleHints(body);
28
+ return {
29
+ id: `fis-platform:${source.matchedFamily}:${httpMethod ?? 'RESOURCE'}:${source.path}:${resolvedEndpointPath}`,
30
+ title: `${httpMethod ? `${httpMethod} ` : ''}${resolvedEndpointPath}`,
31
+ path: source.path,
32
+ body,
33
+ sourceKind: 'backend-endpoint',
34
+ repoKind: 'fis-platform',
35
+ metadata: {
36
+ httpMethod,
37
+ endpointPath: resolvedEndpointPath,
38
+ apiFamily: source.matchedFamily,
39
+ paramNames,
40
+ ruleHints,
41
+ evidencePaths: [source.path],
42
+ confidence: source.confidence,
43
+ whyMatched: source.whyMatched,
44
+ },
45
+ };
46
+ }
47
+ function joinPaths(base, child) {
48
+ const parts = [base, child]
49
+ .filter((part) => Boolean(part))
50
+ .map((part) => part.replace(/^\/+|\/+$/g, ''));
51
+ return `/${parts.join('/')}`.replace(/\/+/g, '/');
52
+ }
53
+ function firstMatch(body, pattern) {
54
+ return body.match(pattern)?.[1];
55
+ }
56
+ function extractMatches(body, pattern) {
57
+ return [...body.matchAll(pattern)].map((match) => match[1]);
58
+ }
59
+ function extractRuleHints(body) {
60
+ const hints = body
61
+ .split(/\r?\n/)
62
+ .map((line) => line.trim())
63
+ .filter((line) => /validate|required|permission|async|task|complete|status|throw|Exception/i.test(line))
64
+ .slice(0, 8);
65
+ return hints;
66
+ }
@@ -2,6 +2,9 @@ import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { syncDocumentsByRepoKinds } from '../knowledge/repositories.js';
5
+ import { buildApiDiscoveryCatalog } from './extractors/apiDiscoveryExtractor.js';
6
+ import { extractApiInteractions } from './extractors/apiInteractionExtractor.js';
7
+ import { extractBackendContracts } from './extractors/backendContractExtractor.js';
5
8
  import { extractFrontendContract } from './extractors/frontendContractExtractor.js';
6
9
  import { SnapshotStore } from './snapshotStore.js';
7
10
  import { ensureDirectory } from '../utils/fileUtils.js';
@@ -15,6 +18,13 @@ export async function refreshKnowledge(context) {
15
18
  if (fs.existsSync(context.config.repos.autotests2)) {
16
19
  repoKindsToSync.push('autotests2');
17
20
  const docs = context.autotests2.getAreaDocs();
21
+ const apiCatalog = await buildApiDiscoveryCatalog({
22
+ root: context.config.repos.autotests2,
23
+ maxSelectedFamilies: 3,
24
+ });
25
+ snapshotStore.write('api-discovery-catalog', apiCatalog);
26
+ const apiInteractions = extractApiInteractions({ catalog: apiCatalog });
27
+ snapshotStore.write('api-interactions', apiInteractions);
18
28
  const testAssets = {
19
29
  specs: await context.autotests2.findSpecs(),
20
30
  commands: await context.autotests2.findCommands(),
@@ -83,6 +93,36 @@ export async function refreshKnowledge(context) {
83
93
  }));
84
94
  }
85
95
  }
96
+ for (const record of apiInteractions.documents) {
97
+ documents.push(documentFrom({
98
+ sourceKind: record.sourceKind,
99
+ repoKind: record.repoKind,
100
+ path: record.path,
101
+ title: record.title,
102
+ body: record.body,
103
+ metadata: record.metadata,
104
+ updatedAt: now,
105
+ }));
106
+ }
107
+ warnings.push(...apiCatalog.warnings, ...apiInteractions.warnings);
108
+ if (context.fisPlatform && context.config.repos.fisPlatform && fs.existsSync(context.config.repos.fisPlatform)) {
109
+ repoKindsToSync.push('fis-platform');
110
+ const backendSources = await context.fisPlatform.listFocusedSources(apiCatalog);
111
+ const backendContracts = extractBackendContracts({ sources: backendSources });
112
+ snapshotStore.write('backend-contracts', backendContracts);
113
+ for (const record of backendContracts.documents) {
114
+ documents.push(documentFrom({
115
+ sourceKind: record.sourceKind,
116
+ repoKind: record.repoKind,
117
+ path: record.path,
118
+ title: record.title,
119
+ body: record.body,
120
+ metadata: record.metadata,
121
+ updatedAt: now,
122
+ }));
123
+ }
124
+ warnings.push(...backendContracts.warnings);
125
+ }
86
126
  }
87
127
  else {
88
128
  warnings.push(`Skipped autotests2 refresh because '${context.config.repos.autotests2}' is unavailable.`);
@@ -4,6 +4,7 @@ import { analyzeTestGaps } from '../domain/analyzeTestGaps.js';
4
4
  import { buildRerunScope } from '../domain/buildRerunScope.js';
5
5
  import { buildSetupTeardownPlan } from '../domain/buildSetupTeardownPlan.js';
6
6
  import { findExistingTestAssets } from '../domain/findExistingTestAssets.js';
7
+ import { findBackendTestContext } from '../domain/findBackendTestContext.js';
7
8
  import { findFrontendContract } from '../domain/findFrontendContract.js';
8
9
  import { findFrontendRuntimeLogic } from '../domain/findFrontendRuntimeLogic.js';
9
10
  import { findTestAreaContext } from '../domain/findTestAreaContext.js';
@@ -22,6 +23,13 @@ export function buildToolRegistry() {
22
23
  outputSchema: toolOutputSchema,
23
24
  execute: findExistingTestAssets,
24
25
  },
26
+ {
27
+ name: 'find_backend_test_context',
28
+ description: 'Finds autotests2 API interaction flows plus backend endpoint, validation, async-task, and error hints.',
29
+ inputSchema: z.object({ query: z.string().min(1) }),
30
+ outputSchema: toolOutputSchema,
31
+ execute: findBackendTestContext,
32
+ },
25
33
  {
26
34
  name: 'find_test_area_context',
27
35
  description: 'Returns conventions, init patterns, and area context for constructor or formRunner.',
@@ -5,8 +5,9 @@
5
5
  1. Run `find_existing_test_assets` for the target widget or feature.
6
6
  2. Run `find_frontend_contract`.
7
7
  3. Run `find_frontend_runtime_logic`.
8
- 4. Run `generate_spec_blueprint`.
9
- 5. Review reuse candidates, setup hints, and selector guidance before editing `autotests2`.
8
+ 4. For API-heavy flows, run `find_backend_test_context` to inspect `ApiDiscoveryCatalog`, `api-interaction`, and `backend-endpoint` evidence.
9
+ 5. Run `generate_spec_blueprint`.
10
+ 6. Review reuse candidates, setup hints, selector guidance, backend params, validation hints, and async completion hints before editing `autotests2`.
10
11
 
11
12
  ## Find coverage gaps
12
13
 
@@ -20,7 +21,17 @@
20
21
  1. Run `summarize_ci_context`.
21
22
  2. Run `triage_failed_run`.
22
23
  3. Run `build_rerun_scope`.
23
- 4. If frontend files changed, run `analyze_diff_impact`.
24
+ 4. If frontend or backend files changed, run `analyze_diff_impact`.
25
+ 5. When API capture files exist, treat triage backend hints as evidence-backed hypotheses only when the capture path matches high-confidence `api-interaction` and `backend-endpoint` records.
26
+
27
+ ## Use backend indexing
28
+
29
+ 1. Set optional `repos.fisPlatform` to `../fis_platform` in `overkill.config.json`.
30
+ 2. Run `npm run index`; refresh builds `ApiDiscoveryCatalog` from `autotests2` support code before scanning backend sources.
31
+ 3. Review snapshots under `.overkill-cache/snapshots`: `api-discovery-catalog.json`, `api-interactions.json`, and `backend-contracts.json`.
32
+ 4. Use backend context for debugging request flows discovered from support code or API capture, generating API-heavy blueprints, triaging failures with API captures, and analyzing backend REST/resource diffs.
33
+ 5. Do not treat backend indexing as a hardcoded file allowlist. Discovery uses REST/JAX-RS signals and evidence metadata such as `confidence`, `whySelected`, `whyMatched`, `normalizedPaths`, and `evidencePaths`.
34
+ 6. Runtime API captures are on-demand in iteration 1; raw captures are not persisted to the knowledge store.
24
35
 
25
36
  ## Onboard a new engineer
26
37