Mindbridge Β· track status

EDGE-TRUTH

The v2 graph's edge vocabulary does not mean what it says β€” one edge type carries three unrelated relationships, a merge routine repoints edges onto nodes their own name forbids, and nothing validates the (edge_type, source node_type, target node_type) triple at any point in its life.

Opened 2026-08-06 Β· re-measured against production 2026-08-08 Β· plan Β· ledger. Verify before starting: every count here decays with each merge β€” re-derive from the plan's stage table and production, never quote this page.

Next β†’ #1465 relationship typing is structurally wrong β€” the graph inverts families (a sister typed as spouse) Decision β†’ family disclosure (D20): AMA surfaces family relationships the person page hides β€” pick one policy
Silent data loss (D23) β€” worst open defect, still homeless: pending_attribution.target_node_id is ON DELETE CASCADE, so a twin-merge silently destroys an in-flight forward-reference attribution, and drain_pending never consults the merge-redirect resolver. A PRIME-DIRECTIVE item sitting in no stage and no issue. Unmeasured.
Audit done, remediation open β€” 6 defects fixed+live (D1, D5, D10, D13, D14, D21) Β· 18 live edge types still carry a defect verdict, findings running to D25 Β· 4 owner rulings open Β· 1 silent-data-loss finding unfiled.
7open issues
58/59types adjudicated
6defects fixed + live
9 of 9rules choke-pointed
16,231MENTIONED_ENTITY rows (08-08)
2,312AUTHORED rows (08-08)
Open work Owner rulings Standing rulings History & forensics

Open work

Wrong edges β€” user-visible

p1 #1465 fix(extraction): relationship typing structurally wrong β€” sister typed as spouse, PARENT both-directions, no reconciliation family structure inverted by extraction All
Read-only audit of the owner's tenant, checked against ground truth: Yasmine (sister) typed SPOUSE; Rama (spouse) nearly missed; Hana and Uschi (children) direction-inverted; an ex-partner's phone number typed SPOUSE. Three layers, none constraining relationship semantics: the extraction prompt is a flat type list (no disambiguation, no direction); the schema's person_a/person_b is positional (no parent/child direction); the materializer is a pass-through (no symmetry canonicalization, no PARENT↔CHILD unification, no per-pair conflict resolution). Fix is eval-gated re-extraction, not manual data edits. Pairs with the D20 ruling below.

Stale content β€” v2 recency

p1 #2994 node_viewer_scoped_repository orders entity nodes by nodes_v2.created_at β€” a rebuild timestamp, not content recency rebuild order, not content recency All
_ENTITY_NODES_BY_TYPE_SQL ends ORDER BY n.created_at DESC β€” on v2 that is the materialization timestamp, identical across a full rebuild, so it collapses to the n.id tiebreak and surfaces year-old items as "recent". Deliberately deferred as "parity with the prior client read"; the fix already exists in find_recent_by_type, which ranks by provenance-conversation activity and is live in three resolvers. Related: #1467 (person-card stale items), #1483 (same substrate cause).
p1 #1483 suggested-questions v2: personalized ranker uses placeholder recency on v2 (provenance-conversation cutover) no staleness filter on v2 All
The personalized ranker opts into a per-type recency cliff, but build_cliff_sql_filter deliberately raises under v2 (edges_v2.last_seen is the rebuild timestamp, not content recency), so it degrades to a Python pass-through with no real staleness filter. PR9-flip blocker: after the default-to-v2 flip, home feed + onboarding rank without real recency. Fix: cut over to provenance-conversation recency (MAX(conversations.last_activity_at)), mirroring surface_open_questions / now_feed.

Write & read-path gaps

