Skip to content

Commit 2ff519c

Browse files
committed
feat(delphi): plumb D11 consensus + D12 priorities through to_dict / to_dynamo_dict
commit-id:ff4764e4
1 parent f0d21d6 commit 2ff519c

2 files changed

Lines changed: 121 additions & 12 deletions

File tree

delphi/polismath/conversation/conversation.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,12 +1891,15 @@ def numpy_to_list(arr):
18911891
# a list-of-dicts format that would break server/src/report.ts,
18921892
# server/src/utils/pca.ts, and client-participation-alpha consumers.
18931893

1894-
# Add empty consensus structure for compatibility
1895-
result['consensus'] = {
1896-
'agree': [],
1897-
'disagree': [],
1898-
'comment-stats': {}
1899-
}
1894+
# Surface D11 consensus comments (Clojure parity: client-report's Majority
1895+
# view consumes result['consensus']). Pre-Investigation-B this block was
1896+
# hardcoded empty, which silently zeroed the Majority view regardless of
1897+
# the D11 selection. Falls back to the empty shape when repness is missing
1898+
# or did not produce a consensus_comments dict (older blobs, no-group convs).
1899+
result['consensus'] = (
1900+
self.repness.get('consensus_comments', {'agree': [], 'disagree': []})
1901+
if self.repness else {'agree': [], 'disagree': []}
1902+
)
19001903

19011904
# Add math_tick value
19021905
current_time = int(time.time())
@@ -2432,12 +2435,14 @@ def float_to_decimal(obj):
24322435
}
24332436
result['pca'] = float_to_decimal(pca_data)
24342437

2435-
# Add consensus structure
2436-
result['consensus'] = {
2437-
'agree': [],
2438-
'disagree': [],
2439-
'comment_stats': {}
2440-
}
2438+
# Surface D11 consensus comments (Clojure parity). Pre-Investigation-B
2439+
# this block was hardcoded empty, so the DynamoDB blob never carried the
2440+
# D11 dict even when repness produced one. Falls back to the empty shape
2441+
# when repness is missing or didn't produce consensus_comments.
2442+
result['consensus'] = (
2443+
self.repness.get('consensus_comments', {'agree': [], 'disagree': []})
2444+
if self.repness else {'agree': [], 'disagree': []}
2445+
)
24412446

24422447
# Add math_tick value
24432448
current_time = int(time.time())

