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.
- package/README.md +8 -0
- package/dist/cli/commands/check.js +2 -2
- package/dist/cli/commands/classify.js +2 -0
- package/dist/cli/commands/dashboard.js +9 -69
- package/dist/cli/commands/help.js +1 -3
- package/dist/cli/commands/run/receipt.js +1 -0
- package/dist/cli/commands/run.js +14 -1
- package/dist/cli/commands/verify/evidence-input.js +269 -0
- package/dist/cli/commands/verify/input.js +212 -0
- package/dist/cli/commands/verify/state-paths.js +33 -0
- package/dist/cli/commands/verify.js +29 -511
- package/dist/cli/i18n/en.js +3 -0
- package/dist/cli/i18n/es.js +3 -0
- package/dist/cli/i18n/fr.js +3 -0
- package/dist/cli/i18n/hi.js +3 -0
- package/dist/cli/i18n/ko.js +3 -0
- package/dist/cli/i18n/zh.js +3 -0
- package/dist/cli/lib/dashboard-export.js +2 -0
- package/dist/cli/lib/dashboard-mutations.js +79 -0
- package/dist/cli/lib/doc-review-ledger.js +1 -3
- package/dist/cli/lib/local-index/command-effect-index.js +25 -0
- package/dist/cli/lib/local-index/hashing.js +7 -0
- package/dist/cli/lib/local-index/index.js +127 -826
- package/dist/cli/lib/local-index/source-index.js +137 -0
- package/dist/cli/lib/local-index/verification-evidence.js +451 -0
- package/dist/cli/lib/local-index/workflow-documents.js +204 -0
- package/dist/cli/lib/manifest-lock.js +1 -3
- package/dist/cli/lib/repo-map-frontmatter.js +53 -0
- package/dist/cli/lib/repo-map.js +10 -57
- package/dist/cli/lib/run-root-trust.js +27 -0
- package/dist/cli/lib/validation/index.js +6 -2
- package/dist/core/change-classification-policy.js +47 -0
- package/dist/core/change-classification.js +10 -43
- package/dist/core/check-issues.js +11 -7
- package/dist/core/command-contract-validation.js +22 -20
- package/dist/core/contract-lint.js +6 -2
- package/dist/core/correlation-id.js +16 -0
- package/dist/core/run-receipt.js +1 -0
- package/package.json +4 -1
- package/schemas/README.md +4 -0
- package/schemas/change-verification-report.schema.json +4 -0
- package/schemas/classify-report.schema.json +4 -0
- package/schemas/dashboard-export.schema.json +4 -0
- package/schemas/latest-run-pointer.schema.json +4 -0
- package/schemas/run-receipt.schema.json +4 -0
- package/schemas/verify-report.schema.json +4 -0
- package/schemas/verify-run-manifest.schema.json +4 -0
- package/templates/default/i18n.toml +3 -3
- package/templates/default/locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md +25 -2
- package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +9 -1
- package/templates/default/locales/en/.mustflow/skills/test-design-guard/SKILL.md +9 -1
- package/templates/default/locales/ko/.mustflow/context/INDEX.md +12 -12
- package/templates/default/locales/ko/.mustflow/context/PROJECT.md +6 -6
- package/templates/default/locales/ko/.mustflow/docs/agent-workflow.md +70 -70
- package/templates/default/locales/ko/AGENTS.md +14 -14
- 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
|
+
}
|
package/dist/cli/lib/repo-map.js
CHANGED
|
@@ -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 =
|
|
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
|
|
1645
|
-
const
|
|
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
|
|
7
|
+
function compileRule(definition) {
|
|
7
8
|
return {
|
|
8
|
-
|
|
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:
|
|
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
|
|
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(
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
severity
|
|
93
|
-
|
|
94
|
-
|
|
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
|
}
|