Files
ss-tools/specs/017-llm-analysis-plugin/tasks.md
2026-05-31 17:38:43 +03:00

394 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Tasks: LLM Analysis & Documentation Plugins v2
**Feature**: `017-llm-analysis-plugin`
**Status**: v1 Completed; v2 Planned
**Spec**: [spec.md](spec.md) | **Plan**: [plan.md](plan.md) | **Data Model**: [data-model.md](data-model.md)
## User Stories & Priorities
| ID | Story | Priority | Phase |
|----|-------|----------|-------|
| US1 | Create & Schedule Validation Task | P1 | 3 |
| US2 | Validation Task List & History | P1 | 3 |
| US3 | Dual-Path Dashboard Health Analysis | P1 | 4, 5 |
| US4 | URL Parsing Integration | P1 | 6 |
| US5 | Provider/Prompt Per-Task + Settings Cleanup | P2 | 7 |
| US6 | Dashboard Hub Cleanup | P2 | 8 |
| US7 | Dataset Documentation (unchanged) | P2 | — |
| US8 | Git Commit Suggestion (unchanged) | P3 | — |
## Dependencies
```
Phase 1 (Setup)
└─→ Phase 2 (Foundational)
├─→ Phase 3 (US1 + US2) — Task CRUD
│ └─→ Phase 7 (US5) — Provider/Prompt
├─→ Phase 4 (US3 Path B) — Text-only execution
├─→ Phase 5 (US3 Path A) — Multi-chunk screenshots
├─→ Phase 6 (US4) — URL parsing
└─→ Phase 8 (US6) — Hub cleanup (independent)
└─→ Phase 9 (Polish)
```
**Parallel**: Phase 4 + 5 + 6 can run concurrently after Phase 2. Phase 7 is tight with Phase 3. Phase 8 is independent.
---
## Phase 1: Setup — Database Migrations
**Goal**: Schema changes for v2 models. Blocks everything.
- [ ] T001 [P] Add `sources` ORM relationship + `source_snapshot` JSON (audit only, nullable) to `ValidationPolicy` in `backend/src/models/llm.py`. No JSON denormalization column for authoritative state — relational `ValidationSource` table is the single source of truth.
- [ ] T002 [P] Create `ValidationSource` SQLAlchemy model in `backend/src/models/llm.py` (id, policy_id FK, type, value, parsed_context JSON, is_valid, title, last_error)
- [ ] T003 [P] Add new columns to `ValidationPolicy` model: `provider_id` (String), `prompt_template` (Text, nullable), `screenshot_enabled` (Boolean, default true), `logs_enabled` (Boolean, default true), `execute_chart_data` (Boolean, default false), `llm_batch_size` (Integer, default 1 — full isolation), `policy_dashboard_concurrency_limit` (Integer, default 3) in `backend/src/models/llm.py`
- [ ] T003a [P] Add `global_validation_worker_limit` config setting (default: 3) — max concurrent policy runs across all tasks. Stored in app config, not per-policy. in `backend/src/core/config_models.py`
- [ ] T004 [P] Add new columns to `ValidationRecord` model: `execution_path` (String), `source_id` (FK → ValidationSource, nullable), `dataset_health` (JSON, nullable), `chart_data_results` (JSON, nullable), `tab_screenshots` (JSON, nullable), `token_usage` (JSON, nullable), `dataset_key` (String, nullable) in `backend/src/models/llm.py`
- [ ] T004a [P] Create `ValidationRun` SQLAlchemy model in `backend/src/models/llm.py`: id, policy_id (FK), task_id (nullable), started_at, finished_at, trigger (manual/scheduled), status (completed/partial/failed), dashboard_count, pass_count, warn_count, fail_count, unknown_count
- [ ] T005 Create Alembic migration for all T001T004a schema changes in `backend/alembic/versions/`
- [ ] T006 Write data migration script: convert existing `dashboard_ids: list[str]``sources: [{type: "dashboard_id", value: id, is_valid: true}]` in `backend/src/plugins/llm_analysis/migrations/`
- [ ] T007 [P] Add `DEFAULT_LLM_PROMPTS["dashboard_validation_prompt_multimodal"]` (Path A, image-focused) and `DEFAULT_LLM_PROMPTS["dashboard_validation_prompt_text"]` (Path B, topology-focused) to `backend/src/services/llm_prompt_templates.py`
**Verification**: `pytest backend/tests/models/test_llm_v2.py -v` — all models create/query correctly. Migration runs without data loss.
---
## Phase 2: Foundational — Services & Refactored Plugin Core
**Goal**: `ValidationTaskService`, `DatasetHealthChecker`, refactored `ScreenshotService` (multi-chunk), `LLMClient` dual-mode. Blocks US1US6.
### 2.1 ValidationTaskService
- [ ] T008 [P] Implement `ValidationTaskService.create_task()` — creates `ValidationPolicy` + `ValidationSource[]` rows, validates provider_id against execution path, in `backend/src/services/validation_service.py`
- [ ] T009 [P] Implement `ValidationTaskService.get_task(id)` — returns policy + sources with current status, in `backend/src/services/validation_service.py`
- [ ] T010 [P] Implement `ValidationTaskService.list_tasks(env_id)` — returns all policies with aggregated last-run status, in `backend/src/services/validation_service.py`
- [ ] T011 [P] Implement `ValidationTaskService.update_task(id, data)` — updates policy fields + sources, in `backend/src/services/validation_service.py`
- [ ] T012 [P] Implement `ValidationTaskService.delete_task(id)` — deletes policy + cascades to sources + records, in `backend/src/services/validation_service.py`
- [ ] T013 [P] Implement `ValidationTaskService.trigger_run(policy_id)` — checks FR-054 (same task not already running via ValidationRun with status=running), creates a `ValidationRun` record (FR-055d), enqueues dashboard validation tasks per FR-055 (FIFO with `global_validation_worker_limit`), in `backend/src/services/validation_service.py`
- [ ] T014 [P] Implement `ValidationTaskService.get_run_history(policy_id, page)` — paginated `ValidationRun` list with aggregate counts, in `backend/src/services/validation_service.py`
- [ ] T015 [P] Implement `ValidationTaskService.get_run_detail(run_id)` — full run with all dashboard records, aggregate counts, in `backend/src/services/validation_service.py`
- [ ] T015a [P] Implement `ValidationTaskService._finalize_run(run_id)` — computes aggregate counts from all `ValidationRecord`s with this `run_id`, sets finished_at and status, in `backend/src/services/validation_service.py`
### 2.2 DatasetHealthChecker (Path B)
- [ ] T016 [P] Implement `DatasetHealthChecker` class in `backend/src/plugins/llm_analysis/service.py`:
- `check_dataset_health(dataset_id: int, client: SupersetClient) → DatasetHealth` — calls `GET /api/v1/dataset/{id}`, extracts database name, backend, kind, verifies accessibility
- `check_dashboard_datasets(dashboard_id: int, chart_list: list[dict], client: SupersetClient, execute_data: bool) → tuple[list[DatasetHealth], list[ChartDataResult]]`
### 2.3 ScreenshotService Refactor (multi-chunk for Path A)
- [ ] T017 [P] Implement `ScreenshotService.capture_dashboard_chunks(dashboard_id, parsed_filters, output_dir) → list[{tab_name, path}]` in `backend/src/plugins/llm_analysis/service.py`:
- Login → navigate with filters/tabs → iterate tabs → per-tab viewport (1920×1200) → CDP screenshot → collect PNG paths
### 2.3b Image Pipeline (PNG → JPEG + WebP)
- [ ] T017a [P] Implement `ScreenshotService._convert_screenshots_for_llm(png_paths: list[str]) → list[str]` in `backend/src/plugins/llm_analysis/service.py`:
- PNG → Pillow convert → JPEG quality=60, max 1024px → save as temp JPEG → return paths
- JPEG files deleted after LLM call succeeds
- [ ] T017b [P] Implement `ScreenshotService._archive_screenshots_as_webp(png_paths: list[str], output_dir: str) → list[{tab_name, webp_path}]` in `backend/src/plugins/llm_analysis/service.py`:
- PNG → Pillow convert → WebP quality=80, lossy → save to `{output_dir}/{tab_name}_{timestamp}.webp`
- Delete PNG after WebP saved
- Fallback: if WebP encode fails → keep PNG, log WARN (FR-059 edge case)
- [ ] T017c [P] Implement `ScreenshotService._cleanup_temp_files(paths: list[str])` in `backend/src/plugins/llm_analysis/service.py`:
- Delete PNG and JPEG intermediates after successful archive + LLM call
### 2.4 LLMClient Dual-Mode
- [ ] T018 [P] Implement `LLMClient.analyze_dashboard_multimodal(screenshot_paths: list[str], logs: list[str], prompt: str) → dict` in `backend/src/plugins/llm_analysis/service.py`:
- Sends all images in single `content[]` array → multimodal LLM → JSON result
- [ ] T019 [P] Implement `LLMClient.analyze_dashboard_text(topology_text: str, dataset_health: list[dict], logs: list[str], prompt: str) → dict` in `backend/src/plugins/llm_analysis/service.py`:
- Text-only LLM call with dashboard topology → JSON result
- [ ] T020 [P] Implement `LLMClient._estimate_payload_size()` and `LLMClient._reduce_image_quality()` in `backend/src/plugins/llm_analysis/service.py`:
- FR-056: estimate → progressive reduction (JPEG quality 60→30, width 1024→800) → Path B fallback
- **Belief-runtime**: `belief_scope("payload_reduction")` + `reason()` before each reduction + `reflect()` after outcome
### 2.5 Plugin Refactor (dispatch to Path A / Path B)
- [ ] T021 Implement `DashboardValidationPlugin._build_dashboard_topology(dashboard_data: dict, charts_data: list[dict]) → str` in `backend/src/plugins/llm_analysis/plugin.py`:
- Recursive `position_json` parsing → tab → row → chart hierarchy → text description
- [ ] T022 Refactor `DashboardValidationPlugin.execute()` to dispatch based on `screenshot_enabled`:
- True → `_execute_path_a()`: call ScreenshotService → LLMClient.analyze_dashboard_multimodal()
- False → `_execute_path_b()`: call API chain → DatasetHealthChecker → LLMClient.analyze_dashboard_text()
- **Belief-runtime**: `belief_scope("execute_path_a")` / `belief_scope("execute_path_b")` + `reason()` at entry + `reflect()` at exit
### 2.7 Security & Retention (new)
- [ ] T024a [P] Implement `RedactionService` in `backend/src/plugins/llm_analysis/service.py`:
- `redact_logs(logs: list[str]) → list[str]` — strip PII, credentials, query text, tokens from URLs, native filter values
- `redact_raw_response(raw: str) → str` — strip sensitive data before persistence
- Applied at capture boundary (before LLM send + before DB save) per FR-029/FR-029a
- [ ] T024b [P] Add retention config to app settings: `LLM_SCREENSHOT_RETENTION_DAYS=30`, `LLM_RAW_RESPONSE_RETENTION_DAYS=30` in `backend/src/core/config_models.py`
- [ ] T024c [P] Rename all «molecular-cot» references in logs to «decision audit logging»: REASON/REFLECT markers use structured JSON with deterministic metadata only (no free-form reasoning text, no PII, no screenshot content) per FR-029b. Update docstrings in `backend/src/core/logger.py` and skill files.
- [ ] T023 [P] Add `dashboard_validation_prompt_multimodal` (Path A) template to `DEFAULT_LLM_PROMPTS` in `backend/src/services/llm_prompt_templates.py`:
- «Analyze the attached {N} dashboard tab screenshots and execution logs for visual and data health issues...»
- [ ] T024 [P] Add `dashboard_validation_prompt_text` (Path B) template to `DEFAULT_LLM_PROMPTS` in `backend/src/services/llm_prompt_templates.py`:
- «Analyze the following dashboard topology, dataset health, and execution logs for data consistency and KXD connectivity issues...»
**Verification**: `pytest backend/tests/services/test_validation_task_service.py backend/tests/plugins/llm_analysis/test_service.py -v`
---
## Phase 3: US1 + US2 — Task CRUD API + Task List Page
**Goal**: REST API for validation tasks + frontend task list and create/edit form.
### 3.1 API Routes
- [ ] T025 [US1] Implement `POST /api/validation-tasks` — creates `ValidationPolicy` + `ValidationSource[]` in `backend/src/api/routes/validation_tasks.py`
- [ ] T026 [US1] Implement `GET /api/validation-tasks` — list all tasks with aggregated last-run status in `backend/src/api/routes/validation_tasks.py`
- [ ] T027 [US2] Implement `GET /api/validation-tasks/{policy_id}` — task detail + per-source status in `backend/src/api/routes/validation_tasks.py`
- [ ] T028 [US1] Implement `PUT /api/validation-tasks/{policy_id}` — update task in `backend/src/api/routes/validation_tasks.py`
- [ ] T029 [US1] Implement `DELETE /api/validation-tasks/{policy_id}` — delete task (cascade sources + records), in `backend/src/api/routes/validation_tasks.py`
- [ ] T030 [US1] Implement `POST /api/validation-tasks/{policy_id}/run` — manual trigger (checks FR-054 + FR-055) in `backend/src/api/routes/validation_tasks.py`
- [ ] T031 [US2] Implement `GET /api/validation-tasks/{policy_id}/runs` — paginated run history (ValidationRun list, not individual records) in `backend/src/api/routes/validation_tasks.py`
- [ ] T032 [US2] Implement `GET /api/validation-tasks/{policy_id}/runs/{run_id}` — full run detail: run metadata + all per-dashboard ValidationRecords with dataset_health, screenshots, logs, timings, in `backend/src/api/routes/validation_tasks.py`
- [ ] T032b Implement legacy redirect: `GET /reports/llm/{taskId}` → 302 to `/validation-tasks/{policy_id}/runs/{run_id}` when `taskId` maps to `ValidationRun.task_id`, in `backend/src/api/routes/validation_tasks.py`
### 3.2 Task List Page
- [ ] T035 [US2] Create `frontend/src/routes/validation-tasks/+page.svelte` — task list page with columns: name, schedule, last run status (PASS/WARN/FAIL/UNKNOWN), dashboard count, status (active/inactive), actions (edit/delete/run now)
- [ ] T036 [US2] Create `frontend/src/routes/validation-tasks/+page.js` — load function calling `GET /api/validation-tasks`
### 3.3 Task Create/Edit Form
- [ ] T037 [US1] Create `frontend/src/routes/validation-tasks/new/+page.svelte` — task creation page
- [ ] T038 [US1] Create `frontend/src/components/llm/ValidationTaskForm.svelte` — multi-step form component:
- Step 1: Name, description, environment selector
- Step 2: Sources — dashboard ID multi-select + URL paste tabs
- Step 3: Analysis options — provider selector (filtered per FR-050), prompt textarea (pre-filled with path-specific default: multimodal when screenshot_enabled=true, text when false), screenshot toggle, logs toggle, execute_chart_data toggle (Path B only). When toggling screenshot_enabled on a task with custom prompt → show warning «Prompt may no longer match execution path» (FR-048a)
- Step 4: Schedule — cron input + preview next runs
- [ ] T039 [US1] Create `frontend/src/routes/validation-tasks/[policyId]/edit/+page.svelte` — task edit page (reuses `ValidationTaskForm`)
### 3.5 Task Detail Page + Report Page
- [ ] T040 [US2] Create `frontend/src/routes/validation-tasks/[policyId]/+page.svelte` — task detail page:
- Task info (name, environment, schedule, provider)
- Per-source breakdown table: dashboard title, last status, issues count, last run timestamp, link to latest run
- Run history table: timestamp, trigger type, aggregate status (N PASS / M WARN / K FAIL), duration, link to run detail
- Actions: Run Now, Edit, Delete
- [ ] T041 [US2] Create `frontend/src/routes/validation-tasks/[policyId]/+page.js` — load function calling `GET /api/validation-tasks/{id}` + `GET /api/validation-tasks/{id}/runs`
- [ ] T041a [US2] Create `frontend/src/routes/validation-tasks/[policyId]/runs/[runId]/+page.svelte` — multi-dashboard run report page (FR-055b):
- Run header: policy name, timestamp, trigger, duration, aggregate badge (e.g. «3 PASS • 1 WARN • 0 FAIL»)
- Collapsible dashboard list: each row = one dashboard, shows status icon + issue count + mini-summary
- Expanded dashboard row: full issues list + execution path badge (Path A/B) + dataset health table (Path B) + screenshot thumbnails with tab selector (Path A) + logs sent to LLM + task execution logs
- Handles mixed runs: some dashboards Path A, some Path B — renders appropriate sections per dashboard
- [ ] T041b Implement `GET /api/validation-tasks/{policy_id}/runs/{run_id}` — returns full ValidationRun with all dashboard records, each containing dataset_health, chart_data_results, tab_screenshots, token_usage, timings. Also resolves policy name, provider name, and dashboard titles for display.
### 3.6 Shared Report Component
- [ ] T042 [US1] Create `frontend/src/components/llm/ValidationTaskReport.svelte` — shared report component:
- Check result badge (PASS/WARN/FAIL) with `getCheckResultClasses()`
- Summary text
- Issues table (severity, message, location)
- Execution path indicator (Path A icon / Path B icon)
- Placeholder slots for Path A (screenshots) and Path B (dataset health)
**Verification**: Create task via UI → list shows task → detail shows sources → run now → report renders.
---
## Phase 4: US3 — Path B Execution (Text-Only + Batching)
**Goal**: Text-only validation with dataset health checking, optional chart data execution, and LLM batching.
- [ ] T043 [US3] Implement `DashboardValidationPlugin._execute_path_b(run_id, dashboard_sources, params, context)` in `backend/src/plugins/llm_analysis/plugin.py`:
1. For each dashboard: collect topology via `_build_dashboard_topology()` + dataset health via `DatasetHealthChecker` + logs
2. Default: `llm_batch_size=1` — one LLM call per dashboard (full isolation). Values >1 group dashboards into batches per FR-045a (experimental, documented accuracy risk in research decision 15). Path A dashboards excluded from batching
3. For each batch: build structured prompt with per-dashboard labeled sections + strict JSON response schema `{dashboards: [{dashboard_id, status, summary, issues}]}`
4. Call `LLMClient.analyze_dashboard_text_batch(dashboard_payloads, prompt)` → parse response
5. Missing/parse-error dashboards → mark UNKNOWN individually, preserve other dashboards in batch
6. Persist individual `ValidationRecord` per dashboard, linked to `run_id`
- **Belief-runtime**: `belief_scope("execute_path_b_batch")` + `reason(f"batch {i}/{total}: {n} dashboards")` + `reflect()` with token_usage per batch
- [ ] T043a [US3] Implement `LLMClient.analyze_dashboard_text_batch(payloads: list[dict], prompt: str) → dict` in `backend/src/plugins/llm_analysis/service.py`:
- Builds prompt: per-dashboard labeled sections, enforced `{dashboards: [{dashboard_id, ...}]}` JSON schema
- Validates response completeness: every input dashboard_id must appear; missing → UNKNOWN fallback per-dashboard (not whole batch)
- [ ] T044 [US3] Create `frontend/src/components/llm/DatasetHealthTable.svelte`
- [ ] T045 [US3] Create `frontend/src/components/llm/ChartDataResults.svelte`
**Rejected-path regression test**:
- [ ] T047 [US3] Add test: Path B batch of 5 dashboards, 1 with KXD error → only that dashboard UNKNOWN, other 4 preserved with valid results, in `backend/tests/plugins/llm_analysis/test_path_b_batch.py`
- [ ] T047a [US3] Add test: batch_size=1 (full isolation) → each dashboard gets individual LLM call with no cross-contamination, in `backend/tests/plugins/llm_analysis/test_path_b_isolated.py`
**Verification**: Create task with 5 Path B dashboards, `llm_batch_size=1` (default) → 5 individual LLM calls, full isolation. Opt-in `llm_batch_size=3` → 2 LLM calls, structured prompt with per-dashboard isolation. Dashboard with broken dataset → only its record UNKNOWN, others preserved.
---
## Phase 5: US3 — Path A Execution (Multi-Chunk Screenshots)
**Goal**: Per-tab screenshots + multimodal LLM + payload estimation.
- [ ] T048 [US3] Implement `DashboardValidationPlugin._execute_path_a(params, context)` in `backend/src/plugins/llm_analysis/plugin.py`:
1. Parse URL filters if source is URL type (re-parse per FR-057)
2. `ScreenshotService.capture_dashboard_chunks()` → per-tab screenshots
3. `GET /api/v1/log/` → execution logs
4. `LLMClient._estimate_payload_size()` → check token budget per FR-056
5. Quality reduction loop if needed (FR-056)
6. `LLMClient.analyze_dashboard_multimodal()` → JSON result
7. Fallback to `_execute_path_b()` if payload still exceeds limit
8. Persist `ValidationRecord` with `execution_path="screenshot"`, `tab_screenshots`, `token_usage`
- **Belief-runtime**: `belief_scope("execute_path_a")` + `reason()` before payload reduction + `reflect()` after each reduction + `reason()` on Path B fallback
- [ ] T049 [US3] Update `frontend/src/routes/validation-tasks/[policyId]/runs/[runId]/+page.svelte` — add Path A sections: tab selector (from `tab_screenshots`), per-tab image viewer with zoom, issues table with tab/chart location. Legacy `/reports/llm/[taskId]` is a redirect-only passthrough — NOT updated.
**Rejected-path regression test**:
- [ ] T050 [US3] Add test: multi-chunk screenshots exceed token limit → quality reduction triggered → Path B fallback when still exceeded, logged via molecular-cot, in `backend/tests/plugins/llm_analysis/test_payload_reduction.py`
**Verification**: Create task with `screenshot_enabled=true`, 5-tab dashboard → report shows per-tab screenshots + issues.
---
## Phase 6: US4 — URL Parsing Integration
**Goal**: Paste Superset URL → parse → use in task creation + execution.
- [ ] T051 [US4] Implement `POST /api/validation-tasks/parse-url` in `backend/src/api/routes/validation_tasks.py`:
- Receives `{url, environment_id}`
- Calls `SupersetContextExtractor.parse_superset_link(url)`
- Returns `{dashboard_id, title, native_filters, activeTabs, anchor, partial_recovery, warnings}`
- [ ] T052 [US4] Create `frontend/src/components/llm/UrlParser.svelte`:
- URL paste input with debounced parse
- Loading state during API call
- Success: green banner with dashboard title, native_filters count, activeTabs list
- Error: red banner with `partial_recovery` warnings or parse failure message
- Copy button: «Dashboard structure changed? Paste new URL»
- [ ] T053 [US4] Integrate `UrlParser` into `ValidationTaskForm.svelte` — add «Paste URL» tab in source selector (Step 2)
- [ ] T054 [US4] Implement URL re-parse on execution per FR-057 in `DashboardValidationPlugin`:
- For URL-type sources: call `SupersetContextExtractor.parse_superset_link()` on every run
- If fails → mark source `is_valid=false`, set `last_error`, log WARN
- If dashboard_id changed → log WARN, use new ID
**Rejected-path regression test**:
- [ ] T055 [US4] Add test: URL source with deleted dashboard → re-parse fails → source marked invalid, error logged, task skips source and continues with other valid ones, in `backend/tests/plugins/llm_analysis/test_url_reparse.py`
**Verification**: Paste Superset URL with filters → form shows dashboard title + 3 native filters + 2 active tabs → create task → execution re-parses URL successfully.
---
## Phase 7: US5 — Provider/Prompt Per-Task + Settings Cleanup
**Goal**: Remove `dashboard_validation` binding from LLM settings. Provider + prompt configured per-task.
- [ ] T056 [US5] Remove `"dashboard_validation": ""` from `DEFAULT_LLM_PROVIDER_BINDINGS` in `backend/src/services/llm_prompt_templates.py`
- [ ] T057 [US5] Remove dashboard_validation binding UI row from `frontend/src/routes/admin/settings/llm/+page.svelte`
- [ ] T058 [US5] Update LLM settings API schema to exclude `dashboard_validation` from provider_bindings payload in `backend/src/schemas/llm.py`
- [ ] T059 [US5] Implement provider selector filter logic in `ValidationTaskForm.svelte` (Step 3):
- `screenshot_enabled=true` → show only multimodal providers (`is_multimodal=true`)
- `screenshot_enabled=false` → show all active providers
- [ ] T060 [US5] Add `ValidationTaskService.create_task()` validation: reject if `screenshot_enabled=true` but provider `is_multimodal=false`
**Rejected-path regression test**:
- [ ] T061 [US5] Add test: create task with `screenshot_enabled=true` and non-multimodal provider → HTTP 400, in `backend/tests/api/test_validation_tasks_api.py`
- [ ] T061a [US5] Implement `DELETE /api/llm/providers/{id}` enforcement: query active ValidationPolicy references, return 409 with `{error, blocking_tasks: [{id, name}]}` (FR-058), in `backend/src/api/routes/llm.py`
- [ ] T062 [US5] Add test: delete provider with active tasks → HTTP 409 with blocking task list (FR-058), in `backend/tests/api/test_llm_provider_delete.py`
- [ ] T063 [US5] Add test: POST /api/tasks with `plugin_id=llm_dashboard_validation` → HTTP 400 with migration message (FR hard cut), in `backend/tests/api/test_tasks_deprecated_llm.py`
**Verification**: LLM settings page shows only 3 bindings. Create task with multimodal provider → works. Create with non-multimodal + screenshot → rejected.
---
## Phase 8: US6 — Dashboard Hub Cleanup + Validation Indicator
**Goal**: Remove Validate button + LLM status column. Add lightweight validation indicator per dashboard row + detail page history.
- [ ] T064 [US6] Remove `handleValidate()` function from `frontend/src/routes/dashboards/+page.svelte`
- [ ] T065 [US6] Remove LLM validation status column from dashboard hub grid (column header + `getLlmSummaryLabel()` + `getValidationBadgeClass()` rendering) in `frontend/src/routes/dashboards/+page.svelte`
- [ ] T065a [US6] Implement `GET /api/dashboards/{env_id}/validation-status` in `backend/src/api/routes/dashboards.py` — batch endpoint accepting `dashboard_ids` list, returns `{dashboard_id: {status, last_run_at, task_name, run_id}}` per dashboard
- [ ] T065b [US6] Add validation indicator dot (PASS/WARN/FAIL/UNKNOWN) next to dashboard title in hub rows. Click opens dropdown with last 3 runs (date, aggregate status across all dashboards in that run, task name) + «View all» link → `/validation-tasks/{policyId}/runs/{runId}`, in `frontend/src/routes/dashboards/+page.svelte`
- [ ] T066 [US6] Remove «Validate» action from action dropdown menu in `frontend/src/routes/dashboards/+page.svelte`
- [ ] T067 [US6] Deprecate (comment out) `normalizeValidationStatus()` + `getLlmSummaryLabel()` — replaced by indicator + batch endpoint, in `frontend/src/routes/dashboards/+page.svelte`
- [ ] T068 [US6] Remove LLM validation status loading from `frontend/src/routes/dashboards/[id]/+page.svelte` (old `runLlmValidationTask()` + `openLlmReport()`)
- [ ] T068a [US6] Add «Validation History» section to `frontend/src/routes/dashboards/[id]/+page.svelte` — last 10 runs for this dashboard across all tasks, links to canonical run detail pages
- [ ] T069 [US6] Add «Create Validation Task» button to dashboard hub toolbar that navigates to `/validation-tasks/new?ids=1,2,3` (pre-fills selected dashboard IDs) in `frontend/src/routes/dashboards/+page.svelte`
- [ ] T070 [US6] Update `frontend/src/lib/components/layout/TaskDrawer.svelte` — ensure LLM validation tasks still show correct status after refactor
**Verification**: Dashboard hub has no Validate button, no LLM column. Colored dot indicator per row with dropdown. Detail page has Validation History section. «Create Validation Task» button pre-fills IDs.
---
## Phase 9: Polish & Cross-Cutting Verification
**Goal**: Migration testing, backward compat, ADR updates, final verification.
### 9.1 Migration & Backward Compat
- [ ] T071 Run data migration on test DB: verify all existing `dashboard_ids``sources[]` conversion in `backend/tests/migrations/test_v2_data_migration.py`
- [ ] T072 Update assistant tool `_tool_llm_validation.py` — replace ad-hoc `run_llm_validation` dispatch with task creation via `POST /api/validation-tasks`, in `backend/src/api/routes/assistant/_tool_llm_validation.py`
### 9.2 ADR Updates
- [ ] T073 Update `docs/adr/ADR-0008-assistant-tool-registry.md` — note that `_tool_llm_validation.py` now creates tasks instead of ad-hoc validation
- [ ] T074 Update `docs/adr/ADR-0004-plugin-architecture.md` — mention v2 dual-path + task-based flow as plugin evolution example
### 9.3 Final Verification
- [ ] T075 Run full end-to-end: create task → URL source (with filters) → Path A run → view report with per-tab screenshots
- [ ] T076 Run full end-to-end: create task → dashboard ID sources → Path B run (execute_chart_data=true) → view report with dataset health + chart data
- [ ] T077 Run full end-to-end: create task → Path B run against dashboard with broken dataset → verify LLM reports UNHEALTHY
- [ ] T078 Run full end-to-end: delete provider used by active task → verify HTTP 409 with blocking task names
- [ ] T079 Run full test suite: `cd backend && source .venv/bin/activate && python -m pytest -v`
- [ ] T080 Run frontend tests: `cd frontend && npm run test`
- [ ] T081 Run backend lint: `cd backend && python -m ruff check .`
- [ ] T082 Run frontend lint: `cd frontend && npm run lint`
- [ ] T083 Run semantic audit: `axiom_semantic_validation audit_belief_protocol` on all new C4/C5 contracts
- [ ] T084 Run semantic audit: `axiom_semantic_validation audit_contracts` on `backend/src/plugins/llm_analysis/` and `backend/src/services/validation_service.py`
---
## Verification Criteria per User Story
| Story | Independent Verification |
|-------|-------------------------|
| **US1** | Create task with 2 dashboards (ID + URL), Path A, cron `0 8 * * *` → task appears in list → scheduled execution succeeds → report shows per-tab screenshots |
| **US2** | Task list shows 3 tasks with different statuses → click task → detail shows per-dashboard breakdown → click dashboard → full report renders |
| **US3** | Path B against broken dataset → report shows dataset UNHEALTHY + affected charts. Path A against dashboard with visual error → report shows issue with tab/chart location |
| **US4** | Paste Superset URL with native_filters → parse returns dashboard title + 3 filters + 2 tabs → saved in ValidationSource → re-parsed on execution |
| **US5** | LLM settings shows 3 bindings (no dashboard_validation). Create task with multimodal provider → works. Create with non-multimodal + screenshot → rejected |
| **US6** | Dashboard hub: no Validate button, no LLM column. «Create Validation Task» toolbar button pre-fills selected IDs |
## Rejected-Path Regression Coverage
| Rejected Path | Test Task | Expected Behavior |
|---------------|-----------|-------------------|
| Old `POST /api/tasks` with `llm_dashboard_validation` | T063 | HTTP 400: «deprecated, use /api/validation-tasks» |
| Provider deletion when used by active tasks | T062 | HTTP 409: returns blocking task names |
| Duplicate manual trigger of same task | T013 (FR-054 check) | Toast «Already running», no queue |
| Non-multimodal provider + screenshot task | T061 | HTTP 400: «provider must be multimodal» |
| URL re-parse fails (dashboard deleted) | T055 | Source marked invalid, task skips, continues with other sources |
| Multi-chunk token limit exceeded | T050 | Quality reduction → Path B fallback, logged via molecular-cot |
| Path B dataset health check fails | T047 | LLM report flags dataset UNHEALTHY + affected charts |
## Belief-Runtime Instrumentation Tasks
| Contract | Tier | Task | Instrumentation |
|----------|------|------|-----------------|
| `LLMClient._estimate_payload_size` + `_reduce_image_quality` | C4 | T020 | `belief_scope("payload_reduction")` + `reason()` per reduction + `reflect()` on outcome |
| `DashboardValidationPlugin._execute_path_a` | C4 | T048 | `belief_scope("execute_path_a")` + `reason()` at navigation/tab/resize + `reflect()` with timings |
| `DashboardValidationPlugin._execute_path_b` | C4 | T043 | `belief_scope("execute_path_b")` + `reason()` at each API call + `reflect()` with token_usage |
| `ValidationTaskService.trigger_run` | C4 | T013 | `belief_scope("trigger_run")` + `reason()` on queue decision + `reflect()` after enqueue |
| `ScreenshotService.capture_dashboard_chunks` | C4 | T017 | `belief_scope("capture_chunks")` + `reason()` per tab switch + `reflect()` with screenshot count |
## Parallel Execution Opportunities
| Group | Tasks | Dependencies |
|-------|-------|--------------|
| **Phase 1** | T001T007 | None (all parallel) |
| **Phase 2** | T008T015 (service), T016 (health checker), T017 (screenshot), T018T020 (LLM client) | After Phase 1 |
| **Phase 3** | T025T034 (API), T035T042 (frontend) | After Phase 2 |
| **Phase 4+5+6** | T043T047 (Path B), T048T050 (Path A), T051T055 (URL) | After Phase 2, all three parallel |
| **Phase 7+8** | T056T063 (provider/prompt), T064T070 (hub) | After Phase 3, both parallel |
| **Phase 9** | T071T084 | After all phases |
## Total: 98 tasks across 9 phases
| Phase | Tasks | User Stories |
|-------|-------|-------------|
| 1 — Setup | 8 | — |
| 2 — Foundational | 23 | — |
| 3 — Task CRUD + Reports | 22 | US1, US2 |
| 4 — Path B | 5 | US3 |
| 5 — Path A | 3 | US3 |
| 6 — URL Parsing | 5 | US4 |
| 7 — Provider/Prompt | 8 | US5 |
| 8 — Hub Cleanup | 10 | US6 |
| 9 — Polish | 14 | All |