@functional-systems/srcx 0.1.0-dev.4 → 0.1.0-dev.40
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 +179 -0
- package/CONTRIBUTING.md +4 -0
- package/DESIGN.md +378 -58
- package/README.md +628 -35
- package/RELEASING.md +15 -3
- package/VALIDATION.md +1120 -16
- package/dist/artifact.d.ts +31 -0
- package/dist/artifact.js +237 -0
- package/dist/artifact.js.map +1 -0
- package/dist/build.d.ts +86 -3
- package/dist/build.js +233 -71
- package/dist/build.js.map +1 -1
- package/dist/chunk.d.ts +9 -3
- package/dist/chunk.js +341 -14
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +130 -21
- package/dist/cli.js.map +1 -1
- package/dist/file-policy.d.ts +20 -0
- package/dist/file-policy.js +103 -0
- package/dist/file-policy.js.map +1 -0
- package/dist/implementation.d.ts +11 -0
- package/dist/implementation.js +191 -0
- package/dist/implementation.js.map +1 -0
- package/dist/publish.d.ts +18 -0
- package/dist/publish.js +224 -46
- package/dist/publish.js.map +1 -1
- package/dist/releases.js +21 -3
- package/dist/releases.js.map +1 -1
- package/dist/remote.d.ts +1 -0
- package/dist/remote.js +50 -9
- package/dist/remote.js.map +1 -1
- package/dist/repository.d.ts +4 -1
- package/dist/repository.js +26 -11
- package/dist/repository.js.map +1 -1
- package/dist/rerank.d.ts +16 -0
- package/dist/rerank.js +87 -0
- package/dist/rerank.js.map +1 -0
- package/dist/search.d.ts +18 -3
- package/dist/search.js +143 -23
- package/dist/search.js.map +1 -1
- package/dist/skills.d.ts +24 -0
- package/dist/skills.js +155 -0
- package/dist/skills.js.map +1 -0
- package/package.json +17 -6
- package/runtime/grammars/README.md +50 -0
- package/runtime/grammars/tree-sitter-bash.LICENSE +21 -0
- package/runtime/grammars/tree-sitter-bash.wasm +0 -0
- package/runtime/grammars/tree-sitter-sql.LICENSE +21 -0
- package/runtime/grammars/tree-sitter-sql.wasm +0 -0
- package/runtime/grammars/tree-sitter-wasm.LICENSE +21 -0
- package/runtime/qwen.py +132 -0
- package/runtime/requirements.txt +34 -0
- package/skills/srcx-search/SKILL.md +103 -0
- package/src/artifact.ts +303 -0
- package/src/build.ts +313 -84
- package/src/chunk.ts +360 -12
- package/src/cli.ts +263 -23
- package/src/file-policy.ts +166 -0
- package/src/implementation.ts +253 -0
- package/src/publish.ts +321 -47
- package/src/releases.ts +36 -3
- package/src/remote.ts +70 -13
- package/src/repository.ts +40 -11
- package/src/rerank.ts +131 -0
- package/src/search.ts +229 -28
- package/src/skills.ts +205 -0
package/src/build.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { mkdir,
|
|
2
|
-
import { createReadStream } from "node:fs";
|
|
3
|
-
import { createInterface } from "node:readline";
|
|
1
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
4
2
|
import { join, resolve } from "node:path";
|
|
5
3
|
import { Blobs, inventory, resolveCommit, type Identity } from "./git.js";
|
|
6
4
|
import { chunk, CHUNKER, tokens } from "./chunk.js";
|
|
5
|
+
import {
|
|
6
|
+
ArtifactWriter,
|
|
7
|
+
artifactRecords,
|
|
8
|
+
validateStorage,
|
|
9
|
+
type ArtifactStorage,
|
|
10
|
+
type RecordPart,
|
|
11
|
+
} from "./artifact.js";
|
|
7
12
|
import {
|
|
8
13
|
atomic,
|
|
9
14
|
canonical,
|
|
@@ -15,6 +20,26 @@ import {
|
|
|
15
20
|
type Doc,
|
|
16
21
|
optionalJson,
|
|
17
22
|
} from "./common.js";
|
|
23
|
+
import {
|
|
24
|
+
FILE_POLICY,
|
|
25
|
+
normalizeFilePolicy,
|
|
26
|
+
hardExclusion,
|
|
27
|
+
privateKeyContent,
|
|
28
|
+
fileDecision,
|
|
29
|
+
type FilePolicy,
|
|
30
|
+
type FileDecision,
|
|
31
|
+
} from "./file-policy.js";
|
|
32
|
+
export const ANALYZERS = ["english", "japanese", "korean", "standard"] as const;
|
|
33
|
+
export type Analyzer = (typeof ANALYZERS)[number];
|
|
34
|
+
export function normalizeAnalyzers(value: unknown): Analyzer[] {
|
|
35
|
+
invariant(
|
|
36
|
+
Array.isArray(value) &&
|
|
37
|
+
value.length > 0 &&
|
|
38
|
+
value.every((name) => ANALYZERS.includes(name)),
|
|
39
|
+
`Analyzers must be a nonempty list of: ${ANALYZERS.join(", ")}.`,
|
|
40
|
+
);
|
|
41
|
+
return [...new Set(value as Analyzer[])].sort();
|
|
42
|
+
}
|
|
18
43
|
export const INDEX_CONFIGS = {
|
|
19
44
|
kind: { type: "keyword" },
|
|
20
45
|
path: { type: "keyword" },
|
|
@@ -25,18 +50,123 @@ export const INDEX_CONFIGS = {
|
|
|
25
50
|
} as const;
|
|
26
51
|
export type Preset = {
|
|
27
52
|
schemaVersion: 1;
|
|
53
|
+
analyzers: readonly Analyzer[];
|
|
28
54
|
chunker: typeof CHUNKER;
|
|
29
55
|
mode: "syntax" | "window";
|
|
56
|
+
/** Internal evaluation override; the CLI keeps its original enrichment. */
|
|
57
|
+
enrichment?: "path-only-v1";
|
|
30
58
|
maxFileBytes: number;
|
|
31
|
-
|
|
59
|
+
filePolicy: FilePolicy;
|
|
60
|
+
embedding: {
|
|
61
|
+
model: string;
|
|
62
|
+
dimensions: number;
|
|
63
|
+
managed?: true;
|
|
64
|
+
provider?: "openai";
|
|
65
|
+
similarity?: "cosine";
|
|
66
|
+
sourceField?: "embeddingText";
|
|
67
|
+
} | null;
|
|
32
68
|
};
|
|
33
69
|
export const PRESET: Preset = {
|
|
34
70
|
schemaVersion: 1,
|
|
71
|
+
analyzers: ["standard"],
|
|
35
72
|
chunker: CHUNKER,
|
|
36
73
|
mode: "syntax",
|
|
37
74
|
maxFileBytes: 1024 * 1024,
|
|
75
|
+
filePolicy: FILE_POLICY,
|
|
38
76
|
embedding: null,
|
|
39
77
|
};
|
|
78
|
+
/** Embedding variants share the same chunking behavior. */
|
|
79
|
+
export const MANAGED_PRESET: Preset = {
|
|
80
|
+
...PRESET,
|
|
81
|
+
embedding: {
|
|
82
|
+
managed: true,
|
|
83
|
+
provider: "openai",
|
|
84
|
+
model: "text-embedding-3-small",
|
|
85
|
+
dimensions: 1536,
|
|
86
|
+
similarity: "cosine",
|
|
87
|
+
sourceField: "embeddingText",
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
export const MANAGED_LARGE_PRESET: Preset = {
|
|
91
|
+
...MANAGED_PRESET,
|
|
92
|
+
embedding: {
|
|
93
|
+
...MANAGED_PRESET.embedding!,
|
|
94
|
+
model: "text-embedding-3-large",
|
|
95
|
+
dimensions: 3072,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
export function presetFor(
|
|
99
|
+
embedding = "none",
|
|
100
|
+
analyzers: readonly string[] = PRESET.analyzers,
|
|
101
|
+
filePolicy: unknown = FILE_POLICY,
|
|
102
|
+
): Preset {
|
|
103
|
+
invariant(
|
|
104
|
+
["none", "text-embedding-3-small", "text-embedding-3-large"].includes(
|
|
105
|
+
embedding,
|
|
106
|
+
),
|
|
107
|
+
"Embedding must be none, text-embedding-3-small or text-embedding-3-large.",
|
|
108
|
+
);
|
|
109
|
+
const base =
|
|
110
|
+
embedding === "none"
|
|
111
|
+
? PRESET
|
|
112
|
+
: embedding === "text-embedding-3-large"
|
|
113
|
+
? MANAGED_LARGE_PRESET
|
|
114
|
+
: MANAGED_PRESET;
|
|
115
|
+
const normalized = normalizeAnalyzers(analyzers);
|
|
116
|
+
const configured = {
|
|
117
|
+
...base,
|
|
118
|
+
analyzers: normalized,
|
|
119
|
+
filePolicy: normalizeFilePolicy(filePolicy),
|
|
120
|
+
};
|
|
121
|
+
return hash(configured) === hash(base) ? base : configured;
|
|
122
|
+
}
|
|
123
|
+
export function supportedPreset(preset: unknown): preset is Preset {
|
|
124
|
+
if (!preset || typeof preset !== "object" || Array.isArray(preset))
|
|
125
|
+
return false;
|
|
126
|
+
try {
|
|
127
|
+
const analyzers = normalizeAnalyzers((preset as Preset).analyzers);
|
|
128
|
+
const filePolicy = normalizeFilePolicy((preset as Preset).filePolicy);
|
|
129
|
+
return [PRESET, MANAGED_PRESET, MANAGED_LARGE_PRESET].some(
|
|
130
|
+
(base) => hash({ ...base, analyzers, filePolicy }) === hash(preset),
|
|
131
|
+
);
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export function indexConfigs(preset: Preset = PRESET) {
|
|
137
|
+
const text = { type: "text" as const, analyzers: [...preset.analyzers] };
|
|
138
|
+
const indexes = { ...INDEX_CONFIGS, searchText: text };
|
|
139
|
+
if (!preset.embedding?.managed) return indexes;
|
|
140
|
+
const { managed: _, ...embedding } = preset.embedding;
|
|
141
|
+
return {
|
|
142
|
+
...indexes,
|
|
143
|
+
embeddingText: { ...text, analyzers: [...preset.analyzers] },
|
|
144
|
+
embedding: { type: "vector", managedEmbedding: true, embedding },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** Validate server enrichment separately; all other payload fields remain exact. */
|
|
148
|
+
export function recordHash(doc: Doc, preset: Preset): string {
|
|
149
|
+
if (!preset.embedding?.managed) return hash(doc);
|
|
150
|
+
const { embedding, ...source } = doc;
|
|
151
|
+
if (doc.kind === "chunk" && doc.embeddingStatus === "managed") {
|
|
152
|
+
invariant(
|
|
153
|
+
typeof doc.embeddingText === "string" &&
|
|
154
|
+
doc.embeddingText === doc.searchText &&
|
|
155
|
+
doc.embeddingInputHash ===
|
|
156
|
+
hash([preset.embedding, doc.embeddingText]) &&
|
|
157
|
+
Array.isArray(embedding) &&
|
|
158
|
+
embedding.length === preset.embedding.dimensions &&
|
|
159
|
+
embedding.every((v) => typeof v === "number" && Number.isFinite(v)) &&
|
|
160
|
+
embedding.some((v) => v !== 0),
|
|
161
|
+
"Missing or invalid managed embedding.",
|
|
162
|
+
);
|
|
163
|
+
} else
|
|
164
|
+
invariant(
|
|
165
|
+
embedding === undefined && doc.embeddingText === undefined,
|
|
166
|
+
"Unexpected managed embedding on an ineligible record.",
|
|
167
|
+
);
|
|
168
|
+
return hash(source);
|
|
169
|
+
}
|
|
40
170
|
export type InventoryItem = {
|
|
41
171
|
path: string;
|
|
42
172
|
pathBase64: string;
|
|
@@ -45,6 +175,8 @@ export type InventoryItem = {
|
|
|
45
175
|
bytes: number;
|
|
46
176
|
status: "included" | "excluded";
|
|
47
177
|
reason?: string;
|
|
178
|
+
retrieval?: "lexical" | "semantic";
|
|
179
|
+
policyReason?: string;
|
|
48
180
|
targetBase64?: string;
|
|
49
181
|
fileId?: string;
|
|
50
182
|
chunkIds?: string[];
|
|
@@ -52,10 +184,13 @@ export type InventoryItem = {
|
|
|
52
184
|
parseStatus?: string;
|
|
53
185
|
tokens?: number;
|
|
54
186
|
embedded?: number;
|
|
187
|
+
managed?: number;
|
|
188
|
+
managedTokens?: number;
|
|
55
189
|
skipped?: number;
|
|
56
190
|
};
|
|
57
191
|
export type Build = {
|
|
58
|
-
format: 1;
|
|
192
|
+
format: 1 | 2;
|
|
193
|
+
recordParts?: RecordPart[];
|
|
59
194
|
directory: string;
|
|
60
195
|
repoKey: string;
|
|
61
196
|
configHash: string;
|
|
@@ -83,58 +218,46 @@ export type Embedder = {
|
|
|
83
218
|
dimensions: number;
|
|
84
219
|
embed: (text: string) => Promise<number[]>;
|
|
85
220
|
};
|
|
86
|
-
export async function* records(
|
|
87
|
-
const
|
|
88
|
-
|
|
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
|
-
}
|
|
221
|
+
export async function* records(build: Build | string): AsyncGenerator<Doc> {
|
|
222
|
+
const b = typeof build === "string" ? await loadBuild(build) : build;
|
|
223
|
+
yield* artifactRecords(b);
|
|
97
224
|
}
|
|
98
225
|
export async function loadBuild(directory: string): Promise<Build> {
|
|
99
226
|
const b = await json<Build>(join(directory, "build.json"));
|
|
100
227
|
invariant(
|
|
101
|
-
b.format === 1 && b.configHash === hash(b.preset),
|
|
228
|
+
(b.format === 1 || b.format === 2) && b.configHash === hash(b.preset),
|
|
102
229
|
"Invalid build configuration.",
|
|
103
230
|
);
|
|
104
231
|
b.directory = resolve(directory);
|
|
232
|
+
validateStorage(b);
|
|
105
233
|
return b;
|
|
106
234
|
}
|
|
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
235
|
export async function materialize(args: {
|
|
124
236
|
identity: Identity;
|
|
125
237
|
ref: string;
|
|
238
|
+
resolvedCommit?: Awaited<ReturnType<typeof resolveCommit>>;
|
|
126
239
|
output: string;
|
|
127
240
|
previous?: Build;
|
|
241
|
+
storage?: ArtifactStorage;
|
|
128
242
|
preset?: Preset;
|
|
129
243
|
embedder?: Embedder;
|
|
130
244
|
cache?: string;
|
|
131
245
|
}): Promise<Build> {
|
|
246
|
+
const storage = args.storage ?? "jsonl";
|
|
247
|
+
invariant(
|
|
248
|
+
["jsonl", "segments"].includes(storage),
|
|
249
|
+
"Artifact storage must be jsonl or segments.",
|
|
250
|
+
);
|
|
132
251
|
const preset = args.preset ?? PRESET,
|
|
133
252
|
configHash = hash(preset),
|
|
134
253
|
prev = args.previous;
|
|
135
254
|
invariant(
|
|
136
|
-
hash(preset.
|
|
255
|
+
hash(preset.filePolicy) === hash(normalizeFilePolicy(preset.filePolicy)) &&
|
|
256
|
+
hash(preset.analyzers) === hash(normalizeAnalyzers(preset.analyzers)) &&
|
|
257
|
+
hash(preset.chunker) === hash(CHUNKER) &&
|
|
137
258
|
["syntax", "window"].includes(preset.mode) &&
|
|
259
|
+
(preset.enrichment === undefined ||
|
|
260
|
+
preset.enrichment === "path-only-v1") &&
|
|
138
261
|
preset.maxFileBytes === PRESET.maxFileBytes,
|
|
139
262
|
"Unsupported build preset.",
|
|
140
263
|
);
|
|
@@ -147,26 +270,25 @@ export async function materialize(args: {
|
|
|
147
270
|
}
|
|
148
271
|
invariant(
|
|
149
272
|
!preset.embedding ||
|
|
273
|
+
(preset.embedding.managed && supportedPreset(preset)) ||
|
|
150
274
|
(args.embedder?.model === preset.embedding.model &&
|
|
151
275
|
args.embedder.dimensions === preset.embedding.dimensions),
|
|
152
276
|
"Embedding provider must match the pinned preset.",
|
|
153
277
|
);
|
|
154
|
-
const commit =
|
|
278
|
+
const commit =
|
|
279
|
+
args.resolvedCommit ?? (await resolveCommit(args.identity.path, args.ref));
|
|
155
280
|
const directory = resolve(args.output);
|
|
156
281
|
await mkdir(directory, { recursive: false, mode: 0o700 });
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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");
|
|
282
|
+
let writer: ArtifactWriter;
|
|
283
|
+
try {
|
|
284
|
+
writer = await ArtifactWriter.create(directory, storage);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
await rm(directory, { recursive: true, force: true });
|
|
287
|
+
throw error;
|
|
169
288
|
}
|
|
289
|
+
const recordHashes = writer.recordHashes;
|
|
290
|
+
let writeError = false;
|
|
291
|
+
const emit = (doc: Doc) => writer.emit(doc);
|
|
170
292
|
const items: InventoryItem[] = [];
|
|
171
293
|
const blobs = new Blobs(args.identity.path);
|
|
172
294
|
const old = new Map(prev?.inventory.map((e) => [e.pathBase64, e]) ?? []);
|
|
@@ -200,7 +322,10 @@ export async function materialize(args: {
|
|
|
200
322
|
item.targetBase64 = (await blobs.read(e)).toString("base64");
|
|
201
323
|
continue;
|
|
202
324
|
}
|
|
203
|
-
const
|
|
325
|
+
const pathDecision = fileDecision(e.path, preset.filePolicy);
|
|
326
|
+
const reason =
|
|
327
|
+
hardExclusion(e.path) ??
|
|
328
|
+
(pathDecision.action === "exclude" ? pathDecision.reason : undefined);
|
|
204
329
|
if (reason) {
|
|
205
330
|
item.reason = reason;
|
|
206
331
|
continue;
|
|
@@ -233,7 +358,17 @@ export async function materialize(args: {
|
|
|
233
358
|
item.reason = "lfs-pointer";
|
|
234
359
|
continue;
|
|
235
360
|
}
|
|
236
|
-
|
|
361
|
+
if (privateKeyContent(source)) {
|
|
362
|
+
item.reason = "private-key-content";
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
const decision = fileDecision(e.path, preset.filePolicy, source);
|
|
366
|
+
const parsed = await chunk(
|
|
367
|
+
source,
|
|
368
|
+
e.path,
|
|
369
|
+
preset.mode,
|
|
370
|
+
preset.enrichment,
|
|
371
|
+
);
|
|
237
372
|
const contentHash = hash(data);
|
|
238
373
|
const fileId = `f-${hash([e.pathBase64, contentHash])}`;
|
|
239
374
|
const base = { schemaVersion: 1, configHash };
|
|
@@ -256,6 +391,8 @@ export async function materialize(args: {
|
|
|
256
391
|
}
|
|
257
392
|
Object.assign(item, {
|
|
258
393
|
status: "included",
|
|
394
|
+
retrieval: decision.action,
|
|
395
|
+
policyReason: decision.reason,
|
|
259
396
|
contentHash,
|
|
260
397
|
fileId,
|
|
261
398
|
chunkIds: [],
|
|
@@ -282,39 +419,52 @@ export async function materialize(args: {
|
|
|
282
419
|
? "structural-only"
|
|
283
420
|
: "embedding-disabled",
|
|
284
421
|
};
|
|
285
|
-
const eligible =
|
|
422
|
+
const eligible =
|
|
423
|
+
decision.action === "semantic" &&
|
|
424
|
+
!["imports", "structural"].includes(span.chunkKind);
|
|
286
425
|
if (preset.embedding && eligible) {
|
|
287
426
|
const inputHash = hash([preset.embedding, span.searchText]);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
427
|
+
if (preset.embedding.managed) {
|
|
428
|
+
doc.embeddingText = span.searchText;
|
|
429
|
+
doc.embeddingInputHash = inputHash;
|
|
430
|
+
doc.embeddingStatus = "managed";
|
|
431
|
+
delete doc.embeddingSkipReason;
|
|
432
|
+
item.managed = (item.managed ?? 0) + 1;
|
|
433
|
+
item.managedTokens = (item.managedTokens ?? 0) + span.tokenCount;
|
|
434
|
+
} else {
|
|
435
|
+
const cachePath = args.cache
|
|
436
|
+
? join(args.cache, inputHash + ".json")
|
|
437
|
+
: undefined;
|
|
438
|
+
let vector = cachePath
|
|
439
|
+
? await optionalJson<number[]>(cachePath)
|
|
440
|
+
: undefined;
|
|
441
|
+
if (!vector) {
|
|
442
|
+
vector = await args.embedder!.embed(span.searchText);
|
|
443
|
+
invariant(
|
|
444
|
+
vector.length === preset.embedding.dimensions &&
|
|
445
|
+
vector.every(Number.isFinite),
|
|
446
|
+
"Invalid embedding output.",
|
|
447
|
+
);
|
|
448
|
+
if (cachePath) await atomic(cachePath, vector);
|
|
449
|
+
}
|
|
296
450
|
invariant(
|
|
297
451
|
vector.length === preset.embedding.dimensions &&
|
|
298
452
|
vector.every(Number.isFinite),
|
|
299
|
-
"Invalid
|
|
453
|
+
"Invalid cached vector.",
|
|
300
454
|
);
|
|
301
|
-
|
|
455
|
+
doc.embedding = vector;
|
|
456
|
+
doc.embeddingInputHash = inputHash;
|
|
457
|
+
doc.embeddingStatus = "embedded";
|
|
458
|
+
delete doc.embeddingSkipReason;
|
|
459
|
+
item.embedded!++;
|
|
302
460
|
}
|
|
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
461
|
} else {
|
|
314
462
|
doc.embeddingSkipReason = preset.embedding
|
|
315
|
-
?
|
|
316
|
-
?
|
|
317
|
-
: "
|
|
463
|
+
? decision.action === "lexical"
|
|
464
|
+
? decision.reason
|
|
465
|
+
: span.chunkKind === "imports"
|
|
466
|
+
? "imports-only"
|
|
467
|
+
: "structural-only"
|
|
318
468
|
: "embedding-disabled";
|
|
319
469
|
item.skipped!++;
|
|
320
470
|
}
|
|
@@ -323,20 +473,34 @@ export async function materialize(args: {
|
|
|
323
473
|
await emit(doc);
|
|
324
474
|
}
|
|
325
475
|
}
|
|
326
|
-
if (prev && reuse.size)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
reuse.
|
|
476
|
+
if (prev && reuse.size) {
|
|
477
|
+
if (storage === "segments" && prev.format === 2) {
|
|
478
|
+
const files = new Map(prev.inventory.map((e) => [e.fileId, e]));
|
|
479
|
+
for (const part of prev.recordParts!) {
|
|
480
|
+
if (!reuse.has(part.fileId)) continue;
|
|
481
|
+
const ids = [part.fileId, ...files.get(part.fileId)!.chunkIds!];
|
|
482
|
+
await writer.retain(prev, part, ids);
|
|
483
|
+
for (const id of ids) reuse.delete(id);
|
|
331
484
|
}
|
|
485
|
+
} else
|
|
486
|
+
for await (const doc of records(prev))
|
|
487
|
+
if (reuse.has(doc.id)) {
|
|
488
|
+
await emit(doc);
|
|
489
|
+
reuse.delete(doc.id);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
332
492
|
invariant(reuse.size === 0, "Previous artifact lacks retained records.");
|
|
493
|
+
await writer.finish();
|
|
333
494
|
} catch (e) {
|
|
334
495
|
writeError = true;
|
|
335
496
|
throw e;
|
|
336
497
|
} finally {
|
|
337
498
|
blobs.close();
|
|
338
|
-
|
|
339
|
-
|
|
499
|
+
try {
|
|
500
|
+
await writer.close();
|
|
501
|
+
} finally {
|
|
502
|
+
if (writeError) await rm(directory, { recursive: true, force: true });
|
|
503
|
+
}
|
|
340
504
|
}
|
|
341
505
|
const changes: Build["changes"] = {
|
|
342
506
|
added: [],
|
|
@@ -358,7 +522,8 @@ export async function materialize(args: {
|
|
|
358
522
|
for (const e of old.values())
|
|
359
523
|
if (!now.has(e.pathBase64)) changes.deleted.push(e.path);
|
|
360
524
|
const build: Build = {
|
|
361
|
-
format: 1,
|
|
525
|
+
format: storage === "segments" ? 2 : 1,
|
|
526
|
+
...(storage === "segments" ? { recordParts: writer.parts } : {}),
|
|
362
527
|
directory,
|
|
363
528
|
repoKey: args.identity.key,
|
|
364
529
|
configHash,
|
|
@@ -388,6 +553,15 @@ export async function materialize(args: {
|
|
|
388
553
|
excluded: items.filter((e) => e.status === "excluded").length,
|
|
389
554
|
chunks: items.reduce((n, e) => n + (e.chunkIds?.length ?? 0), 0),
|
|
390
555
|
tokens: items.reduce((n, e) => n + (e.tokens ?? 0), 0),
|
|
556
|
+
...(preset.embedding?.managed
|
|
557
|
+
? {
|
|
558
|
+
managed: items.reduce((n, e) => n + (e.managed ?? 0), 0),
|
|
559
|
+
managedTokens: items.reduce(
|
|
560
|
+
(n, e) => n + (e.managedTokens ?? 0),
|
|
561
|
+
0,
|
|
562
|
+
),
|
|
563
|
+
}
|
|
564
|
+
: {}),
|
|
391
565
|
embedded: items.reduce((n, e) => n + (e.embedded ?? 0), 0),
|
|
392
566
|
skipped: items.reduce((n, e) => n + (e.skipped ?? 0), 0),
|
|
393
567
|
},
|
|
@@ -405,9 +579,15 @@ export async function validateBuild(b: Build): Promise<void> {
|
|
|
405
579
|
"Build commit identity mismatch; rebuild from the intended Git commit.",
|
|
406
580
|
);
|
|
407
581
|
invariant(
|
|
408
|
-
hash(b.preset
|
|
582
|
+
hash(b.preset.filePolicy) ===
|
|
583
|
+
hash(normalizeFilePolicy(b.preset.filePolicy)) &&
|
|
584
|
+
hash(b.preset.analyzers) ===
|
|
585
|
+
hash(normalizeAnalyzers(b.preset.analyzers)) &&
|
|
586
|
+
hash(b.preset) === b.configHash &&
|
|
587
|
+
hash(b.inventory) === b.inventoryHash,
|
|
409
588
|
"Build metadata hash mismatch.",
|
|
410
589
|
);
|
|
590
|
+
validateStorage(b);
|
|
411
591
|
const seen = new Set<string>();
|
|
412
592
|
let activeFile: Doc | undefined;
|
|
413
593
|
let end = 0,
|
|
@@ -417,6 +597,7 @@ export async function validateBuild(b: Build): Promise<void> {
|
|
|
417
597
|
.filter((e) => e.status === "included")
|
|
418
598
|
.map((e) => [e.fileId!, e]),
|
|
419
599
|
);
|
|
600
|
+
let activeDecision: FileDecision | undefined;
|
|
420
601
|
const completedFiles = new Set<string>();
|
|
421
602
|
const finish = () => {
|
|
422
603
|
if (activeFile) {
|
|
@@ -429,7 +610,7 @@ export async function validateBuild(b: Build): Promise<void> {
|
|
|
429
610
|
completedFiles.add(activeFile.id);
|
|
430
611
|
}
|
|
431
612
|
};
|
|
432
|
-
for await (const doc of records(b
|
|
613
|
+
for await (const doc of records(b)) {
|
|
433
614
|
invariant(
|
|
434
615
|
!seen.has(doc.id) && b.recordHashes[doc.id] === hash(doc),
|
|
435
616
|
"Corrupted or unexpected artifact record.",
|
|
@@ -439,12 +620,36 @@ export async function validateBuild(b: Build): Promise<void> {
|
|
|
439
620
|
doc.configHash === b.configHash && doc.schemaVersion === 1,
|
|
440
621
|
"Mixed configuration.",
|
|
441
622
|
);
|
|
623
|
+
if (b.preset.embedding?.managed) {
|
|
624
|
+
invariant(
|
|
625
|
+
doc.embedding === undefined,
|
|
626
|
+
"Managed artifacts must not contain vectors.",
|
|
627
|
+
);
|
|
628
|
+
if (doc.kind !== "chunk")
|
|
629
|
+
invariant(
|
|
630
|
+
doc.embeddingText === undefined,
|
|
631
|
+
"Only eligible chunks can contain embedding input.",
|
|
632
|
+
);
|
|
633
|
+
}
|
|
442
634
|
if (doc.kind === "file") {
|
|
443
635
|
finish();
|
|
444
636
|
activeFile = doc;
|
|
445
637
|
end = 0;
|
|
446
638
|
count = 0;
|
|
447
639
|
const entry = expectedFile.get(doc.id);
|
|
640
|
+
activeDecision = fileDecision(
|
|
641
|
+
doc.path as string,
|
|
642
|
+
b.preset.filePolicy,
|
|
643
|
+
doc.sourceText as string,
|
|
644
|
+
);
|
|
645
|
+
invariant(
|
|
646
|
+
!hardExclusion(doc.path as string) &&
|
|
647
|
+
!privateKeyContent(doc.sourceText as string) &&
|
|
648
|
+
activeDecision.action !== "exclude" &&
|
|
649
|
+
entry?.retrieval === activeDecision.action &&
|
|
650
|
+
entry?.policyReason === activeDecision.reason,
|
|
651
|
+
"File policy or inventory decision mismatch.",
|
|
652
|
+
);
|
|
448
653
|
const raw = Buffer.from(doc.sourceText as string);
|
|
449
654
|
invariant(
|
|
450
655
|
entry &&
|
|
@@ -494,9 +699,33 @@ export async function validateBuild(b: Build): Promise<void> {
|
|
|
494
699
|
Number(doc.tokenCount) <= CHUNKER.maxTokens,
|
|
495
700
|
"Invalid chunk source/position/token binding.",
|
|
496
701
|
);
|
|
497
|
-
if (
|
|
702
|
+
if (b.preset.embedding?.managed) {
|
|
703
|
+
const eligible =
|
|
704
|
+
activeDecision!.action === "semantic" &&
|
|
705
|
+
!["imports", "structural"].includes(String(doc.chunkKind));
|
|
706
|
+
invariant(
|
|
707
|
+
eligible
|
|
708
|
+
? doc.embeddingStatus === "managed" &&
|
|
709
|
+
doc.embeddingText === doc.searchText &&
|
|
710
|
+
doc.embeddingInputHash ===
|
|
711
|
+
hash([b.preset.embedding, doc.searchText]) &&
|
|
712
|
+
doc.embeddingSkipReason === undefined
|
|
713
|
+
: doc.embeddingStatus === "skipped" &&
|
|
714
|
+
doc.embeddingText === undefined &&
|
|
715
|
+
doc.embeddingInputHash === undefined &&
|
|
716
|
+
doc.embeddingSkipReason ===
|
|
717
|
+
(activeDecision!.action === "lexical"
|
|
718
|
+
? activeDecision!.reason
|
|
719
|
+
: doc.chunkKind === "imports"
|
|
720
|
+
? "imports-only"
|
|
721
|
+
: "structural-only"),
|
|
722
|
+
"Invalid managed embedding input or eligibility.",
|
|
723
|
+
);
|
|
724
|
+
} else if (doc.embeddingStatus === "embedded")
|
|
498
725
|
invariant(
|
|
499
726
|
b.preset.embedding &&
|
|
727
|
+
activeDecision!.action === "semantic" &&
|
|
728
|
+
!["imports", "structural"].includes(String(doc.chunkKind)) &&
|
|
500
729
|
Array.isArray(doc.embedding) &&
|
|
501
730
|
doc.embedding.length === b.preset.embedding.dimensions &&
|
|
502
731
|
doc.embedding.every(Number.isFinite),
|