@functional-systems/srcx 0.1.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/CONTRIBUTING.md +54 -0
  3. package/DESIGN.md +1057 -0
  4. package/LICENSE +202 -0
  5. package/README.md +209 -0
  6. package/RELEASING.md +137 -0
  7. package/VALIDATION.md +181 -0
  8. package/dist/build.d.ts +94 -0
  9. package/dist/build.js +433 -0
  10. package/dist/build.js.map +1 -0
  11. package/dist/chunk.d.ts +30 -0
  12. package/dist/chunk.js +296 -0
  13. package/dist/chunk.js.map +1 -0
  14. package/dist/cli.d.ts +2 -0
  15. package/dist/cli.js +276 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/common.d.ts +17 -0
  18. package/dist/common.js +74 -0
  19. package/dist/common.js.map +1 -0
  20. package/dist/git.d.ts +38 -0
  21. package/dist/git.js +224 -0
  22. package/dist/git.js.map +1 -0
  23. package/dist/publish.d.ts +60 -0
  24. package/dist/publish.js +341 -0
  25. package/dist/publish.js.map +1 -0
  26. package/dist/releases.d.ts +8 -0
  27. package/dist/releases.js +79 -0
  28. package/dist/releases.js.map +1 -0
  29. package/dist/remote.d.ts +53 -0
  30. package/dist/remote.js +136 -0
  31. package/dist/remote.js.map +1 -0
  32. package/dist/repository.d.ts +29 -0
  33. package/dist/repository.js +180 -0
  34. package/dist/repository.js.map +1 -0
  35. package/dist/search.d.ts +46 -0
  36. package/dist/search.js +181 -0
  37. package/dist/search.js.map +1 -0
  38. package/dist/settings.d.ts +10 -0
  39. package/dist/settings.js +35 -0
  40. package/dist/settings.js.map +1 -0
  41. package/package.json +58 -0
  42. package/src/build.ts +587 -0
  43. package/src/chunk.ts +328 -0
  44. package/src/cli.ts +366 -0
  45. package/src/common.ts +71 -0
  46. package/src/git.ts +286 -0
  47. package/src/publish.ts +487 -0
  48. package/src/releases.ts +108 -0
  49. package/src/remote.ts +230 -0
  50. package/src/repository.ts +306 -0
  51. package/src/search.ts +282 -0
  52. package/src/settings.ts +59 -0
