SPIKE v2: Money stored as human-readable Markdown (+ card payment plans and subscriptions) #404

Closed
opened 2026-09-29 05:24:27 +00:00 by kayg · 30 comments
Owner

Owner feedback on the #335 spike (2026-09-29)

  • F1: "the format is not readable at all! can't we store everything in markdown? please think about it! the format needs to be very human readable" — the custom record format is rejected as the canonical form.
  • F3: "we need to plan credit card payments/due dates and subscriptions too. this is an awesome feature that neither app does. and yes, preserve data, never lose."

Spike v2: Money in Markdown

Design and prototype a Markdown representation that a person can read and edit in any editor, and that calternal can parse losslessly:

  • Files a person recognises: Money/<Budget>/Budget.md (categories as headings, monthly assignments), Money/<Budget>/Accounts.md, one Money/<Budget>/<YYYY-MM>.md per month with transactions as list items; splits, notes and attachments as nested items; stable IDs as block IDs (^t-002), the same mechanism Notes already uses; extra fields as inline key:: value properties or frontmatter.
  • Card payment plans and due dates, and subscriptions (recurring charges with next due date, cadence, cancel-by date, price history): model them in the same Markdown and show how they feed the budget (card reserve, upcoming bills).
  • Forward compatibility: unknown lines and properties survive a read/write cycle byte for byte; the writer never reorders or drops what it does not understand.
  • Keep the exact arithmetic of the v1 spike (decimal minor units, derived balances, the 24-month property test) on top of the Markdown parser.
  • Deliver: docs/research/money-markdown.md with the full spec on one page and side-by-side examples (a real month with 60 transactions, splits, a card, a subscription, a transfer), the parser/writer prototype with round-trip tests, and a readability comparison against the v1 format and hledger. The owner reads the spec before any product code.
## Owner feedback on the #335 spike (2026-09-29) - F1: "the format is not readable at all! can't we store everything in markdown? please think about it! the format needs to be very human readable" — the custom record format is rejected as the canonical form. - F3: "we need to plan credit card payments/due dates and subscriptions too. this is an awesome feature that neither app does. and yes, preserve data, never lose." ## Spike v2: Money in Markdown Design and prototype a Markdown representation that a person can read and edit in any editor, and that calternal can parse losslessly: - Files a person recognises: `Money/<Budget>/Budget.md` (categories as headings, monthly assignments), `Money/<Budget>/Accounts.md`, one `Money/<Budget>/<YYYY-MM>.md` per month with transactions as list items; splits, notes and attachments as nested items; stable IDs as block IDs (`^t-002`), the same mechanism Notes already uses; extra fields as inline `key:: value` properties or frontmatter. - Card payment plans and due dates, and subscriptions (recurring charges with next due date, cadence, cancel-by date, price history): model them in the same Markdown and show how they feed the budget (card reserve, upcoming bills). - Forward compatibility: unknown lines and properties survive a read/write cycle byte for byte; the writer never reorders or drops what it does not understand. - Keep the exact arithmetic of the v1 spike (decimal minor units, derived balances, the 24-month property test) on top of the Markdown parser. - Deliver: docs/research/money-markdown.md with the full spec on one page and side-by-side examples (a real month with 60 transactions, splits, a card, a subscription, a transfer), the parser/writer prototype with round-trip tests, and a readability comparison against the v1 format and hledger. The owner reads the spec before any product code.
Author
Owner

Starting Money Markdown spike v2 on job/money-md, based on dev at d0d83a32b41e67b64f6586690ba4946e41878160. I have read #404, #335 and #316 with their comments, the Money research and DESIGN §48. I will add only the requested research spec and prototype under spikes/money-markdown/; no product code.

Starting Money Markdown spike v2 on `job/money-md`, based on `dev` at `d0d83a32b41e67b64f6586690ba4946e41878160`. I have read #404, #335 and #316 with their comments, the Money research and DESIGN §48. I will add only the requested research spec and prototype under `spikes/money-markdown/`; no product code.
Author
Owner

Finding: a monthly subscription due on January 31 advanced to February 28, then incorrectly drifted to March 28. The recurrence now derives each date from the original day; the regression expects March 31 and passes. A second regression showed that set_property treated a payment plan:: example inside an HTML comment as a real field and rejected the edit as a duplicate. The scanner now keeps HTML comments opaque, and the byte-preserving edit test passes.

Finding: a monthly subscription due on January 31 advanced to February 28, then incorrectly drifted to March 28. The recurrence now derives each date from the original day; the regression expects March 31 and passes. A second regression showed that `set_property` treated a `payment plan::` example inside an HTML comment as a real field and rejected the edit as a duplicate. The scanner now keeps HTML comments opaque, and the byte-preserving edit test passes.
Author
Owner

