From 3247bc9f68c134bb62078e3937f40d93e27729a1 Mon Sep 17 00:00:00 2001
From: shaiananvari8 <228813044+shaiananvari8@users.noreply.github.com>
Date: Wed, 3 Jun 2026 22:43:43 -0500
Subject: [PATCH] Add revenue recognition deferral guard
---
.../package.json | 20 +
revenue-recognition-deferral-guard/readme.md | 38 ++
.../reports/revenue-recognition-packet.json | 141 ++++++
.../reports/revenue-recognition-report.md | 38 ++
.../reports/summary.svg | 18 +
.../scripts/demo.js | 29 ++
.../src/index.js | 457 ++++++++++++++++++
.../test/index.test.js | 98 ++++
8 files changed, 839 insertions(+)
create mode 100644 revenue-recognition-deferral-guard/package.json
create mode 100644 revenue-recognition-deferral-guard/readme.md
create mode 100644 revenue-recognition-deferral-guard/reports/revenue-recognition-packet.json
create mode 100644 revenue-recognition-deferral-guard/reports/revenue-recognition-report.md
create mode 100644 revenue-recognition-deferral-guard/reports/summary.svg
create mode 100644 revenue-recognition-deferral-guard/scripts/demo.js
create mode 100644 revenue-recognition-deferral-guard/src/index.js
create mode 100644 revenue-recognition-deferral-guard/test/index.test.js
diff --git a/revenue-recognition-deferral-guard/package.json b/revenue-recognition-deferral-guard/package.json
new file mode 100644
index 00000000..033713f4
--- /dev/null
+++ b/revenue-recognition-deferral-guard/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "revenue-recognition-deferral-guard",
+ "version": "1.0.0",
+ "description": "Synthetic deferred revenue and recognition guard for SCIBASE revenue infrastructure.",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "check": "node --check src/index.js && node --check test/index.test.js && node --check scripts/demo.js",
+ "test": "node --test test/index.test.js",
+ "demo": "node scripts/demo.js"
+ },
+ "keywords": [
+ "scibase",
+ "revenue",
+ "deferred-revenue",
+ "recognition",
+ "finance-controls"
+ ],
+ "license": "MIT"
+}
diff --git a/revenue-recognition-deferral-guard/readme.md b/revenue-recognition-deferral-guard/readme.md
new file mode 100644
index 00000000..6003d923
--- /dev/null
+++ b/revenue-recognition-deferral-guard/readme.md
@@ -0,0 +1,38 @@
+# Revenue Recognition Deferral Guard
+
+This module is a focused SCIBASE.AI issue #20 submission for Revenue Infrastructure.
+
+It validates synthetic finance-close packets before revenue is accepted as recognized. The guard checks annual subscription service periods, AI compute credit consumption, licensing/export delivery evidence, and allocation reconciliation so unearned amounts stay in deferred revenue until delivery is supported.
+
+## What It Covers
+
+- Performance obligation allocation reconciliation against invoice totals.
+- Subscription revenue recognition based on elapsed service period.
+- AI compute-credit revenue recognition only after consumption or expiry.
+- Licensing/export revenue requiring delivery evidence before close.
+- Deterministic finance remediation actions for held obligations.
+
+## What It Does Not Do
+
+- No processor, financial-account, tax-filing, invoice-send, refund-send, or external service calls.
+- No personal billing data, cards, account numbers, payout details, or real customer records.
+- No live SCIBASE revenue is read or mutated.
+- No refund-liability, cancellation, priority-support, quote approval, or named-seat roster module is reimplemented.
+
+## Reviewer Path
+
+```bash
+npm run check
+npm test
+npm run demo
+```
+
+Generated reviewer artifacts:
+
+- `reports/revenue-recognition-packet.json`
+- `reports/revenue-recognition-report.md`
+- `reports/summary.svg`
+
+## Claim
+
+Use `/claim #20` in the pull request body.
diff --git a/revenue-recognition-deferral-guard/reports/revenue-recognition-packet.json b/revenue-recognition-deferral-guard/reports/revenue-recognition-packet.json
new file mode 100644
index 00000000..c7420c7e
--- /dev/null
+++ b/revenue-recognition-deferral-guard/reports/revenue-recognition-packet.json
@@ -0,0 +1,141 @@
+{
+ "title": "SCIBASE Revenue Recognition Deferral Guard",
+ "issue": "SCIBASE.AI#20",
+ "claim": "/claim #20",
+ "purpose": "Prevent revenue close packets from recognizing subscription, compute-credit, or licensing revenue before delivery evidence exists.",
+ "evaluation": {
+ "status": "block_close",
+ "asOf": "2026-06-04T00:00:00.000Z",
+ "packetId": "scibase-revenue-recognition-close-2026-06",
+ "digest": "8b6865927e5e430b33924f2853f0d9c0e7c005bb6002eabd6250d82d57c0ebe2",
+ "totals": {
+ "contracts": 2,
+ "obligations": 3,
+ "acceptedRecognizedCents": 0,
+ "allowedRecognizedCents": 908800,
+ "deferredCents": 2391200,
+ "blockers": 6,
+ "warnings": 0,
+ "heldObligations": 3
+ },
+ "blockers": [
+ {
+ "code": "premature_revenue_recognition",
+ "contractId": "contract:lab-annual-pro",
+ "obligationId": "obl:annual-subscription",
+ "recognizedCents": 1800000,
+ "allowedRecognizedCents": 759452,
+ "message": "Revenue is recognized before the obligation is sufficiently delivered."
+ },
+ {
+ "code": "premature_revenue_recognition",
+ "contractId": "contract:lab-annual-pro",
+ "obligationId": "obl:compute-credits",
+ "recognizedCents": 450000,
+ "allowedRecognizedCents": 120000,
+ "message": "Revenue is recognized before the obligation is sufficiently delivered."
+ },
+ {
+ "code": "usage_credits_recognized_before_consumption",
+ "contractId": "contract:lab-annual-pro",
+ "obligationId": "obl:compute-credits",
+ "consumedCents": 120000,
+ "recognizedCents": 450000,
+ "message": "Usage credit revenue exceeds consumed or expired value."
+ },
+ {
+ "code": "missing_recognition_evidence",
+ "contractId": "contract:lab-annual-pro",
+ "obligationId": "obl:compute-credits",
+ "message": "Recognized revenue lacks required delivery evidence."
+ },
+ {
+ "code": "premature_revenue_recognition",
+ "contractId": "contract:consortium-license",
+ "obligationId": "obl:analytics-license-export",
+ "recognizedCents": 900000,
+ "allowedRecognizedCents": 29348,
+ "message": "Revenue is recognized before the obligation is sufficiently delivered."
+ },
+ {
+ "code": "missing_recognition_evidence",
+ "contractId": "contract:consortium-license",
+ "obligationId": "obl:analytics-license-export",
+ "message": "Recognized revenue lacks required delivery evidence."
+ }
+ ],
+ "warnings": [],
+ "actions": [],
+ "obligationActions": [
+ {
+ "contractId": "contract:lab-annual-pro",
+ "obligationId": "obl:annual-subscription",
+ "kind": "subscription",
+ "decision": "hold",
+ "allocatedCents": 1800000,
+ "recognizedCents": 1800000,
+ "allowedRecognizedCents": 759452,
+ "deferredCents": 1040548,
+ "reasons": [
+ "premature_recognition"
+ ],
+ "requiredActions": [
+ "Move unearned revenue into deferred revenue until service is delivered."
+ ]
+ },
+ {
+ "contractId": "contract:lab-annual-pro",
+ "obligationId": "obl:compute-credits",
+ "kind": "usage_credit",
+ "decision": "hold",
+ "allocatedCents": 600000,
+ "recognizedCents": 450000,
+ "allowedRecognizedCents": 120000,
+ "deferredCents": 480000,
+ "reasons": [
+ "premature_recognition",
+ "unconsumed_usage_credits",
+ "missing_recognition_evidence"
+ ],
+ "requiredActions": [
+ "Move unearned revenue into deferred revenue until service is delivered.",
+ "Recognize compute-credit revenue only as credits are consumed or expire.",
+ "Attach delivery, consumption, or export evidence for the recognized amount."
+ ]
+ },
+ {
+ "contractId": "contract:consortium-license",
+ "obligationId": "obl:analytics-license-export",
+ "kind": "license_export",
+ "decision": "hold",
+ "allocatedCents": 900000,
+ "recognizedCents": 900000,
+ "allowedRecognizedCents": 29348,
+ "deferredCents": 870652,
+ "reasons": [
+ "premature_recognition",
+ "missing_recognition_evidence"
+ ],
+ "requiredActions": [
+ "Move unearned revenue into deferred revenue until service is delivered.",
+ "Attach delivery, consumption, or export evidence for the recognized amount."
+ ]
+ }
+ ],
+ "policy": {
+ "maxRoundingDriftCents": 2,
+ "requireEvidenceForRecognition": true,
+ "blockFutureServiceRecognition": true,
+ "blockUnconsumedUsageCredits": true,
+ "requireContractWindow": true,
+ "requireAllocationMatchesInvoice": true
+ }
+ },
+ "reviewerChecklist": [
+ "Invoice amount reconciles to performance obligation allocations.",
+ "Subscription revenue is deferred until the service period is earned.",
+ "Compute-credit revenue is recognized only on consumption or expiry.",
+ "Licensing/export revenue has delivery evidence before close.",
+ "Held obligations include deterministic remediation actions."
+ ]
+}
diff --git a/revenue-recognition-deferral-guard/reports/revenue-recognition-report.md b/revenue-recognition-deferral-guard/reports/revenue-recognition-report.md
new file mode 100644
index 00000000..ff665d6d
--- /dev/null
+++ b/revenue-recognition-deferral-guard/reports/revenue-recognition-report.md
@@ -0,0 +1,38 @@
+# Revenue Recognition Deferral Guard Report
+
+Issue: SCIBASE.AI#20
+Claim marker: `/claim #20`
+Status: `block_close`
+Digest: `8b6865927e5e430b33924f2853f0d9c0e7c005bb6002eabd6250d82d57c0ebe2`
+
+## Reviewer Checklist
+- Invoice amount reconciles to performance obligation allocations.
+- Subscription revenue is deferred until the service period is earned.
+- Compute-credit revenue is recognized only on consumption or expiry.
+- Licensing/export revenue has delivery evidence before close.
+- Held obligations include deterministic remediation actions.
+
+## Totals
+- Contracts inspected: 2
+- Obligations inspected: 3
+- Allowed recognized revenue: $9088.00
+- Deferred revenue: $23912.00
+- Held obligations: 3
+- Blockers: 6
+- Warnings: 0
+
+## Blockers
+- premature_revenue_recognition: Revenue is recognized before the obligation is sufficiently delivered.
+- premature_revenue_recognition: Revenue is recognized before the obligation is sufficiently delivered.
+- usage_credits_recognized_before_consumption: Usage credit revenue exceeds consumed or expired value.
+- missing_recognition_evidence: Recognized revenue lacks required delivery evidence.
+- premature_revenue_recognition: Revenue is recognized before the obligation is sufficiently delivered.
+- missing_recognition_evidence: Recognized revenue lacks required delivery evidence.
+
+## Required Actions
+- No finance actions required.
+
+## Obligation Decisions
+- contract:lab-annual-pro/obl:annual-subscription: hold; recognized $18000.00, allowed $7594.52, deferred $10405.48
+- contract:lab-annual-pro/obl:compute-credits: hold; recognized $4500.00, allowed $1200.00, deferred $4800.00
+- contract:consortium-license/obl:analytics-license-export: hold; recognized $9000.00, allowed $293.48, deferred $8706.52
diff --git a/revenue-recognition-deferral-guard/reports/summary.svg b/revenue-recognition-deferral-guard/reports/summary.svg
new file mode 100644
index 00000000..cfb01e17
--- /dev/null
+++ b/revenue-recognition-deferral-guard/reports/summary.svg
@@ -0,0 +1,18 @@
+
diff --git a/revenue-recognition-deferral-guard/scripts/demo.js b/revenue-recognition-deferral-guard/scripts/demo.js
new file mode 100644
index 00000000..1684ef27
--- /dev/null
+++ b/revenue-recognition-deferral-guard/scripts/demo.js
@@ -0,0 +1,29 @@
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import {
+ buildReviewerPacket,
+ demoPacket,
+ renderMarkdownReport,
+ renderSvgSummary
+} from "../src/index.js";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const moduleRoot = path.resolve(__dirname, "..");
+const reportsDir = path.join(moduleRoot, "reports");
+const packet = demoPacket();
+const reviewerPacket = buildReviewerPacket(packet, { asOf: packet.asOf });
+
+fs.mkdirSync(reportsDir, { recursive: true });
+fs.writeFileSync(path.join(reportsDir, "revenue-recognition-packet.json"), `${JSON.stringify(reviewerPacket, null, 2)}\n`);
+fs.writeFileSync(path.join(reportsDir, "revenue-recognition-report.md"), renderMarkdownReport(packet, { asOf: packet.asOf }));
+fs.writeFileSync(path.join(reportsDir, "summary.svg"), renderSvgSummary(packet, { asOf: packet.asOf }));
+
+console.log(JSON.stringify({
+ status: reviewerPacket.evaluation.status,
+ digest: reviewerPacket.evaluation.digest,
+ blockers: reviewerPacket.evaluation.totals.blockers,
+ deferredCents: reviewerPacket.evaluation.totals.deferredCents,
+ reportsDir
+}, null, 2));
diff --git a/revenue-recognition-deferral-guard/src/index.js b/revenue-recognition-deferral-guard/src/index.js
new file mode 100644
index 00000000..116f9ada
--- /dev/null
+++ b/revenue-recognition-deferral-guard/src/index.js
@@ -0,0 +1,457 @@
+import crypto from "node:crypto";
+
+const DEFAULT_POLICY = {
+ maxRoundingDriftCents: 2,
+ requireEvidenceForRecognition: true,
+ blockFutureServiceRecognition: true,
+ blockUnconsumedUsageCredits: true,
+ requireContractWindow: true,
+ requireAllocationMatchesInvoice: true
+};
+
+export function evaluateRevenueRecognitionPacket(packet, options = {}) {
+ const normalized = normalizePacket(packet);
+ const policy = { ...DEFAULT_POLICY, ...normalized.policy, ...(options.policy ?? {}) };
+ const asOf = new Date(options.asOf ?? normalized.asOf);
+ const blockers = [];
+ const warnings = [];
+ const actions = [];
+ const obligationActions = [];
+
+ for (const contract of normalized.contracts) {
+ const allocationSum = contract.obligations.reduce((sum, obligation) => sum + obligation.allocatedCents, 0);
+ const allocationDrift = Math.abs(allocationSum - contract.invoiceCents);
+ if (policy.requireAllocationMatchesInvoice && allocationDrift > policy.maxRoundingDriftCents) {
+ blockers.push({
+ code: "allocation_does_not_match_invoice",
+ contractId: contract.id,
+ invoiceCents: contract.invoiceCents,
+ allocatedCents: allocationSum,
+ driftCents: allocationDrift,
+ message: "Performance obligation allocations do not reconcile with the invoice amount."
+ });
+ actions.push({
+ type: "reconcile_allocation",
+ contractId: contract.id,
+ reason: "Revenue cannot be recognized until invoice and obligation allocation agree."
+ });
+ }
+
+ for (const obligation of contract.obligations) {
+ const recognition = recognizeObligation(contract, obligation, normalized.usageEvents, asOf);
+ const recognizedTooEarly = recognition.recognizedCents > recognition.allowedRecognizedCents + policy.maxRoundingDriftCents;
+ const missingEvidence =
+ policy.requireEvidenceForRecognition && recognition.recognizedCents > 0 && recognition.evidenceState !== "complete";
+ const futureService =
+ policy.blockFutureServiceRecognition && obligation.kind === "subscription" && recognition.serviceProgressRatio < 1;
+ const unconsumedCredits =
+ policy.blockUnconsumedUsageCredits &&
+ obligation.kind === "usage_credit" &&
+ recognition.allowedRecognizedCents + policy.maxRoundingDriftCents < recognition.recognizedCents;
+
+ const action = {
+ contractId: contract.id,
+ obligationId: obligation.id,
+ kind: obligation.kind,
+ decision: "accept",
+ allocatedCents: obligation.allocatedCents,
+ recognizedCents: recognition.recognizedCents,
+ allowedRecognizedCents: recognition.allowedRecognizedCents,
+ deferredCents: Math.max(0, obligation.allocatedCents - recognition.allowedRecognizedCents),
+ reasons: [],
+ requiredActions: []
+ };
+
+ if (policy.requireContractWindow && !obligation.startDate && obligation.kind !== "usage_credit") {
+ action.decision = "hold";
+ action.reasons.push("missing_service_window");
+ action.requiredActions.push("Add a service start and end date before recognition.");
+ blockers.push({
+ code: "missing_service_window",
+ contractId: contract.id,
+ obligationId: obligation.id,
+ message: "Non-usage obligations need an explicit service period."
+ });
+ }
+
+ if (recognizedTooEarly || futureService) {
+ action.decision = "hold";
+ action.reasons.push("premature_recognition");
+ action.requiredActions.push("Move unearned revenue into deferred revenue until service is delivered.");
+ blockers.push({
+ code: "premature_revenue_recognition",
+ contractId: contract.id,
+ obligationId: obligation.id,
+ recognizedCents: recognition.recognizedCents,
+ allowedRecognizedCents: recognition.allowedRecognizedCents,
+ message: "Revenue is recognized before the obligation is sufficiently delivered."
+ });
+ }
+
+ if (unconsumedCredits) {
+ action.decision = "hold";
+ action.reasons.push("unconsumed_usage_credits");
+ action.requiredActions.push("Recognize compute-credit revenue only as credits are consumed or expire.");
+ blockers.push({
+ code: "usage_credits_recognized_before_consumption",
+ contractId: contract.id,
+ obligationId: obligation.id,
+ consumedCents: recognition.consumedCents,
+ recognizedCents: recognition.recognizedCents,
+ message: "Usage credit revenue exceeds consumed or expired value."
+ });
+ }
+
+ if (missingEvidence) {
+ action.decision = "hold";
+ action.reasons.push("missing_recognition_evidence");
+ action.requiredActions.push("Attach delivery, consumption, or export evidence for the recognized amount.");
+ blockers.push({
+ code: "missing_recognition_evidence",
+ contractId: contract.id,
+ obligationId: obligation.id,
+ message: "Recognized revenue lacks required delivery evidence."
+ });
+ }
+
+ if (recognition.roundingDriftCents > policy.maxRoundingDriftCents) {
+ warnings.push({
+ code: "recognition_rounding_drift",
+ contractId: contract.id,
+ obligationId: obligation.id,
+ driftCents: recognition.roundingDriftCents,
+ message: "Recognition schedule has rounding drift outside policy tolerance."
+ });
+ }
+
+ obligationActions.push(action);
+ }
+ }
+
+ const acceptedRecognizedCents = obligationActions
+ .filter((action) => action.decision === "accept")
+ .reduce((sum, action) => sum + action.recognizedCents, 0);
+ const allowedRecognizedCents = obligationActions.reduce((sum, action) => sum + action.allowedRecognizedCents, 0);
+ const deferredCents = obligationActions.reduce((sum, action) => sum + action.deferredCents, 0);
+ const status = blockers.length ? "block_close" : warnings.length ? "needs_finance_review" : "close_ready";
+
+ return {
+ status,
+ asOf: asOf.toISOString(),
+ packetId: normalized.packetId,
+ digest: digest({ packetId: normalized.packetId, blockers, warnings, obligationActions }),
+ totals: {
+ contracts: normalized.contracts.length,
+ obligations: obligationActions.length,
+ acceptedRecognizedCents,
+ allowedRecognizedCents,
+ deferredCents,
+ blockers: blockers.length,
+ warnings: warnings.length,
+ heldObligations: obligationActions.filter((action) => action.decision === "hold").length
+ },
+ blockers,
+ warnings,
+ actions,
+ obligationActions,
+ policy
+ };
+}
+
+export function recognizeObligation(contract, obligation, usageEvents, asOf) {
+ const recognizedCents = obligation.recognizedCents;
+ const evidenceState = obligation.evidence?.state ?? "missing";
+ let allowedRecognizedCents = 0;
+ let consumedCents = 0;
+ let serviceProgressRatio = 0;
+
+ if (obligation.kind === "subscription" || obligation.kind === "license_export") {
+ serviceProgressRatio = progressBetween(obligation.startDate, obligation.endDate, asOf);
+ allowedRecognizedCents = Math.round(obligation.allocatedCents * serviceProgressRatio);
+ } else if (obligation.kind === "usage_credit") {
+ consumedCents = usageEvents
+ .filter((event) => event.contractId === contract.id && event.obligationId === obligation.id)
+ .filter((event) => new Date(event.occurredAt) <= asOf)
+ .reduce((sum, event) => sum + event.consumedCents, 0);
+ const expired = obligation.expiresAt && new Date(obligation.expiresAt) <= asOf;
+ allowedRecognizedCents = expired ? obligation.allocatedCents : Math.min(obligation.allocatedCents, consumedCents);
+ serviceProgressRatio = obligation.allocatedCents ? allowedRecognizedCents / obligation.allocatedCents : 0;
+ } else {
+ throw new Error(`Unsupported obligation kind: ${obligation.kind}`);
+ }
+
+ return {
+ recognizedCents,
+ allowedRecognizedCents,
+ consumedCents,
+ serviceProgressRatio,
+ evidenceState,
+ roundingDriftCents: Math.abs(Math.round(allowedRecognizedCents) - allowedRecognizedCents)
+ };
+}
+
+export function buildReviewerPacket(packet, options = {}) {
+ const evaluation = evaluateRevenueRecognitionPacket(packet, options);
+ return {
+ title: "SCIBASE Revenue Recognition Deferral Guard",
+ issue: "SCIBASE.AI#20",
+ claim: "/claim #20",
+ purpose:
+ "Prevent revenue close packets from recognizing subscription, compute-credit, or licensing revenue before delivery evidence exists.",
+ evaluation,
+ reviewerChecklist: [
+ "Invoice amount reconciles to performance obligation allocations.",
+ "Subscription revenue is deferred until the service period is earned.",
+ "Compute-credit revenue is recognized only on consumption or expiry.",
+ "Licensing/export revenue has delivery evidence before close.",
+ "Held obligations include deterministic remediation actions."
+ ]
+ };
+}
+
+export function renderMarkdownReport(packet, options = {}) {
+ const review = buildReviewerPacket(packet, options);
+ const { evaluation } = review;
+ const lines = [
+ "# Revenue Recognition Deferral Guard Report",
+ "",
+ `Issue: ${review.issue}`,
+ `Claim marker: \`${review.claim}\``,
+ `Status: \`${evaluation.status}\``,
+ `Digest: \`${evaluation.digest}\``,
+ "",
+ "## Reviewer Checklist",
+ ...review.reviewerChecklist.map((item) => `- ${item}`),
+ "",
+ "## Totals",
+ `- Contracts inspected: ${evaluation.totals.contracts}`,
+ `- Obligations inspected: ${evaluation.totals.obligations}`,
+ `- Allowed recognized revenue: ${formatCents(evaluation.totals.allowedRecognizedCents)}`,
+ `- Deferred revenue: ${formatCents(evaluation.totals.deferredCents)}`,
+ `- Held obligations: ${evaluation.totals.heldObligations}`,
+ `- Blockers: ${evaluation.totals.blockers}`,
+ `- Warnings: ${evaluation.totals.warnings}`,
+ "",
+ "## Blockers",
+ ...formatFindings(evaluation.blockers),
+ "",
+ "## Required Actions",
+ ...(evaluation.actions.length
+ ? evaluation.actions.map((action) => `- ${action.type}: ${action.reason}`)
+ : ["- No finance actions required."]),
+ "",
+ "## Obligation Decisions",
+ ...evaluation.obligationActions.map(
+ (action) =>
+ `- ${action.contractId}/${action.obligationId}: ${action.decision}; recognized ${formatCents(action.recognizedCents)}, allowed ${formatCents(action.allowedRecognizedCents)}, deferred ${formatCents(action.deferredCents)}`
+ )
+ ];
+ return `${lines.join("\n")}\n`;
+}
+
+export function renderSvgSummary(packet, options = {}) {
+ const review = buildReviewerPacket(packet, options);
+ const { evaluation } = review;
+ const max = Math.max(
+ evaluation.totals.allowedRecognizedCents,
+ evaluation.totals.deferredCents,
+ evaluation.totals.heldObligations * 100_000,
+ 1
+ );
+ return `
+`;
+}
+
+export function demoPacket() {
+ return {
+ packetId: "scibase-revenue-recognition-close-2026-06",
+ asOf: "2026-06-04T00:00:00.000Z",
+ policy: {
+ maxRoundingDriftCents: 2
+ },
+ contracts: [
+ {
+ id: "contract:lab-annual-pro",
+ customerType: "lab",
+ invoiceCents: 2400000,
+ currency: "USD",
+ obligations: [
+ {
+ id: "obl:annual-subscription",
+ kind: "subscription",
+ allocatedCents: 1800000,
+ recognizedCents: 1800000,
+ startDate: "2026-01-01T00:00:00.000Z",
+ endDate: "2026-12-31T23:59:59.000Z",
+ evidence: { state: "complete", reference: "synthetic-service-ledger" }
+ },
+ {
+ id: "obl:compute-credits",
+ kind: "usage_credit",
+ allocatedCents: 600000,
+ recognizedCents: 450000,
+ expiresAt: "2026-12-31T23:59:59.000Z",
+ evidence: { state: "partial", reference: "synthetic-meter-events" }
+ }
+ ]
+ },
+ {
+ id: "contract:consortium-license",
+ customerType: "institution",
+ invoiceCents: 900000,
+ currency: "USD",
+ obligations: [
+ {
+ id: "obl:analytics-license-export",
+ kind: "license_export",
+ allocatedCents: 900000,
+ recognizedCents: 900000,
+ startDate: "2026-06-01T00:00:00.000Z",
+ endDate: "2026-08-31T23:59:59.000Z",
+ evidence: { state: "missing", reference: null }
+ }
+ ]
+ }
+ ],
+ usageEvents: [
+ {
+ contractId: "contract:lab-annual-pro",
+ obligationId: "obl:compute-credits",
+ occurredAt: "2026-05-20T00:00:00.000Z",
+ consumedCents: 120000,
+ source: "synthetic-ai-meter"
+ }
+ ]
+ };
+}
+
+export function normalizePacket(packet) {
+ if (!packet || typeof packet !== "object") {
+ throw new TypeError("A revenue recognition packet object is required.");
+ }
+ return {
+ packetId: nonEmpty(packet.packetId, "packetId"),
+ asOf: nonEmpty(packet.asOf, "asOf"),
+ policy: packet.policy ?? {},
+ contracts: ensureArray(packet.contracts, "contracts").map(normalizeContract),
+ usageEvents: ensureArray(packet.usageEvents ?? [], "usageEvents").map(normalizeUsageEvent)
+ };
+}
+
+function normalizeContract(contract) {
+ const invoiceCents = cents(contract.invoiceCents, "contract.invoiceCents");
+ if (invoiceCents <= 0) {
+ throw new Error("contract.invoiceCents must be positive.");
+ }
+ return {
+ id: nonEmpty(contract.id, "contract.id"),
+ customerType: contract.customerType ?? "unknown",
+ invoiceCents,
+ currency: contract.currency ?? "USD",
+ obligations: ensureArray(contract.obligations, "contract.obligations").map(normalizeObligation)
+ };
+}
+
+function normalizeObligation(obligation) {
+ return {
+ id: nonEmpty(obligation.id, "obligation.id"),
+ kind: nonEmpty(obligation.kind, "obligation.kind"),
+ allocatedCents: cents(obligation.allocatedCents, "obligation.allocatedCents"),
+ recognizedCents: cents(obligation.recognizedCents ?? 0, "obligation.recognizedCents"),
+ startDate: obligation.startDate,
+ endDate: obligation.endDate,
+ expiresAt: obligation.expiresAt,
+ evidence: obligation.evidence ?? { state: "missing" }
+ };
+}
+
+function normalizeUsageEvent(event) {
+ return {
+ contractId: nonEmpty(event.contractId, "usageEvent.contractId"),
+ obligationId: nonEmpty(event.obligationId, "usageEvent.obligationId"),
+ occurredAt: nonEmpty(event.occurredAt, "usageEvent.occurredAt"),
+ consumedCents: cents(event.consumedCents, "usageEvent.consumedCents"),
+ source: event.source ?? "synthetic"
+ };
+}
+
+function progressBetween(startDate, endDate, asOf) {
+ if (!startDate || !endDate) {
+ return 0;
+ }
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+ if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || end <= start) {
+ return 0;
+ }
+ if (asOf <= start) {
+ return 0;
+ }
+ if (asOf >= end) {
+ return 1;
+ }
+ return (asOf.getTime() - start.getTime()) / (end.getTime() - start.getTime());
+}
+
+function cents(value, name) {
+ const amount = Number(value);
+ if (!Number.isInteger(amount) || amount < 0) {
+ throw new TypeError(`${name} must be a non-negative integer cent amount.`);
+ }
+ return amount;
+}
+
+function ensureArray(value, name) {
+ if (!Array.isArray(value)) {
+ throw new TypeError(`${name} must be an array.`);
+ }
+ return value;
+}
+
+function nonEmpty(value, name) {
+ if (typeof value !== "string" || !value.trim()) {
+ throw new TypeError(`${name} must be a non-empty string.`);
+ }
+ return value.trim();
+}
+
+function digest(value) {
+ return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
+}
+
+function formatFindings(findings) {
+ if (!findings.length) {
+ return ["- None."];
+ }
+ return findings.map((finding) => `- ${finding.code}: ${finding.message}`);
+}
+
+function formatCents(centsValue) {
+ return `$${(centsValue / 100).toFixed(2)}`;
+}
+
+function svgMetric(label, displayValue, value, max, x, y, color) {
+ const width = Math.max(10, Math.round((value / max) * 340));
+ return `${escapeXml(label)}: ${escapeXml(displayValue)}
+
+ `;
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll("\"", """);
+}
diff --git a/revenue-recognition-deferral-guard/test/index.test.js b/revenue-recognition-deferral-guard/test/index.test.js
new file mode 100644
index 00000000..a830d60f
--- /dev/null
+++ b/revenue-recognition-deferral-guard/test/index.test.js
@@ -0,0 +1,98 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+
+import {
+ buildReviewerPacket,
+ demoPacket,
+ evaluateRevenueRecognitionPacket,
+ normalizePacket,
+ recognizeObligation,
+ renderMarkdownReport,
+ renderSvgSummary
+} from "../src/index.js";
+
+test("blocks premature subscription revenue recognition", () => {
+ const result = evaluateRevenueRecognitionPacket(demoPacket(), {
+ asOf: "2026-06-04T00:00:00.000Z"
+ });
+
+ assert.equal(result.status, "block_close");
+ assert.ok(result.blockers.some((blocker) => blocker.code === "premature_revenue_recognition"));
+ assert.ok(
+ result.obligationActions.some(
+ (action) => action.obligationId === "obl:annual-subscription" && action.decision === "hold"
+ )
+ );
+});
+
+test("blocks unconsumed compute credits", () => {
+ const result = evaluateRevenueRecognitionPacket(demoPacket(), {
+ asOf: "2026-06-04T00:00:00.000Z"
+ });
+
+ assert.ok(result.blockers.some((blocker) => blocker.code === "usage_credits_recognized_before_consumption"));
+ const usageAction = result.obligationActions.find((action) => action.obligationId === "obl:compute-credits");
+ assert.equal(usageAction.decision, "hold");
+ assert.equal(usageAction.allowedRecognizedCents, 120000);
+});
+
+test("blocks missing delivery evidence for recognized license revenue", () => {
+ const result = evaluateRevenueRecognitionPacket(demoPacket(), {
+ asOf: "2026-06-04T00:00:00.000Z"
+ });
+
+ assert.ok(result.blockers.some((blocker) => blocker.code === "missing_recognition_evidence"));
+ assert.ok(
+ result.obligationActions.some(
+ (action) => action.obligationId === "obl:analytics-license-export" && action.reasons.includes("missing_recognition_evidence")
+ )
+ );
+});
+
+test("accepts a clean fully delivered synthetic close packet", () => {
+ const packet = demoPacket();
+ packet.asOf = "2027-01-02T00:00:00.000Z";
+ packet.contracts[0].obligations[0].recognizedCents = 1800000;
+ packet.contracts[0].obligations[1].recognizedCents = 600000;
+ packet.contracts[0].obligations[1].expiresAt = "2026-12-31T23:59:59.000Z";
+ packet.contracts[0].obligations[1].evidence = { state: "complete", reference: "synthetic-expiry-ledger" };
+ packet.contracts[1].obligations[0].evidence = { state: "complete", reference: "synthetic-export-receipt" };
+
+ const result = evaluateRevenueRecognitionPacket(packet, {
+ asOf: "2027-01-02T00:00:00.000Z"
+ });
+
+ assert.equal(result.status, "close_ready");
+ assert.equal(result.totals.blockers, 0);
+ assert.equal(result.totals.heldObligations, 0);
+});
+
+test("normalizes packets and rejects bad cent amounts", () => {
+ assert.equal(normalizePacket(demoPacket()).contracts.length, 2);
+ const broken = demoPacket();
+ broken.contracts[0].invoiceCents = 23.4;
+ assert.throws(() => normalizePacket(broken), /integer cent amount/);
+});
+
+test("recognizes subscription progress by service window", () => {
+ const packet = normalizePacket(demoPacket());
+ const contract = packet.contracts[0];
+ const obligation = contract.obligations[0];
+ const recognition = recognizeObligation(contract, obligation, packet.usageEvents, new Date("2026-07-01T00:00:00.000Z"));
+
+ assert.ok(recognition.serviceProgressRatio > 0.49);
+ assert.ok(recognition.allowedRecognizedCents < obligation.allocatedCents);
+});
+
+test("produces reviewer artifacts with claim marker", () => {
+ const packet = demoPacket();
+ const review = buildReviewerPacket(packet, { asOf: packet.asOf });
+ const markdown = renderMarkdownReport(packet, { asOf: packet.asOf });
+ const svg = renderSvgSummary(packet, { asOf: packet.asOf });
+
+ assert.equal(review.claim, "/claim #20");
+ assert.match(markdown, /Revenue Recognition Deferral Guard Report/);
+ assert.match(markdown, /`\/claim #20`/);
+ assert.match(svg, /