8000 merge: main into v3 (v2.0.1, #323, #381, #315, #389, #297) by adityamukho · Pull Request #412 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content

merge: main into v3 (v2.0.1, #323, #381, #315, #389, #297) - #412

Merged
adityamukho merged 12 commits into
v3from
merge/main-into-v3
Sep 26, 2026
Merged

adityamukho merged 12 commits into
v3from
merge/main-into-v3

Conversation

@adityamukho
Copy link
Copy Markdown
Collaborator

Brings everything on main since the v8 split into v3. Merge with a merge commit, not squash or rebase, so git records main as merged and future main→v3 merges stay small.

Keeping v8

main contains 15aaf22 (#378), which reverts format v8. It is recorded as merged with git merge -s ours (commit e884c42), so v3 keeps its v8 code. The rest of main is merged normally in cf23bfc.

Conflict resolutions

Verification

cargo fmt --check and cargo clippy -- -D warnings are clean. cargo test: 1198 passed, 0 failed, 8 ignored.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

adityamukho and others added 12 commits September 25, 2026 10:41
Main must stay releasable as v2.x so v2.1–v2.3 (and #370's unreleased
fix) can ship without a forced one-way format migration. The v8 work
(#371, #287) is preserved unchanged on the long-lived `v3` branch
(at b8a9762), which will merge main regularly and becomes main when
v3.0.0 is cut.

This reverts commit b8a9762.


Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Patch release on the v2.x line; file format unchanged (v7), no API
changes. Ships #372: the index rebuild on open now uses each fact's
real on-disk location, so entity-bound lookups no longer come back
empty after a crash-triggered rebuild of a multi-checkpoint file.

Doc sync: CHANGELOG (v2.0.1 entry + known issues #371/#287, #373/#374),
CLAUDE.md, README.md, ROADMAP.md (v2.x on main, v3.0.0 on the `v3`
branch), docs/TEST_COVERAGE.md (1154 tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy
…mitigation) (#380)

* docs: design spec for point-query history scan mitigation (#323)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* docs: implementation plan for #323; spec: drop dedup, threshold collapse

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* perf: add in-crate FxHash hasher for hot-path maps (#323)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* perf: borrow keys and use FxHash in net_asserted_facts (#323)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* perf: add (entity, attribute) EAVT range lookup (#323)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* perf: narrow bound-entity fetch to queried attributes, drop redundant dedup (#323)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* perf: rotate FxHasher output so hashbrown's low-bit bucket index is mixed (#323)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* bench: point_query_chain_depth (#323)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* docs: changelog, benchmarks and test counts for #323; revise spec acceptance to measured

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* fix: bound (entity, attribute) EAVT scan exactly; pin same-tx multi-window fix (#323)

End the committed range at attribute + NUL (exact successor) instead of
incrementing the last byte, which returned None for attributes ending in
0x7F or a 0xBF UTF-8 byte and left the scan unbounded to the end of the
index. Also stops resolving prefix siblings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* perf: keep std's keyed hasher in net_asserted_facts; drop FxHash (#323)

Fact values are often untrusted text (agent memory); a fixed-seed fast
hash would let crafted colliding values make queries quadratic. The
borrowed-key / encode-once rewrite stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* docs: final #323 benchmark numbers (std hasher, AC) and test counts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…382)

get_facts_by_attribute ended its AEVT range at next_string_prefix(attr),
which increments the last byte. For attributes ending in 0x7F or 0xBF
(e.g. `:丿`, `:ÿ`) that is not valid UTF-8, so the committed scan ran to
the end of the AEVT index. It also covered prefix siblings (`:ab` for `:a`).

End the range at `attribute + "\0"`, the exact successor, as #380 does for
the entity path. Remove the now-unused next_string_prefix.

Sparse `:丿` scan (100 of 40.1k facts, checkpointed): 45.4 ms -> 251 µs.

Closes #381
Refs #323


Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Shared runners make single-night Bencher alerts mostly noise. From
Jul 1 to Sep 25, 31 of 87 nightly runs raised alerts, and none were
real regressions (see #301).

The report step now:
- saves the Bencher output and reads the alerted benchmark names
- reads the same suite's alerts from the previous completed run
- reports only benchmarks that alerted on both runs
- comments on an open regression issue instead of skipping it, so an
  old open issue no longer hides new alerts
- still reports at once when Bencher fails without listing alerts

Adds the actions: read permission to read the previous run's job logs.


Claude-Session: https://claude.ai/code/session_019dotPmboGEcQoQWSngbceY

Co-authored-by: Claude <noreply@anthropic.com>
…every index (#315) (#402)

* docs: design spec for incremental checkpoint index rebuild (#315)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* docs: implementation plan for incremental checkpoint (#315)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* bench: add checkpoint/after_1_fact group (#315)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* bench: run one dirty fact per checkpoint iteration (#315)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* refactor(btree_v6): extract leaf encoding, fill rule, internal-level build (#315)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* feat(btree_v6): incremental rebuild that copies untouched leaves (#315)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* perf: checkpoint copies untouched index leaves and skips re-hashing fact pages (#315)

save() snapshots each index's raw leaf pages and rebuilds via
rebuild_btree_incremental: leaves no pending entry touches are copied
verbatim, touched leaves split balanced so repeated checkpoints do not
fragment the tree. The CRC32 state over unchanged fact pages is cached
so each save hashes only new fact pages and the index region. File
format and stored checksum are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* docs: checkpoint cost and durability semantics; #315 benchmarks and doc sync

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

* fix: reject cyclic leaf chains and damaged leaves before copying them (#315)

Untouched leaves are now copied verbatim and sealed under a fresh file
checksum, so collect_leaf_pages bounds-checks each leaf's slot directory
and caps the chain at the file's page count instead of looping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#403)

v2.3.0 becomes v2.0.2: the merged #323/#381/#315 mitigations plus
#389, #393, #395, #397, #399, #400. The new production-readiness
issues (#383 tracker) go into v3.0.0 with format v8 and #371. Features
from v2.1.0/v2.2.0 and the old v3.0.0 scope move to a new v3.1.0.


Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…/WAL and deleting the WAL (#389) (#404)

On POSIX a created or deleted file survives a power loss only once its
parent directory is fsynced. Minigraf fsynced file contents but never a
directory, so a power loss could lose a freshly created .graph or
<db>.wal, or resurrect a WAL that checkpoint() had deleted.

Add storage::dir_sync::sync_parent_dir (open parent dir + sync_all on
Unix; no-op elsewhere) and call it:
- in FileBackend::open_with after writing the header of a new/empty file
- in WalWriter::open_or_create after creating the WAL, and after
  re-initializing a headerless WAL left by a crash
- in WalWriter::delete_file after a successful remove

A test-only thread-local log records each sync and whether the child
existed at that moment, so unit tests check both that the sync happens
and that it follows the create/remove.

Resurrected-WAL replay idempotence was already covered by
tests/edge_cases_test.rs::test_stale_wal_after_checkpoint_is_idempotent.


Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…408)

- ERROR_REFERENCE: WAL-001/WAL-002/STG-011 said deleting the .wal loses
  no data, or that a database can be rebuilt from the WAL alone. Both
  are wrong. Replace with safe steps. Fix ~50 wiki links that pointed
  at a local .wiki path.
- ROADMAP/CLAUDE.md: match the v2.0.2, v3.0.0 and v3.1.0 milestones.
  Document error.rs, magic_sets.rs, fault_inject.rs and src/browser/.
- README: state MSRV 1.89, show the #371 known issue, fix Maven
  coordinates (io.github.project-minigraf), Android on Maven Central,
  and point binding downloads to the binding repos.
- SECURITY.md: 2.x is the supported line.

Refs #383, #397, #399


Claude-Session: https://claude.ai/code/session_01BBxgR4mnF8VMVGa42idcxx

Co-authored-by: Claude <noreply@anthropic.com>
…#297) (#409)

A recursive rule whose intermediate variable is bound by a call to another
user rule, e.g. (chain ?x ?y) :- (link ?x ?mid) (chain ?mid ?y), failed
with "Unbound variable in rule head: ?mid" (INT-022) when queried with a
literal first argument such as (chain :a ?y).

build_propagation_rules built the magic rule for the recursive call from
the guard plus only the non-RuleInvocation clauses preceding it, so it
emitted (__magic_chain_bf ?mid) :- (__magic_chain_bf ?x) with ?mid unbound.
#300 fixed only the case where a fact pattern binds the intermediate.

Keep every preceding clause, including rule invocations, as standard
magic-sets rewriting does. All rewritten rules are positive, so the added
dependency on the (guarded) called predicate is safe.

Adds integration tests for the reopened repro and a left-recursive variant,
and a unit test pinning the propagation-rule body.


Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The v8 revert on main (#378, 15aaf22) was recorded with `-s ours` in the
previous merge commit, so v3 keeps file format v8.

Conflict resolutions:
- storage.rs: #381's exact `attribute + "\0"` AEVT bound and #323's
  (entity, attribute) EAVT range use the v8 key constructors; adds
  `EavtKey::entity_attribute_start` since v8 keys carry value_bytes/asserted.
- executor.rs: take main's attribute-narrowed selective fetch without
  dedup (#323). v8's net_asserted_facts keeps one winner per
  (entity, attribute, value, valid window), so duplicate records are
  harmless and the #371 widened dedup key is no longer needed.
- persistent_facts.rs: keep #315's cached fact-page CRC prefix; drop the
  v7-era fact-only checksum fallback, which v3 already removed. save()'s
  leaf copy only sees v8 leaves: a v7 open rebuilds and writes a v8 header
  before any save.
- Docs: ROADMAP takes main's milestone layout; CHANGELOG keeps v3's
  Unreleased section and adds main's v2.0.2 entries and v2.0.1 below it;
  test counts updated (1198 passing, 8 ignored).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BedF9ZmqVZLkvDwn91fuKy
@adityamukho
adityamukho merged commit 9d81340 into v3 Sep 26, 2026
@adityamukho
adityamukho deleted the merge/main-into-v3 branch September 26, 2026 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0