@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 +28 -0
- package/dist/cli/index.js +39 -6
- 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 +38 -5
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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",
|