Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
60f3ac9
feat(detectors): pin and version the thresholds behind the non-spine …
robotrocketscience Aug 5, 2026
5186491
test(detectors): assert the manifest against live source, and sweep f…
robotrocketscience Aug 5, 2026
af0cd65
docs(changelog): record the detector-threshold manifest (#1355)
robotrocketscience Aug 5, 2026
0851202
fix(detectors): pin booleans as literals so size_of and pin_value agree
robotrocketscience Aug 5, 2026
cb6bb3a
refactor(detectors): defer the annotation-only Sequence import
robotrocketscience Aug 5, 2026
1331078
docs(changelog): indent the #1355 limits paragraph into its own bullet
robotrocketscience Aug 5, 2026
7306bf0
test(detectors): close two evasions in the writer-coverage sweep
robotrocketscience Aug 6, 2026
37bcfab
docs(changelog): the manifest spans 9 modules, not 8
robotrocketscience Aug 6, 2026
1ff8323
fix(detectors): key the manifest digest by version so a value change …
robotrocketscience Aug 6, 2026
9139497
fix(detectors): correct five gates strings and the TEMPORAL_NEXT excl…
robotrocketscience Aug 6, 2026
7565889
docs(changelog): state what the digest guard enforces and what it doe…
robotrocketscience Aug 6, 2026
525ca86
refactor(detectors): narrow the sequence cast to the isinstance guard
robotrocketscience Aug 6, 2026
4347d85
test(detectors): replace a tautological digest assert with a reachabl…
robotrocketscience Aug 6, 2026
cff7be8
test(detectors): correct the digest comment and de-vacuum the scalar …
robotrocketscience Aug 6, 2026
ec25ee6
style(detectors): sort __all__ in the threshold manifest (#1355)
robotrocketscience Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG/v4.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- **`ingest_log.derived_edge_ids` now populates, forward-only, and an edge-set divergence is drift ([#1354](https://github.com/robotrocketscience/aelfrice/issues/1354), [#1157](https://github.com/robotrocketscience/aelfrice/issues/1157) AC3/AC4).** The column was NULL on **every** row (0 of 140,251 on the development store) because all six `derive()` return paths hardcoded `edges=[]`, so no edge had a log row pointing at its origin. `derive()` now emits `DERIVED_FROM` from a reserved `raw_meta.derived_from` block carrying the preceding sentence's verbatim text, and the derivation worker stamps what it emitted. The input travels in `raw_meta` rather than as a `DerivationInput` field because `raw_meta` is already passed through verbatim by both reconstructors — the worker and the replay harness — so it cannot drift between them; text rather than an id keeps `_belief_id` the single minter. **Forward-only with no migration and no new column:** `[]` now means "derived none" and SQL `NULL` means "no edge-aware writer ever saw this row", and since `list_unstamped_ingest_log` filters on `derived_belief_ids IS NULL`, no pass ever revisits a stamped row. The replay probe's edge comparison moves out of the informational `mutable_divergence` bucket into its own `edge_set_divergence` counter that **does** trigger `has_drift` — guarded on the raw column being non-NULL, not on the decoded set, because `_logged_edge_set` collapses NULL, `'[]'` and corruption to the same empty set and a guard written against its output would exempt corruption along with history. **Two corrections to the issue's own premise, both measured.** `DERIVED_FROM` is not emittable without new inputs: neither production writer's target is on the log row — the intra-turn one resolves it from the worker's outcomes map *after* derivation, the inter-turn one from an in-memory per-session dict. And the reachable share is much smaller than the `8.9%` the issue scoped from: only the intra-turn writer is liftable, and attributing the live set by endpoint timestamp puts it at **at most 918 of 47,533 edges (1.93%)** — 21.7% of `DERIVED_FROM`, not all of it. The inter-turn writer's `src` is the turn's *last newly-inserted* belief, which is not knowable at `record_ingest` time. Also fixes a latent crash this would otherwise have shipped: the worker's edge loop was a bare `INSERT` into a table keyed `PRIMARY KEY (src, dst, type)` with no per-row `try/except`, so the first `derive()` to emit an edge turned re-ingest — the documented idempotency contract — into an `IntegrityError`.
- **The thresholds behind the 2.8% non-spine edges are pinned and versioned, so "edges are recomputable" is now checkable for them ([#1355](https://github.com/robotrocketscience/aelfrice/issues/1355), [#1283](https://github.com/robotrocketscience/aelfrice/issues/1283) AC2).** #1283 restated AC2 in two halves; the recompute half shipped, and this is the other one. Edges that are neither `TEMPORAL_NEXT` nor `DERIVED_FROM` are a function of the belief set **and** of detector thresholds, so recomputability holds only if those thresholds are pinned — and they were bare module constants with no guard at all. `aelfrice.detector_thresholds` records **22 constants across 9 modules** behind `DETECTOR_THRESHOLDS_VERSION = 1`: the five `relationship_detector` cutoffs and caps plus its quantifier axis and three token/pattern tables, `contradiction.SUPERSEDES_WEIGHT` and its precedence ladder, the `triple_extractor` phrase-to-edge-type table and anchor width, four `value_compare` slot-gate constants, and the constants on the two paths that actually decide which phantoms reach the `RELATES_TO` writer. **The defect being fixed was a test that could not fail.** `tests/test_relationship_detector.py` asserted `cfg.jaccard_min == DEFAULT_JACCARD_MIN`, comparing the constant to itself; changing `DEFAULT_JACCARD_MIN` from 0.4 to **0.9** in source leaves that test **green**, which is measured here rather than asserted — the same defect class as the #1353 synth-exclusion pin. The manifest holds hand-written literals and **imports nothing from `aelfrice`**, precisely so it cannot repeat that mistake: the test does the importing and re-derives each pinned form from the live object, and scalars must stay literals (a separate arm rejects "repairing" a red scalar by converting it to a digest). Collections pin as a digest of a canonical form that **includes regex flags** — dropping `re.IGNORECASE` from the triple patterns changes no pattern text but changes which triples match, and it goes red. A content digest is recorded **per version** in `DIGEST_HISTORY` rather than as one literal beside the manifest, and that distinction is the difference between enforcing the version bump and merely announcing it. A single `MANIFEST_DIGEST = "<hex>"` does not force anything: edit the constant, edit its manifest entry, edit the digest, and the suite is green again with the version untouched — two different edge-producing behaviours both shipping as version 1, which is the exact failure the version exists to prevent. Keyed by version, that repair is gone: the ways back to green are to revert, or to bump and **append** a row. Stated limit, because the guard is not airtight — a historical row can still be overwritten, and only a merge-base check in CI (if the content digest differs from `main`'s, require the version to have increased) would be fully mechanical; that is deliberately not built here. A comment-only edit stays green, because the digest is over data rather than source text. Coverage is **swept from source, not hand-listed** — every module calling `insert_edge` must be either pinned or excluded with a stated reason (**4 covered, 6 excluded**: the spine, the two `DERIVED_FROM` paths, the cross-store copier, and two synthetic-fixture builders), and a separate arm asserts only the store writes the `edges` table directly, so the sweep cannot be evaded with raw SQL. **Reachability was checked, not assumed, and it moved two things.** A first pass pinned six `wonder.{evaluator,strategies}` constants as the gate on `RELATES_TO`; the package docstring says outright that those strategies are research-only and do not write to a live store, and their sole importer builds against `MemoryStore(":memory:")`, so they decide no edge in any user's store and were dropped rather than shipped with `gates` text that could not be true. The paths that *do* decide it — BFS hops ranked by `wonder_consolidation.score` then sliced by `--top`, and the dispatch-seeded persist-docs path — are pinned instead. The four `value_compare` entries are kept but relabelled **dormant**: no shipped caller passes `use_value_comparison=True`, so the slot gate mints nothing today, and pinning it now makes flipping that flag a one-line change against a known baseline. Relatedly, the coverage sweep can only see modules that *call* `insert_edge`, so three upstream suppliers were added by hand — `bm25._TOKEN_PATTERN` (the token universe for every Jaccard and membership test in the detector), `models.ANCHOR_TEXT_MAX_LEN`, and `wonder_consolidation._TOKENIZER_DROP` — and the two known-unpinned suppliers (`dedup`'s prefilter semantics, `config_discovery`'s file resolution) are named in the module docstring so their absence is a recorded decision.

**Two limits stated rather than papered over.** Three of the entries are defaults a `.aelfrice.toml` `[relationship_detector]` section can override, so for those the manifest pins the *shipped default*, not the value a given store actually ran with; every entry names its override mechanism so the two are distinguishable. And this is **forward-only** — the `edges` table has no version and no `created_at`, so pinning today does not make a historical edge attributable to the thresholds that produced it. Adding those columns is the `edges`-table migration that left stores unopenable-forever in #1161, and historical reproduction stays explicitly out of scope.
- **The injected block can now carry the evidence behind each belief, grouped by trust tier ([#1326](https://github.com/robotrocketscience/aelfrice/issues/1326), [#1177](https://github.com/robotrocketscience/aelfrice/issues/1177) proposal 18).** The per-turn line rendered `id`, `lock` and (since #1171) `speculative`, and threw away everything else the store knows about how far to trust a belief. `[hook] provenance_render` (default-**off**, `AELFRICE_PROVENANCE_RENDER` overrides) groups the block into `<user-locked>` / `<observed>` / `<inferred>` with a framing clause each, and emits `origin`, `n` (= `alpha + beta`), `mu` and `seen` on non-locked lines. Every value is already on the belief at render time — measured, all four populated on **74 of 74** hits in a live pack — so there is no new query. The point is `n`: `mu = 0.6 at n = 2` is byte-identical to `mu = 0.6 at n = 200` at every scoring site, and one live pack carried **25 distinct `n` values from 1.6 to 363.2** inside a single turn's block, so the signal the ranker must collapse is one the model can weigh contextually. Section membership is a **total** function of `lock_level` and `origin` — the proposal as filed classified origins with two literal sets that between them stranded **6,396 active beliefs (14.3%)** in no section at all, and named two origins (`commit`, `file`) that do not exist; a renderer written to it would have dropped 14.3% of the block with no error. Every `models.ORIGIN_*` constant is now classified, an unrecognised origin falls back to `<inferred>` rather than vanishing, and a test enumerates the constants from `models` so a new origin cannot be added without being classified. `speculative="1"` is folded into `origin="speculative"` rather than emitted alongside it, while the #1171 framing sentence still fires. With the flag off the block is byte-identical to before, asserted against literal expected bytes rather than recomputed.
- **The temporal spine is recomputable from the log, and the gap is named rather than averaged ([#1283](https://github.com/robotrocketscience/aelfrice/issues/1283)).** `aelf spine verify` recomputes the `TEMPORAL_NEXT` set on the ratified key — `(created_at, ingest_log ULID)` — and reports the divergence against what shipped. **This is the recompute half only; the writer still orders by `(created_at, rowid)`, so the number is a gap against the contract, not drift**, and the command says so in its own output because "93.68%" alone reads as decay. `rowid` is exactly the problem: it is implicit and `VACUUM` may renumber it, which is why the ratified key is the log's ULID rather than anything read off the belief table. **Three rules, none a heuristic.** Synth log rows are excluded by `source_kind = 'legacy_unknown'` — a stated durable column, exact in both directions here — because the #263 synthesis minted 20,852 rows in a 201 ms window whose order is `beliefs.rowid` relabelled, and honouring them would launder rowid order into the key the contract calls durable. **The rule is a forward safeguard, and its measured effect on this store is zero** — neutralising the exclusion so synth rows do supply keys leaves the report identical in every bucket, with the recomputed edge sets differing by 0 and the no-log sets by 0. 20,852 beliefs take a different sort key under that arm, but only **5** are session-scoped and each of those also carries a non-synth row, so nothing moves. "Exact in both directions" is a property of the column, not a measured effect, and the effect is worth naming as inert here before it is read as the reason the rule earns its place: it earns it against a store whose migration ran at a different time, which this one did not. Two heuristic detectors were measured first and both failed loudly: a prefix-versus-`ts` disagreement threshold catches legitimately delayed derivation, flags 30,738 rows and collapses reproduction to 34.75%; excluding the largest ULID-prefix date cluster would drop **51.8% of the log**, because that cluster is a real bulk backfill carrying 20,095 session-scoped beliefs, not a synth event (`benchmarks/ingest_log_ulid_clusters.py`). A belief takes its **earliest** qualifying log row, since later rows are corroborations and only the first records insertion. Beliefs with no qualifying row sort last within their `created_at` group — **a forward convention, explicitly not a recovery**: 2,426 such beliefs carry only 433 distinct timestamps, so 94% sit inside a tie where the only other durable column is a content-addressed id, and three placement rules were measured with one link of spread between them. That ordering is **unreconstructible**, and saying so is the deliverable rather than a caveat. Divergence is reported in **three buckets, never one percentage**, because only one is a defect anyone can fix and a single number lets the unreconstructible bucket mask a real key disagreement: on the development store, 41,929 shipped and 39,280 reproduced (93.68%), missing 2,100 no-log / 546 fan-in / **3 other**. Those figures reproduce the issue's gate measurement and its correction to the digit from an independent implementation. The recompute is read-only and opened read-only (#1328); 0.86 s on a 44,594-belief store.

Expand Down
Loading
Loading