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.
Files changed (64) 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 +6 -0
  5. package/dist/cli/commands/index.js +5 -0
  6. package/dist/cli/commands/run.js +4 -1
  7. package/dist/cli/commands/verify.js +488 -43
  8. package/dist/cli/i18n/en.js +61 -10
  9. package/dist/cli/i18n/es.js +61 -10
  10. package/dist/cli/i18n/fr.js +61 -10
  11. package/dist/cli/i18n/hi.js +61 -10
  12. package/dist/cli/i18n/ko.js +61 -10
  13. package/dist/cli/i18n/zh.js +61 -10
  14. package/dist/cli/lib/dashboard-export.js +62 -12
  15. package/dist/cli/lib/dashboard-html/client-script.js +1936 -0
  16. package/dist/cli/lib/dashboard-html/locale-bootstrap.js +8 -0
  17. package/dist/cli/lib/dashboard-html/styles.js +572 -0
  18. package/dist/cli/lib/dashboard-html/template.js +134 -0
  19. package/dist/cli/lib/dashboard-html/types.js +1 -0
  20. package/dist/cli/lib/dashboard-html.js +1 -1907
  21. package/dist/cli/lib/dashboard-locale.js +37 -0
  22. package/dist/cli/lib/local-index/constants.js +48 -0
  23. package/dist/cli/lib/local-index/index.js +2256 -0
  24. package/dist/cli/lib/local-index/sql.js +15 -0
  25. package/dist/cli/lib/local-index/types.js +1 -0
  26. package/dist/cli/lib/local-index.js +1 -1911
  27. package/dist/cli/lib/run-plan.js +76 -1
  28. package/dist/cli/lib/templates.js +18 -1
  29. package/dist/cli/lib/validation/command-intents.js +11 -0
  30. package/dist/cli/lib/validation/constants.js +238 -0
  31. package/dist/cli/lib/validation/index.js +1384 -0
  32. package/dist/cli/lib/validation/primitives.js +198 -0
  33. package/dist/cli/lib/validation/test-selection.js +95 -0
  34. package/dist/cli/lib/validation/types.js +1 -0
  35. package/dist/cli/lib/validation.js +1 -1770
  36. package/dist/core/check-issues.js +6 -0
  37. package/dist/core/completion-verdict.js +209 -0
  38. package/dist/core/contract-lint.js +221 -6
  39. package/dist/core/external-evidence.js +9 -0
  40. package/dist/core/public-json-contracts.js +21 -0
  41. package/dist/core/repeated-failure.js +17 -0
  42. package/dist/core/repro-evidence.js +53 -0
  43. package/dist/core/scope-risk.js +64 -0
  44. package/dist/core/skill-route-alignment.js +20 -0
  45. package/dist/core/source-anchor-status.js +4 -1
  46. package/dist/core/test-selection.js +3 -0
  47. package/dist/core/validation-ratchet.js +52 -0
  48. package/dist/core/verification-evidence.js +249 -0
  49. package/examples/README.md +12 -4
  50. package/package.json +1 -1
  51. package/schemas/README.md +13 -3
  52. package/schemas/change-verification-report.schema.json +16 -2
  53. package/schemas/commands.schema.json +4 -0
  54. package/schemas/contract-lint-report.schema.json +29 -0
  55. package/schemas/dashboard-export.schema.json +227 -0
  56. package/schemas/latest-run-pointer.schema.json +384 -0
  57. package/schemas/run-receipt.schema.json +4 -0
  58. package/schemas/test-selection.schema.json +81 -0
  59. package/schemas/verify-report.schema.json +361 -1
  60. package/schemas/verify-run-manifest.schema.json +410 -0
  61. package/templates/default/i18n.toml +1 -1
  62. package/templates/default/locales/en/.mustflow/skills/INDEX.md +124 -29
  63. package/templates/default/locales/en/.mustflow/skills/routes.toml +289 -0
  64. package/templates/default/manifest.toml +29 -2