Owner review of spike v2 (2026-09-29): approved, with two changes.

  1. Accounts and categories are Markdown links to their stable block anchors, not @names: [Amex](Accounts.md#^amex) and [Groceries](Budget.md#^groceries). This applies everywhere a reference appears: the transaction line, split::, transfer::, subscription account:: and category::, and the card payment category. The link text is the display name. A rename rewrites the link text only; the anchor is the identity.
  2. The field separator is ", " instead of " | ". Fixed fields: date, amount, account, category. Everything after the fourth separator is payee and memo, which may contain commas. A comma inside a link's text never separates fields. Amounts in files are always plain decimals with no grouping separators (1234.50); the UI formats them per locale. Add property tests: payees with commas, link text with commas and brackets, and round trips.
    Also: a tag such as #weekly on a transaction is an ordinary calternal tag, never added automatically.
Owner review of spike v2 (2026-09-29): approved, with two changes. 1. Accounts and categories are Markdown links to their stable block anchors, not @names: `[Amex](Accounts.md#^amex)` and `[Groceries](Budget.md#^groceries)`. This applies everywhere a reference appears: the transaction line, split::, transfer::, subscription account:: and category::, and the card payment category. The link text is the display name. A rename rewrites the link text only; the anchor is the identity. 2. The field separator is ", " instead of " | ". Fixed fields: date, amount, account, category. Everything after the fourth separator is payee and memo, which may contain commas. A comma inside a link's text never separates fields. Amounts in files are always plain decimals with no grouping separators (1234.50); the UI formats them per locale. Add property tests: payees with commas, link text with commas and brackets, and round trips. Also: a tag such as #weekly on a transaction is an ordinary calternal tag, never added automatically.
Author
Owner

Completed

  • Added the one-page Markdown format proposal and side-by-side Markdown, v1 and hledger examples.
  • Added a standard-library parser/writer prototype. It preserves raw UTF-8 lines, line endings, IDs and unknown content; a stable-ID property edit changes only the existing value.
  • Added readable Budget.md, Accounts.md and 2026-09.md fixtures with 60 transactions, 25 expense categories, two card plans, FX, a split, a transfer, reconciliation, subscription history and unknown fields.
  • Kept this as a research spike. No product code or lockfiles changed.

Files

  • docs/research/money-markdown.md
  • spikes/money-markdown/money_markdown.py
  • spikes/money-markdown/test_money_markdown.py
  • spikes/money-markdown/examples/calternal-markdown/Budget.md
  • spikes/money-markdown/examples/calternal-markdown/Accounts.md
  • spikes/money-markdown/examples/calternal-markdown/2026-09.md

Branch

job/money-md is pushed. Head: 2140aaaf0d8850814faa47e12b7b0f66b317805d.
I merged dev once before the final gates; it was already up to date.

Gate output

cargo fmt --check: exit 0; no output.

cargo clippy --all-targets -- -D warnings (exit 0):

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 13.42s

The first clippy attempt failed because the apps/web/build/ embed directory was absent. I built the frontend from the pinned Bun lockfile, then reran clippy successfully.

cargo test (exit 0; 76 test result summaries; 1,537 passed, 0 failed, 13 ignored):

    Finished `test` profile [unoptimized + debuginfo] target(s) in 136m 53s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 63 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 25.01s
test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.00s
test result: ok. 23 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.77s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.93s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.61s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 49.26s
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.79s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.73s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.88s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 10.40s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.90s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 19.99s
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.11s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 21.10s
test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 9.69s
test result: ok. 16 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.53s
test result: ok. 20 passed; 0 failed; 4 ignored; 0 measured; 0 filtered out; finished in 0.17s
test result: ok. 38 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 12.49s
test result: ok. 41 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.60s
test result: ok. 497 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.44s
test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.21s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.47s
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.73s
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.37s
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.57s
test result: ok. 31 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.78s
test result: ok. 49 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.27s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.06s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.48s
test result: ok. 129 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 97.18s
test result: ok. 17 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.23s
test result: ok. 109 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 62.03s
test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.38s
test result: ok. 44 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 10.38s
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.78s
test result: ok. 33 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 1.34s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.48s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s
test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 13.09s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 1 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 12.19s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 81 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 12.81s
test result: ok. 55 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.45s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 13.52s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

bun run check (exit 0):

$ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
Text sizes use shared role tokens.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/money-md/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

bun run test (exit 0):

 Test Files  123 passed (123)
      Tests  787 passed (787)
   Start at  12:29:39
   Duration  83.47s (transform 53%, environment 17%, import 16%, tests 10%, setup 4%)

Vitest also printed jsdom scrollTo notices and one CSS parse notice; it reported 123 files and 787 passing tests.

Prototype acceptance tests (exit 0):

Ran 8 tests in 0.300s

OK

Existing v1 tests (exit 0):

Ran 6 tests in 0.273s

OK

cargo clean output:

     Removed 13967 files, 10.8GiB total

Known gaps

  • The prototype does not implement full M01–M34 coverage.
  • It edits existing properties only; it does not add or remove properties.
  • Subscription and card-plan logic is prototype arithmetic, not product behavior.

Decisions for owner review

DESIGN §48 did not settle these details. The spec proposes:

  1. Use a format:: money/1 version property, heading anchors and pipe-delimited transaction rows.
  2. Support statement-balance, minimum and fixed card plans. Fixed plans repeat monthly; a clamped month-end date returns to its original day later.
  3. Store subscription price history as repeated dated properties.
  4. Derive upcoming bills by due date; do not post them or assign money automatically.
  5. Preserve raw Markdown and limit the prototype writer to editing existing properties.
## Completed - Added the one-page Markdown format proposal and side-by-side Markdown, v1 and hledger examples. - Added a standard-library parser/writer prototype. It preserves raw UTF-8 lines, line endings, IDs and unknown content; a stable-ID property edit changes only the existing value. - Added readable `Budget.md`, `Accounts.md` and `2026-09.md` fixtures with 60 transactions, 25 expense categories, two card plans, FX, a split, a transfer, reconciliation, subscription history and unknown fields. - Kept this as a research spike. No product code or lockfiles changed. ## Files - `docs/research/money-markdown.md` - `spikes/money-markdown/money_markdown.py` - `spikes/money-markdown/test_money_markdown.py` - `spikes/money-markdown/examples/calternal-markdown/Budget.md` - `spikes/money-markdown/examples/calternal-markdown/Accounts.md` - `spikes/money-markdown/examples/calternal-markdown/2026-09.md` ## Branch `job/money-md` is pushed. Head: `2140aaaf0d8850814faa47e12b7b0f66b317805d`. I merged `dev` once before the final gates; it was already up to date. ## Gate output `cargo fmt --check`: exit 0; no output. `cargo clippy --all-targets -- -D warnings` (exit 0): ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 13.42s ``` The first clippy attempt failed because the `apps/web/build/` embed directory was absent. I built the frontend from the pinned Bun lockfile, then reran clippy successfully. `cargo test` (exit 0; 76 test result summaries; 1,537 passed, 0 failed, 13 ignored): ```text Finished `test` profile [unoptimized + debuginfo] target(s) in 136m 53s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 63 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 25.01s test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.00s test result: ok. 23 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.77s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.93s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.61s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 49.26s test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.79s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.73s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.88s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 10.40s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.90s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 19.99s test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.11s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 21.10s test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 9.69s test result: ok. 16 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.53s test result: ok. 20 passed; 0 failed; 4 ignored; 0 measured; 0 filtered out; finished in 0.17s test result: ok. 38 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 12.49s test result: ok. 41 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.60s test result: ok. 497 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.44s test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.21s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.47s test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.73s test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.37s test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.57s test result: ok. 31 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.78s test result: ok. 49 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.27s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.06s test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.48s test result: ok. 129 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 97.18s test result: ok. 17 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.23s test result: ok. 109 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 62.03s test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.38s test result: ok. 44 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 10.38s test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.78s test result: ok. 33 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 1.34s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.48s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 13.09s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s test result: ok. 1 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 12.19s test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s test result: ok. 81 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 12.81s test result: ok. 55 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.45s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 13.52s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `bun run check` (exit 0): ```text $ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Text sizes use shared role tokens. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/money-md/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run test` (exit 0): ```text Test Files 123 passed (123) Tests 787 passed (787) Start at 12:29:39 Duration 83.47s (transform 53%, environment 17%, import 16%, tests 10%, setup 4%) ``` Vitest also printed jsdom `scrollTo` notices and one CSS parse notice; it reported 123 files and 787 passing tests. Prototype acceptance tests (exit 0): ```text Ran 8 tests in 0.300s OK ``` Existing v1 tests (exit 0): ```text Ran 6 tests in 0.273s OK ``` `cargo clean` output: ```text Removed 13967 files, 10.8GiB total ``` ## Known gaps - The prototype does not implement full M01–M34 coverage. - It edits existing properties only; it does not add or remove properties. - Subscription and card-plan logic is prototype arithmetic, not product behavior. ## Decisions for owner review DESIGN §48 did not settle these details. The spec proposes: 1. Use a `format:: money/1` version property, heading anchors and pipe-delimited transaction rows. 2. Support `statement-balance`, `minimum` and `fixed` card plans. Fixed plans repeat monthly; a clamped month-end date returns to its original day later. 3. Store subscription price history as repeated dated properties. 4. Derive upcoming bills by due date; do not post them or assign money automatically. 5. Preserve raw Markdown and limit the prototype writer to editing existing properties.
Author
Owner

Starting continuation on job/money-md from base 2140aaaf0d. I’ll apply the approved link-reference and comma-separator format to DESIGN §48, the research spec, prototype, fixtures, and property tests, then stop before product UI for the break-the-numbers review.

Starting continuation on job/money-md from base 2140aaaf0d8850814faa47e12b7b0f66b317805d. I’ll apply the approved link-reference and comma-separator format to DESIGN §48, the research spec, prototype, fixtures, and property tests, then stop before product UI for the break-the-numbers review.
Author
Owner

Finding: the v1 arithmetic helper _minor used Decimal under the default 28-digit context. A 30-digit USD value scaled to minor units incorrectly (confirmed at 30, 100 and 500 integer digits). The Money codec boundary test now exercises 300 digits; I changed the helper to exact decimal-string scaling so the existing arithmetic oracle remains exact without changing currency scale rules.

Finding: the v1 arithmetic helper _minor used Decimal under the default 28-digit context. A 30-digit USD value scaled to minor units incorrectly (confirmed at 30, 100 and 500 integer digits). The Money codec boundary test now exercises 300 digits; I changed the helper to exact decimal-string scaling so the existing arithmetic oracle remains exact without changing currency scale rules.
Author
Owner

Finding: the v1 calculator reads a transfer child only for its card-payment path and ignores split children. The spike could therefore return plausible but wrong category balances for a split row. The Markdown projection now rejects split and general-transfer rows that this calculator cannot model; the codec still parses and resolves their stable links. The research page records this limit.

Finding: the v1 calculator reads a transfer child only for its card-payment path and ignores split children. The spike could therefore return plausible but wrong category balances for a split row. The Markdown projection now rejects split and general-transfer rows that this calculator cannot model; the codec still parses and resolves their stable links. The research page records this limit.
Author
Owner

Completed

Built the approved Money/1 Markdown spike and stopped before product UI for the requested break-the-numbers review.

Files changed: docs/DESIGN.md, docs/research/money-markdown.md, spikes/money-markdown/money_markdown.py, spikes/money-markdown/test_money_markdown.py, the three spikes/money-markdown/examples/calternal-markdown/ fixtures, spikes/money-format/budget_math.py, and spikes/money-format/test_money_format.py.

Head: 232702c08c23063c58df9433d8512b466abaf735.

Gates

  • cargo fmt --check: exit 0, no output.
  • Python spike tests: 23 passed (16 Markdown, 7 exact-money); Python compile and git diff --check passed.
  • bun run check:
    Text sizes use shared role tokens.
    Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/money-md/apps/web
    Getting Svelte diagnostics...
    
    svelte-check found 0 errors and 0 warnings
    
  • bun run test:
     Test Files  125 passed (125)
          Tests  800 passed (800)
       Duration  195.83s
    
  • cargo clippy -q --workspace --all-targets -- -D warnings: failed in untouched vendored mail dependency, exit 101. Verbatim diagnostic:
    error: only one of 'runtime-async-std' or 'runtime-tokio' features must be enabled
      --> crates/plugins/mail/vendor/async-imap/src/lib.rs:83:1
    error: could not compile `async-imap` (lib) due to 14 previous errors
    
  • cargo test --workspace: stopped before the test phase at the four-hour job cap while compiling optional workspace plugins. The log ended at Compiling keyring-core v1.0.0; the command was terminated. The shell wrapper then reported zsh:7: read-only variable: status while recording the signal exit, so there is no Cargo test summary.
  • cargo clean: Removed 13588 files, 6.9GiB total. Removed apps/web/build and apps/web/.svelte-kit.

Adversarial round

Ran once. It reported 92 findings: 88 SLOW load findings and four non-SLOW output entries. The Reminders VTODO completion path returned 200 but lost its existing DESCRIPTION; evidence was filed on #393. Three Appearance body-size proxy messages corresponded to two oversized requests; direct server verification returned 413. This proxy false-positive is already tracked by #394. No Money endpoint was added.

Decisions for owner confirmation

  • Split and transfer markers use stable destination category links to fit the four required transaction fields.
  • The canonical link writer escapes ] in link labels.
  • Arithmetic accepts extra fractional digits only when they are zero; the source spelling stays intact. Negative zero reduces to integer zero in arithmetic.
  • The v1 projection rejects split and general transfer transactions that the unchanged calculator cannot calculate correctly; parsing and reference resolution still work.
  • Full Actual M01–M34 and FX coverage remain outside this spike.
  • The dev merge kept Money §48 and the incoming Integrations §49.
