docgrity 0.1.3 → 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 +37 -7
- package/action.yml +4 -1
- package/bin/action.js +12 -6
- package/bin/docgrity.js +23 -9
- package/package.json +1 -1
- package/src/heuristics.js +146 -0
- package/src/issues.js +4 -1
- package/src/report.js +2 -1
- package/src/scan.js +32 -7
package/README.md
CHANGED
|
@@ -17,13 +17,20 @@ Part of the Docgrity family:
|
|
|
17
17
|
```yaml
|
|
18
18
|
- uses: ujjavala/docgrity-vscode/action@main
|
|
19
19
|
env:
|
|
20
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
20
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # for issue creation
|
|
21
21
|
with:
|
|
22
|
-
provider:
|
|
22
|
+
provider: anthropic # or openai / gemini / ollama
|
|
23
|
+
api_key: ${{ secrets.DOCGRITY_API_KEY }}
|
|
23
24
|
create_issues: 'true' # opt-in; default false
|
|
24
25
|
max_new_issues: 5
|
|
25
26
|
```
|
|
26
27
|
|
|
28
|
+
> **Note:** GitHub Models (`provider: github-models`) is being retired by GitHub —
|
|
29
|
+
> runs may fail with HTTP 410 (`github_models_retirement`). Use a BYO-key provider
|
|
30
|
+
> (`anthropic` / `openai` / `gemini`) or `ollama` instead. Copilot models are **not**
|
|
31
|
+
> available here: Copilot has no API outside the editor — only the VS Code extension
|
|
32
|
+
> can use it.
|
|
33
|
+
|
|
27
34
|
Full example with weekly schedule, PR trigger and Pages report publishing:
|
|
28
35
|
[examples/docgrity.yml](examples/docgrity.yml).
|
|
29
36
|
|
|
@@ -43,9 +50,29 @@ What it does per run:
|
|
|
43
50
|
|
|
44
51
|
| provider | key | cost |
|
|
45
52
|
|---|---|---|
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
53
|
+
| `anthropic` / `openai` / `gemini` | `api_key` input (use a repo secret) — defaults: `claude-3-5-haiku-latest`, `gpt-4o-mini`, `gemini-flash` | your key |
|
|
54
|
+
| `ollama` (CLI default) | none — local or tunnelled endpoint | free, fully private |
|
|
55
|
+
| `none` (no-agent mode) | none — zero LLM calls | free, instant, fully offline |
|
|
56
|
+
| `github-models` | none — uses `GITHUB_TOKEN` with `models: read` | **being retired by GitHub** (HTTP 410) — switch to a provider above |
|
|
57
|
+
|
|
58
|
+
### No-agent mode (`provider: none`)
|
|
59
|
+
|
|
60
|
+
Pure algorithms, no model, no keys, no network:
|
|
61
|
+
|
|
62
|
+
| Check | How | Notes |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| **Duplicates** | verbatim shared-block detection + TF-IDF similarity | catches copy-paste duplication; paraphrased duplication needs AI |
|
|
65
|
+
| **Open questions** | explicit markers (`TODO`, `TBD`, `FIXME`, `???`, "open question"…) | deterministic — high confidence; subtle unanswered questions need AI |
|
|
66
|
+
| **Contradictions** | ❌ **requires AI intelligence** (semantic understanding) — skipped, and the report says so | never guessed at heuristically |
|
|
67
|
+
|
|
68
|
+
Confidence is *measured*, not guessed: duplicate confidence comes from the
|
|
69
|
+
actual verbatim-overlap ratio; marker-based open questions score 0.85–0.95.
|
|
70
|
+
Tuned for precision — paraphrased text, shared code samples and boilerplate
|
|
71
|
+
headings never fire. Evidence is verbatim by construction. Findings carry
|
|
72
|
+
`method: heuristic`, and issue drafts use a deterministic template.
|
|
73
|
+
|
|
74
|
+
Great as a zero-setup PR pre-check (heuristics on every PR, full AI scan weekly)
|
|
75
|
+
or when no key/model is available.
|
|
49
76
|
|
|
50
77
|
## Local CLI (read-only)
|
|
51
78
|
|
|
@@ -73,7 +100,8 @@ Usage: docgrity scan [options]
|
|
|
73
100
|
--max-pairs <n> Max document pairs (default: 25)
|
|
74
101
|
--threshold-duplicate / --threshold-contradiction / --threshold-open-question <0..1>
|
|
75
102
|
|
|
76
|
-
--provider <p> ollama | gemini | openai | anthropic | github-models
|
|
103
|
+
--provider <p> none | ollama | gemini | openai | anthropic | github-models
|
|
104
|
+
(none = no-agent mode: algorithms only, contradictions skipped)
|
|
77
105
|
--model <m> Model name
|
|
78
106
|
--endpoint <url> Ollama endpoint (default http://localhost:11434)
|
|
79
107
|
|
|
@@ -82,11 +110,13 @@ Usage: docgrity scan [options]
|
|
|
82
110
|
```
|
|
83
111
|
|
|
84
112
|
Provider auto-detection: `DOCGRITY_API_KEY` set → `gemini`; else `GITHUB_TOKEN` →
|
|
85
|
-
`github-models
|
|
113
|
+
`github-models` (retiring — pass `--provider` explicitly); else → `ollama` (local,
|
|
114
|
+
fully private — nothing leaves your machine).
|
|
86
115
|
|
|
87
116
|
Examples:
|
|
88
117
|
|
|
89
118
|
```bash
|
|
119
|
+
docgrity scan --provider none --open # no-agent: no model, no keys
|
|
90
120
|
docgrity scan --checks contradictions # one check only
|
|
91
121
|
docgrity scan --checks duplicates,open-questions --max-pairs 10
|
|
92
122
|
docgrity scan --provider ollama --model llama3.1:8b # fully local
|
package/action.yml
CHANGED
|
@@ -8,7 +8,10 @@ branding:
|
|
|
8
8
|
color: 'blue'
|
|
9
9
|
inputs:
|
|
10
10
|
provider:
|
|
11
|
-
description:
|
|
11
|
+
description: >-
|
|
12
|
+
LLM provider: anthropic, openai, gemini (api_key required), ollama, github-models
|
|
13
|
+
(retiring), or "none" for no-agent mode — heuristic duplicates + open questions
|
|
14
|
+
only; contradiction detection requires an AI model and is skipped.
|
|
12
15
|
default: 'github-models'
|
|
13
16
|
api_key:
|
|
14
17
|
description: 'API key for gemini/openai/anthropic (pass a repo secret). Not needed for github-models.'
|
package/bin/action.js
CHANGED
|
@@ -18,13 +18,19 @@ const slug = process.env.GITHUB_REPOSITORY ?? '';
|
|
|
18
18
|
const branch = (process.env.GITHUB_REF_NAME ?? 'main').replace(/^refs\/heads\//, '');
|
|
19
19
|
|
|
20
20
|
try {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
const provider = input('provider', 'github-models');
|
|
22
|
+
// provider: none → no-agent mode: heuristic duplicates + open questions,
|
|
23
|
+
// contradictions skipped (they need AI), template-drafted issues.
|
|
24
|
+
const client =
|
|
25
|
+
provider === 'none'
|
|
26
|
+
? null
|
|
27
|
+
: makeClient({
|
|
28
|
+
provider,
|
|
29
|
+
apiKey: input('api_key'),
|
|
30
|
+
githubToken: token,
|
|
31
|
+
});
|
|
26
32
|
|
|
27
|
-
console.log(`Docgrity scan on ${slug} (${branch})`);
|
|
33
|
+
console.log(`Docgrity scan on ${slug} (${branch})${provider === 'none' ? ' — no-agent (heuristic) mode' : ''}`);
|
|
28
34
|
const { findings, stats } = await runScan(root, { client, log: (m) => console.log(m) });
|
|
29
35
|
|
|
30
36
|
// Opt-in issue sync (deduped by fingerprint, capped, auto-close resolved).
|
package/bin/docgrity.js
CHANGED
|
@@ -54,8 +54,12 @@ Options:
|
|
|
54
54
|
--threshold-contradiction <0..1> Min confidence, contradictions (default: 0.7)
|
|
55
55
|
--threshold-open-question <0..1> Min confidence, open questions (default: 0.6)
|
|
56
56
|
|
|
57
|
-
--provider <p> ollama | gemini | openai | anthropic | github-models
|
|
57
|
+
--provider <p> none | ollama | gemini | openai | anthropic | github-models
|
|
58
58
|
(default: auto — see below)
|
|
59
|
+
none = no-agent mode: pure algorithms, zero LLM calls.
|
|
60
|
+
Detects duplicates (verbatim shared blocks + TF-IDF)
|
|
61
|
+
and open questions (TODO/TBD/FIXME/???… markers).
|
|
62
|
+
Contradictions REQUIRE an AI model and are skipped.
|
|
59
63
|
--model <m> Model name (provider-specific default otherwise)
|
|
60
64
|
--endpoint <url> Ollama endpoint (default: http://localhost:11434,
|
|
61
65
|
or DOCGRITY_OLLAMA_URL; supports tunnelled remotes)
|
|
@@ -74,6 +78,7 @@ Provider auto-detection (when --provider is omitted):
|
|
|
74
78
|
|
|
75
79
|
Examples:
|
|
76
80
|
docgrity scan --open
|
|
81
|
+
docgrity scan --provider none # no-agent: no model, no keys, instant
|
|
77
82
|
docgrity scan --checks contradictions
|
|
78
83
|
docgrity scan --checks duplicates,open-questions --max-pairs 10
|
|
79
84
|
docgrity scan --provider ollama --model llama3.1:8b
|
|
@@ -148,16 +153,22 @@ const provider =
|
|
|
148
153
|
const checks = parseChecks(args.checks);
|
|
149
154
|
|
|
150
155
|
try {
|
|
151
|
-
const client =
|
|
152
|
-
provider
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
const client =
|
|
157
|
+
provider === 'none'
|
|
158
|
+
? null
|
|
159
|
+
: makeClient({
|
|
160
|
+
provider,
|
|
161
|
+
apiKey: process.env.DOCGRITY_API_KEY,
|
|
162
|
+
githubToken: process.env.GITHUB_TOKEN || process.env.GH_TOKEN,
|
|
163
|
+
model: args.model,
|
|
164
|
+
endpoint: args.endpoint,
|
|
165
|
+
});
|
|
158
166
|
|
|
159
167
|
const enabled = Object.entries(checks).filter(([, on]) => on).map(([k]) => k).join(', ');
|
|
160
|
-
console.log(`Docgrity local scan (read-only) — provider: ${provider}; checks: ${enabled}`);
|
|
168
|
+
console.log(`Docgrity local scan (read-only) — provider: ${provider}${provider === 'none' ? ' (no-agent, heuristics only)' : ''}; checks: ${enabled}`);
|
|
169
|
+
if (provider === 'none' && checks.contradictions) {
|
|
170
|
+
console.log(' Note: contradiction detection needs AI intelligence — it will be skipped. Use an LLM provider to enable it.');
|
|
171
|
+
}
|
|
161
172
|
const { findings, stats } = await runScan(root, {
|
|
162
173
|
client,
|
|
163
174
|
checks,
|
|
@@ -181,6 +192,9 @@ try {
|
|
|
181
192
|
await writeFile(path.join(outDir, 'findings.json'), JSON.stringify({ findings, stats }, null, 2));
|
|
182
193
|
|
|
183
194
|
console.log(`\n${findings.length} finding(s) across ${stats.docs} docs.`);
|
|
195
|
+
for (const f of findings) {
|
|
196
|
+
console.log(` [${f.severity}] ${f.type} — ${(f.confidence * 100).toFixed(0)}% confidence — ${f.files.join(' + ')}`);
|
|
197
|
+
}
|
|
184
198
|
console.log(`Report: ${reportPath}`);
|
|
185
199
|
console.log('Note: local scans are read-only — review owners and evidence in the report; issues are only raised by CI (opt-in).');
|
|
186
200
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docgrity",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"bin": { "docgrity": "bin/docgrity.js" },
|
|
@@ -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
|
+
}
|
package/src/issues.js
CHANGED
|
@@ -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
|
---
|
package/src/report.js
CHANGED
|
@@ -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>
|
package/src/scan.js
CHANGED
|
@@ -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
|
}
|