docgrity 0.1.2 → 0.1.4
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 +109 -126
- package/{action/action.yml → action.yml} +4 -1
- package/{action/bin → bin}/action.js +12 -6
- package/{action/bin → bin}/docgrity.js +23 -9
- package/package.json +31 -164
- package/src/heuristics.js +146 -0
- package/{action/src → src}/issues.js +4 -1
- package/{action/src → src}/report.js +2 -1
- package/{action/src → src}/scan.js +32 -7
- package/.github/workflows/ci.yml +0 -54
- package/.vscodeignore +0 -13
- package/action/LICENSE +0 -21
- package/action/README.md +0 -104
- package/action/examples/docgrity.yml +0 -61
- package/action/package.json +0 -38
- package/action/test/corpus.test.mjs +0 -59
- package/action/test/issues.test.mjs +0 -89
- package/action/test/report.test.mjs +0 -76
- package/docgrity_logo.png +0 -0
- package/image.png +0 -0
- package/media/icon.png +0 -0
- package/media/icon.svg +0 -5
- package/samples/api-limits.md +0 -23
- package/samples/architecture-notes.md +0 -28
- package/samples/deployment-guide.md +0 -23
- package/samples/integration-guide.md +0 -21
- package/samples/release-process.md +0 -23
- package/src/agents/assess.ts +0 -187
- package/src/agents/prompts.ts +0 -94
- package/src/agents/selectModel.ts +0 -50
- package/src/core/json.ts +0 -58
- package/src/core/prefilter.ts +0 -56
- package/src/core/slug.ts +0 -10
- package/src/core/verify.ts +0 -15
- package/src/extension.ts +0 -142
- package/src/findings/diagnostics.ts +0 -78
- package/src/findings/report.ts +0 -68
- package/src/findings/store.ts +0 -60
- package/src/findings/tree.ts +0 -93
- package/src/github/issues.ts +0 -90
- package/src/github/owners.ts +0 -60
- package/src/log.ts +0 -22
- package/src/scanner/candidates.ts +0 -62
- package/src/scanner/corpus.ts +0 -75
- package/src/scanner/scan.ts +0 -215
- package/test/candidates.test.ts +0 -63
- package/test/json.test.ts +0 -87
- package/test/prefilter.test.ts +0 -65
- package/test/slug.test.ts +0 -31
- package/test/verify.test.ts +0 -47
- package/tsconfig.json +0 -15
- package/vitest.config.mts +0 -9
- /package/{action/src → src}/corpus.js +0 -0
- /package/{action/src → src}/llm.js +0 -0
- /package/{action/src → src}/prompts.js +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* No-agent (heuristic) checks — pure algorithms, zero LLM calls.
|
|
3
|
+
*
|
|
4
|
+
* What works without a model:
|
|
5
|
+
* - duplicates: TF-IDF similarity + verbatim shared-block detection
|
|
6
|
+
* - open questions: explicit unresolved markers (TODO/TBD/FIXME/???/…)
|
|
7
|
+
* What does NOT work without a model:
|
|
8
|
+
* - contradictions: require semantic understanding — explicitly skipped,
|
|
9
|
+
* and the report says so, rather than emitting noisy guesses.
|
|
10
|
+
*
|
|
11
|
+
* Evidence is verbatim by construction (extracted from the source text), so
|
|
12
|
+
* the same hallucination guard in scan.js passes trivially. Confidence for
|
|
13
|
+
* duplicates is derived from measured overlap; for open questions the
|
|
14
|
+
* markers are deterministic, so confidence is fixed high per marker class.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const stripFences = (text) => text.replace(/```[\s\S]*?```/g, (m) => m.replace(/[^\n]/g, ''));
|
|
18
|
+
|
|
19
|
+
const normLine = (s) => s.replace(/\s+/g, ' ').trim();
|
|
20
|
+
|
|
21
|
+
/** Lines substantive enough to count as shared content (not markdown noise). */
|
|
22
|
+
const substantive = (line) => {
|
|
23
|
+
const l = normLine(line);
|
|
24
|
+
if (l.length < 40) return false;
|
|
25
|
+
if (/^[#>\-*|=\s`~[\]().\d]+$/.test(l)) return false; // pure punctuation/structure
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Find consecutive runs of lines from A that also appear (normalized) in B.
|
|
31
|
+
* Returns blocks sorted by length desc.
|
|
32
|
+
*/
|
|
33
|
+
export function sharedBlocks(textA, textB) {
|
|
34
|
+
const linesA = stripFences(textA).split('\n');
|
|
35
|
+
const setB = new Set(stripFences(textB).split('\n').map(normLine).filter((l) => l.length >= 20));
|
|
36
|
+
const blocks = [];
|
|
37
|
+
let current = [];
|
|
38
|
+
for (const raw of linesA) {
|
|
39
|
+
const l = normLine(raw);
|
|
40
|
+
if (l.length >= 20 && setB.has(l)) {
|
|
41
|
+
current.push(raw.trim());
|
|
42
|
+
} else if (l.length > 0) {
|
|
43
|
+
if (current.length) blocks.push(current), (current = []);
|
|
44
|
+
}
|
|
45
|
+
// blank lines don't break a block
|
|
46
|
+
}
|
|
47
|
+
if (current.length) blocks.push(current);
|
|
48
|
+
return blocks
|
|
49
|
+
.map((lines) => ({ lines, text: lines.join('\n'), chars: lines.join(' ').length }))
|
|
50
|
+
.filter((b) => b.lines.some(substantive))
|
|
51
|
+
.sort((x, y) => y.chars - x.chars);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Heuristic duplicate assessment. `similarity` is the TF-IDF cosine from
|
|
56
|
+
* candidate selection. Flags only when there is *verbatim* shared content —
|
|
57
|
+
* vocabulary similarity alone is not enough (false-positive guard).
|
|
58
|
+
*/
|
|
59
|
+
export function heuristicDuplicate(a, b, similarity = 0) {
|
|
60
|
+
const blocks = sharedBlocks(a.text, b.text);
|
|
61
|
+
const sharedChars = blocks.reduce((n, bl) => n + bl.chars, 0);
|
|
62
|
+
const minLen = Math.max(1, Math.min(a.text.length, b.text.length));
|
|
63
|
+
const sharedRatio = Math.min(1, sharedChars / minLen);
|
|
64
|
+
|
|
65
|
+
// Require a meaningful verbatim block; similarity alone never fires.
|
|
66
|
+
const hasBlock = blocks.length > 0 && blocks[0].chars >= 120;
|
|
67
|
+
const is_duplicate = hasBlock && (sharedRatio >= 0.2 || similarity >= 0.6);
|
|
68
|
+
|
|
69
|
+
// Confidence is measured, not guessed: verbatim overlap dominates.
|
|
70
|
+
const confidence = is_duplicate
|
|
71
|
+
? Math.min(0.99, Math.round((0.55 * Math.min(1, sharedRatio * 2) + 0.45 * similarity) * 100) / 100)
|
|
72
|
+
: Math.round(Math.max(sharedRatio, similarity) * 100) / 100;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
output: {
|
|
76
|
+
is_duplicate,
|
|
77
|
+
confidence,
|
|
78
|
+
summary: is_duplicate
|
|
79
|
+
? `Verbatim duplicated content between "${a.relPath}" and "${b.relPath}" — ${blocks.length} shared block(s), ~${Math.round(sharedRatio * 100)}% of the smaller doc (TF-IDF similarity ${similarity.toFixed(2)}).`
|
|
80
|
+
: 'No verbatim duplication detected.',
|
|
81
|
+
recommended_action: 'CONSOLIDATE',
|
|
82
|
+
evidence: blocks.slice(0, 3).map((bl) => ({ page: 'A', excerpt: bl.text.slice(0, 1000) })),
|
|
83
|
+
},
|
|
84
|
+
model: 'heuristic (tf-idf + shared blocks)',
|
|
85
|
+
promptVersion: 'heuristic-v1',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Explicit unresolved-marker classes — deterministic, hence high confidence. */
|
|
90
|
+
const OQ_MARKERS = [
|
|
91
|
+
{ re: /\b(TODO|FIXME)\b(?!\s*:?\s*none)/, label: 'TODO/FIXME marker', confidence: 0.95, severity: 'MEDIUM' },
|
|
92
|
+
{ re: /\b(TBD|TBC)\b|\bto be (decided|determined|confirmed|announced)\b/i, label: 'TBD marker', confidence: 0.95, severity: 'MEDIUM' },
|
|
93
|
+
{ re: /\?{3,}|\[\?\]/, label: 'placeholder question marks', confidence: 0.9, severity: 'MEDIUM' },
|
|
94
|
+
// Affirmative context required ("remains unresolved"), so negations like
|
|
95
|
+
// "nothing unresolved" don't fire (false-positive guard).
|
|
96
|
+
{ re: /\bopen question\b|\b(is|are|remains?|still|left)\s+(unresolved|undecided)\b|\bnot (yet )?(decided|determined|finali[sz]ed)\b/i, label: 'explicit open-question language', confidence: 0.85, severity: 'MEDIUM' },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Heuristic open-question detection. Scans prose only (fenced code stripped —
|
|
101
|
+
* TODOs inside code samples are code concerns, not doc integrity), one
|
|
102
|
+
* finding per line, deduplicated.
|
|
103
|
+
*/
|
|
104
|
+
export function heuristicOpenQuestions(doc) {
|
|
105
|
+
const lines = stripFences(doc.text).split('\n');
|
|
106
|
+
const questions = [];
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
for (const raw of lines) {
|
|
109
|
+
const line = normLine(raw);
|
|
110
|
+
if (!line || seen.has(line)) continue;
|
|
111
|
+
for (const m of OQ_MARKERS) {
|
|
112
|
+
if (m.re.test(line)) {
|
|
113
|
+
seen.add(line);
|
|
114
|
+
questions.push({
|
|
115
|
+
question: `Unresolved ${m.label}: "${line.slice(0, 200)}"`,
|
|
116
|
+
excerpt: raw.trim().slice(0, 1000),
|
|
117
|
+
confidence: m.confidence,
|
|
118
|
+
severity: m.severity,
|
|
119
|
+
});
|
|
120
|
+
break; // one finding per line even if several markers match
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
output: { questions },
|
|
126
|
+
model: 'heuristic (signal markers)',
|
|
127
|
+
promptVersion: 'heuristic-v1',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Template-based issue draft — used when no LLM client is available. */
|
|
132
|
+
export function templateIssue(finding) {
|
|
133
|
+
const title = `[docgrity] ${finding.type.replace('_', ' ')}: ${finding.summary.slice(0, 140)}`;
|
|
134
|
+
const body = [
|
|
135
|
+
`**Type:** ${finding.type} `,
|
|
136
|
+
`**Severity:** ${finding.severity} · **Confidence:** ${(finding.confidence * 100).toFixed(0)}% `,
|
|
137
|
+
`**Docs:** ${finding.files.map((f) => `\`${f}\``).join(', ')} `,
|
|
138
|
+
`**Potential owner(s):** ${finding.potentialOwners.join(', ') || 'unknown'} *(from git history — potential, not asserted)*`,
|
|
139
|
+
'',
|
|
140
|
+
'### Evidence',
|
|
141
|
+
...finding.evidence.map((e) => `> ${e.excerpt.replace(/\n/g, '\n> ')}\n> — \`${e.sourceLabel}\``),
|
|
142
|
+
'',
|
|
143
|
+
`_Detected by Docgrity in no-agent (heuristic) mode — evidence is verbatim from the docs._`,
|
|
144
|
+
].join('\n');
|
|
145
|
+
return { title: title.slice(0, 200), body: body.slice(0, 20000) };
|
|
146
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Only runs when create_issues is explicitly enabled (opt-in guard).
|
|
8
8
|
*/
|
|
9
9
|
import { draftIssue } from './llm.js';
|
|
10
|
+
import { templateIssue } from './heuristics.js';
|
|
10
11
|
|
|
11
12
|
const MARKER = (fp) => `<!-- docgrity:fingerprint:${fp} -->`;
|
|
12
13
|
const LABEL = 'docgrity';
|
|
@@ -71,7 +72,9 @@ export async function syncIssues({ client, token, slug, findings, maxNewIssues =
|
|
|
71
72
|
result.skipped++;
|
|
72
73
|
continue;
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
+
// No LLM client (no-agent mode) → deterministic template draft; evidence is
|
|
76
|
+
// already verbatim, so nothing is lost except prose polish.
|
|
77
|
+
const draft = client ? await draftIssue(client, f) : templateIssue(f);
|
|
75
78
|
const body = `${draft.body}
|
|
76
79
|
|
|
77
80
|
---
|
|
@@ -154,7 +154,8 @@ export function renderReport({ findings, stats, repoSlug, branch }) {
|
|
|
154
154
|
<div class="wrap">
|
|
155
155
|
<div class="hero">
|
|
156
156
|
<h1>Docgrity — documentation-integrity report</h1>
|
|
157
|
-
<div class="meta">${esc(repoSlug ?? 'local scan')} · scanned ${esc(stats.scannedAt)} · ${stats.docs} markdown docs · ${stats.pairs} pairs assessed</div>
|
|
157
|
+
<div class="meta">${esc(repoSlug ?? 'local scan')} · scanned ${esc(stats.scannedAt)} · ${stats.docs} markdown docs · ${stats.pairs} pairs assessed${stats.mode === 'heuristic' ? ' · no-agent (heuristic) mode' : ''}</div>
|
|
158
|
+
${(stats.notes ?? []).map((n) => `<div class="meta" style="margin-top:6px">⚠ ${esc(n)}</div>`).join('')}
|
|
158
159
|
</div>
|
|
159
160
|
<div class="stats">
|
|
160
161
|
<button class="stat" data-target="all"><div><div class="label">Open findings</div><div class="num">${findings.length}</div><div class="sub">view all</div></div><div class="badge ${findings.length ? 'findings' : 'ok'}">${icon(findings.length ? 'findings' : 'check')}</div></button>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import crypto from 'crypto';
|
|
7
7
|
import { collectCorpus, selectCandidatePairs, potentialOwner } from './corpus.js';
|
|
8
8
|
import { assessContradiction, assessDuplicate, assessOpenQuestions } from './llm.js';
|
|
9
|
+
import { heuristicDuplicate, heuristicOpenQuestions } from './heuristics.js';
|
|
9
10
|
|
|
10
11
|
const norm = (s) => s.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
11
12
|
|
|
@@ -37,9 +38,17 @@ export async function runScan(root, opts = {}) {
|
|
|
37
38
|
} = opts;
|
|
38
39
|
|
|
39
40
|
const docs = await collectCorpus(root, { maxFiles });
|
|
41
|
+
// No-agent (heuristic) mode when no LLM client is provided.
|
|
42
|
+
const heuristic = !client;
|
|
43
|
+
const notes = [];
|
|
44
|
+
if (heuristic && checks.contradictions) {
|
|
45
|
+
notes.push('Contradiction detection requires an AI model (semantic understanding) — skipped in no-agent mode.');
|
|
46
|
+
log('Note: contradictions need AI — skipped (no-agent mode).');
|
|
47
|
+
}
|
|
40
48
|
// Pair selection is only needed for the pairwise checks.
|
|
41
|
-
const
|
|
42
|
-
|
|
49
|
+
const pairwise = checks.duplicates || (checks.contradictions && !heuristic);
|
|
50
|
+
const pairs = pairwise ? selectCandidatePairs(docs, maxPairs) : [];
|
|
51
|
+
log(`Corpus: ${docs.length} markdown docs; assessing ${pairs.length} candidate pairs${heuristic ? ' (heuristic, no LLM)' : ''}`);
|
|
43
52
|
const findings = [];
|
|
44
53
|
const now = new Date().toISOString();
|
|
45
54
|
|
|
@@ -53,10 +62,14 @@ export async function runScan(root, opts = {}) {
|
|
|
53
62
|
};
|
|
54
63
|
|
|
55
64
|
let i = 0;
|
|
56
|
-
for (const { a, b } of pairs) {
|
|
65
|
+
for (const { a, b, similarity } of pairs) {
|
|
57
66
|
i++;
|
|
58
67
|
log(`Pair ${i}/${pairs.length}: ${a.relPath} <-> ${b.relPath}`);
|
|
59
|
-
const dup = checks.duplicates
|
|
68
|
+
const dup = checks.duplicates
|
|
69
|
+
? heuristic
|
|
70
|
+
? heuristicDuplicate(a, b, similarity)
|
|
71
|
+
: await assessDuplicate(client, a, b)
|
|
72
|
+
: null;
|
|
60
73
|
if (
|
|
61
74
|
dup &&
|
|
62
75
|
dup.output.is_duplicate &&
|
|
@@ -79,11 +92,12 @@ export async function runScan(root, opts = {}) {
|
|
|
79
92
|
potentialOwners: await owners([a, b]),
|
|
80
93
|
model: dup.model,
|
|
81
94
|
promptVersion: dup.promptVersion,
|
|
95
|
+
method: heuristic ? 'heuristic' : 'llm',
|
|
82
96
|
createdAt: now,
|
|
83
97
|
});
|
|
84
98
|
}
|
|
85
99
|
|
|
86
|
-
const con = checks.contradictions ? await assessContradiction(client, a, b) : null;
|
|
100
|
+
const con = checks.contradictions && !heuristic ? await assessContradiction(client, a, b) : null;
|
|
87
101
|
if (
|
|
88
102
|
con &&
|
|
89
103
|
con.output.is_contradiction &&
|
|
@@ -106,6 +120,7 @@ export async function runScan(root, opts = {}) {
|
|
|
106
120
|
potentialOwners: await owners([a, b]),
|
|
107
121
|
model: con.model,
|
|
108
122
|
promptVersion: con.promptVersion,
|
|
123
|
+
method: 'llm',
|
|
109
124
|
createdAt: now,
|
|
110
125
|
});
|
|
111
126
|
}
|
|
@@ -116,7 +131,7 @@ export async function runScan(root, opts = {}) {
|
|
|
116
131
|
for (const doc of oqDocs) {
|
|
117
132
|
j++;
|
|
118
133
|
log(`Open questions ${j}/${oqDocs.length}: ${doc.relPath}`);
|
|
119
|
-
const oq = await assessOpenQuestions(client, doc);
|
|
134
|
+
const oq = heuristic ? heuristicOpenQuestions(doc) : await assessOpenQuestions(client, doc);
|
|
120
135
|
const kept = oq.output.questions.filter(
|
|
121
136
|
(q) => q.confidence >= thresholds.openQuestion && verifyExcerpts([q], [doc])
|
|
122
137
|
);
|
|
@@ -133,10 +148,20 @@ export async function runScan(root, opts = {}) {
|
|
|
133
148
|
potentialOwners: await owners([doc]),
|
|
134
149
|
model: oq.model,
|
|
135
150
|
promptVersion: oq.promptVersion,
|
|
151
|
+
method: heuristic ? 'heuristic' : 'llm',
|
|
136
152
|
createdAt: now,
|
|
137
153
|
});
|
|
138
154
|
}
|
|
139
155
|
}
|
|
140
156
|
|
|
141
|
-
return {
|
|
157
|
+
return {
|
|
158
|
+
findings,
|
|
159
|
+
stats: {
|
|
160
|
+
docs: docs.length,
|
|
161
|
+
pairs: pairs.length,
|
|
162
|
+
scannedAt: now,
|
|
163
|
+
mode: heuristic ? 'heuristic' : 'llm',
|
|
164
|
+
...(notes.length ? { notes } : {}),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
142
167
|
}
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
|
|
8
|
-
permissions:
|
|
9
|
-
contents: read
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
build:
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
steps:
|
|
15
|
-
- uses: actions/checkout@v4
|
|
16
|
-
- uses: actions/setup-node@v4
|
|
17
|
-
with:
|
|
18
|
-
node-version: 22
|
|
19
|
-
cache: npm
|
|
20
|
-
- name: Install dependencies
|
|
21
|
-
run: npm ci
|
|
22
|
-
- name: Compile (strict TypeScript)
|
|
23
|
-
run: npm run compile
|
|
24
|
-
- name: Unit tests
|
|
25
|
-
run: npm test
|
|
26
|
-
- name: Action unit tests (zero-dependency, node:test)
|
|
27
|
-
working-directory: action
|
|
28
|
-
run: npm test
|
|
29
|
-
- name: Dependency vulnerability audit
|
|
30
|
-
run: npm audit --audit-level=high
|
|
31
|
-
- name: Secret pattern scan (source only)
|
|
32
|
-
run: |
|
|
33
|
-
! grep -rInE "(sk-[A-Za-z0-9]{20,}|AIza[A-Za-z0-9_-]{30,}|ghp_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,})" src/ action/src/ action/bin/ \
|
|
34
|
-
&& echo "No hardcoded credentials found."
|
|
35
|
-
- name: Package extension
|
|
36
|
-
run: npx --yes @vscode/vsce package --no-dependencies
|
|
37
|
-
- name: Upload .vsix artifact
|
|
38
|
-
uses: actions/upload-artifact@v4
|
|
39
|
-
with:
|
|
40
|
-
name: docgrity-vsix
|
|
41
|
-
path: '*.vsix'
|
|
42
|
-
|
|
43
|
-
codeql:
|
|
44
|
-
runs-on: ubuntu-latest
|
|
45
|
-
permissions:
|
|
46
|
-
contents: read
|
|
47
|
-
security-events: write
|
|
48
|
-
steps:
|
|
49
|
-
- uses: actions/checkout@v4
|
|
50
|
-
- uses: github/codeql-action/init@v3
|
|
51
|
-
with:
|
|
52
|
-
languages: javascript-typescript
|
|
53
|
-
queries: security-and-quality
|
|
54
|
-
- uses: github/codeql-action/analyze@v3
|
package/.vscodeignore
DELETED
package/action/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Ujjavala
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/action/README.md
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# Docgrity Action & CLI
|
|
2
|
-
|
|
3
|
-
**Continuous doc-integrity for your repo's markdown: contradictions, duplicates and open
|
|
4
|
-
questions — as a GitHub Action (with deduplicated issues) and a read-only local CLI.**
|
|
5
|
-
|
|
6
|
-
Part of the Docgrity family:
|
|
7
|
-
|
|
8
|
-
| Surface | Job | Acts? |
|
|
9
|
-
|---|---|---|
|
|
10
|
-
| [Confluence app](https://ujjavala.github.io/docgrity-site/) | wiki integrity | comments (human-approved) |
|
|
11
|
-
| VS Code extension | interactive repo-doc scans | raises issues (human-approved) |
|
|
12
|
-
| **This Action** | continuous CI enforcement | issues (opt-in, deduped, capped) + report |
|
|
13
|
-
| **This CLI** | local observation | **read-only** — report dashboard only |
|
|
14
|
-
|
|
15
|
-
## GitHub Action
|
|
16
|
-
|
|
17
|
-
```yaml
|
|
18
|
-
- uses: ujjavala/docgrity-vscode/action@main
|
|
19
|
-
env:
|
|
20
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
21
|
-
with:
|
|
22
|
-
provider: github-models # free — uses GITHUB_TOKEN, no API key
|
|
23
|
-
create_issues: 'true' # opt-in; default false
|
|
24
|
-
max_new_issues: 5
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Full example with weekly schedule, PR trigger and Pages report publishing:
|
|
28
|
-
[examples/docgrity.yml](examples/docgrity.yml).
|
|
29
|
-
|
|
30
|
-
What it does per run:
|
|
31
|
-
|
|
32
|
-
1. Collects markdown docs (`**/*.md`, capped), selects candidate pairs locally (TF-IDF),
|
|
33
|
-
assesses with the LLM using versioned prompts and typed-JSON validation, verifies
|
|
34
|
-
every evidence excerpt verbatim against the source (hallucination guard).
|
|
35
|
-
2. Writes a **job summary** table and a **static HTML report** (`docgrity-report/`)
|
|
36
|
-
with evidence, links to docs on GitHub, and *potential* owners from git history.
|
|
37
|
-
3. **Opt-in** (`create_issues: true`): syncs GitHub issues **deduplicated by a stable
|
|
38
|
-
finding fingerprint** — new findings create issues (capped per run), unchanged ones
|
|
39
|
-
are left alone, resolved ones are auto-closed with a comment. Labels: `docgrity`,
|
|
40
|
-
`docgrity:<type>`.
|
|
41
|
-
|
|
42
|
-
### Providers
|
|
43
|
-
|
|
44
|
-
| provider | key | cost |
|
|
45
|
-
|---|---|---|
|
|
46
|
-
| `github-models` (default in CI) | none — uses `GITHUB_TOKEN` with `models: read` | free (being retired by GitHub — prefer a BYO provider) |
|
|
47
|
-
| `gemini` / `openai` / `anthropic` | `api_key` input (use a repo secret) | your key |
|
|
48
|
-
| `ollama` | none — local or tunnelled endpoint | free, fully private |
|
|
49
|
-
|
|
50
|
-
## Local CLI (read-only)
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
npm i -g docgrity
|
|
54
|
-
docgrity scan --open
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
or without installing: `npx docgrity scan --open` (from a repo checkout:
|
|
58
|
-
`npx github:ujjavala/docgrity-vscode scan --open`).
|
|
59
|
-
|
|
60
|
-
Runs the same scan locally and opens the **report dashboard**: findings, evidence,
|
|
61
|
-
doc links and potential owners. **The CLI never raises issues or takes any action** —
|
|
62
|
-
by design, local scans observe; only CI (explicitly opted in) acts.
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
Usage: docgrity scan [options]
|
|
66
|
-
|
|
67
|
-
--dir <path> Directory to scan (default: .)
|
|
68
|
-
--out <path> Report output directory (default: docgrity-report)
|
|
69
|
-
--open Open the HTML report when done
|
|
70
|
-
|
|
71
|
-
--checks <list> duplicates, contradictions, open-questions — any combination
|
|
72
|
-
--max-files <n> Max markdown files (default: 200)
|
|
73
|
-
--max-pairs <n> Max document pairs (default: 25)
|
|
74
|
-
--threshold-duplicate / --threshold-contradiction / --threshold-open-question <0..1>
|
|
75
|
-
|
|
76
|
-
--provider <p> ollama | gemini | openai | anthropic | github-models
|
|
77
|
-
--model <m> Model name
|
|
78
|
-
--endpoint <url> Ollama endpoint (default http://localhost:11434)
|
|
79
|
-
|
|
80
|
-
--version, -v Installed version + latest on npm
|
|
81
|
-
--help, -h Full help
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Provider auto-detection: `DOCGRITY_API_KEY` set → `gemini`; else `GITHUB_TOKEN` →
|
|
85
|
-
`github-models`; else → `ollama` (local, fully private — nothing leaves your machine).
|
|
86
|
-
|
|
87
|
-
Examples:
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
docgrity scan --checks contradictions # one check only
|
|
91
|
-
docgrity scan --checks duplicates,open-questions --max-pairs 10
|
|
92
|
-
docgrity scan --provider ollama --model llama3.1:8b # fully local
|
|
93
|
-
DOCGRITY_API_KEY=... docgrity scan --provider gemini --open
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## Design principles (shared across all Docgrity surfaces)
|
|
97
|
-
|
|
98
|
-
- Typed JSON outputs only; model responses validated in code.
|
|
99
|
-
- Every finding requires verbatim evidence, verified against the source file.
|
|
100
|
-
- Ownership is always *potential* (last git author), never asserted.
|
|
101
|
-
- Action-taking is opt-in, capped, and auditable (issue trailer records model +
|
|
102
|
-
prompt version + fingerprint).
|
|
103
|
-
- Doc content is untrusted input — it cannot override agent instructions.
|
|
104
|
-
- Zero dependencies; plain Node 20+ ESM.
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# Example workflow: copy into your repo as .github/workflows/docgrity.yml
|
|
2
|
-
name: Docgrity docs scan
|
|
3
|
-
|
|
4
|
-
on:
|
|
5
|
-
schedule:
|
|
6
|
-
- cron: '0 6 * * 1' # weekly, Monday 06:00 UTC
|
|
7
|
-
pull_request:
|
|
8
|
-
paths: ['**/*.md']
|
|
9
|
-
workflow_dispatch: {}
|
|
10
|
-
|
|
11
|
-
permissions:
|
|
12
|
-
contents: read
|
|
13
|
-
issues: write # only needed when create_issues: true
|
|
14
|
-
models: read # GitHub Models (default provider, free)
|
|
15
|
-
pages: write # only needed for the Pages report job below
|
|
16
|
-
id-token: write # only needed for the Pages report job below
|
|
17
|
-
|
|
18
|
-
jobs:
|
|
19
|
-
scan:
|
|
20
|
-
runs-on: ubuntu-latest
|
|
21
|
-
steps:
|
|
22
|
-
- uses: actions/checkout@v4
|
|
23
|
-
with:
|
|
24
|
-
fetch-depth: 0 # full history so potential owners resolve via git log
|
|
25
|
-
|
|
26
|
-
- name: Docgrity scan
|
|
27
|
-
id: docgrity
|
|
28
|
-
uses: ujjavala/docgrity-vscode/action@main
|
|
29
|
-
env:
|
|
30
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
31
|
-
with:
|
|
32
|
-
provider: github-models # or gemini/openai/anthropic + api_key
|
|
33
|
-
# api_key: ${{ secrets.DOCGRITY_API_KEY }}
|
|
34
|
-
create_issues: ${{ github.event_name == 'schedule' }} # opt-in: only weekly runs raise issues
|
|
35
|
-
max_new_issues: 5
|
|
36
|
-
|
|
37
|
-
- name: Upload report artifact
|
|
38
|
-
uses: actions/upload-artifact@v4
|
|
39
|
-
with:
|
|
40
|
-
name: docgrity-report
|
|
41
|
-
path: docgrity-report/
|
|
42
|
-
|
|
43
|
-
# Optional: publish the report to GitHub Pages (visibility follows repo access).
|
|
44
|
-
publish-report:
|
|
45
|
-
needs: scan
|
|
46
|
-
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
|
47
|
-
runs-on: ubuntu-latest
|
|
48
|
-
environment:
|
|
49
|
-
name: github-pages
|
|
50
|
-
url: ${{ steps.deployment.outputs.page_url }}
|
|
51
|
-
steps:
|
|
52
|
-
- uses: actions/download-artifact@v4
|
|
53
|
-
with:
|
|
54
|
-
name: docgrity-report
|
|
55
|
-
path: site
|
|
56
|
-
- uses: actions/configure-pages@v5
|
|
57
|
-
- uses: actions/upload-pages-artifact@v3
|
|
58
|
-
with:
|
|
59
|
-
path: site
|
|
60
|
-
- id: deployment
|
|
61
|
-
uses: actions/deploy-pages@v4
|
package/action/package.json
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "docgrity",
|
|
3
|
-
"version": "0.1.2",
|
|
4
|
-
"description": "Docgrity doc-integrity scans for CI and local use: contradictions, duplicates and open questions across repository markdown docs. Read-only CLI — generates an HTML report; never posts anything.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": { "docgrity": "bin/docgrity.js" },
|
|
7
|
-
"files": [
|
|
8
|
-
"bin/",
|
|
9
|
-
"src/",
|
|
10
|
-
"action.yml",
|
|
11
|
-
"README.md",
|
|
12
|
-
"LICENSE"
|
|
13
|
-
],
|
|
14
|
-
"engines": { "node": ">=20" },
|
|
15
|
-
"scripts": {
|
|
16
|
-
"scan": "node bin/docgrity.js scan",
|
|
17
|
-
"test": "node --test \"test/*.test.mjs\""
|
|
18
|
-
},
|
|
19
|
-
"repository": {
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/ujjavala/docgrity-vscode.git",
|
|
22
|
-
"directory": "action"
|
|
23
|
-
},
|
|
24
|
-
"homepage": "https://ujjavala.github.io/docgrity-vscode-site/",
|
|
25
|
-
"bugs": "https://github.com/ujjavala/docgrity-vscode/issues",
|
|
26
|
-
"keywords": [
|
|
27
|
-
"documentation",
|
|
28
|
-
"markdown",
|
|
29
|
-
"lint",
|
|
30
|
-
"contradiction",
|
|
31
|
-
"duplicate",
|
|
32
|
-
"llm",
|
|
33
|
-
"cli",
|
|
34
|
-
"github-action"
|
|
35
|
-
],
|
|
36
|
-
"author": "ujjavala",
|
|
37
|
-
"license": "MIT"
|
|
38
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/** corpus.js tests — md-only collection, TF-IDF pairing, owner inference. */
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
import assert from 'node:assert/strict';
|
|
4
|
-
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { collectCorpus, selectCandidatePairs, githubRepoSlug } from '../src/corpus.js';
|
|
8
|
-
|
|
9
|
-
const doc = (relPath, text) => ({ relPath, text, hash: relPath });
|
|
10
|
-
|
|
11
|
-
test('selectCandidatePairs pairs similar docs, ignores unrelated ones', () => {
|
|
12
|
-
const a = doc('a.md', 'deployment guide for the payments service using canary rollout and grafana dashboards '.repeat(4));
|
|
13
|
-
const b = doc('b.md', 'release process for the payments service with canary rollout watched in grafana '.repeat(4));
|
|
14
|
-
const c = doc('c.md', 'chocolate cake recipe flour sugar eggs butter vanilla oven baking whisk frosting '.repeat(4));
|
|
15
|
-
const pairs = selectCandidatePairs([a, b, c]);
|
|
16
|
-
assert.ok(pairs.some((p) => p.a.relPath === 'a.md' && p.b.relPath === 'b.md'), 'similar docs must pair');
|
|
17
|
-
assert.ok(!pairs.some((p) => p.a.relPath === 'c.md' || p.b.relPath === 'c.md'), 'unrelated doc must not pair');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test('selectCandidatePairs strips code blocks before comparing', () => {
|
|
21
|
-
const code = '```\nconst deploy = canary(grafana, rollout, payments, service);\n```';
|
|
22
|
-
const a = doc('a.md', `${code} completely unrelated prose about gardening tulips soil watering sunlight`.repeat(3));
|
|
23
|
-
const b = doc('b.md', `${code} astronomy telescope galaxy nebula orbit planets observation stars`.repeat(3));
|
|
24
|
-
const pairs = selectCandidatePairs([a, b]);
|
|
25
|
-
assert.equal(pairs.length, 0, 'shared code blocks alone must not create a pair');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('selectCandidatePairs respects maxPairs cap', () => {
|
|
29
|
-
const docs = Array.from({ length: 6 }, (_, i) =>
|
|
30
|
-
doc(`d${i}.md`, 'payments service deployment canary rollout grafana monitoring alerts '.repeat(4))
|
|
31
|
-
);
|
|
32
|
-
assert.ok(selectCandidatePairs(docs, 3).length <= 3);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('collectCorpus: markdown only, skips ignored dirs and trivial files', async () => {
|
|
36
|
-
const root = await mkdtemp(path.join(tmpdir(), 'docgrity-'));
|
|
37
|
-
try {
|
|
38
|
-
const big = 'This document is long enough to be included in the corpus. '.repeat(3);
|
|
39
|
-
await writeFile(path.join(root, 'keep.md'), big);
|
|
40
|
-
await writeFile(path.join(root, 'skip.txt'), big);
|
|
41
|
-
await writeFile(path.join(root, 'tiny.md'), 'too short');
|
|
42
|
-
await mkdir(path.join(root, 'node_modules'), { recursive: true });
|
|
43
|
-
await writeFile(path.join(root, 'node_modules', 'dep.md'), big);
|
|
44
|
-
const docs = await collectCorpus(root);
|
|
45
|
-
assert.deepEqual(docs.map((d) => d.relPath), ['keep.md']);
|
|
46
|
-
assert.match(docs[0].hash, /^[0-9a-f]{64}$/);
|
|
47
|
-
} finally {
|
|
48
|
-
await rm(root, { recursive: true, force: true });
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('githubRepoSlug returns undefined outside a git repo', async () => {
|
|
53
|
-
const root = await mkdtemp(path.join(tmpdir(), 'docgrity-'));
|
|
54
|
-
try {
|
|
55
|
-
assert.equal(await githubRepoSlug(root), undefined);
|
|
56
|
-
} finally {
|
|
57
|
-
await rm(root, { recursive: true, force: true });
|
|
58
|
-
}
|
|
59
|
-
});
|