Skip to content

feat(EN-1334): usagebuilder subsystem + event-counter cutover (EN-1420)#1464

Open
gfyrag wants to merge 39 commits into
release/v3.0from
feat/EN-1334-usagebuilder
Open

feat(EN-1334): usagebuilder subsystem + event-counter cutover (EN-1420)#1464
gfyrag wants to merge 39 commits into
release/v3.0from
feat/EN-1334-usagebuilder

Conversation

@gfyrag

@gfyrag gfyrag commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • EN-1334 — new usagebuilder subsystem materialises per-Numscript-template invocation counters (count + lastUsed) from the FSM audit chain into a dedicated peer secondary Pebble store (usagestore). Exposed via GET /v3/{ledger}/numscripts/{name}/usage.
  • EN-1420 / EN-1422 — the per-ledger stat counters migrate out of LedgerBoundaries into usagestore. This is the full cutover: the event counters (posting, revert, numscript_execution, reference) AND the attribute-derived counters (Volume, EphemeralEvicted, TransientUsed) now live in usagestore; MetadataCount is dropped for now (no sound foundation — see below). The FSM increment sites are removed and the proto fields deleted + reserved. LedgerStats sources every migrated counter from usagestore.

Architecture notes

  • The usagebuilder tails the audit chain (AuditEntry + AuditItem), not the log stream, because the log's CreatedTransaction payload doesn't carry NumscriptReference — the raw order in AuditItem.SerializedOrder does. When a posting count is needed (revert / script-backed create) the specific log is fetched via ReadLogBySequence — a single point read per item on hot Pebble cache.
  • Volume-count derivation rides on three disjoint per-log annotation lists on LedgerLogNewKeptVolumes (+1), PurgedVolumes (−1), EphemeralVolumes (0, tracked for EphemeralEvictedCount) — split this way (vs. the earlier 2-list encoding) to keep ephemeral-heavy workloads from paying 2× bytes on the log payload (EN-1422).
  • Cursor semantics: last consumed audit sequence, persisted in usagestore at [0xFE][0x01]. LogCommitted notification wakes the loop; the audit chain advances in the same batch as the log. If the primary store is rolled back beneath the cursor, the builder self-heals: it wipes the projection and replays from sequence 0 (usagestore.Store.Reset() on boot).
  • Physical separation: usagestore opens its own Pebble instance at <data-dir>/usage/ (peer to readstore). Rebuild = drop the directory (or Reset()); the builder replays from cursor=0.
  • Notifications wiring uses constructor injection (per feedback_constructor_injection in project memory), and the existing signal.FanOut is extended to a 4th target.

Cutover semantics (EN-1420 / EN-1422)

On production upgrade, every migrated counter resets to 0 and repopulates from the earliest audit entry still reachable in Pebble. Historic pre-upgrade counter values are lost. This is the trade-off — accepted by product to avoid a stateful migration. A one-time seed from LedgerBoundaries was explicitly considered and rejected because the LedgerBoundaries counters were themselves inaccurate on any ledger with pre-existing history.

  • Increment sites: gone from processor_transaction.go, processor_revert_transaction.go, processor_mirror.go, and write_set_counters.go.
  • Proto: fields 3–10 deleted AND reserved on LedgerBoundaries (reserved 3..10 + names) — the vtprotobuf unmarshaller retains unknown-field bytes and re-emits them on MarshalVT, so the tags must never be reused.
  • Reader path: GetLedgerStats reads posting / revert / numscript_execution / reference / ephemeral_evicted / transient_used / volume from usagestore.GetCounter. Missing key = 0. Checkpoint-scoped (historical) reads short-circuit before touching the usagestore and return only the boundary-derived TransactionCount + LogCount.
  • MetadataCount is intentionally not returned. The admission preload no longer injects old metadata values, so the FSM-side counter can no longer distinguish "new key" from "overwrite". It is disabled until it can be rebuilt on a sound foundation (open question, no ticket yet).

