uu-link-r 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. uu_link_r-0.4.0/PKG-INFO +60 -0
  2. uu_link_r-0.4.0/README.md +41 -0
  3. uu_link_r-0.4.0/graph-r/.gitignore +23 -0
  4. uu_link_r-0.4.0/graph-r/CHANGELOG.md +285 -0
  5. uu_link_r-0.4.0/graph-r/Cargo.lock +950 -0
  6. uu_link_r-0.4.0/graph-r/Cargo.toml +68 -0
  7. uu_link_r-0.4.0/graph-r/LICENSE +27 -0
  8. uu_link_r-0.4.0/graph-r/README.md +196 -0
  9. uu_link_r-0.4.0/graph-r/benches/baselines/store.json +24 -0
  10. uu_link_r-0.4.0/graph-r/benches/store_bench.rs +230 -0
  11. uu_link_r-0.4.0/graph-r/deny.toml +74 -0
  12. uu_link_r-0.4.0/graph-r/rust-toolchain.toml +24 -0
  13. uu_link_r-0.4.0/graph-r/rustfmt.toml +14 -0
  14. uu_link_r-0.4.0/graph-r/src/bytesio.rs +232 -0
  15. uu_link_r-0.4.0/graph-r/src/compact.rs +562 -0
  16. uu_link_r-0.4.0/graph-r/src/container.rs +319 -0
  17. uu_link_r-0.4.0/graph-r/src/error.rs +94 -0
  18. uu_link_r-0.4.0/graph-r/src/format/base.rs +760 -0
  19. uu_link_r-0.4.0/graph-r/src/format/mod.rs +16 -0
  20. uu_link_r-0.4.0/graph-r/src/format/wal.rs +637 -0
  21. uu_link_r-0.4.0/graph-r/src/fsio.rs +120 -0
  22. uu_link_r-0.4.0/graph-r/src/hash.rs +140 -0
  23. uu_link_r-0.4.0/graph-r/src/key.rs +141 -0
  24. uu_link_r-0.4.0/graph-r/src/lib.rs +115 -0
  25. uu_link_r-0.4.0/graph-r/src/mmap.rs +123 -0
  26. uu_link_r-0.4.0/graph-r/src/overlay.rs +285 -0
  27. uu_link_r-0.4.0/graph-r/src/query.rs +576 -0
  28. uu_link_r-0.4.0/graph-r/src/rank.rs +112 -0
  29. uu_link_r-0.4.0/graph-r/src/snapshot.rs +678 -0
  30. uu_link_r-0.4.0/graph-r/src/store.rs +821 -0
  31. uu_link_r-0.4.0/graph-r/src/text.rs +146 -0
  32. uu_link_r-0.4.0/graph-r/src/traverse.rs +214 -0
  33. uu_link_r-0.4.0/graph-r/src/ttl.rs +162 -0
  34. uu_link_r-0.4.0/graph-r/src/writer.rs +450 -0
  35. uu_link_r-0.4.0/graph-r/tests/api_v030.rs +211 -0
  36. uu_link_r-0.4.0/graph-r/tests/concurrency.rs +114 -0
  37. uu_link_r-0.4.0/graph-r/tests/hermetic.rs +377 -0
  38. uu_link_r-0.4.0/graph-r/tests/lineage.rs +202 -0
  39. uu_link_r-0.4.0/graph-r/tests/proptest_model.proptest-regressions +10 -0
  40. uu_link_r-0.4.0/graph-r/tests/proptest_model.rs +206 -0
  41. uu_link_r-0.4.0/graph-r/tests/regression.rs +602 -0
  42. uu_link_r-0.4.0/graph-r/tests/security.rs +385 -0
  43. uu_link_r-0.4.0/graph-r/tests/support/golden.rs +191 -0
  44. uu_link_r-0.4.0/link-r/.gitignore +30 -0
  45. uu_link_r-0.4.0/link-r/CHANGELOG.md +199 -0
  46. uu_link_r-0.4.0/link-r/Cargo.lock +3913 -0
  47. uu_link_r-0.4.0/link-r/Cargo.toml +171 -0
  48. uu_link_r-0.4.0/link-r/LICENSE +27 -0
  49. uu_link_r-0.4.0/link-r/README.md +175 -0
  50. uu_link_r-0.4.0/link-r/benches/baselines/index.json +27 -0
  51. uu_link_r-0.4.0/link-r/benches/crawl_bench.rs +318 -0
  52. uu_link_r-0.4.0/link-r/benches/index_bench.rs +240 -0
  53. uu_link_r-0.4.0/link-r/benches/kb_bench.rs +106 -0
  54. uu_link_r-0.4.0/link-r/benches/support/mod.rs +99 -0
  55. uu_link_r-0.4.0/link-r/bindings/python/Cargo.lock +3624 -0
  56. uu_link_r-0.4.0/link-r/bindings/python/Cargo.toml +45 -0
  57. uu_link_r-0.4.0/link-r/bindings/python/README.md +41 -0
  58. uu_link_r-0.4.0/link-r/bindings/python/link_r.pyi +115 -0
  59. uu_link_r-0.4.0/link-r/bindings/python/src/lib.rs +384 -0
  60. uu_link_r-0.4.0/link-r/bindings/python/tests/test_smoke.py +111 -0
  61. uu_link_r-0.4.0/link-r/bindings/python/tests/test_threading.py +122 -0
  62. uu_link_r-0.4.0/link-r/deny.toml +74 -0
  63. uu_link_r-0.4.0/link-r/examples/crawl_github_docs.rs +69 -0
  64. uu_link_r-0.4.0/link-r/rust-toolchain.toml +27 -0
  65. uu_link_r-0.4.0/link-r/src/auth/anonymous.rs +41 -0
  66. uu_link_r-0.4.0/link-r/src/auth/mod.rs +136 -0
  67. uu_link_r-0.4.0/link-r/src/auth/oauth.rs +175 -0
  68. uu_link_r-0.4.0/link-r/src/auth/static_token.rs +160 -0
  69. uu_link_r-0.4.0/link-r/src/bridge.rs +429 -0
  70. uu_link_r-0.4.0/link-r/src/embed/hash.rs +179 -0
  71. uu_link_r-0.4.0/link-r/src/embed/mod.rs +81 -0
  72. uu_link_r-0.4.0/link-r/src/embed/onnx.rs +138 -0
  73. uu_link_r-0.4.0/link-r/src/error.rs +307 -0
  74. uu_link_r-0.4.0/link-r/src/extract/html.rs +442 -0
  75. uu_link_r-0.4.0/link-r/src/extract/markdown.rs +256 -0
  76. uu_link_r-0.4.0/link-r/src/extract/mod.rs +369 -0
  77. uu_link_r-0.4.0/link-r/src/facade.rs +1984 -0
  78. uu_link_r-0.4.0/link-r/src/fetch/http.rs +337 -0
  79. uu_link_r-0.4.0/link-r/src/fetch/mod.rs +75 -0
  80. uu_link_r-0.4.0/link-r/src/index/builder.rs +495 -0
  81. uu_link_r-0.4.0/link-r/src/index/dense.rs +266 -0
  82. uu_link_r-0.4.0/link-r/src/index/format.rs +437 -0
  83. uu_link_r-0.4.0/link-r/src/index/fuse.rs +127 -0
  84. uu_link_r-0.4.0/link-r/src/index/graph.rs +225 -0
  85. uu_link_r-0.4.0/link-r/src/index/meta.rs +389 -0
  86. uu_link_r-0.4.0/link-r/src/index/mod.rs +703 -0
  87. uu_link_r-0.4.0/link-r/src/index/quant.rs +355 -0
  88. uu_link_r-0.4.0/link-r/src/index/sparse.rs +453 -0
  89. uu_link_r-0.4.0/link-r/src/kb.rs +602 -0
  90. uu_link_r-0.4.0/link-r/src/lib.rs +136 -0
  91. uu_link_r-0.4.0/link-r/src/metric.rs +45 -0
  92. uu_link_r-0.4.0/link-r/src/payload.rs +173 -0
  93. uu_link_r-0.4.0/link-r/src/query.rs +151 -0
  94. uu_link_r-0.4.0/link-r/src/remote.rs +378 -0
  95. uu_link_r-0.4.0/link-r/src/resource.rs +365 -0
  96. uu_link_r-0.4.0/link-r/src/source/crawl.rs +1576 -0
  97. uu_link_r-0.4.0/link-r/src/source/fs.rs +123 -0
  98. uu_link_r-0.4.0/link-r/src/source/github.rs +689 -0
  99. uu_link_r-0.4.0/link-r/src/source/mod.rs +41 -0
  100. uu_link_r-0.4.0/link-r/src/text/mod.rs +27 -0
  101. uu_link_r-0.4.0/link-r/src/url_key.rs +234 -0
  102. uu_link_r-0.4.0/link-r/tests/api_v030.rs +143 -0
  103. uu_link_r-0.4.0/link-r/tests/bridge_e2e.rs +409 -0
  104. uu_link_r-0.4.0/link-r/tests/conditional_recrawl.rs +226 -0
  105. uu_link_r-0.4.0/link-r/tests/fixtures/v0.4.0/facade.dump +52 -0
  106. uu_link_r-0.4.0/link-r/tests/fixtures/v0.4.0/facade.lnkr +0 -0
  107. uu_link_r-0.4.0/link-r/tests/github_source.rs +347 -0
  108. uu_link_r-0.4.0/link-r/tests/golden_v040.rs +85 -0
  109. uu_link_r-0.4.0/link-r/tests/hermetic_pipeline.rs +171 -0
  110. uu_link_r-0.4.0/link-r/tests/kb.rs +285 -0
  111. uu_link_r-0.4.0/link-r/tests/kb_gcs.rs +130 -0
  112. uu_link_r-0.4.0/link-r/tests/proptest.rs +143 -0
  113. uu_link_r-0.4.0/link-r/tests/proptest_kb.rs +110 -0
  114. uu_link_r-0.4.0/link-r/tests/regression.rs +191 -0
  115. uu_link_r-0.4.0/link-r/tests/remote.rs +274 -0
  116. uu_link_r-0.4.0/link-r/tests/security.rs +154 -0
  117. uu_link_r-0.4.0/link-r/tests/support/golden.rs +123 -0
  118. uu_link_r-0.4.0/link-r/tests/support/kb_site.rs +164 -0
  119. uu_link_r-0.4.0/link_r.pyi +115 -0
  120. uu_link_r-0.4.0/objstore-r/.gitignore +14 -0
  121. uu_link_r-0.4.0/objstore-r/CHANGELOG.md +64 -0
  122. uu_link_r-0.4.0/objstore-r/Cargo.lock +2005 -0
  123. uu_link_r-0.4.0/objstore-r/Cargo.toml +104 -0
  124. uu_link_r-0.4.0/objstore-r/LICENSE +27 -0
  125. uu_link_r-0.4.0/objstore-r/README.md +137 -0
  126. uu_link_r-0.4.0/objstore-r/benches/mirror_bench.rs +92 -0
  127. uu_link_r-0.4.0/objstore-r/benches/objstore_bench.rs +59 -0
  128. uu_link_r-0.4.0/objstore-r/deny.toml +62 -0
  129. uu_link_r-0.4.0/objstore-r/rust-toolchain.toml +24 -0
  130. uu_link_r-0.4.0/objstore-r/rustfmt.toml +14 -0
  131. uu_link_r-0.4.0/objstore-r/src/error.rs +97 -0
  132. uu_link_r-0.4.0/objstore-r/src/fenced.rs +158 -0
  133. uu_link_r-0.4.0/objstore-r/src/gcs/mod.rs +438 -0
  134. uu_link_r-0.4.0/objstore-r/src/gcs/wire.rs +101 -0
  135. uu_link_r-0.4.0/objstore-r/src/http.rs +258 -0
  136. uu_link_r-0.4.0/objstore-r/src/join.rs +204 -0
  137. uu_link_r-0.4.0/objstore-r/src/key.rs +171 -0
  138. uu_link_r-0.4.0/objstore-r/src/lib.rs +197 -0
  139. uu_link_r-0.4.0/objstore-r/src/local.rs +395 -0
  140. uu_link_r-0.4.0/objstore-r/src/mem.rs +208 -0
  141. uu_link_r-0.4.0/objstore-r/src/mirror/fsio.rs +35 -0
  142. uu_link_r-0.4.0/objstore-r/src/mirror/manifest.rs +178 -0
  143. uu_link_r-0.4.0/objstore-r/src/mirror/mod.rs +733 -0
  144. uu_link_r-0.4.0/objstore-r/src/net.rs +175 -0
  145. uu_link_r-0.4.0/objstore-r/src/sse.rs +170 -0
  146. uu_link_r-0.4.0/objstore-r/src/testing.rs +230 -0
  147. uu_link_r-0.4.0/objstore-r/src/token.rs +379 -0
  148. uu_link_r-0.4.0/objstore-r/src/transport.rs +380 -0
  149. uu_link_r-0.4.0/objstore-r/tests/conformance.rs +51 -0
  150. uu_link_r-0.4.0/objstore-r/tests/mirror.rs +387 -0
  151. uu_link_r-0.4.0/objstore-r/tests/mirror_gcs.rs +146 -0
  152. uu_link_r-0.4.0/objstore-r/tests/proptest_mirror.rs +110 -0
  153. uu_link_r-0.4.0/objstore-r/tests/proptest_model.proptest-regressions +8 -0
  154. uu_link_r-0.4.0/objstore-r/tests/proptest_model.rs +151 -0
  155. uu_link_r-0.4.0/objstore-r/tests/security.rs +242 -0
  156. uu_link_r-0.4.0/objstore-r/tests/support/golden.rs +191 -0
  157. uu_link_r-0.4.0/objstore-r/tests/transport.rs +216 -0
  158. uu_link_r-0.4.0/pyproject.toml +50 -0
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: uu-link-r
3
+ Version: 0.4.0
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: 3
6
+ Classifier: Programming Language :: Python :: 3.12
7
+ Classifier: Programming Language :: Python :: 3.13
8
+ Classifier: Programming Language :: Python :: Implementation :: CPython
9
+ Classifier: Topic :: Text Processing :: Indexing
10
+ Requires-Dist: pytest>=7 ; extra == 'test'
11
+ Provides-Extra: test
12
+ Summary: Tiny embeddable crawl-and-resolve link index: crawl a parent URL, build a hybrid index, resolve queries to ranked links.
13
+ Keywords: search,index,crawler,embeddings,bm25
14
+ Author-email: lituus-io <support@lituus.io>
15
+ License-Expression: AGPL-3.0-or-later OR LicenseRef-Proprietary
16
+ Requires-Python: >=3.12
17
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
18
+
19
+ # link-r
20
+
21
+ `pip install uu-link-r` → `import link_r` (Python ≥ 3.12, one `abi3` wheel per
22
+ platform).
23
+
24
+ A tiny, embeddable crawl-and-resolve link index. Give it a parent URL: it
25
+ crawls sublinks to a configurable depth, distills keywords and topics from
26
+ each page, and builds a compact hybrid index (dense embeddings + BM25 +
27
+ metadata filters). Queries resolve to ranked **remote URLs** — document bodies
28
+ stay at the URL and are fetched lazily by the caller.
29
+
30
+ ```python
31
+ import link_r
32
+
33
+ idx = link_r.LinkIndex.open_or_create("kb.lnkr")
34
+ idx.update("https://docs.example.com/", depth=2)
35
+ idx.save()
36
+
37
+ for hit in idx.search("how does auth work", k=10):
38
+ print(f"{hit.score:.3f} {hit.url}")
39
+
40
+ # Keep the knowledge base fresh: 304s are free, dead links are evicted.
41
+ idx.refresh(ttl_secs=86_400)
42
+ ```
43
+
44
+ - **Zero-copy.** The on-disk index *is* the file: search runs against mapped
45
+ pages with no deserialization step, verified byte-reproducible.
46
+ - **Hybrid retrieval.** Dense embeddings (deterministic hash embedder by
47
+ default; ONNX `bge-small` behind the `onnx` feature) fused with BM25 via
48
+ reciprocal-rank fusion, with categorical prefilters and an optional
49
+ link-graph boost.
50
+ - **Conditional everything.** Stored `ETag`s become `If-None-Match`; an
51
+ unchanged page answers `304` and transfers no body, and previously indexed
52
+ links keep the crawl frontier alive through it.
53
+ - **Credential-safe.** Tokens are host-scoped, stripped on cross-host
54
+ redirects, never rendered in debug output; URL userinfo is never persisted.
55
+ - **Thread-safe.** Blocking calls detach from the interpreter; driving the
56
+ index through `asyncio.to_thread` is the intended pattern. One `abi3` wheel
57
+ per platform (Python ≥ 3.12).
58
+
59
+ Dual-licensed AGPL-3.0-or-later / commercial (contact spicyzhug@gmail.com).
60
+
@@ -0,0 +1,41 @@
1
+ # link-r
2
+
3
+ `pip install uu-link-r` → `import link_r` (Python ≥ 3.12, one `abi3` wheel per
4
+ platform).
5
+
6
+ A tiny, embeddable crawl-and-resolve link index. Give it a parent URL: it
7
+ crawls sublinks to a configurable depth, distills keywords and topics from
8
+ each page, and builds a compact hybrid index (dense embeddings + BM25 +
9
+ metadata filters). Queries resolve to ranked **remote URLs** — document bodies
10
+ stay at the URL and are fetched lazily by the caller.
11
+
12
+ ```python
13
+ import link_r
14
+
15
+ idx = link_r.LinkIndex.open_or_create("kb.lnkr")
16
+ idx.update("https://docs.example.com/", depth=2)
17
+ idx.save()
18
+
19
+ for hit in idx.search("how does auth work", k=10):
20
+ print(f"{hit.score:.3f} {hit.url}")
21
+
22
+ # Keep the knowledge base fresh: 304s are free, dead links are evicted.
23
+ idx.refresh(ttl_secs=86_400)
24
+ ```
25
+
26
+ - **Zero-copy.** The on-disk index *is* the file: search runs against mapped
27
+ pages with no deserialization step, verified byte-reproducible.
28
+ - **Hybrid retrieval.** Dense embeddings (deterministic hash embedder by
29
+ default; ONNX `bge-small` behind the `onnx` feature) fused with BM25 via
30
+ reciprocal-rank fusion, with categorical prefilters and an optional
31
+ link-graph boost.
32
+ - **Conditional everything.** Stored `ETag`s become `If-None-Match`; an
33
+ unchanged page answers `304` and transfers no body, and previously indexed
34
+ links keep the crawl frontier alive through it.
35
+ - **Credential-safe.** Tokens are host-scoped, stripped on cross-host
36
+ redirects, never rendered in debug output; URL userinfo is never persisted.
37
+ - **Thread-safe.** Blocking calls detach from the interpreter; driving the
38
+ index through `asyncio.to_thread` is the intended pattern. One `abi3` wheel
39
+ per platform (Python ≥ 3.12).
40
+
41
+ Dual-licensed AGPL-3.0-or-later / commercial (contact spicyzhug@gmail.com).
@@ -0,0 +1,23 @@
1
+ # Build artifacts
2
+ /target
3
+ **/target/
4
+ Cargo.lock.orig
5
+
6
+ # Fuzzing corpus and crash artifacts (findings are promoted to
7
+ # tests/regression.rs, so the corpus itself is not tracked)
8
+ /fuzz/corpus/
9
+ /fuzz/artifacts/
10
+
11
+ # Criterion output (baselines under benches/baselines/ ARE tracked)
12
+ /target/criterion/
13
+
14
+ # Python build/venv
15
+ *.so
16
+ dist/
17
+ __pycache__/
18
+ .pytest_cache/
19
+ .venv/
20
+
21
+ # OS / editor
22
+ .DS_Store
23
+ *.swp
@@ -0,0 +1,285 @@
1
+ # Changelog
2
+
3
+ All notable changes to graph-r are documented here. Format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning is
5
+ [SemVer](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 0.4.0
8
+
9
+ Breaking: base format v2 and WAL format v2 (stores from 0.3 must be rebuilt
10
+ or re-ingested); several unused APIs removed. Every capability a consumer
11
+ (kb, mirror, the Python binding) uses is kept.
12
+
13
+ **One foundation for the stack**
14
+ - The core (no features: xxh3 alone) is now what link-r, objstore-r and
15
+ crab-r build on: `text` (tokenizer, case folding, char n-grams, stopwords,
16
+ `estimate_tokens` / `bytes_for_tokens`) moved here from link-r and is the
17
+ rule graph-r's lexicon and link-r's BM25 both use; `hash::fmix64` and
18
+ `hash::trigram`; `fsio::{lock, sync_dir, write_atomic_with}`;
19
+ `bytesio::{put_f32, put_len_prefixed}` and `Reader::{f32, len_prefixed}`;
20
+ `mmap::cast_f32`; per-section flags in `container`
21
+ (`Writer::section_flagged`, `Dir::{section_flags, has}`).
22
+ - Published as **uu-graph-r** on crates.io (the library is still
23
+ `graph_r`), one self-contained crate. Its satellites moved to the crates
24
+ they compose with, so the four libraries form no dependency cycle: the
25
+ link-r bridge is `link_r::bridge` (uu-link-r feature `graph`; the `bridge`
26
+ feature here is gone), the knowledge base is `link_r::kb` (feature `kb`),
27
+ and store mirroring is `objstore_r::mirror` (uu-objstore-r feature
28
+ `mirror`), built on the shared `objstore_r::fenced` protocol.
29
+ `graph_r::store::now_ms` is `graph_r::fsio::now_ms`.
30
+
31
+ **Fixed (independent review)**
32
+ - A read-only reader that caught a compaction between its two renames
33
+ reported `Corrupt`; it now re-reads and, if the window persists, returns
34
+ `Busy`. Only a writable open still treats a newer log as damage.
35
+ - A compaction refused in memory (every ring slot pinned) left a durable
36
+ base whose generation number the next compaction reused; readers then kept
37
+ the first base and skipped ops by count. Generations are now never reused,
38
+ and readers diff the log by sequence.
39
+ - An empty log on a writable open (external truncation) made the next
40
+ commit write a headerless file; the header is now restored.
41
+ - `reload_image` had no guard around the single-producer overlay; reloads
42
+ are now one at a time per handle, and both reloads are no-ops on writable
43
+ handles.
44
+ - The bridge never grew the interval of a known, unchanged page re-fetched
45
+ without a validator (no ETag), so it was re-crawled at the base interval
46
+ forever.
47
+
48
+ **Faster, cheaper**
49
+ - `reload_if_stale` reads only the log bytes appended since the last reload.
50
+ - Queries look tokens up through the hash order, a string-order index and a
51
+ trigram index instead of scanning the vocabulary, and score into reused
52
+ dense buffers: 9× faster at 10k documents, 2.2 ms at 100k with every
53
+ document matching. `due` selects instead of sorting everything.
54
+ - The bridge compacts after an ingest only when it added or changed
55
+ documents.
56
+ - Opening validates each byte once (header, directory, section checksums; no
57
+ whole-file trailer) and finds sections in O(1). Mirror no longer validates
58
+ a base twice.
59
+ - Community detection, computed at every compaction and read by nothing,
60
+ is gone; rank records shrink from 8 to 2 bytes.
61
+
62
+ **New**
63
+ - Lineage model: `EdgeType::{Contains, Derives, Conditions}`, the
64
+ `eflags::EXACT` copy flag (`Writer::set_edges_with_flags`), inbound edges
65
+ (`Snapshot::in_edges`, written as a reverse CSR at compaction), node
66
+ attributes for large values (`Writer::set_attrs`, `Snapshot::attr(s)`),
67
+ and one reusable, allocation-free walk (`Snapshot::walk` with `Walk`,
68
+ `Direction::{Out, In}`). Up to 4096 edges per node.
69
+ - A core build (`default-features = false`) of hashing, keys, byte codecs,
70
+ `container` (the checksummed sectioned file framing) and `fsio` (atomic
71
+ writes, the cross-process lock, the clock), with xxhash-rust as the only
72
+ dependency; `mmap` exposes `MappedFile`; `store` (default) is the store.
73
+
74
+ **Removed** (no consumer used them): the `llm` feature and `enrich`, segment
75
+ importance overrides, edge tiers, `community_summary`, `surprises`, `path`
76
+ (use `walk`), `pending_ops`, `Writer::staged`, `Snapshot::check`, the
77
+ `*_with` bridge variants (the plain ones are generic over the embedder), and
78
+ the `libc` and `thiserror` dependencies (std's `File::try_lock` now holds the
79
+ writer lock on every platform, released by the OS on crash).
80
+
81
+ - `hash`: the crate's xxh3 family (`xxh3_64`, `xxh3_64_seeded`, `xxh3_128`,
82
+ streaming `Hasher`) as thin public wrappers, so dependants fingerprint with
83
+ the exact function behind `UrlKey` without a second hashing crate.
84
+
85
+ ## 0.3.0
86
+
87
+ Additive release: every 0.2 API, file format and default behaviour is
88
+ unchanged (`cargo semver-checks` against 0.2.1: no breaking change), and
89
+ stores written by 0.2.1 open and read identically (golden fixtures in
90
+ `tests/fixtures/v0.2.1`, including byte-identical compaction output and WAL
91
+ frames).
92
+
93
+ ### New: `graph-r-mirror` (separate crate in `mirror/`)
94
+
95
+ Keeps a store directory in sync with an object store (Google Cloud Storage,
96
+ a local directory, or memory) through objstore-r. The files are moved byte
97
+ for byte, so a store can live on tmpfs on a stateless instance and be durable
98
+ in a bucket: `pull_into`, incremental `publish` (only new WAL frames; the new
99
+ base after a compaction; coalescing of long segment runs), `refresh_into` for
100
+ read-only replicas, a `HEAD` compare-and-swap that fences a second writer,
101
+ and an optional writer lease. `load_image` / `refresh_image` keep a replica
102
+ entirely in memory; the base and all segments are fetched concurrently. It is a separate crate so graph-r's own
103
+ manifest gains no dependency.
104
+
105
+ ### New: `graph-r-kb` (separate crate in `kb/`)
106
+
107
+ graph-r + link-r as **one knowledge base** on one object store: a writer
108
+ (lease-holding, graph on tmpfs, durability in the bucket) crawls
109
+ conditionally — validators and known links come from the graph, so unchanged
110
+ pages cost a 304 — absorbs through the bridge, and publishes both halves
111
+ concurrently, fenced; `refresh_due` revalidates exactly what the graph's
112
+ adaptive freshness says is due. A replica needs no disk: the graph base and
113
+ the link index are served in place from the fetched bytes, and refreshes cost
114
+ two metadata requests when nothing changed. `lookup` fuses graph-r's
115
+ anchored, graph-aware ranking with link-r's hybrid search (reciprocal-rank
116
+ fusion; the graph work overlaps a remote query embedding). Tested end to end
117
+ on memory, local-directory, fake-gcs-server and a live bucket, with a
118
+ writer-vs-replica property test.
119
+
120
+ ### New API
121
+
122
+ - `Store::open_image(base, wal)` and `Store::reload_image`: read-only stores
123
+ served straight from memory (e.g. objects fetched from a bucket) — the base
124
+ is used in place, fully validated, no file or mapping; `Store::is_image`.
125
+ - `bridge::{absorb_with, ingest_update_with, ingest_refresh_with}`: the
126
+ bridge for link-r indexes built with any embedder.
127
+ - `Store::dir()`, `Store::generation()`.
128
+ - `Store::set_compact_inline(bool)` and `Store::compaction_due()`: move
129
+ compaction off the commit path (inline remains the default).
130
+ - `format::wal::committed_len` and `committed_frames_len`: the intact prefix
131
+ `replay` would report, without collecting ops.
132
+ - `store::{BASE_FILE, WAL_FILE, LOCK_FILE}`.
133
+
134
+ ### Fixes and performance
135
+
136
+ - Loading is one validated code path for files and images; replaying a
137
+ generation no longer clones every op into the overlay twice.
138
+ - `Store::reload_if_stale` is now cheap when nothing changed (two `stat`
139
+ calls; no remap, no read — the 0.2.1 notes promised this, but every call
140
+ re-mapped the base and re-read the whole WAL), and re-reads only the log
141
+ when only the log grew.
142
+ - Temporary files are named by pid **and** a process-wide counter and created
143
+ exclusively, so two stores or threads in one process can never collide on a
144
+ temporary, and a leftover from a crash is never reused.
145
+ - Windows: the writer lock is now an OS file lock (`LockFileEx` via std),
146
+ released by the kernel when the process dies. 0.2 created the lock file
147
+ exclusively and relied on deleting it, so a crashed writer left the store
148
+ locked until someone removed `graph.lock` by hand. (Do not run a 0.2 and a
149
+ 0.3 writer on the same directory concurrently on Windows; unix is
150
+ unaffected — both use `flock`.)
151
+ - WAL framing hashes `seq‖payload` by streaming (identical checksums) instead
152
+ of building a concatenated copy per frame, on both encode and replay.
153
+
154
+ ### Security
155
+
156
+ - Lockfiles (crate and Python binding) move to rustls 0.23.45 for
157
+ RUSTSEC-2026-0285 (TLS 1.3 handshake messages accepted across encryption
158
+ levels); reached through link-r's HTTP fetcher under `bridge`.
159
+
160
+ ### Tooling
161
+
162
+ - New fuzz targets `fuzz_wal_committed_len` and `fuzz_open_image` (and `fuzz_manifest` in
163
+ `mirror/fuzz`).
164
+ - CI: a mirror job (memory, local directory, GCS emulator), a semver job
165
+ (additive-only against the previous tag), the 0.2.1 golden suite in the
166
+ security workflow, and `scripts/ci-local.sh` running every gate locally.
167
+ - `rust-version` is now 1.89 (std file locking); the pinned nightly is
168
+ unchanged.
169
+
170
+ ## 0.2.1
171
+
172
+ ### Python bindings
173
+
174
+ - `Store.reload_if_stale()` is now exposed: read-only handles pick up commits
175
+ and compactions made by a writing process — cheap when nothing changed (a
176
+ header comparison, no remap), returning True when a new generation or fresh
177
+ WAL frames became visible. The Rust API always had it; the binding gap made
178
+ long-lived readers (e.g. an MCP server holding a read-only store while a
179
+ background sync writes) serve stale results until reopen.
180
+
181
+ ## 0.2.0
182
+
183
+ ### GitHub repositories as a first-class source
184
+
185
+ `Store.sync` now routes by source form: a GitHub repository spec
186
+ (`https://github.com/owner/repo[/tree/ref[/dir]]` or
187
+ `github:owner/repo@ref[//dir]`) goes through link-r's new tree-API source —
188
+ one API call lists every file with its blob SHA, files whose SHA the graph
189
+ already stores are revalidated **without any fetch**, and only new/changed
190
+ blobs transfer. An unchanged repository costs exactly one HTTPS request per
191
+ sync, from a cold start included: the SHAs ride the existing `etag` →
192
+ `crawl_seed` → validators plumbing, unchanged.
193
+
194
+ - `depth` means directory levels below the subdir on this path (link hops on
195
+ the crawl path, as before); `max_bytes` skips oversized files from the
196
+ tree's own size field, no fetch issued.
197
+ - `token` (a PAT) reaches only the GitHub API and raw hosts — never a host a
198
+ document links to. Public, private, and internal repositories all work.
199
+ - `api_base`/`raw_base` (set together) target GitHub Enterprise.
200
+ - Crawl-only options passed with a GitHub spec raise `ValueError` rather than
201
+ being silently ignored.
202
+
203
+ ### Cross-platform persistence and memory, tested
204
+
205
+ - The binding CI job now runs the full suite on Linux, macOS, and Windows on
206
+ every push. New `test_persistence.py` pins the file contract per-OS:
207
+ write→close→reopen→write-again cycles, stores under paths with spaces and
208
+ non-ASCII characters, fifty open/close cycles proving the lock releases
209
+ every time, a cross-process write via a child interpreter, and
210
+ `os.replace` stamp atomicity beside the store.
211
+ - New `test_memory.py` pins the harness read path's zero-growth property:
212
+ hundreds of query/due cycles — and a reader running under a live writer —
213
+ hold RSS flat and leave no Python-heap accumulation.
214
+
215
+ ## 0.1.0
216
+
217
+ Initial release: an embedded, persistent knowledge graph that serves as the
218
+ durable backend for [link-r](https://github.com/lituus-io/link-r).
219
+
220
+ ### Storage
221
+
222
+ - Two files. `graph.base` — an immutable, mmapped, checksummed, sectioned
223
+ snapshot (sorted fixed-width 72-byte node records for zero-copy binary search,
224
+ CSR edges, label heap, lexicon with delta-varint postings, persisted ranks).
225
+ `graph.wal` — an append-only op log with per-frame xxh3; torn tails are
226
+ truncated on open.
227
+ - Compaction folds base + WAL into a new **byte-reproducible** base via atomic
228
+ rename. A crash at any point in the sequence recovers: WAL replay skips
229
+ sequences already folded into the base.
230
+ - Every file is validated end to end before a single record is read — magic,
231
+ version, header and per-section checksums, directory bounds and alignment,
232
+ label-heap offsets, CSR monotonicity, edge destinations, lexicon ordering, and
233
+ node sortedness — which is what makes the zero-copy accessors infallible.
234
+
235
+ ### Concurrency
236
+
237
+ - Any number of snapshot readers against a 4-slot generation ring; one writer,
238
+ serialized in-process by a mutex and across processes by `flock`.
239
+ - Snapshots are lifetime-bound borrow guards: no `Arc`, no async runtime, and a
240
+ single audited `unsafe` island (`src/os.rs`: mmap + flock) under a crate-wide
241
+ `#![deny(unsafe_code)]`.
242
+
243
+ ### Freshness and intelligence
244
+
245
+ - Adaptive per-node revalidation interval — grown on an unchanged check, cut
246
+ sharply on change, clamped, and biased by importance — with `due()` work-lists
247
+ carrying stored ETags for conditional revalidation. Tombstones preserve the
248
+ history of sources that have gone away.
249
+ - Deterministic PageRank and hub-muted label-propagation communities with stable
250
+ ids, computed at compaction and persisted.
251
+ - Token-budgeted lookups that answer with URLs and heading anchors, never
252
+ document bodies: IDF-weighted tiered lexical scoring, per-term seed
253
+ guarantees, hub-refusing bounded BFS, and seeds-first rendering.
254
+
255
+ ### Tiers and extension seams
256
+
257
+ - Crawl-tier writes never destroy enrichment-tier edges or scores, and vice
258
+ versa; segments are keyed by heading path so enrichment survives a re-crawl.
259
+ - Features: `default = []` (the pure embedded store), `bridge` (link-r ingest
260
+ and due-list refresh loop), `llm` (a vendor-free `Enricher` trait seam).
261
+
262
+ ### The stateless loop and the Python wheel
263
+
264
+ - `bridge::crawl_seed` extracts the graph's stored validators and edges in
265
+ link-r's vocabulary, so a *fresh* index revalidates a known site with zero
266
+ body transfers — the "absorb and discard the index" design now survives a
267
+ process restart.
268
+ - Change detection in the bridge is graph-relative: a changed page arriving as
269
+ `Added` from an ephemeral index still cuts its refresh interval, because the
270
+ committed content hash — not the crawl's index — decides what changed.
271
+ - Python bindings (`pip install graph-r`, abi3, Python ≥ 3.12): one `Store`
272
+ class carrying the whole loop — `sync` (crawl → absorb → compact in a single
273
+ call, interpreter detached), `query`/`related`/`due`, the `add`/`touch`
274
+ writer seam, `pin`/`remove`/`compact`. Usable from any thread.
275
+
276
+ ### Verification
277
+
278
+ - Unit, hermetic lifecycle, bridge end-to-end, and concurrency-stress suites.
279
+ - A model-based property test checking arbitrary op sequences against a
280
+ reference model through compaction and reopen, plus byte-reproducible
281
+ compaction and torn-WAL-at-any-byte recovery.
282
+ - Four fuzz targets over every decoder: arbitrary bytes must produce a typed
283
+ error, never a panic or an over-allocation.
284
+ - Criterion benchmarks, and zero clippy warnings across all features and
285
+ targets.