Archive code-review-fix change and sync specs to main

- Synced 14 capability delta specs to main specs
- Created 6 new main specs: api-authentication, error-boundary, input-validation, security-headers, shared-types
- Updated 8 existing specs with security, validation, and performance requirements
- Archived change to openspec/changes/archive/2026-02-20-code-review-fix/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marko Djordjevic 2026-02-20 08:54:59 +01:00
parent adb93a2d2e
commit 925e7284e3
32 changed files with 691 additions and 4 deletions

View file

@ -0,0 +1,39 @@
## ADDED Requirements
### Requirement: Next.js API key middleware
The system SHALL enforce API key authentication on all `/api/*` routes via Next.js middleware (`src/middleware.ts`). The middleware SHALL read the expected key from the `API_KEY` environment variable. Requests MUST include the key in the `X-API-Key` header. If the key is missing or incorrect, the middleware SHALL return HTTP 401 with `{ "error": "Unauthorized" }`. The `/api/health` endpoint SHALL be exempt from authentication.
#### Scenario: Valid API key
- **WHEN** a request to `/api/candles` includes header `X-API-Key: <correct key>`
- **THEN** the request proceeds to the route handler normally
#### Scenario: Missing API key
- **WHEN** a request to `/api/candles` has no `X-API-Key` header
- **THEN** the middleware returns HTTP 401 with `{ "error": "Unauthorized" }`
#### Scenario: Invalid API key
- **WHEN** a request to `/api/candles` includes header `X-API-Key: wrong-key`
- **THEN** the middleware returns HTTP 401 with `{ "error": "Unauthorized" }`
#### Scenario: Health endpoint exempt
- **WHEN** a request to `/api/health` has no `X-API-Key` header
- **THEN** the request proceeds normally (health check is unauthenticated)
#### Scenario: API_KEY not configured
- **WHEN** the `API_KEY` environment variable is not set
- **THEN** the middleware SHALL allow all requests (auth disabled) and log a warning at startup
### Requirement: FastAPI API key dependency
The FastAPI ML service SHALL enforce API key authentication via a shared `Depends()` dependency. The dependency SHALL read the expected key from the `API_KEY` environment variable. Requests MUST include the key in the `X-API-Key` header. The `/health` endpoint SHALL be exempt.
#### Scenario: Valid API key on ML service
- **WHEN** a request to `/predict` includes the correct `X-API-Key` header
- **THEN** the request proceeds to the endpoint handler
#### Scenario: Unauthorized ML service request
- **WHEN** a request to `/predict` has no `X-API-Key` header and `API_KEY` is configured
- **THEN** the service returns HTTP 401 with `{ "detail": "Unauthorized" }`
#### Scenario: Next.js proxy forwards API key
- **WHEN** the Next.js proxy route calls the ML service
- **THEN** it SHALL include the `X-API-Key` header from its own environment variable

View file

@ -157,3 +157,76 @@ The system SHALL provide a `GET /api/model/info` Next.js API route that proxies
#### Scenario: No model available
- **WHEN** GET /api/model/info is called and the inference service returns 503
- **THEN** the route returns HTTP 503 with `{ "error": "No model available" }`
### Requirement: Generic error responses
All Next.js API routes SHALL return generic error messages to clients for 500-level errors. The response body SHALL be `{ "error": "Internal server error" }`. The full error details SHALL be logged server-side via `console.error` with request context.
#### Scenario: Internal error returns generic message
- **WHEN** an API route handler throws an unexpected error
- **THEN** the client receives HTTP 500 with `{ "error": "Internal server error" }` and the full error is logged server-side
#### Scenario: No stack traces in response
- **WHEN** a database query fails in any API route
- **THEN** the response does NOT contain table names, file paths, connection strings, or stack traces
### Requirement: Scoped bulk annotation delete
The `DELETE /api/annotations` endpoint SHALL require a `chartId` query parameter when `all=true` is specified. Unscoped delete-all (without chartId) SHALL be rejected with HTTP 400.
#### Scenario: Scoped bulk delete
- **WHEN** `DELETE /api/annotations?all=true&chartId=5` is called
- **THEN** all annotations for chart 5 are deleted
#### Scenario: Unscoped bulk delete rejected
- **WHEN** `DELETE /api/annotations?all=true` is called without chartId
- **THEN** the route returns HTTP 400 with `{ "error": "chartId is required for bulk delete" }`
### Requirement: Transaction-wrapped chart cascade delete
The `DELETE /api/charts/[id]` route SHALL wrap all related deletions (annotations, span annotations, candles, chart) in a single database transaction using `db.transaction()`.
#### Scenario: Cascade delete in transaction
- **WHEN** `DELETE /api/charts/5` is called
- **THEN** span annotations, annotations, candles, and the chart record for chart 5 are all deleted within a single transaction
#### Scenario: Partial failure rolls back
- **WHEN** the candles deletion fails mid-transaction
- **THEN** all deletions are rolled back and the chart remains intact
### Requirement: Span annotations included in chart cascade delete
The `DELETE /api/charts/[id]` route SHALL delete span annotations for the chart in addition to annotations and candles.
#### Scenario: Span annotations deleted with chart
- **WHEN** `DELETE /api/charts/5` is called
- **THEN** all `span_annotations` rows with `chart_id=5` are deleted before the chart record
### Requirement: parseInt validation
All API routes that parse integer query parameters SHALL use `parseInt(value, 10)` with radix 10 and check for `isNaN()`. Invalid integer parameters SHALL return HTTP 400.
#### Scenario: Valid integer parameter
- **WHEN** `GET /api/candles?chartId=5` is called
- **THEN** chartId is parsed as integer 5
#### Scenario: Invalid integer parameter
- **WHEN** `GET /api/candles?chartId=abc` is called
- **THEN** the route returns HTTP 400 with `{ "error": "Invalid chartId" }`
### Requirement: CSV injection protection on exports
All CSV export routes SHALL prefix cell values starting with `=`, `+`, `-`, or `@` with a single quote (`'`) to prevent spreadsheet formula injection.
#### Scenario: Dangerous cell value escaped
- **WHEN** an annotation note contains `=CMD("calc")`
- **THEN** the exported CSV cell contains `'=CMD("calc")`
#### Scenario: Normal values unchanged
- **WHEN** an annotation note contains `regular text`
- **THEN** the exported CSV cell contains `regular text` (no prefix)
### Requirement: response.ok checks on all fetch calls
All `fetch()` calls in frontend components (`page.tsx`, `CandleChart.tsx`) SHALL check `response.ok` before calling `response.json()`. If `!response.ok`, the code SHALL throw an error or handle the failure explicitly.
#### Scenario: Successful response parsed
- **WHEN** a fetch call returns HTTP 200
- **THEN** `response.json()` is called and the data is used normally
#### Scenario: Error response handled
- **WHEN** a fetch call returns HTTP 500
- **THEN** the code detects `!response.ok` and shows an error message instead of attempting JSON parse

