diff --git a/compute-meter-replay-guard/README.md b/compute-meter-replay-guard/README.md new file mode 100644 index 00000000..6e8c0a2d --- /dev/null +++ b/compute-meter-replay-guard/README.md @@ -0,0 +1,76 @@ +# Compute Meter Replay Guard + +This module adds a deterministic guard for AI compute revenue metering before +usage events are converted into invoice lines. + +It is scoped to synthetic billing telemetry only. It does not connect to Stripe, +PayPal, bank accounts, wallets, customer systems, cloud meters, or private +research data. + +## Why This Belongs In Revenue Infrastructure + +Usage-based AI billing depends on reliable metering. A replay job, duplicate +event, missing idempotency key, stale correction, or unauthorized negative +adjustment can overbill customers, underbill usage, or create finance disputes. +This guard reviews a batch of synthetic meter events and decides whether the +invoice run can proceed, needs repricing, or must be held. + +## Checks + +- Missing idempotency keys before billing. +- Duplicate idempotency keys inside the replay window. +- Reused invoice-line IDs across separate usage events. +- Source sequence gaps that may indicate lost meter events. +- Corrections older than the allowed correction window. +- Negative adjustments without an authorized finance or meter role. +- Plan and entitlement mismatches between event metadata and account state. +- Event timestamp ordering problems. + +## Usage + +```bash +node compute-meter-replay-guard/src/compute-meter-replay-guard.mjs \ + compute-meter-replay-guard/fixtures/compute-meter-events.json \ + --format markdown +``` + +JSON output: + +```bash +node compute-meter-replay-guard/src/compute-meter-replay-guard.mjs \ + compute-meter-replay-guard/fixtures/compute-meter-events.json \ + --format json +``` + +## Validation + +```bash +node compute-meter-replay-guard/test/run-tests.mjs +``` + +Expected result: + +```text +compute meter replay guard tests passed +``` + +## Demo Artifacts + +- `demo/sample-report.md` +- `demo/sample-report.json` +- `demo/decision-flow.svg` +- `demo/decision-flow.png` +- `demo/compute-meter-replay-demo.mp4` + +## Scope Boundaries + +This assistant intentionally avoids: + +- live payment processors, banks, wallets, cards, or invoices; +- real customer data; +- external APIs or cloud billing systems; +- tax, accounting, legal, or financial advice; +- replacing human finance approval. + +It is a pre-invoice control that highlights billing evidence gaps and replay +risks for review. diff --git a/compute-meter-replay-guard/demo/compute-meter-replay-demo.mp4 b/compute-meter-replay-guard/demo/compute-meter-replay-demo.mp4 new file mode 100644 index 00000000..ed88919a Binary files /dev/null and b/compute-meter-replay-guard/demo/compute-meter-replay-demo.mp4 differ diff --git a/compute-meter-replay-guard/demo/decision-flow.png b/compute-meter-replay-guard/demo/decision-flow.png new file mode 100644 index 00000000..1bcf5fc3 Binary files /dev/null and b/compute-meter-replay-guard/demo/decision-flow.png differ diff --git a/compute-meter-replay-guard/demo/decision-flow.svg b/compute-meter-replay-guard/demo/decision-flow.svg new file mode 100644 index 00000000..1e0847ee --- /dev/null +++ b/compute-meter-replay-guard/demo/decision-flow.svg @@ -0,0 +1,25 @@ + + + Compute Meter Replay Guard + Pre-invoice control for replayed AI usage-meter events + + 1. Load meter events + Synthetic usage, corrections, accounts + + 2. Detect replay risk + Idempotency, sequence gaps, stale fixes + + 3. Hold or reprice + Prevent duplicate billing and disputes + + + + Demo result: HOLD + Critical: duplicate idempotency key, duplicate invoice line, unauthorized negative adjustment + High: source sequence gap, stale correction, plan mismatch, missing idempotency key + + + + + + diff --git a/compute-meter-replay-guard/demo/sample-report.json b/compute-meter-replay-guard/demo/sample-report.json new file mode 100644 index 00000000..dd16bd65 --- /dev/null +++ b/compute-meter-replay-guard/demo/sample-report.json @@ -0,0 +1,96 @@ +{ + "runId": "REV-METER-001", + "title": "June AI compute replay pre-invoice check", + "generatedAt": "2026-06-01T13:50:00Z", + "assistant": "compute-meter-replay-guard", + "decision": "hold", + "severityCounts": { + "critical": 3, + "high": 4, + "medium": 1, + "low": 0 + }, + "findings": [ + { + "severity": "critical", + "eventIds": [ + "E-001", + "E-002" + ], + "check": "duplicate-idempotency-key", + "evidence": "Idempotency key lab-pro:run-77:chunk-1 appears 2 times over 0.0 hours.", + "remediation": "Bill only the first accepted event and quarantine replay duplicates before invoice lines are generated." + }, + { + "severity": "critical", + "eventIds": [ + "E-007", + "E-008" + ], + "check": "duplicate-invoice-line", + "evidence": "Invoice line line-502 is referenced by 2 events.", + "remediation": "Hold the invoice run and regenerate invoice-line mapping from accepted meter events." + }, + { + "severity": "critical", + "eventIds": [ + "E-004" + ], + "check": "unauthorized-negative-adjustment", + "evidence": "E-004 applies -2400 cents with role support-agent.", + "remediation": "Require finance or meter-admin approval before applying a negative adjustment." + }, + { + "severity": "high", + "eventIds": [ + "E-005" + ], + "check": "entitlement-plan-mismatch", + "evidence": "E-005 was metered as individual-pro, but account acct-institution is on institution.", + "remediation": "Reprice the event under the account's active plan or hold invoice release." + }, + { + "severity": "high", + "eventIds": [ + "E-006" + ], + "check": "missing-idempotency-key", + "evidence": "E-006 has no idempotency key.", + "remediation": "Reject or enrich the event before it can create an invoice line." + }, + { + "severity": "high", + "eventIds": [ + "E-002", + "E-003" + ], + "check": "source-sequence-gap", + "evidence": "acct-lab-pro:usage-api jumps from sequence 101 to 104.", + "remediation": "Check replay source logs for missing usage before releasing invoice totals." + }, + { + "severity": "high", + "eventIds": [ + "E-004" + ], + "check": "stale-correction", + "evidence": "E-004 correction age is 167.1 hours.", + "remediation": "Route old corrections through a manual reprice or credit memo review." + }, + { + "severity": "medium", + "eventIds": [ + "E-006" + ], + "check": "timestamp-ordering", + "evidence": "E-006 was received before it occurred.", + "remediation": "Normalize source clocks or hold the event from invoice release." + } + ], + "financePrompts": [ + "Which replay jobs can create duplicate invoice lines, and where should idempotency be enforced?", + "Which meter source logs prove whether missing sequence numbers represent lost usage or skipped test events?", + "Which correction approvals should route to credit memo review instead of automatic invoice repricing?", + "Which account plan snapshot should be used as the billable source of truth for this invoice run?" + ] +} diff --git a/compute-meter-replay-guard/demo/sample-report.md b/compute-meter-replay-guard/demo/sample-report.md new file mode 100644 index 00000000..85f49f06 --- /dev/null +++ b/compute-meter-replay-guard/demo/sample-report.md @@ -0,0 +1,59 @@ +# Compute Meter Replay Guard - REV-METER-001 + +Decision: HOLD + +## Severity Counts + +- Critical: 3 +- High: 4 +- Medium: 1 +- Low: 0 + +## Findings + +### [CRITICAL] duplicate-idempotency-key +- Events: E-001, E-002 +- Evidence: Idempotency key lab-pro:run-77:chunk-1 appears 2 times over 0.0 hours. +- Remediation: Bill only the first accepted event and quarantine replay duplicates before invoice lines are generated. + +### [CRITICAL] duplicate-invoice-line +- Events: E-007, E-008 +- Evidence: Invoice line line-502 is referenced by 2 events. +- Remediation: Hold the invoice run and regenerate invoice-line mapping from accepted meter events. + +### [CRITICAL] unauthorized-negative-adjustment +- Events: E-004 +- Evidence: E-004 applies -2400 cents with role support-agent. +- Remediation: Require finance or meter-admin approval before applying a negative adjustment. + +### [HIGH] entitlement-plan-mismatch +- Events: E-005 +- Evidence: E-005 was metered as individual-pro, but account acct-institution is on institution. +- Remediation: Reprice the event under the account's active plan or hold invoice release. + +### [HIGH] missing-idempotency-key +- Events: E-006 +- Evidence: E-006 has no idempotency key. +- Remediation: Reject or enrich the event before it can create an invoice line. + +### [HIGH] source-sequence-gap +- Events: E-002, E-003 +- Evidence: acct-lab-pro:usage-api jumps from sequence 101 to 104. +- Remediation: Check replay source logs for missing usage before releasing invoice totals. + +### [HIGH] stale-correction +- Events: E-004 +- Evidence: E-004 correction age is 167.1 hours. +- Remediation: Route old corrections through a manual reprice or credit memo review. + +### [MEDIUM] timestamp-ordering +- Events: E-006 +- Evidence: E-006 was received before it occurred. +- Remediation: Normalize source clocks or hold the event from invoice release. + +## Finance Prompts + +- Which replay jobs can create duplicate invoice lines, and where should idempotency be enforced? +- Which meter source logs prove whether missing sequence numbers represent lost usage or skipped test events? +- Which correction approvals should route to credit memo review instead of automatic invoice repricing? +- Which account plan snapshot should be used as the billable source of truth for this invoice run? diff --git a/compute-meter-replay-guard/fixtures/compute-meter-events.json b/compute-meter-replay-guard/fixtures/compute-meter-events.json new file mode 100644 index 00000000..f7912b00 --- /dev/null +++ b/compute-meter-replay-guard/fixtures/compute-meter-events.json @@ -0,0 +1,140 @@ +{ + "runId": "REV-METER-001", + "title": "June AI compute replay pre-invoice check", + "generatedAt": "2026-06-01T13:50:00Z", + "policy": { + "duplicateReplayWindowHours": 24, + "maxCorrectionAgeHours": 72, + "allowedNegativeAdjustmentRoles": ["finance-ops", "meter-admin"], + "invoiceHoldThresholdCents": 10000 + }, + "accounts": [ + { + "id": "acct-lab-pro", + "currentPlan": "lab-pro", + "allowedMeterSources": ["usage-api", "batch-replay"], + "monthlyQuotaTokens": 1000000 + }, + { + "id": "acct-institution", + "currentPlan": "institution", + "allowedMeterSources": ["meter-gateway"], + "monthlyQuotaTokens": 5000000 + } + ], + "events": [ + { + "eventId": "E-001", + "accountId": "acct-lab-pro", + "source": "usage-api", + "sequence": 100, + "type": "usage", + "plan": "lab-pro", + "idempotencyKey": "lab-pro:run-77:chunk-1", + "invoiceLineId": "line-100", + "usageTokens": 120000, + "amountCents": 2400, + "occurredAt": "2026-05-31T09:00:00Z", + "receivedAt": "2026-05-31T09:00:08Z" + }, + { + "eventId": "E-002", + "accountId": "acct-lab-pro", + "source": "usage-api", + "sequence": 101, + "type": "usage", + "plan": "lab-pro", + "idempotencyKey": "lab-pro:run-77:chunk-1", + "invoiceLineId": "line-101", + "usageTokens": 120000, + "amountCents": 2400, + "occurredAt": "2026-05-31T09:01:00Z", + "receivedAt": "2026-05-31T09:01:06Z" + }, + { + "eventId": "E-003", + "accountId": "acct-lab-pro", + "source": "usage-api", + "sequence": 104, + "type": "usage", + "plan": "lab-pro", + "idempotencyKey": "lab-pro:run-77:chunk-4", + "invoiceLineId": "line-104", + "usageTokens": 80000, + "amountCents": 1600, + "occurredAt": "2026-05-31T09:04:00Z", + "receivedAt": "2026-05-31T09:04:04Z" + }, + { + "eventId": "E-004", + "accountId": "acct-lab-pro", + "source": "batch-replay", + "sequence": 105, + "type": "correction", + "plan": "lab-pro", + "idempotencyKey": "lab-pro:correction:E-001", + "correctionOf": "E-001", + "adjustmentRole": "support-agent", + "usageTokens": -120000, + "amountCents": -2400, + "occurredAt": "2026-05-25T10:00:00Z", + "receivedAt": "2026-06-01T09:05:00Z" + }, + { + "eventId": "E-005", + "accountId": "acct-institution", + "source": "meter-gateway", + "sequence": 1, + "type": "usage", + "plan": "individual-pro", + "idempotencyKey": "inst:job-4:part-1", + "invoiceLineId": "line-500", + "usageTokens": 900000, + "amountCents": 18000, + "occurredAt": "2026-05-31T12:00:00Z", + "receivedAt": "2026-05-31T12:00:12Z" + }, + { + "eventId": "E-006", + "accountId": "acct-institution", + "source": "meter-gateway", + "sequence": 2, + "type": "usage", + "plan": "institution", + "idempotencyKey": "", + "invoiceLineId": "line-501", + "usageTokens": 300000, + "amountCents": 6000, + "occurredAt": "2026-05-31T12:06:00Z", + "receivedAt": "2026-05-31T12:05:55Z" + }, + { + "eventId": "E-007", + "accountId": "acct-institution", + "source": "meter-gateway", + "sequence": 3, + "type": "usage", + "plan": "institution", + "idempotencyKey": "inst:job-5:part-1", + "invoiceLineId": "line-502", + "usageTokens": 100000, + "amountCents": 2000, + "occurredAt": "2026-05-31T12:07:00Z", + "receivedAt": "2026-05-31T12:07:04Z" + }, + { + "eventId": "E-008", + "accountId": "acct-institution", + "source": "meter-gateway", + "sequence": 4, + "type": "usage", + "plan": "institution", + "idempotencyKey": "inst:job-5:part-2", + "invoiceLineId": "line-502", + "usageTokens": 100000, + "amountCents": 2000, + "occurredAt": "2026-05-31T12:08:00Z", + "receivedAt": "2026-05-31T12:08:04Z" + } + ] +} diff --git a/compute-meter-replay-guard/src/compute-meter-replay-guard.mjs b/compute-meter-replay-guard/src/compute-meter-replay-guard.mjs new file mode 100644 index 00000000..04e3655e --- /dev/null +++ b/compute-meter-replay-guard/src/compute-meter-replay-guard.mjs @@ -0,0 +1,307 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; + +const SEVERITY_SCORE = { + critical: 4, + high: 3, + medium: 2, + low: 1 +}; + +function hoursBetween(startIso, endIso) { + const start = new Date(startIso).getTime(); + const end = new Date(endIso).getTime(); + return (end - start) / 36e5; +} + +function finding(severity, eventIds, check, evidence, remediation) { + return { severity, eventIds, check, evidence, remediation }; +} + +function makeAccountMap(packet) { + return new Map((packet.accounts || []).map((account) => [account.id, account])); +} + +function groupBy(events, keyFn) { + const grouped = new Map(); + for (const event of events) { + const key = keyFn(event); + if (!key) continue; + if (!grouped.has(key)) grouped.set(key, []); + grouped.get(key).push(event); + } + return grouped; +} + +function checkEventBasics(packet, accountMap) { + const findings = []; + const policy = packet.policy || {}; + const allowedNegativeRoles = new Set(policy.allowedNegativeAdjustmentRoles || []); + + for (const event of packet.events || []) { + const account = accountMap.get(event.accountId); + + if (!account) { + findings.push(finding( + "critical", + [event.eventId], + "unknown-account", + `${event.eventId} references missing account ${event.accountId}.`, + "Hold billing until the meter event is linked to a valid account record." + )); + continue; + } + + if (!event.idempotencyKey) { + findings.push(finding( + "high", + [event.eventId], + "missing-idempotency-key", + `${event.eventId} has no idempotency key.`, + "Reject or enrich the event before it can create an invoice line." + )); + } + + if (!account.allowedMeterSources.includes(event.source)) { + findings.push(finding( + "high", + [event.eventId], + "unauthorized-meter-source", + `${event.source} is not an allowed meter source for ${event.accountId}.`, + "Route the event through an approved metering source or hold billing." + )); + } + + if (event.plan !== account.currentPlan) { + findings.push(finding( + "high", + [event.eventId], + "entitlement-plan-mismatch", + `${event.eventId} was metered as ${event.plan}, but account ${event.accountId} is on ${account.currentPlan}.`, + "Reprice the event under the account's active plan or hold invoice release." + )); + } + + if (event.usageTokens > account.monthlyQuotaTokens) { + findings.push(finding( + "medium", + [event.eventId], + "quota-outlier", + `${event.eventId} reports ${event.usageTokens} tokens against quota ${account.monthlyQuotaTokens}.`, + "Confirm overage policy before invoice generation." + )); + } + + if (new Date(event.receivedAt).getTime() < new Date(event.occurredAt).getTime()) { + findings.push(finding( + "medium", + [event.eventId], + "timestamp-ordering", + `${event.eventId} was received before it occurred.`, + "Normalize source clocks or hold the event from invoice release." + )); + } + + if ((event.amountCents || 0) < 0 && !allowedNegativeRoles.has(event.adjustmentRole)) { + findings.push(finding( + "critical", + [event.eventId], + "unauthorized-negative-adjustment", + `${event.eventId} applies ${event.amountCents} cents with role ${event.adjustmentRole || "missing"}.`, + "Require finance or meter-admin approval before applying a negative adjustment." + )); + } + + if (event.type === "correction") { + const ageHours = hoursBetween(event.occurredAt, event.receivedAt); + if (ageHours > (policy.maxCorrectionAgeHours || 72)) { + findings.push(finding( + "high", + [event.eventId], + "stale-correction", + `${event.eventId} correction age is ${ageHours.toFixed(1)} hours.`, + "Route old corrections through a manual reprice or credit memo review." + )); + } + } + } + + return findings; +} + +function checkDuplicateKeys(packet) { + const findings = []; + const policy = packet.policy || {}; + const events = packet.events || []; + const byKey = groupBy(events, (event) => `${event.accountId}:${event.source}:${event.idempotencyKey}`); + + for (const [, matches] of byKey) { + if (matches.length < 2) continue; + const sorted = [...matches].sort((a, b) => new Date(a.receivedAt) - new Date(b.receivedAt)); + const spanHours = hoursBetween(sorted[0].receivedAt, sorted[sorted.length - 1].receivedAt); + const severity = spanHours <= (policy.duplicateReplayWindowHours || 24) ? "critical" : "medium"; + findings.push(finding( + severity, + sorted.map((event) => event.eventId), + "duplicate-idempotency-key", + `Idempotency key ${sorted[0].idempotencyKey} appears ${sorted.length} times over ${spanHours.toFixed(1)} hours.`, + "Bill only the first accepted event and quarantine replay duplicates before invoice lines are generated." + )); + } + + const byInvoiceLine = groupBy(events, (event) => event.invoiceLineId); + for (const [invoiceLineId, matches] of byInvoiceLine) { + if (matches.length < 2) continue; + findings.push(finding( + "critical", + matches.map((event) => event.eventId), + "duplicate-invoice-line", + `Invoice line ${invoiceLineId} is referenced by ${matches.length} events.`, + "Hold the invoice run and regenerate invoice-line mapping from accepted meter events." + )); + } + + return findings; +} + +function checkSequences(packet) { + const findings = []; + const byStream = groupBy(packet.events || [], (event) => `${event.accountId}:${event.source}`); + + for (const [stream, events] of byStream) { + const sorted = [...events].sort((a, b) => a.sequence - b.sequence); + for (let index = 1; index < sorted.length; index += 1) { + const previous = sorted[index - 1]; + const current = sorted[index]; + const gap = current.sequence - previous.sequence; + if (gap > 1) { + findings.push(finding( + "high", + [previous.eventId, current.eventId], + "source-sequence-gap", + `${stream} jumps from sequence ${previous.sequence} to ${current.sequence}.`, + "Check replay source logs for missing usage before releasing invoice totals." + )); + } + } + } + + return findings; +} + +function summarizeDecision(findings) { + const counts = findings.reduce((acc, item) => { + acc[item.severity] = (acc[item.severity] || 0) + 1; + return acc; + }, {}); + + let decision = "invoice"; + if ((counts.critical || 0) > 0) { + decision = "hold"; + } else if ((counts.high || 0) > 0 || (counts.medium || 0) >= 3) { + decision = "reprice"; + } + + return { + decision, + counts: { + critical: counts.critical || 0, + high: counts.high || 0, + medium: counts.medium || 0, + low: counts.low || 0 + } + }; +} + +function makeFinancePrompts(findings) { + const checks = new Set(findings.map((item) => item.check)); + const prompts = []; + if (checks.has("duplicate-idempotency-key") || checks.has("duplicate-invoice-line")) { + prompts.push("Which replay jobs can create duplicate invoice lines, and where should idempotency be enforced?"); + } + if (checks.has("source-sequence-gap")) { + prompts.push("Which meter source logs prove whether missing sequence numbers represent lost usage or skipped test events?"); + } + if (checks.has("stale-correction") || checks.has("unauthorized-negative-adjustment")) { + prompts.push("Which correction approvals should route to credit memo review instead of automatic invoice repricing?"); + } + if (checks.has("entitlement-plan-mismatch")) { + prompts.push("Which account plan snapshot should be used as the billable source of truth for this invoice run?"); + } + return prompts; +} + +export function analyzePacket(packet) { + const accountMap = makeAccountMap(packet); + const findings = [ + ...checkEventBasics(packet, accountMap), + ...checkDuplicateKeys(packet), + ...checkSequences(packet) + ]; + const summary = summarizeDecision(findings); + + return { + runId: packet.runId, + title: packet.title, + generatedAt: packet.generatedAt, + assistant: "compute-meter-replay-guard", + decision: summary.decision, + severityCounts: summary.counts, + findings: findings.sort((a, b) => { + return (SEVERITY_SCORE[b.severity] || 0) - (SEVERITY_SCORE[a.severity] || 0) + || a.check.localeCompare(b.check); + }), + financePrompts: makeFinancePrompts(findings) + }; +} + +export function renderMarkdown(report) { + const lines = [ + `# Compute Meter Replay Guard - ${report.runId}`, + "", + `Decision: ${report.decision.toUpperCase()}`, + "", + "## Severity Counts", + "", + `- Critical: ${report.severityCounts.critical}`, + `- High: ${report.severityCounts.high}`, + `- Medium: ${report.severityCounts.medium}`, + `- Low: ${report.severityCounts.low}`, + "", + "## Findings", + "" + ]; + + for (const item of report.findings) { + lines.push(`### [${item.severity.toUpperCase()}] ${item.check}`); + lines.push(`- Events: ${item.eventIds.join(", ")}`); + lines.push(`- Evidence: ${item.evidence}`); + lines.push(`- Remediation: ${item.remediation}`); + lines.push(""); + } + + lines.push("## Finance Prompts"); + lines.push(""); + for (const prompt of report.financePrompts) { + lines.push(`- ${prompt}`); + } + lines.push(""); + + return lines.join("\n"); +} + +function runCli() { + const [, scriptPath, fixturePath, , format = "json"] = process.argv; + if (!fixturePath || !scriptPath.endsWith("compute-meter-replay-guard.mjs")) return; + + const packet = JSON.parse(fs.readFileSync(path.resolve(fixturePath), "utf8")); + const report = analyzePacket(packet); + if (format === "markdown") { + process.stdout.write(renderMarkdown(report)); + } else { + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); + } +} + +runCli(); diff --git a/compute-meter-replay-guard/test/run-tests.mjs b/compute-meter-replay-guard/test/run-tests.mjs new file mode 100644 index 00000000..165e98a7 --- /dev/null +++ b/compute-meter-replay-guard/test/run-tests.mjs @@ -0,0 +1,79 @@ +#!/usr/bin/env node +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { analyzePacket, renderMarkdown } from "../src/compute-meter-replay-guard.mjs"; + +const fixturePath = path.resolve("compute-meter-replay-guard/fixtures/compute-meter-events.json"); +const packet = JSON.parse(fs.readFileSync(fixturePath, "utf8")); +const report = analyzePacket(packet); + +assert.equal(report.assistant, "compute-meter-replay-guard"); +assert.equal(report.decision, "hold"); +assert.equal(report.severityCounts.critical, 3); +assert.equal(report.severityCounts.high, 4); +assert.equal(report.severityCounts.medium, 1); +assert.ok(report.findings.some((finding) => finding.check === "duplicate-idempotency-key")); +assert.ok(report.findings.some((finding) => finding.check === "duplicate-invoice-line")); +assert.ok(report.findings.some((finding) => finding.check === "unauthorized-negative-adjustment")); +assert.ok(report.findings.some((finding) => finding.check === "entitlement-plan-mismatch")); +assert.ok(report.financePrompts.some((prompt) => prompt.includes("idempotency"))); + +const markdown = renderMarkdown(report); +assert.match(markdown, /Decision: HOLD/); +assert.match(markdown, /Compute Meter Replay Guard/); +assert.match(markdown, /duplicate-invoice-line/); + +const cleanPacket = { + runId: "REV-CLEAN", + title: "Clean invoice run", + generatedAt: "2026-06-01T13:50:00Z", + policy: { + duplicateReplayWindowHours: 24, + maxCorrectionAgeHours: 72, + allowedNegativeAdjustmentRoles: ["finance-ops"], + invoiceHoldThresholdCents: 10000 + }, + accounts: [{ + id: "acct-clean", + currentPlan: "lab-pro", + allowedMeterSources: ["usage-api"], + monthlyQuotaTokens: 1000000 + }], + events: [ + { + eventId: "E-clean-1", + accountId: "acct-clean", + source: "usage-api", + sequence: 1, + type: "usage", + plan: "lab-pro", + idempotencyKey: "clean:1", + invoiceLineId: "clean-line-1", + usageTokens: 250000, + amountCents: 5000, + occurredAt: "2026-06-01T01:00:00Z", + receivedAt: "2026-06-01T01:00:03Z" + }, + { + eventId: "E-clean-2", + accountId: "acct-clean", + source: "usage-api", + sequence: 2, + type: "usage", + plan: "lab-pro", + idempotencyKey: "clean:2", + invoiceLineId: "clean-line-2", + usageTokens: 100000, + amountCents: 2000, + occurredAt: "2026-06-01T02:00:00Z", + receivedAt: "2026-06-01T02:00:02Z" + } + ] +}; + +const cleanReport = analyzePacket(cleanPacket); +assert.equal(cleanReport.decision, "invoice"); +assert.equal(cleanReport.findings.length, 0); + +console.log("compute meter replay guard tests passed");