Skip to content

fix(entitlement): preserve grant usage across balance snapshots - #4846

Open
GAlexIHU wants to merge 8 commits into
mainfrom
codex/entitlement-balance-snapshots
Open

fix(entitlement): preserve grant usage across balance snapshots#4846
GAlexIHU wants to merge 8 commits into
mainfrom
codex/entitlement-balance-snapshots

Conversation

@GAlexIHU

@GAlexIHU GAlexIHU commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What changed

  • make balance snapshots carry complete usage-period state, including cumulative grant usage
  • anchor burn-down history to a complete starting snapshot and expose reset rollover as an explicit history segment
  • persist and select only resumable snapshots while retaining the legacy usage shape for rolling-deploy compatibility
  • fall back to measurement start when no compatible snapshot exists
  • add regression and continuity coverage for snapshot pruning, insertion, resets, and resumed calculations
  • document entitlement and credit domain semantics, including minute-level time resolution and snapshot lifecycle

Why

totalAvailableGrantAmount was reconstructed from grant usage after the latest persisted snapshot. Grant usage from earlier in the same usage period was therefore lost, allowing API responses where usage exceeded total available grants while balance remained positive and overage stayed zero.

A persisted snapshot must be a complete, independent checkpoint. Its result must not depend on earlier snapshots still being present.

Impact

Entitlement balance reads now preserve the full usage-period grant history when resuming from snapshots. Existing snapshots without the new usage state are ignored and recalculated from measurement start until new checkpoints are written. The legacy field remains written so old and new binaries can overlap during deployment.

Checks

  • full non-e2e Go test suite
  • repository lint
  • focused credit engine and metered entitlement regression tests
  • documentation diff and link checks

Summary by CodeRabbit

  • New Features

    • Credit balances now preserve usage details across snapshots, resets, rollovers, and split processing.
    • Balance calculations now include accumulated grant usage for more accurate available-credit totals.
    • Added support for independent, versioned balance snapshots.
  • Bug Fixes

    • Improved handling of incomplete or invalid snapshots.
    • Ensured deterministic grant consumption and correct rollover history.
  • Documentation

    • Added guidance explaining credit and entitlement behavior, lifecycle, resets, and domain boundaries.

Greptile Summary

The PR makes balance snapshots independent resumable checkpoints by persisting cumulative usage-period state and ignoring legacy incomplete snapshots.

  • Adds cumulative usage and grant-usage state to engine snapshots.
  • Represents reset rollover explicitly in burn-down history.
  • Adds nullable snapshot persistence with rolling-deployment compatibility.
  • Falls back to replay from measurement start when no compatible checkpoint exists.
  • Expands regression coverage and domain documentation.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains within the eligible follow-up review scope.

Important Files Changed

Filename Overview
openmeter/credit/balance/balance.go Introduces complete starting snapshots, cumulative usage-period state, and deep snapshot cloning.
openmeter/credit/engine/run.go Propagates cumulative usage and grant usage through engine runs and reset transitions.
openmeter/credit/engine/history.go Anchors burn-down history to a starting snapshot and reconstructs complete state at segment boundaries.
openmeter/credit/engine/reset.go Models reset rollover and preserved-overage burn as explicit history and snapshot state.
openmeter/credit/adapter/balance_snapshot.go Persists complete usage snapshots, retains the legacy representation, and excludes incomplete rows from selection.
openmeter/credit/helper.go Falls back to measurement start when no reusable snapshot exists and persists eligible complete checkpoints.
openmeter/entitlement/metered/balance.go Derives entitlement usage and total available grant amount from cumulative snapshot state.
openmeter/ent/schema/balance_snapshot.go Adds the optional JSON usage-snapshot persistence field.
tools/migrate/migrations/20260803135655_add_balance_snapshot_usage_snapshot.up.sql Adds the nullable usage_snapshot column needed to distinguish resumable snapshots from legacy rows.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Read[Balance read] --> Select{Complete compatible snapshot?}
  Select -->|Yes| Resume[Resume from persisted checkpoint]
  Select -->|No| Start[Start from measurement start]
  Resume --> Engine[Run credit engine]
  Start --> Engine
  Engine --> Reset{Reset encountered?}
  Reset -->|Yes| Rollover[Apply rollover and preserved overage]
  Rollover --> Continue[Continue new usage period]
  Reset -->|No| Continue
  Continue --> Result[Balance and cumulative usage state]
  Result --> Persist[Persist complete snapshot and legacy usage shape]