View file

@ -66,3 +66,50 @@ The chart SHALL display visual markers for existing annotations using the `serie
#### Scenario: Markers refresh on chart switch
- **WHEN** user switches to a different chart
- **THEN** markers from the previous chart are cleared and markers for the new chart's annotations are loaded
### Requirement: Refs for event handler closure values
The CandleChart component SHALL use `useRef` for state values that are read inside event handlers (`drawingState`, `selectedLineId`, `dragState`, `annotations`). The ref SHALL be updated alongside every `setState` call. Event handlers SHALL read from the ref instead of the closure variable.
#### Scenario: Click handler reads current state
- **WHEN** the user clicks on the chart after state has changed
- **THEN** the click handler reads the current value from the ref (not a stale closure value)
#### Scenario: Reduced re-subscription frequency
- **WHEN** refs are used for mutable state in event handlers
- **THEN** the `useEffect` dependency array for chart event subscriptions is smaller, reducing re-subscription frequency
### Requirement: Theme change without chart re-creation
The CandleChart component SHALL apply theme changes using `chart.applyOptions()` instead of destroying and re-creating the entire chart instance. This preserves scroll position, zoom level, and attached primitives.
#### Scenario: Theme toggle preserves state
- **WHEN** the user toggles between light and dark theme
- **THEN** the chart colors update without losing scroll position, zoom level, or annotation primitives
### Requirement: Dynamic candle interval detection
The CandleChart component SHALL determine the actual candle interval from the data (by examining the time difference between consecutive candles) instead of hardcoding 60 seconds. The detected interval SHALL be used for span annotation iteration loops.
#### Scenario: 1-minute candles
- **WHEN** candle data has 60-second intervals
- **THEN** the span iteration step is 60 seconds
#### Scenario: 1-hour candles
- **WHEN** candle data has 3600-second intervals
- **THEN** the span iteration step is 3600 seconds (not 60)
#### Scenario: No performance degradation on high timeframes
- **WHEN** iterating over a span on daily candles
- **THEN** the loop iterates over actual candle count (not 1440x more iterations)
### Requirement: Named constants for magic numbers
The CandleChart component SHALL extract hardcoded magic numbers (8px padding, 60s interval, color values) into named constants at the module level.
#### Scenario: Constants used instead of magic numbers
- **WHEN** the CandleChart source is inspected
- **THEN** magic numbers like `8`, `60`, hardcoded colors are replaced with descriptive constant names
### Requirement: Module-level empty Set default
The `new Set<string>()` default prop value SHALL be defined as a module-level constant instead of being recreated on every render.
#### Scenario: Stable default reference
- **WHEN** CandleChart renders without `hiddenLabels` prop
- **THEN** it uses a module-level `EMPTY_SET` constant (same reference across renders)

View file

@ -46,9 +46,9 @@ The project SHALL include docker-compose.yml for simplified deployment orchestra
- **WHEN** docker-compose up runs
- **THEN** the candle-annotator service starts only after the postgres service is healthy (`depends_on: postgres: condition: service_healthy`)
#### Scenario: Frontend DATABASE_URL
#### Scenario: Frontend DATABASE_URL uses env var interpolation
- **WHEN** the candle-annotator service starts
- **THEN** the `DATABASE_URL` environment variable is set to `postgresql://ml_user:ml_password@postgres:5432/candle_annotator`
- **THEN** the `DATABASE_URL` environment variable uses `${POSTGRES_PASSWORD}` interpolation: `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}`
#### Scenario: Restart policy
- **WHEN** container crashes or stops
@ -58,12 +58,32 @@ The project SHALL include docker-compose.yml for simplified deployment orchestra
- **WHEN** docker-compose.yml is parsed
- **THEN** there is no `candle-data` volume defined or mounted
#### Scenario: PostgreSQL port bound to localhost only
- **WHEN** docker-compose up runs
- **THEN** the postgres service port mapping is `127.0.0.1:5432:5432` (not `5432:5432`)
#### Scenario: MLflow port bound to localhost only
- **WHEN** docker-compose up runs
- **THEN** the mlflow service port mapping is `127.0.0.1:5000:5000`
#### Scenario: ML service port bound to localhost only
- **WHEN** docker-compose up runs
- **THEN** the ml-service port mapping is `127.0.0.1:8001:8001`
#### Scenario: Credentials via env var interpolation
- **WHEN** docker-compose.yml is parsed
- **THEN** all database credentials use `${POSTGRES_USER}`, `${POSTGRES_PASSWORD}`, and `${POSTGRES_DB}` variable interpolation from `.env`
### Requirement: Environment variable configuration
The project SHALL use environment variables for runtime configuration.
#### Scenario: .env.example file
#### Scenario: .env.example file with placeholder credentials
- **WHEN** repository is cloned
- **THEN** includes .env.example file documenting all configurable environment variables with example values
- **THEN** `.env.example` contains `POSTGRES_PASSWORD=change_me_to_a_strong_password` (not a real password)
#### Scenario: .env file gitignored
- **WHEN** `.gitignore` is inspected
- **THEN** it includes `.env` (not just `.env*.local`)
#### Scenario: DATABASE_URL configuration
- **WHEN** `DATABASE_URL` environment variable is set
@ -81,6 +101,10 @@ The project SHALL use environment variables for runtime configuration.
- **WHEN** NODE_ENV environment variable is set to 'production'
- **THEN** Next.js runs in production mode with optimizations enabled
#### Scenario: API_KEY configuration
- **WHEN** `API_KEY` environment variable is set
- **THEN** both Next.js middleware and FastAPI dependency use this key for authentication
### Requirement: Health check endpoint
The API SHALL provide a health check endpoint for container orchestration.
@ -141,6 +165,35 @@ The Docker image SHALL be optimized for production use with minimal size.
- **WHEN** Docker image build completes
- **THEN** final image size is under 200MB (excluding data volume)
#### Scenario: Base images pinned to digest
- **WHEN** Dockerfiles specify base images
- **THEN** images use `@sha256:<hash>` pinning for reproducible builds
### Requirement: ML service non-root user
The ML service Dockerfile SHALL create a non-root user and run the application as that user. The Dockerfile SHALL include `RUN useradd -m -r appuser` and `USER appuser` directives.
#### Scenario: Container runs as non-root
- **WHEN** the ML service container starts
- **THEN** the application process runs as user `appuser` (not root)
### Requirement: TA-Lib downloaded over HTTPS with checksum
The ML service Dockerfile SHALL download TA-Lib source over HTTPS (not HTTP). The download SHALL be verified with a SHA256 checksum before extraction.
#### Scenario: HTTPS download
- **WHEN** the Dockerfile downloads TA-Lib source
- **THEN** the URL uses `https://` protocol
#### Scenario: Checksum verification
- **WHEN** the TA-Lib tarball is downloaded
- **THEN** a `sha256sum -c` check runs before extraction, and the build fails if the checksum does not match
### Requirement: .dockerignore file exists
The project SHALL include a `.dockerignore` file at the repository root that excludes `.git`, `.env`, `.env*`, `node_modules`, `.next`, `data/`, `*.md`, `__pycache__/`, `mlruns/`, and `models/`.
#### Scenario: Docker context excludes sensitive files
- **WHEN** `docker build` runs
- **THEN** `.env`, `.git`, and `node_modules` are not included in the build context
### Requirement: Database persistence
The deployment SHALL ensure PostgreSQL data persists across container restarts.
@ -201,3 +254,7 @@ The Docker setup SHALL follow security best practices.
#### Scenario: Minimal attack surface
- **WHEN** container runs
- **THEN** only port 3000 is exposed, no SSH, no unnecessary services, alpine base reduces package vulnerabilities
#### Scenario: No node_modules in production image
- **WHEN** the Next.js production Docker image is built
- **THEN** the `COPY --from=builder /app/node_modules` line is removed (standalone output bundles needed deps)

