IntelShed: An Open-Source Platform for OSINT, AI Research, and Collaborative Intelligence

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 failed
  • test_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.