diff --git a/specs/001-result-auto-refresh/checklists/requirements.md b/specs/001-result-auto-refresh/checklists/requirements.md new file mode 100644 index 00000000..4dacf301 --- /dev/null +++ b/specs/001-result-auto-refresh/checklists/requirements.md @@ -0,0 +1,43 @@ +# Specification Quality Checklist: Result View Auto-Refresh on Logical ID Change + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-02-17 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## UI Feature Validation + +- [x] Decision Analysis section completed with primary goal and key decisions +- [x] Screen Progression table covers the happy path (at least 3 steps) +- [x] UI States defined for empty, loading, error, and success conditions +- [x] User decision inputs are identified (what information helps users decide) + +## Notes + +- All items pass validation. Spec is ready for `/speckit.clarify` or `/speckit.plan`. +- UI Feature Validation included because spec contains a "User Interface Flow" section (result views are visual panel/tab components). +- No [NEEDS CLARIFICATION] markers — all aspects have reasonable defaults from the E04 epic architecture and CONSTITUTION.md constraints. diff --git a/specs/001-result-auto-refresh/contracts/auto-refresh-controller.ts b/specs/001-result-auto-refresh/contracts/auto-refresh-controller.ts new file mode 100644 index 00000000..55e2de94 --- /dev/null +++ b/specs/001-result-auto-refresh/contracts/auto-refresh-controller.ts @@ -0,0 +1,116 @@ +/** + * Auto-Refresh Controller contract. + * Feature: 089-result-auto-refresh (E04) + * + * Coordinates auto-refresh lifecycle for result views bound to logical + * result IDs. Subscribes to ResultIdRegistry change events, manages + * per-view state (paused, stale, visible), debounces rapid updates, + * and records provenance via LogService. + */ + +import type { ResultIdChangeEvent, ResultIdRegistry } from '../../services/session-state/src/registry/types'; + +// ─── Auto-Refresh State ────────────────────────────────────────────── + +export type AutoRefreshStatus = 'active' | 'paused' | 'error' | 'unavailable'; + +export interface AutoRefreshState { + readonly resultId: string; + readonly viewId: string; + readonly paused: boolean; + readonly stale: boolean; + readonly visible: boolean; + readonly lastRefreshTimestamp: number | null; + readonly pendingEvent: ResultIdChangeEvent | null; + readonly status: AutoRefreshStatus; + readonly errorMessage: string | null; +} + +// ─── Viewport State ────────────────────────────────────────────────── + +export interface ViewportState { + readonly signals: Record; + readonly capturedAt: number; +} + +// ─── Refresh Callback ──────────────────────────────────────────────── + +/** + * Callback invoked when a view should refresh. + * The implementation loads the new dataset, transforms it, and re-renders + * the chart while preserving the provided viewport state (if any). + */ +export type RefreshCallback = ( + event: ResultIdChangeEvent, + viewportState: ViewportState | null +) => void; + +// ─── Controller Interface ──────────────────────────────────────────── + +export interface AutoRefreshController { + /** + * Register a view for auto-refresh monitoring. + * Subscribes to the ResultIdRegistry for the given resultId. + * Returns a cleanup function to unregister. + */ + register( + viewId: string, + resultId: string, + onRefresh: RefreshCallback + ): () => void; + + /** + * Pause auto-refresh for a specific view. + * Change events are captured but not acted upon. + */ + pause(viewId: string): void; + + /** + * Resume auto-refresh for a specific view. + * If events arrived while paused, triggers an immediate refresh + * with the latest event. + */ + resume(viewId: string): void; + + /** + * Mark a view as visible or hidden. + * Hidden views defer refresh; becoming visible flushes stale state. + */ + setVisible(viewId: string, visible: boolean): void; + + /** + * Get the current auto-refresh state for a specific view. + */ + getState(viewId: string): AutoRefreshState | undefined; + + /** + * Subscribe to state changes for a specific view. + * Returns an unsubscribe function. + */ + onStateChange( + viewId: string, + callback: (state: AutoRefreshState) => void + ): () => void; + + /** + * Dispose the controller, unsubscribing from all registry events + * and cleaning up all view registrations. + */ + dispose(): void; +} + +// ─── Factory ───────────────────────────────────────────────────────── + +export interface AutoRefreshControllerOptions { + /** The ResultIdRegistry to subscribe to. */ + registry: ResultIdRegistry; + /** Debounce interval in milliseconds. Default: 300. */ + debounceMs?: number; +} + +/** + * Create an AutoRefreshController instance. + */ +export type CreateAutoRefreshController = ( + options: AutoRefreshControllerOptions +) => AutoRefreshController; diff --git a/specs/001-result-auto-refresh/contracts/chart-renderer-viewport.ts b/specs/001-result-auto-refresh/contracts/chart-renderer-viewport.ts new file mode 100644 index 00000000..f6ec9de0 --- /dev/null +++ b/specs/001-result-auto-refresh/contracts/chart-renderer-viewport.ts @@ -0,0 +1,46 @@ +/** + * Chart Renderer viewport state extension contract. + * Feature: 089-result-auto-refresh (E04) + * + * Extends the existing ChartRenderer (#085) with viewport state + * capture and restore capabilities needed for auto-refresh. + */ + +import type { ViewportState } from './auto-refresh-controller'; + +// ─── Viewport-Aware Chart Renderer ────────────────────────────────── + +/** + * Extended ChartRenderer ref handle exposing viewport operations. + * Accessed via React.useImperativeHandle on the ChartRenderer component. + */ +export interface ChartRendererHandle { + /** + * Capture the current viewport state (zoom, pan, selections). + * Returns null if no interactive signals are active. + */ + captureViewport(): ViewportState | null; + + /** + * Restore a previously captured viewport state. + * Applies signals to the Vega view and re-runs the dataflow. + * No-op if the view is not yet rendered or if viewportState is null. + */ + restoreViewport(viewportState: ViewportState | null): Promise; +} + +// ─── Interactive Signal Detection ──────────────────────────────────── + +/** + * Well-known Vega signal prefixes that indicate interactive viewport state. + * These are the signals generated by Vega-Lite's selection parameters + * with type "interval" (used for zoom/pan). + */ +export const VIEWPORT_SIGNAL_PREFIXES = [ + 'brush_', // Default interval selection + 'zoom_', // Zoom interaction + 'pan_', // Pan interaction + 'grid_', // Grid selection (bidirectional zoom) + 'x_', // X-axis domain override + 'y_', // Y-axis domain override +] as const; diff --git a/specs/001-result-auto-refresh/contracts/use-auto-refresh.ts b/specs/001-result-auto-refresh/contracts/use-auto-refresh.ts new file mode 100644 index 00000000..4eb98e85 --- /dev/null +++ b/specs/001-result-auto-refresh/contracts/use-auto-refresh.ts @@ -0,0 +1,47 @@ +/** + * React hook contract for consuming auto-refresh state. + * Feature: 089-result-auto-refresh (E04) + * + * Provides a React-friendly interface to the AutoRefreshController + * for use in shared components (ChartPanelWrapper, future editor tabs). + */ + +import type { AutoRefreshState, ViewportState } from './auto-refresh-controller'; + +// ─── Hook Return Type ──────────────────────────────────────────────── + +export interface UseAutoRefreshReturn { + /** Current auto-refresh state for this view. */ + state: AutoRefreshState; + + /** Pause auto-refresh for this view. */ + pause: () => void; + + /** Resume auto-refresh for this view. */ + resume: () => void; + + /** Toggle pause/resume. */ + toggle: () => void; + + /** Whether there is a pending update (paused with pending event, or stale). */ + hasPendingUpdate: boolean; +} + +// ─── Hook Signature ────────────────────────────────────────────────── + +/** + * React hook that connects a result view to the auto-refresh controller. + * + * @param resultId - The logical result ID this view is bound to. + * @param viewId - Unique identifier for this view instance. + * @param onRefresh - Callback invoked when the view should re-render with new data. + * Receives the change event and the preserved viewport state. + * + * Automatically registers/unregisters the view with the controller + * on mount/unmount. Tracks visibility via document/tab focus events. + */ +export type UseAutoRefresh = ( + resultId: string, + viewId: string, + onRefresh: (newPath: string, viewportState: ViewportState | null) => void +) => UseAutoRefreshReturn; diff --git a/specs/001-result-auto-refresh/data-model.md b/specs/001-result-auto-refresh/data-model.md new file mode 100644 index 00000000..2da21956 --- /dev/null +++ b/specs/001-result-auto-refresh/data-model.md @@ -0,0 +1,120 @@ +# Data Model: Result View Auto-Refresh + +**Feature**: 089 (001-result-auto-refresh) | **Date**: 2026-02-17 + +## Entities + +### AutoRefreshState + +Per-view state managed by the auto-refresh controller. + +| Field | Type | Description | +|-------|------|-------------| +| `resultId` | `string` | The logical result ID this view is bound to | +| `viewId` | `string` | Unique identifier for the view instance | +| `paused` | `boolean` | Whether auto-refresh is paused for this view | +| `stale` | `boolean` | Whether data has changed while the view was not visible or paused | +| `visible` | `boolean` | Whether the view is currently visible to the user | +| `lastRefreshTimestamp` | `number \| null` | Epoch timestamp of the last successful refresh | +| `pendingEvent` | `ResultIdChangeEvent \| null` | The most recent unprocessed change event (when paused or not visible) | +| `status` | `'active' \| 'paused' \| 'error' \| 'unavailable'` | Current auto-refresh status | +| `errorMessage` | `string \| null` | Description of the error if status is `'error'` or `'unavailable'` | + +### ViewportState + +Captured viewport state for preservation across refreshes. + +| Field | Type | Description | +|-------|------|-------------| +| `signals` | `Record` | Named Vega signals representing interactive state (domain ranges, selections) | +| `capturedAt` | `number` | Epoch timestamp when the viewport was captured | + +### RefreshEvent (Provenance) + +Provenance record for each refresh cycle, logged via LogService. + +| Field | Type | Description | +|-------|------|-------------| +| `operation` | `'result:refresh'` | Operation type constant | +| `resultId` | `string` | Logical result ID that triggered the refresh | +| `previousPath` | `string \| null` | Previous file path (null if first render) | +| `newPath` | `string` | New file path after the change | +| `previousVersion` | `number \| null` | Previous version number | +| `newVersion` | `number \| null` | New version number | +| `viewportPreserved` | `boolean` | Whether viewport state was successfully preserved | +| `timestamp` | `number` | When the refresh occurred | + +## Relationships + +``` +ResultIdRegistry (existing #087) + │ + │ emits ResultIdChangeEvent + ▼ +AutoRefreshController (new) + │ + │ manages 1..* AutoRefreshState (one per open view) + │ + │ captures/restores ViewportState + │ + │ logs RefreshEvent via LogService + ▼ +Result Views (existing: ChartPanelWrapper, future: Custom Editor) +``` + +## State Transitions + +### AutoRefreshState Status + +``` + ┌──────────┐ + subscribe │ active │◄──── initial state (on view open) + ┌───────────►│ │ + │ └──┬───┬───┘ + │ │ │ + │ user pauses │ │ registry error + │ ▼ ▼ + │ ┌────────┐ ┌─────────────┐ + │ │ paused │ │ unavailable │ + │ └───┬────┘ └──────┬──────┘ + │ │ │ + │ user resumes registry recovers + │ │ │ + │ ▼ │ + │ (flush pending) ────┘ + │ │ + └─────────────┘ +``` + +### Stale Flag Lifecycle + +``` +1. View visible, data changes → immediate refresh (stale stays false) +2. View hidden, data changes → stale = true, no refresh +3. View becomes visible, stale = true → trigger refresh, stale = false +4. View paused, data changes → pendingEvent updated, stale stays as-is +5. View resumed → flush pendingEvent, trigger refresh if needed +``` + +## Existing Types Consumed (No Modifications) + +These types from #087 are consumed as-is: + +- `ResultIdMapping` — resolved mapping from logical ID to file path +- `ResultIdChangeEvent` — change notification with previous/new paths +- `ResultIdChangeCallback` — callback signature for subscriptions +- `ResultIdRegistry` — the registry interface with subscribe/subscribeAll + +These types from #085 are consumed as-is: + +- `DatasetEnvelope` — standard result dataset schema +- `TransformResult` — transformer output (ok/error discriminated union) +- `ChartRendererProps` — React component props + +## Validation Rules + +- `resultId` must be a non-empty string matching an existing registry entry +- `viewId` must be unique across all active auto-refresh states +- `pendingEvent` is only set when `paused = true` or `visible = false` +- `stale` is only `true` when `visible = false` and an event was received +- `errorMessage` is only non-null when `status` is `'error'` or `'unavailable'` diff --git a/specs/001-result-auto-refresh/media/linkedin-planning.md b/specs/001-result-auto-refresh/media/linkedin-planning.md new file mode 100644 index 00000000..59c2b245 --- /dev/null +++ b/specs/001-result-auto-refresh/media/linkedin-planning.md @@ -0,0 +1,9 @@ +Re-running an analysis tool shouldn't mean hunting for the right chart and re-opening it. In Future Debrief, we're planning auto-refresh for result views -- when an analyst re-runs a tool with different parameters, any open chart showing those results updates in place, preserving zoom and pan state. + +The interesting bit: we don't need any new infrastructure for this. The Result ID Registry already emits events when a logical result changes. Result views just need to subscribe and re-render. The hardest part is deciding what "same result" means when tool parameters change -- and that's what we're working through now. + +This is part of the Results Visualization epic, and it's one of those features where getting the design right matters more than the implementation. + +Full planning post: [LINK] + +#FutureDebrief #MaritimeAnalysis #OpenSource diff --git a/specs/001-result-auto-refresh/media/planning-post.md b/specs/001-result-auto-refresh/media/planning-post.md new file mode 100644 index 00000000..343d774e --- /dev/null +++ b/specs/001-result-auto-refresh/media/planning-post.md @@ -0,0 +1,37 @@ +--- +layout: future-post +title: "Planning: Result View Auto-Refresh" +date: 2026-02-17 +track: [momentum] +author: Ian +reading_time: 4 +tags: [tracer-bullet, results-visualization, e04] +excerpt: "Auto-updating result charts when tools re-run, preserving zoom and pan state" +--- + +## What We're Building + +An analyst runs a zone histogram, zooms into a cluster of interest, then adjusts a parameter and re-runs the tool. Today that means closing the chart, re-opening it, and navigating back to the same zoom region. With auto-refresh (#089), the chart updates in place. The zoom level and pan position stay exactly where they were. The analyst's focus stays on the data, not on window management. + +The mechanism is straightforward. The Logical Result ID Registry (#087) already emits change events when a tool re-run produces new output for a known result ID. The auto-refresh controller subscribes to those events, debounces rapid updates (300ms trailing edge), and coordinates with the chart renderer to capture viewport state before re-rendering and restore it after. A `useAutoRefresh` React hook exposes this to the UI layer. No file watchers, no polling -- just the event system we already have. + +## How It Fits + +This is the capstone of Epic E04 (Results Visualization). It sits on top of four features we have already built or are building: the chart renderer (#085) provides the rendering surface and will be extended with viewport capture/restore via Vega's signal API, the results bottom panel (#086) provides the tabbed view host, the result ID registry (#087) provides the change events, and the custom editor provider (#088) will eventually provide a second view host. The controller itself lives in `services/session-state` following our thick-services pattern -- coordination logic in the service layer, a thin React hook in the component layer. + +## Key Decisions + +- **Registry events, not file watchers.** The Result ID Registry already knows when a tool re-run updates a result. Subscribing to its change events gives us immediate, precise notifications without duplicating file system concerns. +- **Vega signal API for viewport preservation.** Before re-rendering, we read Vega view signals (`x_domain`, `y_domain`, selection signals) to capture the analyst's current viewport. After the new spec is embedded, we write them back. This preserves interactive zoom and pan state natively. +- **300ms debounce, per result ID.** Batch tool re-runs can produce several updates in quick succession. A trailing-edge debounce ensures we render only the final state, avoiding flicker. Each result ID has its own debounce timer so one burst doesn't delay an unrelated result. +- **Deferred refresh for background tabs.** If a result view isn't visible (behind another tab), we mark it stale and skip the render. When the tab activates, we check the stale flag and refresh then. No wasted rendering cycles. +- **Pause/resume toggle per view.** Analysts studying a chart in detail can pause auto-refresh. Incoming changes are captured but not applied. On resume, the view jumps to the latest state -- no intermediate versions, just the current data. +- **Controller in session-state, hook in shared components.** The orchestration logic (subscriptions, debounce, pause/stale tracking) belongs in the service layer. The UI layer gets a hook that returns state and handlers. This keeps the logic frontend-agnostic -- it works the same in VS Code webviews and the web shell. + +## What We'd Love Feedback On + +- **Viewport restoration fidelity**: When re-run data changes substantially (different number of bins, shifted axis range), preserving the exact zoom region may show an empty area or clip new data. Should we detect this and offer a "reset view" option, or is preserving the analyst's position always the right default? +- **Stale indicator design**: When a paused view has pending updates, we plan to show a badge on the tab. What about non-obvious staleness -- should background tabs that refresh on activation show a brief visual cue that the data just changed? +- **Debounce interval**: 300ms feels right for typical use, but analysts running parameter sweeps might produce dozens of updates in seconds. Should we expose the debounce interval as a setting, or is a fixed default sufficient? + +> [Join the discussion](https://github.com/debrief/debrief-future/discussions) diff --git a/specs/001-result-auto-refresh/plan.md b/specs/001-result-auto-refresh/plan.md new file mode 100644 index 00000000..16a406c0 --- /dev/null +++ b/specs/001-result-auto-refresh/plan.md @@ -0,0 +1,148 @@ +# Implementation Plan: Result View Auto-Refresh on Logical ID Change + +**Branch**: `001-result-auto-refresh` | **Date**: 2026-02-17 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/001-result-auto-refresh/spec.md` + +## Summary + +Implement automatic refresh of result views (charts, images) when their bound logical result ID's underlying data changes. The auto-refresh controller subscribes to the existing Result ID Registry (#087) change events, debounces rapid updates, defers refresh for non-visible views, and preserves Vega-Lite viewport state (zoom/pan) across re-renders. A pause/resume toggle gives analysts control over when updates occur. + +The controller lives in `services/session-state/src/refresh/` following the thick-services pattern, consumed by `shared/components/` via a `useAutoRefresh` React hook. The existing ChartRenderer (#085) is extended with viewport capture/restore via Vega's signal API. + +## Technical Context + +**Language/Version**: TypeScript 5.x +**Primary Dependencies**: `@debrief/session-state` (ResultIdRegistry, LogService), `@debrief/components` (ChartRenderer, ChartPanelWrapper), `vega-embed` 6.x (viewport signal access), React 18.x +**Storage**: Local filesystem (STAC assets) — no database +**Testing**: Vitest (unit), Playwright (E2E via Storybook) +**Target Platform**: VS Code Extension (1.85+) webview, browser (web-shell) +**Project Type**: Multi-package workspace (services + shared components + VS Code extension) +**Performance Goals**: Refresh within 2 seconds of change detection; single render for burst updates +**Constraints**: Fully offline (no network calls), debounce 300ms trailing edge, zero re-renders for unaffected views +**Scale/Scope**: Typically 1-5 simultaneous result views per session + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +| Article | Requirement | Status | Notes | +|---------|-------------|--------|-------| +| I.1 Offline by default | All core functionality works without network | PASS | Auto-refresh operates on local files only (FR-011) | +| I.3 No silent failures | Operations succeed fully or fail explicitly | PASS | Error states with warning banners (FR-009, FR-010) | +| I.4 Reproducibility | Same inputs → same results | PASS | Refresh displays the deterministic output of tool execution | +| III.1 Provenance always | Every transformation records lineage | PASS | FR-012: refresh events logged via LogService | +| III.4 Data stays local | No telemetry or external calls | PASS | Zero network dependency (SC-004) | +| IV.1 Services never touch UI | Python services return data only | PASS | Controller is TypeScript service layer; UI in React hooks | +| IV.2 Frontends never persist | All data writes through services | PASS | No data writes — read-only refresh of existing results | +| VI.2 Services require unit tests | No service code without tests | PASS | Unit tests for controller, hook, and viewport APIs | +| VII.1 Tests before implementation | Define expected behaviour first | PASS | Contract types define API; tests written against contracts | +| VIII.1 Specs before code | Written specification exists | PASS | spec.md complete | +| IX.1 Minimal dependencies | Prefer standard library | PASS | No new dependencies — uses existing vega-embed signal API | +| XI.1 I18N from the start | User-facing strings externalisable | PASS | Status messages (loading, error, paused) are externalisable strings | + +**Post-design re-check**: All gates still pass. No new dependencies introduced. Controller is a pure TypeScript module with no UI coupling. + +## Project Structure + +### Documentation (this feature) + +```text +specs/001-result-auto-refresh/ +├── spec.md # Feature specification +├── plan.md # This file +├── research.md # Phase 0: technical research decisions +├── data-model.md # Phase 1: entity definitions +├── quickstart.md # Phase 1: developer onboarding +├── contracts/ # Phase 1: TypeScript API contracts +│ ├── auto-refresh-controller.ts +│ ├── chart-renderer-viewport.ts +│ └── use-auto-refresh.ts +├── checklists/ +│ └── requirements.md # Specification quality checklist +├── media/ +│ ├── planning-post.md +│ └── linkedin-planning.md +└── tasks.md # Phase 2 output (via /speckit.tasks) +``` + +### Source Code (repository root) + +```text +services/session-state/ +├── src/ +│ ├── refresh/ # NEW — Auto-refresh coordination +│ │ ├── controller.ts # AutoRefreshController implementation +│ │ ├── types.ts # AutoRefreshState, ViewportState, etc. +│ │ └── index.ts # Public API exports +│ ├── registry/ # EXISTING — Result ID Registry (#087) +│ │ ├── resultIdRegistry.ts +│ │ └── types.ts +│ └── log/ # EXISTING — LogService (provenance) +└── tests/ + └── refresh/ # NEW — Controller unit tests + └── controller.test.ts + +shared/components/ +├── src/ +│ ├── ChartRenderer/ +│ │ ├── ChartRenderer.tsx # MODIFIED — add useImperativeHandle for viewport +│ │ └── ChartRenderer.stories.tsx # MODIFIED — add auto-refresh stories +│ ├── panels/ +│ │ └── ChartPanelWrapper.tsx # MODIFIED — integrate useAutoRefresh hook +│ └── hooks/ +│ └── useAutoRefresh.ts # NEW — React hook wrapping controller +└── tests/ + ├── hooks/ + │ └── useAutoRefresh.test.ts # NEW — Hook unit tests + └── ChartRenderer/ + └── viewport.test.ts # NEW — Viewport capture/restore tests + +apps/vscode/ +└── src/ + └── extension.ts # MODIFIED — wire controller to registry +``` + +**Structure Decision**: Follows the existing multi-package workspace pattern. The auto-refresh controller is a new module in `services/session-state/` (service layer), consumed by `shared/components/` (UI layer) via a React hook. The VS Code extension wires the pieces together at activation. No new packages introduced. + +## Media Components + +| Component | Story Source | Bundle Name | Purpose | +|-----------|--------------|-------------|---------| +| ChartRenderer (auto-refresh) | `shared/components/src/ChartRenderer/ChartRenderer.stories.tsx` | `chart-auto-refresh.js` | Demonstrates live auto-refresh with viewport preservation | + +**Inclusion Criteria Applied**: +- [ ] New visual component +- [x] Significant visual change +- [x] Interactive demo adds narrative value + +**Bundleability Verified**: +- [x] Stories exist in Storybook +- [x] Components render standalone (no app context required) +- [ ] Reasonable bundle size expected (< 500KB) + +**Storybook Link**: `https://debrief.github.io/debrief-future/storybook/?path=/story/chartrenderer--auto-refresh` + +## Storybook E2E Testing + +| Story | Test Coverage | Theme Variants | Interactions | +|-------|--------------|----------------|--------------| +| `ChartRenderer.stories.tsx` (auto-refresh story) | Rendering, refresh animation, viewport preservation | light, dark, vscode | Data update trigger, pause/resume toggle, zoom then refresh | + +**Testing Strategy**: +- [x] Component renders correctly in all theme variants +- [x] Interactive elements respond to user input +- [x] Accessibility attributes present (data-testid, aria-*) +- [x] Screenshots captured for evidence + +**Test File Location**: `shared/components/e2e/ChartAutoRefresh.spec.ts` + +**Theme Variant URLs** (for Storybook): +``` +/iframe.html?id=chartrenderer--auto-refresh&globals=theme:light +/iframe.html?id=chartrenderer--auto-refresh&globals=theme:dark +/iframe.html?id=chartrenderer--auto-refresh&globals=theme:vscode +``` + +## Complexity Tracking + +No constitution violations to justify — all gates pass. diff --git a/specs/001-result-auto-refresh/quickstart.md b/specs/001-result-auto-refresh/quickstart.md new file mode 100644 index 00000000..b4f7f2bc --- /dev/null +++ b/specs/001-result-auto-refresh/quickstart.md @@ -0,0 +1,131 @@ +# Quickstart: Result View Auto-Refresh + +**Feature**: 089 (001-result-auto-refresh) | **Date**: 2026-02-17 + +## What This Feature Does + +When an analyst has a result chart open and re-runs the tool that produced it, the chart automatically updates with the new data — without needing to close and re-open the view. Zoom and pan state are preserved across refreshes. + +## Architecture Overview + +``` +ResultIdRegistry (#087) + │ + │ ResultIdChangeEvent + ▼ +AutoRefreshController (new — services/session-state/src/refresh/) + │ + │ debounce (300ms), per-view stale/pause tracking + │ + ├──► ChartPanelWrapper (shared/components — via useAutoRefresh hook) + │ │ + │ │ captureViewport() → reload data → transformDataset() → restoreViewport() + │ ▼ + │ ChartRenderer (#085) — extended with viewport capture/restore + │ + └──► Custom Editor Tab (future #088 — same hook, same flow) +``` + +## Key Files + +| Location | Purpose | +|----------|---------| +| `services/session-state/src/refresh/controller.ts` | AutoRefreshController — core coordination logic | +| `services/session-state/src/refresh/types.ts` | Type definitions for auto-refresh state | +| `shared/components/src/hooks/useAutoRefresh.ts` | React hook for consuming auto-refresh in views | +| `shared/components/src/ChartRenderer/ChartRenderer.tsx` | Extended with `ChartRendererHandle` for viewport ops | +| `shared/components/src/panels/ChartPanelWrapper.tsx` | Updated to use `useAutoRefresh` hook | +| `apps/vscode/src/extension.ts` | Wiring: creates controller, passes to views | + +## Development Workflow + +### 1. Run the session-state service tests + +```bash +cd services/session-state +pnpm test +``` + +### 2. Run the shared components tests + +```bash +cd shared/components +pnpm test +``` + +### 3. Storybook (visual testing) + +```bash +cd shared/components +pnpm storybook +``` + +Navigate to **ChartRenderer** stories to test auto-refresh behaviour. + +### 4. VS Code Extension (integration testing) + +```bash +cd apps/vscode +npm run compile && code --extensionDevelopmentPath=. +``` + +1. Open a plot with result artifacts +2. Run a tool that produces results +3. Verify the chart auto-refreshes +4. Zoom into the chart, re-run the tool, verify viewport is preserved + +## Integration Points + +### Consuming the Controller (Service Layer) + +```typescript +import { createAutoRefreshController } from '@debrief/session-state/refresh'; +import { createResultIdRegistry } from '@debrief/session-state/registry'; + +const registry = createResultIdRegistry(); +const controller = createAutoRefreshController({ registry, debounceMs: 300 }); + +// Register a view +const unregister = controller.register('view-1', 'histogram-zone-counts', (event, viewport) => { + // Load new data from event.newPath + // Transform with transformDataset() + // Re-render chart, restore viewport +}); + +// Cleanup +unregister(); +controller.dispose(); +``` + +### Consuming in React (Component Layer) + +```typescript +import { useAutoRefresh } from '@debrief/components/hooks/useAutoRefresh'; + +function ResultView({ resultId, viewId }) { + const { state, pause, resume, toggle, hasPendingUpdate } = useAutoRefresh( + resultId, + viewId, + (newPath, viewportState) => { + // Re-fetch data, transform, re-render with viewport restoration + } + ); + + return ( +
+ + {hasPendingUpdate && Update available} + +
+ ); +} +``` + +## Dependencies + +- **#085 Chart Renderer** — Existing. Needs viewport API extension. +- **#086/#095 Results Bottom Panel** — Existing. Needs `useAutoRefresh` integration. +- **#087 Result ID Registry** — Existing. Used as-is. +- **#088 Custom Editor Provider** — Not yet implemented. Editor tab auto-refresh deferred. diff --git a/specs/001-result-auto-refresh/research.md b/specs/001-result-auto-refresh/research.md new file mode 100644 index 00000000..c37c19a9 --- /dev/null +++ b/specs/001-result-auto-refresh/research.md @@ -0,0 +1,110 @@ +# Research: Result View Auto-Refresh on Logical ID Change + +**Feature**: 089 (001-result-auto-refresh) | **Date**: 2026-02-17 + +## Research Questions + +### RQ-1: How should the auto-refresh controller subscribe to Result ID Registry change events? + +**Decision**: Subscribe via `ResultIdRegistry.subscribe(resultId, callback)` for per-view watchers, and `ResultIdRegistry.subscribeAll(callback)` for a global coordinator. + +**Rationale**: The Result ID Registry (#087) already implements a complete pub/sub system with per-ID and global subscriptions (see `services/session-state/src/registry/resultIdRegistry.ts`). The registry emits `ResultIdChangeEvent` objects containing `resultId`, `previousPath`, `newPath`, and version info. This is the exact event shape needed to trigger auto-refresh — no additional file watchers or polling required. + +**Alternatives considered**: +- **File system watchers (fs.watch / vscode.workspace.createFileSystemWatcher)**: Rejected — duplicates the registry's responsibility, and file paths can change while the logical ID stays stable. The registry already abstracts this. +- **Polling on an interval**: Rejected — inefficient and introduces latency. The registry's event-driven model is immediate. + +### RQ-2: How should viewport state be captured and restored across Vega-Lite re-renders? + +**Decision**: Use Vega view's `signal()` API to capture viewport signals before re-render, then restore them after the new spec is embedded. + +**Rationale**: Vega-Lite charts expose their interactive state through Vega signals. For charts with zoom/pan (`selection` parameters), the view object accessible via `vega-embed`'s `Result.view` exposes signals like `x_domain`, `y_domain`, and custom selection signals. These can be read with `view.signal(name)` and written back with `view.signal(name, value)` followed by `view.runAsync()`. + +The current `ChartRenderer` component (`shared/components/src/ChartRenderer/ChartRenderer.tsx`) stores the `Result` object in a local ref and calls `finalize()` on cleanup. We need to extend this to: (1) expose a `getViewportState()` / `restoreViewportState()` API, and (2) delay finalization until after viewport state is captured. + +**Alternatives considered**: +- **Re-creating the spec with pre-applied domain constraints**: Rejected — this would modify the Vega-Lite spec before embedding, losing the ability to detect which signals are user-applied vs data-driven. +- **Wrapping in a container that preserves CSS transforms**: Rejected — Vega-Lite renders on canvas, CSS transforms would only scale the image, not preserve interactive zoom/pan state. + +### RQ-3: What debounce strategy should be used for rapid successive updates? + +**Decision**: Use a 300ms debounce with trailing edge execution, per logical result ID. + +**Rationale**: The existing codebase uses 100ms debounce for map viewport updates (`mapPanel.ts`). For result refresh, a slightly longer window (300ms) is appropriate because: (1) chart re-rendering involves dataset transformation + Vega-Lite compilation, which is more expensive than a viewport message; (2) tool batch re-runs can produce multiple updates within milliseconds; (3) 300ms is below the human perception threshold for "immediate" response. + +Per-ID debouncing ensures that rapid updates to one result don't delay a separate result's refresh. + +**Alternatives considered**: +- **Throttle (leading edge)**: Rejected — would show intermediate states during batch updates, causing visual flicker. +- **No debounce**: Rejected — burst updates from tool re-runs would cause excessive re-renders and potential UI freezes. +- **Configurable interval**: Rejected per spec assumption — 300ms is a reasonable default that doesn't need user configuration. + +### RQ-4: How should visibility-deferred refresh work for background tabs? + +**Decision**: Track a `stale` flag per view. When a change event arrives for a non-visible view, set `stale = true` without re-rendering. When the view becomes visible (tab activated or panel revealed), check the stale flag and trigger a refresh. + +**Rationale**: The existing `ChartPanelWrapper` component uses a tab bar with `activeChartTabId` to control which tab's content is rendered. Only the active tab renders its `TabContent` component. This architecture naturally supports deferred refresh — we just need to track staleness and trigger a refresh on tab activation. + +For VS Code panels, the `onDidChangeViewState` event fires when a panel becomes visible, providing the activation hook. + +**Alternatives considered**: +- **IntersectionObserver**: Rejected — overkill for tab-based visibility; the tab activation callback is simpler and more reliable. +- **Always re-render in background**: Rejected — wastes resources and violates FR-006. + +### RQ-5: How should pause/resume be implemented? + +**Decision**: Add a `paused` boolean and `pendingEvent` reference per view to the auto-refresh controller. When paused, incoming change events are captured but not acted upon. On resume, the latest pending event triggers a single refresh. + +**Rationale**: This follows the simple "flag + latest event" pattern. Only the most recent event matters because it represents the current state of the result file. Storing all intermediate events would be wasteful since only the latest data is displayed. + +The pause/resume toggle is exposed as a button in the tab header area, consistent with VS Code's editor toolbar pattern. + +**Alternatives considered**: +- **Unsubscribe on pause, resubscribe on resume**: Rejected — would miss events during the paused period, requiring a full data reload on resume rather than an incremental refresh. +- **Queue all events**: Rejected — only the latest state matters for display; queuing adds complexity without value. + +### RQ-6: Where does the auto-refresh controller live architecturally? + +**Decision**: The auto-refresh logic lives as a coordination layer in `services/session-state/src/refresh/` (the controller/orchestrator), with UI integration hooks in `shared/components/` (React hook for consuming refresh state). + +**Rationale**: Following the project's "thick services, thin frontends" principle (Constitution Article IV), the refresh coordination logic (subscribing to registry events, debouncing, managing pause/stale state) belongs in the session-state service layer. The React component layer consumes this via a hook (`useAutoRefresh`) that connects the service-layer controller to the component lifecycle. + +This mirrors the existing pattern where `services/session-state/` provides store + subscriptions, and `shared/components/` consumes them via hooks and context. + +**Alternatives considered**: +- **All logic in React components**: Rejected — violates thick services principle; would couple refresh logic to React lifecycle. +- **VS Code extension host only**: Rejected — the web-shell also needs auto-refresh; the logic must be frontend-agnostic. + +### RQ-7: How should provenance be recorded for refresh events (FR-012)? + +**Decision**: Log each refresh event via the existing LogService with a new operation type `result:refresh`, recording the logical result ID, previous/new paths, and timestamp. + +**Rationale**: The project's LogService (`services/session-state/src/log/`) already handles provenance recording for tool operations. Adding a `result:refresh` operation type follows the existing pattern without introducing a new logging mechanism. This satisfies Constitution Article III (provenance always). + +**Alternatives considered**: +- **Separate refresh log file**: Rejected — fragments provenance across multiple stores. +- **No logging**: Rejected — violates FR-012 and Constitution Article III. + +## Dependencies Assessment + +| Dependency | Status | Ready for #089? | +|------------|--------|-----------------| +| #085 Chart Renderer | Implemented | Needs viewport state API extension | +| #086 Results Bottom Panel | Implemented (as #095) | Ready — tab structure in place | +| #087 Result ID Registry | Implemented | Ready — subscribe/subscribeAll API in place | +| #088 Custom Editor Provider | Not yet implemented | Partial blocker — auto-refresh for editor tabs deferred to P2 | + +### Dependency Risk: #088 Not Implemented + +The Custom Editor Provider (#088) is not yet implemented. However, the auto-refresh feature can proceed with full support for the results bottom panel (#086/095), deferring editor tab auto-refresh until #088 is delivered. The architecture is designed to work identically for both view types — the auto-refresh controller binds to logical result IDs, not to specific view hosts. + +## Technology Decisions + +| Concern | Choice | Rationale | +|---------|--------|-----------| +| Change detection | Registry events | Already implemented in #087 | +| Viewport capture | Vega signal API | Native to Vega runtime, no external deps | +| Debounce | 300ms trailing edge | Balances responsiveness with render cost | +| State management | session-state service | Follows thick services pattern | +| UI integration | React hook | Follows existing shared/components pattern | +| Provenance | LogService operation | Follows existing provenance pattern | diff --git a/specs/001-result-auto-refresh/spec.md b/specs/001-result-auto-refresh/spec.md new file mode 100644 index 00000000..9969f275 --- /dev/null +++ b/specs/001-result-auto-refresh/spec.md @@ -0,0 +1,168 @@ +# Feature Specification: Result View Auto-Refresh on Logical ID Change + +**Feature Branch**: `001-result-auto-refresh` +**Created**: 2026-02-17 +**Status**: Draft +**Input**: User description: "Result view auto-refresh on logical ID change — watches logical result IDs, re-renders preserving viewport; absorbs E03 #083" + +## Clarifications + +### Session 2026-02-18 + +- Q: Where does the pause/resume toggle appear in the UI? → A: Small icon button in the tab header, next to the close button (per-tab action). +- Q: When the same logical result ID is open in two views (e.g., bottom panel and editor tab), how do they refresh? → A: Both views refresh independently, each with its own auto-refresh state (pause, stale, visible tracked per view instance). + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Auto-Refresh on Tool Re-Run (Priority: P1) + +An analyst has a result chart open (e.g., a zone histogram) in either the results bottom panel or as an editor tab. They adjust tool parameters and re-run the analysis. The tool overwrites the result dataset file. Because the view is bound to the logical result ID (not the file path), the system detects the change and automatically re-renders the chart with updated data — without the analyst needing to close and re-open the view. + +**Why this priority**: This is the core value proposition of the feature. Without auto-refresh, analysts must manually close and re-open result views after every tool re-run, breaking their analytical flow. + +**Independent Test**: Can be fully tested by opening a result view, modifying the underlying result dataset file, and verifying the view updates automatically. + +**Acceptance Scenarios**: + +1. **Given** a result view is open and bound to a logical result ID, **When** the underlying result file is updated by a tool re-run, **Then** the view re-renders with the new data automatically. +2. **Given** a result view is open in the bottom panel, **When** the logical result ID's mapped file path changes, **Then** the view loads and renders the new file's data. +3. **Given** a result view is open as an editor tab, **When** the underlying result is updated, **Then** the editor tab view also auto-refreshes. + +--- + +### User Story 2 - Viewport Preservation Across Refreshes (Priority: P1) + +An analyst has zoomed into a specific region of a chart (e.g., a particular time range on a speed profile, or a specific cluster of bars on a histogram). When the result updates and the view auto-refreshes, their zoom level, pan position, and any other viewport adjustments are preserved. They do not lose their place in the data. + +**Why this priority**: Viewport preservation is essential to the auto-refresh experience. Without it, every refresh resets the view to the default zoom, forcing analysts to re-navigate to their area of interest — which defeats the purpose of seamless auto-refresh. + +**Independent Test**: Can be fully tested by opening a result view, zooming/panning to a specific region, triggering a data update, and verifying the viewport state is unchanged after the refresh. + +**Acceptance Scenarios**: + +1. **Given** a result chart is zoomed to a specific region, **When** the result data updates and the view auto-refreshes, **Then** the zoom level and visible region remain the same. +2. **Given** a result chart is panned to show a specific data range, **When** the result auto-refreshes, **Then** the pan position is preserved. +3. **Given** a result chart has user-applied viewport adjustments, **When** the underlying data changes significantly (e.g., different number of data points), **Then** the viewport state is preserved to the extent the data allows (zoom level and center point maintained even if some data points fall outside the visible area). + +--- + +### User Story 3 - Multiple Simultaneous Result Views (Priority: P2) + +An analyst has multiple result views open — perhaps a histogram in the bottom panel and a range-bearing plot as an editor tab, each bound to different logical result IDs. When a tool re-run updates one of those results, only the affected view refreshes. The other views remain undisturbed. + +**Why this priority**: Analysts routinely compare multiple results side-by-side. Independent refresh per logical ID ensures that only relevant views update, avoiding unnecessary re-renders and maintaining the analyst's multi-view workspace. + +**Independent Test**: Can be fully tested by opening two result views bound to different logical IDs, updating the data for one, and verifying only the affected view refreshes. + +**Acceptance Scenarios**: + +1. **Given** two result views are open, each bound to a different logical result ID, **When** one result's data is updated, **Then** only the view bound to that logical ID refreshes, and the other view remains unchanged. +2. **Given** multiple tabs in the bottom panel show different results, **When** a result update occurs for one tab, **Then** only that tab's content refreshes; inactive tabs do not re-render until they become visible. + +--- + +### User Story 4 - Pause and Resume Auto-Refresh (Priority: P3) + +An analyst is examining a result view in detail and does not want it to change while they are studying it. They pause auto-refresh for that view. When they are ready, they resume auto-refresh, and the view updates to the latest data if any changes occurred while paused. + +**Why this priority**: Gives analysts control over when views update, preventing unwanted interruptions during detailed examination. Lower priority because the auto-refresh is generally desired, but there are scenarios where stability is preferred. + +**Independent Test**: Can be fully tested by pausing auto-refresh, updating underlying data, verifying the view does not change, then resuming and verifying the view updates to latest data. + +**Acceptance Scenarios**: + +1. **Given** an analyst pauses auto-refresh on a result view, **When** the underlying data changes, **Then** the view does not update. +2. **Given** auto-refresh is paused and data has changed, **When** the analyst resumes auto-refresh, **Then** the view immediately updates to show the latest data. +3. **Given** auto-refresh is paused and no data changes occur, **When** the analyst resumes auto-refresh, **Then** the view remains as-is (no unnecessary re-render). + +--- + +### Edge Cases + +- What happens when a result file is deleted while a view is open? The view should display a clear message indicating the result is no longer available and stop watching for changes. +- What happens when a result file is updated rapidly in quick succession (e.g., a batch re-run)? The system should debounce updates to avoid excessive re-renders, displaying only the final state. +- What happens when the logical result ID registry itself is unavailable or fails? The view should display the last-known data with a warning indicator that auto-refresh is temporarily unavailable. +- What happens when a view is open but the tab/panel is not visible (e.g., behind another tab)? The refresh should be deferred until the view becomes visible to avoid wasting resources. +- What happens when the viewport state cannot be fully preserved (e.g., the data range changed drastically)? The system should preserve what it can (zoom level, center point) and fall back gracefully rather than resetting entirely. +- What happens when the system is offline? Auto-refresh must function fully offline since all result data is local. No network dependency is permitted. +- What happens when the same logical result ID is open in multiple views (e.g., bottom panel and editor tab simultaneously)? Each view refreshes independently with its own auto-refresh state. Pausing one view does not affect the other. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST watch for changes to result files associated with logical result IDs and automatically trigger a re-render of any open views bound to those IDs. +- **FR-002**: System MUST preserve viewport state (zoom level, pan position) across auto-refresh re-renders. +- **FR-003**: System MUST support auto-refresh for result views displayed in both the results bottom panel and custom editor tabs. +- **FR-004**: System MUST refresh only the specific view(s) affected by a data change; views bound to unaffected logical IDs MUST NOT re-render. When multiple views are bound to the same logical result ID, each MUST refresh independently with its own auto-refresh state. +- **FR-005**: System MUST debounce rapid successive updates to avoid excessive re-renders, ensuring only the final state is displayed after a burst of changes. +- **FR-006**: System MUST defer re-renders for views that are not currently visible (e.g., background tabs), refreshing them when they become visible. +- **FR-007**: System MUST provide a pause/resume mechanism that allows users to temporarily disable auto-refresh for individual views. +- **FR-008**: When auto-refresh is resumed after being paused, the system MUST immediately update the view to the latest available data if changes occurred during the pause. +- **FR-009**: System MUST display a clear message when a result file is deleted or becomes unavailable while a view is open. +- **FR-010**: System MUST display a warning indicator when auto-refresh is temporarily unavailable (e.g., registry failure) while still showing the last-known data. +- **FR-011**: System MUST function fully offline — auto-refresh operates on local result files with no network dependency. +- **FR-012**: System MUST record provenance information for each refresh event (what changed, when, which logical ID). + +### Key Entities + +- **Logical Result ID**: A stable, human-readable identifier (e.g., `histogram-zone-counts`) that maps to a current result file path. Owned by the Logical Result ID Registry (#087). Views bind to these IDs rather than to file paths. +- **Result View**: An instance of a rendered result, displayed in either the results bottom panel or as a custom editor tab. Each view is bound to exactly one logical result ID. +- **Viewport State**: The current zoom level, pan position, and any other user-applied view adjustments for a given result view. Preserved across auto-refresh cycles. +- **Change Event**: A notification emitted by the Logical Result ID Registry when the file path mapped to a logical result ID changes or the mapped file's content is updated. +- **Refresh Cycle**: A single auto-refresh operation: detecting a change event, loading the updated data, transforming it, and re-rendering the view while preserving viewport state. + +## User Interface Flow *(optional - include for UI features)* + +### Decision Analysis + +- **Primary Goal**: Keep result views up-to-date with the latest tool output without manual intervention, while maintaining the analyst's current view context. +- **Key Decision(s)**: + 1. Whether to pause auto-refresh when studying a result in detail + 2. Whether to manually trigger a refresh if auto-refresh is paused +- **Decision Inputs**: A small icon button in each tab header (next to the close button) shows auto-refresh status (active/paused) and toggles pause/resume. When paused with pending updates, a badge on the tab indicates new data is available. The per-tab placement ensures the control is co-located with the view it affects. + +### Screen Progression + +| Step | Screen/State | User Action | Result | +|------|---------------------------|---------------------------------------|--------------------------------------------------------------| +| 1 | Result view open, auto-refresh active | Analyst re-runs a tool | Result data updates, view auto-refreshes with preserved viewport | +| 2 | Auto-refresh active, indicator visible | Analyst clicks pause indicator | Auto-refresh pauses; indicator changes to show paused state | +| 3 | Auto-refresh paused, data changed | Analyst sees "pending update" badge | View remains stable; badge indicates new data is available | +| 4 | Auto-refresh paused, pending update | Analyst clicks resume/refresh | View updates to latest data with viewport preserved | +| 5 | Result file deleted | System detects file removal | View shows "result no longer available" message | + +### UI States + +- **Empty State**: Not applicable — auto-refresh only activates on views that already display a result. If the underlying result is removed, the view transitions to the error state. +- **Loading State**: A subtle refresh indicator (e.g., brief shimmer or spinner overlay) appears while the updated data is being loaded and re-rendered. The previous chart remains visible underneath to avoid blank flashes. +- **Error State**: If the result file is deleted or the registry is unavailable, the view shows the last-known chart with a warning banner explaining the situation (e.g., "Result no longer available" or "Auto-refresh temporarily unavailable"). +- **Success State**: The chart displays the latest data. The auto-refresh indicator shows an active/healthy status. No additional confirmation is needed — seamless updates are the goal. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: When a result file is updated, the corresponding open view refreshes within 2 seconds of the change being detected. +- **SC-002**: Viewport state (zoom level, pan position) is preserved in 100% of auto-refresh cycles where the data range has not fundamentally changed. +- **SC-003**: Rapid successive updates (5+ changes within 1 second) result in only a single re-render displaying the final state. +- **SC-004**: Auto-refresh works with zero network calls — fully operational offline. +- **SC-005**: Views bound to unaffected logical IDs experience zero re-renders when a different result updates. +- **SC-006**: Analysts can pause and resume auto-refresh per view, with the view updating to the latest data immediately upon resume. +- **SC-007**: Background (non-visible) views consume no rendering resources until they become visible. + +## Assumptions + +- The Logical Result ID Registry (#087) is implemented and emits change events when mapped file paths or file contents change. +- The Results Bottom Panel (#086) and Custom Editor Provider (#088) are implemented and provide views that can accept re-render requests. +- The Dataset-to-Spec Transformer (#085) supports re-transformation of updated datasets without side effects. +- Viewport state capture and restoration is supported by the chart rendering component (#085). +- Result files are stored locally as STAC assets, and file system change detection is available on all supported platforms. +- Debounce interval for rapid updates is a reasonable default (e.g., 300-500ms) and does not need to be user-configurable. + +## Dependencies + +- **#085** — Chart Renderer: must support viewport state capture/restore and re-rendering with new data. +- **#086** — Results Bottom Panel: provides the tabbed panel views that this feature refreshes. +- **#087** — Logical Result ID Registry: provides the change events that trigger auto-refresh. +- **#088** — Custom Editor Provider: provides the editor tab views that this feature refreshes. diff --git a/specs/001-result-auto-refresh/tasks.md b/specs/001-result-auto-refresh/tasks.md new file mode 100644 index 00000000..3f78f479 --- /dev/null +++ b/specs/001-result-auto-refresh/tasks.md @@ -0,0 +1,302 @@ +# Tasks: Result View Auto-Refresh on Logical ID Change + +**Input**: Design documents from `/specs/001-result-auto-refresh/` +**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/ + +**Tests**: Tests are included — Constitution Article VI.2 requires unit tests for all service code. + +**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. + +--- + +## Evidence Requirements + +> **Purpose**: Capture artifacts that demonstrate the feature works as expected. These are used in PR descriptions, documentation, and future blog posts. + +**Evidence Directory**: `specs/001-result-auto-refresh/evidence/` +**Media Directory**: `specs/001-result-auto-refresh/media/` + +### Planned Artifacts + +| Artifact | Description | Captured When | +|----------|-------------|---------------| +| test-summary.md | Vitest results for controller, hook, and viewport tests | After all tests pass | +| usage-example.md | Code example showing controller + hook integration | After core implementation complete | +| screenshots/auto-refresh-active.png | Chart panel with active auto-refresh indicator | After UI integration | +| screenshots/auto-refresh-paused.png | Chart panel with paused indicator and pending badge | After pause/resume works | + +### Media Content + +| Artifact | Description | Created When | +|----------|-------------|--------------| +| media/planning-post.md | Blog post announcing the feature | During /speckit.plan (done) | +| media/linkedin-planning.md | LinkedIn summary for planning | During /speckit.plan (done) | +| media/shipped-post.md | Blog post celebrating completion | During Polish phase | +| media/linkedin-shipped.md | LinkedIn summary for shipped | During Polish phase | + +### PR Creation + +| Action | Description | Created When | +|--------|-------------|--------------| +| Feature PR | PR in debrief-future with evidence | Final task in Polish phase | +| Blog PR | PR in debrief.github.io with post | Triggered by /speckit.pr | + +--- + +## Phase 1: Setup + +**Purpose**: Create the module structure and type definitions for the auto-refresh feature. + +- [ ] T001 Create auto-refresh types module `services/session-state/src/refresh/types.ts` +- [ ] T002 [P] Create auto-refresh module index with public exports `services/session-state/src/refresh/index.ts` +- [ ] T003 [P] Create useAutoRefresh hook file `shared/components/src/hooks/useAutoRefresh.ts` + +--- + +## Phase 2: Foundation (Blocking Prerequisites) + +**Purpose**: Core auto-refresh controller and ChartRenderer viewport extension that ALL user stories depend on. + +**CRITICAL**: No user story work can begin until this phase is complete. + +### Tests for Foundation + +> **NOTE: Write these tests FIRST, ensure they FAIL before implementation** + +- [ ] T004 [test] Write controller unit tests: register, dispose, event forwarding `services/session-state/tests/refresh/controller.test.ts` +- [ ] T005 [P][test] Write viewport capture/restore unit tests `shared/components/tests/ChartRenderer/viewport.test.ts` + +### Implementation for Foundation + +- [ ] T006 Implement AutoRefreshController: register/unregister, event subscription, state management `services/session-state/src/refresh/controller.ts` +- [ ] T007 Extend ChartRenderer with useImperativeHandle exposing ChartRendererHandle (captureViewport, restoreViewport) `shared/components/src/ChartRenderer/ChartRenderer.tsx` +- [ ] T008 Implement useAutoRefresh React hook: register on mount, unregister on unmount, expose state/pause/resume/toggle `shared/components/src/hooks/useAutoRefresh.ts` + +**Checkpoint**: Controller can register views, subscribe to registry events, and forward change events. ChartRenderer exposes viewport capture/restore. Hook bridges controller to React lifecycle. + +--- + +## Phase 3: User Story 1 - Auto-Refresh on Tool Re-Run (Priority: P1) + +**Goal**: When an analyst has a result chart open and re-runs the tool, the chart auto-refreshes with new data. + +**Independent Test**: Open a result view, modify the underlying dataset file, verify the view updates automatically. + +### Tests for User Story 1 + +- [ ] T009 [test] Write controller test: change event triggers onRefresh callback `services/session-state/tests/refresh/controller.test.ts` +- [ ] T010 [P][test] Write controller test: file path change triggers refresh with new path `services/session-state/tests/refresh/controller.test.ts` + +### Implementation for User Story 1 + +- [ ] T011 Implement controller event handling: subscribe to registry per resultId, invoke onRefresh callback on change `services/session-state/src/refresh/controller.ts` +- [ ] T012 Integrate useAutoRefresh hook into ChartPanelWrapper: bind active tab to auto-refresh, reload data on refresh callback `shared/components/src/panels/ChartPanelWrapper.tsx` +- [ ] T013 Wire AutoRefreshController in VS Code extension: create controller, pass registry, connect to webview panel `apps/vscode/src/extension.ts` +- [ ] T014 Add provenance logging for refresh events via LogService (FR-012) `services/session-state/src/refresh/controller.ts` + +**Checkpoint**: Result views auto-refresh when registry emits a change event. Provenance is recorded. + +--- + +## Phase 4: User Story 2 - Viewport Preservation Across Refreshes (Priority: P1) + +**Goal**: Zoom and pan state are preserved when a chart auto-refreshes with new data. + +**Independent Test**: Open a result view, zoom/pan to a region, trigger a data update, verify viewport is unchanged. + +### Tests for User Story 2 + +- [ ] T015 [test] Write viewport test: signals captured before re-render, restored after `shared/components/tests/ChartRenderer/viewport.test.ts` +- [ ] T016 [P][test] Write controller test: refresh callback receives viewport state from captureViewport `services/session-state/tests/refresh/controller.test.ts` + +### Implementation for User Story 2 + +- [ ] T017 Implement captureViewport: read Vega view signals matching VIEWPORT_SIGNAL_PREFIXES `shared/components/src/ChartRenderer/ChartRenderer.tsx` +- [ ] T018 Implement restoreViewport: write signals back to Vega view and run dataflow `shared/components/src/ChartRenderer/ChartRenderer.tsx` +- [ ] T019 Update ChartPanelWrapper refresh flow: capture viewport before data reload, restore after re-render `shared/components/src/panels/ChartPanelWrapper.tsx` + +**Checkpoint**: Zooming into a chart, triggering an update, and verifying the zoom level is preserved. + +--- + +## Phase 5: User Story 3 - Multiple Simultaneous Result Views (Priority: P2) + +**Goal**: Only the view(s) bound to the changed logical ID refresh; other views stay undisturbed. + +**Independent Test**: Open two result views with different logical IDs, update one, verify only the affected view refreshes. + +### Tests for User Story 3 + +- [ ] T020 [test] Write controller test: two views registered to different IDs, change event for one ID triggers only that view's callback `services/session-state/tests/refresh/controller.test.ts` +- [ ] T021 [P][test] Write controller test: two views bound to same ID both receive refresh independently `services/session-state/tests/refresh/controller.test.ts` + +### Implementation for User Story 3 + +- [ ] T022 Implement per-resultId subscription in controller: each register() creates an independent subscription, multiple views per ID supported `services/session-state/src/refresh/controller.ts` +- [ ] T023 Implement debouncing per logical result ID (300ms trailing edge) in controller (FR-005) `services/session-state/src/refresh/controller.ts` +- [ ] T024 Implement visibility-deferred refresh: setVisible(false) sets stale flag, setVisible(true) flushes stale (FR-006) `services/session-state/src/refresh/controller.ts` +- [ ] T025 Connect tab activation to setVisible in ChartPanelWrapper: active tab = visible, inactive tabs = not visible `shared/components/src/panels/ChartPanelWrapper.tsx` + +**Checkpoint**: Multiple result views work independently. Debouncing prevents burst re-renders. Background tabs defer refresh. + +--- + +## Phase 6: User Story 4 - Pause and Resume Auto-Refresh (Priority: P3) + +**Goal**: Analysts can pause auto-refresh per view and resume to get latest data. + +**Independent Test**: Pause auto-refresh, update data, verify no change, resume, verify view updates. + +### Tests for User Story 4 + +- [ ] T026 [test] Write controller test: pause() suppresses refresh, pending event captured `services/session-state/tests/refresh/controller.test.ts` +- [ ] T027 [P][test] Write controller test: resume() flushes pending event, triggers refresh `services/session-state/tests/refresh/controller.test.ts` +- [ ] T028 [P][test] Write hook test: toggle() switches between paused/active, hasPendingUpdate reflects state `shared/components/tests/hooks/useAutoRefresh.test.ts` + +### Implementation for User Story 4 + +- [ ] T029 Implement pause/resume in controller: pause stores pendingEvent, resume flushes it (FR-007, FR-008) `services/session-state/src/refresh/controller.ts` +- [ ] T030 Implement onStateChange subscription in controller for UI reactivity `services/session-state/src/refresh/controller.ts` +- [ ] T031 Add pause/resume icon button to ChartPanelWrapper tab header (next to close button) `shared/components/src/panels/ChartPanelWrapper.tsx` +- [ ] T032 Add pending update badge to tab header when paused with pending event `shared/components/src/panels/ChartPanelWrapper.tsx` +- [ ] T033 Handle error/unavailable states: display warning banner with last-known data (FR-009, FR-010) `shared/components/src/panels/ChartPanelWrapper.tsx` + +**Checkpoint**: Pause/resume toggle visible in tab header. Pending badge appears when paused with updates. Error states show warning banners. + +--- + +## Phase 7: Storybook & E2E + +**Purpose**: Add Storybook stories for the auto-refresh feature and E2E tests. + +> **PLAYWRIGHT WORKS IN CLOUD SESSIONS** — Do NOT skip these tests. The project uses `@sparticuz/chromium` (bundled Linux Chromium via npm). Run `node apps/web-shell/run-playwright.mjs` to extract and configure. Full details: `docs/project_notes/playwright-installation-research.md` + +### Storybook Stories + +- [ ] T034 Add auto-refresh Storybook story: simulates data updates with viewport preservation `shared/components/src/ChartRenderer/ChartRenderer.stories.tsx` +- [ ] T035 [P] Add pause/resume Storybook story: demonstrates toggle and pending badge `shared/components/src/ChartRenderer/ChartRenderer.stories.tsx` + +### E2E Tests + +- [ ] T036 Create Playwright test for auto-refresh rendering across theme variants `shared/components/e2e/ChartAutoRefresh.spec.ts` +- [ ] T037 [P] Add interaction tests: data update trigger, pause/resume toggle, zoom-then-refresh `shared/components/e2e/ChartAutoRefresh.spec.ts` +- [ ] T038 Run e2e tests: `pnpm --filter @debrief/components test:e2e ChartAutoRefresh` + +**Checkpoint**: Storybook stories demonstrate auto-refresh behaviour. E2E tests capture screenshots across themes. + +--- + +## Phase 8: Polish & Cross-Cutting Concerns + +**Purpose**: Evidence collection, media content, and PR creation. + +### Evidence Collection + +- [ ] T039 Create evidence directory `specs/001-result-auto-refresh/evidence/` +- [ ] T040 Capture test summary with pass/fail counts `specs/001-result-auto-refresh/evidence/test-summary.md` +- [ ] T041 [P] Create usage example showing controller + hook integration `specs/001-result-auto-refresh/evidence/usage-example.md` +- [ ] T042 [P] Capture screenshots of auto-refresh states `specs/001-result-auto-refresh/evidence/screenshots/` + +### E2E Evidence Collection + +- [ ] T043 Run full e2e suite: `pnpm --filter @debrief/components test:e2e` +- [ ] T044 [P] Capture theme variant screenshots `specs/001-result-auto-refresh/evidence/screenshots/` +- [ ] T045 Document e2e results `specs/001-result-auto-refresh/evidence/e2e-summary.md` + +### Media Content + +- [ ] T046 Create shipped blog post `specs/001-result-auto-refresh/media/shipped-post.md` +- [ ] T047 [P] Create LinkedIn shipped summary `specs/001-result-auto-refresh/media/linkedin-shipped.md` + +### PR Creation + +- [ ] T048 Create PR and publish blog: run /speckit.pr + +**Task T048 must run last. It depends on all evidence and media tasks being complete.** + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies — can start immediately +- **Foundation (Phase 2)**: Depends on Phase 1 — BLOCKS all user stories +- **User Story 1 (Phase 3)**: Depends on Phase 2 — core auto-refresh +- **User Story 2 (Phase 4)**: Depends on Phase 2 — viewport preservation (can run in parallel with Phase 3) +- **User Story 3 (Phase 5)**: Depends on Phase 3 — multi-view + debounce builds on basic refresh +- **User Story 4 (Phase 6)**: Depends on Phase 2 — pause/resume (can run in parallel with Phases 3-5) +- **Storybook & E2E (Phase 7)**: Depends on Phases 3-6 complete +- **Polish (Phase 8)**: Depends on all phases complete + +### User Story Dependencies + +- **User Story 1 (P1)**: Can start after Foundation — no dependencies on other stories +- **User Story 2 (P1)**: Can start after Foundation — no dependencies on other stories (parallel with US1) +- **User Story 3 (P2)**: Depends on US1 completion — builds on basic refresh with multi-view isolation and debouncing +- **User Story 4 (P3)**: Can start after Foundation — no dependencies on other stories (parallel with US1/US2) + +### Within Each User Story + +- Tests MUST be written and FAIL before implementation +- Types and contracts before service logic +- Service logic before UI integration +- Core implementation before edge case handling + +### Parallel Opportunities + +- Phase 1: All setup tasks marked [P] can run in parallel +- Phase 2: T004 and T005 (tests) can run in parallel +- Phase 3 & Phase 4: User Stories 1 and 2 can run in parallel (both depend only on Foundation) +- Phase 6: Can run in parallel with Phases 3-5 (depends only on Foundation) +- Phase 8: Evidence tasks marked [P] can run in parallel + +--- + +## Parallel Example: Phases 3 & 4 + +```bash +# US1 (auto-refresh) and US2 (viewport) can run in parallel: +# Stream A: Phase 3 tasks T009 → T014 +# Stream B: Phase 4 tasks T015 → T019 + +# Within Phase 5, tests can run in parallel: +Task T020: "controller test: two views, different IDs" +Task T021: "controller test: two views, same ID" +``` + +--- + +## Implementation Strategy + +### Incremental Delivery + +1. Complete Setup + Foundation → Controller shell, viewport API, hook shell +2. Add User Story 1 → Basic auto-refresh works end-to-end +3. Add User Story 2 → Viewport preserved across refreshes +4. Add User Story 3 → Multi-view isolation + debouncing +5. Add User Story 4 → Pause/resume UI toggle +6. Storybook + E2E → Visual testing and evidence +7. Polish → Evidence, media, PR + +### Single Developer Strategy + +1. Phase 1 + Phase 2 (Setup + Foundation) +2. Phase 3 (US1 — core auto-refresh) +3. Phase 4 (US2 — viewport preservation) +4. Phase 5 (US3 — multi-view + debounce) +5. Phase 6 (US4 — pause/resume) +6. Phase 7 (Storybook + E2E) +7. Phase 8 (Polish + PR) + +--- + +## Notes + +- [P] tasks = different files, no dependencies +- [test] = test task, write before implementation +- Each user story is independently testable at its checkpoint +- Commit after each task or logical group +- Stop at any checkpoint to validate story independently +- **Evidence is required** — capture artifacts that prove the feature works +- Run `/speckit.pr` after all tasks complete to create PR with evidence