docguard-cli 0.14.0 → 0.15.1
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/cli/commands/guard.mjs +1 -1
- package/cli/commands/init.mjs +4 -0
- package/cli/scanners/memory-plan.mjs +46 -1
- package/cli/scanners/schemas.mjs +37 -12
- package/cli/validators/cross-reference.mjs +43 -5
- package/cli/validators/drift.mjs +26 -11
- package/cli/validators/metrics-consistency.mjs +28 -4
- package/cli/validators/todo-tracking.mjs +52 -41
- package/cli/writers/mechanical.mjs +33 -0
- package/extensions/spec-kit-docguard/extension.yml +1 -1
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +1 -1
- package/package.json +2 -1
- package/schemas/docguard-config.schema.json +155 -0
package/cli/commands/guard.mjs
CHANGED
|
@@ -198,7 +198,7 @@ export function runGuardInternal(projectDir, config) {
|
|
|
198
198
|
* Freshness (git log), Traceability (REQ scan), Doc-Quality (prose lint) —
|
|
199
199
|
* stay off for speed.
|
|
200
200
|
*/
|
|
201
|
-
export const CHANGED_ONLY_VALIDATORS = ['docsSync', 'environment', 'apiSurface'];
|
|
201
|
+
export const CHANGED_ONLY_VALIDATORS = ['docsSync', 'environment', 'apiSurface', 'drift', 'todoTracking'];
|
|
202
202
|
|
|
203
203
|
/**
|
|
204
204
|
* Build a validators map that enables only the pre-commit-lite set.
|
package/cli/commands/init.mjs
CHANGED
|
@@ -167,6 +167,10 @@ export async function runInit(projectDir, config, flags) {
|
|
|
167
167
|
const ptc = typeDefaults[detectedType] || typeDefaults.unknown;
|
|
168
168
|
|
|
169
169
|
const defaultConfig = {
|
|
170
|
+
// v0.15-P4: $schema reference enables VS Code / IDE autocomplete +
|
|
171
|
+
// validation for .docguard.json fields. Picked up by any
|
|
172
|
+
// JSON-Schema-aware editor; ignored by DocGuard itself.
|
|
173
|
+
$schema: 'https://raccioly.github.io/docguard/schemas/docguard-config.schema.json',
|
|
170
174
|
projectName: config.projectName,
|
|
171
175
|
version: '0.5',
|
|
172
176
|
profile: profileName,
|
|
@@ -29,13 +29,58 @@ const md = {
|
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* v0.15-P1: in-process cache. buildMemoryPlan is expensive (~400ms on
|
|
34
|
+
* wu-whatsappinbox, 33% of total guard validator time) because it triggers
|
|
35
|
+
* routes/schemas/screens/frontend scanners — all of which walk the source
|
|
36
|
+
* tree. Within a single guard run, sync, generate, and the Generated-
|
|
37
|
+
* Staleness validator all ask for the SAME plan; without caching they each
|
|
38
|
+
* re-pay the cost.
|
|
39
|
+
*
|
|
40
|
+
* Cache key: projectDir + a config fingerprint that captures the fields the
|
|
41
|
+
* scanners actually consume (sourceRoot, ignore, projectType). Other config
|
|
42
|
+
* mutations (e.g. changedFiles per-validator) don't invalidate the plan.
|
|
43
|
+
*
|
|
44
|
+
* Bypass with `_skipCache: true` in opts — used by tests and any caller that
|
|
45
|
+
* wants a fresh scan.
|
|
46
|
+
*/
|
|
47
|
+
const _memoryPlanCache = new Map(); // key → plan
|
|
48
|
+
|
|
49
|
+
export function clearMemoryPlanCache() {
|
|
50
|
+
_memoryPlanCache.clear();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function _cacheKey(projectDir, config) {
|
|
54
|
+
return JSON.stringify({
|
|
55
|
+
dir: projectDir,
|
|
56
|
+
sourceRoot: config.sourceRoot,
|
|
57
|
+
ignore: Array.isArray(config.ignore) ? [...config.ignore].sort() : null,
|
|
58
|
+
projectType: config.projectType,
|
|
59
|
+
profile: config.profile,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
32
63
|
/**
|
|
33
64
|
* Build the full memory plan for a project.
|
|
34
65
|
* @returns {{ profile, surface, docs, agentTasks }}
|
|
35
66
|
* docs[].sections[]: { id, source:'code', body } OR { id, source:'human', task, grounding }
|
|
36
67
|
* agentTasks: flattened prose tasks the AI must write.
|
|
37
68
|
*/
|
|
38
|
-
export function buildMemoryPlan(projectDir, config = {}) {
|
|
69
|
+
export function buildMemoryPlan(projectDir, config = {}, opts = {}) {
|
|
70
|
+
if (!opts._skipCache) {
|
|
71
|
+
const key = _cacheKey(projectDir, config);
|
|
72
|
+
const cached = _memoryPlanCache.get(key);
|
|
73
|
+
if (cached) return cached;
|
|
74
|
+
}
|
|
75
|
+
const result = _buildMemoryPlanUncached(projectDir, config);
|
|
76
|
+
if (!opts._skipCache) {
|
|
77
|
+
_memoryPlanCache.set(_cacheKey(projectDir, config), result);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Original implementation, renamed so the public buildMemoryPlan can wrap it.
|
|
83
|
+
function _buildMemoryPlanUncached(projectDir, config = {}) {
|
|
39
84
|
const profile = detectProjectProfile(projectDir, config);
|
|
40
85
|
const primaryFramework = profile.primary?.framework || profile.frameworks[0] || '';
|
|
41
86
|
|
package/cli/scanners/schemas.mjs
CHANGED
|
@@ -690,20 +690,45 @@ function readFileSafe(path) {
|
|
|
690
690
|
try { return readFileSync(path, 'utf-8'); } catch { return null; }
|
|
691
691
|
}
|
|
692
692
|
|
|
693
|
+
// v0.15-P2: walkDir is called 8 times across schemas.mjs (Pydantic, Mongoose,
|
|
694
|
+
// Prisma, SQLAlchemy, Sequelize, GORM, Sqlx, Hibernate). Each call walks the
|
|
695
|
+
// same tree. Cache the file list per (dir, extension-set) so subsequent
|
|
696
|
+
// callers iterate an array instead of re-traversing.
|
|
697
|
+
//
|
|
698
|
+
// Cache key: just the dir path. The extension filter is constant across all
|
|
699
|
+
// callers (the regex hard-coded below), so a single cache slot per dir works.
|
|
700
|
+
// Lifetime: per-process. `clearWalkDirCache()` invalidates for tests.
|
|
701
|
+
const _walkDirCache = new Map(); // dir → string[] of file paths
|
|
702
|
+
|
|
703
|
+
export function clearWalkDirCache() {
|
|
704
|
+
_walkDirCache.clear();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const _CODE_FILE_RE = /\.(js|mjs|cjs|ts|tsx|jsx|py|rs|go|java|kt|rb)$/;
|
|
708
|
+
|
|
709
|
+
function _collectFiles(dir, out) {
|
|
710
|
+
let entries;
|
|
711
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
712
|
+
for (const entry of entries) {
|
|
713
|
+
if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
714
|
+
const fullPath = join(dir, entry.name);
|
|
715
|
+
if (entry.isDirectory()) {
|
|
716
|
+
_collectFiles(fullPath, out);
|
|
717
|
+
} else if (entry.isFile() && _CODE_FILE_RE.test(entry.name)) {
|
|
718
|
+
out.push(fullPath);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
693
723
|
function walkDir(dir, callback) {
|
|
694
724
|
if (!existsSync(dir)) return;
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
} else if (entry.isFile() && /\.(js|mjs|cjs|ts|tsx|jsx|py|rs|go|java|kt|rb)$/.test(entry.name)) {
|
|
703
|
-
callback(fullPath);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
} catch { /* skip */ }
|
|
725
|
+
let files = _walkDirCache.get(dir);
|
|
726
|
+
if (!files) {
|
|
727
|
+
files = [];
|
|
728
|
+
_collectFiles(dir, files);
|
|
729
|
+
_walkDirCache.set(dir, files);
|
|
730
|
+
}
|
|
731
|
+
for (const f of files) callback(f);
|
|
707
732
|
}
|
|
708
733
|
|
|
709
734
|
/**
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
30
|
-
import { resolve, join, dirname, basename } from 'node:path';
|
|
30
|
+
import { resolve, join, dirname, basename, relative } from 'node:path';
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Slugify a heading the way GitHub's markdown anchors work.
|
|
@@ -294,6 +294,7 @@ function collectCanonicalDocs(projectDir) {
|
|
|
294
294
|
export function validateCrossReferences(projectDir, _config = {}) {
|
|
295
295
|
const errors = [];
|
|
296
296
|
const warnings = [];
|
|
297
|
+
const fixes = [];
|
|
297
298
|
let passed = 0;
|
|
298
299
|
let total = 0;
|
|
299
300
|
|
|
@@ -364,13 +365,27 @@ export function validateCrossReferences(projectDir, _config = {}) {
|
|
|
364
365
|
if (!matches) {
|
|
365
366
|
const where = targetPath === docPath ? 'same doc' : basename(targetPath);
|
|
366
367
|
// S-12: suggest the closest matching anchor when there's a near-miss.
|
|
367
|
-
// Three of five wu user-fixes were "heading renamed, link not updated"
|
|
368
|
-
// — a suggested-slug hint makes those deterministic-fixable.
|
|
369
368
|
const suggestion = anchors ? suggestAnchor(normalizedAnchor, anchors) : null;
|
|
370
369
|
const hint = suggestion ? ` (did you mean #${suggestion}?)` : '';
|
|
370
|
+
// v0.14.1-S12+: when the suggestion is HIGH-CONFIDENCE — unambiguous
|
|
371
|
+
// and very close (edit distance <= 2) — emit a mechanical fix so
|
|
372
|
+
// `docguard fix --write` resolves it without AI. Other near-misses
|
|
373
|
+
// still get the hint but no fix (the user needs to verify intent).
|
|
374
|
+
const isHighConfidence = suggestion && isUnambiguousSuggestion(normalizedAnchor, suggestion, anchors);
|
|
371
375
|
warnings.push(
|
|
372
|
-
`${docName}:${ref.line} — broken anchor: "#${ref.anchor}" in ${where} doesn't match any heading${hint}`
|
|
376
|
+
`${docName}:${ref.line} — broken anchor: "#${ref.anchor}" in ${where} doesn't match any heading${hint}` +
|
|
377
|
+
(isHighConfidence ? ' [auto-fixable]' : '')
|
|
373
378
|
);
|
|
379
|
+
if (isHighConfidence) {
|
|
380
|
+
fixes.push({
|
|
381
|
+
type: 'replace-anchor',
|
|
382
|
+
doc: relative(projectDir, docPath),
|
|
383
|
+
line: ref.line,
|
|
384
|
+
from: ref.anchor,
|
|
385
|
+
to: suggestion,
|
|
386
|
+
summary: `${docName}:${ref.line} #${ref.anchor} → #${suggestion}`,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
374
389
|
continue;
|
|
375
390
|
}
|
|
376
391
|
}
|
|
@@ -379,5 +394,28 @@ export function validateCrossReferences(projectDir, _config = {}) {
|
|
|
379
394
|
}
|
|
380
395
|
}
|
|
381
396
|
|
|
382
|
-
return { errors, warnings, passed, total };
|
|
397
|
+
return { errors, warnings, passed, total, fixes };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* v0.14.1-S12+ — is the suggested anchor an unambiguous, high-confidence
|
|
402
|
+
* match? Two criteria, both must hold:
|
|
403
|
+
* 1. Edit distance between the broken anchor and the suggestion is <= 2
|
|
404
|
+
* (catches typos and minor renames, refuses major slug rewrites).
|
|
405
|
+
* 2. The suggestion is the ONLY anchor that close — no other anchor
|
|
406
|
+
* within distance <= 2. Avoids fixing to the wrong candidate when
|
|
407
|
+
* multiple are similar.
|
|
408
|
+
*/
|
|
409
|
+
function isUnambiguousSuggestion(broken, suggestion, anchors) {
|
|
410
|
+
if (!broken || !suggestion || !anchors) return false;
|
|
411
|
+
const sugDist = editDistance(broken, suggestion);
|
|
412
|
+
if (sugDist > 2) return false;
|
|
413
|
+
// Count other anchors that are also within the same tight budget.
|
|
414
|
+
let closeCandidates = 0;
|
|
415
|
+
for (const a of anchors) {
|
|
416
|
+
if (a === suggestion) continue;
|
|
417
|
+
if (editDistance(broken, a) <= 2) closeCandidates++;
|
|
418
|
+
if (closeCandidates > 0) return false; // ambiguous
|
|
419
|
+
}
|
|
420
|
+
return true;
|
|
383
421
|
}
|
package/cli/validators/drift.mjs
CHANGED
|
@@ -20,21 +20,27 @@ const IGNORE_DIRS = new Set([
|
|
|
20
20
|
export function validateDrift(projectDir, config) {
|
|
21
21
|
const results = { name: 'drift', errors: [], warnings: [], passed: 0, total: 0 };
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// v0.15-P3: when config.changedFiles is set (--changed-only mode), only
|
|
24
|
+
// visit the listed paths. Drift comments in unchanged files are still in
|
|
25
|
+
// git so they'll be caught by a full guard run; pre-commit hooks care
|
|
26
|
+
// about NEW drift comments in this commit.
|
|
27
|
+
const scanFile = (filePath) => {
|
|
26
28
|
const ext = extname(filePath);
|
|
27
29
|
if (!CODE_EXTENSIONS.has(ext)) return;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
30
|
+
// v0.15 hotfix: test files commonly contain literal `// DRIFT:` inside
|
|
31
|
+
// string fixtures (e.g. `'// DRIFT: a-drift\n'`). Reading the test as
|
|
32
|
+
// source would treat the string as a real drift comment. Skip test
|
|
33
|
+
// files unless the user opts in — same pattern TODO-Tracking uses.
|
|
34
|
+
const rel = filePath.replace(projectDir + '/', '');
|
|
35
|
+
const includeTests = config?.drift?.includeTestFiles === true;
|
|
36
|
+
if (!includeTests && /(^|\/)(__tests__|tests?|spec)\/|\.(test|spec)\.[^.]+$/.test(rel)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let content;
|
|
40
|
+
try { content = readFileSync(filePath, 'utf-8'); } catch { return; }
|
|
32
41
|
if (!content.includes('DRIFT:')) return;
|
|
33
|
-
|
|
34
42
|
const lines = content.split('\n');
|
|
35
|
-
|
|
36
43
|
lines.forEach((line, i) => {
|
|
37
|
-
// Match various comment styles: // DRIFT:, # DRIFT:, /* DRIFT:, -- DRIFT:
|
|
38
44
|
const match = line.match(/(?:\/\/|#|\/\*|\-\-)\s*DRIFT:\s*(.+)/i);
|
|
39
45
|
if (match) {
|
|
40
46
|
driftComments.push({
|
|
@@ -44,7 +50,16 @@ export function validateDrift(projectDir, config) {
|
|
|
44
50
|
});
|
|
45
51
|
}
|
|
46
52
|
});
|
|
47
|
-
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const driftComments = [];
|
|
56
|
+
if (Array.isArray(config.changedFiles) && config.changedFiles.length > 0) {
|
|
57
|
+
for (const rel of config.changedFiles) {
|
|
58
|
+
scanFile(resolve(projectDir, rel));
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
walkDir(projectDir, scanFile);
|
|
62
|
+
}
|
|
48
63
|
|
|
49
64
|
if (driftComments.length === 0) {
|
|
50
65
|
// No // DRIFT: comments to reconcile — not applicable (NOT a pass).
|
|
@@ -64,6 +64,14 @@ export function validateMetricsConsistency(projectDir, config, guardResults) {
|
|
|
64
64
|
{ key: 'validators', regex: /(?<!\d\/)\b(\d{2,})\s+validators?\b/gi, label: 'validators' },
|
|
65
65
|
];
|
|
66
66
|
|
|
67
|
+
// v0.14.1-N1: dedup by (file, label, found) — a file that mentions the
|
|
68
|
+
// stale number multiple times produces ONE warning, not one per occurrence.
|
|
69
|
+
// The replace-count applier already uses replace-all semantics, so a single
|
|
70
|
+
// fix per (file, label) is sufficient. Previously: "X.md" appearing 2× with
|
|
71
|
+
// the same drift would generate 2 warnings + 2 fixes (the second a no-op).
|
|
72
|
+
const reportedDrift = new Set(); // key: `${relPath}|${label}|${found}`
|
|
73
|
+
const reportedPass = new Set(); // key: `${relPath}|${label}` — only count one pass per (file, label)
|
|
74
|
+
|
|
67
75
|
for (const mdFile of mdFiles) {
|
|
68
76
|
const relPath = relative(projectDir, mdFile);
|
|
69
77
|
// Skip changelog (historical numbers are fine by definition)
|
|
@@ -79,16 +87,32 @@ export function validateMetricsConsistency(projectDir, config, guardResults) {
|
|
|
79
87
|
|
|
80
88
|
regex.lastIndex = 0;
|
|
81
89
|
let match;
|
|
90
|
+
// Collect distinct (found-value) instances within THIS file first,
|
|
91
|
+
// then emit ONE warning per distinct value. A file that says "20" on
|
|
92
|
+
// line 5 and "20" on line 50 is the same drift; "20" on line 5 and
|
|
93
|
+
// "19" on line 50 are two distinct drifts.
|
|
94
|
+
const distinctFoundInFile = new Set();
|
|
82
95
|
while ((match = regex.exec(content)) !== null) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
distinctFoundInFile.add(parseInt(match[1], 10));
|
|
97
|
+
}
|
|
98
|
+
if (distinctFoundInFile.size === 0) continue;
|
|
99
|
+
|
|
100
|
+
for (const found of distinctFoundInFile) {
|
|
101
|
+
if (found > 0 && found !== actuals[key]) {
|
|
102
|
+
const driftKey = `${relPath}|${label}|${found}`;
|
|
103
|
+
if (reportedDrift.has(driftKey)) continue;
|
|
104
|
+
reportedDrift.add(driftKey);
|
|
105
|
+
total++;
|
|
86
106
|
warnings.push(
|
|
87
107
|
`${relPath} says "${found} ${label}" but actual count is ${actuals[key]}. Fix with \`docguard fix --write\``
|
|
88
108
|
);
|
|
89
|
-
// Deterministic, surgical token replacement — safe to auto-apply.
|
|
90
109
|
fixes.push({ type: 'replace-count', file: relPath, label, found, actual: actuals[key] });
|
|
91
110
|
} else {
|
|
111
|
+
// Matches the actual count — one pass per (file, label), not per occurrence.
|
|
112
|
+
const passKey = `${relPath}|${label}`;
|
|
113
|
+
if (reportedPass.has(passKey)) continue;
|
|
114
|
+
reportedPass.add(passKey);
|
|
115
|
+
total++;
|
|
92
116
|
passed++;
|
|
93
117
|
}
|
|
94
118
|
}
|
|
@@ -329,6 +329,20 @@ function isSelfPath(fullPath) {
|
|
|
329
329
|
}
|
|
330
330
|
|
|
331
331
|
function findTodos(rootDir, dir, todos, config) {
|
|
332
|
+
// v0.15-P3: when config.changedFiles is set (--changed-only mode), only
|
|
333
|
+
// scan those paths. New TODOs in this commit get caught; pre-existing
|
|
334
|
+
// TODOs in unchanged files are still tracked by full guard runs.
|
|
335
|
+
if (dir === rootDir && Array.isArray(config?.changedFiles) && config.changedFiles.length > 0) {
|
|
336
|
+
for (const rel of config.changedFiles) {
|
|
337
|
+
const full = resolve(rootDir, rel);
|
|
338
|
+
let stat;
|
|
339
|
+
try { stat = statSync(full); } catch { continue; }
|
|
340
|
+
if (!stat.isFile()) continue;
|
|
341
|
+
_scanTodoFile(rootDir, full, todos, config);
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
332
346
|
let entries;
|
|
333
347
|
try { entries = readdirSync(dir); } catch { return; }
|
|
334
348
|
|
|
@@ -345,47 +359,44 @@ function findTodos(rootDir, dir, todos, config) {
|
|
|
345
359
|
if (stat.isDirectory()) {
|
|
346
360
|
findTodos(rootDir, full, todos, config);
|
|
347
361
|
} else {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
}
|
|
362
|
+
_scanTodoFile(rootDir, full, todos, config);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* v0.15-P3: per-file TODO scan extracted so both the full-tree walker and
|
|
369
|
+
* the --changed-only path can reuse it. Honors test-file filtering,
|
|
370
|
+
* self-path skip, ignore patterns, and the TODO regex.
|
|
371
|
+
*/
|
|
372
|
+
function _scanTodoFile(rootDir, full, todos, config) {
|
|
373
|
+
const includeTests = config?.todoTracking?.includeTestFiles === true;
|
|
374
|
+
const ext = extname(full).toLowerCase();
|
|
375
|
+
if (!SOURCE_EXTENSIONS.has(ext)) return;
|
|
376
|
+
|
|
377
|
+
const relPath = relative(rootDir, full);
|
|
378
|
+
|
|
379
|
+
if (!includeTests && isTestFilePath(relPath)) return;
|
|
380
|
+
if (isSelfPath(full)) return;
|
|
381
|
+
if (config && shouldIgnore(relPath, config, 'todoIgnore')) return;
|
|
382
|
+
|
|
383
|
+
let content;
|
|
384
|
+
try { content = readFileSync(full, 'utf-8'); } catch { return; }
|
|
385
|
+
if (!TODO_PATTERN.test(content)) return;
|
|
386
|
+
|
|
387
|
+
const lines = content.split('\n');
|
|
388
|
+
for (let i = 0; i < lines.length; i++) {
|
|
389
|
+
const commentText = commentPortion(lines[i]);
|
|
390
|
+
if (commentText === null) continue;
|
|
391
|
+
if (TODO_PATTERN.test(commentText)) {
|
|
392
|
+
const match = commentText.match(TODO_EXTRACT);
|
|
393
|
+
if (match) {
|
|
394
|
+
todos.push({
|
|
395
|
+
keyword: match[1].toUpperCase(),
|
|
396
|
+
text: match[2].trim(),
|
|
397
|
+
file: relPath,
|
|
398
|
+
line: i + 1,
|
|
399
|
+
});
|
|
389
400
|
}
|
|
390
401
|
}
|
|
391
402
|
}
|
|
@@ -143,12 +143,45 @@ function applyRegenerateSection(projectDir, fix) {
|
|
|
143
143
|
return { applied: true, detail: `${fix.doc}: regenerated § ${fix.sectionId}` };
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* v0.14.1-S12+ — replace-anchor: rewrite a broken markdown anchor with a
|
|
148
|
+
* high-confidence suggested slug. Emitted by Cross-Reference when its
|
|
149
|
+
* fuzzy match is unambiguous (edit distance <= 2, no other close candidates).
|
|
150
|
+
*
|
|
151
|
+
* fix shape: { type: 'replace-anchor', doc, from, to, line?, summary? }
|
|
152
|
+
*
|
|
153
|
+
* Bounded: only rewrites occurrences of `](#${from})` and `](#X${from})`-like
|
|
154
|
+
* forms — won't touch the broken slug if it happens to appear as plain text.
|
|
155
|
+
* Idempotent: if no occurrence is found (already fixed), no-op.
|
|
156
|
+
*/
|
|
157
|
+
function applyReplaceAnchor(projectDir, fix) {
|
|
158
|
+
if (!fix.doc || !fix.from || !fix.to) {
|
|
159
|
+
return { applied: false, skipped: 'replace-anchor needs doc, from, to' };
|
|
160
|
+
}
|
|
161
|
+
const full = resolve(projectDir, fix.doc);
|
|
162
|
+
if (!existsSync(full)) return { applied: false, skipped: `doc not found: ${fix.doc}` };
|
|
163
|
+
const content = readFileSync(full, 'utf-8');
|
|
164
|
+
|
|
165
|
+
// Match an anchor inside a markdown link: `](#from)` OR `](path#from)`.
|
|
166
|
+
// Use a regex that captures the prefix and suffix so we only touch the
|
|
167
|
+
// anchor part — leaving the link text and path intact.
|
|
168
|
+
const fromEsc = esc(fix.from);
|
|
169
|
+
const re = new RegExp(`(\\]\\([^)]*#)${fromEsc}([)\\s])`, 'g');
|
|
170
|
+
const next = content.replace(re, `$1${fix.to}$2`);
|
|
171
|
+
if (next === content) {
|
|
172
|
+
return { applied: false, skipped: `${fix.doc}: anchor #${fix.from} not found (already fixed?)` };
|
|
173
|
+
}
|
|
174
|
+
writeFileSync(full, next, 'utf-8');
|
|
175
|
+
return { applied: true, detail: `${fix.doc}: #${fix.from} → #${fix.to}` };
|
|
176
|
+
}
|
|
177
|
+
|
|
146
178
|
const APPLIERS = {
|
|
147
179
|
'replace-count': applyReplaceCount,
|
|
148
180
|
'replace-version': applyReplaceVersion,
|
|
149
181
|
'insert-changelog-unreleased': applyInsertChangelogUnreleased,
|
|
150
182
|
'remove-endpoint': applyRemoveEndpoint,
|
|
151
183
|
'regenerate-section': applyRegenerateSection,
|
|
184
|
+
'replace-anchor': applyReplaceAnchor,
|
|
152
185
|
};
|
|
153
186
|
|
|
154
187
|
export const MECHANICAL_FIX_TYPES = Object.keys(APPLIERS);
|
|
@@ -3,7 +3,7 @@ schema_version: "1.0"
|
|
|
3
3
|
extension:
|
|
4
4
|
id: "docguard"
|
|
5
5
|
name: "DocGuard — CDD Enforcement"
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.15.1"
|
|
7
7
|
description: "Canonical-Driven Development enforcement as a true spec-kit extension. LLM-first design with 19 automated validators, 4 AI behavior skills, spec-kit skill chaining, and workflow hooks. Zero NPM runtime dependencies."
|
|
8
8
|
author: "Ricardo Accioly"
|
|
9
9
|
repository: "https://github.com/raccioly/docguard"
|
|
@@ -6,10 +6,10 @@ description: AI-driven documentation repair with structured research workflow, t
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.
|
|
9
|
+
version: 0.15.1
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-fix
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.15.1 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Fix Skill
|
|
15
15
|
|
|
@@ -7,10 +7,10 @@ description: Run DocGuard guard validation against Canonical-Driven Development
|
|
|
7
7
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
8
8
|
metadata:
|
|
9
9
|
author: docguard
|
|
10
|
-
version: 0.
|
|
10
|
+
version: 0.15.1
|
|
11
11
|
source: extensions/spec-kit-docguard/skills/docguard-guard
|
|
12
12
|
---
|
|
13
|
-
<!-- docguard:version: 0.
|
|
13
|
+
<!-- docguard:version: 0.15.1 -->
|
|
14
14
|
|
|
15
15
|
# DocGuard Guard Skill
|
|
16
16
|
|
|
@@ -6,10 +6,10 @@ description: Cross-document consistency analysis and quality assessment. Perform
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.
|
|
9
|
+
version: 0.15.1
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-review
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.15.1 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Review Skill
|
|
15
15
|
|
|
@@ -6,10 +6,10 @@ description: CDD maturity assessment with category-aware improvement roadmap. Ru
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.
|
|
9
|
+
version: 0.15.1
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-score
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.15.1 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Score Skill
|
|
15
15
|
|
|
@@ -4,7 +4,7 @@ description: Keep canonical documentation ALWAYS UP TO DATE. Refreshes code-trut
|
|
|
4
4
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
5
5
|
metadata:
|
|
6
6
|
author: docguard
|
|
7
|
-
version: 0.
|
|
7
|
+
version: 0.15.1
|
|
8
8
|
source: extensions/spec-kit-docguard/skills/docguard-sync
|
|
9
9
|
---
|
|
10
10
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docguard-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "The enforcement tool for Canonical-Driven Development (CDD). Audit, generate, and guard your project documentation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"commands/",
|
|
52
52
|
"extensions/",
|
|
53
53
|
"docs/",
|
|
54
|
+
"schemas/",
|
|
54
55
|
"STANDARD.md",
|
|
55
56
|
"PHILOSOPHY.md",
|
|
56
57
|
"README.md",
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://raccioly.github.io/docguard/schemas/docguard-config.schema.json",
|
|
4
|
+
"title": "DocGuard project config (.docguard.json)",
|
|
5
|
+
"description": "Schema for the .docguard.json file that DocGuard reads at the root of every project. Add `\"$schema\": \"https://raccioly.github.io/docguard/schemas/docguard-config.schema.json\"` to your file to get autocomplete + validation in VS Code and other JSON-Schema-aware editors.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"$schema": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "JSON Schema reference for editor autocomplete. Not consumed by DocGuard itself."
|
|
11
|
+
},
|
|
12
|
+
"version": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "Schema version (0.1, 0.2, ... 0.5). Bumped when fields are added or behavior changes. Migrate with `docguard upgrade --apply`.",
|
|
15
|
+
"pattern": "^\\d+\\.\\d+(\\.\\d+)?$"
|
|
16
|
+
},
|
|
17
|
+
"projectName": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Human-friendly project name used in guard output."
|
|
20
|
+
},
|
|
21
|
+
"profile": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"enum": ["starter", "standard", "enterprise"],
|
|
24
|
+
"description": "Compliance profile. starter = minimal, standard = full CDD, enterprise = adds advanced validators.",
|
|
25
|
+
"default": "standard"
|
|
26
|
+
},
|
|
27
|
+
"projectType": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"enum": ["cli", "library", "webapp", "api", "unknown"],
|
|
30
|
+
"description": "Project shape. Affects which validators run (e.g. webapp + api need env vars; cli/library can skip)."
|
|
31
|
+
},
|
|
32
|
+
"projectTypeConfig": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"description": "Per-type behavior knobs that override profile defaults.",
|
|
35
|
+
"properties": {
|
|
36
|
+
"needsEnvVars": { "type": "boolean" },
|
|
37
|
+
"needsEnvExample":{ "type": "boolean" },
|
|
38
|
+
"needsE2E": { "type": "boolean" },
|
|
39
|
+
"needsDatabase": { "type": "boolean" }
|
|
40
|
+
},
|
|
41
|
+
"additionalProperties": true
|
|
42
|
+
},
|
|
43
|
+
"sourceRoot": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Subdirectory containing the project's source files (e.g. `backend/src`). Validators scope to this when set."
|
|
46
|
+
},
|
|
47
|
+
"ignore": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"items": { "type": "string" },
|
|
50
|
+
"description": "Glob patterns for paths every validator should skip. Merged with `.docguardignore` at runtime."
|
|
51
|
+
},
|
|
52
|
+
"securityIgnore": {
|
|
53
|
+
"type": "array",
|
|
54
|
+
"items": { "type": "string" },
|
|
55
|
+
"description": "Glob patterns the Security validator additionally skips (e.g. test fixtures with intentional secrets)."
|
|
56
|
+
},
|
|
57
|
+
"todoIgnore": {
|
|
58
|
+
"type": "array",
|
|
59
|
+
"items": { "type": "string" },
|
|
60
|
+
"description": "Glob patterns the TODO-Tracking validator additionally skips."
|
|
61
|
+
},
|
|
62
|
+
"requiredFiles": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"description": "Files DocGuard expects to exist. Missing files become Structure validator errors.",
|
|
65
|
+
"properties": {
|
|
66
|
+
"canonical": {
|
|
67
|
+
"type": "array",
|
|
68
|
+
"items": { "type": "string" },
|
|
69
|
+
"description": "Canonical doc paths (e.g. docs-canonical/ARCHITECTURE.md)."
|
|
70
|
+
},
|
|
71
|
+
"agentFile": {
|
|
72
|
+
"type": ["array", "string"],
|
|
73
|
+
"description": "Agent rule file(s) (e.g. AGENTS.md, CLAUDE.md). Array means any one suffices."
|
|
74
|
+
},
|
|
75
|
+
"changelog": { "type": "string" },
|
|
76
|
+
"driftLog": { "type": "string" },
|
|
77
|
+
"root": { "type": "array", "items": { "type": "string" } }
|
|
78
|
+
},
|
|
79
|
+
"additionalProperties": true
|
|
80
|
+
},
|
|
81
|
+
"validators": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"description": "Enable / disable individual validators. Set to false to skip.",
|
|
84
|
+
"properties": {
|
|
85
|
+
"structure": { "type": "boolean" },
|
|
86
|
+
"docsSync": { "type": "boolean" },
|
|
87
|
+
"drift": { "type": "boolean" },
|
|
88
|
+
"changelog": { "type": "boolean" },
|
|
89
|
+
"testSpec": { "type": "boolean" },
|
|
90
|
+
"environment": { "type": "boolean" },
|
|
91
|
+
"security": { "type": "boolean" },
|
|
92
|
+
"architecture": { "type": "boolean" },
|
|
93
|
+
"freshness": { "type": "boolean" },
|
|
94
|
+
"traceability": { "type": "boolean" },
|
|
95
|
+
"docsDiff": { "type": "boolean" },
|
|
96
|
+
"apiSurface": { "type": "boolean" },
|
|
97
|
+
"metadataSync": { "type": "boolean" },
|
|
98
|
+
"docsCoverage": { "type": "boolean" },
|
|
99
|
+
"docQuality": { "type": "boolean" },
|
|
100
|
+
"todoTracking": { "type": "boolean" },
|
|
101
|
+
"schemaSync": { "type": "boolean" },
|
|
102
|
+
"specKit": { "type": "boolean" },
|
|
103
|
+
"crossReference": { "type": "boolean" },
|
|
104
|
+
"generatedStaleness":{ "type": "boolean" },
|
|
105
|
+
"metricsConsistency":{ "type": "boolean" }
|
|
106
|
+
},
|
|
107
|
+
"additionalProperties": false
|
|
108
|
+
},
|
|
109
|
+
"severity": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"description": "Per-validator severity overrides. Affects EXIT CODE only — display is unchanged. high = warnings fail CI (exit 1). low = warnings ignored. medium (default) = exit 2.",
|
|
112
|
+
"additionalProperties": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"enum": ["high", "medium", "low"]
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"draftStalenessDays": {
|
|
118
|
+
"type": "integer",
|
|
119
|
+
"minimum": 1,
|
|
120
|
+
"description": "How many days a `status: draft` doc may sit unmodified before Generated-Staleness warns. Default 14."
|
|
121
|
+
},
|
|
122
|
+
"testPattern": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"description": "Single glob identifying test files (legacy single-string form)."
|
|
125
|
+
},
|
|
126
|
+
"testPatterns": {
|
|
127
|
+
"type": "array",
|
|
128
|
+
"items": { "type": "string" },
|
|
129
|
+
"description": "Glob patterns identifying test files. Preferred over testPattern."
|
|
130
|
+
},
|
|
131
|
+
"todoTracking": {
|
|
132
|
+
"type": "object",
|
|
133
|
+
"description": "TODO-Tracking validator overrides.",
|
|
134
|
+
"properties": {
|
|
135
|
+
"includeTestFiles": {
|
|
136
|
+
"type": "boolean",
|
|
137
|
+
"description": "If true, scan test files for TODO/FIXME annotations. Off by default to avoid false positives from comment-marker strings inside test fixtures."
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"additionalProperties": true
|
|
141
|
+
},
|
|
142
|
+
"docQuality": {
|
|
143
|
+
"type": "object",
|
|
144
|
+
"description": "Doc-Quality validator overrides.",
|
|
145
|
+
"properties": {
|
|
146
|
+
"deepScan": {
|
|
147
|
+
"type": "boolean",
|
|
148
|
+
"description": "If true and the `understanding` CLI is on PATH, run its 31-metric deep analysis. Off by default."
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
"additionalProperties": true
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"additionalProperties": true
|
|
155
|
+
}
|