Skip to content

feat: scoped accounts#161

Open
ascandone wants to merge 16 commits into
mainfrom
feat/scoped-accounts
Open

feat: scoped accounts#161
ascandone wants to merge 16 commits into
mainfrom
feat/scoped-accounts

Conversation

@ascandone

Copy link
Copy Markdown
Contributor

No description provided.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The PR migrates the interpreter balance representation from a map to a row-based []BalanceRow model with a new Scope field, refactors AccountAddress from a string alias to a Name/Scope struct, introduces InternalBalances as the runtime cache, defines store contracts with scope-aware querying, and adds an experimental scoped() builtin function. Interpreter execution paths, coercion helpers, funds queue, batch query, specs format, account metadata model, and the test suite are all updated to consume these new types.

Changes

Balances model, typed value semantics, and scoped builtin

Layer / File(s) Summary
Feature flag, scope validation, and error type
internal/flags/flags.go, internal/interpreter/append_scope.go, internal/interpreter/interpreter_error.go
Adds ExperimentalScopedFunction flag constant and AllFlags entry, creates scopeRegex/validateScope helper, and defines the InvalidScope interpreter error type.
AccountAddress struct refactor and value helpers
internal/interpreter/value.go, internal/interpreter/value_test.go, internal/interpreter/append_scope_test.go, internal/interpreter/args_parser_test.go
Redefines AccountAddress as {Name, Scope} struct, updates NewAccountAddress, adds MarshalJSON, String(), NewMonetaryIntBig, Asset.GetBaseAndScale(), and refactors all coercion helpers to value semantics (returning T instead of *T).
Row-based Balances and InternalBalances cache
internal/interpreter/balances.go, internal/interpreter/internal_balances.go, internal/interpreter/internal_balances_test.go
Introduces BalanceRow with Scope, redefines Balances as []BalanceRow, adds FirstDuplicate/comparison/printing methods; adds InternalBalances map cache with FromBalancesRows, DeepClone, fetchBalance, Set, filterQuery, Merge, and unit tests for filter, duplicate detection, and cloning.
Store interface and BalanceQueryItem with Scope
internal/interpreter/store.go
Defines BalanceQueryItem (now with Scope), introduces MetadataQueryItem and changes MetadataQuery from map to slice form, implements StaticStore with exact and catch-all GetBalances logic plus nil-safe GetAccountsMetadata.
Scoped builtin: analyzer registration and checkSource
internal/analysis/check.go
Registers FnVarOriginScoped/"scoped" in the Builtins map with (account, string)→account signature and feature-flag guard; updates checkSource for SourceWithScaling to validate Address and Through as account type.
Scoped builtin: runtime function and test script
internal/interpreter/function_exprs.go, internal/interpreter/testdata/script-tests/experimental/scoped-function/simple.num
Implements scoped() runtime function that validates scope via validateScope and returns AccountAddress{Name, Scope} or InvalidScope; adds test script exercising scoped source/destination in a send.
Accounts metadata refactor to row-based representation
internal/interpreter/accounts_metadata.go
Changes AccountsMetadata from nested map to []AccountMetadataRow; introduces AccountMetadataRow struct with Account, Key, Value, Color, and Scope fields; updates PrettyPrint to build CSV from rows; adds CompareAccountsMetadata for order-insensitive comparison.
Internal accounts metadata cache
internal/interpreter/internal_accounts_metadata.go
Introduces InternalAccountsMetadata in-memory cache keyed by composite (Account, Scope, Key); provides FromAccountsMetadataRows constructor, Get/Set accessors, and toRows flattening with deterministic sort.
Function expression and statement updates
internal/interpreter/function_exprs.go, internal/interpreter/function_statements.go, internal/interpreter/get_involved_accounts.go
Updates meta() to use account.Name/account.Scope and CachedAccountsMeta.Get with scope; updates balance() to use account.String(); updates setTxMeta to index by string(key); updates setAccountMeta to use SetAccountsMeta.Set with account/scope; updates get_involved_accounts to use AccountAddress.String().
Funds queue Sender struct and operations
internal/interpreter/funds_queue.go, internal/interpreter/funds_queue_test.go
Replaces Sender.Name string with Sender.Account AccountAddress; updates compactTop merge comparison and Pull split/exact-match construction; updates all test fixtures to new Sender shape.
Batch balance query with typed accounts and Scope
internal/interpreter/batch_balances_query.go
Refactors batchQuery to typed AccountAddress/Asset/String signature with BalanceQueryItem including Scope and slices.Contains deduplication; updates SaveStatement, SendStatement, SourceAccount, SourceWithScaling, SourceOverdraft call sites.
Interpreter program state and execution paths
internal/interpreter/interpreter.go
Removes old type declarations (moved to store.go); adds Posting.SourceScope/DestinationScope; changes CachedBalances to InternalBalances and SetAccountsMeta to InternalAccountsMetadata; refactors all taking/sending/pushing/scaling paths for typed parameters; conditionally renders Color in PrettyPrintPostings; broadens accountNameRegex to allow leading @; handles kept destinations by restoring sender balances.
Specs format: scope-aware balance and metadata handling
internal/specs_format/index.go
Extends duplicate detection and mergeBalances to include Scope; updates mergeAccountsMeta to deduplicate by (account, key, color, scope); rewrites getMovements with joinScope for scope-aware aggregation; rewrites getBalances to key working set by AccountAddress, apply deltas, then flatten with deterministic sort.
Interpreter test suite migration
internal/interpreter/interpreter_test.go
Migrates all test infra and cases from machine.* to interpreter.* types: TestCase holds interpreter.AccountsMetadata/Balances with BalanceRow helpers; removeRange normalizes multiple error variants; testWithFeatureFlag uses interpreter.RunProgram; all error/posting/value expectations updated; safe-withdraw tests migrated.
Public API and integration updates
internal/cmd/test_init.go, numscript.go, numscript_test.go
Updates StaticStore.Meta initialization to empty AccountsMetadata{}; renames public type alias from AccountMetadata to AccountMetadataRow; updates test metadata initialization and query structures to match new row-based representation.

