@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,196 @@
1
+ ---
2
+ description: Create a plain note in Synapse Vault (bare mode — title + frontmatter, category resolved from Index.md), a tracked task note (--task, scaffolds the checklist skeleton synapse-task expects), list every tracked task (--list), or search existing notes (--search) before creating a new one. Use for a note with no design framing, or any task note not compiled from a design discussion. Not for starting/continuing a design conversation (that's synapse-design-note) or compiling a Ready one into a task (that's synapse-task-note) — both of those delegate to this command themselves.
3
+ ---
4
+
5
+ Create a note in Synapse Vault with the title and options, list existing notes, or search existing notes: $ARGUMENTS
6
+
7
+ ## Argument parsing
8
+
9
+ If `$ARGUMENTS` is `--list` (or starts with `--list`) → **list mode**: see "List mode" below, skip note creation entirely.
10
+
11
+ If `$ARGUMENTS` starts with `--search` → **search mode**: see "Search mode" below, skip note creation entirely. This is also the mode to reach for programmatically (not just when the user explicitly asks to search) — per the Synapse Vault CLAUDE.md instructions, linking to an existing note is the highest-priority step before creating a new one, so run a search here before every bare-mode note creation, not only when a search is requested outright.
12
+
13
+ Otherwise, split `$ARGUMENTS` on `--task`:
14
+
15
+ - If `--task` is present → **task mode**: scaffold the note as a tracked task, following the `synapse-task` skill's conventions. Task notes always live under `tasks/`.
16
+ - Otherwise → **bare mode**: create an empty node (title + frontmatter only). Which category folder it lands in is resolved from `Index.md`, per "Choosing a category (bare mode only)" below — not a fixed set.
17
+
18
+ The title is everything before `--task` (trimmed). Example:
19
+
20
+ - `/synapse-note "My idea"` → bare note titled "My idea"
21
+ - `/synapse-note "proj-035 — Implement Foo" --task` → task note titled "proj-035 — Implement Foo"
22
+
23
+ In task mode, also attempt to extract a task ID from the title by matching a `{prefix}-\d+` pattern (letters, a hyphen, then digits). Use it as `task_id` in frontmatter.
24
+
25
+ If no match is found, **don't just leave it blank** — see "Resolving a missing task ID" below before proceeding.
26
+
27
+ ## List mode
28
+
29
+ Use `mcp__obsidian__search_query` with the JsonLogic query `{"var": "frontmatter.task_id"}` — this returns every file that has a `task_id` set, along with that file's `task_id` value as `result`. For each match, also read the file's `status` and `title` (either via a second query `{"var": "frontmatter.status"}` / `{"var": "frontmatter.title"}`, or via `mcp__obsidian__vault_read` on the handful of matched files — whichever is fewer round-trips for the count involved).
30
+
31
+ Categorize:
32
+ - **Prefixed notes**: `task_id` matches `{prefix}-\d+`. Group by the distinct prefix found (whatever
33
+ prefixes actually appear — don't assume a fixed set). Within each prefix, split further into:
34
+ - **Open**: `status` is `TODO`, `IN-PROGRESS`, or `REVIEW` (or missing — treat as open)
35
+ - **Closed**: `status` is `DONE`, `CANCELED`, or `CANCELLED`
36
+ - **Other notes**: no `task_id`, or one that doesn't match `{prefix}-\d+`
37
+
38
+ Sort each prefix group's notes numerically by task id (`{prefix}-9` before `{prefix}-10`); sort other notes alphabetically by title. Report one section per prefix found — "{prefix} notes — open", "{prefix} notes — closed" — plus "Other notes", each line as `{task-id or filename} — {title} [{status}]`. Omit a section header if it has zero entries. End with a total count.
39
+
40
+ Do not modify any files in list mode.
41
+
42
+ ## Search mode
43
+
44
+ Everything after `--search` (trimmed, quotes stripped) is the query.
45
+
46
+ 1. Run `mcp__obsidian__search_simple` with the query — this gives full-text relevance-ranked matches with context, the closest equivalent to a title/body search.
47
+ 2. If the query looks like it's targeting metadata specifically (a tag, a task ID, a status value) rather than free text, also run `mcp__obsidian__search_query` with an appropriate JsonLogic filter (e.g. `{"==": [{"var": "frontmatter.task_id"}, "proj-032"]}`).
48
+ 3. Report matches as `{title} — {file path relative to vault root}`, deduped across both. If nothing matches, say so plainly — the caller (agent or user) needs a clear "no existing note" signal to proceed with `--task`-less creation.
49
+
50
+ Do not modify any files in search mode.
51
+
52
+ ## Resolving a missing task ID (task mode only)
53
+
54
+ Triggered when `--task` is given but the title doesn't match `{prefix}-\d+`.
55
+
56
+ The known project/prefix pairs live in a plain local file named `synapse-projects.conf` (one
57
+ `project-name=prefix` line each) — read/appended with the Read/Edit tools, not the `obsidian`
58
+ MCP server, since it's outside the vault. It is deliberately **not** part of the portable
59
+ Synapse package and never copied between machines, so contexts that shouldn't mix (e.g.
60
+ personal vs. work projects) never end up in the same file. It's self-managed — this command appends
61
+ newly resolved pairs to it — but also plain text, so the user can add, fix, or remove a line by hand
62
+ at any time.
63
+
64
+ Resolve *which* file that is with the same tiered lookup every `synapse-*.conf` file uses for
65
+ reading: first `$XDG_CONFIG_HOME/synapse/synapse-projects.conf` if `$XDG_CONFIG_HOME` is set, else
66
+ `~/.config/synapse/synapse-projects.conf`; then `~/.claude/synapse-projects.conf`. Read whichever of
67
+ those exists first. Append a newly resolved pair (step 5 below) to that same file.
68
+
69
+ If neither exists yet, decide where to create it fresh: `$XDG_CONFIG_HOME/synapse/synapse-projects.conf`
70
+ if `$XDG_CONFIG_HOME` is set; else `~/.config/synapse/synapse-projects.conf` if `~/.config` already
71
+ exists as a directory on this machine (it's adopted XDG conventions for other tools even without ever
72
+ setting the env var); else `~/.claude/synapse-projects.conf` as the final fallback — today's default,
73
+ unchanged for anyone who has never touched an XDG config directory.
74
+
75
+ 1. Identify the current project from context: the repo's `CLAUDE.md` (title/"About" section) or
76
+ `git remote`.
77
+ 2. Check the resolved `synapse-projects.conf` for a line whose project name matches (loosely —
78
+ case/whitespace-insensitive). If found, use that prefix directly — no need to ask.
79
+ 3. If the file doesn't have it yet, fall back to deducing from the vault itself (useful the first
80
+ time this runs, or for a project whose notes predate this file): `search_simple` for the
81
+ project/repo name across existing notes, and/or `search_query` on
82
+ `{"var": "frontmatter.task_id"}` to see which prefixes exist, then check whether any of those
83
+ prefixed notes reference this project. If exactly one prefix confidently matches, use it.
84
+ 4. If nothing confidently matches (new project, or an ambiguous/multiple match), ask the user
85
+ directly: "What's the project prefix for this task?" — plain free-text, not a multiple-choice
86
+ list. Don't offer or hint at any other project's prefix as an option.
87
+ 5. Whenever step 3 or step 4 resolves a pair not already in the conf file (including a fresh
88
+ `project-name=prefix` line matching what was just deduced or asked), append it — so the next task
89
+ for this project resolves from step 2 without a search or a question.
90
+ 6. Once the prefix is known, find the next number: run
91
+ `mcp__obsidian__search_query` with `{"var": "frontmatter.task_id"}`,
92
+ filter the returned `result` values client-side for ones matching
93
+ `{prefix}-\d+`, take the highest number found, add 1. If none exist yet
94
+ for that prefix, start at 1.
95
+ 7. Format the new task ID **zero-padded to 3 digits** (`{prefix}-001`, `{prefix}-030`,
96
+ `{prefix}-037`, ...), matching the org-roam-era convention — widening
97
+ naturally past 3 digits if a prefix ever needs it.
98
+ 8. **Prepend the resolved task ID to the title itself** — the final title
99
+ becomes `{task-id} — {original title}` (em dash). Use this same final
100
+ title for both the `title` frontmatter field and the `# ` heading, and
101
+ use the resolved task ID for `task_id`. Don't let the frontmatter task
102
+ ID and the visible title disagree.
103
+
104
+ `/synapse-design-note`/`/synapse-task-note` read the same conf file directly for the same reason —
105
+ they don't duplicate this resolution logic, just this file.
106
+
107
+ ## Choosing a category (bare mode only)
108
+
109
+ Task mode always uses `tasks/` — skip this step entirely in task mode.
110
+
111
+ In bare mode, ask the user which category the note belongs to. Read
112
+ `Index.md`'s folder list first — every top-level folder listed there
113
+ except `designs`/`tasks`/`synapse` (structurally fixed, not a bare-mode
114
+ destination — see `synapse-claude.md`'s Folders bullet) is a candidate
115
+ category, offered with that folder's own `Index.md` description as the
116
+ option's description. Don't hardcode a fixed option set: a fresh vault's
117
+ `Index.md` lists `research`/`scratchpad`/`inbox` (see
118
+ `${CLAUDE_PLUGIN_ROOT}/Index.md.template`), but a vault owner's own `Index.md` may have
119
+ renamed or restructured these, and whatever it currently says is what gets
120
+ offered.
121
+
122
+ Resolve this to a `category` matching the folder name exactly as
123
+ `Index.md` currently spells it, before moving on to the creation steps
124
+ below. No project-slug question is needed here — Obsidian filenames are
125
+ the title itself, not a slug-prefixed timestamp, so there's no separate
126
+ namespacing concern to resolve. The note always lands flat at
127
+ `{category}/{filename}.md` — never inferred into a subfolder such as a
128
+ triage/priority one a vault owner might maintain by hand (e.g.
129
+ `inbox/{high,medium,low}/`, per that folder's own `Index.md` description);
130
+ sorting a note into one of those, if a category has one, is never an
131
+ agent's call to make.
132
+
133
+ ## Resolving the project folder (task mode only)
134
+
135
+ Task notes are grouped one level deeper by project, `tasks/{project}/{filename}.md` — see
136
+ `Index.md`'s `tasks/` section. The prefix (`proj`, `sb`, ...) is not itself the folder name — it
137
+ names the *task*, not the *project* — so resolve what project it belongs to, regardless of how
138
+ the prefix became known (matched from the title, resolved in "Resolving a missing task ID" above,
139
+ or supplied directly by a caller like `/synapse-task-note`):
140
+
141
+ 1. Reverse-lookup the prefix in the resolved `synapse-projects.conf` (same tiered lookup as
142
+ "Resolving a missing task ID" above) — find the line whose value after `=` equals the prefix;
143
+ its key is the project name.
144
+ 2. If no line matches, check `Index.md`'s `tasks/` section, which documents the prefix-to-project
145
+ mapping directly (e.g. `proj-NNN` → `widget`).
146
+ 3. If still unresolved (a genuinely new prefix with no mapping anywhere), ask the user for the
147
+ project name and append `{project-name}={prefix}` to the conf file — so the next task note
148
+ under this prefix resolves without asking.
149
+
150
+ ## Creating the note
151
+
152
+ 1. Sanitize the title into a filename: replace filesystem-illegal
153
+ characters (`/ : * ? " < > |`) with `-`, collapse repeated whitespace.
154
+ No timestamp prefix, no project-slug prefix — the filename is just the
155
+ (sanitized) title.
156
+ 2. Fetch machine local time: `date '+%Y-%m-%d %H:%M'` — never use inferred
157
+ time. Use this for the `created` frontmatter field.
158
+ 3. Build the file content:
159
+
160
+ **Bare mode:**
161
+ ```
162
+ ---
163
+ title: "{title}"
164
+ created: "{now}"
165
+ ---
166
+
167
+ ```
168
+
169
+ **Task mode:**
170
+ ```
171
+ ---
172
+ title: "{title}"
173
+ created: "{now}"
174
+ task_id: {task-id}
175
+ status: TODO
176
+ last_updated: "{now}"
177
+ ---
178
+
179
+ # {title}
180
+
181
+ ## Notes
182
+
183
+ ```
184
+ 4. Write it with `mcp__obsidian__vault_write`. Task mode: path
185
+ `tasks/{project}/{filename}.md` (project resolved in "Resolving the
186
+ project folder" above). Bare mode: path `{category}/{filename}.md`
187
+ (category resolved above).
188
+
189
+ ## Confirm
190
+
191
+ Report the file path back to the user.
192
+
193
+ - Bare mode: note that the note is intentionally near-empty.
194
+ - Task mode: note the task ID extracted or resolved, and remind the user
195
+ to populate the `## Notes` section and checklist before starting work,
196
+ per the `synapse-task` skill.
@@ -0,0 +1,314 @@
1
+ ---
2
+ name: synapse-rebuild-diff
3
+ description: Manually bring a repo's Synapse namespace back in line after major same-branch drift — a pull, a rebase, or a long absence. Triages each drifted node into reseat / patch-from-diff / re-orient rather than rebuilding everything. Refuses outright on a cross-branch mismatch; for a full rebuild from scratch, use /synapse-rebuild-full instead.
4
+ ---
5
+
6
+ # Synapse Rebuild Diff: Reconcile a Namespace After Same-Branch Drift
7
+
8
+ `/synapse-init` builds a namespace. The two staleness tiers and `synapse query drift` *detect*
9
+ that it has moved. This command is the deliberate, human-invoked repair for the case where enough has
10
+ moved that lazy per-read regeneration is the wrong instrument.
11
+
12
+ **Same-branch only.** Every scenario below happens on the branch the namespace already describes —
13
+ a pull, a rebase, time passing, your own hand-written commits. None of them involve the current
14
+ checkout being on a *different* branch than the namespace's own recorded `branch:` field. If it is,
15
+ this command refuses outright rather than attempting a diff — see the branch-identity check under
16
+ Prerequisites. Comparing one branch's tree against another isn't drift, it's just two unrelated
17
+ states, and none of the triage classes below (reseat / patch / re-orient) were built for that.
18
+
19
+ ## When to run it
20
+
21
+ Manually, when you already expect major drift:
22
+
23
+ - **A plain `pull`** that landed a meaningful number of commits — fast-forward, same branch throughout.
24
+ - **A `pull --rebase`** — your local commits get new SHAs, so `synapse query drift` will likely
25
+ report the baseline as "not an ancestor of HEAD". That's expected here, not a sign of anything
26
+ wrong: you were on the same branch the whole time, only its history got rewritten.
27
+ - **A large merge landing in your current branch** — another branch's tip merged in via `git merge`.
28
+ Still same-branch throughout: `HEAD` gains a merge commit, it doesn't move to a different branch.
29
+ - **A long absence** — weeks or months of other people's commits landed while you were elsewhere, on
30
+ the same branch.
31
+ - **You wrote a lot of code by hand.** The plainest case and probably the most common: days of ordinary
32
+ work in your own branch, in your own editor, with no model involved. Tier 1 only fires on
33
+ `Write`/`Edit`/`MultiEdit` *in this session*, so none of it was flagged as it happened. `stale` will
34
+ still catch content changes to files a node already claims — but it reports a rename as "gone", and a
35
+ **newly added file it cannot see at all**, because a path in no node's `sources` has nothing to
36
+ compare against. Real feature work adds files, so this is exactly where the graph goes quietly out of
37
+ date. Same applies to anything else that bypasses the session: an IDE refactor (which produces both
38
+ bad cases at once — renames *and* new paths), a `sed -i`, generated code that the build rewrote from a
39
+ schema, a dependency bump, or a moved submodule pointer.
40
+
41
+ Do **not** run it after an ordinary pull. Tier 1 flags what this session edited, the `synapse-node`
42
+ skill regenerates a node lazily when its body is actually needed, and `synapse query drift` is the
43
+ cheap check that tells you whether anything more is warranted. This command exists for when the answer
44
+ is clearly yes.
45
+
46
+ **A large job is the expected outcome, not a warning sign.** On a monorepo with a hundred thousand
47
+ files and heavy traffic, most of the graph moving at once is simply what the situation looks like, and
48
+ forty nodes in *re-orient* is a normal shape for this command rather than a reason to hesitate. Nothing
49
+ invokes this automatically — a human typed it, knowing their own repo and why they are here. So report
50
+ the size, then **do the work**. Do not recommend against a rebuild on the grounds that it is expensive,
51
+ do not offer a reduced version of it unasked, and do not describe replacing the graph as destructive:
52
+ replacing it is the entire point. Volunteer a smaller option only where a *correctness* reason argues
53
+ for one, and even then do the full job if the human says so.
54
+
55
+ **One mechanical fact about branches, because it is not guessable.** A namespace is keyed by repo
56
+ *and branch* (`synapse/{repo}@{branch}/`), so each branch has its own or has none. A branch switch
57
+ therefore no longer invalidates anything: the graph you built on the mainline stays intact and keeps
58
+ describing the mainline, and the branch you switched to simply has no namespace until someone runs
59
+ `/synapse-init` there. That is an ordinary state, not a problem to fix.
60
+
61
+ So the massive-drift case this command exists for is now the *unusual* one rather than the norm. It
62
+ still happens — a branch can be checked out inside any worktree, and a long-lived branch gets rebased
63
+ onto a moved trunk, which leaves the recorded baseline off the current line exactly as a branch switch
64
+ used to. Read a "not an ancestor of HEAD" warning as "history moved under this graph", and reach for
65
+ this command when it does. What no longer happens is arriving here merely because you changed branch.
66
+
67
+ ## Prerequisites
68
+
69
+ - The namespace must exist. If `synapse/{repo}@{branch}/Index.md` is absent, this is a first build — use
70
+ `/synapse-init`.
71
+ - **Branch-identity check — hard stop, run this before anything else, including drift/grounding.**
72
+ Compare the current checkout's branch against the namespace's own recorded `branch:` frontmatter
73
+ field:
74
+ ```sh
75
+ current_branch="$(git symbolic-ref --short HEAD)"
76
+ ns_branch="$(grep -m1 '^branch:' "synapse/{repo}@{branch}/Index.md" \
77
+ | sed -e 's/^branch: *//' -e 's/^"//' -e 's/"$//')"
78
+ ```
79
+ If they don't match, **refuse immediately** — do not run `synapse query drift`, do not read
80
+ anything else. Say plainly that this namespace describes a different branch than the current
81
+ checkout, and point at `/synapse-init` (if the current branch has no namespace of its own) or at
82
+ checking out the branch/worktree the namespace actually describes. This is a distinct, harder check
83
+ than the "baseline is not an ancestor of HEAD" ancestry signal below — that one is a *soft*,
84
+ informational finding (expected after a same-branch `pull --rebase`); this one is a hard refusal,
85
+ because it is not this checkout's namespace to diff at all. Never conflate the two: a non-ancestor
86
+ baseline on a branch-identity match still proceeds normally, per the "One mechanical fact about
87
+ branches" section above.
88
+ - The work directory (`$SYNAPSE_WORK_DIR`, default `~/.claude/synapse-work/{repo}@{branch}/`) ideally
89
+ holds the `manifest.tsv` from the original build. Without it, new paths cannot be classified as
90
+ auto-claimable, and clustering decisions have to be re-derived — say so rather than proceeding as if
91
+ nothing were missing. `synapse/{repo}@{branch}/_manifest.tsv` is the fallback copy.
92
+ - Read `synapse/{repo}@{branch}/_profile.txt` if it exists, before triaging anything. It records the
93
+ aggregations that carried signal for this repo and the searches that came back empty.
94
+
95
+ ## Procedure
96
+
97
+ ### 1. Size the job before doing any of it
98
+
99
+ ```sh
100
+ ~/.synapse query drift
101
+ ~/.synapse query grounding
102
+ ```
103
+
104
+ Report what it says, in the human's terms, **before** touching anything: how far the baseline is from
105
+ HEAD (and whether it is an ancestor at all), how many nodes are flagged in each class, and how many
106
+ added paths need a decision. Silence means nothing to rebuild — say so and stop, including when the
107
+ repo is behind its upstream: drift prints that only alongside a finding, because an accurate graph
108
+ plus unpulled commits is nothing to repair yet.
109
+
110
+ Two answers change the plan:
111
+
112
+ - **"baseline … is not an ancestor of HEAD"** — a branch switch or a reset. The file-level diff is
113
+ still exactly right (it compares trees, not history), but expect deletions to dominate: files that
114
+ exist on the built line and simply are not here.
115
+ - **"no commit recorded"** or **"baseline … not in local history"** — those nodes cannot be diffed at
116
+ all. They go straight to the *re-orient* class in step 3; there is no cheaper option for them.
117
+
118
+ ### 2. Mechanical phase — always, and cheap
119
+
120
+ ```sh
121
+ ~/.synapse build-lists --reenumerate
122
+ ```
123
+
124
+ `--reenumerate` matters here: without it an existing `all.txt` is reused, so a branch switch would be
125
+ invisible to enumeration. **Read the coverage report.** On a branch switch, per-node list sizes will
126
+ move a lot and some may reach zero.
127
+
128
+ - **A node whose list is now empty** means that subsystem does not exist on this branch. **Do not
129
+ write it** — `synapse write-node` refuses an empty path list, and that refusal is correct. Report
130
+ the node and leave it in place, untouched. **Never delete a node to tidy up a branch switch:**
131
+ `## Notes` is human-authored, lives outside the generated fence, and is unrecoverable.
132
+ - **Unclaimed added paths** are a judgment call: widen an existing manifest line where a path belongs
133
+ to a cluster that already exists, and leave a genuinely new subsystem for a new manifest line and
134
+ its own node. Re-run `synapse build-lists` after editing the manifest, and check coverage again.
135
+
136
+ Then rebuild the reverse index so the hook and the read path agree with the new enumeration:
137
+
138
+ ```sh
139
+ ~/.synapse build-index
140
+ ```
141
+
142
+ ### 3. Triage each flagged node — reseat, patch, or re-orient
143
+
144
+ **Hard requirement for the *re-orient* class, not a recommendation: invoke the `synapse-orientation`
145
+ skill before writing one sentence of new prose for that node.** Reading the node's own source
146
+ directly and reasoning from what you already know about the codebase is not a substitute, no matter
147
+ how accurate or hard-won that knowledge is — a node's premises being re-derived is the same problem
148
+ a first build faces, and it gets the same required technique, every time, not just when nothing
149
+ faster is available. The reason this can't be skipped even by a confident, correct reading of the
150
+ source: the skill's own verification step is to *run* something real (`synapse tags`/`synapse query`
151
+ against a real sample) and judge the actual output, not what the source implies the output should
152
+ be — and only an executed check can surface a gap in the tooling itself, one the source reading alone
153
+ has no way to reveal, however carefully it's done. A node whose own subject is the tagging or
154
+ extraction pipeline is exactly the case most tempting to shortcut this way, and exactly the case
155
+ where doing so costs the most.
156
+
157
+ `synapse-node-format` is the other skill to load before triaging, for every class, not just
158
+ *re-orient*: the node contract — frontmatter, the crux pointer, `## Links`, `grounded_in` — the same
159
+ one `/synapse-init` and the `synapse-node` skill write against.
160
+
161
+ **The principle: compute new prose from the diff, not by re-reading the node's sources.** A node
162
+ covering 15,000 files where 12 changed already has prose encoding the other 14,988. Re-reading it all
163
+ is the expensive mistake this command exists to avoid, and it also throws away hard-won findings the
164
+ diff has nothing to say about.
165
+
166
+ **Take the nodes that lost files first, and keep the full list of deleted paths in front of you for
167
+ every node after that.** A deletion in one node is routinely the other half of an addition in
168
+ another — a type moved from the binary into a library, a module promoted out of a `util`. Triaged in
169
+ drift's arbitrary order, the node that *gained* the file is patched first, with no way to know the
170
+ file came from anywhere, and the patch fills the gap with a guess. One cheap list, read once:
171
+
172
+ ```sh
173
+ git diff --diff-filter=D --name-only -M <commit>..HEAD
174
+ ```
175
+
176
+ **Size each node by changed lines, not by changed files.** Drift reports file counts because that is
177
+ what it can compute without a diff, but a file count saturates immediately in a repo of small
178
+ modules: a node of 9 files with 5 touched reads as 56% when the actual change is 199 lines out of
179
+ 2,077, and every one of them a formatting or import edit. Get the real ratio before choosing:
180
+
181
+ ```sh
182
+ git diff --numstat <commit>..HEAD -- $(tr '\n' ' ' < "$W/lists/NN.txt") \
183
+ | awk '{a += $1; d += $2} END {print a + d}'
184
+ ```
185
+
186
+ against the node's own line count, and use `synapse query sources "{Node}" --count` for the file
187
+ count drift's numbers are relative to. Then pick one of three strategies and **say which one you
188
+ picked and why**:
189
+
190
+ **Restore the crux directive before writing any node back.** `synapse query body` returns the
191
+ *expanded* crux — the fenced code the writer sliced — not the directive that produced it. Writing that
192
+ straight back stores a quote of a file as it looked at the old baseline, presented as if it were
193
+ current. So rebuild the directive from the pointer the writer recorded:
194
+
195
+ ```sh
196
+ ~/.synapse query field "{Node}" crux_path
197
+ ~/.synapse query field "{Node}" crux_lines
198
+ ```
199
+
200
+ and replace the fenced block with `<!-- crux: <crux_path> <crux_lines> -->` so it is cut from the
201
+ current file. One judgment goes with it: re-slicing the same range is only safe if that file did not
202
+ change. If it did, the line numbers may now point at something else entirely — treat the crux as
203
+ needing a fresh pointer, exactly as the prose needs a fresh sentence. A node with no `crux_path` had
204
+ `none`, and stays that way.
205
+
206
+ **The same applies to groundings, and forgetting them loses more.** `grounded_in` is frontmatter and
207
+ its directives are stripped from the body, so a recovered body contains none — write it back as-is and
208
+ the node's provenance is gone with no error. Recover the pointers per node:
209
+
210
+ ```sh
211
+ ~/.synapse query grounding "{Node}" --list # path<TAB>lines
212
+ ```
213
+
214
+ and re-emit a `<!-- grounded_in: <path> <lines> -->` for each. Run `synapse query grounding` before
215
+ triaging: it is cheaper than the diff and sharper than a churn ratio. A **`moved`** line hands you the
216
+ corrected range outright, no reading. A **`changed`** line points at evidence that no longer says what
217
+ the summary claims — which is a better reason to re-read a node than any percentage, because it names
218
+ the sentence at risk rather than the volume of change around it.
219
+
220
+ **Reseat** — renames only, no content change. No reading at all. Recover the existing prose with
221
+ `synapse query body "{Node}"`, drop its trailing `## Sources` block (the writer regenerates that),
222
+ re-enumerate so the list holds the new paths, and write it back. Repeating this is safe: the writer
223
+ trims the body's leading and trailing blank lines, so a reseat is idempotent rather than accreting
224
+ padding each time. The concept did not change; only paths moved. This also
225
+ works on a machine that never built the namespace, because the body came from the node itself rather
226
+ than from a work-dir file.
227
+
228
+ **Patch from the diff** — a small fraction of the node's *lines* changed (rule of thumb: under ~15%),
229
+ and the file its `crux` quotes still exists. Read three things and nothing else:
230
+
231
+ 1. the current prose — `synapse query body "{Node}"`;
232
+ 2. `git diff --name-status -M <commit>..HEAD` restricted to that node's paths, for *which* files moved;
233
+ 3. hunks for a **bounded** selection of those files — always including any file the `crux` quotes.
234
+
235
+ Then amend only the sentences the diff contradicts, and keep everything else verbatim.
236
+
237
+ **A patch may say what the node now contains. It must not say where something came from** unless the
238
+ rename is in the diff it read. Provenance is the one claim a node-restricted diff structurally cannot
239
+ support: the other end of the move is in a different node's paths, so the diff shows an unexplained
240
+ new file, and the plausible local origin is an invention. `git`'s rename detection is not a
241
+ safety net here — a file that moved between modules and was rewritten on the way lands below the
242
+ similarity threshold and shows up as a delete in one node and an add in another even under
243
+ `-M --find-copies-harder`. Which is why deletions get read first, below.
244
+
245
+ **Re-orient** — a large fraction changed, the baseline is unusable, or there is a structural signal:
246
+ the `crux` file is gone or renamed, whole modules entered or left the node, or a package root changed
247
+ name. Here the prose's premises are suspect, so patching would preserve a claim that is no longer
248
+ true. Re-run this node's aggregations from `_profile.txt` (path-level, so cheap even on a hub node),
249
+ read the few load-bearing files the aggregations point at, and re-author as `/synapse-init` would.
250
+
251
+ **The diff must be projected as carefully as `sources` is.** `git diff` with hunks across a hub node's
252
+ paths over hundreds of commits runs to megabytes — the same constraint that makes `sources` unreadable
253
+ applies to the diff. So: `--name-status` for names, `--stat` to size it, and hunks only for a bounded
254
+ selection. Never pipe an unbounded `git diff <commit>..HEAD` into a context window.
255
+
256
+ ### 4. Write each rebuilt node
257
+
258
+ ```sh
259
+ ~/.synapse write-node --title "{Node}" --summary "{one line}" \
260
+ --paths "$W/lists/NN.txt" --body "$W/body.md"
261
+ ```
262
+
263
+ It re-records `commit`, so this checkout becomes the node's new baseline — which is what makes the next
264
+ `drift` meaningful. **Re-check the one-line `summary`**: after a branch switch it can be wrong in kind,
265
+ not merely stale, if the subsystem's shape differs on this line.
266
+
267
+ ### 5. Rebuild the projections and verify
268
+
269
+ ```sh
270
+ ~/.synapse build-index
271
+ ~/.synapse build-project-index
272
+ ~/.synapse query drift # expect silence
273
+ ~/.synapse query stale # expect silence
274
+ ~/.synapse query grounding # expect silence: re-pointed, not dropped
275
+ ~/.synapse query links --check # expect silence: no dangling targets
276
+ ```
277
+
278
+ `links --check` covers what used to be a manual instruction here: a broken `[[wikilink]]` is a valid
279
+ link to a not-yet-existing note, so Obsidian renders it without complaint and nothing else in the
280
+ system notices. It now reports `Node<TAB>relation -> Target (no such node)` per dangling edge.
281
+
282
+ One check is still yours, because no command performs it: **every node file appears in `Index.md`**. An
283
+ unlisted node exists and is reachable by search, but is invisible to anyone reading the map.
284
+
285
+ ### 6. Report what happened
286
+
287
+ Per node: the strategy chosen and why. Plus what was deliberately left alone — nodes drift did not
288
+ flag, and nodes whose sources vanished on this branch. A rebuild that silently re-authored forty nodes
289
+ is indistinguishable, from the outside, from one that did nothing.
290
+
291
+ ## Guardrails
292
+
293
+ - **Never `pull`, `fetch --prune`, `rebase`, `reset` or `checkout`.** The human chose this checkout;
294
+ this command describes and records it. Report how far behind the upstream ref is and stop there.
295
+ - **Never re-read a node's full sources to patch a small diff.** That is the specific waste this
296
+ command is built to avoid.
297
+ - **Know what patching cannot fix.** It keeps every sentence the diff does not contradict, so a claim
298
+ that was wrong when the node was *built* survives every future patch untouched — the diff has
299
+ nothing to say about a statement that was never true. Patching is therefore only as good as the
300
+ baseline prose, and a node's most likely error is not drift but an explanation invented at build
301
+ time. The `crux` is no longer the exposure it was — `synapse write-node` slices it out of the
302
+ file from a `<!-- crux: path start-end -->` directive, so it is verbatim by construction rather than
303
+ by instruction. What remains unguarded is the prose. A sentence asserting a *mechanism* ("X is
304
+ behind a mutex, which is why Y")
305
+ deserves more suspicion than one asserting structure — if the diff touches its file at all, verify
306
+ it rather than carrying it over.
307
+ - **Never write a node with an empty path list**, and never delete a node whose sources vanished — its
308
+ `## Notes` is human-authored and outside the generated fence.
309
+ - **Never hand-write frontmatter or the `## Sources` mirror.** `synapse write-node` owns them; doing
310
+ it by hand cannot scale to a hub node and silently drops `summary` and `commit`.
311
+ - **Never rebuild a node drift did not flag.** Regeneration has real cost and it is not free of risk —
312
+ each rewrite is a chance to lose a good sentence.
313
+ - **Say when the graph now describes a different branch than it did before**, in the final report. That
314
+ fact outlives the session, and the next reader has no other way to know.