Story 2.1 - Municipality-to-Cycle Kanban Entry Point
- ElectionCycleKanbanController (GET /api/election-cycles/kanban) with
ClientServicesAccess RBAC policy
- ElectionCycleKanbanReadModel aggregates active jobs and unassigned
municipalities into multi-lane board; legacy Access tables read-only
- Frontend: electionCycleKanbanView with keyboard-navigable lanes, card
windowing, sticky lane headers, and Unassigned lane always present
- Frontend: electionCycleKanbanContracts with fetch, focus management,
and visibility utilities
- InMemoryElectionCycleJobRepository with seed data for kanban display
- 5 integration tests covering grouping, unassigned municipalities,
inactive exclusion, RBAC enforcement, and legacy read-only invariant
Story 2.2 - Create Election-Cycle Job
- POST /api/election-cycles/jobs accepts existing cycle reference or
new cycle name; persists with "In Setup" status, actor identity,
and server timestamp in extension-layer storage only
- ElectionCycleJobsController with validation (rejects missing JCode
or cycle selection), audit emission via shared IAuditService, and
idempotent creates for duplicate jCode+cycleId pairs
- Frontend: CreateJobModal with existing-cycle selector, new-cycle-name
input, inline validation errors, and kanban reload on success
- "Create cycle job" button wired into Unassigned lane cards; card
relocates to selected cycle lane after creation
10 backend integration tests covering happy path, new cycle generation,
missing cycle/JCode validation, unauthenticated/wrong-role rejection,
audit event emission, legacy read-only invariant, idempotency, and 404
Repository:
- Add _lock to InMemoryMunicipalityProfileRepository; make duplicate-JCode
check + insert atomic in CreateAsync (TOCTOU fix) and wrap UpdateAsync
read-modify-write in lock (lost-update fix)
- Normalize JCode before passing to ILegacyLinkValidator so validation and
storage use the same form (was: raw input validated, ToUpperInvariant stored)
- UpdateAsync returns ProfileNotFound (new factory on MunicipalityProfileSaveResult)
so controller can distinguish 404 from 422
Controller:
- Null-guard post-save/update GetByIdAsync result instead of bang-deref
- Return 404 (not 422) when update target profile does not exist
- GET /api/municipalities/jurisdictions accepted as in-scope extension
Data access:
- FromJsonSeedFile: catch(Exception ex) with Console.Error.WriteLine;
DistinctBy(JCode) before projection to suppress duplicates in seed file
- 422 body shape mismatch: problem.error ?? 'Validation failed.' fallback
in createMunicipalityProfile and updateMunicipalityProfile
Frontend:
- Split Promise.all into independent profile + jurisdiction loads so a
jurisdiction failure does not suppress the profile table
- Add jurisdictionsLoadError state; disable "New" button and show warning
alert when jurisdictions are unavailable
Tests:
- Override ILegacyDataAccess in AuthIntegrationTestFactory with hardcoded
defaults, isolating integration tests from the development seed file
- Add wrong-role 403, jurisdiction endpoint 401, update-not-found 404,
and AC#3 audit assertion tests (create + update) to controller suite
- Add fetchAvailableJurisdictions contract tests (success + failure)
- Story 1.10 marked done; sprint-status synced
- wire legacy schema checks to live OleDb inspection and durable history
- harden schema baseline parsing and add constraint drift detection
- authenticate admin schema API calls from the client
- add release-gate workflow for legacy schema validation
- add extension-record save path with pre-save legacy link validation
- add link ambiguity detection, provider coverage reporting, and nightly integrity scheduler
- mark stories 1.7 and 1.8 done after review fixes
Introduces the ExtensionData namespace with the full contract and service
layer that future extension records (municipality profiles, election jobs,
service configs) will use to store and validate their legacy foreign references.
- LegacyLinkType enum (JurisdictionJCode, KitId, ContactId)
- LegacyLinkReference value record with typed factory methods
- ILegacyLinkValidator / LegacyLinkValidator — validates each link type
against the anti-corruption layer; returns descriptive errors on failure
- ILegacyLinkedRecord / ILegacyLinkedRecordProvider — contracts extension
record types implement to participate in integrity checks
- ILegacyLinkIntegrityCheck / LegacyLinkIntegrityService — scans all
registered providers, computes consistency percentage (NFR13 target 99.9%)
- POST /api/admin/legacy-link/integrity-check (Admin-only, audit-logged)
- 19 unit tests covering all four ACs
Stories 1.10-1.13 register ILegacyLinkedRecordProvider implementations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>