Sequence Diagram(s)

sequenceDiagram
  participant Parser
  participant Analyzer
  participant Interpreter
  participant CachedBalances
  participant Store
  Parser->>Analyzer: scoped(`@src`, "x")
  Analyzer->>Analyzer: checkFnCall validates FnVarOriginScoped registered
  Interpreter->>Interpreter: evaluate scoped(account, scope)
  Interpreter->>Interpreter: expectAccount validates `@src`
  Interpreter->>Interpreter: expectString extracts "x"
  Interpreter->>Interpreter: validateScope checks ^[a-z0-9_]*$
  Interpreter-->>Parser: AccountAddress{Name: "src", Scope: "x"}
  Parser->>Interpreter: send via scoped source
  Interpreter->>CachedBalances: fetchBalance(src/x, asset, color)
  CachedBalances-->>Interpreter: *big.Int balance
  Interpreter->>Store: query balances for src/x
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • formancehq/numscript#160: Refactors balance handling to a row-based Balances/BalanceRow model with CSV PrettyPrint and balance comparison logic.

Suggested reviewers

  • Azorlogh
  • gfyrag

Poem

🐇 Hop hop, the accounts now carry a name and a scope,
No more plain strings — structs help us all cope.
BalanceRow lines up with Color and Scope in a row,
scoped(@src, "x") tells the ledger just so.
The rabbit stamps the cache with a tidy deep clone,
Every test migrated, every pointer now gone! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided by the author, making it impossible to evaluate whether any description content is related to the changeset. Add a pull request description explaining the purpose, scope, and rationale for implementing scoped accounts functionality.
Docstring Coverage ⚠️ Warning Docstring coverage is 11.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding scoped accounts functionality throughout the codebase, including new data structures, validation, and interpreter functions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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.

@ascandone ascandone force-pushed the feat/scoped-accounts branch from 3613cf9 to bcba9a9 Compare June 12, 2026 14:52
NumaryBot

This comment was marked as resolved.

NumaryBot

This comment was marked as resolved.

Base automatically changed from feat/color-as-first-class-citizen to main June 15, 2026 13:13
An error occurred while trying to automatically change base from feat/color-as-first-class-citizen to main June 15, 2026 13:13
NumaryBot

This comment was marked as resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/specs_format/index.go (1)

87-107: ⚠️ Potential issue | 🟡 Minor

Check function has an implicit precondition: specs must be pre-validated, but this is not documented.

While the production code path in runRawSpec (runner.go:160) does guard Check with validateSpecs, the Check function is exported and lacks documentation of this precondition. Tests intentionally call Check directly without validation—which is fine for unit testing—but any external caller or future code path that directly invokes Check could bypass the guard.

Consider either:

  • Adding a docstring to Check documenting the precondition that validateSpecs must be called first
  • Making Check internal (unexported) to enforce the contract at the type level
  • Moving validation into Check itself to ensure it always holds
🤖 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/specs_format/index.go` around lines 87 - 107, The exported Check
function has an implicit precondition that the specs parameter must be
pre-validated by calling validateSpecs first, but this requirement is not
documented. Add a docstring to the Check function that clearly documents this
precondition, explaining that callers must ensure specs have been validated
before invoking Check. This will make the contract explicit and prevent external
callers or future code paths from accidentally bypassing the validation guard.
♻️ Duplicate comments (2)
internal/analysis/check.go (1)

127-131: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the scoped feature flag for scoped() version gating.

Line 130 still references flags.ExperimentalGetAmountFunctionFeatureFlag for FnVarOriginScoped. This makes static analysis require the wrong flag for scoped() and can diverge from interpreter behavior. Switch it to flags.ExperimentalScopedFunction.

Proposed fix
 	FnVarOriginScoped: VarOriginFnCallResolution{
 		Params: []string{TypeAccount, TypeString},
 		Return: TypeAccount,
 		Docs:   "returns the scoped version of that account. Empty string means no scope. Overwrites the previous scope",
 		VersionConstraints: []VersionClause{
 			{
 				Version:     parser.NewVersionInterpreter(0, 0, 25),
-				FeatureFlag: flags.ExperimentalGetAmountFunctionFeatureFlag,
+				FeatureFlag: flags.ExperimentalScopedFunction,
 			},
 		},
 	},
🤖 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/analysis/check.go` around lines 127 - 131, The VersionClause for
FnVarOriginScoped (the scoped() function) in the check.go file is using the
incorrect feature flag. Replace flags.ExperimentalGetAmountFunctionFeatureFlag
with flags.ExperimentalScopedFunction in the FeatureFlag field of the
VersionClause that has Version 0.0.25. This ensures that static analysis uses
the correct scoped feature flag for version gating and aligns with the
interpreter behavior.
internal/interpreter/interpreter.go (1)

