docgrity 0.1.2 → 0.1.3
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 +80 -127
- package/package.json +31 -164
- 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/action.yml → action.yml} +0 -0
- /package/{action/bin → bin}/action.js +0 -0
- /package/{action/bin → bin}/docgrity.js +0 -0
- /package/{action/src → src}/corpus.js +0 -0
- /package/{action/src → src}/issues.js +0 -0
- /package/{action/src → src}/llm.js +0 -0
- /package/{action/src → src}/prompts.js +0 -0
- /package/{action/src → src}/report.js +0 -0
- /package/{action/src → src}/scan.js +0 -0
|
@@ -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
|
-
});
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/** issues.js tests — dedupe by fingerprint, close resolved, cap creation.
|
|
2
|
-
* GitHub API is mocked via global fetch; the LLM client is a stub. */
|
|
3
|
-
import { test, beforeEach } from 'node:test';
|
|
4
|
-
import assert from 'node:assert/strict';
|
|
5
|
-
import { syncIssues } from '../src/issues.js';
|
|
6
|
-
|
|
7
|
-
const SLUG = 'me/repo';
|
|
8
|
-
const MARKER = (fp) => `<!-- docgrity:fingerprint:${fp} -->`;
|
|
9
|
-
|
|
10
|
-
const stubClient = {
|
|
11
|
-
model: 'stub',
|
|
12
|
-
async complete() {
|
|
13
|
-
return JSON.stringify({ title: 'Drafted title', body: 'Drafted body' });
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const finding = (fp, over = {}) => ({
|
|
18
|
-
fingerprint: fp,
|
|
19
|
-
type: 'duplicate',
|
|
20
|
-
severity: 'MEDIUM',
|
|
21
|
-
confidence: 0.8,
|
|
22
|
-
summary: 'dup',
|
|
23
|
-
evidence: [{ sourceLabel: 'a.md', excerpt: 'x' }],
|
|
24
|
-
files: ['a.md'],
|
|
25
|
-
potentialOwners: [],
|
|
26
|
-
model: 'stub',
|
|
27
|
-
promptVersion: 'v1',
|
|
28
|
-
...over,
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
let calls;
|
|
32
|
-
let openIssues;
|
|
33
|
-
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
calls = [];
|
|
36
|
-
openIssues = [];
|
|
37
|
-
let nextNumber = 100;
|
|
38
|
-
globalThis.fetch = async (url, init = {}) => {
|
|
39
|
-
const method = init.method ?? 'GET';
|
|
40
|
-
const u = new URL(url);
|
|
41
|
-
calls.push({ method, path: u.pathname, body: init.body ? JSON.parse(init.body) : null });
|
|
42
|
-
assert.match(init.headers.Authorization, /^Bearer /, 'token must be a bearer header');
|
|
43
|
-
if (method === 'GET') return { ok: true, status: 200, json: async () => openIssues };
|
|
44
|
-
if (method === 'POST' && u.pathname.endsWith('/issues')) {
|
|
45
|
-
const n = nextNumber++;
|
|
46
|
-
return { ok: true, status: 201, json: async () => ({ number: n, html_url: `https://github.com/${SLUG}/issues/${n}` }) };
|
|
47
|
-
}
|
|
48
|
-
return { ok: true, status: 200, json: async () => ({}) };
|
|
49
|
-
};
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('creates issues for new findings with fingerprint marker and labels', async () => {
|
|
53
|
-
const f = finding('aaaaaaaaaaaaaaaa');
|
|
54
|
-
const result = await syncIssues({ client: stubClient, token: 't', slug: SLUG, findings: [f] });
|
|
55
|
-
assert.equal(result.created.length, 1);
|
|
56
|
-
const create = calls.find((c) => c.method === 'POST' && c.path === `/repos/${SLUG}/issues`);
|
|
57
|
-
assert.ok(create.body.body.includes(MARKER('aaaaaaaaaaaaaaaa')), 'marker must be embedded');
|
|
58
|
-
assert.deepEqual(create.body.labels, ['docgrity', 'docgrity:duplicate']);
|
|
59
|
-
assert.ok(f.issueUrl, 'finding gets its issue url');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('dedupes: existing fingerprint means no new issue', async () => {
|
|
63
|
-
openIssues = [{ number: 7, html_url: 'u', body: `text ${MARKER('bbbbbbbbbbbbbbbb')}` }];
|
|
64
|
-
const result = await syncIssues({
|
|
65
|
-
client: stubClient,
|
|
66
|
-
token: 't',
|
|
67
|
-
slug: SLUG,
|
|
68
|
-
findings: [finding('bbbbbbbbbbbbbbbb')],
|
|
69
|
-
});
|
|
70
|
-
assert.equal(result.created.length, 0);
|
|
71
|
-
assert.equal(result.unchanged, 1);
|
|
72
|
-
assert.ok(!calls.some((c) => c.method === 'POST' && c.path === `/repos/${SLUG}/issues`));
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('closes issues whose finding is resolved', async () => {
|
|
76
|
-
openIssues = [{ number: 9, html_url: 'u', body: MARKER('cccccccccccccccc') }];
|
|
77
|
-
const result = await syncIssues({ client: stubClient, token: 't', slug: SLUG, findings: [] });
|
|
78
|
-
assert.deepEqual(result.closed, [9]);
|
|
79
|
-
const patch = calls.find((c) => c.method === 'PATCH');
|
|
80
|
-
assert.deepEqual(patch.body, { state: 'closed' });
|
|
81
|
-
assert.ok(calls.some((c) => c.path.endsWith('/9/comments')), 'closing comment posted');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('caps new issues per run', async () => {
|
|
85
|
-
const findings = ['1111111111111111', '2222222222222222', '3333333333333333'].map(finding);
|
|
86
|
-
const result = await syncIssues({ client: stubClient, token: 't', slug: SLUG, findings, maxNewIssues: 2 });
|
|
87
|
-
assert.equal(result.created.length, 2);
|
|
88
|
-
assert.equal(result.skipped, 1);
|
|
89
|
-
});
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/** report.js tests — dashboard rendering, XSS escaping, empty state. */
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
import assert from 'node:assert/strict';
|
|
4
|
-
import { renderReport, renderSummaryMarkdown } from '../src/report.js';
|
|
5
|
-
|
|
6
|
-
const stats = { docs: 5, pairs: 9, scannedAt: '2026-09-05T00:00:00Z' };
|
|
7
|
-
|
|
8
|
-
const finding = (over = {}) => ({
|
|
9
|
-
fingerprint: 'abcd1234abcd1234',
|
|
10
|
-
type: 'contradiction',
|
|
11
|
-
severity: 'HIGH',
|
|
12
|
-
confidence: 0.91,
|
|
13
|
-
summary: 'Docs disagree on rate limits',
|
|
14
|
-
detail: { conflicting_claims: ['100 rpm', '1000 rpm'] },
|
|
15
|
-
evidence: [{ sourceLabel: 'a.md', excerpt: 'limited to 100 requests' }],
|
|
16
|
-
files: ['a.md', 'b.md'],
|
|
17
|
-
potentialOwners: ['alice'],
|
|
18
|
-
model: 'gemini:test',
|
|
19
|
-
promptVersion: 'v1',
|
|
20
|
-
createdAt: '2026-09-05T00:00:00Z',
|
|
21
|
-
...over,
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('renderReport shows stat cards with correct counts', () => {
|
|
25
|
-
const html = renderReport({
|
|
26
|
-
findings: [finding(), finding({ type: 'duplicate', fingerprint: 'ffff0000ffff0000' })],
|
|
27
|
-
stats,
|
|
28
|
-
});
|
|
29
|
-
assert.match(html, /Open findings/);
|
|
30
|
-
assert.match(html, /<div class="num">2<\/div>/); // total
|
|
31
|
-
assert.match(html, /Duplicates/);
|
|
32
|
-
assert.match(html, /Contradictions/);
|
|
33
|
-
assert.match(html, /Open questions/);
|
|
34
|
-
assert.match(html, /class="ic/); // icons present
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('renderReport escapes HTML everywhere (XSS guard)', () => {
|
|
38
|
-
const html = renderReport({
|
|
39
|
-
findings: [
|
|
40
|
-
finding({
|
|
41
|
-
summary: '<script>alert(1)</script>',
|
|
42
|
-
files: ['<img src=x onerror=1>.md'],
|
|
43
|
-
evidence: [{ sourceLabel: '"><svg>', excerpt: '<iframe>' }],
|
|
44
|
-
potentialOwners: ['<b>owner</b>'],
|
|
45
|
-
detail: { conflicting_claims: ['<script>x</script>'] },
|
|
46
|
-
}),
|
|
47
|
-
],
|
|
48
|
-
stats,
|
|
49
|
-
});
|
|
50
|
-
assert.ok(!html.includes('<script>alert(1)</script>'));
|
|
51
|
-
assert.ok(!html.includes('<img src=x'));
|
|
52
|
-
assert.ok(!html.includes('<iframe>'));
|
|
53
|
-
assert.ok(!html.includes('<b>owner</b>'));
|
|
54
|
-
assert.ok(html.includes('<script>alert(1)</script>'));
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('renderReport links files only when repoSlug present', () => {
|
|
58
|
-
const withSlug = renderReport({ findings: [finding()], stats, repoSlug: 'me/repo', branch: 'main' });
|
|
59
|
-
assert.match(withSlug, /https:\/\/github\.com\/me\/repo\/blob\/main\/a\.md/);
|
|
60
|
-
const noSlug = renderReport({ findings: [finding()], stats });
|
|
61
|
-
assert.ok(!noSlug.includes('github.com/me/repo'));
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('renderReport shows friendly empty state', () => {
|
|
65
|
-
const html = renderReport({ findings: [], stats });
|
|
66
|
-
assert.match(html, /No findings/);
|
|
67
|
-
assert.match(html, /class="empty"/);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('renderSummaryMarkdown escapes pipes and lists findings', () => {
|
|
71
|
-
const md = renderSummaryMarkdown({ findings: [finding({ summary: 'a | b' })], stats });
|
|
72
|
-
assert.match(md, /a \\\| b/);
|
|
73
|
-
assert.match(md, /\| contradiction \| HIGH \| 91% \|/);
|
|
74
|
-
const empty = renderSummaryMarkdown({ findings: [], stats });
|
|
75
|
-
assert.match(empty, /No findings/);
|
|
76
|
-
});
|
package/docgrity_logo.png
DELETED
|
Binary file
|
package/image.png
DELETED
|
Binary file
|
package/media/icon.png
DELETED
|
Binary file
|
package/media/icon.svg
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
-
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
|
3
|
-
<path d="M14 2v6h6"/>
|
|
4
|
-
<path d="M9 15l2 2 4-4"/>
|
|
5
|
-
</svg>
|
package/samples/api-limits.md
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# API Rate Limits
|
|
2
|
-
|
|
3
|
-
Rate limiting policy for the public payments API.
|
|
4
|
-
|
|
5
|
-
## Limits
|
|
6
|
-
|
|
7
|
-
Every API key is limited to **100 requests per minute**. Requests beyond the
|
|
8
|
-
limit receive HTTP `429 Too Many Requests` with a `Retry-After` header.
|
|
9
|
-
|
|
10
|
-
Burst traffic is not tolerated: the limiter uses a fixed one-minute window
|
|
11
|
-
with no burst allowance.
|
|
12
|
-
|
|
13
|
-
## Authentication
|
|
14
|
-
|
|
15
|
-
All API requests must authenticate with the `X-Api-Key` header. Bearer tokens
|
|
16
|
-
are **not supported** on the public API — they are reserved for internal
|
|
17
|
-
service-to-service calls.
|
|
18
|
-
|
|
19
|
-
## Webhooks
|
|
20
|
-
|
|
21
|
-
Webhook deliveries are retried up to 3 times with exponential backoff. After
|
|
22
|
-
the third failure the webhook endpoint is disabled and the account owner is
|
|
23
|
-
emailed.
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# Architecture Notes — Payments Service
|
|
2
|
-
|
|
3
|
-
Working notes from the March architecture review. Several decisions are still
|
|
4
|
-
open and need owners.
|
|
5
|
-
|
|
6
|
-
## Data store
|
|
7
|
-
|
|
8
|
-
We currently use Postgres for the ledger. TBD: do we shard by merchant ID or
|
|
9
|
-
by region? The sharding decision blocks the multi-region rollout and nobody
|
|
10
|
-
has been assigned to make the call.
|
|
11
|
-
|
|
12
|
-
## Event bus
|
|
13
|
-
|
|
14
|
-
Kafka vs SQS is still an open question. Kafka gives us replay but SQS is what
|
|
15
|
-
the platform team supports. Who decides this? Unclear — the platform team says
|
|
16
|
-
it is a product decision, product says it is a platform decision.
|
|
17
|
-
|
|
18
|
-
## Idempotency
|
|
19
|
-
|
|
20
|
-
TODO: we have not defined what the idempotency key format should be for the
|
|
21
|
-
new refunds endpoint. The old endpoint used UUIDv4 but there was a proposal to
|
|
22
|
-
switch to ULIDs — no conclusion was reached in the review.
|
|
23
|
-
|
|
24
|
-
## PCI scope
|
|
25
|
-
|
|
26
|
-
It remains unresolved whether the new tokenisation proxy takes the webhook
|
|
27
|
-
relay out of PCI scope. Awaiting a ruling from the compliance team (asked in
|
|
28
|
-
January, no response yet).
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# Deployment Guide
|
|
2
|
-
|
|
3
|
-
This document explains how to deploy the payments service to production.
|
|
4
|
-
|
|
5
|
-
## Prerequisites
|
|
6
|
-
|
|
7
|
-
- Node.js 22 installed locally
|
|
8
|
-
- Access to the `prod-deployers` GitHub team
|
|
9
|
-
- The `PAYMENTS_DEPLOY_KEY` secret configured in your environment
|
|
10
|
-
|
|
11
|
-
## Steps
|
|
12
|
-
|
|
13
|
-
1. Create a release branch from `main` named `release/vX.Y.Z`.
|
|
14
|
-
2. Run the full test suite: `npm test -- --coverage`.
|
|
15
|
-
3. Bump the version in `package.json` and update `CHANGELOG.md`.
|
|
16
|
-
4. Tag the release: `git tag vX.Y.Z && git push --tags`.
|
|
17
|
-
5. Trigger the deploy workflow from the Actions tab, selecting the tag.
|
|
18
|
-
6. Watch the canary rollout in Grafana for 15 minutes before promoting to 100%.
|
|
19
|
-
|
|
20
|
-
## Rollback
|
|
21
|
-
|
|
22
|
-
If error rates exceed 1% during canary, run the `rollback` workflow with the
|
|
23
|
-
previous tag. Rollbacks complete in under 5 minutes.
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# Partner Integration Guide
|
|
2
|
-
|
|
3
|
-
A quick-start guide for partners integrating with the public payments API.
|
|
4
|
-
|
|
5
|
-
## Getting started
|
|
6
|
-
|
|
7
|
-
Request an API key from the developer portal. Every API key allows
|
|
8
|
-
**1000 requests per minute**, and the rate limiter supports short bursts of
|
|
9
|
-
up to 2000 requests thanks to a sliding-window algorithm with burst credit.
|
|
10
|
-
|
|
11
|
-
## Authentication
|
|
12
|
-
|
|
13
|
-
Authenticate every request by sending your key as a Bearer token in the
|
|
14
|
-
`Authorization` header: `Authorization: Bearer <api-key>`. This is the only
|
|
15
|
-
supported authentication method for the public payments API.
|
|
16
|
-
|
|
17
|
-
## Webhooks
|
|
18
|
-
|
|
19
|
-
Webhook deliveries are retried up to 10 times over 24 hours. Endpoints are
|
|
20
|
-
never disabled automatically — failed deliveries simply expire after the
|
|
21
|
-
final retry.
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# Release Process
|
|
2
|
-
|
|
3
|
-
How to release the payments service to production.
|
|
4
|
-
|
|
5
|
-
## Before you start
|
|
6
|
-
|
|
7
|
-
- Node.js 22 installed locally
|
|
8
|
-
- Membership of the `prod-deployers` GitHub team
|
|
9
|
-
- The `PAYMENTS_DEPLOY_KEY` secret configured in your environment
|
|
10
|
-
|
|
11
|
-
## Process
|
|
12
|
-
|
|
13
|
-
1. Create a release branch from `main` named `release/vX.Y.Z`.
|
|
14
|
-
2. Run the full test suite with coverage: `npm test -- --coverage`.
|
|
15
|
-
3. Bump the version in `package.json` and update `CHANGELOG.md`.
|
|
16
|
-
4. Tag the release: `git tag vX.Y.Z && git push --tags`.
|
|
17
|
-
5. Trigger the deploy workflow from the GitHub Actions tab, selecting the tag.
|
|
18
|
-
6. Watch the canary rollout in Grafana for 15 minutes before promoting to 100%.
|
|
19
|
-
|
|
20
|
-
## Rolling back
|
|
21
|
-
|
|
22
|
-
If error rates exceed 1% during the canary phase, run the `rollback` workflow
|
|
23
|
-
with the previous tag. A rollback completes in under 5 minutes.
|
package/src/agents/assess.ts
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LLM access — all requests go through vscode.lm (the user's own Copilot
|
|
3
|
-
* subscription). Typed JSON only: every response is extracted and validated
|
|
4
|
-
* in code; invalid output is rejected, never partially trusted.
|
|
5
|
-
*/
|
|
6
|
-
import * as vscode from 'vscode';
|
|
7
|
-
import { PROMPTS } from './prompts';
|
|
8
|
-
import type { Doc } from '../scanner/corpus';
|
|
9
|
-
import { log } from '../log';
|
|
10
|
-
import {
|
|
11
|
-
extractJson,
|
|
12
|
-
num,
|
|
13
|
-
str,
|
|
14
|
-
validateEvidence,
|
|
15
|
-
SEVERITIES,
|
|
16
|
-
} from '../core/json';
|
|
17
|
-
|
|
18
|
-
export type { Evidence } from '../core/json';
|
|
19
|
-
|
|
20
|
-
export interface OpenQuestion {
|
|
21
|
-
question: string;
|
|
22
|
-
excerpt: string;
|
|
23
|
-
confidence: number;
|
|
24
|
-
severity: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function pickModel(): Promise<vscode.LanguageModelChat> {
|
|
28
|
-
// Models rotate; select at call time. Vendor/family are configurable so any
|
|
29
|
-
// vscode.lm provider works — Copilot, or local models (e.g. Ollama/llama via
|
|
30
|
-
// Copilot's "Manage models" BYOK, or a local-provider extension's vendor).
|
|
31
|
-
const cfg = vscode.workspace.getConfiguration('docgrity');
|
|
32
|
-
const vendor = cfg.get<string>('model.vendor', 'copilot');
|
|
33
|
-
const family = cfg.get<string>('model.family', '');
|
|
34
|
-
|
|
35
|
-
if (family) {
|
|
36
|
-
const preferred = await vscode.lm.selectChatModels({ vendor, family });
|
|
37
|
-
if (preferred.length > 0) return preferred[0];
|
|
38
|
-
}
|
|
39
|
-
const any = await vscode.lm.selectChatModels(vendor ? { vendor } : {});
|
|
40
|
-
if (any.length === 0) {
|
|
41
|
-
throw new Error(
|
|
42
|
-
`No language model available for vendor "${vendor || 'any'}". Sign in to GitHub Copilot, ` +
|
|
43
|
-
'or set docgrity.model.vendor/family to a locally provided model (e.g. Ollama).'
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
log.debug(`Model selected: ${any[0].id} (vendor=${vendor || 'any'}, no family match)`);
|
|
47
|
-
return any[0];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function completeJson<T>(opts: {
|
|
51
|
-
system: string;
|
|
52
|
-
prompt: string;
|
|
53
|
-
validate: (raw: any) => T;
|
|
54
|
-
token: vscode.CancellationToken;
|
|
55
|
-
}): Promise<{ output: T; model: string }> {
|
|
56
|
-
const model = await pickModel();
|
|
57
|
-
const messages = [
|
|
58
|
-
vscode.LanguageModelChatMessage.Assistant(opts.system),
|
|
59
|
-
vscode.LanguageModelChatMessage.User(opts.prompt),
|
|
60
|
-
];
|
|
61
|
-
const started = Date.now();
|
|
62
|
-
const response = await model.sendRequest(messages, {}, opts.token);
|
|
63
|
-
let text = '';
|
|
64
|
-
for await (const chunk of response.text) text += chunk;
|
|
65
|
-
try {
|
|
66
|
-
const output = opts.validate(extractJson(text));
|
|
67
|
-
log.trace(`LLM response validated (model=${model.id}, ${Date.now() - started}ms)`);
|
|
68
|
-
return { output, model: model.id };
|
|
69
|
-
} catch (err) {
|
|
70
|
-
log.warn(`LLM response rejected (model=${model.id}): ${(err as Error).message}`);
|
|
71
|
-
throw err;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function pairPrompt(a: Doc, b: Doc): string {
|
|
76
|
-
return `PAGE A — "${a.relPath}":\n${a.text.slice(0, 8000)}\n\n---\n\nPAGE B — "${b.relPath}":\n${b.text.slice(0, 8000)}`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function validateDuplicate(o: any) {
|
|
80
|
-
return {
|
|
81
|
-
is_duplicate: Boolean(o.is_duplicate),
|
|
82
|
-
confidence: num(o.confidence),
|
|
83
|
-
summary: str(o.summary).slice(0, 2000),
|
|
84
|
-
recommended_action: str(o.recommended_action || 'REVIEW'),
|
|
85
|
-
evidence: validateEvidence(o.evidence),
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function validateContradiction(o: any) {
|
|
90
|
-
return {
|
|
91
|
-
is_contradiction: Boolean(o.is_contradiction),
|
|
92
|
-
confidence: num(o.confidence),
|
|
93
|
-
severity: SEVERITIES.has(o.severity) ? (o.severity as string) : 'MEDIUM',
|
|
94
|
-
summary: str(o.summary).slice(0, 2000),
|
|
95
|
-
conflicting_claims: (Array.isArray(o.conflicting_claims) ? o.conflicting_claims : []).map(
|
|
96
|
-
(c: unknown) => str(c).slice(0, 500)
|
|
97
|
-
),
|
|
98
|
-
evidence: validateEvidence(o.evidence),
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Combined duplicate + contradiction assessment in a single LLM call — the
|
|
104
|
-
* model reads the same two documents once instead of twice, halving pair
|
|
105
|
-
* latency and token cost with the same rules and output contracts.
|
|
106
|
-
*/
|
|
107
|
-
export async function assessPair(a: Doc, b: Doc, token: vscode.CancellationToken) {
|
|
108
|
-
const p = PROMPTS.pair;
|
|
109
|
-
const { output, model } = await completeJson({
|
|
110
|
-
system: p.system,
|
|
111
|
-
prompt: pairPrompt(a, b),
|
|
112
|
-
token,
|
|
113
|
-
validate: (o) => ({
|
|
114
|
-
duplicate: validateDuplicate(o.duplicate ?? {}),
|
|
115
|
-
contradiction: validateContradiction(o.contradiction ?? {}),
|
|
116
|
-
}),
|
|
117
|
-
});
|
|
118
|
-
return { output, model, promptVersion: p.version };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export async function assessDuplicate(a: Doc, b: Doc, token: vscode.CancellationToken) {
|
|
122
|
-
const p = PROMPTS.duplicate;
|
|
123
|
-
const { output, model } = await completeJson({
|
|
124
|
-
system: p.system,
|
|
125
|
-
prompt: pairPrompt(a, b),
|
|
126
|
-
token,
|
|
127
|
-
validate: validateDuplicate,
|
|
128
|
-
});
|
|
129
|
-
return { output, model, promptVersion: p.version };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export async function assessContradiction(a: Doc, b: Doc, token: vscode.CancellationToken) {
|
|
133
|
-
const p = PROMPTS.contradiction;
|
|
134
|
-
const { output, model } = await completeJson({
|
|
135
|
-
system: p.system,
|
|
136
|
-
prompt: pairPrompt(a, b),
|
|
137
|
-
token,
|
|
138
|
-
validate: validateContradiction,
|
|
139
|
-
});
|
|
140
|
-
return { output, model, promptVersion: p.version };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export async function assessOpenQuestions(doc: Doc, token: vscode.CancellationToken) {
|
|
144
|
-
const p = PROMPTS.open_question;
|
|
145
|
-
const { output, model } = await completeJson({
|
|
146
|
-
system: p.system,
|
|
147
|
-
prompt: `Document path: ${doc.relPath}\n\nDocument content:\n${doc.text.slice(0, 12000)}`,
|
|
148
|
-
token,
|
|
149
|
-
validate: (o) => ({
|
|
150
|
-
questions: (Array.isArray(o.questions) ? o.questions : [])
|
|
151
|
-
.map(
|
|
152
|
-
(q: any): OpenQuestion => ({
|
|
153
|
-
question: str(q.question).slice(0, 500),
|
|
154
|
-
excerpt: str(q.excerpt).slice(0, 1000),
|
|
155
|
-
confidence: num(q.confidence),
|
|
156
|
-
severity: SEVERITIES.has(q.severity) ? (q.severity as string) : 'MEDIUM',
|
|
157
|
-
})
|
|
158
|
-
)
|
|
159
|
-
.filter((q: OpenQuestion) => q.question && q.excerpt),
|
|
160
|
-
}),
|
|
161
|
-
});
|
|
162
|
-
return { output, model, promptVersion: p.version };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export async function draftIssue(
|
|
166
|
-
finding: {
|
|
167
|
-
type: string;
|
|
168
|
-
summary: string;
|
|
169
|
-
evidence: { sourceLabel: string; excerpt: string }[];
|
|
170
|
-
potentialOwners: string[];
|
|
171
|
-
},
|
|
172
|
-
token: vscode.CancellationToken
|
|
173
|
-
): Promise<{ title: string; body: string }> {
|
|
174
|
-
const p = PROMPTS.issue;
|
|
175
|
-
const { output } = await completeJson({
|
|
176
|
-
system: p.system,
|
|
177
|
-
prompt: `Finding type: ${finding.type}\nSummary: ${finding.summary}\nEvidence:\n${finding.evidence
|
|
178
|
-
.map((e) => `- [${e.sourceLabel}] "${e.excerpt}"`)
|
|
179
|
-
.join('\n')}\nPotential owners: ${finding.potentialOwners.join(', ') || 'unknown'}`,
|
|
180
|
-
token,
|
|
181
|
-
validate: (o) => ({
|
|
182
|
-
title: str(o.title).slice(0, 200) || 'Docgrity finding',
|
|
183
|
-
body: str(o.body).slice(0, 20000),
|
|
184
|
-
}),
|
|
185
|
-
});
|
|
186
|
-
return output;
|
|
187
|
-
}
|