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.
Files changed (55) hide show
  1. package/README.md +109 -126
  2. package/{action/action.yml → action.yml} +4 -1
  3. package/{action/bin → bin}/action.js +12 -6
  4. package/{action/bin → bin}/docgrity.js +23 -9
  5. package/package.json +31 -164
  6. package/src/heuristics.js +146 -0
  7. package/{action/src → src}/issues.js +4 -1
  8. package/{action/src → src}/report.js +2 -1
  9. package/{action/src → src}/scan.js +32 -7
  10. package/.github/workflows/ci.yml +0 -54
  11. package/.vscodeignore +0 -13
  12. package/action/LICENSE +0 -21
  13. package/action/README.md +0 -104
  14. package/action/examples/docgrity.yml +0 -61
  15. package/action/package.json +0 -38
  16. package/action/test/corpus.test.mjs +0 -59
  17. package/action/test/issues.test.mjs +0 -89
  18. package/action/test/report.test.mjs +0 -76
  19. package/docgrity_logo.png +0 -0
  20. package/image.png +0 -0
  21. package/media/icon.png +0 -0
  22. package/media/icon.svg +0 -5
  23. package/samples/api-limits.md +0 -23
  24. package/samples/architecture-notes.md +0 -28
  25. package/samples/deployment-guide.md +0 -23
  26. package/samples/integration-guide.md +0 -21
  27. package/samples/release-process.md +0 -23
  28. package/src/agents/assess.ts +0 -187
  29. package/src/agents/prompts.ts +0 -94
  30. package/src/agents/selectModel.ts +0 -50
  31. package/src/core/json.ts +0 -58
  32. package/src/core/prefilter.ts +0 -56
  33. package/src/core/slug.ts +0 -10
  34. package/src/core/verify.ts +0 -15
  35. package/src/extension.ts +0 -142
  36. package/src/findings/diagnostics.ts +0 -78
  37. package/src/findings/report.ts +0 -68
  38. package/src/findings/store.ts +0 -60
  39. package/src/findings/tree.ts +0 -93
  40. package/src/github/issues.ts +0 -90
  41. package/src/github/owners.ts +0 -60
  42. package/src/log.ts +0 -22
  43. package/src/scanner/candidates.ts +0 -62
  44. package/src/scanner/corpus.ts +0 -75
  45. package/src/scanner/scan.ts +0 -215
  46. package/test/candidates.test.ts +0 -63
  47. package/test/json.test.ts +0 -87
  48. package/test/prefilter.test.ts +0 -65
  49. package/test/slug.test.ts +0 -31
  50. package/test/verify.test.ts +0 -47
  51. package/tsconfig.json +0 -15
  52. package/vitest.config.mts +0 -9
  53. /package/{action/src → src}/corpus.js +0 -0
  54. /package/{action/src → src}/llm.js +0 -0
  55. /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
- const draft = await draftIssue(client, f);
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 pairs = checks.duplicates || checks.contradictions ? selectCandidatePairs(docs, maxPairs) : [];
42
- log(`Corpus: ${docs.length} markdown docs; assessing ${pairs.length} candidate pairs`);
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 ? await assessDuplicate(client, a, b) : null;
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 { findings, stats: { docs: docs.length, pairs: pairs.length, scannedAt: now } };
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
  }
@@ -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
@@ -1,13 +0,0 @@
1
- .vscode/**
2
- src/**
3
- test/**
4
- node_modules/**
5
- samples/**
6
- action/**
7
- .github/**
8
- tsconfig.json
9
- vitest.config.ts
10
- .gitignore
11
- **/*.map
12
- media/icon.svg.png
13
- *.vsix
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
@@ -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
- });