Skip to content

Commit 8565016

Browse files
author
gluck
committed
docs: strip em dashes from public-facing markdown
README, docs/, skills/, scripts/ READMEs and the top-level handoff. Mechanical: scripts/strip-emdash.py turns a dash into a comma (colon in headings, a hyphen inside box drawings and table cells) and folds line-wrapped dashes back onto their line. plans/ follows separately.
1 parent 333632c commit 8565016

16 files changed

Lines changed: 150 additions & 76 deletions

File tree

‎HANDOFF-trunks.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# HANDOFF — the presentation hierarchy: promote, ls, and delete
1+
# HANDOFF: the presentation hierarchy: promote, ls, and delete
22

33
Worktree `~/dev/figaro-qua/trunkfix`
44
Branch `fix/trunk-presentation` @ `4bc5ad0c`

‎docs/store/architecture.md‎

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ below it, and the public surface a consumer addresses is the *top* layer
2424
│ depends on
2525
┌───────────────────────────▼─────────────────────────────────┐
2626
│ segment append-only segment files + ValueHash; │
27-
│ (segment.go, the leaf — no deps on the layers above │
27+
│ (segment.go, the leaf - no deps on the layers above │
2828
│ hash.go) │
2929
└─────────────────────────────────────────────────────────────┘
3030
```
@@ -36,15 +36,15 @@ files are all internal mechanism the upper layers manage on its behalf.
3636

3737
---
3838

39-
## `segment` — the leaf
39+
## `segment`: the leaf
4040

4141
**File:** `segment/segment.go`, `segment/hash.go`
4242

4343
An append-only file framed by a `SegmentCodec`. A `Segment` owns one
4444
file, a `baseIndex`, a count, and an in-memory offset index rebuilt by
4545
`recover()` on open (the standard WAL recovery: scan forward, accept
4646
clean frames, truncate any torn tail). It has no knowledge of forks,
47-
channels, or trunks — it is the bottom of the stack and depends on
47+
channels, or trunks, it is the bottom of the stack and depends on
4848
nothing above it.
4949

5050
Provides:
@@ -60,12 +60,12 @@ Provides:
6060
canonicalizes JSON (object keys sorted, whitespace stripped, numbers
6161
preserved via `json.Number`) and returns the truncated SHA-256
6262
(16 hex chars / 64 bits) of the canonical form. JSON-equal inputs hash
63-
identically regardless of key order or formatting — the property the
63+
identically regardless of key order or formatting, the property the
6464
upper layers rely on to compare entries by value.
6565

6666
---
6767

68-
## `disk.Log` — the fork engine over segments
68+
## `disk.Log`: the fork engine over segments
6969

7070
**File:** `disk/log.go`, `disk/fork.go`. **Depends on:** `segment`.
7171

@@ -84,7 +84,7 @@ by delegating to the parent chain (`Read`, `Range`, `HeaderAt`,
8484
`StateAt` all walk up when `idx < forkBase`). This is the watermark
8585
boundary: own-range entries live in this log's own segments, the
8686
inherited prefix lives in ancestors. `LastIndex` of an empty-own fork
87-
reports `forkBase-1` — it sits immediately after the parent's tail.
87+
reports `forkBase-1`, it sits immediately after the parent's tail.
8888
Reducible (header-mode) logs additionally carry a per-segment watermark
8989
header computed by `OnSegmentOpen`; `StateAt(idx)` folds the segment's
9090
watermark with its entries up to `idx`.
@@ -108,13 +108,13 @@ only source for indices at and beyond that boundary.
108108
log keeps the prefix `[FirstIndex, atIdx-1]` and becomes read-only; the
109109
suffix moves into an "old-future" subdir; a fresh empty child subdir is
110110
created, writable from `atIdx`. Branch points are N-ary and
111-
re-splittable. The *heuristic* re-home applies — on a re-split (own
111+
re-splittable. The *heuristic* re-home applies, on a re-split (own
112112
segments split) all existing children move into the old-future.
113113
- `ForkRehome(atIdx, child, oldFuture, rehome)` forks with an *explicit*
114114
re-home list instead of the heuristic. The xwal joint fork uses this so
115115
every channel re-homes the **same** children (decided once from the
116116
main channel) and the triune's trees stay in lockstep.
117-
- `ChildForkBases()` returns each child subdir's `.fork` base — the input
117+
- `ChildForkBases()` returns each child subdir's `.fork` base, the input
118118
the joint fork uses to decide which children re-home.
119119

120120
**Empty-log handling.** An empty root forks at index 1; an **empty-own**
@@ -131,7 +131,7 @@ still has the sentinel.
131131

132132
---
133133

134-
## `xwal.XWAL` — the multi-channel "triune" join
134+
## `xwal.XWAL`: the multi-channel "triune" join
135135

136136
**File:** `xwal/xwal.go`, `xwal/fork.go`. **Depends on:** `disk.Log`.
137137

@@ -147,14 +147,14 @@ provider-owned payload bytes. Decoding accepts both frame shapes.
147147

148148
A `Config` describes the join at creation:
149149

150-
- `Main` — the main channel name.
151-
- `Channels` — the `ChannelSpec` list (name, kind, reducer key, runtime
150+
- `Main`, the main channel name.
151+
- `Channels`, the `ChannelSpec` list (name, kind, reducer key, runtime
152152
`SyncMode`, persisted `Opaque` payload encoding).
153-
- `Registry` — maps reducer names to `Reducer{Reduce, Initial}`;
153+
- `Registry`, maps reducer names to `Reducer{Reduce, Initial}`;
154154
resolved on every open (functions are never persisted).
155-
- `Codec` — `"jsonl"` (default) or `"binary"`; persisted in the manifest.
156-
- `Genesis` — custom main-channel genesis payload (creation only).
157-
- `MintTrunkID` — pluggable opaque trunk-id generator (consumed by the
155+
- `Codec`, `"jsonl"` (default) or `"binary"`; persisted in the manifest.
156+
- `Genesis`, custom main-channel genesis payload (creation only).
157+
- `MintTrunkID`, pluggable opaque trunk-id generator (consumed by the
158158
Trunks layer).
159159

160160
After first open the on-disk `xwal.json` manifest is authoritative for channel
@@ -172,7 +172,7 @@ Repeated and last-wins lookups remain O(1) once their suffix is indexed.
172172
**Forks all channels as a unit** (`fork.go`). `XWAL.Fork(atMainLT,
173173
child, oldFuture)` builds a `forkPlan`: the main channel forks at
174174
`atMainLT`, each related channel forks at its own boundary
175-
(`boundaryFor` — the first own entry whose main LT ≥ `atMainLT`, or
175+
(`boundaryFor`, the first own entry whose main LT ≥ `atMainLT`, or
176176
own-tail+1 if it has not caught up; empty-own channels fork at their own
177177
first index). The re-home set is decided once from the main channel and
178178
applied to every channel by name via `ForkRehome`. A `.xwal-fork-pending`
@@ -198,17 +198,17 @@ invalidate the cache.
198198

199199
---
200200

201-
## `xwal.Trunks` — the trunk-addressed view
201+
## `xwal.Trunks`: the trunk-addressed view
202202

203203
**File:** `xwal/trunks.go`. **Depends on:** `xwal.XWAL`.
204204

205205
`Trunks` is the surface a consumer addresses. A **trunk** is one
206-
continuation chain across all channels (the triune forked as a unit) —
207-
the stable handle — while the per-fork **node** is plumbing.
206+
continuation chain across all channels (the triune forked as a unit),
207+
the stable handle, while the per-fork **node** is plumbing.
208208

209209
**DISK IS THE SOLE SOURCE OF TRUTH.** The node tree *is* the main
210210
channel's directory tree: `nN` subdirs plus `.fork` markers. Everything
211-
about a node is derivable from that tree by walking it — *except* a
211+
about a node is derivable from that tree by walking it, *except* a
212212
node's trunk id. That single non-derivable datum, and only that, is
213213
persisted: a `.trunk` file per node dir (in the main channel), sitting
214214
alongside `.fork`. The in-memory structure (`nodes`, `heads`, sequence
@@ -270,7 +270,7 @@ invariant.
270270

271271
Every record xwal writes carries a server timestamp (`t`, unix millis) in
272272
its frame, stamped at append time from `Config.Now` (a test seam;
273-
defaults to the real clock). It is mandatory and supplied by xwal itself —
273+
defaults to the real clock). It is mandatory and supplied by xwal itself,
274274
callers cannot set or omit it. Records written before timestamps existed
275275
simply lack the field and decode with `TS == 0`; they are tolerated
276276
forever and never migrated (the JSONL `_hash` covers the payload, so old
@@ -284,7 +284,7 @@ that asks it N times per listing:
284284
per-node `atomic.Int64` counters (16 bytes per node ever addressed,
285285
retained for the life of the `Trunks`). An XWAL opened through Trunks
286286
wires its internal `lastTS` pointer to the registry's counter, so every
287-
append through any handle — and every `sharedView` of it — advances the
287+
append through any handle, and every `sharedView` of it, advances the
288288
one counter a listing reads. Open, close, and hot-store eviction do not
289289
touch it. This is not a cache of derived data: it is the same primitive
290290
the appender bumps, merely kept.
@@ -293,19 +293,19 @@ that asks it N times per listing:
293293
- **Cold reads are a file read.** First query for a node this process has
294294
never opened runs a bounded tail probe: the lexically newest segment
295295
file per channel directory the node owns, scan to the last complete
296-
frame, decode its `t`, take the max — no full `Open`, no segment
296+
frame, decode its `t`, take the max, no full `Open`, no segment
297297
indexing, no watermark folds. Measured: ~36µs, ~73KB transient, 126
298298
allocs, paid once per node per process (the probe hydrates the retained
299299
counter). A full `Open` later merges monotonically (CAS-max) into the
300300
same counter, so probe and hydration can never regress an append.
301301
- **Never a wake.** LastTS opens (at most) segment files. Whether the
302302
consumer's node has an agent, a daemon, or anything else attached is
303-
invisible from here — a listing over dormant nodes stays dormant.
303+
invisible from here, a listing over dormant nodes stays dormant.
304304

305305
The frame's `t` sits in ALPHABETICAL key position (`m, p/p64, t, x`)
306306
because the JSONL codec canonicalizes key order on disk; encoder output
307307
and disk output are byte-identical, and the fast-path frame decoder
308308
speaks that one grammar. (The first draft ordered the encoder differently
309309
from the disk and every meta-carrying segment read silently fell to the
310-
reflection path — found by benchmark comparison, not by tests, and pinned
310+
reflection path, found by benchmark comparison, not by tests, and pinned
311311
by canaries that round-trip the codec since.)

‎internal/store/crashtest/FINDINGS.md‎

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ findings are races against wall-clock delays, so a replay reproduces the
1717
same distribution (and usually the same findings) rather than the exact
1818
cycle indexes. Occurrence counts below are from the recorded campaign logs.
1919

20-
## Kill mode (pure SIGKILL — completed writes survive in the page cache)
20+
## Kill mode (pure SIGKILL: completed writes survive in the page cache)
2121

2222
### K1. Fork commit is not crash-atomic: trunk id lost
2323

@@ -36,20 +36,20 @@ next writer found zero trunks.
3636
forward"; in practice a mid-fork SIGKILL can leave a `.fork-pending`
3737
sentinel that every subsequent `OpenTrunks` refuses:
3838

39-
- seed=11 store=6 cycle=3 childSeed=7342284354246113453 — sentinel left in
39+
- seed=11 store=6 cycle=3 childSeed=7342284354246113453, sentinel left in
4040
a related-channel branch dir (`notes/n0/n2/n3`); open fails "fork in
4141
progress" permanently.
42-
- seed=12 store=2 cycle=1 childSeed=6759699898442884760 — sentinel in
42+
- seed=12 store=2 cycle=1 childSeed=6759699898442884760, sentinel in
4343
`notes/n0`, same permanent failure.
44-
- seed=12 store=11 cycle=2 childSeed=4073270498405317002 — recovery is not
44+
- seed=12 store=11 cycle=2 childSeed=4073270498405317002, recovery is not
4545
idempotent: "recover interrupted fork: fork \"main\": open: fork in
4646
progress: dir contains .fork-pending sentinel: .../main". The roll-forward
4747
path itself trips over the sentinel it is supposed to consume.
4848

4949
### K3. Mid-fork kill can cut the HEAD of a related channel
5050

5151
`head-cut`, seed=12 store=2 cycle=1: after reopen the notes channel starts
52-
at chanLT 2 — record 1 is gone. Loss from the front, so the surviving log
52+
at chanLT 2, record 1 is gone. Loss from the front, so the surviving log
5353
is not a prefix at all.
5454

5555
### K4. Positive results
@@ -62,8 +62,8 @@ crash windows look solid at e2f5c89; fork commit is the one broken window.
6262
## Corrupt mode (torn-tail simulation)
6363

6464
Repair at e2f5c89 assumes only the last segment of the writing node can be
65-
torn. A torn tail in any OTHER recently written segment — one sealed by
66-
rotation during the cycle, or a fork-parent segment — is not detected:
65+
torn. A torn tail in any OTHER recently written segment, one sealed by
66+
rotation during the cycle, or a fork-parent segment, is not detected:
6767

6868
### C1. Silent renumbering
6969

@@ -128,7 +128,7 @@ never undetected payload corruption.
128128

129129
# memory-first findings (branch memory-first merged at 8b888d7)
130130

131-
Harness rebound to `xwal.OpenStore` (bind_trunks.go): no SyncChannel — the
131+
Harness rebound to `xwal.OpenStore` (bind_trunks.go): no SyncChannel, the
132132
durable lower bound is time-based (append stamp older than
133133
FlushInterval+slack before the kill must survive), plus Close-drain
134134
(everything durable once Close returns) and Kick as a scheduling hint.
@@ -151,7 +151,7 @@ cycles (seeds 41/42/43), plus the targeted flush-error test.
151151

152152
seed=33 store=7 cycle=1 childSeed=602310214271270794: after a mid-fork
153153
kill, reopen fails "fork in progress: dir contains .fork-pending
154-
sentinel: .../state/n0" — permanently. 1 occurrence in 808 kill cycles.
154+
sentinel: .../state/n0", permanently. 1 occurrence in 808 kill cycles.
155155

156156
### M3 = C1-C5. Torn tails outside the active segment still misplace data
157157

@@ -163,7 +163,7 @@ crash tails but does not detect a torn SEALED segment (rotated or
163163
fork-parent); subsequent records renumber or the store bricks, same
164164
failure shapes as e2f5c89.
165165

166-
### M4. Repair is gated on `.unclean` — corruption after clean Close slips through
166+
### M4. Repair is gated on `.unclean`: corruption after clean Close slips through
167167

168168
A1's declared risk, confirmed: 34 of the damaging corrupt cycles were
169169
`cleanclose` (child Closed cleanly, then bytes were torn, then reopen):
@@ -177,11 +177,11 @@ childSeed=2833144618262241349 (flip in `main/n0/...0039.jsonl`).
177177
`reducible-ahead` in 808 kill cycles (e2f5c89 produced both after any
178178
main-tail repair). The vectored coherent-cut flush works.
179179
- Kill-mode head-cut (K3): not observed in 808 cycles.
180-
- Durability contract: zero `durable-lost` — every append older than
180+
- Durability contract: zero `durable-lost`, every append older than
181181
FlushInterval(+500 ms slack) before the kill survived; Close-drain
182182
lost nothing (all cleanclose kill-mode cycles verified full survival);
183183
kill-mid-Close cycles repaired cleanly on reopen.
184-
- Flush-error path: TestFlushErrorReadOnlyRecovers — segment dirs made
184+
- Flush-error path: TestFlushErrorReadOnlyRecovers, segment dirs made
185185
read-only mid-run: appends keep succeeding in memory, flusher retries,
186186
full recovery after permissions restored, clean Close, zero loss.
187187
- Peer-goroutine append/fork interleaving: no divergence, no violations
@@ -193,7 +193,7 @@ childSeed=2833144618262241349 (flip in `main/n0/...0039.jsonl`).
193193
## Expected-loss profile
194194

195195
`append-lost` findings (contract-permitted flush lag): ~580 per 300-cycle
196-
kill run, nearly always 1-3 records per channel — the in-memory tail
196+
kill run, nearly always 1-3 records per channel, the in-memory tail
197197
since the last flusher pass. e2f5c89 showed zero here (its writes hit the
198198
page cache synchronously); this is the traded loss window of
199199
memory-first, bounded and honored.
@@ -202,6 +202,6 @@ memory-first, bounded and honored.
202202

203203
Kill-mode known gaps: `append-lost`, `trunk-lost-mid-fork` (M1), plus
204204
fork-wreckage classes only when a fork was in flight (M2). Corrupt-mode
205-
known gaps: M3/M4 classes. Everything else — durable-lost, checksum,
206-
panic, mismatch/gap/head-cut outside fork or corruption windows — fails
205+
known gaps: M3/M4 classes. Everything else, durable-lost, checksum,
206+
panic, mismatch/gap/head-cut outside fork or corruption windows, fails
207207
the suite.

‎scripts/callpath/README.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# callpath — the disk-to-wire enumerator
1+
# callpath: the disk-to-wire enumerator
22

33
A tool, not a list. **If it cannot be re-run after a refactor, it is a snapshot
44
of someone's reading and it will be wrong within a day.**
@@ -16,10 +16,10 @@ Every one of these was paid for by a failure on 2026-08-18:
1616
- **THE ALGORITHM AND ITS PRECISION.** A VTA edge and a CHA edge are different
1717
claims. CHA admits every implementation of the interface method; VTA narrows
1818
by value flow. They are never printed identically.
19-
- **THE CUT** — pattern, tests on/off, package list. *Anything outside the cut
19+
- **THE CUT**, pattern, tests on/off, package list. *Anything outside the cut
2020
is invisible, not absent.*
2121
- **CALL PATHS ARE NOT DATA PATHS.** An absent path may mean the bytes crossed
22-
**by value** — a `Log` returned by `Open` and invoked from another stack has
22+
**by value**, a `Log` returned by `Open` and invoked from another stack has
2323
no call path from `Open` and is not unrelated. An empty result means one of
2424
three things and only one of them is a finding.
2525
- **STATIC vs DISPATCH[n]**, with the **candidate set as inline children**
@@ -30,7 +30,7 @@ Every one of these was paid for by a failure on 2026-08-18:
3030
that draws the syscall unconditionally **asserts a call most reads never
3131
make**.
3232
- **[GENERIC INSTANTIATION]** where one runtime frame appears as two symbols,
33-
and type arguments on the symbol — because `decodeRecord[message.Message]`
33+
and type arguments on the symbol, because `decodeRecord[message.Message]`
3434
and `decodeRecord[[]json.RawMessage]` are the fig IR channel and the
3535
translator channel, and printing them identically merges two different byte
3636
movements into one line.

‎scripts/live/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Each of these starts a real daemon against a real store and drives real
44
verbs. They exist because **a green unit suite is not evidence about the
55
product**: between them they found nine bugs during the state-layer work
6-
(session 3), and four of those were in the direction that loses data — a
6+
(session 3), and four of those were in the direction that loses data, a
77
libretto stump drawn in `fig ls`, a fork entry point the CLI uses and the
88
tests did not, a repair command that declined to repair, a study of a deleted
99
form that could not be dropped after a restart.

‎scripts/strip-emdash.py‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env python3
2+
"""Strip em dashes from markdown, mechanically and losslessly.
3+
4+
Rules, in order:
5+
1. A table cell holding only an em dash becomes "-" (the n/a marker).
6+
2. An em dash alone at the end of a line becomes a comma on that line.
7+
3. An em dash starting a line moves back as a comma on the previous line.
8+
4. Inside a box-drawing line, " — " becomes " - " (same width).
9+
5. In a heading, " — " becomes ": ".
10+
6. Any other inline " — " becomes ", ".
11+
Anything left over is reported, not guessed at.
12+
"""
13+
import re
14+
import sys
15+
16+
CELL = re.compile(r"(?<=\|)(\s*)—(\s*)(?=\|)")
17+
18+
19+
def strip(text: str) -> str:
20+
prev = None
21+
while prev != text:
22+
prev = text
23+
text = CELL.sub(lambda m: m.group(1) + "-" + m.group(2), text)
24+
25+
lines = text.split("\n")
26+
for i, line in enumerate(lines):
27+
# em dash left dangling at end of a wrapped line
28+
lines[i] = re.sub(r"\s+—$", ",", lines[i])
29+
for i, line in enumerate(lines):
30+
m = re.match(r"^(\s*)—\s+", line)
31+
if m and i > 0 and lines[i - 1].strip():
32+
lines[i] = m.group(1) + line[m.end():]
33+
if not lines[i - 1].rstrip().endswith((",", ":", ";")):
34+
lines[i - 1] = re.sub(r"\s*$", ",", lines[i - 1])
35+
text = "\n".join(lines)
36+
37+
lines = text.split("\n")
38+
for i, line in enumerate(lines):
39+
if "—" not in line:
40+
continue
41+
if re.search(r"[\u2500-\u257f]", line): # box drawing: keep width
42+
lines[i] = re.sub(r" +— +", " - ", line)
43+
elif re.match(r"\s*#{1,6} ", line): # heading: colon reads better
44+
lines[i] = re.sub(r" +— +", ": ", line)
45+
else:
46+
lines[i] = re.sub(r" +— +", ", ", line)
47+
text = "\n".join(lines)
48+
text = re.sub(r",[ \t]*,", ",", text)
49+
return text
50+
51+
52+
def main(paths):
53+
left = 0
54+
changed = 0
55+
for p in paths:
56+
with open(p, encoding="utf-8") as f:
57+
src = f.read()
58+
if "—" not in src:
59+
continue
60+
out = strip(src)
61+
if out != src:
62+
with open(p, "w", encoding="utf-8") as f:
63+
f.write(out)
64+
changed += 1
65+
for n, line in enumerate(out.split("\n"), 1):
66+
if "—" in line:
67+
left += 1
68+
print(f"LEFTOVER {p}:{n}: {line.strip()[:100]}", file=sys.stderr)
69+
print(f"{changed} files rewritten, {left} em dashes left")
70+
return 1 if left else 0
71+
72+
73+
if __name__ == "__main__":
74+
sys.exit(main(sys.argv[1:]))

0 commit comments

Comments
 (0)