@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,1462 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import {
|
|
5
|
+
lstat,
|
|
6
|
+
mkdir,
|
|
7
|
+
mkdtemp,
|
|
8
|
+
open,
|
|
9
|
+
readFile,
|
|
10
|
+
readdir,
|
|
11
|
+
realpath,
|
|
12
|
+
rm,
|
|
13
|
+
stat,
|
|
14
|
+
writeFile,
|
|
15
|
+
} from "node:fs/promises";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
verifyFrozenEvaluationSnapshot,
|
|
22
|
+
type VerifyFrozenEvaluationSnapshotOptions,
|
|
23
|
+
} from "./evaluation-kb.js";
|
|
24
|
+
import type {
|
|
25
|
+
EvaluationRetrieverOperationMeasurementV2,
|
|
26
|
+
KnowledgeBaseEvaluationRunnerV2Dependencies,
|
|
27
|
+
} from "./evaluation-kb-runner-v2.js";
|
|
28
|
+
import {
|
|
29
|
+
evaluationRetrieverDescriptorDigestV2,
|
|
30
|
+
type EvaluationMeasurementProfileV2,
|
|
31
|
+
type EvaluationResourceAccountingV2,
|
|
32
|
+
type EvaluationRetrieverDescriptorV2,
|
|
33
|
+
type EvaluationRetrieverTraceV2,
|
|
34
|
+
type RetrievalEvaluationCorpusV2,
|
|
35
|
+
} from "./evaluation-v2.js";
|
|
36
|
+
import {
|
|
37
|
+
indexSemanticVault,
|
|
38
|
+
recommendedEmbeddingModel,
|
|
39
|
+
recommendedEmbeddingModelSha256,
|
|
40
|
+
type SemanticIndexResult,
|
|
41
|
+
} from "./semantic.js";
|
|
42
|
+
import { scanVault, type VaultSnapshot } from "./vault.js";
|
|
43
|
+
|
|
44
|
+
const CHILD_ARGUMENT = "--kb-evaluation-measurement-child-v2";
|
|
45
|
+
const PROTOCOL = "kb-evaluation-measurement-child-v2";
|
|
46
|
+
const PROTOCOL_VERSION = 1;
|
|
47
|
+
const WORK_DIRECTORY_PREFIX = "hraness-kb-evaluation-measurement-";
|
|
48
|
+
const PREPARATION_MARKER = ".incremental-prepared-v2.json";
|
|
49
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60_000;
|
|
50
|
+
const MAX_TIMEOUT_MS = 30 * 60_000;
|
|
51
|
+
const MAX_PROTOCOL_BYTES = 64 * 1_024;
|
|
52
|
+
const MAX_STDERR_BYTES = 256 * 1_024;
|
|
53
|
+
const MAX_CACHE_ENTRIES = 100_000;
|
|
54
|
+
const MAX_MUTATION_BYTES = 64 * 1_024;
|
|
55
|
+
const MAX_PATH_BYTES = 4 * 1_024;
|
|
56
|
+
|
|
57
|
+
const qmdLanes = new Set(["hybrid", "keyword", "semantic"]);
|
|
58
|
+
const sha256Pattern = /^[0-9a-f]{64}$/u;
|
|
59
|
+
const objectIdPattern = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/u;
|
|
60
|
+
const requestIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
61
|
+
|
|
62
|
+
export type EvaluationMeasurementChildPhaseV2 =
|
|
63
|
+
| "cold-index"
|
|
64
|
+
| "incremental-prepare"
|
|
65
|
+
| "incremental-update";
|
|
66
|
+
|
|
67
|
+
export type EvaluationIncrementalMutationV2 = Readonly<{
|
|
68
|
+
/** Vault-relative Markdown note changed between the prepared and measured generations. */
|
|
69
|
+
readonly sourcePath: string;
|
|
70
|
+
/** Exact text appended to sourcePath. It must start and end with a line feed. */
|
|
71
|
+
readonly appendText: string;
|
|
72
|
+
/** SHA-256 of the complete UTF-8 document after the append is applied. */
|
|
73
|
+
readonly expectedPostMutationSha256: string;
|
|
74
|
+
/** Digest returned by evaluationIncrementalMutationSha256V2. */
|
|
75
|
+
readonly sha256: string;
|
|
76
|
+
}>;
|
|
77
|
+
|
|
78
|
+
export type EvaluationMeasurementChildProcessRequestV2 = Readonly<{
|
|
79
|
+
readonly command: readonly string[];
|
|
80
|
+
readonly cwd: string;
|
|
81
|
+
readonly environment: Readonly<Record<string, string>>;
|
|
82
|
+
readonly stdin: Uint8Array;
|
|
83
|
+
readonly timeoutMs: number;
|
|
84
|
+
readonly maxStdoutBytes: number;
|
|
85
|
+
readonly maxStderrBytes: number;
|
|
86
|
+
readonly signal: AbortSignal;
|
|
87
|
+
}>;
|
|
88
|
+
|
|
89
|
+
export type EvaluationMeasurementChildProcessResultV2 = Readonly<{
|
|
90
|
+
readonly termination: "aborted" | "exit" | "timeout";
|
|
91
|
+
readonly exitCode: number | null;
|
|
92
|
+
readonly stdout: Uint8Array;
|
|
93
|
+
readonly stderr: Uint8Array;
|
|
94
|
+
}>;
|
|
95
|
+
|
|
96
|
+
export type EvaluationMeasurementChildProcessFactoryV2 = (
|
|
97
|
+
request: EvaluationMeasurementChildProcessRequestV2,
|
|
98
|
+
) => Promise<EvaluationMeasurementChildProcessResultV2>;
|
|
99
|
+
|
|
100
|
+
export type CreateKnowledgeBaseEvaluationRetrieverOperationMeasurerV2Options = Readonly<{
|
|
101
|
+
readonly repository: string;
|
|
102
|
+
readonly root: string;
|
|
103
|
+
readonly embeddingModelFile: string;
|
|
104
|
+
readonly mutation: EvaluationIncrementalMutationV2;
|
|
105
|
+
readonly timeoutMs?: number;
|
|
106
|
+
readonly temporaryDirectory?: string;
|
|
107
|
+
readonly childCommand?: readonly string[];
|
|
108
|
+
readonly childProcessFactory?: EvaluationMeasurementChildProcessFactoryV2;
|
|
109
|
+
}>;
|
|
110
|
+
|
|
111
|
+
type FrozenSnapshotRequest = RetrievalEvaluationCorpusV2["frozen"];
|
|
112
|
+
|
|
113
|
+
export type EvaluationMeasurementChildRequestV2 = Readonly<{
|
|
114
|
+
readonly protocol: typeof PROTOCOL;
|
|
115
|
+
readonly version: typeof PROTOCOL_VERSION;
|
|
116
|
+
readonly kind: "request";
|
|
117
|
+
readonly requestId: string;
|
|
118
|
+
readonly phase: EvaluationMeasurementChildPhaseV2;
|
|
119
|
+
readonly repository: string;
|
|
120
|
+
readonly root: string;
|
|
121
|
+
readonly frozen: FrozenSnapshotRequest;
|
|
122
|
+
readonly embeddingModelFile: string;
|
|
123
|
+
readonly workRoot: string;
|
|
124
|
+
readonly mutation?: EvaluationIncrementalMutationV2;
|
|
125
|
+
}>;
|
|
126
|
+
|
|
127
|
+
type MeasuredSemanticIndex = Readonly<{
|
|
128
|
+
readonly model: string;
|
|
129
|
+
readonly documentCount: number;
|
|
130
|
+
readonly update: SemanticIndexResult["update"];
|
|
131
|
+
readonly embedding: SemanticIndexResult["embedding"];
|
|
132
|
+
}>;
|
|
133
|
+
|
|
134
|
+
export type EvaluationMeasurementChildResponseV2 = Readonly<{
|
|
135
|
+
readonly protocol: typeof PROTOCOL;
|
|
136
|
+
readonly version: typeof PROTOCOL_VERSION;
|
|
137
|
+
readonly kind: "response";
|
|
138
|
+
readonly requestId: string;
|
|
139
|
+
readonly phase: EvaluationMeasurementChildPhaseV2;
|
|
140
|
+
readonly elapsedMs: number;
|
|
141
|
+
readonly index: MeasuredSemanticIndex;
|
|
142
|
+
readonly resources: EvaluationResourceAccountingV2;
|
|
143
|
+
}>;
|
|
144
|
+
|
|
145
|
+
type ChildVerificationRequest = Readonly<{
|
|
146
|
+
readonly repository: string;
|
|
147
|
+
readonly root: string;
|
|
148
|
+
readonly frozen: FrozenSnapshotRequest;
|
|
149
|
+
}>;
|
|
150
|
+
|
|
151
|
+
export type EvaluationMeasurementChildV2Dependencies = Readonly<{
|
|
152
|
+
readonly verifyFrozenSnapshot?: (request: ChildVerificationRequest) => Promise<void>;
|
|
153
|
+
readonly scanVault?: (root: string) => Promise<Pick<VaultSnapshot, "notes">>;
|
|
154
|
+
readonly indexSemanticVault?: typeof indexSemanticVault;
|
|
155
|
+
readonly measureCacheBytes?: (root: string) => Promise<number>;
|
|
156
|
+
readonly peakRssBytes?: () => number;
|
|
157
|
+
readonly now?: () => number;
|
|
158
|
+
readonly environment?: Readonly<Record<string, string | undefined>>;
|
|
159
|
+
}>;
|
|
160
|
+
|
|
161
|
+
type WorkIdentities = Readonly<{
|
|
162
|
+
readonly workRoot: string;
|
|
163
|
+
readonly vault: string;
|
|
164
|
+
readonly cache: string;
|
|
165
|
+
readonly database: string;
|
|
166
|
+
readonly xdgCache: string;
|
|
167
|
+
readonly marker: string;
|
|
168
|
+
readonly forbiddenGenerateModel: string;
|
|
169
|
+
readonly forbiddenRerankModel: string;
|
|
170
|
+
}>;
|
|
171
|
+
|
|
172
|
+
type PreparationMarker = Readonly<{
|
|
173
|
+
readonly protocol: typeof PROTOCOL;
|
|
174
|
+
readonly version: typeof PROTOCOL_VERSION;
|
|
175
|
+
readonly kind: "incremental-prepared";
|
|
176
|
+
readonly requestId: string;
|
|
177
|
+
readonly snapshotSha256: string;
|
|
178
|
+
readonly mutationSha256: string;
|
|
179
|
+
readonly sourcePath: string;
|
|
180
|
+
readonly documentCount: number;
|
|
181
|
+
readonly cacheManifestSha256: string;
|
|
182
|
+
readonly vaultManifestSha256: string;
|
|
183
|
+
}>;
|
|
184
|
+
|
|
185
|
+
function record(value: unknown, label: string): Readonly<Record<string, unknown>> {
|
|
186
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
187
|
+
throw new TypeError(`${label} must be an object.`);
|
|
188
|
+
}
|
|
189
|
+
return value as Readonly<Record<string, unknown>>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function exactKeys(
|
|
193
|
+
value: Readonly<Record<string, unknown>>,
|
|
194
|
+
required: readonly string[],
|
|
195
|
+
optional: readonly string[],
|
|
196
|
+
label: string,
|
|
197
|
+
): void {
|
|
198
|
+
const allowed = new Set([...required, ...optional]);
|
|
199
|
+
const actual = Object.keys(value);
|
|
200
|
+
const missing = required.filter((key) => !(key in value));
|
|
201
|
+
const extra = actual.filter((key) => !allowed.has(key));
|
|
202
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
203
|
+
throw new TypeError(`${label} has an invalid field set.`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function boundedString(value: unknown, label: string, maximumBytes = MAX_PATH_BYTES): string {
|
|
208
|
+
if (
|
|
209
|
+
typeof value !== "string"
|
|
210
|
+
|| value.length === 0
|
|
211
|
+
|| /[\0\r\n]/u.test(value)
|
|
212
|
+
|| Buffer.byteLength(value, "utf8") > maximumBytes
|
|
213
|
+
) {
|
|
214
|
+
throw new TypeError(`${label} must be a bounded non-empty string.`);
|
|
215
|
+
}
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function absolutePath(value: unknown, label: string): string {
|
|
220
|
+
const path = boundedString(value, label);
|
|
221
|
+
if (!isAbsolute(path) || resolve(path) !== path) {
|
|
222
|
+
throw new TypeError(`${label} must be an absolute normalized path.`);
|
|
223
|
+
}
|
|
224
|
+
return path;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function nonnegativeInteger(value: unknown, label: string): number {
|
|
228
|
+
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
|
229
|
+
throw new TypeError(`${label} must be a non-negative safe integer.`);
|
|
230
|
+
}
|
|
231
|
+
return value as number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function positiveInteger(value: unknown, label: string): number {
|
|
235
|
+
const parsed = nonnegativeInteger(value, label);
|
|
236
|
+
if (parsed === 0) throw new TypeError(`${label} must be positive.`);
|
|
237
|
+
return parsed;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function nonnegativeNumber(value: unknown, label: string): number {
|
|
241
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
242
|
+
throw new TypeError(`${label} must be non-negative and finite.`);
|
|
243
|
+
}
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function confinedPath(root: string, candidate: string): boolean {
|
|
248
|
+
const fromRoot = relative(root, candidate);
|
|
249
|
+
return fromRoot === ""
|
|
250
|
+
|| (fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function notePath(value: unknown, label: string): string {
|
|
254
|
+
const path = boundedString(value, label);
|
|
255
|
+
if (
|
|
256
|
+
path !== path.normalize("NFC")
|
|
257
|
+
|| path.startsWith("/")
|
|
258
|
+
|| path.includes("\\")
|
|
259
|
+
|| path.split("/").some((part) => part === "" || part === "." || part === "..")
|
|
260
|
+
|| !path.endsWith(".md")
|
|
261
|
+
) {
|
|
262
|
+
throw new TypeError(`${label} must be a confined NFC Markdown path.`);
|
|
263
|
+
}
|
|
264
|
+
return path;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Digest the exact one-note transition named by an incremental measurement. */
|
|
268
|
+
export function evaluationIncrementalMutationSha256V2(
|
|
269
|
+
mutation: Pick<
|
|
270
|
+
EvaluationIncrementalMutationV2,
|
|
271
|
+
"appendText" | "expectedPostMutationSha256" | "sourcePath"
|
|
272
|
+
>,
|
|
273
|
+
): string {
|
|
274
|
+
return createHash("sha256")
|
|
275
|
+
.update("kb-evaluation-incremental-mutation-v2\0")
|
|
276
|
+
.update(mutation.sourcePath)
|
|
277
|
+
.update("\0")
|
|
278
|
+
.update(mutation.appendText)
|
|
279
|
+
.update("\0")
|
|
280
|
+
.update(mutation.expectedPostMutationSha256)
|
|
281
|
+
.digest("hex");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function parseMutation(value: unknown, label: string): EvaluationIncrementalMutationV2 {
|
|
285
|
+
const input = record(value, label);
|
|
286
|
+
exactKeys(
|
|
287
|
+
input,
|
|
288
|
+
["appendText", "expectedPostMutationSha256", "sha256", "sourcePath"],
|
|
289
|
+
[],
|
|
290
|
+
label,
|
|
291
|
+
);
|
|
292
|
+
const sourcePath = notePath(input.sourcePath, `${label}.sourcePath`);
|
|
293
|
+
if (sourcePath === "index.md" || basename(sourcePath) === "AGENTS.md") {
|
|
294
|
+
throw new TypeError(`${label}.sourcePath must identify an indexed content note.`);
|
|
295
|
+
}
|
|
296
|
+
if (
|
|
297
|
+
typeof input.appendText !== "string"
|
|
298
|
+
|| !input.appendText.startsWith("\n")
|
|
299
|
+
|| !input.appendText.endsWith("\n")
|
|
300
|
+
|| input.appendText.includes("\0")
|
|
301
|
+
|| input.appendText.includes("\r")
|
|
302
|
+
|| Buffer.byteLength(input.appendText, "utf8") > MAX_MUTATION_BYTES
|
|
303
|
+
) {
|
|
304
|
+
throw new TypeError(`${label}.appendText must be bounded LF-delimited UTF-8 text.`);
|
|
305
|
+
}
|
|
306
|
+
if (typeof input.sha256 !== "string" || !sha256Pattern.test(input.sha256)) {
|
|
307
|
+
throw new TypeError(`${label}.sha256 must be a lowercase SHA-256 digest.`);
|
|
308
|
+
}
|
|
309
|
+
if (
|
|
310
|
+
typeof input.expectedPostMutationSha256 !== "string"
|
|
311
|
+
|| !sha256Pattern.test(input.expectedPostMutationSha256)
|
|
312
|
+
) {
|
|
313
|
+
throw new TypeError(`${label}.expectedPostMutationSha256 must be a lowercase SHA-256 digest.`);
|
|
314
|
+
}
|
|
315
|
+
const parsed = Object.freeze({
|
|
316
|
+
sourcePath,
|
|
317
|
+
appendText: input.appendText,
|
|
318
|
+
expectedPostMutationSha256: input.expectedPostMutationSha256,
|
|
319
|
+
sha256: input.sha256,
|
|
320
|
+
});
|
|
321
|
+
if (evaluationIncrementalMutationSha256V2(parsed) !== parsed.sha256) {
|
|
322
|
+
throw new TypeError(`${label}.sha256 does not bind the declared mutation.`);
|
|
323
|
+
}
|
|
324
|
+
return parsed;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function parseFrozen(value: unknown, label: string): FrozenSnapshotRequest {
|
|
328
|
+
const input = record(value, label);
|
|
329
|
+
exactKeys(input, ["repositoryCommit", "vaultRoot", "vaultTree"], [], label);
|
|
330
|
+
if (typeof input.repositoryCommit !== "string" || !objectIdPattern.test(input.repositoryCommit)) {
|
|
331
|
+
throw new TypeError(`${label}.repositoryCommit is invalid.`);
|
|
332
|
+
}
|
|
333
|
+
if (typeof input.vaultTree !== "string" || !objectIdPattern.test(input.vaultTree)) {
|
|
334
|
+
throw new TypeError(`${label}.vaultTree is invalid.`);
|
|
335
|
+
}
|
|
336
|
+
if (
|
|
337
|
+
typeof input.vaultRoot !== "string"
|
|
338
|
+
|| input.vaultRoot.length === 0
|
|
339
|
+
|| /[\0\r\n]/u.test(input.vaultRoot)
|
|
340
|
+
|| input.vaultRoot !== input.vaultRoot.normalize("NFC")
|
|
341
|
+
|| input.vaultRoot.startsWith("/")
|
|
342
|
+
|| input.vaultRoot.includes("\\")
|
|
343
|
+
|| (input.vaultRoot !== "."
|
|
344
|
+
&& input.vaultRoot.split("/").some((part) => part === "" || part === "." || part === ".."))
|
|
345
|
+
|| Buffer.byteLength(input.vaultRoot, "utf8") > MAX_PATH_BYTES
|
|
346
|
+
) {
|
|
347
|
+
throw new TypeError(`${label}.vaultRoot must be a confined NFC path.`);
|
|
348
|
+
}
|
|
349
|
+
const vaultRoot = input.vaultRoot;
|
|
350
|
+
return Object.freeze({
|
|
351
|
+
repositoryCommit: input.repositoryCommit,
|
|
352
|
+
vaultTree: input.vaultTree,
|
|
353
|
+
vaultRoot,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function parsePhase(value: unknown): EvaluationMeasurementChildPhaseV2 {
|
|
358
|
+
if (value !== "cold-index" && value !== "incremental-prepare" && value !== "incremental-update") {
|
|
359
|
+
throw new TypeError("measurement child phase is invalid.");
|
|
360
|
+
}
|
|
361
|
+
return value;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function parseChildRequest(value: unknown): EvaluationMeasurementChildRequestV2 {
|
|
365
|
+
const input = record(value, "measurement child request");
|
|
366
|
+
exactKeys(input, [
|
|
367
|
+
"embeddingModelFile",
|
|
368
|
+
"frozen",
|
|
369
|
+
"kind",
|
|
370
|
+
"phase",
|
|
371
|
+
"protocol",
|
|
372
|
+
"repository",
|
|
373
|
+
"requestId",
|
|
374
|
+
"root",
|
|
375
|
+
"version",
|
|
376
|
+
"workRoot",
|
|
377
|
+
], ["mutation"], "measurement child request");
|
|
378
|
+
if (input.protocol !== PROTOCOL || input.version !== PROTOCOL_VERSION || input.kind !== "request") {
|
|
379
|
+
throw new TypeError("measurement child protocol identity is invalid.");
|
|
380
|
+
}
|
|
381
|
+
if (typeof input.requestId !== "string" || !requestIdPattern.test(input.requestId)) {
|
|
382
|
+
throw new TypeError("measurement child requestId is invalid.");
|
|
383
|
+
}
|
|
384
|
+
const phase = parsePhase(input.phase);
|
|
385
|
+
const mutation = input.mutation === undefined ? undefined : parseMutation(input.mutation, "mutation");
|
|
386
|
+
if ((phase === "cold-index") !== (mutation === undefined)) {
|
|
387
|
+
throw new TypeError("measurement child mutation presence does not match its phase.");
|
|
388
|
+
}
|
|
389
|
+
return Object.freeze({
|
|
390
|
+
protocol: PROTOCOL,
|
|
391
|
+
version: PROTOCOL_VERSION,
|
|
392
|
+
kind: "request",
|
|
393
|
+
requestId: input.requestId,
|
|
394
|
+
phase,
|
|
395
|
+
repository: absolutePath(input.repository, "repository"),
|
|
396
|
+
root: absolutePath(input.root, "root"),
|
|
397
|
+
frozen: parseFrozen(input.frozen, "frozen"),
|
|
398
|
+
embeddingModelFile: absolutePath(input.embeddingModelFile, "embeddingModelFile"),
|
|
399
|
+
workRoot: absolutePath(input.workRoot, "workRoot"),
|
|
400
|
+
...(mutation === undefined ? {} : { mutation }),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function workIdentities(workRoot: string): WorkIdentities {
|
|
405
|
+
return Object.freeze({
|
|
406
|
+
workRoot,
|
|
407
|
+
vault: join(workRoot, "vault"),
|
|
408
|
+
cache: join(workRoot, "cache"),
|
|
409
|
+
database: join(workRoot, "cache", "qmd.sqlite"),
|
|
410
|
+
xdgCache: join(workRoot, "cache", "xdg"),
|
|
411
|
+
marker: join(workRoot, PREPARATION_MARKER),
|
|
412
|
+
forbiddenGenerateModel: join(workRoot, ".generative-llm-forbidden.gguf"),
|
|
413
|
+
forbiddenRerankModel: join(workRoot, ".reranker-llm-forbidden.gguf"),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function validatedWorkIdentities(workRoot: string): Promise<WorkIdentities> {
|
|
418
|
+
const metadata = await lstat(workRoot);
|
|
419
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
420
|
+
throw new TypeError("measurement work root must be a real directory.");
|
|
421
|
+
}
|
|
422
|
+
const canonical = await realpath(workRoot);
|
|
423
|
+
if (canonical !== workRoot || !basename(workRoot).startsWith(WORK_DIRECTORY_PREFIX)) {
|
|
424
|
+
throw new TypeError("measurement work root identity is invalid.");
|
|
425
|
+
}
|
|
426
|
+
return workIdentities(workRoot);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function validateIsolatedDirectory(path: string, label: string): Promise<void> {
|
|
430
|
+
const metadata = await lstat(path);
|
|
431
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory() || await realpath(path) !== path) {
|
|
432
|
+
throw new TypeError(`${label} must be a canonical non-symlink directory.`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function validateWorkLayout(
|
|
437
|
+
identities: WorkIdentities,
|
|
438
|
+
phase: EvaluationMeasurementChildPhaseV2,
|
|
439
|
+
): Promise<void> {
|
|
440
|
+
const names = (await readdir(identities.workRoot)).toSorted();
|
|
441
|
+
if (phase === "cold-index" || phase === "incremental-prepare") {
|
|
442
|
+
if (names.length !== 0) {
|
|
443
|
+
throw new TypeError("cold measurement work root must be empty.");
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const expected = [PREPARATION_MARKER, "cache", "vault"].toSorted();
|
|
448
|
+
if (names.length !== expected.length || names.some((name, index) => name !== expected[index])) {
|
|
449
|
+
throw new TypeError("incremental measurement work root has an invalid layout.");
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function fileErrorCode(error: unknown): string | undefined {
|
|
454
|
+
if (error === null || typeof error !== "object" || !("code" in error)) return undefined;
|
|
455
|
+
return typeof error.code === "string" ? error.code : undefined;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function requireAbsent(path: string, label: string): Promise<void> {
|
|
459
|
+
try {
|
|
460
|
+
await lstat(path);
|
|
461
|
+
} catch (error: unknown) {
|
|
462
|
+
if (fileErrorCode(error) === "ENOENT") return;
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
throw new TypeError(`${label} must remain absent.`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function snapshotSha256(frozen: FrozenSnapshotRequest): string {
|
|
469
|
+
return createHash("sha256")
|
|
470
|
+
.update(`${frozen.repositoryCommit}\0${frozen.vaultTree}\0${frozen.vaultRoot}`)
|
|
471
|
+
.digest("hex");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function vaultManifestSha256(notes: readonly { readonly path: string; readonly content: string }[]): string {
|
|
475
|
+
const hash = createHash("sha256").update("kb-evaluation-materialized-vault-v2\0");
|
|
476
|
+
for (const note of notes.toSorted((left, right) => left.path.localeCompare(right.path))) {
|
|
477
|
+
hash.update(note.path).update("\0");
|
|
478
|
+
hash.update(createHash("sha256").update(note.content).digest("hex")).update("\0");
|
|
479
|
+
}
|
|
480
|
+
return hash.digest("hex");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function indexedDocumentCount(notes: readonly { readonly path: string }[]): number {
|
|
484
|
+
return notes.filter(({ path }) => path !== "index.md" && basename(path) !== "AGENTS.md").length;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function materializeVault(
|
|
488
|
+
root: string,
|
|
489
|
+
notes: readonly { readonly path: string; readonly content: string }[],
|
|
490
|
+
): Promise<void> {
|
|
491
|
+
await mkdir(root, { mode: 0o700 });
|
|
492
|
+
const seen = new Set<string>();
|
|
493
|
+
for (const note of notes.toSorted((left, right) => left.path.localeCompare(right.path))) {
|
|
494
|
+
const path = notePath(note.path, "snapshot note path");
|
|
495
|
+
if (seen.has(path)) throw new TypeError("snapshot note paths must be unique.");
|
|
496
|
+
seen.add(path);
|
|
497
|
+
const destination = resolve(root, path);
|
|
498
|
+
if (!confinedPath(root, destination) || destination === root) {
|
|
499
|
+
throw new TypeError("snapshot note escaped the materialized vault.");
|
|
500
|
+
}
|
|
501
|
+
await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
|
|
502
|
+
await writeFile(destination, note.content, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function validateIndexResult(
|
|
507
|
+
result: SemanticIndexResult,
|
|
508
|
+
phase: EvaluationMeasurementChildPhaseV2,
|
|
509
|
+
documentCount: number,
|
|
510
|
+
): void {
|
|
511
|
+
if (result.model !== recommendedEmbeddingModel) {
|
|
512
|
+
throw new TypeError("semantic indexing returned the wrong model identity.");
|
|
513
|
+
}
|
|
514
|
+
const update = result.update;
|
|
515
|
+
for (const [key, value] of Object.entries(update)) nonnegativeInteger(value, `semantic update.${key}`);
|
|
516
|
+
if (update.collections !== 1 || update.removed !== 0 || result.embedding === null) {
|
|
517
|
+
throw new TypeError("semantic indexing did not satisfy the measured generation invariant.");
|
|
518
|
+
}
|
|
519
|
+
const embedding = result.embedding;
|
|
520
|
+
nonnegativeInteger(embedding.docsProcessed, "semantic embedding.docsProcessed");
|
|
521
|
+
nonnegativeInteger(embedding.chunksEmbedded, "semantic embedding.chunksEmbedded");
|
|
522
|
+
nonnegativeInteger(embedding.errors, "semantic embedding.errors");
|
|
523
|
+
nonnegativeNumber(embedding.durationMs, "semantic embedding.durationMs");
|
|
524
|
+
if (
|
|
525
|
+
embedding.errors !== 0
|
|
526
|
+
|| (embedding.failures?.length ?? 0) !== 0
|
|
527
|
+
|| embedding.chunksEmbedded < embedding.docsProcessed
|
|
528
|
+
) {
|
|
529
|
+
throw new TypeError("semantic embedding did not complete cleanly.");
|
|
530
|
+
}
|
|
531
|
+
if (phase === "cold-index" || phase === "incremental-prepare") {
|
|
532
|
+
if (
|
|
533
|
+
documentCount < 1
|
|
534
|
+
|| update.indexed !== documentCount
|
|
535
|
+
|| update.updated !== 0
|
|
536
|
+
|| update.unchanged !== 0
|
|
537
|
+
|| update.needsEmbedding !== documentCount
|
|
538
|
+
|| embedding.docsProcessed !== documentCount
|
|
539
|
+
) {
|
|
540
|
+
throw new TypeError("cold semantic indexing counts violate the exact corpus invariant.");
|
|
541
|
+
}
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (
|
|
545
|
+
update.indexed !== 0
|
|
546
|
+
|| update.updated !== 1
|
|
547
|
+
|| update.unchanged !== documentCount - 1
|
|
548
|
+
|| update.needsEmbedding !== 1
|
|
549
|
+
|| embedding.docsProcessed !== 1
|
|
550
|
+
) {
|
|
551
|
+
throw new TypeError("incremental semantic indexing counts violate the exact one-note invariant.");
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function defaultPeakRssBytes(): number {
|
|
556
|
+
const current = process.memoryUsage().rss;
|
|
557
|
+
const reported = process.resourceUsage().maxRSS;
|
|
558
|
+
if (!Number.isSafeInteger(current) || current < 1 || !Number.isSafeInteger(reported) || reported < 1) {
|
|
559
|
+
throw new TypeError("runtime peak RSS accounting is unavailable.");
|
|
560
|
+
}
|
|
561
|
+
const normalized = reported >= current ? reported : reported * 1_024;
|
|
562
|
+
if (!Number.isSafeInteger(normalized) || normalized < current) {
|
|
563
|
+
throw new TypeError("runtime peak RSS accounting is invalid.");
|
|
564
|
+
}
|
|
565
|
+
return normalized;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export type EvaluationCacheManifestV2 = Readonly<{
|
|
569
|
+
readonly bytes: number;
|
|
570
|
+
readonly sha256: string;
|
|
571
|
+
}>;
|
|
572
|
+
|
|
573
|
+
async function digestEvaluationCacheFile(path: string): Promise<Readonly<{
|
|
574
|
+
readonly bytes: number;
|
|
575
|
+
readonly sha256: string;
|
|
576
|
+
}>> {
|
|
577
|
+
const pathBefore = await lstat(path);
|
|
578
|
+
if (pathBefore.isSymbolicLink() || !pathBefore.isFile() || pathBefore.nlink !== 1) {
|
|
579
|
+
throw new TypeError("evaluation cache files must be regular and singly linked.");
|
|
580
|
+
}
|
|
581
|
+
if (!Number.isSafeInteger(pathBefore.size) || pathBefore.size < 0) {
|
|
582
|
+
throw new RangeError("evaluation cache file size is invalid.");
|
|
583
|
+
}
|
|
584
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
585
|
+
try {
|
|
586
|
+
const before = await handle.stat();
|
|
587
|
+
if (
|
|
588
|
+
!before.isFile()
|
|
589
|
+
|| before.nlink !== 1
|
|
590
|
+
|| before.dev !== pathBefore.dev
|
|
591
|
+
|| before.ino !== pathBefore.ino
|
|
592
|
+
|| before.size !== pathBefore.size
|
|
593
|
+
) {
|
|
594
|
+
throw new TypeError("evaluation cache file identity changed before it was read.");
|
|
595
|
+
}
|
|
596
|
+
const hash = createHash("sha256");
|
|
597
|
+
const buffer = new Uint8Array(1_024 * 1_024);
|
|
598
|
+
let observed = 0;
|
|
599
|
+
for (;;) {
|
|
600
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, null);
|
|
601
|
+
if (bytesRead === 0) break;
|
|
602
|
+
observed += bytesRead;
|
|
603
|
+
if (!Number.isSafeInteger(observed)) {
|
|
604
|
+
throw new RangeError("evaluation cache file byte count overflowed.");
|
|
605
|
+
}
|
|
606
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
607
|
+
}
|
|
608
|
+
const after = await handle.stat();
|
|
609
|
+
const pathAfter = await lstat(path);
|
|
610
|
+
if (
|
|
611
|
+
!after.isFile()
|
|
612
|
+
|| after.nlink !== 1
|
|
613
|
+
|| after.dev !== before.dev
|
|
614
|
+
|| after.ino !== before.ino
|
|
615
|
+
|| after.size !== before.size
|
|
616
|
+
|| after.mtimeMs !== before.mtimeMs
|
|
617
|
+
|| observed !== before.size
|
|
618
|
+
|| pathAfter.isSymbolicLink()
|
|
619
|
+
|| !pathAfter.isFile()
|
|
620
|
+
|| pathAfter.nlink !== 1
|
|
621
|
+
|| pathAfter.dev !== before.dev
|
|
622
|
+
|| pathAfter.ino !== before.ino
|
|
623
|
+
|| pathAfter.size !== before.size
|
|
624
|
+
) {
|
|
625
|
+
throw new Error("evaluation cache file changed while it was measured.");
|
|
626
|
+
}
|
|
627
|
+
return Object.freeze({ bytes: observed, sha256: hash.digest("hex") });
|
|
628
|
+
} finally {
|
|
629
|
+
await handle.close();
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** Hash one complete isolated cache tree without following aliases or special files. */
|
|
634
|
+
export async function measureEvaluationCacheManifestV2(
|
|
635
|
+
root: string,
|
|
636
|
+
): Promise<EvaluationCacheManifestV2> {
|
|
637
|
+
const requestedRoot = resolve(root);
|
|
638
|
+
const rootMetadata = await lstat(requestedRoot);
|
|
639
|
+
if (rootMetadata.isSymbolicLink() || !rootMetadata.isDirectory()) {
|
|
640
|
+
throw new TypeError("evaluation cache root must be a non-symlink directory.");
|
|
641
|
+
}
|
|
642
|
+
const canonicalRoot = await realpath(requestedRoot);
|
|
643
|
+
const entries: { readonly kind: "directory" | "file"; readonly path: string }[] = [];
|
|
644
|
+
let bytes = 0;
|
|
645
|
+
const pending = [canonicalRoot];
|
|
646
|
+
while (pending.length > 0) {
|
|
647
|
+
const directory = pending.pop();
|
|
648
|
+
if (directory === undefined) break;
|
|
649
|
+
const children = await readdir(directory, { withFileTypes: true });
|
|
650
|
+
for (const child of children.toSorted((left, right) => left.name.localeCompare(right.name))) {
|
|
651
|
+
if (entries.length >= MAX_CACHE_ENTRIES) throw new RangeError("evaluation cache has too many entries.");
|
|
652
|
+
const path = join(directory, child.name);
|
|
653
|
+
const metadata = await lstat(path);
|
|
654
|
+
if (metadata.isSymbolicLink()) throw new TypeError("evaluation cache must not contain symbolic links.");
|
|
655
|
+
if (metadata.isDirectory()) {
|
|
656
|
+
entries.push({ kind: "directory", path });
|
|
657
|
+
pending.push(path);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
if (!metadata.isFile()) throw new TypeError("evaluation cache must contain only regular files.");
|
|
661
|
+
if (metadata.nlink !== 1) throw new TypeError("evaluation cache files must be singly linked.");
|
|
662
|
+
entries.push({ kind: "file", path });
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const hash = createHash("sha256").update("kb-evaluation-cache-manifest-v2\0");
|
|
666
|
+
for (const entry of entries.toSorted((left, right) => left.path.localeCompare(right.path))) {
|
|
667
|
+
const relativePath = relative(canonicalRoot, entry.path).split(sep).join("/");
|
|
668
|
+
hash.update(entry.kind).update("\0").update(relativePath).update("\0");
|
|
669
|
+
if (entry.kind === "directory") continue;
|
|
670
|
+
const file = await digestEvaluationCacheFile(entry.path);
|
|
671
|
+
bytes += file.bytes;
|
|
672
|
+
if (!Number.isSafeInteger(bytes)) throw new RangeError("evaluation cache byte count overflowed.");
|
|
673
|
+
hash.update(String(file.bytes)).update("\0").update(file.sha256).update("\0");
|
|
674
|
+
}
|
|
675
|
+
return Object.freeze({ bytes, sha256: hash.digest("hex") });
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** Sum regular files in one isolated cache tree and reject aliases or special files. */
|
|
679
|
+
export async function measureEvaluationCacheBytesV2(root: string): Promise<number> {
|
|
680
|
+
return (await measureEvaluationCacheManifestV2(root)).bytes;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async function defaultVerifyFrozenSnapshot(request: ChildVerificationRequest): Promise<void> {
|
|
684
|
+
const corpus: VerifyFrozenEvaluationSnapshotOptions["corpus"] = { frozen: request.frozen };
|
|
685
|
+
await verifyFrozenEvaluationSnapshot({
|
|
686
|
+
repository: request.repository,
|
|
687
|
+
root: request.root,
|
|
688
|
+
corpus,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function scan(root: string, dependencies: EvaluationMeasurementChildV2Dependencies) {
|
|
693
|
+
return await (dependencies.scanVault ?? ((path: string) => scanVault(path, { mentionScope: false })))(root);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async function readPreparationMarker(path: string): Promise<PreparationMarker> {
|
|
697
|
+
const metadata = await lstat(path);
|
|
698
|
+
if (metadata.isSymbolicLink() || !metadata.isFile() || metadata.nlink !== 1 || metadata.size > MAX_PROTOCOL_BYTES) {
|
|
699
|
+
throw new TypeError("incremental preparation marker is invalid.");
|
|
700
|
+
}
|
|
701
|
+
const input = record(JSON.parse(await readFile(path, "utf8")) as unknown, "incremental preparation marker");
|
|
702
|
+
exactKeys(input, [
|
|
703
|
+
"cacheManifestSha256",
|
|
704
|
+
"documentCount",
|
|
705
|
+
"kind",
|
|
706
|
+
"mutationSha256",
|
|
707
|
+
"protocol",
|
|
708
|
+
"requestId",
|
|
709
|
+
"snapshotSha256",
|
|
710
|
+
"sourcePath",
|
|
711
|
+
"vaultManifestSha256",
|
|
712
|
+
"version",
|
|
713
|
+
], [], "incremental preparation marker");
|
|
714
|
+
if (
|
|
715
|
+
input.protocol !== PROTOCOL
|
|
716
|
+
|| input.version !== PROTOCOL_VERSION
|
|
717
|
+
|| input.kind !== "incremental-prepared"
|
|
718
|
+
|| typeof input.requestId !== "string"
|
|
719
|
+
|| !requestIdPattern.test(input.requestId)
|
|
720
|
+
|| typeof input.snapshotSha256 !== "string"
|
|
721
|
+
|| !sha256Pattern.test(input.snapshotSha256)
|
|
722
|
+
|| typeof input.mutationSha256 !== "string"
|
|
723
|
+
|| !sha256Pattern.test(input.mutationSha256)
|
|
724
|
+
|| typeof input.cacheManifestSha256 !== "string"
|
|
725
|
+
|| !sha256Pattern.test(input.cacheManifestSha256)
|
|
726
|
+
|| typeof input.vaultManifestSha256 !== "string"
|
|
727
|
+
|| !sha256Pattern.test(input.vaultManifestSha256)
|
|
728
|
+
) {
|
|
729
|
+
throw new TypeError("incremental preparation marker identity is invalid.");
|
|
730
|
+
}
|
|
731
|
+
return Object.freeze({
|
|
732
|
+
protocol: PROTOCOL,
|
|
733
|
+
version: PROTOCOL_VERSION,
|
|
734
|
+
kind: "incremental-prepared",
|
|
735
|
+
requestId: input.requestId,
|
|
736
|
+
snapshotSha256: input.snapshotSha256,
|
|
737
|
+
mutationSha256: input.mutationSha256,
|
|
738
|
+
sourcePath: notePath(input.sourcePath, "incremental preparation marker.sourcePath"),
|
|
739
|
+
documentCount: positiveInteger(input.documentCount, "incremental preparation marker.documentCount"),
|
|
740
|
+
cacheManifestSha256: input.cacheManifestSha256,
|
|
741
|
+
vaultManifestSha256: input.vaultManifestSha256,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async function prepareIncrementalMutation(
|
|
746
|
+
request: EvaluationMeasurementChildRequestV2,
|
|
747
|
+
identities: WorkIdentities,
|
|
748
|
+
notes: readonly { readonly path: string; readonly content: string }[],
|
|
749
|
+
documentCount: number,
|
|
750
|
+
dependencies: EvaluationMeasurementChildV2Dependencies,
|
|
751
|
+
): Promise<void> {
|
|
752
|
+
const mutation = request.mutation;
|
|
753
|
+
if (mutation === undefined) throw new TypeError("incremental preparation requires a mutation.");
|
|
754
|
+
const source = notes.find(({ path }) => path === mutation.sourcePath);
|
|
755
|
+
if (source === undefined) throw new TypeError("incremental mutation source is absent from the frozen vault.");
|
|
756
|
+
const sourceFile = resolve(identities.vault, mutation.sourcePath);
|
|
757
|
+
if (!confinedPath(identities.vault, sourceFile)) throw new TypeError("incremental mutation escaped the vault.");
|
|
758
|
+
const metadata = await lstat(sourceFile);
|
|
759
|
+
if (metadata.isSymbolicLink() || !metadata.isFile() || metadata.nlink !== 1) {
|
|
760
|
+
throw new TypeError("incremental mutation source must be a regular singly linked file.");
|
|
761
|
+
}
|
|
762
|
+
const changedContent = `${source.content}${mutation.appendText}`;
|
|
763
|
+
if (
|
|
764
|
+
createHash("sha256").update(changedContent, "utf8").digest("hex")
|
|
765
|
+
!== mutation.expectedPostMutationSha256
|
|
766
|
+
) {
|
|
767
|
+
throw new TypeError("incremental mutation does not produce its sealed post-mutation digest.");
|
|
768
|
+
}
|
|
769
|
+
await writeFile(sourceFile, changedContent, { encoding: "utf8", flag: "w" });
|
|
770
|
+
const changed = await scan(identities.vault, dependencies);
|
|
771
|
+
if (indexedDocumentCount(changed.notes) !== documentCount) {
|
|
772
|
+
throw new TypeError("incremental mutation changed the indexed document set.");
|
|
773
|
+
}
|
|
774
|
+
const cacheManifest = await measureEvaluationCacheManifestV2(identities.cache);
|
|
775
|
+
const marker: PreparationMarker = Object.freeze({
|
|
776
|
+
protocol: PROTOCOL,
|
|
777
|
+
version: PROTOCOL_VERSION,
|
|
778
|
+
kind: "incremental-prepared",
|
|
779
|
+
requestId: request.requestId,
|
|
780
|
+
snapshotSha256: snapshotSha256(request.frozen),
|
|
781
|
+
mutationSha256: mutation.sha256,
|
|
782
|
+
sourcePath: mutation.sourcePath,
|
|
783
|
+
documentCount,
|
|
784
|
+
cacheManifestSha256: cacheManifest.sha256,
|
|
785
|
+
vaultManifestSha256: vaultManifestSha256(changed.notes),
|
|
786
|
+
});
|
|
787
|
+
await writeFile(identities.marker, JSON.stringify(marker), { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function resourcesFor(result: SemanticIndexResult, peakRssBytes: number, cacheBytes: number) {
|
|
791
|
+
const embedding = result.embedding;
|
|
792
|
+
return Object.freeze({
|
|
793
|
+
llm: Object.freeze({ calls: 0, inputTokens: 0, outputTokens: 0 }),
|
|
794
|
+
embedding: Object.freeze({
|
|
795
|
+
calls: embedding === null ? 0 : 1,
|
|
796
|
+
// QMD exposes exact embedding duration and work counts, but not tokenizer input count.
|
|
797
|
+
inputTokens: 0,
|
|
798
|
+
...(embedding === null ? {} : { inputTokensMeasured: false as const }),
|
|
799
|
+
durationMs: embedding?.durationMs ?? 0,
|
|
800
|
+
}),
|
|
801
|
+
packedContext: Object.freeze({ utf8Bytes: 0, readerTokens: 0 }),
|
|
802
|
+
peakRssBytes,
|
|
803
|
+
cacheBytes,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** Execute one protocol phase. Dependencies make the child logic testable without QMD or model I/O. */
|
|
808
|
+
export async function executeKnowledgeBaseEvaluationMeasurementChildV2(
|
|
809
|
+
value: unknown,
|
|
810
|
+
dependencies: EvaluationMeasurementChildV2Dependencies = {},
|
|
811
|
+
): Promise<EvaluationMeasurementChildResponseV2> {
|
|
812
|
+
const request = parseChildRequest(value);
|
|
813
|
+
const identities = await validatedWorkIdentities(request.workRoot);
|
|
814
|
+
const environment = dependencies.environment ?? process.env;
|
|
815
|
+
if (
|
|
816
|
+
environment.XDG_CACHE_HOME !== identities.xdgCache
|
|
817
|
+
|| environment.HF_HUB_OFFLINE !== "1"
|
|
818
|
+
|| environment.TRANSFORMERS_OFFLINE !== "1"
|
|
819
|
+
|| environment.GGML_METAL_NO_RESIDENCY !== "1"
|
|
820
|
+
|| environment.QMD_EMBED_PARALLELISM !== "1"
|
|
821
|
+
|| environment.QMD_LLAMA_GPU !== "auto"
|
|
822
|
+
|| environment.QMD_GENERATE_MODEL !== identities.forbiddenGenerateModel
|
|
823
|
+
|| environment.QMD_RERANK_MODEL !== identities.forbiddenRerankModel
|
|
824
|
+
|| environment.TMPDIR !== identities.workRoot
|
|
825
|
+
|| environment.TMP !== identities.workRoot
|
|
826
|
+
|| environment.TEMP !== identities.workRoot
|
|
827
|
+
|| environment.NODE_OPTIONS !== undefined
|
|
828
|
+
|| environment.BUN_OPTIONS !== undefined
|
|
829
|
+
|| environment.CI !== undefined
|
|
830
|
+
|| environment.QMD_FORCE_CPU !== undefined
|
|
831
|
+
) {
|
|
832
|
+
throw new TypeError("measurement child execution environment is not isolated and pinned.");
|
|
833
|
+
}
|
|
834
|
+
await requireAbsent(identities.forbiddenGenerateModel, "generative model guard");
|
|
835
|
+
await requireAbsent(identities.forbiddenRerankModel, "reranker model guard");
|
|
836
|
+
await validateWorkLayout(identities, request.phase);
|
|
837
|
+
if (confinedPath(identities.cache, request.embeddingModelFile)) {
|
|
838
|
+
throw new TypeError("the pinned model file must be independent of the measured cache.");
|
|
839
|
+
}
|
|
840
|
+
const verify = dependencies.verifyFrozenSnapshot ?? defaultVerifyFrozenSnapshot;
|
|
841
|
+
const index = dependencies.indexSemanticVault ?? indexSemanticVault;
|
|
842
|
+
const now = dependencies.now ?? (() => performance.now());
|
|
843
|
+
const cacheBytes = dependencies.measureCacheBytes ?? measureEvaluationCacheBytesV2;
|
|
844
|
+
const peakRss = dependencies.peakRssBytes ?? defaultPeakRssBytes;
|
|
845
|
+
let snapshot: Pick<VaultSnapshot, "notes">;
|
|
846
|
+
let documentCount: number;
|
|
847
|
+
|
|
848
|
+
if (request.phase === "cold-index" || request.phase === "incremental-prepare") {
|
|
849
|
+
await verify({ repository: request.repository, root: request.root, frozen: request.frozen });
|
|
850
|
+
snapshot = await scan(request.root, dependencies);
|
|
851
|
+
documentCount = indexedDocumentCount(snapshot.notes);
|
|
852
|
+
if (documentCount < 1) throw new TypeError("measurement vault must contain an indexed content note.");
|
|
853
|
+
await materializeVault(identities.vault, snapshot.notes);
|
|
854
|
+
await mkdir(identities.xdgCache, { recursive: true, mode: 0o700 });
|
|
855
|
+
} else {
|
|
856
|
+
await validateIsolatedDirectory(identities.vault, "incremental prepared vault");
|
|
857
|
+
await validateIsolatedDirectory(identities.cache, "incremental prepared cache");
|
|
858
|
+
await validateIsolatedDirectory(identities.xdgCache, "incremental prepared XDG cache");
|
|
859
|
+
const marker = await readPreparationMarker(identities.marker);
|
|
860
|
+
const mutation = request.mutation;
|
|
861
|
+
if (
|
|
862
|
+
mutation === undefined
|
|
863
|
+
|| marker.requestId !== request.requestId
|
|
864
|
+
|| marker.snapshotSha256 !== snapshotSha256(request.frozen)
|
|
865
|
+
|| marker.mutationSha256 !== mutation.sha256
|
|
866
|
+
|| marker.sourcePath !== mutation.sourcePath
|
|
867
|
+
) {
|
|
868
|
+
throw new TypeError("incremental update does not match its prepared generation.");
|
|
869
|
+
}
|
|
870
|
+
snapshot = await scan(identities.vault, dependencies);
|
|
871
|
+
documentCount = indexedDocumentCount(snapshot.notes);
|
|
872
|
+
if (
|
|
873
|
+
documentCount !== marker.documentCount
|
|
874
|
+
|| vaultManifestSha256(snapshot.notes) !== marker.vaultManifestSha256
|
|
875
|
+
) {
|
|
876
|
+
throw new TypeError("incremental prepared vault changed before measurement.");
|
|
877
|
+
}
|
|
878
|
+
const databaseMetadata = await lstat(identities.database);
|
|
879
|
+
if (
|
|
880
|
+
databaseMetadata.isSymbolicLink()
|
|
881
|
+
|| !databaseMetadata.isFile()
|
|
882
|
+
|| databaseMetadata.nlink !== 1
|
|
883
|
+
) {
|
|
884
|
+
throw new TypeError("incremental prepared database is absent or aliased.");
|
|
885
|
+
}
|
|
886
|
+
if (
|
|
887
|
+
(await measureEvaluationCacheManifestV2(identities.cache)).sha256
|
|
888
|
+
!== marker.cacheManifestSha256
|
|
889
|
+
) {
|
|
890
|
+
throw new TypeError("incremental prepared cache changed before measurement.");
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const startedAt = now();
|
|
895
|
+
const indexed = await index({
|
|
896
|
+
root: identities.vault,
|
|
897
|
+
database: identities.database,
|
|
898
|
+
embeddingModelFile: request.embeddingModelFile,
|
|
899
|
+
});
|
|
900
|
+
const elapsedMs = nonnegativeNumber(now() - startedAt, "measurement elapsedMs");
|
|
901
|
+
if (resolve(indexed.root) !== identities.vault || resolve(indexed.database) !== identities.database) {
|
|
902
|
+
throw new TypeError("semantic indexing escaped the isolated measurement identities.");
|
|
903
|
+
}
|
|
904
|
+
validateIndexResult(indexed, request.phase, documentCount);
|
|
905
|
+
const resources = resourcesFor(
|
|
906
|
+
indexed,
|
|
907
|
+
positiveInteger(peakRss(), "measurement peakRssBytes"),
|
|
908
|
+
positiveInteger(await cacheBytes(identities.cache), "measurement cacheBytes"),
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
if (request.phase === "incremental-prepare") {
|
|
912
|
+
await prepareIncrementalMutation(
|
|
913
|
+
request,
|
|
914
|
+
identities,
|
|
915
|
+
snapshot.notes,
|
|
916
|
+
documentCount,
|
|
917
|
+
dependencies,
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
return Object.freeze({
|
|
921
|
+
protocol: PROTOCOL,
|
|
922
|
+
version: PROTOCOL_VERSION,
|
|
923
|
+
kind: "response",
|
|
924
|
+
requestId: request.requestId,
|
|
925
|
+
phase: request.phase,
|
|
926
|
+
elapsedMs,
|
|
927
|
+
index: Object.freeze({
|
|
928
|
+
model: indexed.model,
|
|
929
|
+
documentCount,
|
|
930
|
+
update: Object.freeze({ ...indexed.update }),
|
|
931
|
+
embedding: indexed.embedding === null ? null : Object.freeze({ ...indexed.embedding }),
|
|
932
|
+
}),
|
|
933
|
+
resources,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function parseUpdate(value: unknown, label: string): SemanticIndexResult["update"] {
|
|
938
|
+
const input = record(value, label);
|
|
939
|
+
exactKeys(input, ["collections", "indexed", "needsEmbedding", "removed", "unchanged", "updated"], [], label);
|
|
940
|
+
return Object.freeze({
|
|
941
|
+
collections: nonnegativeInteger(input.collections, `${label}.collections`),
|
|
942
|
+
indexed: nonnegativeInteger(input.indexed, `${label}.indexed`),
|
|
943
|
+
updated: nonnegativeInteger(input.updated, `${label}.updated`),
|
|
944
|
+
unchanged: nonnegativeInteger(input.unchanged, `${label}.unchanged`),
|
|
945
|
+
removed: nonnegativeInteger(input.removed, `${label}.removed`),
|
|
946
|
+
needsEmbedding: nonnegativeInteger(input.needsEmbedding, `${label}.needsEmbedding`),
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function parseEmbedding(value: unknown, label: string): NonNullable<SemanticIndexResult["embedding"]> | null {
|
|
951
|
+
if (value === null) return null;
|
|
952
|
+
const input = record(value, label);
|
|
953
|
+
exactKeys(input, ["chunksEmbedded", "docsProcessed", "durationMs", "errors"], ["failures"], label);
|
|
954
|
+
if (input.failures !== undefined && (!Array.isArray(input.failures) || input.failures.length !== 0)) {
|
|
955
|
+
throw new TypeError(`${label}.failures must be absent or empty.`);
|
|
956
|
+
}
|
|
957
|
+
return Object.freeze({
|
|
958
|
+
docsProcessed: nonnegativeInteger(input.docsProcessed, `${label}.docsProcessed`),
|
|
959
|
+
chunksEmbedded: nonnegativeInteger(input.chunksEmbedded, `${label}.chunksEmbedded`),
|
|
960
|
+
errors: nonnegativeInteger(input.errors, `${label}.errors`),
|
|
961
|
+
...(input.failures === undefined ? {} : { failures: Object.freeze([]) }),
|
|
962
|
+
durationMs: nonnegativeNumber(input.durationMs, `${label}.durationMs`),
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function parseResources(value: unknown): EvaluationResourceAccountingV2 {
|
|
967
|
+
const input = record(value, "measurement child response.resources");
|
|
968
|
+
exactKeys(input, ["cacheBytes", "embedding", "llm", "packedContext", "peakRssBytes"], [], "measurement child response.resources");
|
|
969
|
+
const llm = record(input.llm, "measurement child response.resources.llm");
|
|
970
|
+
exactKeys(llm, ["calls", "inputTokens", "outputTokens"], [], "measurement child response.resources.llm");
|
|
971
|
+
if (llm.calls !== 0 || llm.inputTokens !== 0 || llm.outputTokens !== 0) {
|
|
972
|
+
throw new TypeError("measurement child reported nonzero generative LLM work.");
|
|
973
|
+
}
|
|
974
|
+
const embedding = record(input.embedding, "measurement child response.resources.embedding");
|
|
975
|
+
exactKeys(
|
|
976
|
+
embedding,
|
|
977
|
+
["calls", "durationMs", "inputTokens"],
|
|
978
|
+
["inputTokensMeasured"],
|
|
979
|
+
"measurement child response.resources.embedding",
|
|
980
|
+
);
|
|
981
|
+
const packed = record(input.packedContext, "measurement child response.resources.packedContext");
|
|
982
|
+
exactKeys(packed, ["readerTokens", "utf8Bytes"], [], "measurement child response.resources.packedContext");
|
|
983
|
+
if (packed.readerTokens !== 0 || packed.utf8Bytes !== 0) {
|
|
984
|
+
throw new TypeError("index measurement child reported packed-context work.");
|
|
985
|
+
}
|
|
986
|
+
const embeddingCalls = nonnegativeInteger(embedding.calls, "measurement embedding.calls");
|
|
987
|
+
const embeddingInputTokens = nonnegativeInteger(
|
|
988
|
+
embedding.inputTokens,
|
|
989
|
+
"measurement embedding.inputTokens",
|
|
990
|
+
);
|
|
991
|
+
if (
|
|
992
|
+
(embeddingCalls === 0 && embedding.inputTokensMeasured !== undefined)
|
|
993
|
+
|| (embeddingCalls > 0 && embedding.inputTokensMeasured !== false)
|
|
994
|
+
|| (embedding.inputTokensMeasured === false && embeddingInputTokens !== 0)
|
|
995
|
+
) {
|
|
996
|
+
throw new TypeError(
|
|
997
|
+
"measurement embedding input-token accounting must explicitly mark unavailable counts.",
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
return Object.freeze({
|
|
1001
|
+
llm: Object.freeze({ calls: 0, inputTokens: 0, outputTokens: 0 }),
|
|
1002
|
+
embedding: Object.freeze({
|
|
1003
|
+
calls: embeddingCalls,
|
|
1004
|
+
inputTokens: embeddingInputTokens,
|
|
1005
|
+
...(embedding.inputTokensMeasured === false ? { inputTokensMeasured: false as const } : {}),
|
|
1006
|
+
durationMs: nonnegativeNumber(embedding.durationMs, "measurement embedding.durationMs"),
|
|
1007
|
+
}),
|
|
1008
|
+
packedContext: Object.freeze({ utf8Bytes: 0, readerTokens: 0 }),
|
|
1009
|
+
peakRssBytes: positiveInteger(input.peakRssBytes, "measurement peakRssBytes"),
|
|
1010
|
+
cacheBytes: positiveInteger(input.cacheBytes, "measurement cacheBytes"),
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function parseChildResponse(
|
|
1015
|
+
bytes: Uint8Array,
|
|
1016
|
+
request: EvaluationMeasurementChildRequestV2,
|
|
1017
|
+
): EvaluationMeasurementChildResponseV2 {
|
|
1018
|
+
if (bytes.byteLength === 0 || bytes.byteLength > MAX_PROTOCOL_BYTES) {
|
|
1019
|
+
throw new TypeError("measurement child response has an invalid byte length.");
|
|
1020
|
+
}
|
|
1021
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1022
|
+
if (text.trim() !== text) throw new TypeError("measurement child response framing is invalid.");
|
|
1023
|
+
const input = record(JSON.parse(text) as unknown, "measurement child response");
|
|
1024
|
+
exactKeys(input, ["elapsedMs", "index", "kind", "phase", "protocol", "requestId", "resources", "version"], [], "measurement child response");
|
|
1025
|
+
if (
|
|
1026
|
+
input.protocol !== PROTOCOL
|
|
1027
|
+
|| input.version !== PROTOCOL_VERSION
|
|
1028
|
+
|| input.kind !== "response"
|
|
1029
|
+
|| input.requestId !== request.requestId
|
|
1030
|
+
|| input.phase !== request.phase
|
|
1031
|
+
) {
|
|
1032
|
+
throw new TypeError("measurement child response does not match its request.");
|
|
1033
|
+
}
|
|
1034
|
+
const indexInput = record(input.index, "measurement child response.index");
|
|
1035
|
+
exactKeys(indexInput, ["documentCount", "embedding", "model", "update"], [], "measurement child response.index");
|
|
1036
|
+
if (indexInput.model !== recommendedEmbeddingModel) {
|
|
1037
|
+
throw new TypeError("measurement child response has the wrong model identity.");
|
|
1038
|
+
}
|
|
1039
|
+
const index = Object.freeze({
|
|
1040
|
+
model: recommendedEmbeddingModel,
|
|
1041
|
+
documentCount: positiveInteger(indexInput.documentCount, "measurement documentCount"),
|
|
1042
|
+
update: parseUpdate(indexInput.update, "measurement child response.index.update"),
|
|
1043
|
+
embedding: parseEmbedding(indexInput.embedding, "measurement child response.index.embedding"),
|
|
1044
|
+
});
|
|
1045
|
+
const elapsedMs = nonnegativeNumber(input.elapsedMs, "measurement child response.elapsedMs");
|
|
1046
|
+
const resources = parseResources(input.resources);
|
|
1047
|
+
validateIndexResult({ root: "", database: "", ...index }, request.phase, index.documentCount);
|
|
1048
|
+
if (
|
|
1049
|
+
resources.embedding.calls !== 1
|
|
1050
|
+
|| resources.embedding.durationMs !== index.embedding?.durationMs
|
|
1051
|
+
|| resources.embedding.inputTokens !== 0
|
|
1052
|
+
) {
|
|
1053
|
+
throw new TypeError("measurement child response accounting contradicts its semantic result.");
|
|
1054
|
+
}
|
|
1055
|
+
return Object.freeze({
|
|
1056
|
+
protocol: PROTOCOL,
|
|
1057
|
+
version: PROTOCOL_VERSION,
|
|
1058
|
+
kind: "response",
|
|
1059
|
+
requestId: request.requestId,
|
|
1060
|
+
phase: request.phase,
|
|
1061
|
+
elapsedMs,
|
|
1062
|
+
index,
|
|
1063
|
+
resources,
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function timeoutMs(value: number | undefined): number {
|
|
1068
|
+
const parsed = value ?? DEFAULT_TIMEOUT_MS;
|
|
1069
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > MAX_TIMEOUT_MS) {
|
|
1070
|
+
throw new TypeError(`measurement timeoutMs must be from 1 through ${MAX_TIMEOUT_MS}.`);
|
|
1071
|
+
}
|
|
1072
|
+
return parsed;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function childCommand(value: readonly string[] | undefined): readonly string[] {
|
|
1076
|
+
const command = value ?? Object.freeze([process.execPath, fileURLToPath(import.meta.url), CHILD_ARGUMENT]);
|
|
1077
|
+
if (
|
|
1078
|
+
command.length < 1
|
|
1079
|
+
|| command.length > 16
|
|
1080
|
+
|| command.some((part) => typeof part !== "string" || part.length === 0 || /[\0\r\n]/u.test(part))
|
|
1081
|
+
) {
|
|
1082
|
+
throw new TypeError("measurement child command is invalid.");
|
|
1083
|
+
}
|
|
1084
|
+
return Object.freeze([...command]);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function environmentFor(identities: WorkIdentities): Readonly<Record<string, string>> {
|
|
1088
|
+
const environment: Record<string, string> = {
|
|
1089
|
+
LANG: "C",
|
|
1090
|
+
LC_ALL: "C",
|
|
1091
|
+
PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
1092
|
+
};
|
|
1093
|
+
environment.XDG_CACHE_HOME = identities.xdgCache;
|
|
1094
|
+
environment.HF_HUB_OFFLINE = "1";
|
|
1095
|
+
environment.TRANSFORMERS_OFFLINE = "1";
|
|
1096
|
+
environment.GGML_METAL_NO_RESIDENCY = "1";
|
|
1097
|
+
environment.QMD_EMBED_PARALLELISM = "1";
|
|
1098
|
+
environment.QMD_LLAMA_GPU = "auto";
|
|
1099
|
+
environment.TMPDIR = identities.workRoot;
|
|
1100
|
+
environment.TMP = identities.workRoot;
|
|
1101
|
+
environment.TEMP = identities.workRoot;
|
|
1102
|
+
// A future accidental generate or rerank call must fail before it can produce work.
|
|
1103
|
+
environment.QMD_GENERATE_MODEL = identities.forbiddenGenerateModel;
|
|
1104
|
+
environment.QMD_RERANK_MODEL = identities.forbiddenRerankModel;
|
|
1105
|
+
return Object.freeze(environment);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/** Default bounded process transport. Each call creates exactly one fresh OS process. */
|
|
1109
|
+
export const spawnEvaluationMeasurementChildV2: EvaluationMeasurementChildProcessFactoryV2 =
|
|
1110
|
+
async (request) => await new Promise((resolvePromise, rejectPromise) => {
|
|
1111
|
+
const [executable, ...arguments_] = request.command;
|
|
1112
|
+
if (executable === undefined) {
|
|
1113
|
+
rejectPromise(new TypeError("measurement child executable is missing."));
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
const child = spawn(executable, arguments_, {
|
|
1117
|
+
cwd: request.cwd,
|
|
1118
|
+
env: {
|
|
1119
|
+
...request.environment,
|
|
1120
|
+
NODE_ENV: "production",
|
|
1121
|
+
},
|
|
1122
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1123
|
+
});
|
|
1124
|
+
const stdout: Buffer[] = [];
|
|
1125
|
+
const stderr: Buffer[] = [];
|
|
1126
|
+
let stdoutBytes = 0;
|
|
1127
|
+
let stderrBytes = 0;
|
|
1128
|
+
let termination: EvaluationMeasurementChildProcessResultV2["termination"] = "exit";
|
|
1129
|
+
let settled = false;
|
|
1130
|
+
let outputFailure: Error | undefined;
|
|
1131
|
+
const abort = () => {
|
|
1132
|
+
if (termination === "exit") termination = "aborted";
|
|
1133
|
+
child.kill("SIGKILL");
|
|
1134
|
+
};
|
|
1135
|
+
const timer = setTimeout(() => {
|
|
1136
|
+
if (termination === "exit") termination = "timeout";
|
|
1137
|
+
child.kill("SIGKILL");
|
|
1138
|
+
}, request.timeoutMs);
|
|
1139
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
1140
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
1141
|
+
stdoutBytes += chunk.byteLength;
|
|
1142
|
+
if (stdoutBytes > request.maxStdoutBytes) {
|
|
1143
|
+
outputFailure = new RangeError("measurement child stdout exceeded its bound.");
|
|
1144
|
+
child.kill("SIGKILL");
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
stdout.push(Buffer.from(chunk));
|
|
1148
|
+
});
|
|
1149
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
1150
|
+
stderrBytes += chunk.byteLength;
|
|
1151
|
+
if (stderrBytes > request.maxStderrBytes) {
|
|
1152
|
+
outputFailure = new RangeError("measurement child stderr exceeded its bound.");
|
|
1153
|
+
child.kill("SIGKILL");
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
stderr.push(Buffer.from(chunk));
|
|
1157
|
+
});
|
|
1158
|
+
const streamError = () => {
|
|
1159
|
+
outputFailure = new Error("measurement child process stream failed.");
|
|
1160
|
+
child.kill("SIGKILL");
|
|
1161
|
+
};
|
|
1162
|
+
child.stdin.once("error", streamError);
|
|
1163
|
+
child.stdout.once("error", streamError);
|
|
1164
|
+
child.stderr.once("error", streamError);
|
|
1165
|
+
child.once("error", (error) => {
|
|
1166
|
+
if (settled) return;
|
|
1167
|
+
settled = true;
|
|
1168
|
+
clearTimeout(timer);
|
|
1169
|
+
request.signal.removeEventListener("abort", abort);
|
|
1170
|
+
rejectPromise(error);
|
|
1171
|
+
});
|
|
1172
|
+
child.once("close", (exitCode) => {
|
|
1173
|
+
if (settled) return;
|
|
1174
|
+
settled = true;
|
|
1175
|
+
clearTimeout(timer);
|
|
1176
|
+
request.signal.removeEventListener("abort", abort);
|
|
1177
|
+
if (outputFailure !== undefined) {
|
|
1178
|
+
rejectPromise(outputFailure);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
resolvePromise(Object.freeze({
|
|
1182
|
+
termination,
|
|
1183
|
+
exitCode,
|
|
1184
|
+
stdout: Buffer.concat(stdout),
|
|
1185
|
+
stderr: Buffer.concat(stderr),
|
|
1186
|
+
}));
|
|
1187
|
+
});
|
|
1188
|
+
if (request.signal.aborted) abort();
|
|
1189
|
+
child.stdin.end(request.stdin);
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
function traceFor(
|
|
1193
|
+
descriptor: EvaluationRetrieverDescriptorV2,
|
|
1194
|
+
measured: boolean,
|
|
1195
|
+
): EvaluationRetrieverTraceV2 {
|
|
1196
|
+
return Object.freeze({
|
|
1197
|
+
laneOutcomes: Object.freeze(descriptor.lanes.map((laneId) => {
|
|
1198
|
+
const applicable = qmdLanes.has(laneId);
|
|
1199
|
+
return Object.freeze({
|
|
1200
|
+
laneId,
|
|
1201
|
+
applicability: measured && applicable ? "applied" as const : "skipped" as const,
|
|
1202
|
+
status: measured ? "ready" as const : "unavailable" as const,
|
|
1203
|
+
reasonCodes: Object.freeze(measured && applicable ? [] : ["operation-not-applicable"]),
|
|
1204
|
+
rawRanking: Object.freeze([]),
|
|
1205
|
+
});
|
|
1206
|
+
})),
|
|
1207
|
+
candidateDecisions: Object.freeze([]),
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function zeroResources(): EvaluationResourceAccountingV2 {
|
|
1212
|
+
return Object.freeze({
|
|
1213
|
+
llm: Object.freeze({ calls: 0, inputTokens: 0, outputTokens: 0 }),
|
|
1214
|
+
embedding: Object.freeze({ calls: 0, inputTokens: 0, durationMs: 0 }),
|
|
1215
|
+
packedContext: Object.freeze({ utf8Bytes: 0, readerTokens: 0 }),
|
|
1216
|
+
peakRssBytes: 0,
|
|
1217
|
+
cacheBytes: 0,
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function validateMeasurementInput(input: Readonly<{
|
|
1222
|
+
readonly operation: "cold-index" | "incremental-update";
|
|
1223
|
+
readonly corpus: RetrievalEvaluationCorpusV2;
|
|
1224
|
+
readonly descriptor: EvaluationRetrieverDescriptorV2;
|
|
1225
|
+
readonly profile: EvaluationMeasurementProfileV2;
|
|
1226
|
+
readonly repetition: number;
|
|
1227
|
+
}>): void {
|
|
1228
|
+
if (
|
|
1229
|
+
input.profile.operation !== input.operation
|
|
1230
|
+
|| input.profile.scope !== "retriever"
|
|
1231
|
+
|| input.profile.concurrency !== 1
|
|
1232
|
+
|| input.profile.cacheState !== (input.operation === "cold-index" ? "cold" : "changed-generation")
|
|
1233
|
+
|| !Number.isSafeInteger(input.repetition)
|
|
1234
|
+
|| input.repetition < 1
|
|
1235
|
+
|| input.repetition > input.profile.repetitions
|
|
1236
|
+
) {
|
|
1237
|
+
throw new TypeError("retriever measurement input does not match its locked profile.");
|
|
1238
|
+
}
|
|
1239
|
+
const localModel = input.corpus.experiment.environment.localModel;
|
|
1240
|
+
if (
|
|
1241
|
+
localModel.kind !== "model"
|
|
1242
|
+
|| localModel.id !== recommendedEmbeddingModel
|
|
1243
|
+
|| localModel.sha256 !== recommendedEmbeddingModelSha256
|
|
1244
|
+
) {
|
|
1245
|
+
throw new TypeError("QMD measurement requires the pinned local embedding model identity.");
|
|
1246
|
+
}
|
|
1247
|
+
const lockedDescriptor = input.corpus.retrievers.find(({ id }) => id === input.descriptor.id);
|
|
1248
|
+
if (
|
|
1249
|
+
lockedDescriptor === undefined
|
|
1250
|
+
|| evaluationRetrieverDescriptorDigestV2(lockedDescriptor)
|
|
1251
|
+
!== evaluationRetrieverDescriptorDigestV2(input.descriptor)
|
|
1252
|
+
) {
|
|
1253
|
+
throw new TypeError("retriever measurement descriptor is absent from the sealed corpus.");
|
|
1254
|
+
}
|
|
1255
|
+
const lockedProfile = input.corpus.measurementProfiles.find(({ id }) => id === input.profile.id);
|
|
1256
|
+
if (
|
|
1257
|
+
lockedProfile === undefined
|
|
1258
|
+
|| lockedProfile.operation !== input.profile.operation
|
|
1259
|
+
|| lockedProfile.scope !== input.profile.scope
|
|
1260
|
+
|| lockedProfile.cacheState !== input.profile.cacheState
|
|
1261
|
+
|| lockedProfile.concurrency !== input.profile.concurrency
|
|
1262
|
+
|| lockedProfile.repetitions !== input.profile.repetitions
|
|
1263
|
+
) {
|
|
1264
|
+
throw new TypeError("retriever measurement profile is absent from the sealed corpus.");
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function assertMutationMatchesSealedExperiment(
|
|
1269
|
+
corpus: RetrievalEvaluationCorpusV2,
|
|
1270
|
+
mutation: EvaluationIncrementalMutationV2,
|
|
1271
|
+
): void {
|
|
1272
|
+
const sealed = corpus.experiment.environment.incrementalMutation;
|
|
1273
|
+
const appendUtf8Sha256 = createHash("sha256")
|
|
1274
|
+
.update(mutation.appendText, "utf8")
|
|
1275
|
+
.digest("hex");
|
|
1276
|
+
if (
|
|
1277
|
+
mutation.sourcePath !== sealed.sourcePath
|
|
1278
|
+
|| appendUtf8Sha256 !== sealed.appendUtf8Sha256
|
|
1279
|
+
|| mutation.expectedPostMutationSha256 !== sealed.expectedPostMutationSha256
|
|
1280
|
+
) {
|
|
1281
|
+
throw new TypeError("incremental measurement mutation does not match the sealed experiment identity.");
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
async function runChildPhase(options: Readonly<{
|
|
1286
|
+
readonly phase: EvaluationMeasurementChildPhaseV2;
|
|
1287
|
+
readonly requestId: string;
|
|
1288
|
+
readonly workRoot: string;
|
|
1289
|
+
readonly repository: string;
|
|
1290
|
+
readonly root: string;
|
|
1291
|
+
readonly frozen: FrozenSnapshotRequest;
|
|
1292
|
+
readonly embeddingModelFile: string;
|
|
1293
|
+
readonly mutation: EvaluationIncrementalMutationV2;
|
|
1294
|
+
readonly command: readonly string[];
|
|
1295
|
+
readonly timeoutMs: number;
|
|
1296
|
+
readonly signal: AbortSignal;
|
|
1297
|
+
readonly childProcessFactory: EvaluationMeasurementChildProcessFactoryV2;
|
|
1298
|
+
}>): Promise<EvaluationMeasurementChildResponseV2> {
|
|
1299
|
+
if (options.signal.aborted) {
|
|
1300
|
+
throw options.signal.reason ?? new Error("measurement child request was aborted.");
|
|
1301
|
+
}
|
|
1302
|
+
const request: EvaluationMeasurementChildRequestV2 = Object.freeze({
|
|
1303
|
+
protocol: PROTOCOL,
|
|
1304
|
+
version: PROTOCOL_VERSION,
|
|
1305
|
+
kind: "request",
|
|
1306
|
+
requestId: options.requestId,
|
|
1307
|
+
phase: options.phase,
|
|
1308
|
+
repository: options.repository,
|
|
1309
|
+
root: options.root,
|
|
1310
|
+
frozen: options.frozen,
|
|
1311
|
+
embeddingModelFile: options.embeddingModelFile,
|
|
1312
|
+
workRoot: options.workRoot,
|
|
1313
|
+
...(options.phase === "cold-index" ? {} : { mutation: options.mutation }),
|
|
1314
|
+
});
|
|
1315
|
+
const serialized = Buffer.from(JSON.stringify(request), "utf8");
|
|
1316
|
+
if (serialized.byteLength > MAX_PROTOCOL_BYTES) throw new RangeError("measurement child request is too large.");
|
|
1317
|
+
const identities = workIdentities(options.workRoot);
|
|
1318
|
+
const result = await options.childProcessFactory({
|
|
1319
|
+
command: options.command,
|
|
1320
|
+
cwd: options.repository,
|
|
1321
|
+
environment: environmentFor(identities),
|
|
1322
|
+
stdin: serialized,
|
|
1323
|
+
timeoutMs: options.timeoutMs,
|
|
1324
|
+
maxStdoutBytes: MAX_PROTOCOL_BYTES,
|
|
1325
|
+
maxStderrBytes: MAX_STDERR_BYTES,
|
|
1326
|
+
signal: options.signal,
|
|
1327
|
+
});
|
|
1328
|
+
if (result.stdout.byteLength > MAX_PROTOCOL_BYTES || result.stderr.byteLength > MAX_STDERR_BYTES) {
|
|
1329
|
+
throw new RangeError("measurement child output exceeded its bound.");
|
|
1330
|
+
}
|
|
1331
|
+
if (result.termination !== "exit") {
|
|
1332
|
+
throw new Error(`measurement child ${result.termination}.`);
|
|
1333
|
+
}
|
|
1334
|
+
if (result.exitCode !== 0) throw new Error("measurement child exited unsuccessfully.");
|
|
1335
|
+
return parseChildResponse(result.stdout, request);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/** Create a runner-compatible cold-index and one-note-update measurement adapter. */
|
|
1339
|
+
export function createKnowledgeBaseEvaluationRetrieverOperationMeasurerV2(
|
|
1340
|
+
options: CreateKnowledgeBaseEvaluationRetrieverOperationMeasurerV2Options,
|
|
1341
|
+
): KnowledgeBaseEvaluationRunnerV2Dependencies["measureRetrieverOperation"] {
|
|
1342
|
+
const repository = resolve(options.repository);
|
|
1343
|
+
const root = resolve(options.root);
|
|
1344
|
+
const embeddingModelFile = resolve(options.embeddingModelFile);
|
|
1345
|
+
const mutation = parseMutation(options.mutation, "mutation");
|
|
1346
|
+
const timeout = timeoutMs(options.timeoutMs);
|
|
1347
|
+
const command = childCommand(options.childCommand);
|
|
1348
|
+
const temporaryDirectory = resolve(options.temporaryDirectory ?? tmpdir());
|
|
1349
|
+
const childProcessFactory = options.childProcessFactory ?? spawnEvaluationMeasurementChildV2;
|
|
1350
|
+
const sharedSubstrateMeasurements = new Map<
|
|
1351
|
+
string,
|
|
1352
|
+
Promise<EvaluationMeasurementChildResponseV2>
|
|
1353
|
+
>();
|
|
1354
|
+
|
|
1355
|
+
const measureSharedSubstrate = async (
|
|
1356
|
+
input: Parameters<KnowledgeBaseEvaluationRunnerV2Dependencies["measureRetrieverOperation"]>[0],
|
|
1357
|
+
): Promise<EvaluationMeasurementChildResponseV2> => {
|
|
1358
|
+
const canonicalTemporaryDirectory = await realpath(temporaryDirectory);
|
|
1359
|
+
const temporaryMetadata = await stat(canonicalTemporaryDirectory);
|
|
1360
|
+
if (!temporaryMetadata.isDirectory()) {
|
|
1361
|
+
throw new TypeError("measurement temporaryDirectory must be a directory.");
|
|
1362
|
+
}
|
|
1363
|
+
const workRoot = await mkdtemp(join(canonicalTemporaryDirectory, WORK_DIRECTORY_PREFIX));
|
|
1364
|
+
const requestId = randomUUID();
|
|
1365
|
+
try {
|
|
1366
|
+
const common = {
|
|
1367
|
+
requestId,
|
|
1368
|
+
workRoot,
|
|
1369
|
+
repository,
|
|
1370
|
+
root,
|
|
1371
|
+
frozen: input.corpus.frozen,
|
|
1372
|
+
embeddingModelFile,
|
|
1373
|
+
mutation,
|
|
1374
|
+
command,
|
|
1375
|
+
timeoutMs: timeout,
|
|
1376
|
+
signal: input.signal,
|
|
1377
|
+
childProcessFactory,
|
|
1378
|
+
} as const;
|
|
1379
|
+
if (input.operation === "cold-index") {
|
|
1380
|
+
return await runChildPhase({ ...common, phase: "cold-index" });
|
|
1381
|
+
}
|
|
1382
|
+
await runChildPhase({ ...common, phase: "incremental-prepare" });
|
|
1383
|
+
return await runChildPhase({ ...common, phase: "incremental-update" });
|
|
1384
|
+
} finally {
|
|
1385
|
+
await rm(workRoot, { recursive: true, force: true });
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
return async (input): Promise<EvaluationRetrieverOperationMeasurementV2> => {
|
|
1390
|
+
validateMeasurementInput(input);
|
|
1391
|
+
assertMutationMatchesSealedExperiment(input.corpus, mutation);
|
|
1392
|
+
if (!input.descriptor.lanes.some((lane) => qmdLanes.has(lane))) {
|
|
1393
|
+
return Object.freeze({
|
|
1394
|
+
status: "unavailable",
|
|
1395
|
+
timings: Object.freeze({ elapsedMs: 0, indexMs: 0, updateMs: 0, queryMs: 0, packingMs: 0 }),
|
|
1396
|
+
resources: zeroResources(),
|
|
1397
|
+
trace: traceFor(input.descriptor, false),
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
if (input.signal.aborted) throw input.signal.reason ?? new Error("retriever measurement was aborted.");
|
|
1401
|
+
const measurementIdentity = JSON.stringify([
|
|
1402
|
+
input.corpus.frozen.repositoryCommit,
|
|
1403
|
+
input.corpus.frozen.vaultTree,
|
|
1404
|
+
input.corpus.experiment.environment.localModel,
|
|
1405
|
+
input.operation,
|
|
1406
|
+
input.profile,
|
|
1407
|
+
input.repetition,
|
|
1408
|
+
mutation.sha256,
|
|
1409
|
+
]);
|
|
1410
|
+
let pending = sharedSubstrateMeasurements.get(measurementIdentity);
|
|
1411
|
+
if (pending === undefined) {
|
|
1412
|
+
pending = measureSharedSubstrate(input);
|
|
1413
|
+
sharedSubstrateMeasurements.set(measurementIdentity, pending);
|
|
1414
|
+
void pending.catch(() => {
|
|
1415
|
+
if (sharedSubstrateMeasurements.get(measurementIdentity) === pending) {
|
|
1416
|
+
sharedSubstrateMeasurements.delete(measurementIdentity);
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
const measured = await pending;
|
|
1421
|
+
if (input.signal.aborted) throw input.signal.reason ?? new Error("retriever measurement was aborted.");
|
|
1422
|
+
return Object.freeze({
|
|
1423
|
+
status: "ready",
|
|
1424
|
+
timings: Object.freeze({
|
|
1425
|
+
elapsedMs: measured.elapsedMs,
|
|
1426
|
+
indexMs: input.operation === "cold-index" ? measured.elapsedMs : 0,
|
|
1427
|
+
updateMs: input.operation === "incremental-update" ? measured.elapsedMs : 0,
|
|
1428
|
+
queryMs: 0,
|
|
1429
|
+
packingMs: 0,
|
|
1430
|
+
}),
|
|
1431
|
+
resources: measured.resources,
|
|
1432
|
+
trace: traceFor(input.descriptor, true),
|
|
1433
|
+
});
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
async function readStdin(): Promise<Uint8Array> {
|
|
1438
|
+
const chunks: Buffer[] = [];
|
|
1439
|
+
let bytes = 0;
|
|
1440
|
+
for await (const chunk of process.stdin) {
|
|
1441
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array);
|
|
1442
|
+
bytes += buffer.byteLength;
|
|
1443
|
+
if (bytes > MAX_PROTOCOL_BYTES) throw new RangeError("measurement child stdin exceeded its bound.");
|
|
1444
|
+
chunks.push(buffer);
|
|
1445
|
+
}
|
|
1446
|
+
return Buffer.concat(chunks);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
async function childMain(): Promise<void> {
|
|
1450
|
+
try {
|
|
1451
|
+
const bytes = await readStdin();
|
|
1452
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1453
|
+
if (text.trim() !== text) throw new TypeError("measurement child request framing is invalid.");
|
|
1454
|
+
const response = await executeKnowledgeBaseEvaluationMeasurementChildV2(JSON.parse(text) as unknown);
|
|
1455
|
+
process.stdout.write(JSON.stringify(response));
|
|
1456
|
+
} catch {
|
|
1457
|
+
process.stderr.write("Knowledge-base evaluation measurement child failed.\n");
|
|
1458
|
+
process.exitCode = 1;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
if (import.meta.main && process.argv.at(-1) === CHILD_ARGUMENT) await childMain();
|