Skip to content

feat: scoped accounts#161

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

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

Conversation

@ascandone

@ascandone ascandone commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

This PR implements a scoped() function which "tags" accounts as scoped. We now carry around a {name: string, scope: string} struct to represent accounts. No scope is the same as empty scope (empty string). Semantics are same as before, we just represent account with this tuple instead. As a consequence, metadata is keyed by scope as well. This also involves the contract with the numscript consumer

In order to avoid using some "special" syntax to encode that (e.g. acc/scope) we also needed to add a accountScope and destinationScope field in a bunch of place where json repr is involved: inputs format, specs format, postings.

The reason why we aren't using special syntax for the scope had been discussed in previous threads; the TL;DR is:

  • can't quite use "/" due to grammar ambiguity
  • could use # but we want to be able to reserve that for other stuff in the future
  • it wouldn't play well anyway because it'd be something like @acc # "scope" (scopes must be repr'd as a string because they can be variable, and we don't want to introduce a new ad-hoc datatype just for this, for now)

@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

This PR introduces an experimental scoped() builtin function enabling scoped account addresses. AccountAddress is refactored from a string to a {Name, Scope} struct, Balances and AccountsMetadata become row-based slices with scope-aware comparison/merge, and interpreter execution, specs runner, CLI, and MCP handlers are updated to propagate scope end-to-end.

Changes

Scoped accounts and row-based balances/metadata refactor

