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

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 (60) hide show
  1. package/CHANGELOG.md +132 -1
  2. package/DESIGN.md +193 -58
  3. package/README.md +542 -38
  4. package/RELEASING.md +30 -9
  5. package/VALIDATION.md +834 -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.js +38 -9
  26. package/dist/remote.js.map +1 -1
  27. package/dist/repository.d.ts +4 -1
  28. package/dist/repository.js +15 -11
  29. package/dist/repository.js.map +1 -1
  30. package/dist/rerank.d.ts +16 -0
  31. package/dist/rerank.js +87 -0
  32. package/dist/rerank.js.map +1 -0
  33. package/dist/search.d.ts +14 -4
  34. package/dist/search.js +119 -23
  35. package/dist/search.js.map +1 -1
  36. package/dist/skills.d.ts +24 -0
  37. package/dist/skills.js +155 -0
  38. package/dist/skills.js.map +1 -0
  39. package/package.json +16 -5
  40. package/runtime/grammars/README.md +50 -0
  41. package/runtime/grammars/tree-sitter-bash.LICENSE +21 -0
  42. package/runtime/grammars/tree-sitter-bash.wasm +0 -0
  43. package/runtime/grammars/tree-sitter-sql.LICENSE +21 -0
  44. package/runtime/grammars/tree-sitter-sql.wasm +0 -0
  45. package/runtime/grammars/tree-sitter-wasm.LICENSE +21 -0
  46. package/runtime/qwen.py +132 -0
  47. package/runtime/requirements.txt +34 -0
  48. package/skills/srcx-search/SKILL.md +85 -0
  49. package/src/build.ts +258 -47
  50. package/src/chunk.ts +360 -12
  51. package/src/cli.ts +212 -24
  52. package/src/file-policy.ts +157 -0
  53. package/src/implementation.ts +253 -0
  54. package/src/publish.ts +226 -34
  55. package/src/releases.ts +36 -3
  56. package/src/remote.ts +59 -13
  57. package/src/repository.ts +20 -11
  58. package/src/rerank.ts +131 -0
  59. package/src/search.ts +154 -28
  60. package/src/skills.ts +205 -0
package/src/chunk.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { fileURLToPath } from "node:url";
1
2
  import { createRequire } from "node:module";
2
3
  import { Parser, Language, type Node } from "web-tree-sitter";
3
4
  import { getEncoding } from "js-tiktoken";
@@ -8,15 +9,19 @@ export function tokens(text: string): number {
8
9
  return encoding.encode(text, [], []).length;
9
10
  }
10
11
  export const CHUNKER = {
11
- version: 1,
12
+ version: 2,
12
13
  parser: "web-tree-sitter@0.25.10",
13
14
  grammars: "tree-sitter-wasms@0.1.13",
15
+ sqlGrammar:
16
+ "tree-sitter-wasm@2.0.2/sql/b77530893b1dd6d1d4814eacf34d25bcbe4a12ec9a25a8c45b320d447265f42b",
17
+ shellGrammar:
18
+ "tree-sitter-wasm@2.0.2/bash/5daf3f2ac1cea01a64cfd6878ef7d247310925a4c62238eb4b8c1feb5dce355a",
14
19
  tokenizer: "js-tiktoken@1.0.21/cl100k_base",
15
20
  targetTokens: 800,
16
21
  maxTokens: 1500,
17
22
  fallbackOverlap: 0.1,
18
23
  enrichment: "path-scope-symbol-v1",
19
- policy: "source-v1",
24
+ policy: "source-v2",
20
25
  } as const;
