mustflow 1.31.0 → 2.16.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.
Files changed (66) hide show
  1. package/README.md +23 -9
  2. package/dist/cli/commands/classify.js +61 -6
  3. package/dist/cli/commands/contract-lint.js +13 -4
  4. package/dist/cli/commands/dashboard.js +77 -2
  5. package/dist/cli/commands/explain-verify.js +11 -1
  6. package/dist/cli/commands/index.js +14 -0
  7. package/dist/cli/commands/run.js +4 -1
  8. package/dist/cli/commands/verify.js +986 -43
  9. package/dist/cli/i18n/en.js +61 -10
  10. package/dist/cli/i18n/es.js +61 -10
  11. package/dist/cli/i18n/fr.js +61 -10
  12. package/dist/cli/i18n/hi.js +61 -10
  13. package/dist/cli/i18n/ko.js +61 -10
  14. package/dist/cli/i18n/zh.js +61 -10
  15. package/dist/cli/lib/dashboard-export.js +62 -12
  16. package/dist/cli/lib/dashboard-html/client-script.js +1936 -0
  17. package/dist/cli/lib/dashboard-html/locale-bootstrap.js +8 -0
  18. package/dist/cli/lib/dashboard-html/styles.js +572 -0
  19. package/dist/cli/lib/dashboard-html/template.js +134 -0
  20. package/dist/cli/lib/dashboard-html/types.js +1 -0
  21. package/dist/cli/lib/dashboard-html.js +1 -1907
  22. package/dist/cli/lib/dashboard-locale.js +37 -0
  23. package/dist/cli/lib/local-index/constants.js +48 -0
  24. package/dist/cli/lib/local-index/index.js +2951 -0
  25. package/dist/cli/lib/local-index/sql.js +15 -0
  26. package/dist/cli/lib/local-index/types.js +1 -0
  27. package/dist/cli/lib/local-index.js +1 -1911
  28. package/dist/cli/lib/run-plan.js +76 -1
  29. package/dist/cli/lib/templates.js +18 -1
  30. package/dist/cli/lib/validation/command-intents.js +11 -0
  31. package/dist/cli/lib/validation/constants.js +238 -0
  32. package/dist/cli/lib/validation/index.js +1384 -0
  33. package/dist/cli/lib/validation/primitives.js +198 -0
  34. package/dist/cli/lib/validation/test-selection.js +95 -0
  35. package/dist/cli/lib/validation/types.js +1 -0
  36. package/dist/cli/lib/validation.js +1 -1770
  37. package/dist/core/check-issues.js +6 -0
  38. package/dist/core/completion-verdict.js +341 -0
  39. package/dist/core/contract-lint.js +221 -6
  40. package/dist/core/external-evidence.js +9 -0
  41. package/dist/core/public-json-contracts.js +21 -0
  42. package/dist/core/repeated-failure.js +179 -0
  43. package/dist/core/repro-evidence.js +134 -0
  44. package/dist/core/scope-risk.js +64 -0
  45. package/dist/core/skill-route-alignment.js +20 -0
  46. package/dist/core/source-anchor-status.js +4 -1
  47. package/dist/core/test-selection.js +3 -0
  48. package/dist/core/validation-ratchet.js +196 -0
  49. package/dist/core/verification-evidence.js +249 -0
  50. package/examples/README.md +12 -4
  51. package/package.json +3 -3
  52. package/schemas/README.md +13 -3
  53. package/schemas/change-verification-report.schema.json +16 -2
  54. package/schemas/commands.schema.json +4 -0
  55. package/schemas/contract-lint-report.schema.json +29 -0
  56. package/schemas/dashboard-export.schema.json +310 -0
  57. package/schemas/explain-report.schema.json +173 -1
  58. package/schemas/latest-run-pointer.schema.json +601 -0
  59. package/schemas/run-receipt.schema.json +4 -0
  60. package/schemas/test-selection.schema.json +81 -0
  61. package/schemas/verify-report.schema.json +578 -1
  62. package/schemas/verify-run-manifest.schema.json +627 -0
  63. package/templates/default/i18n.toml +1 -1
  64. package/templates/default/locales/en/.mustflow/skills/INDEX.md +124 -29
  65. package/templates/default/locales/en/.mustflow/skills/routes.toml +289 -0
  66. package/templates/default/manifest.toml +29 -2
