@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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,51 @@ 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
|
+
|
|
38
|
+
## [0.1.19] — 2026-06-01
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- **Policy block v4: targeted Read-before-Edit for graph-discovered files.**
|
|
43
|
+
Claude Code's `Edit` tool requires a file to have been opened with its own
|
|
44
|
+
`Read` tool; a `graph_read` slice does not satisfy that gate. Previously,
|
|
45
|
+
editing a file known only through `graph_read` would fail with *"File has
|
|
46
|
+
not been read yet"* and force a whole-file `Read` — eroding token savings on
|
|
47
|
+
edit-heavy sessions. The v4 policy now instructs: take the line range already
|
|
48
|
+
reported in the `graph_read` header (e.g. `…::handler (L120-168)`), do a
|
|
49
|
+
targeted `Read` with matching `offset`/`limit`, then `Edit`. This satisfies
|
|
50
|
+
the gate while keeping the read small. Existing v3 blocks auto-upgrade on the
|
|
51
|
+
next `syn .` run.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
10
55
|
## [0.1.18] — 2026-06-01
|
|
11
56
|
|
|
12
57
|
### Fixed
|
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
|
},
|
|
@@ -3213,7 +3213,7 @@ import { basename as basename4 } from "path";
|
|
|
3213
3213
|
// src/hooks/claude-md.ts
|
|
3214
3214
|
import { readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
|
|
3215
3215
|
import { basename as basename3, dirname as dirname6 } from "path";
|
|
3216
|
-
var POLICY_VERSION =
|
|
3216
|
+
var POLICY_VERSION = 4;
|
|
3217
3217
|
var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
|
|
3218
3218
|
var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
|
|
3219
3219
|
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;
|
|
@@ -3282,6 +3282,17 @@ function policyBlock() {
|
|
|
3282
3282
|
"- If `graph_continue`'s `Files` list contains a `::` entry, pass it",
|
|
3283
3283
|
" verbatim to `graph_read`.",
|
|
3284
3284
|
"",
|
|
3285
|
+
"### Editing a file",
|
|
3286
|
+
"",
|
|
3287
|
+
"Claude Code's `Edit` tool (and `Write` when overwriting) only accepts a",
|
|
3288
|
+
"file that was opened with the **`Read` tool** \u2014 a `graph_read` slice does",
|
|
3289
|
+
'not count, and editing such a file fails with *"File has not been read',
|
|
3290
|
+
'yet."* So before editing a file you only know through `graph_read`: take',
|
|
3291
|
+
"the line range from its header (e.g. `\u2026::handler (L120-168)`), `Read` that",
|
|
3292
|
+
"file with a matching `offset`/`limit`, then `Edit`. That satisfies the",
|
|
3293
|
+
"gate while keeping the read small \u2014 don't whole-file `Read` unless the",
|
|
3294
|
+
"edit spans most of the file.",
|
|
3295
|
+
"",
|
|
3285
3296
|
"### Don'ts",
|
|
3286
3297
|
"",
|
|
3287
3298
|
"- Don't Grep / Glob before calling `graph_continue` when required \u2014 the",
|
|
@@ -3612,10 +3623,12 @@ function scoreFiles(inputs) {
|
|
|
3612
3623
|
}
|
|
3613
3624
|
const symbols = symbolsByFile.get(file.path) ?? [];
|
|
3614
3625
|
let symHits = 0;
|
|
3626
|
+
let exactSym = 0;
|
|
3615
3627
|
for (const sym of symbols) {
|
|
3616
3628
|
const name = sym.name.toLowerCase();
|
|
3617
3629
|
if (qTokens.has(name)) {
|
|
3618
3630
|
symHits += 3;
|
|
3631
|
+
exactSym += 1;
|
|
3619
3632
|
} else {
|
|
3620
3633
|
for (const t of qTokens) {
|
|
3621
3634
|
if (name.includes(t) || t.includes(name)) {
|
|
@@ -3640,7 +3653,7 @@ function scoreFiles(inputs) {
|
|
|
3640
3653
|
score2 += 5;
|
|
3641
3654
|
reasons.push("seed");
|
|
3642
3655
|
}
|
|
3643
|
-
scored.push({ file, score: score2, reasons });
|
|
3656
|
+
scored.push({ file, score: score2, reasons, symHits, exactSym });
|
|
3644
3657
|
}
|
|
3645
3658
|
const positivePaths = new Set(scored.filter((s) => s.score > 0).map((s) => s.file.path));
|
|
3646
3659
|
if (positivePaths.size > 0) {
|
|
@@ -3675,7 +3688,8 @@ async function retrieve(graph, query, options = {}) {
|
|
|
3675
3688
|
return {
|
|
3676
3689
|
files: [],
|
|
3677
3690
|
confidence: "low",
|
|
3678
|
-
reason: qTokens.length === 0 ? "empty query" : "empty graph"
|
|
3691
|
+
reason: qTokens.length === 0 ? "empty query" : "empty graph",
|
|
3692
|
+
symbolMatched: false
|
|
3679
3693
|
};
|
|
3680
3694
|
}
|
|
3681
3695
|
const rankInputs = {
|
|
@@ -3691,10 +3705,13 @@ async function retrieve(graph, query, options = {}) {
|
|
|
3691
3705
|
return {
|
|
3692
3706
|
files: [],
|
|
3693
3707
|
confidence: "low",
|
|
3694
|
-
reason: `no matches for ${JSON.stringify(qTokens)}
|
|
3708
|
+
reason: `no matches for ${JSON.stringify(qTokens)}`,
|
|
3709
|
+
symbolMatched: false
|
|
3695
3710
|
};
|
|
3696
3711
|
}
|
|
3697
|
-
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);
|
|
3698
3715
|
const topScore = positive[0]?.score ?? 0;
|
|
3699
3716
|
const secondScore = positive[1]?.score ?? 0;
|
|
3700
3717
|
let confidence;
|
|
@@ -3706,7 +3723,8 @@ async function retrieve(graph, query, options = {}) {
|
|
|
3706
3723
|
return {
|
|
3707
3724
|
files: top,
|
|
3708
3725
|
confidence,
|
|
3709
|
-
reason: `top: ${reasons}
|
|
3726
|
+
reason: `top: ${reasons}`,
|
|
3727
|
+
symbolMatched
|
|
3710
3728
|
};
|
|
3711
3729
|
}
|
|
3712
3730
|
|
|
@@ -4541,6 +4559,16 @@ function extractQuery(toolName, input) {
|
|
|
4541
4559
|
}
|
|
4542
4560
|
return null;
|
|
4543
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
|
+
}
|
|
4544
4572
|
function recentlyTouchedMatchesQuery(recentPaths, queryTokens) {
|
|
4545
4573
|
const matches = [];
|
|
4546
4574
|
for (const path of recentPaths) {
|
|
@@ -4582,6 +4610,14 @@ async function handleGate(req, ctx) {
|
|
|
4582
4610
|
await logDecision(ctx, req.tool_name, null, res2.decision, res2.reason);
|
|
4583
4611
|
return res2;
|
|
4584
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
|
+
}
|
|
4585
4621
|
const retrieval = await retrieve(ctx.graph, query);
|
|
4586
4622
|
if (retrieval.confidence === "low") {
|
|
4587
4623
|
const res2 = {
|
|
@@ -4602,6 +4638,14 @@ async function handleGate(req, ctx) {
|
|
|
4602
4638
|
await logDecision(ctx, req.tool_name, query, res2.decision, res2.reason);
|
|
4603
4639
|
return res2;
|
|
4604
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
|
+
}
|
|
4605
4649
|
const top = retrieval.files.slice(0, 3).map((f) => f.path).join(", ");
|
|
4606
4650
|
const res = {
|
|
4607
4651
|
decision: "block",
|