p1 #3307 edges_v2 concurrent upserts deadlock in a 4-way cycle β€” CI retries and swallows it, production has no such retry four-way deadlock, CI swallows it All
Four-process deadlock cycle on the edges_v2 upsert: N workers touch overlapping (tenant_id, source_id, target_id, edge_type) keys in different orders within one pass. The suite passes because CI retries and swallows it; production has no retry, so it surfaces as a failed materialization phase. Known root (ADR-112 Β§Eβ‘ /β‘‘), deferred with no tracking issue until now. Cure: the edge coalescer (one deterministic upsert per key per pass) or a deterministic global key ordering β€” not a retry, not a bigger lock. Ruled 08-14: does not outrank the rest of this track.
p2 #3256 BOUND_READ_VOCABULARIES is a convention with no enforcing hook β€” bind a literal for any of 77 edge types and nothing fails 77 types bind, nothing fails Dev only
contract.py::BOUND_READ_VOCABULARIES exists so the read/write parity gate survives a bound SQL literal, but nothing enforces registration: check.py has zero references to it, and the one test inspects only a single allowlisted type (INVOLVES_SKILL, 1 of 78). Recommended: a NewType returned only from the register functions, so mypy becomes the hook across ~28 call sites β€” structural, not prose. This is the gap #3246 hit, caught only because INVOLVES_SKILL happened to be allowlisted.
p3 #3102 EDGE-TRUTH: persist the matched SKILL on MATCHES_OPPORTUNITY_CAPABILITY edges card shows name, not skill All
D1's fix surfaces opportunity↔capability pairs, but the matched SKILL that makes the suggestion meaningful is not persisted β€” the "who should connect" card renders the partner's name ("Amira: … ↔ Kareem: Kareem"). One nullable JSONB field, one kwarg, one read; no migration. Explicitly rejected: re-deriving the skill at read time (fabrication). The existing rows keep the name fallback; a backfill is possible but not obviously worth it.
p3 #3003 graph_expand's target_type / _filter_by_target_type is unreachable from production dead filter, nothing reaches it Dev only
graph_expand(target_type=…) and _filter_by_target_type have one call site, and the only production caller (traverse_graph.py:92) never passes target_type β€” so the filter is dead in production and nothing catches a regression in it (proven: making it raise left all 6 e2e tests green). Decision: wire it (if "show me pictures" should route through it) or delete it β€” effective_target_types already covers the intent-driven case.

Open owner rulings β€” four

41 retyped edgesβ€” D5 is forward-only: re-point, drop, or flag?
ORGANIZED vs ORGANIZED_BYβ€” collapse to one edge, or union at the read seam? The last type left unaudited.
Calendar provenance (1c)β€” populate it and 2,583 calendar events appear in the now-feed at once.
Family relationships (D20)β€” AMA surfaces family relationships the person page hides; pick one policy.
Why is it in this order?

