Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,31 @@ jobs:
- name: Build
run: dotnet build CritterCab.slnx --no-restore --configuration Release

# Pull every Testcontainers image serially BEFORE the test run, so nothing is fetched from
# Docker Hub while tests are starting.
#
# Why this is needed: xUnit runs the two test assemblies in parallel, and each one stands up
# its own Testcontainers session — a Ryuk resource reaper plus a Postgres per fixture plus a
# Kafka broker. Once Dispatch grew its own Kafka-backed fixture (W006 slice 5) that became six
# containers and two reapers racing to pull at once, and the runner's connection to
# registry-1.docker.io started timing out mid-pull. The symptom is misleading: the failure
# surfaces as DockerApiException "context deadline exceeded" inside
# ResourceReaper.GetAndStartNewAsync, in whichever suite happened to lose the race, so it
# reads like a broken test rather than a saturated network.
#
# After this step the images are local and the fixtures' PullPolicy.Missing makes every pull a
# no-op. Keep this list in sync with the tags pinned in the fixtures — a tag that drifts out
# of sync here does not fail the build, it just quietly restores the old behaviour.
#
# The ryuk tag is NOT ours to choose: Testcontainers pins it internally (4.13.0 → 0.14.0,
# by digest). Re-read it from the package when Testcontainers is bumped, otherwise this line
# pre-pulls an image nothing uses and the reaper goes back to racing for its own.
- name: Pre-pull Testcontainers images
run: |
docker pull postgres:18-alpine
docker pull confluentinc/cp-kafka:7.6.1
docker pull testcontainers/ryuk:0.14.0

- name: Test
run: >
dotnet test CritterCab.slnx
Expand Down
11 changes: 10 additions & 1 deletion apphost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,20 @@
// launch profile — the AppHost-declared endpoints are authoritative. gRPC rides
// the HTTPS endpoint via Kestrel HTTP/2; 5312 is reserved if a dedicated gRPC
// listener is ever needed. See docs/skills/aspire/SKILL.md § Port allocation.
//
// Dispatch LISTENS to telemetry.driver-location-updated (W006 §6.5) — its first transport, and the
// second half of CritterCab's first cross-service flow. Same named-connection arrangement as
// Telemetry below: the reference injects the broker address under the key "kafka", which is what
// UseKafkaUsingNamedConnection reads. Unlike Telemetry, Dispatch does NOT AutoProvision — the
// producer owns the topic. WaitFor is therefore ordering hygiene here rather than a hard
// requirement: a listener that starts before the broker retries, it does not fail.
builder.AddProject<Projects.CritterCab_Dispatch>("dispatch", launchProfileName: null)
.WithHttpsEndpoint(port: 5310, name: "https")
.WithHttpEndpoint(port: 5311, name: "http")
.WithReference(dispatchDb)
.WaitFor(dispatchDb);
.WaitFor(dispatchDb)
.WithReference(kafka)
.WaitFor(kafka);

