RFC Review: Legacy Migration — CRM Contact Activity Logs → CDP (audits → activity_logs)
Companion review for
rfc-legacy-migration-crm-activity-logs.md, produced by therfc-reviewerskill. Lives beside the RFC; valid only for the RFC revision inreviewed_rfc_last_updated(2026-07-15 working tree, post theAuditEventBuildermapper re-grounding). This is review cycle R5. R1 judged82c8f91(6.5, HOLD); R2 judgedc26fce4(8.0, PROCEED w/ notes); R3 judged2503887(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.FCategoryMapperre-derived from CRM's live-feed emitterAuditEventBuilder:companylink/unlink restored viaCrm::Person/Crm::Company, ticket-resolvedcorrected to stagewin_probability ≥ 1.0, scope list widened to eightauditable_types,ContactResolverextended 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-2accuracy_pctdenominator 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 (CNTat 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 advancingin-review, and reconcile the two R5 findings (NEW-1 rollback scope, NEW-2accuracy_pctdenominator) — 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-auditedupdate_columnatsend.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 unknowns— resolved fromqontak.comsource (2026-07-15): theCategoryMapperis a port of CRM's live-feed emitterAuditEventBuilder. Link/unlink fromCrm::PeopleDeal(deal),PeopleTicket(ticket),Crm::Task, andCrm::Person/Crm::Company(company link/unlink — restored, notCrm::CompaniesDeal, which stays out of scope). Deal/ticketresolved= stagewin_probability ≥ 1.0(fan out tocrm_leads; ticket via the omnichannelTicket/ticket_stage, notcrm_ticket_status); taskcompleted=crm_task_status_id == 4. No product call; residual OQ-16 = whether the backfill applies the live feed'sunified_appuser 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 Element | RFC Section | Coverage |
|---|---|---|
| CALM-S01 (run batch migration) | §2.4 trigger + §2.F consumer + §4.D chunks 2,5,6 | Full |
| CALM-S02 (idempotent re-run) | §2.3 partial unique index + §2.4 /migrate + §2.E | Full |
| 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 change | Full |
| CALM-S05 (status indicator) | §2.A + §2.4 IAG proxy + §4.D chunk 7 | Full — design-pending (OQ-10) |
| CALM-S06-NEG (no dup on re-run) | §2.3 index + §2.4 flag-gate | Full |
| CALM-S07 (pre-sync watermark) | §2.F SyncWatermarkResolver + D16 + chunk 8 | Full (edge OQ-15 flagged) |
| CALM-S08 (association enrichment) | §2.F AssociationLogEnricher + D17 + chunk 9 | Full (P99 re-pin OQ-13) |
| CALM-S09 (Phase-A coverage gate) | §2.4 trigger gate 422 + D18 + chunk 5 | Full |
| PRD §11 metric: zero double-import | §1 Success Criteria + CALM-S07 ACs | Full in RFC; PRD §11 backfill = REV-16, a PM edit |
| PRD §11 metric: enrichment ≥99% | §1 Success Criteria + CALM-S08 ACs | Full in RFC; same REV-16 caveat |
| Every PRD § (1–15 + App A) | Detail 1.A "PRD Section Coverage" table | Full — 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)
| # | Category | Source | Score | Evidence-Based Rationale |
|---|---|---|---|---|
| 1 | PRT — PRD Traceability | Merged | 9.0 | FE: 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. |
| 2 | TDC — Technical Decisions | Merged | 9.0 | 18 decisions + 14 full ADR blocks. FE D14/D15 present; cross-layer conflict resolved (D15 kills the browser-S2S mismatch). |
| 3 | CNT — Contract Specificity | FE | 7.5 | MigrationStatusProps (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. |
| 4 | SCB — Scope Boundaries | FE | 9.0 | Detail 2.I exemplary: explicit FE create / modify / NOT touched (5,000 cap, pagination, TS interface named out-of-scope). |
| 5 | DEP — Dependencies | FE | 8.5 | Owner/status/blocking table; FE chunk 7 gated on BE IAG proxy (chunk 5). USMAN downgraded with justification. |
| 6 | NFS — Non-Functional | FE | 8.0 | Perf (zero render cost; 1 poll/15s), a11y (§3.E role="status", keyboard, contrast), browser support (existing app). No budget needed for a trivial surface. |
| 7 | TPS — Test Plan | FE | 8.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. |
| 8 | DMS — Data Model & Schema | BE | 9.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. |
| 9 | ACV — API Contract & Versioning | BE | 9.0 | §2.4: 6 endpoints full-columned + examples; §2.G confirms FE reads what BE sends (snake_case, no transform). |
| 10 | DIC — Data Integrity & Consistency | BE | 8.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. |
| 11 | FMC — Failure Mode Coverage | Merged | 8.5 | BE: §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. |
| 12 | CSS — Concurrency & Scaling | BE | 8.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. |
| 13 | SAS — Security & Authorization | BE | 8.5 | Threat model + mitigations (parameterized read-only DSN, per-tenant scoping, Vault); Role×Endpoint matrix; cross-tenant → 403. |
| 14 | ROL — Rollout & Rollback | Merged | 9.0 | Deploy 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. |
| 15 | OBS — Observability | Merged | 8.5 | BE: StatsD naming, events, alert thresholds, PII-scrubbed logs, traces. FE: banner_shown/banner_hidden{reason}/poll_failed (REV-14). Correlation addressed. |
| 16 | RCS — Resource & Cost | BE | 8.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). |
| 17 | SBC — Service Boundary & Coupling | BE | 8.5 | §2.F.1 responsibility matrix; D18 keeps contact-service read-only on CRM; CRM-schema coupling flagged (D11 trade-off). |
| 18 | CPA — Pattern Alignment | Merged | 9.0 | Patterns-to-Follow per layer; the one deviation (Redis→Mongo store) flagged + justified (D8). FE Pinia/ofetch, snake_case-direct matched. |
| 19 | CDG — Compliance & Data Governance | BE | 8.5 | Triggered (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
| # | Decision | Status | Residual (gate/edge, not a design hole) |
|---|---|---|---|
| 1 | New S2S endpoints (underscore ns) | Resolved | — |
| 2 | external_id + partial unique index | Resolved | — |
| 3 | source_tag new const | Resolved | Sole alternative (RFC admits it) — acceptable |
| 4 & 10 | Parse audited_changes YAML, 2 branches | Resolved | column→label map = OQ-6 (local default) |
| 5 | Re-implement Audit mapping in Go | Resolved | Rails-sync burden (known limitation #2) |
| 6 | Actor resolution — 4 cases, two-hop | Resolved | deactivated-user rep = OQ-9 (safe default) |
| 7 | Cursor pagination on audits.id | Resolved | query plan = OQ-2 (Stage-0 gate) |
| 8 | Durable Mongo job store | Resolved | — |
| 9 | Contact resolution via crm_data.id | Resolved | <99% fallback designed (REV-5) |
| 11 | Direct read-only Postgres extraction | Resolved | access = OQ-1 (Stage-0 gate) |
| 12 | Underscore namespace | Resolved | — |
| 13 | Append-only for migrated logs | Resolved | amended by D17 exception |
| 14 | FE ships after BE, decoupled | Resolved | — |
| 15 | FE reads status via IAG proxy | Resolved | closes former REV-1/OQ-11 cap |
| 16 | Watermark = CDP contact.created_at | Resolved | cross-channel under-cut = OQ-15 (correctness risk) |
| 17 | Enricher patches in place | Resolved | ±60 s re-pin to P99 = OQ-13 (measure-confirm) |
| 18 | Unsynced contacts — Phase-A gate only | Resolved | — |
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_atearlier 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_appfilter) make a legitimately large fraction of in-scope source audits not get migrated — everypost_sync_live_feedskip and every non-unified_appaudit. But the headline success metric is still defined asaccuracy_pct = records_correctly_migrated / total_in_scope_source_records(§1 Success Criteria, PRD §11) andValidationRunnercomputes it ascountDocuments({source_tag,company_sso_id})vs sourceCOUNT(§2.F). If that sourceCOUNTis the raw eight-type scope count (the same predicate as Gate B1rows_in_scope, which has no watermark orunified_apppredicate), the denominator includes rows the design intends to skip, soaccuracy_pctreads well under 99% on any account with meaningful post-sync history — directly tripping theaccuracy_pct < 90% → PagerDuty + PMalert (§3) and theaccuracy_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 isDELETE 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}) == 0as the rollback-complete check) both silently assume the enricher wrote nothing outsidesource_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 (verifiedschema.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 concreteCREATE INDEX CONCURRENTLYremedy owned by CRM. Honest about the unverified plan. - Reversibility: Swap extractor behind the
CRMExtractorinterface — 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
| Endpoint | Backend Response Schema | Frontend Expected Schema | Match? | Gaps |
|---|---|---|---|---|
GET /v1/activity_logs | existing fields + source_tag,metadata (snake_case, omitempty) | existing 10 fields; ignores new fields | Yes | Additive, no badge (PRD v2.3). |
GET /v1/crm_migration/status (IAG proxy) | {status, progress_pct, records_*, started_at, duration_seconds} snake_case, tenant from session | useCrmMigrationStatus reads same keys; sends no company_sso_id | Yes | Resolved via D15/REV-1 — tenant server-derived, cross-tenant → 403. |
GET /private/.../status (ops) | same + accuracy_pct | n/a — S2S ops only | Yes (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
| Scenario | FE | BE | Works? | Notes |
|---|---|---|---|---|
| Pre-deploy | Old | Old | Yes | Baseline |
| Backend first | Old | New | Yes | Logs render as normal records; indicator absent |
| Frontend first | New | Old | Yes | Indicator hidden (404/not_started); logs render natively once BE ships |
| Both deployed | New | New | Yes | Target |
| Backend rollback | New | Old | Yes | Status API gone → indicator fail-silent; logs removed by rollback |
| Frontend rollback | Old | New | Yes | Logs 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 Path | Txn Scope | Partial Failure | Idempotency Key | Consistency | Duplicate Handling |
|---|---|---|---|---|---|
MigrateBatch upsert ≤20 | per-record upsert | retry 1× → end-of-run queue → halt >10% | (company_sso_id, external_id) (no TTL) | eventual | skip-on-conflict → duplicate_external_id |
| job-state update | single-doc | consumer-loop retry | job_id | strong | n/a |
AssociationLogEnricher | single-doc $set-on-missing | idempotent re-run | match-key (±60 s) | eventual | ambiguous >1 → enrich_ambiguous skip |
| rollback delete-by-tag | WHERE source_tag AND company_sso_id | retry → rollback_failed alert | idempotent (re-delete = 0) | eventual | n/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
| Job | Trigger | Input | Retry | DLQ | Concurrency | Idempotency | Timeout | Assessment |
|---|---|---|---|---|---|---|---|---|
CrmActivityLogMigrationConsumer | trigger enqueue | {job_id, company_sso_id, date_range_start?} | job: resume last_audit_id; batch: 1× then end-of-run | gocraft dead set + failed-record queue 30 d | 1/account | job_id + external_id | USMAN 3 s; Mongo driver default | 8/8 specified |
Textbook — every column grounded (worker_service.go:144-156, activity_log_migration_service.go).
Compliance Trigger Check
| Trigger | Found? | Location | Classification | Assessment |
|---|---|---|---|---|
| PII (name/email/phone) | Yes | actor, user_id, changes[].from/to | UU PDP / GDPR | Handled — §3.D, PII-scrubbed logs |
| Payment / Health | No | — | — | n/a |
| User content w/ retention | Yes | activity_logs audit trail | per existing policy | Handled |
| Auth/session | No (S2S only) | — | — | Vault secrets |
| Cross-border | No | same AliCloud region | UU PDP | Handled — in-jurisdiction |
CDG Status: Active — scored 8.5.
Agentic Readiness Deep-Dive
Vague Word Audit
| # | Word/Phrase | Location | Impact | Replacement |
|---|---|---|---|---|
| 1 | "some records may still be loading" | §2.A banner copy | UX copy, not spec | Fine as interim; finalize w/ OQ-10 |
| 2 | "worker pool default" | §2.F | inherits existing gocraft pool | Acceptable — 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:lineverified; 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_columnonly 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_pctdenominator not reconciled to the new skip classes (R5). The watermark (D16) and, if adopted, the OQ-16unified_appfilter intentionally leave a large share of in-scope audits unmigrated, butaccuracy_pct = migrated / total_in_scope_source_records(§1, §2.F, PRD §11) never nets those out — soValidationRunnerwill 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).
AssociationLogEnricherpatches live-feed docs in place; they carry nosource_tag; rollback deletes onlysource_tag='crm_migration'. So §4's "fully reversible / blast radius = one account's migrated logs" and §4.E'scountDocuments({source_tag})==0completeness 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
- NEW-2 — fix the
accuracy_pctdenominator (§1 Success Criteria, §2.FValidationRunner, PRD §11) so it nets outpost_sync_live_feed/unmapped/ intentionalcontact_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. - 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.
- 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. - §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+).
- OQ-16 — decide the
unified_appbackfill 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. - 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.
- 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-2accuracy_pctdenominator (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_appbackfill filter — couples to NEW-2). Gate E (OQ-12) is resolved from source (mapper ported fromAuditEventBuilder).
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.
| Order | Chunk | Files | Acceptance Criteria | Deps |
|---|---|---|---|---|
| 1 | Schema + const + indexes | activity_log/base.go, consts/const.go, db/migrations/0NN_*.{up,down}.json | 3 indexes created; build compiles; migrate-down clean | None |
| 2 | Contact finder | repository/contact/base.go | resolves crm_data.id→contact; miss→not-found | 1 |
| 3 | Transformer pkg | …/crm_migration/transformer*.go | golden-file: 2 branches, 4 actor cases, category map, unmapped→skip | 2 |
| 4 | MigrateBatch + /migrate | service/…, handler, payload, rest_router.go | ≤20 enforced; conflict→skip; re-run inserts 0 | 3 |
| 5 | Trigger + status + IAG proxy + rollback + durable store + Phase-A gate | handler/service/repo + rest_router.go | flag OFF→403; coverage<threshold→422; 2nd→409; IAG cross-tenant→403 | 4 |
| 6 | Consumer + worker reg + extractor | consumer/…, worker_service.go, job_enqueuer.go | enqueue→consume→cursor→transform→write; resume from cursor | 5 |
| 7 | FE status indicator + composable | new component + useCrmMigrationStatus, DetailPage.vue | 4 states; API error→hidden | 5 + OQ-10 |
| 8 | SyncWatermarkResolver | …/crm_migration/watermark*.go | <watermark→migrate; ≥→skip post_sync_live_feed | 3 |
| 9 | AssociationLogEnricher | …/crm_migration/association_enricher*.go | fill-on-missing; live-feed match; ambiguous→skip; idempotent | 4, 8 |
Dangling Decisions Log
| # | Decision | Location | Owner | Deadline |
|---|---|---|---|---|
| — | None. All 18 decisions resolved. | — | — | — |
Open Questions
| # | Question | Category | Severity |
|---|---|---|---|
| NEW-2 | accuracy_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 halts | DIC / OBS / metric-definition | Important (production-impacting) |
| NEW-1 | Rollback (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 reversibility | DIC / ROL | Important (before Stage 2 / rollback drill) |
| OQ-16 | Should 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 denominator | ACV / metric-scope | Important (before Stage 2; safe default set) |
| GAP-1 | Metadata table DRI/Author (Zhelia Alifa) contradicts frontmatter (Julio J Maliangkay) — which is authoritative? | PRT / doc-integrity | Important (pre-in-review) |
| GAP-2 | Assumptions list mis-numbered (1,6,2,3,4,5) | doc-integrity | Nice-to-have |
| OQ-10 | Figma frame for the status indicator, or bless the pixel3 interim as final? | CNT | Important (FE) |
| OQ-15 | Cross-channel contacts under-cut the watermark → silent pre-sync data loss; quantify + decide special-case | DIC | Important (before Stage 2) |
| OQ-1/2 | CRM read-only DSN + extraction query plan | RCS / CSS | Blocking (Stage-0 run) |
| OQ-7 | Per-account crm_data.id coverage ≥ 99% | DEP | Blocking (per-account run) |
| OQ-9 | USMAN deactivated-user representation | SAS / FMC | Important (safe default set) |
| OQ-12 | Resolved — CategoryMapper ported from live-feed AuditEventBuilder (qontak.com); company link via Crm::Person/Crm::Company, resolved via stage win_probability | ACV | Resolved (no product call; residual OQ-16 = unified_app backfill filter) |
| OQ-13 | Re-pin ±60 s enrichment window to observed P99 | DIC | Nice-to-have (measure-confirm) |
| REV-16 | Mirror two RFC-born metrics into PRD §11 | PRT | Important (PM-owned) |
Evidence Notes
- Detail 2.0 Source Verification (lines 973–1008) — spine of the High confidence; two rows
NOT VERIFIEDand 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_leadsfan-out are grounded. Introduced OQ-16 (unified_appfilter), 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_tagcan'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_pctdenominator 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.