View file

@ -0,0 +1,23 @@
## ADDED Requirements
### Requirement: React Error Boundary component
The application SHALL include a React Error Boundary component that catches JavaScript errors in its child component tree and renders a fallback UI instead of crashing to a white screen. The fallback SHALL display: an error message, a "Reload" button that calls `window.location.reload()`, and a "Try Again" button that resets the error boundary state.
#### Scenario: Child component throws during render
- **WHEN** CandleChart or any child component throws an error during rendering
- **THEN** the Error Boundary catches the error and renders the fallback UI instead of a white screen
#### Scenario: Fallback UI interaction
- **WHEN** the user sees the error fallback and clicks "Try Again"
- **THEN** the Error Boundary resets its state and attempts to re-render the child component tree
#### Scenario: Error logging
- **WHEN** the Error Boundary catches an error
- **THEN** the error and component stack are logged via `console.error`
### Requirement: Error Boundary placement in layout
The root layout (`src/app/layout.tsx`) SHALL wrap `{children}` with the Error Boundary component so that any render error in any page is caught.
#### Scenario: Layout wraps children
- **WHEN** the application renders any page
- **THEN** the page content is wrapped inside an Error Boundary

View file

@ -0,0 +1,76 @@
## ADDED Requirements
### Requirement: Zod validation on proxy routes
All Next.js proxy routes SHALL validate request bodies using Zod schemas before forwarding to the ML service. If validation fails, the route SHALL return HTTP 400 with `{ "error": "<zod error message>" }` without contacting the ML service.
#### Scenario: Valid predict request
- **WHEN** POST `/api/predict` receives `{ pair: "EURUSD", timeframe: "1H", candles: [{time: 1, open: 1, high: 2, low: 0.5, close: 1.5}] }`
- **THEN** the request passes validation and is forwarded to the ML service
#### Scenario: Invalid predict request
- **WHEN** POST `/api/predict` receives `{ pair: 123, candles: "not-an-array" }`
- **THEN** the route returns HTTP 400 with a Zod validation error message
#### Scenario: Valid training start request
- **WHEN** POST `/api/training/start` receives `{ model_type: "random_forest" }`
- **THEN** the request passes validation and is forwarded
#### Scenario: Valid model load request
- **WHEN** POST `/api/model/load` receives `{ run_id: "abc-123" }`
- **THEN** the request passes validation and is forwarded
#### Scenario: Valid pattern detection request
- **WHEN** POST `/api/patterns/detect` receives `{ candles: [...], patterns: ["CDLENGULFING"] }`
- **THEN** the request passes validation and is forwarded
### Requirement: run_id format validation
All routes and endpoints that accept a `run_id` parameter SHALL validate that the value matches the pattern `/^[a-zA-Z0-9_-]+$/`. If validation fails, the route SHALL return HTTP 400 with `{ "error": "Invalid run_id format" }`.
#### Scenario: Valid run_id in URL path
- **WHEN** DELETE `/api/training/runs/abc-123_v2` is called
- **THEN** the request proceeds normally
#### Scenario: Path traversal attempt in run_id
- **WHEN** DELETE `/api/training/runs/../../admin/delete-all` is called
- **THEN** the route returns HTTP 400 with `{ "error": "Invalid run_id format" }`
#### Scenario: Python service run_id validation
- **WHEN** POST `/model/load` receives `{ run_id: "../../../etc/passwd" }`
- **THEN** the FastAPI endpoint returns HTTP 400 with `{ "detail": "Invalid run_id format" }`
### Requirement: File upload size limit
The `POST /api/upload` route SHALL reject files larger than 10MB. The route SHALL check `file.size` before processing and return HTTP 413 with `{ "error": "File too large. Maximum size is 10MB." }` if exceeded. The route SHALL also limit the number of rows inserted to 500,000.
#### Scenario: File within size limit
- **WHEN** a 5MB CSV file is uploaded
- **THEN** the upload proceeds normally
#### Scenario: File exceeds size limit
- **WHEN** a 15MB CSV file is uploaded
- **THEN** the route returns HTTP 413 with `{ "error": "File too large. Maximum size is 10MB." }`
#### Scenario: Row count limit
- **WHEN** a CSV file contains 600,000 rows
- **THEN** the route returns HTTP 400 with `{ "error": "Too many rows. Maximum is 500,000." }`
### Requirement: Batch prediction input size limit
The FastAPI `/predict/batch` endpoint SHALL validate that the requested date range does not exceed 1 year. If exceeded, the endpoint SHALL return HTTP 400.
#### Scenario: Date range within limit
- **WHEN** POST `/predict/batch` requests a 6-month range
- **THEN** the request proceeds normally
#### Scenario: Date range exceeds limit
- **WHEN** POST `/predict/batch` requests a 2-year range
- **THEN** the endpoint returns HTTP 400 with `{ "detail": "Date range exceeds maximum of 1 year" }`
### Requirement: File type validation on upload
The `POST /api/upload` route SHALL validate that the uploaded file has a `.csv` extension and a text-based MIME type. Other file types SHALL be rejected with HTTP 400.
#### Scenario: Valid CSV upload
- **WHEN** a file named `data.csv` with MIME type `text/csv` is uploaded
- **THEN** the upload proceeds normally
#### Scenario: Invalid file type
- **WHEN** a file named `model.pkl` is uploaded
- **THEN** the route returns HTTP 400 with `{ "error": "Only CSV files are accepted" }`

