@functional-systems/srcx 0.1.0-dev.3 → 0.1.0-dev.31

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 (61) hide show
  1. package/CHANGELOG.md +137 -1
  2. package/DESIGN.md +193 -58
  3. package/README.md +553 -38
  4. package/RELEASING.md +30 -9
  5. package/VALIDATION.md +855 -25
  6. package/dist/build.d.ts +81 -1
  7. package/dist/build.js +187 -37
  8. package/dist/build.js.map +1 -1
  9. package/dist/chunk.d.ts +9 -3
  10. package/dist/chunk.js +341 -14
  11. package/dist/chunk.js.map +1 -1
  12. package/dist/cli.js +103 -19
  13. package/dist/cli.js.map +1 -1
  14. package/dist/file-policy.d.ts +20 -0
  15. package/dist/file-policy.js +96 -0
  16. package/dist/file-policy.js.map +1 -0
  17. package/dist/implementation.d.ts +11 -0
  18. package/dist/implementation.js +191 -0
  19. package/dist/implementation.js.map +1 -0
  20. package/dist/publish.d.ts +18 -0
  21. package/dist/publish.js +158 -34
  22. package/dist/publish.js.map +1 -1
  23. package/dist/releases.js +21 -3
  24. package/dist/releases.js.map +1 -1
  25. package/dist/remote.d.ts +1 -0
  26. package/dist/remote.js +50 -9
  27. package/dist/remote.js.map +1 -1
  28. package/dist/repository.d.ts +4 -1
  29. package/dist/repository.js +26 -11
  30. package/dist/repository.js.map +1 -1
  31. package/dist/rerank.d.ts +16 -0
  32. package/dist/rerank.js +87 -0
  33. package/dist/rerank.js.map +1 -0
  34. package/dist/search.d.ts +14 -4
  35. package/dist/search.js +119 -23
  36. package/dist/search.js.map +1 -1
  37. package/dist/skills.d.ts +24 -0
  38. package/dist/skills.js +155 -0
  39. package/dist/skills.js.map +1 -0
  40. package/package.json +16 -5
  41. package/runtime/grammars/README.md +50 -0
  42. package/runtime/grammars/tree-sitter-bash.LICENSE +21 -0
  43. package/runtime/grammars/tree-sitter-bash.wasm +0 -0
  44. package/runtime/grammars/tree-sitter-sql.LICENSE +21 -0
  45. package/runtime/grammars/tree-sitter-sql.wasm +0 -0
  46. package/runtime/grammars/tree-sitter-wasm.LICENSE +21 -0
  47. package/runtime/qwen.py +132 -0
  48. package/runtime/requirements.txt +34 -0
  49. package/skills/srcx-search/SKILL.md +85 -0
  50. package/src/build.ts +258 -47
  51. package/src/chunk.ts +360 -12
  52. package/src/cli.ts +212 -24
  53. package/src/file-policy.ts +157 -0
  54. package/src/implementation.ts +253 -0
  55. package/src/publish.ts +226 -34
  56. package/src/releases.ts +36 -3
  57. package/src/remote.ts +70 -13
  58. package/src/repository.ts +40 -11
  59. package/src/rerank.ts +131 -0
  60. package/src/search.ts +154 -28
  61. package/src/skills.ts +205 -0
package/src/build.ts CHANGED
@@ -15,6 +15,26 @@ import {
15
15
  type Doc,
16
16
  optionalJson,
17
17
  } from "./common.js";
