engrava 0.3.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 (95) hide show
  1. engrava-0.3.0/CHANGELOG.md +760 -0
  2. engrava-0.3.0/LICENSE +21 -0
  3. engrava-0.3.0/MANIFEST.in +10 -0
  4. engrava-0.3.0/PKG-INFO +287 -0
  5. engrava-0.3.0/README.md +235 -0
  6. engrava-0.3.0/docs/api-reference.md +300 -0
  7. engrava-0.3.0/docs/architecture.md +139 -0
  8. engrava-0.3.0/docs/benchmarks.md +126 -0
  9. engrava-0.3.0/docs/configuration.md +181 -0
  10. engrava-0.3.0/docs/dreaming.md +277 -0
  11. engrava-0.3.0/docs/extension-hooks.md +126 -0
  12. engrava-0.3.0/docs/extensions.md +308 -0
  13. engrava-0.3.0/docs/known-limitations.md +94 -0
  14. engrava-0.3.0/docs/mindql.md +112 -0
  15. engrava-0.3.0/docs/observability.md +58 -0
  16. engrava-0.3.0/docs/quickstart.md +210 -0
  17. engrava-0.3.0/docs/scopes.md +40 -0
  18. engrava-0.3.0/docs/search.md +183 -0
  19. engrava-0.3.0/docs/upgrade.md +98 -0
  20. engrava-0.3.0/pyproject.toml +132 -0
  21. engrava-0.3.0/setup.cfg +4 -0
  22. engrava-0.3.0/src/engrava/__init__.py +213 -0
  23. engrava-0.3.0/src/engrava/benchmarks/__init__.py +6 -0
  24. engrava-0.3.0/src/engrava/benchmarks/longmemeval/README.md +179 -0
  25. engrava-0.3.0/src/engrava/benchmarks/longmemeval/__init__.py +30 -0
  26. engrava-0.3.0/src/engrava/benchmarks/longmemeval/__main__.py +10 -0
  27. engrava-0.3.0/src/engrava/benchmarks/longmemeval/cli.py +178 -0
  28. engrava-0.3.0/src/engrava/benchmarks/longmemeval/dataset_loader.py +282 -0
  29. engrava-0.3.0/src/engrava/benchmarks/longmemeval/evaluate.py +240 -0
  30. engrava-0.3.0/src/engrava/benchmarks/longmemeval/runner.py +512 -0
  31. engrava-0.3.0/src/engrava/benchmarks/synthetic/README.md +38 -0
  32. engrava-0.3.0/src/engrava/benchmarks/synthetic/__init__.py +19 -0
  33. engrava-0.3.0/src/engrava/benchmarks/synthetic/__main__.py +10 -0
  34. engrava-0.3.0/src/engrava/benchmarks/synthetic/datasets/__init__.py +7 -0
  35. engrava-0.3.0/src/engrava/benchmarks/synthetic/datasets/synthetic-v1.json +17021 -0
  36. engrava-0.3.0/src/engrava/benchmarks/synthetic/evaluate.py +759 -0
  37. engrava-0.3.0/src/engrava/benchmarks/synthetic/generate.py +686 -0
  38. engrava-0.3.0/src/engrava/benchmarks/synthetic/runner.py +805 -0
  39. engrava-0.3.0/src/engrava/benchmarks/synthetic/scenarios.py +718 -0
  40. engrava-0.3.0/src/engrava/cli/__init__.py +1 -0
  41. engrava-0.3.0/src/engrava/cli/config.py +68 -0
  42. engrava-0.3.0/src/engrava/cli/main.py +1078 -0
  43. engrava-0.3.0/src/engrava/config.py +1973 -0
  44. engrava-0.3.0/src/engrava/domain/__init__.py +1 -0
  45. engrava-0.3.0/src/engrava/domain/enums.py +287 -0
  46. engrava-0.3.0/src/engrava/domain/exceptions.py +192 -0
  47. engrava-0.3.0/src/engrava/domain/manifest.py +43 -0
  48. engrava-0.3.0/src/engrava/domain/models/__init__.py +31 -0
  49. engrava-0.3.0/src/engrava/domain/models/action.py +91 -0
  50. engrava-0.3.0/src/engrava/domain/models/edge.py +56 -0
  51. engrava-0.3.0/src/engrava/domain/models/embedding.py +81 -0
  52. engrava-0.3.0/src/engrava/domain/models/journal.py +82 -0
  53. engrava-0.3.0/src/engrava/domain/models/metrics.py +63 -0
  54. engrava-0.3.0/src/engrava/domain/models/mutation_type.py +28 -0
  55. engrava-0.3.0/src/engrava/domain/models/search.py +42 -0
  56. engrava-0.3.0/src/engrava/domain/models/thought.py +254 -0
  57. engrava-0.3.0/src/engrava/domain/models/ttl.py +51 -0
  58. engrava-0.3.0/src/engrava/domain/protocols/__init__.py +17 -0
  59. engrava-0.3.0/src/engrava/domain/protocols/embedding_provider.py +71 -0
  60. engrava-0.3.0/src/engrava/domain/protocols/engrava_core.py +366 -0
  61. engrava-0.3.0/src/engrava/domain/protocols/hooks.py +205 -0
  62. engrava-0.3.0/src/engrava/embeddings/__init__.py +4 -0
  63. engrava-0.3.0/src/engrava/embeddings/callback.py +93 -0
  64. engrava-0.3.0/src/engrava/embeddings/huggingface.py +158 -0
  65. engrava-0.3.0/src/engrava/embeddings/ollama.py +166 -0
  66. engrava-0.3.0/src/engrava/embeddings/openai_compatible.py +183 -0
  67. engrava-0.3.0/src/engrava/embeddings/sentence_transformer.py +150 -0
  68. engrava-0.3.0/src/engrava/extensions/__init__.py +6 -0
  69. engrava-0.3.0/src/engrava/extensions/discovery.py +80 -0
  70. engrava-0.3.0/src/engrava/extensions/dreaming.py +1535 -0
  71. engrava-0.3.0/src/engrava/extensions/dreaming_cluster_quality.py +512 -0
  72. engrava-0.3.0/src/engrava/extensions/dreaming_keyphrases.py +664 -0
  73. engrava-0.3.0/src/engrava/extensions/dreaming_reflection_content.py +455 -0
  74. engrava-0.3.0/src/engrava/extensions/dreaming_signals.py +312 -0
  75. engrava-0.3.0/src/engrava/extensions/vector_sqlite_vec.py +223 -0
  76. engrava-0.3.0/src/engrava/infrastructure/__init__.py +1 -0
  77. engrava-0.3.0/src/engrava/infrastructure/read_only_store.py +268 -0
  78. engrava-0.3.0/src/engrava/infrastructure/service_manager.py +325 -0
  79. engrava-0.3.0/src/engrava/infrastructure/sqlite/__init__.py +7 -0
  80. engrava-0.3.0/src/engrava/infrastructure/sqlite/centroid.py +58 -0
  81. engrava-0.3.0/src/engrava/infrastructure/sqlite/engrava_core.py +4008 -0
  82. engrava-0.3.0/src/engrava/infrastructure/sqlite/extension_migrations.py +235 -0
  83. engrava-0.3.0/src/engrava/infrastructure/sqlite/journal_writer.py +308 -0
  84. engrava-0.3.0/src/engrava/infrastructure/sqlite/schema_core.sql +185 -0
  85. engrava-0.3.0/src/engrava/metadata.py +135 -0
  86. engrava-0.3.0/src/engrava/mindql/__init__.py +1 -0
  87. engrava-0.3.0/src/engrava/mindql/executor.py +287 -0
  88. engrava-0.3.0/src/engrava/mindql/parser.py +293 -0
  89. engrava-0.3.0/src/engrava/py.typed +0 -0
  90. engrava-0.3.0/src/engrava.egg-info/PKG-INFO +287 -0
  91. engrava-0.3.0/src/engrava.egg-info/SOURCES.txt +93 -0
  92. engrava-0.3.0/src/engrava.egg-info/dependency_links.txt +1 -0
  93. engrava-0.3.0/src/engrava.egg-info/entry_points.txt +2 -0
  94. engrava-0.3.0/src/engrava.egg-info/requires.txt +32 -0
  95. engrava-0.3.0/src/engrava.egg-info/top_level.txt +1 -0
