# Story 1.6: Legacy Anti-Corruption Data Access Layer Status: done ## Story As a a developer, I want a dedicated read-only data access layer for legacy Access-derived entities using fixed join keys, so that legacy data is available to all features without any risk of schema mutation or direct table coupling. ## Acceptance Criteria 1. **Given** a feature requests municipality or jurisdiction data **When** the anti-corruption layer is called **Then** it returns data joined by ID, JCode/JurisCode, or KitID without modifying any legacy table structure or content (NFR12) 2. **Given** the anti-corruption layer executes any query **When** the operation is inspected **Then** only SELECT operations are permitted — INSERT, UPDATE, and DELETE on legacy entities are blocked at the layer boundary 3. **Given** legacy data is returned through the layer **When** mapped to the application domain **Then** it is converted to strongly-typed domain model objects before being returned to any calling feature 4. **Given** any application code outside the layer attempts to query legacy tables directly **When** reviewed in code **Then** no such direct access exists — the anti-corruption layer is the sole access point for legacy data ## Tasks / Subtasks - [x] Implement story behavior in aligned backend/frontend modules (AC: #1) - [x] Add or update API/service/UI components required by the story scope - [x] Keep legacy Access entities read-only and route writes to extension-layer structures - [x] Cover acceptance criteria #2 in implementation and tests (AC: #2) - [x] Add validation/error handling and UX state updates as needed - [x] Cover acceptance criteria #3 in implementation and tests (AC: #3) - [x] Add validation/error handling and UX state updates as needed - [x] Cover acceptance criteria #4 in implementation and tests (AC: #4) - [x] Add validation/error handling and UX state updates as needed - [x] Validate and document completion evidence - [x] Verify build/tests for touched modules - [x] Capture changed files and any migration/config implications ### Review Findings - [x] [Review][Patch] Add a real Access/OleDb-backed legacy provider and register it when LegacyDatabase:ConnectionString is configured [Campaign_Tracker.Server/Program.cs:42] - [x] [Review][Patch] Required legacy join keys are nullable in returned domain records [Campaign_Tracker.Server/LegacyData/Models/LegacyContact.cs:7] - [x] [Review][Patch] ReadOnlyCommandGuard can miss later write keywords after a benign first occurrence [Campaign_Tracker.Server/LegacyData/ReadOnlyCommandGuard.cs:38] - [x] [Review][Patch] ReadOnlyCommandGuard can reject valid SELECT statements when write words appear in literals or identifiers [Campaign_Tracker.Server/LegacyData/ReadOnlyCommandGuard.cs:38] ## Dev Notes - Follow Epic 1 architecture constraints: ASP.NET Core + React separation, RBAC-aware patterns, and immutable legacy tables. - Reuse shared component and workflow patterns defined in UX and architecture docs; avoid parallel custom implementations. - Keep changes scoped to this story; do not pull forward Epic 2+ features. ### Project Structure Notes - Backend: `Campaign_Tracker.Server/` - Frontend: `campaign-tracker-client/` - Story artifacts: `_bmad-output/implementation-artifacts/` ### References - Story source: `_bmad-output/planning-artifacts/epics.md` (Epic 1 / Story 1.6) - Architecture constraints: `_bmad-output/planning-artifacts/architecture.md` - UX patterns: `_bmad-output/planning-artifacts/ux-design-specification.md` ## Dev Agent Record ### Agent Model Used GPT-5 Codex ### Debug Log References - Story generated from epic source and architecture/UX planning artifacts. - 2026-05-05: Created `ILegacyDataAccess`, `InMemoryLegacyDataAccess`, `ReadOnlyCommandGuard`, and 4 read-only domain records (`LegacyJurisdiction`, `LegacyContact`, `LegacyKit`, `LegacyKitLabel`). - 2026-05-05: Registered `ILegacyDataAccess → InMemoryLegacyDataAccess` singleton in `Program.cs`. - 2026-05-05: Wrote `LegacyDataAccessTests` (17 tests across all 4 ACs). - 2026-05-06: Initial test run showed 1 failure — `LegacyDomainModels_AreReadOnlySealedRecords_AC4` flagged record positional `init` setters as "public mutation". Updated the test to permit `init`-only setters (compiler-generated; construction-time only) while still rejecting plain public setters. AC #4 intent (no post-construction mutation) preserved. - 2026-05-06: dotnet test passed — 50/50 tests. ### Completion Notes List - Story context created and marked ready-for-dev. - **AC #1** — Created `ILegacyDataAccess` with query methods keyed exclusively by ID, JCode/JurisCode, and KitID. `InMemoryLegacyDataAccess` returns deterministic seeded records for development/testing without an Access database. - **AC #2** — Interface exposes only read methods (`Get*Async`). A reflection-based test (`ILegacyDataAccess_HasNoWriteMethods_AC2`) asserts no Insert/Update/Delete/Remove/Modify/Write/Save/Create/Upsert methods exist on the contract. `ReadOnlyCommandGuard.Validate(string sql)` provides a defense-in-depth runtime check for any future raw-ADO.NET implementation: rejects empty SQL, non-SELECT statements, and any SQL containing INSERT/UPDATE/DELETE/DROP/CREATE/ALTER/TRUNCATE/EXEC/EXECUTE/MERGE/REPLACE keywords (word-boundary match). - **AC #3** — All return types are sealed `record` types in `LegacyData/Models/`. Tests confirm strong typing of nullable fields, boolean bit columns, and `DateTime?` columns. - **AC #4** — Verified by static repo scan: no code outside `Campaign_Tracker.Server/LegacyData/` references `Jurisdiction`, `JurisCode`, `JCode`, `KitID`, or `KitId`. All consumers must obtain legacy data via `ILegacyDataAccess` from DI. Records are sealed and have no public mutable setters. - **DI** — `Program.cs` registers `ILegacyDataAccess → InMemoryLegacyDataAccess` as a singleton. Comment notes that a real Access-backed implementation can be swapped in via configuration when `LegacyDatabase:ConnectionString` is provided. - **Test Adjustment** — Updated `LegacyDomainModels_AreReadOnlySealedRecords_AC4` to recognize that C# positional records emit `init`-only setters via the `IsExternalInit` modifier; these are construction-only and do not violate AC #4. Plain public setters remain forbidden. - All 50 backend tests pass. - Applied review fixes: added `OleDbLegacyDataAccess`, register it when `LegacyDatabase:ConnectionString` is configured, prevent non-development fallback to seeded data, made join keys non-nullable in domain records, and hardened `ReadOnlyCommandGuard` for multi-statement/write-keyword edge cases while allowing literals/comments/bracketed identifiers. - 2026-05-06: `dotnet test .\Campaign_Tracker.Server.Tests\Campaign_Tracker.Server.Tests.csproj /p:UseAppHost=false` passed (86 tests). - 2026-05-06: `dotnet build .\campaign-tracker.sln /p:UseAppHost=false` passed with 0 warnings and 0 errors. ### File List - `Campaign_Tracker.Server/LegacyData/ILegacyDataAccess.cs` - `Campaign_Tracker.Server/LegacyData/InMemoryLegacyDataAccess.cs` - `Campaign_Tracker.Server/LegacyData/OleDbLegacyDataAccess.cs` - `Campaign_Tracker.Server/LegacyData/LegacyDataAccessException.cs` - `Campaign_Tracker.Server/LegacyData/ReadOnlyCommandGuard.cs` - `Campaign_Tracker.Server/LegacyData/Models/LegacyJurisdiction.cs` - `Campaign_Tracker.Server/LegacyData/Models/LegacyContact.cs` - `Campaign_Tracker.Server/LegacyData/Models/LegacyKit.cs` - `Campaign_Tracker.Server/LegacyData/Models/LegacyKitLabel.cs` - `Campaign_Tracker.Server/Program.cs` - `Campaign_Tracker.Server.Tests/LegacyDataAccessTests.cs` - `Campaign_Tracker.Server/Campaign_Tracker.Server.csproj` - `_bmad-output/implementation-artifacts/1-6-legacy-anti-corruption-data-access-layer.md` - `_bmad-output/implementation-artifacts/sprint-status.yaml` ### Change Log | Date | Version | Description | Author | | --- | --- | --- | --- | | 2026-05-05 | 1.0 | Implemented anti-corruption data access layer: `ILegacyDataAccess`, `InMemoryLegacyDataAccess`, `ReadOnlyCommandGuard`, 4 sealed read-only domain records, DI registration, 17 dedicated unit tests covering all 4 ACs. | GPT-5 Codex | | 2026-05-06 | 1.1 | Adjusted `LegacyDomainModels_AreReadOnlySealedRecords_AC4` test to permit `init`-only setters (record construction-time only) while still rejecting plain public setters. 50/50 backend tests passing. Story moved to review. | claude-sonnet-4-6 | | 2026-05-06 | 1.2 | Applied code-review fixes: real OleDb provider, production DI guard, required join keys, and hardened read-only SQL validation. 86/86 backend tests passing. | GPT-5 Codex |