mustflow 2.74.7 → 2.75.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.
@@ -0,0 +1,538 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
5
+ export const CODE_PACK_ID = 'code';
6
+ export const CODE_OUTLINE_SCRIPT_ID = 'outline';
7
+ export const CODE_OUTLINE_SCRIPT_REF = `${CODE_PACK_ID}/${CODE_OUTLINE_SCRIPT_ID}`;
8
+ export const CODE_SYMBOL_READ_SCRIPT_ID = 'symbol-read';
9
+ export const CODE_SYMBOL_READ_SCRIPT_REF = `${CODE_PACK_ID}/${CODE_SYMBOL_READ_SCRIPT_ID}`;
10
+ const DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
11
+ const DEFAULT_MAX_FILES = 200;
12
+ const DEFAULT_CONTEXT_LINES = 0;
13
+ const DEFAULT_MAX_SNIPPET_LINES = 250;
14
+ const CODE_FILE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
15
+ const IGNORED_DIRECTORIES = [
16
+ '.git',
17
+ '.mustflow/cache',
18
+ '.mustflow/state',
19
+ 'node_modules',
20
+ 'dist',
21
+ 'build',
22
+ 'coverage',
23
+ '.next',
24
+ '.turbo',
25
+ ];
26
+ const ERROR_OUTLINE_CODES = new Set([
27
+ 'code_outline_path_outside_root',
28
+ 'code_outline_unreadable_path',
29
+ 'code_outline_file_too_large',
30
+ 'code_outline_max_files_exceeded',
31
+ ]);
32
+ const ERROR_SYMBOL_READ_CODES = new Set([
33
+ 'code_symbol_read_path_outside_root',
34
+ 'code_symbol_read_unreadable_path',
35
+ 'code_symbol_read_invalid_range',
36
+ 'code_symbol_read_no_symbol_at_line',
37
+ 'code_symbol_read_snippet_too_large',
38
+ ]);
39
+ function toPosixPath(value) {
40
+ return value.replace(/\\/gu, '/');
41
+ }
42
+ function normalizeRelativePath(value) {
43
+ return toPosixPath(value).replace(/^\.\/+/u, '') || '.';
44
+ }
45
+ function sha256Tagged(buffer) {
46
+ return `sha256:${createHash('sha256').update(buffer).digest('hex')}`;
47
+ }
48
+ function languageForPath(filePath) {
49
+ switch (path.extname(filePath).toLowerCase()) {
50
+ case '.ts':
51
+ case '.mts':
52
+ case '.cts':
53
+ return 'typescript';
54
+ case '.tsx':
55
+ return 'tsx';
56
+ case '.js':
57
+ return 'javascript';
58
+ case '.jsx':
59
+ return 'jsx';
60
+ case '.mjs':
61
+ return 'javascript-module';
62
+ case '.cjs':
63
+ return 'javascript-commonjs';
64
+ default:
65
+ return null;
66
+ }
67
+ }
68
+ function isIgnoredDirectory(relativePath) {
69
+ const normalized = normalizeRelativePath(relativePath);
70
+ return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
71
+ }
72
+ function makeOutlineFinding(code, severity, pathValue, message) {
73
+ return { code, severity, path: pathValue, message };
74
+ }
75
+ function makeSymbolReadFinding(code, severity, pathValue, startLine, endLine, message) {
76
+ return { code, severity, path: pathValue, start_line: startLine, end_line: endLine, message };
77
+ }
78
+ function addMaxFilesFinding(findings, issues, policy, pathValue) {
79
+ if (findings.some((finding) => finding.code === 'code_outline_max_files_exceeded')) {
80
+ return;
81
+ }
82
+ const message = `Code outline matched more than ${policy.max_files} files; remaining files were skipped.`;
83
+ issues.push(`${pathValue}: ${message}`);
84
+ findings.push(makeOutlineFinding('code_outline_max_files_exceeded', 'high', pathValue, message));
85
+ }
86
+ function normalizeTargetPath(projectRoot, targetPath) {
87
+ const absolutePath = path.resolve(process.cwd(), targetPath);
88
+ ensureInside(projectRoot, absolutePath);
89
+ return {
90
+ absolutePath,
91
+ relativePath: normalizeRelativePath(path.relative(projectRoot, absolutePath)),
92
+ };
93
+ }
94
+ function collectCandidatesFromDirectory(projectRoot, absoluteDirectory, candidates, findings, issues, policy) {
95
+ const relativeDirectory = normalizeRelativePath(path.relative(projectRoot, absoluteDirectory));
96
+ if (isIgnoredDirectory(relativeDirectory)) {
97
+ return;
98
+ }
99
+ let entries;
100
+ try {
101
+ ensureInsideWithoutSymlinks(projectRoot, absoluteDirectory);
102
+ entries = readdirSync(absoluteDirectory, { withFileTypes: true });
103
+ }
104
+ catch (error) {
105
+ const message = error instanceof Error ? error.message : String(error);
106
+ issues.push(`${relativeDirectory}: ${message}`);
107
+ findings.push(makeOutlineFinding('code_outline_unreadable_path', 'high', relativeDirectory, message));
108
+ return;
109
+ }
110
+ for (const entry of entries) {
111
+ if (candidates.length >= policy.max_files) {
112
+ addMaxFilesFinding(findings, issues, policy, relativeDirectory);
113
+ return;
114
+ }
115
+ const absoluteEntry = path.join(absoluteDirectory, entry.name);
116
+ const relativeEntry = normalizeRelativePath(path.relative(projectRoot, absoluteEntry));
117
+ if (entry.isDirectory()) {
118
+ collectCandidatesFromDirectory(projectRoot, absoluteEntry, candidates, findings, issues, policy);
119
+ continue;
120
+ }
121
+ if (!entry.isFile()) {
122
+ continue;
123
+ }
124
+ const language = languageForPath(absoluteEntry);
125
+ if (language) {
126
+ candidates.push({ absolutePath: absoluteEntry, relativePath: relativeEntry, language });
127
+ }
128
+ }
129
+ }
130
+ function collectSourceCandidates(projectRoot, options, policy, findings, issues) {
131
+ const candidates = [];
132
+ for (const targetPath of options.paths) {
133
+ let absolutePath;
134
+ let relativePath;
135
+ try {
136
+ const normalized = normalizeTargetPath(projectRoot, targetPath);
137
+ absolutePath = normalized.absolutePath;
138
+ relativePath = normalized.relativePath;
139
+ ensureInsideWithoutSymlinks(projectRoot, absolutePath);
140
+ }
141
+ catch (error) {
142
+ const message = error instanceof Error ? error.message : String(error);
143
+ issues.push(message);
144
+ findings.push(makeOutlineFinding('code_outline_path_outside_root', 'high', targetPath, message));
145
+ continue;
146
+ }
147
+ let stats;
148
+ try {
149
+ stats = lstatSync(absolutePath);
150
+ }
151
+ catch (error) {
152
+ const message = error instanceof Error ? error.message : String(error);
153
+ issues.push(`${relativePath}: ${message}`);
154
+ findings.push(makeOutlineFinding('code_outline_unreadable_path', 'high', relativePath, message));
155
+ continue;
156
+ }
157
+ if (stats.isDirectory()) {
158
+ collectCandidatesFromDirectory(projectRoot, absolutePath, candidates, findings, issues, policy);
159
+ continue;
160
+ }
161
+ if (!stats.isFile()) {
162
+ findings.push(makeOutlineFinding('code_outline_unsupported_file', 'low', relativePath, `${relativePath} is not a regular source file.`));
163
+ continue;
164
+ }
165
+ const language = languageForPath(absolutePath);
166
+ if (!language) {
167
+ findings.push(makeOutlineFinding('code_outline_unsupported_file', 'low', relativePath, `${relativePath} is not a supported code file.`));
168
+ continue;
169
+ }
170
+ candidates.push({ absolutePath, relativePath, language });
171
+ }
172
+ if (candidates.length > policy.max_files) {
173
+ const overflow = candidates.length - policy.max_files;
174
+ findings.push(makeOutlineFinding('code_outline_max_files_exceeded', 'high', '.', `Code outline matched ${candidates.length} files; max_files is ${policy.max_files}. ${overflow} files were skipped.`));
175
+ }
176
+ return candidates.slice(0, policy.max_files);
177
+ }
178
+ function stripLineForBraceScan(line) {
179
+ return line
180
+ .replace(/\/\/.*$/u, '')
181
+ .replace(/"(?:\\.|[^"\\])*"/gu, '""')
182
+ .replace(/'(?:\\.|[^'\\])*'/gu, "''")
183
+ .replace(/`(?:\\.|[^`\\])*`/gu, '``');
184
+ }
185
+ function braceDelta(line) {
186
+ const stripped = stripLineForBraceScan(line);
187
+ let delta = 0;
188
+ for (const character of stripped) {
189
+ if (character === '{') {
190
+ delta += 1;
191
+ }
192
+ else if (character === '}') {
193
+ delta -= 1;
194
+ }
195
+ }
196
+ return delta;
197
+ }
198
+ function declarationFromLine(line) {
199
+ const trimmed = line.trim();
200
+ const functionMatch = /^(?<exported>export\s+(?:default\s+)?)?(?<async>async\s+)?function\s+(?<name>[$A-Z_a-z][$\w]*)\s*\(/u.exec(trimmed);
201
+ if (functionMatch?.groups) {
202
+ return {
203
+ kind: 'function',
204
+ name: functionMatch.groups.name ?? '<anonymous>',
205
+ exported: Boolean(functionMatch.groups.exported),
206
+ async: Boolean(functionMatch.groups.async),
207
+ };
208
+ }
209
+ const shapeMatch = /^(?<exported>export\s+)?(?<kind>class|interface|type|enum)\s+(?<name>[$A-Z_a-z][$\w]*)/u.exec(trimmed);
210
+ if (shapeMatch?.groups) {
211
+ return {
212
+ kind: shapeMatch.groups.kind,
213
+ name: shapeMatch.groups.name ?? '<anonymous>',
214
+ exported: Boolean(shapeMatch.groups.exported),
215
+ async: false,
216
+ };
217
+ }
218
+ const variableMatch = /^(?<exported>export\s+)?(?:const|let|var)\s+(?<name>[$A-Z_a-z][$\w]*)\s*[:=]/u.exec(trimmed);
219
+ if (variableMatch?.groups && (Boolean(variableMatch.groups.exported) || /=>|function\s*\(/u.test(trimmed))) {
220
+ return {
221
+ kind: /=>|function\s*\(/u.test(trimmed) ? 'function' : 'variable',
222
+ name: variableMatch.groups.name ?? '<anonymous>',
223
+ exported: Boolean(variableMatch.groups.exported),
224
+ async: /async\s*(?:\(|[A-Z_a-z_$])/u.test(trimmed),
225
+ };
226
+ }
227
+ return null;
228
+ }
229
+ function findDeclarationEndLine(lines, startIndex) {
230
+ let balance = 0;
231
+ let sawBrace = false;
232
+ for (let index = startIndex; index < lines.length; index += 1) {
233
+ const line = lines[index] ?? '';
234
+ const delta = braceDelta(line);
235
+ if (line.includes('{')) {
236
+ sawBrace = true;
237
+ }
238
+ balance += delta;
239
+ if (sawBrace && balance <= 0) {
240
+ return index + 1;
241
+ }
242
+ if (!sawBrace && /;\s*$/u.test(line.trim())) {
243
+ return index + 1;
244
+ }
245
+ }
246
+ return lines.length;
247
+ }
248
+ function buildSignature(lines, startLine, endLine) {
249
+ const startIndex = startLine - 1;
250
+ const endIndex = Math.min(endLine, startLine + 4);
251
+ const parts = [];
252
+ for (let index = startIndex; index < endIndex; index += 1) {
253
+ const trimmed = (lines[index] ?? '').trim();
254
+ if (trimmed.length > 0) {
255
+ parts.push(trimmed);
256
+ }
257
+ if (/[{;]\s*$/u.test(trimmed) || /=>/u.test(trimmed)) {
258
+ break;
259
+ }
260
+ }
261
+ const signature = parts.join(' ').replace(/\s+/gu, ' ');
262
+ return signature.length > 240 ? `${signature.slice(0, 237)}...` : signature;
263
+ }
264
+ function extractSymbols(relativePath, language, contentSha256, text) {
265
+ const lines = text.split(/\r\n|\r|\n/u);
266
+ const symbols = [];
267
+ for (const [index, line] of lines.entries()) {
268
+ const match = declarationFromLine(line);
269
+ if (!match) {
270
+ continue;
271
+ }
272
+ const startLine = index + 1;
273
+ const endLine = findDeclarationEndLine(lines, index);
274
+ symbols.push({
275
+ id: `${relativePath}:${startLine}:${match.kind}:${match.name}`,
276
+ path: relativePath,
277
+ language,
278
+ kind: match.kind,
279
+ name: match.name,
280
+ start_line: startLine,
281
+ end_line: endLine,
282
+ signature: buildSignature(lines, startLine, endLine),
283
+ exported: match.exported,
284
+ async: match.async,
285
+ parent: null,
286
+ content_sha256: contentSha256,
287
+ });
288
+ }
289
+ return symbols;
290
+ }
291
+ function createOutlineInputHash(policy, files, findings) {
292
+ const inputState = {
293
+ policy,
294
+ files: files.map((file) => ({
295
+ path: file.path,
296
+ sha256: file.sha256,
297
+ size_bytes: file.size_bytes,
298
+ symbol_count: file.symbol_count,
299
+ })),
300
+ input_errors: findings
301
+ .filter((finding) => ERROR_OUTLINE_CODES.has(finding.code))
302
+ .map((finding) => ({ code: finding.code, path: finding.path })),
303
+ };
304
+ return sha256Tagged(JSON.stringify(inputState));
305
+ }
306
+ function outlineStatus(findings) {
307
+ return findings.some((finding) => ERROR_OUTLINE_CODES.has(finding.code))
308
+ ? 'error'
309
+ : findings.length > 0
310
+ ? 'failed'
311
+ : 'passed';
312
+ }
313
+ export function inspectCodeOutline(projectRoot, options) {
314
+ const root = path.resolve(projectRoot);
315
+ const policy = {
316
+ max_file_bytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES,
317
+ max_files: options.maxFiles ?? DEFAULT_MAX_FILES,
318
+ extensions: [...CODE_FILE_EXTENSIONS],
319
+ ignored_directories: [...IGNORED_DIRECTORIES],
320
+ };
321
+ const files = [];
322
+ const symbols = [];
323
+ const findings = [];
324
+ const issues = [];
325
+ const candidates = collectSourceCandidates(root, options, policy, findings, issues);
326
+ for (const candidate of candidates) {
327
+ let buffer;
328
+ try {
329
+ buffer = readFileInsideWithoutSymlinks(root, candidate.absolutePath, { maxBytes: policy.max_file_bytes });
330
+ }
331
+ catch (error) {
332
+ const message = error instanceof Error ? error.message : String(error);
333
+ const code = message.includes('exceeds maximum size')
334
+ ? 'code_outline_file_too_large'
335
+ : 'code_outline_unreadable_path';
336
+ issues.push(`${candidate.relativePath}: ${message}`);
337
+ findings.push(makeOutlineFinding(code, 'high', candidate.relativePath, message));
338
+ continue;
339
+ }
340
+ const contentSha256 = sha256Tagged(buffer);
341
+ const text = buffer.toString('utf8');
342
+ const fileSymbols = extractSymbols(candidate.relativePath, candidate.language, contentSha256, text);
343
+ symbols.push(...fileSymbols);
344
+ files.push({
345
+ kind: 'source_file',
346
+ path: candidate.relativePath,
347
+ language: candidate.language,
348
+ sha256: contentSha256,
349
+ size_bytes: buffer.byteLength,
350
+ line_count: text.split(/\r\n|\r|\n/u).length,
351
+ symbol_count: fileSymbols.length,
352
+ });
353
+ }
354
+ const status = outlineStatus(findings);
355
+ return {
356
+ schema_version: '1',
357
+ command: 'script-pack',
358
+ pack_id: CODE_PACK_ID,
359
+ script_id: CODE_OUTLINE_SCRIPT_ID,
360
+ script_ref: CODE_OUTLINE_SCRIPT_REF,
361
+ action: 'scan',
362
+ status,
363
+ ok: status === 'passed',
364
+ mustflow_root: root,
365
+ policy,
366
+ input_hash: createOutlineInputHash(policy, files, findings),
367
+ files,
368
+ symbols: symbols.sort((left, right) => left.path.localeCompare(right.path) || left.start_line - right.start_line),
369
+ findings,
370
+ issues,
371
+ };
372
+ }
373
+ function createEmptySymbolReadReport(root, policy, findings, issues) {
374
+ const status = findings.some((finding) => ERROR_SYMBOL_READ_CODES.has(finding.code)) ? 'error' : 'passed';
375
+ return {
376
+ schema_version: '1',
377
+ command: 'script-pack',
378
+ pack_id: CODE_PACK_ID,
379
+ script_id: CODE_SYMBOL_READ_SCRIPT_ID,
380
+ script_ref: CODE_SYMBOL_READ_SCRIPT_REF,
381
+ action: 'read',
382
+ status,
383
+ ok: false,
384
+ mustflow_root: root,
385
+ policy,
386
+ input_hash: sha256Tagged(JSON.stringify({ policy, findings, issues })),
387
+ target: null,
388
+ snippet: null,
389
+ findings,
390
+ issues,
391
+ };
392
+ }
393
+ function createSymbolReadInputHash(policy, target, findings) {
394
+ const inputState = {
395
+ policy,
396
+ target: target === null
397
+ ? null
398
+ : {
399
+ path: target.path,
400
+ sha256: target.sha256,
401
+ requested_start_line: target.requested_start_line,
402
+ requested_end_line: target.requested_end_line,
403
+ context_start_line: target.context_start_line,
404
+ context_end_line: target.context_end_line,
405
+ },
406
+ input_errors: findings
407
+ .filter((finding) => ERROR_SYMBOL_READ_CODES.has(finding.code))
408
+ .map((finding) => ({
409
+ code: finding.code,
410
+ path: finding.path,
411
+ start_line: finding.start_line,
412
+ end_line: finding.end_line,
413
+ })),
414
+ };
415
+ return sha256Tagged(JSON.stringify(inputState));
416
+ }
417
+ export function readCodeSymbol(projectRoot, options) {
418
+ const root = path.resolve(projectRoot);
419
+ const policy = {
420
+ start_line: options.startLine,
421
+ end_line: options.endLine ?? null,
422
+ context_lines: options.contextLines ?? DEFAULT_CONTEXT_LINES,
423
+ max_file_bytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES,
424
+ max_snippet_lines: options.maxSnippetLines ?? DEFAULT_MAX_SNIPPET_LINES,
425
+ };
426
+ const findings = [];
427
+ const issues = [];
428
+ if (policy.start_line < 1 || (policy.end_line !== null && policy.end_line < policy.start_line) || policy.context_lines < 0) {
429
+ 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.'));
430
+ return createEmptySymbolReadReport(root, policy, findings, issues);
431
+ }
432
+ let absolutePath;
433
+ let relativePath;
434
+ try {
435
+ const normalized = normalizeTargetPath(root, options.path);
436
+ absolutePath = normalized.absolutePath;
437
+ relativePath = normalized.relativePath;
438
+ ensureInsideWithoutSymlinks(root, absolutePath);
439
+ }
440
+ catch (error) {
441
+ const message = error instanceof Error ? error.message : String(error);
442
+ issues.push(message);
443
+ findings.push(makeSymbolReadFinding('code_symbol_read_path_outside_root', 'high', options.path, policy.start_line, policy.end_line, message));
444
+ return createEmptySymbolReadReport(root, policy, findings, issues);
445
+ }
446
+ const language = languageForPath(absolutePath);
447
+ if (!language || !existsSync(absolutePath)) {
448
+ const message = `${relativePath} is not a supported existing code file.`;
449
+ issues.push(message);
450
+ findings.push(makeSymbolReadFinding('code_symbol_read_unreadable_path', 'high', relativePath, policy.start_line, policy.end_line, message));
451
+ return createEmptySymbolReadReport(root, policy, findings, issues);
452
+ }
453
+ let buffer;
454
+ try {
455
+ buffer = readFileInsideWithoutSymlinks(root, absolutePath, { maxBytes: policy.max_file_bytes });
456
+ }
457
+ catch (error) {
458
+ const message = error instanceof Error ? error.message : String(error);
459
+ issues.push(`${relativePath}: ${message}`);
460
+ findings.push(makeSymbolReadFinding('code_symbol_read_unreadable_path', 'high', relativePath, policy.start_line, policy.end_line, message));
461
+ return createEmptySymbolReadReport(root, policy, findings, issues);
462
+ }
463
+ const contentSha256 = sha256Tagged(buffer);
464
+ const text = buffer.toString('utf8');
465
+ const lines = text.split(/\r\n|\r|\n/u);
466
+ if (policy.start_line > lines.length || (policy.end_line !== null && policy.end_line > lines.length)) {
467
+ const message = `Line range ${policy.start_line}${policy.end_line === null ? '' : `-${policy.end_line}`} is outside ${relativePath}, which has ${lines.length} lines.`;
468
+ issues.push(message);
469
+ findings.push(makeSymbolReadFinding('code_symbol_read_invalid_range', 'high', relativePath, policy.start_line, policy.end_line, message));
470
+ return createEmptySymbolReadReport(root, policy, findings, issues);
471
+ }
472
+ const symbols = extractSymbols(relativePath, language, contentSha256, text);
473
+ const matchedSymbol = policy.end_line === null
474
+ ? (symbols.find((symbol) => symbol.start_line === policy.start_line) ??
475
+ symbols.find((symbol) => symbol.start_line <= policy.start_line && symbol.end_line >= policy.start_line) ??
476
+ null)
477
+ : null;
478
+ const resolvedStartLine = matchedSymbol?.start_line ?? policy.start_line;
479
+ const resolvedEndLine = matchedSymbol?.end_line ?? policy.end_line;
480
+ if (resolvedEndLine === null) {
481
+ const message = `No outline symbol starts at or contains line ${policy.start_line} in ${relativePath}. Pass --end-line to read an explicit range.`;
482
+ issues.push(message);
483
+ findings.push(makeSymbolReadFinding('code_symbol_read_no_symbol_at_line', 'high', relativePath, policy.start_line, null, message));
484
+ }
485
+ const contextStartLine = resolvedEndLine === null ? null : Math.max(1, resolvedStartLine - policy.context_lines);
486
+ const contextEndLine = resolvedEndLine === null ? null : Math.min(lines.length, resolvedEndLine + policy.context_lines);
487
+ const target = {
488
+ path: relativePath,
489
+ language,
490
+ sha256: contentSha256,
491
+ size_bytes: buffer.byteLength,
492
+ line_count: lines.length,
493
+ requested_start_line: policy.start_line,
494
+ requested_end_line: policy.end_line,
495
+ resolved_start_line: resolvedEndLine === null ? null : resolvedStartLine,
496
+ resolved_end_line: resolvedEndLine,
497
+ context_start_line: contextStartLine,
498
+ context_end_line: contextEndLine,
499
+ symbol: matchedSymbol,
500
+ };
501
+ let snippet = null;
502
+ if (contextStartLine !== null && contextEndLine !== null) {
503
+ const snippetLineCount = contextEndLine - contextStartLine + 1;
504
+ if (snippetLineCount > policy.max_snippet_lines) {
505
+ const message = `Snippet has ${snippetLineCount} lines; max_snippet_lines is ${policy.max_snippet_lines}.`;
506
+ issues.push(message);
507
+ findings.push(makeSymbolReadFinding('code_symbol_read_snippet_too_large', 'high', relativePath, contextStartLine, contextEndLine, message));
508
+ }
509
+ else {
510
+ snippet = {
511
+ path: relativePath,
512
+ start_line: contextStartLine,
513
+ end_line: contextEndLine,
514
+ text: lines.slice(contextStartLine - 1, contextEndLine).join('\n'),
515
+ };
516
+ }
517
+ }
518
+ const status = findings.some((finding) => ERROR_SYMBOL_READ_CODES.has(finding.code))
519
+ ? 'error'
520
+ : 'passed';
521
+ return {
522
+ schema_version: '1',
523
+ command: 'script-pack',
524
+ pack_id: CODE_PACK_ID,
525
+ script_id: CODE_SYMBOL_READ_SCRIPT_ID,
526
+ script_ref: CODE_SYMBOL_READ_SCRIPT_REF,
527
+ action: 'read',
528
+ status,
529
+ ok: status === 'passed',
530
+ mustflow_root: root,
531
+ policy,
532
+ input_hash: createSymbolReadInputHash(policy, target, findings),
533
+ target,
534
+ snippet,
535
+ findings,
536
+ issues,
537
+ };
538
+ }
@@ -232,6 +232,42 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
232
232
  '--json',
233
233
  ],
234
234
  },
235
+ {
236
+ id: 'code-outline-report',
237
+ schemaFile: 'code-outline-report.schema.json',
238
+ producer: 'mf script-pack run code/outline scan <path...> --json',
239
+ packaged: true,
240
+ documented: true,
241
+ installedCommand: [
242
+ 'mf',
243
+ 'script-pack',
244
+ 'run',
245
+ 'code/outline',
246
+ 'scan',
247
+ 'node_modules/mustflow/dist/cli/index.js',
248
+ '--json',
249
+ ],
250
+ },
251
+ {
252
+ id: 'code-symbol-read-report',
253
+ schemaFile: 'code-symbol-read-report.schema.json',
254
+ producer: 'mf script-pack run code/symbol-read read <path> --start-line <line> --json',
255
+ packaged: true,
256
+ documented: true,
257
+ installedCommand: [
258
+ 'mf',
259
+ 'script-pack',
260
+ 'run',
261
+ 'code/symbol-read',
262
+ 'read',
263
+ 'node_modules/mustflow/dist/cli/index.js',
264
+ '--start-line',
265
+ '1',
266
+ '--end-line',
267
+ '10',
268
+ '--json',
269
+ ],
270
+ },
235
271
  {
236
272
  id: 'text-budget-report',
237
273
  schemaFile: 'text-budget-report.schema.json',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.74.7",
3
+ "version": "2.75.0",
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
@@ -64,6 +64,14 @@ Current schemas:
64
64
  and associated report schemas
65
65
  - `script-pack-suggestion-report.schema.json`: output of `mf script-pack suggest --json`, containing
66
66
  path, skill, phase, and changed-file evidence used to recommend optional script-pack helpers
67
+ - `code-outline-report.schema.json`: output of
68
+ `mf script-pack run code/outline scan <path...> --json`, containing a bounded TypeScript and
69
+ JavaScript source outline with file hashes, symbol names, declaration kinds, line ranges,
70
+ signatures, export flags, and stable input-limit finding codes
71
+ - `code-symbol-read-report.schema.json`: output of
72
+ `mf script-pack run code/symbol-read read <path> --start-line <line> --json`, containing a
73
+ 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
67
75
  - `text-budget-report.schema.json`: output of
68
76
  `mf script-pack run core/text-budget check <path...> --json`, containing
69
77
  exact text-budget metrics, input content hashes, policy metadata, findings, and JSON Pointer field