Skip to main content

RFC Review: Legacy Migration — CRM Contact Activity Logs → CDP

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-06-18 / commit c26fce4). This is review cycle R2 — a delta re-review of the R1-fix revision (R1 judged commit 82c8f91, score 6.5).

Executive Summary

  • Overall Score: 8.0/10
  • Rating: Strong
  • RFC Type: full-stack
  • Sub-Type: frontend: enhancement · backend: new-feature
  • Assessment Confidence: High (both layers richly specified; the previous cross-layer mismatch is verifiably closed)
  • Applied Caps/Gates: No cap applied. The §2.G cross-layer mismatch that capped R1 at 6.5 is resolved (the FE now consumes a real, contract-specified IAG session-authed proxy GET /v1/crm_migration/status — §2.4). No category scores below 5.0. No deploy-order gap. The score is now set by category judgment, not by a gate. Not raised to ≥8.5 because two decisions remain operationally gated (OQ-1/OQ-2 external access) and ACV/DIC/FMC/CNT are not all ≥8.5 (the 9.0+ bar).
  • Implementation Readiness Verdict: PROCEED with notes — code for all 8 chunks is writable now; Stage-0 operational gates (OQ-1/2/7) and edge confirmations (OQ-9/12) gate the migration RUN, not the build.
  • Report Path: cdp/legacy-migration-crm-activity-logs/rfcs/rfc-legacy-migration-crm-activity-logs-review.md
  • RFC Author: Zhelia Alifa | Reviewed: 2026-06-18

The R1-fix revision is a substantive, honest improvement — not score-gaming. All twelve R1 findings were addressed in the RFC text and the fixes are sound: the cap-setting blocker (REV-1) is closed with a fully specified endpoint (auth, tenant derivation, request, response, status codes) re-wired into the FE composable; the USMAN path (REV-2) moved from NOT VERIFIED to a verified, named client method (GetUsersByEmails, qontak_launchpad.go:264) with the integer→email→UUID two-hop now explicit in Decision 6 (REV-6); the CategoryMapper (REV-7) is rebuilt on a defensible CDP-owned predicate with a safe fallthrough; append-only enforcement (REV-8), the failure_rate formula (REV-10), and :cutoff (REV-11) are all pinned. What keeps this short of a clean PROCEED/Agentic-Ready is not specification thinness — it is that two decisions (CRM read DSN, extraction query plan) depend on facts that live in another team's production system and genuinely cannot be resolved on paper. The author's §7 reclassification of those as "operational Stage-0 gates, not implementation blockers" is legitimate (verdict below): the code can be written and unit-tested now; only the live run waits.


Quick Verdict

Why this RFC can be implemented agentically:

  • The single R1 cap-setter is gone: §2.4 now carries a complete contract row for GET /iag/v1/crm_migration/status (IAG session + RequirePermissionMiddleware(CustomersCustomersViewKey), tenant from consts.CompanySSOKey, no client-supplied company_sso_id, response subset, 200/200-not_started/401/403). §2.A's useCrmMigrationStatus is re-pointed at it and §2.G self-rates the match "yes". CALM-S05 / chunk 8 is now buildable.
  • Backend spec remains at DDL/handler precision (§2.3 struct diff + index JSON, §2.4 endpoint contracts, §2.D/E integrity + concurrency, §2.F 9/9 async consumer spec, §3.A/B catalogs, §4.D 8-chunk plan with make/pnpm commands). All anchors verified to path:line; the two former NOT VERIFIED rows are now one VERIFIED (USMAN) and one explicitly-external (CRM DSN).

Why this RFC will cause agent guessing or rework (residual):

  • Two id-set / class-name unknowns remain for the CategoryMapper "resolved/won/done" rows (OQ-12). The RFC defines a safe fallthrough (…/update, no record dropped), so this is non-blocking — but an agent that wants the precise verb mapping still cannot get it from the RFC alone.
  • The extraction query plan on a 15M-row table (OQ-2) is unknown until EXPLAIN ANALYZE runs; the agent can write the cursor query but cannot guarantee it performs without the Stage-0 probe. This is correctly scoped as an operational gate, but it is real.

Findings Ledger (carry-forward)

Stable, never-renumbered finding ids. R1 minted REV-1..REV-12. This R2 cycle reconciles each against commit c26fce4.

