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" }],