Layer / File(s) Summary
Feature flag, scope validation, and error types
internal/flags/flags.go, internal/interpreter/interpreter.go, internal/interpreter/interpreter_error.go, internal/interpreter/accounts_metadata_test.go
Adds ExperimentalScopedFunction flag, scopeRegex/checkScopeName validation, and new/extended errors (InvalidScope, CannotCastScopedAccountToString, Scope fields on existing errors).
AccountAddress struct refactor
internal/interpreter/value.go, internal/interpreter/value_test.go, internal/interpreter/args_parser_test.go, internal/interpreter/evaluate_expr.go
AccountAddress becomes {Name, Scope}, adds tagged JSON marshalling via ParseTaggedValue, updated String() formatting, and scope-aware castToString.
Row-based Balances model
internal/interpreter/balances.go, internal/interpreter/balances_test.go
BalanceRow gains Scope; FirstDuplicate, PrettyPrint, CompareBalances, CompareBalancesIncluding become scope-aware multiset operations.
CSV pretty-print utility
internal/utils/pretty_csv.go, internal/utils/pretty_csv_test.go
Adds CsvPrettyOmitEmptyCols to drop all-empty columns, used across balances/metadata/postings printers.
Internal balances cache
internal/interpreter/internal_balances.go, internal/interpreter/internal_balances_test.go
InternalBalances re-keyed by AccountAddress; Set, has, filterQuery, Merge updated accordingly.
Store query contracts
internal/interpreter/store.go
BalanceQueryItem/MetadataQueryItem gain Scope; GetBalances matching now scope-aware.
Row-based AccountsMetadata model
internal/interpreter/accounts_metadata.go, internal/interpreter/accounts_metadata_test.go, internal/interpreter/pretty_print_meta_test.go
AccountMetadataRow/AccountsMetadata slice model with scope-aware duplicate detection and CSV printing.
Internal accounts metadata caches
internal/interpreter/internal_accounts_metadata.go, internal/interpreter/set_accounts_metadata.go
Adds read cache InternalAccountsMetadata and write cache internalSetAccountsMeta with tagged-value support.
Scoped builtin registration & runtime
internal/analysis/check.go, internal/interpreter/function_exprs.go, internal/interpreter/testdata/script-tests/experimental/scoped-function/*
Registers FnVarOriginScoped builtin and implements scoped() runtime with test scripts.
Function statement/expression updates
internal/interpreter/function_statements.go
meta(), balance(), setTxMeta, setAccountMeta updated to use account.Name/account.Scope.
Funds queue AccountAddress migration
internal/interpreter/funds_queue.go, internal/interpreter/funds_queue_test.go
Sender.Name replaced with Sender.Account AccountAddress.
Interpreter execution wiring
internal/interpreter/interpreter.go, internal/interpreter/batch_balances_query.go, internal/interpreter/pretty_print_postings_test.go
Posting gains scope fields, sending/pushing/scaling logic uses typed AccountAddress, PrettyPrintPostings shows scope columns.
Interpreter test suite migration
internal/interpreter/interpreter_test.go
Migrates fixtures/assertions to new struct forms; adds scoped-send test scenarios.
Specs format scope-aware merge/comparison
internal/specs_format/index.go, internal/specs_format/compare_movements_test.go, internal/specs_format/runner_test.go
Movement/balance/metadata merging and comparison become scope-aware.
CLI, MCP, public API scope integration
internal/cmd/run.go, internal/cmd/test_init.go, internal/cmd/test_init_test.go, internal/mcp_impl/handlers.go, internal/mcp_impl/handlers_test.go, numscript.go, numscript_test.go
Duplicate detection and public aliases updated for scope.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Script
  participant Interpreter
  participant CachedBalances
  participant FundsQueue
  participant Postings

  Script->>Interpreter: scoped(`@account`, "reserve")
  Interpreter->>Interpreter: checkScopeName(scope)
  Interpreter-->>Script: AccountAddress{Name, Scope}
  Script->>Interpreter: send from scoped account
  Interpreter->>CachedBalances: fetchBalance(AccountAddress)
  CachedBalances-->>Interpreter: amount
  Interpreter->>FundsQueue: pushSender(Sender{Account, Amount})
  FundsQueue-->>Interpreter: pulled senders
  Interpreter->>Postings: newPosting(SourceScope, DestinationScope)
  Postings-->>Script: ExecutionResult with scoped postings
Loading

Possibly related PRs

  • formancehq/numscript#14: Both PRs extend internal/analysis/check.go's Builtins map by registering a new var-origin function (scoped vs overdraft) via the same builtin-resolution mechanism.
  • formancehq/numscript#27: Both PRs modify pushSender/pushReceiver posting-emission logic and <kept>/KEPT_ADDR handling in internal/interpreter/interpreter.go.
  • formancehq/numscript#160: Both PRs modify the same Balances/BalanceRow and Posting structures, one extending with Scope and the other with Color.

Suggested reviewers: Azorlogh, gfyrag

Poem

A rabbit hops with scope in paw,
Scoped accounts now follow the law,
Rows replace maps, balances align,
Metadata sorted, key by key, fine,
Through burrows of code, new paths unfold —
Scoped and true, this warren's gold! 🐇✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No PR description was provided, so there is nothing meaningful to assess. Add a short description of the scoped-accounts changes and any notable behavior or migration notes.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed It is concise and directly reflects the main change: adding scoped accounts.
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.
✨ 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),

@coderabbitai coderabbitai Bot Jun 15, 2026

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().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

that's not a bug, that is never nil

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "evaluateColor" --type=go -C3

Length of output: 4192


🏁 Script executed:

#!/bin/bash
grep -rn "evaluateColor" --include="*.go" .

Length of output: 500


🏁 Script executed:

#!/bin/bash
sed -n '500,545p;670,695p' internal/interpreter/interpreter.go
grep -n "colorExpr" internal/interpreter/*.go
grep -rn "Color \+parser.ValueExpr\|Color parser.ValueExpr" --include="*.go" .
grep -n "Color " internal/parser/ast.go

Length of output: 3207


Confirmed — both call sites in internal/interpreter/interpreter.go pass colorExpr straight through from takeAllFromAccount/tryTakingFromAccount, and both explicitly check if colorExpr != nil right before calling evaluateColor, but that check only gates the feature-flag validation; they still call evaluateColor(colorExpr) unconditionally afterward, including when colorExpr is nil. In that nil case, evaluateOptExprAs short-circuits and returns ("", nil) without invoking expect, so GetRange() is never reached because the err != nil branch isn't taken and the color validation with colorExpr.GetRange() only executes when isValidColor is false — which can't happen when color == "" matches colorRe (assuming the empty string is a valid match). So while colorExpr can indeed be nil at runtime, it never reaches the GetRange() line unless the empty string fails the color regex.

You're right that this isn't a live panic given the current regex/empty-string behavior — withdrawing the concern.

(‑, ‑)ゞ


🧬 Code Graph Analysis Results

internal/interpreter/evaluate_expr.go

L12-L111 (handles all parser.ValueExpr variants; default/unreachable paths call utils.NonExhaustiveMatchPanic and then return nil, nil):

func (st *programState) evaluateExpr(expr parser.ValueExpr) (Value, InterpreterError) {
	switch expr := expr.(type) {
	case *parser.AssetLiteral:
		return Asset(expr.Asset), nil
	case *parser.AccountInterpLiteral:
		var parts []string
		for _, part := range expr.Parts {
			switch part := part.(type) {
			case parser.AccountTextPart:
				parts = append(parts, part.Name)
			case *parser.Variable:
				err := st.checkFeatureFlag(flags.ExperimentalAccountInterpolationFlag)
				if err != nil {
					return nil, err
				}

				value, err := st.evaluateExpr(part)
				if err != nil {
					return nil, err
				}
				strValue, err := castToString(value, expr.Range)
				if err != nil {
					return nil, err
				}
				parts = append(parts, strValue)
			}
		}
		name := strings.Join(parts, "")
		return NewAccountAddress(name)

	case *parser.StringLiteral:
		return String(expr.String), nil
	case *parser.PercentageLiteral:
		return Portion(*expr.ToRatio()), nil
	case *parser.NumberLiteral:
		return MonetaryInt(*expr.Number), nil
	case *parser.MonetaryLiteral:
		asset, err := evaluateExprAs(st, expr.Asset, expectAsset)
		if err != nil {
			return nil, err
		}

		amount, err := evaluateExprAs(st, expr.Amount, expectNumber)
		if err != nil {
			return nil, err
		}

		return Monetary{Asset: asset, Amount: amount}, nil

	case *parser.Variable:
		value, ok := st.ParsedVars[expr.Name]
		if !ok {
			return nil, UnboundVariableErr{
				Name:  expr.Name,
				Range: expr.Range,
			}
		}
		return value, nil

	case *parser.BinaryInfix:
		switch expr.Operator {
		case parser.InfixOperatorPlus:
			return st.plusOp(expr.Left, expr.Right)

		case parser.InfixOperatorMinus:
			return st.subOp(expr.Left, expr.Right)

		case parser.InfixOperatorDiv:
			return st.divOp(expr.Range, expr.Left, expr.Right)

		default:
			utils.NonExhaustiveMatchPanic[any](expr.Operator)
			return nil, nil
		}

	case *parser.Prefix:
		switch expr.Operator {
		case parser.PrefixOperatorMinus:
			return st.unaryNegOp(expr.Expr)

		default:
			utils.NonExhaustiveMatchPanic[any](expr.Operator)
			return nil, nil
		}

	case *parser.FnCall:
		if !st.varOriginPosition {
			err := st.checkFeatureFlag(flags.ExperimentalMidScriptFunctionCall)
			if err != nil {
				return nil, err
			}
		}

		return st.handleFnCall(nil, *expr)

	default:
		utils.NonExhaustiveMatchPanic[any](expr)
		return nil, nil
	}
}

L113-L119 (evaluateOptExprAs returns zero value t with nil error when expr == nil):

func evaluateOptExprAs[T any](st *programState, expr parser.ValueExpr, expect func(Value, parser.Range) (T, InterpreterError)) (T, InterpreterError) {
	var t T
	if expr == nil {
		return t, nil
	}
	return evaluateExprAs(st, expr, expect)
}

L121-L134 (evaluateExprAs does not nil-check expr; it calls expr.GetRange()):

func evaluateExprAs[T any](st *programState, expr parser.ValueExpr, expect func(Value, parser.Range) (T, InterpreterError)) (T, InterpreterError) {
	var default_ T
	value, err := st.evaluateExpr(expr)
	if err != nil {
		return default_, err
	}

	res, err := expect(value, expr.GetRange())
	if err != nil {
		return default_, err
	}

	return res, nil
}

internal/parser/ast.go

L8-L11 (ValueExpr requires Ranged; evaluateExprAs calls expr.GetRange()):

ValueExpr interface {
	Ranged
	valueExpr()
}

internal/utils/utils.go

L8-L10 (NonExhaustiveMatchPanic panics and formats the unexpected value):

func NonExhaustiveMatchPanic[T any](value any) T {
	panic(fmt.Sprintf("Non exhaustive match (got '%#v')", value))
}

Comment thread internal/interpreter/function_exprs.go Outdated
Comment thread internal/interpreter/internal_balances.go Outdated
Comment thread internal/interpreter/store.go
Comment thread internal/mcp_impl/handlers.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.

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 73.48837% with 114 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.74%. Comparing base (1643d54) to head (36267c3).

Files with missing lines Patch % Lines
internal/specs_format/index.go 51.02% 44 Missing and 4 partials ⚠️
internal/interpreter/value.go 60.00% 16 Missing and 14 partials ⚠️
internal/interpreter/set_accounts_metadata.go 83.33% 4 Missing and 4 partials ⚠️
internal/cmd/run.go 0.00% 7 Missing ⚠️
internal/interpreter/interpreter_error.go 0.00% 7 Missing ⚠️
internal/mcp_impl/handlers.go 44.44% 4 Missing and 1 partial ⚠️
internal/interpreter/function_exprs.go 83.33% 2 Missing and 2 partials ⚠️
internal/interpreter/accounts_metadata.go 83.33% 1 Missing and 1 partial ⚠️
internal/interpreter/internal_balances.go 81.81% 2 Missing ⚠️
internal/cmd/test_init.go 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #161      +/-   ##
==========================================
+ Coverage   70.04%   70.74%   +0.70%     
==========================================
  Files          55       57       +2     
  Lines        5024     5258     +234     
==========================================
+ Hits         3519     3720     +201     
- Misses       1313     1324      +11     
- Partials      192      214      +22     

☔ 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 Outdated
@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.

@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 Outdated
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

@ascandone ascandone force-pushed the feat/scoped-accounts branch from f8c7fae to 4eb0731 Compare June 30, 2026 08:59
@NumaryBot

NumaryBot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

🛑 Changes requested — automated review

The PR introduces scoped accounts to Numscript but has several significant correctness issues that must be addressed before merging. Two items that were previously flagged as blockers (undefined AccountMetadataRow type alias and scoped world account bypassing overdraft checks) appear in resolved threads, indicating they may have been addressed in subsequent commits — however, the feature-flag mismatch between the static analyzer and the runtime remains unresolved and open. Additional major issues include slash-scoped account strings not being split into their Scope field, TestInitStore not propagating Scope for generated balances, CompareAccountsMetadata using a subset check instead of a multiset comparison, and GetInvolvedAccounts lacking a case for scoped(). Several minor issues around rendering, pretty-printing, and schema alignment are also present. The analysis/runtime flag mismatch in particular can cause scripts to pass static analysis while failing at runtime (or vice versa), which is a fundamental correctness problem for the new feature.

@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.

NumaryBot posted 10 new inline findings.

Summary: #161 (comment)

Comment thread numscript.go
Comment thread internal/analysis/check.go
Comment thread internal/interpreter/value.go
Comment thread internal/analysis/check.go
Comment thread internal/interpreter/store.go
Comment thread internal/interpreter/accounts_metadata.go
Comment thread internal/mcp_impl/handlers.go
Comment thread internal/interpreter/value.go
Comment thread specs.schema.json
Comment thread internal/interpreter/balances.go

@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.

NumaryBot posted 9 new inline findings.

Summary: #161 (comment)

Comment thread numscript.go
Comment thread internal/analysis/check.go
Comment thread internal/analysis/check.go
Comment thread internal/interpreter/value.go
Comment thread internal/interpreter/store.go
Comment thread internal/interpreter/accounts_metadata.go
Comment thread internal/mcp_impl/handlers.go
Comment thread internal/interpreter/balances.go
Comment thread internal/utils/pretty_csv.go
@ascandone

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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.

NumaryBot posted 7 new inline findings.

Summary: #161 (comment)

Comment thread numscript.go
Comment thread internal/analysis/check.go
Comment thread internal/analysis/check.go
Comment thread internal/interpreter/accounts_metadata.go
Comment thread internal/interpreter/batch_balances_query.go
Comment thread internal/interpreter/value.go
Comment thread internal/utils/pretty_csv.go

@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: 4

Caution

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

⚠️ Outside diff range comments (1)
internal/interpreter/balances.go (1)

100-108: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make subset checks multiplicity-aware.

CompareBalancesIncluding still reuses the first matching row for every duplicate in b1. Since internal/specs_format/index.go uses this helper for expect.endBalances.include, an assertion like [x, x] currently passes against [x]. Count matches the same way as CompareBalances, or reject duplicate rows up front.

Suggested fix
 func CompareBalancesIncluding(b1 Balances, b2 Balances) bool {
-	for _, entry := range b1 {
-		amount2, ok := findRow(b2, entry.Account, entry.Asset, entry.Color, entry.Scope)
-		if !ok || !amountsEqual(entry.Amount, amount2) {
-			return false
-		}
-	}
-	return true
+	type rowKey struct{ account, asset, color, scope, amount string }
+	mk := func(r BalanceRow) rowKey {
+		amount := "0"
+		if r.Amount != nil {
+			amount = r.Amount.String()
+		}
+		return rowKey{r.Account, r.Asset, r.Color, r.Scope, amount}
+	}
+
+	counts := make(map[rowKey]int, len(b2))
+	for _, r := range b2 {
+		counts[mk(r)]++
+	}
+	for _, r := range b1 {
+		k := mk(r)
+		counts[k]--
+		if counts[k] < 0 {
+			return false
+		}
+	}
+	return 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/balances.go` around lines 100 - 108, Make
CompareBalancesIncluding multiplicity-aware: it currently reuses the same
matching row from b2 for duplicate entries in b1, so [x, x] can incorrectly
match [x]. Update the logic in CompareBalancesIncluding to track matched rows
(or otherwise count occurrences the same way CompareBalances does), or
explicitly reject duplicate rows before comparing; keep the behavior consistent
with how internal/specs_format/index.go uses this helper for
expect.endBalances.include.
♻️ Duplicate comments (1)
internal/interpreter/accounts_metadata.go (1)

35-44: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Value column can be dropped when all values are empty strings.

CsvPrettyOmitEmptyCols treats every column uniformly, but only Scope is meant to be optional here per the comment on Line 36. If all rows have an empty Value, the column disappears entirely, making explicit empty-value metadata indistinguishable from the previous always-shown value output. Flagged previously as unresolved.

♻️ Possible fix direction
-func CsvPrettyOmitEmptyCols(header []string, rows [][]string, sortRows bool) string
+func CsvPrettyOmitEmptyCols(header []string, rows [][]string, sortRows bool, optionalCols ...int) string

Then only consider columns in optionalCols for omission, always keeping the rest (e.g. Value).

🤖 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 35 - 44,
AccountsMetadata.PrettyPrint is letting the Value column be omitted because
CsvPrettyOmitEmptyCols currently drops any all-empty column, but only Scope
should be optional. Update the pretty-print path in AccountsMetadata.PrettyPrint
(and/or the CsvPrettyOmitEmptyCols handling it uses) so only columns explicitly
marked optional can be removed. Keep Value always included even when every row
has an empty string, while preserving the existing Scope-dropping behavior.
🧹 Nitpick comments (3)
internal/specs_format/compare_movements_test.go (1)

10-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add scope-sensitive cases here.

This only proves multiplicity and amount handling. compareMovements now keys on SourceScope and DestinationScope too, so a regression that collapses scoped and unscoped movements together would still pass this test.

Suggested addition
 func TestCompareMovementsMultiplicity(t *testing.T) {
 	x := Movement{Source: "world", Destination: "a", Asset: "USD", Amount: big.NewInt(1)}
 	y := Movement{Source: "world", Destination: "b", Asset: "USD", Amount: big.NewInt(1)}
@@
 	// differing amount on the same key is not equal
 	z := Movement{Source: "world", Destination: "a", Asset: "USD", Amount: big.NewInt(2)}
 	require.False(t, compareMovements(Movements{x}, Movements{z}))
+
+	// scope is part of the identity
+	scopedSource := Movement{Source: "world", SourceScope: "s1", Destination: "a", Asset: "USD", Amount: big.NewInt(1)}
+	scopedDestination := Movement{Source: "world", Destination: "a", DestinationScope: "s1", Asset: "USD", Amount: big.NewInt(1)}
+	require.False(t, compareMovements(Movements{x}, Movements{scopedSource}))
+	require.False(t, compareMovements(Movements{x}, Movements{scopedDestination}))
 }
🤖 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/compare_movements_test.go` around lines 10 - 25, Update
TestCompareMovementsMultiplicity to cover SourceScope and DestinationScope in
compareMovements so scoped and unscoped movements are not treated as equal. Add
cases using the existing Movement and compareMovements symbols that assert
identical Source/Destination/Asset/Amount but different scope values do not
compare equal, and that matching scope values still do. Keep the current
multiplicity and amount assertions, but extend them with scope-sensitive pairs
to prevent regressions where scope is ignored.
internal/interpreter/interpreter.go (1)

36-49: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Copy the amount inside newPosting.

newPosting stores the caller-owned *big.Int directly; the pushReceiver path passes queue-owned sender.Amount. Since big.Int is mutable, copy it here so emitted postings cannot accidentally alias later queue/balance operations.

Proposed fix
 func newPosting(source AccountAddress, destination AccountAddress, amount *big.Int, asset string, color string) Posting {
+	amountCopy := new(big.Int).Set(amount)
 	return Posting{
 		Source:           source.Name,
 		SourceScope:      source.Scope,
 		Destination:      destination.Name,
 		DestinationScope: destination.Scope,
-		Amount:           amount,
+		Amount:           amountCopy,
 		Asset:            asset,
 		Color:            color,
 	}
 }
🤖 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 36 - 49, newPosting is
reusing the caller-owned *big.Int directly, which can alias mutable
queue/balance state. Update newPosting in interpreter.go to make an internal
copy of the amount before assigning it to Posting.Amount, using the existing
newPosting helper and its callers like pushReceiver as the fix point. Ensure the
returned Posting owns its own amount value so later mutations to sender.Amount
cannot affect already-emitted postings.
internal/interpreter/value_test.go (1)

91-99: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider comparing parsed values with require.Equal, not just String().

Comparing only via v.String() won't catch a bug that swaps struct fields (e.g. Name/Scope) yet happens to produce the same formatted string, or a parse result of the wrong concrete type with a matching String() output.

♻️ Suggested strengthening
 		parsed, err := interpreter.ParseTaggedValue(j)
 		require.Nil(t, err)
-		// compare on the canonical source form
-		require.Equal(t, v.String(), parsed.String())
+		require.Equal(t, v, parsed)
🤖 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 91 - 99, Strengthen the
ParseTaggedValue round-trip test in value_test by comparing the parsed value
directly instead of only relying on v.String(). Update the assertion in the loop
around interpreter.ParseTaggedValue and the value types being marshaled so the
test verifies the actual concrete value/state (including struct fields like Name
and Scope), which will catch swapped fields or wrong types even when String()
matches.
🤖 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/value.go`:
- Around line 124-129: The tagged JSON account decode branch in ParseTaggedValue
currently returns an AccountAddress from taggedAccount without validation, so it
can admit invalid name/scope values. Update the valueTypeAccount case in
internal/interpreter/value.go to validate the decoded taggedAccount fields using
the same account-name and scope checks used by the other account-entry paths
before constructing the AccountAddress, and return an error on invalid input so
malformed specs/API payloads are rejected early.
- Around line 44-48: Normalize account inputs in NewAccountAddress before
persisting them: the constructor currently stores the raw src value in
AccountAddress.Name, which can preserve a leading @ and break downstream
rendering/querying. Update NewAccountAddress to either strip a single leading @
from accepted inputs before returning AccountAddress{Name: ...}, or reject
`@-prefixed` values consistently so Name always contains the bare identifier.

In `@internal/specs_format/index.go`:
- Around line 21-35: Update ExpectedTxMeta.UnmarshalJSON so it preserves null as
a nil/empty value instead of always allocating a map, matching the behavior of
the other expect.* fields. Also make it accept legacy scalar metadata entries by
handling non-object values before calling interpreter.ParseTaggedValue, so both
tagged objects and simple string/scalar forms continue to unmarshal
successfully. Then ensure the downstream tx-metadata assertion path that
consumes ExpectedTxMeta does not treat a null input as a failure.

In `@numscript.go`:
- Around line 62-68: The removal of the exported ParseResult.GetInvolvedAccounts
API is a breaking change, so restore backward compatibility or provide a clear
migration path. Update numscript.go to keep the public ParseResult method and
the accounts package symbols, even if they delegate to the new
interpreter-backed implementation or return a limited result for scoped
accounts. If you cannot keep the API, add a compatibility shim or ensure the
change is explicitly called out in the release notes/changelog with the
replacement symbol names.

---

Outside diff comments:
In `@internal/interpreter/balances.go`:
- Around line 100-108: Make CompareBalancesIncluding multiplicity-aware: it
currently reuses the same matching row from b2 for duplicate entries in b1, so
[x, x] can incorrectly match [x]. Update the logic in CompareBalancesIncluding
to track matched rows (or otherwise count occurrences the same way
CompareBalances does), or explicitly reject duplicate rows before comparing;
keep the behavior consistent with how internal/specs_format/index.go uses this
helper for expect.endBalances.include.

---

Duplicate comments:
In `@internal/interpreter/accounts_metadata.go`:
- Around line 35-44: AccountsMetadata.PrettyPrint is letting the Value column be
omitted because CsvPrettyOmitEmptyCols currently drops any all-empty column, but
only Scope should be optional. Update the pretty-print path in
AccountsMetadata.PrettyPrint (and/or the CsvPrettyOmitEmptyCols handling it
uses) so only columns explicitly marked optional can be removed. Keep Value
always included even when every row has an empty string, while preserving the
existing Scope-dropping behavior.

---

Nitpick comments:
In `@internal/interpreter/interpreter.go`:
- Around line 36-49: newPosting is reusing the caller-owned *big.Int directly,
which can alias mutable queue/balance state. Update newPosting in interpreter.go
to make an internal copy of the amount before assigning it to Posting.Amount,
using the existing newPosting helper and its callers like pushReceiver as the
fix point. Ensure the returned Posting owns its own amount value so later
mutations to sender.Amount cannot affect already-emitted postings.

In `@internal/interpreter/value_test.go`:
- Around line 91-99: Strengthen the ParseTaggedValue round-trip test in
value_test by comparing the parsed value directly instead of only relying on
v.String(). Update the assertion in the loop around interpreter.ParseTaggedValue
and the value types being marshaled so the test verifies the actual concrete
value/state (including struct fields like Name and Scope), which will catch
swapped fields or wrong types even when String() matches.

In `@internal/specs_format/compare_movements_test.go`:
- Around line 10-25: Update TestCompareMovementsMultiplicity to cover
SourceScope and DestinationScope in compareMovements so scoped and unscoped
movements are not treated as equal. Add cases using the existing Movement and
compareMovements symbols that assert identical Source/Destination/Asset/Amount
but different scope values do not compare equal, and that matching scope values
still do. Keep the current multiplicity and amount assertions, but extend them
with scope-sensitive pairs to prevent regressions where scope is ignored.
🪄 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: 2380aed5-5081-4908-a258-85f7a743da3f

📥 Commits

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

⛔ Files ignored due to path filters (30)
  • inputs.schema.json is excluded by !**/*.json
  • internal/interpreter/__snapshots__/accounts_metadata_test.snap is excluded by !**/*.snap, !**/*.snap
  • internal/interpreter/__snapshots__/pretty_print_meta_test.snap is excluded by !**/*.snap, !**/*.snap
  • internal/interpreter/__snapshots__/pretty_print_postings_test.snap is excluded by !**/*.snap, !**/*.snap
  • internal/interpreter/testdata/numscript-cookbook/experimental/meta-calc.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/add-numbers.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/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/scoped-function/allotment.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/balance.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/capped.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/color-and-scope.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/overdraft.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/read-account-meta.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/save.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/set-account-meta.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/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
  • internal/interpreter/testdata/script-tests/set-tx-meta.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/sub-monetaries.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/sub-numbers.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/specs_format/__snapshots__/runner_test.snap is excluded by !**/*.snap, !**/*.snap
  • internal/utils/__snapshots__/pretty_csv_test.snap is excluded by !**/*.snap, !**/*.snap
  • specs.schema.json is excluded by !**/*.json
📒 Files selected for processing (48)
  • 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/accounts_metadata_test.go
  • internal/interpreter/append_scope.go
  • internal/interpreter/append_scope_test.go
  • internal/interpreter/args_parser_test.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/funds_queue.go
  • internal/interpreter/funds_queue_test.go
  • internal/interpreter/internal_accounts_metadata.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/pretty_print_meta_test.go
  • internal/interpreter/pretty_print_postings_test.go
  • internal/interpreter/set_accounts_metadata.go
  • internal/interpreter/store.go
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/allotment.num
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/balance.num
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/capped.num
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/color-and-scope.num
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/overdraft.num
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/read-account-meta.num
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/save.num
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/set-account-meta.num
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/simple.num
  • internal/interpreter/value.go
  • internal/interpreter/value_test.go
  • internal/mcp_impl/handlers.go
  • internal/mcp_impl/handlers_test.go
  • internal/specs_format/compare_movements_test.go
  • internal/specs_format/index.go
  • internal/specs_format/runner_test.go
  • internal/utils/pretty_csv.go
  • internal/utils/pretty_csv_test.go
  • numscript.go
  • numscript_test.go
✅ Files skipped from review due to trivial changes (4)
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/capped.num
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/set-account-meta.num
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/save.num
  • internal/interpreter/args_parser_test.go
🚧 Files skipped from review as they are similar to previous changes (12)
  • internal/interpreter/internal_balances_test.go
  • internal/cmd/test_init.go
  • internal/interpreter/funds_queue.go
  • internal/interpreter/funds_queue_test.go
  • internal/flags/flags.go
  • internal/interpreter/function_statements.go
  • internal/interpreter/append_scope.go
  • numscript_test.go
  • internal/interpreter/function_exprs.go
  • internal/interpreter/testdata/script-tests/experimental/scoped-function/simple.num
  • internal/interpreter/store.go
  • internal/interpreter/interpreter_test.go

Comment thread internal/interpreter/value.go
Comment thread internal/interpreter/value.go
Comment thread internal/specs_format/index.go Outdated
Comment on lines +21 to +35
func (m *ExpectedTxMeta) UnmarshalJSON(data []byte) error {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
out := ExpectedTxMeta{}
for k, v := range raw {
value, err := interpreter.ParseTaggedValue(v)
if err != nil {
return err
}
out[k] = value
}
*m = out
return nil

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve null and scalar tx-metadata compatibility.

Line 21 now always materializes a non-nil map, so "expect.txMetadata": null starts triggering the assertion at Line 200 instead of behaving like the other expect.* fields. It also rejects existing scalar forms such as "k": "v", because ParseTaggedValue only accepts tagged objects. Both are user-visible spec-format regressions.

Suggested fix
+import "bytes"
+
 func (m *ExpectedTxMeta) UnmarshalJSON(data []byte) error {
+	if bytes.Equal(bytes.TrimSpace(data), []byte("null")) {
+		*m = nil
+		return nil
+	}
+
 	var raw map[string]json.RawMessage
 	if err := json.Unmarshal(data, &raw); err != nil {
 		return err
 	}
-	out := ExpectedTxMeta{}
+	out := make(ExpectedTxMeta, len(raw))
 	for k, v := range raw {
 		value, err := interpreter.ParseTaggedValue(v)
 		if err != nil {
-			return err
+			var s string
+			if err2 := json.Unmarshal(v, &s); err2 == nil {
+				out[k] = interpreter.String(s)
+				continue
+			}
+			return err
 		}
 		out[k] = value
 	}
 	*m = out
 	return nil
 }

Also applies to: 200-217

🤖 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 21 - 35, Update
ExpectedTxMeta.UnmarshalJSON so it preserves null as a nil/empty value instead
of always allocating a map, matching the behavior of the other expect.* fields.
Also make it accept legacy scalar metadata entries by handling non-object values
before calling interpreter.ParseTaggedValue, so both tagged objects and simple
string/scalar forms continue to unmarshal successfully. Then ensure the
downstream tx-metadata assertion path that consumes ExpectedTxMeta does not
treat a null input as a failure.

Comment thread numscript.go

@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.

NumaryBot posted 9 new inline findings.

Summary: #161 (comment)

Comment thread numscript.go
Comment thread internal/interpreter/value.go
Comment thread internal/interpreter/store.go
Comment thread internal/interpreter/accounts_metadata.go
Comment thread internal/mcp_impl/handlers.go
Comment thread internal/interpreter/value.go
Comment thread internal/interpreter/balances.go
Comment thread internal/utils/pretty_csv.go
Comment thread specs.schema.json

@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.

NumaryBot posted 9 new inline findings.

Summary: #161 (comment)

Comment thread internal/interpreter/interpreter.go
Comment thread internal/analysis/check.go
Comment thread internal/interpreter/value.go
Comment thread internal/interpreter/store.go
Comment thread internal/interpreter/accounts_metadata.go
Comment thread internal/interpreter/value.go
Comment thread specs.schema.json
Comment thread internal/mcp_impl/handlers.go
Comment thread internal/utils/pretty_csv.go
@ascandone

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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/interpreter/accounts_metadata_test.go (1)

1-46: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing coverage for the actual AccountsMetadata multiset comparison.

This test thoroughly covers multiplicity semantics for CompareSetAccountsMetadata/SetAccountsMetadata, but this file (the row-based AccountsMetadata model layer) has no equivalent test for the plain AccountsMetadata comparison function. Per review feedback on this PR, that comparison "uses a subset-style check instead of a multiset comparison, which can hide failures when duplicate rows exist." A parallel test here (mirroring this one, but against AccountsMetadata/AccountMetadataRow) would have caught that regression.

Want me to draft a TestCompareAccountsMetadata test with the same multiplicity cases as here?

🤖 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_test.go` around lines 1 - 46, The
row-based AccountsMetadata comparison is missing the same multiplicity coverage
already exercised for CompareSetAccountsMetadata. Add a parallel
TestCompareAccountsMetadata in the interpreter test suite that uses
AccountMetadataRow and AccountsMetadata to verify order-insensitive equality
plus duplicate-sensitive multiset behavior, including cases like [x, x] vs [x,
y] and identical duplicates, so the plain comparison can’t pass with
subset-style logic.
🤖 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/interpreter/accounts_metadata_test.go`:
- Around line 1-46: The row-based AccountsMetadata comparison is missing the
same multiplicity coverage already exercised for CompareSetAccountsMetadata. Add
a parallel TestCompareAccountsMetadata in the interpreter test suite that uses
AccountMetadataRow and AccountsMetadata to verify order-insensitive equality
plus duplicate-sensitive multiset behavior, including cases like [x, x] vs [x,
y] and identical duplicates, so the plain comparison can’t pass with
subset-style logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 26bb24af-4775-4985-9da6-e625fcba4f38

📥 Commits

Reviewing files that changed from the base of the PR and between 118ac39 and e522c0c.

⛔ Files ignored due to path filters (12)
  • internal/interpreter/testdata/numscript-cookbook/experimental/meta-calc.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/add-numbers.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/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/set-account-meta.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/set-tx-meta.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/sub-monetaries.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/sub-numbers.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
  • specs.schema.json is excluded by !**/*.json
