hadara 0.3.2 → 0.3.3

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 (52) hide show
  1. package/README.md +49 -34
  2. package/dist/cli/context.js +110 -0
  3. package/dist/cli/help.js +6 -7
  4. package/dist/cli/init.js +56 -58
  5. package/dist/cli/main.js +12 -0
  6. package/dist/cli/session.js +37 -0
  7. package/dist/cli/task.js +52 -0
  8. package/dist/context/code-graph-extractor.js +123 -0
  9. package/dist/context/code-index.js +1154 -0
  10. package/dist/context/context-cache-store.js +925 -0
  11. package/dist/context/context-graph-builder.js +341 -0
  12. package/dist/context/context-graph.js +42 -0
  13. package/dist/context/context-pack.js +457 -0
  14. package/dist/context/context-slice-boundary.js +37 -0
  15. package/dist/context/context-slice.js +487 -0
  16. package/dist/context/document-extractors.js +343 -0
  17. package/dist/context/evidence-extractors.js +179 -0
  18. package/dist/context/extractor-contract.js +166 -0
  19. package/dist/context/registry-extractors.js +177 -0
  20. package/dist/context/release-extractors.js +175 -0
  21. package/dist/context/session-start.js +297 -0
  22. package/dist/context/source-manifest.js +566 -0
  23. package/dist/context/state-projection.js +209 -0
  24. package/dist/context/task-extractors.js +168 -0
  25. package/dist/core/schema.js +26 -0
  26. package/dist/harness/validate.js +7 -8
  27. package/dist/schemas/code-index.schema.json +173 -0
  28. package/dist/schemas/context-cache-record.schema.json +56 -0
  29. package/dist/schemas/context-cache-status.schema.json +167 -0
  30. package/dist/schemas/context-cache-warm.schema.json +222 -0
  31. package/dist/schemas/context-graph.schema.json +286 -0
  32. package/dist/schemas/context-pack.schema.json +246 -0
  33. package/dist/schemas/context-slice.schema.json +94 -0
  34. package/dist/schemas/context-source-manifest.schema.json +147 -0
  35. package/dist/schemas/schema-index.json +91 -0
  36. package/dist/schemas/session-start.schema.json +158 -0
  37. package/dist/schemas/task-close-repair-plan.schema.json +67 -0
  38. package/dist/schemas/task-context.schema.json +125 -0
  39. package/dist/schemas/task-finalize.schema.json +98 -0
  40. package/dist/schemas/task-lifecycle.schema.json +84 -0
  41. package/dist/services/capability-registry.js +266 -9
  42. package/dist/services/lifecycle-guide.js +23 -29
  43. package/dist/services/protocol-consistency.js +6 -8
  44. package/dist/services/workbench-next-actions.js +2 -0
  45. package/dist/task/acceptance.js +171 -0
  46. package/dist/task/task-close-repair-plan.js +190 -0
  47. package/dist/task/task-close.js +34 -35
  48. package/dist/task/task-finalize.js +377 -0
  49. package/dist/task/task-lifecycle.js +210 -0
  50. package/dist/task/task-next.js +10 -1
  51. package/dist/task/task-ready.js +4 -0
  52. package/package.json +1 -1