Test plan

  • just pre-commit — 0 issues (verified locally)
  • GOROOT= go test ./internal/... — all pass locally (verified)
  • GOROOT= go build ./... — clean (verified)
  • Unit tests: usagestore/store_test.go covers round-trip Get/Put + DeleteLedger cascade + Reset; usagebuilder/process_audit_test.go covers batchState aggregation, applyDelta, mergeTemplateUsage, timestampGreater.
  • Manual verification: on a fresh ledger, invoke a template and confirm counters land at /v3/{ledger}/numscripts/{name}/usage and in GetLedgerStats.
  • Manual verification: ledgerctl store rebuild-usage --data-dir <dir> drops and rebuilds the store.
  • E2E (deferred to reviewer): confirm LedgerStats values on a ledger with existing history — expect the migrated counters to be lower than pre-cutover values (that's the reset).

Files

  • New packages: internal/storage/usagestore/, internal/application/usagebuilder/
  • New handlers: internal/adapter/http/handlers_get_numscript_usage.go, cmd/ledgerctl/store/rebuild_usage.go
  • Proto: misc/proto/common.proto (+TemplateUsage), misc/proto/bucket.proto (+RPC + request), misc/proto/raft_cmd.proto (LedgerBoundaries fields 3–10 deleted + reserved)
  • Wiring: internal/bootstrap/module.go, internal/application/ctrl/controller.go, internal/application/ctrl/controller_default.go, internal/adapter/grpc/{server,client}_bucket.go, internal/bootstrap/controller_routed.go
  • FSM: internal/domain/processing/processor_{transaction,revert_transaction,mirror}.go, internal/infra/state/write_set{,_counters,_new_volumes,_ephemeral_purge}.go
  • Checker: internal/application/check/checker.go (exclusion projection now folds EphemeralVolumes alongside PurgedVolumes)
  • Config / dashboard: .golangci.yaml, misc/devenv/monitoring-dashboards/jsonnet/lib/metrics.libsonnet
  • Docs: openapi.yml, docs/technical/contributing/api-comparison.md, docs/ops/cli.md, docs/technical/architecture/subsystems/usage/

Follow-ups

  • MetadataCount — decide how to rebuild it on a sound foundation (the preload no longer carries old metadata values). No ticket yet.
  • Once the usagebuilder is validated in prod, consider removing PebbleLastAuditSequence observability once accuracy on truncated-audit ledgers is understood.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • main

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4edf109d-d4e3-46de-8cdb-7bdd7dbc9295

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/EN-1334-usagebuilder

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.

@NumaryBot

NumaryBot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

🛑 Changes requested — automated review

The consolidated review covers the single round of new findings from the current head. All previously raised issues (HTTP DTO camelCase serialization, epoch timestamp handling, stale cursor boot guard, VolumeCount seeding for pre-upgrade ledgers, checker/usagestore scope, LedgerStats.metadata_count proto reservation, and volume annotation category verification) have been resolved or explicitly accepted by design through prior discussion threads and are not repeated here.

Two new major issues were identified in the current diff, both stemming from the color dimension being dropped in two separate volume-processing paths. First, the indexbuilder's purged/ephemeral exclusion key is constructed without the volume color, so the isExcluded lookup (which uses the full (account, asset, color) tuple) never matches colored volumes; this leaves stale readstore entries for colored postings after eviction and can also over-exclude uncolored postings. Second, the usagebuilder's per-audit deduplication key omits the color field, meaning that if the same account/asset appears with two different colors in a single audit entry, only the first is counted, causing VolumeCount and EphemeralEvictedCount to undercount in GetLedgerStats. Both issues need to be fixed before merge.

@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 4 new inline findings.

Summary: #1464 (comment)

Comment thread internal/application/usagebuilder/builder.go Outdated
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/application/ctrl/controller_default.go Outdated

@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 6 new inline findings.

Summary: #1464 (comment)

Comment thread internal/application/usagebuilder/builder.go Outdated
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/storage/usagestore/store.go
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/adapter/http/handlers_get_numscript_usage.go Outdated

@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 5 new inline findings.

Summary: #1464 (comment)

Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/storage/usagestore/store.go
Comment thread internal/application/ctrl/controller_default.go Outdated
Comment thread internal/adapter/http/handlers_get_numscript_usage.go Outdated
Comment thread internal/application/ctrl/controller_default.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 8 new inline findings.

Summary: #1464 (comment)

Comment thread internal/application/usagebuilder/builder.go Outdated
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/storage/usagestore/store.go
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/application/ctrl/controller_default.go
Comment thread internal/application/ctrl/controller_default.go Outdated
Comment thread internal/adapter/http/handlers_get_numscript_usage.go Outdated

@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 5 new inline findings.

Summary: #1464 (comment)

Comment thread internal/application/usagebuilder/builder.go Outdated
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/application/ctrl/controller_default.go
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/application/ctrl/controller_default.go Outdated
@gfyrag gfyrag force-pushed the feat/EN-1334-usagebuilder branch from a705c93 to 5733eb9 Compare July 2, 2026 08:23

@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 2 new inline findings.

Summary: #1464 (comment)

Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/application/ctrl/controller_default.go Outdated
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.32947% with 273 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.03%. Comparing base (59f7ce8) to head (ea05119).

Files with missing lines Patch % Lines
internal/application/usagebuilder/process_audit.go 70.17% 55 Missing and 30 partials ⚠️
internal/storage/usagestore/store.go 70.71% 33 Missing and 8 partials ⚠️
internal/storage/usagestore/comparer.go 14.89% 40 Missing ⚠️
internal/application/ctrl/controller_default.go 19.35% 18 Missing and 7 partials ⚠️
internal/storage/usagestore/snapshot.go 44.82% 15 Missing and 1 partial ⚠️
...ernal/adapter/http/handlers_get_numscript_usage.go 31.57% 13 Missing ⚠️
internal/application/usagebuilder/builder.go 84.33% 9 Missing and 4 partials ⚠️
internal/adapter/grpc/server_bucket.go 0.00% 8 Missing ⚠️
internal/storage/usagestore/metrics.go 78.94% 4 Missing and 4 partials ⚠️
internal/adapter/grpc/client_bucket.go 0.00% 5 Missing ⚠️
... and 7 more

❌ Your patch check has failed because the patch coverage (68.32%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@              Coverage Diff               @@
##           release/v3.0    #1464    +/-   ##
==============================================
  Coverage         75.02%   75.03%            
==============================================
  Files               432      441     +9     
  Lines             45925    46582   +657     
==============================================
+ Hits              34455    34951   +496     
- Misses             8405     8508   +103     
- Partials           3065     3123    +58     
Flag Coverage Δ
e2e 75.03% <68.32%> (+<0.01%) ⬆️
scenario 75.03% <68.32%> (+<0.01%) ⬆️
unit 75.03% <68.32%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@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 4 new inline findings.

Summary: #1464 (comment)

Comment thread internal/application/usagebuilder/process_audit.go
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/application/ctrl/controller_default.go Outdated
Comment thread internal/application/usagebuilder/process_audit.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 8 new inline findings.

Summary: #1464 (comment)

Comment thread internal/application/usagebuilder/builder.go Outdated
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/application/ctrl/controller_default.go
Comment thread internal/adapter/http/handlers_get_numscript_usage.go
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/application/usagebuilder/process_audit.go
@gfyrag gfyrag force-pushed the feat/EN-1334-usagebuilder branch from c638a14 to 41c8ba7 Compare July 3, 2026 09:24

@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: #1464 (comment)

Comment thread internal/application/usagebuilder/process_audit.go
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/application/ctrl/controller_default.go
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/application/usagebuilder/process_audit.go Outdated
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/application/ctrl/controller_default.go
@gfyrag gfyrag force-pushed the feat/EN-1334-usagebuilder branch from 41c8ba7 to 4c64ab8 Compare July 3, 2026 13:53

@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 6 new inline findings.

Summary: #1464 (comment)

Comment thread internal/application/usagebuilder/process_audit.go
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/storage/usagestore/store.go
Comment thread internal/application/ctrl/controller_default.go
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/application/usagebuilder/process_audit.go Outdated

@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: #1464 (comment)

Comment thread internal/application/usagebuilder/process_audit.go
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/storage/usagestore/store.go
Comment thread internal/application/ctrl/controller_default.go
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/application/usagebuilder/process_audit.go Outdated

@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: #1464 (comment)

Comment thread internal/application/usagebuilder/process_audit.go
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/application/ctrl/controller_default.go
Comment thread internal/application/usagebuilder/builder.go
Comment thread internal/storage/usagestore/store.go
Comment thread internal/adapter/http/handlers_get_numscript_usage.go
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated

@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: #1464 (comment)

Comment thread internal/application/usagebuilder/process_audit.go
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/storage/usagestore/store.go
Comment thread internal/application/ctrl/controller_default.go
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/adapter/http/handlers_get_numscript_usage.go
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread openapi.yml

@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 6 new inline findings.

Summary: #1464 (comment)

Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/storage/usagestore/store.go
Comment thread internal/application/ctrl/controller_default.go
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/application/usagebuilder/process_audit.go Outdated
Comment thread internal/application/usagebuilder/process_audit.go
@gfyrag gfyrag force-pushed the feat/EN-1334-usagebuilder branch from aaa6014 to cedcab0 Compare July 8, 2026 13:31
@gfyrag

gfyrag commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on release/v3.0 (13 commits on top of d2c54f504). Only conflicts were on generated proto files — resolved by regen (just generate-proto) from the cleanly-merged .proto sources. Force-pushed.

Addressed the 9 outstanding NumaryBot threads inline; summary:

  • Stale (already fixed on HEAD): mirror/DeleteLedger nil-apply early return, checkpoint stats mix, lastUsed nil fallback, --usage-dir symlink overlap, rebuild-usage opens <data-dir>/live, batched-delete stale-resurrection (markLedgerDeleted clears deltas + DeleteRange sequenced first in the Pebble batch), mirror reference counting.
  • By design:
    • No checker pass for the usagestore — it's a peer side-store rebuildable via ledgerctl store rebuild-usage; invariant feat(CI): Add GoReleaser #8 scopes the checker to projections inside the primary Pebble store.
    • Pre-upgrade counter values reset to 0 after cutover — documented under Cutover semantics (EN-1420) in the PR body; product-accepted.

just pre-commit clean, unit tests green locally.

@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: #1464 (comment)

Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/application/ctrl/controller_default.go
Comment thread internal/application/usagebuilder/builder.go
Comment thread internal/application/usagebuilder/process_audit.go Outdated
Comment thread cmd/ledgerctl/store/rebuild_usage.go Outdated
Comment thread internal/application/ctrl/controller_default.go
Comment thread internal/adapter/http/handlers_get_numscript_usage.go
Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/application/usagebuilder/process_audit.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 2 new inline findings.

Summary: #1464 (comment)

Comment thread internal/adapter/http/handlers_get_numscript_usage.go Outdated
Comment thread internal/application/usagebuilder/process_audit.go Outdated

@gfyrag gfyrag left a comment

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.

Found one invariant-level issue.

This PR introduces a new persisted projection store (internal/storage/usagestore) for template usage and the ledger usage counters, but internal/application/check/checker.go does not add any compare pass that re-derives those values from the audit chain and compares them against the stored usagestore rows. The current Check flow still stops at the existing projections (compareVolumes, compareMetadata, compareTransactions, compareExclusionProjections, compareIdempotencyOutcomes, compareIndexes, etc.); rg finds no usagestore / usage-counter / template-usage verification in the checker.

That violates invariant #8 from CLAUDE.md: the audit log is the only source of truth, every other persisted dataset is a projection, and every new persisted projection must be verified by the checker. As written, tampering with PrefixTemplate or PrefixCounter rows can change GetTemplateUsage / GetLedgerStats results without CheckStore surfacing a mismatch. Please add checker coverage that replays the audit-derived usagebuilder state and compares every persisted usage projection, emitting an appropriate CHECK_STORE_ERROR_TYPE_* on divergence.

@gfyrag gfyrag force-pushed the feat/EN-1334-usagebuilder branch from 0649e1a to 18a8629 Compare July 10, 2026 07:52

@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 3 new inline findings.

Summary: #1464 (comment)

Comment thread internal/adapter/http/handlers_get_numscript_usage.go Outdated
Comment thread internal/application/usagebuilder/process_audit.go Outdated
Comment thread internal/application/ctrl/controller_default.go
@gfyrag gfyrag force-pushed the feat/EN-1334-usagebuilder branch from 18a8629 to 1443eb1 Compare July 10, 2026 11:12
Geoffrey Ragot added 2 commits July 13, 2026 14:38
…usagebuilder

# Conflicts:
#	internal/application/ctrl/controller_default.go
#	internal/bootstrap/module.go
After the watermark/rollback revert (b09f824) the docs still described a
rollback-rewind mechanism and a checker backstop that no longer exist:

- AGENTS.md: the volume-preload invariant claimed 'the checker's volume-count
  pass would surface the drift'. usagestore is a rebuildable peer side-store
  outside checker scope (invariant #8 = primary-store projections only), so
  there is no such backstop — reworded to reflect that (which makes preload
  correctness more load-bearing, not less).
- module.go: the NewBuilder comment claimed a shared 'rebuild threshold' + a
  rollback-then-catch-up gap heuristic. The final arg is batchSize (0=default)
  and there is no rollback guard (append-only audit).
- usagestore/store.go: Reset() doc framed it as builder-triggered on rollback
  detection; it is a full-rebuild primitive not called for rollbacks. Also
  corrected the crash-safety note to rely on single-batch atomicity.
- counters.md: dropped the 'automatic boot/tick rewind / Reset() on rollback
  detection' claim; the fold only moves forward from the cursor.

Docs/comments only; build clean.

@flemzord flemzord left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Current-head re-review: the High usagestore integrity finding remains. PR #1581 does not clear it under its own proposed classification because these API-visible rows and their cursor affect correctness independently and have no executable archival-safe rebuild/detection path. Details are posted in the existing inline thread.

@flemzord flemzord left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Current-head, conversation-aware re-review: the existing High remains. The latest response reclassifies the side-store but does not provide an executable, archival-safe rebuild: Reset() has no production caller, the CLI was removed, and replay after archival is explicitly known to undercount VolumeCount. Details and current-head evidence are in the existing inline thread: #1464 (comment). No additional distinct High/Critical finding was found.

…usagebuilder

# Conflicts:
#	tests/e2e/business/transient_accounts_test.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 1 new inline finding.

Summary: #1464 (comment)

Comment thread internal/application/ctrl/controller_default.go

@flemzord flemzord left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Current-head re-review at 6f3685135: the release/v3.0 merge resolves only a test conflict and does not address the existing High. The usagestore remains served without checker coverage or a wired archival-safe rebuild/detection path; current-head evidence is in the existing inline thread: #1464 (comment). The new NumaryBot migration-reset finding matches the explicitly accepted cutover semantics and is not an additional High/Critical blocker.

@gfyrag

gfyrag commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Proto cleanup finding from the EN-1551 review: the LedgerBoundaries cleanup is correctly reserving fields 3-10 and their names. However, this PR also removes LedgerStats.metadata_count = 3 from misc/proto/common.proto without reserving either the tag or the name.

Please add reserved 3; and reserved "metadata_count"; to LedgerStats, then regenerate the protobuf outputs. This follows the repository protobuf rule and prevents accidental reuse of a removed public response field. If the same semantic field is deliberately reintroduced later, that should require an explicit decision to remove the reservation.

…usagebuilder

# Conflicts:
#	internal/proto/raftcmdpb/raft_cmd.pb.go
#	internal/proto/raftcmdpb/raft_cmd_reader.pb.go
#	internal/proto/raftcmdpb/raft_cmd_vtproto.pb.go
#	misc/proto/raft_cmd.proto

@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 2 new inline findings.

Summary: #1464 (comment)

Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/application/usagebuilder/process_audit.go Outdated
@gfyrag gfyrag requested a review from flemzord July 15, 2026 11:30

@flemzord flemzord left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Current-head re-review at 47dce64: the release/v3.0 merge does not touch the usagebuilder or usagestore recovery/integrity paths, so the existing High blocker remains. API-visible usage rows are still unchecked, Store.Reset() still has no production caller, and the documented rebuild cannot be exact after archival. Please address the existing thread before re-requesting review: #1464 (comment). The two latest skipped-order accounting findings are already covered by NumaryBot and are not duplicated here.

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

Multi-model PR review (Claude + Codex)

Two independent reviewers (Claude, grounded in the PR-head tree; Codex via codex exec) covered architecture, correctness, concurrency, error handling, security, the audit-chain cursor semantics, the checker invariant, the proto cutover, API/contract changes, tests, and doc drift. Findings were then re-verified adversarially against the exact PR head (47dce641d) before posting.

Important: the local review worktree was 8 commits behind the pushed head, so several early findings referenced stale code. All findings below were re-checked against the real head; the stale ones were rejected.

Verification outcome

12 Critical/Major/Medium candidates entered adversarial verification: 3 survived (posted inline), 4 were downgraded below the posting threshold (listed below), and 5 were rejected as false:

  • Rejected — timestamp µs-vs-ns bug in the usage handler: refuted. At head the handler uses ts.AsTime().UTC().Format(...) (the correct microsecond path) and TestToTemplateUsageJSON_MicrosecondUnitAndCamelCase locks it. The buggy time.Unix(0, ...) was only in stale code.
  • Rejected — readLog silently skips missing/non-Apply logs (invariant #7): refuted at head.
  • Rejected — usage endpoint missing 404 in openapi: the 404 is present (openapi.yml:1704) and was added by this PR; the 401/403 comparison used the wrong sibling endpoint (Numscript-Library siblings also carry none).
  • Rejected — lag gauge conflates log vs audit sequence: refuted against head code.
  • Rejected — GetLedgerStats composes two unsynchronized stores: the usagestore is by design an eventually-consistent lagging projection; no contract requires cross-store atomicity, and intra-counter atomicity is already provided by the single snapshot.

Posted inline (3)

  • Majormisc/proto/raft_cmd.proto — merge into current release/v3.0 won't compile (compareBoundaries reads the deleted counter getters).
  • Mediuminternal/adapter/http/handlers_get_ledger_stats.go — openapi LedgerStats schema mismatches the handler DTO.
  • Mediuminternal/application/usagebuilder/process_audit_test.go — audit-processing pipeline (processAuditEntries + dispatch fan-out + dedup) has no end-to-end test.

Notable point of agreement / adjustment

Both reviewers flagged that the new usagestore projection is not verified by the checker (invariant #8). On verification this was downgraded to Minor / documented tech-debt: usagestore is a physically-separate secondary Pebble store (peer to readstore, which is itself not content-verified — checker.go:485-488), it is explicitly documented as out of checker scope in the invariant-#6 note and the new docs/.../usage/ pages, and it is rebuildable from the audit chain. It is not an unacknowledged violation — but a client-served projection with no cryptographic backstop is a real residual item, same as the one readstore already carries.

Not posted inline — Minor / Low (would still be worth addressing)

  • internal/application/check/checker.go (Minor) — no compareUsage*/collectUsage* pass for the usagestore counters (documented gap; see above).
  • internal/application/usagebuilder/builder.go:172,216 (Minor) — the PR description's "self-heal on boot / automatic rollback-reset" is not implemented as an automatic mechanism. usagestore.Store.Reset() exists (store.go:204) but is unwired — called only from tests, no boot hook, no rollback detection, no ledgerctl command. boot() computes the cursor-vs-audit-head gap and discards it. The append-only justification holds for follower-sync and, via the fresh-data-dir guard (module_restore.go:57-64), for the documented backup-restore flow — but that guard scans checkpoints/, not usage/, so an in-place restore where an operator clears only checkpoints/ would strand a stale usage cursor with no wired remedy. Consider wiring Reset() to a ledgerctl store rebuild-usage command (its own doc claims it exists) or a boot-time cursor > pebbleLast self-heal.
  • misc/proto/common.proto:1647 (Low) — LedgerStats drops field 3 (metadata_count) without a reserved 3; clause, unlike LedgerBoundaries in the same PR. LedgerStats is never persisted (fresh per-RPC DTO), so impact is low, but adding reserved 3; restores consistency and proto hygiene.
  • internal/application/ctrl/controller_default.go:645 (Low) — checkpoint-scoped GetLedgerStats now returns zero usage counters (pre-PR it returned real checkpointed counts). This is documented and intentional (usagestore is live-only), but silent zeros are indistinguishable from an empty ledger — consider omitting the fields or signaling "not available for historical scope."

Event kept as COMMENT (no approve/request-changes).

Comment thread misc/proto/raft_cmd.proto
// unknown-field bytes and re-emits them on MarshalVT, so an old persisted
// Pebble record carrying e.g. volume_count at tag 3 would be mis-decoded
// as whatever new field claimed tag 3.
reserved 3, 4, 5, 6, 7, 8, 9, 10;

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] Merging this branch into current release/v3.0 will not compile — compareBoundaries reads the getters this PR deletes

This branch's merge base is ea3dfba, which predates compareBoundaries in internal/application/check/checker.go (added on release/v3.0 by 310ba1f, after the base). Current release/v3.0's compareBoundaries (checker.go ~5331-5336) calls GetPostingCount(), GetRevertCount(), GetNumscriptExecutionCount(), GetVolumeCount(), GetMetadataCount(), GetReferenceCount() on LedgerBoundaries — the exact fields reserved here (tags 3-10). A 3-way merge takes both sides with no textual conflict (proto and checker are different files), so the merged tree keeps compareBoundaries while those getters are gone → a deterministic go build failure on six undefined method calls. Verified: the getters no longer exist in the generated raftcmdpb at this head.

Fix: rebase onto current release/v3.0 and reconcile compareBoundaries — drop the removed counters from the boundary compare and re-home their verification (see the usagestore/checker gap in the review summary). Re-run GOROOT= go build ./... after the rebase.

@@ -10,7 +10,6 @@ import (
type ledgerStatsJSON struct {
TransactionCount uint64 `json:"transactionCount"`

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.

[Medium] openapi LedgerStats schema does not match this DTO

This handler serializes nine fields (transactionCount, volumeCount, referenceCount, postingCount, logCount, ephemeralEvictedCount, transientUsedCount, revertCount, numscriptExecutionCount) and never emits accountCount or metadataCount. But openapi.yml's LedgerStats schema (openapi.yml:3634-3649; endpoint description at :291) declares only accountCount (marked required) and transactionCount — a required field the server never returns, plus all eight real counters missing. Clients generated from the spec will mis-model this response.

The drift is pre-existing (the schema block is not in this diff), but this PR reworks the stats contract — it drops metadataCount and re-sources the counters from usagestore — so this is the right change to also sync the spec, per the repo's "update openapi.yml if HTTP endpoints change" rule. Fix: replace the schema properties with the nine fields actually serialized, drop accountCount from properties/required, and update the endpoint description.

"github.com/formancehq/ledger/v3/internal/proto/commonpb"
"github.com/formancehq/ledger/v3/internal/storage/usagestore"
)

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.

[Medium] The audit-processing pipeline has no end-to-end test coverage

The tests here cover the pure aggregation helpers (addCounter, addTemplateUsage, applyDelta, mergeTemplateUsage, timestampGreater), and commitBatch + the store layer are integration-tested against a temp-dir store (TestCommitBatch_SameBatchDeleteRecreateDoesNotResurrect, usagestore/store_test.go). But the load-bearing ingestion path above commitBatch is untested: processAuditEntries (batch boundaries, EOF break, cursor advance), the dispatchOrder/dispatchCreateTransaction/dispatchRevertTransaction/dispatchMirrorIngest fan-out, applyVolumeAnnotations per-audit-entry dedup (the "shared account counted once across N orders" invariant), and readLog. These are exactly the paths most likely to regress silently — and per invariant #8 there is no checker backstop for the usagestore counters. Add table-driven tests that feed synthetic AuditEntry/AuditItem/Log fixtures through processAuditEntries against a real temp-dir usagestore and assert the resulting counters, including the multi-order dedup case.

gfyrag pushed a commit that referenced this pull request Jul 15, 2026
…ing peer store

Per review: usagestore does not exist on release/v3.0 (introduced by the
unmerged #1464/EN-1334). Cite the readstore as the concrete peer secondary store
today and mark the usagestore counters as forthcoming, so the invariant is
accurate on this branch's base.

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

My bad, wrong approve, please fix the comments if you can

gfyrag added a commit that referenced this pull request Jul 16, 2026
* Clarify checker store invariant

* Address review: keep emission-gating cursors verified; require wired rebuild

- Remove 'local cursors' from the derived-exempt category. A cursor that
  gates business proposal emission is correctness-affecting and stays under
  checker/recovery validation. Call out MirrorCursor explicitly (ahead of
  the audited v2LogId skips source logs; behind it replays, no dedup).
- Tighten the derived-exemption: only when the projection is deterministically
  rebuildable from still-retained verified state AND that rebuild is a tested,
  wired detect->reset->rebuild path run before the projection is served;
  'rebuildable in principle' is not enough. Note the dedicated rebuild path
  (e.g. usagestore) is a follow-up PR.

* Reclassify MirrorCursor as technical state, not a checker business invariant

Re-verified the mirror mechanism: MirrorCursor is the worker's progress
pointer into an external v2 source. The business effect (ingested
transactions) is audit-bound (each MirrorIngest is an audit-chain order)
and already checker-verified via recordMirrorIngestMutations, so ledger
correctness holds regardless of the cursor. A wrong cursor only affects
future ingestion fidelity (skip/re-emit) — an operational/recovery concern
reconciled against source-head / max audited v2LogId, not a checker compare
pass. The checker guards the business invariants of the main store, which
are audit-derived and independent of replication cursors.

* MirrorCursor: classify as technical state with precise tamper analysis

The cursor rides in the same atomic Raft-applied Pebble batch as the
MirrorIngest orders it acknowledges (worker bundles the MirrorSync TU into
the data proposal; applyProposal drains orders+TUs through one WriteSet.Merge
and one batch.Commit). It can never lag/lead the ingested data operationally.

- behind-tamper (replay): self-heals into an audit-consistent state, so a
  cursor/volume compare cannot see the doubling; the real fix is FSM v2LogId
  idempotency (functional follow-up), not a checker pass.
- ahead-tamper (skip): a v2-parity property against the external source-head,
  not audit-vs-store integrity; home is the mirror worker's recovery
  reconciliation.

Reconcile audit-vs-technical-state.md to match (was framed as a must-be-
checker-enforced integrity gap).

* Invariant #8: classify by store — peer secondary stores out of checker scope

Replace the ambiguous 'derived-exemption needs a wired rebuild' clause (which
read as 'usagestore needs checker coverage until rebuilt is wired') with an
explicit store-based classification:

- Primary FSM store: in scope; derived-exemption needs a real wired rebuild.
- Distinct peer secondary Pebble stores (usagestore, readstore): out of scope
  by construction — Check() never opens them. They are pure derived caches of
  the audit chain (the verified source of truth), so corruption is a rebuild
  event, not an undetected integrity violation. API-visibility != authoritative.
  Automating their drop+rebuild lifecycle is operational follow-up, not a
  checker-coverage gap.

* docs: align readstore framing with main-store-only checker scope

The checker verifies the authoritative MAIN store only. The readstore inverted
index is a distinct per-replica peer Pebble store (not on the FSM apply path,
populated locally by the indexbuilder) — out of invariant #8 scope by
construction, same as the usagestore. Reframe the 'Readstore and Indexes'
section, the rebuildable-projection table row, and the summary list: readstore
integrity is the indexbuilder's rebuild-health concern (EN-1323), repaired by
drop+re-index from the verified audit, not a compareIndexes / main-store checker
obligation. compareIndexes still verifies the registry (which lives in the main
store).

* docs(checker): align linked pages + exemption bases with invariant #8 and merged EN-1550

Addresses NumaryBot review on the invariant clarification:
- checker/README.md + audit-chain.md: stop listing the mirror cursor and readstore
  contents as 'tracked integrity gaps'. The mirror high-water mark
  (last_mirror_v2_log_id) is now verified by compareMirrorV2LogID (EN-1550); the
  readstore is a peer secondary store, out of checker scope by construction with
  automated detect/drop/rebuild tracked under EN-1323 (not yet wired).
- README.md #16: add invariant #8's scope refinements (primary store; peer stores
  out by construction; wired-rebuild-or-informational exemption).
- audit-vs-technical-state.md: update the MirrorCursor section — the *behind*
  tamper case is now closed by EN-1550 idempotency (only the ahead-case worker
  reconciliation remains); make the readstore EN-1323 open gap explicit.
- AGENTS.md invariant #8 (b): split the two exemption bases — wired RebuildDelta
  rebuild vs purely-informational-carried-across-restore — so the informational
  counters are no longer incorrectly attributed to RebuildDelta.

* docs(checker): mark usagestore as forthcoming (EN-1334), not an existing peer store

Per review: usagestore does not exist on release/v3.0 (introduced by the
unmerged #1464/EN-1334). Cite the readstore as the concrete peer secondary store
today and mark the usagestore counters as forthcoming, so the invariant is
accurate on this branch's base.

* docs(checker): frame ahead-cursor as a current gap; fix usagestore refs in README + audit-vs-technical

Per flemzord re-review of #1581:
- audit-vs-technical-state.md: the *ahead* MirrorCursor case is now stated as a
  CURRENT correctness gap — an advanced/corrupted cursor fetches no source logs
  and reports FOLLOWING (cursor >= sourceHead), silently under-ingesting v2->v3
  with no worker/startup safeguard wired yet. It stays a v2-parity concern (not
  checker scope), but is explicitly open until reconciliation lands, not framed
  as a tidy tracked follow-up.
- checker/README.md + audit-vs-technical-state.md: usagestore marked as
  forthcoming (EN-1334), matching the AGENTS.md fix — it does not exist on
  release/v3.0 yet.

* docs(checker): align peer-store & MirrorCursor gap wording across linked pages

flemzord re-review: the checker pages contradicted audit-vs-technical-state.md.
Align all of them to one scope + gap classification:
- readstore/peer stores are out of *main-store checker* scope by construction,
  but that is NOT a claim they are integrity-safe — their contents are a current
  open integrity gap until per-replica detect/drop/rebuild is wired (EN-1323).
  Dropped the 'rebuild event, not an undetected integrity violation' framing in
  AGENTS.md and the 'no longer integrity gaps' framing in checker/README.md.
- MirrorCursor: the pointer/high-water mark is verified (compareMirrorV2LogID),
  not a gap; but the advanced-cursor path (cursor beyond source head → silent
  under-ingestion) remains a current open correctness gap until worker/startup
  reconciliation is wired. Stated consistently in README + audit-chain, matching
  audit-vs-technical-state.md.
- audit-chain.md's normative 'verify each such projection' now scoped to the
  primary FSM store, with the peer-store carve-out called out.

---------

Co-authored-by: Geoffrey Ragot <geoffrey@formance.com>
Geoffrey Ragot added 2 commits July 16, 2026 14:06
The release/v3.0 merge brought in compareBoundaries (checker) + RebuildDelta
(backup) code that reads/writes the per-ledger usage counters (PostingCount,
RevertCount, NumscriptExecutionCount, VolumeCount, MetadataCount, ReferenceCount)
on LedgerBoundaries — but EN-1334 moved those counters to the usagestore peer
secondary store, so the fields no longer exist there (compile break).

Resolve in the peer-store direction, consistent with the checker-scope framing
merged in #1581: the usagestore is a peer secondary store, out of main-store
checker scope by construction; its counters are a rebuild-health concern, not an
invariant-#8 main-store projection.

- checker.go: compareBoundaries now verifies only NextTransactionId/NextLogId;
  dropped the counter folds in advanceExpectedBoundaries /
  collectAuditOrderBoundaryEffects (kept the NextTransactionId advances) and the
  6 counter rows + attribute recounts; removed the now-unused
  countLedgerAttributeKeys helper.
- rebuild.go: RebuildDelta no longer folds those counters onto LedgerBoundaries
  (kept NextTransactionId/NextLogId + mirror fill-gap advances); removed the
  countNetAttributes/countAttributeKeys helpers and the trimmed
  recordTransactionBoundary signature.
- tests: dropped assertions on the removed main-store counters (kept the
  NextTransactionId/NextLogId + reference-index checks).
- AGENTS.md invariant #8: compareBoundaries clause updated — usage counters live
  in the usagestore peer store, out of main-store checker scope.

Verified: go build ./..., go test ./internal/application/check/...
./internal/infra/backup/..., golangci-lint — all green.
@gfyrag

gfyrag commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@flemzord PR rebasée sur release/v3.0 et le standoff usagestore/checker résolu — prête pour re-review.

Le cadrage peer-store est désormais acté et mergé dans release/v3.0 via #1581 : un peer secondary store (readstore aujourd'hui, usagestore ici) est hors périmètre du checker main-store par construction (Check() ne l'ouvre jamais) ; son intégrité est une préoccupation de rebuild-health (suivie), pas une projection invariant #8 du main-store.

En rebasant, le merge a fait remonter le compareBoundaries (checker) + RebuildDelta (backup) de release/v3.0, qui lisent/écrivent les compteurs par-ledger (PostingCount, RevertCount, NumscriptExecutionCount, VolumeCount, MetadataCount, ReferenceCount) sur LedgerBoundaries — or EN-1334 les a déplacés dans le usagestore. J'ai résolu dans le sens peer-store, cohérent avec #1581 :

  • checker.gocompareBoundaries ne vérifie plus que NextTransactionId/NextLogId (le high-water mirror reste couvert par compareMirrorV2LogID) ; folds de compteurs retirés (advances NextTransactionId conservés) ; helper countLedgerAttributeKeys supprimé ;
  • rebuild.goRebuildDelta ne reconstruit plus ces compteurs sur LedgerBoundaries (NextTransactionId/NextLogId + fill-gap mirror conservés) ;
  • tests — assertions sur les compteurs main-store retirées ;
  • AGENTS.md invariant feat(CI): Add GoReleaser #8 — clause compareBoundaries mise à jour (compteurs = usagestore peer store, hors périmètre checker main-store).

Vérif locale : go build ./..., go test ./internal/application/check/... ./internal/infra/backup/..., golangci-lint — tout vert. Aucun consommateur hors peer-store ne dépendait des champs retirés (balayage repo confirmé : les seuls Get*Count restants sont sur LedgerStats, message API distinct).

Restent hors périmètre de cette PR : codecov/patch (couverture) et les findings NumaryBot. Si tu es d'accord avec le cadrage peer-store, la question de fond du standoff est tranchée.

@gfyrag gfyrag requested a review from flemzord July 16, 2026 13:13

@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 3 new inline findings.

Summary: #1464 (comment)

Comment thread internal/application/usagebuilder/process_audit.go
Comment thread internal/application/usagebuilder/process_audit.go Outdated
Comment thread misc/proto/common.proto

@flemzord flemzord left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Requesting changes on current head 6f4519f. I validated the current NumaryBot findings against the code and am not duplicating its inline threads.

Blocking issues:

  1. Skipped CreateTransaction orders still inflate usage counters. processAuditEntries dispatches any audit item with a non-zero LogSequence, including OrderSkipped logs. readLog() returns empty annotations for an OrderSkipped payload, but dispatchCreateTransaction still increments CounterReference, CounterNumscriptExecution, and template usage directly from the raw order at internal/application/usagebuilder/process_audit.go:469-491. A skipped transaction should contribute no successful transaction/template usage counters; gate those increments on the produced log being a real CreatedTransaction.

  2. Skipped RevertTransaction orders still inflate RevertCount. dispatchRevertTransaction increments CounterRevert before inspecting the produced log at internal/application/usagebuilder/process_audit.go:508. If the audit item points at an OrderSkipped log, readLog() returns no reverted transaction, but the revert counter is already bumped. Only increment after confirming the log payload is RevertedTransaction.

  3. LedgerStats.metadata_count = 3 was removed without reserving the tag/name. misc/proto/common.proto:1647-1655 jumps from field 2 to field 4 but does not reserve 3 or "metadata_count". This is a removed public/protobuf field; please add reserved 3; and reserved "metadata_count"; and regenerate.

Non-blocking but should be cleaned: docs/technical/architecture/subsystems/usage/counters.md still opens by saying the page explains how the checker verifies these counters, while the current design explicitly keeps usagestore out of main-store checker scope. Please align that intro with the accepted peer-store/rebuild-health framing.

flemzord review: dispatch* incremented success-scoped usage counters from the raw
order even when the produced log was an OrderSkipped (order committed nothing).

- readLog now reports the produced log kind (isCreatedTx / isRevertedTx).
- dispatchCreateTransaction: gate the reference / numscript-execution /
  template-usage increments on a produced CreatedTransaction (skips contribute
  nothing; posting/volume counters already derive from the produced log).
- dispatchRevertTransaction: gate CounterRevert on a produced RevertedTransaction
  (previously bumped unconditionally before reading the log).
- docs/usage/counters.md: intro aligned to the peer-store framing (usagestore is
  out of main-store checker scope; integrity is rebuild-health, not verified by
  the checker); Revert/Numscript/Reference deltas documented as per-successful-order.

go build ./..., usagebuilder tests, golangci-lint — green.
@gfyrag

gfyrag commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@flemzord merci pour la re-review — traité dans 0510143f8.

1. CreateTransaction skippé gonflait les compteurs ✅ — readLog expose désormais le type de log produit (isCreatedTx/isRevertedTx) ; dispatchCreateTransaction gate les incréments CounterReference / CounterNumscriptExecution / template-usage sur un CreatedTransaction réellement produit. Un OrderSkipped ne compte plus rien (posting/volume dérivaient déjà du log produit, donc naturellement nuls).

2. RevertTransaction skippé gonflait CounterRevert ✅ — l'incrément est déplacé après readLog et gaté sur un RevertedTransaction produit.

3. LedgerStats.metadata_count = 3 retiré sans reserved — volontairement pas de reserved ajouté. On est pré-GA et on ne conserve pas la compat (même ligne que pour l'enum 59 sur #1560) : un trou de numéro de champ est valide en proto3, et réserver le numéro/nom serait de l'hygiène anti-réutilisation qu'on ne veut pas porter avant la GA. Si un besoin apparaît à la GA, on réservera à ce moment-là.

4. (non-bloquant) usage/counters.md ✅ — l'intro n'affirme plus que le checker vérifie ces compteurs ; elle est alignée sur le cadrage peer-store/rebuild-health (usagestore hors périmètre checker main-store par construction). J'en ai profité pour préciser les deltas Revert/Numscript/Reference en « per successful order ».

go build ./..., tests usagebuilder, golangci-lint — verts. Restent hors périmètre : codecov/patch + les findings NumaryBot.

@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 2 new inline findings.

Summary: #1464 (comment)

Comment thread misc/proto/common.proto
Comment thread openapi.yml

@flemzord flemzord left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-checking current head 0510143f.

Thanks, the skipped-order counter bugs and the usagebuilder counters doc issue are fixed in this head.

One blocker remains: LedgerStats.metadata_count = 3 is still removed from misc/proto/common.proto without reserving the field number and name. Current LedgerStats jumps from field 2 to field 4, but public/protobuf removals must reserve both the tag and the name to prevent future incompatible reuse.

Please add:

reserved 3;
reserved "metadata_count";

inside message LedgerStats, then regenerate the protobuf outputs.

…usagebuilder

# Conflicts:
#	internal/infra/state/write_set_ephemeral_purge.go
#	internal/proto/commonpb/common.pb.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 2 new inline findings.

Summary: #1464 (comment)


out := make(map[domain.AccountAssetKey]struct{}, len(purged)+len(ephemeral))
for _, v := range purged {
out[domain.AccountAssetKey{Account: v.GetAccount(), Asset: v.GetAsset()}] = struct{}{}

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] Color dimension dropped when building purged/ephemeral volume exclusion keys

When a purged or ephemeral volume is colored, the exclusion key is built with the zero color. The isExcluded lookup later uses the full (account, asset, color) tuple, so colored purged postings are never excluded from the readstore account-to-transaction mappings. Uncolored postings for the same account/asset can also be over-excluded. This causes colored-balance queries to return stale index entries after an eviction.

Suggestion: Include the volume's color field when constructing the exclusion key so that the lookup tuple (account, asset, color) matches exactly what isExcluded expects.


// volumeSetKey mirrors state.volumeSetKey — kept local to avoid crossing
// package boundaries just for a triple of strings.
type volumeSetKey struct {

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] Color dimension omitted from usage volume dedup keys causes undercounting

The usagebuilder deduplicates volume annotations per audit entry using only (ledger, account, asset), but volume identity now includes color. If the same account/asset pair has two distinct colors created, purged, or marked ephemeral within a single audit entry, the second color is silently dropped, causing VolumeCount and/or EphemeralEvictedCount in GetLedgerStats to undercount.

Suggestion: Add the color field to the deduplication key so that each (ledger, account, asset, color) tuple is treated independently.

@gfyrag gfyrag left a comment

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.

Re-reviewing current head ea05119a: not approvable yet.

I revalidated the current diff and agree with the two latest NumaryBot color findings; I am not duplicating those inline threads. I am also not blocking on the LedgerStats.metadata_count reservation request under the current pre-GA/no-compat direction.

Blocking issues:

  1. The indexbuilder drops TouchedVolume.color when building purged/ephemeral exclusion keys.

misc/proto/common.proto defines TouchedVolume as (account, asset, color), and internal/application/indexbuilder/process_logs.go later checks exclusions with the full (account, asset, color) tuple. However, internal/application/indexbuilder/applied_proposal_sync.go builds the purged/ephemeral exclusion set using only account and asset. A colored purged/ephemeral bucket therefore does not match the exclusion lookup for that same colored posting, leaving stale account-to-transaction mappings in the readstore; the uncolored bucket for the same account/asset can also be over-excluded.

  1. The usagebuilder deduplicates per-entry volume annotations without color.

internal/application/usagebuilder/process_audit.go defines its local volumeSetKey as (ledger, account, asset), then uses it for newKept, purged, and ephemeral volume annotations. Volume identity is now (ledger, account, asset, color), so two colors of the same account/asset in the same audit entry collapse into one counter delta. That undercounts VolumeCount and/or EphemeralEvictedCount in GetLedgerStats.

Non-blocking but should be fixed in this PR:

  • openapi.yml still documents TemplateUsage.count as int64, while the proto/Go/JSON surface is fixed64/uint64. Generated clients can use the wrong signed type.
  • The LedgerStats OpenAPI schema still describes only accountCount and transactionCount, but the HTTP DTO now returns transactionCount, volumeCount, referenceCount, postingCount, logCount, ephemeralEvictedCount, transientUsedCount, revertCount, and numscriptExecutionCount.
  • docs/technical/architecture/subsystems/usage/usagebuilder.md still says a primary-store rollback/cursor-ahead situation triggers usagestore.Reset() on the next boot/tick, while the code explicitly documents that no rollback/cursor-ahead re-check is needed and Reset() has no production caller.

Checks run locally:

git diff --check origin/release/v3.0...HEAD
GOROOT= go test ./internal/application/usagebuilder ./internal/storage/usagestore ./internal/adapter/http -run 'Test.*(Usage|Counter|Template|LedgerStats|Numscript|Store|Process)' -count=1
GOROOT= go test ./internal/application/usagebuilder ./internal/application/indexbuilder -run 'Test.*(Color|Volume|Purged|Ephemeral|Excluded|Dedup)' -count=1

They pass, but the last command reports [no tests to run] for usagebuilder, so the colored-volume counter bug is currently uncovered.

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.

4 participants