184-184: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep canonical account validation free of the DSL @ sigil.

Line 184 now accepts an optional leading @. This allows raw variable values like @src to validate as account addresses and propagate into postings/store-facing data, which breaks canonical account contract boundaries.

Suggested fix
-var accountNameRegex = regexp.MustCompile("^@?" + accountSegmentRegex + "(:" + accountSegmentRegex + ")*(?:/[a-z_]+)?$")
+var accountNameRegex = regexp.MustCompile("^" + accountSegmentRegex + "(:" + accountSegmentRegex + ")*(?:/[a-z_]+)?$")
🤖 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/interpreter.go` at line 184, The accountNameRegex
variable at line 184 includes an optional leading @ character (^@?) which allows
DSL-specific variable values like `@src` to be validated as canonical account
addresses, violating the account contract boundary. Remove the @? optional
matching from the beginning of the regex pattern so it only validates canonical
account names without the DSL @ sigil prefix.
🤖 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/balances.go`:
- Around line 83-88: The CompareBalances function can incorrectly return true
for non-equal multisets when duplicate (account, asset, color) keys exist,
because CompareBalancesIncluding only resolves the first match of each key. Add
duplicate key validation in the CompareBalances function to check both input
Balances for duplicate keys and return false immediately if any duplicates are
found, ensuring that only well-formed (non-duplicate) Balances can pass the
equality check.

In `@internal/interpreter/evaluate_expr.go`:
- Around line 154-158: The code unconditionally calls colorExpr.GetRange() when
constructing the InvalidColor error return at line 157, but colorExpr can be nil
according to the evaluateOptExprAs contract. Add a nil check for colorExpr
before dereferencing it; if colorExpr is nil, either omit the Range field from
the InvalidColor struct or provide a zero/default Range value instead of calling
GetRange().

In `@internal/interpreter/function_exprs.go`:
- Around line 181-185: Remove the invalid pointer dereferences in the scoped()
function. The parameters scope and acc are value types (string type aliases),
not pointers, so they should not be dereferenced with the * operator. Remove the
* prefix from all occurrences: the *scope in the validateScope call, the *scope
in the InvalidScope struct literal, and both *acc and *scope in the appendScope
call.

In `@internal/interpreter/internal_balances.go`:
- Around line 71-82: In the Set method of InternalBalances, the code currently
stores caller-owned *big.Int pointers directly (at both the assignment on line
74 and the append on line 81), which allows external mutation of cached state
and permits nil pointers to enter the runtime balance state. Copy the amount
value instead of storing the pointer directly: create a new big.Int and use the
Set method to copy the value (e.g., new(big.Int).Set(amount)) at both locations
where Amount is assigned, and ensure nil amounts are handled appropriately by
normalizing them to zero or a safe default big.Int value.

In `@internal/interpreter/store.go`:
- Around line 47-52: Add nil checks before dereferencing row.Amount in the
BalanceRow initialization within the store materialization code. When
constructing BalanceRow objects and assigning Amount using
new(big.Int).Set(row.Amount), first check if row.Amount is nil to prevent panics
during query execution. Apply this same nil-checking pattern to all locations
where row.Amount is dereferenced through the Set method call, ensuring the code
gracefully handles null amount values.

In `@internal/mcp_impl/handlers.go`:
- Around line 69-72: The error handling for BindArguments is inconsistent with
other error handling in this handler. Currently, when BindArguments fails, the
code returns (nil, err) which propagates it as a server-level error. Refactor
this to wrap the error in mcp.NewToolResultError(...) and return
(mcp.NewToolResultError(...), nil) instead, matching the pattern used for other
errors in the same handler such as RequireString failures and interpreter
errors. This ensures all tool-level errors are handled consistently as
structured tool result errors rather than server-level errors.

---

Outside diff comments:
In `@internal/specs_format/index.go`:
- Around line 87-107: The exported Check function has an implicit precondition
that the specs parameter must be pre-validated by calling validateSpecs first,
but this requirement is not documented. Add a docstring to the Check function
that clearly documents this precondition, explaining that callers must ensure
specs have been validated before invoking Check. This will make the contract
explicit and prevent external callers or future code paths from accidentally
bypassing the validation guard.

---

Duplicate comments:
In `@internal/analysis/check.go`:
- Around line 127-131: The VersionClause for FnVarOriginScoped (the scoped()
function) in the check.go file is using the incorrect feature flag. Replace
flags.ExperimentalGetAmountFunctionFeatureFlag with
flags.ExperimentalScopedFunction in the FeatureFlag field of the VersionClause
that has Version 0.0.25. This ensures that static analysis uses the correct
scoped feature flag for version gating and aligns with the interpreter behavior.

In `@internal/interpreter/interpreter.go`:
- Line 184: The accountNameRegex variable at line 184 includes an optional
leading @ character (^@?) which allows DSL-specific variable values like `@src` to
be validated as canonical account addresses, violating the account contract
boundary. Remove the @? optional matching from the beginning of the regex
pattern so it only validates canonical account names without the DSL @ sigil
prefix.
🪄 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: ce101ddb-95c1-4b5f-a5ba-b512764f07fe

