mustflow 2.22.5 → 2.22.12

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 (56) hide show
  1. package/README.md +8 -0
  2. package/dist/cli/commands/check.js +2 -2
  3. package/dist/cli/commands/classify.js +2 -0
  4. package/dist/cli/commands/dashboard.js +9 -69
  5. package/dist/cli/commands/help.js +1 -3
  6. package/dist/cli/commands/run/receipt.js +1 -0
  7. package/dist/cli/commands/run.js +14 -1
  8. package/dist/cli/commands/verify/evidence-input.js +269 -0
  9. package/dist/cli/commands/verify/input.js +212 -0
  10. package/dist/cli/commands/verify/state-paths.js +33 -0
  11. package/dist/cli/commands/verify.js +29 -511
  12. package/dist/cli/i18n/en.js +3 -0
  13. package/dist/cli/i18n/es.js +3 -0
  14. package/dist/cli/i18n/fr.js +3 -0
  15. package/dist/cli/i18n/hi.js +3 -0
  16. package/dist/cli/i18n/ko.js +3 -0
  17. package/dist/cli/i18n/zh.js +3 -0
  18. package/dist/cli/lib/dashboard-export.js +2 -0
  19. package/dist/cli/lib/dashboard-mutations.js +79 -0
  20. package/dist/cli/lib/doc-review-ledger.js +1 -3
  21. package/dist/cli/lib/local-index/command-effect-index.js +25 -0
  22. package/dist/cli/lib/local-index/hashing.js +7 -0
  23. package/dist/cli/lib/local-index/index.js +127 -826
  24. package/dist/cli/lib/local-index/source-index.js +137 -0
  25. package/dist/cli/lib/local-index/verification-evidence.js +451 -0
  26. package/dist/cli/lib/local-index/workflow-documents.js +204 -0
  27. package/dist/cli/lib/manifest-lock.js +1 -3
  28. package/dist/cli/lib/repo-map-frontmatter.js +53 -0
  29. package/dist/cli/lib/repo-map.js +10 -57
  30. package/dist/cli/lib/run-root-trust.js +27 -0
  31. package/dist/cli/lib/validation/index.js +6 -2
  32. package/dist/core/change-classification-policy.js +47 -0
  33. package/dist/core/change-classification.js +10 -43
  34. package/dist/core/check-issues.js +11 -7
  35. package/dist/core/command-contract-validation.js +22 -20
  36. package/dist/core/contract-lint.js +6 -2
  37. package/dist/core/correlation-id.js +16 -0
  38. package/dist/core/run-receipt.js +1 -0
  39. package/package.json +4 -1
  40. package/schemas/README.md +4 -0
  41. package/schemas/change-verification-report.schema.json +4 -0
  42. package/schemas/classify-report.schema.json +4 -0
  43. package/schemas/dashboard-export.schema.json +4 -0
  44. package/schemas/latest-run-pointer.schema.json +4 -0
  45. package/schemas/run-receipt.schema.json +4 -0
  46. package/schemas/verify-report.schema.json +4 -0
  47. package/schemas/verify-run-manifest.schema.json +4 -0
  48. package/templates/default/i18n.toml +3 -3
  49. package/templates/default/locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md +25 -2
  50. package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +9 -1
  51. package/templates/default/locales/en/.mustflow/skills/test-design-guard/SKILL.md +9 -1
  52. package/templates/default/locales/ko/.mustflow/context/INDEX.md +12 -12
  53. package/templates/default/locales/ko/.mustflow/context/PROJECT.md +6 -6
  54. package/templates/default/locales/ko/.mustflow/docs/agent-workflow.md +70 -70
  55. package/templates/default/locales/ko/AGENTS.md +14 -14
  56. package/templates/default/manifest.toml +1 -1