@@ -35,6 +35,12 @@ const CHECK_ISSUE_ID_RULES = [
35
35
  ['mustflow.skill.index_route_broad_catch_all', /^Strict warning: \.mustflow\/skills\/INDEX\.md \.mustflow\/skills\/[^/]+\/SKILL\.md route uses broad catch-all trigger ".+" that can shadow narrower skills$/u],
36
36
  ['mustflow.skill.index_route_identical_trigger', /^Strict warning: \.mustflow\/skills\/INDEX\.md \.mustflow\/skills\/[^/]+\/SKILL\.md and \.mustflow\/skills\/[^/]+\/SKILL\.md have identical skill route trigger text$/u],
37
37
  ['mustflow.skill.index_route_duplicate_surface', /^Strict warning: \.mustflow\/skills\/INDEX\.md \.mustflow\/skills\/[^/]+\/SKILL\.md and \.mustflow\/skills\/[^/]+\/SKILL\.md have duplicate edit scope, risk, and expected output route surface$/u],
38
+ ['mustflow.skill.route_metadata_missing', /^Strict: \.mustflow\/skills\/routes\.toml is missing metadata for route "[^"]+"$/u],
39
+ ['mustflow.skill.route_metadata_unlisted', /^Strict: \.mustflow\/skills\/routes\.toml route "[^"]+" is not listed in \.mustflow\/skills\/INDEX\.md$/u],
40
+ ['mustflow.skill.route_metadata_missing_document', /^Strict: \.mustflow\/skills\/routes\.toml route "[^"]+" points to a missing skill document$/u],
41
+ ['mustflow.skill.route_metadata_category_mismatch', /^Strict: \.mustflow\/skills\/INDEX\.md route "[^"]+" must appear under the .+ category section from \.mustflow\/skills\/routes\.toml$/u],
42
+ ['mustflow.skill.route_metadata_unknown_reference', /^Strict: \.mustflow\/skills\/routes\.toml route "[^"]+" references unknown mutually exclusive route "[^"]+"$/u],
43
+ ['mustflow.skill.route_metadata_asymmetric_exclusion', /^Strict warning: \.mustflow\/skills\/routes\.toml route "[^"]+" lists "[^"]+" as mutually exclusive but the reverse route does not$/u],
38
44
  ['mustflow.skill.resource_unknown_command_intent', /^Strict: \.mustflow\/skills\/[^/]+\/resources\.toml script [^\s]+ references unknown command intent "[^"]+"$/u],
39
45
  ['mustflow.source_anchor.invalid_format', /^Strict: source anchor .+ has invalid format:/u],
40
46
  ['mustflow.source_anchor.duplicate_id', /^Strict: source anchor id "[^"]+" is duplicated:/u],
@@ -0,0 +1,341 @@
1
+ function createRiskEvidence(input) {
2
+ return {
3
+ source_anchor: input.sourceAnchorRiskCount ?? 0,
4
+ scope_diff: input.scopeDiffRiskCount ?? 0,
5
+ repeated_failure: input.repeatedFailureCount ?? 0,
6
+ validation_ratchet: input.validationRatchetRiskCount ?? 0,
7
+ repro_evidence: input.reproEvidenceRiskCount ?? 0,
8
+ external_evidence: input.externalEvidenceRiskCount ?? 0,
9
+ write_drift: input.writeDriftRiskCount ?? 0,
10
+ receipt_binding: input.receiptBindingRiskCount ?? 0,
11
+ stale_receipt: input.staleReceiptCount ?? 0,
12
+ plan_mismatch: input.planMismatchCount ?? 0,
13
+ };
14
+ }
15
+ function emptyReceiptBindingEvidence() {
16
+ return {
17
+ plan_bound_count: 0,
18
+ plan_unbound_count: 0,
19
+ fingerprint_bound_count: 0,
20
+ fingerprint_unbound_count: 0,
21
+ current_state_bound_count: 0,
22
+ current_state_unavailable_count: 0,
23
+ stale_count: 0,
24
+ plan_mismatch_count: 0,
25
+ };
26
+ }
27
+ function emptyCriteriaEvidence() {
28
+ return {
29
+ total: 0,
30
+ covered: 0,
31
+ partially_covered: 0,
32
+ uncovered: 0,
33
+ blocked: 0,
34
+ contradicted: 0,
35
+ };
36
+ }
37
+ function normalizeVerifyCompletionInput(input) {
38
+ const missingReceiptCount = Math.max(0, input.ranIntents - input.receiptCount);
39
+ if (missingReceiptCount === 0) {
40
+ return input;
41
+ }
42
+ return {
43
+ ...input,
44
+ receiptBindingRiskCount: (input.receiptBindingRiskCount ?? 0) + missingReceiptCount,
45
+ };
46
+ }
47
+ function verifyStatus(input) {
48
+ const contradictions = [];
49
+ if (input.failedIntents > 0) {
50
+ contradictions.push('one_or_more_selected_verification_intents_failed');
51
+ }
52
+ if ((input.planMismatchCount ?? 0) > 0) {
53
+ contradictions.push('plan_receipt_mismatch');
54
+ }
55
+ if ((input.reproEvidenceContradictionCount ?? 0) > 0) {
56
+ contradictions.push('repro_evidence_contradicted');
57
+ }
58
+ if ((input.validationRatchetContradictionCount ?? 0) > 0) {
59
+ contradictions.push('validation_ratchet_contradicted');
60
+ }
61
+ if (contradictions.length > 0) {
62
+ if (input.failedIntents > 0 && (input.repeatedFailureCount ?? 0) > 0) {
63
+ contradictions.push('repeated_verification_failure');
64
+ }
65
+ return {
66
+ status: 'contradicted',
67
+ primaryReason: input.failedIntents > 0
68
+ ? 'verification_failed'
69
+ : (input.planMismatchCount ?? 0) > 0
70
+ ? 'plan_receipt_mismatch'
71
+ : (input.reproEvidenceContradictionCount ?? 0) > 0
72
+ ? 'repro_evidence_contradicted'
73
+ : 'validation_ratchet_contradicted',
74
+ blockers: [],
75
+ contradictions,
76
+ limitations: [],
77
+ };
78
+ }
79
+ if ((input.repeatedFailureBlockerCount ?? 0) > 0) {
80
+ return {
81
+ status: 'blocked',
82
+ primaryReason: 'repeated_failure_requires_new_evidence',
83
+ blockers: ['repeated_failure_requires_new_evidence'],
84
+ contradictions: [],
85
+ limitations: [],
86
+ };
87
+ }
88
+ if (input.ranIntents === 0 && input.skippedIntents > 0) {
89
+ const blockers = ['all_matching_verification_intents_were_skipped'];
90
+ if ((input.repeatedFailureCount ?? 0) > 0) {
91
+ blockers.push('repeated_verification_failure');
92
+ }
93
+ return {
94
+ status: 'blocked',
95
+ primaryReason: 'no_runnable_verification_intents',
96
+ blockers,
97
+ contradictions: [],
98
+ limitations: [],
99
+ };
100
+ }
101
+ if (input.ranIntents === 0) {
102
+ const limitations = ['no_verification_intents_ran'];
103
+ if ((input.repeatedFailureCount ?? 0) > 0) {
104
+ limitations.push('repeated_verification_failure');
105
+ }
106
+ return {
107
+ status: 'unverified',
108
+ primaryReason: 'no_verification_evidence',
109
+ blockers: [],
110
+ contradictions: [],
111
+ limitations,
112
+ };
113
+ }
114
+ if (input.skippedIntents > 0) {
115
+ const limitations = ['one_or_more_matching_verification_intents_were_skipped'];
116
+ if ((input.repeatedFailureCount ?? 0) > 0) {
117
+ limitations.push('repeated_verification_failure');
118
+ }
119
+ return {
120
+ status: 'partially_verified',
121
+ primaryReason: 'some_verification_skipped',
122
+ blockers: [],
123
+ contradictions: [],
124
+ limitations,
125
+ };
126
+ }
127
+ if ((input.reproEvidenceUnverifiedCount ?? 0) > 0) {
128
+ return {
129
+ status: 'unverified',
130
+ primaryReason: 'repro_evidence_unverified',
131
+ blockers: [],
132
+ contradictions: [],
133
+ limitations: ['repro_evidence_missing'],
134
+ };
135
+ }
136
+ const downgradeLimitations = [];
137
+ if ((input.sourceAnchorRiskCount ?? 0) > 0) {
138
+ downgradeLimitations.push('high_risk_source_anchor_requires_review');
139
+ }
140
+ if ((input.scopeDiffRiskCount ?? 0) > 0) {
141
+ downgradeLimitations.push('scope_diff_risk_requires_review');
142
+ }
143
+ if ((input.validationRatchetRiskCount ?? 0) > 0) {
144
+ downgradeLimitations.push('validation_ratchet_risk_requires_review');
145
+ }
146
+ if ((input.writeDriftRiskCount ?? 0) > 0) {
147
+ downgradeLimitations.push('write_drift_requires_review');
148
+ }
149
+ if ((input.receiptBindingRiskCount ?? 0) > 0) {
150
+ downgradeLimitations.push('receipt_binding_requires_review');
151
+ }
152
+ if ((input.staleReceiptCount ?? 0) > 0) {
153
+ downgradeLimitations.push('stale_receipt_requires_review');
154
+ }
155
+ if ((input.reproEvidenceRiskCount ?? 0) > 0) {
156
+ downgradeLimitations.push('repro_evidence_missing');
157
+ }
158
+ if ((input.externalEvidenceRiskCount ?? 0) > 0) {
159
+ downgradeLimitations.push('external_evidence_requires_review');
160
+ }
161
+ if (downgradeLimitations.length > 0) {
162
+ return {
163
+ status: 'partially_verified',
164
+ primaryReason: (input.sourceAnchorRiskCount ?? 0) > 0
165
+ ? 'source_anchor_invariant_review_required'
166
+ : (input.scopeDiffRiskCount ?? 0) > 0
167
+ ? 'scope_diff_review_required'
168
+ : (input.validationRatchetRiskCount ?? 0) > 0
169
+ ? 'validation_ratchet_review_required'
170
+ : (input.writeDriftRiskCount ?? 0) > 0
171
+ ? 'write_drift_review_required'
172
+ : (input.receiptBindingRiskCount ?? 0) > 0
173
+ ? 'receipt_binding_review_required'
174
+ : (input.staleReceiptCount ?? 0) > 0
175
+ ? 'stale_receipt_review_required'
176
+ : (input.reproEvidenceRiskCount ?? 0) > 0
177
+ ? 'repro_evidence_missing'
178
+ : 'external_evidence_review_required',
179
+ blockers: [],
180
+ contradictions: [],
181
+ limitations: downgradeLimitations,
182
+ };
183
+ }
184
+ if (input.passedIntents === input.ranIntents) {
185
+ return {
186
+ status: 'verified',
187
+ primaryReason: 'all_selected_verification_passed',
188
+ blockers: [],
189
+ contradictions: [],
190
+ limitations: [],
191
+ };
192
+ }
193
+ return {
194
+ status: 'unverified',
195
+ primaryReason: 'verification_evidence_inconclusive',
196
+ blockers: [],
197
+ contradictions: [],
198
+ limitations: ['selected_verification_did_not_produce_a_clear_pass_or_fail'],
199
+ };
200
+ }
201
+ export function createVerifyCompletionVerdict(input) {
202
+ const normalizedInput = normalizeVerifyCompletionInput(input);
203
+ const result = verifyStatus(normalizedInput);
204
+ const risks = createRiskEvidence(normalizedInput);
205
+ const receiptBinding = normalizedInput.receiptBinding ?? emptyReceiptBindingEvidence();
206
+ const criteria = normalizedInput.criteria ?? emptyCriteriaEvidence();
207
+ return {
208
+ schema_version: '1',
209
+ status: result.status,
210
+ primary_reason: result.primaryReason,
211
+ evidence: {
212
+ source: 'mf_verify',
213
+ verification_plan_id: normalizedInput.verificationPlanId,
214
+ changed_file_count: null,
215
+ criteria,
216
+ matched_intents: normalizedInput.matchedIntents,
217
+ ran_intents: normalizedInput.ranIntents,
218
+ passed_intents: normalizedInput.passedIntents,
219
+ failed_intents: normalizedInput.failedIntents,
220
+ skipped_intents: normalizedInput.skippedIntents,
221
+ receipt_count: normalizedInput.receiptCount,
222
+ gap_count: normalizedInput.skippedIntents,
223
+ source_anchor_risk_count: normalizedInput.sourceAnchorRiskCount ?? 0,
224
+ scope_diff_risk_count: normalizedInput.scopeDiffRiskCount ?? 0,
225
+ repeated_failure_count: normalizedInput.repeatedFailureCount ?? 0,
226
+ validation_ratchet_risk_count: normalizedInput.validationRatchetRiskCount ?? 0,
227
+ repro_evidence_risk_count: normalizedInput.reproEvidenceRiskCount ?? 0,
228
+ external_evidence_risk_count: normalizedInput.externalEvidenceRiskCount ?? 0,
229
+ write_drift_risk_count: normalizedInput.writeDriftRiskCount ?? 0,
230
+ receipt_binding_risk_count: normalizedInput.receiptBindingRiskCount ?? 0,
231
+ stale_receipt_count: normalizedInput.staleReceiptCount ?? 0,
232
+ plan_mismatch_count: normalizedInput.planMismatchCount ?? 0,
233
+ risks,
234
+ receipt_binding: receiptBinding,
235
+ latest_run_status: null,
236
+ },
237
+ blockers: result.blockers,
238
+ contradictions: result.contradictions,
239
+ limitations: result.limitations,
240
+ };
241
+ }
242
+ export function createDashboardCompletionVerdict(input) {
243
+ const risks = createRiskEvidence(input);
244
+ const receiptBinding = input.receiptBinding ?? emptyReceiptBindingEvidence();
245
+ const latestRunFailed = input.latestRunStatus === 'failed' ||
246
+ input.latestRunStatus === 'timed_out' ||
247
+ input.latestRunStatus === 'start_failed';
248
+ let status = 'unverified';
249
+ let primaryReason = 'dashboard_does_not_execute_verification';
250
+ const blockers = [];
251
+ const contradictions = [];
252
+ const limitations = ['dashboard_export_is_read_only'];
253
+ if (latestRunFailed) {
254
+ status = 'contradicted';
255
+ primaryReason = 'latest_run_failed';
256
+ contradictions.push('latest_run_status_is_not_passing');
257
+ }
258
+ else if ((input.sourceAnchorRiskCount ?? 0) > 0) {
259
+ status = 'partially_verified';
260
+ primaryReason = 'source_anchor_invariant_review_required';
261
+ limitations.push('high_risk_source_anchor_requires_review');
262
+ if ((input.scopeDiffRiskCount ?? 0) > 0) {
263
+ limitations.push('scope_diff_risk_requires_review');
264
+ }
265
+ }
266
+ else if ((input.scopeDiffRiskCount ?? 0) > 0) {
267
+ status = 'partially_verified';
268
+ primaryReason = 'scope_diff_review_required';
269
+ limitations.push('scope_diff_risk_requires_review');
270
+ }
271
+ else if (input.gapCount > 0) {
272
+ status = 'blocked';
273
+ primaryReason = 'verification_gaps_present';
274
+ blockers.push('dashboard_verification_graph_reports_gaps');
275
+ }
276
+ else if (input.changedFileCount > 0 && !input.latestRunExists) {
277
+ status = 'unverified';
278
+ primaryReason = 'changed_files_without_run_receipt';
279
+ limitations.push('no_latest_run_receipt');
280
+ }
281
+ else if (input.changedFileCount > 0 && !input.latestRunValid) {
282
+ status = 'unverified';
283
+ primaryReason = 'changed_files_with_invalid_run_receipt';
284
+ limitations.push('latest_run_receipt_is_invalid');
285
+ }
286
+ else if (input.changedFileCount > 0 && input.runnableIntentCount > 0) {
287
+ status = 'partially_verified';
288
+ primaryReason = 'verification_recommendations_available';
289
+ limitations.push('dashboard_recommendations_are_not_executed_receipts');
290
+ }
291
+ else if (input.latestRunValid && input.latestRunStatus === 'passed') {
292
+ status = 'partially_verified';
293
+ primaryReason = 'latest_run_passed_without_current_claim_binding';
294
+ limitations.push('latest_run_is_not_bound_to_a_current_completion_claim');
295
+ }
296
+ const criteria = input.criteria ??
297
+ (input.changedFileCount > 0 || input.runnableIntentCount > 0 || input.skippedIntentCount > 0 || input.gapCount > 0
298
+ ? {
299
+ total: 1,
300
+ covered: 0,
301
+ partially_covered: status === 'partially_verified' ? 1 : 0,
302
+ uncovered: status === 'unverified' ? 1 : 0,
303
+ blocked: status === 'blocked' ? 1 : 0,
304
+ contradicted: status === 'contradicted' ? 1 : 0,
305
+ }
306
+ : emptyCriteriaEvidence());
307
+ return {
308
+ schema_version: '1',
309
+ status,
310
+ primary_reason: primaryReason,
311
+ evidence: {
312
+ source: 'dashboard_export',
313
+ verification_plan_id: null,
314
+ changed_file_count: input.changedFileCount,
315
+ criteria,
316
+ matched_intents: input.runnableIntentCount + input.skippedIntentCount,
317
+ ran_intents: 0,
318
+ passed_intents: 0,
319
+ failed_intents: latestRunFailed ? 1 : 0,
320
+ skipped_intents: input.skippedIntentCount,
321
+ receipt_count: input.latestRunExists && input.latestRunValid ? 1 : 0,
322
+ gap_count: input.gapCount,
323
+ source_anchor_risk_count: input.sourceAnchorRiskCount ?? 0,
324
+ scope_diff_risk_count: input.scopeDiffRiskCount ?? 0,
325
+ repeated_failure_count: input.repeatedFailureCount ?? 0,
326
+ validation_ratchet_risk_count: input.validationRatchetRiskCount ?? 0,
327
+ repro_evidence_risk_count: input.reproEvidenceRiskCount ?? 0,
328
+ external_evidence_risk_count: input.externalEvidenceRiskCount ?? 0,
329
+ write_drift_risk_count: input.writeDriftRiskCount ?? 0,
330
+ receipt_binding_risk_count: input.receiptBindingRiskCount ?? 0,
331
+ stale_receipt_count: input.staleReceiptCount ?? 0,
332
+ plan_mismatch_count: input.planMismatchCount ?? 0,
333
+ risks,
334
+ receipt_binding: receiptBinding,
335
+ latest_run_status: input.latestRunStatus,
336
+ },
337
+ blockers,
338
+ contradictions,
339
+ limitations,
340
+ };
341
+ }
@@ -48,6 +48,9 @@ const COMMANDS_CONFIG_PATH = '.mustflow/config/commands.toml';
48
48
  const SKILL_INDEX_PATH = '.mustflow/skills/INDEX.md';
49
49
  const CHANGE_CLASSIFICATION_SOURCE_PATH = 'src/core/change-classification.ts';
50
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'];
51
54
  function uniqueSorted(values) {
52
55
  return [...new Set(values)].sort((left, right) => left.localeCompare(right));
53
56
  }
@@ -67,6 +70,195 @@ function writesAreValid(intent) {
67
70
  const value = intent.writes;
68
71
  return value === undefined || (Array.isArray(value) && value.every((entry) => typeof entry === 'string'));
69
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
+ }
70
262
  function pushIssue(issues, severity, code, intent, message) {
71
263
  issues.push({ severity, code, intent, message });
72
264
  }
@@ -143,6 +335,25 @@ function lintIntent(name, value, issues) {
143
335
  }
144
336
  return value;
145
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
+ }
146
357
  function collectRequiredAfterReasons(contract) {
147
358
  const reasonToIntents = new Map();
148
359
  for (const [name, intent] of Object.entries(contract.intents)) {
@@ -343,24 +554,28 @@ export function lintCommandContract(contract, options = {}) {
343
554
  const issues = [];
344
555
  const intentEntries = Object.entries(contract.intents);
345
556
  const intentTables = intentEntries
346
- .map(([name, value]) => lintIntent(name, value, issues))
347
- .filter((intent) => intent !== null);
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);
348
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;
349
563
  const errors = issues.filter((issue) => issue.severity === 'error').length;
350
564
  const warnings = issues.length - errors;
351
565
  return {
352
566
  status: getStatus(errors, warnings),
353
567
  summary: {
354
568
  totalIntents: intentEntries.length,
355
- configured: intentTables.filter((intent) => readString(intent, 'status') === 'configured').length,
356
- runnable: intentTables.filter(configuredIntentIsRunnable).length,
357
- manualOnly: intentTables.filter((intent) => readString(intent, 'status') === 'manual_only').length,
358
- unknown: intentTables.filter((intent) => readString(intent, 'status') === 'unknown').length,
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,
359
573
  errors,
360
574
  warnings,
361
575
  },
362
576
  issues,
363
577
  sourceFiles: CONTRACT_LINT_SOURCE_FILES,
364
578
  coverage,
579
+ suggestions,
365
580
  };
366
581
  }
@@ -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',
@@ -78,6 +85,13 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
78
85
  installedCommand: ['mf', 'line-endings', 'check', '--json'],
79
86
  expectedExitCodes: [0, 1],
80
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
+ },
81
95
  {
82
96
  id: 'handoff-validation-report',
83
97
  schemaFile: 'handoff-validation-report.schema.json',
@@ -118,6 +132,13 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
118
132
  documented: true,
119
133
  installedCommand: ['mf', 'verify', '--reason', 'schema_verify', '--json'],
120
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
+ },
121
142
  {
122
143
  id: 'change-verification-report',
123
144
  schemaFile: 'change-verification-report.schema.json',