dotmd-cli 0.65.1 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/doctor.mjs +8 -0
- package/src/index.mjs +4 -0
- package/src/skill-drift.mjs +85 -0
package/package.json
CHANGED
package/src/doctor.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { renderIndexFile, writeIndex } from './index-file.mjs';
|
|
|
8
8
|
import { renderCheck, renderManualFixes } from './render.mjs';
|
|
9
9
|
import { bold, dim, green, yellow } from './color.mjs';
|
|
10
10
|
import { checkClaudeCommands, removeGeneratedSlashCommands } from './claude-commands.mjs';
|
|
11
|
+
import { checkSkillDrift } from './skill-drift.mjs';
|
|
11
12
|
import { runMigrateTemplate } from './migrate-template.mjs';
|
|
12
13
|
import { runMigratePrompts } from './migrate-prompts.mjs';
|
|
13
14
|
import { runFrontmatterFix } from './frontmatter-fix.mjs';
|
|
@@ -191,6 +192,7 @@ function runDoctorProject(config, { json = false } = {}) {
|
|
|
191
192
|
const claudeCommandWarnings = checkClaudeCommands(config.repoRoot);
|
|
192
193
|
const deprecatedCommandMentions = findDeprecatedCommandMentions(config);
|
|
193
194
|
const { docsWithoutFrontmatter, planStatusGaps } = findWorkflowDrift(config);
|
|
195
|
+
const skillDriftWarnings = checkSkillDrift(config);
|
|
194
196
|
const result = {
|
|
195
197
|
cliVersion: cliPackage?.version ?? null,
|
|
196
198
|
packageDependency: depVersion,
|
|
@@ -198,6 +200,7 @@ function runDoctorProject(config, { json = false } = {}) {
|
|
|
198
200
|
deprecatedCommandMentions,
|
|
199
201
|
docsWithoutFrontmatter,
|
|
200
202
|
planStatusGaps,
|
|
203
|
+
skillDriftWarnings,
|
|
201
204
|
};
|
|
202
205
|
|
|
203
206
|
if (json) {
|
|
@@ -226,6 +229,11 @@ function runDoctorProject(config, { json = false } = {}) {
|
|
|
226
229
|
} else {
|
|
227
230
|
process.stdout.write('- plan status vocab: ok\n');
|
|
228
231
|
}
|
|
232
|
+
if (skillDriftWarnings.length) {
|
|
233
|
+
process.stdout.write(yellow(`- canonical workflow block: drifted — CLAUDE.md and the plugin SKILL.md teach different workflows. Reconcile the block between the \`dotmd:canonical-workflow\` markers in both files.`) + '\n');
|
|
234
|
+
} else {
|
|
235
|
+
process.stdout.write('- canonical workflow block: in sync\n');
|
|
236
|
+
}
|
|
229
237
|
return result;
|
|
230
238
|
}
|
|
231
239
|
|
package/src/index.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { validateDoc, validatePlanShape, validateDocShape, checkBidirectionalRef
|
|
|
7
7
|
import { checkIndex } from './index-file.mjs';
|
|
8
8
|
import { checkClaudeCommands } from './claude-commands.mjs';
|
|
9
9
|
import { checkGlossaryConfig } from './glossary-check.mjs';
|
|
10
|
+
import { checkSkillDrift } from './skill-drift.mjs';
|
|
10
11
|
|
|
11
12
|
// `fast: true` skips every pass that produces warnings/errors — the rendered
|
|
12
13
|
// index file consumes only status/title/snapshot/etc., not the validation
|
|
@@ -135,6 +136,9 @@ export function buildIndex(config, opts = {}) {
|
|
|
135
136
|
|
|
136
137
|
const glossaryWarnings = checkGlossaryConfig(config);
|
|
137
138
|
warnings.push(...glossaryWarnings);
|
|
139
|
+
|
|
140
|
+
const skillDriftWarnings = checkSkillDrift(config);
|
|
141
|
+
warnings.push(...skillDriftWarnings);
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
return {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// dotmd is a drift- and staleness-catcher that didn't, until now, catch drift
|
|
5
|
+
// in its OWN plugin surface. Two canonical workflow-doc surfaces are kept in
|
|
6
|
+
// lockstep by hand: the repo's CLAUDE.md (its own instructions) and the plugin
|
|
7
|
+
// SKILL.md (what every OTHER repo's session learns the workflow from). CLAUDE.md
|
|
8
|
+
// literally says "keep it in sync with the guidance below" — this guard makes
|
|
9
|
+
// that lockstep mechanical instead of manual.
|
|
10
|
+
//
|
|
11
|
+
// The comparison unit is a marked block (not a fuzzy phrase heuristic):
|
|
12
|
+
// deterministic, robust, and zero false positives. Each surface wraps the
|
|
13
|
+
// irreducible workflow contract between the markers below; the guard extracts
|
|
14
|
+
// both and compares them whitespace-tolerantly. Editing the contract in one
|
|
15
|
+
// surface and forgetting the other is exactly what this catches.
|
|
16
|
+
|
|
17
|
+
export const CANONICAL_MARKERS = {
|
|
18
|
+
start: '<!-- dotmd:canonical-workflow:start -->',
|
|
19
|
+
end: '<!-- dotmd:canonical-workflow:end -->',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const CLAUDE_MD = 'CLAUDE.md';
|
|
23
|
+
const SKILL_MD = path.join('plugins', 'dotmd', 'skills', 'dotmd', 'SKILL.md');
|
|
24
|
+
|
|
25
|
+
// Pull the inner text between the canonical markers. Returns null when the
|
|
26
|
+
// markers are absent or malformed — callers treat null as "this surface doesn't
|
|
27
|
+
// participate in the lockstep block", which is what keeps a user repo that has
|
|
28
|
+
// its own CLAUDE.md (but never adopted the block) from ever tripping the guard.
|
|
29
|
+
export function extractCanonicalBlock(text) {
|
|
30
|
+
if (typeof text !== 'string') return null;
|
|
31
|
+
const startIdx = text.indexOf(CANONICAL_MARKERS.start);
|
|
32
|
+
if (startIdx === -1) return null;
|
|
33
|
+
const afterStart = startIdx + CANONICAL_MARKERS.start.length;
|
|
34
|
+
const endIdx = text.indexOf(CANONICAL_MARKERS.end, afterStart);
|
|
35
|
+
if (endIdx === -1 || endIdx < afterStart) return null;
|
|
36
|
+
return text.slice(afterStart, endIdx);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Whitespace-tolerant so a CRLF vs LF or a stray trailing space between the two
|
|
40
|
+
// files isn't reported as drift — only meaningful content divergence is.
|
|
41
|
+
function normalizeBlock(block) {
|
|
42
|
+
return block
|
|
43
|
+
.replace(/\r\n?/g, '\n')
|
|
44
|
+
.split('\n')
|
|
45
|
+
.map(line => line.replace(/\s+$/, ''))
|
|
46
|
+
.join('\n')
|
|
47
|
+
.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readIfPresent(filePath) {
|
|
51
|
+
try {
|
|
52
|
+
if (!existsSync(filePath)) return null;
|
|
53
|
+
return readFileSync(filePath, 'utf8');
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Guard: the canonical workflow block must stay identical across CLAUDE.md and
|
|
60
|
+
// the plugin SKILL.md. Returns [] unless BOTH files exist AND BOTH carry the
|
|
61
|
+
// block — so it only fires in a repo that has adopted the lockstep convention
|
|
62
|
+
// (i.e. the dotmd repo itself), never in a user repo that merely has its own
|
|
63
|
+
// CLAUDE.md. That strict gate is the price of zero false positives; the
|
|
64
|
+
// trade-off is that deleting the markers from one surface silently disables the
|
|
65
|
+
// guard rather than warning (a deliberate, visible act, unlike a content edit).
|
|
66
|
+
export function checkSkillDrift(config) {
|
|
67
|
+
const repoRoot = config?.repoRoot;
|
|
68
|
+
if (!repoRoot) return [];
|
|
69
|
+
|
|
70
|
+
const claudeText = readIfPresent(path.join(repoRoot, CLAUDE_MD));
|
|
71
|
+
const skillText = readIfPresent(path.join(repoRoot, SKILL_MD));
|
|
72
|
+
if (claudeText === null || skillText === null) return [];
|
|
73
|
+
|
|
74
|
+
const claudeBlock = extractCanonicalBlock(claudeText);
|
|
75
|
+
const skillBlock = extractCanonicalBlock(skillText);
|
|
76
|
+
if (claudeBlock === null || skillBlock === null) return [];
|
|
77
|
+
|
|
78
|
+
if (normalizeBlock(claudeBlock) === normalizeBlock(skillBlock)) return [];
|
|
79
|
+
|
|
80
|
+
return [{
|
|
81
|
+
path: SKILL_MD,
|
|
82
|
+
level: 'warning',
|
|
83
|
+
message: `canonical workflow block drifted from \`${CLAUDE_MD}\`. The block between \`${CANONICAL_MARKERS.start}\` and \`${CANONICAL_MARKERS.end}\` must stay identical in both files — reconcile them so the plugin skill and the repo instructions teach the same workflow.`,
|
|
84
|
+
}];
|
|
85
|
+
}
|