Skip to content

Commit abb81b2

Browse files
committed
fix: defensive cohort fallbacks (normalized forge approval, undefined-safe verdict rank + actorCount)
1 parent 835d6dd commit abb81b2

2 files changed

Lines changed: 17 additions & 5 deletions

File tree

src/dashboard/src/components/layout/VerdictModal.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,14 @@ function CohortVerdictDetails({ v }: { v: Record<string, unknown> }) {
3434
const keyDivergence = String(v.keyDivergence || '');
3535
const rankings = (Array.isArray(v.rankings) ? v.rankings : []) as CohortRankingEntry[];
3636
const reasoning = String(v.reasoning || '');
37-
const actorCount = Array.isArray(v.actors) ? (v.actors as unknown[]).length : rankings.length;
37+
// Schema requires `rankings` to carry ≥ 2 entries, but a malformed
38+
// verdict payload could still arrive empty; the floor of 1 keeps the
39+
// kicker from rendering "Cohort of 0" which would never match the
40+
// banner's actor count and looks like a bug to the user.
41+
const actorCount = Math.max(
42+
1,
43+
Array.isArray(v.actors) ? (v.actors as unknown[]).length : rankings.length,
44+
);
3845

3946
return (
4047
<div className={styles.cohortDetails}>
@@ -57,11 +64,11 @@ function CohortVerdictDetails({ v }: { v: Record<string, unknown> }) {
5764
const color = getActorColorVar(r.actorIndex);
5865
return (
5966
<li
60-
key={`${r.rank}-${r.actorName}`}
67+
key={`${r.rank ?? 'unranked'}-${r.actorName}-${r.actorIndex}`}
6168
className={styles.cohortRankingEntry}
6269
style={{ '--actor-color': color } as CSSProperties}
6370
>
64-
<div className={styles.cohortRankBadge}>#{r.rank}</div>
71+
<div className={styles.cohortRankBadge}>#{r.rank ?? '?'}</div>
6572
<div className={styles.cohortRankBody}>
6673
<div className={styles.cohortRankName}>{r.actorName}</div>
6774
<div className={styles.cohortRankScores}>

src/dashboard/src/components/viz/CohortSwarmGrid.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,20 @@ function buildForgeFeedFor(events: ProcessedEvent[]): { attempts: ForgeAttempt[]
8585
for (const evt of events) {
8686
if (evt.type === 'forge_attempt') {
8787
const d = evt.data || {};
88+
// Anthropic SSE payloads sometimes serialize the boolean as the
89+
// string 'true' instead of `true`; the dashboard accepts both.
90+
// Compute the normalized value once and reuse it for both the
91+
// attempts entry and the firstByName lookup so they can't drift.
92+
const isApproved = d.approved === true || d.approved === 'true';
8893
attempts.push({
8994
turn: Number(d.turn ?? 0),
9095
eventIndex: Number(d.eventIndex ?? 0),
9196
department: String(d.department || ''),
9297
name: String(d.name || ''),
93-
approved: d.approved === true || d.approved === 'true',
98+
approved: isApproved,
9499
confidence: typeof d.confidence === 'number' ? d.confidence : undefined,
95100
});
96-
if (d.name && d.department && d.approved === true && !firstByName.has(String(d.name))) {
101+
if (d.name && d.department && isApproved && !firstByName.has(String(d.name))) {
97102
firstByName.set(String(d.name), String(d.department));
98103
}
99104
continue;

0 commit comments

Comments
 (0)