@functional-systems/srcx 0.1.0-dev.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/CHANGELOG.md +20 -0
- package/CONTRIBUTING.md +54 -0
- package/DESIGN.md +1057 -0
- package/LICENSE +202 -0
- package/README.md +209 -0
- package/RELEASING.md +137 -0
- package/VALIDATION.md +181 -0
- package/dist/build.d.ts +94 -0
- package/dist/build.js +433 -0
- package/dist/build.js.map +1 -0
- package/dist/chunk.d.ts +30 -0
- package/dist/chunk.js +296 -0
- package/dist/chunk.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +276 -0
- package/dist/cli.js.map +1 -0
- package/dist/common.d.ts +17 -0
- package/dist/common.js +74 -0
- package/dist/common.js.map +1 -0
- package/dist/git.d.ts +38 -0
- package/dist/git.js +224 -0
- package/dist/git.js.map +1 -0
- package/dist/publish.d.ts +60 -0
- package/dist/publish.js +341 -0
- package/dist/publish.js.map +1 -0
- package/dist/releases.d.ts +8 -0
- package/dist/releases.js +79 -0
- package/dist/releases.js.map +1 -0
- package/dist/remote.d.ts +53 -0
- package/dist/remote.js +136 -0
- package/dist/remote.js.map +1 -0
- package/dist/repository.d.ts +29 -0
- package/dist/repository.js +180 -0
- package/dist/repository.js.map +1 -0
- package/dist/search.d.ts +46 -0
- package/dist/search.js +181 -0
- package/dist/search.js.map +1 -0
- package/dist/settings.d.ts +10 -0
- package/dist/settings.js +35 -0
- package/dist/settings.js.map +1 -0
- package/package.json +58 -0
- package/src/build.ts +587 -0
- package/src/chunk.ts +328 -0
- package/src/cli.ts +366 -0
- package/src/common.ts +71 -0
- package/src/git.ts +286 -0
- package/src/publish.ts +487 -0
- package/src/releases.ts +108 -0
- package/src/remote.ts +230 -0
- package/src/repository.ts +306 -0
- package/src/search.ts +282 -0
- package/src/settings.ts +59 -0
package/src/build.ts
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import { mkdir, open, rm } from "node:fs/promises";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { Blobs, inventory, resolveCommit, type Identity } from "./git.js";
|
|
6
|
+
import { chunk, CHUNKER, tokens } from "./chunk.js";
|
|
7
|
+
import {
|
|
8
|
+
atomic,
|
|
9
|
+
canonical,
|
|
10
|
+
hash,
|
|
11
|
+
invariant,
|
|
12
|
+
lineAt,
|
|
13
|
+
json,
|
|
14
|
+
utf8,
|
|
15
|
+
type Doc,
|
|
16
|
+
optionalJson,
|
|
17
|
+
} from "./common.js";
|
|
18
|
+
export const INDEX_CONFIGS = {
|
|
19
|
+
kind: { type: "keyword" },
|
|
20
|
+
path: { type: "keyword" },
|
|
21
|
+
language: { type: "keyword" },
|
|
22
|
+
fileId: { type: "keyword" },
|
|
23
|
+
symbol: { type: "keyword" },
|
|
24
|
+
searchText: { type: "text", analyzers: ["standard"] },
|
|
25
|
+
} as const;
|
|
26
|
+
export type Preset = {
|
|
27
|
+
schemaVersion: 1;
|
|
28
|
+
chunker: typeof CHUNKER;
|
|
29
|
+
mode: "syntax" | "window";
|
|
30
|
+
maxFileBytes: number;
|
|
31
|
+
embedding: { model: string; dimensions: number } | null;
|
|
32
|
+
};
|
|
33
|
+
export const PRESET: Preset = {
|
|
34
|
+
schemaVersion: 1,
|
|
35
|
+
chunker: CHUNKER,
|
|
36
|
+
mode: "syntax",
|
|
37
|
+
maxFileBytes: 1024 * 1024,
|
|
38
|
+
embedding: null,
|
|
39
|
+
};
|
|
40
|
+
export type InventoryItem = {
|
|
41
|
+
path: string;
|
|
42
|
+
pathBase64: string;
|
|
43
|
+
oid: string;
|
|
44
|
+
mode: string;
|
|
45
|
+
bytes: number;
|
|
46
|
+
status: "included" | "excluded";
|
|
47
|
+
reason?: string;
|
|
48
|
+
targetBase64?: string;
|
|
49
|
+
fileId?: string;
|
|
50
|
+
chunkIds?: string[];
|
|
51
|
+
contentHash?: string;
|
|
52
|
+
parseStatus?: string;
|
|
53
|
+
tokens?: number;
|
|
54
|
+
embedded?: number;
|
|
55
|
+
skipped?: number;
|
|
56
|
+
};
|
|
57
|
+
export type Build = {
|
|
58
|
+
format: 1;
|
|
59
|
+
directory: string;
|
|
60
|
+
repoKey: string;
|
|
61
|
+
configHash: string;
|
|
62
|
+
preset: Preset;
|
|
63
|
+
commitOid: string;
|
|
64
|
+
commitTime: string;
|
|
65
|
+
requestedRef: string;
|
|
66
|
+
branchRef?: string;
|
|
67
|
+
buildId: string;
|
|
68
|
+
inventory: InventoryItem[];
|
|
69
|
+
recordHashes: Record<string, string>;
|
|
70
|
+
recordsHash: string;
|
|
71
|
+
inventoryHash: string;
|
|
72
|
+
changes: {
|
|
73
|
+
added: string[];
|
|
74
|
+
modified: string[];
|
|
75
|
+
deleted: string[];
|
|
76
|
+
unchanged: string[];
|
|
77
|
+
};
|
|
78
|
+
obsoleteIds: string[];
|
|
79
|
+
counts: Record<string, number>;
|
|
80
|
+
};
|
|
81
|
+
export type Embedder = {
|
|
82
|
+
model: string;
|
|
83
|
+
dimensions: number;
|
|
84
|
+
embed: (text: string) => Promise<number[]>;
|
|
85
|
+
};
|
|
86
|
+
export async function* records(directory: string): AsyncGenerator<Doc> {
|
|
87
|
+
const input = createReadStream(join(directory, "records.jsonl"), {
|
|
88
|
+
encoding: "utf8",
|
|
89
|
+
});
|
|
90
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
91
|
+
try {
|
|
92
|
+
for await (const line of lines) if (line) yield JSON.parse(line) as Doc;
|
|
93
|
+
} finally {
|
|
94
|
+
lines.close();
|
|
95
|
+
input.destroy();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export async function loadBuild(directory: string): Promise<Build> {
|
|
99
|
+
const b = await json<Build>(join(directory, "build.json"));
|
|
100
|
+
invariant(
|
|
101
|
+
b.format === 1 && b.configHash === hash(b.preset),
|
|
102
|
+
"Invalid build configuration.",
|
|
103
|
+
);
|
|
104
|
+
b.directory = resolve(directory);
|
|
105
|
+
return b;
|
|
106
|
+
}
|
|
107
|
+
function exclude(path: string): string | undefined {
|
|
108
|
+
if (
|
|
109
|
+
/(^|\/)(node_modules|vendor|vendored|dist|build|target|coverage|\.git|\.next)(\/|$)/.test(
|
|
110
|
+
path,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
return "dependency-or-build-output";
|
|
114
|
+
if (
|
|
115
|
+
/\.(png|jpe?g|gif|ico|pdf|zip|gz|jar|class|woff2?|mp[34]|exe|dll|so|dylib|wasm)$/i.test(
|
|
116
|
+
path,
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
return "binary-extension";
|
|
120
|
+
if (/\.(min\.(js|css)|map)$/.test(path)) return "generated-output";
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
export async function materialize(args: {
|
|
124
|
+
identity: Identity;
|
|
125
|
+
ref: string;
|
|
126
|
+
output: string;
|
|
127
|
+
previous?: Build;
|
|
128
|
+
preset?: Preset;
|
|
129
|
+
embedder?: Embedder;
|
|
130
|
+
cache?: string;
|
|
131
|
+
}): Promise<Build> {
|
|
132
|
+
const preset = args.preset ?? PRESET,
|
|
133
|
+
configHash = hash(preset),
|
|
134
|
+
prev = args.previous;
|
|
135
|
+
invariant(
|
|
136
|
+
hash(preset.chunker) === hash(CHUNKER) &&
|
|
137
|
+
["syntax", "window"].includes(preset.mode) &&
|
|
138
|
+
preset.maxFileBytes === PRESET.maxFileBytes,
|
|
139
|
+
"Unsupported build preset.",
|
|
140
|
+
);
|
|
141
|
+
if (prev) {
|
|
142
|
+
invariant(
|
|
143
|
+
prev.repoKey === args.identity.key && prev.configHash === configHash,
|
|
144
|
+
"Previous build repository/config mismatch.",
|
|
145
|
+
);
|
|
146
|
+
await validateBuild(prev);
|
|
147
|
+
}
|
|
148
|
+
invariant(
|
|
149
|
+
!preset.embedding ||
|
|
150
|
+
(args.embedder?.model === preset.embedding.model &&
|
|
151
|
+
args.embedder.dimensions === preset.embedding.dimensions),
|
|
152
|
+
"Embedding provider must match the pinned preset.",
|
|
153
|
+
);
|
|
154
|
+
const commit = await resolveCommit(args.identity.path, args.ref);
|
|
155
|
+
const directory = resolve(args.output);
|
|
156
|
+
await mkdir(directory, { recursive: false, mode: 0o700 });
|
|
157
|
+
const output = await open(join(directory, "records.jsonl"), "wx", 0o600);
|
|
158
|
+
const recordHashes: Record<string, string> = {};
|
|
159
|
+
let writeError = false;
|
|
160
|
+
async function emit(doc: Doc) {
|
|
161
|
+
const data = canonical(doc);
|
|
162
|
+
invariant(
|
|
163
|
+
Buffer.byteLength(data) <= 4_800_000,
|
|
164
|
+
`Serialized document exceeds safe limit: ${doc.id}`,
|
|
165
|
+
);
|
|
166
|
+
invariant(!recordHashes[doc.id], "Duplicate document ID.");
|
|
167
|
+
recordHashes[doc.id] = hash(doc);
|
|
168
|
+
await output.write(data + "\n");
|
|
169
|
+
}
|
|
170
|
+
const items: InventoryItem[] = [];
|
|
171
|
+
const blobs = new Blobs(args.identity.path);
|
|
172
|
+
const old = new Map(prev?.inventory.map((e) => [e.pathBase64, e]) ?? []);
|
|
173
|
+
const reuse = new Set<string>();
|
|
174
|
+
try {
|
|
175
|
+
const entries = await inventory(args.identity.path, commit.oid);
|
|
176
|
+
for (const e of entries) {
|
|
177
|
+
const item: InventoryItem = {
|
|
178
|
+
path: e.path,
|
|
179
|
+
pathBase64: e.pathBase64,
|
|
180
|
+
oid: e.oid,
|
|
181
|
+
mode: e.mode,
|
|
182
|
+
bytes: e.bytes,
|
|
183
|
+
status: "excluded",
|
|
184
|
+
};
|
|
185
|
+
items.push(item);
|
|
186
|
+
if (!e.validPath) {
|
|
187
|
+
item.reason = "invalid-utf8-path";
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (e.mode === "160000") {
|
|
191
|
+
item.reason = "submodule";
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (e.bytes > preset.maxFileBytes) {
|
|
195
|
+
item.reason = "oversized";
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (e.mode === "120000") {
|
|
199
|
+
item.reason = "symlink";
|
|
200
|
+
item.targetBase64 = (await blobs.read(e)).toString("base64");
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const reason = exclude(e.path);
|
|
204
|
+
if (reason) {
|
|
205
|
+
item.reason = reason;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
invariant(e.type === "blob", "Unsupported Git entry type.");
|
|
209
|
+
const former = old.get(e.pathBase64);
|
|
210
|
+
if (
|
|
211
|
+
former?.status === "included" &&
|
|
212
|
+
former.oid === e.oid &&
|
|
213
|
+
former.mode === e.mode
|
|
214
|
+
) {
|
|
215
|
+
Object.assign(item, former);
|
|
216
|
+
for (const id of [former.fileId!, ...former.chunkIds!]) reuse.add(id);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const data = await blobs.read(e);
|
|
220
|
+
if (data.includes(0)) {
|
|
221
|
+
item.reason = "binary-content";
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const source = utf8(data);
|
|
225
|
+
if (source === undefined) {
|
|
226
|
+
item.reason = "invalid-utf8-content";
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (
|
|
230
|
+
source.startsWith("version https://git-lfs.github.com/spec/v1\n") ||
|
|
231
|
+
source.startsWith("version https://git-lfs.github.com/spec/v1\r\n")
|
|
232
|
+
) {
|
|
233
|
+
item.reason = "lfs-pointer";
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const parsed = await chunk(source, e.path, preset.mode);
|
|
237
|
+
const contentHash = hash(data);
|
|
238
|
+
const fileId = `f-${hash([e.pathBase64, contentHash])}`;
|
|
239
|
+
const base = { schemaVersion: 1, configHash };
|
|
240
|
+
const file: Doc = {
|
|
241
|
+
...base,
|
|
242
|
+
id: fileId,
|
|
243
|
+
kind: "file",
|
|
244
|
+
path: e.path,
|
|
245
|
+
blobOid: e.oid,
|
|
246
|
+
contentHash,
|
|
247
|
+
byteLength: data.length,
|
|
248
|
+
sourceText: source,
|
|
249
|
+
language: parsed.language,
|
|
250
|
+
parseStatus: parsed.parseStatus,
|
|
251
|
+
chunkCount: parsed.spans.length,
|
|
252
|
+
};
|
|
253
|
+
if (Buffer.byteLength(canonical(file)) > 4_800_000) {
|
|
254
|
+
item.reason = "serialized-source-too-large";
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
Object.assign(item, {
|
|
258
|
+
status: "included",
|
|
259
|
+
contentHash,
|
|
260
|
+
fileId,
|
|
261
|
+
chunkIds: [],
|
|
262
|
+
parseStatus: parsed.parseStatus,
|
|
263
|
+
tokens: 0,
|
|
264
|
+
embedded: 0,
|
|
265
|
+
skipped: 0,
|
|
266
|
+
});
|
|
267
|
+
await emit(file);
|
|
268
|
+
for (const [ordinal, span] of parsed.spans.entries()) {
|
|
269
|
+
const id = `c-${hash([fileId, configHash, span.startByte, span.endByte, ordinal])}`;
|
|
270
|
+
const doc: Doc = {
|
|
271
|
+
...base,
|
|
272
|
+
id,
|
|
273
|
+
kind: "chunk",
|
|
274
|
+
fileId,
|
|
275
|
+
path: e.path,
|
|
276
|
+
contentHash,
|
|
277
|
+
language: parsed.language,
|
|
278
|
+
...span,
|
|
279
|
+
ordinal,
|
|
280
|
+
embeddingStatus: "skipped",
|
|
281
|
+
embeddingSkipReason: preset.embedding
|
|
282
|
+
? "structural-only"
|
|
283
|
+
: "embedding-disabled",
|
|
284
|
+
};
|
|
285
|
+
const eligible = !["imports", "structural"].includes(span.chunkKind);
|
|
286
|
+
if (preset.embedding && eligible) {
|
|
287
|
+
const inputHash = hash([preset.embedding, span.searchText]);
|
|
288
|
+
const cachePath = args.cache
|
|
289
|
+
? join(args.cache, inputHash + ".json")
|
|
290
|
+
: undefined;
|
|
291
|
+
let vector = cachePath
|
|
292
|
+
? await optionalJson<number[]>(cachePath)
|
|
293
|
+
: undefined;
|
|
294
|
+
if (!vector) {
|
|
295
|
+
vector = await args.embedder!.embed(span.searchText);
|
|
296
|
+
invariant(
|
|
297
|
+
vector.length === preset.embedding.dimensions &&
|
|
298
|
+
vector.every(Number.isFinite),
|
|
299
|
+
"Invalid embedding output.",
|
|
300
|
+
);
|
|
301
|
+
if (cachePath) await atomic(cachePath, vector);
|
|
302
|
+
}
|
|
303
|
+
invariant(
|
|
304
|
+
vector.length === preset.embedding.dimensions &&
|
|
305
|
+
vector.every(Number.isFinite),
|
|
306
|
+
"Invalid cached vector.",
|
|
307
|
+
);
|
|
308
|
+
doc.embedding = vector;
|
|
309
|
+
doc.embeddingInputHash = inputHash;
|
|
310
|
+
doc.embeddingStatus = "embedded";
|
|
311
|
+
delete doc.embeddingSkipReason;
|
|
312
|
+
item.embedded!++;
|
|
313
|
+
} else {
|
|
314
|
+
doc.embeddingSkipReason = preset.embedding
|
|
315
|
+
? span.chunkKind === "imports"
|
|
316
|
+
? "imports-only"
|
|
317
|
+
: "structural-only"
|
|
318
|
+
: "embedding-disabled";
|
|
319
|
+
item.skipped!++;
|
|
320
|
+
}
|
|
321
|
+
item.chunkIds!.push(id);
|
|
322
|
+
item.tokens! += span.tokenCount;
|
|
323
|
+
await emit(doc);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (prev && reuse.size)
|
|
327
|
+
for await (const doc of records(prev.directory))
|
|
328
|
+
if (reuse.has(doc.id)) {
|
|
329
|
+
await emit(doc);
|
|
330
|
+
reuse.delete(doc.id);
|
|
331
|
+
}
|
|
332
|
+
invariant(reuse.size === 0, "Previous artifact lacks retained records.");
|
|
333
|
+
} catch (e) {
|
|
334
|
+
writeError = true;
|
|
335
|
+
throw e;
|
|
336
|
+
} finally {
|
|
337
|
+
blobs.close();
|
|
338
|
+
await output.close();
|
|
339
|
+
if (writeError) await rm(directory, { recursive: true, force: true });
|
|
340
|
+
}
|
|
341
|
+
const changes: Build["changes"] = {
|
|
342
|
+
added: [],
|
|
343
|
+
modified: [],
|
|
344
|
+
deleted: [],
|
|
345
|
+
unchanged: [],
|
|
346
|
+
};
|
|
347
|
+
const now = new Map(items.map((e) => [e.pathBase64, e]));
|
|
348
|
+
for (const e of items) {
|
|
349
|
+
const o = old.get(e.pathBase64);
|
|
350
|
+
changes[
|
|
351
|
+
!o
|
|
352
|
+
? "added"
|
|
353
|
+
: o.oid === e.oid && o.mode === e.mode && o.status === e.status
|
|
354
|
+
? "unchanged"
|
|
355
|
+
: "modified"
|
|
356
|
+
].push(e.path);
|
|
357
|
+
}
|
|
358
|
+
for (const e of old.values())
|
|
359
|
+
if (!now.has(e.pathBase64)) changes.deleted.push(e.path);
|
|
360
|
+
const build: Build = {
|
|
361
|
+
format: 1,
|
|
362
|
+
directory,
|
|
363
|
+
repoKey: args.identity.key,
|
|
364
|
+
configHash,
|
|
365
|
+
preset,
|
|
366
|
+
commitOid: commit.oid,
|
|
367
|
+
commitTime: commit.time,
|
|
368
|
+
requestedRef: args.ref,
|
|
369
|
+
branchRef: commit.branch,
|
|
370
|
+
buildId: hash([
|
|
371
|
+
args.identity.key,
|
|
372
|
+
commit.oid,
|
|
373
|
+
configHash,
|
|
374
|
+
"srcx-builder-v1",
|
|
375
|
+
]),
|
|
376
|
+
inventory: items,
|
|
377
|
+
recordHashes,
|
|
378
|
+
recordsHash: hash(
|
|
379
|
+
Object.entries(recordHashes).sort(([a], [b]) => a.localeCompare(b)),
|
|
380
|
+
),
|
|
381
|
+
inventoryHash: hash(items),
|
|
382
|
+
changes,
|
|
383
|
+
obsoleteIds: Object.keys(prev?.recordHashes ?? {}).filter(
|
|
384
|
+
(id) => !recordHashes[id],
|
|
385
|
+
),
|
|
386
|
+
counts: {
|
|
387
|
+
files: items.filter((e) => e.status === "included").length,
|
|
388
|
+
excluded: items.filter((e) => e.status === "excluded").length,
|
|
389
|
+
chunks: items.reduce((n, e) => n + (e.chunkIds?.length ?? 0), 0),
|
|
390
|
+
tokens: items.reduce((n, e) => n + (e.tokens ?? 0), 0),
|
|
391
|
+
embedded: items.reduce((n, e) => n + (e.embedded ?? 0), 0),
|
|
392
|
+
skipped: items.reduce((n, e) => n + (e.skipped ?? 0), 0),
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
await atomic(join(directory, "build.json"), build);
|
|
396
|
+
await validateBuild(build);
|
|
397
|
+
return build;
|
|
398
|
+
}
|
|
399
|
+
export async function validateBuild(b: Build): Promise<void> {
|
|
400
|
+
invariant(
|
|
401
|
+
typeof b.commitOid === "string" &&
|
|
402
|
+
/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(b.commitOid) &&
|
|
403
|
+
b.buildId ===
|
|
404
|
+
hash([b.repoKey, b.commitOid, b.configHash, "srcx-builder-v1"]),
|
|
405
|
+
"Build commit identity mismatch; rebuild from the intended Git commit.",
|
|
406
|
+
);
|
|
407
|
+
invariant(
|
|
408
|
+
hash(b.preset) === b.configHash && hash(b.inventory) === b.inventoryHash,
|
|
409
|
+
"Build metadata hash mismatch.",
|
|
410
|
+
);
|
|
411
|
+
const seen = new Set<string>();
|
|
412
|
+
let activeFile: Doc | undefined;
|
|
413
|
+
let end = 0,
|
|
414
|
+
count = 0;
|
|
415
|
+
const expectedFile = new Map(
|
|
416
|
+
b.inventory
|
|
417
|
+
.filter((e) => e.status === "included")
|
|
418
|
+
.map((e) => [e.fileId!, e]),
|
|
419
|
+
);
|
|
420
|
+
const completedFiles = new Set<string>();
|
|
421
|
+
const finish = () => {
|
|
422
|
+
if (activeFile) {
|
|
423
|
+
const entry = expectedFile.get(activeFile.id)!;
|
|
424
|
+
invariant(
|
|
425
|
+
count === entry.chunkIds!.length &&
|
|
426
|
+
end === Buffer.byteLength(activeFile.sourceText as string),
|
|
427
|
+
"Incomplete source coverage.",
|
|
428
|
+
);
|
|
429
|
+
completedFiles.add(activeFile.id);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
for await (const doc of records(b.directory)) {
|
|
433
|
+
invariant(
|
|
434
|
+
!seen.has(doc.id) && b.recordHashes[doc.id] === hash(doc),
|
|
435
|
+
"Corrupted or unexpected artifact record.",
|
|
436
|
+
);
|
|
437
|
+
seen.add(doc.id);
|
|
438
|
+
invariant(
|
|
439
|
+
doc.configHash === b.configHash && doc.schemaVersion === 1,
|
|
440
|
+
"Mixed configuration.",
|
|
441
|
+
);
|
|
442
|
+
if (doc.kind === "file") {
|
|
443
|
+
finish();
|
|
444
|
+
activeFile = doc;
|
|
445
|
+
end = 0;
|
|
446
|
+
count = 0;
|
|
447
|
+
const entry = expectedFile.get(doc.id);
|
|
448
|
+
const raw = Buffer.from(doc.sourceText as string);
|
|
449
|
+
invariant(
|
|
450
|
+
entry &&
|
|
451
|
+
hash(raw) === doc.contentHash &&
|
|
452
|
+
entry.contentHash === doc.contentHash &&
|
|
453
|
+
entry.path === doc.path &&
|
|
454
|
+
entry.oid === doc.blobOid &&
|
|
455
|
+
entry.bytes === raw.length &&
|
|
456
|
+
doc.byteLength === raw.length &&
|
|
457
|
+
doc.chunkCount === entry.chunkIds?.length &&
|
|
458
|
+
doc.id === `f-${hash([entry.pathBase64, doc.contentHash])}`,
|
|
459
|
+
"Invalid file content or inventory binding.",
|
|
460
|
+
);
|
|
461
|
+
} else {
|
|
462
|
+
invariant(
|
|
463
|
+
doc.kind === "chunk" && activeFile && doc.fileId === activeFile.id,
|
|
464
|
+
"Orphan chunk.",
|
|
465
|
+
);
|
|
466
|
+
const a = doc.startByte as number,
|
|
467
|
+
z = doc.endByte as number;
|
|
468
|
+
invariant(
|
|
469
|
+
Number.isInteger(a) &&
|
|
470
|
+
Number.isInteger(z) &&
|
|
471
|
+
a >= 0 &&
|
|
472
|
+
a <= end &&
|
|
473
|
+
z > a &&
|
|
474
|
+
z <= Buffer.byteLength(activeFile.sourceText as string),
|
|
475
|
+
"Invalid chunk range.",
|
|
476
|
+
);
|
|
477
|
+
invariant(
|
|
478
|
+
expectedFile.get(activeFile.id)!.chunkIds!.includes(doc.id),
|
|
479
|
+
"Unexpected chunk membership.",
|
|
480
|
+
);
|
|
481
|
+
const raw = Buffer.from(activeFile.sourceText as string);
|
|
482
|
+
const slice = raw.subarray(a, z);
|
|
483
|
+
invariant(
|
|
484
|
+
utf8(slice) !== undefined &&
|
|
485
|
+
doc.path === activeFile.path &&
|
|
486
|
+
doc.contentHash === activeFile.contentHash &&
|
|
487
|
+
doc.ordinal === count &&
|
|
488
|
+
doc.id === `c-${hash([activeFile.id, b.configHash, a, z, count])}` &&
|
|
489
|
+
doc.startLine === lineAt(raw, a) &&
|
|
490
|
+
doc.endLine === lineAt(raw, z - 1) &&
|
|
491
|
+
typeof doc.searchText === "string" &&
|
|
492
|
+
doc.searchText.endsWith(slice.toString("utf8")) &&
|
|
493
|
+
doc.tokenCount === tokens(doc.searchText) &&
|
|
494
|
+
Number(doc.tokenCount) <= CHUNKER.maxTokens,
|
|
495
|
+
"Invalid chunk source/position/token binding.",
|
|
496
|
+
);
|
|
497
|
+
if (doc.embeddingStatus === "embedded")
|
|
498
|
+
invariant(
|
|
499
|
+
b.preset.embedding &&
|
|
500
|
+
Array.isArray(doc.embedding) &&
|
|
501
|
+
doc.embedding.length === b.preset.embedding.dimensions &&
|
|
502
|
+
doc.embedding.every(Number.isFinite),
|
|
503
|
+
"Invalid embedded chunk.",
|
|
504
|
+
);
|
|
505
|
+
else
|
|
506
|
+
invariant(
|
|
507
|
+
doc.embeddingStatus === "skipped" &&
|
|
508
|
+
typeof doc.embeddingSkipReason === "string" &&
|
|
509
|
+
doc.embedding === undefined,
|
|
510
|
+
"Unaccounted embedding state.",
|
|
511
|
+
);
|
|
512
|
+
end = Math.max(end, z);
|
|
513
|
+
count++;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
finish();
|
|
517
|
+
invariant(
|
|
518
|
+
completedFiles.size === expectedFile.size,
|
|
519
|
+
"Missing inventory file records.",
|
|
520
|
+
);
|
|
521
|
+
invariant(
|
|
522
|
+
seen.size === Object.keys(b.recordHashes).length,
|
|
523
|
+
"Missing artifact records.",
|
|
524
|
+
);
|
|
525
|
+
invariant(
|
|
526
|
+
hash(
|
|
527
|
+
Object.entries(b.recordHashes).sort(([a], [b]) => a.localeCompare(b)),
|
|
528
|
+
) === b.recordsHash,
|
|
529
|
+
"Record inventory hash mismatch.",
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
export function corpusMetadata(
|
|
533
|
+
b: Build,
|
|
534
|
+
repoId: string,
|
|
535
|
+
indexId: string,
|
|
536
|
+
attemptId: string,
|
|
537
|
+
): Doc[] {
|
|
538
|
+
const parts: Doc[] = [];
|
|
539
|
+
let entries: InventoryItem[] = [];
|
|
540
|
+
let bytes = 0;
|
|
541
|
+
const flush = () => {
|
|
542
|
+
if (entries.length) {
|
|
543
|
+
const n = parts.length;
|
|
544
|
+
parts.push({
|
|
545
|
+
id: `inventory-${n}`,
|
|
546
|
+
kind: "manifest",
|
|
547
|
+
role: "inventory",
|
|
548
|
+
schemaVersion: 1,
|
|
549
|
+
configHash: b.configHash,
|
|
550
|
+
partOrdinal: n,
|
|
551
|
+
partHash: hash(entries),
|
|
552
|
+
entries,
|
|
553
|
+
});
|
|
554
|
+
entries = [];
|
|
555
|
+
bytes = 0;
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
for (const entry of b.inventory) {
|
|
559
|
+
const size = Buffer.byteLength(canonical(entry));
|
|
560
|
+
invariant(size < 4_000_000, "Single inventory entry too large.");
|
|
561
|
+
if (bytes + size > 1_000_000) flush();
|
|
562
|
+
entries.push(entry);
|
|
563
|
+
bytes += size;
|
|
564
|
+
}
|
|
565
|
+
flush();
|
|
566
|
+
const root: Doc = {
|
|
567
|
+
id: "__manifest__",
|
|
568
|
+
kind: "manifest",
|
|
569
|
+
role: "corpus",
|
|
570
|
+
schemaVersion: 1,
|
|
571
|
+
configHash: b.configHash,
|
|
572
|
+
repoId,
|
|
573
|
+
indexId,
|
|
574
|
+
commitOid: b.commitOid,
|
|
575
|
+
commitTime: b.commitTime,
|
|
576
|
+
// Normalize the remote manifest so two ref spellings of one commit reuse it.
|
|
577
|
+
requestedRef: b.commitOid,
|
|
578
|
+
config: b.preset,
|
|
579
|
+
inventoryHash: b.inventoryHash,
|
|
580
|
+
recordsHash: b.recordsHash,
|
|
581
|
+
counts: b.counts,
|
|
582
|
+
buildId: b.buildId,
|
|
583
|
+
attemptId,
|
|
584
|
+
inventoryPartIds: parts.map((p) => p.id),
|
|
585
|
+
};
|
|
586
|
+
return [...parts, root];
|
|
587
|
+
}
|