// Telemetry is CritterCab's second service (stream-processing shape, W006). Ports follow
// the +5 slot convention after Dispatch's 5310; 5315 https / 5316 http. See
Expand Down
1 change: 1 addition & 0 deletions docs/prompts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,4 @@ Subsequent sections are prompt-specific. Existing prompts in this directory serv
- [`implementations/006-telemetry-skeleton-and-slice-1-config.md`](./implementations/006-telemetry-skeleton-and-slice-1-config.md) — **Second service in the repo** and the opening PR of the W006 Telemetry transport chain. Bootstraps the `CritterCab.Telemetry` service skeleton **and** W006 slice 1 (`TelemetryPolicyConfigured` config-as-events) in one PR, per the named skeleton-plus-first-slice cadence exception (mirrors the Dispatch skeleton + slice-5.1 precedent). Config-as-events is the dependency-correct first slice: slice 2's gRPC ingest reads this slice's `TelemetryPolicy` view (`throttlePolicyVersion`, `h3Resolution`, intervals). Establishes two firsts in code — **first config-as-events instance** (ADR-011's third instance, first realized; Dispatch/Onboarding were design-only) and **first FluentValidation boundary validation**. Four pre-flight `jasperfx-source-verifier` gates (`IInitialData` seed vehicle, `long` stream-version property, self-aggregating `LiveStreamAggregation` registration + Marten-9 `partial` scope, Wolverine.HTTP FluentValidation middleware). None of W006 §11's three ADR candidates fired (all later-arc), but the Phase-2 audit surfaced an ADR-011 Option-A/B-for-Marten gap that — **per user direction, expanding the session mid-flight** — was resolved in-PR via an **ADR-011 amendment** (`IInitialData` as the canonical Marten Option-A realization; LWW for config singletons). No narrative anchor (PR #40: the narrative layer does not apply to Telemetry). Kafka deliberately **not** wired into `apphost.cs` this PR (transport lands with the slice that needs it — Dispatch-skeleton precedent). Substantive spec delta: W006 §6.1 designed → realized. Status: pending (authored 2026-07-10).
- [`implementations/007-telemetry-slices-4-and-2-transport.md`](./implementations/007-telemetry-slices-4-and-2-transport.md) — **CritterCab's first transport in code.** W006 slice 4 (`LastKnownPosition` store + heartbeat-absence eviction) and slice 2 (gRPC `ReportLocations` client-streaming ingest) in one PR under the **coupled-slices** reading of the cadence rule, not the skeleton-plus-first-slice exception (already spent on 006): slice 2's publish trigger evaluates against slice 4's document and slice 4's document is written only when slice 2 publishes, so building either alone means evaluating against a document that does not exist or writing one nothing reads. Establishes five firsts in code — **first gRPC surface serving traffic**, **first client-streaming RPC**, **first proto codegen** (`protos/` had been contracts with no consumer since PR #39), **first non-event-sourced document write path**, and **first recurring/scheduled work**. Retires the three-month client-streaming forward-constraint: WolverineFx.Grpc **6.21.0** auto-generates the shape, so nothing is hand-wired, and the two gRPC skills that still described the workaround are corrected in-session under the session-runner-blocking exception (a session cannot follow a skill telling it to hand-wire). Eleven Verify-before-wiring gates were source-verified before any code; gate 5 **corrected the prompt's own hypothesis** (Wolverine has no recurring-message primitive — the idiom is a plain `BackgroundService`) and gate 11 dissolved (Alba's `TestServer` feeds a `GrpcChannel` directly, so the Alba-first default holds for gRPC). Two W006 under-specifications surfaced and were resolved by user sign-off rather than silently: the `LastKnownPosition` field set (`lastPublishedAt` collapsed into `serverReceivedAt`, since upsert-on-publish-only makes them one instant) and the `accuracyMeters` threshold (**100m, invented at implementation time — W006 names the threshold but fixes no value**). Also fixed `apphost.cs`, which had not compiled since PR #42 and which CI does not build. Substantive spec delta: W006 §6.2 + §6.4 designed → realized; the §11 *windowed client-streaming* ADR candidate lands as a skill, not an ADR. Status: complete (authored 2026-07-20, executed 2026-07-24). Produced retro at [`retrospectives/implementations/007-telemetry-slices-4-and-2-transport.md`](../retrospectives/implementations/007-telemetry-slices-4-and-2-transport.md).
- [`implementations/008-telemetry-slice-3-kafka-publish.md`](./implementations/008-telemetry-slice-3-kafka-publish.md) — **CritterCab's first Kafka topic and second live transport.** W006 slice 3: `DriverLocationUpdated` published to `telemetry.driver-location-updated`, partitioned by `driverId`, dedup-keyed on `serverReceivedAt`, no outbox. Deliberately the smallest session that could make a transport real — PR #45 had already put the proto through codegen and typed the `IDriverLocationPublisher` seam against the generated `DriverLocationUpdated`, so this session swapped one implementation class and the host wiring; **`ReportLocationsHandler.cs` does not appear in the diff.** Also wires Aspire's Kafka resource (host port 5392, a slot the `aspire` skill had reserved before any broker existed) and a Kafka Testcontainer on its own fixture, so the slice-1/2/4 suites never wait on a broker. **The load-bearing finding is a gap between spec and library defaults** that the authoring-time `jasperfx-source-verifier` pass caught: W006 §6.3 argues publish-before-store from failure-mode asymmetry, but that argument silently assumes the publish's outcome is known when the store runs — and a Kafka publishing endpoint defaults to `BufferedInMemory` (returns before the broker acks), while even `SendInline()` alone is insufficient because Wolverine's default async retry block swallows the send failure and returns success. Resolved by user sign-off to `SendInline()` + process-global `Durability.UseSyncRetryBlock` + `UseIdempotentProducer()`, and recorded in W006 as an **amendment, not a correction** (§6.3's reasoning stands; it did not name its own precondition). Fires W006 §11 candidate #1 as **[ADR-019 — Transport-Agnostic Topic Naming](../decisions/019-transport-agnostic-topic-naming.md)**, generalizing ADR-014's `<source-bc>.<event-name-kebab>` across transports while explicitly withholding its two ASB-specific operational clauses (session keying, outbox coordination) — which also serves as the ADR-004 design-return interleave for the third consecutive Telemetry implementation PR. **Second escalated fork:** `wolverine-kafka/SKILL.md` said Cab uses default JSON and deferred protobuf to "a future phase" — overridden to endpoint-scoped binary protobuf by user sign-off, since the payload is generated from the `.proto` ADR-009 makes the contract of record. That skill's topic-naming section had *also* independently proposed a rule contradicting ADR-014; both corrected in-PR under the session-runner-blocking exception, the listener half deferred to PR D as DEBT. **First prompt whose Verify-before-wiring gates all closed at authoring time**, and first whose `critter-skill-auditor` Phase 1 pass materially corrected an already-source-verified prompt (a missing governing skill, a wrong bootstrap lean, an already-allocated port, two skill-vs-spec contradictions). 33/33 green. Status: complete (authored + executed 2026-07-24). Produced retro at [`retrospectives/implementations/008-telemetry-slice-3-kafka-publish.md`](../retrospectives/implementations/008-telemetry-slice-3-kafka-publish.md).
- [`implementations/009-dispatch-w006-slice-5-nearby-available-drivers.md`](./implementations/009-dispatch-w006-slice-5-nearby-available-drivers.md) — **CritterCab's first cross-service flow**, and the session that completes W006's slice walk. W006 slice 5: Dispatch consumes `telemetry.driver-location-updated` into per-driver `AvailableDriver` documents, backs the slice-5.3 `INearbyAvailableDriversSource` port with an H3 k-ring query over them, and demotes `NearbyAvailableDriversStub` from production registration to test double — closing **W001 §10 parking-lot #4 in code** three months after the design closed it on paper. First code in `CritterCab.Dispatch` since prompt 005, and Dispatch's **first transport**: everything it did before was in-process Marten over HTTP. Also the first time two services compile the same `.proto` — deliberately not a shared assembly, which holds because Wolverine's message identity is `Type.FullName`-based and assembly-agnostic (the real coupling is the proto's `csharp_namespace`, not the assembly). `CandidateSelectionAutomation.cs` **does not appear in the diff**, which was the whole point of building the port as a seam in slice 5.3. **Second consecutive prompt whose Verify-before-wiring gates all closed at authoring time, and three of eight premises were contradicted by source** — most consequentially that Marten's `TryUpdateRevision` is an atomic single-statement LWW upsert, strictly better than the read-then-conditionally-store that "LWW upsert" naturally suggests and which would have been racy. `critter-skill-auditor` Phase 1 again corrected an already-source-verified prompt, this time catching that it **named the wrong governing skill** (`wolverine-marten-automation`, which is scoped to Marten-stream-forwarded events; replaced with `wolverine-handlers` + `wolverine-messaging-handlers`). Three durable forks resolved by user sign-off — availability-half exclusion with an unbound `DriverAvailabilityChanged` handler (the ASB half is a forward-constraint to an un-workshopped Driver Profile BC, and fabricating `Available`/`STANDARD` defaults was explicitly rejected), H3 query resolution read from the most recently ingested document rather than a Dispatch-side constant, and `EtaSeconds` derived from distance against a constant **invented at implementation time**. A **fourth** fork surfaced mid-session and was escalated rather than absorbed: the cold-start offset policy (`BeginAtLatest()`, chosen on W006 §6.4's own eviction logic, with a fixture warm-up handshake as the test cost — `BeginAtEarliest()` would have turned four failing tests green in one line while silently committing the service to replaying its entire retained topic on every fresh deploy). **A strengthened test caught a real geometry bug:** the k-ring derivation divided by the axis-aligned hexagon spacing (`edge × √3`) rather than the worst-case per-ring advance (`edge × 1.5`), so at Dispatch's production 5 km radius drivers near the edge were silently dropped — invisible at the 1 km value the first version of the test used. A `code-review` two-axis pass then found the session's most serious defect, after the source-verifier and BOTH auditor passes had run clean, with **both axes converging on it independently**: the implementation used one shared Marten revision column where W006 §6.5 locks "LWW per driver **per side**", so a heartbeat position stamped after an availability transition would silently discard it and leave an **offline driver dispatchable**. Fixed by separating business ordering (per-side timestamp comparison) from write concurrency (an incrementing revision plus a `ConcurrencyException` retry policy) — and the regression tests were themselves proven by reverting the old behaviour, after a first version of them passed against both implementations. Also drains the `wolverine-kafka` listener DEBT row **inside an implementation PR**, establishing that a row blocked on code that does not exist yet is drained by the session that writes the code. 57/57 green. Status: complete (authored + executed 2026-07-24). Produced retro at [`retrospectives/implementations/009-dispatch-w006-slice-5-nearby-available-drivers.md`](../retrospectives/implementations/009-dispatch-w006-slice-5-nearby-available-drivers.md).
Loading
Loading