View file

@ -105,3 +105,68 @@ The system SHALL group consecutive candle predictions with the same non-"O" labe
#### Scenario: Break on label change
- **WHEN** candle T1 is "bull_flag" and candle T2 is "bear_flag"
- **THEN** the system creates two separate spans
### Requirement: CORS restricted to explicit origins
The FastAPI ML service SHALL configure CORS with explicit allowed origins instead of wildcard `*`. The default allowed origins SHALL be `["http://localhost:3000"]`. Additional origins MAY be configured via the `CORS_ORIGINS` environment variable (comma-separated).
#### Scenario: Same-origin request allowed
- **WHEN** a request from `http://localhost:3000` hits the ML service
- **THEN** the CORS headers allow the request
#### Scenario: Unknown origin blocked
- **WHEN** a request from `http://evil.com` hits the ML service
- **THEN** the CORS headers do not include `Access-Control-Allow-Origin` for that origin
### Requirement: Generic error responses from ML service
All FastAPI endpoints SHALL return generic error messages for unexpected exceptions. The response SHALL be `{ "detail": "Internal server error" }` with HTTP 500. Full exception details SHALL be logged via `logging.error` with traceback.
#### Scenario: Internal error generic response
- **WHEN** a predict endpoint throws an unexpected exception
- **THEN** the client receives `{ "detail": "Internal server error" }` and the traceback is logged server-side
### Requirement: Model file integrity check
When loading a model via `joblib.load()`, the system SHALL verify the model file's SHA256 hash against a manifest file (`models/checksums.sha256`). If the hash does not match or the manifest entry is missing, the load SHALL fail with an error.
#### Scenario: Valid model file
- **WHEN** `joblib.load("models/abc123.pkl")` is called and the file's SHA256 matches the manifest
- **THEN** the model loads successfully
#### Scenario: Tampered model file
- **WHEN** the model file's SHA256 does not match the manifest entry
- **THEN** the system refuses to load the model and returns HTTP 500 with `{ "detail": "Model integrity check failed" }`
### Requirement: Thread-safe model reads
The ML service SHALL use a lock for model reads during prediction, not just model writes. All code that reads the current model reference SHALL acquire the `_model_swap_lock` or use an atomic reference swap pattern.
#### Scenario: Concurrent prediction and model swap
- **WHEN** a prediction request is in progress and a model load request arrives
- **THEN** the prediction completes with the old model (or waits), and the new model is loaded atomically
### Requirement: Path traversal prevention on model operations
The FastAPI endpoints that accept `run_id` for model load and delete SHALL validate the `run_id` format and verify that the resolved file path is within the expected `models/` directory using `Path.resolve()`.
#### Scenario: Valid run_id resolves within models directory
- **WHEN** POST `/model/load` receives `{ run_id: "abc123" }`
- **THEN** the path resolves to `models/abc123.pkl` within the models directory
#### Scenario: Path traversal attempt blocked
- **WHEN** POST `/model/load` receives `{ run_id: "../../etc/passwd" }`
- **THEN** the endpoint returns HTTP 400 before attempting any file operation
### Requirement: Real health check
The `GET /health` endpoint SHALL perform actual connectivity checks instead of returning hardcoded status. It SHALL execute `SELECT 1` against PostgreSQL and attempt an MLflow API call.
#### Scenario: All services healthy
- **WHEN** PostgreSQL responds to `SELECT 1` and MLflow API is reachable
- **THEN** the health endpoint returns `{ "status": "healthy", "database": "connected", "mlflow": "connected" }`
#### Scenario: Database unreachable
- **WHEN** PostgreSQL does not respond to `SELECT 1`
- **THEN** the health endpoint returns `{ "status": "degraded", "database": "disconnected" }` with HTTP 200
### Requirement: Candle time-sort validation
The `POST /predict` endpoint SHALL validate that input candles are sorted by time in ascending order. If candles are not sorted, the endpoint SHALL sort them before processing.
#### Scenario: Unsorted candles auto-sorted
- **WHEN** candles are provided in random time order
- **THEN** the endpoint sorts them by time before feature engineering and prediction

