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 (
- - - - - } - /> + } /> - -
-
-

─ A report on visual authenticity ─

-
- -

鉴 别 报 告

- -
-

DEVELOP

-
- - № DV-2026-1121-003 - -
-
- - 2026 · 11 · 21 - -
-
- -
- -
-

一. 送检材料

- -
-
类型
-
图片
-
尺寸
-
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} -

- ))} -
-
- -
- -
-

DEVELOP · 由系统自动生成 · 仅供参考

-

报告时间 ─ {formatDateTime(detail?.report?.createdAt ?? detail?.completedAt)}

-
-
+
+
+

- A report on visual authenticity -

+

鉴别报告

+

DEVELOP

+
{id ?? 'UNKNOWN'}
+
+ +
+
+

一、送检材料

+ {preview ? Submitted evidence 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}

)}
+
+ +
+

检测结果是辅助信号,不应作为高风险决策的唯一依据。

报告时间 - {formatDateTime(detail?.report?.createdAt ?? detail?.completedAt)}

+
- - - 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, + }; +}