Tasks · 002-downstream-consumers
Downstream consumers
Ordered, parallelizable work. Tests precede implementation. Drive through with
/spectastic.implement — one task per invocation.
build-out
Created
Read time
14 tasks across four phases. Setup (the H2 test dependency) → Foundational (the event, the bus port, D-006's bus,
D-008's DDL) → User stories (US1 the adapter emits and Risk hears · US2 P&L is idempotent) → Polish. Tasks marked
[P] can run in parallel — different files, no dependency. Tests for a story MUST
be written and failing before its implementation tasks start. US1 also lands ADR-0007's fitness function where the build
runs it — DataAccessRulesTest moves from enforcement/ into src/test and gains a SARIF
writer, since ArchUnit ships no emitter of its own.
1 · Execution strategy
Setup → Foundational → US1 → US2 → Polish. US1 is the feature's whole point (the emission guarantee and its fitness function); US2 is the consumer-side courtesy. This is the tab in use.
US1 alone — Risk hears every change and the ArchUnit rule proves the adapter is the only writer.
Not applicable — one engineer.
2 · Phase 1 — Setup
testImplementation 'com.h2database:h2:2.3.232' (the version the design's grant spike ran against); 003 promotes it to implementation. build.gradle3 · Phase 2 — Foundational
The event and its port belong to the JDK-only core; the bus is D-006; the DDL is D-008's substance.
record PositionChanged(String instrument, long quantity, String tradeId) and interface EventBus { void emit(PositionChanged); void subscribe(Consumer<PositionChanged>); }; java.util.function only, so CoreRulesTest still holds. src/main/java/xyz/briancorbin/pk/hex/core/SyncEventBus: an ordered list of subscribers, emit calls each in turn on the caller's thread before returning (D-006; spec NFR-001). src/main/java/xyz/briancorbin/pk/hex/events/SyncEventBus.javaschema.sql: positions(instrument VARCHAR PRIMARY KEY, quantity BIGINT NOT NULL) and applied_trades(trade_id VARCHAR PRIMARY KEY, instrument VARCHAR NOT NULL); grants.sql: CREATE USER pk_adapter, CREATE USER pk_reader, GRANT SELECT, INSERT, UPDATE ON positions TO pk_adapter, GRANT SELECT, INSERT ON applied_trades TO pk_adapter, GRANT SELECT ON positions TO pk_reader — D-008's backstop as files. db/4 · Phase 3a — US1 · A booked trade reaches Risk, and nothing changes a position silently
Tests (write & fail first)
SyncEventBus → InMemoryPositionRepository(bus) → PositionService, subscribe a RiskExposureView and a recording subscriber; book t-1 AAPL +100 and assert the view's exposure for AAPL is 100 before book returns, the recorded event is (AAPL, 100, t-1), and re-booking t-1 records no second event. src/test/java/xyz/briancorbin/pk/hex/consumers/DownstreamConsumersTest.javagit mv enforcement/DataAccessRulesTest.java here; keep the rule name only_the_persistence_adapter_touches_data_access_apis (D-007's run= names this class); rewrite as a plain @Test that imports production classes only (DO_NOT_INCLUDE_TESTS — the grant test opens H2), evaluates the rule, always writes SarifReport to build/reports/archunit/archunit-results.sarif, then asserts no violation. src/test/java/xyz/briancorbin/pk/enforcement/DataAccessRulesTest.javadb/schema.sql and db/grants.sql into an in-memory H2 as sa; assert UPDATE positions as pk_reader throws SQL state 90096 and as pk_adapter updates one row. src/test/java/xyz/briancorbin/pk/hex/persistence/GrantBackstopTest.javaImplementation
InMemoryPositionRepository(EventBus); write updates the map and the applied set, then emit(new PositionChanged(instrument, quantity, tradeId)) on the same call (spec FR-001, FR-004, NFR-001); keep a no-arg constructor over a fresh SyncEventBus so 001's test is untouched. src/main/java/xyz/briancorbin/pk/hex/persistence/InMemoryPositionRepository.javalong exposure(String instrument) returns the last quantity seen (0 when none) — spec FR-002. src/main/java/xyz/briancorbin/pk/hex/consumers/RiskExposureView.javaSarifReport.write(Path out, String ruleId, EvaluationResult): SARIF 2.1.0, tool driver ArchUnit, one result per failure detail with ruleId, message.text = the detail, and a physicalLocation whose uri is src/main/java/<fqcn as path>.java and startLine the (File.java:NN) the detail names; an empty results array when the rule holds. src/test/java/xyz/briancorbin/pk/enforcement/SarifReport.java./gradlew test --tests '*DownstreamConsumersTest' --tests '*DataAccessRulesTest' --tests '*GrantBackstopTest'; confirm all green and that build/reports/archunit/archunit-results.sarif exists with zero results.5 · Phase 3b — US2 · P&L revalues once per tradeId
Tests (write & fail first)
(AAPL, 100, t-1) twice through a SyncEventBus to a PnlRevaluer; assert revaluations() is 1 and marked("AAPL") is 100; deliver t-2 and assert 2. src/test/java/xyz/briancorbin/pk/hex/consumers/PnlRevaluerTest.javaImplementation
./gradlew test --tests '*PnlRevaluerTest' and confirm one revaluation for two deliveries.6 · Phase 4 — Polish
./gradlew build exits 0; enforcement/ no longer holds a Java file (the fixture SARIFs stay until stage 7 replaces them with real output); spectastic validate clean. enforcement/7 · Dependencies
Setup → Foundational (H2 before the grant test)
Foundational → US1, US2 (T-010 first; T-011, T-012 in parallel)
US1 → US2 (the bus and the adapter's emission)
US1, US2 → Polish
8 · Change log
- Status flipped Draft → Accepted on 18 Sep 2026 — zero remaining unchecked tasks (14/14); ./gradlew build green with DownstreamConsumersTest (SC-001), PnlRevaluerTest (SC-002), DataAccessRulesTest and GrantBackstopTest verified passing per author confirmation. Sibling bundle (REQ-LIFECYCLE-005).
- Initial task breakdown — 14 tasks, 6 parallel; ADR-0007's ArchUnit rule moves into the Gradle test source set with a hand-written SARIF writer.