@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,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
+