mustflow 2.23.0 → 2.24.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.
- package/README.md +12 -2
- package/dist/cli/commands/adapters.js +11 -9
- package/dist/cli/commands/api.js +263 -113
- package/dist/cli/commands/check.js +11 -7
- package/dist/cli/commands/classify.js +16 -42
- package/dist/cli/commands/context.js +18 -31
- package/dist/cli/commands/contract-lint.js +12 -7
- package/dist/cli/commands/dashboard.js +65 -114
- package/dist/cli/commands/docs.js +43 -26
- package/dist/cli/commands/doctor.js +11 -7
- package/dist/cli/commands/evidence.js +642 -0
- package/dist/cli/commands/explain-verify.js +1 -59
- package/dist/cli/commands/explain.js +84 -36
- package/dist/cli/commands/handoff.js +13 -17
- package/dist/cli/commands/impact.js +14 -20
- package/dist/cli/commands/index.js +15 -9
- package/dist/cli/commands/init.js +56 -70
- package/dist/cli/commands/line-endings.js +15 -9
- package/dist/cli/commands/map.js +30 -42
- package/dist/cli/commands/next.js +300 -0
- package/dist/cli/commands/onboard.js +136 -0
- package/dist/cli/commands/run.js +47 -42
- package/dist/cli/commands/search.js +43 -69
- package/dist/cli/commands/status.js +9 -6
- package/dist/cli/commands/update.js +16 -10
- package/dist/cli/commands/upgrade.js +9 -6
- package/dist/cli/commands/verify/args.js +55 -249
- package/dist/cli/commands/verify.js +2 -1
- package/dist/cli/commands/version-sources.js +9 -6
- package/dist/cli/commands/version.js +9 -6
- package/dist/cli/commands/workspace.js +564 -0
- package/dist/cli/i18n/en.js +60 -1
- package/dist/cli/i18n/es.js +60 -1
- package/dist/cli/i18n/fr.js +60 -1
- package/dist/cli/i18n/hi.js +60 -1
- package/dist/cli/i18n/ko.js +60 -1
- package/dist/cli/i18n/zh.js +60 -1
- package/dist/cli/index.js +28 -25
- package/dist/cli/lib/agent-context.js +8 -9
- package/dist/cli/lib/command-registry.js +24 -0
- package/dist/cli/lib/dashboard-html/client-script.js +1 -1
- package/dist/cli/lib/local-index/database-path.js +5 -0
- package/dist/cli/lib/local-index/database-read.js +88 -0
- package/dist/cli/lib/local-index/effect-graph-read-model.js +112 -0
- package/dist/cli/lib/local-index/freshness.js +60 -0
- package/dist/cli/lib/local-index/index.js +12 -1866
- package/dist/cli/lib/local-index/path-surface-read-model.js +134 -0
- package/dist/cli/lib/local-index/populate.js +474 -0
- package/dist/cli/lib/local-index/schema.js +413 -0
- package/dist/cli/lib/local-index/search-read-model.js +533 -0
- package/dist/cli/lib/local-index/search-text.js +79 -0
- package/dist/cli/lib/option-parser.js +93 -0
- package/dist/cli/lib/repo-map.js +2 -2
- package/dist/cli/lib/run-plan.js +5 -22
- package/dist/core/change-verification.js +11 -5
- package/dist/core/command-effects.js +1 -3
- package/dist/core/command-intent-eligibility.js +14 -0
- package/dist/core/command-preconditions.js +8 -4
- package/dist/core/command-run-constraints.js +43 -0
- package/dist/core/public-json-contracts.js +57 -0
- package/dist/core/test-selection.js +8 -2
- package/dist/core/verification-plan.js +32 -4
- package/package.json +1 -1
- package/schemas/README.md +16 -0
- package/schemas/api-serve-response.schema.json +89 -0
- package/schemas/change-verification-report.schema.json +4 -1
- package/schemas/contract-lint-report.schema.json +1 -0
- package/schemas/evidence-report.schema.json +287 -0
- package/schemas/explain-report.schema.json +4 -0
- package/schemas/next-report.schema.json +121 -0
- package/schemas/onboard-commands-report.schema.json +100 -0
- package/schemas/workspace-command-catalog.schema.json +172 -0
- package/schemas/workspace-status.schema.json +141 -0
- package/schemas/workspace-verification-plan.schema.json +195 -0
- package/templates/default/manifest.toml +1 -1
|
@@ -7,6 +7,7 @@ import { copyFileInsideWithoutSymlinks, ensureFileTargetInsideWithoutSymlinks, e
|
|
|
7
7
|
import { localeMessage, t } from '../lib/i18n.js';
|
|
8
8
|
import { isLocaleTag } from '../lib/locale-tags.js';
|
|
9
9
|
import { MANIFEST_LOCK_RELATIVE_PATH, sha256File } from '../lib/manifest-lock.js';
|
|
10
|
+
import { formatCliOptionParseError, hasCliOptionToken, parseCliOptions } from '../lib/option-parser.js';
|
|
10
11
|
import { isCommitMessageStyle, isTestAuthoringPolicy } from '../lib/preferences-options.js';
|
|
11
12
|
import { getDefaultTemplate, getTemplateFiles } from '../lib/templates.js';
|
|
12
13
|
const MUSTFLOW_BLOCK_START = '<!-- mustflow:start schema=1 -->';
|
|
@@ -24,6 +25,20 @@ const LOCALE_LABELS = {
|
|
|
24
25
|
fr: 'French',
|
|
25
26
|
hi: 'Hindi',
|
|
26
27
|
};
|
|
28
|
+
const INIT_BOOLEAN_OPTIONS = new Set(['--yes', '--dry-run', '--merge', '--force', '--interactive']);
|
|
29
|
+
const INIT_OPTION_SPECS = [
|
|
30
|
+
{ name: '--yes', kind: 'boolean' },
|
|
31
|
+
{ name: '--dry-run', kind: 'boolean' },
|
|
32
|
+
{ name: '--merge', kind: 'boolean' },
|
|
33
|
+
{ name: '--force', kind: 'boolean' },
|
|
34
|
+
{ name: '--interactive', kind: 'boolean' },
|
|
35
|
+
{ name: '--set', kind: 'string' },
|
|
36
|
+
{ name: '--profile', kind: 'string' },
|
|
37
|
+
{ name: '--locale', kind: 'string' },
|
|
38
|
+
{ name: '--agent-lang', kind: 'string' },
|
|
39
|
+
{ name: '--product-source-locale', kind: 'string' },
|
|
40
|
+
{ name: '--product-locale', kind: 'string' },
|
|
41
|
+
];
|
|
27
42
|
function getMustflowRouterBlock(locale) {
|
|
28
43
|
return localeMessage(locale, 'init.routerBlock');
|
|
29
44
|
}
|
|
@@ -91,30 +106,12 @@ export function getInitHelp(lang = 'en') {
|
|
|
91
106
|
],
|
|
92
107
|
}, lang);
|
|
93
108
|
}
|
|
94
|
-
function
|
|
109
|
+
function splitOptionName(arg) {
|
|
95
110
|
const equalsIndex = arg.indexOf('=');
|
|
96
111
|
if (equalsIndex === -1) {
|
|
97
|
-
return
|
|
112
|
+
return arg;
|
|
98
113
|
}
|
|
99
|
-
return
|
|
100
|
-
name: arg.slice(0, equalsIndex),
|
|
101
|
-
value: arg.slice(equalsIndex + 1),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
function readRequiredOptionValue(args, index, parsed, reporter, lang) {
|
|
105
|
-
if (parsed.value !== undefined) {
|
|
106
|
-
if (parsed.value.trim().length === 0) {
|
|
107
|
-
printUsageError(reporter, t(lang, 'cli.error.missingValue', { option: parsed.name }), 'mf init --help', getInitHelp(lang), lang);
|
|
108
|
-
return undefined;
|
|
109
|
-
}
|
|
110
|
-
return parsed.value;
|
|
111
|
-
}
|
|
112
|
-
const next = args[index + 1];
|
|
113
|
-
if (!next || next.startsWith('-')) {
|
|
114
|
-
printUsageError(reporter, t(lang, 'cli.error.missingValue', { option: parsed.name }), 'mf init --help', getInitHelp(lang), lang);
|
|
115
|
-
return undefined;
|
|
116
|
-
}
|
|
117
|
-
return next;
|
|
114
|
+
return arg.slice(0, equalsIndex);
|
|
118
115
|
}
|
|
119
116
|
function parseBoolean(value) {
|
|
120
117
|
if (value === 'true') {
|
|
@@ -371,78 +368,67 @@ function parseOptions(args, reporter, lang) {
|
|
|
371
368
|
let productSourceLocale;
|
|
372
369
|
const productLocales = [];
|
|
373
370
|
const preferenceOverrides = [];
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const parsed = splitLongOption(arg);
|
|
380
|
-
if (['--yes', '--dry-run', '--merge', '--force', '--interactive'].includes(parsed.name) && parsed.value !== undefined) {
|
|
381
|
-
printUsageError(reporter, t(lang, 'cli.error.unexpectedValue', { option: parsed.name }), 'mf init --help', getInitHelp(lang), lang);
|
|
371
|
+
const parsed = parseCliOptions(args, INIT_OPTION_SPECS);
|
|
372
|
+
if (parsed.error) {
|
|
373
|
+
const optionName = splitOptionName(parsed.error.option);
|
|
374
|
+
if (parsed.error.kind === 'unknown_option' && INIT_BOOLEAN_OPTIONS.has(optionName) && parsed.error.option.includes('=')) {
|
|
375
|
+
printUsageError(reporter, t(lang, 'cli.error.unexpectedValue', { option: optionName }), 'mf init --help', getInitHelp(lang), lang);
|
|
382
376
|
return undefined;
|
|
383
377
|
}
|
|
384
|
-
|
|
378
|
+
printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf init --help', getInitHelp(lang), lang);
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
for (const occurrence of parsed.occurrences) {
|
|
382
|
+
if (occurrence.name === '--yes') {
|
|
385
383
|
yes = true;
|
|
386
384
|
continue;
|
|
387
385
|
}
|
|
388
|
-
if (
|
|
386
|
+
if (occurrence.name === '--dry-run') {
|
|
389
387
|
dryRun = true;
|
|
390
388
|
continue;
|
|
391
389
|
}
|
|
392
|
-
if (
|
|
390
|
+
if (occurrence.name === '--merge') {
|
|
393
391
|
merge = true;
|
|
394
392
|
continue;
|
|
395
393
|
}
|
|
396
|
-
if (
|
|
394
|
+
if (occurrence.name === '--force') {
|
|
397
395
|
force = true;
|
|
398
396
|
continue;
|
|
399
397
|
}
|
|
400
|
-
if (
|
|
398
|
+
if (occurrence.name === '--interactive') {
|
|
401
399
|
interactive = true;
|
|
402
400
|
continue;
|
|
403
401
|
}
|
|
404
|
-
if (
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const override = parsePreferenceOverride(value, reporter, lang);
|
|
402
|
+
if (typeof occurrence.value !== 'string') {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (occurrence.value.trim().length === 0) {
|
|
406
|
+
printUsageError(reporter, t(lang, 'cli.error.missingValue', { option: occurrence.name }), 'mf init --help', getInitHelp(lang), lang);
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
if (occurrence.name === '--set') {
|
|
410
|
+
const override = parsePreferenceOverride(occurrence.value, reporter, lang);
|
|
413
411
|
if (!override) {
|
|
414
412
|
return undefined;
|
|
415
413
|
}
|
|
416
414
|
preferenceOverrides.push(override);
|
|
417
415
|
continue;
|
|
418
416
|
}
|
|
419
|
-
if (
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
else if (parsed.name === '--agent-lang') {
|
|
434
|
-
agentLang = value;
|
|
435
|
-
}
|
|
436
|
-
else if (parsed.name === '--product-source-locale') {
|
|
437
|
-
productSourceLocale = value;
|
|
438
|
-
}
|
|
439
|
-
else {
|
|
440
|
-
productLocales.push(value);
|
|
441
|
-
}
|
|
442
|
-
continue;
|
|
417
|
+
if (occurrence.name === '--profile') {
|
|
418
|
+
profile = occurrence.value;
|
|
419
|
+
}
|
|
420
|
+
else if (occurrence.name === '--locale') {
|
|
421
|
+
locale = occurrence.value;
|
|
422
|
+
}
|
|
423
|
+
else if (occurrence.name === '--agent-lang') {
|
|
424
|
+
agentLang = occurrence.value;
|
|
425
|
+
}
|
|
426
|
+
else if (occurrence.name === '--product-source-locale') {
|
|
427
|
+
productSourceLocale = occurrence.value;
|
|
428
|
+
}
|
|
429
|
+
else if (occurrence.name === '--product-locale') {
|
|
430
|
+
productLocales.push(occurrence.value);
|
|
443
431
|
}
|
|
444
|
-
printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: arg }), 'mf init --help', getInitHelp(lang), lang);
|
|
445
|
-
return undefined;
|
|
446
432
|
}
|
|
447
433
|
if (merge && force) {
|
|
448
434
|
printUsageError(reporter, t(lang, 'init.error.cannotCombineMergeForce'), 'mf init --help', getInitHelp(lang), lang);
|
|
@@ -948,7 +934,7 @@ function writeManifestLock(projectRoot, template, plannedFiles, options, customi
|
|
|
948
934
|
writeUtf8FileInsideWithoutSymlinks(projectRoot, lockPath, renderManifestLock(template, plannedFiles, options, customizedFiles));
|
|
949
935
|
}
|
|
950
936
|
export async function runInit(args, reporter, lang = 'en') {
|
|
951
|
-
if (args
|
|
937
|
+
if (hasCliOptionToken(args, '--help', ['-h'])) {
|
|
952
938
|
reporter.stdout(getInitHelp(lang));
|
|
953
939
|
return 0;
|
|
954
940
|
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
2
2
|
import { t } from '../lib/i18n.js';
|
|
3
|
+
import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
|
|
3
4
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
4
5
|
import { inspectLineEndings } from '../../core/line-endings.js';
|
|
6
|
+
const LINE_ENDING_OPTIONS = [
|
|
7
|
+
{ name: '--json', kind: 'boolean' },
|
|
8
|
+
{ name: '--all', kind: 'boolean' },
|
|
9
|
+
{ name: '--apply', kind: 'boolean' },
|
|
10
|
+
{ name: '--dry-run', kind: 'boolean' },
|
|
11
|
+
];
|
|
5
12
|
export function getLineEndingsHelp(lang = 'en') {
|
|
6
13
|
return renderHelp({
|
|
7
14
|
usage: 'mf line-endings <check|normalize> [options]',
|
|
@@ -26,12 +33,11 @@ export function getLineEndingsHelp(lang = 'en') {
|
|
|
26
33
|
}
|
|
27
34
|
function parseLineEndingOptions(args, lang) {
|
|
28
35
|
const [action, ...rest] = args;
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const unsupported = rest.find((arg) => arg.startsWith('-') && !supported.has(arg));
|
|
36
|
+
const parsed = parseCliOptions(rest, LINE_ENDING_OPTIONS, { allowPositionals: true });
|
|
37
|
+
const json = hasParsedCliOption(parsed, '--json');
|
|
38
|
+
const apply = hasParsedCliOption(parsed, '--apply');
|
|
39
|
+
const dryRun = hasParsedCliOption(parsed, '--dry-run');
|
|
40
|
+
const all = hasParsedCliOption(parsed, '--all');
|
|
35
41
|
if (action !== 'check' && action !== 'normalize') {
|
|
36
42
|
return {
|
|
37
43
|
action: 'check',
|
|
@@ -42,8 +48,8 @@ function parseLineEndingOptions(args, lang) {
|
|
|
42
48
|
error: action ? t(lang, 'lineEndings.error.unknownAction', { action }) : t(lang, 'lineEndings.error.missingAction'),
|
|
43
49
|
};
|
|
44
50
|
}
|
|
45
|
-
if (
|
|
46
|
-
return { action, json, apply, dryRun, all, error:
|
|
51
|
+
if (parsed.error) {
|
|
52
|
+
return { action, json, apply, dryRun, all, error: formatCliOptionParseError(parsed.error, lang) };
|
|
47
53
|
}
|
|
48
54
|
if (action === 'check' && (apply || dryRun)) {
|
|
49
55
|
return { action, json, apply, dryRun, all, error: t(lang, 'lineEndings.error.checkModeOption') };
|
|
@@ -79,7 +85,7 @@ function renderLineEndingSummary(report, lang) {
|
|
|
79
85
|
return lines.join('\n');
|
|
80
86
|
}
|
|
81
87
|
export function runLineEndings(args, reporter, lang = 'en') {
|
|
82
|
-
if (args
|
|
88
|
+
if (hasCliOptionToken(args, '--help', ['-h'])) {
|
|
83
89
|
reporter.stdout(getLineEndingsHelp(lang));
|
|
84
90
|
return 0;
|
|
85
91
|
}
|
package/dist/cli/commands/map.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
2
2
|
import { t } from '../lib/i18n.js';
|
|
3
|
+
import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
|
|
3
4
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
4
5
|
import { generateRepoMap, writeRepoMap } from '../lib/repo-map.js';
|
|
6
|
+
const MAP_OPTIONS = [
|
|
7
|
+
{ name: '--stdout', kind: 'boolean' },
|
|
8
|
+
{ name: '--write', kind: 'boolean' },
|
|
9
|
+
{ name: '--depth', kind: 'string' },
|
|
10
|
+
{ name: '--include-nested', kind: 'boolean' },
|
|
11
|
+
{ name: '--root-only', kind: 'boolean' },
|
|
12
|
+
];
|
|
5
13
|
export function getMapHelp(lang = 'en') {
|
|
6
14
|
return renderHelp({
|
|
7
15
|
usage: 'mf map [options]',
|
|
@@ -31,54 +39,34 @@ export function getMapHelp(lang = 'en') {
|
|
|
31
39
|
}, lang);
|
|
32
40
|
}
|
|
33
41
|
export function runMap(args, reporter, lang = 'en') {
|
|
34
|
-
if (args
|
|
42
|
+
if (hasCliOptionToken(args, '--help', ['-h'])) {
|
|
35
43
|
reporter.stdout(getMapHelp(lang));
|
|
36
44
|
return 0;
|
|
37
45
|
}
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
const parsed = parseCliOptions(args, MAP_OPTIONS);
|
|
47
|
+
if (parsed.error) {
|
|
48
|
+
printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf map --help', getMapHelp(lang), lang);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
let shouldPrint = hasParsedCliOption(parsed, '--stdout');
|
|
52
|
+
const shouldWrite = hasParsedCliOption(parsed, '--write');
|
|
40
53
|
let depth = 3;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
if (arg === '--write') {
|
|
49
|
-
shouldWrite = true;
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
if (arg === '--include-nested') {
|
|
53
|
-
if (includeNested === false) {
|
|
54
|
-
printUsageError(reporter, t(lang, 'map.error.nestedConflict'), 'mf map --help', getMapHelp(lang), lang);
|
|
55
|
-
return 1;
|
|
56
|
-
}
|
|
57
|
-
includeNested = true;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
if (arg === '--root-only') {
|
|
61
|
-
if (includeNested === true) {
|
|
62
|
-
printUsageError(reporter, t(lang, 'map.error.nestedConflict'), 'mf map --help', getMapHelp(lang), lang);
|
|
63
|
-
return 1;
|
|
64
|
-
}
|
|
65
|
-
includeNested = false;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (arg === '--depth') {
|
|
69
|
-
const rawDepth = args[index + 1];
|
|
70
|
-
const parsedDepth = Number.parseInt(rawDepth ?? '', 10);
|
|
71
|
-
if (!Number.isInteger(parsedDepth) || parsedDepth < 1) {
|
|
72
|
-
printUsageError(reporter, t(lang, 'map.error.invalidDepth'), 'mf map --help', getMapHelp(lang), lang);
|
|
73
|
-
return 1;
|
|
74
|
-
}
|
|
75
|
-
depth = parsedDepth;
|
|
76
|
-
index += 1;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: arg }), 'mf map --help', getMapHelp(lang), lang);
|
|
54
|
+
const wantsIncludeNested = hasParsedCliOption(parsed, '--include-nested');
|
|
55
|
+
const wantsRootOnly = hasParsedCliOption(parsed, '--root-only');
|
|
56
|
+
let includeNested = wantsIncludeNested ? true : wantsRootOnly ? false : undefined;
|
|
57
|
+
const rawDepth = getParsedCliStringOption(parsed, '--depth');
|
|
58
|
+
if (wantsIncludeNested && wantsRootOnly) {
|
|
59
|
+
printUsageError(reporter, t(lang, 'map.error.nestedConflict'), 'mf map --help', getMapHelp(lang), lang);
|
|
80
60
|
return 1;
|
|
81
61
|
}
|
|
62
|
+
if (rawDepth !== null) {
|
|
63
|
+
const parsedDepth = Number.parseInt(rawDepth, 10);
|
|
64
|
+
if (!Number.isInteger(parsedDepth) || parsedDepth < 1) {
|
|
65
|
+
printUsageError(reporter, t(lang, 'map.error.invalidDepth'), 'mf map --help', getMapHelp(lang), lang);
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
depth = parsedDepth;
|
|
69
|
+
}
|
|
82
70
|
if (!shouldPrint && !shouldWrite) {
|
|
83
71
|
shouldPrint = true;
|
|
84
72
|
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { createClassifyOutput } from './classify.js';
|
|
2
|
+
import { createChangeVerificationReport, } from '../../core/change-verification.js';
|
|
3
|
+
import { isRecord, readCommandContract, } from '../../core/config-loading.js';
|
|
4
|
+
import { createVerificationPlanId } from '../../core/verification-plan-id.js';
|
|
5
|
+
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
6
|
+
import { getAgentContext } from '../lib/agent-context.js';
|
|
7
|
+
import { t } from '../lib/i18n.js';
|
|
8
|
+
import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
|
|
9
|
+
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
10
|
+
import { createRunPlan } from '../lib/run-plan.js';
|
|
11
|
+
import { checkMustflowProjectReport } from '../lib/validation.js';
|
|
12
|
+
const NEXT_SCHEMA_VERSION = '1';
|
|
13
|
+
const COMMAND_AUTHORITY = '.mustflow/config/commands.toml';
|
|
14
|
+
const NEXT_OPTIONS = [
|
|
15
|
+
{ name: '--json', kind: 'boolean' },
|
|
16
|
+
];
|
|
17
|
+
export function getNextHelp(lang = 'en') {
|
|
18
|
+
return renderHelp({
|
|
19
|
+
usage: 'mf next [options]',
|
|
20
|
+
summary: t(lang, 'next.help.summary'),
|
|
21
|
+
options: [
|
|
22
|
+
{ label: '--json', description: t(lang, 'cli.option.json') },
|
|
23
|
+
{ label: '-h, --help', description: t(lang, 'cli.option.help') },
|
|
24
|
+
],
|
|
25
|
+
examples: ['mf next', 'mf next --json'],
|
|
26
|
+
exitCodes: [
|
|
27
|
+
{ label: '0', description: t(lang, 'next.help.exit.ok') },
|
|
28
|
+
{ label: '1', description: t(lang, 'next.help.exit.fail') },
|
|
29
|
+
],
|
|
30
|
+
}, lang);
|
|
31
|
+
}
|
|
32
|
+
function createPolicy() {
|
|
33
|
+
return {
|
|
34
|
+
command_authority: COMMAND_AUTHORITY,
|
|
35
|
+
direct_commands_allowed: false,
|
|
36
|
+
grants_command_authority: false,
|
|
37
|
+
writes_files: false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function createState(input) {
|
|
41
|
+
return {
|
|
42
|
+
installed: input.installed,
|
|
43
|
+
check_ok: input.check_ok,
|
|
44
|
+
command_contract_ok: input.command_contract_ok,
|
|
45
|
+
git_status: input.git_status ?? 'unavailable',
|
|
46
|
+
changed_file_count: input.changed_file_count ?? null,
|
|
47
|
+
validation_reasons: input.validation_reasons ?? [],
|
|
48
|
+
selected_intents: input.selected_intents ?? [],
|
|
49
|
+
gap_count: input.gap_count ?? 0,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function action(kind, command, title, detail) {
|
|
53
|
+
return { kind, command, title, detail };
|
|
54
|
+
}
|
|
55
|
+
function toOutput(input) {
|
|
56
|
+
return {
|
|
57
|
+
schema_version: NEXT_SCHEMA_VERSION,
|
|
58
|
+
command: 'next',
|
|
59
|
+
mustflow_root: input.mustflowRoot,
|
|
60
|
+
status: input.status,
|
|
61
|
+
policy: createPolicy(),
|
|
62
|
+
state: input.state,
|
|
63
|
+
decision: input.decision,
|
|
64
|
+
recommended_commands: uniqueCommands(input.recommendedCommands),
|
|
65
|
+
blockers: input.blockers ?? [],
|
|
66
|
+
warnings: input.warnings ?? [],
|
|
67
|
+
gaps: input.gaps ?? [],
|
|
68
|
+
verification_plan_id: input.verificationPlanId ?? null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function uniqueCommands(commands) {
|
|
72
|
+
return [...new Set(commands.filter((command) => command.length > 0))];
|
|
73
|
+
}
|
|
74
|
+
function selectedIntents(report) {
|
|
75
|
+
return report.schedule.entries.map((entry) => entry.intent);
|
|
76
|
+
}
|
|
77
|
+
function toNextGap(gap) {
|
|
78
|
+
return {
|
|
79
|
+
reason: gap.reason,
|
|
80
|
+
files: gap.files,
|
|
81
|
+
surfaces: gap.surfaces,
|
|
82
|
+
detail: gap.detail,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function runnableIntentCommand(mustflowRoot, contract, intentName) {
|
|
86
|
+
const rawIntent = contract.intents[intentName];
|
|
87
|
+
if (!isRecord(rawIntent)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return createRunPlan(mustflowRoot, contract, intentName).ok ? `mf run ${intentName}` : null;
|
|
91
|
+
}
|
|
92
|
+
function fallbackCommands(mustflowRoot, contract) {
|
|
93
|
+
return [
|
|
94
|
+
'mf api verification-plan --changed --json',
|
|
95
|
+
'mf onboard commands',
|
|
96
|
+
runnableIntentCommand(mustflowRoot, contract, 'mustflow_check') ?? '',
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
function createChangedOutput(mustflowRoot, contract, classification, report) {
|
|
100
|
+
const intents = selectedIntents(report);
|
|
101
|
+
const gaps = report.gaps.map(toNextGap);
|
|
102
|
+
const verificationPlanId = createVerificationPlanId(report, contract);
|
|
103
|
+
const state = createState({
|
|
104
|
+
installed: true,
|
|
105
|
+
check_ok: true,
|
|
106
|
+
command_contract_ok: true,
|
|
107
|
+
git_status: 'available',
|
|
108
|
+
changed_file_count: classification.summary.fileCount,
|
|
109
|
+
validation_reasons: classification.summary.validationReasons,
|
|
110
|
+
selected_intents: intents,
|
|
111
|
+
gap_count: gaps.length,
|
|
112
|
+
});
|
|
113
|
+
if (gaps.length > 0) {
|
|
114
|
+
const detail = gaps[0]?.detail ?? 'At least one changed-file requirement has no runnable configured command.';
|
|
115
|
+
return toOutput({
|
|
116
|
+
mustflowRoot,
|
|
117
|
+
status: 'blocked',
|
|
118
|
+
state,
|
|
119
|
+
decision: action('configure_commands', 'mf onboard commands', 'Review command-contract gaps', `${detail} Do not guess package-manager commands directly.`),
|
|
120
|
+
recommendedCommands: [...fallbackCommands(mustflowRoot, contract), ...(intents.length > 0 ? ['mf verify --changed --json'] : [])],
|
|
121
|
+
blockers: gaps.map((gap) => gap.detail),
|
|
122
|
+
gaps,
|
|
123
|
+
verificationPlanId,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (intents.length > 0) {
|
|
127
|
+
return toOutput({
|
|
128
|
+
mustflowRoot,
|
|
129
|
+
status: 'needs_verification',
|
|
130
|
+
state,
|
|
131
|
+
decision: action('verify', 'mf verify --changed --json', 'Run configured verification', 'Changed files have runnable configured verification intents. Use mustflow verification instead of direct commands.'),
|
|
132
|
+
recommendedCommands: ['mf api verification-plan --changed --json', 'mf verify --changed --json'],
|
|
133
|
+
verificationPlanId,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return toOutput({
|
|
137
|
+
mustflowRoot,
|
|
138
|
+
status: 'idle',
|
|
139
|
+
state,
|
|
140
|
+
decision: action('inspect', 'mf api verification-plan --changed --json', 'Inspect verification plan', 'No runnable verification entries were selected.'),
|
|
141
|
+
recommendedCommands: ['mf api verification-plan --changed --json'],
|
|
142
|
+
verificationPlanId,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function createNextOutput() {
|
|
146
|
+
const mustflowRoot = resolveMustflowRoot();
|
|
147
|
+
const context = getAgentContext(mustflowRoot);
|
|
148
|
+
const check = checkMustflowProjectReport(mustflowRoot, { strict: false });
|
|
149
|
+
if (!context.installed) {
|
|
150
|
+
return toOutput({
|
|
151
|
+
mustflowRoot,
|
|
152
|
+
status: 'setup_required',
|
|
153
|
+
state: createState({ installed: false, check_ok: false, command_contract_ok: false }),
|
|
154
|
+
decision: action('setup', 'mf init --dry-run', 'Install mustflow workflow', 'This root does not have a complete mustflow installation yet.'),
|
|
155
|
+
recommendedCommands: ['mf init --dry-run', 'mf init --yes'],
|
|
156
|
+
blockers: ['mustflow is not installed in this root'],
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (check.issues.length > 0) {
|
|
160
|
+
return toOutput({
|
|
161
|
+
mustflowRoot,
|
|
162
|
+
status: 'blocked',
|
|
163
|
+
state: createState({
|
|
164
|
+
installed: true,
|
|
165
|
+
check_ok: false,
|
|
166
|
+
command_contract_ok: context.command_contract.exists,
|
|
167
|
+
}),
|
|
168
|
+
decision: action('repair', 'mf check --strict', 'Fix mustflow contract issues', 'The installed workflow has validation issues; inspect those before choosing verification.'),
|
|
169
|
+
recommendedCommands: ['mf check --strict', 'mf status --json', 'mf update --dry-run'],
|
|
170
|
+
blockers: check.issues,
|
|
171
|
+
warnings: check.warnings,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
let contract;
|
|
175
|
+
try {
|
|
176
|
+
contract = readCommandContract(mustflowRoot);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
+
return toOutput({
|
|
181
|
+
mustflowRoot,
|
|
182
|
+
status: 'blocked',
|
|
183
|
+
state: createState({ installed: true, check_ok: true, command_contract_ok: false }),
|
|
184
|
+
decision: action('repair', 'mf check --strict', 'Repair command contract', 'The command contract could not be read.'),
|
|
185
|
+
recommendedCommands: ['mf check --strict', 'mf doctor --json'],
|
|
186
|
+
blockers: [message],
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
let classification;
|
|
190
|
+
try {
|
|
191
|
+
classification = createClassifyOutput(mustflowRoot, 'changed', []);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
195
|
+
return toOutput({
|
|
196
|
+
mustflowRoot,
|
|
197
|
+
status: 'unavailable',
|
|
198
|
+
state: createState({ installed: true, check_ok: true, command_contract_ok: true }),
|
|
199
|
+
decision: action('inspect', 'mf doctor --json', 'Inspect repository state', 'Changed-file detection is unavailable.'),
|
|
200
|
+
recommendedCommands: ['mf doctor --json', 'mf context --json'],
|
|
201
|
+
warnings: [message],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (classification.summary.fileCount === 0) {
|
|
205
|
+
return toOutput({
|
|
206
|
+
mustflowRoot,
|
|
207
|
+
status: 'idle',
|
|
208
|
+
state: createState({
|
|
209
|
+
installed: true,
|
|
210
|
+
check_ok: true,
|
|
211
|
+
command_contract_ok: true,
|
|
212
|
+
git_status: 'available',
|
|
213
|
+
changed_file_count: 0,
|
|
214
|
+
}),
|
|
215
|
+
decision: action('none', null, 'No changed files', 'There is no changed-file verification to select.'),
|
|
216
|
+
recommendedCommands: ['mf doctor --json', 'mf context --json'],
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const report = createChangeVerificationReport(classification, contract, mustflowRoot);
|
|
221
|
+
return createChangedOutput(mustflowRoot, contract, classification, report);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
225
|
+
return toOutput({
|
|
226
|
+
mustflowRoot,
|
|
227
|
+
status: 'unavailable',
|
|
228
|
+
state: createState({
|
|
229
|
+
installed: true,
|
|
230
|
+
check_ok: true,
|
|
231
|
+
command_contract_ok: true,
|
|
232
|
+
git_status: 'available',
|
|
233
|
+
changed_file_count: classification.summary.fileCount,
|
|
234
|
+
validation_reasons: classification.summary.validationReasons,
|
|
235
|
+
}),
|
|
236
|
+
decision: action('inspect', 'mf api verification-plan --changed --json', 'Inspect verification plan', 'Verification planning could not be summarized.'),
|
|
237
|
+
recommendedCommands: ['mf api verification-plan --changed --json', 'mf doctor --json'],
|
|
238
|
+
warnings: [message],
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function renderList(values, lang) {
|
|
243
|
+
return values.length > 0 ? values.join(', ') : t(lang, 'value.none');
|
|
244
|
+
}
|
|
245
|
+
function renderNextOutput(output, lang) {
|
|
246
|
+
const lines = [
|
|
247
|
+
t(lang, 'next.title'),
|
|
248
|
+
`${t(lang, 'label.mustflowRoot')}: ${output.mustflow_root}`,
|
|
249
|
+
`${t(lang, 'next.label.status')}: ${output.status}`,
|
|
250
|
+
`${t(lang, 'next.label.nextAction')}: ${output.decision.command ?? t(lang, 'value.none')}`,
|
|
251
|
+
`${t(lang, 'next.label.reason')}: ${output.decision.detail}`,
|
|
252
|
+
`${t(lang, 'label.changedFiles')}: ${output.state.changed_file_count ?? t(lang, 'value.none')}`,
|
|
253
|
+
`${t(lang, 'classify.label.validationReasons')}: ${renderList(output.state.validation_reasons, lang)}`,
|
|
254
|
+
`${t(lang, 'next.label.selectedIntents')}: ${renderList(output.state.selected_intents, lang)}`,
|
|
255
|
+
`${t(lang, 'next.label.gaps')}: ${output.gaps.length}`,
|
|
256
|
+
];
|
|
257
|
+
if (output.gaps.length > 0) {
|
|
258
|
+
lines.push('', t(lang, 'next.section.gaps'));
|
|
259
|
+
for (const gap of output.gaps) {
|
|
260
|
+
lines.push(`- ${gap.reason}: ${gap.detail}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (output.blockers.length > 0) {
|
|
264
|
+
lines.push('', t(lang, 'next.section.blockers'));
|
|
265
|
+
for (const blocker of output.blockers) {
|
|
266
|
+
lines.push(`- ${blocker}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (output.warnings.length > 0) {
|
|
270
|
+
lines.push('', t(lang, 'next.section.warnings'));
|
|
271
|
+
for (const warning of output.warnings) {
|
|
272
|
+
lines.push(`- ${warning}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
lines.push('', t(lang, 'next.section.commands'));
|
|
276
|
+
for (const command of output.recommended_commands) {
|
|
277
|
+
lines.push(`- ${command}`);
|
|
278
|
+
}
|
|
279
|
+
lines.push('', t(lang, 'update.plan.noFilesWritten'));
|
|
280
|
+
return lines.join('\n');
|
|
281
|
+
}
|
|
282
|
+
export function runNext(args, reporter, lang = 'en') {
|
|
283
|
+
if (hasCliOptionToken(args, '--help', ['-h'])) {
|
|
284
|
+
reporter.stdout(getNextHelp(lang));
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
const options = parseCliOptions(args, NEXT_OPTIONS);
|
|
288
|
+
if (options.error) {
|
|
289
|
+
printUsageError(reporter, formatCliOptionParseError(options.error, lang), 'mf next --help', getNextHelp(lang), lang);
|
|
290
|
+
return 1;
|
|
291
|
+
}
|
|
292
|
+
const output = createNextOutput();
|
|
293
|
+
if (hasParsedCliOption(options, '--json')) {
|
|
294
|
+
reporter.stdout(JSON.stringify(output, null, 2));
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
reporter.stdout(renderNextOutput(output, lang));
|
|
298
|
+
}
|
|
299
|
+
return output.status === 'unavailable' ? 1 : 0;
|
|
300
|
+
}
|