@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.
- package/CHANGELOG.md +45 -0
- package/dist/cli/index.js +51 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +1 -1
- package/dist/dashboard/index.js.map +1 -1
- package/dist/server/index.js +50 -6
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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",
|