diff --git a/subscription-quota-rollover-guard/README.md b/subscription-quota-rollover-guard/README.md
new file mode 100644
index 00000000..195e9ebb
--- /dev/null
+++ b/subscription-quota-rollover-guard/README.md
@@ -0,0 +1,38 @@
+# Subscription Quota Rollover Guard
+
+This slice adds a dependency-free Revenue Infrastructure guard for subscription AI-compute or token quota rollover. It audits synthetic quota ledgers before invoice release so SCIBASE can avoid double billing, expired quota application, and unsupported carry-forward during plan changes.
+
+The scope is intentionally distinct from existing same-issue submissions for proration, renewal notice, entitlement downgrade, usage replay/idempotency, prepaid credit breakage, committed drawdown, quote approval, invoice delivery, tax, collections, analytics-seat leakage, and payment authorization freshness.
+
+## What It Checks
+
+- Carry-forward units exceeding the plan rollover cap.
+- Posted rollover totals that do not match prior-cycle unused quota.
+- Duplicate rollover lot identifiers.
+- Expired rollover lots applied to the current invoice.
+- Applied units that do not reconcile to lot-level records.
+- Plan downgrade carry limits.
+- Unapproved negative quota adjustments.
+- Overage line items that fail to subtract applied rollover.
+- Missing finance hold when blockers exist.
+
+## Reviewer Output
+
+Running the demo creates:
+
+- `reports/quota-rollover-report.json`
+- `reports/quota-rollover-report.md`
+- `reports/summary.svg`
+- `reports/demo-script.txt`
+- `reports/demo.gif`
+- `reports/demo.mp4`
+
+All inputs are synthetic. The guard uses no credentials, private customer data, payment processors, live billing systems, analytics APIs, or external services.
+
+## Commands
+
+```bash
+npm test
+npm run demo
+npm run demo:video
+```
diff --git a/subscription-quota-rollover-guard/demo.js b/subscription-quota-rollover-guard/demo.js
new file mode 100644
index 00000000..be74ac30
--- /dev/null
+++ b/subscription-quota-rollover-guard/demo.js
@@ -0,0 +1,25 @@
+import { mkdir, writeFile } from "node:fs/promises";
+import { auditQuotaRollover, buildFinanceMarkdown, buildSummarySvg } from "./index.js";
+import { riskyQuotaLedger } from "./sample-data.js";
+
+const reportsDir = new URL("./reports/", import.meta.url);
+await mkdir(reportsDir, { recursive: true });
+
+const report = auditQuotaRollover(riskyQuotaLedger);
+await writeFile(new URL("quota-rollover-report.json", reportsDir), `${JSON.stringify(report, null, 2)}\n`);
+await writeFile(new URL("quota-rollover-report.md", reportsDir), buildFinanceMarkdown(report));
+await writeFile(new URL("summary.svg", reportsDir), buildSummarySvg(report));
+await writeFile(
+ new URL("demo-script.txt", reportsDir),
+ [
+ "Demo: Subscription Quota Rollover Guard",
+ `Account: ${report.accountId}`,
+ `Billing cycle: ${report.billingCycle}`,
+ `Decision: ${report.decision}`,
+ `Risk score: ${report.riskScore}/100`,
+ `Blockers: ${report.summary.blockCount}`,
+ "Finance action: hold invoice until duplicate/expired rollover, downgrade caps, and overage units reconcile.",
+ ].join("\n"),
+);
+
+console.log(JSON.stringify(report.summary, null, 2));
diff --git a/subscription-quota-rollover-guard/demo_video.py b/subscription-quota-rollover-guard/demo_video.py
new file mode 100644
index 00000000..2d77ce71
--- /dev/null
+++ b/subscription-quota-rollover-guard/demo_video.py
@@ -0,0 +1,45 @@
+from pathlib import Path
+
+import imageio.v3 as iio
+import numpy as np
+from PIL import Image, ImageDraw, ImageFont
+
+
+ROOT = Path(__file__).resolve().parent
+REPORTS = ROOT / "reports"
+REPORTS.mkdir(exist_ok=True)
+
+
+def font(size):
+ for name in ("arial.ttf", "segoeui.ttf"):
+ try:
+ return ImageFont.truetype(name, size)
+ except OSError:
+ pass
+ return ImageFont.load_default()
+
+
+slides = [
+ ("Subscription Quota Rollover Guard", "Synthetic revenue control for SCIBASE #20"),
+ ("Decision", "hold-invoice · risk score 100/100"),
+ ("Detected", "expired lots · duplicate carry-forward · downgrade cap breach"),
+ ("Finance Action", "Hold invoice and reconcile overage before release"),
+]
+
+frames = []
+for title, subtitle in slides:
+ image = Image.new("RGB", (960, 540), "#111827")
+ draw = ImageDraw.Draw(image)
+ draw.rectangle((48, 58, 912, 482), outline="#374151", width=3)
+ draw.text((82, 132), title, fill="#f9fafb", font=font(42))
+ draw.text((82, 214), subtitle, fill="#d1d5db", font=font(24))
+ draw.rectangle((82, 340, 690, 380), fill="#dc2626")
+ draw.text((82, 410), "Synthetic data only. No payment processor or live billing calls.", fill="#9ca3af", font=font(20))
+ frames.extend([image] * 14)
+
+gif_path = REPORTS / "demo.gif"
+frames[0].save(gif_path, save_all=True, append_images=frames[1:], duration=120, loop=0)
+mp4_path = REPORTS / "demo.mp4"
+iio.imwrite(mp4_path, [np.asarray(frame) for frame in frames], fps=8, codec="libx264")
+print(f"wrote {gif_path}")
+print(f"wrote {mp4_path}")
diff --git a/subscription-quota-rollover-guard/index.js b/subscription-quota-rollover-guard/index.js
new file mode 100644
index 00000000..d23539e9
--- /dev/null
+++ b/subscription-quota-rollover-guard/index.js
@@ -0,0 +1,254 @@
+const SEVERITY_ORDER = { block: 3, warn: 2, info: 1 };
+
+function finding(code, severity, message, evidence, remediation) {
+ return { code, severity, message, evidence, remediation };
+}
+
+function cycleToNumber(cycle) {
+ const match = String(cycle || "").match(/^(\d{4})-(\d{2})$/);
+ if (!match) return Number.NaN;
+ return Number(match[1]) * 12 + Number(match[2]);
+}
+
+function sum(values) {
+ return values.reduce((total, value) => total + Number(value || 0), 0);
+}
+
+export function auditQuotaRollover(ledger) {
+ if (!ledger || typeof ledger !== "object") {
+ throw new TypeError("quota ledger must be an object");
+ }
+
+ const issues = [];
+ const plan = ledger.plan || {};
+ const prior = ledger.priorCycle || {};
+ const current = ledger.currentCycle || {};
+ const lots = ledger.rolloverLots || [];
+ const adjustments = ledger.adjustments || [];
+ const invoice = ledger.invoicePreview || {};
+
+ if (prior.carryForwardUnits > plan.rolloverCapUnits) {
+ issues.push(
+ finding(
+ "ROLLOVER_CAP_EXCEEDED",
+ "block",
+ "Prior-cycle unused quota exceeds the contractual rollover cap.",
+ {
+ carryForwardUnits: prior.carryForwardUnits,
+ rolloverCapUnits: plan.rolloverCapUnits,
+ },
+ "Clamp carry-forward units to the plan cap and regenerate the invoice preview before release.",
+ ),
+ );
+ }
+
+ if (prior.postedRolloverUnits !== prior.carryForwardUnits) {
+ issues.push(
+ finding(
+ "POSTED_ROLLOVER_MISMATCH",
+ "block",
+ "Posted rollover units do not match the calculated prior-cycle carry-forward units.",
+ {
+ postedRolloverUnits: prior.postedRolloverUnits,
+ carryForwardUnits: prior.carryForwardUnits,
+ },
+ "Reconcile the quota ledger before overage or credit lines are posted.",
+ ),
+ );
+ }
+
+ const seenLots = new Set();
+ for (const lot of lots) {
+ if (seenLots.has(lot.lotId)) {
+ issues.push(
+ finding(
+ "DUPLICATE_ROLLOVER_LOT",
+ "block",
+ "The same rollover lot appears more than once and could be carried forward twice.",
+ { lotId: lot.lotId },
+ "Deduplicate rollover lots by immutable lot id before invoice generation.",
+ ),
+ );
+ }
+ seenLots.add(lot.lotId);
+
+ if (cycleToNumber(lot.expiresAfterCycle) < cycleToNumber(ledger.billingCycle) && lot.appliedUnits > 0) {
+ issues.push(
+ finding(
+ "EXPIRED_ROLLOVER_APPLIED",
+ "block",
+ "Expired quota was applied to the current invoice.",
+ {
+ lotId: lot.lotId,
+ expiresAfterCycle: lot.expiresAfterCycle,
+ billingCycle: ledger.billingCycle,
+ appliedUnits: lot.appliedUnits,
+ },
+ "Remove expired lots from available balance and route the invoice to finance review.",
+ ),
+ );
+ }
+ }
+
+ const totalAppliedFromLots = sum(lots.map((lot) => lot.appliedUnits));
+ if (totalAppliedFromLots !== current.rolloverAppliedUnits) {
+ issues.push(
+ finding(
+ "ROLLOVER_APPLICATION_MISMATCH",
+ "block",
+ "Applied rollover units do not equal the sum of applied rollover lots.",
+ {
+ totalAppliedFromLots,
+ rolloverAppliedUnits: current.rolloverAppliedUnits,
+ },
+ "Regenerate applied quota from lot-level records instead of invoice-line totals.",
+ ),
+ );
+ }
+
+ if (current.planChangedFrom !== current.planChangedTo && current.rolloverAppliedUnits > plan.downgradeCarryLimitUnits) {
+ issues.push(
+ finding(
+ "DOWNGRADE_CARRY_LIMIT_EXCEEDED",
+ "block",
+ "A downgraded account is applying more carried quota than the downgrade policy allows.",
+ {
+ planChangedFrom: current.planChangedFrom,
+ planChangedTo: current.planChangedTo,
+ rolloverAppliedUnits: current.rolloverAppliedUnits,
+ downgradeCarryLimitUnits: plan.downgradeCarryLimitUnits,
+ },
+ "Apply the downgrade carry limit or hold the invoice for contract review.",
+ ),
+ );
+ }
+
+ const unapprovedNegativeAdjustments = adjustments.filter((adjustment) => adjustment.units < 0 && !adjustment.approvedByFinance);
+ if (unapprovedNegativeAdjustments.length > 0) {
+ issues.push(
+ finding(
+ "UNAPPROVED_NEGATIVE_ADJUSTMENT",
+ "warn",
+ "Negative quota adjustments exist without finance approval.",
+ { adjustmentIds: unapprovedNegativeAdjustments.map((adjustment) => adjustment.adjustmentId) },
+ "Require finance approval before negative units affect quota or invoice totals.",
+ ),
+ );
+ }
+
+ const expectedOverageUnits = Math.max(0, current.usageUnits - current.includedUnits - current.rolloverAppliedUnits);
+ if (invoice.lineItems?.some((line) => line.code === "ai-compute-overage") && current.invoiceOverageUnits !== expectedOverageUnits) {
+ issues.push(
+ finding(
+ "OVERAGE_DOUBLE_BILLING_RISK",
+ "block",
+ "Invoice overage units do not subtract applied rollover correctly.",
+ {
+ usageUnits: current.usageUnits,
+ includedUnits: current.includedUnits,
+ rolloverAppliedUnits: current.rolloverAppliedUnits,
+ invoiceOverageUnits: current.invoiceOverageUnits,
+ expectedOverageUnits,
+ },
+ "Recompute overage after rollover application to prevent double billing.",
+ ),
+ );
+ }
+
+ if (issues.some((issue) => issue.severity === "block") && invoice.reviewerHold !== true) {
+ issues.push(
+ finding(
+ "MISSING_FINANCE_HOLD",
+ "block",
+ "The invoice preview is not on hold despite quota rollover blockers.",
+ { reviewerHold: invoice.reviewerHold },
+ "Place the invoice on finance hold until quota carry-forward and overage lines reconcile.",
+ ),
+ );
+ }
+
+ const blockCount = issues.filter((issue) => issue.severity === "block").length;
+ const warnCount = issues.filter((issue) => issue.severity === "warn").length;
+ return {
+ accountId: ledger.accountId,
+ billingCycle: ledger.billingCycle,
+ decision: blockCount > 0 ? "hold-invoice" : warnCount > 0 ? "finance-review" : "ready-to-invoice",
+ riskScore: Math.min(100, blockCount * 24 + warnCount * 9),
+ summary: {
+ blockCount,
+ warnCount,
+ findingCount: issues.length,
+ appliedRolloverUnits: current.rolloverAppliedUnits || 0,
+ expectedOverageUnits,
+ },
+ findings: issues.sort((a, b) => SEVERITY_ORDER[b.severity] - SEVERITY_ORDER[a.severity] || a.code.localeCompare(b.code)),
+ };
+}
+
+export function buildFinanceMarkdown(report) {
+ const lines = [
+ `# Subscription Quota Rollover Guard: ${report.accountId}`,
+ "",
+ `Billing cycle: **${report.billingCycle}**`,
+ `Decision: **${report.decision}**`,
+ `Risk score: **${report.riskScore}/100**`,
+ "",
+ `Findings: ${report.summary.blockCount} blockers, ${report.summary.warnCount} warnings.`,
+ `Applied rollover units: ${report.summary.appliedRolloverUnits}`,
+ `Expected overage units: ${report.summary.expectedOverageUnits}`,
+ "",
+ ];
+
+ for (const item of report.findings) {
+ lines.push(`## ${item.severity.toUpperCase()}: ${item.code}`);
+ lines.push(item.message);
+ lines.push("");
+ lines.push(`Evidence: \`${JSON.stringify(item.evidence)}\``);
+ lines.push("");
+ lines.push(`Remediation: ${item.remediation}`);
+ lines.push("");
+ }
+
+ if (report.findings.length === 0) {
+ lines.push("No rollover blockers or warnings were detected in the synthetic ledger.");
+ lines.push("");
+ }
+
+ return lines.join("\n");
+}
+
+export function buildSummarySvg(report) {
+ const color = report.decision === "hold-invoice" ? "#dc2626" : report.decision === "finance-review" ? "#d97706" : "#16a34a";
+ return `
+`;
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """);
+}
diff --git a/subscription-quota-rollover-guard/package.json b/subscription-quota-rollover-guard/package.json
new file mode 100644
index 00000000..08ef4c7c
--- /dev/null
+++ b/subscription-quota-rollover-guard/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "subscription-quota-rollover-guard",
+ "version": "1.0.0",
+ "description": "Synthetic quota rollover audit guard for SCIBASE revenue infrastructure.",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "test": "node test.js",
+ "demo": "node demo.js",
+ "demo:video": "python demo_video.py"
+ }
+}
diff --git a/subscription-quota-rollover-guard/reports/demo-script.txt b/subscription-quota-rollover-guard/reports/demo-script.txt
new file mode 100644
index 00000000..50600235
--- /dev/null
+++ b/subscription-quota-rollover-guard/reports/demo-script.txt
@@ -0,0 +1,7 @@
+Demo: Subscription Quota Rollover Guard
+Account: LAB-QUOTA-1042
+Billing cycle: 2026-06
+Decision: hold-invoice
+Risk score: 100/100
+Blockers: 6
+Finance action: hold invoice until duplicate/expired rollover, downgrade caps, and overage units reconcile.
\ No newline at end of file
diff --git a/subscription-quota-rollover-guard/reports/demo.gif b/subscription-quota-rollover-guard/reports/demo.gif
new file mode 100644
index 00000000..1197c266
Binary files /dev/null and b/subscription-quota-rollover-guard/reports/demo.gif differ
diff --git a/subscription-quota-rollover-guard/reports/demo.mp4 b/subscription-quota-rollover-guard/reports/demo.mp4
new file mode 100644
index 00000000..d4d3e9af
Binary files /dev/null and b/subscription-quota-rollover-guard/reports/demo.mp4 differ
diff --git a/subscription-quota-rollover-guard/reports/quota-rollover-report.json b/subscription-quota-rollover-guard/reports/quota-rollover-report.json
new file mode 100644
index 00000000..8e5cf608
--- /dev/null
+++ b/subscription-quota-rollover-guard/reports/quota-rollover-report.json
@@ -0,0 +1,91 @@
+{
+ "accountId": "LAB-QUOTA-1042",
+ "billingCycle": "2026-06",
+ "decision": "hold-invoice",
+ "riskScore": 100,
+ "summary": {
+ "blockCount": 6,
+ "warnCount": 1,
+ "findingCount": 7,
+ "appliedRolloverUnits": 3900,
+ "expectedOverageUnits": 1300
+ },
+ "findings": [
+ {
+ "code": "DOWNGRADE_CARRY_LIMIT_EXCEEDED",
+ "severity": "block",
+ "message": "A downgraded account is applying more carried quota than the downgrade policy allows.",
+ "evidence": {
+ "planChangedFrom": "Lab Pro",
+ "planChangedTo": "Individual Pro",
+ "rolloverAppliedUnits": 3900,
+ "downgradeCarryLimitUnits": 1000
+ },
+ "remediation": "Apply the downgrade carry limit or hold the invoice for contract review."
+ },
+ {
+ "code": "DUPLICATE_ROLLOVER_LOT",
+ "severity": "block",
+ "message": "The same rollover lot appears more than once and could be carried forward twice.",
+ "evidence": {
+ "lotId": "duplicate-may"
+ },
+ "remediation": "Deduplicate rollover lots by immutable lot id before invoice generation."
+ },
+ {
+ "code": "EXPIRED_ROLLOVER_APPLIED",
+ "severity": "block",
+ "message": "Expired quota was applied to the current invoice.",
+ "evidence": {
+ "lotId": "legacy-feb",
+ "expiresAfterCycle": "2026-04",
+ "billingCycle": "2026-06",
+ "appliedUnits": 900
+ },
+ "remediation": "Remove expired lots from available balance and route the invoice to finance review."
+ },
+ {
+ "code": "MISSING_FINANCE_HOLD",
+ "severity": "block",
+ "message": "The invoice preview is not on hold despite quota rollover blockers.",
+ "evidence": {
+ "reviewerHold": false
+ },
+ "remediation": "Place the invoice on finance hold until quota carry-forward and overage lines reconcile."
+ },
+ {
+ "code": "OVERAGE_DOUBLE_BILLING_RISK",
+ "severity": "block",
+ "message": "Invoice overage units do not subtract applied rollover correctly.",
+ "evidence": {
+ "usageUnits": 11200,
+ "includedUnits": 6000,
+ "rolloverAppliedUnits": 3900,
+ "invoiceOverageUnits": 5200,
+ "expectedOverageUnits": 1300
+ },
+ "remediation": "Recompute overage after rollover application to prevent double billing."
+ },
+ {
+ "code": "ROLLOVER_CAP_EXCEEDED",
+ "severity": "block",
+ "message": "Prior-cycle unused quota exceeds the contractual rollover cap.",
+ "evidence": {
+ "carryForwardUnits": 3900,
+ "rolloverCapUnits": 3000
+ },
+ "remediation": "Clamp carry-forward units to the plan cap and regenerate the invoice preview before release."
+ },
+ {
+ "code": "UNAPPROVED_NEGATIVE_ADJUSTMENT",
+ "severity": "warn",
+ "message": "Negative quota adjustments exist without finance approval.",
+ "evidence": {
+ "adjustmentIds": [
+ "manual-negative-1"
+ ]
+ },
+ "remediation": "Require finance approval before negative units affect quota or invoice totals."
+ }
+ ]
+}
diff --git a/subscription-quota-rollover-guard/reports/quota-rollover-report.md b/subscription-quota-rollover-guard/reports/quota-rollover-report.md
new file mode 100644
index 00000000..5e5bc0d4
--- /dev/null
+++ b/subscription-quota-rollover-guard/reports/quota-rollover-report.md
@@ -0,0 +1,58 @@
+# Subscription Quota Rollover Guard: LAB-QUOTA-1042
+
+Billing cycle: **2026-06**
+Decision: **hold-invoice**
+Risk score: **100/100**
+
+Findings: 6 blockers, 1 warnings.
+Applied rollover units: 3900
+Expected overage units: 1300
+
+## BLOCK: DOWNGRADE_CARRY_LIMIT_EXCEEDED
+A downgraded account is applying more carried quota than the downgrade policy allows.
+
+Evidence: `{"planChangedFrom":"Lab Pro","planChangedTo":"Individual Pro","rolloverAppliedUnits":3900,"downgradeCarryLimitUnits":1000}`
+
+Remediation: Apply the downgrade carry limit or hold the invoice for contract review.
+
+## BLOCK: DUPLICATE_ROLLOVER_LOT
+The same rollover lot appears more than once and could be carried forward twice.
+
+Evidence: `{"lotId":"duplicate-may"}`
+
+Remediation: Deduplicate rollover lots by immutable lot id before invoice generation.
+
+## BLOCK: EXPIRED_ROLLOVER_APPLIED
+Expired quota was applied to the current invoice.
+
+Evidence: `{"lotId":"legacy-feb","expiresAfterCycle":"2026-04","billingCycle":"2026-06","appliedUnits":900}`
+
+Remediation: Remove expired lots from available balance and route the invoice to finance review.
+
+## BLOCK: MISSING_FINANCE_HOLD
+The invoice preview is not on hold despite quota rollover blockers.
+
+Evidence: `{"reviewerHold":false}`
+
+Remediation: Place the invoice on finance hold until quota carry-forward and overage lines reconcile.
+
+## BLOCK: OVERAGE_DOUBLE_BILLING_RISK
+Invoice overage units do not subtract applied rollover correctly.
+
+Evidence: `{"usageUnits":11200,"includedUnits":6000,"rolloverAppliedUnits":3900,"invoiceOverageUnits":5200,"expectedOverageUnits":1300}`
+
+Remediation: Recompute overage after rollover application to prevent double billing.
+
+## BLOCK: ROLLOVER_CAP_EXCEEDED
+Prior-cycle unused quota exceeds the contractual rollover cap.
+
+Evidence: `{"carryForwardUnits":3900,"rolloverCapUnits":3000}`
+
+Remediation: Clamp carry-forward units to the plan cap and regenerate the invoice preview before release.
+
+## WARN: UNAPPROVED_NEGATIVE_ADJUSTMENT
+Negative quota adjustments exist without finance approval.
+
+Evidence: `{"adjustmentIds":["manual-negative-1"]}`
+
+Remediation: Require finance approval before negative units affect quota or invoice totals.
diff --git a/subscription-quota-rollover-guard/reports/summary.svg b/subscription-quota-rollover-guard/reports/summary.svg
new file mode 100644
index 00000000..debcf9e7
--- /dev/null
+++ b/subscription-quota-rollover-guard/reports/summary.svg
@@ -0,0 +1,23 @@
+
diff --git a/subscription-quota-rollover-guard/requirements-map.md b/subscription-quota-rollover-guard/requirements-map.md
new file mode 100644
index 00000000..5e59c41d
--- /dev/null
+++ b/subscription-quota-rollover-guard/requirements-map.md
@@ -0,0 +1,29 @@
+# Requirements Map
+
+Issue #20 asks for revenue infrastructure across subscription billing, usage-based AI compute billing, real-time usage meters, quotas, top-ups, and licensing controls.
+
+This contribution covers a distinct revenue control: validating subscription quota rollover before invoice release.
+
+| Issue capability | Implementation |
+| --- | --- |
+| Tiered subscription billing | Audits included quota, rollover caps, downgrade carry limits, and plan-change behavior. |
+| Usage-based AI compute billing | Reconciles usage units, applied rollover, and expected overage units. |
+| Transparent quotas and real-time usage meters | Treats rollover lots and quota adjustments as auditable ledger records. |
+| Billing safety before invoice release | Emits `hold-invoice`, `finance-review`, or `ready-to-invoice` decisions with remediation. |
+| Reviewer-ready evidence | Generates JSON, Markdown, SVG, and GIF demo artifacts from synthetic data. |
+
+## Non-Overlap
+
+This is not:
+
+- Plan migration proration.
+- Subscription renewal notice.
+- Entitlement downgrade.
+- Usage replay or idempotency.
+- Prepaid credit breakage.
+- Committed revenue drawdown.
+- Quote approval.
+- Invoice delivery or collections.
+- Analytics-seat/API licensing.
+
+It specifically verifies whether unused monthly subscription quota is carried, expired, capped, adjusted, and applied correctly before overage charges are released.
diff --git a/subscription-quota-rollover-guard/sample-data.js b/subscription-quota-rollover-guard/sample-data.js
new file mode 100644
index 00000000..c53b61b0
--- /dev/null
+++ b/subscription-quota-rollover-guard/sample-data.js
@@ -0,0 +1,85 @@
+export const riskyQuotaLedger = {
+ accountId: "LAB-QUOTA-1042",
+ billingCycle: "2026-06",
+ plan: {
+ name: "Lab Pro",
+ includedUnits: 10000,
+ rolloverCapUnits: 3000,
+ rolloverExpiresAfterCycles: 2,
+ downgradeCarryLimitUnits: 1000,
+ },
+ priorCycle: {
+ cycle: "2026-05",
+ includedUnits: 10000,
+ usedUnits: 6100,
+ carryForwardUnits: 3900,
+ postedRolloverUnits: 3900,
+ },
+ currentCycle: {
+ includedUnits: 6000,
+ planChangedFrom: "Lab Pro",
+ planChangedTo: "Individual Pro",
+ usageUnits: 11200,
+ invoiceOverageUnits: 5200,
+ availableRolloverUnits: 3900,
+ rolloverAppliedUnits: 3900,
+ },
+ rolloverLots: [
+ { lotId: "may-unused", units: 2500, originatedCycle: "2026-05", expiresAfterCycle: "2026-07", appliedUnits: 2500 },
+ { lotId: "legacy-feb", units: 900, originatedCycle: "2026-02", expiresAfterCycle: "2026-04", appliedUnits: 900 },
+ { lotId: "duplicate-may", units: 500, originatedCycle: "2026-05", expiresAfterCycle: "2026-07", appliedUnits: 500 },
+ { lotId: "duplicate-may", units: 500, originatedCycle: "2026-05", expiresAfterCycle: "2026-07", appliedUnits: 0 },
+ ],
+ adjustments: [
+ { adjustmentId: "manual-negative-1", units: -700, approvedByFinance: false, reason: "support request" },
+ ],
+ invoicePreview: {
+ lineItems: [
+ { code: "base-plan", units: 6000, amountUsd: 499 },
+ { code: "ai-compute-overage", units: 5200, amountUsd: 416 },
+ { code: "rollover-credit", units: -3900, amountUsd: -312 },
+ ],
+ reviewerHold: false,
+ },
+};
+
+export const cleanQuotaLedger = {
+ accountId: "LAB-QUOTA-2048",
+ billingCycle: "2026-06",
+ plan: {
+ name: "Institutional",
+ includedUnits: 50000,
+ rolloverCapUnits: 12000,
+ rolloverExpiresAfterCycles: 2,
+ downgradeCarryLimitUnits: 4000,
+ },
+ priorCycle: {
+ cycle: "2026-05",
+ includedUnits: 50000,
+ usedUnits: 42000,
+ carryForwardUnits: 8000,
+ postedRolloverUnits: 8000,
+ },
+ currentCycle: {
+ includedUnits: 50000,
+ planChangedFrom: "Institutional",
+ planChangedTo: "Institutional",
+ usageUnits: 54800,
+ invoiceOverageUnits: 0,
+ availableRolloverUnits: 8000,
+ rolloverAppliedUnits: 4800,
+ },
+ rolloverLots: [
+ { lotId: "may-unused", units: 8000, originatedCycle: "2026-05", expiresAfterCycle: "2026-07", appliedUnits: 4800 },
+ ],
+ adjustments: [
+ { adjustmentId: "finance-credit-1", units: -300, approvedByFinance: true, reason: "documented SLA credit" },
+ ],
+ invoicePreview: {
+ lineItems: [
+ { code: "base-plan", units: 50000, amountUsd: 2499 },
+ { code: "rollover-credit", units: -4800, amountUsd: 0 },
+ ],
+ reviewerHold: true,
+ },
+};
diff --git a/subscription-quota-rollover-guard/test.js b/subscription-quota-rollover-guard/test.js
new file mode 100644
index 00000000..2353889e
--- /dev/null
+++ b/subscription-quota-rollover-guard/test.js
@@ -0,0 +1,30 @@
+import assert from "node:assert/strict";
+import { auditQuotaRollover, buildFinanceMarkdown, buildSummarySvg } from "./index.js";
+import { cleanQuotaLedger, riskyQuotaLedger } from "./sample-data.js";
+
+const risky = auditQuotaRollover(riskyQuotaLedger);
+assert.equal(risky.decision, "hold-invoice");
+assert.ok(risky.riskScore >= 90);
+assert.ok(risky.findings.some((finding) => finding.code === "ROLLOVER_CAP_EXCEEDED"));
+assert.ok(risky.findings.some((finding) => finding.code === "EXPIRED_ROLLOVER_APPLIED"));
+assert.ok(risky.findings.some((finding) => finding.code === "DUPLICATE_ROLLOVER_LOT"));
+assert.ok(risky.findings.some((finding) => finding.code === "DOWNGRADE_CARRY_LIMIT_EXCEEDED"));
+assert.ok(risky.findings.some((finding) => finding.code === "OVERAGE_DOUBLE_BILLING_RISK"));
+assert.ok(risky.findings.some((finding) => finding.code === "MISSING_FINANCE_HOLD"));
+
+const clean = auditQuotaRollover(cleanQuotaLedger);
+assert.equal(clean.decision, "ready-to-invoice");
+assert.equal(clean.riskScore, 0);
+assert.equal(clean.summary.findingCount, 0);
+
+assert.throws(() => auditQuotaRollover(undefined), /quota ledger must be an object/);
+
+const markdown = buildFinanceMarkdown(risky);
+assert.match(markdown, /hold-invoice/);
+assert.match(markdown, /ROLLOVER_CAP_EXCEEDED/);
+
+const svg = buildSummarySvg(risky);
+assert.match(svg, /