| Stories delivered | 13 of 13 (100%) |
| Test growth | Backend 4 → 155, Frontend 18 → 45, no regressions |
| Course corrections absorbed mid-epic | 2 (Logout SCP, Spreadsheet-Source SCP) |
| Stories that landed clean (≤1 review patch) | 1-1, 1-2, 1-12, 1-13 |
| Stories with heavy review cycles (6+ patches) | 1-3, 1-7, 1-8, 1-9, 1-10, 1-11 (14 patches) |
| Reusable patterns hardened | IAuditService (append-only), ILegacyLinkedRecord(Provider), AuthIntegrationTestFactory |
| New CI gate | .gitea/workflows/release-gates.yml |
ILegacyLinkedRecord from Story 1.8 became the integration seam every later municipality story (1-10, 1-12) plugged into automatically.done.IAuditService having no update/delete methods is now the de-facto pattern.SeedAsync() was a placeholder and not DI-registered, 1-11 wouldn't compile.ReadOnlyCommandGuard rejecting valid SELECTs (1-6), zero/negative IDs accepted (1-8), raw-string rules with no scope metadata (1-9).AuthIntegrationTestFactory had to override IAuditService (1-5) and again ILegacyDataAccess (1-10). Pattern emerged twice — now a guardrail.Routing the work to the right model matters. Local-hosted AI handled bounded CRUD/UI-binding work (1-1, 1-2, 1-12, 1-13) cleanly. The same model struggled where the AC was satisfied by a stub but the actual deliverable was system-level wiring — DI registration, scheduler hookup, pipeline integration, real implementation behind an interface. That's exactly the heavy-rework set: 1-7, 1-8, 1-9, 1-11.
Story 1-11 (confirmed local AI, 14 review patches, didn't compile on first pass) is the canonical signature of the mismatch.
ai_routing field at creation timeAdd to story frontmatter:
ai_routing: local-ok # CRUD shape, bounded surface, contract == behavior
ai_routing: cloud-only # infrastructure, wiring, multi-component integration
Heuristics:
local-ok: single-table CRUD, UI binding to existing API, read-only views, isolated test additions.cloud-only: introduces a new cross-cutting contract, requires DI registration in Program.cs, requires scheduler/pipeline hookup, or has a “real implementation behind interface” requirement.Routing applied to Epic 2:
| Story | Routing |
|---|---|
| 2.1 Municipality-to-Cycle Kanban Entry Point | cloud-only |
| 2.2 Create Election-Cycle Job | local-ok |
| 2.3 Election-Cycle Key Dates | local-ok |
| 2.4 Prior-Cycle Defaults Application | cloud-only |
| 2.5 Readiness Status & SafeCommitRail | cloud-only |
| 2.6 Spreadsheet Import & Column Mapping | cloud-only |
Owner: Daniel
Trigger: Apply during bmad-create-story for next Epic 2 story
Success criteria: Every Epic 2 story has ai_routing set before development begins
Add a standard AC to any cloud-only story:
“The production caller invokes this on the real path — verified by an integration test that fails if DI registration, scheduler hookup, or pipeline gate is missing.”
Why: Would have caught 1-7, 1-8, 1-9, 1-11 on first pass. Closes the “contract satisfies AC but production wiring is absent” gap. Owner: Daniel (apply at story creation) Success criteria: No cloud-only story merged without this AC verified
Make TDD non-negotiable on cloud-only stories: failing test added and committed before implementation. Optional on local-ok stories.
Why: 1-12 and 1-13 used this discipline and combined for 1 review patch. 1-11 (no tests-first) took 14. Owner: Daniel (enforce via dev-story workflow) Success criteria: Cloud-only story records explicitly note “tests added before implementation”
Daniel chose to carry these forward rather than schedule a hardening pass. Track and address opportunistically inside Epic 2 stories where they intersect.
HIGH severity
AuthorizationProbeController ships canned operational routes in production controllers (from 1-4)workspacePath not validated as relative path → latent open-redirect (from 1-3)MEDIUM severity
GetAllJurisdictionsAsync and GetByJCode (from 1-10) — relevant to Story 2.2 join pathsOutcome="updated display name" becomes misleading once profile gains fields (from 1-10)pendingCallbackSequence not scoped per callback invocation; double-mount of useOidcSession would skip CSRF (from 1-3)LOW severity
ProfileId uses Guid.ToString("N") (no hyphens) — cross-system UUID format riskCreatedAt stored but absent from API DTO (1-10)MunicipalityAddress.State is free-text with no validation (1-11)MunicipalityId existence not validated pre-insert; surfaces as DbUpdateException (1-11)refresh() doesn't reload jurisdiction list (1-10)| Dimension | Status |
|---|---|
| Testing & Quality | Solid — 155 backend / 45 frontend, monotonic growth |
| Deployment | Local-only (no production deploy planned for Epic 1 alone) |
| Stakeholder Acceptance | Deferred — no external review until Epic 2+ adds visible value |
| Technical Health | Feels solid (Daniel's gut-check) |
| Unresolved Blockers | None — deferred items carried forward, not blocking |
6 stories, all already at ready-for-dev. Direct dependencies on Epic 1 work are intact:
No epic update required. Nothing from Epic 1 invalidates Epic 2's current plan. Story 2.6 (spreadsheet import) was added via SCP and is already accounted for in the story list.
None. Daniel can begin bmad-create-story for Story 2.1 in a fresh context window.
None requiring epic plan updates.
Retrospective facilitated 2026-05-07.
Powered by TurnKey Linux.