📒 Files selected for processing (6)
  • internal/interpreter/accounts_metadata_test.go
  • internal/interpreter/function_exprs.go
  • internal/interpreter/interpreter.go
  • internal/interpreter/value.go
  • internal/interpreter/value_test.go
  • internal/specs_format/index.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/interpreter/function_exprs.go
  • internal/interpreter/value_test.go
  • internal/interpreter/interpreter.go
  • internal/interpreter/value.go

@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.

NumaryBot posted 11 new inline findings.

Summary: #161 (comment)

Comment thread numscript.go
Comment thread internal/interpreter/interpreter.go
Comment thread internal/analysis/check.go
Comment thread internal/interpreter/value.go
Comment thread internal/interpreter/store.go
Comment thread internal/specs_format/index.go
Comment thread numscript.go
Comment thread internal/interpreter/value.go
Comment thread internal/mcp_impl/handlers.go
Comment thread internal/utils/pretty_csv.go

@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.

NumaryBot posted 12 new inline findings.

Summary: #161 (comment)

VersionConstraints: []VersionClause{
{
Version: parser.NewVersionInterpreter(0, 0, 25),
FeatureFlag: flags.ExperimentalScopedFunction,

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.

🟠 [major] Wrong feature flag gates scoped() in static analysis
reported by NumaryBot, codex

scoped() is registered in the static analyzer under experimental-get-amount-function instead of experimental-scoped-function. Scripts that enable the documented scoped flag still receive a missing-flag diagnostic from numscript check/LSP, while scripts enabling only the get-amount flag pass static analysis but fail at runtime because the interpreter checks ExperimentalScopedFunction. This analysis/runtime mismatch makes the feature unreliable for any user relying on static checking.

Suggestion: Change the feature-flag constraint for the scoped builtin in the checker to reference experimental-scoped-function / ExperimentalScopedFunction, matching what the interpreter enforces at runtime.

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{

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.

🟠 [major] Mid-script function call flag not required for scoped() expression usage

When scoped() is used in mid-script expression positions (e.g., source = scoped(@src, "s") or balance(scoped(@src, "s"), USD)), the runtime's evaluateExpr requires experimental-mid-script-function-call in addition to experimental-scoped-function. The analyzer only checks for the scoped flag, so scripts accepted by numscript check can still fail at runtime unless both flags are enabled. The requirement should be made consistent between the checker and interpreter.

Suggestion: Either require both flags in the checker registration, or ensure the runtime does not require the mid-script flag for scoped() expressions, making the two consistent.

return AccountAddress{}, InvalidAccountName{Name: src}
}
return AccountAddress(src), nil
return AccountAddress{Name: src}, nil

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.

🟠 [major] Slash-scoped account strings accepted but not split into AccountAddress.Scope

When an account value arrives as a raw string in the form src/x (the format emitted by AccountAddress.String()), the updated regex accepts it but NewAccountAddress stores the entire string in Name with an empty Scope. Balance and metadata lookups therefore query account="src/x", scope="" instead of account="src", scope="x", so they cannot match scoped rows and postings won't populate sourceScope/destinationScope.

Suggestion: In NewAccountAddress, split the accepted string on / to populate Name and Scope separately when a slash is present.

Account string
Asset string
Color string
Scope string

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.

🟠 [major] TestInitStore does not propagate Scope when seeding generated balances

TestInitStore.GetBalances keys and appends generated balances only by account/asset/color, omitting Scope. When test-init funds a scoped source (e.g., scoped(@src, "x")), the generated spec records an unscoped balance. Replaying the spec then queries the scoped balance as zero and produces incorrect expected balances or assertion failures.

Suggestion: Include Scope in the key used for deduplication and populate BalanceRow{Scope: b.Scope} when appending generated balance rows to the spec store.

}

return utils.CsvPretty(header, rows, true)
return utils.CsvPrettyOmitEmptyCols(header, rows, true)

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.

🟠 [major] CompareAccountsMetadata uses subset check instead of multiset comparison

slices.Contains only checks presence, not count. Two AccountsMetadata slices that differ only in duplicate rows can incorrectly compare as equal (e.g., expected [A, A] vs. actual [A, B] when lengths match), masking spec assertion failures where expected rows appear more times than actual rows.

Suggestion: Use a frequency-count map: increment counts for each row in slice a, decrement for each row in b, and return false if any count goes below zero.

return AccountAddress{Name: v.Name, Scope: v.Scope}, nil

case valueTypeAsset:
var v taggedAsset

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.

🟡 [minor] Asset values not validated when decoding tagged JSON

Tagged JSON account values are validated before becoming in-memory values, but tagged asset values are accepted directly without validation. Payloads like {"type":"asset","name":"bad asset"} can create impossible Asset values that all normal DSL and variable-entry paths would reject, meaning malformed metadata expectations are only caught much later or not at all at the boundary.

Suggestion: Apply the same asset-name validation used elsewhere (e.g., checkAssetName) to the asset and monetary branches of ParseTaggedValue before returning the value.

return CsvPretty(header, rows, sortRows)
}

