@imunitic/synapse 0.0.1-test.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.
Files changed (40) hide show
  1. package/Index.md.template +23 -0
  2. package/bin/synapse-hook.cjs +19 -0
  3. package/bin/synapse-setup.cjs +420 -0
  4. package/bin/synapse.cjs +20 -0
  5. package/commands/synapse-design-note.md +229 -0
  6. package/commands/synapse-init.md +354 -0
  7. package/commands/synapse-note.md +196 -0
  8. package/commands/synapse-rebuild-diff.md +314 -0
  9. package/commands/synapse-rebuild-full.md +152 -0
  10. package/commands/synapse-status.md +144 -0
  11. package/commands/synapse-task-note.md +133 -0
  12. package/commands/synapse-vault-tidy.md +187 -0
  13. package/harness/claude/hooks.json +54 -0
  14. package/harness/codex/hooks.json +54 -0
  15. package/harness/codex/skills/synapse-design-note/SKILL.md +236 -0
  16. package/harness/codex/skills/synapse-init/SKILL.md +354 -0
  17. package/harness/codex/skills/synapse-note/SKILL.md +212 -0
  18. package/harness/codex/skills/synapse-rebuild-diff/SKILL.md +315 -0
  19. package/harness/codex/skills/synapse-rebuild-full/SKILL.md +149 -0
  20. package/harness/codex/skills/synapse-status/SKILL.md +146 -0
  21. package/harness/codex/skills/synapse-task-note/SKILL.md +133 -0
  22. package/harness/codex/skills/synapse-vault-tidy/SKILL.md +187 -0
  23. package/harness/opencode/plugin/synapse.js +164 -0
  24. package/lib/obsidian-mcp-refresh.cjs +303 -0
  25. package/lib/resolve-binaries.cjs +54 -0
  26. package/package.json +26 -0
  27. package/skills/synapse-node/SKILL.md +211 -0
  28. package/skills/synapse-node-authoring/SKILL.md +188 -0
  29. package/skills/synapse-node-format/SKILL.md +205 -0
  30. package/skills/synapse-orientation/SKILL.md +468 -0
  31. package/skills/synapse-query/SKILL.md +99 -0
  32. package/skills/synapse-task/SKILL.md +261 -0
  33. package/skills/synapse-vault/SKILL.md +107 -0
  34. package/synapse-claude.md +220 -0
  35. package/synapse-fence-languages.conf.template +24 -0
  36. package/synapse-ignore-files.conf.template +45 -0
  37. package/synapse-module-boilerplate.conf.template +24 -0
  38. package/synapse-projects.conf.template +14 -0
  39. package/synapse-prompt-stopwords.conf.template +594 -0
  40. package/synapse.conf.template +23 -0