## Completed Built the approved Money/1 Markdown spike and stopped before product UI for the requested break-the-numbers review. Files changed: `docs/DESIGN.md`, `docs/research/money-markdown.md`, `spikes/money-markdown/money_markdown.py`, `spikes/money-markdown/test_money_markdown.py`, the three `spikes/money-markdown/examples/calternal-markdown/` fixtures, `spikes/money-format/budget_math.py`, and `spikes/money-format/test_money_format.py`. Head: `232702c08c23063c58df9433d8512b466abaf735`. ## Gates - `cargo fmt --check`: exit 0, no output. - Python spike tests: 23 passed (16 Markdown, 7 exact-money); Python compile and `git diff --check` passed. - `bun run check`: ``` Text sizes use shared role tokens. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/money-md/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` - `bun run test`: ``` Test Files 125 passed (125) Tests 800 passed (800) Duration 195.83s ``` - `cargo clippy -q --workspace --all-targets -- -D warnings`: failed in untouched vendored mail dependency, exit 101. Verbatim diagnostic: ``` error: only one of 'runtime-async-std' or 'runtime-tokio' features must be enabled --> crates/plugins/mail/vendor/async-imap/src/lib.rs:83:1 error: could not compile `async-imap` (lib) due to 14 previous errors ``` - `cargo test --workspace`: stopped before the test phase at the four-hour job cap while compiling optional workspace plugins. The log ended at `Compiling keyring-core v1.0.0`; the command was terminated. The shell wrapper then reported `zsh:7: read-only variable: status` while recording the signal exit, so there is no Cargo test summary. - `cargo clean`: `Removed 13588 files, 6.9GiB total`. Removed `apps/web/build` and `apps/web/.svelte-kit`. ## Adversarial round Ran once. It reported 92 findings: 88 SLOW load findings and four non-SLOW output entries. The Reminders VTODO completion path returned 200 but lost its existing DESCRIPTION; evidence was filed on #393. Three Appearance body-size proxy messages corresponded to two oversized requests; direct server verification returned 413. This proxy false-positive is already tracked by #394. No Money endpoint was added. ## Decisions for owner confirmation - Split and transfer markers use stable destination category links to fit the four required transaction fields. - The canonical link writer escapes `]` in link labels. - Arithmetic accepts extra fractional digits only when they are zero; the source spelling stays intact. Negative zero reduces to integer zero in arithmetic. - The v1 projection rejects split and general transfer transactions that the unchanged calculator cannot calculate correctly; parsing and reference resolution still work. - Full Actual M01–M34 and FX coverage remain outside this spike. - The dev merge kept Money §48 and the incoming Integrations §49.
Author
Owner

Starting independent Money number review on job/money-review at 232702c08c (base spike job/money-md). I will post reproduced findings, fixes, gates, and verdict here.

Starting independent Money number review on job/money-review at 232702c08c23063c58df9433d8512b466abaf735 (base spike job/money-md). I will post reproduced findings, fixes, gates, and verdict here.
Author
Owner

Independent #404 review findings, reproduced with the new tests:

  • HIGH: a dated row without ^id disappeared from the arithmetic projection; duplicate IDs, including collisions with prose and subscription anchors, were accepted. The first fix rejects missing and duplicate anchors.
  • HIGH: subscription price history silently skipped malformed entries and selected an arbitrary entry for a repeated effective date. It also allowed a bill currency different from the linked account. The first fix rejects these cases.
  • HIGH: a card refund left a stale payment reserve; a naive release could take another category's reserve. The second fix tracks funded card spend by category and releases only its remaining reserve.
  • MEDIUM: split and transfer rows were labeled unsupported before checking a one-cent imbalance or same-account transfer. The second fix validates the legs exactly before the unsupported-feature gate.
  • MEDIUM: nonbreaking and thin spaces at amount edges could be stripped into a valid amount. The second fix limits amount-field trimming to ASCII space and tab.

The Python suite currently reports 42 passed, 44 subtests passed. I will finish the remaining edge cases, final gates, and verdict in the final comment.

Independent #404 review findings, reproduced with the new tests: - HIGH: a dated row without ^id disappeared from the arithmetic projection; duplicate IDs, including collisions with prose and subscription anchors, were accepted. The first fix rejects missing and duplicate anchors. - HIGH: subscription price history silently skipped malformed entries and selected an arbitrary entry for a repeated effective date. It also allowed a bill currency different from the linked account. The first fix rejects these cases. - HIGH: a card refund left a stale payment reserve; a naive release could take another category's reserve. The second fix tracks funded card spend by category and releases only its remaining reserve. - MEDIUM: split and transfer rows were labeled unsupported before checking a one-cent imbalance or same-account transfer. The second fix validates the legs exactly before the unsupported-feature gate. - MEDIUM: nonbreaking and thin spaces at amount edges could be stripped into a valid amount. The second fix limits amount-field trimming to ASCII space and tab. The Python suite currently reports 42 passed, 44 subtests passed. I will finish the remaining edge cases, final gates, and verdict in the final comment.
Author
Owner

Independent Money Markdown number review (#404)

Branch: job/money-review; base spike: 232702c0; head: ba2cab778db399876040a0ce463f2a8c06d53d22. Merged local dev once before final gates. No UI, push, deploy, or merge to dev was done.

Severity Reproduction / evidence Fix or proposal
HIGH, fixed A dated transaction without ^id vanished from the projected ledger. Reused IDs, including IDs also on prose or subscription blocks, were accepted. Reject missing IDs and duplicate anchors in each Money file before arithmetic or bill derivation. Regression tests cover month, Budget and Accounts references.
HIGH, fixed Repeated subscription price:: dates selected an arbitrary row; malformed price rows were silently skipped; scheduled bill currency could differ from the linked account. Reject duplicate effective dates, malformed prices and currency mismatches.
HIGH, fixed A card refund larger than a purchase left a stale payment reserve. Releasing from the total reserve could also consume another category's funded reserve. Track funded card spend by category in the derived calculator and cap each refund's reserve release to that category.
HIGH, product gap A valid split or general transfer cannot be calculated by the v1 projection. FX is rejected even with an fx total. Implement and property-test these in the product arithmetic before Money can post them. The spike currently fails closed.
MEDIUM, fixed A split short by one cent and an unbalanced or same-account transfer reached the generic unsupported-feature error. Compare exact rational legs before that gate; reject the precise error. A three-way split requires an explicit remainder cent.
MEDIUM, fixed NBSP and thin spaces at an amount edge could be stripped into an accepted value. The arithmetic oracle also accepted Unicode digits and Unicode edge spaces. Accept ASCII decimal syntax only. Preserve permitted ASCII tabs and original source bytes.
MEDIUM, product gap The oracle handles a reviewed currency subset. An unknown code is rejected; other valid ISO codes still need a maintained scale table. Use an authoritative versioned ISO 4217 scale table in the Money product. CLF four-place support was added to this oracle (SIX amendment 157: https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/amendments/dl_currency_iso_amendment_157.pdf).
MEDIUM, product gap A statement plan can exceed the funded card reserve while the card is overspent. The due bill and debt gap are derived separately but there is no explicit bill-coverage result. Show the due amount, current reserve and uncovered amount separately. Never post or assign a bill automatically.
MEDIUM, product gap The Markdown month stores a date, not an instant or time zone. A source event at 23:30 local on a month end has no defined import conversion here. Set the transaction's local calendar date in the importer before writing the month file; test month-end time zones and daylight-saving transitions.
PASS Commas in link labels and payees, escaped/nested brackets, Markdown emphasis, RTL text, emoji, long lines, BOM, CRLF, tabs, rename to a label containing ] and , , unknown prose, missing anchors and cross-budget links. Parse/write/parse and raw-byte tests pass; unknown lines remain unchanged.
PASS Negative and positive zero, values over 10^15, 18+ decimal places, JPY/KWD/CLF scales, unknown currency, scientific notation, grouping and locale commas, February 29/30/31 clamping, and a refund larger than a purchase. Exact integer/rational tests pass or reject invalid input. No floating-point arithmetic is used.

Hypothesis generates 100 random cases each for transaction round trips, assignment inverse, cash-ledger conservation and split/merge identity. The conservation property is scoped to cash accounts; card payment reserves are an additional internal allocation. The split/merge property covers exact syntax and amounts; posting splits remains unsupported by the prototype.

Verdict: not safe to build the full Money product on yet. The Markdown syntax is readable and its tested subset can round-trip losslessly, but valid split, transfer and FX rows cannot be posted, and currency and time-zone rules need product implementations. The prototype rejects unsupported rows rather than deriving wrong balances.

Decisions where DESIGN §48 is silent: block IDs are unique within each Money file; effective-price dates cannot repeat; amount digits and non-edge separators are ASCII; a three-way split stores its remainder explicitly; card payments consume funded purchase attribution in first-seen order, only to cap later refund releases. Confirm the last attribution policy before product code.

Changed files: spikes/money-markdown/money_markdown.py, spikes/money-markdown/test_money_adversarial.py, spikes/money-format/budget_math.py. Five small commits plus one dev merge. No Rust or web files were changed by this review.

