diff --git a/README.md b/README.md
index b2ba835..67e1179 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,8 @@ MVP supports image-only detection:
- Upload one JPG, PNG, or WebP image.
- Store the file and image metadata.
- Create a detection task in a Java backend.
-- Call a Python model service for real inference.
+- Call a versioned Python model-service contract. Local development uses the
+ heuristic runtime; real weights and CUDA verification are server-pending.
- Store model output, threshold, version, latency, and image hash.
- Generate a report and detection history.
- Show model health and registry information.
@@ -28,6 +29,10 @@ Out of scope for MVP:
- User billing, tenants, RBAC, or complex audit workflows.
- Custom model training or stacking meta-learners.
+Video and expert-ensemble visuals are development showcases, not formal
+product capabilities. See the [product capability matrix](docs/capability-matrix.md)
+for implementation evidence and deferred boundaries.
+
## Architecture
```text
diff --git a/docs/README.md b/docs/README.md
index 607d2c3..a9044ec 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -9,10 +9,15 @@ Workbench.
commands.
- [Documentation Standards](documentation-standards.md): how this repository
writes and maintains technical documentation.
+- [Architecture Decision Records](adr/README.md): durable cross-cutting
+ decisions, alternatives, and consequences.
- [Project Worklog](project-worklog.md): what has been built, why it was built,
how it was verified, and what is deferred.
-- [Improvement Roadmap](project-improvement-roadmap.md): strict interviewer-style
- gap analysis and the recommended next phases.
+- [Product Capability Matrix](capability-matrix.md): implemented, server-pending,
+ showcase, and non-goal capabilities with verification evidence.
+- [Historical Improvement Roadmap](project-improvement-roadmap.md): superseded
+ early gap analysis retained for development history; use the active long-term
+ roadmap below for current decisions.
## Runbooks
diff --git a/docs/adr/0001-record-architecture-decisions.md b/docs/adr/0001-record-architecture-decisions.md
new file mode 100644
index 0000000..84e4315
--- /dev/null
+++ b/docs/adr/0001-record-architecture-decisions.md
@@ -0,0 +1,36 @@
+# ADR-0001: Record Cross-Cutting Architecture Decisions
+
+- Status: Accepted
+- Date: 2026-07-11
+- Owners: Project maintainers
+
+## Context
+
+Specifications explain complete product or feature designs, while the worklog
+records branch chronology. Neither gives a concise, immutable answer to why a
+cross-cutting architecture choice exists or what alternatives were rejected.
+
+## Decision
+
+Record durable cross-cutting choices as numbered ADRs under `docs/adr/`. Keep
+each ADR focused on one decision. Supersede accepted decisions with a new ADR
+instead of rewriting project history.
+
+## Consequences
+
+- Reviewers can trace architecture from rationale to implementation commits.
+- Later maintainers can distinguish constraints from accidental code shape.
+- Feature specifications remain readable instead of becoming decision logs.
+- Every meaningful decision adds a small documentation maintenance cost.
+
+## Alternatives Considered
+
+- Use commit messages only: rejected because commits describe changes but do
+ not consistently preserve alternatives and long-term consequences.
+- Put every decision in one architecture document: rejected because updates
+ erase chronology and create a large, difficult review surface.
+
+## Verification
+
+`docs/documentation-standards.md` defines when an ADR is required, and
+`docs/adr/README.md` provides the lifecycle, template, and index.
diff --git a/docs/adr/README.md b/docs/adr/README.md
new file mode 100644
index 0000000..2bfa068
--- /dev/null
+++ b/docs/adr/README.md
@@ -0,0 +1,36 @@
+# Architecture Decision Records
+
+This directory stores short, durable records for cross-cutting architecture
+decisions. Product and feature requirements remain in `docs/superpowers/specs/`.
+
+## Lifecycle
+
+- `Proposed`: under review and not yet binding.
+- `Accepted`: current decision.
+- `Superseded`: replaced by a later ADR named in the record.
+
+Accepted ADR content is not rewritten when preferences change. Add a new ADR
+so readers can follow the project's evolution.
+
+## Template
+
+```markdown
+# ADR-NNNN: Decision Title
+
+- Status: Proposed | Accepted | Superseded
+- Date: YYYY-MM-DD
+- Owners: Project maintainers
+- Superseded by: ADR-NNNN (only when applicable)
+
+## Context
+## Decision
+## Consequences
+## Alternatives Considered
+## Verification
+```
+
+## Index
+
+| ADR | Status | Decision |
+| --- | --- | --- |
+| [ADR-0001](0001-record-architecture-decisions.md) | Accepted | Record durable cross-cutting decisions as ADRs. |
diff --git a/docs/capability-matrix.md b/docs/capability-matrix.md
new file mode 100644
index 0000000..0a405fa
--- /dev/null
+++ b/docs/capability-matrix.md
@@ -0,0 +1,48 @@
+# Product Capability Matrix
+
+- Status: Active
+- Owners: Project maintainers
+- Last reviewed: 2026-07-11
+
+This matrix is the reviewer-facing source of truth for product capability
+status. It mirrors `src/config/capabilities.ts` and prevents showcase visuals
+from being mistaken for implemented workflows.
+
+## Status Definitions
+
+| Status | Meaning |
+| --- | --- |
+| `Implemented` | Formal workflow backed by persisted state, code, and tests. |
+| `Server Pending` | Integration boundary exists, but server hardware or runtime evidence is missing. |
+| `Showcase` | Visual concept outside formal product navigation and claims. |
+| `Non-goal` | Deliberately outside the product boundary. |
+
+## Matrix
+
+| Capability | Status | Entry point | Implementation evidence | Verification |
+| --- | --- | --- | --- | --- |
+| Image detection | Implemented | `/detect/image` | Secure ingestion, PostgreSQL outbox, Redis worker, execution lease, persisted report | Java tests, frontend tests, smoke workflow |
+| Model evaluation | Implemented | `/admin/evaluations` | Evaluation runs, samples, metrics, retry state | Java evaluation tests and frontend insight tests |
+| Model registry | Implemented | `/admin/models` | Registry table, endpoint synchronization, health API | Model registry and controller tests |
+| Operational review view | Implemented | `/admin/review` | Read-only failed task, failed evaluation, and wrong-sample aggregation | Frontend build and backend APIs |
+| GPU model runtime | Server Pending | None | Runtime adapter and health contract exist; weights and CUDA evidence do not | Server deployment phase |
+| Video detection concept | Showcase | `/dev/showcase/video-detection` | Static interaction concept using fixture data | Frontend build only; not product evidence |
+| Expert and LoRA concepts | Showcase | `/dev/showcase/image-pipeline` | Static visual concept | Frontend build only; not product evidence |
+| Audio detection | Non-goal | None | None by design | Product boundary review |
+| Model training | Non-goal | None | Existing open-source model integration only | Product boundary review |
+
+The current operational review view is not yet the durable human-review
+workflow. `feature/review-case-workflow` will add claim, resolution, audit, and
+candidate-dataset behavior before that stronger claim is made.
+
+## Maintenance Rule
+
+A capability status change must update all of these in one pull request:
+
+1. `src/config/capabilities.ts` and its tests.
+2. This matrix.
+3. `README.md` when the core product claim changes.
+4. `docs/project-worklog.md` with implementation and verification evidence.
+
+Showcase animation, mock data, or screenshots cannot be used as evidence for
+an `Implemented` capability.
diff --git a/docs/documentation-standards.md b/docs/documentation-standards.md
index a6ad284..9f911df 100644
--- a/docs/documentation-standards.md
+++ b/docs/documentation-standards.md
@@ -40,6 +40,34 @@ Use one primary purpose per document.
Do not mix a tutorial, API reference, and design essay in the same document.
If a document starts doing two jobs, split it or link to another page.
+## Document Lifecycle
+
+Durable specifications and explanation documents must include this metadata
+immediately below the title:
+
+```markdown
+- Status: Draft | Active | Superseded | Historical
+- Owners: Project maintainers
+- Last reviewed: YYYY-MM-DD
+- Superseded by: `path/to/replacement.md` (only when applicable)
+```
+
+Lifecycle meanings:
+
+| Status | Meaning |
+| --- | --- |
+| `Draft` | Proposed content that is not yet the implementation baseline. |
+| `Active` | Current source of truth for implementation and review. |
+| `Superseded` | Replaced by a named newer document and retained for history. |
+| `Historical` | Records completed research or evolution but is not prescriptive. |
+
+Plans use checkboxes instead of lifecycle metadata. Worklog entries use
+chronology. Runbooks are active unless their heading explicitly says otherwise.
+
+When a document becomes superseded, update both documents and `docs/README.md`
+in the same pull request. Never silently delete an architectural decision that
+explains committed code.
+
## File Naming
- Use lowercase kebab-case: `project-worklog.md`.
@@ -160,6 +188,33 @@ Should work now.
For documentation-only branches, `git diff --check` is the minimum verification.
For code branches, run the relevant test or build command.
+## Maintenance Triggers
+
+The same pull request must update durable documentation when code changes any
+of these contracts:
+
+- public API request, response, status, or error behavior;
+- environment variable, default, deployment requirement, or health behavior;
+- formal capability status or user-facing workflow;
+- database or queue state machine and recovery procedure;
+- architecture boundary or dependency ownership;
+- model, dataset, metric, threshold, or performance claim.
+
+A review fails when code and its active durable documentation disagree. Small
+internal refactors that preserve every contract need only a worklog entry when
+they are architecturally meaningful.
+
+## Architecture Decision Records
+
+Use `docs/adr/` for concise cross-cutting decisions whose consequences outlive
+one feature branch. Use a specification for full product or feature design.
+
+- Number ADRs sequentially with four digits.
+- Accepted ADRs are immutable except for factual corrections.
+- Replace a decision by adding a new ADR and marking the old one superseded.
+- Link relevant ADRs from specifications and runbooks.
+- Record alternatives and consequences, not meeting history.
+
## Update Checklist
Before committing documentation changes:
@@ -169,6 +224,9 @@ Before committing documentation changes:
- [ ] Commands are copyable.
- [ ] Claims are backed by links, code references, or verification commands.
- [ ] Deferred work is explicit.
+- [ ] Lifecycle metadata and supersession links are correct where required.
+- [ ] API, configuration, capability, and state-machine changes updated their active docs.
+- [ ] A cross-cutting architecture decision has an ADR when needed.
- [ ] No model weights, uploads, generated reports, or local database files are
referenced as committed artifacts.
- [ ] `git diff --check` passes.
diff --git a/docs/project-worklog.md b/docs/project-worklog.md
index 1926f5d..2797477 100644
--- a/docs/project-worklog.md
+++ b/docs/project-worklog.md
@@ -737,27 +737,46 @@ Verification:
---
-## Next Recommended Work
-
-Add interview-visible operational observability:
+### 2026-07-11: Product Scope Truthfulness
```text
-feature/observability-correlation
+refactor/product-scope-truthfulness
```
-Scope:
+What changed:
-- Propagate a correlation id across HTTP requests, outbox events, Redis jobs,
- model calls, and persisted execution records.
-- Add Micrometer counters and timers for dispatch, retries, queue outcomes,
- inference latency, and upload rejection reasons.
-- Define structured logging fields without logging image bytes or model raw
- payloads.
-- Document local metrics endpoints and production exposure boundaries.
+- Established document lifecycle governance and architecture decision records.
+- Added a tested capability registry and reviewer-facing capability matrix.
+- Removed video detection from formal product navigation and moved retained
+ concepts under development showcase routes.
+- Removed fabricated confidence, evidence marks, timelines, and export success
+ behavior from the formal report page.
+- Corrected README model-runtime wording to distinguish implemented integration
+ from server-pending weights and CUDA evidence.
-Reason:
+Why:
+
+- Every formal product claim must map to persisted behavior and tests.
+- Showcase quality is useful, but it cannot substitute for backend capability.
+- Reviewers need one maintained place to distinguish current, deferred, and
+ deliberately excluded work.
+
+Verification:
+
+- Frontend policy and report-presentation tests increased the suite from 8 to
+ 18 passing tests before final cross-project verification.
+- Frontend lint and production build passed after route and report changes.
+
+---
+
+## Next Recommended Work
+
+Continue with the real evaluation execution boundary:
+
+```text
+feature/evaluation-real-execution-boundary
+```
-The project now handles failure and recovery, but operators cannot yet answer
-which request produced a task, where latency accumulated, or how often retries
-and security rejections occur. Correlated traces and bounded metrics turn the
-reliability features into an operable system.
+Replace the default CRC evaluation client, split model calls out of database
+transactions, and add durable evaluation execution ownership without requiring
+real model weights.
diff --git a/docs/superpowers/plans/2026-07-11-product-scope-truthfulness.md b/docs/superpowers/plans/2026-07-11-product-scope-truthfulness.md
new file mode 100644
index 0000000..b15b0a4
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-11-product-scope-truthfulness.md
@@ -0,0 +1,298 @@
+# Product Scope Truthfulness Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Ensure every formal frontend capability maps to implemented behavior while preserving unsupported visual concepts only as explicit development showcases.
+
+**Architecture:** A typed capability registry becomes the frontend source for formal and showcase status. Formal routes stop importing mock-only pages, the report renders only backend state, and a durable capability matrix mirrors the registry for reviewers.
+
+**Tech Stack:** React 18, TypeScript, React Router 7, Node test runner, CSS Modules, Markdown.
+
+## Global Constraints
+
+- Preserve the existing visual language and CSS Modules.
+- Formal product scope is image detection, evaluation, model registry, review, and operations.
+- Video and expert concepts remain available only under `/dev/showcase/*`.
+- No formal page may fabricate model scores, evidence, timestamps, or task state.
+- Do not add a frontend testing framework; keep policy logic testable with the existing Node runner.
+- Each task ends with a focused commit and updates this checklist as work progresses.
+
+---
+
+### Task 0: Documentation Lifecycle And Decision Records
+
+**Files:**
+- Modify: `docs/documentation-standards.md`
+- Modify: `docs/README.md`
+- Create: `docs/adr/README.md`
+- Create: `docs/adr/0001-record-architecture-decisions.md`
+
+**Interfaces:**
+- Produces: required document lifecycle values, supersession rules, ADR format,
+ and branch documentation checklist used by every later task.
+
+- [x] **Step 1: Define document lifecycle**
+
+Add `Draft`, `Active`, `Superseded`, and `Historical` definitions. Require each
+durable spec and explanation document to declare status, owner, last reviewed
+date, and `Superseded by` when applicable. Plans and worklog entries are exempt
+because their state is represented by checkboxes and chronology.
+
+- [x] **Step 2: Define maintenance triggers**
+
+Require the same PR to update documentation when it changes a public API,
+configuration variable, capability status, state machine, operational recovery
+procedure, architecture decision, or model claim. Define review failure when
+code and durable documentation disagree.
+
+- [x] **Step 3: Add the ADR index and first ADR**
+
+Use this exact ADR shape:
+
+```markdown
+# ADR-NNNN: Decision Title
+
+- Status: Accepted | Superseded
+- Date: YYYY-MM-DD
+- Owners: Project maintainers
+
+## Context
+## Decision
+## Consequences
+## Alternatives Considered
+## Verification
+```
+
+ADR-0001 records the decision to use short, immutable decision records for
+cross-cutting architecture choices while specs retain full product design.
+
+- [x] **Step 4: Verify and commit**
+
+Run: `git diff --check` and scan the new files for `TBD` or incomplete headings.
+
+Commit: `docs: establish documentation lifecycle governance`
+
+### Task 1: Typed Capability Registry
+
+**Files:**
+- Create: `src/config/capabilities.ts`
+- Create: `src/config/capabilities.test.ts`
+- Modify: `package.json`
+
+**Interfaces:**
+- Produces: `CapabilityStatus`, `ProductCapability`, `productCapabilities`, `formalCapabilityIds`, and `showcaseCapabilityIds`.
+- Consumed by: route and documentation tasks.
+
+- [x] **Step 1: Write the failing registry test**
+
+Test exact invariants:
+
+```ts
+assert.deepEqual(formalCapabilityIds, [
+ 'image-detection', 'evaluation', 'model-registry', 'review-queue',
+]);
+assert.equal(byId('video-detection').status, 'showcase');
+assert.equal(byId('real-model-runtime').status, 'server-pending');
+assert.equal(productCapabilities.some((item) => item.status === 'implemented' && !item.evidence.length), false);
+```
+
+Add `src/config/capabilities.test.ts` to the existing `npm test` command.
+
+- [x] **Step 2: Run the test and confirm RED**
+
+Run: `npm run test`
+
+Expected: TypeScript module resolution failure for `./capabilities.ts`.
+
+- [x] **Step 3: Implement the registry**
+
+Use these public types:
+
+```ts
+export type CapabilityStatus = 'implemented' | 'server-pending' | 'showcase' | 'non-goal';
+
+export interface ProductCapability {
+ id: string;
+ name: string;
+ status: CapabilityStatus;
+ formalRoute?: string;
+ showcaseRoute?: string;
+ evidence: readonly string[];
+}
+```
+
+Register image detection, evaluation, model registry, review queue, real model
+runtime, video detection, expert LoRA concepts, audio detection, and model
+training. Only implemented entries may expose `formalRoute`.
+
+- [x] **Step 4: Verify and commit**
+
+Run: `npm run test`
+
+Expected: all frontend tests pass.
+
+Commit: `test: define product capability contract`
+
+### Task 2: Formal And Showcase Route Separation
+
+**Files:**
+- Modify: `src/App.tsx`
+- Modify: `src/pages/DetectChoice/index.tsx`
+- Modify: `src/pages/DetectChoice/DetectChoice.module.css`
+- Create: `src/config/routePolicy.ts`
+- Create: `src/config/routePolicy.test.ts`
+- Modify: `package.json`
+
+**Interfaces:**
+- Consumes: capability ids and route fields from Task 1.
+- Produces: `formalRoutes`, `showcaseRoutes`, and `isFormalRoute(path)`.
+
+- [x] **Step 1: Write failing route-policy tests**
+
+```ts
+assert.equal(isFormalRoute('/detect/image'), true);
+assert.equal(isFormalRoute('/detect/video'), false);
+assert.equal(isFormalRoute('/dev/showcase/video-detection'), false);
+assert.equal(formalRoutes.some((route) => route.includes('showcase')), false);
+```
+
+- [x] **Step 2: Verify RED and implement policy**
+
+Run: `npm run test`
+
+Expected: missing `routePolicy.ts`.
+
+Implement immutable formal and showcase route lists. Do not parse route intent
+from display labels.
+
+- [x] **Step 3: Update application routes**
+
+- Remove the formal `/detect/video` component route.
+- Redirect `/detect/video` to `/detect` for old bookmarks.
+- Move video and image visual concepts to
+ `/dev/showcase/video-detection` and `/dev/showcase/image-pipeline`.
+- Remove `pipeline/showcase/*` from the nested admin route tree.
+- Remove the video card and `FilmReel` import from `DetectChoice`.
+- Let the image card occupy the existing constrained layout without changing
+ its typography, colors, motion, or radius system.
+
+- [x] **Step 4: Verify and commit**
+
+Run: `npm run test && npm run lint && npm run build`
+
+Expected: all commands pass and production output contains no formal
+`/detect/video` lazy route.
+
+Commit: `refactor: separate formal and showcase routes`
+
+### Task 3: Evidence-Only Detection Report
+
+**Files:**
+- Modify: `src/pages/Report/index.tsx`
+- Modify: `src/pages/Report/Report.module.css`
+- Create: `src/pages/Report/reportPresentation.ts`
+- Create: `src/pages/Report/reportPresentation.test.ts`
+- Modify: `package.json`
+
+**Interfaces:**
+- Produces: `buildReportPresentation(detail)` returning verdict, confidence,
+ evidence rows, and timeline rows derived only from `DetectionDetailResponse`.
+
+- [x] **Step 1: Write failing presentation tests**
+
+Cover these exact rules:
+
+```ts
+assert.equal(buildReportPresentation(null).state, 'unavailable');
+assert.deepEqual(buildReportPresentation(completedWithoutPredictions).evidence, []);
+assert.equal(buildReportPresentation(failedTask).verdict, 'FAILED');
+assert.equal(buildReportPresentation(realCompletedTask).confidence, 0.86);
+```
+
+- [x] **Step 2: Verify RED and implement the mapper**
+
+Run: `npm run test`
+
+Expected: missing `reportPresentation.ts`.
+
+The mapper must not import `src/data/mocks.ts`. Empty backend evidence remains
+empty; it does not synthesize anomaly marks.
+
+- [x] **Step 3: Refactor the report page**
+
+- Remove `imageDemo`, static fake timeline, and synthetic evidence fallbacks.
+- Require a non-demo route id and load backend data.
+- Render a stable loading state before the request completes.
+- Render the existing error treatment when neither task nor report resolves.
+- Use the session preview only when it belongs to the current task.
+- When no preview exists, render a metadata placeholder rather than a sample
+ image.
+- Disable PDF/archive commands or label them unavailable until a backend export
+ contract exists; do not show a false success toast.
+
+- [x] **Step 4: Verify and commit**
+
+Run: `npm run test && npm run lint && npm run build`
+
+Commit: `refactor: remove fabricated report evidence`
+
+### Task 4: Capability Matrix And Documentation Governance
+
+**Files:**
+- Create: `docs/capability-matrix.md`
+- Modify: `README.md`
+- Modify: `docs/README.md`
+- Modify: `docs/project-worklog.md`
+- Modify: `docs/superpowers/plans/2026-07-11-product-scope-truthfulness.md`
+
+**Interfaces:**
+- Consumes: the capability ids and statuses from Task 1.
+- Produces: the reviewer-facing source of capability truth.
+
+- [x] **Step 1: Write the matrix**
+
+For each registry entry, record status, user entry point, implementation
+evidence, verification command, and next phase. Include a maintenance rule:
+changing a status requires registry tests, this matrix, README, and worklog in
+the same PR.
+
+- [x] **Step 2: Reconcile overview claims**
+
+- Replace “call a Python model service for real inference” with wording that
+ distinguishes implemented service integration from server-pending weights.
+- Link the capability matrix near the README scope.
+- State that video visuals are showcases and not a product capability.
+- Add the completed branch entry and exact verification evidence to worklog.
+
+- [x] **Step 3: Complete plan checkboxes and verify docs**
+
+Run:
+
+```powershell
+git diff --check
+rg -n "TBD|TODO" docs/capability-matrix.md README.md
+```
+
+Expected: no whitespace errors or placeholders.
+
+Commit: `docs: publish product capability matrix`
+
+### Task 5: Final Verification And Delivery
+
+**Files:** No new production files.
+
+- [x] **Step 1: Run complete local verification**
+
+Run frontend tests, lint, build, Java tests, model-service tests, smoke tests,
+`npm audit --audit-level=low`, and `git diff --check`.
+
+- [x] **Step 2: Review the branch as an interviewer**
+
+Confirm formal navigation exposes no mock-only capability, formal report code
+does not import mocks, and the matrix links every implemented claim to code or
+tests.
+
+- [ ] **Step 3: Push and open PR**
+
+Push `refactor/product-scope-truthfulness`, open a ready PR referencing roadmap
+PR #27, wait for all CI jobs, squash merge, and delete the remote branch.
diff --git a/docs/superpowers/specs/2026-07-11-ai-application-product-roadmap-design.md b/docs/superpowers/specs/2026-07-11-ai-application-product-roadmap-design.md
index fb5dd4b..6267b5f 100644
--- a/docs/superpowers/specs/2026-07-11-ai-application-product-roadmap-design.md
+++ b/docs/superpowers/specs/2026-07-11-ai-application-product-roadmap-design.md
@@ -1,5 +1,9 @@
# AI Application Product Boundary And Long-Term Roadmap
+- Status: Active
+- Owners: Project maintainers
+- Last reviewed: 2026-07-11
+
## 1. Purpose
This specification defines the durable product boundary and delivery order for
diff --git a/package.json b/package.json
index f4b7a2d..4242ea0 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
- "test": "node --test --experimental-strip-types src/api/errorMessage.test.ts src/pages/AdminEvaluations/evaluationInsights.test.ts",
+ "test": "node --test --experimental-strip-types src/api/errorMessage.test.ts src/config/capabilities.test.ts src/config/routePolicy.test.ts src/pages/AdminEvaluations/evaluationInsights.test.ts src/pages/Report/reportPresentation.test.ts",
"preview": "vite preview"
},
"dependencies": {
diff --git a/src/App.tsx b/src/App.tsx
index 192f2b3..c9d015e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -14,7 +14,6 @@ const AdminDetections = lazy(() => import('@/pages/AdminDetections').then((modul
const AdminEvaluations = lazy(() => import('@/pages/AdminEvaluations').then((module) => ({ default: module.AdminEvaluations })));
const AdminModels = lazy(() => import('@/pages/AdminModels').then((module) => ({ default: module.AdminModels })));
const AdminReview = lazy(() => import('@/pages/AdminReview').then((module) => ({ default: module.AdminReview })));
-const VideoShowcase = lazy(() => import('@/pages/AdminPipeline/VideoShowcase').then((module) => ({ default: module.VideoShowcase })));
const ImageShowcase = lazy(() => import('@/pages/AdminPipeline/ImageShowcase').then((module) => ({ default: module.ImageShowcase })));
const Dev = lazy(() => import('@/pages/Dev').then((module) => ({ default: module.Dev })));
const NotFound = lazy(() => import('@/pages/NotFound/NotFound').then((module) => ({ default: module.NotFound })));
@@ -36,7 +35,7 @@ function AnimatedRoutes() {
} />
} />
} />
- } />
+ } />
} />
}>
} />
@@ -45,12 +44,12 @@ function AnimatedRoutes() {
} />
} />
} />
- } />
- } />
} />
} />
} />
+ } />
+ } />
} />
diff --git a/src/config/capabilities.test.ts b/src/config/capabilities.test.ts
new file mode 100644
index 0000000..115ca7d
--- /dev/null
+++ b/src/config/capabilities.test.ts
@@ -0,0 +1,42 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import {
+ formalCapabilityIds,
+ productCapabilities,
+ showcaseCapabilityIds,
+} from './capabilities.ts';
+
+function byId(id: string) {
+ const capability = productCapabilities.find((item) => item.id === id);
+ assert.ok(capability, `Missing capability: ${id}`);
+ return capability;
+}
+
+test('formal capabilities expose only implemented product workflows', () => {
+ assert.deepEqual(formalCapabilityIds, [
+ 'image-detection',
+ 'evaluation',
+ 'model-registry',
+ 'review-queue',
+ ]);
+ assert.equal(formalCapabilityIds.every((id) => byId(id).status === 'implemented'), true);
+});
+
+test('unsupported visual concepts remain explicit showcases', () => {
+ assert.equal(byId('video-detection').status, 'showcase');
+ assert.equal(byId('expert-lora').status, 'showcase');
+ assert.deepEqual(showcaseCapabilityIds, ['video-detection', 'expert-lora']);
+});
+
+test('server and non-goal boundaries remain explicit', () => {
+ assert.equal(byId('real-model-runtime').status, 'server-pending');
+ assert.equal(byId('audio-detection').status, 'non-goal');
+ assert.equal(byId('model-training').status, 'non-goal');
+});
+
+test('every implemented capability links to evidence', () => {
+ assert.equal(
+ productCapabilities.some((item) => item.status === 'implemented' && item.evidence.length === 0),
+ false,
+ );
+});
diff --git a/src/config/capabilities.ts b/src/config/capabilities.ts
new file mode 100644
index 0000000..7f3f648
--- /dev/null
+++ b/src/config/capabilities.ts
@@ -0,0 +1,91 @@
+export type CapabilityStatus = 'implemented' | 'server-pending' | 'showcase' | 'non-goal';
+
+export interface ProductCapability {
+ id: string;
+ name: string;
+ status: CapabilityStatus;
+ description: string;
+ formalRoute?: string;
+ showcaseRoute?: string;
+ evidence: readonly string[];
+}
+
+export const productCapabilities = [
+ {
+ id: 'image-detection',
+ name: 'Image detection',
+ status: 'implemented',
+ description: 'Validated image ingestion, durable dispatch, model-service execution, and reports.',
+ formalRoute: '/detect/image',
+ evidence: ['backend-java detection workflow', 'Redis outbox worker', 'frontend image detection flow'],
+ },
+ {
+ id: 'evaluation',
+ name: 'Model evaluation',
+ status: 'implemented',
+ description: 'Persisted evaluation runs, sample results, aggregate metrics, and error inspection.',
+ formalRoute: '/admin/evaluations',
+ evidence: ['evaluation_run and evaluation_sample tables', 'evaluation backend tests', 'admin evaluation UI'],
+ },
+ {
+ id: 'model-registry',
+ name: 'Model registry',
+ status: 'implemented',
+ description: 'Registered model endpoints, versions, thresholds, weights, and health checks.',
+ formalRoute: '/admin/models',
+ evidence: ['model_registry table', 'model registry service tests', 'admin model UI'],
+ },
+ {
+ id: 'review-queue',
+ name: 'Operational review view',
+ status: 'implemented',
+ description: 'Read-only aggregation of failed tasks, failed evaluations, and incorrect samples.',
+ formalRoute: '/admin/review',
+ evidence: ['AdminReview backend integration', 'detection failure records', 'evaluation sample results'],
+ },
+ {
+ id: 'real-model-runtime',
+ name: 'GPU model runtime',
+ status: 'server-pending',
+ description: 'The runtime adapter exists; real weights, CUDA, and benchmark evidence require the server.',
+ evidence: [],
+ },
+ {
+ id: 'video-detection',
+ name: 'Video detection concept',
+ status: 'showcase',
+ description: 'Visual interaction concept without a production backend or model contract.',
+ showcaseRoute: '/dev/showcase/video-detection',
+ evidence: [],
+ },
+ {
+ id: 'expert-lora',
+ name: 'Expert and LoRA concepts',
+ status: 'showcase',
+ description: 'Visual model-ensemble concepts that are not formal product capabilities.',
+ showcaseRoute: '/dev/showcase/image-pipeline',
+ evidence: [],
+ },
+ {
+ id: 'audio-detection',
+ name: 'Audio detection',
+ status: 'non-goal',
+ description: 'Outside the image-only product boundary.',
+ evidence: [],
+ },
+ {
+ id: 'model-training',
+ name: 'Model training',
+ status: 'non-goal',
+ description: 'The project integrates proven models and does not claim original model training.',
+ evidence: [],
+ },
+] as const satisfies readonly ProductCapability[];
+
+export const formalCapabilityIds = productCapabilities
+ .filter((capability) => capability.status === 'implemented')
+ .map((capability) => capability.id);
+
+export const showcaseCapabilityIds = productCapabilities
+ .filter((capability) => capability.status === 'showcase')
+ .map((capability) => capability.id);
diff --git a/src/config/routePolicy.test.ts b/src/config/routePolicy.test.ts
new file mode 100644
index 0000000..8b814a9
--- /dev/null
+++ b/src/config/routePolicy.test.ts
@@ -0,0 +1,17 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { formalRoutes, isFormalRoute, showcaseRoutes } from './routePolicy.ts';
+
+test('formal routes contain only implemented product workflows', () => {
+ assert.equal(isFormalRoute('/detect/image'), true);
+ assert.equal(isFormalRoute('/detect/video'), false);
+ assert.equal(formalRoutes.some((route) => route.includes('showcase')), false);
+});
+
+test('showcase routes remain outside formal product scope', () => {
+ assert.equal(isFormalRoute('/dev/showcase/video-detection'), false);
+ assert.deepEqual(showcaseRoutes, [
+ '/dev/showcase/video-detection',
+ '/dev/showcase/image-pipeline',
+ ]);
+});
diff --git a/src/config/routePolicy.ts b/src/config/routePolicy.ts
new file mode 100644
index 0000000..54b1c04
--- /dev/null
+++ b/src/config/routePolicy.ts
@@ -0,0 +1,13 @@
+import { productCapabilities } from './capabilities.ts';
+
+export const formalRoutes = productCapabilities
+ .filter((capability) => capability.status === 'implemented' && 'formalRoute' in capability)
+ .map((capability) => capability.formalRoute);
+
+export const showcaseRoutes = productCapabilities
+ .filter((capability) => capability.status === 'showcase' && 'showcaseRoute' in capability)
+ .map((capability) => capability.showcaseRoute);
+
+export function isFormalRoute(path: string) {
+ return formalRoutes.some((route) => path === route || path.startsWith(`${route}/`));
+}
diff --git a/src/pages/DetectChoice/DetectChoice.module.css b/src/pages/DetectChoice/DetectChoice.module.css
index 23fbd42..0d3e1c4 100644
--- a/src/pages/DetectChoice/DetectChoice.module.css
+++ b/src/pages/DetectChoice/DetectChoice.module.css
@@ -65,14 +65,6 @@
stroke-dashoffset: 0;
}
-.videoIcon {
- transition: transform 700ms ease-out;
-}
-
-.choiceCard:hover .videoIcon {
- transform: translateY(-4px) scale(1.04);
-}
-
.center {
display: grid;
gap: var(--space-2);
diff --git a/src/pages/DetectChoice/index.tsx b/src/pages/DetectChoice/index.tsx
index a5d61d6..c73da79 100644
--- a/src/pages/DetectChoice/index.tsx
+++ b/src/pages/DetectChoice/index.tsx
@@ -1,6 +1,6 @@
import { Link } from 'react-router-dom';
import { PageContainer } from '@/components/primitives';
-import { FilmReel, ImageFrame } from '@/components/icons';
+import { ImageFrame } from '@/components/icons';
import { UserTopbar } from '@/components/UserTopbar/UserTopbar';
import styles from './DetectChoice.module.css';
@@ -9,34 +9,23 @@ export function DetectChoice() {
-
- ─ Choose the material to develop ─
- 请选择待检材料
-
-
-
-
- 图片
- IMAGE
-
-
- 一帧一帧地显影
- ─ Frame by frame
-
-
-
-
-
- 视频
- VIDEO
-
-
- 沿着时间显影
- ─ Along the time
-
-
-
-
+
+ - Choose the material to develop -
+ 请选择待检测材料
+
+
+
+
+ 图片
+ IMAGE
+
+
+ 单张图片真实性分析
+ - Image authenticity analysis
+
+
+
+
);
diff --git a/src/pages/Report/Report.module.css b/src/pages/Report/Report.module.css
index 4c12263..06239a1 100644
--- a/src/pages/Report/Report.module.css
+++ b/src/pages/Report/Report.module.css
@@ -197,6 +197,16 @@
border: 1px solid var(--rule);
}
+.mediaPlaceholder {
+ display: grid;
+ min-height: 240px;
+ place-items: center;
+ border: 1px solid var(--rule);
+ color: var(--ink-3);
+ font-family: var(--font-mono);
+ font-size: var(--text-sm);
+}
+
.meta {
display: grid;
grid-template-columns: 80px 1fr;
diff --git a/src/pages/Report/index.tsx b/src/pages/Report/index.tsx
index d97b06c..21d9914 100644
--- a/src/pages/Report/index.tsx
+++ b/src/pages/Report/index.tsx
@@ -1,55 +1,12 @@
import { useEffect, useMemo, useState } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';
-import { Button, EdgeRule, Modal, PageContainer, useToast } from '@/components/primitives';
+import { Button, EdgeRule, PageContainer } from '@/components/primitives';
import { UserTopbar } from '@/components/UserTopbar/UserTopbar';
import { getDetection, getReport, type DetectionDetailResponse } from '@/api/backend';
-import { imageDemo } from '@/data/mocks';
+import { buildReportPresentation, type ReportEvidence } from './reportPresentation';
import styles from './Report.module.css';
-interface EvidenceItemProps {
- code: string;
- name: string;
- description: string;
- thumb: string;
-}
-
-function EvidenceItem({ code, name, description, thumb }: EvidenceItemProps) {
- const [open, setOpen] = useState(false);
-
- return (
-
-
-
- {open ? (
-
-
- {description}
-
- ) : null}
-
-
- );
-}
-
-const timelineEntries = [
- ['14:23:08', '材料登记', ''],
- ['14:23:11', '全局语义读取', '场景判定: 街景'],
- ['14:23:13', '局部细节核查', '识别 7 个实体'],
- ['14:23:18', '异构专家协同', ''],
- ['14:23:22', '证据汇总', ''],
- ['14:23:23', '出具结论', ''],
-];
-
function formatPercent(value: number) {
return `${Math.round(value * 100)}%`;
}
@@ -57,21 +14,8 @@ function formatPercent(value: number) {
function formatDateTime(value?: string | null) {
if (!value) return 'N/A';
return new Intl.DateTimeFormat('zh-CN', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- }).format(new Date(value));
-}
-
-function formatClock(value?: string | null) {
- if (!value) return '--:--:--';
- return new Intl.DateTimeFormat('zh-CN', {
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- hour12: false,
+ year: 'numeric', month: '2-digit', day: '2-digit',
+ hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
}).format(new Date(value));
}
@@ -80,211 +24,114 @@ function formatFileSize(bytes: number) {
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
}
-function verdictText(detail: DetectionDetailResponse | null) {
- if (detail?.status === 'FAILED') return { cn: '检测失败', en: 'FAILED', confidence: 0 };
- if (detail?.report?.verdict === 'LIKELY_AUTHENTIC') return { cn: '可能真实', en: 'REAL', confidence: detail.report.confidence };
- if (detail?.report?.verdict === 'UNCERTAIN') return { cn: '结果不确定', en: 'UNCERTAIN', confidence: detail.report.confidence };
- return { cn: 'AI 生成', en: 'FAKE', confidence: detail?.report?.confidence ?? imageDemo.confidence };
+function verdictLabel(verdict: string | null) {
+ if (verdict === 'LIKELY_AUTHENTIC') return { cn: '可能真实', en: 'LIKELY AUTHENTIC' };
+ if (verdict === 'LIKELY_SYNTHETIC') return { cn: '可能由 AI 生成', en: 'LIKELY SYNTHETIC' };
+ if (verdict === 'UNCERTAIN') return { cn: '结果不确定', en: 'UNCERTAIN' };
+ if (verdict === 'FAILED') return { cn: '检测失败', en: 'FAILED' };
+ return { cn: '等待检测结果', en: 'PENDING' };
+}
+
+function EvidenceItem({ evidence }: { evidence: ReportEvidence }) {
+ const [open, setOpen] = useState(false);
+ return (
+
+
+
+ {open ? (
+
+ {evidence.description}
+
+ ) : null}
+
+
+ );
}
export function Report() {
- const [modal, setModal] = useState<'pdf' | 'archive' | null>(null);
const [detail, setDetail] = useState(null);
+ const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState(null);
- const { showToast } = useToast();
const { id } = useParams();
const location = useLocation();
const routeState = location.state as { imageSrc?: string } | null;
+ const invalidId = !id || id === 'demo';
useEffect(() => {
if (!id || id === 'demo') return undefined;
let active = true;
getDetection(id)
.catch(() => getReport(id))
- .then((nextDetail) => {
- if (active) setDetail(nextDetail);
- })
- .catch((error) => {
- if (active) setLoadError(error instanceof Error ? error.message : 'Report load failed.');
- });
- return () => {
- active = false;
- };
+ .then((nextDetail) => { if (active) setDetail(nextDetail); })
+ .catch((error) => { if (active) setLoadError(error instanceof Error ? error.message : 'Report load failed.'); })
+ .finally(() => { if (active) setLoading(false); });
+ return () => { active = false; };
}, [id]);
- const reportVerdict = verdictText(detail);
- const reportImageSrc = routeState?.imageSrc ?? (id ? window.sessionStorage.getItem(`detection-preview:${id}`) : null) ?? imageDemo.src;
- const evidenceItems = useMemo(() => {
- if (!detail?.predictions.length) {
- return imageDemo.marks.map((mark) => ({
- code: mark.label,
- name: mark.name,
- description: `${mark.name} region shows visual inconsistency with the surrounding image.`,
- }));
- }
- return detail.predictions.map((prediction, index) => ({
- code: `M-${String(index + 1).padStart(2, '0')}`,
- name: prediction.modelId,
- description: `${prediction.modelId} ${prediction.modelVersion} returned ${prediction.label} with score ${formatPercent(prediction.normalizedScore)}, threshold ${prediction.threshold.toFixed(2)}, latency ${prediction.latencyMs}ms.`,
- }));
- }, [detail]);
-
- const timeline = useMemo(() => {
- if (!detail) return timelineEntries;
- return [
- [formatClock(detail.createdAt), '任务创建', detail.filename],
- [formatClock(detail.startedAt), '模型推理', detail.status],
- [formatClock(detail.completedAt), '报告生成', detail.report?.riskLevel ?? detail.failureReason ?? '等待结果'],
- ];
- }, [detail]);
-
- const completeAction = () => {
- showToast(modal === 'pdf' ? 'PDF 导出任务已创建' : '报告已归档', 'success');
- setModal(null);
- };
+ const presentation = useMemo(() => buildReportPresentation(detail), [detail]);
+ const verdict = verdictLabel(presentation.verdict);
+ const preview = routeState?.imageSrc ?? (id ? window.sessionStorage.getItem(`detection-preview:${id}`) : null);
+ const visibleError = invalidId ? 'A persisted detection or report id is required.' : loadError;
return (
-
-
-
- >
- }
- />
+ >} />
-
-
-
-
-
-
-
- 一. 送检材料
-
-
- - 类型
- - 图片
- - 尺寸
- - 1920 × 1080
- - 送检
- - 2026.11.21 14:23
-
- {detail ? (
-
- - API Type
- - {detail.contentType}
- - API Size
- - {detail.width} × {detail.height}
- - SHA-256
- - {detail.sha256.slice(0, 16)}...
- - File
- - {formatFileSize(detail.fileSize)}
-
- ) : null}
-
-
-
-
-
- 二. 鉴别结论
- {detail ? (
-
-
{reportVerdict.cn}
-
{reportVerdict.en}
-
-
- {formatPercent(reportVerdict.confidence)} confidence
-
-
-
- ) : null}
- {loadError ? {loadError}
: null}
- {!detail ? (
-
-
AI 生成
-
FAKE
-
-
- {Math.round(imageDemo.confidence * 100)}% confidence
-
-
-
- ) : null}
-
-
-
-
-
- 三. 三条关键证据
-
- {evidenceItems.map((mark) => (
-
- ))}
-
-
-
-
-
-
- 四. 显影过程
-
- {timeline.map(([time, event, note]) => (
-
- {time}
- ─ {event}
- {note ? ─ {note} : null}
-
- ))}
-
-
-
-
-
-
-
+
+
+
+
+
+ 一、送检材料
+ {preview ?
: Preview unavailable after reload
}
+ {detail ?
+ - 文件
- {detail.filename}
+ - 类型
- {detail.contentType}
+ - 尺寸
- {detail.width} x {detail.height}
+ - 大小
- {formatFileSize(detail.fileSize)}
+ - SHA-256
- {detail.sha256}
+
: null}
+
+
+
+
+ 二、鉴别结论
+ {loading && !invalidId ? Loading persisted report...
: null}
+ {visibleError ? {visibleError}
: null}
+ {!loading && !visibleError ?
+
{verdict.cn}
+
{verdict.en}
+ {presentation.confidence !== null ?
{formatPercent(presentation.confidence)} confidence
: null}
+ {presentation.summary ?
{presentation.summary}
: null}
+
: null}
+
+
+
+
+ 三、模型证据
+ {presentation.evidence.map((item) => )}
+ {!presentation.evidence.length ? No persisted model predictions are available.
: null}
+
+
+
+
+ 四、处理过程
+ {presentation.timeline.map((entry) =>
{formatDateTime(entry.timestamp)}- {entry.event}{entry.note ? - {entry.note} : null}
)}
+
+
+
+
+
-
-
- setModal(null)}>
-
-
{modal === 'pdf' ? '确认导出 PDF' : '确认归档报告'}
-
{modal === 'pdf' ? '系统将生成当前报告的 PDF 文件。' : '报告将进入治理归档池。'}
-
-
-
);
}
diff --git a/src/pages/Report/reportPresentation.test.ts b/src/pages/Report/reportPresentation.test.ts
new file mode 100644
index 0000000..d2a7783
--- /dev/null
+++ b/src/pages/Report/reportPresentation.test.ts
@@ -0,0 +1,51 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import type { DetectionDetailResponse } from '@/api/backend';
+import { buildReportPresentation } from './reportPresentation.ts';
+
+function detail(overrides: Partial = {}): DetectionDetailResponse {
+ return {
+ taskId: 'task-1', assetId: 'asset-1', status: 'COMPLETED', failureReason: null,
+ filename: 'evidence.png', contentType: 'image/png', fileSize: 100, sha256: 'a'.repeat(64),
+ width: 100, height: 80, createdAt: '2026-07-11T00:00:00Z',
+ startedAt: '2026-07-11T00:00:01Z', completedAt: '2026-07-11T00:00:02Z',
+ predictions: [], report: null, ...overrides,
+ };
+}
+
+test('does not fabricate a report when backend data is unavailable', () => {
+ const presentation = buildReportPresentation(null);
+ assert.equal(presentation.state, 'unavailable');
+ assert.equal(presentation.confidence, null);
+ assert.deepEqual(presentation.evidence, []);
+ assert.deepEqual(presentation.timeline, []);
+});
+
+test('keeps completed tasks without predictions evidence-free', () => {
+ assert.deepEqual(buildReportPresentation(detail()).evidence, []);
+});
+
+test('presents failed tasks without a confidence claim', () => {
+ const presentation = buildReportPresentation(detail({ status: 'FAILED', failureReason: 'model timeout' }));
+ assert.equal(presentation.state, 'failed');
+ assert.equal(presentation.verdict, 'FAILED');
+ assert.equal(presentation.confidence, null);
+});
+
+test('uses only persisted report confidence and prediction evidence', () => {
+ const presentation = buildReportPresentation(detail({
+ predictions: [{
+ predictionId: 'prediction-1', modelId: 'nonescape-mini', modelVersion: 'v1', rawScore: 0.86,
+ normalizedScore: 0.86, label: 'SYNTHETIC', threshold: 0.5, latencyMs: 42,
+ createdAt: '2026-07-11T00:00:02Z',
+ }],
+ report: {
+ reportId: 'report-1', verdict: 'LIKELY_SYNTHETIC', confidence: 0.86,
+ summary: 'Persisted summary', riskLevel: 'HIGH', createdAt: '2026-07-11T00:00:02Z',
+ },
+ }));
+ assert.equal(presentation.state, 'complete');
+ assert.equal(presentation.confidence, 0.86);
+ assert.equal(presentation.evidence.length, 1);
+ assert.match(presentation.evidence[0].description, /nonescape-mini v1/);
+});
diff --git a/src/pages/Report/reportPresentation.ts b/src/pages/Report/reportPresentation.ts
new file mode 100644
index 0000000..658f741
--- /dev/null
+++ b/src/pages/Report/reportPresentation.ts
@@ -0,0 +1,62 @@
+import type { DetectionDetailResponse } from '../../api/backend.ts';
+
+export interface ReportEvidence {
+ code: string;
+ name: string;
+ description: string;
+}
+
+export interface ReportTimelineEntry {
+ timestamp: string;
+ event: string;
+ note: string;
+}
+
+export interface ReportPresentation {
+ state: 'unavailable' | 'pending' | 'failed' | 'complete';
+ verdict: 'LIKELY_AUTHENTIC' | 'LIKELY_SYNTHETIC' | 'UNCERTAIN' | 'FAILED' | null;
+ confidence: number | null;
+ summary: string | null;
+ evidence: ReportEvidence[];
+ timeline: ReportTimelineEntry[];
+}
+
+export function buildReportPresentation(detail: DetectionDetailResponse | null): ReportPresentation {
+ if (!detail) {
+ return { state: 'unavailable', verdict: null, confidence: null, summary: null, evidence: [], timeline: [] };
+ }
+
+ const timeline: ReportTimelineEntry[] = [
+ { timestamp: detail.createdAt, event: 'Task created', note: detail.filename },
+ ];
+ if (detail.startedAt) timeline.push({ timestamp: detail.startedAt, event: 'Inference started', note: detail.status });
+ if (detail.completedAt) {
+ timeline.push({
+ timestamp: detail.completedAt,
+ event: detail.status === 'FAILED' ? 'Task failed' : 'Task completed',
+ note: detail.failureReason ?? detail.report?.riskLevel ?? '',
+ });
+ }
+
+ if (detail.status === 'FAILED') {
+ return {
+ state: 'failed', verdict: 'FAILED', confidence: null,
+ summary: detail.failureReason, evidence: [], timeline,
+ };
+ }
+
+ const evidence = detail.predictions.map((prediction, index) => ({
+ code: `M-${String(index + 1).padStart(2, '0')}`,
+ name: prediction.modelId,
+ description: `${prediction.modelId} ${prediction.modelVersion} returned ${prediction.label} with score ${prediction.normalizedScore.toFixed(4)}, threshold ${prediction.threshold.toFixed(2)}, latency ${prediction.latencyMs}ms.`,
+ }));
+
+ if (!detail.report) {
+ return { state: 'pending', verdict: null, confidence: null, summary: null, evidence, timeline };
+ }
+
+ return {
+ state: 'complete', verdict: detail.report.verdict, confidence: detail.report.confidence,
+ summary: detail.report.summary, evidence, timeline,
+ };
+}