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.
Files changed (82) hide show
  1. package/README.md +35 -11
  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 +6 -0
  5. package/dist/cli/commands/index.js +5 -0
  6. package/dist/cli/commands/run.js +224 -48
  7. package/dist/cli/commands/upgrade.js +65 -0
  8. package/dist/cli/commands/verify.js +550 -33
  9. package/dist/cli/i18n/en.js +73 -10
  10. package/dist/cli/i18n/es.js +73 -10
  11. package/dist/cli/i18n/fr.js +73 -10
  12. package/dist/cli/i18n/hi.js +73 -10
  13. package/dist/cli/i18n/ko.js +73 -10
  14. package/dist/cli/i18n/zh.js +73 -10
  15. package/dist/cli/index.js +27 -46
  16. package/dist/cli/lib/command-registry.js +5 -0
  17. package/dist/cli/lib/dashboard-export.js +62 -12
  18. package/dist/cli/lib/dashboard-html/client-script.js +1936 -0
  19. package/dist/cli/lib/dashboard-html/locale-bootstrap.js +8 -0
  20. package/dist/cli/lib/dashboard-html/styles.js +572 -0
  21. package/dist/cli/lib/dashboard-html/template.js +134 -0
  22. package/dist/cli/lib/dashboard-html/types.js +1 -0
  23. package/dist/cli/lib/dashboard-html.js +1 -1907
  24. package/dist/cli/lib/dashboard-locale.js +37 -0
  25. package/dist/cli/lib/local-index/constants.js +48 -0
  26. package/dist/cli/lib/local-index/index.js +2256 -0
  27. package/dist/cli/lib/local-index/sql.js +15 -0
  28. package/dist/cli/lib/local-index/types.js +1 -0
  29. package/dist/cli/lib/local-index.js +1 -1908
  30. package/dist/cli/lib/reporter.js +6 -0
  31. package/dist/cli/lib/run-plan.js +96 -4
  32. package/dist/cli/lib/templates.js +18 -1
  33. package/dist/cli/lib/validation/command-intents.js +11 -0
  34. package/dist/cli/lib/validation/constants.js +238 -0
  35. package/dist/cli/lib/validation/index.js +1384 -0
  36. package/dist/cli/lib/validation/primitives.js +198 -0
  37. package/dist/cli/lib/validation/test-selection.js +95 -0
  38. package/dist/cli/lib/validation/types.js +1 -0
  39. package/dist/cli/lib/validation.js +1 -1661
  40. package/dist/core/bounded-output.js +38 -0
  41. package/dist/core/change-classification.js +6 -2
  42. package/dist/core/change-verification.js +240 -6
  43. package/dist/core/check-issues.js +12 -0
  44. package/dist/core/command-contract-validation.js +20 -0
  45. package/dist/core/command-effects.js +13 -0
  46. package/dist/core/completion-verdict.js +209 -0
  47. package/dist/core/contract-lint.js +316 -7
  48. package/dist/core/dashboard-verification.js +8 -0
  49. package/dist/core/external-evidence.js +9 -0
  50. package/dist/core/public-json-contracts.js +28 -0
  51. package/dist/core/repeated-failure.js +17 -0
  52. package/dist/core/repro-evidence.js +53 -0
  53. package/dist/core/run-performance-history.js +307 -0
  54. package/dist/core/run-profile.js +87 -0
  55. package/dist/core/run-receipt.js +171 -4
  56. package/dist/core/run-write-drift.js +18 -2
  57. package/dist/core/scope-risk.js +64 -0
  58. package/dist/core/skill-route-alignment.js +110 -0
  59. package/dist/core/source-anchor-status.js +4 -1
  60. package/dist/core/test-selection.js +227 -0
  61. package/dist/core/validation-ratchet.js +52 -0
  62. package/dist/core/verification-decision-graph.js +67 -0
  63. package/dist/core/verification-evidence.js +249 -0
  64. package/dist/core/verification-scheduler.js +96 -2
  65. package/examples/README.md +12 -4
  66. package/package.json +1 -1
  67. package/schemas/README.md +18 -4
  68. package/schemas/change-verification-report.schema.json +169 -5
  69. package/schemas/commands.schema.json +51 -1
  70. package/schemas/contract-lint-report.schema.json +80 -0
  71. package/schemas/dashboard-export.schema.json +500 -0
  72. package/schemas/explain-report.schema.json +2 -0
  73. package/schemas/latest-run-pointer.schema.json +384 -0
  74. package/schemas/run-receipt.schema.json +113 -0
  75. package/schemas/test-selection.schema.json +81 -0
  76. package/schemas/verify-report.schema.json +361 -1
  77. package/schemas/verify-run-manifest.schema.json +410 -0
  78. package/templates/default/common/.mustflow/config/commands.toml +1 -1
  79. package/templates/default/i18n.toml +1 -1
  80. package/templates/default/locales/en/.mustflow/skills/INDEX.md +124 -29
  81. package/templates/default/locales/en/.mustflow/skills/routes.toml +289 -0
  82. package/templates/default/manifest.toml +29 -2
