mustflow 2.75.1 → 2.84.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.
- package/README.md +31 -3
- package/dist/cli/commands/docs.js +86 -2
- package/dist/cli/commands/script-pack.js +5 -0
- package/dist/cli/i18n/en.js +101 -2
- package/dist/cli/i18n/es.js +101 -2
- package/dist/cli/i18n/fr.js +101 -2
- package/dist/cli/i18n/hi.js +101 -2
- package/dist/cli/i18n/ko.js +101 -2
- package/dist/cli/i18n/zh.js +101 -2
- package/dist/cli/lib/script-pack-registry.js +162 -7
- package/dist/cli/script-packs/code-export-diff.js +160 -0
- package/dist/cli/script-packs/code-outline.js +33 -5
- package/dist/cli/script-packs/code-route-outline.js +155 -0
- package/dist/cli/script-packs/docs-reference-drift.js +150 -0
- package/dist/cli/script-packs/repo-config-chain.js +163 -0
- package/dist/cli/script-packs/repo-related-files.js +161 -0
- package/dist/core/code-outline.js +527 -80
- package/dist/core/config-chain.js +595 -0
- package/dist/core/export-diff.js +359 -0
- package/dist/core/public-json-contracts.js +75 -0
- package/dist/core/reference-drift.js +388 -0
- package/dist/core/related-files.js +493 -0
- package/dist/core/route-outline.js +912 -0
- package/dist/core/script-pack-suggestions.js +111 -5
- package/dist/core/source-anchors.js +13 -1
- package/package.json +1 -1
- package/schemas/README.md +28 -5
- package/schemas/code-outline-report.schema.json +47 -1
- package/schemas/code-symbol-read-report.schema.json +64 -4
- package/schemas/config-chain-report.schema.json +187 -0
- package/schemas/export-diff-report.schema.json +220 -0
- package/schemas/reference-drift-report.schema.json +166 -0
- package/schemas/related-files-report.schema.json +145 -0
- package/schemas/route-outline-report.schema.json +200 -0
- package/templates/default/common/.mustflow/config/commands.toml +21 -0
- package/templates/default/i18n.toml +7 -1
- package/templates/default/locales/en/.mustflow/docs/agent-workflow.md +1 -1
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +2 -1
- package/templates/default/locales/en/.mustflow/skills/cross-agent-session-reference/SKILL.md +131 -0
- package/templates/default/locales/en/.mustflow/skills/routes.toml +6 -0
- package/templates/default/manifest.toml +8 -1
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { existsSync, lstatSync, readdirSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
5
|
+
import { listSourceAnchorFiles, parseSourceAnchorsInContent, sourceAnchorTextContainsSecretLike, splitSourceAnchorList, } from './source-anchors.js';
|
|
5
6
|
export const CODE_PACK_ID = 'code';
|
|
6
7
|
export const CODE_OUTLINE_SCRIPT_ID = 'outline';
|
|
7
8
|
export const CODE_OUTLINE_SCRIPT_REF = `${CODE_PACK_ID}/${CODE_OUTLINE_SCRIPT_ID}`;
|
|
@@ -12,7 +13,12 @@ const DEFAULT_MAX_FILES = 200;
|
|
|
12
13
|
const DEFAULT_CONTEXT_LINES = 0;
|
|
13
14
|
const DEFAULT_MAX_SNIPPET_LINES = 250;
|
|
14
15
|
const RETURN_PREVIEW_MAX_CHARS = 120;
|
|
15
|
-
const
|
|
16
|
+
const TYPESCRIPT_JAVASCRIPT_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
|
|
17
|
+
const ASTRO_EXTENSIONS = ['.astro'];
|
|
18
|
+
const SVELTE_EXTENSIONS = ['.svelte'];
|
|
19
|
+
const GO_EXTENSIONS = ['.go'];
|
|
20
|
+
const RUST_EXTENSIONS = ['.rs'];
|
|
21
|
+
const PYTHON_EXTENSIONS = ['.py'];
|
|
16
22
|
const IGNORED_DIRECTORIES = [
|
|
17
23
|
'.git',
|
|
18
24
|
'.mustflow/cache',
|
|
@@ -35,6 +41,9 @@ const ERROR_SYMBOL_READ_CODES = new Set([
|
|
|
35
41
|
'code_symbol_read_unreadable_path',
|
|
36
42
|
'code_symbol_read_invalid_range',
|
|
37
43
|
'code_symbol_read_no_symbol_at_line',
|
|
44
|
+
'code_symbol_read_anchor_not_found',
|
|
45
|
+
'code_symbol_read_anchor_ambiguous',
|
|
46
|
+
'code_symbol_read_anchor_without_symbol',
|
|
38
47
|
'code_symbol_read_snippet_too_large',
|
|
39
48
|
]);
|
|
40
49
|
function toPosixPath(value) {
|
|
@@ -46,7 +55,7 @@ function normalizeRelativePath(value) {
|
|
|
46
55
|
function sha256Tagged(buffer) {
|
|
47
56
|
return `sha256:${createHash('sha256').update(buffer).digest('hex')}`;
|
|
48
57
|
}
|
|
49
|
-
function
|
|
58
|
+
function typescriptJavascriptLanguageForPath(filePath) {
|
|
50
59
|
switch (path.extname(filePath).toLowerCase()) {
|
|
51
60
|
case '.ts':
|
|
52
61
|
case '.mts':
|
|
@@ -66,6 +75,27 @@ function languageForPath(filePath) {
|
|
|
66
75
|
return null;
|
|
67
76
|
}
|
|
68
77
|
}
|
|
78
|
+
function astroLanguageForPath(filePath) {
|
|
79
|
+
return path.extname(filePath).toLowerCase() === '.astro' ? 'astro' : null;
|
|
80
|
+
}
|
|
81
|
+
function svelteLanguageForPath(filePath) {
|
|
82
|
+
return path.extname(filePath).toLowerCase() === '.svelte' ? 'svelte' : null;
|
|
83
|
+
}
|
|
84
|
+
function goLanguageForPath(filePath) {
|
|
85
|
+
return path.extname(filePath).toLowerCase() === '.go' ? 'go' : null;
|
|
86
|
+
}
|
|
87
|
+
function rustLanguageForPath(filePath) {
|
|
88
|
+
return path.extname(filePath).toLowerCase() === '.rs' ? 'rust' : null;
|
|
89
|
+
}
|
|
90
|
+
function pythonLanguageForPath(filePath) {
|
|
91
|
+
return path.extname(filePath).toLowerCase() === '.py' ? 'python' : null;
|
|
92
|
+
}
|
|
93
|
+
function languageAdapterForPath(filePath) {
|
|
94
|
+
return CODE_OUTLINE_LANGUAGE_ADAPTERS.find((adapter) => adapter.languageForPath(filePath) !== null) ?? null;
|
|
95
|
+
}
|
|
96
|
+
export function languageForPath(filePath) {
|
|
97
|
+
return languageAdapterForPath(filePath)?.languageForPath(filePath) ?? null;
|
|
98
|
+
}
|
|
69
99
|
function isIgnoredDirectory(relativePath) {
|
|
70
100
|
const normalized = normalizeRelativePath(relativePath);
|
|
71
101
|
return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
|
|
@@ -205,6 +235,7 @@ function declarationFromLine(line) {
|
|
|
205
235
|
name: functionMatch.groups.name ?? '<anonymous>',
|
|
206
236
|
exported: Boolean(functionMatch.groups.exported),
|
|
207
237
|
async: Boolean(functionMatch.groups.async),
|
|
238
|
+
returnType: null,
|
|
208
239
|
};
|
|
209
240
|
}
|
|
210
241
|
const shapeMatch = /^(?<exported>export\s+)?(?<kind>class|interface|type|enum)\s+(?<name>[$A-Z_a-z][$\w]*)/u.exec(trimmed);
|
|
@@ -214,6 +245,7 @@ function declarationFromLine(line) {
|
|
|
214
245
|
name: shapeMatch.groups.name ?? '<anonymous>',
|
|
215
246
|
exported: Boolean(shapeMatch.groups.exported),
|
|
216
247
|
async: false,
|
|
248
|
+
returnType: null,
|
|
217
249
|
};
|
|
218
250
|
}
|
|
219
251
|
const variableMatch = /^(?<exported>export\s+)?(?:const|let|var)\s+(?<name>[$A-Z_a-z][$\w]*)\s*[:=]/u.exec(trimmed);
|
|
@@ -223,6 +255,7 @@ function declarationFromLine(line) {
|
|
|
223
255
|
name: variableMatch.groups.name ?? '<anonymous>',
|
|
224
256
|
exported: Boolean(variableMatch.groups.exported),
|
|
225
257
|
async: /async\s*(?:\(|[A-Z_a-z_$])/u.test(trimmed),
|
|
258
|
+
returnType: null,
|
|
226
259
|
};
|
|
227
260
|
}
|
|
228
261
|
return null;
|
|
@@ -255,7 +288,7 @@ function buildSignature(lines, startLine, endLine) {
|
|
|
255
288
|
if (trimmed.length > 0) {
|
|
256
289
|
parts.push(trimmed);
|
|
257
290
|
}
|
|
258
|
-
if (/[{;]\s*$/u.test(trimmed) || /=>/u.test(trimmed)) {
|
|
291
|
+
if (/[{;]\s*$/u.test(trimmed) || /=>/u.test(trimmed) || /^(?:async\s+)?(?:def|class)\b.*:\s*$/u.test(trimmed)) {
|
|
259
292
|
break;
|
|
260
293
|
}
|
|
261
294
|
}
|
|
@@ -269,6 +302,9 @@ function extractExplicitReturnType(signature, match) {
|
|
|
269
302
|
if (match.kind !== 'function') {
|
|
270
303
|
return null;
|
|
271
304
|
}
|
|
305
|
+
if (match.returnType !== null) {
|
|
306
|
+
return match.returnType;
|
|
307
|
+
}
|
|
272
308
|
const functionReturnMatch = /\bfunction(?:\s+[$A-Z_a-z][$\w]*)?\s*\([^)]*\)\s*:\s*(?<returnType>[^={;]+?)(?=\s*(?:\{|$))/u.exec(signature);
|
|
273
309
|
if (functionReturnMatch?.groups?.returnType) {
|
|
274
310
|
return normalizeReturnType(functionReturnMatch.groups.returnType);
|
|
@@ -303,7 +339,7 @@ function extractArrowExpressionReturnPreview(signature) {
|
|
|
303
339
|
function isIdentifierCharacter(value) {
|
|
304
340
|
return typeof value === 'string' && /[$\w]/u.test(value);
|
|
305
341
|
}
|
|
306
|
-
function findReturnKeywordIndex(line) {
|
|
342
|
+
function findReturnKeywordIndex(line, lineCommentStart) {
|
|
307
343
|
let quote = null;
|
|
308
344
|
let escaped = false;
|
|
309
345
|
for (let index = 0; index < line.length; index += 1) {
|
|
@@ -323,7 +359,8 @@ function findReturnKeywordIndex(line) {
|
|
|
323
359
|
}
|
|
324
360
|
continue;
|
|
325
361
|
}
|
|
326
|
-
if (character === '/' && nextCharacter === '/')
|
|
362
|
+
if ((lineCommentStart === '//' && character === '/' && nextCharacter === '/') ||
|
|
363
|
+
(lineCommentStart === '#' && character === '#')) {
|
|
327
364
|
return -1;
|
|
328
365
|
}
|
|
329
366
|
if (character === '"' || character === "'" || character === '`') {
|
|
@@ -338,21 +375,21 @@ function findReturnKeywordIndex(line) {
|
|
|
338
375
|
}
|
|
339
376
|
return -1;
|
|
340
377
|
}
|
|
341
|
-
function extractReturnExpressionPreview(line) {
|
|
342
|
-
const returnIndex = findReturnKeywordIndex(line);
|
|
378
|
+
function extractReturnExpressionPreview(line, lineCommentStart) {
|
|
379
|
+
const returnIndex = findReturnKeywordIndex(line, lineCommentStart);
|
|
343
380
|
if (returnIndex < 0) {
|
|
344
381
|
return null;
|
|
345
382
|
}
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
383
|
+
const rawExpression = line.slice(returnIndex + 'return'.length);
|
|
384
|
+
const commentIndex = rawExpression.indexOf(lineCommentStart);
|
|
385
|
+
const expression = (commentIndex >= 0 ? rawExpression.slice(0, commentIndex) : rawExpression)
|
|
349
386
|
.trim()
|
|
350
387
|
.replace(/;\s*$/u, '')
|
|
351
388
|
.trim();
|
|
352
389
|
return expression.length === 0 ? null : truncateReturnPreview(expression);
|
|
353
390
|
}
|
|
354
|
-
function hasReturnStatement(line) {
|
|
355
|
-
return findReturnKeywordIndex(line) >= 0;
|
|
391
|
+
function hasReturnStatement(line, lineCommentStart) {
|
|
392
|
+
return findReturnKeywordIndex(line, lineCommentStart) >= 0;
|
|
356
393
|
}
|
|
357
394
|
function simpleBodyThrowsOnly(lines, startLine, endLine, returnType) {
|
|
358
395
|
let sawThrow = false;
|
|
@@ -372,7 +409,26 @@ function simpleBodyThrowsOnly(lines, startLine, endLine, returnType) {
|
|
|
372
409
|
}
|
|
373
410
|
return sawThrow && (!sawOtherExecutableLine || returnType === 'never');
|
|
374
411
|
}
|
|
375
|
-
function
|
|
412
|
+
function stripLineComment(line, lineCommentStart) {
|
|
413
|
+
const commentIndex = line.indexOf(lineCommentStart);
|
|
414
|
+
return commentIndex >= 0 ? line.slice(0, commentIndex) : line;
|
|
415
|
+
}
|
|
416
|
+
function extractTailExpressionReturnPreview(lines, startLine, endLine) {
|
|
417
|
+
let index = endLine - 2;
|
|
418
|
+
while (index >= startLine) {
|
|
419
|
+
const trimmed = stripLineComment(lines[index] ?? '', '//').trim();
|
|
420
|
+
if (trimmed.length === 0 || trimmed === '{' || trimmed === '}') {
|
|
421
|
+
index -= 1;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (trimmed.endsWith(';') || /^(?:let|return|if|for|while|loop|match)\b/u.test(trimmed)) {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
return truncateReturnPreview(trimmed.replace(/,\s*$/u, ''));
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
function extractReturnMetadata(lines, startLine, endLine, signature, match, lineCommentStart = '//', tailExpressionReturn = false) {
|
|
376
432
|
const returnType = extractExplicitReturnType(signature, match);
|
|
377
433
|
if (match.kind !== 'function') {
|
|
378
434
|
return {
|
|
@@ -388,11 +444,11 @@ function extractReturnMetadata(lines, startLine, endLine, signature, match) {
|
|
|
388
444
|
let voidReturnCount = 0;
|
|
389
445
|
for (let index = startLine - 1; index < endLine; index += 1) {
|
|
390
446
|
const line = lines[index] ?? '';
|
|
391
|
-
if (!hasReturnStatement(line)) {
|
|
447
|
+
if (!hasReturnStatement(line, lineCommentStart)) {
|
|
392
448
|
continue;
|
|
393
449
|
}
|
|
394
450
|
returnLines.push(index + 1);
|
|
395
|
-
const preview = extractReturnExpressionPreview(line);
|
|
451
|
+
const preview = extractReturnExpressionPreview(line, lineCommentStart);
|
|
396
452
|
if (preview === null) {
|
|
397
453
|
voidReturnCount += 1;
|
|
398
454
|
}
|
|
@@ -401,11 +457,12 @@ function extractReturnMetadata(lines, startLine, endLine, signature, match) {
|
|
|
401
457
|
}
|
|
402
458
|
}
|
|
403
459
|
const arrowExpressionPreview = returnLines.length === 0 ? extractArrowExpressionReturnPreview(signature) : null;
|
|
460
|
+
const tailExpressionPreview = tailExpressionReturn && returnLines.length === 0 && returnType !== null ? extractTailExpressionReturnPreview(lines, startLine, endLine) : null;
|
|
404
461
|
let returnBehavior;
|
|
405
462
|
if (valueReturnPreviews.length > 0 && voidReturnCount > 0) {
|
|
406
463
|
returnBehavior = 'mixed';
|
|
407
464
|
}
|
|
408
|
-
else if (valueReturnPreviews.length > 0 || arrowExpressionPreview !== null) {
|
|
465
|
+
else if (valueReturnPreviews.length > 0 || arrowExpressionPreview !== null || tailExpressionPreview !== null) {
|
|
409
466
|
returnBehavior = 'value';
|
|
410
467
|
}
|
|
411
468
|
else if (voidReturnCount > 0) {
|
|
@@ -422,21 +479,141 @@ function extractReturnMetadata(lines, startLine, endLine, signature, match) {
|
|
|
422
479
|
return_behavior: returnBehavior,
|
|
423
480
|
return_count: returnLines.length,
|
|
424
481
|
return_lines: returnLines,
|
|
425
|
-
return_preview: valueReturnPreviews[0] ?? arrowExpressionPreview,
|
|
482
|
+
return_preview: valueReturnPreviews[0] ?? arrowExpressionPreview ?? tailExpressionPreview,
|
|
426
483
|
};
|
|
427
484
|
}
|
|
428
|
-
function
|
|
485
|
+
function startsWithUppercase(value) {
|
|
486
|
+
const [first] = value;
|
|
487
|
+
return typeof first === 'string' && first.toLocaleUpperCase() === first && first.toLocaleLowerCase() !== first;
|
|
488
|
+
}
|
|
489
|
+
function goDeclarationFromLine(line) {
|
|
490
|
+
const trimmed = line.trim();
|
|
491
|
+
const functionMatch = /^func\s+(?:\([^)]*\)\s*)?(?<name>[A-Z_a-z]\w*)\s*\([^)]*\)\s*(?<returnType>[^{]+?)?\s*(?:\{|$)/u.exec(trimmed);
|
|
492
|
+
if (functionMatch?.groups) {
|
|
493
|
+
const returnType = functionMatch.groups.returnType?.trim() ?? '';
|
|
494
|
+
return {
|
|
495
|
+
kind: 'function',
|
|
496
|
+
name: functionMatch.groups.name ?? '<anonymous>',
|
|
497
|
+
exported: startsWithUppercase(functionMatch.groups.name ?? ''),
|
|
498
|
+
async: false,
|
|
499
|
+
returnType: returnType.length > 0 ? normalizeReturnType(returnType) : null,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const typeMatch = /^type\s+(?<name>[A-Z_a-z]\w*)\s+(?<shape>struct|interface)?\b/u.exec(trimmed);
|
|
503
|
+
if (typeMatch?.groups) {
|
|
504
|
+
return {
|
|
505
|
+
kind: typeMatch.groups.shape === 'interface' ? 'interface' : 'type',
|
|
506
|
+
name: typeMatch.groups.name ?? '<anonymous>',
|
|
507
|
+
exported: startsWithUppercase(typeMatch.groups.name ?? ''),
|
|
508
|
+
async: false,
|
|
509
|
+
returnType: null,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
function rustDeclarationFromLine(line) {
|
|
515
|
+
const trimmed = line.trim();
|
|
516
|
+
const visibility = String.raw `(?:pub(?:\([^)]*\))?\s+)?`;
|
|
517
|
+
const functionPattern = [
|
|
518
|
+
String.raw `^${visibility}`,
|
|
519
|
+
String.raw `(?:(?:async|const|unsafe)\s+)*`,
|
|
520
|
+
String.raw `(?:extern\s+"[^"]+"\s+)?`,
|
|
521
|
+
String.raw `fn\s+(?<name>[A-Z_a-z]\w*)\s*[^{;]*?`,
|
|
522
|
+
String.raw `(?:->\s*(?<returnType>[^{]+?))?\s*(?:where\b|\{|$)`,
|
|
523
|
+
].join('');
|
|
524
|
+
const functionMatch = new RegExp(functionPattern, 'u').exec(trimmed);
|
|
525
|
+
if (functionMatch?.groups) {
|
|
526
|
+
const returnType = functionMatch.groups.returnType?.replace(/\bwhere\b.*$/u, '').trim() ?? '';
|
|
527
|
+
return {
|
|
528
|
+
kind: 'function',
|
|
529
|
+
name: functionMatch.groups.name ?? '<anonymous>',
|
|
530
|
+
exported: /^pub\b/u.test(trimmed),
|
|
531
|
+
async: /\basync\s+fn\b/u.test(trimmed) || /\basync\s+(?:const\s+|unsafe\s+)*fn\b/u.test(trimmed),
|
|
532
|
+
returnType: returnType.length > 0 ? normalizeReturnType(returnType) : null,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
const shapeMatch = new RegExp(String.raw `^${visibility}(?<shape>struct|trait|type|enum)\s+(?<name>[A-Z_a-z]\w*)\b`, 'u').exec(trimmed);
|
|
536
|
+
if (shapeMatch?.groups) {
|
|
537
|
+
const shape = shapeMatch.groups.shape;
|
|
538
|
+
return {
|
|
539
|
+
kind: shape === 'trait' ? 'interface' : shape === 'enum' ? 'enum' : 'type',
|
|
540
|
+
name: shapeMatch.groups.name ?? '<anonymous>',
|
|
541
|
+
exported: /^pub\b/u.test(trimmed),
|
|
542
|
+
async: false,
|
|
543
|
+
returnType: null,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
function pythonDeclarationFromLine(line) {
|
|
549
|
+
const trimmed = line.trim();
|
|
550
|
+
const functionMatch = /^(?<async>async\s+)?def\s+(?<name>[A-Z_a-z]\w*)\s*\([^)]*\)\s*(?:->\s*(?<returnType>[^:]+))?:/u.exec(trimmed);
|
|
551
|
+
if (functionMatch?.groups) {
|
|
552
|
+
const returnType = functionMatch.groups.returnType?.trim() ?? '';
|
|
553
|
+
return {
|
|
554
|
+
kind: 'function',
|
|
555
|
+
name: functionMatch.groups.name ?? '<anonymous>',
|
|
556
|
+
exported: !functionMatch.groups.name?.startsWith('_'),
|
|
557
|
+
async: Boolean(functionMatch.groups.async),
|
|
558
|
+
returnType: returnType.length > 0 ? normalizeReturnType(returnType) : null,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
const classMatch = /^class\s+(?<name>[A-Z_a-z]\w*)\b/u.exec(trimmed);
|
|
562
|
+
if (classMatch?.groups) {
|
|
563
|
+
return {
|
|
564
|
+
kind: 'class',
|
|
565
|
+
name: classMatch.groups.name ?? '<anonymous>',
|
|
566
|
+
exported: !classMatch.groups.name?.startsWith('_'),
|
|
567
|
+
async: false,
|
|
568
|
+
returnType: null,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
function leadingWhitespaceWidth(line) {
|
|
574
|
+
let width = 0;
|
|
575
|
+
for (const character of line) {
|
|
576
|
+
if (character === ' ') {
|
|
577
|
+
width += 1;
|
|
578
|
+
}
|
|
579
|
+
else if (character === '\t') {
|
|
580
|
+
width += 4;
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return width;
|
|
587
|
+
}
|
|
588
|
+
function findPythonDeclarationEndLine(lines, startIndex) {
|
|
589
|
+
const startIndent = leadingWhitespaceWidth(lines[startIndex] ?? '');
|
|
590
|
+
let index = startIndex + 1;
|
|
591
|
+
while (index < lines.length) {
|
|
592
|
+
const line = lines[index] ?? '';
|
|
593
|
+
const trimmed = line.trim();
|
|
594
|
+
if (trimmed.length === 0 || trimmed.startsWith('#')) {
|
|
595
|
+
index += 1;
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (leadingWhitespaceWidth(line) <= startIndent) {
|
|
599
|
+
return index;
|
|
600
|
+
}
|
|
601
|
+
index += 1;
|
|
602
|
+
}
|
|
603
|
+
return lines.length;
|
|
604
|
+
}
|
|
605
|
+
function extractSymbolsFromDeclarations(relativePath, language, contentSha256, text, declarationForLine, endLineForMatch, lineCommentStart, tailExpressionReturn = false) {
|
|
429
606
|
const lines = text.split(/\r\n|\r|\n/u);
|
|
430
607
|
const symbols = [];
|
|
431
608
|
for (const [index, line] of lines.entries()) {
|
|
432
|
-
const match =
|
|
609
|
+
const match = declarationForLine(line);
|
|
433
610
|
if (!match) {
|
|
434
611
|
continue;
|
|
435
612
|
}
|
|
436
613
|
const startLine = index + 1;
|
|
437
|
-
const endLine =
|
|
614
|
+
const endLine = endLineForMatch(lines, index);
|
|
438
615
|
const signature = buildSignature(lines, startLine, endLine);
|
|
439
|
-
const returnMetadata = extractReturnMetadata(lines, startLine, endLine, signature, match);
|
|
616
|
+
const returnMetadata = extractReturnMetadata(lines, startLine, endLine, signature, match, lineCommentStart, tailExpressionReturn);
|
|
440
617
|
symbols.push({
|
|
441
618
|
id: `${relativePath}:${startLine}:${match.kind}:${match.name}`,
|
|
442
619
|
path: relativePath,
|
|
@@ -455,7 +632,172 @@ function extractSymbols(relativePath, language, contentSha256, text) {
|
|
|
455
632
|
}
|
|
456
633
|
return symbols;
|
|
457
634
|
}
|
|
458
|
-
function
|
|
635
|
+
function extractTypescriptJavascriptSymbols(relativePath, language, contentSha256, text) {
|
|
636
|
+
return extractSymbolsFromDeclarations(relativePath, language, contentSha256, text, declarationFromLine, findDeclarationEndLine, '//');
|
|
637
|
+
}
|
|
638
|
+
function maskAstroFrontmatter(text) {
|
|
639
|
+
const lines = text.split(/\r\n|\r|\n/u);
|
|
640
|
+
if ((lines[0] ?? '').trim() !== '---') {
|
|
641
|
+
return lines.map(() => '').join('\n');
|
|
642
|
+
}
|
|
643
|
+
const masked = lines.map(() => '');
|
|
644
|
+
let index = 1;
|
|
645
|
+
while (index < lines.length) {
|
|
646
|
+
const line = lines[index] ?? '';
|
|
647
|
+
if (line.trim() === '---') {
|
|
648
|
+
return masked.join('\n');
|
|
649
|
+
}
|
|
650
|
+
masked[index] = line;
|
|
651
|
+
index += 1;
|
|
652
|
+
}
|
|
653
|
+
return lines.map(() => '').join('\n');
|
|
654
|
+
}
|
|
655
|
+
function maskSvelteScriptBlocks(text) {
|
|
656
|
+
const lines = text.split(/\r\n|\r|\n/u);
|
|
657
|
+
const masked = lines.map(() => '');
|
|
658
|
+
let insideScript = false;
|
|
659
|
+
for (const [index, line] of lines.entries()) {
|
|
660
|
+
let remaining = line;
|
|
661
|
+
if (!insideScript) {
|
|
662
|
+
const openMatch = /<script\b[^>]*>/iu.exec(remaining);
|
|
663
|
+
if (!openMatch || openMatch.index === undefined) {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
remaining = remaining.slice(openMatch.index + openMatch[0].length);
|
|
667
|
+
insideScript = true;
|
|
668
|
+
}
|
|
669
|
+
const closeIndex = remaining.search(/<\/script\s*>/iu);
|
|
670
|
+
if (closeIndex >= 0) {
|
|
671
|
+
masked[index] = remaining.slice(0, closeIndex);
|
|
672
|
+
insideScript = false;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
masked[index] = remaining;
|
|
676
|
+
}
|
|
677
|
+
return masked.join('\n');
|
|
678
|
+
}
|
|
679
|
+
function extractAstroSymbols(relativePath, language, contentSha256, text) {
|
|
680
|
+
return extractTypescriptJavascriptSymbols(relativePath, language, contentSha256, maskAstroFrontmatter(text));
|
|
681
|
+
}
|
|
682
|
+
function extractSvelteSymbols(relativePath, language, contentSha256, text) {
|
|
683
|
+
return extractTypescriptJavascriptSymbols(relativePath, language, contentSha256, maskSvelteScriptBlocks(text));
|
|
684
|
+
}
|
|
685
|
+
function extractGoSymbols(relativePath, language, contentSha256, text) {
|
|
686
|
+
return extractSymbolsFromDeclarations(relativePath, language, contentSha256, text, goDeclarationFromLine, findDeclarationEndLine, '//');
|
|
687
|
+
}
|
|
688
|
+
function extractRustSymbols(relativePath, language, contentSha256, text) {
|
|
689
|
+
return extractSymbolsFromDeclarations(relativePath, language, contentSha256, text, rustDeclarationFromLine, findDeclarationEndLine, '//', true);
|
|
690
|
+
}
|
|
691
|
+
function extractPythonSymbols(relativePath, language, contentSha256, text) {
|
|
692
|
+
return extractSymbolsFromDeclarations(relativePath, language, contentSha256, text, pythonDeclarationFromLine, findPythonDeclarationEndLine, '#');
|
|
693
|
+
}
|
|
694
|
+
const TYPESCRIPT_JAVASCRIPT_LANGUAGE_ADAPTER = {
|
|
695
|
+
id: 'typescript-javascript',
|
|
696
|
+
extensions: TYPESCRIPT_JAVASCRIPT_EXTENSIONS,
|
|
697
|
+
languageForPath: typescriptJavascriptLanguageForPath,
|
|
698
|
+
extractSymbols: extractTypescriptJavascriptSymbols,
|
|
699
|
+
};
|
|
700
|
+
const ASTRO_LANGUAGE_ADAPTER = {
|
|
701
|
+
id: 'astro',
|
|
702
|
+
extensions: ASTRO_EXTENSIONS,
|
|
703
|
+
languageForPath: astroLanguageForPath,
|
|
704
|
+
extractSymbols: extractAstroSymbols,
|
|
705
|
+
};
|
|
706
|
+
const SVELTE_LANGUAGE_ADAPTER = {
|
|
707
|
+
id: 'svelte',
|
|
708
|
+
extensions: SVELTE_EXTENSIONS,
|
|
709
|
+
languageForPath: svelteLanguageForPath,
|
|
710
|
+
extractSymbols: extractSvelteSymbols,
|
|
711
|
+
};
|
|
712
|
+
const GO_LANGUAGE_ADAPTER = {
|
|
713
|
+
id: 'go',
|
|
714
|
+
extensions: GO_EXTENSIONS,
|
|
715
|
+
languageForPath: goLanguageForPath,
|
|
716
|
+
extractSymbols: extractGoSymbols,
|
|
717
|
+
};
|
|
718
|
+
const RUST_LANGUAGE_ADAPTER = {
|
|
719
|
+
id: 'rust',
|
|
720
|
+
extensions: RUST_EXTENSIONS,
|
|
721
|
+
languageForPath: rustLanguageForPath,
|
|
722
|
+
extractSymbols: extractRustSymbols,
|
|
723
|
+
};
|
|
724
|
+
const PYTHON_LANGUAGE_ADAPTER = {
|
|
725
|
+
id: 'python',
|
|
726
|
+
extensions: PYTHON_EXTENSIONS,
|
|
727
|
+
languageForPath: pythonLanguageForPath,
|
|
728
|
+
extractSymbols: extractPythonSymbols,
|
|
729
|
+
};
|
|
730
|
+
const CODE_OUTLINE_LANGUAGE_ADAPTERS = [
|
|
731
|
+
TYPESCRIPT_JAVASCRIPT_LANGUAGE_ADAPTER,
|
|
732
|
+
ASTRO_LANGUAGE_ADAPTER,
|
|
733
|
+
SVELTE_LANGUAGE_ADAPTER,
|
|
734
|
+
GO_LANGUAGE_ADAPTER,
|
|
735
|
+
RUST_LANGUAGE_ADAPTER,
|
|
736
|
+
PYTHON_LANGUAGE_ADAPTER,
|
|
737
|
+
];
|
|
738
|
+
const CODE_FILE_EXTENSIONS = CODE_OUTLINE_LANGUAGE_ADAPTERS.flatMap((adapter) => adapter.extensions);
|
|
739
|
+
export function extractSymbols(relativePath, language, contentSha256, text) {
|
|
740
|
+
const adapter = CODE_OUTLINE_LANGUAGE_ADAPTERS.find((candidate) => candidate.languageForPath(relativePath) === language);
|
|
741
|
+
return adapter?.extractSymbols(relativePath, language, contentSha256, text) ?? [];
|
|
742
|
+
}
|
|
743
|
+
function sourceAnchorEndLine(lineStart, rawText) {
|
|
744
|
+
return lineStart + rawText.split(/\r\n|\r|\n/u).length - 1;
|
|
745
|
+
}
|
|
746
|
+
function canBridgeAnchorToSymbol(lines, anchorEndLine, symbolStartLine) {
|
|
747
|
+
if (symbolStartLine <= anchorEndLine) {
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
for (let lineNumber = anchorEndLine + 1; lineNumber < symbolStartLine; lineNumber += 1) {
|
|
751
|
+
const trimmed = (lines[lineNumber - 1] ?? '').trim();
|
|
752
|
+
if (trimmed.length === 0 ||
|
|
753
|
+
trimmed.startsWith('//') ||
|
|
754
|
+
trimmed.startsWith('#') ||
|
|
755
|
+
trimmed.startsWith('*') ||
|
|
756
|
+
trimmed.startsWith('@')) {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
function findAnchorTargetSymbol(relativePath, lines, anchorEndLine, symbols) {
|
|
764
|
+
const followingSymbols = symbols
|
|
765
|
+
.filter((symbol) => symbol.path === relativePath && symbol.start_line > anchorEndLine)
|
|
766
|
+
.sort((left, right) => left.start_line - right.start_line);
|
|
767
|
+
const target = followingSymbols[0] ?? null;
|
|
768
|
+
if (!target || !canBridgeAnchorToSymbol(lines, anchorEndLine, target.start_line)) {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
return target;
|
|
772
|
+
}
|
|
773
|
+
function extractAnchors(relativePath, lines, text, symbols) {
|
|
774
|
+
const anchors = [];
|
|
775
|
+
for (const anchor of parseSourceAnchorsInContent(relativePath, text)) {
|
|
776
|
+
if (!anchor.idValid || sourceAnchorTextContainsSecretLike(anchor.rawText)) {
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
const lineEnd = sourceAnchorEndLine(anchor.lineStart, anchor.rawText);
|
|
780
|
+
const target = findAnchorTargetSymbol(relativePath, lines, lineEnd, symbols);
|
|
781
|
+
anchors.push({
|
|
782
|
+
id: anchor.rawId,
|
|
783
|
+
path: relativePath,
|
|
784
|
+
line_start: anchor.lineStart,
|
|
785
|
+
line_end: lineEnd,
|
|
786
|
+
purpose: anchor.fields.get('purpose') ?? null,
|
|
787
|
+
search: splitSourceAnchorList(anchor.fields.get('search')),
|
|
788
|
+
invariant: anchor.fields.get('invariant') ?? null,
|
|
789
|
+
risk: splitSourceAnchorList(anchor.fields.get('risk')),
|
|
790
|
+
navigation_only: true,
|
|
791
|
+
can_instruct_agent: false,
|
|
792
|
+
target_symbol_id: target?.id ?? null,
|
|
793
|
+
target_kind: target?.kind ?? null,
|
|
794
|
+
target_name: target?.name ?? null,
|
|
795
|
+
target_start_line: target?.start_line ?? null,
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
return anchors;
|
|
799
|
+
}
|
|
800
|
+
function createOutlineInputHash(policy, files, anchors, findings) {
|
|
459
801
|
const inputState = {
|
|
460
802
|
policy,
|
|
461
803
|
files: files.map((file) => ({
|
|
@@ -464,6 +806,13 @@ function createOutlineInputHash(policy, files, findings) {
|
|
|
464
806
|
size_bytes: file.size_bytes,
|
|
465
807
|
symbol_count: file.symbol_count,
|
|
466
808
|
})),
|
|
809
|
+
anchors: anchors.map((anchor) => ({
|
|
810
|
+
id: anchor.id,
|
|
811
|
+
path: anchor.path,
|
|
812
|
+
line_start: anchor.line_start,
|
|
813
|
+
line_end: anchor.line_end,
|
|
814
|
+
target_symbol_id: anchor.target_symbol_id,
|
|
815
|
+
})),
|
|
467
816
|
input_errors: findings
|
|
468
817
|
.filter((finding) => ERROR_OUTLINE_CODES.has(finding.code))
|
|
469
818
|
.map((finding) => ({ code: finding.code, path: finding.path })),
|
|
@@ -486,6 +835,7 @@ export function inspectCodeOutline(projectRoot, options) {
|
|
|
486
835
|
ignored_directories: [...IGNORED_DIRECTORIES],
|
|
487
836
|
};
|
|
488
837
|
const files = [];
|
|
838
|
+
const anchors = [];
|
|
489
839
|
const symbols = [];
|
|
490
840
|
const findings = [];
|
|
491
841
|
const issues = [];
|
|
@@ -506,15 +856,18 @@ export function inspectCodeOutline(projectRoot, options) {
|
|
|
506
856
|
}
|
|
507
857
|
const contentSha256 = sha256Tagged(buffer);
|
|
508
858
|
const text = buffer.toString('utf8');
|
|
859
|
+
const lines = text.split(/\r\n|\r|\n/u);
|
|
509
860
|
const fileSymbols = extractSymbols(candidate.relativePath, candidate.language, contentSha256, text);
|
|
861
|
+
const fileAnchors = extractAnchors(candidate.relativePath, lines, text, fileSymbols);
|
|
510
862
|
symbols.push(...fileSymbols);
|
|
863
|
+
anchors.push(...fileAnchors);
|
|
511
864
|
files.push({
|
|
512
865
|
kind: 'source_file',
|
|
513
866
|
path: candidate.relativePath,
|
|
514
867
|
language: candidate.language,
|
|
515
868
|
sha256: contentSha256,
|
|
516
869
|
size_bytes: buffer.byteLength,
|
|
517
|
-
line_count:
|
|
870
|
+
line_count: lines.length,
|
|
518
871
|
symbol_count: fileSymbols.length,
|
|
519
872
|
});
|
|
520
873
|
}
|
|
@@ -530,8 +883,9 @@ export function inspectCodeOutline(projectRoot, options) {
|
|
|
530
883
|
ok: status === 'passed',
|
|
531
884
|
mustflow_root: root,
|
|
532
885
|
policy,
|
|
533
|
-
input_hash: createOutlineInputHash(policy, files, findings),
|
|
886
|
+
input_hash: createOutlineInputHash(policy, files, anchors, findings),
|
|
534
887
|
files,
|
|
888
|
+
anchors: anchors.sort((left, right) => left.path.localeCompare(right.path) || left.line_start - right.line_start),
|
|
535
889
|
symbols: symbols.sort((left, right) => left.path.localeCompare(right.path) || left.start_line - right.start_line),
|
|
536
890
|
findings,
|
|
537
891
|
issues,
|
|
@@ -565,10 +919,12 @@ function createSymbolReadInputHash(policy, target, findings) {
|
|
|
565
919
|
: {
|
|
566
920
|
path: target.path,
|
|
567
921
|
sha256: target.sha256,
|
|
922
|
+
requested_anchor_id: target.requested_anchor_id,
|
|
568
923
|
requested_start_line: target.requested_start_line,
|
|
569
924
|
requested_end_line: target.requested_end_line,
|
|
570
925
|
context_start_line: target.context_start_line,
|
|
571
926
|
context_end_line: target.context_end_line,
|
|
927
|
+
anchor_id: target.anchor?.id ?? null,
|
|
572
928
|
},
|
|
573
929
|
input_errors: findings
|
|
574
930
|
.filter((finding) => ERROR_SYMBOL_READ_CODES.has(finding.code))
|
|
@@ -581,75 +937,66 @@ function createSymbolReadInputHash(policy, target, findings) {
|
|
|
581
937
|
};
|
|
582
938
|
return sha256Tagged(JSON.stringify(inputState));
|
|
583
939
|
}
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
issues.push(message);
|
|
610
|
-
findings.push(makeSymbolReadFinding('code_symbol_read_path_outside_root', 'high', options.path, policy.start_line, policy.end_line, message));
|
|
611
|
-
return createEmptySymbolReadReport(root, policy, findings, issues);
|
|
612
|
-
}
|
|
613
|
-
const language = languageForPath(absolutePath);
|
|
614
|
-
if (!language || !existsSync(absolutePath)) {
|
|
615
|
-
const message = `${relativePath} is not a supported existing code file.`;
|
|
616
|
-
issues.push(message);
|
|
617
|
-
findings.push(makeSymbolReadFinding('code_symbol_read_unreadable_path', 'high', relativePath, policy.start_line, policy.end_line, message));
|
|
618
|
-
return createEmptySymbolReadReport(root, policy, findings, issues);
|
|
619
|
-
}
|
|
620
|
-
let buffer;
|
|
621
|
-
try {
|
|
622
|
-
buffer = readFileInsideWithoutSymlinks(root, absolutePath, { maxBytes: policy.max_file_bytes });
|
|
623
|
-
}
|
|
624
|
-
catch (error) {
|
|
625
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
626
|
-
issues.push(`${relativePath}: ${message}`);
|
|
627
|
-
findings.push(makeSymbolReadFinding('code_symbol_read_unreadable_path', 'high', relativePath, policy.start_line, policy.end_line, message));
|
|
628
|
-
return createEmptySymbolReadReport(root, policy, findings, issues);
|
|
940
|
+
function collectAnchorReadCandidates(root, anchorId, maxFileBytes) {
|
|
941
|
+
const candidates = [];
|
|
942
|
+
for (const relativePath of listSourceAnchorFiles(root, { allowedExtensions: CODE_FILE_EXTENSIONS, maxFileBytes })) {
|
|
943
|
+
const absolutePath = path.join(root, ...relativePath.split('/'));
|
|
944
|
+
const language = languageForPath(absolutePath);
|
|
945
|
+
if (!language || !existsSync(absolutePath)) {
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
let buffer;
|
|
949
|
+
try {
|
|
950
|
+
buffer = readFileInsideWithoutSymlinks(root, absolutePath, { maxBytes: maxFileBytes });
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
const contentSha256 = sha256Tagged(buffer);
|
|
956
|
+
const text = buffer.toString('utf8');
|
|
957
|
+
const lines = text.split(/\r\n|\r|\n/u);
|
|
958
|
+
const symbols = extractSymbols(relativePath, language, contentSha256, text);
|
|
959
|
+
const anchors = extractAnchors(relativePath, lines, text, symbols);
|
|
960
|
+
for (const anchor of anchors) {
|
|
961
|
+
if (anchor.id === anchorId) {
|
|
962
|
+
candidates.push({ relativePath, anchor });
|
|
963
|
+
}
|
|
964
|
+
}
|
|
629
965
|
}
|
|
966
|
+
return candidates.sort((left, right) => left.relativePath.localeCompare(right.relativePath) || left.anchor.line_start - right.anchor.line_start);
|
|
967
|
+
}
|
|
968
|
+
function createSymbolReadReportFromFile(root, policy, relativePath, language, buffer, findings, issues, anchor) {
|
|
630
969
|
const contentSha256 = sha256Tagged(buffer);
|
|
631
970
|
const text = buffer.toString('utf8');
|
|
632
971
|
const lines = text.split(/\r\n|\r|\n/u);
|
|
633
|
-
if (policy.start_line > lines.length || (policy.end_line !== null && policy.end_line > lines.length)) {
|
|
634
|
-
const
|
|
972
|
+
if ((policy.start_line !== null && policy.start_line > lines.length) || (policy.end_line !== null && policy.end_line > lines.length)) {
|
|
973
|
+
const requestedRange = policy.start_line === null
|
|
974
|
+
? `anchor ${policy.anchor_id ?? '<unknown>'}`
|
|
975
|
+
: `${policy.start_line}${policy.end_line === null ? '' : `-${policy.end_line}`}`;
|
|
976
|
+
const message = `Line range ${requestedRange} is outside ${relativePath}, which has ${lines.length} lines.`;
|
|
635
977
|
issues.push(message);
|
|
636
978
|
findings.push(makeSymbolReadFinding('code_symbol_read_invalid_range', 'high', relativePath, policy.start_line, policy.end_line, message));
|
|
637
979
|
return createEmptySymbolReadReport(root, policy, findings, issues);
|
|
638
980
|
}
|
|
639
981
|
const symbols = extractSymbols(relativePath, language, contentSha256, text);
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
982
|
+
const requestedStartLine = policy.start_line;
|
|
983
|
+
const matchedSymbol = anchor
|
|
984
|
+
? (symbols.find((symbol) => symbol.id === anchor.target_symbol_id) ?? null)
|
|
985
|
+
: policy.end_line === null && requestedStartLine !== null
|
|
986
|
+
? (symbols.find((symbol) => symbol.start_line === requestedStartLine) ??
|
|
987
|
+
symbols.find((symbol) => symbol.start_line <= requestedStartLine && symbol.end_line >= requestedStartLine) ??
|
|
988
|
+
null)
|
|
989
|
+
: null;
|
|
990
|
+
const resolvedStartLine = matchedSymbol?.start_line ?? requestedStartLine;
|
|
646
991
|
const resolvedEndLine = matchedSymbol?.end_line ?? policy.end_line;
|
|
647
992
|
if (resolvedEndLine === null) {
|
|
648
|
-
const message =
|
|
993
|
+
const message = anchor
|
|
994
|
+
? `Source anchor ${anchor.id} in ${relativePath}:${anchor.line_start} does not target a readable outline symbol.`
|
|
995
|
+
: `No outline symbol starts at or contains line ${policy.start_line} in ${relativePath}. Pass --end-line to read an explicit range.`;
|
|
649
996
|
issues.push(message);
|
|
650
|
-
findings.push(makeSymbolReadFinding('code_symbol_read_no_symbol_at_line', 'high', relativePath, policy.start_line, null, message));
|
|
997
|
+
findings.push(makeSymbolReadFinding(anchor ? 'code_symbol_read_anchor_without_symbol' : 'code_symbol_read_no_symbol_at_line', 'high', relativePath, policy.start_line ?? anchor?.line_start ?? null, null, message));
|
|
651
998
|
}
|
|
652
|
-
const contextStartLine = resolvedEndLine === null ? null : Math.max(1, resolvedStartLine - policy.context_lines);
|
|
999
|
+
const contextStartLine = resolvedEndLine === null || resolvedStartLine === null ? null : Math.max(1, resolvedStartLine - policy.context_lines);
|
|
653
1000
|
const contextEndLine = resolvedEndLine === null ? null : Math.min(lines.length, resolvedEndLine + policy.context_lines);
|
|
654
1001
|
const target = {
|
|
655
1002
|
path: relativePath,
|
|
@@ -657,12 +1004,14 @@ export function readCodeSymbol(projectRoot, options) {
|
|
|
657
1004
|
sha256: contentSha256,
|
|
658
1005
|
size_bytes: buffer.byteLength,
|
|
659
1006
|
line_count: lines.length,
|
|
1007
|
+
requested_anchor_id: policy.anchor_id,
|
|
660
1008
|
requested_start_line: policy.start_line,
|
|
661
1009
|
requested_end_line: policy.end_line,
|
|
662
1010
|
resolved_start_line: resolvedEndLine === null ? null : resolvedStartLine,
|
|
663
1011
|
resolved_end_line: resolvedEndLine,
|
|
664
1012
|
context_start_line: contextStartLine,
|
|
665
1013
|
context_end_line: contextEndLine,
|
|
1014
|
+
anchor,
|
|
666
1015
|
symbol: matchedSymbol,
|
|
667
1016
|
};
|
|
668
1017
|
let snippet = null;
|
|
@@ -703,3 +1052,101 @@ export function readCodeSymbol(projectRoot, options) {
|
|
|
703
1052
|
issues,
|
|
704
1053
|
};
|
|
705
1054
|
}
|
|
1055
|
+
export function readCodeSymbol(projectRoot, options) {
|
|
1056
|
+
const root = path.resolve(projectRoot);
|
|
1057
|
+
const policy = {
|
|
1058
|
+
anchor_id: options.anchorId ?? null,
|
|
1059
|
+
start_line: options.startLine ?? null,
|
|
1060
|
+
end_line: options.endLine ?? null,
|
|
1061
|
+
context_lines: options.contextLines ?? DEFAULT_CONTEXT_LINES,
|
|
1062
|
+
max_file_bytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES,
|
|
1063
|
+
max_snippet_lines: options.maxSnippetLines ?? DEFAULT_MAX_SNIPPET_LINES,
|
|
1064
|
+
};
|
|
1065
|
+
const findings = [];
|
|
1066
|
+
const issues = [];
|
|
1067
|
+
if (policy.context_lines < 0 ||
|
|
1068
|
+
(policy.start_line !== null && policy.start_line < 1) ||
|
|
1069
|
+
(policy.end_line !== null && (policy.start_line === null || policy.end_line < policy.start_line))) {
|
|
1070
|
+
findings.push(makeSymbolReadFinding('code_symbol_read_invalid_range', 'high', options.path ?? '.', policy.start_line, policy.end_line, 'Line range must use 1-based positive lines, and end_line must be greater than or equal to start_line.'));
|
|
1071
|
+
return createEmptySymbolReadReport(root, policy, findings, issues);
|
|
1072
|
+
}
|
|
1073
|
+
if (policy.anchor_id !== null) {
|
|
1074
|
+
const anchorCandidates = collectAnchorReadCandidates(root, policy.anchor_id, policy.max_file_bytes);
|
|
1075
|
+
if (anchorCandidates.length === 0) {
|
|
1076
|
+
const message = `No source anchor found with id ${policy.anchor_id}.`;
|
|
1077
|
+
issues.push(message);
|
|
1078
|
+
findings.push(makeSymbolReadFinding('code_symbol_read_anchor_not_found', 'high', '.', null, null, message));
|
|
1079
|
+
return createEmptySymbolReadReport(root, policy, findings, issues);
|
|
1080
|
+
}
|
|
1081
|
+
if (anchorCandidates.length > 1) {
|
|
1082
|
+
const locations = anchorCandidates.map((candidate) => `${candidate.relativePath}:${candidate.anchor.line_start}`).join(', ');
|
|
1083
|
+
const message = `Source anchor id ${policy.anchor_id} is ambiguous: ${locations}.`;
|
|
1084
|
+
issues.push(message);
|
|
1085
|
+
findings.push(makeSymbolReadFinding('code_symbol_read_anchor_ambiguous', 'high', '.', null, null, message));
|
|
1086
|
+
return createEmptySymbolReadReport(root, policy, findings, issues);
|
|
1087
|
+
}
|
|
1088
|
+
const candidate = anchorCandidates[0];
|
|
1089
|
+
const anchor = candidate?.anchor ?? null;
|
|
1090
|
+
const relativePath = candidate?.relativePath ?? '.';
|
|
1091
|
+
const absolutePath = path.join(root, ...relativePath.split('/'));
|
|
1092
|
+
const language = languageForPath(absolutePath);
|
|
1093
|
+
if (!anchor || !language || !existsSync(absolutePath)) {
|
|
1094
|
+
const message = `${relativePath} is not a supported existing code file.`;
|
|
1095
|
+
issues.push(message);
|
|
1096
|
+
findings.push(makeSymbolReadFinding('code_symbol_read_unreadable_path', 'high', relativePath, null, null, message));
|
|
1097
|
+
return createEmptySymbolReadReport(root, policy, findings, issues);
|
|
1098
|
+
}
|
|
1099
|
+
let buffer;
|
|
1100
|
+
try {
|
|
1101
|
+
buffer = readFileInsideWithoutSymlinks(root, absolutePath, { maxBytes: policy.max_file_bytes });
|
|
1102
|
+
}
|
|
1103
|
+
catch (error) {
|
|
1104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1105
|
+
issues.push(`${relativePath}: ${message}`);
|
|
1106
|
+
findings.push(makeSymbolReadFinding('code_symbol_read_unreadable_path', 'high', relativePath, null, null, message));
|
|
1107
|
+
return createEmptySymbolReadReport(root, policy, findings, issues);
|
|
1108
|
+
}
|
|
1109
|
+
const anchorPolicy = {
|
|
1110
|
+
...policy,
|
|
1111
|
+
start_line: null,
|
|
1112
|
+
end_line: null,
|
|
1113
|
+
};
|
|
1114
|
+
return createSymbolReadReportFromFile(root, anchorPolicy, relativePath, language, buffer, findings, issues, anchor);
|
|
1115
|
+
}
|
|
1116
|
+
if (!options.path || policy.start_line === null) {
|
|
1117
|
+
findings.push(makeSymbolReadFinding('code_symbol_read_invalid_range', 'high', options.path ?? '.', policy.start_line, policy.end_line, 'Path and --start-line are required unless --anchor is provided.'));
|
|
1118
|
+
return createEmptySymbolReadReport(root, policy, findings, issues);
|
|
1119
|
+
}
|
|
1120
|
+
let absolutePath;
|
|
1121
|
+
let relativePath;
|
|
1122
|
+
try {
|
|
1123
|
+
const normalized = normalizeTargetPath(root, options.path);
|
|
1124
|
+
absolutePath = normalized.absolutePath;
|
|
1125
|
+
relativePath = normalized.relativePath;
|
|
1126
|
+
ensureInsideWithoutSymlinks(root, absolutePath);
|
|
1127
|
+
}
|
|
1128
|
+
catch (error) {
|
|
1129
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1130
|
+
issues.push(message);
|
|
1131
|
+
findings.push(makeSymbolReadFinding('code_symbol_read_path_outside_root', 'high', options.path, policy.start_line, policy.end_line, message));
|
|
1132
|
+
return createEmptySymbolReadReport(root, policy, findings, issues);
|
|
1133
|
+
}
|
|
1134
|
+
const language = languageForPath(absolutePath);
|
|
1135
|
+
if (!language || !existsSync(absolutePath)) {
|
|
1136
|
+
const message = `${relativePath} is not a supported existing code file.`;
|
|
1137
|
+
issues.push(message);
|
|
1138
|
+
findings.push(makeSymbolReadFinding('code_symbol_read_unreadable_path', 'high', relativePath, policy.start_line, policy.end_line, message));
|
|
1139
|
+
return createEmptySymbolReadReport(root, policy, findings, issues);
|
|
1140
|
+
}
|
|
1141
|
+
let buffer;
|
|
1142
|
+
try {
|
|
1143
|
+
buffer = readFileInsideWithoutSymlinks(root, absolutePath, { maxBytes: policy.max_file_bytes });
|
|
1144
|
+
}
|
|
1145
|
+
catch (error) {
|
|
1146
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1147
|
+
issues.push(`${relativePath}: ${message}`);
|
|
1148
|
+
findings.push(makeSymbolReadFinding('code_symbol_read_unreadable_path', 'high', relativePath, policy.start_line, policy.end_line, message));
|
|
1149
|
+
return createEmptySymbolReadReport(root, policy, findings, issues);
|
|
1150
|
+
}
|
|
1151
|
+
return createSymbolReadReportFromFile(root, policy, relativePath, language, buffer, findings, issues, null);
|
|
1152
|
+
}
|