Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 28 additions & 22 deletions docs/schemas/agent-cost-account-balance-v1.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,25 @@
"title": "Agent Cost Account Balance Snapshot v1",
"type": "object",
"additionalProperties": false,
"required": ["schema", "generatedAt", "snapshotAt", "plan", "credits", "provenance"],
"required": [
"schema",
"generatedAt",
"effectiveAt",
"plan",
"balances",
"sourceSchema",
"sourceKind",
"sourcePathEvidence",
"confidence",
"operatorNote"
],
"properties": {
"schema": { "const": "priority/agent-cost-account-balance@v1" },
"generatedAt": { "type": "string", "format": "date-time" },
"effectiveAt": { "type": "string", "format": "date-time" },
"capturedAt": { "type": "string", "format": "date-time" },
"snapshotAt": { "type": "string", "format": "date-time" },
"renewalCycleBoundaryAt": { "type": "string", "format": "date-time" },
"plan": {
"type": "object",
"additionalProperties": false,
Expand All @@ -19,31 +33,23 @@
"daysRemaining": { "type": "integer", "minimum": 0 }
}
},
"credits": {
"balances": {
"type": "object",
"additionalProperties": false,
"required": ["total", "used", "remaining"],
"required": ["totalCredits", "usedCredits", "remainingCredits"],
"properties": {
"total": { "type": "integer", "minimum": 0 },
"used": { "type": "integer", "minimum": 0 },
"remaining": { "type": "integer", "minimum": 0 }
"totalCredits": { "type": "integer", "minimum": 0 },
"usedCredits": { "type": "integer", "minimum": 0 },
"remainingCredits": { "type": "integer", "minimum": 0 }
}
},
"provenance": {
"type": "object",
"additionalProperties": false,
"required": ["sourceSchema", "sourceKind", "sourcePath", "observedAt", "confidence", "operatorNote"],
"properties": {
"sourceSchema": { "type": ["string", "null"], "minLength": 1 },
"sourceKind": {
"type": "string",
"enum": ["operator-account-state", "billing-export", "manual-baseline"]
},
"sourcePath": { "type": "string", "minLength": 1 },
"observedAt": { "type": "string", "format": "date-time" },
"confidence": { "type": "string", "enum": ["low", "medium", "high"] },
"operatorNote": { "type": "string", "minLength": 1 }
}
}
"sourceSchema": { "type": "string", "minLength": 1 },
"sourceKind": {
"type": "string",
"enum": ["operator-account-state", "billing-export", "manual-baseline"]
},
"sourcePathEvidence": { "type": "string", "minLength": 1 },
"confidence": { "type": "string", "enum": ["low", "medium", "high"] },
"operatorNote": { "type": "string", "minLength": 1 }
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
{
"schema": "priority/agent-cost-private-account-balance@v1",
"snapshotAt": "2026-03-21T12:00:00.000Z",
"capturedAt": "2026-03-21T12:00:00.000Z",
"effectiveAt": "2026-03-21T12:00:00.000Z",
"renewalCycleBoundaryAt": "2026-04-15T00:00:00.000Z",
"plan": {
"name": "business",
"renewsAt": "2026-04-15"
},
"credits": {
"total": 27500,
"used": 15800,
"remaining": 11700
"balances": {
"totalCredits": 27500,
"usedCredits": 15800,
"remainingCredits": 11700
},
"provenance": {
"sourceKind": "operator-account-state",
"sourcePath": "C:/Users/operator/Downloads/account-balance-20260321.json",
"observedAt": "2026-03-21T12:00:00.000Z",
"confidence": "high",
"operatorNote": "Sanitized operator-provided account balance snapshot."
}
"sourceSchema": "priority/agent-cost-private-account-balance@v1",
"sourceKind": "operator-account-state",
"sourcePathEvidence": "tools/priority/__fixtures__/agent-cost-rollup/private-account-balance-sample.json",
"confidence": "high",
"operatorNote": "Sanitized operator-provided account balance snapshot."
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ import {

const repoRoot = path.resolve(process.cwd());
const fixtureRoot = path.join(repoRoot, 'tools', 'priority', '__fixtures__', 'agent-cost-rollup');
const canonicalOutputPath = path.join(
repoRoot,
'tests',
'results',
'_agent',
'cost',
'account-balances',
'account-balance-2026-03-21.json'
);

function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
Expand All @@ -39,17 +48,22 @@ test('buildNormalizedAccountBalanceReceiptFromSnapshot normalizes a private acco
const report = buildNormalizedAccountBalanceReceiptFromSnapshot(snapshot);

assert.equal(report.schema, 'priority/agent-cost-account-balance@v1');
assert.equal(report.snapshotAt, '2026-03-21T12:00:00.000Z');
assert.equal(report.effectiveAt, '2026-03-21T12:00:00.000Z');
assert.equal(report.capturedAt, '2026-03-21T12:00:00.000Z');
assert.equal(report.renewalCycleBoundaryAt, '2026-04-15T00:00:00.000Z');
assert.equal(report.plan.name, 'business');
assert.equal(report.plan.renewsAt, '2026-04-15');
assert.equal(report.plan.daysRemaining, 25);
assert.equal(report.credits.total, 27500);
assert.equal(report.credits.used, 15800);
assert.equal(report.credits.remaining, 11700);
assert.equal(report.provenance.sourceSchema, 'priority/agent-cost-private-account-balance@v1');
assert.equal(report.provenance.sourceKind, 'operator-account-state');
assert.equal(report.provenance.observedAt, '2026-03-21T12:00:00.000Z');
assert.equal(report.provenance.confidence, 'high');
assert.deepEqual(report.balances, {
totalCredits: 27500,
usedCredits: 15800,
remainingCredits: 11700
});
assert.equal(report.sourceSchema, 'priority/agent-cost-private-account-balance@v1');
assert.equal(report.sourceKind, 'operator-account-state');
assert.equal(report.sourcePathEvidence, 'tools/priority/__fixtures__/agent-cost-rollup/private-account-balance-sample.json');
assert.equal(report.confidence, 'high');
assert.equal(report.operatorNote, 'Sanitized operator-provided account balance snapshot.');
});

test('runAgentCostAccountBalanceNormalize writes a normalized account-balance receipt to the requested path', () => {
Expand All @@ -60,10 +74,31 @@ test('runAgentCostAccountBalanceNormalize writes a normalized account-balance re
outputPath
});

assert.equal(result.report.credits.remaining, 11700);
assert.deepEqual(result.report.balances, {
totalCredits: 27500,
usedCredits: 15800,
remainingCredits: 11700
});
assert.equal(fs.existsSync(outputPath), true);
});

test('runAgentCostAccountBalanceNormalize auto-discovers the canonical account-balance output path when none is provided', () => {
if (fs.existsSync(canonicalOutputPath)) {
fs.rmSync(canonicalOutputPath, { force: true });
}

try {
const result = runAgentCostAccountBalanceNormalize({
snapshotPath: path.join(fixtureRoot, 'private-account-balance-sample.json')
});

assert.equal(result.outputPath, canonicalOutputPath);
assert.equal(fs.existsSync(canonicalOutputPath), true);
} finally {
fs.rmSync(canonicalOutputPath, { force: true });
}
});

test('agent-cost-account-balance-normalize CLI writes a receipt directly', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-cost-account-balance-normalize-cli-'));
const outputPath = path.join(tmpDir, 'account-balance.json');
Expand Down
95 changes: 74 additions & 21 deletions tools/priority/agent-cost-account-balance-normalize.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';

export const INPUT_SCHEMA = 'priority/agent-cost-private-account-balance@v1';
export const REPORT_SCHEMA = 'priority/agent-cost-account-balance@v1';
export const DEFAULT_OUTPUT_PATH = path.join('tests', 'results', '_agent', 'cost', 'agent-cost-account-balance.json');
export const DEFAULT_OUTPUT_DIRECTORY = path.join('tests', 'results', '_agent', 'cost', 'account-balances');

const CONFIDENCE_LEVELS = new Set(['low', 'medium', 'high']);

Expand Down Expand Up @@ -38,6 +38,14 @@ function normalizeDate(value) {
return Number.isFinite(parsed) ? new Date(parsed).toISOString().slice(0, 10) : '';
}

function toUtcMidnightDateTime(value) {
const text = normalizeDate(value);
if (!text) {
return '';
}
return `${text}T00:00:00.000Z`;
}

function normalizeConfidence(value) {
const text = normalizeText(value).toLowerCase();
return CONFIDENCE_LEVELS.has(text) ? text : '';
Expand All @@ -52,9 +60,9 @@ function toNonNegativeInteger(value) {
}

function deriveBalanceTotals(payload) {
let total = toNonNegativeInteger(payload?.credits?.total ?? payload?.totalCredits);
let used = toNonNegativeInteger(payload?.credits?.used ?? payload?.usedCredits);
let remaining = toNonNegativeInteger(payload?.credits?.remaining ?? payload?.remainingCredits);
let total = toNonNegativeInteger(payload?.balances?.totalCredits ?? payload?.credits?.total ?? payload?.totalCredits);
let used = toNonNegativeInteger(payload?.balances?.usedCredits ?? payload?.credits?.used ?? payload?.usedCredits);
let remaining = toNonNegativeInteger(payload?.balances?.remainingCredits ?? payload?.credits?.remaining ?? payload?.remainingCredits);

if (total == null && used != null && remaining != null) {
total = used + remaining;
Expand Down Expand Up @@ -116,8 +124,13 @@ function validateInputPayload(payload) {
throw new Error('Account balance payload must include plan.renewsAt.');
}

if (!normalizeText(payload.sourcePath) && !normalizeText(payload?.provenance?.sourcePath)) {
throw new Error('Account balance payload must include sourcePath.');
if (
!normalizeText(payload.sourcePathEvidence) &&
!normalizeText(payload.sourcePath) &&
!normalizeText(payload?.provenance?.sourcePathEvidence) &&
!normalizeText(payload?.provenance?.sourcePath)
) {
throw new Error('Account balance payload must include sourcePathEvidence.');
}
}

Expand Down Expand Up @@ -172,50 +185,88 @@ export function buildNormalizedAccountBalanceReceiptFromSnapshot(snapshotPayload
throw new Error('Account balance payload snapshotAt must be a valid date-time.');
}

const capturedAt = normalizeDateTime(snapshotPayload.capturedAt) || snapshotAt;
const effectiveAt = normalizeDateTime(snapshotPayload.effectiveAt) || snapshotAt;

const renewsAt = normalizeDate(snapshotPayload?.plan?.renewsAt ?? snapshotPayload?.renewsAt);
if (!renewsAt) {
throw new Error('Account balance payload plan.renewsAt must be a valid date.');
}

const renewalCycleBoundaryAt = toUtcMidnightDateTime(renewsAt);
if (!renewalCycleBoundaryAt) {
throw new Error('Account balance payload plan.renewsAt must resolve to a cycle boundary.');
}

const credits = deriveBalanceTotals(snapshotPayload);
const cycleDaysRemaining = deriveCycleDaysRemaining(snapshotPayload, snapshotAt, renewsAt);
if (cycleDaysRemaining == null) {
throw new Error('Account balance payload must include cycle.daysRemaining or enough dates to derive it.');
}

const confidence = normalizeConfidence(snapshotPayload?.provenance?.confidence ?? snapshotPayload.confidence) || 'high';
const sourcePath = normalizeText(snapshotPayload.sourcePath) || normalizeText(snapshotPayload?.provenance?.sourcePath);
const sourceSchema = normalizeText(snapshotPayload?.sourceSchema ?? snapshotPayload?.provenance?.sourceSchema) || INPUT_SCHEMA;
const sourceKind = normalizeText(snapshotPayload?.sourceKind ?? snapshotPayload?.provenance?.sourceKind) || 'operator-account-state';
const sourcePathEvidence =
normalizeText(snapshotPayload.sourcePathEvidence) ||
normalizeText(snapshotPayload.sourcePath) ||
normalizeText(snapshotPayload?.provenance?.sourcePathEvidence) ||
normalizeText(snapshotPayload?.provenance?.sourcePath);
const operatorNote =
normalizeText(snapshotPayload.operatorNote) ||
normalizeText(snapshotPayload?.provenance?.operatorNote) ||
'Normalized from a local private account balance snapshot.';
const generatedAt = normalizeDateTime(options.generatedAt) || new Date().toISOString();

return {
schema: REPORT_SCHEMA,
generatedAt,
effectiveAt,
snapshotAt,
capturedAt,
renewalCycleBoundaryAt,
plan: {
name: normalizeText(snapshotPayload?.plan?.name) || 'business',
renewsAt,
daysRemaining: cycleDaysRemaining
},
credits,
provenance: {
sourceSchema: normalizeText(snapshotPayload.schema) || INPUT_SCHEMA,
sourceKind: normalizeText(snapshotPayload?.provenance?.sourceKind) || 'operator-account-state',
sourcePath,
observedAt: normalizeDateTime(snapshotPayload?.provenance?.observedAt) || snapshotAt,
confidence,
operatorNote:
normalizeText(snapshotPayload?.provenance?.operatorNote) ||
'Normalized from a local private account balance snapshot.'
}
balances: {
totalCredits: credits.total,
usedCredits: credits.used,
remainingCredits: credits.remaining
},
sourceSchema,
sourceKind,
sourcePathEvidence,
confidence,
operatorNote
};
}

function deriveDefaultOutputPath(report) {
const effectiveDate = normalizeDate(report?.effectiveAt) || normalizeDate(report?.capturedAt) || normalizeDate(report?.snapshotAt);
if (!effectiveDate) {
throw new Error('Normalized account balance receipts must include an effectiveAt, capturedAt, or snapshotAt date.');
}
return path.join(DEFAULT_OUTPUT_DIRECTORY, `account-balance-${effectiveDate}.json`);
}

export function runAgentCostAccountBalanceNormalize(options) {
const snapshot = readSnapshot(options.snapshotPath);
const report = buildNormalizedAccountBalanceReceiptFromSnapshot(snapshot.payload, {
const payload = {
...snapshot.payload,
sourcePathEvidence:
normalizeText(snapshot.payload?.sourcePathEvidence) ||
normalizeText(snapshot.payload?.sourcePath) ||
normalizeText(snapshot.payload?.provenance?.sourcePathEvidence) ||
normalizeText(snapshot.payload?.provenance?.sourcePath) ||
snapshot.resolved
};
const report = buildNormalizedAccountBalanceReceiptFromSnapshot(payload, {
generatedAt: options.generatedAt
});
const outputPath = path.resolve(options.outputPath || DEFAULT_OUTPUT_PATH);
const defaultOutputPath = deriveDefaultOutputPath(report);
const outputPath = path.resolve(options.outputPath || defaultOutputPath);
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
return {
Expand All @@ -230,7 +281,9 @@ function printUsage() {
console.log('');
console.log('Options:');
console.log(` --snapshot <path> Local private account balance snapshot JSON (${INPUT_SCHEMA}) (required).`);
console.log(' --output <path> Optional account-balance receipt output override.');
console.log(
' --output <path> Optional account-balance receipt output override (defaults to tests/results/_agent/cost/account-balances/account-balance-<YYYY-MM-DD>.json).'
);
console.log(' -h, --help Show help and exit.');
}

Expand Down
Loading