@@ -0,0 +1,38 @@
1
+ export class BoundedOutputBuffer {
2
+ #maxTailBytes;
3
+ #chunks = [];
4
+ #tailBytes = 0;
5
+ #bytes = 0;
6
+ constructor(maxTailBytes) {
7
+ this.#maxTailBytes = Math.max(0, maxTailBytes);
8
+ }
9
+ append(chunk) {
10
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, 'utf8');
11
+ this.#bytes += buffer.byteLength;
12
+ if (this.#maxTailBytes === 0 || buffer.byteLength === 0) {
13
+ return;
14
+ }
15
+ this.#chunks.push(buffer);
16
+ this.#tailBytes += buffer.byteLength;
17
+ while (this.#tailBytes > this.#maxTailBytes && this.#chunks.length > 0) {
18
+ const first = this.#chunks[0];
19
+ const overflow = this.#tailBytes - this.#maxTailBytes;
20
+ if (!first) {
21
+ break;
22
+ }
23
+ if (first.byteLength <= overflow) {
24
+ this.#chunks.shift();
25
+ this.#tailBytes -= first.byteLength;
26
+ continue;
27
+ }
28
+ this.#chunks[0] = first.subarray(overflow);
29
+ this.#tailBytes -= overflow;
30
+ }
31
+ }
32
+ toSnapshot() {
33
+ return {
34
+ bytes: this.#bytes,
35
+ tail: Buffer.concat(this.#chunks, this.#tailBytes).toString('utf8'),
36
+ };
37
+ }
38
+ }
@@ -14,7 +14,8 @@ function surface(kind, category, isPublicSurface, validationReasons, affectedCon
14
14
  driftChecks,
15
15
  };
16
16
  }
17
- const UNKNOWN_SURFACE = surface('unclassified_path', 'unknown', false, [], [], 'not_applicable', []);
17
+ const UNKNOWN_CHANGE_REASON = 'unknown_change';
18
+ const UNKNOWN_SURFACE = surface('unclassified_path', 'unknown', false, [UNKNOWN_CHANGE_REASON], ['unclassified repository path'], 'not_applicable', ['classification rule coverage']);
18
19
  function rule(id, match, changeKinds, surfaceContract) {
19
20
  return {
20
21
  id,
@@ -80,7 +81,10 @@ export function listChangeClassificationRuleDescriptors() {
80
81
  }));
81
82
  }
82
83
  export function listChangeClassificationValidationReasons() {
83
- return uniqueSorted(CHANGE_CLASSIFICATION_RULES.flatMap((classificationRule) => classificationRule.surface.validationReasons));
84
+ return uniqueSorted([
85
+ ...CHANGE_CLASSIFICATION_RULES.flatMap((classificationRule) => classificationRule.surface.validationReasons),
86
+ ...UNKNOWN_SURFACE.validationReasons,
87
+ ]);
84
88
  }
