Skip to content

Commit 00fd80f

Browse files
feat(sidecar): cross-process locking + atomic writes, wire provenance/history CLI (V-L2-F4, #150) (#151)
Harden the JSON sidecar with dependency-free cross-process write locking (advisory <path>.lock with stale-steal, RAII release) held across the load→mutate→save cycle, plus documented atomic-rename durability. Wire the `provenance` and `history` CLI subcommands to query the sidecar (sqlite + json; postgres refuses). Docs (README backend section + matrix), json example, version 0.1.0→0.2.0 + CHANGELOG. Closes #150
1 parent f8815bc commit 00fd80f

14 files changed

Lines changed: 698 additions & 16 deletions

File tree

CHANGELOG.adoc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,27 @@ All notable changes to verisimiser will be documented in this file.
88
This format is based on https://keepachangelog.com/en/1.1.0/[Keep a Changelog],
99
and this project adheres to https://semver.org/spec/v2.0.0.html[Semantic Versioning].
1010

11+
== [0.2.0] - 2026-05-30
12+
13+
=== Added
14+
* JSON-family sidecar storage backend (`[sidecar].storage = "json"`) with a
15+
`[sidecar].format` key — `plain` | `ld` (JSON-LD) | `ndjson` — at full
16+
runtime parity with the SQLite path: hash-chained provenance (incl. forks),
17+
temporal versioning, drift, and gc (#146, V-L2-F3).
18+
* `sidecar::StorageKind` — single source of truth resolving
19+
`[sidecar].storage` (+ `format`) to a backend; `validate`/`generate`/
20+
`drift`/`gc` all defer to it.
21+
* Cross-process write locking for the JSON sidecar (advisory `<path>.lock`
22+
with stale-steal) plus documented atomic-rename durability (#150, V-L2-F4).
23+
* `provenance <entity>` and `history <entity> [--at <RFC3339>]` CLI
24+
subcommands now query the sidecar (sqlite + json) instead of stubbing
25+
(#150, V-L2-F4).
26+
* `examples/json-sidecar/` manifest demonstrating the NDJSON sidecar.
27+
28+
=== Changed
29+
* `verisimiser generate` emits a `sidecar_schema.{json,jsonld,ndjson}`
30+
scaffold for the json family (SQL backends still emit DDL).
31+
1132
== [0.1.0] - 2026-03-21
1233

1334
=== Phase 1 — RSR Compliance Sweep

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# SPDX-License-Identifier: PMPL-1.0-or-later
22
[package]
33
name = "verisimiser"
4-
version = "0.1.0"
4+
version = "0.2.0"
55
edition = "2024"
66
rust-version = "1.85"
77
authors = ["Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>"]

README.adoc

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,55 @@ The `verisimiser octad` subcommand prints the active concerns from your
266266
manifest; `verisimiser doctor` checks that sidecars, thresholds, and
267267
retention bounds are configured consistently.
268268

269+
=== Sidecar storage backends (Phase 1 schema)
270+
271+
The Phase 1 manifest uses a single `[sidecar]` section. All octad data
272+
(provenance log, temporal versions, lineage edges, access policies) lives
273+
in one sidecar store — never in your target database:
274+
275+
[source,toml]
276+
----
277+
[sidecar]
278+
# storage backend: "sqlite" (default), "postgres"/"postgresql", or "json"
279+
storage = "json"
280+
# json on-disk encoding (ignored for sql backends): "plain" | "ld" | "ndjson"
281+
format = "ndjson"
282+
path = ".verisim/sidecar.ndjson"
283+
----
284+
285+
* **sqlite** (default) — the reference store; embedded and transactional.
286+
* **postgres** — the same overlay schema, emitted as PostgreSQL DDL by `generate`.
287+
* **json** — an append-only document store mirroring the `verisimdb_*` tables,
288+
in one of three encodings:
289+
+
290+
--
291+
** **plain** — one JSON object keyed by table name.
292+
** **ld** — JSON-LD (`@context` + a `@graph` of typed, `@id`-addressed nodes), for linked-data tooling.
293+
** **ndjson** — newline-delimited JSON, one record per line.
294+
--
295+
296+
The JSON store has full parity with the SQLite runtime path: hash-chained
297+
provenance (including first-class forks), temporal versioning, drift, and
298+
gc. Writes are crash-safe (atomic temp-file + `rename`) and serialised
299+
across processes by an advisory lock file (`<path>.lock`).
300+
301+
[cols="2,1,1,1",options="header"]
302+
|===
303+
| Command | sqlite | postgres | json
304+
305+
| `generate` | DDL | DDL | scaffold
306+
| `provenance` / `history` | ✓ | — | ✓
307+
| `drift` / `gc` | ✓ | — | ✓
308+
|===
309+
310+
`generate` writes `sidecar_schema.sql` for SQL backends and a
311+
`sidecar_schema.{json,jsonld,ndjson}` scaffold for the json family.
312+
`verisimiser provenance <entity>` prints the entity's chain and its
313+
verification status (plus any fork points); `verisimiser history <entity>
314+
[--at <RFC3339>]` lists temporal versions, or the point-in-time snapshot per
315+
modality. PostgreSQL *runtime* reads (`provenance`/`history`/`drift`/`gc`)
316+
are not yet implemented and refuse explicitly rather than silently no-op.
317+
269318
== Architecture
270319

271320
[source]

examples/README.adoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
12
= examples Pillar
3+
4+
Worked examples of `verisimiser.toml` manifests.
5+
6+
* **blog-db/** — augment a typical blog database (SQLite target) with the
7+
octad dimensions, using the default SQLite sidecar.
8+
* **json-sidecar/** — the same octad augmentation, but stored in a
9+
JSON-family (NDJSON) sidecar instead of SQLite. Shows `[sidecar].storage =
10+
"json"` + `format`.

examples/blog-db/verisimiser.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,7 @@ enable-simulation = false
2323

2424
[sidecar]
2525
storage = "sqlite"
26+
# To use the JSON document store instead (see examples/json-sidecar):
27+
# storage = "json"
28+
# format = "ndjson" # "plain" | "ld" | "ndjson"
2629
path = ".verisim/sidecar.db"

examples/json-sidecar/README.adoc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
= JSON sidecar example
3+
4+
Demonstrates the JSON-family sidecar backend (V-L2-F3/F4): `[sidecar].storage
5+
= "json"` with `format = "ndjson"`. The octad data (provenance, temporal,
6+
lineage, access policies) is stored as an append-only NDJSON document store
7+
mirroring the `verisimdb_*` tables, with the same runtime parity as SQLite —
8+
hash-chained provenance, temporal versioning, drift, and gc.
9+
10+
[source,sh]
11+
----
12+
# Validate the manifest (checks storage + format resolve):
13+
verisimiser validate -m examples/json-sidecar/verisimiser.toml
14+
15+
# Emit the JSON sidecar scaffold:
16+
verisimiser generate -m examples/json-sidecar/verisimiser.toml -o .verisim
17+
18+
# After data accrues, query it:
19+
verisimiser provenance -m examples/json-sidecar/verisimiser.toml <entity-id>
20+
verisimiser history -m examples/json-sidecar/verisimiser.toml <entity-id> --at 2026-01-01T00:00:00Z
21+
verisimiser gc -m examples/json-sidecar/verisimiser.toml --dry-run
22+
----
23+
24+
Set `format` to `"ld"` for JSON-LD (`@context` + `@graph`) or `"plain"` for a
25+
single keyed JSON object. Writes are crash-safe (atomic rename) and serialised
26+
across processes by a `<path>.lock` advisory lock.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# VeriSimiser manifest: JSON-family (NDJSON) sidecar example.
3+
#
4+
# Same octad augmentation as the blog-db example, but the sidecar is an
5+
# append-only NDJSON document store instead of SQLite — handy when you want
6+
# the provenance/temporal data as plain, diff-/grep-able text. Switch
7+
# `format` to "ld" for JSON-LD (linked-data tooling) or "plain" for a single
8+
# keyed JSON object.
9+
10+
[project]
11+
name = "json-sidecar-example"
12+
version = "0.1.0"
13+
description = "Example: octad augmentation with a JSON-family (NDJSON) sidecar"
14+
15+
[database]
16+
backend = "sqlite"
17+
connection-string-env = "APP_DATABASE_URL"
18+
# schema-source = "schema.sql" # optional; omitted here for a self-contained example
19+
20+
[octad]
21+
enable-provenance = true
22+
enable-lineage = true
23+
enable-temporal = true
24+
enable-access-control = true
25+
enable-simulation = false
26+
27+
[sidecar]
28+
storage = "json"
29+
format = "ndjson" # "plain" | "ld" | "ndjson"
30+
path = ".verisim/sidecar.ndjson"

src/gc.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,20 @@ fn run_gc_json(
6565
dry_run: bool,
6666
format: crate::sidecar::JsonFormat,
6767
) -> Result<GcReport> {
68+
use crate::sidecar::json::{self, JsonStore};
6869
let sidecar_path = &manifest.sidecar.path;
69-
let mut store = crate::sidecar::json::JsonStore::open(sidecar_path, format)
70-
.with_context(|| format!("opening json sidecar at {}", sidecar_path))?;
71-
let counts = store.gc_purge(&manifest.retention, dry_run);
72-
if !dry_run {
73-
store
74-
.save()
75-
.with_context(|| format!("saving json sidecar at {}", sidecar_path))?;
76-
}
70+
let retention = &manifest.retention;
71+
let counts = if dry_run {
72+
// Read-only: count purge candidates without locking or writing.
73+
let mut store = JsonStore::open(sidecar_path, format)
74+
.with_context(|| format!("opening json sidecar at {}", sidecar_path))?;
75+
store.gc_purge(retention, true)
76+
} else {
77+
// Mutating: hold the cross-process write lock across load→purge→save.
78+
json::with_locked(sidecar_path, format, |store| {
79+
Ok(store.gc_purge(retention, false))
80+
})?
81+
};
7782
Ok(GcReport {
7883
sidecar: sidecar_path.clone(),
7984
dry_run,

0 commit comments

Comments
 (0)