Final Python gate, verbatim:

................................... [ 76%]
...........                                           [100%]
46 passed, 56 subtests passed in 5.69s

Command: TMPDIR=$PWD/target/tmp CARGO_PROFILE_DEV_DEBUG=line-tables-only CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=4 uv run --no-project --with hypothesis --with pytest python -m pytest -q spikes/money-markdown spikes/money-format (exit 0). git diff --check was clean. cargo clean reported Removed 1 file, 356B total. Rust gates were not run because no Rust files were touched.

# Independent Money Markdown number review (#404) Branch: `job/money-review`; base spike: `232702c0`; head: `ba2cab778db399876040a0ce463f2a8c06d53d22`. Merged local `dev` once before final gates. No UI, push, deploy, or merge to `dev` was done. | Severity | Reproduction / evidence | Fix or proposal | |---|---|---| | HIGH, fixed | A dated transaction without `^id` vanished from the projected ledger. Reused IDs, including IDs also on prose or subscription blocks, were accepted. | Reject missing IDs and duplicate anchors in each Money file before arithmetic or bill derivation. Regression tests cover month, Budget and Accounts references. | | HIGH, fixed | Repeated subscription `price::` dates selected an arbitrary row; malformed price rows were silently skipped; scheduled bill currency could differ from the linked account. | Reject duplicate effective dates, malformed prices and currency mismatches. | | HIGH, fixed | A card refund larger than a purchase left a stale payment reserve. Releasing from the total reserve could also consume another category's funded reserve. | Track funded card spend by category in the derived calculator and cap each refund's reserve release to that category. | | HIGH, product gap | A valid split or general transfer cannot be calculated by the v1 projection. FX is rejected even with an `fx total`. | Implement and property-test these in the product arithmetic before Money can post them. The spike currently fails closed. | | MEDIUM, fixed | A split short by one cent and an unbalanced or same-account transfer reached the generic unsupported-feature error. | Compare exact rational legs before that gate; reject the precise error. A three-way split requires an explicit remainder cent. | | MEDIUM, fixed | NBSP and thin spaces at an amount edge could be stripped into an accepted value. The arithmetic oracle also accepted Unicode digits and Unicode edge spaces. | Accept ASCII decimal syntax only. Preserve permitted ASCII tabs and original source bytes. | | MEDIUM, product gap | The oracle handles a reviewed currency subset. An unknown code is rejected; other valid ISO codes still need a maintained scale table. | Use an authoritative versioned ISO 4217 scale table in the Money product. CLF four-place support was added to this oracle (SIX amendment 157: https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/amendments/dl_currency_iso_amendment_157.pdf). | | MEDIUM, product gap | A statement plan can exceed the funded card reserve while the card is overspent. The due bill and debt gap are derived separately but there is no explicit bill-coverage result. | Show the due amount, current reserve and uncovered amount separately. Never post or assign a bill automatically. | | MEDIUM, product gap | The Markdown month stores a date, not an instant or time zone. A source event at 23:30 local on a month end has no defined import conversion here. | Set the transaction's local calendar date in the importer before writing the month file; test month-end time zones and daylight-saving transitions. | | PASS | Commas in link labels and payees, escaped/nested brackets, Markdown emphasis, RTL text, emoji, long lines, BOM, CRLF, tabs, rename to a label containing `]` and `, `, unknown prose, missing anchors and cross-budget links. | Parse/write/parse and raw-byte tests pass; unknown lines remain unchanged. | | PASS | Negative and positive zero, values over 10^15, 18+ decimal places, JPY/KWD/CLF scales, unknown currency, scientific notation, grouping and locale commas, February 29/30/31 clamping, and a refund larger than a purchase. | Exact integer/rational tests pass or reject invalid input. No floating-point arithmetic is used. | Hypothesis generates 100 random cases each for transaction round trips, assignment inverse, cash-ledger conservation and split/merge identity. The conservation property is scoped to cash accounts; card payment reserves are an additional internal allocation. The split/merge property covers exact syntax and amounts; posting splits remains unsupported by the prototype. **Verdict: not safe to build the full Money product on yet.** The Markdown syntax is readable and its tested subset can round-trip losslessly, but valid split, transfer and FX rows cannot be posted, and currency and time-zone rules need product implementations. The prototype rejects unsupported rows rather than deriving wrong balances. Decisions where DESIGN §48 is silent: block IDs are unique within each Money file; effective-price dates cannot repeat; amount digits and non-edge separators are ASCII; a three-way split stores its remainder explicitly; card payments consume funded purchase attribution in first-seen order, only to cap later refund releases. Confirm the last attribution policy before product code. Changed files: `spikes/money-markdown/money_markdown.py`, `spikes/money-markdown/test_money_adversarial.py`, `spikes/money-format/budget_math.py`. Five small commits plus one `dev` merge. No Rust or web files were changed by this review. Final Python gate, verbatim: ``` ................................... [ 76%] ........... [100%] 46 passed, 56 subtests passed in 5.69s ``` Command: `TMPDIR=$PWD/target/tmp CARGO_PROFILE_DEV_DEBUG=line-tables-only CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=4 uv run --no-project --with hypothesis --with pytest python -m pytest -q spikes/money-markdown spikes/money-format` (exit 0). `git diff --check` was clean. `cargo clean` reported `Removed 1 file, 356B total`. Rust gates were not run because no Rust files were touched.
Author
Owner

Starting round 3 on job/money-md at 232702c08c (based on dev at 191b179baa). I have read the independent review table. I am merging job/money-review at ba2cab778d before the missing posting, currency, reserve, and date work.

Starting round 3 on job/money-md at 232702c08c23063c58df9433d8512b466abaf735 (based on dev at 191b179baac3ef4f5bebfe07ce91c4b7a887ace2). I have read the independent review table. I am merging job/money-review at ba2cab778db399876040a0ce463f2a8c06d53d22 before the missing posting, currency, reserve, and date work.
Author
Owner

Finding: after merging review ba2cab7, the Markdown projector still rejects every split and general transfer, and it drops fx total before calculation; budget_math.py has six currency scales. Upcoming card bills report the full bill amount but do not report the funded reserve or uncovered shortfall. I am adding posting around exact integer minor units and keeping the foreign source amount separate from its budget-currency fx total.

Finding: after merging review ba2cab7, the Markdown projector still rejects every split and general transfer, and it drops fx total before calculation; budget_math.py has six currency scales. Upcoming card bills report the full bill amount but do not report the funded reserve or uncovered shortfall. I am adding posting around exact integer minor units and keeping the foreign source amount separate from its budget-currency fx total.
Author
Owner

Owner decisions (2026-09-29): (1) the reviewer's five decisions are accepted: IDs unique per file; no two subscription prices on the same date; ASCII decimals; explicit split-remainder cents; card payments cover the oldest funded purchases first. (2) File-level properties move to YAML frontmatter (format, budget-id, currency, month), Obsidian's native Properties, matching notes and tasks. key:: value (Dataview inline fields) stays only inside the document: account, category, subscription and transaction child lines. (3) A second independent breakage round follows round 3. (4) The UI may start before the format is final: the Rust Money plugin (model, budget maths, API) and the first screens start now; split, transfer and FX UI wait for the second review.

Owner decisions (2026-09-29): (1) the reviewer's five decisions are accepted: IDs unique per file; no two subscription prices on the same date; ASCII decimals; explicit split-remainder cents; card payments cover the oldest funded purchases first. (2) **File-level properties move to YAML frontmatter** (format, budget-id, currency, month), Obsidian's native Properties, matching notes and tasks. `key:: value` (Dataview inline fields) stays only inside the document: account, category, subscription and transaction child lines. (3) A second independent breakage round follows round 3. (4) The UI may start before the format is final: the Rust Money plugin (model, budget maths, API) and the first screens start now; split, transfer and FX UI wait for the second review.
Author
Owner

Starting round 3 on branch job/money-md, based on dev at 43c1377c8f. The branch already contains job/money-review at ba2cab77; git merge job/money-review reported Already up to date.

Starting round 3 on branch job/money-md, based on dev at 43c1377c8fd3b1b495841446989a64156f8ae1e4. The branch already contains job/money-review at ba2cab77; git merge job/money-review reported Already up to date.
Author
Owner

Findings from round 3:

  • A transaction on an account whose currency differs from the budget was rejected before its explicit fx total could post. The projector now carries the budget currency from frontmatter and uses the declared FX total for exact budget-currency balances. The regression passes.
  • A malformed future subscription price was skipped when the requested month had no due bill. The reader now validates all price rows before selecting the month. The regression failed before the fix and passes after it.

The checked-in scale table matches all 165 SIX List One codes with numeric minor units in the 2026-09-17 publication; the check found no missing codes, extra codes or scale differences.

Findings from round 3: - A transaction on an account whose currency differs from the budget was rejected before its explicit `fx total` could post. The projector now carries the budget currency from frontmatter and uses the declared FX total for exact budget-currency balances. The regression passes. - A malformed future subscription price was skipped when the requested month had no due bill. The reader now validates all price rows before selecting the month. The regression failed before the fix and passes after it. The checked-in scale table matches all 165 SIX List One codes with numeric minor units in the 2026-09-17 publication; the check found no missing codes, extra codes or scale differences.
Author
Owner

