@sdsrs/code-graph 0.154.0 → 0.156.0

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/README.md CHANGED
@@ -427,7 +427,7 @@ skill does is also runnable by hand.
427
427
  - **Rust has no `inherits` edges** — the language has no class inheritance, so `impl Trait for Type` is recorded as `implements` and an `inherits`-filtered query returns empty for Rust.
428
428
  - **Kotlin/Swift interface conformance** is recorded as `inherits` (both use a single `: Type` grammar for base classes and protocols/interfaces), so `implements`-filtered queries return empty for these two languages.
429
429
  - **Cross-file dead-code detection** may false-positive a type whose only cross-file reference sits beyond the 4096-byte stored-content cap per node (documented accepted limitation, v0.97.1).
430
- - **Rust: borrowing a binding named `raw`** (`&raw`, `&raw[..]`, `&raw.field`) is misread by the pinned `tree-sitter-rust` 0.23 grammar as the start of the `&raw const` / `&raw mut` pointer operator, so the enclosing expression can be dropped from the index. `&mut raw`, `&self.raw` and the genuine `&raw const x` operator all parse correctly. The index reports the damage rather than hiding it. A run prints `Syntax errors in <file> — symbols may be incomplete` per file plus an `N file(s) parsed with syntax errors` total, and the verdict is stored with the index, so it survives the run: `health-check` names the files (`Parse: N file(s) indexed over a damaged parse`, plus `files_with_parse_errors` / `parse_error_files` under `--json`), `doctor` shows a `Parse` row, and the MCP `get_index_status` carries the same two fields. An incremental run over unrelated files does not clear it — only re-parsing the offending file, or deleting it, does. Workaround: rename the binding. Measured on this repo by indexing it fresh at v0.150.0: **9 of 163** tracked Rust files trip the warning, and the run's total says 10 — a Markdown file trips the same counter for an unrelated reason. The stored set names the same ten, which is the cheapest way to check this table has not drifted again: it is a reading of one tree at one commit, not a constant, and an earlier revision of this bullet carried 8/161/9 long after they stopped being true. Recovery is usually partial rather than total: across a fixture covering every trigger shape, the surrounding functions and their call edges survived.
430
+ - **A file tree-sitter cannot fully parse is indexed over a damaged parse.** Symbols inside the damaged region can be missing or mis-scoped — e.g. a Swift class whose body holds `#if`/`#error` directives, or macro-heavy C++ headers. The index reports the damage rather than hiding it. A run prints `Syntax errors in <file> — symbols may be incomplete` per file plus an `N file(s) parsed with syntax errors` total, and the verdict is stored with the index, so it survives the run: `health-check` names the files (`Parse: N file(s) indexed over a damaged parse`, plus `files_with_parse_errors` / `parse_error_files` under `--json`), `doctor` shows a `Parse` row, and the MCP `get_index_status` carries the same two fields. An incremental run over unrelated files does not clear it — only re-parsing the offending file, or deleting it, does. Rust borrows of a binding named `raw` (`&raw`, `&raw[..]`, `&raw.field`), which the old `tree-sitter-rust` 0.23 grammar misread as the `&raw const` operator, parse cleanly since the tree-sitter 0.25 upgrade: this repo indexed fresh went from 9 damaged Rust files to 0.
431
431
 
432
432
  ## Team-shared graph snapshot
433
433
 
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.154.0",
7
+ "version": "0.156.0",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -129,6 +129,11 @@ function firstShellClause(cmd) {
129
129
  if (c === quote) quote = null;
130
130
  continue;
131
131
  }
132
+ // Outside quotes a backslash escapes the next character: `\"` opens no
133
+ // quote and `\;` / `\|` are literal. quotedSpans reads patterns by the same
134
+ // rule; a splitter that disagreed let `grep \"X\" src/ && echo "Y"` hand the
135
+ // echo's `Y` to the pattern reader (pre-ship review F1).
136
+ if (c === '\\') { i++; continue; }
132
137
  if (c === '"' || c === "'") { quote = c; continue; }
133
138
  // Control operators END the grep's argument list → truncate. NOT redirects
134
139
  // (`>` `<`): `2>&1`, `>out`, and process substitution `-f <(cat pats) src/`
@@ -227,8 +232,47 @@ function extractPatterns(cmd) {
227
232
  // Collect every quoted argument — first one is the pattern in standard grep
228
233
  // usage; subsequent ones (e.g. `-e "second"`) are also patterns or filter
229
234
  // expressions and worth screening too.
230
- const matches = [...stripped.matchAll(/"([^"]+)"|'([^']+)'/g)];
231
- return matches.map(m => m[1] !== undefined ? m[1] : m[2]);
235
+ return quotedSpans(stripped).map(s => s.body).filter(Boolean);
236
+ }
237
+
238
+ /**
239
+ * The quoted spans of a shell clause, read the way the shell reads them — the
240
+ * rules firstShellClause, splitTopLevelSegments and extractUnansweredTail also
241
+ * follow (lesson #9656 inside quotes; pre-ship review F1 outside), which a span regex (`"([^"]+)"|'([^']+)'`) did not: it closed a
242
+ * double-quoted argument at the first `\"`, so
243
+ * `grep "if:\s*'\|\"if\"\|statusMessage"` yielded `\|statusMessage`, and its
244
+ * translation `|statusMessage` matches every line (observed 2026-09-25).
245
+ * - `'…'` is literal to the next `'`.
246
+ * - `"…"`: a backslash escapes `"` `\` `$` and backtick (the backslash is
247
+ * dropped) and, before a newline, removes both characters (a line
248
+ * continuation); before anything else it is literal (`"a\|b"`).
249
+ * - Outside quotes a backslash escapes the next character, so `\"` opens
250
+ * nothing — the shell hands grep the quote character itself.
251
+ * An unterminated quote ends the scan: what follows is not a span we can read.
252
+ * @returns {{start: number, end: number, body: string}[]} `end` is exclusive,
253
+ * covering both quote characters.
254
+ */
255
+ function quotedSpans(s) {
256
+ const spans = [];
257
+ if (typeof s !== 'string') return spans;
258
+ for (let i = 0; i < s.length; i++) {
259
+ const c = s[i];
260
+ if (c === '\\') { i++; continue; }
261
+ if (c !== '"' && c !== "'") continue;
262
+ let body = '';
263
+ let j = i + 1;
264
+ for (; j < s.length && s[j] !== c; j++) {
265
+ if (c === '"' && s[j] === '\\' && j + 1 < s.length && '"\\$`\n'.includes(s[j + 1])) {
266
+ j++;
267
+ if (s[j] === '\n') continue; // backslash-newline is a line continuation: both go
268
+ }
269
+ body += s[j];
270
+ }
271
+ if (j >= s.length) break;
272
+ spans.push({ start: i, end: j + 1, body });
273
+ i = j;
274
+ }
275
+ return spans;
232
276
  }