package/src/remote.ts ADDED
@@ -0,0 +1,230 @@
1
+ import {
2
+ LambdaDBClient,
3
+ branchRef,
4
+ tagRef,
5
+ branchSource,
6
+ tagSource,
7
+ tagTarget,
8
+ type IndexConfigsUnion,
9
+ } from "@functional-systems/lambdadb";
10
+ import { invariant, type Doc } from "./common.js";
11
+ import type { Settings } from "./settings.js";
12
+ export type Ref = { kind: "branch" | "tag"; name: string };
13
+ export type Snapshot = { name: string; snapshotId: string };
14
+ export type Branch = {
15
+ name: string;
16
+ snapshotId: string | null;
17
+ parent: string | null;
18
+ };
19
+ export type Alias = {
20
+ name: string;
21
+ target: string;
22
+ kind: string;
23
+ dangling: boolean;
24
+ };
25
+ export type Hit = { doc: Doc; score?: number };
26
+ /** Narrow protocol boundary for fault-injection tests; LambdaDB is the only CLI backend. */
27
+ export interface CollectionStore {
28
+ fetch(ref: Ref, ids: string[], consistent?: boolean): Promise<Doc[]>;
29
+ list(ref: Ref): AsyncIterable<Doc>;
30
+ upsert(branch: string, docs: Doc[]): Promise<void>;
31
+ delete(branch: string, ids: string[]): Promise<void>;
32
+ branches(): Promise<Branch[]>;
33
+ branch(name: string, source: string): Promise<void>;
34
+ tags(): Promise<Snapshot[]>;
35
+ tag(name: string, source: Ref): Promise<Snapshot>;
36
+ aliases(): Promise<Alias[]>;
37
+ alias(name: string, target: string, exists: boolean): Promise<void>;
38
+ query(
39
+ tag: string,
40
+ query: Record<string, unknown>,
41
+ size: number,
42
+ ): Promise<Hit[]>;
43
+ }
44
+ export class RemoteError extends Error {}
45
+ async function request<T>(
46
+ operation: string,
47
+ call: () => Promise<T>,
48
+ ): Promise<T> {
49
+ try {
50
+ return await call();
51
+ } catch (e) {
52
+ // SDK errors can contain response bodies/source text. Never echo them or credentials.
53
+ const status = (e as { statusCode?: number }).statusCode;
54
+ throw new RemoteError(
55
+ `LambdaDB ${operation} failed${typeof status === "number" ? ` (HTTP ${status})` : ""}. Mutation outcome may be unknown; retain the retry journal.`,
56
+ { cause: e },
57
+ );
58
+ }
59
+ }
60
+ export class LambdaRemote {
61
+ readonly client: LambdaDBClient;
62
+ constructor(readonly settings: Settings) {
63
+ const key = process.env[settings.apiKeyEnv];
64
+ invariant(key, `Missing environment variable ${settings.apiKeyEnv}.`);
65
+ this.client = new LambdaDBClient({
66
+ baseUrl: settings.endpoint,
67
+ projectName: settings.project,
68
+ projectApiKey: key,
69
+ retryConfig: { strategy: "none" },
70
+ timeoutMs: 30_000,
71
+ });
72
+ }
73
+ async collections() {
74
+ return request(
75
+ "list collections",
76
+ async () =>
77
+ (await this.client.listAllCollections({ size: 100 })).collections,
78
+ );
79
+ }
80
+ async create(
81
+ name: string,
82
+ indexConfigs: Record<string, IndexConfigsUnion>,
83
+ description: string,
84
+ tags: Record<string, string>,
85
+ ) {
86
+ await request("create collection", () =>
87
+ this.client.createCollection({
88
+ collectionName: name,
89
+ indexConfigs,
90
+ description,
91
+ tags,
92
+ }),
93
+ );
94
+ }
95
+ store(name: string): CollectionStore {
96
+ const c = this.client.collection(name);
97
+ const ref = (r: Ref) =>
98
+ r.kind === "tag" ? tagRef(r.name) : branchRef(r.name);
99
+ return {
100
+ fetch: (r, ids, consistent = false) =>
101
+ request("fetch", async () => {
102
+ invariant(
103
+ !consistent || r.kind === "branch",
104
+ "Consistent reads require a Branch.",
105
+ );
106
+ const result = await c.docs.fetch(
107
+ r.kind === "branch"
108
+ ? {
109
+ ref: branchRef(r.name),
110
+ ids,
111
+ consistentRead: consistent,
112
+ includeVectors: true,
113
+ }
114
+ : {
115
+ ref: tagRef(r.name),
116
+ ids,
117
+ consistentRead: false,
118
+ includeVectors: true,
119
+ },
120
+ );
121
+ return result.docs.map((d) => d.doc as Doc);
122
+ }),
123
+ async *list(r) {
124
+ let cursor: string | undefined;
125
+ const seen = new Set<string>();
126
+ do {
127
+ const page = await request("list documents", () =>
128
+ c.docs.list({
129
+ ref: ref(r),
130
+ size: 100,
131
+ includeVectors: true,
132
+ pageToken: cursor,
133
+ }),
134
+ );
135
+ for (const d of page.docs) yield d.doc as Doc;
136
+ cursor = page.nextPageToken || undefined;
137
+ if (cursor) {
138
+ invariant(
139
+ !seen.has(cursor),
140
+ "LambdaDB repeated a pagination cursor.",
141
+ );
142
+ seen.add(cursor);
143
+ }
144
+ } while (cursor);
145
+ },
146
+ upsert: async (branch, docs) => {
147
+ await request("upsert", () => c.docs.upsert({ branch, docs }));
148
+ },
149
+ delete: async (branch, ids) => {
150
+ if (ids.length)
151
+ await request("delete", () => c.docs.delete({ branch, ids }));
152
+ },
153
+ branches: () =>
154
+ request("list branches", async () =>
155
+ (await c.branches.list()).branches.map((b) => ({
156
+ name: b.name,
157
+ snapshotId: b.headSnapshot?.snapshotId ?? null,
158
+ parent: b.parentBranch?.name ?? null,
159
+ })),
160
+ ),
161
+ branch: async (name, source) => {
162
+ await request("create branch", () =>
163
+ c.branches.create({ branchName: name, source: branchSource(source) }),
164
+ );
165
+ },
166
+ tags: () =>
167
+ request("list tags", async () =>
168
+ (await c.tags.list()).tags.map((t) => ({
169
+ name: t.name,
170
+ snapshotId: t.snapshotId,
171
+ })),
172
+ ),
173
+ tag: (name, source) =>
174
+ request("create tag", async () => {
175
+ const t = (
176
+ await c.tags.create({
177
+ tagName: name,
178
+ source:
179
+ source.kind === "tag"
180
+ ? tagSource(source.name)
181
+ : branchSource(source.name),
182
+ })
183
+ ).tag;
184
+ return { name: t.name, snapshotId: t.snapshotId };
185
+ }),
186
+ aliases: () =>
187
+ request("list aliases", async () =>
188
+ (await c.aliases.list()).aliases.map((a) => ({
189
+ name: a.aliasName,
190
+ target: a.targetName,
191
+ kind: a.targetKind,
192
+ dangling: a.dangling,
193
+ })),
194
+ ),
195
+ alias: async (name, target, exists) => {
196
+ await request("set alias", () =>
197
+ exists
198
+ ? c.aliases.retarget(name, { target: tagTarget(target) })
199
+ : c.aliases.create({ aliasName: name, target: tagTarget(target) }),
200
+ );
201
+ },
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
+ ),
213
+ };
214
+ }
215
+ }
216
+ export const branch = (name: string): Ref => ({ kind: "branch", name });
217
+ export const tag = (name: string): Ref => ({ kind: "tag", name });
218
+ export async function one(
219
+ store: CollectionStore,
220
+ ref: Ref,
221
+ id: string,
222
+ consistent = false,
223
+ ): Promise<Doc | undefined> {
224
+ const docs = await store.fetch(ref, [id], consistent);
225
+ invariant(
226
+ docs.length <= 1 && docs.every((d) => d.id === id),
227
+ "Unexpected fetch result.",
228
+ );
229
+ return docs[0];
230
+ }
@@ -0,0 +1,306 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import {
4
+ atomic,
5
+ hash,
6
+ invariant,
7
+ optionalJson,
8
+ PURPOSE,
9
+ type Doc,
10
+ } from "./common.js";
11
+ import { PRESET, INDEX_CONFIGS } from "./build.js";
12
+ import { identity, type Identity } from "./git.js";
13
+ import { LambdaRemote, one, branch } from "./remote.js";
14
+ import { exclusive, type Binding } from "./publish.js";
15
+ import { stateRoot, type Settings } from "./settings.js";
16
+ import type { IndexConfigsUnion } from "@functional-systems/lambdadb";
17
+ export type Repository = Binding & {
18
+ collection: string;
19
+ name: string;
20
+ description: string;
21
+ tags: Record<string, string>;
22
+ };
23
+ export function destination(s: Settings, collection: string): string {
24
+ return join(
25
+ stateRoot(),
26
+ "destinations",
27
+ hash([s.endpoint, s.project]),
28
+ collection,
29
+ );
30
+ }
31
+ export function collectionName(
32
+ key: string,
33
+ name: string,
34
+ configHash: string,
35
+ ): string {
36
+ const slug =
37
+ name
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9]+/g, "-")
40
+ .replace(/^-+|-+$/g, "")
41
+ .slice(0, 24) || "repo";
42
+ return `code-${slug}-${hash(["collection-v1", key, configHash]).slice(0, 16)}`;
43
+ }
44
+ /** LambdaDB adds its built-in keyword id index to collection metadata. */
45
+ export function matchesIndexSchema(actual: unknown): boolean {
46
+ if (!actual || typeof actual !== "object" || Array.isArray(actual))
47
+ return false;
48
+ const { id, ...fields } = actual as Record<string, unknown>;
49
+ if (id !== undefined && hash(id) !== hash({ type: "keyword" })) return false;
50
+ return hash(fields) === hash(INDEX_CONFIGS);
51
+ }
52
+ function descriptor(r: Repository): Doc {
53
+ return {
54
+ id: "__repo__",
55
+ kind: "manifest",
56
+ role: "repository",
57
+ schemaVersion: 1,
58
+ configHash: r.configHash,
59
+ repoKey: r.repoKey,
60
+ repoId: r.repoId,
61
+ indexId: r.indexId,
62
+ name: r.name,
63
+ preset: PRESET,
64
+ initialization: "ready",
65
+ };
66
+ }
67
+ function decode(
68
+ doc: Doc | undefined,
69
+ c: {
70
+ collectionName: string;
71
+ description: string;
72
+ tags: Record<string, string>;
73
+ indexConfigs: unknown;
74
+ },
75
+ ): Repository {
76
+ invariant(
77
+ doc?.role === "repository" &&
78
+ doc.initialization === "ready" &&
79
+ doc.configHash === hash(PRESET) &&
80
+ hash(doc.preset) === hash(PRESET),
81
+ "Repository is partially initialized or uses an unsupported preset.",
82
+ );
83
+ invariant(
84
+ doc.indexId === c.tags["index-id"] &&
85
+ doc.configHash === c.tags["config-hash"] &&
86
+ c.tags.purpose === PURPOSE,
87
+ "Repository descriptor and Collection labels disagree.",
88
+ );
89
+ invariant(
90
+ matchesIndexSchema(c.indexConfigs),
91
+ "Collection schema differs from the pinned preset.",
92
+ );
93
+ invariant(
94
+ typeof doc.repoKey === "string" &&
95
+ typeof doc.repoId === "string" &&
96
+ typeof doc.indexId === "string" &&
97
+ typeof doc.name === "string",
98
+ "Malformed repository descriptor.",
99
+ );
100
+ return {
101
+ collection: c.collectionName,
102
+ repoKey: doc.repoKey,
103
+ repoId: doc.repoId,
104
+ indexId: doc.indexId,
105
+ name: doc.name,
106
+ configHash: String(doc.configHash),
107
+ description: c.description,
108
+ tags: c.tags,
109
+ };
110
+ }
111
+ export async function discover(remote: LambdaRemote): Promise<{
112
+ repositories: Repository[];
113
+ partial: { collection: string; reason: string }[];
114
+ }> {
115
+ const repositories: Repository[] = [];
116
+ const partial: { collection: string; reason: string }[] = [];
117
+ for (const c of await remote.collections()) {
118
+ if (c.tags.purpose !== PURPOSE) continue;
119
+ const doc = await one(
120
+ remote.store(c.collectionName),
121
+ branch("main"),
122
+ "__repo__",
123
+ true,
124
+ );
125
+ try {
126
+ repositories.push(decode(doc, c));
127
+ } catch {
128
+ partial.push({
129
+ collection: c.collectionName,
130
+ reason: "Missing/incompatible repository descriptor or schema.",
131
+ });
132
+ }
133
+ }
134
+ return { repositories, partial };
135
+ }
136
+ export async function selectRepository(
137
+ remote: LambdaRemote,
138
+ selector: string,
139
+ ): Promise<Repository> {
140
+ const { repositories, partial } = await discover(remote);
141
+ const selected = repositories.filter(
142
+ (r) =>
143
+ r.name === selector ||
144
+ r.repoKey === selector ||
145
+ r.repoKey.split("/").slice(1).join("/") === selector ||
146
+ r.collection === selector,
147
+ );
148
+ invariant(
149
+ selected.length === 1,
150
+ selected.length
151
+ ? "Ambiguous repository; use its full source identity or Collection name."
152
+ : `Repository not found${partial.length ? "; partially initialized collections exist (see repo list)" : ""}.`,
153
+ );
154
+ return selected[0]!;
155
+ }
156
+ export async function register(
157
+ remote: LambdaRemote,
158
+ args: {
159
+ path: string;
160
+ remote?: string;
161
+ description?: string;
162
+ labels?: Record<string, string>;
163
+ },
164
+ ): Promise<Repository> {
165
+ const source = await identity(args.path, args.remote);
166
+ const configHash = hash(PRESET),
167
+ collection = collectionName(source.key, source.name, configHash),
168
+ state = destination(remote.settings, collection);
169
+ return exclusive(state, async () => {
170
+ const initPath = join(state, "initialization.json");
171
+ let intent = await optionalJson<Repository>(initPath);
172
+ const existing = (await remote.collections()).find(
173
+ (c) => c.collectionName === collection,
174
+ );
175
+ if (existing) {
176
+ const d = await one(
177
+ remote.store(collection),
178
+ branch("main"),
179
+ "__repo__",
180
+ true,
181
+ );
182
+ if (d) {
183
+ const result = decode(d, existing);
184
+ invariant(result.repoKey === source.key, "Collection name collision.");
185
+ await atomic(join(state, "attachment.json"), source);
186
+ return result;
187
+ }
188
+ }
189
+ if (!intent) {
190
+ invariant(
191
+ !existing,
192
+ "Collection exists without a descriptor or a local provisioning journal; refusing adoption.",
193
+ );
194
+ const labels = args.labels ?? {};
195
+ const reserved = ["purpose", "repository", "index-id", "config-hash"];
196
+ invariant(
197
+ Object.keys(labels).length <= 1 &&
198
+ !Object.keys(labels).some((k) => reserved.includes(k)),
199
+ "At most one nonreserved metadata tag is allowed.",
200
+ );
201
+ invariant(
202
+ Object.entries(labels).every(
203
+ ([k, v]) =>
204
+ /^[A-Za-z0-9_.-]{1,63}$/.test(k) &&
205
+ v.length >= 1 &&
206
+ v.length <= 127 &&
207
+ v.trim().length > 0 &&
208
+ !/[:#,]/.test(v),
209
+ ),
210
+ "Invalid metadata tag.",
211
+ );
212
+ const description =
213
+ args.description ??
214
+ `Source, tests, and documentation for ${source.key.slice(0, 170)}; version-aware code search.`;
215
+ invariant(
216
+ description.length <= 255,
217
+ "Description exceeds 255 characters.",
218
+ );
219
+ const indexId = randomUUID();
220
+ intent = {
221
+ repoKey: source.key,
222
+ name: source.name,
223
+ repoId: randomUUID(),
224
+ indexId,
225
+ collection,
226
+ configHash,
227
+ description,
228
+ tags: {
229
+ purpose: PURPOSE,
230
+ ...(source.key.length <= 127 && !/[:#,]/.test(source.key)
231
+ ? { repository: source.key }
232
+ : {}),
233
+ "index-id": indexId,
234
+ "config-hash": configHash,
235
+ ...labels,
236
+ },
237
+ };
238
+ await atomic(initPath, intent);
239
+ }
240
+ invariant(
241
+ intent.repoKey === source.key && intent.configHash === configHash,
242
+ "Provisioning journal identity mismatch.",
243
+ );
244
+ if (existing)
245
+ invariant(
246
+ hash(existing.tags) === hash(intent.tags) &&
247
+ matchesIndexSchema(existing.indexConfigs),
248
+ "Existing Collection differs from provisioning journal.",
249
+ );
250
+ else
251
+ await remote.create(
252
+ collection,
253
+ JSON.parse(JSON.stringify(INDEX_CONFIGS)) as Record<
254
+ string,
255
+ IndexConfigsUnion
256
+ >,
257
+ intent.description,
258
+ intent.tags,
259
+ );
260
+ const store = remote.store(collection);
261
+ const branches = await store.branches();
262
+ const empty = branches.find((b) => b.name === "checkpoint-empty");
263
+ if (empty)
264
+ invariant(
265
+ empty.snapshotId === null && empty.parent === "main",
266
+ "Empty checkpoint has been modified.",
267
+ );
268
+ else {
269
+ const main = branches.find((b) => b.name === "main");
270
+ invariant(
271
+ main && main.snapshotId === null,
272
+ "Main is not empty; cannot safely initialize checkpoint.",
273
+ );
274
+ await store.branch("checkpoint-empty", "main");
275
+ }
276
+ await store.upsert("main", [descriptor(intent)]);
277
+ const c = (await remote.collections()).find(
278
+ (c) => c.collectionName === collection,
279
+ );
280
+ invariant(c, "Created Collection not discoverable; retry repo add.");
281
+ const result = decode(
282
+ await one(store, branch("main"), "__repo__", true),
283
+ c,
284
+ );
285
+ await atomic(join(state, "attachment.json"), source);
286
+ return result;
287
+ });
288
+ }
289
+ export async function attachment(
290
+ s: Settings,
291
+ r: Repository,
292
+ ): Promise<Identity> {
293
+ const saved = await optionalJson<Identity>(
294
+ join(destination(s, r.collection), "attachment.json"),
295
+ );
296
+ invariant(
297
+ saved,
298
+ "No local checkout attached. Use repo add --path for this repository.",
299
+ );
300
+ const current = await identity(saved.path, saved.remote);
301
+ invariant(
302
+ current.key === r.repoKey,
303
+ "Attached checkout now has a different source identity.",
304
+ );
305
+ return current;
306
+ }