From 020c8b8e6c6a19f55f67b53f635ab4d93f2e7559 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Sat, 9 May 2026 09:26:35 +0300 Subject: [PATCH 01/10] =?UTF-8?q?refactor(core):=20rename=20SuspectedCause?= =?UTF-8?q?=20=E2=86=92=20Hypothesis=20+=205-state=20HypothesisStatus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/azure/server.js | 2 +- .../editor/InvestigationWorkspace.tsx | 6 +- .../InvestigationWorkspace.mapwall.test.tsx | 2 +- .../__tests__/ReportView.evidenceMap.test.tsx | 8 +- .../useInvestigationIndexing.test.ts | 2 +- .../src/features/ai/actionToolHandlers.ts | 10 +- .../__tests__/useWallBackgroundJobs.test.ts | 2 +- .../useInvestigationOrchestration.ts | 18 +- apps/azure/src/hooks/useDataIngestion.ts | 5 +- apps/azure/src/pages/Editor.tsx | 2 +- .../src/persistence/AzureHubRepository.ts | 4 +- .../__tests__/AzureHubRepository.read.test.ts | 4 +- .../persistence/__tests__/applyAction.test.ts | 4 +- .../__tests__/investigationSerializer.test.ts | 97 +++-- .../src/services/investigationSerializer.ts | 103 ++--- apps/pwa/src/db/schema.ts | 4 +- .../useInvestigationOrchestration.ts | 18 +- apps/pwa/src/hooks/useDataIngestion.ts | 5 +- apps/pwa/src/persistence/PwaHubRepository.ts | 4 +- .../__tests__/PwaHubRepository.test.ts | 2 +- .../charts/src/EvidenceMap/SynthesisLayer.tsx | 24 +- .../src/InvestigationWall/CommandPalette.tsx | 10 +- .../src/InvestigationWall/HypothesisCard.tsx | 6 +- .../charts/src/InvestigationWall/Minimap.tsx | 4 +- .../src/InvestigationWall/MobileCardList.tsx | 10 +- .../src/InvestigationWall/TributaryFooter.tsx | 6 +- .../src/InvestigationWall/WallCanvas.tsx | 12 +- .../__tests__/CommandPalette.test.tsx | 10 +- .../DraggableHypothesisCard.test.tsx | 4 +- .../__tests__/HypothesisCard.test.tsx | 8 +- .../__tests__/Minimap.test.tsx | 8 +- .../__tests__/MobileCardList.test.tsx | 14 +- .../__tests__/TributaryFooter.test.tsx | 6 +- .../__tests__/WallCanvas.test.tsx | 18 +- .../core/src/__tests__/signalCards.test.ts | 6 +- .../core/src/actions/suspectedCauseActions.ts | 10 +- .../src/ai/__tests__/coScoutContext.test.ts | 34 +- .../src/ai/__tests__/coScoutPhases.test.ts | 4 +- .../src/ai/__tests__/promptTierSafety.test.ts | 2 +- .../ai/__tests__/suggestedQuestions.test.ts | 2 +- .../src/ai/__tests__/toolRegistry.test.ts | 16 +- packages/core/src/ai/actionTools.ts | 2 +- .../critiqueInvestigationState.test.ts | 8 +- .../proposeDisconfirmationMove.test.ts | 6 +- .../ai/actions/critiqueInvestigationState.ts | 4 +- .../ai/actions/proposeDisconfirmationMove.ts | 6 +- packages/core/src/ai/buildAIContext.ts | 8 +- .../prompts/coScout/context/investigation.ts | 18 +- packages/core/src/ai/prompts/coScout/index.ts | 4 +- .../core/src/ai/prompts/coScout/legacy.ts | 84 ++-- .../src/ai/prompts/coScout/modes/defect.ts | 2 +- .../src/ai/prompts/coScout/modes/standard.ts | 6 +- .../src/ai/prompts/coScout/phases/improve.ts | 16 +- .../ai/prompts/coScout/phases/investigate.ts | 18 +- .../coScout/tier2/investigationDiscipline.ts | 4 +- .../src/ai/prompts/coScout/tools/registry.ts | 24 +- packages/core/src/ai/prompts/narration.ts | 2 +- packages/core/src/ai/suggestedQuestions.ts | 2 +- packages/core/src/ai/types.ts | 9 +- packages/core/src/evidenceMap/index.ts | 4 +- .../src/findings/__tests__/helpers.test.ts | 121 +++++- .../src/findings/__tests__/hypothesis.test.ts | 85 ++++ .../hypothesisConditionEvaluator.test.ts | 8 +- .../__tests__/mechanismBranch.test.ts | 10 +- .../__tests__/problemStatement.test.ts | 2 +- .../findings/__tests__/suspectedCause.test.ts | 397 ------------------ packages/core/src/findings/factories.ts | 16 +- packages/core/src/findings/helpers.ts | 60 +-- packages/core/src/findings/hmwPrompts.ts | 2 +- .../core/src/findings/hypothesisCondition.ts | 2 +- .../findings/hypothesisConditionEvaluator.ts | 10 +- packages/core/src/findings/mechanismBranch.ts | 36 +- .../core/src/findings/problemStatement.ts | 16 +- packages/core/src/findings/types.ts | 69 ++- packages/core/src/frame/types.ts | 4 +- packages/core/src/i18n/messages/ar.ts | 2 +- packages/core/src/i18n/messages/bg.ts | 2 +- packages/core/src/i18n/messages/cs.ts | 2 +- packages/core/src/i18n/messages/da.ts | 2 +- packages/core/src/i18n/messages/de.ts | 2 +- packages/core/src/i18n/messages/el.ts | 2 +- packages/core/src/i18n/messages/en.ts | 2 +- packages/core/src/i18n/messages/es.ts | 2 +- packages/core/src/i18n/messages/fi.ts | 2 +- packages/core/src/i18n/messages/fr.ts | 2 +- packages/core/src/i18n/messages/he.ts | 2 +- packages/core/src/i18n/messages/hi.ts | 2 +- packages/core/src/i18n/messages/hr.ts | 2 +- packages/core/src/i18n/messages/hu.ts | 2 +- packages/core/src/i18n/messages/id.ts | 2 +- packages/core/src/i18n/messages/it.ts | 2 +- packages/core/src/i18n/messages/ja.ts | 2 +- packages/core/src/i18n/messages/ko.ts | 2 +- packages/core/src/i18n/messages/ms.ts | 2 +- packages/core/src/i18n/messages/nb.ts | 2 +- packages/core/src/i18n/messages/nl.ts | 2 +- packages/core/src/i18n/messages/pl.ts | 2 +- packages/core/src/i18n/messages/pt.ts | 2 +- packages/core/src/i18n/messages/ro.ts | 2 +- packages/core/src/i18n/messages/sk.ts | 2 +- packages/core/src/i18n/messages/sv.ts | 2 +- packages/core/src/i18n/messages/th.ts | 2 +- packages/core/src/i18n/messages/tr.ts | 2 +- packages/core/src/i18n/messages/uk.ts | 2 +- packages/core/src/i18n/messages/vi.ts | 2 +- packages/core/src/i18n/messages/zhHans.ts | 2 +- packages/core/src/i18n/messages/zhHant.ts | 2 +- packages/core/src/i18n/types.ts | 2 +- packages/core/src/index.ts | 11 +- .../core/src/persistence/HubRepository.ts | 10 +- packages/core/src/persistence/index.ts | 2 +- .../core/src/survey/__tests__/survey.test.ts | 57 ++- packages/core/src/survey/evaluator.ts | 15 +- packages/core/src/survey/types.ts | 4 +- .../src/samples/investigation-showcase.ts | 6 +- .../data/src/samples/syringe-barrel-weight.ts | 6 +- packages/data/src/types.ts | 2 +- .../useCanvasInvestigationOverlays.test.ts | 6 +- .../__tests__/useCurrentUnderstanding.test.ts | 8 +- .../useHasInvestigationContent.test.ts | 6 +- .../src/__tests__/useHubComputations.test.ts | 6 +- .../src/__tests__/useSharedWallProps.test.ts | 8 +- .../src/__tests__/useSuspectedCauses.test.ts | 82 ++-- packages/hooks/src/index.ts | 4 +- packages/hooks/src/types.ts | 6 +- packages/hooks/src/useAIContext.ts | 4 +- .../src/useCanvasInvestigationOverlays.ts | 16 +- packages/hooks/src/useCurrentUnderstanding.ts | 16 +- packages/hooks/src/useDataIngestion.ts | 6 +- packages/hooks/src/useEvidenceMapData.ts | 6 +- packages/hooks/src/useEvidenceMapTimeline.ts | 8 +- packages/hooks/src/useHubCommentStream.ts | 2 +- packages/hooks/src/useHubComputations.ts | 10 +- packages/hooks/src/useSharedWallProps.ts | 4 +- packages/hooks/src/useSuspectedCauses.ts | 57 ++- .../src/__tests__/investigationStore.test.ts | 14 +- .../src/__tests__/wallSelectors.test.ts | 30 +- packages/stores/src/investigationStore.ts | 38 +- packages/stores/src/projectStore.ts | 4 +- packages/stores/src/wallSelectors.ts | 10 +- .../src/components/Canvas/CanvasWorkspace.tsx | 4 +- .../Canvas/__tests__/Canvas.test.tsx | 2 +- .../Canvas/internal/StepNodeMarker.tsx | 8 +- .../__tests__/CanvasStepCard.test.tsx | 2 +- .../__tests__/CanvasWallOverlay.test.tsx | 8 +- .../__tests__/StepNodeMarker.test.tsx | 2 +- .../CoScoutPanel/ActionProposalCard.tsx | 4 +- .../InvestigationConclusion.tsx | 8 +- .../InvestigationConclusion/HubCard.tsx | 14 +- .../InvestigationConclusion/HubComposer.tsx | 6 +- .../__tests__/HubCard.test.tsx | 8 +- .../__tests__/HubComposer.test.tsx | 10 +- .../ConclusionCard.tsx | 16 +- .../QuestionsTabView.tsx | 14 +- .../__tests__/ConclusionCard.test.tsx | 8 +- .../__tests__/QuestionsTabView.test.tsx | 6 +- .../ProcessIntelligencePanel/index.ts | 2 +- .../ReportView/ReportInvestigationSummary.tsx | 4 +- packages/ui/src/index.ts | 2 +- 159 files changed, 1048 insertions(+), 1191 deletions(-) create mode 100644 packages/core/src/findings/__tests__/hypothesis.test.ts delete mode 100644 packages/core/src/findings/__tests__/suspectedCause.test.ts diff --git a/apps/azure/server.js b/apps/azure/server.js index f9db08986..a1749c8e4 100644 --- a/apps/azure/server.js +++ b/apps/azure/server.js @@ -655,7 +655,7 @@ app.get('/api/brainstorm/active', (req, res) => { // ─── Hub Comments (Investigation Wall — per-hub SSE collaboration) ─────────── // -// Mirrors the brainstorm SSE pattern above. Each hypothesis hub (SuspectedCause) +// Mirrors the brainstorm SSE pattern above. Each hypothesis hub (Hypothesis) // on the Investigation Wall can host a live comment thread. Clients subscribe // per `${projectId}:${hubId}` and receive `init` + `comment` events. Storage is // in-memory (ephemeral, per-pod) with a 24h TTL — customer-owned persistence diff --git a/apps/azure/src/components/editor/InvestigationWorkspace.tsx b/apps/azure/src/components/editor/InvestigationWorkspace.tsx index cfaf6149d..ab2ffc40f 100644 --- a/apps/azure/src/components/editor/InvestigationWorkspace.tsx +++ b/apps/azure/src/components/editor/InvestigationWorkspace.tsx @@ -99,7 +99,7 @@ interface InvestigationWorkspaceProps { handleSearchKnowledge: () => void; // Column aliases columnAliases: Record; - // Hub model (SuspectedCause CRUD from useInvestigationOrchestration) + // Hub model (Hypothesis CRUD from useInvestigationOrchestration) suspectedCausesState: UseInvestigationOrchestrationReturn['suspectedCausesState']; // Derived investigation data (from orchestration hook) questionsMap: Record; @@ -345,7 +345,7 @@ export const InvestigationWorkspace: React.FC = ({ }; }, [factorIntelQuestions]); - // ── Hub model computations (SuspectedCause hubs) ─────────────────────── + // ── Hub model computations (Hypothesis hubs) ─────────────────────── const hubs = suspectedCausesState.hubs; // Phase 13 — pan-to-node: replicate WallCanvas's deterministic layout so the @@ -457,7 +457,7 @@ export const InvestigationWorkspace: React.FC = ({ }, problemStatement, questions: questionsState.questions, - suspectedCauseHubs: hubs, + hypothesisHubs: hubs, onCurrentUnderstandingChange: handleCurrentUnderstandingChange, }); diff --git a/apps/azure/src/components/editor/__tests__/InvestigationWorkspace.mapwall.test.tsx b/apps/azure/src/components/editor/__tests__/InvestigationWorkspace.mapwall.test.tsx index 2c4d171fe..b868f4647 100644 --- a/apps/azure/src/components/editor/__tests__/InvestigationWorkspace.mapwall.test.tsx +++ b/apps/azure/src/components/editor/__tests__/InvestigationWorkspace.mapwall.test.tsx @@ -298,7 +298,7 @@ describe('InvestigationWorkspace Map/Wall toggle', () => { synthesis: '', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: '', updatedAt: '', }, diff --git a/apps/azure/src/components/views/__tests__/ReportView.evidenceMap.test.tsx b/apps/azure/src/components/views/__tests__/ReportView.evidenceMap.test.tsx index 84162a1cd..2ab1b2285 100644 --- a/apps/azure/src/components/views/__tests__/ReportView.evidenceMap.test.tsx +++ b/apps/azure/src/components/views/__tests__/ReportView.evidenceMap.test.tsx @@ -9,7 +9,7 @@ import { describe, it, expect } from 'vitest'; import { renderHook, act } from '@testing-library/react'; import { useEvidenceMapTimeline } from '@variscout/hooks'; -import type { CausalLink, Finding, Question, SuspectedCause } from '@variscout/core/findings'; +import type { CausalLink, Finding, Question, Hypothesis } from '@variscout/core/findings'; // ============================================================================ // Test helpers @@ -73,12 +73,12 @@ function makeQuestion(overrides?: Partial): Question { } as Question; } -function makeSuspectedCause(overrides?: Partial): SuspectedCause { +function makeSuspectedCause(overrides?: Partial): Hypothesis { return { id: 'hub-1', name: 'Temperature hypothesis', synthesis: '', - status: 'suspected', + status: 'proposed', questionIds: [], findingIds: [], createdAt: Date.parse('2026-03-17T12:00:00.000Z'), @@ -295,7 +295,7 @@ describe('useEvidenceMapTimeline', () => { }); describe('frame content with mixed artifacts', () => { - it('includes hub ID in visibleHubs when SuspectedCause is provided', () => { + it('includes hub ID in visibleHubs when Hypothesis is provided', () => { const hub = makeSuspectedCause({ id: 'hub-xyz' }); const { result } = renderHook(() => useEvidenceMapTimeline({ suspectedCauses: [hub] })); diff --git a/apps/azure/src/features/ai/__tests__/useInvestigationIndexing.test.ts b/apps/azure/src/features/ai/__tests__/useInvestigationIndexing.test.ts index d4e29e56d..5f2363175 100644 --- a/apps/azure/src/features/ai/__tests__/useInvestigationIndexing.test.ts +++ b/apps/azure/src/features/ai/__tests__/useInvestigationIndexing.test.ts @@ -18,7 +18,7 @@ const mockOnSuspectedCausesChange = vi.fn(); const mockSerializerInstance = { onFindingsChange: mockOnFindingsChange, onQuestionsChange: mockOnQuestionsChange, - onSuspectedCausesChange: mockOnSuspectedCausesChange, + onHypothesesChange: mockOnSuspectedCausesChange, dispose: mockDispose, }; diff --git a/apps/azure/src/features/ai/actionToolHandlers.ts b/apps/azure/src/features/ai/actionToolHandlers.ts index 123880c53..192688f99 100644 --- a/apps/azure/src/features/ai/actionToolHandlers.ts +++ b/apps/azure/src/features/ai/actionToolHandlers.ts @@ -12,7 +12,7 @@ import type { Question, FilterAction, ActionProposal, - SuspectedCause, + Hypothesis, CausalLink, } from '@variscout/core'; import { @@ -35,7 +35,7 @@ export interface ActionToolDeps { filters: Record; filterStack: FilterAction[]; /** Existing suspected cause hubs — used by connect_hub_evidence handler */ - suspectedCauses?: SuspectedCause[]; + suspectedCauses?: Hypothesis[]; /** Existing causal links — used by suggest_causal_link handler */ causalLinks?: CausalLink[]; /** Available factor column names — used for validation */ @@ -338,7 +338,7 @@ export function buildActionToolHandlers({ return JSON.stringify({ proposal: true, ...proposal }); }, - suggest_suspected_cause: async (args: Record) => { + suggest_hypothesis: async (args: Record) => { const name = args.name as string; const synthesis = args.synthesis as string; const questionIds = args.questionIds as string[]; @@ -363,14 +363,14 @@ export function buildActionToolHandlers({ const proposal: ActionProposal = { id: generateProposalId(), - tool: 'suggest_suspected_cause', + tool: 'suggest_hypothesis', params: { name, synthesis, questionIds, findingIds }, preview: { name, synthesis, questionCount: questionIds.length, findingCount: findingIds.length, - previewText: `Create suspected cause: "${name}"\nConnecting ${questionIds.length} question${questionIds.length !== 1 ? 's' : ''} + ${findingIds.length} finding${findingIds.length !== 1 ? 's' : ''}`, + previewText: `Create hypothesis: "${name}"\nConnecting ${questionIds.length} question${questionIds.length !== 1 ? 's' : ''} + ${findingIds.length} finding${findingIds.length !== 1 ? 's' : ''}`, }, status: 'pending', filterStackHash: hashFilterStack(filterStack), diff --git a/apps/azure/src/features/investigation/__tests__/useWallBackgroundJobs.test.ts b/apps/azure/src/features/investigation/__tests__/useWallBackgroundJobs.test.ts index 4c4c7ee87..4e6e1f9c4 100644 --- a/apps/azure/src/features/investigation/__tests__/useWallBackgroundJobs.test.ts +++ b/apps/azure/src/features/investigation/__tests__/useWallBackgroundJobs.test.ts @@ -179,7 +179,7 @@ describe('useWallBackgroundJobs (azure)', () => { synthesis: '', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: Date.parse('2026-04-19T00:00:00Z'), updatedAt: Date.parse('2026-04-19T00:00:00Z'), deletedAt: null, diff --git a/apps/azure/src/features/investigation/useInvestigationOrchestration.ts b/apps/azure/src/features/investigation/useInvestigationOrchestration.ts index 909451f0d..9ca31daad 100644 --- a/apps/azure/src/features/investigation/useInvestigationOrchestration.ts +++ b/apps/azure/src/features/investigation/useInvestigationOrchestration.ts @@ -14,7 +14,7 @@ import { } from './investigationStore'; import type { QuestionDisplayData } from './investigationStore'; import { usePanelsStore } from '../panels/panelsStore'; -import { useSuspectedCauses, type SuspectedCauseUpdate } from '@variscout/hooks'; +import { useSuspectedCauses, type HypothesisUpdate } from '@variscout/hooks'; import { useInvestigationStore } from '@variscout/stores'; import type { Finding, @@ -23,7 +23,7 @@ import type { IdeaImpact, ProcessContext, StatsResult, - SuspectedCause, + Hypothesis, } from '@variscout/core'; import type { UseQuestionsReturn } from '@variscout/hooks'; @@ -56,18 +56,18 @@ export interface UseInvestigationOrchestrationReturn { handleSetFindingStatus: (id: string, status: FindingStatus) => void; /** Full suspected causes hook state — hub CRUD operations for the Investigation workspace */ suspectedCausesState: { - hubs: SuspectedCause[]; - createHub: (name: string, synthesis: string) => SuspectedCause; - updateHub: (hubId: string, updates: SuspectedCauseUpdate) => void; + hubs: Hypothesis[]; + createHub: (name: string, synthesis: string) => Hypothesis; + updateHub: (hubId: string, updates: HypothesisUpdate) => void; deleteHub: (hubId: string) => void; - resetHubs: (newHubs: SuspectedCause[]) => void; + resetHubs: (newHubs: Hypothesis[]) => void; connectQuestion: (hubId: string, questionId: string) => void; disconnectQuestion: (hubId: string, questionId: string) => void; connectFinding: (hubId: string, findingId: string) => void; disconnectFinding: (hubId: string, findingId: string) => void; - setHubStatus: (hubId: string, status: SuspectedCause['status']) => void; - getHubForQuestion: (questionId: string) => SuspectedCause | undefined; - getHubForFinding: (findingId: string) => SuspectedCause | undefined; + setHubStatus: (hubId: string, status: Hypothesis['status']) => void; + getHubForQuestion: (questionId: string) => Hypothesis | undefined; + getHubForFinding: (findingId: string) => Hypothesis | undefined; }; /** Map of question ID to display data for FindingCard */ questionsMap: Record; diff --git a/apps/azure/src/hooks/useDataIngestion.ts b/apps/azure/src/hooks/useDataIngestion.ts index b52535469..e7b096a2e 100644 --- a/apps/azure/src/hooks/useDataIngestion.ts +++ b/apps/azure/src/hooks/useDataIngestion.ts @@ -8,7 +8,7 @@ import { useIsMobile } from '@variscout/ui'; import { useProjectStore, useInvestigationStore } from '@variscout/stores'; -import type { SuspectedCause, CausalLink } from '@variscout/core'; +import type { Hypothesis, CausalLink } from '@variscout/core'; import { useDataIngestion as useDataIngestionBase, type UseDataIngestionOptions, @@ -44,8 +44,7 @@ export const useDataIngestion = (options?: UseDataIngestionOptions) => { setQuestions: useProjectStore(s => s.setQuestions), setCategories: useProjectStore(s => s.setCategories), setDefectMapping: useProjectStore(s => s.setDefectMapping), - setSuspectedCauses: (hubs: SuspectedCause[]) => - useInvestigationStore.getState().resetHubs(hubs), + setSuspectedCauses: (hubs: Hypothesis[]) => useInvestigationStore.getState().resetHubs(hubs), setCausalLinks: (links: CausalLink[]) => useInvestigationStore.getState().loadInvestigationState({ causalLinks: links }), setProcessContext: useProjectStore(s => s.setProcessContext), diff --git a/apps/azure/src/pages/Editor.tsx b/apps/azure/src/pages/Editor.tsx index 7d9b192d3..7f9a7432a 100644 --- a/apps/azure/src/pages/Editor.tsx +++ b/apps/azure/src/pages/Editor.tsx @@ -875,7 +875,7 @@ export const Editor: React.FC = ({ setQuestionLinkPromptOpen(false); }, []); - // Wall-variant propose-hypothesis CTA — creates a new SuspectedCause hub seeded + // Wall-variant propose-hypothesis CTA — creates a new Hypothesis hub seeded // from the finding and links the finding as the first piece of evidence. const wallViewMode = useWallLayoutStore(s => s.viewMode); const createHubFromFinding = useInvestigationStore(s => s.createHubFromFinding); diff --git a/apps/azure/src/persistence/AzureHubRepository.ts b/apps/azure/src/persistence/AzureHubRepository.ts index 95c7b52f0..21506dbd6 100644 --- a/apps/azure/src/persistence/AzureHubRepository.ts +++ b/apps/azure/src/persistence/AzureHubRepository.ts @@ -17,7 +17,7 @@ import type { FindingReadAPI, QuestionReadAPI, CausalLinkReadAPI, - SuspectedCauseReadAPI, + HypothesisReadAPI, CanvasStateReadAPI, } from '@variscout/core/persistence'; import type { HubAction } from '@variscout/core/actions'; @@ -173,7 +173,7 @@ export class AzureHubRepository implements HubRepository { }, }; - suspectedCauses: SuspectedCauseReadAPI = { + hypotheses: HypothesisReadAPI = { // Azure has no dedicated suspectedCauses table today; F3 normalizes this. async get(_id) { return undefined; diff --git a/apps/azure/src/persistence/__tests__/AzureHubRepository.read.test.ts b/apps/azure/src/persistence/__tests__/AzureHubRepository.read.test.ts index caa529a36..ad58252e8 100644 --- a/apps/azure/src/persistence/__tests__/AzureHubRepository.read.test.ts +++ b/apps/azure/src/persistence/__tests__/AzureHubRepository.read.test.ts @@ -395,11 +395,11 @@ describe('AzureHubRepository read APIs (Dexie tables)', () => { }); it('suspectedCauses.get returns undefined', async () => { - expect(await repo.suspectedCauses.get('any')).toBeUndefined(); + expect(await repo.hypotheses.get('any')).toBeUndefined(); }); it('suspectedCauses.listByInvestigation returns []', async () => { - expect(await repo.suspectedCauses.listByInvestigation('inv-1')).toEqual([]); + expect(await repo.hypotheses.listByInvestigation('inv-1')).toEqual([]); }); }); }); diff --git a/apps/azure/src/persistence/__tests__/applyAction.test.ts b/apps/azure/src/persistence/__tests__/applyAction.test.ts index cb7b37686..a713eef12 100644 --- a/apps/azure/src/persistence/__tests__/applyAction.test.ts +++ b/apps/azure/src/persistence/__tests__/applyAction.test.ts @@ -43,7 +43,7 @@ import type { HubAction } from '@variscout/core/actions'; // Exhaustiveness test: build a minimal stub for each no-op session-only action kind. // These are passed to handlers that do nothing with the payload; shape validity is // enforced at the type level but the exhaustiveness suite uses `as HubAction` casts -// to avoid duplicating complex Finding/Question/CausalLink/SuspectedCause fixtures. +// to avoid duplicating complex Finding/Question/CausalLink/Hypothesis fixtures. // Real round-trip tests (OUTCOME_*, EVIDENCE_*) use fully typed fixtures below. // --------------------------------------------------------------------------- @@ -524,7 +524,7 @@ describe('applyAction — EVIDENCE_SOURCE_REMOVE', () => { describe('applyAction — session-only no-ops', () => { // Session-only no-op tests use `as HubAction` casts for complex payloads - // (Finding, Question, CausalLink, SuspectedCause) because the handlers consume + // (Finding, Question, CausalLink, Hypothesis) because the handlers consume // nothing from the payload — they are structural no-ops. Full type-safety is // enforced at real call sites; cast only in tests for session-only no-ops. diff --git a/apps/azure/src/services/__tests__/investigationSerializer.test.ts b/apps/azure/src/services/__tests__/investigationSerializer.test.ts index c0547dbdd..403528911 100644 --- a/apps/azure/src/services/__tests__/investigationSerializer.test.ts +++ b/apps/azure/src/services/__tests__/investigationSerializer.test.ts @@ -2,12 +2,12 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { serializeFindings, serializeQuestions, - serializeSuspectedCauses, + serializeHypotheses, serializeInvestigationState, deserializeInvestigationState, createInvestigationSerializer, } from '../investigationSerializer'; -import type { Finding, Question, SuspectedCause } from '@variscout/core'; +import type { Finding, Question, Hypothesis } from '@variscout/core'; // --------------------------------------------------------------------------- // Fixtures @@ -41,7 +41,7 @@ function makeQuestion(overrides: Partial = {}): Question { }; } -function makeSuspectedCause(overrides: Partial = {}): SuspectedCause { +function makeSuspectedCause(overrides: Partial = {}): Hypothesis { return { id: 'sc-1', name: 'Nozzle wear on night shift', @@ -56,7 +56,7 @@ function makeSuspectedCause(overrides: Partial = {}): SuspectedC description: 'Explains 62% of variation', }, }, - status: 'suspected', + status: 'proposed', createdAt: 1704067200000, // 2024-01-01T00:00:00.000Z updatedAt: 1704153600000, // 2024-01-02T00:00:00.000Z deletedAt: null, @@ -270,13 +270,13 @@ describe('serializeQuestions', () => { }); // --------------------------------------------------------------------------- -// serializeSuspectedCauses +// serializeHypotheses // --------------------------------------------------------------------------- -describe('serializeSuspectedCauses', () => { +describe('serializeHypotheses', () => { it('produces valid JSONL — one JSON object per line', () => { const hubs = [makeSuspectedCause({ id: 'sc-1' }), makeSuspectedCause({ id: 'sc-2' })]; - const jsonl = serializeSuspectedCauses(hubs); + const jsonl = serializeHypotheses(hubs); const lines = jsonl.split('\n'); expect(lines).toHaveLength(2); lines.forEach(line => { @@ -284,10 +284,10 @@ describe('serializeSuspectedCauses', () => { }); }); - it('sets type to "suspected-cause"', () => { - const jsonl = serializeSuspectedCauses([makeSuspectedCause()]); + it('sets type to "hypothesis"', () => { + const jsonl = serializeHypotheses([makeSuspectedCause()]); const parsed = JSON.parse(jsonl); - expect(parsed.type).toBe('suspected-cause'); + expect(parsed.type).toBe('hypothesis'); }); it('includes all key hub fields', () => { @@ -303,7 +303,7 @@ describe('serializeSuspectedCauses', () => { evidence, status: 'confirmed', }); - const parsed = JSON.parse(serializeSuspectedCauses([hub])); + const parsed = JSON.parse(serializeHypotheses([hub])); expect(parsed.name).toBe('Nozzle wear on night shift'); expect(parsed.synthesis).toBe('Both factors confirm the pattern.'); expect(parsed.questionIds).toEqual(['q-1', 'q-2']); @@ -314,23 +314,23 @@ describe('serializeSuspectedCauses', () => { it('includes selectedForImprovement when set', () => { const hub = makeSuspectedCause({ status: 'confirmed', selectedForImprovement: true }); - const parsed = JSON.parse(serializeSuspectedCauses([hub])); + const parsed = JSON.parse(serializeHypotheses([hub])); expect(parsed.selectedForImprovement).toBe(true); }); it('omits selectedForImprovement when undefined', () => { const hub = makeSuspectedCause({ status: 'confirmed', selectedForImprovement: undefined }); - const parsed = JSON.parse(serializeSuspectedCauses([hub])); + const parsed = JSON.parse(serializeHypotheses([hub])); expect(parsed.selectedForImprovement).toBeUndefined(); }); - it('excludes not-confirmed hubs from Foundry IQ output', () => { + it('excludes refuted hubs from Foundry IQ output', () => { const hubs = [ - makeSuspectedCause({ id: 'sc-1', status: 'suspected' }), + makeSuspectedCause({ id: 'sc-1', status: 'proposed' }), makeSuspectedCause({ id: 'sc-2', status: 'confirmed' }), - makeSuspectedCause({ id: 'sc-3', status: 'not-confirmed' }), + makeSuspectedCause({ id: 'sc-3', status: 'refuted' }), ]; - const jsonl = serializeSuspectedCauses(hubs); + const jsonl = serializeHypotheses(hubs); const lines = jsonl.split('\n'); expect(lines).toHaveLength(2); const ids = lines.map(l => JSON.parse(l).id); @@ -340,15 +340,15 @@ describe('serializeSuspectedCauses', () => { }); it('returns empty string for empty hubs array', () => { - expect(serializeSuspectedCauses([])).toBe(''); + expect(serializeHypotheses([])).toBe(''); }); - it('returns empty string when all hubs are not-confirmed', () => { + it('returns empty string when all hubs are refuted', () => { const hubs = [ - makeSuspectedCause({ status: 'not-confirmed' }), - makeSuspectedCause({ id: 'sc-2', status: 'not-confirmed' }), + makeSuspectedCause({ status: 'refuted' }), + makeSuspectedCause({ id: 'sc-2', status: 'refuted' }), ]; - expect(serializeSuspectedCauses(hubs)).toBe(''); + expect(serializeHypotheses(hubs)).toBe(''); }); }); @@ -439,7 +439,7 @@ describe('deserializeInvestigationState', () => { expect(result.suspectedCauses).toEqual([]); }); - it('migrates legacy totalContribution (number) to SuspectedCauseEvidence on load', () => { + it('migrates legacy totalContribution (number) to HypothesisEvidence on load', () => { const raw = { findings: [], questions: [], @@ -451,7 +451,7 @@ describe('deserializeInvestigationState', () => { questionIds: [], findingIds: [], totalContribution: 0.52, - status: 'suspected', + status: 'proposed', createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', }, @@ -473,6 +473,32 @@ describe('deserializeInvestigationState', () => { }); }); + it('migrates legacy hypothesis status values on load', () => { + const raw = { + findings: [], + questions: [], + suspectedCauses: [ + { + ...makeSuspectedCause({ id: 'legacy-suspected' }), + status: 'suspected', + }, + { + ...makeSuspectedCause({ id: 'legacy-not-confirmed' }), + status: 'not-confirmed', + }, + ], + }; + + const result = deserializeInvestigationState( + raw as unknown as import('../investigationSerializer').SerializedInvestigationState + ); + + expect(result.suspectedCauses.map(hub => [hub.id, hub.status])).toEqual([ + ['legacy-suspected', 'proposed'], + ['legacy-not-confirmed', 'refuted'], + ]); + }); + it('does not overwrite existing evidence when both totalContribution and evidence are present', () => { const existingEvidence = { mode: 'capability' as const, @@ -490,7 +516,7 @@ describe('deserializeInvestigationState', () => { findingIds: [], totalContribution: 0.52, evidence: existingEvidence, - status: 'suspected', + status: 'proposed', createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', }, @@ -626,9 +652,9 @@ describe('createInvestigationSerializer', () => { const serializer = createInvestigationSerializer({ projectId: 'proj-sc', uploadBlob }); const hubs = [makeSuspectedCause()]; - serializer.onSuspectedCausesChange(hubs); - serializer.onSuspectedCausesChange(hubs); - serializer.onSuspectedCausesChange(hubs); + serializer.onHypothesesChange(hubs); + serializer.onHypothesesChange(hubs); + serializer.onHypothesesChange(hubs); expect(uploadBlob).not.toHaveBeenCalled(); @@ -636,7 +662,7 @@ describe('createInvestigationSerializer', () => { expect(uploadBlob).toHaveBeenCalledTimes(1); expect(uploadBlob).toHaveBeenCalledWith( - 'proj-sc/investigation/suspected-causes.jsonl', + 'proj-sc/investigation/hypotheses.jsonl', expect.any(String) ); serializer.dispose(); @@ -675,13 +701,10 @@ describe('createInvestigationSerializer', () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const serializer = createInvestigationSerializer({ projectId: 'proj-sc-err', uploadBlob }); - serializer.onSuspectedCausesChange([makeSuspectedCause()]); + serializer.onHypothesesChange([makeSuspectedCause()]); await expect(vi.runAllTimersAsync()).resolves.not.toThrow(); - expect(warnSpy).toHaveBeenCalledWith( - '[KB] Failed to serialize suspected causes:', - expect.any(Error) - ); + expect(warnSpy).toHaveBeenCalledWith('[KB] Failed to serialize hypotheses:', expect.any(Error)); warnSpy.mockRestore(); serializer.dispose(); @@ -715,7 +738,7 @@ describe('createInvestigationSerializer', () => { const uploadBlob = vi.fn().mockResolvedValue(undefined); const serializer = createInvestigationSerializer({ projectId: 'proj-sc-dispose', uploadBlob }); - serializer.onSuspectedCausesChange([makeSuspectedCause()]); + serializer.onHypothesesChange([makeSuspectedCause()]); serializer.dispose(); await vi.runAllTimersAsync(); @@ -745,7 +768,7 @@ describe('createInvestigationSerializer', () => { serializer.onFindingsChange([makeFinding()]); serializer.onQuestionsChange([makeQuestion()]); - serializer.onSuspectedCausesChange([makeSuspectedCause()]); + serializer.onHypothesesChange([makeSuspectedCause()]); await vi.runAllTimersAsync(); @@ -753,7 +776,7 @@ describe('createInvestigationSerializer', () => { const paths = uploadBlob.mock.calls.map(([p]) => p); expect(paths).toContain('proj-8/investigation/findings.jsonl'); expect(paths).toContain('proj-8/investigation/questions.jsonl'); - expect(paths).toContain('proj-8/investigation/suspected-causes.jsonl'); + expect(paths).toContain('proj-8/investigation/hypotheses.jsonl'); serializer.dispose(); }); }); diff --git a/apps/azure/src/services/investigationSerializer.ts b/apps/azure/src/services/investigationSerializer.ts index aaba1e229..f7d1e6f1a 100644 --- a/apps/azure/src/services/investigationSerializer.ts +++ b/apps/azure/src/services/investigationSerializer.ts @@ -1,4 +1,10 @@ -import type { Finding, Question, SuspectedCause, SuspectedCauseEvidence } from '@variscout/core'; +import type { + Finding, + Question, + Hypothesis, + HypothesisEvidence, + HypothesisStatus, +} from '@variscout/core'; import { migrateCauseRolesToHubs } from '@variscout/core'; // --------------------------------------------------------------------------- @@ -13,17 +19,24 @@ import { migrateCauseRolesToHubs } from '@variscout/core'; export interface SerializedInvestigationState { findings: Finding[]; questions: Question[]; - suspectedCauses?: SuspectedCause[]; + suspectedCauses?: Hypothesis[]; } /** * Shape of a suspected cause as it may appear in legacy stored data, * before the `totalContribution` → `evidence` migration. */ -interface LegacyStoredHub extends Omit { - // Old numeric field, present before SuspectedCauseEvidence was introduced +interface LegacyStoredHub extends Omit { + // Old numeric field, present before HypothesisEvidence was introduced totalContribution?: number; - evidence?: SuspectedCauseEvidence; + evidence?: HypothesisEvidence; + status: HypothesisStatus | 'suspected' | 'not-confirmed'; +} + +function normalizeHypothesisStatus(status: LegacyStoredHub['status']): HypothesisStatus { + if (status === 'suspected') return 'proposed'; + if (status === 'not-confirmed') return 'refuted'; + return status; } /** @@ -33,7 +46,7 @@ interface LegacyStoredHub extends Omit { export function serializeInvestigationState( findings: Finding[], questions: Question[], - suspectedCauses: SuspectedCause[] + suspectedCauses: Hypothesis[] ): SerializedInvestigationState { const state: SerializedInvestigationState = { findings, questions }; if (suspectedCauses.length > 0) { @@ -47,45 +60,44 @@ export function serializeInvestigationState( * * Migrations applied on load: * 1. If `suspectedCauses` is absent but questions have `causeRole === 'suspected-cause'`, - * those questions are migrated into individual SuspectedCause hubs. + * those questions are migrated into individual Hypothesis hubs. * 2. If a hub has `totalContribution` (legacy numeric field) but no `evidence`, - * a basic `SuspectedCauseEvidence` is synthesised from it. + * a basic `HypothesisEvidence` is synthesised from it. * 3. `selectedForImprovement` defaults to `undefined` when absent. */ export function deserializeInvestigationState(raw: SerializedInvestigationState): { findings: Finding[]; questions: Question[]; - suspectedCauses: SuspectedCause[]; + suspectedCauses: Hypothesis[]; } { const findings = raw.findings ?? []; const questions = raw.questions ?? []; if (raw.suspectedCauses !== undefined) { // Data already has hubs — apply field-level migration then return - const migratedHubs = (raw.suspectedCauses as LegacyStoredHub[]).map( - (stored): SuspectedCause => { - const { totalContribution: _legacy, ...clean } = stored; - const hub: SuspectedCause = { - ...clean, - evidence: stored.evidence, - selectedForImprovement: stored.selectedForImprovement, + const migratedHubs = (raw.suspectedCauses as LegacyStoredHub[]).map((stored): Hypothesis => { + const { totalContribution: _legacy, ...clean } = stored; + const hub: Hypothesis = { + ...clean, + status: normalizeHypothesisStatus(stored.status), + evidence: stored.evidence, + selectedForImprovement: stored.selectedForImprovement, + }; + + // Migrate legacy totalContribution (number) → HypothesisEvidence + if (stored.totalContribution != null && !stored.evidence) { + hub.evidence = { + mode: 'standard', + contribution: { + value: stored.totalContribution, + label: 'R²adj', + description: `Explains ${Math.round(stored.totalContribution * 100)}% of variation`, + }, }; - - // Migrate legacy totalContribution (number) → SuspectedCauseEvidence - if (stored.totalContribution != null && !stored.evidence) { - hub.evidence = { - mode: 'standard', - contribution: { - value: stored.totalContribution, - label: 'R²adj', - description: `Explains ${Math.round(stored.totalContribution * 100)}% of variation`, - }, - }; - } - - return hub; } - ); + + return hub; + }); return { findings, questions, suspectedCauses: migratedHubs }; } @@ -156,16 +168,16 @@ export function serializeQuestions(questions: Question[]): string { } /** - * Serialize suspected cause hubs to JSONL for Foundry IQ indexing. - * Only confirmed and suspected hubs are included; not-confirmed hubs are skipped. + * serialize hypothesis hubs to JSONL for Foundry IQ indexing. + * Only non-refuted hypotheses are included; refuted hypotheses are skipped. */ -export function serializeSuspectedCauses(hubs: SuspectedCause[]): string { +export function serializeHypotheses(hubs: Hypothesis[]): string { return hubs - .filter(h => h.status !== 'not-confirmed') + .filter(h => h.status !== 'refuted') .map(h => JSON.stringify({ id: h.id, - type: 'suspected-cause', + type: 'hypothesis', name: h.name, synthesis: h.synthesis, status: h.status, @@ -192,7 +204,7 @@ interface SerializerOptions { export function createInvestigationSerializer(options: SerializerOptions) { let findingsTimer: ReturnType | null = null; let questionsTimer: ReturnType | null = null; - let suspectedCausesTimer: ReturnType | null = null; + let hypothesesTimer: ReturnType | null = null; const DEBOUNCE_MS = 5000; return { @@ -220,17 +232,14 @@ export function createInvestigationSerializer(options: SerializerOptions) { }, DEBOUNCE_MS); }, - onSuspectedCausesChange(hubs: SuspectedCause[]) { - if (suspectedCausesTimer) clearTimeout(suspectedCausesTimer); - suspectedCausesTimer = setTimeout(async () => { + onHypothesesChange(hubs: Hypothesis[]) { + if (hypothesesTimer) clearTimeout(hypothesesTimer); + hypothesesTimer = setTimeout(async () => { try { - const jsonl = serializeSuspectedCauses(hubs); - await options.uploadBlob( - `${options.projectId}/investigation/suspected-causes.jsonl`, - jsonl - ); + const jsonl = serializeHypotheses(hubs); + await options.uploadBlob(`${options.projectId}/investigation/hypotheses.jsonl`, jsonl); } catch (err) { - console.warn('[KB] Failed to serialize suspected causes:', err); + console.warn('[KB] Failed to serialize hypotheses:', err); } }, DEBOUNCE_MS); }, @@ -238,7 +247,7 @@ export function createInvestigationSerializer(options: SerializerOptions) { dispose() { if (findingsTimer) clearTimeout(findingsTimer); if (questionsTimer) clearTimeout(questionsTimer); - if (suspectedCausesTimer) clearTimeout(suspectedCausesTimer); + if (hypothesesTimer) clearTimeout(hypothesesTimer); }, }; } diff --git a/apps/pwa/src/db/schema.ts b/apps/pwa/src/db/schema.ts index 25fb65929..4299c7b1b 100644 --- a/apps/pwa/src/db/schema.ts +++ b/apps/pwa/src/db/schema.ts @@ -35,7 +35,7 @@ import type { EvidenceSourceCursor, RowProvenanceTag, } from '@variscout/core'; -import type { Finding, Question, CausalLink, SuspectedCause } from '@variscout/core/findings'; +import type { Finding, Question, CausalLink, Hypothesis } from '@variscout/core/findings'; import type { ProcessMap } from '@variscout/core/frame'; // --------------------------------------------------------------------------- @@ -77,7 +77,7 @@ export type InvestigationRow = ProcessHubInvestigation; export type FindingRow = Finding; export type QuestionRow = Question; export type CausalLinkRow = CausalLink; -export type SuspectedCauseRow = SuspectedCause; +export type SuspectedCauseRow = Hypothesis; // --------------------------------------------------------------------------- // Database diff --git a/apps/pwa/src/features/investigation/useInvestigationOrchestration.ts b/apps/pwa/src/features/investigation/useInvestigationOrchestration.ts index 06dee5a4d..d33e8acbb 100644 --- a/apps/pwa/src/features/investigation/useInvestigationOrchestration.ts +++ b/apps/pwa/src/features/investigation/useInvestigationOrchestration.ts @@ -14,7 +14,7 @@ import { } from './investigationStore'; import type { QuestionDisplayData } from './investigationStore'; import { usePanelsStore } from '../panels/panelsStore'; -import { useSuspectedCauses, type SuspectedCauseUpdate } from '@variscout/hooks'; +import { useSuspectedCauses, type HypothesisUpdate } from '@variscout/hooks'; import type { Finding, FindingProjection, @@ -22,7 +22,7 @@ import type { IdeaImpact, ProcessContext, StatsResult, - SuspectedCause, + Hypothesis, } from '@variscout/core'; import type { UseQuestionsReturn } from '@variscout/hooks'; @@ -55,18 +55,18 @@ export interface UseInvestigationOrchestrationReturn { handleSetFindingStatus: (id: string, status: FindingStatus) => void; /** Full suspected causes hook state — hub CRUD operations for the Investigation workspace */ suspectedCausesState: { - hubs: SuspectedCause[]; - createHub: (name: string, synthesis: string) => SuspectedCause; - updateHub: (hubId: string, updates: SuspectedCauseUpdate) => void; + hubs: Hypothesis[]; + createHub: (name: string, synthesis: string) => Hypothesis; + updateHub: (hubId: string, updates: HypothesisUpdate) => void; deleteHub: (hubId: string) => void; - resetHubs: (newHubs: SuspectedCause[]) => void; + resetHubs: (newHubs: Hypothesis[]) => void; connectQuestion: (hubId: string, questionId: string) => void; disconnectQuestion: (hubId: string, questionId: string) => void; connectFinding: (hubId: string, findingId: string) => void; disconnectFinding: (hubId: string, findingId: string) => void; - setHubStatus: (hubId: string, status: SuspectedCause['status']) => void; - getHubForQuestion: (questionId: string) => SuspectedCause | undefined; - getHubForFinding: (findingId: string) => SuspectedCause | undefined; + setHubStatus: (hubId: string, status: Hypothesis['status']) => void; + getHubForQuestion: (questionId: string) => Hypothesis | undefined; + getHubForFinding: (findingId: string) => Hypothesis | undefined; }; /** Map of question ID to display data for FindingCard */ questionsMap: Record; diff --git a/apps/pwa/src/hooks/useDataIngestion.ts b/apps/pwa/src/hooks/useDataIngestion.ts index a5e27ac5b..e59bdfca6 100644 --- a/apps/pwa/src/hooks/useDataIngestion.ts +++ b/apps/pwa/src/hooks/useDataIngestion.ts @@ -6,7 +6,7 @@ import { useIsMobile } from '@variscout/ui'; import { useProjectStore, useInvestigationStore } from '@variscout/stores'; -import type { SuspectedCause, CausalLink } from '@variscout/core'; +import type { Hypothesis, CausalLink } from '@variscout/core'; import { useDataIngestion as useDataIngestionBase, type UseDataIngestionOptions, @@ -39,8 +39,7 @@ export const useDataIngestion = (options?: UseDataIngestionOptions) => { setFindings: useProjectStore(s => s.setFindings), setQuestions: useProjectStore(s => s.setQuestions), setCategories: useProjectStore(s => s.setCategories), - setSuspectedCauses: (hubs: SuspectedCause[]) => - useInvestigationStore.getState().resetHubs(hubs), + setSuspectedCauses: (hubs: Hypothesis[]) => useInvestigationStore.getState().resetHubs(hubs), setCausalLinks: (links: CausalLink[]) => useInvestigationStore.getState().loadInvestigationState({ causalLinks: links }), setProcessContext: useProjectStore(s => s.setProcessContext), diff --git a/apps/pwa/src/persistence/PwaHubRepository.ts b/apps/pwa/src/persistence/PwaHubRepository.ts index 0671b6867..f837e37c7 100644 --- a/apps/pwa/src/persistence/PwaHubRepository.ts +++ b/apps/pwa/src/persistence/PwaHubRepository.ts @@ -41,7 +41,7 @@ import type { FindingReadAPI, QuestionReadAPI, CausalLinkReadAPI, - SuspectedCauseReadAPI, + HypothesisReadAPI, CanvasStateReadAPI, } from '@variscout/core/persistence'; import type { HubAction } from '@variscout/core/actions'; @@ -239,7 +239,7 @@ export class PwaHubRepository implements HubRepository { }, }; - suspectedCauses: SuspectedCauseReadAPI = { + hypotheses: HypothesisReadAPI = { get: async id => { const row = await db.suspectedCauses.get(id); if (!row || row.deletedAt !== null) return undefined; diff --git a/apps/pwa/src/persistence/__tests__/PwaHubRepository.test.ts b/apps/pwa/src/persistence/__tests__/PwaHubRepository.test.ts index a96146a16..9accec63b 100644 --- a/apps/pwa/src/persistence/__tests__/PwaHubRepository.test.ts +++ b/apps/pwa/src/persistence/__tests__/PwaHubRepository.test.ts @@ -464,7 +464,7 @@ describe('PwaHubRepository — stub read APIs (empty tables until F3.5/F5)', () it('suspectedCauses.listByInvestigation returns []', async () => { const repo = new PwaHubRepository(); - expect(await repo.suspectedCauses.listByInvestigation('inv-x')).toEqual([]); + expect(await repo.hypotheses.listByInvestigation('inv-x')).toEqual([]); }); }); diff --git a/packages/charts/src/EvidenceMap/SynthesisLayer.tsx b/packages/charts/src/EvidenceMap/SynthesisLayer.tsx index b1419331a..091433160 100644 --- a/packages/charts/src/EvidenceMap/SynthesisLayer.tsx +++ b/packages/charts/src/EvidenceMap/SynthesisLayer.tsx @@ -2,7 +2,7 @@ * SynthesisLayer — Layer 3 of the Evidence Map * * Renders convergence zones where multiple causal chains meet, - * SuspectedCause hub labels, status indicators, and projected improvements. + * Hypothesis hub labels, status indicators, and projected improvements. * Azure only — not rendered in PWA. */ @@ -21,11 +21,15 @@ function getStatusColor(status?: string): string { switch (status) { case 'confirmed': return chartColors.pass; - case 'not-confirmed': + case 'refuted': return getChromeColors(true).labelMuted; // slate-500 - case 'suspected': - default: + case 'needs-disconfirmation': + return chartColors.warning; + case 'evidenced': return chartColors.cpPotential; + case 'proposed': + default: + return chartColors.selected; } } @@ -81,7 +85,7 @@ const SynthesisLayer: React.FC = ({ x={0} y={zoneRadius + 18} textAnchor="middle" - fill={isDark ? '#c4b5fd' : '#6d28d9'} + fill={isDark ? chartColors.quadratic : chartColors.cpPotential} fontSize={9} fontWeight="bold" > @@ -90,9 +94,13 @@ const SynthesisLayer: React.FC = ({ {point.hubStatus === 'confirmed' ? 'CONFIRMED' - : point.hubStatus === 'not-confirmed' + : point.hubStatus === 'refuted' ? 'NOT CONFIRMED' - : 'SUSPECTED ROOT CAUSE'} + : point.hubStatus === 'needs-disconfirmation' + ? 'NEEDS DISCONFIRMATION' + : point.hubStatus === 'evidenced' + ? 'EVIDENCED HYPOTHESIS' + : 'PROPOSED HYPOTHESIS'} )} @@ -103,7 +111,7 @@ const SynthesisLayer: React.FC = ({ x={0} y={-(zoneRadius + 8)} textAnchor="middle" - fill={isDark ? '#86efac' : '#15803d'} + fill={chartColors.pass} fontSize={8} fontWeight="bold" > diff --git a/packages/charts/src/InvestigationWall/CommandPalette.tsx b/packages/charts/src/InvestigationWall/CommandPalette.tsx index 743b8092d..6f7165192 100644 --- a/packages/charts/src/InvestigationWall/CommandPalette.tsx +++ b/packages/charts/src/InvestigationWall/CommandPalette.tsx @@ -12,7 +12,7 @@ */ import React, { useEffect, useMemo, useRef, useState } from 'react'; -import type { Finding, MessageCatalog, Question, SuspectedCause } from '@variscout/core'; +import type { Finding, MessageCatalog, Question, Hypothesis } from '@variscout/core'; import { getMessage } from '@variscout/core/i18n'; import { useWallLocale } from './hooks/useWallLocale'; @@ -21,7 +21,7 @@ export interface CommandPaletteProps { onClose: () => void; /** Called with the id of the selected result when Enter is pressed. */ onPanTo: (nodeId: string) => void; - hubs: SuspectedCause[]; + hubs: Hypothesis[]; questions: Question[]; findings: Finding[]; } @@ -39,11 +39,7 @@ const RESULT_KIND_KEY: Record = { finding: 'wall.palette.kind.finding', }; -function buildResults( - hubs: SuspectedCause[], - questions: Question[], - findings: Finding[] -): Result[] { +function buildResults(hubs: Hypothesis[], questions: Question[], findings: Finding[]): Result[] { return [ ...hubs.map(h => ({ id: h.id, kind: 'hub', label: h.name })), ...questions.map(q => ({ id: q.id, kind: 'question', label: q.text })), diff --git a/packages/charts/src/InvestigationWall/HypothesisCard.tsx b/packages/charts/src/InvestigationWall/HypothesisCard.tsx index f2ff8c443..59b81684d 100644 --- a/packages/charts/src/InvestigationWall/HypothesisCard.tsx +++ b/packages/charts/src/InvestigationWall/HypothesisCard.tsx @@ -1,5 +1,5 @@ /** - * HypothesisCard — status-bordered Mechanism Branch card for a SuspectedCause hub. + * HypothesisCard — status-bordered Mechanism Branch card for a Hypothesis hub. * * Displays the suspected mechanism, branch status, clue/check counts, and an * optional evidence-gap warning badge. Positioned by its center-top point (x, y). @@ -10,14 +10,14 @@ */ import React from 'react'; -import type { MechanismBranchViewModel, MessageCatalog, SuspectedCause } from '@variscout/core'; +import type { MechanismBranchViewModel, MessageCatalog, Hypothesis } from '@variscout/core'; import { formatMessage, getMessage } from '@variscout/core/i18n'; import { chartColors } from '../colors'; import type { WallStatus } from './types'; import { useWallLocale } from './hooks/useWallLocale'; export interface HypothesisCardProps { - hub: SuspectedCause; + hub: Hypothesis; branch?: MechanismBranchViewModel; displayStatus: WallStatus; x: number; diff --git a/packages/charts/src/InvestigationWall/Minimap.tsx b/packages/charts/src/InvestigationWall/Minimap.tsx index 863d431c6..fb30f88ee 100644 --- a/packages/charts/src/InvestigationWall/Minimap.tsx +++ b/packages/charts/src/InvestigationWall/Minimap.tsx @@ -14,7 +14,7 @@ */ import React from 'react'; -import type { SuspectedCause, Question } from '@variscout/core'; +import type { Hypothesis, Question } from '@variscout/core'; import { getMessage } from '@variscout/core/i18n'; import { chartColors } from '../colors'; import { CANVAS_W, CANVAS_H } from './WallCanvas'; @@ -29,7 +29,7 @@ const HUB_Y = 400; const QUESTION_Y = 900; export interface MinimapProps { - hubs: SuspectedCause[]; + hubs: Hypothesis[]; questions: Question[]; /** Current viewport zoom. */ zoom: number; diff --git a/packages/charts/src/InvestigationWall/MobileCardList.tsx b/packages/charts/src/InvestigationWall/MobileCardList.tsx index b344ed027..d025f4d0d 100644 --- a/packages/charts/src/InvestigationWall/MobileCardList.tsx +++ b/packages/charts/src/InvestigationWall/MobileCardList.tsx @@ -7,7 +7,7 @@ * status-colored left border. * * Status derivation mirrors `deriveDisplayStatus` in WallCanvas: confirmed - * and not-confirmed map directly; otherwise "evidenced" when at least one + * and refuted map directly; otherwise "evidenced" when at least one * supporting finding exists without a contradictor, else "proposed". */ @@ -16,7 +16,7 @@ import { projectMechanismBranch, type MessageCatalog, type ProcessMap, - type SuspectedCause, + type Hypothesis, type Finding, type Question, } from '@variscout/core'; @@ -27,7 +27,7 @@ import type { WallStatus } from './types'; import { useWallLocale } from './hooks/useWallLocale'; export interface MobileCardListProps { - hubs: SuspectedCause[]; + hubs: Hypothesis[]; findings: Finding[]; /** * Question list. Currently only `hub.questionIds.length` (linked questions) @@ -67,9 +67,9 @@ const STATUS_ACCENT: Record = { * WallCanvas) to avoid introducing a shared-helpers file mid-phase — the * computation is tiny and the two call sites share nothing else. */ -function deriveDisplayStatus(hub: SuspectedCause, findings: Finding[]): WallStatus { +function deriveDisplayStatus(hub: Hypothesis, findings: Finding[]): WallStatus { if (hub.status === 'confirmed') return 'confirmed'; - if (hub.status === 'not-confirmed') return 'refuted'; + if (hub.status === 'refuted') return 'refuted'; const supporting = hub.findingIds .map(id => findings.find(f => f.id === id)) .filter((f): f is Finding => !!f); diff --git a/packages/charts/src/InvestigationWall/TributaryFooter.tsx b/packages/charts/src/InvestigationWall/TributaryFooter.tsx index 2cf1068db..88cc5f9ea 100644 --- a/packages/charts/src/InvestigationWall/TributaryFooter.tsx +++ b/packages/charts/src/InvestigationWall/TributaryFooter.tsx @@ -2,18 +2,18 @@ * TributaryFooter — horizontal chip row below the Investigation Wall canvas. * * Shows all tributaries from the Process Map. Chips for tributaries that are - * referenced by at least one SuspectedCause hub are fully opaque; orphan + * referenced by at least one Hypothesis hub are fully opaque; orphan * tributaries (no hub reference) are dimmed to signal measurement gaps. */ import React from 'react'; -import type { ProcessMapTributary, SuspectedCause } from '@variscout/core'; +import type { ProcessMapTributary, Hypothesis } from '@variscout/core'; import { getMessage } from '@variscout/core/i18n'; import { useWallLocale } from './hooks/useWallLocale'; export interface TributaryFooterProps { tributaries: ProcessMapTributary[]; - hubs: SuspectedCause[]; + hubs: Hypothesis[]; y: number; canvasWidth: number; } diff --git a/packages/charts/src/InvestigationWall/WallCanvas.tsx b/packages/charts/src/InvestigationWall/WallCanvas.tsx index 72b4e139b..c878b644b 100644 --- a/packages/charts/src/InvestigationWall/WallCanvas.tsx +++ b/packages/charts/src/InvestigationWall/WallCanvas.tsx @@ -11,7 +11,7 @@ import React, { useMemo } from 'react'; import { DndContext } from '@dnd-kit/core'; import type { - SuspectedCause, + Hypothesis, Finding, Question, ProcessMap, @@ -34,7 +34,7 @@ import { useWallDragDrop } from './hooks/useWallDragDrop'; import { useWallIsMobile } from './hooks/useWallBreakpoint'; export interface WallCanvasProps { - hubs: SuspectedCause[]; + hubs: Hypothesis[]; findings: Finding[]; questions: Question[]; processMap?: ProcessMap; @@ -103,9 +103,9 @@ export interface WallCanvasProps { export const CANVAS_W = 2000; export const CANVAS_H = 1400; -function deriveDisplayStatus(hub: SuspectedCause, findings: Finding[]): WallStatus { +function deriveDisplayStatus(hub: Hypothesis, findings: Finding[]): WallStatus { if (hub.status === 'confirmed') return 'confirmed'; - if (hub.status === 'not-confirmed') return 'refuted'; + if (hub.status === 'refuted') return 'refuted'; const supporting = hub.findingIds .map(id => findings.find(f => f.id === id)) .filter((f): f is Finding => !!f); @@ -167,7 +167,7 @@ export const WallCanvas: React.FC = ({ const tributaryById = new Map(tributaries.map(t => [t.id, t])); type Bucket = { tributary: (typeof tributaries)[number] | null; - hubs: SuspectedCause[]; + hubs: Hypothesis[]; }; const buckets: Bucket[] = tributaries.map(t => ({ tributary: t, hubs: [] })); const unassigned: Bucket = { tributary: null, hubs: [] }; @@ -241,7 +241,7 @@ export const WallCanvas: React.FC = ({ const hubY = 400; const hubSpacing = CANVAS_W / (hubs.length + 1); - const renderHubAt = (hub: SuspectedCause, x: number) => { + const renderHubAt = (hub: Hypothesis, x: number) => { const hubProps = { hub, branch: branchByHubId.get(hub.id), diff --git a/packages/charts/src/InvestigationWall/__tests__/CommandPalette.test.tsx b/packages/charts/src/InvestigationWall/__tests__/CommandPalette.test.tsx index 5e361c8b0..bf5173897 100644 --- a/packages/charts/src/InvestigationWall/__tests__/CommandPalette.test.tsx +++ b/packages/charts/src/InvestigationWall/__tests__/CommandPalette.test.tsx @@ -8,16 +8,16 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import { CommandPalette } from '../CommandPalette'; -import type { SuspectedCause, Question, Finding } from '@variscout/core'; +import type { Hypothesis, Question, Finding } from '@variscout/core'; -const hubs: SuspectedCause[] = [ +const hubs: Hypothesis[] = [ { id: 'h-night', name: 'Night shift thermal drift', synthesis: '', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: '', updatedAt: '', }, @@ -27,7 +27,7 @@ const hubs: SuspectedCause[] = [ synthesis: '', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: '', updatedAt: '', }, @@ -37,7 +37,7 @@ const hubs: SuspectedCause[] = [ synthesis: '', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: '', updatedAt: '', }, diff --git a/packages/charts/src/InvestigationWall/__tests__/DraggableHypothesisCard.test.tsx b/packages/charts/src/InvestigationWall/__tests__/DraggableHypothesisCard.test.tsx index dbd0abf36..21b13916e 100644 --- a/packages/charts/src/InvestigationWall/__tests__/DraggableHypothesisCard.test.tsx +++ b/packages/charts/src/InvestigationWall/__tests__/DraggableHypothesisCard.test.tsx @@ -2,9 +2,9 @@ import { describe, it, expect } from 'vitest'; import { render } from '@testing-library/react'; import { DndContext } from '@dnd-kit/core'; import { DraggableHypothesisCard } from '../DraggableHypothesisCard'; -import type { SuspectedCause } from '@variscout/core'; +import type { Hypothesis } from '@variscout/core'; -const hub: SuspectedCause = { +const hub: Hypothesis = { id: 'h1', name: 'Nozzle runs hot', synthesis: '', diff --git a/packages/charts/src/InvestigationWall/__tests__/HypothesisCard.test.tsx b/packages/charts/src/InvestigationWall/__tests__/HypothesisCard.test.tsx index 5a75c0955..624c1a6de 100644 --- a/packages/charts/src/InvestigationWall/__tests__/HypothesisCard.test.tsx +++ b/packages/charts/src/InvestigationWall/__tests__/HypothesisCard.test.tsx @@ -5,10 +5,10 @@ import { projectMechanismBranch, type Finding, type Question, - type SuspectedCause, + type Hypothesis, } from '@variscout/core'; -const hub: SuspectedCause = { +const hub: Hypothesis = { id: 'h1', name: 'Nozzle runs hot on night shift', synthesis: '', @@ -96,7 +96,7 @@ describe('HypothesisCard', () => { const branch = projectMechanismBranch( { ...hub, - status: 'suspected', + status: 'proposed', nextMove: 'Run a late-shift temperature check.', questionIds: ['q1'], }, @@ -106,7 +106,7 @@ describe('HypothesisCard', () => { render( = {}): SuspectedCause => ({ +const makeHub = (overrides: Partial = {}): Hypothesis => ({ id: 'h1', name: 'Nozzle runs hot', synthesis: '', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: '', updatedAt: '', ...overrides, }); -const hubA: SuspectedCause = makeHub({ +const hubA: Hypothesis = makeHub({ id: 'hA', name: 'Nozzle runs hot', findingIds: ['f1', 'f2', 'f3'], @@ -23,12 +23,12 @@ const hubA: SuspectedCause = makeHub({ status: 'confirmed', }); -const hubB: SuspectedCause = makeHub({ +const hubB: Hypothesis = makeHub({ id: 'hB', name: 'Operator variance', findingIds: [], questionIds: [], - status: 'suspected', + status: 'proposed', }); describe('MobileCardList', () => { @@ -55,7 +55,7 @@ describe('MobileCardList', () => { validationStatus: 'supports', } as unknown as Finding, ]; - const hub = makeHub({ id: 'h-ev', findingIds: ['f1'], status: 'suspected' }); + const hub = makeHub({ id: 'h-ev', findingIds: ['f1'], status: 'proposed' }); render(); expect(screen.getByTestId('wall-mobile-hub-h-ev')).toHaveAttribute('data-status', 'evidenced'); }); diff --git a/packages/charts/src/InvestigationWall/__tests__/TributaryFooter.test.tsx b/packages/charts/src/InvestigationWall/__tests__/TributaryFooter.test.tsx index 69638d8ea..1c51c4574 100644 --- a/packages/charts/src/InvestigationWall/__tests__/TributaryFooter.test.tsx +++ b/packages/charts/src/InvestigationWall/__tests__/TributaryFooter.test.tsx @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import { TributaryFooter } from '../TributaryFooter'; -import type { ProcessMapTributary, SuspectedCause } from '@variscout/core'; +import type { ProcessMapTributary, Hypothesis } from '@variscout/core'; const tributaries: ProcessMapTributary[] = [ { id: 't1', stepId: 'n1', column: 'SHIFT' }, @@ -29,13 +29,13 @@ describe('TributaryFooter', () => { }); it('does NOT dim tributaries referenced by a hub', () => { - const hub: SuspectedCause = { + const hub: Hypothesis = { id: 'h1', name: '', synthesis: '', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', tributaryIds: ['t1'], createdAt: '', updatedAt: '', diff --git a/packages/charts/src/InvestigationWall/__tests__/WallCanvas.test.tsx b/packages/charts/src/InvestigationWall/__tests__/WallCanvas.test.tsx index 9a5f8c147..6c3a5b685 100644 --- a/packages/charts/src/InvestigationWall/__tests__/WallCanvas.test.tsx +++ b/packages/charts/src/InvestigationWall/__tests__/WallCanvas.test.tsx @@ -1,7 +1,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen } from '@testing-library/react'; import { WallCanvas } from '../WallCanvas'; -import type { SuspectedCause, ProcessMap, Question } from '@variscout/core'; +import type { Hypothesis, ProcessMap, Question } from '@variscout/core'; /** * Override `window.matchMedia` for a single test to force the mobile branch @@ -42,7 +42,7 @@ const processMap: ProcessMap = { updatedAt: '', }; -const hub: SuspectedCause = { +const hub: Hypothesis = { id: 'h1', name: 'Nozzle runs hot', synthesis: '', @@ -123,7 +123,7 @@ describe('WallCanvas', () => { }); it('flags missing-column when hub condition references a column not in activeColumns', () => { - const hubWithCondition: SuspectedCause = { + const hubWithCondition: Hypothesis = { ...hub, id: 'h-missing', condition: { @@ -148,7 +148,7 @@ describe('WallCanvas', () => { }); it('does not flag missing-column when all referenced columns are present', () => { - const hubWithCondition: SuspectedCause = { + const hubWithCondition: Hypothesis = { ...hub, id: 'h-ok', condition: { @@ -173,7 +173,7 @@ describe('WallCanvas', () => { }); it('does not flag missing-column when activeColumns is not provided', () => { - const hubWithCondition: SuspectedCause = { + const hubWithCondition: Hypothesis = { ...hub, id: 'h-unknown', condition: { @@ -332,8 +332,8 @@ describe('WallCanvas', () => { }); it('wraps hubs in a tributary-group frame when groupByTributary is on', () => { - const hubA: SuspectedCause = { ...hub, id: 'hA', tributaryIds: ['t1'] }; - const hubB: SuspectedCause = { ...hub, id: 'hB', tributaryIds: ['t1'] }; + const hubA: Hypothesis = { ...hub, id: 'hA', tributaryIds: ['t1'] }; + const hubB: Hypothesis = { ...hub, id: 'hB', tributaryIds: ['t1'] }; const { container } = render( { }); it('does not render tributary-group frame when groupByTributary is off', () => { - const hubA: SuspectedCause = { ...hub, id: 'hA', tributaryIds: ['t1'] }; + const hubA: Hypothesis = { ...hub, id: 'hA', tributaryIds: ['t1'] }; const { container } = render( { }); it('buckets hubs without matching tributary into an unassigned group', () => { - const hubA: SuspectedCause = { ...hub, id: 'hA' }; // no tributaryIds + const hubA: Hypothesis = { ...hub, id: 'hA' }; // no tributaryIds const { container } = render( = {}): SignalCard => ({ id: 'sig-weight', @@ -48,13 +48,13 @@ const finding = (): Finding => ({ validationStatus: 'supports', }); -const branch = (overrides: Partial = {}): SuspectedCause => ({ +const branch = (overrides: Partial = {}): Hypothesis => ({ id: 'hub-1', name: 'Nozzle wear', synthesis: 'Machine B separates from the rest.', questionIds: ['q-1'], findingIds: ['f-1'], - status: 'suspected', + status: 'proposed', investigationId: 'inv-test-001', createdAt: 1745625600000, updatedAt: 1745625600000, diff --git a/packages/core/src/actions/suspectedCauseActions.ts b/packages/core/src/actions/suspectedCauseActions.ts index 8192a3222..94a93c4be 100644 --- a/packages/core/src/actions/suspectedCauseActions.ts +++ b/packages/core/src/actions/suspectedCauseActions.ts @@ -1,15 +1,15 @@ -import type { SuspectedCause } from '../findings/types'; +import type { Hypothesis } from '../findings/types'; import type { ProcessHubInvestigation } from '../processHub'; export type SuspectedCauseAction = | { kind: 'SUSPECTED_CAUSE_ADD'; investigationId: ProcessHubInvestigation['id']; - cause: SuspectedCause; + cause: Hypothesis; } | { kind: 'SUSPECTED_CAUSE_UPDATE'; - causeId: SuspectedCause['id']; - patch: Partial; + causeId: Hypothesis['id']; + patch: Partial; } - | { kind: 'SUSPECTED_CAUSE_ARCHIVE'; causeId: SuspectedCause['id'] }; + | { kind: 'SUSPECTED_CAUSE_ARCHIVE'; causeId: Hypothesis['id'] }; diff --git a/packages/core/src/ai/__tests__/coScoutContext.test.ts b/packages/core/src/ai/__tests__/coScoutContext.test.ts index 634c7b91e..54b200ddb 100644 --- a/packages/core/src/ai/__tests__/coScoutContext.test.ts +++ b/packages/core/src/ai/__tests__/coScoutContext.test.ts @@ -74,9 +74,9 @@ describe('formatInvestigationContext', () => { expect(result).toContain('1 ruled-out'); }); - it('uses ONLY suspectedCauseHubs, not legacy suspectedCauses', () => { + it('uses ONLY hypothesisHubs, not legacy suspectedCauses', () => { const result = formatInvestigationContext({ - // Legacy causeRole-based suspected causes — should be IGNORED + // Legacy causeRole-based hypotheses — should be IGNORED suspectedCauses: [ { id: 'sc1', @@ -85,13 +85,13 @@ describe('formatInvestigationContext', () => { status: 'investigating', }, ], - // Hub-based suspected causes — should be INCLUDED - suspectedCauseHubs: [ + // Hub-based hypotheses — should be INCLUDED + hypothesisHubs: [ { id: 'hub1', name: 'Raw material moisture', synthesis: 'Incoming moisture varies by supplier', - status: 'active', + status: 'evidenced', questionCount: 3, findingCount: 2, evidence: { value: 0.45, label: 'Strong (R²adj=45%)', description: 'test' }, @@ -101,9 +101,9 @@ describe('formatInvestigationContext', () => { }); // Hub content should be present - expect(result).toContain('Suspected cause hubs:'); + expect(result).toContain('Hypotheses:'); expect(result).toContain('Raw material moisture'); - expect(result).toContain('[active]'); + expect(result).toContain('[evidenced]'); expect(result).toContain('3Q, 2F'); expect(result).toContain('Strong (R²adj=45%)'); expect(result).toContain('[selected for improvement]'); @@ -216,12 +216,12 @@ describe('formatInvestigationContext', () => { coveragePercent: 12, questionsChecked: 2, questionsTotal: 5, - suspectedCauseHubs: [ + hypothesisHubs: [ { id: 'hub1', name: 'Nozzle Wear', synthesis: 'Nozzle degradation varies by shift', - status: 'active', + status: 'evidenced', questionCount: 2, findingCount: 1, }, @@ -237,12 +237,12 @@ describe('formatInvestigationContext', () => { it('omits evidence warning when coveragePercent >= 25', () => { const result = formatInvestigationContext({ coveragePercent: 45, - suspectedCauseHubs: [ + hypothesisHubs: [ { id: 'hub1', name: 'Machine Setup', synthesis: 'Setup variation across shifts', - status: 'active', + status: 'evidenced', questionCount: 3, findingCount: 2, }, @@ -253,12 +253,12 @@ describe('formatInvestigationContext', () => { it('uses per-hub evidence.value when coveragePercent is absent and value < 0.25', () => { const result = formatInvestigationContext({ - suspectedCauseHubs: [ + hypothesisHubs: [ { id: 'hub1', name: 'Nozzle Wear', synthesis: 'Nozzle degradation', - status: 'active', + status: 'evidenced', questionCount: 1, findingCount: 0, evidence: { value: 0.12, label: 'Weak (R²adj=12%)', description: 'test' }, @@ -272,12 +272,12 @@ describe('formatInvestigationContext', () => { it('omits evidence warning when per-hub evidence.value >= 0.25 and coveragePercent absent', () => { const result = formatInvestigationContext({ - suspectedCauseHubs: [ + hypothesisHubs: [ { id: 'hub1', name: 'Machine Setup', synthesis: 'Setup variation', - status: 'active', + status: 'evidenced', questionCount: 3, findingCount: 2, evidence: { value: 0.38, label: 'Strong (R²adj=38%)', description: 'test' }, @@ -290,12 +290,12 @@ describe('formatInvestigationContext', () => { it('omits open question count suffix when questionsTotal and questionsChecked are absent', () => { const result = formatInvestigationContext({ coveragePercent: 8, - suspectedCauseHubs: [ + hypothesisHubs: [ { id: 'hub1', name: 'Raw Material', synthesis: 'Incoming moisture varies', - status: 'active', + status: 'evidenced', questionCount: 0, findingCount: 0, }, diff --git a/packages/core/src/ai/__tests__/coScoutPhases.test.ts b/packages/core/src/ai/__tests__/coScoutPhases.test.ts index 28a9ba679..21711c61b 100644 --- a/packages/core/src/ai/__tests__/coScoutPhases.test.ts +++ b/packages/core/src/ai/__tests__/coScoutPhases.test.ts @@ -86,7 +86,7 @@ describe('Phase coaching modules', () => { it('includes hub synthesis coaching for validating', () => { const result = buildInvestigateCoaching('standard', 'validating'); - expect(result.toLowerCase()).toContain('suggest_suspected_cause'); + expect(result.toLowerCase()).toContain('suggest_hypothesis'); }); it('includes hub synthesis coaching for converging', () => { @@ -157,7 +157,7 @@ describe('Phase coaching modules', () => { mode: 'standard', investigationPhase: 'validating', }); - expect(result).toContain('suggest_suspected_cause'); + expect(result).toContain('suggest_hypothesis'); }); it('passes entryScenario to scout coaching', () => { diff --git a/packages/core/src/ai/__tests__/promptTierSafety.test.ts b/packages/core/src/ai/__tests__/promptTierSafety.test.ts index 9ae956567..ece3ea5ec 100644 --- a/packages/core/src/ai/__tests__/promptTierSafety.test.ts +++ b/packages/core/src/ai/__tests__/promptTierSafety.test.ts @@ -23,7 +23,7 @@ const RICH_CONTEXT: AIContext = { // eslint-disable-next-line @typescript-eslint/no-explicit-any questionTree: [{ id: 'q1', text: 'Test question?', status: 'answered', children: [] }] as any, // eslint-disable-next-line @typescript-eslint/no-explicit-any - suspectedCauseHubs: [{ name: 'Nozzle Wear', status: 'suspected' }] as any, + hypothesisHubs: [{ name: 'Nozzle Wear', status: 'proposed' }] as any, }, }; diff --git a/packages/core/src/ai/__tests__/suggestedQuestions.test.ts b/packages/core/src/ai/__tests__/suggestedQuestions.test.ts index daf66f243..8e0e4de5b 100644 --- a/packages/core/src/ai/__tests__/suggestedQuestions.test.ts +++ b/packages/core/src/ai/__tests__/suggestedQuestions.test.ts @@ -165,7 +165,7 @@ describe('buildSuggestedQuestions', () => { }, }; const result = buildSuggestedQuestions(context); - expect(result).toContain('Are the corrective actions addressing the suspected cause?'); + expect(result).toContain('Are the corrective actions addressing the hypothesis?'); // Also check for Capability chart question expect( result.some(q => q.includes('Capability chart') || q.includes('corrective actions')) diff --git a/packages/core/src/ai/__tests__/toolRegistry.test.ts b/packages/core/src/ai/__tests__/toolRegistry.test.ts index c268ff799..ad7a7d280 100644 --- a/packages/core/src/ai/__tests__/toolRegistry.test.ts +++ b/packages/core/src/ai/__tests__/toolRegistry.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { TOOL_REGISTRY, getToolsForPhase } from '../prompts/coScout/tools'; -import type { SuspectedCause } from '../../findings/types'; +import type { Hypothesis } from '../../findings/types'; // ── Helpers ──────────────────────────────────────────────────────────── @@ -25,7 +25,7 @@ const SCOUT_ACTION_TOOLS = [ const INVESTIGATE_ACTION_TOOLS = [ 'create_question', 'answer_question', - 'suggest_suspected_cause', + 'suggest_hypothesis', 'connect_hub_evidence', 'suggest_improvement_idea', 'suggest_action', @@ -163,28 +163,28 @@ describe('getToolsForPhase', () => { expect(toolNames(improveTools)).toContain('notify_action_owners'); }); - it('suggest_suspected_cause requires validating or converging phase', () => { + it('suggest_hypothesis requires validating or converging phase', () => { // Without investigation phase — excluded const tools1 = getToolsForPhase('investigate', 'standard'); - expect(toolNames(tools1)).not.toContain('suggest_suspected_cause'); + expect(toolNames(tools1)).not.toContain('suggest_hypothesis'); // With diverging phase — excluded const tools2 = getToolsForPhase('investigate', 'standard', { investigationPhase: 'diverging', }); - expect(toolNames(tools2)).not.toContain('suggest_suspected_cause'); + expect(toolNames(tools2)).not.toContain('suggest_hypothesis'); // With validating phase — included const tools3 = getToolsForPhase('investigate', 'standard', { investigationPhase: 'validating', }); - expect(toolNames(tools3)).toContain('suggest_suspected_cause'); + expect(toolNames(tools3)).toContain('suggest_hypothesis'); // With converging phase — included const tools4 = getToolsForPhase('investigate', 'standard', { investigationPhase: 'converging', }); - expect(toolNames(tools4)).toContain('suggest_suspected_cause'); + expect(toolNames(tools4)).toContain('suggest_hypothesis'); }); it('connect_hub_evidence requires existing hubs', () => { @@ -205,7 +205,7 @@ describe('getToolsForPhase', () => { synthesis: '', questionIds: [], findingIds: [], - } as unknown as SuspectedCause, + } as unknown as Hypothesis, ], }); expect(toolNames(tools3)).toContain('connect_hub_evidence'); diff --git a/packages/core/src/ai/actionTools.ts b/packages/core/src/ai/actionTools.ts index d1810f60b..6714817d6 100644 --- a/packages/core/src/ai/actionTools.ts +++ b/packages/core/src/ai/actionTools.ts @@ -32,7 +32,7 @@ export type ActionToolName = | 'publish_report' | 'notify_action_owners' | 'navigate_to' // Project dashboard navigation - | 'suggest_suspected_cause' + | 'suggest_hypothesis' | 'connect_hub_evidence' | 'suggest_causal_link' | 'highlight_map_pattern'; diff --git a/packages/core/src/ai/actions/__tests__/critiqueInvestigationState.test.ts b/packages/core/src/ai/actions/__tests__/critiqueInvestigationState.test.ts index 7b3ae718d..ec2caa993 100644 --- a/packages/core/src/ai/actions/__tests__/critiqueInvestigationState.test.ts +++ b/packages/core/src/ai/actions/__tests__/critiqueInvestigationState.test.ts @@ -1,17 +1,17 @@ import { describe, it, expect } from 'vitest'; import { critiqueInvestigationState } from '../critiqueInvestigationState'; -import type { SuspectedCause, Question, Finding } from '@variscout/core'; +import type { Hypothesis, Question, Finding } from '@variscout/core'; const FIXED_NOW = Date.parse('2026-04-19T00:00:00Z'); -function hub(id: string, findingIds: string[], questionIds: string[] = []): SuspectedCause { +function hub(id: string, findingIds: string[], questionIds: string[] = []): Hypothesis { return { id, name: id, synthesis: '', questionIds, findingIds, - status: 'suspected', + status: 'proposed', createdAt: FIXED_NOW, updatedAt: FIXED_NOW, deletedAt: null, @@ -78,7 +78,7 @@ describe('critiqueInvestigationState', () => { }); it('flags orphan open questions not linked to any hub', () => { - const hubs: SuspectedCause[] = []; + const hubs: Hypothesis[] = []; const questions = [question('q1'), question('q2', 'answered')]; const result = critiqueInvestigationState({ hubs, questions, findings: [] }); // q1 is open and orphan; q2 is answered (not orphan-flaggable). diff --git a/packages/core/src/ai/actions/__tests__/proposeDisconfirmationMove.test.ts b/packages/core/src/ai/actions/__tests__/proposeDisconfirmationMove.test.ts index ecddc614e..545e5f431 100644 --- a/packages/core/src/ai/actions/__tests__/proposeDisconfirmationMove.test.ts +++ b/packages/core/src/ai/actions/__tests__/proposeDisconfirmationMove.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { proposeDisconfirmationMove } from '../proposeDisconfirmationMove'; -import type { SuspectedCause, Finding } from '@variscout/core'; +import type { Hypothesis, Finding } from '@variscout/core'; import { DEFAULT_TIME_LENS } from '@variscout/core'; function finding( @@ -23,14 +23,14 @@ function finding( }; } -function hub(id: string, findingIds: string[]): SuspectedCause { +function hub(id: string, findingIds: string[]): Hypothesis { return { id, name: id, synthesis: '', questionIds: [], findingIds, - status: 'suspected', + status: 'proposed', createdAt: 1745625600000, updatedAt: 1745625600000, investigationId: 'inv-test-001', diff --git a/packages/core/src/ai/actions/critiqueInvestigationState.ts b/packages/core/src/ai/actions/critiqueInvestigationState.ts index 1910df4d2..6aa85fee4 100644 --- a/packages/core/src/ai/actions/critiqueInvestigationState.ts +++ b/packages/core/src/ai/actions/critiqueInvestigationState.ts @@ -10,7 +10,7 @@ * - stale-question: open question older than STALE_DAYS */ -import type { SuspectedCause, Question, Finding } from '../..'; +import type { Hypothesis, Question, Finding } from '../..'; const MIN_SUPPORTERS_FOR_DISCONFIRMATION_GAP = 3; const STALE_DAYS = 7; @@ -23,7 +23,7 @@ export type InvestigationGap = | { kind: 'stale-question'; questionId: string; questionText: string; daysOpen: number }; export interface CritiqueInput { - hubs: SuspectedCause[]; + hubs: Hypothesis[]; questions: Question[]; findings: Finding[]; } diff --git a/packages/core/src/ai/actions/proposeDisconfirmationMove.ts b/packages/core/src/ai/actions/proposeDisconfirmationMove.ts index d3f5485cd..8b85dbb6c 100644 --- a/packages/core/src/ai/actions/proposeDisconfirmationMove.ts +++ b/packages/core/src/ai/actions/proposeDisconfirmationMove.ts @@ -1,13 +1,13 @@ /** * Pure heuristic that proposes a complementary brush region analysts should try - * in order to disconfirm a suspected cause. Emits a `SuggestedBrush` the Wall UI + * in order to disconfirm a hypothesis. Emits a `SuggestedBrush` the Wall UI * can hand to CoScout's `critique_investigation_state` tool output. * * Gate: hub has ≥3 supporting findings AND no existing finding with * validationStatus === 'contradicts'. Returns undefined otherwise. */ -import type { SuspectedCause, Finding, FindingSource, DataRow } from '../..'; +import type { Hypothesis, Finding, FindingSource, DataRow } from '../..'; export interface SuggestedBrush { chart: FindingSource['chart']; @@ -21,7 +21,7 @@ export interface SuggestedBrush { const SUPPORT_THRESHOLD = 3; export function proposeDisconfirmationMove( - hub: SuspectedCause, + hub: Hypothesis, findings: Finding[], data: DataRow[] ): SuggestedBrush | undefined { diff --git a/packages/core/src/ai/buildAIContext.ts b/packages/core/src/ai/buildAIContext.ts index e123480a2..26abbbd51 100644 --- a/packages/core/src/ai/buildAIContext.ts +++ b/packages/core/src/ai/buildAIContext.ts @@ -4,7 +4,7 @@ import type { AIContext, ProcessContext, TargetMetric, InvestigationPhase } from './types'; import type { InsightChartType } from './chartInsights'; -import type { Finding, Question, InvestigationCategory, SuspectedCause } from '../findings'; +import type { Finding, Question, InvestigationCategory, Hypothesis } from '../findings'; import type { StagedComparison } from '../stats/staged'; import { groupFindingsByStatus, getCategoryForFactor } from '../findings'; import { computeOptimum } from '../stats/safeMath'; @@ -99,7 +99,7 @@ export interface BuildAIContextOptions { /** Current analysis mode */ analysisMode?: AnalysisMode; /** Suspected cause hubs from investigation */ - suspectedCauses?: SuspectedCause[]; + suspectedCauses?: Hypothesis[]; /** R²adj-weighted coverage percentage (0-100) */ coveragePercent?: number; /** Number of questions that have been checked (answered or ruled-out) */ @@ -508,9 +508,9 @@ export function buildAIContext(options: BuildAIContextOptions): AIContext { context.investigation.focusedQuestionId = focusedQuestionId; } - // Suspected cause hubs (Phase 6 — CoScout as Investigation Partner) + // Hypothesis hubs (Phase 6 — CoScout as Investigation Partner) if (options.suspectedCauses && options.suspectedCauses.length > 0) { - context.investigation.suspectedCauseHubs = options.suspectedCauses.map(hub => ({ + context.investigation.hypothesisHubs = options.suspectedCauses.map(hub => ({ id: hub.id, name: hub.name, synthesis: hub.synthesis, diff --git a/packages/core/src/ai/prompts/coScout/context/investigation.ts b/packages/core/src/ai/prompts/coScout/context/investigation.ts index 3ce70fbcc..a7e765ed3 100644 --- a/packages/core/src/ai/prompts/coScout/context/investigation.ts +++ b/packages/core/src/ai/prompts/coScout/context/investigation.ts @@ -2,13 +2,13 @@ * Investigation context formatter for CoScout Tier 2 (semi-static context). * * Formats investigation state into human-readable text blocks. - * CRITICAL: Uses ONLY suspectedCauseHubs — ignores legacy causeRole-based + * CRITICAL: Uses ONLY hypothesisHubs — ignores legacy causeRole-based * suspectedCauses from question fields (contradiction resolution #1). */ import type { AIContext } from '../../../types'; -/** Minimum R²adj coverage for a suspected cause hub to be considered sufficiently evidenced */ +/** Minimum R²adj coverage for a hypothesis hub to be considered sufficiently evidenced */ export const EVIDENCE_SUFFICIENCY_THRESHOLD = 0.25; /** @@ -18,7 +18,7 @@ export const EVIDENCE_SUFFICIENCY_THRESHOLD = 0.25; * - Current understanding / problem condition * - Problem statement with stage * - Question tree summary (counts by status, top 3 by priority) - * - Suspected cause hubs (ONLY hub entities, not legacy causeRole) + * - Hypothesis hubs (ONLY hub entities, not legacy causeRole) * - Evidence Map topology summary * - Causal links * @@ -93,9 +93,9 @@ export function formatInvestigationContext( } } - // Suspected cause hubs (ONLY hub entities — not legacy causeRole) - if (investigation.suspectedCauseHubs && investigation.suspectedCauseHubs.length > 0) { - const hubLines = investigation.suspectedCauseHubs.map(hub => { + // Hypothesis hubs (ONLY hub entities — not legacy causeRole) + if (investigation.hypothesisHubs && investigation.hypothesisHubs.length > 0) { + const hubLines = investigation.hypothesisHubs.map(hub => { const parts = [` - "${hub.name}" [${hub.status}]`]; if (hub.questionCount > 0 || hub.findingCount > 0) { parts.push(`(${hub.questionCount}Q, ${hub.findingCount}F)`); @@ -108,7 +108,7 @@ export function formatInvestigationContext( } return parts.join(' '); }); - lines.push(`Suspected cause hubs:\n${hubLines.join('\n')}`); + lines.push(`Hypotheses:\n${hubLines.join('\n')}`); // Evidence sufficiency warning: check coveragePercent or per-hub rSquaredAdj const coverage = investigation.coveragePercent; @@ -122,11 +122,11 @@ export function formatInvestigationContext( ? ` Consider investigating the ${openCount} remaining open question${openCount === 1 ? '' : 's'}.` : ''; lines.push( - `⚠ Evidence note: Combined suspected causes explain ~${Math.round(coverage)}% of variation — significant sources may remain unexplored.${openSuffix}` + `⚠ Evidence note: Combined hypotheses explain ~${Math.round(coverage)}% of variation — significant sources may remain unexplored.${openSuffix}` ); } else if (coverage === undefined) { // Fall back to per-hub rSquaredAdj when coveragePercent is not set - for (const hub of investigation.suspectedCauseHubs) { + for (const hub of investigation.hypothesisHubs) { const hubR2 = hub.evidence?.value; if (hubR2 !== undefined && hubR2 < EVIDENCE_SUFFICIENCY_THRESHOLD) { const pct = Math.round(hubR2 * 100); diff --git a/packages/core/src/ai/prompts/coScout/index.ts b/packages/core/src/ai/prompts/coScout/index.ts index 91e6c9590..0e42ca936 100644 --- a/packages/core/src/ai/prompts/coScout/index.ts +++ b/packages/core/src/ai/prompts/coScout/index.ts @@ -35,7 +35,7 @@ import { buildModeWorkflow } from './modes'; import { formatInvestigationContext, formatDataContext, formatKnowledgeContext } from './context'; import { getToolsForPhase } from './tools'; import { buildLocaleHint, TERMINOLOGY_INSTRUCTION } from '../shared'; -import type { SuspectedCause } from '../../../findings/types'; +import type { Hypothesis } from '../../../findings/types'; import { investigationDisciplinePrompt } from './tier2'; /** @@ -124,7 +124,7 @@ export function assembleCoScoutPrompt( const tools = getToolsForPhase(phase, mode, { isTeamPlan, investigationPhase, - existingHubs: context?.investigation?.suspectedCauseHubs as SuspectedCause[] | undefined, + existingHubs: context?.investigation?.hypothesisHubs as Hypothesis[] | undefined, }); return { diff --git a/packages/core/src/ai/prompts/coScout/legacy.ts b/packages/core/src/ai/prompts/coScout/legacy.ts index 46eb940aa..33b5186cb 100644 --- a/packages/core/src/ai/prompts/coScout/legacy.ts +++ b/packages/core/src/ai/prompts/coScout/legacy.ts @@ -17,7 +17,7 @@ import type { import type { ToolDefinition, MessageContent, InputContentPart } from '../../responsesApi'; import type { Locale } from '../../../i18n/types'; import type { AnalysisMode } from '../../../types'; -import type { SuspectedCause } from '../../../findings/types'; +import type { Hypothesis } from '../../../findings/types'; import { formatStatistic } from '../../../i18n/format'; import { buildLocaleHint, TERMINOLOGY_INSTRUCTION } from '../shared'; import { buildSummaryPrompt } from '../narration'; @@ -31,8 +31,8 @@ export interface BuildCoScoutToolsOptions { investigationPhase?: InvestigationPhase; /** Whether user is on Team plan (enables sharing tools) */ isTeamPlan?: boolean; - /** Existing suspected cause hubs — enables connect_hub_evidence when non-empty */ - existingHubs?: SuspectedCause[]; + /** Existing Hypothesis hubs — enables connect_hub_evidence when non-empty */ + existingHubs?: Hypothesis[]; } /** @@ -310,7 +310,7 @@ export function buildCoScoutTools(options: BuildCoScoutToolsOptions = {}): ToolD properties: { text: { type: 'string', - description: 'Question text describing the suspected cause to investigate', + description: 'Question text describing the hypothesis to investigate', }, factor: { type: ['string', 'null'], @@ -375,7 +375,7 @@ export function buildCoScoutTools(options: BuildCoScoutToolsOptions = {}): ToolD type: 'function', name: 'suggest_improvement_idea', description: - 'Propose an improvement idea for an answered question. Ideas bridge root cause analysis and corrective actions. The analyst can edit, run What-If simulation, and select for implementation. Use the Four Ideation Directions to classify the approach. Prefer lean improvements — simplest fix that addresses the root cause.', + 'Propose an improvement idea for an answered question. Ideas bridge mechanism analysis and corrective actions. The analyst can edit, run What-If simulation, and select for implementation. Use the Four Ideation Directions to classify the approach. Prefer lean improvements — simplest fix that addresses the key contribution.', parameters: { type: 'object', properties: { @@ -442,11 +442,11 @@ export function buildCoScoutTools(options: BuildCoScoutToolsOptions = {}): ToolD properties: { question_id: { type: 'string', - description: 'ID of the question (suspected cause) being brainstormed', + description: 'ID of the question (hypothesis) being brainstormed', }, cause_name: { type: 'string', - description: 'Name of the suspected cause for context', + description: 'Name of the hypothesis for context', }, ideas: { type: 'array', @@ -489,7 +489,7 @@ export function buildCoScoutTools(options: BuildCoScoutToolsOptions = {}): ToolD type: 'string', description: 'Concise insight text, e.g., "Nozzle 3 shows 2x variation of other nozzles — ' + - 'cleaning frequency is the likely root cause (eta-squared 0.42)"', + 'cleaning frequency is the likely key contribution (eta-squared 0.42)"', }, reasoning: { type: 'string', @@ -614,9 +614,9 @@ export function buildCoScoutTools(options: BuildCoScoutToolsOptions = {}): ToolD if (investigationPhase === 'validating' || investigationPhase === 'converging') { tools.push({ type: 'function', - name: 'suggest_suspected_cause', + name: 'suggest_hypothesis', description: - 'Suggest a suspected cause hub that connects related questions and findings into a named mechanism. Use when you notice 2+ answered questions pointing to the same root cause during validating or converging phase.', + 'Suggest a Hypothesis hub that connects related questions and findings into a named mechanism. Use when you notice 2+ answered questions pointing to the same contribution during validating or converging phase.', parameters: { type: 'object', properties: { @@ -653,11 +653,11 @@ export function buildCoScoutTools(options: BuildCoScoutToolsOptions = {}): ToolD type: 'function', name: 'connect_hub_evidence', description: - 'Connect newly answered questions or findings to an existing suspected cause hub. Use when new evidence supports an already-named mechanism.', + 'Connect newly answered questions or findings to an existing Hypothesis hub. Use when new evidence supports an already-named mechanism.', parameters: { type: 'object', properties: { - hubId: { type: 'string', description: 'ID of the existing suspected cause hub' }, + hubId: { type: 'string', description: 'ID of the existing Hypothesis hub' }, questionIds: { type: 'array', items: { type: 'string' }, @@ -919,10 +919,10 @@ Never invent data or statistics. If the context does not contain enough informat invParts.push(`**Currently investigating:** ${investigation.focusedQuestionText}`); } - // Convergence synthesis — the analyst's suspected cause narrative + // Convergence synthesis — the analyst's hypothesis narrative if (options.synthesis) { invParts.push( - `Synthesis (suspected cause narrative): "${options.synthesis}". Use this to ground improvement suggestions. Do not say "confirmed" — use "the evidence suggests" or "the evidence points to".` + `Synthesis (hypothesis narrative): "${options.synthesis}". Use this to ground improvement suggestions. Do not say "confirmed" — use "the evidence suggests" or "the evidence points to".` ); } @@ -978,10 +978,10 @@ Never invent data or statistics. If the context does not contain enough informat diverging: 'The investigation is exploring questions — some have been answered, new follow-up questions are emerging. Tell the user: "You\'re exploring questions — let\'s check the open questions systematically, starting with the highest-ranked ones."', validating: - 'Evidence is building — some questions are answered, some ruled out. Tell the user: "Evidence is building — let\'s see which suspected causes the answered questions point to."', + 'Evidence is building — some questions are answered, some ruled out. Tell the user: "Evidence is building — let\'s see which hypotheses the answered questions point to."', converging: hasAnsweredQuestions - ? 'The investigation is narrowing down. Tell the user: "You\'re identifying suspected causes — answered questions found. Let\'s brainstorm improvement ideas."' - : 'The investigation is narrowing down. Tell the user: "You\'re identifying suspected causes — let\'s synthesize findings into a coherent story."', + ? 'The investigation is narrowing down. Tell the user: "You\'re identifying hypotheses — answered questions found. Let\'s brainstorm improvement ideas."' + : 'The investigation is narrowing down. Tell the user: "You\'re identifying hypotheses — let\'s synthesize findings into a coherent story."', improving: (() => { const sf = investigation?.selectedFinding; const hasActions = sf?.actions && sf.actions.length > 0; @@ -992,13 +992,13 @@ Never invent data or statistics. If the context does not contain enough informat } else if (hasActions) { return 'PDCA: Do — Corrective actions are in progress. Track progress, flag overdue items, and keep the team focused on execution. Do not suggest new actions unless asked.'; } else { - return 'PDCA: Plan — No corrective actions yet. Help the analyst brainstorm improvement ideas targeting the suspected causes identified from answered questions, search the Knowledge Base for similar past fixes, and convert selected ideas into executable action items.'; + return 'PDCA: Plan — No corrective actions yet. Help the analyst brainstorm improvement ideas targeting the hypotheses identified from answered questions, search the Knowledge Base for similar past fixes, and convert selected ideas into executable action items.'; } })(), }; - // Override converging/improving with suspected cause context when available - // Supports multiple suspected causes from question-driven investigation + // Override converging/improving with hypothesis context when available + // Supports multiple hypotheses from question-driven investigation if (investigation.suspectedCauses && investigation.suspectedCauses.length > 0) { const causes = investigation.suspectedCauses; @@ -1020,7 +1020,7 @@ Never invent data or statistics. If the context does not contain enough informat const causesSummary = parts.join('. ') + '.'; if (investigation.phase === 'converging') { - phaseInstructions.converging = `The investigation is narrowing down. ${causesSummary} Let's brainstorm improvement ideas targeting each suspected cause.`; + phaseInstructions.converging = `The investigation is narrowing down. ${causesSummary} Let's brainstorm improvement ideas targeting each hypothesis.`; } if (investigation.phase === 'improving') { const basePdca = phaseInstructions.improving; @@ -1109,7 +1109,7 @@ Never invent data or statistics. If the context does not contain enough informat if (f.outcome) { line += ` outcome: ${f.outcome.effective}`; if (f.outcome.cpkDelta !== undefined) { - line += ` cpkDelta: ${f.outcome.cpkDelta > 0 ? '+' : ''}${f.outcome.cpkDelta.toFixed(2)}`; + line += ` cpkDelta: ${f.outcome.cpkDelta > 0 ? '+' : ''}${formatStatistic(f.outcome.cpkDelta, 'en', 2)}`; } } return line; @@ -1131,9 +1131,7 @@ Never invent data or statistics. If the context does not contain enough informat const significantInteractions = investigation.interactionEffects .filter(r => r.deltaRSquaredAdj > 0.02) .map(r => { - const deltaStr = Number.isFinite(r.deltaRSquaredAdj) - ? (r.deltaRSquaredAdj * 100).toFixed(1) - : '?'; + const deltaStr = formatStatistic(r.deltaRSquaredAdj * 100, 'en', 1); return `${r.factors[0]} \u00d7 ${r.factors[1]}: \u0394R\u00b2adj=${deltaStr}%`; }); if (significantInteractions.length > 0) { @@ -1182,15 +1180,15 @@ Never invent data or statistics. If the context does not contain enough informat The analyst is viewing Cp/Cpk per subgroup on the I-Chart. This reveals whether process capability itself is stable over time. Current state: ${cs.subgroupCount} subgroups (${subgroupDesc}) -Mean Cpk: ${cs.meanCpk.toFixed(2)}, range: ${cs.minCpk.toFixed(2)}–${cs.maxCpk.toFixed(2)} +Mean Cpk: ${formatStatistic(cs.meanCpk, 'en', 2)}, range: ${formatStatistic(cs.minCpk, 'en', 2)}–${formatStatistic(cs.maxCpk, 'en', 2)} In-control: ${cs.cpkInControl}/${cs.subgroupCount} subgroups`; if (cs.cpkTarget !== undefined && cs.subgroupsMeetingTarget !== undefined) { - capSection += `\nCpk target: ${cs.cpkTarget.toFixed(2)} — ${cs.subgroupsMeetingTarget}/${cs.subgroupCount} subgroups meet target`; + capSection += `\nCpk target: ${formatStatistic(cs.cpkTarget, 'en', 2)} — ${cs.subgroupsMeetingTarget}/${cs.subgroupCount} subgroups meet target`; } if (cs.centeringLoss !== undefined) { - capSection += `\nCentering loss: ${cs.centeringLoss.toFixed(3)} (gap between mean Cp and mean Cpk)`; + capSection += `\nCentering loss: ${formatStatistic(cs.centeringLoss, 'en', 3)} (gap between mean Cp and mean Cpk)`; } capSection += ` @@ -1270,7 +1268,7 @@ Coaching workflow: 1. "Which channels need attention?" → Read the Pareto. Count critical/warning channels. 2. "Why is this channel worst?" → Compare its boxplot to peers. Centering issue or spread issue? 3. "Is the problem systematic?" → Check the I-Chart. Are bad channels clustered or scattered? -4. "What should I do?" → Click the worst channel to switch to standard analysis. Add factors (Shift, Operator) to investigate root cause. +4. "What should I do?" → Click the worst channel to switch to standard analysis. Add factors (Shift, Operator) to investigate key contribution. Never use standard SPC terminology (control limits, Nelson rules) for the channel comparison view. Those apply after drilling into a single channel.` ); @@ -1295,9 +1293,9 @@ Never use standard SPC terminology (control limits, Nelson rules) for the channe diverging: "The analyst is exploring broadly. Encourage checking unexplored factors — mention coverage progress if available. Suggest gemba walks or expert input for factors that data alone can't explain.", validating: - 'The analyst is gathering evidence. Focus on evidence quality — suggest gemba validation for statistical findings, expert input where data is inconclusive. When you see 2+ answered questions pointing to the same mechanism, use suggest_suspected_cause to help them name it.', + 'The analyst is gathering evidence. Focus on evidence quality — suggest gemba validation for statistical findings, expert input where data is inconclusive. When you see 2+ answered questions pointing to the same mechanism, use suggest_hypothesis to help them name it.', converging: - 'The analyst is synthesizing findings into mechanisms. Help them name suspected causes — use suggest_suspected_cause when you see evidence clustering. Connect new evidence to existing hubs with connect_hub_evidence. Highlight coverage progress.', + 'The analyst is synthesizing findings into mechanisms. Help them name hypotheses — use suggest_hypothesis when you see evidence clustering. Connect new evidence to existing hubs with connect_hub_evidence. Highlight coverage progress.', improving: 'The analyst is in the improvement phase. Focus on PDCA execution — track actions, verify outcomes, suggest sustaining controls.', }; @@ -1316,7 +1314,7 @@ Never use standard SPC terminology (control limits, Nelson rules) for the channe yamazumi: 'Focus on waste elimination. Which activities contribute most waste? Think lean: eliminate, simplify, combine, reduce.', performance: - 'Focus on channel health. Which channels are worst performers? Same root cause across channels?', + 'Focus on channel health. Which channels are worst performers? Same contribution across channels?', }; const currentMode = options.analysisMode ?? 'standard'; @@ -1328,7 +1326,7 @@ Never use standard SPC terminology (control limits, Nelson rules) for the channe parts.push( `Evidence Map coaching: - When you see interaction terms with \u0394R\u00b2 > 2%, consider suggesting a causal link using suggest_causal_link. -- When you see a convergence point (factor with 2+ incoming causal links) without a SuspectedCause hub, suggest creating one. +- When you see a convergence point (factor with 2+ incoming causal links) without a Hypothesis hub, suggest creating one. - When you see a causal link with evidenceType "unvalidated", suggest what evidence would validate it (gemba observation, expert consultation, or additional data collection). - Use [REF:evidence-node:FACTOR_NAME]factor text[/REF] to create clickable highlights on the Evidence Map. - Use [REF:evidence-edge:LINK_ID]link description[/REF] to highlight a specific causal link on the map.` @@ -1374,7 +1372,7 @@ Never use standard SPC terminology (control limits, Nelson rules) for the channe - A validated question conclusion (answered or ruled out) - A quantitative process insight (specific eta-squared, Cpk shift, or defect rate) - A negative learning (approach tried and found ineffective — equally valuable) - - A root cause identification with supporting evidence + - A key contribution identification with supporting evidence - A cross-factor interaction discovered during drill-down - Include negative learnings: "Adjusting temperature had no effect on variation (eta-squared < 0.01)" is as valuable as positive findings. - Do NOT suggest saving generic observations or restating what's already in findings. @@ -1592,7 +1590,7 @@ PDCA coaching (when investigation phase is 'improving'): - If ideas have What-If projections, compare projected impact. Suggest prioritizing the idea with highest Cpk improvement. - Suggest classifying ideas by direction: prevent (stop the cause), detect (catch it sooner), simplify (reduce complexity), eliminate (remove the step). - Use suggest_action to convert selected ideas into executable tasks with clear owners and deadlines. - - Use suggest_improvement_idea to propose ideas for answered questions based on the suspected cause and KB results. + - Use suggest_improvement_idea to propose ideas for answered questions based on the hypothesis and KB results. - DO (finding at 'improving' status, actions in progress): - Acknowledge action progress: "N of M actions complete." @@ -1616,7 +1614,7 @@ PDCA coaching (when investigation phase is 'improving'): - Cpk improved but still below target: suggest "partial" - Cpk unchanged or degraded: suggest "not effective" - For effective: suggest sustaining controls — update SOPs, set control chart limits, schedule 30-day follow-up. - - For partial/not effective: suggest which factors to re-investigate, whether the root cause was actually addressed. + - For partial/not effective: suggest which factors to re-investigate, whether the key contribution was actually addressed. Improvement idea guidance (converging/IMPROVE): - Use suggest_improvement_idea when a question is answered and the analyst needs ideas for what to try. @@ -1629,8 +1627,8 @@ Improvement idea guidance (converging/IMPROVE): - Always estimate timeframe: just-do (existing resources, no approval), days (minor coordination), weeks (planning, moderate resources), months (investment, cross-team). - Always estimate cost: none (no cost), low (team budget), medium (needs approval), high (capital investment). - If you can assess the risk, provide risk_axis1 (process impact 1-3) and risk_axis2 (safety impact 1-3). Set null if uncertain. -- Prefer lean improvements — the simplest fix that addresses the root cause. Suggest just-do and days timeframe ideas first. -- Assess feasibility: Does it remove the root cause? Can the team do it themselves? Can they try small first? Can they measure the result? +- Prefer lean improvements — the simplest fix that addresses the key contribution. Suggest just-do and days timeframe ideas first. +- Assess feasibility: Does it remove the key contribution? Can the team do it themselves? Can they try small first? Can they measure the result? - If Knowledge Base search revealed a past fix for a similar cause, suggest it as an improvement idea with the source cited. - suggest_improvement_idea only works on questions with 'answered' or 'investigating' status. @@ -1646,13 +1644,13 @@ Issue statement sharpening: - The issue statement should get more specific with each answered question. - Do NOT suggest sharpening after every minor observation — only after key insights. -Multiple suspected causes: -- Real investigations often identify multiple contributing factors, not a single root cause. +Multiple hypotheses: +- Real investigations often identify multiple contributing factors, not a single key contribution. - When setting causeRole, use 'suspected-cause' for factors with strong evidence (eta-squared > 15% or R²adj contribution), 'contributing' for moderate, 'ruled-out' for tested and eliminated. - Multiple 'suspected-cause' entries are valid — each becomes an improvement target. - Ruled-out factors are valuable negative learnings. Always acknowledge what was checked and eliminated. - When the user asks about a factor that appears in the question tree as ruled-out, reference its evidence so the analyst can cite it to stakeholders. -- When synthesizing results, list suspected causes ranked by evidence strength. +- When synthesizing results, list hypotheses ranked by evidence strength. The entry scenario may have changed since the previous turn. Always reference the current scenario in your tool decisions. @@ -1676,13 +1674,13 @@ function buildEntryScenarioGuidance(scenario: EntryScenario): string { return `Entry scenario: Problem to solve — The analyst has a specific problem (e.g., Cpk below target). Factor Intelligence has ranked the most likely factor combinations. - SCOUT: Start by reviewing the top-ranked questions from Factor Intelligence. Use compare_categories to verify the top contributors. Suggest apply_filter to drill into the highest-ranked factor. Propose create_finding for key observations. - INVESTIGATE: Guide the analyst to check open questions systematically, starting with the highest-ranked ones. Create follow-up questions as answers emerge. -- IMPROVE: Check whether Cpk has reached the original target. In PLAN sub-state, suggest ideas targeting the suspected causes from answered questions. In CHECK, compare before/after Cpk against the stated problem threshold. In ACT, assess whether the original problem is resolved.`; +- IMPROVE: Check whether Cpk has reached the original target. In PLAN sub-state, suggest ideas targeting the hypotheses from answered questions. In CHECK, compare before/after Cpk against the stated problem threshold. In ACT, assess whether the original problem is resolved.`; case 'exploration': return `Entry scenario: Exploration — The analyst entered with an upfront theory to check. The upfront theory becomes the first question to check. - SCOUT: Immediately use compare_categories on the factor named in the theory to verify it. Report η² and per-category stats. If answered (>=15%), suggest apply_filter and create_finding. - INVESTIGATE: Propose create_question with the upfront theory as the root question. Then suggest follow-up questions based on answers. -- IMPROVE: After addressing the suspected causes, compare before/after on the metric linked to the original theory. In PLAN, search KB for fixes to the confirmed factor. In ACT, verify whether the theory-specific metric improved.`; +- IMPROVE: After addressing the hypotheses, compare before/after on the metric linked to the original theory. In PLAN, search KB for fixes to the confirmed factor. In ACT, verify whether the theory-specific metric improved.`; case 'routine': return `Entry scenario: Routine check — No specific problem or theory. Scanning for signals. Factor Intelligence questions are available for proactive scanning. diff --git a/packages/core/src/ai/prompts/coScout/modes/defect.ts b/packages/core/src/ai/prompts/coScout/modes/defect.ts index ccd2887fc..4133a0a6f 100644 --- a/packages/core/src/ai/prompts/coScout/modes/defect.ts +++ b/packages/core/src/ai/prompts/coScout/modes/defect.ts @@ -38,7 +38,7 @@ Focus: Systematic Pareto-driven drill-down. Address the vital few before the tri 2. For each suspected factor, check eta-squared — does it explain significant variation in defect rate? 3. Use composition analysis: filter to a type, then switch Pareto factor to see which machines/products/shifts contribute 4. Validate with gemba: observe the process during high-defect periods, photograph defect examples -5. Synthesize suspected causes: "Machine 3 nozzle wear causes seal failures on night shift" +5. Synthesize hypotheses: "Machine 3 nozzle wear causes seal failures on night shift" Evidence strength: R-squared-adj from ANOVA on aggregated defect rates. Factors with eta-squared >= 15% are strong contributors. Negative learnings: Document factors that were checked and ruled out (eta-squared < 5%).`, diff --git a/packages/core/src/ai/prompts/coScout/modes/standard.ts b/packages/core/src/ai/prompts/coScout/modes/standard.ts index f81b3d9d7..ac3cfdf3e 100644 --- a/packages/core/src/ai/prompts/coScout/modes/standard.ts +++ b/packages/core/src/ai/prompts/coScout/modes/standard.ts @@ -31,18 +31,18 @@ Focus: Systematic factor-by-factor stratification. Evidence strength = R-squared investigate: `Workflow steps: 1. Create questions for top factors — each question targets a specific factor or level 2. Validate with three evidence types: data (auto eta-squared), gemba (go-see), expert knowledge -3. Synthesize answered questions into suspected causes — name the mechanism, not just the factor +3. Synthesize answered questions into hypotheses — name the mechanism, not just the factor 4. Use the Evidence Map to visualize factor relationships and causal links Focus: Question-driven investigation. Each question narrows the search space.`, improve: `Workflow steps: -1. HMW brainstorm per suspected cause — How Might We prevent, detect, simplify, or eliminate? +1. HMW brainstorm per hypothesis — How Might We prevent, detect, simplify, or eliminate? 2. Prioritize by impact x effort — use the Prioritization Matrix to rank ideas 3. PDCA execution — Plan actions, Do the work, Check with staged analysis, Act on results 4. Verify with Before/After comparison — staged analysis confirms improvement -Focus: Lean improvement targeting the root cause. Simplest fix that addresses the mechanism.`, +Focus: Lean improvement targeting the contribution. Simplest fix that addresses the mechanism.`, }; /** diff --git a/packages/core/src/ai/prompts/coScout/phases/improve.ts b/packages/core/src/ai/prompts/coScout/phases/improve.ts index e8e99d701..ec638833d 100644 --- a/packages/core/src/ai/prompts/coScout/phases/improve.ts +++ b/packages/core/src/ai/prompts/coScout/phases/improve.ts @@ -1,7 +1,7 @@ /** * IMPROVE phase coaching — HMW brainstorm, PDCA execution, staged verification. * - * The analyst has identified suspected causes and is now improving the process. + * The analyst has identified hypotheses and is now improving the process. * CoScout coaches through the PDCA cycle: Plan, Do, Check, Act. */ @@ -10,9 +10,9 @@ import type { EntryScenario } from '../../../types'; const MODE_IMPROVE_GUIDANCE: Record = { standard: `Improvement focus: -- Target improvements at the suspected causes identified from answered questions. -- Use R-squared-adj contribution to prioritize which causes to address first. -- Prefer lean improvements — the simplest fix that addresses the root cause. +- Target improvements at the hypotheses identified from answered questions. +- Use R-squared-adj contribution to prioritize which hypotheses to address first. +- Prefer lean improvements — the simplest fix that addresses the mechanism. - After improvement, verify using staged analysis: compare before/after Cpk and variation ratio.`, yamazumi: `Improvement focus: @@ -30,7 +30,7 @@ const MODE_IMPROVE_GUIDANCE: Record = { performance: `Improvement focus: - Target worst-performing channels — prioritize by Cpk gap to target. -- Check whether multiple bad channels share a root cause (position, maintenance, operating conditions). +- Check whether multiple bad channels share a mechanism (position, maintenance, operating conditions). - After fixing channels, verify using staged analysis: compare before/after worst-channel Cpk. - If the problem is centering (mean off-target), the fix is different from spread (too much variation).`, }; @@ -54,12 +54,12 @@ ${MODE_IMPROVE_GUIDANCE[mode]}`); parts.push(`HMW brainstorm coaching: - Use suggest_improvement_idea when a question is answered and the analyst needs ideas for what to try. - Classify each idea using the Four Ideation Directions: - - prevent: stop the cause from occurring (poka-yoke, maintenance schedule, SOP update) + - prevent: stop the mechanism from occurring (poka-yoke, maintenance schedule, SOP update) - detect: catch it sooner before defects (sensor, alarm, control chart alert) - simplify: reduce complexity to reduce error opportunities (fewer steps, visual guides) - eliminate: remove the step or factor entirely (automate, redesign) - Suggest just-do and days timeframe ideas first — lean improvements over capital projects. -- If Knowledge Base search reveals a past fix for a similar cause, suggest it with the source cited.`); +- If Knowledge Base search reveals a past fix for a similar mechanism, suggest it with the source cited.`); // PDCA execution coaching parts.push(`PDCA execution coaching: @@ -74,7 +74,7 @@ ${MODE_IMPROVE_GUIDANCE[mode]}`); - ACT (all actions complete): Propose outcome assessment: - Effective: Cpk after >= target AND variation ratio < 1.0 — suggest sustaining controls (update SOPs, set control chart limits, schedule 30-day follow-up) - Partial: Cpk improved but still below target — suggest which factors to re-investigate - - Not effective: Cpk unchanged or degraded — suggest whether the root cause was actually addressed`); + - Not effective: Cpk unchanged or degraded — suggest whether the hypothesized mechanism was actually addressed`); // Action tracking parts.push(`Action tracking: diff --git a/packages/core/src/ai/prompts/coScout/phases/investigate.ts b/packages/core/src/ai/prompts/coScout/phases/investigate.ts index 9bd5cc1bd..8943e43bb 100644 --- a/packages/core/src/ai/prompts/coScout/phases/investigate.ts +++ b/packages/core/src/ai/prompts/coScout/phases/investigate.ts @@ -2,7 +2,7 @@ * INVESTIGATE phase coaching — question-driven EDA, Evidence Map, hub synthesis. * * The analyst is building an investigation tree, validating evidence, and - * synthesizing suspected causes. This is the longest phase with three sub-phases: + * synthesizing hypotheses. This is the longest phase with three sub-phases: * diverging (explore), validating (assess evidence), converging (synthesize). */ @@ -33,7 +33,7 @@ const MODE_QUESTION_GUIDANCE: Record = { - Frame questions around channel health: "Why does channel [X] have lower Cpk than its neighbors?" - Evidence strength = Cpk deviation from the fleet average. - Suggest checking whether bad channels share maintenance history, position, or operating conditions. -- Cross-channel questions: "Is the same root cause affecting multiple channels?"`, +- Cross-channel questions: "Is the same contribution affecting multiple channels?"`, }; const SUB_PHASE_COACHING: Record = { @@ -51,11 +51,11 @@ const SUB_PHASE_COACHING: Record = { validating: `Sub-phase: Validating — Evidence is building. Some questions are answered, some ruled out. Help the analyst: - Focus on evidence quality — suggest gemba validation for statistical findings. - Suggest expert input where data is inconclusive. -- When you see 2+ answered questions pointing to the same mechanism, use suggest_suspected_cause to help them name it. +- When you see 2+ answered questions pointing to the same mechanism, use suggest_hypothesis to help them name it. - Validate each evidence type: data (auto eta-squared), gemba (go-see observation), expert (domain knowledge).`, - converging: `Sub-phase: Converging — The investigation is narrowing down to suspected causes. Help the analyst: -- Name suspected causes — use suggest_suspected_cause when you see evidence clustering. + converging: `Sub-phase: Converging — The investigation is narrowing down to hypotheses. Help the analyst: +- Name hypotheses — use suggest_hypothesis when you see evidence clustering. - Connect new evidence to existing hubs with connect_hub_evidence. - Highlight coverage progress — which areas have been thoroughly investigated vs gaps. - Begin transitioning to improvement thinking: "What would it take to address this cause?"`, @@ -99,7 +99,7 @@ ${MODE_QUESTION_GUIDANCE[mode]}`); // Evidence Map coaching parts.push(`Evidence Map coaching: - When you see interaction terms with delta-R-squared > 2%, consider suggesting a causal link using suggest_causal_link. -- When you see a convergence point (factor with 2+ incoming causal links) without a SuspectedCause hub, suggest creating one. +- When you see a convergence point (factor with 2+ incoming causal links) without a Hypothesis hub, suggest creating one. - When you see a causal link with evidenceType "unvalidated", suggest what evidence would validate it (gemba observation, expert consultation, or additional data). - Use [REF:evidence-node:FACTOR_NAME]factor text[/REF] to create clickable highlights on the Evidence Map. - Use [REF:evidence-edge:LINK_ID]link description[/REF] to highlight a specific causal link on the map.`); @@ -107,10 +107,10 @@ ${MODE_QUESTION_GUIDANCE[mode]}`); // Hub synthesis coaching (validating + converging) if (investigationPhase === 'validating' || investigationPhase === 'converging') { parts.push(`Hub synthesis coaching: -- When 2+ answered questions point to the same root cause, use suggest_suspected_cause to name the mechanism. -- Each suspected cause hub connects related questions and findings into a named mechanism. +- When 2+ answered questions point to the same contribution, use suggest_hypothesis to name the mechanism. +- Each Hypothesis hub connects related questions and findings into a named mechanism. - Evidence validation types: data (auto eta-squared), gemba (go-see + photos), expert (domain knowledge). -- Multiple suspected causes are normal — real investigations often identify several contributing factors. +- Multiple hypotheses are normal — real investigations often identify several contributing factors. - Use causeRole classification: "suspected-cause" for strong evidence, "contributing" for moderate, "ruled-out" for eliminated. - Ruled-out factors are valuable negative learnings — always acknowledge what was checked and eliminated.`); } diff --git a/packages/core/src/ai/prompts/coScout/tier2/investigationDiscipline.ts b/packages/core/src/ai/prompts/coScout/tier2/investigationDiscipline.ts index 227702173..8b6841607 100644 --- a/packages/core/src/ai/prompts/coScout/tier2/investigationDiscipline.ts +++ b/packages/core/src/ai/prompts/coScout/tier2/investigationDiscipline.ts @@ -3,7 +3,7 @@ * phase (applies to both Map and Wall views — coaching is view-agnostic). * * Terminology rules (ESLint `no-root-cause-language`, `no-interaction-moderator`): - * - Never "root cause" — use "contribution" or "suspected cause" + * - Never "root cause" — use "contribution" or "hypothesis" * - Never "moderator" / "primary factor" — describe interactions as ordinal / disordinal * - Reference chart elements via REF tokens (ADR-057), never raw row indices */ @@ -12,6 +12,6 @@ export const investigationDisciplinePrompt = `When the analyst is in the Investi - Prioritize disconfirmation over confirmation. Flag hypotheses with 3 or more supporters and no attempted contradictor. - For each hypothesis without a guiding question, propose one. - When best-subsets reveals a column with ΔR²adj > 0.10 that no hypothesis covers, suggest adding it. -- Describe hypotheses as contributions or suspected causes. +- Describe hypotheses as contributions or candidate explanations. - Reference chart elements via REF tokens (ADR-057), never raw row indices. - Describe interaction findings as ordinal or disordinal.`; diff --git a/packages/core/src/ai/prompts/coScout/tools/registry.ts b/packages/core/src/ai/prompts/coScout/tools/registry.ts index a770137a5..aba580377 100644 --- a/packages/core/src/ai/prompts/coScout/tools/registry.ts +++ b/packages/core/src/ai/prompts/coScout/tools/registry.ts @@ -14,7 +14,7 @@ import type { ToolDefinition } from '../../../responsesApi'; import type { JourneyPhase, InvestigationPhase } from '../../../types'; import type { AnalysisMode } from '../../../../types'; -import type { SuspectedCause } from '../../../../findings/types'; +import type { Hypothesis } from '../../../../findings/types'; // ── Registry entry type ──────────────────────────────────────────────── @@ -32,7 +32,7 @@ export interface ToolRegistryEntry { /** Optional: dynamic availability condition */ condition?: (ctx: { investigationPhase?: InvestigationPhase; - existingHubs?: SuspectedCause[]; + existingHubs?: Hypothesis[]; }) => boolean; } @@ -368,7 +368,7 @@ export const TOOL_REGISTRY: Record = { properties: { text: { type: 'string', - description: 'Question text describing the suspected cause to investigate', + description: 'Question text describing the hypothesis to investigate', }, factor: { type: ['string', 'null'], @@ -446,7 +446,7 @@ export const TOOL_REGISTRY: Record = { type: 'function', name: 'propose_hypothesis_from_finding', description: - "Create a new suspected-cause hub seeded with an existing finding as first evidence. Condition auto-derives from the finding's source. Requires analyst confirmation before the hub is committed.", + "Create a new Hypothesis hub seeded with an existing finding as first evidence. Condition auto-derives from the finding's source. Requires analyst confirmation before the hub is committed.", parameters: { type: 'object', properties: { @@ -468,12 +468,12 @@ export const TOOL_REGISTRY: Record = { phases: ['investigate'], }, - suggest_suspected_cause: { + suggest_hypothesis: { definition: { type: 'function', - name: 'suggest_suspected_cause', + name: 'suggest_hypothesis', description: - 'Suggest a suspected cause hub that connects related questions and findings into a named mechanism. Use when you notice 2+ answered questions pointing to the same contributing factor during validating or converging phase.', + 'Suggest a Hypothesis hub that connects related questions and findings into a named mechanism. Use when you notice 2+ answered questions pointing to the same contributing factor during validating or converging phase.', parameters: { type: 'object', properties: { @@ -513,11 +513,11 @@ export const TOOL_REGISTRY: Record = { type: 'function', name: 'connect_hub_evidence', description: - 'Connect newly answered questions or findings to an existing suspected cause hub. Use when new evidence supports an already-named mechanism.', + 'Connect newly answered questions or findings to an existing Hypothesis hub. Use when new evidence supports an already-named mechanism.', parameters: { type: 'object', properties: { - hubId: { type: 'string', description: 'ID of the existing suspected cause hub' }, + hubId: { type: 'string', description: 'ID of the existing Hypothesis hub' }, questionIds: { type: 'array', items: { type: 'string' }, @@ -653,11 +653,11 @@ export const TOOL_REGISTRY: Record = { properties: { question_id: { type: 'string', - description: 'ID of the question (suspected cause) being brainstormed', + description: 'ID of the question (hypothesis) being brainstormed', }, cause_name: { type: 'string', - description: 'Name of the suspected cause for context', + description: 'Name of the hypothesis for context', }, ideas: { type: 'array', @@ -895,7 +895,7 @@ export function getToolsForPhase( options?: { isTeamPlan?: boolean; investigationPhase?: InvestigationPhase; - existingHubs?: SuspectedCause[]; + existingHubs?: Hypothesis[]; } ): ToolDefinition[] { return Object.values(TOOL_REGISTRY) diff --git a/packages/core/src/ai/prompts/narration.ts b/packages/core/src/ai/prompts/narration.ts index 4bed8832c..8dfc1c763 100644 --- a/packages/core/src/ai/prompts/narration.ts +++ b/packages/core/src/ai/prompts/narration.ts @@ -170,7 +170,7 @@ export function buildSummaryPrompt(context: AIContext): string { // Convergence synthesis if (context.process?.synthesis) { - parts.push(`Synthesis (suspected cause): ${context.process.synthesis}`); + parts.push(`Synthesis (hypothesis): ${context.process.synthesis}`); } // Staged comparison (Before/After verification) diff --git a/packages/core/src/ai/suggestedQuestions.ts b/packages/core/src/ai/suggestedQuestions.ts index 30b041c81..87d758aab 100644 --- a/packages/core/src/ai/suggestedQuestions.ts +++ b/packages/core/src/ai/suggestedQuestions.ts @@ -52,7 +52,7 @@ const PHASE_QUESTIONS: Record = { "What's the simplest change to improve process capability?", ], improving: [ - 'Are the corrective actions addressing the suspected cause?', + 'Are the corrective actions addressing the hypothesis?', 'What does the Capability chart show — is Cpk improving?', ], }; diff --git a/packages/core/src/ai/types.ts b/packages/core/src/ai/types.ts index d7217024d..8c3ae9d2d 100644 --- a/packages/core/src/ai/types.ts +++ b/packages/core/src/ai/types.ts @@ -11,6 +11,7 @@ import type { EvidenceSource, EvidenceSnapshot } from '../evidenceSources'; import type { ProcessMomentDefinition } from '../processMoments'; import type { SignalCard } from '../signalCards'; import type { ProcessStateNote } from '../processStateNote'; +import type { HypothesisStatus } from '../findings/types'; /** AI model tier — maps to ARM deployment names ('fast' or 'reasoning') */ export type AITier = 'fast' | 'reasoning'; @@ -133,7 +134,7 @@ export interface ProcessContext { targetDirection?: 'minimize' | 'maximize' | 'target'; /** Factor role classifications derived from investigation categories */ factorRoles?: Record; - /** Convergence synthesis — suspected cause narrative (max 500 chars) */ + /** Convergence synthesis — hypothesis narrative (max 500 chars) */ synthesis?: string; /** * User-built visual Process Map (FRAME workspace). @@ -327,12 +328,12 @@ export interface AIContext { focusedQuestionId?: string; /** Text of the question currently in focus in the PI panel */ focusedQuestionText?: string; - /** Suspected cause hubs (Phase 6 — distinct from legacy causeRole-based suspectedCauses) */ - suspectedCauseHubs?: Array<{ + /** Hypothesis hubs (Phase 6 — distinct from legacy causeRole-based suspectedCauses) */ + hypothesisHubs?: Array<{ id: string; name: string; synthesis: string; - status: string; + status: HypothesisStatus; questionCount: number; findingCount: number; evidence?: { diff --git a/packages/core/src/evidenceMap/index.ts b/packages/core/src/evidenceMap/index.ts index aa2703c15..4dfcdd8ef 100644 --- a/packages/core/src/evidenceMap/index.ts +++ b/packages/core/src/evidenceMap/index.ts @@ -14,6 +14,8 @@ * in @variscout/charts where they belong. */ +import type { HypothesisStatus } from '../findings/types'; + import type { RelationshipType } from '../stats/causalGraph'; export type { RelationshipType }; @@ -105,6 +107,6 @@ export interface ConvergencePointData { y: number; incomingCount: number; hubName?: string; - hubStatus?: 'suspected' | 'confirmed' | 'not-confirmed'; + hubStatus?: HypothesisStatus; projectedImprovement?: string; } diff --git a/packages/core/src/findings/__tests__/helpers.test.ts b/packages/core/src/findings/__tests__/helpers.test.ts index caa3de29d..0c18715c9 100644 --- a/packages/core/src/findings/__tests__/helpers.test.ts +++ b/packages/core/src/findings/__tests__/helpers.test.ts @@ -1,6 +1,12 @@ import { describe, it, expect } from 'vitest'; -import { computeHubProjection, detectEvidenceClusters } from '../helpers'; -import type { SuspectedCause, Question } from '../types'; +import { + computeHubContribution, + computeHubEvidence, + computeHubProjection, + detectEvidenceClusters, + migrateCauseRolesToHubs, +} from '../helpers'; +import type { Hypothesis, Question } from '../types'; import type { BestSubsetsResult, BestSubsetResult } from '../../stats/bestSubsets'; // --------------------------------------------------------------------------- @@ -20,13 +26,13 @@ function makeQuestion(overrides: Partial & { id: string }): Question { }; } -function makeHub(overrides: Partial & { id: string }): SuspectedCause { +function makeHub(overrides: Partial & { id: string }): Hypothesis { return { name: 'Test hub', synthesis: '', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, investigationId: 'inv-test-001', @@ -66,6 +72,113 @@ function makeSubset( }; } +// --------------------------------------------------------------------------- +// Tests: contribution + evidence helpers +// --------------------------------------------------------------------------- + +describe('computeHubContribution', () => { + it('sums etaSquared and falls back to rSquaredAdj for linked questions only', () => { + const questions = [ + makeQuestion({ id: 'q1', evidence: { etaSquared: 0.34 } }), + makeQuestion({ id: 'q2', evidence: { rSquaredAdj: 0.22 } }), + makeQuestion({ id: 'q3', evidence: { etaSquared: 0.5 } }), + ]; + const hub = makeHub({ id: 'h1', questionIds: ['q1', 'q2'] }); + + expect(computeHubContribution(hub, questions)).toBeCloseTo(0.56); + }); + + it('returns zero for a hypothesis with no connected questions', () => { + expect(computeHubContribution(makeHub({ id: 'h1', questionIds: [] }), [])).toBe(0); + }); +}); + +describe('migrateCauseRolesToHubs', () => { + it('creates one proposed hypothesis for each legacy suspected-cause question with a factor', () => { + const questions = [ + makeQuestion({ + id: 'q1', + factor: 'Shift', + causeRole: 'suspected-cause', + linkedFindingIds: ['f1'], + }), + makeQuestion({ id: 'q2', factor: 'Head', causeRole: 'suspected-cause' }), + makeQuestion({ id: 'q3', factor: 'Batch', causeRole: 'ruled-out' }), + makeQuestion({ id: 'q4', causeRole: 'suspected-cause' }), + ]; + + const hubs = migrateCauseRolesToHubs(questions); + + expect(hubs).toHaveLength(2); + expect(hubs[0]).toMatchObject({ + name: 'Shift', + questionIds: ['q1'], + findingIds: ['f1'], + status: 'proposed', + }); + expect(hubs[1]).toMatchObject({ name: 'Head', questionIds: ['q2'], status: 'proposed' }); + }); +}); + +describe('computeHubEvidence', () => { + const bestSubsets = makeBestSubsetsResult( + [ + makeSubset(['Shift'], 0.34, new Map()), + makeSubset(['Head'], 0.28, new Map()), + makeSubset(['Head', 'Shift'], 0.52, new Map()), + ], + 50 + ); + + it('uses Best Subsets R-squared-adj for combined factors instead of naive sum', () => { + const questions = [ + makeQuestion({ id: 'q1', factor: 'Shift', evidence: { etaSquared: 0.34 } }), + makeQuestion({ id: 'q2', factor: 'Head', evidence: { etaSquared: 0.28 } }), + ]; + const hub = makeHub({ id: 'h1', questionIds: ['q1', 'q2'] }); + + const evidence = computeHubEvidence(hub, questions, bestSubsets); + + expect(evidence.contribution.value).toBeCloseTo(0.52); + expect(evidence.contribution.label).toBe('R²adj'); + expect(evidence.contribution.description).toContain('52%'); + }); + + it('falls back to capped evidence sums and partial factor matches', () => { + const questions = [ + makeQuestion({ id: 'q1', factor: 'Shift', evidence: { etaSquared: 0.34 } }), + makeQuestion({ id: 'q2', factor: 'Head', evidence: { etaSquared: 0.28 } }), + makeQuestion({ id: 'q3', factor: 'Batch' }), + ]; + const hub = makeHub({ id: 'h1', questionIds: ['q1', 'q2', 'q3'] }); + + expect(computeHubEvidence(hub, questions, null).contribution.value).toBeCloseTo(0.62); + expect(computeHubEvidence(hub, questions, bestSubsets).contribution.value).toBeCloseTo(0.52); + }); + + it('deduplicates duplicate factors and skips Best Subsets for yamazumi mode', () => { + const duplicateFactorQuestions = [ + makeQuestion({ id: 'q1', factor: 'Shift', evidence: { etaSquared: 0.34 } }), + makeQuestion({ id: 'q2', factor: 'Shift', evidence: { etaSquared: 0.28 } }), + ]; + const duplicateHub = makeHub({ id: 'h1', questionIds: ['q1', 'q2'] }); + + expect( + computeHubEvidence(duplicateHub, duplicateFactorQuestions, bestSubsets).contribution.value + ).toBeCloseTo(0.34); + + const yamazumiEvidence = computeHubEvidence( + duplicateHub, + duplicateFactorQuestions, + bestSubsets, + 'yamazumi' + ); + expect(yamazumiEvidence.mode).toBe('yamazumi'); + expect(yamazumiEvidence.contribution.label).toBe('Waste %'); + expect(yamazumiEvidence.contribution.value).toBeCloseTo(0.62); + }); +}); + // --------------------------------------------------------------------------- // Tests: computeHubProjection // --------------------------------------------------------------------------- diff --git a/packages/core/src/findings/__tests__/hypothesis.test.ts b/packages/core/src/findings/__tests__/hypothesis.test.ts new file mode 100644 index 000000000..fdcfcb50f --- /dev/null +++ b/packages/core/src/findings/__tests__/hypothesis.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from 'vitest'; +import { createHypothesis } from '../factories'; +import type { Hypothesis, HypothesisStatus } from '../types'; + +type Equal = + (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 ? true : false; + +describe('HypothesisStatus', () => { + it('uses the canonical five response-path states', () => { + const statuses = [ + 'proposed', + 'evidenced', + 'confirmed', + 'refuted', + 'needs-disconfirmation', + ] as const satisfies readonly HypothesisStatus[]; + + const exact: Equal = true; + expect(exact).toBe(true); + expect(statuses).toEqual([ + 'proposed', + 'evidenced', + 'confirmed', + 'refuted', + 'needs-disconfirmation', + ]); + }); +}); + +describe('Hypothesis', () => { + it('supports the canonical minimal entity shape and theme tags', () => { + const hypothesis: Hypothesis = { + id: 'hypothesis-1', + name: 'Nozzle wear on night shift', + synthesis: 'Worn nozzles overheat during long night runs', + questionIds: ['question-1'], + findingIds: ['finding-1'], + investigationId: 'investigation-1', + status: 'evidenced', + themeTags: ['equipment', 'night-shift'], + selectedForImprovement: true, + nextMove: 'Run disconfirmation check', + counterFindingIds: ['finding-2'], + checkQuestionIds: ['question-2'], + condition: undefined, + tributaryIds: ['tributary-1'], + signalCardIds: ['signal-card-1'], + createdAt: 1714000000000, + updatedAt: 1714000000000, + deletedAt: null, + }; + + expect(hypothesis.status).toBe('evidenced'); + expect(hypothesis.themeTags).toEqual(['equipment', 'night-shift']); + }); +}); + +describe('createHypothesis', () => { + it('creates a new hypothesis with proposed status by default', () => { + const hypothesis = createHypothesis( + 'Nozzle wear on night shift', + 'Worn nozzles overheat during long night runs', + ['question-1', 'question-2'], + ['finding-1'], + 'investigation-1' + ); + + expect(hypothesis.id).toBeTruthy(); + expect(hypothesis.name).toBe('Nozzle wear on night shift'); + expect(hypothesis.synthesis).toBe('Worn nozzles overheat during long night runs'); + expect(hypothesis.questionIds).toEqual(['question-1', 'question-2']); + expect(hypothesis.findingIds).toEqual(['finding-1']); + expect(hypothesis.investigationId).toBe('investigation-1'); + expect(hypothesis.status).toBe('proposed'); + expect(hypothesis.createdAt).toBeTruthy(); + expect(hypothesis.updatedAt).toBeTruthy(); + }); + + it('defaults connected IDs to empty arrays', () => { + const hypothesis = createHypothesis('Test', ''); + + expect(hypothesis.questionIds).toEqual([]); + expect(hypothesis.findingIds).toEqual([]); + }); +}); diff --git a/packages/core/src/findings/__tests__/hypothesisConditionEvaluator.test.ts b/packages/core/src/findings/__tests__/hypothesisConditionEvaluator.test.ts index 60613b63a..19ec52d6b 100644 --- a/packages/core/src/findings/__tests__/hypothesisConditionEvaluator.test.ts +++ b/packages/core/src/findings/__tests__/hypothesisConditionEvaluator.test.ts @@ -198,16 +198,16 @@ describe('evaluateCondition', () => { }); import { runAndCheck } from '../hypothesisConditionEvaluator'; -import type { SuspectedCause, GateNode } from '@variscout/core'; +import type { Hypothesis, GateNode } from '@variscout/core'; -function hub(id: string, cond: HypothesisCondition | undefined): SuspectedCause { +function hub(id: string, cond: HypothesisCondition | undefined): Hypothesis { return { id, name: id, synthesis: '', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', condition: cond, createdAt: 1745625600000, updatedAt: 1745625600000, @@ -217,7 +217,7 @@ function hub(id: string, cond: HypothesisCondition | undefined): SuspectedCause } describe('runAndCheck', () => { - const hubs: SuspectedCause[] = [ + const hubs: Hypothesis[] = [ hub('h1', { kind: 'leaf', column: 'SHIFT', op: 'eq', value: 'night' }), hub('h2', { kind: 'leaf', column: 'SUPPLIER', op: 'eq', value: 'B' }), hub('h3', undefined), diff --git a/packages/core/src/findings/__tests__/mechanismBranch.test.ts b/packages/core/src/findings/__tests__/mechanismBranch.test.ts index fa73904c3..26e90d13c 100644 --- a/packages/core/src/findings/__tests__/mechanismBranch.test.ts +++ b/packages/core/src/findings/__tests__/mechanismBranch.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import type { Finding, Question, SuspectedCause } from '../types'; +import type { Finding, Hypothesis, Question } from '../types'; import type { ProcessMap } from '../../frame/types'; import { projectMechanismBranch, projectMechanismBranches } from '../mechanismBranch'; @@ -34,14 +34,14 @@ function makeFinding(overrides: Partial = {}): Finding { }; } -function makeHub(overrides: Partial = {}): SuspectedCause { +function makeHub(overrides: Partial = {}): Hypothesis { return { id: 'hub-1', name: 'Nozzle heat drift on night shift', synthesis: 'Heat accumulates during long overnight runs.', questionIds: ['q-support', 'q-open'], findingIds: ['f-support', 'f-counter'], - status: 'suspected', + status: 'proposed', createdAt: 1745625600000, updatedAt: 1745625600000, investigationId: 'inv-test-001', @@ -60,7 +60,7 @@ const processMap: ProcessMap = { }; describe('projectMechanismBranch', () => { - it('projects a SuspectedCause hub into a branch with linked clues, checks, next move, and process context', () => { + it('projects a Hypothesis hub into a branch with linked clues, checks, next move, and process context', () => { const branch = projectMechanismBranch( makeHub({ tributaryIds: ['trib-shift'], @@ -165,7 +165,7 @@ describe('projectMechanismBranch', () => { ).toEqual({ value: 'ready-to-act', label: 'Ready to act' }); expect( - projectMechanismBranch(makeHub({ status: 'not-confirmed' }), { + projectMechanismBranch(makeHub({ status: 'refuted' }), { questions: [], findings: [], }).readiness diff --git a/packages/core/src/findings/__tests__/problemStatement.test.ts b/packages/core/src/findings/__tests__/problemStatement.test.ts index 2c1744392..2a6a81097 100644 --- a/packages/core/src/findings/__tests__/problemStatement.test.ts +++ b/packages/core/src/findings/__tests__/problemStatement.test.ts @@ -62,7 +62,7 @@ describe('buildProblemStatement', () => { expect(result).not.toContain('Cpk'); }); - it('handles no suspected causes', () => { + it('handles no hypotheses', () => { const result = buildProblemStatement({ outcome: 'Weight', suspectedCauses: [], diff --git a/packages/core/src/findings/__tests__/suspectedCause.test.ts b/packages/core/src/findings/__tests__/suspectedCause.test.ts deleted file mode 100644 index bd9008821..000000000 --- a/packages/core/src/findings/__tests__/suspectedCause.test.ts +++ /dev/null @@ -1,397 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { createSuspectedCause } from '../factories'; -import { computeHubContribution, computeHubEvidence, migrateCauseRolesToHubs } from '../helpers'; -import type { Question, SuspectedCause, FindingComment } from '../types'; -import type { BestSubsetsResult } from '../../stats/bestSubsets'; -import type { HypothesisCondition } from '../hypothesisCondition'; - -describe('createSuspectedCause', () => { - it('should create a hub with name, synthesis, and connected IDs', () => { - const hub = createSuspectedCause( - 'Nozzle wear on night shift', - 'Worn nozzles overheat during long night runs', - ['q1', 'q2'], - ['f1'] - ); - expect(hub.id).toBeTruthy(); - expect(hub.name).toBe('Nozzle wear on night shift'); - expect(hub.synthesis).toBe('Worn nozzles overheat during long night runs'); - expect(hub.questionIds).toEqual(['q1', 'q2']); - expect(hub.findingIds).toEqual(['f1']); - expect(hub.status).toBe('suspected'); - expect(hub.createdAt).toBeTruthy(); - expect(hub.updatedAt).toBeTruthy(); - }); - - it('should default to empty arrays', () => { - const hub = createSuspectedCause('Test', ''); - expect(hub.questionIds).toEqual([]); - expect(hub.findingIds).toEqual([]); - }); -}); - -describe('computeHubContribution', () => { - const makeQuestion = (id: string, eta?: number, rSq?: number): Question => ({ - id, - text: '', - status: 'answered', - linkedFindingIds: [], - createdAt: 1714000000000, - updatedAt: 1714000000000, - investigationId: 'inv-test-001', - deletedAt: null, - evidence: { etaSquared: eta, rSquaredAdj: rSq }, - }); - - it('should sum etaSquared from connected questions', () => { - const questions = [makeQuestion('q1', 0.34), makeQuestion('q2', 0.22)]; - const hub: SuspectedCause = { - id: 'h1', - name: '', - synthesis: '', - questionIds: ['q1', 'q2'], - findingIds: [], - status: 'suspected', - createdAt: 1714000000000, - updatedAt: 1714000000000, - investigationId: 'inv-test-001', - deletedAt: null, - }; - expect(computeHubContribution(hub, questions)).toBeCloseTo(0.56); - }); - - it('should fall back to rSquaredAdj when etaSquared missing', () => { - const questions = [makeQuestion('q1', undefined, 0.47)]; - const hub: SuspectedCause = { - id: 'h1', - name: '', - synthesis: '', - questionIds: ['q1'], - findingIds: [], - status: 'suspected', - createdAt: 1714000000000, - updatedAt: 1714000000000, - investigationId: 'inv-test-001', - deletedAt: null, - }; - expect(computeHubContribution(hub, questions)).toBeCloseTo(0.47); - }); - - it('should return 0 for hub with no connected questions', () => { - const hub: SuspectedCause = { - id: 'h1', - name: '', - synthesis: '', - questionIds: [], - findingIds: [], - status: 'suspected', - createdAt: 1714000000000, - updatedAt: 1714000000000, - investigationId: 'inv-test-001', - deletedAt: null, - }; - expect(computeHubContribution(hub, [])).toBe(0); - }); - - it('should skip questions not in the hub', () => { - const questions = [makeQuestion('q1', 0.34), makeQuestion('q2', 0.22)]; - const hub: SuspectedCause = { - id: 'h1', - name: '', - synthesis: '', - questionIds: ['q1'], - findingIds: [], - status: 'suspected', - createdAt: 1714000000000, - updatedAt: 1714000000000, - investigationId: 'inv-test-001', - deletedAt: null, - }; - expect(computeHubContribution(hub, questions)).toBeCloseTo(0.34); - }); -}); - -describe('migrateCauseRolesToHubs', () => { - const makeQuestion = ( - id: string, - factor: string, - causeRole: string, - findingIds: string[] = [] - ): Question => ({ - id, - text: `${factor} matters`, - status: 'answered', - factor, - causeRole: causeRole as 'suspected-cause' | 'contributing' | 'ruled-out', - evidence: { etaSquared: 0.3 }, - linkedFindingIds: findingIds, - createdAt: 1714000000000, - updatedAt: 1714000000000, - investigationId: 'inv-test-001', - deletedAt: null, - }); - - it('should create individual hubs for suspected-cause questions', () => { - const questions = [ - makeQuestion('q1', 'Shift', 'suspected-cause', ['f1']), - makeQuestion('q2', 'Head', 'suspected-cause'), - ]; - const hubs = migrateCauseRolesToHubs(questions); - expect(hubs).toHaveLength(2); - expect(hubs[0].name).toBe('Shift'); - expect(hubs[0].questionIds).toEqual(['q1']); - expect(hubs[0].findingIds).toEqual(['f1']); - expect(hubs[1].name).toBe('Head'); - }); - - it('should skip ruled-out and contributing questions', () => { - const questions = [ - makeQuestion('q1', 'Shift', 'suspected-cause'), - makeQuestion('q2', 'Batch', 'ruled-out'), - makeQuestion('q3', 'Temp', 'contributing'), - ]; - const hubs = migrateCauseRolesToHubs(questions); - expect(hubs).toHaveLength(1); - expect(hubs[0].name).toBe('Shift'); - }); - - it('should return empty array when no suspected causes', () => { - expect(migrateCauseRolesToHubs([])).toEqual([]); - }); - - it('should skip questions without a factor', () => { - const questions: Question[] = [ - { - id: 'q1', - text: 'test', - status: 'answered', - causeRole: 'suspected-cause', - linkedFindingIds: [], - createdAt: 1714000000000, - updatedAt: 1714000000000, - investigationId: 'inv-test-001', - deletedAt: null, - }, - ]; - expect(migrateCauseRolesToHubs(questions)).toEqual([]); - }); -}); - -describe('computeHubEvidence', () => { - const makeQuestion = (id: string, factor: string, eta?: number, rSq?: number): Question => ({ - id, - text: '', - status: 'answered', - factor, - linkedFindingIds: [], - createdAt: 1714000000000, - updatedAt: 1714000000000, - investigationId: 'inv-test-001', - deletedAt: null, - evidence: { etaSquared: eta, rSquaredAdj: rSq }, - }); - - const makeHub = (questionIds: string[]): SuspectedCause => ({ - id: 'h1', - name: 'Test', - synthesis: '', - questionIds, - findingIds: [], - status: 'suspected', - createdAt: 1714000000000, - updatedAt: 1714000000000, - investigationId: 'inv-test-001', - deletedAt: null, - }); - - const bestSubsets: BestSubsetsResult = { - subsets: [ - { - factors: ['Shift'], - factorCount: 1, - rSquared: 0.35, - rSquaredAdj: 0.34, - fStatistic: 10, - pValue: 0.001, - isSignificant: true, - dfModel: 1, - levelEffects: new Map(), - cellMeans: new Map(), - }, - { - factors: ['Head'], - factorCount: 1, - rSquared: 0.29, - rSquaredAdj: 0.28, - fStatistic: 8, - pValue: 0.004, - isSignificant: true, - dfModel: 1, - levelEffects: new Map(), - cellMeans: new Map(), - }, - { - factors: ['Head', 'Shift'], - factorCount: 2, - rSquared: 0.53, - rSquaredAdj: 0.52, - fStatistic: 18, - pValue: 0.0001, - isSignificant: true, - dfModel: 3, - levelEffects: new Map(), - cellMeans: new Map(), - }, - ], - n: 300, - totalFactors: 2, - factorNames: ['Shift', 'Head'], - grandMean: 50, - ssTotal: 1000, - }; - - it('should use Best Subsets R²adj for combined factors', () => { - const questions = [makeQuestion('q1', 'Shift', 0.34), makeQuestion('q2', 'Head', 0.28)]; - const hub = makeHub(['q1', 'q2']); - const evidence = computeHubEvidence(hub, questions, bestSubsets); - // Should use the combined subset (52%), not naive sum (62%) - expect(evidence.contribution.value).toBeCloseTo(0.52); - expect(evidence.contribution.label).toBe('R²adj'); - expect(evidence.contribution.description).toContain('52%'); - expect(evidence.mode).toBe('standard'); - }); - - it('should fall back to capped sum when no Best Subsets result', () => { - const questions = [makeQuestion('q1', 'Shift', 0.34), makeQuestion('q2', 'Head', 0.28)]; - const hub = makeHub(['q1', 'q2']); - const evidence = computeHubEvidence(hub, questions, null); - // Naive sum = 0.62, capped at 1.0 - expect(evidence.contribution.value).toBeCloseTo(0.62); - }); - - it('should use partial match when exact combination not found', () => { - const questions = [ - makeQuestion('q1', 'Shift', 0.34), - makeQuestion('q2', 'Head', 0.28), - makeQuestion('q3', 'Batch'), - ]; - const hub = makeHub(['q1', 'q2', 'q3']); - // No 3-factor subset exists — should use best matching 2-factor subset - const evidence = computeHubEvidence(hub, questions, bestSubsets); - expect(evidence.contribution.value).toBeCloseTo(0.52); - }); - - it('should handle hub with no factor-linked questions', () => { - const questions: Question[] = [ - { - id: 'q1', - text: 'gemba check', - status: 'answered', - linkedFindingIds: [], - createdAt: 1714000000000, - updatedAt: 1714000000000, - investigationId: 'inv-test-001', - deletedAt: null, - }, - ]; - const hub = makeHub(['q1']); - const evidence = computeHubEvidence(hub, questions, bestSubsets); - expect(evidence.contribution.value).toBe(0); - }); - - it('should accept mode parameter', () => { - const hub = makeHub([]); - const evidence = computeHubEvidence(hub, [], null, 'yamazumi'); - expect(evidence.mode).toBe('yamazumi'); - }); - - it('should dedup duplicate factors in hub', () => { - const questions = [ - makeQuestion('q1', 'Shift', 0.34), - makeQuestion('q2', 'Shift', 0.28), // same factor! - ]; - const hub = makeHub(['q1', 'q2']); - const evidence = computeHubEvidence(hub, questions, bestSubsets, 'standard'); - // Should find ['Shift'] (deduped) → match single-factor subset - expect(evidence.contribution.value).toBeCloseTo(0.34); - }); - - it('should use lean evidence for yamazumi mode (no Best Subsets)', () => { - const questions = [makeQuestion('q1', 'Step', 0.42)]; - const hub = makeHub(['q1']); - const evidence = computeHubEvidence(hub, questions, bestSubsets, 'yamazumi'); - // Should NOT use Best Subsets even though it's provided - expect(evidence.contribution.value).toBeCloseTo(0.42); - expect(evidence.contribution.label).toBe('Waste %'); - expect(evidence.mode).toBe('yamazumi'); - }); -}); - -describe('SuspectedCause optional Wall fields', () => { - it('accepts an undefined condition (default for existing hubs)', () => { - const hub: SuspectedCause = { - id: 'hub-1', - name: 'Nozzle runs hot', - synthesis: '', - questionIds: [], - findingIds: [], - status: 'suspected', - createdAt: 1745625600000, - updatedAt: 1745625600000, - investigationId: 'inv-test-001', - deletedAt: null, - }; - expect(hub.condition).toBeUndefined(); - }); - - it('accepts a condition predicate tree', () => { - const condition: HypothesisCondition = { - kind: 'and', - children: [ - { kind: 'leaf', column: 'SHIFT', op: 'eq', value: 'night' }, - { kind: 'leaf', column: 'NOZZLE.TEMP', op: 'gt', value: 120 }, - ], - }; - const hub: SuspectedCause = { - id: 'hub-2', - name: 'Hot nozzle night shift', - synthesis: '', - questionIds: [], - findingIds: [], - status: 'suspected', - condition, - createdAt: 1745625600000, - updatedAt: 1745625600000, - investigationId: 'inv-test-001', - deletedAt: null, - }; - expect(hub.condition?.kind).toBe('and'); - }); - - it('accepts tributaryIds and comments', () => { - const comment: FindingComment = { - id: 'c-1', - text: 'H1 looks tight', - createdAt: 1714000000000, - parentId: 'hub-3', - parentKind: 'suspectedCause', - deletedAt: null, - }; - const hub: SuspectedCause = { - id: 'hub-3', - name: 'Low viscosity', - synthesis: '', - questionIds: [], - findingIds: [], - status: 'suspected', - tributaryIds: ['trib-123'], - comments: [comment], - createdAt: 1745625600000, - updatedAt: 1745625600000, - investigationId: 'inv-test-001', - deletedAt: null, - }; - expect(hub.tributaryIds).toEqual(['trib-123']); - expect(hub.comments?.length).toBe(1); - }); -}); diff --git a/packages/core/src/findings/factories.ts b/packages/core/src/findings/factories.ts index 3d082696a..e69e5cda9 100644 --- a/packages/core/src/findings/factories.ts +++ b/packages/core/src/findings/factories.ts @@ -16,7 +16,7 @@ import { type ImprovementIdea, type PhotoAttachment, type InvestigationCategory, - type SuspectedCause, + type Hypothesis, type CausalLink, } from './types'; @@ -132,7 +132,7 @@ export function createCommentAttachment( * Create a timestamped comment with a unique ID. * * @param text - Comment text - * @param parentId - ID of the owning entity (Finding or SuspectedCause) + * @param parentId - ID of the owning entity (Finding or Hypothesis) * @param parentKind - Which entity type owns this comment * @param author - Optional author display name */ @@ -285,10 +285,10 @@ export function createFactorFinding( } /** - * Create a new SuspectedCause hub with a unique ID. + * Create a new Hypothesis with a unique ID. * - * A hub groups one or more questions (and their findings) under a named suspected - * cause, enabling the analyst to synthesize multiple evidence streams into a + * A hypothesis groups one or more questions (and their findings) under a named + * mechanism, enabling the analyst to synthesize multiple evidence streams into a * coherent explanation. The aggregate evidence contribution is computed separately * via `computeHubContribution` in helpers. * @@ -297,13 +297,13 @@ export function createFactorFinding( * @param questionIds - IDs of questions linked to this hub * @param findingIds - IDs of findings linked to this hub */ -export function createSuspectedCause( +export function createHypothesis( name: string, synthesis: string, questionIds: string[] = [], findingIds: string[] = [], investigationId = 'general-unassigned' // callers must pass explicitly; sentinel until F6 first-class investigations -): SuspectedCause { +): Hypothesis { const now = Date.now(); return { id: generateDeterministicId(), @@ -312,7 +312,7 @@ export function createSuspectedCause( questionIds, findingIds, investigationId, - status: 'suspected', + status: 'proposed', createdAt: now, updatedAt: now, deletedAt: null, diff --git a/packages/core/src/findings/helpers.ts b/packages/core/src/findings/helpers.ts index ba41fd474..c80a6db72 100644 --- a/packages/core/src/findings/helpers.ts +++ b/packages/core/src/findings/helpers.ts @@ -8,10 +8,10 @@ import type { FindingSource, InvestigationCategory, Question, - SuspectedCause, - SuspectedCauseEvidence, + Hypothesis, + HypothesisEvidence, } from './types'; -import { createSuspectedCause } from './factories'; +import { createHypothesis } from './factories'; import type { BestSubsetsResult, BestSubsetResult, LevelChange } from '../stats/bestSubsets'; import { predictFromModel, predictFromUnifiedModel } from '../stats/bestSubsets'; @@ -143,24 +143,24 @@ export function getScopedFindings(findings: Finding[]): Finding[] { } // ============================================================================ -// SuspectedCause hub helpers +// Hypothesis hub helpers // ============================================================================ /** - * Compute the aggregate evidence contribution for a SuspectedCause hub. + * Compute the aggregate evidence contribution for a Hypothesis hub. * * Sums η² (etaSquared) from each connected question. Falls back to rSquaredAdj * when etaSquared is absent. Questions not present in the provided list are * silently skipped (e.g. questions from a different investigation scope). * - * @param hub - The SuspectedCause hub to compute contribution for + * @param hub - The Hypothesis hub to compute contribution for * @param questions - All questions in scope (e.g. from the investigation store) * @returns Aggregate contribution as a decimal (e.g. 0.56 = 56%) * * @deprecated Use `computeHubEvidence` instead, which uses Best Subsets R²adj * for correlated factors and always returns a value ≤ 1.0. */ -export function computeHubContribution(hub: SuspectedCause, questions: Question[]): number { +export function computeHubContribution(hub: Hypothesis, questions: Question[]): number { const hubQuestionIds = new Set(hub.questionIds); let total = 0; for (const q of questions) { @@ -235,7 +235,7 @@ function computeChannelEvidence(questions: Question[], questionIds: string[]): n /** Mode-specific evidence computation — follows analysisStrategy.ts pattern */ const evidenceComputers: Record< - SuspectedCauseEvidence['mode'], + HypothesisEvidence['mode'], ( hubFactors: string[], questions: Question[], @@ -249,7 +249,7 @@ const evidenceComputers: Record< performance: (_hf, q, qIds) => computeChannelEvidence(q, qIds), }; -const EVIDENCE_LABELS: Record = { +const EVIDENCE_LABELS: Record = { standard: 'R²adj', capability: 'Cpk impact', yamazumi: 'Waste %', @@ -257,7 +257,7 @@ const EVIDENCE_LABELS: Record = { }; /** - * Compute mode-aware evidence for a SuspectedCause hub. + * Compute mode-aware evidence for a Hypothesis hub. * * Uses a mode-dispatched function map following the analysisStrategy.ts pattern. * Standard and capability modes use Best Subsets R²adj for correlated factors, @@ -267,18 +267,18 @@ const EVIDENCE_LABELS: Record = { * Duplicate factors across connected questions are deduplicated before lookup * to prevent match failures when multiple questions share the same factor. * - * @param hub - The SuspectedCause hub to compute evidence for + * @param hub - The Hypothesis hub to compute evidence for * @param questions - All questions in scope (e.g. from the investigation store) * @param bestSubsetsResult - Best Subsets analysis result, or null for fallback * @param mode - Analysis mode (default: 'standard') - * @returns Structured SuspectedCauseEvidence object + * @returns Structured HypothesisEvidence object */ export function computeHubEvidence( - hub: SuspectedCause, + hub: Hypothesis, questions: Question[], bestSubsetsResult: BestSubsetsResult | null, - mode: SuspectedCauseEvidence['mode'] = 'standard' -): SuspectedCauseEvidence { + mode: HypothesisEvidence['mode'] = 'standard' +): HypothesisEvidence { // Collect unique factors linked to hub questions (dedup prevents match failures) const hubFactors = [ ...new Set( @@ -302,10 +302,10 @@ export function computeHubEvidence( } // ============================================================================ -// Hub projection (model-based prediction for SuspectedCause) +// Hub projection (model-based prediction for Hypothesis) // ============================================================================ -/** Projection result for a SuspectedCause hub based on level-effects model */ +/** Projection result for a Hypothesis hub based on level-effects model */ export interface HubProjection { /** Change in predicted mean (target - current) */ predictedMeanDelta: number; @@ -322,7 +322,7 @@ export interface HubProjection { } /** - * Compute a model-based projection for a SuspectedCause hub. + * Compute a model-based projection for a Hypothesis hub. * * Finds the best matching factor subset from the Best Subsets result, * uses level effects to predict the mean change when switching from @@ -336,7 +336,7 @@ export interface HubProjection { * Falls back to the categorical path for any factor not present in the * numeric values map. * - * @param hub - The SuspectedCause hub + * @param hub - The Hypothesis hub * @param questions - All questions in scope * @param bestSubsetsResult - Best Subsets analysis result (null → return null) * @param currentWorstLevels - Current worst factor levels (factor → level string). @@ -344,7 +344,7 @@ export interface HubProjection { * @param options - Optional spec target, characteristic type, and continuous values */ export function computeHubProjection( - hub: SuspectedCause, + hub: Hypothesis, questions: Question[], bestSubsetsResult: BestSubsetsResult | null, currentWorstLevels: Record, @@ -548,7 +548,7 @@ export function computeHubProjection( // Evidence clustering (factor overlap detection) // ============================================================================ -/** A cluster of questions sharing factors that could form a SuspectedCause hub */ +/** A cluster of questions sharing factors that could form a Hypothesis hub */ export interface EvidenceCluster { /** Factors shared by the clustered questions */ factors: string[]; @@ -562,7 +562,7 @@ export interface EvidenceCluster { /** * Detect clusters of answered questions that share factors and could - * form new SuspectedCause hubs. + * form new Hypothesis hubs. * * Groups answered questions by their factor, excludes factors already * covered by existing hubs, and returns clusters with 2+ questions @@ -570,12 +570,12 @@ export interface EvidenceCluster { * * @param questions - All questions in scope * @param findings - All findings in scope - * @param existingHubs - Existing SuspectedCause hubs (factors excluded) + * @param existingHubs - Existing Hypothesis hubs (factors excluded) */ export function detectEvidenceClusters( questions: Question[], findings: Finding[], - existingHubs: SuspectedCause[] + existingHubs: Hypothesis[] ): EvidenceCluster[] { // Collect factors already covered by existing hubs const coveredFactors = new Set(); @@ -622,24 +622,24 @@ export function detectEvidenceClusters( /** * Migrate legacy `causeRole: 'suspected-cause'` tags on questions into individual - * SuspectedCause hub instances. + * Hypothesis hub instances. * * One hub is created per question that has `causeRole === 'suspected-cause'` and * a non-empty `factor` name. Questions with `ruled-out` or `contributing` roles, * or questions without a factor, are skipped. * * This is a one-time migration helper — new investigations should use the - * SuspectedCause hub model directly. + * Hypothesis hub model directly. * * @param questions - All questions in the investigation tree - * @returns New SuspectedCause hubs (one per migrated question) + * @returns New Hypothesis hubs (one per migrated question) */ -export function migrateCauseRolesToHubs(questions: Question[]): SuspectedCause[] { - const hubs: SuspectedCause[] = []; +export function migrateCauseRolesToHubs(questions: Question[]): Hypothesis[] { + const hubs: Hypothesis[] = []; for (const q of questions) { if (q.causeRole !== 'suspected-cause') continue; if (!q.factor) continue; - hubs.push(createSuspectedCause(q.factor, '', [q.id], q.linkedFindingIds ?? [])); + hubs.push(createHypothesis(q.factor, '', [q.id], q.linkedFindingIds ?? [])); } return hubs; } diff --git a/packages/core/src/findings/hmwPrompts.ts b/packages/core/src/findings/hmwPrompts.ts index 7effa21c2..f5dfde6f3 100644 --- a/packages/core/src/findings/hmwPrompts.ts +++ b/packages/core/src/findings/hmwPrompts.ts @@ -1,7 +1,7 @@ import type { IdeaDirection } from './types'; /** - * Generate 4 "How Might We" prompts for a suspected cause. + * Generate 4 "How Might We" prompts for a hypothesis. * Used in the Brainstorm Modal to frame ideation across all 4 directions. */ export function generateHMWPrompts( diff --git a/packages/core/src/findings/hypothesisCondition.ts b/packages/core/src/findings/hypothesisCondition.ts index a4d75a63f..e844cda4b 100644 --- a/packages/core/src/findings/hypothesisCondition.ts +++ b/packages/core/src/findings/hypothesisCondition.ts @@ -1,7 +1,7 @@ /** * Predicate tree that can be evaluated against a data row. * - * Used by Investigation Wall hub `SuspectedCause.condition` to turn a hypothesis + * Used by Investigation Wall hub `Hypothesis.condition` to turn a hypothesis * into a disconfirmable claim — auto-derived from a finding's source on first * hub creation; editable afterwards. * diff --git a/packages/core/src/findings/hypothesisConditionEvaluator.ts b/packages/core/src/findings/hypothesisConditionEvaluator.ts index 14bc7e457..33498c3a9 100644 --- a/packages/core/src/findings/hypothesisConditionEvaluator.ts +++ b/packages/core/src/findings/hypothesisConditionEvaluator.ts @@ -7,7 +7,7 @@ */ import type { HypothesisCondition } from './hypothesisCondition'; -import type { SuspectedCause, GateNode } from './types'; +import type { GateNode, Hypothesis } from './types'; export type DataRow = Record; @@ -42,11 +42,7 @@ export interface AndCheckResult { * Hubs without a condition evaluate to false (cannot be disconfirmed without a rule). * Unknown hub IDs evaluate to false. */ -export function runAndCheck( - tree: GateNode, - hubs: SuspectedCause[], - rows: DataRow[] -): AndCheckResult { +export function runAndCheck(tree: GateNode, hubs: Hypothesis[], rows: DataRow[]): AndCheckResult { const hubById = new Map(hubs.map(h => [h.id, h])); const matching: number[] = []; @@ -56,7 +52,7 @@ export function runAndCheck( return { total: rows.length, holds: matching.length, matchingRowIndices: matching }; } -function evaluateGate(node: GateNode, hubById: Map, row: DataRow): boolean { +function evaluateGate(node: GateNode, hubById: Map, row: DataRow): boolean { switch (node.kind) { case 'hub': { const hub = hubById.get(node.hubId); diff --git a/packages/core/src/findings/mechanismBranch.ts b/packages/core/src/findings/mechanismBranch.ts index c4b307a8a..6a6f7898c 100644 --- a/packages/core/src/findings/mechanismBranch.ts +++ b/packages/core/src/findings/mechanismBranch.ts @@ -1,10 +1,4 @@ -import type { - Finding, - MechanismBranchReadiness, - MechanismBranchStatus, - Question, - SuspectedCause, -} from './types'; +import type { Finding, Hypothesis, HypothesisStatus, Question } from './types'; import type { ProcessMap, ProcessMapTributary } from '../frame/types'; import { buildBranchSignalWarnings } from '../signalCards'; import type { BranchSignalWarning, SignalCard } from '../signalCards'; @@ -31,7 +25,7 @@ export interface MechanismBranchClueView { } export interface MechanismBranchReadinessView { - value: MechanismBranchReadiness; + value: 'not-tested' | 'needs-check' | 'evidence-backed' | 'ready-to-act' | 'closed'; label: string; } @@ -40,8 +34,8 @@ export interface MechanismBranchViewModel { hubId: string; suspectedMechanism: string; synthesis: string; - status: SuspectedCause['status']; - branchStatus: MechanismBranchStatus; + status: HypothesisStatus; + branchStatus: HypothesisStatus; readiness: MechanismBranchReadinessView; nextMove?: string; supportingClues: MechanismBranchClueView[]; @@ -62,7 +56,7 @@ export interface MechanismBranchProjectionOptions { signalCards?: SignalCard[]; } -const READINESS_LABELS: Record = { +const READINESS_LABELS: Record = { 'not-tested': 'Not tested', 'needs-check': 'Needs check', 'evidence-backed': 'Evidence backed', @@ -89,23 +83,17 @@ function toClueView(finding: Finding): MechanismBranchClueView { }; } -function deriveBranchStatus(hub: SuspectedCause): MechanismBranchStatus { - if (hub.branchStatus) return hub.branchStatus; - if (hub.status === 'confirmed') return 'confirmed'; - if (hub.status === 'not-confirmed') return 'not-confirmed'; - return 'active'; +function deriveBranchStatus(hub: Hypothesis): HypothesisStatus { + return hub.status; } function deriveReadiness( - hub: SuspectedCause, + hub: Hypothesis, supportingClues: MechanismBranchClueView[], counterClues: MechanismBranchClueView[], openChecks: MechanismBranchQuestionView[] ): MechanismBranchReadinessView { - if (hub.branchReadiness) { - return { value: hub.branchReadiness, label: READINESS_LABELS[hub.branchReadiness] }; - } - if (hub.status === 'not-confirmed') return { value: 'closed', label: READINESS_LABELS.closed }; + if (hub.status === 'refuted') return { value: 'closed', label: READINESS_LABELS.closed }; if (hub.status === 'confirmed') { return { value: 'ready-to-act', label: READINESS_LABELS['ready-to-act'] }; } @@ -119,7 +107,7 @@ function deriveReadiness( } function projectProcessContext( - hub: SuspectedCause, + hub: Hypothesis, linkedQuestions: Question[], processMap: ProcessMap | undefined ): MechanismBranchProcessContext | undefined { @@ -141,7 +129,7 @@ function projectProcessContext( } export function projectMechanismBranch( - hub: SuspectedCause, + hub: Hypothesis, options: MechanismBranchProjectionOptions ): MechanismBranchViewModel { const questionById = new Map(options.questions.map(question => [question.id, question])); @@ -207,7 +195,7 @@ export function projectMechanismBranch( } export function projectMechanismBranches( - hubs: SuspectedCause[], + hubs: Hypothesis[], options: MechanismBranchProjectionOptions ): MechanismBranchViewModel[] { return hubs.map(hub => projectMechanismBranch(hub, options)); diff --git a/packages/core/src/findings/problemStatement.ts b/packages/core/src/findings/problemStatement.ts index 7756de792..9bf6df1f2 100644 --- a/packages/core/src/findings/problemStatement.ts +++ b/packages/core/src/findings/problemStatement.ts @@ -4,10 +4,11 @@ * Watson's 3 questions: * 1. What measure needs to change? * 2. How should it change? - * 3. What is the scope? (suspected causes) + * 3. What is the scope? (hypotheses) */ import type { CharacteristicType } from '../types'; +import { formatStatistic } from '../i18n/format'; export interface ProblemStatementInput { /** The outcome measure (e.g., "Fill Weight") */ @@ -79,26 +80,27 @@ export function buildProblemStatement(input: ProblemStatementInput): string { if (input.locationEffect) { const { level, effect } = input.locationEffect; const sign = effect >= 0 ? '+' : ''; - const effectStr = Math.abs(effect) >= 10 ? effect.toFixed(1) : effect.toFixed(2); + const effectAbs = Math.abs(effect); + const effectStr = formatStatistic(effectAbs, 'en', effectAbs >= 10 ? 1 : 2); measurePart += ` for ${level} (adds ${sign}${effectStr}`; if (input.currentCpk != null && input.targetValue != null) { - measurePart += `, Cpk ${input.currentCpk.toFixed(2)} \u2192 ${input.targetValue.toFixed(2)})`; + measurePart += `, Cpk ${formatStatistic(input.currentCpk, 'en', 2)} \u2192 ${formatStatistic(input.targetValue, 'en', 2)})`; } else { measurePart += ')'; } } else if (input.currentCpk != null && input.targetValue != null) { - measurePart += ` (Cpk ${input.currentCpk.toFixed(2)} \u2192 target ${input.targetValue.toFixed(2)})`; + measurePart += ` (Cpk ${formatStatistic(input.currentCpk, 'en', 2)} \u2192 target ${formatStatistic(input.targetValue, 'en', 2)})`; } else if (input.targetValue != null) { - measurePart += ` to target ${input.targetValue.toFixed(2)}`; + measurePart += ` to target ${formatStatistic(input.targetValue, 'en', 2)}`; } parts.push(measurePart); - // Q3: Scope (suspected causes) + // Q3: Scope (hypotheses) if (input.suspectedCauses.length > 0) { const causeDescriptions = input.suspectedCauses.map(c => { let desc = c.factor; if (c.level) desc += ` (${c.level})`; - if (c.evidence != null) desc += ` [${(c.evidence * 100).toFixed(0)}%]`; + if (c.evidence != null) desc += ` [${formatStatistic(c.evidence * 100, 'en', 0)}%]`; return desc; }); parts.push(`driven by ${causeDescriptions.join(' and ')}`); diff --git a/packages/core/src/findings/types.ts b/packages/core/src/findings/types.ts index 4e554f01c..aa750f1ed 100644 --- a/packages/core/src/findings/types.ts +++ b/packages/core/src/findings/types.ts @@ -114,9 +114,9 @@ export interface FindingComment extends EntityBase { author?: string; /** * Explicit parent entity this comment belongs to. Required for normalized storage. - * Polymorphic: a comment can belong to a Finding or a SuspectedCause. + * Polymorphic: a comment can belong to a Finding or a Hypothesis. */ - parentId: Finding['id'] | SuspectedCause['id']; + parentId: Finding['id'] | Hypothesis['id']; /** Discriminator for parentId — which entity type owns this comment. */ parentKind: 'finding' | 'suspectedCause'; /** Photo attachments (Team plan only) */ @@ -167,7 +167,7 @@ export interface FindingOutcome { } // ============================================================================ -// Improvement Idea Types (creative bridge between suspected cause and actions) +// Improvement Idea Types (creative bridge between hypothesis and actions) // ============================================================================ /** Implementation timeframe for an improvement idea (replaces IdeaEffort) */ @@ -248,7 +248,7 @@ export function computeRiskLevel(axis1: RiskLevel, axis2: RiskLevel): ComputedRi /** * An improvement idea attached to an answered/investigating question. - * Bridges validated suspected cause and corrective actions. + * Bridges validated hypothesis and corrective actions. */ export interface ImprovementIdea extends EntityBase { /** Idea description (e.g., "Simplify setup with visual guides") */ @@ -347,8 +347,8 @@ export interface Question extends EntityBase { ideas?: ImprovementIdea[]; /** * Role in investigation conclusion — multiple 'suspected-cause' allowed per tree. - * @deprecated Use SuspectedCause hub membership instead. Retained for backward - * compatibility and migration. New investigations should create SuspectedCause + * @deprecated Use Hypothesis membership instead. Retained for backward + * compatibility and migration. New investigations should create Hypothesis * hubs and connect questions via questionIds. */ causeRole?: 'suspected-cause' | 'contributing' | 'ruled-out'; @@ -523,7 +523,7 @@ export interface Finding extends EntityBase { source?: FindingSource; /** Optional assignee for Team plan @mention workflow */ assignee?: FindingAssignee; - /** Link to a question (replaces deprecated suspectedCause) */ + /** Link to a question (replaces deprecated hypothesis linkage) */ questionId?: Question['id']; /** How this finding relates to its linked question */ validationStatus?: 'supports' | 'contradicts' | 'inconclusive'; @@ -572,7 +572,7 @@ export interface InvestigationCategory extends EntityBase { /** Source of a projection scenario — what mechanism is being addressed */ export type ProjectionSource = | { type: 'drill'; factors: string[]; levels: string[] } - | { type: 'suspected-cause'; causeId: string; factors: string[] } + | { type: 'hypothesis'; hypothesisId: string; factors: string[] } | { type: 'centering' } | { type: 'idea'; ideaId: string } | { type: 'measured'; stageIndex: number }; @@ -630,11 +630,11 @@ export interface ProjectionScenario { } // ============================================================================ -// Suspected Cause Evidence +// Hypothesis Evidence // ============================================================================ -/** Mode-aware evidence on a suspected cause */ -export interface SuspectedCauseEvidence { +/** Mode-aware evidence on a hypothesis */ +export interface HypothesisEvidence { /** Mode active when evidence was computed */ mode: 'standard' | 'capability' | 'performance' | 'yamazumi'; /** How much of the problem this mechanism explains */ @@ -649,30 +649,27 @@ export interface SuspectedCauseEvidence { } // ============================================================================ -// Suspected Cause Hub (Investigation Reframing) +// Hypothesis (Investigation Reframing) // ============================================================================ -/** User-facing Mechanism Branch lifecycle status. */ -export type MechanismBranchStatus = 'active' | 'confirmed' | 'not-confirmed' | 'parked'; - -/** Deterministic readiness bucket shown on Mechanism Branch cards. */ -export type MechanismBranchReadiness = - | 'not-tested' - | 'needs-check' - | 'evidence-backed' - | 'ready-to-act' - | 'closed'; +/** Canonical hypothesis lifecycle status. */ +export type HypothesisStatus = + | 'proposed' + | 'evidenced' + | 'confirmed' + | 'refuted' + | 'needs-disconfirmation'; /** - * A suspected cause hub — a named mechanism that connects multiple evidence + * A hypothesis — a named mechanism that connects multiple evidence * threads (questions, findings) into one coherent story. * - * This is the primary output of the Investigation Diamond. Each hub drives + * This is the primary output of the Investigation Diamond. Each hypothesis drives * one HMW brainstorm session in the IMPROVE phase. * * See: docs/superpowers/specs/2026-04-03-investigation-workspace-reframing-design.md */ -export interface SuspectedCause extends EntityBase { +export interface Hypothesis extends EntityBase { /** Analyst-chosen name: "Nozzle wear on night shift" */ name: string; /** Analyst's synthesis: how the evidence connects */ @@ -686,15 +683,13 @@ export interface SuspectedCause extends EntityBase { /** FK to the owning investigation. Required for normalized storage. */ investigationId: ProcessHubInvestigation['id']; /** Mode-aware evidence — contribution stored, projection computed live */ - evidence?: SuspectedCauseEvidence; - /** Whether this cause is selected for the current improvement round */ + evidence?: HypothesisEvidence; + /** Whether this hypothesis is selected for the current improvement round */ selectedForImprovement?: boolean; - /** Status: suspected → confirmed (outcome-based) */ - status: 'suspected' | 'confirmed' | 'not-confirmed'; - /** Optional user-facing branch lifecycle status. Defaults are derived from `status`. */ - branchStatus?: MechanismBranchStatus; - /** Optional user-facing branch readiness override. Usually derived from evidence/checks. */ - branchReadiness?: MechanismBranchReadiness; + /** Canonical hypothesis status. */ + status: HypothesisStatus; + /** Theme tags for grouping related hypotheses. */ + themeTags?: string[]; /** Branch-level next move. Investigation-level `ProcessContext.nextMove` remains separate. */ nextMove?: string; /** Explicit finding IDs that should render as counter-clues for this branch. */ @@ -731,11 +726,11 @@ export interface CausalLink extends EntityBase { questionIds: Question['id'][]; // Questions supporting this link findingIds: Finding['id'][]; // Findings supporting this link /** - * The SuspectedCause this link belongs to. + * The Hypothesis this link belongs to. * Renamed from `hubId` (R5) — the old name was misleading; it references - * SuspectedCause.id, not ProcessHub.id. + * Hypothesis.id, not ProcessHub.id. */ - suspectedCauseId?: SuspectedCause['id']; + suspectedCauseId?: Hypothesis['id']; strength?: number; // ΔR² or computed from R²adj comparison relationshipType?: 'independent' | 'overlapping' | 'synergistic' | 'interactive' | 'redundant'; source: 'analyst' | 'coscout' | 'auto'; @@ -767,7 +762,7 @@ export const CATEGORY_COLORS = [ // ============================================================================ /** - * Composition tree for the Investigation Wall. Leaves reference `SuspectedCause` + * Composition tree for the Investigation Wall. Leaves reference `Hypothesis` * hubs; branches compose them with boolean gates (AND / OR / NOT). Persisted on * `investigationStore.problemContributionTree` so team-authored contribution stories * survive reload. Terminology: "contribution tree", never "root cause" (P5 amended). diff --git a/packages/core/src/frame/types.ts b/packages/core/src/frame/types.ts index 5f03293f8..4cf195b92 100644 --- a/packages/core/src/frame/types.ts +++ b/packages/core/src/frame/types.ts @@ -79,7 +79,7 @@ export interface ProcessMapTributary { contextColumns?: string[]; } -/** A pre-data hunch pinned to a step or tributary, lifted later to a SuspectedCause. */ +/** A pre-data hunch pinned to a step or tributary, lifted later to a Hypothesis. */ export interface ProcessMapHunch { id: string; /** Human-readable hunch (e.g. "Nozzle wear on night shift"). */ @@ -124,7 +124,7 @@ export interface ProcessMap { * Consumers (e.g. SubgroupConfigPopover) resolve these to column names via `tributaries`. */ subgroupAxes?: string[]; - /** Pre-data hunches. Drafted here; promoted to SuspectedCause hubs in Investigation. */ + /** Pre-data hunches. Drafted here; promoted to Hypothesis hubs in Investigation. */ hunches?: ProcessMapHunch[]; /** Optional layout hints. */ layout?: ProcessMapLayout; diff --git a/packages/core/src/i18n/messages/ar.ts b/packages/core/src/i18n/messages/ar.ts index 6fd01e8c2..91ebf5359 100644 --- a/packages/core/src/i18n/messages/ar.ts +++ b/packages/core/src/i18n/messages/ar.ts @@ -270,7 +270,7 @@ export const ar: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/bg.ts b/packages/core/src/i18n/messages/bg.ts index af6792871..574d6fd95 100644 --- a/packages/core/src/i18n/messages/bg.ts +++ b/packages/core/src/i18n/messages/bg.ts @@ -270,7 +270,7 @@ export const bg: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/cs.ts b/packages/core/src/i18n/messages/cs.ts index e0855e3bb..6f374d9ef 100644 --- a/packages/core/src/i18n/messages/cs.ts +++ b/packages/core/src/i18n/messages/cs.ts @@ -250,7 +250,7 @@ export const cs: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/da.ts b/packages/core/src/i18n/messages/da.ts index da4fdf0a7..d910bd09a 100644 --- a/packages/core/src/i18n/messages/da.ts +++ b/packages/core/src/i18n/messages/da.ts @@ -250,7 +250,7 @@ export const da: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/de.ts b/packages/core/src/i18n/messages/de.ts index d86c94c9c..7929a791e 100644 --- a/packages/core/src/i18n/messages/de.ts +++ b/packages/core/src/i18n/messages/de.ts @@ -273,7 +273,7 @@ export const de: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Erkenntnis speichern', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/el.ts b/packages/core/src/i18n/messages/el.ts index b91e84e18..f892bfdc2 100644 --- a/packages/core/src/i18n/messages/el.ts +++ b/packages/core/src/i18n/messages/el.ts @@ -270,7 +270,7 @@ export const el: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/en.ts b/packages/core/src/i18n/messages/en.ts index 48575c3c6..bc8658d77 100644 --- a/packages/core/src/i18n/messages/en.ts +++ b/packages/core/src/i18n/messages/en.ts @@ -277,7 +277,7 @@ export const en: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/es.ts b/packages/core/src/i18n/messages/es.ts index 1082862e0..9944bea01 100644 --- a/packages/core/src/i18n/messages/es.ts +++ b/packages/core/src/i18n/messages/es.ts @@ -272,7 +272,7 @@ export const es: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/fi.ts b/packages/core/src/i18n/messages/fi.ts index e59963330..839991166 100644 --- a/packages/core/src/i18n/messages/fi.ts +++ b/packages/core/src/i18n/messages/fi.ts @@ -272,7 +272,7 @@ export const fi: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Tallenna havainto', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/fr.ts b/packages/core/src/i18n/messages/fr.ts index ba0a77e5a..1213cafe7 100644 --- a/packages/core/src/i18n/messages/fr.ts +++ b/packages/core/src/i18n/messages/fr.ts @@ -273,7 +273,7 @@ export const fr: MessageCatalog = { 'ai.tool.suggestSaveFinding': "Enregistrer l'insight", 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/he.ts b/packages/core/src/i18n/messages/he.ts index b4670e485..3f6b2f91b 100644 --- a/packages/core/src/i18n/messages/he.ts +++ b/packages/core/src/i18n/messages/he.ts @@ -270,7 +270,7 @@ export const he: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/hi.ts b/packages/core/src/i18n/messages/hi.ts index 5725f9c43..44afeb204 100644 --- a/packages/core/src/i18n/messages/hi.ts +++ b/packages/core/src/i18n/messages/hi.ts @@ -270,7 +270,7 @@ export const hi: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/hr.ts b/packages/core/src/i18n/messages/hr.ts index 31e278e65..fecf25c41 100644 --- a/packages/core/src/i18n/messages/hr.ts +++ b/packages/core/src/i18n/messages/hr.ts @@ -270,7 +270,7 @@ export const hr: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/hu.ts b/packages/core/src/i18n/messages/hu.ts index 9bf22874f..7035e997b 100644 --- a/packages/core/src/i18n/messages/hu.ts +++ b/packages/core/src/i18n/messages/hu.ts @@ -250,7 +250,7 @@ export const hu: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/id.ts b/packages/core/src/i18n/messages/id.ts index 5f7131f0d..d3496eb76 100644 --- a/packages/core/src/i18n/messages/id.ts +++ b/packages/core/src/i18n/messages/id.ts @@ -270,7 +270,7 @@ export const id: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/it.ts b/packages/core/src/i18n/messages/it.ts index 46d75281d..7bf690b44 100644 --- a/packages/core/src/i18n/messages/it.ts +++ b/packages/core/src/i18n/messages/it.ts @@ -252,7 +252,7 @@ export const it: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/ja.ts b/packages/core/src/i18n/messages/ja.ts index f554d767c..06ab6dc2c 100644 --- a/packages/core/src/i18n/messages/ja.ts +++ b/packages/core/src/i18n/messages/ja.ts @@ -252,7 +252,7 @@ export const ja: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'インサイトを保存', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/ko.ts b/packages/core/src/i18n/messages/ko.ts index 60cd0a681..bf0526a84 100644 --- a/packages/core/src/i18n/messages/ko.ts +++ b/packages/core/src/i18n/messages/ko.ts @@ -252,7 +252,7 @@ export const ko: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/ms.ts b/packages/core/src/i18n/messages/ms.ts index dfd0b4ecc..f350faf5d 100644 --- a/packages/core/src/i18n/messages/ms.ts +++ b/packages/core/src/i18n/messages/ms.ts @@ -270,7 +270,7 @@ export const ms: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/nb.ts b/packages/core/src/i18n/messages/nb.ts index 594a7771e..f326708f9 100644 --- a/packages/core/src/i18n/messages/nb.ts +++ b/packages/core/src/i18n/messages/nb.ts @@ -250,7 +250,7 @@ export const nb: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/nl.ts b/packages/core/src/i18n/messages/nl.ts index d921fa47b..bf143a45f 100644 --- a/packages/core/src/i18n/messages/nl.ts +++ b/packages/core/src/i18n/messages/nl.ts @@ -252,7 +252,7 @@ export const nl: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/pl.ts b/packages/core/src/i18n/messages/pl.ts index 09ae3756e..dee1d8eca 100644 --- a/packages/core/src/i18n/messages/pl.ts +++ b/packages/core/src/i18n/messages/pl.ts @@ -252,7 +252,7 @@ export const pl: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/pt.ts b/packages/core/src/i18n/messages/pt.ts index f5c85afb0..048503ea0 100644 --- a/packages/core/src/i18n/messages/pt.ts +++ b/packages/core/src/i18n/messages/pt.ts @@ -272,7 +272,7 @@ export const pt: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/ro.ts b/packages/core/src/i18n/messages/ro.ts index 6675cbe59..b9e16c56e 100644 --- a/packages/core/src/i18n/messages/ro.ts +++ b/packages/core/src/i18n/messages/ro.ts @@ -270,7 +270,7 @@ export const ro: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/sk.ts b/packages/core/src/i18n/messages/sk.ts index 98e1c1c2e..128d01c08 100644 --- a/packages/core/src/i18n/messages/sk.ts +++ b/packages/core/src/i18n/messages/sk.ts @@ -270,7 +270,7 @@ export const sk: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/sv.ts b/packages/core/src/i18n/messages/sv.ts index e2b0a7984..3cd5fe202 100644 --- a/packages/core/src/i18n/messages/sv.ts +++ b/packages/core/src/i18n/messages/sv.ts @@ -250,7 +250,7 @@ export const sv: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Spara insikt', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/th.ts b/packages/core/src/i18n/messages/th.ts index 6cce00ab6..57b2fc1b3 100644 --- a/packages/core/src/i18n/messages/th.ts +++ b/packages/core/src/i18n/messages/th.ts @@ -270,7 +270,7 @@ export const th: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/tr.ts b/packages/core/src/i18n/messages/tr.ts index c4d6b676c..e63a7cc29 100644 --- a/packages/core/src/i18n/messages/tr.ts +++ b/packages/core/src/i18n/messages/tr.ts @@ -252,7 +252,7 @@ export const tr: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/uk.ts b/packages/core/src/i18n/messages/uk.ts index 4c7ba8e3f..2bc5305f3 100644 --- a/packages/core/src/i18n/messages/uk.ts +++ b/packages/core/src/i18n/messages/uk.ts @@ -270,7 +270,7 @@ export const uk: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/vi.ts b/packages/core/src/i18n/messages/vi.ts index 9e8fa0598..172c6eed1 100644 --- a/packages/core/src/i18n/messages/vi.ts +++ b/packages/core/src/i18n/messages/vi.ts @@ -270,7 +270,7 @@ export const vi: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/zhHans.ts b/packages/core/src/i18n/messages/zhHans.ts index e08885558..14b3640a1 100644 --- a/packages/core/src/i18n/messages/zhHans.ts +++ b/packages/core/src/i18n/messages/zhHans.ts @@ -252,7 +252,7 @@ export const zhHans: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/messages/zhHant.ts b/packages/core/src/i18n/messages/zhHant.ts index 4faa7e8cf..c38b198fd 100644 --- a/packages/core/src/i18n/messages/zhHant.ts +++ b/packages/core/src/i18n/messages/zhHant.ts @@ -252,7 +252,7 @@ export const zhHant: MessageCatalog = { 'ai.tool.suggestSaveFinding': 'Save insight', 'ai.tool.navigateTo': 'Navigate to', 'ai.tool.answerQuestion': 'Answer question', - 'ai.tool.suggestSuspectedCause': 'Suggest suspected cause', + 'ai.tool.suggestHypothesis': 'Suggest hypothesis', 'ai.tool.connectHubEvidence': 'Connect hub evidence', 'ai.tool.suggestCausalLink': 'Suggest causal link', 'ai.tool.highlightMapPattern': 'Highlight map pattern', diff --git a/packages/core/src/i18n/types.ts b/packages/core/src/i18n/types.ts index 4b4fdac6c..7065de4cb 100644 --- a/packages/core/src/i18n/types.ts +++ b/packages/core/src/i18n/types.ts @@ -386,7 +386,7 @@ export interface MessageCatalog { 'ai.tool.suggestSaveFinding': string; 'ai.tool.navigateTo': string; 'ai.tool.answerQuestion': string; - 'ai.tool.suggestSuspectedCause': string; + 'ai.tool.suggestHypothesis': string; 'ai.tool.connectHubEvidence': string; 'ai.tool.suggestCausalLink': string; 'ai.tool.highlightMapPattern': string; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f71b1c9d9..cc040ebc4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -767,9 +767,9 @@ export type { IdeaCategory, FindingRole, BenchmarkStats, - SuspectedCause, - MechanismBranchReadiness, - MechanismBranchStatus, + Hypothesis, + HypothesisEvidence, + HypothesisStatus, MechanismBranchClueView, MechanismBranchProcessContext, MechanismBranchProjectionOptions, @@ -781,14 +781,13 @@ export type { CausalDirection, CausalEvidenceType, CausalSource, - // Projection types (SuspectedCause evidence model) + // Projection types (Hypothesis evidence model) ProjectionSource, ProjectionMethod, StatisticalProjectionResult, LeanProjectionResult, ProjectionResult, ProjectionScenario, - SuspectedCauseEvidence, // Investigation Wall — contribution tree GateNode, // HypothesisCondition evaluator (DataRow is re-exported via line 7 from './types') @@ -834,7 +833,7 @@ export { createImprovementIdea, createFactorFinding, createInvestigationCategory, - createSuspectedCause, + createHypothesis, createCausalLink, // HypothesisCondition evaluator evaluateCondition, diff --git a/packages/core/src/persistence/HubRepository.ts b/packages/core/src/persistence/HubRepository.ts index 16d8497e7..569472d58 100644 --- a/packages/core/src/persistence/HubRepository.ts +++ b/packages/core/src/persistence/HubRepository.ts @@ -1,7 +1,7 @@ import type { HubAction } from '../actions/HubAction'; import type { ProcessHub, OutcomeSpec, ProcessHubInvestigation } from '../processHub'; import type { EvidenceSource, EvidenceSnapshot, EvidenceSourceCursor } from '../evidenceSources'; -import type { Finding, Question, CausalLink, SuspectedCause } from '../findings/types'; +import type { Finding, Question, CausalLink, Hypothesis } from '../findings/types'; import type { ProcessMap } from '../frame/types'; export interface HubReadAPI { @@ -48,9 +48,9 @@ export interface CausalLinkReadAPI { listByInvestigation(investigationId: ProcessHubInvestigation['id']): Promise; } -export interface SuspectedCauseReadAPI { - get(id: SuspectedCause['id']): Promise; - listByInvestigation(investigationId: ProcessHubInvestigation['id']): Promise; +export interface HypothesisReadAPI { + get(id: Hypothesis['id']): Promise; + listByInvestigation(investigationId: ProcessHubInvestigation['id']): Promise; } export interface CanvasStateReadAPI { @@ -76,6 +76,6 @@ export interface HubRepository { findings: FindingReadAPI; questions: QuestionReadAPI; causalLinks: CausalLinkReadAPI; - suspectedCauses: SuspectedCauseReadAPI; + hypotheses: HypothesisReadAPI; canvasState: CanvasStateReadAPI; } diff --git a/packages/core/src/persistence/index.ts b/packages/core/src/persistence/index.ts index e539b80f4..660f9a44c 100644 --- a/packages/core/src/persistence/index.ts +++ b/packages/core/src/persistence/index.ts @@ -8,7 +8,7 @@ export type { FindingReadAPI, QuestionReadAPI, CausalLinkReadAPI, - SuspectedCauseReadAPI, + HypothesisReadAPI, CanvasStateReadAPI, } from './HubRepository'; export type { EntityKind, CascadeRule, CascadeRuleset } from './cascadeRules'; diff --git a/packages/core/src/survey/__tests__/survey.test.ts b/packages/core/src/survey/__tests__/survey.test.ts index 2c80ca002..456a3c549 100644 --- a/packages/core/src/survey/__tests__/survey.test.ts +++ b/packages/core/src/survey/__tests__/survey.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { evaluateSurvey } from '../index'; -import type { DataRow, Finding, ProcessMap, Question, SuspectedCause } from '../../index'; +import type { DataRow, Finding, ProcessMap, Question, Hypothesis } from '../../index'; const data: DataRow[] = [ { SampleTime: '2026-04-01T08:00:00Z', Fill_Weight: 10.1, Machine: 'M1', Shift: 'Day' }, @@ -48,13 +48,13 @@ const finding = (overrides: Partial = {}): Finding => ({ ...overrides, }); -const branch = (overrides: Partial = {}): SuspectedCause => ({ +const branch = (overrides: Partial = {}): Hypothesis => ({ id: 'hub-1', name: 'Nozzle wear on M2', synthesis: 'M2 behaves differently after warmup.', questionIds: ['q-1'], findingIds: ['f-1'], - status: 'suspected', + status: 'proposed', createdAt: 1745625600000, updatedAt: 1745625600000, investigationId: 'inv-test-001', @@ -159,6 +159,57 @@ describe('evaluateSurvey', () => { ); }); + it('keeps counter-check recommendations for evidenced and disconfirmation-ready hypotheses', () => { + const baseInput = { + data, + outcomeColumn: 'Fill_Weight', + factorColumns: ['Machine'], + questions: [question()], + findings: [finding()], + }; + + for (const status of ['evidenced', 'needs-disconfirmation'] as const) { + const survey = evaluateSurvey({ + ...baseInput, + branches: [branch({ status })], + }); + + expect(survey.recommendations.map(rec => rec.id)).toContain('branch:hub-1:add-counter-check'); + } + }); + + it('does not recommend branch checks for terminal hypotheses', () => { + const baseInput = { + data, + outcomeColumn: 'Fill_Weight', + factorColumns: ['Machine'], + questions: [ + question(), + question({ + id: 'q-counter', + text: 'Does M2 still separate during Day shift only?', + status: 'open', + linkedFindingIds: [], + }), + ], + findings: [finding()], + }; + + for (const status of ['confirmed', 'refuted'] as const) { + const survey = evaluateSurvey({ + ...baseInput, + branches: [branch({ status, checkQuestionIds: ['q-counter'] })], + }); + + expect(survey.recommendations.map(rec => rec.id)).not.toContain( + 'branch:hub-1:add-counter-check' + ); + expect(survey.recommendations.map(rec => rec.id)).not.toContain( + 'branch:hub-1:complete-open-checks' + ); + } + }); + it('returns recommendations in deterministic order', () => { const input = { data, diff --git a/packages/core/src/survey/evaluator.ts b/packages/core/src/survey/evaluator.ts index 64127d59d..c87a86d42 100644 --- a/packages/core/src/survey/evaluator.ts +++ b/packages/core/src/survey/evaluator.ts @@ -666,11 +666,12 @@ function addBranchRecommendations( projected.forEach((branchView, index) => { const priority = 200 + index * 2; - if ( - branchView.branchStatus === 'active' && + const needsCounterCheck = + branchView.branchStatus !== 'confirmed' && + branchView.branchStatus !== 'refuted' && branchView.supportingClues.length > 0 && - branchView.counterClues.length === 0 - ) { + branchView.counterClues.length === 0; + if (needsCounterCheck) { store.add({ id: `branch:${branchView.id}:add-counter-check`, kind: 'add-counter-check', @@ -684,7 +685,11 @@ function addBranchRecommendations( }); } - if (branchView.openChecks.length > 0) { + if ( + branchView.branchStatus !== 'confirmed' && + branchView.branchStatus !== 'refuted' && + branchView.openChecks.length > 0 + ) { store.add({ id: `branch:${branchView.id}:complete-open-checks`, kind: 'add-counter-check', diff --git a/packages/core/src/survey/types.ts b/packages/core/src/survey/types.ts index 3510add40..c19fdec51 100644 --- a/packages/core/src/survey/types.ts +++ b/packages/core/src/survey/types.ts @@ -1,7 +1,7 @@ import type { DetectedColumns } from '../parser'; import type { DefectDetection, DefectMapping } from '../defect'; import type { ProcessContext } from '../ai'; -import type { Finding, Question, SuspectedCause } from '../findings'; +import type { Finding, Question, Hypothesis } from '../findings'; import type { DataRow, SpecLimits, WideFormatDetection } from '../types'; import type { YamazumiColumnMapping, YamazumiDetection } from '../yamazumi'; import type { Gap, InferredMode, ModeInferenceResult, ProcessMap } from '../frame'; @@ -139,5 +139,5 @@ export interface SurveyEvaluationInput { >; questions?: Question[]; findings?: Finding[]; - branches?: SuspectedCause[]; + branches?: Hypothesis[]; } diff --git a/packages/data/src/samples/investigation-showcase.ts b/packages/data/src/samples/investigation-showcase.ts index 46ec0582e..4ac847a3e 100644 --- a/packages/data/src/samples/investigation-showcase.ts +++ b/packages/data/src/samples/investigation-showcase.ts @@ -1,6 +1,6 @@ import type { SampleDataset } from '../types'; import { seedRandom, generateNormal, round } from '../utils'; -import type { Finding, Question, SuspectedCause, InvestigationCategory } from '@variscout/core'; +import type { Finding, Question, Hypothesis, InvestigationCategory } from '@variscout/core'; import { DEFAULT_TIME_LENS } from '@variscout/core'; // ============================================================================ @@ -411,7 +411,7 @@ function buildFindings(): Finding[] { ]; } -function buildSuspectedCauses(): SuspectedCause[] { +function buildSuspectedCauses(): Hypothesis[] { return [ { id: IDS.HUB_NOZZLE, @@ -433,7 +433,7 @@ function buildSuspectedCauses(): SuspectedCause[] { }, }, selectedForImprovement: true, - status: 'suspected', + status: 'proposed', createdAt: epoch(28), updatedAt: epoch(48), }, diff --git a/packages/data/src/samples/syringe-barrel-weight.ts b/packages/data/src/samples/syringe-barrel-weight.ts index d788fd2b6..68ca3f89f 100644 --- a/packages/data/src/samples/syringe-barrel-weight.ts +++ b/packages/data/src/samples/syringe-barrel-weight.ts @@ -17,7 +17,7 @@ import { mulberry32 } from '../utils'; import type { Finding, Question, - SuspectedCause, + Hypothesis, CausalLink, InvestigationCategory, } from '@variscout/core'; @@ -573,7 +573,7 @@ function buildFindings(): Finding[] { ]; } -function buildSuspectedCauses(): SuspectedCause[] { +function buildSuspectedCauses(): Hypothesis[] { return [ { id: IDS.HUB_LOT3_PRESSURE, @@ -599,7 +599,7 @@ function buildSuspectedCauses(): SuspectedCause[] { }, }, selectedForImprovement: true, - status: 'suspected', + status: 'proposed', createdAt: epochAt(20), updatedAt: epochAt(26), }, diff --git a/packages/data/src/types.ts b/packages/data/src/types.ts index 4b09571c6..49f4d4600 100644 --- a/packages/data/src/types.ts +++ b/packages/data/src/types.ts @@ -33,7 +33,7 @@ export interface SampleDataset { export interface SampleInvestigationState { findings?: import('@variscout/core').Finding[]; questions?: import('@variscout/core').Question[]; - suspectedCauses?: import('@variscout/core').SuspectedCause[]; + suspectedCauses?: import('@variscout/core').Hypothesis[]; causalLinks?: import('@variscout/core').CausalLink[]; categories?: import('@variscout/core').InvestigationCategory[]; } diff --git a/packages/hooks/src/__tests__/useCanvasInvestigationOverlays.test.ts b/packages/hooks/src/__tests__/useCanvasInvestigationOverlays.test.ts index 84049e338..720e9706b 100644 --- a/packages/hooks/src/__tests__/useCanvasInvestigationOverlays.test.ts +++ b/packages/hooks/src/__tests__/useCanvasInvestigationOverlays.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import type { CausalLink, Finding, Question, SuspectedCause } from '@variscout/core'; +import type { CausalLink, Finding, Question, Hypothesis } from '@variscout/core'; import type { ProcessMap } from '@variscout/core/frame'; import { CANVAS_OVERLAY_REGISTRY, @@ -57,14 +57,14 @@ function finding(overrides: Partial & { id: string }): Finding { }; } -function hub(overrides: Partial & { id: string }): SuspectedCause { +function hub(overrides: Partial & { id: string }): Hypothesis { const { id } = overrides; return { name: `Hub ${id}`, synthesis: 'Evidence connects here.', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, deletedAt: null, diff --git a/packages/hooks/src/__tests__/useCurrentUnderstanding.test.ts b/packages/hooks/src/__tests__/useCurrentUnderstanding.test.ts index e1927bb70..e391f6810 100644 --- a/packages/hooks/src/__tests__/useCurrentUnderstanding.test.ts +++ b/packages/hooks/src/__tests__/useCurrentUnderstanding.test.ts @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import type { ProcessContext, Question, SuspectedCause } from '@variscout/core'; +import type { ProcessContext, Question, Hypothesis } from '@variscout/core'; import { useCurrentUnderstanding } from '../useCurrentUnderstanding'; function makeQuestion(overrides: Partial = {}): Question { @@ -21,7 +21,7 @@ function makeQuestion(overrides: Partial = {}): Question { }; } -function makeHub(overrides: Partial = {}): SuspectedCause { +function makeHub(overrides: Partial = {}): Hypothesis { return { id: 'hub-1', name: 'Changeover setup', @@ -29,7 +29,7 @@ function makeHub(overrides: Partial = {}): SuspectedCause { questionIds: ['q-1'], findingIds: [], selectedForImprovement: false, - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, deletedAt: null, @@ -57,7 +57,7 @@ describe('useCurrentUnderstanding', () => { generatedDraft: null, }, questions: [makeQuestion()], - suspectedCauseHubs: [makeHub()], + hypothesisHubs: [makeHub()], }) ); diff --git a/packages/hooks/src/__tests__/useHasInvestigationContent.test.ts b/packages/hooks/src/__tests__/useHasInvestigationContent.test.ts index 25e33acae..a153e3577 100644 --- a/packages/hooks/src/__tests__/useHasInvestigationContent.test.ts +++ b/packages/hooks/src/__tests__/useHasInvestigationContent.test.ts @@ -1,14 +1,14 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { renderHook } from '@testing-library/react'; import { getInvestigationInitialState, useInvestigationStore } from '@variscout/stores'; -import type { Question, SuspectedCause } from '@variscout/core'; +import type { Question, Hypothesis } from '@variscout/core'; import { useHasInvestigationContent } from '../useHasInvestigationContent'; -const sampleHub: SuspectedCause = { +const sampleHub: Hypothesis = { id: 'hub-1', name: 'Test hub', synthesis: '', - status: 'suspected', + status: 'proposed', questionIds: [], findingIds: [], createdAt: 1714000000000, diff --git a/packages/hooks/src/__tests__/useHubComputations.test.ts b/packages/hooks/src/__tests__/useHubComputations.test.ts index 6e452d5c9..a044c0772 100644 --- a/packages/hooks/src/__tests__/useHubComputations.test.ts +++ b/packages/hooks/src/__tests__/useHubComputations.test.ts @@ -8,7 +8,7 @@ import { } from '@variscout/stores'; import { useHubComputations } from '../useHubComputations'; import type { BestSubsetsResult } from '@variscout/core/stats'; -import type { SuspectedCause, Question } from '@variscout/core/findings'; +import type { Hypothesis, Question } from '@variscout/core/findings'; // ============================================================================ // Helpers @@ -27,13 +27,13 @@ function makeQuestion(overrides: Partial & { id: string }): Question { }; } -function makeHub(overrides: Partial & { id: string }): SuspectedCause { +function makeHub(overrides: Partial & { id: string }): Hypothesis { return { name: 'Test hub', synthesis: '', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, deletedAt: null, diff --git a/packages/hooks/src/__tests__/useSharedWallProps.test.ts b/packages/hooks/src/__tests__/useSharedWallProps.test.ts index 0b58676d4..641a928b7 100644 --- a/packages/hooks/src/__tests__/useSharedWallProps.test.ts +++ b/packages/hooks/src/__tests__/useSharedWallProps.test.ts @@ -10,11 +10,11 @@ import { type UseSharedWallPropsArgs, type UseSharedWallPropsReturn, } from '../useSharedWallProps'; -import type { Finding, Question, SuspectedCause } from '@variscout/core'; +import type { Finding, Question, Hypothesis } from '@variscout/core'; import type { ProcessMap } from '@variscout/core/frame'; interface MutableWallCanvasDataProps { - hubs: SuspectedCause[]; + hubs: Hypothesis[]; findings: Finding[]; questions: Question[]; processMap?: ProcessMap; @@ -26,13 +26,13 @@ interface MutableWallCanvasDataProps { groupByTributary?: boolean; } -function makeHub(overrides: Partial & { id: string }): SuspectedCause { +function makeHub(overrides: Partial & { id: string }): Hypothesis { return { name: 'Night shift setup', synthesis: 'Setup variation likely drives defects.', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, deletedAt: null, diff --git a/packages/hooks/src/__tests__/useSuspectedCauses.test.ts b/packages/hooks/src/__tests__/useSuspectedCauses.test.ts index c5e0b7344..218307068 100644 --- a/packages/hooks/src/__tests__/useSuspectedCauses.test.ts +++ b/packages/hooks/src/__tests__/useSuspectedCauses.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, vi } from 'vitest'; import { renderHook, act } from '@testing-library/react'; import { useSuspectedCauses } from '../useSuspectedCauses'; -import { createSuspectedCause } from '@variscout/core/findings'; -import type { SuspectedCause } from '@variscout/core'; +import { createHypothesis } from '@variscout/core/findings'; +import type { Hypothesis } from '@variscout/core'; describe('useSuspectedCauses', () => { it('starts with empty hubs when no initialHubs provided', () => { @@ -11,9 +11,7 @@ describe('useSuspectedCauses', () => { }); it('starts with provided initialHubs', () => { - const initial: SuspectedCause[] = [ - createSuspectedCause('Nozzle wear', 'High vibration at night'), - ]; + const initial: Hypothesis[] = [createHypothesis('Nozzle wear', 'High vibration at night')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); expect(result.current.hubs).toHaveLength(1); expect(result.current.hubs[0].name).toBe('Nozzle wear'); @@ -22,7 +20,7 @@ describe('useSuspectedCauses', () => { describe('createHub', () => { it('adds a hub and returns it', () => { const { result } = renderHook(() => useSuspectedCauses({ initialHubs: [] })); - let created: SuspectedCause; + let created: Hypothesis; act(() => { created = result.current.createHub('Shift effect', 'Night shift runs hotter'); }); @@ -44,13 +42,13 @@ describe('useSuspectedCauses', () => { expect(onChange).toHaveBeenCalledTimes(1); }); - it('new hub has status suspected and empty connections', () => { + it('new hub has status proposed and empty connections', () => { const { result } = renderHook(() => useSuspectedCauses({ initialHubs: [] })); act(() => { result.current.createHub('Test hub', ''); }); const hub = result.current.hubs[0]; - expect(hub.status).toBe('suspected'); + expect(hub.status).toBe('proposed'); expect(hub.questionIds).toEqual([]); expect(hub.findingIds).toEqual([]); }); @@ -58,11 +56,11 @@ describe('useSuspectedCauses', () => { describe('resetHubs', () => { it('replaces all hubs with the provided list', () => { - const initial = [createSuspectedCause('Old hub', '')]; + const initial = [createHypothesis('Old hub', '')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); const replacement = [ - createSuspectedCause('New hub A', 'synthesis A'), - createSuspectedCause('New hub B', 'synthesis B'), + createHypothesis('New hub A', 'synthesis A'), + createHypothesis('New hub B', 'synthesis B'), ]; act(() => { result.current.resetHubs(replacement); @@ -77,7 +75,7 @@ describe('useSuspectedCauses', () => { const { result } = renderHook(() => useSuspectedCauses({ initialHubs: [], onHubsChange: onChange }) ); - const newHubs = [createSuspectedCause('Migrated hub', 'from legacy causeRole')]; + const newHubs = [createHypothesis('Migrated hub', 'from legacy causeRole')]; act(() => { result.current.resetHubs(newHubs); }); @@ -86,7 +84,7 @@ describe('useSuspectedCauses', () => { }); it('replaces existing hubs with an empty list', () => { - const initial = [createSuspectedCause('Hub', '')]; + const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); act(() => { result.current.resetHubs([]); @@ -97,7 +95,7 @@ describe('useSuspectedCauses', () => { describe('updateHub', () => { it('updates name and synthesis', () => { - const initial = [createSuspectedCause('Old name', 'Old synthesis')]; + const initial = [createHypothesis('Old name', 'Old synthesis')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); const hubId = initial[0].id; act(() => { @@ -109,7 +107,7 @@ describe('useSuspectedCauses', () => { }); it('updates branch nextMove without changing linked evidence', () => { - const initial = [createSuspectedCause('Hub', '', ['q-001'], ['f-001'])]; + const initial = [createHypothesis('Hub', '', ['q-001'], ['f-001'])]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); act(() => { result.current.updateHub(initial[0].id, { @@ -123,7 +121,7 @@ describe('useSuspectedCauses', () => { }); it('updates updatedAt to a valid ISO timestamp', () => { - const initial = [createSuspectedCause('Hub', '')]; + const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); act(() => { result.current.updateHub(initial[0].id, { name: 'Updated' }); @@ -134,7 +132,7 @@ describe('useSuspectedCauses', () => { it('calls onHubsChange', () => { const onChange = vi.fn(); - const initial = [createSuspectedCause('Hub', '')]; + const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial, onHubsChange: onChange }) ); @@ -145,7 +143,7 @@ describe('useSuspectedCauses', () => { }); it('does nothing for unknown hubId', () => { - const initial = [createSuspectedCause('Hub', '')]; + const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); act(() => { result.current.updateHub('nonexistent-id', { name: 'Ghost' }); @@ -156,7 +154,7 @@ describe('useSuspectedCauses', () => { describe('deleteHub', () => { it('removes the hub by id', () => { - const initial = [createSuspectedCause('Hub A', ''), createSuspectedCause('Hub B', '')]; + const initial = [createHypothesis('Hub A', ''), createHypothesis('Hub B', '')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); act(() => { result.current.deleteHub(initial[0].id); @@ -167,7 +165,7 @@ describe('useSuspectedCauses', () => { it('calls onHubsChange', () => { const onChange = vi.fn(); - const initial = [createSuspectedCause('Hub', '')]; + const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial, onHubsChange: onChange }) ); @@ -180,7 +178,7 @@ describe('useSuspectedCauses', () => { describe('connectQuestion', () => { it('adds a questionId to the hub', () => { - const initial = [createSuspectedCause('Hub', '')]; + const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); act(() => { result.current.connectQuestion(initial[0].id, 'q-001'); @@ -189,7 +187,7 @@ describe('useSuspectedCauses', () => { }); it('does not add duplicate question connections', () => { - const initial = [createSuspectedCause('Hub', '', ['q-001'])]; + const initial = [createHypothesis('Hub', '', ['q-001'])]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); act(() => { result.current.connectQuestion(initial[0].id, 'q-001'); @@ -199,7 +197,7 @@ describe('useSuspectedCauses', () => { it('updates updatedAt and calls onHubsChange', () => { const onChange = vi.fn(); - const initial = [createSuspectedCause('Hub', '')]; + const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial, onHubsChange: onChange }) ); @@ -214,7 +212,7 @@ describe('useSuspectedCauses', () => { describe('disconnectQuestion', () => { it('removes a questionId from the hub', () => { - const initial = [createSuspectedCause('Hub', '', ['q-001', 'q-002'])]; + const initial = [createHypothesis('Hub', '', ['q-001', 'q-002'])]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); act(() => { result.current.disconnectQuestion(initial[0].id, 'q-001'); @@ -225,7 +223,7 @@ describe('useSuspectedCauses', () => { it('calls onHubsChange', () => { const onChange = vi.fn(); - const initial = [createSuspectedCause('Hub', '', ['q-001'])]; + const initial = [createHypothesis('Hub', '', ['q-001'])]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial, onHubsChange: onChange }) ); @@ -238,7 +236,7 @@ describe('useSuspectedCauses', () => { describe('connectFinding', () => { it('adds a findingId to the hub', () => { - const initial = [createSuspectedCause('Hub', '')]; + const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); act(() => { result.current.connectFinding(initial[0].id, 'f-001'); @@ -247,7 +245,7 @@ describe('useSuspectedCauses', () => { }); it('does not add duplicate finding connections', () => { - const initial = [createSuspectedCause('Hub', '', [], ['f-001'])]; + const initial = [createHypothesis('Hub', '', [], ['f-001'])]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); act(() => { result.current.connectFinding(initial[0].id, 'f-001'); @@ -257,7 +255,7 @@ describe('useSuspectedCauses', () => { it('updates updatedAt and calls onHubsChange', () => { const onChange = vi.fn(); - const initial = [createSuspectedCause('Hub', '')]; + const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial, onHubsChange: onChange }) ); @@ -270,7 +268,7 @@ describe('useSuspectedCauses', () => { describe('disconnectFinding', () => { it('removes a findingId from the hub', () => { - const initial = [createSuspectedCause('Hub', '', [], ['f-001', 'f-002'])]; + const initial = [createHypothesis('Hub', '', [], ['f-001', 'f-002'])]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); act(() => { result.current.disconnectFinding(initial[0].id, 'f-001'); @@ -281,7 +279,7 @@ describe('useSuspectedCauses', () => { it('calls onHubsChange', () => { const onChange = vi.fn(); - const initial = [createSuspectedCause('Hub', '', [], ['f-001'])]; + const initial = [createHypothesis('Hub', '', [], ['f-001'])]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial, onHubsChange: onChange }) ); @@ -294,7 +292,7 @@ describe('useSuspectedCauses', () => { describe('setHubStatus', () => { it('updates status to confirmed', () => { - const initial = [createSuspectedCause('Hub', '')]; + const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); act(() => { result.current.setHubStatus(initial[0].id, 'confirmed'); @@ -302,18 +300,18 @@ describe('useSuspectedCauses', () => { expect(result.current.hubs[0].status).toBe('confirmed'); }); - it('updates status to not-confirmed', () => { - const initial = [createSuspectedCause('Hub', '')]; + it('updates status to refuted', () => { + const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); act(() => { - result.current.setHubStatus(initial[0].id, 'not-confirmed'); + result.current.setHubStatus(initial[0].id, 'refuted'); }); - expect(result.current.hubs[0].status).toBe('not-confirmed'); + expect(result.current.hubs[0].status).toBe('refuted'); }); it('updates updatedAt and calls onHubsChange', () => { const onChange = vi.fn(); - const initial = [createSuspectedCause('Hub', '')]; + const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial, onHubsChange: onChange }) ); @@ -329,8 +327,8 @@ describe('useSuspectedCauses', () => { describe('getHubForQuestion', () => { it('returns the hub containing the given questionId', () => { const initial = [ - createSuspectedCause('Hub A', '', ['q-001']), - createSuspectedCause('Hub B', '', ['q-002']), + createHypothesis('Hub A', '', ['q-001']), + createHypothesis('Hub B', '', ['q-002']), ]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); expect(result.current.getHubForQuestion('q-001')?.name).toBe('Hub A'); @@ -338,7 +336,7 @@ describe('useSuspectedCauses', () => { }); it('returns undefined when no hub contains the questionId', () => { - const initial = [createSuspectedCause('Hub', '', ['q-001'])]; + const initial = [createHypothesis('Hub', '', ['q-001'])]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); expect(result.current.getHubForQuestion('q-999')).toBeUndefined(); }); @@ -352,8 +350,8 @@ describe('useSuspectedCauses', () => { describe('getHubForFinding', () => { it('returns the hub containing the given findingId', () => { const initial = [ - createSuspectedCause('Hub A', '', [], ['f-001']), - createSuspectedCause('Hub B', '', [], ['f-002']), + createHypothesis('Hub A', '', [], ['f-001']), + createHypothesis('Hub B', '', [], ['f-002']), ]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); expect(result.current.getHubForFinding('f-001')?.name).toBe('Hub A'); @@ -361,7 +359,7 @@ describe('useSuspectedCauses', () => { }); it('returns undefined when no hub contains the findingId', () => { - const initial = [createSuspectedCause('Hub', '', [], ['f-001'])]; + const initial = [createHypothesis('Hub', '', [], ['f-001'])]; const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); expect(result.current.getHubForFinding('f-999')).toBeUndefined(); }); diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index c20e4c2c3..42c042a08 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -489,10 +489,10 @@ export { type UseCanvasInvestigationOverlaysResult, } from './useCanvasInvestigationOverlays'; -// Suspected Cause Hubs (named mechanism groupings for investigation synthesis) +// Hypothesis Hubs (named mechanism groupings for investigation synthesis) export { useSuspectedCauses, - type SuspectedCauseUpdate, + type HypothesisUpdate, type UseSuspectedCausesOptions, type UseSuspectedCausesReturn, } from './useSuspectedCauses'; diff --git a/packages/hooks/src/types.ts b/packages/hooks/src/types.ts index 4ae675cde..f4ca7f1fd 100644 --- a/packages/hooks/src/types.ts +++ b/packages/hooks/src/types.ts @@ -15,7 +15,7 @@ import type { StageOrderMode, Finding, Question, - SuspectedCause, + Hypothesis, CausalLink, ProcessContext, InvestigationCategory, @@ -179,8 +179,8 @@ export interface AnalysisState { categories?: InvestigationCategory[]; // --- Suspected causes (investigation synthesis) --- - /** SuspectedCause hubs connecting evidence threads */ - suspectedCauses?: SuspectedCause[]; + /** Hypothesis hubs connecting evidence threads */ + suspectedCauses?: Hypothesis[]; // --- Causal links (investigation DAG) --- /** Causal links between factors (investigation DAG) */ diff --git a/packages/hooks/src/useAIContext.ts b/packages/hooks/src/useAIContext.ts index 7839a4ab5..decffae09 100644 --- a/packages/hooks/src/useAIContext.ts +++ b/packages/hooks/src/useAIContext.ts @@ -14,7 +14,7 @@ import type { StagedComparison, Locale, EntryScenario, - SuspectedCause, + Hypothesis, } from '@variscout/core'; import type { StatsResult, @@ -85,7 +85,7 @@ export interface UseAIContextOptions { /** Evidence Map topology for graph-aware CoScout reasoning (ADR-066) */ evidenceMapTopology?: BuildAIContextOptions['evidenceMapTopology']; /** Suspected cause hubs from investigation (ADR-064) */ - suspectedCauses?: SuspectedCause[]; + suspectedCauses?: Hypothesis[]; /** Best subsets result for model equation and interaction effects context */ bestSubsetsResult?: BuildAIContextOptions['bestSubsetsResult']; } diff --git a/packages/hooks/src/useCanvasInvestigationOverlays.ts b/packages/hooks/src/useCanvasInvestigationOverlays.ts index 731707c1a..1a3e4cc97 100644 --- a/packages/hooks/src/useCanvasInvestigationOverlays.ts +++ b/packages/hooks/src/useCanvasInvestigationOverlays.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import type { CausalLink, Finding, Question, SuspectedCause } from '@variscout/core'; +import type { CausalLink, Finding, Question, Hypothesis } from '@variscout/core'; import type { ProcessMap } from '@variscout/core/frame'; import { selectHypothesisTributaries } from '@variscout/stores'; @@ -90,7 +90,7 @@ export interface CanvasOverlayFindingItem { export interface CanvasOverlaySuspectedCauseItem { id: string; name: string; - status: SuspectedCause['status']; + status: Hypothesis['status']; questionId?: string; focus: CanvasInvestigationFocus; } @@ -132,7 +132,7 @@ export interface BuildCanvasInvestigationOverlaysArgs { map: ProcessMap; questions?: readonly Question[]; findings?: readonly Finding[]; - suspectedCauses?: readonly SuspectedCause[]; + suspectedCauses?: readonly Hypothesis[]; causalLinks?: readonly CausalLink[]; } @@ -200,7 +200,7 @@ function hubStepIds({ map, columnMap, }: { - hub: SuspectedCause; + hub: Hypothesis; findings: readonly Finding[]; questions: readonly Question[]; map: ProcessMap; @@ -229,11 +229,11 @@ function hubStepIds({ return stepsForColumns(columns, columnMap); } -function isPromotedHub(hub: SuspectedCause): boolean { - return hub.status === 'suspected' || hub.status === 'confirmed'; +function isPromotedHub(hub: Hypothesis): boolean { + return hub.status !== 'refuted'; } -function hubOwnsLink(hub: SuspectedCause, link: CausalLink): boolean { +function hubOwnsLink(hub: Hypothesis, link: CausalLink): boolean { if (link.suspectedCauseId && link.suspectedCauseId === hub.id) return true; return ( link.questionIds.some(id => hub.questionIds.includes(id)) || @@ -344,7 +344,7 @@ export function buildCanvasInvestigationOverlays({ const step = byStep[stepId]; if (!step) continue; if (isPromotedHub(hub)) addUnique(step.suspectedCauses, item); - if (hub.status === 'not-confirmed') step.investigationCounts.refuted += 1; + if (hub.status === 'refuted') step.investigationCounts.refuted += 1; else if (hub.status === 'confirmed') step.investigationCounts.supported += 1; else step.investigationCounts.open += 1; } diff --git a/packages/hooks/src/useCurrentUnderstanding.ts b/packages/hooks/src/useCurrentUnderstanding.ts index d9804cb28..597751129 100644 --- a/packages/hooks/src/useCurrentUnderstanding.ts +++ b/packages/hooks/src/useCurrentUnderstanding.ts @@ -6,7 +6,7 @@ import { type ProblemCondition, type ProcessContext, type Question, - type SuspectedCause, + type Hypothesis, } from '@variscout/core'; export interface CurrentUnderstandingStats { @@ -29,7 +29,7 @@ export interface UseCurrentUnderstandingOptions { stats?: CurrentUnderstandingStats | null; problemStatement?: CurrentUnderstandingProblemStatementState; questions?: Question[]; - suspectedCauseHubs?: SuspectedCause[]; + hypothesisHubs?: Hypothesis[]; scopedPattern?: string | null; onCurrentUnderstandingChange?: ( currentUnderstanding: CurrentUnderstanding | undefined, @@ -97,9 +97,9 @@ function activeMechanismsFromQuestions(questions: Question[]) { .filter((mechanism): mechanism is NonNullable => mechanism !== null); } -function activeMechanismsFromHubs(hubs: SuspectedCause[]) { +function activeMechanismsFromHubs(hubs: Hypothesis[]) { return hubs - .filter(hub => hub.status !== 'not-confirmed') + .filter(hub => hub.status !== 'refuted') .map(hub => { const evidenceLabel = hub.evidence?.contribution.description; @@ -138,7 +138,7 @@ export function useCurrentUnderstanding({ stats, problemStatement, questions = [], - suspectedCauseHubs = [], + hypothesisHubs = [], scopedPattern, onCurrentUnderstandingChange, }: UseCurrentUnderstandingOptions): UseCurrentUnderstandingReturn { @@ -160,8 +160,8 @@ export function useCurrentUnderstanding({ const currentUnderstanding = useMemo(() => { const mechanisms = - suspectedCauseHubs.length > 0 - ? activeMechanismsFromHubs(suspectedCauseHubs) + hypothesisHubs.length > 0 + ? activeMechanismsFromHubs(hypothesisHubs) : activeMechanismsFromQuestions(questions); const liveStatement = problemStatement?.draft ?? @@ -185,7 +185,7 @@ export function useCurrentUnderstanding({ problemStatement?.liveStatement, questions, scopedPattern, - suspectedCauseHubs, + hypothesisHubs, ]); const derivedSignature = signature(currentUnderstanding, problemCondition); diff --git a/packages/hooks/src/useDataIngestion.ts b/packages/hooks/src/useDataIngestion.ts index 42296da1d..b9449bdeb 100644 --- a/packages/hooks/src/useDataIngestion.ts +++ b/packages/hooks/src/useDataIngestion.ts @@ -38,7 +38,7 @@ import { type Finding, type Question, type InvestigationCategory, - type SuspectedCause, + type Hypothesis, type CausalLink, type SubgroupConfig, type DisplayOptions, @@ -76,8 +76,8 @@ export interface DataIngestionActions { setQuestions?: (questions: Question[]) => void; /** Set pre-populated investigation categories (for showcase/demo datasets) */ setCategories?: (categories: InvestigationCategory[]) => void; - /** Replace SuspectedCause hubs with a seeded set (showcase/demo datasets) */ - setSuspectedCauses?: (hubs: SuspectedCause[]) => void; + /** Replace Hypothesis hubs with a seeded set (showcase/demo datasets) */ + setSuspectedCauses?: (hubs: Hypothesis[]) => void; /** Replace CausalLinks with a seeded set (showcase/demo datasets) */ setCausalLinks?: (links: CausalLink[]) => void; /** Set the process context (used to seed FRAME Process Map on showcases) */ diff --git a/packages/hooks/src/useEvidenceMapData.ts b/packages/hooks/src/useEvidenceMapData.ts index 00c0784ec..c4c864f36 100644 --- a/packages/hooks/src/useEvidenceMapData.ts +++ b/packages/hooks/src/useEvidenceMapData.ts @@ -21,7 +21,7 @@ import { computeEvidenceMapLayout } from '@variscout/core/stats'; import type { EvidenceMapLayout, FactorNodeLayout } from '@variscout/core/stats'; import { findConvergencePoints } from '@variscout/core/stats'; import type { ResolvedMode } from '@variscout/core/strategy'; -import type { CausalLink, Question, Finding, SuspectedCause } from '@variscout/core/findings'; +import type { CausalLink, Question, Finding, Hypothesis } from '@variscout/core/findings'; import type { FactorNodeData, RelationshipEdgeData, @@ -63,7 +63,7 @@ export interface UseEvidenceMapDataOptions { /** Findings (Layer 2 evidence counts) */ findings?: Finding[]; /** Suspected cause hubs (Layer 3 convergence enrichment) */ - suspectedCauses?: SuspectedCause[]; + suspectedCauses?: Hypothesis[]; } export interface UseEvidenceMapDataReturn { @@ -184,7 +184,7 @@ function mapCausalEdge( function mapConvergencePoint( cp: { factor: string; incomingLinks: CausalLink[] }, nodePositions: Map, - suspectedCauses: SuspectedCause[] + suspectedCauses: Hypothesis[] ): ConvergencePointData | null { const pos = nodePositions.get(cp.factor); if (!pos) return null; diff --git a/packages/hooks/src/useEvidenceMapTimeline.ts b/packages/hooks/src/useEvidenceMapTimeline.ts index 0f7c05a50..f7d59af5a 100644 --- a/packages/hooks/src/useEvidenceMapTimeline.ts +++ b/packages/hooks/src/useEvidenceMapTimeline.ts @@ -10,7 +10,7 @@ */ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import type { CausalLink, Question, Finding, SuspectedCause } from '@variscout/core/findings'; +import type { CausalLink, Question, Finding, Hypothesis } from '@variscout/core/findings'; // ============================================================================ // Types @@ -26,7 +26,7 @@ export interface TimelineFrame { visibleFactors: string[]; /** CausalLink IDs visible up to this frame */ visibleLinks: string[]; - /** SuspectedCause IDs visible up to this frame */ + /** Hypothesis IDs visible up to this frame */ visibleHubs: string[]; } @@ -38,7 +38,7 @@ export interface UseEvidenceMapTimelineOptions { /** Findings with createdAt timestamps (numeric, converted to ISO) */ findings?: Finding[]; /** Suspected cause hubs with createdAt timestamps */ - suspectedCauses?: SuspectedCause[]; + suspectedCauses?: Hypothesis[]; /** Playback interval in milliseconds (default: 1500) */ intervalMs?: number; /** Maximum number of frames before grouping by day (default: 30) */ @@ -78,7 +78,7 @@ function collectArtifacts( causalLinks: CausalLink[], questions: Question[], findings: Finding[], - suspectedCauses: SuspectedCause[] + suspectedCauses: Hypothesis[] ): TimestampedArtifact[] { const artifacts: TimestampedArtifact[] = []; diff --git a/packages/hooks/src/useHubCommentStream.ts b/packages/hooks/src/useHubCommentStream.ts index e823a81aa..6b39b29ff 100644 --- a/packages/hooks/src/useHubCommentStream.ts +++ b/packages/hooks/src/useHubCommentStream.ts @@ -2,7 +2,7 @@ * useHubCommentStream — live SSE subscription to per-hub comment streams. * * Mirrors the brainstorm session SSE pattern in `useBrainstormSession.ts`. - * The Investigation Wall renders hypothesis hubs (SuspectedCause), each with + * The Investigation Wall renders hypothesis hubs (Hypothesis), each with * an optional live team discussion. This hook opens one EventSource per * visible hub and dispatches incoming `comment` events to the * `investigationStore` via `addHubComment`'s server-idempotent id path, or diff --git a/packages/hooks/src/useHubComputations.ts b/packages/hooks/src/useHubComputations.ts index f4ff19c21..6411acbfa 100644 --- a/packages/hooks/src/useHubComputations.ts +++ b/packages/hooks/src/useHubComputations.ts @@ -6,7 +6,7 @@ * shared hook. * * Computes: - * - hubEvidences: Map of hub id → SuspectedCauseEvidence (R²adj-based) + * - hubEvidences: Map of hub id → HypothesisEvidence (R²adj-based) * - worstLevels: Record of factor → worst level string (for projection) * - hubProjections: Map of hub id → HubProjection (predicted improvement) */ @@ -14,14 +14,14 @@ import { useMemo } from 'react'; import { computeHubEvidence, computeHubProjection } from '@variscout/core/findings'; import type { BestSubsetsResult } from '@variscout/core/stats'; -import type { HubProjection, SuspectedCauseEvidence } from '@variscout/core/findings'; +import type { HubProjection, HypothesisEvidence } from '@variscout/core/findings'; import type { Question } from '@variscout/core'; import { resolveMode } from '@variscout/core/strategy'; import { useInvestigationStore, useProjectStore } from '@variscout/stores'; export interface UseHubComputationsReturn { /** Evidence summary per hub (undefined when no hubs exist) */ - hubEvidences: Map | undefined; + hubEvidences: Map | undefined; /** Worst factor levels derived from best-subsets level effects */ worstLevels: Record; /** Projection per hub (undefined when no hubs or no best-subsets result) */ @@ -48,7 +48,7 @@ export function useHubComputations( const hubEvidences = useMemo(() => { if (hubs.length === 0) return undefined; - const evidenceMode: SuspectedCauseEvidence['mode'] = + const evidenceMode: HypothesisEvidence['mode'] = resolved === 'capability' ? 'capability' : resolved === 'performance' @@ -57,7 +57,7 @@ export function useHubComputations( ? 'yamazumi' : 'standard'; - const map = new Map(); + const map = new Map(); for (const hub of hubs) { map.set(hub.id, computeHubEvidence(hub, questions, bestSubsets, evidenceMode)); } diff --git a/packages/hooks/src/useSharedWallProps.ts b/packages/hooks/src/useSharedWallProps.ts index 7435005c8..d0bf36fbf 100644 --- a/packages/hooks/src/useSharedWallProps.ts +++ b/packages/hooks/src/useSharedWallProps.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import { useInvestigationStore, useWallLayoutStore } from '@variscout/stores'; -import type { Finding, Question, SuspectedCause } from '@variscout/core'; +import type { Finding, Question, Hypothesis } from '@variscout/core'; import type { ProcessMap } from '@variscout/core/frame'; export interface UseSharedWallPropsArgs { @@ -12,7 +12,7 @@ export interface UseSharedWallPropsArgs { } export interface UseSharedWallPropsReturn { - hubs: SuspectedCause[]; + hubs: Hypothesis[]; findings: Finding[]; questions: Question[]; processMap: ProcessMap | undefined; diff --git a/packages/hooks/src/useSuspectedCauses.ts b/packages/hooks/src/useSuspectedCauses.ts index 4a462b830..aa3ce50c8 100644 --- a/packages/hooks/src/useSuspectedCauses.ts +++ b/packages/hooks/src/useSuspectedCauses.ts @@ -1,6 +1,6 @@ import { useState, useCallback } from 'react'; -import { createSuspectedCause } from '@variscout/core/findings'; -import type { SuspectedCause } from '@variscout/core'; +import { createHypothesis } from '@variscout/core/findings'; +import type { Hypothesis } from '@variscout/core'; // ============================================================================ // Types @@ -8,38 +8,32 @@ import type { SuspectedCause } from '@variscout/core'; export interface UseSuspectedCausesOptions { /** Initial hubs (for restoring persisted state) */ - initialHubs: SuspectedCause[]; + initialHubs: Hypothesis[]; /** Callback when hubs change (for external persistence) */ - onHubsChange?: (hubs: SuspectedCause[]) => void; + onHubsChange?: (hubs: Hypothesis[]) => void; } -export type SuspectedCauseUpdate = Partial< +export type HypothesisUpdate = Partial< Pick< - SuspectedCause, - | 'name' - | 'synthesis' - | 'nextMove' - | 'branchStatus' - | 'branchReadiness' - | 'counterFindingIds' - | 'checkQuestionIds' + Hypothesis, + 'name' | 'synthesis' | 'status' | 'nextMove' | 'counterFindingIds' | 'checkQuestionIds' > >; export interface UseSuspectedCausesReturn { - /** Current list of suspected cause hubs */ - hubs: SuspectedCause[]; + /** Current list of hypothesis hubs */ + hubs: Hypothesis[]; /** Create a new hub and return it */ - createHub: (name: string, synthesis: string) => SuspectedCause; + createHub: (name: string, synthesis: string) => Hypothesis; /** Update name and/or synthesis of an existing hub */ - updateHub: (hubId: string, updates: SuspectedCauseUpdate) => void; + updateHub: (hubId: string, updates: HypothesisUpdate) => void; /** Delete a hub by id */ deleteHub: (hubId: string) => void; /** * Replace the entire hub list atomically (e.g. after migration). * Updates hook state and fires onHubsChange so the store stays in sync. */ - resetHubs: (newHubs: SuspectedCause[]) => void; + resetHubs: (newHubs: Hypothesis[]) => void; /** Connect a question to a hub (no-op if already connected) */ connectQuestion: (hubId: string, questionId: string) => void; /** Disconnect a question from a hub */ @@ -49,11 +43,11 @@ export interface UseSuspectedCausesReturn { /** Disconnect a finding from a hub */ disconnectFinding: (hubId: string, findingId: string) => void; /** Update the status of a hub */ - setHubStatus: (hubId: string, status: SuspectedCause['status']) => void; + setHubStatus: (hubId: string, status: Hypothesis['status']) => void; /** Find the hub that contains a given questionId */ - getHubForQuestion: (questionId: string) => SuspectedCause | undefined; + getHubForQuestion: (questionId: string) => Hypothesis | undefined; /** Find the hub that contains a given findingId */ - getHubForFinding: (findingId: string) => SuspectedCause | undefined; + getHubForFinding: (findingId: string) => Hypothesis | undefined; } // ============================================================================ @@ -61,7 +55,7 @@ export interface UseSuspectedCausesReturn { // ============================================================================ /** - * Manages suspected cause hubs — named groupings of questions and findings + * Manages hypothesis hubs — named groupings of questions and findings * that represent a single mechanism the analyst believes is driving variation. * * Each hub connects multiple evidence threads (questions + findings) under a @@ -72,11 +66,11 @@ export interface UseSuspectedCausesReturn { */ export function useSuspectedCauses(options: UseSuspectedCausesOptions): UseSuspectedCausesReturn { const { initialHubs, onHubsChange } = options; - const [hubs, setHubs] = useState(initialHubs); + const [hubs, setHubs] = useState(initialHubs); /** Update state and notify external listener via functional updater pattern */ const update = useCallback( - (updater: (prev: SuspectedCause[]) => SuspectedCause[]) => { + (updater: (prev: Hypothesis[]) => Hypothesis[]) => { setHubs(prev => { const next = updater(prev); onHubsChange?.(next); @@ -87,8 +81,8 @@ export function useSuspectedCauses(options: UseSuspectedCausesOptions): UseSuspe ); const createHub = useCallback( - (name: string, synthesis: string): SuspectedCause => { - const hub = createSuspectedCause(name, synthesis); + (name: string, synthesis: string): Hypothesis => { + const hub = createHypothesis(name, synthesis); update(prev => [...prev, hub]); return hub; }, @@ -96,7 +90,7 @@ export function useSuspectedCauses(options: UseSuspectedCausesOptions): UseSuspe ); const updateHub = useCallback( - (hubId: string, updates: SuspectedCauseUpdate): void => { + (hubId: string, updates: HypothesisUpdate): void => { update(prev => prev.map(h => (h.id === hubId ? { ...h, ...updates, updatedAt: Date.now() } : h)) ); @@ -180,7 +174,7 @@ export function useSuspectedCauses(options: UseSuspectedCausesOptions): UseSuspe ); const resetHubs = useCallback( - (newHubs: SuspectedCause[]): void => { + (newHubs: Hypothesis[]): void => { setHubs(newHubs); onHubsChange?.(newHubs); }, @@ -188,21 +182,20 @@ export function useSuspectedCauses(options: UseSuspectedCausesOptions): UseSuspe ); const setHubStatus = useCallback( - (hubId: string, status: SuspectedCause['status']): void => { + (hubId: string, status: Hypothesis['status']): void => { update(prev => prev.map(h => (h.id !== hubId ? h : { ...h, status, updatedAt: Date.now() }))); }, [update] ); const getHubForQuestion = useCallback( - (questionId: string): SuspectedCause | undefined => + (questionId: string): Hypothesis | undefined => hubs.find(h => h.questionIds.includes(questionId)), [hubs] ); const getHubForFinding = useCallback( - (findingId: string): SuspectedCause | undefined => - hubs.find(h => h.findingIds.includes(findingId)), + (findingId: string): Hypothesis | undefined => hubs.find(h => h.findingIds.includes(findingId)), [hubs] ); diff --git a/packages/stores/src/__tests__/investigationStore.test.ts b/packages/stores/src/__tests__/investigationStore.test.ts index 66838220f..7f4b18234 100644 --- a/packages/stores/src/__tests__/investigationStore.test.ts +++ b/packages/stores/src/__tests__/investigationStore.test.ts @@ -5,7 +5,7 @@ import { useWallLayoutStore, getWallLayoutInitialState } from '../wallLayoutStor import type { FindingContext, FindingOutcome, - SuspectedCause, + Hypothesis, InvestigationCategory, GateNode, } from '@variscout/core'; @@ -489,7 +489,7 @@ describe('investigationStore — suspected cause hubs', () => { expect(state.suspectedCauses).toHaveLength(1); expect(state.suspectedCauses[0].name).toBe('Nozzle wear'); expect(state.suspectedCauses[0].synthesis).toBe('Night shift causes wear'); - expect(state.suspectedCauses[0].status).toBe('suspected'); + expect(state.suspectedCauses[0].status).toBe('proposed'); expect(hub.id).toBe(state.suspectedCauses[0].id); }); @@ -518,7 +518,7 @@ describe('investigationStore — suspected cause hubs', () => { const persisted = state.suspectedCauses[0]; expect(persisted.findingIds).toEqual([finding.id]); expect(persisted.name.startsWith('Suspected mechanism:')).toBe(true); - expect(persisted.status).toBe('suspected'); + expect(persisted.status).toBe('proposed'); }); it('createHubFromFinding uses fallback name when finding text is empty', () => { @@ -598,14 +598,14 @@ describe('investigationStore — suspected cause hubs', () => { useInvestigationStore.getState().createHub('B', 'b'); expect(useInvestigationStore.getState().suspectedCauses).toHaveLength(2); - const newHubs: SuspectedCause[] = [ + const newHubs: Hypothesis[] = [ { id: 'h-new', name: 'New', synthesis: 'Fresh', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, deletedAt: null, @@ -755,13 +755,13 @@ describe('investigationStore — bulk operations', () => { deletedAt: null as null, investigationId: 'inv-test-001', }; - const hub: SuspectedCause = { + const hub: Hypothesis = { id: 'h-1', name: 'Hub', synthesis: 'Synth', questionIds: ['q-1'], findingIds: ['f-1'], - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, deletedAt: null, diff --git a/packages/stores/src/__tests__/wallSelectors.test.ts b/packages/stores/src/__tests__/wallSelectors.test.ts index b672bf28a..33da2ace0 100644 --- a/packages/stores/src/__tests__/wallSelectors.test.ts +++ b/packages/stores/src/__tests__/wallSelectors.test.ts @@ -5,7 +5,7 @@ import { selectOpenQuestionsWithoutHub, selectQuestionsForHub, } from '../wallSelectors'; -import type { SuspectedCause, Finding, Question, FindingComment } from '@variscout/core'; +import type { Hypothesis, Finding, Question, FindingComment } from '@variscout/core'; import type { ProcessMap } from '@variscout/core/frame'; function fc(id: string, text: string, createdAt: number): FindingComment { @@ -25,13 +25,13 @@ describe('selectHubCommentStream', () => { const fComment1 = fc('fc1', 'chart note A', 1000); const fComment2 = fc('fc2', 'chart note B', 3000); - const hub: SuspectedCause = { + const hub: Hypothesis = { id: 'h1', name: 'H1', synthesis: '', questionIds: [], findingIds: ['f1'], - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, deletedAt: null, @@ -77,13 +77,13 @@ describe('selectHypothesisTributaries', () => { }; it('prefers explicit tributaryIds when set', () => { - const hub: SuspectedCause = { + const hub: Hypothesis = { id: 'h1', name: 'h', synthesis: '', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, deletedAt: null, @@ -95,13 +95,13 @@ describe('selectHypothesisTributaries', () => { }); it('returns empty when neither tributaryIds nor finding columns provided', () => { - const hub: SuspectedCause = { + const hub: Hypothesis = { id: 'h1', name: 'h', synthesis: '', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, deletedAt: null, @@ -112,13 +112,13 @@ describe('selectHypothesisTributaries', () => { }); it('derives tributaries from findings context filters', () => { - const hub: SuspectedCause = { + const hub: Hypothesis = { id: 'h1', name: 'h', synthesis: '', questionIds: [], findingIds: ['f1'], - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, deletedAt: null, @@ -143,13 +143,13 @@ describe('selectHypothesisTributaries', () => { }); it('returns empty when processMap is undefined', () => { - const hub: SuspectedCause = { + const hub: Hypothesis = { id: 'h1', name: 'h', synthesis: '', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, deletedAt: null, @@ -163,14 +163,14 @@ describe('selectHypothesisTributaries', () => { describe('selectOpenQuestionsWithoutHub', () => { it('returns open questions not linked to any hub', () => { - const hubs: SuspectedCause[] = [ + const hubs: Hypothesis[] = [ { id: 'h1', name: '', synthesis: '', questionIds: ['q1'], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, deletedAt: null, @@ -216,14 +216,14 @@ describe('selectOpenQuestionsWithoutHub', () => { describe('selectQuestionsForHub', () => { it('returns questions referenced by hub.questionIds', () => { - const hubs: SuspectedCause[] = [ + const hubs: Hypothesis[] = [ { id: 'h1', name: '', synthesis: '', questionIds: ['q1', 'q2'], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, deletedAt: null, diff --git a/packages/stores/src/investigationStore.ts b/packages/stores/src/investigationStore.ts index f205dbb7f..bbb3711ac 100644 --- a/packages/stores/src/investigationStore.ts +++ b/packages/stores/src/investigationStore.ts @@ -29,8 +29,8 @@ import type { QuestionValidationType, ImprovementIdea, InvestigationCategory, - SuspectedCause, - SuspectedCauseEvidence, + Hypothesis, + HypothesisEvidence, CausalLink, GateNode, } from '@variscout/core'; @@ -40,7 +40,7 @@ import { createActionItem, createQuestion, createImprovementIdea, - createSuspectedCause, + createHypothesis, createCausalLink, insertHubAsAndChild, type GatePath, @@ -48,16 +48,10 @@ import { export const STORE_LAYER = 'document' as const; -type SuspectedCauseUpdate = Partial< +type HypothesisUpdate = Partial< Pick< - SuspectedCause, - | 'name' - | 'synthesis' - | 'nextMove' - | 'branchStatus' - | 'branchReadiness' - | 'counterFindingIds' - | 'checkQuestionIds' + Hypothesis, + 'name' | 'synthesis' | 'status' | 'nextMove' | 'counterFindingIds' | 'checkQuestionIds' > >; @@ -78,7 +72,7 @@ export const MAX_CHILDREN_PER_PARENT = 8; export interface InvestigationState { findings: Finding[]; questions: Question[]; - suspectedCauses: SuspectedCause[]; + suspectedCauses: Hypothesis[]; causalLinks: CausalLink[]; categories: InvestigationCategory[]; problemContributionTree?: GateNode; @@ -179,22 +173,22 @@ export interface InvestigationActions { updateIdeaProjection: (questionId: string, ideaId: string, projection: FindingProjection) => void; // --- Hub actions --- - createHub: (name: string, synthesis: string) => SuspectedCause; + createHub: (name: string, synthesis: string) => Hypothesis; /** - * Create a new SuspectedCause hub from a finding. The hub is seeded with a + * Create a new Hypothesis hub from a finding. The hub is seeded with a * default name derived from the finding's text (truncated), linked to the * finding, and returned. Returns null if the finding doesn't exist. */ - createHubFromFinding: (findingId: string) => SuspectedCause | null; - updateHub: (hubId: string, updates: SuspectedCauseUpdate) => void; + createHubFromFinding: (findingId: string) => Hypothesis | null; + updateHub: (hubId: string, updates: HypothesisUpdate) => void; deleteHub: (hubId: string) => void; connectQuestionToHub: (hubId: string, questionId: string) => void; disconnectQuestionFromHub: (hubId: string, questionId: string) => void; connectFindingToHub: (hubId: string, findingId: string) => void; disconnectFindingFromHub: (hubId: string, findingId: string) => void; - setHubStatus: (hubId: string, status: SuspectedCause['status']) => void; - setHubEvidence: (hubId: string, evidence: SuspectedCauseEvidence) => void; - resetHubs: (hubs: SuspectedCause[]) => void; + setHubStatus: (hubId: string, status: Hypothesis['status']) => void; + setHubEvidence: (hubId: string, evidence: HypothesisEvidence) => void; + resetHubs: (hubs: Hypothesis[]) => void; /** * Append a comment to a hub (Investigation Wall team discussion). * Optimistically updates the hub's `comments` array, then POSTs to @@ -844,7 +838,7 @@ export const useInvestigationStore = create { - const hub = createSuspectedCause(name, synthesis); + const hub = createHypothesis(name, synthesis); set(state => ({ suspectedCauses: [...state.suspectedCauses, hub] })); return hub; }, @@ -854,7 +848,7 @@ export const useInvestigationStore = create 0 ? `Suspected mechanism: ${excerpt}` : 'New mechanism branch'; - const hub = createSuspectedCause(name, '', [], [findingId]); + const hub = createHypothesis(name, '', [], [findingId]); set(state => ({ suspectedCauses: [...state.suspectedCauses, hub] })); return hub; }, diff --git a/packages/stores/src/projectStore.ts b/packages/stores/src/projectStore.ts index 7cd4defb0..cda9ad21e 100644 --- a/packages/stores/src/projectStore.ts +++ b/packages/stores/src/projectStore.ts @@ -22,7 +22,7 @@ import type { InvestigationCategory, Finding, Question, - SuspectedCause, + Hypothesis, CausalLink, ParetoRow, DataQualityReport, @@ -97,7 +97,7 @@ export interface SerializedProject { findings?: Finding[]; questions?: Question[]; categories?: InvestigationCategory[]; - suspectedCauses?: SuspectedCause[]; + suspectedCauses?: Hypothesis[]; causalLinks?: CausalLink[]; } diff --git a/packages/stores/src/wallSelectors.ts b/packages/stores/src/wallSelectors.ts index 795ad6a63..698ad79bf 100644 --- a/packages/stores/src/wallSelectors.ts +++ b/packages/stores/src/wallSelectors.ts @@ -6,7 +6,7 @@ * See: docs/superpowers/plans/2026-04-19-investigation-wall.md, Task 4.4 */ -import type { SuspectedCause, Finding, FindingComment, Question } from '@variscout/core'; +import type { Hypothesis, Finding, FindingComment, Question } from '@variscout/core'; import type { ProcessMap, ProcessMapTributary } from '@variscout/core/frame'; // ───────────────────────────────────────────────────────────────────────────── @@ -28,7 +28,7 @@ export interface HubCommentEntry extends FindingComment { */ export function selectHubCommentStream( hubId: string, - hubs: SuspectedCause[], + hubs: Hypothesis[], findings: Finding[] ): HubCommentEntry[] { const hub = hubs.find(h => h.id === hubId); @@ -65,7 +65,7 @@ export function selectHubCommentStream( * Returns [] when processMap is undefined (mapless projects remain valid). */ export function selectHypothesisTributaries( - hub: SuspectedCause, + hub: Hypothesis, findings: Finding[], processMap: ProcessMap | undefined ): ProcessMapTributary[] { @@ -108,7 +108,7 @@ export function selectHypothesisTributaries( */ export function selectOpenQuestionsWithoutHub( questions: Question[], - hubs: SuspectedCause[] + hubs: Hypothesis[] ): Question[] { const inHub = new Set(hubs.flatMap(h => h.questionIds)); return questions.filter(q => q.status === 'open' && !inHub.has(q.id)); @@ -124,7 +124,7 @@ export function selectOpenQuestionsWithoutHub( */ export function selectQuestionsForHub( hubId: string, - hubs: SuspectedCause[], + hubs: Hypothesis[], questions: Question[] ): Question[] { const hub = hubs.find(h => h.id === hubId); diff --git a/packages/ui/src/components/Canvas/CanvasWorkspace.tsx b/packages/ui/src/components/Canvas/CanvasWorkspace.tsx index aa2b5d970..5d16359f7 100644 --- a/packages/ui/src/components/Canvas/CanvasWorkspace.tsx +++ b/packages/ui/src/components/Canvas/CanvasWorkspace.tsx @@ -21,7 +21,7 @@ import { type Question, type SpecLimits, type StepCapabilityStamp, - type SuspectedCause, + type Hypothesis, type TimelineWindow, type WorkflowReadinessSignals, } from '@variscout/core'; @@ -54,7 +54,7 @@ export interface CanvasWorkspaceProps { onHandoff?: (stepId: string) => void; questions?: readonly Question[]; findings?: readonly Finding[]; - suspectedCauses?: readonly SuspectedCause[]; + suspectedCauses?: readonly Hypothesis[]; causalLinks?: readonly CausalLink[]; problemCpk?: number; eventsPerWeek?: number; diff --git a/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx b/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx index 3e4f1c417..1656ba17d 100644 --- a/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx +++ b/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx @@ -155,7 +155,7 @@ const investigationOverlays: CanvasInvestigationOverlayModel = { { id: 'hub-1', name: 'Pressure setup drift', - status: 'suspected', + status: 'proposed', questionId: 'q-1', focus: { kind: 'suspected-cause', id: 'hub-1', questionId: 'q-1' }, }, diff --git a/packages/ui/src/components/Canvas/internal/StepNodeMarker.tsx b/packages/ui/src/components/Canvas/internal/StepNodeMarker.tsx index c42d1a253..46846e017 100644 --- a/packages/ui/src/components/Canvas/internal/StepNodeMarker.tsx +++ b/packages/ui/src/components/Canvas/internal/StepNodeMarker.tsx @@ -1,10 +1,10 @@ import { Flag } from 'lucide-react'; -import type { SuspectedCause } from '@variscout/core'; +import type { Hypothesis } from '@variscout/core'; export interface StepNodeMarkerHub { id: string; name: string; - status: SuspectedCause['status']; + status: Hypothesis['status']; } export interface StepNodeMarkerProps { @@ -15,8 +15,8 @@ export interface StepNodeMarkerProps { export function StepNodeMarker({ hubs, onClick }: StepNodeMarkerProps) { if (hubs.length === 0) return null; - const anySuspected = hubs.some(hub => hub.status === 'suspected'); - const colorClasses = anySuspected + const anyOpen = hubs.some(hub => hub.status !== 'confirmed' && hub.status !== 'refuted'); + const colorClasses = anyOpen ? 'border-status-warning bg-status-warning-soft text-status-warning' : 'border-status-info bg-status-info-soft text-status-info'; const names = hubs.map(hub => hub.name).join(', '); diff --git a/packages/ui/src/components/Canvas/internal/__tests__/CanvasStepCard.test.tsx b/packages/ui/src/components/Canvas/internal/__tests__/CanvasStepCard.test.tsx index faff69035..1a5c78ed7 100644 --- a/packages/ui/src/components/Canvas/internal/__tests__/CanvasStepCard.test.tsx +++ b/packages/ui/src/components/Canvas/internal/__tests__/CanvasStepCard.test.tsx @@ -32,7 +32,7 @@ const overlayWithPromoted: CanvasStepInvestigationOverlay = { { id: 'hub-1', name: 'Thermal drift', - status: 'suspected', + status: 'proposed', questionId: undefined, focus: { kind: 'suspected-cause', id: 'hub-1' }, }, diff --git a/packages/ui/src/components/Canvas/internal/__tests__/CanvasWallOverlay.test.tsx b/packages/ui/src/components/Canvas/internal/__tests__/CanvasWallOverlay.test.tsx index c01dc09b5..87bbfea67 100644 --- a/packages/ui/src/components/Canvas/internal/__tests__/CanvasWallOverlay.test.tsx +++ b/packages/ui/src/components/Canvas/internal/__tests__/CanvasWallOverlay.test.tsx @@ -1,7 +1,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { Finding, SuspectedCause } from '@variscout/core'; +import type { Finding, Hypothesis } from '@variscout/core'; import { getInvestigationInitialState, useInvestigationStore, @@ -14,7 +14,7 @@ vi.mock('@variscout/charts', () => ({ useWallIsMobile: vi.fn(), WallCanvas: (props: { mode?: string; - hubs: SuspectedCause[]; + hubs: Hypothesis[]; findings: Finding[]; onSelectHub?: (id: string) => void; onPromoteQuestion?: (id: string) => void; @@ -55,11 +55,11 @@ vi.mock('@variscout/charts', () => ({ const useWallIsMobileMock = vi.mocked(useWallIsMobile); -const sampleHub: SuspectedCause = { +const sampleHub: Hypothesis = { id: 'hub-1', name: 'Thermal drift', synthesis: '', - status: 'suspected', + status: 'proposed', questionIds: [], findingIds: [], createdAt: 1714000000000, diff --git a/packages/ui/src/components/Canvas/internal/__tests__/StepNodeMarker.test.tsx b/packages/ui/src/components/Canvas/internal/__tests__/StepNodeMarker.test.tsx index 1a0869eea..7d51dedc7 100644 --- a/packages/ui/src/components/Canvas/internal/__tests__/StepNodeMarker.test.tsx +++ b/packages/ui/src/components/Canvas/internal/__tests__/StepNodeMarker.test.tsx @@ -5,7 +5,7 @@ import { StepNodeMarker } from '../StepNodeMarker'; const suspectedHub = { id: 'hub-1', name: 'Chamber 3 thermal drift', - status: 'suspected' as const, + status: 'proposed' as const, }; const confirmedHub = { diff --git a/packages/ui/src/components/CoScoutPanel/ActionProposalCard.tsx b/packages/ui/src/components/CoScoutPanel/ActionProposalCard.tsx index bb55e886f..03758724f 100644 --- a/packages/ui/src/components/CoScoutPanel/ActionProposalCard.tsx +++ b/packages/ui/src/components/CoScoutPanel/ActionProposalCard.tsx @@ -55,8 +55,8 @@ const TOOL_CONFIG: Record< notify_action_owners: { labelKey: 'ai.tool.notifyOwners', icon: Bell, editable: false }, navigate_to: { labelKey: 'ai.tool.navigateTo', icon: Navigation, editable: false }, answer_question: { labelKey: 'ai.tool.answerQuestion', icon: CircleCheck, editable: true }, - suggest_suspected_cause: { - labelKey: 'ai.tool.suggestSuspectedCause', + suggest_hypothesis: { + labelKey: 'ai.tool.suggestHypothesis', icon: GitBranch, editable: true, }, diff --git a/packages/ui/src/components/FindingsWindow/InvestigationConclusion.tsx b/packages/ui/src/components/FindingsWindow/InvestigationConclusion.tsx index c5f6534b9..058d22ba9 100644 --- a/packages/ui/src/components/FindingsWindow/InvestigationConclusion.tsx +++ b/packages/ui/src/components/FindingsWindow/InvestigationConclusion.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useEffect } from 'react'; import { ChevronDown, ChevronRight, ClipboardCheck, Sparkles, Check, X, Plus } from 'lucide-react'; -import type { Question, Finding, SuspectedCause, SuspectedCauseEvidence } from '@variscout/core'; +import type { Question, Finding, Hypothesis, HypothesisEvidence } from '@variscout/core'; import type { HubProjection, EvidenceCluster } from '@variscout/core/findings'; import { HubComposer, type HubComposerBranchFields } from '../InvestigationConclusion/HubComposer'; import { HubCard } from '../InvestigationConclusion/HubCard'; @@ -35,9 +35,9 @@ export interface InvestigationConclusionProps { onAcceptProblemStatement?: (text: string) => void; /** Dismiss the draft */ onDismissProblemStatement?: () => void; - /** SuspectedCause hub management */ - hubs?: SuspectedCause[]; - hubEvidences?: Map; + /** Hypothesis hub management */ + hubs?: Hypothesis[]; + hubEvidences?: Map; hubProjections?: Map; onCreateHub?: ( name: string, diff --git a/packages/ui/src/components/InvestigationConclusion/HubCard.tsx b/packages/ui/src/components/InvestigationConclusion/HubCard.tsx index eee6c04e3..3b6b25662 100644 --- a/packages/ui/src/components/InvestigationConclusion/HubCard.tsx +++ b/packages/ui/src/components/InvestigationConclusion/HubCard.tsx @@ -1,11 +1,11 @@ import React, { useState } from 'react'; import { ChevronDown, ChevronRight, Pencil, Lightbulb, CheckSquare } from 'lucide-react'; -import type { SuspectedCause, SuspectedCauseEvidence } from '@variscout/core'; +import type { Hypothesis, HypothesisEvidence } from '@variscout/core'; import type { HubProjection } from '@variscout/core/findings'; export interface HubCardProps { - hub: SuspectedCause; - evidence?: SuspectedCauseEvidence; + hub: Hypothesis; + evidence?: HypothesisEvidence; projection?: HubProjection; questionsCount: number; findingsCount: number; @@ -14,10 +14,12 @@ export interface HubCardProps { onBrainstorm: () => void; } -const STATUS_STYLES: Record = { - suspected: { dot: 'bg-amber-500', label: 'Suspected' }, +const STATUS_STYLES: Record = { + proposed: { dot: 'bg-slate-400', label: 'Proposed' }, + evidenced: { dot: 'bg-amber-500', label: 'Evidenced' }, confirmed: { dot: 'bg-green-500', label: 'Confirmed' }, - 'not-confirmed': { dot: 'bg-content-muted/40', label: 'Not confirmed' }, + refuted: { dot: 'bg-content-muted/40', label: 'Refuted' }, + 'needs-disconfirmation': { dot: 'bg-orange-500', label: 'Needs disconfirmation' }, }; const HubCard: React.FC = ({ diff --git a/packages/ui/src/components/InvestigationConclusion/HubComposer.tsx b/packages/ui/src/components/InvestigationConclusion/HubComposer.tsx index 799685a3a..ec243c11b 100644 --- a/packages/ui/src/components/InvestigationConclusion/HubComposer.tsx +++ b/packages/ui/src/components/InvestigationConclusion/HubComposer.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { X, ChevronDown, ChevronRight } from 'lucide-react'; -import type { Question, Finding, SuspectedCause, SuspectedCauseEvidence } from '@variscout/core'; +import type { Question, Finding, Hypothesis, HypothesisEvidence } from '@variscout/core'; import type { HubProjection } from '@variscout/core/findings'; export interface HubComposerBranchFields { @@ -17,9 +17,9 @@ export interface HubComposerProps { /** All available findings for connection */ findings: Finding[]; /** Existing hub to edit (undefined = creating new) */ - editingHub?: SuspectedCause; + editingHub?: Hypothesis; /** Evidence from computeHubEvidence */ - evidence?: SuspectedCauseEvidence; + evidence?: HypothesisEvidence; /** Projection from computeHubProjection */ projection?: HubProjection; /** Called when hub is created/saved */ diff --git a/packages/ui/src/components/InvestigationConclusion/__tests__/HubCard.test.tsx b/packages/ui/src/components/InvestigationConclusion/__tests__/HubCard.test.tsx index 3f5e25c0c..4fdf006f2 100644 --- a/packages/ui/src/components/InvestigationConclusion/__tests__/HubCard.test.tsx +++ b/packages/ui/src/components/InvestigationConclusion/__tests__/HubCard.test.tsx @@ -1,21 +1,21 @@ import { describe, it, expect, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import { HubCard } from '../HubCard'; -import type { SuspectedCause, SuspectedCauseEvidence } from '@variscout/core/findings'; +import type { Hypothesis, HypothesisEvidence } from '@variscout/core/findings'; import type { HubProjection } from '@variscout/core/findings'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -function makeHub(overrides: Partial = {}): SuspectedCause { +function makeHub(overrides: Partial = {}): Hypothesis { return { id: 'hub-1', name: 'Nozzle wear on night shift', synthesis: 'Night shift thermal stress causes wear', questionIds: ['q1', 'q2'], findingIds: ['f1'], - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, deletedAt: null, @@ -45,7 +45,7 @@ describe('HubCard', () => { }); it('shows evidence badge when evidence provided', () => { - const evidence: SuspectedCauseEvidence = { + const evidence: HypothesisEvidence = { mode: 'standard', contribution: { value: 0.52, diff --git a/packages/ui/src/components/InvestigationConclusion/__tests__/HubComposer.test.tsx b/packages/ui/src/components/InvestigationConclusion/__tests__/HubComposer.test.tsx index 7a88eecf4..b63161816 100644 --- a/packages/ui/src/components/InvestigationConclusion/__tests__/HubComposer.test.tsx +++ b/packages/ui/src/components/InvestigationConclusion/__tests__/HubComposer.test.tsx @@ -1,7 +1,7 @@ import { describe, it, expect, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import { HubComposer } from '../HubComposer'; -import type { Question, Finding, SuspectedCause } from '@variscout/core/findings'; +import type { Question, Finding, Hypothesis } from '@variscout/core/findings'; // --------------------------------------------------------------------------- // Helpers @@ -123,13 +123,13 @@ describe('HubComposer', () => { }); it('pre-populates name and synthesis when editingHub provided', () => { - const editingHub: SuspectedCause = { + const editingHub: Hypothesis = { id: 'hub-1', name: 'Existing cause', synthesis: 'Evidence connects via thermal stress', questionIds: ['q1'], findingIds: [], - status: 'suspected', + status: 'proposed', createdAt: 1714000000000, updatedAt: 1714000000000, deletedAt: null, @@ -145,13 +145,13 @@ describe('HubComposer', () => { }); it('pre-populates branch nextMove when editingHub provided', () => { - const editingHub: SuspectedCause = { + const editingHub: Hypothesis = { id: 'hub-1', name: 'Existing cause', synthesis: '', questionIds: [], findingIds: [], - status: 'suspected', + status: 'proposed', nextMove: 'Check nozzle temperature after the night run.', createdAt: 1714000000000, updatedAt: 1714000000000, diff --git a/packages/ui/src/components/ProcessIntelligencePanel/ConclusionCard.tsx b/packages/ui/src/components/ProcessIntelligencePanel/ConclusionCard.tsx index f231c7f17..b8c399e16 100644 --- a/packages/ui/src/components/ProcessIntelligencePanel/ConclusionCard.tsx +++ b/packages/ui/src/components/ProcessIntelligencePanel/ConclusionCard.tsx @@ -1,33 +1,35 @@ import React from 'react'; import { Check, ArrowRight } from 'lucide-react'; import type { - SuspectedCause as SuspectedCauseHub, - SuspectedCauseEvidence, + Hypothesis as SuspectedCauseHub, + HypothesisEvidence, HubProjection, } from '@variscout/core'; -export interface SuspectedCause { +export interface Hypothesis { factor: string; projectedCpk?: number; } export interface ConclusionCardProps { - suspectedCauses: SuspectedCause[]; + suspectedCauses: Hypothesis[]; currentCpk?: number; combinedProjectedCpk?: number; targetCpk?: number; /** Hub-based display (new model) */ hubs?: SuspectedCauseHub[]; - hubEvidences?: Map; + hubEvidences?: Map; /** Model-based projections per hub (from computeHubProjection) */ hubProjections?: Map; onNavigateToInvestigation?: () => void; } const HUB_STATUS_DOT: Record = { - suspected: 'bg-amber-500', + proposed: 'bg-slate-400', + evidenced: 'bg-amber-500', confirmed: 'bg-green-500', - 'not-confirmed': 'bg-content-muted/40', + refuted: 'bg-content-muted/40', + 'needs-disconfirmation': 'bg-orange-500', }; /** diff --git a/packages/ui/src/components/ProcessIntelligencePanel/QuestionsTabView.tsx b/packages/ui/src/components/ProcessIntelligencePanel/QuestionsTabView.tsx index 9846950c0..bfa8415da 100644 --- a/packages/ui/src/components/ProcessIntelligencePanel/QuestionsTabView.tsx +++ b/packages/ui/src/components/ProcessIntelligencePanel/QuestionsTabView.tsx @@ -5,19 +5,19 @@ import type { CurrentUnderstanding } from '@variscout/core'; import type { QuestionStatus } from '@variscout/core/findings'; import type { BestSubsetResult } from '@variscout/core/stats'; import type { - SuspectedCause as SuspectedCauseHub, - SuspectedCauseEvidence, + Hypothesis as SuspectedCauseHub, + HypothesisEvidence, HubProjection, } from '@variscout/core'; import QuestionRow from './QuestionRow'; import ConclusionCard from './ConclusionCard'; -import type { SuspectedCause } from './ConclusionCard'; +import type { Hypothesis } from './ConclusionCard'; import EquationDisplay from './EquationDisplay'; import { QuestionInputModal } from './QuestionInputModal'; import { QuestionLinkModal } from './QuestionLinkModal'; -// Re-export SuspectedCause so consumers can import it from this module -export type { SuspectedCause }; +// Re-export Hypothesis so consumers can import it from this module +export type { Hypothesis }; export interface QuestionsTabViewProps { questions: Question[]; @@ -28,7 +28,7 @@ export interface QuestionsTabViewProps { targetCpk?: number; phaseBadge?: string; activeQuestionId?: string | null; - suspectedCauses?: SuspectedCause[]; + suspectedCauses?: Hypothesis[]; combinedProjectedCpk?: number; /** Record of question id → projected Cpk value (for expanded QuestionRow detail) */ projectedCpkMap?: Record; @@ -57,7 +57,7 @@ export interface QuestionsTabViewProps { /** Hub-based suspected causes (new model) — passed to ConclusionCard */ hubs?: SuspectedCauseHub[]; /** Hub evidence map — passed to ConclusionCard */ - hubEvidences?: Map; + hubEvidences?: Map; /** Hub model projections — passed to ConclusionCard */ hubProjections?: Map; /** Navigate to the Investigation workspace (passed to ConclusionCard) */ diff --git a/packages/ui/src/components/ProcessIntelligencePanel/__tests__/ConclusionCard.test.tsx b/packages/ui/src/components/ProcessIntelligencePanel/__tests__/ConclusionCard.test.tsx index 3ae61da50..1e3bb1f4b 100644 --- a/packages/ui/src/components/ProcessIntelligencePanel/__tests__/ConclusionCard.test.tsx +++ b/packages/ui/src/components/ProcessIntelligencePanel/__tests__/ConclusionCard.test.tsx @@ -1,17 +1,13 @@ import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import ConclusionCard from '../ConclusionCard'; -import type { SuspectedCause, HubProjection } from '@variscout/core'; +import type { Hypothesis, HubProjection } from '@variscout/core'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -function makeHub( - id: string, - name: string, - status: SuspectedCause['status'] = 'suspected' -): SuspectedCause { +function makeHub(id: string, name: string, status: Hypothesis['status'] = 'proposed'): Hypothesis { return { id, name, diff --git a/packages/ui/src/components/ProcessIntelligencePanel/__tests__/QuestionsTabView.test.tsx b/packages/ui/src/components/ProcessIntelligencePanel/__tests__/QuestionsTabView.test.tsx index 5c823156f..78ca7a265 100644 --- a/packages/ui/src/components/ProcessIntelligencePanel/__tests__/QuestionsTabView.test.tsx +++ b/packages/ui/src/components/ProcessIntelligencePanel/__tests__/QuestionsTabView.test.tsx @@ -2,7 +2,7 @@ import { describe, it, expect, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import QuestionsTabView from '../QuestionsTabView'; import type { Question, Finding } from '@variscout/core/findings'; -import type { SuspectedCause } from '../ConclusionCard'; +import type { Hypothesis } from '../ConclusionCard'; // --------------------------------------------------------------------------- // Helpers @@ -246,7 +246,7 @@ describe('QuestionsTabView — observations section', () => { describe('QuestionsTabView — conclusion card', () => { it('renders conclusion card when suspected causes exist', () => { - const causes: SuspectedCause[] = [{ factor: 'Operator', projectedCpk: 1.5 }]; + const causes: Hypothesis[] = [{ factor: 'Operator', projectedCpk: 1.5 }]; render( ); @@ -260,7 +260,7 @@ describe('QuestionsTabView — conclusion card', () => { }); it('shows combined projection footer when combinedProjectedCpk is provided', () => { - const causes: SuspectedCause[] = [{ factor: 'Material' }]; + const causes: Hypothesis[] = [{ factor: 'Material' }]; render( Date: Sat, 9 May 2026 10:43:41 +0300 Subject: [PATCH 02/10] =?UTF-8?q?refactor(core):=20rename=20suspectedCause?= =?UTF-8?q?Id=20=E2=86=92=20hypothesisId=20on=20CausalLink=20+=20FindingCo?= =?UTF-8?q?mment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/__tests__/findings.test.ts | 6 ++++++ packages/core/src/findings/factories.ts | 2 +- packages/core/src/findings/types.ts | 10 +++------- packages/data/src/samples/syringe-barrel-weight.ts | 6 +++--- packages/hooks/src/useCanvasInvestigationOverlays.ts | 2 +- packages/hooks/src/useEvidenceMapData.ts | 2 +- .../stores/src/__tests__/investigationStore.test.ts | 10 +++++----- packages/stores/src/__tests__/wallSelectors.test.ts | 2 +- packages/stores/src/investigationStore.ts | 8 +++----- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/core/src/__tests__/findings.test.ts b/packages/core/src/__tests__/findings.test.ts index c032d0cd2..292c6580f 100644 --- a/packages/core/src/__tests__/findings.test.ts +++ b/packages/core/src/__tests__/findings.test.ts @@ -139,6 +139,12 @@ describe('createFindingComment', () => { expect(c.parentKind).toBe('finding'); }); + it('creates a hypothesis comment with parent metadata', () => { + const c = createFindingComment('Checked mechanism notes', 'h-1', 'hypothesis'); + expect(c.parentId).toBe('h-1'); + expect(c.parentKind).toBe('hypothesis'); + }); + it('generates unique ids', () => { const c1 = createFindingComment('a', 'f-1', 'finding'); const c2 = createFindingComment('b', 'f-2', 'finding'); diff --git a/packages/core/src/findings/factories.ts b/packages/core/src/findings/factories.ts index e69e5cda9..60f508650 100644 --- a/packages/core/src/findings/factories.ts +++ b/packages/core/src/findings/factories.ts @@ -139,7 +139,7 @@ export function createCommentAttachment( export function createFindingComment( text: string, parentId: string, - parentKind: 'finding' | 'suspectedCause', + parentKind: 'finding' | 'hypothesis', author?: string ): FindingComment { const comment: FindingComment = { diff --git a/packages/core/src/findings/types.ts b/packages/core/src/findings/types.ts index aa750f1ed..f1b794da9 100644 --- a/packages/core/src/findings/types.ts +++ b/packages/core/src/findings/types.ts @@ -118,7 +118,7 @@ export interface FindingComment extends EntityBase { */ parentId: Finding['id'] | Hypothesis['id']; /** Discriminator for parentId — which entity type owns this comment. */ - parentKind: 'finding' | 'suspectedCause'; + parentKind: 'finding' | 'hypothesis'; /** Photo attachments (Team plan only) */ photos?: PhotoAttachment[]; /** Non-image file attachments (PDF, XLSX, CSV, TXT). Team plan: OneDrive upload. Standard: local reference. */ @@ -725,12 +725,8 @@ export interface CausalLink extends EntityBase { evidenceType: 'data' | 'gemba' | 'expert' | 'unvalidated'; questionIds: Question['id'][]; // Questions supporting this link findingIds: Finding['id'][]; // Findings supporting this link - /** - * The Hypothesis this link belongs to. - * Renamed from `hubId` (R5) — the old name was misleading; it references - * Hypothesis.id, not ProcessHub.id. - */ - suspectedCauseId?: Hypothesis['id']; + /** The Hypothesis this link belongs to. */ + hypothesisId?: Hypothesis['id']; strength?: number; // ΔR² or computed from R²adj comparison relationshipType?: 'independent' | 'overlapping' | 'synergistic' | 'interactive' | 'redundant'; source: 'analyst' | 'coscout' | 'auto'; diff --git a/packages/data/src/samples/syringe-barrel-weight.ts b/packages/data/src/samples/syringe-barrel-weight.ts index 68ca3f89f..acd32259f 100644 --- a/packages/data/src/samples/syringe-barrel-weight.ts +++ b/packages/data/src/samples/syringe-barrel-weight.ts @@ -619,7 +619,7 @@ function buildCausalLinks(): CausalLink[] { evidenceType: 'data', questionIds: [IDS.Q_PRESSURE], findingIds: [IDS.F_PRESSURE_SLOPE], - suspectedCauseId: IDS.HUB_LOT3_PRESSURE, + hypothesisId: IDS.HUB_LOT3_PRESSURE, strength: 0.31, source: 'analyst', createdAt: epochAt(20), @@ -636,7 +636,7 @@ function buildCausalLinks(): CausalLink[] { evidenceType: 'data', questionIds: [IDS.Q_LOT], findingIds: [IDS.F_LOT3_LIGHT], - suspectedCauseId: IDS.HUB_LOT3_PRESSURE, + hypothesisId: IDS.HUB_LOT3_PRESSURE, strength: 0.18, source: 'analyst', createdAt: epochAt(20), @@ -653,7 +653,7 @@ function buildCausalLinks(): CausalLink[] { evidenceType: 'data', questionIds: [IDS.Q_INTERACTION], findingIds: [IDS.F_INTERACTION], - suspectedCauseId: IDS.HUB_LOT3_PRESSURE, + hypothesisId: IDS.HUB_LOT3_PRESSURE, source: 'analyst', relationshipType: 'interactive', createdAt: epochAt(20), diff --git a/packages/hooks/src/useCanvasInvestigationOverlays.ts b/packages/hooks/src/useCanvasInvestigationOverlays.ts index 1a3e4cc97..c43ba3516 100644 --- a/packages/hooks/src/useCanvasInvestigationOverlays.ts +++ b/packages/hooks/src/useCanvasInvestigationOverlays.ts @@ -234,7 +234,7 @@ function isPromotedHub(hub: Hypothesis): boolean { } function hubOwnsLink(hub: Hypothesis, link: CausalLink): boolean { - if (link.suspectedCauseId && link.suspectedCauseId === hub.id) return true; + if (link.hypothesisId && link.hypothesisId === hub.id) return true; return ( link.questionIds.some(id => hub.questionIds.includes(id)) || link.findingIds.some(id => hub.findingIds.includes(id)) diff --git a/packages/hooks/src/useEvidenceMapData.ts b/packages/hooks/src/useEvidenceMapData.ts index c4c864f36..616fdbabe 100644 --- a/packages/hooks/src/useEvidenceMapData.ts +++ b/packages/hooks/src/useEvidenceMapData.ts @@ -195,7 +195,7 @@ function mapConvergencePoint( const incomingQuestionIds = new Set(cp.incomingLinks.flatMap(l => l.questionIds)); const incomingFindingIds = new Set(cp.incomingLinks.flatMap(l => l.findingIds)); const incomingHubIds = new Set( - cp.incomingLinks.map(l => l.suspectedCauseId).filter((id): id is string => id !== undefined) + cp.incomingLinks.map(l => l.hypothesisId).filter((id): id is string => id !== undefined) ); const matchingHub = suspectedCauses.find( diff --git a/packages/stores/src/__tests__/investigationStore.test.ts b/packages/stores/src/__tests__/investigationStore.test.ts index 7f4b18234..3d6cee52a 100644 --- a/packages/stores/src/__tests__/investigationStore.test.ts +++ b/packages/stores/src/__tests__/investigationStore.test.ts @@ -962,22 +962,22 @@ describe('investigationStore — causalLink cascade behavior', () => { expect(useInvestigationStore.getState().causalLinks[0].findingIds).not.toContain(f.id); }); - it('deleteHub clears hubId from causal links', () => { + it('deleteHub clears hypothesisId from causal links', () => { const hub = useInvestigationStore.getState().createHub('Test hub', 'Synthesis'); const link = useInvestigationStore.getState().addCausalLink('A', 'B', 'Test', { source: 'analyst', }); - // Manually set hubId via updateCausalLink — the store doesn't have a direct setHubId action, + // Manually set hypothesisId via updateCausalLink — the store doesn't have a direct setter, // so we use loadInvestigationState to set it useInvestigationStore.setState(state => ({ causalLinks: state.causalLinks.map(l => - l.id === link!.id ? { ...l, suspectedCauseId: hub.id } : l + l.id === link!.id ? { ...l, hypothesisId: hub.id } : l ), })); - expect(useInvestigationStore.getState().causalLinks[0].suspectedCauseId).toBe(hub.id); + expect(useInvestigationStore.getState().causalLinks[0].hypothesisId).toBe(hub.id); useInvestigationStore.getState().deleteHub(hub.id); - expect(useInvestigationStore.getState().causalLinks[0].suspectedCauseId).toBeUndefined(); + expect(useInvestigationStore.getState().causalLinks[0].hypothesisId).toBeUndefined(); }); }); diff --git a/packages/stores/src/__tests__/wallSelectors.test.ts b/packages/stores/src/__tests__/wallSelectors.test.ts index 33da2ace0..4d4ab1300 100644 --- a/packages/stores/src/__tests__/wallSelectors.test.ts +++ b/packages/stores/src/__tests__/wallSelectors.test.ts @@ -15,7 +15,7 @@ function fc(id: string, text: string, createdAt: number): FindingComment { createdAt, deletedAt: null, parentId: 'hub-default', - parentKind: 'suspectedCause', + parentKind: 'hypothesis', }; } diff --git a/packages/stores/src/investigationStore.ts b/packages/stores/src/investigationStore.ts index bbb3711ac..10356f4ed 100644 --- a/packages/stores/src/investigationStore.ts +++ b/packages/stores/src/investigationStore.ts @@ -864,11 +864,9 @@ export const useInvestigationStore = create { set(state => ({ suspectedCauses: state.suspectedCauses.filter(h => h.id !== hubId), - // Clear hubId from causal links that reference the deleted hub + // Clear hypothesisId from causal links that reference the deleted hub causalLinks: state.causalLinks.map(l => - l.suspectedCauseId === hubId - ? { ...l, suspectedCauseId: undefined, updatedAt: Date.now() } - : l + l.hypothesisId === hubId ? { ...l, hypothesisId: undefined, updatedAt: Date.now() } : l ), })); }, @@ -953,7 +951,7 @@ export const useInvestigationStore = create ({ From 8c5132b6317b656894e35ba14f7d71408da0b18d Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Sat, 9 May 2026 11:02:09 +0300 Subject: [PATCH 03/10] =?UTF-8?q?refactor(core):=20rename=20SUSPECTED=5FCA?= =?UTF-8?q?USE=5F*=20HubAction=20kinds=20=E2=86=92=20HYPOTHESIS=5F*?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/persistence/__tests__/applyAction.test.ts | 12 ++++++------ apps/azure/src/persistence/applyAction.ts | 14 +++++++------- .../src/persistence/__tests__/applyAction.test.ts | 10 +++++----- apps/pwa/src/persistence/applyAction.ts | 10 +++++----- packages/core/src/actions/HubAction.ts | 4 ++-- .../src/actions/__tests__/exhaustiveness.test.ts | 8 ++++---- ...spectedCauseActions.ts => hypothesisActions.ts} | 12 ++++++------ packages/core/src/actions/index.ts | 2 +- packages/core/src/index.ts | 2 +- 9 files changed, 37 insertions(+), 37 deletions(-) rename packages/core/src/actions/{suspectedCauseActions.ts => hypothesisActions.ts} (50%) diff --git a/apps/azure/src/persistence/__tests__/applyAction.test.ts b/apps/azure/src/persistence/__tests__/applyAction.test.ts index a713eef12..258fd0d25 100644 --- a/apps/azure/src/persistence/__tests__/applyAction.test.ts +++ b/apps/azure/src/persistence/__tests__/applyAction.test.ts @@ -573,9 +573,9 @@ describe('applyAction — session-only no-ops', () => { ).resolves.toBeUndefined(); }); - it('SUSPECTED_CAUSE_ARCHIVE resolves cleanly', async () => { + it('HYPOTHESIS_ARCHIVE resolves cleanly', async () => { await expect( - applyAction({ kind: 'SUSPECTED_CAUSE_ARCHIVE', causeId: 'cause-1' }) + applyAction({ kind: 'HYPOTHESIS_ARCHIVE', hypothesisId: 'cause-1' }) ).resolves.toBeUndefined(); }); }); @@ -715,12 +715,12 @@ describe('exhaustiveness — every HubAction kind has a handler', () => { { kind: 'CAUSAL_LINK_UPDATE', linkId: 'link-1', patch: {} } as HubAction, { kind: 'CAUSAL_LINK_ARCHIVE', linkId: 'link-1' }, { - kind: 'SUSPECTED_CAUSE_ADD', + kind: 'HYPOTHESIS_ADD', investigationId: 'inv-x', - cause: { id: 'cause-1' }, + hypothesis: { id: 'cause-1' }, } as HubAction, - { kind: 'SUSPECTED_CAUSE_UPDATE', causeId: 'cause-1', patch: {} } as HubAction, - { kind: 'SUSPECTED_CAUSE_ARCHIVE', causeId: 'cause-1' }, + { kind: 'HYPOTHESIS_UPDATE', hypothesisId: 'cause-1', patch: {} } as HubAction, + { kind: 'HYPOTHESIS_ARCHIVE', hypothesisId: 'cause-1' }, // Canvas no-ops. { kind: 'PLACE_CHIP_ON_STEP', chipId: 'chip-1', stepId: 'step-1' }, { kind: 'UNASSIGN_CHIP', chipId: 'chip-1' }, diff --git a/apps/azure/src/persistence/applyAction.ts b/apps/azure/src/persistence/applyAction.ts index ebd83f3de..f6a622cb1 100644 --- a/apps/azure/src/persistence/applyAction.ts +++ b/apps/azure/src/persistence/applyAction.ts @@ -17,7 +17,7 @@ // EVIDENCE_SOURCE_ADD, EVIDENCE_SOURCE_UPDATE_CURSOR, EVIDENCE_SOURCE_REMOVE // // Session-only / no Azure Dexie table today (F3 normalizes): -// INVESTIGATION_*, FINDING_*, QUESTION_*, CAUSAL_LINK_*, SUSPECTED_CAUSE_* +// INVESTIGATION_*, FINDING_*, QUESTION_*, CAUSAL_LINK_*, HYPOTHESIS_* // // Canvas mutations (flow through canvasStore → HUB_PERSIST_SNAPSHOT): // PLACE_CHIP_ON_STEP, UNASSIGN_CHIP, REORDER_CHIP_IN_STEP, ADD_STEP, @@ -275,16 +275,16 @@ export async function applyAction(action: HubAction): Promise { // Azure has no 'causalLink' table today; F3 normalizes — no-op. return; - case 'SUSPECTED_CAUSE_ADD': - // Azure has no 'suspectedCause' table today; F3 normalizes — no-op. + case 'HYPOTHESIS_ADD': + // Azure has no 'hypothesis' table today; F3 normalizes — no-op. return; - case 'SUSPECTED_CAUSE_UPDATE': - // Azure has no 'suspectedCause' table today; F3 normalizes — no-op. + case 'HYPOTHESIS_UPDATE': + // Azure has no 'hypothesis' table today; F3 normalizes — no-op. return; - case 'SUSPECTED_CAUSE_ARCHIVE': - // Azure has no 'suspectedCause' table today; F3 normalizes — no-op. + case 'HYPOTHESIS_ARCHIVE': + // Azure has no 'hypothesis' table today; F3 normalizes — no-op. return; // ------------------------------------------------------------------------- diff --git a/apps/pwa/src/persistence/__tests__/applyAction.test.ts b/apps/pwa/src/persistence/__tests__/applyAction.test.ts index 867045f6e..d941d5275 100644 --- a/apps/pwa/src/persistence/__tests__/applyAction.test.ts +++ b/apps/pwa/src/persistence/__tests__/applyAction.test.ts @@ -469,11 +469,11 @@ describe('applyAction — no-op action kinds', () => { expect(await db.causalLinks.count()).toBe(0); }); - it('SUSPECTED_CAUSE_ADD does not mutate any table', async () => { + it('HYPOTHESIS_ADD does not mutate any table', async () => { await applyAction(db, { - kind: 'SUSPECTED_CAUSE_ADD', + kind: 'HYPOTHESIS_ADD', investigationId: 'inv-x', - cause: { id: 'sc-x' }, + hypothesis: { id: 'sc-x' }, } as unknown as HubAction); expect(await db.suspectedCauses.count()).toBe(0); @@ -501,8 +501,8 @@ describe('applyAction — no-op action kinds', () => { 'QUESTION_ARCHIVE', 'CAUSAL_LINK_UPDATE', 'CAUSAL_LINK_ARCHIVE', - 'SUSPECTED_CAUSE_UPDATE', - 'SUSPECTED_CAUSE_ARCHIVE', + 'HYPOTHESIS_UPDATE', + 'HYPOTHESIS_ARCHIVE', 'PLACE_CHIP_ON_STEP', 'UNASSIGN_CHIP', 'REORDER_CHIP_IN_STEP', diff --git a/apps/pwa/src/persistence/applyAction.ts b/apps/pwa/src/persistence/applyAction.ts index c0cecbdf4..32538ec9e 100644 --- a/apps/pwa/src/persistence/applyAction.ts +++ b/apps/pwa/src/persistence/applyAction.ts @@ -25,7 +25,7 @@ // Out of scope (no-op with comments): // - EVIDENCE_SOURCE_ADD / EVIDENCE_SOURCE_REMOVE — pending future use. // - INVESTIGATION_* / FINDING_* / QUESTION_* / CAUSAL_LINK_* / -// SUSPECTED_CAUSE_* — F5 wires this when investigation entity action +// HYPOTHESIS_* — F5 wires this when investigation entity action // coverage lands. // - CANVAS_* — canvasStore remains the canonical mutation surface; // canonicalProcessMap reaches the canvasState table only via @@ -172,7 +172,7 @@ export async function applyAction(db: PwaDatabase, action: HubAction): Promise; } - | { kind: 'SUSPECTED_CAUSE_ARCHIVE'; causeId: Hypothesis['id'] }; + | { kind: 'HYPOTHESIS_ARCHIVE'; hypothesisId: Hypothesis['id'] }; diff --git a/packages/core/src/actions/index.ts b/packages/core/src/actions/index.ts index 378bbce93..1222cdc6e 100644 --- a/packages/core/src/actions/index.ts +++ b/packages/core/src/actions/index.ts @@ -5,7 +5,7 @@ export type { InvestigationAction } from './investigationActions'; export type { FindingAction } from './findingActions'; export type { QuestionAction } from './questionActions'; export type { CausalLinkAction } from './causalLinkActions'; -export type { SuspectedCauseAction } from './suspectedCauseActions'; +export type { HypothesisAction } from './hypothesisActions'; export type { HubMetaAction } from './hubMetaActions'; export type { CanvasAction } from './canvasActions'; export type { HubAction } from './HubAction'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index cc040ebc4..9c597ff2f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -974,7 +974,7 @@ export type { FindingAction, QuestionAction, CausalLinkAction, - SuspectedCauseAction, + HypothesisAction, HubMetaAction, CanvasAction, } from './actions'; From 429d21634ab0cbdb07299a2a5fa2b9acd6bf27d6 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Sat, 9 May 2026 11:20:54 +0300 Subject: [PATCH 04/10] =?UTF-8?q?refactor:=20move=20InvestigationWall=20pa?= =?UTF-8?q?ckages/charts=20=E2=86=92=20packages/ui=20(per=20RPS=20V1=20D17?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editor/InvestigationWorkspace.tsx | 16 ++++---- .../InvestigationWorkspace.mapwall.test.tsx | 13 ++++--- .../components/views/InvestigationView.tsx | 16 ++++---- .../InvestigationView.mapwall.test.tsx | 17 ++++----- packages/charts/src/index.ts | 3 -- .../Canvas/__tests__/Canvas.test.tsx | 6 +++ .../Canvas/__tests__/CanvasWorkspace.test.tsx | 6 +++ packages/ui/src/components/Canvas/index.tsx | 3 +- .../Canvas/internal/CanvasWallOverlay.tsx | 2 +- .../__tests__/CanvasWallOverlay.test.tsx | 4 +- .../InvestigationWall/CommandPalette.tsx | 0 .../DraggableHypothesisCard.tsx | 0 .../InvestigationWall/DroppableGateBadge.tsx | 0 .../InvestigationWall/EmptyState.tsx | 0 .../InvestigationWall/FindingChip.tsx | 0 .../InvestigationWall/GateBadge.tsx | 2 +- .../InvestigationWall/HypothesisCard.tsx | 2 +- .../components}/InvestigationWall/Minimap.tsx | 2 +- .../MissingEvidenceDigest.tsx | 0 .../InvestigationWall/MobileCardList.tsx | 2 +- .../InvestigationWall/NarratorRail.tsx | 0 .../ProblemConditionCard.tsx | 2 +- .../InvestigationWall/QuestionPill.tsx | 0 .../InvestigationWall/TributaryFooter.tsx | 0 .../InvestigationWall/WallCanvas.tsx | 0 .../__tests__/CommandPalette.test.tsx | 38 ++++++++++++------- .../DraggableHypothesisCard.test.tsx | 6 ++- .../__tests__/DroppableGateBadge.test.tsx | 0 .../__tests__/EmptyState.test.tsx | 0 .../__tests__/FindingChip.test.tsx | 2 + .../__tests__/GateBadge.test.tsx | 0 .../__tests__/HypothesisCard.test.tsx | 18 +++++++-- .../__tests__/Minimap.test.tsx | 18 ++++++--- .../__tests__/MissingEvidenceDigest.test.tsx | 0 .../__tests__/MobileCardList.test.tsx | 16 ++++++-- .../__tests__/NarratorRail.test.tsx | 0 .../__tests__/ProblemConditionCard.test.tsx | 0 .../__tests__/QuestionPill.test.tsx | 0 .../__tests__/TributaryFooter.test.tsx | 6 ++- .../__tests__/WallCanvas.test.tsx | 16 +++++--- .../__tests__/public-export.test.ts | 25 ++++++++++++ .../__tests__/useWallBreakpoint.test.tsx | 0 .../__tests__/useWallDragDrop.test.tsx | 0 .../__tests__/useWallKeyboard.test.tsx | 0 .../hooks/useWallBreakpoint.ts | 10 ++--- .../hooks/useWallDragDrop.ts | 0 .../hooks/useWallKeyboard.ts | 0 .../InvestigationWall/hooks/useWallLocale.ts | 0 .../components}/InvestigationWall/index.ts | 0 .../components}/InvestigationWall/types.ts | 0 packages/ui/src/index.ts | 2 + 51 files changed, 164 insertions(+), 89 deletions(-) rename packages/{charts/src => ui/src/components}/InvestigationWall/CommandPalette.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/DraggableHypothesisCard.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/DroppableGateBadge.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/EmptyState.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/FindingChip.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/GateBadge.tsx (98%) rename packages/{charts/src => ui/src/components}/InvestigationWall/HypothesisCard.tsx (99%) rename packages/{charts/src => ui/src/components}/InvestigationWall/Minimap.tsx (98%) rename packages/{charts/src => ui/src/components}/InvestigationWall/MissingEvidenceDigest.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/MobileCardList.tsx (99%) rename packages/{charts/src => ui/src/components}/InvestigationWall/NarratorRail.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/ProblemConditionCard.tsx (97%) rename packages/{charts/src => ui/src/components}/InvestigationWall/QuestionPill.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/TributaryFooter.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/WallCanvas.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/CommandPalette.test.tsx (88%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/DraggableHypothesisCard.test.tsx (93%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/DroppableGateBadge.test.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/EmptyState.test.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/FindingChip.test.tsx (96%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/GateBadge.test.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/HypothesisCard.test.tsx (95%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/Minimap.test.tsx (92%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/MissingEvidenceDigest.test.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/MobileCardList.test.tsx (94%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/NarratorRail.test.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/ProblemConditionCard.test.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/QuestionPill.test.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/TributaryFooter.test.tsx (94%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/WallCanvas.test.tsx (98%) create mode 100644 packages/ui/src/components/InvestigationWall/__tests__/public-export.test.ts rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/useWallBreakpoint.test.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/useWallDragDrop.test.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/__tests__/useWallKeyboard.test.tsx (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/hooks/useWallBreakpoint.ts (80%) rename packages/{charts/src => ui/src/components}/InvestigationWall/hooks/useWallDragDrop.ts (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/hooks/useWallKeyboard.ts (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/hooks/useWallLocale.ts (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/index.ts (100%) rename packages/{charts/src => ui/src/components}/InvestigationWall/types.ts (100%) diff --git a/apps/azure/src/components/editor/InvestigationWorkspace.tsx b/apps/azure/src/components/editor/InvestigationWorkspace.tsx index ab2ffc40f..70c0bb9e9 100644 --- a/apps/azure/src/components/editor/InvestigationWorkspace.tsx +++ b/apps/azure/src/components/editor/InvestigationWorkspace.tsx @@ -5,6 +5,13 @@ import { InvestigationConclusion, FindingsLog, QuestionLinkPrompt, + WallCanvas, + CommandPalette, + Minimap, + CANVAS_W, + CANVAS_H, + useWallKeyboard, + useWallIsMobile, type HubComposerBranchFields, } from '@variscout/ui'; import { @@ -45,15 +52,6 @@ import { usePreferencesStore, useWallLayoutStore, } from '@variscout/stores'; -import { - WallCanvas, - CommandPalette, - Minimap, - CANVAS_W, - CANVAS_H, - useWallKeyboard, - useWallIsMobile, -} from '@variscout/charts'; import { InvestigationMapView } from './InvestigationMapView'; import { CoScoutSection } from './CoScoutSection'; import { isSpeechToTextAvailable, transcribeAudio } from '../../services/speechService'; diff --git a/apps/azure/src/components/editor/__tests__/InvestigationWorkspace.mapwall.test.tsx b/apps/azure/src/components/editor/__tests__/InvestigationWorkspace.mapwall.test.tsx index b868f4647..dcfc889b4 100644 --- a/apps/azure/src/components/editor/__tests__/InvestigationWorkspace.mapwall.test.tsx +++ b/apps/azure/src/components/editor/__tests__/InvestigationWorkspace.mapwall.test.tsx @@ -23,12 +23,6 @@ vi.mock('@variscout/charts', async importOriginal => { return { ...actual, EvidenceMapBase: () =>
, - WallCanvas: (props: { hubs: unknown[] }) => - props.hubs.length > 0 ? ( -
- ) : ( -
- ), }; }); @@ -82,6 +76,13 @@ vi.mock('@variscout/ui', async importOriginal => { InvestigationConclusion: () => null, FindingsLog: () =>
, QuestionLinkPrompt: () => null, + useWallIsMobile: () => false, + WallCanvas: (props: { hubs: unknown[] }) => + props.hubs.length > 0 ? ( +
+ ) : ( +
+ ), }; }); diff --git a/apps/pwa/src/components/views/InvestigationView.tsx b/apps/pwa/src/components/views/InvestigationView.tsx index a34852f9d..24d5d0fcf 100644 --- a/apps/pwa/src/components/views/InvestigationView.tsx +++ b/apps/pwa/src/components/views/InvestigationView.tsx @@ -14,6 +14,13 @@ import { InvestigationPhaseBadge, InvestigationConclusion, FindingsLog, + WallCanvas, + CommandPalette, + Minimap, + CANVAS_W, + CANVAS_H, + useWallKeyboard, + useWallIsMobile, } from '@variscout/ui'; import { useResizablePanel, @@ -27,15 +34,6 @@ import type { ResolvedMode } from '@variscout/core/strategy'; import type { DrillStep } from '@variscout/hooks'; import { GripVertical } from 'lucide-react'; import { useWallLayoutStore, useProjectStore, useInvestigationStore } from '@variscout/stores'; -import { - WallCanvas, - CommandPalette, - Minimap, - CANVAS_W, - CANVAS_H, - useWallKeyboard, - useWallIsMobile, -} from '@variscout/charts'; import { useFindingsStore } from '../../features/findings/findingsStore'; import { useInvestigationFeatureStore, diff --git a/apps/pwa/src/components/views/__tests__/InvestigationView.mapwall.test.tsx b/apps/pwa/src/components/views/__tests__/InvestigationView.mapwall.test.tsx index 39df36cd4..a3d281d3c 100644 --- a/apps/pwa/src/components/views/__tests__/InvestigationView.mapwall.test.tsx +++ b/apps/pwa/src/components/views/__tests__/InvestigationView.mapwall.test.tsx @@ -20,15 +20,7 @@ import { render, screen, fireEvent } from '@testing-library/react'; vi.mock('@variscout/charts', async importOriginal => { const actual = await importOriginal(); - return { - ...actual, - WallCanvas: (props: { hubs: unknown[] }) => - props.hubs.length > 0 ? ( -
- ) : ( -
- ), - }; + return actual; }); vi.mock('@variscout/hooks', async importOriginal => { @@ -50,6 +42,13 @@ vi.mock('@variscout/ui', async importOriginal => { InvestigationPhaseBadge: () => null, InvestigationConclusion: () => null, FindingsLog: () =>
, + useWallIsMobile: () => false, + WallCanvas: (props: { hubs: unknown[] }) => + props.hubs.length > 0 ? ( +
+ ) : ( +
+ ), }; }); diff --git a/packages/charts/src/index.ts b/packages/charts/src/index.ts index a55078880..394201921 100644 --- a/packages/charts/src/index.ts +++ b/packages/charts/src/index.ts @@ -130,9 +130,6 @@ export type { RelationshipType, } from './EvidenceMap'; -// Investigation Wall components -export * from './InvestigationWall'; - // ScatterFit chart component export { default as ScatterFit, ScatterFitBase } from './ScatterFit'; diff --git a/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx b/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx index 1656ba17d..58957eb86 100644 --- a/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx +++ b/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx @@ -15,6 +15,12 @@ vi.mock('@variscout/charts', async () => { CapabilityBoxplot: () => React.createElement('div', { 'data-testid': 'mock-capability-boxplot' }), StepErrorPareto: () => React.createElement('div', { 'data-testid': 'mock-step-pareto' }), + }; +}); + +vi.mock('../../InvestigationWall', async () => { + const React = await import('react'); + return { useWallIsMobile: () => wallIsMobileRef.current, WallCanvas: ({ findings, diff --git a/packages/ui/src/components/Canvas/__tests__/CanvasWorkspace.test.tsx b/packages/ui/src/components/Canvas/__tests__/CanvasWorkspace.test.tsx index 6fa863551..37eb79f9f 100644 --- a/packages/ui/src/components/Canvas/__tests__/CanvasWorkspace.test.tsx +++ b/packages/ui/src/components/Canvas/__tests__/CanvasWorkspace.test.tsx @@ -30,6 +30,12 @@ vi.mock('@variscout/charts', async importOriginal => { CapabilityBoxplot: () => React.createElement('div', { 'data-testid': 'mock-capability-boxplot' }), StepErrorPareto: () => React.createElement('div', { 'data-testid': 'mock-step-pareto' }), + }; +}); + +vi.mock('../../InvestigationWall', async () => { + const React = await import('react'); + return { useWallIsMobile: () => wallIsMobileRef.current, WallCanvas: ({ findings, diff --git a/packages/ui/src/components/Canvas/index.tsx b/packages/ui/src/components/Canvas/index.tsx index 9bc8eec80..172db7c18 100644 --- a/packages/ui/src/components/Canvas/index.tsx +++ b/packages/ui/src/components/Canvas/index.tsx @@ -4,7 +4,7 @@ */ import React from 'react'; import { DndContext } from '@dnd-kit/core'; -import { chartColors, useWallIsMobile } from '@variscout/charts'; +import { chartColors } from '@variscout/charts'; import { coerceCanvasLens, coerceCanvasOverlays, @@ -44,6 +44,7 @@ import { CanvasStepCard } from './internal/CanvasStepCard'; import { CanvasStepOverlay, type CanvasOverlayAnchorRect } from './internal/CanvasStepOverlay'; import { CanvasWallOverlay } from './internal/CanvasWallOverlay'; import { WallShortcutButton } from './internal/WallShortcutButton'; +import { useWallIsMobile } from '../InvestigationWall'; /** * Canonical FRAME canvas surface. diff --git a/packages/ui/src/components/Canvas/internal/CanvasWallOverlay.tsx b/packages/ui/src/components/Canvas/internal/CanvasWallOverlay.tsx index 483050a2c..20032c3eb 100644 --- a/packages/ui/src/components/Canvas/internal/CanvasWallOverlay.tsx +++ b/packages/ui/src/components/Canvas/internal/CanvasWallOverlay.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useRef } from 'react'; -import { WallCanvas, useWallIsMobile } from '@variscout/charts'; import { useHasInvestigationContent, useSharedWallProps, @@ -8,6 +7,7 @@ import { } from '@variscout/hooks'; import { useWallLayoutStore } from '@variscout/stores'; import type { Finding, ProcessMap } from '@variscout/core'; +import { WallCanvas, useWallIsMobile } from '../../InvestigationWall'; export interface CanvasWallOverlayProps { activeOverlays: CanvasOverlayId[]; diff --git a/packages/ui/src/components/Canvas/internal/__tests__/CanvasWallOverlay.test.tsx b/packages/ui/src/components/Canvas/internal/__tests__/CanvasWallOverlay.test.tsx index 87bbfea67..e77fd1bd9 100644 --- a/packages/ui/src/components/Canvas/internal/__tests__/CanvasWallOverlay.test.tsx +++ b/packages/ui/src/components/Canvas/internal/__tests__/CanvasWallOverlay.test.tsx @@ -7,10 +7,10 @@ import { useInvestigationStore, useWallLayoutStore, } from '@variscout/stores'; -import { useWallIsMobile } from '@variscout/charts'; +import { useWallIsMobile } from '../../../InvestigationWall'; import { CanvasWallOverlay } from '../CanvasWallOverlay'; -vi.mock('@variscout/charts', () => ({ +vi.mock('../../../InvestigationWall', () => ({ useWallIsMobile: vi.fn(), WallCanvas: (props: { mode?: string; diff --git a/packages/charts/src/InvestigationWall/CommandPalette.tsx b/packages/ui/src/components/InvestigationWall/CommandPalette.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/CommandPalette.tsx rename to packages/ui/src/components/InvestigationWall/CommandPalette.tsx diff --git a/packages/charts/src/InvestigationWall/DraggableHypothesisCard.tsx b/packages/ui/src/components/InvestigationWall/DraggableHypothesisCard.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/DraggableHypothesisCard.tsx rename to packages/ui/src/components/InvestigationWall/DraggableHypothesisCard.tsx diff --git a/packages/charts/src/InvestigationWall/DroppableGateBadge.tsx b/packages/ui/src/components/InvestigationWall/DroppableGateBadge.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/DroppableGateBadge.tsx rename to packages/ui/src/components/InvestigationWall/DroppableGateBadge.tsx diff --git a/packages/charts/src/InvestigationWall/EmptyState.tsx b/packages/ui/src/components/InvestigationWall/EmptyState.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/EmptyState.tsx rename to packages/ui/src/components/InvestigationWall/EmptyState.tsx diff --git a/packages/charts/src/InvestigationWall/FindingChip.tsx b/packages/ui/src/components/InvestigationWall/FindingChip.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/FindingChip.tsx rename to packages/ui/src/components/InvestigationWall/FindingChip.tsx diff --git a/packages/charts/src/InvestigationWall/GateBadge.tsx b/packages/ui/src/components/InvestigationWall/GateBadge.tsx similarity index 98% rename from packages/charts/src/InvestigationWall/GateBadge.tsx rename to packages/ui/src/components/InvestigationWall/GateBadge.tsx index fa33b3f67..2af117edc 100644 --- a/packages/charts/src/InvestigationWall/GateBadge.tsx +++ b/packages/ui/src/components/InvestigationWall/GateBadge.tsx @@ -11,7 +11,7 @@ import React from 'react'; import type { MessageCatalog } from '@variscout/core'; import { formatMessage, getMessage } from '@variscout/core/i18n'; -import { chartColors } from '../colors'; +import { chartColors } from '@variscout/charts'; import { useWallLocale } from './hooks/useWallLocale'; export interface GateBadgeProps { diff --git a/packages/charts/src/InvestigationWall/HypothesisCard.tsx b/packages/ui/src/components/InvestigationWall/HypothesisCard.tsx similarity index 99% rename from packages/charts/src/InvestigationWall/HypothesisCard.tsx rename to packages/ui/src/components/InvestigationWall/HypothesisCard.tsx index 59b81684d..80c64c868 100644 --- a/packages/charts/src/InvestigationWall/HypothesisCard.tsx +++ b/packages/ui/src/components/InvestigationWall/HypothesisCard.tsx @@ -12,7 +12,7 @@ import React from 'react'; import type { MechanismBranchViewModel, MessageCatalog, Hypothesis } from '@variscout/core'; import { formatMessage, getMessage } from '@variscout/core/i18n'; -import { chartColors } from '../colors'; +import { chartColors } from '@variscout/charts'; import type { WallStatus } from './types'; import { useWallLocale } from './hooks/useWallLocale'; diff --git a/packages/charts/src/InvestigationWall/Minimap.tsx b/packages/ui/src/components/InvestigationWall/Minimap.tsx similarity index 98% rename from packages/charts/src/InvestigationWall/Minimap.tsx rename to packages/ui/src/components/InvestigationWall/Minimap.tsx index fb30f88ee..6d3e0e965 100644 --- a/packages/charts/src/InvestigationWall/Minimap.tsx +++ b/packages/ui/src/components/InvestigationWall/Minimap.tsx @@ -16,7 +16,7 @@ import React from 'react'; import type { Hypothesis, Question } from '@variscout/core'; import { getMessage } from '@variscout/core/i18n'; -import { chartColors } from '../colors'; +import { chartColors } from '@variscout/charts'; import { CANVAS_W, CANVAS_H } from './WallCanvas'; import { useWallLocale } from './hooks/useWallLocale'; diff --git a/packages/charts/src/InvestigationWall/MissingEvidenceDigest.tsx b/packages/ui/src/components/InvestigationWall/MissingEvidenceDigest.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/MissingEvidenceDigest.tsx rename to packages/ui/src/components/InvestigationWall/MissingEvidenceDigest.tsx diff --git a/packages/charts/src/InvestigationWall/MobileCardList.tsx b/packages/ui/src/components/InvestigationWall/MobileCardList.tsx similarity index 99% rename from packages/charts/src/InvestigationWall/MobileCardList.tsx rename to packages/ui/src/components/InvestigationWall/MobileCardList.tsx index d025f4d0d..b1069f61d 100644 --- a/packages/charts/src/InvestigationWall/MobileCardList.tsx +++ b/packages/ui/src/components/InvestigationWall/MobileCardList.tsx @@ -21,7 +21,7 @@ import { type Question, } from '@variscout/core'; import { formatMessage, getMessage } from '@variscout/core/i18n'; -import { chartColors } from '../colors'; +import { chartColors } from '@variscout/charts'; import { EmptyState } from './EmptyState'; import type { WallStatus } from './types'; import { useWallLocale } from './hooks/useWallLocale'; diff --git a/packages/charts/src/InvestigationWall/NarratorRail.tsx b/packages/ui/src/components/InvestigationWall/NarratorRail.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/NarratorRail.tsx rename to packages/ui/src/components/InvestigationWall/NarratorRail.tsx diff --git a/packages/charts/src/InvestigationWall/ProblemConditionCard.tsx b/packages/ui/src/components/InvestigationWall/ProblemConditionCard.tsx similarity index 97% rename from packages/charts/src/InvestigationWall/ProblemConditionCard.tsx rename to packages/ui/src/components/InvestigationWall/ProblemConditionCard.tsx index 4c176e99f..52dcd76f0 100644 --- a/packages/charts/src/InvestigationWall/ProblemConditionCard.tsx +++ b/packages/ui/src/components/InvestigationWall/ProblemConditionCard.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { formatStatistic, formatMessage, getMessage } from '@variscout/core/i18n'; -import { chartColors } from '../colors'; +import { chartColors } from '@variscout/charts'; import { useWallLocale } from './hooks/useWallLocale'; export interface ProblemConditionCardProps { diff --git a/packages/charts/src/InvestigationWall/QuestionPill.tsx b/packages/ui/src/components/InvestigationWall/QuestionPill.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/QuestionPill.tsx rename to packages/ui/src/components/InvestigationWall/QuestionPill.tsx diff --git a/packages/charts/src/InvestigationWall/TributaryFooter.tsx b/packages/ui/src/components/InvestigationWall/TributaryFooter.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/TributaryFooter.tsx rename to packages/ui/src/components/InvestigationWall/TributaryFooter.tsx diff --git a/packages/charts/src/InvestigationWall/WallCanvas.tsx b/packages/ui/src/components/InvestigationWall/WallCanvas.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/WallCanvas.tsx rename to packages/ui/src/components/InvestigationWall/WallCanvas.tsx diff --git a/packages/charts/src/InvestigationWall/__tests__/CommandPalette.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/CommandPalette.test.tsx similarity index 88% rename from packages/charts/src/InvestigationWall/__tests__/CommandPalette.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/CommandPalette.test.tsx index bf5173897..510e8909c 100644 --- a/packages/charts/src/InvestigationWall/__tests__/CommandPalette.test.tsx +++ b/packages/ui/src/components/InvestigationWall/__tests__/CommandPalette.test.tsx @@ -18,8 +18,10 @@ const hubs: Hypothesis[] = [ questionIds: [], findingIds: [], status: 'proposed', - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', }, { id: 'h-nozzle', @@ -28,8 +30,10 @@ const hubs: Hypothesis[] = [ questionIds: [], findingIds: [], status: 'proposed', - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', }, { id: 'h-cal', @@ -38,8 +42,10 @@ const hubs: Hypothesis[] = [ questionIds: [], findingIds: [], status: 'proposed', - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', }, ]; @@ -49,28 +55,32 @@ const questions: Question[] = [ text: 'What does the night shift do differently?', status: 'open', linkedFindingIds: [], - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', }, { id: 'q-2', text: 'Is Cpk trending?', status: 'open', linkedFindingIds: [], - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', }, ]; const findings: Finding[] = []; describe('CommandPalette', () => { - let onClose: ReturnType; - let onPanTo: ReturnType; + let onClose: ReturnType void>>; + let onPanTo: ReturnType void>>; beforeEach(() => { - onClose = vi.fn(); - onPanTo = vi.fn(); + onClose = vi.fn<() => void>(); + onPanTo = vi.fn<(nodeId: string) => void>(); }); it('renders nothing when open=false', () => { diff --git a/packages/charts/src/InvestigationWall/__tests__/DraggableHypothesisCard.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/DraggableHypothesisCard.test.tsx similarity index 93% rename from packages/charts/src/InvestigationWall/__tests__/DraggableHypothesisCard.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/DraggableHypothesisCard.test.tsx index 21b13916e..66621a1a2 100644 --- a/packages/charts/src/InvestigationWall/__tests__/DraggableHypothesisCard.test.tsx +++ b/packages/ui/src/components/InvestigationWall/__tests__/DraggableHypothesisCard.test.tsx @@ -11,8 +11,10 @@ const hub: Hypothesis = { questionIds: [], findingIds: [], status: 'proposed', - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', }; describe('DraggableHypothesisCard', () => { diff --git a/packages/charts/src/InvestigationWall/__tests__/DroppableGateBadge.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/DroppableGateBadge.test.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/__tests__/DroppableGateBadge.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/DroppableGateBadge.test.tsx diff --git a/packages/charts/src/InvestigationWall/__tests__/EmptyState.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/EmptyState.test.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/__tests__/EmptyState.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/EmptyState.test.tsx diff --git a/packages/charts/src/InvestigationWall/__tests__/FindingChip.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/FindingChip.test.tsx similarity index 96% rename from packages/charts/src/InvestigationWall/__tests__/FindingChip.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/FindingChip.test.tsx index 911b271a1..4bdf6caa1 100644 --- a/packages/charts/src/InvestigationWall/__tests__/FindingChip.test.tsx +++ b/packages/ui/src/components/InvestigationWall/__tests__/FindingChip.test.tsx @@ -8,6 +8,8 @@ const finding: Finding = { id: 'f1', text: 'Night-shift spike', createdAt: 0, + deletedAt: null, + investigationId: 'inv-test', context: { activeFilters: {}, cumulativeScope: null }, status: 'observed', comments: [], diff --git a/packages/charts/src/InvestigationWall/__tests__/GateBadge.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/GateBadge.test.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/__tests__/GateBadge.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/GateBadge.test.tsx diff --git a/packages/charts/src/InvestigationWall/__tests__/HypothesisCard.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/HypothesisCard.test.tsx similarity index 95% rename from packages/charts/src/InvestigationWall/__tests__/HypothesisCard.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/HypothesisCard.test.tsx index 624c1a6de..157ca1f41 100644 --- a/packages/charts/src/InvestigationWall/__tests__/HypothesisCard.test.tsx +++ b/packages/ui/src/components/InvestigationWall/__tests__/HypothesisCard.test.tsx @@ -15,8 +15,10 @@ const hub: Hypothesis = { questionIds: [], findingIds: ['f1', 'f2', 'f3'], status: 'confirmed', - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', }; describe('HypothesisCard', () => { @@ -56,6 +58,8 @@ describe('HypothesisCard', () => { id: 'f1', text: 'Night shift has wider spread', createdAt: 1, + deletedAt: null, + investigationId: 'inv-test', context: { activeFilters: {}, cumulativeScope: null }, status: 'analyzed', comments: [], @@ -66,6 +70,8 @@ describe('HypothesisCard', () => { id: 'f2', text: 'Nozzle temperature rises late in the run', createdAt: 2, + deletedAt: null, + investigationId: 'inv-test', context: { activeFilters: {}, cumulativeScope: null }, status: 'analyzed', comments: [], @@ -76,6 +82,8 @@ describe('HypothesisCard', () => { id: 'f3', text: 'Day shift has one similar spread event', createdAt: 3, + deletedAt: null, + investigationId: 'inv-test', context: { activeFilters: {}, cumulativeScope: null }, status: 'analyzed', comments: [], @@ -89,8 +97,10 @@ describe('HypothesisCard', () => { text: 'Check nozzle temperature after four hours', status: 'open', linkedFindingIds: [], - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', }, ]; const branch = projectMechanismBranch( diff --git a/packages/charts/src/InvestigationWall/__tests__/Minimap.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/Minimap.test.tsx similarity index 92% rename from packages/charts/src/InvestigationWall/__tests__/Minimap.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/Minimap.test.tsx index d74019478..f92688e1d 100644 --- a/packages/charts/src/InvestigationWall/__tests__/Minimap.test.tsx +++ b/packages/ui/src/components/InvestigationWall/__tests__/Minimap.test.tsx @@ -20,8 +20,10 @@ const hubs: Hypothesis[] = [ questionIds: [], findingIds: [], status: 'proposed', - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', }, { id: 'h-2', @@ -30,8 +32,10 @@ const hubs: Hypothesis[] = [ questionIds: [], findingIds: [], status: 'proposed', - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', }, ]; @@ -41,8 +45,10 @@ const questions: Question[] = [ text: 'What about downtime?', status: 'open', linkedFindingIds: [], - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', }, ]; diff --git a/packages/charts/src/InvestigationWall/__tests__/MissingEvidenceDigest.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/MissingEvidenceDigest.test.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/__tests__/MissingEvidenceDigest.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/MissingEvidenceDigest.test.tsx diff --git a/packages/charts/src/InvestigationWall/__tests__/MobileCardList.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/MobileCardList.test.tsx similarity index 94% rename from packages/charts/src/InvestigationWall/__tests__/MobileCardList.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/MobileCardList.test.tsx index bc5e196a6..f061873c9 100644 --- a/packages/charts/src/InvestigationWall/__tests__/MobileCardList.test.tsx +++ b/packages/ui/src/components/InvestigationWall/__tests__/MobileCardList.test.tsx @@ -10,8 +10,10 @@ const makeHub = (overrides: Partial = {}): Hypothesis => ({ questionIds: [], findingIds: [], status: 'proposed', - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', ...overrides, }); @@ -77,6 +79,8 @@ describe('MobileCardList', () => { id: 'f1', text: 'Night shift has wider spread', createdAt: 1, + deletedAt: null, + investigationId: 'inv-test', context: { activeFilters: {}, cumulativeScope: null }, status: 'analyzed', comments: [], @@ -87,6 +91,8 @@ describe('MobileCardList', () => { id: 'f2', text: 'Day shift has one similar event', createdAt: 2, + deletedAt: null, + investigationId: 'inv-test', context: { activeFilters: {}, cumulativeScope: null }, status: 'analyzed', comments: [], @@ -100,8 +106,10 @@ describe('MobileCardList', () => { text: 'Check nozzle temperature after four hours', status: 'open', linkedFindingIds: [], - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', }, ]; diff --git a/packages/charts/src/InvestigationWall/__tests__/NarratorRail.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/NarratorRail.test.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/__tests__/NarratorRail.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/NarratorRail.test.tsx diff --git a/packages/charts/src/InvestigationWall/__tests__/ProblemConditionCard.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/ProblemConditionCard.test.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/__tests__/ProblemConditionCard.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/ProblemConditionCard.test.tsx diff --git a/packages/charts/src/InvestigationWall/__tests__/QuestionPill.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/QuestionPill.test.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/__tests__/QuestionPill.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/QuestionPill.test.tsx diff --git a/packages/charts/src/InvestigationWall/__tests__/TributaryFooter.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/TributaryFooter.test.tsx similarity index 94% rename from packages/charts/src/InvestigationWall/__tests__/TributaryFooter.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/TributaryFooter.test.tsx index 1c51c4574..9da896cb4 100644 --- a/packages/charts/src/InvestigationWall/__tests__/TributaryFooter.test.tsx +++ b/packages/ui/src/components/InvestigationWall/__tests__/TributaryFooter.test.tsx @@ -37,8 +37,10 @@ describe('TributaryFooter', () => { findingIds: [], status: 'proposed', tributaryIds: ['t1'], - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', }; const { container } = render( diff --git a/packages/charts/src/InvestigationWall/__tests__/WallCanvas.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/WallCanvas.test.tsx similarity index 98% rename from packages/charts/src/InvestigationWall/__tests__/WallCanvas.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/WallCanvas.test.tsx index 6c3a5b685..ecaeb14d3 100644 --- a/packages/charts/src/InvestigationWall/__tests__/WallCanvas.test.tsx +++ b/packages/ui/src/components/InvestigationWall/__tests__/WallCanvas.test.tsx @@ -38,8 +38,8 @@ const processMap: ProcessMap = { nodes: [{ id: 'n1', name: 'Fill', order: 0 }], tributaries: [{ id: 't1', stepId: 'n1', column: 'SHIFT' }], ctsColumn: 'FILL', - createdAt: '', - updatedAt: '', + createdAt: '2026-05-09T00:00:00.000Z', + updatedAt: '2026-05-09T00:00:00.000Z', }; const hub: Hypothesis = { @@ -49,8 +49,10 @@ const hub: Hypothesis = { questionIds: [], findingIds: ['f1', 'f2', 'f3'], status: 'confirmed', - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', }; const openQuestion: Question = { @@ -58,8 +60,10 @@ const openQuestion: Question = { text: 'Does fill temperature drive this?', status: 'open', linkedFindingIds: [], - createdAt: '', - updatedAt: '', + createdAt: 1, + updatedAt: 1, + deletedAt: null, + investigationId: 'inv-test', }; describe('WallCanvas', () => { diff --git a/packages/ui/src/components/InvestigationWall/__tests__/public-export.test.ts b/packages/ui/src/components/InvestigationWall/__tests__/public-export.test.ts new file mode 100644 index 000000000..17e5a2429 --- /dev/null +++ b/packages/ui/src/components/InvestigationWall/__tests__/public-export.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; + +import { + CANVAS_H, + CANVAS_W, + HypothesisCard, + WallCanvas, + WALL_MOBILE_BREAKPOINT, + useWallIsMobile, +} from '../../../index'; +import type { WallStatus } from '../../../index'; + +describe('InvestigationWall public exports', () => { + it('exposes wall components, hooks, constants, and types from @variscout/ui', () => { + const status: WallStatus = 'proposed'; + + expect(status).toBe('proposed'); + expect(WallCanvas).toBeTypeOf('function'); + expect(HypothesisCard).toBeTypeOf('function'); + expect(useWallIsMobile).toBeTypeOf('function'); + expect(WALL_MOBILE_BREAKPOINT).toBe(768); + expect(CANVAS_W).toBeGreaterThan(0); + expect(CANVAS_H).toBeGreaterThan(0); + }); +}); diff --git a/packages/charts/src/InvestigationWall/__tests__/useWallBreakpoint.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/useWallBreakpoint.test.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/__tests__/useWallBreakpoint.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/useWallBreakpoint.test.tsx diff --git a/packages/charts/src/InvestigationWall/__tests__/useWallDragDrop.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/useWallDragDrop.test.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/__tests__/useWallDragDrop.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/useWallDragDrop.test.tsx diff --git a/packages/charts/src/InvestigationWall/__tests__/useWallKeyboard.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/useWallKeyboard.test.tsx similarity index 100% rename from packages/charts/src/InvestigationWall/__tests__/useWallKeyboard.test.tsx rename to packages/ui/src/components/InvestigationWall/__tests__/useWallKeyboard.test.tsx diff --git a/packages/charts/src/InvestigationWall/hooks/useWallBreakpoint.ts b/packages/ui/src/components/InvestigationWall/hooks/useWallBreakpoint.ts similarity index 80% rename from packages/charts/src/InvestigationWall/hooks/useWallBreakpoint.ts rename to packages/ui/src/components/InvestigationWall/hooks/useWallBreakpoint.ts index bfe3c2553..5a4c89615 100644 --- a/packages/charts/src/InvestigationWall/hooks/useWallBreakpoint.ts +++ b/packages/ui/src/components/InvestigationWall/hooks/useWallBreakpoint.ts @@ -2,10 +2,9 @@ * useWallBreakpoint — Local media-query hook for the Investigation Wall. * * Detects the 768px mobile breakpoint so WallCanvas can swap its 2000×1400 - * SVG for a vertical card list at narrow viewports. Lives here (rather than - * importing from `@variscout/ui`) because `@variscout/charts` must not - * depend on `@variscout/ui` — the monorepo dependency flow is - * core → hooks → ui → apps, and charts stays at the core/hooks tier. + * SVG for a vertical card list at narrow viewports. The wall now lives in + * `@variscout/ui`, so the hook stays beside the wall components and remains + * shared by PWA and Azure through the UI package. * * Shares the `BREAKPOINTS.mobile = 768` value with `useIsMobile` in * `@variscout/ui`, so PWA and Azure apps switch at the same width. @@ -14,8 +13,7 @@ import { useEffect, useState } from 'react'; /** * Mobile breakpoint in px. Matches `BREAKPOINTS.mobile` in - * `@variscout/ui/hooks/useMediaQuery.ts`. Kept in sync manually — no - * cross-package import is permitted from this file. + * `@variscout/ui/hooks/useMediaQuery.ts`. Kept in sync manually. */ export const WALL_MOBILE_BREAKPOINT = 768; diff --git a/packages/charts/src/InvestigationWall/hooks/useWallDragDrop.ts b/packages/ui/src/components/InvestigationWall/hooks/useWallDragDrop.ts similarity index 100% rename from packages/charts/src/InvestigationWall/hooks/useWallDragDrop.ts rename to packages/ui/src/components/InvestigationWall/hooks/useWallDragDrop.ts diff --git a/packages/charts/src/InvestigationWall/hooks/useWallKeyboard.ts b/packages/ui/src/components/InvestigationWall/hooks/useWallKeyboard.ts similarity index 100% rename from packages/charts/src/InvestigationWall/hooks/useWallKeyboard.ts rename to packages/ui/src/components/InvestigationWall/hooks/useWallKeyboard.ts diff --git a/packages/charts/src/InvestigationWall/hooks/useWallLocale.ts b/packages/ui/src/components/InvestigationWall/hooks/useWallLocale.ts similarity index 100% rename from packages/charts/src/InvestigationWall/hooks/useWallLocale.ts rename to packages/ui/src/components/InvestigationWall/hooks/useWallLocale.ts diff --git a/packages/charts/src/InvestigationWall/index.ts b/packages/ui/src/components/InvestigationWall/index.ts similarity index 100% rename from packages/charts/src/InvestigationWall/index.ts rename to packages/ui/src/components/InvestigationWall/index.ts diff --git a/packages/charts/src/InvestigationWall/types.ts b/packages/ui/src/components/InvestigationWall/types.ts similarity index 100% rename from packages/charts/src/InvestigationWall/types.ts rename to packages/ui/src/components/InvestigationWall/types.ts diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index b35be1faa..62e889395 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -196,6 +196,8 @@ export { type ProbabilityPlotTooltipProps, } from './components/ProbabilityPlotTooltip'; +export * from './components/InvestigationWall'; + export { VerificationCard, type VerificationCardProps, From 3174f15dff0f2751633cb1f74ae30f8bf2ae9aa2 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Sat, 9 May 2026 11:36:22 +0300 Subject: [PATCH 05/10] =?UTF-8?q?refactor(ui):=20collapse=20WallStatus=20?= =?UTF-8?q?=E2=86=92=20HypothesisStatus=205-state=20(per=20RPS=20V1=20D15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/i18n/messages/ar.ts | 1 + packages/core/src/i18n/messages/bg.ts | 1 + packages/core/src/i18n/messages/cs.ts | 1 + packages/core/src/i18n/messages/da.ts | 1 + packages/core/src/i18n/messages/de.ts | 1 + packages/core/src/i18n/messages/el.ts | 1 + packages/core/src/i18n/messages/en.ts | 1 + packages/core/src/i18n/messages/es.ts | 1 + packages/core/src/i18n/messages/fi.ts | 1 + packages/core/src/i18n/messages/fr.ts | 1 + packages/core/src/i18n/messages/he.ts | 1 + packages/core/src/i18n/messages/hi.ts | 1 + packages/core/src/i18n/messages/hr.ts | 1 + packages/core/src/i18n/messages/hu.ts | 1 + packages/core/src/i18n/messages/id.ts | 1 + packages/core/src/i18n/messages/it.ts | 1 + packages/core/src/i18n/messages/ja.ts | 1 + packages/core/src/i18n/messages/ko.ts | 1 + packages/core/src/i18n/messages/ms.ts | 1 + packages/core/src/i18n/messages/nb.ts | 1 + packages/core/src/i18n/messages/nl.ts | 1 + packages/core/src/i18n/messages/pl.ts | 1 + packages/core/src/i18n/messages/pt.ts | 1 + packages/core/src/i18n/messages/ro.ts | 1 + packages/core/src/i18n/messages/sk.ts | 1 + packages/core/src/i18n/messages/sv.ts | 1 + packages/core/src/i18n/messages/th.ts | 1 + packages/core/src/i18n/messages/tr.ts | 1 + packages/core/src/i18n/messages/uk.ts | 1 + packages/core/src/i18n/messages/vi.ts | 1 + packages/core/src/i18n/messages/zhHans.ts | 1 + packages/core/src/i18n/messages/zhHant.ts | 1 + packages/core/src/i18n/types.ts | 1 + .../InvestigationWall/HypothesisCard.tsx | 16 ++++++++---- .../InvestigationWall/MobileCardList.tsx | 25 +++++++++++-------- .../InvestigationWall/WallCanvas.tsx | 15 ++++++++--- .../__tests__/MobileCardList.test.tsx | 17 +++++++++++-- .../__tests__/WallCanvas.test.tsx | 21 ++++++++++++++++ .../__tests__/public-export.test.ts | 12 ++++++--- .../src/components/InvestigationWall/index.ts | 2 +- .../src/components/InvestigationWall/types.ts | 2 -- 41 files changed, 116 insertions(+), 27 deletions(-) diff --git a/packages/core/src/i18n/messages/ar.ts b/packages/core/src/i18n/messages/ar.ts index 91ebf5359..9e6f30e67 100644 --- a/packages/core/src/i18n/messages/ar.ts +++ b/packages/core/src/i18n/messages/ar.ts @@ -908,6 +908,7 @@ export const ar: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/bg.ts b/packages/core/src/i18n/messages/bg.ts index 574d6fd95..211a4817a 100644 --- a/packages/core/src/i18n/messages/bg.ts +++ b/packages/core/src/i18n/messages/bg.ts @@ -917,6 +917,7 @@ export const bg: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/cs.ts b/packages/core/src/i18n/messages/cs.ts index 6f374d9ef..fcb864510 100644 --- a/packages/core/src/i18n/messages/cs.ts +++ b/packages/core/src/i18n/messages/cs.ts @@ -828,6 +828,7 @@ export const cs: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/da.ts b/packages/core/src/i18n/messages/da.ts index d910bd09a..41d84a25d 100644 --- a/packages/core/src/i18n/messages/da.ts +++ b/packages/core/src/i18n/messages/da.ts @@ -881,6 +881,7 @@ export const da: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/de.ts b/packages/core/src/i18n/messages/de.ts index 7929a791e..ee8bba84a 100644 --- a/packages/core/src/i18n/messages/de.ts +++ b/packages/core/src/i18n/messages/de.ts @@ -920,6 +920,7 @@ export const de: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/el.ts b/packages/core/src/i18n/messages/el.ts index f892bfdc2..1c5674a67 100644 --- a/packages/core/src/i18n/messages/el.ts +++ b/packages/core/src/i18n/messages/el.ts @@ -920,6 +920,7 @@ export const el: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/en.ts b/packages/core/src/i18n/messages/en.ts index bc8658d77..22f8e9aef 100644 --- a/packages/core/src/i18n/messages/en.ts +++ b/packages/core/src/i18n/messages/en.ts @@ -926,6 +926,7 @@ export const en: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/es.ts b/packages/core/src/i18n/messages/es.ts index 9944bea01..0ef0c0ae8 100644 --- a/packages/core/src/i18n/messages/es.ts +++ b/packages/core/src/i18n/messages/es.ts @@ -922,6 +922,7 @@ export const es: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/fi.ts b/packages/core/src/i18n/messages/fi.ts index 839991166..9cd03a7df 100644 --- a/packages/core/src/i18n/messages/fi.ts +++ b/packages/core/src/i18n/messages/fi.ts @@ -918,6 +918,7 @@ export const fi: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/fr.ts b/packages/core/src/i18n/messages/fr.ts index 1213cafe7..36e75fa32 100644 --- a/packages/core/src/i18n/messages/fr.ts +++ b/packages/core/src/i18n/messages/fr.ts @@ -926,6 +926,7 @@ export const fr: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/he.ts b/packages/core/src/i18n/messages/he.ts index 3f6b2f91b..0d07c80a6 100644 --- a/packages/core/src/i18n/messages/he.ts +++ b/packages/core/src/i18n/messages/he.ts @@ -907,6 +907,7 @@ export const he: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/hi.ts b/packages/core/src/i18n/messages/hi.ts index 44afeb204..8dacb5995 100644 --- a/packages/core/src/i18n/messages/hi.ts +++ b/packages/core/src/i18n/messages/hi.ts @@ -919,6 +919,7 @@ export const hi: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/hr.ts b/packages/core/src/i18n/messages/hr.ts index fecf25c41..af663339a 100644 --- a/packages/core/src/i18n/messages/hr.ts +++ b/packages/core/src/i18n/messages/hr.ts @@ -914,6 +914,7 @@ export const hr: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/hu.ts b/packages/core/src/i18n/messages/hu.ts index 7035e997b..ccb9495ff 100644 --- a/packages/core/src/i18n/messages/hu.ts +++ b/packages/core/src/i18n/messages/hu.ts @@ -832,6 +832,7 @@ export const hu: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/id.ts b/packages/core/src/i18n/messages/id.ts index d3496eb76..f9c998900 100644 --- a/packages/core/src/i18n/messages/id.ts +++ b/packages/core/src/i18n/messages/id.ts @@ -867,6 +867,7 @@ export const id: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/it.ts b/packages/core/src/i18n/messages/it.ts index 7bf690b44..6705d67d9 100644 --- a/packages/core/src/i18n/messages/it.ts +++ b/packages/core/src/i18n/messages/it.ts @@ -890,6 +890,7 @@ export const it: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/ja.ts b/packages/core/src/i18n/messages/ja.ts index 06ab6dc2c..263a117a9 100644 --- a/packages/core/src/i18n/messages/ja.ts +++ b/packages/core/src/i18n/messages/ja.ts @@ -879,6 +879,7 @@ export const ja: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/ko.ts b/packages/core/src/i18n/messages/ko.ts index bf0526a84..917580af2 100644 --- a/packages/core/src/i18n/messages/ko.ts +++ b/packages/core/src/i18n/messages/ko.ts @@ -879,6 +879,7 @@ export const ko: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/ms.ts b/packages/core/src/i18n/messages/ms.ts index f350faf5d..2c340df49 100644 --- a/packages/core/src/i18n/messages/ms.ts +++ b/packages/core/src/i18n/messages/ms.ts @@ -918,6 +918,7 @@ export const ms: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/nb.ts b/packages/core/src/i18n/messages/nb.ts index f326708f9..7d3b46fda 100644 --- a/packages/core/src/i18n/messages/nb.ts +++ b/packages/core/src/i18n/messages/nb.ts @@ -829,6 +829,7 @@ export const nb: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/nl.ts b/packages/core/src/i18n/messages/nl.ts index bf143a45f..61f1c0f4f 100644 --- a/packages/core/src/i18n/messages/nl.ts +++ b/packages/core/src/i18n/messages/nl.ts @@ -889,6 +889,7 @@ export const nl: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/pl.ts b/packages/core/src/i18n/messages/pl.ts index dee1d8eca..0b7ebe53b 100644 --- a/packages/core/src/i18n/messages/pl.ts +++ b/packages/core/src/i18n/messages/pl.ts @@ -886,6 +886,7 @@ export const pl: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/pt.ts b/packages/core/src/i18n/messages/pt.ts index 048503ea0..9d968f0b0 100644 --- a/packages/core/src/i18n/messages/pt.ts +++ b/packages/core/src/i18n/messages/pt.ts @@ -922,6 +922,7 @@ export const pt: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/ro.ts b/packages/core/src/i18n/messages/ro.ts index b9e16c56e..aac77eb70 100644 --- a/packages/core/src/i18n/messages/ro.ts +++ b/packages/core/src/i18n/messages/ro.ts @@ -868,6 +868,7 @@ export const ro: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/sk.ts b/packages/core/src/i18n/messages/sk.ts index 128d01c08..a93df9846 100644 --- a/packages/core/src/i18n/messages/sk.ts +++ b/packages/core/src/i18n/messages/sk.ts @@ -917,6 +917,7 @@ export const sk: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/sv.ts b/packages/core/src/i18n/messages/sv.ts index 3cd5fe202..322bd943c 100644 --- a/packages/core/src/i18n/messages/sv.ts +++ b/packages/core/src/i18n/messages/sv.ts @@ -879,6 +879,7 @@ export const sv: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/th.ts b/packages/core/src/i18n/messages/th.ts index 57b2fc1b3..392a09c74 100644 --- a/packages/core/src/i18n/messages/th.ts +++ b/packages/core/src/i18n/messages/th.ts @@ -858,6 +858,7 @@ export const th: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/tr.ts b/packages/core/src/i18n/messages/tr.ts index e63a7cc29..429a46ea4 100644 --- a/packages/core/src/i18n/messages/tr.ts +++ b/packages/core/src/i18n/messages/tr.ts @@ -887,6 +887,7 @@ export const tr: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/uk.ts b/packages/core/src/i18n/messages/uk.ts index 2bc5305f3..5a9d49288 100644 --- a/packages/core/src/i18n/messages/uk.ts +++ b/packages/core/src/i18n/messages/uk.ts @@ -868,6 +868,7 @@ export const uk: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/vi.ts b/packages/core/src/i18n/messages/vi.ts index 172c6eed1..52849f678 100644 --- a/packages/core/src/i18n/messages/vi.ts +++ b/packages/core/src/i18n/messages/vi.ts @@ -867,6 +867,7 @@ export const vi: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/zhHans.ts b/packages/core/src/i18n/messages/zhHans.ts index 14b3640a1..d23675271 100644 --- a/packages/core/src/i18n/messages/zhHans.ts +++ b/packages/core/src/i18n/messages/zhHans.ts @@ -870,6 +870,7 @@ export const zhHans: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/messages/zhHant.ts b/packages/core/src/i18n/messages/zhHant.ts index c38b198fd..d2e75af42 100644 --- a/packages/core/src/i18n/messages/zhHant.ts +++ b/packages/core/src/i18n/messages/zhHant.ts @@ -870,6 +870,7 @@ export const zhHant: MessageCatalog = { 'wall.status.evidenced': 'Evidenced', 'wall.status.confirmed': 'Confirmed', 'wall.status.refuted': 'Refuted', + 'wall.status.needsDisconfirmation': 'Needs disconfirmation', 'wall.card.hypothesisLabel': 'Mechanism Branch', 'wall.card.findings': '{count} findings', 'wall.card.questions': '{count} questions', diff --git a/packages/core/src/i18n/types.ts b/packages/core/src/i18n/types.ts index 7065de4cb..4ed1147ab 100644 --- a/packages/core/src/i18n/types.ts +++ b/packages/core/src/i18n/types.ts @@ -1008,6 +1008,7 @@ export interface MessageCatalog { 'wall.status.evidenced': string; 'wall.status.confirmed': string; 'wall.status.refuted': string; + 'wall.status.needsDisconfirmation': string; 'wall.card.hypothesisLabel': string; 'wall.card.findings': string; 'wall.card.evidenceGap': string; diff --git a/packages/ui/src/components/InvestigationWall/HypothesisCard.tsx b/packages/ui/src/components/InvestigationWall/HypothesisCard.tsx index 80c64c868..44b5abe1f 100644 --- a/packages/ui/src/components/InvestigationWall/HypothesisCard.tsx +++ b/packages/ui/src/components/InvestigationWall/HypothesisCard.tsx @@ -10,16 +10,20 @@ */ import React from 'react'; -import type { MechanismBranchViewModel, MessageCatalog, Hypothesis } from '@variscout/core'; +import type { + MechanismBranchViewModel, + MessageCatalog, + Hypothesis, + HypothesisStatus, +} from '@variscout/core'; import { formatMessage, getMessage } from '@variscout/core/i18n'; import { chartColors } from '@variscout/charts'; -import type { WallStatus } from './types'; import { useWallLocale } from './hooks/useWallLocale'; export interface HypothesisCardProps { hub: Hypothesis; branch?: MechanismBranchViewModel; - displayStatus: WallStatus; + displayStatus: HypothesisStatus; x: number; y: number; hasGap?: boolean; @@ -45,19 +49,21 @@ export interface HypothesisCardProps { const CARD_W = 280; const CARD_H = 228; -const STATUS_KEY: Record = { +const STATUS_KEY: Record = { proposed: 'wall.status.proposed', evidenced: 'wall.status.evidenced', confirmed: 'wall.status.confirmed', refuted: 'wall.status.refuted', + 'needs-disconfirmation': 'wall.status.needsDisconfirmation', }; /** Status-specific border colors sourced from chartColors — no hardcoded hex. */ -const STATUS_STROKE: Record = { +const STATUS_STROKE: Record = { proposed: chartColors.mean, // blue-500 — neutral/open evidenced: chartColors.control, // cyan-500 — data linked confirmed: chartColors.pass, // green-500 — outcome verified refuted: chartColors.fail, // red-500 — mechanism rejected + 'needs-disconfirmation': chartColors.warning, // amber — needs disconfirmation check }; export const HypothesisCard: React.FC = ({ diff --git a/packages/ui/src/components/InvestigationWall/MobileCardList.tsx b/packages/ui/src/components/InvestigationWall/MobileCardList.tsx index b1069f61d..bb6fef5bb 100644 --- a/packages/ui/src/components/InvestigationWall/MobileCardList.tsx +++ b/packages/ui/src/components/InvestigationWall/MobileCardList.tsx @@ -5,10 +5,6 @@ * is unreadable. Each hub renders as a structured Mechanism Branch card with * suspected mechanism, clue/check counts, readiness, next move, and a subtle * status-colored left border. - * - * Status derivation mirrors `deriveDisplayStatus` in WallCanvas: confirmed - * and refuted map directly; otherwise "evidenced" when at least one - * supporting finding exists without a contradictor, else "proposed". */ import React from 'react'; @@ -17,13 +13,13 @@ import { type MessageCatalog, type ProcessMap, type Hypothesis, + type HypothesisStatus, type Finding, type Question, } from '@variscout/core'; import { formatMessage, getMessage } from '@variscout/core/i18n'; import { chartColors } from '@variscout/charts'; import { EmptyState } from './EmptyState'; -import type { WallStatus } from './types'; import { useWallLocale } from './hooks/useWallLocale'; export interface MobileCardListProps { @@ -43,11 +39,12 @@ export interface MobileCardListProps { onSeedFromFactorIntel?: () => void; } -const STATUS_KEY: Record = { +const STATUS_KEY: Record = { proposed: 'wall.status.proposed', evidenced: 'wall.status.evidenced', confirmed: 'wall.status.confirmed', refuted: 'wall.status.refuted', + 'needs-disconfirmation': 'wall.status.needsDisconfirmation', }; /** @@ -55,21 +52,29 @@ const STATUS_KEY: Record = { * `HypothesisCard` so the card list reads as a compact version of the SVG * rendering. Sourced from `chartColors` — no hardcoded hex. */ -const STATUS_ACCENT: Record = { +const STATUS_ACCENT: Record = { proposed: chartColors.mean, evidenced: chartColors.control, confirmed: chartColors.pass, refuted: chartColors.fail, + 'needs-disconfirmation': chartColors.warning, }; +const CANONICAL_HYPOTHESIS_STATUSES = new Set([ + 'proposed', + 'evidenced', + 'confirmed', + 'refuted', + 'needs-disconfirmation', +]); + /** * Derive the displayed status. Kept local (instead of imported from * WallCanvas) to avoid introducing a shared-helpers file mid-phase — the * computation is tiny and the two call sites share nothing else. */ -function deriveDisplayStatus(hub: Hypothesis, findings: Finding[]): WallStatus { - if (hub.status === 'confirmed') return 'confirmed'; - if (hub.status === 'refuted') return 'refuted'; +function deriveDisplayStatus(hub: Hypothesis, findings: Finding[]): HypothesisStatus { + if (CANONICAL_HYPOTHESIS_STATUSES.has(hub.status)) return hub.status; const supporting = hub.findingIds .map(id => findings.find(f => f.id === id)) .filter((f): f is Finding => !!f); diff --git a/packages/ui/src/components/InvestigationWall/WallCanvas.tsx b/packages/ui/src/components/InvestigationWall/WallCanvas.tsx index c878b644b..1af014e88 100644 --- a/packages/ui/src/components/InvestigationWall/WallCanvas.tsx +++ b/packages/ui/src/components/InvestigationWall/WallCanvas.tsx @@ -13,6 +13,7 @@ import { DndContext } from '@dnd-kit/core'; import type { Hypothesis, Finding, + HypothesisStatus, Question, ProcessMap, GateNode, @@ -28,7 +29,6 @@ import { TributaryFooter } from './TributaryFooter'; import { EmptyState } from './EmptyState'; import { MissingEvidenceDigest } from './MissingEvidenceDigest'; import { MobileCardList } from './MobileCardList'; -import type { WallStatus } from './types'; import { useWallLocale } from './hooks/useWallLocale'; import { useWallDragDrop } from './hooks/useWallDragDrop'; import { useWallIsMobile } from './hooks/useWallBreakpoint'; @@ -103,9 +103,16 @@ export interface WallCanvasProps { export const CANVAS_W = 2000; export const CANVAS_H = 1400; -function deriveDisplayStatus(hub: Hypothesis, findings: Finding[]): WallStatus { - if (hub.status === 'confirmed') return 'confirmed'; - if (hub.status === 'refuted') return 'refuted'; +const CANONICAL_HYPOTHESIS_STATUSES = new Set([ + 'proposed', + 'evidenced', + 'confirmed', + 'refuted', + 'needs-disconfirmation', +]); + +function deriveDisplayStatus(hub: Hypothesis, findings: Finding[]): HypothesisStatus { + if (CANONICAL_HYPOTHESIS_STATUSES.has(hub.status)) return hub.status; const supporting = hub.findingIds .map(id => findings.find(f => f.id === id)) .filter((f): f is Finding => !!f); diff --git a/packages/ui/src/components/InvestigationWall/__tests__/MobileCardList.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/MobileCardList.test.tsx index f061873c9..6dc00a923 100644 --- a/packages/ui/src/components/InvestigationWall/__tests__/MobileCardList.test.tsx +++ b/packages/ui/src/components/InvestigationWall/__tests__/MobileCardList.test.tsx @@ -47,7 +47,20 @@ describe('MobileCardList', () => { expect(card.textContent).toMatch(/Confirmed/); }); - it('derives "evidenced" when supporting findings are present without contradictors', () => { + it('preserves canonical needs-disconfirmation status from hub.status', () => { + const hub = makeHub({ + id: 'h-needs-disconfirmation', + status: 'needs-disconfirmation', + }); + + render(); + + const card = screen.getByTestId('wall-mobile-hub-h-needs-disconfirmation'); + expect(card).toHaveAttribute('data-status', 'needs-disconfirmation'); + expect(card.textContent).toMatch(/Needs disconfirmation/); + }); + + it('preserves canonical evidenced status from hub.status', () => { const findings: Finding[] = [ { id: 'f1', @@ -57,7 +70,7 @@ describe('MobileCardList', () => { validationStatus: 'supports', } as unknown as Finding, ]; - const hub = makeHub({ id: 'h-ev', findingIds: ['f1'], status: 'proposed' }); + const hub = makeHub({ id: 'h-ev', findingIds: ['f1'], status: 'evidenced' }); render(); expect(screen.getByTestId('wall-mobile-hub-h-ev')).toHaveAttribute('data-status', 'evidenced'); }); diff --git a/packages/ui/src/components/InvestigationWall/__tests__/WallCanvas.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/WallCanvas.test.tsx index ecaeb14d3..8118b0e2a 100644 --- a/packages/ui/src/components/InvestigationWall/__tests__/WallCanvas.test.tsx +++ b/packages/ui/src/components/InvestigationWall/__tests__/WallCanvas.test.tsx @@ -96,6 +96,27 @@ describe('WallCanvas', () => { expect(screen.getByText(/Nozzle runs hot/i)).toBeInTheDocument(); }); + it('renders canonical needs-disconfirmation status stored on a hub', () => { + const needsDisconfirmationHub: Hypothesis = { + ...hub, + id: 'h-needs-disconfirmation', + status: 'needs-disconfirmation', + }; + const { container } = render( + + ); + + expect(screen.getByText(/Needs disconfirmation/i)).toBeInTheDocument(); + expect(container.querySelector('[data-status="needs-disconfirmation"]')).toBeTruthy(); + }); + it('renders branch cards without a process map', () => { render( { it('exposes wall components, hooks, constants, and types from @variscout/ui', () => { - const status: WallStatus = 'proposed'; + const status: HypothesisStatus = 'needs-disconfirmation'; + const removedStatusIsNotExported: RemovedStatusIsNotExported = true; - expect(status).toBe('proposed'); + expect(status).toBe('needs-disconfirmation'); + expect(removedStatusIsNotExported).toBe(true); expect(WallCanvas).toBeTypeOf('function'); expect(HypothesisCard).toBeTypeOf('function'); expect(useWallIsMobile).toBeTypeOf('function'); diff --git a/packages/ui/src/components/InvestigationWall/index.ts b/packages/ui/src/components/InvestigationWall/index.ts index 9cd964fbd..0e1769810 100644 --- a/packages/ui/src/components/InvestigationWall/index.ts +++ b/packages/ui/src/components/InvestigationWall/index.ts @@ -18,7 +18,7 @@ export { MissingEvidenceDigest } from './MissingEvidenceDigest'; export type { MissingEvidenceDigestProps, MissingEvidenceGap } from './MissingEvidenceDigest'; export { EmptyState } from './EmptyState'; export type { EmptyStateProps } from './EmptyState'; -export type { WallStatus, Point } from './types'; +export type { Point } from './types'; export { useWallKeyboard } from './hooks/useWallKeyboard'; export type { UseWallKeyboardOptions } from './hooks/useWallKeyboard'; export { diff --git a/packages/ui/src/components/InvestigationWall/types.ts b/packages/ui/src/components/InvestigationWall/types.ts index e1d36b903..79d639a89 100644 --- a/packages/ui/src/components/InvestigationWall/types.ts +++ b/packages/ui/src/components/InvestigationWall/types.ts @@ -2,5 +2,3 @@ export interface Point { x: number; y: number; } - -export type WallStatus = 'proposed' | 'evidenced' | 'confirmed' | 'refuted'; From 9dd84c8a8274e846c8e5bb5e527caa0d14f0d674 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Sat, 9 May 2026 11:53:42 +0300 Subject: [PATCH 06/10] feat(ui): TagChip component + Hypothesis.themeTags rendering on Wall --- .../InvestigationWall/HypothesisCard.tsx | 42 +++++++++- .../components/InvestigationWall/TagChip.tsx | 37 +++++++++ .../__tests__/HypothesisCard.test.tsx | 76 +++++++++++++++++++ .../__tests__/TagChip.test.tsx | 31 ++++++++ .../src/components/InvestigationWall/index.ts | 2 + 5 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 packages/ui/src/components/InvestigationWall/TagChip.tsx create mode 100644 packages/ui/src/components/InvestigationWall/__tests__/TagChip.test.tsx diff --git a/packages/ui/src/components/InvestigationWall/HypothesisCard.tsx b/packages/ui/src/components/InvestigationWall/HypothesisCard.tsx index 44b5abe1f..a5f4dd762 100644 --- a/packages/ui/src/components/InvestigationWall/HypothesisCard.tsx +++ b/packages/ui/src/components/InvestigationWall/HypothesisCard.tsx @@ -19,6 +19,7 @@ import type { import { formatMessage, getMessage } from '@variscout/core/i18n'; import { chartColors } from '@variscout/charts'; import { useWallLocale } from './hooks/useWallLocale'; +import { TagChip } from './TagChip'; export interface HypothesisCardProps { hub: Hypothesis; @@ -48,6 +49,13 @@ export interface HypothesisCardProps { const CARD_W = 280; const CARD_H = 228; +const BODY_TOP = 64; +const TAG_ROW_Y = 94; +const TAG_ROW_H = 24; +const TAGGED_READINESS_Y = 126; +const TAGGED_CLUE_COUNT_Y = 146; +const DEFAULT_READINESS_Y = 108; +const DEFAULT_CLUE_COUNT_Y = 130; const STATUS_KEY: Record = { proposed: 'wall.status.proposed', @@ -87,6 +95,7 @@ export const HypothesisCard: React.FC = ({ const mechanismName = branch?.suspectedMechanism ?? hub.name; const readinessLabel = branch?.readiness.label; const nextMove = branch?.nextMove ?? hub.nextMove; + const themeTags = (hub.themeTags ?? []).map(tag => tag.trim()).filter(Boolean); const supportingLabel = `${supportingCount} supporting clue${supportingCount === 1 ? '' : 's'}`; const counterLabel = `${counterCount} counter-clue${counterCount === 1 ? '' : 's'}`; const openChecksLabel = `${openCheckCount} open check${openCheckCount === 1 ? '' : 's'}`; @@ -173,14 +182,41 @@ export const HypothesisCard: React.FC = ({ {!isMedium && ( <> - + Suspected mechanism - + {themeTags.length > 0 && ( + +
+ {themeTags.map(tag => ( + + ))} +
+
+ )} + 0 ? TAGGED_READINESS_Y : DEFAULT_READINESS_Y} + className="fill-content-muted text-xs" + > {readinessLabel ?? statusLabel} - + 0 ? TAGGED_CLUE_COUNT_Y : DEFAULT_CLUE_COUNT_Y} + className="fill-content-muted text-xs font-mono" + > {supportingLabel} · {counterLabel} diff --git a/packages/ui/src/components/InvestigationWall/TagChip.tsx b/packages/ui/src/components/InvestigationWall/TagChip.tsx new file mode 100644 index 000000000..3b6de156a --- /dev/null +++ b/packages/ui/src/components/InvestigationWall/TagChip.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { operatorColors } from '@variscout/charts'; + +export interface TagChipProps { + tag: string; +} + +const colorForTag = (tag: string): (typeof operatorColors)[number] => { + const normalized = tag.trim().toLocaleLowerCase(); + let hash = 0; + + for (let i = 0; i < normalized.length; i += 1) { + hash = (hash * 31 + normalized.charCodeAt(i)) >>> 0; + } + + return operatorColors[hash % operatorColors.length]; +}; + +export const TagChip: React.FC = ({ tag }) => { + const label = tag.trim(); + const color = colorForTag(label); + + return ( +
+ + ); +}; diff --git a/packages/ui/src/components/InvestigationWall/__tests__/HypothesisCard.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/HypothesisCard.test.tsx index 157ca1f41..787dd401f 100644 --- a/packages/ui/src/components/InvestigationWall/__tests__/HypothesisCard.test.tsx +++ b/packages/ui/src/components/InvestigationWall/__tests__/HypothesisCard.test.tsx @@ -41,6 +41,82 @@ describe('HypothesisCard', () => { expect(screen.getByText(/3 supporting clues/)).toBeInTheDocument(); }); + it('renders theme tags in the full-card body when present', () => { + const { container } = render( + + + + ); + + expect(container.querySelector('foreignObject')).toBeTruthy(); + expect(screen.getByText('#Night Shift')).toBeInTheDocument(); + expect(screen.getByText('#Nozzle Temp')).toBeInTheDocument(); + }); + + it('keeps tagged layout rows vertically separated', () => { + const { container } = render( + + + + ); + + const tagRow = container.querySelector('foreignObject'); + const readinessText = Array.from(container.querySelectorAll('text')).find( + text => text.textContent === 'Confirmed' && text.getAttribute('y') !== '24' + ); + const clueText = screen.getByText(/3 supporting clues/); + + expect(tagRow).toBeTruthy(); + expect(readinessText).toBeTruthy(); + const tagBottom = + Number(tagRow?.getAttribute('y') ?? 0) + Number(tagRow?.getAttribute('height') ?? 0); + const readinessY = Number(readinessText?.getAttribute('y') ?? 0); + const clueY = Number(clueText.getAttribute('y') ?? 0); + + expect(readinessY - tagBottom).toBeGreaterThanOrEqual(8); + expect(clueY - readinessY).toBeGreaterThanOrEqual(18); + }); + + it('does not render a theme tag container when no tags are present', () => { + const { container } = render( + + + + ); + + expect(container.querySelector('[data-testid="hypothesis-theme-tags"]')).toBeNull(); + expect(container.querySelector('foreignObject')).toBeNull(); + }); + + it('keeps theme tags hidden at medium and glyph LOD', () => { + const taggedHub = { ...hub, themeTags: ['Night Shift'] }; + const { rerender } = render( + + + + ); + + expect(screen.queryByText('#Night Shift')).toBeNull(); + + rerender( + + + + ); + + expect(screen.queryByText('#Night Shift')).toBeNull(); + }); + it('fires onSelect on click', () => { const onSelect = vi.fn(); render( diff --git a/packages/ui/src/components/InvestigationWall/__tests__/TagChip.test.tsx b/packages/ui/src/components/InvestigationWall/__tests__/TagChip.test.tsx new file mode 100644 index 000000000..afd46747a --- /dev/null +++ b/packages/ui/src/components/InvestigationWall/__tests__/TagChip.test.tsx @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { operatorColors } from '@variscout/charts'; +import { TagChip } from '../TagChip'; + +describe('TagChip', () => { + it('renders a compact hash-prefixed tag label', () => { + render(); + + expect(screen.getByText('#Night Shift')).toBeInTheDocument(); + }); + + it('applies a deterministic theme-derived border color', () => { + render( + <> + + + + ); + + const chips = screen + .getAllByText('#Night Shift') + .map(label => label.closest('[data-theme-color]') as HTMLElement); + const firstColor = chips[0].getAttribute('data-theme-color'); + const secondColor = chips[1].getAttribute('data-theme-color'); + + expect(firstColor).toBe(secondColor); + expect(operatorColors).toContain(firstColor); + expect(chips[0]).toHaveStyle({ borderColor: firstColor }); + }); +}); diff --git a/packages/ui/src/components/InvestigationWall/index.ts b/packages/ui/src/components/InvestigationWall/index.ts index 0e1769810..a6bba18cf 100644 --- a/packages/ui/src/components/InvestigationWall/index.ts +++ b/packages/ui/src/components/InvestigationWall/index.ts @@ -8,6 +8,8 @@ export { QuestionPill } from './QuestionPill'; export type { QuestionPillProps } from './QuestionPill'; export { FindingChip } from './FindingChip'; export type { FindingChipProps } from './FindingChip'; +export { TagChip } from './TagChip'; +export type { TagChipProps } from './TagChip'; export { GateBadge } from './GateBadge'; export type { GateBadgeProps } from './GateBadge'; export { NarratorRail } from './NarratorRail'; From 46b82f5daafdab1fce585c8b30ba0e35e8a0da0c Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Sat, 9 May 2026 12:06:23 +0300 Subject: [PATCH 07/10] =?UTF-8?q?docs(adr):=20amend=20ADR-053=20+=20ADR-06?= =?UTF-8?q?4=20=E2=80=94=20Hypothesis-as-downstream-role=20+=20SuspectedCa?= =?UTF-8?q?use=20=E2=86=92=20Hypothesis=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/07-decisions/adr-053-question-driven-investigation.md | 6 ++++++ docs/07-decisions/adr-064-suspected-cause-hub-model.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/docs/07-decisions/adr-053-question-driven-investigation.md b/docs/07-decisions/adr-053-question-driven-investigation.md index 9dd44e0cc..90f3110ef 100644 --- a/docs/07-decisions/adr-053-question-driven-investigation.md +++ b/docs/07-decisions/adr-053-question-driven-investigation.md @@ -144,6 +144,12 @@ See: [Investigation Workspace Reframing Design](../superpowers/specs/2026-04-03- Since **ADR-062** (Apr 2026), the label "Contribution %" has been replaced by the standard statistical term **η²** (eta-squared) throughout the UI and documentation. Findings, filter chips, and the question checklist evidence badge all use η² and n=X notation. The VariationBar component was removed. The underlying metric (eta-squared) is unchanged; only the display label was standardized. +## Amendment — 2026-05-09: Hypothesis as downstream candidate explanation + +Response Path System V1 reuses the word `Hypothesis` for the downstream candidate-explanation entity formerly named `SuspectedCause`. + +This does not reopen ADR-053's core decision. Questions remain the primary investigation artifact: they express open inquiry, drive evidence collection, and structure the investigation tree. A `Hypothesis` is downstream of that question-driven work: a testable candidate mechanism that connects findings and questions after evidence starts to converge. + ## References - Turtiainen, J-M. (2019). _Mental Model for Exploratory Data Analysis Applications for Structured Problem-Solving._ LUT University. diff --git a/docs/07-decisions/adr-064-suspected-cause-hub-model.md b/docs/07-decisions/adr-064-suspected-cause-hub-model.md index 17653d6c0..5d09754a7 100644 --- a/docs/07-decisions/adr-064-suspected-cause-hub-model.md +++ b/docs/07-decisions/adr-064-suspected-cause-hub-model.md @@ -123,3 +123,9 @@ IS one step's deep-dive) and the `nodeMappings` table (B1 shape — investigation covers multiple steps). Neither replaces SuspectedCause; they augment the per-step capability surface that feeds Current Process State and the production-line-glance dashboard. + +## Amendment — 2026-05-09: SuspectedCause renamed to Hypothesis + +Response Path System V1 renames the `SuspectedCause` entity to `Hypothesis`. + +The word "hub" in this ADR's title was descriptive: the entity hubs together evidence threads for one mechanism. It did not introduce a separate parent level above the entity. After the rename, `Hypothesis` is the entity that hubs findings and questions for one mechanism. No structural model change is introduced by the rename. From cd2397ddfc533e5255fd023e739f3ce2ed78aef2 Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Sat, 9 May 2026 13:14:00 +0300 Subject: [PATCH 08/10] refactor: complete Hypothesis naming cascade across apps --- .../src/__tests__/server.integration.test.ts | 8 +- .../azure/src/components/editor/FrameView.tsx | 4 +- .../editor/InvestigationWorkspace.tsx | 52 ++++---- .../azure/src/components/editor/PISection.tsx | 6 +- .../editor/__tests__/FrameView.test.tsx | 6 +- .../InvestigationWorkspace.mapwall.test.tsx | 4 +- .../azure/src/components/views/ReportView.tsx | 6 +- .../__tests__/ReportView.evidenceMap.test.tsx | 8 +- .../useInvestigationIndexing.test.ts | 4 +- .../src/features/ai/actionToolHandlers.ts | 8 +- .../src/features/ai/useAIOrchestration.ts | 15 +-- .../__tests__/useWallBackgroundJobs.test.ts | 6 +- .../useInvestigationOrchestration.ts | 16 +-- .../investigation/useWallBackgroundJobs.ts | 12 +- .../useWallHubCommentLifecycle.ts | 2 +- apps/azure/src/hooks/useDataIngestion.ts | 2 +- apps/azure/src/pages/Editor.tsx | 18 +-- .../src/persistence/AzureHubRepository.ts | 4 +- .../__tests__/AzureHubRepository.read.test.ts | 6 +- apps/azure/src/persistence/cascadeArchive.ts | 10 +- .../__tests__/investigationSerializer.test.ts | 120 +++++++++--------- .../src/services/investigationSerializer.ts | 26 ++-- apps/pwa/src/components/views/FrameView.tsx | 4 +- .../components/views/InvestigationView.tsx | 12 +- .../views/__tests__/FrameView.test.tsx | 6 +- apps/pwa/src/db/schema.ts | 10 +- .../useInvestigationOrchestration.ts | 14 +- apps/pwa/src/hooks/useDataIngestion.ts | 2 +- apps/pwa/src/persistence/PwaHubRepository.ts | 9 +- .../__tests__/PwaHubRepository.test.ts | 4 +- .../persistence/__tests__/applyAction.test.ts | 2 +- .../__tests__/exports.nodeCapability.test.ts | 4 +- packages/core/src/__tests__/stress.test.ts | 4 +- .../src/ai/__tests__/coScoutContext.test.ts | 8 +- .../src/ai/__tests__/promptTemplates.test.ts | 14 +- packages/core/src/ai/buildAIContext.ts | 12 +- .../prompts/coScout/context/investigation.ts | 2 +- .../coScout/context/knowledgeContext.ts | 2 +- .../core/src/ai/prompts/coScout/legacy.ts | 8 +- packages/core/src/ai/types.ts | 12 +- .../findings/__tests__/findingsBarrel.test.ts | 14 +- .../__tests__/problemStatement.test.ts | 30 ++--- packages/core/src/findings/index.ts | 2 +- packages/core/src/findings/mechanismBranch.ts | 8 +- .../core/src/findings/problemStatement.ts | 8 +- packages/core/src/index.ts | 2 +- .../__tests__/cascadeRules.test.ts | 8 +- packages/core/src/persistence/cascadeRules.ts | 6 +- packages/core/src/variation/bestSubgroup.ts | 2 +- .../src/samples/investigation-showcase.ts | 6 +- .../data/src/samples/syringe-barrel-weight.ts | 4 +- packages/data/src/types.ts | 2 +- .../useCanvasInvestigationOverlays.test.ts | 20 +-- .../useHasInvestigationContent.test.ts | 2 +- .../src/__tests__/useHubCommentStream.test.ts | 16 +-- .../src/__tests__/useHubComputations.test.ts | 22 ++-- ...edCauses.test.ts => useHypotheses.test.ts} | 70 +++++----- .../useImprovementProjections.test.ts | 28 ++-- .../src/__tests__/useKnowledgeSearch.test.ts | 2 +- .../src/__tests__/useProblemStatement.test.ts | 22 ++-- .../useProjectActions.roundtrip.test.ts | 2 +- .../src/__tests__/useSharedWallProps.test.ts | 4 +- packages/hooks/src/index.ts | 12 +- packages/hooks/src/types.ts | 4 +- packages/hooks/src/useAIContext.ts | 8 +- .../src/useCanvasInvestigationOverlays.ts | 38 +++--- packages/hooks/src/useDataIngestion.ts | 11 +- packages/hooks/src/useEvidenceMapData.ts | 16 +-- packages/hooks/src/useEvidenceMapTimeline.ts | 14 +- packages/hooks/src/useHMWPrompts.ts | 2 +- .../hooks/src/useHasInvestigationContent.ts | 2 +- packages/hooks/src/useHubCommentStream.ts | 2 +- packages/hooks/src/useHubComputations.ts | 2 +- ...useSuspectedCauses.ts => useHypotheses.ts} | 6 +- .../hooks/src/useImprovementProjections.ts | 12 +- packages/hooks/src/useKnowledgeSearch.ts | 2 +- packages/hooks/src/useProblemStatement.ts | 30 ++--- packages/hooks/src/useProjectActions.ts | 8 +- packages/hooks/src/useSharedWallProps.ts | 2 +- .../src/__tests__/investigationStore.test.ts | 54 ++++---- packages/stores/src/investigationStore.ts | 34 ++--- packages/stores/src/projectStore.ts | 2 +- .../src/components/Canvas/CanvasWorkspace.tsx | 6 +- .../Canvas/__tests__/Canvas.test.tsx | 8 +- .../Canvas/__tests__/CanvasWorkspace.test.tsx | 16 +-- packages/ui/src/components/Canvas/index.tsx | 7 +- .../Canvas/internal/CanvasStepCard.tsx | 8 +- .../Canvas/internal/CanvasStepOverlay.tsx | 4 +- .../__tests__/CanvasOverlayPicker.test.tsx | 10 +- .../__tests__/CanvasStepCard.test.tsx | 8 +- .../__tests__/CanvasStepOverlay.test.tsx | 2 +- .../__tests__/CanvasWallOverlay.test.tsx | 14 +- .../CoScoutPanel/CoScoutPanelBase.tsx | 2 +- .../components/FindingsLog/FindingCard.tsx | 4 +- .../FindingsLog/FindingCardExpanded.tsx | 4 +- .../InvestigationConclusion.tsx | 16 +-- .../FindingsWindow/InvestigationSidebar.tsx | 8 +- .../InvestigationConclusion.test.tsx | 19 +-- .../ConclusionCard.tsx | 22 ++-- .../QuestionsTabContent.tsx | 8 +- .../QuestionsTabView.tsx | 12 +- .../__tests__/ConclusionCard.test.tsx | 15 +-- .../__tests__/QuestionsTabView.test.tsx | 12 +- .../ReportView/ReportInvestigationSummary.tsx | 14 +- .../ReportInvestigationSummary.test.tsx | 4 +- scripts/check-level-boundaries.sh | 2 +- 106 files changed, 600 insertions(+), 638 deletions(-) rename packages/hooks/src/__tests__/{useSuspectedCauses.test.ts => useHypotheses.test.ts} (80%) rename packages/hooks/src/{useSuspectedCauses.ts => useHypotheses.ts} (97%) diff --git a/apps/azure/src/__tests__/server.integration.test.ts b/apps/azure/src/__tests__/server.integration.test.ts index ad4668753..63de4c689 100644 --- a/apps/azure/src/__tests__/server.integration.test.ts +++ b/apps/azure/src/__tests__/server.integration.test.ts @@ -100,6 +100,8 @@ beforeAll(async () => { // Use connection string path so we avoid DefaultAzureCredential flow process.env.AZURE_STORAGE_CONNECTION_STRING = 'DefaultEndpointsProtocol=https;AccountName=teststorage;AccountKey=dGVzdGtleQ==;EndpointSuffix=core.windows.net'; + process.env.VOICE_INPUT_ENABLED = 'true'; + process.env.AI_SPEECH_TO_TEXT_DEPLOYMENT = 'gpt-4o-mini-transcribe'; // Dynamically import so mocks are active before server.js runs const { app } = (await import('../../server.js')) as { app: Express }; @@ -152,18 +154,12 @@ describe('GET /config', () => { }); it('enables microphone permissions and runtime fields when voice input is configured', async () => { - process.env.VOICE_INPUT_ENABLED = 'true'; - process.env.AI_SPEECH_TO_TEXT_DEPLOYMENT = 'gpt-4o-mini-transcribe'; - const res = await request.get('/config'); const body = JSON.parse(res.text); expect(body.voiceInputEnabled).toBe(true); expect(body.speechToTextDeployment).toBe('gpt-4o-mini-transcribe'); expect(res.headers['permissions-policy']).toContain('microphone=(self)'); - - delete process.env.VOICE_INPUT_ENABLED; - delete process.env.AI_SPEECH_TO_TEXT_DEPLOYMENT; }); }); diff --git a/apps/azure/src/components/editor/FrameView.tsx b/apps/azure/src/components/editor/FrameView.tsx index f5086c71e..80e00413d 100644 --- a/apps/azure/src/components/editor/FrameView.tsx +++ b/apps/azure/src/components/editor/FrameView.tsx @@ -43,7 +43,7 @@ const FrameView: React.FC = () => { const setProcessContext = useProjectStore(s => s.setProcessContext); const findings = useInvestigationStore(s => s.findings); const questions = useInvestigationStore(s => s.questions); - const suspectedCauses = useInvestigationStore(s => s.suspectedCauses); + const hypotheses = useInvestigationStore(s => s.hypotheses); const causalLinks = useInvestigationStore(s => s.causalLinks); const activeHubId = useProjectStore(s => s.processContext?.processHubId ?? null); const [priorStepStats, setPriorStepStats] = @@ -152,7 +152,7 @@ const FrameView: React.FC = () => { onFocusedInvestigation={handleFocusedInvestigation} findings={findings} questions={questions} - suspectedCauses={suspectedCauses} + hypotheses={hypotheses} causalLinks={causalLinks} onOpenWall={handleOpenWall} onOpenInvestigationFocus={handleOpenInvestigationFocus} diff --git a/apps/azure/src/components/editor/InvestigationWorkspace.tsx b/apps/azure/src/components/editor/InvestigationWorkspace.tsx index 70c0bb9e9..3a2889969 100644 --- a/apps/azure/src/components/editor/InvestigationWorkspace.tsx +++ b/apps/azure/src/components/editor/InvestigationWorkspace.tsx @@ -98,7 +98,7 @@ interface InvestigationWorkspaceProps { // Column aliases columnAliases: Record; // Hub model (Hypothesis CRUD from useInvestigationOrchestration) - suspectedCausesState: UseInvestigationOrchestrationReturn['suspectedCausesState']; + hypothesesState: UseInvestigationOrchestrationReturn['hypothesesState']; // Derived investigation data (from orchestration hook) questionsMap: Record; ideaImpacts: Record; @@ -132,7 +132,7 @@ export const InvestigationWorkspace: React.FC = ({ actionProposalsState, handleSearchKnowledge, columnAliases, - suspectedCausesState, + hypothesesState, questionsMap, ideaImpacts, viewMode: externalViewMode, @@ -344,7 +344,7 @@ export const InvestigationWorkspace: React.FC = ({ }, [factorIntelQuestions]); // ── Hub model computations (Hypothesis hubs) ─────────────────────── - const hubs = suspectedCausesState.hubs; + const hubs = hypothesesState.hubs; // Phase 13 — pan-to-node: replicate WallCanvas's deterministic layout so the // command palette can center the viewport on a hub/question by id. @@ -390,7 +390,7 @@ export const InvestigationWorkspace: React.FC = ({ const handleViewMode = onViewModeChange ?? setInternalViewMode; // Categorize questions for InvestigationConclusion - const { suspectedCauses, contributing, ruledOut } = useMemo(() => { + const { hypotheses, contributing, ruledOut } = useMemo(() => { const suspected: Question[] = []; const contrib: Question[] = []; const ruled: Question[] = []; @@ -399,7 +399,7 @@ export const InvestigationWorkspace: React.FC = ({ else if (h.causeRole === 'contributing') contrib.push(h); else if (h.causeRole === 'ruled-out') ruled.push(h); } - return { suspectedCauses: suspected, contributing: contrib, ruledOut: ruled }; + return { hypotheses: suspected, contributing: contrib, ruledOut: ruled }; }, [questionsState.questions]); const drillFactors = useMemo(() => drillPath.map(d => d.factor), [drillPath]); @@ -479,12 +479,12 @@ export const InvestigationWorkspace: React.FC = ({ findingIds: string[], branchFields: HubComposerBranchFields ) => { - const hub = suspectedCausesState.createHub(name, synthesis); - for (const qId of questionIds) suspectedCausesState.connectQuestion(hub.id, qId); - for (const fId of findingIds) suspectedCausesState.connectFinding(hub.id, fId); - if (branchFields.nextMove) suspectedCausesState.updateHub(hub.id, branchFields); + const hub = hypothesesState.createHub(name, synthesis); + for (const qId of questionIds) hypothesesState.connectQuestion(hub.id, qId); + for (const fId of findingIds) hypothesesState.connectFinding(hub.id, fId); + if (branchFields.nextMove) hypothesesState.updateHub(hub.id, branchFields); }, - [suspectedCausesState] + [hypothesesState] ); const handleUpdateHub = useCallback( @@ -496,48 +496,48 @@ export const InvestigationWorkspace: React.FC = ({ findingIds: string[], branchFields: HubComposerBranchFields ) => { - suspectedCausesState.updateHub(hubId, { name, synthesis, ...branchFields }); + hypothesesState.updateHub(hubId, { name, synthesis, ...branchFields }); // Sync connections: disconnect removed, connect added const existing = hubs.find(h => h.id === hubId); if (existing) { for (const qId of existing.questionIds) { - if (!questionIds.includes(qId)) suspectedCausesState.disconnectQuestion(hubId, qId); + if (!questionIds.includes(qId)) hypothesesState.disconnectQuestion(hubId, qId); } for (const qId of questionIds) { - if (!existing.questionIds.includes(qId)) suspectedCausesState.connectQuestion(hubId, qId); + if (!existing.questionIds.includes(qId)) hypothesesState.connectQuestion(hubId, qId); } for (const fId of existing.findingIds) { - if (!findingIds.includes(fId)) suspectedCausesState.disconnectFinding(hubId, fId); + if (!findingIds.includes(fId)) hypothesesState.disconnectFinding(hubId, fId); } for (const fId of findingIds) { - if (!existing.findingIds.includes(fId)) suspectedCausesState.connectFinding(hubId, fId); + if (!existing.findingIds.includes(fId)) hypothesesState.connectFinding(hubId, fId); } } }, - [suspectedCausesState, hubs] + [hypothesesState, hubs] ); const handleDeleteHub = useCallback( - (hubId: string) => suspectedCausesState.deleteHub(hubId), - [suspectedCausesState] + (hubId: string) => hypothesesState.deleteHub(hubId), + [hypothesesState] ); const handleToggleHubSelect = useCallback( (hubId: string) => { const hub = hubs.find(h => h.id === hubId); if (hub) { - suspectedCausesState.updateHub(hubId, {}); + hypothesesState.updateHub(hubId, {}); // Toggle selectedForImprovement via setHubStatus or direct update - // The useSuspectedCauses hook manages the selectedForImprovement toggle + // The useHypotheses hook manages the selectedForImprovement toggle // through the hub's status — but for selection we toggle the flag directly. // Since updateHub only accepts name/synthesis, use the store sync approach: const updated = hubs.map(h => h.id === hubId ? { ...h, selectedForImprovement: !h.selectedForImprovement } : h ); - suspectedCausesState.resetHubs(updated); + hypothesesState.resetHubs(updated); } }, - [suspectedCausesState, hubs] + [hypothesesState, hubs] ); const handleBrainstormHub = useCallback((_hubId: string) => { @@ -672,14 +672,14 @@ export const InvestigationWorkspace: React.FC = ({
{/* Investigation conclusion */} - {(suspectedCauses.length > 0 || ruledOut.length > 0 || hubs.length > 0) && ( + {(hypotheses.length > 0 || ruledOut.length > 0 || hubs.length > 0) && (
0 || hubs.length > 0} + hasConclusions={hypotheses.length > 0 || hubs.length > 0} problemStatementDraft={problemStatement.draft} isProblemStatementReady={problemStatement.isReady} onGenerateProblemStatement={problemStatement.generate} @@ -851,7 +851,7 @@ export const InvestigationWorkspace: React.FC = ({ causalLinks, questions: questionsState.questions, findings: findingsState.findings, - suspectedCauses: hubs, + hypotheses: hubs, }} onAskQuestion={handleMapAskQuestion} onCreateFinding={handleMapCreateFinding} diff --git a/apps/azure/src/components/editor/PISection.tsx b/apps/azure/src/components/editor/PISection.tsx index d748514cb..7e81f1a2a 100644 --- a/apps/azure/src/components/editor/PISection.tsx +++ b/apps/azure/src/components/editor/PISection.tsx @@ -105,7 +105,7 @@ export const PISection: React.FC = ({ const defectMapping = useProjectStore(s => s.defectMapping); const processContext = useProjectStore(s => s.processContext); const setProcessContext = useProjectStore(s => s.setProcessContext); - const suspectedCauses = useInvestigationStore(s => s.suspectedCauses); + const hypotheses = useInvestigationStore(s => s.hypotheses); // Panel visibility and tab state from panelsStore const isPISidebarOpen = usePanelsStore(s => s.isPISidebarOpen); @@ -170,7 +170,7 @@ export const PISection: React.FC = ({ processContext: processContext ?? undefined, questions: questionsState.questions, findings: findingsState.findings, - branches: suspectedCauses, + branches: hypotheses, }), [ rawData, @@ -183,7 +183,7 @@ export const PISection: React.FC = ({ processContext, questionsState.questions, findingsState.findings, - suspectedCauses, + hypotheses, ] ); diff --git a/apps/azure/src/components/editor/__tests__/FrameView.test.tsx b/apps/azure/src/components/editor/__tests__/FrameView.test.tsx index 237f1d4a7..a541af67a 100644 --- a/apps/azure/src/components/editor/__tests__/FrameView.test.tsx +++ b/apps/azure/src/components/editor/__tests__/FrameView.test.tsx @@ -37,7 +37,7 @@ const investigationStateRef: { current: Record } = { current: { findings: [], questions: [], - suspectedCauses: [], + hypotheses: [], causalLinks: [], addCausalLink: addCausalLinkMock, linkQuestionToCausalLink: linkQuestionToCausalLinkMock, @@ -216,7 +216,7 @@ describe('FrameView (Azure shell)', () => { investigationStateRef.current = { findings: [{ id: 'f-1' }], questions: [{ id: 'q-1' }], - suspectedCauses: [{ id: 'hub-1' }], + hypotheses: [{ id: 'hub-1' }], causalLinks: [{ id: 'link-1' }], addCausalLink: addCausalLinkMock, linkQuestionToCausalLink: linkQuestionToCausalLinkMock, @@ -241,7 +241,7 @@ describe('FrameView (Azure shell)', () => { setProcessContext: setProcessContextMock, findings: [{ id: 'f-1' }], questions: [{ id: 'q-1' }], - suspectedCauses: [{ id: 'hub-1' }], + hypotheses: [{ id: 'hub-1' }], causalLinks: [{ id: 'link-1' }], signals: { hasIntervention: false, sustainmentConfirmed: false }, }) diff --git a/apps/azure/src/components/editor/__tests__/InvestigationWorkspace.mapwall.test.tsx b/apps/azure/src/components/editor/__tests__/InvestigationWorkspace.mapwall.test.tsx index dcfc889b4..db242070d 100644 --- a/apps/azure/src/components/editor/__tests__/InvestigationWorkspace.mapwall.test.tsx +++ b/apps/azure/src/components/editor/__tests__/InvestigationWorkspace.mapwall.test.tsx @@ -228,7 +228,7 @@ function makeMinimalProps(): React.ComponentProps actionProposalsState: {} as never, handleSearchKnowledge: noOp, columnAliases: {}, - suspectedCausesState: { + hypothesesState: { hubs: [], createHub: vi.fn(() => ({ id: 'hub-1' }) as never), updateHub: noOp, @@ -292,7 +292,7 @@ describe('InvestigationWorkspace Map/Wall toggle', () => { it('renders the WallCanvas for a chart-first investigation without a process map', () => { useWallLayoutStore.getState().setViewMode('wall'); const props = makeMinimalProps(); - props.suspectedCausesState.hubs = [ + props.hypothesesState.hubs = [ { id: 'hub-1', name: 'Nozzle heat drift', diff --git a/apps/azure/src/components/views/ReportView.tsx b/apps/azure/src/components/views/ReportView.tsx index ba3f043d4..913556f23 100644 --- a/apps/azure/src/components/views/ReportView.tsx +++ b/apps/azure/src/components/views/ReportView.tsx @@ -162,7 +162,7 @@ const ReportView: React.FC = ({ const findings = useInvestigationStore(s => s.findings); const questions = useInvestigationStore(s => s.questions); const causalLinks = useInvestigationStore(s => s.causalLinks); - const suspectedCauses = useInvestigationStore(s => s.suspectedCauses); + const hypotheses = useInvestigationStore(s => s.hypotheses); // --------------------------------------------------------------------------- // Evidence Map computation for Report timeline @@ -203,14 +203,14 @@ const ReportView: React.FC = ({ causalLinks, questions, findings, - suspectedCauses: suspectedCauses ?? [], + hypotheses: hypotheses ?? [], }); const timeline = useEvidenceMapTimeline({ causalLinks, questions, findings, - suspectedCauses: suspectedCauses ?? [], + hypotheses: hypotheses ?? [], }); // --------------------------------------------------------------------------- diff --git a/apps/azure/src/components/views/__tests__/ReportView.evidenceMap.test.tsx b/apps/azure/src/components/views/__tests__/ReportView.evidenceMap.test.tsx index 2ab1b2285..0bf67a9c2 100644 --- a/apps/azure/src/components/views/__tests__/ReportView.evidenceMap.test.tsx +++ b/apps/azure/src/components/views/__tests__/ReportView.evidenceMap.test.tsx @@ -73,7 +73,7 @@ function makeQuestion(overrides?: Partial): Question { } as Question; } -function makeSuspectedCause(overrides?: Partial): Hypothesis { +function makeHypothesis(overrides?: Partial): Hypothesis { return { id: 'hub-1', name: 'Temperature hypothesis', @@ -165,7 +165,7 @@ describe('useEvidenceMapTimeline', () => { causalLinks: [], questions: [], findings: [], - suspectedCauses: [], + hypotheses: [], }) ); expect(result.current.frames).toEqual([]); @@ -296,8 +296,8 @@ describe('useEvidenceMapTimeline', () => { describe('frame content with mixed artifacts', () => { it('includes hub ID in visibleHubs when Hypothesis is provided', () => { - const hub = makeSuspectedCause({ id: 'hub-xyz' }); - const { result } = renderHook(() => useEvidenceMapTimeline({ suspectedCauses: [hub] })); + const hub = makeHypothesis({ id: 'hub-xyz' }); + const { result } = renderHook(() => useEvidenceMapTimeline({ hypotheses: [hub] })); expect(result.current.frames.length).toBeGreaterThan(0); expect(result.current.frames[result.current.frames.length - 1].visibleHubs).toContain( diff --git a/apps/azure/src/features/ai/__tests__/useInvestigationIndexing.test.ts b/apps/azure/src/features/ai/__tests__/useInvestigationIndexing.test.ts index 5f2363175..cbce5a1d9 100644 --- a/apps/azure/src/features/ai/__tests__/useInvestigationIndexing.test.ts +++ b/apps/azure/src/features/ai/__tests__/useInvestigationIndexing.test.ts @@ -13,12 +13,12 @@ const mockOnFindingsChange = vi.fn(); const mockOnQuestionsChange = vi.fn(); const mockDispose = vi.fn(); -const mockOnSuspectedCausesChange = vi.fn(); +const mockOnHypothesesChange = vi.fn(); const mockSerializerInstance = { onFindingsChange: mockOnFindingsChange, onQuestionsChange: mockOnQuestionsChange, - onHypothesesChange: mockOnSuspectedCausesChange, + onHypothesesChange: mockOnHypothesesChange, dispose: mockDispose, }; diff --git a/apps/azure/src/features/ai/actionToolHandlers.ts b/apps/azure/src/features/ai/actionToolHandlers.ts index 192688f99..66bc9677c 100644 --- a/apps/azure/src/features/ai/actionToolHandlers.ts +++ b/apps/azure/src/features/ai/actionToolHandlers.ts @@ -34,8 +34,8 @@ export interface ActionToolDeps { questions: Question[]; filters: Record; filterStack: FilterAction[]; - /** Existing suspected cause hubs — used by connect_hub_evidence handler */ - suspectedCauses?: Hypothesis[]; + /** Existing hypothesis hubs — used by connect_hub_evidence handler */ + hypotheses?: Hypothesis[]; /** Existing causal links — used by suggest_causal_link handler */ causalLinks?: CausalLink[]; /** Available factor column names — used for validation */ @@ -52,7 +52,7 @@ export function buildActionToolHandlers({ questions, filters, filterStack, - suspectedCauses, + hypotheses, causalLinks, factors, }: ActionToolDeps): Partial { @@ -388,7 +388,7 @@ export function buildActionToolHandlers({ if (!hubId) return JSON.stringify({ error: 'Missing hubId' }); - const hub = suspectedCauses?.find(h => h.id === hubId); + const hub = hypotheses?.find(h => h.id === hubId); if (!hub) return JSON.stringify({ error: `Suspected cause hub not found: ${hubId}` }); const totalNew = questionIds.length + findingIds.length; diff --git a/apps/azure/src/features/ai/useAIOrchestration.ts b/apps/azure/src/features/ai/useAIOrchestration.ts index 4fd2d9167..1390437da 100644 --- a/apps/azure/src/features/ai/useAIOrchestration.ts +++ b/apps/azure/src/features/ai/useAIOrchestration.ts @@ -165,7 +165,7 @@ export function useAIOrchestration({ // Read domain store for CoScout context (ADR-066) const causalLinks = useInvestigationStore(s => s.causalLinks); - const suspectedCauses = useInvestigationStore(s => s.suspectedCauses); + const hypotheses = useInvestigationStore(s => s.hypotheses); // Per-component preferences (default all on) const prefs = aiPreferences ?? { narration: true, insights: true, coscout: true }; @@ -223,7 +223,7 @@ export function useAIOrchestration({ }; }); - const convergencePoints = (suspectedCauses ?? []).map(hub => ({ + const convergencePoints = (hypotheses ?? []).map(hub => ({ factor: hub.name, incomingCount: hub.questionIds.length + hub.findingIds.length, hubName: hub.name, @@ -240,14 +240,7 @@ export function useAIOrchestration({ }>, convergencePoints, }; - }, [ - evidenceMapTopology, - factors, - questions, - findings, - aiVariationContributions, - suspectedCauses, - ]); + }, [evidenceMapTopology, factors, questions, findings, aiVariationContributions, hypotheses]); // Responses API config (resolved async) const [responsesConfig, setResponsesConfig] = useState(undefined); @@ -304,7 +297,7 @@ export function useAIOrchestration({ analysisMode, focusedQuestionId, evidenceMapTopology: effectiveTopology, - suspectedCauses, + hypotheses, bestSubsetsResult, }); diff --git a/apps/azure/src/features/investigation/__tests__/useWallBackgroundJobs.test.ts b/apps/azure/src/features/investigation/__tests__/useWallBackgroundJobs.test.ts index 4e6e1f9c4..8fb5b638a 100644 --- a/apps/azure/src/features/investigation/__tests__/useWallBackgroundJobs.test.ts +++ b/apps/azure/src/features/investigation/__tests__/useWallBackgroundJobs.test.ts @@ -20,8 +20,8 @@ const mockDetect = vi.mocked(detectBestSubsetsCandidates); function resetStores(): void { // Project store: set minimal shape relevant to the hook. useProjectStore.setState({ rawData: [], outcome: null }); - // Investigation store: clear suspected causes. - useInvestigationStore.setState({ suspectedCauses: [] }); + // Investigation store: clear hypotheses. + useInvestigationStore.setState({ hypotheses: [] }); // AI store: clear suggestions. useAIStore.setState({ wallSuggestions: [] }); } @@ -172,7 +172,7 @@ describe('useWallBackgroundJobs (azure)', () => { act(() => { useProjectStore.setState({ rawData: seedRows(20), outcome: 'Weight' }); useInvestigationStore.setState({ - suspectedCauses: [ + hypotheses: [ { id: 'h1', name: 'H1', diff --git a/apps/azure/src/features/investigation/useInvestigationOrchestration.ts b/apps/azure/src/features/investigation/useInvestigationOrchestration.ts index 9ca31daad..c5b665923 100644 --- a/apps/azure/src/features/investigation/useInvestigationOrchestration.ts +++ b/apps/azure/src/features/investigation/useInvestigationOrchestration.ts @@ -14,7 +14,7 @@ import { } from './investigationStore'; import type { QuestionDisplayData } from './investigationStore'; import { usePanelsStore } from '../panels/panelsStore'; -import { useSuspectedCauses, type HypothesisUpdate } from '@variscout/hooks'; +import { useHypotheses, type HypothesisUpdate } from '@variscout/hooks'; import { useInvestigationStore } from '@variscout/stores'; import type { Finding, @@ -54,8 +54,8 @@ export interface UseInvestigationOrchestrationReturn { clearProjectionTarget: () => void; /** Set finding status with automatic idea-to-action conversion */ handleSetFindingStatus: (id: string, status: FindingStatus) => void; - /** Full suspected causes hook state — hub CRUD operations for the Investigation workspace */ - suspectedCausesState: { + /** Full hypotheses hook state — hub CRUD operations for the Investigation workspace */ + hypothesesState: { hubs: Hypothesis[]; createHub: (name: string, synthesis: string) => Hypothesis; updateHub: (hubId: string, updates: HypothesisUpdate) => void; @@ -85,8 +85,8 @@ export function useInvestigationOrchestration({ }: UseInvestigationOrchestrationOptions): UseInvestigationOrchestrationReturn { // ── Suspected cause hubs ────────────────────────────────────────────── // Sync hubs to the domain store so that other components (e.g. EditorDashboardView) - // can read them via useInvestigationStore(s => s.suspectedCauses) without prop threading. - const suspectedCausesState = useSuspectedCauses({ + // can read them via useInvestigationStore(s => s.hypotheses) without prop threading. + const hypothesesState = useHypotheses({ initialHubs: [], onHubsChange: useInvestigationStore.getState().resetHubs, }); @@ -99,7 +99,7 @@ export function useInvestigationOrchestration({ const migrationDoneRef = useRef(false); useEffect(() => { if (migrationDoneRef.current) return; - if (suspectedCausesState.hubs.length > 0) { + if (hypothesesState.hubs.length > 0) { migrationDoneRef.current = true; return; } @@ -109,7 +109,7 @@ export function useInvestigationOrchestration({ if (questionsWithCauseRole.length > 0) { const migratedHubs = migrateCauseRolesToHubs(questionsWithCauseRole); if (migratedHubs.length > 0) { - suspectedCausesState.resetHubs(migratedHubs); + hypothesesState.resetHubs(migratedHubs); } } migrationDoneRef.current = true; @@ -207,7 +207,7 @@ export function useInvestigationOrchestration({ handleSaveIdeaProjection, clearProjectionTarget, handleSetFindingStatus, - suspectedCausesState, + hypothesesState, questionsMap, ideaImpacts, }; diff --git a/apps/azure/src/features/investigation/useWallBackgroundJobs.ts b/apps/azure/src/features/investigation/useWallBackgroundJobs.ts index eef0ae4ce..9360dba69 100644 --- a/apps/azure/src/features/investigation/useWallBackgroundJobs.ts +++ b/apps/azure/src/features/investigation/useWallBackgroundJobs.ts @@ -1,7 +1,7 @@ /** * useWallBackgroundJobs — background best-subsets pipeline + CoScout emit * - * Subscribes to `projectStore.rawData`/`outcome` and `investigationStore.suspectedCauses` + * Subscribes to `projectStore.rawData`/`outcome` and `investigationStore.hypotheses` * and, after a 2000ms debounce, runs `detectBestSubsetsCandidates(rows, outcome, * allColumns, citedColumns)`. Results are emitted into `aiStore.wallSuggestions` * via `upsertWallSuggestion` under a stable id (`'best-subsets'`) so repeated @@ -49,10 +49,10 @@ function deriveAllColumns(rows: Record[], outcome: string): str } function computeCitedColumns( - suspectedCauses: ReturnType['suspectedCauses'] + hypotheses: ReturnType['hypotheses'] ): string[] { const cited = new Set(); - for (const hub of suspectedCauses) { + for (const hub of hypotheses) { const condition = hub.condition; if (!condition) continue; for (const col of collectReferencedColumns(condition)) { @@ -89,7 +89,7 @@ export function useWallBackgroundJobs(): void { const allColumns = deriveAllColumns(rows, outcome); if (allColumns.length === 0) return; - const citedColumns = computeCitedColumns(investigationState.suspectedCauses); + const citedColumns = computeCitedColumns(investigationState.hypotheses); const candidates = detectBestSubsetsCandidates(rows, outcome, allColumns, citedColumns); @@ -115,9 +115,9 @@ export function useWallBackgroundJobs(): void { } }); - // Investigation store — fires on any change; filter to suspectedCauses. + // Investigation store — fires on any change; filter to hypotheses. const unsubscribeInvestigation = useInvestigationStore.subscribe((state, prev) => { - if (state.suspectedCauses !== prev.suspectedCauses) { + if (state.hypotheses !== prev.hypotheses) { scheduleRun(); } }); diff --git a/apps/azure/src/features/investigation/useWallHubCommentLifecycle.ts b/apps/azure/src/features/investigation/useWallHubCommentLifecycle.ts index a9482ac4c..8303ccdd7 100644 --- a/apps/azure/src/features/investigation/useWallHubCommentLifecycle.ts +++ b/apps/azure/src/features/investigation/useWallHubCommentLifecycle.ts @@ -12,7 +12,7 @@ * and racy against Phase 13's LOD / clustering work. * * The hook is side-effect-only (returns void). Incoming comments flow into - * investigationStore.suspectedCauses[*].comments via the shared hook. + * investigationStore.hypotheses[*].comments via the shared hook. */ import { useMemo } from 'react'; diff --git a/apps/azure/src/hooks/useDataIngestion.ts b/apps/azure/src/hooks/useDataIngestion.ts index e7b096a2e..bfb0dfcc6 100644 --- a/apps/azure/src/hooks/useDataIngestion.ts +++ b/apps/azure/src/hooks/useDataIngestion.ts @@ -44,7 +44,7 @@ export const useDataIngestion = (options?: UseDataIngestionOptions) => { setQuestions: useProjectStore(s => s.setQuestions), setCategories: useProjectStore(s => s.setCategories), setDefectMapping: useProjectStore(s => s.setDefectMapping), - setSuspectedCauses: (hubs: Hypothesis[]) => useInvestigationStore.getState().resetHubs(hubs), + setHypotheses: (hubs: Hypothesis[]) => useInvestigationStore.getState().resetHubs(hubs), setCausalLinks: (links: CausalLink[]) => useInvestigationStore.getState().loadInvestigationState({ causalLinks: links }), setProcessContext: useProjectStore(s => s.setProcessContext), diff --git a/apps/azure/src/pages/Editor.tsx b/apps/azure/src/pages/Editor.tsx index 7f9a7432a..4bde746d9 100644 --- a/apps/azure/src/pages/Editor.tsx +++ b/apps/azure/src/pages/Editor.tsx @@ -345,7 +345,7 @@ export const Editor: React.FC = ({ // Investigation store (domain — findings/questions/categories) const persistedFindings = useInvestigationStore(s => s.findings); const persistedQuestions = useInvestigationStore(s => s.questions); - const suspectedCauses = useInvestigationStore(s => s.suspectedCauses); + const hypotheses = useInvestigationStore(s => s.hypotheses); const categories = useInvestigationStore(s => s.categories); const linkFindingToQuestion = useInvestigationStore(s => s.linkFindingToQuestion); @@ -623,7 +623,7 @@ export const Editor: React.FC = ({ processContext, questions: persistedQuestions, findings: persistedFindings, - branches: suspectedCauses, + branches: hypotheses, }), [ rawData, @@ -636,7 +636,7 @@ export const Editor: React.FC = ({ processContext, persistedQuestions, persistedFindings, - suspectedCauses, + hypotheses, ] ); @@ -654,7 +654,7 @@ export const Editor: React.FC = ({ const dataFlowRef = React.useRef(dataFlow); dataFlowRef.current = dataFlow; - // Load sample passed from portfolio "Try a Sample" (effect below, after suspectedCausesState) + // Load sample passed from portfolio "Try a Sample" (effect below, after hypothesesState) const initialSampleConsumedRef = useRef(false); // Manual data analyze with append-mode merge @@ -1007,7 +1007,7 @@ export const Editor: React.FC = ({ handleSaveIdeaProjection, clearProjectionTarget, handleSetFindingStatus, - suspectedCausesState, + hypothesesState, questionsMap, ideaImpacts, } = useInvestigationOrchestration({ @@ -1023,10 +1023,10 @@ export const Editor: React.FC = ({ if (initialSample && !initialSampleConsumedRef.current) { initialSampleConsumedRef.current = true; dataFlowRef.current.handleLoadSample(initialSample); - // Inject suspected causes for showcase/demo datasets (not in DataContext) - const hubs = initialSample.config.investigation?.suspectedCauses; + // Inject hypotheses for showcase/demo datasets (not in DataContext) + const hubs = initialSample.config.investigation?.hypotheses; if (hubs && hubs.length > 0) { - suspectedCausesState.resetHubs(hubs); + hypothesesState.resetHubs(hubs); } } // eslint-disable-next-line react-hooks/exhaustive-deps -- runs once on mount @@ -1630,7 +1630,7 @@ export const Editor: React.FC = ({ onViewModeChange={(mode: 'list' | 'board' | 'tree') => handleViewStateChange({ findingsViewMode: mode }) } - suspectedCausesState={suspectedCausesState} + hypothesesState={hypothesesState} questionsMap={questionsMap} ideaImpacts={ideaImpacts} /> diff --git a/apps/azure/src/persistence/AzureHubRepository.ts b/apps/azure/src/persistence/AzureHubRepository.ts index 21506dbd6..e6d6a9e9a 100644 --- a/apps/azure/src/persistence/AzureHubRepository.ts +++ b/apps/azure/src/persistence/AzureHubRepository.ts @@ -4,7 +4,7 @@ // Unlike the PWA hub-blob, Azure stores hubs, evidence sources, evidence snapshots, // and evidence source cursors in dedicated Dexie tables (see src/db/schema.ts). // Entities that Azure does not have dedicated tables for today (investigations, findings, -// questions, causalLinks, suspectedCauses, canvas state) are stubbed — F3 normalizes +// questions, causalLinks, hypotheses, canvas state) are stubbed — F3 normalizes // them into Dexie tables. import type { @@ -174,7 +174,7 @@ export class AzureHubRepository implements HubRepository { }; hypotheses: HypothesisReadAPI = { - // Azure has no dedicated suspectedCauses table today; F3 normalizes this. + // Azure has no dedicated hypotheses table today; F3 normalizes this. async get(_id) { return undefined; }, diff --git a/apps/azure/src/persistence/__tests__/AzureHubRepository.read.test.ts b/apps/azure/src/persistence/__tests__/AzureHubRepository.read.test.ts index ad58252e8..19f4e00b0 100644 --- a/apps/azure/src/persistence/__tests__/AzureHubRepository.read.test.ts +++ b/apps/azure/src/persistence/__tests__/AzureHubRepository.read.test.ts @@ -9,7 +9,7 @@ // - evidenceSnapshots.get / listByHub — wired against db.evidenceSnapshots // - evidenceSources.get / listByHub — wired against db.evidenceSources // - evidenceSources.getCursor — wired against db.evidenceSourceCursors -// - stub read APIs (investigations, findings, questions, causalLinks, suspectedCauses) +// - stub read APIs (investigations, findings, questions, causalLinks, hypotheses) // // Mocking strategy: fake-indexeddb/auto (already a devDep) polyfills IndexedDB globally // so the real Dexie instance works end-to-end without a browser. No vi.mock needed here @@ -394,11 +394,11 @@ describe('AzureHubRepository read APIs (Dexie tables)', () => { expect(await repo.causalLinks.listByInvestigation('inv-1')).toEqual([]); }); - it('suspectedCauses.get returns undefined', async () => { + it('hypotheses.get returns undefined', async () => { expect(await repo.hypotheses.get('any')).toBeUndefined(); }); - it('suspectedCauses.listByInvestigation returns []', async () => { + it('hypotheses.listByInvestigation returns []', async () => { expect(await repo.hypotheses.listByInvestigation('inv-1')).toEqual([]); }); }); diff --git a/apps/azure/src/persistence/cascadeArchive.ts b/apps/azure/src/persistence/cascadeArchive.ts index acd902f76..a0ef9a20a 100644 --- a/apps/azure/src/persistence/cascadeArchive.ts +++ b/apps/azure/src/persistence/cascadeArchive.ts @@ -5,7 +5,7 @@ // AZURE PERSISTENCE MODEL: // Azure stores processHubs, evidenceSources, evidenceSnapshots, and // evidenceSourceCursors in dedicated Dexie tables. Investigations, findings, -// questions, causalLinks, suspectedCauses, rowProvenance, and canvasState +// questions, causalLinks, hypotheses, rowProvenance, and canvasState // have no Azure Dexie tables today — their cascade steps are documented no-ops. // // CASCADE STRATEGY: @@ -29,7 +29,7 @@ // // F3 NOTE: // Per audit R10/PWA pattern: investigation/finding/question/causalLink/ -// suspectedCause/rowProvenance/canvasState have no Azure Dexie tables today — +// hypothesis/rowProvenance/canvasState have no Azure Dexie tables today — // cascade is a no-op for those kinds. F3 normalization will add the Dexie // tables; updating this helper is part of F3. @@ -187,10 +187,10 @@ async function archiveKindRows( return; // ----------------------------------------------------------------------- - // suspectedCause — no Azure Dexie table today; F3 normalizes. + // hypothesis — no Azure Dexie table today; F3 normalizes. // ----------------------------------------------------------------------- - case 'suspectedCause': - // Azure has no 'suspectedCause' table today; F3 normalization will add + case 'hypothesis': + // Azure has no 'hypothesis' table today; F3 normalization will add // the table; updating this helper is part of F3. return; diff --git a/apps/azure/src/services/__tests__/investigationSerializer.test.ts b/apps/azure/src/services/__tests__/investigationSerializer.test.ts index 403528911..1724dd97f 100644 --- a/apps/azure/src/services/__tests__/investigationSerializer.test.ts +++ b/apps/azure/src/services/__tests__/investigationSerializer.test.ts @@ -41,7 +41,7 @@ function makeQuestion(overrides: Partial = {}): Question { }; } -function makeSuspectedCause(overrides: Partial = {}): Hypothesis { +function makeHypothesis(overrides: Partial = {}): Hypothesis { return { id: 'sc-1', name: 'Nozzle wear on night shift', @@ -275,7 +275,7 @@ describe('serializeQuestions', () => { describe('serializeHypotheses', () => { it('produces valid JSONL — one JSON object per line', () => { - const hubs = [makeSuspectedCause({ id: 'sc-1' }), makeSuspectedCause({ id: 'sc-2' })]; + const hubs = [makeHypothesis({ id: 'sc-1' }), makeHypothesis({ id: 'sc-2' })]; const jsonl = serializeHypotheses(hubs); const lines = jsonl.split('\n'); expect(lines).toHaveLength(2); @@ -285,7 +285,7 @@ describe('serializeHypotheses', () => { }); it('sets type to "hypothesis"', () => { - const jsonl = serializeHypotheses([makeSuspectedCause()]); + const jsonl = serializeHypotheses([makeHypothesis()]); const parsed = JSON.parse(jsonl); expect(parsed.type).toBe('hypothesis'); }); @@ -295,7 +295,7 @@ describe('serializeHypotheses', () => { mode: 'standard' as const, contribution: { value: 0.62, label: 'R²adj', description: 'Explains 62% of variation' }, }; - const hub = makeSuspectedCause({ + const hub = makeHypothesis({ name: 'Nozzle wear on night shift', synthesis: 'Both factors confirm the pattern.', questionIds: ['q-1', 'q-2'], @@ -313,22 +313,22 @@ describe('serializeHypotheses', () => { }); it('includes selectedForImprovement when set', () => { - const hub = makeSuspectedCause({ status: 'confirmed', selectedForImprovement: true }); + const hub = makeHypothesis({ status: 'confirmed', selectedForImprovement: true }); const parsed = JSON.parse(serializeHypotheses([hub])); expect(parsed.selectedForImprovement).toBe(true); }); it('omits selectedForImprovement when undefined', () => { - const hub = makeSuspectedCause({ status: 'confirmed', selectedForImprovement: undefined }); + const hub = makeHypothesis({ status: 'confirmed', selectedForImprovement: undefined }); const parsed = JSON.parse(serializeHypotheses([hub])); expect(parsed.selectedForImprovement).toBeUndefined(); }); it('excludes refuted hubs from Foundry IQ output', () => { const hubs = [ - makeSuspectedCause({ id: 'sc-1', status: 'proposed' }), - makeSuspectedCause({ id: 'sc-2', status: 'confirmed' }), - makeSuspectedCause({ id: 'sc-3', status: 'refuted' }), + makeHypothesis({ id: 'sc-1', status: 'proposed' }), + makeHypothesis({ id: 'sc-2', status: 'confirmed' }), + makeHypothesis({ id: 'sc-3', status: 'refuted' }), ]; const jsonl = serializeHypotheses(hubs); const lines = jsonl.split('\n'); @@ -345,8 +345,8 @@ describe('serializeHypotheses', () => { it('returns empty string when all hubs are refuted', () => { const hubs = [ - makeSuspectedCause({ status: 'refuted' }), - makeSuspectedCause({ id: 'sc-2', status: 'refuted' }), + makeHypothesis({ status: 'refuted' }), + makeHypothesis({ id: 'sc-2', status: 'refuted' }), ]; expect(serializeHypotheses(hubs)).toBe(''); }); @@ -365,43 +365,43 @@ describe('serializeInvestigationState', () => { expect(state.questions).toHaveLength(1); }); - it('includes suspectedCauses when non-empty', () => { - const hubs = [makeSuspectedCause()]; + it('includes hypotheses when non-empty', () => { + const hubs = [makeHypothesis()]; const state = serializeInvestigationState([], [], hubs); - expect(state.suspectedCauses).toHaveLength(1); - expect(state.suspectedCauses![0].name).toBe('Nozzle wear on night shift'); + expect(state.hypotheses).toHaveLength(1); + expect(state.hypotheses![0].name).toBe('Nozzle wear on night shift'); }); - it('omits suspectedCauses field when empty (compact serialization)', () => { + it('omits hypotheses field when empty (compact serialization)', () => { const state = serializeInvestigationState([makeFinding()], [], []); - expect('suspectedCauses' in state).toBe(false); + expect('hypotheses' in state).toBe(false); }); }); describe('deserializeInvestigationState', () => { it('restores findings, questions, and hubs from serialized state', () => { - const hub = makeSuspectedCause(); + const hub = makeHypothesis(); const raw = { findings: [makeFinding()], questions: [makeQuestion()], - suspectedCauses: [hub], + hypotheses: [hub], }; const result = deserializeInvestigationState(raw); expect(result.findings).toHaveLength(1); expect(result.questions).toHaveLength(1); - expect(result.suspectedCauses).toHaveLength(1); - expect(result.suspectedCauses[0].name).toBe('Nozzle wear on night shift'); + expect(result.hypotheses).toHaveLength(1); + expect(result.hypotheses[0].name).toBe('Nozzle wear on night shift'); }); it('returns empty arrays when fields are missing', () => { - const raw = { findings: [], questions: [], suspectedCauses: [] }; + const raw = { findings: [], questions: [], hypotheses: [] }; const result = deserializeInvestigationState(raw); expect(result.findings).toEqual([]); expect(result.questions).toEqual([]); - expect(result.suspectedCauses).toEqual([]); + expect(result.hypotheses).toEqual([]); }); - it('migrates legacy causeRole questions to hubs when suspectedCauses field absent', () => { + it('migrates legacy causeRole questions to hubs when hypotheses field absent', () => { const raw = { findings: [], questions: [ @@ -412,38 +412,38 @@ describe('deserializeInvestigationState', () => { }; const result = deserializeInvestigationState(raw); // Only 'suspected-cause' questions are migrated - expect(result.suspectedCauses).toHaveLength(2); - const names = result.suspectedCauses.map(h => h.name); + expect(result.hypotheses).toHaveLength(2); + const names = result.hypotheses.map(h => h.name); expect(names).toContain('Machine'); expect(names).toContain('Shift'); }); - it('returns empty suspectedCauses when old data has no causeRole questions', () => { + it('returns empty hypotheses when old data has no causeRole questions', () => { const raw = { findings: [makeFinding()], questions: [makeQuestion({ causeRole: undefined })], - // no suspectedCauses field + // no hypotheses field }; const result = deserializeInvestigationState(raw); - expect(result.suspectedCauses).toEqual([]); + expect(result.hypotheses).toEqual([]); }); - it('does not migrate when suspectedCauses field is present (even empty)', () => { + it('does not migrate when hypotheses field is present (even empty)', () => { // Even if questions have causeRole, explicit [] means migration already done const raw = { findings: [], questions: [makeQuestion({ causeRole: 'suspected-cause', factor: 'Shift' })], - suspectedCauses: [], + hypotheses: [], }; const result = deserializeInvestigationState(raw); - expect(result.suspectedCauses).toEqual([]); + expect(result.hypotheses).toEqual([]); }); it('migrates legacy totalContribution (number) to HypothesisEvidence on load', () => { const raw = { findings: [], questions: [], - suspectedCauses: [ + hypotheses: [ { id: 'sc-legacy', name: 'Legacy hub', @@ -461,8 +461,8 @@ describe('deserializeInvestigationState', () => { const result = deserializeInvestigationState( raw as unknown as import('../investigationSerializer').SerializedInvestigationState ); - expect(result.suspectedCauses).toHaveLength(1); - const hub = result.suspectedCauses[0]; + expect(result.hypotheses).toHaveLength(1); + const hub = result.hypotheses[0]; expect(hub.evidence).toEqual({ mode: 'standard', contribution: { @@ -477,13 +477,13 @@ describe('deserializeInvestigationState', () => { const raw = { findings: [], questions: [], - suspectedCauses: [ + hypotheses: [ { - ...makeSuspectedCause({ id: 'legacy-suspected' }), + ...makeHypothesis({ id: 'legacy-suspected' }), status: 'suspected', }, { - ...makeSuspectedCause({ id: 'legacy-not-confirmed' }), + ...makeHypothesis({ id: 'legacy-not-confirmed' }), status: 'not-confirmed', }, ], @@ -493,7 +493,7 @@ describe('deserializeInvestigationState', () => { raw as unknown as import('../investigationSerializer').SerializedInvestigationState ); - expect(result.suspectedCauses.map(hub => [hub.id, hub.status])).toEqual([ + expect(result.hypotheses.map(hub => [hub.id, hub.status])).toEqual([ ['legacy-suspected', 'proposed'], ['legacy-not-confirmed', 'refuted'], ]); @@ -507,7 +507,7 @@ describe('deserializeInvestigationState', () => { const raw = { findings: [], questions: [], - suspectedCauses: [ + hypotheses: [ { id: 'sc-both', name: 'Hub with both fields', @@ -526,37 +526,37 @@ describe('deserializeInvestigationState', () => { raw as unknown as import('../investigationSerializer').SerializedInvestigationState ); // evidence wins — totalContribution is ignored when evidence already present - expect(result.suspectedCauses[0].evidence).toEqual(existingEvidence); + expect(result.hypotheses[0].evidence).toEqual(existingEvidence); }); it('preserves selectedForImprovement through deserialize', () => { const raw = { findings: [], questions: [], - suspectedCauses: [{ ...makeSuspectedCause(), selectedForImprovement: true }], + hypotheses: [{ ...makeHypothesis(), selectedForImprovement: true }], }; const result = deserializeInvestigationState(raw); - expect(result.suspectedCauses[0].selectedForImprovement).toBe(true); + expect(result.hypotheses[0].selectedForImprovement).toBe(true); }); it('round-trip: serialize → deserialize → serialize → output is identical', () => { const findings = [makeFinding()]; const questions = [makeQuestion()]; - const hubs = [makeSuspectedCause()]; + const hubs = [makeHypothesis()]; const firstPass = serializeInvestigationState(findings, questions, hubs); const restored = deserializeInvestigationState(firstPass); const secondPass = serializeInvestigationState( restored.findings, restored.questions, - restored.suspectedCauses + restored.hypotheses ); expect(secondPass).toEqual(firstPass); }); it('round-trip preserves evidence field correctly', () => { - const hub = makeSuspectedCause({ + const hub = makeHypothesis({ evidence: { mode: 'capability', contribution: { @@ -568,11 +568,11 @@ describe('deserializeInvestigationState', () => { }); const firstPass = serializeInvestigationState([], [], [hub]); const restored = deserializeInvestigationState(firstPass); - expect(restored.suspectedCauses[0].evidence).toEqual(hub.evidence); + expect(restored.hypotheses[0].evidence).toEqual(hub.evidence); }); it('round-trip preserves branch nextMove and branch clue/check references', () => { - const hub = makeSuspectedCause({ + const hub = makeHypothesis({ nextMove: 'Run a late-shift temperature check.', counterFindingIds: ['f-counter'], checkQuestionIds: ['q-check'], @@ -582,12 +582,12 @@ describe('deserializeInvestigationState', () => { const secondPass = serializeInvestigationState( restored.findings, restored.questions, - restored.suspectedCauses + restored.hypotheses ); - expect(restored.suspectedCauses[0].nextMove).toBe('Run a late-shift temperature check.'); - expect(restored.suspectedCauses[0].counterFindingIds).toEqual(['f-counter']); - expect(restored.suspectedCauses[0].checkQuestionIds).toEqual(['q-check']); + expect(restored.hypotheses[0].nextMove).toBe('Run a late-shift temperature check.'); + expect(restored.hypotheses[0].counterFindingIds).toEqual(['f-counter']); + expect(restored.hypotheses[0].checkQuestionIds).toEqual(['q-check']); expect(secondPass).toEqual(firstPass); }); }); @@ -647,11 +647,11 @@ describe('createInvestigationSerializer', () => { serializer.dispose(); }); - it('debounces suspected causes uploads — only uploads once after rapid changes', async () => { + it('debounces hypotheses uploads — only uploads once after rapid changes', async () => { const uploadBlob = vi.fn().mockResolvedValue(undefined); const serializer = createInvestigationSerializer({ projectId: 'proj-sc', uploadBlob }); - const hubs = [makeSuspectedCause()]; + const hubs = [makeHypothesis()]; serializer.onHypothesesChange(hubs); serializer.onHypothesesChange(hubs); serializer.onHypothesesChange(hubs); @@ -696,12 +696,12 @@ describe('createInvestigationSerializer', () => { serializer.dispose(); }); - it('handles suspected causes upload errors silently', async () => { + it('handles hypotheses upload errors silently', async () => { const uploadBlob = vi.fn().mockRejectedValue(new Error('Blob error')); const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const serializer = createInvestigationSerializer({ projectId: 'proj-sc-err', uploadBlob }); - serializer.onHypothesesChange([makeSuspectedCause()]); + serializer.onHypothesesChange([makeHypothesis()]); await expect(vi.runAllTimersAsync()).resolves.not.toThrow(); expect(warnSpy).toHaveBeenCalledWith('[KB] Failed to serialize hypotheses:', expect.any(Error)); @@ -734,11 +734,11 @@ describe('createInvestigationSerializer', () => { expect(uploadBlob).not.toHaveBeenCalled(); }); - it('dispose clears pending suspected causes timer so upload is never called', async () => { + it('dispose clears pending hypotheses timer so upload is never called', async () => { const uploadBlob = vi.fn().mockResolvedValue(undefined); const serializer = createInvestigationSerializer({ projectId: 'proj-sc-dispose', uploadBlob }); - serializer.onHypothesesChange([makeSuspectedCause()]); + serializer.onHypothesesChange([makeHypothesis()]); serializer.dispose(); await vi.runAllTimersAsync(); @@ -762,13 +762,13 @@ describe('createInvestigationSerializer', () => { serializer.dispose(); }); - it('uploads findings, questions, and suspected causes on independent timers', async () => { + it('uploads findings, questions, and hypotheses on independent timers', async () => { const uploadBlob = vi.fn().mockResolvedValue(undefined); const serializer = createInvestigationSerializer({ projectId: 'proj-8', uploadBlob }); serializer.onFindingsChange([makeFinding()]); serializer.onQuestionsChange([makeQuestion()]); - serializer.onHypothesesChange([makeSuspectedCause()]); + serializer.onHypothesesChange([makeHypothesis()]); await vi.runAllTimersAsync(); diff --git a/apps/azure/src/services/investigationSerializer.ts b/apps/azure/src/services/investigationSerializer.ts index f7d1e6f1a..f177fd386 100644 --- a/apps/azure/src/services/investigationSerializer.ts +++ b/apps/azure/src/services/investigationSerializer.ts @@ -13,17 +13,17 @@ import { migrateCauseRolesToHubs } from '@variscout/core'; /** * Structured representation of the investigation state saved as JSON. - * The `suspectedCauses` field is optional so old project files (without hubs) + * The `hypotheses` field is optional so old project files (without hubs) * remain valid and are migrated on deserialize. */ export interface SerializedInvestigationState { findings: Finding[]; questions: Question[]; - suspectedCauses?: Hypothesis[]; + hypotheses?: Hypothesis[]; } /** - * Shape of a suspected cause as it may appear in legacy stored data, + * Shape of a hypothesis as it may appear in legacy stored data, * before the `totalContribution` → `evidence` migration. */ interface LegacyStoredHub extends Omit { @@ -41,16 +41,16 @@ function normalizeHypothesisStatus(status: LegacyStoredHub['status']): Hypothesi /** * Serialize the investigation state to a plain object suitable for JSON storage. - * `suspectedCauses` is omitted when the array is empty (compact serialization). + * `hypotheses` is omitted when the array is empty (compact serialization). */ export function serializeInvestigationState( findings: Finding[], questions: Question[], - suspectedCauses: Hypothesis[] + hypotheses: Hypothesis[] ): SerializedInvestigationState { const state: SerializedInvestigationState = { findings, questions }; - if (suspectedCauses.length > 0) { - state.suspectedCauses = suspectedCauses; + if (hypotheses.length > 0) { + state.hypotheses = hypotheses; } return state; } @@ -59,7 +59,7 @@ export function serializeInvestigationState( * Deserialize investigation state from a stored object. * * Migrations applied on load: - * 1. If `suspectedCauses` is absent but questions have `causeRole === 'suspected-cause'`, + * 1. If `hypotheses` is absent but questions have `causeRole === 'suspected-cause'`, * those questions are migrated into individual Hypothesis hubs. * 2. If a hub has `totalContribution` (legacy numeric field) but no `evidence`, * a basic `HypothesisEvidence` is synthesised from it. @@ -68,14 +68,14 @@ export function serializeInvestigationState( export function deserializeInvestigationState(raw: SerializedInvestigationState): { findings: Finding[]; questions: Question[]; - suspectedCauses: Hypothesis[]; + hypotheses: Hypothesis[]; } { const findings = raw.findings ?? []; const questions = raw.questions ?? []; - if (raw.suspectedCauses !== undefined) { + if (raw.hypotheses !== undefined) { // Data already has hubs — apply field-level migration then return - const migratedHubs = (raw.suspectedCauses as LegacyStoredHub[]).map((stored): Hypothesis => { + const migratedHubs = (raw.hypotheses as LegacyStoredHub[]).map((stored): Hypothesis => { const { totalContribution: _legacy, ...clean } = stored; const hub: Hypothesis = { ...clean, @@ -98,12 +98,12 @@ export function deserializeInvestigationState(raw: SerializedInvestigationState) return hub; }); - return { findings, questions, suspectedCauses: migratedHubs }; + return { findings, questions, hypotheses: migratedHubs }; } // No hubs in stored data — attempt migration from legacy causeRole questions const migratedHubs = migrateCauseRolesToHubs(questions); - return { findings, questions, suspectedCauses: migratedHubs }; + return { findings, questions, hypotheses: migratedHubs }; } // --------------------------------------------------------------------------- diff --git a/apps/pwa/src/components/views/FrameView.tsx b/apps/pwa/src/components/views/FrameView.tsx index 0d4a07f2c..89d90e2c3 100644 --- a/apps/pwa/src/components/views/FrameView.tsx +++ b/apps/pwa/src/components/views/FrameView.tsx @@ -44,7 +44,7 @@ const FrameView: React.FC = () => { const setProcessContext = useProjectStore(s => s.setProcessContext); const findings = useInvestigationStore(s => s.findings); const questions = useInvestigationStore(s => s.questions); - const suspectedCauses = useInvestigationStore(s => s.suspectedCauses); + const hypotheses = useInvestigationStore(s => s.hypotheses); const causalLinks = useInvestigationStore(s => s.causalLinks); const activeHubId = useSession().hub?.id ?? null; const [priorStepStats, setPriorStepStats] = @@ -151,7 +151,7 @@ const FrameView: React.FC = () => { onFocusedInvestigation={handleFocusedInvestigation} findings={findings} questions={questions} - suspectedCauses={suspectedCauses} + hypotheses={hypotheses} causalLinks={causalLinks} onOpenWall={handleOpenWall} onOpenInvestigationFocus={handleOpenInvestigationFocus} diff --git a/apps/pwa/src/components/views/InvestigationView.tsx b/apps/pwa/src/components/views/InvestigationView.tsx index 24d5d0fcf..f6e62cc67 100644 --- a/apps/pwa/src/components/views/InvestigationView.tsx +++ b/apps/pwa/src/components/views/InvestigationView.tsx @@ -101,7 +101,7 @@ const InvestigationView: React.FC = ({ () => (rawData.length > 0 ? Object.keys(rawData[0]) : undefined), [rawData] ); - const hubs = useInvestigationStore(s => s.suspectedCauses); + const hubs = useInvestigationStore(s => s.hypotheses); const wallFindings = useInvestigationStore(s => s.findings); const wallQuestions = useInvestigationStore(s => s.questions); @@ -164,7 +164,7 @@ const InvestigationView: React.FC = ({ ); // Categorize questions for InvestigationConclusion - const { suspectedCauses, contributing, ruledOut } = useMemo(() => { + const { hypotheses, contributing, ruledOut } = useMemo(() => { const suspected: Question[] = []; const contrib: Question[] = []; const ruled: Question[] = []; @@ -173,7 +173,7 @@ const InvestigationView: React.FC = ({ else if (q.causeRole === 'contributing') contrib.push(q); else if (q.causeRole === 'ruled-out') ruled.push(q); } - return { suspectedCauses: suspected, contributing: contrib, ruledOut: ruled }; + return { hypotheses: suspected, contributing: contrib, ruledOut: ruled }; }, [questionsState.questions]); const drillFactors = useMemo(() => drillPath.map(d => d.factor), [drillPath]); @@ -208,13 +208,13 @@ const InvestigationView: React.FC = ({
{/* Investigation conclusion */} - {(suspectedCauses.length > 0 || ruledOut.length > 0) && ( + {(hypotheses.length > 0 || ruledOut.length > 0) && (
0} + hasConclusions={hypotheses.length > 0} />
)} diff --git a/apps/pwa/src/components/views/__tests__/FrameView.test.tsx b/apps/pwa/src/components/views/__tests__/FrameView.test.tsx index 51c267ed2..3c3544050 100644 --- a/apps/pwa/src/components/views/__tests__/FrameView.test.tsx +++ b/apps/pwa/src/components/views/__tests__/FrameView.test.tsx @@ -36,7 +36,7 @@ const investigationStateRef: { current: Record } = { current: { findings: [], questions: [], - suspectedCauses: [], + hypotheses: [], causalLinks: [], addCausalLink: addCausalLinkMock, linkQuestionToCausalLink: linkQuestionToCausalLinkMock, @@ -219,7 +219,7 @@ describe('FrameView (PWA shell)', () => { investigationStateRef.current = { findings: [{ id: 'f-1' }], questions: [{ id: 'q-1' }], - suspectedCauses: [{ id: 'hub-1' }], + hypotheses: [{ id: 'hub-1' }], causalLinks: [{ id: 'link-1' }], addCausalLink: addCausalLinkMock, linkQuestionToCausalLink: linkQuestionToCausalLinkMock, @@ -244,7 +244,7 @@ describe('FrameView (PWA shell)', () => { setProcessContext: setProcessContextMock, findings: [{ id: 'f-1' }], questions: [{ id: 'q-1' }], - suspectedCauses: [{ id: 'hub-1' }], + hypotheses: [{ id: 'hub-1' }], causalLinks: [{ id: 'link-1' }], signals: { hasIntervention: false, sustainmentConfirmed: false }, }) diff --git a/apps/pwa/src/db/schema.ts b/apps/pwa/src/db/schema.ts index 4299c7b1b..d2e711ffa 100644 --- a/apps/pwa/src/db/schema.ts +++ b/apps/pwa/src/db/schema.ts @@ -21,9 +21,9 @@ // // All other tables (evidenceSnapshots, evidenceSources, evidenceSourceCursors, // rowProvenance, investigations, findings, questions, causalLinks, -// suspectedCauses) start empty in F3 — the dispatch boundary will be wired by +// hypotheses) start empty in F3 — the dispatch boundary will be wired by // F3.5 (evidence) and F5 (investigation/finding/question/causalLink/ -// suspectedCause + canvas action coverage). +// hypothesis + canvas action coverage). // // Spec: docs/superpowers/specs/2026-05-06-data-flow-foundation-design.md §3 D3, §5 @@ -77,7 +77,7 @@ export type InvestigationRow = ProcessHubInvestigation; export type FindingRow = Finding; export type QuestionRow = Question; export type CausalLinkRow = CausalLink; -export type SuspectedCauseRow = Hypothesis; +export type HypothesisRow = Hypothesis; // --------------------------------------------------------------------------- // Database @@ -94,7 +94,7 @@ export class PwaDatabase extends Dexie { findings!: Table; questions!: Table; causalLinks!: Table; - suspectedCauses!: Table; + hypotheses!: Table; canvasState!: Table; meta!: Table; @@ -111,7 +111,7 @@ export class PwaDatabase extends Dexie { findings: '&id, investigationId, deletedAt', questions: '&id, investigationId, deletedAt', causalLinks: '&id, investigationId, deletedAt', - suspectedCauses: '&id, investigationId, deletedAt', + hypotheses: '&id, investigationId, deletedAt', canvasState: '&hubId', meta: '&key', }); diff --git a/apps/pwa/src/features/investigation/useInvestigationOrchestration.ts b/apps/pwa/src/features/investigation/useInvestigationOrchestration.ts index d33e8acbb..b264990a3 100644 --- a/apps/pwa/src/features/investigation/useInvestigationOrchestration.ts +++ b/apps/pwa/src/features/investigation/useInvestigationOrchestration.ts @@ -14,7 +14,7 @@ import { } from './investigationStore'; import type { QuestionDisplayData } from './investigationStore'; import { usePanelsStore } from '../panels/panelsStore'; -import { useSuspectedCauses, type HypothesisUpdate } from '@variscout/hooks'; +import { useHypotheses, type HypothesisUpdate } from '@variscout/hooks'; import type { Finding, FindingProjection, @@ -53,8 +53,8 @@ export interface UseInvestigationOrchestrationReturn { clearProjectionTarget: () => void; /** Set finding status with automatic idea-to-action conversion */ handleSetFindingStatus: (id: string, status: FindingStatus) => void; - /** Full suspected causes hook state — hub CRUD operations for the Investigation workspace */ - suspectedCausesState: { + /** Full hypotheses hook state — hub CRUD operations for the Investigation workspace */ + hypothesesState: { hubs: Hypothesis[]; createHub: (name: string, synthesis: string) => Hypothesis; updateHub: (hubId: string, updates: HypothesisUpdate) => void; @@ -83,7 +83,7 @@ export function useInvestigationOrchestration({ stats, }: UseInvestigationOrchestrationOptions): UseInvestigationOrchestrationReturn { // ── Suspected cause hubs ────────────────────────────────────────────── - const suspectedCausesState = useSuspectedCauses({ + const hypothesesState = useHypotheses({ initialHubs: [], }); @@ -95,7 +95,7 @@ export function useInvestigationOrchestration({ const migrationDoneRef = useRef(false); useEffect(() => { if (migrationDoneRef.current) return; - if (suspectedCausesState.hubs.length > 0) { + if (hypothesesState.hubs.length > 0) { migrationDoneRef.current = true; return; } @@ -105,7 +105,7 @@ export function useInvestigationOrchestration({ if (questionsWithCauseRole.length > 0) { const migratedHubs = migrateCauseRolesToHubs(questionsWithCauseRole); if (migratedHubs.length > 0) { - suspectedCausesState.resetHubs(migratedHubs); + hypothesesState.resetHubs(migratedHubs); } } migrationDoneRef.current = true; @@ -200,7 +200,7 @@ export function useInvestigationOrchestration({ handleSaveIdeaProjection, clearProjectionTarget, handleSetFindingStatus, - suspectedCausesState, + hypothesesState, questionsMap, ideaImpacts, }; diff --git a/apps/pwa/src/hooks/useDataIngestion.ts b/apps/pwa/src/hooks/useDataIngestion.ts index e59bdfca6..408f0a4c5 100644 --- a/apps/pwa/src/hooks/useDataIngestion.ts +++ b/apps/pwa/src/hooks/useDataIngestion.ts @@ -39,7 +39,7 @@ export const useDataIngestion = (options?: UseDataIngestionOptions) => { setFindings: useProjectStore(s => s.setFindings), setQuestions: useProjectStore(s => s.setQuestions), setCategories: useProjectStore(s => s.setCategories), - setSuspectedCauses: (hubs: Hypothesis[]) => useInvestigationStore.getState().resetHubs(hubs), + setHypotheses: (hubs: Hypothesis[]) => useInvestigationStore.getState().resetHubs(hubs), setCausalLinks: (links: CausalLink[]) => useInvestigationStore.getState().loadInvestigationState({ causalLinks: links }), setProcessContext: useProjectStore(s => s.setProcessContext), diff --git a/apps/pwa/src/persistence/PwaHubRepository.ts b/apps/pwa/src/persistence/PwaHubRepository.ts index f837e37c7..76c9aab72 100644 --- a/apps/pwa/src/persistence/PwaHubRepository.ts +++ b/apps/pwa/src/persistence/PwaHubRepository.ts @@ -19,7 +19,7 @@ // - `outcomes.get` / `outcomes.listByHub` filter by `deletedAt === null`. // - `canvasState.getByHub` returns the row, stripped of the `hubId` FK. // - `evidenceSnapshots` / `evidenceSources` / `investigations` / -// `findings` / `questions` / `causalLinks` / `suspectedCauses` query the +// `findings` / `questions` / `causalLinks` / `hypotheses` query the // real (empty) tables. Until F3.5 (evidence) and F5 (investigation // entities) wire writes, these consistently return empty rows. // @@ -241,15 +241,12 @@ export class PwaHubRepository implements HubRepository { hypotheses: HypothesisReadAPI = { get: async id => { - const row = await db.suspectedCauses.get(id); + const row = await db.hypotheses.get(id); if (!row || row.deletedAt !== null) return undefined; return row; }, listByInvestigation: async investigationId => { - const rows = await db.suspectedCauses - .where('investigationId') - .equals(investigationId) - .toArray(); + const rows = await db.hypotheses.where('investigationId').equals(investigationId).toArray(); return rows.filter(r => r.deletedAt === null); }, }; diff --git a/apps/pwa/src/persistence/__tests__/PwaHubRepository.test.ts b/apps/pwa/src/persistence/__tests__/PwaHubRepository.test.ts index 9accec63b..4c1a71e6a 100644 --- a/apps/pwa/src/persistence/__tests__/PwaHubRepository.test.ts +++ b/apps/pwa/src/persistence/__tests__/PwaHubRepository.test.ts @@ -118,7 +118,7 @@ beforeEach(async () => { db.findings.clear(), db.questions.clear(), db.causalLinks.clear(), - db.suspectedCauses.clear(), + db.hypotheses.clear(), ]); }); @@ -462,7 +462,7 @@ describe('PwaHubRepository — stub read APIs (empty tables until F3.5/F5)', () expect(await repo.causalLinks.listByInvestigation('inv-x')).toEqual([]); }); - it('suspectedCauses.listByInvestigation returns []', async () => { + it('hypotheses.listByInvestigation returns []', async () => { const repo = new PwaHubRepository(); expect(await repo.hypotheses.listByInvestigation('inv-x')).toEqual([]); }); diff --git a/apps/pwa/src/persistence/__tests__/applyAction.test.ts b/apps/pwa/src/persistence/__tests__/applyAction.test.ts index d941d5275..bd1991bbc 100644 --- a/apps/pwa/src/persistence/__tests__/applyAction.test.ts +++ b/apps/pwa/src/persistence/__tests__/applyAction.test.ts @@ -476,7 +476,7 @@ describe('applyAction — no-op action kinds', () => { hypothesis: { id: 'sc-x' }, } as unknown as HubAction); - expect(await db.suspectedCauses.count()).toBe(0); + expect(await db.hypotheses.count()).toBe(0); }); it('canvas action (ADD_STEP) does not mutate any table', async () => { diff --git a/packages/core/src/__tests__/exports.nodeCapability.test.ts b/packages/core/src/__tests__/exports.nodeCapability.test.ts index a540fb643..da7b8bb0a 100644 --- a/packages/core/src/__tests__/exports.nodeCapability.test.ts +++ b/packages/core/src/__tests__/exports.nodeCapability.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; describe('@variscout/core exports — nodeCapability surface', () => { - it('exposes calculateNodeCapability via the stats sub-path', async () => { + it('exposes calculateNodeCapability via the stats sub-path', { timeout: 20_000 }, async () => { const stats = await import('@variscout/core/stats'); expect(typeof stats.calculateNodeCapability).toBe('function'); expect(typeof stats.lookupSpecRule).toBe('function'); @@ -10,7 +10,7 @@ describe('@variscout/core exports — nodeCapability surface', () => { expect(typeof stats.suggestNodeMappings).toBe('function'); }); - it('exposes thresholds and the helper surface', async () => { + it('exposes thresholds and the helper surface', { timeout: 20_000 }, async () => { const stats = await import('@variscout/core/stats'); expect(stats.SAMPLE_CONFIDENCE_THRESHOLDS.review).toBe(30); expect(typeof stats.ruleMatches).toBe('function'); diff --git a/packages/core/src/__tests__/stress.test.ts b/packages/core/src/__tests__/stress.test.ts index 6adc876a7..4edf117b6 100644 --- a/packages/core/src/__tests__/stress.test.ts +++ b/packages/core/src/__tests__/stress.test.ts @@ -297,7 +297,7 @@ describe('Timing budgets', () => { expect(durationMs).toBeLessThan(1000); }); - it('getEtaSquared(50K rows) < 500ms', { timeout: 30_000 }, () => { + it('getEtaSquared(50K rows) < 1500ms', { timeout: 30_000 }, () => { const data = generateStressData({ rowCount: 50000, factors: [{ name: 'Group', levels: 50 }], @@ -305,7 +305,7 @@ describe('Timing budgets', () => { }); const { durationMs } = timedExec(() => getEtaSquared(data, 'Group', 'Value')); - expect(durationMs).toBeLessThan(500); + expect(durationMs).toBeLessThan(1500); }); it('calculateKDE(10K values) < 1000ms', { timeout: 30_000 }, () => { diff --git a/packages/core/src/ai/__tests__/coScoutContext.test.ts b/packages/core/src/ai/__tests__/coScoutContext.test.ts index 54b200ddb..30fe3f645 100644 --- a/packages/core/src/ai/__tests__/coScoutContext.test.ts +++ b/packages/core/src/ai/__tests__/coScoutContext.test.ts @@ -74,10 +74,10 @@ describe('formatInvestigationContext', () => { expect(result).toContain('1 ruled-out'); }); - it('uses ONLY hypothesisHubs, not legacy suspectedCauses', () => { + it('uses ONLY hypothesisHubs, not legacy hypotheses', () => { const result = formatInvestigationContext({ // Legacy causeRole-based hypotheses — should be IGNORED - suspectedCauses: [ + hypotheses: [ { id: 'sc1', text: 'Temperature drift', @@ -512,7 +512,7 @@ describe('formatKnowledgeContext', () => { etaSquared: 0.35, cpkBefore: 0.8, cpkAfter: 1.5, - suspectedCause: 'Dark roast moisture retention', + hypothesis: 'Dark roast moisture retention', actionsText: 'Reduced roast time by 15%', outcomeEffective: true, }, @@ -553,7 +553,7 @@ describe('formatKnowledgeContext', () => { etaSquared: null, cpkBefore: null, cpkAfter: null, - suspectedCause: 'test', + hypothesis: 'test', actionsText: '', outcomeEffective: null, }, diff --git a/packages/core/src/ai/__tests__/promptTemplates.test.ts b/packages/core/src/ai/__tests__/promptTemplates.test.ts index 4c8f8d998..119a446f8 100644 --- a/packages/core/src/ai/__tests__/promptTemplates.test.ts +++ b/packages/core/src/ai/__tests__/promptTemplates.test.ts @@ -1026,7 +1026,7 @@ describe('formatKnowledgeContext', () => { etaSquared: 0.42, cpkBefore: 0.85, cpkAfter: 1.45, - suspectedCause: 'Nozzle blockage', + hypothesis: 'Nozzle blockage', actionsText: 'Replaced nozzle weekly', outcomeEffective: true, }, @@ -1049,7 +1049,7 @@ describe('formatKnowledgeContext', () => { etaSquared: null, cpkBefore: null, cpkAfter: null, - suspectedCause: '', + hypothesis: '', actionsText: '', outcomeEffective: null, }, @@ -1071,7 +1071,7 @@ describe('formatKnowledgeContext', () => { etaSquared: 0.3, cpkBefore: null, cpkAfter: null, - suspectedCause: 'Cause A', + hypothesis: 'Cause A', actionsText: '', outcomeEffective: null, }, @@ -1082,7 +1082,7 @@ describe('formatKnowledgeContext', () => { etaSquared: null, cpkBefore: null, cpkAfter: null, - suspectedCause: 'Cause B', + hypothesis: 'Cause B', actionsText: '', outcomeEffective: false, }, @@ -1102,7 +1102,7 @@ describe('formatKnowledgeContext', () => { etaSquared: 0.42, cpkBefore: 0.85, cpkAfter: 1.45, - suspectedCause: 'Nozzle blockage', + hypothesis: 'Nozzle blockage', actionsText: 'Replaced nozzle', outcomeEffective: true, }, @@ -1230,7 +1230,7 @@ describe('buildCoScoutMessages', () => { etaSquared: 0.42, cpkBefore: 0.85, cpkAfter: 1.45, - suspectedCause: 'Nozzle blockage', + hypothesis: 'Nozzle blockage', actionsText: 'Replaced nozzle weekly', outcomeEffective: true, }, @@ -1268,7 +1268,7 @@ describe('buildCoScoutMessages', () => { etaSquared: 0.42, cpkBefore: 0.85, cpkAfter: 1.45, - suspectedCause: 'Nozzle blockage', + hypothesis: 'Nozzle blockage', actionsText: 'Replaced nozzle weekly', outcomeEffective: true, }, diff --git a/packages/core/src/ai/buildAIContext.ts b/packages/core/src/ai/buildAIContext.ts index 26abbbd51..f1d05ce92 100644 --- a/packages/core/src/ai/buildAIContext.ts +++ b/packages/core/src/ai/buildAIContext.ts @@ -99,7 +99,7 @@ export interface BuildAIContextOptions { /** Current analysis mode */ analysisMode?: AnalysisMode; /** Suspected cause hubs from investigation */ - suspectedCauses?: Hypothesis[]; + hypotheses?: Hypothesis[]; /** R²adj-weighted coverage percentage (0-100) */ coveragePercent?: number; /** Number of questions that have been checked (answered or ruled-out) */ @@ -481,7 +481,7 @@ export function buildAIContext(options: BuildAIContextOptions): AIContext { context.investigation.phase = detectInvestigationPhase(questions, findings); // Collect ALL questions with a causeRole (suspected-cause, contributing, ruled-out) - const suspectedCauses = questions + const hypotheses = questions .filter(q => q.causeRole) .map(q => ({ id: q.id, @@ -491,8 +491,8 @@ export function buildAIContext(options: BuildAIContextOptions): AIContext { status: q.status, evidence: q.evidence, })); - if (suspectedCauses.length > 0) { - context.investigation.suspectedCauses = suspectedCauses; + if (hypotheses.length > 0) { + context.investigation.hypotheses = hypotheses; } // Wire focused question from PI panel @@ -509,8 +509,8 @@ export function buildAIContext(options: BuildAIContextOptions): AIContext { } // Hypothesis hubs (Phase 6 — CoScout as Investigation Partner) - if (options.suspectedCauses && options.suspectedCauses.length > 0) { - context.investigation.hypothesisHubs = options.suspectedCauses.map(hub => ({ + if (options.hypotheses && options.hypotheses.length > 0) { + context.investigation.hypothesisHubs = options.hypotheses.map(hub => ({ id: hub.id, name: hub.name, synthesis: hub.synthesis, diff --git a/packages/core/src/ai/prompts/coScout/context/investigation.ts b/packages/core/src/ai/prompts/coScout/context/investigation.ts index a7e765ed3..1f47b9d78 100644 --- a/packages/core/src/ai/prompts/coScout/context/investigation.ts +++ b/packages/core/src/ai/prompts/coScout/context/investigation.ts @@ -3,7 +3,7 @@ * * Formats investigation state into human-readable text blocks. * CRITICAL: Uses ONLY hypothesisHubs — ignores legacy causeRole-based - * suspectedCauses from question fields (contradiction resolution #1). + * hypotheses from question fields (contradiction resolution #1). */ import type { AIContext } from '../../../types'; diff --git a/packages/core/src/ai/prompts/coScout/context/knowledgeContext.ts b/packages/core/src/ai/prompts/coScout/context/knowledgeContext.ts index a954614fb..acb3f5d0f 100644 --- a/packages/core/src/ai/prompts/coScout/context/knowledgeContext.ts +++ b/packages/core/src/ai/prompts/coScout/context/knowledgeContext.ts @@ -26,7 +26,7 @@ export function formatKnowledgeContext( if (results.length > 0) { const lines = results.map((r, i) => { const parts = [ - `${i + 1}. [From: findings] "${r.suspectedCause || 'Unknown cause'}" — ${r.projectName}`, + `${i + 1}. [From: findings] "${r.hypothesis || 'Unknown cause'}" — ${r.projectName}`, ]; parts.push(` Factor: ${r.factor}, Status: ${r.status}`); if (r.etaSquared !== null) diff --git a/packages/core/src/ai/prompts/coScout/legacy.ts b/packages/core/src/ai/prompts/coScout/legacy.ts index 33b5186cb..2f15bdd46 100644 --- a/packages/core/src/ai/prompts/coScout/legacy.ts +++ b/packages/core/src/ai/prompts/coScout/legacy.ts @@ -999,8 +999,8 @@ Never invent data or statistics. If the context does not contain enough informat // Override converging/improving with hypothesis context when available // Supports multiple hypotheses from question-driven investigation - if (investigation.suspectedCauses && investigation.suspectedCauses.length > 0) { - const causes = investigation.suspectedCauses; + if (investigation.hypotheses && investigation.hypotheses.length > 0) { + const causes = investigation.hypotheses; // Group by causeRole const suspected = causes.filter(c => c.causeRole === 'suspected-cause'); @@ -1009,7 +1009,7 @@ Never invent data or statistics. If the context does not contain enough informat const parts: string[] = []; if (suspected.length > 0) { - parts.push(`Suspected causes: ${suspected.map(c => `"${c.text}"`).join(', ')}`); + parts.push(`Hypotheses: ${suspected.map(c => `"${c.text}"`).join(', ')}`); } if (contributing.length > 0) { parts.push(`Contributing: ${contributing.map(c => `"${c.text}"`).join(', ')}`); @@ -1439,7 +1439,7 @@ export function formatKnowledgeContext( if (results.length > 0) { const lines = results.map((r, i) => { const parts = [ - `${i + 1}. [From: findings] "${r.suspectedCause || 'Unknown cause'}" — ${r.projectName}`, + `${i + 1}. [From: findings] "${r.hypothesis || 'Unknown cause'}" — ${r.projectName}`, ]; parts.push(` Factor: ${r.factor}, Status: ${r.status}`); if (r.etaSquared !== null) diff --git a/packages/core/src/ai/types.ts b/packages/core/src/ai/types.ts index 8c3ae9d2d..c21cf250d 100644 --- a/packages/core/src/ai/types.ts +++ b/packages/core/src/ai/types.ts @@ -119,8 +119,8 @@ export interface ProcessContext { currentUnderstanding?: CurrentUnderstanding; /** Measurable problem condition derived from the target metric and current/target values */ problemCondition?: ProblemCondition; - /** Suspected causes from question-driven investigation, ranked by evidence */ - suspectedCauses?: Array<{ + /** Hypotheses from question-driven investigation, ranked by evidence */ + hypotheses?: Array<{ factor: string; level?: string; evidence: number; @@ -308,8 +308,8 @@ export interface AIContext { transitionReason?: string; /** Investigation categories for completeness prompting */ categories?: Array<{ name: string; factorNames: string[] }>; - /** Suspected causes from questions with causeRole (supports multiple) */ - suspectedCauses?: Array<{ + /** Hypotheses from questions with causeRole (supports multiple) */ + hypotheses?: Array<{ id: string; text: string; causeRole: 'suspected-cause' | 'contributing' | 'ruled-out'; @@ -328,7 +328,7 @@ export interface AIContext { focusedQuestionId?: string; /** Text of the question currently in focus in the PI panel */ focusedQuestionText?: string; - /** Hypothesis hubs (Phase 6 — distinct from legacy causeRole-based suspectedCauses) */ + /** Hypothesis hubs (Phase 6 — distinct from legacy causeRole-based hypotheses) */ hypothesisHubs?: Array<{ id: string; name: string; @@ -440,7 +440,7 @@ export interface AIContext { etaSquared: number | null; cpkBefore: number | null; cpkAfter: number | null; - suspectedCause: string; + hypothesis: string; actionsText: string; outcomeEffective: boolean | null; }>; diff --git a/packages/core/src/findings/__tests__/findingsBarrel.test.ts b/packages/core/src/findings/__tests__/findingsBarrel.test.ts index f7219cd50..6ad4774a7 100644 --- a/packages/core/src/findings/__tests__/findingsBarrel.test.ts +++ b/packages/core/src/findings/__tests__/findingsBarrel.test.ts @@ -1,9 +1,13 @@ import { describe, it, expect } from 'vitest'; describe('findings sub-path barrel', () => { - it('re-exports HypothesisCondition from hypothesisCondition.ts', async () => { - const mod = await import('../index'); - // Type-only module — verify it loads without error. - expect(mod).toBeDefined(); - }); + it( + 're-exports HypothesisCondition from hypothesisCondition.ts', + { timeout: 20_000 }, + async () => { + const mod = await import('../index'); + // Type-only module — verify it loads without error. + expect(mod).toBeDefined(); + } + ); }); diff --git a/packages/core/src/findings/__tests__/problemStatement.test.ts b/packages/core/src/findings/__tests__/problemStatement.test.ts index 2a6a81097..1bfe2f28b 100644 --- a/packages/core/src/findings/__tests__/problemStatement.test.ts +++ b/packages/core/src/findings/__tests__/problemStatement.test.ts @@ -8,7 +8,7 @@ describe('buildProblemStatement', () => { targetValue: 1.33, currentCpk: 0.62, targetDirection: 'reduce-variation', - suspectedCauses: [ + hypotheses: [ { factor: 'Shift', level: 'Night', evidence: 0.34 }, { factor: 'Head', level: '5-8', evidence: 0.22 }, ], @@ -27,7 +27,7 @@ describe('buildProblemStatement', () => { const result = buildProblemStatement({ outcome: 'Yield', targetDirection: 'increase', - suspectedCauses: [{ factor: 'Temperature' }], + hypotheses: [{ factor: 'Temperature' }], }); expect(result).toContain('Increase Yield'); }); @@ -36,7 +36,7 @@ describe('buildProblemStatement', () => { const result = buildProblemStatement({ outcome: 'Defect Rate', targetDirection: 'decrease', - suspectedCauses: [{ factor: 'Speed' }], + hypotheses: [{ factor: 'Speed' }], }); expect(result).toContain('Decrease Defect Rate'); }); @@ -44,7 +44,7 @@ describe('buildProblemStatement', () => { it('handles no target value', () => { const result = buildProblemStatement({ outcome: 'Cycle Time', - suspectedCauses: [{ factor: 'Machine', evidence: 0.15 }], + hypotheses: [{ factor: 'Machine', evidence: 0.15 }], }); expect(result).toContain('Cycle Time'); expect(result).toContain('Machine'); @@ -56,7 +56,7 @@ describe('buildProblemStatement', () => { const result = buildProblemStatement({ outcome: 'Diameter', targetValue: 1.5, - suspectedCauses: [], + hypotheses: [], }); expect(result).toContain('to target 1.50'); expect(result).not.toContain('Cpk'); @@ -65,7 +65,7 @@ describe('buildProblemStatement', () => { it('handles no hypotheses', () => { const result = buildProblemStatement({ outcome: 'Weight', - suspectedCauses: [], + hypotheses: [], }); expect(result).toContain('Weight'); expect(result).not.toContain('driven by'); @@ -74,7 +74,7 @@ describe('buildProblemStatement', () => { it('includes evidence percentages', () => { const result = buildProblemStatement({ outcome: 'Diameter', - suspectedCauses: [{ factor: 'Tool', evidence: 0.28 }], + hypotheses: [{ factor: 'Tool', evidence: 0.28 }], }); expect(result).toContain('28%'); }); @@ -82,7 +82,7 @@ describe('buildProblemStatement', () => { it('includes factor level in parentheses', () => { const result = buildProblemStatement({ outcome: 'Weight', - suspectedCauses: [{ factor: 'Operator', level: 'B', evidence: 0.4 }], + hypotheses: [{ factor: 'Operator', level: 'B', evidence: 0.4 }], }); expect(result).toContain('Operator (B)'); expect(result).toContain('[40%]'); @@ -91,7 +91,7 @@ describe('buildProblemStatement', () => { it('handles factor without level or evidence', () => { const result = buildProblemStatement({ outcome: 'Pressure', - suspectedCauses: [{ factor: 'Valve' }], + hypotheses: [{ factor: 'Valve' }], }); expect(result).toContain('driven by Valve'); expect(result).not.toContain('('); @@ -101,7 +101,7 @@ describe('buildProblemStatement', () => { it('defaults to reduce-variation when no direction specified', () => { const result = buildProblemStatement({ outcome: 'Length', - suspectedCauses: [], + hypotheses: [], }); expect(result).toContain('Reduce variation in Length'); }); @@ -111,7 +111,7 @@ describe('buildProblemStatement', () => { const result = buildProblemStatement({ outcome: 'Fill Weight', characteristicType: 'nominal', - suspectedCauses: [], + hypotheses: [], }); expect(result).toContain('Reduce variation in Fill Weight'); }); @@ -120,7 +120,7 @@ describe('buildProblemStatement', () => { const result = buildProblemStatement({ outcome: 'Defect Rate', characteristicType: 'smaller', - suspectedCauses: [], + hypotheses: [], }); expect(result).toContain('Decrease Defect Rate'); }); @@ -129,7 +129,7 @@ describe('buildProblemStatement', () => { const result = buildProblemStatement({ outcome: 'Yield', characteristicType: 'larger', - suspectedCauses: [], + hypotheses: [], }); expect(result).toContain('Increase Yield'); }); @@ -139,7 +139,7 @@ describe('buildProblemStatement', () => { outcome: 'Cycle Time', targetDirection: 'decrease', characteristicType: 'nominal', - suspectedCauses: [], + hypotheses: [], }); expect(result).toContain('Decrease Cycle Time'); expect(result).not.toContain('Reduce variation in'); @@ -148,7 +148,7 @@ describe('buildProblemStatement', () => { it('defaults to reduce-variation when neither targetDirection nor characteristicType is provided', () => { const result = buildProblemStatement({ outcome: 'Pressure', - suspectedCauses: [], + hypotheses: [], }); expect(result).toContain('Reduce variation in Pressure'); }); diff --git a/packages/core/src/findings/index.ts b/packages/core/src/findings/index.ts index d1306e8d8..d58347c16 100644 --- a/packages/core/src/findings/index.ts +++ b/packages/core/src/findings/index.ts @@ -40,7 +40,7 @@ export type { MechanismBranchProcessContext, MechanismBranchProjectionOptions, MechanismBranchQuestionView, - MechanismBranchReadinessView, + MechanismBranchActionStateView, MechanismBranchViewModel, BranchSignalWarning, } from './mechanismBranch'; diff --git a/packages/core/src/findings/mechanismBranch.ts b/packages/core/src/findings/mechanismBranch.ts index 6a6f7898c..0d945bf1a 100644 --- a/packages/core/src/findings/mechanismBranch.ts +++ b/packages/core/src/findings/mechanismBranch.ts @@ -24,7 +24,7 @@ export interface MechanismBranchClueView { validationStatus?: Finding['validationStatus']; } -export interface MechanismBranchReadinessView { +export interface MechanismBranchActionStateView { value: 'not-tested' | 'needs-check' | 'evidence-backed' | 'ready-to-act' | 'closed'; label: string; } @@ -36,7 +36,7 @@ export interface MechanismBranchViewModel { synthesis: string; status: HypothesisStatus; branchStatus: HypothesisStatus; - readiness: MechanismBranchReadinessView; + readiness: MechanismBranchActionStateView; nextMove?: string; supportingClues: MechanismBranchClueView[]; counterClues: MechanismBranchClueView[]; @@ -56,7 +56,7 @@ export interface MechanismBranchProjectionOptions { signalCards?: SignalCard[]; } -const READINESS_LABELS: Record = { +const READINESS_LABELS: Record = { 'not-tested': 'Not tested', 'needs-check': 'Needs check', 'evidence-backed': 'Evidence backed', @@ -92,7 +92,7 @@ function deriveReadiness( supportingClues: MechanismBranchClueView[], counterClues: MechanismBranchClueView[], openChecks: MechanismBranchQuestionView[] -): MechanismBranchReadinessView { +): MechanismBranchActionStateView { if (hub.status === 'refuted') return { value: 'closed', label: READINESS_LABELS.closed }; if (hub.status === 'confirmed') { return { value: 'ready-to-act', label: READINESS_LABELS['ready-to-act'] }; diff --git a/packages/core/src/findings/problemStatement.ts b/packages/core/src/findings/problemStatement.ts index 9bf6df1f2..929791d28 100644 --- a/packages/core/src/findings/problemStatement.ts +++ b/packages/core/src/findings/problemStatement.ts @@ -28,8 +28,8 @@ export interface ProblemStatementInput { characteristicType?: CharacteristicType; /** Current Cpk (shown alongside target for context) */ currentCpk?: number; - /** Suspected causes identified during investigation */ - suspectedCauses: Array<{ + /** Hypotheses identified during investigation */ + hypotheses: Array<{ factor: string; level?: string; /** eta-squared or R²adj percentage (0-1 scale) */ @@ -96,8 +96,8 @@ export function buildProblemStatement(input: ProblemStatementInput): string { parts.push(measurePart); // Q3: Scope (hypotheses) - if (input.suspectedCauses.length > 0) { - const causeDescriptions = input.suspectedCauses.map(c => { + if (input.hypotheses.length > 0) { + const causeDescriptions = input.hypotheses.map(c => { let desc = c.factor; if (c.level) desc += ` (${c.level})`; if (c.evidence != null) desc += ` [${formatStatistic(c.evidence * 100, 'en', 0)}%]`; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9c597ff2f..70cfe5893 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -774,7 +774,7 @@ export type { MechanismBranchProcessContext, MechanismBranchProjectionOptions, MechanismBranchQuestionView, - MechanismBranchReadinessView, + MechanismBranchActionStateView, MechanismBranchViewModel, // Causal link types (investigation DAG) CausalLink, diff --git a/packages/core/src/persistence/__tests__/cascadeRules.test.ts b/packages/core/src/persistence/__tests__/cascadeRules.test.ts index 0443f962d..d5d1cf15b 100644 --- a/packages/core/src/persistence/__tests__/cascadeRules.test.ts +++ b/packages/core/src/persistence/__tests__/cascadeRules.test.ts @@ -17,7 +17,7 @@ const ALL_KINDS: EntityKind[] = [ 'finding', 'question', 'causalLink', - 'suspectedCause', + 'hypothesis', 'canvasState', ]; @@ -59,7 +59,7 @@ describe('transitiveCascade', () => { 'finding', 'question', 'causalLink', - 'suspectedCause', + 'hypothesis', ]; for (const kind of expected) { expect(resultSet.has(kind)).toBe(true); @@ -78,7 +78,7 @@ describe('transitiveCascade', () => { expect(resultSet.has('finding')).toBe(true); expect(resultSet.has('question')).toBe(true); expect(resultSet.has('causalLink')).toBe(true); - expect(resultSet.has('suspectedCause')).toBe(true); + expect(resultSet.has('hypothesis')).toBe(true); expect(result).toHaveLength(4); }); @@ -100,7 +100,7 @@ describe('transitiveCascade', () => { 'finding', 'question', 'causalLink', - 'suspectedCause', + 'hypothesis', 'canvasState', ]; for (const leaf of leaves) { diff --git a/packages/core/src/persistence/cascadeRules.ts b/packages/core/src/persistence/cascadeRules.ts index 9a5d872d4..4a43464bc 100644 --- a/packages/core/src/persistence/cascadeRules.ts +++ b/packages/core/src/persistence/cascadeRules.ts @@ -16,7 +16,7 @@ export type EntityKind = | 'finding' | 'question' | 'causalLink' - | 'suspectedCause' + | 'hypothesis' | 'canvasState'; export interface CascadeRule { @@ -34,11 +34,11 @@ export const cascadeRules: CascadeRuleset = { rowProvenance: { cascadesTo: [] }, evidenceSource: { cascadesTo: ['evidenceSourceCursor'] }, evidenceSourceCursor: { cascadesTo: [] }, - investigation: { cascadesTo: ['finding', 'question', 'causalLink', 'suspectedCause'] }, + investigation: { cascadesTo: ['finding', 'question', 'causalLink', 'hypothesis'] }, finding: { cascadesTo: [] }, question: { cascadesTo: [] }, causalLink: { cascadesTo: [] }, - suspectedCause: { cascadesTo: [] }, + hypothesis: { cascadesTo: [] }, canvasState: { cascadesTo: [] }, }; diff --git a/packages/core/src/variation/bestSubgroup.ts b/packages/core/src/variation/bestSubgroup.ts index 28bd0f178..8b7409c9b 100644 --- a/packages/core/src/variation/bestSubgroup.ts +++ b/packages/core/src/variation/bestSubgroup.ts @@ -3,7 +3,7 @@ import type { CategoryStats } from './types'; /** * Find the best-performing subgroup based on characteristic type. - * Extracted from WhatIfPageBase for reuse in suspected cause evidence. + * Extracted from WhatIfPageBase for reuse in hypothesis evidence. * * - smaller-is-better → lowest mean * - larger-is-better → highest mean diff --git a/packages/data/src/samples/investigation-showcase.ts b/packages/data/src/samples/investigation-showcase.ts index 4ac847a3e..c1f0ce45e 100644 --- a/packages/data/src/samples/investigation-showcase.ts +++ b/packages/data/src/samples/investigation-showcase.ts @@ -411,7 +411,7 @@ function buildFindings(): Finding[] { ]; } -function buildSuspectedCauses(): Hypothesis[] { +function buildHypotheses(): Hypothesis[] { return [ { id: IDS.HUB_NOZZLE, @@ -470,7 +470,7 @@ function buildCategories(): InvestigationCategory[] { export const investigationShowcase: SampleDataset = { name: 'Showcase: Fill Weight Investigation', description: - 'Packaging line fill weight with pre-populated questions, findings, and suspected cause hub. Demonstrates the full investigation workflow.', + 'Packaging line fill weight with pre-populated questions, findings, and hypothesis hub. Demonstrates the full investigation workflow.', icon: 'microscope', urlKey: 'investigation-showcase', category: 'journeys', @@ -484,7 +484,7 @@ export const investigationShowcase: SampleDataset = { investigation: { findings: buildFindings(), questions: buildQuestions(), - suspectedCauses: buildSuspectedCauses(), + hypotheses: buildHypotheses(), categories: buildCategories(), }, processMap: { diff --git a/packages/data/src/samples/syringe-barrel-weight.ts b/packages/data/src/samples/syringe-barrel-weight.ts index acd32259f..0a8adc77c 100644 --- a/packages/data/src/samples/syringe-barrel-weight.ts +++ b/packages/data/src/samples/syringe-barrel-weight.ts @@ -573,7 +573,7 @@ function buildFindings(): Finding[] { ]; } -function buildSuspectedCauses(): Hypothesis[] { +function buildHypotheses(): Hypothesis[] { return [ { id: IDS.HUB_LOT3_PRESSURE, @@ -753,7 +753,7 @@ export const syringeBarrelWeight: SampleDataset = { investigation: { findings: buildFindings(), questions: buildQuestions(), - suspectedCauses: buildSuspectedCauses(), + hypotheses: buildHypotheses(), causalLinks: buildCausalLinks(), categories: buildCategories(), }, diff --git a/packages/data/src/types.ts b/packages/data/src/types.ts index 49f4d4600..1a5d91aca 100644 --- a/packages/data/src/types.ts +++ b/packages/data/src/types.ts @@ -33,7 +33,7 @@ export interface SampleDataset { export interface SampleInvestigationState { findings?: import('@variscout/core').Finding[]; questions?: import('@variscout/core').Question[]; - suspectedCauses?: import('@variscout/core').Hypothesis[]; + hypotheses?: import('@variscout/core').Hypothesis[]; causalLinks?: import('@variscout/core').CausalLink[]; categories?: import('@variscout/core').InvestigationCategory[]; } diff --git a/packages/hooks/src/__tests__/useCanvasInvestigationOverlays.test.ts b/packages/hooks/src/__tests__/useCanvasInvestigationOverlays.test.ts index 720e9706b..2c9aa6543 100644 --- a/packages/hooks/src/__tests__/useCanvasInvestigationOverlays.test.ts +++ b/packages/hooks/src/__tests__/useCanvasInvestigationOverlays.test.ts @@ -95,7 +95,7 @@ describe('canvas overlay registry', () => { expect(enabledCanvasOverlays().map(overlay => overlay.id)).toEqual([ 'investigations', 'hypotheses', - 'suspected-causes', + 'hypothesis-hubs', 'findings', 'wall', ]); @@ -156,10 +156,10 @@ describe('buildCanvasInvestigationOverlays', () => { expect(overlays.byStep.fill.findings.map(item => item.id)).toEqual(['f-linked']); }); - it('projects suspected causes from explicit tributary ids before fallback derivation', () => { + it('projects hypotheses from explicit tributary ids before fallback derivation', () => { const overlays = buildCanvasInvestigationOverlays({ map, - suspectedCauses: [ + hypotheses: [ hub({ id: 'hub-explicit', status: 'confirmed', @@ -169,7 +169,7 @@ describe('buildCanvasInvestigationOverlays', () => { ], }); - expect(overlays.byStep.fill.suspectedCauses).toEqual([ + expect(overlays.byStep.fill.hypotheses).toEqual([ expect.objectContaining({ id: 'hub-explicit', status: 'confirmed', questionId: 'q-fill' }), ]); expect(overlays.byStep.fill.investigationCounts.supported).toBe(1); @@ -184,10 +184,10 @@ describe('buildCanvasInvestigationOverlays', () => { context: { activeFilters: { Machine: ['A'] }, cumulativeScope: 20 }, }), ], - suspectedCauses: [hub({ id: 'hub-derived', findingIds: ['f-machine'] })], + hypotheses: [hub({ id: 'hub-derived', findingIds: ['f-machine'] })], }); - expect(overlays.byStep.mix.suspectedCauses.map(item => item.id)).toEqual(['hub-derived']); + expect(overlays.byStep.mix.hypotheses.map(item => item.id)).toEqual(['hub-derived']); }); it('renders draft causal links as arrows unless a promoted hub owns the link', () => { @@ -197,7 +197,7 @@ describe('buildCanvasInvestigationOverlays', () => { }); const promoted = buildCanvasInvestigationOverlays({ map, - suspectedCauses: [hub({ id: 'hub-promoted', questionIds: ['q-1'] })], + hypotheses: [hub({ id: 'hub-promoted', questionIds: ['q-1'] })], causalLinks: [link({ id: 'link-promoted', questionIds: ['q-1'] })], }); @@ -217,19 +217,19 @@ describe('buildCanvasInvestigationOverlays', () => { context: { activeFilters: { Unknown: ['x'] }, cumulativeScope: null }, }), ], - suspectedCauses: [hub({ id: 'hub-missing', tributaryIds: ['missing-tributary'] })], + hypotheses: [hub({ id: 'hub-missing', tributaryIds: ['missing-tributary'] })], causalLinks: [link({ id: 'link-missing', toFactor: 'Unknown' })], }); expect(overlays.unresolved).toEqual({ questions: ['q-missing'], findings: ['f-missing'], - suspectedCauses: ['hub-missing'], + hypotheses: ['hub-missing'], causalLinks: ['link-missing'], }); expect(overlays.byStep.mix.questions).toEqual([]); expect(overlays.byStep.mix.findings).toEqual([]); - expect(overlays.byStep.mix.suspectedCauses).toEqual([]); + expect(overlays.byStep.mix.hypotheses).toEqual([]); expect(overlays.arrows).toEqual([]); }); }); diff --git a/packages/hooks/src/__tests__/useHasInvestigationContent.test.ts b/packages/hooks/src/__tests__/useHasInvestigationContent.test.ts index a153e3577..116bdd9c3 100644 --- a/packages/hooks/src/__tests__/useHasInvestigationContent.test.ts +++ b/packages/hooks/src/__tests__/useHasInvestigationContent.test.ts @@ -39,7 +39,7 @@ describe('useHasInvestigationContent', () => { }); it('returns true when at least one hub exists', () => { - useInvestigationStore.setState({ suspectedCauses: [sampleHub] }); + useInvestigationStore.setState({ hypotheses: [sampleHub] }); const { result } = renderHook(() => useHasInvestigationContent({ findingsCount: 0 })); expect(result.current).toBe(true); }); diff --git a/packages/hooks/src/__tests__/useHubCommentStream.test.ts b/packages/hooks/src/__tests__/useHubCommentStream.test.ts index e52e6acb6..caf3c8530 100644 --- a/packages/hooks/src/__tests__/useHubCommentStream.test.ts +++ b/packages/hooks/src/__tests__/useHubCommentStream.test.ts @@ -34,7 +34,7 @@ vi.stubGlobal('EventSource', MockEventSourceClass); const stateRef = { current: { - suspectedCauses: [] as Array<{ + hypotheses: [] as Array<{ id: string; comments?: Array<{ id: string; text: string; createdAt: number }>; }>, @@ -90,7 +90,7 @@ describe('useHubCommentStream', () => { drainPendingCommentsMock.mockClear(); wallStateRef.pendingComments = []; stateRef.current = { - suspectedCauses: [ + hypotheses: [ { id: 'hub-1', comments: [] }, { id: 'hub-2', comments: [] }, ], @@ -158,7 +158,7 @@ describe('useHubCommentStream', () => { } as MessageEvent); }); - const hub = stateRef.current.suspectedCauses.find(h => h.id === 'hub-1')!; + const hub = stateRef.current.hypotheses.find(h => h.id === 'hub-1')!; expect(hub.comments).toHaveLength(2); expect(hub.comments?.[0]?.text).toBe('hello'); }); @@ -175,15 +175,13 @@ describe('useHubCommentStream', () => { } as MessageEvent); }); - const hub = stateRef.current.suspectedCauses.find(h => h.id === 'hub-1')!; + const hub = stateRef.current.hypotheses.find(h => h.id === 'hub-1')!; expect(hub.comments?.find(c => c.id === 'c-live')).toBeDefined(); }); it('dedups by id when a comment is re-received (self-echo)', () => { // Seed the hub with an existing optimistic comment. - stateRef.current.suspectedCauses[0].comments = [ - { id: 'c-local', text: 'my own', createdAt: 1 }, - ]; + stateRef.current.hypotheses[0].comments = [{ id: 'c-local', text: 'my own', createdAt: 1 }]; renderHook(() => useHubCommentStream({ projectId: 'proj-1', visibleHubIds: ['hub-1'] })); @@ -196,7 +194,7 @@ describe('useHubCommentStream', () => { } as MessageEvent); }); - const hub = stateRef.current.suspectedCauses.find(h => h.id === 'hub-1')!; + const hub = stateRef.current.hypotheses.find(h => h.id === 'hub-1')!; expect(hub.comments).toHaveLength(1); }); @@ -209,7 +207,7 @@ describe('useHubCommentStream', () => { }); }).not.toThrow(); - const hub = stateRef.current.suspectedCauses.find(h => h.id === 'hub-1')!; + const hub = stateRef.current.hypotheses.find(h => h.id === 'hub-1')!; expect(hub.comments).toHaveLength(0); }); diff --git a/packages/hooks/src/__tests__/useHubComputations.test.ts b/packages/hooks/src/__tests__/useHubComputations.test.ts index a044c0772..7e117f366 100644 --- a/packages/hooks/src/__tests__/useHubComputations.test.ts +++ b/packages/hooks/src/__tests__/useHubComputations.test.ts @@ -103,14 +103,14 @@ describe('useHubComputations', () => { }); it('returns undefined when hubs list is empty even with bestSubsets', () => { - useInvestigationStore.setState({ suspectedCauses: [] }); + useInvestigationStore.setState({ hypotheses: [] }); const { result } = renderHook(() => useHubComputations(makeBestSubsets(), [])); expect(result.current.hubEvidences).toBeUndefined(); }); it('returns a map with one entry per hub when hubs exist', () => { const hub = makeHub({ id: 'hub-1', questionIds: [] }); - useInvestigationStore.setState({ suspectedCauses: [hub] }); + useInvestigationStore.setState({ hypotheses: [hub] }); const { result } = renderHook(() => useHubComputations(makeBestSubsets(), [])); @@ -122,7 +122,7 @@ describe('useHubComputations', () => { it('computes evidence for multiple hubs', () => { const hub1 = makeHub({ id: 'hub-1', questionIds: [] }); const hub2 = makeHub({ id: 'hub-2', questionIds: [] }); - useInvestigationStore.setState({ suspectedCauses: [hub1, hub2] }); + useInvestigationStore.setState({ hypotheses: [hub1, hub2] }); const { result } = renderHook(() => useHubComputations(makeBestSubsets(), [])); @@ -133,7 +133,7 @@ describe('useHubComputations', () => { it('uses performance mode when analysisMode is performance', () => { const hub = makeHub({ id: 'hub-1', questionIds: [] }); - useInvestigationStore.setState({ suspectedCauses: [hub] }); + useInvestigationStore.setState({ hypotheses: [hub] }); useProjectStore.setState({ analysisMode: 'performance' }); const { result } = renderHook(() => useHubComputations(null, [])); @@ -144,7 +144,7 @@ describe('useHubComputations', () => { it('uses yamazumi mode when analysisMode is yamazumi', () => { const hub = makeHub({ id: 'hub-1', questionIds: [] }); - useInvestigationStore.setState({ suspectedCauses: [hub] }); + useInvestigationStore.setState({ hypotheses: [hub] }); useProjectStore.setState({ analysisMode: 'yamazumi' }); const { result } = renderHook(() => useHubComputations(null, [])); @@ -155,7 +155,7 @@ describe('useHubComputations', () => { it('uses standard mode for standard analysisMode', () => { const hub = makeHub({ id: 'hub-1', questionIds: [] }); - useInvestigationStore.setState({ suspectedCauses: [hub] }); + useInvestigationStore.setState({ hypotheses: [hub] }); useProjectStore.setState({ analysisMode: 'standard' }); const { result } = renderHook(() => useHubComputations(null, [])); @@ -167,7 +167,7 @@ describe('useHubComputations', () => { it('factors from linked questions are picked up in evidence', () => { const questions = [makeQuestion({ id: 'q1', factor: 'Machine', status: 'investigating' })]; const hub = makeHub({ id: 'hub-1', questionIds: ['q1'] }); - useInvestigationStore.setState({ suspectedCauses: [hub] }); + useInvestigationStore.setState({ hypotheses: [hub] }); const { result } = renderHook(() => useHubComputations(makeBestSubsets(), questions)); @@ -282,7 +282,7 @@ describe('useHubComputations', () => { it('returns undefined when bestSubsets is null even with hubs', () => { const hub = makeHub({ id: 'hub-1', questionIds: [] }); - useInvestigationStore.setState({ suspectedCauses: [hub] }); + useInvestigationStore.setState({ hypotheses: [hub] }); const { result } = renderHook(() => useHubComputations(null, [])); expect(result.current.hubProjections).toBeUndefined(); @@ -291,7 +291,7 @@ describe('useHubComputations', () => { it('returns a map keyed by hub id when hubs and bestSubsets are provided', () => { const questions = [makeQuestion({ id: 'q1', factor: 'Machine', status: 'investigating' })]; const hub = makeHub({ id: 'hub-1', questionIds: ['q1'] }); - useInvestigationStore.setState({ suspectedCauses: [hub] }); + useInvestigationStore.setState({ hypotheses: [hub] }); const { result } = renderHook(() => useHubComputations(makeBestSubsets(), questions)); @@ -303,7 +303,7 @@ describe('useHubComputations', () => { it('excludes hubs where computeHubProjection returns null', () => { // Hub with no linked questions will find no factors in subsets const hub = makeHub({ id: 'hub-1', questionIds: [] }); - useInvestigationStore.setState({ suspectedCauses: [hub] }); + useInvestigationStore.setState({ hypotheses: [hub] }); const { result } = renderHook(() => useHubComputations(makeBestSubsets(), [])); @@ -319,7 +319,7 @@ describe('useHubComputations', () => { expect(result.current.hubEvidences).toBeUndefined(); useInvestigationStore.setState({ - suspectedCauses: [makeHub({ id: 'hub-1', questionIds: [] })], + hypotheses: [makeHub({ id: 'hub-1', questionIds: [] })], }); rerender(); diff --git a/packages/hooks/src/__tests__/useSuspectedCauses.test.ts b/packages/hooks/src/__tests__/useHypotheses.test.ts similarity index 80% rename from packages/hooks/src/__tests__/useSuspectedCauses.test.ts rename to packages/hooks/src/__tests__/useHypotheses.test.ts index 218307068..e1b9243ad 100644 --- a/packages/hooks/src/__tests__/useSuspectedCauses.test.ts +++ b/packages/hooks/src/__tests__/useHypotheses.test.ts @@ -1,25 +1,25 @@ import { describe, it, expect, vi } from 'vitest'; import { renderHook, act } from '@testing-library/react'; -import { useSuspectedCauses } from '../useSuspectedCauses'; +import { useHypotheses } from '../useHypotheses'; import { createHypothesis } from '@variscout/core/findings'; import type { Hypothesis } from '@variscout/core'; -describe('useSuspectedCauses', () => { +describe('useHypotheses', () => { it('starts with empty hubs when no initialHubs provided', () => { - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: [] })); + const { result } = renderHook(() => useHypotheses({ initialHubs: [] })); expect(result.current.hubs).toEqual([]); }); it('starts with provided initialHubs', () => { const initial: Hypothesis[] = [createHypothesis('Nozzle wear', 'High vibration at night')]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); expect(result.current.hubs).toHaveLength(1); expect(result.current.hubs[0].name).toBe('Nozzle wear'); }); describe('createHub', () => { it('adds a hub and returns it', () => { - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: [] })); + const { result } = renderHook(() => useHypotheses({ initialHubs: [] })); let created: Hypothesis; act(() => { created = result.current.createHub('Shift effect', 'Night shift runs hotter'); @@ -33,7 +33,7 @@ describe('useSuspectedCauses', () => { it('calls onHubsChange after creation', () => { const onChange = vi.fn(); const { result } = renderHook(() => - useSuspectedCauses({ initialHubs: [], onHubsChange: onChange }) + useHypotheses({ initialHubs: [], onHubsChange: onChange }) ); act(() => { result.current.createHub('Wear', ''); @@ -43,7 +43,7 @@ describe('useSuspectedCauses', () => { }); it('new hub has status proposed and empty connections', () => { - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: [] })); + const { result } = renderHook(() => useHypotheses({ initialHubs: [] })); act(() => { result.current.createHub('Test hub', ''); }); @@ -57,7 +57,7 @@ describe('useSuspectedCauses', () => { describe('resetHubs', () => { it('replaces all hubs with the provided list', () => { const initial = [createHypothesis('Old hub', '')]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); const replacement = [ createHypothesis('New hub A', 'synthesis A'), createHypothesis('New hub B', 'synthesis B'), @@ -73,7 +73,7 @@ describe('useSuspectedCauses', () => { it('calls onHubsChange with the new hubs', () => { const onChange = vi.fn(); const { result } = renderHook(() => - useSuspectedCauses({ initialHubs: [], onHubsChange: onChange }) + useHypotheses({ initialHubs: [], onHubsChange: onChange }) ); const newHubs = [createHypothesis('Migrated hub', 'from legacy causeRole')]; act(() => { @@ -85,7 +85,7 @@ describe('useSuspectedCauses', () => { it('replaces existing hubs with an empty list', () => { const initial = [createHypothesis('Hub', '')]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); act(() => { result.current.resetHubs([]); }); @@ -96,7 +96,7 @@ describe('useSuspectedCauses', () => { describe('updateHub', () => { it('updates name and synthesis', () => { const initial = [createHypothesis('Old name', 'Old synthesis')]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); const hubId = initial[0].id; act(() => { result.current.updateHub(hubId, { name: 'New name', synthesis: 'New synthesis' }); @@ -108,7 +108,7 @@ describe('useSuspectedCauses', () => { it('updates branch nextMove without changing linked evidence', () => { const initial = [createHypothesis('Hub', '', ['q-001'], ['f-001'])]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); act(() => { result.current.updateHub(initial[0].id, { nextMove: 'Run a late-shift temperature check.', @@ -122,7 +122,7 @@ describe('useSuspectedCauses', () => { it('updates updatedAt to a valid ISO timestamp', () => { const initial = [createHypothesis('Hub', '')]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); act(() => { result.current.updateHub(initial[0].id, { name: 'Updated' }); }); @@ -134,7 +134,7 @@ describe('useSuspectedCauses', () => { const onChange = vi.fn(); const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => - useSuspectedCauses({ initialHubs: initial, onHubsChange: onChange }) + useHypotheses({ initialHubs: initial, onHubsChange: onChange }) ); act(() => { result.current.updateHub(initial[0].id, { name: 'Updated' }); @@ -144,7 +144,7 @@ describe('useSuspectedCauses', () => { it('does nothing for unknown hubId', () => { const initial = [createHypothesis('Hub', '')]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); act(() => { result.current.updateHub('nonexistent-id', { name: 'Ghost' }); }); @@ -155,7 +155,7 @@ describe('useSuspectedCauses', () => { describe('deleteHub', () => { it('removes the hub by id', () => { const initial = [createHypothesis('Hub A', ''), createHypothesis('Hub B', '')]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); act(() => { result.current.deleteHub(initial[0].id); }); @@ -167,7 +167,7 @@ describe('useSuspectedCauses', () => { const onChange = vi.fn(); const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => - useSuspectedCauses({ initialHubs: initial, onHubsChange: onChange }) + useHypotheses({ initialHubs: initial, onHubsChange: onChange }) ); act(() => { result.current.deleteHub(initial[0].id); @@ -179,7 +179,7 @@ describe('useSuspectedCauses', () => { describe('connectQuestion', () => { it('adds a questionId to the hub', () => { const initial = [createHypothesis('Hub', '')]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); act(() => { result.current.connectQuestion(initial[0].id, 'q-001'); }); @@ -188,7 +188,7 @@ describe('useSuspectedCauses', () => { it('does not add duplicate question connections', () => { const initial = [createHypothesis('Hub', '', ['q-001'])]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); act(() => { result.current.connectQuestion(initial[0].id, 'q-001'); }); @@ -199,7 +199,7 @@ describe('useSuspectedCauses', () => { const onChange = vi.fn(); const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => - useSuspectedCauses({ initialHubs: initial, onHubsChange: onChange }) + useHypotheses({ initialHubs: initial, onHubsChange: onChange }) ); act(() => { result.current.connectQuestion(initial[0].id, 'q-001'); @@ -213,7 +213,7 @@ describe('useSuspectedCauses', () => { describe('disconnectQuestion', () => { it('removes a questionId from the hub', () => { const initial = [createHypothesis('Hub', '', ['q-001', 'q-002'])]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); act(() => { result.current.disconnectQuestion(initial[0].id, 'q-001'); }); @@ -225,7 +225,7 @@ describe('useSuspectedCauses', () => { const onChange = vi.fn(); const initial = [createHypothesis('Hub', '', ['q-001'])]; const { result } = renderHook(() => - useSuspectedCauses({ initialHubs: initial, onHubsChange: onChange }) + useHypotheses({ initialHubs: initial, onHubsChange: onChange }) ); act(() => { result.current.disconnectQuestion(initial[0].id, 'q-001'); @@ -237,7 +237,7 @@ describe('useSuspectedCauses', () => { describe('connectFinding', () => { it('adds a findingId to the hub', () => { const initial = [createHypothesis('Hub', '')]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); act(() => { result.current.connectFinding(initial[0].id, 'f-001'); }); @@ -246,7 +246,7 @@ describe('useSuspectedCauses', () => { it('does not add duplicate finding connections', () => { const initial = [createHypothesis('Hub', '', [], ['f-001'])]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); act(() => { result.current.connectFinding(initial[0].id, 'f-001'); }); @@ -257,7 +257,7 @@ describe('useSuspectedCauses', () => { const onChange = vi.fn(); const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => - useSuspectedCauses({ initialHubs: initial, onHubsChange: onChange }) + useHypotheses({ initialHubs: initial, onHubsChange: onChange }) ); act(() => { result.current.connectFinding(initial[0].id, 'f-001'); @@ -269,7 +269,7 @@ describe('useSuspectedCauses', () => { describe('disconnectFinding', () => { it('removes a findingId from the hub', () => { const initial = [createHypothesis('Hub', '', [], ['f-001', 'f-002'])]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); act(() => { result.current.disconnectFinding(initial[0].id, 'f-001'); }); @@ -281,7 +281,7 @@ describe('useSuspectedCauses', () => { const onChange = vi.fn(); const initial = [createHypothesis('Hub', '', [], ['f-001'])]; const { result } = renderHook(() => - useSuspectedCauses({ initialHubs: initial, onHubsChange: onChange }) + useHypotheses({ initialHubs: initial, onHubsChange: onChange }) ); act(() => { result.current.disconnectFinding(initial[0].id, 'f-001'); @@ -293,7 +293,7 @@ describe('useSuspectedCauses', () => { describe('setHubStatus', () => { it('updates status to confirmed', () => { const initial = [createHypothesis('Hub', '')]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); act(() => { result.current.setHubStatus(initial[0].id, 'confirmed'); }); @@ -302,7 +302,7 @@ describe('useSuspectedCauses', () => { it('updates status to refuted', () => { const initial = [createHypothesis('Hub', '')]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); act(() => { result.current.setHubStatus(initial[0].id, 'refuted'); }); @@ -313,7 +313,7 @@ describe('useSuspectedCauses', () => { const onChange = vi.fn(); const initial = [createHypothesis('Hub', '')]; const { result } = renderHook(() => - useSuspectedCauses({ initialHubs: initial, onHubsChange: onChange }) + useHypotheses({ initialHubs: initial, onHubsChange: onChange }) ); act(() => { result.current.setHubStatus(initial[0].id, 'confirmed'); @@ -330,19 +330,19 @@ describe('useSuspectedCauses', () => { createHypothesis('Hub A', '', ['q-001']), createHypothesis('Hub B', '', ['q-002']), ]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); expect(result.current.getHubForQuestion('q-001')?.name).toBe('Hub A'); expect(result.current.getHubForQuestion('q-002')?.name).toBe('Hub B'); }); it('returns undefined when no hub contains the questionId', () => { const initial = [createHypothesis('Hub', '', ['q-001'])]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); expect(result.current.getHubForQuestion('q-999')).toBeUndefined(); }); it('returns undefined when hubs list is empty', () => { - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: [] })); + const { result } = renderHook(() => useHypotheses({ initialHubs: [] })); expect(result.current.getHubForQuestion('q-001')).toBeUndefined(); }); }); @@ -353,14 +353,14 @@ describe('useSuspectedCauses', () => { createHypothesis('Hub A', '', [], ['f-001']), createHypothesis('Hub B', '', [], ['f-002']), ]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); expect(result.current.getHubForFinding('f-001')?.name).toBe('Hub A'); expect(result.current.getHubForFinding('f-002')?.name).toBe('Hub B'); }); it('returns undefined when no hub contains the findingId', () => { const initial = [createHypothesis('Hub', '', [], ['f-001'])]; - const { result } = renderHook(() => useSuspectedCauses({ initialHubs: initial })); + const { result } = renderHook(() => useHypotheses({ initialHubs: initial })); expect(result.current.getHubForFinding('f-999')).toBeUndefined(); }); }); diff --git a/packages/hooks/src/__tests__/useImprovementProjections.test.ts b/packages/hooks/src/__tests__/useImprovementProjections.test.ts index be20d112b..41a3b844c 100644 --- a/packages/hooks/src/__tests__/useImprovementProjections.test.ts +++ b/packages/hooks/src/__tests__/useImprovementProjections.test.ts @@ -25,10 +25,10 @@ function makeQuestion(overrides: Partial & { id: string }): Question { // ============================================================================ describe('useImprovementProjections', () => { - describe('suspectedCauses', () => { + describe('hypotheses', () => { it('returns empty array when there are no questions', () => { const { result } = renderHook(() => useImprovementProjections([], {})); - expect(result.current.suspectedCauses).toEqual([]); + expect(result.current.hypotheses).toEqual([]); }); it('returns empty array when questions have no suspected-cause role', () => { @@ -38,13 +38,13 @@ describe('useImprovementProjections', () => { makeQuestion({ id: 'q3', factor: 'Operator' }), ]; const { result } = renderHook(() => useImprovementProjections(questions, {})); - expect(result.current.suspectedCauses).toEqual([]); + expect(result.current.hypotheses).toEqual([]); }); it('returns empty array when suspected-cause question has no factor', () => { const questions = [makeQuestion({ id: 'q1', causeRole: 'suspected-cause' })]; const { result } = renderHook(() => useImprovementProjections(questions, {})); - expect(result.current.suspectedCauses).toEqual([]); + expect(result.current.hypotheses).toEqual([]); }); it('returns one entry for a suspected-cause question with a factor', () => { @@ -52,9 +52,9 @@ describe('useImprovementProjections', () => { makeQuestion({ id: 'q1', factor: 'Machine', causeRole: 'suspected-cause' }), ]; const { result } = renderHook(() => useImprovementProjections(questions, { Machine: 1.45 })); - expect(result.current.suspectedCauses).toHaveLength(1); - expect(result.current.suspectedCauses[0].factor).toBe('Machine'); - expect(result.current.suspectedCauses[0].projectedCpk).toBe(1.45); + expect(result.current.hypotheses).toHaveLength(1); + expect(result.current.hypotheses[0].factor).toBe('Machine'); + expect(result.current.hypotheses[0].projectedCpk).toBe(1.45); }); it('returns undefined projectedCpk when factor is not in the map', () => { @@ -62,7 +62,7 @@ describe('useImprovementProjections', () => { makeQuestion({ id: 'q1', factor: 'Machine', causeRole: 'suspected-cause' }), ]; const { result } = renderHook(() => useImprovementProjections(questions, {})); - expect(result.current.suspectedCauses[0].projectedCpk).toBeUndefined(); + expect(result.current.hypotheses[0].projectedCpk).toBeUndefined(); }); it('filters out non-suspected-cause questions from mixed list', () => { @@ -74,8 +74,8 @@ describe('useImprovementProjections', () => { ]; const projectedCpkMap = { Machine: 1.2, Shift: 0.9, Operator: 1.5, Line: 0.8 }; const { result } = renderHook(() => useImprovementProjections(questions, projectedCpkMap)); - expect(result.current.suspectedCauses).toHaveLength(2); - expect(result.current.suspectedCauses.map(sc => sc.factor)).toEqual(['Machine', 'Operator']); + expect(result.current.hypotheses).toHaveLength(2); + expect(result.current.hypotheses.map(sc => sc.factor)).toEqual(['Machine', 'Operator']); }); it('returns multiple entries for multiple suspected-cause questions', () => { @@ -85,7 +85,7 @@ describe('useImprovementProjections', () => { ]; const projectedCpkMap = { Machine: 1.3, Shift: 1.6 }; const { result } = renderHook(() => useImprovementProjections(questions, projectedCpkMap)); - expect(result.current.suspectedCauses).toHaveLength(2); + expect(result.current.hypotheses).toHaveLength(2); }); }); @@ -135,13 +135,13 @@ describe('useImprovementProjections', () => { { initialProps: { qs: questions, map: {} } } ); - expect(result.current.suspectedCauses).toHaveLength(0); + expect(result.current.hypotheses).toHaveLength(0); questions = [makeQuestion({ id: 'q1', factor: 'Machine', causeRole: 'suspected-cause' })]; rerender({ qs: questions, map: { Machine: 1.5 } }); - expect(result.current.suspectedCauses).toHaveLength(1); - expect(result.current.suspectedCauses[0].factor).toBe('Machine'); + expect(result.current.hypotheses).toHaveLength(1); + expect(result.current.hypotheses[0].factor).toBe('Machine'); }); it('updates combinedProjectedCpk when map changes', () => { diff --git a/packages/hooks/src/__tests__/useKnowledgeSearch.test.ts b/packages/hooks/src/__tests__/useKnowledgeSearch.test.ts index 1d6420ded..28ae27fef 100644 --- a/packages/hooks/src/__tests__/useKnowledgeSearch.test.ts +++ b/packages/hooks/src/__tests__/useKnowledgeSearch.test.ts @@ -14,7 +14,7 @@ const mockResult: KnowledgeResult = { etaSquared: 0.42, cpkBefore: 0.85, cpkAfter: 1.45, - suspectedCause: 'Nozzle blockage', + hypothesis: 'Nozzle blockage', actionsText: 'Replaced nozzle weekly', outcomeEffective: true, score: 0.95, diff --git a/packages/hooks/src/__tests__/useProblemStatement.test.ts b/packages/hooks/src/__tests__/useProblemStatement.test.ts index 9868294f0..4df895296 100644 --- a/packages/hooks/src/__tests__/useProblemStatement.test.ts +++ b/packages/hooks/src/__tests__/useProblemStatement.test.ts @@ -23,7 +23,7 @@ function makeQuestion(overrides: Partial = {}): Question { } describe('useProblemStatement', () => { - it('returns isReady=true when outcome and suspected causes exist', () => { + it('returns isReady=true when outcome and hypotheses exist', () => { const { result } = renderHook(() => useProblemStatement({ outcome: 'Fill Weight', @@ -45,7 +45,7 @@ describe('useProblemStatement', () => { expect(result.current.generatedDraft).toBeNull(); }); - it('returns isReady=false when no suspected causes', () => { + it('returns isReady=false when no hypotheses', () => { const { result } = renderHook(() => useProblemStatement({ outcome: 'Weight', @@ -136,7 +136,7 @@ describe('useProblemStatement', () => { expect(result.current.generatedDraft).toContain('50%'); }); - // Early formation tests — Watson Q1+Q2+Q3 without suspected causes + // Early formation tests — Watson Q1+Q2+Q3 without hypotheses describe('early formation (isFormable)', () => { const locationFactor: LocationFactor = { factor: 'Shift', level: 'Night', evidence: 0.42 }; @@ -227,14 +227,14 @@ describe('useProblemStatement', () => { expect(result.current.hasScope).toBe(true); }); - it('hasScope is false when locationFactor is absent and no suspected causes', () => { + it('hasScope is false when locationFactor is absent and no hypotheses', () => { const { result } = renderHook(() => useProblemStatement({ outcome: 'Fill Weight', questions: [] }) ); expect(result.current.hasScope).toBe(false); }); - it('hasScope is true when there are suspected causes (backward compat)', () => { + it('hasScope is true when there are hypotheses (backward compat)', () => { const { result } = renderHook(() => useProblemStatement({ outcome: 'Fill Weight', @@ -292,7 +292,7 @@ describe('useProblemStatement', () => { expect(result.current.liveStatement).toContain('Decrease'); }); - it('liveStatement includes suspected causes after locationFactor', () => { + it('liveStatement includes hypotheses after locationFactor', () => { const { result } = renderHook(() => useProblemStatement({ outcome: 'Fill Weight', @@ -305,7 +305,7 @@ describe('useProblemStatement', () => { expect(result.current.liveStatement).toContain('Operator'); }); - it('liveStatement skips suspected causes that duplicate locationFactor factor name', () => { + it('liveStatement skips hypotheses that duplicate locationFactor factor name', () => { const { result } = renderHook(() => useProblemStatement({ outcome: 'Fill Weight', @@ -320,7 +320,7 @@ describe('useProblemStatement', () => { expect(shiftCount).toBe(1); }); - it('legacy isReady still requires suspected causes (backward compat)', () => { + it('legacy isReady still requires hypotheses (backward compat)', () => { const { result } = renderHook(() => useProblemStatement({ outcome: 'Fill Weight', @@ -329,7 +329,7 @@ describe('useProblemStatement', () => { questions: [], }) ); - // isFormable is true (Q1+Q2+Q3), but isReady is false (no question suspected causes) + // isFormable is true (Q1+Q2+Q3), but isReady is false (no question hypotheses) expect(result.current.isFormable).toBe(true); expect(result.current.isReady).toBe(false); }); @@ -351,7 +351,7 @@ describe('useProblemStatement', () => { expect(result.current.canGenerateDraft).toBe(true); }); - it('canGenerateDraft should be true from legacy path (suspected cause questions)', () => { + it('canGenerateDraft should be true from legacy path (hypothesis questions)', () => { const { result } = renderHook(() => useProblemStatement({ outcome: 'Fill Weight', @@ -375,7 +375,7 @@ describe('useProblemStatement', () => { expect(result.current.canGenerateDraft).toBe(false); }); - it('canGenerateDraft should be false without scope (no locationFactor, no suspected causes)', () => { + it('canGenerateDraft should be false without scope (no locationFactor, no hypotheses)', () => { const { result } = renderHook(() => useProblemStatement({ outcome: 'Fill Weight', diff --git a/packages/hooks/src/__tests__/useProjectActions.roundtrip.test.ts b/packages/hooks/src/__tests__/useProjectActions.roundtrip.test.ts index aa1edeea4..69104a316 100644 --- a/packages/hooks/src/__tests__/useProjectActions.roundtrip.test.ts +++ b/packages/hooks/src/__tests__/useProjectActions.roundtrip.test.ts @@ -416,6 +416,6 @@ describe('useProjectActions persistence roundtrip', () => { expect(is.findings).toEqual([]); expect(is.questions).toEqual([]); expect(is.categories).toEqual([]); - expect(is.suspectedCauses).toEqual([]); + expect(is.hypotheses).toEqual([]); }); }); diff --git a/packages/hooks/src/__tests__/useSharedWallProps.test.ts b/packages/hooks/src/__tests__/useSharedWallProps.test.ts index 641a928b7..4254da6ef 100644 --- a/packages/hooks/src/__tests__/useSharedWallProps.test.ts +++ b/packages/hooks/src/__tests__/useSharedWallProps.test.ts @@ -99,7 +99,7 @@ describe('useSharedWallProps', () => { const question = makeQuestion({ id: 'q-1' }); const findings = [makeFinding({ id: 'finding-1' })]; useInvestigationStore.setState({ - suspectedCauses: [hub], + hypotheses: [hub], questions: [question], }); @@ -113,7 +113,7 @@ describe('useSharedWallProps', () => { }) ); - expect(result.current.hubs).toBe(useInvestigationStore.getState().suspectedCauses); + expect(result.current.hubs).toBe(useInvestigationStore.getState().hypotheses); expect(result.current.questions).toBe(useInvestigationStore.getState().questions); expect(result.current.findings).toBe(findings); }); diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 42c042a08..86d866b25 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -422,7 +422,7 @@ export { useHubComputations, type UseHubComputationsReturn } from './useHubCompu export { useImprovementProjections, type UseImprovementProjectionsReturn, - type SuspectedCauseProjection, + type HypothesisProjection, } from './useImprovementProjections'; // Scoped Models (model scoping for What-If Explorer) @@ -483,7 +483,7 @@ export { type CanvasOverlayFindingItem, type CanvasOverlayId, type CanvasOverlayQuestionItem, - type CanvasOverlaySuspectedCauseItem, + type CanvasOverlayHypothesisItem, type CanvasStepInvestigationOverlay, type UseCanvasInvestigationOverlaysArgs, type UseCanvasInvestigationOverlaysResult, @@ -491,11 +491,11 @@ export { // Hypothesis Hubs (named mechanism groupings for investigation synthesis) export { - useSuspectedCauses, + useHypotheses, type HypothesisUpdate, - type UseSuspectedCausesOptions, - type UseSuspectedCausesReturn, -} from './useSuspectedCauses'; + type UseHypothesesOptions, + type UseHypothesesReturn, +} from './useHypotheses'; // Question Reactivity (drill-down factor → active question lookup) export { useQuestionReactivity } from './useQuestionReactivity'; diff --git a/packages/hooks/src/types.ts b/packages/hooks/src/types.ts index f4ca7f1fd..da3b8147c 100644 --- a/packages/hooks/src/types.ts +++ b/packages/hooks/src/types.ts @@ -178,9 +178,9 @@ export interface AnalysisState { /** User-defined categories grouping factor columns */ categories?: InvestigationCategory[]; - // --- Suspected causes (investigation synthesis) --- + // --- Hypotheses (investigation synthesis) --- /** Hypothesis hubs connecting evidence threads */ - suspectedCauses?: Hypothesis[]; + hypotheses?: Hypothesis[]; // --- Causal links (investigation DAG) --- /** Causal links between factors (investigation DAG) */ diff --git a/packages/hooks/src/useAIContext.ts b/packages/hooks/src/useAIContext.ts index decffae09..4a7bf4351 100644 --- a/packages/hooks/src/useAIContext.ts +++ b/packages/hooks/src/useAIContext.ts @@ -85,7 +85,7 @@ export interface UseAIContextOptions { /** Evidence Map topology for graph-aware CoScout reasoning (ADR-066) */ evidenceMapTopology?: BuildAIContextOptions['evidenceMapTopology']; /** Suspected cause hubs from investigation (ADR-064) */ - suspectedCauses?: Hypothesis[]; + hypotheses?: Hypothesis[]; /** Best subsets result for model equation and interaction effects context */ bestSubsetsResult?: BuildAIContextOptions['bestSubsetsResult']; } @@ -126,7 +126,7 @@ export function useAIContext(options: UseAIContextOptions): UseAIContextReturn { capabilityData, focusedQuestionId, evidenceMapTopology, - suspectedCauses, + hypotheses, bestSubsetsResult, } = options; @@ -155,7 +155,7 @@ export function useAIContext(options: UseAIContextOptions): UseAIContextReturn { drillPathEnriched, capabilityData, evidenceMapTopology, - suspectedCauses, + hypotheses, bestSubsetsResult, }; @@ -202,7 +202,7 @@ export function useAIContext(options: UseAIContextOptions): UseAIContextReturn { capabilityData, focusedQuestionId, evidenceMapTopology, - suspectedCauses, + hypotheses, bestSubsetsResult, ]); diff --git a/packages/hooks/src/useCanvasInvestigationOverlays.ts b/packages/hooks/src/useCanvasInvestigationOverlays.ts index c43ba3516..2301b4492 100644 --- a/packages/hooks/src/useCanvasInvestigationOverlays.ts +++ b/packages/hooks/src/useCanvasInvestigationOverlays.ts @@ -6,7 +6,7 @@ import { selectHypothesisTributaries } from '@variscout/stores'; export type CanvasOverlayId = | 'investigations' | 'hypotheses' - | 'suspected-causes' + | 'hypothesis-hubs' | 'findings' | 'wall'; @@ -30,9 +30,9 @@ export const CANVAS_OVERLAY_REGISTRY: Record(); - for (const hub of suspectedCauses) { + for (const hub of hypotheses) { const steps = validStepIds(hubStepIds({ hub, findings, questions, map, columnMap }), byStep); hubSteps.set(hub.id, steps); - if (steps.length === 0) unresolved.suspectedCauses.push(hub.id); + if (steps.length === 0) unresolved.hypotheses.push(hub.id); } for (const question of questions) { @@ -303,7 +303,7 @@ export function buildCanvasInvestigationOverlays({ steps = validStepIds(stepsForColumns([linkedQuestion.factor], columnMap), byStep); } if (steps.length === 0) { - const linkedHubSteps = suspectedCauses + const linkedHubSteps = hypotheses .filter(hub => hub.findingIds.includes(finding.id)) .flatMap(hub => hubSteps.get(hub.id) ?? []); steps = validStepIds(uniqueStepIds(linkedHubSteps), byStep); @@ -326,10 +326,10 @@ export function buildCanvasInvestigationOverlays({ } } - for (const hub of suspectedCauses) { + for (const hub of hypotheses) { const steps = validStepIds(hubSteps.get(hub.id) ?? [], byStep); if (steps.length === 0) continue; - const item: CanvasOverlaySuspectedCauseItem = { + const item: CanvasOverlayHypothesisItem = { id: hub.id, name: hub.name, status: hub.status, @@ -343,14 +343,14 @@ export function buildCanvasInvestigationOverlays({ for (const stepId of steps) { const step = byStep[stepId]; if (!step) continue; - if (isPromotedHub(hub)) addUnique(step.suspectedCauses, item); + if (isPromotedHub(hub)) addUnique(step.hypotheses, item); if (hub.status === 'refuted') step.investigationCounts.refuted += 1; else if (hub.status === 'confirmed') step.investigationCounts.supported += 1; else step.investigationCounts.open += 1; } } - const promotedHubs = suspectedCauses.filter(isPromotedHub); + const promotedHubs = hypotheses.filter(isPromotedHub); const arrows: CanvasOverlayCausalLinkItem[] = []; for (const link of causalLinks) { if (promotedHubs.some(hub => hubOwnsLink(hub, link))) continue; @@ -387,7 +387,7 @@ export function useCanvasInvestigationOverlays( ): UseCanvasInvestigationOverlaysResult { const overlays = useMemo( () => buildCanvasInvestigationOverlays(args), - [args.map, args.questions, args.findings, args.suspectedCauses, args.causalLinks] + [args.map, args.questions, args.findings, args.hypotheses, args.causalLinks] ); return { overlays }; } diff --git a/packages/hooks/src/useDataIngestion.ts b/packages/hooks/src/useDataIngestion.ts index b9449bdeb..b6d1e6ead 100644 --- a/packages/hooks/src/useDataIngestion.ts +++ b/packages/hooks/src/useDataIngestion.ts @@ -77,7 +77,7 @@ export interface DataIngestionActions { /** Set pre-populated investigation categories (for showcase/demo datasets) */ setCategories?: (categories: InvestigationCategory[]) => void; /** Replace Hypothesis hubs with a seeded set (showcase/demo datasets) */ - setSuspectedCauses?: (hubs: Hypothesis[]) => void; + setHypotheses?: (hubs: Hypothesis[]) => void; /** Replace CausalLinks with a seeded set (showcase/demo datasets) */ setCausalLinks?: (links: CausalLink[]) => void; /** Set the process context (used to seed FRAME Process Map on showcases) */ @@ -182,7 +182,7 @@ export function useDataIngestion( setFindings, setQuestions, setCategories, - setSuspectedCauses, + setHypotheses, setCausalLinks, setProcessContext, getProcessContext, @@ -460,15 +460,14 @@ export function useDataIngestion( if (inv.findings?.length && setFindings) setFindings(inv.findings); if (inv.questions?.length && setQuestions) setQuestions(inv.questions); if (inv.categories?.length && setCategories) setCategories(inv.categories); - if (inv.suspectedCauses?.length && setSuspectedCauses) - setSuspectedCauses(inv.suspectedCauses); + if (inv.hypotheses?.length && setHypotheses) setHypotheses(inv.hypotheses); if (inv.causalLinks?.length && setCausalLinks) setCausalLinks(inv.causalLinks); } else { // Clear stale investigation state from a previous showcase sample if (setFindings) setFindings([]); if (setQuestions) setQuestions([]); if (setCategories) setCategories([]); - if (setSuspectedCauses) setSuspectedCauses([]); + if (setHypotheses) setHypotheses([]); if (setCausalLinks) setCausalLinks([]); } // Seed or clear the FRAME Process Map (ADR-070). Preserves any other @@ -501,7 +500,7 @@ export function useDataIngestion( setFindings, setQuestions, setCategories, - setSuspectedCauses, + setHypotheses, setCausalLinks, setProcessContext, getProcessContext, diff --git a/packages/hooks/src/useEvidenceMapData.ts b/packages/hooks/src/useEvidenceMapData.ts index 616fdbabe..4d5f11d00 100644 --- a/packages/hooks/src/useEvidenceMapData.ts +++ b/packages/hooks/src/useEvidenceMapData.ts @@ -63,7 +63,7 @@ export interface UseEvidenceMapDataOptions { /** Findings (Layer 2 evidence counts) */ findings?: Finding[]; /** Suspected cause hubs (Layer 3 convergence enrichment) */ - suspectedCauses?: Hypothesis[]; + hypotheses?: Hypothesis[]; } export interface UseEvidenceMapDataReturn { @@ -179,17 +179,17 @@ function mapCausalEdge( /** * Map convergence points (from the causal graph) to ConvergencePointData, - * enriched with suspected cause hub data. + * enriched with hypothesis hub data. */ function mapConvergencePoint( cp: { factor: string; incomingLinks: CausalLink[] }, nodePositions: Map, - suspectedCauses: Hypothesis[] + hypotheses: Hypothesis[] ): ConvergencePointData | null { const pos = nodePositions.get(cp.factor); if (!pos) return null; - // Find a suspected cause hub that references this factor + // Find a hypothesis hub that references this factor // A hub is linked if any of its connected question/finding IDs // appear in the incoming links' question/finding IDs const incomingQuestionIds = new Set(cp.incomingLinks.flatMap(l => l.questionIds)); @@ -198,7 +198,7 @@ function mapConvergencePoint( cp.incomingLinks.map(l => l.hypothesisId).filter((id): id is string => id !== undefined) ); - const matchingHub = suspectedCauses.find( + const matchingHub = hypotheses.find( sc => incomingHubIds.has(sc.id) || sc.questionIds.some(qId => incomingQuestionIds.has(qId)) || @@ -232,7 +232,7 @@ export function useEvidenceMapData(options: UseEvidenceMapDataOptions): UseEvide causalLinks = [], questions = [], findings = [], - suspectedCauses = [], + hypotheses = [], } = options; return useMemo(() => { @@ -308,7 +308,7 @@ export function useEvidenceMapData(options: UseEvidenceMapDataOptions): UseEvide // ---- Layer 3: Convergence points ---- const convergenceRaw = findConvergencePoints(causalLinks); const convergencePoints: ConvergencePointData[] = convergenceRaw - .map(cp => mapConvergencePoint(cp, nodePositions, suspectedCauses)) + .map(cp => mapConvergencePoint(cp, nodePositions, hypotheses)) .filter((cp): cp is ConvergencePointData => cp !== null); // Determine active layer @@ -335,6 +335,6 @@ export function useEvidenceMapData(options: UseEvidenceMapDataOptions): UseEvide causalLinks, questions, findings, - suspectedCauses, + hypotheses, ]); } diff --git a/packages/hooks/src/useEvidenceMapTimeline.ts b/packages/hooks/src/useEvidenceMapTimeline.ts index f7d59af5a..579a3bdf6 100644 --- a/packages/hooks/src/useEvidenceMapTimeline.ts +++ b/packages/hooks/src/useEvidenceMapTimeline.ts @@ -2,7 +2,7 @@ * useEvidenceMapTimeline — timeline animation hook for report view replay. * * Builds a chronological sequence of frames from investigation artifacts - * (causal links, suspected causes, questions, findings) and provides + * (causal links, hypotheses, questions, findings) and provides * play/pause/seek controls for animated replay in the report view. * * Each frame represents a point in time showing which factors, links, @@ -38,7 +38,7 @@ export interface UseEvidenceMapTimelineOptions { /** Findings with createdAt timestamps (numeric, converted to ISO) */ findings?: Finding[]; /** Suspected cause hubs with createdAt timestamps */ - suspectedCauses?: Hypothesis[]; + hypotheses?: Hypothesis[]; /** Playback interval in milliseconds (default: 1500) */ intervalMs?: number; /** Maximum number of frames before grouping by day (default: 30) */ @@ -78,7 +78,7 @@ function collectArtifacts( causalLinks: CausalLink[], questions: Question[], findings: Finding[], - suspectedCauses: Hypothesis[] + hypotheses: Hypothesis[] ): TimestampedArtifact[] { const artifacts: TimestampedArtifact[] = []; @@ -111,7 +111,7 @@ function collectArtifacts( }); } - for (const sc of suspectedCauses) { + for (const sc of hypotheses) { artifacts.push({ timestamp: new Date(sc.createdAt).toISOString(), type: 'hub', @@ -217,7 +217,7 @@ export function useEvidenceMapTimeline( causalLinks = [], questions = [], findings = [], - suspectedCauses = [], + hypotheses = [], intervalMs = 1500, maxFrames = 30, } = options; @@ -228,9 +228,9 @@ export function useEvidenceMapTimeline( // Compute frames from all investigation artifacts const frames = useMemo(() => { - const artifacts = collectArtifacts(causalLinks, questions, findings, suspectedCauses); + const artifacts = collectArtifacts(causalLinks, questions, findings, hypotheses); return buildFrames(artifacts, maxFrames); - }, [causalLinks, questions, findings, suspectedCauses, maxFrames]); + }, [causalLinks, questions, findings, hypotheses, maxFrames]); // Clear interval on unmount useEffect(() => { diff --git a/packages/hooks/src/useHMWPrompts.ts b/packages/hooks/src/useHMWPrompts.ts index f2ff63deb..a1ff4c8ce 100644 --- a/packages/hooks/src/useHMWPrompts.ts +++ b/packages/hooks/src/useHMWPrompts.ts @@ -3,7 +3,7 @@ import { generateHMWPrompts } from '@variscout/core/findings'; import type { IdeaDirection } from '@variscout/core'; /** - * Generate 4 HMW prompts for a suspected cause. + * Generate 4 HMW prompts for a hypothesis. * Memoized — stable reference when inputs unchanged. */ export function useHMWPrompts( diff --git a/packages/hooks/src/useHasInvestigationContent.ts b/packages/hooks/src/useHasInvestigationContent.ts index 88303a9f4..890aa7b59 100644 --- a/packages/hooks/src/useHasInvestigationContent.ts +++ b/packages/hooks/src/useHasInvestigationContent.ts @@ -15,7 +15,7 @@ export interface UseHasInvestigationContentArgs { * findings-only graph should not advertise the Wall overlay yet. */ export function useHasInvestigationContent(args: UseHasInvestigationContentArgs): boolean { - const hubsCount = useInvestigationStore(state => state.suspectedCauses.length); + const hubsCount = useInvestigationStore(state => state.hypotheses.length); const openQuestionsCount = useInvestigationStore( state => state.questions.filter(question => question.status === 'open').length ); diff --git a/packages/hooks/src/useHubCommentStream.ts b/packages/hooks/src/useHubCommentStream.ts index 6b39b29ff..4ba921da8 100644 --- a/packages/hooks/src/useHubCommentStream.ts +++ b/packages/hooks/src/useHubCommentStream.ts @@ -54,7 +54,7 @@ const RECONNECT_BACKOFF_MS = 1000; */ function mergeIncomingComment(hubId: string, incoming: FindingComment): void { useInvestigationStore.setState(state => ({ - suspectedCauses: state.suspectedCauses.map(h => { + hypotheses: state.hypotheses.map(h => { if (h.id !== hubId) return h; const existing = h.comments ?? []; if (existing.some(c => c.id === incoming.id)) return h; diff --git a/packages/hooks/src/useHubComputations.ts b/packages/hooks/src/useHubComputations.ts index 6411acbfa..1c0264116 100644 --- a/packages/hooks/src/useHubComputations.ts +++ b/packages/hooks/src/useHubComputations.ts @@ -39,7 +39,7 @@ export function useHubComputations( bestSubsets: BestSubsetsResult | null, questions: Question[] ): UseHubComputationsReturn { - const hubs = useInvestigationStore(s => s.suspectedCauses); + const hubs = useInvestigationStore(s => s.hypotheses); const analysisMode = useProjectStore(s => s.analysisMode); const specs = useProjectStore(s => s.specs); diff --git a/packages/hooks/src/useSuspectedCauses.ts b/packages/hooks/src/useHypotheses.ts similarity index 97% rename from packages/hooks/src/useSuspectedCauses.ts rename to packages/hooks/src/useHypotheses.ts index aa3ce50c8..caedca2f6 100644 --- a/packages/hooks/src/useSuspectedCauses.ts +++ b/packages/hooks/src/useHypotheses.ts @@ -6,7 +6,7 @@ import type { Hypothesis } from '@variscout/core'; // Types // ============================================================================ -export interface UseSuspectedCausesOptions { +export interface UseHypothesesOptions { /** Initial hubs (for restoring persisted state) */ initialHubs: Hypothesis[]; /** Callback when hubs change (for external persistence) */ @@ -20,7 +20,7 @@ export type HypothesisUpdate = Partial< > >; -export interface UseSuspectedCausesReturn { +export interface UseHypothesesReturn { /** Current list of hypothesis hubs */ hubs: Hypothesis[]; /** Create a new hub and return it */ @@ -64,7 +64,7 @@ export interface UseSuspectedCausesReturn { * * Follows the same pattern as `useQuestions` and `useFindings`. */ -export function useSuspectedCauses(options: UseSuspectedCausesOptions): UseSuspectedCausesReturn { +export function useHypotheses(options: UseHypothesesOptions): UseHypothesesReturn { const { initialHubs, onHubsChange } = options; const [hubs, setHubs] = useState(initialHubs); diff --git a/packages/hooks/src/useImprovementProjections.ts b/packages/hooks/src/useImprovementProjections.ts index 491b14fc0..0b7b978bb 100644 --- a/packages/hooks/src/useImprovementProjections.ts +++ b/packages/hooks/src/useImprovementProjections.ts @@ -5,8 +5,8 @@ import type { Question } from '@variscout/core/findings'; // Types // ============================================================================ -export interface SuspectedCauseProjection { - /** Factor column name for this suspected cause */ +export interface HypothesisProjection { + /** Factor column name for this hypothesis */ factor: string; /** Projected Cpk after removing this factor's variation, if available */ projectedCpk: number | undefined; @@ -18,7 +18,7 @@ export interface UseImprovementProjectionsReturn { * Derived from questions with `causeRole === 'suspected-cause'` that have * a linked factor column. */ - suspectedCauses: SuspectedCauseProjection[]; + hypotheses: HypothesisProjection[]; /** * Combined projected Cpk — the maximum of all per-factor projections. * Returns `undefined` when no projections are available. @@ -39,13 +39,13 @@ export interface UseImprovementProjectionsReturn { * * @param questions - All questions from `useQuestionGeneration` * @param projectedCpkMap - Per-factor projected Cpk map from `useQuestionGeneration` - * @returns `{ suspectedCauses, combinedProjectedCpk }` + * @returns `{ hypotheses, combinedProjectedCpk }` */ export function useImprovementProjections( questions: Question[], projectedCpkMap: Record ): UseImprovementProjectionsReturn { - const suspectedCauses = useMemo(() => { + const hypotheses = useMemo(() => { return questions .filter(q => q.causeRole === 'suspected-cause' && q.factor) .map(q => ({ @@ -59,5 +59,5 @@ export function useImprovementProjections( return values.length > 0 ? Math.max(...values) : undefined; }, [projectedCpkMap]); - return { suspectedCauses, combinedProjectedCpk }; + return { hypotheses, combinedProjectedCpk }; } diff --git a/packages/hooks/src/useKnowledgeSearch.ts b/packages/hooks/src/useKnowledgeSearch.ts index 272958351..0a15efdaf 100644 --- a/packages/hooks/src/useKnowledgeSearch.ts +++ b/packages/hooks/src/useKnowledgeSearch.ts @@ -13,7 +13,7 @@ export interface KnowledgeResult { etaSquared: number | null; cpkBefore: number | null; cpkAfter: number | null; - suspectedCause: string; + hypothesis: string; actionsText: string; outcomeEffective: boolean | null; score: number; diff --git a/packages/hooks/src/useProblemStatement.ts b/packages/hooks/src/useProblemStatement.ts index 78bbc1fd4..9407c3a95 100644 --- a/packages/hooks/src/useProblemStatement.ts +++ b/packages/hooks/src/useProblemStatement.ts @@ -44,7 +44,7 @@ export interface UseProblemStatementOptions { } export interface UseProblemStatementReturn { - /** Whether there are enough suspected causes to generate (legacy — checks question suspected causes) */ + /** Whether there are enough hypotheses to generate (legacy — checks question hypotheses) */ isReady: boolean; /** * Whether the Problem Statement can form early from Watson Q1+Q2+Q3: @@ -85,7 +85,7 @@ export interface UseProblemStatementReturn { * Supports two formation modes: * 1. **Early formation** (`isFormable`): Q1 (outcome) + Q2 (characteristicType) + Q3 (locationFactor) * known at FRAME/SCOUT Loop 1 — produces `liveStatement` automatically. - * 2. **Legacy formation** (`isReady`): Requires question-based suspected causes — produces + * 2. **Legacy formation** (`isReady`): Requires question-based hypotheses — produces * `generatedDraft` via manual `generate()` call. * * Both modes are backward compatible: existing callers passing only `outcome` and `questions` @@ -112,8 +112,8 @@ export function useProblemStatement({ // Early formation: all three Watson questions answered const isFormable = q1Ready && q2Ready && hasScope && locationFactor != null; - // Legacy suspected causes from question tree - const suspectedCauses = useMemo( + // Legacy hypotheses from question tree + const hypotheses = useMemo( () => questions .filter(q => q.causeRole === 'suspected-cause' && q.factor) @@ -125,8 +125,8 @@ export function useProblemStatement({ [questions] ); - // Legacy readiness: requires at least one question-based suspected cause - const isReady = suspectedCauses.length > 0 && q1Ready; + // Legacy readiness: requires at least one question-based hypothesis + const isReady = hypotheses.length > 0 && q1Ready; // canGenerateDraft: true when either early or legacy formation path is viable const canGenerateDraft = q1Ready && hasScope; @@ -141,24 +141,16 @@ export function useProblemStatement({ level: locationFactor.level, evidence: locationFactor.evidence, }; - const additionalCauses = suspectedCauses.filter(c => c.factor !== locationFactor.factor); + const additionalCauses = hypotheses.filter(c => c.factor !== locationFactor.factor); return buildProblemStatement({ outcome, targetValue: targetCpk, characteristicType, currentCpk, - suspectedCauses: [locationCause, ...additionalCauses], + hypotheses: [locationCause, ...additionalCauses], }); - }, [ - isFormable, - outcome, - locationFactor, - characteristicType, - targetCpk, - currentCpk, - suspectedCauses, - ]); + }, [isFormable, outcome, locationFactor, characteristicType, targetCpk, currentCpk, hypotheses]); // Legacy generatedDraft (for the manual generate() flow) // Pass characteristicType so direction is derived consistently with liveStatement. @@ -171,9 +163,9 @@ export function useProblemStatement({ targetValue: targetCpk, characteristicType, currentCpk, - suspectedCauses, + hypotheses, }); - }, [isReady, outcome, targetCpk, characteristicType, currentCpk, suspectedCauses]); + }, [isReady, outcome, targetCpk, characteristicType, currentCpk, hypotheses]); const generate = useCallback(() => { if (generatedDraft) setDraft(generatedDraft); diff --git a/packages/hooks/src/useProjectActions.ts b/packages/hooks/src/useProjectActions.ts index aee230f82..e163255fa 100644 --- a/packages/hooks/src/useProjectActions.ts +++ b/packages/hooks/src/useProjectActions.ts @@ -105,7 +105,7 @@ function buildSerializedProject( findings: state.findings ?? [], questions: state.questions ?? [], categories: state.categories ?? [], - suspectedCauses: state.suspectedCauses ?? [], + hypotheses: state.hypotheses ?? [], causalLinks: state.causalLinks ?? [], }; } @@ -170,7 +170,7 @@ function getCurrentStateFromStores(): Omit { if (is.findings.length > 0) state.findings = is.findings; if (is.questions.length > 0) state.questions = is.questions; if (is.categories.length > 0) state.categories = is.categories; - if (is.suspectedCauses.length > 0) state.suspectedCauses = is.suspectedCauses; + if (is.hypotheses.length > 0) state.hypotheses = is.hypotheses; if (is.causalLinks.length > 0) state.causalLinks = is.causalLinks; return state; @@ -221,7 +221,7 @@ export function useProjectActions(persistence: PersistenceAdapter): ProjectActio findings: state.findings ?? [], questions: state.questions ?? [], categories: state.categories ?? [], - suspectedCauses: state.suspectedCauses ?? [], + hypotheses: state.hypotheses ?? [], causalLinks: state.causalLinks ?? [], }); }, @@ -292,7 +292,7 @@ export function useProjectActions(persistence: PersistenceAdapter): ProjectActio findings: state.findings ?? [], questions: state.questions ?? [], categories: state.categories ?? [], - suspectedCauses: state.suspectedCauses ?? [], + hypotheses: state.hypotheses ?? [], causalLinks: state.causalLinks ?? [], }); }, diff --git a/packages/hooks/src/useSharedWallProps.ts b/packages/hooks/src/useSharedWallProps.ts index d0bf36fbf..917520982 100644 --- a/packages/hooks/src/useSharedWallProps.ts +++ b/packages/hooks/src/useSharedWallProps.ts @@ -25,7 +25,7 @@ export interface UseSharedWallPropsReturn { } export function useSharedWallProps(args: UseSharedWallPropsArgs): UseSharedWallPropsReturn { - const hubs = useInvestigationStore(s => s.suspectedCauses); + const hubs = useInvestigationStore(s => s.hypotheses); const questions = useInvestigationStore(s => s.questions); const zoom = useWallLayoutStore(s => s.zoom); const pan = useWallLayoutStore(s => s.pan); diff --git a/packages/stores/src/__tests__/investigationStore.test.ts b/packages/stores/src/__tests__/investigationStore.test.ts index 3d6cee52a..21284e4fa 100644 --- a/packages/stores/src/__tests__/investigationStore.test.ts +++ b/packages/stores/src/__tests__/investigationStore.test.ts @@ -480,30 +480,30 @@ describe('investigationStore — question ideas', () => { // Hub tests // ============================================================================ -describe('investigationStore — suspected cause hubs', () => { +describe('investigationStore — hypothesis hubs', () => { it('creates a hub', () => { const hub = useInvestigationStore .getState() .createHub('Nozzle wear', 'Night shift causes wear'); const state = useInvestigationStore.getState(); - expect(state.suspectedCauses).toHaveLength(1); - expect(state.suspectedCauses[0].name).toBe('Nozzle wear'); - expect(state.suspectedCauses[0].synthesis).toBe('Night shift causes wear'); - expect(state.suspectedCauses[0].status).toBe('proposed'); - expect(hub.id).toBe(state.suspectedCauses[0].id); + expect(state.hypotheses).toHaveLength(1); + expect(state.hypotheses[0].name).toBe('Nozzle wear'); + expect(state.hypotheses[0].synthesis).toBe('Night shift causes wear'); + expect(state.hypotheses[0].status).toBe('proposed'); + expect(hub.id).toBe(state.hypotheses[0].id); }); it('updates a hub', () => { const hub = useInvestigationStore.getState().createHub('Old name', 'Old synthesis'); useInvestigationStore.getState().updateHub(hub.id, { name: 'New name' }); - expect(useInvestigationStore.getState().suspectedCauses[0].name).toBe('New name'); - expect(useInvestigationStore.getState().suspectedCauses[0].synthesis).toBe('Old synthesis'); + expect(useInvestigationStore.getState().hypotheses[0].name).toBe('New name'); + expect(useInvestigationStore.getState().hypotheses[0].synthesis).toBe('Old synthesis'); }); it('createHubFromFinding returns null when the finding does not exist', () => { const result = useInvestigationStore.getState().createHubFromFinding('missing-id'); expect(result).toBeNull(); - expect(useInvestigationStore.getState().suspectedCauses).toHaveLength(0); + expect(useInvestigationStore.getState().hypotheses).toHaveLength(0); }); it('createHubFromFinding creates a hub seeded from the finding text and links it', () => { @@ -514,8 +514,8 @@ describe('investigationStore — suspected cause hubs', () => { const hub = useInvestigationStore.getState().createHubFromFinding(finding.id); expect(hub).not.toBeNull(); const state = useInvestigationStore.getState(); - expect(state.suspectedCauses).toHaveLength(1); - const persisted = state.suspectedCauses[0]; + expect(state.hypotheses).toHaveLength(1); + const persisted = state.hypotheses[0]; expect(persisted.findingIds).toEqual([finding.id]); expect(persisted.name.startsWith('Suspected mechanism:')).toBe(true); expect(persisted.status).toBe('proposed'); @@ -525,7 +525,7 @@ describe('investigationStore — suspected cause hubs', () => { const ctx = makeContext(); const finding = useInvestigationStore.getState().addFinding(' ', ctx); useInvestigationStore.getState().createHubFromFinding(finding.id); - expect(useInvestigationStore.getState().suspectedCauses[0].name).toBe('New mechanism branch'); + expect(useInvestigationStore.getState().hypotheses[0].name).toBe('New mechanism branch'); }); it('connects question and finding to hub', () => { @@ -533,7 +533,7 @@ describe('investigationStore — suspected cause hubs', () => { useInvestigationStore.getState().connectQuestionToHub(hub.id, 'q-1'); useInvestigationStore.getState().connectFindingToHub(hub.id, 'f-1'); - const updated = useInvestigationStore.getState().suspectedCauses[0]; + const updated = useInvestigationStore.getState().hypotheses[0]; expect(updated.questionIds).toEqual(['q-1']); expect(updated.findingIds).toEqual(['f-1']); }); @@ -542,14 +542,14 @@ describe('investigationStore — suspected cause hubs', () => { const hub = useInvestigationStore.getState().createHub('Test', 'Synth'); useInvestigationStore.getState().connectQuestionToHub(hub.id, 'q-1'); useInvestigationStore.getState().connectQuestionToHub(hub.id, 'q-1'); - expect(useInvestigationStore.getState().suspectedCauses[0].questionIds).toEqual(['q-1']); + expect(useInvestigationStore.getState().hypotheses[0].questionIds).toEqual(['q-1']); }); it('no-op when connecting already-connected finding', () => { const hub = useInvestigationStore.getState().createHub('Test', 'Synth'); useInvestigationStore.getState().connectFindingToHub(hub.id, 'f-1'); useInvestigationStore.getState().connectFindingToHub(hub.id, 'f-1'); - expect(useInvestigationStore.getState().suspectedCauses[0].findingIds).toEqual(['f-1']); + expect(useInvestigationStore.getState().hypotheses[0].findingIds).toEqual(['f-1']); }); it('disconnects question from hub', () => { @@ -557,26 +557,26 @@ describe('investigationStore — suspected cause hubs', () => { useInvestigationStore.getState().connectQuestionToHub(hub.id, 'q-1'); useInvestigationStore.getState().connectQuestionToHub(hub.id, 'q-2'); useInvestigationStore.getState().disconnectQuestionFromHub(hub.id, 'q-1'); - expect(useInvestigationStore.getState().suspectedCauses[0].questionIds).toEqual(['q-2']); + expect(useInvestigationStore.getState().hypotheses[0].questionIds).toEqual(['q-2']); }); it('disconnects finding from hub', () => { const hub = useInvestigationStore.getState().createHub('Test', 'Synth'); useInvestigationStore.getState().connectFindingToHub(hub.id, 'f-1'); useInvestigationStore.getState().disconnectFindingFromHub(hub.id, 'f-1'); - expect(useInvestigationStore.getState().suspectedCauses[0].findingIds).toEqual([]); + expect(useInvestigationStore.getState().hypotheses[0].findingIds).toEqual([]); }); it('deletes a hub', () => { const hub = useInvestigationStore.getState().createHub('Test', 'Synth'); useInvestigationStore.getState().deleteHub(hub.id); - expect(useInvestigationStore.getState().suspectedCauses).toHaveLength(0); + expect(useInvestigationStore.getState().hypotheses).toHaveLength(0); }); it('sets hub status', () => { const hub = useInvestigationStore.getState().createHub('Test', 'Synth'); useInvestigationStore.getState().setHubStatus(hub.id, 'confirmed'); - expect(useInvestigationStore.getState().suspectedCauses[0].status).toBe('confirmed'); + expect(useInvestigationStore.getState().hypotheses[0].status).toBe('confirmed'); }); it('sets hub evidence', () => { @@ -590,13 +590,13 @@ describe('investigationStore — suspected cause hubs', () => { }, }; useInvestigationStore.getState().setHubEvidence(hub.id, evidence); - expect(useInvestigationStore.getState().suspectedCauses[0].evidence).toEqual(evidence); + expect(useInvestigationStore.getState().hypotheses[0].evidence).toEqual(evidence); }); it('resets hubs atomically', () => { useInvestigationStore.getState().createHub('A', 'a'); useInvestigationStore.getState().createHub('B', 'b'); - expect(useInvestigationStore.getState().suspectedCauses).toHaveLength(2); + expect(useInvestigationStore.getState().hypotheses).toHaveLength(2); const newHubs: Hypothesis[] = [ { @@ -613,7 +613,7 @@ describe('investigationStore — suspected cause hubs', () => { }, ]; useInvestigationStore.getState().resetHubs(newHubs); - expect(useInvestigationStore.getState().suspectedCauses).toEqual(newHubs); + expect(useInvestigationStore.getState().hypotheses).toEqual(newHubs); }); }); @@ -645,7 +645,7 @@ describe('investigationStore — addHubComment', () => { // Comment is visible synchronously on the hub — optimistic update landed // before the promise resolves. - const liveHub = useInvestigationStore.getState().suspectedCauses[0]; + const liveHub = useInvestigationStore.getState().hypotheses[0]; expect(liveHub.comments).toHaveLength(1); expect(liveHub.comments?.[0]?.text).toBe('First thought'); expect(liveHub.comments?.[0]?.author).toBe('Jane'); @@ -705,7 +705,7 @@ describe('investigationStore — addHubComment', () => { expect(mockFetch).not.toHaveBeenCalled(); // Still optimistically appended locally. - const liveHub = useInvestigationStore.getState().suspectedCauses[0]; + const liveHub = useInvestigationStore.getState().hypotheses[0]; expect(liveHub.comments).toHaveLength(1); }); @@ -778,14 +778,14 @@ describe('investigationStore — bulk operations', () => { useInvestigationStore.getState().loadInvestigationState({ findings: [finding], questions: [question], - suspectedCauses: [hub], + hypotheses: [hub], categories: [category], }); const state = useInvestigationStore.getState(); expect(state.findings).toEqual([finding]); expect(state.questions).toEqual([question]); - expect(state.suspectedCauses).toEqual([hub]); + expect(state.hypotheses).toEqual([hub]); expect(state.categories).toEqual([category]); }); @@ -806,7 +806,7 @@ describe('investigationStore — bulk operations', () => { const state = useInvestigationStore.getState(); expect(state.findings).toEqual([]); expect(state.questions).toEqual([]); - expect(state.suspectedCauses).toEqual([]); + expect(state.hypotheses).toEqual([]); expect(state.categories).toEqual([]); }); diff --git a/packages/stores/src/investigationStore.ts b/packages/stores/src/investigationStore.ts index 10356f4ed..1bda9ef18 100644 --- a/packages/stores/src/investigationStore.ts +++ b/packages/stores/src/investigationStore.ts @@ -1,7 +1,7 @@ /** - * investigationStore — Zustand store for findings, questions, and suspected cause hubs + * investigationStore — Zustand store for findings, questions, and hypothesis hubs * - * Consolidates CRUD from useFindings, useQuestions, and useSuspectedCauses hooks + * Consolidates CRUD from useFindings, useQuestions, and useHypotheses hooks * into a single store. No callbacks — persistence will be via store.subscribe(). * No React imports — pure Zustand, framework-agnostic. */ @@ -72,7 +72,7 @@ export const MAX_CHILDREN_PER_PARENT = 8; export interface InvestigationState { findings: Finding[]; questions: Question[]; - suspectedCauses: Hypothesis[]; + hypotheses: Hypothesis[]; causalLinks: CausalLink[]; categories: InvestigationCategory[]; problemContributionTree?: GateNode; @@ -302,7 +302,7 @@ function collectDescendants(id: string, questions: Question[]): Set { const initialState: InvestigationState = { findings: [], questions: [], - suspectedCauses: [], + hypotheses: [], causalLinks: [], categories: [], problemContributionTree: undefined, @@ -839,7 +839,7 @@ export const useInvestigationStore = create { const hub = createHypothesis(name, synthesis); - set(state => ({ suspectedCauses: [...state.suspectedCauses, hub] })); + set(state => ({ hypotheses: [...state.hypotheses, hub] })); return hub; }, @@ -849,13 +849,13 @@ export const useInvestigationStore = create 0 ? `Suspected mechanism: ${excerpt}` : 'New mechanism branch'; const hub = createHypothesis(name, '', [], [findingId]); - set(state => ({ suspectedCauses: [...state.suspectedCauses, hub] })); + set(state => ({ hypotheses: [...state.hypotheses, hub] })); return hub; }, updateHub: (hubId, updates) => { set(state => ({ - suspectedCauses: state.suspectedCauses.map(h => + hypotheses: state.hypotheses.map(h => h.id === hubId ? { ...h, ...updates, updatedAt: Date.now() } : h ), })); @@ -863,7 +863,7 @@ export const useInvestigationStore = create { set(state => ({ - suspectedCauses: state.suspectedCauses.filter(h => h.id !== hubId), + hypotheses: state.hypotheses.filter(h => h.id !== hubId), // Clear hypothesisId from causal links that reference the deleted hub causalLinks: state.causalLinks.map(l => l.hypothesisId === hubId ? { ...l, hypothesisId: undefined, updatedAt: Date.now() } : l @@ -873,7 +873,7 @@ export const useInvestigationStore = create { set(state => ({ - suspectedCauses: state.suspectedCauses.map(h => { + hypotheses: state.hypotheses.map(h => { if (h.id !== hubId) return h; if (h.questionIds.includes(questionId)) return h; return { @@ -887,7 +887,7 @@ export const useInvestigationStore = create { set(state => ({ - suspectedCauses: state.suspectedCauses.map(h => + hypotheses: state.hypotheses.map(h => h.id !== hubId ? h : { @@ -901,7 +901,7 @@ export const useInvestigationStore = create { set(state => ({ - suspectedCauses: state.suspectedCauses.map(h => { + hypotheses: state.hypotheses.map(h => { if (h.id !== hubId) return h; if (h.findingIds.includes(findingId)) return h; return { @@ -915,7 +915,7 @@ export const useInvestigationStore = create { set(state => ({ - suspectedCauses: state.suspectedCauses.map(h => + hypotheses: state.hypotheses.map(h => h.id !== hubId ? h : { @@ -929,7 +929,7 @@ export const useInvestigationStore = create { set(state => ({ - suspectedCauses: state.suspectedCauses.map(h => + hypotheses: state.hypotheses.map(h => h.id !== hubId ? h : { ...h, status, updatedAt: Date.now() } ), })); @@ -937,14 +937,14 @@ export const useInvestigationStore = create { set(state => ({ - suspectedCauses: state.suspectedCauses.map(h => + hypotheses: state.hypotheses.map(h => h.id !== hubId ? h : { ...h, evidence, updatedAt: Date.now() } ), })); }, resetHubs: hubs => { - set({ suspectedCauses: hubs }); + set({ hypotheses: hubs }); }, addHubComment: async (hubId, text, author) => { @@ -955,7 +955,7 @@ export const useInvestigationStore = create ({ - suspectedCauses: state.suspectedCauses.map(h => + hypotheses: state.hypotheses.map(h => h.id === hubId ? { ...h, @@ -1104,7 +1104,7 @@ export const useInvestigationStore = create ({ findings: partial.findings ?? state.findings, questions: partial.questions ?? state.questions, - suspectedCauses: partial.suspectedCauses ?? state.suspectedCauses, + hypotheses: partial.hypotheses ?? state.hypotheses, causalLinks: partial.causalLinks ?? state.causalLinks, categories: partial.categories ?? state.categories, problemContributionTree: partial.problemContributionTree ?? state.problemContributionTree, diff --git a/packages/stores/src/projectStore.ts b/packages/stores/src/projectStore.ts index cda9ad21e..ef09c9c91 100644 --- a/packages/stores/src/projectStore.ts +++ b/packages/stores/src/projectStore.ts @@ -97,7 +97,7 @@ export interface SerializedProject { findings?: Finding[]; questions?: Question[]; categories?: InvestigationCategory[]; - suspectedCauses?: Hypothesis[]; + hypotheses?: Hypothesis[]; causalLinks?: CausalLink[]; } diff --git a/packages/ui/src/components/Canvas/CanvasWorkspace.tsx b/packages/ui/src/components/Canvas/CanvasWorkspace.tsx index 5d16359f7..4f0af4cac 100644 --- a/packages/ui/src/components/Canvas/CanvasWorkspace.tsx +++ b/packages/ui/src/components/Canvas/CanvasWorkspace.tsx @@ -54,7 +54,7 @@ export interface CanvasWorkspaceProps { onHandoff?: (stepId: string) => void; questions?: readonly Question[]; findings?: readonly Finding[]; - suspectedCauses?: readonly Hypothesis[]; + hypotheses?: readonly Hypothesis[]; causalLinks?: readonly CausalLink[]; problemCpk?: number; eventsPerWeek?: number; @@ -170,7 +170,7 @@ export const CanvasWorkspace: React.FC = ({ onHandoff, questions = [], findings = [], - suspectedCauses = [], + hypotheses = [], causalLinks = [], problemCpk, eventsPerWeek, @@ -310,7 +310,7 @@ export const CanvasWorkspace: React.FC = ({ map, questions, findings, - suspectedCauses, + hypotheses, causalLinks, }); diff --git a/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx b/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx index 58957eb86..0314ec141 100644 --- a/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx +++ b/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx @@ -157,7 +157,7 @@ const investigationOverlays: CanvasInvestigationOverlayModel = { focus: { kind: 'finding', id: 'f-1', questionId: 'q-1' }, }, ], - suspectedCauses: [ + hypotheses: [ { id: 'hub-1', name: 'Pressure setup drift', @@ -182,7 +182,7 @@ const investigationOverlays: CanvasInvestigationOverlayModel = { stepId: 'step-2', questions: [], findings: [], - suspectedCauses: [], + hypotheses: [], causalLinks: [], investigationCounts: { open: 0, supported: 0, refuted: 0 }, }, @@ -197,7 +197,7 @@ const investigationOverlays: CanvasInvestigationOverlayModel = { focus: { kind: 'causal-link', id: 'link-1', questionId: 'q-1' }, }, ], - unresolved: { questions: [], findings: [], suspectedCauses: [], causalLinks: [] }, + unresolved: { questions: [], findings: [], hypotheses: [], causalLinks: [] }, }; function setViewport(width: number, height: number) { @@ -799,7 +799,7 @@ describe('Canvas', () => { signals={SIGNALS} stepCards={stepCards} investigationOverlays={investigationOverlays} - activeOverlays={['investigations', 'findings', 'suspected-causes']} + activeOverlays={['investigations', 'findings', 'hypothesis-hubs']} /> ); diff --git a/packages/ui/src/components/Canvas/__tests__/CanvasWorkspace.test.tsx b/packages/ui/src/components/Canvas/__tests__/CanvasWorkspace.test.tsx index 37eb79f9f..3fa2c8aa2 100644 --- a/packages/ui/src/components/Canvas/__tests__/CanvasWorkspace.test.tsx +++ b/packages/ui/src/components/Canvas/__tests__/CanvasWorkspace.test.tsx @@ -215,13 +215,13 @@ const mockInvestigationOverlays: CanvasInvestigationOverlayModel = { }, ], findings: [], - suspectedCauses: [], + hypotheses: [], causalLinks: [], investigationCounts: { open: 1, supported: 0, refuted: 0 }, }, }, arrows: [], - unresolved: { questions: [], findings: [], suspectedCauses: [], causalLinks: [] }, + unresolved: { questions: [], findings: [], hypotheses: [], causalLinks: [] }, }; vi.mock('@variscout/hooks', () => ({ @@ -270,9 +270,9 @@ vi.mock('@variscout/hooks', () => ({ enabled: true, description: 'Draft causal links rendered as faint step-to-step arrows.', }, - 'suspected-causes': { - id: 'suspected-causes', - label: 'Suspected causes', + 'hypothesis-hubs': { + id: 'hypothesis-hubs', + label: 'Hypothesis hubs', enabled: true, description: 'Promoted mechanism branches rendered as step markers.', }, @@ -291,7 +291,7 @@ vi.mock('@variscout/hooks', () => ({ }, coerceCanvasOverlays: vi.fn((values: unknown[]) => values.filter(value => - ['investigations', 'hypotheses', 'suspected-causes', 'findings', 'wall'].includes( + ['investigations', 'hypotheses', 'hypothesis-hubs', 'findings', 'wall'].includes( String(value) ) ) @@ -310,8 +310,8 @@ vi.mock('@variscout/hooks', () => ({ description: 'Draft causal links rendered as faint step-to-step arrows.', }, { - id: 'suspected-causes', - label: 'Suspected causes', + id: 'hypothesis-hubs', + label: 'Hypothesis hubs', enabled: true, description: 'Promoted mechanism branches rendered as step markers.', }, diff --git a/packages/ui/src/components/Canvas/index.tsx b/packages/ui/src/components/Canvas/index.tsx index 172db7c18..c133459fd 100644 --- a/packages/ui/src/components/Canvas/index.tsx +++ b/packages/ui/src/components/Canvas/index.tsx @@ -231,12 +231,7 @@ export const Canvas: React.FC = ({ const hasInvestigationContent = useHasInvestigationContent({ findingsCount: findings.length }); const wallIsMobile = useWallIsMobile(); const availableOverlays = React.useMemo(() => { - const base: CanvasOverlayId[] = [ - 'investigations', - 'hypotheses', - 'suspected-causes', - 'findings', - ]; + const base: CanvasOverlayId[] = ['investigations', 'hypotheses', 'hypothesis-hubs', 'findings']; return hasInvestigationContent ? [...base, 'wall'] : base; }, [hasInvestigationContent]); const pickerAvailableOverlays = React.useMemo( diff --git a/packages/ui/src/components/Canvas/internal/CanvasStepCard.tsx b/packages/ui/src/components/Canvas/internal/CanvasStepCard.tsx index faba2a7a1..259685ff3 100644 --- a/packages/ui/src/components/Canvas/internal/CanvasStepCard.tsx +++ b/packages/ui/src/components/Canvas/internal/CanvasStepCard.tsx @@ -59,8 +59,8 @@ export const CanvasStepCard: React.FC = ({ const showCapability = activeLens === 'capability' || activeLens === 'default'; const showInvestigations = activeOverlays.includes('investigations') && investigationOverlay; const showFindings = activeOverlays.includes('findings') && investigationOverlay?.findings.length; - const showSuspectedCauses = - activeOverlays.includes('suspected-causes') && investigationOverlay?.suspectedCauses.length; + const showHypotheses = + activeOverlays.includes('hypothesis-hubs') && investigationOverlay?.hypotheses.length; const activityCount = investigationOverlay ? investigationOverlay.investigationCounts.open + investigationOverlay.investigationCounts.supported + @@ -128,9 +128,9 @@ export const CanvasStepCard: React.FC = ({ {investigationOverlay?.findings.length} finding ) : null} - {showSuspectedCauses ? ( + {showHypotheses ? ( ({ + hubs={(investigationOverlay?.hypotheses ?? []).map(cause => ({ id: cause.id, name: cause.name, status: cause.status, diff --git a/packages/ui/src/components/Canvas/internal/CanvasStepOverlay.tsx b/packages/ui/src/components/Canvas/internal/CanvasStepOverlay.tsx index 5915e3ad5..7149643fe 100644 --- a/packages/ui/src/components/Canvas/internal/CanvasStepOverlay.tsx +++ b/packages/ui/src/components/Canvas/internal/CanvasStepOverlay.tsx @@ -288,10 +288,10 @@ export const CanvasStepOverlay: React.FC = ({ {investigationOverlay && (investigationOverlay.questions.length > 0 || investigationOverlay.findings.length > 0 || - investigationOverlay.suspectedCauses.length > 0 || + investigationOverlay.hypotheses.length > 0 || investigationOverlay.causalLinks.length > 0) ? (
- {investigationOverlay.suspectedCauses.map(cause => ( + {investigationOverlay.hypotheses.map(cause => ( )}
@@ -243,7 +243,7 @@ const InvestigationConclusion: React.FC = ({ )}
) : sortedCauses.length > 0 ? ( -
+
Suspected Causes diff --git a/packages/ui/src/components/FindingsWindow/InvestigationSidebar.tsx b/packages/ui/src/components/FindingsWindow/InvestigationSidebar.tsx index 50b23f905..04c2c2cd3 100644 --- a/packages/ui/src/components/FindingsWindow/InvestigationSidebar.tsx +++ b/packages/ui/src/components/FindingsWindow/InvestigationSidebar.tsx @@ -95,7 +95,7 @@ const InvestigationSidebar: React.FC = ({ }, [factorRoles, treeQuestions]); // Compute conclusion data from questions (must be before early return) - const suspectedCauses = React.useMemo( + const hypotheses = React.useMemo( () => (questions ?? []).filter(q => q.causeRole === 'suspected-cause'), [questions] ); @@ -112,7 +112,7 @@ const InvestigationSidebar: React.FC = ({ () => (questions ?? []).filter(q => q.causeRole === 'contributing'), [questions] ); - const hasConclusionData = suspectedCauses.length > 0 || ruledOut.length > 0; + const hasConclusionData = hypotheses.length > 0 || ruledOut.length > 0; const hasQuestions = questions && questions.length > 0; // Toggle button (always visible) @@ -225,10 +225,10 @@ const InvestigationSidebar: React.FC = ({ /> )} - {/* Investigation conclusions (suspected causes, ruled out) */} + {/* Investigation conclusions (hypotheses, ruled out) */} {hasConclusionData && ( { it('returns null when hasConclusions is false', () => { const { container } = render( { expect(container.innerHTML).toBe(''); }); - it('renders suspected causes section', () => { + it('renders hypotheses section', () => { const causes = [ makeQuestion({ id: 'h1', @@ -38,14 +38,9 @@ describe('InvestigationConclusion', () => { }), ]; render( - + ); - expect(screen.getByTestId('suspected-causes')).toBeDefined(); + expect(screen.getByTestId('hypothesis-hubs')).toBeDefined(); expect(screen.getByText('Shift changeover procedure')).toBeDefined(); }); @@ -60,7 +55,7 @@ describe('InvestigationConclusion', () => { ]; render( { it('shows problem statement when provided', () => { render( { ]; render( ; /** Model-based projections per hub (from computeHubProjection) */ hubProjections?: Map; onNavigateToInvestigation?: () => void; } -const HUB_STATUS_DOT: Record = { +const HUB_STATUS_DOT: Record = { proposed: 'bg-slate-400', evidenced: 'bg-amber-500', confirmed: 'bg-green-500', @@ -33,15 +33,15 @@ const HUB_STATUS_DOT: Record = { }; /** - * ConclusionCard — shows suspected causes with Cpk projections when the - * investigation is converging. Returns null when no suspected causes exist. + * ConclusionCard — shows hypotheses with Cpk projections when the + * investigation is converging. Returns null when no hypotheses exist. * * Supports two display modes: - * - Legacy chip model (suspectedCauses prop) + * - Legacy chip model (hypotheses prop) * - Hub model (hubs prop) — takes precedence when provided */ const ConclusionCard: React.FC = ({ - suspectedCauses, + hypotheses, currentCpk, combinedProjectedCpk, targetCpk, @@ -52,7 +52,7 @@ const ConclusionCard: React.FC = ({ }) => { const useHubModel = hubs !== undefined && hubs.length > 0; - if (!useHubModel && suspectedCauses.length === 0) { + if (!useHubModel && hypotheses.length === 0) { return null; } @@ -68,7 +68,7 @@ const ConclusionCard: React.FC = ({ > {/* Header */}
- Suspected causes + Hypotheses
{/* Hub-based display */} @@ -129,7 +129,7 @@ const ConclusionCard: React.FC = ({ ) : ( /* Legacy chip display */
- {suspectedCauses.map(cause => { + {hypotheses.map(cause => { const delta = cause.projectedCpk !== undefined && currentCpk !== undefined ? cause.projectedCpk - currentCpk diff --git a/packages/ui/src/components/ProcessIntelligencePanel/QuestionsTabContent.tsx b/packages/ui/src/components/ProcessIntelligencePanel/QuestionsTabContent.tsx index 3a905217a..40dff36eb 100644 --- a/packages/ui/src/components/ProcessIntelligencePanel/QuestionsTabContent.tsx +++ b/packages/ui/src/components/ProcessIntelligencePanel/QuestionsTabContent.tsx @@ -49,7 +49,7 @@ export interface QuestionsTabContentProps { * QuestionsTabContent — store-aware content for the "Questions" tab in the PI Panel. * * Reads from stores: - * - questions, findings, suspectedCauses from useInvestigationStore + * - questions, findings, hypotheses from useInvestigationStore * - processContext (issueStatement/currentUnderstanding), cpkTarget from useProjectStore * - currentCpk via useAnalysisStats() * @@ -77,7 +77,7 @@ const QuestionsTabContent: React.FC = ({ // Store reads const questions = useInvestigationStore(s => s.questions); const findings = useInvestigationStore(s => s.findings); - const hubs = useInvestigationStore(s => s.suspectedCauses); + const hubs = useInvestigationStore(s => s.hypotheses); const processContext = useProjectStore(s => s.processContext); const projectCpkTarget = useProjectStore(s => s.cpkTarget); const outcome = useProjectStore(s => s.outcome); @@ -99,7 +99,7 @@ const QuestionsTabContent: React.FC = ({ const { hubEvidences, hubProjections } = useHubComputations(bestSubsets, questions); // Improvement projections (shared hook, Task 3) - const { suspectedCauses, combinedProjectedCpk } = useImprovementProjections( + const { hypotheses, combinedProjectedCpk } = useImprovementProjections( questions, projectedCpkMap ); @@ -118,7 +118,7 @@ const QuestionsTabContent: React.FC = ({ currentCpk={currentCpk} targetCpk={cpkTarget} phaseBadge={phaseBadge} - suspectedCauses={suspectedCauses} + hypotheses={hypotheses} combinedProjectedCpk={combinedProjectedCpk} projectedCpkMap={projectedCpkMap} hubs={hubs.length > 0 ? hubs : undefined} diff --git a/packages/ui/src/components/ProcessIntelligencePanel/QuestionsTabView.tsx b/packages/ui/src/components/ProcessIntelligencePanel/QuestionsTabView.tsx index bfa8415da..06cde8b1a 100644 --- a/packages/ui/src/components/ProcessIntelligencePanel/QuestionsTabView.tsx +++ b/packages/ui/src/components/ProcessIntelligencePanel/QuestionsTabView.tsx @@ -5,7 +5,7 @@ import type { CurrentUnderstanding } from '@variscout/core'; import type { QuestionStatus } from '@variscout/core/findings'; import type { BestSubsetResult } from '@variscout/core/stats'; import type { - Hypothesis as SuspectedCauseHub, + Hypothesis as HypothesisHub, HypothesisEvidence, HubProjection, } from '@variscout/core'; @@ -28,7 +28,7 @@ export interface QuestionsTabViewProps { targetCpk?: number; phaseBadge?: string; activeQuestionId?: string | null; - suspectedCauses?: Hypothesis[]; + hypotheses?: Hypothesis[]; combinedProjectedCpk?: number; /** Record of question id → projected Cpk value (for expanded QuestionRow detail) */ projectedCpkMap?: Record; @@ -54,8 +54,8 @@ export interface QuestionsTabViewProps { onAddQuestion?: (text: string) => void; onAddObservation?: (text: string) => void; onLinkObservation?: (findingId: string, questionId: string) => void; - /** Hub-based suspected causes (new model) — passed to ConclusionCard */ - hubs?: SuspectedCauseHub[]; + /** Hub-based hypotheses (new model) — passed to ConclusionCard */ + hubs?: HypothesisHub[]; /** Hub evidence map — passed to ConclusionCard */ hubEvidences?: Map; /** Hub model projections — passed to ConclusionCard */ @@ -94,7 +94,7 @@ const QuestionsTabView: React.FC = ({ targetCpk, phaseBadge, activeQuestionId = null, - suspectedCauses = [], + hypotheses = [], combinedProjectedCpk, projectedCpkMap = {}, bestSubset, @@ -460,7 +460,7 @@ const QuestionsTabView: React.FC = ({ {/* Conclusion card */} { it('renders nothing when no causes and no hubs', () => { - const { container } = render(); + const { container } = render(); expect(container.firstChild).toBeNull(); }); @@ -56,7 +56,7 @@ describe('ConclusionCard', () => { ], ]); - render(); + render(); expect(screen.getByText('Night shift effect')).toBeInTheDocument(); expect(screen.getByText('R²adj 38%')).toBeInTheDocument(); @@ -66,7 +66,7 @@ describe('ConclusionCard', () => { const hubs = [makeHub('h1', 'Night shift effect')]; const projections = new Map([['h1', makeProjection(-1.8, 0.38)]]); - render(); + render(); const projectionEl = screen.getByTestId('conclusion-hub-projection-h1'); expect(projectionEl).toBeInTheDocument(); @@ -79,7 +79,7 @@ describe('ConclusionCard', () => { const hubs = [makeHub('h1', 'Head alignment')]; const projections = new Map([['h1', makeProjection(0.5, 0.22)]]); - render(); + render(); const projectionEl = screen.getByTestId('conclusion-hub-projection-h1'); expect(projectionEl.textContent).toContain('+0.5'); @@ -89,17 +89,14 @@ describe('ConclusionCard', () => { const hubs = [makeHub('h1', 'Unmapped hub')]; const projections = new Map(); // empty map - render(); + render(); expect(screen.queryByTestId('conclusion-hub-projection-h1')).not.toBeInTheDocument(); }); it('renders legacy chip model when no hubs provided', () => { render( - + ); expect(screen.getByText('Machine')).toBeInTheDocument(); diff --git a/packages/ui/src/components/ProcessIntelligencePanel/__tests__/QuestionsTabView.test.tsx b/packages/ui/src/components/ProcessIntelligencePanel/__tests__/QuestionsTabView.test.tsx index 78ca7a265..f3b6c49f0 100644 --- a/packages/ui/src/components/ProcessIntelligencePanel/__tests__/QuestionsTabView.test.tsx +++ b/packages/ui/src/components/ProcessIntelligencePanel/__tests__/QuestionsTabView.test.tsx @@ -245,17 +245,15 @@ describe('QuestionsTabView — observations section', () => { // --------------------------------------------------------------------------- describe('QuestionsTabView — conclusion card', () => { - it('renders conclusion card when suspected causes exist', () => { + it('renders conclusion card when hypotheses exist', () => { const causes: Hypothesis[] = [{ factor: 'Operator', projectedCpk: 1.5 }]; - render( - - ); + render(); expect(screen.getByTestId('conclusion-card')).toBeDefined(); expect(screen.getByTestId('cause-chip-Operator')).toBeDefined(); }); - it('does not render conclusion card when no suspected causes', () => { - render(); + it('does not render conclusion card when no hypotheses', () => { + render(); expect(screen.queryByTestId('conclusion-card')).toBeNull(); }); @@ -265,7 +263,7 @@ describe('QuestionsTabView — conclusion card', () => { { const currentUnderstandingText = @@ -85,7 +85,7 @@ export const ReportInvestigationSummary: React.FC 0) || + (hypotheses && hypotheses.length > 0) || (ruledOut && ruledOut.length > 0); if (!hasContent) return null; @@ -125,13 +125,13 @@ export const ReportInvestigationSummary: React.FC 0 && ( + {hypotheses && hypotheses.length > 0 && (

Suspected Causes

    - {suspectedCauses.map((cause, index) => ( + {hypotheses.map((cause, index) => (
  1. {cause.text} {cause.factor && ( diff --git a/packages/ui/src/components/ReportView/__tests__/ReportInvestigationSummary.test.tsx b/packages/ui/src/components/ReportView/__tests__/ReportInvestigationSummary.test.tsx index 5dca586ff..8cab6df93 100644 --- a/packages/ui/src/components/ReportView/__tests__/ReportInvestigationSummary.test.tsx +++ b/packages/ui/src/components/ReportView/__tests__/ReportInvestigationSummary.test.tsx @@ -15,10 +15,10 @@ describe('ReportInvestigationSummary', () => { expect(screen.getByText('Issue / Concern')).toBeDefined(); }); - it('renders suspected causes with evidence', () => { + it('renders hypotheses with evidence', () => { render( Date: Sat, 9 May 2026 14:36:23 +0300 Subject: [PATCH 09/10] refactor(core): tighten HYPOTHESIS_UPDATE.patch type to omit immutable fields Patch type was Partial, allowing patches to id/createdAt/ deletedAt. Per EntityBase contract these are immutable. Tightened to Partial> per RPS V1 spec D8 + reviewer flag. Co-Authored-By: ruflo --- packages/core/src/actions/hypothesisActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/actions/hypothesisActions.ts b/packages/core/src/actions/hypothesisActions.ts index 530acc1d4..25ebec1ee 100644 --- a/packages/core/src/actions/hypothesisActions.ts +++ b/packages/core/src/actions/hypothesisActions.ts @@ -10,6 +10,6 @@ export type HypothesisAction = | { kind: 'HYPOTHESIS_UPDATE'; hypothesisId: Hypothesis['id']; - patch: Partial; + patch: Partial>; } | { kind: 'HYPOTHESIS_ARCHIVE'; hypothesisId: Hypothesis['id'] }; From a8391de9ea6358b28f889dd88443f43b0eeba96d Mon Sep 17 00:00:00 2001 From: Jukka-Matti Turtiainen Date: Sat, 9 May 2026 14:39:10 +0300 Subject: [PATCH 10/10] refactor: remove legacy-value migration shim per spec D15 (no backward compat) D15 commits to clean breaks in design phase. Stored-value migration shim mapping 'suspected'/'not-confirmed' to new HypothesisStatus values contradicts D15 + dev fixtures already reset via pnpm dev:reset (per OQ7). Replaced normalizeHypothesisStatus() with assertHypothesisStatus() that fails loud on unknown status values. Updated the corresponding test from "migrates legacy values" to "throws on unknown values". Co-Authored-By: ruflo --- .../__tests__/investigationSerializer.test.ts | 19 ++++------- .../src/services/investigationSerializer.ts | 34 +++++++++++++++---- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/apps/azure/src/services/__tests__/investigationSerializer.test.ts b/apps/azure/src/services/__tests__/investigationSerializer.test.ts index 1724dd97f..73c96a448 100644 --- a/apps/azure/src/services/__tests__/investigationSerializer.test.ts +++ b/apps/azure/src/services/__tests__/investigationSerializer.test.ts @@ -473,7 +473,7 @@ describe('deserializeInvestigationState', () => { }); }); - it('migrates legacy hypothesis status values on load', () => { + it('throws on unknown hypothesis status values (no silent migration per RPS V1 D15)', () => { const raw = { findings: [], questions: [], @@ -482,21 +482,14 @@ describe('deserializeInvestigationState', () => { ...makeHypothesis({ id: 'legacy-suspected' }), status: 'suspected', }, - { - ...makeHypothesis({ id: 'legacy-not-confirmed' }), - status: 'not-confirmed', - }, ], }; - const result = deserializeInvestigationState( - raw as unknown as import('../investigationSerializer').SerializedInvestigationState - ); - - expect(result.hypotheses.map(hub => [hub.id, hub.status])).toEqual([ - ['legacy-suspected', 'proposed'], - ['legacy-not-confirmed', 'refuted'], - ]); + expect(() => + deserializeInvestigationState( + raw as unknown as import('../investigationSerializer').SerializedInvestigationState + ) + ).toThrow(/Invalid HypothesisStatus/); }); it('does not overwrite existing evidence when both totalContribution and evidence are present', () => { diff --git a/apps/azure/src/services/investigationSerializer.ts b/apps/azure/src/services/investigationSerializer.ts index f177fd386..07827d55a 100644 --- a/apps/azure/src/services/investigationSerializer.ts +++ b/apps/azure/src/services/investigationSerializer.ts @@ -26,16 +26,34 @@ export interface SerializedInvestigationState { * Shape of a hypothesis as it may appear in legacy stored data, * before the `totalContribution` → `evidence` migration. */ -interface LegacyStoredHub extends Omit { +interface LegacyStoredHub extends Omit { // Old numeric field, present before HypothesisEvidence was introduced totalContribution?: number; evidence?: HypothesisEvidence; - status: HypothesisStatus | 'suspected' | 'not-confirmed'; } -function normalizeHypothesisStatus(status: LegacyStoredHub['status']): HypothesisStatus { - if (status === 'suspected') return 'proposed'; - if (status === 'not-confirmed') return 'refuted'; +const VALID_HYPOTHESIS_STATUSES: ReadonlySet = new Set([ + 'proposed', + 'evidenced', + 'confirmed', + 'refuted', + 'needs-disconfirmation', +]); + +/** + * Strict status validator — fails loud on unknown values. + * + * Per RPS V1 spec D15 (no backward compatibility, design-phase clean breaks), + * we do not silently translate legacy status values like 'suspected' or + * 'not-confirmed'. Dev fixtures reset via `pnpm dev:reset` (OQ7). + */ +function assertHypothesisStatus(status: HypothesisStatus): HypothesisStatus { + if (!VALID_HYPOTHESIS_STATUSES.has(status)) { + throw new Error( + `Invalid HypothesisStatus encountered during deserialization: ${JSON.stringify(status)}. ` + + `Valid values: ${Array.from(VALID_HYPOTHESIS_STATUSES).join(', ')}.` + ); + } return status; } @@ -64,6 +82,10 @@ export function serializeInvestigationState( * 2. If a hub has `totalContribution` (legacy numeric field) but no `evidence`, * a basic `HypothesisEvidence` is synthesised from it. * 3. `selectedForImprovement` defaults to `undefined` when absent. + * + * Status values are asserted strictly: per RPS V1 spec D15 (no backward + * compatibility), unknown status values throw rather than being silently + * translated. */ export function deserializeInvestigationState(raw: SerializedInvestigationState): { findings: Finding[]; @@ -79,7 +101,7 @@ export function deserializeInvestigationState(raw: SerializedInvestigationState) const { totalContribution: _legacy, ...clean } = stored; const hub: Hypothesis = { ...clean, - status: normalizeHypothesisStatus(stored.status), + status: assertHypothesisStatus(stored.status), evidence: stored.evidence, selectedForImprovement: stored.selectedForImprovement, };