wulf-pulse/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-02-SUMMARY.md
lorentz 6df084cb98 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).
2026-08-05 20:24:18 -04:00

7.6 KiB

phase plan subsystem tags dependency-graph tech-stack key-files decisions metrics
24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud 02 aws-route53
route53
sync
drift-detection
soft-delete
sync_history
requires provides affects
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)
lib/services/route53-record-key.ts (buildRecordKey, normalizeRecordSet, recordSetsEqual, classifyDrift, toHistoryPayload)
lib/services/route53-sync-service.ts (Route53SyncService, getRoute53SyncService(), buildDriftHistoryRows)
plan 24-05 (CRUD routes import getRoute53SyncService for post-write history/audit patterns)
plan 24-06 (scheduler wiring calls fullSync()/incrementalSync())
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
created modified
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
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.
duration completed
~45 minutes, 3 tasks, 4 files 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.tsRoute53SyncService 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 .gitignored 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