📥 Commits

Reviewing files that changed from the base of the PR and between cbc11e8 and 7593b6c.

⛔ Files ignored due to path filters (140)
  • inputs.schema.json is excluded by !**/*.json
  • internal/interpreter/testdata/numscript-cookbook/experimental/mixed-source-prefer-single-source.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/numscript-cookbook/experimental/top-up-many.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/numscript-cookbook/experimental/top-up.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/numscript-cookbook/experimental/transfer-example.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/numscript-cookbook/mixed-source-dest-allotment.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/numscript-cookbook/send-max.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/numscript-cookbook/top-up-max.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/allocate-dont-take-too-much.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/allocation.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/ask-balance-twice.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/balance-not-found.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/balance-simple.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/balance.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/bigint-literal.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/capped-when-less-than-needed.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/capped-when-more-than-balance.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/cascading-sources.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/do-not-exceed-overdraft-on-send-all.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/do-not-exceed-overdraft-when-double-spending.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/do-not-exceed-overdraft.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/dynamic-allocation.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/empty-postings.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/account-interpolation/account-interp.num.specs.json is excluded by !**/*.json
  • 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/empty-color.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/interpreter/testdata/script-tests/experimental/asset-scaling/no-solution.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-all-allotment.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-allotment.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-kept.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-send-all.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-with-oneof.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/asset-scaling/update-swap-account-balance.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/get-amount-function/get-amount-function.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/get-asset-function/get-asset-function.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/expr-in-var-origin.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance-after-decrease.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/oneof/oneof-all-failing.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/oneof/oneof-in-send-all.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/oneof/oneof-in-source-send-first-branch.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/oneof/oneof-in-source.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/oneof/oneof-singleton.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/oneof/update-balances-with-oneof.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-use-case-remove-debt.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-negative.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-positive.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-zero.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/overdraft-function/reach-zero.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/simple.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/feature-flag-syntax.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/floating-perc.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/floating-perc2.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/inoder-destination.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/insufficient-funds.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/kept-in-send-all-inorder.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/kept-with-balance.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/many-kept-dest.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/many-max-dest.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/max-with-unbounded-overdraft.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/metadata.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/minus-infix-monetary.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/minus-infix-number.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/minus-prefix-number.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/neg-max-dest.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/negative-max-send-all.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/negative-max.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/nested-remaining-complex.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/ovedrafts-playground-example.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/overdraft-in-send-all-when-noop.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/overdraft-in-send-all.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/overdraft-not-enough-funds.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/overdraft-when-enough-funds.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/overdraft-when-negative-balance-in-send-all.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/overdraft-when-negative-balance.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/overdraft-when-negative-ovedraft-in-send-all.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/overdraft-when-not-enough-funds.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/remaining-kept-in-send-all-inorder.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/remaining-none-in-send-all.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/save/save-from-account__multi-postings.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/save/save-from-account__save-a-different-asset.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/save/save-from-account__save-all-negative-balance.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/save/save-from-account__save-all.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/save/save-from-account__save-causes-failure.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/save/save-from-account__save-more-than-balance.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/save/save-from-account__simple.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/save/save-from-account__with-asset-var.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/save/save-from-account__with-monetary-var.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/send-all-destinatio-allot-complex.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/send-all-destinatio-allot.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/send-all-many-max-in-dest.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/send-all-multi.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/send-all-variable.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/send-all-when-negative-with-overdraft.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/send-all-when-negative.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/send-all.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/send-allt-max-in-dest.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/send-allt-max-in-src.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/send-allt-max-when-no-amount.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/send-when-negative-balance.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/send-zero.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/send.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/source-allotment-invalid-amt.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/source-allotment.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/source-complex.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/source-overlapping.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/source.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/track-balances-send-all.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/track-balances.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/track-balances2.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/track-balances3.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/unbounded-overdraft-when-not-enough-funds.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/update-balances.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/use-balance-twice.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/use-different-assets-with-same-source-account.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/variable-asset.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/variable-balance__1.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/variable-balance__2.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/variable-balance__3.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/variable-balance__4.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/variable-balance__5.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/variables-json.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/variables.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/world-source.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/zero-postings-explicit-allotment.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/zero-postings-explicit-inorder.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/zero-postings.num.specs.json is excluded by !**/*.json
  • internal/parser/__snapshots__/parser_test.snap is excluded by !**/*.snap, !**/*.snap
  • internal/specs_format/__snapshots__/runner_test.snap is excluded by !**/*.snap, !**/*.snap
  • specs.schema.json is excluded by !**/*.json
