From c99b21720a2a363e32d8bb0cfa71ae0f567ecf8d Mon Sep 17 00:00:00 2001 From: Gabor Szabo Date: Fri, 12 Jun 2026 14:43:43 +0200 Subject: [PATCH 1/4] feat(api): extend demo seed profiles to all scenario presets (#391) --- app/features/demo/pipeline.py | 57 +++++++++++-- app/features/demo/tests/test_pipeline.py | 102 +++++++++++++++++++++-- 2 files changed, 144 insertions(+), 15 deletions(-) diff --git a/app/features/demo/pipeline.py b/app/features/demo/pipeline.py index 9af07a3f..66caa76b 100644 --- a/app/features/demo/pipeline.py +++ b/app/features/demo/pipeline.py @@ -32,7 +32,7 @@ from dataclasses import dataclass, field from datetime import UTC, date, datetime, timedelta from pathlib import Path -from typing import Any +from typing import Any, NamedTuple import httpx from fastapi import FastAPI @@ -476,12 +476,47 @@ async def step_reset(ctx: DemoContext, client: _Client) -> StepResult: ) -_SCENARIO_SEED_PROFILE: dict[ScenarioPreset, tuple[int, int, int]] = { - ScenarioPreset.DEMO_MINIMAL: (DEMO_SEED_STORES, DEMO_SEED_PRODUCTS, DEMO_SEED_SPAN_DAYS), +class _SeedProfile(NamedTuple): + """Demo-scaled seed profile for one scenario preset. + + The /seeder/generate request overrides preset dims/window by design + (app/features/seeder/service.py:_build_config_from_params) while + preserving the preset's behavioral character (noise, promos, stockouts, + sparsity, launch ramps). ``window`` pins a fixed calendar range + (holiday_rush); when None the window runs ``span_days`` back from today. + """ + + stores: int + products: int + span_days: int + window: tuple[date, date] | None = None + + +_SCENARIO_SEED_PROFILE: dict[ScenarioPreset, _SeedProfile] = { + ScenarioPreset.DEMO_MINIMAL: _SeedProfile( + DEMO_SEED_STORES, DEMO_SEED_PRODUCTS, DEMO_SEED_SPAN_DAYS + ), # PRP-38 — SHOWCASE_RICH profile mirrors app/shared/seeder/config.py:from_scenario. - ScenarioPreset.SHOWCASE_RICH: (5, 15, 180), + ScenarioPreset.SHOWCASE_RICH: _SeedProfile(5, 15, 180), # PRP-38 — SPARSE picker option exercises the data-shape edge case. - ScenarioPreset.SPARSE: (DEMO_SEED_STORES, DEMO_SEED_PRODUCTS, DEMO_SEED_SPAN_DAYS), + ScenarioPreset.SPARSE: _SeedProfile(DEMO_SEED_STORES, DEMO_SEED_PRODUCTS, DEMO_SEED_SPAN_DAYS), + # E2 (#391) — demo-scaled profiles for the remaining presets; the preset's + # character comes from SeederConfig.from_scenario, dims/window from this + # request (precedence contract: app/features/seeder/service.py). All + # windows stay >= 75 days so a later showcase_rich run with + # skip_seed=true clears the historical_backfill gate. + ScenarioPreset.RETAIL_STANDARD: _SeedProfile(5, 15, 180), + ScenarioPreset.HIGH_VARIANCE: _SeedProfile(5, 15, 180), + ScenarioPreset.STOCKOUT_HEAVY: _SeedProfile(5, 15, 180), + # Extra products for launch variety (the native preset seeds 100). + ScenarioPreset.NEW_LAUNCHES: _SeedProfile(5, 25, 180), + # Calendar-pinned: the preset's HolidayConfig spikes are fixed 2024 dates + # (app/shared/seeder/config.py:from_scenario) — a today-anchored window + # would never contain them. span_days is dead data when window is set + # (the pinned range is 92 days inclusive, delta 91). + ScenarioPreset.HOLIDAY_RUSH: _SeedProfile( + 5, 15, 91, window=(date(2024, 10, 1), date(2024, 12, 31)) + ), } @@ -489,12 +524,16 @@ async def step_seed(ctx: DemoContext, client: _Client) -> StepResult: """Seed the active scenario (skipped when ``skip_seed`` is set).""" if ctx.skip_seed: return ("skip", "skip_seed=true (assuming a seeded database)", {}) - stores, products, span_days = _SCENARIO_SEED_PROFILE.get( + profile = _SCENARIO_SEED_PROFILE.get( ctx.scenario, - (DEMO_SEED_STORES, DEMO_SEED_PRODUCTS, DEMO_SEED_SPAN_DAYS), + _SeedProfile(DEMO_SEED_STORES, DEMO_SEED_PRODUCTS, DEMO_SEED_SPAN_DAYS), ) - seed_end = datetime.now(UTC).date() - seed_start = seed_end - timedelta(days=span_days) + stores, products = profile.stores, profile.products + if profile.window is not None: + seed_start, seed_end = profile.window + else: + seed_end = datetime.now(UTC).date() + seed_start = seed_end - timedelta(days=profile.span_days) body = await client.request( "seed", "POST", diff --git a/app/features/demo/tests/test_pipeline.py b/app/features/demo/tests/test_pipeline.py index b9b37f07..cfd692d0 100644 --- a/app/features/demo/tests/test_pipeline.py +++ b/app/features/demo/tests/test_pipeline.py @@ -8,9 +8,11 @@ from __future__ import annotations +from datetime import date, timedelta from types import SimpleNamespace from typing import Any, cast +import pytest from fastapi import FastAPI from app.features.demo import pipeline @@ -675,11 +677,26 @@ def test_phase_table_showcase_rich_emits_24_steps_with_agents_hitl_and_ops_snaps ] -def test_phase_table_sparse_matches_demo_minimal_shape(): - """PRP-38 — SPARSE is offered in the picker but does not extend the pipeline.""" - sparse_rows = pipeline._phase_table(ScenarioPreset.SPARSE) +@pytest.mark.parametrize( + "preset", + [ + ScenarioPreset.RETAIL_STANDARD, + ScenarioPreset.HOLIDAY_RUSH, + ScenarioPreset.HIGH_VARIANCE, + ScenarioPreset.STOCKOUT_HEAVY, + ScenarioPreset.NEW_LAUNCHES, + ScenarioPreset.SPARSE, + ], +) +def test_phase_table_non_showcase_presets_match_demo_minimal_shape(preset: ScenarioPreset): + """PRP-38 / E2 (#391) — only SHOWCASE_RICH extends the pipeline. + + Every other preset (incl. the 5 newly exposed by E2) runs the legacy + 11-step flow; the picker offers them as data-shape variations only. + """ + rows = pipeline._phase_table(preset) minimal_rows = pipeline._phase_table(ScenarioPreset.DEMO_MINIMAL) - assert [(p, s) for p, s, _ in sparse_rows] == [(p, s) for p, s, _ in minimal_rows] + assert [(p, s) for p, s, _ in rows] == [(p, s) for p, s, _ in minimal_rows] def test_legacy_step_table_adapter_returns_11_pairs(): @@ -1001,8 +1018,6 @@ def test_parse_artifact_key_v2_artifacts_models_path(): def test_parse_artifact_key_rejects_unparseable(): """PRP-40 — a malformed artifact_uri raises ValueError (not a silent miss).""" - import pytest - with pytest.raises(ValueError, match="Cannot parse artifact-key"): pipeline._parse_artifact_key("not-a-model-uri.bin") @@ -2259,3 +2274,78 @@ async def request( assert final.event_type == "pipeline_complete" assert final.status == "fail" assert final.data["workspace_id"] == "ws-e1-test" + + +# ============================================================================= +# E2 (#391) — per-preset demo seed profiles +# ============================================================================= + + +def test_scenario_seed_profile_covers_every_preset(): + """E2 (#391) — every ScenarioPreset member has an explicit seed profile. + + The ``.get`` fallback in step_seed stays (a future 9th member must not + crash), but no CURRENT member may silently fall back to demo_minimal — + the picker cards promise per-preset seed shapes. + """ + assert set(pipeline._SCENARIO_SEED_PROFILE) == set(ScenarioPreset) + + +def test_scenario_seed_profile_windows_clear_backfill_gate(): + """E2 (#391) — every profile window is >= 75 days so a later + showcase_rich run with skip_seed=true clears the historical_backfill gate.""" + for preset, profile in pipeline._SCENARIO_SEED_PROFILE.items(): + if profile.window is not None: + span = (profile.window[1] - profile.window[0]).days + else: + span = profile.span_days + assert span >= 75, f"{preset.value} window spans only {span} days" + + +async def test_step_seed_holiday_rush_posts_pinned_window(): + """E2 (#391) — holiday_rush MUST seed the calendar-pinned 2024 window. + + The preset's HolidayConfig spikes are fixed 2024 dates; a today-anchored + window would never contain them and the preset silently degrades. + """ + ctx = pipeline.DemoContext( + seed=42, skip_seed=False, reset=False, scenario=ScenarioPreset.HOLIDAY_RUSH + ) + client = _RecordingClient( + None, + responses={("POST", "/seeder/generate"): {"records_created": {"sales": 1}}}, + ) + status, detail, _data = await pipeline.step_seed(ctx, _as_client(client)) + assert status == "pass" + body = client.calls[0][2] + assert body is not None + assert body["scenario"] == "holiday_rush" + assert body["start_date"] == "2024-10-01" + assert body["end_date"] == "2024-12-31" + assert body["stores"] == 5 + assert body["products"] == 15 + assert "holiday_rush: 5 stores x 15 products" in detail + + +async def test_step_seed_retail_standard_posts_demo_scaled_profile(): + """E2 (#391) — retail_standard seeds 5x15 over a 180-day today-anchored window.""" + ctx = pipeline.DemoContext( + seed=42, skip_seed=False, reset=False, scenario=ScenarioPreset.RETAIL_STANDARD + ) + client = _RecordingClient( + None, + responses={("POST", "/seeder/generate"): {"records_created": {"sales": 1}}}, + ) + status, _detail, _data = await pipeline.step_seed(ctx, _as_client(client)) + assert status == "pass" + body = client.calls[0][2] + assert body is not None + assert body["scenario"] == "retail_standard" + assert body["stores"] == 5 + assert body["products"] == 15 + start = date.fromisoformat(body["start_date"]) + end = date.fromisoformat(body["end_date"]) + assert end - start == timedelta(days=180) + # sparsity stays 0.0 — the seeder override fires only when > 0, which is + # what preserves the sparse preset's 50%-missing character. + assert body["sparsity"] == 0.0 From f6e86c98b268d3e2c1369aa9603c09ef2e19bf48 Mon Sep 17 00:00:00 2001 From: Gabor Szabo Date: Fri, 12 Jun 2026 14:43:43 +0200 Subject: [PATCH 2/4] feat(ui): expose all eight scenario presets as guided cards (#391) --- .../src/components/demo/PHASE_DEFS.test.ts | 14 ++ .../components/demo/ScenarioPicker.test.tsx | 63 +++++++-- .../src/components/demo/ScenarioPicker.tsx | 128 ++++++++++++------ frontend/src/pages/showcase.tsx | 5 +- frontend/src/types/api.ts | 15 +- 5 files changed, 166 insertions(+), 59 deletions(-) diff --git a/frontend/src/components/demo/PHASE_DEFS.test.ts b/frontend/src/components/demo/PHASE_DEFS.test.ts index 74f0665c..803cc16e 100644 --- a/frontend/src/components/demo/PHASE_DEFS.test.ts +++ b/frontend/src/components/demo/PHASE_DEFS.test.ts @@ -72,6 +72,20 @@ describe('PHASE_DEFS lockstep with backend _phase_table', () => { expect(sparse).toEqual(minimal) }) + // E2 (#391) — lockstep with the backend parametrized test + // test_phase_table_non_showcase_presets_match_demo_minimal_shape: only + // showcase_rich extends the pipeline; the 5 newly exposed presets run the + // legacy 11-step flow. + it.each([ + 'retail_standard', + 'holiday_rush', + 'high_variance', + 'stockout_heavy', + 'new_launches', + ] as const)('%s -> matches the demo_minimal shape (E2 #391)', (preset) => { + expect(phaseDefsForScenario(preset)).toEqual(phaseDefsForScenario('demo_minimal')) + }) + it('PHASE_ORDER contains exactly the ten canonical phases (PRP-41 swaps agent->agents and adds ops)', () => { expect(PHASE_ORDER).toEqual([ 'data', diff --git a/frontend/src/components/demo/ScenarioPicker.test.tsx b/frontend/src/components/demo/ScenarioPicker.test.tsx index 15bc8ac6..044f137a 100644 --- a/frontend/src/components/demo/ScenarioPicker.test.tsx +++ b/frontend/src/components/demo/ScenarioPicker.test.tsx @@ -1,26 +1,63 @@ -import { afterEach, describe, expect, it } from 'vitest' -import { cleanup, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { ScenarioPicker } from './ScenarioPicker' afterEach(cleanup) +const ALL_PRESETS = [ + 'retail_standard', + 'holiday_rush', + 'high_variance', + 'stockout_heavy', + 'new_launches', + 'sparse', + 'demo_minimal', + 'showcase_rich', +] as const + describe('ScenarioPicker', () => { - it('renders the current value on the trigger', () => { + it('renders all 8 preset cards with their monospace ids', () => { render( undefined} />) - const trigger = screen.getByRole('combobox') - expect(trigger).toBeTruthy() - expect(trigger.textContent ?? '').toContain('demo_minimal') + const cards = screen.getAllByRole('button') + expect(cards.length).toBe(8) + for (const preset of ALL_PRESETS) { + expect(screen.getByText(preset)).toBeTruthy() + } }) - it('is disabled when the run is in flight', () => { - render( undefined} disabled />) - const trigger = screen.getByRole('combobox') as HTMLButtonElement - expect(trigger.disabled).toBe(true) + it('fires onChange with the preset value when a card is clicked', () => { + const onChange = vi.fn() + render() + fireEvent.click(screen.getByText('retail_standard').closest('button')!) + expect(onChange).toHaveBeenCalledWith('retail_standard') }) - it('renders the showcase_rich label when that is the selected value', () => { + it('marks the selected card with aria-pressed=true and all others false', () => { render( undefined} />) - const trigger = screen.getByRole('combobox') - expect(trigger.textContent ?? '').toContain('showcase_rich') + const pressed = screen.getAllByRole('button', { pressed: true }) + expect(pressed.length).toBe(1) + expect(pressed[0]!.textContent ?? '').toContain('showcase_rich') + expect(screen.getAllByRole('button', { pressed: false }).length).toBe(7) + }) + + it('disables every card while a run is in flight', () => { + render( undefined} disabled />) + const cards = screen.getAllByRole('button') as HTMLButtonElement[] + expect(cards.length).toBe(8) + for (const card of cards) { + expect(card.disabled).toBe(true) + } + }) + + it('shows the expected-fail caveat on the sparse card', () => { + render( undefined} />) + const sparseCard = screen.getByText('sparse').closest('button')! + expect(sparseCard.textContent ?? '').toContain('expected') + }) + + it('shows the pinned-2024-window caveat on the holiday_rush card', () => { + render( undefined} />) + const holidayCard = screen.getByText('holiday_rush').closest('button')! + expect(holidayCard.textContent ?? '').toContain('2024') }) }) diff --git a/frontend/src/components/demo/ScenarioPicker.tsx b/frontend/src/components/demo/ScenarioPicker.tsx index b19a239c..bbd028e7 100644 --- a/frontend/src/components/demo/ScenarioPicker.tsx +++ b/frontend/src/components/demo/ScenarioPicker.tsx @@ -1,38 +1,74 @@ -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' +import { Badge } from '@/components/ui/badge' +import { cn } from '@/lib/utils' import type { ScenarioPreset } from '@/types/api' interface ScenarioOption { value: ScenarioPreset - label: string + title: string description: string estimatedWallClock: string + caveat?: string + caveatKind?: 'expected-skip' | 'info' } +// E2 (#391) — single source of card copy. Descriptions are truthful to the +// demo-scaled _SeedProfile the pipeline's seed step posts +// (app/features/demo/pipeline.py:_SCENARIO_SEED_PROFILE), NOT to the preset's +// native full-size config. const SCENARIO_OPTIONS: ScenarioOption[] = [ { value: 'demo_minimal', - label: 'demo_minimal', + title: 'Demo minimal', description: '3 stores × 10 products × 92 days — fast smoke loop', estimatedWallClock: '~60 s', }, { value: 'showcase_rich', - label: 'showcase_rich', - description: '5 stores × 15 products × 180 days — V1+V2 modeling', + title: 'Showcase rich', + description: '5 × 15 × 180 days — full 24-step flow, V1+V2 modeling', estimatedWallClock: '~3 min', + caveat: 'Knowledge/agent steps skip without provider keys', + caveatKind: 'info', + }, + { + value: 'retail_standard', + title: 'Retail standard', + description: '5 × 15 × 180 days — steady demand, light promos', + estimatedWallClock: '~90 s', + }, + { + value: 'holiday_rush', + title: 'Holiday rush', + description: '5 × 15 × Oct–Dec 2024 — Black Friday/Christmas spikes', + estimatedWallClock: '~90 s', + caveat: 'Seeds a pinned 2024 window (calendar-pinned holidays)', + caveatKind: 'info', + }, + { + value: 'high_variance', + title: 'High variance', + description: '5 × 15 × 180 days — noisy demand with anomaly spikes', + estimatedWallClock: '~90 s', + }, + { + value: 'stockout_heavy', + title: 'Stockout heavy', + description: '5 × 15 × 180 days — 25% stockout days zero the sales', + estimatedWallClock: '~90 s', + }, + { + value: 'new_launches', + title: 'New launches', + description: '5 × 25 × 180 days — 45-day product launch ramps', + estimatedWallClock: '~2 min', }, { value: 'sparse', - label: 'sparse', - description: 'Sparse + gappy time series — edge-case data shape', + title: 'Sparse', + description: '3 × 10 × 92 days — 50% missing grains + random gaps', estimatedWallClock: '~90 s', + caveat: '⏭️ May fail at features/backtest (NaN WAPE) — expected; see runbook', + caveatKind: 'expected-skip', }, ] @@ -43,39 +79,49 @@ interface ScenarioPickerProps { } /** - * PRP-38 — shadcn ` onChange(v as ScenarioPreset)} - disabled={disabled} - > - - - - - - {SCENARIO_OPTIONS.map((opt) => ( - -
- {opt.label} - - {opt.description} · {opt.estimatedWallClock} - -
-
- ))} -
-
- +
+ {SCENARIO_OPTIONS.map((opt) => ( + + ))} +
+

