@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,188 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: synapse-node-authoring
|
|
3
|
+
description: How to write every node's prose in a build — sequentially by default, or fanned out to a configurable pool of concurrent subagents. Covers pool-size resolution from synapse.conf, the synapse brief data file each author reads, dispatch/verify/retry mechanics, and the standing contract every author works under. Use at /synapse-init's node-authoring step, or anywhere else a batch of nodes needs fresh prose (e.g. /synapse-rebuild-diff's re-orient class).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Writing every node's prose: sequential by default, pooled on request
|
|
7
|
+
|
|
8
|
+
Loaded wherever a *batch* of nodes needs prose written, not a single lazy regeneration (that
|
|
9
|
+
is the `synapse-node` skill's job, one node, triggered by a stale read). Today's caller is
|
|
10
|
+
`/synapse-init`'s node-authoring step. `/synapse-rebuild-full` inherits this for free — it
|
|
11
|
+
delegates to `/synapse-init`'s procedure by reference rather than repeating it.
|
|
12
|
+
`/synapse-rebuild-diff`'s *re-orient* class is a natural second caller (see the design's own
|
|
13
|
+
Open Questions and the vault's `inbox/wire synapse-rebuild-diff's re-orient class onto the
|
|
14
|
+
parallel-authoring skill` note) but is not wired to this skill yet — it still
|
|
15
|
+
gathers its facts ad hoc rather than through `rank --lists`/`link-graph`, and needs that fixed
|
|
16
|
+
first.
|
|
17
|
+
|
|
18
|
+
This skill owns two things: **deciding how many authors run at once**, and **the standing
|
|
19
|
+
contract every author works under**, whichever pool size is in play. Which nodes need
|
|
20
|
+
authoring, and how each node's facts are computed (`build-lists`, `rank --lists`,
|
|
21
|
+
`build-refs` + `link-graph`), stays the caller's job — those steps run before this skill is
|
|
22
|
+
ever loaded.
|
|
23
|
+
|
|
24
|
+
## 1. Resolve the pool size
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pool="${SYNAPSE_AUTHOR_POOL:-}"
|
|
28
|
+
if [ -z "$pool" ]; then
|
|
29
|
+
pool="$(grep -m1 '^SYNAPSE_AUTHOR_POOL=' ~/.claude/synapse.conf 2>/dev/null | cut -d= -f2- | tr -d '"')"
|
|
30
|
+
fi
|
|
31
|
+
pool="${pool:-0}"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Environment variable wins over the conf file, same precedence every other Synapse setting
|
|
35
|
+
uses. Absent, empty, or malformed all fall through to `0`. **This is read by you, the
|
|
36
|
+
orchestrating agent, directly — not by any Zig binary.** Nothing compiled dispatches
|
|
37
|
+
subagents, so there is nothing for a `synapse` flag to feed. The conf file is the shared home
|
|
38
|
+
for the setting; the reader is markdown, not code.
|
|
39
|
+
|
|
40
|
+
`pool = 0` is not "a pool of zero workers" — go to §2. `pool >= 1` is a real worker pool — go
|
|
41
|
+
to §3.
|
|
42
|
+
|
|
43
|
+
## 2. Pool = 0: the original inline procedure
|
|
44
|
+
|
|
45
|
+
Author every node yourself, one after another, in the same session — cross-node memory
|
|
46
|
+
intact, no subagent, no brief file needed. For each node, in list order:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
synapse rank --sources "$SYNAPSE_WORK_DIR/lists/NN.txt" --pool summary
|
|
50
|
+
synapse rank --sources "$SYNAPSE_WORK_DIR/lists/NN.txt" --pool crux
|
|
51
|
+
awk -F'\t' '$1 == "{Node Title}"' "$SYNAPSE_WORK_DIR/links.tsv"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Read the top few files each pool names, read this node's link rows for `## Links`
|
|
55
|
+
candidates, then write `$SYNAPSE_WORK_DIR/b-NN.md` yourself, following the
|
|
56
|
+
`synapse-node-format` contract. This is the whole procedure — no dispatch, no verification
|
|
57
|
+
step beyond what you'd naturally do writing anything else.
|
|
58
|
+
|
|
59
|
+
**This is the default, deliberately.** It's the safest choice, the one every existing
|
|
60
|
+
build has already used, and the only mode that preserves the authorial consistency a
|
|
61
|
+
single continuous session gives you across nodes — something no subagent-based mode, even
|
|
62
|
+
at pool 1, can offer. Fan-out is opt-in, not assumed.
|
|
63
|
+
|
|
64
|
+
## 3. Pool ≥ 1: fan out to a worker pool
|
|
65
|
+
|
|
66
|
+
### 3a. Compute the briefs, once
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
synapse rank --lists "$SYNAPSE_WORK_DIR/lists"
|
|
70
|
+
synapse brief --lists "$SYNAPSE_WORK_DIR/lists"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`build-refs` + `link-graph` should already have run by this point in `/synapse-init` (its own
|
|
74
|
+
step 6) regardless of pool size; `rank --lists` has not, since §2 uses `rank --sources`
|
|
75
|
+
instead and only this branch needs the batched form. `brief` reshapes both into one file per
|
|
76
|
+
node — it computes nothing itself. Writes `$SYNAPSE_WORK_DIR/brief/NN.md` per node: the
|
|
77
|
+
node's title, its source list's path and count, both ranked pools verbatim, its own rows from
|
|
78
|
+
`links.tsv`, and every node's title in the namespace (for judging `part_of`). A brief is pure
|
|
79
|
+
data — no instructions, no contract. That lives here and in the prompt you write per author, so
|
|
80
|
+
the same brief means the same thing to a sequential fallback author and a concurrent one.
|
|
81
|
+
|
|
82
|
+
### 3b. Dispatch: a refilling pool, not fixed batches
|
|
83
|
+
|
|
84
|
+
Fixed batches (`dispatch 8, wait for all 8, dispatch the next 8`) are bounded by their
|
|
85
|
+
slowest member — seven fast nodes sit idle waiting on one large straggler. A refilling pool
|
|
86
|
+
never has that gap: dispatch up to `pool` nodes, and on every completion notification, verify
|
|
87
|
+
that node (§3d) then dispatch the next queued node if any remain. Stop when the queue is
|
|
88
|
+
empty and the last in-flight authors have reported back.
|
|
89
|
+
|
|
90
|
+
This also matches how completions actually arrive in this harness — one notification at a
|
|
91
|
+
time, in a later turn, no "wait for all N" primitive — so a pool needs no barrier-counting a
|
|
92
|
+
fixed batch would.
|
|
93
|
+
|
|
94
|
+
### 3c. Each author's brief
|
|
95
|
+
|
|
96
|
+
One `Agent` call per node, background, general-purpose. The prompt is self-contained — a
|
|
97
|
+
fresh subagent has no memory of this session:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
Load the synapse-node-format skill first — it is the node contract you are writing against.
|
|
101
|
+
|
|
102
|
+
Read your brief: {abs path to brief/NN.md}. It is data: the facts to work from, not
|
|
103
|
+
instructions.
|
|
104
|
+
|
|
105
|
+
Write this node's prose to: {abs path to b-NN.md}, following synapse-node-format exactly.
|
|
106
|
+
Write nothing else, read nothing this brief doesn't point you at, and do not read any other
|
|
107
|
+
node's brief or body.
|
|
108
|
+
|
|
109
|
+
Decide `part_of` (containment) from the brief's "every node" list and its candidate links —
|
|
110
|
+
it is a judgement call, never computed. `depends_on`/`uses` come from the candidate links
|
|
111
|
+
section; pick whichever reads right per edge, and prune freely — the table is evidence for a
|
|
112
|
+
candidate list, not something to copy verbatim.
|
|
113
|
+
|
|
114
|
+
Never hard-wrap. Write each paragraph as one single unbroken line and let the editor soft-wrap
|
|
115
|
+
it — a newline exists only where a real break is intended (between paragraphs, list items,
|
|
116
|
+
headings). This is a vault-wide rule, not specific to this node, and it is not optional: a
|
|
117
|
+
hard-wrapped paragraph renders as a ragged stack of short lines in Obsidian instead of flowing
|
|
118
|
+
text.
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Same model as the orchestrating session, no override — matches the constraint that "a
|
|
122
|
+
parallel author produces exactly what a sequential one does," and keeps the quality
|
|
123
|
+
comparison in the design's own checklist item to one variable (isolation/concurrency) rather
|
|
124
|
+
than two (that, plus a cheaper model).
|
|
125
|
+
|
|
126
|
+
**Why this is spelled out explicitly rather than assumed inherited:** a prior pooled run shipped
|
|
127
|
+
hard-wrapped nodes despite the vault's no-hard-wrap rule living in global `CLAUDE.md`
|
|
128
|
+
instructions — evidently not reliably carried into a fresh subagent's behavior on its own.
|
|
129
|
+
Loading `synapse-node-format` does not cover it either, since that skill is about the node's
|
|
130
|
+
structure, not the vault's prose-formatting convention. State it here, every time, rather than
|
|
131
|
+
assuming it travels for free.
|
|
132
|
+
|
|
133
|
+
### 3d. Verify on completion, retry once, then fall back
|
|
134
|
+
|
|
135
|
+
When a completion notification arrives, read `b-NN.md` yourself — this is a check you read the
|
|
136
|
+
file for, same as the section check next to it, not a shell one-liner (this codebase's own
|
|
137
|
+
frontmatter reader deliberately avoids `grep`/`sed`/`awk` for exactly this field, per
|
|
138
|
+
`core/query.zig`'s docstring — a hand-rolled pattern here would drift from it the same way the
|
|
139
|
+
gap this section exists to close first happened). Confirm the file exists, opens with
|
|
140
|
+
frontmatter carrying a non-empty `summary:` field (`synapse-node-format`'s own contract — see
|
|
141
|
+
its "Each `b-NN.md` opens with its own one-line summary in frontmatter" line), and has
|
|
142
|
+
`## Summary`, `## Crux`, and `## Links` sections. Pass → dispatch the next queued node (§3b) and
|
|
143
|
+
move on.
|
|
144
|
+
Fail (missing file, missing frontmatter summary, missing section, obviously truncated) → **one
|
|
145
|
+
retry**, same brief, fresh subagent. A second failure → **write that one node yourself**,
|
|
146
|
+
inline, the §2 procedure, rather than blocking the rest of the pool on it. Report which nodes
|
|
147
|
+
needed a retry or a fallback in the final summary — a silent recovery hides a brief that might
|
|
148
|
+
be systematically wrong for a whole class of node.
|
|
149
|
+
|
|
150
|
+
Check the frontmatter here, at authoring time, rather than leaving it to `push-nodes` — a body
|
|
151
|
+
that has all three sections but no `summary:` field passes every check above and then fails at
|
|
152
|
+
push, node by node, after the whole pool has already finished and moved on.
|
|
153
|
+
|
|
154
|
+
**Also check for hard-wrapping** — a prose paragraph (not a list) whose lines break before
|
|
155
|
+
reaching a natural sentence boundary. A quick heuristic: within `## Summary`, a run of two or
|
|
156
|
+
more consecutive non-blank lines that are *not* list items (don't start with `-`/`*`/a digit)
|
|
157
|
+
is a hard-wrap, not a paragraph — a real paragraph is one line. Treat this as the same class of
|
|
158
|
+
failure as a missing section: fix by rewriting the paragraph as one line yourself (this is a
|
|
159
|
+
formatting fix, not a content one, so it doesn't need a fresh subagent) rather than letting it
|
|
160
|
+
ship and pushing.
|
|
161
|
+
|
|
162
|
+
### 3e. Pool = 1 is a real, useful degenerate case of this same pipeline
|
|
163
|
+
|
|
164
|
+
Not a synonym for §2. It still bundles a brief, dispatches to an isolated subagent, verifies,
|
|
165
|
+
and retries-then-falls-back — only the concurrency is 1. Useful on its own terms: it isolates
|
|
166
|
+
whether a quality difference against the sequential baseline comes from *isolation* (no
|
|
167
|
+
author sees another's output or memory — present even at pool 1) or from *concurrency*
|
|
168
|
+
itself (only present at pool ≥ 2). Reach for it specifically when running the design's
|
|
169
|
+
still-open "compare prose quality against a sequential build" checklist item, before
|
|
170
|
+
committing to a higher pool size.
|
|
171
|
+
|
|
172
|
+
## Guardrails
|
|
173
|
+
|
|
174
|
+
- **No author writes shared state.** Not another node's `b-NN.md`, not the tags cache, not
|
|
175
|
+
the vault. Every fact an author needs is in its own brief, computed before dispatch and
|
|
176
|
+
read-only during it — this is what removes the concurrent-writer problem rather than merely
|
|
177
|
+
managing it.
|
|
178
|
+
- **No author reads another node's brief or body.** Overlap in *source* reads (two nodes'
|
|
179
|
+
file lists sharing a path) is expected and accepted — see the design's Open Questions — but
|
|
180
|
+
an author's own inputs are its brief and nothing else.
|
|
181
|
+
- **Never grow the pool mid-run.** The size is resolved once at the start (§1); do not read
|
|
182
|
+
`synapse.conf` again partway through a build.
|
|
183
|
+
- **A retry gets the same brief, not a rewritten one.** If the brief itself was the problem,
|
|
184
|
+
fixing it is a §3a-level fix (regenerate every brief and restart), not something to
|
|
185
|
+
improvise per-node inside the retry.
|
|
186
|
+
- **`part_of` is never computed, at any pool size.** It is containment, not reference, and
|
|
187
|
+
deriving it from directory nesting would turn a folder layout into a claim about concepts —
|
|
188
|
+
the same reasoning the design's own Alternatives section already rejected this on.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: synapse-node-format
|
|
3
|
+
description: The contract for a Synapse code-graph node — frontmatter fields, the crux pointer, `## Links`, `grounded_in`, `## Sources`, and what `synapse write-node` adds or refuses. Load before authoring or regenerating any node, whether from /synapse-init's first build, /synapse-rebuild's triage, or the synapse-node skill's lazy regeneration. Not for reading the graph (that is synapse-query) or for task notes in the vault (that is synapse-task).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# What a node is, and how to author one
|
|
7
|
+
|
|
8
|
+
Every component that writes a node loads this: `/synapse-init` (first build), the `synapse-node`
|
|
9
|
+
skill (Tier 2 lazy regeneration), and `/synapse-rebuild` (reseat, patch, re-orient).
|
|
10
|
+
|
|
11
|
+
All three write the same artifact, so the format belongs in one place rather than being restated
|
|
12
|
+
wherever it is used. What stays with each caller is what is genuinely specific to it — the skill's
|
|
13
|
+
traps when *re*-authoring an existing node, rebuild's triage classes — not the contract itself.
|
|
14
|
+
|
|
15
|
+
`synapse write-node` is the enforcement. Where this document states a rule the writer already
|
|
16
|
+
refuses to break, that is deliberate and the script is the authority; this describes the artifact so
|
|
17
|
+
a reader knows what to produce, not so anyone hand-builds one.
|
|
18
|
+
|
|
19
|
+
Author the prose only — put each node's content in
|
|
20
|
+
`$SYNAPSE_WORK_DIR/b-NN.md` (matching its `lists/NN.txt`), then run `synapse push-nodes`,
|
|
21
|
+
which calls `synapse write-node` per node. The contract below is what that writer implements
|
|
22
|
+
and what `synapse query stale` verifies; it is specified here because the two must agree
|
|
23
|
+
exactly, not because you should hand-build the file. A node lands at
|
|
24
|
+
`synapse/{repo}@{branch}/{Node Title}.md`:
|
|
25
|
+
|
|
26
|
+
Each `b-NN.md` opens with its own one-line summary in frontmatter, so everything authored about a
|
|
27
|
+
node is in one file:
|
|
28
|
+
|
|
29
|
+
```markdown
|
|
30
|
+
---
|
|
31
|
+
summary: One line differentiating this node from its siblings.
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Summary
|
|
35
|
+
...
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The driver strips that frontmatter and the line becomes the node's `summary` field, which
|
|
39
|
+
`synapse build-project-index` reads back to build the index bullet. Write it *for the index* — it has to distinguish this node
|
|
40
|
+
from dozens of siblings, which is a different job from the node's opening sentence, whose job is
|
|
41
|
+
to orient someone already inside. A node without one is an error, not a default.
|
|
42
|
+
|
|
43
|
+
- **Filename/title:** short, senior-engineer-style description of the concept (e.g. "World —
|
|
44
|
+
entity/component/resource core"). Filesystem-illegal characters (`/ : * ? " < > |`) are
|
|
45
|
+
sanitized — but **reword the title instead of relying on that**, because Obsidian resolves a
|
|
46
|
+
wikilink by *filename*, so `[[World — entity/component/resource core]]` silently resolves to
|
|
47
|
+
nothing once the file becomes `...entity_component_resource core.md`. A broken wikilink is a
|
|
48
|
+
valid link to a not-yet-existing note, so it fails quietly. The writer warns when a title needs
|
|
49
|
+
sanitizing; treat that warning as "rename this node". Same trap when you *retitle* a node
|
|
50
|
+
mid-build: inbound links already written keep pointing at the old name.
|
|
51
|
+
- **`sources`:** **every** file the node covers — repo-relative path plus that file's
|
|
52
|
+
`git hash-object <path>` output, run from the repo root at the moment of writing. Exhaustive,
|
|
53
|
+
not a sample: this is a **machine** field, and it is what makes Obsidian's search able to reach
|
|
54
|
+
a node from any file it covers (searching a class name that appears in no node's prose still
|
|
55
|
+
finds its node via this list). Do **not** trim it to a handful of "representative" files —
|
|
56
|
+
doing so silently destroys that lookup, leaves the node unable to answer "which files am I
|
|
57
|
+
about", and reduces hash verification to whatever survived the trim. Readability pressure
|
|
58
|
+
belongs on `## Sources` below, never here.
|
|
59
|
+
- **`sources_digest`:** `sha256` over the sorted `path:hash` lines of `sources` (see "Computing
|
|
60
|
+
`sources_digest`" below). Lets a staleness check answer "has this node changed" by reading one
|
|
61
|
+
field instead of every hash.
|
|
62
|
+
- **`built_at`:** machine local time (`date '+%Y-%m-%d %H:%M'`) — never inferred.
|
|
63
|
+
- **`stale`:** `false` — freshly built.
|
|
64
|
+
- **Body:** `summary` (plain-English, the explanation a senior engineer would give walking
|
|
65
|
+
someone through this subsystem), `crux` (the few lines that carry the actual logic — **authored
|
|
66
|
+
as line numbers, stored as text**: you point, the writer slices, so composing is impossible at
|
|
67
|
+
authoring time and nothing decays afterwards the way a stored line number would), `links` (typed
|
|
68
|
+
Obsidian wikilinks to other nodes in this same namespace: `depends_on`, `part_of`, `uses`, or
|
|
69
|
+
another type that fits better if one doesn't — for `depends_on`/`uses` specifically, `/synapse-init`
|
|
70
|
+
computes candidates before any node exists via `synapse link-graph`; read that node's rows from
|
|
71
|
+
`links.tsv` rather than guessing which siblings it relates to, `part_of` stays a judgement call
|
|
72
|
+
with nothing mechanical behind it), a `## Sources` section, and an empty `## Notes` section.
|
|
73
|
+
- **Break a "does N things" enumeration into real bullets, not inline `(1)/(2)/(3)`.** A sentence
|
|
74
|
+
enumerating three or more parallel sub-points reads as a wall of text once each item carries its
|
|
75
|
+
own clause or parenthetical -- the node is read by a human skimming it in Obsidian as much as by
|
|
76
|
+
an agent (see the design note this format came from: hosting the graph as vault-readable markdown
|
|
77
|
+
was chosen specifically so it stays "just as readable by a human directly in Obsidian as it is by
|
|
78
|
+
Claude"), and a dense inline run-on defeats that. Use a markdown bullet list under the sentence
|
|
79
|
+
introducing them instead. This is narrow, not a general "prefer bullets" rule: an aside of one or
|
|
80
|
+
two items, or connected causal narrative ("X, because Y, which is why Z"), stays flowing prose --
|
|
81
|
+
over-bulleting ordinary narrative just trades one readability problem for another.
|
|
82
|
+
- **Never write crux code. Point at it and let the writer cut it out.** In the body, emit a
|
|
83
|
+
directive instead of a code block:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
## Crux
|
|
87
|
+
<!-- crux: crates/matcher/src/lib.rs 412-419 -->
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`synapse write-node` slices those lines out of the file, fences them with a language guessed
|
|
91
|
+
from the extension, appends a `path:start-end` provenance line, and records `crux_path` /
|
|
92
|
+
`crux_lines` in frontmatter. It refuses the write if the path is not one the node claims, if the
|
|
93
|
+
range runs past the end of the file, or if the span reaches 20 lines.
|
|
94
|
+
|
|
95
|
+
This exists because a typed crux can be a paraphrase that merely *looks* like a quote — the
|
|
96
|
+
invented `trait Matcher { /* no engine assumptions */ }` reads perfectly and is worth nothing. A
|
|
97
|
+
rule saying "quote, don't compose" depends on compliance; pointing makes composing impossible,
|
|
98
|
+
which is the same mechanics-belong-to-the-tooling move as the rest of the write path.
|
|
99
|
+
|
|
100
|
+
- **Ground the summary in what the codebase asserts about itself.** Prefer, in this order: a test
|
|
101
|
+
(its name and assertions state behaviour that CI checks on every commit — the strongest evidence
|
|
102
|
+
short of running the code), a doc comment or module header (the author's own claim of intent), and
|
|
103
|
+
only then your own reading. Point at each piece of evidence, repeatably, anywhere in the body:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
<!-- grounded_in: src/test/java/PremiumTest.java 42-48 -->
|
|
107
|
+
<!-- grounded_in: src/main/java/Premium.java 10-14 -->
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The writer records each as path + lines + sha256 of the sliced text in the `grounded_in`
|
|
111
|
+
frontmatter list, then strips the directives — this is provenance, not display, so nothing is
|
|
112
|
+
rendered and the prose stays readable. `synapse query grounding` later re-slices each range and
|
|
113
|
+
compares, which turns "is this summary still true" from a judgement into a check.
|
|
114
|
+
|
|
115
|
+
Why it matters more than it looks: a claim traced to a test or a doc comment is one the codebase
|
|
116
|
+
made, so even a *wrong* doc comment beats an invented explanation — it is attributable and
|
|
117
|
+
findable. What this is guarding against is the confident causal story assembled from a stray
|
|
118
|
+
import ("output stays coherent because the printer is behind a mutex"), which reads exactly like
|
|
119
|
+
understanding and can be false from the moment it is written.
|
|
120
|
+
|
|
121
|
+
Not every sentence can be grounded, and that is expected. Architectural narrative and hard-won
|
|
122
|
+
debugging findings have no test asserting them. Ground what can be grounded; do not manufacture
|
|
123
|
+
evidence for the rest, and do not water down a true synthesis just to make it citable.
|
|
124
|
+
|
|
125
|
+
- **`<!-- crux: none -->` is a real answer — use it.** A trivial data holder, a one-line
|
|
126
|
+
delegation, or logic spread evenly with no focal point genuinely has no crux, and a subsystem
|
|
127
|
+
node often has none either. A required field with no honest answer is exactly how a fabricated
|
|
128
|
+
one appears, so say `none` rather than picking a span to fill the slot. If something adjacent is
|
|
129
|
+
worth quoting instead, point at the module's own doc comment — that is an honest quote of
|
|
130
|
+
something nearby, not a fabricated quote of the thing itself.
|
|
131
|
+
- **Prefer claims about structure over claims about mechanism.** "These three printers implement
|
|
132
|
+
the sink interface" is checkable and stays true; "the parallel path shares a printer behind a
|
|
133
|
+
mutex, which is why output stays coherent" is the kind of causal story that is easy to assemble
|
|
134
|
+
from a stray `use std::sync::Mutex` and wrong. Every later regeneration keeps the sentences the
|
|
135
|
+
diff does not contradict, so a mechanism invented here is permanent. State one only after
|
|
136
|
+
reading the code that implements it.
|
|
137
|
+
- **`## Sources` is the human mirror of `sources`, aggregated rather than enumerated:** one line
|
|
138
|
+
per owning directory or module with a file count, `LC_ALL=C` sorted. A node covering 941 files
|
|
139
|
+
would otherwise put 75 KB of paths in front of a reader who wants to know which modules are
|
|
140
|
+
involved — and the frontmatter already carries every path for search, so the mirror doesn't
|
|
141
|
+
need to repeat them. Rewritten from `sources` on every write, never hand-edited. (Obsidian's
|
|
142
|
+
Properties panel flattens the raw `sources` field into a truncated one-line string, which is
|
|
143
|
+
why a mirror exists at all — but that is an argument for aggregating *the mirror*, not for
|
|
144
|
+
trimming the field.)
|
|
145
|
+
- **`## Notes` is human-authored only.** Claude never writes into it — not at build time, not at
|
|
146
|
+
regeneration. It is created empty and preserved verbatim forever after.
|
|
147
|
+
- **Fence the generated region.** Everything the generator owns sits between
|
|
148
|
+
`<!-- synapse:generated:start -->` and `<!-- synapse:generated:end -->`; everything outside is
|
|
149
|
+
re-emitted byte-for-byte. This is the mechanism behind the `## Notes` guarantee — without it,
|
|
150
|
+
"preserved verbatim" is a promise with nothing enforcing it.
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
---
|
|
154
|
+
title: "World — entity/component/resource core"
|
|
155
|
+
node_type: synapse-node
|
|
156
|
+
project: acme
|
|
157
|
+
sources:
|
|
158
|
+
- path: acme_ecs/world.ml
|
|
159
|
+
hash: <git hash-object output>
|
|
160
|
+
- path: acme_ecs/world.mli
|
|
161
|
+
hash: <git hash-object output>
|
|
162
|
+
# ... every file the node covers, not a selection
|
|
163
|
+
sources_digest: <sha256 over the sorted "path:hash" lines>
|
|
164
|
+
stale: false
|
|
165
|
+
built_at: "<now>"
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
# World — entity/component/resource core
|
|
169
|
+
<!-- synapse:generated:start -->
|
|
170
|
+
|
|
171
|
+
## Summary
|
|
172
|
+
{plain-English explanation}
|
|
173
|
+
|
|
174
|
+
## Crux
|
|
175
|
+
<!-- crux: {path a source line below claims} {start}-{end} -->
|
|
176
|
+
{or `<!-- crux: none -->` when no single span carries it. The writer replaces
|
|
177
|
+
this directive with the sliced code, so never write the code here yourself.}
|
|
178
|
+
|
|
179
|
+
## Links
|
|
180
|
+
- depends_on [[Other Node Title]]
|
|
181
|
+
- part_of [[Another Node Title]]
|
|
182
|
+
|
|
183
|
+
## Sources
|
|
184
|
+
- `acme_ecs` (2)
|
|
185
|
+
<!-- synapse:generated:end -->
|
|
186
|
+
|
|
187
|
+
## Notes
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Computing `sources_digest`
|
|
192
|
+
|
|
193
|
+
Pin this exactly — a writer and a verifier computing it differently is a silent
|
|
194
|
+
false-positive generator, and the point of the field is to be trusted without reading
|
|
195
|
+
`sources` at all. Adopted from Graft's `sources_digest` so the two remain comparable:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
digest = sha256( "\n".join(sorted( f"{path}:{hash}" for each entry in sources )) )
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Sort the joined `path:hash` lines themselves (not the paths, then the hashes), `LC_ALL=C`,
|
|
202
|
+
newline-separated, no trailing newline.
|
|
203
|
+
|
|
204
|
+
`project` is the repo half of the namespace key — `synapse namespace --repo-name`, not the task-prefix scheme that `/synapse-note` uses. The writer fills it in; it is described here so the field's meaning is documented once.
|
|
205
|
+
|