← all branches

feat/ui-impact

needs attentionviewing older commit
b48af56 · incrementalpre-PRreviewed 2026-07-24 03:14 UTC1H · 6M · 6L · 4I
The branch
Purpose
Improve the energy anomaly detection UI for enterprise multi-site consumers — surface actionable insights with a more defensible statistical model
Goal
Replace naive peer-level benchmark (store vs. tariff-cohort median) with difference-in-differences (DiD) so anomalies reflect deviations from a store's own trajectory, not cross-sectional level differences
Sub-goals
  • SG-1: Add DiD engine (`anomaly.ts`) as a pure computation module
  • SG-2: Wire ConsumptionFindingContent to DiD (chart + KPIs + explanation copy)
  • SG-3: Wire Comparativo page to DiD (chart, ranking, KPIs)
  • SG-4: Update findingView labels/descriptions to reflect DiD framing
The changes (whole branch)
What
New `anomaly.ts` pure module implements DiD: `expected[m] = baseStore × (cohortMean[m] / cohortBase)`. ConsumptionFindingContent and Comparativo page rewritten to use `analyzeStore()`. Ranking now sorts by DiD deviation, not peer-level deviation. ReferenceLine added to mark breakout month. Label/description strings updated in findingView.ts.
Why
Cross-sectional benchmarking mis-classifies large stores as anomalous simply because they are big. DiD isolates the climate/seasonal signal via the cohort index, making the anomaly definition store-specific and defensible.
Areas
apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib+1290apps/platform/src/app/[locale]/(dashboard)/energia/comparativo+100110apps/platform/src/app/[locale]/(dashboard)/energia/_components+8070apps/platform/src/app/[locale]/(dashboard)/energia/_lib+22
Blast
4 files, +311/−182 lines. UI-only — no backend, API, DB, or auth touched. Impact limited to energia dashboard views for demo/prototype data.
prototype-code static-demo-data no-api-changes
ci· No CI check data available for this SHA (pre-PR branch)coderabbit· No .coderabbit.yaml in repostale-test· findingView.test.ts line 70 asserts old label 'Consumo alto vs. tiendas comparables' — will fail

Findings · 17

correctness2

low

deviationPct diluted when series is at minimum length (4 months)

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/anomaly.ts:89

When a store has exactly 4 months (`BASE_WINDOW + 1`), `withExp.slice(-RECENT)` (last 3) overlaps with the 3-month baseline. Since `expected ≈ actual` by construction in the baseline, `deviationPct` is understated for short-series stores. Not a crash, not wrong for longer series; will resolve naturally once stores have more history.

info

`shortMonth` locale output is stable within a client component (no SSR mismatch risk)

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/anomaly.ts:125

ICU versions differ between Node and browser, but this component is `'use client'` — both axis labels and ReferenceLine x= values are produced in the same JS runtime, so no mismatch can occur today.

security1

info

No security findings — all data is static, URL param is whitelist-validated

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:44

`storeId` from URL params is validated against `STORES.some(s => s.id === t)` before use. `analyzeStore()` also does its own `STORES.find()`. No XSS surface (outputs go to JSX text nodes / Recharts props, not `dangerouslySetInnerHTML`). No auth, no DB, no secrets touched.

conventions6

medium

18-line block comment violates 'one short line max' rule

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/anomaly.ts:1

The file-top `/** ... */` block explains what DiD is and what the formula computes — this is 'what the code does' documentation, which CLAUDE.md prohibits. The explanation belongs in the PR description or an ADR. Trim to one line or remove; the inline comments (lines 71, 95) already cover the non-obvious WHY.

medium

9-line JSDoc on ConsumptionFindingContent violates comment rules

apps/platform/src/app/[locale]/(dashboard)/energia/_components/ConsumptionFindingContent.tsx:21

The block comment on the component (lines 21-30) re-explains DiD. CLAUDE.md: 'Never write multi-paragraph docstrings or multi-line comment blocks — one short line max.' The component name and its code already convey intent. Remove or reduce to one line.

medium

13-line JSDoc on ComparativoPage violates comment rules

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:20

Same rule violation: 13-line block comment restating the DiD concept. This is 'what the code does' and lives better in the PR description. Trim to one line.

low

`window.location.search` bypasses `useSearchParams` pattern

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:44

The comment acknowledges avoiding `useSearchParams()` to skip Suspense. The codebase pattern is `useSearchParams()` + `<Suspense>`. `window.location.search` in a `useState` initializer reads stale state if the parent forces a remount. For demo code the risk is low, but it creates a precedent. The correct fix is a small `<Suspense fallback={null}>` wrapper.

low

`export type { Tarifa }` re-export from `anomaly.ts` creates an indirect type path

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/anomaly.ts:129

`Tarifa` is defined in `realConsumption.ts`; `anomaly.ts` re-exports it. Nothing in the diff imports `Tarifa` from `anomaly.ts` — both consumers import from `realConsumption.ts` directly. The re-export creates an indirect path that could mislead future readers. Remove; callers import from the source.

low

