← all branches

claude/zealous-lovelace-cbc97f

needs attention
df474d4 · incrementalPR #215reviewed 2026-07-22 23:12 UTC2H · 3M · 6L · 1I
The branch
Purpose
Implements the ADR-020 follow-up: schedule a nightly reconcile-and-repair for the utility_contract_overview read-model that backs /bills/contratos and its realtime subscription. The read-model is transactionally consistent by construction (AFTER triggers run in the writing transaction); the only realistic drift source is a trigger-bypass write.
Goal
Add a pg_cron-scheduled SECURITY DEFINER reconcile function that diffs the stored read-model against a full recompute, repairs up to 2000 divergent pairs, logs every run, and alerts loudly on non-zero drift — so drift from any bypass path is caught within 24 hours.
Sub-goals
  • SG-1: utility_contract_overview_reconcile_runs table (append-only audit log with RLS)
  • SG-2: utility_contract_overview_reconcile() SECURITY DEFINER function with FULL OUTER JOIN diff, cap-limited repair, drift logging, pg_notify alert
  • SG-3: REVOKE from authenticated/anon/public; pg_cron schedule guarded on extension presence
  • SG-4: Re-slot migration from 0061 to 0064 after main merges (post-#324)
The changes (whole branch)
What
Adds reconcile_runs audit table, SECURITY DEFINER reconcile function, REVOKE, and pg_cron nightly schedule (guarded). Also adds findOverviewByContractPublicId query (org-access gate for contract chapters detail reads), utilityService filter case in buildRule, and ne(terminated) guard in buildUniverseWhere. Exports utility-contract-findings schema. Re-slots migration from 0061_common_boomerang to 0064_strong_echo after merging main (#324).
Why
ADR-020 identified the reconcile-and-repair job as a follow-up after the read-model triggers were landed. Without it, any trigger-bypass write (bulk load, logical replication applier, pg_restore) would drift the read-model silently until the next feeder write touches the affected RPU. The nightly job closes that gap and provides drift observability.
Areas
packages/database+2171domains/utility/src/utility-contract-overview+601domains/utility/src/utility-contract-overview/__tests__+1200packages/database/src/schema+140domains/utility+164docs/ADRs+50.squawk.toml+20.github/workflows+30scripts/db+10
Blast
13 files, +919/-4. Blast radius is narrow: the migration is additive (new table, new function, REVOKE), the query additions are gated behind org checks, and the terminated-filter change affects only the Contratos list UI surface. No shared cross-domain types changed.
SECURITY DEFINER function — REVOKE applied in same migration Migration re-slot: 0061_common_boomerang → 0064_strong_echo (stale filename in CLAUDE.md + runbook)
ci· GitHub CI rollup unavailable (token scope)coderabbit· No .coderabbit.yaml in repo

Findings · 13

correctness3

medium

CLAUDE.md references old migration filename 0061_common_boomerang.sql

packages/database/CLAUDE.md

The Notable Tables entry for utility_contract_overview_reconcile_runs cites '0061_common_boomerang.sql' as where the out-of-band SQL (reconcile function + pg_cron) lives. That filename no longer exists — it was re-slotted to 0064_strong_echo.sql. A developer following this note to find the SQL or the operational runbook for applying the cron schedule out-of-band will look for a file that doesn't exist.

medium

Runbook references old migration filename 0061_common_boomerang.sql

docs/development/rpu-history-promote-runbook.md

Line 109 refers to 0061_common_boomerang.sql under a Re-slot #215 note. Same stale reference as packages/database/CLAUDE.md — the actual file is now 0064_strong_echo.sql.

low

buildUniverseWhere terminated filter applies to statusCounts — verify lifecyclePhase never diverges from contractStatus

domains/utility/src/utility-contract-overview/utility-contract-overview.queries.ts

Adding ne(t.contractStatus,'terminated') to the universe means a terminated contract is completely invisible to countOverviewByLifecyclePhase. If a contract can be in contractStatus='terminated' while having a non-terminated lifecyclePhase value (the phase is a 3-level resolution that may not track contractStatus 1:1), the sidebar count would silently zero out that phase bucket. The design is intentional per the comment, but the invariant 'terminated contractStatus → all lifecycle phases excluded' should be verified.

security1

low

Sub-ms PostgREST exposure window between CREATE FUNCTION and REVOKE

packages/database/drizzle/0064_strong_echo.sql

The CREATE OR REPLACE FUNCTION and REVOKE EXECUTE are two separate drizzle-breakpoint statements, not atomic. There is a sub-millisecond window where PostgREST could theoretically expose rpc/utility_contract_overview_reconcile to authenticated/anon callers before the REVOKE fires. In practice the reconcile function only writes aggregate counts to reconcile_runs (no data exfiltration risk) and the window is sub-ms. This is a systemic pattern consistent with 0045/0063. If hardening is desired, issue the REVOKE before the CREATE OR REPLACE in a single DO $$ block.

conventions3

medium

runOverviewReconcile throws instead of returning Result<T,E> — ADR-016 violation

domains/utility/src/utility-contract-overview/utility-contract-overview.queries.ts

ADR-016 requires all fallible operations return Result<T,E>; thrown exceptions are banned. runOverviewReconcile throws a bare Error when the SQL function returns no row. All other query functions in this module return Entity | null (for gets) or plain values (for aggregates) — only shells/decisions use Result. This fallible path that throws should return Result<OverviewReconcileSummary, {_tag:'ReconcileInvariantError', statusCode:500}> and propagate to the calling shell. The throw is seen in cfe-job.queries.ts:331 as well (pre-existing pattern), so this is a recurring deviation from ADR-016 worth tracking.

low

runOverviewReconcile JSDoc explains WHAT rather than WHY

domains/utility/src/utility-contract-overview/utility-contract-overview.queries.ts

The 8-line JSDoc on runOverviewReconcile has an opening paragraph describing WHAT the function does (recompute every overview row, diff, log, RAISE WARNING, pg_notify) — derivable from the function name and the SQL it calls. The genuine WHY paragraphs (why it delegates to a SQL function for cron/Lambda parity; why it takes Database not DbOrTx for the temp-table + advisory-lock constraint) are correct and non-obvious. The first paragraph should be removed, keeping only the constraint-explanation paragraphs.

low

schema/index.ts inline comment for reconcile_runs table explains purpose rather than constraint

packages/database/src/schema/index.ts

The two-line comment above the utilityContractOverviewReconcileRuns export ('Append-only audit log for the nightly read-model reconcile-and-repair job (ADR-020 drift follow-up). Backend-only; written by the reconcile function.') describes WHAT the table is, not a non-obvious constraint. 'Backend-only' is the one useful signal but is derivable from the service_role-only RLS policy. Borderline — consistent with the file's existing grouping comment style but technically against the 'only WHY' rule.

tests3

high

findOverviewByContractPublicId has zero test coverage including the cross-org null-return path

domains/utility/src/utility-contract-overview/__tests__/utility-contract-overview.integration.test.ts

The function is the org-access gate in the contract chapters handler (utility-contracts.handler.ts:942). Three cases need coverage: (a) returns null when orgId doesn't match — the cross-org probe → handler notFound security boundary; (b) returns row when orgId matches; (c) returns row for a terminated contract (status-blind by design). None of these cases appear in any test file. Case (a) is the security boundary: wrong-org request must get null to trigger notFound.

high

ne(t.contractStatus, 'terminated') in buildUniverseWhere has no regression test

domains/utility/src/utility-contract-overview/__tests__/utility-contract-overview.integration.test.ts

The guard is the sole thing preventing a terminated-but-still-linked contract (one with a site_utility_contracts row → overview row with contract_status='terminated') from appearing in listOverviewPage, countOverviewByLifecyclePhase, and listOverviewKeys. The derive function does NOT filter by uc.status, so terminated contracts with a live SUC do get overview rows. The existing hTerminated fixture deliberately has no SUC, so the filter never engages in any test. If removed, all three surfaces would silently start including terminated chapters — a billing-critical regression.

info

utilityService empty-values path not explicitly asserted for this specific filter case

domains/utility/src/utility-contract-overview/__tests__/utility-contract-overview.integration.test.ts

The utilityService buildRule case has the positive-filter and unknown-code paths tested. The empty-values guard is in buildRule itself (returns matchesNothing: true for empty values), tested for isMonitored but not explicitly for utilityService. Low risk — the shared guard applies to all multiselect cases.

improvement3

low

SQL exclusion list (to_jsonb - 'id' - 'updated_at') is a manual-sync pair across SQL and TS

packages/database/drizzle/0064_strong_echo.sql

The FULL OUTER JOIN diff expression (to_jsonb(d) - 'id' - 'updated_at') IS DISTINCT FROM (to_jsonb(s) - 'id' - 'updated_at') appears in both the SQL reconcile function (migration line ~106) and the TS reconcileOverview query. These two sites must stay in sync manually. The migration comment acknowledges this ('the SAME FULL OUTER JOIN diff as UtilityContractOverviewFCIS.reconcileOverview'). If a new auto-managed column is added to utility_contract_overview in the future, both sites need updating. Known manual-sync pair — worth noting in a follow-up.

low

findOverviewByContractPublicId status-blind asymmetry not called out at barrel export

domains/utility/src/utility-contract-overview/utility-contract-overview.queries.ts

listOverviewPage/listOverviewKeys/countOverviewByLifecyclePhase all exclude terminated contracts via buildUniverseWhere. findOverviewByContractPublicId is deliberately status-blind (correct — the detail drawer must show terminated chapters). This is well-commented inline, but the overviewQueries barrel object exports both without indicating the asymmetry. A future caller substituting a list call with this one would be surprised. A brief JSDoc mention ('status-blind by design') or a distinct naming convention would prevent misuse.

low

buildUniverseWhere base conditions are embedded inline — invisible to callers reading buildFullWhere

domains/utility/src/utility-contract-overview/utility-contract-overview.queries.ts

The ne(t.contractStatus,'terminated') condition is embedded directly in the initial array in buildUniverseWhere, making it invisible to readers of buildFullWhere which delegates to it. A hypothetical admin-probe query that bypasses buildUniverseWhere would silently lose the terminated-exclusion filter. Low risk today (reconcileOverview and runOverviewReconcile do their own raw SQL), but worth noting if a third query entrypoint is added.

History · 8 commits

  1. df474d4needs attentionincremental2H · 3M · 6L2026-07-22 23:12current
  2. 8b749afneeds attentionincremental0H · 1M · 3L2026-07-14 17:58
  3. 8f01e3bsafeincremental0H · 0M · 0L2026-07-07 16:58
  4. 6fb3b0aneeds attentionincremental0H · 1M · 3L2026-07-06 16:26
  5. b7dc960needs attentionincremental0H · 1M · 4L2026-07-04 02:25
  6. c0927dfneeds attentionincremental0H · 2M · 3L2026-07-03 06:34
  7. 1328ef5needs attentionincremental1H · 4M · 5L2026-07-03 06:26
  8. 4c3d40dneeds attentionfull6H · 15M · 12L2026-07-03 06:05