Skip to content

chore(backlog): TODO.md orchestration round 4 — category resolution, DB harness, ratchet - #137

Draft
EraPartner wants to merge 39 commits into
mainfrom
claude/vision-backlog-orchestration-vopa7d
Draft

chore(backlog): TODO.md orchestration round 4 — category resolution, DB harness, ratchet#137
EraPartner wants to merge 39 commits into
mainfrom
claude/vision-backlog-orchestration-vopa7d

Conversation

@EraPartner

Copy link
Copy Markdown
Owner

Summary

Continues working the TODO.md backlog queue (round 4). First commit closes out a bookkeeping gap from the previous round: the search-abort/min-length finding was fixed and stamped in #136 but its checkbox was left unticked — the fix was verified present in the tree (SERVER_SEARCH_MIN_LENGTH in VirtualDataTable.tsx plus its tests) before ticking. Further findings from the queue will land on this branch as they are implemented and verified, one commit per fix plus a bookkeeping commit stamping each item.

Type of change

  • chore / ci / test — tooling, build, or tests

Checklist

  • Conventional Commit title (type(scope): subject)
  • No leftover console.log / debug code
  • Tests pass (bun run test) and lint is clean (bun run lint) — TODO.md-only so far; will be verified per code commit
  • Types check (bun run typecheck) — same
  • Affected docs/ pages updated — n/a so far
  • i18n — n/a so far

Architecture & data

  • No architectural change so far — any schema-touching fix will include an Alembic migration and rollback plan in its own commit
  • No API change so far
  • No schema change so far

Verification

Bookkeeping commit only so far: verified the ticked finding's fix exists in the current tree (apps/frontend/src/components/shared/VirtualDataTable.tsx:34 and its test file) before ticking the box. Each subsequent fix commit will carry its own verification, run against the real-DB test harness where money paths are involved.

Residual risk & follow-ups

The TODO.md queue still holds ~410 open findings; this branch works through them incrementally. Stamps in TODO.md reference pre-squash SHAs from earlier PR branches by established convention.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt


Generated by Claude Code

claude added 3 commits July 28, 2026 11:15
… to the 3-level pattern

Closes the TODO.md finding 'Remaining 2-level category-resolution
surfaces: monthly summary (live + MV), recurring detection, sankey'.
mv_monthly_summary's definition, the monthly live path's CTE column,
recurring detection's scan query, and sankey's category join + category
exclusion all now resolve own -> recipient default -> PRIMARY
recipient's default, matching transactionRepository and the fixed
breakdown/pivot. Migration 0085 drops the stale MV on existing installs
(same drop-and-rebuild-at-boot mechanism as 0045/0084; totals are
output-invariant for the MV's only reader — this aligns the stored
category grain, the user-visible fixes are sankey and recurring
detection).

Pinned by 5 live-DB tests (aliasCategoryResolution.db.test.js; 3 fail
pre-fix) plus SQL-shape guards in the three mock suites. Adversarially
verified on a live PG16: alias chains, primary-with-no-default, and
own-category override all match the already-fixed surfaces; migration
round-trips clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…; file 5 residue findings

Ticked with the fix commit's SHA after independent adversarial
verification. Filed the implementer's and verifier's noticed-but-not-
fixed defects: sankey NULL-unsafe/non-alias-aware exclusions (live-
reproduced income-zeroing), sankey missing is_transfer filter,
transactionRepository id-vs-name precedence mismatch, recurring
detection categoryId/grouping asymmetry, and a stale routes comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

Coverage Report for backend

