@swarmvaultai/engine 0.6.6 → 0.6.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/dist/index.js CHANGED
@@ -22,13 +22,44 @@ import {
22
22
  uniqueBy,
23
23
  writeFileIfChanged,
24
24
  writeJsonFile
25
- } from "./chunk-HRRPWXRZ.js";
25
+ } from "./chunk-5Q4IV4O3.js";
26
26
 
27
27
  // src/agents.ts
28
28
  import crypto from "crypto";
29
29
  import fs from "fs/promises";
30
30
  import path from "path";
31
+ import { fileURLToPath } from "url";
31
32
  import YAML from "yaml";
33
+ function resolveHooksDir() {
34
+ const moduleUrl = import.meta.url;
35
+ const modulePath = fileURLToPath(moduleUrl);
36
+ const moduleDir = path.dirname(modulePath);
37
+ if (moduleDir.endsWith(`${path.sep}dist`)) {
38
+ return path.join(moduleDir, "hooks");
39
+ }
40
+ if (moduleDir.endsWith(`${path.sep}src`)) {
41
+ return path.resolve(moduleDir, "..", "dist", "hooks");
42
+ }
43
+ return path.resolve(moduleDir, "hooks");
44
+ }
45
+ var BUILT_HOOKS_DIR = resolveHooksDir();
46
+ var hookContentCache = /* @__PURE__ */ new Map();
47
+ async function readBuiltHook(hookFile) {
48
+ const cached = hookContentCache.get(hookFile);
49
+ if (cached !== void 0) {
50
+ return cached;
51
+ }
52
+ const hookPath2 = path.join(BUILT_HOOKS_DIR, hookFile);
53
+ try {
54
+ const content = await fs.readFile(hookPath2, "utf8");
55
+ hookContentCache.set(hookFile, content);
56
+ return content;
57
+ } catch (error) {
58
+ throw new Error(
59
+ `SwarmVault hook bundle not found at ${hookPath2}. Run 'pnpm --filter @swarmvaultai/engine build' so the hook scripts are emitted to dist/hooks/. Underlying error: ${error instanceof Error ? error.message : String(error)}`
60
+ );
61
+ }
62
+ }
32
63
  var managedStart = "<!-- swarmvault:managed:start -->";
33
64
  var managedEnd = "<!-- swarmvault:managed:end -->";
34
65
  var legacyManagedStart = "<!-- vault:managed:start -->";
@@ -64,6 +95,13 @@ function buildManagedBlock(target) {
64
95
  ""
65
96
  ].join("\n");
66
97
  }
98
+ function buildCursorRule() {
99
+ const frontmatter = YAML.stringify({
100
+ description: "SwarmVault graph-first repository instructions.",
101
+ alwaysApply: true
102
+ }).trimEnd();
103
+ return ["---", frontmatter, "---", "", buildManagedBlock("cursor").trimEnd(), ""].join("\n");
104
+ }
67
105
  function supportsAgentHook(agent) {
68
106
  return agent === "claude" || agent === "opencode" || agent === "gemini" || agent === "copilot";
69
107
  }
