mustflow 2.18.0 → 2.18.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.
@@ -1,10 +1,11 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { createClassifyOutput } from './classify.js';
5
5
  import { runRun } from './run.js';
6
6
  import { createChangeVerificationReport, } from '../../core/change-verification.js';
7
7
  import { createVerifyCompletionVerdict, } from '../../core/completion-verdict.js';
8
+ import { atomicWriteJsonFile, createStateRunId } from '../../core/atomic-state-write.js';
8
9
  import { createExternalEvidenceRisks, } from '../../core/external-evidence.js';
9
10
  import { createRepeatedFailureRisks, createVerificationFailureFingerprint, updateRepeatedFailureState, } from '../../core/repeated-failure.js';
10
11
  import { countReproEvidenceVerdictEffects, createReproEvidenceRisks, } from '../../core/repro-evidence.js';
@@ -17,9 +18,9 @@ import { t } from '../lib/i18n.js';
17
18
  import { readLocalCommandEffectGraph, readLocalPathSurfaces, readLocalSourceAnchorVerdictRisks, } from '../lib/local-index.js';
18
19
  import { resolveMustflowRoot } from '../lib/project-root.js';
19
20
  const VERIFY_SCHEMA_VERSION = '1';
20
- const VERIFY_RUN_DIR = path.join('.mustflow', 'state', 'runs', 'verify-latest');
21
- const VERIFY_MANIFEST_PATH = path.join(VERIFY_RUN_DIR, 'manifest.json');
22
- const LATEST_RUN_RECEIPT_PATH = path.join('.mustflow', 'state', 'runs', 'latest.json');
21
+ const RUN_STATE_DIR = path.join('.mustflow', 'state', 'runs');
22
+ const LATEST_RUN_RECEIPT_PATH = path.join(RUN_STATE_DIR, 'latest.json');
23
+ const DEFAULT_VERIFY_PARALLELISM = 1;
23
24
  function createBufferedOutput() {
24
25
  const stdout = [];
25
26
  const stderr = [];
@@ -52,6 +53,7 @@ export function getVerifyHelp(lang = 'en') {
52
53
  { label: '--write-plan <path>', description: t(lang, 'verify.help.option.writePlan') },
53
54
  { label: '--repro-evidence <path>', description: t(lang, 'verify.help.option.reproEvidence') },
54
55
  { label: '--external-evidence <path>', description: t(lang, 'verify.help.option.externalEvidence') },
56
+ { label: '--parallel <count>', description: t(lang, 'verify.help.option.parallel') },
55
57
  { label: '--plan-only', description: t(lang, 'verify.help.option.planOnly') },
56
58
  { label: '--json', description: t(lang, 'cli.option.json') },
57
59
  { label: '-h, --help', description: t(lang, 'cli.option.help') },
@@ -81,6 +83,7 @@ function parseVerifyArgs(args) {
81
83
  let json = false;
82
84
  let planOnly = false;
83
85
  let changed = false;
86
+ let parallelism = DEFAULT_VERIFY_PARALLELISM;
84
87
  for (let index = 0; index < args.length; index += 1) {
85
88
  const arg = args[index];
86
89
  if (arg === '--json') {
@@ -95,6 +98,19 @@ function parseVerifyArgs(args) {
95
98
  changed = true;
96
99
  continue;
97
100
  }
101
+ if (arg === '--parallel') {
102
+ const value = args[index + 1];
103
+ if (!value || value.startsWith('-')) {
104
+ return { json, planOnly, changed, reason, parallelism, error: 'missing_parallel_value' };
105
+ }
106
+ const parsedParallelism = parseVerifyParallelism(value);
107
+ if (parsedParallelism === null) {
108
+ return { json, planOnly, changed, reason, parallelism, error: 'invalid_parallel_value' };
109
+ }
110
+ parallelism = parsedParallelism;
111
+ index += 1;
112
+ continue;
113
+ }
98
114
  if (arg === '--reason') {
99
115
  const value = args[index + 1];
100
116
  if (!value || value.startsWith('-')) {
@@ -195,6 +211,15 @@ function parseVerifyArgs(args) {
195
211
  reason = value;
196
212
  continue;
197
213
  }
214
+ if (arg.startsWith('--parallel=')) {
215
+ const value = arg.slice('--parallel='.length);
216
+ const parsedParallelism = parseVerifyParallelism(value);
217
+ if (parsedParallelism === null) {
218
+ return { json, planOnly, changed, reason, parallelism, error: 'invalid_parallel_value' };
219
+ }
220
+ parallelism = parsedParallelism;
221
+ continue;
222
+ }
198
223
  if (arg.startsWith('--from-plan=')) {
199
224
  const value = arg.slice('--from-plan='.length);
200
225
  if (value.length === 0) {
@@ -289,7 +314,25 @@ function parseVerifyArgs(args) {
289
314
  error: `unexpected:${arg}`,
290
315
  };
291
316
  }
292
- return { json, planOnly, changed, reason, fromClassification, fromPlan, writePlan, reproEvidence, externalEvidence };
317
+ return {
318
+ json,
319
+ planOnly,
320
+ changed,
321
+ reason,
322
+ fromClassification,
323
+ fromPlan,
324
+ writePlan,
325
+ reproEvidence,
326
+ externalEvidence,
327
+ parallelism,
328
+ };
329
+ }
330
+ function parseVerifyParallelism(value) {
331
+ if (!/^[1-9][0-9]*$/u.test(value)) {
332
+ return null;
333
+ }
334
+ const parsed = Number(value);
335
+ return Number.isSafeInteger(parsed) ? parsed : null;
293
336
  }
294
337
  function uniqueStrings(values) {
295
338
  return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))];
@@ -297,6 +340,18 @@ function uniqueStrings(values) {
297
340
  function toPosixPath(value) {
298
341
  return value.split(path.sep).join('/');
299
342
  }
343
+ function createVerifyRunStatePaths(projectRoot) {
344
+ const runDir = toPosixPath(path.join(RUN_STATE_DIR, createStateRunId('verify')));
345
+ const manifestPath = toPosixPath(path.join(runDir, 'manifest.json'));
346
+ const absoluteRunDir = path.join(projectRoot, runDir);
347
+ return {
348
+ runDir,
349
+ manifestPath,
350
+ absoluteRunDir,
351
+ absoluteIntentDir: path.join(absoluteRunDir, 'intents'),
352
+ absoluteManifestPath: path.join(projectRoot, manifestPath),
353
+ };
354
+ }
300
355
  function sanitizeIntentFilePart(value) {
301
356
  const sanitized = value.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');
302
357
  return sanitized.length > 0 ? sanitized.slice(0, 80) : 'intent';
@@ -445,7 +500,7 @@ function resolvePlanPath(projectRoot, inputPath) {
445
500
  }
446
501
  return resolved;
447
502
  }
448
- export function readInputFromPlan(projectRoot, inputPath) {
503
+ export function readInputFromClassificationReport(projectRoot, inputPath) {
449
504
  let parsed;
450
505
  const planPath = resolvePlanPath(projectRoot, inputPath);
451
506
  try {
@@ -836,7 +891,8 @@ async function runVerificationIntent(intent, lang, verificationPlanId, testTarge
836
891
  if (receiptStatus === 'passed' ||
837
892
  receiptStatus === 'failed' ||
838
893
  receiptStatus === 'timed_out' ||
839
- receiptStatus === 'start_failed') {
894
+ receiptStatus === 'start_failed' ||
895
+ receiptStatus === 'output_limit_exceeded') {
840
896
  status = receiptStatus;
841
897
  }
842
898
  }
@@ -856,12 +912,52 @@ async function runVerificationIntent(intent, lang, verificationPlanId, testTarge
856
912
  receipt,
857
913
  };
858
914
  }
915
+ function entriesForScheduleBatch(entries, batch) {
916
+ const batchIntents = new Set(batch.intents);
917
+ return entries.filter((entry) => batchIntents.has(entry.intent));
918
+ }
919
+ async function runVerificationEntriesSequentially(entries, lang, verificationPlanId, scheduledTestTargets) {
920
+ const results = [];
921
+ for (const entry of entries) {
922
+ results.push(await runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? []));
923
+ }
924
+ return results;
925
+ }
926
+ async function runVerificationEntriesInParallelChunks(entries, parallelism, lang, verificationPlanId, scheduledTestTargets) {
927
+ const results = [];
928
+ for (let index = 0; index < entries.length; index += parallelism) {
929
+ const chunk = entries.slice(index, index + parallelism);
930
+ results.push(...(await Promise.all(chunk.map((entry) => runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? [])))));
931
+ }
932
+ return results;
933
+ }
934
+ async function runScheduledVerificationIntents(report, lang, verificationPlanId, scheduledTestTargets, parallelism) {
935
+ if (parallelism <= DEFAULT_VERIFY_PARALLELISM) {
936
+ return runVerificationEntriesSequentially(report.schedule.entries, lang, verificationPlanId, scheduledTestTargets);
937
+ }
938
+ const results = [];
939
+ for (const batch of report.schedule.batches) {
940
+ const entries = entriesForScheduleBatch(report.schedule.entries, batch);
941
+ if (entries.length === 0) {
942
+ continue;
943
+ }
944
+ if (entries.length > 1 && entries.every((entry) => entry.parallelEligible)) {
945
+ results.push(...(await runVerificationEntriesInParallelChunks(entries, parallelism, lang, verificationPlanId, scheduledTestTargets)));
946
+ continue;
947
+ }
948
+ results.push(...(await runVerificationEntriesSequentially(entries, lang, verificationPlanId, scheduledTestTargets)));
949
+ }
950
+ return results;
951
+ }
859
952
  function summarizeResults(results) {
860
953
  const ran = results.filter((result) => !result.skipped).length;
861
954
  const passed = results.filter((result) => result.status === 'passed').length;
862
955
  const skipped = results.filter((result) => result.skipped).length;
863
956
  const failed = results.filter((result) => !result.skipped &&
864
- (result.status === 'failed' || result.status === 'timed_out' || result.status === 'start_failed')).length;
957
+ (result.status === 'failed' ||
958
+ result.status === 'timed_out' ||
959
+ result.status === 'start_failed' ||
960
+ result.status === 'output_limit_exceeded')).length;
865
961
  return {
866
962
  matched: results.filter((result) => result.intent !== null).length,
867
963
  ran,
@@ -910,11 +1006,15 @@ function timedOutForResult(result) {
910
1006
  return result.status === 'timed_out' || resultSummary?.timed_out === true;
911
1007
  }
912
1008
  function errorKindForResult(result) {
913
- return stringField(resultSummaryForResult(result)?.error_kind) ?? (result.status === 'start_failed' ? 'start_failed' : null);
1009
+ return (stringField(resultSummaryForResult(result)?.error_kind) ??
1010
+ (result.status === 'start_failed' || result.status === 'output_limit_exceeded' ? result.status : null));
914
1011
  }
915
1012
  function failedResults(results) {
916
1013
  return results.filter((result) => !result.skipped &&
917
- (result.status === 'failed' || result.status === 'timed_out' || result.status === 'start_failed'));
1014
+ (result.status === 'failed' ||
1015
+ result.status === 'timed_out' ||
1016
+ result.status === 'start_failed' ||
1017
+ result.status === 'output_limit_exceeded'));
918
1018
  }
919
1019
  function createFailureFingerprintForVerify(input) {
920
1020
  const failures = failedResults(input.results);
@@ -1026,7 +1126,10 @@ function createCriteriaEvidence(report, results) {
1026
1126
  .filter((intent) => intent !== null);
1027
1127
  const gapCount = report.gaps.filter((gap) => gap.reason === requirement.reason).length;
1028
1128
  const selectedResults = selectedIntents.map((intent) => resultForSelectedIntent(results, intent));
1029
- if (selectedResults.some((result) => result?.status === 'failed' || result?.status === 'timed_out' || result?.status === 'start_failed')) {
1129
+ if (selectedResults.some((result) => result?.status === 'failed' ||
1130
+ result?.status === 'timed_out' ||
1131
+ result?.status === 'start_failed' ||
1132
+ result?.status === 'output_limit_exceeded')) {
1030
1133
  return { ...current, contradicted: current.contradicted + 1 };
1031
1134
  }
1032
1135
  if (gapCount > 0 || (selectedIntents.length === 0 && skippedIntents.length > 0)) {
@@ -1181,20 +1284,18 @@ function createVerificationPlanId(report, contract) {
1181
1284
  return hashTextSha256(stableJson(fingerprintSource));
1182
1285
  }
1183
1286
  function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks, scopeDiffRisks, validationRatchetRisks, reproEvidence, externalChecks) {
1184
- const runDir = path.join(projectRoot, VERIFY_RUN_DIR);
1185
- const intentDir = path.join(runDir, 'intents');
1287
+ const statePaths = createVerifyRunStatePaths(projectRoot);
1186
1288
  const receipts = [];
1187
1289
  const results = [];
1188
- rmSync(runDir, { recursive: true, force: true });
1189
- mkdirSync(intentDir, { recursive: true });
1290
+ mkdirSync(statePaths.absoluteIntentDir, { recursive: true });
1190
1291
  for (const [index, result] of output.results.entries()) {
1191
1292
  let receiptPath = null;
1192
1293
  let receiptSha256 = null;
1193
1294
  let receipt = result.receipt;
1194
1295
  if (result.intent && result.receipt) {
1195
1296
  const fileName = `${String(index + 1).padStart(3, '0')}-${sanitizeIntentFilePart(result.intent)}.json`;
1196
- const absoluteReceiptPath = path.join(intentDir, fileName);
1197
- receiptPath = toPosixPath(path.join(VERIFY_RUN_DIR, 'intents', fileName));
1297
+ const absoluteReceiptPath = path.join(statePaths.absoluteIntentDir, fileName);
1298
+ receiptPath = toPosixPath(path.join(statePaths.runDir, 'intents', fileName));
1198
1299
  receipt = {
1199
1300
  ...result.receipt,
1200
1301
  verification_plan_id: output.verification_plan_id,
@@ -1202,7 +1303,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1202
1303
  };
1203
1304
  const receiptContent = `${JSON.stringify(receipt, null, 2)}\n`;
1204
1305
  receiptSha256 = hashTextSha256(receiptContent);
1205
- writeFileSync(absoluteReceiptPath, receiptContent, 'utf8');
1306
+ atomicWriteJsonFile(absoluteReceiptPath, receipt);
1206
1307
  }
1207
1308
  receipts.push({
1208
1309
  intent: result.intent,
@@ -1272,6 +1373,8 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1272
1373
  completion_verdict: completionVerdict,
1273
1374
  failure_fingerprint: failureFingerprint,
1274
1375
  repeated_failure_summary: repeatedFailureSummary,
1376
+ run_dir: statePaths.runDir,
1377
+ manifest_path: statePaths.manifestPath,
1275
1378
  results,
1276
1379
  evidence_model: createVerifyEvidenceModel({
1277
1380
  report,
@@ -1306,7 +1409,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1306
1409
  ...(outputWithReceiptPaths.external_checks ? { external_checks: outputWithReceiptPaths.external_checks } : {}),
1307
1410
  receipts,
1308
1411
  };
1309
- writeFileSync(path.join(runDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
1412
+ atomicWriteJsonFile(statePaths.absoluteManifestPath, manifest);
1310
1413
  const latest = {
1311
1414
  schema_version: '1',
1312
1415
  command: 'verify',
@@ -1324,13 +1427,13 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1324
1427
  summary: outputWithReceiptPaths.summary,
1325
1428
  ...(outputWithReceiptPaths.repro_evidence ? { repro_evidence: outputWithReceiptPaths.repro_evidence } : {}),
1326
1429
  ...(outputWithReceiptPaths.external_checks ? { external_checks: outputWithReceiptPaths.external_checks } : {}),
1327
- run_dir: toPosixPath(VERIFY_RUN_DIR),
1328
- manifest_path: toPosixPath(VERIFY_MANIFEST_PATH),
1430
+ run_dir: statePaths.runDir,
1431
+ manifest_path: statePaths.manifestPath,
1329
1432
  };
1330
- writeFileSync(path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), `${JSON.stringify(latest, null, 2)}\n`, 'utf8');
1433
+ atomicWriteJsonFile(path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), latest);
1331
1434
  return outputWithReceiptPaths;
1332
1435
  }
1333
- async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvidence = null, externalChecks = []) {
1436
+ async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvidence = null, externalChecks = [], parallelism = DEFAULT_VERIFY_PARALLELISM) {
1334
1437
  const contract = readCommandContract(projectRoot);
1335
1438
  const report = createChangeVerificationReport(input.classificationReport, contract, projectRoot);
1336
1439
  const verificationPlanId = createVerificationPlanId(report, contract);
@@ -1343,10 +1446,7 @@ async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvi
1343
1446
  const reproEvidenceRisks = createReproEvidenceRisks(reproEvidence, { verificationPlanId });
1344
1447
  const reproEvidenceVerdictEffects = countReproEvidenceVerdictEffects(reproEvidenceRisks);
1345
1448
  const externalEvidenceRisks = createExternalEvidenceRisks(externalChecks);
1346
- const results = [];
1347
- for (const entry of report.schedule.entries) {
1348
- results.push(await runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? []));
1349
- }
1449
+ const results = await runScheduledVerificationIntents(report, lang, verificationPlanId, scheduledTestTargets, parallelism);
1350
1450
  results.push(...createSkippedResults(report.candidates, scheduledIntents, report.gaps));
1351
1451
  const summary = summarizeResults(results);
1352
1452
  const status = getVerificationStatus(summary);
@@ -1416,8 +1516,8 @@ async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvi
1416
1516
  summary,
1417
1517
  ...(reproEvidence ? { repro_evidence: reproEvidence } : {}),
1418
1518
  ...(externalChecks.length > 0 ? { external_checks: externalChecks } : {}),
1419
- run_dir: toPosixPath(VERIFY_RUN_DIR),
1420
- manifest_path: toPosixPath(VERIFY_MANIFEST_PATH),
1519
+ run_dir: '',
1520
+ manifest_path: '',
1421
1521
  results,
1422
1522
  };
1423
1523
  return writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks, scopeDiffRisks, validationRatchetRisks, reproEvidence, externalChecks);
@@ -1491,19 +1591,23 @@ export async function runVerify(args, reporter, lang = 'en') {
1491
1591
  if (parsed.error) {
1492
1592
  const message = parsed.error === 'missing_reason_value'
1493
1593
  ? t(lang, 'cli.error.missingValue', { option: '--reason' })
1494
- : parsed.error === 'missing_from_classification_value'
1495
- ? t(lang, 'cli.error.missingValue', { option: '--from-classification' })
1496
- : parsed.error === 'missing_from_plan_value'
1497
- ? t(lang, 'cli.error.missingValue', { option: '--from-plan' })
1498
- : parsed.error === 'missing_write_plan_value'
1499
- ? t(lang, 'cli.error.missingValue', { option: '--write-plan' })
1500
- : parsed.error === 'missing_repro_evidence_value'
1501
- ? t(lang, 'cli.error.missingValue', { option: '--repro-evidence' })
1502
- : parsed.error === 'missing_external_evidence_value'
1503
- ? t(lang, 'cli.error.missingValue', { option: '--external-evidence' })
1504
- : parsed.error.startsWith('unexpected:')
1505
- ? t(lang, 'cli.error.unexpectedArgument', { argument: parsed.error.slice('unexpected:'.length) })
1506
- : t(lang, 'cli.error.unknownOption', { option: parsed.error });
1594
+ : parsed.error === 'missing_parallel_value'
1595
+ ? t(lang, 'cli.error.missingValue', { option: '--parallel' })
1596
+ : parsed.error === 'invalid_parallel_value'
1597
+ ? t(lang, 'verify.error.invalidParallel')
1598
+ : parsed.error === 'missing_from_classification_value'
1599
+ ? t(lang, 'cli.error.missingValue', { option: '--from-classification' })
1600
+ : parsed.error === 'missing_from_plan_value'
1601
+ ? t(lang, 'cli.error.missingValue', { option: '--from-plan' })
1602
+ : parsed.error === 'missing_write_plan_value'
1603
+ ? t(lang, 'cli.error.missingValue', { option: '--write-plan' })
1604
+ : parsed.error === 'missing_repro_evidence_value'
1605
+ ? t(lang, 'cli.error.missingValue', { option: '--repro-evidence' })
1606
+ : parsed.error === 'missing_external_evidence_value'
1607
+ ? t(lang, 'cli.error.missingValue', { option: '--external-evidence' })
1608
+ : parsed.error.startsWith('unexpected:')
1609
+ ? t(lang, 'cli.error.unexpectedArgument', { argument: parsed.error.slice('unexpected:'.length) })
1610
+ : t(lang, 'cli.error.unknownOption', { option: parsed.error });
1507
1611
  printUsageError(reporter, message, 'mf verify --help', getVerifyHelp(lang), lang);
1508
1612
  return 1;
1509
1613
  }
@@ -1552,7 +1656,7 @@ export async function runVerify(args, reporter, lang = 'en') {
1552
1656
  changedPlan = changedInput.plan;
1553
1657
  }
1554
1658
  else if (parsed.fromClassification || parsed.fromPlan) {
1555
- input = readInputFromPlan(projectRoot, (parsed.fromClassification ?? parsed.fromPlan));
1659
+ input = readInputFromClassificationReport(projectRoot, (parsed.fromClassification ?? parsed.fromPlan));
1556
1660
  }
1557
1661
  else {
1558
1662
  input = {
@@ -1588,7 +1692,7 @@ export async function runVerify(args, reporter, lang = 'en') {
1588
1692
  reporter.stdout(JSON.stringify(await createPlanOnlyOutput(input, projectRoot), null, 2));
1589
1693
  return 0;
1590
1694
  }
1591
- const output = await createVerifyOutput(input, parsed.fromClassification ?? parsed.fromPlan ?? (parsed.changed ? 'changed' : null), projectRoot, lang, reproEvidence, externalChecks);
1695
+ const output = await createVerifyOutput(input, parsed.fromClassification ?? parsed.fromPlan ?? (parsed.changed ? 'changed' : null), projectRoot, lang, reproEvidence, externalChecks, parsed.parallelism ?? DEFAULT_VERIFY_PARALLELISM);
1592
1696
  if (parsed.json) {
1593
1697
  reporter.stdout(JSON.stringify(output, null, 2));
1594
1698
  }
@@ -750,11 +750,12 @@ Read these files before working:
750
750
  "verify.help.summary": "Run configured verification intents selected by required_after metadata.",
751
751
  "verify.help.option.reason": "Select the required_after reason to verify",
752
752
  "verify.help.option.fromClassification": "Read verification reasons from an mf classify report inside this repository",
753
- "verify.help.option.fromPlan": "Compatibility alias for --from-classification",
753
+ "verify.help.option.fromPlan": "Deprecated compatibility alias for --from-classification; it still expects an mf classify report",
754
754
  "verify.help.option.changed": "Classify current Git changes and verify the matching reasons",
755
755
  "verify.help.option.writePlan": "Compatibility option that writes the changed-file classification report",
756
756
  "verify.help.option.reproEvidence": "Read structured bug-fix reproduction evidence from a repository-local JSON summary",
757
757
  "verify.help.option.externalEvidence": "Read lower-authority external CI evidence from a repository-local JSON summary",
758
+ "verify.help.option.parallel": "Run safe non-conflicting schedule batches with up to this many commands; default is 1",
758
759
  "verify.help.option.planOnly": "Print the verification plan without running commands; requires --json",
759
760
  "verify.help.exit.ok": "All selected verification intents passed",
760
761
  "verify.help.exit.fail": "Verification failed, was partial, was blocked, or input was invalid",
@@ -769,6 +770,7 @@ Read these files before working:
769
770
  "verify.error.planOnlyJson": "--plan-only requires --json",
770
771
  "verify.error.reproEvidenceRequiresRun": "--repro-evidence cannot be used with --plan-only",
771
772
  "verify.error.externalEvidenceRequiresRun": "--external-evidence cannot be used with --plan-only",
773
+ "verify.error.invalidParallel": "--parallel must be a positive integer",
772
774
  "verify.error.invalid_plan_file": "Classification report must be a readable JSON file",
773
775
  "verify.error.unsupported_plan_source": "Verification input must be an mf classify report",
774
776
  "verify.error.plan_root_mismatch": "Classification report must come from this mustflow root",
@@ -750,11 +750,12 @@ Lee estos archivos antes de trabajar:
750
750
  "verify.help.summary": "Ejecuta intenciones de verificación configuradas seleccionadas por metadatos required_after.",
751
751
  "verify.help.option.reason": "Selecciona la razón required_after que se debe verificar",
752
752
  "verify.help.option.fromClassification": "Lee razones de verificación desde un informe de mf classify dentro de este repositorio",
753
- "verify.help.option.fromPlan": "Alias de compatibilidad para --from-classification",
753
+ "verify.help.option.fromPlan": "Alias de compatibilidad obsoleto para --from-classification; todavía espera un informe de mf classify",
754
754
  "verify.help.option.changed": "Clasifica los cambios actuales de Git y verifica las razones correspondientes",
755
755
  "verify.help.option.writePlan": "Opción de compatibilidad que escribe el informe de clasificación de cambios",
756
756
  "verify.help.option.reproEvidence": "Lee evidencia estructurada de reproducción de errores desde un resumen JSON local del repositorio",
757
757
  "verify.help.option.externalEvidence": "Lee evidencia de CI externa de menor autoridad desde un resumen JSON local del repositorio",
758
+ "verify.help.option.parallel": "Ejecuta lotes programados seguros y sin conflictos con hasta esta cantidad de comandos; el valor predeterminado es 1",
758
759
  "verify.help.option.planOnly": "Imprime el plan de verificación sin ejecutar comandos; requiere --json",
759
760
  "verify.help.exit.ok": "Todas las intenciones de verificación seleccionadas pasaron",
760
761
  "verify.help.exit.fail": "La verificación falló, fue parcial, quedó bloqueada o la entrada no fue válida",
@@ -769,6 +770,7 @@ Lee estos archivos antes de trabajar:
769
770
  "verify.error.planOnlyJson": "--plan-only requiere --json",
770
771
  "verify.error.reproEvidenceRequiresRun": "--repro-evidence no se puede usar con --plan-only",
771
772
  "verify.error.externalEvidenceRequiresRun": "--external-evidence no se puede usar con --plan-only",
773
+ "verify.error.invalidParallel": "--parallel debe ser un entero positivo",
772
774
  "verify.error.invalid_plan_file": "El informe de clasificación debe ser un archivo JSON legible",
773
775
  "verify.error.unsupported_plan_source": "La entrada de verificación debe ser un informe de mf classify",
774
776
  "verify.error.plan_root_mismatch": "El informe de clasificación debe provenir de esta raíz mustflow",
@@ -750,11 +750,12 @@ Lisez ces fichiers avant de travailler :
750
750
  "verify.help.summary": "Exécute les intentions de vérification configurées sélectionnées par les métadonnées required_after.",
751
751
  "verify.help.option.reason": "Sélectionne la raison required_after à vérifier",
752
752
  "verify.help.option.fromClassification": "Lit les raisons de vérification depuis un rapport mf classify dans ce dépôt",
753
- "verify.help.option.fromPlan": "Alias de compatibilité pour --from-classification",
753
+ "verify.help.option.fromPlan": "Alias de compatibilité obsolète pour --from-classification; il attend toujours un rapport mf classify",
754
754
  "verify.help.option.changed": "Classe les changements Git actuels et vérifie les raisons correspondantes",
755
755
  "verify.help.option.writePlan": "Option de compatibilité qui écrit le rapport de classification des changements",
756
756
  "verify.help.option.reproEvidence": "Lit une preuve structurée de reproduction de bogue depuis un résumé JSON local au dépôt",
757
757
  "verify.help.option.externalEvidence": "Lit une preuve CI externe de moindre autorité depuis un résumé JSON local au dépôt",
758
+ "verify.help.option.parallel": "Exécute les lots planifiés sûrs et sans conflit avec au plus ce nombre de commandes ; valeur par défaut : 1",
758
759
  "verify.help.option.planOnly": "Affiche le plan de vérification sans exécuter de commandes; nécessite --json",
759
760
  "verify.help.exit.ok": "Toutes les intentions de vérification sélectionnées ont réussi",
760
761
  "verify.help.exit.fail": "La vérification a échoué, est partielle, est bloquée ou l'entrée est invalide",
@@ -769,6 +770,7 @@ Lisez ces fichiers avant de travailler :
769
770
  "verify.error.planOnlyJson": "--plan-only nécessite --json",
770
771
  "verify.error.reproEvidenceRequiresRun": "--repro-evidence ne peut pas être utilisé avec --plan-only",
771
772
  "verify.error.externalEvidenceRequiresRun": "--external-evidence ne peut pas être utilisé avec --plan-only",
773
+ "verify.error.invalidParallel": "--parallel doit être un entier positif",
772
774
  "verify.error.invalid_plan_file": "Le rapport de classification doit être un fichier JSON lisible",
773
775
  "verify.error.unsupported_plan_source": "L'entrée de vérification doit être un rapport mf classify",
774
776
  "verify.error.plan_root_mismatch": "Le rapport de classification doit venir de cette racine mustflow",
@@ -750,11 +750,12 @@ export const hiMessages = {
750
750
  "verify.help.summary": "required_after metadata से चुने गए configured verification intents चलाएँ।",
751
751
  "verify.help.option.reason": "Verify करने के लिए required_after reason चुनें",
752
752
  "verify.help.option.fromClassification": "इस repository के अंदर mf classify report से verification reasons पढ़ें",
753
- "verify.help.option.fromPlan": "--from-classification का compatibility alias",
753
+ "verify.help.option.fromPlan": "--from-classification का deprecated compatibility alias; input अब भी mf classify report होना चाहिए",
754
754
  "verify.help.option.changed": "Current Git changes classify करके matching reasons verify करें",
755
755
  "verify.help.option.writePlan": "Changed-file classification report लिखने वाला compatibility option",
756
756
  "verify.help.option.reproEvidence": "Repository-local JSON summary से structured bug reproduction evidence पढ़ें",
757
757
  "verify.help.option.externalEvidence": "Repository-local JSON summary से lower-authority external CI evidence पढ़ें",
758
+ "verify.help.option.parallel": "Safe और non-conflicting schedule batches को इतने commands तक साथ चलाएं; default 1 है",
758
759
  "verify.help.option.planOnly": "Commands चलाए बिना verification plan print करें; --json चाहिए",
759
760
  "verify.help.exit.ok": "सभी selected verification intents pass हुए",
760
761
  "verify.help.exit.fail": "Verification fail हुआ, partial रहा, blocked रहा, या input invalid था",
@@ -769,6 +770,7 @@ export const hiMessages = {
769
770
  "verify.error.planOnlyJson": "--plan-only के लिए --json चाहिए",
770
771
  "verify.error.reproEvidenceRequiresRun": "--repro-evidence को --plan-only के साथ इस्तेमाल नहीं किया जा सकता",
771
772
  "verify.error.externalEvidenceRequiresRun": "--external-evidence को --plan-only के साथ इस्तेमाल नहीं किया जा सकता",
773
+ "verify.error.invalidParallel": "--parallel positive integer होना चाहिए",
772
774
  "verify.error.invalid_plan_file": "Classification report readable JSON file होना चाहिए",
773
775
  "verify.error.unsupported_plan_source": "Verification input mf classify report होना चाहिए",
774
776
  "verify.error.plan_root_mismatch": "Classification report इसी mustflow root से आना चाहिए",
@@ -750,11 +750,12 @@ export const koMessages = {
750
750
  "verify.help.summary": "required_after 메타데이터로 선택된 설정된 검증 의도를 실행합니다.",
751
751
  "verify.help.option.reason": "검증할 required_after 이유를 지정합니다",
752
752
  "verify.help.option.fromClassification": "이 저장소 안의 mf classify 보고서에서 검증 이유를 읽습니다",
753
- "verify.help.option.fromPlan": "--from-classification과 같은 호환 옵션입니다",
753
+ "verify.help.option.fromPlan": "--from-classification과 같은 폐기 예정 호환 옵션입니다. 입력은 여전히 mf classify 보고서여야 합니다",
754
754
  "verify.help.option.changed": "현재 Git 변경을 분류하고 맞는 검증 이유를 실행합니다",
755
755
  "verify.help.option.writePlan": "변경 파일 분류 보고서를 쓰는 호환 옵션입니다",
756
756
  "verify.help.option.reproEvidence": "저장소 안의 JSON 요약에서 구조화된 버그 재현 증거를 읽습니다",
757
757
  "verify.help.option.externalEvidence": "저장소 안의 JSON 요약에서 낮은 권한의 외부 CI 증거를 읽습니다",
758
+ "verify.help.option.parallel": "안전하고 서로 충돌하지 않는 예정 실행 묶음을 이 개수까지 함께 실행합니다. 기본값은 1입니다",
758
759
  "verify.help.option.planOnly": "명령을 실행하지 않고 검증 계획만 출력합니다. --json이 필요합니다",
759
760
  "verify.help.exit.ok": "선택된 모든 검증 의도가 통과했습니다",
760
761
  "verify.help.exit.fail": "검증이 실패했거나, 일부만 실행됐거나, 막혔거나, 입력이 올바르지 않습니다",
@@ -769,6 +770,7 @@ export const koMessages = {
769
770
  "verify.error.planOnlyJson": "--plan-only에는 --json이 필요합니다",
770
771
  "verify.error.reproEvidenceRequiresRun": "--repro-evidence는 --plan-only와 함께 사용할 수 없습니다",
771
772
  "verify.error.externalEvidenceRequiresRun": "--external-evidence는 --plan-only와 함께 사용할 수 없습니다",
773
+ "verify.error.invalidParallel": "--parallel 값은 양의 정수여야 합니다",
772
774
  "verify.error.invalid_plan_file": "분류 보고서는 읽을 수 있는 JSON 파일이어야 합니다",
773
775
  "verify.error.unsupported_plan_source": "검증 입력은 mf classify 보고서여야 합니다",
774
776
  "verify.error.plan_root_mismatch": "분류 보고서는 현재 mustflow 루트에서 나온 것이어야 합니다",
@@ -750,11 +750,12 @@ export const zhMessages = {
750
750
  "verify.help.summary": "运行由 required_after 元数据选出的已配置验证意图。",
751
751
  "verify.help.option.reason": "选择要验证的 required_after 原因",
752
752
  "verify.help.option.fromClassification": "从此仓库内的 mf classify 报告读取验证原因",
753
- "verify.help.option.fromPlan": "--from-classification 的兼容别名",
753
+ "verify.help.option.fromPlan": "--from-classification 的已弃用兼容别名;输入仍必须是 mf classify 报告",
754
754
  "verify.help.option.changed": "分类当前 Git 变更并验证匹配的原因",
755
755
  "verify.help.option.writePlan": "写入变更文件分类报告的兼容选项",
756
756
  "verify.help.option.reproEvidence": "从仓库本地 JSON 摘要读取结构化的 bug 复现证据",
757
757
  "verify.help.option.externalEvidence": "从仓库本地 JSON 摘要读取低权限外部 CI 证据",
758
+ "verify.help.option.parallel": "最多并行执行这个数量的安全、无冲突计划批次命令;默认值为 1",
758
759
  "verify.help.option.planOnly": "仅输出验证计划,不执行命令;需要 --json",
759
760
  "verify.help.exit.ok": "选中的所有验证意图均已通过",
760
761
  "verify.help.exit.fail": "验证失败、部分完成、被阻止,或输入无效",
@@ -769,6 +770,7 @@ export const zhMessages = {
769
770
  "verify.error.planOnlyJson": "--plan-only 需要 --json",
770
771
  "verify.error.reproEvidenceRequiresRun": "--repro-evidence 不能与 --plan-only 一起使用",
771
772
  "verify.error.externalEvidenceRequiresRun": "--external-evidence 不能与 --plan-only 一起使用",
773
+ "verify.error.invalidParallel": "--parallel 必须是正整数",
772
774
  "verify.error.invalid_plan_file": "分类报告必须是可读取的 JSON 文件",
773
775
  "verify.error.unsupported_plan_source": "验证输入必须是 mf classify 报告",
774
776
  "verify.error.plan_root_mismatch": "分类报告必须来自当前 mustflow 根目录",
@@ -11,6 +11,8 @@ const REPO_MAP_GENERATOR = 'mustflow';
11
11
  const REPO_MAP_RELATIVE_ROOT = '.';
12
12
  const REPO_MAP_SOURCE_POLICY = 'anchors_only';
13
13
  const REPO_MAP_PRIVACY_MODE = 'minimal';
14
+ const GIT_LS_FILES_TIMEOUT_MS = 5_000;
15
+ const GIT_LS_FILES_MAX_BUFFER_BYTES = 1_048_576;
14
16
  const EXCLUDED_SEGMENTS = new Set([
15
17
  '.astro',
16
18
  '.cache',
@@ -240,10 +242,15 @@ function getRepoMapConfig(projectRoot) {
240
242
  },
241
243
  };
242
244
  }
243
- function getGitFiles(projectRoot) {
244
- const result = spawnSync('git', ['ls-files', '-z'], {
245
+ export function listGitFilesForRepoMap(projectRoot, options = {}) {
246
+ const spawnGit = options.spawnGit ??
247
+ ((command, args, spawnOptions) => spawnSync(command, [...args], spawnOptions));
248
+ const result = spawnGit('git', ['ls-files', '-z'], {
245
249
  cwd: projectRoot,
246
250
  encoding: 'utf8',
251
+ maxBuffer: options.maxBuffer ?? GIT_LS_FILES_MAX_BUFFER_BYTES,
252
+ timeout: options.timeout ?? GIT_LS_FILES_TIMEOUT_MS,
253
+ windowsHide: true,
247
254
  });
248
255
  if (result.status !== 0 || result.error) {
249
256
  return [];
@@ -282,7 +289,7 @@ function listAnchorCandidateFilesRecursive(rootPath, depth, priorityPaths) {
282
289
  }
283
290
  function getRepositoryFiles(projectRoot, depth, priorityPaths) {
284
291
  const files = new Set();
285
- for (const relativePath of getGitFiles(projectRoot)) {
292
+ for (const relativePath of listGitFilesForRepoMap(projectRoot)) {
286
293
  files.add(relativePath);
287
294
  }
288
295
  for (const relativePath of listAnchorCandidateFilesRecursive(projectRoot, depth, priorityPaths)) {
@@ -0,0 +1,31 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { mkdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ function tempFilePath(targetPath) {
5
+ const suffix = `${process.pid}-${Date.now()}-${randomBytes(6).toString('hex')}`;
6
+ return path.join(path.dirname(targetPath), `.${path.basename(targetPath)}.${suffix}.tmp`);
7
+ }
8
+ export function createStateRunId(prefix) {
9
+ const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-');
10
+ return `${prefix}-${timestamp}-${process.pid}-${randomBytes(6).toString('hex')}`;
11
+ }
12
+ export function atomicWriteTextFile(targetPath, content) {
13
+ mkdirSync(path.dirname(targetPath), { recursive: true });
14
+ const temporaryPath = tempFilePath(targetPath);
15
+ try {
16
+ writeFileSync(temporaryPath, content, { encoding: 'utf8', flag: 'wx' });
17
+ renameSync(temporaryPath, targetPath);
18
+ }
19
+ catch (error) {
20
+ try {
21
+ unlinkSync(temporaryPath);
22
+ }
23
+ catch {
24
+ // Best-effort cleanup for a temporary file that may not have been created.
25
+ }
26
+ throw error;
27
+ }
28
+ }
29
+ export function atomicWriteJsonFile(targetPath, value) {
30
+ atomicWriteTextFile(targetPath, `${JSON.stringify(value, null, 2)}\n`);
31
+ }
@@ -1,3 +1,24 @@
1
+ function isUtf8ContinuationByte(value) {
2
+ return value !== undefined && (value & 0xc0) === 0x80;
3
+ }
4
+ function findUtf8TailStart(buffer, startOffset) {
5
+ let start = Math.min(buffer.byteLength, Math.max(0, Math.trunc(startOffset)));
6
+ while (start < buffer.byteLength && isUtf8ContinuationByte(buffer[start])) {
7
+ start += 1;
8
+ }
9
+ return start;
10
+ }
11
+ export function decodeUtf8Tail(buffer, maxTailBytes) {
12
+ if (maxTailBytes <= 0) {
13
+ return { text: '', truncated: buffer.byteLength > 0 };
14
+ }
15
+ const rawStart = buffer.byteLength > maxTailBytes ? buffer.byteLength - maxTailBytes : 0;
16
+ const start = findUtf8TailStart(buffer, rawStart);
17
+ return {
18
+ text: buffer.subarray(start).toString('utf8'),
19
+ truncated: buffer.byteLength > maxTailBytes || start > 0,
20
+ };
21
+ }
1
22
  export class BoundedOutputBuffer {
2
23
  #maxTailBytes;
3
24
  #chunks = [];
@@ -30,9 +51,10 @@ export class BoundedOutputBuffer {
30
51
  }
31
52
  }
32
53
  toSnapshot() {
54
+ const tail = decodeUtf8Tail(Buffer.concat(this.#chunks, this.#tailBytes), this.#maxTailBytes);
33
55
  return {
34
56
  bytes: this.#bytes,
35
- tail: Buffer.concat(this.#chunks, this.#tailBytes).toString('utf8'),
57
+ tail: tail.text,
36
58
  };
37
59
  }
38
60
  }
@@ -14,6 +14,7 @@ const CHECK_ISSUE_ID_RULES = [
14
14
  ['mustflow.command_contract.effects_invalid', /^(?:Strict: )?(?:\[commands\.(?:resources|intents\.[^\]]+\.effects)[^\]]*\]|Command effect for intent [^\s]+ must define path, paths, or lock)/u],
15
15
  ['mustflow.command_contract.effect_path_escape', /^Strict: Command effect path must stay inside the current root:/u],
16
16
  ['mustflow.command_contract.shared_writes_without_effects', /^Strict warning: configured agent-runnable intents .+ share path:.+ through writes without explicit effects or resource locks$/u],
17
+ ['mustflow.command_contract.broad_env_inheritance', /^Strict warning: configured agent-runnable intent [^\s]+ (?:implicitly inherits the host environment|uses env_policy = "inherit")/u],
17
18
  ['mustflow.prompt_cache.required', /^Strict: \[prompt_cache\] table is required$/u],
18
19
  ['mustflow.prompt_cache.volatile_in_stable', /^Strict: \[prompt_cache\.layers\.stable\]\.read must not include volatile path /u],
19
20
  ['mustflow.refresh.hash_method_required', /^Strict: \[refresh\]\.default_method should be "hash_check" for cache-friendly refresh$/u],