📒 Files selected for processing (36)
  • internal/analysis/check.go
  • internal/cmd/run.go
  • internal/cmd/test_init.go
  • internal/cmd/test_init_test.go
  • internal/flags/flags.go
  • internal/interpreter/accounts_metadata.go
  • internal/interpreter/append_scope.go
  • internal/interpreter/append_scope_test.go
  • internal/interpreter/args_parser.go
  • internal/interpreter/args_parser_test.go
  • internal/interpreter/asset_scaling.go
  • internal/interpreter/balances.go
  • internal/interpreter/balances_test.go
  • internal/interpreter/batch_balances_query.go
  • internal/interpreter/evaluate_expr.go
  • internal/interpreter/function_exprs.go
  • internal/interpreter/function_statements.go
  • internal/interpreter/infix_overloads.go
  • internal/interpreter/internal_balances.go
  • internal/interpreter/internal_balances_test.go
  • internal/interpreter/interpreter.go
  • internal/interpreter/interpreter_error.go
  • internal/interpreter/interpreter_test.go
  • internal/interpreter/store.go
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/simple.num
  • internal/interpreter/value.go
  • internal/mcp_impl/handlers.go
  • internal/parser/ast.go
  • internal/specs_format/index.go
  • internal/specs_format/parse_test.go
  • internal/specs_format/run_test.go
  • internal/specs_format/runner.go
  • internal/specs_format/runner_test.go
  • internal/utils/utils.go
  • numscript.go
  • numscript_test.go
💤 Files with no reviewable changes (2)
  • internal/utils/utils.go
  • internal/parser/ast.go