Order: user-visible wrong edges first, then stale-content surfaces, then structural write/read gaps β€” and within a cohort by priority (p1 β†’ p2 β†’ p3) then oldest-first. It ignores what blocks what (GitHub carries no dependency edge we populate), how long anything takes, and which commitment an issue serves. The 08-14 ruling applies on top: the upsert deadlock (#3307) does not outrank the rest of the track.

Standing rulings

  • The edges_v2 upsert deadlock (#3307) does not outrank the rest of the track (track-order.json, 2026-08-14).
  • Sharing counts on a list surface, not the single-owner lookup β€” distinct documents per person 261 β†’ 13,489, people with any 207 β†’ 1,872, across 15 tenants (08-08).
  • Interaction signals out of assistant traversal and the entity detail surface; the person page keeps them β€” one derived source, default withholds rather than discloses (08-08).
  • HHI_AUTHORED: ship, and log it open β€” the flip takes five tenants to exactly 0; the metric is internal-only and measurement is owner-deferred (08-08).
  • Content concentration: ruled out of this track β€” admin-only, does not feed the tenant-visible composite; ADR-143 says otherwise and is wrong (08-07).
  • Client-side AUTHORED filter (1x): decided against β€” correctness by "every caller remembers"; stage 5 shipped instead.

History & forensics

Discovery, stage 1–2, and the AUTHORED split β€” done

Rounds 1+2 censused 59 live edge types and adjudicated 58 (ORGANIZED/ORGANIZED_BY held back β€” a design decision, not a defect). Round 2 changed what the track is about: round 1 found wrong edges, round 2 mostly found wrong readers β€” contracts honoured by one consumer and silently absent from another. Six defects fixed and live: D1 (connection resolver dropped all 109 MATCHES_OPPORTUNITY_CAPABILITY rows β€” #3104), D5 (reconciliation retyped pinned edges β€” #3098; forward-only, 41 rows still wrong), D10 (intent-less AMA traversal hit the DB unfiltered β€” #3097), D13 (completed actions listed as open β€” #3114), D14 (met needs / resolved problems offered as open β€” #3120), D21 (skill 2-hop dispatched for three types its SQL can never match β€” #3246).

Stage 2 (#3160, issue #3140) landed the contract, the consumer half, the attribution vocabulary and the reader census as one release with nine choke-point rows. Its third adversarial panel β€” run after the fixes β€” found six more defects plus the same comment-stripper failure in three security gates, all previously failing open.

The AUTHORED split (ADR-143, accepted 08-07) minted MENTIONED_ENTITY for the entity-mention meaning. Stage 5a shipped across #3129 (reader prep), #3210 (remaining readers + intent-map reachability), and #3222 (the flip + reclassification). 5a-live, measured on prod after the deploy: AUTHORED now means authorship in 100% of its rows (was 1.3%); MENTIONED_ENTITY 16,169, AUTHORED 2,303, rollout residue 0; the producer was OBSERVED writing the new spelling (14:40–14:48Z, Regina). A 4-lens panel before merge found silent data loss in the migration: the DELETE lacked the UPDATE's collision guard, so a row committed between the two statements was destroyed and counted as a duplicate. Also fixed: a provenance merge, a guard test that was vacuous by execution, and a MENTIONED_ENTITY label missing from both locales. The 2c nightly gate's runner was structurally dead since 2026-05-13 (87 "success" runs, 0 rows) and was repaired (#3225); 2c's precondition is a row in invariant_runs, not a green workflow run.

Remaining open defect verdicts β€” D6–D25, unfiled

D6 ANSWERED direction diverges from ADR-071 (1,957 rows) Β· D7 ATTACHED_TO/CALLED have live producers and zero rows Β· D8 8 dead enum members + a wrong types.py header Β· D9 fetchPersonEdges shares one LIMIT 200 across all 15 edge types Β· D11 personal names typed node_type='company' (β‰₯3 in ~140 sampled) Β· D12 intent matcher fed the entity name, not the query (1f β€” the obvious fix activates a banned English lexical matcher, needs an ADR) Β· D15 ATTENDED_BY 7,482 rows, zero consumers Β· D15b use-entity spreads OUTGOING unfiltered β†’ 404 links Β· D16 INVOLVED_IN 309 rows, no consumer Β· D17 MEMBER_OF 731 rows drive access-control, unreachable from any surface Β· D19 MANAGER=REPORT=124 exactly (one few-shot example mints both); PARENT/CHILD drift 33/5 Β· D22 match cards render a 0.70 guess identically to a 1.0 match (1j, unblocked, cheap) Β· D23 pending_attribution silent data loss (surfaced above, unfiled) Β· D24 SHARED_JOB_POSTING 259 rows, zero readers Β· D25 INVOLVES_SKILL has no producer, types.py claims otherwise. Not-started stages: 2c nightly drift gate (use created_at DESC, never ORDER BY id β€” edges_v2.id is a random v4 UUID), 3 comment + dead-code corrections (trivial), 5b/5c/5e/6 (retyped targets, the six render surfaces β€” narrower than the plan implies after the flip β€” the shared 200-row budget, corrected-vocabulary consumers).

Verified against production β€” do not re-derive

The unique index is (tenant_id, source_id, target_id, edge_type) β€” a unique index with no named constraint. edge_count triggers fire on INSERT and DELETE only, so an edge_type-only UPDATE leaves the counter untouched. No live SQL reads AUTHORED at all (zero function bodies, views, matviews). Measured 08-08 21:00Z in one snapshot: AUTHORED 2,312 rows / 12 tenants (answer 2,048, document 264), MENTIONED_ENTITY 16,231 / 17 (company 7,811, location 4,639, product 3,414, project 367), edges_v2 173,417 / 22. The migration moved 16,169 (16,166 predicted + 3 minted, the ~400/day rate). The contract still holds against the live producer: zero rows of either type outside their declared target set. The six-vocabulary survey undercounted β€” the stage-2f census found more, and by 08-08 register_attribution_vocabulary is called 10 times with _UNCONVERTED_FORKS at zero.