[Spec Kit] Initial commit — constitution, spec, plan, and tasks for Reaction Image Board v1
This commit is contained in:
134
specs/001-reaction-image-board/research.md
Normal file
134
specs/001-reaction-image-board/research.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Research: Reaction Image Board v1
|
||||
|
||||
**Phase**: 0 — Pre-design research
|
||||
**Date**: 2026-05-02
|
||||
|
||||
The technology stack is fully specified in the constitution's tech stack table
|
||||
(§6), so this document records rationale and key decisions rather than
|
||||
exploratory research.
|
||||
|
||||
---
|
||||
|
||||
## Decision 1: Project layout
|
||||
|
||||
**Decision**: Two top-level directories (`api/`, `ui/`) in a single
|
||||
repository, each with its own dependency manifest and Dockerfile.
|
||||
|
||||
**Rationale**: Constitution §1 mandates separate deployable artifacts with
|
||||
separate dependency manifests. A monorepo with two independently buildable
|
||||
services satisfies this without requiring a separate repo per service.
|
||||
Docker Compose ties them together for local development (§7.1).
|
||||
|
||||
**Alternatives considered**:
|
||||
- Separate repositories — rejected because it adds checkout/sync overhead
|
||||
for a solo project with no reason to deploy them independently yet.
|
||||
- Single `src/` with both projects — rejected because it would entangle
|
||||
dependency manifests, violating §2.1.
|
||||
|
||||
---
|
||||
|
||||
## Decision 2: SQLAlchemy async + asyncpg driver
|
||||
|
||||
**Decision**: SQLAlchemy 2.x in async mode with the asyncpg driver.
|
||||
|
||||
**Rationale**: Constitution §6 specifies this explicitly. FastAPI's async
|
||||
request handlers benefit directly; asyncpg is the fastest PostgreSQL driver
|
||||
for Python async code. The async session is injected per-request via
|
||||
FastAPI's dependency system.
|
||||
|
||||
**Alternatives considered**: Synchronous SQLAlchemy + psycopg2 — rejected
|
||||
per constitution mandate.
|
||||
|
||||
---
|
||||
|
||||
## Decision 3: Storage object key
|
||||
|
||||
**Decision**: SHA-256 hex digest of file bytes, no prefix, no extension
|
||||
(e.g. `a3f1...`).
|
||||
|
||||
**Rationale**: Spec §3 specifies this explicitly. Content-addressed keys
|
||||
are stable and human-inspectable in the bucket. The same key is stored in
|
||||
the `storage_key` column on the image record, so reconstruction without DB
|
||||
state is possible.
|
||||
|
||||
**Alternatives considered**: UUID key — rejected; UUID keys lose the
|
||||
content-addressing property and the deduplication shortcut.
|
||||
|
||||
---
|
||||
|
||||
## Decision 4: Duplicate detection strategy
|
||||
|
||||
**Decision**: Hash the file bytes in the API process; query PostgreSQL for an
|
||||
existing record with that hash before any storage write.
|
||||
|
||||
**Rationale**: Constitution §4.3 mandates deduplication by SHA-256. Performing
|
||||
the hash and DB check before writing to S3 avoids wasting storage bandwidth
|
||||
and keeps the duplicate-response path (HTTP 200 + `duplicate: true`) cheap.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Hash at upload, always write to S3 then check — rejected because it wastes
|
||||
S3 bandwidth for duplicate uploads.
|
||||
- Client-side hash — rejected because the constitution places no trust in the
|
||||
client and the UI knows nothing about storage implementation (§2.3).
|
||||
|
||||
---
|
||||
|
||||
## Decision 5: Tag validation pattern
|
||||
|
||||
**Decision**: `^[a-z0-9_-]{1,64}$` applied after normalisation (lowercase + trim).
|
||||
|
||||
**Rationale**: Spec §2.8 specifies this pattern exactly. Normalisation happens
|
||||
before validation so that user input like `" Cat "` becomes `"cat"` and passes.
|
||||
|
||||
---
|
||||
|
||||
## Decision 6: Pre-signed URL strategy
|
||||
|
||||
**Decision**: Generate a 1-hour pre-signed URL on each request to
|
||||
`GET /api/v1/images/{id}/file` and return a 302 redirect.
|
||||
|
||||
**Rationale**: Spec §2.4 specifies this approach. The client (browser or
|
||||
Angular app) loads the image directly from S3/MinIO, avoiding API proxying
|
||||
of potentially large files. The 1-hour expiry is short enough to be
|
||||
meaningless at personal-use scale.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Proxy image bytes through the API — rejected because it wastes API memory
|
||||
and bandwidth for potentially large files.
|
||||
- Permanent public S3 URLs — rejected because it exposes the bucket structure
|
||||
and requires the bucket to be public.
|
||||
|
||||
---
|
||||
|
||||
## Decision 7: Test database and storage in integration tests
|
||||
|
||||
**Decision**: Integration tests run against a real PostgreSQL database and a
|
||||
real MinIO instance started by Docker Compose (or a dedicated test compose
|
||||
file). No mocking of the database or storage layer.
|
||||
|
||||
**Rationale**: Constitution §5.2 mandates "API routes tested against a real
|
||||
(test) database and a real (test) S3-compatible bucket (e.g. MinIO in Docker)".
|
||||
Mocking at this layer has historically caused test/prod divergence.
|
||||
|
||||
**Alternatives considered**:
|
||||
- SQLite in-memory for integration tests — rejected; constitution mandates
|
||||
PostgreSQL specifically for the repository layer.
|
||||
- Mocked S3 (moto) — rejected per constitution §5.2.
|
||||
|
||||
---
|
||||
|
||||
## Decision 8: Angular tag filter debounce
|
||||
|
||||
**Decision**: Debounce tag filter bar API calls in `LibraryComponent` using
|
||||
RxJS `debounceTime` (e.g. 300 ms).
|
||||
|
||||
**Rationale**: Spec §4.1 says "updates in real time (debounced API call)".
|
||||
300 ms is a standard UX debounce that prevents excessive API calls while
|
||||
still feeling responsive.
|
||||
|
||||
---
|
||||
|
||||
## All NEEDS CLARIFICATION resolved
|
||||
|
||||
No unresolved clarifications remain. The constitution and spec together fully
|
||||
specify the technology choices and behaviour for v1.
|
||||
Reference in New Issue
Block a user