Comment thread internal/interpreter/balances.go
Comment on lines +154 to +158
isValidColor := colorRe.Match([]byte(string(color)))
if !isValidColor {
return nil, InvalidColor{
return "", InvalidColor{
Range: colorExpr.GetRange(),
Color: *color,
Color: string(color),

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

Guard nil colorExpr before using GetRange().

Line 157 dereferences colorExpr even though evaluateOptExprAs permits colorExpr == nil. If empty color is rejected, this can panic at runtime.

Proposed fix
 func (s *programState) evaluateColor(colorExpr parser.ValueExpr) (String, InterpreterError) {
+	if colorExpr == nil {
+		return "", nil
+	}
+
 	color, err := evaluateOptExprAs(s, colorExpr, expectString)
 	if err != nil {
 		return "", err
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
isValidColor := colorRe.Match([]byte(string(color)))
if !isValidColor {
return nil, InvalidColor{
return "", InvalidColor{
Range: colorExpr.GetRange(),
Color: *color,
Color: string(color),
func (s *programState) evaluateColor(colorExpr parser.ValueExpr) (String, InterpreterError) {
if colorExpr == nil {
return "", nil
}
color, err := evaluateOptExprAs(s, colorExpr, expectString)
if err != nil {
return "", err
}
// ... rest of function continues
🤖 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/evaluate_expr.go` around lines 154 - 158, The code
unconditionally calls colorExpr.GetRange() when constructing the InvalidColor
error return at line 157, but colorExpr can be nil according to the
evaluateOptExprAs contract. Add a nil check for colorExpr before dereferencing
it; if colorExpr is nil, either omit the Range field from the InvalidColor
struct or provide a zero/default Range value instead of calling GetRange().

Comment thread internal/interpreter/function_exprs.go Outdated
Comment thread internal/interpreter/internal_balances.go Outdated
Comment thread internal/interpreter/store.go
Comment on lines +69 to 72
err = request.BindArguments(&args)
if err != nil {
return nil, err
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent error handling for BindArguments failure.

Other errors in this handler (e.g., RequireString at line 53, parse errors at line 62, interpreter errors at line 84) return (mcp.NewToolResultError(...), nil), treating them as tool-level errors. Returning (nil, err) here propagates the error as a server-level error, which may cause different client behavior (e.g., no structured error message).

Proposed fix for consistent error handling
 	err = request.BindArguments(&args)
 	if err != nil {
-		return nil, err
+		return mcp.NewToolResultError(err.Error()), nil
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
err = request.BindArguments(&args)
if err != nil {
return nil, err
}
err = request.BindArguments(&args)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
🤖 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/mcp_impl/handlers.go` around lines 69 - 72, The error handling for
BindArguments is inconsistent with other error handling in this handler.
Currently, when BindArguments fails, the code returns (nil, err) which
propagates it as a server-level error. Refactor this to wrap the error in
mcp.NewToolResultError(...) and return (mcp.NewToolResultError(...), nil)
instead, matching the pattern used for other errors in the same handler such as
RequireString failures and interpreter errors. This ensures all tool-level
errors are handled consistently as structured tool result errors rather than
server-level errors.

NumaryBot

This comment was marked as resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/specs_format/index.go (1)

274-289: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

validateSpecs is defined but never called.

The validateSpecs function validates that balance lists don't contain duplicate entries, but Check() never invokes it. Duplicate balance entries will silently use the last value instead of being rejected as the docstring intends.

Add if err := validateSpecs(specs); err != nil { return SpecsResult{}, ... } at the start of Check().

🤖 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/specs_format/index.go` around lines 274 - 289, The validateSpecs
function is defined but never invoked, allowing duplicate balance entries to
silently use the last value instead of being rejected. Add a call to
validateSpecs at the beginning of the Check() function to validate the specs
Balances before any processing occurs; if validateSpecs returns an error, return
a SpecsResult with the error rather than continuing.
♻️ Duplicate comments (2)
internal/interpreter/value.go (1)

44-49: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scoped account strings passed as variable values are not parsed into separate Name/Scope.

When a caller passes an account string containing a scope suffix (e.g., "src/x" as a variable value), NewAccountAddress stores it entirely in the Name field without extracting the scope. This means AccountAddress{Name: "src/x", Scope: ""} is created instead of {Name: "src", Scope: "x"}.

Balance queries and postings will then use the wrong keys, looking for account="src/x", scope="" rather than account="src", scope="x".

🤖 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/value.go` around lines 44 - 49, The NewAccountAddress
function does not parse scoped account strings that contain a scope suffix. When
a string like "src/x" is passed as the src parameter, the entire string is
stored in the Name field without extracting the scope portion. Modify
NewAccountAddress to parse the src string, extract any scope suffix (typically
separated by a delimiter), and populate both the Name and Scope fields of the
returned AccountAddress struct accordingly. The validation with checkAccountName
should be applied appropriately to handle both scoped and unscoped account
strings.
internal/interpreter/interpreter.go (1)

199-206: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Account regex rejects digit-containing scopes that validateScope accepts.

The regex pattern (?:/[a-z_]+)? only allows lowercase letters and underscores in the scope suffix, but validateScope accepts digits (e.g., "x1"). A script using scoped(@src, "x1") will pass scope validation during interpretation but then fail checkPostingInvariants when the posting is created.

Align the regex with validateScope by changing [a-z_]+ to [a-z0-9_]+.

🤖 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/interpreter.go` around lines 199 - 206, The account name
regex pattern in accountNameRegex uses a character class [a-z_]+ for the scope
suffix that does not match the validation logic in validateScope, which accepts
digits. Update the regex pattern to include digits by changing the scope suffix
pattern from (?:/[a-z_]+)? to (?:/[a-z0-9_]+)? so that the regex accepts
digit-containing scopes like "x1" that validateScope permits, preventing
validation failures downstream.
🧹 Nitpick comments (3)
internal/interpreter/value_test.go (1)

43-51: ⚡ Quick win

Consider adding test coverage for scoped address JSON marshaling.

The test only covers an AccountAddress without a scope. Adding a test case for AccountAddress{Name: "abc", Scope: "xyz"} marshaling to "abc/xyz" would ensure the scope formatting logic is verified.

🤖 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/value_test.go` around lines 43 - 51, The
TestMarshalAddress function currently only tests marshaling of AccountAddress
with a Name field but no Scope. Add an additional test case within
TestMarshalAddress to verify that AccountAddress with both Name and Scope fields
(e.g., Name: "abc", Scope: "xyz") marshals correctly to the format "abc/xyz".
This ensures the scope formatting logic is properly covered by the test suite.
internal/interpreter/interpreter.go (1)

581-586: 💤 Low value

Direct map access on CachedBalances assumes account was pre-queried.

The code accesses s.CachedBalances[account] directly and returns an error if the key is missing. This relies on the balance query batching having populated that key. If the batching logic ever changes or misses a path, this will surface as an opaque InvalidUnboundedAddressInScalingAddress error rather than indicating the actual cache miss.

This is a potential fragility but not an immediate bug given current batching coverage.

🤖 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/interpreter.go` around lines 581 - 586, The direct map
access on s.CachedBalances[account] at the point where the ok check fails
assumes the balance query batching has already populated that key, but provides
no explicit verification or clear indication when a cache miss occurs. Instead
of returning the generic InvalidUnboundedAddressInScalingAddress error, add
logging or improve the error message to explicitly indicate that the account's
balance was not found in the cache, making it clear that the issue is a cache
miss rather than an unbounded address problem. This will help surface any gaps
in the balance query batching logic more explicitly.
internal/interpreter/value.go (1)

275-287: 💤 Low value

Silent fallback to scale=0 on parse failure may hide malformed assets.

When the scale suffix cannot be parsed as an integer, GetBaseAndScale silently returns scale=0. This could mask malformed asset strings that look valid but have non-numeric scale suffixes (e.g., "USD/abc").

Consider whether callers need to distinguish between "no scale specified" and "invalid scale format".

🤖 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/value.go` around lines 275 - 287, The GetBaseAndScale
function on the Asset type silently masks malformed asset strings by returning
scale=0 when the scale suffix fails to parse as an integer, making it impossible
for callers to distinguish between a missing scale and an invalid scale format.
Modify the GetBaseAndScale function signature to return a third error value
(changing from (string, int64) to (string, int64, error)), and when the
strconv.ParseInt call fails, return the error instead of silently returning 0.
This allows callers to detect and handle genuinely malformed assets like
"USD/abc" rather than treating them the same as valid assets without a scale
suffix.
🤖 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.

Outside diff comments:
In `@internal/specs_format/index.go`:
- Around line 274-289: The validateSpecs function is defined but never invoked,
allowing duplicate balance entries to silently use the last value instead of
being rejected. Add a call to validateSpecs at the beginning of the Check()
function to validate the specs Balances before any processing occurs; if
validateSpecs returns an error, return a SpecsResult with the error rather than
continuing.

---

Duplicate comments:
In `@internal/interpreter/interpreter.go`:
- Around line 199-206: The account name regex pattern in accountNameRegex uses a
character class [a-z_]+ for the scope suffix that does not match the validation
logic in validateScope, which accepts digits. Update the regex pattern to
include digits by changing the scope suffix pattern from (?:/[a-z_]+)? to
(?:/[a-z0-9_]+)? so that the regex accepts digit-containing scopes like "x1"
that validateScope permits, preventing validation failures downstream.

In `@internal/interpreter/value.go`:
- Around line 44-49: The NewAccountAddress function does not parse scoped
account strings that contain a scope suffix. When a string like "src/x" is
passed as the src parameter, the entire string is stored in the Name field
without extracting the scope portion. Modify NewAccountAddress to parse the src
string, extract any scope suffix (typically separated by a delimiter), and
populate both the Name and Scope fields of the returned AccountAddress struct
accordingly. The validation with checkAccountName should be applied
appropriately to handle both scoped and unscoped account strings.

---

Nitpick comments:
In `@internal/interpreter/interpreter.go`:
- Around line 581-586: The direct map access on s.CachedBalances[account] at the
point where the ok check fails assumes the balance query batching has already
populated that key, but provides no explicit verification or clear indication
when a cache miss occurs. Instead of returning the generic
InvalidUnboundedAddressInScalingAddress error, add logging or improve the error
message to explicitly indicate that the account's balance was not found in the
cache, making it clear that the issue is a cache miss rather than an unbounded
address problem. This will help surface any gaps in the balance query batching
logic more explicitly.

In `@internal/interpreter/value_test.go`:
- Around line 43-51: The TestMarshalAddress function currently only tests
marshaling of AccountAddress with a Name field but no Scope. Add an additional
test case within TestMarshalAddress to verify that AccountAddress with both Name
and Scope fields (e.g., Name: "abc", Scope: "xyz") marshals correctly to the
format "abc/xyz". This ensures the scope formatting logic is properly covered by
the test suite.

In `@internal/interpreter/value.go`:
- Around line 275-287: The GetBaseAndScale function on the Asset type silently
masks malformed asset strings by returning scale=0 when the scale suffix fails
to parse as an integer, making it impossible for callers to distinguish between
a missing scale and an invalid scale format. Modify the GetBaseAndScale function
signature to return a third error value (changing from (string, int64) to
(string, int64, error)), and when the strconv.ParseInt call fails, return the
error instead of silently returning 0. This allows callers to detect and handle
genuinely malformed assets like "USD/abc" rather than treating them the same as
valid assets without a scale suffix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3b25e2ab-7af6-44c2-882c-bbc9c4753249

📥 Commits

Reviewing files that changed from the base of the PR and between 7593b6c and e73d595.

📒 Files selected for processing (18)
  • internal/interpreter/append_scope.go
  • internal/interpreter/append_scope_test.go
  • internal/interpreter/args_parser_test.go
  • internal/interpreter/balances.go
  • internal/interpreter/batch_balances_query.go
  • internal/interpreter/function_exprs.go
  • internal/interpreter/function_statements.go
  • internal/interpreter/funds_queue.go
  • internal/interpreter/funds_queue_test.go
  • internal/interpreter/get_involved_accounts.go
  • internal/interpreter/internal_balances.go
  • internal/interpreter/internal_balances_test.go
  • internal/interpreter/interpreter.go
  • internal/interpreter/interpreter_test.go
  • internal/interpreter/store.go
  • internal/interpreter/value.go
  • internal/interpreter/value_test.go
  • internal/specs_format/index.go
💤 Files with no reviewable changes (1)
  • internal/interpreter/append_scope.go
✅ Files skipped from review due to trivial changes (1)
  • internal/interpreter/internal_balances_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/interpreter/balances.go
  • internal/interpreter/function_exprs.go
  • internal/interpreter/args_parser_test.go
  • internal/interpreter/interpreter_test.go

NumaryBot

This comment was marked as resolved.

@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.42857% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.66%. Comparing base (cbc11e8) to head (c0851b9).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/specs_format/index.go 80.64% 8 Missing and 4 partials ⚠️
internal/cmd/run.go 0.00% 7 Missing ⚠️
internal/interpreter/accounts_metadata.go 82.60% 2 Missing and 2 partials ⚠️
internal/interpreter/function_exprs.go 78.94% 2 Missing and 2 partials ⚠️
internal/mcp_impl/handlers.go 50.00% 3 Missing and 1 partial ⚠️
internal/interpreter/internal_accounts_metadata.go 92.30% 1 Missing and 1 partial ⚠️
internal/interpreter/internal_balances.go 81.81% 2 Missing ⚠️
internal/interpreter/interpreter_error.go 0.00% 2 Missing ⚠️
internal/cmd/test_init.go 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #161      +/-   ##
==========================================
+ Coverage   67.81%   69.66%   +1.84%     
==========================================
  Files          49       58       +9     
  Lines        5223     5531     +308     
==========================================
+ Hits         3542     3853     +311     
+ Misses       1472     1460      -12     
- Partials      209      218       +9     

☔ View full report in Codecov by Harness.
📢 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.

NumaryBot

This comment was marked as resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@numscript.go`:
- Line 63: The AccountMetadataRow alias on line 63 points to
interpreter.AccountMetadataRow which is not defined or exported, causing a build
failure. Either locate the correct exported type name from the interpreter
package and update the alias to reference that existing type, or ensure that
AccountMetadataRow is properly exported from the internal/interpreter package so
it can be aliased here.
🪄 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: 14d3feb5-8866-4448-9cf0-e92ffcf4d5b8

📥 Commits

Reviewing files that changed from the base of the PR and between 4ce3eda and 7647b9b.

⛔ Files ignored due to path filters (6)
  • inputs.schema.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/metadata.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/neg-max-dest.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/override-account-meta.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/set-account-meta.num.specs.json is excluded by !**/*.json
  • specs.schema.json is excluded by !**/*.json
📒 Files selected for processing (6)
  • internal/cmd/test_init.go
  • internal/interpreter/function_exprs.go
  • internal/interpreter/interpreter_test.go
  • internal/interpreter/store.go
  • numscript.go
  • numscript_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/interpreter/store.go
  • internal/interpreter/interpreter_test.go

Comment thread numscript.go
NumaryBot

This comment was marked as resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/interpreter/accounts_metadata.go (1)

22-30: ⚡ Quick win

Include Scope/Color in metadata pretty-print output.

Line 27 currently drops scoped/color dimensions, so distinct metadata rows can print identically and become hard to debug.

Suggested refactor
 func (m AccountsMetadata) PrettyPrint() string {
-	header := []string{"Account", "Name", "Value"}
+	header := []string{"Account", "Name", "Value"}
+	showColor := false
+	showScope := false
+	for _, row := range m {
+		showColor = showColor || row.Color != ""
+		showScope = showScope || row.Scope != ""
+	}
+	if showColor {
+		header = append(header, "Color")
+	}
+	if showScope {
+		header = append(header, "Scope")
+	}
 
 	var rows [][]string
 	for _, row := range m {
-		rows = append(rows, []string{row.Account, row.Key, row.Value})
+		out := []string{row.Account, row.Key, row.Value}
+		if showColor {
+			out = append(out, row.Color)
+		}
+		if showScope {
+			out = append(out, row.Scope)
+		}
+		rows = append(rows, out)
 	}
 
 	return utils.CsvPretty(header, rows, true)
 }
🤖 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/accounts_metadata.go` around lines 22 - 30, The
PrettyPrint method in AccountsMetadata is missing the Scope and Color dimensions
in its output, which causes distinct metadata rows to print identically. Add
"Scope" and "Color" to the header slice alongside "Account", "Name", and
"Value", and then modify the loop that builds rows to include the Scope and
Color fields from each row object (row.Scope and row.Color) when appending to
the rows slice, ensuring all dimensions are captured in the formatted output.
🤖 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/accounts_metadata.go`:
- Around line 35-44: The CompareAccountsMetadata function uses slices.Contains
to check if elements from slice a exist in slice b, but this approach only
verifies presence and not count, allowing unequal datasets with duplicate values
to incorrectly return true. Fix this by implementing proper multiset comparison
that accounts for element counts: either sort both AccountsMetadata slices and
use slices.Equal for direct comparison, or use a map-based approach to count
occurrences of each element in both slices and verify the counts match. Ensure
the fix correctly handles the case where slices have equal length but different
element frequencies.

---

Nitpick comments:
In `@internal/interpreter/accounts_metadata.go`:
- Around line 22-30: The PrettyPrint method in AccountsMetadata is missing the
Scope and Color dimensions in its output, which causes distinct metadata rows to
print identically. Add "Scope" and "Color" to the header slice alongside
"Account", "Name", and "Value", and then modify the loop that builds rows to
include the Scope and Color fields from each row object (row.Scope and
row.Color) when appending to the rows slice, ensuring all dimensions are
captured in the formatted output.
🪄 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: af3f1cfb-a4a2-4cfc-853a-d9e730c2b869

📥 Commits

Reviewing files that changed from the base of the PR and between 7647b9b and d353ea3.

📒 Files selected for processing (6)
  • internal/interpreter/accounts_metadata.go
  • internal/interpreter/function_exprs.go
  • internal/interpreter/function_statements.go
  • internal/interpreter/internal_accounts_metadata.go
  • internal/interpreter/interpreter.go
  • internal/specs_format/index.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/interpreter/function_exprs.go
  • internal/specs_format/index.go
  • internal/interpreter/interpreter.go

Comment thread internal/interpreter/accounts_metadata.go
@ascandone ascandone force-pushed the feat/scoped-accounts branch from d353ea3 to 62c70e1 Compare June 19, 2026 14:06
NumaryBot

This comment was marked as resolved.

Model accounts as a struct carrying an optional scope instead of encoding
it into the address string. Scope flows structured through balances,
postings, the funds queue, balance/metadata queries and the store.

Account metadata is now row-based externally (AccountsMetadata as a list of
AccountMetadataRow, mirroring Balances) and indexed by a {account, scope,
key} map internally (InternalAccountsMetadata) for fast lookups.
@ascandone ascandone force-pushed the feat/scoped-accounts branch from 62c70e1 to c07d256 Compare June 19, 2026 14:12
NumaryBot

This comment was marked as resolved.

NumaryBot

This comment was marked as resolved.

NumaryBot

This comment was marked as resolved.

NumaryBot

This comment was marked as resolved.

NumaryBot

This comment was marked as resolved.

NumaryBot

This comment was marked as resolved.

NumaryBot

This comment was marked as resolved.

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛑 Changes requested — automated review

The runtime changes are mostly coherent, but the new slice-based metadata comparison can produce false-positive spec assertions when expected metadata contains duplicates.

Comment thread internal/interpreter/accounts_metadata.go
NumaryBot

This comment was marked as resolved.

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛑 Changes requested — automated review

The runtime scoped-account path is mostly covered, but adding the builtin without updating the public involved-account analyzer leaves scoped scripts failing in that API.

Comment thread internal/analysis/check.go
@ascandone

Copy link
Copy Markdown
Contributor Author

@corerabbitai review

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