Skip to main content

RFC Review: Export Customer Data with Layout (XLSX/CSV, async, email + in-app notification)

Companion review for rfc-export-customer-data.md, produced by the rfc-reviewer skill. Current cycle: R2 — 2026-06-30, against RFC last_updated: 2026-06-30 (PRD v2.6 scope). The body below reflects the R2 (current) state; the Findings Ledger and Review History carry the full audit trail from R1 (2026-06-18). R1 reviewed the PRD-v2.2-era RFC and surfaced 10 findings (6 fixed same-day); R2 re-scores the post-fix RFC plus the v2.6 additions (first_10k_sorted selection mode, EXP-S08 export-history reuse, EXP-S09 right-side panel) and the 2026-06-30 banner-copy fix.

Executive Summary

  • Overall Score: 8.5/10 (was 8.0 at R1 — pre-fix snapshot)
  • Rating: Agentic-Ready
  • RFC Type: full-stack
  • Sub-Type: new-feature (FE: new-feature · BE: new-feature; Mobile is render-only / flag-gated)
  • Assessment Confidence: High
  • Applied Caps/Gates: None triggered. NFS 7.0 is not the performance sub-type so the NFS<4.0 cap is inert; no category < 5.0; ACV 9.0, DIC 8.5, SAS 8.5, CDG 8.0, FMC 8.5, ROL 8.5 all clear their floors; no cross-layer contract mismatch (the single §2.G partial on field_properties is resolved by an in-page FE mapping); fewer than 4 categories below 5.0. The 9.0+ tier is unreachable only because NFS 7.0 < 7.5 (no FE Core-Web-Vitals budget) — every other gate for 9.0 is met (ACV/DIC/FMC/CNT ≥ 8.5; zero dangling decisions; no mismatch).
  • Agentic Readiness Verdict: PROCEED — the core pipeline (EXP-S01–S07, S09) is agent-executable now. One story is specified-but-not-buildable: EXP-S08 (export-history reuse) is blocked on a net-new cross-team path (OQ-17); it ships behind a non-fatal ExportHistoryRegistrar interface and is a PRD Should-Have, so it does not gate the core release. Confirm OQ-10 (notif channel) before chunk 7, OQ-20 (timezone value format) before chunks 18/21, and OQ-21 (source enum) before chunk 21 — all carry adopted, reversible defaults.
  • RFC Author: Zhelia Alifa | Reviewed (R2): 2026-06-30 | R1: 2026-06-18

The R1 blocker and majors are now closed: the rate-limit contract (REV-5/OQ-14) that capped R1 has moved from Dangling to a decisive v1 design — extend IRateLimiterService + RateLimitMiddleware with an additive per-route (key, max, window) param (("export", 5, 3600)), grounded against the real middleware (rate_limit_middleware.go:16-55, which today takes only the service, keys on companySsoID, and hardcodes limit=1 + an "import" message), with a documented fallback (inherit the shared limiter + amend PRD §5). The custom-field value resolution (REV-2) is now a concrete algorithm (§2.4 — SearchWithFilters + _id $in + pagination; default field → top-level struct, custom → CustomFields[].Key). The success-banner copy (REV-11) now carries the PRD AC-2 GMT-timezone sentence. Decision discipline expanded from 13 → 17 ADR decisions (Decisions 9–11, 14–17 added for first_10k_sorted, export-history, and the panel) with no dangling decisions remaining. The remaining work is honest cross-team / confirm-item surface introduced by v2.6 — none of it dangling, all chunk-scoped.


R2 Delta vs R1

ClassFindingsDetail
Confirmed fixed (carried)REV-1, REV-2, REV-3, REV-4, REV-6The 5 R1 same-day fixes are present and intact in the 2026-06-30 RFC (EXP-S05 Decision-6 table; contact fetch/field-resolution algorithm; FE /v1 base-path rule; field_properties {data,pagination} envelope; chunk-1 file naming).
Newly resolved in R2REV-5, REV-11REV-5 rate-limit upgraded from "fixed-as-decision" to a decisive, grounded v1 design with a fallback (OQ-14, chunk 9). REV-11 success-banner GMT-timezone copy added (§2.C/§3.C; 2026-06-30 edit).
Still open (carried)REV-7, REV-8, REV-9, REV-10EXP-S07 durable persistence (OQ-15, PM sign-off); NotificationPublisher signature + notif timeout/retry (OQ-10/REV-8); OSS cross-border residency (OQ-16); OSS quota (OQ-4, infra). None regressed; none newly closed.
Newly found in R2 (v2.6 scope)REV-12, REV-13, REV-14, REV-15, REV-16, REV-17EXP-S08 not buildable / cross-team (OQ-17, major); first_10k_sorted _id tie-breaker + get-by-IDs batch net-new (OQ-18, minor); FE MAX_SELECTION=500 vs 10K export cap (OQ-19, minor); EXP-S09 timezone value format ambiguity + net-new BE tz formatting (OQ-20, major); source enum gap Advertisement/Email (OQ-21, minor); 2 edited mermaid blocks not machine-validated post-edit (minor).
Score movement8.0 → 8.5Two R1 majors/blocker closed + banner fixed + 4 new grounded ADRs + zero dangling decisions, net of broadened (but defaulted, chunk-scoped) v2.6 confirm-surface.

