@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +179 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/DESIGN.md +378 -58
  4. package/README.md +628 -35
  5. package/RELEASING.md +15 -3
  6. package/VALIDATION.md +1120 -16
  7. package/dist/artifact.d.ts +31 -0
  8. package/dist/artifact.js +237 -0
  9. package/dist/artifact.js.map +1 -0
  10. package/dist/build.d.ts +86 -3
  11. package/dist/build.js +233 -71
  12. package/dist/build.js.map +1 -1
  13. package/dist/chunk.d.ts +9 -3
  14. package/dist/chunk.js +341 -14
  15. package/dist/chunk.js.map +1 -1
  16. package/dist/cli.js +130 -21
  17. package/dist/cli.js.map +1 -1
  18. package/dist/file-policy.d.ts +20 -0
  19. package/dist/file-policy.js +103 -0
  20. package/dist/file-policy.js.map +1 -0
  21. package/dist/implementation.d.ts +11 -0
  22. package/dist/implementation.js +191 -0
  23. package/dist/implementation.js.map +1 -0
  24. package/dist/publish.d.ts +18 -0
  25. package/dist/publish.js +224 -46
  26. package/dist/publish.js.map +1 -1
  27. package/dist/releases.js +21 -3
  28. package/dist/releases.js.map +1 -1
  29. package/dist/remote.d.ts +1 -0
  30. package/dist/remote.js +50 -9
  31. package/dist/remote.js.map +1 -1
  32. package/dist/repository.d.ts +4 -1
  33. package/dist/repository.js +26 -11
  34. package/dist/repository.js.map +1 -1
  35. package/dist/rerank.d.ts +16 -0
  36. package/dist/rerank.js +87 -0
  37. package/dist/rerank.js.map +1 -0
  38. package/dist/search.d.ts +18 -3
  39. package/dist/search.js +143 -23
  40. package/dist/search.js.map +1 -1
  41. package/dist/skills.d.ts +24 -0
  42. package/dist/skills.js +155 -0
  43. package/dist/skills.js.map +1 -0
  44. package/package.json +17 -6
  45. package/runtime/grammars/README.md +50 -0
  46. package/runtime/grammars/tree-sitter-bash.LICENSE +21 -0
  47. package/runtime/grammars/tree-sitter-bash.wasm +0 -0
  48. package/runtime/grammars/tree-sitter-sql.LICENSE +21 -0
  49. package/runtime/grammars/tree-sitter-sql.wasm +0 -0
  50. package/runtime/grammars/tree-sitter-wasm.LICENSE +21 -0
  51. package/runtime/qwen.py +132 -0
  52. package/runtime/requirements.txt +34 -0
  53. package/skills/srcx-search/SKILL.md +103 -0
  54. package/src/artifact.ts +303 -0
  55. package/src/build.ts +313 -84
  56. package/src/chunk.ts +360 -12
  57. package/src/cli.ts +263 -23
  58. package/src/file-policy.ts +166 -0
  59. package/src/implementation.ts +253 -0
  60. package/src/publish.ts +321 -47
  61. package/src/releases.ts +36 -3
  62. package/src/remote.ts +70 -13
  63. package/src/repository.ts +40 -11
  64. package/src/rerank.ts +131 -0
  65. package/src/search.ts +229 -28
  66. 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 { corpusMetadata, records, validateBuild, type Build } from "./build.js";
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
- if (expected.get(d.id) !== hash(d)) return false;
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.directory)) {
177
+ for await (const d of records(b)) {
127
178
  if (d.kind !== "chunk") continue;
128
- // Keep a complete surface token: the standard analyzer may retain dots
129
- // (e.g. code.ts), so extracting an alphanumeric substring is not a valid probe.
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.fileId,
145
- defaultField: "fileId",
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" || b.recordHashes[h.doc.id] !== hash(h.doc),
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
- "CLI publication currently supports only the embedding=none preset.",
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
- await atomic(join(state, "last-build.json"), {
281
- artifact: b.directory,
282
- version: existing,
283
- });
284
- if (j) await rm(journalPath);
285
- return existing;
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 (args.baseline) {
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 oldIds = new Set<string>();
367
- const oldHashes = new Map<string, string>();
368
- if (j.baseline)
369
- for await (const d of store.list(tag(j.baseline.tagName))) {
370
- oldIds.add(d.id);
371
- oldHashes.set(d.id, hash(d));
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 = [...oldIds].filter((id) => !newIds.has(id));
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.directory))
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
- await validateCandidate(store, j.candidate.name, b, binding, j.attemptId),
451
- "Validated candidate is missing or changed.",
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 = (await store.tags()).find((t) => t.name === name);
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
- final.snapshotId === j.candidate.snapshotId,
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
- await atomic(join(state, "last-build.json"), {
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 { published, type Binding, type Published } from "./publish.js";
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 = await one(store, branch("main"), releaseId(ref), true);
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 d of page.docs) yield d.doc as Doc;
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: (tag, query, size) =>
203
- request("query", async () =>
204
- (
205
- await c.query({
206
- ref: tagRef(tag),
207
- consistentRead: false,
208
- query,
209
- size,
210
- })
211
- ).docs.map((h) => ({ doc: h.doc as Doc, score: h.score })),
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
  }