Tasks · 002-downstream-consumers

Downstream consumers

Ordered, parallelizable work. Tests precede implementation. Drive through with /spectastic.implement — one task per invocation.

Status Accepted Spec 002-downstream-consumers Design design Branch 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

Add H2 to the test classpath — testImplementation 'com.h2database:h2:2.3.232' (the version the design's grant spike ran against); 003 promotes it to implementation. build.gradle

3 · 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.

Define the event and the outbound port — 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/
Implement the synchronous bus — 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.java
Write the store DDL and the grant — schema.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)

Test that booking reaches Risk through the adapter's event — wire 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.java
Move ADR-0007's fitness function into the build — git 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.java
Test D-008's grant against H2 — run db/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.java

Implementation

Make the adapter the single emitter — 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.java
Implement the Risk exposure view — subscribes on construction; long exposure(String instrument) returns the last quantity seen (0 when none) — spec FR-002. src/main/java/xyz/briancorbin/pk/hex/consumers/RiskExposureView.java
Write the ArchUnit → SARIF converter — SarifReport.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
Exercise the emission path and its fitness function — run ./gradlew test --tests '*DownstreamConsumersTest' --tests '*DataAccessRulesTest' --tests '*GrantBackstopTest'; confirm all green and that build/reports/archunit/archunit-results.sarif exists with zero results.

Closes FR-001, FR-002, FR-004, NFR-001, SC-001.

5 · Phase 3b — US2 · P&L revalues once per tradeId

Tests (write & fail first)

Test that a redelivered event revalues once — deliver (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.java

Implementation

Implement the idempotent P&L revaluer — subscribes on construction; keeps the set of tradeIds seen and skips a repeat; otherwise records the quantity per instrument and increments the revaluation count — spec FR-003, FR-005. src/main/java/xyz/briancorbin/pk/hex/consumers/PnlRevaluer.java
Exercise the revaluation path — run ./gradlew test --tests '*PnlRevaluerTest' and confirm one revaluation for two deliveries.

Closes FR-003, FR-005, SC-002.

6 · Phase 4 — Polish

Confirm the full build is green with the moved rule — ./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

  1. 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).
  2. 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.