@northtek/overstory 0.1.0
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/LICENSE +201 -0
- package/README.md +115 -0
- package/dist/build/aggregate.d.ts +9 -0
- package/dist/build/aggregate.js +74 -0
- package/dist/build/builder.d.ts +52 -0
- package/dist/build/builder.js +223 -0
- package/dist/build/inmemory.d.ts +16 -0
- package/dist/build/inmemory.js +79 -0
- package/dist/build/pool.d.ts +3 -0
- package/dist/build/pool.js +16 -0
- package/dist/build/reflexion.d.ts +15 -0
- package/dist/build/reflexion.js +116 -0
- package/dist/build/summarize.d.ts +25 -0
- package/dist/build/summarize.js +146 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +323 -0
- package/dist/core/bm25.d.ts +19 -0
- package/dist/core/bm25.js +48 -0
- package/dist/core/chunk.d.ts +5 -0
- package/dist/core/chunk.js +91 -0
- package/dist/core/corpus.d.ts +21 -0
- package/dist/core/corpus.js +112 -0
- package/dist/core/gate.d.ts +25 -0
- package/dist/core/gate.js +155 -0
- package/dist/core/hash.d.ts +7 -0
- package/dist/core/hash.js +15 -0
- package/dist/core/store.d.ts +74 -0
- package/dist/core/store.js +66 -0
- package/dist/core/types.d.ts +96 -0
- package/dist/core/types.js +1 -0
- package/dist/fix/prompts.d.ts +4 -0
- package/dist/fix/prompts.js +124 -0
- package/dist/fix/scan.d.ts +18 -0
- package/dist/fix/scan.js +197 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +22 -0
- package/dist/llm/anthropic.d.ts +11 -0
- package/dist/llm/anthropic.js +46 -0
- package/dist/llm/mock.d.ts +5 -0
- package/dist/llm/mock.js +20 -0
- package/dist/llm/ollama.d.ts +11 -0
- package/dist/llm/ollama.js +49 -0
- package/dist/llm/provider.d.ts +16 -0
- package/dist/llm/provider.js +7 -0
- package/dist/llm/resolve.d.ts +14 -0
- package/dist/llm/resolve.js +24 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +136 -0
- package/dist/query/ask.d.ts +53 -0
- package/dist/query/ask.js +127 -0
- package/dist/query/notarize.d.ts +40 -0
- package/dist/query/notarize.js +31 -0
- package/dist/registry/github.d.ts +28 -0
- package/dist/registry/github.js +113 -0
- package/dist/registry/publishClient.d.ts +25 -0
- package/dist/registry/publishClient.js +39 -0
- package/dist/registry/registry.d.ts +35 -0
- package/dist/registry/registry.js +57 -0
- package/dist/registry/repoTree.d.ts +9 -0
- package/dist/registry/repoTree.js +32 -0
- package/dist/serve/app.d.ts +4 -0
- package/dist/serve/app.js +809 -0
- package/dist/serve/server.d.ts +9 -0
- package/dist/serve/server.js +180 -0
- package/dist/serve/threads.d.ts +31 -0
- package/dist/serve/threads.js +69 -0
- package/dist/site/data.d.ts +58 -0
- package/dist/site/data.js +88 -0
- package/dist/site/generate.d.ts +21 -0
- package/dist/site/generate.js +827 -0
- package/package.json +64 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Server } from 'node:http';
|
|
2
|
+
import type { ChatProvider } from '../llm/provider.js';
|
|
3
|
+
export interface ServeOptions {
|
|
4
|
+
provider: ChatProvider | null;
|
|
5
|
+
port?: number;
|
|
6
|
+
onReady?: (url: string) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare const createOverstoryHttpServer: (root: string, opts: ServeOptions) => Server;
|
|
9
|
+
export declare const serve: (root: string, opts: ServeOptions) => Promise<Server>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { loadCorpus } from '../core/corpus.js';
|
|
5
|
+
import { verifyTree } from '../core/gate.js';
|
|
6
|
+
import { loadTree, treePath } from '../core/store.js';
|
|
7
|
+
import { ask, buildClaimIndex } from '../query/ask.js';
|
|
8
|
+
import { notarizeClaims } from '../query/notarize.js';
|
|
9
|
+
import { buildSiteData } from '../site/data.js';
|
|
10
|
+
import { appHtml } from './app.js';
|
|
11
|
+
import { ThreadStore } from './threads.js';
|
|
12
|
+
const askBodySchema = z.object({
|
|
13
|
+
question: z.string().min(2).max(2000),
|
|
14
|
+
threadId: z.string().optional(),
|
|
15
|
+
});
|
|
16
|
+
const verifyBodySchema = z.object({
|
|
17
|
+
claims: z
|
|
18
|
+
.array(z.object({
|
|
19
|
+
text: z.string(),
|
|
20
|
+
citations: z.array(z.object({ file: z.string(), startLine: z.number().int().min(1), endLine: z.number().int().min(1) })),
|
|
21
|
+
}))
|
|
22
|
+
.min(1)
|
|
23
|
+
.max(30),
|
|
24
|
+
});
|
|
25
|
+
const sendJson = (res, status, value) => {
|
|
26
|
+
const body = JSON.stringify(value);
|
|
27
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
28
|
+
res.end(body);
|
|
29
|
+
};
|
|
30
|
+
const readBody = async (req) => {
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let size = 0;
|
|
33
|
+
for await (const chunk of req) {
|
|
34
|
+
size += chunk.length;
|
|
35
|
+
if (size > 1_000_000)
|
|
36
|
+
throw new Error('body too large');
|
|
37
|
+
chunks.push(chunk);
|
|
38
|
+
}
|
|
39
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
|
|
40
|
+
};
|
|
41
|
+
/** Suggested questions derived from what the tree actually knows. */
|
|
42
|
+
const suggestionsFor = (tree) => {
|
|
43
|
+
const dirs = Object.values(tree.nodes)
|
|
44
|
+
.filter((n) => n.kind === 'dir' && n.claims.length > 0)
|
|
45
|
+
.slice(0, 2)
|
|
46
|
+
.map((n) => `What lives in ${n.path}/ and how does it fit together?`);
|
|
47
|
+
return [
|
|
48
|
+
`What does ${tree.name} do, in one paragraph?`,
|
|
49
|
+
...dirs,
|
|
50
|
+
'Where would a new contributor start reading?',
|
|
51
|
+
].slice(0, 4);
|
|
52
|
+
};
|
|
53
|
+
export const createOverstoryHttpServer = (root, opts) => {
|
|
54
|
+
const threads = new ThreadStore(root);
|
|
55
|
+
let cached = null;
|
|
56
|
+
const load = async () => {
|
|
57
|
+
let mtimeMs = 0;
|
|
58
|
+
try {
|
|
59
|
+
mtimeMs = (await stat(treePath(root))).mtimeMs;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
if (cached && cached.treeMtimeMs === mtimeMs && now - cached.corpusAt < 10_000)
|
|
66
|
+
return cached;
|
|
67
|
+
const tree = await loadTree(treePath(root));
|
|
68
|
+
if (!tree)
|
|
69
|
+
return null;
|
|
70
|
+
const corpus = await loadCorpus(root, tree.corpusOptions ?? {});
|
|
71
|
+
const scanCorpus = tree.corpusOptions?.include
|
|
72
|
+
? await loadCorpus(root, { maxFiles: tree.corpusOptions.maxFiles })
|
|
73
|
+
: corpus;
|
|
74
|
+
const verification = verifyTree(tree, corpus);
|
|
75
|
+
cached = { tree, corpus, scanCorpus, verification, treeMtimeMs: mtimeMs, corpusAt: now };
|
|
76
|
+
return cached;
|
|
77
|
+
};
|
|
78
|
+
const server = createServer(async (req, res) => {
|
|
79
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
80
|
+
const route = `${req.method} ${url.pathname}`;
|
|
81
|
+
try {
|
|
82
|
+
if (route === 'GET /') {
|
|
83
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
|
84
|
+
res.end(appHtml());
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (route === 'GET /api/overview') {
|
|
88
|
+
const state = await load();
|
|
89
|
+
if (!state)
|
|
90
|
+
return sendJson(res, 404, { error: 'No tree found. Run: overstory build' });
|
|
91
|
+
const { tree, verification } = state;
|
|
92
|
+
return sendJson(res, 200, {
|
|
93
|
+
name: tree.name,
|
|
94
|
+
builtAt: tree.builtAt,
|
|
95
|
+
generator: tree.generator,
|
|
96
|
+
provider: opts.provider?.name ?? 'extractive (no LLM)',
|
|
97
|
+
freshness: verification.freshness,
|
|
98
|
+
verified: [...verification.verdicts.values()].filter((v) => v === 'VERIFIED').length,
|
|
99
|
+
total: verification.verdicts.size,
|
|
100
|
+
nodes: Object.keys(tree.nodes).length,
|
|
101
|
+
suggestions: suggestionsFor(tree),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (route === 'GET /api/tree') {
|
|
105
|
+
const state = await load();
|
|
106
|
+
if (!state)
|
|
107
|
+
return sendJson(res, 404, { error: 'No tree found. Run: overstory build' });
|
|
108
|
+
return sendJson(res, 200, buildSiteData(state.tree, state.verification, state.scanCorpus));
|
|
109
|
+
}
|
|
110
|
+
if (route === 'GET /api/threads')
|
|
111
|
+
return sendJson(res, 200, await threads.list());
|
|
112
|
+
if (route === 'GET /api/thread') {
|
|
113
|
+
const thread = await threads.get(url.searchParams.get('id') ?? '');
|
|
114
|
+
return thread ? sendJson(res, 200, thread) : sendJson(res, 404, { error: 'no such thread' });
|
|
115
|
+
}
|
|
116
|
+
if (route === 'DELETE /api/thread') {
|
|
117
|
+
const ok = await threads.remove(url.searchParams.get('id') ?? '');
|
|
118
|
+
return sendJson(res, ok ? 200 : 404, { ok });
|
|
119
|
+
}
|
|
120
|
+
if (route === 'POST /api/verify') {
|
|
121
|
+
const state = await load();
|
|
122
|
+
if (!state)
|
|
123
|
+
return sendJson(res, 404, { error: 'No tree found. Run: overstory build' });
|
|
124
|
+
const body = verifyBodySchema.parse(await readBody(req));
|
|
125
|
+
const { index, byId } = buildClaimIndex(state.tree);
|
|
126
|
+
return sendJson(res, 200, notarizeClaims(body.claims, state.tree, state.corpus, state.verification, index, byId));
|
|
127
|
+
}
|
|
128
|
+
if (route === 'POST /api/ask') {
|
|
129
|
+
const state = await load();
|
|
130
|
+
if (!state)
|
|
131
|
+
return sendJson(res, 404, { error: 'No tree found. Run: overstory build' });
|
|
132
|
+
const body = askBodySchema.parse(await readBody(req));
|
|
133
|
+
res.writeHead(200, {
|
|
134
|
+
'content-type': 'text/event-stream',
|
|
135
|
+
'cache-control': 'no-cache',
|
|
136
|
+
connection: 'keep-alive',
|
|
137
|
+
});
|
|
138
|
+
const send = (event, data) => {
|
|
139
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
140
|
+
};
|
|
141
|
+
const thread = body.threadId
|
|
142
|
+
? (await threads.get(body.threadId)) ?? (await threads.create(body.question))
|
|
143
|
+
: await threads.create(body.question);
|
|
144
|
+
send('thread', { id: thread.id, title: thread.title });
|
|
145
|
+
try {
|
|
146
|
+
const result = await ask(body.question, state.tree, state.corpus, opts.provider, {
|
|
147
|
+
onPhase: (e) => send('phase', e),
|
|
148
|
+
});
|
|
149
|
+
await threads.addTurn(thread.id, { question: body.question, result, at: new Date().toISOString() });
|
|
150
|
+
send('answer', result);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
send('error', { message: err instanceof Error ? err.message : String(err) });
|
|
154
|
+
}
|
|
155
|
+
res.end();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
sendJson(res, 404, { error: 'not found' });
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
if (!res.headersSent) {
|
|
162
|
+
sendJson(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
res.end();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
return server;
|
|
170
|
+
};
|
|
171
|
+
export const serve = async (root, opts) => {
|
|
172
|
+
const server = createOverstoryHttpServer(root, opts);
|
|
173
|
+
const port = opts.port ?? 7433;
|
|
174
|
+
await new Promise((resolvePromise, reject) => {
|
|
175
|
+
server.once('error', reject);
|
|
176
|
+
server.listen(port, '127.0.0.1', () => resolvePromise());
|
|
177
|
+
});
|
|
178
|
+
opts.onReady?.(`http://127.0.0.1:${port}`);
|
|
179
|
+
return server;
|
|
180
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AskResult } from '../query/ask.js';
|
|
2
|
+
export interface ThreadTurn {
|
|
3
|
+
question: string;
|
|
4
|
+
result: AskResult;
|
|
5
|
+
at: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Thread {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
turns: ThreadTurn[];
|
|
12
|
+
}
|
|
13
|
+
export declare class ThreadStore {
|
|
14
|
+
private root;
|
|
15
|
+
private threads;
|
|
16
|
+
private loaded;
|
|
17
|
+
private chain;
|
|
18
|
+
constructor(root: string);
|
|
19
|
+
private loadOnce;
|
|
20
|
+
private persist;
|
|
21
|
+
list(): Promise<Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
title: string;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
turns: number;
|
|
26
|
+
}>>;
|
|
27
|
+
get(id: string): Promise<Thread | null>;
|
|
28
|
+
create(title: string): Promise<Thread>;
|
|
29
|
+
addTurn(id: string, turn: ThreadTurn): Promise<void>;
|
|
30
|
+
remove(id: string): Promise<boolean>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
const threadsPath = (root) => join(root, '.overstory', 'threads.json');
|
|
5
|
+
export class ThreadStore {
|
|
6
|
+
root;
|
|
7
|
+
threads = [];
|
|
8
|
+
loaded = false;
|
|
9
|
+
chain = Promise.resolve();
|
|
10
|
+
constructor(root) {
|
|
11
|
+
this.root = root;
|
|
12
|
+
}
|
|
13
|
+
async loadOnce() {
|
|
14
|
+
if (this.loaded)
|
|
15
|
+
return;
|
|
16
|
+
this.loaded = true;
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(await readFile(threadsPath(this.root), 'utf8'));
|
|
19
|
+
if (Array.isArray(raw.threads))
|
|
20
|
+
this.threads = raw.threads;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
this.threads = [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
persist() {
|
|
27
|
+
const snapshot = JSON.stringify({ threads: this.threads });
|
|
28
|
+
this.chain = this.chain
|
|
29
|
+
.then(async () => {
|
|
30
|
+
const path = threadsPath(this.root);
|
|
31
|
+
await mkdir(dirname(path), { recursive: true });
|
|
32
|
+
await writeFile(`${path}.tmp`, snapshot, 'utf8');
|
|
33
|
+
await rename(`${path}.tmp`, path);
|
|
34
|
+
})
|
|
35
|
+
.catch(() => { });
|
|
36
|
+
}
|
|
37
|
+
async list() {
|
|
38
|
+
await this.loadOnce();
|
|
39
|
+
return [...this.threads]
|
|
40
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
41
|
+
.map((t) => ({ id: t.id, title: t.title, createdAt: t.createdAt, turns: t.turns.length }));
|
|
42
|
+
}
|
|
43
|
+
async get(id) {
|
|
44
|
+
await this.loadOnce();
|
|
45
|
+
return this.threads.find((t) => t.id === id) ?? null;
|
|
46
|
+
}
|
|
47
|
+
async create(title) {
|
|
48
|
+
await this.loadOnce();
|
|
49
|
+
const thread = { id: randomUUID(), title: title.slice(0, 120), createdAt: new Date().toISOString(), turns: [] };
|
|
50
|
+
this.threads.push(thread);
|
|
51
|
+
this.persist();
|
|
52
|
+
return thread;
|
|
53
|
+
}
|
|
54
|
+
async addTurn(id, turn) {
|
|
55
|
+
await this.loadOnce();
|
|
56
|
+
const thread = this.threads.find((t) => t.id === id);
|
|
57
|
+
if (!thread)
|
|
58
|
+
return;
|
|
59
|
+
thread.turns.push(turn);
|
|
60
|
+
this.persist();
|
|
61
|
+
}
|
|
62
|
+
async remove(id) {
|
|
63
|
+
await this.loadOnce();
|
|
64
|
+
const before = this.threads.length;
|
|
65
|
+
this.threads = this.threads.filter((t) => t.id !== id);
|
|
66
|
+
this.persist();
|
|
67
|
+
return this.threads.length < before;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CorpusSnapshot, Tree, TreeVerification, Verdict } from '../core/types.js';
|
|
2
|
+
export interface SiteSpan {
|
|
3
|
+
file: string;
|
|
4
|
+
startLine: number;
|
|
5
|
+
endLine: number;
|
|
6
|
+
text: string;
|
|
7
|
+
hash: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SiteClaim {
|
|
10
|
+
id: string;
|
|
11
|
+
text: string;
|
|
12
|
+
verdict: Verdict;
|
|
13
|
+
faithfulness: 'supported' | 'unsupported' | 'unchecked';
|
|
14
|
+
spans: SiteSpan[];
|
|
15
|
+
refs: Array<{
|
|
16
|
+
nodeId: string;
|
|
17
|
+
claimId: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
export interface SiteNode {
|
|
21
|
+
id: string;
|
|
22
|
+
kind: 'leaf' | 'dir' | 'root';
|
|
23
|
+
path: string;
|
|
24
|
+
title: string;
|
|
25
|
+
claims: SiteClaim[];
|
|
26
|
+
childIds: string[];
|
|
27
|
+
builtWith: string;
|
|
28
|
+
provider?: string;
|
|
29
|
+
/** Worst verdict anywhere in this subtree — drives the trust dot in the rail. */
|
|
30
|
+
worst: Verdict;
|
|
31
|
+
verified: number;
|
|
32
|
+
total: number;
|
|
33
|
+
}
|
|
34
|
+
export interface SiteFinding {
|
|
35
|
+
kind: string;
|
|
36
|
+
severity: 1 | 2 | 3;
|
|
37
|
+
title: string;
|
|
38
|
+
detail: string;
|
|
39
|
+
receipts: number;
|
|
40
|
+
/** The complete paste-ready prompt (composed server-side, spec shape). */
|
|
41
|
+
prompt: string;
|
|
42
|
+
}
|
|
43
|
+
export interface SiteData {
|
|
44
|
+
name: string;
|
|
45
|
+
builtAt: string;
|
|
46
|
+
generator: string;
|
|
47
|
+
freshness: number;
|
|
48
|
+
verified: number;
|
|
49
|
+
total: number;
|
|
50
|
+
root: string;
|
|
51
|
+
nodes: Record<string, SiteNode>;
|
|
52
|
+
/** Present when a corpus was available at render time (fix prompts need the code). */
|
|
53
|
+
findings?: SiteFinding[];
|
|
54
|
+
}
|
|
55
|
+
/** Flatten the tree + verification into the self-contained model the explorer embeds.
|
|
56
|
+
* With a corpus, also runs the deterministic finding scanner so the explorer can offer
|
|
57
|
+
* paste-ready, receipt-grounded fix prompts. */
|
|
58
|
+
export declare const buildSiteData: (tree: Tree, verification: TreeVerification, corpus?: CorpusSnapshot) => SiteData;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { scanFindings } from '../fix/scan.js';
|
|
2
|
+
import { findingToPrompt } from '../fix/prompts.js';
|
|
3
|
+
const SEVERITY = { VERIFIED: 0, STALE: 1, OUT_OF_CORPUS: 2, UNGROUNDED: 3 };
|
|
4
|
+
/** Flatten the tree + verification into the self-contained model the explorer embeds.
|
|
5
|
+
* With a corpus, also runs the deterministic finding scanner so the explorer can offer
|
|
6
|
+
* paste-ready, receipt-grounded fix prompts. */
|
|
7
|
+
export const buildSiteData = (tree, verification, corpus) => {
|
|
8
|
+
const nodes = {};
|
|
9
|
+
let verifiedTotal = 0;
|
|
10
|
+
let claimTotal = 0;
|
|
11
|
+
for (const node of Object.values(tree.nodes)) {
|
|
12
|
+
const counts = verification.byNode.get(node.id) ?? { verified: 0, total: 0 };
|
|
13
|
+
verifiedTotal += counts.verified;
|
|
14
|
+
claimTotal += counts.total;
|
|
15
|
+
nodes[node.id] = {
|
|
16
|
+
id: node.id,
|
|
17
|
+
kind: node.kind,
|
|
18
|
+
path: node.path,
|
|
19
|
+
title: node.title,
|
|
20
|
+
childIds: node.childIds,
|
|
21
|
+
builtWith: node.builtWith,
|
|
22
|
+
provider: node.provider,
|
|
23
|
+
worst: 'VERIFIED',
|
|
24
|
+
verified: counts.verified,
|
|
25
|
+
total: counts.total,
|
|
26
|
+
claims: node.claims.map((c) => ({
|
|
27
|
+
id: c.id,
|
|
28
|
+
text: c.text,
|
|
29
|
+
verdict: c.verdict ?? verification.verdicts.get(c.id) ?? 'UNGROUNDED',
|
|
30
|
+
faithfulness: c.faithfulness ?? 'unchecked',
|
|
31
|
+
spans: c.citations
|
|
32
|
+
.filter((x) => x.kind === 'span')
|
|
33
|
+
.map((x) => ({
|
|
34
|
+
file: x.span.file,
|
|
35
|
+
startLine: x.span.startLine,
|
|
36
|
+
endLine: x.span.endLine,
|
|
37
|
+
text: x.span.text,
|
|
38
|
+
hash: x.span.contentHash,
|
|
39
|
+
})),
|
|
40
|
+
refs: c.citations
|
|
41
|
+
.filter((x) => x.kind === 'claim')
|
|
42
|
+
.map((x) => x.ref),
|
|
43
|
+
})),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const worstOf = (id, seen = new Set()) => {
|
|
47
|
+
// Revisits report the already-computed worst (diamond shapes), never a blind VERIFIED.
|
|
48
|
+
if (seen.has(id))
|
|
49
|
+
return nodes[id]?.worst ?? 'VERIFIED';
|
|
50
|
+
seen.add(id);
|
|
51
|
+
const node = nodes[id];
|
|
52
|
+
if (!node)
|
|
53
|
+
return 'VERIFIED';
|
|
54
|
+
let worst = 'VERIFIED';
|
|
55
|
+
for (const claim of node.claims)
|
|
56
|
+
if (SEVERITY[claim.verdict] > SEVERITY[worst])
|
|
57
|
+
worst = claim.verdict;
|
|
58
|
+
for (const child of node.childIds) {
|
|
59
|
+
const w = worstOf(child, seen);
|
|
60
|
+
if (SEVERITY[w] > SEVERITY[worst])
|
|
61
|
+
worst = w;
|
|
62
|
+
}
|
|
63
|
+
node.worst = worst;
|
|
64
|
+
return worst;
|
|
65
|
+
};
|
|
66
|
+
worstOf(tree.root);
|
|
67
|
+
const findings = corpus
|
|
68
|
+
? scanFindings(corpus, tree, verification).map((f, i) => ({
|
|
69
|
+
kind: f.kind,
|
|
70
|
+
severity: f.severity,
|
|
71
|
+
title: f.title,
|
|
72
|
+
detail: f.detail,
|
|
73
|
+
receipts: f.receipts.length,
|
|
74
|
+
prompt: findingToPrompt(f, i + 1),
|
|
75
|
+
}))
|
|
76
|
+
: undefined;
|
|
77
|
+
return {
|
|
78
|
+
name: tree.name,
|
|
79
|
+
builtAt: tree.builtAt,
|
|
80
|
+
generator: tree.generator,
|
|
81
|
+
freshness: claimTotal === 0 ? 1 : verifiedTotal / claimTotal,
|
|
82
|
+
verified: verifiedTotal,
|
|
83
|
+
total: claimTotal,
|
|
84
|
+
root: tree.root,
|
|
85
|
+
nodes,
|
|
86
|
+
...(findings ? { findings } : {}),
|
|
87
|
+
};
|
|
88
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SiteData } from './data.js';
|
|
2
|
+
export interface AskPanelOptions {
|
|
3
|
+
/** Repo clone URL for the run-locally instructions. */
|
|
4
|
+
cloneUrl: string;
|
|
5
|
+
repoLabel: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ShareMetaOptions {
|
|
8
|
+
/** Absolute canonical URL of this page (og:url). */
|
|
9
|
+
url: string;
|
|
10
|
+
/** e.g. "94% of 2,477 statements verified against the code right now". */
|
|
11
|
+
status: string;
|
|
12
|
+
}
|
|
13
|
+
/** Render the self-contained explorer. One HTML file, zero external scripts; data embedded
|
|
14
|
+
* as JSON; fonts degrade gracefully offline (air-gapped is a product promise).
|
|
15
|
+
* `ask` (registry pages only) adds the Ask panel: run-locally instructions plus browser-side
|
|
16
|
+
* BYO-key ask — the visitor's key calls Anthropic directly from their browser; the server
|
|
17
|
+
* never sees keys, questions, or answers. */
|
|
18
|
+
export declare const generateSiteHtml: (data: SiteData, opts?: {
|
|
19
|
+
ask?: AskPanelOptions;
|
|
20
|
+
share?: ShareMetaOptions;
|
|
21
|
+
}) => string;
|