mustflow 2.75.0 → 2.75.2

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.
@@ -14,15 +14,15 @@ export const SCRIPT_PACKS = [
14
14
  summaryKey: 'scriptPack.script.codeOutline.summary',
15
15
  actions: ['scan'],
16
16
  useWhen: [
17
- 'Scan TypeScript or JavaScript files for symbol headers before reading large source files chunk by chunk.',
18
- 'Build a bounded source outline with file paths, line ranges, signatures, export flags, and content hashes for codebase orientation.',
17
+ 'Scan TypeScript or JavaScript files for symbol headers and source anchors before reading large source files chunk by chunk.',
18
+ 'Build a bounded source outline with file paths, line ranges, signatures, export flags, source-anchor metadata, and content hashes for codebase orientation.',
19
19
  ],
20
20
  phases: ['before_change', 'during_change', 'review'],
21
21
  readOnly: true,
22
22
  mutates: false,
23
23
  network: false,
24
24
  inputs: ['path', 'max_files', 'max_file_bytes'],
25
- outputs: ['human_summary', 'json_report', 'symbol_outline'],
25
+ outputs: ['human_summary', 'json_report', 'symbol_outline', 'source_anchors'],
26
26
  relatedSkills: [
27
27
  'codebase-orientation',
28
28
  'javascript-code-change',
@@ -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 { 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}`;
@@ -11,6 +12,7 @@ const DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
11
12
  const DEFAULT_MAX_FILES = 200;
12
13
  const DEFAULT_CONTEXT_LINES = 0;
13
14
  const DEFAULT_MAX_SNIPPET_LINES = 250;
15
+ const RETURN_PREVIEW_MAX_CHARS = 120;
14
16
  const CODE_FILE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
15
17
  const IGNORED_DIRECTORIES = [
16
18
  '.git',
@@ -261,6 +263,169 @@ function buildSignature(lines, startLine, endLine) {
261
263
  const signature = parts.join(' ').replace(/\s+/gu, ' ');
262
264
  return signature.length > 240 ? `${signature.slice(0, 237)}...` : signature;
263
265
  }
266
+ function normalizeReturnType(value) {
267
+ return value.replace(/\s+/gu, ' ').replace(/[;{]\s*$/u, '').trim();
268
+ }
269
+ function extractExplicitReturnType(signature, match) {
270
+ if (match.kind !== 'function') {
271
+ return null;
272
+ }
273
+ const functionReturnMatch = /\bfunction(?:\s+[$A-Z_a-z][$\w]*)?\s*\([^)]*\)\s*:\s*(?<returnType>[^={;]+?)(?=\s*(?:\{|$))/u.exec(signature);
274
+ if (functionReturnMatch?.groups?.returnType) {
275
+ return normalizeReturnType(functionReturnMatch.groups.returnType);
276
+ }
277
+ const arrowReturnMatch = /\)\s*:\s*(?<returnType>[^=]+?)\s*=>/u.exec(signature);
278
+ if (arrowReturnMatch?.groups?.returnType) {
279
+ return normalizeReturnType(arrowReturnMatch.groups.returnType);
280
+ }
281
+ const arrowTypeAnnotationMatch = /:\s*(?:async\s*)?\([^)]*\)\s*=>\s*(?<returnType>[^=;]+?)\s*=/u.exec(signature);
282
+ if (arrowTypeAnnotationMatch?.groups?.returnType) {
283
+ return normalizeReturnType(arrowTypeAnnotationMatch.groups.returnType);
284
+ }
285
+ return null;
286
+ }
287
+ function truncateReturnPreview(value) {
288
+ const preview = value.replace(/\s+/gu, ' ').trim();
289
+ return preview.length > RETURN_PREVIEW_MAX_CHARS
290
+ ? `${preview.slice(0, RETURN_PREVIEW_MAX_CHARS - 3)}...`
291
+ : preview;
292
+ }
293
+ function extractArrowExpressionReturnPreview(signature) {
294
+ const arrowIndex = signature.lastIndexOf('=>');
295
+ if (arrowIndex < 0) {
296
+ return null;
297
+ }
298
+ const expression = signature.slice(arrowIndex + 2).trim().replace(/;\s*$/u, '');
299
+ if (expression.length === 0 || expression.startsWith('{')) {
300
+ return null;
301
+ }
302
+ return truncateReturnPreview(expression);
303
+ }
304
+ function isIdentifierCharacter(value) {
305
+ return typeof value === 'string' && /[$\w]/u.test(value);
306
+ }
307
+ function findReturnKeywordIndex(line) {
308
+ let quote = null;
309
+ let escaped = false;
310
+ for (let index = 0; index < line.length; index += 1) {
311
+ const character = line[index];
312
+ const nextCharacter = line[index + 1];
313
+ if (quote !== null) {
314
+ if (escaped) {
315
+ escaped = false;
316
+ continue;
317
+ }
318
+ if (character === '\\') {
319
+ escaped = true;
320
+ continue;
321
+ }
322
+ if (character === quote) {
323
+ quote = null;
324
+ }
325
+ continue;
326
+ }
327
+ if (character === '/' && nextCharacter === '/') {
328
+ return -1;
329
+ }
330
+ if (character === '"' || character === "'" || character === '`') {
331
+ quote = character;
332
+ continue;
333
+ }
334
+ if (line.startsWith('return', index) &&
335
+ !isIdentifierCharacter(line[index - 1]) &&
336
+ !isIdentifierCharacter(line[index + 'return'.length])) {
337
+ return index;
338
+ }
339
+ }
340
+ return -1;
341
+ }
342
+ function extractReturnExpressionPreview(line) {
343
+ const returnIndex = findReturnKeywordIndex(line);
344
+ if (returnIndex < 0) {
345
+ return null;
346
+ }
347
+ const expression = line
348
+ .slice(returnIndex + 'return'.length)
349
+ .replace(/\/\/.*$/u, '')
350
+ .trim()
351
+ .replace(/;\s*$/u, '')
352
+ .trim();
353
+ return expression.length === 0 ? null : truncateReturnPreview(expression);
354
+ }
355
+ function hasReturnStatement(line) {
356
+ return findReturnKeywordIndex(line) >= 0;
357
+ }
358
+ function simpleBodyThrowsOnly(lines, startLine, endLine, returnType) {
359
+ let sawThrow = false;
360
+ let sawOtherExecutableLine = false;
361
+ for (let index = startLine; index < endLine - 1; index += 1) {
362
+ const bodyLine = stripLineForBraceScan(lines[index] ?? '')
363
+ .replace(/[{};]/gu, '')
364
+ .trim();
365
+ if (bodyLine.length === 0) {
366
+ continue;
367
+ }
368
+ if (/^throw\b/u.test(bodyLine)) {
369
+ sawThrow = true;
370
+ continue;
371
+ }
372
+ sawOtherExecutableLine = true;
373
+ }
374
+ return sawThrow && (!sawOtherExecutableLine || returnType === 'never');
375
+ }
376
+ function extractReturnMetadata(lines, startLine, endLine, signature, match) {
377
+ const returnType = extractExplicitReturnType(signature, match);
378
+ if (match.kind !== 'function') {
379
+ return {
380
+ return_type: null,
381
+ return_behavior: 'unknown',
382
+ return_count: 0,
383
+ return_lines: [],
384
+ return_preview: null,
385
+ };
386
+ }
387
+ const returnLines = [];
388
+ const valueReturnPreviews = [];
389
+ let voidReturnCount = 0;
390
+ for (let index = startLine - 1; index < endLine; index += 1) {
391
+ const line = lines[index] ?? '';
392
+ if (!hasReturnStatement(line)) {
393
+ continue;
394
+ }
395
+ returnLines.push(index + 1);
396
+ const preview = extractReturnExpressionPreview(line);
397
+ if (preview === null) {
398
+ voidReturnCount += 1;
399
+ }
400
+ else {
401
+ valueReturnPreviews.push(preview);
402
+ }
403
+ }
404
+ const arrowExpressionPreview = returnLines.length === 0 ? extractArrowExpressionReturnPreview(signature) : null;
405
+ let returnBehavior;
406
+ if (valueReturnPreviews.length > 0 && voidReturnCount > 0) {
407
+ returnBehavior = 'mixed';
408
+ }
409
+ else if (valueReturnPreviews.length > 0 || arrowExpressionPreview !== null) {
410
+ returnBehavior = 'value';
411
+ }
412
+ else if (voidReturnCount > 0) {
413
+ returnBehavior = 'void';
414
+ }
415
+ else if (simpleBodyThrowsOnly(lines, startLine, endLine, returnType)) {
416
+ returnBehavior = 'throws_only';
417
+ }
418
+ else {
419
+ returnBehavior = 'implicit_undefined';
420
+ }
421
+ return {
422
+ return_type: returnType,
423
+ return_behavior: returnBehavior,
424
+ return_count: returnLines.length,
425
+ return_lines: returnLines,
426
+ return_preview: valueReturnPreviews[0] ?? arrowExpressionPreview,
427
+ };
428
+ }
264
429
  function extractSymbols(relativePath, language, contentSha256, text) {
265
430
  const lines = text.split(/\r\n|\r|\n/u);
266
431
  const symbols = [];
@@ -271,6 +436,8 @@ function extractSymbols(relativePath, language, contentSha256, text) {
271
436
  }
272
437
  const startLine = index + 1;
273
438
  const endLine = findDeclarationEndLine(lines, index);
439
+ const signature = buildSignature(lines, startLine, endLine);
440
+ const returnMetadata = extractReturnMetadata(lines, startLine, endLine, signature, match);
274
441
  symbols.push({
275
442
  id: `${relativePath}:${startLine}:${match.kind}:${match.name}`,
276
443
  path: relativePath,
@@ -279,16 +446,70 @@ function extractSymbols(relativePath, language, contentSha256, text) {
279
446
  name: match.name,
280
447
  start_line: startLine,
281
448
  end_line: endLine,
282
- signature: buildSignature(lines, startLine, endLine),
449
+ signature,
283
450
  exported: match.exported,
284
451
  async: match.async,
452
+ ...returnMetadata,
285
453
  parent: null,
286
454
  content_sha256: contentSha256,
287
455
  });
288
456
  }
289
457
  return symbols;
290
458
  }