21
26
  export type Span = {
22
27
  startByte: number;
@@ -46,17 +51,40 @@ async function language(name: string): Promise<Language> {
46
51
  let l = languages.get(name);
47
52
  if (!l) {
48
53
  l = await Language.load(
49
- require.resolve(`tree-sitter-wasms/out/tree-sitter-${name}.wasm`),
54
+ name === "sql" || name === "shell"
55
+ ? fileURLToPath(
56
+ new URL(
57
+ `../runtime/grammars/tree-sitter-${name === "shell" ? "bash" : "sql"}.wasm`,
58
+ import.meta.url,
59
+ ),
60
+ )
61
+ : require.resolve(`tree-sitter-wasms/out/tree-sitter-${name}.wasm`),
50
62
  );
51
63
  languages.set(name, l);
52
64
  }
53
65
  return l;
54
66
  }
55
67
  export function detectLanguage(path: string): string {
68
+ if (path.endsWith(".C")) return "cpp";
56
69
  const ext = path.split(".").at(-1)?.toLowerCase();
57
70
  return (
58
71
  (
59
72
  {
73
+ py: "python",
74
+ pyi: "python",
75
+ go: "go",
76
+ rs: "rust",
77
+ c: "c",
78
+ h: "c",
79
+ cc: "cpp",
80
+ cpp: "cpp",
81
+ cxx: "cpp",
82
+ hh: "cpp",
83
+ hpp: "cpp",
84
+ hxx: "cpp",
85
+ sh: "shell",
86
+ bash: "shell",
87
+ sql: "sql",
60
88
  java: "java",
61
89
  ts: "typescript",
62
90
  tsx: "tsx",
@@ -75,7 +103,9 @@ export function detectLanguage(path: string): string {
75
103
  )[ext ?? ""] ?? "text"
76
104
  );
77
105
  }
78
- function enrichment(path: string, u: Unit): string {
106
+ export type Enrichment = "path-scope-symbol-v1" | "path-only-v1";
107
+ function enrichment(path: string, u: Unit, policy: Enrichment): string {
108
+ if (policy === "path-only-v1") return path.slice(0, 240) + "\n";
79
109
  return (
80
110
  [path.slice(0, 240), u.scope?.slice(0, 160), u.symbol?.slice(0, 160)]
81
111
  .filter(Boolean)
@@ -100,16 +130,289 @@ function scopeName(n: Node): string | undefined {
100
130
  }
101
131
  return names.length ? names.join(".") : undefined;
102
132
  }
133
+
134
+ /** Keep complete functions (including decorators) before token-bound splitting. */
135
+ function pythonUnits(root: Node): Unit[] {
136
+ const units: Unit[] = [];
137
+ function visit(n: Node, scope?: string): void {
138
+ const definition = n.childForFieldName("definition") ?? n;
139
+ const body = definition.childForFieldName("body");
140
+ const symbol = definition.childForFieldName("name")?.text.slice(0, 200);
141
+ const unit: Unit = {
142
+ start: n.startIndex,
143
+ end: n.endIndex,
144
+ kind: classify(definition.type),
145
+ scope,
146
+ symbol,
147
+ };
148
+ if (definition.type === "class_definition" && body) {
149
+ units.push({ ...unit, end: body.startIndex });
150
+ const nested = [scope, symbol].filter(Boolean).join(".");
151
+ for (const child of body.namedChildren) if (child) visit(child, nested);
152
+ } else {
153
+ if (definition.type === "function_definition" && body)
154
+ unit.signature = definition.text
155
+ .slice(0, body.startIndex - definition.startIndex)
156
+ .trim()
157
+ .slice(0, 240);
158
+ if (
159
+ definition.type === "expression_statement" &&
160
+ definition.firstNamedChild?.type === "string"
161
+ )
162
+ unit.kind = "documentation";
163
+ units.push(unit);
164
+ }
165
+ }
166
+ for (const n of root.namedChildren) if (n) visit(n);
167
+ return units;
168
+ }
169
+
170
+ function goUnits(root: Node): Unit[] {
171
+ const units: Unit[] = [];
172
+ for (const n of root.namedChildren) {
173
+ if (!n) continue;
174
+ if (n.type === "type_declaration") {
175
+ // Include the type keyword and grouped declaration delimiters in coverage.
176
+ const specs = n.namedChildren.filter(
177
+ (node): node is Node => node !== null,
178
+ );
179
+ for (const [i, spec] of specs.entries())
180
+ units.push({
181
+ start: i === 0 ? n.startIndex : spec.startIndex,
182
+ end: i === specs.length - 1 ? n.endIndex : spec.endIndex,
183
+ kind: classify(spec.type),
184
+ symbol: spec.childForFieldName("name")?.text.slice(0, 200),
185
+ });
186
+ continue;
187
+ }
188
+ const unit: Unit = {
189
+ start: n.startIndex,
190
+ end: n.endIndex,
191
+ kind: classify(n.type),
192
+ symbol: n.childForFieldName("name")?.text.slice(0, 200),
193
+ };
194
+ let receiver = n
195
+ .childForFieldName("receiver")
196
+ ?.firstNamedChild?.childForFieldName("type");
197
+ if (receiver?.type === "pointer_type") receiver = receiver.firstNamedChild;
198
+ if (receiver?.type === "generic_type")
199
+ receiver = receiver.childForFieldName("type");
200
+ if (receiver) unit.scope = receiver.text.slice(0, 160);
201
+ const body = n.childForFieldName("body");
202
+ if (unit.kind === "function")
203
+ unit.signature = n.text
204
+ .slice(0, body ? body.startIndex - n.startIndex : undefined)
205
+ .trim()
206
+ .slice(0, 240);
207
+ units.push(unit);
208
+ }
209
+ return units;
210
+ }
211
+
212
+ /** Walk declarators rather than parameter/type names (including pointer returns). */
213
+ function declaratorName(n: Node | null): string | undefined {
214
+ if (!n) return undefined;
215
+ if (
216
+ /^(identifier|field_identifier|type_identifier|qualified_identifier|destructor_name|operator_name)$/.test(
217
+ n.type,
218
+ )
219
+ )
220
+ return n.text;
221
+ return declaratorName(
222
+ n.childForFieldName("declarator") ??
223
+ (n.type === "parenthesized_declarator" ? n.firstNamedChild : null),
224
+ );
225
+ }
226
+
227
+ function cUnits(root: Node): Unit[] {
228
+ const units: Unit[] = [];
229
+ function visit(n: Node, scope?: string, start = n.startIndex): void {
230
+ if (n.type === "template_declaration") {
231
+ const declaration = n.namedChildren
232
+ .filter((c): c is Node => !!c && c.type !== "template_parameter_list")
233
+ .at(-1);
234
+ if (declaration) {
235
+ visit(declaration, scope, start);
236
+ return;
237
+ }
238
+ }
239
+ const body = n.childForFieldName("body");
240
+ const symbol =
241
+ n.childForFieldName("name")?.text ??
242
+ declaratorName(n.childForFieldName("declarator"));
243
+ const unit: Unit = {
244
+ start,
245
+ end: n.endIndex,
246
+ kind: n.type === "preproc_include" ? "imports" : classify(n.type),
247
+ scope,
248
+ symbol: symbol?.slice(0, 200),
249
+ };
250
+ if (body && /^(declaration_list|field_declaration_list)$/.test(body.type)) {
251
+ const nested = [scope, symbol].filter(Boolean).join("::") || undefined;
252
+ units.push({ ...unit, end: body.startIndex + 1 });
253
+ for (const child of body.namedChildren) if (child) visit(child, nested);
254
+ units.push({
255
+ start: body.endIndex - 1,
256
+ end: n.endIndex,
257
+ kind: "structural",
258
+ scope: nested,
259
+ });
260
+ } else if (/^preproc_(if|ifdef|elif|else|elifdef)$/.test(n.type)) {
261
+ // Preserve directives and both branches without evaluating the build configuration.
262
+ const condition =
263
+ n.childForFieldName("condition") ?? n.childForFieldName("name");
264
+ const children = n.namedChildren.filter(
265
+ (c): c is Node => !!c && c.id !== condition?.id,
266
+ );
267
+ if (!children.length) {
268
+ units.push(unit);
269
+ return;
270
+ }
271
+ units.push({ ...unit, end: children[0]!.startIndex });
272
+ for (const child of children) visit(child, scope);
273
+ const end = children.at(-1)!.endIndex;
274
+ if (end < n.endIndex)
275
+ units.push({ start: end, end: n.endIndex, kind: "structural", scope });
276
+ } else {
277
+ if (n.type === "function_definition")
278
+ unit.signature = n.text
279
+ .slice(0, body ? body.startIndex - n.startIndex : undefined)
280
+ .trim()
281
+ .slice(0, 240);
282
+ units.push(unit);
283
+ }
284
+ }
285
+ for (const n of root.namedChildren) if (n) visit(n);
286
+ return units;
287
+ }
288
+
289
+ function shellUnits(root: Node): Unit[] {
290
+ return root.namedChildren
291
+ .filter((n): n is Node => !!n)
292
+ .map((n) => {
293
+ const body = n.childForFieldName("body");
294
+ return {
295
+ start: n.startIndex,
296
+ end: n.endIndex,
297
+ kind: classify(n.type),
298
+ symbol:
299
+ n.type === "function_definition"
300
+ ? n.childForFieldName("name")?.text.slice(0, 200)
301
+ : undefined,
302
+ signature:
303
+ n.type === "function_definition"
304
+ ? n.text
305
+ .slice(0, body ? body.startIndex - n.startIndex : undefined)
306
+ .trim()
307
+ .slice(0, 240)
308
+ : undefined,
309
+ };
310
+ });
311
+ }
312
+
313
+ function sqlUnits(root: Node): Unit[] {
314
+ return root.namedChildren
315
+ .filter((n): n is Node => !!n)
316
+ .map((n) => {
317
+ const declaration = n.firstNamedChild;
318
+ // Object names describe DDL declarations, not every table referenced by a query.
319
+ const object = declaration?.type.startsWith("create_")
320
+ ? declaration.namedChildren.find((c) => c?.type === "object_reference")
321
+ : undefined;
322
+ return {
323
+ start: n.startIndex,
324
+ end: n.nextSibling?.type === ";" ? n.nextSibling.endIndex : n.endIndex,
325
+ kind: n.type === "comment" ? "documentation" : "statement",
326
+ symbol: object?.childForFieldName("name")?.text.slice(0, 200),
327
+ scope: object?.childForFieldName("schema")?.text.slice(0, 160),
328
+ };
329
+ });
330
+ }
331
+
332
+ function rustUnits(root: Node): Unit[] {
333
+ const units: Unit[] = [];
334
+ function visit(container: Node, scope?: string): void {
335
+ let prefix: number | undefined;
336
+ for (const n of container.namedChildren) {
337
+ if (!n) continue;
338
+ // Outer attributes/doc comments belong to the following item, not a chunk
339
+ // containing only cfg/derive metadata. Inner module docs remain separate.
340
+ if (
341
+ n.type === "attribute_item" ||
342
+ /^(\/\/\/(?!\/)|\/\*\*(?!\*))/.test(n.text)
343
+ ) {
344
+ prefix ??= n.startIndex;
345
+ continue;
346
+ }
347
+ if (prefix !== undefined && n.type.endsWith("comment")) continue;
348
+ const body = n.childForFieldName("body");
349
+ const name = n.childForFieldName("name")?.text;
350
+ const type = n.childForFieldName("type")?.text;
351
+ const trait = n.childForFieldName("trait")?.text;
352
+ const unit: Unit = {
353
+ start: prefix ?? n.startIndex,
354
+ end: n.endIndex,
355
+ kind:
356
+ n.type === "use_declaration" || n.type === "extern_crate_declaration"
357
+ ? "imports"
358
+ : classify(n.type),
359
+ scope,
360
+ symbol: (
361
+ name ??
362
+ (n.type === "impl_item" ? type : n.childForFieldName("macro")?.text)
363
+ )?.slice(0, 200),
364
+ };
365
+ prefix = undefined;
366
+ if (body?.type === "declaration_list") {
367
+ const owner =
368
+ n.type === "impl_item"
369
+ ? [type, trait].filter(Boolean).join(" as ")
370
+ : name;
371
+ const nested =
372
+ [scope, owner].filter(Boolean).join("::").slice(0, 160) || undefined;
373
+ units.push({ ...unit, end: body.startIndex + 1 });
374
+ visit(body, nested);
375
+ // Keep closing delimiters with their container, including empty bodies.
376
+ units.push({
377
+ start: body.endIndex - 1,
378
+ end: n.endIndex,
379
+ kind: "structural",
380
+ scope: nested,
381
+ });
382
+ } else {
383
+ if (unit.kind === "function")
384
+ unit.signature = n.text
385
+ .slice(0, body ? body.startIndex - n.startIndex : undefined)
386
+ .trim()
387
+ .slice(0, 240);
388
+ units.push(unit);
389
+ }
390
+ }
391
+ if (prefix !== undefined)
392
+ units.push({
393
+ start: prefix,
394
+ end:
395
+ container.type === "declaration_list"
396
+ ? container.endIndex - 1
397
+ : container.endIndex,
398
+ kind: "documentation",
399
+ scope,
400
+ });
401
+ }
402
+ visit(root);
403
+ return units;
404
+ }
103
405
  /** Bound strings without slicing a surrogate pair; prefer line ends where possible. */
104
406
  function splitUnit(
105
407
  source: string,
106
408
  path: string,
107
409
  u: Unit,
108
410
  overlap: number,
411
+ policy: Enrichment,
109
412
  ): Unit[] {
110
413
  const result: Unit[] = [];
111
414
  let start = u.start;
112
- const prefix = enrichment(path, u);
415
+ const prefix = enrichment(path, u, policy);
113
416
  invariant(
114
417
  tokens(prefix) < CHUNKER.maxTokens,
115
418
  "Path/scope enrichment exceeds token limit.",
@@ -185,6 +488,7 @@ export async function chunk(
185
488
  source: string,
186
489
  path: string,
187
490
  mode: "syntax" | "window" = "syntax",
491
+ policy: Enrichment = CHUNKER.enrichment,
188
492
  ): Promise<{ spans: Span[]; parseStatus: string; language: string }> {
189
493
  const lang = detectLanguage(path);
190
494
  if (!source.length)
@@ -194,7 +498,21 @@ export async function chunk(
194
498
  if (mode === "window") {
195
499
  status = "window-baseline";
196
500
  units = [{ start: 0, end: source.length, kind: "fallback" }];
197
- } else if (["java", "typescript", "tsx", "javascript"].includes(lang)) {
501
+ } else if (
502
+ [
503
+ "java",
504
+ "typescript",
505
+ "tsx",
506
+ "javascript",
507
+ "python",
508
+ "go",
509
+ "rust",
510
+ "c",
511
+ "cpp",
512
+ "shell",
513
+ "sql",
514
+ ].includes(lang)
515
+ ) {
198
516
  const grammar = await language(lang);
199
517
  const parser = new Parser();
200
518
  let tree: ReturnType<Parser["parse"]> = null;
@@ -224,12 +542,18 @@ export async function chunk(
224
542
  if (
225
543
  n.namedChildCount &&
226
544
  (container ||
227
- tokens(enrichment(path, u) + n.text) > CHUNKER.maxTokens)
545
+ tokens(enrichment(path, u, policy) + n.text) > CHUNKER.maxTokens)
228
546
  ) {
229
547
  for (const child of n.namedChildren) if (child) visit(child);
230
548
  } else if (n.endIndex > n.startIndex) units.push(u);
231
549
  }
232
- visit(tree.rootNode);
550
+ if (lang === "python") units = pythonUnits(tree.rootNode);
551
+ else if (lang === "go") units = goUnits(tree.rootNode);
552
+ else if (lang === "rust") units = rustUnits(tree.rootNode);
553
+ else if (lang === "c" || lang === "cpp") units = cUnits(tree.rootNode);
554
+ else if (lang === "shell") units = shellUnits(tree.rootNode);
555
+ else if (lang === "sql") units = sqlUnits(tree.rootNode);
556
+ else visit(tree.rootNode);
233
557
  // Attach every gap (punctuation, BOM, comments, whitespace) without altering source bytes.
234
558
  let cursor = 0;
235
559
  const covered: Unit[] = [];
@@ -268,11 +592,15 @@ export async function chunk(
268
592
  const prev = merged.at(-1);
269
593
  if (
270
594
  prev &&
595
+ lang !== "sql" &&
596
+ (!["python", "go", "rust", "c", "cpp", "shell"].includes(lang) ||
597
+ (!prev.symbol && !u.symbol)) &&
271
598
  prev.kind !== "function" &&
272
599
  u.kind === prev.kind &&
273
600
  u.scope === prev.scope &&
274
- tokens(enrichment(path, prev) + source.slice(prev.start, u.end)) <=
275
- CHUNKER.targetTokens
601
+ tokens(
602
+ enrichment(path, prev, policy) + source.slice(prev.start, u.end),
603
+ ) <= CHUNKER.targetTokens
276
604
  ) {
277
605
  prev.end = u.end;
278
606
  delete prev.symbol;
@@ -280,7 +608,7 @@ export async function chunk(
280
608
  } else merged.push({ ...u });
281
609
  }
282
610
  const bounded = merged.flatMap((u) =>
283
- splitUnit(source, path, u, fallback ? CHUNKER.fallbackOverlap : 0),
611
+ splitUnit(source, path, u, fallback ? CHUNKER.fallbackOverlap : 0, policy),
284
612
  );
285
613
  const offsets = new Uint32Array(source.length + 1);
286
614
  let byte = 0;
@@ -298,7 +626,7 @@ export async function chunk(
298
626
  const startByte = offsets[u.start]!,
299
627
  endByte = offsets[u.end]!;
300
628
  const text = source.slice(u.start, u.end);
301
- const searchText = enrichment(path, u) + text;
629
+ const searchText = enrichment(path, u, policy) + text;
302
630
  const kind = text.trim() === "" ? "structural" : u.kind;
303
631
  return {
304
632
  startByte,
@@ -326,3 +654,23 @@ export async function chunk(
326
654
  invariant(covered === raw.length, "Incomplete source coverage.");
327
655
  return { spans, parseStatus: status, language: lang };
328
656
  }
657
+
658
+ /** Inspect a stored source without changing the indexing/chunking policy. */
659
+ export async function withSyntaxTree<T>(
660
+ source: string,
661
+ lang: string,
662
+ inspect: (root: Node) => T,
663
+ ): Promise<T> {
664
+ const grammar = await language(lang);
665
+ const parser = new Parser();
666
+ let tree: ReturnType<Parser["parse"]> = null;
667
+ try {
668
+ parser.setLanguage(grammar);
669
+ tree = parser.parse(source);
670
+ invariant(tree, "Parser returned no tree.");
671
+ return inspect(tree.rootNode);
672
+ } finally {
673
+ tree?.delete();
674
+ parser.delete();
675
+ }
676
+ }