@jefuriiij/synthra 0.1.18 → 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.
@@ -1558,7 +1558,7 @@ import { basename as basename2 } from "path";
1558
1558
  // src/hooks/claude-md.ts
1559
1559
  import { readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
1560
1560
  import { basename, dirname as dirname4 } from "path";
1561
- var POLICY_VERSION = 3;
1561
+ var POLICY_VERSION = 4;
1562
1562
  var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
1563
1563
  var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
1564
1564
  var ANY_BLOCK_RE = /<!--\s*synthra-policy\s+v\d+\s+BEGIN\s*-->[\s\S]*?<!--\s*synthra-policy\s+v\d+\s+END\s*-->\s*/g;
@@ -1627,6 +1627,17 @@ function policyBlock() {
1627
1627
  "- If `graph_continue`'s `Files` list contains a `::` entry, pass it",
1628
1628
  " verbatim to `graph_read`.",
1629
1629
  "",
1630
+ "### Editing a file",
1631
+ "",
1632
+ "Claude Code's `Edit` tool (and `Write` when overwriting) only accepts a",
1633
+ "file that was opened with the **`Read` tool** \u2014 a `graph_read` slice does",
1634
+ 'not count, and editing such a file fails with *"File has not been read',
1635
+ 'yet."* So before editing a file you only know through `graph_read`: take',
1636
+ "the line range from its header (e.g. `\u2026::handler (L120-168)`), `Read` that",
1637
+ "file with a matching `offset`/`limit`, then `Edit`. That satisfies the",
1638
+ "gate while keeping the read small \u2014 don't whole-file `Read` unless the",
1639
+ "edit spans most of the file.",
1640
+ "",
1630
1641
  "### Don'ts",
1631
1642
  "",
1632
1643
  "- Don't Grep / Glob before calling `graph_continue` when required \u2014 the",
@@ -1954,10 +1965,12 @@ function scoreFiles(inputs) {
1954
1965
  }
1955
1966
  const symbols = symbolsByFile.get(file.path) ?? [];
1956
1967
  let symHits = 0;
1968
+ let exactSym = 0;
1957
1969
  for (const sym of symbols) {
1958
1970
  const name = sym.name.toLowerCase();
1959
1971
  if (qTokens.has(name)) {
1960
1972
  symHits += 3;
1973
+ exactSym += 1;
1961
1974
  } else {
1962
1975
  for (const t of qTokens) {
1963
1976
  if (name.includes(t) || t.includes(name)) {
@@ -1982,7 +1995,7 @@ function scoreFiles(inputs) {
1982
1995
  score2 += 5;
1983
1996
  reasons.push("seed");
1984
1997
  }
1985
- scored.push({ file, score: score2, reasons });
1998
+ scored.push({ file, score: score2, reasons, symHits, exactSym });
1986
1999
  }
1987
2000
  const positivePaths = new Set(scored.filter((s) => s.score > 0).map((s) => s.file.path));
1988
2001
  if (positivePaths.size > 0) {
@@ -2017,7 +2030,8 @@ async function retrieve(graph, query, options = {}) {
2017
2030
  return {
2018
2031
  files: [],
2019
2032
  confidence: "low",
2020
- reason: qTokens.length === 0 ? "empty query" : "empty graph"
2033
+ reason: qTokens.length === 0 ? "empty query" : "empty graph",
2034
+ symbolMatched: false
2021
2035
  };
2022
2036
  }
2023
2037
  const rankInputs = {
@@ -2033,10 +2047,13 @@ async function retrieve(graph, query, options = {}) {
2033
2047
  return {
2034
2048
  files: [],
2035
2049
  confidence: "low",
2036
- reason: `no matches for ${JSON.stringify(qTokens)}`
2050
+ reason: `no matches for ${JSON.stringify(qTokens)}`,
2051
+ symbolMatched: false
2037
2052
  };
2038
2053
  }
2039
- const top = positive.slice(0, topK).map((s) => s.file);
2054
+ const topScored = positive.slice(0, topK);
2055
+ const top = topScored.map((s) => s.file);
2056
+ const symbolMatched = topScored.some((s) => s.exactSym > 0);
2040
2057
  const topScore = positive[0]?.score ?? 0;
2041
2058
  const secondScore = positive[1]?.score ?? 0;
2042
2059
  let confidence;
@@ -2048,7 +2065,8 @@ async function retrieve(graph, query, options = {}) {
2048
2065
  return {
2049
2066
  files: top,
2050
2067
  confidence,
2051
- reason: `top: ${reasons}`
2068
+ reason: `top: ${reasons}`,
2069
+ symbolMatched
2052
2070
  };
2053
2071
  }
2054
2072
 
@@ -2902,6 +2920,16 @@ function extractQuery(toolName, input) {
2902
2920
  }
2903
2921
  return null;
2904
2922
  }
2923
+ function looksLikeNonSymbolQuery(pattern) {
2924
+ if (/<\/?[a-zA-Z]/.test(pattern)) return true;
2925
+ if (/[a-zA-Z][\w-]*-[\w-]*\s*=/.test(pattern)) return true;
2926
+ if (/\{/.test(pattern)) return true;
2927
+ if (/\\\.[a-zA-Z]/.test(pattern)) return true;
2928
+ if (/:\s*\d/.test(pattern) || /\d(?:px|rem|em|vh|vw)\b/.test(pattern) || /\d%/.test(pattern)) {
2929
+ return true;
2930
+ }
2931
+ return false;
2932
+ }
2905
2933
  function recentlyTouchedMatchesQuery(recentPaths, queryTokens) {
2906
2934
  const matches = [];
2907
2935
  for (const path of recentPaths) {
@@ -2943,6 +2971,14 @@ async function handleGate(req, ctx) {
2943
2971
  await logDecision(ctx, req.tool_name, null, res2.decision, res2.reason);
2944
2972
  return res2;
2945
2973
  }
2974
+ if (req.tool_name === "Grep" && looksLikeNonSymbolQuery(query)) {
2975
+ const res2 = {
2976
+ decision: "allow",
2977
+ reason: `"${query}" targets markup/CSS/attributes, not code symbols \u2014 letting Grep through (the graph indexes symbols).`
2978
+ };
2979
+ await logDecision(ctx, req.tool_name, query, res2.decision, res2.reason);
2980
+ return res2;
2981
+ }
2946
2982
  const retrieval = await retrieve(ctx.graph, query);
2947
2983
  if (retrieval.confidence === "low") {
2948
2984
  const res2 = {
@@ -2963,6 +2999,14 @@ async function handleGate(req, ctx) {
2963
2999
  await logDecision(ctx, req.tool_name, query, res2.decision, res2.reason);
2964
3000
  return res2;
2965
3001
  }
3002
+ if (!retrieval.symbolMatched) {
3003
+ const res2 = {
3004
+ decision: "allow",
3005
+ 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.`
3006
+ };
3007
+ await logDecision(ctx, req.tool_name, query, res2.decision, res2.reason);
3008
+ return res2;
3009
+ }
2966
3010
  const top = retrieval.files.slice(0, 3).map((f) => f.path).join(", ");
2967
3011
  const res = {
2968
3012
  decision: "block",