Added (owner, 2026-09-29): cards, subscriptions and funding states

B1 Billing cycles. A card account can declare its cycle, and the app computes each statement window and statement balance from the recorded card transactions:

  • statement day:: 20 (the statement closes on the 20th; clamp for short months and return to the 20th after);
  • the due date comes from the issuer's rule, which differs by bank: due after:: 18 days (due = close + N days, so it moves with the month length: a statement on the 20th is due on the 7th after a 30-day month and on the 6th after a 31-day month, and so on), or due day:: 18 (a fixed day of the next month). Support both, and use whichever the account states. A statement that closes or falls due on a weekend or holiday keeps the issuer's date (no shifting unless business days:: true).
  • statement amount:: typed from the real bill overrides the computed balance for that cycle, and the difference is shown as "unrecorded" (interest, fees, missing transactions).
  • Each cycle can be written as a child line when the user confirms a bill: - statement:: 2026-09-20, 812.40 USD, due 2026-10-08 ^st-2026-09. This keeps the history in the file.
  • Card statements from the User's email (later, via the Mail plugin) can prefill statement amount, minimum and due date as a suggestion to confirm, never silently. Record this as a later hook in DESIGN.
    B2 Funding states. Implement and property-test every YNAB state for categories and card payment categories:
  • underspent (available > 0, rolls over);
  • overspent in cash (available < 0 rolls into next month's Ready to Assign as a deduction, the YNAB rule);
  • overspent on credit (the shortfall becomes card debt not covered by the payment category: the "underfunded" card);
  • a card payment category that is correctly funded, underfunded (less than the statement or the plan's due amount) or overfunded (more than the statement: the extra is available for the next cycle);
  • targets or plans "funded for the next N months" (assigned ahead: available covers N future months of the target or bill) and how that shows;
  • rollover of positive balances across months, and a negative balance's effect as above;
  • Ready to Assign going negative ("assigned more than you have").
    The UI wording follows YNAB. Show each state in a worked example under docs/research/money-markdown.md, with the file lines and the computed numbers.
    S1 Charge matching across currencies (important for the owner: Hetzner or VPS charges in EUR or USD, but the card records INR). A subscription priced in a foreign currency (price:: 2026-10-01, 45.00 EUR) matches a card transaction in the card's currency (INR) within the date window. The match tolerance uses the recorded fx total:: (or an FX rate range, ±5% by default, as a named setting), never exact equality. On a match, write the FX pair on the transaction:
- 2026-10-02, -4180.55 INR, [Visa](Accounts.md#^visa), [Hosting](Budget.md#^hosting), Hetzner Online ^card-x
  - fx:: -45.00 EUR

and link it to the subscription. A price change in the source currency proposes a new price:: line. A change in INR alone (FX drift) does not.
S2 Cancel-by reminders. cancel by:: and trial ends:: produce reminders through the remind: system (default 3 days before; see #430 R1–R3).

## Added (owner, 2026-09-29): cards, subscriptions and funding states **B1 Billing cycles.** A card account can declare its cycle, and the app computes each statement window and statement balance from the recorded card transactions: - `statement day:: 20` (the statement closes on the 20th; clamp for short months and return to the 20th after); - the due date comes from the issuer's rule, which differs by bank: `due after:: 18 days` (due = close + N days, so it **moves with the month length**: a statement on the 20th is due on the 7th after a 30-day month and on the 6th after a 31-day month, and so on), or `due day:: 18` (a fixed day of the next month). Support both, and use whichever the account states. A statement that closes or falls due on a weekend or holiday keeps the issuer's date (no shifting unless `business days:: true`). - `statement amount::` typed from the real bill **overrides** the computed balance for that cycle, and the difference is shown as "unrecorded" (interest, fees, missing transactions). - Each cycle can be written as a child line when the user confirms a bill: `- statement:: 2026-09-20, 812.40 USD, due 2026-10-08 ^st-2026-09`. This keeps the history in the file. - Card statements **from the User's email** (later, via the Mail plugin) can prefill statement amount, minimum and due date as a **suggestion** to confirm, never silently. Record this as a later hook in DESIGN. **B2 Funding states.** Implement and property-test every YNAB state for categories and card payment categories: - underspent (available > 0, rolls over); - overspent in cash (available < 0 **rolls into next month's Ready to Assign** as a deduction, the YNAB rule); - overspent on credit (the shortfall becomes card debt not covered by the payment category: the "underfunded" card); - a card payment category that is correctly funded, underfunded (less than the statement or the plan's due amount) or overfunded (more than the statement: the extra is available for the next cycle); - targets or plans "funded for the next N months" (assigned ahead: available covers N future months of the target or bill) and how that shows; - rollover of positive balances across months, and a negative balance's effect as above; - Ready to Assign going negative ("assigned more than you have"). The UI wording follows YNAB. Show each state in a worked example under `docs/research/money-markdown.md`, with the file lines and the computed numbers. **S1 Charge matching across currencies (important for the owner: Hetzner or VPS charges in EUR or USD, but the card records INR).** A subscription priced in a foreign currency (`price:: 2026-10-01, 45.00 EUR`) matches a card transaction in the card's currency (INR) within the date window. The match tolerance uses the recorded `fx total::` (or an FX rate range, ±5% by default, as a named setting), never exact equality. On a match, write the FX pair on the transaction: ``` - 2026-10-02, -4180.55 INR, [Visa](Accounts.md#^visa), [Hosting](Budget.md#^hosting), Hetzner Online ^card-x - fx:: -45.00 EUR ``` and link it to the subscription. A price change in the source currency proposes a new `price::` line. A change in INR alone (FX drift) does not. **S2 Cancel-by reminders.** `cancel by::` and `trial ends::` produce reminders through the `remind:` system (default 3 days before; see #430 R1–R3).
Author
Owner

Starting round 3 on job/money-md at 54b5ecd0ee, based on dev at 9bf3d549b4. I have read the review findings table and owner additions on #404. Merging job/money-review at ba2cab778d before implementation.

Starting round 3 on job/money-md at 54b5ecd0ee898dadbe7aadcb5dd33b8a5360fc06, based on dev at 9bf3d549b41f5fe97c29148220a1a9c484faf63d. I have read the review findings table and owner additions on #404. Merging job/money-review at ba2cab778db399876040a0ce463f2a8c06d53d22 before implementation.
Author
Owner

Finding: the prototype had no card-cycle computation; accounts only described a static due date and amount. It now derives statement windows from recorded card postings, applies the issuer's close and due rules, retains confirmed statement history, and reports the difference from computed activity. business days:: true takes caller-supplied holidays; the default keeps issuer dates unchanged. Funding results now expose category rollover, cash and credit overspend states, target months funded, negative Ready to Assign, and distinct funded, underfunded and overfunded card-payment coverage.

Evidence: TMPDIR=$PWD/target/tmp uv run --no-project --with hypothesis --with pytest python -m pytest -q spikes/money-markdown spikes/money-format passed: 72 passed, 228 subtests passed. The official SIX List One XML has 165 numeric scales; the checked-in table has no missing, extra or different entries. Head: 41939433b3ce442df3d4323877dad60951e61e96.

Finding: the prototype had no card-cycle computation; accounts only described a static due date and amount. It now derives statement windows from recorded card postings, applies the issuer's close and due rules, retains confirmed statement history, and reports the difference from computed activity. `business days:: true` takes caller-supplied holidays; the default keeps issuer dates unchanged. Funding results now expose category rollover, cash and credit overspend states, target months funded, negative Ready to Assign, and distinct funded, underfunded and overfunded card-payment coverage. Evidence: `TMPDIR=$PWD/target/tmp uv run --no-project --with hypothesis --with pytest python -m pytest -q spikes/money-markdown spikes/money-format` passed: 72 passed, 228 subtests passed. The official SIX List One XML has 165 numeric scales; the checked-in table has no missing, extra or different entries. Head: `41939433b3ce442df3d4323877dad60951e61e96`.
Author
Owner

Round 3 implementation progress (2026-09-29):

  • Added posting for explicit splits, same-currency transfers, cross-currency transfer legs, and foreign-currency transactions through exact minor units. Property tests exercise conservation and transaction-date FIFO card payments.
  • Verified the maintained scale table against the official SIX List One XML: all 165 numeric codes and scales match. Unknown codes fail explicitly.
  • Added issuer-cycle projections, statement history and overrides, funding states, charge-match suggestions, source-price proposals, and subscription reminder trigger derivation. YAML frontmatter carries file identity and currency; unknown frontmatter remains lossless.
  • Added the requested date-boundary cases and worked examples. Python suite currently passes: 83 passed, 230 subtests passed.

One interpretation is recorded in DESIGN §48: due after:: N days counts the close date as day one so it matches the supplied due-date examples. The reminder projection returns absolute instants and stable block IDs; writing them through the authenticated Notes reminder API remains a product integration step.

Commits: b34ee6000 and b87b29dc9. No UI was built.

Round 3 implementation progress (2026-09-29): - Added posting for explicit splits, same-currency transfers, cross-currency transfer legs, and foreign-currency transactions through exact minor units. Property tests exercise conservation and transaction-date FIFO card payments. - Verified the maintained scale table against the official SIX List One XML: all 165 numeric codes and scales match. Unknown codes fail explicitly. - Added issuer-cycle projections, statement history and overrides, funding states, charge-match suggestions, source-price proposals, and subscription reminder trigger derivation. YAML frontmatter carries file identity and currency; unknown frontmatter remains lossless. - Added the requested date-boundary cases and worked examples. Python suite currently passes: 83 passed, 230 subtests passed. One interpretation is recorded in DESIGN §48: `due after:: N days` counts the close date as day one so it matches the supplied due-date examples. The reminder projection returns absolute instants and stable block IDs; writing them through the authenticated Notes reminder API remains a product integration step. Commits: b34ee6000 and b87b29dc9. No UI was built.
Author
Owner

Real test data available (owner, 2026-09-29): 14 card statement PDFs from 10 Indian issuers, stored privately on the build host (never in the repo or issues). The second independent breakage review runs against these statements. The format must express every line losslessly (purchases, FX lines with markup, GST on fees and interest, reversals, EMI conversions, cashback and rewards, partial payments), and computed statement balances, minimum due and due dates must match the bank to the paisa. Findings are reported in aggregate, and fixtures are synthetic look-alikes.

Real test data available (owner, 2026-09-29): 14 card statement PDFs from 10 Indian issuers, stored privately on the build host (never in the repo or issues). The second independent breakage review runs **against these statements**. The format must express every line losslessly (purchases, FX lines with markup, GST on fees and interest, reversals, EMI conversions, cashback and rewards, partial payments), and computed statement balances, minimum due and due dates must match the bank to the paisa. Findings are reported in aggregate, and fixtures are synthetic look-alikes.
Author
Owner

Resuming round 3 on job/money-md at 3d1e5ac76. The independent review commit ba2cab778 is already an ancestor of this branch; I will confirm the merge state, finish any remaining requested work, run the Python gates, and stop for the second independent review. No UI work.

Resuming round 3 on job/money-md at 3d1e5ac76. The independent review commit ba2cab778 is already an ancestor of this branch; I will confirm the merge state, finish any remaining requested work, run the Python gates, and stop for the second independent review. No UI work.
Author
Owner

Finding: the currency-scale round-trip assertion was outside its loop, so it checked only the final currency. I moved it into each per-currency subtest. Evidence: the Python suite now passes 83 tests and 230 subtests, including all 165 listed scale codes.

Finding: the currency-scale round-trip assertion was outside its loop, so it checked only the final currency. I moved it into each per-currency subtest. Evidence: the Python suite now passes 83 tests and 230 subtests, including all 165 listed scale codes.
Author
Owner

Round 3 implementation is complete and ready for the second independent review. No UI was built.

What changed

  • The Markdown projection posts split children, general transfers and foreign-currency rows using exact integer minor units. A foreign row uses its explicit fx total in the budget currency.
  • Added the checked-in SIX ISO 4217 scale table from 2026-09-17. It covers all 165 listed numeric scales, including 0, 2, 3 and 4 decimal places. Unknown codes return an error.
  • Added statement cycles, due-date rules, statement overrides and history, explicit card-bill coverage, and YNAB funding states and rollover.
  • Added cross-currency subscription match suggestions, source-price proposals, and stable block reminder triggers.
  • File fields use YAML frontmatter. Unknown frontmatter and Markdown remain lossless. Transaction dates remain the written calendar dates.
  • Fixed the scale round-trip test so it checks every currency in the table.

Files

  • docs/DESIGN.md
  • docs/research/money-markdown.md
  • spikes/money-format/budget_math.py
  • spikes/money-format/money_units.py
  • spikes/money-format/test_money_format.py
  • spikes/money-markdown/money_markdown.py
  • spikes/money-markdown/test_money_markdown.py
  • spikes/money-markdown/test_money_adversarial.py
  • spikes/money-markdown/examples/calternal-markdown/{Budget.md,Accounts.md,2026-09.md}

Branch

Branch: job/money-md
Head: 4f49dfde62e71aceb177be4118df095aabd86095

Gates

cargo fmt --check: exit 0; no output.

Python command:
TMPDIR="$PWD/target/tmp" PYTHONDONTWRITEBYTECODE=1 uv run --no-project --with hypothesis --with pytest python -m pytest -p no:cacheprovider -q spikes/money-markdown spikes/money-format

Verbatim result:

83 passed, 230 subtests passed in 8.19s

bun run check:

$ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
Text sizes use shared role tokens.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/money-md/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

bun run test:

 Test Files  129 passed (129)
      Tests  825 passed (825)
   Start at  22:08:36
   Duration  104.75s (transform 52%, environment 21%, import 12%, tests 10%, setup 4%)

Environment  |component| jsdom was created 41 times · 127.83s total, 27% of tracked time
             create it once per worker with pool: 'vmThreads' (keeps per-file isolation) or isolate: false (shares it across files)
             learn more: https://vitest.dev/guide/improving-performance#test-environments

Vitest also printed its existing jsdom scrollTo and CSS parser notices. No Rust crate changed, so Cargo clippy and tests were not run. cargo clean completed with:

     Removed 0 files

git diff --check was clean. The worktree is clean.

Known gaps

  • This is a research prototype. There is no Money product UI or API.
  • The caller supplies FX rates and issuer holiday dates. There is no live rate provider or holiday calendar.
  • Mail suggestions and writing reminders through the existing block-reminder API remain integration work.
  • Full M01–M34 coverage remains outside this spike.

Decisions

The five reviewer decisions are accepted and implemented: IDs are unique per file; subscription prices cannot share an effective date; decimal syntax is ASCII; split remainder cents are explicit; and card payments consume funded purchases oldest first. File-level properties use YAML frontmatter as directed by the owner.

due after:: N days counts the close date as day one. DESIGN §48 records this rule, which matches both supplied issuer examples. I made no additional decisions outside DESIGN §48.

Round 3 implementation is complete and ready for the second independent review. No UI was built. ## What changed - The Markdown projection posts split children, general transfers and foreign-currency rows using exact integer minor units. A foreign row uses its explicit `fx total` in the budget currency. - Added the checked-in SIX ISO 4217 scale table from 2026-09-17. It covers all 165 listed numeric scales, including 0, 2, 3 and 4 decimal places. Unknown codes return an error. - Added statement cycles, due-date rules, statement overrides and history, explicit card-bill coverage, and YNAB funding states and rollover. - Added cross-currency subscription match suggestions, source-price proposals, and stable block reminder triggers. - File fields use YAML frontmatter. Unknown frontmatter and Markdown remain lossless. Transaction dates remain the written calendar dates. - Fixed the scale round-trip test so it checks every currency in the table. ## Files - `docs/DESIGN.md` - `docs/research/money-markdown.md` - `spikes/money-format/budget_math.py` - `spikes/money-format/money_units.py` - `spikes/money-format/test_money_format.py` - `spikes/money-markdown/money_markdown.py` - `spikes/money-markdown/test_money_markdown.py` - `spikes/money-markdown/test_money_adversarial.py` - `spikes/money-markdown/examples/calternal-markdown/{Budget.md,Accounts.md,2026-09.md}` ## Branch Branch: `job/money-md` Head: `4f49dfde62e71aceb177be4118df095aabd86095` ## Gates `cargo fmt --check`: exit 0; no output. Python command: `TMPDIR="$PWD/target/tmp" PYTHONDONTWRITEBYTECODE=1 uv run --no-project --with hypothesis --with pytest python -m pytest -p no:cacheprovider -q spikes/money-markdown spikes/money-format` Verbatim result: ```text 83 passed, 230 subtests passed in 8.19s ``` `bun run check`: ```text $ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Text sizes use shared role tokens. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/money-md/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run test`: ```text Test Files 129 passed (129) Tests 825 passed (825) Start at 22:08:36 Duration 104.75s (transform 52%, environment 21%, import 12%, tests 10%, setup 4%) Environment |component| jsdom was created 41 times · 127.83s total, 27% of tracked time create it once per worker with pool: 'vmThreads' (keeps per-file isolation) or isolate: false (shares it across files) learn more: https://vitest.dev/guide/improving-performance#test-environments ``` Vitest also printed its existing jsdom `scrollTo` and CSS parser notices. No Rust crate changed, so Cargo clippy and tests were not run. `cargo clean` completed with: ```text Removed 0 files ``` `git diff --check` was clean. The worktree is clean. ## Known gaps - This is a research prototype. There is no Money product UI or API. - The caller supplies FX rates and issuer holiday dates. There is no live rate provider or holiday calendar. - Mail suggestions and writing reminders through the existing block-reminder API remain integration work. - Full M01–M34 coverage remains outside this spike. ## Decisions The five reviewer decisions are accepted and implemented: IDs are unique per file; subscription prices cannot share an effective date; decimal syntax is ASCII; split remainder cents are explicit; and card payments consume funded purchases oldest first. File-level properties use YAML frontmatter as directed by the owner. `due after:: N days` counts the close date as day one. DESIGN §48 records this rule, which matches both supplied issuer examples. I made no additional decisions outside DESIGN §48.
Author
Owner

Starting second independent #404 review on job/money-review2, base 4f49dfde62e71aceb177be4118df095aabd86095. Read CLAUDE.md, CONTEXT.md, DESIGN §48, the format spec and first review findings. Review scope: private statement transcription and exact reconciliation, issuer cycles, separate FX markup and taxes, and synthetic regression probes of round 3. Private source data stays outside the worktree. Reports contain aggregate findings only.

Starting second independent #404 review on `job/money-review2`, base `4f49dfde62e71aceb177be4118df095aabd86095`. Read CLAUDE.md, CONTEXT.md, DESIGN §48, the format spec and first review findings. Review scope: private statement transcription and exact reconciliation, issuer cycles, separate FX markup and taxes, and synthetic regression probes of round 3. Private source data stays outside the worktree. Reports contain aggregate findings only.
Author
Owner

Second independent review finding: seven synthetic probes reproduced four round 3 defects. Malformed assignment rows vanished from Ready to Assign; duplicate month inputs double-posted; opaque fenced/comment transaction examples were rejected during projection; and positive FX credits matched recurring charges. Fixed with new regression tests. Python gate: 90 passed, 230 subtests passed in 13.86s. No existing expectation changed.

Private extraction aggregate: 14 PDFs attempted, 13 extracted, one password-protected file skipped. One extracted file has no usable statement table; another is an account statement rather than a card bill. Reconciliation work is in the private directory; no statement values or identifying text are posted.

Second independent review finding: seven synthetic probes reproduced four round 3 defects. Malformed assignment rows vanished from Ready to Assign; duplicate month inputs double-posted; opaque fenced/comment transaction examples were rejected during projection; and positive FX credits matched recurring charges. Fixed with new regression tests. Python gate: `90 passed, 230 subtests passed in 13.86s`. No existing expectation changed. Private extraction aggregate: 14 PDFs attempted, 13 extracted, one password-protected file skipped. One extracted file has no usable statement table; another is an account statement rather than a card bill. Reconciliation work is in the private directory; no statement values or identifying text are posted.
Author
Owner

Finding fixed: mixed cash and credit category overspending used the last spending row's account type for the whole deficit. It could either erase cash overspending at rollover or deduct credit debt from Ready to Assign. Cash now has first claim on the monthly envelope, and only the cash portion rolls into Ready to Assign. An 80-case property test checks both same-date file orders, exact reserves and next-month Ready to Assign. Source behavior: https://support.ynab.com/overspending-in-ynab-a-guide-ryWoxEyi?mobile-help=true . Python gate: 91 passed, 230 subtests passed in 13.23s. Commit 5ef585953.

Private findings: one issuer's outstanding total uses whole-unit rounding; an EMI bill separates total outstanding debt from the current amount due. No source amounts or identifying data are included here. The current cycle-only projection also omits carried debt. Existing round 3 expectations explicitly assert that behavior; I will keep them and report the conflict rather than change the expectations to make a new calculation pass.

Finding fixed: mixed cash and credit category overspending used the last spending row's account type for the whole deficit. It could either erase cash overspending at rollover or deduct credit debt from Ready to Assign. Cash now has first claim on the monthly envelope, and only the cash portion rolls into Ready to Assign. An 80-case property test checks both same-date file orders, exact reserves and next-month Ready to Assign. Source behavior: https://support.ynab.com/overspending-in-ynab-a-guide-ryWoxEyi?mobile-help=true . Python gate: `91 passed, 230 subtests passed in 13.23s`. Commit `5ef585953`. Private findings: one issuer's outstanding total uses whole-unit rounding; an EMI bill separates total outstanding debt from the current amount due. No source amounts or identifying data are included here. The current cycle-only projection also omits carried debt. Existing round 3 expectations explicitly assert that behavior; I will keep them and report the conflict rather than change the expectations to make a new calculation pass.
Author
Owner

Second independent review of the Money Markdown format (#404): final report

Branch job/money-review2, base 4f49dfde6 (head of job/money-md), head 70ca1944fe1e5d779c4599e359503d9ff0fbd563. git merge origin/dev: already up to date. No push, no merge. Private transcriptions and scripts stay in the private directory on the build host (mode 700). This report has aggregates only. Repo fixtures (spikes/money-markdown/examples/review2/) are synthetic look-alikes with invented values.

Data coverage

14 PDFs attempted: 1 needs a password (skipped), 1 has no usable transaction table, 1 is a bank account statement (not a card). 11 card statements from 8 issuers were transcribed into Money Markdown: 228 rows (145 purchases, 46 payments, 25 cashback credits, 7 EMI rows, 4 GST rows, 3 interest rows). Each statement has one cycle only, so cross-month consistency of an issuer rule cannot be checked. The statements contain no foreign-currency rows, late fees or reversals; those shapes are covered by synthetic fixtures only.

Findings

# Area Result (aggregate) Status
1 Lossless transcription 11/11 statements, 228/228 rows parse and write back byte for byte, frontmatter included. PASS
2 Statement total, round 3 rule (cycle activity only) 0/11 match the issuer total. Every issuer bills the whole debt at the close, including debt carried from earlier cycles and after partial payments. Fixed (additive): CardStatement.closing_balance_minor
3 Statement total, closing balance through card_statements 8/11 exact to the paisa; 3/11 differ by less than one rupee because 2 issuers print the total rounded to a whole rupee (their ledgers keep paise). Issuer rule, recorded (see follow-ups)
4 EMI bill 1 statement bills an instalment from a separate instalment balance and prints "total outstanding" apart from "amount due". Modelled as a transfer between the card and a kind:: tracking instalment account, total outstanding reconciles (within the rounding in row 3). The EMI-inclusive amount due depends on an instalment balance the statement does not print. Expressible; amount due not derivable from one statement
5 Minimum due Round 3 statement:: history could not store it (only the current minimum amount::). Now 11/11 stored and read back. 8/11 follow a simple issuer rule (zero on a credit balance, full total under a floor, a percentage, or a fixed floor); 3/11 need issuer data that is not in the rows (per-line MAD contributions, EMI and past-due amounts). Fixed: optional , minimum 25.00 USD suffix; impossible minimums rejected. Minimum stays an issuer figure, never derived
6 Due dates card_statements reproduces 11/11 issuer due dates with due after:: N days (N from 18 to 21). due day:: fits 5/11. 2 issuers are due in the same month as the close, which due day:: (next month only) cannot express; this matters only if such an issuer uses a fixed-day rule. 1 issuer prints a statement date one day after its period end: the close must be the period end. PASS with note
7 FX and subscription matching on real data No real foreign-currency rows. One issuer states that markup and its GST are included in the INR line; the others charge them as separate lines. Both shapes are expressible (synthetic fixture: exact rational rate, separate markup and GST, later-cycle refund and markup reversal). Not testable on real data
8 Transaction times 77 rows from 2 issuers carry a time of day. A row stores a date only; the time survives only as an unknown child line. Not a blocker; record for the importer
9 Round 3 re-attack (earlier in this job) Malformed assignments vanished from Ready to Assign; duplicate month inputs double-posted; fenced/comment examples failed projection; FX refunds matched recurring charges; mixed cash/credit overspending used the last row's account type; two exact FX values in one currency were chosen by child order; two cards could share one payment reserve. Fixed (commits 73126e57d, 5ef585953, ba22ab1cc)
10 Account kinds A typo such as kind:: crad produced an account that no rule reads (no cycle, no reserve, no error). A statement:: line appended below the last heading landed on a non-card account and was ignored. This review's own private script hit the second case. Fixed (70ca1944f): kinds are cash, card, tracking; card properties elsewhere are rejected
11 YNAB funding states, property tests Underspent, cash and credit overspending (mixed order property, 80 cases), card payment funded/under/over, Ready to Assign negative, no floats: pass on round 3 plus fixes. PASS

Gate (verbatim)

Command: TMPDIR=$PWD/target/tmp PYTHONDONTWRITEBYTECODE=1 uv run --no-project --with hypothesis --with pytest python -m pytest -p no:cacheprovider -q spikes/money-markdown spikes/money-format

.........................................................................................................                          [100%]
105 passed, 230 subtests passed in 3.17s

git diff --check 4f49dfde6: clean. No Rust or web files changed, so their gates were not run. No existing test expectation was changed.

Verdict

The Markdown format is solid: it expressed every real statement row losslessly, and with the closing balance the numbers reconcile to the paisa or to the issuer's printed rounding on 11/11 statements. It is ready for the Money plugin model, parser and budget maths, and for the budget, account and transaction UI.

One item blocks the card statement / bill UI (not the rest of Money):

  1. Owner decision: what "Unrecorded" compares against. Round 3 computes Unrecorded as confirmed bill minus cycle activity. With real statements this reports carried debt as "unrecorded" on every card that was not paid in full. The review added closing_balance_minor next to the unchanged computed_minor, but kept the round 3 expectation (test_statement_cycle_uses_open_previous_close_and_closed_current_date asserts Unrecorded = amount − cycle activity). Recommendation: Unrecorded = confirmed bill − closing balance, and the bill UI shows the closing balance as the statement amount. Once confirmed, flip that one expectation.

Follow-ups (do not block):

  • Optional issuer property for whole-unit rounding of the printed total (for example due rounding:: 1.00 INR), so the sub-rupee difference does not show as Unrecorded. Until then, the confirmed statement:: line keeps the exact bank figure.
  • Importer: map a source time of day to the local calendar date; keep the time as a child line if wanted.
  • Real FX coverage: re-run the matcher when a statement with a foreign charge is available.

Decisions (DESIGN §48 was silent; please confirm)

  • statement:: history takes an optional , minimum <amount> <currency> suffix. A minimum is zero or more and not more than a positive amount due; a credit balance has minimum zero.
  • EMI conversion = transfer from the card to a kind:: tracking instalment account; each billed instalment = transfer back. The purchase is spending once. (No new account kind.)
  • Account kinds are exactly cash, card, tracking; card cycle and payment properties under another kind are an error.
  • closing_balance_minor includes every recorded card posting dated on or before the close.

Files changed in this job: spikes/money-markdown/money_markdown.py, spikes/money-format/budget_math.py, spikes/money-markdown/test_money_review2.py, spikes/money-markdown/examples/review2/{Budget,Accounts,2028-01,2028-02}.md, docs/DESIGN.md, docs/research/money-markdown.md.

# Second independent review of the Money Markdown format (#404): final report Branch `job/money-review2`, base `4f49dfde6` (head of `job/money-md`), head **`70ca1944fe1e5d779c4599e359503d9ff0fbd563`**. `git merge origin/dev`: already up to date. No push, no merge. Private transcriptions and scripts stay in the private directory on the build host (mode 700). This report has aggregates only. Repo fixtures (`spikes/money-markdown/examples/review2/`) are synthetic look-alikes with invented values. ## Data coverage 14 PDFs attempted: 1 needs a password (skipped), 1 has no usable transaction table, 1 is a bank account statement (not a card). **11 card statements from 8 issuers** were transcribed into Money Markdown: 228 rows (145 purchases, 46 payments, 25 cashback credits, 7 EMI rows, 4 GST rows, 3 interest rows). Each statement has one cycle only, so cross-month consistency of an issuer rule cannot be checked. The statements contain **no foreign-currency rows, late fees or reversals**; those shapes are covered by synthetic fixtures only. ## Findings | # | Area | Result (aggregate) | Status | |---|---|---|---| | 1 | Lossless transcription | 11/11 statements, 228/228 rows parse and write back byte for byte, frontmatter included. | PASS | | 2 | Statement total, round 3 rule (cycle activity only) | **0/11** match the issuer total. Every issuer bills the whole debt at the close, including debt carried from earlier cycles and after partial payments. | **Fixed (additive)**: `CardStatement.closing_balance_minor` | | 3 | Statement total, closing balance through `card_statements` | **8/11 exact to the paisa**; **3/11 differ by less than one rupee** because 2 issuers print the total rounded to a whole rupee (their ledgers keep paise). | Issuer rule, recorded (see follow-ups) | | 4 | EMI bill | 1 statement bills an instalment from a separate instalment balance and prints "total outstanding" apart from "amount due". Modelled as a transfer between the card and a `kind:: tracking` instalment account, total outstanding reconciles (within the rounding in row 3). The EMI-inclusive amount due depends on an instalment balance the statement does not print. | Expressible; amount due not derivable from one statement | | 5 | Minimum due | Round 3 `statement::` history could not store it (only the current `minimum amount::`). Now 11/11 stored and read back. 8/11 follow a simple issuer rule (zero on a credit balance, full total under a floor, a percentage, or a fixed floor); 3/11 need issuer data that is not in the rows (per-line MAD contributions, EMI and past-due amounts). | **Fixed**: optional `, minimum 25.00 USD` suffix; impossible minimums rejected. Minimum stays an issuer figure, never derived | | 6 | Due dates | `card_statements` reproduces **11/11** issuer due dates with `due after:: N days` (N from 18 to 21). `due day::` fits 5/11. 2 issuers are due in the **same** month as the close, which `due day::` (next month only) cannot express; this matters only if such an issuer uses a fixed-day rule. 1 issuer prints a statement date one day after its period end: the close must be the period end. | PASS with note | | 7 | FX and subscription matching on real data | No real foreign-currency rows. One issuer states that markup and its GST are included in the INR line; the others charge them as separate lines. Both shapes are expressible (synthetic fixture: exact rational rate, separate markup and GST, later-cycle refund and markup reversal). | Not testable on real data | | 8 | Transaction times | 77 rows from 2 issuers carry a time of day. A row stores a date only; the time survives only as an unknown child line. | Not a blocker; record for the importer | | 9 | Round 3 re-attack (earlier in this job) | Malformed assignments vanished from Ready to Assign; duplicate month inputs double-posted; fenced/comment examples failed projection; FX refunds matched recurring charges; mixed cash/credit overspending used the last row's account type; two exact FX values in one currency were chosen by child order; two cards could share one payment reserve. | **Fixed** (commits `73126e57d`, `5ef585953`, `ba22ab1cc`) | | 10 | Account kinds | A typo such as `kind:: crad` produced an account that no rule reads (no cycle, no reserve, no error). A `statement::` line appended below the last heading landed on a non-card account and was ignored. This review's own private script hit the second case. | **Fixed** (`70ca1944f`): kinds are `cash`, `card`, `tracking`; card properties elsewhere are rejected | | 11 | YNAB funding states, property tests | Underspent, cash and credit overspending (mixed order property, 80 cases), card payment funded/under/over, Ready to Assign negative, no floats: pass on round 3 plus fixes. | PASS | ## Gate (verbatim) Command: `TMPDIR=$PWD/target/tmp PYTHONDONTWRITEBYTECODE=1 uv run --no-project --with hypothesis --with pytest python -m pytest -p no:cacheprovider -q spikes/money-markdown spikes/money-format` ``` ......................................................................................................... [100%] 105 passed, 230 subtests passed in 3.17s ``` `git diff --check 4f49dfde6`: clean. No Rust or web files changed, so their gates were not run. No existing test expectation was changed. ## Verdict **The Markdown format is solid: it expressed every real statement row losslessly, and with the closing balance the numbers reconcile to the paisa or to the issuer's printed rounding on 11/11 statements. It is ready for the Money plugin model, parser and budget maths, and for the budget, account and transaction UI.** **One item blocks the card statement / bill UI** (not the rest of Money): 1. **Owner decision: what "Unrecorded" compares against.** Round 3 computes Unrecorded as confirmed bill minus *cycle activity*. With real statements this reports carried debt as "unrecorded" on every card that was not paid in full. The review added `closing_balance_minor` next to the unchanged `computed_minor`, but kept the round 3 expectation (`test_statement_cycle_uses_open_previous_close_and_closed_current_date` asserts Unrecorded = amount − cycle activity). Recommendation: Unrecorded = confirmed bill − closing balance, and the bill UI shows the closing balance as the statement amount. Once confirmed, flip that one expectation. Follow-ups (do not block): - Optional issuer property for whole-unit rounding of the printed total (for example `due rounding:: 1.00 INR`), so the sub-rupee difference does not show as Unrecorded. Until then, the confirmed `statement::` line keeps the exact bank figure. - Importer: map a source time of day to the local calendar date; keep the time as a child line if wanted. - Real FX coverage: re-run the matcher when a statement with a foreign charge is available. ## Decisions (DESIGN §48 was silent; please confirm) - `statement::` history takes an optional `, minimum <amount> <currency>` suffix. A minimum is zero or more and not more than a positive amount due; a credit balance has minimum zero. - EMI conversion = transfer from the card to a `kind:: tracking` instalment account; each billed instalment = transfer back. The purchase is spending once. (No new account kind.) - Account kinds are exactly `cash`, `card`, `tracking`; card cycle and payment properties under another kind are an error. - `closing_balance_minor` includes every recorded card posting dated on or before the close. Files changed in this job: `spikes/money-markdown/money_markdown.py`, `spikes/money-format/budget_math.py`, `spikes/money-markdown/test_money_review2.py`, `spikes/money-markdown/examples/review2/{Budget,Accounts,2028-01,2028-02}.md`, `docs/DESIGN.md`, `docs/research/money-markdown.md`.
Author
Owner

Owner decision (2026-10-01): "Unrecorded" compares the statement total with the account balance as of the statement closing date. Some transactions dated on the closing date may not appear on the statement, because of the bank's cut-off time. So same-day transactions that are missing from the statement must not count as unrecorded: treat closing-date transactions as "may fall in either cycle", show them separately, and let the User mark them. The minimum payment shows on the statement line.

**Owner decision (2026-10-01):** "Unrecorded" compares the statement total with the account balance **as of the statement closing date**. Some transactions dated on the closing date may not appear on the statement, because of the bank's cut-off time. So same-day transactions that are missing from the statement must not count as unrecorded: treat closing-date transactions as "may fall in either cycle", show them separately, and let the User mark them. The minimum payment shows on the statement line.
Author
Owner

Superseded by the owner-approved Money Markdown format in DESIGN §48 (48c94c977) and implementation tracked by #462. The accepted spec is docs/research/money-markdown.md; the spike prototype has round-trip coverage in spikes/money-markdown/test_money_markdown.py.

Superseded by the owner-approved Money Markdown format in DESIGN §48 (48c94c977) and implementation tracked by #462. The accepted spec is docs/research/money-markdown.md; the spike prototype has round-trip coverage in spikes/money-markdown/test_money_markdown.py.
kayg closed this issue 2026-10-03 12:48:23 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#404
No description provided.