diff --git a/apps/azure/e2e/full-lifecycle.spec.ts b/apps/azure/e2e/full-lifecycle.spec.ts deleted file mode 100644 index 6b473cd08..000000000 --- a/apps/azure/e2e/full-lifecycle.spec.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -const DB_NAME = 'VaRiScoutAzure'; -const NOW = 1_777_161_600_000; // 2026-04-26T00:00:00.000Z - -async function waitForAppReady(page: Page) { - await page.goto('/'); - await expect( - page - .locator('text=Start Your Analysis') - .or(page.locator('[data-testid="project-dashboard"]')) - .or(page.locator('text=Process Hubs').first()) - ).toBeVisible({ timeout: 15000 }); -} - -async function seedLifecycleHub(page: Page) { - await waitForAppReady(page); - - await page.evaluate( - ({ dbName, now }) => - new Promise((resolve, reject) => { - const openReq = indexedDB.open(dbName); - openReq.onerror = () => reject(new Error(`IDB open failed: ${openReq.error?.message}`)); - openReq.onsuccess = () => { - const db = openReq.result; - const hubId = 'lifecycle-hub'; - const investigationId = 'lifecycle-investigation'; - const projectId = 'lifecycle-ip'; - const sustainmentId = 'lifecycle-sustainment'; - - try { - const tx = db.transaction( - [ - 'projects', - 'processHubs', - 'improvementProjects', - 'actionItems', - 'sustainmentRecords', - ], - 'readwrite' - ); - - tx.objectStore('projects').put({ - name: 'Lifecycle Demo Hub', - location: 'personal', - modified: new Date('2026-05-01T00:00:00.000Z'), - synced: false, - data: {}, - }); - - tx.objectStore('processHubs').put({ - id: hubId, - name: 'Lifecycle Demo Hub', - processGoal: 'Demonstrate the full response path lifecycle.', - outcomes: [ - { - id: 'outcome-1', - hubId, - columnName: 'fill_weight', - characteristicType: 'nominalIsBest', - createdAt: now, - deletedAt: null, - }, - ], - investigations: [ - { - id: investigationId, - name: 'Fill weight lifecycle', - createdAt: now, - updatedAt: now, - deletedAt: null, - metadata: { - processHubId: hubId, - investigationStatus: 'controlled', - findingCounts: {}, - questionCounts: {}, - actionCounts: { total: 1, completed: 1, overdue: 0 }, - }, - }, - ], - createdAt: now, - updatedAt: now, - deletedAt: null, - }); - - tx.objectStore('improvementProjects').put({ - id: projectId, - hubId, - status: 'closed', - metadata: { title: 'Reduce fill-weight drift', investigationId }, - goal: { - outcomeGoal: { outcomeSpecId: 'outcome-1', target: 1.33 }, - freeText: 'Hold Cpk above 1.33.', - }, - sections: { - background: {}, - investigationLineage: {}, - approach: { actionItemIds: ['action-1'] }, - outcomeReference: { sustainmentRecordId: sustainmentId }, - }, - createdAt: now, - updatedAt: now, - deletedAt: null, - }); - - tx.objectStore('actionItems').put({ - id: 'action-1', - hubId, - text: 'Lock revised fill-weight standard work', - stepId: 'step-1', - status: 'done', - completedAt: '2026-04-20T00:00:00.000Z', - parentImprovementProjectId: projectId, - parentImprovementIdeaId: null, - createdAt: now, - deletedAt: null, - }); - - tx.objectStore('sustainmentRecords').put({ - id: sustainmentId, - investigationId, - hubId, - status: 'confirmed-sustained', - title: 'Hold fill-weight gain', - improvementProjectId: projectId, - targetSummary: 'Cpk stays above 1.33.', - consecutiveOnTargetTicks: 4, - hasOverride: false, - lastEvaluatedSnapshotId: 'snapshot-1', - cadence: 'weekly', - latestVerdict: 'holding', - latestReviewAt: '2026-03-01T00:00:00.000Z', - createdAt: now - 70 * 24 * 60 * 60 * 1000, - updatedAt: now - 60 * 24 * 60 * 60 * 1000, - deletedAt: null, - }); - - tx.oncomplete = () => { - db.close(); - resolve(); - }; - tx.onerror = () => { - db.close(); - reject(new Error(`IDB write failed: ${tx.error?.message}`)); - }; - } catch (error) { - db.close(); - reject(error); - } - }; - }), - { dbName: DB_NAME, now: NOW } - ); - - await page.reload(); - await expect(page.locator('text=Process Hubs').first()).toBeVisible({ timeout: 15000 }); -} - -test.describe('Azure full response-path lifecycle', () => { - test('opens a confirmed sustainment handoff prompt and advances handoff controls', async ({ - page, - }) => { - await seedLifecycleHub(page); - - await page.getByRole('button', { name: 'Open Lifecycle Demo Hub' }).click(); - - await expect(page.getByText(/Hold fill-weight gain confirmed sustained/i)).toBeVisible({ - timeout: 10000, - }); - await page.getByRole('button', { name: 'Record control handoff' }).click(); - - await expect(page.getByRole('heading', { name: 'Handoff' })).toBeVisible({ timeout: 10000 }); - await expect(page.getByText('Hold fill-weight gain')).toBeVisible(); - - await page.getByLabel('System name').fill('QMS-42'); - await page.getByLabel('Process owner').fill('Process owner'); - await page.getByLabel('Escalation path').fill('Escalate drift to production manager'); - await page - .getByRole('textbox', { name: 'Reaction plan' }) - .fill('Restore standard work before next shift'); - await page.getByRole('button', { name: 'Acknowledge handoff' }).click(); - await page.getByRole('button', { name: 'Mark operational' }).click(); - - await expect(page.getByText('Status operational')).toBeVisible({ timeout: 5000 }); - await expect( - page - .getByRole('button', { name: 'Run sponsor signoff' }) - .or(page.getByRole('button', { name: 'Sponsor signoff locked' })) - ).toBeVisible(); - }); -}); diff --git a/apps/azure/src/App.tsx b/apps/azure/src/App.tsx index 418ace863..be474ed75 100644 --- a/apps/azure/src/App.tsx +++ b/apps/azure/src/App.tsx @@ -190,7 +190,6 @@ function AppContent({ // When true, Editor mounts directly into PasteScreen (used by "Add framing" CTA). // Reset to false once consumed so subsequent navigations don't re-trigger paste. const [pendingStartPaste, setPendingStartPaste] = useState(false); - const [pendingHandoffTargetId, setPendingHandoffTargetId] = useState(null); // Resolve deep link from URL params const deepLink = useMemo(() => { @@ -248,23 +247,16 @@ function AppContent({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const navigateToEditor = ( - projectId?: string, - processHubId?: string, - startPaste?: boolean, - handoffTargetId?: string - ) => { + const navigateToEditor = (projectId?: string, processHubId?: string, startPaste?: boolean) => { setCurrentProject(projectId || null); setPendingProcessHubId(processHubId || null); if (startPaste) setPendingStartPaste(true); - setPendingHandoffTargetId(handoffTargetId || null); setCurrentView('editor'); }; const navigateToDashboard = () => { setCurrentProject(null); setPendingStartPaste(false); - setPendingHandoffTargetId(null); setCurrentView('dashboard'); }; @@ -300,8 +292,8 @@ function AppContent({ )} {currentView === 'dashboard' && !deepLinkError && ( - navigateToEditor(id, processHubId, startPaste, handoffTargetId) + onOpenProject={(id, processHubId, startPaste) => + navigateToEditor(id, processHubId, startPaste) } onLoadSample={handleLoadSample} /> @@ -311,7 +303,6 @@ function AppContent({ projectId={currentProject} onBack={navigateToDashboard} initialProcessHubId={currentProject ? undefined : (pendingProcessHubId ?? undefined)} - initialHandoffTargetId={pendingHandoffTargetId ?? undefined} initialSample={pendingSample} startPasteOnMount={pendingStartPaste} onOpenSettings={() => setIsSettingsOpen(true)} diff --git a/apps/azure/src/components/ProcessHubCadenceQueues.tsx b/apps/azure/src/components/ProcessHubCadenceQueues.tsx index 56f686557..b919d4c15 100644 --- a/apps/azure/src/components/ProcessHubCadenceQueues.tsx +++ b/apps/azure/src/components/ProcessHubCadenceQueues.tsx @@ -26,7 +26,6 @@ interface ProcessHubCadenceQueuesProps { onOpenInvestigation: (id: string) => void; onSetupSustainment: (investigationId: string) => void; onLogReview: (recordId: string) => void; - onRecordHandoff: (investigationId: string) => void; } const DEPTH_SECTIONS: Array<{ depth: InvestigationDepth; label: string }> = [ @@ -121,7 +120,6 @@ const ProcessHubCadenceQueues: React.FC = ({ onOpenInvestigation, onSetupSustainment, onLogReview, - onRecordHandoff, }) => { const hasActiveWork = DEPTH_SECTIONS.some( ({ depth }) => cadence.activeWork[depth].totalCount > 0 @@ -346,7 +344,6 @@ const ProcessHubCadenceQueues: React.FC = ({ onOpenInvestigation={onOpenInvestigation} onSetupSustainment={onSetupSustainment} onLogReview={onLogReview} - onRecordHandoff={onRecordHandoff} /> ) : hasActiveReviewItems ? null : ( diff --git a/apps/azure/src/components/ProcessHubReviewPanel.tsx b/apps/azure/src/components/ProcessHubReviewPanel.tsx index a086ed5ed..886326afb 100644 --- a/apps/azure/src/components/ProcessHubReviewPanel.tsx +++ b/apps/azure/src/components/ProcessHubReviewPanel.tsx @@ -25,7 +25,6 @@ interface ProcessHubReviewPanelProps { onStartInvestigation: () => void; onSetupSustainment: (investigationId: string) => void; onLogReview: (recordId: string) => void; - onRecordHandoff: (investigationId: string) => void; onResponsePathAction: (item: ProcessStateItem, action: ResponsePathAction, hubId: string) => void; /** Notes wiring */ onRequestAddNote: (item: ProcessStateItem, hubId: string) => void; @@ -64,7 +63,6 @@ const ProcessHubReviewPanel: React.FC = ({ onStartInvestigation, onSetupSustainment, onLogReview, - onRecordHandoff, onResponsePathAction, onRequestAddNote, onRequestEditNote, @@ -133,10 +131,6 @@ const ProcessHubReviewPanel: React.FC = ({ onOpenInvestigation(targetId); return; } - if (surface === 'handoff' && targetId) { - onRecordHandoff(targetId); - return; - } if (surface === 'improvement-projects' && targetId) { onOpenInvestigation(targetProject?.metadata.investigationId ?? targetId); return; @@ -299,7 +293,6 @@ const ProcessHubReviewPanel: React.FC = ({ onOpenInvestigation={onOpenInvestigation} onSetupSustainment={onSetupSustainment} onLogReview={onLogReview} - onRecordHandoff={onRecordHandoff} /> ); diff --git a/apps/azure/src/components/ProcessHubSustainmentRegion.tsx b/apps/azure/src/components/ProcessHubSustainmentRegion.tsx index 370743428..61a297c1b 100644 --- a/apps/azure/src/components/ProcessHubSustainmentRegion.tsx +++ b/apps/azure/src/components/ProcessHubSustainmentRegion.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import { ShieldCheck, ShieldAlert, ArrowRight, History } from 'lucide-react'; +import { ShieldCheck, ShieldAlert, History } from 'lucide-react'; import { - selectControlHandoffCandidates, selectSustainmentBuckets, type ProcessHubCadenceSummary, type ProcessHubInvestigation, @@ -16,7 +15,6 @@ export interface ProcessHubSustainmentRegionProps { onOpenInvestigation: (id: string) => void; onSetupSustainment: (investigationId: string) => void; onLogReview: (recordId: string) => void; - onRecordHandoff: (investigationId: string) => void; } interface BucketSectionProps { @@ -80,7 +78,6 @@ const ProcessHubSustainmentRegion: React.FC = onOpenInvestigation, onSetupSustainment, onLogReview, - onRecordHandoff, }) => { const renderDate = new Date(); @@ -91,17 +88,11 @@ const ProcessHubSustainmentRegion: React.FC = renderDate ); - const handoffCandidates = selectControlHandoffCandidates( - rollup.investigations, - rollup.controlHandoffs - ); - const dueAndOverdueIds = new Set([ ...buckets.dueNow.map(item => item.investigation.id), ...buckets.overdue.map(item => item.investigation.id), ]); const reviewedIds = new Set(buckets.recentlyReviewed.map(item => item.investigation.id)); - const handoffIds = new Set(handoffCandidates.map(item => item.investigation.id)); const setupCandidates = rollup.investigations.filter(inv => { const status = inv.metadata?.investigationStatus; @@ -109,7 +100,6 @@ const ProcessHubSustainmentRegion: React.FC = if (inv.metadata?.sustainment) return false; if (dueAndOverdueIds.has(inv.id)) return false; if (reviewedIds.has(inv.id)) return false; - if (handoffIds.has(inv.id)) return false; return true; }); @@ -134,10 +124,7 @@ const ProcessHubSustainmentRegion: React.FC = }; const totalSustainmentItems = - buckets.dueNow.length + - buckets.overdue.length + - buckets.recentlyReviewed.length + - handoffCandidates.length; + buckets.dueNow.length + buckets.overdue.length + buckets.recentlyReviewed.length; return (
@@ -183,20 +170,6 @@ const ProcessHubSustainmentRegion: React.FC = /> )} - {handoffCandidates.length > 0 && ( - } - items={handoffCandidates} - onItemClick={item => onRecordHandoff(item.investigation.id)} - itemAriaLabel={item => `Record control handoff for ${item.investigation.name}`} - iconForItem={} - renderSubline={() => 'Needs control handoff'} - testId="sustainment-handoff" - /> - )} - {setupCandidates.length > 0 ? (
diff --git a/apps/azure/src/components/ProcessHubView.tsx b/apps/azure/src/components/ProcessHubView.tsx index 087c0b527..f11347640 100644 --- a/apps/azure/src/components/ProcessHubView.tsx +++ b/apps/azure/src/components/ProcessHubView.tsx @@ -33,7 +33,6 @@ export interface ProcessHubViewProps { onStartInvestigation: () => void; onSetupSustainment: (investigationId: string) => void; onLogReview: (recordId: string) => void; - onRecordHandoff: (investigationId: string) => void; onResponsePathAction: (item: ProcessStateItem, action: ResponsePathAction, hubId: string) => void; onRequestAddNote: (item: ProcessStateItem, hubId: string) => void; onRequestEditNote: (item: ProcessStateItem, note: ProcessStateNote, hubId: string) => void; diff --git a/apps/azure/src/components/__tests__/ProcessHubSustainmentRegion.test.tsx b/apps/azure/src/components/__tests__/ProcessHubSustainmentRegion.test.tsx index c09ac5768..742ad6af2 100644 --- a/apps/azure/src/components/__tests__/ProcessHubSustainmentRegion.test.tsx +++ b/apps/azure/src/components/__tests__/ProcessHubSustainmentRegion.test.tsx @@ -117,7 +117,6 @@ describe('ProcessHubSustainmentRegion', () => { onOpenInvestigation={noOp} onSetupSustainment={noOp} onLogReview={noOp} - onRecordHandoff={noOp} /> ); @@ -146,7 +145,6 @@ describe('ProcessHubSustainmentRegion', () => { onOpenInvestigation={noOp} onSetupSustainment={onSetupSustainment} onLogReview={noOp} - onRecordHandoff={noOp} /> ); @@ -209,7 +207,6 @@ describe('ProcessHubSustainmentRegion', () => { onOpenInvestigation={noOp} onSetupSustainment={noOp} onLogReview={onLogReview} - onRecordHandoff={noOp} /> ); @@ -262,7 +259,6 @@ describe('ProcessHubSustainmentRegion', () => { onOpenInvestigation={noOp} onSetupSustainment={noOp} onLogReview={noOp} - onRecordHandoff={noOp} /> ); @@ -270,37 +266,6 @@ describe('ProcessHubSustainmentRegion', () => { expect(screen.getByText('Pasteurizer Temp')).toBeInTheDocument(); }); - it('renders handoff candidates and calls onRecordHandoff on click', () => { - const onRecordHandoff = vi.fn(); - const inv = makeInvestigation({ - id: 'inv-4', - name: 'Torque Study', - metadata: { investigationStatus: 'controlled' }, - }); - - const cadence = makeEmptyCadence(); - const rollup = makeEmptyRollup([inv]); - - render( - - ); - - expect(screen.getByText('Torque Study')).toBeInTheDocument(); - expect(screen.getByText('Needs control handoff')).toBeInTheDocument(); - - fireEvent.click( - screen.getByRole('button', { name: /Record control handoff for Torque Study/ }) - ); - expect(onRecordHandoff).toHaveBeenCalledWith('inv-4'); - }); - it('hides all sustainment buckets when a controlled investigation has retainSustainmentReview=false', () => { const inv = makeInvestigation({ id: 'inv-5', @@ -349,7 +314,6 @@ describe('ProcessHubSustainmentRegion', () => { onOpenInvestigation={noOp} onSetupSustainment={noOp} onLogReview={noOp} - onRecordHandoff={noOp} /> ); diff --git a/apps/azure/src/components/__tests__/ProcessHubView.test.tsx b/apps/azure/src/components/__tests__/ProcessHubView.test.tsx index 5208020ac..6b8daa33f 100644 --- a/apps/azure/src/components/__tests__/ProcessHubView.test.tsx +++ b/apps/azure/src/components/__tests__/ProcessHubView.test.tsx @@ -27,7 +27,6 @@ const baseProps = { onStartInvestigation: noop, onSetupSustainment: noop, onLogReview: noop, - onRecordHandoff: noop, onResponsePathAction: noop, onRequestAddNote: noop, onRequestEditNote: noop, diff --git a/apps/azure/src/components/editor/FrameView.tsx b/apps/azure/src/components/editor/FrameView.tsx index fad4c0280..13c0ee99e 100644 --- a/apps/azure/src/components/editor/FrameView.tsx +++ b/apps/azure/src/components/editor/FrameView.tsx @@ -25,10 +25,8 @@ import type { EvidenceSnapshot, StepCapabilityStamp, SustainmentRecord, - WorkflowReadinessSignals, } from '@variscout/core'; import { createActionItem, type ActionItem } from '@variscout/core/findings'; -import type { ImprovementProject } from '@variscout/core/improvementProject'; import { surveyInboxRules } from '@variscout/core/survey'; import { azureHubRepository } from '../../persistence'; import { usePanelsStore } from '../../features/panels/panelsStore'; @@ -61,26 +59,6 @@ function priorStepStatsFromSnapshots( return new Map(stamps.map(stamp => [stamp.stepId, stamp])); } -function hasCompletedInterventionEvidence( - projects: readonly ImprovementProject[], - items: readonly ActionItem[] -): boolean { - const completedActionIds = new Set( - items - .filter( - item => - item.deletedAt === null && - (item.completedAt !== undefined || item.status === 'done' || item.doneAt != null) - ) - .map(item => item.id) - ); - return projects.some(project => { - if (project.deletedAt !== null || project.status !== 'closed') return false; - const actionItemIds = project.sections.approach.actionItemIds ?? []; - return actionItemIds.some(id => completedActionIds.has(id)); - }); -} - const FrameView: React.FC = () => { const rawData = useProjectStore(s => s.rawData); const outcome = useProjectStore(s => s.outcome); @@ -208,19 +186,6 @@ const FrameView: React.FC = () => { ]; }, [activeHubId, controlHandoffs, hypotheses, projectsByHub, sustainmentRecords]); - const signals: WorkflowReadinessSignals = React.useMemo(() => { - const improvementProjects = (activeHubId ? (projectsByHub[activeHubId] ?? []) : []).filter( - project => project.deletedAt === null - ); - - return { - hasIntervention: hasCompletedInterventionEvidence(improvementProjects, actionItems), - sustainmentConfirmed: sustainmentRecords.some( - record => record.deletedAt === null && record.status === 'confirmed-sustained' - ), - }; - }, [activeHubId, actionItems, projectsByHub, sustainmentRecords]); - const inboxPrompts = React.useMemo(() => { const improvementProjects = (activeHubId ? (projectsByHub[activeHubId] ?? []) : []).filter( project => project.deletedAt === null @@ -315,14 +280,6 @@ const FrameView: React.FC = () => { usePanelsStore.getState().showCharter(); }, []); - const handleSustainment = React.useCallback(() => { - usePanelsStore.getState().showSustainment(); - }, []); - - const handleHandoff = React.useCallback(() => { - usePanelsStore.getState().showHandoff(); - }, []); - const handleInboxNavigate = React.useCallback((prompt: InboxDigestPrompt) => { const surface = prompt.action?.opensSurface; if (surface === 'sustainment') { @@ -391,10 +348,7 @@ const FrameView: React.FC = () => { onOpenInvestigationFocus={handleOpenInvestigationFocus} onAddCausalLink={handleAddCausalLink} onRemoveCausalLink={handleRemoveCausalLink} - signals={signals} onCharter={handleCharter} - onSustainment={handleSustainment} - onHandoff={handleHandoff} contextLinkGroups={contextLinkGroups} onNavigateContextLink={handleNavigateContextLink} priorStepStats={priorStepStats} diff --git a/apps/azure/src/components/editor/__tests__/FrameView.test.tsx b/apps/azure/src/components/editor/__tests__/FrameView.test.tsx index 971a30534..6c14d31a8 100644 --- a/apps/azure/src/components/editor/__tests__/FrameView.test.tsx +++ b/apps/azure/src/components/editor/__tests__/FrameView.test.tsx @@ -1,6 +1,5 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { WorkflowReadinessSignals } from '@variscout/core'; const setProcessContextMock = vi.fn(); const setMeasureSpecMock = vi.fn(); @@ -123,7 +122,6 @@ vi.mock('@variscout/ui', async () => { : null ), CanvasWorkspace: (props: { - signals: WorkflowReadinessSignals; onSeeData: () => void; onQuickAction?: (stepId: string) => void; onLogQuickAction?: ( @@ -141,8 +139,6 @@ vi.mock('@variscout/ui', async () => { ) => void; onRemoveCausalLink?: (linkId: string) => void; onCharter?: () => void; - onSustainment?: () => void; - onHandoff?: () => void; priorStepStats?: ReadonlyMap; actionItems?: unknown[]; contextLinkGroups?: { surfaceType: string; items: { id: string }[] }[]; @@ -200,16 +196,6 @@ vi.mock('@variscout/ui', async () => { 'button', { type: 'button', 'data-testid': 'cta-charter', onClick: props.onCharter }, 'Charter' - ), - React.createElement( - 'button', - { type: 'button', 'data-testid': 'cta-sustainment', onClick: props.onSustainment }, - 'Sustainment' - ), - React.createElement( - 'button', - { type: 'button', 'data-testid': 'cta-handoff', onClick: props.onHandoff }, - 'Handoff' ) ); }, @@ -329,7 +315,6 @@ describe('FrameView (Azure shell)', () => { questions: [{ id: 'q-1' }], hypotheses: [{ id: 'hub-1' }], causalLinks: [{ id: 'link-1' }], - signals: { hasIntervention: false, sustainmentConfirmed: false }, }) ); }); @@ -608,55 +593,15 @@ describe('FrameView (Azure shell)', () => { expect(removeCausalLinkMock).toHaveBeenCalledWith('link-created'); }); - it('wires Canvas charter/sustainment/handoff CTAs to the panels-store show actions', () => { + it('wires Canvas charter CTA to the panels-store show action', () => { render(); fireEvent.click(screen.getByTestId('cta-charter')); - fireEvent.click(screen.getByTestId('cta-sustainment')); - fireEvent.click(screen.getByTestId('cta-handoff')); expect(showCharterMock).toHaveBeenCalledTimes(1); - expect(showSustainmentMock).toHaveBeenCalledTimes(1); - expect(showHandoffMock).toHaveBeenCalledTimes(1); - }); - - it('marks Sustainment ready only when a closed project has completed intervention evidence and keeps Handoff gated until sustainment is confirmed', async () => { - improvementProjectStateRef.current = { - projectsByHub: { - 'hub-1': [ - { - id: 'ip-1', - hubId: 'hub-1', - status: 'closed', - metadata: { title: 'Reduce rework' }, - goal: { outcomeGoal: { outcomeSpecId: 'outcome-1', target: 98 } }, - sections: { - background: {}, - investigationLineage: {}, - approach: { actionItemIds: ['action-1'] }, - outcomeReference: {}, - }, - createdAt: 1, - updatedAt: 1, - deletedAt: null, - }, - ], - }, - getProjectsForHub: () => [], - }; - hoisted.actionItemsListByHubMock.mockResolvedValue([ - { ...actionItem('action-1', 'Change nozzle'), completedAt: 1714000000000 }, - ]); - - render(); - - await waitFor(() => { - const props = hoisted.canvasWorkspaceMock.mock.lastCall?.[0]; - expect(props?.signals).toEqual({ hasIntervention: true, sustainmentConfirmed: false }); - }); }); - it('marks Handoff ready and includes sustainment context links when a live record is confirmed', async () => { + it('includes sustainment context links when a live record is confirmed', async () => { hoisted.sustainmentRecordsListByHubMock.mockResolvedValue([ { id: 'sr-1', @@ -678,7 +623,6 @@ describe('FrameView (Azure shell)', () => { await waitFor(() => { const props = hoisted.canvasWorkspaceMock.mock.lastCall?.[0]; - expect(props?.signals.sustainmentConfirmed).toBe(true); expect( props?.contextLinkGroups?.find( (group: { surfaceType: string }) => group.surfaceType === 'sustainment' diff --git a/apps/azure/src/components/handoff/HandoffPanel.tsx b/apps/azure/src/components/handoff/HandoffPanel.tsx deleted file mode 100644 index 713ef6ee5..000000000 --- a/apps/azure/src/components/handoff/HandoffPanel.tsx +++ /dev/null @@ -1,297 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { HandoffForm, type HandoffChangePatch } from '@variscout/ui'; -import { useTier } from '@variscout/hooks'; -import type { ControlHandoff, ProcessHub, SustainmentRecord } from '@variscout/core'; -import { azureHubRepository } from '../../persistence'; - -interface HandoffPanelProps { - activeHub?: ProcessHub; - targetId?: string; - onBack: () => void; -} - -function makeId(): string { - if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) return crypto.randomUUID(); - return `handoff-${Date.now()}`; -} - -function liveConfirmedRecords(records: SustainmentRecord[] | undefined): SustainmentRecord[] { - return (records ?? []).filter( - record => record.deletedAt === null && record.status === 'confirmed-sustained' - ); -} - -function selectRecord(records: SustainmentRecord[], targetId: string | undefined) { - if (targetId) { - const byId = records.find(record => record.id === targetId); - if (byId) return byId; - const byHandoff = records.find(record => record.controlHandoffId === targetId); - if (byHandoff) return byHandoff; - } - return records[0] ?? null; -} - -function selectHandoff( - handoffs: ControlHandoff[], - record: SustainmentRecord | null, - targetId: string | undefined -) { - if (targetId) { - const byId = handoffs.find(handoff => handoff.id === targetId); - if (byId) return byId; - } - if (!record) return null; - return ( - handoffs.find(handoff => handoff.id === record.controlHandoffId) ?? - handoffs.find(handoff => handoff.investigationId === record.investigationId) ?? - null - ); -} - -function buildDraftHandoff(hub: ProcessHub, record: SustainmentRecord): ControlHandoff { - const now = Date.now(); - return { - id: makeId(), - investigationId: record.investigationId, - hubId: hub.id, - status: 'pending', - surface: 'qms-procedure', - systemName: '', - operationalOwner: record.owner ?? hub.processOwner ?? { displayName: '' }, - handoffDate: now, - description: record.targetSummary ?? '', - retainSustainmentReview: true, - recordedBy: { displayName: 'Azure user' }, - escalationPath: record.openConcerns, - reactionPlan: '', - createdAt: now, - deletedAt: null, - }; -} - -const HandoffPanel: React.FC = ({ activeHub, targetId, onBack }) => { - const { isPaid } = useTier(); - const [records, setRecords] = useState([]); - const [handoffs, setHandoffs] = useState([]); - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(Boolean(activeHub)); - const creatingForRecordRef = useRef(null); - - useEffect(() => { - setRecords(liveConfirmedRecords(activeHub?.sustainmentRecords)); - setHandoffs((activeHub?.controlHandoffs ?? []).filter(handoff => handoff.deletedAt === null)); - setError(null); - setIsLoading(Boolean(activeHub)); - - if (!activeHub) { - setIsLoading(false); - return; - } - - let cancelled = false; - void Promise.all([ - azureHubRepository.sustainmentRecords.listByHub(activeHub.id), - azureHubRepository.controlHandoffs.listByHub(activeHub.id), - ]) - .then(([loadedRecords, loadedHandoffs]) => { - if (cancelled) return; - setRecords(liveConfirmedRecords(loadedRecords)); - setHandoffs(loadedHandoffs.filter(handoff => handoff.deletedAt === null)); - }) - .catch(() => { - if (cancelled) return; - setRecords(liveConfirmedRecords(activeHub.sustainmentRecords)); - setHandoffs( - (activeHub.controlHandoffs ?? []).filter(handoff => handoff.deletedAt === null) - ); - }) - .finally(() => { - if (!cancelled) setIsLoading(false); - }); - - return () => { - cancelled = true; - }; - }, [activeHub]); - - const selectedRecord = useMemo(() => selectRecord(records, targetId), [records, targetId]); - const selectedHandoff = useMemo( - () => selectHandoff(handoffs, selectedRecord, targetId), - [handoffs, selectedRecord, targetId] - ); - - useEffect(() => { - if (!activeHub || isLoading || !selectedRecord || selectedHandoff) return; - if (creatingForRecordRef.current === selectedRecord.id) return; - - const draft = buildDraftHandoff(activeHub, selectedRecord); - creatingForRecordRef.current = selectedRecord.id; - setError(null); - let cancelled = false; - - void (async () => { - await azureHubRepository.dispatch({ - kind: 'CONTROL_HANDOFF_CREATE', - hubId: activeHub.id, - handoff: draft, - }); - if (!selectedRecord.controlHandoffId) { - await azureHubRepository.dispatch({ - kind: 'SUSTAINMENT_RECORD_UPDATE', - recordId: selectedRecord.id, - patch: { controlHandoffId: draft.id }, - }); - } - })() - .then(() => { - if (cancelled) return; - setHandoffs(current => [...current, draft]); - setRecords(current => - current.map(record => - record.id === selectedRecord.id ? { ...record, controlHandoffId: draft.id } : record - ) - ); - }) - .catch(() => { - if (!cancelled) setError('Could not create a handoff.'); - }) - .finally(() => { - if (creatingForRecordRef.current === selectedRecord.id) creatingForRecordRef.current = null; - }); - - return () => { - cancelled = true; - }; - }, [activeHub, isLoading, selectedHandoff, selectedRecord]); - - const patchHandoff = useCallback( - (patch: HandoffChangePatch) => { - if (!selectedHandoff) return; - const next = { ...selectedHandoff, ...patch }; - setHandoffs(current => - current.map(handoff => (handoff.id === selectedHandoff.id ? next : handoff)) - ); - void azureHubRepository - .dispatch({ kind: 'CONTROL_HANDOFF_UPDATE', handoffId: selectedHandoff.id, patch }) - .catch(() => setError('Could not save handoff changes.')); - }, - [selectedHandoff] - ); - - const acknowledge = useCallback(() => { - if (!selectedHandoff) return; - const acknowledgedAt = Date.now(); - const acknowledgedBy = selectedHandoff.operationalOwner; - setHandoffs(current => - current.map(handoff => - handoff.id === selectedHandoff.id - ? { - ...handoff, - status: 'acknowledged', - acknowledgedAt, - ownerAcknowledgement: { acknowledgedBy }, - } - : handoff - ) - ); - void azureHubRepository.dispatch({ - kind: 'CONTROL_HANDOFF_ACKNOWLEDGE', - handoffId: selectedHandoff.id, - acknowledgedAt, - acknowledgedBy, - }); - }, [selectedHandoff]); - - const markOperational = useCallback(() => { - if (!selectedHandoff) return; - const operationalAt = Date.now(); - setHandoffs(current => - current.map(handoff => - handoff.id === selectedHandoff.id - ? { ...handoff, status: 'operational', operationalAt } - : handoff - ) - ); - void azureHubRepository.dispatch({ - kind: 'CONTROL_HANDOFF_MARK_OPERATIONAL', - handoffId: selectedHandoff.id, - operationalAt, - }); - }, [selectedHandoff]); - - const sponsorSignoff = useCallback(() => { - if (!selectedHandoff) return; - const signoff = { approvedAt: Date.now(), approvedBy: { displayName: 'Sponsor' } }; - setHandoffs(current => - current.map(handoff => - handoff.id === selectedHandoff.id - ? { - ...handoff, - status: 'operational', - operationalAt: handoff.operationalAt ?? signoff.approvedAt, - signoff: { ...(handoff.signoff ?? {}), ...signoff }, - } - : handoff - ) - ); - void azureHubRepository.dispatch({ - kind: 'CONTROL_HANDOFF_SIGNOFF', - handoffId: selectedHandoff.id, - signoff, - }); - }, [selectedHandoff]); - - const heading = activeHub?.name ?? 'No active hub'; - - return ( -
-
-
-

Handoff

-

{heading}

-
- -
- - {!activeHub ? ( -

- Create or select a Process Hub before opening handoff. -

- ) : error ? ( -

- {error} -

- ) : isLoading ? ( -

- Loading handoff... -

- ) : selectedHandoff ? ( - - ) : selectedRecord ? ( -

- Creating handoff... -

- ) : ( -

- Confirm sustainment before recording handoff. -

- )} -
- ); -}; - -export default HandoffPanel; diff --git a/apps/azure/src/features/panels/__tests__/panelsStore.test.ts b/apps/azure/src/features/panels/__tests__/panelsStore.test.ts index b01d2f187..893462f77 100644 --- a/apps/azure/src/features/panels/__tests__/panelsStore.test.ts +++ b/apps/azure/src/features/panels/__tests__/panelsStore.test.ts @@ -451,10 +451,10 @@ describe('panelsStore', () => { expect(usePanelsStore.getState().activeView).toBe('sustainment'); }); - it('showHandoff sets activeView to handoff', () => { + it('showHandoff redirects to sustainment (handoff folded into sustainment in wedge V1)', () => { usePanelsStore.getState().showHandoff('sr-1'); - expect(usePanelsStore.getState().activeView).toBe('handoff'); - expect(usePanelsStore.getState().handoffTargetId).toBe('sr-1'); + expect(usePanelsStore.getState().activeView).toBe('sustainment'); + expect(usePanelsStore.getState().sustainmentTargetId).toBe('sr-1'); }); }); diff --git a/apps/azure/src/features/panels/panelsStore.ts b/apps/azure/src/features/panels/panelsStore.ts index 74685f977..545f6900e 100644 --- a/apps/azure/src/features/panels/panelsStore.ts +++ b/apps/azure/src/features/panels/panelsStore.ts @@ -12,8 +12,7 @@ interface PanelsState { | 'improvement' | 'report' | 'charter' - | 'sustainment' - | 'handoff'; + | 'sustainment'; isDataTableOpen: boolean; /** @deprecated Findings are moving to the Investigation workspace. Kept for backward compat; always false. Task 10 will remove consumers. */ isFindingsOpen: boolean; @@ -39,7 +38,6 @@ interface PanelsState { /** ID of idea highlighted via matrix<->card bidirectional navigation */ highlightedIdeaId: string | null; sustainmentTargetId: string | null; - handoffTargetId: string | null; selectedProjectId: string | null; } @@ -90,8 +88,7 @@ interface PanelsActions { | 'improvement' | 'report' | 'charter' - | 'sustainment' - | 'handoff'; + | 'sustainment'; isFindingsOpen?: boolean; isWhatIfOpen?: boolean; } | null @@ -121,7 +118,6 @@ export const usePanelsStore = create(set => ({ activeImprovementView: 'plan', highlightedIdeaId: null, sustainmentTargetId: null, - handoffTargetId: null, selectedProjectId: null, // Workspace navigation (ADR-055 + header-redesign spec, extended with 'frame' per ADR-070) @@ -151,11 +147,12 @@ export const usePanelsStore = create(set => ({ isFindingsOpen: false, sustainmentTargetId: targetId ?? null, })), + // Alias for showSustainment — wedge V1 folds Handoff into Sustainment-closure (ADR-082). Inbox prompts + context links still emit surface === 'handoff'; routing through this alias keeps them reachable. showHandoff: targetId => set(() => ({ - activeView: 'handoff', + activeView: 'sustainment', isFindingsOpen: false, - handoffTargetId: targetId ?? null, + sustainmentTargetId: targetId ?? null, })), // Data table diff --git a/apps/azure/src/features/panels/usePanelsPersistence.ts b/apps/azure/src/features/panels/usePanelsPersistence.ts index b895812bf..aae9b35cc 100644 --- a/apps/azure/src/features/panels/usePanelsPersistence.ts +++ b/apps/azure/src/features/panels/usePanelsPersistence.ts @@ -3,7 +3,7 @@ import { usePanelsStore } from './panelsStore'; import type { ViewState } from '@variscout/hooks'; /** Stub views are transient surfaces; do not persist them to ViewState. */ -const STUB_VIEWS = new Set(['charter', 'sustainment', 'handoff'] as const); +const STUB_VIEWS = new Set(['charter', 'sustainment'] as const); type PersistedActiveView = NonNullable; @@ -41,7 +41,7 @@ export function usePanelsPersistence( } prevRef.current = { isFindingsOpen, isWhatIfOpen, activeView }; // Stub views are not persisted — omit activeView from the payload when on a stub. - const persistedView = STUB_VIEWS.has(activeView as 'charter' | 'sustainment' | 'handoff') + const persistedView = STUB_VIEWS.has(activeView as 'charter' | 'sustainment') ? undefined : (activeView as PersistedActiveView | undefined); onViewStateChange?.({ isFindingsOpen, isWhatIfOpen, activeView: persistedView }); diff --git a/apps/azure/src/lib/__tests__/__snapshots__/processHubRoutes.test.ts.snap b/apps/azure/src/lib/__tests__/__snapshots__/processHubRoutes.test.ts.snap index e36f1a5f4..1a98b72e8 100644 --- a/apps/azure/src/lib/__tests__/__snapshots__/processHubRoutes.test.ts.snap +++ b/apps/azure/src/lib/__tests__/__snapshots__/processHubRoutes.test.ts.snap @@ -5,8 +5,7 @@ exports[`actionToHref > snapshot — URL shapes are stable 1`] = ` "chartered": "/editor/X?intent=chartered", "focused": "/editor/X?intent=focused", "quick": "/editor/X?intent=quick", - "sustainmentHandoff": "/editor/X/sustainment?surface=handoff", - "sustainmentReview": "/editor/X/sustainment", + "sustainment": "/editor/X/sustainment", "unsupportedInfo": null, "unsupportedPlanned": null, } diff --git a/apps/azure/src/lib/__tests__/processHubRoutes.test.ts b/apps/azure/src/lib/__tests__/processHubRoutes.test.ts index 79e948f43..9e2e6ea93 100644 --- a/apps/azure/src/lib/__tests__/processHubRoutes.test.ts +++ b/apps/azure/src/lib/__tests__/processHubRoutes.test.ts @@ -40,24 +40,14 @@ describe('actionToHref', () => { expect(actionToHref(action)).toBe('/editor/inv-q?intent=quick'); }); - it('builds /editor/:id/sustainment for open-sustainment/review', () => { + it('builds /editor/:id/sustainment for open-sustainment', () => { const action: ResponsePathAction = { kind: 'open-sustainment', investigationId: 'inv-s', - surface: 'review', }; expect(actionToHref(action)).toBe('/editor/inv-s/sustainment'); }); - it('builds /editor/:id/sustainment?surface=handoff for open-sustainment/handoff', () => { - const action: ResponsePathAction = { - kind: 'open-sustainment', - investigationId: 'inv-h', - surface: 'handoff', - }; - expect(actionToHref(action)).toBe('/editor/inv-h/sustainment?surface=handoff'); - }); - it('snapshot — URL shapes are stable', () => { expect({ focused: actionToHref({ @@ -71,15 +61,9 @@ describe('actionToHref', () => { intent: 'chartered', }), quick: actionToHref({ kind: 'open-investigation', investigationId: 'X', intent: 'quick' }), - sustainmentReview: actionToHref({ - kind: 'open-sustainment', - investigationId: 'X', - surface: 'review', - }), - sustainmentHandoff: actionToHref({ + sustainment: actionToHref({ kind: 'open-sustainment', investigationId: 'X', - surface: 'handoff', }), unsupportedPlanned: actionToHref({ kind: 'unsupported', reason: 'planned' }), unsupportedInfo: actionToHref({ kind: 'unsupported', reason: 'informational' }), diff --git a/apps/azure/src/lib/processHubRoutes.ts b/apps/azure/src/lib/processHubRoutes.ts index 96349d1f8..d4df90ece 100644 --- a/apps/azure/src/lib/processHubRoutes.ts +++ b/apps/azure/src/lib/processHubRoutes.ts @@ -11,8 +11,7 @@ export function actionToHref(action: ResponsePathAction): string | null { case 'open-investigation': return `/editor/${action.investigationId}?intent=${action.intent}`; case 'open-sustainment': { - const base = `/editor/${action.investigationId}/sustainment`; - return action.surface === 'handoff' ? `${base}?surface=handoff` : base; + return `/editor/${action.investigationId}/sustainment`; } default: return assertNever(action); diff --git a/apps/azure/src/pages/Dashboard.tsx b/apps/azure/src/pages/Dashboard.tsx index 10a2004f3..73ae0bc00 100644 --- a/apps/azure/src/pages/Dashboard.tsx +++ b/apps/azure/src/pages/Dashboard.tsx @@ -44,17 +44,9 @@ import EvidenceSheet from '../components/EvidenceSheet'; import ProcessHubView from '../components/ProcessHubView'; import SampleDataPicker from '../components/SampleDataPicker'; import StateItemNotesDrawer from '../components/StateItemNotesDrawer'; -import { usePanelsStore } from '../features/panels/panelsStore'; - -const PENDING_HANDOFF_TARGET_KEY = 'variscout.pendingHandoffTargetId'; interface DashboardProps { - onOpenProject: ( - id?: string, - processHubId?: string, - startPaste?: boolean, - handoffTargetId?: string - ) => void; + onOpenProject: (id?: string, processHubId?: string, startPaste?: boolean) => void; /** Load a .vrs project file (from SharePoint download) */ onLoadProjectFile?: (file: File) => void; /** Load a sample dataset directly into a new analysis */ @@ -260,19 +252,6 @@ export const Dashboard: React.FC = ({ [sustainmentRecords, onOpenProject] ); - const handleRecordHandoff = useCallback( - (targetId: string) => { - const record = sustainmentRecords.find( - r => r.id === targetId || r.controlHandoffId === targetId || r.investigationId === targetId - ); - const handoffTargetId = record?.id ?? targetId; - window.sessionStorage.setItem(PENDING_HANDOFF_TARGET_KEY, handoffTargetId); - usePanelsStore.getState().showHandoff(handoffTargetId); - onOpenProject(undefined, record?.hubId, false, handoffTargetId); - }, - [sustainmentRecords, onOpenProject] - ); - const handleResponsePathAction = useCallback( (item: ProcessStateItem, action: ResponsePathAction, hubId: string) => { const href = actionToHref(action); @@ -876,7 +855,6 @@ export const Dashboard: React.FC = ({ onStartInvestigation={() => onOpenProject(undefined, selectedHubRollup.hub.id)} onSetupSustainment={handleSetupSustainment} onLogReview={handleLogReview} - onRecordHandoff={handleRecordHandoff} onResponsePathAction={handleResponsePathAction} onRequestAddNote={handleRequestAddNote} onRequestEditNote={handleRequestEditNote} diff --git a/apps/azure/src/pages/Editor.tsx b/apps/azure/src/pages/Editor.tsx index 524e3f253..00ee3af7d 100644 --- a/apps/azure/src/pages/Editor.tsx +++ b/apps/azure/src/pages/Editor.tsx @@ -114,7 +114,6 @@ import { InvestigationWorkspace } from '../components/editor/InvestigationWorksp import FrameView from '../components/editor/FrameView'; import ImprovementProjectPanel from '../components/charter/ImprovementProjectPanel'; import SustainmentPanel from '../components/sustainment/SustainmentPanel'; -import HandoffPanel from '../components/handoff/HandoffPanel'; import { EditorModals } from '../components/editor/EditorModals'; import { EditorMobileSheet } from '../components/editor/EditorMobileSheet'; import ProjectDashboard from '../components/ProjectDashboard'; @@ -125,7 +124,6 @@ const WhatIfPage = lazyWithRetry(() => import('../components/WhatIfPage')); const ReportView = lazyWithRetry(() => import('../components/views/ReportView')); const INVESTIGATION_DEPTHS: InvestigationDepth[] = ['quick', 'focused', 'chartered']; -const PENDING_HANDOFF_TARGET_KEY = 'variscout.pendingHandoffTargetId'; const INVESTIGATION_STATUSES: InvestigationStatus[] = [ 'issue-captured', 'framing', @@ -296,8 +294,6 @@ interface EditorProps { initialSample?: SampleDataset | null; /** Process Hub to assign when starting a new investigation from the hub home */ initialProcessHubId?: string; - /** Open Handoff after cross-view navigation from Survey Inbox. */ - initialHandoffTargetId?: string; /** * When true, open PasteScreen immediately on mount (used by "Add framing" CTA * to route directly to the paste flow rather than stopping at EditorEmptyState). @@ -315,7 +311,6 @@ export const Editor: React.FC = ({ initialMode, initialSample, initialProcessHubId, - initialHandoffTargetId, startPasteOnMount, }) => { const { @@ -515,12 +510,8 @@ export const Editor: React.FC = ({ // Panel visibility and chart/table sync (Zustand store) const activeView = usePanelsStore(s => s.activeView); - const handoffTargetId = usePanelsStore(s => s.handoffTargetId); const sustainmentTargetId = usePanelsStore(s => s.sustainmentTargetId); const selectedProjectId = usePanelsStore(s => s.selectedProjectId); - const [navigationHandoffTargetId, setNavigationHandoffTargetId] = useState( - initialHandoffTargetId ?? null - ); const isCoScoutOpen = usePanelsStore(s => s.isCoScoutOpen); const isWhatIfOpen = usePanelsStore(s => s.isWhatIfOpen); const isPISidebarOpen = usePanelsStore(s => s.isPISidebarOpen); @@ -533,22 +524,6 @@ export const Editor: React.FC = ({ usePanelsStore.getState().initFromViewState(viewState); }, [viewState]); - useEffect(() => { - const pendingHandoffTargetId = - initialHandoffTargetId ?? window.sessionStorage.getItem(PENDING_HANDOFF_TARGET_KEY); - if (pendingHandoffTargetId) { - window.sessionStorage.removeItem(PENDING_HANDOFF_TARGET_KEY); - setNavigationHandoffTargetId(pendingHandoffTargetId); - usePanelsStore.getState().showHandoff(pendingHandoffTargetId); - } - }, [initialHandoffTargetId]); - - useEffect(() => { - if (handoffTargetId && activeView !== 'handoff') { - usePanelsStore.getState().showHandoff(handoffTargetId); - } - }, [activeView, handoffTargetId]); - // Bridge hook: persists Zustand panel state to DataContext (IndexedDB/OneDrive) usePanelsPersistence(handleViewStateChange); @@ -1714,9 +1689,7 @@ export const Editor: React.FC = ({ saveStatus={saveStatus} hasData={rawData.length > 0} activeView={ - activeView === 'charter' || activeView === 'sustainment' || activeView === 'handoff' - ? undefined - : activeView + activeView === 'charter' || activeView === 'sustainment' ? undefined : activeView } openQuestionCount={ questionsState.questions.filter(h => h.questionSource && h.status === 'open').length @@ -1797,16 +1770,7 @@ export const Editor: React.FC = ({ }} className="flex-1 flex flex-col min-h-0 bg-surface rounded-xl border border-edge overflow-hidden" > - {activeView === 'handoff' || navigationHandoffTargetId ? ( - { - setNavigationHandoffTargetId(null); - usePanelsStore.getState().showFrame(); - }} - /> - ) : activeView === 'sustainment' ? ( + {activeView === 'sustainment' ? ( import('./components/views/FrameView')); const ImprovementProjectPanel = lazyWithRetry(() => import('./components/ImprovementProjectPanel')); const SustainmentPanel = lazyWithRetry(() => import('./components/SustainmentPanel')); -const HandoffPanel = lazyWithRetry(() => import('./components/HandoffPanel')); const InvestigationView = lazyWithRetry(() => import('./components/views/InvestigationView')); const ImprovementView = lazyWithRetry(() => import('./components/views/ImprovementView')); const ProjectsTabView = lazyWithRetry(() => import('./components/ProjectsTabView')); @@ -1070,8 +1069,7 @@ function AppMain() { !importFlow.isManualEntry && !importFlow.isMapping && panels.activeView !== 'charter' && - panels.activeView !== 'sustainment' && - panels.activeView !== 'handoff' + panels.activeView !== 'sustainment' ? panels.activeView : undefined } @@ -1320,12 +1318,6 @@ function AppMain() { targetId={panels.sustainmentTargetId ?? undefined} onBack={panels.showFrame} /> - ) : panels.activeView === 'handoff' ? ( - ) : panels.activeView === 'investigation' ? ( void; -} - -function makeId(): string { - if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) return crypto.randomUUID(); - return `handoff-${Date.now()}`; -} - -function liveConfirmedRecords(records: SustainmentRecord[] | undefined): SustainmentRecord[] { - return (records ?? []).filter( - record => record.deletedAt === null && record.status === 'confirmed-sustained' - ); -} - -function selectRecord(records: SustainmentRecord[], targetId: string | undefined) { - if (targetId) { - const byId = records.find(record => record.id === targetId); - if (byId) return byId; - const byHandoff = records.find(record => record.controlHandoffId === targetId); - if (byHandoff) return byHandoff; - } - return records[0] ?? null; -} - -function selectHandoff( - handoffs: ControlHandoff[], - record: SustainmentRecord | null, - targetId: string | undefined -) { - if (targetId) { - const byId = handoffs.find(handoff => handoff.id === targetId); - if (byId) return byId; - } - if (!record) return null; - return ( - handoffs.find(handoff => handoff.id === record.controlHandoffId) ?? - handoffs.find(handoff => handoff.investigationId === record.investigationId) ?? - null - ); -} - -function buildDraftHandoff(hub: ProcessHub, record: SustainmentRecord): ControlHandoff { - const now = Date.now(); - return { - id: makeId(), - investigationId: record.investigationId, - hubId: hub.id, - status: 'pending', - surface: 'qms-procedure', - systemName: '', - operationalOwner: record.owner ?? hub.processOwner ?? { displayName: '' }, - handoffDate: now, - description: record.targetSummary ?? '', - retainSustainmentReview: true, - recordedBy: { displayName: 'Local browser' }, - escalationPath: record.openConcerns, - reactionPlan: '', - createdAt: now, - deletedAt: null, - }; -} - -const HandoffPanel: React.FC = ({ activeHub, targetId, onBack }) => { - const { isPaid } = useTier(); - const [records, setRecords] = useState([]); - const [handoffs, setHandoffs] = useState([]); - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(Boolean(activeHub)); - const creatingForRecordRef = useRef(null); - - useEffect(() => { - setRecords(liveConfirmedRecords(activeHub?.sustainmentRecords)); - setHandoffs((activeHub?.controlHandoffs ?? []).filter(handoff => handoff.deletedAt === null)); - setError(null); - setIsLoading(Boolean(activeHub)); - - if (!activeHub) { - setIsLoading(false); - return; - } - - let cancelled = false; - void Promise.all([ - pwaHubRepository.sustainmentRecords.listByHub(activeHub.id), - pwaHubRepository.controlHandoffs.listByHub(activeHub.id), - ]) - .then(([loadedRecords, loadedHandoffs]) => { - if (cancelled) return; - setRecords(liveConfirmedRecords(loadedRecords)); - setHandoffs(loadedHandoffs.filter(handoff => handoff.deletedAt === null)); - }) - .catch(() => { - if (cancelled) return; - setRecords(liveConfirmedRecords(activeHub.sustainmentRecords)); - setHandoffs( - (activeHub.controlHandoffs ?? []).filter(handoff => handoff.deletedAt === null) - ); - }) - .finally(() => { - if (!cancelled) setIsLoading(false); - }); - - return () => { - cancelled = true; - }; - }, [activeHub]); - - const selectedRecord = useMemo(() => selectRecord(records, targetId), [records, targetId]); - const selectedHandoff = useMemo( - () => selectHandoff(handoffs, selectedRecord, targetId), - [handoffs, selectedRecord, targetId] - ); - - useEffect(() => { - if (!activeHub || isLoading || !selectedRecord || selectedHandoff) return; - if (creatingForRecordRef.current === selectedRecord.id) return; - - const draft = buildDraftHandoff(activeHub, selectedRecord); - creatingForRecordRef.current = selectedRecord.id; - setError(null); - let cancelled = false; - - void (async () => { - await pwaHubRepository.dispatch({ - kind: 'CONTROL_HANDOFF_CREATE', - hubId: activeHub.id, - handoff: draft, - }); - if (!selectedRecord.controlHandoffId) { - await pwaHubRepository.dispatch({ - kind: 'SUSTAINMENT_RECORD_UPDATE', - recordId: selectedRecord.id, - patch: { controlHandoffId: draft.id }, - }); - } - })() - .then(() => { - if (cancelled) return; - setHandoffs(current => [...current, draft]); - setRecords(current => - current.map(record => - record.id === selectedRecord.id ? { ...record, controlHandoffId: draft.id } : record - ) - ); - }) - .catch(() => { - if (!cancelled) setError('Could not create a handoff.'); - }) - .finally(() => { - if (creatingForRecordRef.current === selectedRecord.id) creatingForRecordRef.current = null; - }); - - return () => { - cancelled = true; - }; - }, [activeHub, isLoading, selectedHandoff, selectedRecord]); - - const patchHandoff = useCallback( - (patch: HandoffChangePatch) => { - if (!selectedHandoff) return; - const next = { ...selectedHandoff, ...patch }; - setHandoffs(current => - current.map(handoff => (handoff.id === selectedHandoff.id ? next : handoff)) - ); - void pwaHubRepository - .dispatch({ kind: 'CONTROL_HANDOFF_UPDATE', handoffId: selectedHandoff.id, patch }) - .catch(() => setError('Could not save handoff changes.')); - }, - [selectedHandoff] - ); - - const acknowledge = useCallback(() => { - if (!selectedHandoff) return; - const acknowledgedAt = Date.now(); - const acknowledgedBy = selectedHandoff.operationalOwner; - setHandoffs(current => - current.map(handoff => - handoff.id === selectedHandoff.id - ? { - ...handoff, - status: 'acknowledged', - acknowledgedAt, - ownerAcknowledgement: { acknowledgedBy }, - } - : handoff - ) - ); - void pwaHubRepository.dispatch({ - kind: 'CONTROL_HANDOFF_ACKNOWLEDGE', - handoffId: selectedHandoff.id, - acknowledgedAt, - acknowledgedBy, - }); - }, [selectedHandoff]); - - const markOperational = useCallback(() => { - if (!selectedHandoff) return; - const operationalAt = Date.now(); - setHandoffs(current => - current.map(handoff => - handoff.id === selectedHandoff.id - ? { ...handoff, status: 'operational', operationalAt } - : handoff - ) - ); - void pwaHubRepository.dispatch({ - kind: 'CONTROL_HANDOFF_MARK_OPERATIONAL', - handoffId: selectedHandoff.id, - operationalAt, - }); - }, [selectedHandoff]); - - const sponsorSignoff = useCallback(() => { - if (!selectedHandoff) return; - const signoff = { approvedAt: Date.now(), approvedBy: { displayName: 'Sponsor' } }; - setHandoffs(current => - current.map(handoff => - handoff.id === selectedHandoff.id - ? { - ...handoff, - status: 'operational', - operationalAt: handoff.operationalAt ?? signoff.approvedAt, - signoff: { ...(handoff.signoff ?? {}), ...signoff }, - } - : handoff - ) - ); - void pwaHubRepository.dispatch({ - kind: 'CONTROL_HANDOFF_SIGNOFF', - handoffId: selectedHandoff.id, - signoff, - }); - }, [selectedHandoff]); - - const heading = activeHub?.name ?? 'No active hub'; - - return ( -
-
-
-

Handoff

-

{heading}

-
- -
- - {!activeHub ? ( -

- Create or select a Process Hub before opening handoff. -

- ) : error ? ( -

- {error} -

- ) : isLoading ? ( -

- Loading handoff... -

- ) : selectedHandoff ? ( - - ) : selectedRecord ? ( -

- Creating handoff... -

- ) : ( -

- Confirm sustainment before recording handoff. -

- )} -
- ); -}; - -export default HandoffPanel; diff --git a/apps/pwa/src/components/views/FrameView.tsx b/apps/pwa/src/components/views/FrameView.tsx index 5435e1911..63a278aa1 100644 --- a/apps/pwa/src/components/views/FrameView.tsx +++ b/apps/pwa/src/components/views/FrameView.tsx @@ -25,10 +25,8 @@ import type { StepCapabilityStamp, ControlHandoff, SustainmentRecord, - WorkflowReadinessSignals, } from '@variscout/core'; import { createActionItem, type ActionItem } from '@variscout/core/findings'; -import type { ImprovementProject } from '@variscout/core/improvementProject'; import { surveyInboxRules } from '@variscout/core/survey'; import { pwaHubRepository } from '../../persistence'; import { useSession } from '../../store/sessionStore'; @@ -62,26 +60,6 @@ function priorStepStatsFromSnapshots( return new Map(stamps.map(stamp => [stamp.stepId, stamp])); } -function hasCompletedInterventionEvidence( - projects: readonly ImprovementProject[], - items: readonly ActionItem[] -): boolean { - const completedActionIds = new Set( - items - .filter( - item => - item.deletedAt === null && - (item.completedAt !== undefined || item.status === 'done' || item.doneAt != null) - ) - .map(item => item.id) - ); - return projects.some(project => { - if (project.deletedAt !== null || project.status !== 'closed') return false; - const actionItemIds = project.sections.approach.actionItemIds ?? []; - return actionItemIds.some(id => completedActionIds.has(id)); - }); -} - const FrameView: React.FC = () => { const rawData = useProjectStore(s => s.rawData); const outcome = useProjectStore(s => s.outcome); @@ -222,19 +200,6 @@ const FrameView: React.FC = () => { sustainmentRecords, ]); - const signals: WorkflowReadinessSignals = React.useMemo(() => { - const improvementProjects = ( - activeHubId ? (projectsByHub[activeHubId] ?? activeHub?.improvementProjects ?? []) : [] - ).filter(project => project.deletedAt === null); - - return { - hasIntervention: hasCompletedInterventionEvidence(improvementProjects, actionItems), - sustainmentConfirmed: sustainmentRecords.some( - record => record.deletedAt === null && record.status === 'confirmed-sustained' - ), - }; - }, [activeHub?.improvementProjects, activeHubId, actionItems, projectsByHub, sustainmentRecords]); - const inboxPrompts = React.useMemo(() => { const improvementProjects = ( activeHubId ? (projectsByHub[activeHubId] ?? activeHub?.improvementProjects ?? []) : [] @@ -336,14 +301,6 @@ const FrameView: React.FC = () => { usePanelsStore.getState().showCharter(); }, []); - const handleSustainment = React.useCallback(() => { - usePanelsStore.getState().showSustainment(); - }, []); - - const handleHandoff = React.useCallback(() => { - usePanelsStore.getState().showHandoff(); - }, []); - const handleInboxNavigate = React.useCallback((prompt: InboxDigestPrompt) => { const surface = prompt.action?.opensSurface; if (surface === 'sustainment') { @@ -412,10 +369,7 @@ const FrameView: React.FC = () => { onOpenInvestigationFocus={handleOpenInvestigationFocus} onAddCausalLink={handleAddCausalLink} onRemoveCausalLink={handleRemoveCausalLink} - signals={signals} onCharter={handleCharter} - onSustainment={handleSustainment} - onHandoff={handleHandoff} contextLinkGroups={contextLinkGroups} onNavigateContextLink={handleNavigateContextLink} priorStepStats={priorStepStats} diff --git a/apps/pwa/src/components/views/__tests__/FrameView.test.tsx b/apps/pwa/src/components/views/__tests__/FrameView.test.tsx index 537a6e397..b82f74efa 100644 --- a/apps/pwa/src/components/views/__tests__/FrameView.test.tsx +++ b/apps/pwa/src/components/views/__tests__/FrameView.test.tsx @@ -1,6 +1,5 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { WorkflowReadinessSignals } from '@variscout/core'; const setProcessContextMock = vi.fn(); const setMeasureSpecMock = vi.fn(); @@ -124,7 +123,6 @@ vi.mock('@variscout/ui', async () => { ), CanvasWorkspace: (props: { canvasViewportHubId?: string | null; - signals: WorkflowReadinessSignals; onSeeData: () => void; onQuickAction?: (stepId: string) => void; onLogQuickAction?: ( @@ -142,8 +140,6 @@ vi.mock('@variscout/ui', async () => { ) => void; onRemoveCausalLink?: (linkId: string) => void; onCharter?: () => void; - onSustainment?: () => void; - onHandoff?: () => void; priorStepStats?: ReadonlyMap; actionItems?: unknown[]; contextLinkGroups?: { surfaceType: string; items: { id: string }[] }[]; @@ -201,16 +197,6 @@ vi.mock('@variscout/ui', async () => { 'button', { type: 'button', 'data-testid': 'cta-charter', onClick: props.onCharter }, 'Charter' - ), - React.createElement( - 'button', - { type: 'button', 'data-testid': 'cta-sustainment', onClick: props.onSustainment }, - 'Sustainment' - ), - React.createElement( - 'button', - { type: 'button', 'data-testid': 'cta-handoff', onClick: props.onHandoff }, - 'Handoff' ) ); }, @@ -334,7 +320,6 @@ describe('FrameView (PWA shell)', () => { questions: [{ id: 'q-1' }], hypotheses: [{ id: 'hub-1' }], causalLinks: [{ id: 'link-1' }], - signals: { hasIntervention: false, sustainmentConfirmed: false }, }) ); }); @@ -615,55 +600,15 @@ describe('FrameView (PWA shell)', () => { expect(removeCausalLinkMock).toHaveBeenCalledWith('link-created'); }); - it('wires Canvas charter/sustainment/handoff CTAs to the panels-store show actions', () => { + it('wires Canvas charter CTA to the panels-store show action', () => { render(); fireEvent.click(screen.getByTestId('cta-charter')); - fireEvent.click(screen.getByTestId('cta-sustainment')); - fireEvent.click(screen.getByTestId('cta-handoff')); expect(showCharterMock).toHaveBeenCalledTimes(1); - expect(showSustainmentMock).toHaveBeenCalledTimes(1); - expect(showHandoffMock).toHaveBeenCalledTimes(1); - }); - - it('marks Sustainment ready only when a closed project has completed intervention evidence and keeps Handoff gated until sustainment is confirmed', async () => { - improvementProjectStateRef.current = { - projectsByHub: { - 'hub-1': [ - { - id: 'ip-1', - hubId: 'hub-1', - status: 'closed', - metadata: { title: 'Reduce rework' }, - goal: { outcomeGoal: { outcomeSpecId: 'outcome-1', target: 98 } }, - sections: { - background: {}, - investigationLineage: {}, - approach: { actionItemIds: ['action-1'] }, - outcomeReference: {}, - }, - createdAt: 1, - updatedAt: 1, - deletedAt: null, - }, - ], - }, - getProjectsForHub: () => [], - }; - hoisted.actionItemsListByHubMock.mockResolvedValue([ - { ...actionItem('action-1', 'Change nozzle'), completedAt: 1714000000000 }, - ]); - - render(); - - await waitFor(() => { - const props = hoisted.canvasWorkspaceMock.mock.lastCall?.[0]; - expect(props?.signals).toEqual({ hasIntervention: true, sustainmentConfirmed: false }); - }); }); - it('marks Handoff ready and includes sustainment context links when a live record is confirmed', async () => { + it('includes sustainment context links when a live record is confirmed', async () => { hoisted.sustainmentRecordsListByHubMock.mockResolvedValue([ { id: 'sr-1', @@ -685,7 +630,6 @@ describe('FrameView (PWA shell)', () => { await waitFor(() => { const props = hoisted.canvasWorkspaceMock.mock.lastCall?.[0]; - expect(props?.signals.sustainmentConfirmed).toBe(true); expect( props?.contextLinkGroups?.find( (group: { surfaceType: string }) => group.surfaceType === 'sustainment' diff --git a/apps/pwa/src/features/panels/__tests__/panelsStore.test.ts b/apps/pwa/src/features/panels/__tests__/panelsStore.test.ts index ab20cc6c2..13253104e 100644 --- a/apps/pwa/src/features/panels/__tests__/panelsStore.test.ts +++ b/apps/pwa/src/features/panels/__tests__/panelsStore.test.ts @@ -162,10 +162,10 @@ describe('panelsStore', () => { expect(usePanelsStore.getState().activeView).toBe('sustainment'); }); - it('showHandoff sets activeView to handoff', () => { + it('showHandoff redirects to sustainment (handoff folded into sustainment in wedge V1)', () => { usePanelsStore.getState().showHandoff('sr-1'); - expect(usePanelsStore.getState().activeView).toBe('handoff'); - expect(usePanelsStore.getState().handoffTargetId).toBe('sr-1'); + expect(usePanelsStore.getState().activeView).toBe('sustainment'); + expect(usePanelsStore.getState().sustainmentTargetId).toBe('sr-1'); }); }); diff --git a/apps/pwa/src/features/panels/panelsStore.ts b/apps/pwa/src/features/panels/panelsStore.ts index b3d3e19d4..f01c1a0d6 100644 --- a/apps/pwa/src/features/panels/panelsStore.ts +++ b/apps/pwa/src/features/panels/panelsStore.ts @@ -14,8 +14,7 @@ interface PanelsState { | 'projects' | 'report' | 'charter' - | 'sustainment' - | 'handoff'; + | 'sustainment'; // Panel visibility isSettingsOpen: boolean; @@ -33,7 +32,6 @@ interface PanelsState { showResetConfirm: boolean; openSpecEditorRequested: boolean; sustainmentTargetId: string | null; - handoffTargetId: string | null; selectedProjectId: string | null; } @@ -96,7 +94,6 @@ export const initialPanelsState: PanelsState = { showResetConfirm: false, openSpecEditorRequested: false, sustainmentTargetId: null, - handoffTargetId: null, selectedProjectId: null, }; @@ -120,8 +117,13 @@ export const usePanelsStore = create(set => ({ isFindingsOpen: false, sustainmentTargetId: targetId ?? null, }), + // Alias for showSustainment — wedge V1 folds Handoff into Sustainment-closure (ADR-082). Inbox prompts + context links still emit surface === 'handoff'; routing through this alias keeps them reachable. showHandoff: targetId => - set({ activeView: 'handoff', isFindingsOpen: false, handoffTargetId: targetId ?? null }), + set({ + activeView: 'sustainment', + isFindingsOpen: false, + sustainmentTargetId: targetId ?? null, + }), // Simple toggles setSettingsOpen: open => set({ isSettingsOpen: open }), diff --git a/apps/pwa/src/hooks/useAppPanels.ts b/apps/pwa/src/hooks/useAppPanels.ts index d0363db90..231ef3ab3 100644 --- a/apps/pwa/src/hooks/useAppPanels.ts +++ b/apps/pwa/src/hooks/useAppPanels.ts @@ -23,8 +23,7 @@ export interface UseAppPanelsReturn { | 'projects' | 'report' | 'charter' - | 'sustainment' - | 'handoff'; + | 'sustainment'; showFrame: () => void; showHome: () => void; showAnalysis: () => void; @@ -52,7 +51,6 @@ export interface UseAppPanelsReturn { isDesktop: boolean; openSpecEditorRequested: boolean; sustainmentTargetId: string | null; - handoffTargetId: string | null; selectedProjectId: string | null; setOpenSpecEditorRequested: (v: boolean) => void; openDataTableAtRow: (index: number) => void; @@ -95,7 +93,6 @@ export function useAppPanels(options: UseAppPanelsOptions): UseAppPanelsReturn { const isPISidebarOpen = usePanelsStore(s => s.isPISidebarOpen); const openSpecEditorRequested = usePanelsStore(s => s.openSpecEditorRequested); const sustainmentTargetId = usePanelsStore(s => s.sustainmentTargetId); - const handoffTargetId = usePanelsStore(s => s.handoffTargetId); const selectedProjectId = usePanelsStore(s => s.selectedProjectId); // ── Action selectors (stable function references from the store) ────── @@ -205,7 +202,6 @@ export function useAppPanels(options: UseAppPanelsOptions): UseAppPanelsReturn { isDesktop, openSpecEditorRequested, sustainmentTargetId, - handoffTargetId, selectedProjectId, isPISidebarOpen, diff --git a/docs/investigations.md b/docs/investigations.md index 8a6e48321..7234ac543 100644 --- a/docs/investigations.md +++ b/docs/investigations.md @@ -40,6 +40,14 @@ Code-level smells, UX follow-ups, and architectural questions surfaced during wo - **Sponsor placeholder copy in `IPDetailPage`** — `data-testid="sponsor-report-panel"` placeholder points the Sponsor to "the top navigation Report tab." The wording "top navigation" is generic; if the V1 6-tab nav lands a localized label, the placeholder should reference the actual tab label. Promotion: PR-WV1-5 (nav reorder + tier-gating retirement sweep) — pick up when nav labels are finalized. +### 2026-05-17: Survey-rule layer still emits 'handoff' surface prompts + +`packages/core/src/survey/handoff.ts:69` produces `surveyInboxRules` entries with `surface: 'handoff'`, which now route through the `showHandoff → showSustainment` alias in panelsStore. Wedge V1 (PR-WV1-4) retired the canvas-CTA handoff path; the survey rule layer is intentionally out-of-scope per ADR-082. **Follow-up**: in a later wedge cleanup PR, retire the survey rule too (or rename to `surface: 'sustainment'` + reword the prompt text). Source: Opus review of PR #187 (2026-05-17). + +**Surfaced by:** Opus final code review of PR #187 (feat/wedge-pr-wv1-4-canvas-persona), 2026-05-17. + +**Promotion path:** Wedge cleanup PR post-V1 stabilisation — rename `surface: 'handoff'` → `surface: 'sustainment'` and reword prompt copy, or retire the rule entirely if the survey cadence no longer fits the Sustainment stage flow. + ### Durable cross-device invitation persistence **Surfaced by:** PR-WV1-3a Implementation 2026-05-16. diff --git a/packages/core/src/__tests__/processHub.test.ts b/packages/core/src/__tests__/processHub.test.ts index 0865af7f1..0044c18bf 100644 --- a/packages/core/src/__tests__/processHub.test.ts +++ b/packages/core/src/__tests__/processHub.test.ts @@ -766,36 +766,6 @@ describe('buildProcessHubCadence — sustainment lane', () => { expect(cadence.sustainment.totalCount).toBe(1); expect(cadence.sustainment.items[0].investigation.id).toBe('inv-due'); }); - - it('includes controlled investigations missing a ControlHandoff in the sustainment lane', () => { - const hubs: ProcessHub[] = [ - { id: 'hub-1', name: 'Line 4', createdAt: 1777075200000, deletedAt: null }, - ]; - const now = new Date('2026-04-26T00:00:00.000Z'); - const investigations = [ - { - id: 'inv-controlled', - name: 'Needs handoff', - updatedAt: 1777161600000, - createdAt: 1777161600000, - deletedAt: null, - metadata: makeMetadata({ - processHubId: 'hub-1', - investigationStatus: 'controlled', - }), - }, - ]; - - const [rollup] = buildProcessHubRollups(hubs, investigations, { - sustainmentRecords: [], - controlHandoffs: [], - }); - const cadence = buildProcessHubCadence(rollup, now); - - expect(cadence.sustainment.totalCount).toBe(1); - expect(cadence.sustainment.items[0].investigation.id).toBe('inv-controlled'); - expect(cadence.sustainment.items[0].reasons).toContain('control-handoff-missing'); - }); }); describe('buildProcessHubRollups', () => { diff --git a/packages/core/src/__tests__/processState.test.ts b/packages/core/src/__tests__/processState.test.ts index 635612da7..474c8b9aa 100644 --- a/packages/core/src/__tests__/processState.test.ts +++ b/packages/core/src/__tests__/processState.test.ts @@ -213,7 +213,6 @@ describe('buildCurrentProcessState', () => { expect(state.responsePathCounts['focused-investigation']).toBeGreaterThan(0); expect(state.responsePathCounts['measurement-system-work']).toBeGreaterThan(0); expect(state.responsePathCounts['sustainment-review']).toBe(1); - expect(state.responsePathCounts['control-handoff']).toBe(1); expect(state.responsePathCounts['quick-action']).toBeGreaterThan(0); expect(state.responsePathCounts['chartered-project']).toBe(1); expect(state.items).toEqual( @@ -248,11 +247,6 @@ describe('buildCurrentProcessState', () => { lens: 'sustainment', responsePath: 'sustainment-review', }), - expect.objectContaining({ - id: 'control-handoff', - lens: 'sustainment', - responsePath: 'control-handoff', - }), expect.objectContaining({ id: 'active:quick', responsePath: 'quick-action', diff --git a/packages/core/src/__tests__/responsePathAction.test.ts b/packages/core/src/__tests__/responsePathAction.test.ts index da46a6a28..112c5c3cf 100644 --- a/packages/core/src/__tests__/responsePathAction.test.ts +++ b/packages/core/src/__tests__/responsePathAction.test.ts @@ -61,7 +61,7 @@ describe('deriveResponsePathAction', () => { }); }); - it('maps sustainment-review to open-sustainment/review', () => { + it('maps sustainment-review to open-sustainment', () => { const action = deriveResponsePathAction( baseItem({ responsePath: 'sustainment-review' }), DEFAULT_ID @@ -69,19 +69,6 @@ describe('deriveResponsePathAction', () => { expect(action).toEqual({ kind: 'open-sustainment', investigationId: DEFAULT_ID, - surface: 'review', - }); - }); - - it('maps control-handoff to open-sustainment/handoff', () => { - const action = deriveResponsePathAction( - baseItem({ responsePath: 'control-handoff' }), - DEFAULT_ID - ); - expect(action).toEqual({ - kind: 'open-sustainment', - investigationId: DEFAULT_ID, - surface: 'handoff', }); }); diff --git a/packages/core/src/__tests__/responsePathReadiness.test.ts b/packages/core/src/__tests__/responsePathReadiness.test.ts index 168028929..de308a492 100644 --- a/packages/core/src/__tests__/responsePathReadiness.test.ts +++ b/packages/core/src/__tests__/responsePathReadiness.test.ts @@ -2,7 +2,6 @@ import { describe, it, expect } from 'vitest'; import { isCharterReady, isSustainmentReady, - isHandoffReady, type WorkflowReadinessSignals, } from '../responsePathReadiness'; @@ -37,18 +36,3 @@ describe('isSustainmentReady', () => { expect(isSustainmentReady({ ...empty, hasIntervention: true, isDemo: false })).toBe(true); }); }); - -describe('isHandoffReady', () => { - it('returns false when sustainment not confirmed and not demo', () => { - expect(isHandoffReady(empty)).toBe(false); - }); - it('returns false even with intervention if sustainment not confirmed', () => { - expect(isHandoffReady({ ...empty, hasIntervention: true })).toBe(false); - }); - it('returns true when sustainment is confirmed', () => { - expect(isHandoffReady({ ...empty, sustainmentConfirmed: true })).toBe(true); - }); - it('returns true in demo mode regardless of sustainment state', () => { - expect(isHandoffReady({ ...empty, isDemo: true })).toBe(true); - }); -}); diff --git a/packages/core/src/__tests__/sustainment.test.ts b/packages/core/src/__tests__/sustainment.test.ts index 2fe9b935f..ae513759c 100644 --- a/packages/core/src/__tests__/sustainment.test.ts +++ b/packages/core/src/__tests__/sustainment.test.ts @@ -5,7 +5,6 @@ import { isSustainmentDue, isSustainmentOverdue, nextDueFromCadence, - selectControlHandoffCandidates, selectSustainmentBuckets, selectSustainmentReviews, type ControlHandoff, @@ -511,30 +510,6 @@ describe('selectSustainmentReviews', () => { }); }); -describe('selectControlHandoffCandidates', () => { - it('returns controlled investigations with no handoff record', () => { - const investigations = [ - makeInvestigation('inv-1', 'controlled'), - makeInvestigation('inv-2', 'resolved'), - makeInvestigation('inv-3', 'controlled'), - ]; - const handoffs: ControlHandoff[] = [makeHandoff('inv-3', true, 'qms-procedure')]; - - const result = selectControlHandoffCandidates(investigations, handoffs); - - expect(result.map(r => r.investigation.id)).toEqual(['inv-1']); - }); - - it('returns empty when all controlled investigations have handoffs', () => { - const investigations = [makeInvestigation('inv-1', 'controlled')]; - const handoffs: ControlHandoff[] = [makeHandoff('inv-1', true)]; - - const result = selectControlHandoffCandidates(investigations, handoffs); - - expect(result).toEqual([]); - }); -}); - describe('selectSustainmentBuckets', () => { const NOW = new Date('2026-04-26T00:00:00.000Z'); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 23b517c4c..5f9001edf 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -203,7 +203,7 @@ export { } from './tier'; // Response-path readiness helpers (prerequisite checks for canvas CTAs) -export { isCharterReady, isSustainmentReady, isHandoffReady } from './responsePathReadiness'; +export { isCharterReady, isSustainmentReady } from './responsePathReadiness'; export type { WorkflowReadinessSignals } from './responsePathReadiness'; // Process Hub review signals @@ -563,7 +563,6 @@ export { isSustainmentOverdue, selectSustainmentReviews, selectSustainmentBuckets, - selectControlHandoffCandidates, sustainmentRecordBlobPath, sustainmentReviewBlobPath, controlHandoffBlobPath, diff --git a/packages/core/src/processHub.ts b/packages/core/src/processHub.ts index f371caae0..b3947078b 100644 --- a/packages/core/src/processHub.ts +++ b/packages/core/src/processHub.ts @@ -19,7 +19,6 @@ import type { SpecLimits } from './types'; import { isSustainmentDue, isSustainmentOverdue, - selectControlHandoffCandidates, selectSustainmentBuckets, selectSustainmentReviews, type ControlHandoff, @@ -29,7 +28,7 @@ import { } from './sustainment'; export { buildReviewItem } from './processHubReview'; -export { isCharterReady, isSustainmentReady, isHandoffReady } from './responsePathReadiness'; +export { isCharterReady, isSustainmentReady } from './responsePathReadiness'; export type { WorkflowReadinessSignals } from './responsePathReadiness'; /** @@ -310,8 +309,7 @@ export type ProcessHubAttentionReason = | 'overdue-actions' | 'next-move' | 'sustainment' - | 'sustainment-due' - | 'control-handoff-missing'; + | 'sustainment-due'; export type ProcessHubReadinessReason = | 'missing-metadata' @@ -891,11 +889,7 @@ export function buildProcessHubCadence item.reasons.includes('control-handoff-missing'))) { - items.push({ - id: 'control-handoff', - lens: 'sustainment', - severity: 'amber', - responsePath: 'control-handoff', - source: 'sustainment', - label: 'Control handoff needed', - count: cadence.sustainment.items.filter(item => - item.reasons.includes('control-handoff-missing') - ).length, - investigationIds: cadence.sustainment.items - .filter(item => item.reasons.includes('control-handoff-missing')) - .map(item => item.investigation.id), - }); - } - - const sustainmentReviewItems = cadence.sustainment.items.filter( - item => !item.reasons.includes('control-handoff-missing') - ); + const sustainmentReviewItems = cadence.sustainment.items; if (sustainmentReviewItems.length > 0) { items.push({ id: 'sustainment', diff --git a/packages/core/src/responsePathAction.ts b/packages/core/src/responsePathAction.ts index a2100fac2..8468bb10b 100644 --- a/packages/core/src/responsePathAction.ts +++ b/packages/core/src/responsePathAction.ts @@ -7,11 +7,7 @@ export type ResponsePathAction = investigationId: string; intent: 'focused' | 'chartered' | 'quick'; } - | { - kind: 'open-sustainment'; - investigationId: string; - surface: 'review' | 'handoff'; - } + | { kind: 'open-sustainment'; investigationId: string } | { kind: 'unsupported'; reason: 'planned' | 'informational' }; /** @@ -44,9 +40,7 @@ export function deriveResponsePathAction( case 'chartered-project': return { kind: 'open-investigation', investigationId, intent: 'chartered' }; case 'sustainment-review': - return { kind: 'open-sustainment', investigationId, surface: 'review' }; - case 'control-handoff': - return { kind: 'open-sustainment', investigationId, surface: 'handoff' }; + return { kind: 'open-sustainment', investigationId }; default: return assertNever(path); } diff --git a/packages/core/src/responsePathReadiness.ts b/packages/core/src/responsePathReadiness.ts index e8803347d..1ab3d507e 100644 --- a/packages/core/src/responsePathReadiness.ts +++ b/packages/core/src/responsePathReadiness.ts @@ -27,7 +27,3 @@ export function isCharterReady(_signals: WorkflowReadinessSignals): boolean { export function isSustainmentReady(signals: WorkflowReadinessSignals): boolean { return signals.isDemo === true || signals.hasIntervention; } - -export function isHandoffReady(signals: WorkflowReadinessSignals): boolean { - return signals.isDemo === true || signals.sustainmentConfirmed; -} diff --git a/packages/core/src/sustainment.ts b/packages/core/src/sustainment.ts index 7cf5bad6d..6b253a3e1 100644 --- a/packages/core/src/sustainment.ts +++ b/packages/core/src/sustainment.ts @@ -415,24 +415,6 @@ export function selectSustainmentBuckets( return { dueNow, overdue, recentlyReviewed }; } -/** - * Returns investigations whose effective status is `controlled` but which lack - * a ControlHandoff record. These should surface in the cadence board as a - * prompt to either record the handoff or revert the status. - */ -export function selectControlHandoffCandidates( - investigations: TInv[], - handoffs: ControlHandoff[] -): ProcessHubReviewItem[] { - const handoffByInvestigation = new Set(handoffs.map(h => h.investigationId)); - return investigations - .filter( - inv => - inv.metadata?.investigationStatus === 'controlled' && !handoffByInvestigation.has(inv.id) - ) - .map(inv => buildSustainmentReviewItem(inv, ['control-handoff-missing'])); -} - // ── Blob path helpers ───────────────────────────────────────────────────── function safePathSegment(value: string): string { diff --git a/packages/ui/src/components/Canvas/CanvasWorkspace.tsx b/packages/ui/src/components/Canvas/CanvasWorkspace.tsx index feaff566c..452a8143d 100644 --- a/packages/ui/src/components/Canvas/CanvasWorkspace.tsx +++ b/packages/ui/src/components/Canvas/CanvasWorkspace.tsx @@ -25,7 +25,6 @@ import { type StepCapabilityStamp, type Hypothesis, type TimelineWindow, - type WorkflowReadinessSignals, } from '@variscout/core'; import { isValidLevel, type CanvasLevel } from '@variscout/core/canvas'; import type { ActionItem } from '@variscout/core/findings'; @@ -62,13 +61,10 @@ export interface CanvasWorkspaceProps { setMeasureSpec: (column: string, partial: Partial) => void; setProcessContext: (context: ProcessContext | null) => void; onSeeData: () => void; - signals: WorkflowReadinessSignals; onQuickAction?: (stepId: string) => void; onLogQuickAction?: (stepId: string, payload: LogActionPayload) => void; onFocusedInvestigation?: (stepId: string) => void; onCharter?: (stepId: string) => void; - onSustainment?: (stepId: string) => void; - onHandoff?: (stepId: string) => void; questions?: readonly Question[]; findings?: readonly Finding[]; hypotheses?: readonly Hypothesis[]; @@ -200,14 +196,11 @@ export const CanvasWorkspace: React.FC = ({ setFactors, setMeasureSpec, setProcessContext, - signals, onSeeData, onQuickAction, onLogQuickAction, onFocusedInvestigation, onCharter, - onSustainment, - onHandoff, questions = [], findings = [], hypotheses = [], @@ -604,13 +597,10 @@ export const CanvasWorkspace: React.FC = ({ onOpenWall={onOpenWall} onAddCausalLink={onAddCausalLink} onRemoveCausalLink={onRemoveCausalLink} - signals={signals} onQuickAction={onQuickAction} onLogQuickAction={onLogQuickAction} onFocusedInvestigation={onFocusedInvestigation} onCharter={onCharter} - onSustainment={onSustainment} - onHandoff={onHandoff} onOpenInvestigationFocus={onOpenInvestigationFocus} onOpenColumnDetail={onOpenColumnDetail} contextLinkGroups={contextLinkGroups} diff --git a/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx b/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx index 0d131d1af..ec60dfa6d 100644 --- a/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx +++ b/packages/ui/src/components/Canvas/__tests__/Canvas.test.tsx @@ -81,8 +81,6 @@ import { Canvas } from '../index'; // Cast helper: acceptable inside test files per project convention const h = (id: string) => id as ProcessHubId; -const SIGNALS = { hasIntervention: false, sustainmentConfirmed: false }; - const map: ProcessMap = { version: 1, nodes: [], @@ -256,7 +254,6 @@ function renderCanvas(overrides: Partial> = {}) { onChange: vi.fn(), data, filter, - signals: SIGNALS, stepCards, ...overrides, }; @@ -298,7 +295,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} /> ); @@ -583,7 +579,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} mode="author" chips={[{ chipId: 'Bake_Time', label: 'Bake Time', role: 'factor' }]} /> @@ -605,7 +600,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} mode="read" chips={[{ chipId: 'Bake_Time', label: 'Bake Time', role: 'factor' }]} /> @@ -626,7 +620,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} mode="author" /> ); @@ -646,7 +639,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} mode="author" disabled chips={[{ chipId: 'Bake_Time', label: 'Bake Time', role: 'factor' }]} @@ -668,7 +660,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} mode="read" onModeChange={vi.fn()} chips={[{ chipId: 'Bake_Time', label: 'Bake Time', role: 'factor' }]} @@ -692,7 +683,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} mode="author" onModeChange={vi.fn()} onAddStep={onAddStep} @@ -720,7 +710,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} mode="author" chips={[{ chipId: 'Bake_Time', label: 'Bake Time', role: 'factor' }]} onPlaceChip={onPlaceChip} @@ -743,7 +732,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} onUngroupSubStep={onUngroupSubStep} /> ); @@ -768,7 +756,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} activeLens="default" onLensChange={onLensChange} @@ -798,7 +785,6 @@ describe('Canvas', () => { onChange={onChange} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} activeOverlays={[]} onOverlayToggle={onOverlayToggle} @@ -918,7 +904,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} activeCanvasTool="select" onCanvasToolChange={onCanvasToolChange} @@ -941,7 +926,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} activeCanvasTool="draw-hypothesis" onCanvasToolChange={onCanvasToolChange} @@ -1005,7 +989,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} activeCanvasTool="draw-hypothesis" onAddCausalLink={onAddCausalLink} @@ -1034,7 +1017,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={metriclessStepCards} activeCanvasTool="draw-hypothesis" /> @@ -1060,7 +1042,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} activeCanvasTool="draw-hypothesis" /> @@ -1084,7 +1065,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} activeCanvasTool="draw-hypothesis" /> @@ -1108,7 +1088,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={metriclessStepCards} activeCanvasTool="draw-hypothesis" /> @@ -1132,7 +1111,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} investigationOverlays={investigationOverlays} activeOverlays={[]} @@ -1150,7 +1128,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} investigationOverlays={investigationOverlays} activeOverlays={['investigations', 'findings', 'hypothesis-hubs']} @@ -1172,7 +1149,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} investigationOverlays={investigationOverlays} activeOverlays={['hypotheses']} @@ -1215,7 +1191,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} investigationOverlays={investigationOverlays} activeOverlays={['hypotheses']} @@ -1306,7 +1281,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} /> ); @@ -1343,7 +1317,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} /> ); @@ -1377,7 +1350,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} /> ); @@ -1413,7 +1385,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} /> ); @@ -1439,7 +1410,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} /> ); @@ -1460,7 +1430,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} investigationOverlays={investigationOverlays} onOpenInvestigationFocus={onOpenInvestigationFocus} @@ -1493,7 +1462,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} investigationOverlays={investigationOverlays} onRemoveCausalLink={onRemoveCausalLink} @@ -1518,7 +1486,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} onStepSpecsRequest={onStepSpecsRequest} /> @@ -1563,7 +1530,6 @@ describe('Canvas', () => { onChange={() => {}} data={data} filter={filter} - signals={SIGNALS} stepCards={stepCards} onQuickAction={onQuickAction} onFocusedInvestigation={onFocusedInvestigation} diff --git a/packages/ui/src/components/Canvas/__tests__/CanvasWorkspace.test.tsx b/packages/ui/src/components/Canvas/__tests__/CanvasWorkspace.test.tsx index 84feaa824..369c4a768 100644 --- a/packages/ui/src/components/Canvas/__tests__/CanvasWorkspace.test.tsx +++ b/packages/ui/src/components/Canvas/__tests__/CanvasWorkspace.test.tsx @@ -618,8 +618,6 @@ import { CanvasWorkspace } from '../CanvasWorkspace'; // Cast helper: acceptable inside test files per project convention const h = (id: string) => id as ProcessHubId; -const SIGNALS = { hasIntervention: false, sustainmentConfirmed: false }; - const rawData = [ { Fill_Weight: 12, Bake_Time: 30, Machine: 'A' }, { Fill_Weight: 13, Bake_Time: 31, Machine: 'B' }, @@ -682,7 +680,6 @@ function renderWorkspace(overrides: Partial { factors={[]} measureSpecs={{}} processContext={processContext} - signals={SIGNALS} setOutcome={vi.fn()} setFactors={vi.fn()} setMeasureSpec={vi.fn()} @@ -1256,7 +1252,6 @@ describe('CanvasWorkspace', () => { factors={[]} measureSpecs={{}} processContext={processContext} - signals={SIGNALS} setOutcome={vi.fn()} setFactors={vi.fn()} setMeasureSpec={vi.fn()} @@ -1304,7 +1299,6 @@ describe('CanvasWorkspace', () => { factors={[]} measureSpecs={{}} processContext={processContext} - signals={SIGNALS} setOutcome={vi.fn()} setFactors={vi.fn()} setMeasureSpec={vi.fn()} @@ -1365,7 +1359,6 @@ describe('CanvasWorkspace', () => { factors={[]} measureSpecs={{}} processContext={processContext} - signals={SIGNALS} setOutcome={vi.fn()} setFactors={vi.fn()} setMeasureSpec={vi.fn()} @@ -1399,7 +1392,6 @@ describe('CanvasWorkspace', () => { factors={[]} measureSpecs={{}} processContext={processContext} - signals={SIGNALS} setOutcome={vi.fn()} setFactors={vi.fn()} setMeasureSpec={vi.fn()} diff --git a/packages/ui/src/components/Canvas/index.tsx b/packages/ui/src/components/Canvas/index.tsx index 30aa3eec2..fd4279f53 100644 --- a/packages/ui/src/components/Canvas/index.tsx +++ b/packages/ui/src/components/Canvas/index.tsx @@ -33,7 +33,6 @@ import { type Hypothesis, type SpecLimits, type Question, - type WorkflowReadinessSignals, } from '@variscout/core'; import type { ActionItem, ColumnTypeMap } from '@variscout/core/findings'; import type { CanvasLevel } from '@variscout/core/canvas'; @@ -224,14 +223,11 @@ export interface CanvasProps { options?: { questionIds?: string[] } ) => void; investigationOverlays?: CanvasInvestigationOverlayModel; - signals: WorkflowReadinessSignals; onStepSpecsRequest?: (column: string, stepId: string) => void; onQuickAction?: (stepId: string) => void; onLogQuickAction?: (stepId: string, payload: LogActionPayload) => void; onFocusedInvestigation?: (stepId: string) => void; onCharter?: (stepId: string) => void; - onSustainment?: (stepId: string) => void; - onHandoff?: (stepId: string) => void; onOpenInvestigationFocus?: (focus: CanvasInvestigationFocus) => void; onRemoveCausalLink?: (linkId: string) => void; contextLinkGroups?: readonly ContextLinkGroup[]; @@ -291,14 +287,11 @@ export const Canvas: React.FC = ({ hypotheses = [], onAddCausalLink, investigationOverlays, - signals, onStepSpecsRequest, onQuickAction, onLogQuickAction, onFocusedInvestigation, onCharter, - onSustainment, - onHandoff, onOpenInvestigationFocus, onRemoveCausalLink, contextLinkGroups, @@ -768,8 +761,6 @@ export const Canvas: React.FC = ({ onLogQuickAction={onLogQuickAction} onFocusedInvestigation={onFocusedInvestigation} onCharter={onCharter} - onSustainment={onSustainment} - onHandoff={onHandoff} resolvedL3Archetype={resolvedL3Archetype} authoringMode={authoringMode} disabled={disabled} @@ -822,13 +813,10 @@ export const Canvas: React.FC = ({ card={activeStepCard} anchorRect={stepOverlayAnchor} onClose={handleCloseStepOverlay} - signals={signals} onQuickAction={onQuickAction} onLogQuickAction={onLogQuickAction} onFocusedInvestigation={onFocusedInvestigation} onCharter={onCharter} - onSustainment={onSustainment} - onHandoff={onHandoff} investigationOverlay={activeStepInvestigationOverlay} onOpenInvestigationFocus={onOpenInvestigationFocus} onRemoveCausalLink={onRemoveCausalLink} diff --git a/packages/ui/src/components/Canvas/internal/CanvasLevelRouter.tsx b/packages/ui/src/components/Canvas/internal/CanvasLevelRouter.tsx index e9fffd9a9..0806187de 100644 --- a/packages/ui/src/components/Canvas/internal/CanvasLevelRouter.tsx +++ b/packages/ui/src/components/Canvas/internal/CanvasLevelRouter.tsx @@ -81,8 +81,6 @@ export interface CanvasLevelRouterProps { onLogQuickAction?: (stepId: string, payload: LogActionPayload) => void; onFocusedInvestigation?: (stepId: string) => void; onCharter?: (stepId: string) => void; - onSustainment?: (stepId: string) => void; - onHandoff?: (stepId: string) => void; // L3 mode toggle resolvedL3Archetype: CanvasL3Archetype; authoringMode: CanvasAuthoringMode; @@ -131,8 +129,6 @@ export function CanvasLevelRouter({ onLogQuickAction, onFocusedInvestigation, onCharter, - onSustainment, - onHandoff, resolvedL3Archetype, authoringMode, disabled, @@ -209,8 +205,6 @@ export function CanvasLevelRouter({ onLogQuickAction={onLogQuickAction} onFocusedInvestigation={onFocusedInvestigation} onCharter={onCharter} - onSustainment={onSustainment} - onHandoff={onHandoff} /> ) : ( diff --git a/packages/ui/src/components/Canvas/internal/CanvasStepOverlay.tsx b/packages/ui/src/components/Canvas/internal/CanvasStepOverlay.tsx index 521daf910..cb902e8e6 100644 --- a/packages/ui/src/components/Canvas/internal/CanvasStepOverlay.tsx +++ b/packages/ui/src/components/Canvas/internal/CanvasStepOverlay.tsx @@ -1,19 +1,13 @@ import React from 'react'; import FocusTrap from 'focus-trap-react'; import { formatStatistic } from '@variscout/core/i18n'; -import type { WorkflowReadinessSignals } from '@variscout/core'; import type { ActionItem } from '@variscout/core/findings'; import type { CanvasInvestigationFocus, CanvasStepCardModel, CanvasStepInvestigationOverlay, } from '@variscout/hooks'; -import { useTranslation } from '@variscout/hooks'; -import { - computeCtaState, - type ResponsePathKind, - type PrerequisiteLockedReason, -} from './responsePathCta'; +import { computeCtaState, type ResponsePathKind } from './responsePathCta'; import { ContextBadgesRow, type ContextLinkGroup, type ContextLinkItem } from '../../CrossSurface'; import { LogActionModal, RecentActivityPanel, type LogActionPayload } from '../../QuickAction'; @@ -30,13 +24,10 @@ interface CanvasStepOverlayProps { card: CanvasStepCardModel; anchorRect?: CanvasOverlayAnchorRect | null; onClose: () => void; - signals: WorkflowReadinessSignals; onQuickAction?: (stepId: string) => void; onLogQuickAction?: (stepId: string, payload: LogActionPayload) => void; onFocusedInvestigation?: (stepId: string) => void; onCharter?: (stepId: string) => void; - onSustainment?: (stepId: string) => void; - onHandoff?: (stepId: string) => void; investigationOverlay?: CanvasStepInvestigationOverlay; onOpenInvestigationFocus?: (focus: CanvasInvestigationFocus) => void; onRemoveCausalLink?: (linkId: string) => void; @@ -104,29 +95,16 @@ const CTA_LABELS: Record = { 'quick-action': 'Quick action', 'focused-investigation': 'Focused investigation', charter: 'Improvement Project', - sustainment: 'Sustainment', - handoff: 'Handoff', -}; - -const PREREQUISITE_TOOLTIP_KEY: Record< - PrerequisiteLockedReason, - keyof import('@variscout/core').MessageCatalog -> = { - 'no-intervention': 'frame.canvasOverlay.cta.sustainment.notReady', - 'no-sustainment-confirmed': 'frame.canvasOverlay.cta.handoff.notReady', }; export const CanvasStepOverlay: React.FC = ({ card, anchorRect, onClose, - signals, onQuickAction, onLogQuickAction, onFocusedInvestigation, onCharter, - onSustainment, - onHandoff, investigationOverlay, onOpenInvestigationFocus, onRemoveCausalLink, @@ -134,7 +112,6 @@ export const CanvasStepOverlay: React.FC = ({ onNavigateContextLink, actionItems = [], }) => { - const { t } = useTranslation(); const [showLogAction, setShowLogAction] = React.useState(false); const touchStartY = React.useRef(null); const mobile = isMobileViewport(); @@ -143,44 +120,25 @@ export const CanvasStepOverlay: React.FC = ({ 'quick-action': onLogQuickAction ? () => setShowLogAction(true) : onQuickAction, 'focused-investigation': onFocusedInvestigation, charter: onCharter, - sustainment: onSustainment, - handoff: onHandoff, }; const renderCta = (path: ResponsePathKind, extraClass?: string): React.ReactNode => { const handler = handlerMap[path]; - const state = computeCtaState({ path, signals, hasHandler: handler !== undefined }); + const state = computeCtaState({ path, hasHandler: handler !== undefined }); const baseClass = 'rounded-md border border-edge bg-surface-secondary px-3 py-2 text-sm font-medium'; const cls = extraClass ? `${baseClass} ${extraClass}` : baseClass; if (state.kind === 'hidden') return null; - if (state.kind === 'active') { - return ( - - ); - } - return ( @@ -380,9 +338,7 @@ export const CanvasStepOverlay: React.FC = ({
{renderCta('quick-action')} {renderCta('focused-investigation')} - {renderCta('charter')} - {renderCta('sustainment')} - {renderCta('handoff', 'sm:col-span-2')} + {renderCta('charter', 'sm:col-span-2')}
diff --git a/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx b/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx index 66f8d8b4c..b08e5a918 100644 --- a/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx +++ b/packages/ui/src/components/Canvas/internal/LocalMechanismView.tsx @@ -38,8 +38,6 @@ export interface LocalMechanismViewProps { onLogQuickAction?: (stepId: string, payload: LogActionPayload) => void; onFocusedInvestigation?: (stepId: string) => void; onCharter?: (stepId: string) => void; - onSustainment?: (stepId: string) => void; - onHandoff?: (stepId: string) => void; } const EMPTY_ROWS: ReadonlyArray = []; @@ -184,8 +182,6 @@ function ColumnMiniChart({ onOpenQuickAction, onFocusedInvestigation, onCharter, - onSustainment, - onHandoff, }: { column: string; kind: string | undefined; @@ -196,8 +192,6 @@ function ColumnMiniChart({ onOpenQuickAction: (column: string) => void; onFocusedInvestigation?: (column: string) => void; onCharter?: (column: string) => void; - onSustainment?: (column: string) => void; - onHandoff?: (column: string) => void; }) { const values = numericValues(rows, column); const categories = distribution(rows, column); @@ -225,7 +219,7 @@ function ColumnMiniChart({ {getMessage(locale, 'canvas.localMechanism.actionButton')} - {onFocusedInvestigation || onCharter || onSustainment || onHandoff ? ( + {onFocusedInvestigation || onCharter ? (
{onFocusedInvestigation ? ( - ) : null} - {onHandoff ? ( - - ) : null}
) : null}