+ Tick Re-seed first when switching presets — without + it the run reuses the currently seeded dataset. +

) } diff --git a/frontend/src/pages/showcase.tsx b/frontend/src/pages/showcase.tsx index 0f590218..6f6e38ed 100644 --- a/frontend/src/pages/showcase.tsx +++ b/frontend/src/pages/showcase.tsx @@ -155,8 +155,9 @@ export default function ShowcasePage() {

Run the full forecasting pipeline live — phase by phase. The same flow as{' '} make demo, streamed to - the browser. Pick a scenario to control depth (demo_minimal stays fast; - showcase_rich exercises V1+V2 modeling). + the browser. Pick a scenario to control depth and data shape — all eight seeder + presets are available (demo_minimal stays fast; showcase_rich exercises V1+V2 + modeling).

diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 88a204e1..54ff956a 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -742,9 +742,18 @@ export interface VerifyResult { export type DemoStepStatus = 'running' | 'pass' | 'fail' | 'skip' | 'warn' export type DemoEventType = 'step_start' | 'step_complete' | 'pipeline_complete' | 'error' -// PRP-38 — seeder scenario presets the picker offers. Mirrors the backend -// app/shared/seeder/config.py:ScenarioPreset enum's string values. -export type ScenarioPreset = 'demo_minimal' | 'showcase_rich' | 'sparse' +// PRP-38 / E2 (#391) — seeder scenario presets the picker offers. Mirrors +// the backend app/shared/seeder/config.py:ScenarioPreset enum's string +// values — all 8 members. +export type ScenarioPreset = + | 'retail_standard' + | 'holiday_rush' + | 'high_variance' + | 'stockout_heavy' + | 'new_launches' + | 'sparse' + | 'demo_minimal' + | 'showcase_rich' // One streamed pipeline event from WS /demo/stream (matches the backend // StepEvent Pydantic model; snake_case on the wire). From ce7033dbc8a1387d873a07070e32f8ac081491e6 Mon Sep 17 00:00:00 2001 From: Gabor Szabo Date: Fri, 12 Jun 2026 14:43:43 +0200 Subject: [PATCH 3/4] docs(api): document full scenario union and preset outcomes (#391) --- docs/_base/API_CONTRACTS.md | 4 ++-- docs/_base/RUNBOOKS.md | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/_base/API_CONTRACTS.md b/docs/_base/API_CONTRACTS.md index abcebd1a..68d73c5d 100644 --- a/docs/_base/API_CONTRACTS.md +++ b/docs/_base/API_CONTRACTS.md @@ -58,7 +58,7 @@ All endpoints serve JSON; error responses use `application/problem+json` (RFC 78 | agents | WS | `/agents/stream` | Token-by-token streaming + tool-call events | | seeder | (see `app/features/seeder/routes.py`) | `/seeder/*` | Trigger scenarios, status, customization | | seeder | POST | `/seeder/phase2-enrichment` | PRP-38 — run Phase 2 generators (lifecycle, replenishment, exogenous, returns) against the existing seeded data. `422 application/problem+json` on an empty database. | -| demo | POST | `/demo/run` | Run the end-to-end demo pipeline in-process; returns a `DemoRunResult`. `409 application/problem+json` if a run is already active. **PRP-38** — body accepts an Optional `scenario: 'demo_minimal' \| 'showcase_rich' \| 'sparse'` field; default `'demo_minimal'` (back-compat). **E1 (#390)** — body accepts additive Optional `preservation: 'ephemeral' \| 'keep'` (default `'ephemeral'`, today's no-row behavior) and `workspace_name: str \| null` (pattern `^[a-z0-9][a-z0-9\-_]*$`, ≤100 chars); `workspace_name` without `preservation='keep'` → `422 application/problem+json`. `preservation='keep'` records the run as a `showcase_workspace` row; `DemoRunResult` gains an additive Optional `workspace_id: str \| null`. | +| demo | POST | `/demo/run` | Run the end-to-end demo pipeline in-process; returns a `DemoRunResult`. `409 application/problem+json` if a run is already active. **PRP-38** — body accepts an Optional `scenario: 'demo_minimal' \| 'showcase_rich' \| 'sparse'` field; default `'demo_minimal'` (back-compat). **E1 (#390)** — body accepts additive Optional `preservation: 'ephemeral' \| 'keep'` (default `'ephemeral'`, today's no-row behavior) and `workspace_name: str \| null` (pattern `^[a-z0-9][a-z0-9\-_]*$`, ≤100 chars); `workspace_name` without `preservation='keep'` → `422 application/problem+json`. `preservation='keep'` records the run as a `showcase_workspace` row; `DemoRunResult` gains an additive Optional `workspace_id: str \| null`. **E2 (#391)** — `scenario` accepts all 8 `ScenarioPreset` values (`retail_standard` / `holiday_rush` / `high_variance` / `stockout_heavy` / `new_launches` / `sparse` / `demo_minimal` / `showcase_rich`); only `showcase_rich` changes the step table (24 rows), every other preset runs the legacy 11-row flow. | | demo | WS | `/demo/stream` | Stream one `StepEvent` per pipeline step for the live Showcase page | | config | GET | `/config/ai` | Effective AI-model config (agent LLM + RAG embeddings); API keys masked, never raw | | config | PATCH | `/config/ai` | Persist + apply AI-model changes live (no restart). `409` if an embedding-dimension change would orphan indexed RAG chunks (resend with `force=true`) | @@ -83,7 +83,7 @@ Verified against `app/features/agents/websocket.py` and `app/features/agents/sch Drives the end-to-end demo pipeline for the dashboard Showcase page. Verified against `app/features/demo/routes.py` and `app/features/demo/schemas.py` (`StepEvent`). -- **Client → server (one start frame):** `{"seed": int, "reset": bool, "skip_seed": bool, "scenario"?: "demo_minimal" | "showcase_rich" | "sparse", "preservation"?: "ephemeral" | "keep", "workspace_name"?: str}` — all fields optional (`DemoRunRequest` supplies defaults `seed=42`, `reset=false`, `skip_seed=true`, `scenario="demo_minimal"`, `preservation="ephemeral"`, `workspace_name=null`). E1 (#390) — `workspace_name` requires `preservation="keep"` (else one `error` event from validation); unknown start-frame keys remain ignored (forward/backward compat). The pipeline runs once, then the server closes. +- **Client → server (one start frame):** `{"seed": int, "reset": bool, "skip_seed": bool, "scenario"?: "demo_minimal" | "showcase_rich" | "sparse", "preservation"?: "ephemeral" | "keep", "workspace_name"?: str}` — all fields optional (`DemoRunRequest` supplies defaults `seed=42`, `reset=false`, `skip_seed=true`, `scenario="demo_minimal"`, `preservation="ephemeral"`, `workspace_name=null`). E1 (#390) — `workspace_name` requires `preservation="keep"` (else one `error` event from validation); unknown start-frame keys remain ignored (forward/backward compat). E2 (#391) — `scenario` accepts all 8 `ScenarioPreset` values (`retail_standard` / `holiday_rush` / `high_variance` / `stockout_heavy` / `new_launches` / `sparse` / `demo_minimal` / `showcase_rich`); only `showcase_rich` changes the step table (24 rows), every other preset runs the legacy 11-row flow. The pipeline runs once, then the server closes. - **Server → client (every frame):** Pydantic-serialized `StepEvent` — `{"event_type", "step_name", "step_index", "total_steps", "status", "detail", "duration_ms", "data", "timestamp", "phase_name"?, "phase_index"?, "phase_total"?}`. PRP-38 — the three `phase_*` fields are Optional + Nullable so legacy clients that don't render phases keep working. - **`event_type` values (Literal in `StepEvent`):** - `step_start` — a step began; `status` is `null`. diff --git a/docs/_base/RUNBOOKS.md b/docs/_base/RUNBOOKS.md index 4ba53dca..df636648 100644 --- a/docs/_base/RUNBOOKS.md +++ b/docs/_base/RUNBOOKS.md @@ -134,10 +134,15 @@ uv run python scripts/run_demo.py --seed 42 --quiet 2>&1 | tee demo.log 25. **`agent_hitl_flow` step shows ⏭️ with `agent did not trigger save_scenario` (PRP-41, `showcase_rich` only)** — the agent answered the prompt directly (no `tool_save_scenario` call) so `pending_approval=false` came back on the chat response. Cause: model picked a different tool / answered in chat. Pipeline still greens. Fix: re-run; the model's response is non-deterministic. If the model ALWAYS skips the tool, raise the temperature in `agent_default_model` or re-prompt. 26. **`ops_snapshot` step shows ⚠️ with `/ops/* all 4xx/5xx -- ops snapshot unavailable` (PRP-41, `showcase_rich` only)** — all three of `GET /ops/summary`, `/ops/retraining-candidates`, `/ops/model-health` returned non-2xx. Cause: DB unreachable, alembic migration drift, OpsService change broke the schema. Pipeline still warn (NEVER fail). Fix: `docker compose ps`; `uv run alembic upgrade head`; re-run. 27. **Stop button used mid-run** — the Stop button on `/showcase` closes the WebSocket; the backend's `WebSocketDisconnect` handler at `app/features/demo/routes.py:74` releases `_pipeline_lock`. Page returns to `idle` within ~5 s with banner "Pipeline cancelled by user.". To resume, click Run again. Half-finished registry rows / scenario plans persist (the backend doesn't roll them back — they're operator-visible artefacts of a partial run). +28. **A newly exposed preset run ends red/skipped (E2 #391)** — the scenario card grid exposes all 8 `ScenarioPreset` values; some presets have documented non-green outcomes. Cause: a re-seed posts a demo-scaled dims/window override while keeping the preset's behavioral character from `SeederConfig.from_scenario` (noise, promos, stockouts, gaps), and some characters legitimately break pipeline steps. Per-preset expected-outcome matrix: + - `sparse` — **may FAIL** at `features`/`backtest`: 50% missing (store, product) grains + random 2–10-day gaps can leave the discovered demo grain with too-thin history, or produce an all-NaN WAPE which the `step_backtest` NaN gate fails by design (a graceful-skip would mask real regressions on healthy presets). The card carries an expected-fail badge; either green or this fail is a documented outcome. + - `holiday_rush` — seeds a **pinned Oct–Dec 2024 window** (the preset's `HolidayConfig` spikes are fixed 2024 dates; a today-anchored window would never contain them). Re-seeding ADDS rows without wiping prior data, so after a holiday_rush re-seed `/seeder/status` reports the union range (e.g. `2024-10-01..today`); tick **Reset database** together with **Re-seed first** for a clean pinned window, and again when switching back to a today-anchored preset. Expected green on the 11-step flow. + - `retail_standard` / `high_variance` / `stockout_heavy` — demo-scaled 5×15×180d, today-anchored; `new_launches` — 5×25×180d. All expected **green** on the legacy 11-step flow (only `showcase_rich` runs the 24-step table). + Fix: none for the documented outcomes above. If a normally-green preset fails, make sure **Re-seed first** was ticked (without it the run reuses the currently seeded dataset, whatever preset produced it), then re-run. > ⚠️ **RAG embedding-dim mismatch can orphan chunks (R4).** PRP-40 indexes a curated 5-file subset; if the operator switches the embedding provider mid-showcase, indexed chunks orphan (pgvector assumes one fixed dimension per column). PRP-40 does NOT ship a `clear_rag` UI toggle — that's a future PRP. Stick to one provider for the showcase run. -**Notes:** the `POST /demo/run` body and `WS /demo/stream` events are documented in `docs/_base/API_CONTRACTS.md`. The pipeline mirrors `scripts/run_demo.py`; the per-step diagnosis for `make demo` above applies to the same steps. PRP-38 added the `scenario` field on `DemoRunRequest` (defaults to `demo_minimal`) and the additive `phase_name` / `phase_index` / `phase_total` fields on every `StepEvent`. PRP-39 added four new steps (`champion_compat_compare`, `stale_alias_trigger`, `safer_promote_flow`, `batch_preset`) and a new `portfolio` phase between `decision` and `verify`. PRP-40 added the `planning` + `knowledge` phases (5 steps inserted after `portfolio`, before `verify`) and the additive `IndexProjectDocsRequest.path_prefix` field on the RAG slice. PRP-41 — design Z renames the legacy `agent` phase to `agents`, swaps the legacy `step_agent` for `agent_hitl_flow` (HITL approval round-trip), and appends a new `ops` phase carrying `ops_snapshot` immediately before `cleanup`. Total: 24 rows / 10 phases on `showcase_rich`; demo_minimal / sparse keep the 11-row layout under the unified `agents` phase id. The frontend's `DemoPhasePanel.tsx` now carries `onValueChange` (issue #311) and the Showcase page adds a KPI strip + Run-history strip + Stop button + Inspect-Artifacts panel + one-click Approve button on the HITL step card. +**Notes:** the `POST /demo/run` body and `WS /demo/stream` events are documented in `docs/_base/API_CONTRACTS.md`. The pipeline mirrors `scripts/run_demo.py`; the per-step diagnosis for `make demo` above applies to the same steps. PRP-38 added the `scenario` field on `DemoRunRequest` (defaults to `demo_minimal`) and the additive `phase_name` / `phase_index` / `phase_total` fields on every `StepEvent`. PRP-39 added four new steps (`champion_compat_compare`, `stale_alias_trigger`, `safer_promote_flow`, `batch_preset`) and a new `portfolio` phase between `decision` and `verify`. PRP-40 added the `planning` + `knowledge` phases (5 steps inserted after `portfolio`, before `verify`) and the additive `IndexProjectDocsRequest.path_prefix` field on the RAG slice. PRP-41 — design Z renames the legacy `agent` phase to `agents`, swaps the legacy `step_agent` for `agent_hitl_flow` (HITL approval round-trip), and appends a new `ops` phase carrying `ops_snapshot` immediately before `cleanup`. Total: 24 rows / 10 phases on `showcase_rich`; demo_minimal / sparse keep the 11-row layout under the unified `agents` phase id. The frontend's `DemoPhasePanel.tsx` now carries `onValueChange` (issue #311) and the Showcase page adds a KPI strip + Run-history strip + Stop button + Inspect-Artifacts panel + one-click Approve button on the HITL step card. E2 (#391) — the Scenario control is a card grid exposing all 8 `ScenarioPreset` values with per-preset demo seed profiles (`_SCENARIO_SEED_PROFILE` is exhaustive over the enum; `holiday_rush` seeds a pinned Oct–Dec 2024 window); the 5 newly exposed presets keep the legacy 11-row layout. ### release-please skipped the bump after a dev → main merge **Symptoms:** `dev → main` PR is merged, `CD Release` workflow on `main` completes in ~10s, **no Release PR** is opened. release-please log shows `No user facing commits found since - skipping`. From 39eb42964f073f3c1bf8008986d8e15662836353 Mon Sep 17 00:00:00 2001 From: Gabor Szabo Date: Fri, 12 Jun 2026 14:43:43 +0200 Subject: [PATCH 4/4] docs(repo): track showcase workspace e2 prp (#391) --- ...P-showcase-workspace-E2-preset-exposure.md | 620 ++++++++++++++++++ 1 file changed, 620 insertions(+) create mode 100644 PRPs/PRP-showcase-workspace-E2-preset-exposure.md diff --git a/PRPs/PRP-showcase-workspace-E2-preset-exposure.md b/PRPs/PRP-showcase-workspace-E2-preset-exposure.md new file mode 100644 index 00000000..e20eb7a5 --- /dev/null +++ b/PRPs/PRP-showcase-workspace-E2-preset-exposure.md @@ -0,0 +1,620 @@ +name: "PRP — Showcase Workspace E2: Full Preset Exposure (issue #391)" +description: | + +## Purpose + +Implement the first Parallel epic of the showcase-workspace initiative (umbrella #389): +surface all 8 `ScenarioPreset` values as guided, business-friendly cards in the +frontend `ScenarioPicker`, give per-preset demo seed profiles to the pipeline's +seed step, and attach expected-skip semantics (card caveat + runbook entry) to +presets that cannot complete every pipeline step. Frontend-mostly; the backend +already accepts the full enum. + +## Core Principles + +1. **Context is King**: every reference below was verified against the live code on 2026-06-12 (branch dev @ 0493192, post-E1 merge). +2. **Validation Loops**: each level is executable as written. +3. **Information Dense**: patterns cite exact file:line. +4. **Progressive Success**: backend seed profiles → types → picker cards → lockstep tests → docs → browser dogfood. +5. **Global rules**: follow CLAUDE.md / AGENTS.md; all five backend CI gates must pass; UI work follows `.claude/rules/ui-design.md` + `.claude/rules/shadcn-ui.md`. + +--- + +## Goal + +A user on `/showcase` can pick any of the 8 seeder presets (`retail_standard`, +`holiday_rush`, `high_variance`, `stockout_heavy`, `new_launches`, `sparse`, +`demo_minimal`, `showcase_rich`) from a card grid that explains, per preset: +what data it seeds (stores × products × window), its business character (promos, +stockouts, launches, noise), an estimated wall-clock, and — where applicable — +an **expected-skip/fail caveat** so a non-green outcome reads as documented +behavior, not a bug. Re-seeding with any preset produces a pipeline run that +either goes green or fails/skips exactly as its card predicts. + +**Deliverable** (all additive): + +- `app/features/demo/pipeline.py` — `_SCENARIO_SEED_PROFILE` extended from 3 to all 8 presets via a `_SeedProfile` NamedTuple that supports an optional calendar-pinned window (needed by `holiday_rush`); `step_seed` honors the pinned window. +- `frontend/src/types/api.ts` — `ScenarioPreset` union widened from 3 to all 8 string values. +- `frontend/src/components/demo/ScenarioPicker.tsx` — shadcn `