Status Category Percentage Covered / Total
🔵 Lines 83.38% (🎯 80%) 12254 / 14696
🔵 Statements 81.44% (🎯 78%) 13586 / 16682
🔵 Functions 79.91% (🎯 76%) 2236 / 2798
🔵 Branches 71.91% (🎯 68%) 8796 / 12231
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
apps/node-backend/src/middleware/errorHandler.js 96.22% 94.36% 100% 97.91% 194, 199
apps/node-backend/src/repositories/accountBalanceSql.js 100% 100% 100% 100%
apps/node-backend/src/repositories/infoRepository.js 100% 100% 100% 100%
apps/node-backend/src/repositories/infoRepositoryAverageVsCurrent.js 98.24% 96% 85.71% 97.95% 196
apps/node-backend/src/repositories/infoRepositoryBanks.js 100% 90.47% 100% 100%
apps/node-backend/src/repositories/infoRepositoryForecast.js 98.62% 92.68% 100% 98.51% 378, 574
apps/node-backend/src/repositories/infoRepositoryMonthly.js 100% 93.61% 100% 100%
apps/node-backend/src/repositories/infoRepositoryNetWorth.js 100% 88% 100% 100%
apps/node-backend/src/repositories/infoRepositoryPlanned.js 86.11% 66.15% 100% 92.94% 46, 49-50, 64-66, 119, 157, 159, 174, 300, 301, 302
apps/node-backend/src/repositories/infoRepositoryRecipients.js 98.95% 96.61% 100% 100% 114
apps/node-backend/src/repositories/plannedTransactionRepository.js 78.04% 76.92% 86.66% 85.87% 130, 131, 132, 133, 134, 135, 136, 163, 198-211, 228-229, 342-346, 507, 526-532, 579, 710-715, 787-793
apps/node-backend/src/repositories/splitRepository.js 68.93% 58.49% 75% 70.65% 91, 104-106, 129, 159, 172, 263-277, 443, 457, 556-643
apps/node-backend/src/repositories/transactionRepository.js 93.65% 90.35% 97.29% 94.41% 305, 374-375, 378-379, 382-383, 386-387, 649, 932-933
apps/node-backend/src/services/accountMergeService.js 96.15% 88.23% 100% 97.56% 66, 145
apps/node-backend/src/services/accountService.js 92.3% 88.75% 100% 94.11% 207, 221, 240, 270-272, 294
apps/node-backend/src/services/aggregationRefresh.js 89.47% 83.33% 71.42% 88.23% 46-47
apps/node-backend/src/services/aiChatService.js 85.12% 78.18% 61.53% 88.49% 61, 65, 69-72, 93, 122, 207-214, 450-490
apps/node-backend/src/services/attachmentCleanup.js 75% 100% 100% 75% 27-30
apps/node-backend/src/services/attachmentService.js 6.52% 0% 0% 6.97% 31-75, 87-154
apps/node-backend/src/services/belgianInflationService.js 75.74% 60.93% 82.97% 82.67% 40-41, 92, 102, 114-119, 128, 130, 156-158, 175, 180, 183, 215-219, 223, 227, 229, 246, 269, 284-290, 301-322, 332, 342, 348, 352, 358, 361, 381, 390, 424, 435, 482, 492, 535, 571, 572, 594-597, 614
apps/node-backend/src/services/bulkSelection.js 97.56% 90.9% 100% 97.36% 122
apps/node-backend/src/services/cashForecastInsightService.js 97.36% 90.24% 100% 100% 104
apps/node-backend/src/services/crossWorkspaceAnalytics.js 100% 80.85% 100% 100%
apps/node-backend/src/services/crossWorkspaceDataService.js 100% 72.22% 100% 100%
apps/node-backend/src/services/dataImportService.js 89.65% 82.69% 100% 90.36% 45, 109, 152-153, 190, 199-200, 229-230
apps/node-backend/src/services/dbEditor.js 85.47% 64.93% 94.28% 85.84% 167, 244, 248, 253-261, 315, 344-345, 358, 360, 400-403, 409, 412, 423, 429, 456, 466, 472, 476, 545, 581, 651-653, 659-665, 677-683
apps/node-backend/src/services/deduplication.js 100% 76.92% 100% 100%
apps/node-backend/src/services/marketLookupService.js 95.23% 87.87% 100% 98.21% 53, 55, 226, 229, 237, 386
apps/node-backend/src/services/materializedViewService.js 95.08% 93.75% 86.66% 94.91% 39, 243, 282
apps/node-backend/src/services/plannedExecutionService.js 95.65% 95.45% 50% 100% 76
apps/node-backend/src/services/plannedMatchService.js 90.21% 78.87% 100% 98.59% 54, 80, 85, 86, 98, 131, 180-184, 204, 209
apps/node-backend/src/services/portfolioImportBatchService.js 42.85% 24% 50% 40% 38-67
apps/node-backend/src/services/portfolioPerformanceSnapshotService.js 100% 100% 100% 100%
apps/node-backend/src/services/priceProviderService.js 81.08% 70.46% 90.9% 83.82% 98, 127-128, 199-208, 252, 254, 260, 262, 266-267, 310, 320, 342-344, 347-369, 374, 401-402, 413-414, 430-431, 437-438, 445-446, 474, 478, 502-503, 540, 562
apps/node-backend/src/services/providerHealthService.js 77.04% 94.11% 56.52% 78.18% 32, 52-88, 101-111, 120
apps/node-backend/src/services/quoteBackfillService.js 83.33% 79.45% 87.5% 86.72% 176, 192, 198, 202, 210, 213, 360, 390, 408, 415, 448, 503, 527-569, 586, 612-616, 649-652, 660-663, 715-718
apps/node-backend/src/services/recipientClusterService.js 0% 0% 0% 0% 16-85
apps/node-backend/src/services/recipientPatternService.js 31.91% 35.05% 35% 34.45% 49-52, 59, 60, 78, 85-95, 102-103, 130, 143, 146, 155-277, 296-362, 397, 413-431
apps/node-backend/src/services/reconcileService.js 96.55% 87.5% 100% 100% 90
apps/node-backend/src/services/recurringDetectionService.js 82.6% 64.51% 91.66% 88.88% 101, 126-141, 150, 158, 159, 160, 168, 271, 280, 288, 297, 300, 316, 367-368
apps/node-backend/src/services/routeManifest.js 93.1% 79.31% 100% 100% 43, 49
apps/node-backend/src/services/tagService.js 100% 100% 100% 100%
apps/node-backend/src/services/transactionBulkService.js 100% 100% 100% 100%
apps/node-backend/src/services/transactionExport.js 86.88% 73.46% 93.75% 89.09% 97, 239, 254-259
apps/node-backend/src/services/transactionService.js 100% 100% 100% 100%
apps/node-backend/src/services/transferReconciliationService.js 94.04% 88.67% 94.44% 95.83% 52-53, 203
apps/node-backend/src/services/calculations/aggregation/sankey.js 89.36% 70% 87.5% 89.13% 135-138, 148-152
apps/node-backend/src/services/calculations/forecast/index.js 81.5% 72.64% 73.91% 83.33% 134-137, 159-162, 183-186, 259, 260, 266-267, 272, 297-316, 413-414, 420-423, 545, 560-580
apps/node-backend/src/services/currency/currencyConversionService.js 89.37% 75.46% 92% 93.01% 163, 193-198, 211, 217, 230-233, 244-249, 312, 314, 355-356, 436, 451, 503, 521, 524, 527, 576, 582, 605, 642
apps/node-backend/src/services/currency/rateFetcher.js 90.99% 85.71% 96.29% 92.7% 57, 138, 255-256, 290, 416-432, 575-578
apps/node-backend/src/services/importPipeline/brokerageRouting.js 100% 90% 100% 100%
apps/node-backend/src/services/importPipeline/commit.js 84.33% 68.51% 100% 85% 83, 117-119, 205-208, 250, 260-263, 273-275
apps/node-backend/src/services/importPipeline/index.js 0% 0% 0% 0% 68-186
apps/node-backend/src/services/importPipeline/match.js 70.12% 50% 45.45% 74.62% 55, 88-93, 99-100, 103-133, 179
apps/node-backend/src/services/importPipeline/stage.js 94.44% 81.25% 100% 100% 98, 114
apps/node-backend/src/services/importPipeline/validate.js 94.73% 77.27% 100% 98.03% 67, 113, 155
apps/node-backend/src/services/importPipeline/adapters/_shared.js 89.47% 81.91% 100% 94.89% 52, 108, 111, 113, 134, 172, 175, 201, 244, 257, 304, 328
apps/node-backend/src/services/importPipeline/adapters/belfius.js 94.56% 77.27% 100% 97.36% 29, 62-63, 81, 142
apps/node-backend/src/services/importPipeline/adapters/bnp.js 93.05% 83.87% 100% 100% 67, 85, 122, 144, 149
apps/node-backend/src/services/importPipeline/adapters/generic.js 85.1% 73.8% 80% 90.24% 62, 65, 77-78, 134, 150
apps/node-backend/src/services/importPipeline/adapters/index.js 83.87% 80% 100% 91.66% 51, 85, 87, 91-94
apps/node-backend/src/services/importPipeline/adapters/ing.js 87.71% 75% 100% 97.91% 46, 59, 62, 93, 113, 118, 121
apps/node-backend/src/services/importPipeline/adapters/kbc.js 96.2% 86.3% 85.71% 96.82% 32, 112, 115
apps/node-backend/src/services/importPipeline/adapters/revolut.js 94.52% 74% 100% 98.27% 37, 62, 75, 123
apps/node-backend/src/services/importPipeline/adapters/sabb.js 85.18% 75% 66.66% 88.63% 34, 42, 49, 91-93, 116
apps/node-backend/src/services/importPipeline/adapters/vision.js 95.83% 81.81% 100% 97.43% 23, 113
apps/node-backend/src/services/importPipeline/adapters/wise.js 87.01% 74.75% 80% 93.54% 24, 67, 72, 91, 93, 129-133, 156
apps/node-backend/src/services/info/cache.js 84.12% 80% 100% 84.74% 101-111, 130
apps/node-backend/src/services/info/liveSummary.js 75% 75% 100% 75% 52-56
apps/node-backend/src/services/info/performanceHelpers.js 66.66% 58.33% 57.14% 65% 103-107, 154, 174-188
apps/node-backend/src/services/portfolioImportPipeline/commit.js 88.79% 67.02% 100% 92.38% 135, 163-165, 173, 193-195, 219, 256-263, 287
apps/node-backend/src/services/portfolioImportPipeline/index.js 94.11% 90.9% 75% 94.11% 145, 184
apps/node-backend/src/services/portfolioImportPipeline/matchInvestments.js 95.94% 86.27% 100% 98.48% 58, 131, 174
apps/node-backend/src/services/portfolioImportPipeline/portfolioGenericAdapter.js 88.57% 68.62% 100% 96.55% 67, 77, 91, 147
apps/node-backend/src/services/portfolioImportPipeline/portfolioTypeNormalizer.js 100% 100% 100% 100%
apps/node-backend/src/services/portfolioImportPipeline/stage.js 9.09% 23.4% 16.66% 10.71% 66-128
apps/node-backend/src/services/portfolioImportPipeline/validate.js 75% 47.88% 100% 78.65% 81, 112-117, 141, 145-148, 172, 189-196, 204-205, 208, 210, 230-231
apps/node-backend/src/services/prices/priceCache.js 91.97% 92.79% 95% 95.37% 108, 142, 260, 274-279, 281, 287-303, 325, 330
apps/node-backend/src/services/prices/priceProviderRegistry.js 86.86% 79.64% 100% 92.12% 56, 69, 72, 142, 180, 207-210, 218, 240, 284, 314, 357, 385, 392, 394, 438, 447, 473, 489, 520, 534, 556, 563, 594-596, 600-601, 609, 634-635, 646-647, 655-656, 660
apps/node-backend/src/services/tax/deductionCandidatesService.js 100% 91.66% 100% 100%
apps/node-backend/src/services/tax/deductionClassifier.js 100% 100% 100% 100%
apps/node-backend/src/types/rows.js 100% 100% 100% 100%
Generated in workflow #605 for commit 2438281 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 59.4% (🎯 57%) 8478 / 14272
🔵 Statements 57.21% (🎯 54%) 9334 / 16314
🔵 Functions 49.6% (🎯 47%) 2527 / 5094
🔵 Branches 47.33% (🎯 45%) 6366 / 13449
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
apps/frontend/src/lib/api/imports.ts 69.86% 64.7% 72.22% 71.42% 54-56, 91, 138, 144-169, 229-239
Generated in workflow #605 for commit 2438281 by the Vitest Coverage Report Action

