@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/publish.ts
CHANGED
|
@@ -10,7 +10,15 @@ import {
|
|
|
10
10
|
sleep,
|
|
11
11
|
type Doc,
|
|
12
12
|
} from "./common.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
corpusMetadata,
|
|
15
|
+
loadBuild,
|
|
16
|
+
records,
|
|
17
|
+
recordHash,
|
|
18
|
+
supportedPreset,
|
|
19
|
+
validateBuild,
|
|
20
|
+
type Build,
|
|
21
|
+
} from "./build.js";
|
|
14
22
|
import { branch, tag, one, type CollectionStore } from "./remote.js";
|
|
15
23
|
export type Binding = {
|
|
16
24
|
repoId: string;
|
|
@@ -36,6 +44,8 @@ export type Journal = {
|
|
|
36
44
|
attemptId: string;
|
|
37
45
|
writer: string;
|
|
38
46
|
source: string;
|
|
47
|
+
gitRef?: string;
|
|
48
|
+
reuseWriter?: boolean;
|
|
39
49
|
baseline?: Published;
|
|
40
50
|
phase: "branch" | "writing" | "waiting" | "publishing";
|
|
41
51
|
batches: {
|
|
@@ -45,6 +55,43 @@ export type Journal = {
|
|
|
45
55
|
}[];
|
|
46
56
|
candidate?: { name: string; snapshotId: string };
|
|
47
57
|
};
|
|
58
|
+
/** The applied snapshot may differ from the shared canonical commit Tag. */
|
|
59
|
+
export type TrackedBranch = Doc & {
|
|
60
|
+
gitRef: string;
|
|
61
|
+
writer: string;
|
|
62
|
+
applied?: Published;
|
|
63
|
+
publishedTagName?: string;
|
|
64
|
+
pending?: { attemptId: string; buildId: string; commitOid: string };
|
|
65
|
+
};
|
|
66
|
+
export function gitBranchName(ref: string): string {
|
|
67
|
+
invariant(
|
|
68
|
+
ref.startsWith("refs/heads/") && ref.length > 11,
|
|
69
|
+
"Expected a full Git branch ref.",
|
|
70
|
+
);
|
|
71
|
+
return `git-${hash(ref).slice(0, 40)}`;
|
|
72
|
+
}
|
|
73
|
+
export const gitBranchId = (ref: string): string => `branch-${hash(ref)}`;
|
|
74
|
+
export const lastBuildPath = (state: string, ref?: string): string =>
|
|
75
|
+
join(state, ref ? `last-build-${hash(ref)}.json` : "last-build.json");
|
|
76
|
+
export async function trackedBranch(
|
|
77
|
+
store: CollectionStore,
|
|
78
|
+
binding: Binding,
|
|
79
|
+
ref: string,
|
|
80
|
+
): Promise<TrackedBranch | undefined> {
|
|
81
|
+
const doc = await one(store, branch("main"), gitBranchId(ref), true);
|
|
82
|
+
if (!doc) return undefined;
|
|
83
|
+
invariant(
|
|
84
|
+
doc.kind === "manifest" &&
|
|
85
|
+
doc.role === "git-branch" &&
|
|
86
|
+
doc.gitRef === ref &&
|
|
87
|
+
doc.writer === gitBranchName(ref) &&
|
|
88
|
+
doc.repoId === binding.repoId &&
|
|
89
|
+
doc.indexId === binding.indexId &&
|
|
90
|
+
doc.configHash === binding.configHash,
|
|
91
|
+
"Tracked branch identity mismatch.",
|
|
92
|
+
);
|
|
93
|
+
return doc as TrackedBranch;
|
|
94
|
+
}
|
|
48
95
|
export function versionTag(oid: string): string {
|
|
49
96
|
return `ver-${hash(oid).slice(0, 40)}`;
|
|
50
97
|
}
|
|
@@ -115,7 +162,11 @@ export async function validateCandidate(
|
|
|
115
162
|
for (const d of corpusMetadata(b, binding.repoId, binding.indexId, attemptId))
|
|
116
163
|
expected.set(d.id, hash(d));
|
|
117
164
|
for await (const d of store.list(tag(name))) {
|
|
118
|
-
|
|
165
|
+
try {
|
|
166
|
+
if (expected.get(d.id) !== recordHash(d, b.preset)) return false;
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
119
170
|
expected.delete(d.id);
|
|
120
171
|
}
|
|
121
172
|
if (expected.size) return false;
|
|
@@ -123,12 +174,10 @@ export async function validateCandidate(
|
|
|
123
174
|
let query: Record<string, unknown> = {
|
|
124
175
|
queryString: { query: "kind:chunk" },
|
|
125
176
|
};
|
|
126
|
-
for await (const d of records(b
|
|
177
|
+
for await (const d of records(b)) {
|
|
127
178
|
if (d.kind !== "chunk") continue;
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
const term = String(d.symbol ?? d.searchText).match(/\S+/u)?.[0];
|
|
131
|
-
if (!term) continue;
|
|
179
|
+
// Probe an indexed chunk ID: a valid source may contain only stop words
|
|
180
|
+
// or punctuation, so text-query matches cannot establish readiness.
|
|
132
181
|
query = {
|
|
133
182
|
bool: [
|
|
134
183
|
{
|
|
@@ -141,16 +190,8 @@ export async function validateCandidate(
|
|
|
141
190
|
},
|
|
142
191
|
{
|
|
143
192
|
queryString: {
|
|
144
|
-
query: d.
|
|
145
|
-
defaultField: "
|
|
146
|
-
skipSyntax: true,
|
|
147
|
-
},
|
|
148
|
-
occur: "filter",
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
queryString: {
|
|
152
|
-
query: term,
|
|
153
|
-
defaultField: "searchText",
|
|
193
|
+
query: d.id,
|
|
194
|
+
defaultField: "id",
|
|
154
195
|
skipSyntax: true,
|
|
155
196
|
},
|
|
156
197
|
occur: "must",
|
|
@@ -164,7 +205,8 @@ export async function validateCandidate(
|
|
|
164
205
|
!found.length ||
|
|
165
206
|
found.some(
|
|
166
207
|
(h) =>
|
|
167
|
-
h.doc.kind !== "chunk" ||
|
|
208
|
+
h.doc.kind !== "chunk" ||
|
|
209
|
+
b.recordHashes[h.doc.id] !== recordHash(h.doc, b.preset),
|
|
168
210
|
)
|
|
169
211
|
)
|
|
170
212
|
return false;
|
|
@@ -228,6 +270,72 @@ export async function published(
|
|
|
228
270
|
a.commitOid.localeCompare(b.commitOid),
|
|
229
271
|
);
|
|
230
272
|
}
|
|
273
|
+
/** A local artifact is only a cache for this exact, previously published baseline. */
|
|
274
|
+
async function baselineHashes(
|
|
275
|
+
store: CollectionStore,
|
|
276
|
+
binding: Binding,
|
|
277
|
+
preset: Build["preset"],
|
|
278
|
+
baseline: Published,
|
|
279
|
+
state: string,
|
|
280
|
+
gitRef?: string,
|
|
281
|
+
local?: Build,
|
|
282
|
+
): Promise<Map<string, string>> {
|
|
283
|
+
const pin = (await store.tags()).find((t) => t.name === baseline.tagName);
|
|
284
|
+
const root = await one(store, tag(baseline.tagName), "__manifest__");
|
|
285
|
+
invariant(
|
|
286
|
+
pin?.snapshotId === baseline.snapshotId &&
|
|
287
|
+
root?.repoId === binding.repoId &&
|
|
288
|
+
root.indexId === binding.indexId &&
|
|
289
|
+
root.configHash === binding.configHash &&
|
|
290
|
+
root.buildId === baseline.buildId &&
|
|
291
|
+
root.commitOid === baseline.commitOid &&
|
|
292
|
+
root.attemptId === baseline.attemptId &&
|
|
293
|
+
root.recordsHash === baseline.recordsHash &&
|
|
294
|
+
root.inventoryHash === baseline.inventoryHash,
|
|
295
|
+
"Baseline Tag is missing or changed.",
|
|
296
|
+
);
|
|
297
|
+
if (!local) {
|
|
298
|
+
const last = await optionalJson<{ artifact: string }>(
|
|
299
|
+
lastBuildPath(state, gitRef),
|
|
300
|
+
);
|
|
301
|
+
if (last) {
|
|
302
|
+
try {
|
|
303
|
+
local = await loadBuild(last.artifact);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (
|
|
310
|
+
local &&
|
|
311
|
+
local.repoKey === binding.repoKey &&
|
|
312
|
+
local.configHash === binding.configHash &&
|
|
313
|
+
local.buildId === baseline.buildId &&
|
|
314
|
+
local.commitOid === baseline.commitOid &&
|
|
315
|
+
local.recordsHash === baseline.recordsHash &&
|
|
316
|
+
local.inventoryHash === baseline.inventoryHash
|
|
317
|
+
) {
|
|
318
|
+
try {
|
|
319
|
+
await validateBuild(local);
|
|
320
|
+
const hashes = new Map(Object.entries(local.recordHashes));
|
|
321
|
+
for (const doc of corpusMetadata(
|
|
322
|
+
local,
|
|
323
|
+
binding.repoId,
|
|
324
|
+
binding.indexId,
|
|
325
|
+
baseline.attemptId,
|
|
326
|
+
))
|
|
327
|
+
hashes.set(doc.id, hash(doc));
|
|
328
|
+
return hashes;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Retain recovery without local files, including explicit same-artifact retries.
|
|
334
|
+
const hashes = new Map<string, string>();
|
|
335
|
+
for await (const doc of store.list(tag(baseline.tagName)))
|
|
336
|
+
hashes.set(doc.id, recordHash(doc, preset));
|
|
337
|
+
return hashes;
|
|
338
|
+
}
|
|
231
339
|
/** Caller holds the per-collection lock for the complete import. */
|
|
232
340
|
export async function publish(args: {
|
|
233
341
|
store: CollectionStore;
|
|
@@ -245,8 +353,8 @@ export async function publish(args: {
|
|
|
245
353
|
"Build does not belong to this index.",
|
|
246
354
|
);
|
|
247
355
|
invariant(
|
|
248
|
-
!b.preset.embedding,
|
|
249
|
-
"
|
|
356
|
+
!b.preset.embedding || supportedPreset(b.preset),
|
|
357
|
+
"Publication requires a supported lexical or managed embedding preset.",
|
|
250
358
|
);
|
|
251
359
|
await validateBuild(b);
|
|
252
360
|
const journalPath = join(state, "pending.json");
|
|
@@ -255,7 +363,9 @@ export async function publish(args: {
|
|
|
255
363
|
invariant(
|
|
256
364
|
j.buildId === b.buildId &&
|
|
257
365
|
j.recordsHash === b.recordsHash &&
|
|
258
|
-
j.inventoryHash === b.inventoryHash
|
|
366
|
+
j.inventoryHash === b.inventoryHash &&
|
|
367
|
+
(j.gitRef === b.branchRef ||
|
|
368
|
+
(j.reuseWriter === undefined && j.writer.startsWith("work-"))),
|
|
259
369
|
"Another build is pending. Resume the artifact recorded in pending.json before importing a new commit.",
|
|
260
370
|
);
|
|
261
371
|
invariant(
|
|
@@ -266,6 +376,22 @@ export async function publish(args: {
|
|
|
266
376
|
const existing = (await published(store, binding)).find(
|
|
267
377
|
(v) => v.commitOid === b.commitOid,
|
|
268
378
|
);
|
|
379
|
+
const tracked = b.branchRef
|
|
380
|
+
? await trackedBranch(store, binding, b.branchRef)
|
|
381
|
+
: undefined;
|
|
382
|
+
if (tracked?.pending)
|
|
383
|
+
invariant(
|
|
384
|
+
j?.attemptId === tracked.pending.attemptId,
|
|
385
|
+
"Git branch has a pending import; resume with its original retry journal.",
|
|
386
|
+
);
|
|
387
|
+
if (j?.gitRef && j.phase !== "branch")
|
|
388
|
+
invariant(
|
|
389
|
+
tracked?.pending?.attemptId === j.attemptId ||
|
|
390
|
+
(j.phase === "publishing" &&
|
|
391
|
+
!tracked?.pending &&
|
|
392
|
+
tracked?.applied?.attemptId === j.attemptId),
|
|
393
|
+
"Tracked branch import ownership changed; refusing to replay this journal.",
|
|
394
|
+
);
|
|
269
395
|
if (existing) {
|
|
270
396
|
invariant(
|
|
271
397
|
await validateCandidate(
|
|
@@ -277,17 +403,79 @@ export async function publish(args: {
|
|
|
277
403
|
),
|
|
278
404
|
"Existing published version differs from this build.",
|
|
279
405
|
);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
406
|
+
// A shared commit Tag does not mean this Git branch has applied that commit.
|
|
407
|
+
if (!j && (!b.branchRef || tracked?.applied?.commitOid === b.commitOid)) {
|
|
408
|
+
if (tracked?.applied) {
|
|
409
|
+
const writer = (await store.branches()).find(
|
|
410
|
+
(x) => x.name === tracked.writer,
|
|
411
|
+
);
|
|
412
|
+
invariant(
|
|
413
|
+
writer?.snapshotId === tracked.applied.snapshotId,
|
|
414
|
+
"Writer baseline has changed; refusing to update tracked branch.",
|
|
415
|
+
);
|
|
416
|
+
invariant(
|
|
417
|
+
await validateCandidate(
|
|
418
|
+
store,
|
|
419
|
+
tracked.applied.tagName,
|
|
420
|
+
b,
|
|
421
|
+
binding,
|
|
422
|
+
tracked.applied.attemptId,
|
|
423
|
+
),
|
|
424
|
+
"Tracked branch baseline validation failed.",
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
await atomic(lastBuildPath(state, b.branchRef), {
|
|
428
|
+
artifact: b.directory,
|
|
429
|
+
version: existing,
|
|
430
|
+
});
|
|
431
|
+
return existing;
|
|
432
|
+
}
|
|
433
|
+
// Legacy manual imports can finish bookkeeping after publication on retry.
|
|
434
|
+
if (j && !j.gitRef) {
|
|
435
|
+
await atomic(lastBuildPath(state), {
|
|
436
|
+
artifact: b.directory,
|
|
437
|
+
version: existing,
|
|
438
|
+
});
|
|
439
|
+
await rm(journalPath);
|
|
440
|
+
return existing;
|
|
441
|
+
}
|
|
286
442
|
}
|
|
287
443
|
if (!j) {
|
|
288
444
|
let source = "checkpoint-empty";
|
|
289
445
|
let baseline: Published | undefined;
|
|
290
|
-
if (
|
|
446
|
+
if (b.branchRef) {
|
|
447
|
+
const writer = (await store.branches()).find(
|
|
448
|
+
(x) => x.name === gitBranchName(b.branchRef!),
|
|
449
|
+
);
|
|
450
|
+
if (tracked?.applied) {
|
|
451
|
+
baseline = tracked.applied;
|
|
452
|
+
invariant(
|
|
453
|
+
writer?.snapshotId === baseline.snapshotId,
|
|
454
|
+
"Writer baseline has changed; refusing to update tracked branch.",
|
|
455
|
+
);
|
|
456
|
+
const pin = (await store.tags()).find(
|
|
457
|
+
(t) => t.name === baseline!.tagName,
|
|
458
|
+
);
|
|
459
|
+
const root = await one(store, tag(baseline.tagName), "__manifest__");
|
|
460
|
+
invariant(
|
|
461
|
+
pin?.snapshotId === baseline.snapshotId &&
|
|
462
|
+
root?.repoId === binding.repoId &&
|
|
463
|
+
root.indexId === binding.indexId &&
|
|
464
|
+
root.configHash === binding.configHash &&
|
|
465
|
+
root.buildId === baseline.buildId &&
|
|
466
|
+
root.attemptId === baseline.attemptId &&
|
|
467
|
+
root.recordsHash === baseline.recordsHash &&
|
|
468
|
+
root.inventoryHash === baseline.inventoryHash &&
|
|
469
|
+
root.commitOid === baseline.commitOid,
|
|
470
|
+
"Tracked branch baseline Tag is missing or changed.",
|
|
471
|
+
);
|
|
472
|
+
} else {
|
|
473
|
+
invariant(
|
|
474
|
+
!writer && !tracked,
|
|
475
|
+
"Unowned or unfinished tracked branch; retain its retry journal.",
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
} else if (args.baseline?.version.writer.startsWith("work-")) {
|
|
291
479
|
const prev = args.baseline;
|
|
292
480
|
await validateBuild(prev.build);
|
|
293
481
|
invariant(
|
|
@@ -322,7 +510,9 @@ export async function publish(args: {
|
|
|
322
510
|
inventoryHash: b.inventoryHash,
|
|
323
511
|
artifact: b.directory,
|
|
324
512
|
attemptId,
|
|
325
|
-
writer: `work-${attemptId}`,
|
|
513
|
+
writer: b.branchRef ? gitBranchName(b.branchRef) : `work-${attemptId}`,
|
|
514
|
+
gitRef: b.branchRef,
|
|
515
|
+
reuseWriter: !!tracked?.applied,
|
|
326
516
|
source,
|
|
327
517
|
baseline,
|
|
328
518
|
phase: "branch",
|
|
@@ -332,15 +522,48 @@ export async function publish(args: {
|
|
|
332
522
|
}
|
|
333
523
|
const save = () => atomic(journalPath, j);
|
|
334
524
|
if (j.phase === "branch") {
|
|
525
|
+
if (j.gitRef) {
|
|
526
|
+
// Record intent before any corpus mutation, including before branch creation.
|
|
527
|
+
// A fresh local state must not treat a lagging, partially written head as clean.
|
|
528
|
+
const current = await trackedBranch(store, binding, j.gitRef);
|
|
529
|
+
invariant(
|
|
530
|
+
(!current?.pending || current.pending.attemptId === j.attemptId) &&
|
|
531
|
+
current?.applied?.snapshotId === j.baseline?.snapshotId,
|
|
532
|
+
"Git branch ownership or baseline changed before import.",
|
|
533
|
+
);
|
|
534
|
+
await store.upsert("main", [
|
|
535
|
+
{
|
|
536
|
+
...current,
|
|
537
|
+
id: gitBranchId(j.gitRef),
|
|
538
|
+
kind: "manifest",
|
|
539
|
+
role: "git-branch",
|
|
540
|
+
schemaVersion: 1,
|
|
541
|
+
repoId: binding.repoId,
|
|
542
|
+
indexId: binding.indexId,
|
|
543
|
+
configHash: binding.configHash,
|
|
544
|
+
gitRef: j.gitRef,
|
|
545
|
+
writer: j.writer,
|
|
546
|
+
pending: {
|
|
547
|
+
attemptId: j.attemptId,
|
|
548
|
+
buildId: j.buildId,
|
|
549
|
+
commitOid: b.commitOid,
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
]);
|
|
553
|
+
}
|
|
335
554
|
const branches = await store.branches();
|
|
336
555
|
const found = branches.find((x) => x.name === j!.writer);
|
|
337
556
|
if (found)
|
|
338
557
|
invariant(
|
|
339
|
-
found.parent === j.source &&
|
|
558
|
+
(j.reuseWriter || found.parent === j.source) &&
|
|
340
559
|
found.snapshotId === (j.baseline?.snapshotId ?? null),
|
|
341
560
|
"Partially created writer has an unexpected baseline.",
|
|
342
561
|
);
|
|
343
562
|
else {
|
|
563
|
+
invariant(
|
|
564
|
+
!j.reuseWriter,
|
|
565
|
+
"Tracked writer is missing; refusing to recreate a nonempty baseline.",
|
|
566
|
+
);
|
|
344
567
|
const source = branches.find((x) => x.name === j!.source);
|
|
345
568
|
invariant(
|
|
346
569
|
source &&
|
|
@@ -363,18 +586,22 @@ export async function publish(args: {
|
|
|
363
586
|
const marker = metadata.at(-1)!;
|
|
364
587
|
if (j.phase === "writing") {
|
|
365
588
|
// Reconcile from the immutable old Tag, never from a half-written writer.
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
589
|
+
const oldHashes = j.baseline
|
|
590
|
+
? await baselineHashes(
|
|
591
|
+
store,
|
|
592
|
+
binding,
|
|
593
|
+
b.preset,
|
|
594
|
+
j.baseline,
|
|
595
|
+
state,
|
|
596
|
+
j.gitRef,
|
|
597
|
+
args.baseline?.build,
|
|
598
|
+
)
|
|
599
|
+
: new Map<string, string>();
|
|
373
600
|
const newIds = new Set([
|
|
374
601
|
...Object.keys(b.recordHashes),
|
|
375
602
|
...metadata.map((d) => d.id),
|
|
376
603
|
]);
|
|
377
|
-
const obsolete = [...
|
|
604
|
+
const obsolete = [...oldHashes.keys()].filter((id) => !newIds.has(id));
|
|
378
605
|
const mutate = async (
|
|
379
606
|
operation: string,
|
|
380
607
|
payload: unknown,
|
|
@@ -396,9 +623,10 @@ export async function publish(args: {
|
|
|
396
623
|
await mutate("delete", ids, () => store.delete(j!.writer, ids));
|
|
397
624
|
}
|
|
398
625
|
async function* changed() {
|
|
399
|
-
for await (const d of records(b
|
|
626
|
+
for await (const d of records(b))
|
|
627
|
+
if (oldHashes.get(d.id) !== hash(d)) yield d;
|
|
628
|
+
for (const d of metadata.slice(0, -1))
|
|
400
629
|
if (oldHashes.get(d.id) !== hash(d)) yield d;
|
|
401
|
-
for (const d of metadata.slice(0, -1)) yield d;
|
|
402
630
|
}
|
|
403
631
|
for await (const docs of batches(changed()))
|
|
404
632
|
await mutate("upsert", docs, () => store.upsert(j!.writer, docs));
|
|
@@ -414,6 +642,7 @@ export async function publish(args: {
|
|
|
414
642
|
const deadline = Date.now() + (args.timeoutMs ?? 120_000);
|
|
415
643
|
const pause = args.pollMs ?? 1000;
|
|
416
644
|
let rejectedSnapshot: string | undefined;
|
|
645
|
+
let validatedHere = false;
|
|
417
646
|
while (j.phase === "waiting") {
|
|
418
647
|
invariant(
|
|
419
648
|
Date.now() < deadline,
|
|
@@ -442,22 +671,33 @@ export async function publish(args: {
|
|
|
442
671
|
continue;
|
|
443
672
|
}
|
|
444
673
|
j.candidate = candidate;
|
|
674
|
+
validatedHere = true;
|
|
445
675
|
j.phase = "publishing";
|
|
446
676
|
await save();
|
|
447
677
|
}
|
|
448
678
|
invariant(j.candidate, "Missing validated candidate.");
|
|
679
|
+
// Never persist this shortcut: a resumed journal must revalidate the full corpus.
|
|
680
|
+
if (!validatedHere)
|
|
681
|
+
invariant(
|
|
682
|
+
await validateCandidate(store, j.candidate.name, b, binding, j.attemptId),
|
|
683
|
+
"Validated candidate is missing or changed.",
|
|
684
|
+
);
|
|
685
|
+
const pins = await store.tags();
|
|
449
686
|
invariant(
|
|
450
|
-
|
|
451
|
-
|
|
687
|
+
pins.find((t) => t.name === j!.candidate!.name)?.snapshotId ===
|
|
688
|
+
j.candidate.snapshotId,
|
|
689
|
+
"Validated candidate Tag is missing or changed.",
|
|
452
690
|
);
|
|
453
691
|
const name = versionTag(b.commitOid);
|
|
454
|
-
const found =
|
|
692
|
+
const found = pins.find((t) => t.name === name);
|
|
455
693
|
const final = found ?? (await store.tag(name, tag(j.candidate.name)));
|
|
456
694
|
invariant(
|
|
457
|
-
|
|
695
|
+
existing
|
|
696
|
+
? final.snapshotId === existing.snapshotId
|
|
697
|
+
: final.snapshotId === j.candidate.snapshotId,
|
|
458
698
|
"Published Tag name already points to a different snapshot.",
|
|
459
699
|
);
|
|
460
|
-
const result: Published = {
|
|
700
|
+
const result: Published = existing ?? {
|
|
461
701
|
id: versionId(b.commitOid),
|
|
462
702
|
kind: "manifest",
|
|
463
703
|
role: "published",
|
|
@@ -477,8 +717,42 @@ export async function publish(args: {
|
|
|
477
717
|
counts: b.counts,
|
|
478
718
|
validation: { inventory: true, content: true, query: true },
|
|
479
719
|
};
|
|
480
|
-
await store.upsert("main", [result]);
|
|
481
|
-
|
|
720
|
+
if (!existing) await store.upsert("main", [result]);
|
|
721
|
+
if (j.gitRef) {
|
|
722
|
+
const current = await trackedBranch(store, binding, j.gitRef);
|
|
723
|
+
invariant(
|
|
724
|
+
current?.pending?.attemptId === j.attemptId ||
|
|
725
|
+
(!current?.pending && current?.applied?.attemptId === j.attemptId),
|
|
726
|
+
"Tracked branch publication ownership changed.",
|
|
727
|
+
);
|
|
728
|
+
// Pin the exact writer baseline even when another branch published this commit first.
|
|
729
|
+
const applied: Published = {
|
|
730
|
+
...result,
|
|
731
|
+
writer: j.writer,
|
|
732
|
+
snapshotId: j.candidate.snapshotId,
|
|
733
|
+
tagName:
|
|
734
|
+
final.snapshotId === j.candidate.snapshotId
|
|
735
|
+
? final.name
|
|
736
|
+
: j.candidate.name,
|
|
737
|
+
attemptId: j.attemptId,
|
|
738
|
+
};
|
|
739
|
+
await store.upsert("main", [
|
|
740
|
+
{
|
|
741
|
+
id: gitBranchId(j.gitRef),
|
|
742
|
+
kind: "manifest",
|
|
743
|
+
role: "git-branch",
|
|
744
|
+
schemaVersion: 1,
|
|
745
|
+
repoId: binding.repoId,
|
|
746
|
+
indexId: binding.indexId,
|
|
747
|
+
configHash: binding.configHash,
|
|
748
|
+
gitRef: j.gitRef,
|
|
749
|
+
writer: j.writer,
|
|
750
|
+
applied,
|
|
751
|
+
publishedTagName: result.tagName,
|
|
752
|
+
},
|
|
753
|
+
]);
|
|
754
|
+
}
|
|
755
|
+
await atomic(lastBuildPath(state, j.gitRef), {
|
|
482
756
|
artifact: b.directory,
|
|
483
757
|
version: result,
|
|
484
758
|
});
|
package/src/releases.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { hash, invariant, type Doc } from "./common.js";
|
|
2
2
|
import { gitTags } from "./git.js";
|
|
3
3
|
import { one, branch, type CollectionStore } from "./remote.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
published,
|
|
6
|
+
trackedBranch,
|
|
7
|
+
type Binding,
|
|
8
|
+
type Published,
|
|
9
|
+
} from "./publish.js";
|
|
5
10
|
export const releaseAlias = (ref: string) => `rel-${hash(ref).slice(0, 40)}`;
|
|
6
11
|
export const releaseId = (ref: string) => `ref-${hash(ref)}`;
|
|
7
12
|
/** Only observed local tags. Absence is not deletion authority; this command never prunes. */
|
|
@@ -58,11 +63,39 @@ export async function resolveVersion(
|
|
|
58
63
|
binding: Binding,
|
|
59
64
|
selector: string,
|
|
60
65
|
): Promise<Published> {
|
|
61
|
-
const versions = await published(store, binding);
|
|
62
66
|
const ref = selector.startsWith("refs/tags/")
|
|
63
67
|
? selector
|
|
64
68
|
: `refs/tags/${selector}`;
|
|
65
|
-
const control =
|
|
69
|
+
const control = selector.startsWith("refs/heads/")
|
|
70
|
+
? undefined
|
|
71
|
+
: await one(store, branch("main"), releaseId(ref), true);
|
|
72
|
+
const headRef = selector.startsWith("refs/heads/")
|
|
73
|
+
? selector
|
|
74
|
+
: `refs/heads/${selector}`;
|
|
75
|
+
const head =
|
|
76
|
+
!selector.startsWith("refs/") || selector.startsWith("refs/heads/")
|
|
77
|
+
? await trackedBranch(store, binding, headRef)
|
|
78
|
+
: undefined;
|
|
79
|
+
invariant(
|
|
80
|
+
!(head && control),
|
|
81
|
+
"Ambiguous branch/tag name: use refs/heads/... or refs/tags/...",
|
|
82
|
+
);
|
|
83
|
+
// Pin moving ref control before listing versions. Otherwise publication can
|
|
84
|
+
// advance the ref to a version absent from the earlier list.
|
|
85
|
+
const versions = await published(store, binding);
|
|
86
|
+
if (head) {
|
|
87
|
+
invariant(
|
|
88
|
+
head.applied,
|
|
89
|
+
"Git branch has no published version yet; finish its import.",
|
|
90
|
+
);
|
|
91
|
+
const result = versions.find(
|
|
92
|
+
(v) =>
|
|
93
|
+
v.commitOid === head.applied!.commitOid &&
|
|
94
|
+
v.tagName === head.publishedTagName,
|
|
95
|
+
);
|
|
96
|
+
invariant(result, "Git branch target is not a published version.");
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
66
99
|
// Like Git import, a known tag name takes precedence over an OID spelling.
|
|
67
100
|
// Its pending or invalid state must not fall back to an unrelated commit.
|
|
68
101
|
if (!control && !selector.startsWith("refs/")) {
|
package/src/remote.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
tagTarget,
|
|
8
8
|
type IndexConfigsUnion,
|
|
9
9
|
} from "@functional-systems/lambdadb";
|
|
10
|
-
import { invariant, type Doc } from "./common.js";
|
|
10
|
+
import { hash, invariant, type Doc } from "./common.js";
|
|
11
11
|
import type { Settings } from "./settings.js";
|
|
12
12
|
export type Ref = { kind: "branch" | "tag"; name: string };
|
|
13
13
|
export type Snapshot = { name: string; snapshotId: string };
|
|
@@ -77,6 +77,17 @@ export class LambdaRemote {
|
|
|
77
77
|
(await this.client.listAllCollections({ size: 100 })).collections,
|
|
78
78
|
);
|
|
79
79
|
}
|
|
80
|
+
async collection(name: string) {
|
|
81
|
+
return request("get collection", async () => {
|
|
82
|
+
try {
|
|
83
|
+
return (await this.client.collection(name).get()).collection;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if ((error as { statusCode?: number }).statusCode === 404)
|
|
86
|
+
return undefined;
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
80
91
|
async create(
|
|
81
92
|
name: string,
|
|
82
93
|
indexConfigs: Record<string, IndexConfigsUnion>,
|
|
@@ -96,6 +107,43 @@ export class LambdaRemote {
|
|
|
96
107
|
const c = this.client.collection(name);
|
|
97
108
|
const ref = (r: Ref) =>
|
|
98
109
|
r.kind === "tag" ? tagRef(r.name) : branchRef(r.name);
|
|
110
|
+
// Some deployments omit managed vectors from list/query responses despite
|
|
111
|
+
// includeVectors. Keep the same immutable Tag and exact non-vector payload.
|
|
112
|
+
const hydrate = async (r: Ref, docs: Doc[]): Promise<Doc[]> => {
|
|
113
|
+
const missing = docs.filter(
|
|
114
|
+
(d) => d.embeddingStatus === "managed" && d.embedding === undefined,
|
|
115
|
+
);
|
|
116
|
+
const hydrated = new Map<string, Doc>();
|
|
117
|
+
if (missing.length) {
|
|
118
|
+
invariant(
|
|
119
|
+
r.kind === "tag",
|
|
120
|
+
"Managed vector hydration requires an immutable Tag.",
|
|
121
|
+
);
|
|
122
|
+
const fetched = await request("fetch omitted vectors", () =>
|
|
123
|
+
c.docs.fetch({
|
|
124
|
+
ref: tagRef(r.name),
|
|
125
|
+
ids: missing.map((d) => d.id),
|
|
126
|
+
consistentRead: false,
|
|
127
|
+
includeVectors: true,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
const expected = new Map(missing.map((d) => [d.id, hash(d)]));
|
|
131
|
+
for (const item of fetched.docs) {
|
|
132
|
+
const doc = item.doc as Doc;
|
|
133
|
+
const { embedding: _, ...source } = doc;
|
|
134
|
+
invariant(
|
|
135
|
+
!hydrated.has(doc.id) && expected.get(doc.id) === hash(source),
|
|
136
|
+
"Returned/fetched managed payloads disagree.",
|
|
137
|
+
);
|
|
138
|
+
hydrated.set(doc.id, doc);
|
|
139
|
+
}
|
|
140
|
+
invariant(
|
|
141
|
+
hydrated.size === expected.size,
|
|
142
|
+
"Returned managed records are missing from fetch.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return docs.map((doc) => hydrated.get(doc.id) ?? doc);
|
|
146
|
+
};
|
|
99
147
|
return {
|
|
100
148
|
fetch: (r, ids, consistent = false) =>
|
|
101
149
|
request("fetch", async () => {
|
|
@@ -132,7 +180,11 @@ export class LambdaRemote {
|
|
|
132
180
|
pageToken: cursor,
|
|
133
181
|
}),
|
|
134
182
|
);
|
|
135
|
-
for (const
|
|
183
|
+
for (const doc of await hydrate(
|
|
184
|
+
r,
|
|
185
|
+
page.docs.map((d) => d.doc as Doc),
|
|
186
|
+
))
|
|
187
|
+
yield doc;
|
|
136
188
|
cursor = page.nextPageToken || undefined;
|
|
137
189
|
if (cursor) {
|
|
138
190
|
invariant(
|
|
@@ -199,17 +251,22 @@ export class LambdaRemote {
|
|
|
199
251
|
: c.aliases.create({ aliasName: name, target: tagTarget(target) }),
|
|
200
252
|
);
|
|
201
253
|
},
|
|
202
|
-
query: (
|
|
203
|
-
request("query",
|
|
204
|
-
(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
254
|
+
query: async (name, query, size) => {
|
|
255
|
+
const result = await request("query", () =>
|
|
256
|
+
c.query({
|
|
257
|
+
ref: tagRef(name),
|
|
258
|
+
consistentRead: false,
|
|
259
|
+
query,
|
|
260
|
+
size,
|
|
261
|
+
includeVectors: true,
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
const docs = await hydrate(
|
|
265
|
+
{ kind: "tag", name },
|
|
266
|
+
result.docs.map((h) => h.doc as Doc),
|
|
267
|
+
);
|
|
268
|
+
return result.docs.map((h, i) => ({ doc: docs[i]!, score: h.score }));
|
|
269
|
+
},
|
|
213
270
|
};
|
|
214
271
|
}
|
|
215
272
|
}
|