@@ -0,0 +1,198 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { isRecord } from '../command-contract.js';
4
+ import { readTomlFile } from '../toml.js';
5
+ import { REQUIRED_FILES, } from './constants.js';
6
+ import { VERSIONING_CONFIG_PATH } from '../../../core/version-sources.js';
7
+ export function hasOwn(table, key) {
8
+ return Object.prototype.hasOwnProperty.call(table, key);
9
+ }
10
+ export function isPositiveInteger(value) {
11
+ return Number.isInteger(value) && Number(value) > 0;
12
+ }
13
+ export function isSafeRelativePath(value) {
14
+ if (typeof value !== 'string' || value.trim().length === 0) {
15
+ return false;
16
+ }
17
+ const normalized = value.replace(/\\/g, '/');
18
+ const segments = normalized.split('/').filter(Boolean);
19
+ if (path.posix.isAbsolute(normalized) || path.win32.isAbsolute(value)) {
20
+ return false;
21
+ }
22
+ return segments.length > 0 && segments.every((segment) => segment !== '.' && segment !== '..');
23
+ }
24
+ export function validateRequiredFiles(projectRoot, issues) {
25
+ for (const relativePath of REQUIRED_FILES) {
26
+ if (!existsSync(path.join(projectRoot, relativePath))) {
27
+ issues.push({ message: `Missing ${relativePath}` });
28
+ }
29
+ }
30
+ }
31
+ export function validateToml(projectRoot, issues) {
32
+ const parsedFiles = {};
33
+ for (const relativePath of [
34
+ '.mustflow/config/mustflow.toml',
35
+ '.mustflow/config/commands.toml',
36
+ '.mustflow/config/preferences.toml',
37
+ VERSIONING_CONFIG_PATH,
38
+ ]) {
39
+ const filePath = path.join(projectRoot, relativePath);
40
+ if (!existsSync(filePath)) {
41
+ continue;
42
+ }
43
+ try {
44
+ const parsed = readTomlFile(filePath);
45
+ if (!isRecord(parsed)) {
46
+ issues.push({ message: `${relativePath} must contain a TOML table` });
47
+ continue;
48
+ }
49
+ if (relativePath.endsWith('mustflow.toml')) {
50
+ parsedFiles.mustflowToml = parsed;
51
+ }
52
+ if (relativePath.endsWith('commands.toml')) {
53
+ parsedFiles.commandsToml = parsed;
54
+ }
55
+ if (relativePath.endsWith('preferences.toml')) {
56
+ parsedFiles.preferencesToml = parsed;
57
+ }
58
+ if (relativePath.endsWith('versioning.toml')) {
59
+ parsedFiles.versioningToml = parsed;
60
+ }
61
+ }
62
+ catch (error) {
63
+ const message = error instanceof Error ? error.message : String(error);
64
+ issues.push({ message: `Invalid TOML in ${relativePath}: ${message}` });
65
+ }
66
+ }
67
+ return parsedFiles;
68
+ }
69
+ export function validateTable(config, tableName, issues) {
70
+ if (!hasOwn(config, tableName)) {
71
+ return undefined;
72
+ }
73
+ const table = config[tableName];
74
+ if (!isRecord(table)) {
75
+ issues.push({ message: `[${tableName}] must be a TOML table` });
76
+ return undefined;
77
+ }
78
+ return table;
79
+ }
80
+ export function validateBooleanField(table, key, label, issues) {
81
+ if (hasOwn(table, key) && typeof table[key] !== 'boolean') {
82
+ issues.push({ message: `${label} must be a boolean` });
83
+ }
84
+ }
85
+ export function validateStringField(table, key, label, issues) {
86
+ if (hasOwn(table, key) && (typeof table[key] !== 'string' || table[key].trim().length === 0)) {
87
+ issues.push({ message: `${label} must be a string` });
88
+ }
89
+ }
90
+ export function validateRequiredStringField(table, key, label, issues) {
91
+ if (!hasOwn(table, key)) {
92
+ issues.push({ message: `${label} must be a string` });
93
+ return;
94
+ }
95
+ validateStringField(table, key, label, issues);
96
+ }
97
+ export function validateStringArrayField(table, key, label, issues) {
98
+ if (!hasOwn(table, key)) {
99
+ return;
100
+ }
101
+ const value = table[key];
102
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string' || entry.trim().length === 0)) {
103
+ issues.push({ message: `${label} must be a string array` });
104
+ }
105
+ }
106
+ export function validateStringArrayMembers(table, key, label, allowedValues, unsupportedLabel, issues) {
107
+ if (!hasOwn(table, key)) {
108
+ return;
109
+ }
110
+ const value = table[key];
111
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string' || entry.trim().length === 0)) {
112
+ issues.push({ message: `${label} must be a string array` });
113
+ return;
114
+ }
115
+ for (const entry of value) {
116
+ if (!allowedValues.has(entry)) {
117
+ issues.push({ message: `${label} contains unsupported ${unsupportedLabel} "${entry}"` });
118
+ }
119
+ }
120
+ }
121
+ export function validateExactStringArrayField(table, key, label, expectedValues, issues) {
122
+ if (!hasOwn(table, key)) {
123
+ return;
124
+ }
125
+ const value = table[key];
126
+ if (!Array.isArray(value) || value.length !== expectedValues.length) {
127
+ issues.push({ message: `${label} must be ${expectedValues.map((entry) => `"${entry}"`).join(', ')}` });
128
+ return;
129
+ }
130
+ const hasExpectedValues = expectedValues.every((entry, index) => value[index] === entry);
131
+ if (!hasExpectedValues) {
132
+ issues.push({ message: `${label} must be ${expectedValues.map((entry) => `"${entry}"`).join(', ')}` });
133
+ }
134
+ }
135
+ export function validatePositiveIntegerField(table, key, label, issues) {
136
+ if (hasOwn(table, key) && !isPositiveInteger(table[key])) {
137
+ issues.push({ message: `${label} must be a positive integer` });
138
+ }
139
+ }
140
+ export function validateNestedTable(table, key, label, issues) {
141
+ if (!hasOwn(table, key)) {
142
+ return undefined;
143
+ }
144
+ const value = table[key];
145
+ if (!isRecord(value)) {
146
+ issues.push({ message: `${label} must be a TOML table` });
147
+ return undefined;
148
+ }
149
+ return value;
150
+ }
151
+ export function validateAllowedStringField(table, key, label, allowedValues, issues) {
152
+ if (!hasOwn(table, key)) {
153
+ return;
154
+ }
155
+ if (typeof table[key] !== 'string' || !allowedValues.has(table[key])) {
156
+ issues.push({ message: `${label} must be ${Array.from(allowedValues).map((value) => `"${value}"`).join(' or ')}` });
157
+ }
158
+ }
159
+ export function validatePathField(table, key, label, issues) {
160
+ if (hasOwn(table, key) && !isSafeRelativePath(table[key])) {
161
+ issues.push({ message: `${label} must be a non-empty relative path` });
162
+ }
163
+ }
164
+ export function validateRequiredPathField(table, key, label, issues) {
165
+ if (!hasOwn(table, key)) {
166
+ issues.push({ message: `${label} must be a non-empty relative path` });
167
+ return;
168
+ }
169
+ validatePathField(table, key, label, issues);
170
+ }
171
+ export function validatePathArrayField(table, key, label, issues) {
172
+ if (!hasOwn(table, key)) {
173
+ return undefined;
174
+ }
175
+ const value = table[key];
176
+ if (!Array.isArray(value) || value.length === 0 || !value.every(isSafeRelativePath)) {
177
+ issues.push({ message: `${label} entries must be non-empty relative paths` });
178
+ return undefined;
179
+ }
180
+ return value;
181
+ }
182
+ export function validateWorkspaceRoots(table, issues) {
183
+ if (!hasOwn(table, 'roots')) {
184
+ return undefined;
185
+ }
186
+ const value = table.roots;
187
+ if (!Array.isArray(value) || !value.every(isSafeRelativePath)) {
188
+ issues.push({ message: '[workspace].roots entries must be relative paths inside the current root' });
189
+ return undefined;
190
+ }
191
+ return value;
192
+ }
193
+ export function pushStrictIssue(issues, message) {
194
+ issues.push({ message: `Strict: ${message}` });
195
+ }
196
+ export function pushStrictWarning(issues, message) {
197
+ issues.push({ message: `Strict warning: ${message}`, severity: 'warning' });
198
+ }
@@ -0,0 +1,95 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { isRecord } from '../command-contract.js';
4
+ import { readTomlFile } from '../toml.js';
5
+ import { ALLOWED_TEST_SELECTION_RISKS, FORBIDDEN_TEST_SELECTION_COMMAND_AUTHORITY_FIELDS, TEST_SELECTION_CONFIG_PATH, } from './constants.js';
6
+ import { isConfiguredCommandIntent, isDeclaredCommandIntent } from './command-intents.js';
7
+ import { hasOwn, pushStrictIssue, validateAllowedStringField, validateNestedTable, validatePathArrayField, validateRequiredStringField, validateStringArrayField, } from './primitives.js';
8
+ function validateNoTestSelectionCommandAuthorityFields(label, table, issues) {
9
+ for (const field of FORBIDDEN_TEST_SELECTION_COMMAND_AUTHORITY_FIELDS) {
10
+ if (hasOwn(table, field)) {
11
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label}.${field} cannot define command authority; use .mustflow/config/commands.toml`);
12
+ }
13
+ }
14
+ }
15
+ function validateTestSelectionIntentReference(value, label, commandsToml, issues) {
16
+ if (typeof value !== 'string' || value.trim().length === 0) {
17
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label} must be a command intent name`);
18
+ return;
19
+ }
20
+ if (!isDeclaredCommandIntent(commandsToml, value)) {
21
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label} references unknown command intent "${value}"`);
22
+ return;
23
+ }
24
+ if (!isConfiguredCommandIntent(commandsToml, value)) {
25
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label} references command intent "${value}" that is not configured`);
26
+ }
27
+ }
28
+ function validateTestSelectionRule(rule, index, commandsToml, issues) {
29
+ const label = `rules[${index}]`;
30
+ if (!isRecord(rule)) {
31
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label} must be a TOML table`);
32
+ return;
33
+ }
34
+ validateNoTestSelectionCommandAuthorityFields(label, rule, issues);
35
+ validateRequiredStringField(rule, 'id', `${TEST_SELECTION_CONFIG_PATH} ${label}.id`, issues);
36
+ validateRequiredStringField(rule, 'reason', `${TEST_SELECTION_CONFIG_PATH} ${label}.reason`, issues);
37
+ validateRequiredStringField(rule, 'risk', `${TEST_SELECTION_CONFIG_PATH} ${label}.risk`, issues);
38
+ validateAllowedStringField(rule, 'risk', `${TEST_SELECTION_CONFIG_PATH} ${label}.risk`, ALLOWED_TEST_SELECTION_RISKS, issues);
39
+ const match = validateNestedTable(rule, 'match', `${TEST_SELECTION_CONFIG_PATH} ${label}.match`, issues);
40
+ if (!hasOwn(rule, 'match')) {
41
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label} must define match`);
42
+ }
43
+ if (match) {
44
+ validateNoTestSelectionCommandAuthorityFields(`${label}.match`, match, issues);
45
+ validatePathArrayField(match, 'paths', `${TEST_SELECTION_CONFIG_PATH} ${label}.match.paths`, issues);
46
+ validateStringArrayField(match, 'surfaces', `${TEST_SELECTION_CONFIG_PATH} ${label}.match.surfaces`, issues);
47
+ if (!hasOwn(match, 'paths')) {
48
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label}.match must define paths`);
49
+ }
50
+ if (!hasOwn(match, 'surfaces')) {
51
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label}.match must define surfaces`);
52
+ }
53
+ }
54
+ const select = validateNestedTable(rule, 'select', `${TEST_SELECTION_CONFIG_PATH} ${label}.select`, issues);
55
+ if (!hasOwn(rule, 'select')) {
56
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} ${label} must define select`);
57
+ }
58
+ if (select) {
59
+ validateNoTestSelectionCommandAuthorityFields(`${label}.select`, select, issues);
60
+ validateTestSelectionIntentReference(select.intent, `${label}.select.intent`, commandsToml, issues);
61
+ validateTestSelectionIntentReference(select.fallback_intent, `${label}.select.fallback_intent`, commandsToml, issues);
62
+ validatePathArrayField(select, 'test_targets', `${TEST_SELECTION_CONFIG_PATH} ${label}.select.test_targets`, issues);
63
+ }
64
+ }
65
+ export function validateStrictTestSelectionConfig(projectRoot, commandsToml, issues) {
66
+ const configPath = path.join(projectRoot, ...TEST_SELECTION_CONFIG_PATH.split('/'));
67
+ if (!existsSync(configPath)) {
68
+ return;
69
+ }
70
+ let parsed;
71
+ try {
72
+ parsed = readTomlFile(configPath);
73
+ }
74
+ catch (error) {
75
+ const message = error instanceof Error ? error.message : String(error);
76
+ pushStrictIssue(issues, `Invalid TOML in ${TEST_SELECTION_CONFIG_PATH}: ${message}`);
77
+ return;
78
+ }
79
+ if (!isRecord(parsed)) {
80
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} must contain a TOML table`);
81
+ return;
82
+ }
83
+ validateNoTestSelectionCommandAuthorityFields('root', parsed, issues);
84
+ validateRequiredStringField(parsed, 'schema_version', `${TEST_SELECTION_CONFIG_PATH}.schema_version`, issues);
85
+ if (parsed.schema_version !== '1') {
86
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH}.schema_version must be "1"`);
87
+ }
88
+ if (!Array.isArray(parsed.rules) || parsed.rules.length === 0) {
89
+ pushStrictIssue(issues, `${TEST_SELECTION_CONFIG_PATH} must define [[rules]]`);
90
+ return;
91
+ }
92
+ for (const [index, rule] of parsed.rules.entries()) {
93
+ validateTestSelectionRule(rule, index, commandsToml, issues);
94
+ }
95
+ }
@@ -0,0 +1 @@
1
+ export {};