Skip to content

feat(fx): opt-in FX-FIFO movement trace for audit/diagnostics (#230)#238

Merged
GeiserX merged 2 commits into
mainfrom
feat/fx-trace-audit
Jun 17, 2026
Merged

feat(fx): opt-in FX-FIFO movement trace for audit/diagnostics (#230)#238
GeiserX merged 2 commits into
mainfrom
feat/fx-trace-audit

Conversation

@GeiserX

@GeiserX GeiserX commented Jun 17, 2026

Copy link
Copy Markdown
Owner

What

An opt-in audit ledger of the FX-FIFO engine's internal movements, so a developer or advisor can verify exactly how a Casilla 1633/1637 figure was built — the full acquire → park → unpark → discard → profit → dispose sequence, each row carrying the running pool and parked FCY balances after the event.

Born from issue #230: the reporter was keeping a hand console.log ledger to check the carry-basis FX math. This turns that into a first-class, lossless artifact (and makes the engine mechanically testable via golden-ledger tests).

Design — a developer/advisor tool, never a user feature

  • Zero-cost when off. FxFifoEngine.enableTrace() flips a flag; every record() returns immediately unless enabled. All 32 pre-existing FX tests pass unchanged — the trace never alters a computed casilla (pinned by an e2e test asserting byte-identical figures with/without the trace).
  • Never in the standard UI. The reporter's explicit ask: the FX-FIFO is not shown to normal users. Web exposure is gated behind #debug (or localStorage.declarenta_debug); the wizard/results flow is byte-identical for everyone else.
  • Ignored under monodivisa (skipFx) — the FX engine doesn't run, so fxTrace is gracefully undefined.

Surface

  • Engine (fx-fifo.ts): enableTrace() / getTrace(). New types FxTraceEvent / FxTraceKind (all monetary fields are decimal strings — no Decimal dep in consumers).
  • Report (report.ts): ReportOptions.fxTraceTaxSummary.fxTrace (the full all-year ledger, so a lot's whole lifecycle reconciles).
  • Serializer (generators/fx-trace.ts): serializeFxTrace(trace, "jsonl" | "csv").
  • CLI: --fx-trace [file] / --fx-trace-format jsonl|csv (writes to a file, or stderr so it never corrupts a stdout JSON/CSV payload).
  • Web: a #debug-gated "Descargar traza de cálculo FX" download.
  • Public exports added to src/index.ts; documented in CLAUDE.md.

The discard kind — the audit-visible proof of the loss-sell principle (issue #230)

Dollars spent inside a losing position that never converted to EUR appear in the ledger as discard (no FX realized), never as dispose. That's the mechanical evidence that an un-converted loss-sell books nothing into 1633/1637 (Art. 14.2.e) — exactly the reporter's "hamburger in dollars" principle.

Verified end-to-end

Smoke-tested on a real IBKR multicurrency fixture → a 31-row CSV ledger (park/unpark/profit, running balances, uncovered notes, per-position keys). 42 new tests (engine trace, serializer, e2e golden-ledger reconciliation: Σ dispose.gainLossEur === fxGains.netGainLoss). Full suite 1653 passing, typecheck + typecheck:tests + eslint clean.

Refs #230

Summary by CodeRabbit

Release Notes

  • New Features

    • Added opt-in FX-FIFO movement audit trace for detailed transaction tracking
    • CLI support for exporting traces in JSONL or CSV formats
    • Debug mode with downloadable FX trace export for auditors and developers
  • Documentation

    • Added comprehensive FX-FIFO trace documentation
  • Tests

    • Added test coverage for FX trace functionality

Adds an opt-in audit ledger of the FX-FIFO engine's movements so a developer or
advisor can verify exactly how a 1633/1637 figure was built (acquire → park →
unpark → discard → profit → dispose, each with running pool/parked FCY
balances). Born from issue #230: the reporter kept a hand console.log ledger to
check the FX math — this makes it a first-class, lossless artifact.

- Engine: FxFifoEngine.enableTrace()/getTrace() record each pool/park movement.
  OFF by default and ZERO-COST when off (record() returns immediately unless
  enabled); all 32 existing FX tests pass unchanged. New FxTraceEvent/FxTraceKind
  types (decimal strings, no Decimal dep in consumers).
- Report: ReportOptions.fxTrace -> TaxSummary.fxTrace (full all-year ledger);
  ignored under monodivisa (skipFx). Never changes any computed casilla.
- Serializer: src/generators/fx-trace.ts serializeFxTrace(trace, jsonl|csv).
- CLI: --fx-trace [file] / --fx-trace-format jsonl|csv (file or stderr).
- Web: download gated behind #debug / localStorage.declarenta_debug — NEVER in
  the standard UI (the reporter's explicit ask).
- The 'discard' kind is the audit-visible proof of the loss-sell principle:
  un-converted loss-spent dollars show as discard (no FX), never dispose.

42 new tests (engine trace, serializer, e2e golden-ledger reconciliation:
Sigma dispose gainLossEur === fxGains.netGainLoss). Full suite 1653 passing,
typecheck + typecheck:tests + eslint clean.

Refs #230
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@GeiserX, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 49 minutes and 31 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 08577746-8a91-4ede-bc1b-ee0fff7958a1

📥 Commits

Reviewing files that changed from the base of the PR and between 969f33a and 5f92141.

📒 Files selected for processing (3)
  • CLAUDE.md
  • src/types/tax.ts
  • tests/integration/fx-trace-e2e.test.ts
📝 Walkthrough

Walkthrough

Adds an opt-in FX-FIFO movement audit trace to the tax report engine. FxFifoEngine gains enableTrace()/getTrace() methods that record acquire/dispose/park/unpark/discard/profit events per FIFO operation. generateTaxReport accepts fxTrace?: boolean to activate it. A new serializeFxTrace function outputs JSONL or CSV. The CLI and web UI each expose the feature behind a flag/debug gate.

Changes

FX-FIFO Opt-in Audit Trace Ledger

Layer / File(s) Summary
Trace types: FxTraceKind, FxTraceEvent, TaxSummary.fxTrace
src/types/tax.ts
Defines FxTraceKind, FxTraceEvent (decimal-string monetary fields, conditional dispose-only fields, pool/park balances), and adds optional fxTrace?: FxTraceEvent[] to TaxSummary.
FxFifoEngine trace instrumentation
src/engine/fx-fifo.ts
Adds traceEnabled state, enableTrace()/getTrace() public methods, a record() helper, and emit calls for all six movement kinds at each FIFO operation site; trace buffers reset per processEvents run.
serializeFxTrace (JSONL/CSV) and public re-export
src/generators/fx-trace.ts, src/index.ts
Introduces FxTraceFormat, stable CSV_COLUMNS order, and serializeFxTrace producing header+rows for CSV or one JSON object per line for JSONL; re-exported from the library's public surface.
generateTaxReport opt-in wiring
src/generators/report.ts
ReportOptions gains fxTrace?: boolean; when set and skipFx is false, enables engine trace, captures the result, and conditionally spreads fxTrace into the returned TaxSummary.
CLI --fx-trace and web debug-mode export
src/cli/index.ts, src/web/main.ts
CLI adds --fx-trace [file] and --fx-trace-format options, passes fxTrace into generateTaxReport, writes serialized output to a file or stderr. Web adds isDebugMode() gating on URL hash/localStorage, passes fxTrace: isDebugMode() to report generation, and injects a CSV download button in debug mode.
Engine trace unit tests
tests/engine/fx-fifo-trace.test.ts
Covers default-off behavior, acquire/dispose/park/unpark/profit/discard event contents, uncovered-park null rate, running balance conservation, and strict seq ordering.
serializeFxTrace unit tests
tests/generators/fx-trace.test.ts
Tests JSONL round-trip, null rate, empty/undefined input, default format, CSV header+row structure, empty-cell rendering for absent optionals, and CSV escaping for commas and double-quotes.
End-to-end integration tests
tests/integration/fx-trace-e2e.test.ts
Five scenarios: opt-in absence/presence with stable casilla values, golden-ledger dispose reconciliation to netGainLoss, loss-sell discard-only trace, skipFx graceful absence, and serializeFxTrace JSONL/CSV round-trip reconciliation.
CLAUDE.md documentation
CLAUDE.md
Documents the fxTrace flag, event kinds, serialization formats, CLI flags, web debug gate, skipFx exclusion, and discard-as-loss-sell-proof semantics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • GeiserX/DeclaRenta#123: Extended FxDisposal with lotId/trigger fields in src/engine/fx-fifo.ts and src/types/tax.ts — the same event metadata this PR records into FxTraceEvent.
  • GeiserX/DeclaRenta#169: Introduced skipFx (monodivisa) in ReportOptions — the same gate this PR checks before enabling FxFifoEngine.enableTrace().
  • GeiserX/DeclaRenta#237: Pinned the loss-sell-with-no-EUR-conversion rule and carry-basis docs for casillas 1633/1637 — the exact behavior this PR instruments as discard events in the trace ledger.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main feature: opt-in FX-FIFO movement tracing for audit purposes, directly matching the PR's primary objective and implementation.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fx-trace-audit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.19%. Comparing base (12d1328) to head (969f33a).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #238      +/-   ##
==========================================
+ Coverage   95.14%   95.19%   +0.05%     
==========================================
  Files          52       53       +1     
  Lines        4981     5038      +57     
  Branches     1666     1686      +20     
==========================================
+ Hits         4739     4796      +57     
  Misses        209      209              
  Partials       33       33              
Files with missing lines Coverage Δ
src/engine/fx-fifo.ts 98.04% <100.00%> (+0.30%) ⬆️
src/generators/fx-trace.ts 100.00% <100.00%> (ø)
src/generators/report.ts 96.19% <100.00%> (+0.06%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/engine/fx-fifo-trace.test.ts`:
- Around line 35-39: In the Op type definition, replace all `number` type
annotations that represent monetary amounts, rates, and prices with `string`
across all tuple variants (fund, conv, buy, and sell) to avoid binary rounding
before values are converted to Decimal, ensuring compliance with the coding
guideline requiring Decimal usage for all monetary calculations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b93bba53-f40f-4661-adc9-b0ef659b45d6

📥 Commits

Reviewing files that changed from the base of the PR and between b4576cc and 969f33a.

📒 Files selected for processing (11)
  • CLAUDE.md
  • src/cli/index.ts
  • src/engine/fx-fifo.ts
  • src/generators/fx-trace.ts
  • src/generators/report.ts
  • src/index.ts
  • src/types/tax.ts
  • src/web/main.ts
  • tests/engine/fx-fifo-trace.test.ts
  • tests/generators/fx-trace.test.ts
  • tests/integration/fx-trace-e2e.test.ts

Comment on lines +35 to +39
type Op =
| ["fund", string, number, number]
| ["conv", string, number, number]
| ["buy", string, number] // cost (rate irrelevant — buy parks, never realizes)
| ["sell", string, number, number, number]; // cost, proceeds, saleRate

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid number in the monetary Op DSL.

Op currently models amounts/rates as number, which can introduce binary rounding before values are converted to Decimal. Model these operands as strings instead.

Suggested patch
-type Op =
-  | ["fund", string, number, number]
-  | ["conv", string, number, number]
-  | ["buy", string, number] // cost (rate irrelevant — buy parks, never realizes)
-  | ["sell", string, number, number, number]; // cost, proceeds, saleRate
+type Op =
+  | ["fund", string, string, string]
+  | ["conv", string, string, string]
+  | ["buy", string, string] // cost (rate irrelevant — buy parks, never realizes)
+  | ["sell", string, string, string, string]; // cost, proceeds, saleRate

As per coding guidelines, “Always use Decimal from decimal.js for any monetary calculation; never use JavaScript Number for amounts, rates, or prices.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/engine/fx-fifo-trace.test.ts` around lines 35 - 39, In the Op type
definition, replace all `number` type annotations that represent monetary
amounts, rates, and prices with `string` across all tuple variants (fund, conv,
buy, and sell) to avoid binary rounding before values are converted to Decimal,
ensuring compliance with the coding guideline requiring Decimal usage for all
monetary calculations.

Source: Coding guidelines

…edger test

Review follow-up (PR #238 review, MEDIUM finding): the FX trace is the full
all-year ledger but the casillas are year-filtered, so the golden-ledger
identity is 'Sigma dispose.gainLossEur WHERE date in year === fxGains.netGainLoss'
— summing ALL dispose rows over/under-states the box on a multi-year account.

- Document the per-year reconciliation rule on TaxSummary.fxTrace (types/tax.ts)
  and in the CLAUDE.md trace note.
- Add a multi-year e2e test pinning BOTH directions: all-year sum (450) != year
  casilla (300), and year-filtered sum (300) === casilla. Engine-verified.

Docs + test only; no engine/behavior change. Suite 1656 passing.

Refs #230
@GeiserX GeiserX merged commit 33176d0 into main Jun 17, 2026
5 checks passed
@GeiserX GeiserX deleted the feat/fx-trace-audit branch June 17, 2026 12:32
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.

1 participant