85
89
  export function createChangeClassificationReport(source, relativePaths) {
86
90
  const files = uniqueSorted(relativePaths.map(normalizeStatusPath).filter((filePath) => filePath.length > 0));
@@ -1,7 +1,9 @@
1
+ import { isRecord, readStringArray } from './config-loading.js';
1
2
  import { CHANGE_CLASSIFICATION_SURFACE_AUTHORITY, createPathTarget, } from './surface-decision-model.js';
2
- import { createVerificationPlan, } from './verification-plan.js';
3
+ import { classifyVerificationCandidate, createVerificationPlan, } from './verification-plan.js';
3
4
  import { createVerificationDecisionGraph, } from './verification-decision-graph.js';
4
5
  import { createVerificationSchedule, } from './verification-scheduler.js';
6
+ import { createProjectTestSelectionPlan, } from './test-selection.js';
5
7
  export const CHANGE_VERIFICATION_SCHEMA_VERSION = '1';
6
8
  function uniqueSorted(values) {
7
9
  return [...new Set(values)].sort((left, right) => left.localeCompare(right));
@@ -9,6 +11,21 @@ function uniqueSorted(values) {
9
11
  function uniqueUpdatePolicies(values) {
10
12
  return uniqueSorted([...values].filter((value) => value !== 'not_applicable'));
11
13
  }
14
+ const DECLARED_ESCALATION_SIGNALS = [
15
+ 'public_api',
16
+ 'release',
17
+ 'package',
18
+ 'packaging',
19
+ 'security',
20
+ 'selector',
21
+ 'command contract',
22
+ 'classifier',
23
+ 'classification',
24
+ 'verification planner',
25
+ 'verification planning',
26
+ 'machine-readable output contract',
27
+ 'JSON schema',
28
+ ];
12
29
  function classificationsForReason(classificationReport, reason) {
13
30
  return classificationReport.classifications.filter((classification) => classification.surface.validationReasons.includes(reason));
14
31
  }
@@ -40,13 +57,209 @@ function createVerificationRequirement(classificationReport, reason) {
40
57
  source: 'change_classification',
41
58
  };
42
59
  }
43
- function toChangeVerificationCandidate(reason, candidate) {
60
+ function candidateKey(reason, intent) {
61
+ return `${reason}\0${intent}`;
62
+ }
63
+ function readIntentRelationList(commandContract, intent, key) {
64
+ const rawIntent = commandContract.intents[intent];
65
+ if (!isRecord(rawIntent) || !isRecord(rawIntent.relations)) {
66
+ return [];
67
+ }
68
+ return readStringArray(rawIntent.relations, key) ?? [];
69
+ }
70
+ function readIntentSelectionList(commandContract, intent, key) {
71
+ const rawIntent = commandContract.intents[intent];
72
+ if (!isRecord(rawIntent) || !isRecord(rawIntent.selection)) {
73
+ return [];
74
+ }
75
+ return readStringArray(rawIntent.selection, key) ?? [];
76
+ }
77
+ function readIntentCostExpectedSeconds(commandContract, intent) {
78
+ const rawIntent = commandContract.intents[intent];
79
+ if (!isRecord(rawIntent) || !isRecord(rawIntent.cost)) {
80
+ return null;
81
+ }
82
+ const expectedSeconds = rawIntent.cost.expected_seconds;
83
+ return Number.isInteger(expectedSeconds) && Number(expectedSeconds) >= 0 ? Number(expectedSeconds) : null;
84
+ }
85
+ function intentCoverageSignature(commandContract, intent) {
86
+ const rawIntent = commandContract.intents[intent];
87
+ if (!isRecord(rawIntent) || !isRecord(rawIntent.covers)) {
88
+ return null;
89
+ }
90
+ const signature = {
91
+ contracts: uniqueSorted(readStringArray(rawIntent.covers, 'contracts') ?? []),
92
+ paths: uniqueSorted(readStringArray(rawIntent.covers, 'paths') ?? []),
93
+ reasons: uniqueSorted(readStringArray(rawIntent.covers, 'reasons') ?? []),
94
+ surfaces: uniqueSorted(readStringArray(rawIntent.covers, 'surfaces') ?? []),
95
+ };
96
+ if (signature.contracts.length === 0 &&
97
+ signature.paths.length === 0 &&
98
+ signature.reasons.length === 0 &&
99
+ signature.surfaces.length === 0) {
100
+ return null;
101
+ }
102
+ return JSON.stringify(signature);
103
+ }
104
+ function fallbackIntentsForCandidate(commandContract, candidate) {
105
+ if (candidate.intent.length === 0 || candidate.status === 'runnable') {
106
+ return [];
107
+ }
108
+ return uniqueSorted([
109
+ ...readIntentSelectionList(commandContract, candidate.intent, 'fallback_intents'),
110
+ ...readIntentSelectionList(commandContract, candidate.intent, 'escalate_to'),
111
+ ...readIntentRelationList(commandContract, candidate.intent, 'escalate_to'),
112
+ ]);
113
+ }
114
+ function escalationIntentsForCandidate(commandContract, candidate) {
115
+ if (candidate.intent.length === 0) {
116
+ return [];
117
+ }
118
+ return uniqueSorted([
119
+ ...readIntentSelectionList(commandContract, candidate.intent, 'escalate_to'),
120
+ ...readIntentRelationList(commandContract, candidate.intent, 'escalate_to'),
121
+ ]);
122
+ }
123
+ function expandCandidatesWithDeclaredFallbacks(commandContract, candidates) {
124
+ const hasRunnableCandidate = candidates.some((candidate) => candidate.status === 'runnable' && candidate.intent.length > 0);
125
+ if (hasRunnableCandidate) {
126
+ return candidates;
127
+ }
128
+ const existingIntents = new Set(candidates.map((candidate) => candidate.intent).filter((intent) => intent.length > 0));
129
+ const fallbackIntents = uniqueSorted(candidates.flatMap((candidate) => fallbackIntentsForCandidate(commandContract, candidate))).filter((intent) => !existingIntents.has(intent));
130
+ if (fallbackIntents.length === 0) {
131
+ return candidates;
132
+ }
133
+ return [
134
+ ...candidates,
135
+ ...fallbackIntents.map((intent) => {
136
+ const fallback = classifyVerificationCandidate(intent, commandContract.intents[intent]);
137
+ return {
138
+ ...fallback,
139
+ detail: fallback.status === 'runnable' ? 'Declared fallback for unavailable verification intent.' : fallback.detail,
140
+ };
141
+ }),
142
+ ];
143
+ }
144
+ function requirementNeedsDeclaredEscalation(requirement) {
145
+ if (requirement.surfaces.length > 1) {
146
+ return true;
147
+ }
148
+ const signalText = [
149
+ requirement.reason,
150
+ ...requirement.affectedContracts,
151
+ ...requirement.driftChecks,
152
+ ...requirement.surfaces,
153
+ ].join('\n').toLowerCase();
154
+ return DECLARED_ESCALATION_SIGNALS.some((signal) => signalText.includes(signal.toLowerCase()));
155
+ }
156
+ function expandCandidatesWithDeclaredEscalations(commandContract, requirement, candidates) {
157
+ if (!requirementNeedsDeclaredEscalation(requirement)) {
158
+ return candidates;
159
+ }
160
+ const existingIntents = new Set(candidates.map((candidate) => candidate.intent).filter((intent) => intent.length > 0));
161
+ const escalationIntents = uniqueSorted(candidates.flatMap((candidate) => escalationIntentsForCandidate(commandContract, candidate))).filter((intent) => !existingIntents.has(intent));
162
+ if (escalationIntents.length === 0) {
163
+ return candidates;
164
+ }
165
+ return [
166
+ ...candidates,
167
+ ...escalationIntents.map((intent) => {
168
+ const escalation = classifyVerificationCandidate(intent, commandContract.intents[intent]);
169
+ return {
170
+ ...escalation,
171
+ detail: escalation.status === 'runnable'
172
+ ? 'Declared escalation for a high-risk verification requirement.'
173
+ : escalation.detail,
174
+ };
175
+ }),
176
+ ];
177
+ }
178
+ function intentExplicitlySubsumes(commandContract, broaderIntent, narrowerIntent) {
179
+ return readIntentRelationList(commandContract, broaderIntent, 'subsumes').includes(narrowerIntent);
180
+ }
181
+ function intentExplicitlySubsumedBy(commandContract, narrowerIntent, broaderIntent) {
182
+ return readIntentRelationList(commandContract, narrowerIntent, 'subsumed_by').includes(broaderIntent);
183
+ }
184
+ function intentRequiresCompanion(commandContract, intent) {
185
+ return readIntentRelationList(commandContract, intent, 'requires_with').length > 0;
186
+ }
187
+ function intentIsExplicitlySubsumed(commandContract, narrowerIntent, broaderIntent) {
188
+ return (intentExplicitlySubsumedBy(commandContract, narrowerIntent, broaderIntent) ||
189
+ intentExplicitlySubsumes(commandContract, broaderIntent, narrowerIntent));
190
+ }
191
+ function selectVerificationCandidates(commandContract, candidates) {
192
+ const runnableCandidates = candidates.filter((candidate) => candidate.status === 'runnable' && candidate.intent.length > 0);
193
+ const selectedIntents = new Set(runnableCandidates.map((candidate) => candidate.intent));
194
+ for (const candidate of runnableCandidates) {
195
+ const isSubsumed = runnableCandidates.some((other) => other.intent !== candidate.intent && intentIsExplicitlySubsumed(commandContract, candidate.intent, other.intent));
196
+ if (isSubsumed) {
197
+ selectedIntents.delete(candidate.intent);
198
+ }
199
+ }
200
+ const costComparableCandidates = runnableCandidates.filter((candidate) => selectedIntents.has(candidate.intent) &&
201
+ !intentRequiresCompanion(commandContract, candidate.intent) &&
202
+ intentCoverageSignature(commandContract, candidate.intent) !== null &&
203
+ readIntentCostExpectedSeconds(commandContract, candidate.intent) !== null);
204
+ const costComparableGroups = new Map();
205
+ for (const candidate of costComparableCandidates) {
206
+ const signature = intentCoverageSignature(commandContract, candidate.intent);
207
+ if (signature === null) {
208
+ continue;
209
+ }
210
+ costComparableGroups.set(signature, [...(costComparableGroups.get(signature) ?? []), candidate]);
211
+ }
212
+ for (const group of costComparableGroups.values()) {
213
+ if (group.length < 2) {
214
+ continue;
215
+ }
216
+ const costs = group.map((candidate) => readIntentCostExpectedSeconds(commandContract, candidate.intent));
217
+ if (costs.some((cost) => cost === null)) {
218
+ continue;
219
+ }
220
+ const minCost = Math.min(...costs);
221
+ const winners = group.filter((candidate) => readIntentCostExpectedSeconds(commandContract, candidate.intent) === minCost);
222
+ if (winners.length !== 1) {
223
+ continue;
224
+ }
225
+ for (const candidate of group) {
226
+ if (candidate.intent !== winners[0]?.intent) {
227
+ selectedIntents.delete(candidate.intent);
228
+ }
229
+ }
230
+ }
231
+ if (selectedIntents.size === 0 && runnableCandidates.length > 0) {
232
+ return runnableCandidates;
233
+ }
234
+ return runnableCandidates.filter((candidate) => selectedIntents.has(candidate.intent));
235
+ }
236
+ function uniqueVerificationCandidates(candidates) {
237
+ const byIntent = new Map();
238
+ for (const candidate of candidates) {
239
+ if (candidate.intent.length === 0 || byIntent.has(candidate.intent)) {
240
+ continue;
241
+ }
242
+ byIntent.set(candidate.intent, candidate);
243
+ }
244
+ return [...byIntent.values()].sort((left, right) => left.intent.localeCompare(right.intent));
245
+ }
246
+ function toChangeVerificationCandidate(reason, candidate, selectedCandidateKeys) {
247
+ const hasIntent = candidate.intent.length > 0;
248
+ const isMissingIntent = !hasIntent || candidate.reason === 'no_matching_intents';
249
+ const isEligible = hasIntent && candidate.status === 'runnable';
44
250
  return {
45
251
  reason,
46
252
  intent: candidate.intent.length > 0 ? candidate.intent : null,
47
253
  status: candidate.status,
48
254
  skipReason: candidate.reason,
49
255
  detail: candidate.detail,
256
+ candidateState: isMissingIntent ? 'gap' : 'candidate',
257
+ eligibilityState: isMissingIntent ? 'missing' : isEligible ? 'eligible' : 'ineligible',
258
+ selectionState: isEligible
259
+ ? selectedCandidateKeys.has(candidateKey(reason, candidate.intent))
260
+ ? 'selected'
261
+ : 'not_selected'
262
+ : 'not_applicable',
50
263
  };
51
264
  }
52
265
  function gapForRequirement(requirement, candidates) {
@@ -62,17 +275,37 @@ function gapForRequirement(requirement, candidates) {
62
275
  };
63
276
  }
64
277
  export function createChangeVerificationReport(classificationReport, commandContract, projectRoot) {
278
+ const testSelectionPlan = createProjectTestSelectionPlan(projectRoot, classificationReport, commandContract);
65
279
  const requirements = classificationReport.summary.validationReasons.map((reason) => createVerificationRequirement(classificationReport, reason));
66
280
  const plans = requirements.map((requirement) => ({
67
281
  requirement,
68
- candidates: createVerificationPlan(commandContract, requirement.reason).candidates,
282
+ candidates: expandCandidatesWithDeclaredEscalations(commandContract, requirement, expandCandidatesWithDeclaredFallbacks(commandContract, createVerificationPlan(commandContract, requirement.reason).candidates)),
283
+ }));
284
+ const plansWithProjectTestSelection = plans.map((plan) => {
285
+ const additionalCandidates = testSelectionPlan.candidates
286
+ .filter((candidate) => candidate.reason === plan.requirement.reason)
287
+ .filter((candidate) => !plan.candidates.some((existing) => existing.intent === candidate.candidate.intent))
288
+ .map((candidate) => candidate.candidate);
289
+ return additionalCandidates.length > 0
290
+ ? { ...plan, candidates: [...plan.candidates, ...additionalCandidates] }
291
+ : plan;
292
+ });
293
+ const selectedPlans = plansWithProjectTestSelection.map((plan) => ({
294
+ ...plan,
295
+ selectedCandidates: uniqueVerificationCandidates([
296
+ ...selectVerificationCandidates(commandContract, plan.candidates),
297
+ ...testSelectionPlan.selectedCandidates
298
+ .filter((candidate) => candidate.reason === plan.requirement.reason)
299
+ .map((candidate) => candidate.candidate),
300
+ ]),
69
301
  }));
70
- const candidatePlans = plans.flatMap((plan) => plan.candidates);
71
- const candidates = plans.flatMap((plan) => plan.candidates.map((candidate) => toChangeVerificationCandidate(plan.requirement.reason, candidate)));
302
+ const selectedCandidatePlans = selectedPlans.flatMap((plan) => plan.selectedCandidates);
303
+ const selectedCandidateKeys = new Set(selectedPlans.flatMap((plan) => plan.selectedCandidates.map((candidate) => candidateKey(plan.requirement.reason, candidate.intent))));
304
+ const schedule = createVerificationSchedule(projectRoot, commandContract, selectedCandidatePlans);
305
+ const candidates = plansWithProjectTestSelection.flatMap((plan) => plan.candidates.map((candidate) => toChangeVerificationCandidate(plan.requirement.reason, candidate, selectedCandidateKeys)));
72
306
  const gaps = requirements
73
307
  .map((requirement) => gapForRequirement(requirement, candidates))
74
308
  .filter((gap) => gap !== null);
75
- const schedule = createVerificationSchedule(projectRoot, commandContract, candidatePlans);
76
309
  return {
77
310
  schema_version: CHANGE_VERIFICATION_SCHEMA_VERSION,
78
311
  source: classificationReport.source,
@@ -83,5 +316,6 @@ export function createChangeVerificationReport(classificationReport, commandCont
83
316
  gaps,
84
317
  schedule,
85
318
  decision_graph: createVerificationDecisionGraph(commandContract, requirements, candidates, gaps, schedule),
319
+ test_selection: testSelectionPlan.report,
86
320
  };
87
321
  }
@@ -24,11 +24,23 @@ const CHECK_ISSUE_ID_RULES = [
24
24
  ['mustflow.contract_model.command_authority_field', /^Strict: \.mustflow\/config\/(?:changes|surfaces)\.toml .+ cannot define command authority; use \.mustflow\/config\/commands\.toml$/u],
25
25
  ['mustflow.contract_model.invalid_match_kind', /^Strict: \.mustflow\/config\/(?:changes|surfaces)\.toml rules\[\d+\]\.match\.kind must be "exact", "prefix", or "glob"; regular expressions are deferred$/u],
26
26
  ['mustflow.contract_model.invalid_shape', /^Strict: \.mustflow\/config\/(?:changes|surfaces)\.toml .+(?:must be|must define|is not allowed)/u],
27
+ ['mustflow.test_selection.command_authority_field', /^Strict: \.mustflow\/config\/test-selection\.toml .+ cannot define command authority; use \.mustflow\/config\/commands\.toml$/u],
28
+ ['mustflow.test_selection.unknown_command_intent', /^Strict: \.mustflow\/config\/test-selection\.toml .+ references unknown command intent "[^"]+"$/u],
29
+ ['mustflow.test_selection.invalid_shape', /^Strict: \.mustflow\/config\/test-selection\.toml .+(?:must be|must define|is not allowed|references command intent "[^"]+" that is not configured)/u],
27
30
  ['mustflow.skill.procedure_only', /^Strict: \.mustflow\/skills\/[^/]+\/SKILL\.md metadata\.mustflow_kind must be "procedure"$/u],
28
31
  ['mustflow.skill.raw_command_block', /^Strict: \.mustflow\/skills\/[^/]+\/SKILL\.md contains a raw shell command block; reference command intents instead$/u],
29
32
  ['mustflow.skill.command_permission_claim', /^Strict: \.mustflow\/skills\/[^/]+\/SKILL\.md claims command execution permission; keep permissions in \.mustflow\/config\/commands\.toml$/u],
30
33
  ['mustflow.skill.unknown_command_intent', /^Strict: \.mustflow\/skills\/[^/]+\/SKILL\.md metadata\.command_intents references unknown command intent "[^"]+"$/u],
31
34
  ['mustflow.skill.index_route_unknown_command_intent', /^Strict: \.mustflow\/skills\/INDEX\.md route \.mustflow\/skills\/[^/]+\/SKILL\.md references command intent "[^"]+" not declared by the skill frontmatter$/u],
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
+ ['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
+ ['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],
32
44
  ['mustflow.skill.resource_unknown_command_intent', /^Strict: \.mustflow\/skills\/[^/]+\/resources\.toml script [^\s]+ references unknown command intent "[^"]+"$/u],
33
45
  ['mustflow.source_anchor.invalid_format', /^Strict: source anchor .+ has invalid format:/u],
34
46
  ['mustflow.source_anchor.duplicate_id', /^Strict: source anchor id "[^"]+" is duplicated:/u],
@@ -120,6 +120,25 @@ function validateCommandIntentEffects(intentName, intent, issues) {
120
120
  }
121
121
  }
122
122
  }
123
+ function validateCommandIntentSelection(intentName, intent, issues) {
124
+ if (!hasOwn(intent, 'selection')) {
125
+ return;
126
+ }
127
+ if (!isRecord(intent.selection)) {
128
+ issues.push(commandContractIssue(`[commands.intents.${intentName}.selection] must be a TOML table`));
129
+ return;
130
+ }
131
+ const selection = intent.selection;
132
+ validateStringField(selection, 'coverage_level', `[commands.intents.${intentName}.selection].coverage_level`, issues);
133
+ validateStringField(selection, 'coverage_confidence', `[commands.intents.${intentName}.selection].coverage_confidence`, issues);
134
+ validateStringField(selection, 'accepts_changed_files', `[commands.intents.${intentName}.selection].accepts_changed_files`, issues);
135
+ validateStringArrayField(selection, 'fallback_intents', `[commands.intents.${intentName}.selection].fallback_intents`, issues);
136
+ validateStringArrayField(selection, 'escalate_to', `[commands.intents.${intentName}.selection].escalate_to`, issues);
137
+ validateBooleanField(selection, 'accepts_test_targets', `[commands.intents.${intentName}.selection].accepts_test_targets`, issues);
138
+ if (selection.accepts_test_targets === true && intent.status === 'configured' && !Array.isArray(intent.argv)) {
139
+ issues.push(commandContractIssue(`[commands.intents.${intentName}.selection].accepts_test_targets requires argv command mode`));
140
+ }
141
+ }
123
142
  function validateCommandIntent(intentName, intent, issues) {
124
143
  if (!commandIntentNameIsSafe(intentName)) {
125
144
  issues.push(commandContractIssue(`Intent ${intentName} name must contain only letters, numbers, underscores, and hyphens`));
@@ -129,6 +148,7 @@ function validateCommandIntent(intentName, intent, issues) {
129
148
  validateAllowedStringField(intent, 'run_policy', `[commands.intents.${intentName}].run_policy`, COMMAND_RUN_POLICIES, issues);
130
149
  validateAllowedStringField(intent, 'env_policy', `[commands.intents.${intentName}].env_policy`, COMMAND_ENV_POLICIES, issues);
131
150
  validateStringArrayField(intent, 'env_allowlist', `[commands.intents.${intentName}].env_allowlist`, issues);
151
+ validateCommandIntentSelection(intentName, intent, issues);
132
152
  if (intent.status !== 'configured') {
133
153
  return;
134
154
  }
@@ -70,6 +70,19 @@ function normalizeDeclaredEffect(projectRoot, commandContract, intentName, inten
70
70
  if (!lock && paths.length === 0) {
71
71
  throw new Error(`Command effect for intent ${intentName} must define path, paths, or lock`);
72
72
  }
73
+ if (paths.length === 0) {
74
+ return [
75
+ {
76
+ intent: intentName,
77
+ source: 'effects',
78
+ access,
79
+ mode,
80
+ path: null,
81
+ lock: lock,
82
+ concurrency,
83
+ },
84
+ ];
85
+ }
73
86
  return paths.map((rawPath) => {
74
87
  const normalizedPath = validateEffectPath(projectRoot, intent, rawPath);
75
88
  return {
@@ -0,0 +1,209 @@
1
+ function verifyStatus(input) {
2
+ if (input.failedIntents > 0) {
3
+ const contradictions = ['one_or_more_selected_verification_intents_failed'];
4
+ if ((input.repeatedFailureCount ?? 0) > 0) {
5
+ contradictions.push('repeated_verification_failure');
6
+ }
7
+ return {
8
+ status: 'contradicted',
9
+ primaryReason: 'verification_failed',
10
+ blockers: [],
11
+ contradictions,
12
+ limitations: [],
13
+ };
14
+ }
15
+ if (input.ranIntents === 0 && input.skippedIntents > 0) {
16
+ const blockers = ['all_matching_verification_intents_were_skipped'];
17
+ if ((input.repeatedFailureCount ?? 0) > 0) {
18
+ blockers.push('repeated_verification_failure');
19
+ }
20
+ return {
21
+ status: 'blocked',
22
+ primaryReason: 'no_runnable_verification_intents',
23
+ blockers,
24
+ contradictions: [],
25
+ limitations: [],
26
+ };
27
+ }
28
+ if (input.ranIntents === 0) {
29
+ const limitations = ['no_verification_intents_ran'];
30
+ if ((input.repeatedFailureCount ?? 0) > 0) {
31
+ limitations.push('repeated_verification_failure');
32
+ }
33
+ return {
34
+ status: 'unverified',
35
+ primaryReason: 'no_verification_evidence',
36
+ blockers: [],
37
+ contradictions: [],
38
+ limitations,
39
+ };
40
+ }
41
+ if (input.skippedIntents > 0) {
42
+ const limitations = ['one_or_more_matching_verification_intents_were_skipped'];
43
+ if ((input.repeatedFailureCount ?? 0) > 0) {
44
+ limitations.push('repeated_verification_failure');
45
+ }
46
+ return {
47
+ status: 'partially_verified',
48
+ primaryReason: 'some_verification_skipped',
49
+ blockers: [],
50
+ contradictions: [],
51
+ limitations,
52
+ };
53
+ }
54
+ const downgradeLimitations = [];
55
+ if ((input.sourceAnchorRiskCount ?? 0) > 0) {
56
+ downgradeLimitations.push('high_risk_source_anchor_requires_review');
57
+ }
58
+ if ((input.scopeDiffRiskCount ?? 0) > 0) {
59
+ downgradeLimitations.push('scope_diff_risk_requires_review');
60
+ }
61
+ if ((input.validationRatchetRiskCount ?? 0) > 0) {
62
+ downgradeLimitations.push('validation_ratchet_risk_requires_review');
63
+ }
64
+ if ((input.reproEvidenceRiskCount ?? 0) > 0) {
65
+ downgradeLimitations.push('repro_evidence_missing');
66
+ }
67
+ if ((input.externalEvidenceRiskCount ?? 0) > 0) {
68
+ downgradeLimitations.push('external_evidence_requires_review');
69
+ }
70
+ if (downgradeLimitations.length > 0) {
71
+ return {
72
+ status: 'partially_verified',
73
+ primaryReason: (input.sourceAnchorRiskCount ?? 0) > 0
74
+ ? 'source_anchor_invariant_review_required'
75
+ : (input.scopeDiffRiskCount ?? 0) > 0
76
+ ? 'scope_diff_review_required'
77
+ : (input.validationRatchetRiskCount ?? 0) > 0
78
+ ? 'validation_ratchet_review_required'
79
+ : (input.reproEvidenceRiskCount ?? 0) > 0
80
+ ? 'repro_evidence_missing'
81
+ : 'external_evidence_review_required',
82
+ blockers: [],
83
+ contradictions: [],
84
+ limitations: downgradeLimitations,
85
+ };
86
+ }
87
+ if (input.passedIntents === input.ranIntents) {
88
+ return {
89
+ status: 'verified',
90
+ primaryReason: 'all_selected_verification_passed',
91
+ blockers: [],
92
+ contradictions: [],
93
+ limitations: [],
94
+ };
95
+ }
96
+ return {
97
+ status: 'unverified',
98
+ primaryReason: 'verification_evidence_inconclusive',
99
+ blockers: [],
100
+ contradictions: [],
101
+ limitations: ['selected_verification_did_not_produce_a_clear_pass_or_fail'],
102
+ };
103
+ }
104
+ export function createVerifyCompletionVerdict(input) {
105
+ const result = verifyStatus(input);
106
+ return {
107
+ schema_version: '1',
108
+ status: result.status,
109
+ primary_reason: result.primaryReason,
110
+ evidence: {
111
+ source: 'mf_verify',
112
+ verification_plan_id: input.verificationPlanId,
113
+ changed_file_count: null,
114
+ matched_intents: input.matchedIntents,
115
+ ran_intents: input.ranIntents,
116
+ passed_intents: input.passedIntents,
117
+ failed_intents: input.failedIntents,
118
+ skipped_intents: input.skippedIntents,
119
+ receipt_count: input.receiptCount,
120
+ gap_count: input.skippedIntents,
121
+ source_anchor_risk_count: input.sourceAnchorRiskCount ?? 0,
122
+ scope_diff_risk_count: input.scopeDiffRiskCount ?? 0,
123
+ repeated_failure_count: input.repeatedFailureCount ?? 0,
124
+ validation_ratchet_risk_count: input.validationRatchetRiskCount ?? 0,
125
+ latest_run_status: null,
126
+ },
127
+ blockers: result.blockers,
128
+ contradictions: result.contradictions,
129
+ limitations: result.limitations,
130
+ };
131
+ }
132
+ export function createDashboardCompletionVerdict(input) {
133
+ const latestRunFailed = input.latestRunStatus === 'failed' ||
134
+ input.latestRunStatus === 'timed_out' ||
135
+ input.latestRunStatus === 'start_failed';
136
+ let status = 'unverified';
137
+ let primaryReason = 'dashboard_does_not_execute_verification';
138
+ const blockers = [];
139
+ const contradictions = [];
140
+ const limitations = ['dashboard_export_is_read_only'];
141
+ if (latestRunFailed) {
142
+ status = 'contradicted';
143
+ primaryReason = 'latest_run_failed';
144
+ contradictions.push('latest_run_status_is_not_passing');
145
+ }
146
+ else if ((input.sourceAnchorRiskCount ?? 0) > 0) {
147
+ status = 'partially_verified';
148
+ primaryReason = 'source_anchor_invariant_review_required';
149
+ limitations.push('high_risk_source_anchor_requires_review');
150
+ if ((input.scopeDiffRiskCount ?? 0) > 0) {
151
+ limitations.push('scope_diff_risk_requires_review');
152
+ }
153
+ }
154
+ else if ((input.scopeDiffRiskCount ?? 0) > 0) {
155
+ status = 'partially_verified';
156
+ primaryReason = 'scope_diff_review_required';
157
+ limitations.push('scope_diff_risk_requires_review');
158
+ }
159
+ else if (input.gapCount > 0) {
160
+ status = 'blocked';
161
+ primaryReason = 'verification_gaps_present';
162
+ blockers.push('dashboard_verification_graph_reports_gaps');
163
+ }
164
+ else if (input.changedFileCount > 0 && !input.latestRunExists) {
165
+ status = 'unverified';
166
+ primaryReason = 'changed_files_without_run_receipt';
167
+ limitations.push('no_latest_run_receipt');
168
+ }
169
+ else if (input.changedFileCount > 0 && !input.latestRunValid) {
170
+ status = 'unverified';
171
+ primaryReason = 'changed_files_with_invalid_run_receipt';
172
+ limitations.push('latest_run_receipt_is_invalid');
173
+ }
174
+ else if (input.changedFileCount > 0 && input.runnableIntentCount > 0) {
175
+ status = 'partially_verified';
176
+ primaryReason = 'verification_recommendations_available';
177
+ limitations.push('dashboard_recommendations_are_not_executed_receipts');
178
+ }
179
+ else if (input.latestRunValid && input.latestRunStatus === 'passed') {
180
+ status = 'partially_verified';
181
+ primaryReason = 'latest_run_passed_without_current_claim_binding';
182
+ limitations.push('latest_run_is_not_bound_to_a_current_completion_claim');
183
+ }
184
+ return {
185
+ schema_version: '1',
186
+ status,
187
+ primary_reason: primaryReason,
188
+ evidence: {
189
+ source: 'dashboard_export',
190
+ verification_plan_id: null,
191
+ changed_file_count: input.changedFileCount,
192
+ matched_intents: input.runnableIntentCount + input.skippedIntentCount,
193
+ ran_intents: 0,
194
+ passed_intents: 0,
195
+ failed_intents: latestRunFailed ? 1 : 0,
196
+ skipped_intents: input.skippedIntentCount,
197
+ receipt_count: input.latestRunExists && input.latestRunValid ? 1 : 0,
198
+ gap_count: input.gapCount,
199
+ source_anchor_risk_count: input.sourceAnchorRiskCount ?? 0,
200
+ scope_diff_risk_count: input.scopeDiffRiskCount ?? 0,
201
+ repeated_failure_count: input.repeatedFailureCount ?? 0,
202
+ validation_ratchet_risk_count: input.validationRatchetRiskCount ?? 0,
203
+ latest_run_status: input.latestRunStatus,
204
+ },
205
+ blockers,
206
+ contradictions,
207
+ limitations,
208
+ };
209
+ }