@@ -0,0 +1,1154 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CODE_FILE_LANGUAGES = exports.CODE_FILE_KINDS = exports.CODE_INDEX_EDGE_TYPES = exports.CODE_INDEX_IGNORED_PATHS = exports.CODE_INDEX_DEFAULT_BUDGETS = exports.CODE_INDEX_FILE_SUMMARY_CACHE_VERSION = exports.CODE_INDEX_FILE_SUMMARY_CACHE_SCHEMA_ID = exports.CODE_INDEX_FILE_SUMMARY_CACHE_ROOT = exports.CODE_INDEX_CACHE_ROOT = exports.CODE_INDEX_EXTRACTOR_VERSION = exports.CODE_INDEX_COMMAND = exports.CODE_INDEX_SCHEMA_ID = void 0;
7
+ exports.buildCodeIndexReport = buildCodeIndexReport;
8
+ exports.codeIndexFileSummaryCachePath = codeIndexFileSummaryCachePath;
9
+ exports.discoverCodeIndexFiles = discoverCodeIndexFiles;
10
+ exports.createCodeFileNode = createCodeFileNode;
11
+ exports.extractCodeFileReferences = extractCodeFileReferences;
12
+ exports.classifyCodeFile = classifyCodeFile;
13
+ exports.detectCodeFileLanguage = detectCodeFileLanguage;
14
+ exports.shouldIgnoreCodeIndexPath = shouldIgnoreCodeIndexPath;
15
+ exports.createCodeFileNodeId = createCodeFileNodeId;
16
+ exports.createCodeSymbolNodeId = createCodeSymbolNodeId;
17
+ exports.toCodeIndexRelativePath = toCodeIndexRelativePath;
18
+ exports.summarizeCodeIndex = summarizeCodeIndex;
19
+ const node_fs_1 = __importDefault(require("node:fs"));
20
+ const node_path_1 = __importDefault(require("node:path"));
21
+ const fs_1 = require("../core/fs");
22
+ const extractor_contract_1 = require("./extractor-contract");
23
+ const capability_registry_1 = require("../services/capability-registry");
24
+ exports.CODE_INDEX_SCHEMA_ID = 'hadara.codeIndex.v1';
25
+ exports.CODE_INDEX_COMMAND = 'code.index';
26
+ exports.CODE_INDEX_EXTRACTOR_VERSION = 'c2-code-index-v1';
27
+ exports.CODE_INDEX_CACHE_ROOT = '.hadara/local/cache/context';
28
+ exports.CODE_INDEX_FILE_SUMMARY_CACHE_ROOT = `${exports.CODE_INDEX_CACHE_ROOT}/code-index-files`;
29
+ exports.CODE_INDEX_FILE_SUMMARY_CACHE_SCHEMA_ID = 'hadara.codeIndex.fileSummaryCacheRecord.v1';
30
+ exports.CODE_INDEX_FILE_SUMMARY_CACHE_VERSION = 'c6.6-code-index-file-summary-v1';
31
+ exports.CODE_INDEX_DEFAULT_BUDGETS = {
32
+ maxIndexedFiles: 2000,
33
+ maxIndexedBytes: 20 * 1024 * 1024,
34
+ maxSingleFileBytes: 1024 * 1024
35
+ };
36
+ exports.CODE_INDEX_IGNORED_PATHS = [
37
+ 'node_modules',
38
+ 'dist',
39
+ 'coverage',
40
+ '.git',
41
+ '.hadara/local',
42
+ '.hadara/tmp',
43
+ '.pytest_cache',
44
+ '.mypy_cache',
45
+ '.ruff_cache',
46
+ '.venv',
47
+ 'venv'
48
+ ];
49
+ exports.CODE_INDEX_EDGE_TYPES = [
50
+ 'IMPORTS',
51
+ 'EXPORTS',
52
+ 'DEFINES_SYMBOL',
53
+ 'TESTS_FILE',
54
+ 'IMPLEMENTS_COMMAND',
55
+ 'REFERENCED_BY_DOC',
56
+ 'VALIDATED_BY_EVIDENCE'
57
+ ];
58
+ exports.CODE_FILE_KINDS = ['source', 'test', 'fixture', 'script', 'config', 'unknown'];
59
+ exports.CODE_FILE_LANGUAGES = ['typescript', 'javascript', 'json', 'markdown', 'unknown'];
60
+ const COMMAND_REGISTRY_SOURCE_PATH = 'src/services/capability-registry.ts';
61
+ function buildCodeIndexReport(options) {
62
+ const generatedAt = options.generatedAt ?? new Date().toISOString();
63
+ const budgets = normalizeCodeIndexBudgets(options.budgets);
64
+ const discovered = discoverCodeIndexInputs(options.projectRoot, {
65
+ budgets,
66
+ sourceEntries: options.sourceEntries
67
+ });
68
+ const rawSummaries = [];
69
+ const issues = [...discovered.issues];
70
+ let indexedBytes = 0;
71
+ let skippedFiles = discovered.skippedFiles;
72
+ const cacheStats = createCodeIndexFileSummaryCacheStats();
73
+ const cacheOptions = options.fileSummaryCache;
74
+ const extractorVersion = cacheOptions?.extractorVersion ?? exports.CODE_INDEX_EXTRACTOR_VERSION;
75
+ for (const relativePath of discovered.paths) {
76
+ const absolutePath = node_path_1.default.join(options.projectRoot, relativePath);
77
+ try {
78
+ const sourceEntry = discovered.sourceEntriesByPath.get(relativePath) ?? createCodeIndexSourceEntry(options.projectRoot, relativePath);
79
+ if (sourceEntry.sizeBytes > budgets.maxSingleFileBytes) {
80
+ skippedFiles += 1;
81
+ issues.push(createBudgetIssue({
82
+ message: `Skipped ${relativePath} because it exceeds the single-file code index budget (${sourceEntry.sizeBytes} bytes > ${budgets.maxSingleFileBytes} bytes).`,
83
+ path: relativePath,
84
+ fixHint: 'Reduce the file size or wait for a future configurable code index budget.'
85
+ }));
86
+ continue;
87
+ }
88
+ if (indexedBytes + sourceEntry.sizeBytes > budgets.maxIndexedBytes) {
89
+ skippedFiles += 1;
90
+ issues.push(createBudgetIssue({
91
+ message: `Skipped ${relativePath} because the code index byte budget would be exceeded (${indexedBytes + sourceEntry.sizeBytes} bytes > ${budgets.maxIndexedBytes} bytes).`,
92
+ path: relativePath,
93
+ fixHint: 'Reduce indexed source size or wait for a future configurable code index budget.'
94
+ }));
95
+ continue;
96
+ }
97
+ const cached = readCodeIndexFileSummaryCache({
98
+ projectRoot: options.projectRoot,
99
+ sourceEntry,
100
+ cacheOptions,
101
+ extractorVersion
102
+ });
103
+ updateCodeIndexFileSummaryCacheStats(cacheStats, cached.status);
104
+ if (cached.issue)
105
+ issues.push(cached.issue);
106
+ if (cached.summary) {
107
+ rawSummaries.push(cached.summary);
108
+ indexedBytes += sourceEntry.sizeBytes;
109
+ continue;
110
+ }
111
+ const content = node_fs_1.default.readFileSync(absolutePath, 'utf8');
112
+ indexedBytes += sourceEntry.sizeBytes;
113
+ const summary = extractCodeFileSummary({
114
+ projectRoot: options.projectRoot,
115
+ path: relativePath,
116
+ content,
117
+ sizeBytes: sourceEntry.sizeBytes
118
+ });
119
+ issues.push(...summary.issues);
120
+ rawSummaries.push(summary);
121
+ if (cacheOptions?.mode === 'read-write') {
122
+ writeCodeIndexFileSummaryCache({
123
+ projectRoot: options.projectRoot,
124
+ sourceEntry,
125
+ summary,
126
+ createdAt: cacheOptions.createdAt ?? generatedAt,
127
+ extractorVersion
128
+ });
129
+ }
130
+ }
131
+ catch (error) {
132
+ issues.push({
133
+ severity: 'warning',
134
+ code: 'CODE_INDEX_FILE_READ_FAILED',
135
+ message: `Failed to read code index file ${relativePath}: ${error instanceof Error ? error.message : String(error)}.`,
136
+ path: relativePath,
137
+ fixHint: 'Check file permissions or remove the unreadable file from indexed source paths.'
138
+ });
139
+ }
140
+ }
141
+ const sanitized = sanitizeCodeFileSummaries(rawSummaries);
142
+ const fileSummaries = sanitized.summaries;
143
+ issues.push(...sanitized.issues);
144
+ const commandHints = createCommandHints(fileSummaries);
145
+ const commandFamiliesByPath = createCommandFamiliesByPath(commandHints);
146
+ const files = fileSummaries.map((summary) => createCodeFileNodeFromSummary(summary, {
147
+ imports: summary.imports.map((importReference) => importReference.resolvedPath ?? importReference.specifier),
148
+ exports: summary.exports.map((exportReference) => exportReference.name),
149
+ commandFamilies: commandFamiliesByPath.get(summary.filePath) ?? []
150
+ }));
151
+ const symbols = createCodeSymbolNodes(fileSummaries);
152
+ const edges = [
153
+ ...createImportEdges(fileSummaries),
154
+ ...createSymbolEdges(fileSummaries),
155
+ ...createCommandHintEdges(fileSummaries, commandHints),
156
+ ...createTestRelationEdges(options.projectRoot, fileSummaries)
157
+ ].sort((a, b) => a.id.localeCompare(b.id));
158
+ return {
159
+ schemaVersion: exports.CODE_INDEX_SCHEMA_ID,
160
+ command: exports.CODE_INDEX_COMMAND,
161
+ ok: !issues.some((issue) => issue.severity === 'error'),
162
+ generatedAt,
163
+ projectRoot: options.projectRoot,
164
+ sourceHash: (0, extractor_contract_1.hashContextGraphSources)(files.map((file) => ({ path: file.path, hash: file.hash }))),
165
+ files,
166
+ symbols,
167
+ edges,
168
+ summary: summarizeCodeIndex(files, symbols, edges, issues),
169
+ budget: {
170
+ ...budgets,
171
+ indexedFiles: files.length,
172
+ indexedBytes,
173
+ skippedFiles
174
+ },
175
+ cache: createCodeIndexCacheMetadata(cacheOptions, cacheStats),
176
+ issues
177
+ };
178
+ }
179
+ function codeIndexFileSummaryCachePath(relativePath) {
180
+ const normalizedPath = (0, extractor_contract_1.normalizeContextGraphPath)(relativePath);
181
+ const fingerprint = (0, extractor_contract_1.hashContextGraphText)(normalizedPath).replace(/^sha256:/, '');
182
+ return `${exports.CODE_INDEX_FILE_SUMMARY_CACHE_ROOT}/${fingerprint}.json`;
183
+ }
184
+ function discoverCodeIndexFiles(projectRoot, options = {}) {
185
+ const budgets = normalizeCodeIndexBudgets(options.budgets);
186
+ const paths = [];
187
+ const issues = [];
188
+ let skippedFiles = 0;
189
+ let fileBudgetIssueRecorded = false;
190
+ const root = node_path_1.default.resolve(projectRoot);
191
+ function visit(relativeDir) {
192
+ if (relativeDir && shouldIgnoreCodeIndexPath(relativeDir))
193
+ return;
194
+ const absoluteDir = node_path_1.default.join(root, relativeDir);
195
+ let entries;
196
+ try {
197
+ entries = node_fs_1.default.readdirSync(absoluteDir, { withFileTypes: true });
198
+ }
199
+ catch (error) {
200
+ issues.push({
201
+ severity: 'warning',
202
+ code: 'CODE_INDEX_FILE_READ_FAILED',
203
+ message: `Failed to read code index directory ${(0, extractor_contract_1.normalizeContextGraphPath)(relativeDir || '.')}: ${error instanceof Error ? error.message : String(error)}.`,
204
+ path: (0, extractor_contract_1.normalizeContextGraphPath)(relativeDir || '.'),
205
+ fixHint: 'Check directory permissions or remove the unreadable directory from indexed source paths.'
206
+ });
207
+ return;
208
+ }
209
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
210
+ const relativePath = (0, extractor_contract_1.normalizeContextGraphPath)(node_path_1.default.join(relativeDir, entry.name));
211
+ if (shouldIgnoreCodeIndexPath(relativePath))
212
+ continue;
213
+ if (entry.isDirectory()) {
214
+ visit(relativePath);
215
+ continue;
216
+ }
217
+ if (entry.isFile() && classifyCodeFile(relativePath) !== 'unknown') {
218
+ if (paths.length >= budgets.maxIndexedFiles) {
219
+ skippedFiles += 1;
220
+ if (!fileBudgetIssueRecorded) {
221
+ fileBudgetIssueRecorded = true;
222
+ issues.push(createBudgetIssue({
223
+ message: `Code index exceeded max indexed files budget (${budgets.maxIndexedFiles}); partial results returned.`,
224
+ path: relativePath,
225
+ fixHint: 'Reduce indexed files or wait for a future configurable code index budget.'
226
+ }));
227
+ }
228
+ continue;
229
+ }
230
+ paths.push(relativePath);
231
+ }
232
+ }
233
+ }
234
+ visit('');
235
+ return { paths: Array.from(new Set(paths)).sort(), issues, skippedFiles };
236
+ }
237
+ function discoverCodeIndexInputs(projectRoot, options) {
238
+ if (!options.sourceEntries) {
239
+ const discovered = discoverCodeIndexFiles(projectRoot, { budgets: options.budgets });
240
+ return {
241
+ ...discovered,
242
+ sourceEntriesByPath: new Map()
243
+ };
244
+ }
245
+ const sourceEntriesByPath = new Map();
246
+ const paths = [];
247
+ const issues = [];
248
+ let skippedFiles = 0;
249
+ let fileBudgetIssueRecorded = false;
250
+ for (const sourceEntry of options.sourceEntries) {
251
+ if (!sourceEntry.extractorKeys?.includes('codeIndex'))
252
+ continue;
253
+ const relativePath = (0, extractor_contract_1.normalizeContextGraphPath)(sourceEntry.path);
254
+ if (shouldIgnoreCodeIndexPath(relativePath) || classifyCodeFile(relativePath) === 'unknown')
255
+ continue;
256
+ if (paths.length >= options.budgets.maxIndexedFiles) {
257
+ skippedFiles += 1;
258
+ if (!fileBudgetIssueRecorded) {
259
+ fileBudgetIssueRecorded = true;
260
+ issues.push(createBudgetIssue({
261
+ message: `Code index exceeded max indexed files budget (${options.budgets.maxIndexedFiles}); partial results returned.`,
262
+ path: relativePath,
263
+ fixHint: 'Reduce indexed files or wait for a future configurable code index budget.'
264
+ }));
265
+ }
266
+ continue;
267
+ }
268
+ const normalizedEntry = {
269
+ ...sourceEntry,
270
+ path: relativePath
271
+ };
272
+ paths.push(relativePath);
273
+ sourceEntriesByPath.set(relativePath, normalizedEntry);
274
+ }
275
+ return {
276
+ paths: Array.from(new Set(paths)).sort(),
277
+ sourceEntriesByPath,
278
+ issues,
279
+ skippedFiles
280
+ };
281
+ }
282
+ function createCodeIndexSourceEntry(projectRoot, relativePath) {
283
+ const normalizedPath = (0, extractor_contract_1.normalizeContextGraphPath)(relativePath);
284
+ const stats = node_fs_1.default.statSync(node_path_1.default.join(projectRoot, normalizedPath));
285
+ return {
286
+ path: normalizedPath,
287
+ sizeBytes: stats.size,
288
+ mtimeMs: stats.mtimeMs,
289
+ metadataHash: (0, extractor_contract_1.hashContextGraphJson)({
290
+ path: normalizedPath,
291
+ sizeBytes: stats.size,
292
+ mtimeMs: stats.mtimeMs
293
+ }),
294
+ extractorKeys: ['codeIndex']
295
+ };
296
+ }
297
+ function normalizeCodeIndexBudgets(input = {}) {
298
+ return {
299
+ maxIndexedFiles: normalizeBudgetValue(input.maxIndexedFiles, exports.CODE_INDEX_DEFAULT_BUDGETS.maxIndexedFiles),
300
+ maxIndexedBytes: normalizeBudgetValue(input.maxIndexedBytes, exports.CODE_INDEX_DEFAULT_BUDGETS.maxIndexedBytes),
301
+ maxSingleFileBytes: normalizeBudgetValue(input.maxSingleFileBytes, exports.CODE_INDEX_DEFAULT_BUDGETS.maxSingleFileBytes)
302
+ };
303
+ }
304
+ function normalizeBudgetValue(value, fallback) {
305
+ if (value === undefined || !Number.isFinite(value))
306
+ return fallback;
307
+ return Math.max(0, Math.floor(value));
308
+ }
309
+ function createBudgetIssue(input) {
310
+ return {
311
+ severity: 'warning',
312
+ code: 'CODE_INDEX_TOO_LARGE',
313
+ message: input.message,
314
+ ...(input.path ? { path: input.path } : {}),
315
+ fixHint: input.fixHint
316
+ };
317
+ }
318
+ function createCodeFileNode(relativePath, content, metadata = {}) {
319
+ const normalizedPath = (0, extractor_contract_1.normalizeContextGraphPath)(relativePath);
320
+ return {
321
+ id: createCodeFileNodeId(normalizedPath),
322
+ path: normalizedPath,
323
+ kind: classifyCodeFile(normalizedPath),
324
+ language: detectCodeFileLanguage(normalizedPath),
325
+ hash: (0, extractor_contract_1.hashContextGraphText)(content),
326
+ lineCount: countLines(content),
327
+ exports: uniqueSorted(metadata.exports ?? []),
328
+ imports: uniqueSorted(metadata.imports ?? []),
329
+ commandFamilies: uniqueSorted(metadata.commandFamilies ?? [])
330
+ };
331
+ }
332
+ function createCodeFileNodeFromSummary(summary, metadata = {}) {
333
+ return {
334
+ id: createCodeFileNodeId(summary.filePath),
335
+ path: (0, extractor_contract_1.normalizeContextGraphPath)(summary.filePath),
336
+ kind: summary.kind,
337
+ language: summary.language,
338
+ hash: summary.hash,
339
+ lineCount: summary.lineCount,
340
+ exports: uniqueSorted(metadata.exports ?? []),
341
+ imports: uniqueSorted(metadata.imports ?? []),
342
+ commandFamilies: uniqueSorted(metadata.commandFamilies ?? [])
343
+ };
344
+ }
345
+ function extractCodeFileReferences(input) {
346
+ const imports = [];
347
+ const exports = [];
348
+ const issues = [];
349
+ const normalizedPath = (0, extractor_contract_1.normalizeContextGraphPath)(input.path);
350
+ const language = detectCodeFileLanguage(normalizedPath);
351
+ if (language !== 'typescript' && language !== 'javascript') {
352
+ return { imports, exports, issues };
353
+ }
354
+ const lines = input.content.split(/\r?\n/);
355
+ lines.forEach((line, index) => {
356
+ const lineNumber = index + 1;
357
+ for (const specifier of extractImportSpecifiersFromLine(line)) {
358
+ const resolvedPath = resolveRelativeCodeImport({
359
+ projectRoot: input.projectRoot,
360
+ fromPath: normalizedPath,
361
+ specifier
362
+ });
363
+ imports.push({
364
+ specifier,
365
+ ...(resolvedPath ? { resolvedPath } : {}),
366
+ line: lineNumber
367
+ });
368
+ if (isRelativeImportSpecifier(specifier) && !resolvedPath) {
369
+ issues.push({
370
+ severity: 'warning',
371
+ code: 'CODE_INDEX_IMPORT_UNRESOLVED',
372
+ message: `Could not resolve relative import ${specifier} from ${normalizedPath}.`,
373
+ path: normalizedPath,
374
+ fixHint: 'Check that the import target exists, uses a supported extension, and is not ignored by code index rules.'
375
+ });
376
+ }
377
+ }
378
+ for (const exportReference of extractExportReferencesFromLine(line)) {
379
+ exports.push({ ...exportReference, line: lineNumber });
380
+ }
381
+ });
382
+ return {
383
+ imports: dedupeImports(imports),
384
+ exports: dedupeExports(exports),
385
+ issues
386
+ };
387
+ }
388
+ function extractCodeFileSummary(input) {
389
+ const normalizedPath = (0, extractor_contract_1.normalizeContextGraphPath)(input.path);
390
+ const references = extractCodeFileReferences({
391
+ projectRoot: input.projectRoot,
392
+ path: normalizedPath,
393
+ content: input.content
394
+ });
395
+ return {
396
+ filePath: normalizedPath,
397
+ kind: classifyCodeFile(normalizedPath),
398
+ language: detectCodeFileLanguage(normalizedPath),
399
+ hash: (0, extractor_contract_1.hashContextGraphText)(input.content),
400
+ lineCount: countLines(input.content),
401
+ sizeBytes: input.sizeBytes,
402
+ imports: references.imports,
403
+ exports: references.exports,
404
+ commandMentions: extractCommandMentions(input.content),
405
+ ...(normalizedPath === COMMAND_REGISTRY_SOURCE_PATH
406
+ ? { commandIdLines: Object.fromEntries(createRegistryLineMap(input.content).entries()) }
407
+ : {}),
408
+ issues: references.issues
409
+ };
410
+ }
411
+ function extractCommandMentions(content) {
412
+ const mentions = [];
413
+ for (const entry of (0, capability_registry_1.listCommandRegistryEntries)()) {
414
+ const line = findCommandMentionLine(content, entry.id);
415
+ if (line !== undefined)
416
+ mentions.push({ commandId: entry.id, line });
417
+ }
418
+ return mentions.sort((a, b) => `${a.commandId}:${a.line}`.localeCompare(`${b.commandId}:${b.line}`));
419
+ }
420
+ function classifyCodeFile(inputPath) {
421
+ const filePath = (0, extractor_contract_1.normalizeContextGraphPath)(inputPath);
422
+ if (filePath === 'package.json' || filePath === 'tsconfig.json')
423
+ return 'config';
424
+ if (filePath.startsWith('tests/fixtures/'))
425
+ return 'fixture';
426
+ if (filePath.startsWith('tests/') && (filePath.endsWith('.test.ts') || filePath.endsWith('.spec.ts')))
427
+ return 'test';
428
+ if (filePath.startsWith('src/') && (filePath.endsWith('.ts') || filePath.endsWith('.js')))
429
+ return 'source';
430
+ if (filePath.startsWith('scripts/'))
431
+ return 'script';
432
+ return 'unknown';
433
+ }
434
+ function detectCodeFileLanguage(inputPath) {
435
+ const filePath = (0, extractor_contract_1.normalizeContextGraphPath)(inputPath);
436
+ if (filePath.endsWith('.ts'))
437
+ return 'typescript';
438
+ if (filePath.endsWith('.js'))
439
+ return 'javascript';
440
+ if (filePath.endsWith('.json'))
441
+ return 'json';
442
+ if (filePath.endsWith('.md'))
443
+ return 'markdown';
444
+ return 'unknown';
445
+ }
446
+ function shouldIgnoreCodeIndexPath(inputPath) {
447
+ const normalizedPath = (0, extractor_contract_1.normalizeContextGraphPath)(inputPath);
448
+ if (!normalizedPath || normalizedPath === '.')
449
+ return false;
450
+ return exports.CODE_INDEX_IGNORED_PATHS.some((ignoredPath) => normalizedPath === ignoredPath || normalizedPath.startsWith(`${ignoredPath}/`));
451
+ }
452
+ function createCodeFileNodeId(inputPath) {
453
+ return `file:${(0, extractor_contract_1.normalizeContextGraphPath)(inputPath)}`;
454
+ }
455
+ function createCodeSymbolNodeId(inputPath, name) {
456
+ return `symbol:${(0, extractor_contract_1.normalizeContextGraphPath)(inputPath)}#${name}`;
457
+ }
458
+ function toCodeIndexRelativePath(projectRoot, absoluteOrRelativePath) {
459
+ return (0, extractor_contract_1.toProjectRelativeContextPath)(projectRoot, absoluteOrRelativePath);
460
+ }
461
+ function summarizeCodeIndex(files, symbols, edges, issues) {
462
+ return {
463
+ sourceFiles: files.filter((file) => file.kind === 'source').length,
464
+ testFiles: files.filter((file) => file.kind === 'test').length,
465
+ fixtureFiles: files.filter((file) => file.kind === 'fixture').length,
466
+ configFiles: files.filter((file) => file.kind === 'config').length,
467
+ symbols: symbols.length,
468
+ edges: edges.length,
469
+ degraded: issues.some((issue) => issue.severity === 'warning' || issue.severity === 'error')
470
+ };
471
+ }
472
+ function sanitizeCodeFileSummaries(summaries) {
473
+ const availablePaths = new Set(summaries.map((summary) => summary.filePath));
474
+ const issues = [];
475
+ return {
476
+ summaries: summaries.map((summary) => {
477
+ const imports = summary.imports.map((importReference) => {
478
+ if (!importReference.resolvedPath || availablePaths.has(importReference.resolvedPath))
479
+ return importReference;
480
+ if (isRelativeImportSpecifier(importReference.specifier)) {
481
+ issues.push({
482
+ severity: 'warning',
483
+ code: 'CODE_INDEX_IMPORT_UNRESOLVED',
484
+ message: `Could not resolve relative import ${importReference.specifier} from ${summary.filePath}.`,
485
+ path: summary.filePath,
486
+ fixHint: 'Check that the import target exists, uses a supported extension, and is not ignored by code index rules.'
487
+ });
488
+ }
489
+ return {
490
+ specifier: importReference.specifier,
491
+ line: importReference.line
492
+ };
493
+ });
494
+ return { ...summary, imports: dedupeImports(imports) };
495
+ }),
496
+ issues
497
+ };
498
+ }
499
+ function createCodeIndexFileSummaryCacheStats() {
500
+ return {
501
+ readCount: 0,
502
+ reusedFileCount: 0,
503
+ recomputedFileCount: 0,
504
+ missingFileCount: 0,
505
+ staleFileCount: 0,
506
+ corruptFileCount: 0,
507
+ schemaMismatchFileCount: 0
508
+ };
509
+ }
510
+ function updateCodeIndexFileSummaryCacheStats(stats, status) {
511
+ if (status === 'disabled')
512
+ return;
513
+ stats.readCount += 1;
514
+ if (status === 'fresh')
515
+ stats.reusedFileCount += 1;
516
+ else {
517
+ stats.recomputedFileCount += 1;
518
+ if (status === 'missing')
519
+ stats.missingFileCount += 1;
520
+ if (status === 'stale')
521
+ stats.staleFileCount += 1;
522
+ if (status === 'corrupt')
523
+ stats.corruptFileCount += 1;
524
+ if (status === 'schema-mismatch')
525
+ stats.schemaMismatchFileCount += 1;
526
+ }
527
+ }
528
+ function createCodeIndexCacheMetadata(cacheOptions, stats) {
529
+ if (!cacheOptions)
530
+ return { used: false, hit: false };
531
+ return {
532
+ used: stats.reusedFileCount > 0,
533
+ hit: stats.readCount > 0 && stats.recomputedFileCount === 0,
534
+ mode: 'code-index-file-summaries',
535
+ cachePath: exports.CODE_INDEX_FILE_SUMMARY_CACHE_ROOT,
536
+ readFileSummaryCount: stats.readCount,
537
+ reusedFileSummaryCount: stats.reusedFileCount,
538
+ recomputedFileSummaryCount: stats.recomputedFileCount,
539
+ missingFileSummaryCount: stats.missingFileCount,
540
+ staleFileSummaryCount: stats.staleFileCount,
541
+ corruptFileSummaryCount: stats.corruptFileCount,
542
+ schemaMismatchFileSummaryCount: stats.schemaMismatchFileCount
543
+ };
544
+ }
545
+ function readCodeIndexFileSummaryCache(input) {
546
+ if (!input.cacheOptions)
547
+ return { status: 'disabled' };
548
+ const cachePath = codeIndexFileSummaryCachePath(input.sourceEntry.path);
549
+ const absolutePath = node_path_1.default.join(input.projectRoot, cachePath);
550
+ if (!node_fs_1.default.existsSync(absolutePath))
551
+ return { status: 'missing', path: cachePath };
552
+ let parsed;
553
+ try {
554
+ parsed = JSON.parse(node_fs_1.default.readFileSync(absolutePath, 'utf8'));
555
+ }
556
+ catch (error) {
557
+ return {
558
+ status: 'corrupt',
559
+ path: cachePath,
560
+ issue: {
561
+ severity: 'warning',
562
+ code: 'CODE_INDEX_FILE_CACHE_CORRUPT',
563
+ message: `Code index file summary cache at ${cachePath} could not be parsed: ${error instanceof Error ? error.message : String(error)}.`,
564
+ path: input.sourceEntry.path,
565
+ fixHint: 'Refresh the context cache with context cache warm --execute.'
566
+ }
567
+ };
568
+ }
569
+ if (!isCodeIndexFileSummaryCacheRecord(parsed)) {
570
+ return {
571
+ status: 'schema-mismatch',
572
+ path: cachePath,
573
+ issue: {
574
+ severity: 'warning',
575
+ code: 'CODE_INDEX_FILE_CACHE_SCHEMA_MISMATCH',
576
+ message: `Code index file summary cache at ${cachePath} does not match ${exports.CODE_INDEX_FILE_SUMMARY_CACHE_SCHEMA_ID}.`,
577
+ path: input.sourceEntry.path,
578
+ fixHint: 'Refresh the context cache with context cache warm --execute.'
579
+ }
580
+ };
581
+ }
582
+ if (!isFreshCodeIndexFileSummaryCacheRecord(parsed, input.sourceEntry, input.extractorVersion)) {
583
+ return { status: 'stale', path: cachePath, record: parsed };
584
+ }
585
+ return { status: 'fresh', path: cachePath, record: parsed, summary: parsed.summary };
586
+ }
587
+ function writeCodeIndexFileSummaryCache(input) {
588
+ const normalizedPath = (0, extractor_contract_1.normalizeContextGraphPath)(input.sourceEntry.path);
589
+ const record = {
590
+ schemaVersion: exports.CODE_INDEX_FILE_SUMMARY_CACHE_SCHEMA_ID,
591
+ cacheRecordVersion: exports.CODE_INDEX_FILE_SUMMARY_CACHE_VERSION,
592
+ createdAt: input.createdAt,
593
+ path: normalizedPath,
594
+ extractorVersion: input.extractorVersion,
595
+ source: {
596
+ path: normalizedPath,
597
+ sizeBytes: input.sourceEntry.sizeBytes,
598
+ ...(input.sourceEntry.mtimeMs === undefined ? {} : { mtimeMs: input.sourceEntry.mtimeMs }),
599
+ ...(input.sourceEntry.contentHash ? { contentHash: input.sourceEntry.contentHash } : {}),
600
+ ...(input.sourceEntry.metadataHash ? { metadataHash: input.sourceEntry.metadataHash } : {})
601
+ },
602
+ summary: input.summary
603
+ };
604
+ (0, fs_1.atomicWriteTextFile)(input.projectRoot, codeIndexFileSummaryCachePath(normalizedPath), `${JSON.stringify(record, null, 2)}\n`);
605
+ return record;
606
+ }
607
+ function isFreshCodeIndexFileSummaryCacheRecord(record, sourceEntry, extractorVersion) {
608
+ const normalizedPath = (0, extractor_contract_1.normalizeContextGraphPath)(sourceEntry.path);
609
+ return record.path === normalizedPath
610
+ && record.source.path === normalizedPath
611
+ && record.summary.filePath === normalizedPath
612
+ && record.extractorVersion === extractorVersion
613
+ && record.source.sizeBytes === sourceEntry.sizeBytes
614
+ && record.source.mtimeMs === sourceEntry.mtimeMs
615
+ && record.source.contentHash === sourceEntry.contentHash
616
+ && record.source.metadataHash === sourceEntry.metadataHash;
617
+ }
618
+ function isCodeIndexFileSummaryCacheRecord(value) {
619
+ if (!value || typeof value !== 'object')
620
+ return false;
621
+ const record = value;
622
+ return record.schemaVersion === exports.CODE_INDEX_FILE_SUMMARY_CACHE_SCHEMA_ID
623
+ && record.cacheRecordVersion === exports.CODE_INDEX_FILE_SUMMARY_CACHE_VERSION
624
+ && typeof record.createdAt === 'string'
625
+ && typeof record.path === 'string'
626
+ && typeof record.extractorVersion === 'string'
627
+ && Boolean(record.source)
628
+ && typeof record.source === 'object'
629
+ && typeof record.source.path === 'string'
630
+ && typeof record.source.sizeBytes === 'number'
631
+ && isCodeFileExtractionSummary(record.summary);
632
+ }
633
+ function isCodeFileExtractionSummary(value) {
634
+ if (!value || typeof value !== 'object')
635
+ return false;
636
+ const summary = value;
637
+ return typeof summary.filePath === 'string'
638
+ && exports.CODE_FILE_KINDS.includes(summary.kind)
639
+ && exports.CODE_FILE_LANGUAGES.includes(summary.language)
640
+ && typeof summary.hash === 'string'
641
+ && typeof summary.lineCount === 'number'
642
+ && typeof summary.sizeBytes === 'number'
643
+ && Array.isArray(summary.imports)
644
+ && Array.isArray(summary.exports)
645
+ && Array.isArray(summary.commandMentions)
646
+ && Array.isArray(summary.issues);
647
+ }
648
+ function countLines(content) {
649
+ if (content.length === 0)
650
+ return 0;
651
+ return content.endsWith('\n') ? content.split('\n').length - 1 : content.split('\n').length;
652
+ }
653
+ function extractImportSpecifiersFromLine(line) {
654
+ const specifiers = [];
655
+ const importMatch = line.match(/^\s*import\s+(?:type\s+)?(?:.+?\s+from\s+)?['"]([^'"]+)['"]/);
656
+ if (importMatch?.[1])
657
+ specifiers.push(importMatch[1]);
658
+ const exportFromMatch = line.match(/^\s*export\s+(?:type\s+)?\{[^}]*\}\s+from\s+['"]([^'"]+)['"]/);
659
+ if (exportFromMatch?.[1])
660
+ specifiers.push(exportFromMatch[1]);
661
+ const requirePattern = /require\(\s*['"]([^'"]+)['"]\s*\)/g;
662
+ for (const requireMatch of line.matchAll(requirePattern)) {
663
+ if (requireMatch[1])
664
+ specifiers.push(requireMatch[1]);
665
+ }
666
+ return specifiers;
667
+ }
668
+ function extractExportReferencesFromLine(line) {
669
+ const trimmed = line.trim();
670
+ const exports = [];
671
+ const declarationMatch = trimmed.match(/^export\s+(?:async\s+)?(function|class|interface|type|const)\s+([A-Za-z_$][\w$]*)/);
672
+ if (declarationMatch?.[1] && declarationMatch[2]) {
673
+ exports.push({
674
+ name: declarationMatch[2],
675
+ kind: declarationMatch[1]
676
+ });
677
+ }
678
+ const listMatch = trimmed.match(/^export\s+(?:type\s+)?\{([^}]+)\}/);
679
+ if (listMatch?.[1]) {
680
+ for (const part of listMatch[1].split(',')) {
681
+ const candidate = part.trim();
682
+ if (!candidate)
683
+ continue;
684
+ const aliasParts = candidate.split(/\s+as\s+/);
685
+ const exportedName = (aliasParts[aliasParts.length - 1] ?? '').trim();
686
+ if (/^[A-Za-z_$][\w$]*$/.test(exportedName))
687
+ exports.push({
688
+ name: exportedName,
689
+ kind: 'unknown'
690
+ });
691
+ }
692
+ }
693
+ return exports;
694
+ }
695
+ function resolveRelativeCodeImport(input) {
696
+ if (!isRelativeImportSpecifier(input.specifier))
697
+ return undefined;
698
+ const fromDir = node_path_1.default.posix.dirname((0, extractor_contract_1.normalizeContextGraphPath)(input.fromPath));
699
+ const basePath = (0, extractor_contract_1.normalizeContextGraphPath)(node_path_1.default.posix.join(fromDir, input.specifier));
700
+ const candidates = [
701
+ basePath,
702
+ `${basePath}.ts`,
703
+ `${basePath}.js`,
704
+ `${basePath}.json`,
705
+ `${basePath}/index.ts`,
706
+ `${basePath}/index.js`,
707
+ `${basePath}/index.json`
708
+ ];
709
+ for (const candidate of candidates) {
710
+ if (shouldIgnoreCodeIndexPath(candidate) || classifyCodeFile(candidate) === 'unknown')
711
+ continue;
712
+ if (node_fs_1.default.existsSync(node_path_1.default.join(input.projectRoot, candidate)))
713
+ return candidate;
714
+ }
715
+ return undefined;
716
+ }
717
+ function createImportEdges(importReferences) {
718
+ const edges = [];
719
+ const seen = new Set();
720
+ for (const sourceFile of importReferences) {
721
+ const from = createCodeFileNodeId(sourceFile.filePath);
722
+ for (const importReference of sourceFile.imports) {
723
+ if (!importReference.resolvedPath)
724
+ continue;
725
+ const source = (0, extractor_contract_1.createContextGraphSourceRef)({
726
+ path: sourceFile.filePath,
727
+ line: importReference.line,
728
+ hash: sourceFile.hash,
729
+ extractor: 'extractCodeImports'
730
+ });
731
+ const edge = {
732
+ id: createCodeEdgeId({
733
+ type: 'IMPORTS',
734
+ from,
735
+ to: createCodeFileNodeId(importReference.resolvedPath),
736
+ source,
737
+ reason: `File ${sourceFile.filePath} imports ${importReference.resolvedPath}.`
738
+ }),
739
+ from,
740
+ to: createCodeFileNodeId(importReference.resolvedPath),
741
+ type: 'IMPORTS',
742
+ confidence: 'explicit',
743
+ reason: `File ${sourceFile.filePath} imports ${importReference.resolvedPath}.`,
744
+ source
745
+ };
746
+ if (seen.has(edge.id))
747
+ continue;
748
+ seen.add(edge.id);
749
+ edges.push(edge);
750
+ }
751
+ }
752
+ return edges.sort((a, b) => a.id.localeCompare(b.id));
753
+ }
754
+ function createCodeSymbolNodes(fileReferences) {
755
+ const symbols = [];
756
+ const seen = new Set();
757
+ for (const sourceFile of fileReferences) {
758
+ for (const exportReference of sourceFile.exports) {
759
+ const id = createCodeSymbolNodeId(sourceFile.filePath, exportReference.name);
760
+ if (seen.has(id))
761
+ continue;
762
+ seen.add(id);
763
+ symbols.push({
764
+ id,
765
+ name: exportReference.name,
766
+ kind: exportReference.kind,
767
+ path: (0, extractor_contract_1.normalizeContextGraphPath)(sourceFile.filePath),
768
+ exported: true,
769
+ line: exportReference.line
770
+ });
771
+ }
772
+ }
773
+ return symbols.sort((a, b) => a.id.localeCompare(b.id));
774
+ }
775
+ function createSymbolEdges(fileReferences) {
776
+ const edges = [];
777
+ const seen = new Set();
778
+ for (const sourceFile of fileReferences) {
779
+ const from = createCodeFileNodeId(sourceFile.filePath);
780
+ for (const exportReference of sourceFile.exports) {
781
+ const to = createCodeSymbolNodeId(sourceFile.filePath, exportReference.name);
782
+ const source = (0, extractor_contract_1.createContextGraphSourceRef)({
783
+ path: sourceFile.filePath,
784
+ line: exportReference.line,
785
+ hash: sourceFile.hash,
786
+ extractor: 'extractCodeSymbols'
787
+ });
788
+ for (const type of ['DEFINES_SYMBOL', 'EXPORTS']) {
789
+ const reason = type === 'DEFINES_SYMBOL'
790
+ ? `File ${sourceFile.filePath} defines exported symbol ${exportReference.name}.`
791
+ : `File ${sourceFile.filePath} exports symbol ${exportReference.name}.`;
792
+ const edge = {
793
+ id: createCodeEdgeId({ type, from, to, source, reason }),
794
+ from,
795
+ to,
796
+ type,
797
+ confidence: 'explicit',
798
+ reason,
799
+ source
800
+ };
801
+ if (seen.has(edge.id))
802
+ continue;
803
+ seen.add(edge.id);
804
+ edges.push(edge);
805
+ }
806
+ }
807
+ }
808
+ return edges.sort((a, b) => a.id.localeCompare(b.id));
809
+ }
810
+ function createCommandHints(fileReferences) {
811
+ const availablePaths = new Set(fileReferences.map((reference) => reference.filePath));
812
+ const registryLineByCommand = new Map(Object.entries(fileReferences.find((reference) => reference.filePath === COMMAND_REGISTRY_SOURCE_PATH)?.commandIdLines ?? {}));
813
+ const hints = [];
814
+ for (const entry of (0, capability_registry_1.listCommandRegistryEntries)()) {
815
+ const explicitImplementationFiles = normalizeExistingHintPaths(entry.implementationFiles ?? [], availablePaths);
816
+ const heuristicImplementationFiles = explicitImplementationFiles.length > 0
817
+ ? []
818
+ : normalizeExistingHintPaths(inferCommandImplementationFiles(entry), availablePaths);
819
+ const testFiles = normalizeExistingHintPaths(entry.testFiles ?? [], availablePaths);
820
+ if (explicitImplementationFiles.length === 0 && heuristicImplementationFiles.length === 0 && testFiles.length === 0) {
821
+ continue;
822
+ }
823
+ hints.push({
824
+ commandId: entry.id,
825
+ commandFamily: entry.family,
826
+ implementationFiles: explicitImplementationFiles.length > 0 ? explicitImplementationFiles : heuristicImplementationFiles,
827
+ implementationConfidence: explicitImplementationFiles.length > 0 ? 'explicit' : 'heuristic',
828
+ testFiles,
829
+ sourceLine: registryLineByCommand.get(entry.id)
830
+ });
831
+ }
832
+ return hints.sort((a, b) => a.commandId.localeCompare(b.commandId));
833
+ }
834
+ function createCommandFamiliesByPath(hints) {
835
+ const familiesByPath = new Map();
836
+ for (const hint of hints) {
837
+ for (const filePath of [...hint.implementationFiles, ...hint.testFiles]) {
838
+ const families = familiesByPath.get(filePath) ?? new Set();
839
+ families.add(hint.commandFamily);
840
+ familiesByPath.set(filePath, families);
841
+ }
842
+ }
843
+ return new Map([...familiesByPath.entries()].map(([filePath, families]) => [filePath, [...families].sort()]));
844
+ }
845
+ function createCommandHintEdges(fileReferences, hints) {
846
+ const summaryByPath = new Map(fileReferences.map((reference) => [reference.filePath, reference]));
847
+ const edges = [];
848
+ const seen = new Set();
849
+ for (const hint of hints) {
850
+ const commandNodeId = (0, extractor_contract_1.createCommandNodeId)(hint.commandId);
851
+ const registrySummary = summaryByPath.get(COMMAND_REGISTRY_SOURCE_PATH);
852
+ const source = (0, extractor_contract_1.createContextGraphSourceRef)({
853
+ path: COMMAND_REGISTRY_SOURCE_PATH,
854
+ ...(hint.sourceLine === undefined ? {} : { line: hint.sourceLine }),
855
+ ...(registrySummary ? { hash: registrySummary.hash } : {}),
856
+ extractor: 'extractCommandHints'
857
+ });
858
+ for (const filePath of hint.implementationFiles) {
859
+ const from = createCodeFileNodeId(filePath);
860
+ const reason = hint.implementationConfidence === 'explicit'
861
+ ? `Command registry explicitly maps command ${hint.commandId} to implementation file ${filePath}.`
862
+ : `Command ${hint.commandId} maps heuristically to CLI handler file ${filePath}.`;
863
+ const edge = {
864
+ id: createCodeEdgeId({ type: 'IMPLEMENTS_COMMAND', from, to: commandNodeId, source, reason }),
865
+ from,
866
+ to: commandNodeId,
867
+ type: 'IMPLEMENTS_COMMAND',
868
+ confidence: hint.implementationConfidence,
869
+ reason,
870
+ source
871
+ };
872
+ if (!seen.has(edge.id)) {
873
+ seen.add(edge.id);
874
+ edges.push(edge);
875
+ }
876
+ }
877
+ for (const filePath of hint.testFiles) {
878
+ const from = createCodeFileNodeId(filePath);
879
+ const reason = `Command registry explicitly maps command ${hint.commandId} to test file ${filePath}.`;
880
+ const edge = {
881
+ id: createCodeEdgeId({ type: 'TESTS_FILE', from, to: commandNodeId, source, reason }),
882
+ from,
883
+ to: commandNodeId,
884
+ type: 'TESTS_FILE',
885
+ confidence: 'explicit',
886
+ reason,
887
+ source
888
+ };
889
+ if (!seen.has(edge.id)) {
890
+ seen.add(edge.id);
891
+ edges.push(edge);
892
+ }
893
+ }
894
+ }
895
+ return edges.sort((a, b) => a.id.localeCompare(b.id));
896
+ }
897
+ function createTestRelationEdges(projectRoot, fileReferences) {
898
+ const edges = [];
899
+ const seen = new Set();
900
+ const sourceFiles = fileReferences.filter((reference) => classifyCodeFile(reference.filePath) === 'source');
901
+ const testFiles = fileReferences.filter((reference) => classifyCodeFile(reference.filePath) === 'test');
902
+ const sourceFilesByStem = createSourceFilesByStem(sourceFiles);
903
+ const testPaths = new Set(testFiles.map((reference) => reference.filePath));
904
+ for (const testFile of testFiles) {
905
+ const testNodeId = createCodeFileNodeId(testFile.filePath);
906
+ for (const importReference of testFile.imports) {
907
+ if (!importReference.resolvedPath || classifyCodeFile(importReference.resolvedPath) !== 'source')
908
+ continue;
909
+ const source = (0, extractor_contract_1.createContextGraphSourceRef)({
910
+ path: testFile.filePath,
911
+ line: importReference.line,
912
+ hash: testFile.hash,
913
+ extractor: 'extractTestRelations'
914
+ });
915
+ pushUniqueCodeEdge(edges, seen, {
916
+ type: 'TESTS_FILE',
917
+ from: testNodeId,
918
+ to: createCodeFileNodeId(importReference.resolvedPath),
919
+ confidence: 'explicit',
920
+ reason: `Test file ${testFile.filePath} imports source file ${importReference.resolvedPath}.`,
921
+ source
922
+ });
923
+ }
924
+ for (const sourceFile of sourceFilesByStem.get(testFileStem(testFile.filePath)) ?? []) {
925
+ const source = (0, extractor_contract_1.createContextGraphSourceRef)({
926
+ path: testFile.filePath,
927
+ line: 1,
928
+ hash: testFile.hash,
929
+ extractor: 'extractTestRelations'
930
+ });
931
+ pushUniqueCodeEdge(edges, seen, {
932
+ type: 'TESTS_FILE',
933
+ from: testNodeId,
934
+ to: createCodeFileNodeId(sourceFile.filePath),
935
+ confidence: 'derived',
936
+ reason: `Test file ${testFile.filePath} matches source file ${sourceFile.filePath} by filename stem.`,
937
+ source
938
+ });
939
+ }
940
+ for (const mention of testFile.commandMentions) {
941
+ const source = (0, extractor_contract_1.createContextGraphSourceRef)({
942
+ path: testFile.filePath,
943
+ line: mention.line,
944
+ hash: testFile.hash,
945
+ extractor: 'extractTestRelations'
946
+ });
947
+ pushUniqueCodeEdge(edges, seen, {
948
+ type: 'TESTS_FILE',
949
+ from: testNodeId,
950
+ to: (0, extractor_contract_1.createCommandNodeId)(mention.commandId),
951
+ confidence: 'heuristic',
952
+ reason: `Test file ${testFile.filePath} mentions command id ${mention.commandId}.`,
953
+ source
954
+ });
955
+ }
956
+ }
957
+ for (const evidenceReference of readEvidenceTestReferences(projectRoot, testPaths)) {
958
+ const source = (0, extractor_contract_1.createContextGraphSourceRef)({
959
+ path: evidenceReference.evidencePath,
960
+ line: evidenceReference.line,
961
+ content: evidenceReference.content,
962
+ extractor: 'extractEvidenceTestReferences'
963
+ });
964
+ pushUniqueCodeEdge(edges, seen, {
965
+ type: 'VALIDATED_BY_EVIDENCE',
966
+ from: createCodeFileNodeId(evidenceReference.testPath),
967
+ to: evidenceReference.evidenceId,
968
+ confidence: 'explicit',
969
+ reason: `Evidence record ${evidenceReference.evidenceId} references indexed test file ${evidenceReference.testPath}.`,
970
+ source
971
+ });
972
+ }
973
+ return edges.sort((a, b) => a.id.localeCompare(b.id));
974
+ }
975
+ function pushUniqueCodeEdge(edges, seen, input) {
976
+ const edge = {
977
+ id: createCodeEdgeId(input),
978
+ from: input.from,
979
+ to: input.to,
980
+ type: input.type,
981
+ confidence: input.confidence,
982
+ reason: input.reason,
983
+ source: input.source
984
+ };
985
+ if (seen.has(edge.id))
986
+ return;
987
+ seen.add(edge.id);
988
+ edges.push(edge);
989
+ }
990
+ function createSourceFilesByStem(sourceFiles) {
991
+ const filesByStem = new Map();
992
+ for (const sourceFile of sourceFiles) {
993
+ const stem = sourceFileStem(sourceFile.filePath);
994
+ const files = filesByStem.get(stem) ?? [];
995
+ files.push(sourceFile);
996
+ filesByStem.set(stem, files.sort((a, b) => a.filePath.localeCompare(b.filePath)));
997
+ }
998
+ return filesByStem;
999
+ }
1000
+ function sourceFileStem(filePath) {
1001
+ return node_path_1.default.posix.basename((0, extractor_contract_1.normalizeContextGraphPath)(filePath)).replace(/\.(?:ts|js|json)$/, '');
1002
+ }
1003
+ function testFileStem(filePath) {
1004
+ return node_path_1.default.posix.basename((0, extractor_contract_1.normalizeContextGraphPath)(filePath)).replace(/\.(?:test|spec)\.(?:ts|js)$/, '');
1005
+ }
1006
+ function findCommandMentionLine(content, commandId) {
1007
+ const pattern = new RegExp(`(^|[^A-Za-z0-9_.-])${escapeRegex(commandId)}([^A-Za-z0-9_.-]|$)`);
1008
+ const lines = content.split(/\r?\n/);
1009
+ const index = lines.findIndex((line) => pattern.test(line));
1010
+ return index >= 0 ? index + 1 : undefined;
1011
+ }
1012
+ function readEvidenceTestReferences(projectRoot, testPaths) {
1013
+ const tasksDir = node_path_1.default.join(projectRoot, 'tasks');
1014
+ if (!node_fs_1.default.existsSync(tasksDir))
1015
+ return [];
1016
+ const references = [];
1017
+ const taskDirs = node_fs_1.default.readdirSync(tasksDir, { withFileTypes: true })
1018
+ .filter((entry) => entry.isDirectory())
1019
+ .map((entry) => entry.name)
1020
+ .sort();
1021
+ for (const taskDir of taskDirs) {
1022
+ const evidencePath = (0, extractor_contract_1.normalizeContextGraphPath)(node_path_1.default.posix.join('tasks', taskDir, 'evidence.jsonl'));
1023
+ const absolutePath = node_path_1.default.join(projectRoot, evidencePath);
1024
+ if (!node_fs_1.default.existsSync(absolutePath))
1025
+ continue;
1026
+ let content;
1027
+ try {
1028
+ content = node_fs_1.default.readFileSync(absolutePath, 'utf8');
1029
+ }
1030
+ catch {
1031
+ continue;
1032
+ }
1033
+ content.split(/\r?\n/).forEach((line, index) => {
1034
+ if (!line.trim())
1035
+ return;
1036
+ const evidenceId = readEvidenceIdFromLine(line) ?? `evidence:${evidencePath}#L${index + 1}`;
1037
+ for (const testPath of testPaths) {
1038
+ if (!line.includes(testPath))
1039
+ continue;
1040
+ references.push({
1041
+ evidencePath,
1042
+ line: index + 1,
1043
+ content,
1044
+ evidenceId,
1045
+ testPath
1046
+ });
1047
+ }
1048
+ });
1049
+ }
1050
+ return references.sort((a, b) => `${a.evidencePath}:${a.line}:${a.testPath}`.localeCompare(`${b.evidencePath}:${b.line}:${b.testPath}`));
1051
+ }
1052
+ function readEvidenceIdFromLine(line) {
1053
+ try {
1054
+ const parsed = JSON.parse(line);
1055
+ return typeof parsed.id === 'string' && parsed.id.length > 0 ? parsed.id : undefined;
1056
+ }
1057
+ catch {
1058
+ return undefined;
1059
+ }
1060
+ }
1061
+ function normalizeExistingHintPaths(paths, availablePaths) {
1062
+ return uniqueSorted(paths.map(extractor_contract_1.normalizeContextGraphPath).filter((filePath) => availablePaths.has(filePath)));
1063
+ }
1064
+ function createRegistryLineMap(content) {
1065
+ const lineByCommand = new Map();
1066
+ const lines = content.split(/\r?\n/);
1067
+ lines.forEach((line, index) => {
1068
+ const match = line.match(/\bid:\s*['"]([^'"]+)['"]/);
1069
+ if (match?.[1] && !lineByCommand.has(match[1]))
1070
+ lineByCommand.set(match[1], index + 1);
1071
+ });
1072
+ return lineByCommand;
1073
+ }
1074
+ function inferCommandImplementationFiles(entry) {
1075
+ const exact = {
1076
+ 'ci.gate': ['src/cli/ci.ts'],
1077
+ 'context.graph': ['src/cli/context.ts'],
1078
+ 'dev.docker-check': ['src/cli/dev.ts'],
1079
+ 'docs.managed.list': ['src/cli/docs.ts'],
1080
+ 'docs.managed.explain': ['src/cli/docs.ts'],
1081
+ 'docs.required-reading': ['src/cli/docs.ts'],
1082
+ 'evidence.add-command': ['src/cli/evidence.ts'],
1083
+ 'evidence.collect': ['src/cli/evidence.ts'],
1084
+ 'install.plan': ['src/cli/install.ts'],
1085
+ 'mcp.serve': ['src/cli/mcp.ts'],
1086
+ 'ops.status': ['src/cli/status.ts'],
1087
+ 'package.smoke': ['src/cli/package-smoke.ts'],
1088
+ 'policy.check-shell': ['src/cli/policy.ts'],
1089
+ 'policy.preflight-shell': ['src/cli/policy.ts'],
1090
+ 'release.artifact': ['src/cli/release-artifact.ts'],
1091
+ 'release.dry-run': ['src/cli/release-dry-run.ts'],
1092
+ 'release.gate': ['src/cli/release-gate.ts'],
1093
+ 'release.publish': ['src/cli/release-publish.ts'],
1094
+ 'run.scaffold': ['src/cli/run-scaffold.ts'],
1095
+ 'run-state.resume': ['src/cli/run-state.ts'],
1096
+ 'run-state.show': ['src/cli/run-state.ts'],
1097
+ 'smoke.clean-checkout': ['src/cli/smoke.ts'],
1098
+ 'smoke.run': ['src/cli/smoke.ts'],
1099
+ 'task.audit-close': ['src/cli/task.ts'],
1100
+ 'task.upgrade-scaffold': ['src/cli/task.ts'],
1101
+ 'write.preflight': ['src/cli/write-preflight.ts']
1102
+ };
1103
+ if (exact[entry.id])
1104
+ return exact[entry.id];
1105
+ const familyPrefix = entry.id.split('.')[0] ?? entry.id;
1106
+ const familyPath = `src/cli/${familyPrefix}.ts`;
1107
+ return [familyPath];
1108
+ }
1109
+ function createCodeEdgeId(input) {
1110
+ const fingerprint = (0, extractor_contract_1.hashContextGraphText)(JSON.stringify({
1111
+ type: input.type,
1112
+ from: input.from,
1113
+ to: input.to,
1114
+ source: {
1115
+ path: input.source.path,
1116
+ line: input.source.line ?? null,
1117
+ extractor: input.source.extractor
1118
+ },
1119
+ reason: input.reason
1120
+ })).replace(/^sha256:/, '');
1121
+ return `code-edge:${input.type}:${fingerprint}`;
1122
+ }
1123
+ function escapeRegex(value) {
1124
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1125
+ }
1126
+ function isRelativeImportSpecifier(specifier) {
1127
+ return specifier.startsWith('./') || specifier.startsWith('../');
1128
+ }
1129
+ function dedupeImports(imports) {
1130
+ const seen = new Set();
1131
+ const deduped = [];
1132
+ for (const importReference of imports) {
1133
+ const key = `${importReference.specifier}\0${importReference.resolvedPath ?? ''}\0${importReference.line}`;
1134
+ if (seen.has(key))
1135
+ continue;
1136
+ seen.add(key);
1137
+ deduped.push(importReference);
1138
+ }
1139
+ return deduped;
1140
+ }
1141
+ function dedupeExports(exports) {
1142
+ const seen = new Set();
1143
+ const deduped = [];
1144
+ for (const exportReference of exports) {
1145
+ if (seen.has(exportReference.name))
1146
+ continue;
1147
+ seen.add(exportReference.name);
1148
+ deduped.push(exportReference);
1149
+ }
1150
+ return deduped;
1151
+ }
1152
+ function uniqueSorted(values) {
1153
+ return Array.from(new Set(values)).sort();
1154
+ }