<!--nav-->
**index** · [next](s0-s3-foundations.md) →
<!--/nav-->
# Macrame — Consolidated Architecture
**A Bitemporal Graph Ledger on libSQL · Hybrid Path A/D**
| | |
|---|---|
| Document version | 0.17.0 (consolidated — supersedes v0.1, v0.2, v0.3-Amendment-A, v0.4.0, v0.4.5, v0.5.0, v0.5.1, v0.5.2, v0.5.3, v0.5.4, v0.5.5, v0.5.6, v0.6.0, v0.7.0, v0.8.0, and v0.9.0, each of which was a separate document; from 0.10.0 the document *is* this directory and the revision history below is the record) |
| Date | 2026-08-29 |
| Status | Approved for implementation. [§4](s4-schema.md#4-schema) (schema), [Appendix A](appendices.md#appendix-a--public-api-normative) (API) and [Appendix D](appendices.md#appendix-d--the-stability-contract-normative) (what 1.0 freezes) are the normative surfaces; all other sections are rationale and mechanism. **This file was damaged in transport between 0.5.2 and 0.5.4** — every `#` heading marker stripped, `_` eaten out of identifiers as markdown emphasis, fenced code blocks reduced to a bare language name, and every span between a `<` and the next `>` consumed, sometimes across dozens of lines. It is now fully repaired: headings and fences restored, mangled identifiers corrected throughout, and the eaten spans rebuilt — [§4.1](s4-schema.md#41-concepts-and-per-model-embeddings)'s `retired`/`valid_to` note and its embedding DDL, [§4.3](s4-schema.md#43-the-transaction-log)'s **entire trigger set** (recovered from `schema::ddl`, which turned out to disagree with the surrounding prose in three ways, recorded in place), [§8](s6-s10-flows-to-dependencies.md#8-testing-strategy)'s test list, and the generics throughout [§5.1](s5-modules.md#51-connectionrs--the-handle-the-pragmas-and-the-write-actor)'s sketches. [§5.2](s5-modules.md#52-graphbuilderrs--traversal-valid-time-and-attribute-fidelity)–[§5.9](s5-modules.md#59-vector--embeddings-the-model-registry-and-search), [§6](s6-s10-flows-to-dependencies.md#6-data-flows) and [Appendix A](appendices.md#appendix-a--public-api-normative) had been lost outright and are restored from the 0.5.1 source, forward-ported to the canonical timestamp form ([D-029](s13-decision-register.md#d-029)), the revised archive-session marker ([D-008](s13-decision-register.md#d-008) revised), the cold-database read path ([D-026](s13-decision-register.md#d-026)), the re-derived materialization ([D-035](s13-decision-register.md#d-035)) and native analytics ([D-039](s13-decision-register.md#d-039)). [Appendix A](appendices.md#appendix-a--public-api-normative) is additionally rewritten against the implementation, with the divergences tabulated rather than erased ([D-040](s13-decision-register.md#d-040)). |
| Target platform | Windows desktop, embedded, single-file, no server process |
| Core stack | libSQL (MIT, unmodified) · Rust, async |
| Audience | Implementing engineers, reviewers, future maintainers |
## Revision history
| Version | Cycle | Substance |
|---|---|---|
| 0.1.0 | Initial draft | libSQL-as-dependency doctrine; CTE traversal; DiskANN vectors; CDC replay; checkpoints |
| 0.2.0 | Review cycle 1 | Axis separation in retirement; 5-column bitemporal PK; links_current materialization; transaction_log replaces CDC; per-model embeddings; properties; archiving |
| 0.3.0-A | Review cycle 2 | AttributeMode with log hydration; in-engine vector distance; byte-budget cost model; rebuild_current() |
| 0.4.0 | Consolidation | Single authoritative document; full prose rationale; corrected archive-guard formulation; decision register; glossary |
| 0.4.5 | Amendment A | Priority Write Actor: sole write connection in dedicated task; two-tier biased channels; cooperative chunking (estimated at 500–1,000 rows here; measured per path in 0.5.6, [D-058](s13-decision-register.md#d-058)); chunked-write fidelity boundary; writer-aware shutdown and snapshot coordination; WriterUnavailable / WriterDroppedResponder containment; concurrency tests; [D-009](s13-decision-register.md#d-009)…[D-016](s13-decision-register.md#d-016) |
| 0.5.0 | Review cleanup 1 | Identifier unification to snake_case; SQL repairs (json_object, temp.sqlite_master, malformed CTE); integrity additions (recorded_at monotonicity trigger, PRAGMA query_only, PRAGMA recursive_triggers); "checkpoint" → "snapshot" rename; [§9](s6-s10-flows-to-dependencies.md#9-performance-budgets) performance table completed; [D-017](s13-decision-register.md#d-017)…[D-021](s13-decision-register.md#d-021) |
| 0.5.1 | Review cleanup 2 | seq_id "gap-free" → "strictly monotonic" (rollback gaps documented); archive scope defined (concepts never physically archived — FK/DiskANN rationale); rebuild_current() sizing note and operational guidance; retired/valid_to semantic clarification; SystemClock monotonicity floor specification; assert→retire→re-assert scenario test; INSTR cycle-detection performance note; TEMP table connection-locality documented; snapshot file cleanup noted; [D-022](s13-decision-register.md#d-022)…[D-025](s13-decision-register.md#d-025) |
| 0.5.2 | Final consolidation | Cold-database ATTACH read path for reconstruct(ts) specified ([§5.5](s5-modules.md#55-temporalreplayrs-and-temporalsnapshotrs--reconstruction-and-snapshots), [D-026](s13-decision-register.md#d-026)); strict SystemClock ISO-8601 parsing contract with graceful startup fallback ([§5.1.2](s5-modules.md#512-handle-shape-and-the-clock-contract), [D-027](s13-decision-register.md#d-027)); write-queue latency semantics and timeout-≠-cancellation rule documented ([§5.1.8](s5-modules.md#518-write-queue-latency-and-caller-timeouts-052-d-028), [D-028](s13-decision-register.md#d-028)); [R14](s11-s12-milestones-and-risks.md#r14) added |
| 0.5.3 | Implementation cycle 1 | [D-008](s13-decision-register.md#d-008) corrected: the `temp.sqlite_master` archive-guard probe is unimplementable — SQLite rejects cross-database references at CREATE TRIGGER time — and is replaced by a `main.sqlite_master` marker whose lifetime is bounded by the archive transaction; `trg_concepts_guard_delete` becomes unconditional per [D-022](s13-decision-register.md#d-022); a delete guard is added to transaction_log; archive-session behaviour under lock contention documented |
| 0.5.4 | Implementation cycle 2 | **Schema change.** [§4.1](s4-schema.md#41-concepts-and-per-model-embeddings) now mandates a single fixed timestamp *width*, not merely a `Z` suffix: every temporal column is exactly `YYYY-MM-DDTHH:MM:SS.ffffffZ`, enforced by `CHECK` on all four tables, and the open-interval sentinel widens to `9999-12-31T23:59:59.999999Z` ([D-029](s13-decision-register.md#d-029)). `audit_current()` corrected to count the symmetric difference — the 0.4.5–0.5.3 query reduced to a constant zero and certified any corruption as clean ([D-030](s13-decision-register.md#d-030)). `format_iso8601_utc` / `parse_iso8601_utc` given real implementations ([D-031](s13-decision-register.md#d-031)). Migrations become a real `user_version` ladder with a legacy-free baseline at v2 ([D-032](s13-decision-register.md#d-032)). `reconstruct()`'s ATTACH is released on all paths and snapshots are written atomically. Engine-error classification is concentrated in one boundary ([D-033](s13-decision-register.md#d-033)); the write commands carry typed payloads and both actor matches become exhaustive ([D-034](s13-decision-register.md#d-034)). Model-based property suites added for [Doctrine VI](s0-s3-foundations.md#doctrine-vi) and for the doctrine as a whole; the first of them found a live bitemporal defect in `archive()`, whose `links_current` compensation filtered on valid time while its own archivability predicate also required transaction time, so `archive()` now re-derives the materialization rather than describing the deletion's shadow ([D-035](s13-decision-register.md#d-035)). The 1.0 on-disk compatibility contract is settled — ledger tables frozen and additive-only, derivative tables explicitly rebuildable — with executable tests for both halves ([D-036](s13-decision-register.md#d-036)). Dependency moved to libSQL 0.9.30; [R15](s11-s12-milestones-and-risks.md#r15) rediagnosed as a concurrency fault rather than a volume one, mitigated by serialising libtest and gating the generated-history binaries behind a `property-tests` feature. Vector path implemented: per-model embedding tables and their DiskANN indexes are created by `register_model()`, model names are validated as SQL identifiers by a `ModelName` newtype (the search path previously spliced an unvalidated caller string into a table name), `search_vector()` goes through `vector_top_k` and `vector_distance_cos` — it had been calling `vector_distance`, which does not exist, so the path had never executed — and the tautological dimension check is replaced by the dimension declared in the schema. [§4.1](s4-schema.md#41-concepts-and-per-model-embeddings)'s claim about engine-side enforcement corrected ([D-037](s13-decision-register.md#d-037)); migration verification changed from counting `sqlite_master` to checking required objects by name ([D-038](s13-decision-register.md#d-038)). petgraph dropped for a native adjacency-list Subgraph and five in-crate algorithms, validated by brute-force oracles rather than by a dependency’s track record; the Louvain that was replaced was a stub returning one community per node. Traversal edge types become bind parameters (they were spliced into the CTE as literals on a read path nothing validated), and `TraversalBuilder::attribute_mode` is now read rather than silently ignored. `SubgraphTooLarge` is constructed for the first time, enforcing [D-007](s13-decision-register.md#d-007)’s byte budget ([D-039](s13-decision-register.md#d-039)). **Document restoration:** [§5.2](s5-modules.md#52-graphbuilderrs--traversal-valid-time-and-attribute-fidelity)–[§5.8](s5-modules.md#58-integrity--audit-and-rebuild) and [§6](s6-s10-flows-to-dependencies.md#6-data-flows) were destroyed by a transport corruption that consumed everything between each `<` and the next `>`, and are restored from the 0.5.1 source and forward-ported; [§5.3](s5-modules.md#53-graphvector_filterrs--strategies-and-the-byte-budget-cost-model), [§5.4](s5-modules.md#54-graphsubgraphrs-and-graphalgorithmsrs--native-in-memory-analytics) and [§5.9](s5-modules.md#59-vector--embeddings-the-model-registry-and-search) are recovered from the 0.4.5 document, which carried the material 0.5.1 had reduced to "unchanged from 0.4.0" stubs — the byte-budget cost formulas, the `analytics_annotations` design, and the whole vector-module section that the 0.5.x renumbering dropped without relocating (it had been [§5.6](s5-modules.md#56-temporalas_ofrs--valid-time-queries-and-attribute-hydration); it is [§5.9](s5-modules.md#59-vector--embeddings-the-model-registry-and-search) here, and `search.rs`'s stale citations are repointed). Three premises of the [§5.3](s5-modules.md#53-graphvector_filterrs--strategies-and-the-byte-budget-cost-model) design are recorded as unestablished, one of them measured false: `PRAGMA query_only` rejects `CREATE TEMP TABLE`, so `TwoPhaseTempTable` cannot run on the read connection as configured. Recovering [§5.4](s5-modules.md#54-graphsubgraphrs-and-graphalgorithmsrs--native-in-memory-analytics) surfaced a live defect and it is fixed here: analytics write-back overwrote `concepts.content` with the annotation value and routed derived labels into the ledger, so the schema gains the derivative `analytics_annotations` table ([§4.5](s4-schema.md#45-analytics-annotations--the-second-derivative-table-054-d-041)) on a `v2 → v3` rung — the [D-032](s13-decision-register.md#d-032) ladder's first step beyond the baseline — with `Annotation`, `Database::write_analytics_annotations`, and four regression tests ([D-041](s13-decision-register.md#d-041)). [Appendix A](appendices.md#appendix-a--public-api-normative) is rewritten against the crate with its divergences tabulated ([D-040](s13-decision-register.md#d-040)). **Document de-corruption pass:** the remaining sections — [§0](s0-s3-foundations.md#0-doctrine)–[§4](s4-schema.md#4-schema), [§7](s6-s10-flows-to-dependencies.md#7-errors)–[§13](s13-decision-register.md#13-decision-register) and Appendices B–C — are repaired to match, which recovered [§4.3](s4-schema.md#43-the-transaction-log)'s trigger DDL from `schema::ddl` and exposed three prose-versus-schema divergences there (trigger names, the total absence of any delete-logging trigger, and `embedding_model` missing from concept payloads); [§9](s6-s10-flows-to-dependencies.md#9-performance-budgets) is annotated to say that its budgets are not CI gates and nothing measures them; [§11](s11-s12-milestones-and-risks.md#11-milestones)'s milestone gates are marked against what is actually delivered. **External review, three accepted and two declined:** a covering index for the traversal CTE replaces `idx_lc_src_active`, making the recursive step index-only, with the column order settled by `EXPLAIN QUERY PLAN` after the first reasoning about it proved to be an artifact of the subsumed index still being present ([D-042](s13-decision-register.md#d-042)); snapshots gain a versioned header so an incompatible file is refused rather than deserialized into wrong values ([D-043](s13-decision-register.md#d-043)); a best-effort DETACH before each ATTACH closes the panic-shaped hole that pairing cannot, the proposed RAII guard being unusable because `Drop` cannot await ([D-044](s13-decision-register.md#d-044)). Actor `catch_unwind` and single-flight coalescing of `reconstruct` are declined with reasons recorded ([D-045](s13-decision-register.md#d-045)). A second review, proposing five of `petgraph`'s internal design choices for `Subgraph`, found two already implemented and two real-but-unmeasured — and led to a defect neither it nor the original work had noticed: the byte-budget check ran per row and made loading O(E²), fixed with incremental accounting and a mutation-verified growth-rate test ([D-047](s13-decision-register.md#d-047)). Snapshot composition, recorded as specified-and-not-implemented ([D-046](s13-decision-register.md#d-046)), is then implemented: `reconstruct` composes the newest snapshot at or before `ts` with an anchored `seq_id > :anchor` fold, tombstones and all, gated by a property test that the composed and full-fold answers agree over generated histories — and the anchored fold is the first code [D-024](s13-decision-register.md#d-024)'s inequality rule has ever bound. Measuring that rule showed its stated mechanism was wrong: a rolled-back transaction does *not* leave a `seq_id` gap, because `sqlite_sequence` rolls back with it; the archive's scattered deletions are where gaps actually come from ([D-049](s13-decision-register.md#d-049)). **The vector write path is closed:** `Database::register_model` and `Database::upsert_embeddings` route through the Write Actor, so Phase 3 is reachable from the public API for the first time — it had been green and unreachable, its suite passing because every test opened its own connection ([D-048](s13-decision-register.md#d-048)) |
| 0.5.5 | Implementation cycle 3 | **Phase 5 closed and filtered vector search implemented.** The [§8](s6-s10-flows-to-dependencies.md#8-testing-strategy) test matrix is complete: [Doctrine VIII](s0-s3-foundations.md#doctrine-viii) pinned by requiring `as_of` and `reconstruct` to *differ* across a retroactive correction, in both directions — belief withdrawn and belief added, two tests because an implementation can lose one and keep the other; archive crash safety pinned by failing a session at both dangerous points and requiring the delete guards to re-arm ([D-012](s13-decision-register.md#d-012)); and [Doctrine VII](s0-s3-foundations.md#doctrine-vii) given a generated suite driven through the handle alone ([D-048](s13-decision-register.md#d-048)), including one property that runs a history twice, with and without its embedding writes, because "the derivative is not an input to the ledger" is not a claim one database can be asked. [§5.3](s5-modules.md#53-graphvector_filterrs--strategies-and-the-byte-budget-cost-model) is implemented as `FilteredVectorSearch` and **loses a strategy**: measuring the three premises 0.5.4 had recorded as unestablished showed `TwoPhaseTempTable` has neither of its two mechanisms — `CREATE TEMP TABLE` on the read connection returns `SQLITE_READONLY (8)` under [D-019](s13-decision-register.md#d-019), and there is no allow-list to push into, `vector_top_k` refusing a fourth argument and `vectorIndexSearch` rejecting `argc != 3` in the bundled amalgamation — so it is removed on [D-039](s13-decision-register.md#d-039)'s precedent rather than kept as a name with nothing behind it ([D-050](s13-decision-register.md#d-050)). `CostEstimator` now reads the `byte_budget` it had carried unused, selectivity is *measured* by a bounded counting probe that doubles as the candidate set, and the plan is returned as a `CostEstimate` rather than only logged — closing [D-007](s13-decision-register.md#d-007)'s empirical requirement with a value a test can assert on instead of log output it would have to scrape. The governing rule is that **the strategy may never change the answer**: a `PostFilter` pass that comes back short from a *saturated* index scan escalates to the exact strategy, and the acceptance gate is that the two agree across filter tightness and k. **Two process findings.** An un-reverted mutation was found in `archive.rs` — the archive-session marker created before `BEGIN`, as committed state — which broke every archive across four test binaries and was invisible because `cargo test` without `--no-fail-fast` stops at the one binary already known to fail and never reaches the eleven behind it. And two newly written tests were caught asserting nothing before they were trusted: the chunk-merge test exercised a single chunk while claiming to cross the boundary, and would have passed even sized up, because candidate ids arrive in id order while the fixture made distance monotone in id. |
| 0.5.5b | Hybrid search | **[§5.9](s5-modules.md#59-vector--embeddings-the-model-registry-and-search)'s keyword arm exists.** `concepts_fts` arrives in [§4.6](s4-schema.md#46-the-concept-text-index--the-third-derivative-table-055-d-051) on a `v4 → v5` rung as an FTS5 **external-content** index — the tokens are indexed and the text is not duplicated, so the index cannot come to disagree with the concept about what the concept says, and [D-036](s13-decision-register.md#d-036)'s rebuildability becomes the engine's own `'rebuild'` command rather than code of ours that could drift from the triggers ([D-051](s13-decision-register.md#d-051)). The rung backfills, which [D-041](s13-decision-register.md#d-041)'s could not. `HybridSearch` fuses the vector and keyword arms by RRF at k = 60, reading each arm to a depth of `max(5 × top_k, 50)` because fusing two top-k lists is not the top k of the fusion. Three things are recorded because they are the parts that fail quietly: the update trigger must **retract** old terms using the OLD values or the index goes on matching text the concept no longer contains; arbitrary text is escaped before reaching MATCH, since FTS5 syntax makes a malformed query an exception and `NOT` a *wrong answer*; and there is no delete trigger only because [D-022](s13-decision-register.md#d-022)'s guard is unconditional — a dependency running the wrong way round, where a change to the archive would break the search index. `reciprocal_rank_fusion` is corrected to break ties by id: it sorted on score alone, and ties are the common case, so the same query could answer in a different order twice ([D-047](s13-decision-register.md#d-047)'s determinism trap as a search result that will not sit still). **The suite is green for the first time in several cycles (177/177).** The last red was `a_high_priority_write_completes_while_the_backlog_is_still_queued`, and it was the test that was wrong: it `.await`ed the probe, which yields, so the actor drained all forty queued chunks before the probe's command ever reached the channel — and its `COUNT == 0` assertion was a wall-clock race besides, where [§8](s6-s10-flows-to-dependencies.md#8-testing-strategy) requires an ordering property over committed `seq_id`s. Preempting already-accepted work is not something two-tier channels can do. |
| 0.5.5c | Archive read path | **A live wrong-answer defect, found by taking [D-049](s13-decision-register.md#d-049)'s carve-out seriously ([D-052](s13-decision-register.md#d-052)).** `reconstruct(ts)` decided whether it needed the cold file by asking `MIN(recorded_at) <= ts` — how far back the hot log *reaches*, not whether it is *complete*. `LOG_ARCHIVABLE` removes **superseded** rows, scattered rather than in a prefix, so one entity archived beside one entity never superseded separates the two questions: the unarchived one keeps `MIN` pointing before the cutoff while the archived one's winning row sits in cold. Reproduced through the public API — a concept **vanished entirely** from a pre-cutoff reconstruction, with no error, which is the failure [Doctrine II](s0-s3-foundations.md#doctrine-ii) exists to prevent. The sound test rests on the one guarantee the archive makes, that the newest row per entity is never archivable: hot alone suffices exactly when `ts` is at or after the newest hot stamp, which is `reconstruct(now)` and nothing else. Two tempting widenings are recorded as unsound rather than left for someone to rediscover. Closing it also closed the composition carve-out for free — `ANCHORED_COLD_FOLD` unions both files above the anchor, so the row the delta was missing is visible again — and the test that had pinned the refusal is replaced by [D-049](s13-decision-register.md#d-049)'s own gate extended across the boundary: composed equals full-fold on both sides of the cutoff, mutation-verified by folding the delta over hot alone. **The lesson recorded with it** is about the register rather than the code: this was written down in 0.5.4 as "not a sound completeness test" and sat there, because an unsound test named as a caveat reads as a known limitation instead of as a bug. |
| 0.5.5d | Snapshot cadence | **[D-049](s13-decision-register.md#d-049)'s second carve-out closed, and with it both ([D-053](s13-decision-register.md#d-053)).** [§5.5](s5-modules.md#55-temporalreplayrs-and-temporalsnapshotrs--reconstruction-and-snapshots) had specified a read-side maintenance task writing an anchor every 10,000 log entries; only `close()` ever wrote one, so a long-running session accumulated an unbounded delta and [§9](s6-s10-flows-to-dependencies.md#9-performance-budgets)'s "≤ 200 ms with snapshot" described a mechanism that ran at exit. The specified design is honoured — a fold plus a file write, never the write connection, so [§5.1.5](s5-modules.md#515-cooperative-chunking--the-golden-rule)'s latency bound is untouched — and the lifecycle it was left open on is settled: `Database` owns the task, a `watch` channel stops it (a dropped sender counts, so a handle dropped rather than closed leaves nothing running), and `close()` stops and joins it **before** stopping the actor and taking the final snapshot, because both it and `write_final` end by running retention and retention deletes files. The trigger is a **distance** in log entries rather than a schedule, so an idle database is never anchored; anchoring is at `MAX(recorded_at)` rather than the clock, so no snapshot claims an instant later than anything it reflects. **The stop test asserted nothing until mutation said so** — close, wait, check nothing appeared passes whether or not the task is alive, since nothing is writing after `close()`; it now keeps the log growing through a raw connection and fails under a leaked stop signal. **What this makes newly load-bearing is retention**, still newest-five-flat against a specified "last five plus one daily for thirty days": harmless when snapshots were written once per shutdown, and now the reason older instants fall back to a full fold. |
| 0.5.5e | Snapshot retention | **The daily tier arrives, one entry after the cadence made it matter ([D-054](s13-decision-register.md#d-054)).** [§5.5](s5-modules.md#55-temporalreplayrs-and-temporalsnapshotrs--reconstruction-and-snapshots) has specified "the last five plus one daily for thirty days" since 0.4.0 and the code kept newest-five-flat; the divergence was harmless while a snapshot meant a shutdown, and [D-053](s13-decision-register.md#d-053) turned it into a rule that defeated the feature it had just been given — under load five anchors span minutes, so every older instant folded the whole log. The container header goes to **format v2**, carrying the snapshot's own instant so retention can bucket by day for eighteen bytes a file instead of decompressing each one. That is deliberately a *second copy* of `MaterializedState.timestamp`, which [D-030](s13-decision-register.md#d-030), [D-035](s13-decision-register.md#d-035) and [D-047](s13-decision-register.md#d-047) are all about refusing; the exception is argued rather than assumed, and rests on both being written from one value in one statement with nothing ever rewriting a snapshot in place. The format bump costs nothing new — a schema change already invalidated every snapshot on the same rule one entry earlier, and [D-043](s13-decision-register.md#d-043)'s refusal path is graceful by construction. "Today" is the newest snapshot's own day rather than the wall clock, so retention is a function of the directory's contents and a database untouched for a year is not emptied by the first write after it wakes. **A fixture bug caught by the mutation pass**, and the third of that kind this cycle: the tests built dates with `format!("2026-01-{:02}", day + 1)`, which produces the calendar-invalid `2026-01-41` past day 39, so those snapshots carried no instant at all and two tests were measuring the dateless path while claiming to measure the daily one. |
| 0.5.5f | §9 measured | **The budgets stop being unfalsifiable ([D-055](s13-decision-register.md#d-055)).** [§9](s6-s10-flows-to-dependencies.md#9-performance-budgets) has asserted "these budgets are CI gates" since 0.4.0 with no harness and not one row measured; `benches/budgets.rs` now covers twelve under criterion. They **measure and do not gate**, which contradicts §9's wording deliberately: the numbers are stated for named reference hardware, so an absolute `≤ 5 ms` in CI is an assertion about whichever runner took the job — the flaky red this project refuses by name in the R15 note, and the reason [D-047](s13-decision-register.md#d-047)'s test asserts a ratio ("absolute timings are a property of the machine, the growth rate is a property of the algorithm"). Regression detection is criterion baselines, machine against itself. **Eleven rows pass with room** — three-hop traversal 2.1 ms of 10, `audit_current` 13.8 ms of 200, vector top-10 294 µs of 20 ms, hybrid top-10 2.0 ms of 50, full fold 21 ms of 100, and composition 3.4 ms against the same, the first direct evidence [D-049](s13-decision-register.md#d-049)'s snapshot path earns its keep. **One misses by 20×, and it is the load-bearing one:** `Chunk commit, 500 rows` measures ≈62 ms against ≤ 3 ms, and that row is [§5.1.5](s5-modules.md#515-cooperative-chunking--the-golden-rule)'s golden-rule calibration — the figure [D-011](s13-decision-register.md#d-011) cites when promising the actor yields promptly at chunk boundaries, so an interactive assertion behind a chunk waits twenty times longer than the design claims. Diagnosed (`write_edges_atomic` re-prepares `INSERT_LINK` per row, and `links` has two triggers compiled into every preparation) and deliberately left unfixed, because instrumenting and optimising in one change leaves neither reviewable. **The chunk benchmark first measured its own fixture** — one database appended to per iteration, spread `[296 ms … 780 ms]`, 2.6× where every other row is flat to a few percent; `iter_batched` made setup untimed and the spread closed to 2.6%. The *variance* exposed it, as it did the retention fixture and the chunk-merge test before that. |
| 0.5.5g | Chunk commit | **The one missing row is half a defect and half a wrong budget ([D-056](s13-decision-register.md#d-056)).** `write_edges_atomic` called `tx.execute(INSERT_LINK, …)` per row and libsql prepares on every call, with both of `links`'s triggers compiled into each preparation; hoisting one `prepare` outside the loop (with `reset()` between rows, because libsql's `execute` binds and steps without resetting) took 500 rows from **≈62 ms to ≈37 ms, 41%**. [D-055](s13-decision-register.md#d-055) had named preparation as the suspect and this entry's first draft called it "most of the chunk" — it was 41%, and the harness is what corrected that. Dropping `trg_links_log_insert` and `trg_links_current_sync` on a scratch database and committing the identical 500 rows takes **2.96 ms**, so trigger amplification is ~92% of the residual, most of it `json_object(…)`/`json(NEW.properties)` per row. **And 2.96 ms is the ≤ 3 ms budget to within 1%** — [§9](s6-s10-flows-to-dependencies.md#9-performance-budgets)'s preamble states amplification is included, and the measured *un-amplified* cost lands on the number, so the budget was almost certainly derived from an insert without its triggers. It is therefore unreachable by construction rather than by defect: hitting it means not writing the ledger entry, which is [Doctrine IV](s0-s3-foundations.md#doctrine-iv). The consequence falls on [§5.1.5](s5-modules.md#515-cooperative-chunking--the-golden-rule)'s golden rule — at ~74 µs per amplified row a 3 ms bound implies chunks of ~40 rows, not 500 — and is left open with the measurements attached, because a budget revised to match the code is not a budget and `CHUNK_ROWS` is a throughput-for-latency decision with [D-014](s13-decision-register.md#d-014) attached. |
| 0.5.5h | Bulk paths | **The same hoist in the other three bulk paths, and the numbers invert the reason given for the first one ([D-057](s13-decision-register.md#d-057)).** `write_concepts_atomic`, `write_annotations_atomic` and `upsert_embedding_chunk` all called `execute` per row inside a transaction with an invariant statement text; each now prepares once, resets between rows, and releases the statement before commit or rollback. Measured at 500 rows in a new `bulk_chunks` group rather than claimed by analogy: concepts **34.1 → 11.9 ms** (65%), annotations **4.60 → 2.13 ms** (54%), embeddings **73.4 → 67.4 ms** (8%), against the edge chunk's 41%. [D-056](s13-decision-register.md#d-056) had explained 41% by `links` carrying two triggers whose bodies compile into every preparation, implying the saving tracks trigger weight — it does not. `analytics_annotations` has **no** triggers and gave back 54%; the DiskANN-indexed embedding tables, by far the most expensive per row, gave 8%. Preparation is roughly a constant per row, so the saving is largest where the row is cheapest, and the edge chunk's 41% was mid-range rather than explanatory. Two by-products: the untriggered 2.13 ms is the control this project lacked and corroborates [D-056](s13-decision-register.md#d-056)'s 2.96 ms trigger-free figure; and the embedding chunk at ≈135 µs per vector is now the worst bulk path in the system, which makes [§5.1.5](s5-modules.md#515-cooperative-chunking--the-golden-rule)'s open `CHUNK_ROWS` question sharper for embeddings than for edges. The concepts chunk no longer routes through `upsert_concept` (which prepares per call by construction); both bind through a shared `UPSERT_CONCEPT` and `concept_params` so the column sets cannot drift, and `classify`/`WriteOp::Concept` classification is unchanged. |
| 0.5.5i | Golden rule | **[§5.1.5](s5-modules.md#515-cooperative-chunking--the-golden-rule) re-derived from measurement, and three of its numbers were wrong ([D-058](s13-decision-register.md#d-058)).** The rule is a bound on *duration* — the SQLite write lock is not preemptible, so an interactive write queued behind a chunk waits it out in full — and `CHUNK_ROWS = 1000` was an answer to it presented as the rule itself. Now `CHUNK_BUDGET` = 3 ms (§9's own figure, kept) with four measured row counts: **edges 90, concepts 70, annotations 600, embeddings 30**, measuring **2.39 / 2.35 / 2.36 / 2.06 ms** against the single old constant's **89 / 24 / 3.5 / 143 ms**. Corrections, from a `chunk_scaling` sweep over n ∈ {1…1000}: (1) one constant cannot express one duration across paths whose per-row costs span **60×**; (2) per-transaction overhead is **not** "noise" but a flat **~0.8 ms** — over a quarter of the bound before any row is written, and a floor below ~30 rows; (3) two of the four paths are **superlinear** in chunk size (edges ~11 → ~103 µs/row, embeddings ~35 → ~151 µs/row), so for them the old chunk was the worst latency *and* the worst throughput at once — 1,000 edges cost 88.5 ms as one chunk against ~27 ms as eleven, **3.3× faster for a 37× lower stall**. [D-056](s13-decision-register.md#d-056) had deferred this as "a throughput-for-latency trade"; on half the paths there was nothing to trade. Also corrects that entry's "~40 rows" (extrapolated from the 500-row point, assuming the linearity this disproves — it is 90) and its "unreachable by construction" (the ≤ 3 ms budget *is* reachable fully amplified; what was unreachable was the row count bundled into it). Sizes solve for ≈2.5 ms, not 3: the first pass hit 2.93 ms median with a 2.96 upper estimate, and a bound that holds only for the median chunk is not a latency bound. **Open:** the superlinearity is unexplained, and if it is WAL page spill then every one of these constants is really a function of `PRAGMA cache_size`. |
| 0.5.5j | Superlinearity | **Diagnosed, and it invalidates a claim [0.5.5i](s13-decision-register.md#d-058) made ([D-059](s13-decision-register.md#d-059)).** The `chunk_scaling` sweep measured every chunk size into a *fresh* database, so chunk size and table size were **the same variable**. Separated: a fixed 90-row chunk into a hub of 0 / 2,000 / 8,000 edges costs **4.4 / 18.4 / 47.7 ms** — the cost is in the table, not the chunk. So D-058's "1,000 edges are 3.3× faster as eleven chunks" is wrong; it was eleven copies of the first chunk. End to end, both arms finishing at the same table: **85.5 ms as one transaction, 94.7 ms as eleven** — chunking is ~11% *slower*, and smaller chunks buy latency at a throughput cost on every path, as [D-056](s13-decision-register.md#d-056) originally said. The bound and the four constants stand (measured directly, not extrapolated). **The edge path's growth is a defect:** `trg_links_single_open`'s `EXISTS` plans as `SEARCH links_current USING COVERING INDEX idx_lc_traversal_cover (source_id=?)` — one equality column, because that index *contains* every column the predicate mentions and so wins as covering, while the PK autoindex would bind three but lacks `valid_to`. Every edge insert scans its source's whole out-degree; 90 rows into a 90,000-edge hub take **1.06 s**. Proof by fix: an index on `(source_id, target_id, edge_type, valid_to, valid_from)` gives a four-column lookup and **47.7 → 8.0 ms, flat** (`valid_from` only to keep it covering — omitting it changed nothing, because the planner kept the old index). **This is not a chunking problem**: it slows every interactive `assert_edge` on a high-degree node, so §9's ≤ 5 ms single-assertion budget is met on an empty database and not on a large one. Not shipped — a [D-036](s13-decision-register.md#d-036) schema change wants its own migration rung. **The embedding path is the same shape and not a defect** (49 → 224 µs/vector as the corpus grows 0 → 8,000; DiskANN insertion rewires a graph that grows as the chunk fills it). **Still open:** with the guard gone the `links_current` upsert still runs 25 → 66 µs/row — not the commit (2.2 ms of 63), not the page cache (512 MB changes nothing), not the index count, not table size. Evidence: `examples/chunk_diag.rs`. |
| 0.5.6 | Waves 1–5: the review acted on | **A full read of the crate against this document (2026-07-30) found ten defects inside a green suite, six of them returning a wrong answer rather than an error, and the four waves that answer them are delivered.** *Wave 1 — the silent defects.* The concept log payload omitted `embedding_model`, so every temporal read lost it and `AttributeMode::AtTime` — the mode [Doctrine VIII](s0-s3-foundations.md#doctrine-viii) exists to offer — returned a *less* faithful record than the mode [§5.2](s5-modules.md#52-graphbuilderrs--traversal-valid-time-and-attribute-fidelity) documents as wrong for historical text; payload v2 fixes it, and [Doctrine VII](s0-s3-foundations.md#doctrine-vii)'s static guard had to be narrowed to permit a scalar model *name* where it had banned the substring `embedding` outright. The folds partitioned on `entity_id` alone, so a concept whose id collided with a link key **vanished from the reconstruction**. `Subgraph` let its adjacency reference nodes absent from `nodes`, and the five algorithms met that four different ways — `louvain` panicked, `scc` emitted phantom components, `k_core` inflated degrees, `dijkstra` returned distances to unlookupable nodes; the fix states a *closure invariant* on the type and none of the five needed changing. Three attribute readers held three retirement semantics. `classify_archive_violation` was **deleted** rather than wired up, because `error::classify` already did the same job and [D-033](s13-decision-register.md#d-033) requires one classifier — the defect had been marked Fixed a cycle earlier by a commit that made it *delegate* instead of *called*. *Wave 2 — the deferred decisions.* [D-059](s13-decision-register.md#d-059)'s index ships as the `v5 → v6` rung; overlapping **closed** valid-time intervals are refused in the write actor rather than in a trigger, with the honest consequence recorded in [§4.2](s4-schema.md#42-links-assertion-history-and-current-belief-materialization) that the storage layer permits what the API refuses ([D-060](s13-decision-register.md#d-060)); identifiers become opaque with `\|` and `/` reserved — *neither* offered option survived contact, since requiring ULIDs would have invalidated every id the crate has been used with and full opacity leaves `transaction_log.entity_id` ambiguous between different links ([D-061](s13-decision-register.md#d-061)); and the clock becomes injectable, with flooring lifted into the `Clock` trait because "strictly increasing across restarts" is not a property any clock can hold alone ([D-062](s13-decision-register.md#d-062)). *Wave 3 — measurement.* [D-047](s13-decision-register.md#d-047)'s deferral condition is finally met and **retires** the `Subgraph` rewrite ([D-063](s13-decision-register.md#d-063)); the wave also found a defect of its own making — the Wave 2 overlap guard carried a provably safe narrowing predicate that made a *covering* index win over the selective one, reproducing [D-059](s13-decision-register.md#d-059)'s defect one wave after fixing it, worth +9.8 ms per chunk with every correctness test passing throughout ([D-064](s13-decision-register.md#d-064)) — and **declined** **AF**'s planner cache after measuring it at under 1% of a query, the stated justification for it being false of `corpus_size` ([D-065](s13-decision-register.md#d-065)). *Wave 4 — hardening.* The cadence gets its own connection, `close()` stops discarding the writer's `Result`, and the `Drop` `debug_assert` [§7.3](../Macrame%20Implementation%20Plan%20v0.5.6.md) recommended was implemented, fired on ~30 tests, and **reduced to a warning** — what dropping costs is one snapshot, which [Doctrine VI](s0-s3-foundations.md#doctrine-vi) makes disposable ([D-066](s13-decision-register.md#d-066)); an upgraded database is re-anchored at open, gated twice after the first attempt broke two contracts the suite already pinned ([D-067](s13-decision-register.md#d-067)); `raw()` stays public with actor containment documented as a convention ([D-068](s13-decision-register.md#d-068)); three errors that named the wrong subject are corrected, including `archive_horizon.archived_at` written with the cutoff — recording that every archive ran at the instant it was archiving *up to* ([D-069](s13-decision-register.md#d-069)); and `load_subgraph`'s superlinearity is explained as an O(E log E) `DISTINCT` sort with two attempted fixes measured and rejected ([D-070](s13-decision-register.md#d-070)). *Wave 5 — the last silent paths, in progress.* `reconstruct`'s `'D'` branch handled an operation no trigger writes and now refuses it; `Delta::edges_gone` went with it, since that branch was its only writer and closing one unreachable path by opening another is not a fix ([D-072](s13-decision-register.md#d-072)). The FTS5 `VACUUM` hazard was investigated and **nothing was built**: it is not reachable, because [D-022](s13-decision-register.md#d-022)'s unconditional delete guard keeps `concepts.rowid` dense so renumbering is the identity — making that guard load-bearing for the *search index* as well as the ledger — and the `verify_fts()` that would have proved it was not shipped, because FTS5's `'integrity-check'` verifies internal consistency and **not** agreement with content, reporting healthy on an emptied index ([D-071](s13-decision-register.md#d-071)). `load_subgraph_with` closes the missing `edge_types`/`min_weight` filter — a **reachability** limit rather than a convenience one, since the byte budget bounds the *unfiltered* neighbourhood — and turned up two defects older than itself: the running byte total drifted from `estimated_bytes()` by one byte per edge, and delegating with `TraversalBuilder`'s `min_weight` default of `0.0` silently disarmed the negative-weight guard, turning a typed refusal into a graph quietly missing edges ([D-073](s13-decision-register.md#d-073)). And the three invariants enforced *above* the storage layer are stated once in [§4.7](s4-schema.md#47-what-this-schema-does-not-enforce) instead of three times in three sections — which exposed that they are not the same shape: for overlapping intervals and `raw()` the write API enforces the rule and only an outside writer opens the gap, but `assert_edge` accepts a negative weight, so a file this crate wrote alone can hold a row it declines to read back ([D-074](s13-decision-register.md#d-074)). Finally `write_annotations` — which took `Vec<ConceptUpsert>` and wrote **concepts**, having kept the name when [D-041](s13-decision-register.md#d-041) split it from `write_analytics_annotations` three releases earlier — becomes `write_concepts`, closing [Appendix A.2](appendices.md#appendix-a--public-api-normative)'s last open entry; and clearing the clippy backlog **by hand** found that one of the warnings was ours: `OverlappingInterval`'s seven `String`s made `DbError` 168 bytes, so [D-060](s13-decision-register.md#d-060)'s Wave 2 fix had silently doubled the size of every `Result` in the crate, `Ok` paths included. Boxed, and pinned by a `size_of` unit test, because the failure mode was a line in a build log rather than a red test ([D-075](s13-decision-register.md#d-075)). Suite 171 → 221 (240 with the property features), clippy clean on `--all-targets --all-features`. |
| 0.6.0 | Hardening | **Schema v7 closes the third §4.7 invariant** (D-083): `CHECK (weight >= 0.0 AND weight < 9e999 AND typeof(weight) = 'real')` on `links.weight` — negative and text weights are now refused at the engine level, not just in the loader. `idx_lc_open_interval` ships as the `v5 → v6` rung (D-059), fixing the superlinearity that made every edge insert scan its source's whole out-degree. Overlapping **closed** intervals are refused in the write actor with `OverlappingInterval` (D-060), with the honest consequence stated in [§4.7](s4-schema.md#47-what-this-schema-does-not-enforce) that the storage layer still permits what the API refuses. The chunked rebuild (`rebuild_current_chunked`) ships with shadow-swap (D-082): builds `links_current_shadow` across many small transactions and swaps it in under one, longest hold 353→47 ms (7.6×). Metrics are feature-gated and zero-cost when off (D-079); `HoldTimer` reads no clock when `metrics` is off. `AttributeModeUnstated` becomes a typed error rather than a `tracing::warn!` (D-085): `as_of(ts)` without a stated `attribute_mode` raises `AttributeModeUnstated`. `OverlappingInterval` is boxed (D-075): seven `String`s = 168 bytes pushed `DbError` over `clippy::result_large_err`'s threshold; boxing keeps the error small. NaN is **not** a schema gap (D-078): `weight REAL NOT NULL` rejects NaN at the engine — `weight.is_nan()` in the loader is unreachable. `low_chunked` deduplicates the four bulk loops (D-086): four copies of a yield-critical loop are four places for the yield to be lost. `RebuildInterrupted` ≠ `RebuildFailed` (D-082): the repair *did not run* is not *the repair did not repair*; action is to retry. `raw()` is `#[doc(hidden)]` (D-091): provoking a guard is its legitimate use. `verify_snapshot_chain` (D-092): folds from genesis by withholding the snapshot directory, compares against the composed answer — `seq_anchor` reported but never compared (they legitimately differ); edges compared as a *set*; capped at `SAMPLE_LIMIT = 32` with a `truncated` flag. Unreadable indices `idx_annotations_label` and `idx_lc_tgt_active` identified (D-089) — they are in the v7 baseline, not a v8 rung, and removal was scheduled for 0.7.0 and did not happen. Suite: 240+ tests, all green. |
| 0.7.0 | Python bindings | **A synchronous Python binding** (D-095) built on pyo3 0.29 and maturin, delivered as a wheel alongside the Rust crate. The binding is synchronous because the Write Actor serialises every write through one channel, so exposing `await` on the write path advertises concurrency the architecture does not grant. Runtime boundary: `Python::detach` around `Runtime::block_on`, releasing the GIL; single process-global multi-threaded runtime behind a `OnceLock`; `PyDatabase` is `#[pyclass(frozen)]` over `RwLock<Option<Database>>` — reads take a read lock and run concurrently, `close()` takes the write lock and waits. Error mapping: every `DbError` variant maps to its own Python exception class with its fields as attributes; completeness enforced by an exhaustive `match` with no wildcard arm. Value types: timestamps accept `str` or aware `datetime` in, always return `datetime` with `tzinfo=utc`; open interval crosses as `None` (not as a sentinel `datetime` — `datetime.max` cannot survive `.astimezone()` east of UTC); embeddings accept `bytes` (fast path, 60.8 µs for 768 dims) or any sequence of floats; `Subgraph` stays opaque. Packaging: distribution `macrame-db`, import `macrame`; wheels on `manylinux_2_28` x86_64 + aarch64, macOS universal2, Windows x86_64; `abi3-py310` (D-094): one wheel per platform; wheel ships with `metrics` on (D-093) because a feature flag does not survive into a binary artifact. R15 is transparent to the boundary: 48 concurrent opens from 48 threads fault 2/12, matching the Rust control arm. Stubs: hand-written `_macrame.pyi`, compared to the live extension both ways and to `errors.rs`, verified by five injection tests. |
| 0.8.0 | Schema v8, the graph API break, and the gates | **Schema v7 → v8, and it is the last cheap rung — [D-036](s13-decision-register.md#d-036) freezes the ledger tables at 1.0, after which a primary-key change is forbidden outright.** `concepts` gains `rowid_pk INTEGER PRIMARY KEY`, `id` becomes `NOT NULL UNIQUE`, and `concepts_fts` is re-keyed onto the stored column ([D-119](s13-decision-register.md#d-119)); `trg_concepts_fts_delete` is installed **inert**, ahead of the 0.9.0 archival that will make it fire. The rung needs foreign keys suspended, so `suspends_foreign_keys` becomes a rung *kind* ([D-117](s13-decision-register.md#d-117)): the pragma toggles outside the transaction because inside it is silently ignored, `PRAGMA foreign_key_check` runs inside before the version stamp, and the pragma is restored on every path including error. Two unread indices are dropped ([D-118](s13-decision-register.md#d-118)), worth **−7.9%** off `assert_edge`, measured on `star_of_stars` with the arms alternated rather than argued from the plan. **A correction, found by a control arm the test did not need to have:** the hazard `rowid_pk` was justified by was never live — `VACUUM` renumbers a sparse implicit rowid **only for a table with no index at all**, and `concepts` has always carried the `id` autoindex ([D-120](s13-decision-register.md#d-120)), so v7 was safe twice over by two accidents. The rung is still right, on the honest ground that SQLite *documents* the renumbering as permitted and an index's correctness should not rest on an optimisation declining to exercise it. **What the rung costs is measured rather than estimated ([D-125](s13-decision-register.md#d-125)):** ~10–13 µs per concept — 2.7 s on a 733 MiB database of 200k concepts / 600k links / 800k log rows — peak disk **1.09×** the starting file, settling to **1.00×** after a checkpoint, and `foreign_key_check` at 13–17% of the rung. The reflex estimate of 2× is right about the *table* and wrong about the *file*. **The graph API breaks, deliberately and once.** `Subgraph`, `NodeData` and `EdgeRef` fields become private behind accessors ([D-114](s13-decision-register.md#d-114)) so that the representation can change without the signature moving — which it immediately does: interning `EdgeRef` against a per-`Subgraph` string pool cuts the per-edge cost from 342/378/454 bytes to **59/62/67** at 8/26/64-byte ids, **5.8×–6.8×** ([D-115](s13-decision-register.md#d-115)), and **no `#[pymethods]` signature moved**, because [D-101](s13-decision-register.md#d-101)'s opaque handle meant there was no copy whose layout had to track it. `concepts.content` leaves the default load ([D-116](s13-decision-register.md#d-116)) and `load_subgraph(…, content=False)` makes the default overridable ([D-123](s13-decision-register.md#d-123)); absent content is `None` and never `""`, because the empty string is a valid value of the type. **`reconstruct` below the log floor is not a corruption ([D-121](s13-decision-register.md#d-121)).** Asking for a state before the ledger began returned `ReplayCorrupt` on a database that had never archived anything; it now returns the empty state with `predates_recorded_history` set. The hot-side marker the plan projected for this is **not needed** — `MIN(seq_id) = 1 AND COUNT(*) = MAX(seq_id)` answers "was anything ever removed" exactly, in both directions, and rollback leaves no gap. The old test was pinning the defect and is replaced. **Louvain stays phase-one-only, and the reason it gave was measured false ([D-122](s13-decision-register.md#d-122)).** Two-phase diverges at 6,144 nodes — well inside the byte budget — so "graphs larger than the budget admits" was wrong; but phase-one recovers the ground truth *exactly* at every size while two-phase earns its higher Q by **merging whole cliques**. That is the modularity resolution limit, so **a Q comparison cannot be the criterion for a change whose whole effect is to optimise Q harder** — the gate as the plan specified it would have said ship, and shipping would have been wrong. **The gates learned to say why they failed.** `scripts/run_rust_suite.py` classifies every run as `CRASH`/`FAILED`/`INCOMPLETE`/`TEARDOWN`/`BUILD` and retries only `CRASH` ([D-110](s13-decision-register.md#d-110)), keying on the presence of each target's `test result:` line — which is what [R15](s11-s12-milestones-and-risks.md#r15) has always said to key on and what a pass-count sum cannot see. `libsql` **stays at 0.9.30**: R15 reproduces on `0.10.0-pre.4`, and the instructive number is that 0.9.30 scored 0/10 in the same session against 2/10 recorded for it earlier — ten runs cannot distinguish these rates in either direction ([D-111](s13-decision-register.md#d-111)). `macos-latest` joins the Rust matrix and the Node 20 actions are bumped to their **first** `node24` major rather than the newest ([D-112](s13-decision-register.md#d-112)). And the register becomes executable ([D-113](s13-decision-register.md#d-113)): a decision that says it awaits a named release must close itself out by name once that release ships — [D-087](s13-decision-register.md#d-087) and [D-089](s13-decision-register.md#d-089) both said *Scheduled for 0.7.0*, 0.7.0 shipped without either, and nothing went red for a whole release. **One reported regression is retracted ([D-124](s13-decision-register.md#d-124)).** The quarantined property gate was recorded as newly red on `main` with the cause unidentified. It was not: `main` was green on every 0.8.0 commit on all three platforms, the "regression" was measured against a `~3/25` figure stale since 0.5.4 that three other files had already superseded, and eight runs could not have resolved the comparison anyway — re-measured at n=20 interleaved, the same executable gave 75% and then 45%, and a binary built at `v0.7.0` faults at the same rate as HEAD's. The rate now lives in `.cargo/config.toml` alone. Two facts survive: R15 is **effectively Windows-only** in practice (46 s on macOS and 58 s on Ubuntu against 4 m 30 s on Windows), and the multi-thread test runtime is recorded as an untested candidate with its measurement attached rather than as a hunch. Two gate holes closed in passing: `--features metrics` never reached the `property-tests` binaries, so **they had never been linted**, and nothing built the docs, so 18 rustdoc warnings had accumulated unnoticed — both are now gates. **Looking forward: 0.9.0 needs a rung after all ([D-126](s13-decision-register.md#d-126)).** `trg_concepts_guard_delete` is still unconditional in v8, `CREATE TRIGGER IF NOT EXISTS` keeps the old body on an existing name, and `verify()` compares trigger *names* and never bodies — so a baseline re-issue cannot make it marker-gated and nothing would notice. Not folded into v8 on purpose: the marker exists during *links* archive sessions, so shipping the conditional guard early would leave concepts deletable during them. **[§9](s6-s10-flows-to-dependencies.md#9-performance-budgets)'s budget table is re-measured ([D-127](s13-decision-register.md#d-127)), since three items changed the read path:** three-hop traversal **2.1 → 1.66 ms**, vector top-10 **294 → 246 µs**, hybrid top-10 **2.0 → 1.77 ms**, full fold **21 → 16.9 ms**, composition **3.4 → 2.18 ms**. The gains are attributed by **two** controls rather than claimed — [D-090](s13-decision-register.md#d-090)'s fixed `control/select_1` is unchanged at 1.51–1.62 µs, and the chunk-commit path 0.8.0 never touched is 2.39 → 2.40 ms — because five unrelated paths improving at once is also what a faster machine looks like. Suites: **321** Rust, **348** Python. |
| 0.9.0 | Concepts cross the archive boundary | **Concepts archive and rehydrate, and it took two rungs (`v8 → v9`, `v9 → v10`) that this plan had projected as none.** Archivability is **reachability, not expiry** ([D-128](s13-decision-register.md#d-128)): a concept is archivable when it is retired, its `valid_to` and its `recorded_at` both precede the cutoff, and **no surviving hot link names it in either direction** — so concept archival is strictly downstream of link archival, and the predicate is evaluated *after* the link delete inside the same session. The two derived-row foreign keys are deliberately not clauses of it, because blocking on a recomputable artifact would answer "not yet" forever for anything ever embedded. **`v8 → v9` closes a hole [D-126](s13-decision-register.md#d-126) found** ([D-129](s13-decision-register.md#d-129)): `trg_concepts_guard_delete` was unconditional, `CREATE TRIGGER IF NOT EXISTS` keeps the *old body* on an existing name, and `verify()` compared `type, name` and never bodies — so a baseline re-issue could not fix it and nothing would notice. `verify()` now reads the delete guards' bodies. **What crosses the boundary is a partition, not a convenience** ([D-130](s13-decision-register.md#d-130)): the concept row moves **column for column, `content` included**, because a move that drops a column is a rewrite and [Doctrine V](s0-s3-foundations.md#doctrine-v) does not permit an absence the ledger cannot explain; `analytics_annotations` and `embeddings_*` are **deleted**, recomputable under [Doctrine VII](s0-s3-foundations.md#doctrine-vii) and unrepresentable on a plain ATTACHed file anyway. Entity data crosses; derivative data does not. The tension with [D-116](s13-decision-register.md#d-116) is apparent only — that governs the *in-memory* representation, this is *on-disk* storage. **Rehydration is a physical move back** ([D-131](s13-decision-register.md#d-131)), minting no transaction-time facts, and `v9 → v10` is the mechanism rather than a correction: the fold resolves last-writer-wins by **`seq_id`, not `recorded_at`**, so a log row written at rehydration would take a new sequence number and outrank the concept's own retirement — the concept would come back *alive* at every instant after its creation. Gating `trg_concepts_log_insert` on the session marker is what prevents it. `rowid_pk` is reinstated when free and reassigned when taken, with `concepts_fts` re-pointed to match. **The hot-side archive marker is refused outright rather than deferred a third time** ([D-132](s13-decision-register.md#d-132)): the richer error message it was wanted for is strictly *weaker* than what the hot log already carries — `MAX(seq_id) - COUNT(*)` says how much went and `MIN(seq_id)` says how far back what remains reaches, while a marker would carry only the archive timestamp, which answers neither. `archive_hint` ships the better message with no rung. **Rehydration measured**: 3.71 ms fixed and ~74 µs per concept, linear to n=1,000 and **14.2× for the next decade** — attributed to FTS5 index maintenance by a trigger-free control arm (53% of the cost at n=10,000) rather than guessed, against a row-movement path that stays linear to within 1%. Archiving the same 1,000 rows costs 20.37 ms, a **3.8× asymmetry** that is set-based-versus-per-id by design. The fixture matrix cannot move it (5.8%, noise) and is measured anyway, because the predicate severs the matrix's axis and an unmeasured "no difference" is still a claim. Windowing is **declined with a number**: 10,000 concepts hold the write lock 1.105 s against a contract already tolerating ~50 s, and windowing would trade away the atomicity that makes a partial rehydration impossible. **The binding catches up** ([D-133](s13-decision-register.md#d-133)): `ArchiveReport.concepts_archived`, `db.rehydrate(ids)` and `RehydrateReport` cross, and the round-trip equality is asserted from Python as well as Rust. C1–C3 added **no** `DbError` variant, so [D-099](s13-decision-register.md#d-099)'s exhaustive `match` had nothing to catch — recorded because *no error work was needed* and *the error work was forgotten* look identical from a green build. Half of C3's gate **cannot** cross: `keyword_search` and `load_subgraph` both filter `retired = 0` and an archivable concept is retired by definition, so no Python reader can witness a rehydration; the FTS half stays in the Rust suite. **Two findings about gates, and they matter more than either fix.** An injection probe showed `archive_hint` being computed on *every* cold fold for a string almost every caller discards — work on an error path is invisible to every timing measurement, because it sits beside the I/O that dominates it, and only a panic in the branch found it. And `archiving_links_only_enlarges_the_archivable_set` had been **false since C2** while C2, C3 and C4 each reported green: it is behind the `property-tests` feature and all three ran the classifier on the default set. **An exit gate that does not name its feature set has not said what it ran.** Suites: **330** Rust · **339** `metrics` · **362** `--all-features` · **353** Python. |
| 0.10.0 | Published claims get a registry | **A release with no feature in it, and it found its own drift class first.** Every performance claim this project publishes now has a registry entry naming where it is stated and what measured it, and the dry-run that built it changed the key figure before the registry existed ([D-138](s13-decision-register.md#d-138)) — the counts in the first draft were wrong, which is the drift the registry exists to catch, appearing in the commit that introduced it. The diagnostic path is bounded to one open and measured on both sides, and `ATTACH` is **probed rather than assumed** — it does not escape a read-only open ([D-137](s13-decision-register.md#d-137)). The out-degree caveat that had qualified traversal figures since 0.5.5 is retired by measurement rather than by argument, with a third arm added to make the curve flat. `--all-features` stops being a published test configuration ([D-140](s13-decision-register.md#d-140)): it is `metrics` + `property-tests` together, which puts the R15-prone binaries back inside the main run — 4 of 4 runs crashed at one attempt. [§9](s6-s10-flows-to-dependencies.md#9-performance-budgets) gets a full re-measurement as the 0.10.0 column, median of three sessions, controls published. |
| 0.11.0 | The chunk residual, attributed | **Both items [Appendix C](appendices.md#appendix-c--future-considerations-deliberately-deferred) named for this release, in the order it named them, and the first answer was wrong.** The 9.06 ms edge-chunk row against a 3 ms bound ([D-058](s13-decision-register.md#d-058)) is attributed by isolation ([D-142](s13-decision-register.md#d-142)), and then the constants are re-derived against [D-088](s13-decision-register.md#d-088)'s fixture matrix ([D-143](s13-decision-register.md#d-143)): four shapes agree, and the constant they name is **not adopted** — no row count satisfies a fixed duration on a path whose per-row cost grows with the table, which is what makes 0.12.0's change the only remaining answer. **No code that runs changed**, so [§9](s6-s10-flows-to-dependencies.md#9-performance-budgets) publishes no 0.11.0 column: a second measurement of the same crate is a second measurement, not a data point. [D-144](s13-decision-register.md#d-144) names documentation drift as a category rather than an incident. |
| 0.12.0 | Time-based chunking | **The chunk loop stops on elapsed time rather than on a row count ([D-146](s13-decision-register.md#d-146))**, which is [§5.1.5](s5-modules.md#515-cooperative-chunking--the-golden-rule)'s golden rule read literally: the rule was always a bound on *duration*, and the four `chunk_rows` constants were an approximation of it fitted at one population. They become **ceilings** — the loop starts there and sizes each chunk from the last one's measured hold, down to a floor of 35 rows. R15 gets a number instead of an adjective ([D-147](s13-decision-register.md#d-147)): **93% per attempt** on the quarantined step under sustained load, which is a property of the machine and the load rather than of this crate, and the retry budget is set from it. |
| 0.13.0 | Twenty-six commits: R15, statistics, tuning, and Python parity | **R15 had been called a concurrency bug in the documentation for six releases and is a volume fault ([D-148](s13-decision-register.md#d-148))** — the mitigation had been describing the wrong thing, and distinct databases plus no threshold is what the rewritten report says. The planner stops running on default selectivity: bounded `ANALYZE` statistics that are schedulable ([D-149](s13-decision-register.md#d-149)), and a plan-pinning fixture with rows in it, which found the review's own claim wrong in both directions ([D-150](s13-decision-register.md#d-150)). The links ledger's two archive seeks are indexed ([D-151](s13-decision-register.md#d-151)). The metrics surface is frozen while freezing is still free, `Rehydrate` gets its own `CommandKind`, and `metrics` becomes a **default** feature ([D-152](s13-decision-register.md#d-152)…[D-154](s13-decision-register.md#d-154)). Operational control consolidates into one `Tuning` struct with `checkpoint()`, a settable `wal_autocheckpoint`, and the page cache as **two** knobs rather than one ([D-155](s13-decision-register.md#d-155)…[D-159](s13-decision-register.md#d-159)). Six Python parity gaps close at once ([D-161](s13-decision-register.md#d-161)…[D-165](s13-decision-register.md#d-165)) — a gap opened in the release that creates the feature is the one that never becomes a convention. |
| 0.13.1–0.13.38 | The road to 1.0 | **The wave series toward 0.14.0**, recorded in [D-170](s13-decision-register.md#d-170)…[D-211](s13-decision-register.md#d-211) and planned in [Road to 1.0](../Macrame%20Road%20to%201.0.md). `as_of` splits into two axes and the second one had nowhere to read from ([D-174](s13-decision-register.md#d-174), [D-175](s13-decision-register.md#d-175)). Snapshot integrity moves to the front of the reader — a checksum that is verified before anything is deserialized, atomic rename that knows whether the *name* reached the disk, and a fuzz corpus derived from files the writer actually wrote ([D-184](s13-decision-register.md#d-184)…[D-188](s13-decision-register.md#d-188)). One visibility predicate replaces three readers' private answers, and a search at an instant is separated from a search with none ([D-189](s13-decision-register.md#d-189)…[D-194](s13-decision-register.md#d-194)). The graph algorithms move onto a borrowed dense integer view, with no public signature moving ([D-200](s13-decision-register.md#d-200)…[D-202](s13-decision-register.md#d-202)). And the 1.0 public-surface freeze: the surface becomes a checked-in file with a gate ([D-205](s13-decision-register.md#d-205)), the actor protocol leaves it ([D-206](s13-decision-register.md#d-206)), `DbError` becomes `#[non_exhaustive]` and the guarantee that costs is bought back rather than written off ([D-207](s13-decision-register.md#d-207)), and every public item collapses to one canonical path ([D-208](s13-decision-register.md#d-208)). |
| 0.14.0 | Acceptance | **§14's eleven items read as evidence rather than ticked** ([D-212](s13-decision-register.md#d-212)). Four were not what they said: the 0.13.0 surface diff had never been reviewed (**601 → 619 items**, 73 identities out and all 73 accounted for — [`api-review-0.14.0.md`](api-review-0.14.0.md)); the cross-axis read's operation count had been measured since 0.13.23 and never pinned; the one-hour fuzz had never been run; and the item's own decision numbers belonged to W5. §14.1 is the carry list “zero silent carries” needed and did not have. |
| 0.14.1 | W12.1 | **The branch framing is written down before the schema is** ([D-213](s13-decision-register.md#d-213)). A branch is *transaction time with a tree order* — a fork in the sequence in which the database was told things, not a third axis. [Doctrine II](s0-s3-foundations.md#doctrine-ii) gains a clause and the count of doctrines does not move, because lineage is a property of the second clock and the eight are frozen ([D-211](s13-decision-register.md#d-211)). Nothing is built: no `branches` table, no `branch_id`, no rung to v12. The release changes the doctrine's text and the static gate that reads it, for the reason [D-160](s13-decision-register.md#d-160) and [D-174](s13-decision-register.md#d-174) were kept one release apart. |
| 0.14.2 | W12.2 | **Schema v12: the branch storage model, storage only** ([D-214](s13-decision-register.md#d-214) … [D-218](s13-decision-register.md#d-218)). A `branches` register, `branch_id` on all four ledger tables, `links_current` re-keyed per lineage, three log triggers redefined and four guards added. **The central decision is that a branch *inherits* concepts rather than restating them**: `concepts.id` keeps its `NOT NULL UNIQUE` and its parenthood of `links`, so `branch_id` there is provenance and never identity — chosen after `examples/branch_identity_probe.rs` measured both alternatives on libSQL and found that widening the uniqueness leaves every insert failing with `foreign key mismatch` and a composite key forbids the copy-on-write that makes a fork O(1) ([D-214](s13-decision-register.md#d-214)). What that costs is stated rather than minimised — a branch cannot correct or retire a concept it did not mint — and §15.2 records the overlay that would buy it, its five costs, and the retirement symptom that reopens the question. **The lineage column carries a real foreign key**, taken on behaviour libSQL and SQLite disagree about: probe §15 isolates the axis to *rows present and keys on* — an empty table accepts the statement, which is why the first reading was right about what it saw and wrong about what it meant — and the rung buys it with `suspends_foreign_keys`, then proves the result is a real key rather than a decoration ([D-215](s13-decision-register.md#d-215)). **The three log triggers and the four replay folds are one repair in two files**: a fold partitioning on a column every trigger leaves reading `'main'` partitions on a constant, and that defect is invisible in a green suite ([D-216](s13-decision-register.md#d-216)). And the fixtures found a production defect older than the wave — **three rungs restored triggers from the *live* DDL, so a v4 database climbing today failed at the v4 → v5 rung**, three rungs before branching exists; the ladder also turns out not to be re-entrant over a version stamp, which invalidated a fixture idiom the suite had used since v5 ([D-217](s13-decision-register.md#d-217)). **C-1 closes here rather than at 0.15.0**, because this is the release that made it worth closing: a third foreign key on `links` takes an unqualified `FOREIGN KEY constraint failed` from ambiguous between two columns to ambiguous between three, so a refused edge now names the endpoint that is missing ([D-218](s13-decision-register.md#d-218)). It reached the tree with no Rust test, and what caught it was a *Python* test pinning the old answer — a stale test doing a missing one's job, which is luck and not method. Public API +18 items and −0, all schema text plus three `#[non_exhaustive]` `AbortKind` variants — the plan predicted no movement and was wrong. |
| 0.14.3 | W12.3 | **The measurement §15.3 asked for, and three of its claims did not survive it** ([D-219](s13-decision-register.md#d-219)). `examples/branch_traversal_probe.rs` measures depth-3 traversal against branch chains of 1, 10 and 100. **Resolving along the ancestry costs a constant, not a factor of chain depth** — 1.1–1.3× the unfiltered traversal at every depth tried, because the ancestry CTE is materialised once per query rather than re-walked per hop. **The index the plan proposed is the slowest of three**: leading on `branch_id` does buy a covering plan and is still 13% behind, because it displaces the column the recursive step seeks on — [D-042](s13-decision-register.md#d-042) arriving a second time in the same index. And **the form §15.3 describes is not a resolution**: `branch_id IN (ancestry)` returns a branch's corrected edge *and* its ancestor's superseded one, both naming the same nodes, so the reachable set is unchanged and nothing in the answer says anything is wrong — [D-216](s13-decision-register.md#d-216)'s failure mode one release later, in the read path instead of the fold. The nearest-ancestor form that fixes it costs 3.0×, which makes 0.14.4's choice *correct versus fast* rather than the one the plan set up. No shipped code and no schema change: a probe, a decision, and four corrections to the section that asked for it. |
| 0.14.4 | W12.4 | **The read path resolves lineage, and a third fold was found carrying D-216's defect** ([D-220](s13-decision-register.md#d-220), [D-221](s13-decision-register.md#d-221)). `TraversalBuilder::on_branch`, `query_as_of_edges_on` and `load_subgraph_with` each pick one of two shapes from a single probe of `branches`: the trunk form is today's SQL, exact whenever the register holds one row; the resolved form joins the ancestry and takes **one row per edge key from the nearest lineage that holds it**. Nearest rather than union, decided by probe §4b — the union form leaves a shadowed retirement with no effect at all, so it does not merely report a stale weight, it discards the only cross-lineage retirement Doctrine III permits. Two shapes rather than one, decided by probe §6 — resolution is 3.02× on a database with nothing to resolve, which is every database this crate has written. An unregistered branch is refused rather than answered for the trunk, and an unbranched read on a forked ledger is the trunk rather than the union ([D-069](s13-decision-register.md#d-069)). **`links_at_tx` partitioned on `entity_id` alone** — the edge key, shared across lineages by design — so two lineages' assertions collapsed to whichever was written last; D-216 swept `replay.rs` and skipped this one because its rustdoc argued the partition was sound about something else. And D-221 records the fold that is *still* wrong: `reconstruct` collapses two beliefs into one edge in Rust, downstream of the correct SQL, pinned by a test that asserts the wrong answer until 0.14.5 can widen a public tuple. |
| 0.14.5 | W12.5 | **The reconstruction stops collapsing lineages, and the container version is what refuses the files that cannot hold the new shape** ([D-222](s13-decision-register.md#d-222), closing [D-221](s13-decision-register.md#d-221)). `MaterializedState::edges` becomes `Vec<EdgeBelief>` — the five fields the tuple had plus `branch_id` — all four fold constants project the column they have partitioned on since [D-216](s13-decision-register.md#d-216), and `Delta` is keyed on the edge key **and** the lineage. A ledger holding a trunk assertion and a branch's shadow of it reconstructs to two labelled beliefs where it returned one, with the survivor previously decided by the fold's emission order. **The defect was a container that could not hold what the query returned**, which is the third appearance of one shape this wave: D-216 widened four partitions, D-220 found a fifth fold never widened, and this is that widening reaching the Rust immediately downstream of correct SQL — a lineage-aware query feeding a lineage-blind structure loses the lineage silently and returns a plausible number of plausible rows. `#[non_exhaustive]` **with** `EdgeBelief::new`, because `save_snapshot` is public: the attribute alone would not have made the next field additive, it would have made a public function uncallable from outside the crate. `SNAP_FORMAT_VERSION` goes 3 → 4 for a payload change with the header unchanged, which is what [D-043](s13-decision-register.md#d-043) says the number is for — `bincode` is not self-describing, so a v3 payload read as v4 does not fail, it reads the next edge's `source_id` as this edge's `branch_id`. Python's `MaterializedState.edges` is a six-tuple and `query_as_of_edges` stays at five: *unresolved against resolved*, since a read that named its lineage does not need the label repeated back. |
| 0.14.6 | W12.6 | **The fork point becomes a visibility cutoff, and the repair is not the filter it looks like** ([D-223](s13-decision-register.md#d-223)). [D-220](s13-decision-register.md#d-220) resolved *which* lineage holds an edge and never read `branches.forked_at`, so a branch kept absorbing its parent's later writes — the opposite of §15.3, and of `ddl`'s own comment claiming that column is *"what §15.3's visibility cutoffs are computed over"*. Nothing computed them. **`links_current` answers *current as of now* and structurally cannot answer *current as of t***: it holds one belief per key per lineage and the sync trigger's `DO UPDATE` carries `recorded_at` forward, so once an ancestor churns an edge the inherited version is not in the table at all — and a `recorded_at <= cutoff` predicate over it *removes* that edge along with the subtree below it, which is a quieter wrong answer than the one it replaces. The read becomes a **hybrid**: the projection for keys the ancestors have not touched, a log fold bounded per lineage for the keys they have, the two arms disjoint by one comparison on one row. Four churn kinds are separated because they fail differently and neither historical form fails on the same ones — the retirement case loses a subtree under *both*. Cost measured against a prediction written first: the churn-linear half held, the **1.45× at zero churn did not**, and probe §6 puts it in the arm's machinery rather than its rows — so the named escalation is a third shape chosen by a probe, and the index that looked like the lever is declined at 3–13%. `fork()` is deliberately not in this release: the wrongness is unreachable through the public surface today and a semantic break the moment a `fork()` exists, which is [D-160](s13-decision-register.md#d-160) → [D-174](s13-decision-register.md#d-174)'s ordering a third time. |
| 0.14.7 | W12.7 | **`fork()` ships into a reader that already reads like a fork, and the invariant the schema promised turns out to be uncheckable** ([D-224](s13-decision-register.md#d-224)). `BranchId`, `Branch`, `Database::fork(name, from)` and `branches()` — **one row in `branches` and nothing else**, asserted as a count over the four ledger tables in both languages. The read half landed at [D-220](s13-decision-register.md#d-220) and [D-223](s13-decision-register.md#d-223), so this is a lifecycle arriving at a reader that has been resolving lineage for three releases — [D-160](s13-decision-register.md#d-160) → [D-174](s13-decision-register.md#d-174)'s ordering paying off rather than being argued for. **`ddl` has promised since v12 that `fork()` would enforce `forked_at >= parent.created_at`, and that rule is not checkable at all**: `seed_root_branch` stamps the trunk's `created_at` from the wall clock inside `migrations::run`, before the injected clock is resolved and unable to move after it (the clock's floor reads `MAX(recorded_at)` over tables the migration must create first), so the comparison is between two clocks and refuses every fork on every `FakeClock` database. The shipped rule is `forked_at >= parent.forked_at` — two values on one clock by construction, guaranteeing fork points are **non-decreasing down a root path**, which is precisely what D-223's ancestry clamp already assumed. Found by building what the comment described, which is how D-223's false comment was found one release earlier; a comment about an unbuilt caller is verified by nothing. `BranchId` deliberately does **not** copy `ModelName`'s rule — that rule exists to make an identifier *splice* safe and a branch id is always a bound value, and it would reject the UUID and the path-like turn id §15.5 actually generates — so the rule is instead about an append-only key that can never be corrected: a trailing space is not a typo, it is a second lineage that prints as the first. An unregistered lineage stops being `NotFound` (*"node {0} not found"*, the wrong noun) and becomes `UnknownBranch` everywhere. The Python surface ships in the same release, W6's convention holding a third time. **Readable and not yet writable**: `EdgeAssertion` carries no lineage, so a fork is a *view* of its parent's history as of an instant and every write still lands on the trunk — said in the rustdoc because the gap is invisible from the signatures. |
| 0.14.8 | W12.8 | **The write path carries lineage, and the overlap guard was asking a question that stops meaning anything the moment a second lineage can write** ([D-225](s13-decision-register.md#d-225)). `EdgeAssertion::on_branch`, `ConceptUpsert::on_branch`, `Database::retire_edge_on`, and `branch_id` bound by `INSERT_LINK` and `UPSERT_CONCEPT` — so 0.14.7's *readable, not writable* paragraph is deleted rather than restated, and §17's second acceptance criterion is assertable for the first time. **The finding is in `reject_overlapping_interval`**, which read `links_current` for the edge key with no lineage predicate at all: exact while every row was `main`'s, and wrong in *both directions at once* thereafter — a branch refused for overlapping the belief it forked to supersede, the trunk refused for overlapping a branch's belief it cannot see. `AND branch_id = ?` is not the repair; it fixes the trunk and inverts the branch, which is defect AA ([D-060](s13-decision-register.md#d-060)) reintroduced across lineages by the fix for it — the same shape [D-223](s13-decision-register.md#d-223) found one release earlier, where the obvious `WHERE` clause made an inherited edge vanish. What ships is the read's own resolution narrowed to one edge key: **what a lineage may not overlap is what that lineage can see**. `trg_links_single_open`'s v12 comment parked *whether the inherited interval should also close* as "§15.4's write-path question"; it should not and cannot — closing the ancestor's row is the parent corruption Doctrine III forbids — so retirement across lineages is **shadow retirement**, which [`visible_cte`] has described as a write since 0.14.4. Every write asks `lineage_shape` before it takes the lock, so an unregistered branch is `UnknownBranch` by name rather than an unqualified foreign-key failure; a database that has never forked answers `Trunk` and runs the statements it always has. `DbError::CrossLineage` is the thirty-eighth variant and closes a guard that had been in the schema since v12 with **no arm in `classify`** — machinery written for an unbuilt caller is exercised by nothing, which is D-224's finding on a third kind of artefact. `EdgeAssertion` and `ConceptUpsert` become `#[non_exhaustive]`. The Python surface ships in the same release, W6's convention holding a fourth time. |
| 0.14.9 | W12.9 | **The branch view, which buys no capability — and that is what makes it one release rather than a fifth of them** ([D-226](s13-decision-register.md#d-226)). `BranchView` is a `Database` plus a [`BranchId`], so a caller who forked reads and writes through the fork instead of naming it at every call; every method on it exists on `Database` already, and the tests pin that going through the view produces the same rows and the same errors as naming the branch by hand. **The `Arc` it holds is the design.** `Database::close` takes `self` by value and an `Arc` cannot surrender that while a clone survives, so a view *cannot* end the handle it reads through — the restriction is structural rather than documented, and it is [D-203](s13-decision-register.md#d-203)'s argument for declining `Database: Clone` reaching the use §15.4 reserved it for. Construction does no I/O and cannot fail: whether the lineage is registered is a question every operation already asks. A write naming a *different* lineage is refused as `BranchMismatch`, the thirty-ninth variant, rather than relabelled — an assertion that names somewhere else is evidence of a belief, and the failure it catches is two views held at once. **The Python view is written in Python, because the guarantee does not cross**: `close()` is a method on the handle the caller already holds, so the Python view gives the ergonomics and not the structural half, and a pyo3 class would have been a parallel implementation implying otherwise. Fifth holding of W6's convention, and the first that is not a pyo3 class. |
| 0.14.10 | W12.10 | **The fork-point cutoff had reached two of the three read paths, and the third is the one that is not built by the shared builder** ([D-227](s13-decision-register.md#d-227)). `temporal::query_as_of_edges_on` resolved lineage from 0.14.4 and never looked at `branches.forked_at`: a branch was handed trunk edges recorded after it forked, **and** lost an inherited edge the moment the trunk retired it — the projection row was the trunk's closed one and nothing went to the log for the entry behind it. On the same fixture the traversal reached four nodes and this reader returned two edges, with nothing in either answer saying they disagreed. It now emits the hybrid the traversal emits, assembled from the same `graph::lineage` functions rather than a second copy; the trunk's answer is unchanged and an unforked database never reaches the resolved arm. **One cause, two symptoms:** the same *not built by the builder* is why this was also the one read surface [D-220](s13-decision-register.md#d-220) never bound into Python, so `branch=` on `Database.query_as_of_edges` and a sixth read on the Python `BranchView` ship here too — a repair Python cannot observe is a repair nobody there can test. First recorded **lapse** of W6's binding convention rather than a holding of it. |
| 0.14.11 | W12.11 | **`diff(a, b)`, and the plan's reason for it being cheap is a special case** ([D-228](s13-decision-register.md#d-228)). §15.4 says divergence "is exactly the set of rows carrying the branch's own id"; that is true of a fresh fork against a parent that has not churned since, and of nothing else. It **under-reports** — a trunk that reweights after the fork leaves the branch believing a row the *trunk* wrote, and two siblings disagree through a row their common ancestor wrote, with neither name on it — and it **over-reports**, because re-asserting an inherited edge at its existing value writes a row and concludes nothing. What ships is the belief-level definition: `a` holds an edge key `b` does not, or holds it over a different interval or at a different weight. **One statement, not two reads**, because two statements against a shared `read_conn()` are two snapshots and can report a difference that never existed; which is what makes the four shared CTE builders take a name tag rather than gain a second hand-written copy. **No `ts`**: a retirement is a divergence about an instant having passed, and any instant filter drops it from `a`'s side. `Divergence`, `Database.diff` and `BranchView.diff` reach Python in the same release — W6's convention held for the sixth time, immediately after its one lapse. |
| 0.14.12 | W12.12 | **The archive predicates never learned the lineage v12 gave the folds, and both spellings of that delete rows the ledger still believes** ([D-229](s13-decision-register.md#d-229)). A link's `entity_id` carries no lineage on purpose, so `LINKS_ARCHIVABLE`'s "a later assertion for the same interval key" matched **across** branches: a branch writing at the trunk's key made the trunk's own open, current row archivable, and one `archive` left the trunk unable to reach a node it still believed. It ran in both directions — whichever lineage wrote second pruned the other. `LOG_ARCHIVABLE` promised "the newest entry per entity stays hot", written when the fold partitioned by entity; since v12 it partitions by `(entity, branch)`. Separately, **"a closed interval is history" is false of a shadow**: archiving a branch's own closed row at an ancestor's key removes the branch's disbelief and the ancestor's open row wins again, so an archive that asserts nothing un-retired an edge. Both clauses take exact-branch equality, never ancestry. **`audit_current` reports 0 across all of it** — the projection is honestly re-derived from a ledger that has been wrongly pruned, and Doctrine VI's check has nothing outside the ledger to compare against. Found while preparing §15.4's abandonment arm and shipped ahead of it. |
| 0.14.13 | W12.13 | **The abandonment arm, and the plan sentence it had to refute to exist** ([D-230](s13-decision-register.md#d-230), schema **v13**). `archive_branch(name)` moves one lineage's whole ledger to the cold file: its `links`, its `concepts`, its `transaction_log` entries **and its `branches` row**, in one transaction. §15.4 justified the arm on "an abandoned branch's rows are a contiguous archivable set by construction"; a concept is keyed by identity across the whole ledger ([D-214](s13-decision-register.md#d-214)), so a trunk or sibling edge may name one minted on the branch — measured by probe — and a branch's log rows are scattered rather than a prefix. Both refutations shaped the design: the first became a **refusal**, and the second forced the chain — links go, so the log must go, so the lineage record must go, because `hot_log_reach`'s soundness rests on *the newest row per entity is never archivable*. **That last step is what makes a fold omitting the lineage correct rather than silently short**: afterwards the name is unknown, so a read naming it is refused instead of being handed its parent's view. Also refused for the trunk, for a branch with descendants, and for one whose concepts another lineage's hot edge names. Schema v13 gates `trg_branches_frozen_delete` on the archive session — a rung and not a re-issue, for [D-126](s13-decision-register.md#d-126)'s reason — and `cold.branches` is the table [D-217](s13-decision-register.md#d-217) said this arm would need. |
| 0.14.14 | W12.14 | **The index rung the plan owes, measured against the reader that now exists** ([D-231](s13-decision-register.md#d-231), schema **v14**). §15.4 has carried *"`idx_lc_traversal_cover` gains `branch_id`"* since 0.14.3, on numbers [D-219](s13-decision-register.md#d-219) took against `branch_id IN (ancestry)` — the form that same probe run proved is not a resolution, and which 0.14.4 therefore did not ship. **The shipped reader does not walk `links_current` at all**: its walk joins a CTE, and the only base scans are `churned` and `links_cut`, which bind `(branch_id, recorded_at)`. So D-219's preferred shape buys nothing measurable — 6.18 ms against 6.27, with the plan still reading `AUTOMATIC PARTIAL COVERING INDEX (branch_id=?)` — and **every shape leading on `branch_id` takes the trunk walk off its covering index**, which is what `the_shipped_traversal_cte_stays_on_the_covering_index` refuses. What ships is a **second** index, `idx_lc_lineage_cut`, against D-219 §4's *"there is no fourth index"*: the two shapes stopped sharing an access path, so they can no longer share an index. Branched reads 1.20× on a fresh fork and **2.28×** on a churned trunk; the trunk plan byte-identical; assertions +12.6%, stated rather than buried. Found on the way: four index rungs re-issued the whole of `CREATE_INDICES`, making each a statement about *today's* schema — ten migration tests went red from one cause, and each rung now names what it owes. |
| 0.14.15 | W12.15 | **`links` is keyed by lineage** ([D-232](s13-decision-register.md#d-232), schema **v15**). §15.4 carried the collision as latent — two lineages asserting one edge key at one `recorded_at` fail on the ledger's primary key — and called it "unreachable through the crate", on a clock argument that is sound for two *sequential* writes and describes nothing the crate does in a batch. **Both bulk surfaces take one stamp for the whole batch by contract** ([D-014](s13-decision-register.md#d-014)), and `reject_overlaps_within` groups by `(source, target, type, branch)`, so a trunk row and a branch row about one edge are not an overlap and collide at the insert with raw engine text. Reachable since **0.14.8** and unnoticed for seven releases, because the test pinning it pinned the failure as a guarantee. Widened rather than refused: two lineages are *allowed* to disagree about an edge, and the materialization has held both rows since v12. `branch_id` goes **last**, on plans and not on §15.3's reasoning — appending keeps the supersession probe's five-column covering seek that the archive sweep runs per candidate row, and leading with it drops to one bound column, which is [D-042](s13-decision-register.md#d-042)'s eviction for the third time in three releases. The rebuild is **measured** where the v6 → v7 rung's was estimated: 105.8 ms at 50,000 rows. `cold.links` takes the same key in the same release, or `archive` becomes the one operation that still refuses what the ledger now accepts; `verify` learns to check a **key**, because unlike every previous rung this one adds no object with a name. |
| 0.14.16 | W12.16 | **One `CommandKind`, one structural hold distribution** ([D-233](s13-decision-register.md#d-233)). `ShadowRebuild` covered both halves of a chunked rebuild — the fill turns, meant to fit [`CHUNK_BUDGET`](s5-modules.md#515-cooperative-chunking--the-golden-rule), and the swap turn, which builds three indexes under the write lock at 46.8 ms against 3 ms ([D-082](s13-decision-register.md#d-082)) and cannot. The kind was not exempt, so **every successful rebuild put a permanent entry in `budget_violations()`** — the method documented as the one-line answer to whether the bound is holding — and a fill regression arrived as `+1` on a counter that was never zero. [D-030](s13-decision-register.md#d-030)'s shape in a counter: one that cannot be zero trains discounting. The evidence that the cost was already being paid is in the crate: `tests_py/test_end_to_end.py` carried a carve-out filtering the kind out of `violations()`, which is a workaround an operator's dashboard would also need and has no way to learn. The swap is `ShadowSwap` and **exempt**, on the criterion the register had applied three times without naming (stated then as *expected-on-healthy overages are exempt*, re-anchored at 0.14.17 on **inapplicability** — the swap is atomic by necessity and has no smaller unit, so the bound was never about it) — and nothing is lost to the exemption, because `over_budget` counts **occurrences, not magnitude**, so the swap's growth with table size lives in the histogram and `longest` either way. The fill half stays counted, which is what [D-082](s13-decision-register.md#d-082) was protecting and what the split now protects structurally. Unscoped work in an acceptance release, taken on evidence of cost already paid and recorded in §17 rather than carried silently ([D-212](s13-decision-register.md#d-212)). |
| 0.14.17 | W12.17 | **The criterion, the asymmetry, and three gates that were not gates** ([D-234](s13-decision-register.md#d-234), [D-235](s13-decision-register.md#d-235), [D-205](s13-decision-register.md#d-205) amended). Documentation and scripts only; no library change. Three things shipped in 0.14.16 said the exemption pair "fails in both directions", and it does not: **widening an exemption only removes entries from `budget_violations()`**, so every zero-assertion stays green under every widening. The pair is one instrument with two asymmetric halves — `a_swap_over_budget_is_not_a_violation` owns narrowing and the truth-condition, `a_long_fill_is_a_violation_and_a_long_swap_is_not` alone owns widening — and that is now what all three say. The anti-vacuity precondition is restated as a **tripwire**: if the swap ever stops exceeding, the exemption has lost the fact it rests on and must be removed rather than resized around. The exemption criterion is re-anchored on **inapplicability** — *the operation is atomic by necessity and has no smaller unit*, so the bound was never about it — with both falsified predecessors kept, because the house records evolutions rather than final states: v1 (*expected-on-healthy*) is refuted by `Optimize` running on every close and staying counted; v2 (*if `over_budget` can differ from `turns`, count it*) is refuted by `Archive`, `Rehydrate` and `Checkpoint`, all of which can come in under budget. Both were observational; inapplicability is decidable at design time, which is what lets a criterion settle the next case instead of rationalising the last. `scripts/check_public_api.py` now holds appendix D's stated count to the baseline it blesses and refuses to exit 0 on a mismatch — the ordering that let that gate sit **red at HEAD through a shipped release** at 0.14.15 — and says out loud that `macrame::prelude` makes one enum variant **two** baseline items. Mutating that new check found a defect it was not aimed at: the script documents **three** exit codes and had only ever had two, because every *cannot measure* path was `sys.exit("…")` with a **string**, which prints and exits **1** — so `ci.yml`'s `code = 2` branch was unreachable and a missing nightly was reported as *the surface moved*, the exact collapse [D-205](s13-decision-register.md#d-205) forbids in the paragraph that states the contract. And CI: [D-234](s13-decision-register.md#d-234) records that `ci.yml` triggers on `push: [main]` and `pull_request`, so **CI had never run once across the sixteen releases of W12** — verified, thoroughly, but on one machine and unreplicated; branches now live under pull requests and push triggers stay main-only. [D-235](s13-decision-register.md#d-235) records the fuzz gate that bounded whole-process RSS when its assertion was about a single allocation, failed on AddressSanitizer's 202 MiB free-quarantine, and thereby **skipped v0.14.0's crates.io publish**. |
| 0.14.18 | W12.18 | **The 0.15.0 release document, written before the merge rather than after it** ([D-212](s13-decision-register.md#d-212)'s habit applied to the release note itself). `docs/releases/v0.15.0.md` states what the merge will guarantee: schema v11 → v15, snapshot payload v3 → v4, `DbError` 33 → 40 variants, public surface 1,313 → 1,558 items, decisions D-213…D-235 — and reports §17's acceptance list as **nine criteria of which three are restated and one is open**, rather than as nine ticks. Item 4, *cross-branch edges are refused with a named error*, is the one still owed a build; the note says so and says what it will most likely produce, which is a fourth restatement, because an edge carries exactly one `branch_id` and concepts are shared vocabulary across the whole ledger ([D-214](s13-decision-register.md#d-214)) — so an edge whose endpoints lie in different lineages is not a thing this schema can represent. Recorded in the same note: §17 has **no item 10**, 11 having been appended against a mis-read of where the list ended, and the numbers stay as they are because the register cites them. The top-level README moves with it — a branching row in the capability table, four rungs in the schema ladder, schema version 15, the suite at 627 / 608 / 553, `branch` in the module map, the binding surface at 0.15.0 with `BranchError` as a seventh intermediate group, and the register pointer from D-173 to D-235. One claim there was not stale but **false**: *v8 is the last rung that could change a primary key before the 1.0 freeze*, which v15 refuted three releases ago. |
| 0.14.19 | W12.19 | **A gate that could not pass, and the quarantine decision reaching the wiring it was never applied to** ([D-236](s13-decision-register.md#d-236)). Workflow and script only; the library is untouched. `Cargo.toml` has said since 0.8.0 that the `property-tests` binaries are *"unusable as a gate on Windows"* — that sentence is why the feature exists — and `release.yml` gated `publish` on them anyway, not by choosing to but because the step sits in `ci.yml`'s `test` job and `verify` calls that workflow whole. v0.14.0's publish took two `gh run rerun --failed` cycles against a tree nothing was wrong with. **At 65% red on a healthy tree the step has no contrast between its healthy and unhealthy output**, so blocking on it selected the runs where the coin landed heads rather than verifying the tree. Now `continue-on-error`: it runs on every push and every pull request, reports, and blocks nothing. Because the exit code no longer reaches a reader, `run_rust_suite.py` emits a four-state verdict — `completed` / `crashed-R15` / `named failures` / `did not run` — to stdout and `$GITHUB_STEP_SUMMARY`; exit codes are untouched, so the crash-aware gate D-236 files under *revisit* stays a few lines away. The accepted risk is stated rather than implied: a publish can ship with **zero completed property runs** for its SHA, and [D-234](s13-decision-register.md#d-234)'s release obligation gains the clause that replaces the block — *the property step is read*. Also surveyed and recorded: `main` has **no branch protection and no rulesets**, so the merge was gated by a reviewer's reading rather than a required check, and the one `ci.yml` edit makes the disposition uniform. Third instrument finding in three releases, and the rule they share is in the entry: **an instrument with no contrast between its healthy and unhealthy output is decoration.** |
| 0.14.20 | W12.20 | **The first defect D-234's standing obligation actually found, and the gate was working the whole time** ([D-237](s13-decision-register.md#d-237)). `python.yml` runs `mypy --strict python/macrame`; it had been red since 0.14.9, when `BranchView` shipped with **no annotations on twelve methods** — 19 errors in the only hand-written Python file in the package, against a README that advertises the gate as a guarantee. The reason eleven releases went past it is [D-234](s13-decision-register.md#d-234)'s reason exactly: `python.yml` triggers on `push: branches: [main]` plus `pull_request`, `BranchView` has never existed on `main`, and so the code and the gate that judges it were never in the same place until PR #2 put them there. **This one is not another broken instrument.** D-233 was a counter that could not be zero, [D-234](s13-decision-register.md#d-234) a gate that never ran, [D-236](s13-decision-register.md#d-236) a gate that could not pass; this gate ran, was right, was red, and was unread. Fixed by annotating all twelve signatures — `_claim` over a value-restricted `TypeVar`, `Sequence[…]` on the writes, return types on the reads. The delegating reads keep `**kwargs: Any` **deliberately**: restating the binding's twelve keyword defaults would be the second copy the class was written to avoid, and a copy nothing compares goes stale silently ([D-227](s13-decision-register.md#d-227)). Also recorded rather than repaired: `mypy` is not in the local gate loop, so this red was visible only in CI — which is the whole argument for the obligation. |
| 0.14.21 | W12.21 | **The last acceptance criterion, and the reach it was reaching for** ([D-238](s13-decision-register.md#d-238)). §17 item 4 asked that cross-branch edges be refused with a named error; the shape cannot be built, because an edge carries one `branch_id` and concepts are shared vocabulary ([D-214](s13-decision-register.md#d-214)). The 0.15.0 note predicted a fourth restatement and the prediction was incomplete: **the criterion was reaching at something real.** A lineage can make its belief depend on another lineage's vocabulary — `beta` asserting an edge onto a concept `alpha` minted — and that is *accepted*, because the predicate refusing it would also refuse the trunk naming a branch's concept. The refusal is one boundary later and already existed: `archive_branch` refuses to forget `alpha` while another lineage's hot edge names its concept, naming the concept ([D-230](s13-decision-register.md#d-230)). **What was missing was the test.** The guard's predicate is `l.branch_id <> :branch` — *any other lineage* — and its one test used **the trunk**, the single lineage that can never be abandoned and therefore the worst witness a general predicate could have; the sibling arm had been measured once by a probe and pinned nowhere. `tests/cross_branch_tests.rs` pins the acceptance, the sibling refusal, and the remedy the error message instructs — retire, then archive past the closed interval, because `links` is append-only and retirement alone leaves the row hot. Verified by three mutations, each killing the tests it should. |
| 0.14.22 | W12.22 | **A threshold test is calibrated for one machine, and CI's Windows runner is the faster machine** ([D-239](s13-decision-register.md#d-239)). `a_swap_over_budget_is_not_a_violation` asserts that a shadow swap over the chunk budget is not counted as a violation, and first asserts that the swap *did* exceed — the anti-vacuity guard [D-233](s13-decision-register.md#d-233) wrote, because on a small fixture the claim passes whether the exemption exists or not. That precondition failed in CI: **2.572 ms against a 3 ms budget**, on a commit where Linux, macOS and the development box passed. The tripwire names two readings and the measurement chose between them — fastest of three in a debug build: 3.6 ms at 400 × 4, 12.3 ms at 1,600 × 4, 25.4 ms at 3,200 × 4, against [D-082](s13-decision-register.md#d-082)'s 46.8 ms at 10,000 keys — so the swap has no smaller unit and the fixture is what grows. Now 3,200 × 4: ~8× margin here, ~6× on the runner that failed. The rule the entry adds to [D-236](s13-decision-register.md#d-236)'s: **contrast can be a property of the machine rather than of the instrument**, and an instrument that only has contrast on the box it was written on is decoration everywhere else. Second dividend of [D-234](s13-decision-register.md#d-234)'s read-the-run obligation in three releases, after [D-237](s13-decision-register.md#d-237). Mutated in both directions: un-exempting reddens the claim, shrinking the fixture reddens the tripwire. |
| 0.14.23 | W12.23 | **A full disk said the ledger was damaged** ([D-240](s13-decision-register.md#d-240), §14.1 C-2). Every failure inside `save_snapshot` — the directory, the serialization, the compression, the temp file, both writes, `sync_all`, the rename, the directory flush — returned `ReplayCorrupt`, *"replay corrupt at seq N"*, which is the worst thing this system can say about itself. Nothing in that function can damage the log; it reads a materialized state and writes a file. **The same correction was made to the read path at 0.13.12** ([D-185](s13-decision-register.md#d-185)) and the write path kept the wrong subject for ten releases — [D-227](s13-decision-register.md#d-227)'s finding reached through an error taxonomy instead of a query builder. `DbError::SnapshotWriteFailed { path, reason }` is the forty-first variant and completes the family: a foreign file, a damaged file, a damaged ledger, and now a file that could not be written — the only one where **nothing is damaged and nothing is lost**, because [Doctrine VI](s0-s3-foundations.md#doctrine-vi) makes a snapshot derivative and the cost is a slower start. One variant covers the directory flush as well, which is [D-186](s13-decision-register.md#d-186)'s own classification rather than a shortcut. Found by re-reading §17 item 9, which also found that the note's claim that C-2 through C-4 were *"re-carried in writing"* had no writing behind it. Surface 1,558 → 1,567. |
| 0.14.24 | W12.24 | **The feature-off configuration was compiled and never run** ([D-241](s13-decision-register.md#d-241), §14.1 C-4). CI's only `--no-default-features` gate was `cargo check --all-targets`, which proves the configuration compiles; nothing proved its 613 tests pass, while `README.md` published the count and [D-154](s13-decision-register.md#d-154) said the path "is still built and tested" — [D-169](s13-decision-register.md#d-169)'s class one level up, a number nothing could falsify. C-4 asked for a *build* gate "not a `cargo check`", and the measurement refused the phrasing: reintroducing D-169's own defect showed the check catches it, and `cargo test` does **not** build `benches/budgets.rs` (`harness = false`), so replacing the check would have lost a target. **Both are kept**, each demonstrated to see what the other cannot — the run's half proved by widening `MICROS_CEILING` so the packing invariant fails, which the check reports green. Ubuntu only, blocking, because it passes on a healthy tree ([D-236](s13-decision-register.md#d-236)'s criterion). Also found: a bare `cargo test --no-default-features` stops at the first [R15](s13-decision-register.md#d-147) crash and reports 485 of 613 as a total. |
| 0.14.25 | W12.25 | **`DbError::kind()`, and §14.1 is closed** ([D-242](s13-decision-register.md#d-242), C-3). [D-207](s13-decision-register.md#d-207) listed this among its rejections — *"a stable discriminant so the exhaustive match moves inside the defining crate"* — as "a real option" that belonged on its own merits. Here on them: `kind()` is one match with **no wildcard**, in the crate that defines the enum, so `#[non_exhaustive]` does not apply and a variant added without a classification **does not compile** ([D-099](s13-decision-register.md#d-099)'s guarantee, recovered where it can still be had). The twelve kinds are not new — they are the seven groups the Python bindings publish plus the five failures that belong to no group, because a Rust-only taxonomy is [D-227](s13-decision-register.md#d-227) committed deliberately, and a new gate pins the two spellings together across all forty-one variants. `ErrorKind` is itself `#[non_exhaustive]`: an exhaustive one would make a new *category* a major version, which is the trap D-207 rejected by name. Surface 1,567 → 1,624. **The last row of §14.1 is now closed; C-5 is at 1.0 by decision.** |
| 0.15.0 | Branching | **The wave lands.** Twenty-five releases, [D-213](s13-decision-register.md#d-213)…[D-242](s13-decision-register.md#d-242), and one capability: a lineage of belief that forks in O(1), reads its ancestry as of the fork point, writes its own rows beside its parent's, answers `diff(a, b)`, and is abandoned in one transaction. Schema v11 → **v15** over four rungs; snapshot payload v3 → **v4**; `DbError` 33 → **41 variants**; public surface 1,313 → **1,624 items**. Merge is refused in writing — [Doctrine III](s0-s3-foundations.md#doctrine-iii) has no neutral answer to which assertion supersedes which. **The shape of the cycle is that the plan was wrong in public more often than it was right**: §15.3's factor of chain depth is a constant, §15.4's "unreachable through the crate" was reachable for seven releases, its "exactly the set of rows carrying the branch's own id" is wrong in both directions, and three of §17's nine criteria are **restated rather than ticked**, each restatement a register entry. §14.1's carry table is **closed** — C-1 at 0.14.2, C-2 at 0.14.23, C-3 at 0.14.25, C-4 at 0.14.24, none of them the way its row predicted — leaving C-5 (`cargo-semver-checks`), which is at 1.0 by decision because it wants a released baseline. The last four releases are three carried items and the instrument work around them; see [`docs/releases/v0.15.0.md`](../releases/v0.15.0.md). |
| 0.15.1 | W13.1 | **One lowering for the lineage read, and W13 brought forward** ([D-243](s13-decision-register.md#d-243), review A-1). The prelude every branched read emits — ancestry, fold or hybrid cut, nearest-lineage window — was assembled in three readers from the same five generators, each deciding for itself which relation to join; [D-227](s13-decision-register.md#d-227) is what that cost. `graph/plan.rs` lowers a `Resolution { shape, branch_slot, recorded_slot, tag }` to the CTE list and the source once, and the traversal, `query_as_of_edges_on` and `diff_sql` splice it. **Twenty-seven SQL texts captured before the change are byte-identical after it**; no pin was re-pinned. The road map named W13 as the first thing to cut from 0.15.0 and it was cut; the 0.15.0 review argues it is the first thing to build for 0.16.0, because C-7, C-8 and C-10 are each a fourth spelling otherwise. `docs/Macrame Update Plan v0.16.0.md` is the cycle's plan. Surface unchanged at 1,624; no rung. |
| 0.15.2 | W13.2 | **The trunk of a forked ledger reads as itself, and the transaction-time fold was a co-routine** ([D-244](s13-decision-register.md#d-244), review C-7). `LineageShape::TrunkOnForked`: `branches` holds more than one row and the requested lineage is a root, so the lowering emits the trunk prelude plus one predicate on `branch_id` — the escalation [D-223](s13-decision-register.md#d-223) named, taken where its probe's answer is structural. All three readers on one day, through `lower()`. The predicate carries a unary `+`, because without it the planner serves the equality from `idx_lc_lineage_cut` and scans the whole trunk per hop, exactly as [D-231](s13-decision-register.md#d-231) predicted for an index it declined to build; pinned by plan. Found on the way: `links_at_tx` ran as a co-routine inside the recursive step, re-folding the log once per walk row — **10.6 s against 59 ms** on 11,110 edges, on every shape, since 0.13.2 — hidden because the branched read's window materialises on its own and it was the branched read every probe timed. `AS MATERIALIZED`, pinned on all three shapes. The forked trunk's current read: 18.4 ms against the 43.1 ms it paid, 1.06× the unforked trunk. One VDBE pin re-pinned after the sweep (the materialised fold rewinds once); the loader mutation survived the old fixture and has its own test now. Surface unchanged at 1,624; no rung. |
| 0.15.3 | W14.1 | **The archive stops rebuilding the whole projection to repair a slice of it** ([D-245](s13-decision-register.md#d-245), review C-1, the review's only High). Every session that deleted a link re-projected all of `links` under the write lock; [D-077](s13-decision-register.md#d-077) measured it and [D-080](s13-decision-register.md#d-080) bounded how *often* it ran, never what one run costs, and `Archive` is budget-exempt so nothing flagged it. The projection is pointwise in the key, so the session collects the keys its `DELETE` will disturb — with the delete's own predicate, before it runs — and re-derives only those. **Flat where it was linear**: 2.26 / 2.52 / 2.97 ms repairing a 200-key slice of 2K / 20K / 100K links, against 13 / 133 / 701 ms, and the shape is the result rather than the ratio; **447 to 115 ms** end to end through `Database::archive` on a 20K ledger. The restricted projection needs its `WHERE` inside the window, which `shadow.rs` knew and kept a private copy of; there is one generator now. A too-wide key set is *correct* and costs everything this release saves, so it is pinned on the key set rather than on the answer. Surface unchanged at 1,624; no rung. |
| 0.15.4 | W14.2 | **The reach guard consults the instant it is handed** ([D-246](s13-decision-register.md#d-246), review C-2). `hot_log_answers_for` took a `_ts` and returned one bit — *was anything ever archived* — so the first archive session took `as_of_recorded` and `AttributeMode::AtTime` away for the ledger's whole history, `now` included. The cutoff the old note said was missing is not needed: the newest row per entity is never archivable, so an instant at or after the newest surviving stamp folds completely. Which surfaced a second defect in the arm 0.5.5 did not reach — with no archive file *passed*, the reach test was still `MIN(recorded_at) <= ts`, and a path-less `reconstruct` returned a state holding an edge out of a concept it said did not exist, silently. One rule, one place, three callers. Three mutations, one re-pinned test that had been passing for the wrong reason. Surface unchanged at 1,624; no rung. |
| 0.15.5 | W14.4 | **The reach guard's two queries were not comparable, and the free one was the new one** ([D-247](s13-decision-register.md#d-247)). [D-246](s13-decision-register.md#d-246)'s cost paragraph said *two aggregates where there was one*; measuring showed the intactness check is a covering **scan** — 0.1 ms at 2,000 log rows, 24 ms at 500,000, linear since 0.8.0 — and the stamp is a 3.4 µs **seek**, flat. `MAX <= ts` covers under both rules, so it is sound before the case split rather than inside a branch of it: asked first, the scan never runs at or after the newest surviving stamp, which is where `as_of_recorded(now)` and `reconstruct(now)` ask. **24.24 ms → 0.004 ms** at 500,000 rows; the historical arm is unchanged and still linear, and there is no exact cheaper test for it without the marker [D-132](s13-decision-register.md#d-132) refused. Which surfaced a budget: §9's *AtTime hydration ≤ 30 ms* is justified as bounded by the result set, and the fold is — flat at 0.14 ms across the whole range — while the guard in front of it was **173× the read** at 500,000 rows. A pure reordering, so the deliverable is the state-space table that says so: five unit tests over intact and gapped logs at five instants, catching four mutations on their own. Surface unchanged at 1,624; no rung. |
| 0.15.6 | W14.3 | **The write actor had a connection and no memory** ([D-248](s13-decision-register.md#d-248), review C-6, C-24, A-3). Every command was handed `&conn` and every fact it established died with the turn, so a single-edge assertion asked `branches` how many lineages exist, compiled the overlap guard, and compiled `INSERT_LINK` — having done all three on the call before it. `ActorState` holds the three, invalidated by name: `Fork` and `ArchiveBranch` forget the lineages, the three sessions that attach a second database forget the statements. The safety argument is one sentence ([D-014](s13-decision-register.md#d-014)): the only writer is the one holding the cache. **0.184 → 0.099 ms** on the trunk and **0.401 → 0.106 ms** once forked — a forked database was paying 2.2× on every write, almost all of it compiling the guard's resolved form. C-24 came with it and is no longer cosmetic: the shape stopped being a function of the row count at 0.15.2 and the loop went on keeping its last answer, correct only because the guard compiles one statement for both shapes, which is what W13.3 changes. Five mutations, two of which required rewriting the test that was supposed to catch them. Surface unchanged at 1,624; no rung. |
| 0.15.7 | W14.5 | **The log now records whether anything has left it** ([D-249](s13-decision-register.md#d-249), review C-5, schema **v16**). `hot_log_is_intact` was `COUNT(*)` over the whole hot log on every recorded-time read below the newest surviving stamp — **32.6 ms at 500,000 rows**, in front of a hydration that is flat at 0.14 ms. The review asked for a cache in `ActorShared`; there is no such place, because both readers arrive on `read_conn`, which the actor does not own and which is `query_only` and refuses a temp table. So the fact is kept where it is generated: one row in `log_integrity`, maintained by `trg_txlog_mark_gap` on the only operation that can change it, **0.033 ms at every log size**. A trigger and not Rust because [§4.2](s4-schema.md) admits raw SQL against the file, and a bit maintained in the crate would be wrong after exactly that, silently. Costed on the other side too: **0.43 µs per row deleted, 5.6% of a 333,000-row archive session**. The rung seeds from `sqlite_sequence`'s high-water mark, which is **stricter than the query it retires** — the old form called an *empty* log intact, so a fully archived database reported its own emptiness as history, and `hot_log_answers_for` had no archive path to catch it with. Surface 1,624 → 1,627. |
| 0.15.8 | W13.3 | **The overlap guard reads through the readers' lowering** ([D-250](s13-decision-register.md#d-250)). `key_visibility_cte` was a fifty-line second spelling of the lineage resolution, kept honest by one test asserting it agreed with the first — [D-227](s13-decision-register.md#d-227) is four releases of what that costs. `Resolution` carries an optional `KeySlots` now, and `lower()` puts it in two places: the reader's own `WHERE` on the trunk shapes, the **base scans** under `Resolved`, where appending it to the tail would narrow a relation already built over the ledger. The forked trunk gains **6.6%** by stopping taking the resolved form at all — which is [D-248](s13-decision-register.md#d-248)'s C-24 repair, filed as cosmetic, turning load-bearing. The branch write pays **3.0%** for the shared `visible` join and the reason is written down. Six mutations: the survivor was a *correctness* hole — an unnarrowed churned set makes another key's pre-fork interval this key's overlap — and it has a test now. |
| 0.15.9 | W13.4 | **`ReadPlan` is public, and the reads it names share one statement** ([D-251](s13-decision-register.md#d-251), review F-34). Three qualifiers — lineage, valid instant, recorded instant — appear on every read surface, and each surface spelled them itself, so *"read `exp` as it stood on Tuesday, under what we believed in March"* was not a value the crate could pass, store or compare. It is one now: `TraversalBuilder::plan`/`read_plan` and a new `Database::edges` that takes one. `edges` is the surface that gains a **capability** — `query_as_of_edges_on` has no transaction-time argument, so that question previously meant walking from a start node it does not have or folding the whole log — and it returns `EdgeBelief`, so a row says **which lineage holds it**. `query_as_of_edges_on` is now that statement with `recorded` unset and its own two-arm `match` deleted, which is [D-243](s13-decision-register.md#d-243) one layer up. Python gets `ReadPlan` in the same release, spelled as keywords rather than a builder, on purpose. Surface **1,627 → 1,662**, all additive. |
| 0.15.10 | W13.5 | **The ceiling goes inside the recursion, and the walk says whether it bit** ([D-252](s13-decision-register.md#d-252), review C-8). `probe_cap` ran the traversal to the end of the graph and dropped the tail, so a name that read as a bound on cost was a bound on the size of the answer. The plan's own sketch — a `LIMIT` on the walk's outer `SELECT` — repeats that one line down, because that projection sorts and a sort materialises the whole walk first: measured on a 20,050-edge hub graph, **outer `LIMIT 20` visits 20,050 edges and `LIMIT 20` inside the CTE visits 7,250**. Inside it is. `n` counts *walk rows*, and dedupe on `(node, depth)` plus the retired filter both drop rows the walk has paid for, so the answer is at most `n` and `len()` cannot say whether the ceiling bit — hence `WalkOutcome`, read from the walk's own row count in the same statement, on a projection **anchored** on that count so a walk whose every concept is retired still reports. `ReadPlan` gains the field 0.15.9 promised it; Python gains `limit=` and `traverse_ids_explained`. Nine mutations: the survivor was `AtLeast(probe_cap)` for `AtLeast(ids.len())`, a number nothing counted. Surface **1,662 → 1,684**, all additive. |
| 0.15.11 | W15.1 | **A concept can outlive its lineage, and the refusal says which** ([D-253](s13-decision-register.md#d-253), review C-3). `archive_branch` takes a lineage's `branches` row into the cold file; `rehydrate` returns a concept still carrying the `branch_id` it was minted on, into a column that references that row. Measured, the collision arrived as `ErrorKind::Engine` reading `FOREIGN KEY constraint failed` — naming neither the concept nor the lineage, blaming the table being written rather than the one with the missing row, and classified as *the database is unwell* when nothing was. `DbError::BranchArchived { branch, concept }` carries both under `ErrorKind::Branch`, refused **before** the insert rather than by it: after it, the foreign key fires first and the caller is back to the old message. The review's other repair — reinstate the `branches` row inside the session — is rejected, because D-230 leaves that decision to the caller and it would return a lineage known again with none of its links. The remedy is measured rather than asserted: `fork`, then the same call, and the concept comes back on `alt`. Python gains `BranchArchivedError` with both names as attributes. Nine mutations, nine caught, two of them by a pre-column rehydrate test written four releases ago. Surface **1,684 → 1,693**, all additive. |
| 0.15.12 | W15.2 | **The fold's partition, in an index that is not the one the review asked for** ([D-254](s13-decision-register.md#d-254), review C-4, schema **v17**). C-4 asks for `(entity_id, branch_id)` because *"the fold partitions on that"*; the four folds in `temporal::replay` partition on **`(table_name, entity_id, branch_id)`**, `table_name` leading deliberately since a concept's id and a link's synthetic id share a namespace. Measured, C-4's shape is **never faster than no index** — 73.1 ms against 64.2, and 64.6 against 63.6 on a second run — because it is wide enough to pull the fold off `idx_txlog_time` and cannot supply the window's order either, so the plan becomes a full scan plus the same sort. What ships is `(table_name, entity_id, branch_id, seq_id DESC)`: **46.2 ms, and `reconstruct` 99.5 → 72.6**, for +8.2% file and **+5.0%** on single-edge assertions. **The `DESC` is the whole effect** — the ascending form of the same columns is used and still sorts, so the pin is a *negative* one and `Expect` gains a `forbidden` field. The covering form is 1.18× faster again and is refused: +51% on the one table that only grows. **The cold file earns its own index by the union's shape, not by symmetry** — `UNION ALL` compiles as a `MERGE` that sorts each side independently, so 127.2 / 110.1 / **96.3** ms, and it goes in a second list because it names a column `upgrade_cold_lineage` may be about to add. Two *other* folds read this table and the index reached both: `links_at_tx` trades a `recorded_at` seek for the order and has a **crossing** at just under half the log (−22% at the wide bound, +31% at the narrow one), four plan pins re-blessed with the table beside them; the concept hydrate moves off `idx_txlog_entity` and gains nothing, +8% of a 0.10 ms call, recorded rather than repaired. `idx_txlog_time`'s registry entry named the fold and now names the stamp aggregates. Four mutations, one survived — deleting the cold index's creation, invisible to all sixty-eight tests on the archive path — and that is the test the pass bought. Surface unchanged at 1,693. |
| 0.15.13 | W15.3 | **Every public struct with public fields is `#[non_exhaustive]`, and three of them could not be built at all** ([D-255](s13-decision-register.md#d-255), review C-11). C-11 names three structs and says each should gain a builder before 1.0, because a field added after 1.0 is a major version. **The defect is twenty-one structs wide**, counted from the checked-in surface, and the argument for each is the argument C-11 makes for `Tuning`; they are all here, because there is one window and splitting it costs a caller two breaks for one decision. The attribute has a **second failure mode, and it is the silent one**: on a struct nothing else can build it makes the type *unconstructible* outside the crate while the crate itself keeps compiling. It fired three times — `Overlap`, whose only external construction is the binding's `DbError` sample; `NodeAttributes`, fabricated by fixtures; and `MaterializedState`, which `save_snapshot` takes *as a parameter*. Each gained a constructor, and the general case gained a test. `tests/api_growth_tests.rs` registers all twenty-nine types with the reason in the entry and asserts four properties against the checked-in surface — which is what caught that `ConceptUpsert` and `ReadPlan`, attributed releases ago, had never had the setter-per-field contract checked. It replaces a **canary this release had to kill**: `tuning_tests` named every `Tuning` field on purpose, and broke three times as designed. [D-155](s13-decision-register.md#d-155) is **superseded and its *Rejected* line is what shipped** — nothing in it was wrong, the `E0639` fact still holds, the deadline moved. Three documents had claimed the attribute the code did not have; the majority is made true rather than the minority edited. `scripts/api_review.py` closes an eighteen-release hole: `api-review-0.14.0.md` quoted [D-205](s13-decision-register.md#d-205)'s *a review nobody can re-run is a review nobody can check* and was itself unre-runnable. Its finding is its own blind spot — **+38 items and zero removals in a breaking release**, because `#[non_exhaustive]` removes a construction form and no item. **Eight mutations and five survived**, the worst ratio of the cycle and on the release that was about instruments: two setters writing the wrong field or none — a failure mode the struct literal could not have, since a literal names each field once — `Overlap::new` swapping the two intervals the tuple grouping protects the *caller* from swapping, and, sharpest, **deleting the attribute from `src/` left every assertion green**, because they all read the blessed baseline. Three tests bought, all five now caught. Surface **1,693 → 1,730**, `#[non_exhaustive]` types 21 → 44. |
| 0.15.14 | W15.4 | **The diagnostic connection is minted once, and the call every document named as *the open* opens nothing** ([D-256](s13-decision-register.md#d-256), review C-9). C-9 asks for one cached connection per `Database` instead of one per call. The first shape written was the compromise — cache the `libsql::Database` handle, mint a connection per call — because `diagnostic_conn` promises **a new, independently owned** connection. The measurement refused it: `Builder::…build()` costs **0.10 µs and opens nothing**, succeeding against a path that does not exist, while `connect()` is **51.5 µs of an 82.7 µs call** and is where `SQLITE_CANTOPEN` arrives. **82.7 µs → 19.9 µs**, and what remains is the `stat`, not the connection. **Three documents named `build()` as the open and as R15's exposure** — the method's rustdoc, the binding's mutex justification, and the probe's own docstring — each drawing the right conclusion from the wrong mechanism, which survives exactly until someone optimises against the mechanism. R15 re-measured across four shapes at width 48, mutex removed so the arms differ only in Rust: **3/30** per call, 2/30 with the handle cached, 1/18 with `connect()` locked, **0/30** with one connection — locating the race past *two opens at once* and at minting against the **use** of the connections already out. What is given up is isolation between diagnostic callers, and the test that pinned the old contract now pins the new one, with the half of [D-091](s13-decision-register.md#d-091) that mattered getting its own pin for the first time. Five mutations, one survived — the `stat`, which is now **100% of a warm call** and had no assertion; its test skips on Windows at run time, so it is **CI that verifies it and not the box this was cut on**. The binding keeps its mutex as margin rather than measurement. Surface unchanged at 1,730. |
| 0.15.15 | W15.5 | **A shared connection given arbitrary SQL is scrubbed before the next caller, and the worst thing it carried was not on the list** ([D-257](s13-decision-register.md#d-257)). [D-256] shared one diagnostic connection between callers and documented what that meant: an `ATTACH`, a `PRAGMA`, a temp table outliving the call that made them. Measurement found a fourth it had not named, and it is the only one whose damage leaves this surface — **a leaked `BEGIN` pins a WAL read snapshot**. Measured: 200 writes later the diagnostic surface answered **1 row where the database held 201**, and `checkpoint()` — a typed public method — moved nothing, leaving the WAL at **8.5 MB** until the transaction was rolled back. `diagnostic_conn()` now scrubs on entry: `is_autocommit()` at **0.04 µs** gates a `ROLLBACK`, and `PRAGMA temp.schema_version` + `PRAGMA database_list` at **2.4 µs** against the sketched `temp.sqlite_master` + `pragma_database_list`'s **7.8 µs** discard a connection carrying temp objects or an attachment. The crate's own pragmas are restated at 1.0 µs because **no dirt check can see a pragma**. A clean warm call goes **18.6 µs → 29.8 µs** and a dirty one pays 56.5 µs more; R15 stays at **0 bad in 30** unlocked. Two things the release believed and measurement removed: that the gap was the future's size (boxing the cold open changed nothing — the parts simply do not add, 18.3 + 3.5 apart against 27.6 together), and that the Python binding needed an exit-side scrub (a mutation deleted it and nothing failed, because a bare `BEGIN` pins nothing and every statement that would arrives through the entry scrub). Surface **1,730 → 1,733**: `scrub_diagnostic_conn()`, for the Rust caller the entry scrub cannot reach. |
| 0.15.16 | W15.5b | **The read-only flag bounds writes to the file, and one pragma reachable through the side door bounds nothing at all** ([D-258](s13-decision-register.md#d-258)). [D-257] documented the pragma residue its scrub cannot reach and bounded it — *"it cannot change any typed answer"* — which is true of per-connection state and was written without checking that every pragma reachable through `diagnostic_query` is per-connection. Seven were measured, one process each: six hold the claim, and `PRAGMA hard_heap_limit = 1` leaves the **whole process** unable to write, read, `checkpoint()`, `close()`, or **open an unrelated database file**, permanently, all with `out of memory`. `SQLITE_OPEN_READ_ONLY` is no obstacle because setting it is not a write to the file, and the scrub is no help because the limit lives in the SQLite library rather than on any connection — **re-measured against the 0.15.13 per-call shape, the outcome is identical**, so this is not a cost of [D-256]'s sharing and no hygiene reaches it. It belongs with the `ATTACH` warning [D-091] has carried since the beginning: both are properties of arbitrary SQL, not of the flags. Documented rather than blocked — matching SQL text does not survive whitespace or a comment and would do nothing for a Rust caller holding the connection. Doc-only: no code, surface unchanged at **1,733**, suites unchanged. |
| 0.15.17 | W16.1 | **The ancestry is walked once in Rust and bound as a list, and `reconstruct` can answer for one lineage** ([D-259](s13-decision-register.md#d-259)). Two review items, one change seen from both ends. [A-2] asked why every read recomputes its ancestry inside SQL: the recursive CTE becomes `WITH lineage(branch_id, dist, cutoff) AS (VALUES …)`, three bound placeholders per ancestor appended after each reader's own slots. Portability is the argument that started it — Turso has no `WITH RECURSIVE` — and the honest expectation was parity, since [D-219] had measured the CTE as a constant. Measured joined the way the readers join it, it is **−53% at fork depth 1**, −24% at 8, and **+8% at 16**, the two forms crossing near **depth 13**; on a whole read through the public API the only movement that clears the control's 1.5–1.9% noise floor is the recorded-instant read on a fork, **2254.6 → 1879.7 µs (−17%)** at depth 1, and nothing regresses at depth 8. **A-2's cached `Vec<Branch>` was not built**: loading all 17 rows costs **9.6 µs** against the three-aggregate `SELECT`'s **10.4 µs**, so the rows arrive for less than the answer they replace and the read side gained no cache to keep coherent. [C-10] asked why `MaterializedState::edges` returns every lineage's belief unresolved — the nearest-holder rule existed only inside `visible_cte` — and the answer is `reconstruct_on(ts, branch)`, one fold with the ancestry joined in and each ancestor cut at its own fork point, plus a pure `resolve_beliefs` and `Database::ancestry`. **The shape that shipped is not the one its own documentation argued for a day earlier**: fold-per-distinct-instant reuses `reconstruct` whole, snapshot composition included, and measured against the single bounded fold it wins **one configuration in four** by 0.9 ms while costing a factor of 7 at depth 8 (24.0 ms against 3.2). The re-measurement that confirmed it ran both shapes in one process, asserted they return the same edges, and counted the snapshot files before quoting a number — the first comparison did none of the three. Written down rather than glossed: `concepts` is narrowed but **not** resolved by lineage, because it is keyed by id alone. Surface **1,733 → 1,757**. |
| 0.15.18 | W16.1b | **The concept trade-off [D-259] documented is a rule with nothing to decide, and the tie-break it named was not the code's either** ([D-260](s13-decision-register.md#d-260)). D-259 shipped `reconstruct_on` saying its `concepts` are narrowed but not resolved, and that *"where two visible lineages both wrote a concept, the winner is the later log row rather than the nearer lineage"* — written from the shape of the data (a folded concept row carries no `branch_id`) without asking whether that state can exist. It cannot. `concepts.id` is `NOT NULL UNIQUE` and `trg_concepts_cross_lineage` refuses the collision by name, so **four of the five routes to it are refused by the schema**: a fork restating an inherited concept, two siblings minting one id, the trunk minting an id its fork owns, and the trunk minting an id its fork retired. The fifth — `archive_branch` then re-mint — gets past a guard that reads the live table, and **still reaches no reader**: an archived lineage is gone from `branches`, so it is in nobody's ancestry, and the `JOIN` drops its rows on both arms of the fold (the cold arm joins the *union*, not each file), which was measured on the cold arm at an instant where both rows exist. [D-253]'s refusal closes the other end — `rehydrate` will not return a concept whose lineage the ledger has forgotten. The named tie-break was wrong on its own terms too: the fold's outer select has no `ORDER BY`, so the survivor would have been whichever row the engine returned last. A caveat that overstates a hazard is not conservative — this one invited a reader to distrust a correct answer — and what is actually deferred is the *disagreement*, not the resolution: a branch cannot hold its own version of a concept at all, which is [D-214]'s overlay. Doc-only: no code, surface unchanged at **1,757**, suites unchanged. |
| 0.15.19 | W16.2 | **The hygiene batch, and one item of it was a live defect** ([D-261](s13-decision-register.md#d-261)). Eleven review items, C-12 … C-22, in one release. Ten are what the review called them. **C-13 was not.** It read as a note about `COLD_SCHEMA` running before `BEGIN IMMEDIATE`; probed, `ATTACH` creates the cold file before any DDL, so the DDL's placement is not what puts a file on disk — and the consequence is not the mild one recorded. `rehydrate` on a ledger that had never been archived used to ATTACH, meet `no such table: cold.concepts`, and return that error, **leaving a 0-byte cold file behind**; every later `reconstruct` below the newest hot stamp then took the cold arm and failed with a raw `no such table: cold.transaction_log`. A database's entire history, unreadable, because a caller asked to rehydrate something that had never been archived. Fixed in three parts — an empty file is not an archive (which heals a database already in that state), `rehydrate` does not attach when there is nothing to attach, and the schema pass moved inside the session transaction — and pinned by a mutation-found test. The rest: the shadow swap recreates DDL from two named lists with completeness gates instead of `contains("links_current")` [C-12]; `save_and_prune`'s join arm says `SnapshotWriteFailed`, the last arm [D-240] did not reach [C-14]; `registered_models` uses `GLOB`, because `NOT LIKE '%_shadow'` was hiding any model whose name ends in *shadow* [C-15]; the hybrid rank lookup is a `HashMap` instead of ~10M string comparisons [C-16]; the anchored fold's comment stops repeating the rollback mechanism [D-049] disproved [C-17]; `verify_last_link` checks one link for one anchored delta and the cadence runs it after every anchor, so the chain is checked by default for the first time [C-18]; the cadence skips its two aggregates when the actor's turn count has not moved [C-19]; `abort_kind` gates on `SqliteFailure(1811, …)` before reading text [C-20]; and the closed-interval arm's cost is written into its rustdoc [C-21] while `rehydrate` reads and deletes by chunk [C-22]. Surface **1,757 → 1,761**; suites **745** and **726** Rust, **592** Python. |
| 0.15.20 | — | **The branch filter, because a policy with a manual step was forgotten for nineteen releases** ([D-262](s13-decision-register.md#d-262), review C-23). `ci.yml` and `python.yml` trigger on `push` to `main` **and `dev/**`**. [D-234](s13-decision-register.md#d-234) had rejected exactly this by name, and its reasoning was sound: the gap it found was a long-lived branch with no pull request, so the remedy was a draft PR per line and `push:` stayed main-only. **The policy was right and it did not hold.** `dev/0.16.0` was branched and nobody opened one; measured at 0.15.19, `gh run list` returns nothing newer than `v0.15.0` on `main` — **D-243 through D-261 with no CI run of any kind**, which is D-234's own defect arriving through the remedy's single manual step. The trade is restated with the term D-234 could not price: duplicate runs cost runner minutes while a PR from a `dev/**` branch is open; the alternative cost nineteen releases of evidence. **No concurrency group**, deliberately — cancelling the superseded run would remove the duplicate and weaken D-234's standing obligation that *a run exists for the pushed SHA*, since two release commits pushed inside one 46-minute suite would leave the first with its evidence cancelled. **Nothing on this path can publish**, verified on two independent layers: `release.yml` and `wheels.yml` trigger on `push: tags: ["v*"]` and `workflow_dispatch` only, so a branch push never starts them, and their publish jobs are guarded again by `startsWith(github.ref, 'refs/tags/v') || inputs.publish == true`, which a `refs/heads/dev/**` ref fails. `workflow_call` is a separate entry point a push trigger cannot fire. Makes criterion 10 of the 0.16.0 plan reachable — the one `docs/releases/v0.16.0.md` reports as not met, because no run existed to read. CI-only: surface unchanged at **1,761**, suites unchanged at **745** / **726** Rust, **592** Python. |
| 0.15.21 | — | **Three red jobs from the first CI run in nineteen releases, and none of them a defect in the change that triggered it** ([D-263](s13-decision-register.md#d-263)). [D-262](s13-decision-register.md#d-262) widened the branch filter so §8's criterion 10 would have a run to read; run `34023622475` came back red in `fuzz`, `clippy` and `test (windows-latest)`. **The fuzz crate had not compiled since 0.15.13**: [D-255](s13-decision-register.md#d-255) made `NodeAttributes` and `MaterializedState` `#[non_exhaustive]`, `fuzz/src/bin/seed.rs` builds both by struct literal, and that is refused *outside the defining crate only* — so every in-crate use kept working and `fuzz/`, deliberately not a workspace member, is reached by no local gate. Rebuilt on the constructors; given `--fuzz-check` in `run_rust_suite.py` and a stable `cargo check --all-targets` step in `lint`. **Nine rustdoc `-D warnings` errors**, including a link to `CREATE_TXLOG_GUARD_DELETE`, a const that has never existed. `--docs` runs exactly CI's step and has since [D-144](s13-decision-register.md#d-144); what was run instead was bare `cargo doc`, where the same nine arrive as warning lines and a steady warning count reads as a baseline. **The Windows attempt budget had never met a Windows runner**: the main suite crashed 3/3, a different target each time, zero named failures — [D-147](s13-decision-register.md#d-147)'s R15 signature — while ubuntu and macOS passed. Per-OS in the matrix now, three there and **eight on Windows**, which is what the local release gate has used since 0.12.0. Doc comments only in `src/`: surface unchanged at **1,761**, suites unchanged at **745** / **726** Rust, **592** Python. |
| 0.15.22 | — | **A-6 measured: its mechanism no longer exists, the hang it predicted does, and it is a different wait in a different place** ([D-264](s13-decision-register.md#d-264), review A-6). A-6 blames SRWLock's lack of writer preference for a `close()` a hot read loop can starve. Rust's standard library stopped using SRWLock for `x86_64-pc-windows-msvc` — the futex `RwLock` it uses instead refuses a new reader once a writer is waiting — and nothing here targets the `win7` tuple that still gets SRWLock. The read-loop test passes and is **kept as a canary**: the priority policy is documented as unspecified, so it is an implementation detail to monitor rather than rely on. **The real wait is the lock a call already holds.** `with_db` holds the read lock for a whole call, so a bulk import holds it for the whole import and `close()` blocks on `inner.write()` for all of it — **74 ms at 500 edges, 379 ms at 2,000, 992 ms at 4,000**, linear, silent, and indistinguishable from a hang from outside. The drain is not that wait: `close()` holds the write lock before draining, so nobody else's work can be queued by then. And no acknowledged write ever is — `high` awaits a `oneshot` per command — so bounding the caller's wait cannot turn a hang into data loss. Measurement only: four Python tests, **596** passed / 2 skipped, no crate code, surface unchanged at **1,761**, Rust suites unchanged at **745** / **726**. The remedy — a *closing* flag at `with_db` and a bound on acquiring the write lock, neither cancelling anything — lands next, designed against these numbers rather than before them. |
| 0.15.23 | — | **`close()` stops being a silent wait: it refuses latecomers at the door and lets the caller bound the rest** ([D-265](s13-decision-register.md#d-265), review A-6, follows [D-264](s13-decision-register.md#d-264)). A `closing` flag goes up before the write lock is asked for, so every call arriving after a `close()` gets `MacrameClosedError` at once instead of queueing in front of the shutdown — it was going to be refused anyway, and making it wait only lengthens the drain. `close(timeout=…)` bounds the acquisition and raises the new `CloseTimeoutError` with `in_flight` and `waited`. **Neither cancels anything**: the in-flight call finishes and returns its true result, the actor keeps draining, and `high` awaits a `oneshot` per command, so no acknowledged write is ever still queued. A timed-out handle stays *closing* — `close()` again **resumes** the wait — because un-setting the flag would race the next close. `timeout` defaults to `None`, which is the old behaviour exactly, and `__exit__` passes it. Binding only: the Rust crate is untouched, surface unchanged at **1,761**, Rust suites unchanged at **745** / **726**, Python **606** passed / 2 skipped with ten added. |
| 0.15.24 | — | **The four parsers a caller's text passes through get fuzzed, and the mechanism they needed was already in the file** ([D-266](s13-decision-register.md#d-266), review A-5). `timestamp::parse`, `escape_fts5_query`, `validate_id` and `BranchId::new` — four targets, one function each, because libFuzzer scores a corpus against the coverage a target reaches and two parsers behind one binary split a budget. **Not panicking is the floor**: `normalize` must produce what `is_canonical` accepts and be idempotent, `parse` must round-trip to the same instant, and the two refusals are checked on the **accepting** side, since a refusal that is too loose becomes a key nobody can rewrite. `escape_fts5_query` is the one taking end-user text and the only one that cannot refuse, so its property is that the output is quoted alphanumeric runs, quotes balanced, escaping idempotent. **The plan written one commit earlier claimed CI could not run these and wanted a weekly schedule; `ci.yml` has had a nightly `fuzz` job since [D-187](s13-decision-register.md#d-187), on every push** — so these are four steps in it, at 30 s rather than 90. Locally 31 s each gave 1.77 M, 1.63 M, 7.05 M and 5.54 M executions and **zero artifacts**. No crate code: suites unchanged at **745** / **726** Rust and **606** Python, surface unchanged at **1,761**. |
| 0.15.25 | — | **A step that was allowed to fail was never told when to stop, and nothing else in the repository was either** ([D-267](s13-decision-register.md#d-267)). [D-236](s13-decision-register.md#d-236) gave the property-test step the right to fail; it never granted a clock, and no `timeout-minutes` existed anywhere in the four workflow files, so every job ran under GitHub's silent **360-minute** default. Measured over the twelve runs before this one, the quarantined step takes **17.6 / 35.9 / 51.9 min** on Windows (min / median / max) against 1.7–3.7 elsewhere, inside a job whose worst total was **64.7 min** — correcting an earlier report of *"over two hours"*, which was a watcher timing out at fifty minutes and not a measurement. **The cap goes on the step, not only on the job, and that is the design**: a job killed at its limit is *cancelled*, which `continue-on-error` cannot absorb and which would block `release.yml`'s `verify` on exactly the flakiness the quarantine excludes, while a **step** timeout is a step failure the quarantine already handles. **75 min**, ~45% above the worst *healthy* run — on Windows the R15 retries are the healthy case, so a bound drawn from a clean pass would cut runs that work today. Fifteen job caps ride along at 3–5× each job's observed maximum, with that maximum in the comment beside it; `verify` and `suite` are `uses:` jobs and cannot carry one, inheriting the callee's. Noted, not answered: whether the sixth of six attempts against a known-crashing suite buys signal. Workflow-only — suites unchanged at **745** / **726** Rust and **606** Python, surface unchanged at **1,761**. |
| 0.15.26 | — | **A generated history gains a second lineage, and it found a live defect in about a second** ([D-268](s13-decision-register.md#d-268), [D-269](s13-decision-register.md#d-269)). Every property this codebase checked by generation ran on a one-branch graph, so the whole branching wave — a schema version and two archive predicates — was generated-tested only in the shape where every row belongs to `main`. `lineage_property_tests` adds `Fork`, on-branch assert and retire, `archive` and `archive_branch`, with the **key pool weighted 5:5:1:1** toward the keys the trunk seeds, because both of [D-229](s13-decision-register.md#d-229)'s symptoms need a branch writing at an ancestor's *exact* interval key and a uniform draw reaches that never. Sixteen cases of 6–20 ops, not 200 of 5 ([R15](s13-decision-register.md#d-147) counts opens, [D-148](s13-decision-register.md#d-148)); four invariants checked after **every** op. Promotion is automatic and does not use `.proptest-regressions`: that file belongs to the quarantined binary whose exit code nothing reads ([D-236](s13-decision-register.md#d-236)), so a failure writes the history as **text** into `tests/lineage_cases/`, replayed by a second, non-gated binary through the same runner. A third test draws 2,000 histories with no database and holds five shapes between a floor and a **ceiling** — including *"a history that never forks"*, so the ordinary path cannot quietly stop being generated. It reproduced D-229 against a reverted clause in under a second, and then **found something new**: a fork point pins the row its cutoff points at, D-229's clause asks *whose* row is newer and never *when*, so a fork plus an idempotent restatement plus one ordinary `archive` takes a branch's belief away with **no branch write anywhere in the history**. Fixed by three clauses — the one that closes it is on `LOG_ARCHIVABLE`, and which predicate that was got **measured after the symmetrical guess fixed nothing**. Two of D-229's own counts move, with their reach assertions unchanged, which is what makes a moved number legible. Suites **745 → 749** Rust, **726 → 730** feature-off, Python **606** / 2 skipped unchanged, surface **1,761** unchanged, schema **v17** unchanged. |
| 0.15.27 | — | **The two halves of the timestamp module disagreed about their own range** ([D-270](s13-decision-register.md#d-270)). [D-266](s13-decision-register.md#d-266)'s `timestamp_parse` fuzz target went red on 0.15.26's own CI run (`34060567071`) on the input `0060-06-22T22:22:24Z`, which round-tripped to `1970-01-01T00:00:00.000000Z`. `parse` accepts any canonical stamp and canonical means a **four-digit year**, so its range starts at year 0; `format` began `duration_since(UNIX_EPOCH).unwrap_or_default()`, so its range started at **1970**, and every instant between the two came back as the epoch — the wrong day, no error. `format`'s own rustdoc said the schema had no use for pre-1970 stamps and **was wrong about its own schema**: `0060-06-22` is four digits, the `CHECK` glob accepts it, the storage layer stores it, and it sorts correctly against every later stamp. Valid time is the caller's, not the clock's. Fixed by widening `format` rather than narrowing `parse` — narrowing would remove a capability the storage layer already has — with saturation kept at the ends the *form* imposes (year 0000 and the open sentinel) so `format` stays infallible and always canonical. **Writing the test for that found a second defect underneath**: `parse` built one `Duration` from `secs.unsigned_abs()` and the microseconds and then chose add-or-subtract by the sign, which subtracts the microseconds too on the pre-epoch side — `1969-12-31T23:59:59.999999Z` parsed nearly two seconds early. Now one signed quantity, split once. Neither was visible on the Windows box every local gate runs on: `SystemTime` is a `FILETIME` whose own epoch is **1601**, so `parse` refuses the whole pre-1601 range here with a typed, platform-named error — which is [D-234](s13-decision-register.md#d-234)'s argument arriving from a new direction. Suites **749 → 751** Rust, **730 → 732** feature-off; Python **606** / 2 skipped, surface **1,761** and schema **v17** unchanged. |
| 0.15.28 | — | **The operations nobody bounds get a report, and pricing them found a branch write costing a thousand times a trunk write** ([D-271](s13-decision-register.md#d-271), review A-5, Part 3). A-5 said the exempt kinds' cost is invisible; reading the code, the numbers were being *collected* all along and the **report** dropped them. `record_hold` skips only the `over_budget` counter for an exempt kind, and `budget_violations()` filters on `over_budget > 0` — zero by construction — so no workload could ever put an archive, a rebuild, a checkpoint or a shadow swap in that list, and **a list that cannot be non-empty is indistinguishable from a clean bill of health**. `MetricsSnapshot::exempt_costs()` is the companion report, sorted by `longest` because `over_budget` here is a column of zeros, and it ships in **Python as well as Rust** — more than Part 3 wrote down, because the gap A-5 names is a dashboard's. A criterion group `exempt_kinds` prices the four kinds that had no arm, each stating its expected shape so two scales falsify it without a baseline file: `rebuild_current` **30.4 ms → 156.8 ms** (grows with the ledger, held — 5.2x here against 4.2x in an earlier session that measured the same 8,000 figure and the same 1.51 µs control, so the spread is this arm's own and two points cannot tell linear from mildly super-linear), `checkpoint` **1.52 ms → 1.52 ms** (flat, held), `archive_branch` **10.7 ms → 23.4 ms** (expected flat in the trunk, **falsified** — no index on `links` or `transaction_log` leads with `branch_id`, so five lineage-scoped statements each scan a trunk-sized table), `shadow_swap` **7.6 ms → 49.5 ms** (**6.5x on a 4x fixture**, super-linear where the comment said linear; [D-082](s13-decision-register.md#d-082)'s 46.8 ms at 10,000 keys sits on the same curve). **The largest number is in a path this group does not measure**: making the lineage the archive arm needs, the same 200-edge batch costs **55 ms on `main` and 75 s on a fork of it** — about 1,350x, growing with the *trunk* — because the write path's per-row overlap resolution plans its `transaction_log` arm as `SEARCH transaction_log USING INDEX idx_txlog_fold_partition (table_name=?)`: the whole links log, per asserted row, discarded. Recorded and **not repaired here** — a write-path change wants its own before-and-after, and a release that made costs visible should not also be the one that changed them. Nothing is gated ([D-055](s13-decision-register.md#d-055)). Surface **1,761 → 1,763**; suites **751 → 753** Rust, **732** feature-off unchanged, Python **606 → 607** / 2 skipped; schema **v17** unchanged. |
| 0.15.29 | — | **Four words of join order, and a branch write that had been reading the whole log once per asserted row** ([D-272](s13-decision-register.md#d-272), found by [D-271](s13-decision-register.md#d-271)). D-271 deferred this on a stated condition — its own before-and-after on both shapes, and its own argument about which of two costs was being fixed — and both are here. **The join order, alone**: the automatic index D-271 named as the other suspect survives the repair untouched and costs nothing measurable. The plan was also worse than that entry recorded: `churned` was not evaluated once and found empty, it was **inlined as the inner loop** and re-derived per log row with one column bound, losing both [D-250](s13-decision-register.md#d-250)'s key narrowing and the `recorded_at > cutoff` bound `idx_lc_lineage_cut` exists for — so the arm is O(log rows × lineage rows) *per asserted row*, which is why the guard cost **268.6 ms/row at a 2,000-edge trunk and 5,408.1 ms/row at 8,000**: twenty times for four times the trunk. `FROM churned k CROSS JOIN transaction_log` nails the loops down, sound because `links_current`'s primary key makes `churned` unique on the `(entity_id, branch_id)` pair the join matches, and the log then becomes a **three-column** seek — so **[D-254](s13-decision-register.md#d-254)'s index is not the villain and is not touched**; it was being used with a third of itself bound. End to end, one 200-edge batch on a 2,000-edge trunk: **68.0 s → 49.0 ms** on a fork, nothing on the trunk. **The arm is not the write path's alone** — every branched *current-belief read* emits it unkeyed, and `edges(plan)` at fork depth 8 goes **13.6 ms → 1.3 ms** and stops growing with depth at all, while the transaction-time read and the trunk control do not move. Gated by plan and not by millisecond ([D-055](s13-decision-register.md#d-055)): `the_log_arm_is_driven_by_the_churned_set` pins the bound column on the generated statement, asserts the two trunk shapes touch the log not at all, holds it on an empty fixture where the planner has only the query text, and was verified by injection. The 0.15.8 rustdoc claiming this plan still held is left standing with the correction beside it — it was true when written and false four releases later, which is what a measured plan without a pin is worth. Suites **753 → 754** Rust, **732 → 733** feature-off; Python **607** / 2 skipped, surface **1,763** and schema **v17** all unchanged. |
| 0.15.30 | — | **The archive stopped reading the trunk to find a lineage, and the index that fixed it costs the write path nothing** ([D-273](s13-decision-register.md#d-273), schema **v17 → v18**). [D-271](s13-decision-register.md#d-271)'s other open question, deferred there in as many words: whether an index leading with `branch_id` earns its write cost. **As asked, no; in a form it did not consider, yes.** Six statements in `archive_branch_session` filter on `branch_id = ?` and nothing on `links` or `transaction_log` led with that column, so archiving a twenty-row lineage cost what the whole ledger cost — **9.5 ms at a 2,000-edge trunk against 22.0 ms at 8,000**, and [D-230](s13-decision-register.md#d-230)'s all-or-nothing chain means every millisecond of it is a held write lock. Full indexes on both tables make it **6.8 ms and flat in the trunk**, and cost **10–15% of every bulk write in the crate, forever**, so that a hand-run operation is fast; refused. What ships instead is the same pair **partial**, `WHERE branch_id <> 'main'` — sound because `refuse_unarchivable_branch` refuses the trunk in its first three lines, so the rows every ordinary write adds do not belong in an index built for archival. Same plans, **22.0 → 12.0 ms**, a 200-edge batch at **24.8 ms against the unindexed 24.9**, and **+20 KB on disk against +260 KB**. The price is that five statements must restate `AND branch_id <> 'main'`, because SQLite uses a partial index only where the query's `WHERE` *implies* the index's — and that price is the safety: neither index can be reached by any other reader of these tables, so nothing new appears in front of the fold ([D-254](s13-decision-register.md#d-254)) or the log arm [D-272](s13-decision-register.md#d-272) nailed down one release ago, and the statistics `ANALYZE` records (`80 20`) describe branches and stay true however large the trunk grows, where a full index's (`9144 1829`) decay toward the ratio at which the planner declines it. **What it does not close is measured and named**: the foreign-key child search `DELETE FROM branches` performs on all four ledger tables is SQLite's own text, carries no predicate, and no partial index can serve it — full ones would (7.2 ms, flat) for the write cost this entry declined. Gated by plan, not by millisecond ([D-055](s13-decision-register.md#d-055)), on a fixture with **four live lineages**, because a partial index is empty on a branchless ledger and an empty index is attractive to any planner; the cutoff path's statements are pinned as *not* touching either index, and the whole was verified by injection three ways. Suites **754 → 757** Rust, **733 → 736** feature-off; Python **607** / 2 skipped and surface **1,763** unchanged; schema **v17 → v18**. |
| 0.16.0 | The 0.15.0 review | **The cycle lands.** Thirty releases, [D-243](s13-decision-register.md#d-243)…[D-273](s13-decision-register.md#d-273), against a written review of 0.15.0 — twenty-four numbered code findings and six architectural notes, of which **twenty-seven are closed**. Schema **v15 → v18** over three rungs; snapshot container v4 and its v3 header unchanged; MSRV 1.88 unchanged; `DbError` 41 → 42 variants; public surface 1,624 → **1,763 items**. **The interesting half is that the review was reliably right that something was wrong and much less reliably right about why**, and every correction here is a measurement rather than a re-reading: its only High was High, its C-4 asked for an index that is never faster than no index, its C-11 named three structs where the defect was twenty-one wide, its C-8's suggested fix repeats the bug one line further down, and its C-13 — filed **Low**, *"nothing downstream is wrong"* — was a live data-availability bug. **And the largest single defect of the cycle is not in the review at all**: `links_at_tx` had been a co-routine inside the recursive step since 0.13.2, 10.6 s where the answer takes 59 ms, hidden from eleven releases of branching probes because they were all shaped to time the branched read. Waves W13 (the read path's one lowering), W14 (the log and the reach guard), W15 (the public surface and `#[non_exhaustive]`), W16 (cost visibility, and the two findings that visibility produced). Of §8's ten acceptance criteria, seven met, two restated then met, and the tenth met late — after two further releases, because reaching it meant first building the thing that could check it. Suites 637 → **757** Rust, 618 → **736** feature-off, 559 → **607** Python / 2 skipped, replicated on three runners at the release commit. Three items remain open by decision and are named in the note ([A-4](../Macrame%20Codebase%20Review%20v0.15.0.md), A-5, and `cargo-semver-checks` at 1.0), and A-4 is the one this cycle made worse. |
| 0.16.1 | — | **The spike ladder, and the defect it found on the bulk path** ([D-274](s13-decision-register.md#d-274), [D-275](s13-decision-register.md#d-275)). A fresh-file `bulk_import` was superlinear — 16,000 edges in 8.6–12 s where the fix takes **2.22 s (3.9×)** — because on a database with rows and no statistics the planner served `trg_links_single_open`'s probe from `idx_lc_lineage_cut` with only `branch_id` bound: a whole-lineage scan **per trigger firing**, O(rows) per row. The plan pin had been green throughout because its fixture was empty — D-273's partial-index lesson from the other side. The branch predicate carries a unary `+` now (D-250's idiom, schema **v18 → v19**), per-row cost is flat across the ladder, and the registry is swept across the statistics-free state so the next instance is a red rung rather than four releases of drift. **Measured and closed alongside**: the per-row overlap guard costs 1.4 µs/row (batching saves 0.6), a multi-row `INSERT` is 7% *slower* than per-row executes, and the WAL during a random-order bulk is a dial — 5.0 s at the default threshold, 3.7 s at 10,000 pages with a ~43 MB WAL — documented as a recipe in `bulk_import`'s docstrings. The embedding path's growth is engine-side (DiskANN) and stays deferred to the OKFgraph rework. Suites **757 → 759** Rust, **736 → 738** feature-off; Python **607** / 2 skipped and surface **1,763** unchanged; schema **v18 → v19**. |
| 0.16.2 | The 0.16.1 comparison | **The vector build is dissected, and the recipe ships callable** ([D-276](s13-decision-register.md#d-276), plan §9.1). The gap in the comparison table was the embedding path: 2k×256 at 29.8 s against 3.5 s, and superlinear in dimension (4× dims, 7.8× time). The dissection closes three hypotheses in one run: blob inserts are flat at ~5 µs/row at every width, so the cost is DiskANN maintenance engine-side; the drop → load → rebuild recipe is real but bounded — **1.4–1.8×** (19.7 s vs 31.0 s at dim 256; **48.6 s vs 89.2 s** at 5k×256), the rest of the gap being DiskANN-vs-HNSW economics inside libSQL; and the WAL recipe that closed a third of the edge path's residual closes nothing here. `Database::bulk_embeddings[_with]` ships the recipe through the write actor with the trade stated in the docstring — unsearchable and storage-unchecked between drop and rebuild, **the failure path rebuilds before it reports**, and the rebuild is budget-exempt by D-233's criterion because it is one indivisible `CREATE INDEX`. The query side closes too: bare `vector_top_k` is 99% of `search_vector`'s cost, so the search gap is the DiskANN search itself. Suites **759 → 764** Rust, **738 → 743** feature-off; Python **607 → 612** / 2 skipped; surface **1,763 → 1,773**; schema unchanged at **v19**. |
| 0.16.3 | The 0.16.1 comparison | **The materialization-skipping bulk ships, and the edge path's last lever lands** ([D-277](s13-decision-register.md#d-277), plan §8.1). Scoped first by measurement (`examples/bulk_skip_probe.rs`): the `links_current` mirror trigger is 2.2× of the random-pair bulk, and `rebuild_current_chunked` re-derives the projection for 191 ms per 16k — so the skip arm totals **2.48 s against the shipped 5.08 s (2.05×)**, flat where the shipped arm grows. `bulk_import_deferred[_with]` drops the mirror, loads through the same chunked path, restores **before** rebuilding, and reports only after both; the ledger is complete at every instant of the window (the log mirror and the single-open guard stay up), so what lags is Doctrine VI's projection, never the past. A failure restores and rebuilds before it reports; the toggle is `links_current_mirror`, one DDL statement, exempt like its D-276 siblings. Suites **764 → 769** Rust, **743 → 747** feature-off; Python **612 → 616** / 2 skipped; surface **1,773 → 1,781**; schema unchanged at **v19**. |
| 0.17.0 | The 0.16.1 comparison | **The comparison cycle lands.** Four releases, [D-274](s13-decision-register.md#d-274)…[D-277](s13-decision-register.md#d-277), against a table rather than a review: the same battery run against ladybug 0.20.3 on the same box, with 0.16.1, 0.16.2 and 0.16.3 plugged in as they landed ([D-274](s13-decision-register.md#d-274), [D-275](s13-decision-register.md#d-275), [D-276](s13-decision-register.md#d-276), [D-277](s13-decision-register.md#d-277)). Schema **v18 → v19** at 0.16.1 and unchanged after; MSRV 1.88 unchanged; `DbError` unchanged at 42 variants; public surface 1,763 → **1,781 items**, every one additive and every one opt-in. **The cycle is measured rather than argued, and its plan document is its own acceptance list**: the edge bulk went from 8.6–12 s at 16k to 2.2 s, then to **2.48 s incl. the chunked rebuild via `bulk_import_deferred` (2.05×)**; the vector build got the drop-rebuild recipe (**1.4–1.8× at every width**, 48.6 s vs 89.2 s at 5k × 256) with the trade stated in the docstring; and three hypotheses — the ladder residual, the footprint, the keyword cell — closed with numbers instead of code ([D-277]'s probe found the mirror trigger was 2.2× of the random-pair bulk; `dbstat` found two-thirds of the footprint is the DiskANN shadow for 5.3 MB of vectors; the full 20k-row match is ~32 ms against ladybug's 47 ms for 18k). What remains is named rather than glossed: DiskANN-vs-HNSW build economics and the shadow's size are libSQL's — the crate does not fork its engine, and the search side is flat in corpus size and inside §9's ≤20 ms line. Suites 757 → **769** Rust, 736 → **747** feature-off, 607 → **616** Python / 2 skipped, replicated on three runners at the pre-release tip (code-identical; the release commit changes version strings and prose only). |
Contents — [§0](s0-s3-foundations.md#0-doctrine) Doctrine · [§1](s0-s3-foundations.md#1-purpose-and-scope) Purpose · [§2](s0-s3-foundations.md#2-system-context) System context · [§3](s0-s3-foundations.md#3-crate-layout) Crate layout · [§4](s4-schema.md#4-schema) Schema · [§5](s5-modules.md#5-modules) Modules · [§6](s6-s10-flows-to-dependencies.md#6-data-flows) Data flows · [§7](s6-s10-flows-to-dependencies.md#7-errors) Errors · [§8](s6-s10-flows-to-dependencies.md#8-testing-strategy) Testing · [§9](s6-s10-flows-to-dependencies.md#9-performance-budgets) Performance · [§10](s6-s10-flows-to-dependencies.md#10-dependencies) Dependencies · [§11](s11-s12-milestones-and-risks.md#11-milestones) Milestones · [§12](s11-s12-milestones-and-risks.md#12-risks) Risks · [§13](s13-decision-register.md#13-decision-register) Decision register · [§14](s14-python-bindings.md#14-python-bindings) Python bindings · Appendices A–D
<!--index-->
---
## The document, by file
This is the original single-file architecture document, split into one file per group of sections. **The prose is unchanged**: concatenating the files below, minus the navigation header each carries and this table, reproduces the former `Macrame v0.5.4.md` — see [REJOIN.md](REJOIN.md), where the command is checked rather than asserted. The one edit since the split is markup, not text: cross-references are now links (below), and stripping that markup back out returns the document byte for byte.
| File | Sections | What it settles |
|---|---|---|
| [Foundations](s0-s3-foundations.md) | §0 Doctrine · §1 Purpose · §2 System context · §3 Crate layout | The eight invariants, the `as_of` / `reconstruct` distinction, the boundary diagram, and one concern per module |
| [Schema](s4-schema.md) | §4 | **Normative.** Tables, triggers, guards, the canonical timestamp form, the two derivative tables |
| [Modules](s5-modules.md) | §5 | The Write Actor, traversal, replay and snapshots, archive, integrity, vectors |
| [Flows to dependencies](s6-s10-flows-to-dependencies.md) | §6 Data flows · §7 Errors · §8 Testing · §9 Performance · §10 Dependencies | Five end-to-end paths, `DbError`, what each test layer can prove, the budgets, the tree |
| [Milestones and risks](s11-s12-milestones-and-risks.md) | §11 · §12 | M1–M5 with exit gates; R1–R15 with mitigations |
| [Decision register](s13-decision-register.md) | §13 | D-001…D-277: the authoritative record of intent |
| [API review 0.13.0 → 0.14.0](api-review-0.14.0.md) | — | Generated. The item-by-item surface review §14's item 6 asked for, and the evidence for [D-212](s13-decision-register.md#d-212) |
| [API review 0.15.0 → 0.16.0](api-review-0.16.0.md) | — | Generated, and open for the cycle. The item-by-item surface diff, and the paragraph on what an item-level review cannot see ([D-255](s13-decision-register.md#d-255)) |
| [API review 0.16.0 → 0.17.0](api-review-0.17.0.md) | — | Generated. **+7 distinct items, 0 removed** — two bulk methods and their `_with` siblings, three `CommandKind` variants ([D-276](s13-decision-register.md#d-276), [D-277](s13-decision-register.md#d-277)); 1,763 → 1,781 lines counted the way Appendix D counts |
| [Python bindings](s14-python-bindings.md) | §14 | The wheel: build topology, the async→sync boundary, errors and timestamps across the FFI, and what it deliberately does not expose |
| [Appendices](appendices.md) | A · B · C · D | **Normative (A, D).** The API surface and what it was once promised to be; the glossary; the deferred list; and what 1.0 freezes — and does not |
**Start here** if you are new: the doctrine in [Foundations](s0-s3-foundations.md), then [Schema](s4-schema.md), then the [Decision register](s13-decision-register.md). The doctrine says what may not change, the schema is what encodes it, and the register is why everything else looks the way it does.
**Cross-references are links.** Every `§5.5`, `D-029`, `R15`, `Doctrine VI` and `Appendix A` in the prose resolves to where it is defined, across files as well as within them. Section links target the heading's own anchor; decisions, risks and doctrines have no headings of their own, so each definition site carries an invisible `<a id="…"></a>` — `#d-029`, `#r15`, `#doctrine-vi` — and that is the only thing added to the text besides the links themselves.
Four things are deliberately *not* linked: headings, so a section title never links to itself; fenced code and inline code spans, where `§5.5` is a quoted string rather than a reference; the definition site of each decision, risk and doctrine, which carries the anchor instead; and the table above, whose file links are already the point.