mustflow 2.18.20 → 2.18.21
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/verify.js +45 -15
- package/dist/cli/lib/git-changes.js +7 -1
- package/dist/cli/lib/local-index/index.js +6 -27
- package/dist/core/change-classification.js +24 -2
- package/dist/core/command-contract-rules.js +6 -0
- package/dist/core/run-write-drift.js +4 -0
- package/package.json +1 -1
- package/templates/default/manifest.toml +1 -1
|
@@ -16,7 +16,7 @@ import { readCommandContract } from '../../core/config-loading.js';
|
|
|
16
16
|
import { DEFAULT_VERIFY_PARALLELISM, parseVerifyArgs } from './verify/args.js';
|
|
17
17
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
18
18
|
import { t } from '../lib/i18n.js';
|
|
19
|
-
import {
|
|
19
|
+
import { readLocalCommandEffectGraphs, readLocalPathSurfaces, readLocalSourceAnchorVerdictRisks, } from '../lib/local-index.js';
|
|
20
20
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
21
21
|
const VERIFY_SCHEMA_VERSION = '1';
|
|
22
22
|
const RUN_STATE_DIR = path.join('.mustflow', 'state', 'runs');
|
|
@@ -578,6 +578,20 @@ function skippedResult(candidate) {
|
|
|
578
578
|
receipt: null,
|
|
579
579
|
};
|
|
580
580
|
}
|
|
581
|
+
function stoppedAfterFailedBatchResult(entry, verificationPlanId) {
|
|
582
|
+
return {
|
|
583
|
+
intent: entry.intent,
|
|
584
|
+
status: 'skipped',
|
|
585
|
+
skipped: true,
|
|
586
|
+
reason: 'stopped_after_failed_batch',
|
|
587
|
+
detail: 'Skipped because an earlier verification batch failed and the schedule failure policy stops before the next batch.',
|
|
588
|
+
exit_code: null,
|
|
589
|
+
verification_plan_id: verificationPlanId,
|
|
590
|
+
receipt_path: null,
|
|
591
|
+
receipt_sha256: null,
|
|
592
|
+
receipt: null,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
581
595
|
function candidateResultKey(candidate) {
|
|
582
596
|
return candidate.intent
|
|
583
597
|
? `intent:${candidate.intent}`
|
|
@@ -670,21 +684,40 @@ async function runVerificationEntriesInParallelChunks(entries, parallelism, lang
|
|
|
670
684
|
}
|
|
671
685
|
return results;
|
|
672
686
|
}
|
|
687
|
+
function verificationResultFailed(result) {
|
|
688
|
+
return (!result.skipped &&
|
|
689
|
+
(result.status === 'failed' ||
|
|
690
|
+
result.status === 'timed_out' ||
|
|
691
|
+
result.status === 'start_failed' ||
|
|
692
|
+
result.status === 'output_limit_exceeded'));
|
|
693
|
+
}
|
|
673
694
|
async function runScheduledVerificationIntents(report, lang, verificationPlanId, scheduledTestTargets, parallelism) {
|
|
674
|
-
if (parallelism <= DEFAULT_VERIFY_PARALLELISM) {
|
|
675
|
-
return runVerificationEntriesSequentially(report.schedule.entries, lang, verificationPlanId, scheduledTestTargets);
|
|
676
|
-
}
|
|
677
695
|
const results = [];
|
|
678
|
-
for (
|
|
696
|
+
for (let batchIndex = 0; batchIndex < report.schedule.batches.length; batchIndex += 1) {
|
|
697
|
+
const batch = report.schedule.batches[batchIndex];
|
|
679
698
|
const entries = entriesForScheduleBatch(report.schedule.entries, batch);
|
|
680
699
|
if (entries.length === 0) {
|
|
681
700
|
continue;
|
|
682
701
|
}
|
|
702
|
+
let batchResults;
|
|
683
703
|
if (entries.length > 1 && entries.every((entry) => entry.parallelEligible)) {
|
|
684
|
-
|
|
704
|
+
batchResults =
|
|
705
|
+
parallelism > DEFAULT_VERIFY_PARALLELISM
|
|
706
|
+
? await runVerificationEntriesInParallelChunks(entries, parallelism, lang, verificationPlanId, scheduledTestTargets)
|
|
707
|
+
: await runVerificationEntriesSequentially(entries, lang, verificationPlanId, scheduledTestTargets);
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
batchResults = await runVerificationEntriesSequentially(entries, lang, verificationPlanId, scheduledTestTargets);
|
|
711
|
+
}
|
|
712
|
+
results.push(...batchResults);
|
|
713
|
+
if (!batchResults.some(verificationResultFailed)) {
|
|
685
714
|
continue;
|
|
686
715
|
}
|
|
687
|
-
|
|
716
|
+
const remainingEntries = report.schedule.batches
|
|
717
|
+
.slice(batchIndex + 1)
|
|
718
|
+
.flatMap((remainingBatch) => entriesForScheduleBatch(report.schedule.entries, remainingBatch));
|
|
719
|
+
results.push(...remainingEntries.map((entry) => stoppedAfterFailedBatchResult(entry, verificationPlanId)));
|
|
720
|
+
break;
|
|
688
721
|
}
|
|
689
722
|
return results;
|
|
690
723
|
}
|
|
@@ -1276,14 +1309,11 @@ async function createPlanOnlyOutput(input, projectRoot) {
|
|
|
1276
1309
|
if (!firstEntry) {
|
|
1277
1310
|
return { ...report, verification_plan_id: verificationPlanId, requirements };
|
|
1278
1311
|
}
|
|
1279
|
-
const
|
|
1280
|
-
const graphsByIntent =
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
graphsByIntent.set(entry.intent, await readLocalCommandEffectGraph(projectRoot, entry.intent));
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1312
|
+
const scheduledIntents = Array.from(new Set(report.schedule.entries.map((entry) => entry.intent)));
|
|
1313
|
+
const graphsByIntent = await readLocalCommandEffectGraphs(projectRoot, scheduledIntents);
|
|
1314
|
+
const firstGraph = graphsByIntent.get(firstEntry.intent);
|
|
1315
|
+
if (!firstGraph) {
|
|
1316
|
+
return { ...report, verification_plan_id: verificationPlanId, requirements };
|
|
1287
1317
|
}
|
|
1288
1318
|
return {
|
|
1289
1319
|
...report,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { parseGitStatusOutput } from '../../core/change-classification.js';
|
|
3
|
+
const GIT_STATUS_TIMEOUT_MS = 10_000;
|
|
4
|
+
const GIT_STATUS_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
3
5
|
export class GitChangedFilesError extends Error {
|
|
4
6
|
result;
|
|
5
7
|
constructor(result) {
|
|
@@ -9,9 +11,13 @@ export class GitChangedFilesError extends Error {
|
|
|
9
11
|
}
|
|
10
12
|
}
|
|
11
13
|
export function readGitChangedFiles(projectRoot) {
|
|
12
|
-
const result = spawnSync('git', ['status', '--
|
|
14
|
+
const result = spawnSync('git', ['status', '--porcelain=v1', '-z', '--untracked-files=all'], {
|
|
13
15
|
cwd: projectRoot,
|
|
14
16
|
encoding: 'utf8',
|
|
17
|
+
input: '',
|
|
18
|
+
maxBuffer: GIT_STATUS_MAX_BUFFER_BYTES,
|
|
19
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
20
|
+
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
15
21
|
windowsHide: true,
|
|
16
22
|
});
|
|
17
23
|
if (result.status !== 0 || typeof result.stdout !== 'string') {
|
|
@@ -331,16 +331,16 @@ function collectSourceAnchorCandidatePaths(projectRoot, sourceConfig) {
|
|
|
331
331
|
excludeGeneratedOrVendor: true,
|
|
332
332
|
});
|
|
333
333
|
}
|
|
334
|
-
function
|
|
334
|
+
function collectFastPreflightIndexedFileRecords(projectRoot, includeSource, sourceConfig) {
|
|
335
335
|
const records = new Map();
|
|
336
336
|
for (const relativePath of getExistingIndexablePaths(projectRoot)) {
|
|
337
|
-
records.set(relativePath,
|
|
337
|
+
records.set(relativePath, readIndexedFileRecord(projectRoot, relativePath, 'workflow'));
|
|
338
338
|
}
|
|
339
339
|
if (includeSource) {
|
|
340
340
|
try {
|
|
341
341
|
for (const sourcePath of collectSourceAnchorCandidatePaths(projectRoot, sourceConfig)) {
|
|
342
342
|
if (!records.has(sourcePath)) {
|
|
343
|
-
records.set(sourcePath,
|
|
343
|
+
records.set(sourcePath, readIndexedFileRecord(projectRoot, sourcePath, 'source_anchor'));
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
}
|
|
@@ -349,7 +349,7 @@ function collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSour
|
|
|
349
349
|
}
|
|
350
350
|
}
|
|
351
351
|
if (existsSync(path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/')))) {
|
|
352
|
-
records.set(LATEST_RUN_STATE_RELATIVE_PATH,
|
|
352
|
+
records.set(LATEST_RUN_STATE_RELATIVE_PATH, readIndexedFileRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
|
|
353
353
|
}
|
|
354
354
|
return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
355
355
|
}
|
|
@@ -2053,27 +2053,6 @@ function createStoredLocalIndexResult(projectRoot, databasePath, dryRun, indexMo
|
|
|
2053
2053
|
indexed_paths: readStoredIndexedPaths(database),
|
|
2054
2054
|
};
|
|
2055
2055
|
}
|
|
2056
|
-
function indexedFileMetadataMatch(database, currentFiles) {
|
|
2057
|
-
const rows = queryRows(database, 'SELECT path, source_scope, size_bytes, mtime_ms, parser_version FROM indexed_files ORDER BY path');
|
|
2058
|
-
if (rows.length !== currentFiles.length) {
|
|
2059
|
-
return false;
|
|
2060
|
-
}
|
|
2061
|
-
const currentByPath = new Map(currentFiles.map((file) => [file.path, file]));
|
|
2062
|
-
for (const row of rows) {
|
|
2063
|
-
const storedPath = toSearchString(row.path);
|
|
2064
|
-
const current = currentByPath.get(storedPath);
|
|
2065
|
-
if (!current) {
|
|
2066
|
-
return false;
|
|
2067
|
-
}
|
|
2068
|
-
if (normalizeIndexedFileSourceScope(row.source_scope) !== current.sourceScope ||
|
|
2069
|
-
row.size_bytes !== current.sizeBytes ||
|
|
2070
|
-
row.mtime_ms !== current.mtimeMs ||
|
|
2071
|
-
toSearchString(row.parser_version) !== LOCAL_INDEX_PARSER_VERSION) {
|
|
2072
|
-
return false;
|
|
2073
|
-
}
|
|
2074
|
-
}
|
|
2075
|
-
return true;
|
|
2076
|
-
}
|
|
2077
2056
|
function indexedFilesMatch(database, currentFiles) {
|
|
2078
2057
|
const rows = queryRows(database, 'SELECT path, source_scope, content_hash, parser_version FROM indexed_files ORDER BY path');
|
|
2079
2058
|
if (rows.length !== currentFiles.length) {
|
|
@@ -2116,7 +2095,7 @@ async function readIncrementalPreflightReuse(SQL, databasePath, projectRoot, cur
|
|
|
2116
2095
|
if (!hasTable(database, 'indexed_files')) {
|
|
2117
2096
|
return { result: null, rebuildReason: 'indexed_files_missing' };
|
|
2118
2097
|
}
|
|
2119
|
-
if (!
|
|
2098
|
+
if (!indexedFilesMatch(database, currentFiles)) {
|
|
2120
2099
|
return { result: null, rebuildReason: 'file_fingerprint_mismatch' };
|
|
2121
2100
|
}
|
|
2122
2101
|
const capabilities = readStoredSearchCapabilities(database);
|
|
@@ -2190,7 +2169,7 @@ export async function createLocalIndex(projectRoot, options = {}) {
|
|
|
2190
2169
|
capabilities = detectLocalSearchCapabilities(capabilityDatabase);
|
|
2191
2170
|
capabilityDatabase.close();
|
|
2192
2171
|
if (incremental) {
|
|
2193
|
-
const preflightFiles =
|
|
2172
|
+
const preflightFiles = collectFastPreflightIndexedFileRecords(projectRoot, includeSource, sourceConfig);
|
|
2194
2173
|
const preflightReuse = await readIncrementalPreflightReuse(SQL, databasePath, projectRoot, preflightFiles, sourceScopeHash, dryRun, indexMode);
|
|
2195
2174
|
if (preflightReuse.result) {
|
|
2196
2175
|
return preflightReuse.result;
|
|
@@ -46,14 +46,36 @@ function uniqueSorted(values) {
|
|
|
46
46
|
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
47
47
|
}
|
|
48
48
|
function toPosixPath(value) {
|
|
49
|
-
return value.
|
|
49
|
+
return value.replaceAll('\\', '/');
|
|
50
50
|
}
|
|
51
51
|
export function normalizeStatusPath(value) {
|
|
52
|
-
const pathText = toPosixPath(value);
|
|
52
|
+
const pathText = toPosixPath(value.trim());
|
|
53
53
|
const renameTarget = pathText.includes(' -> ') ? (pathText.split(' -> ').pop() ?? pathText) : pathText;
|
|
54
54
|
return renameTarget.replace(/^"|"$/gu, '');
|
|
55
55
|
}
|
|
56
|
+
function normalizePorcelainStatusPath(value) {
|
|
57
|
+
return toPosixPath(value);
|
|
58
|
+
}
|
|
59
|
+
function parseGitPorcelainStatusOutput(output) {
|
|
60
|
+
const paths = [];
|
|
61
|
+
const parts = output.split('\0').filter((part) => part.length > 0);
|
|
62
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
63
|
+
const entry = parts[index] ?? '';
|
|
64
|
+
const status = entry.slice(0, 2);
|
|
65
|
+
const filePath = normalizePorcelainStatusPath(entry.slice(3));
|
|
66
|
+
if (filePath.length > 0) {
|
|
67
|
+
paths.push(filePath);
|
|
68
|
+
}
|
|
69
|
+
if (status.includes('R') || status.includes('C')) {
|
|
70
|
+
index += 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return uniqueSorted(paths);
|
|
74
|
+
}
|
|
56
75
|
export function parseGitStatusOutput(output) {
|
|
76
|
+
if (output.includes('\0')) {
|
|
77
|
+
return parseGitPorcelainStatusOutput(output);
|
|
78
|
+
}
|
|
57
79
|
const paths = output
|
|
58
80
|
.split(/\r?\n/u)
|
|
59
81
|
.map((line) => line.slice(3))
|
|
@@ -125,6 +125,12 @@ export function commandIntentBlockedCommandPattern(intent) {
|
|
|
125
125
|
detail: 'Shell command contains a blocked long-running or background pattern.',
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
|
+
if (intent.mode === 'shell' && typeof intent.cmd === 'string' && commandTextHasLongRunningPattern(intent.cmd)) {
|
|
129
|
+
return {
|
|
130
|
+
code: 'long_running_command_pattern',
|
|
131
|
+
detail: `Shell command contains a blocked long-running pattern: ${intent.cmd}.`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
128
134
|
const argv = readStringArray(intent, 'argv');
|
|
129
135
|
if (!argv) {
|
|
130
136
|
return null;
|
|
@@ -130,6 +130,10 @@ function listObservedChangedPaths(before, after) {
|
|
|
130
130
|
function declaredPathCoversObservedPath(declaredPath, observedPath) {
|
|
131
131
|
const declaredKey = pathKey(declaredPath);
|
|
132
132
|
const observedKey = pathKey(observedPath);
|
|
133
|
+
if (declaredKey.endsWith('/**')) {
|
|
134
|
+
const baseKey = declaredKey.slice(0, -3) || '.';
|
|
135
|
+
return baseKey === '.' || observedKey === baseKey || observedKey.startsWith(`${baseKey}/`);
|
|
136
|
+
}
|
|
133
137
|
return declaredKey === '.' || observedKey === declaredKey || observedKey.startsWith(`${declaredKey}/`);
|
|
134
138
|
}
|
|
135
139
|
function truncatePaths(paths) {
|
package/package.json
CHANGED