docguard-cli 0.38.0 → 0.39.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 +2 -2
- package/cli/commands/feedback.mjs +147 -6
- package/cli/commands/specs.mjs +21 -2
- package/cli/docguard.mjs +15 -4
- package/cli/feedback-fixture.mjs +188 -0
- package/cli/validators/security.mjs +5 -4
- 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 +2 -2
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +1 -1
- package/package.json +2 -1
- package/schemas/docguard-benchmark.schema.json +84 -0
- package/schemas/docguard-feedback-fixture.schema.json +54 -0
- package/templates/ci/github-actions.yml +1 -1
- package/templates/feedback-fixture.json +18 -0
package/README.md
CHANGED
|
@@ -294,10 +294,10 @@ DocGuard ships **23 commands** (the "Daily 5" + 18 situational tools, including
|
|
|
294
294
|
| `explain <warning\|CODE>` | Paste any warning — or a finding code like `SEC001` — to get the validator's docstring, fix path, and how to suppress |
|
|
295
295
|
| `verify --semantic` | Extract documented numbers/limits/enums (retention days, rate limits, GSI/role counts, status enums) as a task list for an agent to check against code — the semantic-drift class regex/AST can't see |
|
|
296
296
|
| `verify --instructions` | Audit AGENTS.md/CLAUDE.md themselves for drift: duplicate rules, never-vs-always contradictions, stale file pointers, unknown commands — plus clustered rule pairs as agent judgment tasks |
|
|
297
|
-
| `feedback` |
|
|
297
|
+
| `feedback` | Review any finding or a synthetic false-positive/false-negative/unsupported fixture; verify its opposite control, reduce it deterministically, search open and closed duplicates, and optionally emit a test-only contribution. Nothing is submitted automatically. |
|
|
298
298
|
| `retire` | Find completed or superseded planning material (`--plan`/`--check`; `--fail-on-warning` gates advisory candidates) and explicitly remove clean tracked documentation from active AI context. `.docguard-archive.json` records recovery metadata and retired requirement identities, and `--retention-ref` proves the source revision remains reachable. This is separate from the Spec Kit Archive extension, which consolidates feature documents. |
|
|
299
299
|
| `reconcile` | Build a read-only code↔spec review graph since a Git ref. Classifies mechanical facts, approved intent, decisions, unrelated changes, and unsupported evidence; `--write` applies only mechanical generated-section refreshes. |
|
|
300
|
-
| `specs` | Maintain the versioned spec registry, preflight new specs, and apply evidence-gated completion transactions with bounded outcomes and active-context regeneration. |
|
|
300
|
+
| `specs` | Maintain the versioned spec registry, preflight new specs, and apply evidence-gated completion transactions with bounded outcomes and active-context regeneration. Verified living specs can record later reviewed maintenance without reopening or duplicating the specification. |
|
|
301
301
|
| `specs --check` / `specs --write` | Validate or refresh `.docguard-specs.json`, the byte-stable index of immutable spec IDs, reviewed lifecycle/lineage/scope, artifact digests, task state, explicitly scoped test evidence, and archive tombstones. Refreshes preserve the reviewed block. |
|
|
302
302
|
| `specs preflight [--path <spec>]` | Before specification, print current spec lifecycle and evidence. Before planning, check the generated draft for structural blockers and report semantic overlap as review-only evidence. |
|
|
303
303
|
| `mcp` | MCP server — exposes guard/score/explain/verify/report/diagnose as native tools for Claude, Cursor, and any MCP client. Stdio: `claude mcp add docguard -- npx docguard-cli mcp`. Team-shared HTTP: `docguard mcp --transport http --port 8585` (loopback by default; non-loopback binds require `--api-key`) |
|
|
@@ -12,15 +12,23 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Nothing is transmitted automatically. This command never scaffolds skills
|
|
14
14
|
* or edits source files. Zero npm dependencies — pure Node.js built-ins.
|
|
15
|
+
* @implements docguard.precision-evidence-loop#FR-012
|
|
16
|
+
* @implements docguard.precision-evidence-loop#FR-015
|
|
17
|
+
* @implements docguard.precision-evidence-loop#FR-016
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
|
-
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
18
|
-
import { resolve, dirname } from 'node:path';
|
|
20
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { resolve, dirname, join, relative, sep } from 'node:path';
|
|
22
|
+
import { tmpdir } from 'node:os';
|
|
19
23
|
import { fileURLToPath } from 'node:url';
|
|
20
24
|
import { c } from '../shared.mjs';
|
|
21
25
|
import { CODES } from '../findings.mjs';
|
|
22
26
|
import { safeWrite } from '../writers/generate-io.mjs';
|
|
23
27
|
import { runGuardInternal } from './guard.mjs';
|
|
28
|
+
import {
|
|
29
|
+
buildTestOnlyContribution, feedbackFindingIdentity, feedbackSearchUrls, parseFeedbackFixture,
|
|
30
|
+
reduceFixtureDeterministically,
|
|
31
|
+
} from '../feedback-fixture.mjs';
|
|
24
32
|
|
|
25
33
|
const _PKG = JSON.parse(
|
|
26
34
|
readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json'), 'utf-8')
|
|
@@ -39,16 +47,20 @@ function shortId(str) {
|
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
/** Shared output deliberately excludes every source-derived string. */
|
|
42
|
-
export function buildIssueUrl(finding) {
|
|
50
|
+
export function buildIssueUrl(finding, context = null) {
|
|
43
51
|
const code = Object.hasOwn(CODES, finding.code || '') ? finding.code : 'FINDING';
|
|
44
52
|
const validator = CODES[code]?.validator || 'unknown';
|
|
45
|
-
const
|
|
53
|
+
const classification = context?.classification || 'false_positive';
|
|
54
|
+
const title = `[feedback] ${code} (${validator}): ${classification.replaceAll('_', ' ')}`;
|
|
46
55
|
const confidence = ['high', 'medium', 'low'].includes(finding.confidence) ? finding.confidence : 'unknown';
|
|
47
56
|
const body = [
|
|
48
57
|
`DocGuard v${CLI_VERSION}`,
|
|
49
58
|
`- Code: ${code}`,
|
|
50
59
|
`- Validator: ${validator}`,
|
|
51
60
|
`- Confidence: ${confidence}`,
|
|
61
|
+
`- Classification: ${classification}`,
|
|
62
|
+
...(context?.parserTier ? [`- Parser tier: ${context.parserTier}`] : []),
|
|
63
|
+
...(context?.duplicateIdentity ? [`- Duplicate identity: ${context.duplicateIdentity}`] : []),
|
|
52
64
|
'',
|
|
53
65
|
'Expected behavior:',
|
|
54
66
|
'Actual behavior:',
|
|
@@ -60,12 +72,139 @@ export function buildIssueUrl(finding) {
|
|
|
60
72
|
'Generated by docguard feedback. No project paths, messages, source code, or secret values are included.',
|
|
61
73
|
].join('\n');
|
|
62
74
|
const url = `${ISSUES_BASE}/new?labels=docguard-feedback&title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
|
|
63
|
-
const query = `repo:raccioly/docguard ${code}`;
|
|
75
|
+
const query = context?.duplicateIdentity ? `repo:raccioly/docguard "${context.duplicateIdentity}"` : `repo:raccioly/docguard ${code}`;
|
|
64
76
|
const searchUrl = `https://github.com/search?q=${encodeURIComponent(query)}&type=issues`;
|
|
65
77
|
return { url: url.length <= URL_CAP ? url : `${ISSUES_BASE}/new`, title, searchUrl };
|
|
66
78
|
}
|
|
67
79
|
|
|
80
|
+
const FEEDBACK_CLASSES = new Set(['false_positive', 'false_negative', 'unsupported_syntax', 'ambiguous', 'policy_disagreement']);
|
|
81
|
+
|
|
82
|
+
function projectFile(projectDir, value, label) {
|
|
83
|
+
if (typeof value !== 'string' || !value || value.includes('\\')) throw new Error(`${label} must be a project-relative path.`);
|
|
84
|
+
const root = resolve(projectDir);
|
|
85
|
+
const path = resolve(root, value);
|
|
86
|
+
const rel = relative(root, path);
|
|
87
|
+
if (!rel || rel.startsWith(`..${sep}`) || rel === '..' || rel.split(sep).some(part => ['.git', '.local'].includes(part.toLowerCase()))) {
|
|
88
|
+
throw new Error(`${label} must remain in the project and outside protected directories.`);
|
|
89
|
+
}
|
|
90
|
+
return path;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function evaluateFileSet(manifest, fileSet) {
|
|
94
|
+
const root = mkdtempSync(join(tmpdir(), 'docguard-feedback-fixture-'));
|
|
95
|
+
try {
|
|
96
|
+
for (const file of fileSet.files) {
|
|
97
|
+
const target = resolve(root, file.path);
|
|
98
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
99
|
+
writeFileSync(target, file.content, 'utf8');
|
|
100
|
+
}
|
|
101
|
+
const report = runGuardInternal(root, manifest.config);
|
|
102
|
+
const validator = (report.validators || []).find(item => item.key === manifest.detector.validator);
|
|
103
|
+
return {
|
|
104
|
+
identities: (report.findings || []).filter(item => item.code === manifest.detector.code).map(feedbackFindingIdentity).sort(),
|
|
105
|
+
applicability: validator?.applicability?.status || 'unknown',
|
|
106
|
+
};
|
|
107
|
+
} finally {
|
|
108
|
+
rmSync(root, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function predicateMatches(manifest, result) {
|
|
113
|
+
if (manifest.interestingness.predicate === 'finding_present') return result.identities.includes(manifest.expectedIdentity);
|
|
114
|
+
if (manifest.interestingness.predicate === 'finding_absent') return !result.identities.includes(manifest.expectedIdentity);
|
|
115
|
+
return result.applicability === 'unsupported';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function oppositeMatches(manifest, result) {
|
|
119
|
+
if (manifest.interestingness.predicate === 'finding_present') return result.identities.includes(manifest.expectedIdentity);
|
|
120
|
+
if (manifest.interestingness.predicate === 'finding_absent') return !result.identities.includes(manifest.expectedIdentity);
|
|
121
|
+
return result.applicability === 'checked';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function runFixtureFeedback(projectDir, flags) {
|
|
125
|
+
let manifest;
|
|
126
|
+
try {
|
|
127
|
+
const manifestPath = projectFile(projectDir, flags.fixtureManifest, '--fixture-manifest');
|
|
128
|
+
if (!existsSync(manifestPath) || lstatSync(manifestPath).isSymbolicLink()) throw new Error('Fixture manifest must be a regular non-symlink file.');
|
|
129
|
+
const text = readFileSync(manifestPath, 'utf8');
|
|
130
|
+
if (Buffer.byteLength(text) > 262_144) throw new Error('Fixture manifest exceeds 256 KiB.');
|
|
131
|
+
manifest = parseFeedbackFixture(JSON.parse(text));
|
|
132
|
+
if (flags.classification && flags.classification !== manifest.classification) throw new Error('--classification disagrees with the fixture manifest.');
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.log(JSON.stringify({ status: 'ERROR', error: error.message }, null, 2));
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const initial = evaluateFileSet(manifest, manifest.fixture);
|
|
140
|
+
const control = evaluateFileSet(manifest, manifest.oppositeControl);
|
|
141
|
+
const reproduced = predicateMatches(manifest, initial);
|
|
142
|
+
const controlConfirmed = oppositeMatches(manifest, control);
|
|
143
|
+
if (!reproduced || !controlConfirmed) {
|
|
144
|
+
console.log(JSON.stringify({
|
|
145
|
+
status: 'NOT_REPRODUCED', classification: manifest.classification,
|
|
146
|
+
reproductionConfirmed: reproduced, controlConfirmed,
|
|
147
|
+
message: 'The explicit predicate and opposite control must both reproduce before reduction or contribution.',
|
|
148
|
+
}, null, 2));
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let reduction = null;
|
|
154
|
+
if (flags.reduce) {
|
|
155
|
+
reduction = reduceFixtureDeterministically(manifest, candidate => predicateMatches(candidate, evaluateFileSet(candidate, candidate.fixture)));
|
|
156
|
+
manifest = reduction.manifest;
|
|
157
|
+
}
|
|
158
|
+
const searches = feedbackSearchUrls(manifest, ISSUES_BASE);
|
|
159
|
+
const issue = buildIssueUrl({ code: manifest.detector.code, confidence: 'unknown' }, {
|
|
160
|
+
classification: manifest.classification, parserTier: manifest.parserTier, duplicateIdentity: searches.identity,
|
|
161
|
+
});
|
|
162
|
+
const feedbackDir = resolve(projectDir, '.docguard', 'feedback');
|
|
163
|
+
let record = null;
|
|
164
|
+
let contribution = null;
|
|
165
|
+
let contributionPreview = null;
|
|
166
|
+
try {
|
|
167
|
+
if (!flags.preview) {
|
|
168
|
+
record = resolve(feedbackDir, `${searches.identity}.fixture.json`);
|
|
169
|
+
safeWrite(record, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
170
|
+
}
|
|
171
|
+
if (flags.contribution) {
|
|
172
|
+
if (!/^tests\/[^/]+\.test\.mjs$/.test(flags.contribution)) {
|
|
173
|
+
throw new Error('--contribution must be a direct tests/<name>.test.mjs path.');
|
|
174
|
+
}
|
|
175
|
+
contributionPreview = buildTestOnlyContribution(manifest);
|
|
176
|
+
if (!flags.preview) {
|
|
177
|
+
contribution = projectFile(projectDir, flags.contribution, '--contribution');
|
|
178
|
+
safeWrite(contribution, contributionPreview);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.log(JSON.stringify({ status: 'ERROR', error: error.message }, null, 2));
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const output = {
|
|
187
|
+
status: 'READY', preview: Boolean(flags.preview), classification: manifest.classification,
|
|
188
|
+
reproductionConfirmed: true, controlConfirmed: true, reduction: reduction ? { status: reduction.status, attempts: reduction.attempts } : null,
|
|
189
|
+
duplicateIdentity: searches.identity, searchUrls: searches, issueUrl: issue.url,
|
|
190
|
+
record: record ? relative(projectDir, record).split(sep).join('/') : null,
|
|
191
|
+
contribution: contribution ? relative(projectDir, contribution).split(sep).join('/') : null,
|
|
192
|
+
contributionPreview: flags.preview && flags.contribution ? contributionPreview : undefined,
|
|
193
|
+
};
|
|
194
|
+
console.log(JSON.stringify(output, null, 2));
|
|
195
|
+
}
|
|
196
|
+
|
|
68
197
|
export function runFeedback(projectDir, config, flags) {
|
|
198
|
+
if (flags.fixtureManifest) return runFixtureFeedback(projectDir, flags);
|
|
199
|
+
const classification = flags.classification || 'false_positive';
|
|
200
|
+
if (!FEEDBACK_CLASSES.has(classification) || classification === 'false_negative' || classification === 'unsupported_syntax') {
|
|
201
|
+
const error = classification === 'false_negative' || classification === 'unsupported_syntax'
|
|
202
|
+
? 'This classification requires --fixture-manifest with an explicit expected identity and opposite control.'
|
|
203
|
+
: 'Unknown feedback classification.';
|
|
204
|
+
if (flags.format === 'json') console.log(JSON.stringify({ error, reportable: [] })); else console.error(error);
|
|
205
|
+
process.exitCode = 1;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
69
208
|
const data = runGuardInternal(projectDir, config);
|
|
70
209
|
const selectedCode = typeof flags.code === 'string' ? flags.code.toUpperCase() : null;
|
|
71
210
|
if (flags.code !== undefined && (!selectedCode || !Object.hasOwn(CODES, selectedCode))) {
|
|
@@ -93,7 +232,7 @@ export function runFeedback(projectDir, config, flags) {
|
|
|
93
232
|
const feedbackDir = resolve(projectDir, '.docguard', 'feedback');
|
|
94
233
|
const items = reportable.map((f) => {
|
|
95
234
|
const id = shortId(`${f.code}|${f.location || f.message}`);
|
|
96
|
-
const { url, title, searchUrl } = buildIssueUrl(f);
|
|
235
|
+
const { url, title, searchUrl } = buildIssueUrl(f, { classification });
|
|
97
236
|
const fileName = `${(f.code || 'finding').toLowerCase()}-${id}.json`;
|
|
98
237
|
const filePath = resolve(feedbackDir, fileName);
|
|
99
238
|
return { finding: f, id, url, title, searchUrl, fileName, filePath, saved: false, error: null };
|
|
@@ -105,6 +244,7 @@ export function runFeedback(projectDir, config, flags) {
|
|
|
105
244
|
if (!existsSync(feedbackDir)) mkdirSync(feedbackDir, { recursive: true });
|
|
106
245
|
safeWrite(it.filePath, JSON.stringify({
|
|
107
246
|
capturedBy: `docguard feedback (v${CLI_VERSION})`,
|
|
247
|
+
classification,
|
|
108
248
|
finding: it.finding,
|
|
109
249
|
issueUrl: it.url,
|
|
110
250
|
searchUrl: it.searchUrl,
|
|
@@ -123,6 +263,7 @@ export function runFeedback(projectDir, config, flags) {
|
|
|
123
263
|
console.log(JSON.stringify({
|
|
124
264
|
version: CLI_VERSION,
|
|
125
265
|
preview: Boolean(flags.preview),
|
|
266
|
+
classification,
|
|
126
267
|
reportable: items.map(it => ({
|
|
127
268
|
code: it.finding.code,
|
|
128
269
|
location: it.finding.location,
|
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/docguard.mjs
CHANGED
|
@@ -301,10 +301,10 @@ const COMMAND_HELP = {
|
|
|
301
301
|
examples: ['docguard memory', 'docguard memory --diff'],
|
|
302
302
|
},
|
|
303
303
|
feedback: {
|
|
304
|
-
summary: 'Review detection feedback locally
|
|
305
|
-
usage: 'docguard feedback [--code <CODE> | --all] [--preview] [--format json]',
|
|
306
|
-
flags: [['--code <CODE>', 'Select a finding regardless of confidence'], ['--all', 'Select every active finding'], ['--preview', 'Skip local
|
|
307
|
-
examples: ['docguard feedback', 'docguard feedback --code TRC005 --preview', 'docguard feedback --
|
|
304
|
+
summary: 'Review detection feedback locally, or validate and reduce a synthetic fixture manifest. Duplicate searches cover open and closed work; nothing is submitted automatically.',
|
|
305
|
+
usage: 'docguard feedback [--code <CODE> | --all] [--classification <class>] [--fixture-manifest <path> [--reduce] [--contribution <path>]] [--preview] [--format json]',
|
|
306
|
+
flags: [['--code <CODE>', 'Select a finding regardless of confidence'], ['--all', 'Select every active finding'], ['--classification <class>', 'false_positive, false_negative, unsupported_syntax, ambiguous, or policy_disagreement'], ['--fixture-manifest <path>', 'Validate a reviewed synthetic fixture and opposite control'], ['--reduce', 'Deterministically reduce a reproducing fixture'], ['--contribution <path>', 'Write a test-only contribution when required evidence is present'], ['--preview', 'Skip local writes and return contribution text inline'], ['--format json', 'Machine-readable evidence and issue/search URLs']],
|
|
307
|
+
examples: ['docguard feedback', 'docguard feedback --code TRC005 --preview', 'docguard feedback --fixture-manifest feedback.json --reduce --preview --format json'],
|
|
308
308
|
},
|
|
309
309
|
verify: {
|
|
310
310
|
summary: 'Extract the semantic claims in your canonical docs — documented numbers, limits, and enums (retention days, rate limits, GSI/role counts, status enums) — as a verification task list the agent checks against the code. This is the highest-value bug class (a doc value that drifted from code) and the one regex/AST cannot judge. DocGuard finds the claims; the LLM confirms them.',
|
|
@@ -626,6 +626,17 @@ async function main() {
|
|
|
626
626
|
flags.all = true;
|
|
627
627
|
} else if (args[i] === '--preview') {
|
|
628
628
|
flags.preview = true;
|
|
629
|
+
} else if (args[i] === '--classification' && args[i + 1]) {
|
|
630
|
+
flags.classification = args[i + 1].replaceAll('-', '_');
|
|
631
|
+
i++;
|
|
632
|
+
} else if (args[i] === '--fixture-manifest' && args[i + 1]) {
|
|
633
|
+
flags.fixtureManifest = args[i + 1];
|
|
634
|
+
i++;
|
|
635
|
+
} else if (args[i] === '--reduce') {
|
|
636
|
+
flags.reduce = true;
|
|
637
|
+
} else if (args[i] === '--contribution' && args[i + 1]) {
|
|
638
|
+
flags.contribution = args[i + 1];
|
|
639
|
+
i++;
|
|
629
640
|
} else if (args[i] === '--signals') {
|
|
630
641
|
flags.signals = true;
|
|
631
642
|
} else if (args[i] === '--debate') {
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict, privacy-bounded feedback fixtures and deterministic reduction.
|
|
3
|
+
* @implements docguard.precision-evidence-loop#FR-011
|
|
4
|
+
* @implements docguard.precision-evidence-loop#FR-013
|
|
5
|
+
* @implements docguard.precision-evidence-loop#FR-014
|
|
6
|
+
* @implements docguard.precision-evidence-loop#FR-015
|
|
7
|
+
* @implements docguard.precision-evidence-loop#FR-016
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { extname, isAbsolute } from 'node:path';
|
|
12
|
+
|
|
13
|
+
export const FEEDBACK_SCHEMA_URL = 'https://raccioly.github.io/docguard/schemas/docguard-feedback-fixture.schema.json';
|
|
14
|
+
const CLASSIFICATIONS = new Set(['false_positive', 'false_negative', 'unsupported_syntax', 'ambiguous', 'policy_disagreement']);
|
|
15
|
+
const PARSER_TIERS = new Set(['js-ast', 'py-ast', 'regex-fallback', 'fallback-language', 'mixed', 'not-applicable']);
|
|
16
|
+
const PREDICATES = new Set(['finding_present', 'finding_absent', 'validator_unsupported']);
|
|
17
|
+
const ROOT_KEYS = new Set(['$schema', 'schemaVersion', 'classification', 'detector', 'parserTier', 'config', 'expectedIdentity', 'interestingness', 'fixture', 'oppositeControl', 'provenance', 'contribution']);
|
|
18
|
+
|
|
19
|
+
function object(value, label) {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function exactKeys(value, keys, label) {
|
|
25
|
+
const unknown = Object.keys(value).filter(key => !keys.has(key));
|
|
26
|
+
if (unknown.length) throw new Error(`${label} has unknown field(s): ${unknown.join(', ')}.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function safePath(value, label) {
|
|
30
|
+
if (typeof value !== 'string' || !value || value.includes('\\') || isAbsolute(value)) throw new Error(`${label} must be a POSIX relative path.`);
|
|
31
|
+
const parts = value.split('/');
|
|
32
|
+
if (parts.some(part => !part || part === '.' || part === '..' || ['.git', '.local'].includes(part.toLowerCase()))) {
|
|
33
|
+
throw new Error(`${label} enters a protected or escaping path.`);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateConfig(value, label = 'config', depth = 0) {
|
|
39
|
+
if (depth > 6) throw new Error(`${label} is too deeply nested.`);
|
|
40
|
+
if (value === null || typeof value === 'boolean' || Number.isFinite(value)) return;
|
|
41
|
+
if (typeof value === 'string') {
|
|
42
|
+
if (value.length > 10_000 || value.includes('\0') || isAbsolute(value) || /(?:^|[/\\])\.\.(?:[/\\]|$)/.test(value) || value.includes('.local') || value.includes('.git')) {
|
|
43
|
+
throw new Error(`${label} contains an unsafe value.`);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
if (value.length > 100) throw new Error(`${label} is too large.`);
|
|
49
|
+
value.forEach((item, index) => validateConfig(item, `${label}[${index}]`, depth + 1));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
object(value, label);
|
|
53
|
+
const keys = Object.keys(value);
|
|
54
|
+
if (keys.length > 100 || keys.some(key => ['__proto__', 'prototype', 'constructor'].includes(key))) throw new Error(`${label} has unsafe keys.`);
|
|
55
|
+
for (const key of keys) validateConfig(value[key], `${label}.${key}`, depth + 1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseFiles(value, label) {
|
|
59
|
+
const holder = object(value, label);
|
|
60
|
+
exactKeys(holder, new Set(['files']), label);
|
|
61
|
+
if (!Array.isArray(holder.files) || holder.files.length < 1 || holder.files.length > 16) throw new Error(`${label}.files requires 1-16 files.`);
|
|
62
|
+
let bytes = 0;
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const files = holder.files.map((entry, index) => {
|
|
65
|
+
object(entry, `${label}.files[${index}]`);
|
|
66
|
+
exactKeys(entry, new Set(['path', 'content']), `${label}.files[${index}]`);
|
|
67
|
+
const path = safePath(entry.path, `${label}.files[${index}].path`);
|
|
68
|
+
if (seen.has(path)) throw new Error(`${label}.files contains duplicate path ${path}.`);
|
|
69
|
+
seen.add(path);
|
|
70
|
+
if (typeof entry.content !== 'string' || entry.content.includes('\0')) throw new Error(`${label}.files[${index}].content must be text.`);
|
|
71
|
+
bytes += Buffer.byteLength(entry.content);
|
|
72
|
+
return { path, content: entry.content };
|
|
73
|
+
});
|
|
74
|
+
if (bytes > 131_072) throw new Error(`${label} exceeds 128 KiB.`);
|
|
75
|
+
return { files };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseContribution(value) {
|
|
79
|
+
if (value === undefined) return null;
|
|
80
|
+
const contribution = object(value, 'contribution');
|
|
81
|
+
exactKeys(contribution, new Set(['testOnly', 'scopeDocumented', 'benchmarkDelta']), 'contribution');
|
|
82
|
+
const delta = object(contribution.benchmarkDelta, 'contribution.benchmarkDelta');
|
|
83
|
+
exactKeys(delta, new Set(['falsePositives', 'falseNegatives', 'unsupportedCases', 'abstainedSupportedCases']), 'contribution.benchmarkDelta');
|
|
84
|
+
for (const [key, count] of Object.entries(delta)) {
|
|
85
|
+
if (!Number.isInteger(count) || count < 0) throw new Error(`contribution.benchmarkDelta.${key} must be a non-negative integer.`);
|
|
86
|
+
}
|
|
87
|
+
return { testOnly: contribution.testOnly === true, scopeDocumented: contribution.scopeDocumented === true, benchmarkDelta: delta };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function parseFeedbackFixture(value) {
|
|
91
|
+
const root = object(value, 'feedback fixture');
|
|
92
|
+
exactKeys(root, ROOT_KEYS, 'feedback fixture');
|
|
93
|
+
if (root.$schema !== FEEDBACK_SCHEMA_URL || root.schemaVersion !== 1) throw new Error('Feedback fixture uses an unsupported schema contract.');
|
|
94
|
+
if (!CLASSIFICATIONS.has(root.classification)) throw new Error('feedback fixture classification is invalid.');
|
|
95
|
+
if (!PARSER_TIERS.has(root.parserTier)) throw new Error('feedback fixture parserTier is invalid.');
|
|
96
|
+
const detector = object(root.detector, 'detector');
|
|
97
|
+
exactKeys(detector, new Set(['code', 'validator']), 'detector');
|
|
98
|
+
if (!/^[A-Z]{3}\d{3}$/.test(detector.code) || !/^[A-Za-z][A-Za-z0-9]{1,63}$/.test(detector.validator)) throw new Error('detector code or validator is invalid.');
|
|
99
|
+
const match = String(root.expectedIdentity || '').match(/^([A-Z]{3}\d{3})@(.+)$/);
|
|
100
|
+
if (!match || match[1] !== detector.code) throw new Error('expectedIdentity must use the detector code.');
|
|
101
|
+
const expectedPath = safePath(match[2], 'expectedIdentity path');
|
|
102
|
+
const interestingness = object(root.interestingness, 'interestingness');
|
|
103
|
+
exactKeys(interestingness, new Set(['predicate']), 'interestingness');
|
|
104
|
+
if (!PREDICATES.has(interestingness.predicate)) throw new Error('interestingness.predicate is invalid.');
|
|
105
|
+
const fixture = parseFiles(root.fixture, 'fixture');
|
|
106
|
+
const oppositeControl = parseFiles(root.oppositeControl, 'oppositeControl');
|
|
107
|
+
const fixturePaths = fixture.files.map(file => file.path).sort();
|
|
108
|
+
const controlPaths = oppositeControl.files.map(file => file.path).sort();
|
|
109
|
+
if (JSON.stringify(fixturePaths) !== JSON.stringify(controlPaths) || !fixturePaths.includes(expectedPath)) {
|
|
110
|
+
throw new Error('fixture and oppositeControl must share paths and include the expectedIdentity path.');
|
|
111
|
+
}
|
|
112
|
+
const provenance = object(root.provenance, 'provenance');
|
|
113
|
+
exactKeys(provenance, new Set(['synthetic', 'redactionAttested']), 'provenance');
|
|
114
|
+
if (provenance.synthetic !== true || provenance.redactionAttested !== true) throw new Error('Synthetic provenance and reviewed redaction must both be attested.');
|
|
115
|
+
validateConfig(root.config);
|
|
116
|
+
return {
|
|
117
|
+
$schema: FEEDBACK_SCHEMA_URL, schemaVersion: 1, classification: root.classification,
|
|
118
|
+
detector: { code: detector.code, validator: detector.validator }, parserTier: root.parserTier,
|
|
119
|
+
config: root.config, expectedIdentity: `${detector.code}@${expectedPath}`,
|
|
120
|
+
interestingness: { predicate: interestingness.predicate }, fixture, oppositeControl,
|
|
121
|
+
provenance: { synthetic: true, redactionAttested: true }, contribution: parseContribution(root.contribution),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function shape(files) {
|
|
126
|
+
return files.map(file => {
|
|
127
|
+
const normalized = file.content
|
|
128
|
+
.replace(/(['"`])(?:\\.|(?!\1).)*\1/g, ' STRING ')
|
|
129
|
+
.replace(/\b\d+(?:\.\d+)?\b/g, ' NUMBER ')
|
|
130
|
+
.replace(/\b[A-Za-z_$][\w$]*\b/g, ' ID ')
|
|
131
|
+
.replace(/\s+/g, ' ').trim();
|
|
132
|
+
return `${extname(file.path).toLowerCase()}:${normalized}`;
|
|
133
|
+
}).sort().join('|');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function feedbackDuplicateIdentity(manifest) {
|
|
137
|
+
const input = [manifest.detector.code, manifest.classification, manifest.parserTier, shape(manifest.fixture.files)].join('|');
|
|
138
|
+
return `dgf-${createHash('sha256').update(input).digest('hex').slice(0, 16)}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function feedbackFindingIdentity(finding) {
|
|
142
|
+
const raw = typeof finding.location === 'string' ? finding.location : finding.location?.file || '<project>';
|
|
143
|
+
return `${finding.code}@${raw.replace(/:\d+(?::\d+)?$/, '').replaceAll('\\', '/')}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function feedbackSearchUrls(manifest, issuesBase = 'https://github.com/raccioly/docguard/issues') {
|
|
147
|
+
const repository = issuesBase.replace(/^https:\/\/github\.com\//, '').replace(/\/issues.*$/, '');
|
|
148
|
+
const identity = feedbackDuplicateIdentity(manifest);
|
|
149
|
+
const build = state => `https://github.com/search?q=${encodeURIComponent(`repo:${repository} "${identity}" state:${state}`)}&type=issues`;
|
|
150
|
+
return { identity, all: build('open').replace(encodeURIComponent(' state:open'), ''), open: build('open'), closed: build('closed') };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function reduceFixtureDeterministically(manifest, interesting, maxAttempts = 100) {
|
|
154
|
+
let current = structuredClone(manifest);
|
|
155
|
+
let attempts = 0;
|
|
156
|
+
if (!interesting(current)) return { status: 'NOT_REPRODUCED', attempts, manifest: current };
|
|
157
|
+
for (let fileIndex = 0; fileIndex < current.fixture.files.length && attempts < maxAttempts; fileIndex++) {
|
|
158
|
+
let changed = true;
|
|
159
|
+
while (changed && attempts < maxAttempts) {
|
|
160
|
+
changed = false;
|
|
161
|
+
const lines = current.fixture.files[fileIndex].content.split('\n');
|
|
162
|
+
if (lines.length <= 1) break;
|
|
163
|
+
for (let line = 0; line < lines.length && attempts < maxAttempts; line++) {
|
|
164
|
+
const candidate = structuredClone(current);
|
|
165
|
+
candidate.fixture.files[fileIndex].content = lines.filter((_, index) => index !== line).join('\n');
|
|
166
|
+
attempts++;
|
|
167
|
+
if (interesting(candidate)) { current = candidate; changed = true; break; }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { status: attempts >= maxAttempts ? 'REDUCED_LIMIT' : 'REDUCED', attempts, manifest: current };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function assertContributionReady(manifest) {
|
|
175
|
+
if (!['false_positive', 'false_negative', 'unsupported_syntax'].includes(manifest.classification)) {
|
|
176
|
+
throw new Error('Ambiguous and policy-disagreement fixtures require adjudication before a test contribution.');
|
|
177
|
+
}
|
|
178
|
+
if (!manifest.contribution?.testOnly || !manifest.contribution.scopeDocumented) {
|
|
179
|
+
throw new Error('Contribution requires testOnly and scopeDocumented attestations plus benchmarkDelta.');
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function buildTestOnlyContribution(manifest) {
|
|
185
|
+
assertContributionReady(manifest);
|
|
186
|
+
const encoded = JSON.stringify(manifest);
|
|
187
|
+
return `import { test } from 'node:test';\nimport assert from 'node:assert/strict';\nimport { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { runGuardInternal } from '../cli/commands/guard.mjs';\nimport { feedbackFindingIdentity } from '../cli/feedback-fixture.mjs';\n\nconst manifest = ${encoded};\nfunction run(files) {\n const root = mkdtempSync(join(tmpdir(), 'docguard-contribution-'));\n try {\n for (const file of files) { const target = join(root, file.path); mkdirSync(dirname(target), { recursive: true }); writeFileSync(target, file.content); }\n return runGuardInternal(root, manifest.config);\n } finally { rmSync(root, { recursive: true, force: true }); }\n}\n\ntest('${manifest.detector.code} ${manifest.classification} synthetic reproduction (${feedbackDuplicateIdentity(manifest)})', () => {\n const fixture = run(manifest.fixture.files);\n const control = run(manifest.oppositeControl.files);\n const fixtureHas = fixture.findings.some(finding => feedbackFindingIdentity(finding) === manifest.expectedIdentity);\n const controlHas = control.findings.some(finding => feedbackFindingIdentity(finding) === manifest.expectedIdentity);\n const fixtureApplicability = fixture.validators.find(item => item.key === manifest.detector.validator)?.applicability?.status;\n const controlApplicability = control.validators.find(item => item.key === manifest.detector.validator)?.applicability?.status;\n ${manifest.classification === 'false_positive' ? 'assert.equal(fixtureHas, false); assert.equal(controlHas, true);' : manifest.classification === 'false_negative' ? 'assert.equal(fixtureHas, true); assert.equal(controlHas, false);' : `assert.notEqual(fixtureApplicability, 'unsupported'); assert.equal(controlApplicability, 'checked'); assert.equal(controlHas, false);`}\n});\n`;
|
|
188
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Respects config.securityIgnore (glob patterns) and config.ignore (global).
|
|
5
5
|
* Uses shared-ignore.mjs for consistent filtering (Constitution IV, v1.1.0).
|
|
6
|
+
* @implements docguard.precision-evidence-loop#FR-003
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
@@ -35,10 +36,10 @@ const IGNORE_DIRS = new Set([
|
|
|
35
36
|
|
|
36
37
|
// Patterns that might indicate hardcoded secrets
|
|
37
38
|
const SECRET_PATTERNS = [
|
|
38
|
-
{ pattern: /(?:password|passwd|pwd)
|
|
39
|
-
{ pattern: /(?:api[_-]?key|apikey)
|
|
40
|
-
{ pattern: /(?:secret[_-]?key|secretkey)
|
|
41
|
-
{ pattern: /(?:access[_-]?token|accesstoken)
|
|
39
|
+
{ pattern: /(?:password|passwd|pwd)\??\s*(?:=\s*|:\s*(?:[^'";=\n]+?=\s*)?)['"][^'"]{8,}['"]/gi, label: 'hardcoded password' },
|
|
40
|
+
{ pattern: /(?:api[_-]?key|apikey)\??\s*(?:=\s*|:\s*(?:[^'";=\n]+?=\s*)?)['"][^'"]{16,}['"]/gi, label: 'hardcoded API key' },
|
|
41
|
+
{ pattern: /(?:secret[_-]?key|secretkey)\??\s*(?:=\s*|:\s*(?:[^'";=\n]+?=\s*)?)['"][^'"]{16,}['"]/gi, label: 'hardcoded secret key' },
|
|
42
|
+
{ pattern: /(?:access[_-]?token|accesstoken)\??\s*(?:=\s*|:\s*(?:[^'";=\n]+?=\s*)?)['"][^'"]{16,}['"]/gi, label: 'hardcoded access token' },
|
|
42
43
|
{ pattern: /AKIA[0-9A-Z]{16}/g, label: 'AWS Access Key ID' },
|
|
43
44
|
{ pattern: /(?:sk-|sk_live_|sk_test_)[a-zA-Z0-9]{20,}/g, label: 'API secret key (Stripe/OpenAI pattern)' },
|
|
44
45
|
];
|
|
@@ -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.39.0"
|
|
7
7
|
description: "Canonical-Driven Development enforcement as a true spec-kit extension. LLM-first design with automated validators, 5 AI behavior skills, spec-kit skill chaining, and workflow hooks. One pinned runtime dependency (@babel/parser); pure Node.js otherwise."
|
|
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.39.0
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-fix
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.39.0 -->
|
|
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.39.0
|
|
11
11
|
source: extensions/spec-kit-docguard/skills/docguard-guard
|
|
12
12
|
---
|
|
13
|
-
<!-- docguard:version: 0.
|
|
13
|
+
<!-- docguard:version: 0.39.0 -->
|
|
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.39.0
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-review
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.39.0 -->
|
|
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.39.0
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-score
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.39.0 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Score Skill
|
|
15
15
|
|
|
@@ -4,10 +4,10 @@ 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.39.0
|
|
8
8
|
source: extensions/spec-kit-docguard/skills/docguard-sync
|
|
9
9
|
---
|
|
10
|
-
<!-- docguard:version: 0.
|
|
10
|
+
<!-- docguard:version: 0.39.0 -->
|
|
11
11
|
|
|
12
12
|
# DocGuard Sync Skill
|
|
13
13
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docguard-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
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": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"guard": "node cli/docguard.mjs guard",
|
|
13
13
|
"init": "node cli/docguard.mjs init",
|
|
14
14
|
"score": "node cli/docguard.mjs score",
|
|
15
|
+
"benchmark": "node benchmarks/run.mjs",
|
|
15
16
|
"diff": "node cli/docguard.mjs diff",
|
|
16
17
|
"generate": "node cli/docguard.mjs generate",
|
|
17
18
|
"hooks": "node cli/docguard.mjs hooks",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raccioly.github.io/docguard/schemas/docguard-benchmark.schema.json",
|
|
4
|
+
"title": "DocGuard benchmark corpus",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["$schema", "schemaVersion", "cases"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "const": "https://raccioly.github.io/docguard/schemas/docguard-benchmark.schema.json" },
|
|
10
|
+
"schemaVersion": { "const": 1 },
|
|
11
|
+
"cases": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"minItems": 1,
|
|
14
|
+
"items": { "$ref": "#/$defs/case" }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"$defs": {
|
|
18
|
+
"identity": { "type": "string", "pattern": "^[A-Z]{3}[0-9]{3}@[^/\\\\][^\\\\]*$" },
|
|
19
|
+
"source": {
|
|
20
|
+
"oneOf": [
|
|
21
|
+
{
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"required": ["kind", "path"],
|
|
25
|
+
"properties": {
|
|
26
|
+
"kind": { "const": "fixture" },
|
|
27
|
+
"path": { "type": "string", "pattern": "^fixtures/" }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "object",
|
|
32
|
+
"additionalProperties": false,
|
|
33
|
+
"required": ["kind", "url", "revision", "license"],
|
|
34
|
+
"properties": {
|
|
35
|
+
"kind": { "const": "git" },
|
|
36
|
+
"url": { "type": "string", "pattern": "^https://[^@]+\\.git$" },
|
|
37
|
+
"revision": { "type": "string", "pattern": "^[0-9a-f]{40}([0-9a-f]{24})?$" },
|
|
38
|
+
"license": { "type": "string", "minLength": 1, "maxLength": 128 }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"mutation": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"additionalProperties": false,
|
|
46
|
+
"required": ["path", "find", "replace", "expectedOccurrences"],
|
|
47
|
+
"properties": {
|
|
48
|
+
"path": { "type": "string", "minLength": 1 },
|
|
49
|
+
"find": { "type": "string", "minLength": 1, "maxLength": 20000 },
|
|
50
|
+
"replace": { "type": "string", "maxLength": 20000 },
|
|
51
|
+
"expectedOccurrences": { "type": "integer", "minimum": 1, "maximum": 20 }
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"case": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"additionalProperties": false,
|
|
57
|
+
"required": ["id", "split", "repositoryGroup", "causalFamily", "parserTier", "classification", "source", "scope", "expected", "forbidden", "repairOutcome"],
|
|
58
|
+
"properties": {
|
|
59
|
+
"id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{2,127}$" },
|
|
60
|
+
"split": { "enum": ["development", "evaluation"] },
|
|
61
|
+
"repositoryGroup": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
62
|
+
"causalFamily": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
63
|
+
"parserTier": { "enum": ["js-ast", "py-ast", "regex-fallback", "fallback-language", "mixed", "not-applicable"] },
|
|
64
|
+
"classification": { "enum": ["defect", "clean_control", "ambiguous", "unsupported_syntax", "policy_disagreement"] },
|
|
65
|
+
"repairOutcome": { "enum": ["accepted", "rejected", "not_evaluated"] },
|
|
66
|
+
"source": { "$ref": "#/$defs/source" },
|
|
67
|
+
"scope": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"additionalProperties": false,
|
|
70
|
+
"required": ["validatorKey", "codes"],
|
|
71
|
+
"properties": {
|
|
72
|
+
"validatorKey": { "type": "string", "minLength": 1, "maxLength": 64 },
|
|
73
|
+
"codes": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "pattern": "^[A-Z]{3}[0-9]{3}$" } }
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"config": { "type": "object" },
|
|
77
|
+
"mutations": { "type": "array", "items": { "$ref": "#/$defs/mutation" } },
|
|
78
|
+
"expected": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/identity" } },
|
|
79
|
+
"forbidden": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/identity" } },
|
|
80
|
+
"oppositeControl": { "type": ["string", "null"], "pattern": "^[a-z0-9][a-z0-9._-]{2,127}$" }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raccioly.github.io/docguard/schemas/docguard-feedback-fixture.schema.json",
|
|
4
|
+
"title": "DocGuard synthetic feedback fixture",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["$schema", "schemaVersion", "classification", "detector", "parserTier", "config", "expectedIdentity", "interestingness", "fixture", "oppositeControl", "provenance"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "const": "https://raccioly.github.io/docguard/schemas/docguard-feedback-fixture.schema.json" },
|
|
10
|
+
"schemaVersion": { "const": 1 },
|
|
11
|
+
"classification": { "enum": ["false_positive", "false_negative", "unsupported_syntax", "ambiguous", "policy_disagreement"] },
|
|
12
|
+
"detector": {
|
|
13
|
+
"type": "object", "additionalProperties": false, "required": ["code", "validator"],
|
|
14
|
+
"properties": { "code": { "type": "string", "pattern": "^[A-Z]{3}[0-9]{3}$" }, "validator": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9]{1,63}$" } }
|
|
15
|
+
},
|
|
16
|
+
"parserTier": { "enum": ["js-ast", "py-ast", "regex-fallback", "fallback-language", "mixed", "not-applicable"] },
|
|
17
|
+
"config": { "type": "object" },
|
|
18
|
+
"expectedIdentity": { "type": "string", "pattern": "^[A-Z]{3}[0-9]{3}@.+$" },
|
|
19
|
+
"interestingness": {
|
|
20
|
+
"type": "object", "additionalProperties": false, "required": ["predicate"],
|
|
21
|
+
"properties": { "predicate": { "enum": ["finding_present", "finding_absent", "validator_unsupported"] } }
|
|
22
|
+
},
|
|
23
|
+
"fixture": { "$ref": "#/$defs/fileSet" },
|
|
24
|
+
"oppositeControl": { "$ref": "#/$defs/fileSet" },
|
|
25
|
+
"provenance": {
|
|
26
|
+
"type": "object", "additionalProperties": false, "required": ["synthetic", "redactionAttested"],
|
|
27
|
+
"properties": { "synthetic": { "const": true }, "redactionAttested": { "const": true } }
|
|
28
|
+
},
|
|
29
|
+
"contribution": {
|
|
30
|
+
"type": "object", "additionalProperties": false, "required": ["testOnly", "scopeDocumented", "benchmarkDelta"],
|
|
31
|
+
"properties": {
|
|
32
|
+
"testOnly": { "const": true }, "scopeDocumented": { "const": true },
|
|
33
|
+
"benchmarkDelta": {
|
|
34
|
+
"type": "object", "additionalProperties": false,
|
|
35
|
+
"required": ["falsePositives", "falseNegatives", "unsupportedCases", "abstainedSupportedCases"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"falsePositives": { "type": "integer", "minimum": 0 }, "falseNegatives": { "type": "integer", "minimum": 0 },
|
|
38
|
+
"unsupportedCases": { "type": "integer", "minimum": 0 }, "abstainedSupportedCases": { "type": "integer", "minimum": 0 }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"$defs": {
|
|
45
|
+
"fileSet": {
|
|
46
|
+
"type": "object", "additionalProperties": false, "required": ["files"],
|
|
47
|
+
"properties": { "files": { "type": "array", "minItems": 1, "maxItems": 16, "items": { "$ref": "#/$defs/file" } } }
|
|
48
|
+
},
|
|
49
|
+
"file": {
|
|
50
|
+
"type": "object", "additionalProperties": false, "required": ["path", "content"],
|
|
51
|
+
"properties": { "path": { "type": "string", "minLength": 1 }, "content": { "type": "string", "maxLength": 131072 } }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raccioly.github.io/docguard/schemas/docguard-feedback-fixture.schema.json",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"classification": "false_positive",
|
|
5
|
+
"detector": { "code": "SEC001", "validator": "security" },
|
|
6
|
+
"parserTier": "js-ast",
|
|
7
|
+
"config": { "profile": "starter", "diskCache": false, "validators": { "security": true } },
|
|
8
|
+
"expectedIdentity": "SEC001@src/case.js",
|
|
9
|
+
"interestingness": { "predicate": "finding_present" },
|
|
10
|
+
"fixture": { "files": [{ "path": "src/case.js", "content": "const password = \"validation message with several words\";\n" }] },
|
|
11
|
+
"oppositeControl": { "files": [{ "path": "src/case.js", "content": "const password = \"real-looking-password\";\n" }] },
|
|
12
|
+
"provenance": { "synthetic": true, "redactionAttested": true },
|
|
13
|
+
"contribution": {
|
|
14
|
+
"testOnly": true,
|
|
15
|
+
"scopeDocumented": true,
|
|
16
|
+
"benchmarkDelta": { "falsePositives": 0, "falseNegatives": 0, "unsupportedCases": 0, "abstainedSupportedCases": 0 }
|
|
17
|
+
}
|
|
18
|
+
}
|