@functional-systems/srcx 0.1.0-dev.6 → 0.1.0-dev.8
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 +9 -2
- package/DESIGN.md +55 -21
- package/README.md +55 -1
- package/VALIDATION.md +72 -0
- package/dist/build.d.ts +68 -0
- package/dist/build.js +106 -19
- package/dist/build.js.map +1 -1
- package/dist/cli.js +20 -7
- package/dist/cli.js.map +1 -1
- package/dist/publish.js +11 -5
- package/dist/publish.js.map +1 -1
- package/dist/remote.js +30 -3
- package/dist/remote.js.map +1 -1
- package/dist/repository.d.ts +4 -1
- package/dist/repository.js +15 -11
- package/dist/repository.js.map +1 -1
- package/dist/search.d.ts +7 -2
- package/dist/search.js +24 -4
- package/dist/search.js.map +1 -1
- package/package.json +5 -3
- package/src/build.ts +138 -22
- package/src/cli.ts +59 -10
- package/src/publish.ts +18 -6
- package/src/remote.ts +44 -2
- package/src/repository.ts +20 -11
- package/src/search.ts +37 -8
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 };
|
|
@@ -132,7 +132,48 @@ export class LambdaRemote {
|
|
|
132
132
|
pageToken: cursor,
|
|
133
133
|
}),
|
|
134
134
|
);
|
|
135
|
-
|
|
135
|
+
// Some deployments omit managed vectors from list responses even with
|
|
136
|
+
// includeVectors. Fetch only those records from the same immutable Tag;
|
|
137
|
+
// require identical non-vector payloads rather than weakening validation.
|
|
138
|
+
const missing = page.docs
|
|
139
|
+
.map((d) => d.doc as Doc)
|
|
140
|
+
.filter(
|
|
141
|
+
(d) =>
|
|
142
|
+
d.embeddingStatus === "managed" && d.embedding === undefined,
|
|
143
|
+
);
|
|
144
|
+
const hydrated = new Map<string, Doc>();
|
|
145
|
+
if (missing.length) {
|
|
146
|
+
invariant(
|
|
147
|
+
r.kind === "tag",
|
|
148
|
+
"Managed vector hydration requires an immutable Tag.",
|
|
149
|
+
);
|
|
150
|
+
const fetched = await request("fetch listed vectors", () =>
|
|
151
|
+
c.docs.fetch({
|
|
152
|
+
ref: tagRef(r.name),
|
|
153
|
+
ids: missing.map((d) => d.id),
|
|
154
|
+
consistentRead: false,
|
|
155
|
+
includeVectors: true,
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
const expected = new Map(missing.map((d) => [d.id, hash(d)]));
|
|
159
|
+
for (const item of fetched.docs) {
|
|
160
|
+
const doc = item.doc as Doc;
|
|
161
|
+
const { embedding: _, ...source } = doc;
|
|
162
|
+
invariant(
|
|
163
|
+
!hydrated.has(doc.id) && expected.get(doc.id) === hash(source),
|
|
164
|
+
"Listed/fetched managed payloads disagree.",
|
|
165
|
+
);
|
|
166
|
+
hydrated.set(doc.id, doc);
|
|
167
|
+
}
|
|
168
|
+
invariant(
|
|
169
|
+
hydrated.size === expected.size,
|
|
170
|
+
"Listed managed records are missing from fetch.",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
for (const d of page.docs) {
|
|
174
|
+
const doc = d.doc as Doc;
|
|
175
|
+
yield hydrated.get(doc.id) ?? doc;
|
|
176
|
+
}
|
|
136
177
|
cursor = page.nextPageToken || undefined;
|
|
137
178
|
if (cursor) {
|
|
138
179
|
invariant(
|
|
@@ -207,6 +248,7 @@ export class LambdaRemote {
|
|
|
207
248
|
consistentRead: false,
|
|
208
249
|
query,
|
|
209
250
|
size,
|
|
251
|
+
includeVectors: true,
|
|
210
252
|
})
|
|
211
253
|
).docs.map((h) => ({ doc: h.doc as Doc, score: h.score })),
|
|
212
254
|
),
|
package/src/repository.ts
CHANGED
|
@@ -8,13 +8,14 @@ import {
|
|
|
8
8
|
PURPOSE,
|
|
9
9
|
type Doc,
|
|
10
10
|
} from "./common.js";
|
|
11
|
-
import { PRESET,
|
|
11
|
+
import { PRESET, indexConfigs, supportedPreset, type Preset } from "./build.js";
|
|
12
12
|
import { identity, type Identity } from "./git.js";
|
|
13
13
|
import { LambdaRemote, one, branch } from "./remote.js";
|
|
14
14
|
import { exclusive, type Binding } from "./publish.js";
|
|
15
15
|
import { stateRoot, type Settings } from "./settings.js";
|
|
16
16
|
import type { IndexConfigsUnion } from "@functional-systems/lambdadb";
|
|
17
17
|
export type Repository = Binding & {
|
|
18
|
+
preset?: Preset;
|
|
18
19
|
collection: string;
|
|
19
20
|
name: string;
|
|
20
21
|
description: string;
|
|
@@ -42,12 +43,15 @@ export function collectionName(
|
|
|
42
43
|
return `code-${slug}-${hash(["collection-v1", key, configHash]).slice(0, 16)}`;
|
|
43
44
|
}
|
|
44
45
|
/** LambdaDB adds its built-in keyword id index to collection metadata. */
|
|
45
|
-
export function matchesIndexSchema(
|
|
46
|
+
export function matchesIndexSchema(
|
|
47
|
+
actual: unknown,
|
|
48
|
+
preset: Preset = PRESET,
|
|
49
|
+
): boolean {
|
|
46
50
|
if (!actual || typeof actual !== "object" || Array.isArray(actual))
|
|
47
51
|
return false;
|
|
48
52
|
const { id, ...fields } = actual as Record<string, unknown>;
|
|
49
53
|
if (id !== undefined && hash(id) !== hash({ type: "keyword" })) return false;
|
|
50
|
-
return hash(fields) === hash(
|
|
54
|
+
return hash(fields) === hash(indexConfigs(preset));
|
|
51
55
|
}
|
|
52
56
|
function descriptor(r: Repository): Doc {
|
|
53
57
|
return {
|
|
@@ -60,7 +64,7 @@ function descriptor(r: Repository): Doc {
|
|
|
60
64
|
repoId: r.repoId,
|
|
61
65
|
indexId: r.indexId,
|
|
62
66
|
name: r.name,
|
|
63
|
-
preset: PRESET,
|
|
67
|
+
preset: r.preset ?? PRESET,
|
|
64
68
|
initialization: "ready",
|
|
65
69
|
};
|
|
66
70
|
}
|
|
@@ -76,8 +80,8 @@ function decode(
|
|
|
76
80
|
invariant(
|
|
77
81
|
doc?.role === "repository" &&
|
|
78
82
|
doc.initialization === "ready" &&
|
|
79
|
-
doc.
|
|
80
|
-
|
|
83
|
+
supportedPreset(doc.preset) &&
|
|
84
|
+
doc.configHash === hash(doc.preset),
|
|
81
85
|
"Repository is partially initialized or uses an unsupported preset.",
|
|
82
86
|
);
|
|
83
87
|
invariant(
|
|
@@ -87,7 +91,7 @@ function decode(
|
|
|
87
91
|
"Repository descriptor and Collection labels disagree.",
|
|
88
92
|
);
|
|
89
93
|
invariant(
|
|
90
|
-
matchesIndexSchema(c.indexConfigs),
|
|
94
|
+
matchesIndexSchema(c.indexConfigs, doc.preset as Preset),
|
|
91
95
|
"Collection schema differs from the pinned preset.",
|
|
92
96
|
);
|
|
93
97
|
invariant(
|
|
@@ -98,6 +102,7 @@ function decode(
|
|
|
98
102
|
"Malformed repository descriptor.",
|
|
99
103
|
);
|
|
100
104
|
return {
|
|
105
|
+
preset: doc.preset as Preset,
|
|
101
106
|
collection: c.collectionName,
|
|
102
107
|
repoKey: doc.repoKey,
|
|
103
108
|
repoId: doc.repoId,
|
|
@@ -148,7 +153,7 @@ export async function selectRepository(
|
|
|
148
153
|
invariant(
|
|
149
154
|
selected.length === 1,
|
|
150
155
|
selected.length
|
|
151
|
-
? "Ambiguous repository; use its
|
|
156
|
+
? "Ambiguous repository; use its exact Collection name (presets have separate Collections)."
|
|
152
157
|
: `Repository not found${partial.length ? "; partially initialized collections exist (see repo list)" : ""}.`,
|
|
153
158
|
);
|
|
154
159
|
return selected[0]!;
|
|
@@ -160,10 +165,13 @@ export async function register(
|
|
|
160
165
|
remote?: string;
|
|
161
166
|
description?: string;
|
|
162
167
|
labels?: Record<string, string>;
|
|
168
|
+
preset?: Preset;
|
|
163
169
|
},
|
|
164
170
|
): Promise<Repository> {
|
|
165
171
|
const source = await identity(args.path, args.remote);
|
|
166
|
-
const
|
|
172
|
+
const preset = args.preset ?? PRESET;
|
|
173
|
+
invariant(supportedPreset(preset), "Unsupported repository preset.");
|
|
174
|
+
const configHash = hash(preset),
|
|
167
175
|
collection = collectionName(source.key, source.name, configHash),
|
|
168
176
|
state = destination(remote.settings, collection);
|
|
169
177
|
return exclusive(state, async () => {
|
|
@@ -218,6 +226,7 @@ export async function register(
|
|
|
218
226
|
);
|
|
219
227
|
const indexId = randomUUID();
|
|
220
228
|
intent = {
|
|
229
|
+
preset,
|
|
221
230
|
repoKey: source.key,
|
|
222
231
|
name: source.name,
|
|
223
232
|
repoId: randomUUID(),
|
|
@@ -244,13 +253,13 @@ export async function register(
|
|
|
244
253
|
if (existing)
|
|
245
254
|
invariant(
|
|
246
255
|
hash(existing.tags) === hash(intent.tags) &&
|
|
247
|
-
matchesIndexSchema(existing.indexConfigs),
|
|
256
|
+
matchesIndexSchema(existing.indexConfigs, preset),
|
|
248
257
|
"Existing Collection differs from provisioning journal.",
|
|
249
258
|
);
|
|
250
259
|
else
|
|
251
260
|
await remote.create(
|
|
252
261
|
collection,
|
|
253
|
-
JSON.parse(JSON.stringify(
|
|
262
|
+
JSON.parse(JSON.stringify(indexConfigs(preset))) as Record<
|
|
254
263
|
string,
|
|
255
264
|
IndexConfigsUnion
|
|
256
265
|
>,
|
package/src/search.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { one, tag, type CollectionStore } from "./remote.js";
|
|
|
5
5
|
import { stateRoot, type Settings } from "./settings.js";
|
|
6
6
|
import type { Repository } from "./repository.js";
|
|
7
7
|
import type { Published } from "./publish.js";
|
|
8
|
-
import type
|
|
8
|
+
import { PRESET, recordHash, type InventoryItem } from "./build.js";
|
|
9
9
|
export type Handle = {
|
|
10
10
|
id: string;
|
|
11
11
|
endpoint: string;
|
|
@@ -84,6 +84,33 @@ export function lexicalQuery(
|
|
|
84
84
|
],
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
+
export type SearchMode = "lexical" | "semantic" | "hybrid";
|
|
88
|
+
export function retrievalQuery(
|
|
89
|
+
query: string,
|
|
90
|
+
size: number,
|
|
91
|
+
filters: { path?: string; language?: string },
|
|
92
|
+
mode: SearchMode,
|
|
93
|
+
): Record<string, unknown> {
|
|
94
|
+
invariant(
|
|
95
|
+
["lexical", "semantic", "hybrid"].includes(mode),
|
|
96
|
+
"Unknown search mode.",
|
|
97
|
+
);
|
|
98
|
+
const lexical = lexicalQuery(query, filters);
|
|
99
|
+
if (mode === "lexical") return lexical;
|
|
100
|
+
const semantic = {
|
|
101
|
+
knn: {
|
|
102
|
+
field: "embedding",
|
|
103
|
+
queryText: query,
|
|
104
|
+
k: size,
|
|
105
|
+
filter: {
|
|
106
|
+
bool: (lexical.bool as { occur: string }[]).filter(
|
|
107
|
+
(c) => c.occur === "filter",
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
return mode === "semantic" ? semantic : { rrf: [lexical, semantic] };
|
|
113
|
+
}
|
|
87
114
|
export async function search(
|
|
88
115
|
store: CollectionStore,
|
|
89
116
|
s: Settings,
|
|
@@ -92,21 +119,23 @@ export async function search(
|
|
|
92
119
|
query: string,
|
|
93
120
|
size = 10,
|
|
94
121
|
filters: { path?: string; language?: string } = {},
|
|
122
|
+
mode: SearchMode = "lexical",
|
|
95
123
|
): Promise<unknown[]> {
|
|
96
124
|
invariant(
|
|
97
125
|
Number.isInteger(size) && size > 0 && size <= 100,
|
|
98
126
|
"Limit must be an integer from 1 to 100.",
|
|
99
127
|
);
|
|
128
|
+
invariant(
|
|
129
|
+
mode === "lexical" || r.preset?.embedding?.managed,
|
|
130
|
+
"Semantic/hybrid search requires a managed embedding Collection; register with --embedding text-embedding-3-small.",
|
|
131
|
+
);
|
|
132
|
+
const request = retrievalQuery(query, size, filters, mode);
|
|
100
133
|
const entries = await inventoryAt(store, r, v);
|
|
101
134
|
const files = new Map(
|
|
102
135
|
entries.filter((e) => e.status === "included").map((e) => [e.fileId, e]),
|
|
103
136
|
);
|
|
104
137
|
const result = [];
|
|
105
|
-
for (const hit of await store.query(
|
|
106
|
-
v.tagName,
|
|
107
|
-
lexicalQuery(query, filters),
|
|
108
|
-
size,
|
|
109
|
-
)) {
|
|
138
|
+
for (const hit of await store.query(v.tagName, request, size)) {
|
|
110
139
|
const d = hit.doc,
|
|
111
140
|
e = files.get(d.fileId as string);
|
|
112
141
|
invariant(
|
|
@@ -128,7 +157,7 @@ export async function search(
|
|
|
128
157
|
path: String(d.path),
|
|
129
158
|
contentHash: String(d.contentHash),
|
|
130
159
|
chunkId: d.id,
|
|
131
|
-
chunkHash:
|
|
160
|
+
chunkHash: recordHash(d, r.preset ?? PRESET),
|
|
132
161
|
startLine: Number(d.startLine),
|
|
133
162
|
endLine: Number(d.endLine),
|
|
134
163
|
};
|
|
@@ -222,7 +251,7 @@ export async function readHandle(
|
|
|
222
251
|
const d = await one(store, tag(v.tagName), h.chunkId);
|
|
223
252
|
invariant(
|
|
224
253
|
d &&
|
|
225
|
-
|
|
254
|
+
recordHash(d, h.repository.preset ?? PRESET) === h.chunkHash &&
|
|
226
255
|
d.fileId === h.fileId &&
|
|
227
256
|
d.contentHash === h.contentHash,
|
|
228
257
|
"Pinned chunk has changed or is missing.",
|