@@ -0,0 +1,760 @@
1
+ # Changelog
2
+
3
+ All notable changes to engrava will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## 0.3.0 (2026-06-02)
9
+
10
+ * ci: add on-demand smoke-gate workflow (#10) ([50e2bf2](https://github.com/sovantica/engrava/commit/50e2bf2)), closes [#10](https://github.com/sovantica/engrava/issues/10)
11
+ * ci: automated release pipeline ([18c1d68](https://github.com/sovantica/engrava/commit/18c1d68))
12
+ * ci: quote semantic_version input to fix release workflow startup ([2f3c9c3](https://github.com/sovantica/engrava/commit/2f3c9c3))
13
+ * ci: skip branch-name guard for automated dependency PRs (#8) ([b14bb0a](https://github.com/sovantica/engrava/commit/b14bb0a)), closes [#8](https://github.com/sovantica/engrava/issues/8)
14
+ * ci: skip upgrade smoke test when the baseline is not yet published (#9) ([cc05ed7](https://github.com/sovantica/engrava/commit/cc05ed7)), closes [#9](https://github.com/sovantica/engrava/issues/9)
15
+ * ci: use semantic-release CLI directly to satisfy actions allowlist ([0ebf9f1](https://github.com/sovantica/engrava/commit/0ebf9f1))
16
+ * release: merge dev into release/v0.3.0 (automated release pipeline) ([02d3f05](https://github.com/sovantica/engrava/commit/02d3f05))
17
+ * release: v0.3.0 — first public release ([8bd705e](https://github.com/sovantica/engrava/commit/8bd705e))
18
+ * feat: graph memory database — dreaming consolidation, hybrid search, audit trail ([ed82259](https://github.com/sovantica/engrava/commit/ed82259))
19
+ * test: refresh stale FTS-upgrade fixture and public-export baseline (#11) ([46cab01](https://github.com/sovantica/engrava/commit/46cab01)), closes [#11](https://github.com/sovantica/engrava/issues/11)
20
+ * chore(deps): bump actions/checkout from 4 to 6 (#3) ([840ab88](https://github.com/sovantica/engrava/commit/840ab88)), closes [#3](https://github.com/sovantica/engrava/issues/3)
21
+ * chore(deps): bump actions/download-artifact from 4 to 8 (#7) ([c7ca7cf](https://github.com/sovantica/engrava/commit/c7ca7cf)), closes [#7](https://github.com/sovantica/engrava/issues/7)
22
+ * chore(deps): bump actions/setup-node from 4 to 6 (#6) ([99acddc](https://github.com/sovantica/engrava/commit/99acddc)), closes [#6](https://github.com/sovantica/engrava/issues/6)
23
+ * chore(deps): bump actions/upload-artifact from 4 to 7 (#5) ([7ad3f2f](https://github.com/sovantica/engrava/commit/7ad3f2f)), closes [#5](https://github.com/sovantica/engrava/issues/5)
24
+ * chore(deps): bump softprops/action-gh-release from 2 to 3 (#4) ([9eb3729](https://github.com/sovantica/engrava/commit/9eb3729)), closes [softprops/action-#release](https://github.com/softprops/action-/issues/release) [#4](https://github.com/sovantica/engrava/issues/4)
25
+ * Bump idna from 3.13 to 3.15 (#1) ([f402fd2](https://github.com/sovantica/engrava/commit/f402fd2)), closes [#1](https://github.com/sovantica/engrava/issues/1)
26
+ * docs: align metadata and docs with product tagline; add dependabot and issue config (#2) ([b3bb62d](https://github.com/sovantica/engrava/commit/b3bb62d)), closes [#2](https://github.com/sovantica/engrava/issues/2)
27
+
28
+ # Changelog
29
+
30
+ All notable changes to engrava will be documented in this file.
31
+
32
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
33
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
34
+
35
+ ## [0.3.0]
36
+
37
+ ### Documentation
38
+
39
+ - **docs:** align package description and documentation with the product
40
+ tagline ("The memory database for AI agents") and the public contact
41
+ address.
42
+
43
+ ### Maintenance
44
+
45
+ - **Pre-publish smoke gate.** The publish workflow now runs a hard-fail
46
+ quality gate before any wheel or sdist is built: the bundled
47
+ synthetic benchmark is driven in its binding acceptance-criterion
48
+ mode and the committed floors (synthesis coverage, direct-query
49
+ neutrality, sanity-scenario neutrality, sanity with reflection-boost)
50
+ are enforced from source. A breach blocks publish. A standalone
51
+ ``scripts/check_smoke_gate.py`` exposes the same gate for local runs
52
+ and is wired into ``make smoke-gate``. An optional LongMemEval
53
+ recall@5 probe enforces a calibrated absolute floor when invoked via
54
+ ``--include-longmemeval``; the probe stays off in CI by default
55
+ because the LongMemEval dataset is user-download and the run takes
56
+ several minutes, but maintainers invoke it manually before tagging a
57
+ release.
58
+
59
+ ### Added
60
+
61
+ - **Bundled walkthrough example + self-anchored metadata helpers.** A
62
+ single-file script a Free-tier user can run directly from the repo
63
+ to see the engine work in under a minute:
64
+
65
+ * `examples/quickstart.py` — boots an in-memory store with a local
66
+ embedding encoder, ingests a handful of percepts and utterances
67
+ built via the new `engrava.metadata` helpers, runs one dreaming
68
+ consolidation cycle, and queries via hybrid search.
69
+
70
+ The script pre-flights `sentence_transformers` with a clean
71
+ actionable message and `sys.exit(2)` when the `[embeddings-local]`
72
+ extra is missing.
73
+
74
+ Adds three pure-function helpers in `engrava.metadata`, re-exported
75
+ from the top-level package: `percept(...)` for input arriving at
76
+ the agent, `utterance(...)` for the agent's own outgoing content,
77
+ and `thought(...)` for the agent's internal cognition. They build
78
+ the structured `metadata` dictionary the persistence layer
79
+ recognises, anchoring every stored thought to a perspective and a
80
+ source. Same arguments always return an equal dictionary.
81
+
82
+ `docs/quickstart.md` gains a "Run the bundled walkthrough" section
83
+ with the dreaming and self-anchored-identity narrative; new
84
+ `examples/README.md` indexes the scripts.
85
+
86
+ - **LongMemEval public benchmark harness** — engrava-side runner for
87
+ the published LongMemEval memory-evaluation dataset (Wu et al., ICLR
88
+ 2025, arXiv:2410.10813).
89
+
90
+ * `python -m engrava.benchmarks.longmemeval` ingests each
91
+ question's haystack into a fresh engrava store, optionally runs
92
+ one dreaming consolidation cycle, queries via `search_hybrid`,
93
+ and scores the retrieved chunks.
94
+ * Three evaluation modes: deterministic substring containment
95
+ (default), deterministic cosine-similarity over a configurable
96
+ threshold, and an opt-in LLM judge wired via a thin
97
+ `LLMJudgeClient` protocol so callers can plug in any provider.
98
+ * The dataset is **not bundled** — the loader downloads the
99
+ requested variant from the upstream HuggingFace distribution on
100
+ first use and caches it under `~/.engrava/benchmarks/longmemeval/`.
101
+ * Self-anchored thought metadata follows the same shape as the
102
+ synthetic benchmark (`perspective`, `source.is_self`,
103
+ `session_id`, `turn_index`), so dreaming filters see the same
104
+ inputs they do in production.
105
+ * See `src/engrava/benchmarks/longmemeval/README.md` for
106
+ attribution, license, and download instructions.
107
+
108
+ - **Synthetic benchmark suite** — reproducible dreaming evidence
109
+ runnable on any laptop without API keys or network access.
110
+
111
+ * `python -m engrava.benchmarks.synthetic` runs binding acceptance
112
+ measurements (~5 minutes): synthesis coverage, direct retrieval
113
+ neutrality, sanity tolerance.
114
+ * `python -m engrava.benchmarks.synthetic --with-reproducibility`
115
+ adds full per-scenario texture from the bundled
116
+ `synthetic-v1.json` dataset (~10 minutes total).
117
+ * Measures three properties: synthesis coverage (dreaming produces
118
+ REFLECTIONs that consolidate related facts), direct retrieval
119
+ neutrality (dreaming does not degrade baseline competence), and
120
+ sanity tolerance (small, non-pathological influence on neutral
121
+ queries).
122
+ * See `docs/benchmarks.md` for the interpretation guide and the
123
+ v0.4.0 roadmap (tighter neutrality ceilings + recall-lift
124
+ evidence).
125
+
126
+ - **Cluster quality gates for the dreaming consolidation loop.** Seven
127
+ deterministic gates run on every candidate cluster before a
128
+ REFLECTION is materialised, dropping clusters that would produce
129
+ low-signal or actively misleading memories. Most gates are
130
+ language-agnostic; the contradiction gate (Gate 3) ships with an
131
+ English-only sentiment-token lexicon and silently passes clusters
132
+ in other languages. Gates fire in two phases:
133
+
134
+ *Pre-build (cheap rejections before content assembly):*
135
+
136
+ * **Gate 1 — duplicate content members** rejects clusters that
137
+ contain byte-identical member content within the same cluster
138
+ (a dedup escape that would inflate one statement into a
139
+ pseudo-cluster).
140
+ * **Gate 2 — persona-only** rejects clusters where the share of
141
+ members carrying a persona/identity marker (and no conversation
142
+ marker) exceeds the configured threshold.
143
+ * **Gate 3 — contradictory** rejects clusters with member pairs
144
+ asserting documented opposite predicates (English lexicon).
145
+ * **Gate 4 — low cohesion** rejects clusters whose mean pairwise
146
+ cosine similarity (over L2-normalised embeddings) falls below
147
+ the configured threshold.
148
+ * **Gate 5 — external-source homogeneity** rejects clusters whose
149
+ share of members with `metadata["source"]["is_self"] != True`
150
+ falls below the configured minimum-external fraction; missing
151
+ or malformed `source` is treated as external under safe
152
+ fallback. Belt-and-suspenders over the upstream eligibility
153
+ filter.
154
+ * **Gate 6 — named-entity consistency** rejects clusters where the
155
+ fraction of members whose per-member named-entity set
156
+ intersects the first member's set falls below the configured
157
+ threshold; single-member and empty-NE-on-anchor clusters pass
158
+ vacuously.
159
+
160
+ *Post-build (after `build_reflection_content_v2`):*
161
+
162
+ * **Gate 8 — meaningful keyphrases** rejects REFLECTIONs whose final
163
+ `top_keyphrases` list is empty or composed solely of generic
164
+ filler bigrams.
165
+
166
+ All gates are pure functions in
167
+ `engrava.extensions.dreaming_cluster_quality`. They are wired into
168
+ the consolidation loop with per-gate rejection counters surfaced via
169
+ a single `INFO` summary log per pass.
170
+
171
+ Six new `DreamingGates` fields control the behaviour, all validated
172
+ in `__post_init__` so direct construction cannot bypass the
173
+ `[0.0, 1.0]` contract:
174
+
175
+ * `cluster_quality_gating_enabled: bool = True` — master switch.
176
+ * `cluster_quality_persona_threshold: float = 0.75` — share of
177
+ persona-marked members above which a cluster is persona-only.
178
+ * `cluster_quality_cohesion_threshold: float = 0.40` — minimum mean
179
+ pairwise cosine for the cluster to be considered coherent.
180
+ * `cluster_quality_external_homogeneity_threshold: float = 0.95` —
181
+ minimum fraction of external-source members for the gate to
182
+ pass (under safe fallback, missing/malformed `metadata.source`
183
+ counts as external).
184
+ * `cluster_quality_ne_consistency_threshold: float = 0.60` —
185
+ minimum fraction of members whose named-entity set intersects
186
+ the anchor (first) member's set.
187
+ * `cluster_quality_require_meaningful_keyphrases: bool = True` —
188
+ reject post-build REFLECTIONs without informative keyphrases.
189
+
190
+ Gating is on by default; existing fixtures that intentionally pass
191
+ synthetic, low-signal clusters opt out via
192
+ `cluster_quality_gating_enabled=False`. Legacy thought metadata
193
+ without `source` keys is treated as external under safe fallback,
194
+ preserving backward compatibility.
195
+
196
+ - **Statistical cross-cluster boilerplate filter for REFLECTION
197
+ keyphrases.** Two language-agnostic helpers ship in
198
+ `engrava.extensions.dreaming_keyphrases` —
199
+ `compute_cluster_phrase_frequency` (cluster-count map over the
200
+ per-cluster top-N keyphrase lists) and `is_boilerplate_phrase`
201
+ (case-insensitive threshold check with a small-corpus bypass).
202
+ `build_reflection_content_v2` accepts two new optional kwargs,
203
+ `cluster_phrase_df` and `total_clusters`, and uses them to drop
204
+ phrases that exceed the configured share of clusters; both kwargs
205
+ default to `None`, so existing callers see no behaviour change.
206
+ The dreaming consolidation loop now runs a lightweight pre-pass
207
+ that builds the document-frequency map ahead of REFLECTION
208
+ creation and forwards the kwargs into the content builder.
209
+
210
+ Three new `DreamingConfig` knobs control the filter:
211
+
212
+ * `boilerplate_threshold: float = 0.30` — phrases appearing in
213
+ more than 30 % of clusters are dropped. Set to `1.0` to
214
+ disable the filter.
215
+ * `boilerplate_min_corpus_size: int = 5` — minimum cluster count
216
+ before the filter engages; smaller runs preserve every
217
+ keyphrase regardless of frequency.
218
+ * `boilerplate_min_keyphrases_per_refl: int = 1` — fallback
219
+ guard. If filtering would shrink a REFLECTION's keyphrase
220
+ list below this size the raw list is kept, so REFLECTIONs
221
+ never end up with an empty `top_keyphrases` field.
222
+
223
+ Because the filter is statistical and operates on lowercased
224
+ phrases, it learns "boilerplate" from the live deployment without
225
+ any hardcoded blocklist and works equally well across English,
226
+ Polish, Japanese, French, German and every other language that
227
+ the TF-IDF tokeniser already supports.
228
+
229
+ - **Metadata-aware dreaming filter** on `DreamingConfig`. Five new
230
+ fields gate which thoughts the dreaming pipeline considers eligible
231
+ for promotion and REFLECTION clustering:
232
+
233
+ * `eligible_perspectives: frozenset[Literal["percept", "utterance",
234
+ "thought"]] | None` — positive filter on
235
+ `metadata["perspective"]`.
236
+ * `self_filter_mode: Literal["any", "self_only", "external_only"]`
237
+ — filter on `metadata["source"]["is_self"]`.
238
+ * `min_source_confidence: Literal["high", "medium", "low"]` —
239
+ minimum required `metadata["source"]["confidence"]`, ranked
240
+ `low < medium < high`.
241
+ * `excluded_content_types: frozenset[str]` — negative filter on
242
+ `metadata["content_type"]`; defaults to `frozenset({"code"})`
243
+ because code fragments cluster poorly under cosine similarity.
244
+ * `eligible_content_types: frozenset[str] | None` — optional
245
+ positive filter on `metadata["content_type"]`.
246
+
247
+ The filter is applied during promotion (filtered candidates skip the
248
+ P1-promotion decision) and during REFLECTION creation (only eligible
249
+ cluster members feed the cluster hash, the centroid embedding, the
250
+ structured content payload and the `CONSOLIDATED_FROM` lineage edges
251
+ — a cluster whose eligible subset falls below
252
+ `DreamingGates.min_cluster_size` is dropped entirely). Defaults
253
+ preserve the pre-existing dreaming behaviour: thoughts without any
254
+ structured metadata pass unconditionally, and a freshly-constructed
255
+ `DreamingConfig()` leaves every axis disabled. Promotion-side
256
+ rejections are reported at `DEBUG` level (`dreaming filter: N/M
257
+ candidates rejected by metadata filter (cycle X)`).
258
+
259
+ - **`ThoughtRecord.metadata` accepts nested dict values** for structured
260
+ namespaces. The `MetadataValue` type alias now resolves recursively to
261
+ `str | int | float | bool | None | dict[str, MetadataValue]`, so callers
262
+ can express grouped attributes such as
263
+ `metadata["source"] = {"is_self": True, "confidence": "high", ...}`
264
+ directly instead of flattening to dot-prefixed keys. Leaf values are
265
+ still restricted to JSON scalars; lists, tuples, sets and custom
266
+ containers remain rejected at every depth. The persistence-layer
267
+ validator walks nested dicts recursively and produces dotted key-path
268
+ error messages (e.g. `metadata value at source.tags type list not
269
+ allowed`). Backward-compatible: flat-only callers see no behaviour
270
+ change; the SQLite `metadata_json TEXT` column already stored the
271
+ serialised JSON faithfully, so no schema migration is required.
272
+
273
+ ### Fixed
274
+
275
+ - **REFLECTION freshness (lifecycle-aware consolidation).** A REFLECTION is
276
+ a synthesis of a live cluster of thoughts, but until now it was frozen at
277
+ creation and never re-bound to the current state of that cluster. Three
278
+ related freshness gaps are closed so dreaming improves recall over a
279
+ long-running agent's lifetime instead of slowly polluting it:
280
+
281
+ * **Orphan retire.** The consolidation pass now sweeps existing
282
+ REFLECTIONs and retires (ACTIVE → ARCHIVED) any whose every consolidated
283
+ source thought has left the active set, so an ordinary `gc` reclaims it
284
+ (cascading its centroid embedding and consolidation edges). The sweep
285
+ fires only when *all* sources are gone and the REFLECTION has at least
286
+ one source — a partially-archived cluster keeps its synthesis, and a
287
+ source-less REFLECTION is never retired.
288
+
289
+ * **Centroid re-bind on evolve.** When a source thought's essence or
290
+ content changes and the thought is re-embedded, every REFLECTION that
291
+ was consolidated from it has its centroid recomputed from the current
292
+ member vectors (the same deterministic L2-normalized mean used at
293
+ creation, overwritten in place — no schema change). Metadata- or
294
+ priority-only edits do not re-embed the source and therefore leave
295
+ dependent REFLECTION centroids untouched.
296
+
297
+ * **Recall freshness floor.** Similarity, hybrid, and reflection-only
298
+ search no longer surface a retired REFLECTION in the window between
299
+ archival and physical collection, so a stale synthesis can no longer
300
+ out-rank fresh relevant thoughts. The floor is a lifecycle check at the
301
+ data layer; existing reflection ranking knobs are unchanged.
302
+
303
+ Every mechanism is deterministic (vector mean / cosine / SQL); no model is
304
+ invoked. Pre-publish recall-quality fix.
305
+
306
+ - **Referential integrity (cascade delete + edge FK).** The schema now
307
+ declares foreign keys with `ON DELETE CASCADE` for the three child
308
+ tables that reference `thought` (edges on both endpoints, embeddings,
309
+ and action records). Deleting a thought now actually removes the
310
+ related rows instead of leaving them behind as orphans, and inserting
311
+ an edge to a non-existent thought is rejected with a typed domain
312
+ exception (`ReferentialIntegrityError`) instead of the raw SQLite
313
+ integrity error. The change ships as schema version 12; existing
314
+ databases migrate in place via a recreate-table step that purges
315
+ any pre-existing orphan rows before enabling the constraints. The
316
+ migration is idempotent and recovers cleanly when a previous pass
317
+ was interrupted between tables. The connection-level pragma that
318
+ enables FK enforcement is now issued automatically from
319
+ `ensure_schema`, so callers that construct the store directly (not
320
+ via `from_config`) still get enforcement.
321
+
322
+ - **PyPI wheel and sdist now include `schema_core.sql`.**
323
+ `SqliteEngravaCore.ensure_schema` loads the core SQL schema via
324
+ `importlib.resources`, but `pyproject.toml` had no
325
+ `[tool.setuptools.package-data]` entry and there was no
326
+ `MANIFEST.in`, so neither the wheel nor the sdist actually bundled
327
+ the file. A fresh `pip install engrava` therefore crashed on the
328
+ first `EngravaCore` initialisation with `FileNotFoundError`. The
329
+ fix adds an explicit narrow package-data pattern
330
+ (`engrava = ["infrastructure/sqlite/*.sql"]`) plus a minimal
331
+ `MANIFEST.in` for the sdist side, so installed distributions now
332
+ contain the schema file and `EngravaCore` initialises cleanly out
333
+ of the box. Critical pre-publish fix.
334
+
335
+ ### Maintenance
336
+
337
+ - **Build artifacts removed from tracked state.** The generated
338
+ `coverage.json` is no longer checked into the repository — it is
339
+ regenerated on every CI run. `.gitignore` gains `.coverage.*`,
340
+ `coverage.json`, `coverage.xml`, and a defensive bare-`.env` /
341
+ `.env.*` pair (with a `!.env.example` exception for template
342
+ files), so future coverage and environment files cannot
343
+ accidentally re-enter the public surface. No behavioural change —
344
+ repository hygiene only.
345
+
346
+ - **Packaging guard script + broader package-data coverage.** Adds
347
+ `scripts/verify_wheel_data.py`, a standalone build-and-inspect step
348
+ that rebuilds wheel + sdist and asserts the critical data files
349
+ (`schema_core.sql`, `synthetic-v1.json`) are bundled in both
350
+ artifacts; non-zero exit on missing files. `MANIFEST.in` adds
351
+ `recursive-include src/engrava *.json` so sdist coverage tracks the
352
+ wheel's `[tool.setuptools.package-data]` entry. Closes the
353
+ fresh-install regression class previously fixed only for
354
+ `schema_core.sql` (see Fixed); cumulative outcome with prior
355
+ contributor-instructions and benchmark refreshes.
356
+
357
+ - **Contributor instructions refreshed.** Per-repo agent guidance files
358
+ (`CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `.github/copilot-instructions.md`,
359
+ `BRANCHING.md`) regenerated from canonical templates. Generic OSS-friendly
360
+ guidance on Conventional Commits, branch naming, code quality bar, and
361
+ contribution flow. Internal workflow references removed from the public
362
+ surface.
363
+
364
+ - **Repository metadata aligned with organization branding.** The
365
+ `LICENSE` copyright holder, the `pyproject.toml` `[project] authors`
366
+ field, and the `[project.urls]` GitHub references now reflect the
367
+ Sovantica organization identity instead of an individual maintainer.
368
+ A `hello@sovantica.ai` contact address replaces the previous
369
+ unauthored entry, and the `Documentation` URL points at
370
+ `https://docs.engrava.ai` so PyPI listings link to the product
371
+ documentation rather than the in-repo `docs/` tree. No behavioural
372
+ changes — packaging metadata only.
373
+
374
+ ### Behavior Changes
375
+
376
+ - **`ThoughtRecord.metadata` field for caller-supplied structured attributes.**
377
+ Each thought now carries an extensible `dict[str, MetadataValue]`
378
+ (`MetadataValue = str | int | float | bool | None`) for flat scalar
379
+ attributes such as conversation role, source language, content type,
380
+ external session identifier, turn index or speaker name. The field
381
+ defaults to an empty dict, so existing callers compile and run
382
+ unchanged — no code change is required to upgrade.
383
+
384
+ Persistence: a new `metadata_json` column (`TEXT NOT NULL DEFAULT '{}'`)
385
+ is added to the `thought` table; the core schema bumps from
386
+ `user_version = 10` to `11` and `_migrate_core_v10_to_v11` performs
387
+ the additive migration with duplicate-column tolerance for safe
388
+ re-runs. Pre-existing rows receive the empty-dict default
389
+ automatically. JSON serialization uses `ensure_ascii=False` so
390
+ non-ASCII attribute values round-trip byte-exact.
391
+
392
+ Validation: caller-supplied metadata is checked at both API entries
393
+ (`create_thought` and `update_thought`). Nested or list values raise
394
+ `ValueError` with a per-key message. Serialized payloads above
395
+ ~4 KiB emit a `WARNING` log; payloads above ~64 KiB are rejected
396
+ outright with `ValueError`.
397
+
398
+ No new public API surface beyond the field itself — downstream
399
+ filtering / dispatching consumers are tracked separately.
400
+
401
+ - **`DreamingConfig.max_p1_fraction` defaults to 5 % (was effectively unlimited).**
402
+ Dreaming consolidation now caps the fraction of corpus thoughts at priority P1
403
+ to `max_p1_fraction` (default `0.05`). Once the cap is reached, further
404
+ promotions are silently skipped for that run and `ConsolidationResult.promotion_capped`
405
+ is set to `True`. Motivation: empirical analysis found that
406
+ unrestricted promotion accumulated 29.9 % P1 thoughts, giving those entries a
407
+ systematic 67 % ranking boost in hybrid-search fusion.
408
+
409
+ **Upgrade impact:** existing databases with >5 % P1 will stop receiving new
410
+ P1 promotions until the fraction drops below the cap (via normal thought
411
+ expiry / lifecycle transitions). Run `python -m scripts.rebalance_p1
412
+ --db-path <PATH>` to immediately demote excess P1 thoughts to P2.
413
+ Set `extensions.dreaming.max_p1_fraction: 1.0` in YAML to restore legacy
414
+ unlimited behaviour.
415
+
416
+ - **`DreamingConfig.promote_targets` defaults to `"OBS_ONLY"`.** Only
417
+ `OBSERVATION` thoughts are now eligible for P1 promotion by default.
418
+ `REFLECTION` thoughts can be included by setting
419
+ `extensions.dreaming.promote_targets: ALL` or `REFL_ONLY`.
420
+
421
+ - **`DreamingConfig.reflection_default_priority` defaults to `"P2"`.**
422
+ Newly-created `REFLECTION` thoughts now start at P2 instead of inheriting
423
+ the highest priority of their cluster members (which was effectively P1 in
424
+ most stores). Configure via `extensions.dreaming.reflection_default_priority`.
425
+
426
+ - **`ConsolidationResult` gains two new fields** (zero-impact on existing
427
+ consumers — both default to `False` / `0.0`):
428
+ - `promotion_capped: bool` — True when the P1 fraction cap stopped promotion.
429
+ - `p1_fraction_after: float` — fraction of total corpus at P1 after the run.
430
+
431
+ - **`SqliteEngravaCore.count_thoughts()` gains a `priority` keyword filter.**
432
+ Allows callers to count thoughts at a specific priority level without
433
+ fetching full records (e.g. `await store.count_thoughts(priority="P1")`).
434
+
435
+ - **New utility `python -m scripts.rebalance_p1`** — demotes excess P1 thoughts
436
+ to P2 to meet the `max_p1_fraction` cap on existing databases. Idempotent,
437
+ supports `--dry-run`, and `--max-p1-fraction` to override the target fraction.
438
+
439
+ - **Extended `SENTENCE_STARTER_BLOCKLIST` with 11 empirical entries** from a
440
+ short07 NE top-15 audit (2026-05-04). 11 of the 15 most frequent
441
+ `named_entities` tokens in 47 REFLECTION thoughts were sentence-starter words
442
+ absent from the prior blocklist: `Also`, `Did`, `Embracing`, `For`,
443
+ `Have`, `How`, `Instead`, `Lastly`, `Not`, `Reflecting`, `Ultimately`. An
444
+ additional 15 common gerund/participle starters (`Conducting`, `Connecting`,
445
+ `Continuing`, …) are added as preventive coverage. Additive-only — no
446
+ existing entry removed; real proper nouns (`Alex`, `Cornell`, `1974`, …)
447
+ remain unblocked.
448
+
449
+ - **Structural REFLECTION content schema v2.** The dreaming extension
450
+ now emits a richer structural JSON when it creates a REFLECTION
451
+ thought from a cluster. The legacy three-field layout
452
+ (`member_ids`, `keywords`, `cluster_hash`) is preserved verbatim
453
+ for backward compatibility, and the dict gains nine additional
454
+ fields:
455
+
456
+ - `type` / `version` — schema-dispatch markers
457
+ (`"reflection"` / `2`). Legacy v1 emissions never carried these
458
+ keys; consumers can detect a legacy row by their absence.
459
+ - `member_count` / `cluster_algorithm` / `created_at` — fields
460
+ mandated by the cognitive-boundary REFLECTION spec but missing
461
+ from the previous emitter.
462
+ - `top_keyphrases` — TF-IDF-scored 2-3 word n-grams over the
463
+ cluster, with the corpus baseline supplied by the caller.
464
+ - `member_excerpts` — top-N members by priority + recency, each
465
+ truncated at the word boundary to ~80 characters.
466
+ - `temporal_span` — `min_created_at` / `max_created_at` plus the
467
+ span in days across the cluster.
468
+ - `named_entities` — regex-based capitalised tokens plus year /
469
+ measurement matches.
470
+
471
+ All enrichment is deterministic and LLM-free; the cognitive-
472
+ boundary CI guard test covers the new sibling modules
473
+ (`engrava.extensions.dreaming_keyphrases`,
474
+ `engrava.extensions.dreaming_reflection_content`) so an LLM SDK
475
+ cannot sneak in via either of them.
476
+
477
+ Two new `DreamingConfig` fields tune the structural output:
478
+ `top_keyphrases_count` (default 3) and `top_member_excerpts_count`
479
+ (default 5). Both default to safe values, are additive on the
480
+ dataclass, and can be overridden via the standard
481
+ `extensions.dreaming` YAML section.
482
+
483
+ Existing REFLECTION rows in production databases continue to read
484
+ correctly — the dispatch parser detects the legacy schema by the
485
+ absence of the `version` field. A new opt-in utility
486
+ `python -m scripts.reenrich_reflections_to_v2 --db-path PATH`
487
+ walks the database in batches and rewrites legacy v1 content to
488
+ v2 in place; idempotent (re-running on an already-migrated DB is
489
+ a no-op) with a `--dry-run` mode for previewing.
490
+
491
+ Empirical motivation: AMB PersonaMem MCQ judges score parsable
492
+ prose-like surface higher than terse JSON; the structural
493
+ enrichment closes most of that gap without crossing the no-LLM
494
+ cognitive boundary.
495
+
496
+ - **Structural REFLECTION content quality amendment.** The v2
497
+ builder gains three deterministic quality fixes layered on top of
498
+ the existing schema (no key changes — only field VALUES improve):
499
+
500
+ - **Sentence-starter blocklist for `named_entities`.** Common
501
+ capitalised non-entity words (`Absolutely`, `However`,
502
+ `Therefore`, `Furthermore`, `User`, `Assistant`, `System`, …)
503
+ no longer pollute the entity list. The blocklist is exposed
504
+ publicly as `SENTENCE_STARTER_BLOCKLIST` from
505
+ `engrava.extensions.dreaming_keyphrases` for downstream
506
+ consumers that want the same filter. Real proper nouns
507
+ (`Cornell`, `Berlin`, `Anthropic`, …) are unaffected.
508
+ - **Role-marker stripping before keyphrase extraction.**
509
+ `[USER] User: …` / `[ASSISTANT] Assistant: …` / `[SYSTEM] …`
510
+ prefixes are stripped before tokenisation, so artefact tokens
511
+ like `"user user"` or `"assistant"` no longer surface as
512
+ top-ranked keyphrases or simple keywords.
513
+ - **Member excerpt size raised 80 → 150 characters** and made
514
+ configurable via the new `DreamingConfig.member_excerpt_max_chars`
515
+ field (positive integer; YAML key
516
+ `extensions.dreaming.member_excerpt_max_chars`). The bump
517
+ gives downstream LLM judges a meaningfully longer window into
518
+ each member while keeping the cluster content well within the
519
+ 2 KB structural budget.
520
+
521
+ All three changes are deterministic, LLM-free, and additive — the
522
+ v2 dispatch parser, the 12 v2 schema keys, and existing v1
523
+ consumers are unchanged.
524
+
525
+ - **Opt-in content-hash deduplication on `create_thought`.** The
526
+ persistence layer now exposes a new keyword-only argument
527
+ `deduplicate: bool = False` on
528
+ `SqliteEngravaCore.create_thought`. When `True`, identical
529
+ `content` (SHA-256 hash collision over the UTF-8 bytes, no
530
+ normalization) collapses into a single thought whose
531
+ `confirmation_count` is incremented and `updated_at` refreshed,
532
+ instead of producing a duplicate row. Default behavior is
533
+ unchanged (`deduplicate=False` preserves the legacy create-on-every-
534
+ call semantic).
535
+
536
+ Configuration: a new top-level `IngestConfig` value object exposes
537
+ `deduplication_enabled: bool = True`, accessible via
538
+ `EngravaConfig.ingest`. YAML callers can flip it via
539
+ ```yaml
540
+ ingest:
541
+ deduplication_enabled: false
542
+ ```
543
+ Ingest-pipeline callers (e.g. benchmark adapters, bulk-import
544
+ tooling) should read `config.ingest.deduplication_enabled` and pass
545
+ it through to `create_thought(..., deduplicate=...)`.
546
+
547
+ Schema: the `thought` table gains a nullable `content_hash TEXT`
548
+ column and a supporting `idx_thought_content_hash` index.
549
+ `PRAGMA user_version` is bumped from 9 to 10. A new
550
+ `_migrate_core_v9_to_v10` helper participates in the existing
551
+ `ensure_schema` upgrade cascade, so DBs at any prior supported
552
+ version (v3 through v9) upgrade in a single call. The migration
553
+ is idempotent (ALTER TABLE tolerates duplicate-column errors,
554
+ CREATE INDEX uses `IF NOT EXISTS`).
555
+
556
+ Backfill: pre-v10 thoughts retain `content_hash IS NULL` until the
557
+ bundled `scripts/backfill_content_hashes.py` utility populates them
558
+ in batches; running benchmarks against a freshly-bootstrapped DB
559
+ (e.g. AMB PersonaMem fixtures) is unaffected because the column is
560
+ filled on every insert.
561
+
562
+ Empirical motivation: AMB PersonaMem benchmark runs (with full
563
+ ingest tracing, two independent runs in DREAM and NODREAM modes)
564
+ reproduced ~38.5% duplicate observation thoughts, with persona
565
+ intros multiplied 12-13x per session; clustering and
566
+ reflection-of-duplicates pollution amplified the waste downstream.
567
+
568
+ ### Breaking Changes
569
+
570
+ - **Removed observability hook surface from the public package.** The
571
+ `EngravaObservabilityHooksProtocol`, `ObservabilityDispatcher`,
572
+ `DefaultObservabilityHooks`, `ObservabilityGates`, all 12 event
573
+ dataclasses (`QueryStartEvent`, `QueryEndEvent`, `CandidatesFusedEvent`,
574
+ `CandidateRecord`, `IngestStartEvent`, `IngestEndEvent`,
575
+ `ThoughtCreatedEvent`, `EmbeddingComputedEvent`, `EdgeCreatedEvent`,
576
+ `CycleStartEvent`, `CycleEndEvent`, `ClusterDecisionEvent`,
577
+ `ReflectionCreatedEvent`), the `register_observability_hook()` method
578
+ on the store, and the `obs_dispatcher` / `obs_gates` constructor
579
+ parameters are gone. The `observability:` section in the YAML config
580
+ is no longer parsed.
581
+
582
+ - **`benchmarks/` directory removed** — benchmark runners and their
583
+ adapters / checkpointing / LLM-judge helpers are no longer included
584
+ in the open-source distribution. A public reproducibility benchmark
585
+ remains available via `python -m engrava.benchmarks.synthetic`
586
+ (see `docs/benchmarks.md`).
587
+
588
+ ### Database Changes
589
+
590
+ - Upgrade-path validation is now part of release preparation for minor bumps.
591
+ - Automatic schema migration still runs on first connection via `ensure_schema()`.
592
+ - Releases that change schema behavior must document the change here explicitly.
593
+
594
+ ### Changed
595
+
596
+ - **Search: `default_graph_weight` reverted to `0.0`** — graph-neighbour signal
597
+ disabled by default. Empirical evidence from two independent AMB PersonaMem
598
+ benchmark runs showed `graph_weight=0.3` caused a −8 pp accuracy regression
599
+ (Chi² p=0.045). The score-adjustment mode cascade promotes short REFLECTION
600
+ summaries over detail-rich source OBSERVATIONs. Graph signal remains available
601
+ as an explicit opt-in via `search.default_graph_weight` in YAML config.
602
+ Until candidate-expansion via `CONSOLIDATED_FROM` ships, the graph backend
603
+ stays disabled.
604
+
605
+ ### Added
606
+
607
+ - **`store.metrics()` snapshot API** — added `EngravaMetrics`
608
+ with `thoughts`, `edges`, `storage`, and rolling-window `search_latency`
609
+ percentiles (`p50` / `p95` / `p99`). `engrava info` now renders this
610
+ contract instead of ad-hoc SQL counts, and the new `metrics:` YAML section
611
+ configures the latency window size and opt-out behaviour.
612
+
613
+ - **Dreaming — clustering + REFLECTION thoughts** — `run_consolidation()`
614
+ now builds thought clusters from the ASSOCIATED edge graph (Label Propagation
615
+ Algorithm, with agglomerative cosine-similarity fallback) and creates
616
+ `ThoughtType.REFLECTION` thoughts that summarise each qualifying cluster.
617
+ A centroid embedding (mean of member vectors, L2-normalised) is stored for
618
+ each REFLECTION. `CONSOLIDATED_FROM` edges link the REFLECTION back to each
619
+ cluster member. Idempotent: re-runs skip clusters whose content-hash already
620
+ exists. Opt-out via `DreamingGates.enable_reflections = False`.
621
+ `ConsolidationResult` gains a new `reflections_created` counter.
622
+ - **`DreamingGates` cluster fields** — `min_cluster_size` (default `3`),
623
+ `cluster_similarity_threshold` (default `0.7`), `cluster_algorithm`
624
+ (`"lpa"` | `"agglomerative"`, default `"lpa"`), and `enable_reflections`
625
+ (default `True`).
626
+ - **`search_hybrid()` — `include_reflections` + `reflection_boost`** — callers
627
+ can now pass `include_reflections=False` to exclude REFLECTION thoughts from
628
+ hybrid search results, or a custom `reflection_boost` multiplier to re-rank
629
+ them. The default boost (`SearchConfig.reflection_boost = 1.2`) gives
630
+ REFLECTION thoughts a mild up-ranking.
631
+ - **`search_reflections_only()`** — convenience method on `SqliteEngravaCore`
632
+ that returns only `ThoughtType.REFLECTION` thoughts ranked by hybrid score.
633
+ - **`list_edges()`** — new `SqliteEngravaCore` method for querying edges by
634
+ optional `edge_type` / `source` filters with configurable `limit`.
635
+ - **`SearchConfig.reflection_boost`** — new field (default `1.2`) parsed from
636
+ YAML `search.reflection_boost`.
637
+
638
+ - **Dream-created edges** — `run_consolidation()` now creates
639
+ `ASSOCIATED` edges between promoted thoughts and their nearest neighbours.
640
+ Controlled via `DreamingConfig.edges` (`EdgeCreationConfig`). Edges use
641
+ `source=KnowledgeSource.DREAMING` for attribution. Idempotent: re-runs do
642
+ not create duplicate edges.
643
+ - **Graph-aware hybrid search** — `search_hybrid()` supports an
644
+ optional 5th scoring signal (`graph_weight`) using 1-hop-weighted neighbour
645
+ boost. Disabled by default (`graph_weight=0.0`, opt-in). Controlled via
646
+ `SearchConfig.default_graph_weight`, `graph_edge_decay`, and
647
+ `max_neighbors_per_candidate`.
648
+ - **`KnowledgeSource.DREAMING`** — new enum value for dream-originated edges.
649
+ - **`EdgeCreationConfig`** — frozen dataclass for edge-creation parameters
650
+ (`enabled`, `top_k`, `min_similarity`, `edge_weight_factor`).
651
+ - **Priority signal in hybrid search** — `search_hybrid()` now supports an optional
652
+ 4th scoring signal based on thought `priority` (P1–P4). Controlled via
653
+ `SearchConfig.default_priority_weight` (default `0.05`) and per-priority boost
654
+ multipliers (`priority_boost_p1` through `priority_boost_p4`). Higher-priority
655
+ thoughts receive a proportionally higher score contribution.
656
+ - **`DreamingGates.allow_zero_confirmation`** — new boolean field (default `True`)
657
+ that bypasses the `min_confirmations` gate, allowing freshly ingested thoughts
658
+ with zero confirmations to be eligible for dreaming promotion.
659
+ - `examples/config.yaml` — out-of-the-box configuration with dreaming enabled and
660
+ sensible defaults.
661
+ - **Upgrade documentation and release template** — added `docs/upgrade.md`,
662
+ `.github/release-notes-template.md`, and a README upgrade entry so release
663
+ communication now has a stable place for compatibility guidance.
664
+
665
+ ### Changed
666
+
667
+ - **`DreamingGates.min_age_cycles`** default changed from `10` to `1`. Freshly
668
+ ingested thoughts become eligible for consolidation after a single cycle instead
669
+ of ten.
670
+ - **`SearchConfig.default_vector_weight`** changed from `0.6` to `0.55` to
671
+ accommodate the new priority signal while keeping weights summing to `1.0`.
672
+
673
+ ### Fixed
674
+
675
+ - **Dreaming consolidation now actually runs on fresh batches.** Previously, the
676
+ `min_confirmations=2` gate combined with `min_age_cycles=10` prevented any
677
+ promotion in single-write batch-ingest scenarios (confirmation count = 0, age = 0).
678
+ With `allow_zero_confirmation=True` (new default) and `min_age_cycles=1`, a
679
+ single `run_consolidation()` call on a fresh batch can now promote qualifying
680
+ thoughts.
681
+
682
+ ## [0.2.0] — 2026-04-12
683
+
684
+ ### Breaking Changes
685
+
686
+ - None.
687
+
688
+ ### Database Changes
689
+
690
+ - Schema version bumped to core-5.
691
+ - Added `access_count`, `last_accessed_at`, `confirmation_count`,
692
+ `consolidated_from`, and `visibility` to the thought table.
693
+ - Existing databases upgrade automatically through `ensure_schema()`.
694
+
695
+ ### Added
696
+
697
+ - **Full-text search (FTS5)** — `search_fts()` method with BM25 ranking on `essence`
698
+ and `content` fields. Hybrid search combines vector similarity, text relevance, and
699
+ recency scoring via configurable `SearchConfig` weights.
700
+ - **Extension system** — `EngravaHooksProtocol` with 5 hook points (`on_store`,
701
+ `on_retrieve`, `score_function`, `decay_function`, `mindql_extension_registry`).
702
+ `DefaultEngravaHooks` provides no-op defaults.
703
+ - **Dreaming / memory consolidation** — `DreamingExtension` with 5 pluggable signal
704
+ types (`ConfidenceSignal`, `ConfirmationSignal`, `FrequencySignal`, `RecencySignal`,
705
+ `StalenessSignal`) and configurable gate thresholds.
706
+ - **sqlite-vec backend** — optional `SqliteVecSearchBackend` for hardware-accelerated
707
+ vector search via the `vec` extra.
708
+ - **Multi-service isolation** — `EngravaManager` for running multiple independent
709
+ databases, each with its own schema, embeddings, and FTS index.
710
+ - **YAML configuration** — `load_config()` factory, `EngravaConfig` with `SearchConfig`,
711
+ `DreamingConfig`, `EmbeddingConfig`, and `ServicesConfig` sections.
712
+ - **5 embedding providers** — `SentenceTransformerProvider`, `OpenAICompatibleProvider`,
713
+ `OllamaProvider`, `HuggingFaceProvider`, `CallbackProvider`.
714
+ - **MindQL enhancements** — `COUNT`, `SELECT` with `WHERE` clauses, extensible command
715
+ registry via hooks.
716
+ - **CLI enhancements** — `export`, `import`, `gc`, `migrate` subcommands. Multi-service
717
+ `--service` flag for `snapshot`/`restore`/`export`.
718
+ - **Read-only store** — `ReadOnlyEngrava` wrapper that raises `ReadOnlyViolationError`
719
+ on write attempts.
720
+ - **`ExtensionManifest`** value object for extension discovery and registration.
721
+ - **`HybridSearchResult`** model combining vector, FTS, and recency scores.
722
+ - **`EmbeddingModelMismatchError`** exception for restore-time model validation.
723
+ - Open source release: standalone repository, MIT license, GitHub Actions CI/CD,
724
+ PyPI publishing.
725
+
726
+ ### Changed
727
+
728
+ - Bumped version from 0.1.0 to 0.2.0.
729
+ - `pyproject.toml` URLs now point to the standalone GitHub repository.
730
+ - Description updated to "Thought-graph database for AI agents".
731
+
732
+ ### Fixed
733
+
734
+ - `--re-embed` flag now raises an error when no embedding provider is configured
735
+ instead of silently succeeding.
736
+ - `snapshot --service` validates service existence before attempting export.
737
+ - `EngravaManager.get_store()` uses `asyncio.Lock` to prevent race conditions
738
+ during concurrent lazy initialization.
739
+
740
+ ## [0.1.0] — 2026-04-01
741
+
742
+ ### Breaking Changes
743
+
744
+ - Initial release.
745
+
746
+ ### Database Changes
747
+
748
+ - Initial SQLite schema introduced.
749
+ - No downgrade guarantees; follow forward-only upgrade policy from later releases.
750
+
751
+ ### Added
752
+
753
+ - Initial release (internal).
754
+ - `SqliteEngravaCore` — async thought/edge/embedding/action CRUD.
755
+ - `ThoughtRecord`, `EdgeRecord`, `EmbeddingRecord`, `ActionRecord` frozen Pydantic models.
756
+ - 9 domain enums (`ThoughtType`, `Priority`, `LifecycleStatus`, `EdgeType`, etc.).
757
+ - Brute-force cosine similarity embedding search.
758
+ - `MindQLParser` and `MindQLExecutor` — `FIND` and basic query support.
759
+ - CLI with `info`, `query`, `snapshot`, `restore` subcommands.
760
+ - Schema migration support via `ensure_schema()`.