@jefuriiij/synthra 0.1.19 → 0.1.20

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,34 @@ For older versions, see [GitHub Releases](https://github.com/jefuriiij/synthra/r
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.1.20] — 2026-06-06
11
+
12
+ ### Fixed
13
+
14
+ - **Gate (Moat) no longer blocks Grep/Glob queries the graph cannot answer with a symbol.**
15
+ Previously, the PreToolUse gate blocked whenever retrieval confidence was `medium` or `high`,
16
+ but confidence is driven by keyword and path hits too — not only by symbol matches. This meant
17
+ literal/attribute/CSS-selector patterns (`data-tour=`, `class=`, `: 100%`, `.filter-bar`,
18
+ `<div>`) and path-only Globs were blocked and redirected to `graph_read`, which has no symbol
19
+ slice to return for those queries, so Claude fell back to Grep or a whole-file Read anyway —
20
+ a wasted round-trip. Found across multiple dogfood sessions including well-indexed Svelte
21
+ repos. Two new guards close the gap:
22
+ - **Query-shape pre-filter** — Grep patterns that target markup, CSS, attributes, or string
23
+ literals are allowed through up front, before the retrieval step runs.
24
+ - **Symbol-hit requirement** — the gate now only blocks when retrieval matched a symbol whose
25
+ name the query mentions exactly. `RetrievalResult` gained a `symbolMatched` flag; the scorer
26
+ exposes `exactSym`.
27
+
28
+ Net effect: genuine symbol lookups still block (verified: `fetchWith429Retry`,
29
+ `MAX_ROWS_PER_TABLE`, `verifyPin`, `SOCKET_AUTH_SECRET`, `seedCredentials`); queries that
30
+ could never have been answered by the graph now allow through without the wasted redirect.
31
+ No API, protocol, or policy-block change — purely server-side gate behavior.
32
+
33
+ - **Gate and rank test coverage added** (`tests/gate.test.ts`, `tests/rank.test.ts`).
34
+ Chips at the v0.2 backlog item to fill vitest tests beyond `it.todo` placeholders.
35
+
36
+ ---
37
+
10
38
  ## [0.1.19] — 2026-06-01
11
39
 
12
40
  ### Changed
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.1.19",
21
+ version: "0.1.20",
22
22
  publishConfig: {
23
23
  access: "public"
24
24
  },
@@ -3623,10 +3623,12 @@ function scoreFiles(inputs) {
3623
3623
  }
3624
3624
  const symbols = symbolsByFile.get(file.path) ?? [];
3625
3625
  let symHits = 0;