291
- function createOutlineInputHash(policy, files, findings) {
459
+ function sourceAnchorEndLine(lineStart, rawText) {
460
+ return lineStart + rawText.split(/\r\n|\r|\n/u).length - 1;
461
+ }
462
+ function canBridgeAnchorToSymbol(lines, anchorEndLine, symbolStartLine) {
463
+ if (symbolStartLine <= anchorEndLine) {
464
+ return false;
465
+ }
466
+ for (let lineNumber = anchorEndLine + 1; lineNumber < symbolStartLine; lineNumber += 1) {
467
+ const trimmed = (lines[lineNumber - 1] ?? '').trim();
468
+ if (trimmed.length === 0 || trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('@')) {
469
+ continue;
470
+ }
471
+ return false;
472
+ }
473
+ return true;
474
+ }
475
+ function findAnchorTargetSymbol(relativePath, lines, anchorEndLine, symbols) {
476
+ const followingSymbols = symbols
477
+ .filter((symbol) => symbol.path === relativePath && symbol.start_line > anchorEndLine)
478
+ .sort((left, right) => left.start_line - right.start_line);
479
+ const target = followingSymbols[0] ?? null;
480
+ if (!target || !canBridgeAnchorToSymbol(lines, anchorEndLine, target.start_line)) {
481
+ return null;
482
+ }
483
+ return target;
484
+ }
485
+ function extractAnchors(relativePath, lines, text, symbols) {
486
+ const anchors = [];
487
+ for (const anchor of parseSourceAnchorsInContent(relativePath, text)) {
488
+ if (!anchor.idValid || sourceAnchorTextContainsSecretLike(anchor.rawText)) {
489
+ continue;
490
+ }
491
+ const lineEnd = sourceAnchorEndLine(anchor.lineStart, anchor.rawText);
492
+ const target = findAnchorTargetSymbol(relativePath, lines, lineEnd, symbols);
493
+ anchors.push({
494
+ id: anchor.rawId,
495
+ path: relativePath,
496
+ line_start: anchor.lineStart,
497
+ line_end: lineEnd,
498
+ purpose: anchor.fields.get('purpose') ?? null,
499
+ search: splitSourceAnchorList(anchor.fields.get('search')),
500
+ invariant: anchor.fields.get('invariant') ?? null,
501
+ risk: splitSourceAnchorList(anchor.fields.get('risk')),
502
+ navigation_only: true,
503
+ can_instruct_agent: false,
504
+ target_symbol_id: target?.id ?? null,
505
+ target_kind: target?.kind ?? null,
506
+ target_name: target?.name ?? null,
507
+ target_start_line: target?.start_line ?? null,
508
+ });
509
+ }
510
+ return anchors;
511
+ }
512
+ function createOutlineInputHash(policy, files, anchors, findings) {
292
513
  const inputState = {
293
514
  policy,
294
515
  files: files.map((file) => ({
@@ -297,6 +518,13 @@ function createOutlineInputHash(policy, files, findings) {
297
518
  size_bytes: file.size_bytes,
298
519
  symbol_count: file.symbol_count,
299
520
  })),
521
+ anchors: anchors.map((anchor) => ({
522
+ id: anchor.id,
523
+ path: anchor.path,
524
+ line_start: anchor.line_start,
525
+ line_end: anchor.line_end,
526
+ target_symbol_id: anchor.target_symbol_id,
527
+ })),
300
528
  input_errors: findings
301
529
  .filter((finding) => ERROR_OUTLINE_CODES.has(finding.code))
302
530
  .map((finding) => ({ code: finding.code, path: finding.path })),
@@ -319,6 +547,7 @@ export function inspectCodeOutline(projectRoot, options) {
319
547
  ignored_directories: [...IGNORED_DIRECTORIES],
320
548
  };
321
549
  const files = [];
550
+ const anchors = [];
322
551
  const symbols = [];
323
552
  const findings = [];
324
553
  const issues = [];
@@ -339,15 +568,18 @@ export function inspectCodeOutline(projectRoot, options) {
339
568
  }
340
569
  const contentSha256 = sha256Tagged(buffer);
341
570
  const text = buffer.toString('utf8');
571
+ const lines = text.split(/\r\n|\r|\n/u);
342
572
  const fileSymbols = extractSymbols(candidate.relativePath, candidate.language, contentSha256, text);
573
+ const fileAnchors = extractAnchors(candidate.relativePath, lines, text, fileSymbols);
343
574
  symbols.push(...fileSymbols);
575
+ anchors.push(...fileAnchors);
344
576
  files.push({
345
577
  kind: 'source_file',
346
578
  path: candidate.relativePath,
347
579
  language: candidate.language,
348
580
  sha256: contentSha256,
349
581
  size_bytes: buffer.byteLength,
350
- line_count: text.split(/\r\n|\r|\n/u).length,
582
+ line_count: lines.length,
351
583
  symbol_count: fileSymbols.length,
352
584
  });
353
585
  }
@@ -363,8 +595,9 @@ export function inspectCodeOutline(projectRoot, options) {
363
595
  ok: status === 'passed',
364
596
  mustflow_root: root,
365
597
  policy,
366
- input_hash: createOutlineInputHash(policy, files, findings),
598
+ input_hash: createOutlineInputHash(policy, files, anchors, findings),
367
599
  files,
600
+ anchors: anchors.sort((left, right) => left.path.localeCompare(right.path) || left.line_start - right.line_start),
368
601
  symbols: symbols.sort((left, right) => left.path.localeCompare(right.path) || left.start_line - right.start_line),
369
602
  findings,
370
603
  issues,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.75.0",
3
+ "version": "2.75.2",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
package/schemas/README.md CHANGED
@@ -67,11 +67,12 @@ Current schemas:
67
67
  - `code-outline-report.schema.json`: output of
68
68
  `mf script-pack run code/outline scan <path...> --json`, containing a bounded TypeScript and
69
69
  JavaScript source outline with file hashes, symbol names, declaration kinds, line ranges,
70
- signatures, export flags, and stable input-limit finding codes
70
+ signatures, export flags, source-anchor navigation metadata, static return metadata, and stable
71
+ input-limit finding codes
71
72
  - `code-symbol-read-report.schema.json`: output of
72
73
  `mf script-pack run code/symbol-read read <path> --start-line <line> --json`, containing a
73
74
  focused source snippet selected by outline symbol line or explicit line range with file hash,
74
- resolved line metadata, optional symbol metadata, and stable range/read finding codes
75
+ resolved line metadata, optional symbol and static return metadata, and stable range/read finding codes
75
76
  - `text-budget-report.schema.json`: output of
76
77
  `mf script-pack run core/text-budget check <path...> --json`, containing
77
78
  exact text-budget metrics, input content hashes, policy metadata, findings, and JSON Pointer field
@@ -17,6 +17,7 @@
17
17
  "policy",
18
18
  "input_hash",
19
19
  "files",
20
+ "anchors",
20
21
  "symbols",
21
22
  "findings",
22
23
  "issues"
@@ -37,6 +38,10 @@
37
38
  "type": "array",
38
39
  "items": { "$ref": "#/$defs/file" }
39
40
  },
41
+ "anchors": {
42
+ "type": "array",
43
+ "items": { "$ref": "#/$defs/anchor" }
44
+ },
40
45
  "symbols": {
41
46
  "type": "array",
42
47
  "items": { "$ref": "#/$defs/symbol" }
@@ -90,6 +95,47 @@
90
95
  "symbol_count": { "type": "integer", "minimum": 0 }
91
96
  }
92
97
  },
98
+ "anchor": {
99
+ "type": "object",
100
+ "additionalProperties": false,
101
+ "required": [
102
+ "id",
103
+ "path",
104
+ "line_start",
105
+ "line_end",
106
+ "purpose",
107
+ "search",
108
+ "invariant",
109
+ "risk",
110
+ "navigation_only",
111
+ "can_instruct_agent",
112
+ "target_symbol_id",
113
+ "target_kind",
114
+ "target_name",
115
+ "target_start_line"
116
+ ],
117
+ "properties": {
118
+ "id": { "type": "string" },
119
+ "path": { "type": "string" },
120
+ "line_start": { "type": "integer", "minimum": 1 },
121
+ "line_end": { "type": "integer", "minimum": 1 },
122
+ "purpose": { "type": ["string", "null"] },
123
+ "search": { "$ref": "#/$defs/stringArray" },
124
+ "invariant": { "type": ["string", "null"] },
125
+ "risk": { "$ref": "#/$defs/stringArray" },
126
+ "navigation_only": { "const": true },
127
+ "can_instruct_agent": { "const": false },
128
+ "target_symbol_id": { "type": ["string", "null"] },
129
+ "target_kind": {
130
+ "anyOf": [
131
+ { "$ref": "#/$defs/symbolKind" },
132
+ { "type": "null" }
133
+ ]
134
+ },
135
+ "target_name": { "type": ["string", "null"] },
136
+ "target_start_line": { "type": ["integer", "null"], "minimum": 1 }
137
+ }
138
+ },
93
139
  "symbol": {
94
140
  "type": "object",
95
141
  "additionalProperties": false,
@@ -104,6 +150,11 @@
104
150
  "signature",
105
151
  "exported",
106
152
  "async",
153
+ "return_type",
154
+ "return_behavior",
155
+ "return_count",
156
+ "return_lines",
157
+ "return_preview",
107
158
  "parent",
108
159
  "content_sha256"
109
160
  ],
@@ -118,6 +169,16 @@
118
169
  "signature": { "type": "string" },
119
170
  "exported": { "type": "boolean" },
120
171
  "async": { "type": "boolean" },
172
+ "return_type": { "type": ["string", "null"] },
173
+ "return_behavior": {
174
+ "enum": ["value", "void", "implicit_undefined", "mixed", "throws_only", "unknown"]
175
+ },
176
+ "return_count": { "type": "integer", "minimum": 0 },
177
+ "return_lines": {
178
+ "type": "array",
179
+ "items": { "type": "integer", "minimum": 1 }
180
+ },
181
+ "return_preview": { "type": ["string", "null"] },
121
182
  "parent": { "type": ["string", "null"] },
122
183
  "content_sha256": { "$ref": "#/$defs/sha256" }
123
184
  }
@@ -91,6 +91,11 @@
91
91
  "signature",
92
92
  "exported",
93
93
  "async",
94
+ "return_type",
95
+ "return_behavior",
96
+ "return_count",
97
+ "return_lines",
98
+ "return_preview",
94
99
  "parent",
95
100
  "content_sha256"
96
101
  ],
@@ -105,6 +110,16 @@
105
110
  "signature": { "type": "string" },
106
111
  "exported": { "type": "boolean" },
107
112
  "async": { "type": "boolean" },
113
+ "return_type": { "type": ["string", "null"] },
114
+ "return_behavior": {
115
+ "enum": ["value", "void", "implicit_undefined", "mixed", "throws_only", "unknown"]
116
+ },
117
+ "return_count": { "type": "integer", "minimum": 0 },
118
+ "return_lines": {
119
+ "type": "array",
120
+ "items": { "type": "integer", "minimum": 1 }
121
+ },
122
+ "return_preview": { "type": ["string", "null"] },
108
123
  "parent": { "type": ["string", "null"] },
109
124
  "content_sha256": { "$ref": "#/$defs/sha256" }
110
125
  }
@@ -1,6 +1,6 @@
1
1
  id = "default"
2
2
  name = "default"
3
- version = "2.75.0"
3
+ version = "2.75.2"
4
4
  description = "Minimal workflow for LLM agents to read, edit, and verify their work in a repository."
5
5
  common_root = "common"
6
6
  locales_root = "locales"