Skip to main content

RFC Review: Lite Seats — Phase 1 Lite User Role & Tiered Quota

Companion review for lite-seats.md, produced by the rfc-reviewer skill. Lives beside the RFC; valid only for the RFC revision in reviewed_rfc_last_updated (2026-06-23, working tree).

Executive Summary

  • Overall Score: 8.0/10
  • Rating: Strong
  • RFC Type: full-stack
  • Sub-Type: frontend = enhancement · backend = new-feature
  • Assessment Confidence: High — every BE anchor in §2.0 was independently re-verified against the real qontak-launchpad checkout (see Evidence Notes); contracts and flows are traceable end-to-end.
  • Applied Caps/Gates: None binding. All scored categories ≥ 5.0; no cross-layer contract mismatch; deploy order specified (BE-before-FE, §4.A). The 9.0+ gate is not met (three blocking external dependencies remain open and the Lite-config Figma frame is absent), which is the ceiling on the score — not a cap.
  • Implementation Readiness Verdict: PROCEED with notes for BE chunks 1–8 and FE chunk 10; HOLD on FE chunk 9 (counter + inbox grey-out, blocked on Figma) and on anything that consumes the three blocking external dependencies (Open Q1/Q2/Q3).
  • Report Path: bifrost/lite-seats/rfcs/lite-seats-review.md
  • RFC Author: pm-group.qontak@mekari.com | Reviewed: 2026-06-23

This RFC is, on the whole, agent-executable today for the bulk of its backend work. It is unusually well grounded: every "we reuse X" / "we extend Y" claim in the §2.0 Source Verification table was independently confirmed against the live code (count-based ValidateUserQuota, the SsoInvite-only Redis lock, the lock-less + transaction-less role-change paths, the QONTAKCHAT-User-* component parsing, the 422-vs-400 error constructors). The biggest strength is this code-grounding discipline plus a complete PRD→RFC traceability matrix that maps every story (LITE-S01…S06 + LITE-S01-NEG) to verifiable acceptance criteria and named files. The biggest gap is that three of the RFC's own dependencies are explicitly unresolved external blockers — the exact Lite ModPanel component codes (Open Q2), the lite_seats_enabled delivery mechanism + event schema (Open Q3), and the inbox permission_key blocklist (Open Q1) — each of which an agent cannot invent. The one thing that must change before full agentic execution: those three blocking inputs must be supplied as concrete values (component code strings, event schema, blocklist), because the affected chunks (1, 5, 6, and the inbox validator in 6/9) will otherwise be implemented against placeholders.


Quick Verdict

Why this RFC can be implemented agentically:

  • Every backend change is anchored to a named, verified file with the exact pattern to mirror (§2.0 Existing Code Anchors + Source Verification); the Agent Execution Plan (§4.D) is a 10-chunk ordered manifest with per-chunk files, commands, and acceptance criteria.
  • The hardest correctness traps are explicitly handled: count-model boundary bugs (same-bucket no-op LITE-S05/AC-2; CountUserByBucketExcluding for AC-3), error-code pinning (ErrUnprocessableEntity→422, not 400), and the fact that the role-change paths need new lock+tx wiring (not reuse).

Why this RFC will cause agent guessing or rework:

  • Three blocking external inputs are unresolved (Open Q1 inbox blocklist, Open Q2 exact Lite component code strings, Open Q3 flag event schema). An agent will hardcode placeholders (QONTAKCHAT-LiteUser-Initial, an empty/guessed blocklist) that are almost certainly wrong.
  • Open Q9 (role-change restructure: update.go is goroutine-based + non-transactional today) is the highest-risk chunk and is left as an open A-or-B decision — the agent must pick between refactoring Update vs routing through AssignRole, an architectural choice the author should close.

Findings Ledger (carry-forward)

Stable, never-renumbered finding ids. First review cycle (R1) — all findings are newly minted. Still-open material findings are promoted to the RFC Open-Questions surface by id.

IDSeverityFinding (one line)RFC locationStatusFirst seenResolved inEvidence / fix
REV-1blockerExact Lite ModPanel component code strings unconfirmed; GetBillingInfoBySsoID parse target is a placeholder§1 Dep table, A-2, §4.B, Open Q2openR1Author flags QONTAKCHAT-LiteUser-Initial/-Additional as TBD; agent cannot match unknown Code strings. Needs confirmed strings from ModPanel/Billing.
REV-2blockerlite_seats_enabled delivery mechanism + Kafka event schema unconfirmedA-3, §2.4, §2.F, Open Q3openR1Consumer (chunk 5) needs the event payload schema; design provisions for both event + company_features read but the event field shapes are undefined.
REV-3blockerInbox permission_key blocklist not defined; no inbox classification exists in repo§1 Dep table, ADR-6, §4.B, Open Q1openR1Verified: no inbox flag on permissions/permission_component_codes. Validator + FE disable (chunks 6, 9) need the actual key list; an empty constant silently passes all inbox keys.
REV-4majorRole-change restructure left as open A/B decision (Update refactor vs route through AssignRole)§2.0 contracts, ADR-5, Open Q9, chunk 7openR1update.go verified goroutine+non-tx; assign_role.go verified lock-less+tx-less. Author should pick one path so the agent does not choose architecture.
REV-5majorpermissions_crs is_inbox marker is asserted as additive but its producer is unspecified§2.4 (GET /permissions_crs extend), §2.GopenR1The FE relies on a new is_inbox/inbox marker to grey out keys, but how the BE derives that marker (same blocklist? a DB column?) is not specified — couples to REV-3 and needs a concrete derivation rule.
REV-6minorLite-config Figma frame absent → FE chunk 9 (counter + inbox grey-out) blocked§1 Design References, §5 item 7, chunk 9openR1All Lite UI marked "TBD — pending design" (PRD §7). BE + FE dropdown/error wiring are unblocked; only the counter/grey-out chunk is gated.
REV-7minorModPanel timeout/retry values for the quota read not quantified§3 Failure Mode Catalog, §2.EopenR1RFC says "fail-safe, 503 on ModPanel down" and reuses the existing heimdall client, but no explicit timeout/retry/circuit-breaker numbers are pinned for the Lite read path; inherits existing client config (acceptable, but unstated).
REV-8minorRedis lock failure path on role-change (lock held / not acquired) maps to 429 but UX for a role change 429 is not described FE-side§2.E, §2.4 role-change rowsopenR1Invite has the 429 path; the newly-locked role-change handlers will also emit ErrTooManyConcurrentRequests, but the FE error catalog (§3.C) does not list a 429 string for the edit-user flow.