Hardcoded Spanish strings — i18n debt (pre-existing, carried forward)

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:90

All user-visible strings are hardcoded in Spanish instead of using `next-intl` `useTranslations()`. This is consistent with how the rest of the `energia/` prototype works (pre-existing tech debt), but the new page adds to it. Flag for awareness; not blocking for demo code.

tests4

high

Stale test label assertion will fail CI

apps/platform/src/app/[locale]/(dashboard)/energia/_lib/__tests__/findingView.test.ts:70

Line 70 asserts `v.title` equals `'Consumo alto vs. tiendas comparables'` (the old cross-sectional label). The commit changed `TYPE_LABEL['peer_benchmark_consumption']` to `'Consumo desviado de su tendencia esperada'`. This test will fail. Fix: update the assertion to match the new string.

medium

No unit tests for `anomaly.ts` — DiD algorithm is untested

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/anomaly.ts

The module is pure (no I/O) and directly unit-testable. No test file exists. Key paths lacking coverage: (1) cohort fallback when <2 regional peers, (2) breakoutYm detection across non-contiguous months (months with no cohort data are excluded from `withExp`, so two over-threshold months may not be consecutive in the array even if consecutive in calendar), (3) baseline anchoring when cohort starts later than the store. Not blocking for demo code, but these branches are invisible without tests.

info

Missing `anomaly.ts` tests acceptable for demo code — stale assertion is the only blocker

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/anomaly.ts

`anomaly.ts` operates on a static STORES fixture with no side effects; failures are visible in the UI chart. Not blocking for pre-production demo code. The stale test in `findingView.test.ts` is the only active CI failure.

info

ConsumptionFindingContent and comparativo/page call analyzeStore independently — no redundancy

apps/platform/src/app/[locale]/(dashboard)/energia/_components/ConsumptionFindingContent.tsx:33

These components are never co-rendered in the same React tree (one is a detail drawer, the other a separate route). Each `useMemo` is scoped to its own instance. Correct design.

improvement4

medium

`cohortSize` computed with wrong semantics (all-tariff) vs `a.cohortSize` (DiD peers)

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:54

Line 54-57 computes `cohortSize` as all stores sharing the tariff (includes the selected store). `a.cohortSize` (returned by `analyzeStore`) is the actual DiD peer count (excludes selected store, may narrow to region). Line 131 uses the wrong-semantic `cohortSize` while the KPI at line 162 uses the correct `a.cohortSize`. Replace the useMemo and all usages with `a?.cohortSize`.

low

`store` useMemo duplicates `a.store` — one extra linear scan of STORES

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:52

`analyzeStore()` already returns `{ store, ... }` so `a?.store` equals `store` when `a` is non-null. The separate `store = STORES.find(...)` useMemo does a redundant scan on every `storeId` change. Replace `store` with `a?.store ?? null` and drop the extra memo.

low

`shortMonth` constructs a new `Intl.DateTimeFormat` on every call

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/_lib/anomaly.ts:125

`new Intl.DateTimeFormat(...)` is created inside `shortMonth()` on every invocation. Hoist to a module-level constant. `Intl.DateTimeFormat` construction involves locale data lookup; reuse is the documented best practice. Trivial fix, negligible impact at prototype scale.

info

`analyzeStore` called for every cohort store in `ranking` — selected store called twice

apps/platform/src/app/[locale]/(dashboard)/energia/comparativo/page.tsx:74

The ranking useMemo calls `analyzeStore(s.id)` for all tariff-cohort stores, including the selected store already computed as `a`. At current static dataset scale (30-40 stores) this is harmless. No action needed for prototype.

History · 15 commits

  1. 96ca7d7needs attentionincremental0H · 9M · 12L2026-07-25 16:40
  2. 25516f7needs attentionincremental5H · 7M · 8L2026-07-25 03:29
  3. a7f8d64needs attentionincremental0H · 4M · 5L2026-07-25 01:56
  4. 7fc4ef0needs attentionincremental2H · 6M · 8L2026-07-24 21:04
  5. 027e5eaneeds attentionincremental4H · 9M · 8L2026-07-24 20:04
  6. 95a101eneeds attentionincremental3H · 6M · 5L2026-07-24 16:09
  7. 5d0d186needs attentionincremental1H · 3M · 5L2026-07-24 15:36
  8. c3c5121needs attentionincremental2H · 1M · 5L2026-07-24 15:17
  9. ded4e61needs attentionincremental2H · 3M · 9L2026-07-24 14:22
  10. 312a1f4needs attentionincremental1H · 4M · 4L2026-07-24 04:01
  11. b48af56needs attentionincremental1H · 6M · 6L2026-07-24 03:14current
  12. 6d07cc8needs attentionincremental2H · 4M · 5L2026-07-24 00:50
  13. 261b55eneeds attentionincremental5H · 11M · 6L2026-07-24 00:38
  14. 5b8a252needs attentionincremental3H · 6M · 9L2026-07-24 00:19
  15. f29cc5bneeds attentionfull9H · 17M · 11L2026-07-23 23:07