feat/energia-det
safeviewing older commit40b0c87 · incrementalPR #308reviewed 2026-07-16 14:18 UTC0H · 2M · 3L · 1I- Purpose
- feat/energia-det implements the P1 energy anomaly detection engine for the Energía module — 10 corpus-verified detectors as pure functions writing into utility_contract_findings
- Goal
- Port 10 detectors to pure TypeScript wired into a detection shell; operator script for on-demand runs; nightly sweep Lambda in a separate infra PR
- Sub-goals
- SG-1: 10 pure detectors (billing_error + optimization)
- SG-2: Detection shell — idempotent upsert, never-regress-status
- SG-3: Operator runner script
- SG-4 (this commit): Fix optimistic lock WHERE + counter; widen bill-facts concepts; SQL injection fix; integration tests
- What
- Version predicate added to updateEstimate/updateStatus WHERE clauses; shell counter fix; CONCEPT_NAMES widened (kw/totalBill/dap); integration tests for optimistic lock and bill-facts extraction; SQL injection fix in detect-findings.ts
- Why
- Optimistic lock WHERE clause lacked version predicate (silently inoperative); detect-findings.ts used sql.raw (injection vector); bill-facts concept extraction untested at integration level
- Areas
- domains/utility+4248−1packages/database+174−0apps/platform+62−2scripts/energia+181−0docs/specs+122−0packages/api+1−1domains/core+1−1
- Blast
- 26 files, +4789 / -5 lines; utility domain + operator script; no schema change
Findings · 7
correctness1
Lock-lost updates not reflected in result counters
domains/utility/src/finding/finding.shells.ts
When updateEstimate returns null (lock rejected), the shell skips updated++ silently. Not data corruption — a monitoring transparency gap.
security1
SQL injection fix is correct and complete
scripts/energia/detect-findings.ts
sql.raw replaced with sql.join parameterized bindings. Each org ID is a bound parameter. No residual raw interpolation.
tests3
Inner beforeAll has no explicit timeout (Vitest default 5s)
domains/utility/src/finding/__tests__/finding.integration.test.ts:689
The nested beforeAll inside fetchBillFactRowsForOrg uses Vitest 5s default. Add beforeAll(async () => { ... }, 30_000).
uniq('TAR').toUpperCase() is a no-op
domains/utility/src/finding/__tests__/finding.integration.test.ts
uniq() returns uppercase; .toUpperCase() is redundant and implies case normalisation that isn't actually tested.
Missing edge case: kwhPerDay when periodDays is 0
domains/utility/src/finding/__tests__/finding.integration.test.ts
No test for identical periodStart/periodEnd. If SQL divides by period length, result would be NULL or an error.
improvement2
IN clause: prefer ANY(array) single binding over sql.join loop
scripts/energia/detect-findings.ts
sql.join builds N fragments. Drizzle accepts JS array as one binding: WHERE s.org_id = ANY(${orgs.map(o => o.id)}::uuid[])
publicId uses Math.random() while randomUUID() is imported on next line
domains/utility/src/finding/__tests__/finding.integration.test.ts
Use randomUUID() consistently to eliminate Math.random() + slice(0,30) truncation.