3626
+ let exactSym = 0;
3626
3627
  for (const sym of symbols) {
3627
3628
  const name = sym.name.toLowerCase();
3628
3629
  if (qTokens.has(name)) {
3629
3630
  symHits += 3;
3631
+ exactSym += 1;
3630
3632
  } else {
3631
3633
  for (const t of qTokens) {
3632
3634
  if (name.includes(t) || t.includes(name)) {
@@ -3651,7 +3653,7 @@ function scoreFiles(inputs) {
3651
3653
  score2 += 5;
3652
3654
  reasons.push("seed");
3653
3655
  }
3654
- scored.push({ file, score: score2, reasons });
3656
+ scored.push({ file, score: score2, reasons, symHits, exactSym });
3655
3657
  }
3656
3658
  const positivePaths = new Set(scored.filter((s) => s.score > 0).map((s) => s.file.path));
3657
3659
  if (positivePaths.size > 0) {
@@ -3686,7 +3688,8 @@ async function retrieve(graph, query, options = {}) {
3686
3688
  return {
3687
3689
  files: [],
3688
3690
  confidence: "low",
3689
- reason: qTokens.length === 0 ? "empty query" : "empty graph"
3691
+ reason: qTokens.length === 0 ? "empty query" : "empty graph",
3692
+ symbolMatched: false
3690
3693
  };
3691
3694
  }
3692
3695
  const rankInputs = {
@@ -3702,10 +3705,13 @@ async function retrieve(graph, query, options = {}) {
3702
3705
  return {
3703
3706
  files: [],
3704
3707
  confidence: "low",
3705
- reason: `no matches for ${JSON.stringify(qTokens)}`
3708
+ reason: `no matches for ${JSON.stringify(qTokens)}`,
3709
+ symbolMatched: false
3706
3710
  };
3707
3711
  }
3708
- const top = positive.slice(0, topK).map((s) => s.file);
3712
+ const topScored = positive.slice(0, topK);
3713
+ const top = topScored.map((s) => s.file);
3714
+ const symbolMatched = topScored.some((s) => s.exactSym > 0);
3709
3715
  const topScore = positive[0]?.score ?? 0;
3710
3716
  const secondScore = positive[1]?.score ?? 0;
3711
3717
  let confidence;
@@ -3717,7 +3723,8 @@ async function retrieve(graph, query, options = {}) {
3717
3723
  return {
3718
3724
  files: top,
3719
3725
  confidence,
3720
- reason: `top: ${reasons}`
3726
+ reason: `top: ${reasons}`,
3727
+ symbolMatched
3721
3728
  };
3722
3729
  }
3723
3730
 
@@ -4552,6 +4559,16 @@ function extractQuery(toolName, input) {
4552
4559
  }
4553
4560
  return null;
4554
4561
  }
4562
+ function looksLikeNonSymbolQuery(pattern) {
4563
+ if (/<\/?[a-zA-Z]/.test(pattern)) return true;
4564
+ if (/[a-zA-Z][\w-]*-[\w-]*\s*=/.test(pattern)) return true;
4565
+ if (/\{/.test(pattern)) return true;
4566
+ if (/\\\.[a-zA-Z]/.test(pattern)) return true;
4567
+ if (/:\s*\d/.test(pattern) || /\d(?:px|rem|em|vh|vw)\b/.test(pattern) || /\d%/.test(pattern)) {
4568
+ return true;
4569
+ }
4570
+ return false;
4571
+ }
4555
4572
  function recentlyTouchedMatchesQuery(recentPaths, queryTokens) {
4556
4573
  const matches = [];
4557
4574
  for (const path of recentPaths) {
@@ -4593,6 +4610,14 @@ async function handleGate(req, ctx) {
4593
4610
  await logDecision(ctx, req.tool_name, null, res2.decision, res2.reason);
4594
4611
  return res2;
4595
4612
  }
4613
+ if (req.tool_name === "Grep" && looksLikeNonSymbolQuery(query)) {
4614
+ const res2 = {
4615
+ decision: "allow",
4616
+ reason: `"${query}" targets markup/CSS/attributes, not code symbols \u2014 letting Grep through (the graph indexes symbols).`
4617
+ };
4618
+ await logDecision(ctx, req.tool_name, query, res2.decision, res2.reason);
4619
+ return res2;
4620
+ }
4596
4621
  const retrieval = await retrieve(ctx.graph, query);
4597
4622
  if (retrieval.confidence === "low") {
4598
4623
  const res2 = {
@@ -4613,6 +4638,14 @@ async function handleGate(req, ctx) {
4613
4638
  await logDecision(ctx, req.tool_name, query, res2.decision, res2.reason);
4614
4639
  return res2;
4615
4640
  }
4641
+ if (!retrieval.symbolMatched) {
4642
+ const res2 = {
4643
+ decision: "allow",
4644
+ reason: `confidence=${retrieval.confidence} but only keyword/path matched (no symbol the query names) \u2014 graph_read can't slice it, letting ${req.tool_name} through.`
4645
+ };
4646
+ await logDecision(ctx, req.tool_name, query, res2.decision, res2.reason);
4647
+ return res2;
4648
+ }
4616
4649
  const top = retrieval.files.slice(0, 3).map((f) => f.path).join(", ");
4617
4650
  const res = {
4618
4651
  decision: "block",