Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions specs/001-result-auto-refresh/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -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.
116 changes: 116 additions & 0 deletions specs/001-result-auto-refresh/contracts/auto-refresh-controller.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
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;
46 changes: 46 additions & 0 deletions specs/001-result-auto-refresh/contracts/chart-renderer-viewport.ts
Original file line number Diff line number Diff line change
@@ -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<void>;
}

// ─── 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;
47 changes: 47 additions & 0 deletions specs/001-result-auto-refresh/contracts/use-auto-refresh.ts
Original file line number Diff line number Diff line change
@@ -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;
120 changes: 120 additions & 0 deletions specs/001-result-auto-refresh/data-model.md
Original file line number Diff line number Diff line change
@@ -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<string, unknown>` | 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'`
9 changes: 9 additions & 0 deletions specs/001-result-auto-refresh/media/linkedin-planning.md
Original file line number Diff line number Diff line change
@@ -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
Loading