@functional-systems/srcx 0.1.0-dev.20 → 0.1.0-dev.22

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/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,9 +9,11 @@ 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",
14
17
  tokenizer: "js-tiktoken@1.0.21/cl100k_base",
15
18
  targetTokens: 800,
16
19
  maxTokens: 1500,
@@ -46,17 +49,42 @@ async function language(name: string): Promise<Language> {
46
49
  let l = languages.get(name);
47
50
  if (!l) {
48
51
  l = await Language.load(
49
- require.resolve(`tree-sitter-wasms/out/tree-sitter-${name}.wasm`),
52
+ name === "sql"
53
+ ? fileURLToPath(
54
+ new URL(
55
+ "../runtime/grammars/tree-sitter-sql.wasm",
56
+ import.meta.url,
57
+ ),
58
+ )
59
+ : require.resolve(
60
+ `tree-sitter-wasms/out/tree-sitter-${name === "shell" ? "bash" : name}.wasm`,
61
+ ),
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",
@@ -102,6 +130,278 @@ function scopeName(n: Node): string | undefined {
102
130
  }
103
131
  return names.length ? names.join(".") : undefined;
104
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
+ }
105
405
  /** Bound strings without slicing a surrogate pair; prefer line ends where possible. */
106
406
  function splitUnit(
107
407
  source: string,
@@ -198,7 +498,21 @@ export async function chunk(
198
498
  if (mode === "window") {
199
499
  status = "window-baseline";
200
500
  units = [{ start: 0, end: source.length, kind: "fallback" }];
201
- } 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
+ ) {
202
516
  const grammar = await language(lang);
203
517
  const parser = new Parser();
204
518
  let tree: ReturnType<Parser["parse"]> = null;
@@ -233,7 +547,13 @@ export async function chunk(
233
547
  for (const child of n.namedChildren) if (child) visit(child);
234
548
  } else if (n.endIndex > n.startIndex) units.push(u);
235
549
  }
236
- 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);
237
557
  // Attach every gap (punctuation, BOM, comments, whitespace) without altering source bytes.
238
558
  let cursor = 0;
239
559
  const covered: Unit[] = [];
@@ -272,6 +592,9 @@ export async function chunk(
272
592
  const prev = merged.at(-1);
273
593
  if (
274
594
  prev &&
595
+ lang !== "sql" &&
596
+ (!["python", "go", "rust", "c", "cpp", "shell"].includes(lang) ||
597
+ (!prev.symbol && !u.symbol)) &&
275
598
  prev.kind !== "function" &&
276
599
  u.kind === prev.kind &&
277
600
  u.scope === prev.scope &&
package/src/cli.ts CHANGED
@@ -14,6 +14,8 @@ import {
14
14
  type Preset,
15
15
  PRESET,
16
16
  presetFor,
17
+ normalizeAnalyzers,
18
+ ANALYZERS,
17
19
  } from "./build.js";
18
20
  import { invariant, optionalJson } from "./common.js";
19
21
  import { LambdaRemote } from "./remote.js";
@@ -54,6 +56,13 @@ const integer = (value: string) => {
54
56
  throw new InvalidArgumentError("Integer is too large.");
55
57
  return n;
56
58
  };
59
+ const analyzerOption = (description: string) =>
60
+ new Option(
61
+ "--analyzers <names>",
62
+ description + ` (${ANALYZERS.join(", ")}; default: standard)`,
63
+ ).argParser((value) =>
64
+ normalizeAnalyzers(value.split(",").map((name) => name.trim())),
65
+ );
57
66
  async function connected() {
58
67
  const settings = await loadSettings();
59
68
  return { settings, remote: new LambdaRemote(settings) };
@@ -96,6 +105,7 @@ repo
96
105
  .option("--remote <name>")
97
106
  .option("--description <text>")
98
107
  .option("--tag <key=value>", "One optional Collection metadata tag")
108
+ .addOption(analyzerOption("Comma-separated text analyzers"))
99
109
  .addOption(
100
110
  new Option(
101
111
  "--embedding <model>",
@@ -118,7 +128,7 @@ repo
118
128
  remote: o.remote,
119
129
  description: o.description,
120
130
  labels,
121
- preset: presetFor(o.embedding),
131
+ preset: presetFor(o.embedding, o.analyzers),
122
132
  }),
123
133
  );
124
134
  });
@@ -152,6 +162,11 @@ cli
152
162
  "Preset for --dry-run --path; connected imports use the repository preset",
153
163
  ).choices(["none", "text-embedding-3-small", "text-embedding-3-large"]),
154
164
  )
165
+ .addOption(
166
+ analyzerOption(
167
+ "Text analyzers for --dry-run --path; connected imports use the repository preset",
168
+ ),
169
+ )
155
170
  .option("--output <directory>", "New artifact directory")
156
171
  .option(
157
172
  "--previous <directory>",
@@ -168,6 +183,10 @@ cli
168
183
  !o.embedding || (o.dryRun && o.path),
169
184
  "--embedding is only accepted with --dry-run --path; connected imports use the repository preset.",
170
185
  );
186
+ invariant(
187
+ !o.analyzers || (o.dryRun && o.path),
188
+ "--analyzers is only accepted with --dry-run --path; connected imports use the repository preset.",
189
+ );
171
190
  invariant(!(o.path && o.repo), "Choose --path or --repo.");
172
191
  invariant(
173
192
  !o.previous || o.dryRun,
@@ -217,7 +236,7 @@ cli
217
236
  await validateBuild(b);
218
237
  } else {
219
238
  let source;
220
- let preset = presetFor(o.embedding);
239
+ let preset = presetFor(o.embedding, o.analyzers);
221
240
  if (o.path) source = await identity(o.path, o.remote);
222
241
  else {
223
242
  invariant(o.repo, "Use --path for a credential-free dry run.");
package/src/publish.ts CHANGED
@@ -175,10 +175,8 @@ export async function validateCandidate(
175
175
  };
176
176
  for await (const d of records(b.directory)) {
177
177
  if (d.kind !== "chunk") continue;
178
- // Keep a complete surface token: the standard analyzer may retain dots
179
- // (e.g. code.ts), so extracting an alphanumeric substring is not a valid probe.
180
- const term = String(d.symbol ?? d.searchText).match(/\S+/u)?.[0];
181
- if (!term) continue;
178
+ // Probe an indexed chunk ID: a valid source may contain only stop words
179
+ // or punctuation, so text-query matches cannot establish readiness.
182
180
  query = {
183
181
  bool: [
184
182
  {
@@ -191,16 +189,8 @@ export async function validateCandidate(
191
189
  },
192
190
  {
193
191
  queryString: {
194
- query: d.fileId,
195
- defaultField: "fileId",
196
- skipSyntax: true,
197
- },
198
- occur: "filter",
199
- },
200
- {
201
- queryString: {
202
- query: term,
203
- defaultField: "searchText",
192
+ query: d.id,
193
+ defaultField: "id",
204
194
  skipSyntax: true,
205
195
  },
206
196
  occur: "must",