Skip to content

feat!: expose color as a first-class Posting property#139

Open
gfyrag wants to merge 6 commits into
mainfrom
feat/color-on-posting
Open

feat!: expose color as a first-class Posting property#139
gfyrag wants to merge 6 commits into
mainfrom
feat/color-on-posting

Conversation

@gfyrag
Copy link
Copy Markdown
Contributor

@gfyrag gfyrag commented Jun 3, 2026

Summary

Color is now carried as a dedicated field on the output Posting type and
as a separate dimension throughout the interpreter, instead of being
encoded as an asset suffix (USD_RED). The numscript ↔ host contract
becomes a clean two-dimensional (asset, color) model: downstream
consumers (ledgers, indexers, reporting) can segregate balances by
(account, asset, color) without parsing asset strings.

The semantics already exercised by the experimental-asset-colors
feature flag are unchanged. Only the wire format and Go API change.

Breaking changes

Surface Before After
Posting {Source, Destination, Amount, Asset} + Color string (json "color")
BalanceQuery map[string][]string (asset strings, color encoded as suffix) map[string][]AssetColor with AssetColor{Asset, Color string}
Balances (in-memory) map[account]map[asset]*big.Int map[account]map[asset]map[color]*big.Int
Balances (JSON wire) flat {"alice": {"USD/2": 100}} strict colored {"alice": {"USD/2": {"": 100, "RED": 50}}}
coloredAsset() helper encoded ASSET_COLOR in asset strings removed
fundsQueue.Pull signature Pull(amount, color *string) Pull(amount) — the color filter parameter was dead code, dropped
fundsQueue.PullColored, PullUncolored unused wrappers removed

No backwards-compatibility shim. The flat-shorthand JSON shape
({"USD/2": 100}) is not accepted — every balance amount must be
written under an explicit color, with "" for the uncolored bucket. The
130 fixture .num.specs.json files in the repo were rewritten
accordingly in a single sweep (jq one-liner over the tree).

A small public helper Uncolored(amount) is added so building test
balances stays terse: Uncolored(big.NewInt(100)) produces
ColorBalance{"": amount}.

Why now

A consumer ledger (POC) needs strict color-segregated balances stored as
(account, asset, color) keys, with color flowing through the emitted
posting. As long as numscript strips color from postings and folds it
into the asset string, consumers cannot do that cleanly. This PR fixes
the contract upstream so we do not need to translate at the boundary.

