markindex 0.0.0-stage → 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/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +281 -2
- package/README.zh-CN.md +164 -0
- package/THIRD_PARTY_NOTICES.md +36 -0
- package/dist/adapters/ai-sdk.d.ts +8 -0
- package/dist/adapters/ai-sdk.js +20 -0
- package/dist/adapters/mcp.d.ts +6 -0
- package/dist/adapters/mcp.js +28 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +297 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +18 -0
- package/dist/eval/questions.d.ts +34 -0
- package/dist/eval/questions.js +80 -0
- package/dist/eval/run.d.ts +62 -0
- package/dist/eval/run.js +163 -0
- package/dist/eval/score.d.ts +74 -0
- package/dist/eval/score.js +104 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +23 -0
- package/dist/library/indexer.d.ts +89 -0
- package/dist/library/indexer.js +199 -0
- package/dist/library/scan.d.ts +29 -0
- package/dist/library/scan.js +106 -0
- package/dist/library.d.ts +30 -0
- package/dist/library.js +42 -0
- package/dist/llm/provider.d.ts +10 -0
- package/dist/llm/provider.js +32 -0
- package/dist/llm/rate-limit.d.ts +33 -0
- package/dist/llm/rate-limit.js +95 -0
- package/dist/llm/summarize.d.ts +67 -0
- package/dist/llm/summarize.js +193 -0
- package/dist/parse/markdown.d.ts +38 -0
- package/dist/parse/markdown.js +58 -0
- package/dist/retrieve.d.ts +65 -0
- package/dist/retrieve.js +174 -0
- package/dist/store/db.d.ts +50 -0
- package/dist/store/db.js +399 -0
- package/dist/store/search.d.ts +27 -0
- package/dist/store/search.js +116 -0
- package/dist/text/segment.d.ts +4 -0
- package/dist/text/segment.js +37 -0
- package/dist/text/tokens.d.ts +3 -0
- package/dist/text/tokens.js +13 -0
- package/dist/tools/contract.d.ts +51 -0
- package/dist/tools/contract.js +159 -0
- package/dist/tools/envelope.d.ts +31 -0
- package/dist/tools/envelope.js +62 -0
- package/dist/tools/impl.d.ts +7 -0
- package/dist/tools/impl.js +441 -0
- package/dist/tools/similar.d.ts +8 -0
- package/dist/tools/similar.js +40 -0
- package/dist/tree/build.d.ts +27 -0
- package/dist/tree/build.js +139 -0
- package/dist/tree/optimize.d.ts +39 -0
- package/dist/tree/optimize.js +185 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.js +1 -0
- package/package.json +87 -4
package/dist/eval/run.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { rateLimiterFor } from '../llm/rate-limit.js';
|
|
2
|
+
import { retrieve, RETRIEVE_MAX_INPUT_TOKENS, RETRIEVE_MAX_STEPS } from '../retrieve.js';
|
|
3
|
+
import { searchNodes } from '../store/search.js';
|
|
4
|
+
import { approxTokens } from '../text/tokens.js';
|
|
5
|
+
import { walk } from '../tree/build.js';
|
|
6
|
+
import { aggregate, evidenceLineRanges, evidencePrecision, firstHitRank, } from './score.js';
|
|
7
|
+
const DEFAULT_TOP_K = 5;
|
|
8
|
+
/**
|
|
9
|
+
* Checks every gold location against the index: the document exists, the line range is inside the file and a
|
|
10
|
+
* title_path names an existing node. Returns issues keyed by question-file line (empty = all good).
|
|
11
|
+
*/
|
|
12
|
+
export function checkGold(lib, questions) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
const titlePaths = new Map();
|
|
15
|
+
for (const q of questions) {
|
|
16
|
+
for (const g of q.gold) {
|
|
17
|
+
const doc = lib.store.getDoc(g.doc_path);
|
|
18
|
+
if (!doc) {
|
|
19
|
+
issues.push({ line: q.line, message: `question "${q.id}": gold doc_path "${g.doc_path}" is not in the index (use a library-relative path as printed by \`markindex search\`)` });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (g.end_line !== undefined && g.end_line > doc.lineCount) {
|
|
23
|
+
issues.push({ line: q.line, message: `question "${q.id}": gold lines ${g.start_line}-${g.end_line} exceed "${g.doc_path}" (${doc.lineCount} lines)` });
|
|
24
|
+
}
|
|
25
|
+
if (g.title_path && g.start_line === undefined) {
|
|
26
|
+
let paths = titlePaths.get(g.doc_path);
|
|
27
|
+
if (!paths) {
|
|
28
|
+
paths = new Set([...walk(lib.store.getTree(g.doc_path) ?? [])].map((e) => e.titlePath.join('\0')));
|
|
29
|
+
titlePaths.set(g.doc_path, paths);
|
|
30
|
+
}
|
|
31
|
+
if (!paths.has(g.title_path.join('\0'))) {
|
|
32
|
+
issues.push({ line: q.line, message: `question "${q.id}": gold title_path ${JSON.stringify(g.title_path)} matches no node of "${g.doc_path}" (see \`markindex tree\`)` });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return issues;
|
|
38
|
+
}
|
|
39
|
+
/** Hit / rank (honouring maxEvidenceLines) plus evidence size and precision for one question. */
|
|
40
|
+
function judge(lib, q, evidence, maxEvidenceLines) {
|
|
41
|
+
const ranges = evidenceLineRanges(evidence);
|
|
42
|
+
const rank = firstHitRank(evidence, q.gold, maxEvidenceLines);
|
|
43
|
+
return {
|
|
44
|
+
hit: rank !== null,
|
|
45
|
+
rank,
|
|
46
|
+
evidenceLines: ranges.reduce((n, r) => n + r.endLine - r.startLine + 1, 0),
|
|
47
|
+
evidenceTokens: ranges.reduce((n, r) => n + approxTokens(lib.store.getLines(r.docPath, r.startLine, r.endLine).join('\n')), 0),
|
|
48
|
+
precision: evidencePrecision(evidence, q.gold),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function bm25Runner(lib, topK, maxEvidenceLines) {
|
|
52
|
+
const nodes = new Map();
|
|
53
|
+
const node = (docPath, nodeId) => {
|
|
54
|
+
let m = nodes.get(docPath);
|
|
55
|
+
if (!m) {
|
|
56
|
+
m = new Map([...walk(lib.store.getTree(docPath) ?? [])].map((e) => [e.node.nodeId, e.node]));
|
|
57
|
+
nodes.set(docPath, m);
|
|
58
|
+
}
|
|
59
|
+
return m.get(nodeId);
|
|
60
|
+
};
|
|
61
|
+
return (q) => {
|
|
62
|
+
const hits = searchNodes(lib.store, q.question, { limit: topK });
|
|
63
|
+
const evidence = hits.map((h) => ({
|
|
64
|
+
docPath: h.docPath,
|
|
65
|
+
titlePath: h.titlePath,
|
|
66
|
+
startLine: h.startLine,
|
|
67
|
+
// what BM25 scored is the node's own text, not its whole subtree
|
|
68
|
+
endLine: node(h.docPath, h.nodeId)?.ownEndLine ?? h.endLine,
|
|
69
|
+
}));
|
|
70
|
+
return {
|
|
71
|
+
id: q.id,
|
|
72
|
+
lang: q.lang,
|
|
73
|
+
...judge(lib, q, evidence, maxEvidenceLines),
|
|
74
|
+
tokens: approxTokens(JSON.stringify(hits)),
|
|
75
|
+
steps: 1,
|
|
76
|
+
evidence,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function agentResult(lib, q, opts, rateLimiter) {
|
|
81
|
+
try {
|
|
82
|
+
const r = await retrieve(lib, q.question, {
|
|
83
|
+
model: opts.model,
|
|
84
|
+
...(rateLimiter ? { rateLimiter } : {}),
|
|
85
|
+
...(opts.maxSteps !== undefined ? { maxSteps: opts.maxSteps } : {}),
|
|
86
|
+
...(opts.maxInputTokens !== undefined ? { maxInputTokens: opts.maxInputTokens } : {}),
|
|
87
|
+
});
|
|
88
|
+
const evidence = r.evidence.map(({ docPath, titlePath, startLine, endLine }) => ({ docPath, titlePath, startLine, endLine }));
|
|
89
|
+
return {
|
|
90
|
+
id: q.id,
|
|
91
|
+
lang: q.lang,
|
|
92
|
+
...judge(lib, q, evidence, opts.maxEvidenceLines),
|
|
93
|
+
tokens: r.usage.inputTokens + r.usage.outputTokens,
|
|
94
|
+
steps: r.steps,
|
|
95
|
+
evidence,
|
|
96
|
+
stopReason: r.stopReason,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
return {
|
|
101
|
+
id: q.id,
|
|
102
|
+
lang: q.lang,
|
|
103
|
+
hit: false,
|
|
104
|
+
rank: null,
|
|
105
|
+
tokens: 0,
|
|
106
|
+
steps: 0,
|
|
107
|
+
evidence: [],
|
|
108
|
+
evidenceLines: 0,
|
|
109
|
+
evidenceTokens: 0,
|
|
110
|
+
precision: null,
|
|
111
|
+
error: e instanceof Error ? e.message : String(e),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Runs every question in the given mode and aggregates the results (overall + by language). */
|
|
116
|
+
export async function runEval(lib, questions, opts) {
|
|
117
|
+
const topK = opts.topK ?? DEFAULT_TOP_K;
|
|
118
|
+
const total = questions.length;
|
|
119
|
+
const createdAt = new Date().toISOString();
|
|
120
|
+
const maxLinesOption = opts.maxEvidenceLines === undefined ? {} : { maxEvidenceLines: opts.maxEvidenceLines };
|
|
121
|
+
if (opts.mode === 'bm25') {
|
|
122
|
+
const run = bm25Runner(lib, topK, opts.maxEvidenceLines);
|
|
123
|
+
const results = questions.map((q, i) => {
|
|
124
|
+
const r = run(q);
|
|
125
|
+
opts.onProgress?.(i + 1, total, r);
|
|
126
|
+
return r;
|
|
127
|
+
});
|
|
128
|
+
return { mode: 'bm25', createdAt, options: { topK, ...maxLinesOption }, summary: aggregate(results), results };
|
|
129
|
+
}
|
|
130
|
+
if (!opts.model)
|
|
131
|
+
throw new Error('agent mode needs a model (e.g. --model anthropic:claude-sonnet-5-5)');
|
|
132
|
+
const rateLimiter = opts.rateLimiter ?? rateLimiterFor(opts.rateLimit);
|
|
133
|
+
const results = new Array(total);
|
|
134
|
+
let next = 0;
|
|
135
|
+
let done = 0;
|
|
136
|
+
const worker = async () => {
|
|
137
|
+
while (next < total) {
|
|
138
|
+
const i = next++;
|
|
139
|
+
results[i] = await agentResult(lib, questions[i], opts, rateLimiter);
|
|
140
|
+
opts.onProgress?.(++done, total, results[i]);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const concurrency = Math.max(1, Math.min(total, Math.floor(opts.concurrency ?? 1)));
|
|
144
|
+
await Promise.all(Array.from({ length: concurrency }, worker));
|
|
145
|
+
const report = {
|
|
146
|
+
mode: 'agent',
|
|
147
|
+
...(opts.modelId ? { model: opts.modelId } : {}),
|
|
148
|
+
createdAt,
|
|
149
|
+
options: {
|
|
150
|
+
maxSteps: opts.maxSteps ?? RETRIEVE_MAX_STEPS,
|
|
151
|
+
maxInputTokens: opts.maxInputTokens ?? RETRIEVE_MAX_INPUT_TOKENS,
|
|
152
|
+
...maxLinesOption,
|
|
153
|
+
...(opts.rateLimit ? { rateLimit: opts.rateLimit } : {}),
|
|
154
|
+
},
|
|
155
|
+
summary: aggregate(results),
|
|
156
|
+
results,
|
|
157
|
+
};
|
|
158
|
+
if (opts.baseline ?? true) {
|
|
159
|
+
const run = bm25Runner(lib, topK, opts.maxEvidenceLines);
|
|
160
|
+
report.baseline = { mode: 'bm25', topK, summary: aggregate(questions.map(run)) };
|
|
161
|
+
}
|
|
162
|
+
return report;
|
|
163
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { EvalQuestion, Gold } from './questions.js';
|
|
2
|
+
/** Where a returned piece of evidence points (lines 1-based, inclusive). */
|
|
3
|
+
export interface EvidenceRef {
|
|
4
|
+
docPath: string;
|
|
5
|
+
titlePath: string[];
|
|
6
|
+
startLine: number;
|
|
7
|
+
endLine: number;
|
|
8
|
+
}
|
|
9
|
+
export interface QuestionResult {
|
|
10
|
+
id: string;
|
|
11
|
+
lang: EvalQuestion['lang'];
|
|
12
|
+
hit: boolean;
|
|
13
|
+
/** 1-based position of the first evidence item matching a gold location; null when none does. */
|
|
14
|
+
rank: number | null;
|
|
15
|
+
/** bm25: approx tokens of the search response; agent: model input + output tokens. */
|
|
16
|
+
tokens: number;
|
|
17
|
+
/** bm25: 1 (one search call); agent: model steps. */
|
|
18
|
+
steps: number;
|
|
19
|
+
evidence: EvidenceRef[];
|
|
20
|
+
/** Distinct document lines covered by the evidence (overlapping items counted once). */
|
|
21
|
+
evidenceLines: number;
|
|
22
|
+
/** Approx tokens of those lines' text. */
|
|
23
|
+
evidenceTokens: number;
|
|
24
|
+
/** Share of evidenceLines inside any gold line range; null when no gold has a line range or there is no evidence. */
|
|
25
|
+
precision: number | null;
|
|
26
|
+
stopReason?: string;
|
|
27
|
+
/** Set when the question could not be run (e.g. a provider error); counts as a miss. */
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface Summary {
|
|
31
|
+
questions: number;
|
|
32
|
+
hits: number;
|
|
33
|
+
/** hits / questions */
|
|
34
|
+
hitRate: number;
|
|
35
|
+
/** Share of questions whose FIRST evidence item is a hit (meaningful for ranked bm25 output). */
|
|
36
|
+
hitAt1: number;
|
|
37
|
+
/** Mean reciprocal rank of the first hit (0 for misses). */
|
|
38
|
+
mrr: number;
|
|
39
|
+
avgTokens: number;
|
|
40
|
+
avgSteps: number;
|
|
41
|
+
errors: number;
|
|
42
|
+
meanEvidenceLines: number;
|
|
43
|
+
medianEvidenceLines: number;
|
|
44
|
+
meanEvidenceTokens: number;
|
|
45
|
+
/** Mean over the questions whose precision is not null; null when none has one. */
|
|
46
|
+
meanPrecision: number | null;
|
|
47
|
+
}
|
|
48
|
+
export interface Aggregate {
|
|
49
|
+
overall: Summary;
|
|
50
|
+
byLang: Partial<Record<EvalQuestion['lang'], Summary>>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Hit judgement: same document, and
|
|
54
|
+
* - gold has a line range → the evidence range overlaps it (inclusive);
|
|
55
|
+
* - else gold has title_path → one title path is a prefix of the other (evidence inside, or containing, the gold section);
|
|
56
|
+
* - else (doc-level gold) → any evidence from the document.
|
|
57
|
+
*/
|
|
58
|
+
export declare function matchesGold(e: EvidenceRef, g: Gold): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* 1-based rank of the first evidence item matching any gold. With `maxEvidenceLines`, only items spanning at most
|
|
61
|
+
* that many lines count (a whole-chapter "hit" does not).
|
|
62
|
+
*/
|
|
63
|
+
export declare function firstHitRank(evidence: EvidenceRef[], gold: Gold[], maxEvidenceLines?: number): number | null;
|
|
64
|
+
export interface LineRange {
|
|
65
|
+
docPath: string;
|
|
66
|
+
startLine: number;
|
|
67
|
+
endLine: number;
|
|
68
|
+
}
|
|
69
|
+
/** Evidence as disjoint line ranges: sorted by document then line, overlapping or adjacent items merged. */
|
|
70
|
+
export declare function evidenceLineRanges(evidence: LineRange[]): LineRange[];
|
|
71
|
+
/** Share of distinct evidence lines that fall inside any gold line range (gold without lines is ignored). */
|
|
72
|
+
export declare function evidencePrecision(evidence: EvidenceRef[], gold: Gold[]): number | null;
|
|
73
|
+
/** Overall and per-language summaries; averages run over all questions (errors count as misses with their partial cost). */
|
|
74
|
+
export declare function aggregate(results: QuestionResult[]): Aggregate;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const prefixOf = (a, b) => a.length <= b.length && a.every((t, i) => t === b[i]);
|
|
2
|
+
/**
|
|
3
|
+
* Hit judgement: same document, and
|
|
4
|
+
* - gold has a line range → the evidence range overlaps it (inclusive);
|
|
5
|
+
* - else gold has title_path → one title path is a prefix of the other (evidence inside, or containing, the gold section);
|
|
6
|
+
* - else (doc-level gold) → any evidence from the document.
|
|
7
|
+
*/
|
|
8
|
+
export function matchesGold(e, g) {
|
|
9
|
+
if (e.docPath !== g.doc_path)
|
|
10
|
+
return false;
|
|
11
|
+
if (g.start_line !== undefined && g.end_line !== undefined)
|
|
12
|
+
return e.startLine <= g.end_line && g.start_line <= e.endLine;
|
|
13
|
+
if (g.title_path)
|
|
14
|
+
return prefixOf(g.title_path, e.titlePath) || prefixOf(e.titlePath, g.title_path);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 1-based rank of the first evidence item matching any gold. With `maxEvidenceLines`, only items spanning at most
|
|
19
|
+
* that many lines count (a whole-chapter "hit" does not).
|
|
20
|
+
*/
|
|
21
|
+
export function firstHitRank(evidence, gold, maxEvidenceLines) {
|
|
22
|
+
const i = evidence.findIndex((e) => (maxEvidenceLines === undefined || e.endLine - e.startLine + 1 <= maxEvidenceLines) &&
|
|
23
|
+
gold.some((g) => matchesGold(e, g)));
|
|
24
|
+
return i === -1 ? null : i + 1;
|
|
25
|
+
}
|
|
26
|
+
/** Evidence as disjoint line ranges: sorted by document then line, overlapping or adjacent items merged. */
|
|
27
|
+
export function evidenceLineRanges(evidence) {
|
|
28
|
+
const sorted = evidence
|
|
29
|
+
.map(({ docPath, startLine, endLine }) => ({ docPath, startLine, endLine }))
|
|
30
|
+
.filter((r) => r.endLine >= r.startLine)
|
|
31
|
+
.sort((a, b) => (a.docPath < b.docPath ? -1 : a.docPath > b.docPath ? 1 : a.startLine - b.startLine));
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const r of sorted) {
|
|
34
|
+
const last = out[out.length - 1];
|
|
35
|
+
if (last && last.docPath === r.docPath && r.startLine <= last.endLine + 1)
|
|
36
|
+
last.endLine = Math.max(last.endLine, r.endLine);
|
|
37
|
+
else
|
|
38
|
+
out.push(r);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
const lineCount = (ranges) => ranges.reduce((n, r) => n + r.endLine - r.startLine + 1, 0);
|
|
43
|
+
/** Share of distinct evidence lines that fall inside any gold line range (gold without lines is ignored). */
|
|
44
|
+
export function evidencePrecision(evidence, gold) {
|
|
45
|
+
const goldRanges = evidenceLineRanges(gold.flatMap((g) => g.start_line !== undefined && g.end_line !== undefined
|
|
46
|
+
? [{ docPath: g.doc_path, startLine: g.start_line, endLine: g.end_line }]
|
|
47
|
+
: []));
|
|
48
|
+
const ev = evidenceLineRanges(evidence);
|
|
49
|
+
const total = lineCount(ev);
|
|
50
|
+
if (goldRanges.length === 0 || total === 0)
|
|
51
|
+
return null;
|
|
52
|
+
let inside = 0;
|
|
53
|
+
for (const e of ev) {
|
|
54
|
+
for (const g of goldRanges) {
|
|
55
|
+
if (g.docPath !== e.docPath)
|
|
56
|
+
continue;
|
|
57
|
+
const lo = Math.max(e.startLine, g.startLine);
|
|
58
|
+
const hi = Math.min(e.endLine, g.endLine);
|
|
59
|
+
if (hi >= lo)
|
|
60
|
+
inside += hi - lo + 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return round(inside / total);
|
|
64
|
+
}
|
|
65
|
+
function round(x) {
|
|
66
|
+
return Math.round(x * 10_000) / 10_000;
|
|
67
|
+
}
|
|
68
|
+
function median(xs) {
|
|
69
|
+
if (xs.length === 0)
|
|
70
|
+
return 0;
|
|
71
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
72
|
+
const mid = s.length >> 1;
|
|
73
|
+
return s.length % 2 === 1 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
74
|
+
}
|
|
75
|
+
function summarize(rs) {
|
|
76
|
+
const n = rs.length;
|
|
77
|
+
const avg = (f) => (n === 0 ? 0 : round(rs.reduce((s, r) => s + f(r), 0) / n));
|
|
78
|
+
const hits = rs.filter((r) => r.hit).length;
|
|
79
|
+
const precisions = rs.flatMap((r) => (r.precision === null ? [] : [r.precision]));
|
|
80
|
+
return {
|
|
81
|
+
questions: n,
|
|
82
|
+
hits,
|
|
83
|
+
hitRate: n === 0 ? 0 : round(hits / n),
|
|
84
|
+
hitAt1: avg((r) => (r.rank === 1 ? 1 : 0)),
|
|
85
|
+
mrr: avg((r) => (r.rank === null ? 0 : 1 / r.rank)),
|
|
86
|
+
avgTokens: avg((r) => r.tokens),
|
|
87
|
+
avgSteps: avg((r) => r.steps),
|
|
88
|
+
errors: rs.filter((r) => r.error !== undefined).length,
|
|
89
|
+
meanEvidenceLines: avg((r) => r.evidenceLines),
|
|
90
|
+
medianEvidenceLines: median(rs.map((r) => r.evidenceLines)),
|
|
91
|
+
meanEvidenceTokens: avg((r) => r.evidenceTokens),
|
|
92
|
+
meanPrecision: precisions.length === 0 ? null : round(precisions.reduce((a, b) => a + b, 0) / precisions.length),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Overall and per-language summaries; averages run over all questions (errors count as misses with their partial cost). */
|
|
96
|
+
export function aggregate(results) {
|
|
97
|
+
const byLang = {};
|
|
98
|
+
for (const lang of ['zh', 'en', 'mixed']) {
|
|
99
|
+
const rs = results.filter((r) => r.lang === lang);
|
|
100
|
+
if (rs.length > 0)
|
|
101
|
+
byLang[lang] = summarize(rs);
|
|
102
|
+
}
|
|
103
|
+
return { overall: summarize(results), byLang };
|
|
104
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export * from './config.js';
|
|
2
|
+
export * from './types.js';
|
|
3
|
+
export { approxTokens, type TokenCounter } from './text/tokens.js';
|
|
4
|
+
export { segmentForIndex, segmentQuery } from './text/segment.js';
|
|
5
|
+
export * from './parse/markdown.js';
|
|
6
|
+
export * from './tree/build.js';
|
|
7
|
+
export * from './tree/optimize.js';
|
|
8
|
+
export { openStore, StoreLockedError, type Store } from './store/db.js';
|
|
9
|
+
export { scanLibrary, normalizeIgnorePatterns, IGNORE_FILE, type ScanOptions, type ScannedFile } from './library/scan.js';
|
|
10
|
+
export { indexLibrary, indexFormat, SUSPECT_SETEXT_CHARS, type IndexFormatOptions, type IndexEvent, type IndexOptions, type IndexReport, type IndexStatus, } from './library/indexer.js';
|
|
11
|
+
export { searchNodes, type SearchHit, type SearchOptions } from './store/search.js';
|
|
12
|
+
export { summarizeDocument, estimateSummaryCost, type SummaryOptions, type SummarizeResult, type SummaryFailure, } from './llm/summarize.js';
|
|
13
|
+
export { createRateLimiter, rateLimiterFor, type RateLimiter, type RateLimitOptions, type RateLimiterClock } from './llm/rate-limit.js';
|
|
14
|
+
export { TOOL_CONTRACT, AGENT_INSTRUCTIONS, type ToolName, type ToolInput, } from './tools/contract.js';
|
|
15
|
+
export { splitByBudget, type Envelope, type ErrorCode, type NextSteps } from './tools/envelope.js';
|
|
16
|
+
export { openLibrary, defaultDbFile, type Library, type OpenLibraryOptions } from './library.js';
|
|
17
|
+
export { createAiSdkTools } from './adapters/ai-sdk.js';
|
|
18
|
+
export { retrieve, RETRIEVE_MAX_STEPS, RETRIEVE_MAX_INPUT_TOKENS, type Evidence, type RetrieveOptions, type RetrieveResult } from './retrieve.js';
|
|
19
|
+
export { createMcpServer } from './adapters/mcp.js';
|
|
20
|
+
export { resolveSummaryModel, ProviderError } from './llm/provider.js';
|
|
21
|
+
export { parseQuestions, QuestionFileError, type EvalQuestion, type Gold, type QuestionIssue, } from './eval/questions.js';
|
|
22
|
+
export { matchesGold, firstHitRank, aggregate, evidenceLineRanges, evidencePrecision, type LineRange, type EvidenceRef, type QuestionResult, type Summary, type Aggregate } from './eval/score.js';
|
|
23
|
+
export { runEval, checkGold, type EvalMode, type RunEvalOptions, type EvalReport } from './eval/run.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export * from './config.js';
|
|
2
|
+
export * from './types.js';
|
|
3
|
+
export { approxTokens } from './text/tokens.js';
|
|
4
|
+
export { segmentForIndex, segmentQuery } from './text/segment.js';
|
|
5
|
+
export * from './parse/markdown.js';
|
|
6
|
+
export * from './tree/build.js';
|
|
7
|
+
export * from './tree/optimize.js';
|
|
8
|
+
export { openStore, StoreLockedError } from './store/db.js';
|
|
9
|
+
export { scanLibrary, normalizeIgnorePatterns, IGNORE_FILE } from './library/scan.js';
|
|
10
|
+
export { indexLibrary, indexFormat, SUSPECT_SETEXT_CHARS, } from './library/indexer.js';
|
|
11
|
+
export { searchNodes } from './store/search.js';
|
|
12
|
+
export { summarizeDocument, estimateSummaryCost, } from './llm/summarize.js';
|
|
13
|
+
export { createRateLimiter, rateLimiterFor } from './llm/rate-limit.js';
|
|
14
|
+
export { TOOL_CONTRACT, AGENT_INSTRUCTIONS, } from './tools/contract.js';
|
|
15
|
+
export { splitByBudget } from './tools/envelope.js';
|
|
16
|
+
export { openLibrary, defaultDbFile } from './library.js';
|
|
17
|
+
export { createAiSdkTools } from './adapters/ai-sdk.js';
|
|
18
|
+
export { retrieve, RETRIEVE_MAX_STEPS, RETRIEVE_MAX_INPUT_TOKENS } from './retrieve.js';
|
|
19
|
+
export { createMcpServer } from './adapters/mcp.js';
|
|
20
|
+
export { resolveSummaryModel, ProviderError } from './llm/provider.js';
|
|
21
|
+
export { parseQuestions, QuestionFileError, } from './eval/questions.js';
|
|
22
|
+
export { matchesGold, firstHitRank, aggregate, evidenceLineRanges, evidencePrecision } from './eval/score.js';
|
|
23
|
+
export { runEval, checkGold } from './eval/run.js';
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Store } from '../store/db.js';
|
|
2
|
+
import { type TokenCounter } from '../text/tokens.js';
|
|
3
|
+
import { type SummaryOptions } from '../llm/summarize.js';
|
|
4
|
+
export type IndexStatus = 'added' | 'updated' | 'unchanged' | 'removed' | 'skipped';
|
|
5
|
+
export interface IndexEvent {
|
|
6
|
+
path: string;
|
|
7
|
+
status: IndexStatus;
|
|
8
|
+
reason?: string;
|
|
9
|
+
/** Files processed so far (1-based); removals do not count. */
|
|
10
|
+
done: number;
|
|
11
|
+
/** Files found by the scan. */
|
|
12
|
+
total: number;
|
|
13
|
+
}
|
|
14
|
+
export interface IndexOptions {
|
|
15
|
+
root: string;
|
|
16
|
+
store: Store;
|
|
17
|
+
/** Used for both buildTree and optimizeTree. Defaults to approxTokens. */
|
|
18
|
+
countTokens?: TokenCounter;
|
|
19
|
+
/**
|
|
20
|
+
* Names a custom `countTokens` in the index format fingerprint, so swapping one custom counter for another
|
|
21
|
+
* also triggers a rebuild. Without it every custom counter is recorded as `custom`.
|
|
22
|
+
*/
|
|
23
|
+
tokenCounterId?: string;
|
|
24
|
+
/** optimizeTree `maxNodeTokens` (default MAX_NODE_TOKENS). */
|
|
25
|
+
maxNodeTokens?: number;
|
|
26
|
+
/** optimizeTree `routingCost` (default ROUTING_COST_TOKENS). */
|
|
27
|
+
routingCost?: number;
|
|
28
|
+
/**
|
|
29
|
+
* parseMarkdown `setext`. Set false for converter output whose bare `---` lines are page breaks.
|
|
30
|
+
* Stored in the index format fingerprint: when omitted, the value the index was built with is kept (true for a new
|
|
31
|
+
* index); an explicit value that differs from the stored one re-indexes every document.
|
|
32
|
+
*/
|
|
33
|
+
setext?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Extra gitignore-syntax patterns relative to `root` (on top of `.gitignore` files and a root `.markindexignore`).
|
|
36
|
+
* Documents that become ignored are removed from the index like deleted files.
|
|
37
|
+
*/
|
|
38
|
+
ignore?: string[];
|
|
39
|
+
onProgress?: (e: IndexEvent) => void;
|
|
40
|
+
/** When set, every document (including unchanged ones, so earlier failures are retried) is summarized; omitted = no LLM calls at all. */
|
|
41
|
+
summary?: SummaryOptions;
|
|
42
|
+
}
|
|
43
|
+
export interface IndexReport {
|
|
44
|
+
added: string[];
|
|
45
|
+
updated: string[];
|
|
46
|
+
removed: string[];
|
|
47
|
+
unchanged: string[];
|
|
48
|
+
skipped: {
|
|
49
|
+
path: string;
|
|
50
|
+
reason: string;
|
|
51
|
+
}[];
|
|
52
|
+
/**
|
|
53
|
+
* Only with `summary`: documents whose summarization failed (in whole or in part). The document itself stays
|
|
54
|
+
* indexed and searchable; whatever succeeded was saved, the rest is retried (cache-assisted) on the next
|
|
55
|
+
* `summarizeDocument` call.
|
|
56
|
+
*/
|
|
57
|
+
summaryErrors: {
|
|
58
|
+
path: string;
|
|
59
|
+
nodeId: string | null;
|
|
60
|
+
message: string;
|
|
61
|
+
}[];
|
|
62
|
+
/** Present when the stored index format differed from this run's, so every document was re-indexed. */
|
|
63
|
+
rebuilt?: {
|
|
64
|
+
reason: string;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Non-fatal findings about documents parsed in this run (unchanged documents are not re-checked). Currently:
|
|
68
|
+
* setext h2 headings that look like page breaks (see `SUSPECT_SETEXT_CHARS`), reported only while `setext` is on.
|
|
69
|
+
*/
|
|
70
|
+
warnings: {
|
|
71
|
+
path: string;
|
|
72
|
+
message: string;
|
|
73
|
+
}[];
|
|
74
|
+
}
|
|
75
|
+
export type IndexFormatOptions = Pick<IndexOptions, 'countTokens' | 'tokenCounterId' | 'maxNodeTokens' | 'routingCost' | 'setext'>;
|
|
76
|
+
/** Fingerprint of everything besides file content that shapes the stored trees (INDEX_FORMAT_VERSION + build parameters). */
|
|
77
|
+
export declare function indexFormat(opts: IndexFormatOptions): string;
|
|
78
|
+
/** A setext h2 whose heading text is longer than this many code points (or spans several lines) looks like a page. */
|
|
79
|
+
export declare const SUSPECT_SETEXT_CHARS = 120;
|
|
80
|
+
/**
|
|
81
|
+
* Incrementally indexes the Markdown files under `root` (spec §6): unchanged content hashes are skipped,
|
|
82
|
+
* new / changed files are rebuilt one transaction per document, and documents missing on disk are deleted.
|
|
83
|
+
* A file that cannot be read, decoded or indexed is reported in `skipped`; any previous version of it is
|
|
84
|
+
* dropped so the index never serves content that no longer matches the disk. An unreadable subdirectory is
|
|
85
|
+
* reported in `skipped` (path with trailing `/`) and the documents previously indexed under it are kept.
|
|
86
|
+
* When the stored index format (see `indexFormat`) differs from this run's, every document is re-indexed
|
|
87
|
+
* (summaries are reused through the content-hash cache) and the report carries `rebuilt`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function indexLibrary(opts: IndexOptions): Promise<IndexReport>;
|