claude added 25 commits July 28, 2026 14:29
…suites (64 live-DB tests)

Completes the migration list from the TODO.md harness finding: banks,
forecast, monthly, the infoRepository barrel, and recipients now run
their SQL against the real schema (advisory-lock serialized, self-skip
without TEST_DATABASE_URL; mock suites kept per the increment-2
precedent). The monthly suite executes BOTH the generate_series live
path and an actually-created mv_monthly_summary, proving live-vs-MV
equivalence.

The real DB exposed 9 mock-masked discrepancies, each pinned by a
clearly-marked 'PIN:' test asserting current behavior (filed as
findings in TODO.md, not fixed here): raw pg Date leakage on bank
first/last_transaction, cross-currency summation before FX conversion,
manual-only accounts missing from balance history, forecast ignoring
transfer exclusion (ADR-083), populated-months-only average
denominators (forecast + avg-vs-current), latest-vs-historical FX rate
inconsistency in top merchants, alias pivot label/spend mismatch, and
a net-worth chart step for manual-only accounts.

3247 pass with DB (deterministic x3), 3108 pass / 139 skip without.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…ile 12 findings from pins

The harness item's DECIDED migration list is fully delivered across
three increments. Filed the third increment's discoveries: nine pinned
money-path discrepancies (banks date leakage / cross-currency
summation / manual-only history gap, forecast transfer inclusion and
denominator, recipients FX inconsistency and alias-pivot mismatch,
avg-vs-current denominator, net-worth manual-only step), the latent
planned-month local/UTC mix, and a minor-residue rollup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…47 errors to 0)

