@jefuriiij/synthra 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,40 @@ For older versions, see [GitHub Releases](https://github.com/jefuriiij/synthra/r
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.13.1] — 2026-06-24
11
+
12
+ ### Fixed
13
+
14
+ - **Minified/bundle files are no longer indexed.** Committed vendored plugin JS
15
+ (`*.min.js`, `*.bundle.js`, `*.min.css`, …) has no readable symbols, so indexing
16
+ it only polluted retrieval and caused **useless Moat blocks** on markup-heavy
17
+ projects — a Grep for CSS classes like `nav|menu|toggle` would spuriously match a
18
+ symbol *inside* the minified library and get blocked, only for `graph_continue` to
19
+ then find nothing. The scanner now skips these files (cleaner retrieval, smaller
20
+ graph, no behavior change for real source).
21
+
22
+ ---
23
+
24
+ ## [0.13.0] — 2026-06-24
25
+
26
+ ### Added
27
+
28
+ - **The resume digest now lists the symbols that changed since your last session.**
29
+ The SessionStart "Since you were last here" primer showed *files* touched; it now
30
+ leads its supporting context with the actual **symbols/signatures** that changed —
31
+ e.g. `src/auth.ts::login (function) — function login(creds: Creds): Promise<...>`.
32
+ Computed from a git diff against the previous session's HEAD (committed **and**
33
+ uncommitted changes), overlapped with the current graph. Best-effort: silently
34
+ omitted in non-git projects.
35
+ - **`call_path(from, to)` — trace control flow.** Returns the shortest chain of
36
+ calls from one symbol to another (`handler → service → repo`), so you can see how
37
+ one symbol reaches another. The forward complement to `blast_radius` (callers).
38
+ Each of `from`/`to` is a `file::symbol` target or a bare symbol name when unique.
39
+
40
+ Both reuse the existing call graph + git — no graph schema change, no new dependencies.
41
+
42
+ ---
43
+
10
44
  ## [0.12.0] — 2026-06-24
11
45
 
12
46
  ### Added
