RFC Review: Legacy Migration — CRM Contact Activity Logs → CDP
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-06-18 / commitc26fce4). This is review cycle R2 — a delta re-review of the R1-fix revision (R1 judged commit82c8f91, 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 (the9.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 fromconsts.CompanySSOKey, no client-suppliedcompany_sso_id, response subset, 200/200-not_started/401/403). §2.A'suseCrmMigrationStatusis 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/pnpmcommands). All anchors verified topath:line; the two formerNOT VERIFIEDrows 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 ANALYZEruns; 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.
| ID | Severity | Finding (one line) | RFC location | Status | First seen | Resolved in | Evidence / fix |
|---|---|---|---|---|---|---|---|
REV-1 | blocker | FE 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-11 | fixed | R1 | R2 | §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-2 | blocker | USMAN email→SSO UUID endpoint unverified; no method/path/contract | Detail 2.0 (was NOT VERIFIED), Decision 6, OQ-9 | fixed | R1 | R2 | Source Verification row now VERIFIED — QontakLaunchpadClient.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-3 | blocker | CRM audits/users read-only DSN access unconfirmed; extraction cannot start | Detail 2.0 (NOT VERIFIED → external), OQ-1, Dependencies, §2.F.1 | accepted-risk | R1 | R2 | Genuinely 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-4 | blocker | Extraction query plan unknown on 15M+ row audits with no (auditable_type,created_at) index | Decision 7, §3 Performance, OQ-2 | accepted-risk | R1 | R2 | External/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-5 | blocker | Per-account Contact.source_id coverage unconfirmed; gates contact_not_found < 1%; <99% fallback was undesigned | Decision 9, Success Criteria, OQ-7 | fixed (design) / gate open | R1 | R2 | Decision 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-6 | major | Actor resolution skipped the integer→email hop the PRD specifies | Decision 6, §2.F ActorResolver | fixed | R1 | R2 | Decision 6 now spells the two-hop explicitly: hop 1 (CRM read DSN): SELECT email, full_name, deleted_at FROM users WHERE id=:user_id → hop 2 (USMAN): GetUsersByEmails(...).SsoID. §2.F ActorResolver mirrors it; per-job user_id→{email,sso_uuid} cache named. Four cases retained. |
REV-7 | major | CategoryMapper omitted rows; "resolved"/"completed" predicate undefined | §2.F CategoryMapper | fixed | R1 | R2 | Rewritten 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-8 | major | Append-only policy had no enforcement point | Decision 13, §3.D | fixed | R1 | R2 | §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-9 | minor | FE 5,000-entry cap can make a 99%-accurate migration look truncated in the UI | §2.I, Success Criteria, PRD §4.7 | accepted-risk | R1 | R2 | Metric-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-10 | minor | failure_rate lacked a denominator definition | state machine, §2.2, §3.A | fixed | R1 | R2 | Canonical 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-11 | minor | date_range_start? vs the MIN/MAX cutoff interaction unspecified | §2.F CRMExtractor, Decision 7 | fixed | R1 | R2 | §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-12 | minor | No transactional coupling between batch upsert and the progress counter | §2.D, §2.F | accepted-risk | R1 | R2 | §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). BandNeeds Work → Strong. VerdictHOLD → 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 Element | RFC Section | Coverage |
|---|---|---|
| §1 Problem / one-liner | §1 Overview | Full |
| §3 Personas | Detail 1.A Role Coverage | Full |
| §4.1 Non-contact audits as association events | §1 Out of Scope; §2.F CategoryMapper | Full |
| §4.2 One-time batch | §1 Out of Scope | Full |
| §4.3 Read-only on CRM | §1 Out of Scope; §3 Security | Full |
| §4.5 Two component edits, no new screens | §2.A / §2.I | Full |
§4.6 Retention via MIN/MAX(created_at) | OQ-2 | Full |
| §4.7 5,000-entry FE cap | Known Limitations; Success Criteria metric-framing note (REV-9) | Full (accepted limitation, now framed) |
| §5 New S2S endpoint required | Decision 1; §2.4 | Full |
| §5 Net-new schema fields | §2.3 struct diff + const | Full |
| §5 CRM extraction via direct query | Decision 11; §2.F CRMExtractor | Full (access is operational gate OQ-1 / REV-3) |
§5 Idempotency (company_sso_id, external_id) | Decision 2; §2.3 index | Full |
| §5 Durable job-state store | Decision 8; §2.3 jobs collection | Full |
§5 Contact mapping via source_id | Decision 9; FindOneBySourceID + crm_contact_id_map fallback | Full (coverage number = operational OQ-7 / REV-5) |
§5 user_id int → CRM users → email → USMAN | Decision 6 two-hop; §2.F ActorResolver | Full — integer→email hop now explicit (REV-6 fixed) |
| §6 Migration pipeline component tree | §2.F sub-components | Full |
| §7 Behavior 1 (trigger) | §2.4; §2.2 | Full |
| §7 Behavior 2 (extract) | Decision 7/11; §2.F | Full |
| §7 Behavior 3 (transform) | §2.F ChangesExtractor/ContactResolver | Full |
| §7 Behavior 4 (batch write ≤20) | §2.4 /migrate; §2.D | Full |
| §7 Behavior 5 (status) | §2.4 status (S2S) + IAG proxy | Full — FE consumption gap closed (REV-1 fixed) |
| §7 Behavior 6 (rollback) | §2.4 rollback; §4.E recipe | Full |
| §7.1 CRM source schema | §2.0 Source Verification (+7 columns corrected) | Full |
| §7.2 CDP target schema | §2.3 DDL | Full |
| §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 cases | Decision 6 (4-case two-hop) | Full (improves on PRD) |
| §8 CALM-S01..S06-NEG stories + ACs | Detail 1.C | Full |
| §9 / §9.1 Rollout + transition window | §4 Rollout; §2.F.2 | Full |
| §10 Observability events | §3 Monitoring & Alerting | Full |
| §11 Success metrics | §1 Success Criteria | Full |
| §12 Launch stages | §4 Rollout Strategy | Full |
| §13 Dependencies | §1 Dependencies | Full |
| §14 Decisions D-1..D-10 | Detail 1.B (15 decisions, supersets PRD) | Full + adds D-11..D-15 |
| §15 OQ-1..OQ-7 | §5 OQ-1..OQ-12 | Full + 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)
| # | Category | Source | Score | R1 | Evidence-Based Rationale |
|---|---|---|---|---|---|
| 1 | PRT — PRD Traceability | Merged | 9.5 | 9.0 | Bidirectional matrix; the two R1 Partials (REV-6/7) are now Full. 38/38 Full, 0 Missing, no scope creep. Both new R2 additions justified. |
| 2 | TDC — Technical Decisions | Merged | 8.5 | 8.0 | 15 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). |
| 3 | CNT — Contract Specificity (FE) | FE | 8.5 | 7.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. |
| 4 | SCB — Scope Boundaries (FE) | FE | 9.0 | 9.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. |
| 5 | DEP — Dependencies (FE) | FE | 7.5 | 7.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. |
| 6 | NFS — Non-Functional (FE) | FE | 7.0 | 7.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. |
| 7 | TPS — Test Plan (FE) | FE | 7.5 | 7.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). |
| 8 | DMS — Data Model & Schema (BE) | BE | 8.5 | 8.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). |
| 9 | ACV — API Contract (BE) | BE | 8.5 | 8.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. |
| 10 | DIC — Data Integrity (BE) | BE | 8.0 | 8.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. |
| 11 | FMC — Failure Modes (merged) | Merged | 8.5 | 8.0 | BE: §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. |
| 12 | CSS — Concurrency & Scaling (BE) | BE | 8.0 | 8.0 | One-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. |
| 13 | SAS — Security (BE) | BE | 8.5 | 8.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). |
| 14 | ROL — Rollout & Rollback (merged) | Merged | 8.5 | 7.5 | Deploy 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). |
| 15 | OBS — Observability (merged) | Merged | 8.5 | 8.5 | BE 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. |
| 16 | SBC — Service Boundary (BE) | BE | 8.5 | 8.5 | Topology + 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. |
| 17 | CPA — Pattern Alignment (merged) | Merged | 9.0 | 8.5 | Detail 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. |
| 18 | CDG — Compliance (BE, conditional) | BE | 8.5 | 7.5 | Active — §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
| # | Decision | Status | R1 | Critical Gaps |
|---|---|---|---|---|
| 1 | New S2S migration endpoints | Resolved | Resolved | — |
| 2 | external_id + partial unique index | Resolved | Resolved | — |
| 3 | source_tag new const | Resolved | Resolved | — |
| 4 & 10 | Parse audited_changes YAML, two branches | Resolved | Resolved | — |
| 5 | Re-implement Audit mapping in Go | Resolved | Resolved | Golden-file corpus not yet sampled (acceptance criterion exists) |
| 6 | Actor resolution — 4 cases, two-hop | Resolved | Partial | Integer→email hop now explicit (REV-6 fixed); USMAN verified (REV-2). Residual OQ-9 edge (deactivated rep) is minor |
| 7 | Cursor pagination on audits.id | Resolved (gated) | Partial | :cutoff now defined (REV-11 fixed); query plan empirically gated (OQ-2 / REV-4) — decision itself is complete |
| 8 | Durable MongoDB job-state store | Resolved | Resolved | — |
| 9 | Contact resolution via source_id + fallback | Resolved (gated) | Partial | crm_contact_id_map fallback now designed + hard 99% gate (REV-5 fixed); per-account coverage number = operational OQ-7 |
| 11 | Direct read-only Postgres extraction | Resolved (gated) | Partial | Decision is complete; DSN access grant is operational (OQ-1 / REV-3) |
| 12 | Endpoint namespace underscore | Resolved | Resolved | — |
| 13 | Soft-delete policy (append-only) | Resolved | Partial | Enforcement point named (REV-8 fixed) |
| 14 | FE deploy coupling (BE first) | Resolved | Resolved | — |
| 15 | FE reads status via IAG session-authed proxy | Resolved | (was Dangling OQ-11) | New decision; commits the read-proxy option; contract in §2.4 (REV-1 fixed) |
| (impl.) CategoryMapper | Resolved | Partial | Predicate + 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_id → GetUsersByEmails(...).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
| Component | Loading | Empty | Error | Partial | Success | Assessment |
|---|---|---|---|---|---|---|
| Activity timeline (with badge) | defined | defined | defined | defined | defined | 5/5 — §2.C |
| Migration status indicator | defined | defined (hidden) | defined (fail-silent) | defined | defined (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
| Metric | Target | Source | Assessment |
|---|---|---|---|
| FE bundle delta | none (reuses MpBadge) | §3 Performance | adequate (enhancement) |
| Status poll cost | 1 req / 15s while mounted | §2.B | adequate |
| LCP/INP/CLS | not stated | — | acceptable for enhancement (no new screen) |
| BE write throughput | ≥500 rec/s, batches ≤20 | §3, PRD §5 | quantified |
| Per-account job | P95 ≤ 24h | §3, PRD §5 | quantified |
No Core Web Vitals budget — acceptable for a badge + banner on an existing page.
Accessibility Review
| Aspect | Specified? | Details | Assessment |
|---|---|---|---|
| Keyboard navigation | partial | badge no focus change; banner keyboard-skippable | adequate |
| Focus management | yes | no focus trap | adequate |
| ARIA labels | yes | badge "Migrated from CRM"; banner role="status" polite | adequate |
| Color contrast | yes | not color-only; pixel3 defaults | adequate (verify on real frame — OQ-10) |
| Motion sensitivity | no | not addressed | minor gap (static — low risk) |
| Screen reader | yes | role="status" polite | adequate |
Strong for the scope; pixel/contrast verification waits on Figma frames (OQ-10).
Pattern Alignment Check
| Pattern | RFC Approach | Assessment |
|---|---|---|
| State management (Pinia setup store) | follows (CustomerStore.ts:722) | aligned |
Badge UI (MpBadge/pixel3) | reuses (SlaBadge.vue:13) | aligned |
Data fetching ($customFetch/ofetch) | follows | aligned |
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 / metrics | follows, references files | aligned |
| Job-state store | deviates Redis→Mongo (Decision 8, justified) | aligned with ADR |
| snake_case API → FE | read as-is, no transform | aligned |
No silent parallel patterns. The single deviation is documented as an ADR.
Data Integrity Deep-Dive
| Write Path | Transaction Scope | Partial Failure | Idempotency Key | Consistency | Duplicate Handling |
|---|---|---|---|---|---|
MigrateBatch upsert ≤20 | per-record upsert (no multi-doc txn) | retry once → end-of-run queue → halt >10% | (company_sso_id, external_id), permanent | eventual | skip-on-conflict via partial unique index |
| job-state update | single-doc | consumer-loop retry | job_id | strong | n/a |
| rollback delete-by-tag | account-scoped delete | retry → rollback_failed + alert | idempotent | eventual | n/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 Resource | Writers | Collision | Resolution | Lock Failure |
|---|---|---|---|---|---|
| 1 | jobs collection (per account) | two trigger calls | double job | partial-unique (company_sso_id, status=in_progress) | 2nd → 409 JOB_ALREADY_RUNNING |
| 2 | activity_logs same external_id | retried/re-run batches | duplicate insert | partial-unique (company_sso_id, external_id) | skip + duplicate_external_id |
| 3 | last_audit_id cursor | single consumer | none (one-active-job) | one-active-job invariant | n/a |
Complete and DB-enforced. Strong.
API Contract Completeness Check
| Endpoint | Request | Response | Errors | Auth | Idempotency | Examples | Assessment |
|---|---|---|---|---|---|---|---|
POST .../crm_migration (trigger) | complete | complete | complete | BasicAuth | one-active-job | yes | 6/6 |
POST .../migrate | complete | complete | complete | BasicAuth | per-record | partial | 5/6 |
GET .../crm_migration/status (private) | complete | complete | complete | BasicAuth | n/a | partial | 5/6 |
POST .../crm_migration/rollback | complete | complete | complete | BasicAuth | idempotent | partial | 5/6 |
GET /v1/activity_logs (FE read) | reused | additive fields | existing | IAG | n/a | yes | reused |
GET /iag/v1/crm_migration/status (FE proxy) | complete (none — tenant from session) | complete | complete (401/403/200-not_started) | IAG + RequirePermission + tenant-scope | n/a | yes | 6/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/Consumer | Trigger | Input | Retry | DLQ | Concurrency | Idempotency | Timeout | Assessment |
|---|---|---|---|---|---|---|---|---|
CrmActivityLogMigrationConsumer | trigger enqueue | {job_id, company_sso_id, date_range_start?} | resume from last_audit_id; batch retry-once→queue | gocraft dead set; failed-record queue 30d | 1/account + worker pool | job_id + external_id | USMAN 3s; Mongo default | 9/9 specified |
The most complete async spec the rubric typically sees.
Compliance Trigger Check
| Trigger | Found? | Location | Classification | Assessment |
|---|---|---|---|---|
| PII (name, email, phone) | yes | actor, changes[].from/to, USMAN email | PII | handled (§3.D, log-scrub) |
| User content w/ retention | yes | migrated activity_logs | audit trail, append-only | handled; enforcement point named (REV-8) |
| Auth/session data | partial | S2S BasicAuth, IAG session, SSO UUID | access-audited | handled |
| Cross-border transfer | yes — assessed | CRM PG → CDP Mongo, same AliCloud region | UU PDP | handled — §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
| Endpoint | Backend Response | Frontend Expected | Match? | Gaps |
|---|---|---|---|---|
GET /v1/activity_logs | adds source_tag, metadata (snake_case, omitempty) | reads log.source_tag directly | Yes | additive; clean |
GET /v1/crm_migration/status (IAG proxy) | {status, progress_pct, …} snake_case, tenant from session | useCrmMigrationStatus reads same keys, sends no company_sso_id | Yes | resolved (REV-1) — FE calls the IAG session-authed proxy; server-side tenant scope (reject cross-tenant 403) |
GET .../crm_migration/status (private) | same + accuracy_pct | n/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_idcursor 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
| Scenario | FE | BE | Works? | Notes |
|---|---|---|---|---|
| Pre-deploy | Old | Old | Yes | baseline |
| Backend first | Old | New | Yes | migrated logs show without badge |
| Frontend first | New | Old | Yes | source_tag absent → no badge; status hidden |
| Both deployed | New | New | Yes | target |
| Backend rollback | New | Old | Yes | badge never matches; status fail-silent |
| Frontend rollback | Old | New | Yes | logs 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/Phrase | Location | Impact | Status |
|---|---|---|---|---|
| 1 | "status resolved" / "completed" | §2.F CategoryMapper | (R1) agent guessed the predicate | resolved — 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 ambiguity | resolved — canonical formula pinned |
| 3 | "browser support per existing app" | §3 Performance | unquantified | minor — still vague; inherit the app's matrix |
Material vague phrases remaining in spec sections: 1 (browser support, low impact). Down from 3.
Dangling Alternatives
| # | Alternatives | Location | Impact |
|---|---|---|---|
| — | (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
| Chunk | Acceptance Criteria | Assessment |
|---|---|---|
| 1–6 (BE) | per-chunk verifiable with make commands | verifiable — agent-ready |
| 7 (FE badge) | pnpm test badge in both transforms | verifiable |
| 8 (FE status) | indicator 4 states; depends on chunk 5 IAG proxy + OQ-10 | verifiable / 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 VERIFIEDrows 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_idid-sets and the join-modelauditable_typeclass names are unconfirmed. Safe fallthrough to…/updatemeans 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.
- 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). - OQ-7 (per account) — run
source_idcoverage; ≥99% → proceed;<99%→ backfill or populate the designedcrm_contact_id_map. The migration must not run an account below the gate. - OQ-12 — confirm the join-model class names + the won/resolved/done id-sets; until then the safe
…/updatefallthrough stands. - OQ-9 / OQ-10 — confirm
GetUsersByEmailsdeactivated-user representation; obtain Figma frames for pixel polish. - 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
| Endpoint | Method/Path | AuthZ | Request | Response | Error | Status |
|---|---|---|---|---|---|---|
| trigger | POST /private/activity_logs/crm_migration | BasicAuth S2S | {company_sso_id, date_range_start?} | {job_id, status} | 403/409/422 | Complete |
| migrate | POST /private/activity_logs/migrate | BasicAuth | {records[≤20]} | {inserted, skipped, failed, results[]} | 400/401 | Complete |
| status (private) | GET /private/activity_logs/crm_migration/status | BasicAuth | ?company_sso_id | {status, progress_pct, counts, accuracy_pct?} | 200/not_started | Complete |
| rollback | POST /private/activity_logs/crm_migration/rollback | BasicAuth | {company_sso_id} | {records_removed} | 404/500 | Complete |
| FE status proxy | GET /iag/v1/crm_migration/status | IAG session + RequirePermission(CustomersCustomersViewKey); tenant from consts.CompanySSOKey; cross-tenant→403 | none (tenant from session) | subset of /private (no accuracy_pct) | 200/200-not_started/401/403 | Complete — REV-1 fixed |
Database Changes Details
| Change | Table/Entity | DDL / Shape Diff | Migration | Status |
|---|---|---|---|---|
| add fields | activity_logs | external_id, source_tag, metadata, all omitempty | golang-migrate JSON createIndexes | Complete |
| partial unique index | activity_logs | (company_sso_id, external_id) partial on $exists | §2.3 JSON | Complete |
| lookup index | contacts | (company_sso_id, source_id) | §2.3 JSON | Complete |
| new collection | crm_activity_log_migration_jobs | full doc + 2 indexes (partial-unique on status=in_progress) | lazy create + index migration | Complete |
| 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 output | Complete (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_idcoverage ≥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.
| Order | Chunk | Files | Acceptance Criteria | Dependencies |
|---|---|---|---|---|
| 1 | BE schema + const + indexes | activity_log/base.go, consts/const.go, db/migrations/0NN_*.json | fields compile; indexes exist; migrate-down clean | None |
| 2 | Contact finder | repository/contact/base.go | FindOneBySourceID resolves; miss → not-found | Chunk 1 |
| 3 | Transformer pkg | new …/crm_migration/transformer*.go | golden-file tests (2 change branches, 4 actor cases, CDP category predicate, comment-JSON-first, UTC, unmapped→skip) | Chunk 1 |
| 4 | MigrateBatch + /migrate | service/…, handler, payload, rest_router.go | batch ≤20; conflict→skip; re-run inserts 0 | Chunks 1–3 |
| 5 | Trigger + status (S2S) + IAG status proxy + rollback + durable store | handler/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 |
| 6 | Consumer + worker reg + extractor | consumer/…, worker_service.go, job_enqueuer.go, CRM DSN | enqueue→consume→extract→transform→write; resume from last_audit_id | Chunk 5; run gated by OQ-1/2/9 |
| 7 | FE interface + badge (both transforms) | CustomerStore.ts, ActivityLog.vue, CustomerActivity.vue | badge iff source_tag==='crm_migration' in both; native no badge | Chunk 1 |
| 8 | FE status indicator + composable | new component + useCrmMigrationStatus (calls /v1/crm_migration/status), DetailPage.vue | indicator 4 states | Chunk 5 (IAG proxy); OQ-10 (pixel polish only) |
Dangling Decisions Log
| # | Decision | Location | Owner | Deadline |
|---|---|---|---|---|
| — | 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.
| # | Question | Category | Severity |
|---|---|---|---|
| 1 | Confirm CRM audits/users read-only DSN access (OQ-1 / REV-3) — Stage-0 gate | DEP / SBC | Operational gate |
| 2 | Extraction query plan / volume on 15M+ rows; CRM index needed? (OQ-2 / REV-4) — Stage-0 gate | CSS / DMS | Operational gate |
| 3 | Per-account source_id coverage ≥99% (OQ-7 / REV-5) — per-account gate (fallback designed) | DIC / DMS | Operational gate |
| 4 | Confirm 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 fallthrough | TDC / ACV | Important (non-blocking) |
| 5 | Confirm GetUsersByEmails deactivated-user representation (OQ-9 / REV-2) — safe default specified | DEP / SAS | Minor |
| 6 | Figma frames for badge + status indicator (OQ-10) | NFS / CNT | Important (FE polish) |
| 7 | Assign named reviewers + InfoSec approver (OQ-8) | Process | Important |
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 VERIFIEDUSMAN 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_ratenote + §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
| Cycle | Date | Reviewed RFC revision (last_updated / commit) | Score | Verdict | Findings open → fixed | Notes |
|---|---|---|---|---|---|---|
R1 | 2026-06-18 | 2026-06-18 / 82c8f91 | 6.5 | HOLD | 10 open (5B/3M/2m after accepting REV-9/12), 0 fixed | First 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. |
R2 | 2026-06-18 | 2026-06-18 / c26fce4 | 8.0 | PROCEED with notes | 8 fixed, 2 open (operational gates REV-3/4), 0 newly found | Delta 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. |