Third checkJs-ratchet increment: importPipeline (17 files), portfolio-
ImportPipeline (7), prices (3), info (3), currency (2), tax (2) are
annotated to zero implicit-any errors and added to the ratchet's
directory-prefix list, so new files under them are ratcheted from
birth. 34 service files annotated; 6 new pg-accurate table-row
typedefs in src/types/rows.js verified column-by-column against the
Alembic migrations (NUMERIC/BIGINT as strings, DATE as Date), plus
derived shapes incl. ParsedBankTransaction — the contract all 9 bank
CSV adapters emit. HINT_SCOPE retargeted to src/services/ so the
next round sees already-clean candidates. Ratchet bite re-proven
(TS7006 + exit 1 on a removed annotation).

Typing surfaced 5 genuine mismatches, typed as current reality and
filed in TODO.md — notably import_batches BIGSERIAL ids served as
string on POST /api/import/csv but number on the review-commit route.

75 ratcheted files clean; typecheck unchanged; tests byte-identical
(3108 pass / 139 skip without DB).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…e 3 typing-surfaced findings

Services/ taken subdirectory-wise: 6 subdirectories ratcheted at 0,
remainder enumerated with per-directory error counts in the LEFT
clause. Filed the batch-id wire-type split, the dual rate_date shapes
feeding buildHistoricalRateIndex, and the snapshot ?? 0 mixed types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…rate 8 money-path suites

