workflow-fix: gotchas — batched np.linalg.solve dies on ONE singular slice
Highlight text on any card (body, plan, or an event) to anchor a comment, or leave a whole-task comment here. Mention @claude to summon a reply.
No comments yet.
Overview / Motivation
Auto-filed by the workflow-fix-on-bug protocol from a gotcha_candidate: yes failure-lesson raised on task #1739 (emitting agent: experiment-implementer, r10; orchestrator routed per Step 7 failure-lesson action 3).
Goal
Add a gotchas bullet: numpy batched np.linalg.solve raises ONE LinAlgError for a whole (B,k,k) stack when ANY single slice is singular — wrap batched stacked solves in a batched-first + per-slice pinv fallback with a persisted degenerate flag, never a scale-absorbed ridge jitter or a silent placeholder.
Workflow gap
- Bug observed: A single collinear/constant 3-feature cell killed a 25.2h batched fits grid on task #1739 (src/explore_persona_space/experiments/issue_1739/arms.py run_cell_multi,
beta = np.linalg.solve(ata, atb)on a (Ly,3,3) stack); the trap generalizes to every batched stacked solve in analysis code and is not documented in gotchas.md. - Why it is a workflow gap: gotchas.md is the codebase-trap registry the implementer/planner load on-demand; the batched-solve single-singular-slice semantics (and the float64 scale-absorption of small additive jitter) is a recurring numerics trap the vectorize-first discipline actively steers code INTO (batched solves are the prescribed shape), so it belongs in the registry with the proven fallback recipe.
- Confidence (emitter): high
- verified-at-filing:
grep -in "singular\|batched" .claude/rules/gotchas.md→ 0 hits for a batched-solve singular-slice entry (2026-07-30). Fix recipe + incident record: task #1739epm:failure v1(2026-07-30T05:02:03Z),epm:failure-lesson v1(05:35:07Z), fix commitaff188af67dfon branch issue-1739 (per-slice pinv fallback, array_equal healthy-path pin); agent memory landed on main at1c577f1105(.claude/agent-memory/experiment-implementer/feedback_batched_solve_singular_slice_pinv_fallback.md — the gotchas bullet can largely transclude it).
Proposed change (candidate diff sketch — refine in planning)
.claude/rules/gotchas.md
+ - **Batched np.linalg.solve dies on ONE singular slice.** numpy's stacked
+ solve raises a single LinAlgError for the whole (B,k,k) batch when any
+ slice is singular (one degenerate cell killed a 25.2h grid, #1739). A
+ small additive ridge jitter (1e-8*I) does NOT protect — feature scale
+ absorbs it in float64. Recipe: batched solve first; on the raise,
+ re-solve slices individually with pinv for the singular ones and persist
+ a per-cell degenerate flag (never a silent placeholder). Worked impl:
+ experiments/issue_1739/arms.py _solve_stacked_normal_eqs (#1739 r10).
Scope / surfaces
- Primary target:
.claude/rules/gotchas.md(one bullet; check LESSONS.md index wording needs no change — gotchas.md is already indexed).
Constraints / invariants
- Workflow-surface only.
scripts/workflow_lint.pyno-flags run passes (lessons-index check unaffected — no new rule file). - This session runs under
EPM_WORKFLOW_FIX_SESSION=1and carries aworkflow_fix_target:Provenance line — recursion guard applies.
Provenance
- workflow_fix_target: .claude/rules/gotchas.md
- fingerprint: 86ef744e58d3
target_file: .claude/rules/gotchas.md bug_observed: A single collinear/constant 3-feature cell killed a 25.2h batched fits grid on task 1739 (arms.py run_cell_multi); the trap generalizes to every batched stacked solve in analysis code and is not documented in gotchas.md. why_workflow_gap: gotchas.md is the on-demand codebase-trap registry; the batched-solve single-singular-slice semantics + jitter scale-absorption is a recurring numerics trap the vectorize-first discipline steers code into. proposed_change: Add a gotchas bullet documenting the trap + the batched-first/per-slice-pinv/degenerate-flag recipe. confidence: high related_task: #1739