← all branches

fix/cfe-silent-ok

needs attentionviewing older commit
74161b3 · incrementalpre-PRreviewed 2026-07-17 01:18 UTC0H · 3M · 5L · 8I
The branch
Purpose
Fix BAT-272: a transient Mi Espacio registration failure was byte-identical to a genuinely empty account — both graded `completed` with 0 bills. 112/2,503 prod runs (4.5%, 80 distinct RPUs) were silently mis-graded.
Goal
Recoverable Mi Espacio failure retries via SFN and grades `failed`; if Gobierno/cache still contributed it grades `partial_success`; genuine empty accounts remain `completed`.
Sub-goals
  • SG-1: Repair the recoverable gate + make registration-failure distinguishable (complete — f791fd79)
  • SG-2: Pin the finalize grading + document the lever (complete — 8e2e8085)
  • SG-N: Fold framework learnings into framework files (pending)
The changes (whole branch)
What
Added `recoverable()` error constructor (sets name='RecoverableCollectionFailure' for SFN Retry matching), exported pure `decideMiEspacioFailureAction` routing decision, fixed the `!gobOk` gate (now counts periods not objects), fixed `attempted` flag, added run-level RPU_REGISTRATION_FAILED failure entry for the degrade path, 8 unit tests + 3 regression pins on decideFinalizeJob.
Why
The old gate asked `!gobOk` — wrong because Gobierno succeeds with zero bills for RPUs that depend on Mi Espacio registration. Needed to count contributed periods, not presence of a discovery object. Also: both portals threw bare `new Error()` which the SFN Retry cannot match by name.
Areas
services/utility/bills/cfe/src/handlers/collector.lambda.ts+1309services/utility/bills/cfe/__tests__/unit/handlers/collector-lambda.test.ts+1171domains/utility/src/cfe-job/__tests__/cfe-job.decisions.test.ts+740.claude/rules/cfe-pipeline.md+441.branch/scope.md + intent.md+192298
Blast
6 files, +557/-310 lines total. Production change is collector.lambda.ts only (+130/-9). No CDK/SFN/wire-schema change. All other files are tests, docs, or branch tracking.
no-cdk-change no-wire-schema-change lambda-only
typecheck· tsc --noEmit clean per intent.mdunit-tests· 14/14 pass (6 pre-existing + 8 new); +8 tests, 0 new failures per intent.mdlint· 0 errors; 116 pre-existing no-console warnings, none from new codeci· No open PR; no GitHub CI run availablecoderabbit· No .coderabbit.yaml present

Findings · 14

security2

info

miFailureReason (CFE error text) logged to CloudWatch and SFN execution history

services/utility/bills/cfe/src/handlers/collector.lambda.ts:207

The raw CFE portal rejection message is included in console.warn logs and in the thrown RecoverableCollectionFailure error (propagated to SFN execution history). Both are internal to the AWS account, operator-facing only. No PII — CFE's error strings are generic error codes and totals. No issue; logging is appropriate for incident diagnosis.

info

`period: ''` in degrade failure entry poses no injection risk

services/utility/bills/cfe/src/handlers/collector.lambda.ts:514

Downstream consumers use failures[].period only for Set membership and .length counting. No code path builds S3 keys, SQL queries, or file paths from failures[].period. The empty string is harmless.

conventions5

medium

Multi-paragraph docstring on `decideMiEspacioFailureAction` violates comment conventions

services/utility/bills/cfe/src/handlers/collector.lambda.ts:463

The JSDoc spans 24 lines across 4 paragraphs. Per conventions, comments should capture only the non-obvious WHY in one short line. The key insight (the !gobOk vs period-count distinction) is already in `cfe-pipeline.md` § BAT-272, the test descriptions, and the inline comments. A two-sentence summary would suffice; the rest duplicates what those other docs say.

low

Split `degrade` action across ~130 lines may confuse readers

services/utility/bills/cfe/src/handlers/collector.lambda.ts:212