View file

@ -113,3 +113,44 @@ The system SHALL log the full pipeline YAML config as an MLflow artifact with ea
#### Scenario: Config artifact logged
- **WHEN** a training run starts
- **THEN** the full pipeline.yaml content is logged as "pipeline_config.yaml" artifact in the MLflow run
### Requirement: Training resource limits
The `POST /training/start` endpoint SHALL enforce resource limits: the training dataset file size SHALL not exceed 500MB, and the training thread SHALL have a configurable timeout (default: 30 minutes). If the timeout is exceeded, the training thread SHALL be marked as failed.
#### Scenario: Dataset too large
- **WHEN** the training dataset exceeds 500MB
- **THEN** training fails immediately with `{ "detail": "Dataset too large. Maximum 500MB." }`
#### Scenario: Training timeout
- **WHEN** a training run exceeds the 30-minute timeout
- **THEN** the training status is set to "failed" with reason "Training timed out"
### Requirement: run_id validation on training endpoints
The FastAPI training endpoints (`DELETE /training/runs/{run_id}`, `GET /training/runs/{run_id}`) SHALL validate that `run_id` matches `/^[a-zA-Z0-9_-]+$/` before any database or file operation.
#### Scenario: Valid run_id
- **WHEN** `DELETE /training/runs/run-2024-01-15_v3` is called
- **THEN** the request proceeds normally
#### Scenario: Invalid run_id
- **WHEN** `DELETE /training/runs/../../admin` is called
- **THEN** the endpoint returns HTTP 400 with `{ "detail": "Invalid run_id format" }`
### Requirement: Environment variable configuration (credentials)
The project SHALL use environment variables for runtime configuration. Credentials SHALL NOT be hardcoded in any committed file.
#### Scenario: .env file gitignored
- **WHEN** `.gitignore` is inspected
- **THEN** it includes `.env` (bare, not just `.env*.local`)
#### Scenario: .env removed from git history
- **WHEN** `git ls-files .env` is run
- **THEN** `.env` is NOT tracked by git
#### Scenario: .env.example has placeholder credentials
- **WHEN** `.env.example` is inspected
- **THEN** it contains `POSTGRES_PASSWORD=change_me_to_a_strong_password` (not a real password)
#### Scenario: No credentials in Python source
- **WHEN** `services/ml/app/db.py` is inspected
- **THEN** there are no SQL comments containing usernames or passwords, and the code fails fast if `DATABASE_URL` env var is not set

