From 6df084cb98c5d5be610f1c46de11a3d604b126f0 Mon Sep 17 00:00:00 2001 From: lorentz Date: Wed, 5 Aug 2026 20:24:18 -0400 Subject: [PATCH] docs(24-02): create plan summary Route 53 sync + drift detection plan complete: 3 tasks, 4 files, all verification passing (22 new tests, tsc clean, full suite green aside from pre-existing unrelated itglue-search failures). --- .../24-02-SUMMARY.md | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 .planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-02-SUMMARY.md diff --git a/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-02-SUMMARY.md b/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-02-SUMMARY.md new file mode 100644 index 0000000..92ab85c --- /dev/null +++ b/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-02-SUMMARY.md @@ -0,0 +1,92 @@ +--- +phase: 24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud +plan: 02 +subsystem: aws-route53 +tags: [route53, sync, drift-detection, soft-delete, sync_history] +dependency-graph: + requires: + - "lib/services/route53-factory.ts (getRoute53Client / isRoute53Configured)" + - "lib/types/route53.ts (Route53Record, Route53SyncResult, Route53HistorySource)" + - "migrations/102_route53_tables.sql (route53_zones / route53_records / route53_record_history)" + provides: + - "lib/services/route53-record-key.ts (buildRecordKey, normalizeRecordSet, recordSetsEqual, classifyDrift, toHistoryPayload)" + - "lib/services/route53-sync-service.ts (Route53SyncService, getRoute53SyncService(), buildDriftHistoryRows)" + affects: + - "plan 24-05 (CRUD routes import getRoute53SyncService for post-write history/audit patterns)" + - "plan 24-06 (scheduler wiring calls fullSync()/incrementalSync())" +tech-stack: + added: [] + patterns: + - "Sync-service class shape mirrors lib/services/veeam-sync-service.ts: constructor(client?), isSyncing guard, executeSync() step loop with per-step error isolation, sync_history bookkeeping" + - "Pure drift-classification logic extracted into an injectable, unit-testable function (buildDriftHistoryRows) rather than inlined in the DB-touching sync method" +key-files: + created: + - lib/services/route53-record-key.ts + - lib/services/route53-record-key.test.ts + - lib/services/route53-sync-service.ts + - lib/services/route53-sync-service.test.ts + modified: [] +decisions: + - "Combined Task 2 (sync mirror) and Task 3 (drift detection) into a single write of route53-sync-service.ts rather than writing sync-only first and retrofitting drift after — the plan's own Task 2 action already anticipated buildDriftHistoryRows()'s existence (referencing it in Task 3's read_first), so building both together avoided a throwaway intermediate version of syncRecords()." + - "Symlinked node_modules from the main repo checkout into this worktree (git-ignored, not committed) — the worktree had no node_modules of its own and @aws-sdk/client-route-53 types were required for both writing and type-checking this plan's code." +metrics: + duration: "~45 minutes, 3 tasks, 4 files" + completed: "2026-08-05" +--- + +# Phase 24 Plan 2: Route 53 Sync + Drift Detection Summary + +Built the Route 53 → Postgres mirror sync (`Route53SyncService`) with paginated +zone/record ingestion, soft-delete reconciliation, and whole-recordset drift +detection that writes `sync_detected_drift` history rows whenever a mirrored +record's live AWS value no longer matches Postgres. + +## What Was Built + +**`lib/services/route53-record-key.ts`** — pure, dependency-free helpers: +- `buildRecordKey({ zoneId, name, type, setIdentifier })` → `${zoneId}:${name}:${type}:${setIdentifier ?? ''}`, the `route53_records.record_key` primary key +- `normalizeRecordSet(rs, zoneId)` — lowercases name (preserving trailing dot), uppercases type, coerces missing TTL to `null`, sorts `resourceRecords` by value (so AWS's unordered list never produces phantom drift), passes through `aliasTarget` +- `recordSetsEqual(a, b)` — TTL + serialized resourceRecords + serialized aliasTarget comparison; both-null is equal, one-null is not +- `classifyDrift(prev, next)` → `'create' | 'update' | 'delete' | null`, returning `null` for no material difference (no-op history row avoided) +- `toHistoryPayload(ns)` — whole-recordset JSONB snapshot for `before_value`/`after_value` + +**`lib/services/route53-sync-service.ts`** — `Route53SyncService` class: +- `fullSync()` / `incrementalSync()` both delegate to `executeSync()`, which runs the same two steps in both cases (Route 53 exposes no modification cursor, so "incremental" is the same full diff run more often — documented in a code comment per D-11) +- `syncZones()` — paginates `ListHostedZonesCommand` (Marker/IsTruncated/NextMarker), strips the `/hostedzone/` prefix from each zone id, fetches `GetHostedZoneCommand` per zone for `DelegationSet.NameServers` (backs plan 24-04's D-12 NS-delegation check), upserts into `route53_zones`, then soft-deletes zones no longer returned by AWS +- `syncRecords()` — for each live zone, paginates `ListResourceRecordSetsCommand` (StartRecordName/Type/Identifier), normalizes each recordset, loads the zone's current mirror rows, computes drift via `buildDriftHistoryRows()` **before** upserting (so the pre-sync state is still available), inserts drift history rows in a transaction, then upserts records and soft-deletes anything no longer present +- `buildDriftHistoryRows(prevByKey, nextByKey, zoneId)` — exported pure function; unions both keyed maps, classifies drift per key via `classifyDrift`, skips `null` (no-change) keys, builds `DriftHistoryRow[]` with whole-recordset before/after payloads +- Initial-import guard: if a zone's mirror had zero rows before this sync, drift history generation is skipped entirely for that zone (logged once) — prevents flooding the append-only ledger with meaningless `create` rows on first import +- `sync_history` bookkeeping: `entity_type='route53'`, `sync_type` written as the literal `'full'`/`'incremental'` (matching the table's CHECK constraint), catastrophic-failure catch marks the row `status='failed'` +- All error logging uses `error instanceof Error ? error.message : String(error)` — never the raw AWS SDK error object (T-24-03: avoids leaking `$metadata`/request headers into logs) +- No `integration_settings` check anywhere in the file (D-10: Route 53's disable toggle is display-only, must never gate sync) +- `getRoute53SyncService()` module-level singleton, following `pax8-sync-service.ts`'s lazy-instance pattern, exported for plans 24-05/24-06 + +## Deviations from Plan + +None — plan executed as written. Tasks 2 and 3 were combined into a single write of `route53-sync-service.ts` (see Decisions above) since the file only exists once either way; both tasks' acceptance criteria are independently verified below. + +## Verification + +- `npx vitest run lib/services/route53-record-key.test.ts` — 16 tests passed +- `npx vitest run lib/services/route53-sync-service.test.ts` — 6 tests passed +- `npx tsc --noEmit --pretty` — exits 0 +- `npm test` (full suite) — 489 passed, 2 pre-existing failures in `lib/services/analyzer/itglue-search.test.ts`, unrelated to this plan's files and already logged in `deferred-items.md` from plan 24-01 (neither `itglue-search.ts` nor its test were touched by this plan) +- Acceptance-criteria greps all confirmed: `integration_settings` count 0, `entity_type` present, both pagination loops present (`IsTruncated` count 2), both soft-delete statements present (`is_deleted = true` count 2), `sync_detected_drift` literal present, `changed_by_user_id` NULL for drift rows, initial-import guard present (`prevByKey.size === 0`), `buildDriftHistoryRows` call precedes the `INSERT INTO route53_records` statement in source order + +## Environment Note (not a code deviation) + +This worktree had no `node_modules` directory. Symlinked it from the main +repo checkout (`ln -s /opt/stacks/pulse/node_modules ./node_modules`) so +`@aws-sdk/client-route-53` types and `vitest`/`tsc` were available. This is a +local filesystem convenience, not a git-tracked change — `node_modules` is +`.gitignore`d and no commit references it. + +## Self-Check: PASSED + +- `lib/services/route53-record-key.ts` — FOUND +- `lib/services/route53-record-key.test.ts` — FOUND +- `lib/services/route53-sync-service.ts` — FOUND +- `lib/services/route53-sync-service.test.ts` — FOUND +- Commit `c18271d` (Task 1) — FOUND in `git log --oneline --all` +- Commit `d8c0912` (Task 2) — FOUND in `git log --oneline --all` +- Commit `0acf1fa` (Task 3) — FOUND in `git log --oneline --all`