feat(F-002): database foundation with migrations and dev compose
- node-pg-migrate + pg: baseline migration (extensions, app_meta) with working down - src/infrastructure/db fail-fast pool and typed query helper - docker-compose: postgres:16-alpine + redis:7-alpine with one-command up - table naming convention <module>_<table> documented in README - integration tests (6) against real PostgreSQL; strict identifier validation for test DDL after security-gate hardening round - deps justified in spec/tech.md; all gates approved; verify.sh green
This commit is contained in:
26
work/artifacts/F-002/architect.md
Normal file
26
work/artifacts/F-002/architect.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Architect — F-002 Database foundation with module-owned schemas
|
||||
|
||||
done -> work/artifacts/F-002/architect.md
|
||||
|
||||
## Decision summary
|
||||
- Migration tool: node-pg-migrate + pg driver. Pure npm deps, boring, up/down support, version table owned by the tool.
|
||||
- Single PostgreSQL instance, module ownership by table prefix (`<module>_<table>`). No per-module PG schemas — simplicity first; prefix rule documented and exemplified.
|
||||
- Baseline migration only ships foundation objects (extensions + `app_meta`). Business tables arrive with their modules.
|
||||
- Dev environment: docker-compose with postgres:16-alpine + redis:7-alpine, one command up.
|
||||
- Env via Node 22 `--env-file`; fail fast when `DATABASE_URL` missing. Integration tests skip explicitly when `TEST_DATABASE_URL` absent — no silent magic.
|
||||
|
||||
## Expected blast radius
|
||||
|
||||
```text
|
||||
EXPECTED BLAST RADIUS
|
||||
|
||||
Modules modified: src/infrastructure/db (new)
|
||||
Modules indirectly affected: none (no HTTP API, no business modules)
|
||||
Database changes: baseline migration (extensions, app_meta) - greenfield
|
||||
API changes: none
|
||||
Events added/changed: none
|
||||
Risk level: low
|
||||
```
|
||||
|
||||
## Handoff to implementer
|
||||
Follow specs/F-002-database-foundation/TASKS.md. Docker daemon is available (verified at intake). Definition of done for build stage: compose up green, migrations up/down/up demonstrated against real PostgreSQL, integration tests passing with TEST_DATABASE_URL, and evidence in implementer.md.
|
||||
20
work/artifacts/F-002/documenter.md
Normal file
20
work/artifacts/F-002/documenter.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Documenter — F-002 Database foundation with module-owned schemas
|
||||
|
||||
done -> work/artifacts/F-002/documenter.md
|
||||
|
||||
## Why documentation applies
|
||||
F-002 adds developer-facing contracts: database commands, migration workflow, and the table naming convention.
|
||||
|
||||
## Documents reviewed/produced
|
||||
- `project/README.md` (updated at build stage, verified accurate):
|
||||
- Database section lists every new script: docker:up, docker:down, db:up, db:down, db:status, test:integration — all present in package.json (cross-checked programmatically)
|
||||
- `.env.example` copy step documented
|
||||
- Table naming convention section: `<module>_<table>` rule, no cross-prefix queries, migrations immutable
|
||||
- `spec/tech.md`: dependency justification table extended with pg, node-pg-migrate, @types/pg (security policy requirement)
|
||||
- `specs/F-002-database-foundation/` SPEC/DESIGN/TASKS/TESTS consistent with implementation (migration filename corrected to .js at design stage)
|
||||
|
||||
## Verification
|
||||
- package.json script keys vs README commands cross-checked: all db:/docker: commands documented; remaining scripts (build, start, test, typecheck, lint, lint:boundaries) already documented in F-001 section.
|
||||
|
||||
## Pending docs (not this ticket)
|
||||
- Per-module data access contracts documented as each module lands (F-005+).
|
||||
63
work/artifacts/F-002/implementer.md
Normal file
63
work/artifacts/F-002/implementer.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Implementer — F-002 Database foundation with module-owned schemas
|
||||
|
||||
done -> work/artifacts/F-002/implementer.md
|
||||
|
||||
## Files created (all under project/ unless noted)
|
||||
- docker-compose.yml (postgres:16-alpine + redis:7-alpine, named volumes, healthchecks, dev-only creds)
|
||||
- .env.example
|
||||
- migrations/001_baseline.js (node-pg-migrate; citext + pgcrypto extensions, app_meta table, with down)
|
||||
- src/infrastructure/db/pool.ts (createPoolFromEnv fail-fast, typed query helper)
|
||||
- src/infrastructure/db/tests/db-test-support.ts (recreateDatabase, runMigrations, tableExists helpers; strict identifier validation for DDL-interpolated db name)
|
||||
- src/infrastructure/db/tests/db-test-support.test.ts (pure helper tests incl. identifier rejection)
|
||||
- src/infrastructure/db/tests/migrations.itest.ts
|
||||
- src/infrastructure/db/tests/pool.itest.ts
|
||||
- scripts/db-status.mjs
|
||||
|
||||
## Files modified
|
||||
- package.json: deps (pg, node-pg-migrate; @types/pg dev) + scripts (test:integration, docker:up/down, db:up/down/status)
|
||||
- vitest.config.ts: include *.itest.ts; fileParallelism false (integration files share one test DB)
|
||||
- .gitignore: .env
|
||||
- README.md: Database section + table naming convention
|
||||
- spec/tech.md: dependency justification rows for pg / node-pg-migrate / @types/pg
|
||||
- specs/F-002-database-foundation/DESIGN.md + TASKS.md: migration filename corrected to .js (node-pg-migrate native format)
|
||||
|
||||
## Database migrations
|
||||
- 001_baseline: extensions citext + pgcrypto, table app_meta(key, value, updated_at). Down fully reverts.
|
||||
- Tracking table: pgmigrations (owned by node-pg-migrate).
|
||||
|
||||
## API changes
|
||||
- None.
|
||||
|
||||
## Tests added
|
||||
- migrations.itest.ts: fresh up creates schema / second up is no-op / down rolls back cleanly
|
||||
- pool.itest.ts: SELECT 1 roundtrip / app_meta insert-read-delete via query helper / fail fast without DATABASE_URL
|
||||
|
||||
## Tests passed (evidence)
|
||||
```
|
||||
npm run docker:up -> mdv-dev-postgres Healthy, mdv-dev-redis Healthy
|
||||
npm run db:up (1st) -> MIGRATION 001_baseline (UP), Migrations complete!
|
||||
npm run db:up (2nd) -> No migrations to run! (no-op)
|
||||
npm run db:status -> 001_baseline listed as applied
|
||||
npm run db:down -> MIGRATION 001_baseline (DOWN); psql to_regclass('public.app_meta') -> empty
|
||||
npm run db:up (again) -> re-applies cleanly
|
||||
npm run lint -> OK
|
||||
npm run lint:boundaries -> Boundary check OK: 11 file(s) checked
|
||||
npm run typecheck -> exit 0
|
||||
npm run build -> exit 0
|
||||
npm test -> 4 files passed, 2 integration files skipped explicitly without TEST_DATABASE_URL (11 passed | 6 skipped)
|
||||
npm run test:integration -> Test Files 2 passed (2), Tests 6 passed (6) against real PostgreSQL 16
|
||||
docker exec mdv-dev-redis redis-cli ping -> PONG
|
||||
```
|
||||
|
||||
## Known limitations
|
||||
- node-pg-migrate programmatic runner requires explicit migrationsTable (passed 'pgmigrations' in the test helper to match CLI default).
|
||||
- Integration tests drop/create the shared test database; vitest fileParallelism disabled to avoid the race (documented in vitest.config.ts).
|
||||
- No connection retry/backoff yet; not needed while only dev scripts connect.
|
||||
|
||||
## Follow-up work
|
||||
- F-004 will centralize env/config (current fail-fast reader stays until then).
|
||||
- First real module tables arrive with F-005+ following <module>_<table> convention.
|
||||
|
||||
## Security hardening round (requested by security gate)
|
||||
- db-test-support.ts now validates the database name from TEST_DATABASE_URL against /^[a-zA-Z_][a-zA-Z0-9_]*$/ before interpolating it into DROP/CREATE DATABASE DDL; rejection covered by db-test-support.test.ts.
|
||||
- Re-verified: lint, typecheck, npm test (11 passed), test:integration (6 passed).
|
||||
34
work/artifacts/F-002/leader-close.json
Normal file
34
work/artifacts/F-002/leader-close.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"feature_id": "F-002",
|
||||
"agent": "leader",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "F-002 closed. Database foundation implemented and proven against real PostgreSQL 16 + Redis 7: migrations up/down/no-op, fail-fast pool, one-command dev environment, naming convention documented. All gates APPROVED, verify.sh exit 0.",
|
||||
"gates": {
|
||||
"reviewer": "APPROVED (reviewer.json)",
|
||||
"security": "APPROVED (security.json, after identifier-validation hardening round)",
|
||||
"qa": "APPROVED (qa.json)",
|
||||
"verify_sh": "exit 0"
|
||||
},
|
||||
"deliverables": [
|
||||
"project/migrations/001_baseline.js with working down migration",
|
||||
"project/src/infrastructure/db/pool.ts (fail-fast pool + typed query helper)",
|
||||
"project/docker-compose.yml (postgres:16-alpine + redis:7-alpine)",
|
||||
"npm scripts: db:up/down/status, docker:up/down, test:integration",
|
||||
"6 integration tests passing against real PostgreSQL; helper identifier validation regression-tested",
|
||||
"specs/F-002-database-foundation complete; spec/tech.md dependency justifications"
|
||||
],
|
||||
"process_notes": [
|
||||
"Security gate bounced one low finding (DDL identifier interpolation in test support) back to build; fixed with strict validation + tests, then re-approved. The gate worked as designed."
|
||||
],
|
||||
"next_feature_hint": "F-003 (HTTP foundation/request context) and F-004 (config/flags) only depend on F-001; F-005 identity now unblocked by F-002",
|
||||
"evidence": [
|
||||
"work/artifacts/F-002/architect.md",
|
||||
"work/artifacts/F-002/implementer.md",
|
||||
"work/artifacts/F-002/reviewer.json",
|
||||
"work/artifacts/F-002/security.json",
|
||||
"work/artifacts/F-002/qa.json",
|
||||
"work/artifacts/F-002/documenter.md",
|
||||
"./scripts/verify.sh exit 0 at close"
|
||||
],
|
||||
"timestamp": "2026-08-14T20:04:00Z"
|
||||
}
|
||||
42
work/artifacts/F-002/qa.json
Normal file
42
work/artifacts/F-002/qa.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"feature_id": "F-002",
|
||||
"agent": "qa",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "All 5 acceptance criteria verified with fresh executions against real PostgreSQL 16 + Redis 7 in docker.",
|
||||
"traceability": [
|
||||
{
|
||||
"criterion": "AC1: fresh database + migrations up creates schema; rerun is no-op",
|
||||
"test": "migrations.itest.ts (recreates DB fresh each run) + CLI: npm run db:up on applied DB",
|
||||
"result": "PASS (fresh up -> app_meta exists; second up -> 'No migrations to run!')"
|
||||
},
|
||||
{
|
||||
"criterion": "AC2: down rolls back cleanly",
|
||||
"test": "migrations.itest.ts down case + reviewer CLI roundtrip (db:down verified app_meta gone, db:up re-applied)",
|
||||
"result": "PASS"
|
||||
},
|
||||
{
|
||||
"criterion": "AC3: table naming convention documented and enforced",
|
||||
"test": "README.md 'Table naming convention' section + baseline exemplar app_meta + immutability rule",
|
||||
"result": "PASS"
|
||||
},
|
||||
{
|
||||
"criterion": "AC4: dev PostgreSQL and Redis start with one command",
|
||||
"test": "full docker:down then npm run docker:up",
|
||||
"result": "PASS (both containers Healthy; pg_isready accepting connections; redis-cli ping -> PONG)"
|
||||
},
|
||||
{
|
||||
"criterion": "AC5: verify.sh green",
|
||||
"test": "./scripts/verify.sh",
|
||||
"result": "PASS (exit 0)"
|
||||
}
|
||||
],
|
||||
"regressions": "PASS - F-001 suite re-run green: lint, typecheck, build, unit tests (11 passed | 6 skipped without DB), /health unaffected (no HTTP change)",
|
||||
"evidence": [
|
||||
"npm run test:integration -> Test Files 2 passed (2), Tests 6 passed (6)",
|
||||
"npm run db:up (applied DB) -> No migrations to run!",
|
||||
"docker:down + docker:up -> mdv-dev-postgres Healthy, mdv-dev-redis Healthy, pg_isready OK, redis PONG",
|
||||
"npm run lint/typecheck/build/test -> all exit 0",
|
||||
"./scripts/verify.sh -> exit 0"
|
||||
],
|
||||
"timestamp": "2026-08-14T20:02:00Z"
|
||||
}
|
||||
31
work/artifacts/F-002/reviewer.json
Normal file
31
work/artifacts/F-002/reviewer.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"feature_id": "F-002",
|
||||
"agent": "reviewer",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "Database foundation matches specs/F-002 DESIGN.md. Migration tooling demonstrated against real PostgreSQL 16, pool helper fail-fast, compose one-command dev environment, naming convention documented. No blockers.",
|
||||
"checks": {
|
||||
"design_conformance": "PASS: src/infrastructure/db/pool.ts, migrations/001_baseline.js, docker-compose.yml, npm scripts all present as designed; --env-file-if-exists used instead of strict --env-file (safer, equivalent intent)",
|
||||
"migration_lifecycle": "PASS (re-executed by reviewer): db:up applies 001_baseline; db:down reverts; db:up re-applies; second up is no-op",
|
||||
"naming_convention": "PASS: <module>_<table> documented in README with rules; baseline table app_meta follows prefix rule; no business tables introduced",
|
||||
"test_hygiene": "PASS: integration tests skip explicitly without TEST_DATABASE_URL (npm test green without DB); shared-DB race resolved by disabling fileParallelism with inline justification",
|
||||
"boundary_rules": "PASS: lint:boundaries clean over 11 files; db code lives in infrastructure, no module touched"
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"severity": "info",
|
||||
"note": "node-pg-migrate programmatic runner needs explicit migrationsTable; test helper pins 'pgmigrations' to match CLI default. Documented in implementer.md known limitations."
|
||||
},
|
||||
{
|
||||
"severity": "info",
|
||||
"note": "F-004 will centralize config; current minimal fail-fast env reader in pool.ts is acceptable and called out as follow-up."
|
||||
}
|
||||
],
|
||||
"evidence": [
|
||||
"npm run lint / typecheck / lint:boundaries / test -> all exit 0",
|
||||
"npm run test:integration -> Test Files 2 passed (2), Tests 6 passed (6) against PostgreSQL 16 in docker",
|
||||
"reviewer re-run: db:down then db:up -> 001_baseline DOWN then UP, Migrations complete",
|
||||
"docker exec mdv-dev-redis redis-cli ping -> PONG (from build stage)",
|
||||
"files reviewed: project/migrations/001_baseline.js, src/infrastructure/db/**, docker-compose.yml, scripts/db-status.mjs, README.md, spec/tech.md"
|
||||
],
|
||||
"timestamp": "2026-08-14T19:57:00Z"
|
||||
}
|
||||
28
work/artifacts/F-002/security.json
Normal file
28
work/artifacts/F-002/security.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"feature_id": "F-002",
|
||||
"agent": "security",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "Security gate passed. Zero audit vulnerabilities with new deps (pg, node-pg-migrate), no secrets in code, .env gitignored, parameterized queries everywhere, and the one DDL interpolation point is now guarded by strict identifier validation with regression tests.",
|
||||
"checks": {
|
||||
"secrets": "PASS: no hardcoded secrets in src/scripts/migrations; dev compose credentials are explicitly documented as dev-only; .env confirmed gitignored (git check-ignore .env)",
|
||||
"dependencies": "PASS: npm audit -> 0 vulnerabilities; pg/node-pg-migrate/@types/pg justified in spec/tech.md",
|
||||
"sast_basic": "PASS: no eval/new Function; template-literal SQL limited to 2 DDL statements in test support, guarded by /^[a-zA-Z_][a-zA-Z0-9_]*$/ validation in dbNameFromUrl (rejection covered by db-test-support.test.ts)",
|
||||
"input_validation": "PASS: pool helper uses $n parameterized queries exclusively for data access",
|
||||
"exposure": "INFO accepted: compose binds 5432/6379 on host for local dev only; documented in README"
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"severity": "low",
|
||||
"note": "DDL identifier interpolation in test support",
|
||||
"resolution": "MITIGATED this gate round: strict identifier validation added before any DDL use + unit tests rejecting bad\"name and semi;colon cases"
|
||||
}
|
||||
],
|
||||
"evidence": [
|
||||
"npm audit -> found 0 vulnerabilities",
|
||||
"git check-ignore .env -> ignored",
|
||||
"grep secret scan over src/scripts/migrations -> none",
|
||||
"grep for template-literal queries -> only the 2 guarded DDL statements remain",
|
||||
"npm test after hardening -> 11 passed | 6 skipped; test:integration -> 6 passed"
|
||||
],
|
||||
"timestamp": "2026-08-14T20:00:00Z"
|
||||
}
|
||||
Reference in New Issue
Block a user