mustflow 2.75.2 → 2.85.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +40 -3
  2. package/dist/cli/commands/docs.js +86 -2
  3. package/dist/cli/commands/script-pack.js +9 -0
  4. package/dist/cli/i18n/en.js +180 -2
  5. package/dist/cli/i18n/es.js +180 -2
  6. package/dist/cli/i18n/fr.js +180 -2
  7. package/dist/cli/i18n/hi.js +180 -2
  8. package/dist/cli/i18n/ko.js +180 -2
  9. package/dist/cli/i18n/zh.js +180 -2
  10. package/dist/cli/lib/repo-map.js +27 -6
  11. package/dist/cli/lib/run-root-trust.js +15 -1
  12. package/dist/cli/lib/script-pack-registry.js +275 -6
  13. package/dist/cli/lib/validation/index.js +2 -2
  14. package/dist/cli/lib/validation/primitives.js +4 -1
  15. package/dist/cli/script-packs/code-change-impact.js +172 -0
  16. package/dist/cli/script-packs/code-dependency-graph.js +181 -0
  17. package/dist/cli/script-packs/code-export-diff.js +160 -0
  18. package/dist/cli/script-packs/code-outline.js +33 -5
  19. package/dist/cli/script-packs/code-route-outline.js +155 -0
  20. package/dist/cli/script-packs/docs-reference-drift.js +150 -0
  21. package/dist/cli/script-packs/repo-config-chain.js +163 -0
  22. package/dist/cli/script-packs/repo-env-contract.js +156 -0
  23. package/dist/cli/script-packs/repo-related-files.js +161 -0
  24. package/dist/cli/script-packs/repo-secret-risk-scan.js +147 -0
  25. package/dist/core/change-impact.js +383 -0
  26. package/dist/core/change-verification.js +32 -5
  27. package/dist/core/code-outline.js +460 -79
  28. package/dist/core/config-chain.js +595 -0
  29. package/dist/core/config-loading.js +121 -4
  30. package/dist/core/dependency-graph.js +490 -0
  31. package/dist/core/env-contract.js +450 -0
  32. package/dist/core/export-diff.js +359 -0
  33. package/dist/core/line-endings.js +26 -13
  34. package/dist/core/public-json-contracts.js +126 -0
  35. package/dist/core/reference-drift.js +388 -0
  36. package/dist/core/related-files.js +493 -0
  37. package/dist/core/route-outline.js +964 -0
  38. package/dist/core/script-pack-suggestions.js +131 -5
  39. package/dist/core/secret-risk-scan.js +440 -0
  40. package/dist/core/source-anchors.js +13 -1
  41. package/package.json +1 -1
  42. package/schemas/README.md +44 -6
  43. package/schemas/change-impact-report.schema.json +150 -0
  44. package/schemas/code-outline-report.schema.json +1 -1
  45. package/schemas/code-symbol-read-report.schema.json +64 -4
  46. package/schemas/commands.schema.json +12 -0
  47. package/schemas/config-chain-report.schema.json +187 -0
  48. package/schemas/dependency-graph-report.schema.json +149 -0
  49. package/schemas/env-contract-report.schema.json +203 -0
  50. package/schemas/export-diff-report.schema.json +220 -0
  51. package/schemas/reference-drift-report.schema.json +166 -0
  52. package/schemas/related-files-report.schema.json +145 -0
  53. package/schemas/route-outline-report.schema.json +200 -0
  54. package/schemas/secret-risk-scan-report.schema.json +152 -0
  55. package/templates/default/common/.mustflow/config/commands.toml +21 -0
  56. package/templates/default/i18n.toml +21 -9
  57. package/templates/default/locales/en/.mustflow/docs/agent-workflow.md +1 -1
  58. package/templates/default/locales/en/.mustflow/skills/INDEX.md +8 -2
  59. package/templates/default/locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md +28 -11
  60. package/templates/default/locales/en/.mustflow/skills/astro-code-change/SKILL.md +71 -27
  61. package/templates/default/locales/en/.mustflow/skills/cross-agent-session-reference/SKILL.md +146 -0
  62. package/templates/default/locales/en/.mustflow/skills/dependency-upgrade-review/SKILL.md +3 -1
  63. package/templates/default/locales/en/.mustflow/skills/github-contribution-quality-gate/SKILL.md +48 -11
  64. package/templates/default/locales/en/.mustflow/skills/javascript-code-change/SKILL.md +15 -13
  65. package/templates/default/locales/en/.mustflow/skills/node-code-change/SKILL.md +16 -14
  66. package/templates/default/locales/en/.mustflow/skills/routes.toml +21 -9
  67. package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +3 -1
  68. package/templates/default/locales/en/.mustflow/skills/test-suite-performance-review/SKILL.md +314 -0
  69. package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +13 -10
  70. package/templates/default/manifest.toml +15 -1
