fix/cfe-silent-ok
needs attentionviewing older commit74161b3 · incrementalpre-PRreviewed 2026-07-17 01:18 UTC0H · 3M · 5L · 8I- 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)
- 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+130−9services/utility/bills/cfe/__tests__/unit/handlers/collector-lambda.test.ts+117−1domains/utility/src/cfe-job/__tests__/cfe-job.decisions.test.ts+74−0.claude/rules/cfe-pipeline.md+44−1.branch/scope.md + intent.md+192−298
- 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.
Findings · 14
security2
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.
`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
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.
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.
`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.
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.
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
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`.
`.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.
`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.
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.
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
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.
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.