@@ -0,0 +1,468 @@
1
+ ---
2
+ name: synapse-orientation
3
+ description: How to work out where meaning lives in a codebase you have not seen before — first mechanically, from the repo's own symbol vocabulary, and where no grammar exists, by four questions in order with the cheap commands that answer each. Includes tree-sitter grammar discovery. Use when clustering a repo into Synapse graph nodes for the first time, when re-deriving a node's premises in /synapse-rebuild's re-orient class, or any time you need to orient in an unfamiliar tree before making claims about it.
4
+ ---
5
+
6
+ # Orienting in an unfamiliar repo
7
+
8
+ Loaded by `/synapse-init` at its orientation step, and by `/synapse-rebuild` when a node lands in
9
+ the *re-orient* class and its premises have to be re-derived rather than patched. Useful on its own
10
+ terms too: nothing here is Synapse-specific except what you do with the answers.
11
+
12
+ It is a skill rather than a section inside either caller because it is technique, not procedure —
13
+ how to find where meaning lives in a tree you have never seen. Both callers need exactly the same
14
+ technique, and a copy in each is how the two start giving different advice.
15
+
16
+ The goal is not a summary of every file; it is learning *where meaning lives in this particular
17
+ tree* well enough to cluster it and then write about it.
18
+
19
+ ## Start with the vocabulary — it is mechanical, and it is most of the answer
20
+
21
+ ```
22
+ synapse vocab # writes six tables into $SYNAPSE_WORK_DIR
23
+ ```
24
+
25
+ One command derives what used to be an exploration. It tags every file that has a grammar, splits
26
+ symbol names on CamelCase and snake_case, drops stopwords, and aggregates per directory group:
27
+
28
+ - `counts.tsv` — `group ⇥ file count`, biggest first. **This is question 1, already answered.**
29
+ - `groupwords.tsv` — `group ⇥ word ⇥ count`. **This is question 4, for the whole repo, unsampled.**
30
+ - `groupexts.tsv` — `group ⇥ artifact kind ⇥ count`. What an area is *made of*, over every kept
31
+ path rather than the code subset — the interesting files here are usually the ones no grammar
32
+ can read.
33
+ - `namespaces.tsv` — `group ⇥ namespace ⇥ agree ⇥ total`, where a rule is configured (question 3,
34
+ below, for whichever extensions `~/.claude/synapse-namespace-rules.conf` knows about).
35
+ - `parseable.tsv` and `distinctive.tsv` feed `synapse gate` and this section's own distinctiveness
36
+ question respectively — see immediately below.
37
+
38
+ Measured: the whole of a large repo (125,351 files, 98k of them code) takes ~51 seconds, and clusters
39
+ derived from the result expanded to 99.91% coverage. Read these files and cluster from them.
40
+ Reading is free; only what you print costs, so collapse to a few dozen lines before reading any
41
+ source at all.
42
+
43
+ **Which words are *distinctive* rather than merely frequent has a first answer in `distinctive.tsv`
44
+ now** (`group ⇥ distinctive ⇥ considered`) — how many of a group's top terms score above 0.5 on the
45
+ saturation curve `distinctiveness = D / (D + df)`, `D = max(2, N/K)`, rather than the binary "appears
46
+ in every group" cliff. Read it alongside `groupwords.tsv`, not instead of it: the table says *how
47
+ many* of a group's top terms are shared background versus real signal, not *which* ones or *why* —
48
+ a word appearing in every group is background, a word appearing in two is a concept, and seeing that
49
+ distinction by eye across groups, not down one, is still the part that turns a count into a cluster.
50
+
51
+ **An empty `groupwords.tsv` is a legitimate answer**, not a failure: no file in this tree had a
52
+ usable grammar. `synapse vocab` says so on stderr and exits 0. That is the case the four
53
+ questions below exist for.
54
+
55
+ ## When there is no usable grammar — and as a complement when there is
56
+
57
+ Four questions, in order. They are language-agnostic; how you answer each one is not, and deducing
58
+ that on the spot is the work. With a vocabulary table in hand, 1 and 4 are already answered and 2
59
+ and 3 are still worth asking — question 3 in particular is invisible to the vocabulary, because it
60
+ is about the gap between what the code calls itself and what the *directory* calls it:
61
+
62
+ 1. **Where is the weight?** Group paths by module/directory and count. Tells you which
63
+ subsystems are large enough to deserve a node and which must be grouped with a neighbour.
64
+ 2. **What kind of artifact dominates?** Group by extension and count, per candidate cluster.
65
+ This is the cheapest source of genuine surprise — a cluster that is 60% JSON or `.bpmn` or
66
+ `.sql` is telling you something no module name will.
67
+ 3. **What does the code call itself, versus what the directory calls it?** Derive the code's own
68
+ namespace/package/module declaration, however this ecosystem spells it, and compare it with
69
+ the directory names. **Divergences
70
+ here are the highest-value findings in the whole build** and they are invisible from the
71
+ filesystem: a module named one thing whose code is uniformly named another means every later
72
+ search for the wrong term returns nothing.
73
+ 4. **What are the domain's verbs?** The exported/public symbol names — they read as the
74
+ vocabulary of the domain, and clusters of related names (a state machine, a configuration
75
+ family) are what a node's prose should be about.
76
+
77
+ **Namespace rules are self-populating too, the same way grammar discovery below is — write one
78
+ back when question 3 turns up a real declaration convention.** `~/.claude/synapse-namespace-rules.conf`
79
+ starts empty and nothing seeds it, so `namespaces.tsv` stays empty forever unless something
80
+ writes a rule into it. If you just hand-derived a namespace root for an ecosystem this repo uses
81
+ and `synapse-namespace-rules.conf` has no entry for its extension yet, the derivation you just
82
+ did *is* the rule — write it back (create the file as `{}` first if it doesn't exist) so the next
83
+ repo in this ecosystem gets `namespaces.tsv` for free instead of a repeat of this same
84
+ by-hand analysis. A rule is a `kind` (`in-file`, or `build-file` plus a `file` name to search
85
+ ancestor directories for) and a `prefix`/`terminator` pair bracketing the declared value on its
86
+ own line — verified against one real file in *this* repo before writing, never guessed from
87
+ general knowledge of the ecosystem: a wrong guess caches a bad rule for every future project in
88
+ that ecosystem, not just this one.
89
+
90
+ Every ecosystem is real research the first time it is encountered, not a lookup against a
91
+ pre-built list — same as an unrecognised grammar below: find the file (or the in-file line) that
92
+ declares the namespace, confirm it against the repo's own real source, then write the rule.
93
+
94
+ **Answer these with aggregate shell over the path lists, not by reading files.** Reading is
95
+ internal and free; only what you print costs tokens, so a 15,000-file cluster should collapse to
96
+ a few dozen lines before you read anything. Then let `synapse rank` pick which 2–4 files per
97
+ node are worth actually reading — see "Choosing what to read" below.
98
+
99
+ **Do not sample, and do not invent a sampling rule.** `synapse tags --paths` tags a whole list in
100
+ one invocation (measured 33× faster on 200 files than one-file-at-a-time), and `synapse vocab`
101
+ above uses it to cover an entire repository in under a minute — there's no cost left to bound with
102
+ a sampling rule. Every fixed rule is biased anyway: alphabetical is an accident, largest-file
103
+ favours generated code and god-classes, most-referenced needs the full scan you were avoiding. If
104
+ you find yourself reaching for a sample, run the full pass instead.
105
+
106
+ **When an aggregation is worth repeating, write it down rather than retyping it.** Once you have
107
+ run the same one-liner for the third cluster, record it in `synapse/{repo}@{branch}/_profile.txt` — a
108
+ machine-only sibling of `_manifest.tsv`, never a node — as a fenced command plus one line on what
109
+ it revealed about *this* repo. **Read it, don't execute it:** it is a record of the aggregations
110
+ that earned their keep, so a later run applies the commands itself rather than shelling out to a
111
+ script fetched from a notes vault. Begin any re-run by reading it, and improve it rather than
112
+ re-deriving from scratch. Nothing like it ships, because which aggregations carry signal depends
113
+ on the codebase — a distributed one would encode the wrong ecosystem's conventions.
114
+
115
+ `.txt`, with markdown formatting inside, for a measured reason: Obsidian indexes `.md` files as
116
+ notes, so a `_profile.md` turns up in search, Quick Switcher and the graph, where it is pure noise
117
+ to a human reading notes. A non-`.md` extension is invisible to all of those and still perfectly
118
+ readable. Note the `_` prefix does *nothing* mechanically — it is only a hint to a human who sees
119
+ the file, matching `_manifest.tsv`. Record **negative results** here too ("this abbreviation has no
120
+ expansion anywhere in the repo"); a saved shell script cannot hold a search that came back empty,
121
+ which is the main reason this is prose rather than an executable.
122
+
123
+ ## Choosing what to read
124
+
125
+ Once clusters exist, `synapse rank --sources lists/NN.txt` decides reading order — it does not
126
+ decide coverage, which stays exhaustive. Two pools, because the two halves of authoring want
127
+ different files:
128
+
129
+ ```
130
+ synapse rank --sources lists/NN.txt --pool summary # names: code, tests, DSL consumers
131
+ synapse rank --sources lists/NN.txt --pool crux # implementation only, tests excluded
132
+ ```
133
+
134
+ A summary is made of *names*, so test class names and the names of the code consuming a DSL file
135
+ both count as evidence at zero read cost. A crux is concentrated logic, so tests are excluded from
136
+ that half — density ranks them high for a structural reason (many small test methods, each a
137
+ definition, in a small file), and none of the recorded `crux_path` values on a real namespace is a
138
+ test.
139
+
140
+ **Read the top few, not the list.** The ranking exists so that reading 3 files out of 809 produces
141
+ the same summary as reading all of them, which is the measured result this whole approach rests on.
142
+
143
+ **Tree-sitter acceleration — handling `synapse tags`'s exit codes.** These are the *single-file*
144
+ form's codes. In `--paths` batch form an extension with no grammar produces one warning line on
145
+ stderr and the batch still succeeds, because a mixed repo nearly always has some language that
146
+ works and failing the whole batch for one would throw away every language that did.
147
+
148
+ **A missing grammar gets one line and nothing more.** No coverage report, no per-language
149
+ accounting: a file tree-sitter cannot parse is not source, and a non-source file has nothing to
150
+ contribute to a node's prose. Do not build a tally out of those warnings.
151
+
152
+ - **Exit 0:** use the printed tags directly as clustering signal for this file.
153
+ - **Exit 1:** this language is a known dead end (or tree-sitter/a C compiler isn't available at
154
+ all) — fall back to a full read for this file, silently, no need to re-announce something
155
+ already covered by the up-front C-compiler check.
156
+ - **Exit 2 ("needs discovery" — this extension has never been seen before):** run this discovery
157
+ procedure once, then retry the script:
158
+ 1. Try the naming convention first: `https://github.com/tree-sitter/tree-sitter-{lang}` (covers
159
+ most official grammars) — a quick existence check, no reasoning needed if it just resolves.
160
+ 2. If that doesn't resolve, fall back to a web search for a community-maintained grammar for
161
+ the language.
162
+ 3. **Verify before trusting — two tiers, tried in order, and a repo existing is never
163
+ sufficient on its own for either.** Stop at tier 1 if it verifies; otherwise tier 2
164
+ always runs. The registry records which tier actually won, per step 4 below.
165
+
166
+ **Tier 1 — `queries/tags.scm`.** Check **both** the repo root and any sub-grammar subpath,
167
+ in that order — multi-grammar repos split either way and neither is the rule: some put a
168
+ query under each sub-grammar's own subdirectory, others keep a single shared query at the
169
+ root serving every sub-grammar. Plenty of grammars ship only `queries/highlights.scm` and no
170
+ tags query at all — a repo existing there proves nothing by itself, only reading the file
171
+ does.
172
+
173
+ **Tier 2 — generate a tags.scm, when tier 1 is absent.** No `queries/tags.scm` means no
174
+ reference data at all: a `.ref` role is only ever assigned by `tagFileTags`, tier 1's own
175
+ function — neither reading a grammar's `locals.scm` captures nor classifying its
176
+ `node-types.json` node types can produce one. So this tier's job is not to accept whatever
177
+ definitions those two files hand it and stop; it is to generate a real, tags.scm-shaped
178
+ query file, drawing on whichever of `locals.scm` and `node-types.json` actually exist, and
179
+ reference-pattern generation (`@reference.call`, or the grammar's own call-expression
180
+ equivalent) is **unconditional in every case below** — the one thing neither file's raw
181
+ content can ever supply on its own, so it is never optional, never gated on which source
182
+ material happens to be present.
183
+
184
+ Gather what real source material exists first, then generate from it:
185
+
186
+ - **`locals.scm`, if present, is ground truth for kind labels** — real, grammar-author-chosen
187
+ names (`function`, `method`, `type`, `field`, `var`, `parameter`, ...), strictly better than
188
+ any guessed suffix/prefix heuristic wherever it overlaps one. Read it and judge whether its
189
+ `@local.definition.*` captures are well-formed or noise — the same "verify before trusting"
190
+ standard as tier 1, not a weaker one just because there is less to work with: one grammar's
191
+ `locals.scm` might have well-formed, per-kind captures; another's might produce nothing
192
+ usable — not necessarily because of a blanket `@reference` capture on every identifier
193
+ (confirmed inert: a capture without the `local.definition` prefix never reaches output
194
+ regardless of how broad or noisy it is), but because its own `@definition.*` captures are
195
+ themselves missing the `local.` prefix this convention requires (the same structural gap
196
+ case 1 below describes). A capture with **no** kind suffix at all — bare
197
+ `@local.definition`, not `@local.definition.<kind>` — is not a defect either: it is a real,
198
+ common nvim-treesitter shape, and it maps through the kind-synonym rule list below the same
199
+ as any suffixed one, keyed by the empty string. Then **run it**, against a real sample file
200
+ from the repo being oriented in, not just read the `.scm` text — a query whose every
201
+ pattern leans on a predicate the evaluator can't answer (`#match?`, or any unrecognised
202
+ predicate name) fails to *load* at all rather than matching zero, a distinct outcome
203
+ (`Error.PredicateUnsupported`) that is an outright reject exactly like a candidate that
204
+ matched nothing, not a puzzle to work around.
205
+ - **`node-types.json`, if present, fills whatever `locals.scm` doesn't cover and supplies the
206
+ raw node-type list a reference pattern builds from.** It is a build artifact
207
+ `tree-sitter generate` always produces, so it is available for nearly every grammar — which
208
+ makes it about *quality*, not presence: judge it by running a classification against a real
209
+ sample the same way as `locals.scm`, not by reading the JSON. The zero-cost floor (a
210
+ candidate matching literally nothing is auto-rejected, no judgement needed) is the only
211
+ mechanical check; everything else is read and decided by eye. The classifier's suffix/prefix
212
+ matching and the walk's identifier search are both case-insensitive and
213
+ PascalCase-boundary-aware (a PascalCase node-type name and an all-caps leaf type are both
214
+ caught, confirmed live on a real grammar — 0 tags to 46 on a real file). What still doesn't
215
+ match is a genuinely *different word*, not just different casing: a node-type name carrying
216
+ no suffix this convention recognizes at all, or a leaf type that isn't `identifier`/`name`
217
+ under any casing, both stay unmatched, confirmed directly. That's still not a reason to
218
+ hardcode one grammar's exact spelling into the classifier — the generic case/boundary
219
+ handling covers a real, cross-grammar class, a per-grammar word substitution would not.
220
+
221
+ The kind-synonym rule list both bullets above lean on is
222
+ `~/.claude/synapse-kind-synonyms.conf` (`SYNAPSE_KIND_SYNONYMS_CONF` overrides the path), the
223
+ same shape and precedence as `synapse-grammars.conf`/`synapse-namespace-rules.conf` — ordered
224
+ rules, first match wins, absent means no mapping rather than a guessed one. Keyed by a
225
+ `locals.scm` capture's kind suffix (or the empty string for an unsuffixed capture) or by a
226
+ `node-types.json` node's raw type name, it can either relabel a kind already guessed or
227
+ **force-classify a type the heuristic missed outright** — confirmed needed against a real
228
+ grammar whose full function-with-implementation node type carried no
229
+ `_declaration`/`_definition`/`decl`/`def` suffix at all, so it never became a candidate on its
230
+ own. Absent/empty is a no-op either way, so this never needs touching unless a specific
231
+ grammar needs it.
232
+
233
+ Five source-material cases, tried in this order — every one ends the same way, generating a
234
+ real query with unconditional reference patterns, never accepting definitions alone as good
235
+ enough:
236
+
237
+ 1. **`locals.scm` exists.** Its per-node-type capture pattern is usually close to
238
+ tags.scm-shaped already. If its captures already carry the `local.definition.<kind>`
239
+ prefix, translate them directly. If they don't (the structural gap above, not a content
240
+ one), translate by field rename — confirmed on a real grammar's own `locals.scm`: a
241
+ capture like `(some_declaration (identifier) @definition.namespace)` becomes
242
+ `(some_declaration name: (identifier) @name) @definition.namespace`, one field-name away
243
+ from the real thing. Either way, add reference patterns for the grammar's
244
+ call-expression-shaped node(s) — `locals.scm` never carries these, so this step is never
245
+ satisfied by translation alone.
246
+ 2. **Both `locals.scm` and `node-types.json` exist.** Compose rather than pick one:
247
+ `locals.scm`'s captures win for kind labels wherever they cover a node type: real,
248
+ grammar-author-chosen ground truth beats a guessed suffix/prefix match every time they
249
+ overlap. `node-types.json` fills whatever `locals.scm` doesn't cover (nvim-treesitter's own
250
+ convention is scope-tracking, not exhaustive definition coverage) and supplies the raw
251
+ node-type list to generate reference patterns from — required here the same as every other
252
+ case, since neither source file contributes one on its own.
253
+ 3. **No `locals.scm`; `node-types.json` exists with real declaration-shaped structure.**
254
+ Classify its node types the same way case 2's fill-in step does — suffix/prefix matching,
255
+ case-insensitive and PascalCase-boundary-aware, `synapse-kind-synonyms.conf` filling or
256
+ relabeling what the heuristic misses — and generate reference patterns from its
257
+ call-expression-shaped types.
258
+
259
+ **Before accepting a weak result here, try inference once — verified, not left as a
260
+ guess.** When classification runs but is visibly weak (real declarations missing entirely,
261
+ or caught with an unhelpfully generic kind), read `node-types.json` for the pattern the
262
+ heuristic missed. Two confirmed shapes to recognize: a *prefix*-marked defining keyword
263
+ instead of a suffix-marked one (invisible to a suffix scan built for a
264
+ `noun_declaration`-style convention), and a body/declaration split the classifier has no
265
+ word for (a node type that names a full function-with-implementation but carries none of
266
+ the recognized suffixes). Propose `synapse-kind-synonyms.conf` rules scoped to this
267
+ grammar's own scope for what's missing or mislabeled, then **re-run `synapse tags` against
268
+ the same real sample and confirm the result actually improved** — not just that the rule
269
+ looks reasonable on paper. Text quality is not a proxy for this: a `locals.scm` can read
270
+ as well-formed and still produce zero tags when actually run against a real sample,
271
+ confirmed directly — exactly the gap this verification step exists to catch. Only write to
272
+ the conf file once the improvement is confirmed live, and only there — never
273
+ `$SYNAPSE_WORK_DIR`. These are facts about a *language*, not this repo, and the whole
274
+ reason these conf files are global and permanent (see step 4 below) is so the next project
275
+ in the same language skips rediscovering it; a repo-scoped copy would throw that away for
276
+ exactly the kind of finding worth keeping. This step is additive only and has a real
277
+ ceiling: it can fix a wrong kind or catch a type that exists as its own grammar node but
278
+ was missed, never conjure structure a grammar doesn't have. Some languages' own defining
279
+ forms aren't distinct node types in their grammar at all, just a generic list/call form,
280
+ confirmed directly — no rule fixes an absence of structure to key one on.
281
+ 4. **No `locals.scm`; `node-types.json` exists but is nearly featureless** (a real, confirmed
282
+ shape: some grammars' own `node-types.json` files carry only a few dozen named types
283
+ total, almost none declaration-shaped). Generate from the model's own knowledge of the
284
+ language's defining forms instead of the grammar's files, reference patterns included.
285
+ Route around this tagger's missing `#match?` support explicitly — prefer `#eq?`/`#any-of?`
286
+ (e.g. `(#any-of? @ignore "variant-a" "variant-b")`, not a regex) from the start, rather than
287
+ porting a pattern written for an evaluator with a richer predicate set.
288
+ 5. **`node-types.json` itself is absent.** Probe for the `tree-sitter` CLI the same
289
+ opportunistic way step 3 above probes for a C compiler — used if present, silently skipped
290
+ if not, never installed. If both the CLI and the grammar's own `grammar.js` are present,
291
+ `tree-sitter generate` regenerates `node-types.json` (confirmed live: byte-identical to
292
+ what was already committed), landing back in case 3 or 4, whichever the grammar's real
293
+ design implies — regeneration recovers a missing artifact, it never manufactures structure
294
+ the grammar doesn't have. If either is missing, this collapses straight into case 4 with
295
+ even less grounding.
296
+
297
+ **Every case resolves the same way:** write the candidate query, run `synapse tags` against
298
+ the same real sample, and judge the result by its actual tag output — real names, kinds, and
299
+ spans on real code — never by reading the raw query syntax, which most users have no reason to
300
+ know and can't meaningfully evaluate. If it looks right, write it to
301
+ `$SYNAPSE_GRAMMARS_QUERY_PATH/{ext}.scm`, the same escape-hatch path a human-authored file
302
+ would use — check first that a human hasn't already placed one there, and never overwrite it.
303
+ If it doesn't look right, write nothing; the outcome is an `"unsupported"` registry entry
304
+ (step 4) rather than a silently accepted weak result. No case needs a different rule, and no
305
+ separate approval gate is needed beyond this — a malformed query fails to load outright
306
+ (`Error.QueryInvalid`), one matching nothing is the existing zero-cost floor, one matching the
307
+ wrong things is caught by eye, the same standard every case's output already has to clear.
308
+
309
+ Two harmless quirks to expect while judging output, confirmed live against real grammars with
310
+ no distinct declaration node types of their own — neither is a reason to reject an
311
+ otherwise-working query: `#any-of?` needs every case variant spelled out as a literal (no
312
+ `(?i)` equivalent), so an odd casing can slip through uncaught where a regex-based `#match?`
313
+ wouldn't — acceptable, since the common real-world spelling is what matters, not every deliberately
314
+ mixed-case adversarial input. And this tagger doesn't implement the classic tags.scm
315
+ convention where an early `@ignore` capture suppresses a later pattern from also matching the
316
+ same node — a defining form's own name can end up tagged twice, once as the intended
317
+ definition and once more as a generic call from the catch-all pattern; noise, not a wrong
318
+ answer, and it happens identically with human-authored upstream queries loaded by this same
319
+ tagger, not something specific to a generated one.
320
+
321
+ **When generation was attempted and its real output still doesn't verify** — every case above
322
+ tried and none produced usable, judged-by-eye-correct output, or the language's grammar has no
323
+ distinct node types worth keying anything on at all:
324
+ `$SYNAPSE_GRAMMARS_QUERY_PATH/{ext}.scm` is the sanctioned escape hatch — a human-authored
325
+ query file that preempts the whole cascade, not a tier to reach automatically. Name it as an
326
+ option when reporting the outcome (step 5) rather than silently accepting a weak result or a
327
+ bare `unsupported`.
328
+ 4. Write the result back to `~/.claude/synapse-grammars.conf` (create it as `{}` first if it
329
+ doesn't exist) — a positive entry (`{"repo": "...", "scope": "..."}`) for whichever tier
330
+ verified, `{"unsupported": true}` only when both came up empty or unusable. Record which
331
+ tier with `"queries"`: omit it (or write `"tags"`) for a real `queries/tags.scm` found in the
332
+ grammar's own repo (tier 1). `"locals"` or `"generated"` mean generation was attempted from
333
+ that source material and its real output didn't verify well enough to write — not that the
334
+ tier won outright without an attempt, which is no longer possible once tier 1 is absent.
335
+ Omitted defaults to `"tags"`, so every entry written before this addition stays valid with no
336
+ migration. This is a permanent, cross-project cache keyed by extension — every future project
337
+ skips rediscovery for this language entirely, tier decision included.
338
+
339
+ **The key is the bare extension with no leading dot** — `"rs"`, never `".rs"`, because
340
+ `synapse tags` derives it from the path suffix. Getting this wrong fails *silently*
341
+ and expensively: the lookup misses, the script keeps returning exit 2, and discovery
342
+ re-runs for that language on every file in every project forever, caching nothing. So the
343
+ file should end up shaped like this:
344
+
345
+ ```json
346
+ {
347
+ "xx": { "repo": "https://github.com/tree-sitter/tree-sitter-xx", "scope": "source.xx" },
348
+ "yy": { "repo": "https://github.com/some-org/tree-sitter-yy", "scope": "source.yy",
349
+ "path": "grammars/yy", "queries": "locals" },
350
+ "zz": { "unsupported": true }
351
+ }
352
+ ```
353
+ 5. **Announce the outcome as a hard requirement, not a recommendation — state whether generation
354
+ was attempted and what its real output looked like, every time tier 1 is absent.** A confident
355
+ read of `locals.scm`/`node-types.json` that predicts generation "wouldn't help" is not a
356
+ substitute for running it, the same reason `synapse-rebuild-diff`'s re-orient gate can't be
357
+ skipped by a correct-looking source reading: only an executed check surfaces whether a real
358
+ reference pattern actually matches on this grammar, not what the source implies it should do.
359
+ Say plainly what happened ("generated a tags.scm from `locals.scm` for `.xx`, cached",
360
+ "generation attempted from `node-types.json`, output didn't verify, falling back to `locals`
361
+ classification", or "no usable tree-sitter grammar for `.xx`, falling back to full reads") —
362
+ then retry `synapse tags {path}` now that the registry has an entry (falls back to a full read
363
+ for this file per the Exit 1 case above if discovery came up empty).
364
+
365
+ ## Fixing a `locals.scm` coverage gap: the zero-candidate `.ref` signature
366
+
367
+ A third discovery-procedure step, distinct in trigger shape from the tags.scm-generation one
368
+ above: that one fires on a grammar missing tier 1 entirely, a per-grammar-discovery event. This
369
+ one fires on a specific finding encountered while working with the link graph, not on discovering
370
+ a new extension — **a `.ref` whose name has zero `def` rows anywhere in `_refs.tsv`** (grep the
371
+ whole file, not just the node under review — the join is repo-wide), **in a grammar that already
372
+ has some `locals.scm`** (partial coverage, not missing entirely — a grammar with no `locals.scm`
373
+ at all just keeps today's unfiltered behavior for local refs, nothing to fix here).
374
+
375
+ That signature means the name is very likely a local binding this grammar's shipped `locals.scm`
376
+ doesn't capture — a function parameter, `let`-binding, or module/functor argument whose binder is
377
+ a node type the query never queries for — not a genuine cross-file reference with a missing
378
+ definition. This is a real, confirmed shape: a grammar's shipped `locals.scm` can capture one
379
+ binder construct (say, a plain value or function-parameter binding) while missing another real
380
+ binder construct in the same language entirely, because that construct parses as a distinct node
381
+ type the query was simply never written to cover — so a reference to that binder's name leaks
382
+ through unfiltered.
383
+
384
+ Same hard-requirement enforcement as the tags.scm-generation step, and the same verify-before-write
385
+ discipline — never assume a patch worked from reading it:
386
+
387
+ 1. Read the grammar's own `locals.scm` and the real AST shape of the binder (`tree-sitter parse`
388
+ against the actual file, or equivalent) to find which node type covers this name and is
389
+ currently uncaptured.
390
+ 2. Write an override at `$SYNAPSE_GRAMMARS_QUERY_PATH/{ext}.locals.scm`, adding a capture for that
391
+ node type in the grammar's own `@local.definition[.kind]` convention — check first that a human
392
+ hasn't already placed one there, and never overwrite it.
393
+ 3. Re-run `synapse tags` against the real file that surfaced the finding, override active, and
394
+ confirm the name's `.ref` row is actually gone — not just that the query text reads right.
395
+ 4. **Announce the outcome as a hard requirement, not a recommendation**, the same as the
396
+ tags.scm-generation step: an override was written and verified against the real file, or a
397
+ genuine attempt didn't resolve the finding (state what was tried) — never silently drop a
398
+ confirmed zero-candidate signature.
399
+
400
+ ## Dependency-edge rules: which languages qualify, and writing one back
401
+
402
+ A per-file "this file depends on library L" edge narrows an ambiguous reference (a name defined in
403
+ more than one node) to the candidate the referencing file actually declares a dependency on —
404
+ `core/links.zig`'s own real signal for that case, read from `synapse-dependency-rules.conf`
405
+ (`SYNAPSE_DEPENDENCY_RULES_CONF` overrides the path), a second registry alongside
406
+ `synapse-namespace-rules.conf` and reusing the exact same `core/namespace.Rule`/`Registry` shape
407
+ unchanged — a rule per extension, `kind` `in-file` or `build-file`, ordered `prefix`/`terminator`,
408
+ first match wins, no per-language branching in code. A separate file, not a second key on the same
409
+ one: an extension can need both a namespace rule (what a file *is*) and a dependency rule (what a
410
+ file *depends on*) at once, and a rule-per-extension registry has no room for two different facts
411
+ on the same key — so the dependency rule is extracted by the same mechanism into its own
412
+ `_deps.tsv` artifact instead, one row per file, not a new column on `_refs.tsv`'s
413
+ one-row-per-reference grain.
414
+
415
+ **A language qualifies for a dependency-edge rule at all only if its import/dependency mechanism is
416
+ a reserved, syntactically-distinct construct — confirmed against the language's own spec, never
417
+ assumed from its paradigm or family.** A reserved keyword or a call with a fixed, unambiguous
418
+ textual shape, syntactically separable from ordinary code, qualifies. Family predicts nothing: two
419
+ languages sharing a paradigm can resolve oppositely — one's own file-loading mechanism might be an
420
+ ordinary function/word call with no fixed reserved construct across implementations
421
+ (disqualifying), while a close relative in the very same paradigm might have a real, fixed parsing
422
+ construct for declaring what it imports (qualifying) — checking the language's own actual spec is
423
+ the only test that tells the two apart. A language that fails this test gets no rule at all,
424
+ documented or committed — not a lighter-touch version of the same treatment, because the syntactic
425
+ distinction the rule shape depends on doesn't exist in the language, and no amount of real-file
426
+ verification fixes that.
427
+
428
+ Only a rule or rules already dogfooded against a real ambiguous-reference finding in this project
429
+ ship as real, committed data. Every other qualifying language is real research the first time it
430
+ is encountered, never a lookup against a pre-built list: find the reserved import/dependency
431
+ construct in the language's own spec, verify the exact `prefix`/`terminator` shape against one
432
+ real file in the repo being oriented in, then write the rule back — same precedent
433
+ `synapse-namespace-rules.conf` itself already sets. Never write a rule from general knowledge of
434
+ an ecosystem alone, and never treat a rule confirmed for one language as a template to copy for
435
+ another just because they look similar from the outside.
436
+
437
+ **Known gap in the rule shape itself:** this is a single-line, single-`prefix`/`terminator` rule.
438
+ A multi-line or grouped import/dependency form (several names bracketed across more than one line,
439
+ or wrapped in an outer grouping construct) isn't expressible this way and produces no edge, not a
440
+ wrong one; that is the correct outcome for a form this rule shape can't cover, not a defect needing
441
+ a workaround here.
442
+
443
+ **A file can have more than one valid identity, and the two sides of a match must agree on which
444
+ one.** A namespace rule's own primary `prefix`/`terminator` answers "what does this file call
445
+ itself" — the question `synapse vocab`'s divergence table asks. But the value another file's own
446
+ dependency declaration actually names it by can be a *different* string on the same nearest-ancestor
447
+ file — a build system's internal name versus the name it publishes for dependents to reference, for
448
+ instance. If a real ambiguous-reference finding traces to exactly this mismatch (the candidate's
449
+ declared library never matches, confirmed by checking the real file both values come from, not
450
+ assumed), the namespace rule's `"aliases"` array (each entry its own `prefix`/`terminator` against
451
+ the same file, unbounded) is the fix — verify the new value against the same real file before
452
+ writing it back, same discipline as everything else here.
453
+
454
+ **Self-population needs the same hard-requirement trigger the tags.scm-generation step already
455
+ uses — announced every time an ambiguous reference is met in a language with no existing rule yet,
456
+ not a silently-skippable mention.** State plainly which of four outcomes happened:
457
+
458
+ 1. A rule already existed for this extension — used as-is.
459
+ 2. No rule existed, one was written and verified against a real file in this repo (paste the
460
+ confirmed prefix/terminator match) — then committed to `synapse-dependency-rules.conf`.
461
+ 3. No rule existed, writing one was attempted and skipped because verification failed against a
462
+ real file — state what was tried and why it didn't hold.
463
+ 4. The language doesn't qualify at all per the spec-check test above — say so explicitly, don't
464
+ silently produce nothing and move on.
465
+
466
+ A confident read of a language's import syntax that predicts a rule "would obviously work" is not a
467
+ substitute for verifying it against one real file in the repo being oriented in, the same reason the
468
+ tags.scm-generation step's own announcement can't be skipped by a correct-looking source reading.
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: synapse-query
3
+ description: This repo has a Synapse code graph. For ANY task here — understanding how something works, finding where code lives, tracing what depends on a file, or scoping an edit — consult synapse/{project}/Index.md and synapse query before grepping or reading source files. Grep only after Synapse has named the exact file(s) to read.
4
+ ---
5
+
6
+ # Synapse Query: Day-to-Day Use of the Code Graph
7
+
8
+ `synapse-node` covers Tier 2 staleness checking and regeneration. `synapse-task` covers task-note
9
+ status transitions. This skill is neither of those — it's how to actually *use* the graph for an
10
+ ordinary task: understanding a subsystem, finding where something lives, or scoping an edit before
11
+ touching code. If a query below turns up a stale node, hand off to `synapse-node`'s procedure rather
12
+ than reasoning about staleness here.
13
+
14
+ ## Why this exists
15
+
16
+ The Synapse graph has two parts:
17
+
18
+ 1. **The graph itself** — `synapse/{project}/*.md`, one node per subsystem/concept, LLM-authored:
19
+ a plain-English summary, a crux (the few lines that actually carry the logic), typed relations to
20
+ other nodes (`depends_on`/`part_of`/`uses`/etc, as the node's own data), and an exhaustive
21
+ `sources` list. A broad metadata layer for narrowing down *where* to look — not a replacement for
22
+ reading code, a map to it.
23
+ 2. **`synapse`** — one binary whose subcommands query and extract from part 1 cheaply. Nothing here
24
+ is a daemon or a server; every subcommand reads files on disk and exits. (`synapse-hook` is a
25
+ second binary carrying the Claude Code hooks; it is registered in `settings.json`, never run by
26
+ hand.)
27
+
28
+ **The point of this graph is to make grep the last resort, not a peer option.** Consult Synapse
29
+ first to learn *where* something lives; a node's `sources` (or `crux_path`, if it has one) then
30
+ names the exact file(s) to fetch; grep only re-enters at that point, scoped to a file Synapse
31
+ already named, to locate a specific detail inside it — never as an unscoped repo-wide search run
32
+ instead of asking Synapse first. If you catch yourself about to grep the whole repo before checking
33
+ `synapse/{project}/Index.md`, stop and check the index instead.
34
+
35
+ **Why the cost difference is real, not just tidiness.** `synapse query body <node>` never goes
36
+ through Obsidian's API — it's a direct disk read that extracts only the prose between the generated
37
+ fences, skipping the node's `sources` list entirely. On a hub node, going through the API instead
38
+ would move that node's entire frontmatter — megabytes — to print a few hundred words. On a large
39
+ repo (dozens to hundreds of thousands of tracked files), that difference is the entire reason a
40
+ query stays cheap instead of dominating the turn.
41
+
42
+ ## The tools
43
+
44
+ Not "what's missing versus other code-graph tools" — every capability you'd expect already maps to
45
+ something that exists, composed from Synapse plus what Claude Code already has, no new binary
46
+ required:
47
+
48
+ | Need | Reach for | Why |
49
+ |---|---|---|
50
+ | "Where does X live?" (ranked, natural-language) | `mcp__obsidian__search_simple`/`search_query` over the vault, plus a first read of `synapse/{project}/Index.md` | Full-text, relevance-ranked. Not semantic ranking, but genuinely comparable for locating a concept. |
51
+ | Every occurrence of a pattern | native `grep`/`rg`, **scoped to a file Synapse already named** | Not a repo-wide first move — the deterred, last-resort case. See "Why this exists" above. |
52
+ | A file's API surface | read the file directly | Claude already has direct, cheap filesystem access — no separate view needed. |
53
+ | Who depends on a subsystem, or what it depends on | `synapse query links "{Node}" --inbound` / `--closure` | Real transitive-closure traversal over the typed relations, at node granularity. |
54
+ | **Who calls this method/class, repo-wide** | `synapse callers <name>` | Every call site as `path:line ⇥ calling expression`, off the flat index `synapse build-refs` projects from the tags cache. Well under a second even against a multi-gigabyte index. Needs **no node and no graph** — it works in a repo `/synapse-init` has never touched, as long as the cache is filled. Still name-based rather than type-resolved, so hits are candidates with evidence: the calling expression is on the line, which usually settles the receiver without opening the file. |
55
+ | One frontmatter scalar (`stale`, `built_at`, `commit`, ...) | `synapse query field "{Node}" <key>` | Cheap, targeted extraction — never reads the rest of the node. |
56
+ | A node's prose, without its (possibly huge) `sources` list | `synapse query body "{Node}"` | Disk read, never the API; skips frontmatter and `## Notes`. See the cost note above. |
57
+ | Every file a node covers | `synapse query sources "{Node}" [--count\|--modules\|--filter <p>]` | Filtered/counted/grouped, never the raw megabyte-scale list. |
58
+ | Is this node's understanding still accurate? | `synapse query stale` / `drift` / `grounding` | Hand off to the `synapse-node` skill's procedure — this skill doesn't re-explain that. |
59
+
60
+ ## Usage scenarios
61
+
62
+ | When you're... | Reach for | Not |
63
+ |---|---|---|
64
+ | Orienting on an unfamiliar repo | `synapse/{project}/Index.md`, then the relevant node's `body` | grepping around to build a mental map by hand |
65
+ | Understanding a flow ("how does X work") | `synapse query body "{Node}"` for the node that covers it | reading every file the flow touches, cold |
66
+ | Finding where a change belongs | `search_simple`/`Index.md` to find the owning node, then that node's `sources`/`crux_path` for the exact file(s) | a repo-wide grep for a guessed symbol name |
67
+ | Judging blast radius before an edit | `synapse query links "{Node}" --inbound` (or `--closure` for transitive) | assuming nothing else depends on it |
68
+ | You already know the exact file and line range | just fetch it (`sed`, or a direct file read) | asking Synapse a question you can already answer |
69
+ | Finding every occurrence of a literal pattern | native `grep`/`rg`, scoped to files Synapse already named | an unscoped repo-wide grep before consulting Synapse at all |
70
+ | A node turns up `stale` | hand off to the `synapse-node` skill | trying to reason about staleness inline here |
71
+
72
+ ## When a node isn't enough
73
+
74
+ Synapse has no exact per-symbol call graph — a node's answer is a concept-level summary, not a
75
+ parse. This matters more here than it would for a tool that does have one: short, reused names
76
+ (common in languages that don't require import qualification for same-package references) can't
77
+ always be disambiguated from a node alone. When a node's `crux`/`grounded_in`/`sources` don't
78
+ resolve the specific symbol-level question:
79
+
80
+ 1. Fetch the file(s) the node already named via `sources` or `crux_path` — not a fresh search.
81
+ 2. Grep or read within that known file for the specific detail. This is the expected, first-class
82
+ fallback, not a rare edge case — treat it as normal, not as Synapse having failed.
83
+ 3. If the file Synapse named doesn't exist on disk, the graph is ahead of your checkout (a branch
84
+ switch or an unpulled move) — don't read a missing file; the `synapse-node` skill's staleness
85
+ procedure is what to run, not a workaround here.
86
+
87
+ ## Guardrails
88
+
89
+ - **Never grep the whole repo before checking `synapse/{project}/Index.md`**, unless this repo has
90
+ no Synapse namespace at all (`vault_list` on `synapse/{project}/` comes back empty — in that case
91
+ there's nothing to consult, say so and proceed normally).
92
+ - **Never treat a node's summary as ground truth for a symbol-level claim** it wasn't built to make
93
+ precisely — see "When a node isn't enough" above.
94
+ - **Never `vault_read` a node just to read its prose.** That pulls the full frontmatter, which can
95
+ run to megabytes on a hub node. Use `synapse query body` (see the cost note above).
96
+ - **Never reason about a node's staleness inline in this skill.** Hand off to `synapse-node`'s
97
+ procedure — that's its job, not this skill's.
98
+ - **Never treat `synapse query`'s exit 1 as "clean."** It means the check could not run (no
99
+ vault, no namespace for this repo, a `remote:` mismatch) — not that the graph verified fine.