Ledger summary: 8 open (3 blocker / 3 major... → actually 2 major after REV-5 weighting, see below; counted as: 3 blocker, 2 major, 3 minor). Precisely: 3 blocker (REV-1/2/3), 2 major (REV-4/5), 3 minor (REV-6/7/8). 0 fixed this cycle (first cycle). 0 accepted-risk. Still-open material findings (REV-1…REV-5, plus REV-6 as the FE design blocker) are promoted to the RFC Open-Questions surface by id.

Reconciliation with the RFC's own §6 Comment-log R1 self-review. The RFC author already ran an adversarial R1 self-review (§6, 2026-06-23) that corrected the lock/tx grounding (re-tagged role-change lock/tx as new wiring + restructure, added the shared-lock helper chunk 3b and the Update refactor as Open Q9), added the same-bucket no-op (LITE-S05/AC-2) and CountUserByBucketExcluding (AC-3), and pinned error codes to ErrUnprocessableEntity→422. This review confirms those corrections against the live code and does not re-raise them as new findings — they are already resolved in the reviewed revision. REV-4 tracks only the remaining open decision (the A/B choice in Open Q9), not the grounding correction itself, which is closed.


PRD → RFC Traceability Matrix

Standard format — PRD bifrost/lite-seats/prds/lite-seats.md (v1.2) exists and is linked.

PRD ElementRFC SectionCoverage
LITE-S01 (provision Lite role on activation)§2.2 provisioning seq · §2.F consumer · CreateDefaultCompanyRole · §2.3 row · chunk 5Full
LITE-S01/ERR-1 (retry back-fill, no dup)§2.F idempotency (offset-not-committed → redelivery)Full
LITE-S02 (configure ≤20 non-inbox perms)§2.A UI contract · §2.4 PATCH · role validator chunk 6 · FE chunk 9Partial — FE counter/grey-out blocked on Figma (REV-6); BE validator fully specified
LITE-S02/ERR-1,2,3 (21st block, inbox 422, >20 422)§2.A · §3.B error catalog · chunks 6/9Full (BE); FE counter blocked on design
LITE-S03 (strict standard quota)§2.1 branch flow · standard branch of evaluator · chunks 3/7Full
LITE-S04 (tiered Lite-first deduction)§2.1 · §2.2 · Lite branch evaluator · consumed_bucket · chunks 1–3,7Full
LITE-S04/ERR-2 (ModPanel down → 503 fail-safe)§2.2 failure seq · §3.AFull
LITE-S05 (re-eval + re-bucket on role change)§2.1 NB branch · ADR-5 · chunk 7 · Open Q9Partial — behavior fully specified; the implementation path (refactor Update vs route via AssignRole) is an open decision (REV-4)
LITE-S05/AC-2 (same-bucket no-op)§2.1 NOOP branch · chunk 7Full (added in R1 self-review)
LITE-S05/AC-3 (exclude-self count)CountUserByBucketExcluding §2.3 · chunk 7Full (added in R1 self-review)
LITE-S06 (read two Lite components)GetBillingInfoBySsoID extend · §2.4 · chunk 1Partial — parse logic specified; the component code strings to match are unconfirmed (REV-1)
LITE-S06/ERR-1 (missing components → lite_quota=0)§3.A · §3.A.1Full
LITE-S01-NEG (inbox guard rail)§2.A · §2.4 · §3.B 422 · chunks 6/9Partial — 422 reject specified; the inbox blocklist content is undefined (REV-3)
PRD §8 (API contracts deferred to RFC)§2.4 APIs — explicitly closes the PRD gapFull
PRD §10/§13 (rollout stages)§4 Rollout Strategy + §4.A matrixFull (technical mechanism; stage scheduling delegated to delivery)
PRD §11 (observability events)§3 Monitoring — events realized as slog/Datadog, not a busFull (with documented mapping)
PRD §12 (success metrics)§1 Success Criteria SC-1…SC-5Full
PRD §14 dep "deduct/refund API"ADR-2 reframes to read-only count-based (Open Q4)Full — divergence is explicit and justified, not a silent drop
PRD §16 Q5 (proactive fallback warning)§5 item 5 — default = surface bucket in success toastPartial — deferred to PM/Design, FE-only message

Summary: 13 of 18 PRD elements Full, 5 Partial (each Partial is gated by a named open question, not a silent gap), 0 Missing. 0 RFC decisions without PRD justification — every §2 ADR traces back to a PRD section via the Detail 1.A reverse table. The PRD's "deduction/refund" framing (§14, LITE-S04/S05) is the one place PRD and RFC diverge, and the RFC handles it correctly by explicitly reframing it (ADR-2 + the §1 architectural-clarification callout) rather than silently dropping it.


Scorecard

Full-Stack Scorecard (18 categories)

Merged categories cite both layers; weakest layer drags the score.

#CategorySourceScoreEvidence-Based Rationale
1PRT — PRD TraceabilityMerged9.0FE+BE: explicit bidirectional matrix (Detail 1.A forward+reverse), per-story change map (Detail 1.C) maps every LITE-S0x to files + verifiable ACs; PRD §-coverage table; the one PRD/RFC divergence (deduct/refund) is reconciled explicitly via ADR-2, not dropped.
2TDC — Technical DecisionsMerged8.0BE: 8 ADRs (Detail 1.B) each with chosen option + rejected alternatives + rationale tied to verified code. FE: representation/endpoint decisions resolved. Held below 9 because Open Q9 (chunk-7 restructure) is an explicit unresolved A/B decision (REV-4) and three inputs are external-blocked.
3CNT — Contract Specificity (FE)FE7.0§2.A UI contract + §2.B fetching strategy ($fetch/Kong, fetchPermissionsCrsById/fetchCompanyRoleList) + §2.C state matrix are concrete and grounded. Below 8 because the counter/grey-out component contract depends on the unspecified is_inbox marker derivation (REV-5) and the Figma frame is absent (REV-6).
4SCB — Scope Boundaries (FE)FE8.5Named files for every FE change (FeaturePermission.vue, permissionStore.ts, FieldRoles.vue, EditUsers.vue, InviteUsers.vue); §2.I + §1 Out-of-Scope explicit (no mobile, no qontak-billing, no new access level). Enhancement sub-type's highest-weight category — strong.
5DEP — Dependencies (FE)FE7.5Dependency table names owning team + blocking status for each; FE component lib pinned (@mekari/pixel3 v1.3.7, verified in package.json). Three deps are unresolved-but-flagged blockers (correct behavior; can't score 9 with open blockers).
6NFS — Non-Functional Specificity (FE)FE6.5Performance budget quantified (≤500ms quota p95, ≤1s invite p95, SC-4). a11y §3.E thin (relies on pixel3 defaults; no keyboard-flow/focus spec for the new counter interaction). No browser matrix, no bundle delta. Adequate for an enhancement reusing existing components, but not deep.
7TPS — Test Plan Specificity (FE)FE7.5§4.C + per-chunk acceptance criteria name vitest specs (counter increments, 21st blocked, inbox disabled) mirroring useRoles.spec.ts; BE go-test scenarios per branch. Integration-across-API-boundary tests are implied (mocked ModPanel) but no explicit FE↔BE contract test named.
8DMS — Data Model & Schema (BE)BE8.5§2.3 full DDL: consumed_bucket VARCHAR(16) NOT NULL DEFAULT 'standard' CHECK IN (...), partial index idx_users_company_bucket justified by the COUNT query, up/down migration, backfill-by-default. sqlc query list enumerated. Verified no such column exists today. No new tables (correct — Lite role is a row).
9ACV — API Contract & Versioning (BE)BE8.0§2.4 endpoint table: method/path/status/change/request/response per endpoint; §3.B error catalog with exact resp_desc.en + HTTP code; error→status mapping pinned (422 via ErrUnprocessableEntity, verified). Held below 9 by REV-1 (Lite component code strings) and REV-5 (is_inbox marker producer) leaving two response contracts under-pinned.
10DIC — Data Integrity & Consistency (BE)BE8.5§2.D integrity matrix + the count-model boundary corrections: same-bucket no-op (AC-2), CountUserByBucketExcluding (AC-3), single WithTx tx around role + bucket write, provisioning idempotency guard. The R1 self-review's tx/exclude-self fixes are the strongest evidence here and are verified against the lock-less/tx-less reality.
11FMC — Failure Mode CoverageMerged8.0BE: §3.A catalog (ModPanel-down 503 fail-safe, missing components → lite_quota=0, provisioning redelivery, tx rollback, 422 bypass) + §3.A.1 branch catalog. FE: §2.C state matrix + §3.C message catalog. Cross-layer: FE reads the exact resp_desc.en/customMessages shape the BE emits (verified). Below 8.5 by REV-7 (no pinned timeout/retry numbers) and REV-8 (no FE 429 string for role-change).
12CSS — Concurrency & Scaling (BE)BE8.0§2.E collision map names every shared-resource race + resolution: invite lock (existing, verified), role-change lock (new wiring, correctly flagged), provisioning idempotency, cache-vs-live-count. The headline insight — role change can over-provision exactly like invite but has no lock today — is correct and verified. QPS not quantified (acceptable, not a capacity RFC).
13SAS — Security & Authorization (BE)BE8.0§3 OWASP table: existing SsoAuth+PermissionCheck on all routes (verified), server-side ≤20/no-inbox validator (never trust UI), sqlc parameterized queries, fail-safe on ModPanel down, no-PII-at-Info logging. Ownership check inherits existing middleware. Strong for a new-feature reusing a hardened auth layer.
14ROL — Rollout & RollbackMerged8.0FE: flag-gated (lite_seats_enabled), §4.B config contract. BE: §2.3 migration is additive/reversible, §4.E rollback recipe (flag OFF → revert PR → drop column). Cross: §4.A compatibility matrix with all 4 BE/FE states, deploy order specified (BE before FE, migration first). No "No" in the matrix. Stage scheduling delegated to delivery (acceptable for a technical RFC).
15OBS — ObservabilityMerged7.5BE: §3 maps every PRD §11 event to a concrete slog field/span tag + the two PRD alerts (count>50/1h, p95>1s). FE: error toasts/banners. Honest correction that there is no event bus today (events → structured logs/Datadog). Below 8 because exact metric names are "follow the slog convention" rather than fully enumerated, and no distributed FE→BE trace correlation id is specified.
16SBC — Service Boundary & Coupling (BE)BE8.5§2.F.1 responsibility matrix + §2.0 contracts table: logic lives in launchpad (REST + new consumer pod), ModPanel read-only, no new sync cross-service calls, CRS sync unchanged. "What's new vs reused" is explicit per artifact.
17CPA — Pattern AlignmentMerged8.5BE: §2.0 Patterns-to-Follow table maps each concern to a reference file (sqlc, heimdall client, WithTx, NewErrorResponse, slog) and flags the one deliberate deviation (lock extracted to shared helper) with justification. FE: pixel3/Pinia/$fetch/vee-validate patterns mirrored; honest "no i18n, hardcoded EN" note. No silent new pattern.
18CDG — Compliance & Data Governance (BE)BEN/A§3.D: no new PII; consumed_bucket is non-PII; existing *_encrypted columns unchanged. No compliance trigger introduced by this RFC (it manipulates role/quota metadata, not personal data). Correctly marked N/A.

Resource & Cost Advisory (non-blocking)

  • §4.F: one cached ModPanel read + one indexed COUNT per invite/role-change (marginal on existing fleet), one VARCHAR(16) column + one partial index, one new lightweight Kafka consumer pod, no new external calls. Reasonable and non-blocking. Route the new consumer-pod footprint to infra planning; it does not affect readiness.

Decision Closure Assessment

Decision Index

#DecisionStatusCritical Gaps
ADR-1Lite User = company_roles row (user_access='lite'), not a new access levelResolvednone
ADR-2Count-based per-bucket quota (no Billing deduct/refund ledger)Resolveddepends on Billing confirmation (Open Q4) but the design is fully specified for the count-based path
ADR-3consumed_bucket stored, not inferredResolvednone
ADR-4Flag via Kafka consumer + synchronous company_features readPartialevent schema undefined (REV-2)
ADR-5Re-eval + re-bucket in tx under a shared lock newly applied to role-changePartialimplementation path (refactor Update vs route via AssignRole) is open (REV-4 / Open Q9)
ADR-6Inbox exclusion = code-constant blocklist on permission_keyPartialblocklist content undefined (REV-3); is_inbox marker derivation unspecified (REV-5)
ADR-720-key cap enforced at API (422) + FE counterResolvednone
ADR-8Extend existing endpoints; no new routesResolvednone

Aggregate: 5 of 8 decisions Resolved, 3 Partial (ADR-4, ADR-5, ADR-6), 0 Dangling.


Decision: ADR-1 — Lite User represented as a company_roles row

Status: Resolved

What was decided: Lite User is a company_roles row with user_access='lite', is_default=true, name='Lite User' (Detail 1.B, A-1).

Alternatives considered: new access-level enum value in extdata; dedicated lite_roles table — both rejected with specific reasons (PRD §5.3 forbids a new access level; a new table duplicates role/permission machinery and breaks FieldRoles.vue/AssignRole).

Grounding: company_roles (is_default, user_access, no is_system_role) verified in db/schema.sql; extdata/role_type.go enum verified to have no lite value. References CreateDefaultCompanyRole, GetCompanyRoleByDefaultAndUserAccess.

Interface specification: Fully specified — the row shape and the provisioning query are named.

Failure handling: "system role / non-deletable" enforced by a user_access='lite' delete guard (422), since no is_system_role column exists. The PRD models is_system_role=true; the RFC's substitution is flagged as Open Q6/§5 item 6 for confirmation.

Challenge results:

  • Scale: one row per CID — trivially scalable.
  • Reversibility: high — it is data, not schema; deleting the row reverts.
  • Consistency: consistent with ADR-7/ADR-8.
  • Agent implementability: yes — agent can write the provisioning insert from the named query.

Gaps and suggestions: The only soft spot is the is_system_role substitution; confirm the delete-guard approach is acceptable vs adding the column (already tracked as §5 item 6).


Decision: ADR-2 — Count-based per-bucket quota

Status: Resolved

What was decided: available = entitlement − COUNT(users in bucket); "deduct a Lite seat" = create user with consumed_bucket='lite'; "refund" = re-bucket so the COUNT recomputes. No per-seat deduction call to Billing.

Alternatives considered: explicit deduction/refund ledger calls to Billing — rejected because launchpad has no such call today (verified: validate_user_quota.go counts) and a ledger is Billing-owned, out of phase-1 scope.

Grounding: validate_user_quota.go independently re-verified — it is exactly CountUser(companyID) >= InitialCredit+AdditionalCredit. The reframing of the PRD's "deduct/refund" language is the single most important architectural clarification in the RFC and it is correct.

Interface specification: CountUserByBucket / CountUserByBucketExcluding queries specified (§2.3).

Failure handling: ModPanel-down → 503 fail-safe, no create.

Challenge results:

  • Scale: COUNT on idx_users_company_bucket (partial, company-scoped) is cheap.
  • Reversibility: high — no external ledger to unwind.
  • Consistency: consistent; resolves the PRD framing ambiguity.
  • Agent implementability: yes for the count path. The only residual is Open Q4 (Billing must confirm it does not require launchpad to report per-bucket consumption back) — a confirmation, not a design gap.

Gaps and suggestions: Close Open Q4 with Billing; if a deduct/refund API is later mandated, it is a clean follow-up.


Decision: ADR-4 — Flag delivery via Kafka consumer + synchronous company_features read

Status: Partial

What was decided: A consumer on bifrost.company.settings.events.v1 provisions the Lite role on activation; a synchronous GetCompanyFeatureByCode read gates enforcement at decision points.

Alternatives considered: poll Billing on every invite; FE-only flag — both rejected with reasons.

Grounding: topic verified (internal/kafka/topics.go); features/company_features tables exist but have no query layer (verified — only nav_items.sql matches "feature") so GetCompanyFeatureByCode is correctly tagged new.

Interface specification: The synchronous read query is specified. The event payload schema is not ({cid, feature, state} is a placeholder) — REV-2.

Failure handling: §2.F — DB write failure → offset not committed → redelivery back-fills; lazy-provisioning fallback if the flag arrives only as a company_features row.

Challenge results:

  • Agent implementability: the synchronous-read + lazy-provisioning path is implementable now; the event-driven consumer is not until the schema is supplied. The RFC's dual-path design is a good hedge.

Gaps and suggestions: Obtain the event schema (field names, types, the state enum values) from Billing/Platform — this is Open Q3 / REV-2. Until then, build the synchronous-read path and stub the consumer behind the confirmed schema.


Decision: ADR-5 — Re-eval + re-bucket in a transaction under a newly-applied shared lock

Status: Partial

What was decided: Role change re-evaluates quota and re-buckets, serialized under the same kind of Redis lock used by invite — extracted into a shared helper and newly applied to the role-change handlers; the role + consumed_bucket writes go in a single WithTx tx.

Alternatives considered: best-effort, no lock — rejected because a cross-bucket role change can over-provision under concurrency exactly like invite.

Grounding: Independently verifiedassign_role.go has no SetNx/BeginTx/WithTx/ValidateUserQuota (0 matches); update.go writes the role change in go func() goroutines with a sync.WaitGroup and no transaction; the Redis lock exists only in SsoInvite (handler lines 128/131). The RFC's R1 self-correction (re-tagging this as new wiring, not reuse) is accurate.

Interface specification: CountUserByBucketExcluding, the same-bucket no-op branch, and the shared-lock helper (chunk 3b) are specified.

Failure handling: tx rollback on quota-adjust failure (LITE-S05/ERR-2); revert on exhausted target (ERR-1).

Challenge results:

  • Reversibility: the restructure of update.go is the least reversible chunk (it rewrites a concurrent code path); rework cost is real.
  • Agent implementability: the agent must choose between (a) refactoring Update to be transactional/serialized and (b) routing FE role changes through AssignRole and hardening it. This A/B choice is left open (Open Q9). An agent will guess; the author should decide — REV-4.

Gaps and suggestions: Close Open Q9 by picking (a) or (b). Recommend (b) if Update's goroutine fan-out does other independent work that should not be serialized; recommend (a) if the role write is the only mutation, to avoid a second code path. State which.


Decision: ADR-6 — Inbox exclusion via code-constant blocklist

Status: Partial

What was decided: A code-constant blocklist keyed on permission_component_codes.permission_key, enforced at API (422) + FE disable.

Alternatives considered: DB column flag on permissions — rejected (a code constant is reviewable/versioned with the release).

Grounding: verified — no inbox classification exists today on permissions/permission_component_codes; the permission_key vocabulary (GetPermissionComponentCodesByKey, migration 20250812044119) is real and is the right key namespace.

Interface specification: The validator (count(enabled) ≤ 20 AND ∀ key ∉ blocklist) is specified. The blocklist content is undefined (Open Q1 / REV-3) and the is_inbox marker the FE consumes (added to GET /permissions_crs) has no specified derivation rule (REV-5).

Failure handling: 422 on bypass.

Challenge results:

  • Agent implementability: the validator skeleton is implementable; an empty/guessed blocklist silently passes every inbox key, defeating the guard rail and SC-2. The agent cannot invent the correct keys.

Gaps and suggestions: Supply the definitive inbox permission_key list (Open Q1) and specify how the permissions_crs is_inbox marker is computed from it (same constant, presumably) so FE and BE share one source of truth — REV-3 + REV-5.


UI State Audit

ComponentLoadingEmptyErrorPartialSuccessAssessment
Lite config panel (FeaturePermission.vue)defineddefineddefinedn/a (single source)defined4/4 applicable — §2.C: MpSkeleton / "0/20" / "Failed to load…retry" / saved toast
Invite (InviteUsers.vue + FieldRoles.vue)definedn/adefinedn/adefined3/3 applicable — submit spinner / quota banner / success toast
Edit user role (EditUsers.vue)definedn/adefined (revert + banner)n/adefined3/3 — but no explicit 429 (lock-contention) state for role change (REV-8)

Summary: All data-driven components have their applicable states defined via §2.C. The single gap is a missing FE 429/lock-contention state for the newly-locked role-change path.


Performance Budget Check

MetricTargetCurrent BaselineSourceAssessment
Quota eval latency≤ 500ms p95not statedPRD §6 / SC-4adequate target; baseline unstated (load test to validate, §5 item 8)
Invite/role-create≤ 1s p95not statedPRD §6 / SC-4adequate target
Bundle size deltanot statedNO FE BUNDLE BUDGET — acceptable: enhancement reuses existing pixel3 components, no new package

Accessibility Review

AspectSpecified?DetailsAssessment
Keyboard navigation flownorelies on pixel3 defaultsincomplete — new counter interaction (block 21st check) has no described keyboard behavior
Focus managementnonot addressed (no modal introduced)
ARIA labelspartial§3.E: disabled checkboxes expose tooltip text to SR (pixel3 default)adequate-by-reuse
Heading hierarchynonot addressed (reuses existing settings page)
Color contrastnogrey-out state contrast not verifiedincomplete — greyed inbox keys must still meet contrast for the disabled+tooltip pattern
Motion sensitivityn/ano animation introducedn/a
Screen reader behaviorpartialtooltip text exposedadequate-by-reuse

The a11y posture is "inherit pixel3 defaults," reasonable for an enhancement but the new counter/grey-out interaction deserves an explicit keyboard + contrast note once the Figma frame lands.


Pattern Alignment Check

PatternRFC ApproachAssessment
BE quota check (count vs entitlement)extends validate_user_quota.go per bucketfollows — verified
BE Redis lockextends — extracts SsoInvite lock to shared helper, applies to role-changedeliberate deviation, justified (the one flagged deviation)
BE transaction (WithTx)follows store.gofollows
BE sqlc / error envelope / slogfollows named reference filesfollows
FE Pinia / $fetch-Kong / vee-validate / pixel3follows existing composables + componentsfollows
FE i18nhardcoded EN (repo has no i18n)follows existing (honestly flagged)
Existing analytics eventsPRD §11 events realized as slog/Datadog (no bus today)preserved-by-mapping; honestly corrected

No silent parallel system. The single deviation (shared lock helper) is explicit and justified.


Data Integrity Deep-Dive

Write PathTransaction ScopePartial Failure BehaviorIdempotency KeyConsistency GuaranteeDuplicate Handling
Invite Lite/standard userBEGIN; CreateUser(consumed_bucket); InsertUserRole; COMMIT (§2.2)tx rollback; nothing created if any step failsRedis lock:user_invite:{cid} (10s) prevents concurrent over-provisionstrong (COUNT live from Postgres inside lock)lock + count-inside-lock
Role change re-bucketsingle WithTx tx: role write + UpdateUserConsumedBucket (chunk 7)rollback → role unchanged (LITE-S05/ERR-2)newly-applied shared lock (verified absent today)strong, under lockexclude-self count avoids double-count (AC-3)
Provisioning (consumer)single insert via CreateDefaultCompanyRoleoffset not committed → Kafka redelivery back-fillsGetCompanyRoleByDefaultAndUserAccess(cid,'lite') pre-checkeventual (event-driven)idempotency lookup before insert

Strong across all three write paths. The only residual risk is chunk-7 implementation choice (REV-4), not the integrity model itself.


Concurrency Collision Map

#Shared ResourceWritersCollision ScenarioResolution MechanismLock Failure BehaviorAssessment
1seat count per CID (invite)two concurrent invitesboth read last seat, both createexisting SetNx lock + count-inside-lock (verified)429 ErrTooManyConcurrentRequestsadequate — existing, unchanged
2seat count per CID (role change)two role changes / invite + role changeboth cross buckets, over-provisionnew shared lock applied to role-change handlers + WithTx429 — but no FE string for role-change 429 (REV-8)adequate BE; minor FE gap
3Lite role row per CIDduplicate Kafka eventstwo consumers provision twiceidempotency lookup before insertno-opadequate
4billing cache vs live countcache read vs new seatstale entitlemententitlement cached, COUNT always liven/aadequate

The standout (and verified) insight is collision #2: the RFC correctly identifies that role change has the same over-provision exposure as invite but no lock today.


API Contract Completeness Check

EndpointRequest SchemaResponse SchemaError TaxonomyAuth SpecIdempotencyExample PayloadsAssessment
POST /iag/v1/users/sso_invitecomplete (ApiUserInviteParam, unchanged)partial (200 user)complete (422/422/503 §3.B)inherited SsoAuth+PermissionCheck (verified)lock-basedpartial5/6
PATCH /iag/v1/company_roles/{id}complete ({name,user_access,permissions[],description})completecomplete (two 422s)inheritedn/apartial5/6
PUT /iag/v1/users/{user_id}/roles/{role_id}path paramspartialcomplete (422/500)inheritednew lockmissing4/6
PUT /iag/v1/users/{sso_id}{…,role_crs_id}partialcompleteinheritednew lockmissing4/6
PUT /iag/v1/company_roles/delete/{id}path paramn/acomplete (422 delete guard)inheritedn/an/acomplete
GET /iag/v1/billings/{company_id}/unified_billingpath parampartial — adds lite_initial_credit/lite_additional_credit but the source component codes unconfirmed (REV-1)n/ainheritedn/apartial3/6
GET /iag/v1/permissions_crspartial — adds is_inbox marker, derivation unspecified (REV-5)n/ainheritedno2/6

Contracts are mostly consumer-ready; the two soft endpoints (billing read, permissions_crs) are soft precisely because of the two blocking external inputs.


Async Job / Event Consumer Spec

Job/ConsumerTriggerInput ShapeRetry PolicyDLQConcurrency LimitIdempotency KeyTimeoutAssessment
LiteSeatsConsumerKafka bifrost.company.settings.events.v1, feature=lite_seats_enabled,state=ONspecified as placeholder {cid,feature,state} — schema TBD (REV-2)offset-not-committed → redeliverynot named (relies on Kafka redelivery, no explicit DLQ)not specifiedGetCompanyRoleByDefaultAndUserAccess(cid,'lite')not specified4/7 — idempotency strong; input schema, DLQ, concurrency, timeout open

The consumer's idempotency design is solid, but input schema (REV-2), DLQ, per-message timeout, and concurrency limit are unspecified. Flagged for FMC/DIC per the rubric.


Compliance Trigger Check

TriggerFound?Data LocationClassificationAssessment
PII (name/email/phone/...)no (new)existing users.*_encrypted unchangedn/aRFC adds no PII; §3.D explicit
Payment datanon/a
Health datanon/a
User content w/ retentionnon/a
Auth/session datanoreuses existing SSO/middlewaren/ano new auth data
Cross-border transfernon/a

CDG Status: N/A — no compliance triggers found. consumed_bucket is role/quota metadata, not personal data.


Cross-Layer Contract Verification

EndpointBackend Response SchemaFrontend Expected SchemaMatch?Gaps
invite/role-change errorresp_code + resp_desc.{id,en} (NewErrorResponse)error.value.data?.customMessages?.message || resp_desc?.en (useUsers.ts, verified)Yesnone — existing envelope reused
PATCH /company_roles/{id}{name,user_access,permissions[],description}useRoles.updateRole sends exactly thisYesnone
GET …/unified_billingsnake_case lite_initial_credit/lite_additional_creditbilling store reads snake_caseYes (shape)source component codes unconfirmed (REV-1) — shape aligns, values may be empty
GET /permissions_crsadds is_inbox markerFeaturePermission.vue reads marker for :is-disabledPartialadditive + back-compatible, but BE derivation of is_inbox unspecified (REV-5)

Checks performed:

  • Field name casing consistent (snake_case BE; FE reads snake_case directly — verified, no transform mismatch)
  • Nullability aligned (missing Lite components → lite_quota=0, FE handles standard-only)
  • Error response shape matched (verified FE reads exact BE envelope)
  • Pagination — n/a (no new list pagination)
  • Auth token format — inherited, unchanged

Mismatches found: 0 hard mismatches (no ROL cap triggered). Two additive contracts (REV-1, REV-5) are under-specified on value/derivation, not shape.


Cross-Layer Rollout Compatibility Matrix

ScenarioFrontendBackendWorks?Notes
Pre-deploy (baseline)OldOldYesCurrent state
Backend firstOldNew (flag OFF)Yesbyte-identical to today (SC-5); migration additive
Backend firstOldNew (flag ON)YesLite role exists; BE validators enforce ≤20/no-inbox; FE counter just absent
Frontend firstNewOldPartialFE flag composable reads a flag the old BE may not surface — but FE flag defaults OFF, so Lite UI stays hidden; safe
Both deployedNewNewYesTarget state
Backend rollbackNewOld (rolled back)Yescolumn additive/harmless; flag OFF reverts behavior
Frontend rollbackOld (rolled back)NewYesBE validators remain source of truth

Deploy order: Backend first (explicitly specified §4.A: BE before FE, migration first). Incompatible scenarios: 0. ROL is not capped.


End-to-End Data Flow

Flow: Invite a Lite User (Lite Quota available)

Admin clicks Invite (role=Lite User)
→ FE: InviteUsers.vue + FieldRoles.vue → useUsers.inviteUser ($fetch → Kong)
→ API: POST /iag/v1/users/sso_invite {RoleId=Lite, email, ...}
→ BE: user_handler.SsoInvite → SetNx lock:user_invite:{cid}
→ BillingService.EvaluateQuotaForRole(cid, role=lite)
→ Redis GET Package:Unified:{ext_cid} (cache) / ModPanel unified_subscription (miss)
→ DB COUNT(consumed_bucket='lite'), COUNT('standard') [excludes deleted/consultant]
→ bucket=lite (lite_used < lite_quota)
→ DB: BEGIN; CreateUser(consumed_bucket='lite'); InsertUserRole; COMMIT
→ Side effects: invitation email; slog invite_quota_decision{cid,role,bucket}; DEL lock
→ Response: 200 {user, role="Lite User"}
→ FE: store update → user list shows "Lite User"; success toast

Gaps in flow: none — fully traceable from §2.2 + §2.0. The only undetermined link is the ModPanel component-code match inside EvaluateQuotaForRole (REV-1).

Flow: Change role Lite → Member (re-bucket)

Admin changes role in EditUsers.vue → useUsers.updateUser → PUT /iag/v1/users/{sso_id} {role_crs_id}
→ BE: [chunk 7] acquire shared lock:user_invite:{cid}
→ compute new required bucket; if == consumed_bucket → NO-OP (save role only, AC-2)
→ else EvaluateQuotaForRole with CountUserByBucketExcluding(this user) [AC-3]
→ WithTx: UpdateUserConsumedBucket + role write; rollback on failure (ERR-2)
→ Response: 200 / 422 "Standard quota exhausted — cannot upgrade this user" (ERR-1)
→ FE: success toast / revert selection + banner

Gaps in flow: the BE handler that owns this is an open A/B decision — refactor Update (today goroutine+non-tx, verified) vs route through AssignRole (REV-4 / Open Q9). The flow is specified; the host code path is not chosen.


Agentic Readiness Deep-Dive

Vague Word Audit

#Word/PhraseLocationImpactConcrete Replacement
1{cid, feature, state} "schema TBD"§2.F consumeragent guesses event field names/typessupply the real Kafka event schema (REV-2)
2is_inbox marker (no derivation)§2.4 /permissions_crsagent guesses how BE computes the markerspecify: marker = permission_key ∈ inbox blocklist constant (REV-5)
3"metric/log names follow the slog convention"§3 OBSagent invents field namesenumerate the final metric/field names

Total material vague phrases in spec sections: 3 — each already tracked as an open question or finding. The RFC is otherwise notably free of hand-wave ("handle gracefully" does not appear; every error has a pinned code + string).

Dangling Alternatives

#AlternativesLocationImpact
1refactor Update or route through AssignRoleOpen Q9 / chunk 7 / ADR-5agent must choose the host code path for the highest-risk chunk (REV-4)

Total dangling alternatives: 1 (and it is explicitly surfaced as the highest-risk chunk, not buried).

Task Decomposition Assessment

ChunkAcceptance CriteriaAssessment
1–10 (§4.D Agent Execution Plan)per-chunk files + commands + acceptance criteria + deploy orderverifiable — this is a model task manifest; chunks 1/5/6/9 carry the blocking-dependency caveats inline

The RFC ships its own ordered, command-level execution plan with explicit chunk dependencies and a highest-risk-chunk callout (chunk 7). This is materially better than most RFCs at this stage.


Strengths

  • Code-grounding discipline (§2.0 Source Verification). Every "reuse/extend" claim is backed by a quoted code anchor, and this reviewer independently re-verified the load-bearing ones against the live qontak-launchpad checkout: count-based validate_user_quota.go, the SsoInvite-only Redis lock, the lock-less + go func()/non-tx role-change paths (assign_role.go, update.go), the QONTAKCHAT-User-* component parsing, and the ErrUnprocessableEntity→422 / ErrBadRequestCustomDesc→400 constructors. They all check out. This is the single biggest reason the RFC is agent-ready.
  • Complete, bidirectional PRD traceability (Detail 1.A + 1.C). Every PRD story LITE-S01…S06 + LITE-S01-NEG maps forward to files/endpoints and reverse to PRD ACs, with verifiable go-test/vitest acceptance criteria per chunk. The one PRD/RFC divergence (deduct/refund framing) is reconciled explicitly via ADR-2, not silently dropped.
  • Correctness traps pre-solved (DIC). The R1 self-review caught and fixed the count-model boundary bugs (same-bucket no-op LITE-S05/AC-2, exclude-self CountUserByBucketExcluding AC-3), the transaction boundary for re-bucketing, and the 422-vs-400 error-code mapping — exactly the things an agent would otherwise get wrong on the first pass.

Biggest Gaps

  • Three blocking external inputs cannot be invented by an agent (Open Q1/Q2/Q3 → REV-1/2/3). The exact Lite ModPanel component code strings (chunk 1, §2.4 billing read), the lite_seats_enabled Kafka event schema (chunk 5, §2.F), and the inbox permission_key blocklist (chunks 6/9, ADR-6) are all undefined. An agent will hardcode placeholders that are almost certainly wrong, and an empty inbox blocklist silently defeats the SC-2 guard rail.
  • Highest-risk chunk left as an open A/B decision (Open Q9 → REV-4). The role-change restructure must add a lock + transaction to code that verifiably has neither (update.go goroutine+non-tx, assign_role.go lock-less). The RFC correctly flags this but does not choose between refactoring Update and routing through AssignRole — an architecture decision an agent should not be making.
  • permissions_crs is_inbox marker has no producer rule (§2.4 → REV-5). The FE grey-out depends on a new BE marker whose derivation is unspecified; it is coupled to the (undefined) inbox blocklist and will block the FE inbox-disable behavior even once the Figma frame lands.

Priority Actions

Ordered by impact on implementation readiness.

  1. Open Q2 / REV-1 (billing read, chunk 1) — supply the confirmed Lite ModPanel component Code strings (the real analogs of QONTAKCHAT-User-Initial/-Additional). Without the exact strings, GetBillingInfoBySsoID matches nothing and lite_quota is always 0. This is a one-line input that unblocks the foundational chunk.
  2. Open Q1 + REV-5 (inbox guard, chunks 6/9) — produce the definitive inbox permission_key blocklist constant AND state that the permissions_crs is_inbox marker is computed as permission_key ∈ blocklist, so FE and BE share one source of truth. This unblocks the guard rail (SC-2) and the FE grey-out contract together.
  3. Open Q3 / REV-2 (consumer, chunk 5) — obtain the bifrost.company.settings.events.v1 event schema (field names, types, the state enum). Until then, build only the synchronous company_features read + lazy-provisioning path and stub the consumer behind the confirmed schema.
  4. Open Q9 / REV-4 (chunk 7) — decide (a) refactor Update to a single transactional, lock-serialized role+bucket write, or (b) route FE edit-user role changes through AssignRole and harden that. State the choice in ADR-5 so the agent does not pick the architecture for the highest-risk chunk.

Backend Contract Addendum

Endpoint Contract Details

EndpointMethod/PathAuthZRequest ContractResponse ContractError ContractIdempotency/VersioningStatus
InvitePOST /iag/v1/users/sso_inviteSsoAuth+PermissionCheck (verified)ApiUserInviteParam{RoleId,…} unchanged200 user + consumed_bucket set422/422/503 (§3.B)Redis lock (existing)Complete
Role configPATCH /iag/v1/company_roles/{id}same{name,user_access,permissions[],description}200422 >20, 422 inbox (§3.B)n/aComplete
Role changePUT /iag/v1/users/{user_id}/roles/{role_id} · PUT /iag/v1/users/{sso_id}samepath / {role_crs_id}200422 ERR-1, 500 ERR-2, (429 lock — FE string missing, REV-8)new shared lock + WithTxMissing [host-path A/B decision REV-4]
Role deletePUT /iag/v1/company_roles/delete/{id}samepathn/a422 delete guardn/aComplete
Billing readGET /iag/v1/billings/{company_id}/unified_billingsamepath200 + lite_initial_credit/lite_additional_creditn/aadditiveMissing [component code strings REV-1]
Permission treeGET /iag/v1/permissions_crssame200 + is_inbox markern/aadditiveMissing [marker derivation REV-5]

Database Changes Details

ChangeTable/EntityDDL / Shape DiffData Migration PlanRollback PlanCompatibility WindowStatus
add column + indexusersconsumed_bucket VARCHAR(16) NOT NULL DEFAULT 'standard' CHECK IN ('standard','lite'); partial idx_users_company_bucket (company_id, consumed_bucket) WHERE deleted_at IS NULLbackfill via DEFAULT (no Lite users pre-launch)down: DROP INDEX + DROP COLUMN (additive, harmless)column independent of flag; ships firstComplete
new queriesusers.sql / company_features.sqlCountUserByBucket, CountUserByBucketExcluding, UpdateUserConsumedBucket, GetCompanyFeatureByCodesqlc regeneraterevert query filen/aComplete
Lite role rowcompany_rolesdata only (no DDL)provisioned by consumer/lazydelete row (guarded)per-CIDComplete

Implementation Readiness Checklist

Unblocked (agent can proceed)

  • PRD → RFC traceability matrix complete (bidirectional, Detail 1.A/1.C)
  • Technical decisions resolved with alternatives rejected (5/8 Resolved; 3 Partial gated on named open questions)
  • Failure modes handled with error message catalog (§3.A/§3.B/§3.C)
  • Configuration contract (§4.B: flag, cap=20, component codes, blocklist)
  • Pattern alignment verified (§2.0 patterns table; one justified deviation)
  • Rollout plan with feature flag + rollback (§4.A/§4.E)
  • Observability mapping defined (§3, PRD §11 events → slog/Datadog)
  • Task decomposition with acceptance criteria per chunk (§4.D)
  • Schema at DDL precision (§2.3)
  • Transaction boundaries + idempotency per write path (§2.D, Data Integrity Deep-Dive)
  • Concurrency collision points listed with resolution (§2.E)
  • Security: auth boundaries, validation, injection, fail-safe (§3 OWASP)
  • Cross-layer contract verified — 0 hard mismatches
  • Deploy order specified (BE first, migration first)
  • Cross-layer rollout matrix — no unaddressed "No"
  • End-to-end data flow documented

Blocked (must fix first)

  • REV-1 — confirmed Lite ModPanel component code strings (chunk 1)
  • REV-3 — inbox permission_key blocklist content (chunks 6/9)
  • REV-2 — lite_seats_enabled event schema (chunk 5 event path)
  • REV-4 — choose chunk-7 host path (Update refactor vs AssignRole)
  • REV-5 — permissions_crs is_inbox marker derivation rule
  • REV-6 — Lite-config Figma frame (FE chunk 9 only)

Verdict: Fix 6 blockers first for FULL execution — but BE chunks 1 (shape only), 2, 3, 3b, 4, 7 (pending REV-4), 8 and FE chunk 10 can proceed now; chunks 1 (values), 5 (event path), 6 (validator content), 9 are gated.


Task Manifest

Verifying the RFC's own §4.D Agent Execution Plan — it is well-formed; reproduced with gating annotations.

OrderChunkFiles to Create/ModifyAcceptance CriteriaDependencies
1Lite entitlement readget_billing_info_by_sso_id.go, billing_response.goresponse carries Lite fields; missing → 0REV-1 (component code strings) for real values
2consumed_bucket migration + queriesdb/migration/*, db/query/users.sql, sqlccolumn+index exist, reversibleNone
3Tiered evaluator (+ typed 422)billings/*, user_handler.go, sso_invite.goall 3.A.1 branches; 422/503Chunk 1, 2
3bShared lock helperinternal/pkg/*invite unchanged; helper unit-testedNone
4Feature-flag readcompany_features.sql, service readerflag per CID; OFF→standardNone
5Provisioning consumerconsumer/lite_seats_provision.go, iConsumer.goprovisions one role; replay no-opREV-2 (event schema) for event path
6Role validator + delete guardrole_handler.go + service, blocklist constant422 on >20/inbox/delete-liteREV-3/REV-5 (blocklist + marker)
7Role-change re-bucket (restructure)assign_role.go, update.gore-bucket in tx; no-op; rollbackChunk 3, 3b; REV-4 (host path)
8Swaggerswag annotationsdocs/swagger.* regeneratedChunks 1,3,6,7
9FE counter + inbox-disableFeaturePermission.vue, permissionStore.ts, EditRoles.vue, specscounter N/20; 21st blocked; inbox disabledREV-6 (Figma), REV-5 (marker)
10FE flag gate + dropdown + errorsuseFeatureFlag, FieldRoles.vue, InviteUsers.vue, EditUsers.vueLite shown only flag ON; quota errors surfacedNone

Dangling Decisions Log

#DecisionLocationOwnerDeadline
1Chunk-7 host path: refactor Update vs route via AssignRole (REV-4)Open Q9 / ADR-5 / chunk 7Bifrost Engbefore BE merge

Note: this is the only true dangling decision. REV-1/2/3/5 are dangling inputs (external/owned-elsewhere), tracked as Open Questions, not architecture choices.


Open Questions

#QuestionCategorySeverity
1Exact Lite ModPanel component Code strings? (REV-1)ACV / DEPBlocking
2Inbox permission_key blocklist content + permissions_crs is_inbox derivation rule? (REV-3, REV-5)SAS / ACV / CNTBlocking
3lite_seats_enabled Kafka event schema (fields, types, state enum)? (REV-2)DIC / FMCBlocking
4Chunk-7 host path — Update refactor or AssignRole routing? (REV-4)TDC / DICImportant
5ModPanel quota-read timeout/retry/circuit-breaker numbers for the Lite path? (REV-7)FMCNice-to-have
6FE 429/lock-contention message for role-change flow? (REV-8)FMCNice-to-have
7Lite-config Figma frame (counter + inbox grey-out) — FE chunk 9 (REV-6)CNT / NFSImportant (FE only)
8Count-based model acceptable to Billing (no per-bucket report-back)? (Open Q4)DICImportant
9is_system_role substitution via delete-guard acceptable vs adding column? (Open Q6)DMSNice-to-have

Evidence Notes

  • internal/app/service/billings/validate_user_quota.gore-verified live: exactly CountUser(companyID) >= InitialCredit+AdditionalCredit, no role awareness. Confirms ADR-2's count-based premise and the §1 PRD-reframing callout. Raised PRT/DIC/ADR-2 confidence to High.
  • internal/app/service/users/assign_role.go + update.gore-verified live: assign_role.go has zero SetNx/BeginTx/WithTx/ValidateUserQuota matches; update.go uses sync.WaitGroup + go func() with no transaction. Confirms the RFC's R1 grounding correction (ADR-5, Open Q9, chunk 7) is accurate, and is the basis for REV-4's severity.
  • db/schema.sqlcompany_roles has user_access/is_default, no is_system_role; users has no consumed_bucket. Confirms A-1, ADR-1, ADR-3, §2.3 DDL.
  • internal/app/service/billings/get_billing_info_by_sso_id.go — parses QONTAKCHAT-User-Initial/-Additional. Confirms the parse pattern; the Lite analog strings are the REV-1 gap.
  • internal/pkg/http/default_error.goErrUnprocessableEntity→422, ErrBadRequestCustomDesc→400 both present. Confirms §2.4/§3.B error-code pinning.
  • §6 Comment-log (RFC) — the author's R1 adversarial self-review; reconciled (not double-counted) per the SOP.
  • Discussion/comment sections: §6 Comment-log treated as non-authoritative per the evidence-normalization rule, except where its corrections are already promoted into the authoritative spec body (ADR-5, §2.E, chunk 7, §3.B) — those are scored from the spec body, not the log.

Review History

CycleDateReviewed RFC revision (last_updated / commit)ScoreVerdictFindings open → fixedNotes
R12026-06-23last_updated: 2026-06-23 / working tree (uncommitted)8.0PROCEED with notes (HOLD on FE chunk 9 + blocking deps)8 open (3 blocker, 2 major, 3 minor), 0 fixedFirst external review. Independently re-verified all load-bearing §2.0 grounding claims against live qontak-launchpad; all accurate. Reconciled with the RFC's own §6 R1 self-review (lock/tx correction) without double-counting. Score ceiling is the three open external blockers + missing Figma, not any score cap.