@@ -0,0 +1,204 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { listFilesRecursive, toPosixPath } from '../filesystem.js';
4
+ import { readMustflowTextFile } from '../mustflow-read.js';
5
+ import { MAX_SNIPPET_BYTES_PER_DOCUMENT } from './constants.js';
6
+ import { sha256Text } from './hashing.js';
7
+ export function getExistingIndexablePaths(projectRoot) {
8
+ const paths = new Set();
9
+ const addIfExists = (relativePath) => {
10
+ if (existsSync(path.join(projectRoot, ...relativePath.split('/')))) {
11
+ paths.add(relativePath);
12
+ }
13
+ };
14
+ addIfExists('AGENTS.md');
15
+ for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'docs'))) {
16
+ if (relativePath.endsWith('.md')) {
17
+ paths.add(toPosixPath(path.join('.mustflow', 'docs', relativePath)));
18
+ }
19
+ }
20
+ for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'context'))) {
21
+ if (relativePath.endsWith('.md')) {
22
+ paths.add(toPosixPath(path.join('.mustflow', 'context', relativePath)));
23
+ }
24
+ }
25
+ for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'skills'))) {
26
+ if (relativePath === 'INDEX.md' || relativePath.endsWith('/SKILL.md')) {
27
+ paths.add(toPosixPath(path.join('.mustflow', 'skills', relativePath)));
28
+ }
29
+ }
30
+ for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'config'))) {
31
+ if (relativePath.endsWith('.toml')) {
32
+ paths.add(toPosixPath(path.join('.mustflow', 'config', relativePath)));
33
+ }
34
+ }
35
+ return Array.from(paths).sort((left, right) => left.localeCompare(right));
36
+ }
37
+ export function readText(projectRoot, relativePath) {
38
+ return readMustflowTextFile(projectRoot, relativePath);
39
+ }
40
+ function getDocumentType(relativePath) {
41
+ if (relativePath === 'AGENTS.md') {
42
+ return 'agent_rules';
43
+ }
44
+ if (relativePath.startsWith('.mustflow/config/')) {
45
+ return 'config';
46
+ }
47
+ if (relativePath === '.mustflow/skills/INDEX.md') {
48
+ return 'skill_index';
49
+ }
50
+ if (relativePath === '.mustflow/context/INDEX.md') {
51
+ return 'context_index';
52
+ }
53
+ if (relativePath.startsWith('.mustflow/context/')) {
54
+ return 'context';
55
+ }
56
+ if (relativePath.endsWith('/SKILL.md')) {
57
+ return 'skill';
58
+ }
59
+ if (relativePath.startsWith('.mustflow/docs/')) {
60
+ return 'workflow_doc';
61
+ }
62
+ return 'document';
63
+ }
64
+ function parseFrontmatter(content) {
65
+ if (!content.startsWith('---')) {
66
+ return {};
67
+ }
68
+ const end = content.indexOf('\n---', 3);
69
+ if (end === -1) {
70
+ return {};
71
+ }
72
+ const result = {};
73
+ const rawFrontmatter = content.slice(3, end);
74
+ for (const line of rawFrontmatter.split(/\r?\n/)) {
75
+ const separatorIndex = line.indexOf(':');
76
+ if (separatorIndex === -1) {
77
+ continue;
78
+ }
79
+ const key = line.slice(0, separatorIndex).trim();
80
+ const value = line.slice(separatorIndex + 1).trim();
81
+ if (key.length > 0 && value.length > 0) {
82
+ result[key] = value;
83
+ }
84
+ }
85
+ return result;
86
+ }
87
+ function getTitle(relativePath, content) {
88
+ const heading = content.match(/^#\s+(.+)$/mu)?.[1]?.trim();
89
+ return heading && heading.length > 0 ? heading : path.posix.basename(relativePath);
90
+ }
91
+ function getSections(content) {
92
+ return [...content.matchAll(/^##\s+(.+)$/gmu)].map((match) => match[1]?.trim()).filter((value) => Boolean(value));
93
+ }
94
+ function truncateUtf8(value, maxBytes) {
95
+ const buffer = Buffer.from(value, 'utf8');
96
+ if (buffer.byteLength <= maxBytes) {
97
+ return value;
98
+ }
99
+ return buffer.subarray(0, maxBytes).toString('utf8').replace(/\uFFFD$/u, '');
100
+ }
101
+ export function collectDocumentsFromPaths(projectRoot, relativePaths) {
102
+ return relativePaths.map((relativePath) => {
103
+ const content = readText(projectRoot, relativePath);
104
+ const frontmatter = parseFrontmatter(content);
105
+ const revision = Number.parseInt(frontmatter.revision ?? '', 10);
106
+ return {
107
+ path: relativePath,
108
+ type: getDocumentType(relativePath),
109
+ title: getTitle(relativePath, content),
110
+ locale: frontmatter.locale ?? null,
111
+ revision: Number.isInteger(revision) ? revision : null,
112
+ contentHash: sha256Text(content),
113
+ contentSnippet: truncateUtf8(content, MAX_SNIPPET_BYTES_PER_DOCUMENT),
114
+ sections: getSections(content),
115
+ };
116
+ });
117
+ }
118
+ export function collectDocuments(projectRoot) {
119
+ return collectDocumentsFromPaths(projectRoot, getExistingIndexablePaths(projectRoot));
120
+ }
121
+ export function collectSkills(documents) {
122
+ return documents
123
+ .filter((document) => document.type === 'skill')
124
+ .map((document) => ({
125
+ name: document.path.split('/').at(-2) ?? document.title,
126
+ path: document.path,
127
+ title: document.title,
128
+ }))
129
+ .sort((left, right) => left.name.localeCompare(right.name));
130
+ }
131
+ function normalizeMarkdownCell(value) {
132
+ return value
133
+ .replace(/<br\s*\/?>/giu, ' ')
134
+ .replace(/`([^`]+)`/gu, '$1')
135
+ .replace(/\s+/gu, ' ')
136
+ .trim();
137
+ }
138
+ function parseMarkdownTableRow(line) {
139
+ return line
140
+ .trim()
141
+ .replace(/^\|/u, '')
142
+ .replace(/\|$/u, '')
143
+ .split('|')
144
+ .map((cell) => normalizeMarkdownCell(cell));
145
+ }
146
+ function isMarkdownSeparatorRow(cells) {
147
+ return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/u.test(cell));
148
+ }
149
+ function skillNameFromPath(skillPath) {
150
+ return skillPath.split('/').at(-2) ?? path.posix.basename(skillPath, '.md');
151
+ }
152
+ export function splitVerificationIntents(value) {
153
+ return value
154
+ .split(',')
155
+ .map((item) => item.trim())
156
+ .filter(Boolean)
157
+ .sort((left, right) => left.localeCompare(right));
158
+ }
159
+ export function skillRouteKey(route) {
160
+ return `${route.skillName}\u0000${route.trigger}`;
161
+ }
162
+ export function collectSkillRoutes(projectRoot) {
163
+ const indexPath = path.join(projectRoot, '.mustflow', 'skills', 'INDEX.md');
164
+ if (!existsSync(indexPath)) {
165
+ return [];
166
+ }
167
+ const content = readMustflowTextFile(projectRoot, '.mustflow/skills/INDEX.md');
168
+ const routes = [];
169
+ let inRouteTable = false;
170
+ for (const line of content.split(/\r?\n/u)) {
171
+ if (!line.trim().startsWith('|')) {
172
+ if (inRouteTable && line.trim() === '') {
173
+ inRouteTable = false;
174
+ }
175
+ continue;
176
+ }
177
+ const cells = parseMarkdownTableRow(line);
178
+ if (cells.includes('Skill Document') && cells.includes('Trigger')) {
179
+ inRouteTable = true;
180
+ continue;
181
+ }
182
+ if (!inRouteTable || isMarkdownSeparatorRow(cells) || cells.length < 7) {
183
+ continue;
184
+ }
185
+ const [trigger, skillPath, requiredInput, editScope, risk, verificationIntents, expectedOutput] = cells;
186
+ if (!skillPath?.startsWith('.mustflow/skills/') || !skillPath.endsWith('/SKILL.md')) {
187
+ continue;
188
+ }
189
+ routes.push({
190
+ skillName: skillNameFromPath(skillPath),
191
+ skillPath,
192
+ trigger: trigger ?? '',
193
+ requiredInput: requiredInput ?? '',
194
+ editScope: editScope ?? '',
195
+ risk: risk ?? '',
196
+ verificationIntents: splitVerificationIntents(verificationIntents ?? ''),
197
+ expectedOutput: expectedOutput ?? '',
198
+ });
199
+ }
200
+ return routes.sort((left, right) => {
201
+ const skillOrder = left.skillName.localeCompare(right.skillName);
202
+ return skillOrder === 0 ? left.trigger.localeCompare(right.trigger) : skillOrder;
203
+ });
204
+ }
@@ -2,11 +2,9 @@ import { createHash } from 'node:crypto';
2
2
  import { existsSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { ensureFileTargetInsideWithoutSymlinks, ensureInside, readFileInsideWithoutSymlinks, readUtf8FileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks, } from './filesystem.js';
5
+ import { isRecord } from './command-contract.js';
5
6
  import { parseTomlText, stringifyToml } from './toml.js';
6
7
  export const MANIFEST_LOCK_RELATIVE_PATH = '.mustflow/config/manifest.lock.toml';
7
- function isRecord(value) {
8
- return typeof value === 'object' && value !== null && !Array.isArray(value);
9
- }
10
8
  function readString(table, key, label) {
11
9
  const value = table[key];
12
10
  if (typeof value !== 'string' || value.trim().length === 0) {
@@ -0,0 +1,53 @@
1
+ import { createHash } from 'node:crypto';
2
+ const REPO_MAP_DOC_ID = 'repo-map';
3
+ const REPO_MAP_LIFECYCLE = 'generated';
4
+ const REPO_MAP_GENERATOR = 'mustflow';
5
+ const REPO_MAP_RELATIVE_ROOT = '.';
6
+ const REPO_MAP_SOURCE_POLICY = 'anchors_only';
7
+ const REPO_MAP_PRIVACY_MODE = 'minimal';
8
+ export function getRepoMapSourceFingerprint(input) {
9
+ const payload = {
10
+ depth: input.depth,
11
+ includeNested: input.includeNested,
12
+ gitLsFilesStatus: input.gitLsFilesStatus,
13
+ priorityPaths: [...input.configuredPriorityPaths].sort(),
14
+ anchors: input.anchors.map((anchor) => anchor.relativePath).sort(),
15
+ nestedRepositories: input.nestedRepositories
16
+ .map((repository) => ({
17
+ relativePath: repository.relativePath,
18
+ mustflow: repository.mustflow,
19
+ agentRules: repository.agentRules,
20
+ repoMap: repository.repoMap,
21
+ mustflowConfig: repository.mustflowConfig,
22
+ commandContract: repository.commandContract,
23
+ contextIndex: repository.contextIndex,
24
+ skillIndex: repository.skillIndex,
25
+ rootDocuments: repository.rootDocuments.map((document) => document.relativePath).sort(),
26
+ machineContracts: [...repository.machineContracts].sort(),
27
+ manifests: [...repository.manifests].sort(),
28
+ commandAdapters: [...repository.commandAdapters].sort(),
29
+ editingPolicies: [...repository.editingPolicies].sort(),
30
+ }))
31
+ .sort((left, right) => left.relativePath.localeCompare(right.relativePath)),
32
+ };
33
+ const digest = createHash('sha256').update(JSON.stringify(payload)).digest('hex');
34
+ return `sha256:${digest}`;
35
+ }
36
+ export function renderRepoMapFrontmatter(anchorCount, sourceFingerprint, gitLsFilesStatus) {
37
+ const degraded = gitLsFilesStatus !== 'ok';
38
+ return [
39
+ '---',
40
+ `mustflow_doc: ${REPO_MAP_DOC_ID}`,
41
+ `lifecycle: ${REPO_MAP_LIFECYCLE}`,
42
+ `generated_by: ${REPO_MAP_GENERATOR}`,
43
+ `relative_root: "${REPO_MAP_RELATIVE_ROOT}"`,
44
+ `source_policy: ${REPO_MAP_SOURCE_POLICY}`,
45
+ `privacy_mode: ${REPO_MAP_PRIVACY_MODE}`,
46
+ `anchor_count: ${anchorCount}`,
47
+ `degraded: ${degraded ? 'true' : 'false'}`,
48
+ `git_ls_files_status: ${gitLsFilesStatus}`,
49
+ `source_fingerprint: "${sourceFingerprint}"`,
50
+ '---',
51
+ '',
52
+ ];
53
+ }
@@ -1,17 +1,12 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { createHash } from 'node:crypto';
3
2
  import { existsSync, lstatSync, readdirSync, realpathSync, statSync } from 'node:fs';
4
3
  import path from 'node:path';
5
4
  import { toPosixPath } from './filesystem.js';
5
+ import { getRepoMapSourceFingerprint, renderRepoMapFrontmatter, } from './repo-map-frontmatter.js';
6
6
  import { writeUtf8FileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
7
+ import { isRecord } from './command-contract.js';
7
8
  import { readMustflowTomlFile } from './toml.js';
8
9
  const DEFAULT_DEPTH = 3;
9
- const REPO_MAP_DOC_ID = 'repo-map';
10
- const REPO_MAP_LIFECYCLE = 'generated';
11
- const REPO_MAP_GENERATOR = 'mustflow';
12
- const REPO_MAP_RELATIVE_ROOT = '.';
13
- const REPO_MAP_SOURCE_POLICY = 'anchors_only';
14
- const REPO_MAP_PRIVACY_MODE = 'minimal';
15
10
  const GIT_LS_FILES_TIMEOUT_MS = 5_000;
16
11
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 1_048_576;
17
12
  const EXCLUDED_SEGMENTS = new Set([
@@ -196,9 +191,6 @@ const EXACT_ANCHOR_DESCRIPTIONS = new Map([
196
191
  ['.mustflow/docs/agent-workflow.md', 'Shared workflow policy for agent work.'],
197
192
  ['.mustflow/skills/INDEX.md', 'Index of available procedural skills.'],
198
193
  ]);
199
- function isRecord(value) {
200
- return typeof value === 'object' && value !== null && !Array.isArray(value);
201
- }
202
194
  function getStringArray(value) {
203
195
  return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
204
196
  }
@@ -624,52 +616,6 @@ function countNestedEntrypoints(repository) {
624
616
  ...repository.editingPolicies,
625
617
  ].filter(Boolean).length;
626
618
  }
627
- function getSourceFingerprint(depth, includeNested, configuredPriorityPaths, gitLsFilesStatus, anchors, nestedRepositories) {
628
- const payload = {
629
- depth,
630
- includeNested,
631
- gitLsFilesStatus,
632
- priorityPaths: [...configuredPriorityPaths].sort(),
633
- anchors: anchors.map((anchor) => anchor.relativePath).sort(),
634
- nestedRepositories: nestedRepositories
635
- .map((repository) => ({
636
- relativePath: repository.relativePath,
637
- mustflow: repository.mustflow,
638
- agentRules: repository.agentRules,
639
- repoMap: repository.repoMap,
640
- mustflowConfig: repository.mustflowConfig,
641
- commandContract: repository.commandContract,
642
- contextIndex: repository.contextIndex,
643
- skillIndex: repository.skillIndex,
644
- rootDocuments: repository.rootDocuments.map((document) => document.relativePath).sort(),
645
- machineContracts: [...repository.machineContracts].sort(),
646
- manifests: [...repository.manifests].sort(),
647
- commandAdapters: [...repository.commandAdapters].sort(),
648
- editingPolicies: [...repository.editingPolicies].sort(),
649
- }))
650
- .sort((left, right) => left.relativePath.localeCompare(right.relativePath)),
651
- };
652
- const digest = createHash('sha256').update(JSON.stringify(payload)).digest('hex');
653
- return `sha256:${digest}`;
654
- }
655
- function renderRepoMapFrontmatter(anchorCount, sourceFingerprint, gitLsFilesStatus) {
656
- const degraded = gitLsFilesStatus !== 'ok';
657
- return [
658
- '---',
659
- `mustflow_doc: ${REPO_MAP_DOC_ID}`,
660
- `lifecycle: ${REPO_MAP_LIFECYCLE}`,
661
- `generated_by: ${REPO_MAP_GENERATOR}`,
662
- `relative_root: "${REPO_MAP_RELATIVE_ROOT}"`,
663
- `source_policy: ${REPO_MAP_SOURCE_POLICY}`,
664
- `privacy_mode: ${REPO_MAP_PRIVACY_MODE}`,
665
- `anchor_count: ${anchorCount}`,
666
- `degraded: ${degraded ? 'true' : 'false'}`,
667
- `git_ls_files_status: ${gitLsFilesStatus}`,
668
- `source_fingerprint: "${sourceFingerprint}"`,
669
- '---',
670
- '',
671
- ];
672
- }
673
619
  function renderSourceQuality(gitLsFilesStatus) {
674
620
  if (gitLsFilesStatus === 'ok') {
675
621
  return [];
@@ -701,7 +647,14 @@ export function generateRepoMap(projectRoot, options = {}) {
701
647
  .filter((anchor) => Boolean(anchor));
702
648
  const otherAnchors = anchors.filter((anchor) => !priorityPathSet.has(anchor.relativePath));
703
649
  const anchorCount = anchors.length + nestedRepositories.reduce((total, repository) => total + countNestedEntrypoints(repository), 0);
704
- const sourceFingerprint = getSourceFingerprint(depth, mapConfig.includeNested, configuredPriorityPaths, gitLsFilesStatus, anchors, nestedRepositories);
650
+ const sourceFingerprint = getRepoMapSourceFingerprint({
651
+ depth,
652
+ includeNested: mapConfig.includeNested,
653
+ configuredPriorityPaths,
654
+ gitLsFilesStatus,
655
+ anchors,
656
+ nestedRepositories,
657
+ });
705
658
  return [
706
659
  ...renderRepoMapFrontmatter(anchorCount, sourceFingerprint, gitLsFilesStatus),
707
660
  '# REPO_MAP.md',
@@ -0,0 +1,27 @@
1
+ import { MANIFEST_LOCK_RELATIVE_PATH, readManifestLock } from './manifest-lock.js';
2
+ export const ALLOW_UNTRUSTED_ROOT_OPTION = '--allow-untrusted-root';
3
+ export function assessRunRootTrust(projectRoot) {
4
+ const readResult = readManifestLock(projectRoot);
5
+ if (readResult.kind === 'present') {
6
+ return {
7
+ trusted: true,
8
+ reason: 'manifest_lock_present',
9
+ manifestLockPath: readResult.lockPath,
10
+ detail: null,
11
+ };
12
+ }
13
+ if (readResult.kind === 'invalid') {
14
+ return {
15
+ trusted: false,
16
+ reason: 'manifest_lock_invalid',
17
+ manifestLockPath: readResult.lockPath,
18
+ detail: readResult.message,
19
+ };
20
+ }
21
+ return {
22
+ trusted: false,
23
+ reason: 'manifest_lock_missing',
24
+ manifestLockPath: readResult.lockPath,
25
+ detail: MANIFEST_LOCK_RELATIVE_PATH,
26
+ };
27
+ }
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { isRecord } from '../command-contract.js';
4
4
  import { validateCommandContractConfig, validateCommandContractStrictDefaults, } from '../../../core/command-contract-validation.js';
5
+ import { describeCheckIssues } from '../../../core/check-issues.js';
5
6
  import { ALLOWED_RETENTION_ON_LIMIT, ALLOWED_RETENTION_STORES, DEFAULT_RETENTION_LIMITS, readNestedRetentionTable, readRetentionTable, resolveRetentionLimits, } from '../../../core/retention-policy.js';
6
7
  import { formatManagedMarkdownLabel, getManagedMarkdownExpectation, } from '../../../core/authority-resolution.js';
7
8
  import { SKILL_INDEX_ROUTE_COLUMN_COUNT, SKILL_INDEX_ROUTE_COLUMNS, SKILL_INDEX_SKILL_PATH_COLUMN_INDEX, findSkillRouteConflictWarnings, findSkillIndexRoutePathColumn, parseSkillIndexRoutes, readBacktickValues, } from '../../../core/skill-route-alignment.js';
@@ -1641,12 +1642,15 @@ function collectCheckIssues(projectRoot, options = {}) {
1641
1642
  }
1642
1643
  export function checkMustflowProjectReport(projectRoot, options = {}) {
1643
1644
  const issues = collectCheckIssues(projectRoot, options);
1644
- const errors = issues.filter((issue) => issue.severity !== 'warning').map((issue) => issue.message);
1645
- const warnings = issues.filter((issue) => issue.severity === 'warning').map((issue) => issue.message);
1645
+ const errorIssues = issues.filter((issue) => issue.severity !== 'warning');
1646
+ const warningIssues = issues.filter((issue) => issue.severity === 'warning');
1647
+ const errors = errorIssues.map((issue) => issue.message);
1648
+ const warnings = warningIssues.map((issue) => issue.message);
1646
1649
  return {
1647
1650
  issues: errors,
1648
1651
  warnings,
1649
1652
  allMessages: [...errors, ...warnings],
1653
+ issueDetails: describeCheckIssues([...errorIssues, ...warningIssues]),
1650
1654
  };
1651
1655
  }
1652
1656
  export function checkMustflowProject(projectRoot, options = {}) {
@@ -0,0 +1,47 @@
1
+ export const CHANGE_CLASSIFICATION_POLICY_VERSION = '1';
2
+ function surface(kind, category, isPublicSurface, validationReasons, affectedContracts, updatePolicy, driftChecks) {
3
+ return {
4
+ kind,
5
+ category,
6
+ isPublicSurface,
7
+ validationReasons,
8
+ affectedContracts,
9
+ updatePolicy,
10
+ driftChecks,
11
+ };
12
+ }
13
+ export const UNKNOWN_CHANGE_REASON = 'unknown_change';
14
+ export const UNKNOWN_SURFACE = surface('unclassified_path', 'unknown', false, [UNKNOWN_CHANGE_REASON], ['unclassified repository path'], 'not_applicable', ['classification rule coverage']);
15
+ function rule(id, match, changeKinds, surfaceContract) {
16
+ return {
17
+ id,
18
+ match,
19
+ changeKinds,
20
+ surface: surfaceContract,
21
+ };
22
+ }
23
+ export const CHANGE_CLASSIFICATION_RULE_DEFINITIONS = [
24
+ rule('readme_page', /^README\.md$/u, ['documentation'], surface('readme_page', 'documentation', true, ['docs_change', 'copy_change'], ['public documentation', 'command examples'], 'update', ['link targets', 'command examples', 'package metadata references'])),
25
+ rule('docs_site_translation', /^docs-site\/src\/content\/docs\/(?!en\/)[^/]+\//u, ['documentation', 'translation'], surface('docs_site_translation', 'documentation', true, ['docs_change', 'i18n_change'], ['documentation site', 'localized content', 'navigation links'], 'update_or_mark_stale', ['source page parity', 'navigation links', 'localized examples'])),
26
+ rule('docs_site_page', /^docs-site\/src\/content\/docs\//u, ['documentation'], surface('docs_site_page', 'documentation', true, ['docs_change'], ['documentation site', 'navigation links', 'localized content'], 'update', ['navigation links', 'localized copies', 'command examples'])),
27
+ rule('installed_template_translation', /^templates\/[^/]+\/locales\/[^/]+\//u, ['installed_template', 'translation'], surface('installed_template_translation', 'installed-template', true, ['i18n_change', 'template_version_change'], ['installed template files', 'localized workflow documents', 'template i18n metadata'], 'update_or_mark_stale', ['template i18n metadata', 'localized frontmatter', 'source revision'])),
28
+ rule('installed_template', /^templates\/[^/]+\//u, ['installed_template'], surface('installed_template', 'installed-template', true, ['template_version_change', 'packaging_change'], ['installed template files', 'package contents', 'template manifest'], 'update', ['template manifest', 'package inventory', 'localized copies'])),
29
+ rule('workflow_root', /^(AGENTS\.md|\.mustflow\/(?:docs|context|skills|config)\/)/u, ['workflow'], surface('workflow_root', 'workflow', true, ['mustflow_docs_change', 'mustflow_config_change'], ['agent workflow contract', 'command contract', 'installed workflow files'], 'update', ['strict workflow validation', 'installed template parity', 'skill route alignment'])),
30
+ rule('host_instruction', /^(CLAUDE\.md|GEMINI\.md|\.github\/copilot-instructions\.md|\.cursor\/rules\/[^/]+\.(?:md|mdc))$/u, ['workflow', 'host_instruction'], surface('host_instruction', 'workflow', true, ['mustflow_docs_change'], ['host instruction compatibility', 'agent workflow contract', 'command contract boundary'], 'update_or_mark_stale', ['host instruction conflicts', 'command contract boundary'])),
31
+ rule('example', /^examples\//u, ['example'], surface('example', 'example', true, ['docs_change', 'public_api_change'], ['generated examples', 'human-readable examples'], 'update', ['example commands', 'linked docs', 'public behavior claims'])),
32
+ rule('schema_contract', /^schemas\//u, ['schema'], surface('schema_contract', 'contract', true, ['public_api_change', 'release_risk'], ['JSON schema', 'machine-readable output contract'], 'update', ['schema tests', 'documented JSON fields', 'package inventory'])),
33
+ rule('package_metadata', /^package\.json$/u, ['package_metadata'], surface('package_metadata', 'release', true, ['package_metadata_change', 'release_risk'], ['npm package metadata', 'published package contents'], 'update', ['package metadata tests', 'version source discovery', 'published file inventory'])),
34
+ rule('test_fixture', /^tests\/fixtures\//u, ['test_fixture'], surface('test_fixture', 'test', false, ['test_change'], ['regression fixture inputs'], 'not_applicable', ['fixture safety', 'test route coverage'])),
35
+ rule('test_contract', /^tests\//u, ['test'], surface('test_contract', 'test', false, ['test_change'], ['test behavior contract'], 'not_applicable', ['related test selection'])),
36
+ rule('implementation', /^(src|scripts)\//u, ['implementation'], surface('implementation', 'code', false, ['code_change'], ['runtime behavior when exported through CLI or package output'], 'not_applicable', ['related tests', 'build output'])),
37
+ ];
38
+ export function listChangeClassificationPolicyRuleDescriptors() {
39
+ return CHANGE_CLASSIFICATION_RULE_DEFINITIONS.map((classificationRule) => ({
40
+ id: classificationRule.id,
41
+ patternKind: 'regexp',
42
+ pattern: classificationRule.match.source,
43
+ patternFlags: classificationRule.match.flags,
44
+ changeKinds: classificationRule.changeKinds,
45
+ surface: classificationRule.surface,
46
+ }));
47
+ }
@@ -1,47 +1,21 @@
1
+ import { CHANGE_CLASSIFICATION_RULE_DEFINITIONS, UNKNOWN_SURFACE, listChangeClassificationPolicyRuleDescriptors, } from './change-classification-policy.js';
1
2
  export const PUBLIC_SURFACE_UPDATE_POLICIES = [
2
3
  'update',
3
4
  'update_or_mark_stale',
4
5
  'not_applicable',
5
6
  ];
6
- function surface(kind, category, isPublicSurface, validationReasons, affectedContracts, updatePolicy, driftChecks) {
7
+ function compileRule(definition) {
7
8
  return {
8
- kind,
9
- category,
10
- isPublicSurface,
11
- validationReasons,
12
- affectedContracts,
13
- updatePolicy,
14
- driftChecks,
15
- };
16
- }
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']);
19
- function rule(id, match, changeKinds, surfaceContract) {
20
- return {
21
- id,
9
+ id: definition.id,
22
10
  patternKind: 'regexp',
23
- pattern: match.source,
24
- patternFlags: match.flags,
25
- match,
26
- changeKinds,
27
- surface: surfaceContract,
11
+ pattern: definition.match.source,
12
+ patternFlags: definition.match.flags,
13
+ match: definition.match,
14
+ changeKinds: definition.changeKinds,
15
+ surface: definition.surface,
28
16
  };
29
17
  }
30
- const CHANGE_CLASSIFICATION_RULES = [
31
- rule('readme_page', /^README\.md$/u, ['documentation'], surface('readme_page', 'documentation', true, ['docs_change', 'copy_change'], ['public documentation', 'command examples'], 'update', ['link targets', 'command examples', 'package metadata references'])),
32
- rule('docs_site_translation', /^docs-site\/src\/content\/docs\/(?!en\/)[^/]+\//u, ['documentation', 'translation'], surface('docs_site_translation', 'documentation', true, ['docs_change', 'i18n_change'], ['documentation site', 'localized content', 'navigation links'], 'update_or_mark_stale', ['source page parity', 'navigation links', 'localized examples'])),
33
- rule('docs_site_page', /^docs-site\/src\/content\/docs\//u, ['documentation'], surface('docs_site_page', 'documentation', true, ['docs_change'], ['documentation site', 'navigation links', 'localized content'], 'update', ['navigation links', 'localized copies', 'command examples'])),
34
- rule('installed_template_translation', /^templates\/[^/]+\/locales\/[^/]+\//u, ['installed_template', 'translation'], surface('installed_template_translation', 'installed-template', true, ['i18n_change', 'template_version_change'], ['installed template files', 'localized workflow documents', 'template i18n metadata'], 'update_or_mark_stale', ['template i18n metadata', 'localized frontmatter', 'source revision'])),
35
- rule('installed_template', /^templates\/[^/]+\//u, ['installed_template'], surface('installed_template', 'installed-template', true, ['template_version_change', 'packaging_change'], ['installed template files', 'package contents', 'template manifest'], 'update', ['template manifest', 'package inventory', 'localized copies'])),
36
- rule('workflow_root', /^(AGENTS\.md|\.mustflow\/(?:docs|context|skills|config)\/)/u, ['workflow'], surface('workflow_root', 'workflow', true, ['mustflow_docs_change', 'mustflow_config_change'], ['agent workflow contract', 'command contract', 'installed workflow files'], 'update', ['strict workflow validation', 'installed template parity', 'skill route alignment'])),
37
- rule('host_instruction', /^(CLAUDE\.md|GEMINI\.md|\.github\/copilot-instructions\.md|\.cursor\/rules\/[^/]+\.(?:md|mdc))$/u, ['workflow', 'host_instruction'], surface('host_instruction', 'workflow', true, ['mustflow_docs_change'], ['host instruction compatibility', 'agent workflow contract', 'command contract boundary'], 'update_or_mark_stale', ['host instruction conflicts', 'command contract boundary'])),
38
- rule('example', /^examples\//u, ['example'], surface('example', 'example', true, ['docs_change', 'public_api_change'], ['generated examples', 'human-readable examples'], 'update', ['example commands', 'linked docs', 'public behavior claims'])),
39
- rule('schema_contract', /^schemas\//u, ['schema'], surface('schema_contract', 'contract', true, ['public_api_change', 'release_risk'], ['JSON schema', 'machine-readable output contract'], 'update', ['schema tests', 'documented JSON fields', 'package inventory'])),
40
- rule('package_metadata', /^package\.json$/u, ['package_metadata'], surface('package_metadata', 'release', true, ['package_metadata_change', 'release_risk'], ['npm package metadata', 'published package contents'], 'update', ['package metadata tests', 'version source discovery', 'published file inventory'])),
41
- rule('test_fixture', /^tests\/fixtures\//u, ['test_fixture'], surface('test_fixture', 'test', false, ['test_change'], ['regression fixture inputs'], 'not_applicable', ['fixture safety', 'test route coverage'])),
42
- rule('test_contract', /^tests\//u, ['test'], surface('test_contract', 'test', false, ['test_change'], ['test behavior contract'], 'not_applicable', ['related test selection'])),
43
- rule('implementation', /^(src|scripts)\//u, ['implementation'], surface('implementation', 'code', false, ['code_change'], ['runtime behavior when exported through CLI or package output'], 'not_applicable', ['related tests', 'build output'])),
44
- ];
18
+ const CHANGE_CLASSIFICATION_RULES = CHANGE_CLASSIFICATION_RULE_DEFINITIONS.map(compileRule);
45
19
  function uniqueSorted(values) {
46
20
  return [...new Set(values)].sort((left, right) => left.localeCompare(right));
47
21
  }
@@ -93,14 +67,7 @@ export function classifyChangePath(relativePath) {
93
67
  };
94
68
  }
95
69
  export function listChangeClassificationRuleDescriptors() {
96
- return CHANGE_CLASSIFICATION_RULES.map((classificationRule) => ({
97
- id: classificationRule.id,
98
- patternKind: classificationRule.patternKind,
99
- pattern: classificationRule.pattern,
100
- patternFlags: classificationRule.patternFlags,
101
- changeKinds: classificationRule.changeKinds,
102
- surface: classificationRule.surface,
103
- }));
70
+ return listChangeClassificationPolicyRuleDescriptors();
104
71
  }
105
72
  export function listChangeClassificationValidationReasons() {
106
73
  return uniqueSorted([
@@ -86,11 +86,15 @@ export function getCheckIssueId(message) {
86
86
  }
87
87
  return null;
88
88
  }
89
- export function describeCheckIssues(messages) {
90
- return messages.map((message) => ({
91
- id: getCheckIssueId(message),
92
- severity: message.startsWith('Strict warning: ') ? 'warning' : 'error',
93
- mode: message.startsWith('Strict') ? 'strict' : 'base',
94
- message,
95
- }));
89
+ export function describeCheckIssues(issues) {
90
+ return issues.map((issue) => {
91
+ const message = typeof issue === 'string' ? issue : issue.message;
92
+ const severity = message.startsWith('Strict warning: ') ? 'warning' : 'error';
93
+ return {
94
+ id: typeof issue === 'string' ? getCheckIssueId(message) : issue.id ?? getCheckIssueId(message),
95
+ severity: typeof issue === 'string' ? severity : issue.severity ?? severity,
96
+ mode: message.startsWith('Strict') ? 'strict' : 'base',
97
+ message,
98
+ };
99
+ });
96
100
  }