package/dist/cli/index.js CHANGED
@@ -18,7 +18,7 @@ var init_package = __esm({
18
18
  "package.json"() {
19
19
  package_default = {
20
20
  name: "@jefuriiij/synthra",
21
- version: "0.12.0",
21
+ version: "0.13.1",
22
22
  publishConfig: {
23
23
  access: "public"
24
24
  },
@@ -2890,7 +2890,17 @@ var DEFAULT_IGNORE = [
2890
2890
  ".mypy_cache/",
2891
2891
  ".ruff_cache/",
2892
2892
  // .NET
2893
- "obj/"
2893
+ "obj/",
2894
+ // Generated / minified bundles — no readable symbols, so indexing them only
2895
+ // pollutes retrieval (a markup query like `nav|menu|toggle` spuriously matches
2896
+ // a symbol inside vendored plugin JS → a useless Moat block) and bloats the
2897
+ // graph. Committed bootstrap/swiper-style plugin JS is the common offender.
2898
+ "*.min.js",
2899
+ "*.min.cjs",
2900
+ "*.min.mjs",
2901
+ "*.min.css",
2902
+ "*.bundle.js",
2903
+ "*-min.js"
2894
2904
  ];
2895
2905
  var BINARY_EXTS = /* @__PURE__ */ new Set([
2896
2906
  ".png",
@@ -4244,6 +4254,19 @@ var TOOLS = [
4244
4254
  limit: { type: "number", description: "Cap on returned names. Default 30." }
4245
4255
  }
4246
4256
  }
4257
+ },
4258
+ {
4259
+ name: "call_path",
4260
+ description: "Trace how one symbol reaches another through the call graph \u2014 the shortest chain of calls from 'from' to 'to'. Use to understand control flow ('how does this handler end up hitting the DB layer?'). Each of 'from'/'to' is a 'file::symbol' target or a bare symbol name when unique.",
4261
+ inputSchema: {
4262
+ type: "object",
4263
+ properties: {
4264
+ from: { type: "string", description: "Starting symbol ('file::symbol' or unique name)." },
4265
+ to: { type: "string", description: "Target symbol ('file::symbol' or unique name)." },
4266
+ depth: { type: "number", description: "Max call hops to search. Default 6." }
4267
+ },
4268
+ required: ["from", "to"]
4269
+ }
4247
4270
  }
4248
4271
  ];
4249
4272
  async function callTool(name, args, ctx) {
@@ -4270,6 +4293,8 @@ async function callTool(name, args, ctx) {
4270
4293
  return findSymbol(args, ctx);
4271
4294
  case "duplicate_symbols":
4272
4295
  return duplicateSymbols(args, ctx);
4296
+ case "call_path":
4297
+ return callPath(args, ctx);
4273
4298
  default:
4274
4299
  return errorContent(`Unknown tool: ${name}`);
4275
4300
  }
@@ -4426,6 +4451,97 @@ function testsCoveringLine(graph, filePaths) {
4426
4451
  const omitted = tests.length - shown.length;
4427
4452
  return `Tests covering the impact: ${shown.join(" \xB7 ")}${omitted > 0 ? ` \u2026+${omitted} more` : ""}`;
4428
4453
  }
4454
+ function resolveSymbolArg(ctx, arg) {
4455
+ const a = arg.trim();
4456
+ if (a.includes("::")) {
4457
+ const [rawFile, rawSym] = a.split("::", 2);
4458
+ const resolved = resolveFileTarget(ctx.graph, (rawFile ?? "").trim());
4459
+ if (!("node" in resolved)) return null;
4460
+ const name = (rawSym ?? "").trim();
4461
+ return ctx.graph.nodes.find(
4462
+ (n) => n.kind === "symbol" && n.file === resolved.node.path && n.name === name
4463
+ ) ?? null;
4464
+ }
4465
+ const matches = ctx.graph.nodes.filter(
4466
+ (n) => n.kind === "symbol" && n.name === a
4467
+ );
4468
+ return matches.length === 1 ? matches[0] : null;
4469
+ }
4470
+ function callPath(args, ctx) {
4471
+ const fromArg = typeof args?.from === "string" ? args.from : "";
4472
+ const toArg = typeof args?.to === "string" ? args.to : "";
4473
+ const maxDepth = typeof args?.depth === "number" && args.depth > 0 ? Math.floor(args.depth) : 6;
4474
+ if (!fromArg.trim() || !toArg.trim()) {
4475
+ return errorContent("call_path: 'from' and 'to' (strings) are required");
4476
+ }
4477
+ const from = resolveSymbolArg(ctx, fromArg);
4478
+ if (!from) {
4479
+ return errorContent(
4480
+ `call_path: could not resolve 'from': ${fromArg} (use file::symbol if the name is ambiguous)`
4481
+ );
4482
+ }
4483
+ const to = resolveSymbolArg(ctx, toArg);
4484
+ if (!to) {
4485
+ return errorContent(
4486
+ `call_path: could not resolve 'to': ${toArg} (use file::symbol if the name is ambiguous)`
4487
+ );
4488
+ }
4489
+ if (from.id === to.id) {
4490
+ return textContent(`# call_path
4491
+
4492
+ \`${from.name}\` and \`${to.name}\` are the same symbol.`);
4493
+ }
4494
+ const calleesBy = /* @__PURE__ */ new Map();
4495
+ for (const e of ctx.graph.edges) {
4496
+ if (e.kind !== "calls" || e.from === e.to) continue;
4497
+ (calleesBy.get(e.from) ?? calleesBy.set(e.from, []).get(e.from)).push(e.to);
4498
+ }
4499
+ const symById = /* @__PURE__ */ new Map();
4500
+ for (const n of ctx.graph.nodes) if (n.kind === "symbol") symById.set(n.id, n);
4501
+ const prevOf = /* @__PURE__ */ new Map();
4502
+ const visited = /* @__PURE__ */ new Set([from.id]);
4503
+ let frontier = [from.id];
4504
+ let found = false;
4505
+ for (let d = 0; d < maxDepth && !found && frontier.length > 0; d++) {
4506
+ const next = [];
4507
+ for (const cur2 of frontier) {
4508
+ for (const nb of calleesBy.get(cur2) ?? []) {
4509
+ if (visited.has(nb)) continue;
4510
+ visited.add(nb);
4511
+ prevOf.set(nb, cur2);
4512
+ if (nb === to.id) {
4513
+ found = true;
4514
+ break;
4515
+ }
4516
+ next.push(nb);
4517
+ }
4518
+ if (found) break;
4519
+ }
4520
+ frontier = next;
4521
+ }
4522
+ if (!found) {
4523
+ return textContent(
4524
+ `# call_path: ${from.name} \u2192 ${to.name}
4525
+
4526
+ _(no call path found within depth ${maxDepth})_`
4527
+ );
4528
+ }
4529
+ const chain = [];
4530
+ let cur = to.id;
4531
+ while (cur !== void 0) {
4532
+ chain.unshift(cur);
4533
+ if (cur === from.id) break;
4534
+ cur = prevOf.get(cur);
4535
+ }
4536
+ const syms = chain.map((id) => symById.get(id)).filter((s) => !!s);
4537
+ const hops = syms.length - 1;
4538
+ const rendered = syms.map((s) => `\`${s.name}\` (${s.file}:${s.start_line})`).join("\n \u2192 ");
4539
+ return textContent(
4540
+ `# call_path: ${from.name} \u2192 ${to.name} (${hops} hop${hops === 1 ? "" : "s"})
4541
+
4542
+ ${rendered}`
4543
+ );
4544
+ }
4429
4545
  var LIKELY_ENTRY_PATTERNS = [
4430
4546
  /(?:^|\/)main\.[a-z0-9_]+$/i,
4431
4547
  /(?:^|\/)index\.[a-z0-9_]+$/i,
@@ -4972,11 +5088,51 @@ async function getCommitsSince(projectRoot, sinceIso) {
4972
5088
  return [];
4973
5089
  }
4974
5090
  }
5091
+ async function getHeadSha(projectRoot) {
5092
+ try {
5093
+ const { stdout } = await execFileAsync3("git", ["rev-parse", "HEAD"], { cwd: projectRoot });
5094
+ return stdout.trim();
5095
+ } catch {
5096
+ return "";
5097
+ }
5098
+ }
5099
+ function parseDiffHunks(stdout) {
5100
+ const out = /* @__PURE__ */ new Map();
5101
+ let current = null;
5102
+ for (const line of stdout.split("\n")) {
5103
+ if (line.startsWith("+++ ")) {
5104
+ const p = line.slice(4).trim();
5105
+ current = p === "/dev/null" ? null : p.replace(/^b\//, "");
5106
+ } else if (current && line.startsWith("@@")) {
5107
+ const m = /\+(\d+)(?:,(\d+))?/.exec(line);
5108
+ if (!m) continue;
5109
+ const start = Number(m[1]);
5110
+ const count = m[2] === void 0 ? 1 : Number(m[2]);
5111
+ const end = count === 0 ? start : start + count - 1;
5112
+ const list = out.get(current) ?? [];
5113
+ list.push([start, end]);
5114
+ out.set(current, list);
5115
+ }
5116
+ }
5117
+ return out;
5118
+ }
5119
+ async function getChangedLineRanges(projectRoot, sinceRef) {
5120
+ if (!sinceRef) return /* @__PURE__ */ new Map();
5121
+ try {
5122
+ const { stdout } = await execFileAsync3("git", ["diff", "-U0", "--no-color", sinceRef, "--"], {
5123
+ cwd: projectRoot,
5124
+ maxBuffer: 16 * 1024 * 1024
5125
+ });
5126
+ return parseDiffHunks(stdout);
5127
+ } catch {
5128
+ return /* @__PURE__ */ new Map();
5129
+ }
5130
+ }
4975
5131
 
4976
5132
  // src/memory/session.ts
4977
5133
  import { mkdir as mkdir11, readFile as readFile17, writeFile as writeFile10 } from "fs/promises";
4978
5134
  import { dirname as dirname13 } from "path";
4979
- var SESSION_SCHEMA_VERSION = 1;
5135
+ var SESSION_SCHEMA_VERSION = 2;
4980
5136
  async function readSession(path) {
4981
5137
  try {
4982
5138
  const raw = await readFile17(path, "utf8");
@@ -5005,6 +5161,7 @@ async function captureSnapshot(ctx, branchOverride) {
5005
5161
  for (const p of ctx.activity.recentFilePaths(TOUCHED_WINDOW_MS)) touched.add(p);
5006
5162
  const prev = await readSession(ctx.paths.sessionState);
5007
5163
  const recentCommits = await getCommitsSince(ctx.paths.projectRoot, prev?.endedAt ?? "");
5164
+ const headSha = await getHeadSha(ctx.paths.projectRoot);
5008
5165
  const snapshot = {
5009
5166
  schema_version: SESSION_SCHEMA_VERSION,
5010
5167
  endedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -5015,7 +5172,8 @@ async function captureSnapshot(ctx, branchOverride) {
5015
5172
  tasks: tasks.entries.map((e) => e.content),
5016
5173
  decisions: decisions.entries.map((e) => e.content),
5017
5174
  next: next.entries.map((e) => e.content)
5018
- }
5175
+ },
5176
+ headSha
5019
5177
  };
5020
5178
  await writeSession(ctx.paths.sessionState, snapshot);
5021
5179
  }
@@ -5463,6 +5621,34 @@ var RESUME_PRIMER_MAX_CHARS = 2720;
5463
5621
  var MAX_FILES = 15;
5464
5622
  var MAX_COMMITS2 = 5;
5465
5623
  var MAX_BULLETS2 = 3;
5624
+ var MAX_CHANGED_SYMBOLS = 20;
5625
+ var CHANGED_SIG_MAX = 100;
5626
+ function changedSymbols(ranges, graph) {
5627
+ if (ranges.size === 0) return [];
5628
+ const hits = [];
5629
+ for (const n of graph.nodes) {
5630
+ if (n.kind !== "symbol") continue;
5631
+ const rs = ranges.get(n.file);
5632
+ if (!rs) continue;
5633
+ if (rs.some(([a, b]) => a <= n.end_line && b >= n.start_line)) hits.push(n);
5634
+ }
5635
+ hits.sort((a, b) => a.file === b.file ? a.start_line - b.start_line : a.file < b.file ? -1 : 1);
5636
+ return hits;
5637
+ }
5638
+ function changedSymbolsSection(ranges, graph) {
5639
+ const hits = changedSymbols(ranges, graph);
5640
+ if (hits.length === 0) return [];
5641
+ const shown = hits.slice(0, MAX_CHANGED_SYMBOLS);
5642
+ const lines = ["", "### Changed symbols (since last session)"];
5643
+ for (const s of shown) {
5644
+ lines.push(
5645
+ `- \`${s.file}::${s.name}\` (${s.symbol_kind}) \u2014 ${s.signature.trim().slice(0, CHANGED_SIG_MAX)}`
5646
+ );
5647
+ }
5648
+ const more = hits.length - shown.length;
5649
+ if (more > 0) lines.push(`_(+${more} more)_`);
5650
+ return lines;
5651
+ }
5466
5652
  function legacyPrimer(ctx) {
5467
5653
  const g = ctx.graph;
5468
5654
  return `Synthra context loaded for ${g.root}.
@@ -5473,7 +5659,7 @@ function hasContent(snap) {
5473
5659
  snap.recentCommits.length || snap.filesTouched.length || snap.summary.tasks.length || snap.summary.next.length || snap.summary.decisions.length
5474
5660
  );
5475
5661
  }
5476
- function buildResumeDigest(snap, branchNow) {
5662
+ function buildResumeDigest(snap, branchNow, changedSymbolLines = []) {
5477
5663
  const plural = (n) => n === 1 ? "" : "s";
5478
5664
  const head = `## Since you were last here \u2014 ${snap.branch} (${snap.recentCommits.length} commit${plural(snap.recentCommits.length)}, ${snap.filesTouched.length} file${plural(snap.filesTouched.length)} touched)`;
5479
5665
  const essential = [head];
@@ -5494,7 +5680,7 @@ function buildResumeDigest(snap, branchNow) {
5494
5680
  essential.push("", "### Recent decisions");
5495
5681
  for (const d of snap.summary.decisions.slice(0, MAX_BULLETS2)) essential.push(`- ${d}`);
5496
5682
  }
5497
- const extra = [];
5683
+ const extra = [...changedSymbolLines];
5498
5684
  if (snap.recentCommits.length) {
5499
5685
  extra.push("", "### Recent commits");
5500
5686
  for (const c of snap.recentCommits.slice(0, MAX_COMMITS2)) {
@@ -5521,7 +5707,12 @@ async function handlePrime(ctx, port) {
5521
5707
  return { primer: legacy, port };
5522
5708
  }
5523
5709
  const branchNow = await currentBranch(ctx.paths.projectRoot);
5524
- const digest = buildResumeDigest(snap, branchNow);
5710
+ let changedSymbolLines = [];
5711
+ if (snap.headSha) {
5712
+ const ranges = await getChangedLineRanges(ctx.paths.projectRoot, snap.headSha);
5713
+ changedSymbolLines = changedSymbolsSection(ranges, ctx.graph);
5714
+ }
5715
+ const digest = buildResumeDigest(snap, branchNow, changedSymbolLines);
5525
5716
  return { primer: `${digest}
5526
5717
 
5527
5718
  ---