18
+ import {
19
+ FILE_POLICY,
20
+ normalizeFilePolicy,
21
+ hardExclusion,
22
+ privateKeyContent,
23
+ fileDecision,
24
+ type FilePolicy,
25
+ type FileDecision,
26
+ } from "./file-policy.js";
27
+ export const ANALYZERS = ["english", "japanese", "korean", "standard"] as const;
28
+ export type Analyzer = (typeof ANALYZERS)[number];
29
+ export function normalizeAnalyzers(value: unknown): Analyzer[] {
30
+ invariant(
31
+ Array.isArray(value) &&
32
+ value.length > 0 &&
33
+ value.every((name) => ANALYZERS.includes(name)),
34
+ `Analyzers must be a nonempty list of: ${ANALYZERS.join(", ")}.`,
35
+ );
36
+ return [...new Set(value as Analyzer[])].sort();
37
+ }
18
38
  export const INDEX_CONFIGS = {
19
39
  kind: { type: "keyword" },
20
40
  path: { type: "keyword" },
@@ -25,18 +45,123 @@ export const INDEX_CONFIGS = {
25
45
  } as const;
26
46
  export type Preset = {
27
47
  schemaVersion: 1;
48
+ analyzers: readonly Analyzer[];
28
49
  chunker: typeof CHUNKER;
29
50
  mode: "syntax" | "window";
51
+ /** Internal evaluation override; the CLI keeps its original enrichment. */
52
+ enrichment?: "path-only-v1";
30
53
  maxFileBytes: number;
31
- embedding: { model: string; dimensions: number } | null;
54
+ filePolicy: FilePolicy;
55
+ embedding: {
56
+ model: string;
57
+ dimensions: number;
58
+ managed?: true;
59
+ provider?: "openai";
60
+ similarity?: "cosine";
61
+ sourceField?: "embeddingText";
62
+ } | null;
32
63
  };
33
64
  export const PRESET: Preset = {
34
65
  schemaVersion: 1,
66
+ analyzers: ["standard"],
35
67
  chunker: CHUNKER,
36
68
  mode: "syntax",
37
69
  maxFileBytes: 1024 * 1024,
70
+ filePolicy: FILE_POLICY,
38
71
  embedding: null,
39
72
  };
73
+ /** Embedding variants share the same chunking behavior. */
74
+ export const MANAGED_PRESET: Preset = {
75
+ ...PRESET,
76
+ embedding: {
77
+ managed: true,
78
+ provider: "openai",
79
+ model: "text-embedding-3-small",
80
+ dimensions: 1536,
81
+ similarity: "cosine",
82
+ sourceField: "embeddingText",
83
+ },
84
+ };
85
+ export const MANAGED_LARGE_PRESET: Preset = {
86
+ ...MANAGED_PRESET,
87
+ embedding: {
88
+ ...MANAGED_PRESET.embedding!,
89
+ model: "text-embedding-3-large",
90
+ dimensions: 3072,
91
+ },
92
+ };
93
+ export function presetFor(
94
+ embedding = "none",
95
+ analyzers: readonly string[] = PRESET.analyzers,
96
+ filePolicy: unknown = FILE_POLICY,
97
+ ): Preset {
98
+ invariant(
99
+ ["none", "text-embedding-3-small", "text-embedding-3-large"].includes(
100
+ embedding,
101
+ ),
102
+ "Embedding must be none, text-embedding-3-small or text-embedding-3-large.",
103
+ );
104
+ const base =
105
+ embedding === "none"
106
+ ? PRESET
107
+ : embedding === "text-embedding-3-large"
108
+ ? MANAGED_LARGE_PRESET
109
+ : MANAGED_PRESET;
110
+ const normalized = normalizeAnalyzers(analyzers);
111
+ const configured = {
112
+ ...base,
113
+ analyzers: normalized,
114
+ filePolicy: normalizeFilePolicy(filePolicy),
115
+ };
116
+ return hash(configured) === hash(base) ? base : configured;
117
+ }
118
+ export function supportedPreset(preset: unknown): preset is Preset {
119
+ if (!preset || typeof preset !== "object" || Array.isArray(preset))
120
+ return false;
121
+ try {
122
+ const analyzers = normalizeAnalyzers((preset as Preset).analyzers);
123
+ const filePolicy = normalizeFilePolicy((preset as Preset).filePolicy);
124
+ return [PRESET, MANAGED_PRESET, MANAGED_LARGE_PRESET].some(
125
+ (base) => hash({ ...base, analyzers, filePolicy }) === hash(preset),
126
+ );
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+ export function indexConfigs(preset: Preset = PRESET) {
132
+ const text = { type: "text" as const, analyzers: [...preset.analyzers] };
133
+ const indexes = { ...INDEX_CONFIGS, searchText: text };
134
+ if (!preset.embedding?.managed) return indexes;
135
+ const { managed: _, ...embedding } = preset.embedding;
136
+ return {
137
+ ...indexes,
138
+ embeddingText: { ...text, analyzers: [...preset.analyzers] },
139
+ embedding: { type: "vector", managedEmbedding: true, embedding },
140
+ };
141
+ }
142
+ /** Validate server enrichment separately; all other payload fields remain exact. */
143
+ export function recordHash(doc: Doc, preset: Preset): string {
144
+ if (!preset.embedding?.managed) return hash(doc);
145
+ const { embedding, ...source } = doc;
146
+ if (doc.kind === "chunk" && doc.embeddingStatus === "managed") {
147
+ invariant(
148
+ typeof doc.embeddingText === "string" &&
149
+ doc.embeddingText === doc.searchText &&
150
+ doc.embeddingInputHash ===
151
+ hash([preset.embedding, doc.embeddingText]) &&
152
+ Array.isArray(embedding) &&
153
+ embedding.length === preset.embedding.dimensions &&
154
+ embedding.every((v) => typeof v === "number" && Number.isFinite(v)) &&
155
+ embedding.some((v) => v !== 0),
156
+ "Missing or invalid managed embedding.",
157
+ );
158
+ } else
159
+ invariant(
160
+ embedding === undefined && doc.embeddingText === undefined,
161
+ "Unexpected managed embedding on an ineligible record.",
162
+ );
163
+ return hash(source);
164
+ }
40
165
  export type InventoryItem = {
41
166
  path: string;
42
167
  pathBase64: string;
@@ -45,6 +170,8 @@ export type InventoryItem = {
45
170
  bytes: number;
46
171
  status: "included" | "excluded";
47
172
  reason?: string;
173
+ retrieval?: "lexical" | "semantic";
174
+ policyReason?: string;
48
175
  targetBase64?: string;
49
176
  fileId?: string;
50
177
  chunkIds?: string[];
@@ -52,6 +179,8 @@ export type InventoryItem = {
52
179
  parseStatus?: string;
53
180
  tokens?: number;
54
181
  embedded?: number;
182
+ managed?: number;
183
+ managedTokens?: number;
55
184
  skipped?: number;
56
185
  };
57
186
  export type Build = {
@@ -104,25 +233,10 @@ export async function loadBuild(directory: string): Promise<Build> {
104
233
  b.directory = resolve(directory);
105
234
  return b;
106
235
  }
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
236
  export async function materialize(args: {
124
237
  identity: Identity;
125
238
  ref: string;
239
+ resolvedCommit?: Awaited<ReturnType<typeof resolveCommit>>;
126
240
  output: string;
127
241
  previous?: Build;
128
242
  preset?: Preset;
@@ -133,8 +247,12 @@ export async function materialize(args: {
133
247
  configHash = hash(preset),
134
248
  prev = args.previous;
135
249
  invariant(
136
- hash(preset.chunker) === hash(CHUNKER) &&
250
+ hash(preset.filePolicy) === hash(normalizeFilePolicy(preset.filePolicy)) &&
251
+ hash(preset.analyzers) === hash(normalizeAnalyzers(preset.analyzers)) &&
252
+ hash(preset.chunker) === hash(CHUNKER) &&
137
253
  ["syntax", "window"].includes(preset.mode) &&
254
+ (preset.enrichment === undefined ||
255
+ preset.enrichment === "path-only-v1") &&
138
256
  preset.maxFileBytes === PRESET.maxFileBytes,
139
257
  "Unsupported build preset.",
140
258
  );
@@ -147,11 +265,13 @@ export async function materialize(args: {
147
265
  }
148
266
  invariant(
149
267
  !preset.embedding ||
268
+ (preset.embedding.managed && supportedPreset(preset)) ||
150
269
  (args.embedder?.model === preset.embedding.model &&
151
270
  args.embedder.dimensions === preset.embedding.dimensions),
152
271
  "Embedding provider must match the pinned preset.",
153
272
  );
154
- const commit = await resolveCommit(args.identity.path, args.ref);
273
+ const commit =
274
+ args.resolvedCommit ?? (await resolveCommit(args.identity.path, args.ref));
155
275
  const directory = resolve(args.output);
156
276
  await mkdir(directory, { recursive: false, mode: 0o700 });
157
277
  const output = await open(join(directory, "records.jsonl"), "wx", 0o600);
@@ -200,7 +320,10 @@ export async function materialize(args: {
200
320
  item.targetBase64 = (await blobs.read(e)).toString("base64");
201
321
  continue;
202
322
  }
203
- const reason = exclude(e.path);
323
+ const pathDecision = fileDecision(e.path, preset.filePolicy);
324
+ const reason =
325
+ hardExclusion(e.path) ??
326
+ (pathDecision.action === "exclude" ? pathDecision.reason : undefined);
204
327
  if (reason) {
205
328
  item.reason = reason;
206
329
  continue;
@@ -233,7 +356,17 @@ export async function materialize(args: {
233
356
  item.reason = "lfs-pointer";
234
357
  continue;
235
358
  }
236
- const parsed = await chunk(source, e.path, preset.mode);
359
+ if (privateKeyContent(source)) {
360
+ item.reason = "private-key-content";
361
+ continue;
362
+ }
363
+ const decision = fileDecision(e.path, preset.filePolicy, source);
364
+ const parsed = await chunk(
365
+ source,
366
+ e.path,
367
+ preset.mode,
368
+ preset.enrichment,
369
+ );
237
370
  const contentHash = hash(data);
238
371
  const fileId = `f-${hash([e.pathBase64, contentHash])}`;
239
372
  const base = { schemaVersion: 1, configHash };
@@ -256,6 +389,8 @@ export async function materialize(args: {
256
389
  }
257
390
  Object.assign(item, {
258
391
  status: "included",
392
+ retrieval: decision.action,
393
+ policyReason: decision.reason,
259
394
  contentHash,
260
395
  fileId,
261
396
  chunkIds: [],
@@ -282,39 +417,52 @@ export async function materialize(args: {
282
417
  ? "structural-only"
283
418
  : "embedding-disabled",
284
419
  };
285
- const eligible = !["imports", "structural"].includes(span.chunkKind);
420
+ const eligible =
421
+ decision.action === "semantic" &&
422
+ !["imports", "structural"].includes(span.chunkKind);
286
423
  if (preset.embedding && eligible) {
287
424
  const inputHash = hash([preset.embedding, span.searchText]);
288
- const cachePath = args.cache
289
- ? join(args.cache, inputHash + ".json")
290
- : undefined;
291
- let vector = cachePath
292
- ? await optionalJson<number[]>(cachePath)
293
- : undefined;
294
- if (!vector) {
295
- vector = await args.embedder!.embed(span.searchText);
425
+ if (preset.embedding.managed) {
426
+ doc.embeddingText = span.searchText;
427
+ doc.embeddingInputHash = inputHash;
428
+ doc.embeddingStatus = "managed";
429
+ delete doc.embeddingSkipReason;
430
+ item.managed = (item.managed ?? 0) + 1;
431
+ item.managedTokens = (item.managedTokens ?? 0) + span.tokenCount;
432
+ } else {
433
+ const cachePath = args.cache
434
+ ? join(args.cache, inputHash + ".json")
435
+ : undefined;
436
+ let vector = cachePath
437
+ ? await optionalJson<number[]>(cachePath)
438
+ : undefined;
439
+ if (!vector) {
440
+ vector = await args.embedder!.embed(span.searchText);
441
+ invariant(
442
+ vector.length === preset.embedding.dimensions &&
443
+ vector.every(Number.isFinite),
444
+ "Invalid embedding output.",
445
+ );
446
+ if (cachePath) await atomic(cachePath, vector);
447
+ }
296
448
  invariant(
297
449
  vector.length === preset.embedding.dimensions &&
298
450
  vector.every(Number.isFinite),
299
- "Invalid embedding output.",
451
+ "Invalid cached vector.",
300
452
  );
301
- if (cachePath) await atomic(cachePath, vector);
453
+ doc.embedding = vector;
454
+ doc.embeddingInputHash = inputHash;
455
+ doc.embeddingStatus = "embedded";
456
+ delete doc.embeddingSkipReason;
457
+ item.embedded!++;
302
458
  }
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
459
  } else {
314
460
  doc.embeddingSkipReason = preset.embedding
315
- ? span.chunkKind === "imports"
316
- ? "imports-only"
317
- : "structural-only"
461
+ ? decision.action === "lexical"
462
+ ? decision.reason
463
+ : span.chunkKind === "imports"
464
+ ? "imports-only"
465
+ : "structural-only"
318
466
  : "embedding-disabled";
319
467
  item.skipped!++;
320
468
  }
@@ -388,6 +536,15 @@ export async function materialize(args: {
388
536
  excluded: items.filter((e) => e.status === "excluded").length,
389
537
  chunks: items.reduce((n, e) => n + (e.chunkIds?.length ?? 0), 0),
390
538
  tokens: items.reduce((n, e) => n + (e.tokens ?? 0), 0),
539
+ ...(preset.embedding?.managed
540
+ ? {
541
+ managed: items.reduce((n, e) => n + (e.managed ?? 0), 0),
542
+ managedTokens: items.reduce(
543
+ (n, e) => n + (e.managedTokens ?? 0),
544
+ 0,
545
+ ),
546
+ }
547
+ : {}),
391
548
  embedded: items.reduce((n, e) => n + (e.embedded ?? 0), 0),
392
549
  skipped: items.reduce((n, e) => n + (e.skipped ?? 0), 0),
393
550
  },
@@ -405,7 +562,12 @@ export async function validateBuild(b: Build): Promise<void> {
405
562
  "Build commit identity mismatch; rebuild from the intended Git commit.",
406
563
  );
407
564
  invariant(
408
- hash(b.preset) === b.configHash && hash(b.inventory) === b.inventoryHash,
565
+ hash(b.preset.filePolicy) ===
566
+ hash(normalizeFilePolicy(b.preset.filePolicy)) &&
567
+ hash(b.preset.analyzers) ===
568
+ hash(normalizeAnalyzers(b.preset.analyzers)) &&
569
+ hash(b.preset) === b.configHash &&
570
+ hash(b.inventory) === b.inventoryHash,
409
571
  "Build metadata hash mismatch.",
410
572
  );
411
573
  const seen = new Set<string>();
@@ -417,6 +579,7 @@ export async function validateBuild(b: Build): Promise<void> {
417
579
  .filter((e) => e.status === "included")
418
580
  .map((e) => [e.fileId!, e]),
419
581
  );
582
+ let activeDecision: FileDecision | undefined;
420
583
  const completedFiles = new Set<string>();
421
584
  const finish = () => {
422
585
  if (activeFile) {
@@ -439,12 +602,36 @@ export async function validateBuild(b: Build): Promise<void> {
439
602
  doc.configHash === b.configHash && doc.schemaVersion === 1,
440
603
  "Mixed configuration.",
441
604
  );
605
+ if (b.preset.embedding?.managed) {
606
+ invariant(
607
+ doc.embedding === undefined,
608
+ "Managed artifacts must not contain vectors.",
609
+ );
610
+ if (doc.kind !== "chunk")
611
+ invariant(
612
+ doc.embeddingText === undefined,
613
+ "Only eligible chunks can contain embedding input.",
614
+ );
615
+ }
442
616
  if (doc.kind === "file") {
443
617
  finish();
444
618
  activeFile = doc;
445
619
  end = 0;
446
620
  count = 0;
447
621
  const entry = expectedFile.get(doc.id);
622
+ activeDecision = fileDecision(
623
+ doc.path as string,
624
+ b.preset.filePolicy,
625
+ doc.sourceText as string,
626
+ );
627
+ invariant(
628
+ !hardExclusion(doc.path as string) &&
629
+ !privateKeyContent(doc.sourceText as string) &&
630
+ activeDecision.action !== "exclude" &&
631
+ entry?.retrieval === activeDecision.action &&
632
+ entry?.policyReason === activeDecision.reason,
633
+ "File policy or inventory decision mismatch.",
634
+ );
448
635
  const raw = Buffer.from(doc.sourceText as string);
449
636
  invariant(
450
637
  entry &&
@@ -494,9 +681,33 @@ export async function validateBuild(b: Build): Promise<void> {
494
681
  Number(doc.tokenCount) <= CHUNKER.maxTokens,
495
682
  "Invalid chunk source/position/token binding.",
496
683
  );
497
- if (doc.embeddingStatus === "embedded")
684
+ if (b.preset.embedding?.managed) {
685
+ const eligible =
686
+ activeDecision!.action === "semantic" &&
687
+ !["imports", "structural"].includes(String(doc.chunkKind));
688
+ invariant(
689
+ eligible
690
+ ? doc.embeddingStatus === "managed" &&
691
+ doc.embeddingText === doc.searchText &&
692
+ doc.embeddingInputHash ===
693
+ hash([b.preset.embedding, doc.searchText]) &&
694
+ doc.embeddingSkipReason === undefined
695
+ : doc.embeddingStatus === "skipped" &&
696
+ doc.embeddingText === undefined &&
697
+ doc.embeddingInputHash === undefined &&
698
+ doc.embeddingSkipReason ===
699
+ (activeDecision!.action === "lexical"
700
+ ? activeDecision!.reason
701
+ : doc.chunkKind === "imports"
702
+ ? "imports-only"
703
+ : "structural-only"),
704
+ "Invalid managed embedding input or eligibility.",
705
+ );
706
+ } else if (doc.embeddingStatus === "embedded")
498
707
  invariant(
499
708
  b.preset.embedding &&
709
+ activeDecision!.action === "semantic" &&
710
+ !["imports", "structural"].includes(String(doc.chunkKind)) &&
500
711
  Array.isArray(doc.embedding) &&
501
712
  doc.embedding.length === b.preset.embedding.dimensions &&
502
713
  doc.embedding.every(Number.isFinite),