keep := make([]bool, len(header))

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.

🟡 [minor] CsvPrettyOmitEmptyCols renders empty header for empty result sets

When rows is empty, every keep flag remains false, so the function renders a table with an empty header (|) for empty postings, balances, or metadata. This regresses prior behavior that showed expected column headers even for empty results.

Suggestion: Special-case an empty rows slice to keep all non-optional headers, or treat the header row itself as a signal to preserve column visibility.

Asset string `json:"asset"`
Amount *big.Int `json:"amount"`
Color string `json:"color,omitempty"`
Scope string `json:"scope,omitempty"`

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.

🟡 [minor] Scope not shown in balance pretty-print output

Balances.PrettyPrint renders only account/asset/color/amount columns, so two rows that differ only by scope appear as indistinguishable entries in the pretty output, making debugging difficult.

Suggestion: Add a Scope column to the pretty-printer when any row in the result set has a non-empty scope.

Comment thread specs.schema.json
"required": ["type", "name"],
"properties": {
"type": { "const": "asset" },
"name": { "type": "string", "pattern": "^([A-Z]+(/[0-9]+)?)$" }

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.

🟡 [minor] Asset pattern in spec schema is narrower than the runtime's checkAssetName

The new Value schema for tagged metadata assets only allows letters plus an optional scale, but the interpreter's checkAssetName also accepts digits and an optional _SUFFIX (e.g., USD_TEST/2). Schema validation therefore rejects spec files containing runtime-valid assets.

Suggestion: Align the asset regex in the schema with the pattern actually enforced by checkAssetName in the interpreter.

if err != nil {
return nil, err
return mcp.NewToolResultError(err.Error()), nil
}

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.

🟡 [minor] Inconsistent error handling for BindArguments failure

All other errors in this handler are returned as mcp.NewToolResultError(...) with a nil transport error. A BindArguments failure returns (nil, err), propagating as a server-level error with potentially different client-visible behavior.

Suggestion: Change the BindArguments error path to return mcp.NewToolResultError(err.Error()), nil to be consistent with other error returns in the handler.

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