delphi/tests/test_discrepancy_fixes.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2572,3 +2572,107 @@ def test_p_success_matches_blob(self, clojure_blob, dataset_name):
25722572
assert mismatches.empty, (
25732573
f"[{dataset_name}] {len(mismatches)}/{len(df)} p-success mismatches:\n"
25742574
+ mismatches.head(10).to_string(index=False))
2575+
2576+
2577+
class TestD11D12Serialization:
2578+
"""Round-trip tests for the D11/D12 plumb-through in to_dict / to_dynamo_dict.
2579+
2580+
Investigation B (2026-06-11) discovered that both serializers were hardcoding
2581+
``result['consensus']`` to an empty dict regardless of
2582+
``self.repness['consensus_comments']``, so the D11 consensus dict never
2583+
reached client-report's Majority view and never landed in the DynamoDB
2584+
math blob. ``comment_priorities`` (D12) was already conditionally plumbed
2585+
via ``hasattr/if`` guards; we lock that in with a regression test so a
2586+
future cleanup doesn't silently revert to the empty-default shape.
2587+
"""
2588+
2589+
@staticmethod
2590+
def _make_conversation_with_repness(consensus_comments, priorities):
2591+
"""Build a Conversation with just enough state to exercise the
2592+
serializers. Empty rating matrices and empty group_clusters mean the
2593+
rest of to_dict/to_dynamo_dict iterates over zero rows/cols (cheap)
2594+
while the consensus + priorities fields still flow through end-to-end.
2595+
"""
2596+
conv = Conversation(conversation_id='ztest-serialization')
2597+
conv.repness = {
2598+
'comment_ids': [],
2599+
'group_repness': {},
2600+
'comment_repness': [],
2601+
'consensus_comments': consensus_comments,
2602+
}
2603+
conv.comment_priorities = priorities
2604+
return conv
2605+
2606+
def test_to_dict_surfaces_consensus_comments(self):
2607+
"""``to_dict()`` must surface ``self.repness['consensus_comments']`` into
2608+
``result['consensus']``. Pre-fix this slot was hardcoded
2609+
``{'agree': [], 'disagree': [], 'comment-stats': {}}`` and the D11
2610+
selection was silently dropped on the floor."""
2611+
consensus = {
2612+
'agree': [
2613+
{'tid': 1, 'n-success': 3, 'n-trials': 4,
2614+
'p-success': 0.7, 'p-test': 1.5}
2615+
],
2616+
'disagree': [
2617+
{'tid': 2, 'n-success': 2, 'n-trials': 5,
2618+
'p-success': 0.42, 'p-test': 1.1}
2619+
],
2620+
}
2621+
conv = self._make_conversation_with_repness(consensus, {})
2622+
2623+
result = conv.to_dict()
2624+
2625+
assert result['consensus'] == consensus, (
2626+
"to_dict() must plumb self.repness['consensus_comments'] into "
2627+
"result['consensus']; got " + repr(result['consensus']))
2628+
2629+
def test_to_dict_surfaces_comment_priorities(self):
2630+
"""``to_dict()`` must surface ``self.comment_priorities`` (D12). This is
2631+
a regression lock: the field is currently conditionally plumbed via
2632+
``hasattr/if``; a future cleanup must not revert to the hardcoded
2633+
empty default."""
2634+
priorities = {1: 0.42, 2: 1.7, 3: 0.0}
2635+
conv = self._make_conversation_with_repness(
2636+
{'agree': [], 'disagree': []}, priorities)
2637+
2638+
result = conv.to_dict()
2639+
2640+
# The to_dict key uses underscore form (see line ~1706); no rename
2641+
# happens on the way out, unlike most Clojure-format fields.
2642+
assert 'comment_priorities' in result, (
2643+
"to_dict() must emit 'comment_priorities' when "
2644+
"self.comment_priorities is populated; keys = "
2645+
+ repr(sorted(result.keys())))
2646+
assert result['comment_priorities'] == priorities
2647+
2648+
def test_to_dynamo_dict_surfaces_both(self):
2649+
"""``to_dynamo_dict()`` must surface BOTH consensus comments (D11) and
2650+
comment priorities (D12). The DynamoDB shape uses underscore keys
2651+
(``consensus``, ``comment_priorities``); the consensus inner shape
2652+
matches whatever ``self.repness['consensus_comments']`` holds
2653+
(Clojure-style ``agree``/``disagree`` lists)."""
2654+
consensus = {
2655+
'agree': [
2656+
{'tid': 11, 'n-success': 8, 'n-trials': 10,
2657+
'p-success': 0.83, 'p-test': 2.1}
2658+
],
2659+
'disagree': [],
2660+
}
2661+
# Priorities use comment-id keys; the serializer coerces to int when
2662+
# possible and emits int values (see lines 2447-2456 of conversation.py).
2663+
priorities = {7: 1.5, 9: 0.25}
2664+
conv = self._make_conversation_with_repness(consensus, priorities)
2665+
2666+
result = conv.to_dynamo_dict()
2667+
2668+
assert result['consensus'] == consensus, (
2669+
"to_dynamo_dict() must plumb self.repness['consensus_comments'] "
2670+
"into result['consensus']; got " + repr(result['consensus']))
2671+
assert 'comment_priorities' in result, (
2672+
"to_dynamo_dict() must emit 'comment_priorities' when "
2673+
"self.comment_priorities is populated; keys = "
2674+
+ repr(sorted(result.keys())))
2675+
# to_dynamo_dict coerces values to int via int(priority), so 1.5 -> 1
2676+
# and 0.25 -> 0. We assert the post-coercion shape rather than the
2677+
# raw input to lock in what actually lands in DynamoDB.
2678+
assert result['comment_priorities'] == {7: 1, 9: 0}

0 commit comments

Comments
 (0)