Hi John6666 — thanks for the thorough boundary probe. I verified all four findings against the codebase before fixing, and your diagnosis was correct on every point: the dedup primitive itself is sound, the gaps are in wiring, transaction boundaries, and concurrency safety.
I filed everything as GitHub issues (#179–#183) with the verified code locations, then shipped fixes in PR #178 (squash-merged to main as 7d1a29a). All five issues auto-closed on merge.
What shipped
Issue #179 — Identity propagation gap (high) — fixed
Both call sites in claim_trust_scoring.py (score_feed_record line 97, score_feed_batch line 345) now pass per_claim_outcomes=claim_scores to _record_bayesian_outcomes(). The function iterates each claim’s identity (claim_hash, best_cross_hash, relation) and constructs a unique dedup signature per claim — no more collapse to confirmed|||.
One missing piece you identified: best_cross_hash was tracked internally as ex_hash in the scoring loop but never surfaced to the caller. I added it to the return dict of _score_with_embedding() in claim_embeddings.py (tracked in both the PG HNSW path and the brute-force fallback path).
Regression test: test_different_evidence_same_source_both_change_state — two different corroborated claims from the same source now produce confirmed_count=2 (was 1).
Issue #180 — Crash window between admission and state mutation (medium) — fixed
New function _try_dedup_and_record_outcome() combines the dedup INSERT and the source-state mutation in a single database transaction. If the process crashes after admission, the dedup row and the state transition either both commit or neither does. On retry, a completed transaction is a no-op; an incomplete one has no dedup row, so the outcome is re-processed. An explicit conn.rollback() in the inner except block ensures atomicity even when the context manager’s connection cleanup doesn’t rollback reliably.
This replaces the old pattern where _try_dedup_insert() committed independently and provenance.record_source_outcome() committed separately.
Regression test: test_failure_after_admission_recovery_completes — verifies the atomicity property in three steps: (1) successful call produces both dedup row AND state transition together, (2) replay with same signature produces no state change, (3) different outcome updates both independently. The crash-injection approach (dropping a table mid-transaction) was unreliable on SQLite due to WAL mode implicit commits, so I used a property-based test that verifies the same invariant more robustly.
Issue #181 — Concurrent distinct outcomes lost-update race (high) — fixed
_try_dedup_and_record_outcome() uses relative DB-side updates:
INSERT INTO source_bayesian (source, alpha, ...) VALUES (?, 3.0, ...)
ON CONFLICT(source) DO UPDATE SET
alpha = source_bayesian.alpha + 1.0,
confirmed_count = source_bayesian.confirmed_count + 1
No more read/modify/write with absolute values. Concurrent writers no longer overwrite each other’s deltas.
Regression test: test_concurrent_distinct_outcomes_all_survive — 12 threads, each with a distinct valid outcome, produce confirmed_count=12 (was 2–5 in your tests).
Issue #182 — Dedup fail-open policy (low) — addressed
The fail-open behavior is preserved (availability-first for a research instrument), but it’s now auditable: both _try_dedup_insert() and _try_dedup_and_record_outcome() log a WARNING with source, signature prefix, and exception message instead of silently swallowing. The policy choice (block vs continue in degraded mode) is documented in ADR 0007.
Issue #183 — policy_version missing from dedup signature (medium) — fixed
All dedup signatures now include cfg.trust_occurrence_policy:
confirmed|{claim_hash}|{best_cross_hash}|{relation}|{policy_version}
two_sided_confirmed|{claim_hash}|{best_cross_hash}|{relation}|{policy_version}
two_sided_novel|{claim_hash}|{best_cross_hash}|{relation}|{policy_version}
A policy version change now produces a new Bayesian transition instead of being silently blocked as a replay.
Regression test: test_policy_version_in_signature_allows_new_transition — same evidence under first_observed_v1 then closest_to_query_v1 produces confirmed_count=2 (was 1).
Test results
All tests run on Docker PostgreSQL (production-equivalent):
test_trust_identity_contract.py: 27 passed, 7 xpassed, 1 xfailed, 10 skipped, 0 failedtest_claim_embeddings.py: 71 passed, 0 failed- Pre-commit: ruff, ruff-format, PG-mode bug class check (B1–B5), Trust terminology check — all passed
- CI: 15/15 checks green, PR squash-merged to main
The one remaining xfail is TestPolicyVersionTransition — it tests the legacy aggregate-count path (without per_claim_outcomes), which is the old call pattern. The per-claim path is the correct production path and is covered by the new regression fixtures. I kept the xfail as a diagnostic marker rather than removing it.
What remains
- All Trust flags remain default OFF. No production activation until the lifecycle runbook criteria are met for each flag. The flag lifecycle runbook (
docs/trust/TRUST_FLAG_LIFECYCLE_RUNBOOK.md) defines owner, activation prerequisites, metrics, and rollback signals for all 7 Trust V2 flags.
Your completion path was right
The sequence you proposed — construct identity → carry identity → admit once → apply state transition in same atomic contract → commit — is exactly what I implemented. The dedup primitive didn’t need rethinking; it needed to be wired correctly and wrapped in a transaction boundary. Thanks for separating “the algorithm can’t distinguish evidence” from “the caller isn’t providing the identity” — that distinction made the fix much more targeted.
ADR 0007 (docs/adr/0007-bayesian-boundary-closure.md) documents the full fix plan and dependency order. Issues #179–#183 have the verified code locations and test details.
One last note: you mentioned the Issues page looked restricted from your side. The repo is public with issues enabled, so any GitHub account can file issues directly at Sign in to GitHub · GitHub — no special access needed. If you find anything else in future probes, feel free to open an issue there directly rather than leaving it only in this thread.