@@ -2,7 +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 { parseSourceAnchorsInContent, sourceAnchorTextContainsSecretLike, splitSourceAnchorList, } from './source-anchors.js';
5
+ import { listSourceAnchorFiles, parseSourceAnchorsInContent, sourceAnchorTextContainsSecretLike, splitSourceAnchorList, } from './source-anchors.js';
6
6
  export const CODE_PACK_ID = 'code';
7
7
  export const CODE_OUTLINE_SCRIPT_ID = 'outline';
8
8
  export const CODE_OUTLINE_SCRIPT_REF = `${CODE_PACK_ID}/${CODE_OUTLINE_SCRIPT_ID}`;
@@ -13,7 +13,12 @@ const DEFAULT_MAX_FILES = 200;
13
13
  const DEFAULT_CONTEXT_LINES = 0;
14
14
  const DEFAULT_MAX_SNIPPET_LINES = 250;
15
15
  const RETURN_PREVIEW_MAX_CHARS = 120;
16
- const CODE_FILE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
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'];
17
22
  const IGNORED_DIRECTORIES = [
18
23
  '.git',
19
24
  '.mustflow/cache',
@@ -36,6 +41,9 @@ const ERROR_SYMBOL_READ_CODES = new Set([
36
41
  'code_symbol_read_unreadable_path',
37
42
  'code_symbol_read_invalid_range',
38
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',
39
47
  'code_symbol_read_snippet_too_large',
40
48
  ]);
41
49
  function toPosixPath(value) {
@@ -47,7 +55,7 @@ function normalizeRelativePath(value) {
47
55
  function sha256Tagged(buffer) {
48
56
  return `sha256:${createHash('sha256').update(buffer).digest('hex')}`;
49
57
  }
50
- function languageForPath(filePath) {
58
+ function typescriptJavascriptLanguageForPath(filePath) {
51
59
  switch (path.extname(filePath).toLowerCase()) {
52
60
  case '.ts':
53
61
  case '.mts':
@@ -67,6 +75,27 @@ function languageForPath(filePath) {
67
75
  return null;
68
76
  }
69
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
+ }
70
99
  function isIgnoredDirectory(relativePath) {
71
100
  const normalized = normalizeRelativePath(relativePath);
72
101
  return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
@@ -206,6 +235,7 @@ function declarationFromLine(line) {
206
235
  name: functionMatch.groups.name ?? '<anonymous>',
207
236
  exported: Boolean(functionMatch.groups.exported),
208
237
  async: Boolean(functionMatch.groups.async),
238
+ returnType: null,
209
239
  };
210
240
  }
211
241
  const shapeMatch = /^(?<exported>export\s+)?(?<kind>class|interface|type|enum)\s+(?<name>[$A-Z_a-z][$\w]*)/u.exec(trimmed);
@@ -215,6 +245,7 @@ function declarationFromLine(line) {
215
245
  name: shapeMatch.groups.name ?? '<anonymous>',
216
246
  exported: Boolean(shapeMatch.groups.exported),
217
247
  async: false,
248
+ returnType: null,
218
249
  };
219
250
  }
220
251
  const variableMatch = /^(?<exported>export\s+)?(?:const|let|var)\s+(?<name>[$A-Z_a-z][$\w]*)\s*[:=]/u.exec(trimmed);
@@ -224,6 +255,7 @@ function declarationFromLine(line) {
224
255
  name: variableMatch.groups.name ?? '<anonymous>',
225
256
  exported: Boolean(variableMatch.groups.exported),
226
257
  async: /async\s*(?:\(|[A-Z_a-z_$])/u.test(trimmed),
258
+ returnType: null,
227
259
  };
228
260
  }
229
261
  return null;
@@ -256,7 +288,7 @@ function buildSignature(lines, startLine, endLine) {
256
288
  if (trimmed.length > 0) {
257
289
  parts.push(trimmed);
258
290
  }
259
- 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)) {
260
292
  break;
261
293
  }
262
294
  }
@@ -270,6 +302,9 @@ function extractExplicitReturnType(signature, match) {
270
302
  if (match.kind !== 'function') {
271
303
  return null;
272
304
  }
305
+ if (match.returnType !== null) {
306
+ return match.returnType;
307
+ }
273
308
  const functionReturnMatch = /\bfunction(?:\s+[$A-Z_a-z][$\w]*)?\s*\([^)]*\)\s*:\s*(?<returnType>[^={;]+?)(?=\s*(?:\{|$))/u.exec(signature);
274
309
  if (functionReturnMatch?.groups?.returnType) {
275
310
  return normalizeReturnType(functionReturnMatch.groups.returnType);
@@ -304,7 +339,7 @@ function extractArrowExpressionReturnPreview(signature) {
304
339
  function isIdentifierCharacter(value) {
305
340
  return typeof value === 'string' && /[$\w]/u.test(value);
306
341
  }
307
- function findReturnKeywordIndex(line) {
342
+ function findReturnKeywordIndex(line, lineCommentStart) {
308
343
  let quote = null;
309
344
  let escaped = false;
310
345
  for (let index = 0; index < line.length; index += 1) {
@@ -324,7 +359,8 @@ function findReturnKeywordIndex(line) {
324
359
  }
325
360
  continue;
326
361
  }
327
- if (character === '/' && nextCharacter === '/') {
362
+ if ((lineCommentStart === '//' && character === '/' && nextCharacter === '/') ||
363
+ (lineCommentStart === '#' && character === '#')) {
328
364
  return -1;
329
365
  }
330
366
  if (character === '"' || character === "'" || character === '`') {
@@ -339,21 +375,21 @@ function findReturnKeywordIndex(line) {
339
375
  }
340
376
  return -1;
341
377
  }
342
- function extractReturnExpressionPreview(line) {
343
- const returnIndex = findReturnKeywordIndex(line);
378
+ function extractReturnExpressionPreview(line, lineCommentStart) {
379
+ const returnIndex = findReturnKeywordIndex(line, lineCommentStart);
344
380
  if (returnIndex < 0) {
345
381
  return null;
346
382
  }
347
- const expression = line
348
- .slice(returnIndex + 'return'.length)
349
- .replace(/\/\/.*$/u, '')
383
+ const rawExpression = line.slice(returnIndex + 'return'.length);
384
+ const commentIndex = rawExpression.indexOf(lineCommentStart);
385
+ const expression = (commentIndex >= 0 ? rawExpression.slice(0, commentIndex) : rawExpression)
350
386
  .trim()
351
387
  .replace(/;\s*$/u, '')
352
388
  .trim();
353
389
  return expression.length === 0 ? null : truncateReturnPreview(expression);
354
390
  }
355
- function hasReturnStatement(line) {
356
- return findReturnKeywordIndex(line) >= 0;
391
+ function hasReturnStatement(line, lineCommentStart) {
392
+ return findReturnKeywordIndex(line, lineCommentStart) >= 0;
357
393
  }
358
394
  function simpleBodyThrowsOnly(lines, startLine, endLine, returnType) {
359
395
  let sawThrow = false;
@@ -373,7 +409,26 @@ function simpleBodyThrowsOnly(lines, startLine, endLine, returnType) {
373
409
  }
374
410
  return sawThrow && (!sawOtherExecutableLine || returnType === 'never');
375
411
  }
376
- function extractReturnMetadata(lines, startLine, endLine, signature, match) {
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) {
377
432
  const returnType = extractExplicitReturnType(signature, match);
378
433
  if (match.kind !== 'function') {
379
434
  return {
@@ -389,11 +444,11 @@ function extractReturnMetadata(lines, startLine, endLine, signature, match) {
389
444
  let voidReturnCount = 0;
390
445
  for (let index = startLine - 1; index < endLine; index += 1) {
391
446
  const line = lines[index] ?? '';
392
- if (!hasReturnStatement(line)) {
447
+ if (!hasReturnStatement(line, lineCommentStart)) {
393
448
  continue;
394
449
  }
395
450
  returnLines.push(index + 1);
396
- const preview = extractReturnExpressionPreview(line);
451
+ const preview = extractReturnExpressionPreview(line, lineCommentStart);
397
452
  if (preview === null) {
398
453
  voidReturnCount += 1;
399
454
  }
@@ -402,11 +457,12 @@ function extractReturnMetadata(lines, startLine, endLine, signature, match) {
402
457
  }
403
458
  }
404
459
  const arrowExpressionPreview = returnLines.length === 0 ? extractArrowExpressionReturnPreview(signature) : null;
460
+ const tailExpressionPreview = tailExpressionReturn && returnLines.length === 0 && returnType !== null ? extractTailExpressionReturnPreview(lines, startLine, endLine) : null;
405
461
  let returnBehavior;
406
462
  if (valueReturnPreviews.length > 0 && voidReturnCount > 0) {
407
463
  returnBehavior = 'mixed';
408
464
  }
409
- else if (valueReturnPreviews.length > 0 || arrowExpressionPreview !== null) {
465
+ else if (valueReturnPreviews.length > 0 || arrowExpressionPreview !== null || tailExpressionPreview !== null) {
410
466
  returnBehavior = 'value';
411
467
  }
412
468
  else if (voidReturnCount > 0) {
@@ -423,21 +479,141 @@ function extractReturnMetadata(lines, startLine, endLine, signature, match) {
423
479
  return_behavior: returnBehavior,
424
480
  return_count: returnLines.length,
425
481
  return_lines: returnLines,
426
- return_preview: valueReturnPreviews[0] ?? arrowExpressionPreview,
482
+ return_preview: valueReturnPreviews[0] ?? arrowExpressionPreview ?? tailExpressionPreview,
427
483
  };
428
484
  }
429
- function extractSymbols(relativePath, language, contentSha256, text) {
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) {
430
606
  const lines = text.split(/\r\n|\r|\n/u);
431
607
  const symbols = [];
432
608
  for (const [index, line] of lines.entries()) {
433
- const match = declarationFromLine(line);
609
+ const match = declarationForLine(line);
434
610
  if (!match) {
435
611
  continue;
436
612
  }
437
613
  const startLine = index + 1;
438
- const endLine = findDeclarationEndLine(lines, index);
614
+ const endLine = endLineForMatch(lines, index);
439
615
  const signature = buildSignature(lines, startLine, endLine);
440
- const returnMetadata = extractReturnMetadata(lines, startLine, endLine, signature, match);
616
+ const returnMetadata = extractReturnMetadata(lines, startLine, endLine, signature, match, lineCommentStart, tailExpressionReturn);
441
617
  symbols.push({
442
618
  id: `${relativePath}:${startLine}:${match.kind}:${match.name}`,
443
619
  path: relativePath,
@@ -456,6 +632,114 @@ function extractSymbols(relativePath, language, contentSha256, text) {
456
632
  }
457
633
  return symbols;
458
634
  }
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
+ }
459
743
  function sourceAnchorEndLine(lineStart, rawText) {
460
744
  return lineStart + rawText.split(/\r\n|\r|\n/u).length - 1;
461
745
  }
@@ -465,7 +749,11 @@ function canBridgeAnchorToSymbol(lines, anchorEndLine, symbolStartLine) {
465
749
  }
466
750
  for (let lineNumber = anchorEndLine + 1; lineNumber < symbolStartLine; lineNumber += 1) {
467
751
  const trimmed = (lines[lineNumber - 1] ?? '').trim();
468
- if (trimmed.length === 0 || trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('@')) {
752
+ if (trimmed.length === 0 ||
753
+ trimmed.startsWith('//') ||
754
+ trimmed.startsWith('#') ||
755
+ trimmed.startsWith('*') ||
756
+ trimmed.startsWith('@')) {
469
757
  continue;
470
758
  }
471
759
  return false;
@@ -631,10 +919,12 @@ function createSymbolReadInputHash(policy, target, findings) {
631
919
  : {
632
920
  path: target.path,
633
921
  sha256: target.sha256,
922
+ requested_anchor_id: target.requested_anchor_id,
634
923
  requested_start_line: target.requested_start_line,
635
924
  requested_end_line: target.requested_end_line,
636
925
  context_start_line: target.context_start_line,
637
926
  context_end_line: target.context_end_line,
927
+ anchor_id: target.anchor?.id ?? null,
638
928
  },
639
929
  input_errors: findings
640
930
  .filter((finding) => ERROR_SYMBOL_READ_CODES.has(finding.code))
@@ -647,75 +937,66 @@ function createSymbolReadInputHash(policy, target, findings) {
647
937
  };
648
938
  return sha256Tagged(JSON.stringify(inputState));
649
939
  }
650
- export function readCodeSymbol(projectRoot, options) {
651
- const root = path.resolve(projectRoot);
652
- const policy = {
653
- start_line: options.startLine,
654
- end_line: options.endLine ?? null,
655
- context_lines: options.contextLines ?? DEFAULT_CONTEXT_LINES,
656
- max_file_bytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES,
657
- max_snippet_lines: options.maxSnippetLines ?? DEFAULT_MAX_SNIPPET_LINES,
658
- };
659
- const findings = [];
660
- const issues = [];
661
- if (policy.start_line < 1 || (policy.end_line !== null && policy.end_line < policy.start_line) || policy.context_lines < 0) {
662
- 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.'));
663
- return createEmptySymbolReadReport(root, policy, findings, issues);
664
- }
665
- let absolutePath;
666
- let relativePath;
667
- try {
668
- const normalized = normalizeTargetPath(root, options.path);
669
- absolutePath = normalized.absolutePath;
670
- relativePath = normalized.relativePath;
671
- ensureInsideWithoutSymlinks(root, absolutePath);
672
- }
673
- catch (error) {
674
- const message = error instanceof Error ? error.message : String(error);
675
- issues.push(message);
676
- findings.push(makeSymbolReadFinding('code_symbol_read_path_outside_root', 'high', options.path, policy.start_line, policy.end_line, message));
677
- return createEmptySymbolReadReport(root, policy, findings, issues);
678
- }
679
- const language = languageForPath(absolutePath);
680
- if (!language || !existsSync(absolutePath)) {
681
- const message = `${relativePath} is not a supported existing code file.`;
682
- issues.push(message);
683
- findings.push(makeSymbolReadFinding('code_symbol_read_unreadable_path', 'high', relativePath, policy.start_line, policy.end_line, message));
684
- return createEmptySymbolReadReport(root, policy, findings, issues);
685
- }
686
- let buffer;
687
- try {
688
- buffer = readFileInsideWithoutSymlinks(root, absolutePath, { maxBytes: policy.max_file_bytes });
689
- }
690
- catch (error) {
691
- const message = error instanceof Error ? error.message : String(error);
692
- issues.push(`${relativePath}: ${message}`);
693
- findings.push(makeSymbolReadFinding('code_symbol_read_unreadable_path', 'high', relativePath, policy.start_line, policy.end_line, message));
694
- 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
+ }
695
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) {
696
969
  const contentSha256 = sha256Tagged(buffer);
697
970
  const text = buffer.toString('utf8');
698
971
  const lines = text.split(/\r\n|\r|\n/u);
699
- if (policy.start_line > lines.length || (policy.end_line !== null && policy.end_line > lines.length)) {
700
- const message = `Line range ${policy.start_line}${policy.end_line === null ? '' : `-${policy.end_line}`} is outside ${relativePath}, which has ${lines.length} lines.`;
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.`;
701
977
  issues.push(message);
702
978
  findings.push(makeSymbolReadFinding('code_symbol_read_invalid_range', 'high', relativePath, policy.start_line, policy.end_line, message));
703
979
  return createEmptySymbolReadReport(root, policy, findings, issues);
704
980
  }
705
981
  const symbols = extractSymbols(relativePath, language, contentSha256, text);
706
- const matchedSymbol = policy.end_line === null
707
- ? (symbols.find((symbol) => symbol.start_line === policy.start_line) ??
708
- symbols.find((symbol) => symbol.start_line <= policy.start_line && symbol.end_line >= policy.start_line) ??
709
- null)
710
- : null;
711
- const resolvedStartLine = matchedSymbol?.start_line ?? policy.start_line;
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;
712
991
  const resolvedEndLine = matchedSymbol?.end_line ?? policy.end_line;
713
992
  if (resolvedEndLine === null) {
714
- const message = `No outline symbol starts at or contains line ${policy.start_line} in ${relativePath}. Pass --end-line to read an explicit range.`;
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.`;
715
996
  issues.push(message);
716
- 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));
717
998
  }
718
- 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);
719
1000
  const contextEndLine = resolvedEndLine === null ? null : Math.min(lines.length, resolvedEndLine + policy.context_lines);
720
1001
  const target = {
721
1002
  path: relativePath,
@@ -723,12 +1004,14 @@ export function readCodeSymbol(projectRoot, options) {
723
1004
  sha256: contentSha256,
724
1005
  size_bytes: buffer.byteLength,
725
1006
  line_count: lines.length,
1007
+ requested_anchor_id: policy.anchor_id,
726
1008
  requested_start_line: policy.start_line,
727
1009
  requested_end_line: policy.end_line,
728
1010
  resolved_start_line: resolvedEndLine === null ? null : resolvedStartLine,
729
1011
  resolved_end_line: resolvedEndLine,
730
1012
  context_start_line: contextStartLine,
731
1013
  context_end_line: contextEndLine,
1014
+ anchor,
732
1015
  symbol: matchedSymbol,
733
1016
  };
734
1017
  let snippet = null;
@@ -769,3 +1052,101 @@ export function readCodeSymbol(projectRoot, options) {
769
1052
  issues,
770
1053
  };
771
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
+ }