View file

@ -78,3 +78,40 @@ The project SHALL include a one-time migration script at `scripts/migrate-sqlite
#### Scenario: Idempotent execution
- **WHEN** the migration script is run a second time on an already-migrated database
- **THEN** the script either skips existing data or clears and re-inserts (with a flag), without creating duplicates
### Requirement: Environment variable configuration (credentials)
The project SHALL use environment variables for runtime configuration. Credentials SHALL NOT be hardcoded in any committed file.
#### Scenario: .env file gitignored
- **WHEN** `.gitignore` is inspected
- **THEN** it includes `.env` (bare, not just `.env*.local`)
#### Scenario: .env removed from git history
- **WHEN** `git ls-files .env` is run
- **THEN** `.env` is NOT tracked by git
#### Scenario: .env.example has placeholder credentials
- **WHEN** `.env.example` is inspected
- **THEN** it contains `POSTGRES_PASSWORD=change_me_to_a_strong_password` (not a real password)
#### Scenario: No credentials in Python source
- **WHEN** `services/ml/app/db.py` is inspected
- **THEN** there are no SQL comments containing usernames or passwords, and the code fails fast if `DATABASE_URL` env var is not set
### Requirement: models directory gitignored
The `.gitignore` file SHALL include `models/` and `*.pkl` patterns to prevent model files from being committed.
#### Scenario: Model files excluded
- **WHEN** a model file is saved to `models/best.pkl`
- **THEN** `git status` does not show it as untracked
### Requirement: devDependencies correctly categorized
The `package.json` SHALL list `@types/*`, `typescript`, `eslint`, `eslint-config-next`, `autoprefixer`, and `postcss` under `devDependencies` (not `dependencies`).
#### Scenario: Type packages in devDependencies
- **WHEN** `package.json` is inspected
- **THEN** `@types/node`, `@types/react`, `@types/react-dom`, `@types/papaparse`, `@types/pg` are in `devDependencies`
#### Scenario: Build tools in devDependencies
- **WHEN** `package.json` is inspected
- **THEN** `typescript`, `eslint`, `eslint-config-next`, `autoprefixer`, `postcss` are in `devDependencies`

View file

@ -143,3 +143,43 @@ The system SHALL poll `/api/model/info` every 30 seconds when the inference API
#### Scenario: Annotation independence
- **WHEN** the inference API is unavailable
- **THEN** all human annotation tools continue to work normally
### Requirement: AbortController for prediction requests
The prediction fetching functions (`fetchPredictions`, `handleFetchBatchPredictions`) SHALL use `AbortController` to cancel previous in-flight requests when a new request is initiated. A ref SHALL hold the current controller. Stale responses SHALL be discarded.
#### Scenario: Rapid clicks cancel previous request
- **WHEN** the user clicks "Run on Visible" twice quickly
- **THEN** the first request is aborted and only the second response is rendered
#### Scenario: Batch prediction cancellation
- **WHEN** the user clicks "Predict All" while a previous batch prediction is in progress
- **THEN** the previous batch request is aborted
### Requirement: Bounded prediction cache
The `predictionCacheRef` Map SHALL have a maximum size of 100 entries. When the cache exceeds the limit, the oldest entry (first inserted) SHALL be evicted. The cache SHALL function as a simple FIFO bounded map.
#### Scenario: Cache eviction at limit
- **WHEN** the cache has 100 entries and a new prediction result is cached
- **THEN** the oldest entry is deleted before the new one is inserted
#### Scenario: Cache size never exceeds limit
- **WHEN** 200 unique prediction ranges are fetched
- **THEN** the cache contains at most 100 entries at any point
### Requirement: Stable modelInfo reference for cache key
The `generateCacheKey` function SHALL read `modelInfo` from a ref (not from the `predictionState` closure) to prevent stale cache keys.
#### Scenario: Model change produces correct cache key
- **WHEN** the user loads a new model and then runs predictions
- **THEN** the cache key includes the new model version (not the old one from a stale closure)
### Requirement: Confirmation dialog for delete-all annotations
The "Delete All" annotations action SHALL show a confirmation dialog before executing. The dialog SHALL display "Are you sure you want to delete all annotations?" with "Cancel" and "Delete" buttons.
#### Scenario: User confirms deletion
- **WHEN** the user clicks "Delete All" and then clicks "Delete" in the confirmation dialog
- **THEN** all annotations for the active chart are deleted
#### Scenario: User cancels deletion
- **WHEN** the user clicks "Delete All" and then clicks "Cancel" in the confirmation dialog
- **THEN** no annotations are deleted

View file

@ -0,0 +1,21 @@
## ADDED Requirements
### Requirement: Security response headers
The Next.js application SHALL add security response headers to all routes via the `headers()` function in `next.config.js`. The following headers SHALL be set:
- `X-Frame-Options: DENY`
- `X-Content-Type-Options: nosniff`
- `Referrer-Policy: strict-origin-when-cross-origin`
- `Permissions-Policy: camera=(), microphone=(), geolocation=()`
- `Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:`
#### Scenario: Headers present on HTML response
- **WHEN** a browser requests any page
- **THEN** the response includes all five security headers
#### Scenario: Headers present on API response
- **WHEN** a client requests any `/api/*` endpoint
- **THEN** the response includes all five security headers
#### Scenario: Clickjacking prevented
- **WHEN** a third-party site attempts to embed the application in an iframe
- **THEN** the browser blocks the embedding due to `X-Frame-Options: DENY`

View file

@ -0,0 +1,33 @@
## ADDED Requirements
### Requirement: Centralized TypeScript interfaces
The project SHALL define shared TypeScript interfaces in `src/types/` with the following files:
- `candles.ts`: `Candle` interface with `time`, `open`, `high`, `low`, `close`, optional `volume`
- `annotations.ts`: `Annotation`, `AnnotationType` interfaces
- `charts.ts`: `Chart` interface
- `predictions.ts`: `PredictionSpan`, `PredictionState`, `ModelInfo` interfaces
- `span-annotations.ts`: `SpanAnnotation`, `SpanLabelType`, `SubSpan` interfaces
- `index.ts`: barrel file re-exporting all types
#### Scenario: Import shared Candle type
- **WHEN** a component needs the `Candle` interface
- **THEN** it imports from `@/types` instead of defining its own
#### Scenario: No duplicate interface definitions
- **WHEN** the codebase is searched for `interface Candle` or `interface SpanAnnotation`
- **THEN** each interface is defined exactly once in `src/types/` (not in component files)
#### Scenario: Type consistency across components
- **WHEN** `page.tsx`, `CandleChart.tsx`, `SpanAnnotationManager.tsx`, `Toolbox.tsx`, `SpanAnnotationList.tsx`, and `SpanPopover.tsx` reference shared types
- **THEN** they all import from `@/types` and use the same interface definitions
### Requirement: Replace pervasive any types
All `any` type annotations in component files SHALL be replaced with proper typed interfaces from `src/types/`. Specifically: `geometry` fields SHALL use a defined `Geometry` interface, `sub_spans` SHALL use `SubSpan[]`, candle arrays SHALL use `Candle[]`, and prediction cache values SHALL use `PredictionSpan[]`.
#### Scenario: Geometry field typed
- **WHEN** a component accesses `annotation.geometry`
- **THEN** the type is `Geometry | null` (not `any`)
#### Scenario: Prediction cache typed
- **WHEN** `predictionCacheRef` is accessed
- **THEN** its type is `Map<string, PredictionSpan[]>` (not `Map<string, any>`)

View file

@ -32,3 +32,42 @@ The system SHALL support saving negative annotations when a user dismisses a mod
#### Scenario: Save negative annotation
- **WHEN** user dismisses a "bull_flag" prediction with confidence 0.72
- **THEN** the system creates a span annotation with label "O", source "human_correction", and model_prediction `{ "label": "bull_flag", "confidence": 0.72 }`
### Requirement: Keyboard handler uses stable selectedSpanId
The SpanAnnotationManager keyboard handler SHALL use a `useRef` for `selectedSpanId` to prevent stale closure reads. The `handleDeleteSpan` function SHALL be wrapped in `useCallback` with proper dependencies.
#### Scenario: Delete correct span via keyboard
- **WHEN** user selects span A, then selects span B, then presses Delete
- **THEN** span B is deleted (not span A from a stale closure)
### Requirement: Preview primitive uses ref instead of state
The SpanAnnotationManager preview primitive (shown during span drawing on mouse move) SHALL use a `useRef` instead of `useState` to avoid a state/effect feedback loop and unnecessary re-renders.
#### Scenario: Mouse move during drawing does not cause re-render cascade
- **WHEN** the user moves the mouse while drawing a span annotation
- **THEN** the preview primitive updates via ref mutation without triggering a React re-render
### Requirement: Preview primitive cleanup on unmount
The SpanAnnotationManager SHALL detach the preview primitive in the `useEffect` cleanup function when the component unmounts, preventing a leaked canvas primitive.
#### Scenario: Component unmounts during drawing
- **WHEN** the user navigates away while mid-draw
- **THEN** the preview primitive is detached from the chart
### Requirement: fitContent not called on every span change
The SpanAnnotationManager reconciliation effect SHALL NOT call `chart.timeScale().fitContent()` on every span annotation change. `fitContent()` SHALL only be called on initial chart load.
#### Scenario: Span annotation added preserves zoom
- **WHEN** the user adds a new span annotation
- **THEN** the chart maintains the current scroll position and zoom level (no fitContent reset)
### Requirement: Incremental primitive updates
The SpanAnnotationManager SHALL update only the selection state of existing primitives on selection change instead of detaching all and re-attaching all. Full recreation SHALL only occur when the annotation list itself changes (add/remove/edit).
#### Scenario: Selection change is O(1)
- **WHEN** the user clicks a different span annotation
- **THEN** only the previously selected and newly selected primitives are updated (not all N primitives)
#### Scenario: Annotation add triggers full reconciliation
- **WHEN** a new span annotation is added
- **THEN** the primitive list is reconciled (new primitive added, existing ones kept)

View file

@ -18,3 +18,99 @@ The UI shell SHALL include a theme toggle button in the sidebar. The button SHAL
#### Scenario: Toggle button has tooltip
- **WHEN** user hovers over the theme toggle button
- **THEN** a tooltip displays the current mode name (e.g., "Theme: System", "Theme: Light", "Theme: Dark")
### Requirement: Accessibility on interactive elements
All interactive elements (buttons, dropdowns, modals) SHALL have `aria-label` attributes describing their function. Toggle buttons SHALL use `aria-pressed`. The keyboard shortcuts modal SHALL have `role="dialog"` and `aria-modal="true"`.
#### Scenario: Button has aria-label
- **WHEN** a toolbar button renders
- **THEN** it has an `aria-label` attribute describing its action (e.g., "Draw span annotation")
#### Scenario: Modal has dialog role
- **WHEN** the keyboard shortcuts modal opens
- **THEN** it has `role="dialog"` and `aria-modal="true"`
### Requirement: Focus trapping in modals
Modal dialogs (keyboard shortcuts, confirmation dialogs) SHALL trap focus within the modal while open. Tab and Shift+Tab SHALL cycle through focusable elements within the modal.
#### Scenario: Focus trapped in modal
- **WHEN** a modal is open and the user presses Tab
- **THEN** focus cycles through focusable elements within the modal only
### Requirement: Dark theme on settings pages
The annotation-types and span-label-types settings pages SHALL use theme-aware CSS variables instead of hardcoded light colors. They SHALL render correctly in both light and dark themes.
#### Scenario: Settings page in dark mode
- **WHEN** the theme is set to dark and user navigates to annotation-types page
- **THEN** the page renders with dark background and light text (no hardcoded white backgrounds)
### Requirement: next/font for Google Fonts
The application SHALL use `next/font/google` to load Google Fonts instead of CSS `@import` in `globals.css`. This prevents render-blocking font loading.
#### Scenario: Font loaded via next/font
- **WHEN** the application loads
- **THEN** Google Fonts are loaded via `next/font/google` (not via CSS `@import url()` in globals.css)
### Requirement: Confidence slider debounce
The confidence threshold slider in PredictionPanel SHALL debounce chart re-renders. The slider SHALL update the displayed value immediately but only trigger chart updates after 150ms of inactivity (or on `onPointerUp`).
#### Scenario: Dragging slider does not re-render chart per pixel
- **WHEN** the user drags the confidence slider
- **THEN** the chart re-renders at most once every 150ms (not on every pixel movement)
### Requirement: ChartSelector closes on outside click
The custom chart selector dropdown SHALL close when the user clicks outside of it.
#### Scenario: Click outside closes dropdown
- **WHEN** the chart selector dropdown is open and the user clicks elsewhere on the page
- **THEN** the dropdown closes
### Requirement: Dead code removal
The following dead code SHALL be removed:
- `src/lib/db/migrate.ts` (SQLite migration code)
- `get_db_session()` in `services/ml/app/db.py` (unused session leak)
- Dead filter code (TODO comment, no-op) in `page.tsx`
- Dead `inference*` package reference in `pyproject.toml`
#### Scenario: No dead migration code
- **WHEN** `src/lib/db/migrate.ts` is searched for
- **THEN** the file does not exist
#### Scenario: No unused db session function
- **WHEN** `services/ml/app/db.py` is inspected
- **THEN** `get_db_session()` function is absent
### Requirement: Deprecated Python API replacements
The ML service SHALL replace deprecated Python APIs:
- `@app.on_event("startup")` replaced with lifespan pattern
- `declarative_base()` replaced with `class Base(DeclarativeBase)`
- `datetime.utcnow()` replaced with `datetime.now(datetime.UTC)`
#### Scenario: No deprecated startup event
- **WHEN** `services/ml/app/main.py` is inspected
- **THEN** startup logic uses the FastAPI lifespan pattern (not `@app.on_event`)
#### Scenario: No deprecated utcnow
- **WHEN** `datetime.utcnow()` is searched for in the ML service
- **THEN** zero matches are found (all replaced with `datetime.now(datetime.UTC)`)
### Requirement: Unused import cleanup
Components SHALL not have unused imports. Specifically, `Toolbox.tsx` SHALL remove unused `TrendingUp` and `ChevronUp` imports.
#### Scenario: No unused imports in Toolbox
- **WHEN** `Toolbox.tsx` is inspected
- **THEN** only imported symbols that are used in the component are present
### Requirement: Tooltip component functional or removed
The `ui/tooltip.tsx` component SHALL either be implemented using Radix Tooltip or removed if unused.
#### Scenario: Tooltip is functional
- **WHEN** the Tooltip component is used
- **THEN** it renders an actual tooltip on hover (not a no-op passthrough)
### Requirement: SpanAnnotationList confidence check for zero
The confidence display in SpanAnnotationList SHALL use `!= null` instead of a falsy check, so that a confidence value of `0` is correctly displayed.
#### Scenario: Confidence zero displayed
- **WHEN** a span annotation has confidence value `0`
- **THEN** the list displays "0%" (not hidden as if confidence is absent)