What's covered by tests

  • color_semantics_test.go (black-box, 17 tests): color propagation
    through emitted postings, isolation between colors, source-restriction
    semantics, allocation distributing color across legs, send-all
    draining only the targeted color, JSON shape acceptance/rejection,
    charset enforcement (^[A-Z]*$).
  • funds_queue_color_test.go (white-box, 1 test): compactTop
    never merges senders across colors. The PullColored/PullUncolored
    selectivity tests are gone with their subjects — the upstream
    invariant (tryTakingFromAccount caps push at the per-color balance,
    tryTakingExact validates completeness on pull) makes the filter
    parameter unnecessary.
  • Updated balances_test.go, interpreter_test.go, numscript_test.go,
    specs_format/*_test.go, plus the 130 .num.specs.json fixtures.

Test plan

  • All numscript test packages green; just pre-commit clean.
  • Downstream consumer (ledger POC) integrates against this branch
    via a replace/pseudo-version pin and exercises segregated
    balances end-to-end (formancehq/ledger-v3-poc#234).
  • CodeRabbit's 3 actionable comments addressed:
    - Color + with scaling (Critical): the grammar already
    refuses the combination, and the SourceWithScaling.Color
    field is never populated by the parser — leaving as-is.
    - HasPrefix exact-match in getAssets (Major): fixed in
    stacked PR #140
    on top of failing test #141.
    - MCP float64 → big.Int truncation (Major): fixed in
    stacked PR #143
    on top of failing test #142.

🤖 Generated with Claude Code

Color is now carried as a dedicated field on the output Posting type and as
a separate dimension throughout the interpreter, instead of being encoded as
an asset suffix (`USD_RED`). This makes the numscript ↔ host contract clean:
downstream consumers (ledgers, indexers, reporting) can segregate balances
by (account, asset, color) without having to parse asset strings.

BREAKING CHANGES:
- Posting gains `Color string` (json `"color"`).
- BalanceQuery shape: `map[string][]string` → `map[string][]AssetColor`
  where `AssetColor{Asset, Color string}`. Store implementations must
  honor color as a separate filter rather than splitting suffixed assets.
- Balances shape: `map[account]map[asset]*big.Int` →
  `map[account]map[asset]map[color]*big.Int`. The (asset, color) pair is
  now an explicit key. `Balances.UnmarshalJSON` accepts both the new
  colored form and the legacy `{asset: amount}` shorthand (parsed as the
  uncolored bucket) so existing JSON fixtures keep working without a
  forced migration.
- `coloredAsset()` helper removed; nothing in the public or internal API
  encodes color into the asset string anymore.
- New public helper `Uncolored(amount)` to ergonomically build a
  single-bucket `ColorBalance` in tests / static fixtures.

The semantics already exercised by the experimental-asset-colors feature
flag stay the same — only the wire format changes. Existing colored .num
specs were migrated to the new shape and now assert on `posting.color` in
addition to `posting.asset`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

Walkthrough

This PR introduces per-asset-per-color balance tracking throughout the interpreter. Balances now nest by account, asset, and color instead of encoding color into asset strings. BalanceQuery requests (asset, color) pairs via a new AssetColor type. All balance operations, JSON marshalling, queries, and postings are updated to be color-aware, with comprehensive test coverage including a new color-semantics validation suite.

Changes

Asset-Color Balance Model

Layer / File(s) Summary
Core types and data model
internal/interpreter/interpreter.go
AssetColor struct pairs asset with color string, BalanceQuery now requests []AssetColor items per account, AccountBalance becomes per-asset-per-color nested map, Posting gains explicit Color field for emitted transactions.
Balance value operations
internal/interpreter/balances.go
Uncolored() helper wraps amounts under empty-color key, UnmarshalJSON parses flat or {color: amount} nested shapes, DeepClone/Merge/Compare* iterate nested color maps, PrettyPrint expands to show color dimension.
Balance query and retrieval
internal/interpreter/interpreter.go, internal/interpreter/batch_balances_query.go, internal/interpreter/asset_scaling.go
StaticStore.GetBalances resolves (asset, color) items and handles catch-all asset prefixes, batchQuery deduplicates AssetColor entries, getAssets filters only uncolored buckets per prefix.
Posting generation with color
internal/interpreter/interpreter.go
pushReceiver emits Posting.Asset from context and Posting.Color from sender instead of encoding both into asset string.
Test infrastructure and helpers
internal/interpreter/interpreter_test.go, internal/cmd/test_init.go, internal/mcp_impl/handlers.go, internal/specs_format/index.go
TestCase harness adds coloredBalances field and builtBalances() merger, test_init.GetBalances builds from AssetColor items, JSON parsing accepts flat or nested color formats, helper fetchOrInsertBalance creates nested balance pointers.
Balance operations test coverage
internal/interpreter/balances_test.go
Tests use Uncolored()/ColorBalance constructors, verify filtering/cloning/merging respect color, add color-comparison cases (same asset/different colors differ, color subsets respected).
Funds queue color behavior tests
internal/interpreter/funds_queue_color_test.go
White-box tests validate queue compaction respects color, PullColored selectively drains, PullUncolored treats empty color as distinct.
Color semantics validation suite
internal/interpreter/color_semantics_test.go
Comprehensive tests for ExperimentalAssetColors: color propagation, isolation, immutability, JSON behavior, allocation/send-all semantics, balance query inclusion of colors.
Interpreter test harness updates
internal/interpreter/interpreter_test.go
TestStaticStore subtests use AssetColor queries with color filtering and catch-all patterns, TestTrackBalancesTricky/TestColorRestrictBalance use colored-balance mechanisms.
Public API exports and documentation
numscript.go
Exports AssetColor and ColorBalance type aliases, updates BalanceQuery documentation, exports Uncolored() helper.
Test data and expected value updates
internal/specs_format/parse_test.go, internal/specs_format/run_test.go, numscript_test.go
Wraps balance amounts in Uncolored() helper, updates balance queries to AssetColor{Asset: ...} shape instead of raw strings.

🎯 4 (Complex) | ⏱️ ~60 minutes

A rabbit hops through colored coins with glee,
Each bucket now holds its own treasury.
🐰💰🎨
From asset strings emerged a vibrant sight—
Colors dance through balances, pure delight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately reflects the main change: exposing color as a dedicated Posting property instead of encoding it in asset strings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description clearly relates to the changeset, explaining the migration of color from an encoded asset suffix to a dedicated field on the Posting type and a separate dimension in the interpreter.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/color-on-posting

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

❤️ Share

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

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 3, 2026

Codecov Report

❌ Patch coverage is 86.99187% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.21%. Comparing base (5cc059f) to head (cb5e8ce).

Files with missing lines Patch % Lines
internal/mcp_impl/handlers.go 0.00% 12 Missing ⚠️
internal/interpreter/asset_scaling.go 50.00% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #139      +/-   ##
==========================================
+ Coverage   66.96%   67.21%   +0.24%     
==========================================
  Files          47       47              
  Lines        5068     5109      +41     
==========================================
+ Hits         3394     3434      +40     
- Misses       1477     1479       +2     
+ Partials      197      196       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gfyrag gfyrag marked this pull request as ready for review June 3, 2026 10:56
@gfyrag gfyrag requested review from Azorlogh and ascandone June 3, 2026 10:56
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@internal/interpreter/asset_scaling.go`:
- Around line 46-55: The loop that selects assets uses strings.HasPrefix(asset,
baseAsset) which wrongly includes assets like "USDT" for baseAsset "USD"; update
the condition in the loop (the block iterating over balance and referencing
getAssetScale and result) to accept only the exact base asset or children in the
form baseAsset + "/" (i.e., asset == baseAsset || strings.HasPrefix(asset,
baseAsset + "/")), leaving the rest of the logic (retrieving amount from
colorMap, calling getAssetScale, and assigning result[scale]) unchanged.
- Around line 41-55: getAssets is dropping the color dimension by hardcoding
colorMap[""], which allows colored scaling paths to improperly use uncolored
buckets; fix by making getAssets color-aware: change its signature to accept a
color string (e.g., getAssets(balance AccountBalance, baseAsset, color string)
map[int64]*big.Int), look up amount := colorMap[color] instead of colorMap[""],
and propagate this new parameter to all callers (or alternatively validate and
reject non-empty colors before calling getAssets if you prefer to keep the old
signature); ensure callers in scaling lookup pass source.Color so the correct
color bucket is used.

In `@internal/mcp_impl/handlers.go`:
- Around line 40-57: The code truncates float64 to *big.Int (via
big.NewFloat(...).Int) for both the top-level amount (perAssetRaw -> amount) and
per-color amounts (colorMap -> amountRaw -> amount), allowing fractional values
like 100.9 and losing precision for large integers; change the parsing to
require exact integers: use json.Number (or decoder.UseNumber) or otherwise
validate that the incoming numeric value is an integer (no fractional part and
within exact range) before converting to big.Int, and return a
NewToolResultError when a non-integer or out-of-range value is encountered;
update the branches handling perAssetRaw and amountRaw and ensure conversion to
big.Int uses a string-to-big.Int path (e.g., SetString) rather than
big.NewFloat(...).Int to avoid truncation and rounding issues.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3ea33a0b-01a1-4d14-9fef-bd22c31487a7

📥 Commits

Reviewing files that changed from the base of the PR and between 5cc059f and f4fae45.

⛔ Files ignored due to path filters (12)
  • internal/interpreter/__snapshots__/balances_test.snap is excluded by !**/*.snap, !**/*.snap
  • internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder-send-all.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance-when-missing-funds.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restriction-in-send-all.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-colors/color-send-overdrat.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-colors/color-send.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-colors/color-with-asset-precision.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send-all.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send.num.specs.json is excluded by !**/*.json
  • internal/specs_format/__snapshots__/runner_test.snap is excluded by !**/*.snap, !**/*.snap