First increment of the route-test finding: tests/helpers/routeApp.js
mounts the real router on a throwaway express() app with the
production middleware wired per main.js — requestId, requestMetrics,
json body limit, CSRF guard, ADR-026 wrapResponse, the per-mount
before slot, 404 funneling, and createErrorHandler with injectable
isProduction (making the 5xx-sanitization branch genuinely testable
for the first time). Deliberate omissions (CORS, gzip, app-level rate
limiters, security headers) documented inline with main.js line refs;
the legacy routeHarness.js is header-marked with its gaps.

Migrated: transactions (+8 tests incl. two PINs), the four bulk
suites, transactionsValidationPins, plannedTransactions (real
validateIdParam now runs), importValidationPins (csvUploadError-
Translator on the tested path for the first time). All hand-rolled
mockRouter/mockResponse and vi.mock('express') removed from these 8.
Newly exercised for real: CSRF 403s, 404 envelopes, id-param 400s,
CSV/NDJSON export headers.

Pinned (filed in TODO.md, not fixed): createErrorHandler only honours
err.status on AppError instances, so body-parser errors surface as
500 — malformed JSON (a 400-class client typo) and >1MB payloads
(413) both report INTERNAL_SERVER_ERROR, the latter with its reason
hidden in production.

22 route files remain on the legacy harness. Full suite 3117 pass /
139 skip / 0 fail (+9 from baseline, all in migrated files); lint
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…rorHandler HttpError finding

Harness built, 8 of 30 suites migrated, 22 enumerated as LEFT. Filed
the pinned errorHandler bug: body-parser HttpErrors reported as 500
with the 413 reason production-sanitized away.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…he true window

Closes two TODO.md findings from the harness third increment.

Transfer exclusion: all 8 transactions-hitting queries across the four
exported forecast functions now apply the sibling-identical conditional
AND t.is_transfer = false (runtime user_settings read via
getIncludeTransfers, matching infoRepositoryAverageVsCurrent). The 5
planned_transactions overlays deliberately do not — that table has no
is_transfer column (verified against schema; pinned so a consistency
edit can't add a 42703). No forecast query is balance-anchored, so no
carve-out was needed. Also stops ADR-090 trade cash legs double-
counting against the portfolio and transfer legs inventing category
spend in the by-category forecast.

Denominator: the average now divides by months elapsed from the
ledger's first in-window transaction through the last complete month —
empty months inside history count as real zeros (kills the 24x
inflation from sparse history), months before the ledger existed are
not charged (a 3-month-old install divides by 3). The divisor comes
from an unfiltered MIN(date) ledger probe, adversarially hardened: a
never-executed stale planned row cannot stretch it (was a 24x
deflation), and category/recipient/transfer filters move the numerator
only (a filtered-keys divisor flipped an average from -64.17 to +30).
includeTransfers is now a Monte-Carlo cache-key input so toggling the
setting misses the 6h cache instead of serving a wrong-polarity
forecast.

Adversarially verified end-to-end on live PG16 (independent before/
after: trade-leg -5050 to -50; sparse-month 240 to 10; both defect
orientations reproduced then fixed). 3276 pass with DB, 3128/148-skip
without; lint/typecheck/ratchet clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…and SQL-binding residues

Both money fixes landed after adversarial verification with two
verifier-found divisor defects fixed pre-commit. Filed the remaining
noticed items: the APP_TIMEZONE vs CURRENT_DATE month-boundary drift
and the string-interpolated validated integers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…y, wire-safe bank dates

Closes four TODO.md findings from the harness third increment.

Per-currency balances: getBankBalances no longer sums a multi-
currency account's amounts across currencies before converting at one
rate (100 EUR + 100 USD @0.5 now reports 150, not 100). A stamped
statement balance anchors only its own currency's partition; each
partition converts at its own rate and the account total sums after
conversion. Single-currency accounts are byte-identical to before.
COMPUTED_BALANCE_LATERAL itself is untouched, so the hub, reconcile,
and cross-workspace consumers are unaffected.

Gap-free history: both the banks balance-history walk and the net-
worth history walk drop their WHERE balance IS NOT NULL gates, so
manual-only accounts appear in the charts their headlines already
count — no more non-zero headline above an empty chart, no more
overnight net-worth step reported as a monthly gain. Implemented as a
set-based span expansion (no per-day lateral); an opening-row fold
keeps window cost independent of ledger age. Both surfaces pin the
invariant headline == today's chart point, including under MOVING FX
curves — the headline now converts at today's rate like the chart's
last point (adversarial verification caught the last-activity keying
regressing foreign-currency accounts 32%, and a planner merge-join
degradation making the first series shape 27x slower; both fixed —
end-to-end lands at ~2x HEAD while emitting the previously-dropped
accounts' points, ~1.2x per emitted row). Unknown-currency FX lookups
are memoized once per currency per request instead of once per day.

Bank first/last_transaction now serialize as wire date strings, not
raw pg Dates (day-shift east of UTC), matching the declared frontend
type.

Verified: 100+ randomized ledgers cross-checked against independent
brute-force evaluation (every daily point + headline, both surfaces,
incl. moving-rate curves); 3296 pass with DB, 3137/159-skip without;
lint/typecheck/ratchet clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…etted residues

All four landed after two-round adversarial verification (FX-keying
regression and planner degradation fixed pre-commit). Filed: cross-
currency SUM on the four remaining surfaces, the drift-badge
convention mix, future-dated row divergence, the net-worth tracking-
only fallback wrong-sum, and the over-broad mock dispatcher guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…or, name==id categories

Closes three TODO.md findings from the harness/ratchet passes.

Top merchants now aggregate per (recipient, date, currency) and
convert each row at its historical rate before the per-recipient
reduction — the same shape by-year and pivot use, so one purchase no
longer shows three different EUR figures across the recipients page.
Counts, averages, and first/last-seen bounds re-reduce correctly
(verified against a hostile fixture: same-day rows, multi-currency,
alias roll-up; all three surfaces agree).

avg_monthly_spending and avg_daily_spending now share one denominator:
elapsed months from the ledger's first in-window transaction through
the last complete month (the forecast's counting rule applied to this
card's 6-month window, via the same unfiltered ledger probe). Income-
only months no longer halve the spending average; a single busy month
is no longer reported at full weight; young installs are not deflated.
The old pin was non-discriminating (contiguous fixture) and is
replaced by three tests that fail against the old code.

CATEGORY_NAME_SQL and its three copies (sort column, getAll,
getAllWithCount) now resolve the displayed name with the same
precedence as EFFECTIVE_CATEGORY_ID_SQL (own -> recipient default ->
primary default) — an alias with its own default no longer shows its
primary's category name beside the alias's category id. Pinned across
getById/getAll/getAllWithCount/create/update and the category sort.

Adversarially verified on live PG16 (arithmetic re-derived, edge
clamps probed, avg_daily consumers enumerated — none assumed the old
fixed window). 3303 pass with DB, 3137/166-skip without; lint/
typecheck/ratchet clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…esidues, broaden TZ-drift item

All three landed after adversarial verification (no defects; two
comment/test-name notes fixed pre-commit). Filed the MoM latest-rate
inconsistency, the remaining category-NAME sites (export/splits pc-
first, planned 2-level, infoRepositoryPlanned 1-level — verifier-
completed enumeration), and the denominator-fix residues. The filed
TZ-drift finding now names both affected files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
… the canonical resolution

Closes the TODO.md finding 'Remaining category-NAME resolution
defects'. The CSV/NDJSON export (incl. bulk-export) and the owed-
splits export swap their pc-before-rc CASE to the canonical own ->
recipient default -> primary default order; plannedTransaction-
Repository extends from 2 levels to 3 via a shared PLANNED_CATEGORY_
NAME_SQL constant (six query sites now share one definition);
infoRepositoryPlanned's getPlannedExpensesNextMonth goes from 1 level
to 3. planned_transactions carries its own recipient_id + category_id
so the pattern maps 1:1.

One adjacent correction, surfaced by the fix's own tests: planned
search now matches against the resolved display label instead of
ORing c/rc column pairs — adding pc pairs the old way made every row
match its primary's category regardless of what it displays, and the
old form missed pc-categorised rows entirely while matching rows
whose own category overrode the term it hit.

Pinned in the extended aliasCategoryResolution.db.test.js on the
canonical alias-with-own-default topology, asserting name==resolved-id
against transactionRepository rather than hardcoded strings; 5 pins
fail pre-fix, 3 guards pass both ways. 3311 pass with DB, 3137/174-
skip without; lint/typecheck/ratchet clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…e the recipient_name divergence

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
… wire type for batch_id

Closes two TODO.md findings from the supertest and ratchet passes.

Error handler: a non-AppError carrying an integer 4xx status/
statusCode now returns that status instead of 500 — but its message is
echoed only when the error also carries a type from the body-parser
allowlist; every other forwarded 4xx gets a generic per-status phrase,
so an internal error that happens to hold a status can never leak its
wording (the rule is documented in the handler and pinned both ways).
Malformed JSON is now a 400 naming the parse problem and a >1MB body a
413 whose reason survives production mode; loan-schedule validation
errors surface as 400 instead of 500. Codes reuse the existing ADR-026
vocabulary so nothing ripples into the shared types or i18n.

batch_id: both import pipelines' createBatch now return Number(id) at
the stage boundary, so POST /api/import/csv and the review-commit
route emit the same wire type (was string vs number); openapi.yaml and
the frontend types/generated client follow. This also fixes a live
bug: the SSE review_required event relayed the string, which the
frontend's z.number() schema rejects — any import needing review
failed the progress stream. Route pins assert strict equality of
batch_id across the csv and commit responses through the real
createBatch over a primed pg mock.

23 tests added/flipped with fail-against-old proof (12 and 8 failures
respectively on reverted sources). 3334 pass with DB, 3160/174-skip
without, frontend 2120 pass; lint/typecheck/ratchet/endpoint-matrix
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…file two residues

Filed the MSW import-mock shape divergence (root cause of the missed
wire split) and the loanSchedule plain-Error genericization.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…R-083 transfer setting

Closes two TODO.md findings. The hand-rolled exclusion clauses are
replaced by the shared buildExclusionClauses (the pivot/monthly form):
the -1 NULL sentinel means excluding a category no longer silently
erases every uncategorised row (was: excluding one category removed a
EUR 3000 uncategorised income row, rendering Income 0 with money still
flowing out), and the recipient exclusion is now alias-aware. Sankey
also applies the conditional is_transfer predicate governed by the
runtime includeTransfers setting like every sibling aggregation — a
savings transfer pair no longer inflates both sides of the flow graph
(was: +900 fake income plus a phantom 900 spending node).

The characterization test is split into four real pins on live PG16;
fail-against-old shows exactly the filed shapes (income 0 vs 3000,
alias row surviving, 3900 vs 3000). 3338 pass with DB, 3161/177-skip
without; lint/typecheck/ratchet clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
… residues

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…llback, historical MoM FX

Closes three TODO.md findings.

Planned next month: the window is now built entirely from the ADR-009
string helpers (todayAppDateString/firstOfMonthYmd/addDaysYmd) with
month/year parsed from the window start — east-of-UTC hosts (incl. the
default Europe/Brussels) no longer name the previous month beside a
correct period_start. Window arithmetic brute-forced over a decade by
the verifier (leap/century/year boundaries); pinned under forced
TZ=Asia/Tokyo mid-year and across the year boundary.

Net-worth flow fallback: rows positively attributed to an
in_net_worth=false account are excluded from both fallback CTEs via
NOT EXISTS — an all-tracking ledger reports 0 instead of the tracking
accounts' running total — while unattributed rows (the un-migrated
shape the fallback exists for) stay counted, pinned via the
UPDATE-relabel construction since the sync trigger auto-creates
accounts on INSERT. Verifier probes confirmed the predicate matches
the walk's resolution exactly (FK/NOT NULL make the edge cases
unrepresentable) and caught the multi-currency variant (a tracking-
only USD row polluting the FX-converted sum) — covered.

Recipients MoM: aggregates per (recipient, period, date, currency)
and converts at historical per-date rates like the other three
recipient surfaces (a 2.9x cross-surface disagreement on a stressed
fixture closes to one rounding cent); the reduction uses the Decimal
accumulator to keep the multiplied row count exact. No-DB contract
guard added on the convertRowsToEur options.

Verifier notes folded in pre-commit: the TZ restore no longer writes
the literal string "undefined" when TZ was unset, and the measured
figure in two comments corrected to -143.25. 3344 pass with DB,
3163/181-skip without; lint/typecheck/ratchet clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…TZ, fallback, FX-flag residues

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
Closes the TODO.md finding on expandRecurringOccurrences mixing a pg
local-midnight Date with APP_TIMEZONE strings and UTC comparisons —
on hosts east of the app timezone every occurrence shifted a day back
(a weekly cadence landed 6 days off across a month; a monthly row
picked up a spurious permanent clamp to the wrong day).

The expansion is now pure YMD-string calendar math on the ADR-009
helpers: day cadences step via addDaysYmd, month cadences via a
first-of-month + clamp-to-last-day construction that reproduces the
old sequential clamp cascade byte-for-byte (Jan 31 monthly still walks
Feb 28 -> ... -> Jul 28), and the stale-row fast-forward works in whole-
day diffs. Host-invariance pinned: a TZ x cadence it.each matrix (UTC/
Tokyo/LA, weekly/monthly/clamp-cascade) asserts identical occurrence
days everywhere; fail-against-old reproduces the exact 6-day and
clamp-shift failures under Tokyo. The whole planned suite is green
under UTC, Tokyo, LA, and UTC+14.

3353 pass with DB, 3172/181-skip without; lint/typecheck/ratchet clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…al-stepper residue

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…column

Closes the categoryId half of the recurring-detection finding: the
scan query now selects the same 3-level COALESCE the categoryName is
resolved from, and the pattern emits it — a recipient-default-
categorised pattern no longer reports categoryId null beside a
non-null name. Direct user-visible improvement: creating a planned
payment from a detected pattern now carries the category the panel
displays instead of silently creating it uncategorised
(RecurringDetectionPanel POSTs pattern.categoryId). The alias
grouping/labeling half stays open as a deliberate product decision.

Pinned on live DB (alias topology, fails pre-fix with null) and in
the mock suite (raw null + effective 7 -> categoryId 7). 3355 pass
with DB, 3173/182-skip without; lint/typecheck/ratchet clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
Batch 2a of the supertest migration: accounts, aggregations,
aggregationsForecastBacktest, attachments, categories, crossWorkspace,
recipientBankAccounts, recipients, research, settings, tags, watchlist.
validateIdParam is no longer stubbed anywhere in these suites; 8 new pins
cover the guard now being on-path. The attachments multer stub now drives
req.file through the real middleware chain instead of hand-building it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
claude added 11 commits July 28, 2026 19:58
…s harness

Batch 2b of the supertest migration: admin, ai, marketLookup,
portfolioImport, portfolioImportValidationPins, savedCharts. The admin
auth guard and ai enforceAiChatEnabled gate were unreachable under the
legacy harness and are now genuinely exercised (6 new pins). SSE stream
tests assert the buffered event-stream body instead of a stubbed
res.write array.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…ness

Batch 2c of the supertest migration: import, info, investments, splits.
The import suite's production-sanitization test now actually pins the
sanitized branch via an isProduction routeAgent; multer error paths run
through the real csvUploadErrorTranslator chain via a hoisted uploadState.
One structural test dropped (route-registration check against the mock
router's handler map — no real-router equivalent; both routes keep their
dedicated request tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…info-suite mock residue

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…ss.js

Final batch of the supertest migration: settingsStorage's router-driven
blocks and transactionPatchValidation move to the real-Express harness
(promise-rejection assertions become real 400 envelopes through the error
handler); rateLimiter and validation keep their middleware-unit intent
with small local res stubs; main.test.js sheds a dead express mock it
never invoked. routeHarness.js has zero consumers and is deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…tion complete

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
Top-level services slice of the noImplicitAny ratchet: the 31 files the
gate reported ready are now listed, and 26 more top-level services files
are annotated from 144 implicit-any errors to zero. New multer ambient
shim in src/types/thirdPartyModules.d.ts (imported as a value, so TS7016
fires on the import itself); RecipientMatchPatternRow added to rows.js
against migration 0015. 133 files now gated; bite re-proven.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
Completes the src/services/ top level under the noImplicitAny ratchet
(51/51 files, individual entries — prefix-collapse proven premature since
subdirectories are still dirty). dbEditor typed faithfully to its generic
nature (Record<string, unknown> rows, boundary-cast introspection);
BelgianInflationRateRow verified against migration 0001. 140 files gated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
…re-type residue

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDqYKSgjakVo14SPZXVCJt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants