From 5c5b1a4ae93553b737d51f945a38ad3362006245 Mon Sep 17 00:00:00 2001 From: Janu Yoga Date: Tue, 4 Aug 2026 11:49:13 +0700 Subject: [PATCH] fix(web): read conclusion level from the field Honcho actually sends Dream cards always showed 0 deductive and 0 inductive. The type inference read `conclusion_type`, which no Honcho version returns; live 3.0.11 sends `level`. Every conclusion therefore hit the "explicit" fallback. Verified against a live instance: 87 explicit, 8 inductive, 5 deductive in the first page of conclusions. `premises` and `reasoning_tree` are still unserved, so the premise tree stays empty until Honcho ships them. Co-Authored-By: Claude Opus 5 (1M context) --- packages/web/e2e/dreams.spec.ts | 140 +++++++++--------- .../web/src/components/dreams/DreamDetail.tsx | 6 + .../web/src/components/dreams/DreamList.tsx | 10 +- .../web/src/components/dreams/PremiseTree.tsx | 6 + packages/web/src/lib/dreams.ts | 26 +++- packages/web/src/test/dreams.test.ts | 31 ++-- 6 files changed, 128 insertions(+), 91 deletions(-) diff --git a/packages/web/e2e/dreams.spec.ts b/packages/web/e2e/dreams.spec.ts index d9c7c54..f3632fe 100644 --- a/packages/web/e2e/dreams.spec.ts +++ b/packages/web/e2e/dreams.spec.ts @@ -21,76 +21,76 @@ test.describe("Dreams route", () => { await context.route( (url) => url.pathname.endsWith("/conclusions/list"), async (route) => { - const now = Date.now(); - const iso = (offsetMs: number) => new Date(now - offsetMs).toISOString(); - const items = [ - // Dream A — burst - { - id: "ind-1", - content: "Alice prefers asynchronous communication", - observer_id: "alice", - observed_id: "bob", - session_id: "sess-1", - created_at: iso(1000), - conclusion_type: "inductive", - reasoning_tree: { - conclusion_id: "ind-1", - premises: [{ conclusion_id: "ded-1" }], + const now = Date.now(); + const iso = (offsetMs: number) => new Date(now - offsetMs).toISOString(); + const items = [ + // Dream A — burst + { + id: "ind-1", + content: "Alice prefers asynchronous communication", + observer_id: "alice", + observed_id: "bob", + session_id: "sess-1", + created_at: iso(1000), + level: "inductive", + reasoning_tree: { + conclusion_id: "ind-1", + premises: [{ conclusion_id: "ded-1" }], + }, }, - }, - { - id: "ded-1", - content: "Alice mentioned email twice and declined two meetings", - observer_id: "alice", - observed_id: "bob", - session_id: "sess-1", - created_at: iso(2000), - conclusion_type: "deductive", - reasoning_tree: { - conclusion_id: "ded-1", - premises: [{ conclusion_id: "exp-1" }, { conclusion_id: "exp-2" }], + { + id: "ded-1", + content: "Alice mentioned email twice and declined two meetings", + observer_id: "alice", + observed_id: "bob", + session_id: "sess-1", + created_at: iso(2000), + level: "deductive", + reasoning_tree: { + conclusion_id: "ded-1", + premises: [{ conclusion_id: "exp-1" }, { conclusion_id: "exp-2" }], + }, }, - }, - { - id: "exp-1", - content: "Alice said 'just email me'", - observer_id: "alice", - observed_id: "bob", - session_id: "sess-1", - created_at: iso(3000), - conclusion_type: "explicit", - }, - { - id: "exp-2", - content: "Alice declined the Tuesday standup", - observer_id: "alice", - observed_id: "bob", - session_id: "sess-1", - created_at: iso(4000), - conclusion_type: "explicit", - }, - // Dream B — 30 minutes ago, different pair → clusters separately - { - id: "ded-2", - content: "Carol responds in the evenings", - observer_id: "carol", - observed_id: "dan", - session_id: "sess-2", - created_at: iso(30 * 60_000), - conclusion_type: "deductive", - }, - ]; - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - items, - total: items.length, - pages: 1, - page: 1, - size: items.length, - }), - }); + { + id: "exp-1", + content: "Alice said 'just email me'", + observer_id: "alice", + observed_id: "bob", + session_id: "sess-1", + created_at: iso(3000), + level: "explicit", + }, + { + id: "exp-2", + content: "Alice declined the Tuesday standup", + observer_id: "alice", + observed_id: "bob", + session_id: "sess-1", + created_at: iso(4000), + level: "explicit", + }, + // Dream B — 30 minutes ago, different pair → clusters separately + { + id: "ded-2", + content: "Carol responds in the evenings", + observer_id: "carol", + observed_id: "dan", + session_id: "sess-2", + created_at: iso(30 * 60_000), + level: "deductive", + }, + ]; + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + items, + total: items.length, + pages: 1, + page: 1, + size: items.length, + }), + }); }, ); }); @@ -115,7 +115,7 @@ test.describe("Dreams route", () => { await page.goto("/workspaces/ws-test/dreams"); // Two dreams: alice→bob burst, and the older carol→dan - const rows = page.locator('button[aria-pressed]'); + const rows = page.locator("button[aria-pressed]"); await expect(rows).toHaveCount(2); // Alice→bob row should show count chips @@ -135,7 +135,7 @@ test.describe("Dreams route", () => { test("expands premise tree for an inductive conclusion", async ({ page }) => { await page.goto("/workspaces/ws-test/dreams"); - await page.locator('button[aria-pressed]').first().click(); + await page.locator("button[aria-pressed]").first().click(); const showPremises = page.getByRole("button", { name: /^Show premises$/i }); await expect(showPremises).toBeVisible(); diff --git a/packages/web/src/components/dreams/DreamDetail.tsx b/packages/web/src/components/dreams/DreamDetail.tsx index db267e8..db5a027 100644 --- a/packages/web/src/components/dreams/DreamDetail.tsx +++ b/packages/web/src/components/dreams/DreamDetail.tsx @@ -34,6 +34,11 @@ const COLUMNS: Array<{ type: ConclusionType; label: string; description: string label: "Inductive", description: "Generalized patterns inferred from deductives", }, + { + type: "contradiction", + label: "Contradiction", + description: "Conflicts found between existing observations", + }, ]; interface DreamDetailProps { @@ -51,6 +56,7 @@ export function DreamDetail({ dream, onClose }: DreamDetailProps) { explicit: [], deductive: [], inductive: [], + contradiction: [], }; for (const c of dream.conclusions) { buckets[inferConclusionType(c)].push(c); diff --git a/packages/web/src/components/dreams/DreamList.tsx b/packages/web/src/components/dreams/DreamList.tsx index fe9beff..0b591cd 100644 --- a/packages/web/src/components/dreams/DreamList.tsx +++ b/packages/web/src/components/dreams/DreamList.tsx @@ -157,6 +157,9 @@ function DreamRow({ dream, active, onSelect }: DreamRowProps) { + {counts.contradiction > 0 && ( + + )} = { @@ -188,6 +191,11 @@ function CountChip({ label, value, kind }: { label: string; value: number; kind: }, accent: { bg: COLOR.accentSubtle, fg: COLOR.accentText, border: COLOR.accentBorder }, warning: { bg: "rgba(245,158,11,0.10)", fg: COLOR.warning, border: COLOR.warningBorder }, + destructive: { + bg: COLOR.destructiveDim, + fg: COLOR.destructive, + border: COLOR.destructiveBorder, + }, }; const cfg = palette[kind]; const dim = value === 0; diff --git a/packages/web/src/components/dreams/PremiseTree.tsx b/packages/web/src/components/dreams/PremiseTree.tsx index 27de2ca..d43daa7 100644 --- a/packages/web/src/components/dreams/PremiseTree.tsx +++ b/packages/web/src/components/dreams/PremiseTree.tsx @@ -27,6 +27,12 @@ const TYPE_BADGE: Record< fg: COLOR.warning, border: COLOR.warningBorder, }, + contradiction: { + label: "contradiction", + bg: COLOR.destructiveDim, + fg: COLOR.destructive, + border: COLOR.destructiveBorder, + }, }; export function ConclusionTypeBadge({ type }: { type: ConclusionType }) { diff --git a/packages/web/src/lib/dreams.ts b/packages/web/src/lib/dreams.ts index 43a6d41..ec7e408 100644 --- a/packages/web/src/lib/dreams.ts +++ b/packages/web/src/lib/dreams.ts @@ -2,20 +2,23 @@ import type { components } from "@/api/schema.d.ts"; type ApiConclusion = components["schemas"]["Conclusion"]; -export type ConclusionType = "explicit" | "deductive" | "inductive"; +export type ConclusionType = "explicit" | "deductive" | "inductive" | "contradiction"; export const CONCLUSION_TYPES: readonly ConclusionType[] = [ "explicit", "deductive", "inductive", + "contradiction", ] as const; -// The generated OpenAPI schema does not yet expose `conclusion_type`, `premises`, or -// `reasoning_tree` (Honcho migration f1a2b3c4d5e6 added the columns but the response -// schema hasn't been regenerated client-side). We declare them as optional here so -// the UI consumes them when present and degrades gracefully when absent. +// The generated OpenAPI schema (Honcho 3.0.5) does not expose `level`, `premises`, or +// `reasoning_tree`, but live Honcho 3.0.11 returns `level` on every conclusion. +// Declared optional here so the UI consumes them when present and degrades gracefully +// when absent. `premises`/`reasoning_tree` are still unserved — the premise tree stays +// empty until Honcho ships them. export type ExtendedConclusion = ApiConclusion & { - conclusion_type?: ConclusionType | null; + /** Widened to `string`: Honcho may add levels this client doesn't know yet. */ + level?: string | null; premises?: string[] | null; reasoning_tree?: ReasoningTreeNode | null; }; @@ -41,6 +44,7 @@ export interface DreamCounts { explicit: number; deductive: number; inductive: number; + contradiction: number; total: number; } @@ -52,11 +56,17 @@ export interface ClusterOptions { const DEFAULT_GAP_MS = 60_000; export function inferConclusionType(c: ExtendedConclusion): ConclusionType { - return c.conclusion_type ?? "explicit"; + return CONCLUSION_TYPES.find((t) => t === c.level) ?? "explicit"; } export function dreamCounts(dream: Pick): DreamCounts { - const counts: DreamCounts = { explicit: 0, deductive: 0, inductive: 0, total: 0 }; + const counts: DreamCounts = { + explicit: 0, + deductive: 0, + inductive: 0, + contradiction: 0, + total: 0, + }; for (const c of dream.conclusions) { counts[inferConclusionType(c)]++; counts.total++; diff --git a/packages/web/src/test/dreams.test.ts b/packages/web/src/test/dreams.test.ts index 89061b3..5163e7b 100644 --- a/packages/web/src/test/dreams.test.ts +++ b/packages/web/src/test/dreams.test.ts @@ -115,21 +115,28 @@ describe("clusterConclusionsIntoDreams", () => { expect(dreams[0].latestMs).toBeGreaterThan(dreams[1].latestMs); }); - it("computes counts by inferred conclusion_type, defaulting unknown to explicit", () => { + it("computes counts by inferred level, defaulting unknown to explicit", () => { const conclusions = [ mkConclusion("c1", iso(0)), - mkConclusion("c2", iso(2), { conclusion_type: "deductive" }), - mkConclusion("c3", iso(4), { conclusion_type: "deductive" }), - mkConclusion("c4", iso(6), { conclusion_type: "inductive" }), + mkConclusion("c2", iso(2), { level: "deductive" }), + mkConclusion("c3", iso(4), { level: "deductive" }), + mkConclusion("c4", iso(6), { level: "inductive" }), ]; const [dream] = clusterConclusionsIntoDreams(conclusions); expect(dreamCounts(dream)).toEqual({ - explicit: 1, // c1 has no type → defaults to explicit + explicit: 1, // c1 has no level → defaults to explicit deductive: 2, inductive: 1, + contradiction: 0, total: 4, }); }); + + it("counts a level this client does not know as explicit rather than dropping it", () => { + const conclusions = [mkConclusion("c1", iso(0), { level: "abductive" })]; + const [dream] = clusterConclusionsIntoDreams(conclusions); + expect(dreamCounts(dream).explicit).toBe(1); + }); }); // Premise tree ──────────────────────────────────────────────────────────────── @@ -144,10 +151,10 @@ describe("expandPremiseTree", () => { }); it("expands a flat premises list to direct children", () => { - const p1 = mkConclusion("p1", iso(0), { conclusion_type: "explicit" }); - const p2 = mkConclusion("p2", iso(1), { conclusion_type: "explicit" }); + const p1 = mkConclusion("p1", iso(0), { level: "explicit" }); + const p2 = mkConclusion("p2", iso(1), { level: "explicit" }); const top = mkConclusion("top", iso(5), { - conclusion_type: "inductive", + level: "inductive", premises: ["p1", "p2"], }); const index = buildPremiseIndex([p1, p2, top]); @@ -158,17 +165,17 @@ describe("expandPremiseTree", () => { }); it("walks a multi-level reasoning_tree recursively", () => { - const e1 = mkConclusion("e1", iso(0), { conclusion_type: "explicit" }); - const e2 = mkConclusion("e2", iso(1), { conclusion_type: "explicit" }); + const e1 = mkConclusion("e1", iso(0), { level: "explicit" }); + const e2 = mkConclusion("e2", iso(1), { level: "explicit" }); const d1 = mkConclusion("d1", iso(2), { - conclusion_type: "deductive", + level: "deductive", reasoning_tree: { conclusion_id: "d1", premises: [{ conclusion_id: "e1" }, { conclusion_id: "e2" }], }, }); const ind = mkConclusion("ind", iso(3), { - conclusion_type: "inductive", + level: "inductive", reasoning_tree: { conclusion_id: "ind", premises: [{ conclusion_id: "d1" }],