Mermaid validity: 10 / 10 blocks pass a rule-based pitfall scan (no ; in sequence text; no </>/>= in stateDiagram-v2 transition labels; the only slash-in-[…] cases are a mid-label slash and a quoted parallelogram — both safe). The automated mmdc parser could not run in this sandbox (Chromium launch blocked — exit 1, empty stderr); 8 blocks were machine-validated at R1 and the 2 edited blocks (branch/skip flowchart + happy-path sequence) pass the manual scan — a mmdc/Docusaurus render is the pre-merge TODO the RFC comment log already records (REV-17).


Quick Verdict

Why this RFC can be implemented agentically:

  • §4.D Agent Execution Plan now gives 21 ordered chunks with exact files, repo-sourced commands (make test, pnpm test -- features/customers/export), and a verifiable "done when" per chunk; §2.4 + §3.B pin the full API + error contract (now incl. EXPORT_SELECTION_MODE_INVALID and the layouts/default reuse); §2.D/§2.E/§2.F pin transactions, collisions, retry/DLQ/idempotency.
  • Decisions are closed, not deferred: Detail 1.B lists 17 decisions each with chosen option, rejected alternatives, and reversibility; the v2.6 additions (Decision 9 first_10k_sorted, Decision 10 export-history, Decision 11 panel) are grounded against SearchContacts/SortBy, hub-chat/IAG billing, and the existing ExportCustomerDrawer.vue.
  • The two R1 implementability gaps (rate-limit mechanism, custom-field resolution) are now specified; an agent can build chunks 1–9 (BE) and 11–16 (FE) without a clarification meeting.

Why this RFC will still cause agent guessing or rework:

  • EXP-S08 (export-history) is not buildable today (OQ-17): the chat.qontak.com/reports/export surface is hub-chat → IAG billing (/report/v1/billings/logs/export), billing-only quota types, no CDP "Customer Data" type and no service-to-service register path. The RFC isolates this behind a non-fatal interface and gates chunk 19, but an agent cannot complete EXP-S08 until billing/IAG + Chat/Omni deliver the path.
  • EXP-S09 timezone formatting (OQ-20) is under-pinned for the implementer: the ExportContactRequest.Timezone example carries a label string ("(GMT+07:00) Asia/Jakarta") but Go's time.LoadLocation needs an IANA id ("Asia/Jakarta"); the BE timezone-aware timestamp formatter is "net-new" with no parse contract. An agent building chunk 18/21 would guess the parse + which cells are tz-rendered.

Findings Ledger (carry-forward)

Stable, never-renumbered ids (REV-n). Cycles: R1 = 2026-06-18 (PRD-v2.2-era RFC), R2 = 2026-06-30 (PRD-v2.6 RFC). "Was this captured before?" = the id exists here; "did we improve?" = open → fixed transitions across cycles.

