Skip to content

Commit 4d8e6e3

Browse files
docs(adr): ADR-0013 — json sidecar cross-process locking (V-L2-F4) (#153)
Document V-L2-F4 (#150, shipped in #151): add ADR-0013 (json sidecar cross-process locking + atomic writes + provenance/history CLI), refine ADR-0012's single-writer note to reference it, add the CHANGELOG.md entry, and extend the .claude read-only permission allowlist. Docs + config only. https://claude.ai/code/session_01Ux144vBDdySvLUqUrCgkT4
1 parent 158c10f commit 4d8e6e3

4 files changed

Lines changed: 87 additions & 4 deletions

File tree

.claude/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
"Bash(cargo clippy:*)",
1414
"Bash(cargo fmt:*)",
1515
"Bash(cargo fetch:*)",
16+
"Bash(git rev-parse:*)",
17+
"Bash(git ls-files:*)",
18+
"Bash(git merge-base:*)",
19+
"Bash(git remote:*)",
1620
"mcp__github__pull_request_read",
1721
"mcp__github__issue_read",
1822
"mcp__github__list_pull_requests"

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0
2020

2121
### Added
2222

23+
- feat(sidecar): cross-process advisory write locking + atomic-rename durability for the JSON sidecar, and wire the `provenance`/`history` CLI subcommands against sqlite + json sidecars (V-L2-F4, ADR-0013, closes #150) (#151)
2324
- feat(sidecar): JSON-family sidecar storage backend — plain JSON / JSON-LD / NDJSON with SQLite-parity octad runtime (provenance incl. forks, temporal, drift, gc); new `[sidecar].format` key and a single `StorageKind::resolve` backend resolver (V-L2-F3, ADR-0012, closes #146) (#148)
2425
- feat(codegen): split sidecar DDL by dialect; reject json sidecar (#45) (#133)
2526
- feat(codegen): split sidecar DDL by dialect; reject json sidecar (#45) (#131)

docs/decisions/0012-json-family-sidecar-storage.adoc

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,10 @@ relational invariants in application code with weaker guarantees* — is
5757
answered rather than simply contradicted: #148 introduces the abstraction
5858
(`StorageKind` + the `SidecarData` codec) and re-encodes those invariants in
5959
Rust with documented parity. The one acknowledged difference is concurrency:
60-
the JSON store is single-writer (atomic whole-file rewrite) where SQLite
61-
serialises via `BEGIN IMMEDIATE` locks. The guarantees SQL enforced
60+
the JSON store serialises writers itself — single-writer-by-assumption as of
61+
F3, and since V-L2-F4 (#151, ADR-0013) via a cross-process advisory lock held
62+
across the whole load→mutate→save cycle — where SQLite serialises via
63+
`BEGIN IMMEDIATE` locks. The guarantees SQL enforced
6264
structurally (one current temporal row, monotonic versions, fork-aware
6365
chains, hash identity) are now enforced in code for the json path and
6466
covered by tests mirroring the SQLite suite, across all three formats.
@@ -75,8 +77,9 @@ covered by tests mirroring the SQLite suite, across all three formats.
7577
### Negative
7678

7779
- The json path enforces invariants in application code rather than via SQL
78-
constraints, so it carries a heavier test burden and a documented
79-
single-writer limitation.
80+
constraints, so it carries a heavier test burden. (The single-writer
81+
constraint noted here was subsequently hardened into a cross-process
82+
advisory lock by V-L2-F4 — see ADR-0013.)
8083
- Two storage families to maintain in lockstep (the hash function is shared
8184
via `abi`, which mitigates the highest-risk drift).
8285
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
= Architecture Decision Record: 0013-json-sidecar-cross-process-locking
2+
<!-- SPDX-License-Identifier: PMPL-1.0-or-later -->
3+
<!-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> -->
4+
5+
# 13. Cross-process locking + atomic writes for the JSON sidecar
6+
7+
Date: 2026-05-31
8+
9+
## Status
10+
11+
Accepted (2026-05-31) — implemented in #151. Closes #150 (V-L2-F4).
12+
**Refines [ADR-0012](0012-json-family-sidecar-storage.adoc)** (V-L2-F3): it
13+
does not reverse the JSON-family decision, it hardens its concurrency story.
14+
15+
## Context
16+
17+
ADR-0012 implemented the JSON-family sidecar with crash-safe writes
18+
(temp file + same-directory atomic `rename`) but a **single-writer-by-
19+
assumption** concurrency model: it noted that the SQLite path serialises
20+
writers via the database write lock, whereas the JSON path assumed one
21+
writer at a time. Two processes that each ran the load→mutate→save cycle
22+
against the same sidecar could interleave and silently lose writes — a real
23+
hazard for `gc` running alongside an appending process.
24+
25+
The `provenance` and `history` CLI subcommands were also still stubs, so the
26+
JSON (and SQLite) sidecars could be written but not yet *queried* from the
27+
CLI.
28+
29+
## Decision
30+
31+
Harden the JSON store and wire the read-side CLI (#151):
32+
33+
* **Cross-process advisory write lock.** A dependency-free lock file
34+
(`<path>.lock`) is created exclusively, with bounded backoff and a
35+
stale-lock *steal* heuristic, and released via RAII on drop. It is held
36+
across the **entire** load→mutate→save cycle, so a concurrent `gc` and
37+
`append` serialise instead of clobbering each other. (`src/sidecar/lock.rs`.)
38+
* **Atomic durability, formalised.** The temp-file + same-directory `rename`
39+
in `save()` is documented as the durability boundary and is now performed
40+
under the write lock.
41+
* **`provenance` / `history` wired.** `provenance <entity>` lists the chain
42+
and reports `verify_chain` plus any fork points; `history <entity>
43+
[--at T]` lists temporal versions (or the point-in-time snapshot at `T`).
44+
Both work against the SQLite *and* JSON backends; `postgres` reads remain
45+
an explicit "not yet implemented".
46+
* **Release.** Version `0.1.0``0.2.0`, with a CHANGELOG entry.
47+
48+
## Consequences
49+
50+
### Positive
51+
52+
- Concurrent cross-process writers to a JSON sidecar are safe: serialised by
53+
the advisory lock and crash-safe via atomic rename. This closes the main
54+
parity gap ADR-0012 called out against the SQLite store.
55+
- No new dependencies — the lock is built on the standard library.
56+
- `provenance` and `history` now function against real sidecars, for both
57+
storage families.
58+
59+
### Negative
60+
61+
- The lock is **advisory** (cooperative) with a stale-steal heuristic: a
62+
writer wedged past the staleness window can have its lock stolen. This is
63+
the documented trade-off of a dependency-free, portable lock.
64+
- Writes remain whole-file `O(n)` rewrites (no incremental append for the
65+
JSON encodings) — acceptable for the small-to-medium sidecars this store
66+
targets, but not for very large histories.
67+
- `postgres` provenance/history reads are still unimplemented (tracked
68+
separately).
69+
70+
## References
71+
72+
- Issue #150 (V-L2-F4); PR #151.
73+
- Refines ADR-0012 (V-L2-F3, the JSON-family backend; issue #146 / #148).
74+
- ADR-0010 (provenance forks are first-class) — the chain semantics the
75+
JSON store mirrors.

0 commit comments

Comments
 (0)