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
package/src/core/json.ts DELETED
@@ -1,58 +0,0 @@
1
- /**
2
- * Pure JSON extraction/validation helpers for LLM output. No vscode imports —
3
- * unit-testable in isolation. Every model response passes through these; any
4
- * invalid output is rejected, never partially trusted.
5
- */
6
-
7
- export interface Evidence {
8
- page: 'A' | 'B';
9
- excerpt: string;
10
- }
11
-
12
- export const SEVERITIES = new Set(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']);
13
-
14
- export const num = (v: unknown, lo = 0, hi = 1): number =>
15
- Math.min(hi, Math.max(lo, Number(v) || 0));
16
-
17
- export const str = (v: unknown): string => {
18
- if (typeof v === 'string') return v;
19
- if (typeof v === 'number' || typeof v === 'boolean') return String(v);
20
- return '';
21
- };
22
-
23
- export function validateEvidence(list: unknown): Evidence[] {
24
- return (Array.isArray(list) ? list : [])
25
- .map((e: any) => ({
26
- page: (e?.page === 'B' ? 'B' : 'A') as 'A' | 'B',
27
- excerpt: str(e?.excerpt).slice(0, 1000),
28
- }))
29
- .filter((e) => e.excerpt);
30
- }
31
-
32
- /**
33
- * Extract the first balanced top-level JSON object from model output.
34
- * Handles prose or code fences around the JSON and trailing text after it.
35
- */
36
- export function extractJson(text: string): unknown {
37
- const start = text.indexOf('{');
38
- if (start === -1) throw new Error('No JSON object in model output');
39
- let depth = 0;
40
- let inString = false;
41
- let escaped = false;
42
- for (let i = start; i < text.length; i++) {
43
- const c = text[i];
44
- if (inString) {
45
- if (escaped) escaped = false;
46
- else if (c === '\\') escaped = true;
47
- else if (c === '"') inString = false;
48
- continue;
49
- }
50
- if (c === '"') inString = true;
51
- else if (c === '{') depth++;
52
- else if (c === '}') {
53
- depth--;
54
- if (depth === 0) return JSON.parse(text.slice(start, i + 1));
55
- }
56
- }
57
- throw new Error('Unbalanced JSON object in model output');
58
- }
@@ -1,56 +0,0 @@
1
- /**
2
- * Cheap heuristic gates that decide whether a document is worth an LLM call.
3
- * These mirror the signals the open-question prompt looks for, so skipping
4
- * documents with no signals loses essentially no recall while avoiding the
5
- * most expensive step (an LLM round-trip) for the common clean-doc case.
6
- */
7
-
8
- const MARKERS =
9
- /\b(?:TODO|TBD|TBC|FIXME|to be (?:decided|determined|confirmed)|open question|needs? (?:input|decision|review)|undecided|unresolved)\b/i;
10
- const PLACEHOLDERS = /\?{2,}|<add here>|\[(?:placeholder|fill ?in|xxx)\]/i;
11
-
12
- /** Lines ending in "?" outside fenced code blocks. */
13
- function hasQuestionLine(text: string): boolean {
14
- let inFence = false;
15
- for (const line of text.split('\n')) {
16
- const t = line.trim();
17
- if (t.startsWith('```')) {
18
- inFence = !inFence;
19
- continue;
20
- }
21
- if (!inFence && t.endsWith('?')) return true;
22
- }
23
- return false;
24
- }
25
-
26
- export function hasOpenQuestionSignals(text: string): boolean {
27
- return MARKERS.test(text) || PLACEHOLDERS.test(text) || hasQuestionLine(text);
28
- }
29
-
30
- /**
31
- * Run an async mapper over items with bounded concurrency, preserving order.
32
- * Errors are returned per-item rather than thrown, so one failure never
33
- * aborts the batch.
34
- */
35
- export async function mapLimit<T, R>(
36
- items: readonly T[],
37
- limit: number,
38
- fn: (item: T, index: number) => Promise<R>
39
- ): Promise<Array<{ ok: true; value: R } | { ok: false; error: Error }>> {
40
- const results: Array<{ ok: true; value: R } | { ok: false; error: Error }> = new Array(
41
- items.length
42
- );
43
- let next = 0;
44
- const worker = async () => {
45
- while (next < items.length) {
46
- const i = next++;
47
- try {
48
- results[i] = { ok: true, value: await fn(items[i], i) };
49
- } catch (err) {
50
- results[i] = { ok: false, error: err as Error };
51
- }
52
- }
53
- };
54
- await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
55
- return results;
56
- }
package/src/core/slug.ts DELETED
@@ -1,10 +0,0 @@
1
- /**
2
- * Pure parsing of a GitHub remote URL into an owner/repo slug.
3
- * Supports https, ssh (git@), and ssh:// forms.
4
- */
5
- export function parseGithubSlug(remoteUrl: string): string | undefined {
6
- const m = /github\.com[:/]([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?$/.exec(
7
- remoteUrl.trim()
8
- );
9
- return m ? `${m[1]}/${m[2]}` : undefined;
10
- }
@@ -1,15 +0,0 @@
1
- /**
2
- * Hallucination guard — pure, unit-testable. Evidence excerpts returned by the
3
- * model must actually appear (whitespace-insensitively) in the source texts.
4
- */
5
- export function verifyExcerpts(
6
- excerpts: { excerpt: string }[],
7
- sourceTexts: string[]
8
- ): boolean {
9
- const norm = (s: string) => s.replace(/\s+/g, ' ').trim().toLowerCase();
10
- const haystacks = sourceTexts.map(norm);
11
- return excerpts.every((e) => {
12
- const needle = norm(e.excerpt).slice(0, 200);
13
- return needle.length > 0 && haystacks.some((h) => h.includes(needle));
14
- });
15
- }
package/src/extension.ts DELETED
@@ -1,142 +0,0 @@
1
- import * as vscode from 'vscode';
2
- import { FindingStore, Finding } from './findings/store';
3
- import { FindingsTree } from './findings/tree';
4
- import { registerDiagnostics, findRange } from './findings/diagnostics';
5
- import { buildReport } from './findings/report';
6
- import { runScan } from './scanner/scan';
7
- import { raiseIssue } from './github/issues';
8
- import { selectModelCommand } from './agents/selectModel';
9
- import { initLog, log } from './log';
10
-
11
- function issuesEnabled(): boolean {
12
- return (
13
- vscode.workspace.getConfiguration('docgrity').get<string>('mode', 'report-and-issue') !==
14
- 'report-only'
15
- );
16
- }
17
-
18
- export function activate(context: vscode.ExtensionContext): void {
19
- initLog(context);
20
- log.info(`Docgrity activated (v${context.extension.packageJSON.version})`);
21
- const store = new FindingStore(context.workspaceState);
22
- const tree = new FindingsTree(store);
23
- context.subscriptions.push(vscode.window.createTreeView('docgrity.findings', { treeDataProvider: tree }));
24
- registerDiagnostics(context, store);
25
-
26
- // Mode gating: in report-only mode the raise-issue surface is hidden entirely.
27
- const syncMode = () => {
28
- const enabled = issuesEnabled();
29
- void vscode.commands.executeCommand('setContext', 'docgrity.issuesEnabled', enabled);
30
- log.info(`Mode: ${enabled ? 'report-and-issue' : 'report-only'}`);
31
- };
32
- syncMode();
33
- context.subscriptions.push(
34
- vscode.workspace.onDidChangeConfiguration((e) => {
35
- if (e.affectsConfiguration('docgrity.mode')) syncMode();
36
- })
37
- );
38
-
39
- context.subscriptions.push(
40
- vscode.commands.registerCommand('docgrity.scan', async () => {
41
- try {
42
- const result = await vscode.window.withProgress(
43
- {
44
- location: vscode.ProgressLocation.Notification,
45
- title: 'Docgrity scan',
46
- cancellable: true,
47
- },
48
- (progress, token) => runScan(store, progress, token)
49
- );
50
- const suffix = result.errors > 0 ? ` ${result.errors} assessment(s) failed — see the Docgrity output log.` : '';
51
- const action = await vscode.window.showInformationMessage(
52
- `Docgrity: ${result.findings} finding(s) across ${result.docs} docs (${result.pairs} pairs assessed).${suffix}`,
53
- 'View findings',
54
- 'Open report'
55
- );
56
- if (action === 'View findings') {
57
- await vscode.commands.executeCommand('docgrity.findings.focus');
58
- } else if (action === 'Open report') {
59
- await vscode.commands.executeCommand('docgrity.openReport');
60
- }
61
- } catch (err) {
62
- log.error('Scan failed', err);
63
- void vscode.window.showErrorMessage(`Docgrity scan failed: ${(err as Error).message}`);
64
- }
65
- }),
66
-
67
- vscode.commands.registerCommand('docgrity.raiseIssue', async (node?: { finding?: Finding }) => {
68
- // Defense in depth: the menu is hidden in report-only mode, but the
69
- // command could still be invoked programmatically.
70
- if (!issuesEnabled()) {
71
- void vscode.window.showInformationMessage(
72
- 'Docgrity is in report-only mode (docgrity.mode). Switch to "report-and-issue" to raise GitHub issues.'
73
- );
74
- return;
75
- }
76
- const finding = node?.finding ?? (await pickFinding(store));
77
- if (!finding) return;
78
- try {
79
- await raiseIssue(finding, store);
80
- } catch (err) {
81
- log.error('Raise issue failed', err);
82
- void vscode.window.showErrorMessage(`Docgrity: ${(err as Error).message}`);
83
- }
84
- }),
85
-
86
- vscode.commands.registerCommand('docgrity.clearFindings', () => store.clear()),
87
-
88
- vscode.commands.registerCommand('docgrity.openReport', async () => {
89
- const name = vscode.workspace.workspaceFolders?.[0]?.name ?? 'workspace';
90
- const doc = await vscode.workspace.openTextDocument({
91
- language: 'markdown',
92
- content: buildReport(store.all(), name),
93
- });
94
- await vscode.window.showTextDocument(doc, { preview: false });
95
- // Untitled by design — save it wherever you like (e.g. docgrity-report.md).
96
- }),
97
-
98
- vscode.commands.registerCommand('docgrity.selectModel', selectModelCommand),
99
-
100
- vscode.commands.registerCommand(
101
- 'docgrity.openEvidence',
102
- async (relPath: string, excerpt: string) => {
103
- // Resolve against workspace folders directly — relPath must not be
104
- // treated as a glob (special characters could match the wrong file).
105
- for (const folder of vscode.workspace.workspaceFolders ?? []) {
106
- const uri = vscode.Uri.joinPath(folder.uri, relPath);
107
- try {
108
- const doc = await vscode.workspace.openTextDocument(uri);
109
- const editor = await vscode.window.showTextDocument(doc);
110
- const range = findRange(doc, excerpt);
111
- if (range) {
112
- editor.selection = new vscode.Selection(range.start, range.end);
113
- editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
114
- }
115
- return;
116
- } catch {
117
- // not in this folder; try the next one
118
- }
119
- }
120
- log.warn(`Evidence file not found in workspace: ${relPath}`);
121
- }
122
- )
123
- );
124
- }
125
-
126
- async function pickFinding(store: FindingStore): Promise<Finding | undefined> {
127
- const items = store.all().map((f) => ({
128
- label: `[${f.type}] ${f.summary.slice(0, 80)}`,
129
- description: f.files.join(', '),
130
- finding: f,
131
- }));
132
- if (items.length === 0) {
133
- void vscode.window.showInformationMessage('Docgrity: no findings. Run a scan first.');
134
- return undefined;
135
- }
136
- const pick = await vscode.window.showQuickPick(items, { placeHolder: 'Raise an issue for which finding?' });
137
- return pick?.finding;
138
- }
139
-
140
- export function deactivate(): void {
141
- // Nothing to clean up: all disposables are registered on the extension context.
142
- }
@@ -1,78 +0,0 @@
1
- /**
2
- * Diagnostics — surface evidence excerpts as squiggles on the markdown files
3
- * (the extension's equivalent of Confluence inline visibility).
4
- */
5
- import * as vscode from 'vscode';
6
- import { Finding, FindingStore } from './store';
7
-
8
- const SEVERITY: Record<string, vscode.DiagnosticSeverity> = {
9
- CRITICAL: vscode.DiagnosticSeverity.Error,
10
- HIGH: vscode.DiagnosticSeverity.Error,
11
- MEDIUM: vscode.DiagnosticSeverity.Warning,
12
- LOW: vscode.DiagnosticSeverity.Information,
13
- };
14
-
15
- export function registerDiagnostics(
16
- context: vscode.ExtensionContext,
17
- store: FindingStore
18
- ): void {
19
- const collection = vscode.languages.createDiagnosticCollection('docgrity');
20
- context.subscriptions.push(collection);
21
-
22
- const refresh = async () => {
23
- collection.clear();
24
- const byFile = new Map<string, { finding: Finding; excerpt: string }[]>();
25
- for (const f of store.all()) {
26
- for (const e of f.evidence) {
27
- const list = byFile.get(e.sourceLabel) ?? [];
28
- list.push({ finding: f, excerpt: e.excerpt });
29
- byFile.set(e.sourceLabel, list);
30
- }
31
- }
32
- for (const [relPath, entries] of byFile) {
33
- const uris = await vscode.workspace.findFiles(relPath, undefined, 1);
34
- if (uris.length === 0) continue;
35
- const doc = await vscode.workspace.openTextDocument(uris[0]);
36
- const diagnostics: vscode.Diagnostic[] = [];
37
- for (const { finding, excerpt } of entries) {
38
- const range = findRange(doc, excerpt);
39
- if (!range) continue;
40
- const d = new vscode.Diagnostic(
41
- range,
42
- `Docgrity ${finding.type}: ${finding.summary}`,
43
- SEVERITY[finding.severity] ?? vscode.DiagnosticSeverity.Warning
44
- );
45
- d.source = 'docgrity';
46
- diagnostics.push(d);
47
- }
48
- collection.set(uris[0], diagnostics);
49
- }
50
- };
51
-
52
- store.onDidChange(refresh);
53
- void refresh();
54
- }
55
-
56
- export function findRange(doc: vscode.TextDocument, excerpt: string): vscode.Range | undefined {
57
- const needle = excerpt.replace(/\s+/g, ' ').trim().slice(0, 120);
58
- if (!needle) return undefined;
59
- const text = doc.getText();
60
- const flat = text.replace(/\s+/g, ' ');
61
- const flatIdx = flat.toLowerCase().indexOf(needle.toLowerCase());
62
- if (flatIdx === -1) return undefined;
63
- // Map flattened index back to the original text approximately: walk both.
64
- let orig = 0;
65
- let flatPos = 0;
66
- while (flatPos < flatIdx && orig < text.length) {
67
- if (/\s/.test(text[orig])) {
68
- while (orig < text.length && /\s/.test(text[orig])) orig++;
69
- flatPos++;
70
- } else {
71
- orig++;
72
- flatPos++;
73
- }
74
- }
75
- const start = doc.positionAt(orig);
76
- const end = doc.positionAt(Math.min(orig + needle.length, text.length));
77
- return new vscode.Range(start, end);
78
- }
@@ -1,68 +0,0 @@
1
- /**
2
- * Markdown report generation — a portable artifact of the last scan that can
3
- * be viewed, saved, or shared without the extension UI.
4
- */
5
- import { Finding } from './store';
6
-
7
- const TYPE_LABELS: Record<string, string> = {
8
- contradiction: 'Contradictions',
9
- duplicate: 'Duplicates',
10
- open_question: 'Open questions',
11
- };
12
-
13
- export function buildReport(findings: Finding[], workspaceName: string): string {
14
- const lines: string[] = [];
15
- lines.push(`# Docgrity report — ${workspaceName}`);
16
- lines.push('');
17
- lines.push(`Generated: ${new Date().toISOString()}`);
18
- lines.push('');
19
-
20
- if (findings.length === 0) {
21
- lines.push('No findings. Run **Docgrity: Scan workspace docs** to (re)scan.');
22
- return lines.join('\n');
23
- }
24
-
25
- const byType = new Map<string, Finding[]>();
26
- for (const f of findings) {
27
- const list = byType.get(f.type) ?? [];
28
- list.push(f);
29
- byType.set(f.type, list);
30
- }
31
-
32
- lines.push('## Summary');
33
- lines.push('');
34
- lines.push('| Type | Count |');
35
- lines.push('|---|---|');
36
- for (const [type, list] of byType) {
37
- lines.push(`| ${TYPE_LABELS[type] ?? type} | ${list.length} |`);
38
- }
39
- lines.push(`| **Total** | **${findings.length}** |`);
40
- lines.push('');
41
-
42
- for (const [type, list] of byType) {
43
- lines.push(`## ${TYPE_LABELS[type] ?? type}`);
44
- lines.push('');
45
- for (const f of list) {
46
- lines.push(`### ${f.summary}`);
47
- lines.push('');
48
- lines.push(
49
- `- **Severity:** ${f.severity} · **Confidence:** ${(f.confidence * 100).toFixed(0)}%`
50
- );
51
- lines.push(`- **Files:** ${f.files.map((p) => `\`${p}\``).join(', ')}`);
52
- if (f.potentialOwners.length > 0) {
53
- lines.push(`- **Potential owner(s):** ${f.potentialOwners.join(', ')}`);
54
- }
55
- if (f.issueUrl) lines.push(`- **Issue:** ${f.issueUrl}`);
56
- lines.push(`- _model ${f.model}, prompt ${f.promptVersion}, ${f.createdAt}_`);
57
- lines.push('');
58
- lines.push('**Evidence:**');
59
- lines.push('');
60
- for (const e of f.evidence) {
61
- lines.push(`- \`${e.sourceLabel}\``);
62
- lines.push(` > ${e.excerpt.replace(/\n/g, '\n > ')}`);
63
- }
64
- lines.push('');
65
- }
66
- }
67
- return lines.join('\n');
68
- }
@@ -1,60 +0,0 @@
1
- /**
2
- * Findings model and persistence (workspaceState). Every finding carries
3
- * evidence, the model + prompt version that produced it, and potential-owner
4
- * labels only (never asserted ownership).
5
- */
6
- import * as vscode from 'vscode';
7
-
8
- export interface FindingEvidence {
9
- sourceLabel: string; // repo-relative file path
10
- excerpt: string;
11
- }
12
-
13
- export interface Finding {
14
- id: string;
15
- type: 'duplicate' | 'contradiction' | 'open_question';
16
- severity: string;
17
- confidence: number;
18
- summary: string;
19
- detail?: Record<string, unknown>;
20
- evidence: FindingEvidence[];
21
- files: string[]; // repo-relative paths involved
22
- potentialOwners: string[]; // git authors, labelled potential
23
- model: string;
24
- promptVersion: string;
25
- createdAt: string;
26
- issueUrl?: string;
27
- }
28
-
29
- const KEY = 'docgrity.findings';
30
-
31
- export class FindingStore {
32
- private readonly onChange = new vscode.EventEmitter<void>();
33
- readonly onDidChange = this.onChange.event;
34
-
35
- constructor(private readonly state: vscode.Memento) {}
36
-
37
- all(): Finding[] {
38
- return this.state.get<Finding[]>(KEY, []);
39
- }
40
-
41
- get(id: string): Finding | undefined {
42
- return this.all().find((f) => f.id === id);
43
- }
44
-
45
- async replaceAll(findings: Finding[]): Promise<void> {
46
- await this.state.update(KEY, findings);
47
- this.onChange.fire();
48
- }
49
-
50
- async update(finding: Finding): Promise<void> {
51
- const next = this.all().map((f) => (f.id === finding.id ? finding : f));
52
- await this.state.update(KEY, next);
53
- this.onChange.fire();
54
- }
55
-
56
- async clear(): Promise<void> {
57
- await this.state.update(KEY, []);
58
- this.onChange.fire();
59
- }
60
- }
@@ -1,93 +0,0 @@
1
- /**
2
- * Findings tree view — type -> finding -> evidence excerpts.
3
- */
4
- import * as vscode from 'vscode';
5
- import { Finding, FindingStore } from './store';
6
-
7
- type Node =
8
- | { kind: 'type'; type: string; label: string }
9
- | { kind: 'finding'; finding: Finding }
10
- | { kind: 'evidence'; finding: Finding; index: number };
11
-
12
- const TYPE_LABELS: Record<string, string> = {
13
- contradiction: 'Contradictions',
14
- duplicate: 'Duplicates',
15
- open_question: 'Open questions',
16
- };
17
-
18
- export class FindingsTree implements vscode.TreeDataProvider<Node> {
19
- private readonly emitter = new vscode.EventEmitter<Node | undefined>();
20
- readonly onDidChangeTreeData = this.emitter.event;
21
-
22
- constructor(private readonly store: FindingStore) {
23
- store.onDidChange(() => this.emitter.fire(undefined));
24
- }
25
-
26
- getTreeItem(node: Node): vscode.TreeItem {
27
- if (node.kind === 'type') {
28
- const count = this.store.all().filter((f) => f.type === node.type).length;
29
- const item = new vscode.TreeItem(
30
- `${node.label} (${count})`,
31
- vscode.TreeItemCollapsibleState.Expanded
32
- );
33
- return item;
34
- }
35
- if (node.kind === 'finding') {
36
- const f = node.finding;
37
- const item = new vscode.TreeItem(f.summary, vscode.TreeItemCollapsibleState.Collapsed);
38
- item.contextValue = 'finding';
39
- item.description = `${f.severity} · ${(f.confidence * 100).toFixed(0)}%${f.issueUrl ? ' · issue raised' : ''}`;
40
- item.tooltip = new vscode.MarkdownString(
41
- `**${f.type}** — ${f.summary}\n\nFiles: ${f.files.join(', ')}\n\nPotential owner(s): ${
42
- f.potentialOwners.join(', ') || 'unknown'
43
- }\n\n_model ${f.model}, prompt ${f.promptVersion}_`
44
- );
45
- item.iconPath = new vscode.ThemeIcon(
46
- f.type === 'contradiction' ? 'warning' : f.type === 'duplicate' ? 'copy' : 'question'
47
- );
48
- if (f.evidence.length > 0) {
49
- // Clicking a finding jumps straight to its first evidence excerpt.
50
- item.command = {
51
- command: 'docgrity.openEvidence',
52
- title: 'Open evidence',
53
- arguments: [f.evidence[0].sourceLabel, f.evidence[0].excerpt],
54
- };
55
- }
56
- return item;
57
- }
58
- const e = node.finding.evidence[node.index];
59
- const item = new vscode.TreeItem(
60
- `${e.sourceLabel}: “${e.excerpt.slice(0, 80)}…”`,
61
- vscode.TreeItemCollapsibleState.None
62
- );
63
- item.command = {
64
- command: 'docgrity.openEvidence',
65
- title: 'Open evidence',
66
- arguments: [e.sourceLabel, e.excerpt],
67
- };
68
- item.iconPath = new vscode.ThemeIcon('quote');
69
- return item;
70
- }
71
-
72
- getChildren(node?: Node): Node[] {
73
- if (!node) {
74
- return Object.entries(TYPE_LABELS)
75
- .filter(([type]) => this.store.all().some((f) => f.type === type))
76
- .map(([type, label]) => ({ kind: 'type', type, label }));
77
- }
78
- if (node.kind === 'type') {
79
- return this.store
80
- .all()
81
- .filter((f) => f.type === node.type)
82
- .map((finding) => ({ kind: 'finding', finding }));
83
- }
84
- if (node.kind === 'finding') {
85
- return node.finding.evidence.map((_e, index) => ({
86
- kind: 'evidence',
87
- finding: node.finding,
88
- index,
89
- }));
90
- }
91
- return [];
92
- }
93
- }
@@ -1,90 +0,0 @@
1
- /**
2
- * GitHub issue creation — the notify surface for this extension (the Forge
3
- * app's equivalent of a Confluence comment). Uses VS Code's built-in GitHub
4
- * authentication; requires explicit human approval before anything is posted.
5
- */
6
- import * as vscode from 'vscode';
7
- import { Finding, FindingStore } from '../findings/store';
8
- import { draftIssue } from '../agents/assess';
9
- import { githubRepoSlug } from './owners';
10
- import { log } from '../log';
11
-
12
- export async function raiseIssue(finding: Finding, store: FindingStore): Promise<void> {
13
- if (finding.issueUrl) {
14
- const open = 'Open existing issue';
15
- const pick = await vscode.window.showInformationMessage(
16
- 'An issue was already raised for this finding.',
17
- open
18
- );
19
- if (pick === open) void vscode.env.openExternal(vscode.Uri.parse(finding.issueUrl));
20
- return;
21
- }
22
-
23
- const slug = await githubRepoSlug();
24
- if (!slug) {
25
- void vscode.window.showErrorMessage(
26
- 'Docgrity: no GitHub origin remote found in this workspace.'
27
- );
28
- return;
29
- }
30
-
31
- const draft = await vscode.window.withProgress(
32
- { location: vscode.ProgressLocation.Notification, title: 'Docgrity: drafting issue…' },
33
- (_p, token) =>
34
- draftIssue(
35
- {
36
- type: finding.type,
37
- summary: finding.summary,
38
- evidence: finding.evidence,
39
- potentialOwners: finding.potentialOwners,
40
- },
41
- token
42
- )
43
- );
44
-
45
- // Human approval gate: show the draft before anything leaves the editor.
46
- const preview = await vscode.window.showInformationMessage(
47
- `Raise GitHub issue on ${slug}?\n\n${draft.title}`,
48
- { modal: true, detail: draft.body.slice(0, 1500) },
49
- 'Create issue'
50
- );
51
- if (preview !== 'Create issue') return;
52
-
53
- const session = await vscode.authentication.getSession('github', ['repo'], {
54
- createIfNone: true,
55
- });
56
-
57
- const res = await fetch(`https://api.github.com/repos/${slug}/issues`, {
58
- method: 'POST',
59
- headers: {
60
- Authorization: `Bearer ${session.accessToken}`,
61
- Accept: 'application/vnd.github+json',
62
- 'Content-Type': 'application/json',
63
- 'X-GitHub-Api-Version': '2022-11-28',
64
- },
65
- body: JSON.stringify({
66
- title: draft.title,
67
- body: `${draft.body}\n\n---\n_Raised by Docgrity (${finding.type}, confidence ${finding.confidence.toFixed(2)}, model ${finding.model}, prompt ${finding.promptVersion})._`,
68
- labels: ['docgrity', `docgrity:${finding.type}`],
69
- }),
70
- });
71
-
72
- if (!res.ok) {
73
- const text = await res.text().catch(() => '');
74
- log.error(`GitHub issue creation failed: HTTP ${res.status} on ${slug}`);
75
- void vscode.window.showErrorMessage(
76
- `Docgrity: GitHub issue creation failed (${res.status}). ${text.slice(0, 200)}`
77
- );
78
- return;
79
- }
80
-
81
- const issue = (await res.json()) as { html_url: string; number: number };
82
- log.info(`Issue #${issue.number} created on ${slug} for finding ${finding.id} (${finding.type})`);
83
- await store.update({ ...finding, issueUrl: issue.html_url });
84
- const open = `Open #${issue.number}`;
85
- const pick = await vscode.window.showInformationMessage(
86
- `Docgrity: issue #${issue.number} created on ${slug}.`,
87
- open
88
- );
89
- if (pick === open) void vscode.env.openExternal(vscode.Uri.parse(issue.html_url));
90
- }