IDSeverityFinding (one line)RFC locationStatusFirst seenResolved inEvidence / fix
REV-1blockerEXP-S05 per-type formatting unimplementable: no per-scenario table; number/currency/percentage all share field_type="number"; date omittedDecision 6, §2.1 ERfixedR1R1Decision 6 formatting table (10 repo types + number subtypes + date + default → XLSX/CSV); type source corrected to field_properties.Validation.Number.NumberType + CustomField.{Value,CurrencyCode}. R2: intact.
REV-2majorCustom-field value resolution + bulk-fetch by IDs unspecified (no GetByIDs; default-vs-custom split missing)§2.4 fetch algorithm, §2.H, chunk 8/9fixedR1R1§2.4 "Contact fetch & field-resolution algorithm" (SearchWithFilters + _id $in + pagination; default → top-level struct, custom → CustomFields[].Key). R2: intact + extended for selection_mode.
REV-3majorFE export path double-prefixes /iag → 404§1 Assumptions, §2.B, §2.HfixedR1R1FE paths use /v1/contacts/export + /v1/contacts/field_properties; backend stays /iag/v1/...; §2.B base-path rule. R2: intact.
REV-4majorfield_properties response shape wrong (key/group don't exist)§2.4 row 3, §2.GfixedR1R1{data,pagination} envelope of FieldPropertiesSerializer; name (not key); group derived from is_default/type; FE pages. R2: intact.
REV-5majorRate-limit "5/company/hour" unenforceable: middleware is one global per-company window (default 1/60s) shared with import§3 Security, OQ-14, chunk 9, §4.Bfixed (decision)R1R2R2 upgrade: OQ-14 is now a decisive v1 design grounded in rate_limit_middleware.go:16-55 — extend IRateLimiterService+middleware with an additive per-route (key,max,window) = ("export",5,3600), distinct counter; fallback = inherit shared + amend PRD §5. No longer dangling; chunk-9 AC asserts the chosen limit. Implementation still in chunk 9.
REV-6minorChunk 1 named a directory, not a file, for constants§4.D chunk 1fixedR1R1internal/app/service/export_contact.go (+ ExportContactJobName beside existing job consts). R2: intact.
REV-7majorEXP-S07 durable per-user persistence deferred (v1 = FE localStorage); PRD marks all EXP-S07 ACs "Must Have"§2.B, Detail 1.C, OQ-15open (accepted-risk, PM sign-off)R1v1 ships localStorage. R2 update: AC-1 Timezone now covered via the EXP-S09 panel; Associations still not persisted (§2.B note added — no grounded "associations" field group in repo); AC-4 cross-device scope still deferred. Tracked OQ-15; needs PM sign-off before GA.
REV-8majorNotificationPublisher Go interface signature + notif HTTP timeout/retry budget not pinned; OQ-10 channel unconfirmedDecision 7, chunk 7, §4.Bopen (partial)R1Interface isolation + non-fatal policy present; config keys pinned (§4.B). R2: unchanged — the explicit Go signature Publish(ctx, ExportNotification) error and a timeout/retry number are still not written; OQ-10 channel still cross-team.
REV-9minorOSS bucket region / cross-border data-residency for the PII export body not addressed§3.D, OQ-16openR1InfoSec to confirm OSS region vs UU PDP cross-border rules. R2: unchanged (OQ-16).
REV-10minorOSS quota/headroom for export volumes unconfirmed§1 Assumptions, OQ-4open (infra, Stage-0)R1CDP Infra to confirm ~5–15 MB/file at 48 h before Stage 0. R2: unchanged (OQ-4).
REV-11minorSuccess-banner copy dropped the PRD AC-2 "Data generated in GMT (+07:00) timezone" sentence§2.C, §3.C, sequencefixedR1R22026-06-30 edit: banner now reads "Customer download started — your data is generated in {timezone} (default GMT +07:00). Please check your email at {email}…", {timezone} interpolating the EXP-S09 selection. Was an R1 traceability Partial + OQ-11.
REV-12majorEXP-S08 export-history reuse not buildable today — depends on a net-new cross-team s2s path on the IAG billing surface (no CDP quota type, UI-only row creation)Decision 10, §2.4 #6, §2.F.1 step 4b, OQ-17, chunk 19open (cross-team; mitigated)R2Grounded against hub-chat + IAG /report/v1/billings/logs/export. Isolated behind a non-fatal ExportHistoryRegistrar iface; chunk 19 blocked on OQ-17. PRD Should-Have → does not gate the core release.
REV-13minorfirst_10k_sorted needs a net-new deterministic _id tie-breaker + a get-by-IDs/criteria batch (no $in-by-_id today)Decision 9, §2.4 resolution, OQ-18, chunk 9/20open (decide in chunk 9; mostly resolved)R2Decision 9 specifies the resolution via SearchContacts+SortBy (handler already defaults created_at desc) + added _id secondary sort, limit 10K. Remaining: confirm the _id tie-breaker + add the batch path. Low risk.
REV-14minorFE MAX_SELECTION = 500 conflicts with the 10,000 export cap (net-new FE work)Decision 9, §2.I, OQ-19, chunk 20open (decide in chunk 20)R2ListPage.vue:220 caps list selection at 500; explicit-ID export needs up to 10K and the shortcut needs the criterion path. Default: raise/replace MAX_SELECTION for the export flow only; prefer the criterion over collecting 10K IDs client-side.
REV-15majorEXP-S09 timezone value format ambiguous (label "(GMT+07:00) Asia/Jakarta" vs IANA "Asia/Jakarta") + BE timezone-aware timestamp formatting is net-new with no parse contractDecision 11, §2.4 struct, §2.A.1, OQ-20, chunks 18/21open (confirm before chunk 18/21)R2The Timezone example carries a display label; time.LoadLocation needs IANA. RFC flags OQ-20 ("IANA vs label") but the formatter contract (parse path + which cells are tz-rendered) is unspecified — agent would guess in chunk 18.
REV-16minorEXP-S09 Source filter enum gap — PRD lists "Advertisement"/"Email" but FE sources.ts + contact Source lack themDecision 11, §2.A.1, OQ-21, chunk 21open (confirm before chunk 21)R2Default: reconcile the vocabulary (extend the enum or scope the panel's Source options to existing channels) before build.
REV-17minorThe 2 mermaid blocks edited for v2.6/banner (branch-skip flowchart, happy-path sequence) were not machine-validated post-edit§2.1 branch/skip, §2.2 happy-path; Comment logopen (pre-merge TODO)R2Both pass a rule-based pitfall scan; mmdc was unavailable at R2 (Chromium launch blocked) and at the 2026-06-26 edit. Run mmdc/Docusaurus render before publish — already noted in the RFC comment log.

Ledger summary: 17 findings — 7 fixed (REV-1..6 + REV-11), 10 open (REV-7/8 major-but-mitigated, REV-9/10/13/14/16/17 minor, REV-12/15 major-new). All open material findings are promoted to the RFC Open-Questions table: REV-7→OQ-15, REV-8→OQ-10, REV-9→OQ-16, REV-10→OQ-4, REV-12→OQ-17, REV-13→OQ-18, REV-14→OQ-19, REV-15→OQ-20, REV-16→OQ-21; REV-17 is tracked in the RFC Comment log (pre-merge mmdc TODO).


PRD → RFC Traceability Matrix

Evidence normalization applied: §6 Comment logs were excluded from scoring evidence as non-authoritative discussion context. Where the comment log references fixes, those fixes are independently present in the authoritative body and were scored from there.

Standard format (PRD v2.6 — 9 stories)

PRD ElementRFC SectionCoverage
EXP-S01 Export manually selected§2.A button + §2.4 row 1 + Detail 1.CFull
EXP-S02 Export from filtered result (+ "Select all first 10,000" shortcut)§2.A + Decision 9 + §2.4 selection_mode resolutionFull — shortcut gated on total ≥ 10,000; sends first_10k_sorted criterion not IDs; _id tie-breaker finalised in chunk 9 (OQ-18, REV-13)
EXP-S03 Configure fields by layout§2.A field groups + §2.4 field_properties envelope + §2.BFull
EXP-S04 Receive email + in-app notification§2.F (3 email methods) + §2.4 #5 + §2.F.2Full
EXP-S05 XLSX/CSV formatting by typeDecision 6 + §4.D chunks 4–5 (24 table cases)Full
EXP-S06-NEG guard rails§3 Role × Endpoint + §3.B (403/422/403 FLAG_DISABLED)Full
EXP-S07 Persist last export config (AC-1..AC-9)§2.B localStorage + §2.A.1 panel persist + Detail 1.CPartial — v1 = FE localStorage (+ panel timezone/period/source); durable per-user store (PRD D-8) + AC-4 cross-device + AC-1 Associations deferred (OQ-15, REV-7)
EXP-S08 View export history (company/org)Decision 10 + §2.4 #6 + §2.F.1 step 4bPartial — specified behind a non-fatal ExportHistoryRegistrar iface but not buildable: no CDP quota type / no s2s register path on the IAG billing surface (OQ-17, REV-12). PRD Should-Have
EXP-S09 Download-all via right-side panel§2.A.1 + Decision 11 + §2.4 struct (timezone/period/source[])Full (with confirms) — extend ExportCustomerDrawer.vue; Timezone+Layout gate Download; Period/Source server-side; tz value format (OQ-20, REV-15) + source enum (OQ-21, REV-16) to confirm before chunks 18/21
PRD §5 rate limit "5/company/hour"§3 Security + §4.B + OQ-14Full (as decision) — decisive v1 design (per-route param) grounded in rate_limit_middleware.go; implementation in chunk 9 (REV-5)
PRD §5 selection cap 10,000 (FE + API)Decision 12 + §2.4 422 EXPORT_LIMIT_EXCEEDED + §2.C tooltip; FE MAX_SELECTION raise (OQ-19)Full
PRD §5.1 data lifecycle (status 7d / file 48h / audit 90d)§2.3 per-status lifecycle + §3.DFull
AC-1 success banner copy ("Data generated in GMT (+07:00) timezone")§2.C / §3.C export.startedFull — banner now carries the GMT-timezone sentence, {timezone}-parameterised (REV-11 fixed R2)
PRD §7 #1–#6 API behavior§2.4 (4 endpoints incl. export-history register) + §2.FFull (export-history #6 gated OQ-17)
PRD §8.1 system flow + diagram§2.2 sequence (happy + 2 failure paths)Full
PRD §10 observability (8 events)§3 Monitoring — names preserved exactly (incl. cdp_export_history_registered)Full
PRD §11 / §12 metrics + stage gates§1 Success Criteria + §3 SLO + §4 RolloutFull
PRD §13 dependencies§1 Dependencies (+ v2.6 deps) + §2.F.1Full
RFC decision with no PRD driverAssessment
GET /iag/v1/contacts/export/status/{job_id}Self-flagged new-with-justification (out-of-band status; v1 FE doesn't poll). Justified.
CSV formula-injection guard (= + - @)Sound security addition (§3 Security). Justified.

Summary: of 9 PRD stories — 7 Full (S01–S06, S09-with-confirms), 2 Partial (S07 durable persistence, S08 not-buildable). Supporting PRD elements all Full; the R1 banner-copy and rate-limit Partials are now closed. Bidirectional matrices (Detail 1.A forward + reverse) cover all 9 stories; PRD §-coverage table present.


Scorecard

Full-Stack Scorecard (18 categories) — R2

#CategorySourceR1R2Evidence-Based Rationale (R2)
1PRT — PRD TraceabilityMerged8.59.0Detail 1.A forward+reverse covers all 9 stories; PRD-to-Schema maps every entity/rule → field + endpoint + enforcement. R1 Partials closed (banner, rate-limit, AC-1 timezone); only EXP-S07 durable + EXP-S08 buildability remain Partial.
2TDC — Technical DecisionsMerged8.59.0Detail 1.B = 17 decisions, each chosen option + rejected alternatives + reversibility; Decisions 1–11 in ADR format. Zero dangling (OQ-14 now decided). Docked: Decision 7 (notif channel) is a default-pending-confirm (OQ-10).
3CNT — Contract Specificity (FE)FE8.08.5ExportCustomerPageProps TS interface; §2.A.1 panel control table (required/default/maps-to-request); §2.B fetch + localStorage; §2.G request body + casing. Docked: field-mapping transform still prose ("mirror DownloadTemplateModal").
4SCB — Scope Boundaries (FE)FE9.09.0§2.I enumerates FE create/modify/NOT-touched + shared read-only; chunks 11–16, 20–21 name exact files incl. ExportCustomerDrawer.vue.
5DEP — Dependencies (FE)FE8.58.5§1 table: each dep with availability + path:line + blocking flag; v2.6 deps added (SearchContacts, FilterCheckbox/InputPeriod/timezones.ts, default-layout, ExportCustomerDrawer, hub-chat history). The hub-chat dep is a YES-confirm blocker (OQ-17).
6NFS — Non-Functional Specificity (FE)FE7.07.0§3 endpoint p99 < 300ms, worker < 10min; §3.E a11y. Docked (unchanged): no bundle-size budget, no browser-matrix versions, no LCP/INP/CLS. This single 7.0 is what blocks the 9.0+ overall tier.
7TPS — Test Plan Specificity (FE)FE8.08.0§4.C maps layer→command→proof incl. first_10k_sorted, panel (Timezone/Layout gate, Period/Source, Retry), tz formatting. Docked: no named a11y/axe automation.
8DMS — Data Model & Schema (BE)BE8.08.0§2.3 extends bulk_upload_jobs field-by-field (+failure_reason), erDiagram, example row, PII per field, retention per status. Docked (unchanged): index still "optional" (ship/no-ship ambiguous); failure_reason/list_failed_rows shapes thin.
9ACV — API Contract & Versioning (BE)BE8.59.0§2.4: 4 endpoints, method/path/auth/request/response/codes/idempotency; the field-resolution algorithm + selection_mode resolution; field_properties now a documented envelope (grounded serializer.go:7-32); EXPORT_SELECTION_MODE_INVALID added; example payloads for all 3 selection paths.
10DIC — Data Integrity & Consistency (BE)BE8.58.5§2.D per-write-path transaction scope, idempotency (job_id + terminal-status guard), email single-fire; export-history register added as a non-fatal side effect (consistent policy). Crash-window-between-email-and-status-write caveat persists.
11FMC — Failure Mode CoverageMerged8.58.5§2.C UI matrix (+ panel Retry state) + §3.C i18n catalog; §2.F retry/DLQ/poison; §3.A FE codes = BE codes (incl. EXPORT_SELECTION_MODE_INVALID). Docked: notif/OSS timeout + circuit-breaker values still unpinned (REV-8).
12CSS — Concurrency & Scaling (BE)BE6.57.5Up: the rate-limit mechanism is now a decided, grounded per-route design (REV-5); §2.E collision map intact; first_10k_sorted adds a bounded 10K-row server-side resolution. Docked: worker pool count still "default" (no number); concurrency QA item (5 simultaneous) flagged not resolved.
13SAS — Security & Authorization (BE)BE8.58.5§3 threat model (5 vectors), IAG JWT + permission key, company-scope on every query + OSS path, cross-company rejection, CSV formula guard, SSRF analysis, secrets via config/load.go.
14ROL — Rollout & Rollback (merged)Merged8.58.5BE-first deploy order with rationale; §4.A matrix covers all 6 scenarios; flag default OFF + mobile flag; new config keys (notif, export-history, default timezone) in §4.B. No cap triggered.
15OBS — Observability (merged)Merged8.08.08 Mixpanel events (+ cdp_export_history_registered), names exact; slog fields; 5 alert thresholds → #cdp-ops; SLOs. Docked (unchanged): no distributed trace FE→API→worker.
16SBC — Service Boundary & Coupling (BE)BE8.58.5§2 per-service diagram + table; notif call sync HTTP non-fatal behind interface; export-history registrar s2s behind ExportHistoryRegistrar iface, non-fatal, owner named (OQ-17).
17CPA — Pattern Alignment (merged)Merged9.09.0§2.0 Patterns table maps each concern → repo file → deviation; v2.6 components (extend ExportCustomerDrawer, reuse FilterCheckbox/InputPeriod) all grounded; snake↔camel boundary explicit. Strongest category.
18CDG — Compliance & Data GovernanceBE8.08.0Triggered. §3.D classification/legal basis/retention/encryption/audit/right-to-delete. Docked (unchanged): OSS cross-border residency not addressed (OQ-16); right-to-delete relies on file TTL.

Overall: 8.5 — Agentic-Ready. No score cap triggered. The 9.0+ tier is gated only by NFS 7.0 (< 7.5); all other 9.0 conditions (ACV/DIC/FMC/CNT ≥ 8.5, zero dangling decisions, no cross-layer mismatch) are met.

Resource & Cost Advisory (non-blocking)

  • §4.F: +M worker pods bounded by 5/h/company; few hundred status docs/day + one index; OSS ~5–15 MB/job auto-expiring 48h → near-zero steady-state growth; no new infra. OQ-4 (OSS quota) routed to CDP Infra as a Stage-0 gate. Adequate advisory note.

Decision Closure Assessment (R2)

Decision Index

#DecisionStatusCritical Gaps
1XLSX + CSV in v1 (stdlib encoding/csv)Resolvednone
2Async via gocraft/workResolvednone
3Reuse bulk_upload_jobs + job_type discriminatorResolvedbackfill conditional ("only if") — agent audits callers
4New GenerateAndUploadExcelWithData()Resolvednone
5Buffer → temp *os.File → email → deleteResolvedcrashed-pod temp-file reclamation relies on OS reaping (acknowledged)
6Type-aware serializer + CSV RFC-4180Resolvedcustom-field value resolution now specified (REV-2)
7Notif publish via HTTP heimdall (default)Partialchannel HTTP-vs-Kafka unconfirmed (OQ-10); NotificationPublisher Go signature + timeout/retry still unwritten (REV-8); mitigated by interface + non-fatal policy
8Endpoint namespace /iag/v1/contacts/exportResolvednone (grounded PRD correction)
9first_10k_sorted server-side criterion (not 10K IDs)Resolvednet-new _id tie-breaker + get-by-IDs batch to add in chunk 9 (OQ-18, REV-13)
10Reuse chat.qontak.com/reports/export (register s2s)Resolved-with-blocked-dependencythe decision is made (iface, non-fatal, ship-without) but the path does not exist — needs cross-team work (OQ-17, REV-12); chunk 19 blocked
11Extend ExportCustomerDrawer.vue; BE timezone formatting net-newPartialtimezone value format (label vs IANA) + tz formatter contract unspecified (OQ-20, REV-15); source enum gap (OQ-21, REV-16)
12Cap at both FE + APIResolvedFE MAX_SELECTION=500 raise needed (OQ-19, REV-14)
13Notification job non-fatalResolvednone
14OQ-14 per-route rate-limit (extend middleware)Resolved (was Dangling at R1)implementation in chunk 9; fallback documented
15(= Decision 10 export-history)see #10
16(= Decision 11 panel)see #11
17Export timestamp timezone (BE renders in request tz)Partialparse contract / which cells rendered unspecified (OQ-20, REV-15)

Aggregate: of the 17 decisions, 13 Resolved, 3 Partial (Decision 7 notif channel, Decision 11/17 timezone formatting), 1 Resolved-with-blocked-dependency (Decision 10 export-history), 0 Dangling. The R1 Dangling item (OQ-14) is now resolved. No decision blocks chunks 1–9 (BE core) or 11–16 (FE core); the Partials gate chunks 7, 18, 19, 21.


Decision: 7 — Notification publish channel (HTTP heimdall vs Kafka) — Partial (carried, unchanged)

What was decided Option A (heimdall HTTP POST /notif/v1/notifications) as the default, pending One Notification team confirmation (OQ-10); non-fatal (Decision 13). Still missing (REV-8): (a) confirmed ingest channel; (b) the NotificationPublisher Go signature (type NotificationPublisher interface { Publish(ctx, ExportNotification) error }); (c) HTTP timeout + retry budget on the notif POST. Suggested resolution: write the interface signature + a timeout (mirror iag_mekari.go WithHTTPTimeout) + single retry into the RFC; confirm OQ-10 before chunk 7. Reversible behind the interface.

Decision: 10 — Reuse chat.qontak.com/reports/export for export history — Resolved-with-blocked-dependency (new R2)

What was decided Register each completed export into the existing surface via an ExportHistoryRegistrar interface, non-fatal, gated on OQ-17; ship the pipeline without it. Grounding (strong, honest): the surface is hub-chat → IAG billing /report/v1/billings/logs/export; billing-only quota types; rows created only by the hub-chat UI; no CDP quota type and no s2s register path today (verified, §2.0 source table). Why this is not Dangling but is blocked: the RFC chooses the path and isolates it; the cross-team prerequisite (billing/IAG adds a CDP quota_type + an s2s create accepting a CDP file/link + company/org visibility) does not exist. EXP-S08 is a Should-Have, so chunk 19 is blocked without gating the release. Open question: OQ-17 (binding for EXP-S08 only).

Decision: 11 / 17 — EXP-S09 panel + BE timezone formatting — Partial (new R2)

What was decided Extend the existing ExportCustomerDrawer.vue; reuse FilterCheckbox/InputPeriod/timezones.ts; resolve the set server-side from Period/Source; BE renders timestamps in the request timezone (net-new). Missing (REV-15/REV-16): (a) the timezone value format — the struct example uses a display label "(GMT+07:00) Asia/Jakarta" but time.LoadLocation needs IANA "Asia/Jakarta"; (b) which cells/fields are timezone-rendered and the formatter contract; (c) the Source enum reconciliation (PRD Advertisement/Email absent from sources.ts). Suggested resolution: pin the request timezone as an IANA id (map the FE label → IANA on send), name the tz-rendered fields (Decision 6 date row), and reconcile the source vocabulary — all before chunks 18/21.


Cross-Layer Contract Verification (R2)

EndpointBackend Response SchemaFrontend Expected SchemaMatch?Gaps
POST /iag/v1/contacts/export{job_id, status, email} snake{job_id, status, email}Yesbanner reads email; FE renders {timezone} from panel state / default (REV-11) — FE copy only, no BE field
GET .../export/status/{job_id}{job_id,status,total_records,success_count,failed_count}(v1 doesn't poll)Yes (partial use)FE ignores in v1 (no progress bar) — acceptable
GET .../field_properties?layout_id={data:[{name,name_alias,field_type,is_hidden,is_default,type,validation…}], pagination}{label,value,group,disabled} per checkboxPartialFE maps name_alias→label, name→value, derives group, pages the envelope — in-page mapping, no contract change
GET .../layouts/defaultdefault layout objectpre-selected "Default view"Yesreused for EXP-S09 auto-fill
Request body (panel){…, timezone, period/start_date/end_date, source[]}samePartialtimezone value format (label vs IANA) unresolved (OQ-20, REV-15); source[] enum gap (OQ-21, REV-16)

Mismatches found: 0 hard contract mismatches. The field_properties partial is resolved by the in-page FE mapping (no 6.5 cap). The panel timezone/source items are value-format / vocabulary gaps (confirm-before-chunk), not response-shape mismatches.


End-to-End Data Flow (R2)

EXP-S01/S02 (ids) | EXP-S02 shortcut (first_10k_sorted) | EXP-S09 panel (period/source/timezone)
→ FE: ListTable/Drawer → POST /v1/contacts/export {selection_mode, ids|criterion, layout_id, selected_fields, format, timezone?}
→ API: ExportHandler (flag/perm/cap/format/selection_mode/rate) → resolve set (ids in body | SearchContacts+SortBy+_id tie-breaker)
→ DB: insert bulk_upload_jobs {job_type:export, status:queued} → Enqueue ExportContactJobName → 200 {job_id, queued, email}
→ FE: success banner (with {timezone})
--- async ---
→ Worker: fetch contacts (SearchWithFilters + _id $in) → resolve default/custom fields → formatValue per type (tz-aware for date/timestamp)
→ serialize (excelize / encoding/csv) → temp *os.File → OSS PutObject + SignURL 172800
→ email → publish notification (origin=external_url) → register export-history row (non-fatal, OQ-17) → delete temp → status=completed

Gaps in flow (R2): (1) the timezone formatting step has no parse/render contract (REV-15); (2) the export-history register step targets a path that does not exist yet (REV-12). Everything else is traceable end-to-end from the RFC alone — the R1 custom-field-resolution gap is closed.


Strengths

  • Closed-loop decision discipline at scale: 17 ADR decisions, zero dangling, each with chosen option + rejected alternatives + reversibility; the v2.6 additions are as rigorously grounded as the original scope.
  • Honest cross-team grounding (the EXP-S08 treatment): rather than asserting "reuse for free," the RFC verifies the export-history surface is billing-owned with no s2s path, isolates it behind a non-fatal interface, and gates the chunk — exactly the right way to ship a Should-Have with a real dependency.
  • The R1 blocker is genuinely closed: rate-limit moved from "we could do X or Y" to a grounded, decided per-route middleware extension with a named fallback; an agent can now implement chunk 9.

Biggest Gaps

  • EXP-S08 not buildable (OQ-17, REV-12): the cross-team s2s register path on the IAG billing surface does not exist. Mitigated (interface + non-fatal + Should-Have) but a real dependency to drive with billing/IAG + Chat/Omni.
  • EXP-S09 timezone contract under-pinned (OQ-20, REV-15): label-vs-IANA ambiguity + an unspecified BE timezone formatter would make an agent guess in chunk 18/21.
  • Notification interface still unwritten (OQ-10, REV-8): the NotificationPublisher Go signature + notif timeout/retry remain unspecified — an agent would invent them in chunk 7.

Priority Actions (before merge / before the named chunk)

  1. OQ-20 / chunks 18 & 21 — pin the timezone contract. Specify the timezone request value as an IANA id (FE maps its label → IANA on send), name which cells are tz-rendered (Decision 6 date row), and define the formatter. Without this chunk 18 guesses.
  2. OQ-10 / chunk 7 — write the NotificationPublisher interface signature + notif HTTP timeout/retry, and confirm HTTP-vs-Kafka with the One Notification team.
  3. OQ-17 / chunk 19 — drive the export-history cross-team path (CDP quota type + s2s create + company/org visibility) with billing/IAG + Chat/Omni, or formally defer EXP-S08; the interface lets the core pipeline ship meanwhile.
  4. OQ-19 / chunk 20 + OQ-21 / chunk 21 — confirm the FE MAX_SELECTION raise for export and reconcile the Source enum (Advertisement/Email). Both have adopted defaults; just confirm.
  5. Pre-merge — run mmdc on the 2 edited mermaid blocks (REV-17) once a renderer is available.

Open Questions (mirror of the RFC §5 table — current)

#QuestionCategorySeverityMaps to
OQ-10Notif ingest HTTP or Kafka? + NotificationPublisher signature + timeout/retrySBC / FMCImportantREV-8
OQ-14Per-route rate-limit mechanism (decided: extend middleware; fallback: shared)CSSDecide in chunk 9REV-5 (resolved)
OQ-15EXP-S07 localStorage v1 vs durable per-user store (Timezone covered; Associations not)PRTSign-off before GAREV-7
OQ-16OSS bucket region / cross-border residency for PII export bodyCDGConfirm Stage 0REV-9
OQ-17EXP-S08 export-history: CDP quota type + s2s register path on IAG billingSBC / DMSBlocking (EXP-S08 only)REV-12
OQ-18first_10k_sorted _id tie-breaker + get-by-IDs batchTDC / DMSDecide in chunk 9REV-13
OQ-19FE MAX_SELECTION=500 vs 10,000 export capCNTDecide in chunk 20REV-14
OQ-20EXP-S09 timezone value format (label vs IANA) + tz formatter contractTDC / ACVConfirm before chunk 18/21REV-15
OQ-21EXP-S09 Source enum gap (Advertisement/Email)CNTConfirm before chunk 21REV-16
OQ-4OSS quota for private/exports/ (~5–15 MB/job, 48h)RCS (advisory)Stage-0 infra gateREV-10

Review History

One row per review cycle. Append, never overwrite — the audit trail of how the RFC improved (complementary to git history).

CycleDateReviewed RFC revisionScoreVerdictFindings (open → fixed)Notes
R12026-06-18last_updated: 2026-06-18 (working tree)8.0 — StrongPROCEED with notes10 raised → 6 fixed same-day (REV-1..6); 4 open (REV-7..10)Initial full review (full-stack rubric) + hostile cross-review against PRD v2.2. Scorecard was the pre-fix snapshot.
R22026-06-30last_updated: 2026-06-30 (working tree, uncommitted)8.5 — Agentic-ReadyPROCEEDfixed: REV-5, REV-11 (+ confirmed REV-1..4,6 intact); still open: REV-7,8,9,10; new: REV-12..17Delta re-review against PRD v2.6 + the 2026-06-30 banner fix. Rate-limit (REV-5) decided → CSS 6.5→7.5; banner (REV-11) fixed; 4 new ADRs (Decisions 9–11,14–17) → TDC/PRT/ACV up; zero dangling decisions. New v2.6 surface: EXP-S08 not-buildable/cross-team (OQ-17), timezone contract (OQ-20), source enum (OQ-21), _id tie-breaker (OQ-18), MAX_SELECTION (OQ-19). Mermaid 10/10 rule-based pass; mmdc unavailable in env → pre-merge render TODO (REV-17). Score 8.0→8.5; 9.0+ blocked only by NFS 7.0.