Skip to content

chore(release): 0.6.28 — the lock becomes law (#107) - #108

Merged
styk-tv merged 2 commits into
mainfrom
release-v0.6.28
Aug 12, 2026
Merged

chore(release): 0.6.28 — the lock becomes law (#107)#108
styk-tv merged 2 commits into
mainfrom
release-v0.6.28

Conversation

@styk-tv

@styk-tv styk-tv commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Closes #107. The one boundary in this release line that reported protection without enforcing it becomes enforcing.

What was wrong (measured on 0.6.27)

pgrdf_checkpoint reported locked: true; SELECT pgrdf.clear_graph(...) emptied the locked graph anyway (returned 8, quads 0); the MCP door then refused the repair import — locked and empty, sanctioned route dead. The lock lived in pgrdf_mcp.ledger and only the door's own guardNotLocked() consulted it.

The change

Lock state lives on pgrdf._pgrdf_graphs (locked, lock_reason, locked_at). require_unlocked() is called by every engine write path — 13 hooks / 12 paths:

clear_graph · drop_graph · move_graph (src and dst — move clears src) · copy_graph/carve_graph (dst — src is read) · put_quad · put_construct_row(s) · ingest_dispatch (one hook, whole parse_/load_turtle family) · load_turtle_streaming · load_turtle_staged_run · parse_trig/parse_nquads (parameter graph; payload-named graphs are a documented limitation) · materialize (writes inferred rows)

Reads never blocked — a lock is a write fence.

pgrdf.lock_graph(id, reason) / unlock_graph(id, reason) — reasons mandatory, double-lock refuses, unlock-of-unlocked refuses.

Scope, stated plainly (so #107 cannot recur one level up)

Coordination primitive, not a security boundary. Anyone who can write the graph can lock/unlock it. Security remains grants (partitions inherit since 0.6.25).

Cut mechanics

DDL + both functions in sql/pgrdf--0.6.27--0.6.28.sql; the 0.5.1 bridge replays the delta; Rule 7 reconciled across all six sites.

Verification

main suite   362 passed, 0 failed  (was 356; six new lock tests)
toll         ZERO existing tests broke — nothing ever locked a graph before
test note    per-path probes run in PL/pgSQL subtransactions; a catch_unwind
             version would have passed probes 2-12 vacuously — caught pre-run
fmt clean · clippy -D warnings clean

Step 7 (pgRDF.MCP migrates to pgrdf.lock_graph) follows the release — their ledger keeps lifecycle, the engine keeps the lock.

The checkpoint lock lived in pgrdf_mcp.ledger and only the MCP door's own
writes consulted it. Measured: SQL clear_graph emptied a LOCKED graph
(returned 8, quads 0) and the door then refused the repair import --
locked and empty, the sanctioned route dead. The one boundary in this
release line that reported protection without enforcing it.

Lock state now lives on pgrdf._pgrdf_graphs (locked, lock_reason,
locked_at -- idempotent ALTERs, shipped both in the generated install and
the upgrade script). require_unlocked() is called by EVERY engine write
path: clear_graph, drop_graph, move_graph (src AND dst -- move clears
src), copy_graph/carve_graph (dst only -- src is read), put_quad,
put_construct_row(s), ingest_dispatch (one hook covers the whole
parse_/load_turtle family), load_turtle_streaming, load_turtle_staged_run,
parse_trig/parse_nquads (parameter graph; payload-named graphs are a
documented limitation), and materialize (it writes inferred rows). Reads
are never blocked: a lock is a write fence, not a read fence.

lock_graph/unlock_graph take mandatory reasons -- the reason IS the
record. Double-lock refuses rather than swallowing the standing reason;
unlocking an unlocked graph refuses rather than validating a wrong model
of the state.

SCOPE, stated in the module header so #107 cannot recur one level up:
this is a COORDINATION primitive, not a security boundary. Anyone who can
write the graph can lock/unlock it. Security remains grants.

Test note: the per-path refusal probes run in PL/pgSQL exception blocks
(subtransaction rollback per probe). A catch_unwind version would have
left SPI aborted after probe 1 and passed probes 2-12 for the wrong
reason -- caught before it ever ran.

Suite: 362 passed, 0 failed (was 356). No existing test broke: nothing
ever locked a graph before, so the tightening pays no regression toll.
@styk-tv styk-tv added this to the v0.6.28 milestone Aug 11, 2026
@styk-tv styk-tv added the release Release cut / version bump label Aug 11, 2026
…lock columns

The baseline pins the on-disk shape and its ELSE FALSE arm tripped on the
three new columns — exactly its job. The contract now names them: locked
boolean NOT NULL, lock_reason text NULL, locked_at timestamptz NULL. The
golden is unchanged: the check still collapses to one 't'.
@styk-tv
styk-tv merged commit 4a4a53c into main Aug 12, 2026
4 checks passed
@styk-tv
styk-tv deleted the release-v0.6.28 branch August 12, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release Release cut / version bump

Projects

None yet

Development

Successfully merging this pull request may close these issues.

checkpoint lock is enforced at the MCP layer only — SQL clear_graph empties a locked graph, and the lock then blocks the sanctioned repair

1 participant