From 446da8667b6da9ae1b0c56741c42beae5f101121 Mon Sep 17 00:00:00 2001
From: AlonePenguin <187998801+AlonePenguin@users.noreply.github.com>
Date: Mon, 1 Jun 2026 07:45:29 -0400
Subject: [PATCH] Add analytics data product freshness guard
---
.../README.md | 37 ++
.../demo.js | 52 +++
.../index.js | 377 ++++++++++++++++++
.../make-demo-video.js | 94 +++++
.../package.json | 21 +
.../reports/clean-freshness-packet.json | 109 +++++
.../reports/demo-script.txt | 12 +
.../reports/demo.mp4 | Bin 0 -> 9234 bytes
.../reports/freshness-review-report.md | 30 ++
.../reports/risky-freshness-packet.json | 235 +++++++++++
.../reports/summary.svg | 16 +
.../sample-data.js | 131 ++++++
.../test.js | 46 +++
13 files changed, 1160 insertions(+)
create mode 100644 analytics-data-product-freshness-guard/README.md
create mode 100644 analytics-data-product-freshness-guard/demo.js
create mode 100644 analytics-data-product-freshness-guard/index.js
create mode 100644 analytics-data-product-freshness-guard/make-demo-video.js
create mode 100644 analytics-data-product-freshness-guard/package.json
create mode 100644 analytics-data-product-freshness-guard/reports/clean-freshness-packet.json
create mode 100644 analytics-data-product-freshness-guard/reports/demo-script.txt
create mode 100644 analytics-data-product-freshness-guard/reports/demo.mp4
create mode 100644 analytics-data-product-freshness-guard/reports/freshness-review-report.md
create mode 100644 analytics-data-product-freshness-guard/reports/risky-freshness-packet.json
create mode 100644 analytics-data-product-freshness-guard/reports/summary.svg
create mode 100644 analytics-data-product-freshness-guard/sample-data.js
create mode 100644 analytics-data-product-freshness-guard/test.js
diff --git a/analytics-data-product-freshness-guard/README.md b/analytics-data-product-freshness-guard/README.md
new file mode 100644
index 00000000..6a9a0748
--- /dev/null
+++ b/analytics-data-product-freshness-guard/README.md
@@ -0,0 +1,37 @@
+# Analytics Data Product Freshness Guard
+
+This module is a focused Revenue Infrastructure slice for SCIBASE issue #20. It validates recurring licensed analytics data products before monthly invoice release, after entitlement checks but before finance sends the bill.
+
+The guard checks:
+
+- contracted refresh cadence against snapshot age and delivery date
+- required metric coverage for customer-facing analytics products
+- anonymization threshold coverage before billing for licensed data views
+- source-project withdrawal and embargo impacts on deliverable coverage
+- reproducibility-score feed freshness for billed analytics products
+- customer-facing delivery evidence for exports, portals, and dashboards
+- deterministic invoice, credit, defer, or hold recommendations
+
+It is intentionally separate from generic billing ledgers, usage meters, payment webhooks, payment failover, tax and VAT checks, FX settlement, quote approvals, invoice delivery, collections, storage overage, prepaid compute credits, analytics API usage gates, analytics seat rosters, customer consolidation, renewal/cancellation checks, price-escalation caps, and support entitlement guards. This slice focuses only on whether a licensed analytics data product is fresh and complete enough to bill.
+
+## Reviewer Path
+
+```bash
+npm run check
+npm test
+npm run demo
+npm run verify-video
+```
+
+Generated reviewer artifacts:
+
+- `reports/clean-freshness-packet.json`
+- `reports/risky-freshness-packet.json`
+- `reports/freshness-review-report.md`
+- `reports/summary.svg`
+- `reports/demo-script.txt`
+- `reports/demo.mp4`
+
+## Safety
+
+All fixtures are synthetic. The module does not call private research projects, uploaded datasets, live analytics stores, customer portals, billing systems, payment processors, credential stores, external APIs, payout systems, or financial accounts.
diff --git a/analytics-data-product-freshness-guard/demo.js b/analytics-data-product-freshness-guard/demo.js
new file mode 100644
index 00000000..a9bd501f
--- /dev/null
+++ b/analytics-data-product-freshness-guard/demo.js
@@ -0,0 +1,52 @@
+const fs = require("node:fs");
+const path = require("node:path");
+const { evaluateFreshnessPacket, renderMarkdownReport, renderSvgSummary } = require("./index");
+const { cleanPacket, riskyPacket } = require("./sample-data");
+
+const reportsDir = path.join(__dirname, "reports");
+fs.mkdirSync(reportsDir, { recursive: true });
+
+const cleanEvaluation = evaluateFreshnessPacket(cleanPacket);
+const riskyEvaluation = evaluateFreshnessPacket(riskyPacket);
+
+fs.writeFileSync(
+ path.join(reportsDir, "clean-freshness-packet.json"),
+ `${JSON.stringify({ input: cleanPacket, evaluation: cleanEvaluation }, null, 2)}\n`
+);
+fs.writeFileSync(
+ path.join(reportsDir, "risky-freshness-packet.json"),
+ `${JSON.stringify({ input: riskyPacket, evaluation: riskyEvaluation }, null, 2)}\n`
+);
+fs.writeFileSync(
+ path.join(reportsDir, "freshness-review-report.md"),
+ renderMarkdownReport(riskyPacket, riskyEvaluation)
+);
+fs.writeFileSync(
+ path.join(reportsDir, "summary.svg"),
+ renderSvgSummary(riskyEvaluation)
+);
+fs.writeFileSync(
+ path.join(reportsDir, "demo-script.txt"),
+ [
+ "Analytics data product freshness billing guard demo",
+ "",
+ `Clean packet decision: ${cleanEvaluation.summary.decision}`,
+ `Clean net invoice cents: ${cleanEvaluation.summary.netInvoiceCents}`,
+ `Clean audit digest: ${cleanEvaluation.summary.auditDigest}`,
+ "",
+ `Risky packet decision: ${riskyEvaluation.summary.decision}`,
+ `Risky finding count: ${riskyEvaluation.summary.findingCount}`,
+ `Risky recommended credit cents: ${riskyEvaluation.summary.recommendedCreditCents}`,
+ `Risky audit digest: ${riskyEvaluation.summary.auditDigest}`,
+ "",
+ "The risky packet demonstrates stale snapshots, incomplete metric coverage, anonymization threshold breach, unresolved withdrawal and embargo impacts, stale reproducibility feed evidence, and missing customer delivery proof.",
+ ""
+ ].join("\n")
+);
+
+console.log(JSON.stringify({
+ cleanDecision: cleanEvaluation.summary.decision,
+ riskyDecision: riskyEvaluation.summary.decision,
+ riskyFindings: riskyEvaluation.summary.findingCount,
+ report: "reports/freshness-review-report.md"
+}, null, 2));
diff --git a/analytics-data-product-freshness-guard/index.js b/analytics-data-product-freshness-guard/index.js
new file mode 100644
index 00000000..aec4390d
--- /dev/null
+++ b/analytics-data-product-freshness-guard/index.js
@@ -0,0 +1,377 @@
+const crypto = require("node:crypto");
+
+const DAY_MS = 24 * 60 * 60 * 1000;
+
+function asArray(value) {
+ return Array.isArray(value) ? value : [];
+}
+
+function stableJson(value) {
+ if (Array.isArray(value)) {
+ return `[${value.map(stableJson).join(",")}]`;
+ }
+ if (value && typeof value === "object") {
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
+ }
+ return JSON.stringify(value);
+}
+
+function sha256(value) {
+ return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
+}
+
+function toDate(value) {
+ if (!value) {
+ return null;
+ }
+ const parsed = new Date(value);
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
+}
+
+function daysBetween(laterValue, earlierValue) {
+ const later = toDate(laterValue);
+ const earlier = toDate(earlierValue);
+ if (!later || !earlier) {
+ return null;
+ }
+ return Math.floor((later.getTime() - earlier.getTime()) / DAY_MS);
+}
+
+function severityRank(severity) {
+ return { critical: 4, high: 3, medium: 2, low: 1 }[severity] || 0;
+}
+
+function addFinding(findings, severity, code, message, refs, action, creditCents = 0) {
+ findings.push({
+ severity,
+ code,
+ message,
+ refs: asArray(refs),
+ action,
+ creditCents
+ });
+}
+
+function evaluateFreshnessPacket(packet) {
+ const findings = [];
+ const billingDate = packet.billingDate || new Date().toISOString().slice(0, 10);
+ const contract = packet.contract || {};
+ const product = packet.analyticsProduct || {};
+ const snapshot = product.snapshot || {};
+ const delivery = packet.deliveryEvidence || {};
+ const requiredMetrics = asArray(contract.requiredMetrics);
+ const deliveredMetrics = new Set(asArray(product.deliveredMetrics));
+ const cadenceDays = Number(contract.refreshCadenceDays || 0);
+ const graceDays = Number(contract.graceDays || 0);
+ const monthlyFeeCents = Number(contract.monthlyFeeCents || 0);
+
+ if (!cadenceDays || cadenceDays < 1) {
+ addFinding(
+ findings,
+ "high",
+ "CONTRACT_REFRESH_CADENCE_MISSING",
+ "The analytics license lacks a usable contracted refresh cadence for billing readiness.",
+ [contract.id || "contract"],
+ "add_refresh_cadence_before_billing"
+ );
+ }
+
+ const snapshotAgeDays = daysBetween(billingDate, snapshot.generatedAt);
+ if (snapshotAgeDays === null) {
+ addFinding(
+ findings,
+ "high",
+ "SNAPSHOT_DATE_MISSING",
+ "The analytics product snapshot is missing a generated-at date.",
+ [snapshot.id || "snapshot"],
+ "attach_snapshot_generation_evidence"
+ );
+ } else if (cadenceDays && snapshotAgeDays > cadenceDays + graceDays) {
+ addFinding(
+ findings,
+ "high",
+ "SNAPSHOT_STALE",
+ `Snapshot age is ${snapshotAgeDays} days, beyond the ${cadenceDays} day cadence plus ${graceDays} day grace window.`,
+ [snapshot.id || "snapshot"],
+ "refresh_snapshot_before_invoice",
+ Math.round(monthlyFeeCents * 0.25)
+ );
+ } else if (cadenceDays && snapshotAgeDays > cadenceDays) {
+ addFinding(
+ findings,
+ "medium",
+ "SNAPSHOT_IN_GRACE_WINDOW",
+ `Snapshot age is ${snapshotAgeDays} days, inside grace but past the contracted ${cadenceDays} day cadence.`,
+ [snapshot.id || "snapshot"],
+ "apply_freshness_credit_or_refresh",
+ Math.round(monthlyFeeCents * 0.1)
+ );
+ }
+
+ const missingMetrics = requiredMetrics.filter((metric) => !deliveredMetrics.has(metric));
+ if (missingMetrics.length > 0) {
+ addFinding(
+ findings,
+ missingMetrics.length >= 2 ? "high" : "medium",
+ "METRIC_COVERAGE_INCOMPLETE",
+ `Missing ${missingMetrics.length} contracted analytics metrics: ${missingMetrics.join(", ")}.`,
+ missingMetrics,
+ "restore_metric_coverage_or_credit_invoice",
+ Math.round(monthlyFeeCents * Math.min(0.35, missingMetrics.length * 0.12))
+ );
+ }
+
+ const minimumCohortSize = Number(contract.minimumCohortSize || 0);
+ for (const segment of asArray(product.segments)) {
+ if (minimumCohortSize && segment.published === true && Number(segment.cohortSize || 0) < minimumCohortSize) {
+ addFinding(
+ findings,
+ "critical",
+ "ANONYMIZATION_THRESHOLD_BREACH",
+ `${segment.id || "segment"} was published with cohort size ${segment.cohortSize}, below the contracted minimum ${minimumCohortSize}.`,
+ [segment.id],
+ "suppress_segment_and_hold_invoice"
+ );
+ }
+ }
+
+ for (const source of asArray(product.sourceProjects)) {
+ if (source.withdrawnAt && source.includedInSnapshot === true) {
+ addFinding(
+ findings,
+ "high",
+ "SOURCE_PROJECT_WITHDRAWAL_UNRESOLVED",
+ `${source.id || "source"} was withdrawn before billing but remains included in the analytics snapshot.`,
+ [source.id],
+ "remove_withdrawn_source_and_refresh_snapshot",
+ Math.round(monthlyFeeCents * 0.2)
+ );
+ }
+ if (source.embargoUntil && source.includedInSnapshot === true) {
+ const embargoEndsAfterBilling = daysBetween(source.embargoUntil, billingDate);
+ if (embargoEndsAfterBilling !== null && embargoEndsAfterBilling > 0) {
+ addFinding(
+ findings,
+ "high",
+ "SOURCE_EMBARGO_UNRESOLVED",
+ `${source.id || "source"} remains embargoed for ${embargoEndsAfterBilling} more days but is included in the billed snapshot.`,
+ [source.id],
+ "exclude_embargoed_source_before_billing",
+ Math.round(monthlyFeeCents * 0.2)
+ );
+ }
+ }
+ }
+
+ const maxFeedAgeDays = Number(contract.maxReproducibilityFeedAgeDays || cadenceDays || 0);
+ const scoreFeedAgeDays = daysBetween(billingDate, product.reproducibilityScoreFeedAt);
+ if (maxFeedAgeDays && scoreFeedAgeDays === null) {
+ addFinding(
+ findings,
+ "medium",
+ "REPRODUCIBILITY_FEED_DATE_MISSING",
+ "The analytics product lacks reproducibility-score feed freshness evidence.",
+ [product.id || "analytics-product"],
+ "attach_reproducibility_feed_evidence"
+ );
+ } else if (maxFeedAgeDays && scoreFeedAgeDays > maxFeedAgeDays) {
+ addFinding(
+ findings,
+ "medium",
+ "REPRODUCIBILITY_FEED_STALE",
+ `Reproducibility score feed is ${scoreFeedAgeDays} days old, beyond the ${maxFeedAgeDays} day maximum.`,
+ [product.id || "analytics-product"],
+ "refresh_reproducibility_score_feed",
+ Math.round(monthlyFeeCents * 0.1)
+ );
+ }
+
+ const deliveryAgeDays = daysBetween(billingDate, delivery.deliveredAt);
+ const hasDeliveryReceipt = Boolean(delivery.portalUploadId || delivery.exportDigest || delivery.dashboardSnapshotHash);
+ if (!delivery.deliveredAt || !hasDeliveryReceipt) {
+ addFinding(
+ findings,
+ "high",
+ "DELIVERY_EVIDENCE_MISSING",
+ "Customer-facing delivery evidence is incomplete for the licensed analytics product.",
+ [delivery.id || "delivery"],
+ "attach_delivery_evidence_before_invoice"
+ );
+ } else if (cadenceDays && deliveryAgeDays !== null && deliveryAgeDays > cadenceDays + graceDays) {
+ addFinding(
+ findings,
+ "medium",
+ "DELIVERY_EVIDENCE_STALE",
+ `Customer delivery evidence is ${deliveryAgeDays} days old for a ${cadenceDays} day product cadence.`,
+ [delivery.id || "delivery"],
+ "refresh_customer_delivery_evidence",
+ Math.round(monthlyFeeCents * 0.1)
+ );
+ }
+
+ findings.sort((a, b) => severityRank(b.severity) - severityRank(a.severity) || a.code.localeCompare(b.code));
+ const highestSeverity = findings.reduce((max, finding) => Math.max(max, severityRank(finding.severity)), 0);
+ const creditCents = findings.reduce((sum, finding) => sum + Number(finding.creditCents || 0), 0);
+ const cappedCreditCents = Math.min(monthlyFeeCents, creditCents);
+ const provisionalNetInvoiceCents = Math.max(0, monthlyFeeCents - cappedCreditCents);
+ const decision = highestSeverity >= 3
+ ? "hold_analytics_invoice"
+ : cappedCreditCents > 0
+ ? "invoice_with_freshness_credit"
+ : "release_analytics_invoice";
+ const netInvoiceCents = decision === "hold_analytics_invoice" ? 0 : provisionalNetInvoiceCents;
+
+ const coverage = {
+ requiredMetrics,
+ deliveredMetrics: Array.from(deliveredMetrics),
+ missingMetrics,
+ snapshotAgeDays,
+ cadenceDays,
+ graceDays,
+ scoreFeedAgeDays,
+ deliveryAgeDays,
+ segmentsReviewed: asArray(product.segments).length,
+ sourceProjectsReviewed: asArray(product.sourceProjects).length
+ };
+ const summary = {
+ customerId: packet.customerId,
+ contractId: contract.id,
+ productId: product.id,
+ billingDate,
+ decision,
+ findingCount: findings.length,
+ highOrCriticalFindings: findings.filter((finding) => severityRank(finding.severity) >= 3).length,
+ monthlyFeeCents,
+ recommendedCreditCents: cappedCreditCents,
+ provisionalNetInvoiceCents,
+ netInvoiceCents
+ };
+ const auditDigest = `sha256:${sha256({ summary, findings, coverage }).slice(0, 16)}`;
+
+ return {
+ summary: {
+ ...summary,
+ auditDigest
+ },
+ coverage,
+ findings,
+ financeActions: buildFinanceActions(decision, findings, cappedCreditCents)
+ };
+}
+
+function buildFinanceActions(decision, findings, creditCents) {
+ const actions = [];
+ if (decision === "hold_analytics_invoice") {
+ actions.push({
+ id: "hold_invoice_release",
+ reason: "high_or_critical_freshness_blocker"
+ });
+ } else if (decision === "invoice_with_freshness_credit") {
+ actions.push({
+ id: "apply_freshness_credit",
+ creditCents
+ });
+ } else {
+ actions.push({
+ id: "release_invoice",
+ reason: "freshness_and_delivery_evidence_clean"
+ });
+ }
+
+ const seen = new Set(actions.map((action) => action.id));
+ for (const finding of findings) {
+ if (!finding.action || seen.has(finding.action)) {
+ continue;
+ }
+ seen.add(finding.action);
+ actions.push({
+ id: finding.action,
+ severity: finding.severity,
+ refs: finding.refs
+ });
+ }
+ return actions;
+}
+
+function renderMarkdownReport(packet, evaluation) {
+ const lines = [];
+ lines.push(`# Analytics Data Product Freshness Review: ${packet.customerId}`);
+ lines.push("");
+ lines.push(`Decision: **${evaluation.summary.decision}**`);
+ lines.push(`Audit digest: \`${evaluation.summary.auditDigest}\``);
+ lines.push(`Monthly fee: ${formatMoney(evaluation.summary.monthlyFeeCents)}`);
+ lines.push(`Recommended credit: ${formatMoney(evaluation.summary.recommendedCreditCents)}`);
+ lines.push(`Net invoice: ${formatMoney(evaluation.summary.netInvoiceCents)}`);
+ lines.push("");
+ lines.push("## Findings");
+ lines.push("");
+ if (evaluation.findings.length === 0) {
+ lines.push("No data-product freshness blockers were detected.");
+ } else {
+ lines.push("| Severity | Code | Message | Action | Credit |");
+ lines.push("| --- | --- | --- | --- | --- |");
+ for (const finding of evaluation.findings) {
+ lines.push(`| ${finding.severity} | \`${finding.code}\` | ${escapeMarkdown(finding.message)} | \`${finding.action}\` | ${formatMoney(finding.creditCents || 0)} |`);
+ }
+ }
+ lines.push("");
+ lines.push("## Coverage");
+ lines.push("");
+ lines.push(`- Required metrics: ${evaluation.coverage.requiredMetrics.join(", ") || "none"}`);
+ lines.push(`- Delivered metrics: ${evaluation.coverage.deliveredMetrics.join(", ") || "none"}`);
+ lines.push(`- Missing metrics: ${evaluation.coverage.missingMetrics.join(", ") || "none"}`);
+ lines.push(`- Snapshot age days: ${evaluation.coverage.snapshotAgeDays ?? "unknown"}`);
+ lines.push(`- Reproducibility feed age days: ${evaluation.coverage.scoreFeedAgeDays ?? "unknown"}`);
+ lines.push(`- Delivery evidence age days: ${evaluation.coverage.deliveryAgeDays ?? "unknown"}`);
+ lines.push("");
+ lines.push("Synthetic data only. No private research data, customer portals, billing systems, payment processors, credentials, payout systems, or external APIs are used.");
+ return `${lines.join("\n")}\n`;
+}
+
+function renderSvgSummary(evaluation) {
+ const color = evaluation.summary.decision === "hold_analytics_invoice"
+ ? "#b91c1c"
+ : evaluation.summary.decision === "invoice_with_freshness_credit"
+ ? "#b45309"
+ : "#047857";
+ const rows = evaluation.findings.slice(0, 5).map((finding, index) => {
+ const y = 318 + index * 42;
+ return `${escapeXml(finding.severity.toUpperCase())} ${escapeXml(finding.code)}`;
+ }).join("\n");
+ return `
+
+`;
+}
+
+function formatMoney(cents) {
+ return `$${(Number(cents || 0) / 100).toFixed(2)}`;
+}
+
+function escapeMarkdown(value) {
+ return String(value).replace(/\|/g, "\\|").replace(/\n/g, " ");
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
+
+module.exports = {
+ evaluateFreshnessPacket,
+ renderMarkdownReport,
+ renderSvgSummary,
+ sha256
+};
diff --git a/analytics-data-product-freshness-guard/make-demo-video.js b/analytics-data-product-freshness-guard/make-demo-video.js
new file mode 100644
index 00000000..bd1df718
--- /dev/null
+++ b/analytics-data-product-freshness-guard/make-demo-video.js
@@ -0,0 +1,94 @@
+const fs = require("node:fs");
+const path = require("node:path");
+const { spawnSync } = require("node:child_process");
+const { evaluateFreshnessPacket } = require("./index");
+const { cleanPacket, riskyPacket } = require("./sample-data");
+
+const reportsDir = path.join(__dirname, "reports");
+const framesDir = path.join(reportsDir, "frames");
+fs.mkdirSync(framesDir, { recursive: true });
+
+const clean = evaluateFreshnessPacket(cleanPacket);
+const risky = evaluateFreshnessPacket(riskyPacket);
+const width = 960;
+const height = 540;
+const frames = 72;
+const fps = 18;
+
+function setPixel(buffer, x, y, r, g, b) {
+ if (x < 0 || y < 0 || x >= width || y >= height) {
+ return;
+ }
+ const offset = (y * width + x) * 3;
+ buffer[offset] = r;
+ buffer[offset + 1] = g;
+ buffer[offset + 2] = b;
+}
+
+function fillRect(buffer, x, y, w, h, r, g, b) {
+ for (let row = y; row < y + h; row += 1) {
+ for (let col = x; col < x + w; col += 1) {
+ setPixel(buffer, col, row, r, g, b);
+ }
+ }
+}
+
+function writeFrame(index, progress) {
+ const buffer = Buffer.alloc(width * height * 3, 248);
+ fillRect(buffer, 0, 0, width, height, 248, 250, 252);
+ fillRect(buffer, 54, 48, 852, 444, 255, 255, 255);
+ fillRect(buffer, 54, 48, 852, 8, 15, 23, 42);
+
+ const cleanWidth = Math.floor(320 * Math.min(1, progress * 1.7));
+ const riskyWidth = Math.floor(320 * Math.max(0, (progress - 0.25) * 1.5));
+ fillRect(buffer, 102, 108, 320, 68, 229, 231, 235);
+ fillRect(buffer, 102, 108, cleanWidth, 68, 5, 150, 105);
+ fillRect(buffer, 538, 108, 320, 68, 229, 231, 235);
+ fillRect(buffer, 538, 108, riskyWidth, 68, 185, 28, 28);
+
+ const cleanMetricCount = clean.coverage.deliveredMetrics.length;
+ for (let i = 0; i < cleanMetricCount; i += 1) {
+ fillRect(buffer, 118 + i * 54, 238, 34, 118, 16, 185, 129);
+ }
+
+ for (let i = 0; i < Math.min(8, risky.summary.findingCount); i += 1) {
+ const barHeight = 36 + (i % 4) * 28;
+ fillRect(buffer, 548 + i * 34, 370 - barHeight, 24, barHeight, 220, 38, 38);
+ }
+
+ const netRatio = risky.summary.monthlyFeeCents
+ ? risky.summary.netInvoiceCents / risky.summary.monthlyFeeCents
+ : 0;
+ fillRect(buffer, 104, 430, Math.floor(752 * progress), 18, 37, 99, 235);
+ fillRect(buffer, 104, 458, Math.floor(752 * netRatio * progress), 18, 180, 83, 9);
+
+ const header = Buffer.from(`P6\n${width} ${height}\n255\n`, "ascii");
+ fs.writeFileSync(path.join(framesDir, `frame-${String(index).padStart(3, "0")}.ppm`), Buffer.concat([header, buffer]));
+}
+
+for (let index = 0; index < frames; index += 1) {
+ writeFrame(index, index / (frames - 1));
+}
+
+const output = path.join(reportsDir, "demo.mp4");
+const ffmpeg = process.env.FFMPEG_PATH || "ffmpeg";
+const result = spawnSync(ffmpeg, [
+ "-y",
+ "-framerate",
+ String(fps),
+ "-i",
+ path.join(framesDir, "frame-%03d.ppm"),
+ "-pix_fmt",
+ "yuv420p",
+ "-movflags",
+ "+faststart",
+ output
+], { stdio: "inherit" });
+
+fs.rmSync(framesDir, { recursive: true, force: true });
+
+if (result.status !== 0) {
+ process.exit(result.status || 1);
+}
+
+console.log(`Wrote ${output}`);
diff --git a/analytics-data-product-freshness-guard/package.json b/analytics-data-product-freshness-guard/package.json
new file mode 100644
index 00000000..d24ea359
--- /dev/null
+++ b/analytics-data-product-freshness-guard/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "analytics-data-product-freshness-guard",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Synthetic licensed analytics data-product freshness billing guard for SCIBASE revenue infrastructure.",
+ "main": "index.js",
+ "scripts": {
+ "check": "node --check index.js && node --check sample-data.js && node --check test.js && node --check demo.js && node --check make-demo-video.js",
+ "test": "node test.js",
+ "demo": "node demo.js && node make-demo-video.js",
+ "verify-video": "ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,duration,avg_frame_rate -show_entries format=duration,size -of default=noprint_wrappers=1 reports/demo.mp4"
+ },
+ "keywords": [
+ "scibase",
+ "revenue-infrastructure",
+ "analytics-licensing",
+ "billing-readiness",
+ "freshness"
+ ],
+ "license": "MIT"
+}
diff --git a/analytics-data-product-freshness-guard/reports/clean-freshness-packet.json b/analytics-data-product-freshness-guard/reports/clean-freshness-packet.json
new file mode 100644
index 00000000..b8599616
--- /dev/null
+++ b/analytics-data-product-freshness-guard/reports/clean-freshness-packet.json
@@ -0,0 +1,109 @@
+{
+ "input": {
+ "customerId": "nih-policy-insights-synthetic",
+ "billingDate": "2026-06-01",
+ "contract": {
+ "id": "contract-analytics-2026-nih",
+ "monthlyFeeCents": 120000,
+ "refreshCadenceDays": 14,
+ "graceDays": 2,
+ "minimumCohortSize": 25,
+ "maxReproducibilityFeedAgeDays": 14,
+ "requiredMetrics": [
+ "citation-network-growth",
+ "dataset-reuse-rate",
+ "reproducibility-score-index",
+ "method-adoption-trends"
+ ]
+ },
+ "analyticsProduct": {
+ "id": "analytics-product-open-science-dashboard",
+ "snapshot": {
+ "id": "snapshot-2026-05-25",
+ "generatedAt": "2026-05-25"
+ },
+ "deliveredMetrics": [
+ "citation-network-growth",
+ "dataset-reuse-rate",
+ "reproducibility-score-index",
+ "method-adoption-trends"
+ ],
+ "reproducibilityScoreFeedAt": "2026-05-28",
+ "segments": [
+ {
+ "id": "segment-neuroscience-data-reuse",
+ "cohortSize": 82,
+ "published": true
+ },
+ {
+ "id": "segment-climate-model-methods",
+ "cohortSize": 41,
+ "published": true
+ }
+ ],
+ "sourceProjects": [
+ {
+ "id": "project-synthetic-a",
+ "includedInSnapshot": true
+ },
+ {
+ "id": "project-synthetic-b",
+ "embargoUntil": "2026-05-10",
+ "includedInSnapshot": true
+ }
+ ]
+ },
+ "deliveryEvidence": {
+ "id": "delivery-portal-2026-05-26",
+ "deliveredAt": "2026-05-26",
+ "portalUploadId": "portal-upload-synthetic-9461",
+ "dashboardSnapshotHash": "sha256:239a6f97d079b7b4",
+ "exportDigest": "sha256:f3e6cfe420f531ea"
+ }
+ },
+ "evaluation": {
+ "summary": {
+ "customerId": "nih-policy-insights-synthetic",
+ "contractId": "contract-analytics-2026-nih",
+ "productId": "analytics-product-open-science-dashboard",
+ "billingDate": "2026-06-01",
+ "decision": "release_analytics_invoice",
+ "findingCount": 0,
+ "highOrCriticalFindings": 0,
+ "monthlyFeeCents": 120000,
+ "recommendedCreditCents": 0,
+ "provisionalNetInvoiceCents": 120000,
+ "netInvoiceCents": 120000,
+ "auditDigest": "sha256:1c30445b56d3cfc2"
+ },
+ "coverage": {
+ "requiredMetrics": [
+ "citation-network-growth",
+ "dataset-reuse-rate",
+ "reproducibility-score-index",
+ "method-adoption-trends"
+ ],
+ "deliveredMetrics": [
+ "citation-network-growth",
+ "dataset-reuse-rate",
+ "reproducibility-score-index",
+ "method-adoption-trends"
+ ],
+ "missingMetrics": [],
+ "snapshotAgeDays": 7,
+ "cadenceDays": 14,
+ "graceDays": 2,
+ "scoreFeedAgeDays": 4,
+ "deliveryAgeDays": 6,
+ "segmentsReviewed": 2,
+ "sourceProjectsReviewed": 2
+ },
+ "findings": [],
+ "financeActions": [
+ {
+ "id": "release_invoice",
+ "reason": "freshness_and_delivery_evidence_clean"
+ }
+ ]
+ }
+}
diff --git a/analytics-data-product-freshness-guard/reports/demo-script.txt b/analytics-data-product-freshness-guard/reports/demo-script.txt
new file mode 100644
index 00000000..c75656ca
--- /dev/null
+++ b/analytics-data-product-freshness-guard/reports/demo-script.txt
@@ -0,0 +1,12 @@
+Analytics data product freshness billing guard demo
+
+Clean packet decision: release_analytics_invoice
+Clean net invoice cents: 120000
+Clean audit digest: sha256:1c30445b56d3cfc2
+
+Risky packet decision: hold_analytics_invoice
+Risky finding count: 7
+Risky recommended credit cents: 178200
+Risky audit digest: sha256:88532af412b7f387
+
+The risky packet demonstrates stale snapshots, incomplete metric coverage, anonymization threshold breach, unresolved withdrawal and embargo impacts, stale reproducibility feed evidence, and missing customer delivery proof.
diff --git a/analytics-data-product-freshness-guard/reports/demo.mp4 b/analytics-data-product-freshness-guard/reports/demo.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..593d29987034c9972b1eb3816eb10a10ee205437
GIT binary patch
literal 9234
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
literal 0
HcmV?d00001
diff --git a/analytics-data-product-freshness-guard/reports/freshness-review-report.md b/analytics-data-product-freshness-guard/reports/freshness-review-report.md
new file mode 100644
index 00000000..179e1d6c
--- /dev/null
+++ b/analytics-data-product-freshness-guard/reports/freshness-review-report.md
@@ -0,0 +1,30 @@
+# Analytics Data Product Freshness Review: eu-consortium-analytics-synthetic
+
+Decision: **hold_analytics_invoice**
+Audit digest: `sha256:88532af412b7f387`
+Monthly fee: $1800.00
+Recommended credit: $1782.00
+Net invoice: $0.00
+
+## Findings
+
+| Severity | Code | Message | Action | Credit |
+| --- | --- | --- | --- | --- |
+| critical | `ANONYMIZATION_THRESHOLD_BREACH` | segment-rare-disease-lab-methods was published with cohort size 11, below the contracted minimum 30. | `suppress_segment_and_hold_invoice` | $0.00 |
+| high | `DELIVERY_EVIDENCE_MISSING` | Customer-facing delivery evidence is incomplete for the licensed analytics product. | `attach_delivery_evidence_before_invoice` | $0.00 |
+| high | `METRIC_COVERAGE_INCOMPLETE` | Missing 2 contracted analytics metrics: reproducibility-score-index, project-lineage-map. | `restore_metric_coverage_or_credit_invoice` | $432.00 |
+| high | `SNAPSHOT_STALE` | Snapshot age is 27 days, beyond the 14 day cadence plus 2 day grace window. | `refresh_snapshot_before_invoice` | $450.00 |
+| high | `SOURCE_EMBARGO_UNRESOLVED` | project-embargoed-dataset remains embargoed for 19 more days but is included in the billed snapshot. | `exclude_embargoed_source_before_billing` | $360.00 |
+| high | `SOURCE_PROJECT_WITHDRAWAL_UNRESOLVED` | project-withdrawn-lab-notebook was withdrawn before billing but remains included in the analytics snapshot. | `remove_withdrawn_source_and_refresh_snapshot` | $360.00 |
+| medium | `REPRODUCIBILITY_FEED_STALE` | Reproducibility score feed is 22 days old, beyond the 14 day maximum. | `refresh_reproducibility_score_feed` | $180.00 |
+
+## Coverage
+
+- Required metrics: citation-network-growth, dataset-reuse-rate, reproducibility-score-index, method-adoption-trends, project-lineage-map
+- Delivered metrics: citation-network-growth, dataset-reuse-rate, method-adoption-trends
+- Missing metrics: reproducibility-score-index, project-lineage-map
+- Snapshot age days: 27
+- Reproducibility feed age days: 22
+- Delivery evidence age days: unknown
+
+Synthetic data only. No private research data, customer portals, billing systems, payment processors, credentials, payout systems, or external APIs are used.
diff --git a/analytics-data-product-freshness-guard/reports/risky-freshness-packet.json b/analytics-data-product-freshness-guard/reports/risky-freshness-packet.json
new file mode 100644
index 00000000..d795db56
--- /dev/null
+++ b/analytics-data-product-freshness-guard/reports/risky-freshness-packet.json
@@ -0,0 +1,235 @@
+{
+ "input": {
+ "customerId": "eu-consortium-analytics-synthetic",
+ "billingDate": "2026-06-01",
+ "contract": {
+ "id": "contract-analytics-2026-eu",
+ "monthlyFeeCents": 180000,
+ "refreshCadenceDays": 14,
+ "graceDays": 2,
+ "minimumCohortSize": 30,
+ "maxReproducibilityFeedAgeDays": 14,
+ "requiredMetrics": [
+ "citation-network-growth",
+ "dataset-reuse-rate",
+ "reproducibility-score-index",
+ "method-adoption-trends",
+ "project-lineage-map"
+ ]
+ },
+ "analyticsProduct": {
+ "id": "analytics-product-funder-trends",
+ "snapshot": {
+ "id": "snapshot-2026-05-05",
+ "generatedAt": "2026-05-05"
+ },
+ "deliveredMetrics": [
+ "citation-network-growth",
+ "dataset-reuse-rate",
+ "method-adoption-trends"
+ ],
+ "reproducibilityScoreFeedAt": "2026-05-10",
+ "segments": [
+ {
+ "id": "segment-rare-disease-lab-methods",
+ "cohortSize": 11,
+ "published": true
+ },
+ {
+ "id": "segment-materials-modeling",
+ "cohortSize": 64,
+ "published": true
+ }
+ ],
+ "sourceProjects": [
+ {
+ "id": "project-withdrawn-lab-notebook",
+ "withdrawnAt": "2026-05-20",
+ "includedInSnapshot": true
+ },
+ {
+ "id": "project-embargoed-dataset",
+ "embargoUntil": "2026-06-20",
+ "includedInSnapshot": true
+ }
+ ]
+ },
+ "deliveryEvidence": {
+ "id": "delivery-missing-portal-receipt",
+ "deliveredAt": "",
+ "portalUploadId": "",
+ "dashboardSnapshotHash": "",
+ "exportDigest": ""
+ }
+ },
+ "evaluation": {
+ "summary": {
+ "customerId": "eu-consortium-analytics-synthetic",
+ "contractId": "contract-analytics-2026-eu",
+ "productId": "analytics-product-funder-trends",
+ "billingDate": "2026-06-01",
+ "decision": "hold_analytics_invoice",
+ "findingCount": 7,
+ "highOrCriticalFindings": 6,
+ "monthlyFeeCents": 180000,
+ "recommendedCreditCents": 178200,
+ "provisionalNetInvoiceCents": 1800,
+ "netInvoiceCents": 0,
+ "auditDigest": "sha256:88532af412b7f387"
+ },
+ "coverage": {
+ "requiredMetrics": [
+ "citation-network-growth",
+ "dataset-reuse-rate",
+ "reproducibility-score-index",
+ "method-adoption-trends",
+ "project-lineage-map"
+ ],
+ "deliveredMetrics": [
+ "citation-network-growth",
+ "dataset-reuse-rate",
+ "method-adoption-trends"
+ ],
+ "missingMetrics": [
+ "reproducibility-score-index",
+ "project-lineage-map"
+ ],
+ "snapshotAgeDays": 27,
+ "cadenceDays": 14,
+ "graceDays": 2,
+ "scoreFeedAgeDays": 22,
+ "deliveryAgeDays": null,
+ "segmentsReviewed": 2,
+ "sourceProjectsReviewed": 2
+ },
+ "findings": [
+ {
+ "severity": "critical",
+ "code": "ANONYMIZATION_THRESHOLD_BREACH",
+ "message": "segment-rare-disease-lab-methods was published with cohort size 11, below the contracted minimum 30.",
+ "refs": [
+ "segment-rare-disease-lab-methods"
+ ],
+ "action": "suppress_segment_and_hold_invoice",
+ "creditCents": 0
+ },
+ {
+ "severity": "high",
+ "code": "DELIVERY_EVIDENCE_MISSING",
+ "message": "Customer-facing delivery evidence is incomplete for the licensed analytics product.",
+ "refs": [
+ "delivery-missing-portal-receipt"
+ ],
+ "action": "attach_delivery_evidence_before_invoice",
+ "creditCents": 0
+ },
+ {
+ "severity": "high",
+ "code": "METRIC_COVERAGE_INCOMPLETE",
+ "message": "Missing 2 contracted analytics metrics: reproducibility-score-index, project-lineage-map.",
+ "refs": [
+ "reproducibility-score-index",
+ "project-lineage-map"
+ ],
+ "action": "restore_metric_coverage_or_credit_invoice",
+ "creditCents": 43200
+ },
+ {
+ "severity": "high",
+ "code": "SNAPSHOT_STALE",
+ "message": "Snapshot age is 27 days, beyond the 14 day cadence plus 2 day grace window.",
+ "refs": [
+ "snapshot-2026-05-05"
+ ],
+ "action": "refresh_snapshot_before_invoice",
+ "creditCents": 45000
+ },
+ {
+ "severity": "high",
+ "code": "SOURCE_EMBARGO_UNRESOLVED",
+ "message": "project-embargoed-dataset remains embargoed for 19 more days but is included in the billed snapshot.",
+ "refs": [
+ "project-embargoed-dataset"
+ ],
+ "action": "exclude_embargoed_source_before_billing",
+ "creditCents": 36000
+ },
+ {
+ "severity": "high",
+ "code": "SOURCE_PROJECT_WITHDRAWAL_UNRESOLVED",
+ "message": "project-withdrawn-lab-notebook was withdrawn before billing but remains included in the analytics snapshot.",
+ "refs": [
+ "project-withdrawn-lab-notebook"
+ ],
+ "action": "remove_withdrawn_source_and_refresh_snapshot",
+ "creditCents": 36000
+ },
+ {
+ "severity": "medium",
+ "code": "REPRODUCIBILITY_FEED_STALE",
+ "message": "Reproducibility score feed is 22 days old, beyond the 14 day maximum.",
+ "refs": [
+ "analytics-product-funder-trends"
+ ],
+ "action": "refresh_reproducibility_score_feed",
+ "creditCents": 18000
+ }
+ ],
+ "financeActions": [
+ {
+ "id": "hold_invoice_release",
+ "reason": "high_or_critical_freshness_blocker"
+ },
+ {
+ "id": "suppress_segment_and_hold_invoice",
+ "severity": "critical",
+ "refs": [
+ "segment-rare-disease-lab-methods"
+ ]
+ },
+ {
+ "id": "attach_delivery_evidence_before_invoice",
+ "severity": "high",
+ "refs": [
+ "delivery-missing-portal-receipt"
+ ]
+ },
+ {
+ "id": "restore_metric_coverage_or_credit_invoice",
+ "severity": "high",
+ "refs": [
+ "reproducibility-score-index",
+ "project-lineage-map"
+ ]
+ },
+ {
+ "id": "refresh_snapshot_before_invoice",
+ "severity": "high",
+ "refs": [
+ "snapshot-2026-05-05"
+ ]
+ },
+ {
+ "id": "exclude_embargoed_source_before_billing",
+ "severity": "high",
+ "refs": [
+ "project-embargoed-dataset"
+ ]
+ },
+ {
+ "id": "remove_withdrawn_source_and_refresh_snapshot",
+ "severity": "high",
+ "refs": [
+ "project-withdrawn-lab-notebook"
+ ]
+ },
+ {
+ "id": "refresh_reproducibility_score_feed",
+ "severity": "medium",
+ "refs": [
+ "analytics-product-funder-trends"
+ ]
+ }
+ ]
+ }
+}
diff --git a/analytics-data-product-freshness-guard/reports/summary.svg b/analytics-data-product-freshness-guard/reports/summary.svg
new file mode 100644
index 00000000..ef6e52c4
--- /dev/null
+++ b/analytics-data-product-freshness-guard/reports/summary.svg
@@ -0,0 +1,16 @@
+
+
diff --git a/analytics-data-product-freshness-guard/sample-data.js b/analytics-data-product-freshness-guard/sample-data.js
new file mode 100644
index 00000000..13ca21d2
--- /dev/null
+++ b/analytics-data-product-freshness-guard/sample-data.js
@@ -0,0 +1,131 @@
+const cleanPacket = {
+ customerId: "nih-policy-insights-synthetic",
+ billingDate: "2026-06-01",
+ contract: {
+ id: "contract-analytics-2026-nih",
+ monthlyFeeCents: 120000,
+ refreshCadenceDays: 14,
+ graceDays: 2,
+ minimumCohortSize: 25,
+ maxReproducibilityFeedAgeDays: 14,
+ requiredMetrics: [
+ "citation-network-growth",
+ "dataset-reuse-rate",
+ "reproducibility-score-index",
+ "method-adoption-trends"
+ ]
+ },
+ analyticsProduct: {
+ id: "analytics-product-open-science-dashboard",
+ snapshot: {
+ id: "snapshot-2026-05-25",
+ generatedAt: "2026-05-25"
+ },
+ deliveredMetrics: [
+ "citation-network-growth",
+ "dataset-reuse-rate",
+ "reproducibility-score-index",
+ "method-adoption-trends"
+ ],
+ reproducibilityScoreFeedAt: "2026-05-28",
+ segments: [
+ {
+ id: "segment-neuroscience-data-reuse",
+ cohortSize: 82,
+ published: true
+ },
+ {
+ id: "segment-climate-model-methods",
+ cohortSize: 41,
+ published: true
+ }
+ ],
+ sourceProjects: [
+ {
+ id: "project-synthetic-a",
+ includedInSnapshot: true
+ },
+ {
+ id: "project-synthetic-b",
+ embargoUntil: "2026-05-10",
+ includedInSnapshot: true
+ }
+ ]
+ },
+ deliveryEvidence: {
+ id: "delivery-portal-2026-05-26",
+ deliveredAt: "2026-05-26",
+ portalUploadId: "portal-upload-synthetic-9461",
+ dashboardSnapshotHash: "sha256:239a6f97d079b7b4",
+ exportDigest: "sha256:f3e6cfe420f531ea"
+ }
+};
+
+const riskyPacket = {
+ customerId: "eu-consortium-analytics-synthetic",
+ billingDate: "2026-06-01",
+ contract: {
+ id: "contract-analytics-2026-eu",
+ monthlyFeeCents: 180000,
+ refreshCadenceDays: 14,
+ graceDays: 2,
+ minimumCohortSize: 30,
+ maxReproducibilityFeedAgeDays: 14,
+ requiredMetrics: [
+ "citation-network-growth",
+ "dataset-reuse-rate",
+ "reproducibility-score-index",
+ "method-adoption-trends",
+ "project-lineage-map"
+ ]
+ },
+ analyticsProduct: {
+ id: "analytics-product-funder-trends",
+ snapshot: {
+ id: "snapshot-2026-05-05",
+ generatedAt: "2026-05-05"
+ },
+ deliveredMetrics: [
+ "citation-network-growth",
+ "dataset-reuse-rate",
+ "method-adoption-trends"
+ ],
+ reproducibilityScoreFeedAt: "2026-05-10",
+ segments: [
+ {
+ id: "segment-rare-disease-lab-methods",
+ cohortSize: 11,
+ published: true
+ },
+ {
+ id: "segment-materials-modeling",
+ cohortSize: 64,
+ published: true
+ }
+ ],
+ sourceProjects: [
+ {
+ id: "project-withdrawn-lab-notebook",
+ withdrawnAt: "2026-05-20",
+ includedInSnapshot: true
+ },
+ {
+ id: "project-embargoed-dataset",
+ embargoUntil: "2026-06-20",
+ includedInSnapshot: true
+ }
+ ]
+ },
+ deliveryEvidence: {
+ id: "delivery-missing-portal-receipt",
+ deliveredAt: "",
+ portalUploadId: "",
+ dashboardSnapshotHash: "",
+ exportDigest: ""
+ }
+};
+
+module.exports = {
+ cleanPacket,
+ riskyPacket
+};
diff --git a/analytics-data-product-freshness-guard/test.js b/analytics-data-product-freshness-guard/test.js
new file mode 100644
index 00000000..f9c8a0de
--- /dev/null
+++ b/analytics-data-product-freshness-guard/test.js
@@ -0,0 +1,46 @@
+const assert = require("node:assert/strict");
+const { evaluateFreshnessPacket, renderMarkdownReport, renderSvgSummary } = require("./index");
+const { cleanPacket, riskyPacket } = require("./sample-data");
+
+function clone(value) {
+ return JSON.parse(JSON.stringify(value));
+}
+
+function codes(evaluation) {
+ return new Set(evaluation.findings.map((finding) => finding.code));
+}
+
+const clean = evaluateFreshnessPacket(cleanPacket);
+assert.equal(clean.summary.decision, "release_analytics_invoice");
+assert.equal(clean.findings.length, 0);
+assert.equal(clean.summary.netInvoiceCents, cleanPacket.contract.monthlyFeeCents);
+
+const risky = evaluateFreshnessPacket(riskyPacket);
+assert.equal(risky.summary.decision, "hold_analytics_invoice");
+assert.equal(codes(risky).has("SNAPSHOT_STALE"), true);
+assert.equal(codes(risky).has("METRIC_COVERAGE_INCOMPLETE"), true);
+assert.equal(codes(risky).has("ANONYMIZATION_THRESHOLD_BREACH"), true);
+assert.equal(codes(risky).has("SOURCE_PROJECT_WITHDRAWAL_UNRESOLVED"), true);
+assert.equal(codes(risky).has("SOURCE_EMBARGO_UNRESOLVED"), true);
+assert.equal(codes(risky).has("REPRODUCIBILITY_FEED_STALE"), true);
+assert.equal(codes(risky).has("DELIVERY_EVIDENCE_MISSING"), true);
+assert.ok(risky.summary.recommendedCreditCents > 0);
+
+const creditOnly = clone(cleanPacket);
+creditOnly.analyticsProduct.snapshot.generatedAt = "2026-05-17";
+creditOnly.deliveryEvidence.deliveredAt = "2026-05-18";
+const credit = evaluateFreshnessPacket(creditOnly);
+assert.equal(credit.summary.decision, "invoice_with_freshness_credit");
+assert.equal(codes(credit).has("SNAPSHOT_IN_GRACE_WINDOW"), true);
+assert.ok(credit.summary.recommendedCreditCents > 0);
+assert.ok(credit.summary.netInvoiceCents < credit.summary.monthlyFeeCents);
+
+const markdown = renderMarkdownReport(riskyPacket, risky);
+assert.match(markdown, /Analytics Data Product Freshness Review/);
+assert.match(markdown, /hold_analytics_invoice/);
+
+const svg = renderSvgSummary(risky);
+assert.match(svg, /