mustflow 1.30.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -11
- package/dist/cli/commands/classify.js +61 -6
- package/dist/cli/commands/contract-lint.js +13 -4
- package/dist/cli/commands/dashboard.js +6 -0
- package/dist/cli/commands/index.js +5 -0
- package/dist/cli/commands/run.js +224 -48
- package/dist/cli/commands/upgrade.js +65 -0
- package/dist/cli/commands/verify.js +550 -33
- package/dist/cli/i18n/en.js +73 -10
- package/dist/cli/i18n/es.js +73 -10
- package/dist/cli/i18n/fr.js +73 -10
- package/dist/cli/i18n/hi.js +73 -10
- package/dist/cli/i18n/ko.js +73 -10
- package/dist/cli/i18n/zh.js +73 -10
- package/dist/cli/index.js +27 -46
- package/dist/cli/lib/command-registry.js +5 -0
- package/dist/cli/lib/dashboard-export.js +62 -12
- package/dist/cli/lib/dashboard-html/client-script.js +1936 -0
- package/dist/cli/lib/dashboard-html/locale-bootstrap.js +8 -0
- package/dist/cli/lib/dashboard-html/styles.js +572 -0
- package/dist/cli/lib/dashboard-html/template.js +134 -0
- package/dist/cli/lib/dashboard-html/types.js +1 -0
- package/dist/cli/lib/dashboard-html.js +1 -1907
- package/dist/cli/lib/dashboard-locale.js +37 -0
- package/dist/cli/lib/local-index/constants.js +48 -0
- package/dist/cli/lib/local-index/index.js +2256 -0
- package/dist/cli/lib/local-index/sql.js +15 -0
- package/dist/cli/lib/local-index/types.js +1 -0
- package/dist/cli/lib/local-index.js +1 -1908
- package/dist/cli/lib/reporter.js +6 -0
- package/dist/cli/lib/run-plan.js +96 -4
- package/dist/cli/lib/templates.js +18 -1
- package/dist/cli/lib/validation/command-intents.js +11 -0
- package/dist/cli/lib/validation/constants.js +238 -0
- package/dist/cli/lib/validation/index.js +1384 -0
- package/dist/cli/lib/validation/primitives.js +198 -0
- package/dist/cli/lib/validation/test-selection.js +95 -0
- package/dist/cli/lib/validation/types.js +1 -0
- package/dist/cli/lib/validation.js +1 -1661
- package/dist/core/bounded-output.js +38 -0
- package/dist/core/change-classification.js +6 -2
- package/dist/core/change-verification.js +240 -6
- package/dist/core/check-issues.js +12 -0
- package/dist/core/command-contract-validation.js +20 -0
- package/dist/core/command-effects.js +13 -0
- package/dist/core/completion-verdict.js +209 -0
- package/dist/core/contract-lint.js +316 -7
- package/dist/core/dashboard-verification.js +8 -0
- package/dist/core/external-evidence.js +9 -0
- package/dist/core/public-json-contracts.js +28 -0
- package/dist/core/repeated-failure.js +17 -0
- package/dist/core/repro-evidence.js +53 -0
- package/dist/core/run-performance-history.js +307 -0
- package/dist/core/run-profile.js +87 -0
- package/dist/core/run-receipt.js +171 -4
- package/dist/core/run-write-drift.js +18 -2
- package/dist/core/scope-risk.js +64 -0
- package/dist/core/skill-route-alignment.js +110 -0
- package/dist/core/source-anchor-status.js +4 -1
- package/dist/core/test-selection.js +227 -0
- package/dist/core/validation-ratchet.js +52 -0
- package/dist/core/verification-decision-graph.js +67 -0
- package/dist/core/verification-evidence.js +249 -0
- package/dist/core/verification-scheduler.js +96 -2
- package/examples/README.md +12 -4
- package/package.json +1 -1
- package/schemas/README.md +18 -4
- package/schemas/change-verification-report.schema.json +169 -5
- package/schemas/commands.schema.json +51 -1
- package/schemas/contract-lint-report.schema.json +80 -0
- package/schemas/dashboard-export.schema.json +500 -0
- package/schemas/explain-report.schema.json +2 -0
- package/schemas/latest-run-pointer.schema.json +384 -0
- package/schemas/run-receipt.schema.json +113 -0
- package/schemas/test-selection.schema.json +81 -0
- package/schemas/verify-report.schema.json +361 -1
- package/schemas/verify-run-manifest.schema.json +410 -0
- package/templates/default/common/.mustflow/config/commands.toml +1 -1
- package/templates/default/i18n.toml +1 -1
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +124 -29
- package/templates/default/locales/en/.mustflow/skills/routes.toml +289 -0
- package/templates/default/manifest.toml +29 -2
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { COMMAND_LIFECYCLES, COMMAND_RUN_POLICIES, LONG_RUNNING_LIFECYCLES, isRecord, readPositiveInteger, readString, readStringArray, } from './config-loading.js';
|
|
2
|
-
import { evaluateCommandIntentEligibility } from './command-intent-eligibility.js';
|
|
4
|
+
import { evaluateCommandIntentEligibility, } from './command-intent-eligibility.js';
|
|
3
5
|
import { commandIntentHasBlockedShellBackgroundPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
|
|
4
6
|
import { commandEffectsConflict, normalizeCommandEffects } from './command-effects.js';
|
|
5
7
|
import { listChangeClassificationValidationReasons } from './change-classification.js';
|
|
8
|
+
import { parseSkillIndexRoutes } from './skill-route-alignment.js';
|
|
6
9
|
const CONTRACT_LINT_SOURCE_FILES = [
|
|
7
10
|
'.mustflow/config/commands.toml',
|
|
8
11
|
'.mustflow/docs/agent-workflow.md',
|
|
@@ -41,9 +44,20 @@ const RELEASE_SENSITIVE_REASONS = new Set([
|
|
|
41
44
|
'release_risk',
|
|
42
45
|
'template_version_change',
|
|
43
46
|
]);
|
|
47
|
+
const COMMANDS_CONFIG_PATH = '.mustflow/config/commands.toml';
|
|
48
|
+
const SKILL_INDEX_PATH = '.mustflow/skills/INDEX.md';
|
|
49
|
+
const CHANGE_CLASSIFICATION_SOURCE_PATH = 'src/core/change-classification.ts';
|
|
50
|
+
const AGENT_WORKFLOW_PATH = '.mustflow/docs/agent-workflow.md';
|
|
51
|
+
const PACKAGE_SCRIPT_RUNNERS = new Set(['bun', 'npm', 'pnpm', 'yarn']);
|
|
52
|
+
const MAKEFILE_CANDIDATES = ['Makefile', 'makefile'];
|
|
53
|
+
const JUSTFILE_CANDIDATES = ['justfile', 'Justfile'];
|
|
44
54
|
function uniqueSorted(values) {
|
|
45
55
|
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
46
56
|
}
|
|
57
|
+
function intersectSorted(left, right) {
|
|
58
|
+
const rightSet = new Set(right);
|
|
59
|
+
return uniqueSorted(left.filter((value) => rightSet.has(value)));
|
|
60
|
+
}
|
|
47
61
|
function readBoolean(intent, key) {
|
|
48
62
|
const value = intent[key];
|
|
49
63
|
return typeof value === 'boolean' ? value : null;
|
|
@@ -56,6 +70,195 @@ function writesAreValid(intent) {
|
|
|
56
70
|
const value = intent.writes;
|
|
57
71
|
return value === undefined || (Array.isArray(value) && value.every((entry) => typeof entry === 'string'));
|
|
58
72
|
}
|
|
73
|
+
function readStringList(value) {
|
|
74
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return [...value];
|
|
78
|
+
}
|
|
79
|
+
function normalizeCommandName(value) {
|
|
80
|
+
return path.basename(value).replace(/\.(?:cmd|exe)$/iu, '').toLowerCase();
|
|
81
|
+
}
|
|
82
|
+
function readPackageScriptReference(intent) {
|
|
83
|
+
const argv = readStringList(intent.argv);
|
|
84
|
+
if (!argv || argv.length < 3) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const runner = normalizeCommandName(argv[0]);
|
|
88
|
+
if (!PACKAGE_SCRIPT_RUNNERS.has(runner) || argv[1] !== 'run') {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const scriptName = argv[2];
|
|
92
|
+
return scriptName && !scriptName.startsWith('-') ? scriptName : null;
|
|
93
|
+
}
|
|
94
|
+
function resolveIntentCwd(projectRoot, intent) {
|
|
95
|
+
const cwd = readString(intent, 'cwd') ?? '.';
|
|
96
|
+
const root = path.resolve(projectRoot);
|
|
97
|
+
const resolved = path.resolve(root, cwd);
|
|
98
|
+
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return resolved;
|
|
102
|
+
}
|
|
103
|
+
function toProjectRelativePath(projectRoot, absolutePath) {
|
|
104
|
+
const relativePath = path.relative(projectRoot, absolutePath) || '.';
|
|
105
|
+
return relativePath.split(path.sep).join('/');
|
|
106
|
+
}
|
|
107
|
+
function readPackageScripts(projectRoot, intent) {
|
|
108
|
+
const intentCwd = resolveIntentCwd(projectRoot, intent);
|
|
109
|
+
if (!intentCwd) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const packagePath = path.join(intentCwd, 'package.json');
|
|
113
|
+
if (!existsSync(packagePath)) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
118
|
+
if (!isRecord(parsed) || !isRecord(parsed.scripts)) {
|
|
119
|
+
return {
|
|
120
|
+
relativePath: toProjectRelativePath(projectRoot, packagePath),
|
|
121
|
+
scripts: new Set(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
relativePath: toProjectRelativePath(projectRoot, packagePath),
|
|
126
|
+
scripts: new Set(Object.keys(parsed.scripts)),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function readRootPackageScripts(projectRoot) {
|
|
134
|
+
const packagePath = path.join(projectRoot, 'package.json');
|
|
135
|
+
if (!existsSync(packagePath)) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
140
|
+
if (!isRecord(parsed) || !isRecord(parsed.scripts)) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
return Object.entries(parsed.scripts)
|
|
144
|
+
.filter((entry) => typeof entry[1] === 'string')
|
|
145
|
+
.sort((left, right) => left[0].localeCompare(right[0]));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function readFirstExistingFile(projectRoot, candidates) {
|
|
152
|
+
for (const candidate of candidates) {
|
|
153
|
+
if (existsSync(path.join(projectRoot, candidate))) {
|
|
154
|
+
return candidate;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function readMakeTargets(projectRoot) {
|
|
160
|
+
const relativePath = readFirstExistingFile(projectRoot, MAKEFILE_CANDIDATES);
|
|
161
|
+
if (!relativePath) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
const content = readFileSync(path.join(projectRoot, relativePath), 'utf8');
|
|
165
|
+
const targets = [];
|
|
166
|
+
for (const line of content.split(/\r?\n/u)) {
|
|
167
|
+
if (/^\s/u.test(line) || line.startsWith('#') || line.includes(':=')) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const match = /^([A-Za-z0-9][A-Za-z0-9_-]*)\s*:/u.exec(line);
|
|
171
|
+
if (match) {
|
|
172
|
+
targets.push(match[1]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return uniqueSorted(targets);
|
|
176
|
+
}
|
|
177
|
+
function readJustRecipes(projectRoot) {
|
|
178
|
+
const relativePath = readFirstExistingFile(projectRoot, JUSTFILE_CANDIDATES);
|
|
179
|
+
if (!relativePath) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
const content = readFileSync(path.join(projectRoot, relativePath), 'utf8');
|
|
183
|
+
const recipes = [];
|
|
184
|
+
for (const line of content.split(/\r?\n/u)) {
|
|
185
|
+
if (/^\s/u.test(line) || line.startsWith('#') || line.startsWith('set ') || line.includes(' := ')) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const match = /^([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s+[^:]*)?:\s*(?:#.*)?$/u.exec(line);
|
|
189
|
+
if (match) {
|
|
190
|
+
recipes.push(match[1]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return uniqueSorted(recipes);
|
|
194
|
+
}
|
|
195
|
+
function toTomlString(value) {
|
|
196
|
+
return JSON.stringify(value);
|
|
197
|
+
}
|
|
198
|
+
function normalizeIntentSegment(value) {
|
|
199
|
+
return value
|
|
200
|
+
.toLowerCase()
|
|
201
|
+
.replace(/[^a-z0-9]+/gu, '_')
|
|
202
|
+
.replace(/^_+|_+$/gu, '');
|
|
203
|
+
}
|
|
204
|
+
function uniqueSuggestionIntentName(baseName, usedIntentNames) {
|
|
205
|
+
let candidate = baseName;
|
|
206
|
+
let suffix = 2;
|
|
207
|
+
while (usedIntentNames.has(candidate)) {
|
|
208
|
+
candidate = `${baseName}_${suffix}`;
|
|
209
|
+
suffix += 1;
|
|
210
|
+
}
|
|
211
|
+
usedIntentNames.add(candidate);
|
|
212
|
+
return candidate;
|
|
213
|
+
}
|
|
214
|
+
function createUnknownIntentSnippet(intentName, description, reason) {
|
|
215
|
+
return [
|
|
216
|
+
`[intents.${intentName}]`,
|
|
217
|
+
'status = "unknown"',
|
|
218
|
+
`description = ${toTomlString(description)}`,
|
|
219
|
+
`reason = ${toTomlString(reason)}`,
|
|
220
|
+
'agent_action = "review_and_configure_before_run"',
|
|
221
|
+
].join('\n');
|
|
222
|
+
}
|
|
223
|
+
function createSuggestion(usedIntentNames, sourceFile, sourceKind, sourceName, commandHint) {
|
|
224
|
+
const sourcePrefix = sourceKind.replace(/_script$/u, '').replace(/_target$/u, '').replace(/_recipe$/u, '');
|
|
225
|
+
const intentName = uniqueSuggestionIntentName(`suggest_${sourcePrefix}_${normalizeIntentSegment(sourceName) || 'command'}`, usedIntentNames);
|
|
226
|
+
const reason = `Suggested from ${sourceFile} entry "${sourceName}". Review before adding runnable command fields.`;
|
|
227
|
+
const description = `Review ${commandHint} for a possible command intent.`;
|
|
228
|
+
return {
|
|
229
|
+
sourceFile,
|
|
230
|
+
sourceKind,
|
|
231
|
+
sourceName,
|
|
232
|
+
commandHint,
|
|
233
|
+
suggestedIntent: intentName,
|
|
234
|
+
status: 'unknown',
|
|
235
|
+
reason,
|
|
236
|
+
snippet: createUnknownIntentSnippet(intentName, description, reason),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function suggestCommandContracts(projectRoot, existingIntentNames) {
|
|
240
|
+
if (!projectRoot) {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
const usedIntentNames = new Set(existingIntentNames);
|
|
244
|
+
const suggestions = [];
|
|
245
|
+
for (const [scriptName] of readRootPackageScripts(projectRoot)) {
|
|
246
|
+
suggestions.push(createSuggestion(usedIntentNames, 'package.json', 'package_script', scriptName, `npm run ${scriptName}`));
|
|
247
|
+
}
|
|
248
|
+
const makefilePath = readFirstExistingFile(projectRoot, MAKEFILE_CANDIDATES);
|
|
249
|
+
if (makefilePath) {
|
|
250
|
+
for (const target of readMakeTargets(projectRoot)) {
|
|
251
|
+
suggestions.push(createSuggestion(usedIntentNames, makefilePath, 'make_target', target, `make ${target}`));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const justfilePath = readFirstExistingFile(projectRoot, JUSTFILE_CANDIDATES);
|
|
255
|
+
if (justfilePath) {
|
|
256
|
+
for (const recipe of readJustRecipes(projectRoot)) {
|
|
257
|
+
suggestions.push(createSuggestion(usedIntentNames, justfilePath, 'just_recipe', recipe, `just ${recipe}`));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return suggestions;
|
|
261
|
+
}
|
|
59
262
|
function pushIssue(issues, severity, code, intent, message) {
|
|
60
263
|
issues.push({ severity, code, intent, message });
|
|
61
264
|
}
|
|
@@ -132,6 +335,25 @@ function lintIntent(name, value, issues) {
|
|
|
132
335
|
}
|
|
133
336
|
return value;
|
|
134
337
|
}
|
|
338
|
+
function lintReferencedPackageScripts(projectRoot, intents, issues) {
|
|
339
|
+
if (!projectRoot) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
for (const [name, intent] of intents) {
|
|
343
|
+
if (readString(intent, 'status') !== 'configured') {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const scriptName = readPackageScriptReference(intent);
|
|
347
|
+
if (!scriptName) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const packageScripts = readPackageScripts(projectRoot, intent);
|
|
351
|
+
if (!packageScripts || packageScripts.scripts.has(scriptName)) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
pushIssue(issues, 'warning', 'referenced_package_script_missing', name, `Intent ${name} references package script "${scriptName}" in ${packageScripts.relativePath}, but that script is not declared.`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
135
357
|
function collectRequiredAfterReasons(contract) {
|
|
136
358
|
const reasonToIntents = new Map();
|
|
137
359
|
for (const [name, intent] of Object.entries(contract.intents)) {
|
|
@@ -151,6 +373,87 @@ function collectRequiredAfterReasons(contract) {
|
|
|
151
373
|
}
|
|
152
374
|
return reasonToIntents;
|
|
153
375
|
}
|
|
376
|
+
function readSkillPathsByIntent(projectRoot) {
|
|
377
|
+
const skillPathsByIntent = new Map();
|
|
378
|
+
if (!projectRoot) {
|
|
379
|
+
return skillPathsByIntent;
|
|
380
|
+
}
|
|
381
|
+
const skillIndexPath = path.join(projectRoot, ...SKILL_INDEX_PATH.split('/'));
|
|
382
|
+
if (!existsSync(skillIndexPath)) {
|
|
383
|
+
return skillPathsByIntent;
|
|
384
|
+
}
|
|
385
|
+
const routes = parseSkillIndexRoutes(readFileSync(skillIndexPath, 'utf8'));
|
|
386
|
+
for (const route of routes) {
|
|
387
|
+
for (const intent of route.commandIntents) {
|
|
388
|
+
skillPathsByIntent.set(intent, [...(skillPathsByIntent.get(intent) ?? []), route.skillPath]);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return skillPathsByIntent;
|
|
392
|
+
}
|
|
393
|
+
function classifyReasonSource(reason, knownClassificationReasons, documentedVerificationReasons) {
|
|
394
|
+
if (knownClassificationReasons.includes(reason)) {
|
|
395
|
+
return 'classification';
|
|
396
|
+
}
|
|
397
|
+
if (documentedVerificationReasons.includes(reason)) {
|
|
398
|
+
return 'documented';
|
|
399
|
+
}
|
|
400
|
+
return 'required_after';
|
|
401
|
+
}
|
|
402
|
+
function buildRelatedDocs(source, relatedSkills) {
|
|
403
|
+
const docs = [COMMANDS_CONFIG_PATH];
|
|
404
|
+
if (source === 'classification') {
|
|
405
|
+
docs.push(CHANGE_CLASSIFICATION_SOURCE_PATH);
|
|
406
|
+
}
|
|
407
|
+
if (source === 'documented') {
|
|
408
|
+
docs.push(AGENT_WORKFLOW_PATH);
|
|
409
|
+
}
|
|
410
|
+
if (relatedSkills.length > 0) {
|
|
411
|
+
docs.push(SKILL_INDEX_PATH);
|
|
412
|
+
}
|
|
413
|
+
return uniqueSorted(docs);
|
|
414
|
+
}
|
|
415
|
+
function buildCoverageMatrix(reasonToIntents, knownClassificationReasons, documentedVerificationReasons, skillPathsByIntent) {
|
|
416
|
+
const matrixReasons = uniqueSorted([
|
|
417
|
+
...knownClassificationReasons,
|
|
418
|
+
...documentedVerificationReasons,
|
|
419
|
+
...reasonToIntents.keys(),
|
|
420
|
+
]);
|
|
421
|
+
return matrixReasons.map((reason) => {
|
|
422
|
+
const candidates = [...(reasonToIntents.get(reason) ?? [])].sort((left, right) => left.name.localeCompare(right.name));
|
|
423
|
+
const source = classifyReasonSource(reason, knownClassificationReasons, documentedVerificationReasons);
|
|
424
|
+
const intentNames = candidates.map((candidate) => candidate.name);
|
|
425
|
+
const relatedSkills = uniqueSorted(intentNames.flatMap((intent) => skillPathsByIntent.get(intent) ?? []));
|
|
426
|
+
const gaps = [];
|
|
427
|
+
if (candidates.length === 0) {
|
|
428
|
+
gaps.push('missing_required_after');
|
|
429
|
+
}
|
|
430
|
+
else if (!candidates.some((candidate) => candidate.runnable)) {
|
|
431
|
+
gaps.push('no_runnable_intent');
|
|
432
|
+
}
|
|
433
|
+
if (source === 'required_after') {
|
|
434
|
+
gaps.push('unknown_reason');
|
|
435
|
+
}
|
|
436
|
+
if (intentNames.length > 0 && intersectSorted(intentNames, [...skillPathsByIntent.keys()]).length === 0) {
|
|
437
|
+
gaps.push('no_related_skill_route');
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
reason,
|
|
441
|
+
source,
|
|
442
|
+
intents: candidates.map((candidate) => {
|
|
443
|
+
const eligibility = evaluateCommandIntentEligibility(candidate.name, candidate.intent);
|
|
444
|
+
return {
|
|
445
|
+
intent: candidate.name,
|
|
446
|
+
status: eligibility.code,
|
|
447
|
+
runnable: eligibility.ok,
|
|
448
|
+
detail: eligibility.detail,
|
|
449
|
+
};
|
|
450
|
+
}),
|
|
451
|
+
gaps: uniqueSorted(gaps),
|
|
452
|
+
relatedSkills,
|
|
453
|
+
relatedDocs: buildRelatedDocs(source, relatedSkills),
|
|
454
|
+
};
|
|
455
|
+
});
|
|
456
|
+
}
|
|
154
457
|
function hasExplicitEffectMetadata(intent) {
|
|
155
458
|
return Array.isArray(intent.effects) && intent.effects.some((effect) => isRecord(effect));
|
|
156
459
|
}
|
|
@@ -200,6 +503,7 @@ function lintCoverage(contract, options, issues) {
|
|
|
200
503
|
const documentedVerificationReasons = [...DOCUMENTED_VERIFICATION_REASONS];
|
|
201
504
|
const knownReasons = new Set([...knownClassificationReasons, ...documentedVerificationReasons]);
|
|
202
505
|
const reasonToIntents = collectRequiredAfterReasons(contract);
|
|
506
|
+
const skillPathsByIntent = readSkillPathsByIntent(options.projectRoot);
|
|
203
507
|
const requiredAfterReasons = uniqueSorted(reasonToIntents.keys());
|
|
204
508
|
const runnableReasons = uniqueSorted([...reasonToIntents.entries()]
|
|
205
509
|
.filter(([, candidates]) => candidates.some((candidate) => candidate.runnable))
|
|
@@ -236,6 +540,7 @@ function lintCoverage(contract, options, issues) {
|
|
|
236
540
|
documentedVerificationReasons,
|
|
237
541
|
requiredAfterReasons,
|
|
238
542
|
runnableReasons,
|
|
543
|
+
matrix: buildCoverageMatrix(reasonToIntents, knownClassificationReasons, documentedVerificationReasons, skillPathsByIntent),
|
|
239
544
|
findings,
|
|
240
545
|
};
|
|
241
546
|
}
|
|
@@ -249,24 +554,28 @@ export function lintCommandContract(contract, options = {}) {
|
|
|
249
554
|
const issues = [];
|
|
250
555
|
const intentEntries = Object.entries(contract.intents);
|
|
251
556
|
const intentTables = intentEntries
|
|
252
|
-
.map(([name, value]) => lintIntent(name, value, issues))
|
|
253
|
-
.filter((
|
|
557
|
+
.map(([name, value]) => [name, lintIntent(name, value, issues)])
|
|
558
|
+
.filter((entry) => entry[1] !== null);
|
|
559
|
+
lintReferencedPackageScripts(options.projectRoot, intentTables, issues);
|
|
560
|
+
const validIntents = intentTables.map(([, intent]) => intent);
|
|
254
561
|
const coverage = options.coverage === true ? lintCoverage(contract, options, issues) : undefined;
|
|
562
|
+
const suggestions = options.suggest === true ? suggestCommandContracts(options.projectRoot, intentEntries.map(([name]) => name)) : undefined;
|
|
255
563
|
const errors = issues.filter((issue) => issue.severity === 'error').length;
|
|
256
564
|
const warnings = issues.length - errors;
|
|
257
565
|
return {
|
|
258
566
|
status: getStatus(errors, warnings),
|
|
259
567
|
summary: {
|
|
260
568
|
totalIntents: intentEntries.length,
|
|
261
|
-
configured:
|
|
262
|
-
runnable:
|
|
263
|
-
manualOnly:
|
|
264
|
-
unknown:
|
|
569
|
+
configured: validIntents.filter((intent) => readString(intent, 'status') === 'configured').length,
|
|
570
|
+
runnable: validIntents.filter(configuredIntentIsRunnable).length,
|
|
571
|
+
manualOnly: validIntents.filter((intent) => readString(intent, 'status') === 'manual_only').length,
|
|
572
|
+
unknown: validIntents.filter((intent) => readString(intent, 'status') === 'unknown').length,
|
|
265
573
|
errors,
|
|
266
574
|
warnings,
|
|
267
575
|
},
|
|
268
576
|
issues,
|
|
269
577
|
sourceFiles: CONTRACT_LINT_SOURCE_FILES,
|
|
270
578
|
coverage,
|
|
579
|
+
suggestions,
|
|
271
580
|
};
|
|
272
581
|
}
|
|
@@ -68,6 +68,11 @@ function createEmptyDashboardVerificationSnapshot(changedFiles) {
|
|
|
68
68
|
skipped: [],
|
|
69
69
|
schedule: {
|
|
70
70
|
runner: 'serial_mf_run_receipts',
|
|
71
|
+
failurePolicy: {
|
|
72
|
+
mode: 'batch_boundary',
|
|
73
|
+
startedBatch: 'wait_for_completion',
|
|
74
|
+
nextBatch: 'stop_on_failure',
|
|
75
|
+
},
|
|
71
76
|
batches: [],
|
|
72
77
|
entries: [],
|
|
73
78
|
notes: [],
|
|
@@ -104,6 +109,7 @@ export function createDashboardVerificationSnapshot(projectRoot, rawCommandContr
|
|
|
104
109
|
skipped,
|
|
105
110
|
schedule: {
|
|
106
111
|
runner: verificationReport.schedule.runner,
|
|
112
|
+
failurePolicy: verificationReport.schedule.failurePolicy,
|
|
107
113
|
batches: verificationReport.schedule.batches.map((batch) => ({
|
|
108
114
|
index: batch.index,
|
|
109
115
|
intents: batch.intents,
|
|
@@ -113,6 +119,8 @@ export function createDashboardVerificationSnapshot(projectRoot, rawCommandContr
|
|
|
113
119
|
entries: verificationReport.schedule.entries.map((entry) => ({
|
|
114
120
|
intent: entry.intent,
|
|
115
121
|
command: `mf run ${entry.intent}`,
|
|
122
|
+
parallelEligible: entry.parallelEligible,
|
|
123
|
+
parallelReason: entry.parallelReason,
|
|
116
124
|
locks: entry.locks,
|
|
117
125
|
effects: entry.effects.map((effect) => ({
|
|
118
126
|
access: effect.access,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function createExternalEvidenceRisks(checks) {
|
|
2
|
+
return checks
|
|
3
|
+
.filter((check) => check.status !== 'passed')
|
|
4
|
+
.map((check) => ({
|
|
5
|
+
code: 'external_evidence_requires_review',
|
|
6
|
+
severity: 'medium',
|
|
7
|
+
detail: `External ${check.provider} check ${check.name} reported ${check.status}; review it as supporting evidence, not command authority.`,
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
@@ -38,6 +38,13 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
38
38
|
packaged: true,
|
|
39
39
|
documented: true,
|
|
40
40
|
},
|
|
41
|
+
{
|
|
42
|
+
id: 'test-selection',
|
|
43
|
+
schemaFile: 'test-selection.schema.json',
|
|
44
|
+
producer: 'parsed .mustflow/config/test-selection.toml',
|
|
45
|
+
packaged: true,
|
|
46
|
+
documented: true,
|
|
47
|
+
},
|
|
41
48
|
{
|
|
42
49
|
id: 'contract-lint-report',
|
|
43
50
|
schemaFile: 'contract-lint-report.schema.json',
|
|
@@ -46,6 +53,13 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
46
53
|
documented: true,
|
|
47
54
|
installedCommand: ['mf', 'contract-lint', '--json'],
|
|
48
55
|
},
|
|
56
|
+
{
|
|
57
|
+
id: 'dashboard-export',
|
|
58
|
+
schemaFile: 'dashboard-export.schema.json',
|
|
59
|
+
producer: 'mf dashboard --export-json <path>',
|
|
60
|
+
packaged: true,
|
|
61
|
+
documented: true,
|
|
62
|
+
},
|
|
49
63
|
{
|
|
50
64
|
id: 'classify-report',
|
|
51
65
|
schemaFile: 'classify-report.schema.json',
|
|
@@ -71,6 +85,13 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
71
85
|
installedCommand: ['mf', 'line-endings', 'check', '--json'],
|
|
72
86
|
expectedExitCodes: [0, 1],
|
|
73
87
|
},
|
|
88
|
+
{
|
|
89
|
+
id: 'latest-run-pointer',
|
|
90
|
+
schemaFile: 'latest-run-pointer.schema.json',
|
|
91
|
+
producer: '.mustflow/state/runs/latest.json when written by mf verify',
|
|
92
|
+
packaged: true,
|
|
93
|
+
documented: true,
|
|
94
|
+
},
|
|
74
95
|
{
|
|
75
96
|
id: 'handoff-validation-report',
|
|
76
97
|
schemaFile: 'handoff-validation-report.schema.json',
|
|
@@ -111,6 +132,13 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
111
132
|
documented: true,
|
|
112
133
|
installedCommand: ['mf', 'verify', '--reason', 'schema_verify', '--json'],
|
|
113
134
|
},
|
|
135
|
+
{
|
|
136
|
+
id: 'verify-run-manifest',
|
|
137
|
+
schemaFile: 'verify-run-manifest.schema.json',
|
|
138
|
+
producer: '.mustflow/state/runs/verify-latest/manifest.json',
|
|
139
|
+
packaged: true,
|
|
140
|
+
documented: true,
|
|
141
|
+
},
|
|
114
142
|
{
|
|
115
143
|
id: 'change-verification-report',
|
|
116
144
|
schemaFile: 'change-verification-report.schema.json',
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const UNRESOLVED_VERIFY_STATUSES = new Set(['failed', 'blocked', 'partial']);
|
|
2
|
+
export function createRepeatedFailureRisk(input) {
|
|
3
|
+
if (input.previousVerificationPlanId === null ||
|
|
4
|
+
input.previousStatus === null ||
|
|
5
|
+
input.previousVerificationPlanId !== input.currentVerificationPlanId ||
|
|
6
|
+
!UNRESOLVED_VERIFY_STATUSES.has(input.previousStatus) ||
|
|
7
|
+
!UNRESOLVED_VERIFY_STATUSES.has(input.currentStatus)) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
code: 'repeated_verification_failure',
|
|
12
|
+
severity: 'high',
|
|
13
|
+
previous_status: input.previousStatus,
|
|
14
|
+
verification_plan_id: input.currentVerificationPlanId,
|
|
15
|
+
detail: 'The previous verify summary has the same verification_plan_id and an unresolved status; provide new evidence or a narrower hypothesis before marking the task complete.',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const TEXT_FIELD_LABELS = {
|
|
2
|
+
reported_symptom: 'reported symptom',
|
|
3
|
+
expected_behavior: 'expected behavior',
|
|
4
|
+
observed_behavior: 'observed behavior',
|
|
5
|
+
};
|
|
6
|
+
const ITEM_FIELD_LABELS = {
|
|
7
|
+
original_reproduction: 'original reproduction path',
|
|
8
|
+
evidence_before_fix: 'before-fix evidence',
|
|
9
|
+
evidence_after_fix: 'after-fix evidence',
|
|
10
|
+
regression_guard: 'regression guard',
|
|
11
|
+
};
|
|
12
|
+
export function createReproEvidenceRisks(report) {
|
|
13
|
+
if (!report) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
const risks = [];
|
|
17
|
+
for (const [field, label] of Object.entries(TEXT_FIELD_LABELS)) {
|
|
18
|
+
if (!report[field]) {
|
|
19
|
+
risks.push({
|
|
20
|
+
code: 'repro_evidence_missing',
|
|
21
|
+
severity: 'high',
|
|
22
|
+
detail: `Bug-fix repro evidence is missing ${label}; do not mark the task verified from command receipts alone.`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (const [field, label] of Object.entries(ITEM_FIELD_LABELS)) {
|
|
27
|
+
const item = report[field];
|
|
28
|
+
if (item.status === 'missing') {
|
|
29
|
+
risks.push({
|
|
30
|
+
code: 'repro_evidence_missing',
|
|
31
|
+
severity: 'high',
|
|
32
|
+
detail: `Bug-fix repro evidence is missing ${label}; rerun or explicitly mark it unavailable before claiming verification.`,
|
|
33
|
+
});
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (item.status === 'present' && !item.summary) {
|
|
37
|
+
risks.push({
|
|
38
|
+
code: 'repro_evidence_missing',
|
|
39
|
+
severity: 'high',
|
|
40
|
+
detail: `Bug-fix repro evidence marks ${label} present but does not summarize the evidence.`,
|
|
41
|
+
});
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (item.status === 'unavailable' && !item.reason) {
|
|
45
|
+
risks.push({
|
|
46
|
+
code: 'repro_evidence_missing',
|
|
47
|
+
severity: 'high',
|
|
48
|
+
detail: `Bug-fix repro evidence marks ${label} unavailable without explaining why.`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return risks;
|
|
53
|
+
}
|