Insert a new Phase 1 (deployment: Compose + Traefik on a server, marked Done) between the base project and the OTel concepts phase, and shift the old phases 1-10 to 2-11. Update summary, intro, README and CLAUDE.md references (spoiler rules now target Phase 9, the bottleneck hunt). Add docker-compose.traefik.yml: an override that fronts the gateway with Traefik and Let's Encrypt and removes all other published host ports. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
7.1 KiB
Phase 0 — Build the base project, test-first
Part of docs/roadmap/ — see intro.md for platform/background context and summary.md for how every phase fits together. Prev: intro.md · Next: phase-1.md
Status: Done
Message: Both halves complete and verified against the live stack, not just collected.
- 0a (test suite): 81 tests across
tests/(11 files + a 5-module harness), covering all eleven goals G1–G11 and all 13 named pathologies. Verified genuinely red against the empty repo before any implementation existed;tests/RED-BASELINE.mdrecords that run.tests/README.mddocuments the invented request-contract fields, measurement methodology, and known-flaky tests. - 0b (implementation): all seven services (
gateway,ledger,fraud,sanctions,insights,notifier,audit),db/schema.sql+optional_indexes.sql,scripts/seed.py+smoke.py, the frontend, anddocker-compose.yml. Seeded: 800 accounts, 250,000 transactions, 45 merchant categories, 12,000 sanctions entries, in ~12s.make smokeandmake test-contract(64/64) pass;make test-baselineandmake test-pathologypass except for a handful of tests documented below as load-sensitive rather than broken. - Two changes to the frozen test contract, both made only after flagging
them and getting an explicit go-ahead — not applied unilaterally:
test_row_counts_match_manifest_and_targetsloosened thetransactionsrow-count check from exact equality to>=, since other contract tests in the samemake test-contractrun legitimately insert rows first.test_pool_starvation_plateaus_throughputwas removed outright. Its baseline assertion (throughput scales ≥2.5x from concurrency 5→25) is incompatible with this same spec's own §3 mandate of one uvicorn worker per service — confirmed by reproducing the identical non-scaling behaviour on a bareGET /healthzwith zero backend logic.pool_starvationitself is still fully implemented (services/ledger/db.py's dual-engine pool selection); only that one differential test was dropped.
- Known-flaky, documented rather than papered over:
test_clean_baseline_is_healthy,test_nightmare_degrades_but_survives, the baseline half oftest_blocking_event_loop_serializes_concurrent_requests, andtest_missing_index_forces_seq_scancompare a fast-path measurement against a slow-path one under real concurrent load, and pass at roughly a 60–80% rate on ordinary dev hardware depending on momentary host contention. See "Known-flaky tests" intests/README.md. - Notable implementation deviations (none change observable behaviour of
the 13 pathologies or the request/response contract): gateway's
GET /serves the frontend rather than §4's generic info JSON (the other five HTTP services still return that JSON as specified);insightssplits a cheap PK-onlyquick_summary(embedded in every transaction response) from the real cached/N+1-affected 120-day analytics endpoint, so the missing-index pathology doesn't tax every request;retry_stormfires all 4 attempts unconditionally rather than stopping at first success (spec's "no circuit breaker" taken literally — also the only way to reach the required lookup-multiplication ratio givensanctions_errors' fixed 3% rate);poison_dataresolves via a Redis hash populated at seed time instead of a live table scan, to avoid the deliberately missing index; uvicorn's access log is disabled fleet-wide (redundant with the spec's own logging design, and was double-counting two log-line-based pathology tests); Postgresmax_connectionsraised from 100 to 300 (five services' pool sizes can sum past the default under load).
Time-box: 2–3 evenings. Two agent sessions plus one evening of your own reading.
Deliverable: the working Ledgerline system with no observability, plus a black-box integration suite that pins down both its working state and its deliberate brokenness.
Why TDD here, when the app itself doesn't matter
The microservices are throwaway. The properties aren't. Everything from Phase 3 onward assumes the system still behaves a specific way: seven real services in the path, async work genuinely off the request path, five hot accounts owning a third of the rows, and thirteen pathologies that actually degrade something. You will be editing all seven services for weeks. Without a suite that fails when one of those properties quietly breaks, you'll spend an evening chasing a trace anomaly that turns out to be a regression you introduced yesterday.
Writing the tests first also forces the contract to be explicit before anything is built — which is what makes a second agent able to implement against it without guessing.
0a — the test suite
Agent session one, from phase-0a-test-suite-spec.md. That agent writes only
tests/ and must not create so much as a stub under services/. Its output
should be red, with a tests/RED-BASELINE.md recording why each test fails
right now — a test that passes against an empty repo is a broken test.
The suite asserts goals, not mechanics. Two representative examples worth understanding before you read the code:
- "Stop the
sanctionscontainer and transactions must fail." This is the test that catches an implementation quietly collapsing seven services into one process. Nothing else would. - "With
n_plus_oneon, one insights request issues ≥ 50 SQL statements." Measured viapg_stat_statements, not wall clock — because a query count can't be faked with asleep(), and a latency threshold can.
That second point is the design principle throughout: prefer assertions that
are hard to satisfy without the real mechanism. Query counts, EXPLAIN plan
node types, Redis consumer-group lag, container RSS, and genuine unhandled
exception paths all have that property.
0b — the implementation
Agent session two, from phase-0-base-project-spec.md, with tests/ handed over
as a read-only contract. That agent may not edit, skip, xfail or loosen a
single test. If something looks impossible it stops and reports rather than
adjusting the target.
Done when make test is green three consecutive runs and
git diff --stat tests/ is empty.
Then, yourself
- Read every service's
app.pyend to end. You need to know this codebase cold, because from Phase 3 onward you're modifying all seven services. - Run
make chaos-nightmare, fire a 200-request burst from the frontend, and then try to figure out what's wrong using onlydocker compose logs. Give it twenty real minutes. You will fail, and the specific way you fail — no timing, no causality, no cross-service correlation, aX-Request-Idthat dies at the Redis Stream — is the motivation for everything that follows. Write down what you wished you had. That list is your acceptance criteria for Phase 9.
Done when: make test is green three runs in a row, all nine acceptance
criteria in the base spec pass, and you have that written list of frustrations.