@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.
- package/Index.md.template +23 -0
- package/bin/synapse-hook.cjs +19 -0
- package/bin/synapse-setup.cjs +420 -0
- package/bin/synapse.cjs +20 -0
- package/commands/synapse-design-note.md +229 -0
- package/commands/synapse-init.md +354 -0
- package/commands/synapse-note.md +196 -0
- package/commands/synapse-rebuild-diff.md +314 -0
- package/commands/synapse-rebuild-full.md +152 -0
- package/commands/synapse-status.md +144 -0
- package/commands/synapse-task-note.md +133 -0
- package/commands/synapse-vault-tidy.md +187 -0
- package/harness/claude/hooks.json +54 -0
- package/harness/codex/hooks.json +54 -0
- package/harness/codex/skills/synapse-design-note/SKILL.md +236 -0
- package/harness/codex/skills/synapse-init/SKILL.md +354 -0
- package/harness/codex/skills/synapse-note/SKILL.md +212 -0
- package/harness/codex/skills/synapse-rebuild-diff/SKILL.md +315 -0
- package/harness/codex/skills/synapse-rebuild-full/SKILL.md +149 -0
- package/harness/codex/skills/synapse-status/SKILL.md +146 -0
- package/harness/codex/skills/synapse-task-note/SKILL.md +133 -0
- package/harness/codex/skills/synapse-vault-tidy/SKILL.md +187 -0
- package/harness/opencode/plugin/synapse.js +164 -0
- package/lib/obsidian-mcp-refresh.cjs +303 -0
- package/lib/resolve-binaries.cjs +54 -0
- package/package.json +26 -0
- package/skills/synapse-node/SKILL.md +211 -0
- package/skills/synapse-node-authoring/SKILL.md +188 -0
- package/skills/synapse-node-format/SKILL.md +205 -0
- package/skills/synapse-orientation/SKILL.md +468 -0
- package/skills/synapse-query/SKILL.md +99 -0
- package/skills/synapse-task/SKILL.md +261 -0
- package/skills/synapse-vault/SKILL.md +107 -0
- package/synapse-claude.md +220 -0
- package/synapse-fence-languages.conf.template +24 -0
- package/synapse-ignore-files.conf.template +45 -0
- package/synapse-module-boilerplate.conf.template +24 -0
- package/synapse-projects.conf.template +14 -0
- package/synapse-prompt-stopwords.conf.template +594 -0
- 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.
|