@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,109 @@
1
+ import { CaseplatformConnector } from '../../connectors/caseplatformConnector.js';
2
+ import { extractMatches, truncate } from '../../utils/textUtils.js';
3
+ import { unique } from '../../utils/pathUtils.js';
4
+ export async function extractFrontendContract(input) {
5
+ const connector = new CaseplatformConnector(input.root);
6
+ const files = await connector.resolveFeatureFiles(input.widgetHint);
7
+ const warnings = [];
8
+ const controlBody = files.controlFile ? connector.read(files.controlFile) : '';
9
+ const singletonBody = files.singletonFile ? connector.read(files.singletonFile) : '';
10
+ const handlersBody = files.handlersFile ? connector.read(files.handlersFile) : '';
11
+ const supportBodies = files.supportFiles.map((filePath) => ({ path: filePath, body: connector.read(filePath) }));
12
+ if (files.mode === 'widget' && !files.controlFile) {
13
+ warnings.push(`No control file found for widget '${input.widgetHint}'.`);
14
+ }
15
+ const sources = files.allFiles.map((filePath) => ({
16
+ path: filePath,
17
+ body: connector.read(filePath),
18
+ kind: connector.classifyFileKind(filePath),
19
+ }));
20
+ const properties = files.mode === 'conf'
21
+ ? unique(sources.flatMap((source) => extractConfProperties(source.body)))
22
+ : extractMatches(controlBody, /qname:\s*'([^']+)'/g);
23
+ const events = files.mode === 'conf'
24
+ ? unique(sources.flatMap((source) => extractConfEvents(source.body)))
25
+ : extractNamedObjectKeys(controlBody, 'events');
26
+ const methods = files.mode === 'conf'
27
+ ? unique(sources.flatMap((source) => extractConfMethods(source.body)))
28
+ : [
29
+ ...extractNamedObjectKeys(controlBody, 'methods'),
30
+ ...extractMatches(singletonBody, /\n\s+([gs]et[A-Za-z0-9_]+)\s*\(/g),
31
+ ...extractMatches(singletonBody, /\n\s+(on[A-Za-z0-9_]+)\s*\(/g),
32
+ ...extractMatches(singletonBody, /\.([A-Za-z0-9_]+)\s*=\s*function/g),
33
+ ...extractMatches(handlersBody, /([a-zA-Z0-9_]+):\s*function/g),
34
+ ...extractMatches(handlersBody, /\.([A-Za-z0-9_]+)\s*=\s*function/g),
35
+ ];
36
+ const snippets = files.mode === 'conf'
37
+ ? sources.slice(0, 5).map((source) => ({
38
+ label: source.kind,
39
+ path: source.path,
40
+ snippet: truncate(source.body, 280),
41
+ }))
42
+ : [
43
+ files.controlFile ? { label: 'control', path: files.controlFile, snippet: truncate(controlBody, 280) } : null,
44
+ files.singletonFile ? { label: 'singleton', path: files.singletonFile, snippet: truncate(singletonBody, 280) } : null,
45
+ files.handlersFile ? { label: 'handlers', path: files.handlersFile, snippet: truncate(handlersBody, 280) } : null,
46
+ ...supportBodies.slice(0, 3).map((file) => ({ label: 'support', path: file.path, snippet: truncate(file.body, 280) })),
47
+ ].filter(Boolean);
48
+ return {
49
+ widgetName: input.widgetHint,
50
+ properties: [...new Set(properties)],
51
+ methods: [...new Set(methods)],
52
+ events: [...new Set(events)],
53
+ files: files.mode === 'widget'
54
+ ? unique([files.controlFile, files.singletonFile, files.handlersFile, files.templateFile, files.dataControlFile, ...files.supportFiles].filter(Boolean))
55
+ : files.allFiles,
56
+ snippets,
57
+ warnings,
58
+ };
59
+ }
60
+ function extractNamedObjectKeys(source, objectName) {
61
+ const startMatch = new RegExp(`${objectName}\\s*:\\s*\\{`).exec(source);
62
+ if (!startMatch || startMatch.index === undefined) {
63
+ return [];
64
+ }
65
+ const openBraceIndex = source.indexOf('{', startMatch.index);
66
+ let depth = 0;
67
+ let endIndex = -1;
68
+ for (let index = openBraceIndex; index < source.length; index += 1) {
69
+ const char = source[index];
70
+ if (char === '{') {
71
+ depth += 1;
72
+ }
73
+ else if (char === '}') {
74
+ depth -= 1;
75
+ if (depth === 0) {
76
+ endIndex = index;
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ if (endIndex === -1) {
82
+ return [];
83
+ }
84
+ const block = source.slice(openBraceIndex + 1, endIndex);
85
+ return [...new Set(extractMatches(block, /^\s*([A-Za-z0-9_]+)\s*:\s*\{/gm))];
86
+ }
87
+ function extractConfProperties(source) {
88
+ return unique([
89
+ ...extractMatches(source, /dataIndex:\s*'([^']+)'/g),
90
+ ...extractMatches(source, /testguid\s*[:=]\s*['"]([^'"]+)['"]/g),
91
+ ...extractMatches(source, /title:\s*TEXT\.get\('([^']+)'/g),
92
+ ...extractMatches(source, /iconCls:\s*'([^']+)'/g),
93
+ ...extractMatches(source, /extend:\s*'([^']+)'/g),
94
+ ]);
95
+ }
96
+ function extractConfEvents(source) {
97
+ return unique([
98
+ ...extractNamedObjectKeys(source, 'listeners'),
99
+ ...extractMatches(source, /\.on\('([^']+)'/g),
100
+ ...extractMatches(source, /fireEvent\('([^']+)'/g),
101
+ ]);
102
+ }
103
+ function extractConfMethods(source) {
104
+ return unique([
105
+ ...extractMatches(source, /(?:^|\n)\s*([A-Za-z0-9_]+)\s*:\s*function\b/g),
106
+ ...extractMatches(source, /(?:^|\n)\s*(onFacade[A-Za-z0-9_]+)\s*:\s*function\b/g),
107
+ ...extractMatches(source, /(?:^|\n)\s*(onIncomingMessage[A-Za-z0-9_]+)\s*:\s*function\b/g),
108
+ ]);
109
+ }
@@ -0,0 +1,14 @@
1
+ export type RuntimeSignal = {
2
+ kind: 'method' | 'branch' | 'event' | 'constant';
3
+ name: string;
4
+ detail: string;
5
+ path: string;
6
+ };
7
+ export declare function extractFrontendRuntimeLogic(input: {
8
+ root: string;
9
+ widgetHint: string;
10
+ }): Promise<{
11
+ widgetName: string;
12
+ runtimeSignals: RuntimeSignal[];
13
+ warnings: string[];
14
+ }>;
@@ -0,0 +1,110 @@
1
+ import { CaseplatformConnector } from '../../connectors/caseplatformConnector.js';
2
+ import { scoreTextMatch, truncate } from '../../utils/textUtils.js';
3
+ export async function extractFrontendRuntimeLogic(input) {
4
+ const connector = new CaseplatformConnector(input.root);
5
+ const files = await connector.resolveFeatureFiles(input.widgetHint);
6
+ const runtimeSignals = [];
7
+ const supportFiles = new Set(files.supportFiles);
8
+ const candidates = featureCandidates(input.widgetHint);
9
+ for (const filePath of files.allFiles) {
10
+ const body = connector.read(filePath);
11
+ const lines = body.split(/\r?\n/);
12
+ for (const line of lines) {
13
+ const trimmed = line.trim();
14
+ if (trimmed.startsWith('if ') || trimmed.startsWith('if(') || trimmed.includes('.toggleClass(')) {
15
+ runtimeSignals.push({
16
+ kind: 'branch',
17
+ name: trimmed.slice(0, 60),
18
+ detail: truncate(trimmed, 180),
19
+ path: filePath,
20
+ });
21
+ }
22
+ if (trimmed.includes('fireEventBind(')) {
23
+ runtimeSignals.push({
24
+ kind: 'event',
25
+ name: trimmed.match(/fireEventBind\('([^']+)'/)?.[1] ?? trimmed,
26
+ detail: truncate(trimmed, 180),
27
+ path: filePath,
28
+ });
29
+ }
30
+ const confEventName = trimmed.match(/\.on\('([^']+)'/)?.[1]
31
+ ?? trimmed.match(/listeners\s*:\s*\{/)?.[0];
32
+ if (confEventName && (trimmed.includes(".on('") || trimmed.includes('listeners'))) {
33
+ runtimeSignals.push({
34
+ kind: 'event',
35
+ name: confEventName,
36
+ detail: truncate(trimmed, 180),
37
+ path: filePath,
38
+ });
39
+ }
40
+ const methodName = trimmed.match(/^([A-Za-z0-9_]+)\s*\(/)?.[1];
41
+ if (methodName && ['set', 'get', 'on'].some((prefix) => methodName.startsWith(prefix))) {
42
+ runtimeSignals.push({
43
+ kind: 'method',
44
+ name: methodName,
45
+ detail: truncate(trimmed, 180),
46
+ path: filePath,
47
+ });
48
+ }
49
+ const assignedMethodName = trimmed.match(/(?:[A-Za-z0-9_.]+\.)?([A-Za-z0-9_]+)\s*=\s*function\b/)?.[1];
50
+ if (assignedMethodName && ['set', 'get', 'on'].some((prefix) => assignedMethodName.startsWith(prefix))) {
51
+ runtimeSignals.push({
52
+ kind: 'method',
53
+ name: assignedMethodName,
54
+ detail: truncate(trimmed, 180),
55
+ path: filePath,
56
+ });
57
+ }
58
+ const confMethodName = trimmed.match(/^([A-Za-z0-9_]+)\s*:\s*function\b/)?.[1];
59
+ if (confMethodName) {
60
+ runtimeSignals.push({
61
+ kind: 'method',
62
+ name: confMethodName,
63
+ detail: truncate(trimmed, 180),
64
+ path: filePath,
65
+ });
66
+ }
67
+ if (supportFiles.has(filePath)) {
68
+ const exportedFunction = trimmed.match(/^(?:export\s+)?function\s+([A-Za-z0-9_]+)/)?.[1];
69
+ if (exportedFunction) {
70
+ runtimeSignals.push({
71
+ kind: 'method',
72
+ name: exportedFunction,
73
+ detail: truncate(trimmed, 180),
74
+ path: filePath,
75
+ });
76
+ }
77
+ const exportedConstant = trimmed.match(/^(?:export\s+)?const\s+([A-Z][A-Z0-9_]+)/)?.[1];
78
+ if (exportedConstant) {
79
+ runtimeSignals.push({
80
+ kind: 'constant',
81
+ name: exportedConstant,
82
+ detail: truncate(trimmed, 180),
83
+ path: filePath,
84
+ });
85
+ }
86
+ }
87
+ }
88
+ }
89
+ const orderedSignals = runtimeSignals.sort((left, right) => {
90
+ const leftScore = candidates.reduce((score, candidate) => score + scoreTextMatch(`${left.path}\n${left.name}\n${left.detail}`, candidate), 0);
91
+ const rightScore = candidates.reduce((score, candidate) => score + scoreTextMatch(`${right.path}\n${right.name}\n${right.detail}`, candidate), 0);
92
+ return rightScore - leftScore;
93
+ });
94
+ return {
95
+ widgetName: input.widgetHint,
96
+ runtimeSignals: orderedSignals,
97
+ warnings: files.allFiles.length === 0 ? [`No runtime files found for '${input.widgetHint}'.`] : [],
98
+ };
99
+ }
100
+ function featureCandidates(widgetHint) {
101
+ const candidates = widgetHint
102
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
103
+ .split(/[\s_\-/]+/)
104
+ .map((part) => part.toLowerCase())
105
+ .filter(Boolean);
106
+ if (candidates.some((candidate) => candidate.includes('error'))) {
107
+ candidates.push('apperror', 'validate', 'critical', 'tabpanel', 'onfacadevalidate', 'onfacadeupdateerrors');
108
+ }
109
+ return candidates;
110
+ }
@@ -0,0 +1,13 @@
1
+ export declare function extractTestAssets(input: {
2
+ root: string;
3
+ featureHint: string;
4
+ }): Promise<{
5
+ specs: import("../../connectors/autotests2Connector.js").TestSpecMatch[];
6
+ commands: import("../../connectors/autotests2Connector.js").CommandMatch[];
7
+ helpers: import("../../connectors/autotests2Connector.js").HelperMatch[];
8
+ selectors: {
9
+ path: string;
10
+ body: string;
11
+ }[];
12
+ warnings: string[];
13
+ }>;
@@ -0,0 +1,17 @@
1
+ import { Autotests2Connector } from '../../connectors/autotests2Connector.js';
2
+ export async function extractTestAssets(input) {
3
+ const connector = new Autotests2Connector(input.root);
4
+ const [specs, commands, helpers, selectors] = await Promise.all([
5
+ connector.findSpecs(input.featureHint),
6
+ connector.findCommands(),
7
+ connector.findHelpers(input.featureHint),
8
+ connector.findSelectors(input.featureHint),
9
+ ]);
10
+ return {
11
+ specs,
12
+ commands,
13
+ helpers,
14
+ selectors,
15
+ warnings: specs.length === 0 ? [`No specs matched '${input.featureHint}'.`] : [],
16
+ };
17
+ }
@@ -0,0 +1,12 @@
1
+ import type { AppContext } from '../appContext.js';
2
+ export declare function refreshKnowledge(context: AppContext): Promise<{
3
+ documentsIndexed: number;
4
+ stats: {
5
+ attemptedDocuments: number;
6
+ uniqueDocuments: number;
7
+ autotestsDocuments: number;
8
+ caseplatformDocuments: number;
9
+ bySourceKind: Record<string, number>;
10
+ };
11
+ warnings: string[];
12
+ }>;
@@ -0,0 +1,156 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { syncDocumentsByRepoKinds } from '../knowledge/repositories.js';
5
+ import { extractFrontendContract } from './extractors/frontendContractExtractor.js';
6
+ import { SnapshotStore } from './snapshotStore.js';
7
+ import { ensureDirectory } from '../utils/fileUtils.js';
8
+ export async function refreshKnowledge(context) {
9
+ ensureDirectory(context.config.cacheDir);
10
+ const snapshotStore = new SnapshotStore(path.join(context.config.cacheDir, 'snapshots'));
11
+ const documents = [];
12
+ const now = new Date().toISOString();
13
+ const repoKindsToSync = [];
14
+ const warnings = [];
15
+ if (fs.existsSync(context.config.repos.autotests2)) {
16
+ repoKindsToSync.push('autotests2');
17
+ const docs = context.autotests2.getAreaDocs();
18
+ const testAssets = {
19
+ specs: await context.autotests2.findSpecs(),
20
+ commands: await context.autotests2.findCommands(),
21
+ helpers: await context.autotests2.findHelpers(),
22
+ selectors: await context.autotests2.findSelectors(),
23
+ warnings: [],
24
+ };
25
+ snapshotStore.write('test-assets', testAssets);
26
+ for (const spec of testAssets.specs) {
27
+ documents.push(documentFrom({
28
+ sourceKind: 'spec',
29
+ repoKind: 'autotests2',
30
+ path: spec.path,
31
+ title: spec.title,
32
+ body: spec.body,
33
+ metadata: { score: spec.score },
34
+ updatedAt: now,
35
+ }));
36
+ }
37
+ for (const command of testAssets.commands) {
38
+ documents.push(documentFrom({
39
+ sourceKind: 'command',
40
+ repoKind: 'autotests2',
41
+ path: command.path,
42
+ title: command.name,
43
+ body: command.body,
44
+ metadata: {},
45
+ updatedAt: now,
46
+ }));
47
+ }
48
+ for (const helper of testAssets.helpers) {
49
+ documents.push(documentFrom({
50
+ sourceKind: 'helper',
51
+ repoKind: 'autotests2',
52
+ path: helper.path,
53
+ title: helper.name,
54
+ body: helper.body,
55
+ metadata: {},
56
+ updatedAt: now,
57
+ }));
58
+ }
59
+ for (const selector of testAssets.selectors) {
60
+ documents.push(documentFrom({
61
+ sourceKind: 'selector',
62
+ repoKind: 'autotests2',
63
+ path: selector.path,
64
+ title: path.basename(selector.path),
65
+ body: selector.body,
66
+ metadata: {},
67
+ updatedAt: now,
68
+ }));
69
+ }
70
+ for (const docEntry of [
71
+ { sourceKind: 'doc', path: docs.contextPath, title: 'context.md', body: docs.contextText, metadata: { kind: 'context' } },
72
+ { sourceKind: 'doc', path: docs.testingPath, title: 'testing.md', body: docs.testingText, metadata: { kind: 'testing' } },
73
+ ]) {
74
+ if (docEntry.body) {
75
+ documents.push(documentFrom({
76
+ sourceKind: docEntry.sourceKind,
77
+ repoKind: 'autotests2',
78
+ path: docEntry.path,
79
+ title: docEntry.title,
80
+ body: docEntry.body,
81
+ metadata: docEntry.metadata,
82
+ updatedAt: now,
83
+ }));
84
+ }
85
+ }
86
+ }
87
+ else {
88
+ warnings.push(`Skipped autotests2 refresh because '${context.config.repos.autotests2}' is unavailable.`);
89
+ }
90
+ if (fs.existsSync(context.config.repos.caseplatformWeb)) {
91
+ repoKindsToSync.push('caseplatform-web');
92
+ const frontendContract = await extractFrontendContract({
93
+ root: context.config.repos.caseplatformWeb,
94
+ widgetHint: context.config.projectHints?.defaultWidget ?? 'caption',
95
+ });
96
+ snapshotStore.write('frontend-contract', frontendContract);
97
+ const caseplatformSources = await context.caseplatform.listIndexableSources();
98
+ snapshotStore.write('caseplatform-index-summary', buildSourceKindSummary(caseplatformSources.map((source) => source.sourceKind)));
99
+ for (const source of caseplatformSources) {
100
+ documents.push(documentFrom({
101
+ sourceKind: source.sourceKind,
102
+ repoKind: 'caseplatform-web',
103
+ path: source.path,
104
+ title: source.title,
105
+ body: source.body,
106
+ metadata: source.metadata,
107
+ updatedAt: now,
108
+ }));
109
+ }
110
+ warnings.push(...frontendContract.warnings);
111
+ }
112
+ else {
113
+ warnings.push(`Skipped caseplatform-web refresh because '${context.config.repos.caseplatformWeb}' is unavailable.`);
114
+ }
115
+ if (repoKindsToSync.length > 0) {
116
+ syncDocumentsByRepoKinds(context.database, documents, repoKindsToSync);
117
+ }
118
+ const uniqueDocuments = dedupeDocuments(documents);
119
+ const stats = {
120
+ attemptedDocuments: documents.length,
121
+ uniqueDocuments: uniqueDocuments.length,
122
+ autotestsDocuments: uniqueDocuments.filter((record) => record.repoKind === 'autotests2').length,
123
+ caseplatformDocuments: uniqueDocuments.filter((record) => record.repoKind === 'caseplatform-web').length,
124
+ bySourceKind: buildSourceKindSummary(uniqueDocuments.map((record) => record.sourceKind)),
125
+ };
126
+ return {
127
+ documentsIndexed: documents.length,
128
+ stats,
129
+ warnings,
130
+ };
131
+ }
132
+ function documentFrom(input) {
133
+ return {
134
+ id: crypto.createHash('sha1').update(`${input.sourceKind}:${input.path}`).digest('hex'),
135
+ sourceKind: input.sourceKind,
136
+ repoKind: input.repoKind,
137
+ path: input.path,
138
+ title: input.title,
139
+ body: input.body,
140
+ metadataJson: JSON.stringify(input.metadata),
141
+ updatedAt: input.updatedAt,
142
+ };
143
+ }
144
+ function dedupeDocuments(documents) {
145
+ const seen = new Map();
146
+ for (const document of documents) {
147
+ seen.set(document.id, document);
148
+ }
149
+ return [...seen.values()];
150
+ }
151
+ function buildSourceKindSummary(sourceKinds) {
152
+ return sourceKinds.reduce((summary, sourceKind) => {
153
+ summary[sourceKind] = (summary[sourceKind] ?? 0) + 1;
154
+ return summary;
155
+ }, {});
156
+ }
@@ -0,0 +1,10 @@
1
+ export declare class SnapshotStore {
2
+ private readonly rootDir;
3
+ constructor(rootDir: string);
4
+ private snapshotPath;
5
+ read<T>(key: string): T | null;
6
+ write(key: string, value: unknown): {
7
+ changed: boolean;
8
+ hash: string;
9
+ };
10
+ }
@@ -0,0 +1,45 @@
1
+ import path from 'node:path';
2
+ import crypto from 'node:crypto';
3
+ import { ensureDirectory, readJson, writeJson } from '../utils/fileUtils.js';
4
+ export class SnapshotStore {
5
+ rootDir;
6
+ constructor(rootDir) {
7
+ this.rootDir = rootDir;
8
+ ensureDirectory(this.rootDir);
9
+ }
10
+ snapshotPath(key) {
11
+ return path.join(this.rootDir, `${key}.json`);
12
+ }
13
+ read(key) {
14
+ const filePath = this.snapshotPath(key);
15
+ try {
16
+ return readJson(filePath);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ write(key, value) {
23
+ const filePath = this.snapshotPath(key);
24
+ const serialized = JSON.stringify(value, null, 2);
25
+ const hash = crypto.createHash('sha1').update(serialized).digest('hex');
26
+ const manifest = this.read('manifest') ?? {};
27
+ const previous = manifest[key];
28
+ if (previous?.hash === hash) {
29
+ return {
30
+ changed: false,
31
+ hash,
32
+ };
33
+ }
34
+ writeJson(filePath, value);
35
+ manifest[key] = {
36
+ hash,
37
+ updatedAt: new Date().toISOString(),
38
+ };
39
+ writeJson(this.snapshotPath('manifest'), manifest);
40
+ return {
41
+ changed: true,
42
+ hash,
43
+ };
44
+ }
45
+ }
@@ -0,0 +1,3 @@
1
+ import Database from 'better-sqlite3';
2
+ export type KnowledgeDatabase = Database.Database;
3
+ export declare function createKnowledgeDatabase(filePath: string): KnowledgeDatabase;
@@ -0,0 +1,8 @@
1
+ import Database from 'better-sqlite3';
2
+ import { baseMigrations } from './migrations.js';
3
+ export function createKnowledgeDatabase(filePath) {
4
+ const db = new Database(filePath);
5
+ db.pragma('journal_mode = WAL');
6
+ db.exec(baseMigrations);
7
+ return db;
8
+ }
@@ -0,0 +1 @@
1
+ export declare const baseMigrations = "\nCREATE TABLE IF NOT EXISTS documents (\n id TEXT PRIMARY KEY,\n source_kind TEXT NOT NULL,\n repo_kind TEXT NOT NULL,\n path TEXT NOT NULL,\n title TEXT NOT NULL,\n body TEXT NOT NULL,\n metadata_json TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS documents_fts USING fts5(\n id UNINDEXED,\n path,\n title,\n body,\n tokenize = 'porter unicode61'\n);\n\nCREATE TABLE IF NOT EXISTS indexing_runs (\n run_id TEXT PRIMARY KEY,\n source_kind TEXT NOT NULL,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL,\n metadata_json TEXT NOT NULL\n);\n";
@@ -0,0 +1,28 @@
1
+ export const baseMigrations = `
2
+ CREATE TABLE IF NOT EXISTS documents (
3
+ id TEXT PRIMARY KEY,
4
+ source_kind TEXT NOT NULL,
5
+ repo_kind TEXT NOT NULL,
6
+ path TEXT NOT NULL,
7
+ title TEXT NOT NULL,
8
+ body TEXT NOT NULL,
9
+ metadata_json TEXT NOT NULL,
10
+ updated_at TEXT NOT NULL
11
+ );
12
+
13
+ CREATE VIRTUAL TABLE IF NOT EXISTS documents_fts USING fts5(
14
+ id UNINDEXED,
15
+ path,
16
+ title,
17
+ body,
18
+ tokenize = 'porter unicode61'
19
+ );
20
+
21
+ CREATE TABLE IF NOT EXISTS indexing_runs (
22
+ run_id TEXT PRIMARY KEY,
23
+ source_kind TEXT NOT NULL,
24
+ status TEXT NOT NULL,
25
+ created_at TEXT NOT NULL,
26
+ metadata_json TEXT NOT NULL
27
+ );
28
+ `;
@@ -0,0 +1,28 @@
1
+ import type { KnowledgeDatabase } from './database.js';
2
+ export type KnowledgeDocument = {
3
+ id: string;
4
+ sourceKind: string;
5
+ repoKind: string;
6
+ path: string;
7
+ title: string;
8
+ body: string;
9
+ metadataJson: string;
10
+ updatedAt: string;
11
+ };
12
+ export declare function upsertDocuments(db: KnowledgeDatabase, documents: KnowledgeDocument[]): void;
13
+ export declare function syncDocumentsByRepoKinds(db: KnowledgeDatabase, documents: KnowledgeDocument[], repoKinds: string[]): void;
14
+ export declare function countDocuments(db: KnowledgeDatabase): number;
15
+ type QueryOptions = {
16
+ limit?: number;
17
+ sourceKinds?: string[];
18
+ repoKind?: string;
19
+ };
20
+ export declare function queryDocuments(db: KnowledgeDatabase, query: string, options?: QueryOptions): Array<{
21
+ id: string;
22
+ path: string;
23
+ title: string;
24
+ body: string;
25
+ metadata_json: string;
26
+ rank: number;
27
+ }>;
28
+ export {};
@@ -0,0 +1,78 @@
1
+ export function upsertDocuments(db, documents) {
2
+ const upsertDocument = db.prepare(`
3
+ INSERT INTO documents (id, source_kind, repo_kind, path, title, body, metadata_json, updated_at)
4
+ VALUES (@id, @sourceKind, @repoKind, @path, @title, @body, @metadataJson, @updatedAt)
5
+ ON CONFLICT(id) DO UPDATE SET
6
+ source_kind = excluded.source_kind,
7
+ repo_kind = excluded.repo_kind,
8
+ path = excluded.path,
9
+ title = excluded.title,
10
+ body = excluded.body,
11
+ metadata_json = excluded.metadata_json,
12
+ updated_at = excluded.updated_at
13
+ `);
14
+ const deleteFts = db.prepare('DELETE FROM documents_fts WHERE id = ?');
15
+ const insertFts = db.prepare('INSERT INTO documents_fts (id, path, title, body) VALUES (?, ?, ?, ?)');
16
+ const transaction = db.transaction((records) => {
17
+ for (const record of records) {
18
+ upsertDocument.run(record);
19
+ deleteFts.run(record.id);
20
+ insertFts.run(record.id, record.path, record.title, record.body);
21
+ }
22
+ });
23
+ transaction(documents);
24
+ }
25
+ export function syncDocumentsByRepoKinds(db, documents, repoKinds) {
26
+ upsertDocuments(db, documents);
27
+ const normalizedRepoKinds = [...new Set(repoKinds)];
28
+ const selectIds = db.prepare('SELECT id FROM documents WHERE repo_kind = ?');
29
+ const deleteDocument = db.prepare('DELETE FROM documents WHERE id = ?');
30
+ const deleteFts = db.prepare('DELETE FROM documents_fts WHERE id = ?');
31
+ const transaction = db.transaction(() => {
32
+ for (const repoKind of normalizedRepoKinds) {
33
+ const desiredIds = new Set(documents.filter((record) => record.repoKind === repoKind).map((record) => record.id));
34
+ const existingIds = selectIds.all(repoKind);
35
+ for (const row of existingIds) {
36
+ if (!desiredIds.has(row.id)) {
37
+ deleteDocument.run(row.id);
38
+ deleteFts.run(row.id);
39
+ }
40
+ }
41
+ }
42
+ });
43
+ transaction();
44
+ }
45
+ export function countDocuments(db) {
46
+ const row = db.prepare('SELECT COUNT(*) AS count FROM documents').get();
47
+ return row.count;
48
+ }
49
+ export function queryDocuments(db, query, options = {}) {
50
+ const clauses = ['documents_fts MATCH ?'];
51
+ const params = [normalizeFtsQuery(query)];
52
+ if (options.sourceKinds?.length) {
53
+ clauses.push(`d.source_kind IN (${options.sourceKinds.map(() => '?').join(', ')})`);
54
+ params.push(...options.sourceKinds);
55
+ }
56
+ if (options.repoKind) {
57
+ clauses.push('d.repo_kind = ?');
58
+ params.push(options.repoKind);
59
+ }
60
+ params.push(options.limit ?? 25);
61
+ const statement = db.prepare(`
62
+ SELECT d.id, d.path, d.title, d.body, d.metadata_json, bm25(documents_fts) AS rank
63
+ FROM documents_fts
64
+ JOIN documents d ON d.id = documents_fts.id
65
+ WHERE ${clauses.join(' AND ')}
66
+ ORDER BY rank
67
+ LIMIT ?
68
+ `);
69
+ return statement.all(...params);
70
+ }
71
+ function normalizeFtsQuery(query) {
72
+ return query
73
+ .split(/[\s/\\_\-.]+/)
74
+ .map((token) => token.trim())
75
+ .filter(Boolean)
76
+ .map((token) => `"${token.replaceAll('"', '""')}"`)
77
+ .join(' OR ');
78
+ }
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod';
2
+ import type { AppContext } from '../appContext.js';
3
+ import { toolOutputSchema } from '../contracts/toolOutput.js';
4
+ export type ToolDefinition = {
5
+ name: string;
6
+ description: string;
7
+ inputSchema: z.ZodTypeAny;
8
+ outputSchema: typeof toolOutputSchema;
9
+ execute: (input: any, context: AppContext) => Promise<any>;
10
+ };
11
+ export declare function buildToolRegistry(): ToolDefinition[];