📒 Files selected for processing (16)
  • internal/cmd/test_init.go
  • internal/cmd/test_init_test.go
  • internal/interpreter/asset_scaling.go
  • internal/interpreter/balances.go
  • internal/interpreter/balances_test.go
  • internal/interpreter/batch_balances_query.go
  • internal/interpreter/color_semantics_test.go
  • internal/interpreter/funds_queue_color_test.go
  • internal/interpreter/interpreter.go
  • internal/interpreter/interpreter_test.go
  • internal/mcp_impl/handlers.go
  • internal/specs_format/index.go
  • internal/specs_format/parse_test.go
  • internal/specs_format/run_test.go
  • numscript.go
  • numscript_test.go

Comment on lines +41 to +55
// getAssets returns, for a given baseAsset, the per-scale uncolored balance.
// Asset scaling operates on the uncolored bucket only — colored funds are not
// implicitly converted across scales.
func getAssets(balance AccountBalance, baseAsset string) map[int64]*big.Int {
result := make(map[int64]*big.Int)
for asset, amount := range balance {
if strings.HasPrefix(asset, baseAsset) {
_, scale := getAssetScale(asset)
result[scale] = amount
for asset, colorMap := range balance {
if !strings.HasPrefix(asset, baseAsset) {
continue
}
amount, ok := colorMap[""]
if !ok {
continue
}
_, scale := getAssetScale(asset)
result[scale] = amount
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Don't drop the color dimension during scaling lookup.

getAssets now hardcodes colorMap[""], but scaling sources still accept source.Color. That means a colored scaling path can convert uncolored balances and then withdraw nothing from the requested color bucket, which is enough for send all to emit bogus intermediary conversion postings. Either make this helper color-aware or reject non-empty colors before calling it.

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

In `@internal/interpreter/asset_scaling.go` around lines 41 - 55, getAssets is
dropping the color dimension by hardcoding colorMap[""], which allows colored
scaling paths to improperly use uncolored buckets; fix by making getAssets
color-aware: change its signature to accept a color string (e.g.,
getAssets(balance AccountBalance, baseAsset, color string) map[int64]*big.Int),
look up amount := colorMap[color] instead of colorMap[""], and propagate this
new parameter to all callers (or alternatively validate and reject non-empty
colors before calling getAssets if you prefer to keep the old signature); ensure
callers in scaling lookup pass source.Color so the correct color bucket is used.

Comment on lines +46 to +55
for asset, colorMap := range balance {
if !strings.HasPrefix(asset, baseAsset) {
continue
}
amount, ok := colorMap[""]
if !ok {
continue
}
_, scale := getAssetScale(asset)
result[scale] = amount
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use exact base-asset matching here.

strings.HasPrefix(asset, baseAsset) will treat unrelated assets like USDT as part of the USD scaling pool. Match only the base asset itself or baseAsset/… children.

Suggested fix
 	for asset, colorMap := range balance {
-		if !strings.HasPrefix(asset, baseAsset) {
+		if asset != baseAsset && !strings.HasPrefix(asset, baseAsset+"/") {
 			continue
 		}
 		amount, ok := colorMap[""]
 		if !ok {
 			continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/interpreter/asset_scaling.go` around lines 46 - 55, The loop that
selects assets uses strings.HasPrefix(asset, baseAsset) which wrongly includes
assets like "USDT" for baseAsset "USD"; update the condition in the loop (the
block iterating over balance and referencing getAssetScale and result) to accept
only the exact base asset or children in the form baseAsset + "/" (i.e., asset
== baseAsset || strings.HasPrefix(asset, baseAsset + "/")), leaving the rest of
the logic (retrieving amount from colorMap, calling getAssetScale, and assigning
result[scale]) unchanged.

Comment thread internal/mcp_impl/handlers.go Outdated
@gfyrag gfyrag marked this pull request as draft June 3, 2026 11:05
gfyrag and others added 2 commits June 3, 2026 13:11
Per ledger #234 feedback: the dual JSON parse path was a backward-compat
shim that masked the schema break. With the new (asset, color) keying,
the only canonical shape is

    {"<account>": {"<asset>": {"<color>": <amount>, ...}}}

where color "" is the uncolored bucket. The flat shorthand
\`{"<asset>": <amount>}\` is no longer accepted; balances must spell out
the empty-color key explicitly.

Changes:
- Remove \`Balances.UnmarshalJSON\` and \`decodeColorBalance\`. Go's default
  JSON unmarshal walks the natural 3-level map directly.
- \`mcp_impl.parseBalancesJson\` rejects the shorthand with a clearer
  error message (the helper still hand-parses a generic \`map[string]any\`
  coming from the MCP transport).
- Sweep the 121 \`.num.specs.json\` fixtures via jq to convert every
  \`"<asset>": <number>\` to \`"<asset>": {"": <number>}\`. The walk only
  touches \`balances\` (top-level and \`testCases[].balances\`) — movements
  and post-commit volumes keep their native shapes.
- Update \`specs_format\` table tests for the new shape (run_test.go,
  runner_test.go, parse_test.go).
- Refresh the runner_test snapshot (\`TestSchemaErrSpecs\` now reports
  the friendlier \`cannot unmarshal number into ColorBalance\` instead of
  going through the bespoke shorthand path).
- Update \`color_semantics_test.go\`: a single \`TestBalancesJSONShape\`
  asserts the canonical parse, plus \`TestBalancesJSONRejectsFlatShorthand\`
  pins the rejection.

All numscript test packages green; pre-commit clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… invariant

The Pull(amount, color *string) signature accepted an optional color
filter that is never used by the interpreter — the only caller is
pushReceiver, which always passes nil through PullAnything. Whatever
balance check a colored source needs has already happened in
tryTakingFromAccount before any Sender is pushed onto the queue:
CalculateSafeWithdraw caps the pushed amount at the source's
(asset, color) balance, and tryTakingExact raises MissingFundsErr if
the queue can't supply the requested total.

Cleaning up:
- Pull's signature is now Pull(requiredAmount *big.Int) — the unused
  color filter branch is removed and the function gets a doc-comment
  spelling out the upstream-bounded / caller-validated contract.
- PullColored and PullUncolored wrappers are dropped (no in-tree
  callers outside the tests that exercised them directly).
- funds_queue_test.go: the TestPullColored* + TestReconcileColored*
  tests are removed (they covered the removed branch). TestPush
  switches from PullUncolored to PullAnything (functionally identical).
- funds_queue_color_test.go: collapses to a single
  TestFundsQueueCompactDoesNotMergeAcrossColors that still pins the
  invariant we actually care about — compactTop never merges across
  colors. The PullColored / PullUncolored selectivity tests went away
  with their subject.

No behavioral change for the interpreter: the removed branch was
unreachable from anywhere except the deleted tests, and all
color-segregation integration tests (color_semantics_test.go,
testdata/script-tests/experimental/asset-colors/) still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gfyrag added a commit that referenced this pull request Jun 3, 2026
`getAssets(balance, baseAsset)` uses `strings.HasPrefix(asset, baseAsset)`,
which accidentally matches any asset whose key shares the base name as a
string prefix:

  - `USDT` and `USDT/2` are matched as scaled variants of `USD`
  - `USD_RED` and `USD_RED/2` (suffix-encoded color variants emitted by
    the experimental asset-colors feature flag) are also matched

Worse: because the result map is keyed by precision scale only, an entry
like `USD_RED/2 = 500` overwrites the true `USD/2 = 2` value in the
scaling pool. A user holding both `USD/2 = 2` and `USD_RED/2 = 500` on
the same account will see scaling compute conversions against the wrong
amount — silent corruption.

This commit introduces a failing regression test
`TestGetAssetsRejectsSpuriousPrefixMatches` that pins the bug. CI is
intentionally red on this branch.

The fix is in a stacked PR that targets this branch — merging it
flips the test green.

Flagged by CodeRabbit during review of #139.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gfyrag gfyrag marked this pull request as ready for review June 3, 2026 12:21
@gfyrag gfyrag requested a review from altitude June 3, 2026 12:29
if !ok {
return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Expected float for amount: %v", amountRaw))
}
n, _ := big.NewFloat(amount).Int(new(big.Int))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This still converts JSON numbers through float64 and then big.NewFloat(...).Int(...), which silently truncates fractions and can round large integers before conversion. Since balances are ledger amounts, this should reject non-integer or non-exact JSON numbers, or include the validation from PR #143 before #139 is merged standalone.

_, scale := getAssetScale(asset)
result[scale] = amount
for asset, colorMap := range balance {
if !strings.HasPrefix(asset, baseAsset) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This matches any asset starting with baseAsset, so getAssets(balance, "USD") also includes assets like USDC or USDX/2 in the scaling pool. The match should be exact base asset or baseAsset + "/"; otherwise unrelated asset balances can be converted during asset scaling.


n, _ := big.NewFloat(amount).Int(new(big.Int))
iBalances[account][asset] = n
// Expected shape: { "USD/2": { "": 100, "RED": 50 } }.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The parser now expects the strict colored shape, but the MCP evaluate tool description still documents the old flat shape ({"USD/2": 100}). Callers following the advertised schema will get runtime errors. Please update the tool description/example to show {"USD/2": {"": 100}} and mention that "" is the uncolored bucket.

Destination string `json:"destination"`
Amount *big.Int `json:"amount"`
Asset string `json:"asset"`
Color string `json:"color"`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since Color is now part of the posting contract, pretty output should expose it too. PrettyPrintPostings still renders only source, destination, asset, and amount, so numscript run --output-format pretty and spec failure output can hide the only difference between two colored postings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants