@hraness/kb 0.17.1
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/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/evaluation-redaction.ts
|
|
3
|
+
function redactEvaluationMachinePaths(value) {
|
|
4
|
+
return value.replace(/file:\/\/\/(?:Users|home)\/[^/\\\s"'`]+/giu, "file://<home>").replace(/file:\/\/\/root(?=\/|$)/giu, "file://<home>").replace(/file:\/\/\/(?:private\/tmp|tmp|var\/tmp)(?=\/|$)/giu, "file://<temporary>").replace(/file:\/\/\/(?:private\/)?var\/folders\/[^/\\\s"'`]+\/[^/\\\s"'`]+\/[A-Z](?=\/|$)/giu, "file://<temporary>").replace(/(^|[^A-Za-z0-9:/])\/(?:Users|home)\/[^/\\\s"'`]+/gu, "$1<home>").replace(/(^|[^A-Za-z0-9:/])\/root(?=\/|$)/gu, "$1<home>").replace(/(^|[^A-Za-z0-9:/])\/(?:private\/)?var\/folders\/[^/\\\s"'`]+\/[^/\\\s"'`]+\/[A-Z](?=\/|$)/gu, "$1<temporary>").replace(/(^|[^A-Za-z0-9:/])\/(?:private\/tmp|tmp|var\/tmp)(?=\/|$)/gu, "$1<temporary>").replace(/(^|[^A-Za-z0-9])(?:[A-Za-z]:[\\/]Users[\\/][^\\/\s"'`]+)/giu, "$1<home>");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// src/evaluation.ts
|
|
8
|
+
var RETRIEVAL_EVALUATION_SCHEMA_VERSION = 1;
|
|
9
|
+
var RETRIEVAL_EVALUATION_REPORT_VERSION = 1;
|
|
10
|
+
var MAX_EVALUATION_QUERIES = 2000;
|
|
11
|
+
var MAX_EVALUATION_QRELS_PER_QUERY = 2000;
|
|
12
|
+
var MAX_EVALUATION_RETRIEVERS = 32;
|
|
13
|
+
var MAX_EVALUATION_RESULTS_PER_QUERY = 1000;
|
|
14
|
+
var MAX_EVALUATION_TEXT_BYTES = 16 * 1024;
|
|
15
|
+
var MAX_EVALUATION_EVIDENCE_BYTES = 64 * 1024;
|
|
16
|
+
var MAX_EVALUATION_DIAGNOSTICS = 100;
|
|
17
|
+
var MAX_EVALUATION_TIMEOUT_MS = 5 * 60000;
|
|
18
|
+
var MAX_BOOTSTRAP_RESAMPLES = 1e5;
|
|
19
|
+
var objectIdPattern = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/u;
|
|
20
|
+
var sha256Pattern = /^[0-9a-f]{64}$/u;
|
|
21
|
+
var windowsAbsolutePattern = /^[a-z]:[\\/]/iu;
|
|
22
|
+
function record(value, label) {
|
|
23
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
24
|
+
throw new TypeError(`${label} must be an object.`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function strictKeys(value, allowed, label) {
|
|
29
|
+
const allowedSet = new Set(allowed);
|
|
30
|
+
const extra = Object.keys(value).filter((key) => !allowedSet.has(key));
|
|
31
|
+
if (extra.length > 0)
|
|
32
|
+
throw new TypeError(`${label} has unknown fields: ${extra.toSorted().join(", ")}.`);
|
|
33
|
+
}
|
|
34
|
+
function boundedString(value, label, maximumBytes = MAX_EVALUATION_TEXT_BYTES) {
|
|
35
|
+
if (typeof value !== "string" || value.trim() === "" || /[\0\r\n]/u.test(value) || Buffer.byteLength(value, "utf8") > maximumBytes) {
|
|
36
|
+
throw new TypeError(`${label} must be a non-empty, single-line string of at most ${maximumBytes} UTF-8 bytes.`);
|
|
37
|
+
}
|
|
38
|
+
return value.normalize("NFC");
|
|
39
|
+
}
|
|
40
|
+
function optionalBoundedString(value, label) {
|
|
41
|
+
return value === undefined ? undefined : boundedString(value, label);
|
|
42
|
+
}
|
|
43
|
+
function stringArray(value, label, maximum = 100) {
|
|
44
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > maximum) {
|
|
45
|
+
throw new TypeError(`${label} must be a non-empty array with at most ${maximum} entries.`);
|
|
46
|
+
}
|
|
47
|
+
const strings = value.map((entry, index) => boundedString(entry, `${label}[${index}]`));
|
|
48
|
+
if (new Set(strings).size !== strings.length)
|
|
49
|
+
throw new TypeError(`${label} must not contain duplicates.`);
|
|
50
|
+
return Object.freeze(strings);
|
|
51
|
+
}
|
|
52
|
+
function frozenVaultRoot(value) {
|
|
53
|
+
const path = boundedString(value, "frozen.vaultRoot").replaceAll("\\", "/");
|
|
54
|
+
if (path.startsWith("/") || windowsAbsolutePattern.test(path) || path.split("/").some((part) => part === "" || part === ".."))
|
|
55
|
+
throw new TypeError("frozen.vaultRoot must be a confined repository-relative path.");
|
|
56
|
+
return path.replace(/^\.\//u, "");
|
|
57
|
+
}
|
|
58
|
+
function confinedRelativePath(value, label, allowRoot = false) {
|
|
59
|
+
const path = boundedString(value, label).replaceAll("\\", "/").replace(/^\.\//u, "");
|
|
60
|
+
if (allowRoot && path === ".")
|
|
61
|
+
return path;
|
|
62
|
+
if (path.startsWith("/") || windowsAbsolutePattern.test(path) || path.split("/").some((part) => part === "" || part === "." || part === ".."))
|
|
63
|
+
throw new TypeError(`${label} must be a confined repository-relative path.`);
|
|
64
|
+
return path;
|
|
65
|
+
}
|
|
66
|
+
var queryClasses = new Set([
|
|
67
|
+
"active-plan",
|
|
68
|
+
"code-path-context",
|
|
69
|
+
"conceptual-recall",
|
|
70
|
+
"current-decision",
|
|
71
|
+
"exact-identifier",
|
|
72
|
+
"historical-rationale",
|
|
73
|
+
"no-answer",
|
|
74
|
+
"source-evidence",
|
|
75
|
+
"stale-current-conflict"
|
|
76
|
+
]);
|
|
77
|
+
function parseAssessor(value, index) {
|
|
78
|
+
const input = record(value, `assessment.assessors[${index}]`);
|
|
79
|
+
strictKeys(input, ["id", "displayName", "affiliation"], `assessment.assessors[${index}]`);
|
|
80
|
+
const displayName = optionalBoundedString(input.displayName, `assessment.assessors[${index}].displayName`);
|
|
81
|
+
const affiliation = optionalBoundedString(input.affiliation, `assessment.assessors[${index}].affiliation`);
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
id: boundedString(input.id, `assessment.assessors[${index}].id`, 256),
|
|
84
|
+
...displayName === undefined ? {} : { displayName },
|
|
85
|
+
...affiliation === undefined ? {} : { affiliation }
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function parseQrel(value, queryIndex, index) {
|
|
89
|
+
const label = `queries[${queryIndex}].qrels[${index}]`;
|
|
90
|
+
const input = record(value, label);
|
|
91
|
+
strictKeys(input, ["documentId", "relevance"], label);
|
|
92
|
+
if (input.relevance !== 0 && input.relevance !== 1 && input.relevance !== 2 && input.relevance !== 3)
|
|
93
|
+
throw new TypeError(`${label}.relevance must be an integer from 0 through 3.`);
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
documentId: boundedString(input.documentId, `${label}.documentId`),
|
|
96
|
+
relevance: input.relevance
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function parseAdjudication(value, queryIndex, assessors) {
|
|
100
|
+
const label = `queries[${queryIndex}].adjudication`;
|
|
101
|
+
const input = record(value, label);
|
|
102
|
+
strictKeys(input, ["status", "adjudicatorId", "note"], label);
|
|
103
|
+
if (input.status !== "not-required" && input.status !== "resolved") {
|
|
104
|
+
throw new TypeError(`${label}.status must be not-required or resolved.`);
|
|
105
|
+
}
|
|
106
|
+
const adjudicatorId = optionalBoundedString(input.adjudicatorId, `${label}.adjudicatorId`);
|
|
107
|
+
const note = optionalBoundedString(input.note, `${label}.note`);
|
|
108
|
+
if (input.status === "resolved" && adjudicatorId === undefined) {
|
|
109
|
+
throw new TypeError(`${label}.adjudicatorId is required for resolved judgments.`);
|
|
110
|
+
}
|
|
111
|
+
if (adjudicatorId !== undefined && !assessors.has(adjudicatorId)) {
|
|
112
|
+
throw new TypeError(`${label}.adjudicatorId must name a declared assessor.`);
|
|
113
|
+
}
|
|
114
|
+
return Object.freeze({
|
|
115
|
+
status: input.status,
|
|
116
|
+
...adjudicatorId === undefined ? {} : { adjudicatorId },
|
|
117
|
+
...note === undefined ? {} : { note }
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function parseMetadataFilter(value, queryIndex, index) {
|
|
121
|
+
const label = `queries[${queryIndex}].inputs.metadata.filters[${index}]`;
|
|
122
|
+
const input = record(value, label);
|
|
123
|
+
strictKeys(input, ["kind", "path", "value"], label);
|
|
124
|
+
const path = boundedString(input.path, `${label}.path`, 2048);
|
|
125
|
+
if (input.kind === "exists") {
|
|
126
|
+
if (input.value !== undefined)
|
|
127
|
+
throw new TypeError(`${label}.value is not allowed for an exists filter.`);
|
|
128
|
+
return Object.freeze({ kind: "exists", path });
|
|
129
|
+
}
|
|
130
|
+
if (input.kind !== "equals")
|
|
131
|
+
throw new TypeError(`${label}.kind must be exists or equals.`);
|
|
132
|
+
const filterValue = input.value;
|
|
133
|
+
if (filterValue !== null && typeof filterValue !== "string" && typeof filterValue !== "number" && typeof filterValue !== "boolean")
|
|
134
|
+
throw new TypeError(`${label}.value must be a scalar.`);
|
|
135
|
+
if (typeof filterValue === "number" && !Number.isFinite(filterValue)) {
|
|
136
|
+
throw new TypeError(`${label}.value must be finite.`);
|
|
137
|
+
}
|
|
138
|
+
return Object.freeze({
|
|
139
|
+
kind: "equals",
|
|
140
|
+
path,
|
|
141
|
+
value: typeof filterValue === "string" ? boundedString(filterValue, `${label}.value`) : filterValue
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function optionalStringList(value, label, maximum) {
|
|
145
|
+
if (value === undefined)
|
|
146
|
+
return Object.freeze([]);
|
|
147
|
+
if (!Array.isArray(value) || value.length > maximum) {
|
|
148
|
+
throw new TypeError(`${label} must be an array with at most ${maximum} entries.`);
|
|
149
|
+
}
|
|
150
|
+
const strings = value.map((entry, index) => boundedString(entry, `${label}[${index}]`));
|
|
151
|
+
if (new Set(strings).size !== strings.length)
|
|
152
|
+
throw new TypeError(`${label} must not contain duplicates.`);
|
|
153
|
+
return Object.freeze(strings);
|
|
154
|
+
}
|
|
155
|
+
function parseRetrievalInputs(value, queryIndex) {
|
|
156
|
+
const label = `queries[${queryIndex}].inputs`;
|
|
157
|
+
const input = record(value, label);
|
|
158
|
+
strictKeys(input, ["text", "noteId", "metadata", "graph", "context", "history"], label);
|
|
159
|
+
if (Object.keys(input).length === 0)
|
|
160
|
+
throw new TypeError(`${label} must define at least one retrieval lane input.`);
|
|
161
|
+
const text = optionalBoundedString(input.text, `${label}.text`);
|
|
162
|
+
const noteId = input.noteId === undefined ? undefined : confinedRelativePath(input.noteId, `${label}.noteId`);
|
|
163
|
+
let metadata;
|
|
164
|
+
if (input.metadata !== undefined) {
|
|
165
|
+
const metadataInput = record(input.metadata, `${label}.metadata`);
|
|
166
|
+
strictKeys(metadataInput, ["filters", "tags"], `${label}.metadata`);
|
|
167
|
+
const filtersInput = metadataInput.filters ?? [];
|
|
168
|
+
if (!Array.isArray(filtersInput) || filtersInput.length > 32) {
|
|
169
|
+
throw new TypeError(`${label}.metadata.filters must have at most 32 entries.`);
|
|
170
|
+
}
|
|
171
|
+
const filters = filtersInput.map((entry, index) => parseMetadataFilter(entry, queryIndex, index));
|
|
172
|
+
const tags = optionalStringList(metadataInput.tags, `${label}.metadata.tags`, 32);
|
|
173
|
+
if (filters.length === 0 && tags.length === 0) {
|
|
174
|
+
throw new TypeError(`${label}.metadata must contain a filter or tag.`);
|
|
175
|
+
}
|
|
176
|
+
metadata = Object.freeze({ filters: Object.freeze(filters), tags });
|
|
177
|
+
}
|
|
178
|
+
let graph;
|
|
179
|
+
if (input.graph !== undefined) {
|
|
180
|
+
const graphInput = record(input.graph, `${label}.graph`);
|
|
181
|
+
strictKeys(graphInput, ["seeds", "depth"], `${label}.graph`);
|
|
182
|
+
const seeds = stringArray(graphInput.seeds, `${label}.graph.seeds`, 10).map((seed, index) => confinedRelativePath(seed, `${label}.graph.seeds[${index}]`));
|
|
183
|
+
if (graphInput.depth !== 1 && graphInput.depth !== 2) {
|
|
184
|
+
throw new TypeError(`${label}.graph.depth must be 1 or 2.`);
|
|
185
|
+
}
|
|
186
|
+
graph = Object.freeze({ seeds: Object.freeze(seeds), depth: graphInput.depth });
|
|
187
|
+
}
|
|
188
|
+
let context;
|
|
189
|
+
if (input.context !== undefined) {
|
|
190
|
+
const contextInput = record(input.context, `${label}.context`);
|
|
191
|
+
strictKeys(contextInput, ["repositoryPath"], `${label}.context`);
|
|
192
|
+
context = Object.freeze({
|
|
193
|
+
repositoryPath: confinedRelativePath(contextInput.repositoryPath, `${label}.context.repositoryPath`, true)
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
let history;
|
|
197
|
+
if (input.history !== undefined) {
|
|
198
|
+
const historyInput = record(input.history, `${label}.history`);
|
|
199
|
+
strictKeys(historyInput, ["query", "noteIds"], `${label}.history`);
|
|
200
|
+
const noteIds = optionalStringList(historyInput.noteIds, `${label}.history.noteIds`, 100).map((id, index) => confinedRelativePath(id, `${label}.history.noteIds[${index}]`));
|
|
201
|
+
history = Object.freeze({
|
|
202
|
+
query: boundedString(historyInput.query, `${label}.history.query`, 2048),
|
|
203
|
+
noteIds: Object.freeze(noteIds)
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const parsed = {
|
|
207
|
+
...text === undefined ? {} : { text },
|
|
208
|
+
...noteId === undefined ? {} : { noteId },
|
|
209
|
+
...metadata === undefined ? {} : { metadata },
|
|
210
|
+
...graph === undefined ? {} : { graph },
|
|
211
|
+
...context === undefined ? {} : { context },
|
|
212
|
+
...history === undefined ? {} : { history }
|
|
213
|
+
};
|
|
214
|
+
if (Object.keys(parsed).length === 0) {
|
|
215
|
+
throw new TypeError(`${label} must define at least one retrieval lane input.`);
|
|
216
|
+
}
|
|
217
|
+
return Object.freeze(parsed);
|
|
218
|
+
}
|
|
219
|
+
function parseQuery(value, index, assessorIds) {
|
|
220
|
+
const label = `queries[${index}]`;
|
|
221
|
+
const input = record(value, label);
|
|
222
|
+
strictKeys(input, ["id", "text", "class", "split", "answer", "inputs", "qrels", "assessorIds", "adjudication"], label);
|
|
223
|
+
if (typeof input.class !== "string" || !queryClasses.has(input.class)) {
|
|
224
|
+
throw new TypeError(`${label}.class is not a supported query class.`);
|
|
225
|
+
}
|
|
226
|
+
if (input.split !== "development" && input.split !== "test") {
|
|
227
|
+
throw new TypeError(`${label}.split must be development or test.`);
|
|
228
|
+
}
|
|
229
|
+
if (input.answer !== "answerable" && input.answer !== "no-answer") {
|
|
230
|
+
throw new TypeError(`${label}.answer must be answerable or no-answer.`);
|
|
231
|
+
}
|
|
232
|
+
if (!Array.isArray(input.qrels) || input.qrels.length > MAX_EVALUATION_QRELS_PER_QUERY) {
|
|
233
|
+
throw new TypeError(`${label}.qrels must have at most ${MAX_EVALUATION_QRELS_PER_QUERY} entries.`);
|
|
234
|
+
}
|
|
235
|
+
const qrels = input.qrels.map((entry, qrelIndex) => parseQrel(entry, index, qrelIndex));
|
|
236
|
+
const documentIds = qrels.map(({ documentId }) => documentId);
|
|
237
|
+
if (new Set(documentIds).size !== documentIds.length) {
|
|
238
|
+
throw new TypeError(`${label}.qrels must not repeat a document ID.`);
|
|
239
|
+
}
|
|
240
|
+
const queryAssessorIds = stringArray(input.assessorIds, `${label}.assessorIds`);
|
|
241
|
+
for (const id of queryAssessorIds) {
|
|
242
|
+
if (!assessorIds.has(id))
|
|
243
|
+
throw new TypeError(`${label}.assessorIds names undeclared assessor ${id}.`);
|
|
244
|
+
}
|
|
245
|
+
if (input.answer === "answerable" && !qrels.some(({ relevance }) => relevance > 0)) {
|
|
246
|
+
throw new TypeError(`${label} is answerable but has no positive relevance judgment.`);
|
|
247
|
+
}
|
|
248
|
+
if (input.answer === "no-answer" && qrels.some(({ relevance }) => relevance > 0)) {
|
|
249
|
+
throw new TypeError(`${label} is no-answer but has a positive relevance judgment.`);
|
|
250
|
+
}
|
|
251
|
+
return Object.freeze({
|
|
252
|
+
id: boundedString(input.id, `${label}.id`, 256),
|
|
253
|
+
text: boundedString(input.text, `${label}.text`),
|
|
254
|
+
class: input.class,
|
|
255
|
+
split: input.split,
|
|
256
|
+
answer: input.answer,
|
|
257
|
+
inputs: parseRetrievalInputs(input.inputs, index),
|
|
258
|
+
qrels: Object.freeze(qrels),
|
|
259
|
+
assessorIds: queryAssessorIds,
|
|
260
|
+
adjudication: parseAdjudication(input.adjudication, index, assessorIds)
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function parseRetrievalEvaluationCorpus(inputValue) {
|
|
264
|
+
const input = record(inputValue, "evaluation corpus");
|
|
265
|
+
strictKeys(input, ["schemaVersion", "id", "description", "frozen", "assessment", "queries"], "evaluation corpus");
|
|
266
|
+
if (input.schemaVersion !== RETRIEVAL_EVALUATION_SCHEMA_VERSION) {
|
|
267
|
+
throw new TypeError(`evaluation corpus schemaVersion must be ${RETRIEVAL_EVALUATION_SCHEMA_VERSION}.`);
|
|
268
|
+
}
|
|
269
|
+
const frozenInput = record(input.frozen, "frozen");
|
|
270
|
+
strictKeys(frozenInput, ["repositoryCommit", "vaultTree", "vaultRoot"], "frozen");
|
|
271
|
+
if (typeof frozenInput.repositoryCommit !== "string" || !objectIdPattern.test(frozenInput.repositoryCommit)) {
|
|
272
|
+
throw new TypeError("frozen.repositoryCommit must be a lowercase Git object ID.");
|
|
273
|
+
}
|
|
274
|
+
if (typeof frozenInput.vaultTree !== "string" || !objectIdPattern.test(frozenInput.vaultTree)) {
|
|
275
|
+
throw new TypeError("frozen.vaultTree must be a lowercase Git object ID.");
|
|
276
|
+
}
|
|
277
|
+
const assessmentInput = record(input.assessment, "assessment");
|
|
278
|
+
strictKeys(assessmentInput, ["rubricVersion", "assessors"], "assessment");
|
|
279
|
+
if (!Array.isArray(assessmentInput.assessors) || assessmentInput.assessors.length < 1 || assessmentInput.assessors.length > 100) {
|
|
280
|
+
throw new TypeError("assessment.assessors must have from 1 through 100 entries.");
|
|
281
|
+
}
|
|
282
|
+
const assessors = assessmentInput.assessors.map(parseAssessor);
|
|
283
|
+
const assessorIdList = assessors.map(({ id }) => id);
|
|
284
|
+
if (new Set(assessorIdList).size !== assessorIdList.length) {
|
|
285
|
+
throw new TypeError("assessment.assessors must not repeat an ID.");
|
|
286
|
+
}
|
|
287
|
+
if (!Array.isArray(input.queries) || input.queries.length < 1 || input.queries.length > MAX_EVALUATION_QUERIES) {
|
|
288
|
+
throw new TypeError(`queries must have from 1 through ${MAX_EVALUATION_QUERIES} entries.`);
|
|
289
|
+
}
|
|
290
|
+
const assessorIds = new Set(assessorIdList);
|
|
291
|
+
const queries = input.queries.map((entry, index) => parseQuery(entry, index, assessorIds));
|
|
292
|
+
const queryIds = queries.map(({ id }) => id);
|
|
293
|
+
if (new Set(queryIds).size !== queryIds.length)
|
|
294
|
+
throw new TypeError("queries must not repeat an ID.");
|
|
295
|
+
if (!queries.some(({ split }) => split === "development") || !queries.some(({ split }) => split === "test")) {
|
|
296
|
+
throw new TypeError("evaluation corpus must contain both development and test queries.");
|
|
297
|
+
}
|
|
298
|
+
return Object.freeze({
|
|
299
|
+
schemaVersion: 1,
|
|
300
|
+
id: boundedString(input.id, "id", 256),
|
|
301
|
+
description: boundedString(input.description, "description"),
|
|
302
|
+
frozen: Object.freeze({
|
|
303
|
+
repositoryCommit: frozenInput.repositoryCommit,
|
|
304
|
+
vaultTree: frozenInput.vaultTree,
|
|
305
|
+
vaultRoot: frozenVaultRoot(frozenInput.vaultRoot)
|
|
306
|
+
}),
|
|
307
|
+
assessment: Object.freeze({
|
|
308
|
+
rubricVersion: boundedString(assessmentInput.rubricVersion, "assessment.rubricVersion", 256),
|
|
309
|
+
assessors: Object.freeze(assessors)
|
|
310
|
+
}),
|
|
311
|
+
queries: Object.freeze(queries)
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function nonnegativeNumberMap(value, label) {
|
|
315
|
+
const input = value === undefined ? {} : record(value, label);
|
|
316
|
+
if (Object.keys(input).length > 32)
|
|
317
|
+
throw new TypeError(`${label} may have at most 32 fields.`);
|
|
318
|
+
const output = {};
|
|
319
|
+
for (const [rawKey, candidate] of Object.entries(input).toSorted(([left], [right]) => left.localeCompare(right))) {
|
|
320
|
+
if (typeof candidate !== "number" || !Number.isFinite(candidate) || candidate < 0) {
|
|
321
|
+
throw new TypeError(`${label} ${rawKey} must be a non-negative finite number.`);
|
|
322
|
+
}
|
|
323
|
+
output[boundedString(rawKey, `${label} key`, 256)] = candidate;
|
|
324
|
+
}
|
|
325
|
+
return Object.freeze(output);
|
|
326
|
+
}
|
|
327
|
+
function jsonEvidence(value, label) {
|
|
328
|
+
let serialized;
|
|
329
|
+
try {
|
|
330
|
+
serialized = JSON.stringify(value, (_key, candidate) => typeof candidate === "string" ? redactEvaluationMachinePaths(candidate) : candidate);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
throw new TypeError(`${label} must be JSON-serializable.`, { cause: error });
|
|
333
|
+
}
|
|
334
|
+
if (serialized === undefined || Buffer.byteLength(serialized, "utf8") > MAX_EVALUATION_EVIDENCE_BYTES) {
|
|
335
|
+
throw new TypeError(`${label} must serialize to at most ${MAX_EVALUATION_EVIDENCE_BYTES} UTF-8 bytes.`);
|
|
336
|
+
}
|
|
337
|
+
return JSON.parse(serialized);
|
|
338
|
+
}
|
|
339
|
+
function parseRetrieverResult(value, limit) {
|
|
340
|
+
const input = record(value, "retriever result");
|
|
341
|
+
strictKeys(input, ["status", "hits", "diagnostics", "timings", "resources"], "retriever result");
|
|
342
|
+
if (input.status !== "ready" && input.status !== "degraded" && input.status !== "unavailable") {
|
|
343
|
+
throw new TypeError("retriever result status must be ready, degraded, or unavailable.");
|
|
344
|
+
}
|
|
345
|
+
if (!Array.isArray(input.hits) || input.hits.length > limit) {
|
|
346
|
+
throw new TypeError(`retriever result must have at most ${limit} hits.`);
|
|
347
|
+
}
|
|
348
|
+
const hits = input.hits.map((entry, index) => {
|
|
349
|
+
const label = `retriever result hits[${index}]`;
|
|
350
|
+
const hit = record(entry, label);
|
|
351
|
+
strictKeys(hit, ["documentId", "rank", "score", "evidence"], label);
|
|
352
|
+
if (!Number.isSafeInteger(hit.rank) || hit.rank < 1 || hit.rank > MAX_EVALUATION_RESULTS_PER_QUERY) {
|
|
353
|
+
throw new TypeError(`${label}.rank must be an integer from 1 through ${MAX_EVALUATION_RESULTS_PER_QUERY}.`);
|
|
354
|
+
}
|
|
355
|
+
if (hit.score !== undefined && (typeof hit.score !== "number" || !Number.isFinite(hit.score))) {
|
|
356
|
+
throw new TypeError(`${label}.score must be a finite number.`);
|
|
357
|
+
}
|
|
358
|
+
return Object.freeze({
|
|
359
|
+
documentId: boundedString(hit.documentId, `${label}.documentId`),
|
|
360
|
+
rank: hit.rank,
|
|
361
|
+
...hit.score === undefined ? {} : { score: hit.score },
|
|
362
|
+
...hit.evidence === undefined ? {} : { evidence: jsonEvidence(hit.evidence, `${label}.evidence`) }
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
if (input.status === "unavailable" && hits.length > 0) {
|
|
366
|
+
throw new TypeError("unavailable retriever results may not contain hits.");
|
|
367
|
+
}
|
|
368
|
+
if (new Set(hits.map(({ rank }) => rank)).size !== hits.length) {
|
|
369
|
+
throw new TypeError("retriever result ranks must be unique.");
|
|
370
|
+
}
|
|
371
|
+
if (new Set(hits.map(({ documentId }) => documentId)).size !== hits.length) {
|
|
372
|
+
throw new TypeError("retriever result document IDs must be unique.");
|
|
373
|
+
}
|
|
374
|
+
const diagnosticsInput = input.diagnostics ?? [];
|
|
375
|
+
if (!Array.isArray(diagnosticsInput) || diagnosticsInput.length > MAX_EVALUATION_DIAGNOSTICS) {
|
|
376
|
+
throw new TypeError(`retriever diagnostics must have at most ${MAX_EVALUATION_DIAGNOSTICS} entries.`);
|
|
377
|
+
}
|
|
378
|
+
const diagnostics = diagnosticsInput.map((entry, index) => {
|
|
379
|
+
const label = `retriever result diagnostics[${index}]`;
|
|
380
|
+
const diagnostic = record(entry, label);
|
|
381
|
+
strictKeys(diagnostic, ["lane", "status", "message"], label);
|
|
382
|
+
if (diagnostic.status !== "ready" && diagnostic.status !== "degraded" && diagnostic.status !== "unavailable") {
|
|
383
|
+
throw new TypeError(`${label}.status must be ready, degraded, or unavailable.`);
|
|
384
|
+
}
|
|
385
|
+
const rawMessage = optionalBoundedString(diagnostic.message, `${label}.message`);
|
|
386
|
+
const message = rawMessage === undefined ? undefined : redactEvaluationMachinePaths(rawMessage);
|
|
387
|
+
return Object.freeze({
|
|
388
|
+
lane: boundedString(diagnostic.lane, `${label}.lane`, 256),
|
|
389
|
+
status: diagnostic.status,
|
|
390
|
+
...message === undefined ? {} : { message }
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
const timings = nonnegativeNumberMap(input.timings, "retriever result timings");
|
|
394
|
+
const resources = nonnegativeNumberMap(input.resources, "retriever result resources");
|
|
395
|
+
return Object.freeze({
|
|
396
|
+
status: input.status,
|
|
397
|
+
hits: Object.freeze(hits.toSorted((left, right) => left.rank - right.rank)),
|
|
398
|
+
diagnostics: Object.freeze(diagnostics),
|
|
399
|
+
timings,
|
|
400
|
+
resources
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
function shortError(error) {
|
|
404
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
405
|
+
return redactEvaluationMachinePaths(message.normalize("NFC").replace(/[\0\r\n]/gu, " ")).slice(0, 2000);
|
|
406
|
+
}
|
|
407
|
+
async function evaluateOne(corpus, query, retriever, limit, timeoutMs, now) {
|
|
408
|
+
const controller = new AbortController;
|
|
409
|
+
const startedAt = now();
|
|
410
|
+
let timer;
|
|
411
|
+
try {
|
|
412
|
+
const retrieval = Promise.resolve().then(() => retriever.retrieve({
|
|
413
|
+
corpus: corpus.frozen,
|
|
414
|
+
query,
|
|
415
|
+
limit,
|
|
416
|
+
signal: controller.signal
|
|
417
|
+
}));
|
|
418
|
+
const timed = new Promise((_resolve, reject) => {
|
|
419
|
+
timer = setTimeout(() => {
|
|
420
|
+
controller.abort(new Error(`Retriever exceeded the ${timeoutMs}-millisecond limit.`));
|
|
421
|
+
reject(new Error(`Retriever exceeded the ${timeoutMs}-millisecond limit.`));
|
|
422
|
+
}, timeoutMs);
|
|
423
|
+
});
|
|
424
|
+
const raw = await Promise.race([retrieval, timed]);
|
|
425
|
+
const result = parseRetrieverResult(raw, limit);
|
|
426
|
+
return Object.freeze({
|
|
427
|
+
retrieverId: retriever.id,
|
|
428
|
+
queryId: query.id,
|
|
429
|
+
queryClass: query.class,
|
|
430
|
+
split: query.split,
|
|
431
|
+
status: result.status,
|
|
432
|
+
hits: result.hits,
|
|
433
|
+
diagnostics: result.diagnostics ?? Object.freeze([]),
|
|
434
|
+
timing: Object.freeze({
|
|
435
|
+
elapsedMs: Math.max(0, now() - startedAt),
|
|
436
|
+
backend: result.timings ?? Object.freeze({})
|
|
437
|
+
}),
|
|
438
|
+
resources: result.resources ?? Object.freeze({})
|
|
439
|
+
});
|
|
440
|
+
} catch (error) {
|
|
441
|
+
const timeout = controller.signal.aborted;
|
|
442
|
+
return Object.freeze({
|
|
443
|
+
retrieverId: retriever.id,
|
|
444
|
+
queryId: query.id,
|
|
445
|
+
queryClass: query.class,
|
|
446
|
+
split: query.split,
|
|
447
|
+
status: "failed",
|
|
448
|
+
hits: Object.freeze([]),
|
|
449
|
+
diagnostics: Object.freeze([]),
|
|
450
|
+
timing: Object.freeze({ elapsedMs: Math.max(0, now() - startedAt), backend: Object.freeze({}) }),
|
|
451
|
+
resources: Object.freeze({}),
|
|
452
|
+
failure: Object.freeze({
|
|
453
|
+
kind: timeout ? "timeout" : error instanceof TypeError ? "invalid-result" : "exception",
|
|
454
|
+
message: shortError(error)
|
|
455
|
+
})
|
|
456
|
+
});
|
|
457
|
+
} finally {
|
|
458
|
+
if (timer !== undefined)
|
|
459
|
+
clearTimeout(timer);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async function runRetrievalEvaluation(options) {
|
|
463
|
+
if (options.retrievers.length < 1 || options.retrievers.length > MAX_EVALUATION_RETRIEVERS) {
|
|
464
|
+
throw new RangeError(`Evaluation accepts from 1 through ${MAX_EVALUATION_RETRIEVERS} retrievers.`);
|
|
465
|
+
}
|
|
466
|
+
const retrieverIds = options.retrievers.map(({ id }) => boundedString(id, "retriever id", 256));
|
|
467
|
+
if (new Set(retrieverIds).size !== retrieverIds.length)
|
|
468
|
+
throw new TypeError("Evaluation retriever IDs must be unique.");
|
|
469
|
+
const limit = options.limit ?? 20;
|
|
470
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_EVALUATION_RESULTS_PER_QUERY) {
|
|
471
|
+
throw new RangeError(`Evaluation result limit must be from 1 through ${MAX_EVALUATION_RESULTS_PER_QUERY}.`);
|
|
472
|
+
}
|
|
473
|
+
const timeoutMs = options.timeoutMs ?? 30000;
|
|
474
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_EVALUATION_TIMEOUT_MS) {
|
|
475
|
+
throw new RangeError(`Evaluation timeout must be from 1 through ${MAX_EVALUATION_TIMEOUT_MS} milliseconds.`);
|
|
476
|
+
}
|
|
477
|
+
const split = options.split ?? "test";
|
|
478
|
+
const queries = options.corpus.queries.filter((query) => split === "all" || query.split === split);
|
|
479
|
+
const runs = [];
|
|
480
|
+
for (const retriever of options.retrievers) {
|
|
481
|
+
for (const query of queries) {
|
|
482
|
+
runs.push(await evaluateOne(options.corpus, query, retriever, limit, timeoutMs, options.now ?? performance.now.bind(performance)));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return Object.freeze(runs);
|
|
486
|
+
}
|
|
487
|
+
function metricsFor(query, hits, cutoff) {
|
|
488
|
+
const ranked = hits.filter(({ rank }) => rank <= cutoff).toSorted((left, right) => left.rank - right.rank);
|
|
489
|
+
if (query.answer === "no-answer") {
|
|
490
|
+
return Object.freeze({
|
|
491
|
+
recall: null,
|
|
492
|
+
reciprocalRank: null,
|
|
493
|
+
ndcg: null,
|
|
494
|
+
noAnswerAccuracy: ranked.length === 0 ? 1 : 0
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
const relevance = new Map(query.qrels.map((qrel) => [qrel.documentId, qrel.relevance]));
|
|
498
|
+
const relevantIds = new Set(query.qrels.filter(({ relevance: value }) => value > 0).map(({ documentId }) => documentId));
|
|
499
|
+
const retrievedRelevant = new Set(ranked.filter(({ documentId }) => relevantIds.has(documentId)).map(({ documentId }) => documentId));
|
|
500
|
+
const firstRelevant = ranked.find(({ documentId }) => relevantIds.has(documentId));
|
|
501
|
+
const dcg = ranked.reduce((total, hit) => {
|
|
502
|
+
const grade = relevance.get(hit.documentId) ?? 0;
|
|
503
|
+
return total + (2 ** grade - 1) / Math.log2(hit.rank + 1);
|
|
504
|
+
}, 0);
|
|
505
|
+
const ideal = query.qrels.map(({ relevance: value }) => value).toSorted((left, right) => right - left).slice(0, cutoff).reduce((total, grade, index) => total + (2 ** grade - 1) / Math.log2(index + 2), 0);
|
|
506
|
+
return Object.freeze({
|
|
507
|
+
recall: retrievedRelevant.size / relevantIds.size,
|
|
508
|
+
reciprocalRank: firstRelevant === undefined ? 0 : 1 / firstRelevant.rank,
|
|
509
|
+
ndcg: ideal === 0 ? 0 : dcg / ideal,
|
|
510
|
+
noAnswerAccuracy: null
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function average(values) {
|
|
514
|
+
return values.length === 0 ? null : values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
515
|
+
}
|
|
516
|
+
function percentile(values, proportion) {
|
|
517
|
+
if (values.length === 0)
|
|
518
|
+
return null;
|
|
519
|
+
const sorted = values.toSorted((left, right) => left - right);
|
|
520
|
+
const index = Math.max(0, Math.ceil(proportion * sorted.length) - 1);
|
|
521
|
+
return sorted[index] ?? null;
|
|
522
|
+
}
|
|
523
|
+
function seededRandom(seed) {
|
|
524
|
+
let state = seed >>> 0;
|
|
525
|
+
return () => {
|
|
526
|
+
state = state + 1831565813 >>> 0;
|
|
527
|
+
let value = state;
|
|
528
|
+
value = Math.imul(value ^ value >>> 15, value | 1);
|
|
529
|
+
value ^= value + Math.imul(value ^ value >>> 7, value | 61);
|
|
530
|
+
return ((value ^ value >>> 14) >>> 0) / 4294967296;
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function pairedBootstrapConfidenceInterval(pairs, options) {
|
|
534
|
+
if (pairs.length < 1 || pairs.length > MAX_EVALUATION_QUERIES) {
|
|
535
|
+
throw new RangeError(`Paired bootstrap accepts from 1 through ${MAX_EVALUATION_QUERIES} pairs.`);
|
|
536
|
+
}
|
|
537
|
+
if (!Number.isSafeInteger(options.seed) || options.seed < 0 || options.seed > 4294967295) {
|
|
538
|
+
throw new RangeError("Bootstrap seed must be an unsigned 32-bit integer.");
|
|
539
|
+
}
|
|
540
|
+
const resamples = options.resamples ?? 1e4;
|
|
541
|
+
if (!Number.isSafeInteger(resamples) || resamples < 100 || resamples > MAX_BOOTSTRAP_RESAMPLES) {
|
|
542
|
+
throw new RangeError(`Bootstrap resamples must be from 100 through ${MAX_BOOTSTRAP_RESAMPLES}.`);
|
|
543
|
+
}
|
|
544
|
+
const confidence = options.confidence ?? 0.95;
|
|
545
|
+
if (!Number.isFinite(confidence) || confidence <= 0 || confidence >= 1) {
|
|
546
|
+
throw new RangeError("Bootstrap confidence must be between 0 and 1.");
|
|
547
|
+
}
|
|
548
|
+
for (const pair of pairs) {
|
|
549
|
+
if (!Number.isFinite(pair.baseline) || !Number.isFinite(pair.candidate)) {
|
|
550
|
+
throw new TypeError("Bootstrap pairs must contain finite values.");
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const random = seededRandom(options.seed);
|
|
554
|
+
const samples = [];
|
|
555
|
+
for (let sample = 0;sample < resamples; sample += 1) {
|
|
556
|
+
let difference = 0;
|
|
557
|
+
for (let index = 0;index < pairs.length; index += 1) {
|
|
558
|
+
const pair = pairs[Math.floor(random() * pairs.length)];
|
|
559
|
+
if (pair !== undefined)
|
|
560
|
+
difference += pair.candidate - pair.baseline;
|
|
561
|
+
}
|
|
562
|
+
samples.push(difference / pairs.length);
|
|
563
|
+
}
|
|
564
|
+
samples.sort((left, right) => left - right);
|
|
565
|
+
const alpha = (1 - confidence) / 2;
|
|
566
|
+
const lowerIndex = Math.floor(alpha * resamples);
|
|
567
|
+
const upperIndex = Math.min(resamples - 1, Math.ceil((1 - alpha) * resamples) - 1);
|
|
568
|
+
return Object.freeze({
|
|
569
|
+
pairs: pairs.length,
|
|
570
|
+
seed: options.seed,
|
|
571
|
+
resamples,
|
|
572
|
+
confidence,
|
|
573
|
+
observedDifference: pairs.reduce((sum, pair) => sum + pair.candidate - pair.baseline, 0) / pairs.length,
|
|
574
|
+
lower: samples[lowerIndex] ?? 0,
|
|
575
|
+
upper: samples[upperIndex] ?? 0
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
function validateEnvironment(environment, expectedRetrieverIds) {
|
|
579
|
+
const timestamp = new Date(environment.generatedAt);
|
|
580
|
+
if (Number.isNaN(timestamp.valueOf()) || timestamp.toISOString() !== environment.generatedAt) {
|
|
581
|
+
throw new TypeError("Evaluation environment generatedAt must be a canonical ISO timestamp.");
|
|
582
|
+
}
|
|
583
|
+
if (environment.model.kind === "local" && !sha256Pattern.test(environment.model.sha256)) {
|
|
584
|
+
throw new TypeError("Evaluation model sha256 must be 64 lowercase hexadecimal characters.");
|
|
585
|
+
}
|
|
586
|
+
const runtime = Object.freeze({
|
|
587
|
+
bun: boundedString(environment.runtime.bun, "environment.runtime.bun", 256),
|
|
588
|
+
node: boundedString(environment.runtime.node, "environment.runtime.node", 256),
|
|
589
|
+
os: boundedString(environment.runtime.os, "environment.runtime.os", 256),
|
|
590
|
+
arch: boundedString(environment.runtime.arch, "environment.runtime.arch", 256),
|
|
591
|
+
hardware: boundedString(environment.runtime.hardware, "environment.runtime.hardware")
|
|
592
|
+
});
|
|
593
|
+
const model = environment.model.kind === "none" ? Object.freeze({
|
|
594
|
+
kind: "none",
|
|
595
|
+
reason: boundedString(environment.model.reason, "environment.model.reason")
|
|
596
|
+
}) : Object.freeze({
|
|
597
|
+
kind: "local",
|
|
598
|
+
id: boundedString(environment.model.id, "environment.model.id", 256),
|
|
599
|
+
revision: boundedString(environment.model.revision, "environment.model.revision", 512),
|
|
600
|
+
sha256: environment.model.sha256
|
|
601
|
+
});
|
|
602
|
+
const fingerprint = environment.cache.fingerprint === undefined ? undefined : boundedString(environment.cache.fingerprint, "environment.cache.fingerprint", 512);
|
|
603
|
+
if (environment.cache.state !== "cold" && environment.cache.state !== "mixed" && environment.cache.state !== "not-applicable" && environment.cache.state !== "warm")
|
|
604
|
+
throw new TypeError("environment.cache.state is invalid.");
|
|
605
|
+
if (environment.retrievers.length < 1 || environment.retrievers.length > MAX_EVALUATION_RETRIEVERS)
|
|
606
|
+
throw new TypeError(`environment.retrievers must have from 1 through ${MAX_EVALUATION_RETRIEVERS} entries.`);
|
|
607
|
+
const retrievers = environment.retrievers.map((retriever, index) => {
|
|
608
|
+
const id = boundedString(retriever.id, `environment.retrievers[${index}].id`, 256);
|
|
609
|
+
const entries = Object.entries(retriever.configuration);
|
|
610
|
+
if (entries.length > 64) {
|
|
611
|
+
throw new TypeError(`environment.retrievers[${index}].configuration may have at most 64 fields.`);
|
|
612
|
+
}
|
|
613
|
+
const configuration = {};
|
|
614
|
+
for (const [rawKey, value] of entries.toSorted(([left], [right]) => left.localeCompare(right))) {
|
|
615
|
+
const key = boundedString(rawKey, `environment.retrievers[${index}] configuration key`, 256);
|
|
616
|
+
if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
|
|
617
|
+
throw new TypeError(`environment.retrievers[${index}].configuration.${key} is not scalar.`);
|
|
618
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
619
|
+
throw new TypeError(`environment.retrievers[${index}].configuration.${key} must be finite.`);
|
|
620
|
+
}
|
|
621
|
+
configuration[key] = typeof value === "string" ? boundedString(value, `environment.retrievers[${index}].configuration.${key}`) : value;
|
|
622
|
+
}
|
|
623
|
+
return Object.freeze({
|
|
624
|
+
id,
|
|
625
|
+
version: boundedString(retriever.version, `environment.retrievers[${index}].version`, 512),
|
|
626
|
+
configuration: Object.freeze(configuration)
|
|
627
|
+
});
|
|
628
|
+
}).toSorted((left, right) => left.id.localeCompare(right.id));
|
|
629
|
+
const environmentIds = retrievers.map(({ id }) => id);
|
|
630
|
+
if (new Set(environmentIds).size !== environmentIds.length) {
|
|
631
|
+
throw new TypeError("environment.retrievers must not repeat an ID.");
|
|
632
|
+
}
|
|
633
|
+
if (environmentIds.length !== expectedRetrieverIds.length || environmentIds.some((id, index) => id !== expectedRetrieverIds[index]))
|
|
634
|
+
throw new TypeError("environment.retrievers must exactly describe the report retrievers.");
|
|
635
|
+
return Object.freeze({
|
|
636
|
+
generatedAt: environment.generatedAt,
|
|
637
|
+
runtime,
|
|
638
|
+
model,
|
|
639
|
+
cache: Object.freeze({
|
|
640
|
+
state: environment.cache.state,
|
|
641
|
+
...fingerprint === undefined ? {} : { fingerprint }
|
|
642
|
+
}),
|
|
643
|
+
retrievers: Object.freeze(retrievers)
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
function hashSeed(value) {
|
|
647
|
+
let hash = 2166136261;
|
|
648
|
+
for (const character of value) {
|
|
649
|
+
hash ^= character.codePointAt(0) ?? 0;
|
|
650
|
+
hash = Math.imul(hash, 16777619);
|
|
651
|
+
}
|
|
652
|
+
return hash >>> 0;
|
|
653
|
+
}
|
|
654
|
+
function metricValues(rows) {
|
|
655
|
+
const values = (key) => rows.flatMap(({ metrics }) => {
|
|
656
|
+
const value = metrics[key];
|
|
657
|
+
return value === null ? [] : [value];
|
|
658
|
+
});
|
|
659
|
+
return Object.freeze({
|
|
660
|
+
recall: average(values("recall")),
|
|
661
|
+
reciprocalRank: average(values("reciprocalRank")),
|
|
662
|
+
ndcg: average(values("ndcg")),
|
|
663
|
+
noAnswerAccuracy: average(values("noAnswerAccuracy"))
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
function failedMetricsFor(query) {
|
|
667
|
+
return query.answer === "no-answer" ? Object.freeze({ recall: null, reciprocalRank: null, ndcg: null, noAnswerAccuracy: 0 }) : Object.freeze({ recall: 0, reciprocalRank: 0, ndcg: 0, noAnswerAccuracy: null });
|
|
668
|
+
}
|
|
669
|
+
function buildRetrievalEvaluationReport(options) {
|
|
670
|
+
const cutoff = options.cutoff ?? 10;
|
|
671
|
+
if (!Number.isSafeInteger(cutoff) || cutoff < 1 || cutoff > MAX_EVALUATION_RESULTS_PER_QUERY) {
|
|
672
|
+
throw new RangeError(`Evaluation cutoff must be from 1 through ${MAX_EVALUATION_RESULTS_PER_QUERY}.`);
|
|
673
|
+
}
|
|
674
|
+
const queryById = new Map(options.corpus.queries.map((query) => [query.id, query]));
|
|
675
|
+
if (options.runs.length < 1)
|
|
676
|
+
throw new TypeError("Evaluation report requires at least one run.");
|
|
677
|
+
const runSplits = [...new Set(options.runs.map(({ split: split2 }) => split2))].toSorted();
|
|
678
|
+
const split = runSplits.length === 1 ? runSplits[0] ?? "all" : "all";
|
|
679
|
+
const expectedQueries = options.corpus.queries.filter((query) => split === "all" || query.split === split);
|
|
680
|
+
const expectedQueryIds = expectedQueries.map(({ id }) => id);
|
|
681
|
+
const seen = new Set;
|
|
682
|
+
const measured = options.runs.map((run) => {
|
|
683
|
+
const query = queryById.get(run.queryId);
|
|
684
|
+
if (query === undefined)
|
|
685
|
+
throw new TypeError(`Evaluation run names unknown query ${run.queryId}.`);
|
|
686
|
+
if (run.queryClass !== query.class || run.split !== query.split) {
|
|
687
|
+
throw new TypeError(`Evaluation run metadata does not match query ${run.queryId}.`);
|
|
688
|
+
}
|
|
689
|
+
if (!Number.isFinite(run.timing.elapsedMs) || run.timing.elapsedMs < 0) {
|
|
690
|
+
throw new TypeError(`Evaluation run ${run.retrieverId}/${run.queryId} has invalid elapsed time.`);
|
|
691
|
+
}
|
|
692
|
+
const backend = nonnegativeNumberMap(run.timing.backend, `Evaluation run ${run.retrieverId}/${run.queryId} backend timings`);
|
|
693
|
+
const resources = nonnegativeNumberMap(run.resources, `Evaluation run ${run.retrieverId}/${run.queryId} resources`);
|
|
694
|
+
const hits = Object.freeze(run.hits.map((hit, index) => Object.freeze({
|
|
695
|
+
...hit,
|
|
696
|
+
...hit.evidence === undefined ? {} : {
|
|
697
|
+
evidence: jsonEvidence(hit.evidence, `Evaluation run ${run.retrieverId}/${run.queryId} hits[${index}].evidence`)
|
|
698
|
+
}
|
|
699
|
+
})));
|
|
700
|
+
const diagnostics = Object.freeze(run.diagnostics.map((diagnostic) => Object.freeze({
|
|
701
|
+
...diagnostic,
|
|
702
|
+
...diagnostic.message === undefined ? {} : { message: redactEvaluationMachinePaths(diagnostic.message) }
|
|
703
|
+
})));
|
|
704
|
+
const failure = run.failure === undefined ? undefined : Object.freeze({
|
|
705
|
+
...run.failure,
|
|
706
|
+
message: redactEvaluationMachinePaths(run.failure.message)
|
|
707
|
+
});
|
|
708
|
+
const key = `${run.retrieverId}\x00${run.queryId}`;
|
|
709
|
+
if (seen.has(key))
|
|
710
|
+
throw new TypeError(`Evaluation runs repeat ${run.retrieverId}/${run.queryId}.`);
|
|
711
|
+
seen.add(key);
|
|
712
|
+
return Object.freeze({
|
|
713
|
+
...run,
|
|
714
|
+
hits,
|
|
715
|
+
diagnostics,
|
|
716
|
+
timing: Object.freeze({ elapsedMs: run.timing.elapsedMs, backend }),
|
|
717
|
+
resources,
|
|
718
|
+
...failure === undefined ? {} : { failure },
|
|
719
|
+
metrics: run.status === "failed" || run.status === "unavailable" ? failedMetricsFor(query) : metricsFor(query, hits, cutoff)
|
|
720
|
+
});
|
|
721
|
+
}).toSorted((left, right) => left.retrieverId.localeCompare(right.retrieverId) || options.corpus.queries.findIndex(({ id }) => id === left.queryId) - options.corpus.queries.findIndex(({ id }) => id === right.queryId));
|
|
722
|
+
const retrieverIds = [...new Set(measured.map(({ retrieverId }) => retrieverId))].toSorted();
|
|
723
|
+
for (const retrieverId of retrieverIds) {
|
|
724
|
+
const actualQueryIds = measured.filter((run) => run.retrieverId === retrieverId).map(({ queryId }) => queryId);
|
|
725
|
+
if (actualQueryIds.length !== expectedQueryIds.length || actualQueryIds.some((id, index) => id !== expectedQueryIds[index])) {
|
|
726
|
+
throw new TypeError(`Evaluation retriever ${retrieverId} must contain exactly the ${expectedQueryIds.length} ${split} query runs.`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const summaries = retrieverIds.map((retrieverId) => {
|
|
730
|
+
const rows = measured.filter((run) => run.retrieverId === retrieverId);
|
|
731
|
+
const classes = [...new Set(rows.map(({ queryClass }) => queryClass))].toSorted();
|
|
732
|
+
const latencies = rows.map(({ timing }) => timing.elapsedMs);
|
|
733
|
+
return Object.freeze({
|
|
734
|
+
retrieverId,
|
|
735
|
+
runs: rows.length,
|
|
736
|
+
ready: rows.filter(({ status }) => status === "ready").length,
|
|
737
|
+
degraded: rows.filter(({ status }) => status === "degraded").length,
|
|
738
|
+
unavailable: rows.filter(({ status }) => status === "unavailable").length,
|
|
739
|
+
failed: rows.filter(({ status }) => status === "failed").length,
|
|
740
|
+
metrics: metricValues(rows),
|
|
741
|
+
byClass: Object.freeze(classes.map((queryClass) => {
|
|
742
|
+
const classRows = rows.filter((row) => row.queryClass === queryClass);
|
|
743
|
+
return Object.freeze({
|
|
744
|
+
class: queryClass,
|
|
745
|
+
queries: classRows.length,
|
|
746
|
+
metrics: metricValues(classRows)
|
|
747
|
+
});
|
|
748
|
+
})),
|
|
749
|
+
latencyMs: Object.freeze({
|
|
750
|
+
p50: percentile(latencies, 0.5),
|
|
751
|
+
p90: percentile(latencies, 0.9),
|
|
752
|
+
p95: percentile(latencies, 0.95),
|
|
753
|
+
p99: percentile(latencies, 0.99)
|
|
754
|
+
})
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
const comparisons = [];
|
|
758
|
+
const baseline = options.baselineRetrieverId;
|
|
759
|
+
if (baseline !== undefined) {
|
|
760
|
+
if (!retrieverIds.includes(baseline))
|
|
761
|
+
throw new TypeError(`Unknown baseline retriever ${baseline}.`);
|
|
762
|
+
const metricNames = [
|
|
763
|
+
"recall",
|
|
764
|
+
"reciprocalRank",
|
|
765
|
+
"ndcg",
|
|
766
|
+
"noAnswerAccuracy"
|
|
767
|
+
];
|
|
768
|
+
for (const candidate of retrieverIds.filter((id) => id !== baseline)) {
|
|
769
|
+
for (const metric of metricNames) {
|
|
770
|
+
const pairs = expectedQueries.flatMap(({ id }) => {
|
|
771
|
+
const baselineRow = measured.find((row) => row.retrieverId === baseline && row.queryId === id);
|
|
772
|
+
const candidateRow = measured.find((row) => row.retrieverId === candidate && row.queryId === id);
|
|
773
|
+
const baselineValue = baselineRow?.metrics[metric];
|
|
774
|
+
const candidateValue = candidateRow?.metrics[metric];
|
|
775
|
+
return baselineValue === null || baselineValue === undefined || candidateValue === null || candidateValue === undefined ? [] : [{ baseline: baselineValue, candidate: candidateValue }];
|
|
776
|
+
});
|
|
777
|
+
if (pairs.length === 0)
|
|
778
|
+
continue;
|
|
779
|
+
const seed = ((options.bootstrapSeed ?? 1) ^ hashSeed(`${candidate}\x00${metric}`)) >>> 0;
|
|
780
|
+
comparisons.push(Object.freeze({
|
|
781
|
+
baselineRetrieverId: baseline,
|
|
782
|
+
candidateRetrieverId: candidate,
|
|
783
|
+
metric,
|
|
784
|
+
interval: pairedBootstrapConfidenceInterval(pairs, {
|
|
785
|
+
seed,
|
|
786
|
+
...options.bootstrapResamples === undefined ? {} : { resamples: options.bootstrapResamples }
|
|
787
|
+
})
|
|
788
|
+
}));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return Object.freeze({
|
|
793
|
+
schemaVersion: 1,
|
|
794
|
+
corpus: options.corpus,
|
|
795
|
+
environment: validateEnvironment(options.environment, retrieverIds),
|
|
796
|
+
split,
|
|
797
|
+
queryCount: expectedQueries.length,
|
|
798
|
+
cutoff,
|
|
799
|
+
summaries: Object.freeze(summaries),
|
|
800
|
+
comparisons: Object.freeze(comparisons),
|
|
801
|
+
runs: Object.freeze(measured)
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export { redactEvaluationMachinePaths, RETRIEVAL_EVALUATION_SCHEMA_VERSION, RETRIEVAL_EVALUATION_REPORT_VERSION, MAX_EVALUATION_QUERIES, MAX_EVALUATION_QRELS_PER_QUERY, MAX_EVALUATION_RETRIEVERS, MAX_EVALUATION_RESULTS_PER_QUERY, MAX_EVALUATION_TEXT_BYTES, MAX_EVALUATION_EVIDENCE_BYTES, MAX_EVALUATION_DIAGNOSTICS, MAX_EVALUATION_TIMEOUT_MS, MAX_BOOTSTRAP_RESAMPLES, parseRetrievalEvaluationCorpus, runRetrievalEvaluation, pairedBootstrapConfidenceInterval, buildRetrievalEvaluationReport };
|