Loading

Reviews (2): Last reviewed commit: "docs(entitlement): document balance sema..." | Re-trigger Greptile

Context used:

@GAlexIHU GAlexIHU added the release-note/bug-fix Release note: Bug Fixes label Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds cumulative usage snapshots to credit balances, persists them alongside legacy usage data, updates credit history and rollover accounting, and changes entitlement balance calculations to use the new snapshot fields.

Changes

Credit snapshot contract and persistence

Layer / File(s) Summary
Snapshot contract and persistence
openmeter/credit/balance/*, openmeter/credit/adapter/*, openmeter/ent/schema/*, tools/migrate/migrations/*
Snapshot now includes UsageSnapshot, cloning, and a complete starting-snapshot constructor. Persistence validates, stores, filters, and hydrates usage snapshots while retaining legacy usage fields.
Snapshot service wiring and documentation
openmeter/credit/balance/service.go, openmeter/registry/builder/*, test/**/*, openmeter/credit/README.md, AGENTS.md
Snapshot services now use only repositories. Credit domain documentation and Go guidance were added.
History and rollover transitions
openmeter/credit/engine/history.go, openmeter/credit/engine/reset.go, openmeter/credit/engine/*_test.go
History construction validates snapshots and segment continuity. Reset processing emits rollover segments and carries usage and overage state across boundaries.
Engine usage propagation
openmeter/credit/engine/run.go, openmeter/credit/engine/engine.go, openmeter/credit/engine/*_test.go
Engine runs validate and clone starting snapshots, append rollover segments, and accumulate usage and grant usage into result snapshots.
Entitlement balance integration
openmeter/entitlement/metered/*, openmeter/entitlement/README.md
Metered balances read usage and total grant usage from UsageSnapshot. Tests cover migration, persistence validation, rollover behavior, and independent snapshot versions.

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

Possibly related PRs

Suggested reviewers: tothandras, turip

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preserving grant usage across entitlement balance snapshots.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/entitlement-balance-snapshots

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.

@GAlexIHU
GAlexIHU force-pushed the codex/entitlement-balance-snapshots branch from e365ec2 to 59058ea Compare August 3, 2026 16:46
@GAlexIHU
GAlexIHU marked this pull request as ready for review August 3, 2026 16:48
@GAlexIHU
GAlexIHU requested a review from a team as a code owner August 3, 2026 16:48

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

🧹 Nitpick comments (2)
openmeter/credit/engine/history.go (1)

104-165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the rollover-ordering comparator into a named helper.

The sort.SliceStable comparator at lines 123-129 encodes a real domain rule: rollover segments must sort before usage segments at the same timestamp. Move this into a named helper, for example segmentsLessForSort or rolloverPrecedesUsageAtSameTime. This keeps the domain rule visible outside the closure and makes it independently testable.

The rest of NewGrantBurnDownHistory (copy, per-segment ordering check, anchor validation, contiguity validation) is correct and matches the test coverage in history_test.go.

Based on learnings, AGENTS.md states: "Do not hide type switching, validation, persistence mapping, or meaningful domain translation inside local closures. Use a named helper; reserve inline callbacks for obvious, tiny logic."

♻️ Proposed extraction
-	sort.SliceStable(s, func(i, j int) bool {
-		if s[i].ClosedPeriod.From.Equal(s[j].ClosedPeriod.From) {
-			return s[i].TerminationReasons.Rollover && !s[j].TerminationReasons.Rollover
-		}
-
-		return s[i].ClosedPeriod.From.Before(s[j].ClosedPeriod.From)
-	})
+	sort.SliceStable(s, func(i, j int) bool {
+		return segmentLess(s[i], s[j])
+	})
+}
+
+// segmentLess orders segments chronologically. Rollover transitions precede
+// regular segments at the same timestamp so the reset transition is applied
+// before new-period usage.
+func segmentLess(a, b GrantBurnDownHistorySegment) bool {
+	if a.ClosedPeriod.From.Equal(b.ClosedPeriod.From) {
+		return a.TerminationReasons.Rollover && !b.TerminationReasons.Rollover
+	}
+
+	return a.ClosedPeriod.From.Before(b.ClosedPeriod.From)
🤖 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 `@openmeter/credit/engine/history.go` around lines 104 - 165, Extract the
domain-specific comparator from sort.SliceStable in NewGrantBurnDownHistory into
a named helper such as segmentsLessForSort, preserving chronological ordering
and ensuring rollover segments precede non-rollover segments at equal
timestamps. Pass the helper to sort.SliceStable so the ordering rule is
independently testable.

Source: Coding guidelines

openmeter/credit/balance/balance_test.go (1)

93-114: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extend TestSnapshotCloneCopiesUsageSnapshot to cover Balances and UnitConfig independence.

This test only checks that mutating the clone's UsageSnapshot does not affect the original. Snapshot.Clone() also clones Balances and UnitConfig independently. Add assertions for those two fields so a future regression in either clone path is caught here.

♻️ Suggested extension
 func TestSnapshotCloneCopiesUsageSnapshot(t *testing.T) {
 	snapshot := balance.Snapshot{
+		Balances: balance.Map{"grant-1": 100},
 		UsageSnapshot: &balance.UsageSnapshot{
 			Usage:           5,
 			TotalGrantUsage: 10,
 		},
 	}

 	cloned := snapshot.Clone()
+	cloned.Balances["grant-1"] = 999
+	assert.Equal(t, 100.0, snapshot.Balances["grant-1"])
 	require.NotNil(t, cloned.UsageSnapshot)
 	...

As per path instructions, "Make sure the tests are comprehensive and cover the changes," and Clone() is new behavior introduced by this PR without full field 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 `@openmeter/credit/balance/balance_test.go` around lines 93 - 114, Extend
TestSnapshotCloneCopiesUsageSnapshot to initialize representative Balances and
UnitConfig values, then assert the cloned fields match the original values and
are independent. Mutate the clone’s Balances and UnitConfig after cloning and
verify the corresponding fields on the original snapshot remain unchanged,
alongside the existing UsageSnapshot assertions.

Source: Path instructions

🤖 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 `@openmeter/credit/engine/reset.go`:
- Around line 51-86: Clone rolledOver before passing it to burnDownGrants in the
reset flow so in-place mutations cannot alter the original rollover balances.
Keep balances initialized from rolledOver for the reset snapshot, but pass a
cloned map as burnDownGrants’ balance input; preserve BalanceAtStart as the
pre-burn rolled-over state.

In `@openmeter/credit/engine/run.go`:
- Around line 251-277: Preserve the conversion configuration when constructing
the snapshot returned by runBetweenResets: copy
params.StartingSnapshot.UnitConfig into the new balance.Snapshot alongside the
existing fields. Also update reset() to copy UnitConfig into its returned reset
snapshot so subsequent engine segments retain the same configuration.

In `@openmeter/entitlement/README.md`:
- Around line 27-28: Update the sentence in the entitlement documentation so the
singular subject “Entitlement” uses the possessive pronoun “its” instead of
“their,” leaving the rest of the wording unchanged.

---

Nitpick comments:
In `@openmeter/credit/balance/balance_test.go`:
- Around line 93-114: Extend TestSnapshotCloneCopiesUsageSnapshot to initialize
representative Balances and UnitConfig values, then assert the cloned fields
match the original values and are independent. Mutate the clone’s Balances and
UnitConfig after cloning and verify the corresponding fields on the original
snapshot remain unchanged, alongside the existing UsageSnapshot assertions.

In `@openmeter/credit/engine/history.go`:
- Around line 104-165: Extract the domain-specific comparator from
sort.SliceStable in NewGrantBurnDownHistory into a named helper such as
segmentsLessForSort, preserving chronological ordering and ensuring rollover
segments precede non-rollover segments at equal timestamps. Pass the helper to
sort.SliceStable so the ordering rule is independently testable.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d267b431-eec1-4a79-a976-00385739403a

📥 Commits

Reviewing files that changed from the base of the PR and between c348100 and 59058ea.

⛔ Files ignored due to path filters (9)
  • openmeter/ent/db/balancesnapshot.go is excluded by !**/ent/db/**
  • openmeter/ent/db/balancesnapshot/balancesnapshot.go is excluded by !**/ent/db/**
  • openmeter/ent/db/balancesnapshot/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/balancesnapshot_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/balancesnapshot_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/migrate/schema.go is excluded by !**/ent/db/**
  • openmeter/ent/db/mutation.go is excluded by !**/ent/db/**
  • openmeter/ent/db/runtime.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (34)
  • AGENTS.md
  • openmeter/credit/README.md
  • openmeter/credit/adapter/balance_snapshot.go
  • openmeter/credit/balance/balance.go
  • openmeter/credit/balance/balance_test.go
  • openmeter/credit/balance/repository.go
  • openmeter/credit/balance/service.go
  • openmeter/credit/balance/service_test.go
  • openmeter/credit/engine/engine.go
  • openmeter/credit/engine/engine_test.go
  • openmeter/credit/engine/history.go
  • openmeter/credit/engine/history_test.go
  • openmeter/credit/engine/reset.go
  • openmeter/credit/engine/reset_test.go
  • openmeter/credit/engine/run.go
  • openmeter/credit/engine/run_test.go
  • openmeter/credit/engine/runresult_test.go
  • openmeter/credit/engine/snapshot_test.go
  • openmeter/credit/helper.go
  • openmeter/ent/schema/balance_snapshot.go
  • openmeter/entitlement/README.md
  • openmeter/entitlement/metered/balance.go
  • openmeter/entitlement/metered/balance_test.go
  • openmeter/entitlement/metered/balance_total_available_test.go
  • openmeter/entitlement/metered/balance_unitconfig_test.go
  • openmeter/entitlement/metered/lateevents_test.go
  • openmeter/entitlement/metered/reset_test.go
  • openmeter/entitlement/metered/utils_test.go
  • openmeter/registry/builder/entitlement.go
  • openmeter/subscription/README.md
  • test/billing/subscription_suite.go
  • test/entitlement/regression/framework_test.go
  • tools/migrate/migrations/20260803135655_add_balance_snapshot_usage_snapshot.down.sql
  • tools/migrate/migrations/20260803135655_add_balance_snapshot_usage_snapshot.up.sql
💤 Files with no reviewable changes (1)
  • openmeter/credit/balance/service_test.go

Comment on lines +51 to +86
balances := rolledOver
overage := startingOverage
var grantUsages GrantUsages
if startingOverage != 0 {
balances, grantUsages, overage = e.burnDownGrants(rolledOver, prioritizedGrants, startingOverage)
}

return balance.Snapshot{
// The reset snapshot is the point-in-time balance after grant balance
// rollover and preserved overage burn.
resetSnapshot := balance.Snapshot{
At: at,
Balances: rolledOver,
Overage: startingOverage,
}, nil
Balances: balances,
Overage: overage,
Usage: balance.SnapshottedUsage{
Since: at,
Usage: 0,
},
UsageSnapshot: &balance.UsageSnapshot{
Usage: 0,
TotalGrantUsage: grantUsages.Sum().InexactFloat64(),
},
}

// The rollover segment captures the instantaneous transition from rolled-over
// balances to the reset snapshot.
rolloverSegment := GrantBurnDownHistorySegment{
ClosedPeriod: timeutil.ClosedPeriod{From: at, To: at},
BalanceAtStart: rolledOver,
TerminationReasons: SegmentTerminationReason{
Rollover: true,
},
TotalUsage: 0,
OverageAtStart: startingOverage,
Overage: overage,
GrantUsages: grantUsages,
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- burnDownGrants definition ---"
rg -n -B2 -A40 'func \(e \*engine\) burnDownGrants' --type=go

echo "--- fallback broader search in case receiver differs ---"
rg -n -B2 -A40 'func.*burnDownGrants\(' --type=go

Repository: openmeterio/openmeter

Length of output: 193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- git status/stat ---"
git diff --stat
echo

echo "--- locate reset.go and related credit engine files ---"
git ls-files | rg '(^|/)reset\.go$|credit|engine' | head -200

echo "--- exact filename matches ---"
fd -a -i 'reset\.go$' . || true
fd -a -i 'engine' . || true

Repository: openmeterio/openmeter

Length of output: 12741


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- reset.go size and contents ---"
wc -l openmeter/credit/engine/reset.go
sed -n '1,180p' openmeter/credit/engine/reset.go

echo "--- burnDownGrants exact search ---"
rg -n -B3 -A120 'burnDownGrants' --type=go || true

echo "--- reset_test relevant assertions ---"
sed -n '1,260p' openmeter/credit/engine/reset_test.go

Repository: openmeterio/openmeter

Length of output: 2959


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- all Reset/ResetBehavior references ---"
rg -n -B3 -A50 'ResetBehavior|PreserveOverage|func \(.*engine\)|PrioritizeGrants|GrantBurnDownHistorySegment|BalanceAtStart|TotalUsage' --type=go | head -500 || true

echo "--- files in openmeter/credit/engine ---"
for f in openmeter/credit/engine/*.go; do echo "### $f"; sed -n '1,260p' "$f"; done

echo "--- all occurrences of BurnDown/Burn down / Overages ---"
rg -n -i 'burn(down)?|preserve.?overage|overage|roll.?over' openmeter/credit openmeter/credit/engine openmeter/credit/grant openmeter/credit/balance --type=go

Repository: openmeterio/openmeter

Length of output: 202


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
root = Path('openmeter/credit/engine')
for f in sorted(root.glob('*.go')):
    text = f.read_text(errors='replace').splitlines()
    hits = []
    for i, line in enumerate(text, 1):
        if any(s in line for s in ['burnDownGrants', 'PrioritizeGrants', 'BalanceAtStart', 'GrantBurnDownHistorySegment', 'ResetBehavior', 'PreserveOverage', 'Map.Clone', 'Snapshot.Clone']):
            hits.append((i, line.rstrip()))
    if hits:
        print(f'--- {f} ---')
        for i, line in hits[:30]:
            print(f'{i}: {line}')
PY

echo "--- read-only structural probe: function/member definitions and relevant mutations in credit engine ---"
python3 - <<'PY'
from pathlib import Path
import re

files = [Path('openmeter/credit/engine/reset.go'), Path('openmeter/credit/engine/grant.go'), Path('openmeter/credit/engine/burnphase.go'), Path('openmeter/credit/engine/history.go')]
for f in files:
    if not f.exists():
        print(f'missing {f}')
        continue
    text = f.read_text()
    print(f'### {f} ({len(text)} bytes) ###')
    for name in ['burnDownGrants', 'PrioritizeGrants', 'GrantBurnDownHistorySegment', 'reset']:
        m = re.search(r'(?:func|type)\b[^;]+' + re.escape(name), text)
        print(f'{name}: found={bool(m)}')
    print('assignment lines mentioning rolledOver/balances/startOverage/overage:')
    for i, line in enumerate(text.splitlines(), 1):
        if any(s in line for s in ['rolledOver', 'balances', 'startingOverage', 'overage', 'burnDownGrants']):
            print(f'{i}: {line.rstrip()}')
PY

echo "--- exact all-occurrence search ---"
find . -type f \( -name '*.go' \) -print0 | xargs -0 rg -n 'burnDownGrants|PrioritizeGrants|GrantBurnDownHistorySegment|BalanceAtStart|PreserveOverage|Rotate|RolledOver|rolledOver|burn.*overage|overage.*burn'

Repository: openmeterio/openmeter

Length of output: 48106


Clone rolledOver before calling burnDownGrants.

balances := rolledOver makes both aliases point at the same map. The existing burnDownGrants in engine lifecycle code mutates its balance map in place, so when startingOverage != 0 the BalanceAtStart assignment later would use the post-burn balances instead of the balances after rollover. Use rolledOver.Clone() for the burn-down input only.

🤖 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 `@openmeter/credit/engine/reset.go` around lines 51 - 86, Clone rolledOver
before passing it to burnDownGrants in the reset flow so in-place mutations
cannot alter the original rollover balances. Keep balances initialized from
rolledOver for the reset snapshot, but pass a cloned map as burnDownGrants’
balance input; preserve BalanceAtStart as the pre-burn rolled-over state.

Comment on lines +251 to 277
history, err := NewGrantBurnDownHistory(segments, params.StartingSnapshot)
if err != nil {
return RunResult{}, fmt.Errorf("failed to create grant burn down history: %w", err)
}

totalUsage := history.TotalUsageInHistory()
usage := balance.SnapshottedUsage{
Since: params.StartingSnapshot.Usage.Since,
Usage: params.StartingSnapshot.Usage.Usage + totalUsage,
}
usageSnapshot := balance.UsageSnapshot{
Usage: params.StartingSnapshot.UsageSnapshot.Usage + totalUsage,
TotalGrantUsage: alpacadecimal.NewFromFloat(params.StartingSnapshot.UsageSnapshot.TotalGrantUsage).
Add(history.TotalGrantUsage()).
InexactFloat64(),
}

return RunResult{
Snapshot: balance.Snapshot{
Balances: balancesAtPhaseStart,
Overage: overage,
At: period.To,
Usage: balance.SnapshottedUsage{
Since: params.StartingSnapshot.Usage.Since,
Usage: params.StartingSnapshot.Usage.Usage + history.TotalUsageInHistory(),
},
Usage: usage,
UsageSnapshot: &usageSnapshot,
Balances: balancesAtPhaseStart,
Overage: overage,
At: period.To,
},
History: history,
}, 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.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether snapshot construction sites propagate UnitConfig consistently.
rg -n 'balance\.Snapshot\{' openmeter/credit/engine/*.go -A 8
rg -n 'UnitConfig' openmeter/credit/engine/*.go openmeter/credit/balance/*.go

Repository: openmeterio/openmeter

Length of output: 37532


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== engine run.go relevant sections =="
sed -n '1,90p' openmeter/credit/engine/run.go
sed -n '230,285p' openmeter/credit/engine/run.go

echo "== reset.go relevant sections =="
sed -n '1,95p' openmeter/credit/engine/reset.go

echo "== balance snapshot definition =="
sed -n '115,145p' openmeter/credit/balance/balance.go

echo "== git diff stat/name-only if available =="
git diff --stat
git diff --openmeter/credit/engine/run.go --openmeter/credit/engine/reset.go --openmeter/credit/balance/balance.go --openmeter | sed -n '1,200p' || true

echo "== structural scan balance.Snapshot assignment sites in openmeter/credit =="
python3 - <<'PY'
from pathlib import Path
import re
for p in sorted(Path('openmeter/credit').rglob('*.go')):
    text = p.read_text(errors='ignore')
    if 'balance.Snapshot{' not in text:
        continue
    lines=text.splitlines()
    for i,l in enumerate(lines,1):
        if 'balance.Snapshot{' in l:
            snippet='\n'.join(lines[i-1:min(i+20,len(lines))])
            has_unit='UnitConfig' in snippet
            print(f"{p}:{i}: UnitConfig={has_unit}")
            print(snippet[:800])
            print("----")
PY

Repository: openmeterio/openmeter

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== snapshot validation / read usage =="
rg -n 'func validateStartingSnapshot|StartingSnapshot|UnitConfig' openmeter/credit/openmeter credit openmeter/credit -g '*.go' | sed -n '1,220p'

echo "== balance snapshot read validation =="
rg -n 'func .*Snapshot|Snapshot|UnitConfig' openmeter/credit/adapter openmeter/credit openmeter -g '*.go' | rg -n 'Snapshot|UnitConfig' | sed -n '1,240p'

Repository: openmeterio/openmeter

Length of output: 12212


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== snapshot validation / read usage =="
rg -n 'func validateStartingSnapshot|StartingSnapshot|UnitConfig' openmeter/credit -g '*.go' | sed -n '1,220p'

echo "== balance snapshot read validation =="
rg -n 'Snapshot|UnitConfig' openmeter/credit -g '*.go' | sed -n '1,240p'

Repository: openmeterio/openmeter

Length of output: 35919


Preserve UnitConfig on both returned snapshot sites.

Run() replaces snapshot with runRes.Snapshot, and runBetweenResets() returns a new balance.Snapshot without copying UnitConfig, so later engine runs lose the conversion regime. Add params.StartingSnapshot.UnitConfig here, and carry it through reset() as well where the reset snapshot is returned for the next segment.

🤖 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 `@openmeter/credit/engine/run.go` around lines 251 - 277, Preserve the
conversion configuration when constructing the snapshot returned by
runBetweenResets: copy params.StartingSnapshot.UnitConfig into the new
balance.Snapshot alongside the existing fields. Also update reset() to copy
UnitConfig into its returned reset snapshot so subsequent engine segments retain
the same configuration.

Source: Path instructions

Comment on lines +27 to +28
that materializes and supersedes subscription-managed entitlements.
Entitlement owns their persisted lifecycle and value resolution.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the pronoun-agreement slip.

"Entitlement" is singular, so "their" should be "its" in this sentence.

✏️ Proposed fix
-Entitlement owns their persisted lifecycle and value resolution.
+Entitlement owns its persisted lifecycle and value resolution.
📝 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
that materializes and supersedes subscription-managed entitlements.
Entitlement owns their persisted lifecycle and value resolution.
that materializes and supersedes subscription-managed entitlements.
Entitlement owns its persisted lifecycle and value resolution.
🤖 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 `@openmeter/entitlement/README.md` around lines 27 - 28, Update the sentence in
the entitlement documentation so the singular subject “Entitlement” uses the
possessive pronoun “its” instead of “their,” leaving the rest of the wording
unchanged.

Source: Path instructions

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

Labels

release-note/bug-fix Release note: Bug Fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant