Skip to main content

Task Breakdown — AI Agent Impact Report, Phase 1: Live Impact Report

Source RFC: phase-1-live-impact-report.md (IDEA/draft, 8.0/10 PROCEED-with-notes) Review: phase-1-live-impact-report-review.md Mode: Horizontal (Phase 1 = FE UI with mocked API · Phase 2 = BE implementation + FE API integration). Blocked tasks included inline (full picture). Scope: all 5 impact stories. Repos (verified local siblings): chatbot (Rails 7.1 / Grape BE) · chatbot-fe (Nuxt 3 / Vue 3 / Pinia FE) · qontak-designer (visual reference only, no build input). All file paths below were verified by codebase reconnaissance on 2026-07-08. Deploy order per RFC §4 is BE-first; the horizontal build order is FE-UI-with-mocks in parallel, then BE, then FE-wiring — see Ordering rationale.

Effort Summary

Phase / AreaFE daysBE daysQA daysTotal
Phase 1 — UI (mocked)13316
Phase 2 — API integration2.510517.5
Grand total15.510833.5

Confidence: medium. The estimate is well-grounded on the FE side (every pattern verified against real code) and on the BE scaffolding, but three items carry real risk: the aggregator correctness core (Task 2.2 — reopen self-join REV-1, turns-derivation REV-3, cross-DB sentiment join) is the single biggest unknown; the forecast (Task 2.6) is fully blocked until REV-2/OQ-10 names an algorithm; and OQ-1 (AI add-on feature code) / OQ-2 (canonical FE role strings) gate the entitlement + role-gate tasks. rooms has no supporting indexes (REV-4), so the nightly scan cost is a genuine open risk sized in Task 2.1.


Phase 1 — UI (APIs mocked)

Task 1.1: [FE] Impact-report Pinia store + service + endpoint + mock fixtures (IMPACT-S01..S05)

Foundation: a data layer every tile reads from, returning fixture data so all UI can be built and tested before the BE exists.

Status: ✅ Actionable

Design reference: n/a — no UI (data layer). DS version: @mekari/pixel3@^1.0.12.

What to build

A 6-file Pinia store store/ai-agent-impact/ mirroring store/report/ (uses extractStore, actions sub-store returning { fetch, request: controller }, action-name constants in types.ts), a service common/services/main/v1/ai-agent-impact.ts returning { fetch, controller } with a per-method AbortController, and endpoint registration. In Phase 1 the service resolves from a local mock fixture; Phase 2 (Task 2.7) points it at the real endpoint.

Implementation Plan

ActionFileWhat changes
createstore/ai-agent-impact/index.tsdefineStore("ai-agent-impact", () => ({ ...extractStore(useState()), ...extractStore(useGetters()), ...extractStore(useActions()) }))
createstore/ai-agent-impact/{state,getters,actions,interface,types}.tsfetchStatus machine, report/costAssumption state, action constants
createcommon/services/main/v1/ai-agent-impact.tsgetReport(nuxtApp, params) + getCostAssumption/putCostAssumption; each returns { fetch, controller }
modifycommon/services/main/endpoint.tsadd ai_agent_impact: { report: "v1/reports/ai_agent_impact", cost_assumption: "v1/reports/ai_agent_impact/cost_assumption" } under the v1 key
modifycommon/services/main/index.tsregister the new ./v1/ai-agent-impact service in the aggregator
createstore/ai-agent-impact/__mocks__/report.fixture.tsfixture matching the RFC §2.4 response envelope (net/gross/journey/quality/trend/work_absorbed/forecast)
createcommon/services/main/v1/ai-agent-impact.spec.tsco-located service test ({fetch,controller} shape, abort)
createtests/unit/store/ai-agent-impact/index.spec.tsstore fetch machine test

Implementation steps

  1. Explore — open store/report/{index,actions,state,types}.ts and common/services/main/v1/report.ts; copy the extractStore composition + the { fetch, request: controller } action return exactly. Note endpoints are referenced as endpoint.v1.<feature>.<key> and consumed via services.mainService.default.<feature>.<method>.
  2. Write failing tests (red) — create the store + service specs; run pnpm test -- ai-agent-impact and confirm red.
  3. Scaffold — create the 6 store files + service, wiring action constants from types.ts.
  4. Wire state — action calls the service, $patches report/fetchStatus (pendingresolved/rejected). Stash the AbortController for unmount abort.
  5. Mock — resolve the service from report.fixture.ts behind a build-time flag/injected stub so tiles render real-shaped data. Note: real call added in Task 2.7.
  6. Go greenpnpm test -- ai-agent-impact.
  7. Quality gatepnpm lint.

Acceptance criteria

  • Store exposes report, costAssumption, fetchStatus and a fetch action following the store/report pattern.
  • Service method returns { fetch, controller } and creates its own AbortController.
  • fetchStatus transitions pending → resolved (fixture) and pending → rejected (forced error).
  • Endpoint keys registered under endpoint.v1.ai_agent_impact.*.

Test strategy

vitest: mock $apiMain to resolve the fixture; assert the action sets fetchStatus=resolved and populates report; assert rejected on throw. Service spec asserts the returned shape + that controller.abort() cancels.

Effort estimate

DisciplineDays
Frontend1.5
Backend
QA
Total1.5

Assumptions: direct reuse of the verified 6-file store/report pattern; no persist:true (report store doesn't use it); snake_case consumed raw (OQ-5, verified low-risk).

Run to verify

pnpm test -- ai-agent-impact && pnpm lint

Depends on

  • None (foundation).

Task 1.2: [FE] Feature + role gate middleware (IMPACT-S01-NEG, IMPACT-S02-NEG)

A non-AI account or an ineligible role (Agent) never sees the report entry — navigation is aborted before the page renders.

Status: ⚠️ Partially blocked — the flag-gate half is fully actionable now; the exact AI add-on feature code (OQ-1/REV-5) and canonical role strings (OQ-2/REV-6) must be confirmed to finalize the predicate. Build against placeholders and note the two constants.

Design reference: n/a — no UI. DS version: @mekari/pixel3@^1.0.12.

What to build

middleware/ai-agent-impact-feature.ts extending the verified flag-gate template with an added role check. Aborts with createError({ statusCode: 404 }) when the AI add-on flag is off OR the role is not in {owner, admin, supervisor}.

Implementation Plan

ActionFileWhat changes
createmiddleware/ai-agent-impact-feature.tsflag check (subscriptionData.features.find(code===<OQ-1>).enabled) + role check (authenticationStore().profile.data.role ∈ allowed) → abortNavigation(createError({statusCode:404}))
createtests/unit/middleware/ai-agent-impact-feature.spec.tsflag-off → abort; agent role → abort; owner + flag-on → pass

Implementation steps

  1. Explore — read middleware/ai-assist-dynamic-kb-feature.ts (flag-gate pattern) and how authenticationStore().profile.data.role is read in common/utils/tracking.ts:102 (role is nested under profile.data, untyped).
  2. Write failing tests (red) — three cases above; pnpm test -- ai-agent-impact-feature.
  3. Implement — copy the flag-gate abort, add the role predicate. Use a named constant AI_ADDON_FEATURE_CODE (placeholder until OQ-1) and ALLOWED_ROLES (placeholder casing until OQ-2).
  4. Go greenpnpm test.
  5. Quality gatepnpm lint.

Acceptance criteria

  • Flag off → abortNavigation(404).
  • Role = agent → abortNavigation(404).
  • Owner/admin/supervisor + flag on → navigation proceeds.
  • (pending OQ-1) feature code constant wired to the real value.
  • (pending OQ-2) role strings match canonical casing.

Test strategy

vitest: stub subscriptionStore/authenticationStore; assert abortNavigation called with a 404 error for each deny case, not called on allow.

Effort estimate

DisciplineDays
Frontend0.5
Backend
QA
Total0.5

Assumptions: role predicate is net-new (existing middleware is flag-only, verified); casing/code are one-line swaps once OQ-1/OQ-2 land.

Run to verify

pnpm test -- ai-agent-impact-feature && pnpm lint

Depends on

  • [External: OQ-1 feature code, OQ-2 role strings] — non-blocking to build; blocks final correctness.

Task 1.3: [FE] Report page + view shell + baseline-forming + error/retry states (IMPACT-S01/AC-6, IMPACT-S01/ERR-1)

The admin opens /reports/ai-agent-impact and always gets a coherent screen — a loading skeleton, a baseline-forming empty state when data is thin, or an error+retry when the load fails — never a blank page or a fabricated number.

Status: ✅ Actionable

Design reference: n/a — design pending (OQ-6). DS version: @mekari/pixel3@^1.0.12. Reference: mekari-taste plain-language wireframe + PRD Appendix A Stitch prompt #1.

What to build

The thin page, the composition view (ai-agent-impact.vue), the new module components/ folder (does not exist yet under modules/report/), a new reusable error+retry component (none exists in the repo — verified), reuse of no-data.vue for baseline-forming, the MpSkeleton loading state, and the mount-time fetch + AI_AGENT_IMPACT_VIEWED event.

Implementation Plan

ActionFileWhat changes
createpages/reports/ai-agent-impact/index.vuethin page → mounts the module view; applies ai-agent-impact-feature middleware
createmodules/report/views/ai-agent-impact.vuecomposition root: fetch on mount, fetchStatus switch (loading/baseline/error/success), date-range control, abort on unmount
createmodules/report/components/ai-agent-impact/ErrorRetry.vuenew: MpBanner + MpButton retry (net-new — no reusable error+retry exists)
createmodules/report/components/ai-agent-impact/BaselineForming.vuewraps common/components/error/no-data.vue (volume-only, no %)
modifycommon/contants/mixpanel-events.tsadd AI_AGENT_IMPACT_VIEWED, IMPACT_LOAD_FAILED, IMPACT_BASELINE_FORMING ([CHATBOT] ... strings) — note the real misspelled contants/ dir
createmodules/report/views/ai-agent-impact.spec.tsco-located view test for the state machine

Implementation steps

  1. Explore — read pages/report/index.vue + modules/report/views/bot-peformance.vue for the thin-page→view + fetchStatus (idle/pending/resolved/rejected) convention; read common/components/error/no-data.vue props (title/description/svgName) and common/utils/tracking.ts (trackEvent(name, props, jimo)). Create the modules/report/components/ai-agent-impact/ dir (new).
  2. Write failing tests (red) — view renders skeleton on pending, BaselineForming when report.baseline_forming, ErrorRetry on rejected, tiles slot on resolved; pnpm test -- ai-agent-impact.
  3. Scaffold — page + view shell + the two state components; import store from Task 1.1 (fixture-backed).
  4. Wire state — fetch on mount via store action, stash AbortController, abort on unmount; fire AI_AGENT_IMPACT_VIEWED with {org_id, role, date_range, has_forecast}.
  5. Implement — retry re-invokes the fetch; date-range change refetches.
  6. Go greenpnpm test.
  7. Quality gatepnpm lint && pnpm build.

Acceptance criteria

  • Loading → MpSkeleton tiles + skeleton chart.
  • baseline_forming:true → volume-only empty state, no percentages, impact_report_baseline_forming fired.
  • rejected → error+retry; Retry re-fetches; no partial fabricated data.
  • AI_AGENT_IMPACT_VIEWED fires once on successful mount.
  • Date-range change triggers a refetch.

Test strategy

vitest: drive fetchStatus through the store; assert each branch renders its component; assert trackEvent called with the view event name on resolve. Mock trackEvent.

Effort estimate

DisciplineDays
Frontend2
QA0.5
Total2.5

Assumptions: no-data.vue reused as-is; error+retry is a small new component; view is the composition root that later tile tasks slot into (they don't re-own this file).

Run to verify

pnpm test -- ai-agent-impact && pnpm lint && pnpm build

Depends on

  • [Task 1.1] (store), [Task 1.2] (middleware).

Task 1.4: [FE] Value-delivered grid tiles — hero + volume + after-hours + work-absorbed + headline (IMPACT-S01/AC-1..5)

The admin sees, at a glance, an honest containment hero (net of reopens, with the false-resolution gap), plus volume, after-hours, and money-saved tiles and a plain-language headline — every figure carrying a verdict, never a bare number.

Status: ⚠️ Partially blocked — the after-hours tile is gated on OQ-4 (per-org business-hours config not found; PRD §16 mitigation is to ship without it). Build the tile behind a hasAfterHours guard that hides it when the field is null; everything else is fully actionable.

Design reference: n/a — design pending (OQ-6). DS version: @mekari/pixel3@^1.0.12. Reference: PRD Appendix A Stitch prompt #1. Brand color on the hero number only.

What to build

HonestContainmentTile (net % hero + verdict pill + "leaves out repeat-askers" note), VolumeTile, AfterHoursTile (null-guarded), WorkAbsorbedTile (shows a "set assumption" prompt when work_absorbed is null — no fabricated figure), PlainHeadlineSummary, composed in a ValueDeliveredGrid, all built on MpBox/MpText/MpTag.

Implementation Plan

ActionFileWhat changes
createmodules/report/components/ai-agent-impact/HonestContainmentTile.vueprops { net; gross; reopenGap; verdict; baselineForming }; hero + pill + gap note
createmodules/report/components/ai-agent-impact/VolumeTile.vuevolume count tile
createmodules/report/components/ai-agent-impact/AfterHoursTile.vueprops { count; pct }; rendered only when field non-null (OQ-4)
createmodules/report/components/ai-agent-impact/WorkAbsorbedTile.vue`{ hours; rupiah }
createmodules/report/components/ai-agent-impact/PlainHeadlineSummary.vueplain-language sentence from the metrics
createmodules/report/components/ai-agent-impact/ValueDeliveredGrid.vuelays out the tiles; slots into the view (Task 1.3)
createmodules/report/components/ai-agent-impact/*.spec.tsone spec per tile (verdict-not-number, null degrade)

Implementation steps

  1. Explore — read modules/settings/views/ai-assist.vue for pixel3 MpBox/MpText/MpTag usage + import style (@/, from @mekari/pixel3).
  2. Write failing tests (red) — hero renders verdict text not a bare number; after-hours hidden when null; work-absorbed shows prompt when null; pnpm test.
  3. Scaffold — each tile with typed defineProps.
  4. Implement — verdict pill logic, reopen-gap note, null guards; grid composition.
  5. Go greenpnpm test.
  6. Quality gatepnpm lint && pnpm build.

Acceptance criteria

  • Hero shows the net % + verdict pill + reopen-gap note; never a gross-only bare number (Success Criteria #5).
  • After-hours tile is absent when the field is null (OQ-4 mitigation), present with count+% otherwise.
  • Work-absorbed null → "set assumption" prompt, no fabricated rupiah (IMPACT-S04/AC-3).
  • Plain headline renders a full-sentence summary.

Test strategy

vitest: mount each tile with fixture props; assert verdict text present, bare-number absent; assert conditional rendering for the two nullable tiles.

Effort estimate

DisciplineDays
Frontend2
QA0.5
Total2.5

Assumptions: pure presentational tiles on pixel3 primitives; no new state beyond props; after-hours built now, shown conditionally.

Run to verify

pnpm test -- ai-agent-impact && pnpm lint

Depends on

  • [Task 1.3] (view shell). WorkAbsorbedTile's prompt wires to [Task 1.8] (modal).

Task 1.5: [FE] Quality grid tiles — reopen rate + sentiment delta + turns (IMPACT-S03/AC-1..4)

The admin can judge whether the AI is doing a good job — repeat-ask (reopen) rate, sentiment of AI-contained vs escalated chats, and how many turns it takes to resolve — with each tile degrading to "not available" rather than lying when its data is missing.

Status: ✅ Actionable (UI). Note: the sentiment tile must render a reduced-coverage note when coverage is low and "not available" when the field is null — the coverage threshold that flips the note is undefined (review Vague-word #3); use a placeholder constant and flag it.

Design reference: n/a — design pending (OQ-6). DS version: @mekari/pixel3@^1.0.12.

What to build

ReopenRateTile, SentimentDeltaTile (coverage-aware), TurnsTile, composed in a QualityGrid, each with a per-tile degrade path.

Implementation Plan

ActionFileWhat changes
createmodules/report/components/ai-agent-impact/ReopenRateTile.vue{ reopenRate } + verdict
createmodules/report/components/ai-agent-impact/SentimentDeltaTile.vue`{ delta; coverage }
createmodules/report/components/ai-agent-impact/TurnsTile.vue{ turnsAvg }
createmodules/report/components/ai-agent-impact/QualityGrid.vuelays out the three tiles into the view
createmodules/report/components/ai-agent-impact/*.spec.tsper-tile specs incl. sentiment null/low-coverage

Implementation steps

  1. Explore — reuse the tile idioms established in Task 1.4.
  2. Write failing tests (red) — sentiment null → "not available"; low coverage → note; reopen/turns render values; pnpm test.
  3. Scaffold + implement the three tiles + grid.
  4. Go greenpnpm test.
  5. Quality gatepnpm lint.

Acceptance criteria

  • Reopen-rate tile renders rate + verdict.
  • Sentiment tile shows "not available" on null, reduced-coverage note below threshold, delta otherwise.
  • Turns tile renders the average.
  • A missing tile never blocks the rest of the grid.

Test strategy

vitest: mount with null/low-coverage/normal fixtures; assert the three sentiment branches; assert independent degrade.

Effort estimate

DisciplineDays
Frontend1.5
QA0.5
Total2

Assumptions: presentational tiles; coverage threshold is a one-line constant pending a product decision.

Run to verify

pnpm test -- ai-agent-impact && pnpm lint

Depends on

  • [Task 1.3] (view shell).

Task 1.6: [FE] BlendedJourneyBar — custom SVG 3-segment bar (IMPACT-S02/AC-1..3, ERR-1)

The admin sees how conversations split across AI-resolved-alone / AI-assisted-then-human / escalated as one honest room-grain bar with plain labels.

Status: ✅ Actionable

Design reference: n/a — design pending (OQ-6). Custom SVG/CSS (no chart lib — Decision 7; verified none in package.json). Reference: qontak-designer hand-SVG charts (visual only). Design QA: TBD (OQ-6).

What to build

A hand-built SVG 3-segment horizontal bar summing to 100%, plain labels ("AI resolved alone" / "AI assisted, human closed" / "Escalated to human"), role="img" + aria-label value summary, and a placeholder when journey data is missing.

Implementation Plan

ActionFileWhat changes
createmodules/report/components/ai-agent-impact/BlendedJourneyBar.vueprops { aiOnly; aiAssisted; escalated }; custom SVG; a11y label; missing-data placeholder
createmodules/report/components/ai-agent-impact/BlendedJourneyBar.spec.tssegments sum to 100%; plain labels; placeholder on missing

Implementation steps

  1. Explore — review a qontak-designer hand-SVG chart for the geometry approach (reference only; not imported); confirm no chart dep exists.
  2. Write failing tests (red) — segment widths proportional and sum to 100%; labels are plain-language; placeholder when all-null; pnpm test.
  3. Scaffold + implement the SVG with pixel3 segment colors + Tailwind.
  4. Go greenpnpm test.
  5. Quality gatepnpm lint && pnpm build (watch bundle delta — no new dep).

Acceptance criteria

  • Three segments render with widths proportional to the counts and sum to 100%.
  • Labels are plain-language, not raw enum values.
  • role="img" + aria-label summarizes the split.
  • Missing journey data → placeholder, rest of report unaffected.

Test strategy

vitest: mount with a known split; assert computed segment widths and the aria-label string; assert placeholder branch.

Effort estimate

DisciplineDays
Frontend2
QA0.5
Total2.5

Assumptions: hand-built SVG (no charting lib); geometry is simple proportional widths.

Run to verify

pnpm test -- BlendedJourneyBar && pnpm lint

Depends on

  • [Task 1.3] (view shell).

Task 1.7: [FE] ForecastPanel — custom SVG trend + flag-gated projection (IMPACT-S05/AC-1..4)

The admin sees the containment trend vs the onboarding baseline, and — only when the forecast flag is on — a dashed next-period projection.

Status: ⚠️ Partially blocked — the UI (solid historical line + dashed-projection rendering, flag gating) is fully actionable against trend[] + a forecast.projected_net prop. The projection value itself is blocked on REV-2/OQ-10 (forecast algorithm unspecified); the panel renders whatever forecast the BE supplies, so build the UI now and the number arrives via Task 2.6.

Design reference: n/a — design pending (OQ-6). Custom SVG/CSS (Decision 7). Design QA: TBD (OQ-6).

What to build

A hand-built SVG line: solid past points from trend[], dashed projection appended only when flagOn && forecast; baseline-forming note when trend is too short; historical-only when flag off; prefers-reduced-motion respected.

Implementation Plan

ActionFileWhat changes
createmodules/report/components/ai-agent-impact/ForecastPanel.vueprops { trend:{date;net}[]; forecast?:{projectedNet}; flagOn }; solid + dashed SVG
modifycommon/contants/mixpanel-events.tsadd IMPACT_FORECAST_RENDERED
createmodules/report/components/ai-agent-impact/ForecastPanel.spec.tsdashed hidden when flag off / no forecast; historical always renders

Implementation steps

  1. Explore — reuse the SVG approach from Task 1.6.
  2. Write failing tests (red) — dashed projection absent when flagOn=false or forecast undefined; solid trend always present; pnpm test.
  3. Scaffold + implement the line geometry + dashed segment + baseline note.
  4. Fire IMPACT_FORECAST_RENDERED when the projection shows.
  5. Go greenpnpm test.
  6. Quality gatepnpm lint.

Acceptance criteria

  • Solid historical trend renders from trend[].
  • Dashed projection renders only when flagOn && forecast present.
  • Flag off / insufficient trend → historical-only, no projection.
  • IMPACT_FORECAST_RENDERED fires when projection shown.

Test strategy

vitest: mount with flag on/off × forecast present/absent; assert dashed path presence per matrix.

Effort estimate

DisciplineDays
Frontend2
QA0.5
Total2.5

Assumptions: panel is presentational; the forecast number is a BE responsibility (Task 2.6). Bundle-safe (no chart lib).

Run to verify

pnpm test -- ForecastPanel && pnpm lint

Depends on

  • [Task 1.3] (view shell). Projection value: [Task 2.6] (blocked on REV-2/OQ-10).

Task 1.8: [FE] CostAssumptionModal — pixel3 modal + inline validation (IMPACT-S04/AC-1..3, ERR-1)

An owner/admin can set the agent-hour rate and minutes-per-conversation that turn contained conversations into a rupiah "work absorbed" figure; supervisors/agents see the figure but no editor.

Status: ✅ Actionable (UI + validation). The real save is wired in Task 2.8.

Design reference: n/a — design pending (OQ-6). Build on MpModal* compound (per modules/settings/views/ai-assist.vue). Reference: PRD Appendix A Stitch prompt #2. DS version: @mekari/pixel3@^1.0.12.

What to build

CostAssumptionModal using the pixel3 MpModal/MpModalContent/Header/Body/Footer/Overlay compound; numeric fields with inline non-negative + bounds validation; a hidden editor for non-owner/admin (canEdit=false); first-time blank + "why we ask" helper; save spinner. Emits save/close.

Implementation Plan

ActionFileWhat changes
createmodules/report/components/ai-agent-impact/CostAssumptionModal.vueprops { isOpen; value:{agentHourRate?; minutesPerConversation?}; canEdit }; emits save,close; inline 422-style validation
modifycommon/contants/mixpanel-events.tsadd COST_ASSUMPTION_UPDATED
createmodules/report/components/ai-agent-impact/CostAssumptionModal.spec.tsvalidation + canEdit gating + emits

Implementation steps

  1. Explore — read modules/settings/views/ai-assist.vue:474-513 for the MpModal* compound + :is-open + local open ref.
  2. Write failing tests (red) — negative/non-numeric → inline error, no save emit; canEdit=false → no editor; valid → save payload; pnpm test.
  3. Scaffold + implement the modal, fields, validation, helper copy, spinner.
  4. Go greenpnpm test.
  5. Quality gatepnpm lint.

Acceptance criteria

  • Negative/non-numeric input → inline field error, nothing emitted.
  • canEdit=false → editor hidden (read-only figure only).
  • Valid save emits { agentHourRate, minutesPerConversation } and fires COST_ASSUMPTION_UPDATED.
  • First-time (no value) → blank fields + "why we ask" helper.

Test strategy

vitest: mount with canEdit true/false; submit invalid + valid; assert inline error, emit payloads, and editor visibility.

Effort estimate

DisciplineDays
Frontend1.5
QA0.5
Total2

Assumptions: reuses the verified MpModal* pattern; client validation mirrors the BE 422 bounds (upper bounds pending OQ-3).

Run to verify

pnpm test -- CostAssumptionModal && pnpm lint

Depends on

  • [Task 1.3] (view shell). Real persistence: [Task 2.8].

Phase 2 — API Integration (BE implementation + FE wiring)

Task 2.1: [BE] Migrations — ai_activity_logs + ai_cost_assumptions + rooms supporting indexes (IMPACT-S01..S05, REV-4)

Stand up the report's read spine and the cost-assumption store, and add the missing rooms indexes the nightly aggregation needs so it doesn't table-scan.

Status: ✅ Actionable

Design reference: n/a — BE only.

What to build

Two additive tables per the RFC §2.3 DDL, plus — verified critical (REV-4) — new concurrent composite indexes on rooms. Recon confirmed rooms has no index on contact_id, closed_at, or created_at, and zero composite indexes; the aggregator's org+date filter and the reopen self-join on contact_id would otherwise scan.

Implementation Plan

ActionFileWhat changes
createdb/migrate/<ts>_create_ai_activity_logs.rb16-col table + UNIQUE(organization_id, activity_date); disable_ddl_transaction! + algorithm: :concurrently
createdb/migrate/<ts>_create_ai_cost_assumptions.rbtable + UNIQUE(organization_id) + CHECK(>=0) on rate/minutes
createdb/migrate/<ts>_add_impact_indexes_to_rooms.rbconcurrent composite index (organization_id, closed_at) (+ (organization_id, created_at) if the aggregator groups on created_at) and an index supporting the contact_id + time self-join
verifydb/schema.rbregenerated with new tables + indexes

Implementation steps

  1. Explore — read db/migrate/20260624000001_add_related_key_and_related_type_to_attachments.rb for house style (ActiveRecord::Migration[7.1], disable_ddl_transaction!, add_index ..., algorithm: :concurrently, if_not_exists: true); read db/schema.rb:1761 (rooms) to confirm current index gaps before adding.
  2. Write the three migrations following that style; counts NOT NULL DEFAULT 0.
  3. RunRAILS_ENV=test bundle exec rails db:migrate, then db:rollback to confirm reversibility.
  4. Quality gatebundle exec rubocop on the migration files.

Acceptance criteria

  • Both tables + unique indexes exist; db:rollback reverts cleanly.
  • New rooms composite index(es) created concurrently, if_not_exists: true.
  • CHECK (>= 0) present on agent_hour_rate and minutes_per_conversation.

Test strategy

Migration test: db:migrate up + db:rollback down clean; schema reflects unique + composite indexes.

Effort estimate

DisciplineDays
Backend1
QA
Total1

Assumptions: additive only; the REV-4 index migration is the reason this is 1 day not 0.5. Final index column set confirmed once the aggregator's group-by column (closed_at vs created_at) is fixed in Task 2.2.

Run to verify

RAILS_ENV=test bundle exec rails db:migrate && RAILS_ENV=test bundle exec rails db:rollback

Depends on

  • None.

Task 2.2: [BE] Models + aggregation repository — containment, journey, reopen, sentiment, turns (IMPACT-S01/AC-1..4, S02, S03)

The correctness core: compute per-org-per-day honest containment (net of 48h reopens), the journey split, reopen rate, the cross-DB sentiment delta, and turns-to-resolve — the numbers the whole report exists to make trustworthy.

Status: ⚠️ Partially blocked — must resolve two review findings before the acceptance criteria are frozen: REV-3/OQ-3 (turns source) and the aggregation half of REV-1 (reopen window). Both have a verified path (below), so build can start; the ACs must encode the resolution.

Design reference: n/a — BE only.

What to build

AiActivityLog + AiCostAssumption AR models and a Repositories::AiAgentImpact::Aggregate that computes a day's counts from rooms using the verified COUNT(...) FILTER (WHERE ...) idiom, plus:

  • Containment: RESOLVE_AIassign_channel_agent_id IS NULLclosed_at IS NOT NULL; exclude channel='bot_preview' and closed_reason='SPAM'; bare RESOLVE is not an AI win.
  • Journey: closed_reason → ai_only / ai_assisted / escalated, summing to volume.
  • Reopen (REV-1): same-contact_id new conversation ≤ 48h → reopened_48h; contained_ai_net = gross − reopened_48h.
  • Sentiment (cross-DB): app-level join omnichannel_room_summaries.room_id (string)rooms.channel_room_id (string); record coverage; exclude unknown/missing (L-1).
  • Turns (REV-3): no counter column exists — derive turns_sum/turns_count as COUNT(histories) per room. Note the join-key asymmetry: histories.room_id is bigint → rooms.id, a different key than the string-keyed sentiment join. The aggregator therefore runs two distinct join strategies.

Implementation Plan

ActionFileWhat changes
createapp/models/ai_activity_log.rbAR model, primary DB, validations
createapp/models/ai_cost_assumption.rbAR model + >=0 validations
createapp/core/repositories/ai_agent_impact/aggregate.rbthe COUNT FILTER + reopen self-join + sentiment join + turns COUNT(histories)
createspec/core/repositories/ai_agent_impact/aggregate_spec.rbfixture-org correctness specs

Implementation steps

  1. Explore — read app/core/repositories/custom_report/generate.rb:108-120 (the Room.select("COUNT(id) FILTER (WHERE ...)").where(organization_id:).group("DATE(created_at)") idiom to reuse); read db/schema.rb:1761 (rooms cols), :567 (histories: room_id bigint), db/chatbot_gpt_schema.rb:252 (summaries sentiment + string room_id); confirm closed_reason literals in config/locales/en.yml:345-350note only 4 are defined there; SPAM/WAITING_ASSIGN_AGENT come from write-sites, treat as string constants and centralize them.
  2. Resolve OQ-3 (REV-3) — confirm histories is the turns source and the bigint join; document the two-join approach in the spec.
  3. Resolve REV-1 (aggregation half) — compute reopened_48h for a day by looking 48h forward from each contained room's closed_at; the finalization-across-batches half is Task 2.3.
  4. Write failing tests (red) — on a fixture org: net/gross correct; bot_preview/SPAM/bare RESOLVE excluded; journey sums to volume; reopen self-join within 48h; sentiment coverage recorded + unknown excluded; turns avg = sum/count. bundle exec rspec spec/core/repositories/ai_agent_impact.
  5. Implement the repository (parameterized AR only — no SQL string interpolation of inputs).
  6. Go green + RuboCop.

Acceptance criteria

  • Net/gross containment correct on the fixture; bot_preview + SPAM + bare RESOLVE excluded (Success Criteria #1).
  • Journey segments sum to volume and map closed_reason correctly.
  • Reopen self-join counts same-contact_id reconversation ≤ 48h.
  • Sentiment via app-level string join; coverage recorded; unknown values excluded (L-1, IMPACT-S03/AC-4).
  • (REV-3) Turns derived as COUNT(histories) on the bigint room_id join; turns_sum/turns_count populated.
  • Only the 4 en.yml literals + the 2 write-site literals are treated as known; unknown closed_reason → "other", excluded from AI wins (A2).

Test strategy

rspec on a hand-built fixture org spanning bot_preview/SPAM/RESOLVE/RESOLVE_AI + a 48h reopen + rooms with/without sentiment + rooms with N histories; assert every count. Key mocks: seed rooms, histories, omnichannel_room_summaries.

Effort estimate

DisciplineDays
Backend3
QA1
Total4

Assumptions: reuses the verified COUNT FILTER idiom; the two-join asymmetry (REV-3) + reopen window (REV-1) are the reason this is the largest BE task. Cross-DB join is app-level (no SQL join across DBs).

Run to verify

RAILS_ENV=test bundle exec rspec spec/core/repositories/ai_agent_impact && bundle exec rubocop

Depends on

  • [Task 2.1] (tables). [External: OQ-3 turns source confirmation — path verified, decision pending].

Task 2.3: [BE] Aggregator worker + cron + trailing-day reopen recompute (IMPACT-S01, REV-1)

A nightly job materializes each org's daily row, and — crucially — recomputes the trailing days so a reopen that lands 1–2 days later correctly lowers the earlier day's honest-containment hero.

Status: ⚠️ Partially blocked — the worker scaffold is actionable; the REV-1 trailing-day recompute must be encoded as an acceptance criterion (it is not in the original RFC job spec). Verified support: the (org, activity_date) idempotent upsert already permits re-writing prior days.

Design reference: n/a — BE only.

What to build

AiActivityLogAggregatorWorker (sidekiq, retry: 3) that calls the Task 2.2 repository and upserts one row per org-day; a sidekiq-cron entry at 0 1 * * * Asia/Jakarta on the application_maintenance queue (verified to exist — used by DeleteOldLogsWorker/PartitionMaintenanceWorker); recompute the trailing 2 activity_date rows each run so reopened_48h/contained_ai_net finalize across the batch boundary; 13-month TTL cleanup.

Implementation Plan

ActionFileWhat changes
createapp/workers/ai_activity_log_aggregator_worker.rbinclude Sidekiq::Worker; sidekiq_options queue: :application_maintenance, retry: 3; perform(activity_date = Date.yesterday) + recompute D-1, D-2
modifyconfig/schedule.ymladd ai_activity_log_aggregator cron entry (cron/class/queue)
createspec/app/worker/ai_activity_log_aggregator_worker_spec.rbnote: worker specs live at spec/app/worker/ (singular, verified)

Implementation steps

  1. Explore — read app/workers/assign_agent_worker.rb (worker + sidekiq_options) and config/schedule.yml (sidekiq-cron YAML entries + Asia/Jakarta TZ). Confirm application_maintenance queue usage on the two maintenance workers.
  2. Write failing tests (red) — upserts one row/org-day; idempotent re-run yields identical values; a reopen landing on D+2 decrements day D's net on the next run (REV-1 criterion); sentiment coverage recorded; missing sentiment excluded. bundle exec rspec spec/app/worker/ai_activity_log_aggregator_worker_spec.rb.
  3. Implement — loop orgs, call the repository, upsert on the unique key, recompute trailing 2 days, run 13-month TTL cleanup.
  4. Poison handling — after retry: 3 → dead set + Rollbar; next night recomputes (idempotent).
  5. Go green + RuboCop.

Acceptance criteria

  • Upserts exactly one row per org per day; idempotent re-run is a no-op on values.
  • (REV-1) Each run recomputes the trailing 2 days; a D+2 reopen lowers day D's contained_ai_net.
  • Sentiment coverage recorded; missing sentiment excluded, not zero-filled.
  • 13-month rows past TTL cleaned nightly.
  • Failure → dead set + Rollbar; next run self-heals.

Test strategy

rspec: seed two consecutive days with a cross-boundary reopen; run worker for D then D+2; assert day D's net drops on the second run. Assert idempotency by running twice.

Effort estimate

DisciplineDays
Backend2
QA0.5
Total2.5

Assumptions: reuses the verified sidekiq-cron pattern; trailing-day recompute rides the existing idempotent upsert. TTL cleanup in the same job.

Run to verify

RAILS_ENV=test bundle exec rspec spec/app/worker/ai_activity_log_aggregator_worker_spec.rb && bundle exec rubocop

Depends on

  • [Task 2.2] (repository).

Task 2.4: [BE] Report GET endpoint + use-case + entity + entitlement + role gate (IMPACT-S01..S05, S01-NEG, S02-NEG)

Serve the assembled report over GET /v1/reports/ai_agent_impact — org-scoped, role-gated, entitlement-gated — returning the full nested envelope the FE renders.

Status: ⚠️ Partially blocked — endpoint/use-case/entity fully actionable; the entitlement check needs the AI add-on feature code (OQ-1/REV-5, DB data — verified not in source). Build the gate against a named constant and flag it.

Design reference: n/a — BE only.

What to build

A Grape GET ai_agent_impact in report.rb (mounted via api.rb mount V1::Report => '/v1/reports'), gated by use Middlewares::Ownership (L6) + set_role(%w[owner supervisor admin]) (inside the get block) + an OrganizationFeature entitlement check; a dry-monads use-case that reads ai_activity_logs + ai_cost_assumptions, derives baseline_forming, trend[], and work_absorbed (null when no assumption); and a response entity.

Implementation Plan

ActionFileWhat changes
modifyapp/api/frontend_service/v1/report.rbadd GET /ai_agent_impact with Ownership + set_role + entitlement
createapp/core/use_cases/api/frontend_service/v1/ai_agent_impact/show.rbdry-monads: validate range (≤12mo, start≤end), assemble report
createapp/api/frontend_service/v1/entities/ai_agent_impact/get_response.rbnested envelope (containment/journey/quality/trend/work_absorbed?/forecast?)
createspec/api/frontend_service/v1/ai_agent_impact/show_spec.rbauth matrix + baseline + null work_absorbed

Implementation steps

  1. Explore — read app/api/frontend_service/v1/report.rb (gate placement), app/core/use_cases/api/frontend_service/v1/report/get.rb (dry-monads contract/result/ResultMatcher), app/api/frontend_service/v1/entities/report/report_url.rb (entity convention), subscription_detail.rb:37-52 (entitlement OrganizationFeature.where(company_id:, order_id: orders.pluck(:id), enabled:true); feature code is DB data → OQ-1).
  2. Write failing tests (red) — owner/admin/supervisor → 200; agent → 403; non-AI org → 403; bad range → 400; baseline_forming when < min; work_absorbed null when no assumption. bundle exec rspec spec/api/frontend_service/v1/ai_agent_impact.
  3. Implement the use-case (parameterized AR, org from session never client param), entity, gate.
  4. Go green + RuboCop.

Acceptance criteria

  • owner/admin/supervisor → 200; agent → 403; non-AI org → 403 (entitlement).
  • span > 12mo or start > end → 400.
  • baseline_forming:true under the min threshold (volume only).
  • work_absorbed null when no cost assumption.
  • Every query where(organization_id:)-scoped; org from env['user']['chatbot_organization_id'].
  • (pending OQ-1) entitlement feature code wired to the real value.

Test strategy

rspec request specs across the role×entitlement matrix; assert status + envelope shape + nullability; assert no cross-org leakage.

Effort estimate

DisciplineDays
Backend2
QA0.5
Total2.5

Assumptions: reuses verified Ownership + set_role + dry-monads + entity patterns; trend/baseline derived at read time (no new storage).

Run to verify

RAILS_ENV=test bundle exec rspec spec/api/frontend_service/v1/ai_agent_impact && bundle exec rubocop

Depends on

  • [Task 2.2] (models/repo). [External: OQ-1 feature code].

Task 2.5: [BE] Cost-assumption GET/PUT endpoints + validation (IMPACT-S04/AC-1..3, ERR-1)

Owners/admins persist the cost assumption; the money figure recomputes on the next read. Supervisors read; agents are refused.

Status: ⚠️ Partially blocked — fully actionable except the sane upper bounds for agent_hour_rate / minutes_per_conversation (OQ-3, second half). Ship >= 0 now; add the caps when confirmed.

Design reference: n/a — BE only.

What to build

GET/PUT /v1/reports/ai_agent_impact/cost_assumption; GET gated set_role(%w[owner supervisor admin]), PUT gated set_role(%w[owner admin]); PUT validates non-negative (+ upper bounds pending OQ-3) and upserts on (organization_id); a use-case + entity.

Implementation Plan

ActionFileWhat changes
modifyapp/api/frontend_service/v1/report.rbadd GET + PUT cost_assumption with the two role gates
createapp/core/use_cases/api/frontend_service/v1/ai_agent_impact/cost_assumption/{show,upsert}.rbdry-monads validate + upsert
createapp/api/frontend_service/v1/entities/ai_agent_impact/cost_assumption.rbresponse entity (`data
createspec/api/frontend_service/v1/ai_agent_impact/cost_assumption_spec.rb403/422 matrix + upsert

Implementation steps

  1. Explore — reuse the use-case/entity/gate patterns from Task 2.4.
  2. Write failing tests (red) — PUT owner/admin persists; supervisor/agent → 403; negative/non-numeric → 422 nothing saved; GET returns record or null. bundle exec rspec .../cost_assumption.
  3. Implement validation + upsert on the unique (organization_id) index (strong read-after-write).
  4. Go green + RuboCop.

Acceptance criteria

  • PUT owner/admin → 200 persist; supervisor/agent → 403.
  • negative/non-numeric → 422, nothing persisted.
  • GET → saved record or null.
  • Upsert keyed on (organization_id); last-write-wins.
  • (pending OQ-3) upper-bound caps added to validation.

Test strategy

rspec: role matrix on PUT; invalid-input 422 leaves table unchanged; GET null before first save, record after.

Effort estimate

DisciplineDays
Backend1
QA0.5
Total1.5

Assumptions: single-row upsert; reuses verified gate/use-case patterns.

Run to verify

RAILS_ENV=test bundle exec rspec spec/api/frontend_service/v1/ai_agent_impact/cost_assumption_spec.rb && bundle exec rubocop

Depends on

  • [Task 2.1] (table).

Task 2.6: [BE] Forecast derivation (flag-gated) (IMPACT-S05/AC-2..3)

When the forecast flag is on, the report returns a next-period projected_net derived from the trend.

Status: 🚫 Blocked — REV-2 / OQ-10: the forecasting algorithm is unspecified (only gating, the forecast:{projected_net} output field, and the MAE stop-condition exist). An agent cannot implement this without inventing the model. Unblock: Decision 5 must name the algorithm (e.g. least-squares linear fit over the last N daily net points), the minimum trend length, and the projected_net [0,1] bound. Contained by flag-OFF default, so it lands last.

Design reference: n/a — BE only.

What to build (once unblocked)

The forecast computation behind SystemPreference(rollout/ai_agent_impact_report_forecast), added to the report use-case (Task 2.4); omit when the flag is off or the trend is shorter than the minimum; bound the projection to [0,1].

Implementation Plan

ActionFileWhat changes
modifyapp/core/use_cases/api/frontend_service/v1/ai_agent_impact/show.rbadd flag-gated forecast derivation
modifyapp/api/frontend_service/v1/entities/ai_agent_impact/get_response.rbexpose forecast when present
createspec/.../forecast_spec.rbpresent iff flag on + enough trend; bounded [0,1]

Implementation steps

  1. Blocked — obtain the algorithm + min trend length + bounds from Decision 5 (OQ-10 owner: Dimas + BE).
  2. Exploresystem_preference.rb flag lookup (find_by(group_code:'rollout', code:'ai_agent_impact_report_forecast', enabled:true)).
  3. Write failing tests (red) — forecast present iff flag on and trend ≥ min; value in [0,1].
  4. Implement the named algorithm; go green + RuboCop.

Acceptance criteria

  • (unblock) algorithm + min trend length + bounds specified in Decision 5.
  • forecast present iff flag on and trend ≥ minimum.
  • projected_net bounded to [0,1].

Test strategy

rspec: flag on/off × trend length above/below min; assert presence + bound.

Effort estimate

DisciplineDays
Backend1
QA0.5
Total1.5

Assumptions: estimate assumes a simple linear fit once specified; a seasonal model would raise it.

Run to verify

RAILS_ENV=test bundle exec rspec spec/api/frontend_service/v1/ai_agent_impact && bundle exec rubocop

Depends on

  • [Task 2.4] (report use-case). [External: REV-2 / OQ-10 forecast method — blocking].

Task 2.7: [FE] Wire store/service to the real report endpoint (IMPACT-S01..S05)

Swap the Phase-1 mock for the live GET /v1/reports/ai_agent_impact; all tiles now render real data.

Status: ⚠️ Partially blocked — needs Task 2.4's live contract. Actionable the moment the BE endpoint is deployed behind its flag (deploy order BE-first).

Design reference: n/a — wiring only. DS version: @mekari/pixel3@^1.0.12.

What to build

Point the Task 1.1 service/store at the real endpoint (remove the fixture stub), pass start_date/end_date, and replace mock-based store assertions with real-contract assertions (snake_case consumed raw — OQ-5 verified low-risk).

Implementation Plan

ActionFileWhat changes
modifycommon/services/main/v1/ai-agent-impact.tscall endpoint.v1.ai_agent_impact.report via $apiMain with range params
modifystore/ai-agent-impact/actions.tsconsume res.data (snake_case), set fetchStatus
modifytests/unit/store/ai-agent-impact/index.spec.tsassert real payload mapping + error envelope

Implementation steps

  1. Explore — confirm $apiMain returns response._data and the Bearer injection in api/mainResources.ts.
  2. Write failing tests (red) — store maps a real-shaped 200 payload; 5xx → rejected + impact_report_load_failed.
  3. Implement the real call; delete the fixture branch.
  4. Go greenpnpm test; quality gatepnpm lint && pnpm build.

Acceptance criteria

  • Report GET called with start_date/end_date; tiles render live data.
  • snake_case consumed raw; nullable tiles degrade per the UI State Matrix.
  • 5xx → error+retry + impact_report_load_failed.

Test strategy

vitest with a mocked $apiMain returning the real envelope + an error case; assert mapping + fetchStatus.

Effort estimate

DisciplineDays
Frontend1
QA0.5
Total1.5

Assumptions: no camelCase mapper (OQ-5 verified); most work already done in Task 1.1.

Run to verify

pnpm test -- ai-agent-impact && pnpm lint && pnpm build

Depends on

  • [Task 1.1], [Task 2.4] (live report contract).

Task 2.8: [FE] Wire CostAssumptionModal to GET/PUT + recompute (IMPACT-S04/AC-1..3, ERR-1)

Saving the modal persists via PUT, maps 422s to inline field errors, and the report's money figure recomputes on refetch.

Status: ⚠️ Partially blocked — needs Task 2.5's live contract.

Design reference: n/a — wiring only. DS version: @mekari/pixel3@^1.0.12.

What to build

Wire the Task 1.8 modal save to PUT .../cost_assumption, prefill from GET, map the exact 422 error.messages to field-level inline text (per §2.G), 403 → hide editor, then refetch the report so work_absorbed recomputes.

Implementation Plan

ActionFileWhat changes
modifycommon/services/main/v1/ai-agent-impact.tsgetCostAssumption / putCostAssumption
modifymodules/report/components/ai-agent-impact/CostAssumptionModal.vueon save → PUT → on 200 refetch; map 422 to fields
modifystore/ai-agent-impact/actions.tscost-assumption fetch/save + report refetch
modifymodules/report/components/ai-agent-impact/CostAssumptionModal.spec.ts200/422/403 handling

Implementation steps

  1. Explore — confirm the 422 envelope shape from Task 2.5 (error.messages).
  2. Write failing tests (red) — 200 closes + refetches; 422 → inline; 403 → editor hidden.
  3. Implement the PUT/GET wiring + refetch.
  4. Go greenpnpm test; quality gatepnpm lint.

Acceptance criteria

  • Save → PUT; 200 closes modal and refetches report (work_absorbed recomputes).
  • 422 error.messages mapped to field-level inline text, nothing saved.
  • 403 → editor hidden.

Test strategy

vitest with mocked service for 200/422/403; assert refetch on 200, inline on 422, hidden editor on 403.

Effort estimate

DisciplineDays
Frontend0.5
QA0.5
Total1

Assumptions: modal UI + validation already built in Task 1.8; this is wiring + error mapping.

Run to verify

pnpm test -- CostAssumptionModal && pnpm lint

Depends on

  • [Task 1.8], [Task 2.5] (live cost contract).

The report is reachable from the sidebar for eligible users, and an E2E suite proves the whole flow: owner sees tiles, agent sees no entry, cost save recomputes.

Status: ✅ Actionable once the FE feature is wired (Tasks 2.7/2.8). Note: tests/e2e/ is scaffolded (playwright.config testDir: ./tests/e2e) but currently has no specs — this task adds the first.

Design reference: n/a — nav + tests. DS version: @mekari/pixel3@^1.0.12.

What to build

A new entry in the listMenu computed in layouts/default.vue (flag-gated via an enable flag, following the existing pattern — there is currently no /report menu entry), and tests/e2e/ai-agent-impact.spec.ts.

Implementation Plan

ActionFileWhat changes
modifylayouts/default.vueadd { id, name, label, url:'/reports/ai-agent-impact', enable: aiAgentEnabled } to listMenu
createtests/e2e/ai-agent-impact.spec.tsowner → tiles; agent → no entry; baseline-forming; cost save recompute

Implementation steps

  1. Explore — read layouts/default.vue listMenu shape + how enable flags gate items; playwright.config.ts testDir.
  2. Write E2E — seed a BE (or mock) for the report GET contract; scenarios above.
  3. Implement the menu entry with its enable flag.
  4. Quality gatepnpm lint && pnpm test:e2e.

Acceptance criteria

  • Eligible user sees the sidebar link; ineligible/agent does not.
  • E2E: owner opens report → tiles render; agent → no entry, no surfaced 403.
  • E2E: cost save → money figure recomputes.

Test strategy

Playwright against a seeded BE (report GET contract); assert entry visibility per role and the cost-save recompute.

Effort estimate

DisciplineDays
Frontend1
QA0.5
Total1.5

Assumptions: first E2E spec in the repo (scaffolding exists); nav entry is a small array addition.

Run to verify

pnpm lint && pnpm test:e2e

Depends on

  • [Task 2.7], [Task 2.8].

Ordering rationale

  • Two tracks run in parallel. Phase 1 (FE, mocked) has no BE dependency and can start immediately alongside Phase 2's BE work — the mock fixture (Task 1.1) is the contract seam. This is the horizontal-mode payoff for a full-stack RFC whose deploy order is nevertheless BE-first.
  • Critical path is the BE aggregator chain 2.1 → 2.2 → 2.3 → 2.4, then FE wiring 2.7. Task 2.2 is the single riskiest task (containment correctness, the REV-3 two-join turns derivation, the REV-1 reopen window) — front-load the OQ-3 confirmation and REV-1 decision so its acceptance criteria can be frozen before coding.
  • Resolve the three review findings on their own clocks: REV-1 (reopen window) + REV-3 (turns) before Task 2.2; REV-2 (forecast method) before Task 2.6 — the only fully-blocked task, safely last behind a default-OFF flag.
  • OQ-1 (feature code) / OQ-2 (role strings) gate the final correctness of Tasks 1.2 and 2.4 but not their construction — build against named constants and swap when confirmed.
  • Deploy BE-first (migrations → aggregator → endpoints, flags OFF), then FE; roll back by toggling ai_agent_impact_report OFF (no deploy). Push externally on OQ-1/OQ-2/OQ-3 and the REV-1/REV-2 decisions to unblock the aggregator and forecast.

Skipped stories

None fully omitted — per request, all five impact stories and the NEG stories are broken down, with blocked/partially-blocked items included inline. The table below summarizes every task carrying a blocker so the gates are visible at a glance.

Task / StoryStatusUnblocking condition
2.6 — Forecast (IMPACT-S05/AC-2..3)🚫 BlockedREV-2 / OQ-10 — Decision 5 must name the forecast algorithm, min trend length, [0,1] bound (safe last; flag-OFF default)
2.2 — Aggregator core (IMPACT-S03/AC-3 turns; S01 reopen)⚠️ PartialREV-3 / OQ-3 turns source confirmation (path verified: histories, bigint join) + REV-1 reopen-window decision
2.3 — Aggregator worker (REV-1)⚠️ PartialREV-1 — trailing-day recompute must be an acceptance criterion
1.2 — Middleware; 2.4 — Report entitlement (IMPACT-S01-NEG)⚠️ PartialOQ-1 AI add-on feature code + OQ-2 canonical FE role strings
1.4 — After-hours tile (IMPACT-S01)⚠️ PartialOQ-4 — per-org business-hours config (PRD §16 mitigation: ship without the tile)
2.5 — Cost validation caps (IMPACT-S04)⚠️ PartialOQ-3 (2nd half) — sane upper bounds for rate/minutes
1.3–1.8 — pixel/contrast visual QA⚠️ Design-gatedOQ-6 — Figma frames (layout may proceed against wireframe)
IMPACT-S03-NEG, S04-NEGn/a — not builtOut of scope (no CSAT/quality UI; web-only, no mobile) — correctly excluded from tasks