Skip to main content

RFC Review: Legacy Migration — CRM Contact Activity Logs → CDP (audits → activity_logs)

Companion review for rfc-legacy-migration-crm-activity-logs.md, produced by the rfc-reviewer skill. Lives beside the RFC; valid only for the RFC revision in reviewed_rfc_last_updated (2026-07-15 working tree, post the AuditEventBuilder mapper re-grounding). This is review cycle R5. R1 judged 82c8f91 (6.5, HOLD); R2 judged c26fce4 (8.0, PROCEED w/ notes); R3 judged 2503887 (8.0, PROCEED w/ notes); R4 judged the 2026-07-13 post-R3-fix working tree (8.0, PROCEED w/ notes). R5 re-reviews after the 2026-07-15 mapper re-grounding (§2.F CategoryMapper re-derived from CRM's live-feed emitter AuditEventBuilder: company link/unlink restored via Crm::Person/Crm::Company, ticket-resolved corrected to stage win_probability ≥ 1.0, scope list widened to eight auditable_types, ContactResolver extended to multi-contact fan-out, OQ-16 added). Net of R5: OQ-12 is now fully closed from source (a real improvement), and R5 surfaces two new correctness/operational findings the earlier cycles missed (NEW-1 rollback ≠ enrichment-reversal; NEW-2 accuracy_pct denominator vs. the new skip classes) plus the new OQ-16 mapper-scope call.

Executive Summary

  • Overall Score: 8.0/10
  • Rating: Strong
  • RFC Type: full-stack
  • Sub-Type: backend: new-feature · frontend: enhancement
  • Assessment Confidence: High
  • Applied Caps/Gates: None triggered (no category < 5.0; ACV/DIC/FMC ≥ 8.5; deploy order specified; no cross-layer contract mismatch; end-to-end flow documented). The 9.0+ tier is disqualified by rubric rule (CNT at 7.5 is below the 8.5 floor that tier requires), which — with the doc-integrity defects and the two R5 correctness/operational findings below — keeps the score at the top of the Strong band despite most categories at 8.5–9.0.
  • Implementation Readiness Verdict: PROCEED with notes — build all 9 chunks now; close operational Stage-0 gates before running, fix two doc-integrity defects before advancing in-review, and reconcile the two R5 findings (NEW-1 rollback scope, NEW-2 accuracy_pct denominator) — both are cheap spec edits, neither blocks writing/unit-testing code.
  • Report Path: cdp/legacy-migration-crm-activity-logs/rfcs/rfc-legacy-migration-crm-activity-logs-review.md
  • RFC Author: Julio J Maliangkay (frontmatter) / Zhelia Alifa (Metadata table — see GAP-1) | Reviewed: 2026-07-15

An AI agent can read this RFC and produce correct implementation code for essentially the entire surface without a clarification meeting. The 2026-07-15 mapper re-grounding is a real improvement: the CategoryMapper is now a documented port of CRM's own live-feed emitter AuditEventBuilder (dispatcher + audit_event_builder/*.rb), so OQ-12 is fully closed from source — company link/unlink restored via Crm::Person/Crm::Company, deal/ticket resolved = stage win_probability ≥ 1.0 fanned out to crm_leads, task completed = crm_task_status_id == 4, with only the new OQ-16 (unified_app backfill filter) left as a defaulted product call. Backend — the bulk of the work — is DDL/handler/endpoint precise, every path:line anchor is grounding-verified, and all 18 decisions are resolved. The biggest strength is the grounding discipline (Detail 2.0 + 13 numbered "the repo wins" corrections). The gaps: the one FE surface (status indicator) is still design-pending (OQ-10, CNT 7.5), and R5 surfaces two previously-missed correctness/operational holes — NEW-1: the rollback path (DELETE WHERE source_tag='crm_migration') does not reverse AssociationLogEnricher's in-place patches to live-feed rows (which carry no source_tag), so the "fully reversible / blast radius = one account's migrated logs" claim is overstated; NEW-2: the accuracy_pct denominator (total_in_scope_source_records) was defined before the watermark (D16) and unified_app (OQ-16) skips existed, so unless it nets those out, ValidationRunner will read far below 99% and spuriously trip the <90% PagerDuty/stop-condition. The one thing to change before in-review is still trivial but real: the Metadata table contradicts the frontmatter on DRI and Author.


Quick Verdict

Why this RFC can be implemented agentically:

  • Backend is DDL-precise: struct diffs with bson/json tags (§2.3), three golang-migrate index migrations, 6 endpoints with full request/response/status/idempotency/auth (§2.4), and a 9-chunk Agent Execution Plan (§4.D) with repo-sourced commands + per-chunk acceptance criteria.
  • Every code claim is verified — Detail 2.0 names the two rows it could not verify (CRM DSN, query plan) and marks them blockers instead of papering over them.
  • All 18 decisions resolved with genuine, code-cited alternative rejection (D1 vs external_activity_log_request.go:85-90,122-127; D16 vs the un-audited update_column at send.rb:125).

Why this RFC will cause agent guessing or rework:

  • FE status indicator is design-pending (OQ-10): an agent builds against a pixel3 interim banner and guesses layout/copy/placement until a frame lands.
  • OQ-12 mapper unknownsresolved from qontak.com source (2026-07-15): the CategoryMapper is a port of CRM's live-feed emitter AuditEventBuilder. Link/unlink from Crm::PeopleDeal (deal), PeopleTicket (ticket), Crm::Task, and Crm::Person/Crm::Company (company link/unlink — restored, not Crm::CompaniesDeal, which stays out of scope). Deal/ticket resolved = stage win_probability ≥ 1.0 (fan out to crm_leads; ticket via the omnichannel Ticket/ticket_stage, not crm_ticket_status); task completed = crm_task_status_id == 4. No product call; residual OQ-16 = whether the backfill applies the live feed's unified_app user filter.
  • Doc-integrity defects (DRI/author frontmatter↔table mismatch; Assumptions mis-numbered 1,6,2,3,4,5) are small but sit in a document whose credibility is its precision.

PRD → RFC Traceability Matrix

Standard format (PRD v2.5 exists)

PRD ElementRFC SectionCoverage
CALM-S01 (run batch migration)§2.4 trigger + §2.F consumer + §4.D chunks 2,5,6Full
CALM-S02 (idempotent re-run)§2.3 partial unique index + §2.4 /migrate + §2.EFull
CALM-S03 (transform audit→log)§2.F transformer (Category/Changes/Actor/Contact)Full
CALM-S04 (migrated logs visible, no badge)existing GET /v1/activity_logs, no FE changeFull
CALM-S05 (status indicator)§2.A + §2.4 IAG proxy + §4.D chunk 7Full — design-pending (OQ-10)
CALM-S06-NEG (no dup on re-run)§2.3 index + §2.4 flag-gateFull
CALM-S07 (pre-sync watermark)§2.F SyncWatermarkResolver + D16 + chunk 8Full (edge OQ-15 flagged)
CALM-S08 (association enrichment)§2.F AssociationLogEnricher + D17 + chunk 9Full (P99 re-pin OQ-13)
CALM-S09 (Phase-A coverage gate)§2.4 trigger gate 422 + D18 + chunk 5Full
PRD §11 metric: zero double-import§1 Success Criteria + CALM-S07 ACsFull in RFC; PRD §11 backfill = REV-16, a PM edit
PRD §11 metric: enrichment ≥99%§1 Success Criteria + CALM-S08 ACsFull in RFC; same REV-16 caveat
Every PRD § (1–15 + App A)Detail 1.A "PRD Section Coverage" tableFull — 1:1 section map present

Summary: 9 of 9 stories fully covered; 0 partial; 0 missing. 0 RFC decisions lack a PRD driver (CALM-S07/S08/S09 backfilled into PRD v2.5). The only residual is REV-16 — two RFC-born success metrics not yet mirrored into PRD §11 — correctly assigned to the PM.


Scorecard

Full-Stack Scorecard (19 categories)

#CategorySourceScoreEvidence-Based Rationale
1PRT — PRD TraceabilityMerged9.0FE: Detail 1.A maps CALM-S05 → §2.A/§2.4. BE: every §2.3 field + §2.4 endpoint traces to a PRD-to-Schema row; full §-coverage table. No cross-layer contradiction.
2TDC — Technical DecisionsMerged9.018 decisions + 14 full ADR blocks. FE D14/D15 present; cross-layer conflict resolved (D15 kills the browser-S2S mismatch).
3CNT — Contract SpecificityFE7.5MigrationStatusProps (propless post-REV-13), 4 states, useCrmMigrationStatus, poll interval, cache key specified (§2.A/§2.B). Held down: design-pending (OQ-10) — visual contract interim-only.
4SCB — Scope BoundariesFE9.0Detail 2.I exemplary: explicit FE create / modify / NOT touched (5,000 cap, pagination, TS interface named out-of-scope).
5DEP — DependenciesFE8.5Owner/status/blocking table; FE chunk 7 gated on BE IAG proxy (chunk 5). USMAN downgraded with justification.
6NFS — Non-FunctionalFE8.0Perf (zero render cost; 1 poll/15s), a11y (§3.E role="status", keyboard, contrast), browser support (existing app). No budget needed for a trivial surface.
7TPS — Test PlanFE8.0§4.C repo-sourced commands; FE unit (vitest, 4 states) + lint + build. Honestly flags no typecheck/e2e script; integration test called out to-add.
8DMS — Data Model & SchemaBE9.0§2.3: struct diff w/ tags, const, 3 index migrations (partial-unique + one-active-job partial-unique), erDiagram, example doc, cardinality, per-status lifecycle.
9ACV — API Contract & VersioningBE9.0§2.4: 6 endpoints full-columned + examples; §2.G confirms FE reads what BE sends (snake_case, no transform).
10DIC — Data Integrity & ConsistencyBE8.5§2.D matrix (txn/idempotency/consistency/dup/stale-read); watermark closes double-import seam (D16); enricher additive-idempotent (D17). Down from 9.0 (R5): NEW-1 — rollback can't reverse enricher patches on live-feed rows (no source_tag), contradicting the §4 reversibility claim; NEW-2 — accuracy_pct denominator not reconciled to the new skip classes. Both are spec/wording holes, not code defects, but they sit in the integrity/validation story.
11FMC — Failure Mode CoverageMerged8.5BE: §3.A retry catalog + §3.A.1 branch/skip + failure sequence diagrams. FE: fail-silent + poll_failed breadcrumb (REV-14). Cross: FE matches BE not_started/404 shape.
12CSS — Concurrency & ScalingBE8.5§2.E collision map incl. REV-17 live-feed-vs-enricher row; ≥500 rec/s, batch ≤20, PK cursor, one-active-job invariant, USMAN cache.
13SAS — Security & AuthorizationBE8.5Threat model + mitigations (parameterized read-only DSN, per-tenant scoping, Vault); Role×Endpoint matrix; cross-tenant → 403.
14ROL — Rollout & RollbackMerged9.0Deploy order = backend-first (justified); single flag OFF; 4 stages + stop conditions; §4.A 6-scenario matrix (all pass); §4.G Stage-0 go/no-go; rollback recipe.
15OBS — ObservabilityMerged8.5BE: StatsD naming, events, alert thresholds, PII-scrubbed logs, traces. FE: banner_shown/banner_hidden{reason}/poll_failed (REV-14). Correlation addressed.
16RCS — Resource & CostBE8.0§4.F: compute (existing pods, one job/account), CRM read load (OQ-2-gated), Mongo write ≤500 rec/s, storage ≈ in-scope audits (one-time).
17SBC — Service Boundary & CouplingBE8.5§2.F.1 responsibility matrix; D18 keeps contact-service read-only on CRM; CRM-schema coupling flagged (D11 trade-off).
18CPA — Pattern AlignmentMerged9.0Patterns-to-Follow per layer; the one deviation (Redis→Mongo store) flagged + justified (D8). FE Pinia/ofetch, snake_case-direct matched.
19CDG — Compliance & Data GovernanceBE8.5Triggered (PII). §3.D: classification, UU PDP/GDPR basis, retention, encryption, right-to-delete, cross-border (none — same AliCloud region), append-only point + enrichment exception.

Category aggregate: 18 of 19 ≥ 8.0; lowest CNT 7.5 (DIC trimmed to 8.5 for NEW-1/NEW-2). No cap triggered. The 9.0+ gate requires CNT ≥ 8.5 → disqualified; with the doc-integrity defects and the two R5 findings, the honest landing holds at 8.0 — Strong. (The 2026-07-15 mapper re-grounding closed OQ-12 from source — a genuine plus — but OQ-16 and the two new findings offset it, so R5 does not move the number off R4's 8.0.)


Decision Closure Assessment

Decision Index

#DecisionStatusResidual (gate/edge, not a design hole)
1New S2S endpoints (underscore ns)Resolved
2external_id + partial unique indexResolved
3source_tag new constResolvedSole alternative (RFC admits it) — acceptable
4 & 10Parse audited_changes YAML, 2 branchesResolvedcolumn→label map = OQ-6 (local default)
5Re-implement Audit mapping in GoResolvedRails-sync burden (known limitation #2)
6Actor resolution — 4 cases, two-hopResolveddeactivated-user rep = OQ-9 (safe default)
7Cursor pagination on audits.idResolvedquery plan = OQ-2 (Stage-0 gate)
8Durable Mongo job storeResolved
9Contact resolution via crm_data.idResolved<99% fallback designed (REV-5)
11Direct read-only Postgres extractionResolvedaccess = OQ-1 (Stage-0 gate)
12Underscore namespaceResolved
13Append-only for migrated logsResolvedamended by D17 exception
14FE ships after BE, decoupledResolved
15FE reads status via IAG proxyResolvedcloses former REV-1/OQ-11 cap
16Watermark = CDP contact.created_atResolvedcross-channel under-cut = OQ-15 (correctness risk)
17Enricher patches in placeResolved±60 s re-pin to P99 = OQ-13 (measure-confirm)
18Unsynced contacts — Phase-A gate onlyResolved

Aggregate: 18 of 18 Resolved, 0 Partial, 0 Dangling. Every residual is an operational confirmation or a flagged edge, not an unmade decision. The challenge protocol below stress-tests the three carrying residual correctness risk.


Decision: 16 — Pre-sync cutoff via CDP contact.created_at watermark

Status: Resolved (flagged correctness edge)

What was decided: Per-contact upper bound = CDP contact.created_at; migrate audits.created_at < watermark.

Alternatives: (A) audit that set qontak_customer_id — rejected, doesn't exist (update_column bypasses audited, grounding #12); (B) chosen; (C) account-level cutoff — rejected, too coarse. Genuinely evaluated against code.

Grounding / Interface / Failure: send.rb:125; SyncWatermarkResolver reads already-fetched created_at; skip post_sync_live_feed; missing contact → contact_not_found. All specified.

Challenge results:

  • Scale: Holds — cached per job, no extra round-trip.
  • Reversibility: Query-only.
  • Consistency: Aligned with D18 (both lean on the sync moment).
  • Correctness (the real risk): OQ-15 — a chat-created, later-CRM-linked contact has created_at earlier than its true sync moment, so its watermark under-cuts and a slice of genuine pre-sync history is silently dropped (data loss, not display). Flagged, scoped to cross-channel contacts, deferred to "before Stage 2." Acceptable for building; the single most important thing to validate empirically (Gate G) before wide rollout.
  • Metric consistency (NEW-2 — R5, un-flagged): D16 (and, if adopted, OQ-16's unified_app filter) make a legitimately large fraction of in-scope source audits not get migrated — every post_sync_live_feed skip and every non-unified_app audit. But the headline success metric is still defined as accuracy_pct = records_correctly_migrated / total_in_scope_source_records (§1 Success Criteria, PRD §11) and ValidationRunner computes it as countDocuments({source_tag,company_sso_id}) vs source COUNT (§2.F). If that source COUNT is the raw eight-type scope count (the same predicate as Gate B1 rows_in_scope, which has no watermark or unified_app predicate), the denominator includes rows the design intends to skip, so accuracy_pct reads well under 99% on any account with meaningful post-sync history — directly tripping the accuracy_pct < 90% → PagerDuty + PM alert (§3) and the accuracy_pct < 90% stop condition (§4). The watermark seam got a separate success criterion ("zero double-import") but the accuracy denominator was never reconciled to net out the new skip classes. This is a metric-definition bug that will manifest as spurious production halts, not a code bug.

Suggestion (D16 / OQ-15): Have Gate G quantify the cross-channel population per pilot account so "small gap" is measured, not assumed.

Suggestion (NEW-2): Redefine the accuracy_pct denominator (in §1, §2.F ValidationRunner, and PRD §11) as records_migrated / (in_scope_source_records − post_sync_live_feed_skips − unmapped − contact_not_found[intentional] − unified_app_filtered) — i.e. the count of records the design actually intends to write — or state the denominator predicate explicitly so ValidationRunner's source COUNT uses the same filters as the extractor/transform, not Gate B1's raw scope count. This must be settled alongside OQ-16, since that decision changes the denominator.


Decision: 17 — AssociationLogEnricher in-place patch (incl. live-feed)

Status: Resolved (heuristic match-key, empirically re-pinned — REV-15)

What was decided: Additive $set-on-missing on association logs; live-feed rows matched on (company_sso_id, customer_id, category/action, linked entity id, timestamp≈±60 s); >1 candidate → enrich_ambiguous skip.

Alternatives: enrich-only-migrated (rejected: misses live-feed gap); reconstruct from join tables (rejected: scope creep). Both rejected with reason.

Challenge results:

  • Scale: ±60 s window is the risk at 10x. Rapid same-category/same-entity churn inside 60 s → correctly skipped as ambiguous (fails safe), but enrichment completeness (≥99% metric) could dip on churny accounts. Measured at Gate G.
  • Consistency: REV-17 shows the live-feed-vs-enricher race is benign under $set-on-missing (worst case: stale-but-present value, low blast radius, not corruption).
  • Reversibility (NEW-1 — R5, un-flagged in the RFC): This is the real hole. The enricher patches live-feed rows in place, and those rows carry no source_tag (that is exactly why they need matching by heuristic key — Decision 17 / grounding #11). But the rollback path is DELETE WHERE source_tag='crm_migration' AND company_sso_id (§4, §2.D, §3.D). So a rollback removes the migration's own rows but leaves every additive patch the enricher wrote onto pre-existing live-feed documents. Decision 17's "Reversibility" note only covers re-run ("a re-run recomputes them"), not rollback; §4 "Blast radius: worst case = one account's migrated logs (removable by rollback)" and §4.E step 3 (countDocuments({source_tag}) == 0 as the rollback-complete check) both silently assume the enricher wrote nothing outside source_tag='crm_migration' — which contradicts Decision 17. An agent implementing §4.E literally will report a clean rollback while live-feed docs stay mutated.
  • Agent implementability: Buildable now against the ±60 s default; OQ-13 is a Stage-1 re-pin, not a design hole.

Suggestion (NEW-1): State the intended rollback semantics explicitly — either (a) declare the enrichment patches deliberately not reversed (they are additive, CRM-source-of-truth metadata, arguably harmless) and correct the §4 blast-radius / §4.E "reversible" wording to say so, or (b) tag enriched-in-place live-feed rows (e.g. an enriched_by:"crm_migration" marker on the patched sub-fields) so rollback can null them back. (a) is the lighter fix and probably right, but the RFC must pick one — today it claims full reversibility while the code path can't deliver it.

Suggestion (OQ-13): At Gate G confirm the P99-re-pinned window doesn't push a material share of churny logs into enrich_ambiguous (which would quietly erode the ≥99% metric).


Decision: 11 / 7 — Direct read-only Postgres extraction + cursor pagination

Status: Resolved (execution gated on OQ-1/OQ-2)

Challenge results:

  • Scale (the real risk): ~15M rows, no (auditable_type, created_at) index (verified schema.rb:68-77) — the type/date predicate filters after the PK cursor scan, so early pages can scan mostly-out-of-scope id ranges. §4.G Gate B makes this a hard go/no-go (EXPLAIN ANALYZE, p95 < 500 ms, no Seq Scan) with a concrete CREATE INDEX CONCURRENTLY remedy owned by CRM. Honest about the unverified plan.
  • Reversibility: Swap extractor behind the CRMExtractor interface — clean.
  • Agent implementability: Build now; run is gated. Correct separation.

Suggestion: None blocking — this is the model the rest of the OQs follow.


Cross-Layer Contract Verification

EndpointBackend Response SchemaFrontend Expected SchemaMatch?Gaps
GET /v1/activity_logsexisting fields + source_tag,metadata (snake_case, omitempty)existing 10 fields; ignores new fieldsYesAdditive, no badge (PRD v2.3).
GET /v1/crm_migration/status (IAG proxy){status, progress_pct, records_*, started_at, duration_seconds} snake_case, tenant from sessionuseCrmMigrationStatus reads same keys; sends no company_sso_idYesResolved via D15/REV-1 — tenant server-derived, cross-tenant → 403.
GET /private/.../status (ops)same + accuracy_pctn/a — S2S ops onlyYes (n/a)FE never calls it.

Checks: casing (snake_case consumed directly, CustomerStore.ts:206-211), nullability (omitempty, FE tolerant), error shape (BaseResponse → FE fail-silent), pagination (existing after_id), auth (IAG session vs S2S BasicAuth cleanly separated).

Mismatches found: 0 — no ROL cap triggered.


Cross-Layer Rollout Compatibility Matrix

ScenarioFEBEWorks?Notes
Pre-deployOldOldYesBaseline
Backend firstOldNewYesLogs render as normal records; indicator absent
Frontend firstNewOldYesIndicator hidden (404/not_started); logs render natively once BE ships
Both deployedNewNewYesTarget
Backend rollbackNewOldYesStatus API gone → indicator fail-silent; logs removed by rollback
Frontend rollbackOldNewYesLogs migrated + render natively; no indicator

Deploy order: Backend first (specified + justified — §4, D14). Incompatible scenarios: 0. The no-badge design (PRD v2.3) is what makes every scenario green.


End-to-End Data Flow

Engineer → POST /private/activity_logs/crm_migration {company_sso_id} (S2S BasicAuth)
→ Trigger: flag check + Phase-A coverage gate (422) + one-active-job (409) + insert job + enqueue gocraft/work
→ Consumer: CRMExtractor cursor SELECT (CRM Postgres, read-only)
→ per record: ContactResolver (comment-JSON crm_person_id → YAML → FindOneByCrmDataID)
→ SyncWatermarkResolver (skip ≥ contact.created_at → post_sync_live_feed)
→ ActorResolver (CRM users hop → USMAN GetUsersByEmails, cached) → ChangesExtractor (YAML 2-branch) → CategoryMapper
→ MigrateBatch upsert ≤20 (skip-on-conflict via partial unique index) → activity_logs
→ AssociationLogEnricher (additive in-place, incl. live-feed rows) → activity_logs
→ update job doc (progress_pct, counts, last_audit_id); halt if failure_rate > 10%
→ (later) Agent opens Contact Detail → GET /v1/activity_logs → migrated logs render natively (no badge)
→ DetailPage.vue indicator → GET /v1/crm_migration/status (IAG proxy) → banner while in_progress

Gaps in flow: None — traceable from §2.1/§2.2/§2.H in one place; side effects (observability events, accuracy_pct) shown. The only free choice left to an agent is the FE banner's visual layout (OQ-10).


Data Integrity Deep-Dive

Write PathTxn ScopePartial FailureIdempotency KeyConsistencyDuplicate Handling
MigrateBatch upsert ≤20per-record upsertretry 1× → end-of-run queue → halt >10%(company_sso_id, external_id) (no TTL)eventualskip-on-conflict → duplicate_external_id
job-state updatesingle-docconsumer-loop retryjob_idstrongn/a
AssociationLogEnrichersingle-doc $set-on-missingidempotent re-runmatch-key (±60 s)eventualambiguous >1 → enrich_ambiguous skip
rollback delete-by-tagWHERE source_tag AND company_sso_idretry → rollback_failed alertidempotent (re-delete = 0)eventualn/a — but see NEW-1

Mostly complete. Two R5 caveats: (1) the enricher's live-feed match (no external_id) is non-DB-guaranteed — handled by fail-safe skip, not a guess (fine); (2) NEW-1 — the rollback row above only touches source_tag='crm_migration' docs, so it is not the inverse of the AssociationLogEnricher row for live-feed targets: those patched docs have no source_tag and survive rollback mutated. The write-path table and the §4/§4.E rollback claims are inconsistent on this until NEW-1 is pinned.


Async Job / Event Consumer Spec

JobTriggerInputRetryDLQConcurrencyIdempotencyTimeoutAssessment
CrmActivityLogMigrationConsumertrigger enqueue{job_id, company_sso_id, date_range_start?}job: resume last_audit_id; batch: 1× then end-of-rungocraft dead set + failed-record queue 30 d1/accountjob_id + external_idUSMAN 3 s; Mongo driver default8/8 specified

Textbook — every column grounded (worker_service.go:144-156, activity_log_migration_service.go).


Compliance Trigger Check

TriggerFound?LocationClassificationAssessment
PII (name/email/phone)Yesactor, user_id, changes[].from/toUU PDP / GDPRHandled — §3.D, PII-scrubbed logs
Payment / HealthNon/a
User content w/ retentionYesactivity_logs audit trailper existing policyHandled
Auth/sessionNo (S2S only)Vault secrets
Cross-borderNosame AliCloud regionUU PDPHandled — in-jurisdiction

CDG Status: Active — scored 8.5.


Agentic Readiness Deep-Dive

Vague Word Audit

#Word/PhraseLocationImpactReplacement
1"some records may still be loading"§2.A banner copyUX copy, not specFine as interim; finalize w/ OQ-10
2"worker pool default"§2.Finherits existing gocraft poolAcceptable — grounded

Total in spec sections: ~0. The RFC replaced its own prior vagueness (REV-10 failure_rate, REV-11 :cutoff, REV-15 match-key) with pinned formulas.

Dangling Alternatives

Total: 0 — every "or" in the decision blocks resolves to a chosen option.

Task Decomposition

9 chunks (§4.D), each with a "done when" tied to make test/make build/pnpm test + concrete assertions; dependency-aware (7→5, 8→3, 9→4,8). Verifiable — the strongest artifact in the RFC.


Strengths

  • Grounding discipline (Detail 2.0 + 13 corrections). Every path:line verified; two unverifiable rows marked blockers, not filled. The "repo wins" PRD deltas stop an agent coding against a hallucinated schema.
  • Decision closure (Detail 1.B + 14 ADR blocks). 18/18 resolved with code-cited alternative rejection; D16's rejection of the audit-based watermark against the un-audited update_column only survives real code-reading.
  • Rollout correctness (§4.A + §4.G). No-badge design makes all six deploy/rollback scenarios green; the Stage-0 checklist turns every cross-team unknown into an owned, threshold-gated go/no-go.

Biggest Gaps

  • NEW-2 — accuracy_pct denominator not reconciled to the new skip classes (R5). The watermark (D16) and, if adopted, the OQ-16 unified_app filter intentionally leave a large share of in-scope audits unmigrated, but accuracy_pct = migrated / total_in_scope_source_records (§1, §2.F, PRD §11) never nets those out — so ValidationRunner will read below 99% on normal accounts and spuriously trip the <90% PagerDuty alert and the <90% stop condition (§3, §4). This is the highest-impact R5 finding because it fires in production, not in review. Fix: redefine the denominator as "records the design intends to write" (see NEW-2 suggestion), settled together with OQ-16.
  • NEW-1 — rollback does not reverse enrichment of live-feed rows (R5). AssociationLogEnricher patches live-feed docs in place; they carry no source_tag; rollback deletes only source_tag='crm_migration'. So §4's "fully reversible / blast radius = one account's migrated logs" and §4.E's countDocuments({source_tag})==0 completeness check are overstated — enriched live-feed rows stay mutated after a "clean" rollback. Fix: either declare the additive patches deliberately non-reversed (and correct the §4/§4.E wording) or tag them so rollback can null them (see NEW-1 suggestion).
  • FE surface design-pending (OQ-10, CNT 7.5). No Figma frame; chunk 7 builds against a pixel3 interim, so an agent guesses layout/copy/placement. Fix: attach a frame or bless the interim banner as final in §2.A — this is what lifts CNT to ≥ 8.5.
  • Watermark data-loss edge (OQ-15). Cross-channel contacts can have pre-sync history silently under-cut — correctness, not display — deferred to "before Stage 2." Fix: Gate G quantifies the cross-channel population per pilot account and decides the special-case before Stage 2.
  • Doc-integrity defects undercutting the precision claim. (a) Metadata table DRI/Author = Zhelia Alifa vs frontmatter Julio J Maliangkay — violates the RFC's own "both agree on every shared field" invariant (lines 8/12 vs 140/142). (b) Assumptions mis-numbered 1,6,2,3,4,5 (§1 Assumptions). Fix: reconcile owners; renumber.

Priority Actions

  1. NEW-2 — fix the accuracy_pct denominator (§1 Success Criteria, §2.F ValidationRunner, PRD §11) so it nets out post_sync_live_feed / unmapped / intentional contact_not_found / (if adopted) unified_app-filtered records. Highest priority because, left as-is, it produces spurious <90% PagerDuty alerts and stop-condition halts in production. Settle jointly with OQ-16.
  2. NEW-1 — pin rollback semantics for enriched live-feed rows. Either declare the additive patches deliberately non-reversed and correct §4 "blast radius"/§4.E "reversible" wording, or add a marker so rollback can null them. Today the RFC claims full reversibility the code path can't deliver.
  3. Metadata table + Assumptions numbering — reconcile DRI/Author against frontmatter and renumber Assumptions 1–6. Trivial; closes a self-inconsistency the RFC's conventions forbid; OQ-8 already requires real reviewer/approver names before in-review.
  4. §2.A / OQ-10 FE contract — attach the frame or declare the pixel3 interim banner the shippable final so chunk 7 has a fixed visual contract (lifts CNT ≥ 8.5, unblocking 8.5+).
  5. OQ-16 — decide the unified_app backfill filter. Mapper-scope product call added 2026-07-15; it has a safe default (mirror the live feed) but it changes which records migrate and therefore the NEW-2 denominator — resolve the two together before Stage 2.
  6. Gate G — quantify OQ-15 cross-channel population before Stage 2; decide whether to special-case linkage time. Make the "small gap" a measured number.
  7. REV-16 (PM-owned) — mirror the two RFC-born success metrics into PRD §11 so PRD ⇄ RFC ⇄ test-spec stays consistent.

Implementation Readiness Checklist

Unblocked (agent can proceed)

  • PRD → RFC traceability complete (9/9; full §-map)
  • All decisions resolved with alternatives (18/18)
  • Failure modes handled with error catalog (§3.A/3.B/3.C)
  • Configuration contract documented (§4.B)
  • Pattern alignment verified (deviations flagged)
  • Rollout plan with flag + rollback (§4)
  • Observability metrics + alerts (§3 + REV-14 FE)
  • Task decomposition with per-chunk acceptance criteria (§4.D)
  • Zero vague words in spec sections
  • Schema at DDL precision (§2.3)
  • API contracts with schemas/errors/examples (§2.4)
  • Transaction boundaries + idempotency per write path (§2.D)
  • Concurrency collisions listed with resolution (§2.E)
  • Security: auth, validation, injection, tenancy (§3)
  • Migration plan: additive, backfill, rollback (§4)
  • Compliance handled (§3.D)
  • Cross-layer contract verified (§2.G — 0 mismatches)
  • Deploy order specified (backend first)
  • Cross-layer rollout matrix — no unaddressed "No" (§4.A)
  • End-to-end data flow documented (§2.H)
  • Feature flag coordination defined (single BE flag; FE independent)

Blocked (must fix first)

  • None blocks building. Pre-in-review: reconcile DRI/Author + Assumptions numbering; assign real reviewer/approver names (OQ-8); pin NEW-1 rollback semantics and NEW-2 accuracy_pct denominator (both spec/wording edits — do before the metric/alert and rollback chunks are wired, or the alerting and rollback recipe ship wrong).
  • Before running: Stage-0 Gates A/B/F + D (OQ-1/2/9); per-account Gate C (OQ-7); decide OQ-16 (unified_app backfill filter — couples to NEW-2). Gate E (OQ-12) is resolved from source (mapper ported from AuditEventBuilder).

Verdict: Ready to implement — write + unit-test all 9 chunks now; but reconcile NEW-1/NEW-2 (cheap spec edits) before the rollback recipe (§4.E) and the accuracy alert/stop-condition (§3/§4) are trusted, and close operational gates before execution runs and Stage-1 QA.


Task Manifest

The RFC specifies its own 9-chunk plan (§4.D). Verified — sound, ordered, dependency-aware. No re-proposal needed.

OrderChunkFilesAcceptance CriteriaDeps
1Schema + const + indexesactivity_log/base.go, consts/const.go, db/migrations/0NN_*.{up,down}.json3 indexes created; build compiles; migrate-down cleanNone
2Contact finderrepository/contact/base.goresolves crm_data.id→contact; miss→not-found1
3Transformer pkg…/crm_migration/transformer*.gogolden-file: 2 branches, 4 actor cases, category map, unmapped→skip2
4MigrateBatch + /migrateservice/…, handler, payload, rest_router.go≤20 enforced; conflict→skip; re-run inserts 03
5Trigger + status + IAG proxy + rollback + durable store + Phase-A gatehandler/service/repo + rest_router.goflag OFF→403; coverage<threshold→422; 2nd→409; IAG cross-tenant→4034
6Consumer + worker reg + extractorconsumer/…, worker_service.go, job_enqueuer.goenqueue→consume→cursor→transform→write; resume from cursor5
7FE status indicator + composablenew component + useCrmMigrationStatus, DetailPage.vue4 states; API error→hidden5 + OQ-10
8SyncWatermarkResolver…/crm_migration/watermark*.go<watermark→migrate; →skip post_sync_live_feed3
9AssociationLogEnricher…/crm_migration/association_enricher*.gofill-on-missing; live-feed match; ambiguous→skip; idempotent4, 8

Dangling Decisions Log

#DecisionLocationOwnerDeadline
None. All 18 decisions resolved.

Open Questions

#QuestionCategorySeverity
NEW-2accuracy_pct denominator (total_in_scope_source_records) doesn't net out post_sync_live_feed/unmapped/unified_app skips → reads <99% → spurious <90% PagerDuty + stop-condition haltsDIC / OBS / metric-definitionImportant (production-impacting)
NEW-1Rollback (DELETE WHERE source_tag='crm_migration') does not reverse AssociationLogEnricher's in-place patches to live-feed rows (no source_tag); §4/§4.E overstate reversibilityDIC / ROLImportant (before Stage 2 / rollback drill)
OQ-16Should the historical backfill apply the live feed's unified_app user filter (mirror live feed) or ingest all history? Changes which records migrate + the NEW-2 denominatorACV / metric-scopeImportant (before Stage 2; safe default set)
GAP-1Metadata table DRI/Author (Zhelia Alifa) contradicts frontmatter (Julio J Maliangkay) — which is authoritative?PRT / doc-integrityImportant (pre-in-review)
GAP-2Assumptions list mis-numbered (1,6,2,3,4,5)doc-integrityNice-to-have
OQ-10Figma frame for the status indicator, or bless the pixel3 interim as final?CNTImportant (FE)
OQ-15Cross-channel contacts under-cut the watermark → silent pre-sync data loss; quantify + decide special-caseDICImportant (before Stage 2)
OQ-1/2CRM read-only DSN + extraction query planRCS / CSSBlocking (Stage-0 run)
OQ-7Per-account crm_data.id coverage ≥ 99%DEPBlocking (per-account run)
OQ-9USMAN deactivated-user representationSAS / FMCImportant (safe default set)
OQ-12ResolvedCategoryMapper ported from live-feed AuditEventBuilder (qontak.com); company link via Crm::Person/Crm::Company, resolved via stage win_probabilityACVResolved (no product call; residual OQ-16 = unified_app backfill filter)
OQ-13Re-pin ±60 s enrichment window to observed P99DICNice-to-have (measure-confirm)
REV-16Mirror two RFC-born metrics into PRD §11PRTImportant (PM-owned)

Evidence Notes

  • Detail 2.0 Source Verification (lines 973–1008) — spine of the High confidence; two rows NOT VERIFIED and marked blockers, raising PRT/CPA/DMS.
  • Detail 1.B + Decisions 16/17/18 (lines 442–867) — drove TDC 9.0; code-cited rejection of the audit-based watermark shows real closure.
  • §2.4 + §2.G (lines 1318–1499) — drove ACV 9.0 / cross-layer match; IAG proxy closes the prior capping mismatch.
  • §4.A + §4.G (lines 1742–1923) — drove ROL 9.0; every cross-team unknown is an owned, threshold-gated go/no-go.
  • Comment log 2026-07-13 R3-fix (line 1971) — confirms REV-13/14/15/17 resolved; verified against §2.A (propless prop), §2.E (REV-17 row), §2.F (±60 s), §3 (FE observability).
  • Metadata table vs frontmatter (lines 8/12 vs 136/138) — the one self-invariant violation; held the overall at 8.0 alongside the design-pending FE surface.
  • §2.A + OQ-10 (lines 1358–1369, 1940) — held CNT to 7.5; complete in structure but visually interim — the gating factor between 8.0 and 8.5.
  • §2.F CategoryMapper + comment log 2026-07-15 (lines 1416–1500, 2053) — R5's positive delta: the mapper is now a documented port of AuditEventBuilder, so OQ-12 is closed from source and the eight-type scope + crm_leads fan-out are grounded. Introduced OQ-16 (unified_app filter), which feeds NEW-2.
  • §4 Rollback + §4.E recipe + §3.D vs Decision 17 (lines 1807–1875, 1751–1767) — R5 NEW-1: DELETE WHERE source_tag can't reverse in-place enrichment of live-feed rows; §4 blast-radius/reversibility wording is overstated. Trimmed DIC 9.0→8.5.
  • §1 Success Criteria + §2.F ValidationRunner + §4.G Gate B1 (lines 224–250, 1541, 1911–1915) — R5 NEW-2: the accuracy_pct denominator predates the D16 watermark and OQ-16 skips; if it uses the raw scope count it reads <99% and trips the <90% alert/stop-condition. The single production-impacting R5 finding.