mustflow 2.21.2 → 2.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/run/executor.js +34 -15
- package/dist/cli/commands/run/output.js +24 -0
- package/dist/cli/commands/verify/args.js +34 -0
- package/dist/cli/commands/verify.js +92 -39
- package/dist/cli/i18n/en.js +2 -1
- package/dist/cli/i18n/es.js +2 -1
- package/dist/cli/i18n/fr.js +2 -1
- package/dist/cli/i18n/hi.js +2 -1
- package/dist/cli/i18n/ko.js +2 -1
- package/dist/cli/i18n/zh.js +2 -1
- package/dist/cli/lib/filesystem.js +27 -4
- package/dist/cli/lib/local-index/index.js +87 -19
- package/dist/core/run-write-drift.js +110 -24
- package/dist/core/verification-scheduler.js +1 -1
- package/package.json +1 -1
- package/schemas/run-receipt.schema.json +15 -1
- package/schemas/verify-report.schema.json +28 -0
- package/templates/default/manifest.toml +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { BoundedOutputBuffer } from '../../../core/bounded-output.js';
|
|
3
3
|
import { createPendingTimeoutTermination, forceTerminateProcessTree, getKillMethod, terminateProcessTree, } from './process-tree.js';
|
|
4
|
-
import { createOutputLimitError, isOutputLimitExceededError, writeStreamChunk } from './output.js';
|
|
4
|
+
import { createOutputLimitError, isOutputLimitExceededError, writeOutputLimitTerminationMarker, writeStreamChunk, writeStreamChunkPrefix, } from './output.js';
|
|
5
5
|
const TERMINATION_CONFIRMATION_FALLBACK_MS = 1000;
|
|
6
6
|
function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
7
7
|
return new Promise((resolve) => {
|
|
@@ -17,6 +17,7 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, killAfter
|
|
|
17
17
|
let forceKillTimeout;
|
|
18
18
|
let terminationFallbackTimeout;
|
|
19
19
|
let terminationStarted = false;
|
|
20
|
+
let outputLimitMarkerWritten = false;
|
|
20
21
|
let termination = null;
|
|
21
22
|
const child = spawn(command.executable, command.args ?? [], {
|
|
22
23
|
cwd,
|
|
@@ -92,25 +93,43 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, killAfter
|
|
|
92
93
|
}
|
|
93
94
|
beginTermination();
|
|
94
95
|
};
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (streamOutput) {
|
|
99
|
-
writeStreamChunk(reporter, 'stdout', chunk);
|
|
96
|
+
const writeOutputLimitMarkerOnce = () => {
|
|
97
|
+
if (!streamOutput || outputLimitMarkerWritten) {
|
|
98
|
+
return;
|
|
100
99
|
}
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
outputLimitMarkerWritten = true;
|
|
101
|
+
writeOutputLimitTerminationMarker(reporter);
|
|
102
|
+
};
|
|
103
|
+
const handleOutputChunk = (stream, buffer, chunk) => {
|
|
104
|
+
const previousBytes = stream === 'stdout' ? stdoutBytes : stderrBytes;
|
|
105
|
+
const nextBytes = previousBytes + chunk.byteLength;
|
|
106
|
+
const exceedsLimit = enforceOutputLimit && nextBytes > maxOutputBytes;
|
|
107
|
+
const remainingStreamBytes = enforceOutputLimit ? Math.max(0, maxOutputBytes - previousBytes) : chunk.byteLength;
|
|
108
|
+
buffer.append(chunk);
|
|
109
|
+
if (stream === 'stdout') {
|
|
110
|
+
stdoutBytes = nextBytes;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
stderrBytes = nextBytes;
|
|
103
114
|
}
|
|
104
|
-
});
|
|
105
|
-
child.stderr?.on('data', (chunk) => {
|
|
106
|
-
stderr.append(chunk);
|
|
107
|
-
stderrBytes += chunk.byteLength;
|
|
108
115
|
if (streamOutput) {
|
|
109
|
-
|
|
116
|
+
if (exceedsLimit) {
|
|
117
|
+
writeStreamChunkPrefix(reporter, stream, chunk, remainingStreamBytes);
|
|
118
|
+
writeOutputLimitMarkerOnce();
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
writeStreamChunk(reporter, stream, chunk);
|
|
122
|
+
}
|
|
110
123
|
}
|
|
111
|
-
if (
|
|
112
|
-
stopForOutputLimit(
|
|
124
|
+
if (exceedsLimit) {
|
|
125
|
+
stopForOutputLimit(stream);
|
|
113
126
|
}
|
|
127
|
+
};
|
|
128
|
+
child.stdout?.on('data', (chunk) => {
|
|
129
|
+
handleOutputChunk('stdout', stdout, chunk);
|
|
130
|
+
});
|
|
131
|
+
child.stderr?.on('data', (chunk) => {
|
|
132
|
+
handleOutputChunk('stderr', stderr, chunk);
|
|
114
133
|
});
|
|
115
134
|
child.once('error', (error) => {
|
|
116
135
|
childError = error;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const OUTPUT_LIMIT_ERROR_CODE = 'ENOBUFS';
|
|
2
2
|
const OUTPUT_LIMIT_ERROR_MESSAGE = /\bmaxBuffer\b.*\bexceeded\b/i;
|
|
3
|
+
const OUTPUT_LIMIT_TERMINATION_MARKER = '[mustflow] output limit exceeded; terminating command before streaming more child output.';
|
|
3
4
|
export function emitOutput(reporter, output, stream) {
|
|
4
5
|
if (!output) {
|
|
5
6
|
return;
|
|
@@ -45,6 +46,29 @@ export function writeStreamChunk(reporter, stream, chunk) {
|
|
|
45
46
|
}
|
|
46
47
|
reporter.stderr(chunk.toString());
|
|
47
48
|
}
|
|
49
|
+
function isUtf8ContinuationByte(value) {
|
|
50
|
+
return value !== undefined && (value & 0xc0) === 0x80;
|
|
51
|
+
}
|
|
52
|
+
function findUtf8PrefixEnd(buffer, maxBytes) {
|
|
53
|
+
let end = Math.min(buffer.byteLength, Math.max(0, Math.trunc(maxBytes)));
|
|
54
|
+
if (end >= buffer.byteLength) {
|
|
55
|
+
return buffer.byteLength;
|
|
56
|
+
}
|
|
57
|
+
while (end > 0 && isUtf8ContinuationByte(buffer[end])) {
|
|
58
|
+
end -= 1;
|
|
59
|
+
}
|
|
60
|
+
return end;
|
|
61
|
+
}
|
|
62
|
+
export function writeStreamChunkPrefix(reporter, stream, chunk, maxBytes) {
|
|
63
|
+
const prefixEnd = findUtf8PrefixEnd(chunk, maxBytes);
|
|
64
|
+
if (prefixEnd <= 0) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
writeStreamChunk(reporter, stream, chunk.subarray(0, prefixEnd));
|
|
68
|
+
}
|
|
69
|
+
export function writeOutputLimitTerminationMarker(reporter) {
|
|
70
|
+
reporter.stderr(OUTPUT_LIMIT_TERMINATION_MARKER);
|
|
71
|
+
}
|
|
48
72
|
export function createOutputLimitError(stream, maxOutputBytes) {
|
|
49
73
|
return Object.assign(new Error(`${stream} exceeded per-stream max_output_bytes (${maxOutputBytes})`), {
|
|
50
74
|
code: OUTPUT_LIMIT_ERROR_CODE,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { availableParallelism } from 'node:os';
|
|
1
2
|
export const DEFAULT_VERIFY_PARALLELISM = 1;
|
|
3
|
+
export const MAX_VERIFY_PARALLELISM = 8;
|
|
2
4
|
export function parseVerifyArgs(args) {
|
|
3
5
|
let reason;
|
|
4
6
|
let fromClassification;
|
|
@@ -10,6 +12,7 @@ export function parseVerifyArgs(args) {
|
|
|
10
12
|
let planOnly = false;
|
|
11
13
|
let changed = false;
|
|
12
14
|
let parallelism = DEFAULT_VERIFY_PARALLELISM;
|
|
15
|
+
let parallelismSpecified = false;
|
|
13
16
|
for (let index = 0; index < args.length; index += 1) {
|
|
14
17
|
const arg = args[index];
|
|
15
18
|
if (arg === '--json') {
|
|
@@ -34,6 +37,7 @@ export function parseVerifyArgs(args) {
|
|
|
34
37
|
return { json, planOnly, changed, reason, parallelism, error: 'invalid_parallel_value' };
|
|
35
38
|
}
|
|
36
39
|
parallelism = parsedParallelism;
|
|
40
|
+
parallelismSpecified = true;
|
|
37
41
|
index += 1;
|
|
38
42
|
continue;
|
|
39
43
|
}
|
|
@@ -144,6 +148,7 @@ export function parseVerifyArgs(args) {
|
|
|
144
148
|
return { json, planOnly, changed, reason, parallelism, error: 'invalid_parallel_value' };
|
|
145
149
|
}
|
|
146
150
|
parallelism = parsedParallelism;
|
|
151
|
+
parallelismSpecified = true;
|
|
147
152
|
continue;
|
|
148
153
|
}
|
|
149
154
|
if (arg.startsWith('--from-plan=')) {
|
|
@@ -251,6 +256,7 @@ export function parseVerifyArgs(args) {
|
|
|
251
256
|
reproEvidence,
|
|
252
257
|
externalEvidence,
|
|
253
258
|
parallelism,
|
|
259
|
+
parallelismSpecified,
|
|
254
260
|
};
|
|
255
261
|
}
|
|
256
262
|
function parseVerifyParallelism(value) {
|
|
@@ -260,3 +266,31 @@ function parseVerifyParallelism(value) {
|
|
|
260
266
|
const parsed = Number(value);
|
|
261
267
|
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
262
268
|
}
|
|
269
|
+
function readAvailableParallelism() {
|
|
270
|
+
try {
|
|
271
|
+
const value = availableParallelism();
|
|
272
|
+
return Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
export function resolveVerifyParallelism(requested, cpuAvailable = readAvailableParallelism()) {
|
|
279
|
+
const cpuLimit = cpuAvailable === null ? MAX_VERIFY_PARALLELISM : Math.max(DEFAULT_VERIFY_PARALLELISM, cpuAvailable);
|
|
280
|
+
const effectiveLimit = Math.max(DEFAULT_VERIFY_PARALLELISM, Math.min(MAX_VERIFY_PARALLELISM, cpuLimit));
|
|
281
|
+
const effective = Math.max(DEFAULT_VERIFY_PARALLELISM, Math.min(requested, effectiveLimit));
|
|
282
|
+
const capped = effective !== requested;
|
|
283
|
+
const mode = effective > DEFAULT_VERIFY_PARALLELISM ? 'parallel_chunks' : 'serial';
|
|
284
|
+
const note = mode === 'parallel_chunks'
|
|
285
|
+
? 'Parallel verification is a bounded optimization for eligible non-conflicting entries; it is not stronger evidence than serial verification.'
|
|
286
|
+
: 'Verification runs serially unless an eligible non-conflicting batch receives an effective parallelism greater than 1.';
|
|
287
|
+
return {
|
|
288
|
+
requested,
|
|
289
|
+
effective,
|
|
290
|
+
repositoryMax: MAX_VERIFY_PARALLELISM,
|
|
291
|
+
cpuAvailable,
|
|
292
|
+
capped,
|
|
293
|
+
mode,
|
|
294
|
+
note,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { createClassifyOutput } from './classify.js';
|
|
5
4
|
import { runRun } from './run.js';
|
|
6
5
|
import { createChangeVerificationReport, } from '../../core/change-verification.js';
|
|
7
|
-
import { writeJsonFileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
|
|
6
|
+
import { readUtf8FileInsideWithoutSymlinks, writeJsonFileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
|
|
8
7
|
import { createVerifyCompletionVerdict, } from '../../core/completion-verdict.js';
|
|
9
8
|
import { createStateRunId } from '../../core/atomic-state-write.js';
|
|
10
9
|
import { createExternalEvidenceRisks, } from '../../core/external-evidence.js';
|
|
@@ -13,8 +12,9 @@ import { countReproEvidenceVerdictEffects, createReproEvidenceRisks, } from '../
|
|
|
13
12
|
import { createVerifyEvidenceModel } from '../../core/verification-evidence.js';
|
|
14
13
|
import { createScopeDiffRisks } from '../../core/scope-risk.js';
|
|
15
14
|
import { countValidationRatchetVerdictEffects, createValidationRatchetRisks, } from '../../core/validation-ratchet.js';
|
|
15
|
+
import { finishRunWriteBatchTracking, startRunWriteBatchTracking, } from '../../core/run-write-drift.js';
|
|
16
16
|
import { readCommandContract } from '../../core/config-loading.js';
|
|
17
|
-
import { DEFAULT_VERIFY_PARALLELISM, parseVerifyArgs } from './verify/args.js';
|
|
17
|
+
import { DEFAULT_VERIFY_PARALLELISM, parseVerifyArgs, resolveVerifyParallelism, } from './verify/args.js';
|
|
18
18
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
19
19
|
import { t } from '../lib/i18n.js';
|
|
20
20
|
import { readLocalCommandEffectGraphs, readLocalPathSurfaces, readLocalSourceAnchorVerdictRisks, } from '../lib/local-index.js';
|
|
@@ -240,15 +240,27 @@ function resolvePlanPath(projectRoot, inputPath) {
|
|
|
240
240
|
}
|
|
241
241
|
return resolved;
|
|
242
242
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
function readJsonInputFile(projectRoot, inputPath, invalidCode) {
|
|
244
|
+
const inputFilePath = resolvePlanPath(projectRoot, inputPath);
|
|
245
|
+
let content;
|
|
246
246
|
try {
|
|
247
|
-
|
|
247
|
+
content = readUtf8FileInsideWithoutSymlinks(projectRoot, inputFilePath);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (error instanceof Error && error.message.startsWith('Path must not contain symlinks:')) {
|
|
251
|
+
throw new Error('input_path_contains_symlink');
|
|
252
|
+
}
|
|
253
|
+
throw new Error(invalidCode);
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
return JSON.parse(content);
|
|
248
257
|
}
|
|
249
258
|
catch {
|
|
250
|
-
throw new Error(
|
|
259
|
+
throw new Error(invalidCode);
|
|
251
260
|
}
|
|
261
|
+
}
|
|
262
|
+
export function readInputFromClassificationReport(projectRoot, inputPath) {
|
|
263
|
+
const parsed = readJsonInputFile(projectRoot, inputPath, 'invalid_plan_file');
|
|
252
264
|
const classificationReport = readStrictClassifyPlan(projectRoot, parsed);
|
|
253
265
|
return {
|
|
254
266
|
reasons: classificationReport.summary.validationReasons,
|
|
@@ -469,14 +481,7 @@ function readRegressionGuardEvidence(value) {
|
|
|
469
481
|
};
|
|
470
482
|
}
|
|
471
483
|
function readReproEvidenceFile(projectRoot, inputPath) {
|
|
472
|
-
|
|
473
|
-
const evidencePath = resolvePlanPath(projectRoot, inputPath);
|
|
474
|
-
try {
|
|
475
|
-
parsed = JSON.parse(readFileSync(evidencePath, 'utf8'));
|
|
476
|
-
}
|
|
477
|
-
catch {
|
|
478
|
-
throw new Error('invalid_repro_evidence_file');
|
|
479
|
-
}
|
|
484
|
+
const parsed = readJsonInputFile(projectRoot, inputPath, 'invalid_repro_evidence_file');
|
|
480
485
|
if (!isPlainRecord(parsed) || parsed.schema_version !== '1' || parsed.command !== 'repro-evidence') {
|
|
481
486
|
throw new Error('unsupported_repro_evidence_source');
|
|
482
487
|
}
|
|
@@ -500,14 +505,7 @@ function readReproEvidenceFile(projectRoot, inputPath) {
|
|
|
500
505
|
};
|
|
501
506
|
}
|
|
502
507
|
function readExternalEvidenceFile(projectRoot, inputPath) {
|
|
503
|
-
|
|
504
|
-
const evidencePath = resolvePlanPath(projectRoot, inputPath);
|
|
505
|
-
try {
|
|
506
|
-
parsed = JSON.parse(readFileSync(evidencePath, 'utf8'));
|
|
507
|
-
}
|
|
508
|
-
catch {
|
|
509
|
-
throw new Error('invalid_external_evidence_file');
|
|
510
|
-
}
|
|
508
|
+
const parsed = readJsonInputFile(projectRoot, inputPath, 'invalid_external_evidence_file');
|
|
511
509
|
if (!isPlainRecord(parsed) || parsed.schema_version !== '1' || parsed.command !== 'external-evidence') {
|
|
512
510
|
throw new Error('unsupported_external_evidence_source');
|
|
513
511
|
}
|
|
@@ -552,6 +550,8 @@ export function planErrorMessageKey(code) {
|
|
|
552
550
|
switch (code) {
|
|
553
551
|
case 'plan_path_outside_root':
|
|
554
552
|
return 'verify.error.plan_path_outside_root';
|
|
553
|
+
case 'input_path_contains_symlink':
|
|
554
|
+
return 'verify.error.input_path_contains_symlink';
|
|
555
555
|
case 'missing_plan_reasons':
|
|
556
556
|
return 'verify.error.missing_plan_reasons';
|
|
557
557
|
case 'unsupported_plan_source':
|
|
@@ -677,19 +677,52 @@ async function runVerificationEntriesSequentially(entries, lang, verificationPla
|
|
|
677
677
|
}
|
|
678
678
|
return results;
|
|
679
679
|
}
|
|
680
|
-
async function runVerificationEntriesInParallelChunks(entries, parallelism, lang, verificationPlanId, scheduledTestTargets) {
|
|
680
|
+
async function runVerificationEntriesInParallelChunks(projectRoot, entries, parallelism, lang, verificationPlanId, scheduledTestTargets) {
|
|
681
681
|
const results = [];
|
|
682
682
|
for (let index = 0; index < entries.length; index += parallelism) {
|
|
683
683
|
const chunk = entries.slice(index, index + parallelism);
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
684
|
+
const batchTracker = startRunWriteBatchTracking(projectRoot);
|
|
685
|
+
const chunkResults = await Promise.all(chunk.map((entry) => runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? [])));
|
|
686
|
+
const writeDriftByIntent = finishRunWriteBatchTracking(batchTracker, chunk.map((entry) => ({
|
|
687
|
+
intentName: entry.intent,
|
|
688
|
+
declaredPaths: declaredWritePathsForScheduleEntry(entry),
|
|
689
|
+
observedPaths: observedWriteDriftPaths(chunkResults.find((result) => result.intent === entry.intent)),
|
|
690
|
+
})));
|
|
691
|
+
results.push(...chunkResults.map((result) => applyParallelChunkWriteDrift(result, writeDriftByIntent)));
|
|
690
692
|
}
|
|
691
693
|
return results;
|
|
692
694
|
}
|
|
695
|
+
function declaredWritePathsForScheduleEntry(entry) {
|
|
696
|
+
return [
|
|
697
|
+
...new Set(entry.effects
|
|
698
|
+
.filter((effect) => effect.access === 'write' && typeof effect.path === 'string')
|
|
699
|
+
.map((effect) => effect.path)),
|
|
700
|
+
].sort((left, right) => left.localeCompare(right));
|
|
701
|
+
}
|
|
702
|
+
function observedWriteDriftPaths(result) {
|
|
703
|
+
const writeDrift = objectField(result?.receipt?.write_drift);
|
|
704
|
+
const observedPaths = writeDrift?.observed_paths;
|
|
705
|
+
if (!Array.isArray(observedPaths)) {
|
|
706
|
+
return [];
|
|
707
|
+
}
|
|
708
|
+
return observedPaths.filter((value) => typeof value === 'string');
|
|
709
|
+
}
|
|
710
|
+
function applyParallelChunkWriteDrift(result, writeDriftByIntent) {
|
|
711
|
+
if (!result.intent || !result.receipt) {
|
|
712
|
+
return result;
|
|
713
|
+
}
|
|
714
|
+
const writeDrift = writeDriftByIntent.get(result.intent);
|
|
715
|
+
if (!writeDrift) {
|
|
716
|
+
return result;
|
|
717
|
+
}
|
|
718
|
+
return {
|
|
719
|
+
...result,
|
|
720
|
+
receipt: {
|
|
721
|
+
...result.receipt,
|
|
722
|
+
write_drift: writeDrift,
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
}
|
|
693
726
|
function verificationResultFailed(result) {
|
|
694
727
|
return (!result.skipped &&
|
|
695
728
|
(result.status === 'failed' ||
|
|
@@ -697,7 +730,7 @@ function verificationResultFailed(result) {
|
|
|
697
730
|
result.status === 'start_failed' ||
|
|
698
731
|
result.status === 'output_limit_exceeded'));
|
|
699
732
|
}
|
|
700
|
-
async function runScheduledVerificationIntents(report, lang, verificationPlanId, scheduledTestTargets, parallelism) {
|
|
733
|
+
async function runScheduledVerificationIntents(report, projectRoot, lang, verificationPlanId, scheduledTestTargets, parallelism) {
|
|
701
734
|
const results = [];
|
|
702
735
|
for (let batchIndex = 0; batchIndex < report.schedule.batches.length; batchIndex += 1) {
|
|
703
736
|
const batch = report.schedule.batches[batchIndex];
|
|
@@ -709,7 +742,7 @@ async function runScheduledVerificationIntents(report, lang, verificationPlanId,
|
|
|
709
742
|
if (entries.length > 1 && entries.every((entry) => entry.parallelEligible)) {
|
|
710
743
|
batchResults =
|
|
711
744
|
parallelism > DEFAULT_VERIFY_PARALLELISM
|
|
712
|
-
? await runVerificationEntriesInParallelChunks(entries, parallelism, lang, verificationPlanId, scheduledTestTargets)
|
|
745
|
+
? await runVerificationEntriesInParallelChunks(projectRoot, entries, parallelism, lang, verificationPlanId, scheduledTestTargets)
|
|
713
746
|
: await runVerificationEntriesSequentially(entries, lang, verificationPlanId, scheduledTestTargets);
|
|
714
747
|
}
|
|
715
748
|
else {
|
|
@@ -1001,7 +1034,7 @@ function readVerificationFailureFingerprint(value) {
|
|
|
1001
1034
|
}
|
|
1002
1035
|
function readPreviousVerifyLatestSummary(projectRoot) {
|
|
1003
1036
|
try {
|
|
1004
|
-
const parsed = JSON.parse(
|
|
1037
|
+
const parsed = JSON.parse(readUtf8FileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, LATEST_RUN_RECEIPT_PATH)));
|
|
1005
1038
|
if (parsed.command !== 'verify' ||
|
|
1006
1039
|
parsed.kind !== 'verify_run_summary' ||
|
|
1007
1040
|
typeof parsed.verification_plan_id !== 'string' ||
|
|
@@ -1061,6 +1094,17 @@ function createVerificationPlanId(report, contract) {
|
|
|
1061
1094
|
};
|
|
1062
1095
|
return hashTextSha256(stableJson(fingerprintSource));
|
|
1063
1096
|
}
|
|
1097
|
+
function toParallelismReport(settings) {
|
|
1098
|
+
return {
|
|
1099
|
+
requested: settings.requested,
|
|
1100
|
+
effective: settings.effective,
|
|
1101
|
+
repository_max: settings.repositoryMax,
|
|
1102
|
+
cpu_available: settings.cpuAvailable,
|
|
1103
|
+
capped: settings.capped,
|
|
1104
|
+
mode: settings.mode,
|
|
1105
|
+
note: settings.note,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1064
1108
|
function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks, scopeDiffRisks, validationRatchetRisks, reproEvidence, externalChecks) {
|
|
1065
1109
|
const statePaths = createVerifyRunStatePaths(projectRoot);
|
|
1066
1110
|
const receipts = [];
|
|
@@ -1210,7 +1254,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
|
|
|
1210
1254
|
writeJsonFileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), latest);
|
|
1211
1255
|
return outputWithReceiptPaths;
|
|
1212
1256
|
}
|
|
1213
|
-
async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvidence = null, externalChecks = [], parallelism = DEFAULT_VERIFY_PARALLELISM) {
|
|
1257
|
+
async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvidence = null, externalChecks = [], parallelism = DEFAULT_VERIFY_PARALLELISM, parallelismReport = null) {
|
|
1214
1258
|
const contract = readCommandContract(projectRoot);
|
|
1215
1259
|
const report = createChangeVerificationReport(input.classificationReport, contract, projectRoot);
|
|
1216
1260
|
const verificationPlanId = createVerificationPlanId(report, contract);
|
|
@@ -1223,7 +1267,7 @@ async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvi
|
|
|
1223
1267
|
const reproEvidenceRisks = createReproEvidenceRisks(reproEvidence, { verificationPlanId });
|
|
1224
1268
|
const reproEvidenceVerdictEffects = countReproEvidenceVerdictEffects(reproEvidenceRisks);
|
|
1225
1269
|
const externalEvidenceRisks = createExternalEvidenceRisks(externalChecks);
|
|
1226
|
-
const results = await runScheduledVerificationIntents(report, lang, verificationPlanId, scheduledTestTargets, parallelism);
|
|
1270
|
+
const results = await runScheduledVerificationIntents(report, projectRoot, lang, verificationPlanId, scheduledTestTargets, parallelism);
|
|
1227
1271
|
results.push(...createSkippedResults(report.candidates, scheduledIntents, report.gaps));
|
|
1228
1272
|
const summary = summarizeResults(results);
|
|
1229
1273
|
const status = getVerificationStatus(summary);
|
|
@@ -1291,6 +1335,7 @@ async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvi
|
|
|
1291
1335
|
failure_fingerprint: failureFingerprint,
|
|
1292
1336
|
repeated_failure_summary: null,
|
|
1293
1337
|
summary,
|
|
1338
|
+
...(parallelismReport ? { parallelism: parallelismReport } : {}),
|
|
1294
1339
|
...(reproEvidence ? { repro_evidence: reproEvidence } : {}),
|
|
1295
1340
|
...(externalChecks.length > 0 ? { external_checks: externalChecks } : {}),
|
|
1296
1341
|
run_dir: '',
|
|
@@ -1346,9 +1391,15 @@ function renderVerifyOutput(output, lang) {
|
|
|
1346
1391
|
`passed: ${output.summary.passed}`,
|
|
1347
1392
|
`failed: ${output.summary.failed}`,
|
|
1348
1393
|
`skipped: ${output.summary.skipped}`,
|
|
1349
|
-
'',
|
|
1350
|
-
t(lang, 'verify.label.results'),
|
|
1351
1394
|
];
|
|
1395
|
+
if (output.parallelism) {
|
|
1396
|
+
const cpuAvailable = output.parallelism.cpu_available ?? t(lang, 'value.none');
|
|
1397
|
+
lines.push(`parallelism: requested ${output.parallelism.requested}, effective ${output.parallelism.effective}, repository max ${output.parallelism.repository_max}, cpu available ${cpuAvailable}, mode ${output.parallelism.mode}`);
|
|
1398
|
+
if (output.parallelism.capped) {
|
|
1399
|
+
lines.push(`parallelism note: ${output.parallelism.note}`);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
lines.push('', t(lang, 'verify.label.results'));
|
|
1352
1403
|
for (const result of output.results) {
|
|
1353
1404
|
const intent = result.intent ?? t(lang, 'value.none');
|
|
1354
1405
|
const reason = result.reason ? ` (${result.reason})` : '';
|
|
@@ -1466,7 +1517,9 @@ export async function runVerify(args, reporter, lang = 'en') {
|
|
|
1466
1517
|
reporter.stdout(JSON.stringify(await createPlanOnlyOutput(input, projectRoot), null, 2));
|
|
1467
1518
|
return 0;
|
|
1468
1519
|
}
|
|
1469
|
-
const
|
|
1520
|
+
const parallelismSettings = resolveVerifyParallelism(parsed.parallelism ?? DEFAULT_VERIFY_PARALLELISM);
|
|
1521
|
+
const parallelismReport = parsed.parallelismSpecified ? toParallelismReport(parallelismSettings) : null;
|
|
1522
|
+
const output = await createVerifyOutput(input, parsed.fromClassification ?? parsed.fromPlan ?? (parsed.changed ? 'changed' : null), projectRoot, lang, reproEvidence, externalChecks, parallelismSettings.effective, parallelismReport);
|
|
1470
1523
|
if (parsed.json) {
|
|
1471
1524
|
reporter.stdout(JSON.stringify(output, null, 2));
|
|
1472
1525
|
}
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -757,7 +757,7 @@ Read these files before working:
|
|
|
757
757
|
"verify.help.option.writePlan": "Compatibility option that writes the changed-file classification report",
|
|
758
758
|
"verify.help.option.reproEvidence": "Read structured bug-fix reproduction evidence from a repository-local JSON summary",
|
|
759
759
|
"verify.help.option.externalEvidence": "Read lower-authority external CI evidence from a repository-local JSON summary",
|
|
760
|
-
"verify.help.option.parallel": "Run safe non-conflicting schedule batches with up to this many commands; default is 1",
|
|
760
|
+
"verify.help.option.parallel": "Run safe non-conflicting schedule batches with up to this many commands, capped by local limits; default is 1",
|
|
761
761
|
"verify.help.option.planOnly": "Print the verification plan without running commands; requires --json",
|
|
762
762
|
"verify.help.exit.ok": "All selected verification intents passed",
|
|
763
763
|
"verify.help.exit.fail": "Verification failed, was partial, was blocked, or input was invalid",
|
|
@@ -778,6 +778,7 @@ Read these files before working:
|
|
|
778
778
|
"verify.error.plan_root_mismatch": "Classification report must come from this mustflow root",
|
|
779
779
|
"verify.error.missing_plan_reasons": "Classification report must include summary.validationReasons",
|
|
780
780
|
"verify.error.plan_path_outside_root": "Classification report path must stay inside the mustflow root",
|
|
781
|
+
"verify.error.input_path_contains_symlink": "Input file path must not contain symlinks",
|
|
781
782
|
"verify.error.changed_files_unavailable": "Unable to inspect changed files with git status",
|
|
782
783
|
"verify.error.invalid_repro_evidence_file": "Repro evidence must be a readable JSON summary with structured evidence fields",
|
|
783
784
|
"verify.error.unsupported_repro_evidence_source": "Repro evidence input must use command repro-evidence",
|
package/dist/cli/i18n/es.js
CHANGED
|
@@ -757,7 +757,7 @@ Lee estos archivos antes de trabajar:
|
|
|
757
757
|
"verify.help.option.writePlan": "Opción de compatibilidad que escribe el informe de clasificación de cambios",
|
|
758
758
|
"verify.help.option.reproEvidence": "Lee evidencia estructurada de reproducción de errores desde un resumen JSON local del repositorio",
|
|
759
759
|
"verify.help.option.externalEvidence": "Lee evidencia de CI externa de menor autoridad desde un resumen JSON local del repositorio",
|
|
760
|
-
"verify.help.option.parallel": "Ejecuta lotes programados seguros y sin conflictos con hasta esta cantidad de comandos; el valor predeterminado es 1",
|
|
760
|
+
"verify.help.option.parallel": "Ejecuta lotes programados seguros y sin conflictos con hasta esta cantidad de comandos, limitada por topes locales; el valor predeterminado es 1",
|
|
761
761
|
"verify.help.option.planOnly": "Imprime el plan de verificación sin ejecutar comandos; requiere --json",
|
|
762
762
|
"verify.help.exit.ok": "Todas las intenciones de verificación seleccionadas pasaron",
|
|
763
763
|
"verify.help.exit.fail": "La verificación falló, fue parcial, quedó bloqueada o la entrada no fue válida",
|
|
@@ -778,6 +778,7 @@ Lee estos archivos antes de trabajar:
|
|
|
778
778
|
"verify.error.plan_root_mismatch": "El informe de clasificación debe provenir de esta raíz mustflow",
|
|
779
779
|
"verify.error.missing_plan_reasons": "El informe de clasificación debe incluir summary.validationReasons",
|
|
780
780
|
"verify.error.plan_path_outside_root": "La ruta del informe de clasificación debe permanecer dentro de la raíz mustflow",
|
|
781
|
+
"verify.error.input_path_contains_symlink": "La ruta del archivo de entrada no debe contener enlaces simbólicos",
|
|
781
782
|
"verify.error.changed_files_unavailable": "No se pudieron inspeccionar los archivos cambiados con git status",
|
|
782
783
|
"verify.error.invalid_repro_evidence_file": "La evidencia de reproducción debe ser un resumen JSON legible con campos de evidencia estructurados",
|
|
783
784
|
"verify.error.unsupported_repro_evidence_source": "La entrada de evidencia de reproducción debe usar command repro-evidence",
|
package/dist/cli/i18n/fr.js
CHANGED
|
@@ -757,7 +757,7 @@ Lisez ces fichiers avant de travailler :
|
|
|
757
757
|
"verify.help.option.writePlan": "Option de compatibilité qui écrit le rapport de classification des changements",
|
|
758
758
|
"verify.help.option.reproEvidence": "Lit une preuve structurée de reproduction de bogue depuis un résumé JSON local au dépôt",
|
|
759
759
|
"verify.help.option.externalEvidence": "Lit une preuve CI externe de moindre autorité depuis un résumé JSON local au dépôt",
|
|
760
|
-
"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",
|
|
760
|
+
"verify.help.option.parallel": "Exécute les lots planifiés sûrs et sans conflit avec au plus ce nombre de commandes, plafonné par les limites locales ; valeur par défaut : 1",
|
|
761
761
|
"verify.help.option.planOnly": "Affiche le plan de vérification sans exécuter de commandes; nécessite --json",
|
|
762
762
|
"verify.help.exit.ok": "Toutes les intentions de vérification sélectionnées ont réussi",
|
|
763
763
|
"verify.help.exit.fail": "La vérification a échoué, est partielle, est bloquée ou l'entrée est invalide",
|
|
@@ -778,6 +778,7 @@ Lisez ces fichiers avant de travailler :
|
|
|
778
778
|
"verify.error.plan_root_mismatch": "Le rapport de classification doit venir de cette racine mustflow",
|
|
779
779
|
"verify.error.missing_plan_reasons": "Le rapport de classification doit inclure summary.validationReasons",
|
|
780
780
|
"verify.error.plan_path_outside_root": "Le chemin du rapport de classification doit rester dans la racine mustflow",
|
|
781
|
+
"verify.error.input_path_contains_symlink": "Le chemin du fichier d'entrée ne doit pas contenir de liens symboliques",
|
|
781
782
|
"verify.error.changed_files_unavailable": "Impossible d'inspecter les fichiers modifies avec git status",
|
|
782
783
|
"verify.error.invalid_repro_evidence_file": "La preuve de reproduction doit être un résumé JSON lisible avec des champs de preuve structurés",
|
|
783
784
|
"verify.error.unsupported_repro_evidence_source": "L'entrée de preuve de reproduction doit utiliser command repro-evidence",
|
package/dist/cli/i18n/hi.js
CHANGED
|
@@ -757,7 +757,7 @@ export const hiMessages = {
|
|
|
757
757
|
"verify.help.option.writePlan": "Changed-file classification report लिखने वाला compatibility option",
|
|
758
758
|
"verify.help.option.reproEvidence": "Repository-local JSON summary से structured bug reproduction evidence पढ़ें",
|
|
759
759
|
"verify.help.option.externalEvidence": "Repository-local JSON summary से lower-authority external CI evidence पढ़ें",
|
|
760
|
-
"verify.help.option.parallel": "Safe और non-conflicting schedule batches को इतने commands तक साथ
|
|
760
|
+
"verify.help.option.parallel": "Safe और non-conflicting schedule batches को इतने commands तक साथ चलाएं, local limits से capped; default 1 है",
|
|
761
761
|
"verify.help.option.planOnly": "Commands चलाए बिना verification plan print करें; --json चाहिए",
|
|
762
762
|
"verify.help.exit.ok": "सभी selected verification intents pass हुए",
|
|
763
763
|
"verify.help.exit.fail": "Verification fail हुआ, partial रहा, blocked रहा, या input invalid था",
|
|
@@ -778,6 +778,7 @@ export const hiMessages = {
|
|
|
778
778
|
"verify.error.plan_root_mismatch": "Classification report इसी mustflow root से आना चाहिए",
|
|
779
779
|
"verify.error.missing_plan_reasons": "Classification report में summary.validationReasons होना चाहिए",
|
|
780
780
|
"verify.error.plan_path_outside_root": "Classification report path mustflow root के अंदर रहना चाहिए",
|
|
781
|
+
"verify.error.input_path_contains_symlink": "Input file path में symbolic link नहीं होना चाहिए",
|
|
781
782
|
"verify.error.changed_files_unavailable": "git status से बदली फ़ाइलें नहीं पढ़ी जा सकीं",
|
|
782
783
|
"verify.error.invalid_repro_evidence_file": "Repro evidence structured evidence fields वाला readable JSON summary होना चाहिए",
|
|
783
784
|
"verify.error.unsupported_repro_evidence_source": "Repro evidence input को command repro-evidence इस्तेमाल करना चाहिए",
|
package/dist/cli/i18n/ko.js
CHANGED
|
@@ -757,7 +757,7 @@ export const koMessages = {
|
|
|
757
757
|
"verify.help.option.writePlan": "변경 파일 분류 보고서를 쓰는 호환 옵션입니다",
|
|
758
758
|
"verify.help.option.reproEvidence": "저장소 안의 JSON 요약에서 구조화된 버그 재현 증거를 읽습니다",
|
|
759
759
|
"verify.help.option.externalEvidence": "저장소 안의 JSON 요약에서 낮은 권한의 외부 CI 증거를 읽습니다",
|
|
760
|
-
"verify.help.option.parallel": "안전하고 서로 충돌하지 않는 예정 실행 묶음을 이 개수까지 함께
|
|
760
|
+
"verify.help.option.parallel": "안전하고 서로 충돌하지 않는 예정 실행 묶음을 이 개수까지 함께 실행하되, 로컬 상한을 적용합니다. 기본값은 1입니다",
|
|
761
761
|
"verify.help.option.planOnly": "명령을 실행하지 않고 검증 계획만 출력합니다. --json이 필요합니다",
|
|
762
762
|
"verify.help.exit.ok": "선택된 모든 검증 의도가 통과했습니다",
|
|
763
763
|
"verify.help.exit.fail": "검증이 실패했거나, 일부만 실행됐거나, 막혔거나, 입력이 올바르지 않습니다",
|
|
@@ -778,6 +778,7 @@ export const koMessages = {
|
|
|
778
778
|
"verify.error.plan_root_mismatch": "분류 보고서는 현재 mustflow 루트에서 나온 것이어야 합니다",
|
|
779
779
|
"verify.error.missing_plan_reasons": "분류 보고서에는 summary.validationReasons가 있어야 합니다",
|
|
780
780
|
"verify.error.plan_path_outside_root": "분류 보고서 경로는 mustflow 루트 안에 있어야 합니다",
|
|
781
|
+
"verify.error.input_path_contains_symlink": "입력 파일 경로에는 심볼릭 링크가 포함되면 안 됩니다",
|
|
781
782
|
"verify.error.changed_files_unavailable": "git status로 변경 파일을 확인할 수 없습니다",
|
|
782
783
|
"verify.error.invalid_repro_evidence_file": "재현 증거는 구조화된 증거 필드를 포함한 읽을 수 있는 JSON 요약이어야 합니다",
|
|
783
784
|
"verify.error.unsupported_repro_evidence_source": "재현 증거 입력은 command repro-evidence를 사용해야 합니다",
|
package/dist/cli/i18n/zh.js
CHANGED
|
@@ -757,7 +757,7 @@ export const zhMessages = {
|
|
|
757
757
|
"verify.help.option.writePlan": "写入变更文件分类报告的兼容选项",
|
|
758
758
|
"verify.help.option.reproEvidence": "从仓库本地 JSON 摘要读取结构化的 bug 复现证据",
|
|
759
759
|
"verify.help.option.externalEvidence": "从仓库本地 JSON 摘要读取低权限外部 CI 证据",
|
|
760
|
-
"verify.help.option.parallel": "
|
|
760
|
+
"verify.help.option.parallel": "最多并行执行这个数量的安全、无冲突计划批次命令,并受本地上限限制;默认值为 1",
|
|
761
761
|
"verify.help.option.planOnly": "仅输出验证计划,不执行命令;需要 --json",
|
|
762
762
|
"verify.help.exit.ok": "选中的所有验证意图均已通过",
|
|
763
763
|
"verify.help.exit.fail": "验证失败、部分完成、被阻止,或输入无效",
|
|
@@ -778,6 +778,7 @@ export const zhMessages = {
|
|
|
778
778
|
"verify.error.plan_root_mismatch": "分类报告必须来自当前 mustflow 根目录",
|
|
779
779
|
"verify.error.missing_plan_reasons": "分类报告必须包含 summary.validationReasons",
|
|
780
780
|
"verify.error.plan_path_outside_root": "分类报告路径必须位于 mustflow 根目录内",
|
|
781
|
+
"verify.error.input_path_contains_symlink": "输入文件路径不得包含符号链接",
|
|
781
782
|
"verify.error.changed_files_unavailable": "无法通过 git status 检查变更文件",
|
|
782
783
|
"verify.error.invalid_repro_evidence_file": "复现证据必须是包含结构化证据字段的可读取 JSON 摘要",
|
|
783
784
|
"verify.error.unsupported_repro_evidence_source": "复现证据输入必须使用 command repro-evidence",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
export { ensureFileTargetInsideWithoutSymlinks, ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks, readUtf8FileInsideWithoutSymlinks, writeFileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks, } from '../../core/safe-filesystem.js';
|
|
4
4
|
import { readFileInsideWithoutSymlinks, writeFileInsideWithoutSymlinks, } from '../../core/safe-filesystem.js';
|
|
@@ -9,12 +9,35 @@ export function copyFileInsideWithoutSymlinks(sourceParentPath, sourcePath, targ
|
|
|
9
9
|
const content = readFileInsideWithoutSymlinks(sourceParentPath, sourcePath);
|
|
10
10
|
writeFileInsideWithoutSymlinks(targetParentPath, targetPath, content);
|
|
11
11
|
}
|
|
12
|
+
function pathExistsWithoutFollowingLeaf(filePath) {
|
|
13
|
+
try {
|
|
14
|
+
lstatSync(filePath);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function nearestExistingAncestor(filePath) {
|
|
25
|
+
let currentPath = path.resolve(path.dirname(filePath));
|
|
26
|
+
while (!pathExistsWithoutFollowingLeaf(currentPath)) {
|
|
27
|
+
const parentPath = path.dirname(currentPath);
|
|
28
|
+
if (parentPath === currentPath) {
|
|
29
|
+
throw new Error(`No existing parent directory for path: ${filePath}`);
|
|
30
|
+
}
|
|
31
|
+
currentPath = parentPath;
|
|
32
|
+
}
|
|
33
|
+
return currentPath;
|
|
34
|
+
}
|
|
12
35
|
export function copyFileIfMissing(sourcePath, targetPath, relativePath) {
|
|
13
|
-
if (
|
|
36
|
+
if (pathExistsWithoutFollowingLeaf(targetPath)) {
|
|
14
37
|
return { status: 'skipped', relativePath };
|
|
15
38
|
}
|
|
16
|
-
|
|
17
|
-
|
|
39
|
+
const content = readFileInsideWithoutSymlinks(path.dirname(sourcePath), sourcePath);
|
|
40
|
+
writeFileInsideWithoutSymlinks(nearestExistingAncestor(targetPath), targetPath, content);
|
|
18
41
|
return { status: 'created', relativePath };
|
|
19
42
|
}
|
|
20
43
|
export function listFilesRecursive(rootPath, options = {}) {
|
|
@@ -310,6 +310,15 @@ function readIndexedFileMetadataRecord(projectRoot, relativePath, sourceScope) {
|
|
|
310
310
|
mtimeMs: Math.round(stats.mtimeMs),
|
|
311
311
|
};
|
|
312
312
|
}
|
|
313
|
+
function hashIndexedFileMetadataRecord(projectRoot, metadata) {
|
|
314
|
+
return {
|
|
315
|
+
...metadata,
|
|
316
|
+
contentHash: sha256Bytes(readFileSync(path.join(projectRoot, ...metadata.path.split('/')))),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function hashIndexedFileMetadataRecords(projectRoot, metadataRecords) {
|
|
320
|
+
return metadataRecords.map((metadata) => hashIndexedFileMetadataRecord(projectRoot, metadata));
|
|
321
|
+
}
|
|
313
322
|
function collectIndexedFileRecords(projectRoot, documents, sourceAnchors, sourceAnchorCandidatePaths = []) {
|
|
314
323
|
const records = new Map();
|
|
315
324
|
for (const document of documents) {
|
|
@@ -332,16 +341,16 @@ function collectSourceAnchorCandidatePaths(projectRoot, sourceConfig) {
|
|
|
332
341
|
excludeGeneratedOrVendor: true,
|
|
333
342
|
});
|
|
334
343
|
}
|
|
335
|
-
function
|
|
344
|
+
function collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource, sourceConfig) {
|
|
336
345
|
const records = new Map();
|
|
337
346
|
for (const relativePath of getExistingIndexablePaths(projectRoot)) {
|
|
338
|
-
records.set(relativePath,
|
|
347
|
+
records.set(relativePath, readIndexedFileMetadataRecord(projectRoot, relativePath, 'workflow'));
|
|
339
348
|
}
|
|
340
349
|
if (includeSource) {
|
|
341
350
|
try {
|
|
342
351
|
for (const sourcePath of collectSourceAnchorCandidatePaths(projectRoot, sourceConfig)) {
|
|
343
352
|
if (!records.has(sourcePath)) {
|
|
344
|
-
records.set(sourcePath,
|
|
353
|
+
records.set(sourcePath, readIndexedFileMetadataRecord(projectRoot, sourcePath, 'source_anchor'));
|
|
345
354
|
}
|
|
346
355
|
}
|
|
347
356
|
}
|
|
@@ -350,7 +359,7 @@ function collectFastPreflightIndexedFileRecords(projectRoot, includeSource, sour
|
|
|
350
359
|
}
|
|
351
360
|
}
|
|
352
361
|
if (existsSync(path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/')))) {
|
|
353
|
-
records.set(LATEST_RUN_STATE_RELATIVE_PATH,
|
|
362
|
+
records.set(LATEST_RUN_STATE_RELATIVE_PATH, readIndexedFileMetadataRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
|
|
354
363
|
}
|
|
355
364
|
return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
356
365
|
}
|
|
@@ -2074,6 +2083,27 @@ function indexedFilesMatch(database, currentFiles) {
|
|
|
2074
2083
|
}
|
|
2075
2084
|
return true;
|
|
2076
2085
|
}
|
|
2086
|
+
function indexedFileMetadataMatch(database, currentFiles) {
|
|
2087
|
+
const rows = queryRows(database, 'SELECT path, source_scope, size_bytes, mtime_ms, parser_version FROM indexed_files ORDER BY path');
|
|
2088
|
+
if (rows.length !== currentFiles.length) {
|
|
2089
|
+
return false;
|
|
2090
|
+
}
|
|
2091
|
+
const currentByPath = new Map(currentFiles.map((file) => [file.path, file]));
|
|
2092
|
+
for (const row of rows) {
|
|
2093
|
+
const storedPath = toSearchString(row.path);
|
|
2094
|
+
const current = currentByPath.get(storedPath);
|
|
2095
|
+
if (!current) {
|
|
2096
|
+
return false;
|
|
2097
|
+
}
|
|
2098
|
+
if (normalizeIndexedFileSourceScope(row.source_scope) !== current.sourceScope ||
|
|
2099
|
+
toNullableNumber(row.size_bytes) !== current.sizeBytes ||
|
|
2100
|
+
toNullableNumber(row.mtime_ms) !== current.mtimeMs ||
|
|
2101
|
+
toSearchString(row.parser_version) !== LOCAL_INDEX_PARSER_VERSION) {
|
|
2102
|
+
return false;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
return true;
|
|
2106
|
+
}
|
|
2077
2107
|
async function readIncrementalPreflightReuse(SQL, databasePath, projectRoot, currentFiles, sourceScopeHash, dryRun, indexMode) {
|
|
2078
2108
|
if (!currentFiles) {
|
|
2079
2109
|
return { result: null, rebuildReason: null };
|
|
@@ -2096,7 +2126,11 @@ async function readIncrementalPreflightReuse(SQL, databasePath, projectRoot, cur
|
|
|
2096
2126
|
if (!hasTable(database, 'indexed_files')) {
|
|
2097
2127
|
return { result: null, rebuildReason: 'indexed_files_missing' };
|
|
2098
2128
|
}
|
|
2099
|
-
if (!
|
|
2129
|
+
if (!indexedFileMetadataMatch(database, currentFiles)) {
|
|
2130
|
+
return { result: null, rebuildReason: 'file_fingerprint_mismatch' };
|
|
2131
|
+
}
|
|
2132
|
+
const hashedCurrentFiles = hashIndexedFileMetadataRecords(projectRoot, currentFiles);
|
|
2133
|
+
if (!indexedFilesMatch(database, hashedCurrentFiles)) {
|
|
2100
2134
|
return { result: null, rebuildReason: 'file_fingerprint_mismatch' };
|
|
2101
2135
|
}
|
|
2102
2136
|
const capabilities = readStoredSearchCapabilities(database);
|
|
@@ -2170,7 +2204,7 @@ export async function createLocalIndex(projectRoot, options = {}) {
|
|
|
2170
2204
|
capabilities = detectLocalSearchCapabilities(capabilityDatabase);
|
|
2171
2205
|
capabilityDatabase.close();
|
|
2172
2206
|
if (incremental) {
|
|
2173
|
-
const preflightFiles =
|
|
2207
|
+
const preflightFiles = collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource, sourceConfig);
|
|
2174
2208
|
const preflightReuse = await readIncrementalPreflightReuse(SQL, databasePath, projectRoot, preflightFiles, sourceScopeHash, dryRun, indexMode);
|
|
2175
2209
|
if (preflightReuse.result) {
|
|
2176
2210
|
return preflightReuse.result;
|
|
@@ -2828,8 +2862,8 @@ function getSectionHeadings(database, documentPath) {
|
|
|
2828
2862
|
function getDocumentTerms(database, documentPath) {
|
|
2829
2863
|
return queryRows(database, 'SELECT term FROM document_terms WHERE document_path = ? ORDER BY term', [documentPath]).map((row) => toSearchString(row.term));
|
|
2830
2864
|
}
|
|
2831
|
-
function
|
|
2832
|
-
return
|
|
2865
|
+
function commandEffectFromRow(row) {
|
|
2866
|
+
return {
|
|
2833
2867
|
intent: toSearchString(row.intent),
|
|
2834
2868
|
source: toSearchString(row.source),
|
|
2835
2869
|
access: toSearchString(row.access),
|
|
@@ -2837,7 +2871,35 @@ function getCommandEffects(database, intent) {
|
|
|
2837
2871
|
path: row.path === null || row.path === undefined ? null : toSearchString(row.path),
|
|
2838
2872
|
lock: toSearchString(row.lock),
|
|
2839
2873
|
concurrency: toSearchString(row.concurrency),
|
|
2840
|
-
}
|
|
2874
|
+
};
|
|
2875
|
+
}
|
|
2876
|
+
function sqlPlaceholders(values) {
|
|
2877
|
+
return values.map(() => '?').join(', ');
|
|
2878
|
+
}
|
|
2879
|
+
function queryCandidateRows(database, sql, keyColumn, candidates, indexedMatches) {
|
|
2880
|
+
if (!indexedMatches.active || candidates.size === 0) {
|
|
2881
|
+
return queryRows(database, sql);
|
|
2882
|
+
}
|
|
2883
|
+
const keys = [...candidates].sort((left, right) => left.localeCompare(right));
|
|
2884
|
+
return queryRows(database, `${sql} WHERE ${keyColumn} IN (${sqlPlaceholders(keys)})`, keys);
|
|
2885
|
+
}
|
|
2886
|
+
function getCommandEffectsByIntent(database, intents) {
|
|
2887
|
+
const uniqueIntents = [...new Set(intents)].sort((left, right) => left.localeCompare(right));
|
|
2888
|
+
const effectsByIntent = new Map(uniqueIntents.map((intent) => [intent, []]));
|
|
2889
|
+
if (uniqueIntents.length === 0) {
|
|
2890
|
+
return effectsByIntent;
|
|
2891
|
+
}
|
|
2892
|
+
for (const row of queryRows(database, `SELECT intent, source, access, mode, path, lock, concurrency
|
|
2893
|
+
FROM command_effects
|
|
2894
|
+
WHERE intent IN (${sqlPlaceholders(uniqueIntents)})
|
|
2895
|
+
ORDER BY intent, lock, path, mode`, uniqueIntents)) {
|
|
2896
|
+
const effect = commandEffectFromRow(row);
|
|
2897
|
+
const effects = effectsByIntent.get(effect.intent);
|
|
2898
|
+
if (effects) {
|
|
2899
|
+
effects.push(effect);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
return effectsByIntent;
|
|
2841
2903
|
}
|
|
2842
2904
|
const EMPTY_INDEXED_SEARCH_MATCHES = {
|
|
2843
2905
|
active: false,
|
|
@@ -2921,11 +2983,11 @@ function getIndexedSearchMatches(database, query) {
|
|
|
2921
2983
|
}
|
|
2922
2984
|
}
|
|
2923
2985
|
function matchesIndexedOrTableScan(fields, query, indexedMatches, matchSet, key) {
|
|
2924
|
-
return
|
|
2986
|
+
return indexedMatches.active && matchSet.size > 0 ? matchSet.has(key) : isMatched(fields, query);
|
|
2925
2987
|
}
|
|
2926
2988
|
function scoreIndexedOrTableScan(primaryFields, secondaryFields, query, indexedMatches, matchSet, key) {
|
|
2927
2989
|
const tableScore = scoreMatch(primaryFields, secondaryFields, query);
|
|
2928
|
-
return indexedMatches.active && matchSet.has(key) ? Math.max(tableScore, 20) : tableScore;
|
|
2990
|
+
return indexedMatches.active && matchSet.size > 0 && matchSet.has(key) ? Math.max(tableScore, 20) : tableScore;
|
|
2929
2991
|
}
|
|
2930
2992
|
/**
|
|
2931
2993
|
* mf:anchor cli.search.local-index
|
|
@@ -2958,7 +3020,7 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
|
|
|
2958
3020
|
throw new Error(`Local mustflow index is stale: ${stalePaths.join(', ')}. Run \`mf index\` before searching. Refresh command: mf index`);
|
|
2959
3021
|
}
|
|
2960
3022
|
if (scope === 'workflow' || scope === 'all') {
|
|
2961
|
-
for (const row of
|
|
3023
|
+
for (const row of queryCandidateRows(database, 'SELECT path, type, title, content_snippet FROM documents', 'path', indexedMatches.documents, indexedMatches)) {
|
|
2962
3024
|
const pathValue = toSearchString(row.path);
|
|
2963
3025
|
const typeValue = toSearchString(row.type);
|
|
2964
3026
|
const title = toSearchString(row.title);
|
|
@@ -2981,7 +3043,7 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
|
|
|
2981
3043
|
score: scoreIndexedOrTableScan(primaryFields, secondaryFields, normalizedQuery, indexedMatches, indexedMatches.documents, pathValue),
|
|
2982
3044
|
}, cacheLayers));
|
|
2983
3045
|
}
|
|
2984
|
-
for (const row of
|
|
3046
|
+
for (const row of queryCandidateRows(database, 'SELECT name, path, title FROM skills', 'name', indexedMatches.skills, indexedMatches)) {
|
|
2985
3047
|
const name = toSearchString(row.name);
|
|
2986
3048
|
const pathValue = toSearchString(row.path);
|
|
2987
3049
|
const title = toSearchString(row.title);
|
|
@@ -2999,7 +3061,8 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
|
|
|
2999
3061
|
score: scoreIndexedOrTableScan([name, pathValue, title], [], normalizedQuery, indexedMatches, indexedMatches.skills, name),
|
|
3000
3062
|
}, cacheLayers));
|
|
3001
3063
|
}
|
|
3002
|
-
|
|
3064
|
+
const matchedSkillRouteNames = new Set([...indexedMatches.skillRoutes].map((routeKey) => routeKey.split('\u0000')[0] ?? ''));
|
|
3065
|
+
for (const row of queryCandidateRows(database, 'SELECT skill_name, skill_path, trigger, required_input, edit_scope, risk, verification_intents, expected_output FROM skill_routes', 'skill_name', matchedSkillRouteNames, indexedMatches)) {
|
|
3003
3066
|
const name = toSearchString(row.skill_name);
|
|
3004
3067
|
const pathValue = toSearchString(row.skill_path);
|
|
3005
3068
|
const trigger = toSearchString(row.trigger);
|
|
@@ -3012,7 +3075,8 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
|
|
|
3012
3075
|
const secondaryFields = [pathValue, requiredInput, editScope, risk, expectedOutput];
|
|
3013
3076
|
const fields = [...primaryFields, ...secondaryFields];
|
|
3014
3077
|
const routeKey = skillRouteKey({ skillName: name, trigger });
|
|
3015
|
-
|
|
3078
|
+
const indexedRouteMatch = indexedMatches.active && indexedMatches.skillRoutes.has(routeKey);
|
|
3079
|
+
if (!indexedRouteMatch && !isMatched(fields, normalizedQuery)) {
|
|
3016
3080
|
continue;
|
|
3017
3081
|
}
|
|
3018
3082
|
results.push(withCacheHint({
|
|
@@ -3025,16 +3089,20 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
|
|
|
3025
3089
|
verification_intents: verificationIntents,
|
|
3026
3090
|
...skillAuthority(),
|
|
3027
3091
|
match: getMatchSnippet(fields, normalizedQuery),
|
|
3028
|
-
score:
|
|
3092
|
+
score: indexedRouteMatch
|
|
3093
|
+
? Math.max(scoreMatch(primaryFields, secondaryFields, normalizedQuery), 20)
|
|
3094
|
+
: scoreMatch(primaryFields, secondaryFields, normalizedQuery),
|
|
3029
3095
|
}, cacheLayers));
|
|
3030
3096
|
}
|
|
3031
|
-
|
|
3097
|
+
const commandRows = queryCandidateRows(database, 'SELECT name, status, lifecycle, run_policy, description FROM command_intents', 'name', indexedMatches.commandIntents, indexedMatches);
|
|
3098
|
+
const effectsByIntent = getCommandEffectsByIntent(database, commandRows.map((row) => toSearchString(row.name)));
|
|
3099
|
+
for (const row of commandRows) {
|
|
3032
3100
|
const name = toSearchString(row.name);
|
|
3033
3101
|
const status = toSearchString(row.status);
|
|
3034
3102
|
const lifecycle = toSearchString(row.lifecycle);
|
|
3035
3103
|
const runPolicy = toSearchString(row.run_policy);
|
|
3036
3104
|
const description = toSearchString(row.description);
|
|
3037
|
-
const effects =
|
|
3105
|
+
const effects = effectsByIntent.get(name) ?? [];
|
|
3038
3106
|
const effectLocks = [...new Set(effects.map((effect) => effect.lock))].sort((left, right) => left.localeCompare(right));
|
|
3039
3107
|
const effectPaths = [
|
|
3040
3108
|
...new Set(effects.map((effect) => effect.path).filter((effectPath) => effectPath !== null)),
|
|
@@ -3060,7 +3128,7 @@ export async function searchLocalIndex(projectRoot, query, options = {}) {
|
|
|
3060
3128
|
}
|
|
3061
3129
|
}
|
|
3062
3130
|
if (scope === 'source' || scope === 'all') {
|
|
3063
|
-
for (const row of
|
|
3131
|
+
for (const row of queryCandidateRows(database, 'SELECT source_anchors.id, path, line_start, purpose, search_terms, invariant, risk, source_anchors.navigation_only, source_anchors.can_instruct_agent, status, confidence FROM source_anchors LEFT JOIN source_anchor_status ON source_anchor_status.anchor_id = source_anchors.id', 'source_anchors.id', indexedMatches.sourceAnchors, indexedMatches)) {
|
|
3064
3132
|
const id = toSearchString(row.id);
|
|
3065
3133
|
const pathValue = toSearchString(row.path);
|
|
3066
3134
|
const purpose = toSearchString(row.purpose);
|
|
@@ -186,6 +186,26 @@ function truncatePaths(paths) {
|
|
|
186
186
|
}
|
|
187
187
|
return { paths: paths.slice(0, MAX_REPORTED_PATHS), truncated: true };
|
|
188
188
|
}
|
|
189
|
+
function uniqueSortedPaths(paths) {
|
|
190
|
+
return [...new Set([...paths].map(normalizeRelativePath))].sort((left, right) => left.localeCompare(right));
|
|
191
|
+
}
|
|
192
|
+
function pathsCoverObservedPath(declaredPaths, observedPath) {
|
|
193
|
+
return declaredPaths.some((declaredPath) => declaredPathCoversObservedPath(declaredPath, observedPath));
|
|
194
|
+
}
|
|
195
|
+
function createUnavailableWriteDriftReceipt(declaredPaths, reason) {
|
|
196
|
+
return {
|
|
197
|
+
status: 'unavailable',
|
|
198
|
+
declared_paths: declaredPaths,
|
|
199
|
+
observed_paths: [],
|
|
200
|
+
declared_observed_paths: [],
|
|
201
|
+
undeclared_paths: [],
|
|
202
|
+
observed_count: 0,
|
|
203
|
+
undeclared_count: 0,
|
|
204
|
+
has_undeclared_changes: false,
|
|
205
|
+
truncated: false,
|
|
206
|
+
reason,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
189
209
|
export function startRunWriteTracking(projectRoot, contract, intentName, options = {}) {
|
|
190
210
|
const declaredPaths = [
|
|
191
211
|
...listDeclaredWritePaths(projectRoot, contract, intentName),
|
|
@@ -197,35 +217,101 @@ export function startRunWriteTracking(projectRoot, contract, intentName, options
|
|
|
197
217
|
before: captureSnapshot(projectRoot),
|
|
198
218
|
};
|
|
199
219
|
}
|
|
220
|
+
export function startRunWriteBatchTracking(projectRoot) {
|
|
221
|
+
return {
|
|
222
|
+
projectRoot,
|
|
223
|
+
before: captureSnapshot(projectRoot),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
export function finishRunWriteBatchTracking(tracker, intents) {
|
|
227
|
+
const chunkIntents = intents.map((intent) => intent.intentName).sort((left, right) => left.localeCompare(right));
|
|
228
|
+
const fallbackReceipts = new Map();
|
|
229
|
+
for (const intent of intents) {
|
|
230
|
+
fallbackReceipts.set(intent.intentName, createUnavailableWriteDriftReceipt(uniqueSortedPaths(intent.declaredPaths), tracker.before.reason));
|
|
231
|
+
}
|
|
232
|
+
if (tracker.before.status === 'unavailable') {
|
|
233
|
+
return fallbackReceipts;
|
|
234
|
+
}
|
|
235
|
+
const after = captureSnapshot(tracker.projectRoot);
|
|
236
|
+
if (after.status === 'unavailable') {
|
|
237
|
+
return new Map(intents.map((intent) => [
|
|
238
|
+
intent.intentName,
|
|
239
|
+
createUnavailableWriteDriftReceipt(uniqueSortedPaths(intent.declaredPaths), after.reason),
|
|
240
|
+
]));
|
|
241
|
+
}
|
|
242
|
+
const observedPaths = listObservedChangedPaths(tracker.before.entries, after.entries);
|
|
243
|
+
const declaredObservedByIntent = new Map();
|
|
244
|
+
const undeclaredByIntent = new Map();
|
|
245
|
+
const ambiguousByIntent = new Map();
|
|
246
|
+
for (const intent of intents) {
|
|
247
|
+
declaredObservedByIntent.set(intent.intentName, []);
|
|
248
|
+
undeclaredByIntent.set(intent.intentName, []);
|
|
249
|
+
ambiguousByIntent.set(intent.intentName, []);
|
|
250
|
+
}
|
|
251
|
+
for (const observedPath of observedPaths) {
|
|
252
|
+
const declaredOwners = intents.filter((intent) => pathsCoverObservedPath(intent.declaredPaths, observedPath));
|
|
253
|
+
if (declaredOwners.length === 1) {
|
|
254
|
+
declaredObservedByIntent.get(declaredOwners[0]?.intentName ?? '')?.push(observedPath);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (declaredOwners.length > 1) {
|
|
258
|
+
for (const owner of declaredOwners) {
|
|
259
|
+
ambiguousByIntent.get(owner.intentName)?.push(observedPath);
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const observedWitnesses = intents.filter((intent) => intent.observedPaths.some((intentObservedPath) => pathKey(intentObservedPath) === pathKey(observedPath)));
|
|
264
|
+
if (observedWitnesses.length === 1) {
|
|
265
|
+
undeclaredByIntent.get(observedWitnesses[0]?.intentName ?? '')?.push(observedPath);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const ambiguousTargets = observedWitnesses.length > 0 ? observedWitnesses : intents;
|
|
269
|
+
for (const intent of ambiguousTargets) {
|
|
270
|
+
ambiguousByIntent.get(intent.intentName)?.push(observedPath);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const status = tracker.before.status === 'partial' || after.status === 'partial' ? 'partial' : 'checked';
|
|
274
|
+
const reason = status === 'partial'
|
|
275
|
+
? tracker.before.reason ?? after.reason ?? 'partial_snapshot'
|
|
276
|
+
: null;
|
|
277
|
+
const receipts = new Map();
|
|
278
|
+
for (const intent of intents) {
|
|
279
|
+
const declaredPaths = uniqueSortedPaths(intent.declaredPaths);
|
|
280
|
+
const declaredObservedPaths = uniqueSortedPaths(declaredObservedByIntent.get(intent.intentName) ?? []);
|
|
281
|
+
const undeclaredPaths = uniqueSortedPaths(undeclaredByIntent.get(intent.intentName) ?? []);
|
|
282
|
+
const ambiguousPaths = uniqueSortedPaths(ambiguousByIntent.get(intent.intentName) ?? []);
|
|
283
|
+
const intentObservedPaths = uniqueSortedPaths([...declaredObservedPaths, ...undeclaredPaths, ...ambiguousPaths]);
|
|
284
|
+
const observed = truncatePaths(intentObservedPaths);
|
|
285
|
+
const declaredObserved = truncatePaths(declaredObservedPaths);
|
|
286
|
+
const undeclared = truncatePaths(undeclaredPaths);
|
|
287
|
+
const ambiguous = truncatePaths(ambiguousPaths);
|
|
288
|
+
receipts.set(intent.intentName, {
|
|
289
|
+
status,
|
|
290
|
+
declared_paths: declaredPaths,
|
|
291
|
+
observed_paths: observed.paths,
|
|
292
|
+
declared_observed_paths: declaredObserved.paths,
|
|
293
|
+
undeclared_paths: undeclared.paths,
|
|
294
|
+
observed_count: intentObservedPaths.length,
|
|
295
|
+
undeclared_count: undeclaredPaths.length,
|
|
296
|
+
has_undeclared_changes: undeclaredPaths.length > 0 || ambiguousPaths.length > 0,
|
|
297
|
+
truncated: observed.truncated || declaredObserved.truncated || undeclared.truncated || ambiguous.truncated,
|
|
298
|
+
reason,
|
|
299
|
+
attribution_mode: 'parallel_chunk',
|
|
300
|
+
chunk_intents: chunkIntents,
|
|
301
|
+
attributed_paths: uniqueSortedPaths([...declaredObservedPaths, ...undeclaredPaths]),
|
|
302
|
+
ambiguous_paths: ambiguous.paths,
|
|
303
|
+
ambiguous_count: ambiguousPaths.length,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return receipts;
|
|
307
|
+
}
|
|
200
308
|
export function finishRunWriteTracking(tracker) {
|
|
201
309
|
if (tracker.before.status === 'unavailable') {
|
|
202
|
-
return
|
|
203
|
-
status: 'unavailable',
|
|
204
|
-
declared_paths: tracker.declaredPaths,
|
|
205
|
-
observed_paths: [],
|
|
206
|
-
declared_observed_paths: [],
|
|
207
|
-
undeclared_paths: [],
|
|
208
|
-
observed_count: 0,
|
|
209
|
-
undeclared_count: 0,
|
|
210
|
-
has_undeclared_changes: false,
|
|
211
|
-
truncated: false,
|
|
212
|
-
reason: tracker.before.reason,
|
|
213
|
-
};
|
|
310
|
+
return createUnavailableWriteDriftReceipt(tracker.declaredPaths, tracker.before.reason);
|
|
214
311
|
}
|
|
215
312
|
const after = captureSnapshot(tracker.projectRoot);
|
|
216
313
|
if (after.status === 'unavailable') {
|
|
217
|
-
return
|
|
218
|
-
status: 'unavailable',
|
|
219
|
-
declared_paths: tracker.declaredPaths,
|
|
220
|
-
observed_paths: [],
|
|
221
|
-
declared_observed_paths: [],
|
|
222
|
-
undeclared_paths: [],
|
|
223
|
-
observed_count: 0,
|
|
224
|
-
undeclared_count: 0,
|
|
225
|
-
has_undeclared_changes: false,
|
|
226
|
-
truncated: false,
|
|
227
|
-
reason: after.reason,
|
|
228
|
-
};
|
|
314
|
+
return createUnavailableWriteDriftReceipt(tracker.declaredPaths, after.reason);
|
|
229
315
|
}
|
|
230
316
|
const observedPaths = listObservedChangedPaths(tracker.before.entries, after.entries);
|
|
231
317
|
const declaredObservedPaths = observedPaths.filter((observedPath) => tracker.declaredPaths.some((declaredPath) => declaredPathCoversObservedPath(declaredPath, observedPath)));
|
|
@@ -180,7 +180,7 @@ export function createVerificationSchedule(projectRoot, commandContract, candida
|
|
|
180
180
|
'Only entries backed by explicit effects are marked parallel eligible; writes fallback remains serial-only.',
|
|
181
181
|
...uniqueSorted(latestUndeclaredWriteIntents).map((intent) => `Latest receipt for ${intent} reported undeclared writes; it is not parallel eligible.`),
|
|
182
182
|
'If a future parallel batch has already started, let it finish and stop before the next batch on failure.',
|
|
183
|
-
'mf verify
|
|
183
|
+
'The runner names the default copied-command path; mf verify --parallel may execute eligible entries in bounded chunks and writes the latest run summary after each batch completes.',
|
|
184
184
|
],
|
|
185
185
|
};
|
|
186
186
|
}
|
package/package.json
CHANGED
|
@@ -281,7 +281,21 @@
|
|
|
281
281
|
"undeclared_count": { "type": "integer" },
|
|
282
282
|
"has_undeclared_changes": { "type": "boolean" },
|
|
283
283
|
"truncated": { "type": "boolean" },
|
|
284
|
-
"reason": { "type": ["string", "null"] }
|
|
284
|
+
"reason": { "type": ["string", "null"] },
|
|
285
|
+
"attribution_mode": { "const": "parallel_chunk" },
|
|
286
|
+
"chunk_intents": {
|
|
287
|
+
"type": "array",
|
|
288
|
+
"items": { "type": "string" }
|
|
289
|
+
},
|
|
290
|
+
"attributed_paths": {
|
|
291
|
+
"type": "array",
|
|
292
|
+
"items": { "type": "string" }
|
|
293
|
+
},
|
|
294
|
+
"ambiguous_paths": {
|
|
295
|
+
"type": "array",
|
|
296
|
+
"items": { "type": "string" }
|
|
297
|
+
},
|
|
298
|
+
"ambiguous_count": { "type": "integer", "minimum": 0 }
|
|
285
299
|
}
|
|
286
300
|
}
|
|
287
301
|
}
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"skipped": { "type": "integer" }
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
|
+
"parallelism": { "$ref": "#/$defs/parallelism" },
|
|
67
68
|
"repro_evidence": { "$ref": "#/$defs/reproEvidence" },
|
|
68
69
|
"external_checks": {
|
|
69
70
|
"type": "array",
|
|
@@ -113,6 +114,33 @@
|
|
|
113
114
|
}
|
|
114
115
|
},
|
|
115
116
|
"$defs": {
|
|
117
|
+
"parallelism": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"additionalProperties": false,
|
|
120
|
+
"required": [
|
|
121
|
+
"requested",
|
|
122
|
+
"effective",
|
|
123
|
+
"repository_max",
|
|
124
|
+
"cpu_available",
|
|
125
|
+
"capped",
|
|
126
|
+
"mode",
|
|
127
|
+
"note"
|
|
128
|
+
],
|
|
129
|
+
"properties": {
|
|
130
|
+
"requested": { "type": "integer", "minimum": 1 },
|
|
131
|
+
"effective": { "type": "integer", "minimum": 1 },
|
|
132
|
+
"repository_max": { "type": "integer", "minimum": 1 },
|
|
133
|
+
"cpu_available": {
|
|
134
|
+
"anyOf": [
|
|
135
|
+
{ "type": "integer", "minimum": 1 },
|
|
136
|
+
{ "type": "null" }
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
"capped": { "type": "boolean" },
|
|
140
|
+
"mode": { "enum": ["serial", "parallel_chunks"] },
|
|
141
|
+
"note": { "type": "string" }
|
|
142
|
+
}
|
|
143
|
+
},
|
|
116
144
|
"failureFingerprint": {
|
|
117
145
|
"type": "object",
|
|
118
146
|
"additionalProperties": false,
|