IDSeverityFinding (one line)RFC locationStatusFirst seenResolved inEvidence / fix
REV-1blockerFE status indicator consumed a /private BasicAuth route a web session cannot call; the session-authed read contract was undefined§2.4, §2.A, §2.G, Decision 15, OQ-11fixedR1R2§2.4 adds a full contract row for GET /iag/v1/crm_migration/status (IAG session + RequirePermissionMiddleware(CustomersCustomersViewKey), tenant from consts.CompanySSOKey, no company_sso_id param, response subset, 200/not_started/401/403). §2.A re-points useCrmMigrationStatus; §2.G match = yes; Decision 15 commits the option; role matrix + chunk 5 + existing-contracts table updated. OQ-11 marked RESOLVED.
REV-2blockerUSMAN email→SSO UUID endpoint unverified; no method/path/contractDetail 2.0 (was NOT VERIFIED), Decision 6, OQ-9fixedR1R2Source Verification row now VERIFIEDQontakLaunchpadClient.GetUsersByEmails(ctx, companySsoId, emails) (qontak_launchpad.go:264, iface :26) → GET {root}/private/users?company_sso_id=&emails=LaunchpadUsersResponse.UserResponse[].SsoID. OQ-9 downgraded to a minor edge (deactivated-user representation). Residual: deactivated Status/empty-result handling unconfirmed — captured as OQ-9 (minor, non-blocking; safe default specified).
REV-3blockerCRM audits/users read-only DSN access unconfirmed; extraction cannot startDetail 2.0 (NOT VERIFIED → external), OQ-1, Dependencies, §2.F.1accepted-riskR1R2Genuinely external — DSN grant on another team's prod DB cannot be spec'd on paper. Reframed as an operational Stage-0 go/no-go gate (§2.F.1 row 2, §7, OQ-1). Legitimate: blocks the run, not the build. Still open as a gate — must be confirmed before any extraction. Default (direct read-only DSN) and config var (CRM_AUDIT_DATABASE_URL, §4.B) are specified.
REV-4blockerExtraction query plan unknown on 15M+ row audits with no (auditable_type,created_at) indexDecision 7, §3 Performance, OQ-2accepted-riskR1R2External/empirical — requires EXPLAIN ANALYZE + MIN/MAX/COUNT against CRM prod. Reframed as Stage-0 gate (§3 Performance, §7, OQ-2). The query shape, page size (500), cursor on id PK, and "add a CRM index if the plan is unacceptable" remediation are all specified. Blocks the run, not the build. Still open as a gate.
REV-5blockerPer-account Contact.source_id coverage unconfirmed; gates contact_not_found < 1%; <99% fallback was undesignedDecision 9, Success Criteria, OQ-7fixed (design) / gate openR1R2Decision 9 now designs the crm_contact_id_map fallback collection (doc shape + unique index (company_sso_id, crm_person_id), populated from contact-migration output) and states a hard ≥99% Stage-0 gate — account is blocked below 99% until backfill or fallback population. The design gap is closed; the per-account coverage number is an operational confirmation (OQ-7).
REV-6majorActor resolution skipped the integer→email hop the PRD specifiesDecision 6, §2.F ActorResolverfixedR1R2Decision 6 now spells the two-hop explicitly: hop 1 (CRM read DSN): SELECT email, full_name, deleted_at FROM users WHERE id=:user_idhop 2 (USMAN): GetUsersByEmails(...).SsoID. §2.F ActorResolver mirrors it; per-job user_id→{email,sso_uuid} cache named. Four cases retained.
REV-7majorCategoryMapper omitted rows; "resolved"/"completed" predicate undefined§2.F CategoryMapperfixedR1R2Rewritten with a grounding correction (#8: CRM has NO resolved/completed/associate verb — every update renders "changed", audit.rb:1052-1062). The mapper now owns a CDP predicate keyed on crm_stage_id/crm_task_status_id/crm_ticket_status_id presence in audited_changes; link/unlink derived from join-model create/destroy. Table is internally consistent (every row has category+action; explicit …/update and skip/unmapped_action_type fallthroughs). OQ-12 captures the residual id-set/class-name confirms with a safe default.
REV-8majorAppend-only policy had no enforcement pointDecision 13, §3.DfixedR1R2§3.D names it precisely: the activity_log repo exposes no delete method today (base.go has only Search*/Count/Insert/BulkInsert/UpdateUserIDByBatch — verified), so append-only holds by absence; the only authorized deleter is the rollback service path (scoped to source_tag='crm_migration' AND company_sso_id). Future invariant stated. Cross-border (UU PDP) one-liner added (both stores in same AliCloud region).
REV-9minorFE 5,000-entry cap can make a 99%-accurate migration look truncated in the UI§2.I, Success Criteria, PRD §4.7accepted-riskR1R2Metric-framing note added after Success Criteria: accuracy_pct is a backend measure; the FE display cap is a UI limit, not a migration gap, and must not be read as low accuracy. The framing ask is satisfied. Remains an accepted limitation (cap itself is out of scope per PRD §4.7).
REV-10minorfailure_rate lacked a denominator definitionstate machine, §2.2, §3.AfixedR1R2Canonical definition added after the state machine: failure_rate = records_failed / (records_migrated + records_skipped + records_failed), cumulative, re-evaluated per batch, excludes skips; halt at >0.10. Declared to bind every "failure_rate > 10%" mention.
REV-11minordate_range_start? vs the MIN/MAX cutoff interaction unspecified§2.F CRMExtractor, Decision 7fixedR1R2§2.F CRMExtractor: :cutoff = COALESCE(date_range_start, MIN(created_at) for the account) — trigger param wins, else the Stage-0 OQ-2 probe value.
REV-12minorNo transactional coupling between batch upsert and the progress counter§2.D, §2.Faccepted-riskR1R2§2.F ValidationRunner note added: the completion-time countDocuments({source_tag, company_sso_id}) vs source COUNT is the authoritative accuracy_pct; the in-progress counter is a best-effort gauge that may drift by a batch on crash/resume and is reconciled at completion. The drift is acknowledged and bounded; remains accepted.

Ledger summary (R2): 12 findings — 8 fixed this cycle (REV-1, 2, 5-design, 6, 7, 8, 10, 11), 2 accepted-risk newly settled with notes (REV-9, REV-12), 2 accepted-risk operational gates still open (REV-3/OQ-1, REV-4/OQ-2). 0 newly found, 0 minted — no fix was superficial and no fix introduced a new inconsistency. Open material items reduced to the two external-access gates plus minor residuals (OQ-7 coverage number, OQ-9 edge, OQ-12 id-sets), all non-blocking for the build.


Delta vs R1

  • Score: 6.5 → 8.0 (+1.5). Band Needs Work → Strong. Verdict HOLD → PROCEED with notes.
  • Cap removed: the cross-layer contract mismatch that pinned R1 at 6.5 is gone — §2.G now reports zero mismatches.
  • Blockers: R1 had 5 open blockers (REV-1/2/3/4/5). R2: REV-1, REV-2, REV-5 fixed/closed; REV-3, REV-4 reclassified to accepted-risk operational gates (not spec failures). 0 implementation blockers remain.
  • Findings: 8 fixed, 2 still open (as gates), 0 newly found.

PRD → RFC Traceability Matrix

Standard format (PRD v2.1 exists)

PRD ElementRFC SectionCoverage
§1 Problem / one-liner§1 OverviewFull
§3 PersonasDetail 1.A Role CoverageFull
§4.1 Non-contact audits as association events§1 Out of Scope; §2.F CategoryMapperFull
§4.2 One-time batch§1 Out of ScopeFull
§4.3 Read-only on CRM§1 Out of Scope; §3 SecurityFull
§4.5 Two component edits, no new screens§2.A / §2.IFull
§4.6 Retention via MIN/MAX(created_at)OQ-2Full
§4.7 5,000-entry FE capKnown Limitations; Success Criteria metric-framing note (REV-9)Full (accepted limitation, now framed)
§5 New S2S endpoint requiredDecision 1; §2.4Full
§5 Net-new schema fields§2.3 struct diff + constFull
§5 CRM extraction via direct queryDecision 11; §2.F CRMExtractorFull (access is operational gate OQ-1 / REV-3)
§5 Idempotency (company_sso_id, external_id)Decision 2; §2.3 indexFull
§5 Durable job-state storeDecision 8; §2.3 jobs collectionFull
§5 Contact mapping via source_idDecision 9; FindOneBySourceID + crm_contact_id_map fallbackFull (coverage number = operational OQ-7 / REV-5)
§5 user_id int → CRM users → email → USMANDecision 6 two-hop; §2.F ActorResolverFull — integer→email hop now explicit (REV-6 fixed)
§6 Migration pipeline component tree§2.F sub-componentsFull
§7 Behavior 1 (trigger)§2.4; §2.2Full
§7 Behavior 2 (extract)Decision 7/11; §2.FFull
§7 Behavior 3 (transform)§2.F ChangesExtractor/ContactResolverFull
§7 Behavior 4 (batch write ≤20)§2.4 /migrate; §2.DFull
§7 Behavior 5 (status)§2.4 status (S2S) + IAG proxyFull — FE consumption gap closed (REV-1 fixed)
§7 Behavior 6 (rollback)§2.4 rollback; §4.E recipeFull
§7.1 CRM source schema§2.0 Source Verification (+7 columns corrected)Full
§7.2 CDP target schema§2.3 DDLFull
§7.3 Category/action mapping§2.F CategoryMapper (CDP-defined predicate)Full — completeness + predicate now defined (REV-7 fixed); id-sets = OQ-12 (safe default)
§7.3 Actor casesDecision 6 (4-case two-hop)Full (improves on PRD)
§8 CALM-S01..S06-NEG stories + ACsDetail 1.CFull
§9 / §9.1 Rollout + transition window§4 Rollout; §2.F.2Full
§10 Observability events§3 Monitoring & AlertingFull
§11 Success metrics§1 Success CriteriaFull
§12 Launch stages§4 Rollout StrategyFull
§13 Dependencies§1 DependenciesFull
§14 Decisions D-1..D-10Detail 1.B (15 decisions, supersets PRD)Full + adds D-11..D-15
§15 OQ-1..OQ-7§5 OQ-1..OQ-12Full + extends

Reverse check (RFC → PRD): Every new endpoint, field, and sub-component traces to a PRD AC. The R2 additions (Decision 15 / IAG proxy, OQ-12) are grounding/governance refinements, each justified. No scope creep.

Summary: ~38 of 38 PRD elements Full, 0 Partial (the two R1 Partials — REV-6 actor hop, REV-7 mapper — are now Full), 0 Missing. Exemplary traceability; improved over R1.


Scorecard

Full-Stack Scorecard (18 categories)

#CategorySourceScoreR1Evidence-Based Rationale
1PRT — PRD TraceabilityMerged9.59.0Bidirectional matrix; the two R1 Partials (REV-6/7) are now Full. 38/38 Full, 0 Missing, no scope creep. Both new R2 additions justified.
2TDC — Technical DecisionsMerged8.58.015 ADR blocks with options/rejection/rationale/reversibility, grounded to path:line. The former deferred FE-access decision is now Decision 15 (Resolved). Held off 9.0 by two decisions (7, 11) that remain empirically gated (OQ-1/2).
3CNT — Contract Specificity (FE)FE8.57.0§2.A TS interface diffs, TimelineInterface.isMigrated, badge snippet, MigrationStatusProps; the status composable now targets a real, reachable endpoint (REV-1 fixed) and sends no tenant param. §2.B data-fetching fully specified.
4SCB — Scope Boundaries (FE)FE9.09.0§2.I enumerates BE/FE create/modify/NOT-touched at file granularity, incl. the duplicate updateTimeline() and omitempty shared-module impact. Unchanged — already strong.
5DEP — Dependencies (FE)FE7.57.0§1 Dependencies table with owner/status/blocking; USMAN dep downgraded to "exists — confirm edge". Two deps remain blocked-pending-confirm/needs-confirm (CRM DSN, per-account coverage) — correctly flagged.
6NFS — Non-Functional (FE)FE7.07.0§3 Performance (badge zero net calls; poll 1/15s; no bundle delta); §3.E Accessibility (WCAG AA, role="status" polite, not color-only). Browser support still "per existing app" (slightly vague); pixel verification waits on OQ-10.
7TPS — Test Plan (FE)FE7.57.0§4.C: pnpm test (vitest) badge both-transforms + indicator states; honestly records no typecheck/e2e. The FE↔status-API boundary is now a real contract, so a chunk-8 integration test is at last writable (still not enumerated as a target — keeps it off 8).
8DMS — Data Model & Schema (BE)BE8.58.5§2.3 struct diff, two createIndexes migrations, job-collection partial-unique, example doc, PII classification, per-status lifecycle. crm_contact_id_map fallback doc shape added (Decision 9). Cardinality still "COUNT before run" (OQ-2).
9ACV — API Contract (BE)BE8.58.0§2.4: method/path/authn/request/response/status/idempotency for all endpoints now — including the previously-missing FE proxy row (REV-1). The one endpoint the FE needs is fully specified. /migrate/rollback examples still "partial" keeps it off 9.
10DIC — Data Integrity (BE)BE8.08.0§2.D matrix (txn scope, partial-failure, permanent idempotency key, consistency, dup handling, stale-read); §2.E concurrency map. Counter-drift (REV-12) now explicitly reconciled via authoritative completion-time ValidationRunner count.
11FMC — Failure Modes (merged)Merged8.58.0BE: §3.A retry/timeout/halt catalog; §3.A.1 branch/skip catalog; §2.F poison handling. FE: §3.C fail-silent indicator, [Deleted reference]; §2.C 5-state matrix. The failure_rate ambiguity (REV-10) is now pinned to one canonical formula. FE error handling matches BE shapes.
12CSS — Concurrency & Scaling (BE)BE8.08.0One-active-job partial-unique; cursor pagination O(log n); ≥500 rec/s via ≤20 batches; per-job USMAN cache; halt backpressure. The real scale risk (query plan) is parked in OQ-2 as a Stage-0 probe.
13SAS — Security (BE)BE8.58.5§3 threat model + mitigations: every query scoped by company_sso_id, parameterized read-only DSN, input validation, secrets via Helm/Vault, staticcheck/sonar, PII log-scrub. Role × Endpoint matrix now includes the tenant-scoped IAG proxy (cross-tenant → 403).
14ROL — Rollout & Rollback (merged)Merged8.57.5Deploy order specified (BE first); §4.A full 6-row compat matrix, all "yes"; §4.B config; §4.E rollback recipe with verification queries; single flag default OFF per-account. The R1 hold (REV-1 blocking CALM-S05 rollout) is removed — chunk 8 now has a buildable proxy and clean rollback (§4.A "Backend rollback" → indicator fail-silent).
15OBS — Observability (merged)Merged8.58.5BE Datadog StatsD events 1:1 with PRD §10, alert thresholds, structured slog, dd-trace/OTel; FE poll + fail-silent. Would reach 9 with an explicit FE→status distributed-trace link.
16SBC — Service Boundary (BE)BE8.58.5Topology + per-service diagrams; §2.F.1 Responsibility Boundary Matrix (now includes the Stage-0 access gates as owned steps); sits beside TF-2519 without collision; sync (USMAN) vs async named.
17CPA — Pattern Alignment (merged)Merged9.08.5Detail 2.0 "Patterns to Follow" per layer with reference files; single justified deviation (Redis→Mongo). The IAG-vs-MAG grounding correction (#9) is now consistent across §2.4/§2.G/§3.D — the FE read is correctly attributed to the IAG RequirePermissionMiddleware pattern.
18CDG — Compliance (BE, conditional)BE8.57.5Active — §3.D classification, legal basis (UU PDP/GDPR), retention, encryption, access audit, right-to-delete. Append-only enforcement point now named (no delete method exists; rollback-only deleter; future invariant — REV-8) and cross-border confirmed (same AliCloud region). The two R1 gaps are closed.

Score movement: 9 categories up (PRT, TDC, CNT, DEP, TPS, ACV, FMC, ROL, CPA, CDG), the rest steady, none down. Overall 8.0 — judgment across 18 categories now clustered 7.0–9.5 with no cap; the residual operational gates (OQ-1/2) and the unmet 9.0+ bar (not all of ACV/DIC/FMC/CNT ≥8.5 — DIC is 8.0) hold it at the top of the "Strong" band rather than into "Agentic-Ready".

Resource & Cost Advisory (non-blocking)

  • §4.F: existing worker pods, one long job per account, CRM read load gated by OQ-2, Mongo write ≤500 rec/s, one small jobs collection. The one item to route to infra: a possible new CRM-side index (Decision 7 / OQ-2) on another team's prod DB — track separately. No blocking cost risk.

Decision Closure Assessment

Decision Index

#DecisionStatusR1Critical Gaps
1New S2S migration endpointsResolvedResolved
2external_id + partial unique indexResolvedResolved
3source_tag new constResolvedResolved
4 & 10Parse audited_changes YAML, two branchesResolvedResolved
5Re-implement Audit mapping in GoResolvedResolvedGolden-file corpus not yet sampled (acceptance criterion exists)
6Actor resolution — 4 cases, two-hopResolvedPartialInteger→email hop now explicit (REV-6 fixed); USMAN verified (REV-2). Residual OQ-9 edge (deactivated rep) is minor
7Cursor pagination on audits.idResolved (gated)Partial:cutoff now defined (REV-11 fixed); query plan empirically gated (OQ-2 / REV-4) — decision itself is complete
8Durable MongoDB job-state storeResolvedResolved
9Contact resolution via source_id + fallbackResolved (gated)Partialcrm_contact_id_map fallback now designed + hard 99% gate (REV-5 fixed); per-account coverage number = operational OQ-7
11Direct read-only Postgres extractionResolved (gated)PartialDecision is complete; DSN access grant is operational (OQ-1 / REV-3)
12Endpoint namespace underscoreResolvedResolved
13Soft-delete policy (append-only)ResolvedPartialEnforcement point named (REV-8 fixed)
14FE deploy coupling (BE first)ResolvedResolved
15FE reads status via IAG session-authed proxyResolved(was Dangling OQ-11)New decision; commits the read-proxy option; contract in §2.4 (REV-1 fixed)
(impl.) CategoryMapperResolvedPartialPredicate + completeness defined; id-sets = OQ-12 with safe fallthrough (REV-7 fixed)

Aggregate (R2): 16 decisions → 16 Resolved, 0 Partial, 0 Dangling. Three carry an empirical/operational gate (Decisions 7, 9, 11 — OQ-1/2/7), but the decisions themselves are complete and agent-implementable; what is gated is the run, not the code. This is the key R1→R2 movement: R1 had 8 Resolved / 6 Partial / 1 Dangling; R2 has 16 Resolved / 0 / 0.


Decision: 15 — FE reads migration status via an IAG session-authed proxy (NEW, Resolved)

Status: Resolved (this is the R1 OQ-11 / REV-1 dangling decision, now closed)

What was decided. A new endpoint GET /iag/v1/crm_migration/status (the gateway strips /iag, so the browser calls /v1/crm_migration/status), authenticated by the IAG session + RequirePermissionMiddleware(CustomersCustomersViewKey), with company_sso_id derived server-side from consts.CompanySSOKey — the FE sends no tenant param; cross-tenant is impossible by construction. Response is a subset of the /private payload (accuracy_pct omitted). Decision 15 records the rejected alternatives (call /private from the browser; expose under another group) with grounded reasons.

Grounding. Strong — rest_router.go:216 (existing /iag/v1/activity_logs), require_permission_middleware.go:97 (consts.CompanySSOKey), activity_log_handler.go:68 (reads the tenant key). The grounding note #9 correctly re-attributes the FE read from "MAG" to "IAG".

Interface specification. Complete — §2.4 row gives auth, tenant rule, request (none), response fields, status codes (200 / 200-not_started / 401 / 403). §2.A re-points useCrmMigrationStatus and §2.G rates the match "yes".

Failure handling. FE fail-silent on error (§3.C, CALM-S05/ERR-1); 401/403 specified server-side.

Challenge results. Scale: N/A (read). Reversibility: additive route — low cost. Consistency: §2.A, §2.G, §3.D, Decision 15, chunk 5, and the existing-contracts table all now agree. Agent implementability: an agent on chunk 8 can build the composable and the route without inventing anything. No residual gap.

Decisions 7, 9, 11 — Resolved-but-operationally-gated

These three decisions are internally complete (query shape, fallback schema, extractor interface all specified) but each depends on a fact only obtainable from CRM/Data prod: the query plan (OQ-2), the per-account coverage number (OQ-7), and the DSN grant (OQ-1). An agent can write and unit-test all three; only the live run is gated. This is the correct shape for "external dependency" — see the §7 verdict.

Decision 6 — Actor resolution (now Resolved)

Two-hop chain explicit (SELECT email, full_name, deleted_at FROM users WHERE id=:user_idGetUsersByEmails(...).SsoID), four cases retained, per-job cache named, USMAN verified. Residual OQ-9 is a minor edge (how a deactivated user is represented) with a safe default already specified — does not reopen the decision.

Decisions 1, 2, 3, 4&10, 5, 8, 12, 14 remain Resolved (unchanged from R1).


UI State Audit

ComponentLoadingEmptyErrorPartialSuccessAssessment
Activity timeline (with badge)defineddefineddefineddefineddefined5/5 — §2.C
Migration status indicatordefineddefined (hidden)defined (fail-silent)defineddefined (hidden on complete)5/5 — §2.C; states now backed by a reachable endpoint (REV-1 fixed)

Summary: 2 of 2 components fully defined; the R1 caveat (status states blocked by the OQ-11 contract gap) is removed.


Performance Budget Check

MetricTargetSourceAssessment
FE bundle deltanone (reuses MpBadge)§3 Performanceadequate (enhancement)
Status poll cost1 req / 15s while mounted§2.Badequate
LCP/INP/CLSnot statedacceptable for enhancement (no new screen)
BE write throughput≥500 rec/s, batches ≤20§3, PRD §5quantified
Per-account jobP95 ≤ 24h§3, PRD §5quantified

No Core Web Vitals budget — acceptable for a badge + banner on an existing page.


Accessibility Review

AspectSpecified?DetailsAssessment
Keyboard navigationpartialbadge no focus change; banner keyboard-skippableadequate
Focus managementyesno focus trapadequate
ARIA labelsyesbadge "Migrated from CRM"; banner role="status" politeadequate
Color contrastyesnot color-only; pixel3 defaultsadequate (verify on real frame — OQ-10)
Motion sensitivitynonot addressedminor gap (static — low risk)
Screen readeryesrole="status" politeadequate

Strong for the scope; pixel/contrast verification waits on Figma frames (OQ-10).


Pattern Alignment Check

PatternRFC ApproachAssessment
State management (Pinia setup store)follows (CustomerStore.ts:722)aligned
Badge UI (MpBadge/pixel3)reuses (SlaBadge.vue:13)aligned
Data fetching ($customFetch/ofetch)followsaligned
FE session-authed read (IAG + RequirePermissionMiddleware)follows (rest_router.go:216)aligned — the new proxy reuses the existing tenant-scoped pattern
BE repository / index / queue / error / metricsfollows, references filesaligned
Job-state storedeviates Redis→Mongo (Decision 8, justified)aligned with ADR
snake_case API → FEread as-is, no transformaligned

No silent parallel patterns. The single deviation is documented as an ADR.


Data Integrity Deep-Dive

Write PathTransaction ScopePartial FailureIdempotency KeyConsistencyDuplicate Handling
MigrateBatch upsert ≤20per-record upsert (no multi-doc txn)retry once → end-of-run queue → halt >10%(company_sso_id, external_id), permanenteventualskip-on-conflict via partial unique index
job-state updatesingle-docconsumer-loop retryjob_idstrongn/a
rollback delete-by-tagaccount-scoped deleteretry → rollback_failed + alertidempotenteventualn/a

Counter drift (REV-12): now reconciled — completion-time ValidationRunner count is authoritative accuracy_pct; the in-progress counter is an explicitly-best-effort gauge. Records themselves stay correct (index protects). Acceptable.

Concurrency Collision Map

#Shared ResourceWritersCollisionResolutionLock Failure
1jobs collection (per account)two trigger callsdouble jobpartial-unique (company_sso_id, status=in_progress)2nd → 409 JOB_ALREADY_RUNNING
2activity_logs same external_idretried/re-run batchesduplicate insertpartial-unique (company_sso_id, external_id)skip + duplicate_external_id
3last_audit_id cursorsingle consumernone (one-active-job)one-active-job invariantn/a

Complete and DB-enforced. Strong.

API Contract Completeness Check

EndpointRequestResponseErrorsAuthIdempotencyExamplesAssessment
POST .../crm_migration (trigger)completecompletecompleteBasicAuthone-active-jobyes6/6
POST .../migratecompletecompletecompleteBasicAuthper-recordpartial5/6
GET .../crm_migration/status (private)completecompletecompleteBasicAuthn/apartial5/6
POST .../crm_migration/rollbackcompletecompletecompleteBasicAuthidempotentpartial5/6
GET /v1/activity_logs (FE read)reusedadditive fieldsexistingIAGn/ayesreused
GET /iag/v1/crm_migration/status (FE proxy)complete (none — tenant from session)completecomplete (401/403/200-not_started)IAG + RequirePermission + tenant-scopen/ayes6/6 — REV-1 fixed

The endpoint the FE needs is now fully specified — the R1 "0/6" row is closed.

Async Job / Event Consumer Spec

Job/ConsumerTriggerInputRetryDLQConcurrencyIdempotencyTimeoutAssessment
CrmActivityLogMigrationConsumertrigger enqueue{job_id, company_sso_id, date_range_start?}resume from last_audit_id; batch retry-once→queuegocraft dead set; failed-record queue 30d1/account + worker pooljob_id + external_idUSMAN 3s; Mongo default9/9 specified

The most complete async spec the rubric typically sees.

Compliance Trigger Check

TriggerFound?LocationClassificationAssessment
PII (name, email, phone)yesactor, changes[].from/to, USMAN emailPIIhandled (§3.D, log-scrub)
User content w/ retentionyesmigrated activity_logsaudit trail, append-onlyhandled; enforcement point named (REV-8)
Auth/session datapartialS2S BasicAuth, IAG session, SSO UUIDaccess-auditedhandled
Cross-border transferyes — assessedCRM PG → CDP Mongo, same AliCloud regionUU PDPhandled — §3.D confirms in-jurisdiction (R1 gap closed)

CDG Status: Active — scored 8.5. Both R1 gaps (append-only enforcement, cross-border) closed.


Cross-Layer Contract Verification

EndpointBackend ResponseFrontend ExpectedMatch?Gaps
GET /v1/activity_logsadds source_tag, metadata (snake_case, omitempty)reads log.source_tag directlyYesadditive; clean
GET /v1/crm_migration/status (IAG proxy){status, progress_pct, …} snake_case, tenant from sessionuseCrmMigrationStatus reads same keys, sends no company_sso_idYesresolved (REV-1) — FE calls the IAG session-authed proxy; server-side tenant scope (reject cross-tenant 403)
GET .../crm_migration/status (private)same + accuracy_pctn/a (S2S ops only)Yes (n/a)ops surface only

Checks performed:

  • Field casing consistent (snake_case, read as-is)
  • Nullability aligned (omitempty / optional TS fields)
  • Error shape matched (BaseResponse / fail-silent FE)
  • Pagination aligned (after_id cursor reused)
  • Auth/access flow consistent — now PASSES (IAG session proxy, REV-1 fixed)

Mismatches found: 0 (R1 had 1). The 6.5 cap is removed.

Cross-Layer Rollout Compatibility Matrix

ScenarioFEBEWorks?Notes
Pre-deployOldOldYesbaseline
Backend firstOldNewYesmigrated logs show without badge
Frontend firstNewOldYessource_tag absent → no badge; status hidden
Both deployedNewNewYestarget
Backend rollbackNewOldYesbadge never matches; status fail-silent
Frontend rollbackOldNewYeslogs migrated, no chrome

Deploy order: Backend first (§4 + Decision 14). Incompatible scenarios: 0.

End-to-End Data Flow

Flow: Run migration (CALM-S01/S02/S03) — fully traceable

Engineer → POST /private/.../crm_migration {company_sso_id}
→ Trigger: flag + prereq + insert job{in_progress} + enqueue
→ gocraft/work → CrmActivityLogMigrationConsumer
→ CRMExtractor: SELECT ... WHERE id > cursor AND auditable_type IN(...) AND created_at >= :cutoff
(:cutoff = COALESCE(date_range_start, MIN(created_at)) — REV-11)
→ per record: ContactResolver(comment-JSON → YAML → FindOneBySourceID → crm_contact_id_map fallback)
+ ActorResolver(CRM users SELECT email → USMAN GetUsersByEmails → SsoID, cached) — REV-6
+ ChangesExtractor(YAML 2-branch) + CategoryMapper(CDP predicate) — REV-7
→ MigrateBatch upsert ≤20 (skip-on-conflict) → activity_logs + job doc
→ completion: ValidationRunner countDocuments vs source COUNT → authoritative accuracy_pct — REV-12

Gaps in flow: none in spec. The cursor performance (OQ-2) and per-account coverage (OQ-7) are runtime gates, not flow gaps.

Flow: View migrated log with badge (CALM-S04) — fully traceable, no gaps.

Flow: Status indicator (CALM-S05) — now fully traceable.

DetailPage mounts → useCrmMigrationStatus → GET /v1/crm_migration/status (IAG session, tenant from CompanySSOKey)
→ 200 {status, progress_pct} → banner; error → fail-silent

The R1 break at the API hop is repaired.


Agentic Readiness Deep-Dive

Vague Word Audit

#Word/PhraseLocationImpactStatus
1"status resolved" / "completed"§2.F CategoryMapper(R1) agent guessed the predicateresolved — predicate keyed on crm_stage_id/crm_task_status_id/crm_ticket_status_id; id-sets confirmed via OQ-12 with safe fallthrough
2"failure_rate > 10%"state machine / §2.2(R1) per-batch vs cumulative ambiguityresolved — canonical formula pinned
3"browser support per existing app"§3 Performanceunquantifiedminor — still vague; inherit the app's matrix

Material vague phrases remaining in spec sections: 1 (browser support, low impact). Down from 3.

Dangling Alternatives

#AlternativesLocationImpact
(none)The R1 "MAG proxy or web-session group" dangling alternative is resolved by Decision 15.

Total dangling alternatives: 0 (R1 had 1).

Task Decomposition Assessment

ChunkAcceptance CriteriaAssessment
1–6 (BE)per-chunk verifiable with make commandsverifiable — agent-ready
7 (FE badge)pnpm test badge in both transformsverifiable
8 (FE status)indicator 4 states; depends on chunk 5 IAG proxy + OQ-10verifiable / buildable — the R1 "blocked on undefined contract" is removed; only pixel polish (OQ-10) is outstanding

§4.D is a model task manifest; all 8 chunks are now buildable (chunk 8 unblocked).


Strengths

  • The cap-setter was genuinely fixed, not papered over (REV-1 / Decision 15 / §2.4 / §2.G). A new IAG session-authed proxy with a full contract (auth, server-side tenant derivation, request/response/status codes) is wired end-to-end into §2.A, §2.G, §3.D, chunk 5, and the existing-contracts table — with zero residual inconsistency. The cross-layer mismatch count is now 0.
  • Decision closure jumped from 8/6/1 to 16/0/0. Every R1 Partial/Dangling decision (6, 7, 9, 11, 13, CategoryMapper, OQ-11) is now Resolved; the three that carry a gate carry an operational gate, with the code-side decision complete.
  • Grounding discipline held and improved. The two R1 NOT VERIFIED rows resolved honestly: USMAN is now VERIFIED to a named client method (qontak_launchpad.go:264); CRM DSN is correctly left as an external grant rather than fabricated. The IAG-vs-MAG correction (#9) propagated consistently.

Biggest Gaps (residual, all non-blocking for the build)

  • OQ-1 / OQ-2 (REV-3/4) — two external-access gates. CRM read-only DSN grant and the extraction query plan on a 15M-row table are confirmations on another team's prod system. Correctly reframed as Stage-0 go/no-go gates; they block the run, not the code. The remediation (default DSN, EXPLAIN ANALYZE + index-if-needed) is specified.
  • OQ-12 — CategoryMapper id-sets / join class names. The "won" crm_stage_id, "resolved" crm_ticket_status_id, "done" crm_task_status_id id-sets and the join-model auditable_type class names are unconfirmed. Safe fallthrough to …/update means no record is dropped, but the precise verb mapping is not derivable from the RFC alone until confirmed.
  • OQ-9 / OQ-10 — minor edges. Deactivated-user representation in GetUsersByEmails (safe default specified) and Figma pixel frames (chunk 8 builds against pixel3 interim). Neither blocks code.

Priority Actions

All are operational confirmations or polish — none block writing and unit-testing the code.

  1. OQ-1 / OQ-2 (Stage-0) — confirm the CRM read-only DSN grant; run MIN/MAX/COUNT + EXPLAIN ANALYZE; add a (auditable_type, created_at) CRM index if the plan is unacceptable. Owners/dates already drafted (§5).
  2. OQ-7 (per account) — run source_id coverage; ≥99% → proceed; <99% → backfill or populate the designed crm_contact_id_map. The migration must not run an account below the gate.
  3. OQ-12 — confirm the join-model class names + the won/resolved/done id-sets; until then the safe …/update fallthrough stands.
  4. OQ-9 / OQ-10 — confirm GetUsersByEmails deactivated-user representation; obtain Figma frames for pixel polish.
  5. Nice-to-have — quantify "browser support" by inheriting the app's matrix; add an FE→status distributed-trace link (would lift OBS to 9).

Backend Contract Addendum

Endpoint Contract Details

EndpointMethod/PathAuthZRequestResponseErrorStatus
triggerPOST /private/activity_logs/crm_migrationBasicAuth S2S{company_sso_id, date_range_start?}{job_id, status}403/409/422Complete
migratePOST /private/activity_logs/migrateBasicAuth{records[≤20]}{inserted, skipped, failed, results[]}400/401Complete
status (private)GET /private/activity_logs/crm_migration/statusBasicAuth?company_sso_id{status, progress_pct, counts, accuracy_pct?}200/not_startedComplete
rollbackPOST /private/activity_logs/crm_migration/rollbackBasicAuth{company_sso_id}{records_removed}404/500Complete
FE status proxyGET /iag/v1/crm_migration/statusIAG session + RequirePermission(CustomersCustomersViewKey); tenant from consts.CompanySSOKey; cross-tenant→403none (tenant from session)subset of /private (no accuracy_pct)200/200-not_started/401/403Complete — REV-1 fixed

Database Changes Details

ChangeTable/EntityDDL / Shape DiffMigrationStatus
add fieldsactivity_logsexternal_id, source_tag, metadata, all omitemptygolang-migrate JSON createIndexesComplete
partial unique indexactivity_logs(company_sso_id, external_id) partial on $exists§2.3 JSONComplete
lookup indexcontacts(company_sso_id, source_id)§2.3 JSONComplete
new collectioncrm_activity_log_migration_jobsfull doc + 2 indexes (partial-unique on status=in_progress)lazy create + index migrationComplete
fallback collection (conditional)crm_contact_id_map{company_sso_id, crm_person_id, customer_id} + unique (company_sso_id, crm_person_id)populated from contact-migration outputComplete (design; used only when coverage <99%)

Implementation Readiness Checklist

Unblocked (agent can proceed)

  • PRD → RFC traceability complete (38/38 Full)
  • All technical decisions resolved (16/16; 3 operationally gated)
  • Failure modes handled with error catalog (§3.A/B/C) + canonical failure_rate
  • Configuration contract (§4.B)
  • Pattern alignment verified (Detail 2.0)
  • Rollout plan with flag + rollback recipe (§4)
  • Observability metrics + alerts (§3)
  • Task decomposition with per-chunk acceptance criteria (§4.D, all 8 chunks)
  • BE schema at DDL precision; all API contracts complete (incl. FE proxy); transaction/idempotency per write path; concurrency map; security boundaries
  • Cross-layer contract verified — 0 mismatches
  • Deploy order specified (backend first)
  • FE UI states defined; accessibility specified; status states backed by a reachable endpoint
  • Append-only enforcement point named; cross-border confirmed

Operationally gated (gate the RUN, not the build)

  • OQ-1 (REV-3) — confirm CRM read-only DSN access (Stage-0)
  • OQ-2 (REV-4) — run EXPLAIN ANALYZE + MIN/MAX/COUNT; index if needed (Stage-0)
  • OQ-7 (REV-5) — confirm per-account source_id coverage ≥99% (per account)
  • OQ-9 — confirm deactivated-user representation (safe default specified)
  • OQ-12 — confirm CategoryMapper id-sets / join class names (safe fallthrough)
  • OQ-10 — Figma frames for pixel polish (chunk 8)

Verdict: Ready to implement (all 8 chunks). The remaining items are operational/run-time gates and minor polish, not code blockers.


Task Manifest

The RFC supplies its own (Detail 4.D, 8 chunks, repo-sourced commands). Verified sound for R2 — chunk 5 now includes the IAG proxy and chunk 8 is unblocked.

OrderChunkFilesAcceptance CriteriaDependencies
1BE schema + const + indexesactivity_log/base.go, consts/const.go, db/migrations/0NN_*.jsonfields compile; indexes exist; migrate-down cleanNone
2Contact finderrepository/contact/base.goFindOneBySourceID resolves; miss → not-foundChunk 1
3Transformer pkgnew …/crm_migration/transformer*.gogolden-file tests (2 change branches, 4 actor cases, CDP category predicate, comment-JSON-first, UTC, unmapped→skip)Chunk 1
4MigrateBatch + /migrateservice/…, handler, payload, rest_router.gobatch ≤20; conflict→skip; re-run inserts 0Chunks 1–3
5Trigger + status (S2S) + IAG status proxy + rollback + durable storehandler/service/repo + 3 /private routes + 1 /iag/v1/crm_migration/status (RequirePermission)flag→403; prereq→422; 2nd→409; rollback by tag; proxy tenant-scoped (cross-tenant→403)Chunk 4
6Consumer + worker reg + extractorconsumer/…, worker_service.go, job_enqueuer.go, CRM DSNenqueue→consume→extract→transform→write; resume from last_audit_idChunk 5; run gated by OQ-1/2/9
7FE interface + badge (both transforms)CustomerStore.ts, ActivityLog.vue, CustomerActivity.vuebadge iff source_tag==='crm_migration' in both; native no badgeChunk 1
8FE status indicator + composablenew component + useCrmMigrationStatus (calls /v1/crm_migration/status), DetailPage.vueindicator 4 statesChunk 5 (IAG proxy); OQ-10 (pixel polish only)

Dangling Decisions Log

#DecisionLocationOwnerDeadline
None. The R1 dangling decision (FE status access, OQ-11) is closed by Decision 15.

Open Questions

Still-open material findings, promoted to the RFC's Open-Questions table by id. All are operational gates or minor confirmations — none block the build.

#QuestionCategorySeverity
1Confirm CRM audits/users read-only DSN access (OQ-1 / REV-3) — Stage-0 gateDEP / SBCOperational gate
2Extraction query plan / volume on 15M+ rows; CRM index needed? (OQ-2 / REV-4) — Stage-0 gateCSS / DMSOperational gate
3Per-account source_id coverage ≥99% (OQ-7 / REV-5) — per-account gate (fallback designed)DIC / DMSOperational gate
4Confirm CategoryMapper id-sets ("won" crm_stage_id, "resolved" crm_ticket_status_id, "done" crm_task_status_id) + join class names (OQ-12 / REV-7) — safe fallthroughTDC / ACVImportant (non-blocking)
5Confirm GetUsersByEmails deactivated-user representation (OQ-9 / REV-2) — safe default specifiedDEP / SASMinor
6Figma frames for badge + status indicator (OQ-10)NFS / CNTImportant (FE polish)
7Assign named reviewers + InfoSec approver (OQ-8)ProcessImportant

Evidence Notes

  • §2.4 + Decision 15 + §2.G — the IAG proxy contract and its end-to-end re-wiring; the basis for closing REV-1 and removing the 6.5 cross-layer cap. Lifted ACV, CNT, ROL.
  • Detail 2.0 Source Verification — the former NOT VERIFIED USMAN row is now VERIFIED (qontak_launchpad.go:264); the CRM DSN row is honestly left external. Drove the REV-2 fix and the REV-3/4 reclassification.
  • §2.F CategoryMapper + grounding note #8 — the CDP-owned predicate keyed on status-id presence, with OQ-12 safe fallthrough; basis for the REV-7 fix.
  • Decision 6 two-hop + §2.F ActorResolver — explicit integer→email→UUID chain; basis for the REV-6 fix.
  • §3.D — append-only enforcement by absence-of-delete-method + rollback-only deleter, and the cross-border confirmation; basis for REV-8 and the CDG lift to 8.5.
  • State machine failure_rate note + §2.F :cutoff — REV-10/11 fixes.
  • §7 — the "implementation-ready" reclassification; judged legitimate (see verdict).

Verdict on the §7 "implementation-ready" reclassification

Legitimate, not score-gaming. The test for reframing a blocker as an "operational gate" is whether the code can be written and unit-tested without the answer, or whether the agent must guess a spec detail. For OQ-1 (DSN grant), OQ-2 (query plan), and OQ-7 (coverage number), the answer is the former: the cursor query, the extractor interface, the fallback collection, and the index remediation are all fully specified — what is missing is a production-access grant and an empirical measurement that no amount of RFC writing can produce. Calling those "Stage-0 go/no-go gates that gate running, not building" is an accurate description, and the RFC is careful to say the code can be written and unit-tested now (chunk 6's run is gated, not its implementation). This is materially different from the R1 OQ-11 case, which was a genuine spec hole (an undefined endpoint the FE had to invent) — and that one the author actually fixed rather than reclassified. The distinction is the tell that this is honest: the reclassified items are the ones that truly cannot be resolved on paper, while the resolvable spec hole was resolved. The one place to keep the author honest: OQ-12's id-sets are a spec detail (a real mapping), not purely operational — but the safe …/update fallthrough means an agent never has to guess and never drops a record, so "non-blocking" stands.


Review History

CycleDateReviewed RFC revision (last_updated / commit)ScoreVerdictFindings open → fixedNotes
R12026-06-182026-06-18 / 82c8f916.5HOLD10 open (5B/3M/2m after accepting REV-9/12), 0 fixedFirst cycle. Near-PROCEED specification; held by 5 access/contract blockers (OQ-1/2/7/9/11) + 1 cross-layer mismatch capping overall at 6.5.
R22026-06-182026-06-18 / c26fce48.0PROCEED with notes8 fixed, 2 open (operational gates REV-3/4), 0 newly foundDelta re-review of the R1-fix revision. Cross-layer cap removed (REV-1 closed by Decision 15 IAG proxy); USMAN verified (REV-2/6); CategoryMapper predicate (REV-7); append-only enforcement + cross-border (REV-8); failure_rate/:cutoff pinned (REV-10/11); crm_contact_id_map fallback designed (REV-5). Decision closure 8/6/1 → 16/0/0. §7 "implementation-ready" reclassification judged legitimate. Remaining items gate the migration RUN, not the build.