@@ -181,7 +219,7 @@ async function readJsonWithWarnings(filePath, fallback, label) {
181
219
  async function installClaudeHook(rootDir) {
182
220
  const settingsPath = path.join(rootDir, ".claude", "settings.json");
183
221
  const scriptPath = path.join(rootDir, ".claude", "hooks", "swarmvault-graph-first.js");
184
- await writeOwnedFile(scriptPath, buildClaudeHookScript(), true);
222
+ await writeOwnedFile(scriptPath, await readBuiltHook("claude.js"), true);
185
223
  await ensureDir(path.dirname(settingsPath));
186
224
  const { data: settings, warnings } = await readJsonWithWarnings(settingsPath, {}, ".claude/settings.json");
187
225
  if (warnings.length > 0 && await fileExists(settingsPath)) {
@@ -211,361 +249,10 @@ async function installClaudeHook(rootDir) {
211
249
  `, "utf8");
212
250
  return { path: settingsPath, warnings: [] };
213
251
  }
214
- function buildClaudeHookScript() {
215
- return `#!/usr/bin/env node
216
- import crypto from "node:crypto";
217
- import fs from "node:fs/promises";
218
- import os from "node:os";
219
- import path from "node:path";
220
-
221
- ${markerStateSnippet("claude").trim()}
222
-
223
- async function readInput() {
224
- let body = "";
225
- for await (const chunk of process.stdin) {
226
- body += chunk;
227
- }
228
- if (!body.trim()) {
229
- return {};
230
- }
231
- try {
232
- return JSON.parse(body);
233
- } catch {
234
- return {};
235
- }
236
- }
237
-
238
- function emit(value) {
239
- process.stdout.write(\`\${JSON.stringify(value)}\\n\`);
240
- }
241
-
242
- const mode = process.argv[2] ?? "";
243
- const input = await readInput();
244
- const cwd = resolveInputCwd(input);
245
- const reportNote = "SwarmVault graph report exists at wiki/graph/report.md. Read it before broad grep/glob searching.";
246
-
247
- if (!(await hasReport(cwd))) {
248
- emit({});
249
- process.exit(0);
250
- }
251
-
252
- if (mode === "session-start") {
253
- await resetSession(cwd);
254
- emit({
255
- hookSpecificOutput: {
256
- hookEventName: "SessionStart",
257
- additionalContext: reportNote
258
- }
259
- });
260
- process.exit(0);
261
- }
262
-
263
- const toolName = resolveToolName(input);
264
- if (collectCandidatePaths(input).some((value) => isReportPath(value, cwd))) {
265
- await markReportRead(cwd);
266
- emit({});
267
- process.exit(0);
268
- }
269
-
270
- if (isBroadSearchTool(toolName) && !(await hasSeenReport(cwd))) {
271
- emit({
272
- hookSpecificOutput: {
273
- hookEventName: "PreToolUse",
274
- additionalContext: reportNote
275
- }
276
- });
277
- process.exit(0);
278
- }
279
-
280
- emit({});
281
- `;
282
- }
283
- function markerStateSnippet(agentKey) {
284
- return `
285
- function markerState(cwd) {
286
- const hash = crypto.createHash("sha256").update(cwd).digest("hex");
287
- const dir = path.join(os.tmpdir(), "swarmvault-agent-hooks", "${agentKey}", hash);
288
- return {
289
- dir,
290
- markerPath: path.join(dir, "report-read")
291
- };
292
- }
293
-
294
- function isReportPath(value, cwd) {
295
- if (typeof value !== "string" || value.length === 0) {
296
- return false;
297
- }
298
- const reportSuffix = path.join("wiki", "graph", "report.md");
299
- const normalized = value.replaceAll("\\\\", "/");
300
- const reportNormalized = reportSuffix.replaceAll("\\\\", "/");
301
- if (normalized.endsWith(reportNormalized)) {
302
- return true;
303
- }
304
- return path.resolve(cwd, value) === path.resolve(cwd, reportSuffix);
305
- }
306
-
307
- function collectCandidatePaths(node, acc = []) {
308
- if (typeof node === "string") {
309
- acc.push(node);
310
- return acc;
311
- }
312
- if (!node || typeof node !== "object") {
313
- return acc;
314
- }
315
- if (Array.isArray(node)) {
316
- for (const item of node) {
317
- collectCandidatePaths(item, acc);
318
- }
319
- return acc;
320
- }
321
- for (const [key, value] of Object.entries(node)) {
322
- if (["path", "filePath", "file_path", "paths", "target", "targets"].includes(key)) {
323
- collectCandidatePaths(value, acc);
324
- continue;
325
- }
326
- collectCandidatePaths(value, acc);
327
- }
328
- return acc;
329
- }
330
-
331
- function resolveInputCwd(input) {
332
- return path.resolve(
333
- input?.cwd ??
334
- input?.directory ??
335
- input?.workspace?.cwd ??
336
- input?.toolInput?.cwd ??
337
- process.cwd()
338
- );
339
- }
340
-
341
- function resolveToolName(input) {
342
- return String(input?.toolName ?? input?.tool_name ?? input?.tool?.name ?? input?.name ?? "");
343
- }
344
-
345
- async function hasReport(cwd) {
346
- try {
347
- await fs.access(path.join(cwd, "wiki", "graph", "report.md"));
348
- return true;
349
- } catch {
350
- return false;
351
- }
352
- }
353
-
354
- async function markReportRead(cwd) {
355
- const state = markerState(cwd);
356
- await fs.mkdir(state.dir, { recursive: true });
357
- await fs.writeFile(state.markerPath, "seen\\n", "utf8");
358
- }
359
-
360
- async function hasSeenReport(cwd) {
361
- const state = markerState(cwd);
362
- try {
363
- await fs.access(state.markerPath);
364
- return true;
365
- } catch {
366
- return false;
367
- }
368
- }
369
-
370
- async function resetSession(cwd) {
371
- const state = markerState(cwd);
372
- await fs.rm(state.dir, { recursive: true, force: true });
373
- }
374
-
375
- function isBroadSearchTool(toolName) {
376
- return /grep|glob|search|find/i.test(toolName);
377
- }
378
- `;
379
- }
380
- function buildGeminiHookScript() {
381
- return `#!/usr/bin/env node
382
- import crypto from "node:crypto";
383
- import fs from "node:fs/promises";
384
- import os from "node:os";
385
- import path from "node:path";
386
-
387
- ${markerStateSnippet("gemini").trim()}
388
-
389
- async function readInput() {
390
- let body = "";
391
- for await (const chunk of process.stdin) {
392
- body += chunk;
393
- }
394
- if (!body.trim()) {
395
- return {};
396
- }
397
- try {
398
- return JSON.parse(body);
399
- } catch {
400
- return {};
401
- }
402
- }
403
-
404
- function emit(value) {
405
- process.stdout.write(\`\${JSON.stringify(value)}\\n\`);
406
- }
407
-
408
- const mode = process.argv[2] ?? "";
409
- const input = await readInput();
410
- const cwd = resolveInputCwd(input);
411
- const reportNote = "SwarmVault graph report exists at wiki/graph/report.md. Read it before broad grep/glob searching.";
412
-
413
- if (!(await hasReport(cwd))) {
414
- emit({});
415
- process.exit(0);
416
- }
417
-
418
- if (mode === "session-start") {
419
- await resetSession(cwd);
420
- emit({
421
- systemMessage: reportNote,
422
- hookSpecificOutput: {
423
- hookEventName: "SessionStart",
424
- additionalContext: "SwarmVault graph report: wiki/graph/report.md"
425
- }
426
- });
427
- process.exit(0);
428
- }
429
-
430
- const toolName = resolveToolName(input);
431
- if (collectCandidatePaths(input).some((value) => isReportPath(value, cwd))) {
432
- await markReportRead(cwd);
433
- emit({});
434
- process.exit(0);
435
- }
436
-
437
- if (isBroadSearchTool(toolName) && !(await hasSeenReport(cwd))) {
438
- emit({ systemMessage: reportNote });
439
- process.exit(0);
440
- }
441
-
442
- emit({});
443
- `;
444
- }
445
- function buildCopilotHookScript() {
446
- return `#!/usr/bin/env node
447
- import crypto from "node:crypto";
448
- import fs from "node:fs/promises";
449
- import os from "node:os";
450
- import path from "node:path";
451
-
452
- ${markerStateSnippet("copilot").trim()}
453
-
454
- async function readInput() {
455
- let body = "";
456
- for await (const chunk of process.stdin) {
457
- body += chunk;
458
- }
459
- if (!body.trim()) {
460
- return {};
461
- }
462
- try {
463
- return JSON.parse(body);
464
- } catch {
465
- return {};
466
- }
467
- }
468
-
469
- function emit(value) {
470
- if (value !== undefined) {
471
- process.stdout.write(\`\${JSON.stringify(value)}\\n\`);
472
- }
473
- }
474
-
475
- const mode = process.argv[2] ?? "";
476
- const input = await readInput();
477
- const cwd = resolveInputCwd(input);
478
- const reportNote = "SwarmVault graph report exists at wiki/graph/report.md. Read it before broad grep/glob searching.";
479
-
480
- if (!(await hasReport(cwd))) {
481
- emit({});
482
- process.exit(0);
483
- }
484
-
485
- if (mode === "session-start") {
486
- await resetSession(cwd);
487
- emit({});
488
- process.exit(0);
489
- }
490
-
491
- const toolName = resolveToolName(input);
492
- if (collectCandidatePaths(input).some((value) => isReportPath(value, cwd))) {
493
- await markReportRead(cwd);
494
- emit({});
495
- process.exit(0);
496
- }
497
-
498
- if (isBroadSearchTool(toolName) && !(await hasSeenReport(cwd))) {
499
- emit({
500
- permissionDecision: "deny",
501
- permissionDecisionReason: reportNote
502
- });
503
- process.exit(0);
504
- }
505
-
506
- emit({});
507
- `;
508
- }
509
- function buildOpenCodePlugin() {
510
- return `import path from "node:path";
511
-
512
- const reportRelativePath = path.join("wiki", "graph", "report.md");
513
-
514
- export const name = "swarmvault-graph-first";
515
-
516
- export default async function swarmvaultGraphFirst({ client }) {
517
- let reportSeen = false;
518
-
519
- async function hasReport(cwd) {
520
- try {
521
- await Bun.file(path.join(cwd, reportRelativePath)).arrayBuffer();
522
- return true;
523
- } catch {
524
- return false;
525
- }
526
- }
527
-
528
- async function note(message) {
529
- if (client?.app?.log) {
530
- await client.app.log({
531
- level: "info",
532
- message
533
- });
534
- }
535
- }
536
-
537
- return {
538
- async "session.created"(input) {
539
- reportSeen = false;
540
- const cwd = input?.session?.cwd ?? process.cwd();
541
- if (await hasReport(cwd)) {
542
- await note("SwarmVault graph report exists. Read wiki/graph/report.md before broad workspace searching.");
543
- }
544
- },
545
- async "tool.execute.before"(input) {
546
- const cwd = input?.session?.cwd ?? process.cwd();
547
- if (!(await hasReport(cwd))) {
548
- return;
549
- }
550
-
551
- const argsText = JSON.stringify(input?.args ?? {});
552
- if (argsText.includes("wiki/graph/report.md")) {
553
- reportSeen = true;
554
- return;
555
- }
556
-
557
- if (!reportSeen && ["glob", "grep"].includes(String(input?.tool ?? ""))) {
558
- await note("SwarmVault graph report exists. Read wiki/graph/report.md before broad workspace searching.");
559
- }
560
- }
561
- };
562
- }
563
- `;
564
- }
565
252
  async function installGeminiHook(rootDir) {
566
253
  const settingsPath = path.join(rootDir, ".gemini", "settings.json");
567
254
  const scriptPath = path.join(rootDir, ".gemini", "hooks", "swarmvault-graph-first.js");
568
- await writeOwnedFile(scriptPath, buildGeminiHookScript(), true);
255
+ await writeOwnedFile(scriptPath, await readBuiltHook("gemini.js"), true);
569
256
  const { data: settings, warnings } = await readJsonWithWarnings(settingsPath, {}, ".gemini/settings.json");
570
257
  if (warnings.length > 0 && await fileExists(settingsPath)) {
571
258
  return { paths: [settingsPath, scriptPath], warnings };
@@ -632,7 +319,7 @@ async function installCopilotHook(rootDir) {
632
319
  const hooksDir = path.join(rootDir, ".github", "hooks");
633
320
  const scriptPath = path.join(hooksDir, "swarmvault-graph-first.js");
634
321
  const configPath = path.join(hooksDir, "swarmvault-graph-first.json");
635
- await writeOwnedFile(scriptPath, buildCopilotHookScript(), true);
322
+ await writeOwnedFile(scriptPath, await readBuiltHook("copilot.js"), true);
636
323
  const config = {
637
324
  version: copilotHookVersion,
638
325
  hooks: {
@@ -663,7 +350,7 @@ async function installCopilotHook(rootDir) {
663
350
  }
664
351
  async function installOpenCodeHook(rootDir) {
665
352
  const pluginPath = path.join(rootDir, ".opencode", "plugins", "swarmvault-graph-first.js");
666
- await writeOwnedFile(pluginPath, buildOpenCodePlugin());
353
+ await writeOwnedFile(pluginPath, await readBuiltHook("opencode.js"));
667
354
  return { paths: [pluginPath], warnings: [] };
668
355
  }
669
356
  function stableKeyForAgent(rootDir, agent) {
@@ -690,8 +377,7 @@ async function installAgent(rootDir, agent, options = {}) {
690
377
  await upsertManagedBlock(target, buildManagedBlock("gemini"));
691
378
  break;
692
379
  case "cursor":
693
- await writeOwnedFile(target, `${buildManagedBlock("cursor")}
694
- `);
380
+ await writeOwnedFile(target, buildCursorRule());
695
381
  break;
696
382
  case "aider":
697
383
  await upsertManagedBlock(target, buildManagedBlock("aider"));
@@ -1343,10 +1029,13 @@ function buildBenchmarkArtifact(input) {
1343
1029
  const corpusTokens = Math.max(1, Math.round(input.corpusWords * (100 / 75)));
1344
1030
  const perQuestion = input.perQuestion.filter((entry) => entry.queryTokens > 0).map((entry) => ({
1345
1031
  ...entry,
1346
- reduction: Number(Math.max(0, 1 - entry.queryTokens / Math.max(1, corpusTokens)).toFixed(3))
1032
+ // Honest reduction: negative values mean graph context is larger than the
1033
+ // full corpus, which is the truth on very small vaults. Clamping to zero
1034
+ // hid that signal.
1035
+ reduction: Number((1 - entry.queryTokens / Math.max(1, corpusTokens)).toFixed(3))
1347
1036
  }));
1348
1037
  const avgQueryTokens = perQuestion.length ? Math.max(1, Math.round(perQuestion.reduce((total, entry) => total + entry.queryTokens, 0) / perQuestion.length)) : 0;
1349
- const reductionRatio = avgQueryTokens ? Number(Math.max(0, 1 - avgQueryTokens / Math.max(1, corpusTokens)).toFixed(3)) : 0;
1038
+ const reductionRatio = avgQueryTokens ? Number((1 - avgQueryTokens / Math.max(1, corpusTokens)).toFixed(3)) : 0;
1350
1039
  const uniqueVisitedNodes = new Set(perQuestion.flatMap((entry) => entry.visitedNodeIds)).size;
1351
1040
  const summary = {
1352
1041
  questionCount: input.questions.length,
@@ -1367,7 +1056,6 @@ function buildBenchmarkArtifact(input) {
1367
1056
  reductionRatio,
1368
1057
  sampleQuestions: input.questions,
1369
1058
  perQuestion,
1370
- questionResults: perQuestion,
1371
1059
  summary
1372
1060
  };
1373
1061
  }
@@ -1728,6 +1416,7 @@ import { pathToFileURL } from "url";
1728
1416
  import { Readability } from "@mozilla/readability";
1729
1417
  import matter3 from "gray-matter";
1730
1418
  import ignore from "ignore";
1419
+ import { isText } from "istextorbinary";
1731
1420
  import { JSDOM as JSDOM2 } from "jsdom";
1732
1421
  import mime from "mime-types";
1733
1422
  import TurndownService2 from "turndown";
@@ -2172,104 +1861,431 @@ function treeSitterCompatibilityDiagnostic(language, error) {
2172
1861
  column: 1
2173
1862
  };
2174
1863
  }
2175
- function parsePythonImportStatement(text) {
2176
- const match = text.trim().match(/^import\s+(.+)$/);
2177
- if (!match) {
2178
- return [];
1864
+ function flattenPythonDottedName(node) {
1865
+ if (!node) {
1866
+ return "";
2179
1867
  }
2180
- return match[1].split(",").map((item) => item.trim()).filter(Boolean).map((item) => {
2181
- const [specifier, alias] = item.split(/\s+as\s+/i);
2182
- return {
2183
- specifier: specifier.trim(),
2184
- importedSymbols: [],
2185
- namespaceImport: alias?.trim(),
2186
- isExternal: !specifier.trim().startsWith("."),
2187
- reExport: false
2188
- };
2189
- });
1868
+ return node.namedChildren.filter((child) => child?.type === "identifier").map((child) => child.text.trim()).filter(Boolean).join(".");
2190
1869
  }
2191
- function parsePythonFromImportStatement(text) {
2192
- const match = text.trim().match(/^from\s+([.\w]+)\s+import\s+(.+)$/);
2193
- if (!match) {
1870
+ function flattenPythonRelativeImport(node) {
1871
+ if (!node) {
1872
+ return "";
1873
+ }
1874
+ const prefixNode = node.namedChildren.find((child) => child?.type === "import_prefix") ?? null;
1875
+ const prefix = prefixNode ? prefixNode.text.trim() : "";
1876
+ const moduleNode = node.namedChildren.find((child) => child?.type === "dotted_name") ?? null;
1877
+ const module = flattenPythonDottedName(moduleNode);
1878
+ return prefix + module;
1879
+ }
1880
+ function parsePythonImportStatement(node) {
1881
+ const imports = [];
1882
+ for (const child of node.namedChildren) {
1883
+ if (!child) {
1884
+ continue;
1885
+ }
1886
+ if (child.type === "dotted_name") {
1887
+ const specifier = flattenPythonDottedName(child);
1888
+ if (!specifier) {
1889
+ continue;
1890
+ }
1891
+ imports.push({
1892
+ specifier,
1893
+ importedSymbols: [],
1894
+ isExternal: !specifier.startsWith("."),
1895
+ reExport: false
1896
+ });
1897
+ } else if (child.type === "aliased_import") {
1898
+ const moduleNode = child.namedChildren.find((inner) => inner?.type === "dotted_name") ?? null;
1899
+ const aliasNode = child.namedChildren.find((inner) => inner?.type === "identifier") ?? null;
1900
+ const specifier = flattenPythonDottedName(moduleNode);
1901
+ if (!specifier) {
1902
+ continue;
1903
+ }
1904
+ imports.push({
1905
+ specifier,
1906
+ importedSymbols: [],
1907
+ namespaceImport: aliasNode?.text.trim(),
1908
+ isExternal: !specifier.startsWith("."),
1909
+ reExport: false
1910
+ });
1911
+ }
1912
+ }
1913
+ return imports;
1914
+ }
1915
+ function parsePythonFromImportStatement(node) {
1916
+ const children = node.namedChildren.filter((child) => child !== null);
1917
+ if (children.length === 0) {
1918
+ return [];
1919
+ }
1920
+ const [moduleNode, ...rest] = children;
1921
+ if (!moduleNode) {
2194
1922
  return [];
2195
1923
  }
1924
+ let specifier;
1925
+ if (moduleNode.type === "relative_import") {
1926
+ specifier = flattenPythonRelativeImport(moduleNode);
1927
+ } else if (moduleNode.type === "dotted_name") {
1928
+ specifier = flattenPythonDottedName(moduleNode);
1929
+ } else {
1930
+ return [];
1931
+ }
1932
+ if (!specifier) {
1933
+ return [];
1934
+ }
1935
+ const symbols = [];
1936
+ let hasWildcard = false;
1937
+ for (const entry of rest) {
1938
+ if (entry.type === "wildcard_import") {
1939
+ hasWildcard = true;
1940
+ continue;
1941
+ }
1942
+ if (entry.type === "dotted_name") {
1943
+ const name = flattenPythonDottedName(entry);
1944
+ if (name) {
1945
+ symbols.push(name);
1946
+ }
1947
+ continue;
1948
+ }
1949
+ if (entry.type === "aliased_import") {
1950
+ const moduleChild = entry.namedChildren.find((inner) => inner?.type === "dotted_name") ?? null;
1951
+ const aliasChild = entry.namedChildren.find((inner) => inner?.type === "identifier") ?? null;
1952
+ const baseName = flattenPythonDottedName(moduleChild);
1953
+ const aliasName = aliasChild?.text.trim();
1954
+ if (baseName) {
1955
+ symbols.push(aliasName ? `${baseName} as ${aliasName}` : baseName);
1956
+ }
1957
+ }
1958
+ }
1959
+ if (hasWildcard) {
1960
+ symbols.push("*");
1961
+ }
2196
1962
  return [
2197
1963
  {
2198
- specifier: match[1],
2199
- importedSymbols: match[2].split(",").map((item) => item.trim()).filter(Boolean),
2200
- isExternal: !match[1].startsWith("."),
1964
+ specifier,
1965
+ importedSymbols: symbols,
1966
+ isExternal: !specifier.startsWith("."),
2201
1967
  reExport: false
2202
1968
  }
2203
1969
  ];
2204
1970
  }
2205
- function parseGoImport(text) {
2206
- const match = text.trim().match(/^(?:([._A-Za-z]\w*)\s+)?"([^"]+)"$/);
2207
- if (!match) {
1971
+ function parseGoImport(spec) {
1972
+ let alias;
1973
+ let dotImport = false;
1974
+ let blankImport = false;
1975
+ let specifier;
1976
+ for (const child of spec.namedChildren) {
1977
+ if (!child) {
1978
+ continue;
1979
+ }
1980
+ switch (child.type) {
1981
+ case "package_identifier":
1982
+ alias = child.text.trim();
1983
+ break;
1984
+ case "dot":
1985
+ dotImport = true;
1986
+ break;
1987
+ case "blank_identifier":
1988
+ blankImport = true;
1989
+ break;
1990
+ case "interpreted_string_literal":
1991
+ case "raw_string_literal": {
1992
+ const content = child.namedChildren.find(
1993
+ (inner) => inner?.type === "interpreted_string_literal_content" || inner?.type === "raw_string_literal_content"
1994
+ ) ?? null;
1995
+ specifier = content ? content.text : child.text.replace(/^[`"]|[`"]$/g, "");
1996
+ break;
1997
+ }
1998
+ default:
1999
+ break;
2000
+ }
2001
+ }
2002
+ if (!specifier) {
2208
2003
  return void 0;
2209
2004
  }
2210
2005
  return {
2211
- specifier: match[2],
2006
+ specifier,
2212
2007
  importedSymbols: [],
2213
- namespaceImport: match[1] && ![".", "_"].includes(match[1]) ? match[1] : void 0,
2214
- isExternal: !match[2].startsWith("."),
2008
+ namespaceImport: !dotImport && !blankImport ? alias : void 0,
2009
+ isExternal: !specifier.startsWith("."),
2215
2010
  reExport: false
2216
2011
  };
2217
2012
  }
2218
- function parseRustUse(text) {
2219
- const cleaned = text.replace(/^pub\s+/, "").replace(/^use\s+/, "").replace(/;$/, "").trim();
2220
- const aliasMatch = cleaned.match(/\s+as\s+([A-Za-z_]\w*)$/);
2221
- const withoutAlias = aliasMatch ? cleaned.slice(0, aliasMatch.index).trim() : cleaned;
2222
- const braceMatch = withoutAlias.match(/^(.*)::\{(.+)\}$/);
2223
- const importedSymbols = braceMatch ? braceMatch[2].split(",").map((item) => item.trim()).filter(Boolean) : [aliasMatch ? `${normalizeSymbolReference(withoutAlias)} as ${aliasMatch[1]}` : normalizeSymbolReference(withoutAlias)].filter(
2224
- Boolean
2225
- );
2226
- const specifier = braceMatch ? braceMatch[1].trim() : withoutAlias;
2227
- return {
2228
- specifier,
2229
- importedSymbols,
2230
- isExternal: !/^(crate|self|super)::/.test(specifier),
2231
- reExport: text.trim().startsWith("pub use ")
2232
- };
2013
+ function flattenRustPath(node) {
2014
+ if (!node) {
2015
+ return [];
2016
+ }
2017
+ if (node.type === "crate" || node.type === "self" || node.type === "super") {
2018
+ return [node.type];
2019
+ }
2020
+ if (node.type === "identifier") {
2021
+ return [node.text.trim()].filter(Boolean);
2022
+ }
2023
+ if (node.type === "scoped_identifier") {
2024
+ const segments = [];
2025
+ for (const child of node.namedChildren) {
2026
+ if (!child) {
2027
+ continue;
2028
+ }
2029
+ segments.push(...flattenRustPath(child));
2030
+ }
2031
+ return segments;
2032
+ }
2033
+ return node.namedChildren.filter((child) => child !== null).flatMap((child) => flattenRustPath(child));
2233
2034
  }
2234
- function parseJavaImport(text) {
2235
- const cleaned = text.replace(/^import\s+/, "").replace(/^static\s+/, "").replace(/;$/, "").trim();
2236
- const symbolName = normalizeSymbolReference(cleaned.replace(/\.\*$/, ""));
2035
+ function collectRustUseLeaves(node, prefix, leaves) {
2036
+ if (!node) {
2037
+ return;
2038
+ }
2039
+ switch (node.type) {
2040
+ case "scoped_identifier": {
2041
+ const segments = flattenRustPath(node);
2042
+ if (segments.length === 0) {
2043
+ return;
2044
+ }
2045
+ leaves.push({
2046
+ segments: [...prefix, ...segments],
2047
+ symbol: segments[segments.length - 1] ?? null,
2048
+ wildcard: false
2049
+ });
2050
+ return;
2051
+ }
2052
+ case "identifier":
2053
+ case "crate":
2054
+ case "self":
2055
+ case "super": {
2056
+ const combined = [...prefix];
2057
+ if (node.type === "self" && prefix.length > 0) {
2058
+ leaves.push({ segments: combined, symbol: null, wildcard: false });
2059
+ return;
2060
+ }
2061
+ combined.push(node.type === "identifier" ? node.text.trim() : node.type);
2062
+ leaves.push({
2063
+ segments: combined,
2064
+ symbol: combined[combined.length - 1] ?? null,
2065
+ wildcard: false
2066
+ });
2067
+ return;
2068
+ }
2069
+ case "scoped_use_list": {
2070
+ const pathNode = node.namedChildren[0] ?? null;
2071
+ const listNode = node.namedChildren[1] ?? null;
2072
+ const nextPrefix = [...prefix, ...flattenRustPath(pathNode)];
2073
+ collectRustUseLeaves(listNode, nextPrefix, leaves);
2074
+ return;
2075
+ }
2076
+ case "use_list": {
2077
+ for (const child of node.namedChildren) {
2078
+ collectRustUseLeaves(child, prefix, leaves);
2079
+ }
2080
+ return;
2081
+ }
2082
+ case "use_wildcard": {
2083
+ const pathNode = node.namedChildren[0] ?? null;
2084
+ const pathSegments = pathNode ? flattenRustPath(pathNode) : [];
2085
+ leaves.push({
2086
+ segments: [...prefix, ...pathSegments],
2087
+ symbol: null,
2088
+ wildcard: true
2089
+ });
2090
+ return;
2091
+ }
2092
+ case "use_as_clause": {
2093
+ const pathNode = node.childForFieldName("path") ?? node.namedChildren[0] ?? null;
2094
+ const aliasNode = node.childForFieldName("alias") ?? node.namedChildren[1] ?? null;
2095
+ const before = leaves.length;
2096
+ collectRustUseLeaves(pathNode, prefix, leaves);
2097
+ const alias = aliasNode?.text.trim();
2098
+ if (alias) {
2099
+ for (let index = before; index < leaves.length; index += 1) {
2100
+ const leaf = leaves[index];
2101
+ if (leaf) {
2102
+ leaf.alias = alias;
2103
+ }
2104
+ }
2105
+ }
2106
+ return;
2107
+ }
2108
+ default: {
2109
+ for (const child of node.namedChildren) {
2110
+ collectRustUseLeaves(child, prefix, leaves);
2111
+ }
2112
+ }
2113
+ }
2114
+ }
2115
+ function isRustPubUse(useNode) {
2116
+ for (const child of useNode.children) {
2117
+ if (!child) {
2118
+ continue;
2119
+ }
2120
+ if (child.type === "visibility_modifier") {
2121
+ return true;
2122
+ }
2123
+ if (child.type === "use") {
2124
+ return false;
2125
+ }
2126
+ }
2127
+ return false;
2128
+ }
2129
+ function parseRustUseDeclaration(useNode) {
2130
+ const inner = useNode.namedChildren.find((child) => child !== null) ?? null;
2131
+ if (!inner) {
2132
+ return [];
2133
+ }
2134
+ const leaves = [];
2135
+ collectRustUseLeaves(inner, [], leaves);
2136
+ if (leaves.length === 0) {
2137
+ return [];
2138
+ }
2139
+ const reExport = isRustPubUse(useNode);
2140
+ return leaves.map((leaf) => {
2141
+ const specifier = leaf.segments.join("::");
2142
+ const importedSymbols = leaf.wildcard ? ["*"] : leaf.alias && leaf.symbol ? [`${leaf.symbol} as ${leaf.alias}`] : leaf.symbol ? [leaf.symbol] : [];
2143
+ return {
2144
+ specifier,
2145
+ importedSymbols,
2146
+ isExternal: !/^(?:crate|self|super)(?:$|::)/.test(specifier),
2147
+ reExport
2148
+ };
2149
+ });
2150
+ }
2151
+ function flattenJavaScopedIdentifier(node) {
2152
+ if (!node) {
2153
+ return "";
2154
+ }
2155
+ if (node.type === "identifier") {
2156
+ return node.text.trim();
2157
+ }
2158
+ if (node.type === "scoped_identifier") {
2159
+ const head = node.namedChildren[0] ?? null;
2160
+ const tail = node.namedChildren[node.namedChildren.length - 1] ?? null;
2161
+ const headText = flattenJavaScopedIdentifier(head);
2162
+ const tailText = tail && tail !== head && tail.type === "identifier" ? tail.text.trim() : "";
2163
+ return headText && tailText ? `${headText}.${tailText}` : headText || tailText;
2164
+ }
2165
+ return node.text.trim();
2166
+ }
2167
+ function parseJavaImport(node) {
2168
+ const pathNode = node.namedChildren.find((child) => child?.type === "scoped_identifier") ?? null;
2169
+ const hasAsterisk = node.namedChildren.some((child) => child?.type === "asterisk");
2170
+ const pathText = flattenJavaScopedIdentifier(pathNode);
2171
+ const specifier = hasAsterisk ? `${pathText}.*` : pathText;
2172
+ const symbolName = hasAsterisk ? "" : (pathText.split(".").pop() ?? "").trim();
2237
2173
  return {
2238
- specifier: cleaned.replace(/\.\*$/, ""),
2174
+ specifier,
2239
2175
  importedSymbols: symbolName ? [symbolName] : [],
2240
2176
  isExternal: true,
2241
2177
  reExport: false
2242
2178
  };
2243
2179
  }
2244
- function parseKotlinImport(text) {
2245
- const cleaned = text.trim().replace(/^import\s+/, "");
2246
- if (!cleaned) {
2247
- return void 0;
2180
+ function flattenKotlinIdentifier(node) {
2181
+ if (!node) {
2182
+ return "";
2183
+ }
2184
+ if (node.type === "simple_identifier") {
2185
+ return node.text.trim();
2248
2186
  }
2249
- const aliasMatch = cleaned.match(/^(.+?)\s+as\s+([A-Za-z_]\w*)$/);
2250
- const specifier = (aliasMatch ? aliasMatch[1] : cleaned).trim();
2187
+ return node.namedChildren.filter((child) => child?.type === "simple_identifier").map((child) => child.text.trim()).filter(Boolean).join(".");
2188
+ }
2189
+ function parseKotlinImport(header) {
2190
+ const identifierNode = header.namedChildren.find((child) => child?.type === "identifier") ?? null;
2191
+ const specifier = flattenKotlinIdentifier(identifierNode);
2251
2192
  if (!specifier) {
2252
2193
  return void 0;
2253
2194
  }
2195
+ const hasWildcard = header.descendantsOfType("wildcard_import").some((child) => child !== null);
2196
+ const aliasNode = header.namedChildren.find((child) => child?.type === "import_alias") ?? null;
2197
+ const aliasName = aliasNode ? flattenKotlinIdentifier(aliasNode.namedChildren.find((child) => child?.type === "type_identifier") ?? null) || aliasNode.text.replace(/^as\s+/, "").trim() : void 0;
2254
2198
  return {
2255
- specifier,
2256
- importedSymbols: [],
2257
- namespaceImport: aliasMatch?.[2],
2258
- isExternal: !specifier.startsWith("."),
2199
+ specifier: hasWildcard ? `${specifier}.*` : specifier,
2200
+ importedSymbols: hasWildcard ? ["*"] : [],
2201
+ namespaceImport: aliasName || void 0,
2202
+ isExternal: true,
2259
2203
  reExport: false
2260
2204
  };
2261
2205
  }
2262
- function parseScalaImport(text) {
2263
- const cleaned = text.trim().replace(/^import\s+/, "");
2264
- if (!cleaned) {
2206
+ function flattenScalaStableIdentifier(node) {
2207
+ if (!node) {
2208
+ return "";
2209
+ }
2210
+ if (node.type === "identifier") {
2211
+ return node.text.trim();
2212
+ }
2213
+ if (node.type === "stable_identifier") {
2214
+ return node.namedChildren.filter((child) => child !== null).map((child) => flattenScalaStableIdentifier(child)).filter(Boolean).join(".");
2215
+ }
2216
+ return node.text.trim();
2217
+ }
2218
+ function parseScalaImport(node) {
2219
+ const pathNode = node.namedChildren.find((child) => child?.type === "stable_identifier") ?? node.namedChildren.find((child) => child?.type === "identifier") ?? null;
2220
+ const basePath = flattenScalaStableIdentifier(pathNode);
2221
+ if (!basePath) {
2265
2222
  return [];
2266
2223
  }
2267
- return cleaned.split(",").map((item) => item.trim()).filter(Boolean).map((item) => ({
2268
- specifier: item.replace(/\s*=>\s*/g, " => "),
2269
- importedSymbols: [],
2270
- isExternal: !item.startsWith("."),
2271
- reExport: false
2272
- }));
2224
+ const selectorsNode = node.namedChildren.find((child) => child?.type === "import_selectors") ?? null;
2225
+ const wildcardNode = node.namedChildren.find((child) => child?.type === "wildcard") ?? null;
2226
+ if (selectorsNode) {
2227
+ const results = [];
2228
+ for (const selector of selectorsNode.namedChildren) {
2229
+ if (!selector) {
2230
+ continue;
2231
+ }
2232
+ if (selector.type === "identifier") {
2233
+ const symbol2 = selector.text.trim();
2234
+ if (symbol2) {
2235
+ results.push({
2236
+ specifier: basePath,
2237
+ importedSymbols: [symbol2],
2238
+ isExternal: !basePath.startsWith("."),
2239
+ reExport: false
2240
+ });
2241
+ }
2242
+ continue;
2243
+ }
2244
+ if (selector.type === "renamed_identifier") {
2245
+ const idChildren = selector.namedChildren.filter((child) => child?.type === "identifier");
2246
+ const [original, alias] = [idChildren[0]?.text.trim(), idChildren[1]?.text.trim()];
2247
+ if (original) {
2248
+ results.push({
2249
+ specifier: basePath,
2250
+ importedSymbols: [alias ? `${original} as ${alias}` : original],
2251
+ isExternal: !basePath.startsWith("."),
2252
+ reExport: false
2253
+ });
2254
+ }
2255
+ continue;
2256
+ }
2257
+ if (selector.type === "wildcard") {
2258
+ results.push({
2259
+ specifier: basePath,
2260
+ importedSymbols: ["*"],
2261
+ isExternal: !basePath.startsWith("."),
2262
+ reExport: false
2263
+ });
2264
+ }
2265
+ }
2266
+ return results;
2267
+ }
2268
+ if (wildcardNode) {
2269
+ return [
2270
+ {
2271
+ specifier: basePath,
2272
+ importedSymbols: ["*"],
2273
+ isExternal: !basePath.startsWith("."),
2274
+ reExport: false
2275
+ }
2276
+ ];
2277
+ }
2278
+ const segments = basePath.split(".");
2279
+ const symbol = segments.pop() ?? basePath;
2280
+ const parent = segments.join(".");
2281
+ return [
2282
+ {
2283
+ specifier: parent || basePath,
2284
+ importedSymbols: [symbol],
2285
+ isExternal: !basePath.startsWith("."),
2286
+ reExport: false
2287
+ }
2288
+ ];
2273
2289
  }
2274
2290
  function bashCommandName(commandNode) {
2275
2291
  if (!commandNode) {
@@ -2391,54 +2407,164 @@ function parseZigImport(node) {
2391
2407
  reExport: false
2392
2408
  };
2393
2409
  }
2394
- function parseCSharpUsing(text) {
2395
- const aliasMatch = text.trim().match(/^using\s+([A-Za-z_]\w*)\s*=\s*([^;]+);$/);
2396
- if (aliasMatch) {
2397
- return {
2398
- specifier: aliasMatch[2].trim(),
2399
- importedSymbols: [],
2400
- namespaceImport: aliasMatch[1],
2401
- isExternal: !aliasMatch[2].trim().startsWith("."),
2402
- reExport: false
2403
- };
2410
+ function flattenCSharpQualifiedName(node) {
2411
+ if (!node) {
2412
+ return "";
2404
2413
  }
2405
- const match = text.trim().match(/^using\s+([^;]+);$/);
2406
- if (!match) {
2414
+ if (node.type === "identifier") {
2415
+ return node.text.trim();
2416
+ }
2417
+ if (node.type === "qualified_name") {
2418
+ const [head, tail] = [node.namedChildren[0] ?? null, node.namedChildren[1] ?? null];
2419
+ const headText = flattenCSharpQualifiedName(head);
2420
+ const tailText = tail?.type === "identifier" ? tail.text.trim() : flattenCSharpQualifiedName(tail);
2421
+ return headText && tailText ? `${headText}.${tailText}` : headText || tailText;
2422
+ }
2423
+ return node.text.trim();
2424
+ }
2425
+ function parseCSharpUsing(node) {
2426
+ const namedChildren = node.namedChildren.filter((child) => child !== null);
2427
+ if (namedChildren.length === 0) {
2428
+ return void 0;
2429
+ }
2430
+ let aliasName;
2431
+ let pathNode = null;
2432
+ if (namedChildren.length >= 2 && namedChildren[0]?.type === "identifier" && namedChildren[1]) {
2433
+ aliasName = namedChildren[0].text.trim();
2434
+ pathNode = namedChildren[1];
2435
+ } else {
2436
+ pathNode = namedChildren[0] ?? null;
2437
+ }
2438
+ const specifier = flattenCSharpQualifiedName(pathNode);
2439
+ if (!specifier) {
2407
2440
  return void 0;
2408
2441
  }
2409
2442
  return {
2410
- specifier: match[1].trim(),
2443
+ specifier,
2411
2444
  importedSymbols: [],
2412
- isExternal: !match[1].trim().startsWith("."),
2445
+ namespaceImport: aliasName,
2446
+ isExternal: !specifier.startsWith("."),
2413
2447
  reExport: false
2414
2448
  };
2415
2449
  }
2416
- function parsePhpUse(text) {
2417
- const cleaned = text.trim().replace(/^use\s+/, "").replace(/;$/, "");
2418
- return cleaned.split(",").map((item) => item.trim()).filter(Boolean).map((item) => {
2419
- const aliasMatch = item.match(/^(.+?)\s+as\s+([A-Za-z_]\w*)$/i);
2420
- const specifier = (aliasMatch ? aliasMatch[1] : item).trim();
2421
- return {
2422
- specifier,
2423
- importedSymbols: [],
2424
- namespaceImport: aliasMatch?.[2],
2425
- isExternal: !specifier.startsWith("."),
2426
- reExport: false
2427
- };
2428
- });
2450
+ function flattenPhpQualifiedName(node) {
2451
+ if (!node) {
2452
+ return "";
2453
+ }
2454
+ if (node.type === "name") {
2455
+ return node.text.trim();
2456
+ }
2457
+ if (node.type === "namespace_name") {
2458
+ return node.namedChildren.filter((child) => child?.type === "name").map((child) => child.text.trim()).filter(Boolean).join("\\");
2459
+ }
2460
+ if (node.type === "qualified_name") {
2461
+ const parts = [];
2462
+ for (const child of node.namedChildren) {
2463
+ if (!child) {
2464
+ continue;
2465
+ }
2466
+ if (child.type === "namespace_name") {
2467
+ parts.push(flattenPhpQualifiedName(child));
2468
+ } else if (child.type === "name") {
2469
+ parts.push(child.text.trim());
2470
+ }
2471
+ }
2472
+ return parts.filter(Boolean).join("\\");
2473
+ }
2474
+ return node.text.trim();
2429
2475
  }
2430
- function parseCppInclude(text) {
2431
- const match = text.trim().match(/^#include\s+([<"].+[>"])$/);
2432
- if (!match) {
2476
+ function parsePhpUseClause(clause, prefix) {
2477
+ const names = clause.namedChildren.filter((child) => child?.type === "name");
2478
+ const qualified = clause.namedChildren.find((child) => child?.type === "qualified_name") ?? null;
2479
+ let specifier;
2480
+ let aliasName;
2481
+ if (qualified) {
2482
+ specifier = flattenPhpQualifiedName(qualified);
2483
+ if (names.length >= 1 && names[0]) {
2484
+ aliasName = names[0].text.trim();
2485
+ }
2486
+ } else if (names.length >= 1 && names[0]) {
2487
+ specifier = names[0].text.trim();
2488
+ if (names.length >= 2 && names[1]) {
2489
+ aliasName = names[1].text.trim();
2490
+ }
2491
+ } else {
2433
2492
  return void 0;
2434
2493
  }
2435
- const specifier = quotedPath(match[1]);
2436
- return {
2437
- specifier,
2438
- importedSymbols: [],
2439
- isExternal: match[1].startsWith("<"),
2440
- reExport: false
2441
- };
2494
+ if (prefix && specifier) {
2495
+ specifier = `${prefix}\\${specifier}`;
2496
+ }
2497
+ if (!specifier) {
2498
+ return void 0;
2499
+ }
2500
+ return {
2501
+ specifier,
2502
+ importedSymbols: [],
2503
+ namespaceImport: aliasName,
2504
+ isExternal: true,
2505
+ reExport: false
2506
+ };
2507
+ }
2508
+ function parsePhpUse(node) {
2509
+ const results = [];
2510
+ const groupNode = node.namedChildren.find((child) => child?.type === "namespace_use_group") ?? null;
2511
+ if (groupNode) {
2512
+ const prefixNode = node.namedChildren.find((child) => child?.type === "namespace_name") ?? null;
2513
+ const prefix = flattenPhpQualifiedName(prefixNode);
2514
+ for (const clause of groupNode.namedChildren) {
2515
+ if (!clause || clause.type !== "namespace_use_clause") {
2516
+ continue;
2517
+ }
2518
+ const parsed = parsePhpUseClause(clause, prefix);
2519
+ if (parsed) {
2520
+ results.push(parsed);
2521
+ }
2522
+ }
2523
+ return results;
2524
+ }
2525
+ for (const child of node.namedChildren) {
2526
+ if (!child || child.type !== "namespace_use_clause") {
2527
+ continue;
2528
+ }
2529
+ const parsed = parsePhpUseClause(child, "");
2530
+ if (parsed) {
2531
+ results.push(parsed);
2532
+ }
2533
+ }
2534
+ return results;
2535
+ }
2536
+ function parseCppInclude(node) {
2537
+ for (const child of node.namedChildren) {
2538
+ if (!child) {
2539
+ continue;
2540
+ }
2541
+ if (child.type === "system_lib_string") {
2542
+ const specifier = child.text.replace(/^</, "").replace(/>$/, "").trim();
2543
+ if (!specifier) {
2544
+ return void 0;
2545
+ }
2546
+ return {
2547
+ specifier,
2548
+ importedSymbols: [],
2549
+ isExternal: true,
2550
+ reExport: false
2551
+ };
2552
+ }
2553
+ if (child.type === "string_literal") {
2554
+ const contentNode = child.namedChildren.find((inner) => inner?.type === "string_content") ?? null;
2555
+ const specifier = (contentNode?.text ?? child.text.replace(/^"|"$/g, "")).trim();
2556
+ if (!specifier) {
2557
+ return void 0;
2558
+ }
2559
+ return {
2560
+ specifier,
2561
+ importedSymbols: [],
2562
+ isExternal: false,
2563
+ reExport: false
2564
+ };
2565
+ }
2566
+ }
2567
+ return void 0;
2442
2568
  }
2443
2569
  function rubyStringContent(node) {
2444
2570
  if (!node) {
@@ -2447,14 +2573,19 @@ function rubyStringContent(node) {
2447
2573
  const contentNode = node.descendantsOfType(["string_content", "simple_symbol", "bare_string"]).find((item) => item !== null) ?? null;
2448
2574
  return contentNode?.text.trim() || void 0;
2449
2575
  }
2576
+ function normalizePowerShellDotSourceSpecifier(raw) {
2577
+ const unquoted = raw.replace(/^['"]+|['"]+$/g, "").trim();
2578
+ const withoutScriptRoot = unquoted.replace(/^\$PSScriptRoot(?:[\\/]+|$)/i, "./");
2579
+ return withoutScriptRoot.replace(/\\/g, "/");
2580
+ }
2450
2581
  function parsePowerShellImport(commandNode) {
2451
2582
  const commandName = commandNode.descendantsOfType(["command_name", "command_name_expr"]).find((item) => item !== null)?.text.trim();
2452
2583
  const genericTokens = commandNode.descendantsOfType("generic_token").filter((item) => item !== null).map((item) => item.text.trim());
2453
2584
  if (commandNode.namedChildren.some((child) => child?.type === "command_invokation_operator")) {
2454
- const specifier = commandName?.trim();
2455
- if (specifier) {
2585
+ const raw = commandName?.trim();
2586
+ if (raw) {
2456
2587
  return {
2457
- specifier,
2588
+ specifier: normalizePowerShellDotSourceSpecifier(raw),
2458
2589
  importedSymbols: [],
2459
2590
  isExternal: false,
2460
2591
  reExport: false
@@ -2799,11 +2930,11 @@ function pythonCodeAnalysis(manifest, rootNode, diagnostics) {
2799
2930
  continue;
2800
2931
  }
2801
2932
  if (child.type === "import_statement") {
2802
- imports.push(...parsePythonImportStatement(child.text));
2933
+ imports.push(...parsePythonImportStatement(child));
2803
2934
  continue;
2804
2935
  }
2805
2936
  if (child.type === "import_from_statement") {
2806
- imports.push(...parsePythonFromImportStatement(child.text));
2937
+ imports.push(...parsePythonFromImportStatement(child));
2807
2938
  continue;
2808
2939
  }
2809
2940
  if (child.type === "class_definition") {
@@ -2858,7 +2989,7 @@ function goCodeAnalysis(manifest, rootNode, diagnostics) {
2858
2989
  }
2859
2990
  if (child.type === "import_declaration") {
2860
2991
  for (const spec of child.descendantsOfType("import_spec")) {
2861
- const parsed = spec ? parseGoImport(spec.text) : void 0;
2992
+ const parsed = spec ? parseGoImport(spec) : void 0;
2862
2993
  if (parsed) {
2863
2994
  imports.push(parsed);
2864
2995
  }
@@ -2932,7 +3063,22 @@ function rustCodeAnalysis(manifest, rootNode, diagnostics) {
2932
3063
  continue;
2933
3064
  }
2934
3065
  if (child.type === "use_declaration") {
2935
- imports.push(parseRustUse(child.text));
3066
+ imports.push(...parseRustUseDeclaration(child));
3067
+ continue;
3068
+ }
3069
+ if (child.type === "mod_item") {
3070
+ const hasInlineBody = child.namedChildren.some((item) => item?.type === "declaration_list");
3071
+ if (!hasInlineBody) {
3072
+ const modName = extractIdentifier(child.childForFieldName("name") ?? findNamedChild(child, "identifier"));
3073
+ if (modName) {
3074
+ imports.push({
3075
+ specifier: `self::${modName}`,
3076
+ importedSymbols: [],
3077
+ isExternal: false,
3078
+ reExport: false
3079
+ });
3080
+ }
3081
+ }
2936
3082
  continue;
2937
3083
  }
2938
3084
  const name = child.type === "function_item" ? extractIdentifier(child.childForFieldName("name")) : extractIdentifier(child.childForFieldName("name"));
@@ -2996,11 +3142,15 @@ function javaCodeAnalysis(manifest, rootNode, diagnostics) {
2996
3142
  continue;
2997
3143
  }
2998
3144
  if (child.type === "package_declaration") {
2999
- packageName = child.text.replace(/^package\s+/, "").replace(/;$/, "").trim();
3145
+ const pathNode = child.namedChildren.find((inner) => inner?.type === "scoped_identifier" || inner?.type === "identifier") ?? null;
3146
+ const flattened = flattenJavaScopedIdentifier(pathNode);
3147
+ if (flattened) {
3148
+ packageName = flattened;
3149
+ }
3000
3150
  continue;
3001
3151
  }
3002
3152
  if (child.type === "import_declaration") {
3003
- imports.push(parseJavaImport(child.text));
3153
+ imports.push(parseJavaImport(child));
3004
3154
  continue;
3005
3155
  }
3006
3156
  const name = extractIdentifier(child.childForFieldName("name"));
@@ -3088,7 +3238,7 @@ function kotlinCodeAnalysis(manifest, rootNode, diagnostics) {
3088
3238
  }
3089
3239
  if (child.type === "import_list") {
3090
3240
  for (const importNode of child.descendantsOfType("import_header").filter((item) => item !== null)) {
3091
- const parsed = parseKotlinImport(importNode.text);
3241
+ const parsed = parseKotlinImport(importNode);
3092
3242
  if (parsed) {
3093
3243
  imports.push(parsed);
3094
3244
  }
@@ -3178,7 +3328,7 @@ function scalaCodeAnalysis(manifest, rootNode, diagnostics) {
3178
3328
  continue;
3179
3329
  }
3180
3330
  if (child.type === "import_declaration") {
3181
- imports.push(...parseScalaImport(child.text));
3331
+ imports.push(...parseScalaImport(child));
3182
3332
  continue;
3183
3333
  }
3184
3334
  if (child.type === "function_definition") {
@@ -3359,7 +3509,7 @@ function csharpCodeAnalysis(manifest, rootNode, diagnostics) {
3359
3509
  continue;
3360
3510
  }
3361
3511
  if (child.type === "using_directive") {
3362
- const parsed = parseCSharpUsing(child.text);
3512
+ const parsed = parseCSharpUsing(child);
3363
3513
  if (parsed) {
3364
3514
  imports.push(parsed);
3365
3515
  }
@@ -3451,7 +3601,7 @@ function phpCodeAnalysis(manifest, rootNode, diagnostics) {
3451
3601
  continue;
3452
3602
  }
3453
3603
  if (child.type === "namespace_use_declaration") {
3454
- imports.push(...parsePhpUse(child.text));
3604
+ imports.push(...parsePhpUse(child));
3455
3605
  continue;
3456
3606
  }
3457
3607
  const name = extractIdentifier(child.childForFieldName("name"));
@@ -3518,8 +3668,9 @@ function rubyCodeAnalysis(manifest, rootNode, diagnostics) {
3518
3668
  if (callee === "require" || callee === "require_relative") {
3519
3669
  const specifier = rubyStringContent(child.childForFieldName("arguments") ?? child.namedChildren.at(1) ?? null);
3520
3670
  if (specifier) {
3671
+ const normalizedSpecifier = callee === "require_relative" && !specifier.startsWith(".") && !specifier.startsWith("/") ? `./${specifier}` : specifier;
3521
3672
  imports.push({
3522
- specifier,
3673
+ specifier: normalizedSpecifier,
3523
3674
  importedSymbols: [],
3524
3675
  isExternal: callee === "require" && !specifier.startsWith("."),
3525
3676
  reExport: false
@@ -3674,7 +3825,7 @@ function cFamilyCodeAnalysis(manifest, language, rootNode, diagnostics) {
3674
3825
  continue;
3675
3826
  }
3676
3827
  if (child.type === "preproc_include") {
3677
- const parsed = parseCppInclude(child.text);
3828
+ const parsed = parseCppInclude(child);
3678
3829
  if (parsed) {
3679
3830
  imports.push(parsed);
3680
3831
  }
@@ -3766,7 +3917,7 @@ async function analyzeTreeSitterCode(manifest, content, language) {
3766
3917
  };
3767
3918
  }
3768
3919
  try {
3769
- const diagnostics = diagnosticsFromTree(tree.rootNode);
3920
+ const diagnostics = language === "lua" ? [] : diagnosticsFromTree(tree.rootNode);
3770
3921
  const rationales = extractTreeSitterRationales(manifest, language, tree.rootNode);
3771
3922
  switch (language) {
3772
3923
  case "bash":
@@ -4509,7 +4660,13 @@ function modulePageTitle(manifest) {
4509
4660
  }
4510
4661
  function importResolutionCandidates(basePath, specifier, extensions) {
4511
4662
  const resolved = path6.posix.normalize(path6.posix.join(path6.posix.dirname(basePath), specifier));
4512
- if (path6.posix.extname(resolved)) {
4663
+ const resolvedExt = path6.posix.extname(resolved);
4664
+ if (resolvedExt) {
4665
+ if (extensions.includes(resolvedExt)) {
4666
+ const resolvedBase = resolved.slice(0, -resolvedExt.length);
4667
+ const candidates = [resolved, ...extensions.map((extension) => `${resolvedBase}${extension}`)];
4668
+ return uniqueBy(candidates, (candidate) => candidate);
4669
+ }
4513
4670
  return [resolved];
4514
4671
  }
4515
4672
  const direct = extensions.map((extension) => path6.posix.normalize(`${resolved}${extension}`));
@@ -4602,13 +4759,36 @@ async function readNearestGoModulePath(startPath, cache) {
4602
4759
  current = parent;
4603
4760
  }
4604
4761
  }
4605
- function rustModuleAlias(repoRelativePath) {
4606
- const withoutExt = stripCodeExtension2(normalizeAlias(repoRelativePath));
4607
- const trimmed = withoutExt.replace(/^src\//, "").replace(/\/mod$/i, "");
4608
- if (!trimmed || trimmed === "lib" || trimmed === "main") {
4609
- return "crate";
4762
+ function rustModuleAliases(repoRelativePath) {
4763
+ const withoutExt = stripCodeExtension2(normalizeAlias(repoRelativePath)).replace(/\/mod$/i, "");
4764
+ if (!withoutExt) {
4765
+ return [];
4766
+ }
4767
+ const result = [];
4768
+ const push = (moduleTail) => {
4769
+ const trimmed = moduleTail.replace(/^\/+|\/+$/g, "");
4770
+ if (!trimmed || trimmed === "lib" || trimmed === "main") {
4771
+ result.push("crate");
4772
+ return;
4773
+ }
4774
+ const rootStripped = trimmed.replace(/\/(?:lib|main)$/i, "");
4775
+ if (rootStripped !== trimmed && rootStripped) {
4776
+ result.push(`crate::${rootStripped.replace(/\//g, "::")}`);
4777
+ }
4778
+ result.push(`crate::${trimmed.replace(/\//g, "::")}`);
4779
+ };
4780
+ const srcIdx = withoutExt.lastIndexOf("/src/");
4781
+ if (srcIdx >= 0) {
4782
+ push(withoutExt.slice(srcIdx + "/src/".length));
4783
+ }
4784
+ if (withoutExt.startsWith("src/")) {
4785
+ push(withoutExt.slice("src/".length));
4610
4786
  }
4611
- return `crate::${trimmed.replace(/\//g, "::")}`;
4787
+ const segments = withoutExt.split("/").filter(Boolean);
4788
+ for (let start = 0; start < segments.length; start += 1) {
4789
+ push(segments.slice(start).join("/"));
4790
+ }
4791
+ return uniqueBy(result.filter(Boolean), (item) => item);
4612
4792
  }
4613
4793
  function candidateExtensionsFor(language) {
4614
4794
  switch (language) {
@@ -4684,7 +4864,9 @@ async function buildCodeIndex(rootDir, manifests, analyses) {
4684
4864
  break;
4685
4865
  case "rust":
4686
4866
  if (repoRelativePath) {
4687
- recordAlias(aliases, rustModuleAlias(repoRelativePath));
4867
+ for (const alias of rustModuleAliases(repoRelativePath)) {
4868
+ recordAlias(aliases, alias);
4869
+ }
4688
4870
  }
4689
4871
  break;
4690
4872
  case "go": {
@@ -4795,6 +4977,9 @@ function aliasMatches(lookup, ...aliases) {
4795
4977
  (entry) => entry.sourceId
4796
4978
  );
4797
4979
  }
4980
+ function aliasMatchesExact(lookup, alias) {
4981
+ return lookup.byAlias.get(normalizeAlias(alias)) ?? [];
4982
+ }
4798
4983
  function repoPathMatches(lookup, ...repoPaths) {
4799
4984
  return uniqueBy(
4800
4985
  repoPaths.map((repoPath) => lookup.byRepoPath.get(normalizeAlias(repoPath))).filter((entry) => Boolean(entry)),
@@ -4812,30 +4997,119 @@ function resolvePythonRelativeAliases(repoRelativePath, specifier) {
4812
4997
  }
4813
4998
  function resolveRustAliases(manifest, specifier) {
4814
4999
  const repoRelativePath = manifest.repoRelativePath ? normalizeAlias(manifest.repoRelativePath) : "";
4815
- const currentAlias = repoRelativePath ? rustModuleAlias(repoRelativePath) : void 0;
4816
- if (!specifier.startsWith("self::") && !specifier.startsWith("super::")) {
5000
+ if (!specifier.startsWith("self::") && !specifier.startsWith("super::") && specifier !== "self" && specifier !== "super") {
4817
5001
  return [specifier];
4818
5002
  }
4819
- if (!currentAlias) {
5003
+ const candidateAliases = repoRelativePath ? rustModuleAliases(repoRelativePath) : [];
5004
+ if (candidateAliases.length === 0) {
4820
5005
  return [];
4821
5006
  }
4822
- const currentParts = currentAlias.replace(/^crate::?/, "").split("::").filter(Boolean);
4823
- if (specifier.startsWith("self::")) {
4824
- return [`crate${currentParts.length ? `::${currentParts.join("::")}` : ""}::${specifier.slice("self::".length)}`];
5007
+ const tailAfter = specifier.startsWith("self::") ? specifier.slice("self::".length) : specifier.startsWith("super::") ? specifier.slice("super::".length) : "";
5008
+ const superRelative = specifier.startsWith("super");
5009
+ const expansions = [];
5010
+ for (const currentAlias of candidateAliases) {
5011
+ const currentParts = currentAlias.replace(/^crate(?:::)?/, "").split("::").filter(Boolean);
5012
+ if (superRelative) {
5013
+ if (currentParts.length > 0) {
5014
+ const parentParts = currentParts.slice(0, -1);
5015
+ const expanded2 = `crate${parentParts.length ? `::${parentParts.join("::")}` : ""}${tailAfter ? `::${tailAfter}` : ""}`.replace(/::+/g, "::").replace(/::$/, "");
5016
+ expansions.push(expanded2);
5017
+ }
5018
+ continue;
5019
+ }
5020
+ const expanded = `crate${currentParts.length ? `::${currentParts.join("::")}` : ""}${tailAfter ? `::${tailAfter}` : ""}`.replace(/::+/g, "::").replace(/::$/, "");
5021
+ expansions.push(expanded);
5022
+ }
5023
+ return uniqueBy(expansions, (item) => item);
5024
+ }
5025
+ function rustCrateRootPrefix(repoRelativePath) {
5026
+ if (!repoRelativePath) {
5027
+ return void 0;
5028
+ }
5029
+ const normalized = normalizeAlias(repoRelativePath);
5030
+ const idx = normalized.lastIndexOf("/src/");
5031
+ if (idx >= 0) {
5032
+ return normalized.slice(0, idx + "/src/".length);
4825
5033
  }
4826
- return [
4827
- `crate${currentParts.length > 1 ? `::${currentParts.slice(0, -1).join("::")}` : ""}::${specifier.slice("super::".length)}`.replace(/::+/g, "::").replace(/::$/, "")
4828
- ];
5034
+ if (normalized.startsWith("src/")) {
5035
+ return "src/";
5036
+ }
5037
+ return void 0;
5038
+ }
5039
+ function filterRustCandidatesToSameCrate(candidates, consumerPath) {
5040
+ const normalizedConsumer = consumerPath ? normalizeAlias(consumerPath) : "";
5041
+ const withoutSelf = normalizedConsumer ? candidates.filter((entry) => normalizeAlias(entry.repoRelativePath ?? "") !== normalizedConsumer) : candidates;
5042
+ if (withoutSelf.length <= 1) {
5043
+ return withoutSelf;
5044
+ }
5045
+ const cratePrefix = rustCrateRootPrefix(consumerPath);
5046
+ if (cratePrefix) {
5047
+ const sameCrate = withoutSelf.filter((entry) => normalizeAlias(entry.repoRelativePath ?? "").startsWith(cratePrefix));
5048
+ if (sameCrate.length > 0) {
5049
+ return sameCrate;
5050
+ }
5051
+ }
5052
+ if (normalizedConsumer) {
5053
+ let dir = path6.posix.dirname(normalizedConsumer);
5054
+ while (dir && dir !== "." && dir !== "/") {
5055
+ const prefix = `${dir}/`;
5056
+ const sameTree = withoutSelf.filter((entry) => normalizeAlias(entry.repoRelativePath ?? "").startsWith(prefix));
5057
+ if (sameTree.length > 0) {
5058
+ return sameTree;
5059
+ }
5060
+ const parent = path6.posix.dirname(dir);
5061
+ if (parent === dir) {
5062
+ break;
5063
+ }
5064
+ dir = parent;
5065
+ }
5066
+ }
5067
+ return withoutSelf;
5068
+ }
5069
+ function resolveRustAliasWithStripping(alias, lookup, consumerPath) {
5070
+ const segments = alias.split("::");
5071
+ while (segments.length > 0) {
5072
+ const candidate = segments.join("::");
5073
+ const matches = aliasMatchesExact(lookup, candidate);
5074
+ const filtered = matches.length > 0 ? filterRustCandidatesToSameCrate(matches, consumerPath) : [];
5075
+ if (filtered.length > 0) {
5076
+ return filtered;
5077
+ }
5078
+ if (candidate === "crate" || candidate === "self" || candidate === "super") {
5079
+ break;
5080
+ }
5081
+ segments.pop();
5082
+ }
5083
+ return [];
4829
5084
  }
4830
5085
  function luaSpecifierLooksLocal(specifier) {
4831
5086
  return /^[A-Za-z_][A-Za-z0-9_]*(?:[./][A-Za-z_][A-Za-z0-9_]*)*$/.test(specifier);
4832
5087
  }
4833
- function resolveLuaModuleCandidates(specifier) {
5088
+ function resolveLuaModuleCandidates(specifier, repoRelativePath) {
4834
5089
  const normalized = normalizeAlias(specifier.replace(/\./g, "/"));
4835
5090
  if (!normalized) {
4836
5091
  return [];
4837
5092
  }
4838
- return uniqueBy([`${normalized}.lua`, path6.posix.join(normalized, "init.lua")], (item) => item);
5093
+ const bases = /* @__PURE__ */ new Set([normalized]);
5094
+ bases.add(`src/${normalized}`);
5095
+ bases.add(`lua/${normalized}`);
5096
+ if (repoRelativePath) {
5097
+ let dir = path6.posix.dirname(repoRelativePath);
5098
+ while (dir && dir !== "." && dir !== "/") {
5099
+ bases.add(`${dir}/${normalized}`);
5100
+ const parent = path6.posix.dirname(dir);
5101
+ if (parent === dir) {
5102
+ break;
5103
+ }
5104
+ dir = parent;
5105
+ }
5106
+ }
5107
+ const candidates = [];
5108
+ for (const base of bases) {
5109
+ candidates.push(`${base}.lua`);
5110
+ candidates.push(path6.posix.join(base, "init.lua"));
5111
+ }
5112
+ return uniqueBy(candidates, (item) => item);
4839
5113
  }
4840
5114
  function findImportCandidates(manifest, codeImport, lookup) {
4841
5115
  const language = manifest.language ?? inferCodeLanguage(manifest.originalPath ?? manifest.storedPath, manifest.mimeType);
@@ -4863,7 +5137,7 @@ function findImportCandidates(manifest, codeImport, lookup) {
4863
5137
  case "dart":
4864
5138
  return repoRelativePath && dartSpecifierLooksLocal2(codeImport.specifier) ? repoPathMatches(lookup, ...importResolutionCandidates(repoRelativePath, codeImport.specifier, candidateExtensionsFor(language))) : aliasMatches(lookup, codeImport.specifier);
4865
5139
  case "lua":
4866
- return luaSpecifierLooksLocal(codeImport.specifier) ? repoPathMatches(lookup, ...resolveLuaModuleCandidates(codeImport.specifier)) : aliasMatches(lookup, codeImport.specifier, codeImport.specifier.replace(/\./g, "/"));
5140
+ return luaSpecifierLooksLocal(codeImport.specifier) ? repoPathMatches(lookup, ...resolveLuaModuleCandidates(codeImport.specifier, repoRelativePath)) : aliasMatches(lookup, codeImport.specifier, codeImport.specifier.replace(/\./g, "/"));
4867
5141
  case "zig":
4868
5142
  return repoRelativePath && (!codeImport.isExternal || codeImport.specifier.endsWith(".zig")) ? repoPathMatches(lookup, ...importResolutionCandidates(repoRelativePath, codeImport.specifier, candidateExtensionsFor(language))) : aliasMatches(lookup, codeImport.specifier);
4869
5143
  case "php":
@@ -4882,8 +5156,15 @@ function findImportCandidates(manifest, codeImport, lookup) {
4882
5156
  codeImport.specifier.replace(/\\/g, "/"),
4883
5157
  stripCodeExtension2(codeImport.specifier.replace(/\\/g, "/"))
4884
5158
  );
4885
- case "rust":
4886
- return aliasMatches(lookup, codeImport.specifier, ...resolveRustAliases(manifest, codeImport.specifier));
5159
+ case "rust": {
5160
+ for (const alias of [codeImport.specifier, ...resolveRustAliases(manifest, codeImport.specifier)]) {
5161
+ const matches = resolveRustAliasWithStripping(alias, lookup, repoRelativePath);
5162
+ if (matches.length > 0) {
5163
+ return matches;
5164
+ }
5165
+ }
5166
+ return [];
5167
+ }
4887
5168
  case "c":
4888
5169
  case "cpp":
4889
5170
  return repoRelativePath && !codeImport.isExternal ? repoPathMatches(lookup, ...importResolutionCandidates(repoRelativePath, codeImport.specifier, candidateExtensionsFor(language))) : aliasMatches(lookup, codeImport.specifier);
@@ -4892,7 +5173,7 @@ function findImportCandidates(manifest, codeImport, lookup) {
4892
5173
  }
4893
5174
  }
4894
5175
  function importLooksLocal(manifest, codeImport, candidates) {
4895
- if (candidates.length > 0) {
5176
+ if (candidates.length === 1) {
4896
5177
  return true;
4897
5178
  }
4898
5179
  const language = manifest.language ?? inferCodeLanguage(manifest.originalPath ?? manifest.storedPath, manifest.mimeType);
@@ -5623,7 +5904,7 @@ async function extractEpubChapters(input) {
5623
5904
  if (!markdown) {
5624
5905
  continue;
5625
5906
  }
5626
- const chapterTitle = firstHtmlHeading(html) || markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() || item.href;
5907
+ const chapterTitle = firstHtmlHeading(html) || item.href;
5627
5908
  const normalizedTitle = normalizeWhitespace(chapterTitle);
5628
5909
  if (!normalizedTitle || /^table of contents$/i.test(normalizedTitle)) {
5629
5910
  continue;
@@ -6256,6 +6537,41 @@ async function appendWatchRun(rootDir, run) {
6256
6537
  await appendJsonLine(paths.jobsLogPath, run);
6257
6538
  }
6258
6539
 
6540
+ // src/markdown-ast.ts
6541
+ import { fromMarkdown } from "mdast-util-from-markdown";
6542
+ function parseMarkdownNodes(text) {
6543
+ try {
6544
+ const root = fromMarkdown(text);
6545
+ return Array.isArray(root.children) ? root.children : [];
6546
+ } catch {
6547
+ return [];
6548
+ }
6549
+ }
6550
+ function markdownNodeText(node) {
6551
+ if (node.type === "text" || node.type === "inlineCode" || node.type === "code") {
6552
+ return normalizeWhitespace(node.value ?? "");
6553
+ }
6554
+ if (node.type === "image") {
6555
+ return normalizeWhitespace(node.alt ?? "");
6556
+ }
6557
+ if (node.type === "break" || node.type === "thematicBreak") {
6558
+ return " ";
6559
+ }
6560
+ return normalizeWhitespace((node.children ?? []).map((child) => markdownNodeText(child)).join(" "));
6561
+ }
6562
+ function firstMarkdownHeading(text) {
6563
+ const nodes = parseMarkdownNodes(text);
6564
+ for (const node of nodes) {
6565
+ if (node.type === "heading") {
6566
+ const title = markdownNodeText(node).trim();
6567
+ if (title) {
6568
+ return title;
6569
+ }
6570
+ }
6571
+ }
6572
+ return void 0;
6573
+ }
6574
+
6259
6575
  // src/source-classification.ts
6260
6576
  import path9 from "path";
6261
6577
  var ALL_SOURCE_CLASSES = ["first_party", "third_party", "resource", "generated"];
@@ -6610,7 +6926,7 @@ function inferKind(mimeType, filePath, detectionOptions = {}) {
6610
6926
  if (mimeType === "text/csv" || mimeType === "text/tab-separated-values" || filePath.toLowerCase().endsWith(".csv") || filePath.toLowerCase().endsWith(".tsv")) {
6611
6927
  return "csv";
6612
6928
  }
6613
- if (mimeType.startsWith("text/") || isStructuredTextMime(mimeType) || isKnownTextPath(filePath)) {
6929
+ if (mimeType.startsWith("text/") || isStructuredTextMime(mimeType)) {
6614
6930
  return "text";
6615
6931
  }
6616
6932
  if (mimeType === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || filePath.toLowerCase().endsWith(".xlsx")) {
@@ -6685,8 +7001,7 @@ function titleFromText(fallback, content, filePath) {
6685
7001
  return rstTitle;
6686
7002
  }
6687
7003
  }
6688
- const heading = content.match(/^#\s+(.+)$/m)?.[1]?.trim();
6689
- return heading || fallback;
7004
+ return firstMarkdownHeading(content) ?? fallback;
6690
7005
  }
6691
7006
  function guessMimeType(target) {
6692
7007
  if (isRstFilePath(target)) {
@@ -6696,125 +7011,30 @@ function guessMimeType(target) {
6696
7011
  if (extension === ".ts" || extension === ".tsx" || extension === ".mts" || extension === ".cts") {
6697
7012
  return "text/typescript";
6698
7013
  }
6699
- const looked = mime.lookup(target);
6700
- if (looked) {
6701
- return looked;
6702
- }
6703
- if (isKnownTextPath(target)) {
6704
- return "text/plain";
6705
- }
6706
- return "application/octet-stream";
6707
- }
6708
- var KNOWN_TEXT_DOTFILE_NAMES = /* @__PURE__ */ new Set([
6709
- ".gitignore",
6710
- ".gitattributes",
6711
- ".gitkeep",
6712
- ".gitmodules",
6713
- ".editorconfig",
6714
- ".npmrc",
6715
- ".yarnrc",
6716
- ".prettierignore",
6717
- ".prettierrc",
6718
- ".dockerignore",
6719
- ".eslintignore",
6720
- ".eslintrc",
6721
- ".nvmrc",
6722
- ".node-version",
6723
- ".python-version",
6724
- ".ruby-version",
6725
- ".tool-versions"
6726
- ]);
6727
- var KNOWN_TEXT_BASENAMES = /* @__PURE__ */ new Set([
6728
- "readme",
6729
- "license",
6730
- "licence",
6731
- "copying",
6732
- "unlicense",
6733
- "notice",
6734
- "authors",
6735
- "contributors",
6736
- "patents",
6737
- "maintainers",
6738
- "owners",
6739
- "codeowners",
6740
- "changelog",
6741
- "changes",
6742
- "history",
6743
- "news",
6744
- "todo",
6745
- "install",
6746
- "dockerfile",
6747
- "containerfile",
6748
- "makefile",
6749
- "gnumakefile",
6750
- "rakefile",
6751
- "gemfile",
6752
- "procfile",
6753
- "jenkinsfile",
6754
- "vagrantfile",
6755
- "brewfile",
6756
- "go.mod",
6757
- "go.sum",
6758
- "go.work",
6759
- "go.work.sum",
6760
- "cargo.lock",
6761
- "pipfile",
6762
- "pipfile.lock",
6763
- "poetry.lock",
6764
- "uv.lock",
6765
- "py.typed",
6766
- "package-lock.json",
6767
- "yarn.lock",
6768
- "pnpm-lock.yaml",
6769
- "composer.lock",
6770
- "requirements.txt"
6771
- ]);
6772
- var KNOWN_TEXT_BASENAME_PREFIXES = ["license", "licence", "copying", "unlicense", "readme", "changelog", "dockerfile", "containerfile"];
6773
- var KNOWN_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
6774
- ".toml",
6775
- ".lock",
6776
- ".tmpl",
6777
- ".template",
6778
- ".mustache",
6779
- ".hbs",
6780
- ".handlebars",
6781
- ".ejs",
6782
- ".njk",
6783
- ".liquid",
6784
- ".vim",
6785
- ".typed",
6786
- ".env",
6787
- ".properties",
6788
- ".ini",
6789
- ".cfg",
6790
- ".conf",
6791
- ".config",
6792
- ".bazel",
6793
- ".bzl",
6794
- ".bat",
6795
- ".cmd"
6796
- ]);
6797
- function isKnownTextPath(target) {
6798
- const basename = path12.basename(target).toLowerCase();
6799
- if (KNOWN_TEXT_DOTFILE_NAMES.has(basename)) {
6800
- return true;
6801
- }
6802
- if (basename === ".env" || basename.startsWith(".env.")) {
6803
- return true;
6804
- }
6805
- if (KNOWN_TEXT_BASENAMES.has(basename)) {
6806
- return true;
7014
+ return mime.lookup(target) || "application/octet-stream";
7015
+ }
7016
+ function refineBinaryKindWithBytes(absolutePath, currentKind, bytes) {
7017
+ if (currentKind !== "binary") {
7018
+ return currentKind;
6807
7019
  }
6808
- for (const prefix of KNOWN_TEXT_BASENAME_PREFIXES) {
6809
- if (basename === prefix || basename.startsWith(`${prefix}-`) || basename.startsWith(`${prefix}.`)) {
6810
- return true;
6811
- }
7020
+ const sniffSlice = bytes.length > 4096 ? bytes.subarray(0, 4096) : bytes;
7021
+ return isText(absolutePath, sniffSlice) ? "text" : currentKind;
7022
+ }
7023
+ async function refineBinaryKindWithContentSniff(absolutePath, currentKind) {
7024
+ if (currentKind !== "binary") {
7025
+ return currentKind;
6812
7026
  }
6813
- const extension = path12.extname(target).toLowerCase();
6814
- if (extension && KNOWN_TEXT_EXTENSIONS.has(extension)) {
6815
- return true;
7027
+ let handle;
7028
+ try {
7029
+ handle = await fs11.open(absolutePath, "r");
7030
+ const chunk = Buffer.alloc(4096);
7031
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, 0);
7032
+ return refineBinaryKindWithBytes(absolutePath, currentKind, chunk.subarray(0, bytesRead));
7033
+ } catch {
7034
+ return currentKind;
7035
+ } finally {
7036
+ await handle?.close().catch(() => void 0);
6816
7037
  }
6817
- return false;
6818
7038
  }
6819
7039
  function sourceGroupIdFor(prepared) {
6820
7040
  const originKey = prepared.originType === "url" ? prepared.url ?? prepared.title : prepared.originalPath ?? prepared.title;
@@ -6945,8 +7165,7 @@ function normalizeRstExtractedText(content) {
6945
7165
  }
6946
7166
  function titleFromRst(fallback, content) {
6947
7167
  const normalized = normalizeRstExtractedText(content);
6948
- const heading = normalized.match(/^#+\s+(.+)$/m)?.[1]?.trim();
6949
- return heading || fallback;
7168
+ return firstMarkdownHeading(normalized) ?? fallback;
6950
7169
  }
6951
7170
  function extractedTextForPlainSource(filePath, sourceKind, content) {
6952
7171
  if (sourceKind === "text" && isRstFilePath(filePath)) {
@@ -7666,6 +7885,7 @@ async function collectDirectoryFiles(rootDir, inputDir, repoRoot, options) {
7666
7885
  sourceKind = "chat_export";
7667
7886
  }
7668
7887
  }
7888
+ sourceKind = await refineBinaryKindWithContentSniff(absolutePath, sourceKind);
7669
7889
  const sourceClass = sourceClassForRelativePath(relativePath, options);
7670
7890
  if (!supportedDirectoryKind(sourceKind)) {
7671
7891
  skipped.push({ path: toPosix(path12.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
@@ -8287,7 +8507,7 @@ async function prepareFileInputs(rootDir, absoluteInput, repoRoot, sourceClass)
8287
8507
  }
8288
8508
  const mimeType = guessMimeType(absoluteInput);
8289
8509
  const detectionOptions = await localCodeDetectionOptions(absoluteInput, payloadBytes);
8290
- const sourceKind = inferKind(mimeType, absoluteInput, detectionOptions);
8510
+ const sourceKind = refineBinaryKindWithBytes(absoluteInput, inferKind(mimeType, absoluteInput, detectionOptions), payloadBytes);
8291
8511
  const language = inferCodeLanguage(absoluteInput, mimeType, detectionOptions);
8292
8512
  const storedExtension = path12.extname(absoluteInput) || `.${mime.extension(mimeType) || "bin"}`;
8293
8513
  let title;
@@ -9071,6 +9291,7 @@ async function importInbox(rootDir, inputDir) {
9071
9291
  sourceKind = "chat_export";
9072
9292
  }
9073
9293
  }
9294
+ sourceKind = await refineBinaryKindWithContentSniff(absolutePath, sourceKind);
9074
9295
  if (!isSupportedInboxKind(sourceKind)) {
9075
9296
  skipped.push({ path: toPosix(path12.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
9076
9297
  continue;
@@ -9268,8 +9489,55 @@ import { z as z7 } from "zod";
9268
9489
 
9269
9490
  // src/analysis.ts
9270
9491
  import path14 from "path";
9271
- import { fromMarkdown } from "mdast-util-from-markdown";
9492
+ import nlp2 from "compromise";
9272
9493
  import { z as z2 } from "zod";
9494
+
9495
+ // src/tokenize.ts
9496
+ import nlp from "compromise";
9497
+ var CLOSED_CLASS_POS_SELECTOR = "#Determiner, #Preposition, #Conjunction, #Pronoun, #Auxiliary, #Copula";
9498
+ function splitTermToTokens(term, tokens) {
9499
+ for (const piece of term.split(/[^a-z0-9-]+/)) {
9500
+ const trimmed = piece.replace(/^-+|-+$/g, "");
9501
+ if (trimmed.length >= 2) {
9502
+ tokens.push(trimmed);
9503
+ }
9504
+ }
9505
+ }
9506
+ function tokenize(text) {
9507
+ const lower = text.toLowerCase();
9508
+ try {
9509
+ const terms = nlp(lower).terms().out("array");
9510
+ const tokens = [];
9511
+ for (const term of terms) {
9512
+ splitTermToTokens(term, tokens);
9513
+ }
9514
+ if (tokens.length > 0) {
9515
+ return tokens;
9516
+ }
9517
+ } catch {
9518
+ }
9519
+ return lower.match(/[a-z0-9][a-z0-9-]{1,}/g) ?? [];
9520
+ }
9521
+ function contentTokens(text, minLength = 4) {
9522
+ const lower = text.toLowerCase();
9523
+ const tokens = [];
9524
+ try {
9525
+ const contentDoc = nlp(lower).not(CLOSED_CLASS_POS_SELECTOR);
9526
+ const terms = contentDoc.terms().out("array");
9527
+ for (const term of terms) {
9528
+ splitTermToTokens(term, tokens);
9529
+ }
9530
+ } catch {
9531
+ }
9532
+ if (tokens.length === 0) {
9533
+ for (const piece of lower.match(/[a-z0-9][a-z0-9-]{1,}/g) ?? []) {
9534
+ tokens.push(piece);
9535
+ }
9536
+ }
9537
+ return tokens.filter((token) => token.length >= minLength);
9538
+ }
9539
+
9540
+ // src/analysis.ts
9273
9541
  var ANALYSIS_FORMAT_VERSION = 7;
9274
9542
  var sourceAnalysisSchema = z2.object({
9275
9543
  title: z2.string().min(1),
@@ -9288,46 +9556,6 @@ var sourceAnalysisSchema = z2.object({
9288
9556
  questions: z2.array(z2.string()).max(6).default([]),
9289
9557
  tags: z2.array(z2.string()).max(5).default([])
9290
9558
  });
9291
- var STOPWORDS = /* @__PURE__ */ new Set([
9292
- "about",
9293
- "after",
9294
- "also",
9295
- "been",
9296
- "being",
9297
- "between",
9298
- "both",
9299
- "could",
9300
- "does",
9301
- "each",
9302
- "from",
9303
- "have",
9304
- "into",
9305
- "just",
9306
- "more",
9307
- "much",
9308
- "only",
9309
- "other",
9310
- "over",
9311
- "same",
9312
- "some",
9313
- "such",
9314
- "than",
9315
- "that",
9316
- "their",
9317
- "there",
9318
- "these",
9319
- "they",
9320
- "this",
9321
- "very",
9322
- "what",
9323
- "when",
9324
- "where",
9325
- "which",
9326
- "while",
9327
- "with",
9328
- "would",
9329
- "your"
9330
- ]);
9331
9559
  var HEURISTIC_SECTION_SOURCE_KINDS = /* @__PURE__ */ new Map([
9332
9560
  ["transcript", "Transcript"],
9333
9561
  ["chat_export", "Messages"],
@@ -9336,20 +9564,33 @@ var HEURISTIC_SECTION_SOURCE_KINDS = /* @__PURE__ */ new Map([
9336
9564
  ]);
9337
9565
  function extractTopTerms(text, count) {
9338
9566
  const frequency = /* @__PURE__ */ new Map();
9339
- for (const token of text.toLowerCase().match(/[a-z][a-z0-9-]{3,}/g) ?? []) {
9340
- if (STOPWORDS.has(token)) {
9341
- continue;
9342
- }
9567
+ for (const token of contentTokens(text)) {
9343
9568
  frequency.set(token, (frequency.get(token) ?? 0) + 1);
9344
9569
  }
9345
9570
  return [...frequency.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])).slice(0, count).map(([token]) => token);
9346
9571
  }
9347
9572
  function extractEntities(text, count) {
9348
- const matches = text.match(/\b[A-Z][A-Za-z0-9-]+(?:\s+[A-Z][A-Za-z0-9-]+){0,2}\b/g) ?? [];
9349
- return uniqueBy(
9350
- matches.map((value) => normalizeWhitespace(value)),
9351
- (value) => value.toLowerCase()
9352
- ).slice(0, count);
9573
+ const candidates = [];
9574
+ try {
9575
+ const doc = nlp2(text);
9576
+ const segments = [
9577
+ doc.match("#ProperNoun+").out("array"),
9578
+ doc.people().out("array"),
9579
+ doc.places().out("array"),
9580
+ doc.organizations().out("array"),
9581
+ doc.topics().out("array")
9582
+ ];
9583
+ for (const segment of segments) {
9584
+ for (const term of segment) {
9585
+ const normalized = normalizeWhitespace(term);
9586
+ if (normalized) {
9587
+ candidates.push(normalized);
9588
+ }
9589
+ }
9590
+ }
9591
+ } catch {
9592
+ }
9593
+ return uniqueBy(candidates, (value) => value.toLowerCase()).slice(0, count);
9353
9594
  }
9354
9595
  function detectPolarity(text) {
9355
9596
  if (/\b(no|not|never|cannot|can't|won't|without)\b/i.test(text)) {
@@ -9360,26 +9601,6 @@ function detectPolarity(text) {
9360
9601
  }
9361
9602
  return "neutral";
9362
9603
  }
9363
- function parseMarkdownNodes(text) {
9364
- try {
9365
- const root = fromMarkdown(text);
9366
- return Array.isArray(root.children) ? root.children : [];
9367
- } catch {
9368
- return [];
9369
- }
9370
- }
9371
- function markdownNodeText(node) {
9372
- if (node.type === "text" || node.type === "inlineCode" || node.type === "code") {
9373
- return normalizeWhitespace(node.value ?? "");
9374
- }
9375
- if (node.type === "image") {
9376
- return normalizeWhitespace(node.alt ?? "");
9377
- }
9378
- if (node.type === "break" || node.type === "thematicBreak") {
9379
- return " ";
9380
- }
9381
- return normalizeWhitespace((node.children ?? []).map((child) => markdownNodeText(child)).join(" "));
9382
- }
9383
9604
  function markdownNodesText(nodes) {
9384
9605
  return normalizeWhitespace(nodes.map((node) => markdownNodeText(node)).join("\n"));
9385
9606
  }
@@ -10654,7 +10875,7 @@ function filterGraphBySourceClass(graph, sourceClass) {
10654
10875
  }
10655
10876
 
10656
10877
  // src/graph-enrichment.ts
10657
- var STOPWORDS2 = /* @__PURE__ */ new Set([
10878
+ var STOPWORDS = /* @__PURE__ */ new Set([
10658
10879
  "about",
10659
10880
  "after",
10660
10881
  "also",
@@ -10718,7 +10939,7 @@ function addFeature(bucket, reason, value) {
10718
10939
  }
10719
10940
  function themeTokens(value) {
10720
10941
  return uniqueBy(
10721
- normalizeValue(value).split(/[^a-z0-9]+/i).filter((token) => token.length >= 4 && !STOPWORDS2.has(token)),
10942
+ normalizeValue(value).split(/[^a-z0-9]+/i).filter((token) => token.length >= 4 && !STOPWORDS.has(token)),
10722
10943
  (token) => token
10723
10944
  ).slice(0, 6);
10724
10945
  }
@@ -11068,15 +11289,37 @@ function graphAdjacency(graph) {
11068
11289
  }
11069
11290
  return adjacency;
11070
11291
  }
11292
+ var NODE_TYPE_PRIORITY = {
11293
+ concept: 6,
11294
+ entity: 5,
11295
+ source: 4,
11296
+ module: 3,
11297
+ symbol: 2,
11298
+ rationale: 1
11299
+ };
11300
+ function nodeTypePriority(type) {
11301
+ return NODE_TYPE_PRIORITY[type] ?? 0;
11302
+ }
11303
+ function compareLabelCandidates(left, right) {
11304
+ const priorityDelta = nodeTypePriority(right.type) - nodeTypePriority(left.type);
11305
+ if (priorityDelta !== 0) {
11306
+ return priorityDelta;
11307
+ }
11308
+ const degreeDelta = (right.degree ?? 0) - (left.degree ?? 0);
11309
+ if (degreeDelta !== 0) {
11310
+ return degreeDelta;
11311
+ }
11312
+ return left.id.localeCompare(right.id);
11313
+ }
11071
11314
  function resolveNode(graph, target) {
11072
11315
  const normalized = normalizeTarget(target);
11073
11316
  const byId = nodeById(graph);
11074
11317
  if (byId.has(target)) {
11075
11318
  return byId.get(target);
11076
11319
  }
11077
- const exact = graph.nodes.find((node) => normalizeTarget(node.label) === normalized || normalizeTarget(node.id) === normalized);
11078
- if (exact) {
11079
- return exact;
11320
+ const labelMatches = graph.nodes.filter((node) => normalizeTarget(node.label) === normalized || normalizeTarget(node.id) === normalized);
11321
+ if (labelMatches.length) {
11322
+ return labelMatches.sort(compareLabelCandidates)[0];
11080
11323
  }
11081
11324
  const pages = graph.pages.map((page) => ({
11082
11325
  page,
@@ -11085,7 +11328,7 @@ function resolveNode(graph, target) {
11085
11328
  if (pages[0]) {
11086
11329
  return primaryNodeForPage(graph, pages[0].page);
11087
11330
  }
11088
- return graph.nodes.map((node) => ({ node, score: Math.max(scoreMatch(target, node.label), scoreMatch(target, node.id)) })).filter((item) => item.score > 0).sort((left, right) => right.score - left.score || left.node.label.localeCompare(right.node.label))[0]?.node;
11331
+ return graph.nodes.map((node) => ({ node, score: Math.max(scoreMatch(target, node.label), scoreMatch(target, node.id)) })).filter((item) => item.score > 0).sort((left, right) => right.score - left.score || compareLabelCandidates(left.node, right.node))[0]?.node;
11089
11332
  }
11090
11333
  function communityLabel(graph, communityId) {
11091
11334
  if (!communityId) {
@@ -13331,8 +13574,7 @@ function getDatabaseSync() {
13331
13574
  return builtin.DatabaseSync;
13332
13575
  }
13333
13576
  function toFtsQuery(query) {
13334
- const tokens = query.toLowerCase().match(/[a-z0-9]{2,}/g)?.filter(Boolean) ?? [];
13335
- return tokens.join(" OR ");
13577
+ return tokenize(query).join(" OR ");
13336
13578
  }
13337
13579
  function normalizeKind(value) {
13338
13580
  return value === "index" || value === "source" || value === "module" || value === "concept" || value === "entity" || value === "output" || value === "insight" || value === "graph_report" || value === "community_summary" ? value : void 0;
@@ -13761,7 +14003,7 @@ async function resolveImageGenerationProvider(rootDir) {
13761
14003
  if (!providerConfig) {
13762
14004
  throw new Error(`No provider configured with id "${preferredProviderId}" for task "imageProvider".`);
13763
14005
  }
13764
- const { createProvider: createProvider2 } = await import("./registry-NBLIJHZT.js");
14006
+ const { createProvider: createProvider2 } = await import("./registry-W6ZFRI73.js");
13765
14007
  return createProvider2(preferredProviderId, providerConfig, rootDir);
13766
14008
  }
13767
14009
  async function generateOutputArtifacts(rootDir, input) {
@@ -17860,6 +18102,10 @@ function extractClaimSectionLines(content) {
17860
18102
  }
17861
18103
  return found ? claimLines : null;
17862
18104
  }
18105
+ function isClaimPlaceholderBullet(line) {
18106
+ const trimmed = line.trim();
18107
+ return /^-\s+No\s+claims\s+extracted\.?$/i.test(trimmed);
18108
+ }
17863
18109
  function structuralLintFindings(_rootDir, paths, graph, schemas, manifests, sourceProjects) {
17864
18110
  const manifestMap = new Map(manifests.map((manifest) => [manifest.sourceId, manifest]));
17865
18111
  const pageMap2 = new Map(graph.pages.map((page) => [page.id, page]));
@@ -17907,7 +18153,9 @@ function structuralLintFindings(_rootDir, paths, graph, schemas, manifests, sour
17907
18153
  const content = await fs19.readFile(absolutePath, "utf8");
17908
18154
  const claimLines = extractClaimSectionLines(content);
17909
18155
  if (claimLines !== null) {
17910
- const uncited = claimLines.filter((line) => line.startsWith("- ") && !line.includes("[source:"));
18156
+ const uncited = claimLines.filter(
18157
+ (line) => line.startsWith("- ") && !line.includes("[source:") && !isClaimPlaceholderBullet(line)
18158
+ );
17911
18159
  if (uncited.length) {
17912
18160
  findings.push({
17913
18161
  severity: "warning",
@@ -18022,7 +18270,7 @@ async function bootstrapDemo(rootDir, input) {
18022
18270
  }
18023
18271
 
18024
18272
  // src/mcp.ts
18025
- var SERVER_VERSION = "0.6.6";
18273
+ var SERVER_VERSION = "0.6.8";
18026
18274
  async function createMcpServer(rootDir) {
18027
18275
  const server = new McpServer({
18028
18276
  name: "swarmvault",
@@ -18034,10 +18282,10 @@ async function createMcpServer(rootDir) {
18034
18282
  {
18035
18283
  description: "Return the current SwarmVault workspace paths and high-level counts."
18036
18284
  },
18037
- async () => {
18285
+ safeHandler(async () => {
18038
18286
  const info = await getWorkspaceInfo(rootDir);
18039
18287
  return asToolText(info);
18040
- }
18288
+ })
18041
18289
  );
18042
18290
  server.registerTool(
18043
18291
  "search_pages",
@@ -18048,10 +18296,10 @@ async function createMcpServer(rootDir) {
18048
18296
  limit: z8.number().int().min(1).max(25).optional().describe("Maximum number of results")
18049
18297
  }
18050
18298
  },
18051
- async ({ query, limit }) => {
18299
+ safeHandler(async ({ query, limit }) => {
18052
18300
  const results = await searchVault(rootDir, query, limit ?? 5);
18053
18301
  return asToolText(results);
18054
- }
18302
+ })
18055
18303
  );
18056
18304
  server.registerTool(
18057
18305
  "read_page",
@@ -18061,13 +18309,13 @@ async function createMcpServer(rootDir) {
18061
18309
  path: z8.string().min(1).describe("Path relative to wiki/, for example sources/example.md")
18062
18310
  }
18063
18311
  },
18064
- async ({ path: relativePath }) => {
18312
+ safeHandler(async ({ path: relativePath }) => {
18065
18313
  const page = await readPage(rootDir, relativePath);
18066
18314
  if (!page) {
18067
18315
  return asToolError(`Page not found: ${relativePath}`);
18068
18316
  }
18069
18317
  return asToolText(page);
18070
- }
18318
+ })
18071
18319
  );
18072
18320
  server.registerTool(
18073
18321
  "list_sources",
@@ -18077,10 +18325,10 @@ async function createMcpServer(rootDir) {
18077
18325
  limit: z8.number().int().min(1).max(100).optional().describe("Maximum number of manifests to return")
18078
18326
  }
18079
18327
  },
18080
- async ({ limit }) => {
18328
+ safeHandler(async ({ limit }) => {
18081
18329
  const manifests = await listManifests(rootDir);
18082
18330
  return asToolText(limit ? manifests.slice(0, limit) : manifests);
18083
- }
18331
+ })
18084
18332
  );
18085
18333
  server.registerTool(
18086
18334
  "query_graph",
@@ -18092,22 +18340,22 @@ async function createMcpServer(rootDir) {
18092
18340
  budget: z8.number().int().min(3).max(50).optional().describe("Maximum nodes to summarize")
18093
18341
  }
18094
18342
  },
18095
- async ({ question, traversal, budget }) => {
18343
+ safeHandler(async ({ question, traversal, budget }) => {
18096
18344
  const result = await queryGraphVault(rootDir, question, {
18097
18345
  traversal,
18098
18346
  budget
18099
18347
  });
18100
18348
  return asToolText(result);
18101
- }
18349
+ })
18102
18350
  );
18103
18351
  server.registerTool(
18104
18352
  "graph_report",
18105
18353
  {
18106
18354
  description: "Return the machine-readable graph report and trust artifact."
18107
18355
  },
18108
- async () => {
18356
+ safeHandler(async () => {
18109
18357
  return asToolText(await readGraphReport(rootDir) ?? { error: "Graph report not found. Run `swarmvault compile` first." });
18110
- }
18358
+ })
18111
18359
  );
18112
18360
  server.registerTool(
18113
18361
  "get_node",
@@ -18117,9 +18365,9 @@ async function createMcpServer(rootDir) {
18117
18365
  target: z8.string().min(1).describe("Node or page label/id")
18118
18366
  }
18119
18367
  },
18120
- async ({ target }) => {
18368
+ safeHandler(async ({ target }) => {
18121
18369
  return asToolText(await explainGraphVault(rootDir, target));
18122
- }
18370
+ })
18123
18371
  );
18124
18372
  server.registerTool(
18125
18373
  "get_hyperedges",
@@ -18130,9 +18378,9 @@ async function createMcpServer(rootDir) {
18130
18378
  limit: z8.number().int().min(1).max(50).optional().describe("Maximum hyperedges to return")
18131
18379
  }
18132
18380
  },
18133
- async ({ target, limit }) => {
18381
+ safeHandler(async ({ target, limit }) => {
18134
18382
  return asToolText(await listGraphHyperedges(rootDir, target, limit ?? 25));
18135
- }
18383
+ })
18136
18384
  );
18137
18385
  server.registerTool(
18138
18386
  "get_neighbors",
@@ -18142,10 +18390,10 @@ async function createMcpServer(rootDir) {
18142
18390
  target: z8.string().min(1).describe("Node or page label/id")
18143
18391
  }
18144
18392
  },
18145
- async ({ target }) => {
18393
+ safeHandler(async ({ target }) => {
18146
18394
  const explanation = await explainGraphVault(rootDir, target);
18147
18395
  return asToolText(explanation.neighbors);
18148
- }
18396
+ })
18149
18397
  );
18150
18398
  server.registerTool(
18151
18399
  "shortest_path",
@@ -18156,9 +18404,9 @@ async function createMcpServer(rootDir) {
18156
18404
  to: z8.string().min(1).describe("End node/page label or id")
18157
18405
  }
18158
18406
  },
18159
- async ({ from, to }) => {
18407
+ safeHandler(async ({ from, to }) => {
18160
18408
  return asToolText(await pathGraphVault(rootDir, from, to));
18161
- }
18409
+ })
18162
18410
  );
18163
18411
  server.registerTool(
18164
18412
  "god_nodes",
@@ -18168,9 +18416,9 @@ async function createMcpServer(rootDir) {
18168
18416
  limit: z8.number().int().min(1).max(25).optional().describe("Maximum nodes to return")
18169
18417
  }
18170
18418
  },
18171
- async ({ limit }) => {
18419
+ safeHandler(async ({ limit }) => {
18172
18420
  return asToolText(await listGodNodes(rootDir, limit ?? 10));
18173
- }
18421
+ })
18174
18422
  );
18175
18423
  server.registerTool(
18176
18424
  "query_vault",
@@ -18182,14 +18430,14 @@ async function createMcpServer(rootDir) {
18182
18430
  format: z8.enum(["markdown", "report", "slides", "chart", "image"]).optional().describe("Output format")
18183
18431
  }
18184
18432
  },
18185
- async ({ question, save, format }) => {
18433
+ safeHandler(async ({ question, save, format }) => {
18186
18434
  const result = await queryVault(rootDir, {
18187
18435
  question,
18188
18436
  save: save ?? true,
18189
18437
  format
18190
18438
  });
18191
18439
  return asToolText(result);
18192
- }
18440
+ })
18193
18441
  );
18194
18442
  server.registerTool(
18195
18443
  "ingest_input",
@@ -18199,10 +18447,10 @@ async function createMcpServer(rootDir) {
18199
18447
  input: z8.string().min(1).describe("Local path or URL to ingest")
18200
18448
  }
18201
18449
  },
18202
- async ({ input }) => {
18450
+ safeHandler(async ({ input }) => {
18203
18451
  const result = await ingestInputDetailed(rootDir, input);
18204
18452
  return asToolText(result);
18205
- }
18453
+ })
18206
18454
  );
18207
18455
  server.registerTool(
18208
18456
  "compile_vault",
@@ -18212,20 +18460,20 @@ async function createMcpServer(rootDir) {
18212
18460
  approve: z8.boolean().optional().describe("Stage a review bundle without applying active page changes")
18213
18461
  }
18214
18462
  },
18215
- async ({ approve }) => {
18463
+ safeHandler(async ({ approve }) => {
18216
18464
  const result = await compileVault(rootDir, { approve: approve ?? false });
18217
18465
  return asToolText(result);
18218
- }
18466
+ })
18219
18467
  );
18220
18468
  server.registerTool(
18221
18469
  "lint_vault",
18222
18470
  {
18223
18471
  description: "Run anti-drift and vault health checks."
18224
18472
  },
18225
- async () => {
18473
+ safeHandler(async () => {
18226
18474
  const findings = await lintVault(rootDir);
18227
18475
  return asToolText(findings);
18228
- }
18476
+ })
18229
18477
  );
18230
18478
  server.registerResource(
18231
18479
  "swarmvault-config",
@@ -18396,6 +18644,17 @@ function asToolError(message) {
18396
18644
  ]
18397
18645
  };
18398
18646
  }
18647
+ function safeHandler(handler) {
18648
+ return async (args) => {
18649
+ try {
18650
+ return await handler(args);
18651
+ } catch (error) {
18652
+ const message = error instanceof Error ? error.message : String(error);
18653
+ console.error(`[swarmvault-mcp] tool handler failed: ${message}`);
18654
+ return asToolError(message);
18655
+ }
18656
+ };
18657
+ }
18399
18658
  function asTextResource(uri, text) {
18400
18659
  return {
18401
18660
  contents: [
@@ -18643,13 +18902,22 @@ async function serveSchedules(rootDir, pollMs = 3e4) {
18643
18902
  }
18644
18903
  running = true;
18645
18904
  try {
18646
- const schedules = await listSchedules(rootDir);
18905
+ let schedules = [];
18906
+ try {
18907
+ schedules = await listSchedules(rootDir);
18908
+ } catch (error) {
18909
+ console.error(`[swarmvault-schedule] failed to list schedules: ${error instanceof Error ? error.message : String(error)}`);
18910
+ }
18647
18911
  const due = schedules.filter((item) => item.enabled).filter((item) => !item.nextRunAt || Date.parse(item.nextRunAt) <= Date.now()).sort((left, right) => (left.nextRunAt ?? "").localeCompare(right.nextRunAt ?? ""));
18648
18912
  for (const schedule of due) {
18649
18913
  if (closed) {
18650
18914
  break;
18651
18915
  }
18652
- await runSchedule(rootDir, schedule.jobId);
18916
+ try {
18917
+ await runSchedule(rootDir, schedule.jobId);
18918
+ } catch (error) {
18919
+ console.error(`[swarmvault-schedule] job ${schedule.jobId} crashed: ${error instanceof Error ? error.message : String(error)}`);
18920
+ }
18653
18921
  }
18654
18922
  } finally {
18655
18923
  running = false;
@@ -20592,10 +20860,6 @@ var MAX_BACKOFF_MS = 3e4;
20592
20860
  var BACKOFF_THRESHOLD = 3;
20593
20861
  var CRITICAL_THRESHOLD = 10;
20594
20862
  var REPO_WATCH_IGNORES = /* @__PURE__ */ new Set([".git", ".venv"]);
20595
- function withinRoot3(rootPath, targetPath) {
20596
- const relative = path27.relative(rootPath, targetPath);
20597
- return relative === "" || !relative.startsWith("..") && !path27.isAbsolute(relative);
20598
- }
20599
20863
  function hasIgnoredRepoSegment(baseDir, targetPath) {
20600
20864
  const relativePath = path27.relative(baseDir, targetPath);
20601
20865
  if (!relativePath || relativePath.startsWith("..")) {
@@ -20765,11 +21029,11 @@ async function watchVault(rootDir, options = {}) {
20765
21029
  interval: 100,
20766
21030
  ignored: (targetPath) => {
20767
21031
  const absolutePath = path27.resolve(targetPath);
20768
- const primaryTarget = watchTargets.filter((watchTarget) => withinRoot3(watchTarget, absolutePath)).sort((left, right) => right.length - left.length)[0] ?? null;
21032
+ const primaryTarget = watchTargets.filter((watchTarget) => isPathWithin(watchTarget, absolutePath)).sort((left, right) => right.length - left.length)[0] ?? null;
20769
21033
  if (!primaryTarget) {
20770
21034
  return false;
20771
21035
  }
20772
- if (primaryTarget !== inboxWatchRoot && ignoredRoots.some((ignoreRoot) => withinRoot3(ignoreRoot, absolutePath))) {
21036
+ if (primaryTarget !== inboxWatchRoot && ignoredRoots.some((ignoreRoot) => isPathWithin(ignoreRoot, absolutePath))) {
20773
21037
  return true;
20774
21038
  }
20775
21039
  return hasIgnoredRepoSegment(primaryTarget, absolutePath);
@@ -20953,7 +21217,7 @@ async function watchVault(rootDir, options = {}) {
20953
21217
  }
20954
21218
  };
20955
21219
  const reasonForPath = (targetPath) => {
20956
- const baseDir = watchTargets.filter((watchTarget) => withinRoot3(watchTarget, path27.resolve(targetPath))).sort((left, right) => right.length - left.length)[0] ?? paths.inboxDir;
21220
+ const baseDir = watchTargets.filter((watchTarget) => isPathWithin(watchTarget, path27.resolve(targetPath))).sort((left, right) => right.length - left.length)[0] ?? paths.inboxDir;
20957
21221
  return path27.relative(baseDir, targetPath) || ".";
20958
21222
  };
20959
21223
  watcher.on("add", (filePath) => schedule(`add:${reasonForPath(filePath)}`)).on("change", (filePath) => schedule(`change:${reasonForPath(filePath)}`)).on("unlink", (filePath) => schedule(`unlink:${reasonForPath(filePath)}`)).on("addDir", (dirPath) => schedule(`addDir:${reasonForPath(dirPath)}`)).on("unlinkDir", (dirPath) => schedule(`unlinkDir:${reasonForPath(dirPath)}`)).on("error", (caught) => schedule(`error:${caught instanceof Error ? caught.message : String(caught)}`));
@@ -21115,14 +21379,9 @@ async function startGraphServer(rootDir, port, options = {}) {
21115
21379
  response.end(JSON.stringify({ error: "Missing explain target." }));
21116
21380
  return;
21117
21381
  }
21118
- try {
21119
- const result = await explainGraphVault(rootDir, target2);
21120
- response.writeHead(200, { "content-type": "application/json" });
21121
- response.end(JSON.stringify(result));
21122
- } catch (error) {
21123
- response.writeHead(404, { "content-type": "application/json" });
21124
- response.end(JSON.stringify({ error: error instanceof Error ? error.message : `Could not resolve graph target: ${target2}` }));
21125
- }
21382
+ const result = await explainGraphVault(rootDir, target2);
21383
+ response.writeHead(200, { "content-type": "application/json" });
21384
+ response.end(JSON.stringify(result));
21126
21385
  return;
21127
21386
  }
21128
21387
  if (url.pathname === "/api/search") {
@@ -21261,7 +21520,8 @@ async function startGraphServer(rootDir, port, options = {}) {
21261
21520
  const message = error instanceof Error ? error.message : String(error);
21262
21521
  console.error(`[viewer] ${request.method ?? "GET"} ${url.pathname} failed: ${message}`);
21263
21522
  if (!response.headersSent) {
21264
- response.writeHead(500, { "content-type": "application/json" });
21523
+ const status = /not found|could not resolve|cannot resolve/i.test(message) ? 404 : 500;
21524
+ response.writeHead(status, { "content-type": "application/json" });
21265
21525
  response.end(JSON.stringify({ error: message }));
21266
21526
  } else {
21267
21527
  response.end();