docguard-cli 0.11.2 → 0.13.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/README.md +35 -16
- package/cli/commands/fix.mjs +55 -0
- package/cli/commands/guard.mjs +129 -5
- package/cli/commands/init.mjs +52 -1
- package/cli/commands/sync.mjs +50 -0
- package/cli/commands/trace.mjs +105 -0
- package/cli/commands/upgrade.mjs +250 -0
- package/cli/docguard.mjs +39 -3
- package/cli/shared-git.mjs +0 -0
- package/cli/shared-ignore.mjs +50 -0
- package/cli/shared.mjs +62 -0
- package/cli/validators/cross-reference.mjs +289 -0
- package/cli/validators/docs-sync.mjs +15 -0
- package/cli/validators/freshness.mjs +5 -10
- package/cli/validators/generated-staleness.mjs +97 -0
- package/cli/writers/fix-memory.mjs +133 -0
- package/cli/writers/mechanical.mjs +22 -0
- package/commands/docguard.guard.md +2 -2
- package/docs/quickstart.md +1 -1
- package/extensions/spec-kit-docguard/README.md +1 -1
- package/extensions/spec-kit-docguard/commands/guard.md +1 -1
- package/extensions/spec-kit-docguard/extension.yml +11 -1
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +3 -3
- 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/extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml +51 -0
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +48 -0
- package/package.json +1 -1
- package/templates/commands/docguard.guard.md +2 -2
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
- [What is DocGuard?](#what-is-docguard)
|
|
18
18
|
- [Quick Start](#-quick-start)
|
|
19
19
|
- [Spec Kit Integration](#-spec-kit-integration)
|
|
20
|
-
- [
|
|
20
|
+
- [Usage](#usage)
|
|
21
21
|
- [Validators](#-validators)
|
|
22
22
|
- [Templates](#-templates)
|
|
23
23
|
- [AI Agent Support](#-ai-agent-support)
|
|
@@ -343,7 +343,7 @@ DocGuard provides AI agent slash commands for integrated workflows. Installed au
|
|
|
343
343
|
|
|
344
344
|
| Command | What It Does |
|
|
345
345
|
|:--------|:-------------|
|
|
346
|
-
| `/docguard.guard` | Run quality validation — check all
|
|
346
|
+
| `/docguard.guard` | Run quality validation — check all 22 validators |
|
|
347
347
|
| `/docguard.review` | Analyze doc quality and suggest improvements |
|
|
348
348
|
| `/docguard.fix` | Generate targeted fix prompts for specific issues |
|
|
349
349
|
| `/docguard.score` | Show CDD maturity score with category breakdown |
|
|
@@ -433,20 +433,42 @@ DocGuard runs its own `guard`, `score`, `diff`, `diagnose`, and `badge` commands
|
|
|
433
433
|
|
|
434
434
|
## ⚙️ CI/CD Integration
|
|
435
435
|
|
|
436
|
-
|
|
436
|
+
> **Full recipes:** see [`docs-canonical/CI-RECIPES.md`](./docs-canonical/CI-RECIPES.md) for guard, auto-fix (commits mechanical fixes back to PRs), nightly sync, score-on-PR, and pre-commit configs.
|
|
437
|
+
|
|
438
|
+
### GitHub Actions — Guard (most common)
|
|
437
439
|
|
|
438
440
|
```yaml
|
|
439
|
-
name: DocGuard
|
|
440
|
-
on: [pull_request]
|
|
441
|
+
name: DocGuard Guard
|
|
442
|
+
on: [pull_request, push]
|
|
441
443
|
jobs:
|
|
442
444
|
docguard:
|
|
443
445
|
runs-on: ubuntu-latest
|
|
444
446
|
steps:
|
|
445
447
|
- uses: actions/checkout@v4
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
448
|
+
with: { fetch-depth: 0 }
|
|
449
|
+
- uses: raccioly/docguard@v0.12.0
|
|
450
|
+
with:
|
|
451
|
+
command: guard
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### GitHub Actions — Auto-Fix (commits mechanical fixes back)
|
|
455
|
+
|
|
456
|
+
```yaml
|
|
457
|
+
name: DocGuard Auto-Fix
|
|
458
|
+
on: { pull_request: { types: [opened, synchronize, reopened] } }
|
|
459
|
+
permissions: { contents: write, pull-requests: write }
|
|
460
|
+
jobs:
|
|
461
|
+
autofix:
|
|
462
|
+
runs-on: ubuntu-latest
|
|
463
|
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
464
|
+
steps:
|
|
465
|
+
- uses: actions/checkout@v4
|
|
466
|
+
with:
|
|
467
|
+
ref: ${{ github.event.pull_request.head.ref }}
|
|
468
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
469
|
+
fetch-depth: 0
|
|
470
|
+
- uses: raccioly/docguard@v0.12.0
|
|
471
|
+
with: { command: fix, auto-commit: 'true', comment-on-pr: 'true' }
|
|
450
472
|
```
|
|
451
473
|
|
|
452
474
|
### Pre-commit Hook
|
|
@@ -455,14 +477,11 @@ jobs:
|
|
|
455
477
|
npx docguard-cli hooks --type pre-commit
|
|
456
478
|
```
|
|
457
479
|
|
|
458
|
-
###
|
|
480
|
+
### Workflow starters (copy directly)
|
|
459
481
|
|
|
460
|
-
|
|
461
|
-
-
|
|
462
|
-
|
|
463
|
-
command: guard
|
|
464
|
-
fail-on-warning: true
|
|
465
|
-
```
|
|
482
|
+
Two ready-to-use templates ship with the Spec Kit extension and as standalone files:
|
|
483
|
+
- `extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml` — mandatory CI gate
|
|
484
|
+
- `extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml` — PR auto-fix
|
|
466
485
|
|
|
467
486
|
---
|
|
468
487
|
|
package/cli/commands/fix.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { c } from '../shared.mjs';
|
|
|
21
21
|
import { computeApiSurfaceDrift } from '../validators/api-surface.mjs';
|
|
22
22
|
import { removeEndpoints, hasGeneratedMarker } from '../writers/api-reference.mjs';
|
|
23
23
|
import { applyMechanicalFixes } from '../writers/mechanical.mjs';
|
|
24
|
+
import { loadFixMemory } from '../writers/fix-memory.mjs';
|
|
24
25
|
import { runGuardInternal } from './guard.mjs';
|
|
25
26
|
|
|
26
27
|
const API_DOC = 'docs-canonical/API-REFERENCE.md';
|
|
@@ -281,6 +282,55 @@ export function applyAllMechanicalFixes(projectDir, config, { force = false } =
|
|
|
281
282
|
return { applied, skipped, total: fixes.length };
|
|
282
283
|
}
|
|
283
284
|
|
|
285
|
+
/**
|
|
286
|
+
* M-2 — `docguard fix --history` shows the audit log of mechanical fixes
|
|
287
|
+
* that have been applied to this project. Reads `.docguard/fixed.json`
|
|
288
|
+
* and pretty-prints (or emits JSON when --format json).
|
|
289
|
+
*/
|
|
290
|
+
function runHistoryMode(projectDir, flags) {
|
|
291
|
+
const mem = loadFixMemory(projectDir);
|
|
292
|
+
const isJson = flags.format === 'json';
|
|
293
|
+
|
|
294
|
+
if (isJson) {
|
|
295
|
+
console.log(JSON.stringify(mem, null, 2));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (mem.entries.length === 0) {
|
|
300
|
+
console.log(`${c.bold}🗂 DocGuard Fix History${c.reset}`);
|
|
301
|
+
console.log(`${c.dim} No fixes recorded yet. Run \`docguard fix --write\` to start the audit log.${c.reset}`);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
console.log(`${c.bold}🗂 DocGuard Fix History${c.reset} ${c.dim}(${mem.entries.length} entries, newest first)${c.reset}\n`);
|
|
306
|
+
|
|
307
|
+
// Group by date for readability
|
|
308
|
+
const byDate = new Map();
|
|
309
|
+
for (const e of mem.entries) {
|
|
310
|
+
const day = (e.appliedAt || '').slice(0, 10);
|
|
311
|
+
if (!byDate.has(day)) byDate.set(day, []);
|
|
312
|
+
byDate.get(day).push(e);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Show the most recent N days (cap output at 20 entries)
|
|
316
|
+
let printed = 0;
|
|
317
|
+
for (const [day, dayEntries] of byDate) {
|
|
318
|
+
if (printed >= 20) break;
|
|
319
|
+
console.log(` ${c.cyan}${day}${c.reset} ${c.dim}(${dayEntries.length} fix${dayEntries.length > 1 ? 'es' : ''})${c.reset}`);
|
|
320
|
+
for (const e of dayEntries.slice(0, 5)) {
|
|
321
|
+
if (printed >= 20) break;
|
|
322
|
+
const time = (e.appliedAt || '').slice(11, 16);
|
|
323
|
+
console.log(` ${c.dim}${time}${c.reset} ${e.type} → ${c.cyan}${e.file}${c.reset} ${c.dim}${e.summary || ''}${c.reset}`);
|
|
324
|
+
printed++;
|
|
325
|
+
}
|
|
326
|
+
if (dayEntries.length > 5) console.log(` ${c.dim}... ${dayEntries.length - 5} more on this day${c.reset}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (mem.entries.length > 20) {
|
|
330
|
+
console.log(`\n ${c.dim}... ${mem.entries.length - 20} older entries. Use ${c.cyan}--format json${c.dim} for the full log.${c.reset}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
284
334
|
function runWriteMode(projectDir, config, flags) {
|
|
285
335
|
const isJson = flags.format === 'json';
|
|
286
336
|
const { applied, skipped, total } = applyAllMechanicalFixes(projectDir, config, { force: flags.force });
|
|
@@ -321,6 +371,11 @@ export function runFix(projectDir, config, flags) {
|
|
|
321
371
|
const autoFix = flags.auto || false;
|
|
322
372
|
const specificDoc = flags.doc || null;
|
|
323
373
|
|
|
374
|
+
// M-2: --history shows the audit trail of past mechanical fixes.
|
|
375
|
+
if (flags.history) {
|
|
376
|
+
return runHistoryMode(projectDir, flags);
|
|
377
|
+
}
|
|
378
|
+
|
|
324
379
|
// --write: deterministically APPLY mechanical fixes (no LLM). Currently:
|
|
325
380
|
// remove API-REFERENCE.md endpoints the OpenAPI spec confirms no longer exist.
|
|
326
381
|
if (flags.write) {
|
package/cli/commands/guard.mjs
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* runGuardInternal() → returns data, no side effects (for diagnose, ci)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { c } from '../shared.mjs';
|
|
10
|
+
import { c, resolveSeverity } from '../shared.mjs';
|
|
11
11
|
import { detectAgentMode, isSpecKitInitialized } from '../ensure-skills.mjs';
|
|
12
|
+
import { checkUpgradeStatus } from './upgrade.mjs';
|
|
13
|
+
import { changedFilesSince, isGitRepo } from '../shared-git.mjs';
|
|
12
14
|
import { validateStructure, validateDocSections } from '../validators/structure.mjs';
|
|
13
15
|
import { validateDrift } from '../validators/drift.mjs';
|
|
14
16
|
import { validateChangelog } from '../validators/changelog.mjs';
|
|
@@ -25,6 +27,8 @@ import { validateMetadataSync } from '../validators/metadata-sync.mjs';
|
|
|
25
27
|
import { validateMetricsConsistency } from '../validators/metrics-consistency.mjs';
|
|
26
28
|
import { validateDocsCoverage } from '../validators/docs-coverage.mjs';
|
|
27
29
|
import { validateDocQuality } from '../validators/doc-quality.mjs';
|
|
30
|
+
import { validateCrossReferences } from '../validators/cross-reference.mjs';
|
|
31
|
+
import { validateGeneratedStaleness } from '../validators/generated-staleness.mjs';
|
|
28
32
|
import { validateTodoTracking } from '../validators/todo-tracking.mjs';
|
|
29
33
|
import { validateSchemaSync } from '../validators/schema-sync.mjs';
|
|
30
34
|
import { validateSpecKitIntegration } from '../scanners/speckit.mjs';
|
|
@@ -100,6 +104,8 @@ export function runGuardInternal(projectDir, config) {
|
|
|
100
104
|
{ key: 'todoTracking', name: 'TODO-Tracking', fn: () => validateTodoTracking(projectDir, config) },
|
|
101
105
|
{ key: 'schemaSync', name: 'Schema-Sync', fn: () => validateSchemaSync(projectDir, config) },
|
|
102
106
|
{ key: 'specKit', name: 'Spec-Kit', fn: () => validateSpecKitIntegration(projectDir, config) },
|
|
107
|
+
{ key: 'crossReference', name: 'Cross-Reference', fn: () => validateCrossReferences(projectDir, config) },
|
|
108
|
+
{ key: 'generatedStaleness', name: 'Generated-Staleness', fn: () => validateGeneratedStaleness(projectDir, config) },
|
|
103
109
|
// Metrics-Consistency runs post-loop (needs guard results)
|
|
104
110
|
];
|
|
105
111
|
|
|
@@ -133,6 +139,25 @@ export function runGuardInternal(projectDir, config) {
|
|
|
133
139
|
const totalPassed = activeResults.reduce((sum, r) => sum + r.passed, 0);
|
|
134
140
|
const totalChecks = activeResults.reduce((sum, r) => sum + r.total, 0);
|
|
135
141
|
|
|
142
|
+
// Per-validator severity overrides (v0.5 schema). Affects EXIT-CODE only,
|
|
143
|
+
// not display. Annotate each validator with its resolved severity and roll
|
|
144
|
+
// up effective error/warning counts:
|
|
145
|
+
// - high → validator's warnings get promoted to "effective errors"
|
|
146
|
+
// - low → validator's warnings are demoted (ignored for exit code)
|
|
147
|
+
// - medium (default) → warnings stay as-is
|
|
148
|
+
for (const v of activeResults) {
|
|
149
|
+
v.severity = resolveSeverity(config, v.key);
|
|
150
|
+
}
|
|
151
|
+
let effectiveErrors = totalErrors;
|
|
152
|
+
let effectiveWarnings = 0;
|
|
153
|
+
for (const v of activeResults) {
|
|
154
|
+
const wCount = v.warnings.length;
|
|
155
|
+
if (wCount === 0) continue;
|
|
156
|
+
if (v.severity === 'high') effectiveErrors += wCount;
|
|
157
|
+
else if (v.severity === 'low') { /* ignored for exit */ }
|
|
158
|
+
else effectiveWarnings += wCount;
|
|
159
|
+
}
|
|
160
|
+
|
|
136
161
|
const overallStatus = totalErrors > 0 ? 'FAIL' : totalWarnings > 0 ? 'WARN' : 'PASS';
|
|
137
162
|
|
|
138
163
|
return {
|
|
@@ -143,22 +168,80 @@ export function runGuardInternal(projectDir, config) {
|
|
|
143
168
|
total: totalChecks,
|
|
144
169
|
errors: totalErrors,
|
|
145
170
|
warnings: totalWarnings,
|
|
171
|
+
// v0.5: severity-aware counts for exit-code logic. The display still uses
|
|
172
|
+
// the raw counts above so users see every warning, but CI only fails on
|
|
173
|
+
// things they've marked as high-severity.
|
|
174
|
+
effectiveErrors,
|
|
175
|
+
effectiveWarnings,
|
|
146
176
|
validators: results,
|
|
147
177
|
timestamp: new Date().toISOString(),
|
|
148
178
|
};
|
|
149
179
|
}
|
|
150
180
|
|
|
181
|
+
/**
|
|
182
|
+
* The "pre-commit lite" validator set — fast checks suitable for running
|
|
183
|
+
* on every commit/save. Tuned for <2s wall-clock on average repos.
|
|
184
|
+
*
|
|
185
|
+
* The list is intentionally short: validators that catch >80% of the
|
|
186
|
+
* common doc drift that developers introduce mid-feature (route added but
|
|
187
|
+
* not documented, env var renamed but not updated in ENVIRONMENT.md,
|
|
188
|
+
* endpoint deleted but still in API-REFERENCE.md). Heavy validators —
|
|
189
|
+
* Freshness (git log), Traceability (REQ scan), Doc-Quality (prose lint) —
|
|
190
|
+
* stay off for speed.
|
|
191
|
+
*/
|
|
192
|
+
export const CHANGED_ONLY_VALIDATORS = ['docsSync', 'environment', 'apiSurface'];
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Build a validators map that enables only the pre-commit-lite set.
|
|
196
|
+
* Used by `docguard guard --changed-only`.
|
|
197
|
+
*/
|
|
198
|
+
function liteValidatorsConfig() {
|
|
199
|
+
const all = [
|
|
200
|
+
'structure', 'docsSync', 'drift', 'changelog', 'testSpec', 'environment',
|
|
201
|
+
'security', 'architecture', 'freshness', 'traceability', 'docsDiff',
|
|
202
|
+
'apiSurface', 'metadataSync', 'docsCoverage', 'docQuality', 'todoTracking',
|
|
203
|
+
'schemaSync', 'specKit', 'crossReference', 'generatedStaleness', 'metricsConsistency',
|
|
204
|
+
];
|
|
205
|
+
const out = {};
|
|
206
|
+
for (const k of all) out[k] = CHANGED_ONLY_VALIDATORS.includes(k);
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
151
210
|
/**
|
|
152
211
|
* Public guard — prints results and exits.
|
|
153
212
|
*/
|
|
154
213
|
export function runGuard(projectDir, config, flags) {
|
|
214
|
+
// --changed-only: pre-commit lite mode. Overrides the validator set to a
|
|
215
|
+
// fast subset (Docs-Sync, Environment, API-Surface). Designed for husky/
|
|
216
|
+
// lefthook hooks; expects to finish in under 2 seconds.
|
|
217
|
+
if (flags.changedOnly) {
|
|
218
|
+
// Compute the set of changed files since the given ref (default HEAD~1 —
|
|
219
|
+
// the pre-commit common case: "files changed in this commit vs the last
|
|
220
|
+
// committed state"). Validators that opt into `config.changedFiles` can
|
|
221
|
+
// scope to this list; others run normally over the whole tree.
|
|
222
|
+
const ref = flags.since || 'HEAD~1';
|
|
223
|
+
const changed = isGitRepo(projectDir) ? changedFilesSince(projectDir, ref) : [];
|
|
224
|
+
config = {
|
|
225
|
+
...config,
|
|
226
|
+
validators: liteValidatorsConfig(),
|
|
227
|
+
changedFiles: changed,
|
|
228
|
+
changedSinceRef: ref,
|
|
229
|
+
};
|
|
230
|
+
const label = changed.length > 0
|
|
231
|
+
? `${changed.length} file(s) changed since ${ref}`
|
|
232
|
+
: `no changes since ${ref} — running all ${CHANGED_ONLY_VALIDATORS.length} lite validators on full tree`;
|
|
233
|
+
console.log(`${c.cyan}⚡ docguard guard --changed-only${c.reset} ${c.dim}(${label})${c.reset}\n`);
|
|
234
|
+
}
|
|
235
|
+
|
|
155
236
|
const data = runGuardInternal(projectDir, config);
|
|
156
237
|
|
|
157
238
|
// ── JSON output ──
|
|
158
239
|
if (flags.format === 'json') {
|
|
159
240
|
console.log(JSON.stringify(data, null, 2));
|
|
160
|
-
|
|
161
|
-
|
|
241
|
+
// Use severity-aware effective counts for exit code; raw counts stay in the JSON
|
|
242
|
+
// for display tools that want to show the full picture.
|
|
243
|
+
if (data.effectiveErrors > 0) process.exit(1);
|
|
244
|
+
if (data.effectiveWarnings > 0) process.exit(2);
|
|
162
245
|
process.exit(0);
|
|
163
246
|
}
|
|
164
247
|
|
|
@@ -243,14 +326,55 @@ export function runGuard(projectDir, config, flags) {
|
|
|
243
326
|
const badgeUrl = `https://img.shields.io/badge/CDD_Guard-${data.passed}%2F${data.total}_passed-${bColor}`;
|
|
244
327
|
console.log(`\n ${c.dim}📎 Badge: ${c.reset}`);
|
|
245
328
|
|
|
329
|
+
// Schema upgrade nudge — fires when the project's .docguard.json schema is
|
|
330
|
+
// behind the CLI's CURRENT_SCHEMA_VERSION. Cheap, file-local check; no
|
|
331
|
+
// network access. Suppressed in JSON output to keep machine consumers clean.
|
|
332
|
+
if (!flags || flags.format !== 'json') {
|
|
333
|
+
const upgradeHint = checkUpgradeStatus(projectDir);
|
|
334
|
+
if (upgradeHint) {
|
|
335
|
+
console.log(`\n ${c.yellow}↑ ${upgradeHint}${c.reset}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// K-6 / S-2: sweep-needed nudge. Aggregates freshness warnings — if 2+
|
|
339
|
+
// canonical docs are stale (matching the "X code commits since last doc
|
|
340
|
+
// update" pattern), suggest a single `docguard sync --write` pass that
|
|
341
|
+
// refreshes every code-truth section in one shot. Individual freshness
|
|
342
|
+
// warnings already named the docs; this nudge just turns "5 warnings"
|
|
343
|
+
// into one actionable recommendation.
|
|
344
|
+
const freshness = data.validators.find(v => v.key === 'freshness');
|
|
345
|
+
if (freshness && freshness.warnings) {
|
|
346
|
+
const staleDocs = freshness.warnings.filter(w => /\d+ code commits since/.test(w));
|
|
347
|
+
if (staleDocs.length >= 2) {
|
|
348
|
+
console.log(`\n ${c.yellow}↻ ${staleDocs.length} docs are stale (10+ commits since last update). Run ${c.cyan}docguard sync --write${c.yellow} to refresh code-truth sections in one pass.${c.reset}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
246
353
|
// Spec-kit reminder — persistent nudge if not initialized
|
|
247
354
|
if (!isSpecKitInitialized(projectDir)) {
|
|
248
355
|
console.log(`\n ${c.yellow}💡${c.reset} ${c.dim}Enhance DocGuard with Spec Kit: ${c.cyan}uv tool install specify-cli --from git+https://github.com/github/spec-kit.git${c.reset}`);
|
|
249
356
|
}
|
|
250
357
|
|
|
358
|
+
// When severity overrides demoted warnings to "low" (or promoted them to
|
|
359
|
+
// "high"), show a one-line note so the user knows the exit code may not
|
|
360
|
+
// match what they expected from reading the warning count.
|
|
361
|
+
const severityShifted =
|
|
362
|
+
data.effectiveErrors !== data.errors || data.effectiveWarnings !== data.warnings;
|
|
363
|
+
if (severityShifted) {
|
|
364
|
+
const upgraded = data.effectiveErrors - data.errors;
|
|
365
|
+
const ignored = data.warnings - data.effectiveWarnings - upgraded;
|
|
366
|
+
const parts = [];
|
|
367
|
+
if (upgraded > 0) parts.push(`${upgraded} warning(s) escalated to fail (severity=high)`);
|
|
368
|
+
if (ignored > 0) parts.push(`${ignored} warning(s) ignored for exit code (severity=low)`);
|
|
369
|
+
if (parts.length > 0) {
|
|
370
|
+
console.log(`\n ${c.dim}Severity override: ${parts.join('; ')}.${c.reset}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
251
374
|
console.log('');
|
|
252
375
|
|
|
253
|
-
|
|
254
|
-
if (data.
|
|
376
|
+
// v0.5: severity-aware exit codes (see runGuardInternal for the rollup).
|
|
377
|
+
if (data.effectiveErrors > 0) process.exit(1);
|
|
378
|
+
if (data.effectiveWarnings > 0) process.exit(2);
|
|
255
379
|
process.exit(0);
|
|
256
380
|
}
|
package/cli/commands/init.mjs
CHANGED
|
@@ -168,7 +168,7 @@ export async function runInit(projectDir, config, flags) {
|
|
|
168
168
|
|
|
169
169
|
const defaultConfig = {
|
|
170
170
|
projectName: config.projectName,
|
|
171
|
-
version: '0.
|
|
171
|
+
version: '0.5',
|
|
172
172
|
profile: profileName,
|
|
173
173
|
projectType: detectedType,
|
|
174
174
|
projectTypeConfig: ptc,
|
|
@@ -186,6 +186,13 @@ export async function runInit(projectDir, config, flags) {
|
|
|
186
186
|
environment: true,
|
|
187
187
|
freshness: true,
|
|
188
188
|
},
|
|
189
|
+
// Per-validator severity overrides (v0.5+).
|
|
190
|
+
// 'high': warnings from this validator fail CI (exit 1)
|
|
191
|
+
// 'medium': default — warnings exit 2 (informational)
|
|
192
|
+
// 'low': warnings ignored for exit code (exit 0)
|
|
193
|
+
// Empty by default — every validator uses 'medium'. Add entries to dial
|
|
194
|
+
// strictness up (CI-critical checks) or down (experimental validators).
|
|
195
|
+
severity: {},
|
|
189
196
|
};
|
|
190
197
|
|
|
191
198
|
writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf-8');
|
|
@@ -196,6 +203,50 @@ export async function runInit(projectDir, config, flags) {
|
|
|
196
203
|
console.log(` ${c.yellow}⏭️${c.reset} .docguard.json ${c.dim}(already exists)${c.reset}`);
|
|
197
204
|
}
|
|
198
205
|
|
|
206
|
+
// ── Create .docguardignore ────────────────────────────────────────────
|
|
207
|
+
// Starter ignore file with gitignore-style syntax. Patterns here are merged
|
|
208
|
+
// into config.ignore at load time so every validator honors them.
|
|
209
|
+
const ignorePath = resolve(projectDir, '.docguardignore');
|
|
210
|
+
if (!existsSync(ignorePath)) {
|
|
211
|
+
const ignoreContent = `# .docguardignore — paths to exclude from DocGuard validation.
|
|
212
|
+
# Gitignore-style syntax: one pattern per line, # for comments.
|
|
213
|
+
# Merged into config.ignore (in .docguard.json) at runtime.
|
|
214
|
+
#
|
|
215
|
+
# Common examples:
|
|
216
|
+
# build/ # exclude a directory
|
|
217
|
+
# **/__generated__/** # exclude anything in any __generated__ dir
|
|
218
|
+
# vendor/legacy.ts # exclude a single file
|
|
219
|
+
# **/*.snap # exclude all files matching a glob
|
|
220
|
+
#
|
|
221
|
+
# Build outputs, vendored libs, generated code are good candidates here.
|
|
222
|
+
|
|
223
|
+
# Vendored / generated code that's not yours to document
|
|
224
|
+
**/__generated__/**
|
|
225
|
+
**/generated/**
|
|
226
|
+
**/*.generated.*
|
|
227
|
+
|
|
228
|
+
# Migrations and lock files
|
|
229
|
+
**/migrations/**
|
|
230
|
+
**/*.lock
|
|
231
|
+
package-lock.json
|
|
232
|
+
yarn.lock
|
|
233
|
+
pnpm-lock.yaml
|
|
234
|
+
Cargo.lock
|
|
235
|
+
poetry.lock
|
|
236
|
+
|
|
237
|
+
# Common build artifacts (defaults also cover these, but listing here is clearer)
|
|
238
|
+
# dist/
|
|
239
|
+
# build/
|
|
240
|
+
# coverage/
|
|
241
|
+
`;
|
|
242
|
+
writeFileSync(ignorePath, ignoreContent, 'utf-8');
|
|
243
|
+
created.push('.docguardignore');
|
|
244
|
+
console.log(` ${c.green}✅${c.reset} Created: ${c.cyan}.docguardignore${c.reset} ${c.dim}(gitignore-style exclusions)${c.reset}`);
|
|
245
|
+
} else {
|
|
246
|
+
skipped.push('.docguardignore');
|
|
247
|
+
console.log(` ${c.yellow}⏭️${c.reset} .docguardignore ${c.dim}(already exists)${c.reset}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
199
250
|
// ── Spec-Kit Integration (Extension-First) ────────────────────────────
|
|
200
251
|
// Delegate LLM/IDE detection and spec-kit skill install to `specify init`
|
|
201
252
|
const specKitAvailable = isSpecKitAvailable();
|
package/cli/commands/sync.mjs
CHANGED
|
@@ -34,6 +34,48 @@ function gitChangedFiles(projectDir, since) {
|
|
|
34
34
|
return [...new Set([...committed, ...working])];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* L-1: Map each `source: 'code'` section ID to a predicate that returns true
|
|
39
|
+
* when one of the changed file paths could plausibly affect it. Conservative
|
|
40
|
+
* by design — when in doubt we run the section's sync, never skip it.
|
|
41
|
+
*
|
|
42
|
+
* The predicates are matched against project-relative POSIX paths (the form
|
|
43
|
+
* `git diff --name-only` returns).
|
|
44
|
+
*/
|
|
45
|
+
const SECTION_FILE_MATCHERS = {
|
|
46
|
+
'tech-stack': (p) => /package\.json$|pyproject\.toml$|Cargo\.toml$|go\.mod$|pom\.xml$|Gemfile$/.test(p),
|
|
47
|
+
'frontend-modules': (p) => /(^|\/)(src\/)?(stores|hooks|contexts|features)\//.test(p),
|
|
48
|
+
'endpoints-table': (p) => /(^|\/)(routes|controllers|handlers|app\/api)\//.test(p)
|
|
49
|
+
|| /\.(yaml|yml|json)$/i.test(p) && /openapi|swagger/i.test(p),
|
|
50
|
+
'entities-table': (p) => /(^|\/)(models|schemas|entities)\//.test(p)
|
|
51
|
+
|| /\.prisma$/.test(p),
|
|
52
|
+
'relationships': (p) => /(^|\/)(models|schemas|entities)\//.test(p)
|
|
53
|
+
|| /\.prisma$/.test(p),
|
|
54
|
+
'screens-table': (p) => /(^|\/)(screens|pages|app)\//.test(p)
|
|
55
|
+
|| /\.(tsx|jsx)$/.test(p),
|
|
56
|
+
'flows': (p) => /(^|\/)(screens|pages|app|routes)\//.test(p),
|
|
57
|
+
'integrations-table':(p) => /package\.json$|pyproject\.toml$|requirements.*\.txt$|Cargo\.toml$/.test(p),
|
|
58
|
+
'features-table': (p) => /(^|\/)(features|domains)\//.test(p),
|
|
59
|
+
'features': (p) => /(^|\/)(features|domains)\//.test(p),
|
|
60
|
+
'env-vars-table': (p) => /\.env(\..+)?$|(^|\/)config\//.test(p)
|
|
61
|
+
|| /\.(ts|tsx|js|jsx|mjs|py|go|rs|java|kt|rb)$/.test(p), // any code may use env
|
|
62
|
+
'setup': (p) => /\.env(\..+)?$|(^|\/)config\//.test(p),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Decide whether a given code-truth section should be re-synced based on the
|
|
67
|
+
* set of changed files. Returns true when:
|
|
68
|
+
* - changedFiles is null/empty (no scope info → sync everything), OR
|
|
69
|
+
* - any changed file matches the section's known source patterns, OR
|
|
70
|
+
* - the section has no matcher registered (unknown → conservative: sync)
|
|
71
|
+
*/
|
|
72
|
+
function sectionTouchedByChanges(sectionId, changedFiles) {
|
|
73
|
+
if (!changedFiles || changedFiles.length === 0) return true;
|
|
74
|
+
const matcher = SECTION_FILE_MATCHERS[sectionId];
|
|
75
|
+
if (!matcher) return true; // unknown section → don't accidentally skip it
|
|
76
|
+
return changedFiles.some(matcher);
|
|
77
|
+
}
|
|
78
|
+
|
|
37
79
|
export function runSync(projectDir, config, flags) {
|
|
38
80
|
const plan = buildMemoryPlan(projectDir, config);
|
|
39
81
|
const apply = !!flags.write;
|
|
@@ -63,6 +105,14 @@ export function runSync(projectDir, config, flags) {
|
|
|
63
105
|
const existing = getSection(content, sec.id);
|
|
64
106
|
if (!existing) continue; // sync refreshes sections that already exist
|
|
65
107
|
if (existing.body.trim() === String(sec.body).trim()) continue; // already current
|
|
108
|
+
// L-1: when --since is provided, only update sections whose underlying
|
|
109
|
+
// source files appear in the changed set. Avoids spurious updates when
|
|
110
|
+
// the section's CONTENT would naturally drift (e.g. timestamp-driven
|
|
111
|
+
// counters) but no real source file changed.
|
|
112
|
+
if (changed !== null && !sectionTouchedByChanges(sec.id, changed)) {
|
|
113
|
+
skipped.push({ doc: doc.path, reason: `section ${sec.id} unchanged since ${flags.since} (no underlying source files in diff)` });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
66
116
|
codeSectionChanged = true;
|
|
67
117
|
updates.push({ doc: doc.path, section: sec.id, status: apply ? 'updated' : 'stale' });
|
|
68
118
|
if (apply) { content = replaceSection(content, sec.id, sec.body).content; docChanged = true; }
|
package/cli/commands/trace.mjs
CHANGED
|
@@ -84,7 +84,112 @@ const TRACE_MAP = {
|
|
|
84
84
|
},
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* L-2 / S-3 — Reverse trace: given a code file, find which canonical doc
|
|
89
|
+
* sections mention it. Mirror of the forward trace (doc → code).
|
|
90
|
+
*
|
|
91
|
+
* Match strategies (each yields a hit):
|
|
92
|
+
* 1. Direct path match: full project-relative path appears in doc text.
|
|
93
|
+
* 2. Basename match: e.g. `users.ts` appears (covers cases where the doc
|
|
94
|
+
* refers to the file by name without the full path).
|
|
95
|
+
* 3. Module name match: file stem (e.g. `users`) appears as a fenced
|
|
96
|
+
* `code` reference. Tighter than 2 — avoids matching common nouns.
|
|
97
|
+
*
|
|
98
|
+
* Output: one line per (doc, match-line) pair, with the surrounding context.
|
|
99
|
+
*/
|
|
100
|
+
export function runTraceReverse(projectDir, config, flags) {
|
|
101
|
+
const target = flags.args && flags.args[0];
|
|
102
|
+
if (!target) {
|
|
103
|
+
console.error(`${c.red}Error: trace --reverse requires a target path${c.reset}`);
|
|
104
|
+
console.log(`Usage: ${c.cyan}docguard trace --reverse <code-path>${c.reset}`);
|
|
105
|
+
console.log(`Example: ${c.cyan}docguard trace --reverse src/routes/users.ts${c.reset}`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Suppress chrome in JSON mode so stdout stays parseable.
|
|
110
|
+
const isJson = flags.format === 'json';
|
|
111
|
+
if (!isJson) {
|
|
112
|
+
console.log(`${c.bold}🔄 DocGuard Trace (reverse) — ${target}${c.reset}`);
|
|
113
|
+
console.log(`${c.dim} Finding canonical doc sections that reference this file...${c.reset}\n`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const docsDir = resolve(projectDir, 'docs-canonical');
|
|
117
|
+
if (!existsSync(docsDir)) {
|
|
118
|
+
if (isJson) {
|
|
119
|
+
console.log(JSON.stringify({ target, matches: [], error: 'no docs-canonical/ directory' }, null, 2));
|
|
120
|
+
} else {
|
|
121
|
+
console.log(` ${c.yellow}No docs-canonical/ directory found.${c.reset}`);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Normalize the target path: strip leading ./
|
|
127
|
+
const normalized = target.replace(/^\.\//, '');
|
|
128
|
+
const base = basename(normalized);
|
|
129
|
+
const stem = base.replace(/\.[^.]+$/, '');
|
|
130
|
+
|
|
131
|
+
const matches = []; // { doc, line, content, kind }
|
|
132
|
+
for (const f of readdirSync(docsDir)) {
|
|
133
|
+
if (!f.endsWith('.md')) continue;
|
|
134
|
+
const docPath = resolve(docsDir, f);
|
|
135
|
+
let content;
|
|
136
|
+
try { content = readFileSync(docPath, 'utf-8'); } catch { continue; }
|
|
137
|
+
const lines = content.split('\n');
|
|
138
|
+
for (let i = 0; i < lines.length; i++) {
|
|
139
|
+
const line = lines[i];
|
|
140
|
+
let kind = null;
|
|
141
|
+
if (line.includes(normalized)) kind = 'path';
|
|
142
|
+
else if (line.includes(base)) kind = 'basename';
|
|
143
|
+
else if (new RegExp(`\`${escapeRegex(stem)}\``).test(line)) kind = 'module';
|
|
144
|
+
if (kind) {
|
|
145
|
+
matches.push({ doc: f, line: i + 1, content: line.trim(), kind });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (flags.format === 'json') {
|
|
151
|
+
console.log(JSON.stringify({
|
|
152
|
+
target: normalized,
|
|
153
|
+
matches,
|
|
154
|
+
timestamp: new Date().toISOString(),
|
|
155
|
+
}, null, 2));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (matches.length === 0) {
|
|
160
|
+
console.log(` ${c.yellow}⚠️ No canonical doc references "${normalized}"${c.reset}`);
|
|
161
|
+
console.log(` ${c.dim}Consider documenting this file in docs-canonical/ARCHITECTURE.md or DATA-MODEL.md${c.reset}`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Group by doc for readable output
|
|
166
|
+
const byDoc = new Map();
|
|
167
|
+
for (const m of matches) {
|
|
168
|
+
if (!byDoc.has(m.doc)) byDoc.set(m.doc, []);
|
|
169
|
+
byDoc.get(m.doc).push(m);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log(` ${c.green}✅ ${matches.length} reference(s) across ${byDoc.size} doc(s):${c.reset}\n`);
|
|
173
|
+
for (const [doc, hits] of byDoc) {
|
|
174
|
+
console.log(` ${c.cyan}${doc}${c.reset} ${c.dim}(${hits.length} hit${hits.length > 1 ? 's' : ''})${c.reset}`);
|
|
175
|
+
for (const h of hits.slice(0, 5)) {
|
|
176
|
+
const trimmed = h.content.length > 80 ? h.content.slice(0, 77) + '…' : h.content;
|
|
177
|
+
console.log(` ${c.dim}L${h.line} [${h.kind}]${c.reset} ${trimmed}`);
|
|
178
|
+
}
|
|
179
|
+
if (hits.length > 5) console.log(` ${c.dim}... ${hits.length - 5} more${c.reset}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function escapeRegex(s) {
|
|
184
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
185
|
+
}
|
|
186
|
+
|
|
87
187
|
export function runTrace(projectDir, config, flags) {
|
|
188
|
+
// L-2: dispatch to reverse mode when --reverse is set.
|
|
189
|
+
if (flags.reverse) {
|
|
190
|
+
return runTraceReverse(projectDir, config, flags);
|
|
191
|
+
}
|
|
192
|
+
|
|
88
193
|
console.log(`${c.bold}🔗 DocGuard Trace — ${config.projectName}${c.reset}`);
|
|
89
194
|
console.log(`${c.dim} Directory: ${projectDir}${c.reset}`);
|
|
90
195
|
console.log(`${c.dim} Generating requirements traceability matrix...${c.reset}\n`);
|