mustflow 1.31.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 +23 -9
- 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 +4 -1
- package/dist/cli/commands/verify.js +488 -43
- package/dist/cli/i18n/en.js +61 -10
- package/dist/cli/i18n/es.js +61 -10
- package/dist/cli/i18n/fr.js +61 -10
- package/dist/cli/i18n/hi.js +61 -10
- package/dist/cli/i18n/ko.js +61 -10
- package/dist/cli/i18n/zh.js +61 -10
- 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 -1911
- package/dist/cli/lib/run-plan.js +76 -1
- 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 -1770
- package/dist/core/check-issues.js +6 -0
- package/dist/core/completion-verdict.js +209 -0
- package/dist/core/contract-lint.js +221 -6
- package/dist/core/external-evidence.js +9 -0
- package/dist/core/public-json-contracts.js +21 -0
- package/dist/core/repeated-failure.js +17 -0
- package/dist/core/repro-evidence.js +53 -0
- package/dist/core/scope-risk.js +64 -0
- package/dist/core/skill-route-alignment.js +20 -0
- package/dist/core/source-anchor-status.js +4 -1
- package/dist/core/test-selection.js +3 -0
- package/dist/core/validation-ratchet.js +52 -0
- package/dist/core/verification-evidence.js +249 -0
- package/examples/README.md +12 -4
- package/package.json +1 -1
- package/schemas/README.md +13 -3
- package/schemas/change-verification-report.schema.json +16 -2
- package/schemas/commands.schema.json +4 -0
- package/schemas/contract-lint-report.schema.json +29 -0
- package/schemas/dashboard-export.schema.json +227 -0
- package/schemas/latest-run-pointer.schema.json +384 -0
- package/schemas/run-receipt.schema.json +4 -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/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
|
@@ -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,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
|
+
}
|
|
@@ -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((
|
|
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:
|
|
356
|
-
runnable:
|
|
357
|
-
manualOnly:
|
|
358
|
-
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,
|
|
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',
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const SCOPE_DIFF_BUDGET_DEFAULTS = {
|
|
2
|
+
maxChangedFiles: 8,
|
|
3
|
+
maxPublicSurfaces: 4,
|
|
4
|
+
maxChangeKindFamilies: 3,
|
|
5
|
+
maxPathsPerRisk: 8,
|
|
6
|
+
};
|
|
7
|
+
const CHANGE_KIND_FAMILY_BY_KIND = {
|
|
8
|
+
documentation: 'documentation',
|
|
9
|
+
example: 'documentation',
|
|
10
|
+
translation: 'documentation',
|
|
11
|
+
workflow: 'workflow',
|
|
12
|
+
host_instruction: 'workflow',
|
|
13
|
+
installed_template: 'template',
|
|
14
|
+
package_metadata: 'release',
|
|
15
|
+
schema: 'contract',
|
|
16
|
+
test: 'test',
|
|
17
|
+
test_fixture: 'test',
|
|
18
|
+
implementation: 'implementation',
|
|
19
|
+
unknown: 'unknown',
|
|
20
|
+
};
|
|
21
|
+
function uniqueSorted(values) {
|
|
22
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
23
|
+
}
|
|
24
|
+
function changeKindFamily(kind) {
|
|
25
|
+
return CHANGE_KIND_FAMILY_BY_KIND[kind] ?? kind;
|
|
26
|
+
}
|
|
27
|
+
function firstPaths(paths) {
|
|
28
|
+
return paths.slice(0, SCOPE_DIFF_BUDGET_DEFAULTS.maxPathsPerRisk);
|
|
29
|
+
}
|
|
30
|
+
export function createScopeDiffRisks(report) {
|
|
31
|
+
const risks = [];
|
|
32
|
+
if (report.summary.fileCount > SCOPE_DIFF_BUDGET_DEFAULTS.maxChangedFiles) {
|
|
33
|
+
risks.push({
|
|
34
|
+
code: 'diff_budget_exceeded',
|
|
35
|
+
severity: 'high',
|
|
36
|
+
detail: `Changed file count ${report.summary.fileCount} exceeds the conservative completion budget of ${SCOPE_DIFF_BUDGET_DEFAULTS.maxChangedFiles}.`,
|
|
37
|
+
count: report.summary.fileCount,
|
|
38
|
+
limit: SCOPE_DIFF_BUDGET_DEFAULTS.maxChangedFiles,
|
|
39
|
+
paths: firstPaths(report.files),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (report.summary.publicSurfaceCount > SCOPE_DIFF_BUDGET_DEFAULTS.maxPublicSurfaces) {
|
|
43
|
+
risks.push({
|
|
44
|
+
code: 'public_surface_budget_exceeded',
|
|
45
|
+
severity: 'high',
|
|
46
|
+
detail: `Public surface count ${report.summary.publicSurfaceCount} exceeds the conservative completion budget of ${SCOPE_DIFF_BUDGET_DEFAULTS.maxPublicSurfaces}.`,
|
|
47
|
+
count: report.summary.publicSurfaceCount,
|
|
48
|
+
limit: SCOPE_DIFF_BUDGET_DEFAULTS.maxPublicSurfaces,
|
|
49
|
+
paths: firstPaths(report.classifications.filter((classification) => classification.surface.isPublicSurface).map((classification) => classification.path)),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const changeKindFamilies = uniqueSorted(report.summary.changeKinds.map(changeKindFamily));
|
|
53
|
+
if (changeKindFamilies.length > SCOPE_DIFF_BUDGET_DEFAULTS.maxChangeKindFamilies) {
|
|
54
|
+
risks.push({
|
|
55
|
+
code: 'mixed_change_kind_budget_exceeded',
|
|
56
|
+
severity: 'medium',
|
|
57
|
+
detail: `Change kind family count ${changeKindFamilies.length} exceeds the conservative completion budget of ${SCOPE_DIFF_BUDGET_DEFAULTS.maxChangeKindFamilies}: ${changeKindFamilies.join(', ')}.`,
|
|
58
|
+
count: changeKindFamilies.length,
|
|
59
|
+
limit: SCOPE_DIFF_BUDGET_DEFAULTS.maxChangeKindFamilies,
|
|
60
|
+
paths: firstPaths(report.files),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return risks;
|
|
64
|
+
}
|