docguard-cli 0.38.0 → 0.40.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 +45 -22
- package/cli/commands/agent.mjs +47 -1
- package/cli/commands/explain.mjs +16 -0
- package/cli/commands/feedback.mjs +147 -6
- package/cli/commands/fix.mjs +13 -11
- package/cli/commands/generate.mjs +52 -18
- package/cli/commands/guard.mjs +13 -2
- package/cli/commands/mcp.mjs +22 -2
- package/cli/commands/score.mjs +13 -1
- package/cli/commands/specs.mjs +21 -2
- package/cli/commands/sync.mjs +20 -7
- package/cli/commands/verify.mjs +65 -2
- package/cli/config.mjs +3 -0
- package/cli/docguard.mjs +48 -16
- package/cli/evidence/adapters.mjs +200 -0
- package/cli/evidence/evaluate.mjs +185 -0
- package/cli/evidence/manifest.mjs +194 -0
- package/cli/evidence/markdown.mjs +107 -0
- package/cli/feedback-fixture.mjs +188 -0
- package/cli/findings.mjs +31 -0
- package/cli/repository-root.mjs +159 -0
- package/cli/scanners/py-ast.mjs +39 -2
- package/cli/scanners/task-context.mjs +312 -0
- package/cli/shared-doc-roles.mjs +44 -1
- package/cli/shared-source.mjs +101 -28
- package/cli/validators/architecture.mjs +186 -13
- package/cli/validators/environment.mjs +14 -1
- package/cli/validators/evidence.mjs +52 -0
- package/cli/validators/security.mjs +5 -4
- package/cli/validators/todo-tracking.mjs +45 -2
- package/cli/writers/doc-generators.mjs +31 -17
- package/cli/writers/mechanical.mjs +44 -14
- package/cli/writers/sections.mjs +31 -3
- package/docs/ai-integration.md +31 -6
- package/docs/commands.md +43 -5
- package/docs/configuration.md +11 -3
- package/docs/quickstart.md +1 -1
- package/extensions/spec-kit-docguard/commands/fix.md +4 -2
- package/extensions/spec-kit-docguard/commands/generate.md +6 -1
- package/extensions/spec-kit-docguard/commands/guard.md +3 -2
- package/extensions/spec-kit-docguard/commands/sync.md +1 -1
- package/extensions/spec-kit-docguard/extension.yml +1 -1
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +14 -3
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +16 -5
- package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +8 -3
- package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +3 -2
- package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +6 -3
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml +2 -2
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +4 -4
- package/package.json +3 -1
- package/schemas/docguard-agent-context-benchmark.schema.json +92 -0
- package/schemas/docguard-agent-context-result.schema.json +95 -0
- package/schemas/docguard-benchmark.schema.json +84 -0
- package/schemas/docguard-config.schema.json +1 -0
- package/schemas/docguard-evidence.schema.json +169 -0
- package/schemas/docguard-feedback-fixture.schema.json +54 -0
- package/schemas/docguard-task-context.schema.json +144 -0
- package/templates/AGENTS.md.template +9 -4
- package/templates/ci/github-actions.yml +4 -4
- package/templates/commands/docguard.guard.md +5 -1
- package/templates/commands/docguard.review.md +6 -1
- package/templates/evidence-manifest.json +21 -0
- package/templates/feedback-fixture.json +18 -0
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @implements docguard.language-repository-coverage#FR-009
|
|
3
|
+
* @implements docguard.language-repository-coverage#FR-010
|
|
4
|
+
* @implements docguard.language-repository-coverage#FR-011
|
|
5
|
+
*/
|
|
6
|
+
import { assertMappedFullDocumentWrites, docRolePath, isMappedDocPath, mappedRolesForPath, resolveDocRole } from '../shared-doc-roles.mjs';
|
|
2
7
|
/**
|
|
3
8
|
* Generate Command — Reverse-engineer canonical docs from an existing codebase
|
|
4
9
|
* Scans source code and creates documentation templates pre-filled with project data.
|
|
@@ -6,7 +11,7 @@ import { assertDefaultDocWrites } from '../shared-doc-roles.mjs';
|
|
|
6
11
|
* This is the "killer feature" — take any project and auto-generate CDD docs.
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
|
-
import { existsSync, readFileSync
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
10
15
|
import { resolve, extname, basename, relative } from 'node:path';
|
|
11
16
|
import { c } from '../shared.mjs';
|
|
12
17
|
import { walkFiles as sharedWalkFiles } from '../shared-ignore.mjs';
|
|
@@ -14,7 +19,7 @@ import { detectDocTools } from '../scanners/doc-tools.mjs';
|
|
|
14
19
|
import { scanRoutesDeep } from '../scanners/routes.mjs';
|
|
15
20
|
import { scanSchemasDeep } from '../scanners/schemas.mjs';
|
|
16
21
|
import { buildMemoryPlan } from '../scanners/memory-plan.mjs';
|
|
17
|
-
import { upsertSection } from '../writers/sections.mjs';
|
|
22
|
+
import { assertOwnedCodeSection, replaceSection, upsertSection } from '../writers/sections.mjs';
|
|
18
23
|
import { safeWrite, registerGeneratedCanonicalDocs, surfaceConfidence } from '../writers/generate-io.mjs';
|
|
19
24
|
import {
|
|
20
25
|
generateArchitecture, generateApiReference, generateDataModel,
|
|
@@ -40,11 +45,32 @@ const CODE_EXTENSIONS = new Set([
|
|
|
40
45
|
* inserted as agent-task placeholders), respecting human prose via markers.
|
|
41
46
|
*/
|
|
42
47
|
export function runGeneratePlan(projectDir, config, flags) {
|
|
43
|
-
if (flags.write) assertDefaultDocWrites(config);
|
|
44
48
|
// `--profile <name>` previews a profile's doc set without needing `init` first.
|
|
45
49
|
if (flags.profile) config = { ...config, profile: flags.profile };
|
|
46
50
|
const plan = buildMemoryPlan(projectDir, config);
|
|
47
51
|
|
|
52
|
+
// Existing mapped human documents grant ownership section-by-section. Check
|
|
53
|
+
// every target before the first write so one malformed later file cannot
|
|
54
|
+
// leave an earlier document partially updated.
|
|
55
|
+
if (flags.write) {
|
|
56
|
+
for (const doc of plan.docs) {
|
|
57
|
+
const full = resolve(projectDir, doc.path);
|
|
58
|
+
if (!isMappedDocPath(config, doc.path)) continue;
|
|
59
|
+
if (!existsSync(full)) {
|
|
60
|
+
if (mappedRolesForPath(config, doc.path).length > 1) {
|
|
61
|
+
throw new Error(`Mapped document ${doc.path} serves multiple roles; whole-document scaffolding is unavailable.`);
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const content = readFileSync(full, 'utf8');
|
|
66
|
+
const fullyOwned = /^[ \t]*<!--\s*docguard:generated\s+true\s*-->[ \t]*$/mi.test(content);
|
|
67
|
+
if (fullyOwned) assertMappedFullDocumentWrites(projectDir, config, mappedRolesForPath(config, doc.path));
|
|
68
|
+
else for (const sec of doc.sections.filter(item => item.source === 'code')) {
|
|
69
|
+
assertOwnedCodeSection(content, sec.id, doc.path);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
48
74
|
if (flags.format === 'json') {
|
|
49
75
|
console.log(JSON.stringify({
|
|
50
76
|
project: config.projectName,
|
|
@@ -80,8 +106,6 @@ export function runGeneratePlan(projectDir, config, flags) {
|
|
|
80
106
|
|
|
81
107
|
// --write: scaffold the skeleton docs with code sections + agent-task placeholders.
|
|
82
108
|
if (flags.write) {
|
|
83
|
-
const docsDir = resolve(projectDir, 'docs-canonical');
|
|
84
|
-
if (!existsSync(docsDir)) mkdirSync(docsDir, { recursive: true });
|
|
85
109
|
let wrote = 0;
|
|
86
110
|
for (const doc of plan.docs) {
|
|
87
111
|
const full = resolve(projectDir, doc.path);
|
|
@@ -89,11 +113,16 @@ export function runGeneratePlan(projectDir, config, flags) {
|
|
|
89
113
|
let content = existsSync(full)
|
|
90
114
|
? readFileSync(full, 'utf-8')
|
|
91
115
|
: `# ${title}\n\n<!-- docguard:generated true -->\n`;
|
|
116
|
+
const boundedMapped = isMappedDocPath(config, doc.path) && existsSync(full)
|
|
117
|
+
&& !/^[ \t]*<!--\s*docguard:generated\s+true\s*-->[ \t]*$/mi.test(content);
|
|
92
118
|
for (const sec of doc.sections) {
|
|
119
|
+
if (boundedMapped && sec.source !== 'code') continue;
|
|
93
120
|
const body = sec.source === 'code'
|
|
94
121
|
? sec.body
|
|
95
122
|
: `> **AI task:** ${sec.task}\n<!-- docguard:pending agent writes this section -->`;
|
|
96
|
-
content =
|
|
123
|
+
content = boundedMapped
|
|
124
|
+
? replaceSection(content, sec.id, body).content
|
|
125
|
+
: upsertSection(content, sec.id, body, { source: sec.source }).content;
|
|
97
126
|
}
|
|
98
127
|
// Route through safeWrite: creates the parent dir (docs-implementation/ may
|
|
99
128
|
// not exist yet — was an ENOENT crash) and snapshots a .bak before writing.
|
|
@@ -139,7 +168,6 @@ export function runGeneratePlan(projectDir, config, flags) {
|
|
|
139
168
|
}
|
|
140
169
|
|
|
141
170
|
export function runGenerate(projectDir, config, flags) {
|
|
142
|
-
if (!flags.plan || flags.write) assertDefaultDocWrites(config);
|
|
143
171
|
// --plan: emit the AI-powered "memory plan" — the agent task manifest. The CLI
|
|
144
172
|
// builds the code-truth skeleton (marked sections) + tells the agent exactly
|
|
145
173
|
// what prose to write per section. This is the language-aware Generate path.
|
|
@@ -191,17 +219,23 @@ export function runGenerate(projectDir, config, flags) {
|
|
|
191
219
|
console.log('');
|
|
192
220
|
|
|
193
221
|
// ── 6. Generate Documents ──
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
222
|
+
// Preflight every mapped whole-document target before the first generator
|
|
223
|
+
// writes. Existing targets are skipped unless --force, so they do not need
|
|
224
|
+
// ownership merely to run ordinary generation.
|
|
225
|
+
const candidateRoles = ['architecture', ...(deepRoutes.length > 0 ? ['apiReference'] : []),
|
|
226
|
+
'dataModel', 'environment', 'testSpec', 'security'];
|
|
227
|
+
const writableRoles = candidateRoles.filter(role => {
|
|
228
|
+
const target = resolveDocRole(projectDir, config, role);
|
|
229
|
+
return !existsSync(target) || flags.force;
|
|
230
|
+
});
|
|
231
|
+
assertMappedFullDocumentWrites(projectDir, config, writableRoles);
|
|
198
232
|
|
|
199
233
|
// ── Safety: warn if --force will overwrite existing files ──
|
|
200
234
|
if (flags.force) {
|
|
201
235
|
const targetFiles = [
|
|
202
|
-
'
|
|
203
|
-
'
|
|
204
|
-
'
|
|
236
|
+
docRolePath(config, 'architecture'), docRolePath(config, 'apiReference'),
|
|
237
|
+
docRolePath(config, 'dataModel'), docRolePath(config, 'environment'),
|
|
238
|
+
docRolePath(config, 'testSpec'), docRolePath(config, 'security'),
|
|
205
239
|
'AGENTS.md', 'CHANGELOG.md', 'DRIFT-LOG.md',
|
|
206
240
|
];
|
|
207
241
|
const existing = targetFiles.filter(f => existsSync(resolve(projectDir, f)));
|
|
@@ -248,9 +282,9 @@ export function runGenerate(projectDir, config, flags) {
|
|
|
248
282
|
// B7: keep guard coherent — register the canonical docs we emitted so the
|
|
249
283
|
// traceability validator doesn't flag the generator's own output.
|
|
250
284
|
const registered = registerGeneratedCanonicalDocs(projectDir, [
|
|
251
|
-
'
|
|
252
|
-
'
|
|
253
|
-
'
|
|
285
|
+
docRolePath(config, 'architecture'), docRolePath(config, 'apiReference'),
|
|
286
|
+
docRolePath(config, 'dataModel'), docRolePath(config, 'environment'),
|
|
287
|
+
docRolePath(config, 'testSpec'), docRolePath(config, 'security'),
|
|
254
288
|
]);
|
|
255
289
|
|
|
256
290
|
console.log(`\n${c.bold} ─────────────────────────────────────${c.reset}`);
|
package/cli/commands/guard.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describeCheckCoverage, summarizeCheckCoverage } from '../validator-coverage.mjs';
|
|
2
2
|
import { applyDocRoles } from '../shared-doc-roles.mjs';
|
|
3
3
|
/**
|
|
4
|
+
* @implements docguard.evidence-scoped-verification#FR-010
|
|
4
5
|
* Guard Command — Validate project against its canonical documentation
|
|
5
6
|
* Runs all enabled validators and reports results.
|
|
6
7
|
*
|
|
@@ -157,6 +158,8 @@ import { validateReferenceExistence } from '../validators/reference-existence.mj
|
|
|
157
158
|
import { validateApiDocSmells } from '../validators/api-doc-smells.mjs';
|
|
158
159
|
import { validateDocumentLifecycle } from '../validators/document-lifecycle.mjs';
|
|
159
160
|
import { validateSpecRegistry } from '../validators/spec-registry.mjs';
|
|
161
|
+
import { validateEvidence } from '../validators/evidence.mjs';
|
|
162
|
+
import { coverSemanticClaims } from '../evidence/evaluate.mjs';
|
|
160
163
|
|
|
161
164
|
/**
|
|
162
165
|
* Internal guard — returns structured data, no console output, no process.exit.
|
|
@@ -323,6 +326,7 @@ export function runGuardInternal(projectDir, config) {
|
|
|
323
326
|
{ key: 'specKit', name: 'Spec-Kit', fn: () => validateSpecKitIntegration(projectDir, config) },
|
|
324
327
|
{ key: 'documentLifecycle', name: 'Document-Lifecycle', fn: () => validateDocumentLifecycle(projectDir, config) },
|
|
325
328
|
{ key: 'specRegistry', name: 'Spec-Registry', fn: () => validateSpecRegistry(projectDir, config) },
|
|
329
|
+
{ key: 'evidence', name: 'Evidence', fn: () => validateEvidence(projectDir, config) },
|
|
326
330
|
{ key: 'crossReference', name: 'Cross-Reference', fn: () => validateCrossReferences(projectDir, config) },
|
|
327
331
|
{ key: 'generatedStaleness', name: 'Generated-Staleness', fn: () => validateGeneratedStaleness(projectDir, config) },
|
|
328
332
|
{ key: 'surfaceSync', name: 'Surface-Sync', fn: () => validateSurfaceSync(projectDir, config) },
|
|
@@ -492,9 +496,14 @@ export function runGuardInternal(projectDir, config) {
|
|
|
492
496
|
const lite = Array.isArray(config.changedFiles);
|
|
493
497
|
let coverage = null;
|
|
494
498
|
let semanticClaims = null;
|
|
499
|
+
const evidence = results.find(result => result.key === 'evidence')?.evidence || null;
|
|
495
500
|
if (!lite) {
|
|
496
501
|
try { coverage = computeDocCoverage(projectDir, config); } catch { coverage = null; }
|
|
497
|
-
try {
|
|
502
|
+
try {
|
|
503
|
+
const claims = extractSemanticClaims(projectDir, config);
|
|
504
|
+
const scoped = coverSemanticClaims(claims, evidence);
|
|
505
|
+
semanticClaims = { count: scoped.unverified, discovered: scoped.total, verifiedWithinScope: scoped.verifiedWithinScope, coveredClaimIds: scoped.covered };
|
|
506
|
+
}
|
|
498
507
|
catch { semanticClaims = null; }
|
|
499
508
|
}
|
|
500
509
|
|
|
@@ -518,6 +527,7 @@ export function runGuardInternal(projectDir, config) {
|
|
|
518
527
|
coverage,
|
|
519
528
|
checkCoverage,
|
|
520
529
|
semanticClaims,
|
|
530
|
+
evidence,
|
|
521
531
|
validators: results,
|
|
522
532
|
// Unknown keys in `docguard:validator … n/a` markers — typo protection so
|
|
523
533
|
// a mistyped key doesn't silently fail to suppress. Surfaced by runGuard.
|
|
@@ -539,7 +549,7 @@ export function runGuardInternal(projectDir, config) {
|
|
|
539
549
|
* Freshness (git log), Traceability (REQ scan), Doc-Quality (prose lint) —
|
|
540
550
|
* stay off for speed.
|
|
541
551
|
*/
|
|
542
|
-
export const CHANGED_ONLY_VALIDATORS = ['docsSync', 'environment', 'apiSurface', 'drift', 'todoTracking'];
|
|
552
|
+
export const CHANGED_ONLY_VALIDATORS = ['docsSync', 'environment', 'apiSurface', 'drift', 'todoTracking', 'evidence'];
|
|
543
553
|
|
|
544
554
|
/**
|
|
545
555
|
* Build a validators map that enables the pre-commit-lite set — PLUS any
|
|
@@ -560,6 +570,7 @@ export function liteValidatorsConfig(config = {}) {
|
|
|
560
570
|
'apiSurface', 'metadataSync', 'docsCoverage', 'docQuality', 'todoTracking',
|
|
561
571
|
'schemaSync', 'specKit', 'crossReference', 'generatedStaleness',
|
|
562
572
|
'canonicalSync', 'metricsConsistency',
|
|
573
|
+
'evidence',
|
|
563
574
|
];
|
|
564
575
|
const userValidators = (config && config.validators) || {};
|
|
565
576
|
const out = {};
|
package/cli/commands/mcp.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* @implements docguard.evidence-scoped-verification#FR-009
|
|
3
|
+
* @implements docguard.evidence-scoped-verification#FR-012
|
|
2
4
|
* MCP Command — DocGuard as a Model Context Protocol server (stdio).
|
|
3
5
|
*
|
|
4
6
|
* `docguard mcp` exposes the read-only core (guard / score / explain /
|
|
5
|
-
* verify-claims / diagnose) as MCP tools any MCP client (Claude, Cursor,
|
|
7
|
+
* verify-evidence / verify-claims / diagnose) as MCP tools any MCP client (Claude, Cursor,
|
|
6
8
|
* agent SDKs) can call over stdio. JSON-RPC 2.0, newline-delimited, per the
|
|
7
9
|
* MCP stdio transport (protocol revision 2024-11-05).
|
|
8
10
|
*
|
|
@@ -31,6 +33,7 @@ import { buildReport } from './report.mjs';
|
|
|
31
33
|
import { loadConfig } from '../config.mjs';
|
|
32
34
|
import { CODES } from '../findings.mjs';
|
|
33
35
|
import { extractSemanticClaims, buildSemanticVerifyTasks } from '../scanners/semantic-claims.mjs';
|
|
36
|
+
import { coverSemanticClaims, evaluateEvidence } from '../evidence/evaluate.mjs';
|
|
34
37
|
|
|
35
38
|
const _PKG = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json'), 'utf-8'));
|
|
36
39
|
|
|
@@ -101,6 +104,16 @@ const TOOLS = [
|
|
|
101
104
|
},
|
|
102
105
|
annotations: READONLY_ANNOTATIONS,
|
|
103
106
|
},
|
|
107
|
+
{
|
|
108
|
+
name: 'docguard_verify_evidence',
|
|
109
|
+
title: 'Verify declared evidence',
|
|
110
|
+
description: 'Evaluate `.docguard-evidence.json` against bounded local sources. Returns explicit verified-within-scope, contradicted, stale, inconclusive, and unsupported states; verification applies only to each selected statement.',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: { ...PROJECT_DIR_PROP },
|
|
114
|
+
},
|
|
115
|
+
annotations: READONLY_ANNOTATIONS,
|
|
116
|
+
},
|
|
104
117
|
{
|
|
105
118
|
name: 'docguard_verify_claims',
|
|
106
119
|
title: 'Extract claims to verify',
|
|
@@ -173,13 +186,20 @@ const TOOL_HANDLERS = {
|
|
|
173
186
|
docguard_verify_claims(args, defaultDir) {
|
|
174
187
|
const { dir, config } = resolveTarget(args, defaultDir);
|
|
175
188
|
const claims = extractSemanticClaims(dir, config);
|
|
189
|
+
const coverage = coverSemanticClaims(claims, evaluateEvidence(dir, config));
|
|
176
190
|
return {
|
|
177
191
|
claimCount: claims.length,
|
|
192
|
+
verifiedWithinScope: coverage.verifiedWithinScope,
|
|
178
193
|
note: 'Deterministic discovery, LLM judgment — the caller verifies each claim against the code and reports any mismatch with both values.',
|
|
179
|
-
tasks: buildSemanticVerifyTasks(
|
|
194
|
+
tasks: buildSemanticVerifyTasks(coverage.remaining),
|
|
180
195
|
};
|
|
181
196
|
},
|
|
182
197
|
|
|
198
|
+
docguard_verify_evidence(args, defaultDir) {
|
|
199
|
+
const { dir, config } = resolveTarget(args, defaultDir);
|
|
200
|
+
return evaluateEvidence(dir, config);
|
|
201
|
+
},
|
|
202
|
+
|
|
183
203
|
docguard_report(args, defaultDir) {
|
|
184
204
|
const { dir, config } = resolveTarget(args, defaultDir);
|
|
185
205
|
return buildReport(dir, config);
|
package/cli/commands/score.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { applyDocRoles, remapDocPath, resolveDocRole } from '../shared-doc-roles.mjs';
|
|
2
2
|
/**
|
|
3
|
+
* @implements docguard.evidence-scoped-verification#FR-012
|
|
3
4
|
* Score Command — Calculate CDD maturity score (0-100)
|
|
4
5
|
* Shows category breakdown with weighted scoring.
|
|
5
6
|
*/
|
|
@@ -14,6 +15,7 @@ import { extractSemanticClaims } from '../scanners/semantic-claims.mjs';
|
|
|
14
15
|
import { assessAgentReadability } from '../scanners/agent-readability.mjs';
|
|
15
16
|
import { loadHistory, sparkline } from '../writers/history.mjs';
|
|
16
17
|
import { listCanonicalDocs } from '../shared-ignore.mjs';
|
|
18
|
+
import { coverSemanticClaims, evaluateEvidence } from '../evidence/evaluate.mjs';
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Detect whether the project configures a test runner (the "Check 3" of the
|
|
@@ -415,11 +417,21 @@ export function runScoreInternal(projectDir, config) {
|
|
|
415
417
|
/** Evidence boundary shared by human, CI, report, and MCP score consumers. */
|
|
416
418
|
export function buildScoreAssurance(projectDir, config) {
|
|
417
419
|
let unverifiedClaims = null;
|
|
418
|
-
|
|
420
|
+
let declaredEvidence = null;
|
|
421
|
+
try {
|
|
422
|
+
const claims = extractSemanticClaims(projectDir, config);
|
|
423
|
+
const evaluated = evaluateEvidence(projectDir, config);
|
|
424
|
+
unverifiedClaims = coverSemanticClaims(claims, evaluated).unverified;
|
|
425
|
+
} catch { /* unknown, never zero on failure */ }
|
|
426
|
+
try {
|
|
427
|
+
const evaluated = evaluateEvidence(projectDir, config);
|
|
428
|
+
declaredEvidence = { configured: evaluated.exists, status: evaluated.status, summary: evaluated.summary };
|
|
429
|
+
} catch { /* unknown, never clean on failure */ }
|
|
419
430
|
return {
|
|
420
431
|
status: 'unverified',
|
|
421
432
|
factualAccuracy: null,
|
|
422
433
|
unverifiedClaims,
|
|
434
|
+
declaredEvidence,
|
|
423
435
|
limitation: 'Structural maturity is not factual accuracy. Claim discovery is heuristic; uncaptured prose remains unverified.',
|
|
424
436
|
};
|
|
425
437
|
}
|
package/cli/commands/specs.mjs
CHANGED
|
@@ -56,6 +56,13 @@ function archiveReadiness(spec, targetVerified = false) {
|
|
|
56
56
|
return { status: 'READY', reason: 'Run the reviewed spec retirement flow after the verified state is committed and retained.' };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function completionTransition(spec) {
|
|
60
|
+
if (spec?.reviewed.lifecycle.delivery === 'verified') return 'verified→verified';
|
|
61
|
+
return spec?.reviewed.lifecycle.delivery === 'in_progress'
|
|
62
|
+
? 'in_progress→implemented→verified'
|
|
63
|
+
: 'implemented→verified';
|
|
64
|
+
}
|
|
65
|
+
|
|
59
66
|
export function planSpecCompletion(projectDir, config, flags, options = {}) {
|
|
60
67
|
const projection = projectSpecRegistry(projectDir, config);
|
|
61
68
|
const loaded = readSpecRegistry(projectDir);
|
|
@@ -70,8 +77,12 @@ export function planSpecCompletion(projectDir, config, flags, options = {}) {
|
|
|
70
77
|
|
|
71
78
|
let reconcile = null;
|
|
72
79
|
if (spec) {
|
|
80
|
+
const maintenance = spec.reviewed.lifecycle.delivery === 'verified'
|
|
81
|
+
&& spec.reviewed.lifecycle.persistenceModel === 'living';
|
|
73
82
|
if (spec.reviewed.lifecycle.approval !== 'approved') blockers.push({ code: 'SPC002', message: 'Only an approved spec can become verified.' });
|
|
74
|
-
if (!['in_progress', 'implemented'].includes(spec.reviewed.lifecycle.delivery)
|
|
83
|
+
if (!['in_progress', 'implemented'].includes(spec.reviewed.lifecycle.delivery) && !maintenance) {
|
|
84
|
+
blockers.push({ code: 'SPC002', message: `Expected delivery=in_progress, implemented, or verified with persistenceModel=living; found ${spec.reviewed.lifecycle.delivery}/${spec.reviewed.lifecycle.persistenceModel || 'unset'}.` });
|
|
85
|
+
}
|
|
75
86
|
const tasks = spec.observed.taskCompletion;
|
|
76
87
|
if (!tasks.total || tasks.checked !== tasks.total) blockers.push({ code: 'SPC003', message: `All tasks must be checked (${tasks.checked}/${tasks.total}).` });
|
|
77
88
|
if (spec.observed.implementationEvidence.length === 0) blockers.push({ code: 'SPC004', message: 'At least one qualified source implementation annotation is required.' });
|
|
@@ -92,6 +103,14 @@ export function planSpecCompletion(projectDir, config, flags, options = {}) {
|
|
|
92
103
|
if (reconcile.status === 'UNSUPPORTED' || reconcile.status === 'BLOCKED') blockers.push({ code: 'SPC006', message: 'Reconciliation coverage is unsupported or blocked.' });
|
|
93
104
|
const unresolved = reconcile.classifications.filter(item => item.disposition === 'unsupported_or_ambiguous');
|
|
94
105
|
if (unresolved.length) blockers.push({ code: 'SPC006', message: `Unresolved changed files: ${unresolved.map(item => item.path).join(', ')}.` });
|
|
106
|
+
if (maintenance) {
|
|
107
|
+
const reviewable = reconcile.classifications.filter(item =>
|
|
108
|
+
item.specs.includes(spec.specId)
|
|
109
|
+
&& ['source', 'test', 'canonical_doc', 'decision'].includes(item.kind));
|
|
110
|
+
if (revision === spec.reviewed.reconciliation.lastReviewedRevision || reviewable.length === 0) {
|
|
111
|
+
blockers.push({ code: 'SPC006', message: 'Living-spec maintenance requires a new linked source, test, canonical-document, or decision change since the last reviewed revision.' });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
95
114
|
}
|
|
96
115
|
}
|
|
97
116
|
const guard = options.guardResult || runGuardInternal(projectDir, config);
|
|
@@ -100,7 +119,7 @@ export function planSpecCompletion(projectDir, config, flags, options = {}) {
|
|
|
100
119
|
status: blockers.length ? 'BLOCKED' : 'READY',
|
|
101
120
|
specId: flags.id || null,
|
|
102
121
|
revision,
|
|
103
|
-
transition: spec
|
|
122
|
+
transition: completionTransition(spec),
|
|
104
123
|
blockers,
|
|
105
124
|
reconciliation: reconcile,
|
|
106
125
|
evidence: spec ? [...new Set([
|
package/cli/commands/sync.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isMappedDocPath } from '../shared-doc-roles.mjs';
|
|
2
2
|
/**
|
|
3
3
|
* Sync Command — keep the documentation memory ALWAYS UP TO DATE.
|
|
4
4
|
*
|
|
@@ -15,12 +15,13 @@ import { assertDefaultDocWrites } from '../shared-doc-roles.mjs';
|
|
|
15
15
|
* @implements docguard.document-lifecycle#FR-010
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { existsSync, readFileSync
|
|
18
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
19
19
|
import { resolve } from 'node:path';
|
|
20
20
|
import { execFileSync } from 'node:child_process';
|
|
21
21
|
import { c } from '../shared.mjs';
|
|
22
22
|
import { buildMemoryPlan } from '../scanners/memory-plan.mjs';
|
|
23
|
-
import { getSection, replaceSection } from '../writers/sections.mjs';
|
|
23
|
+
import { assertOwnedCodeSection, getSection, inspectSections, replaceSection } from '../writers/sections.mjs';
|
|
24
|
+
import { safeWrite } from '../writers/generate-io.mjs';
|
|
24
25
|
import { hasGeneratedMarker } from '../writers/api-reference.mjs';
|
|
25
26
|
import { runSyncTests } from './sync-tests.mjs';
|
|
26
27
|
import { sectionTouchedByChanges } from '../shared-sync-scope.mjs';
|
|
@@ -48,7 +49,6 @@ function gitChangedFiles(projectDir, since) {
|
|
|
48
49
|
*/
|
|
49
50
|
|
|
50
51
|
export function runSync(projectDir, config, flags) {
|
|
51
|
-
if (flags.write) assertDefaultDocWrites(config);
|
|
52
52
|
// v0.28 (field report #10): `--tests` reconciles the hand-maintained TEST-SPEC
|
|
53
53
|
// Source-to-Test Map from disk (ghost-source removal + new co-located pairs) —
|
|
54
54
|
// a distinct path from the generated code-truth section refresh below.
|
|
@@ -62,6 +62,7 @@ export function runSync(projectDir, config, flags) {
|
|
|
62
62
|
const updates = []; // { doc, section, status }
|
|
63
63
|
const reviews = []; // { doc, section, reason }
|
|
64
64
|
const skipped = []; // { doc, reason }
|
|
65
|
+
const pendingWrites = [];
|
|
65
66
|
|
|
66
67
|
for (const doc of plan.docs) {
|
|
67
68
|
const full = resolve(projectDir, doc.path);
|
|
@@ -70,7 +71,11 @@ export function runSync(projectDir, config, flags) {
|
|
|
70
71
|
continue;
|
|
71
72
|
}
|
|
72
73
|
let content = readFileSync(full, 'utf-8');
|
|
73
|
-
|
|
74
|
+
const mapped = isMappedDocPath(config, doc.path);
|
|
75
|
+
if (apply && mapped && inspectSections(content).issues.length) {
|
|
76
|
+
throw new Error(`${doc.path}: malformed or duplicate docguard:section markers; no write was applied.`);
|
|
77
|
+
}
|
|
78
|
+
if (!hasGeneratedMarker(content) && !flags.force && !mapped) {
|
|
74
79
|
skipped.push({ doc: doc.path, reason: 'not marked docguard:generated (use --force to sync anyway)' });
|
|
75
80
|
continue;
|
|
76
81
|
}
|
|
@@ -98,7 +103,11 @@ export function runSync(projectDir, config, flags) {
|
|
|
98
103
|
}
|
|
99
104
|
codeSectionChanged = true;
|
|
100
105
|
updates.push({ doc: doc.path, section: sec.id, status: apply ? 'updated' : 'stale' });
|
|
101
|
-
if (apply) {
|
|
106
|
+
if (apply) {
|
|
107
|
+
if (mapped) assertOwnedCodeSection(content, sec.id, doc.path);
|
|
108
|
+
content = replaceSection(content, sec.id, sec.body).content;
|
|
109
|
+
docChanged = true;
|
|
110
|
+
}
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
// If code changed, the prose around it may need an agent's eyes.
|
|
@@ -110,9 +119,13 @@ export function runSync(projectDir, config, flags) {
|
|
|
110
119
|
}
|
|
111
120
|
}
|
|
112
121
|
|
|
113
|
-
if (apply && docChanged)
|
|
122
|
+
if (apply && docChanged) pendingWrites.push({ full, content });
|
|
114
123
|
}
|
|
115
124
|
|
|
125
|
+
// Authorization for every mapped target completed above. Only now expose
|
|
126
|
+
// writes, preserving backup behavior for both default and mapped layouts.
|
|
127
|
+
for (const pending of pendingWrites) safeWrite(pending.full, pending.content);
|
|
128
|
+
|
|
116
129
|
const result = {
|
|
117
130
|
project: config.projectName,
|
|
118
131
|
since: flags.since || null,
|
package/cli/commands/verify.mjs
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* text is the human summary.
|
|
21
21
|
*
|
|
22
22
|
* docguard verify [--semantic | --instructions] [--format json]
|
|
23
|
+
* @implements docguard.evidence-scoped-verification#FR-009
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
26
|
import { basename } from 'node:path';
|
|
@@ -29,6 +30,7 @@ import { extractSemanticClaims, buildSemanticVerifyTasks } from '../scanners/sem
|
|
|
29
30
|
import { auditInstructions } from '../scanners/instruction-audit.mjs';
|
|
30
31
|
import { isGitRepo, getDiffText } from '../shared-git.mjs';
|
|
31
32
|
import { parseUnifiedDiff, activityLabeledDiff } from '../shared-diff.mjs';
|
|
33
|
+
import { coverSemanticClaims, evaluateEvidence } from '../evidence/evaluate.mjs';
|
|
32
34
|
|
|
33
35
|
const CHANGE_CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift|scala|dart)$/;
|
|
34
36
|
|
|
@@ -68,14 +70,30 @@ function taskTouchesChange(task, changedSet, changedBasenames) {
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
export function runVerify(projectDir, config, flags) {
|
|
73
|
+
const selectedModes = ['semantic', 'instructions', 'evidence'].filter(mode => flags[mode]);
|
|
74
|
+
if (selectedModes.length > 1) {
|
|
75
|
+
const message = `Choose exactly one verify mode; --${selectedModes.join(', --')} cannot be combined.`;
|
|
76
|
+
if (flags.format === 'json') {
|
|
77
|
+
console.log(JSON.stringify({ command: 'verify', status: 'error', error: { code: 'VERIFY_MODE_CONFLICT', message } }, null, 2));
|
|
78
|
+
} else {
|
|
79
|
+
console.error(`${c.red}Error: ${message}${c.reset}`);
|
|
80
|
+
}
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
71
84
|
if (flags.instructions) {
|
|
72
85
|
runInstructionAudit(projectDir, config, flags);
|
|
73
86
|
return;
|
|
74
87
|
}
|
|
88
|
+
if (flags.evidence) {
|
|
89
|
+
runEvidenceVerification(projectDir, config, flags);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
75
92
|
|
|
76
93
|
const isJson = flags.format === 'json';
|
|
77
94
|
const claims = extractSemanticClaims(projectDir, config);
|
|
78
|
-
const
|
|
95
|
+
const evidenceCoverage = coverSemanticClaims(claims, evaluateEvidence(projectDir, config));
|
|
96
|
+
const tasks = buildSemanticVerifyTasks(evidenceCoverage.remaining);
|
|
79
97
|
|
|
80
98
|
// Change-aware staging (feat 6): if --since given, attach the structured diff
|
|
81
99
|
// and flag which claims are about just-changed code (verify those first).
|
|
@@ -92,6 +110,9 @@ export function runVerify(projectDir, config, flags) {
|
|
|
92
110
|
console.log(JSON.stringify({
|
|
93
111
|
command: 'verify --semantic',
|
|
94
112
|
project: config.projectName,
|
|
113
|
+
discoveredClaimCount: evidenceCoverage.total,
|
|
114
|
+
verifiedWithinScope: evidenceCoverage.verifiedWithinScope,
|
|
115
|
+
coveredClaimIds: evidenceCoverage.covered,
|
|
95
116
|
claimCount: tasks.length,
|
|
96
117
|
// How to act on this: each task is a claim to confirm against the code.
|
|
97
118
|
howToVerify: changeContext
|
|
@@ -107,7 +128,10 @@ export function runVerify(projectDir, config, flags) {
|
|
|
107
128
|
console.log(`${c.dim} ${config.projectName} · documented numbers / limits / enums to check against code${c.reset}\n`);
|
|
108
129
|
|
|
109
130
|
if (tasks.length === 0) {
|
|
110
|
-
|
|
131
|
+
const message = evidenceCoverage.total > 0
|
|
132
|
+
? `${evidenceCoverage.verifiedWithinScope} discovered claim(s) are already covered by unique current evidence declarations.`
|
|
133
|
+
: 'No semantic claims found in the canonical docs.';
|
|
134
|
+
console.log(` ${c.green}✅ ${message}${c.reset}`);
|
|
111
135
|
console.log(` ${c.dim}(Looks for numbers with units — days/ms/req-s/GSIs/roles/… — and status/enum lists.)${c.reset}\n`);
|
|
112
136
|
return;
|
|
113
137
|
}
|
|
@@ -120,6 +144,9 @@ export function runVerify(projectDir, config, flags) {
|
|
|
120
144
|
}
|
|
121
145
|
|
|
122
146
|
console.log(` ${c.yellow}${tasks.length} claim(s) to verify against the code:${c.reset}\n`);
|
|
147
|
+
if (evidenceCoverage.verifiedWithinScope > 0) {
|
|
148
|
+
console.log(` ${c.green}✓ ${evidenceCoverage.verifiedWithinScope} additional discovered claim(s) have unique verified-within-scope declarations.${c.reset}\n`);
|
|
149
|
+
}
|
|
123
150
|
if (changeContext) {
|
|
124
151
|
const nChanged = tasks.filter(t => t.aboutChangedCode).length;
|
|
125
152
|
console.log(` ${c.cyan}⚡ ${nChanged} claim(s) are about code changed since ${flags.since}${c.reset} ${c.dim}— verify these first (structured diff in --format json).${c.reset}\n`);
|
|
@@ -140,6 +167,42 @@ export function runVerify(projectDir, config, flags) {
|
|
|
140
167
|
console.log(` ${c.dim}Get the machine task list: ${c.cyan}${cmd}${c.dim}, then read each cited file and confirm the value.${c.reset}\n`);
|
|
141
168
|
}
|
|
142
169
|
|
|
170
|
+
function runEvidenceVerification(projectDir, config, flags) {
|
|
171
|
+
const evaluation = evaluateEvidence(projectDir, config);
|
|
172
|
+
if (flags.format === 'json') {
|
|
173
|
+
console.log(JSON.stringify(evaluation, null, 2));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
console.log(`${c.bold}🔬 DocGuard Verify — declared evidence${c.reset}`);
|
|
177
|
+
console.log(`${c.dim} ${config.projectName} · exact statement-to-source checks${c.reset}\n`);
|
|
178
|
+
if (!evaluation.exists) {
|
|
179
|
+
console.log(` ${c.dim}No .docguard-evidence.json manifest is configured.${c.reset}`);
|
|
180
|
+
console.log(` ${c.dim}Start from templates/evidence-manifest.json; heuristic discovery remains available with docguard verify --semantic.${c.reset}\n`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (evaluation.errors.length) {
|
|
184
|
+
console.log(` ${c.red}Invalid evidence manifest:${c.reset}`);
|
|
185
|
+
for (const error of evaluation.errors) console.log(` ${c.red}✗${c.reset} ${error.message}`);
|
|
186
|
+
console.log('');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const symbols = {
|
|
190
|
+
'verified-within-scope': `${c.green}✓${c.reset}`,
|
|
191
|
+
contradicted: `${c.red}✗${c.reset}`,
|
|
192
|
+
stale: `${c.yellow}↻${c.reset}`,
|
|
193
|
+
inconclusive: `${c.yellow}?${c.reset}`,
|
|
194
|
+
unsupported: `${c.yellow}◇${c.reset}`,
|
|
195
|
+
};
|
|
196
|
+
for (const state of ['contradicted', 'stale', 'inconclusive', 'unsupported', 'verified-within-scope']) {
|
|
197
|
+
const results = evaluation.results.filter(result => result.state === state);
|
|
198
|
+
if (!results.length) continue;
|
|
199
|
+
console.log(` ${c.bold}${state}${c.reset} (${results.length})`);
|
|
200
|
+
for (const result of results) console.log(` ${symbols[state]} ${result.declarationId} · ${result.location} · ${result.message}`);
|
|
201
|
+
console.log('');
|
|
202
|
+
}
|
|
203
|
+
console.log(` ${c.dim}${evaluation.scopeLimitation}${c.reset}\n`);
|
|
204
|
+
}
|
|
205
|
+
|
|
143
206
|
// ── verify --instructions: agent-instruction drift/conflict audit ───────────
|
|
144
207
|
|
|
145
208
|
function runInstructionAudit(projectDir, config, flags) {
|
package/cli/config.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { hasWorkerConfig } from './shared-source.mjs';
|
|
2
2
|
import { applyDocRoles } from './shared-doc-roles.mjs';
|
|
3
3
|
/**
|
|
4
|
+
* @implements docguard.evidence-scoped-verification#FR-010
|
|
4
5
|
* DocGuard — configuration loading.
|
|
5
6
|
*
|
|
6
7
|
* Extracted from docguard.mjs (v0.23.0) to break the demo.mjs → docguard.mjs
|
|
@@ -81,6 +82,7 @@ export function loadConfig(projectDir) {
|
|
|
81
82
|
freshness: true,
|
|
82
83
|
documentLifecycle: true,
|
|
83
84
|
specRegistry: true,
|
|
85
|
+
evidence: true,
|
|
84
86
|
// v0.31.0 — all three default ON. Soft (confidence:low, never break CI),
|
|
85
87
|
// heuristic (field cases require ongoing precision checks), and quiet when
|
|
86
88
|
// not applicable (no diff / no API-reference doc). api-doc-smells is
|
|
@@ -225,6 +227,7 @@ const _KNOWN_VALIDATORS = [
|
|
|
225
227
|
'apiSurface', 'metadataSync', 'docsCoverage', 'docQuality', 'todoTracking',
|
|
226
228
|
'schemaSync', 'specKit', 'crossReference', 'generatedStaleness',
|
|
227
229
|
'canonicalSync', 'surfaceSync', 'metricsConsistency',
|
|
230
|
+
'evidence',
|
|
228
231
|
];
|
|
229
232
|
|
|
230
233
|
function _kebabToCamel(k) {
|