Two separate `if (miAction.action === 'degrade')` blocks: the warn at lines 212-217 and the failure push at lines 343-345. The split is structurally necessary (unresolvedFailures doesn't exist until line 316), and the second block has an explanatory comment. A brief 'failure push at line 343' note at the first block would close the readability gap.

low

`period: ''` run-level sentinel is an unnameable magic value

services/utility/bills/cfe/src/handlers/collector.lambda.ts:514

The empty string is the deliberate run-level sentinel meaning 'no single billing period'. It's documented in `cfe-pipeline.md` and the inline comment, but used in three places (code, rule doc, tests) without a shared name. A `RUN_LEVEL_PERIOD = ''` constant would make the intent self-documenting.

info

MiEspacioFailureAction discriminated union shape is correct for a routing decision

services/utility/bills/cfe/src/handlers/collector.lambda.ts:458

Using Result<T,E> would be wrong here — this is a routing decision, not a failure. The three-way union with action as discriminant follows FCIS decision type convention. Correct shape.

info

Wire-level type invariant honored — no fields reshaped

services/utility/bills/cfe/src/handlers/collector.lambda.ts:344

The fix pushes an entry to the existing failures[] array using the existing shape (period: string, code: string, message: string). No new fields on CollectorOutput or SFN payload. period: '' validates against z.string() with no min. canonical-form.md audit satisfied.

tests5

medium

Handler-layer `attempted` wiring is untested

services/utility/bills/cfe/src/handlers/collector.lambda.ts:377

The expression `attempted = allDiscovered.length > 0 || miFailureReason !== null` lives in the imperative shell and is not covered by any unit or integration test. If it regresses (e.g. `&&` instead of `||`), a Mi-Espacio-only failure with zero discovered bills would silently not increment `periodsFailed`, defeating the BAT-272 fix at its most critical seam. The pure-function tests cover `decideMiEspacioFailureAction` but not the point where its output is folded into `attempted`.

medium

`.catch(→null)` exception path not exercised by name in tests

services/utility/bills/cfe/__tests__/unit/handlers/collector-lambda.test.ts:88

The second `miFailureReason` trigger — `miDiscovery === null` from the `.catch(→null)` at line 168 — is documented in intent.md as the 'quieter second trigger the issue missed'. All 8 unit tests pass a non-null `REASON` constant to `decideMiEspacioFailureAction`. The logic is covered indirectly (null vs non-null is the only branch), but passing 'Mi Espacio discovery failed (see preceding warn)' explicitly as miFailureReason in one abort test would make this path visible and prevent the string from being quietly changed.

low

`degrade` OR-boundary: no test where only gob has ONE period (not three)

services/utility/bills/cfe/__tests__/unit/handlers/collector-lambda.test.ts:127

Abort requires BOTH gobPeriodCount===0 AND cachedPeriodCount===0. Test 4 uses {gob:3, cached:0} → degrade. A future regression that changes the condition to `||` instead of `&&` would not be caught. Adding a test with {gob:1, cached:0} → degrade vs {gob:0, cached:0} → abort would pin the AND boundary explicitly.

low

No test covers the shell path: handler → decideMiEspacioFailureAction → periodsFailed increment

services/utility/bills/cfe/__tests__/:1

Unit tests cover the pure function; decideFinalizeJob tests cover the grading contract. But no test exercises the full shell wiring: handler() → attempted flag → RecoverableCollectionFailure thrown → periodsFailed++. The scope.md justification ('prod repro is transient') is sound for a live-CFE test, but a lightweight mock that stubs discoverMiEspacio/discoverGobierno could cover the handler path without credentials.

info

decideFinalizeJob BAT-272 pins are well-shaped

domains/utility/src/cfe-job/__tests__/cfe-job.decisions.test.ts:578

Tests 1 and 2 differ only in periodsFailed (0 vs 1) with identical other inputs — minimal delta that proves the branch without over-specification. Test 3 correctly exercises partial_success. No gaps in these pins.

improvement2

low

Final `null` fallback in `miFailureReason` ternary is unreachable dead code

services/utility/bills/cfe/src/handlers/collector.lambda.ts:198

The ternary ends with `: null` as a catch-all, but user_error is excluded by the early return at lines 176-178, and all other MiDiscoveryResult variants are covered by the preceding branches. Simplifying to `miDiscovery === null ? '...' : miDiscovery.message` makes exhaustiveness locally obvious without requiring readers to trace the early-return guard.

info

Degrade warn logs period counts but not period values

services/utility/bills/cfe/src/handlers/collector.lambda.ts:212

Logging gobOk?.bills.map(b=>b.period) alongside the counts would accelerate incident diagnosis for the degrade path by confirming which periods Gobierno contributed. Existing counts are adequate for most investigations; this is a convenience gap.

History · 4 commits

  1. d7bc0c1safeincremental0H · 1M · 1L2026-07-22 23:35
  2. 87450fbneeds attentionincremental0H · 7M · 5L2026-07-22 16:34
  3. 74161b3needs attentionincremental0H · 3M · 5L2026-07-17 01:18current
  4. 662c8d1safefull0H · 0M · 0L2026-07-17 01:01