thachvd-kit 1.0.37 → 1.0.39
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/LICENSE +21 -21
- package/README.md +303 -11
- package/THIRD_PARTY_NOTICES.md +49 -49
- package/bin/cli.js +1791 -1785
- package/bin/config.js +164 -0
- package/bin/entry.js +11 -1
- package/bin/native-skills.js +183 -149
- package/bin/spec-doctor.js +252 -0
- package/bin/spec-link.js +97 -0
- package/bin/spec-recipe.js +74 -0
- package/bin/spec-site.js +639 -0
- package/bin/spec-state.js +415 -0
- package/bin/spec.js +901 -0
- package/bin/upgrade.js +303 -303
- package/package.json +3 -3
- package/skills/finishing-a-development-branch/SKILL.md +240 -240
- package/skills/requesting-code-review/code-reviewer.md +198 -198
- package/skills/subagent-driven-development/SKILL.md +574 -574
- package/skills/subagent-driven-development/implementer-prompt.md +154 -154
- package/skills/subagent-driven-development/re-review-prompt.md +115 -115
- package/skills/subagent-driven-development/scripts/review-package +53 -53
- package/skills/subagent-driven-development/scripts/review-package.js +52 -52
- package/skills/subagent-driven-development/scripts/sdd-workspace +82 -82
- package/skills/subagent-driven-development/scripts/sdd-workspace-lib.js +62 -62
- package/skills/subagent-driven-development/scripts/sdd-workspace.js +15 -15
- package/skills/subagent-driven-development/scripts/task-brief +43 -43
- package/skills/subagent-driven-development/scripts/task-brief.js +46 -46
- package/skills/subagent-driven-development/task-reviewer-prompt.md +207 -207
- package/skills/system-discovery/SKILL.md +140 -0
- package/skills/system-reverse-engineer/SKILL.md +208 -0
- package/skills/system-spec-review/SKILL.md +177 -0
- package/skills/upstream.json +30 -30
- package/skills/using-git-worktrees/SKILL.md +175 -175
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const META_DIR = '_meta';
|
|
6
|
+
const VERIFICATION_FILE = 'verification.json';
|
|
7
|
+
const REVIEW_FILE = 'review.md';
|
|
8
|
+
const PROGRESS_FILE = 'reverse-progress.json';
|
|
9
|
+
const VERIFICATION_SCHEMA_VERSION = 1;
|
|
10
|
+
const PROGRESS_SCHEMA_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
function portable(value) {
|
|
13
|
+
return value.split(path.sep).join('/');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function nowIso() {
|
|
17
|
+
return new Date().toISOString();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function metaRoot(workspaceRoot, manifest) {
|
|
21
|
+
return path.join(workspaceRoot, manifest.spec_root, META_DIR);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function verificationPath(workspaceRoot, manifest) {
|
|
25
|
+
return path.join(metaRoot(workspaceRoot, manifest), VERIFICATION_FILE);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function reviewPath(workspaceRoot, manifest) {
|
|
29
|
+
return path.join(metaRoot(workspaceRoot, manifest), REVIEW_FILE);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function progressSlug(capability) {
|
|
33
|
+
if (!capability) return null;
|
|
34
|
+
const slug = String(capability).trim().toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
36
|
+
.replace(/^-+|-+$/g, '');
|
|
37
|
+
if (!slug) throw new Error('Capability name must contain letters or numbers');
|
|
38
|
+
return slug;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function progressFileName(capability = null) {
|
|
42
|
+
const slug = progressSlug(capability);
|
|
43
|
+
return slug ? `reverse-progress-${slug}.json` : PROGRESS_FILE;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function progressPath(workspaceRoot, manifest, capability = null) {
|
|
47
|
+
return path.join(metaRoot(workspaceRoot, manifest), progressFileName(capability));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ensureMetaRoot(workspaceRoot, manifest) {
|
|
51
|
+
const directory = metaRoot(workspaceRoot, manifest);
|
|
52
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
53
|
+
return directory;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function runGit(repoRoot, args, options = {}) {
|
|
57
|
+
const spawn = options.spawn || spawnSync;
|
|
58
|
+
const result = spawn('git', args, {
|
|
59
|
+
cwd: repoRoot,
|
|
60
|
+
encoding: 'utf8'
|
|
61
|
+
});
|
|
62
|
+
if (result.status !== 0) {
|
|
63
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
|
|
64
|
+
return { ok: false, error: stderr || `git ${args.join(' ')} failed`, status: result.status };
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, stdout: typeof result.stdout === 'string' ? result.stdout.trim() : '' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getRepoHead(repoRoot, options = {}) {
|
|
70
|
+
const result = runGit(repoRoot, ['rev-parse', 'HEAD'], options);
|
|
71
|
+
if (!result.ok) return result;
|
|
72
|
+
return { ok: true, commit: result.stdout };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function captureRepoHeads(workspaceRoot, manifest, options = {}) {
|
|
76
|
+
const heads = {};
|
|
77
|
+
for (const repo of manifest.repos) {
|
|
78
|
+
const repoRoot = path.resolve(workspaceRoot, repo.path);
|
|
79
|
+
if (!fs.existsSync(repoRoot)) {
|
|
80
|
+
heads[repo.name] = { ok: false, path: repo.path, error: 'repository path is missing' };
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const head = getRepoHead(repoRoot, options);
|
|
84
|
+
heads[repo.name] = head.ok
|
|
85
|
+
? { ok: true, path: repo.path, commit: head.commit }
|
|
86
|
+
: { ok: false, path: repo.path, error: head.error };
|
|
87
|
+
}
|
|
88
|
+
return heads;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readJsonFile(filePath, label) {
|
|
92
|
+
if (!fs.existsSync(filePath)) return null;
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new Error(`Cannot parse ${label}: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readVerification(workspaceRoot, manifest) {
|
|
101
|
+
const filePath = verificationPath(workspaceRoot, manifest);
|
|
102
|
+
const data = readJsonFile(filePath, `${manifest.spec_root}/${META_DIR}/${VERIFICATION_FILE}`);
|
|
103
|
+
if (!data) return null;
|
|
104
|
+
if (data.schema_version !== VERIFICATION_SCHEMA_VERSION) {
|
|
105
|
+
throw new Error(`Unsupported verification schema_version: ${data.schema_version}`);
|
|
106
|
+
}
|
|
107
|
+
if (data.system && data.system !== manifest.name) {
|
|
108
|
+
throw new Error(`verification system mismatch: expected ${manifest.name}, got ${data.system}`);
|
|
109
|
+
}
|
|
110
|
+
if (!data.repositories || typeof data.repositories !== 'object') {
|
|
111
|
+
throw new Error('verification.json requires repositories');
|
|
112
|
+
}
|
|
113
|
+
if (!data.capabilities || typeof data.capabilities !== 'object') {
|
|
114
|
+
throw new Error('verification.json requires capabilities');
|
|
115
|
+
}
|
|
116
|
+
return data;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeSourcePath(value) {
|
|
120
|
+
if (typeof value !== 'string' || !value.trim()) return null;
|
|
121
|
+
const raw = value.trim().replace(/\\/g, '/');
|
|
122
|
+
if (raw.startsWith('/') || /^[A-Za-z]:\//.test(raw)) {
|
|
123
|
+
throw new Error(`source path must be repository-relative: ${value}`);
|
|
124
|
+
}
|
|
125
|
+
const normalized = raw.replace(/^\.\//, '').replace(/\/+/g, '/').replace(/\/$/, '');
|
|
126
|
+
if (!normalized || normalized === '.' || normalized === '..' || normalized.startsWith('../') || normalized.includes('/../')) {
|
|
127
|
+
throw new Error(`source path must stay inside the repository: ${value}`);
|
|
128
|
+
}
|
|
129
|
+
return normalized;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeSourcePaths(value) {
|
|
133
|
+
if (!Array.isArray(value)) return [];
|
|
134
|
+
return [...new Set(value
|
|
135
|
+
.map(normalizeSourcePath)
|
|
136
|
+
.filter(Boolean))];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function validateVerification(manifest, data) {
|
|
140
|
+
if (!data || typeof data !== 'object') throw new Error('verification data must be an object');
|
|
141
|
+
if (data.schema_version !== VERIFICATION_SCHEMA_VERSION) {
|
|
142
|
+
throw new Error(`verification schema_version must be ${VERIFICATION_SCHEMA_VERSION}`);
|
|
143
|
+
}
|
|
144
|
+
if (data.system && data.system !== manifest.name) {
|
|
145
|
+
throw new Error(`verification system mismatch: expected ${manifest.name}, got ${data.system}`);
|
|
146
|
+
}
|
|
147
|
+
const repoNames = new Set(manifest.repos.map(repo => repo.name));
|
|
148
|
+
for (const repo of manifest.repos) {
|
|
149
|
+
if (!data.repositories || !data.repositories[repo.name]) {
|
|
150
|
+
throw new Error(`verification is missing repository: ${repo.name}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const [repoName, repoState] of Object.entries(data.repositories || {})) {
|
|
154
|
+
if (!repoNames.has(repoName)) throw new Error(`verification references unknown repository: ${repoName}`);
|
|
155
|
+
if (!repoState || typeof repoState.commit !== 'string' || !/^[0-9a-f]{40}$/i.test(repoState.commit.trim())) {
|
|
156
|
+
throw new Error(`verification repository ${repoName} requires a full Git commit SHA`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const capabilityEntries = Object.entries(data.capabilities || {});
|
|
160
|
+
if (capabilityEntries.length === 0) throw new Error('verification requires at least one capability');
|
|
161
|
+
for (const [slug, capability] of capabilityEntries) {
|
|
162
|
+
if (!capability || typeof capability !== 'object') throw new Error(`invalid capability verification: ${slug}`);
|
|
163
|
+
if (!['verified', 'needs_review', 'blocked'].includes(capability.status)) {
|
|
164
|
+
throw new Error(`invalid verification status for ${slug}: ${capability.status}`);
|
|
165
|
+
}
|
|
166
|
+
capability.source_paths = capability.source_paths || {};
|
|
167
|
+
capability.verified_commits = capability.verified_commits || {};
|
|
168
|
+
for (const [repoName, sourcePaths] of Object.entries(capability.source_paths)) {
|
|
169
|
+
if (!repoNames.has(repoName)) throw new Error(`capability ${slug} references unknown repository: ${repoName}`);
|
|
170
|
+
capability.source_paths[repoName] = normalizeSourcePaths(sourcePaths);
|
|
171
|
+
const commit = capability.verified_commits[repoName];
|
|
172
|
+
if (capability.status === 'verified' && (!commit || !/^[0-9a-f]{40}$/i.test(commit))) {
|
|
173
|
+
throw new Error(`verified capability ${slug} requires verified_commits.${repoName}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const [repoName, commit] of Object.entries(capability.verified_commits)) {
|
|
177
|
+
if (!repoNames.has(repoName)) throw new Error(`capability ${slug} verified_commits references unknown repository: ${repoName}`);
|
|
178
|
+
if (!/^[0-9a-f]{40}$/i.test(String(commit))) {
|
|
179
|
+
throw new Error(`capability ${slug} has invalid verified commit for ${repoName}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (capability.status === 'verified') {
|
|
184
|
+
const sourceCount = Object.values(capability.source_paths)
|
|
185
|
+
.reduce((sum, paths) => sum + paths.length, 0);
|
|
186
|
+
if (sourceCount === 0) {
|
|
187
|
+
throw new Error(`verified capability ${slug} requires at least one source path`);
|
|
188
|
+
}
|
|
189
|
+
if (!capability.evidence || typeof capability.evidence !== 'object') {
|
|
190
|
+
throw new Error(`verified capability ${slug} requires evidence counts`);
|
|
191
|
+
}
|
|
192
|
+
const requiredCounts = [
|
|
193
|
+
'implementation_claims_checked',
|
|
194
|
+
'test_claims_checked',
|
|
195
|
+
'unsupported_claims',
|
|
196
|
+
'potential_gaps',
|
|
197
|
+
'broken_anchors'
|
|
198
|
+
];
|
|
199
|
+
for (const key of requiredCounts) {
|
|
200
|
+
const value = capability.evidence[key];
|
|
201
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
202
|
+
throw new Error(`verified capability ${slug} requires non-negative integer evidence.${key}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (const key of ['unsupported_claims', 'potential_gaps', 'broken_anchors']) {
|
|
206
|
+
if (capability.evidence[key] !== 0) {
|
|
207
|
+
throw new Error(`verified capability ${slug} cannot have ${key} > 0; use needs_review instead`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return data;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function createProgress(manifest, capability = null) {
|
|
216
|
+
return {
|
|
217
|
+
schema_version: PROGRESS_SCHEMA_VERSION,
|
|
218
|
+
system: manifest.name,
|
|
219
|
+
mode: capability ? 'targeted' : 'full',
|
|
220
|
+
target: capability || null,
|
|
221
|
+
started_at: nowIso(),
|
|
222
|
+
updated_at: nowIso(),
|
|
223
|
+
status: 'pending',
|
|
224
|
+
capabilities: {}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function readProgress(workspaceRoot, manifest, capability = null) {
|
|
229
|
+
const filePath = progressPath(workspaceRoot, manifest, capability);
|
|
230
|
+
const data = readJsonFile(filePath, `${manifest.spec_root}/${META_DIR}/${progressFileName(capability)}`);
|
|
231
|
+
if (!data) return null;
|
|
232
|
+
if (data.schema_version !== PROGRESS_SCHEMA_VERSION) {
|
|
233
|
+
throw new Error(`Unsupported reverse progress schema_version: ${data.schema_version}`);
|
|
234
|
+
}
|
|
235
|
+
return data;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function ensureProgress(workspaceRoot, manifest, capability = null, options = {}) {
|
|
239
|
+
ensureMetaRoot(workspaceRoot, manifest);
|
|
240
|
+
const filePath = progressPath(workspaceRoot, manifest, capability);
|
|
241
|
+
const existing = readProgress(workspaceRoot, manifest, capability);
|
|
242
|
+
if (existing && !options.reset) {
|
|
243
|
+
const sameMode = existing.mode === (capability ? 'targeted' : 'full');
|
|
244
|
+
const sameTarget = (existing.target || null) === (capability || null);
|
|
245
|
+
if (sameMode && sameTarget && existing.status !== 'completed') {
|
|
246
|
+
return { path: filePath, progress: existing, created: false, resumed: true };
|
|
247
|
+
}
|
|
248
|
+
if (sameMode && sameTarget && existing.status === 'completed') {
|
|
249
|
+
return { path: filePath, progress: existing, created: false, resumed: false, completed: true };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const progress = createProgress(manifest, capability);
|
|
253
|
+
fs.writeFileSync(filePath, JSON.stringify(progress, null, 2) + '\n', 'utf8');
|
|
254
|
+
return { path: filePath, progress, created: true, resumed: false };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function pathMatches(changedFile, sourcePath) {
|
|
258
|
+
const changed = portable(changedFile).replace(/^\.\//, '');
|
|
259
|
+
const source = portable(sourcePath).replace(/^\.\//, '').replace(/\/$/, '');
|
|
260
|
+
return changed === source || changed.startsWith(source + '/') || source.startsWith(changed + '/');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function changedFilesSince(repoRoot, baseCommit, options = {}) {
|
|
264
|
+
const head = getRepoHead(repoRoot, options);
|
|
265
|
+
if (!head.ok) return { ok: false, error: head.error };
|
|
266
|
+
|
|
267
|
+
const ancestry = runGit(repoRoot, ['cat-file', '-e', `${baseCommit}^{commit}`], options);
|
|
268
|
+
if (!ancestry.ok) {
|
|
269
|
+
return { ok: false, commit: head.commit, error: `verified commit is unavailable locally: ${baseCommit}` };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const changed = new Set();
|
|
273
|
+
if (head.commit !== baseCommit) {
|
|
274
|
+
const diff = runGit(repoRoot, ['diff', '--name-only', '--no-renames', `${baseCommit}..${head.commit}`], options);
|
|
275
|
+
if (!diff.ok) return { ok: false, commit: head.commit, error: diff.error };
|
|
276
|
+
for (const file of diff.stdout ? diff.stdout.split(/\r?\n/).filter(Boolean) : []) changed.add(portable(file));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (const args of [
|
|
280
|
+
['diff', '--name-only', '--no-renames', 'HEAD'],
|
|
281
|
+
['diff', '--name-only', '--no-renames', '--cached'],
|
|
282
|
+
['ls-files', '--others', '--exclude-standard']
|
|
283
|
+
]) {
|
|
284
|
+
const result = runGit(repoRoot, args, options);
|
|
285
|
+
if (!result.ok) return { ok: false, commit: head.commit, error: result.error };
|
|
286
|
+
for (const file of result.stdout ? result.stdout.split(/\r?\n/).filter(Boolean) : []) changed.add(portable(file));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return { ok: true, commit: head.commit, changed: [...changed].sort() };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function checkSpecDrift(workspaceRoot, manifest, verification, options = {}) {
|
|
293
|
+
validateVerification(manifest, verification);
|
|
294
|
+
const repoStates = {};
|
|
295
|
+
for (const repo of manifest.repos) {
|
|
296
|
+
const verified = verification.repositories[repo.name];
|
|
297
|
+
const repoRoot = path.resolve(workspaceRoot, repo.path);
|
|
298
|
+
const head = getRepoHead(repoRoot, options);
|
|
299
|
+
repoStates[repo.name] = head.ok
|
|
300
|
+
? { ok: true, path: repo.path, verified_commit: verified.commit, commit: head.commit }
|
|
301
|
+
: { ok: false, path: repo.path, verified_commit: verified.commit, error: head.error };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const capabilities = {};
|
|
305
|
+
const deltaCache = new Map();
|
|
306
|
+
for (const [slug, capability] of Object.entries(verification.capabilities)) {
|
|
307
|
+
if (capability.status !== 'verified') {
|
|
308
|
+
capabilities[slug] = {
|
|
309
|
+
status: capability.status,
|
|
310
|
+
stale: true,
|
|
311
|
+
reason: `verification status is ${capability.status}`,
|
|
312
|
+
changed_files: {}
|
|
313
|
+
};
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let stale = false;
|
|
318
|
+
let unknown = false;
|
|
319
|
+
const changedFiles = {};
|
|
320
|
+
const reasons = [];
|
|
321
|
+
|
|
322
|
+
for (const repo of manifest.repos) {
|
|
323
|
+
const repoState = repoStates[repo.name];
|
|
324
|
+
const sourcePaths = normalizeSourcePaths((capability.source_paths || {})[repo.name]);
|
|
325
|
+
if (sourcePaths.length === 0) continue;
|
|
326
|
+
|
|
327
|
+
if (!repoState || !repoState.ok) {
|
|
328
|
+
unknown = true;
|
|
329
|
+
reasons.push(`${repo.name}: ${repoState ? repoState.error : 'missing repository state'}`);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const baseline = (capability.verified_commits || {})[repo.name]
|
|
334
|
+
|| verification.repositories[repo.name].commit;
|
|
335
|
+
const repoRoot = path.resolve(workspaceRoot, repo.path);
|
|
336
|
+
const cacheKey = `${repo.name}:${baseline}`;
|
|
337
|
+
let delta = deltaCache.get(cacheKey);
|
|
338
|
+
if (!delta) {
|
|
339
|
+
delta = changedFilesSince(repoRoot, baseline, options);
|
|
340
|
+
deltaCache.set(cacheKey, delta);
|
|
341
|
+
}
|
|
342
|
+
if (!delta.ok) {
|
|
343
|
+
unknown = true;
|
|
344
|
+
reasons.push(`${repo.name}: ${delta.error}`);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const relevant = delta.changed.filter(file => sourcePaths.some(source => pathMatches(file, source)));
|
|
349
|
+
if (relevant.length > 0) {
|
|
350
|
+
stale = true;
|
|
351
|
+
changedFiles[repo.name] = relevant;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
capabilities[slug] = {
|
|
356
|
+
status: unknown ? 'unknown' : (stale ? 'stale' : 'current'),
|
|
357
|
+
stale: stale || unknown,
|
|
358
|
+
reason: reasons.join('; ') || null,
|
|
359
|
+
changed_files: changedFiles
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
checked_at: nowIso(),
|
|
365
|
+
repositories: repoStates,
|
|
366
|
+
capabilities,
|
|
367
|
+
stale_count: Object.values(capabilities).filter(item => item.stale).length
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function formatDriftReport(report) {
|
|
372
|
+
const lines = ['System spec drift check', ''];
|
|
373
|
+
for (const [slug, result] of Object.entries(report.capabilities)) {
|
|
374
|
+
const marker = result.status === 'current' ? 'OK' : result.status.toUpperCase();
|
|
375
|
+
lines.push(`${marker} ${slug}`);
|
|
376
|
+
if (result.reason) lines.push(` ${result.reason}`);
|
|
377
|
+
for (const [repoName, files] of Object.entries(result.changed_files || {})) {
|
|
378
|
+
lines.push(` ${repoName}: ${files.length} relevant changed file(s)`);
|
|
379
|
+
for (const file of files.slice(0, 10)) lines.push(` - ${file}`);
|
|
380
|
+
if (files.length > 10) lines.push(` - ... ${files.length - 10} more`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
lines.push('');
|
|
384
|
+
lines.push(`Potentially stale/unknown capabilities: ${report.stale_count}`);
|
|
385
|
+
return lines.join('\n');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = {
|
|
389
|
+
META_DIR,
|
|
390
|
+
VERIFICATION_FILE,
|
|
391
|
+
REVIEW_FILE,
|
|
392
|
+
PROGRESS_FILE,
|
|
393
|
+
VERIFICATION_SCHEMA_VERSION,
|
|
394
|
+
PROGRESS_SCHEMA_VERSION,
|
|
395
|
+
metaRoot,
|
|
396
|
+
verificationPath,
|
|
397
|
+
reviewPath,
|
|
398
|
+
progressSlug,
|
|
399
|
+
progressFileName,
|
|
400
|
+
progressPath,
|
|
401
|
+
ensureMetaRoot,
|
|
402
|
+
getRepoHead,
|
|
403
|
+
captureRepoHeads,
|
|
404
|
+
readVerification,
|
|
405
|
+
validateVerification,
|
|
406
|
+
createProgress,
|
|
407
|
+
readProgress,
|
|
408
|
+
ensureProgress,
|
|
409
|
+
normalizeSourcePath,
|
|
410
|
+
normalizeSourcePaths,
|
|
411
|
+
pathMatches,
|
|
412
|
+
changedFilesSince,
|
|
413
|
+
checkSpecDrift,
|
|
414
|
+
formatDriftReport
|
|
415
|
+
};
|