233
277
 
234
278
  // Declaration anchors inside a pattern (`def cascade_failure|class TaskState`)
@@ -349,7 +393,16 @@ function isAgFilenameSearch(clause) {
349
393
  // argument can contain whitespace, so `ag "some_symbol -g x" src/` splits into
350
394
  // three tokens and the middle one looks exactly like a flag. Blanking leaves
351
395
  // an attached value's flag behind (`-g"x"` → `-g`), which is what we want.
352
- const scan = clause.replace(VERB_STRIP, '').replace(/"[^"]*"|'[^']*'/g, ' ');
396
+ // Spans come from quotedSpans, not a span regex: `"a\" -g x"` is ONE
397
+ // argument, and a regex that closed it at the `\"` left `-g` bare.
398
+ const unverbed = clause.replace(VERB_STRIP, '');
399
+ let scan = '';
400
+ let at = 0;
401
+ for (const sp of quotedSpans(unverbed)) {
402
+ scan += unverbed.slice(at, sp.start) + ' ';
403
+ at = sp.end;
404
+ }
405
+ scan += unverbed.slice(at);
353
406
  for (const tok of scan.trim().split(/\s+/)) {
354
407
  if (!tok || tok[0] !== '-') continue;
355
408
  if (tok.startsWith('--')) {
@@ -602,6 +655,7 @@ function extractUnansweredTail(cmd) {
602
655
  if (c === quote) quote = null;
603
656
  continue;
604
657
  }
658
+ if (c === '\\') { i++; continue; } // outside quotes: escapes the next char (see firstShellClause)
605
659
  if (c === '"' || c === "'") { quote = c; continue; }
606
660
  if (c === ';' || (c === '&' && cmd[i + 1] === '&')) {
607
661
  const tail = cmd.slice(i + (c === ';' ? 1 : 2)).trim();
@@ -695,6 +749,15 @@ function splitTopLevelSegments(cmd) {
695
749
  if (c === quote) quote = null;
696
750
  continue;
697
751
  }
752
+ // Outside quotes: escapes the next char, kept verbatim (see firstShellClause)
753
+ // — except a newline, where the pair is a line continuation and the shell
754
+ // removes both. Keeping them left `&& \⏎ grep …` a segment that starts
755
+ // with `\`, which GREP_HEAD rejects (pre-ship review round 2 F1).
756
+ if (c === '\\' && i + 1 < cmd.length) {
757
+ if (cmd[i + 1] !== '\n') cur += c + cmd[i + 1];
758
+ i++;
759
+ continue;
760
+ }
698
761
  if (c === '"' || c === "'") { quote = c; cur += c; continue; }
699
762
  // `&&` and `||` (a single `&`/`|` is NOT a split — `|` is an output-filter
700
763
  // pipe, lone `&` is background and rare in tool calls).
@@ -35,7 +35,7 @@ jobs:
35
35
  node-version: '20'
36
36
  - name: Build snapshot
37
37
  run: |
38
- npx -y -p @sdsrs/code-graph@0.154.0 code-graph-mcp snapshot create --out snapshot.db
38
+ npx -y -p @sdsrs/code-graph@0.156.0 code-graph-mcp snapshot create --out snapshot.db
39
39
  zstd -9 snapshot.db -o snapshot.db.zst
40
40
  mv snapshot.db.zst "code-graph-snapshot-${GITHUB_SHA:0:7}.db.zst"
41
41
  - name: Upload to release
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.154.0",
3
+ "version": "0.156.0",
4
4
  "description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,10 +35,10 @@
35
35
  "node": ">=16"
36
36
  },
37
37
  "optionalDependencies": {
38
- "@sdsrs/code-graph-linux-x64": "0.154.0",
39
- "@sdsrs/code-graph-linux-arm64": "0.154.0",
40
- "@sdsrs/code-graph-darwin-x64": "0.154.0",
41
- "@sdsrs/code-graph-darwin-arm64": "0.154.0",
42
- "@sdsrs/code-graph-win32-x64": "0.154.0"
38
+ "@sdsrs/code-graph-linux-x64": "0.156.0",
39
+ "@sdsrs/code-graph-linux-arm64": "0.156.0",
40
+ "@sdsrs/code-graph-darwin-x64": "0.156.0",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.156.0",
42
+ "@sdsrs/code-graph-win32-x64": "0.156.0"
43
43
  }
44
44
  }