thumbgate 1.28.4 → 1.29.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/.claude/commands/dashboard.md +11 -1
  2. package/.claude/commands/thumbgate-dashboard.md +23 -8
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/llms.txt +18 -10
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +66 -3
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/forge/forge.yaml +3 -3
  9. package/adapters/mcp/server-stdio.js +88 -2
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bin/cli.js +8 -8
  12. package/bin/postinstall.js +4 -13
  13. package/commands/dashboard.md +11 -1
  14. package/commands/thumbgate-dashboard.md +23 -8
  15. package/config/agent-outcome-monitor-thresholds.json +63 -0
  16. package/config/evals/agent-outcomes-baseline.json +17 -0
  17. package/config/evals/agent-outcomes-golden.json +412 -0
  18. package/config/evals/prompt-eval-baseline.json +23 -0
  19. package/config/github-about.json +5 -4
  20. package/config/post-deploy-marketing-pages.json +6 -6
  21. package/config/schemas/task-outcome-receipt.schema.json +296 -0
  22. package/docs/integrations/grafana/README.md +109 -0
  23. package/docs/integrations/grafana/thumbgate-revenue-evidence-dashboard.json +1930 -0
  24. package/openapi/openapi.yaml +475 -5
  25. package/package.json +75 -22
  26. package/public/agent-manager.html +10 -11
  27. package/public/agents-cost-savings.html +2 -2
  28. package/public/assets/brand/thumbgate-logo-transparent.svg +6 -11
  29. package/public/assets/brand/thumbgate-mark-inline-v3.svg +11 -10
  30. package/public/assets/brand/thumbgate-mark.svg +10 -11
  31. package/public/blog/inside-your-boundary.html +114 -0
  32. package/public/blog/process-over-outcome-gates.html +119 -0
  33. package/public/blog.html +296 -402
  34. package/public/brand/thumbgate-mark.svg +5 -9
  35. package/public/codex-enterprise.html +2 -2
  36. package/public/compare.html +12 -3
  37. package/public/diagnostic.html +79 -29
  38. package/public/guide.html +4 -4
  39. package/public/index.html +1090 -2098
  40. package/public/install.html +3 -3
  41. package/public/js/buyer-intent.js +33 -18
  42. package/public/numbers.html +2 -2
  43. package/public/pricing.html +268 -408
  44. package/public/pro.html +4 -4
  45. package/scripts/agent-outcome-eval.js +130 -0
  46. package/scripts/agent-outcome-monitor.js +261 -0
  47. package/scripts/agent-reasoning-traces.js +8 -9
  48. package/scripts/async-job-runner.js +107 -13
  49. package/scripts/billing.js +456 -126
  50. package/scripts/buyer-paths.js +102 -0
  51. package/scripts/cli-feedback.js +2 -2
  52. package/scripts/commercial-offer.js +18 -10
  53. package/scripts/durability/step.js +121 -12
  54. package/scripts/external-customer-audit.js +881 -0
  55. package/scripts/feedback-loop.js +26 -0
  56. package/scripts/gates-engine.js +554 -19
  57. package/scripts/grafana-revenue-evidence.js +856 -0
  58. package/scripts/human-escalation.js +265 -0
  59. package/scripts/hybrid-feedback-context.js +93 -50
  60. package/scripts/jsonl-window.js +89 -0
  61. package/scripts/judge-reward-function.js +30 -18
  62. package/scripts/lesson-embedding-index.js +3 -7
  63. package/scripts/meta-agent-loop.js +20 -2
  64. package/scripts/observability-env.js +139 -0
  65. package/scripts/observability-setup.js +55 -0
  66. package/scripts/plausible-domain-config.js +4 -0
  67. package/scripts/prompt-eval.js +81 -4
  68. package/scripts/provider-live-evidence.js +1290 -0
  69. package/scripts/provider-payment-reconciler.js +442 -0
  70. package/scripts/provider-revenue-evidence.js +249 -0
  71. package/scripts/rate-limiter.js +1 -5
  72. package/scripts/revenue-action-eligibility.js +414 -0
  73. package/scripts/revenue-evidence-remediation.js +694 -0
  74. package/scripts/revenue-offer-system.js +709 -0
  75. package/scripts/sales-pipeline.js +1117 -0
  76. package/scripts/schedule-manager.js +249 -0
  77. package/scripts/seo-gsd.js +8 -4
  78. package/scripts/stripe-credentials.js +37 -0
  79. package/scripts/stripe-revenue-catalog-audit.js +363 -0
  80. package/scripts/stripe-revenue-catalog.js +164 -0
  81. package/scripts/task-outcomes.js +425 -0
  82. package/scripts/telemetry-analytics.js +23 -3
  83. package/scripts/tool-contract-validator.js +287 -59
  84. package/scripts/tool-registry.js +143 -0
  85. package/scripts/vector-store.js +83 -7
  86. package/scripts/workflow-intake-queue.js +483 -0
  87. package/src/api/server.js +647 -118
@@ -0,0 +1,856 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const crypto = require('node:crypto');
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+
8
+ const { ensureParentDir } = require('./fs-utils');
9
+
10
+ const SCHEMA_VERSION = 1;
11
+ const DEFAULT_DASHBOARD_UID = 'thumbgate-revenue-evidence';
12
+ const CLAIM_BOUNDARY = 'Aggregate operational evidence only. A dashboard row does not prove a payment, customer, revenue, or target outcome beyond the provider-verified source snapshot.';
13
+ const LOKI_HOST_PATTERN = /(^|\.)grafana\.net$/i;
14
+ const LOKI_PUSH_PATH = '/loki/api/v1/push';
15
+ const SNAPSHOT_KEYS = new Set([
16
+ 'approvalReady', 'bookedRevenueCents', 'bookedRevenueDollars',
17
+ 'claimBoundary', 'deployedSha', 'event', 'evidenceGapCount',
18
+ 'expectedRevisionDeployed', 'expectedRevisionDeployedValue', 'expectedSha',
19
+ 'firstExternalPayment', 'firstExternalPaymentValue', 'generatedAt',
20
+ 'internalReady', 'observedCheckoutStarts',
21
+ 'observedCompleteWorkflowSprintIntakeCount', 'observedCtaClicks',
22
+ 'observedDiagnosticCheckoutStarts', 'observedFunnelAvailable',
23
+ 'observedFunnelAvailableValue',
24
+ 'observedIntakeCloseQueueAvailable', 'observedIntakeCloseQueueAvailableValue',
25
+ 'observedApprovalReadyIntakeCount',
26
+ 'observedDiscoveryReadyIntakeCount',
27
+ 'observedLifecycleQualifiedWorkflowSprintLeadCount', 'observedPageViews',
28
+ 'observedVisitors', 'observedWorkflowIntakeClicks',
29
+ 'observedWorkflowSprintIntakeCount', 'pipelineTotal', 'productionHealthy',
30
+ 'productionHealthyValue', 'providerCoverageComplete',
31
+ 'providerCoverageCompleteValue', 'sameDayEvidencePriority',
32
+ 'sameDayExternalPayment', 'sameDayExternalPaymentValue',
33
+ 'sameDayReviewPriority', 'schemaVersion', 'sourceDigests',
34
+ 'sourceGeneratedAt', 'targetAchieved', 'targetAchievedValue', 'targetStatus',
35
+ 'stripeCatalogAttached', 'stripeCatalogAttachedValue',
36
+ 'stripeCatalogExpectedOfferCount', 'stripeCatalogExpectedPublicPaymentRailCount',
37
+ 'stripeCatalogPaymentRailDriftCount', 'stripeCatalogPriceDriftCount',
38
+ 'stripeCatalogVerified', 'stripeCatalogVerifiedOfferCount',
39
+ 'stripeCatalogVerifiedPublicPaymentRailCount', 'stripeCatalogVerifiedValue',
40
+ 'stripeCatalogVersion',
41
+ 'verifiedIndividualPaymentCount', 'verifiedPaid',
42
+ ]);
43
+ const SOURCE_GENERATED_AT_KEYS = new Set(['billing', 'remediation', 'stripeCatalog', 'target']);
44
+ const SOURCE_DIGEST_KEYS = new Set(['billingSha256', 'remediationSha256', 'stripeCatalogSha256', 'targetSha256']);
45
+
46
+ function normalizeText(value, maxLength = 1000) {
47
+ if (value === undefined || value === null) return null;
48
+ const normalized = String(value).trim();
49
+ return normalized ? normalized.slice(0, maxLength) : null;
50
+ }
51
+
52
+ function normalizeSafeToken(value, maxLength = 80) {
53
+ const normalized = normalizeText(value, maxLength);
54
+ return normalized && /^[a-z0-9][a-z0-9_.-]*$/i.test(normalized) ? normalized : null;
55
+ }
56
+
57
+ function normalizeSha256(value) {
58
+ const normalized = normalizeText(value, 64)?.toLowerCase();
59
+ return normalized && /^[a-f0-9]{64}$/.test(normalized) ? normalized : null;
60
+ }
61
+
62
+ function normalizeGitRevision(value) {
63
+ const normalized = normalizeText(value, 64)?.toLowerCase();
64
+ return normalized && /^[a-f0-9]{7,64}$/.test(normalized) ? normalized : null;
65
+ }
66
+
67
+ function parseTimestamp(value, label) {
68
+ const normalized = normalizeText(value, 64);
69
+ const parsed = Date.parse(normalized || '');
70
+ if (!Number.isFinite(parsed)) throw new Error(`${label} must be a valid timestamp.`);
71
+ return new Date(parsed).toISOString();
72
+ }
73
+
74
+ function nonNegativeInteger(value, fallback = 0) {
75
+ const parsed = Number(value);
76
+ if (!Number.isFinite(parsed)) return fallback;
77
+ return Math.max(0, Math.trunc(parsed));
78
+ }
79
+
80
+ function booleanValue(value) {
81
+ return value === true;
82
+ }
83
+
84
+ function sha256(value) {
85
+ return crypto.createHash('sha256').update(value).digest('hex');
86
+ }
87
+
88
+ function readJsonFile(filePath, label) {
89
+ if (!filePath) throw new Error(`${label} path is required.`);
90
+ const resolved = path.resolve(filePath);
91
+ let source;
92
+ try {
93
+ source = fs.readFileSync(resolved, 'utf8');
94
+ } catch (error) {
95
+ throw new Error(`${label} could not be read: ${error.message}`);
96
+ }
97
+ try {
98
+ return {
99
+ path: resolved,
100
+ source,
101
+ digest: sha256(source),
102
+ value: JSON.parse(source),
103
+ };
104
+ } catch (error) {
105
+ throw new Error(`${label} is not valid JSON: ${error.message}`);
106
+ }
107
+ }
108
+
109
+ function buildObservedFunnelFields(billing) {
110
+ if (billing === undefined || billing === null) {
111
+ return {
112
+ sourceGeneratedAt: null,
113
+ fields: {
114
+ observedFunnelAvailable: false,
115
+ observedFunnelAvailableValue: 0,
116
+ observedVisitors: 0,
117
+ observedPageViews: 0,
118
+ observedCtaClicks: 0,
119
+ observedCheckoutStarts: 0,
120
+ observedDiagnosticCheckoutStarts: 0,
121
+ observedIntakeCloseQueueAvailable: false,
122
+ observedIntakeCloseQueueAvailableValue: 0,
123
+ observedApprovalReadyIntakeCount: 0,
124
+ observedDiscoveryReadyIntakeCount: 0,
125
+ observedWorkflowIntakeClicks: 0,
126
+ observedWorkflowSprintIntakeCount: 0,
127
+ observedCompleteWorkflowSprintIntakeCount: 0,
128
+ observedLifecycleQualifiedWorkflowSprintLeadCount: 0,
129
+ },
130
+ };
131
+ }
132
+ if (!billing || typeof billing !== 'object' || Array.isArray(billing)) {
133
+ throw new Error('Hosted billing evidence must be a JSON object.');
134
+ }
135
+ const traffic = billing.trafficMetrics || {};
136
+ const ctas = billing.ctas || {};
137
+ const pipeline = billing.pipeline || {};
138
+ const sprint = pipeline.workflowSprintLeads || {};
139
+ const byStatus = sprint.byStatus || {};
140
+ const lifecycleQualifiedCount = [
141
+ 'qualified',
142
+ 'named_pilot',
143
+ 'proof_backed_run',
144
+ 'paid_team',
145
+ ].reduce((total, status) => total + nonNegativeInteger(byStatus[status]), 0);
146
+ const explicitCompleteCount = pipeline.completeWorkflowSprintIntakes?.total;
147
+ // Older production summaries used qualifiedWorkflowSprintLeads for form
148
+ // completeness. It is safe only as a completeness fallback, never as proof
149
+ // that an operator qualified the lead.
150
+ const completeCount = explicitCompleteCount === undefined
151
+ ? pipeline.qualifiedWorkflowSprintLeads?.total
152
+ : explicitCompleteCount;
153
+ const diagnosticCheckoutStarts = nonNegativeInteger(ctas.diagnosticCheckoutStarts);
154
+ const intakeCloseQueueAvailable = Boolean(
155
+ billing.intakeQueue
156
+ && typeof billing.intakeQueue === 'object'
157
+ && !Array.isArray(billing.intakeQueue)
158
+ );
159
+ const checkoutStarts = Math.max(
160
+ nonNegativeInteger(traffic.checkoutStarts ?? ctas.checkoutStarts),
161
+ diagnosticCheckoutStarts
162
+ );
163
+ return {
164
+ sourceGeneratedAt: parseTimestamp(billing.generatedAt, 'billing.generatedAt'),
165
+ fields: {
166
+ observedFunnelAvailable: true,
167
+ observedFunnelAvailableValue: 1,
168
+ observedVisitors: nonNegativeInteger(traffic.visitors),
169
+ observedPageViews: nonNegativeInteger(traffic.pageViews),
170
+ observedCtaClicks: nonNegativeInteger(traffic.ctaClicks ?? ctas.totalClicks),
171
+ observedCheckoutStarts: checkoutStarts,
172
+ observedDiagnosticCheckoutStarts: diagnosticCheckoutStarts,
173
+ observedIntakeCloseQueueAvailable: intakeCloseQueueAvailable,
174
+ observedIntakeCloseQueueAvailableValue: intakeCloseQueueAvailable ? 1 : 0,
175
+ observedApprovalReadyIntakeCount: intakeCloseQueueAvailable
176
+ ? nonNegativeInteger(billing.intakeQueue.approvalReadyTotal)
177
+ : 0,
178
+ observedDiscoveryReadyIntakeCount: intakeCloseQueueAvailable
179
+ ? nonNegativeInteger(billing.intakeQueue.discoveryReadyTotal)
180
+ : 0,
181
+ observedWorkflowIntakeClicks: nonNegativeInteger(ctas.workflowIntakeClicks),
182
+ observedWorkflowSprintIntakeCount: nonNegativeInteger(sprint.total),
183
+ observedCompleteWorkflowSprintIntakeCount: nonNegativeInteger(completeCount),
184
+ observedLifecycleQualifiedWorkflowSprintLeadCount: lifecycleQualifiedCount,
185
+ },
186
+ };
187
+ }
188
+
189
+ function buildStripeCatalogFields(catalogAudit) {
190
+ if (catalogAudit === undefined || catalogAudit === null) {
191
+ return {
192
+ sourceGeneratedAt: null,
193
+ fields: {
194
+ stripeCatalogAttached: false,
195
+ stripeCatalogAttachedValue: 0,
196
+ stripeCatalogVerified: false,
197
+ stripeCatalogVerifiedValue: 0,
198
+ stripeCatalogVersion: null,
199
+ stripeCatalogExpectedOfferCount: 0,
200
+ stripeCatalogVerifiedOfferCount: 0,
201
+ stripeCatalogPriceDriftCount: 0,
202
+ stripeCatalogExpectedPublicPaymentRailCount: 0,
203
+ stripeCatalogVerifiedPublicPaymentRailCount: 0,
204
+ stripeCatalogPaymentRailDriftCount: 0,
205
+ },
206
+ };
207
+ }
208
+ if (!catalogAudit || typeof catalogAudit !== 'object' || Array.isArray(catalogAudit)) {
209
+ throw new Error('Stripe catalog audit must be a JSON object.');
210
+ }
211
+ if (catalogAudit.schemaVersion !== 1 || typeof catalogAudit.verified !== 'boolean') {
212
+ throw new Error('Stripe catalog audit is missing its schema version or verification verdict.');
213
+ }
214
+ const summary = catalogAudit.summary;
215
+ if (!summary || typeof summary !== 'object' || Array.isArray(summary)) {
216
+ throw new Error('Stripe catalog audit is missing its aggregate summary.');
217
+ }
218
+ for (const key of [
219
+ 'expectedOfferCount', 'verifiedOfferCount', 'priceDriftCount',
220
+ 'expectedPublicPaymentRailCount', 'verifiedPublicPaymentRailCount',
221
+ 'paymentRailDriftCount',
222
+ ]) {
223
+ if (!Number.isSafeInteger(summary[key]) || summary[key] < 0) {
224
+ throw new Error(`Stripe catalog audit summary.${key} must be a non-negative integer.`);
225
+ }
226
+ }
227
+ const expectedOfferCount = nonNegativeInteger(summary.expectedOfferCount);
228
+ const verifiedOfferCount = nonNegativeInteger(summary.verifiedOfferCount);
229
+ const priceDriftCount = nonNegativeInteger(summary.priceDriftCount);
230
+ const expectedPublicPaymentRailCount = nonNegativeInteger(summary.expectedPublicPaymentRailCount);
231
+ const verifiedPublicPaymentRailCount = nonNegativeInteger(summary.verifiedPublicPaymentRailCount);
232
+ const paymentRailDriftCount = nonNegativeInteger(summary.paymentRailDriftCount);
233
+ if (verifiedOfferCount > expectedOfferCount
234
+ || priceDriftCount !== expectedOfferCount - verifiedOfferCount
235
+ || verifiedPublicPaymentRailCount > expectedPublicPaymentRailCount
236
+ || paymentRailDriftCount !== expectedPublicPaymentRailCount - verifiedPublicPaymentRailCount) {
237
+ throw new Error('Stripe catalog audit summary does not reconcile expected, verified, and drift counts.');
238
+ }
239
+ const computedVerified = expectedOfferCount > 0
240
+ && expectedPublicPaymentRailCount > 0
241
+ && priceDriftCount === 0
242
+ && paymentRailDriftCount === 0;
243
+ if (catalogAudit.verified !== computedVerified) {
244
+ throw new Error('Stripe catalog audit verdict does not reconcile with its aggregate drift counts.');
245
+ }
246
+ const catalogVersion = normalizeSafeToken(catalogAudit.catalogVersion, 80);
247
+ if (!catalogVersion) throw new Error('Stripe catalog audit requires a safe catalog version.');
248
+ return {
249
+ sourceGeneratedAt: parseTimestamp(catalogAudit.generatedAt, 'stripeCatalog.generatedAt'),
250
+ fields: {
251
+ stripeCatalogAttached: true,
252
+ stripeCatalogAttachedValue: 1,
253
+ stripeCatalogVerified: catalogAudit.verified,
254
+ stripeCatalogVerifiedValue: catalogAudit.verified ? 1 : 0,
255
+ stripeCatalogVersion: catalogVersion,
256
+ stripeCatalogExpectedOfferCount: expectedOfferCount,
257
+ stripeCatalogVerifiedOfferCount: verifiedOfferCount,
258
+ stripeCatalogPriceDriftCount: priceDriftCount,
259
+ stripeCatalogExpectedPublicPaymentRailCount: expectedPublicPaymentRailCount,
260
+ stripeCatalogVerifiedPublicPaymentRailCount: verifiedPublicPaymentRailCount,
261
+ stripeCatalogPaymentRailDriftCount: paymentRailDriftCount,
262
+ },
263
+ };
264
+ }
265
+
266
+ function buildRevenueEvidenceSnapshot({
267
+ target,
268
+ remediation,
269
+ billing,
270
+ stripeCatalog,
271
+ targetDigest,
272
+ remediationDigest,
273
+ billingDigest,
274
+ stripeCatalogDigest,
275
+ generatedAt = new Date().toISOString(),
276
+ } = {}) {
277
+ if (!target || typeof target !== 'object' || Array.isArray(target)) {
278
+ throw new Error('Target-control evidence must be a JSON object.');
279
+ }
280
+ if (!remediation || typeof remediation !== 'object' || Array.isArray(remediation)) {
281
+ throw new Error('Remediation evidence must be a JSON object.');
282
+ }
283
+ if (!target.evidence?.pipeline || !target.evidence?.deployment || !target.evidence?.providerCoverage) {
284
+ throw new Error('Target-control evidence is missing pipeline, deployment, or provider coverage.');
285
+ }
286
+ if (!remediation.summary || typeof remediation.summary !== 'object') {
287
+ throw new Error('Remediation evidence is missing summary.');
288
+ }
289
+
290
+ const snapshotGeneratedAt = parseTimestamp(generatedAt, 'generatedAt');
291
+ const targetGeneratedAt = parseTimestamp(target.generatedAt, 'target.generatedAt');
292
+ const remediationGeneratedAt = parseTimestamp(remediation.generatedAt, 'remediation.generatedAt');
293
+ const pipeline = target.evidence.pipeline;
294
+ const deployment = target.evidence.deployment;
295
+ const providerCoverage = target.evidence.providerCoverage;
296
+ const firstPayment = target.milestones?.firstExternalPayment;
297
+ const sameDayPayment = target.milestones?.sameDayExternalPayment;
298
+ const targetMilestone = target.milestones?.targetWithCurrentControlDeployed;
299
+ const summary = remediation.summary;
300
+ const bookedRevenueCents = nonNegativeInteger(pipeline.bookedRevenueCents);
301
+ const observedFunnel = buildObservedFunnelFields(billing);
302
+ const observedStripeCatalog = buildStripeCatalogFields(stripeCatalog);
303
+
304
+ return {
305
+ schemaVersion: SCHEMA_VERSION,
306
+ event: 'revenue_evidence',
307
+ generatedAt: snapshotGeneratedAt,
308
+ sourceGeneratedAt: {
309
+ target: targetGeneratedAt,
310
+ remediation: remediationGeneratedAt,
311
+ billing: observedFunnel.sourceGeneratedAt,
312
+ stripeCatalog: observedStripeCatalog.sourceGeneratedAt,
313
+ },
314
+ sourceDigests: {
315
+ targetSha256: normalizeSha256(targetDigest),
316
+ remediationSha256: normalizeSha256(remediationDigest),
317
+ billingSha256: normalizeSha256(billingDigest),
318
+ stripeCatalogSha256: normalizeSha256(stripeCatalogDigest),
319
+ },
320
+ claimBoundary: CLAIM_BOUNDARY,
321
+ targetStatus: normalizeSafeToken(target.status, 80) || 'unknown',
322
+ targetAchieved: booleanValue(targetMilestone?.achieved),
323
+ targetAchievedValue: booleanValue(targetMilestone?.achieved) ? 1 : 0,
324
+ firstExternalPayment: booleanValue(firstPayment?.achieved),
325
+ firstExternalPaymentValue: booleanValue(firstPayment?.achieved) ? 1 : 0,
326
+ sameDayExternalPayment: booleanValue(sameDayPayment?.achieved),
327
+ sameDayExternalPaymentValue: booleanValue(sameDayPayment?.achieved) ? 1 : 0,
328
+ verifiedIndividualPaymentCount: nonNegativeInteger(firstPayment?.verifiedIndividualPaymentCount),
329
+ verifiedPaid: nonNegativeInteger(pipeline.verifiedPaid),
330
+ bookedRevenueCents,
331
+ bookedRevenueDollars: bookedRevenueCents / 100,
332
+ pipelineTotal: nonNegativeInteger(pipeline.total),
333
+ evidenceGapCount: nonNegativeInteger(pipeline.evidenceGapCount),
334
+ sameDayEvidencePriority: nonNegativeInteger(summary.sameDayEvidencePriority),
335
+ sameDayReviewPriority: nonNegativeInteger(summary.sameDayReviewPriority),
336
+ approvalReady: nonNegativeInteger(summary.approvalReady),
337
+ internalReady: nonNegativeInteger(summary.internalReady),
338
+ providerCoverageComplete: booleanValue(providerCoverage.completeForGlobalClaim),
339
+ providerCoverageCompleteValue: booleanValue(providerCoverage.completeForGlobalClaim) ? 1 : 0,
340
+ productionHealthy: booleanValue(deployment.healthy),
341
+ productionHealthyValue: booleanValue(deployment.healthy) ? 1 : 0,
342
+ expectedRevisionDeployed: booleanValue(deployment.expectedRevisionDeployed),
343
+ expectedRevisionDeployedValue: booleanValue(deployment.expectedRevisionDeployed) ? 1 : 0,
344
+ expectedSha: normalizeGitRevision(deployment.expectedSha),
345
+ deployedSha: normalizeGitRevision(deployment.deployedSha),
346
+ ...observedFunnel.fields,
347
+ ...observedStripeCatalog.fields,
348
+ };
349
+ }
350
+
351
+ function validateRevenueEvidenceSnapshot(snapshot) {
352
+ if (!snapshot || snapshot.schemaVersion !== SCHEMA_VERSION || snapshot.event !== 'revenue_evidence') {
353
+ throw new Error('A valid revenue evidence snapshot is required.');
354
+ }
355
+ const unknownKeys = Object.keys(snapshot).filter((key) => !SNAPSHOT_KEYS.has(key));
356
+ if (unknownKeys.length) {
357
+ throw new Error(`Revenue evidence snapshot contains non-aggregate fields: ${unknownKeys.join(', ')}.`);
358
+ }
359
+ if (snapshot.claimBoundary !== CLAIM_BOUNDARY) {
360
+ throw new Error('Revenue evidence snapshot must retain the fixed claim boundary.');
361
+ }
362
+ parseTimestamp(snapshot.generatedAt, 'snapshot.generatedAt');
363
+ if (snapshot.targetStatus !== normalizeSafeToken(snapshot.targetStatus, 80)) {
364
+ throw new Error('Revenue evidence snapshot targetStatus must be a safe token.');
365
+ }
366
+ for (const [label, value] of [
367
+ ['expectedSha', snapshot.expectedSha],
368
+ ['deployedSha', snapshot.deployedSha],
369
+ ]) {
370
+ if (value !== null && value !== normalizeGitRevision(value)) {
371
+ throw new Error(`Revenue evidence snapshot ${label} must be a Git revision or null.`);
372
+ }
373
+ }
374
+ for (const key of [
375
+ 'expectedRevisionDeployed', 'firstExternalPayment', 'observedFunnelAvailable',
376
+ 'observedIntakeCloseQueueAvailable',
377
+ 'productionHealthy', 'providerCoverageComplete', 'sameDayExternalPayment',
378
+ 'stripeCatalogAttached', 'stripeCatalogVerified', 'targetAchieved',
379
+ ]) {
380
+ if (typeof snapshot[key] !== 'boolean') {
381
+ throw new Error(`Revenue evidence snapshot ${key} must be boolean.`);
382
+ }
383
+ }
384
+ for (const key of [
385
+ 'approvalReady', 'bookedRevenueCents', 'bookedRevenueDollars',
386
+ 'evidenceGapCount', 'expectedRevisionDeployedValue',
387
+ 'firstExternalPaymentValue', 'internalReady', 'observedCheckoutStarts',
388
+ 'observedApprovalReadyIntakeCount',
389
+ 'observedDiscoveryReadyIntakeCount',
390
+ 'observedCompleteWorkflowSprintIntakeCount', 'observedCtaClicks',
391
+ 'observedDiagnosticCheckoutStarts', 'observedFunnelAvailableValue',
392
+ 'observedIntakeCloseQueueAvailableValue',
393
+ 'observedLifecycleQualifiedWorkflowSprintLeadCount', 'observedPageViews',
394
+ 'observedVisitors', 'observedWorkflowIntakeClicks',
395
+ 'observedWorkflowSprintIntakeCount', 'pipelineTotal',
396
+ 'productionHealthyValue', 'providerCoverageCompleteValue',
397
+ 'sameDayEvidencePriority', 'sameDayExternalPaymentValue',
398
+ 'sameDayReviewPriority', 'stripeCatalogAttachedValue',
399
+ 'stripeCatalogExpectedOfferCount', 'stripeCatalogExpectedPublicPaymentRailCount',
400
+ 'stripeCatalogPaymentRailDriftCount', 'stripeCatalogPriceDriftCount',
401
+ 'stripeCatalogVerifiedOfferCount', 'stripeCatalogVerifiedPublicPaymentRailCount',
402
+ 'stripeCatalogVerifiedValue', 'targetAchievedValue',
403
+ 'verifiedIndividualPaymentCount', 'verifiedPaid',
404
+ ]) {
405
+ if (!Number.isFinite(snapshot[key]) || snapshot[key] < 0) {
406
+ throw new Error(`Revenue evidence snapshot ${key} must be a non-negative number.`);
407
+ }
408
+ }
409
+ if (snapshot.observedDiagnosticCheckoutStarts > snapshot.observedCheckoutStarts) {
410
+ throw new Error('Revenue evidence snapshot diagnostic checkout starts must be a subset of checkout starts.');
411
+ }
412
+ if (snapshot.stripeCatalogVersion !== null
413
+ && snapshot.stripeCatalogVersion !== normalizeSafeToken(snapshot.stripeCatalogVersion, 80)) {
414
+ throw new Error('Revenue evidence snapshot stripeCatalogVersion must be a safe token or null.');
415
+ }
416
+ if (snapshot.stripeCatalogVerifiedOfferCount > snapshot.stripeCatalogExpectedOfferCount
417
+ || snapshot.stripeCatalogPriceDriftCount
418
+ !== snapshot.stripeCatalogExpectedOfferCount - snapshot.stripeCatalogVerifiedOfferCount
419
+ || snapshot.stripeCatalogVerifiedPublicPaymentRailCount > snapshot.stripeCatalogExpectedPublicPaymentRailCount
420
+ || snapshot.stripeCatalogPaymentRailDriftCount
421
+ !== snapshot.stripeCatalogExpectedPublicPaymentRailCount - snapshot.stripeCatalogVerifiedPublicPaymentRailCount) {
422
+ throw new Error('Revenue evidence snapshot Stripe catalog counts do not reconcile.');
423
+ }
424
+ if (snapshot.stripeCatalogVerified
425
+ !== (snapshot.stripeCatalogAttached
426
+ && snapshot.stripeCatalogExpectedOfferCount > 0
427
+ && snapshot.stripeCatalogExpectedPublicPaymentRailCount > 0
428
+ && snapshot.stripeCatalogPriceDriftCount === 0
429
+ && snapshot.stripeCatalogPaymentRailDriftCount === 0)) {
430
+ throw new Error('Revenue evidence snapshot Stripe catalog verdict does not reconcile.');
431
+ }
432
+ for (const [label, value, allowed] of [
433
+ ['sourceGeneratedAt', snapshot.sourceGeneratedAt, SOURCE_GENERATED_AT_KEYS],
434
+ ['sourceDigests', snapshot.sourceDigests, SOURCE_DIGEST_KEYS],
435
+ ]) {
436
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
437
+ throw new Error(`Revenue evidence snapshot ${label} must be an object.`);
438
+ }
439
+ const unknownNestedKeys = Object.keys(value).filter((key) => !allowed.has(key));
440
+ if (unknownNestedKeys.length) {
441
+ throw new Error(`Revenue evidence snapshot ${label} contains non-aggregate fields: ${unknownNestedKeys.join(', ')}.`);
442
+ }
443
+ }
444
+ for (const [key, value] of Object.entries(snapshot.sourceGeneratedAt)) {
445
+ if (value !== null) parseTimestamp(value, `snapshot.sourceGeneratedAt.${key}`);
446
+ }
447
+ for (const [key, value] of Object.entries(snapshot.sourceDigests)) {
448
+ if (value !== null && value !== normalizeSha256(value)) {
449
+ throw new Error(`Revenue evidence snapshot sourceDigests.${key} must be SHA-256 or null.`);
450
+ }
451
+ }
452
+ return snapshot;
453
+ }
454
+
455
+ function buildLokiPayload(snapshot, { timestamp = snapshot?.generatedAt } = {}) {
456
+ validateRevenueEvidenceSnapshot(snapshot);
457
+ const timestampIso = parseTimestamp(timestamp, 'Loki timestamp');
458
+ const timestampNs = String(BigInt(Date.parse(timestampIso)) * 1000000n);
459
+ return {
460
+ streams: [{
461
+ stream: {
462
+ service_name: 'thumbgate',
463
+ event: 'revenue_evidence',
464
+ environment: 'production',
465
+ schema_version: String(SCHEMA_VERSION),
466
+ },
467
+ values: [[timestampNs, JSON.stringify(snapshot)]],
468
+ }],
469
+ };
470
+ }
471
+
472
+ function validateRevenueEvidenceLokiPayload(payload) {
473
+ if (!payload || !Array.isArray(payload.streams) || payload.streams.length !== 1) {
474
+ throw new Error('Grafana payload must contain exactly one revenue evidence stream.');
475
+ }
476
+ const stream = payload.streams[0];
477
+ if (stream?.stream?.service_name !== 'thumbgate' || stream?.stream?.event !== 'revenue_evidence') {
478
+ throw new Error('Grafana payload must use the fixed ThumbGate revenue evidence labels.');
479
+ }
480
+ if (!Array.isArray(stream.values) || stream.values.length !== 1 || !Array.isArray(stream.values[0])) {
481
+ throw new Error('Grafana payload must contain exactly one revenue evidence log line.');
482
+ }
483
+ let snapshot;
484
+ try {
485
+ snapshot = JSON.parse(stream.values[0][1]);
486
+ } catch {
487
+ throw new Error('Grafana payload revenue evidence log line must be valid JSON.');
488
+ }
489
+ validateRevenueEvidenceSnapshot(snapshot);
490
+ return payload;
491
+ }
492
+
493
+ function validateLokiEndpoint(value) {
494
+ let parsed;
495
+ try {
496
+ parsed = new URL(String(value || '').trim());
497
+ } catch {
498
+ throw new Error('Grafana Loki URL must be an absolute HTTPS URL.');
499
+ }
500
+ if (parsed.protocol !== 'https:') throw new Error('Grafana Loki URL must use HTTPS.');
501
+ if (parsed.port && parsed.port !== '443') throw new Error('Grafana Loki URL must use the default HTTPS port.');
502
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
503
+ throw new Error('Grafana Loki URL must not contain credentials, query parameters, or fragments.');
504
+ }
505
+ if (!LOKI_HOST_PATTERN.test(parsed.hostname)) {
506
+ throw new Error('Grafana Loki host must be grafana.net or one of its subdomains.');
507
+ }
508
+ if (parsed.pathname.replace(/\/+$/, '') !== LOKI_PUSH_PATH) {
509
+ throw new Error(`Grafana Loki URL must end with ${LOKI_PUSH_PATH}.`);
510
+ }
511
+ parsed.hash = '';
512
+ parsed.search = '';
513
+ return parsed;
514
+ }
515
+
516
+ async function sendLokiPayload(payload, {
517
+ endpoint,
518
+ userId,
519
+ token,
520
+ fetchImpl = global.fetch,
521
+ timeoutMs = 15000,
522
+ } = {}) {
523
+ validateRevenueEvidenceLokiPayload(payload);
524
+ const parsed = validateLokiEndpoint(endpoint);
525
+ const normalizedUserId = normalizeText(userId, 160);
526
+ const normalizedToken = normalizeText(token, 4096);
527
+ if (!normalizedUserId || !normalizedToken) {
528
+ throw new Error('Grafana Loki user ID and access-policy token are required.');
529
+ }
530
+ if (/[:\r\n]/.test(normalizedUserId)) {
531
+ throw new Error('Grafana Loki user ID must not contain a colon or line break.');
532
+ }
533
+ if (typeof fetchImpl !== 'function') throw new Error('A fetch implementation is required.');
534
+
535
+ const body = JSON.stringify(payload);
536
+ const response = await fetchImpl(parsed, {
537
+ method: 'POST',
538
+ headers: {
539
+ authorization: `Basic ${Buffer.from(`${normalizedUserId}:${normalizedToken}`).toString('base64')}`,
540
+ 'content-type': 'application/json',
541
+ 'user-agent': 'thumbgate-revenue-evidence/1',
542
+ },
543
+ body,
544
+ redirect: 'error',
545
+ signal: AbortSignal.timeout(Math.max(1000, nonNegativeInteger(timeoutMs, 15000))),
546
+ });
547
+ if (!response?.ok) {
548
+ throw new Error(`Grafana Loki rejected the aggregate evidence payload with HTTP ${response?.status || 'unknown'}.`);
549
+ }
550
+ return {
551
+ sent: true,
552
+ httpStatus: response.status,
553
+ endpointHost: parsed.hostname,
554
+ payloadSha256: sha256(body),
555
+ };
556
+ }
557
+
558
+ function buildStatPanel({ id, title, field, x, y, unit = 'short', boolean = false }) {
559
+ const panel = {
560
+ id,
561
+ type: 'stat',
562
+ title,
563
+ datasource: { type: 'loki', uid: '${DS_LOKI}' },
564
+ gridPos: { h: 5, w: 6, x, y },
565
+ fieldConfig: {
566
+ defaults: {
567
+ unit,
568
+ color: { mode: 'thresholds' },
569
+ thresholds: {
570
+ mode: 'absolute',
571
+ steps: boolean
572
+ ? [{ color: 'red', value: null }, { color: 'green', value: 1 }]
573
+ : [{ color: 'green', value: null }],
574
+ },
575
+ },
576
+ overrides: [],
577
+ },
578
+ options: {
579
+ colorMode: 'background',
580
+ graphMode: 'none',
581
+ justifyMode: 'auto',
582
+ orientation: 'auto',
583
+ reduceOptions: { calcs: ['lastNotNull'], fields: '', values: false },
584
+ textMode: 'auto',
585
+ wideLayout: true,
586
+ },
587
+ targets: [{
588
+ refId: 'A',
589
+ datasource: { type: 'loki', uid: '${DS_LOKI}' },
590
+ editorMode: 'code',
591
+ expr: `last_over_time({service_name="thumbgate", event="revenue_evidence"} | json | unwrap ${field} [$__range])`,
592
+ queryType: 'instant',
593
+ }],
594
+ };
595
+ if (boolean) {
596
+ panel.fieldConfig.defaults.mappings = [{
597
+ type: 'value',
598
+ options: {
599
+ 0: { color: 'red', index: 0, text: 'NO' },
600
+ 1: { color: 'green', index: 1, text: 'YES' },
601
+ },
602
+ }];
603
+ }
604
+ return panel;
605
+ }
606
+
607
+ function buildGrafanaDashboard({ uid = DEFAULT_DASHBOARD_UID } = {}) {
608
+ return {
609
+ annotations: { list: [] },
610
+ editable: true,
611
+ fiscalYearStartMonth: 0,
612
+ graphTooltip: 1,
613
+ id: null,
614
+ links: [],
615
+ liveNow: false,
616
+ panels: [
617
+ buildStatPanel({ id: 1, title: 'First external payment', field: 'firstExternalPaymentValue', x: 0, y: 0, boolean: true }),
618
+ buildStatPanel({ id: 2, title: 'Paid today', field: 'sameDayExternalPaymentValue', x: 6, y: 0, boolean: true }),
619
+ buildStatPanel({ id: 3, title: 'Booked revenue', field: 'bookedRevenueDollars', x: 12, y: 0, unit: 'currencyUSD' }),
620
+ buildStatPanel({ id: 4, title: '$1k/hour control achieved', field: 'targetAchievedValue', x: 18, y: 0, boolean: true }),
621
+ buildStatPanel({ id: 5, title: 'Verified paid rows', field: 'verifiedPaid', x: 0, y: 5 }),
622
+ buildStatPanel({ id: 6, title: 'Pipeline evidence gaps', field: 'evidenceGapCount', x: 6, y: 5 }),
623
+ buildStatPanel({ id: 7, title: 'Verified same-day signals', field: 'sameDayEvidencePriority', x: 12, y: 5 }),
624
+ buildStatPanel({ id: 8, title: 'Review-only signals', field: 'sameDayReviewPriority', x: 18, y: 5 }),
625
+ buildStatPanel({ id: 9, title: 'Approval-ready actions', field: 'approvalReady', x: 0, y: 10 }),
626
+ buildStatPanel({ id: 10, title: 'Provider coverage complete', field: 'providerCoverageCompleteValue', x: 6, y: 10, boolean: true }),
627
+ buildStatPanel({ id: 11, title: 'Production healthy', field: 'productionHealthyValue', x: 12, y: 10, boolean: true }),
628
+ buildStatPanel({ id: 12, title: 'Candidate deployed', field: 'expectedRevisionDeployedValue', x: 18, y: 10, boolean: true }),
629
+ buildStatPanel({ id: 15, title: 'Observed visitors', field: 'observedVisitors', x: 0, y: 15 }),
630
+ buildStatPanel({ id: 16, title: 'Observed CTA clicks', field: 'observedCtaClicks', x: 6, y: 15 }),
631
+ buildStatPanel({ id: 17, title: 'Observed checkout starts', field: 'observedCheckoutStarts', x: 12, y: 15 }),
632
+ buildStatPanel({ id: 18, title: 'Observed diagnostic starts', field: 'observedDiagnosticCheckoutStarts', x: 18, y: 15 }),
633
+ buildStatPanel({ id: 19, title: 'Observed sprint intakes', field: 'observedWorkflowSprintIntakeCount', x: 0, y: 20 }),
634
+ buildStatPanel({ id: 20, title: 'Observed complete intakes', field: 'observedCompleteWorkflowSprintIntakeCount', x: 6, y: 20 }),
635
+ buildStatPanel({ id: 21, title: 'Lifecycle-qualified leads', field: 'observedLifecycleQualifiedWorkflowSprintLeadCount', x: 12, y: 20 }),
636
+ buildStatPanel({ id: 22, title: 'Hosted funnel attached', field: 'observedFunnelAvailableValue', x: 18, y: 20, boolean: true }),
637
+ buildStatPanel({ id: 23, title: 'Reviewed intake close queue attached', field: 'observedIntakeCloseQueueAvailableValue', x: 0, y: 25, boolean: true }),
638
+ buildStatPanel({ id: 24, title: 'Approval-ready reviewed intakes', field: 'observedApprovalReadyIntakeCount', x: 6, y: 25 }),
639
+ buildStatPanel({ id: 25, title: 'Discovery replies awaiting approval', field: 'observedDiscoveryReadyIntakeCount', x: 12, y: 25 }),
640
+ buildStatPanel({ id: 26, title: 'Stripe catalog attached', field: 'stripeCatalogAttachedValue', x: 0, y: 30, boolean: true }),
641
+ buildStatPanel({ id: 27, title: 'Stripe catalog verified', field: 'stripeCatalogVerifiedValue', x: 6, y: 30, boolean: true }),
642
+ buildStatPanel({ id: 28, title: 'Stripe price drift', field: 'stripeCatalogPriceDriftCount', x: 12, y: 30 }),
643
+ buildStatPanel({ id: 29, title: 'Stripe payment-rail drift', field: 'stripeCatalogPaymentRailDriftCount', x: 18, y: 30 }),
644
+ {
645
+ id: 13,
646
+ type: 'logs',
647
+ title: 'Revenue evidence snapshots',
648
+ datasource: { type: 'loki', uid: '${DS_LOKI}' },
649
+ gridPos: { h: 10, w: 24, x: 0, y: 35 },
650
+ options: {
651
+ dedupStrategy: 'signature',
652
+ enableLogDetails: true,
653
+ prettifyLogMessage: true,
654
+ showCommonLabels: false,
655
+ showLabels: false,
656
+ showTime: true,
657
+ sortOrder: 'Descending',
658
+ wrapLogMessage: true,
659
+ },
660
+ targets: [{
661
+ refId: 'A',
662
+ datasource: { type: 'loki', uid: '${DS_LOKI}' },
663
+ editorMode: 'code',
664
+ expr: '{service_name="thumbgate", event="revenue_evidence"} | json',
665
+ queryType: 'range',
666
+ }],
667
+ },
668
+ {
669
+ id: 14,
670
+ type: 'text',
671
+ title: 'Claim boundary',
672
+ gridPos: { h: 5, w: 24, x: 0, y: 45 },
673
+ options: {
674
+ mode: 'markdown',
675
+ content: `**${CLAIM_BOUNDARY}**\n\nObserved funnel panels are first-party activity counts, not payment proof. A lead is lifecycle-qualified only after its stored status reaches \`qualified\` or later. Approval-ready reviewed intakes are aggregate operator actions, not authorization, outreach, payment, or revenue. Stripe catalog verification proves exact offer and public Payment Link configuration, not a purchase. Zero-spend rule: keep Grafana Cloud on the free-forever plan and use aggregate fields only.`,
676
+ },
677
+ },
678
+ ],
679
+ refresh: '5m',
680
+ schemaVersion: 41,
681
+ tags: ['thumbgate', 'revenue', 'evidence'],
682
+ templating: {
683
+ list: [{
684
+ name: 'DS_LOKI',
685
+ type: 'datasource',
686
+ label: 'Grafana Cloud Logs',
687
+ query: 'loki',
688
+ current: {},
689
+ hide: 0,
690
+ includeAll: false,
691
+ multi: false,
692
+ options: [],
693
+ refresh: 1,
694
+ regex: '',
695
+ skipUrlSync: false,
696
+ }],
697
+ },
698
+ time: { from: 'now-24h', to: 'now' },
699
+ timepicker: {},
700
+ timezone: 'browser',
701
+ title: 'ThumbGate Revenue Evidence',
702
+ uid,
703
+ version: 1,
704
+ weekStart: '',
705
+ };
706
+ }
707
+
708
+ function parseArgs(argv = []) {
709
+ const options = { send: false, dashboard: false };
710
+ for (let index = 0; index < argv.length; index += 1) {
711
+ const arg = argv[index];
712
+ if (arg === '--send') {
713
+ options.send = true;
714
+ continue;
715
+ }
716
+ if (arg === '--dashboard') {
717
+ options.dashboard = true;
718
+ continue;
719
+ }
720
+ if (!arg.startsWith('--')) continue;
721
+ const separator = arg.indexOf('=');
722
+ const rawKey = separator === -1 ? arg.slice(2) : arg.slice(2, separator);
723
+ const key = rawKey.replaceAll(/-([a-z])/g, (_, letter) => letter.toUpperCase());
724
+ if (separator !== -1) {
725
+ options[key] = arg.slice(separator + 1);
726
+ continue;
727
+ }
728
+ if (argv[index + 1] && !argv[index + 1].startsWith('--')) {
729
+ options[key] = argv[index + 1];
730
+ index += 1;
731
+ } else {
732
+ options[key] = true;
733
+ }
734
+ }
735
+ return options;
736
+ }
737
+
738
+ function writeJson(outPath, value) {
739
+ if (!outPath) return null;
740
+ const resolved = path.resolve(outPath);
741
+ ensureParentDir(resolved);
742
+ fs.writeFileSync(resolved, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
743
+ return resolved;
744
+ }
745
+
746
+ async function runCli(argv = process.argv.slice(2), {
747
+ env = process.env,
748
+ fetchImpl = global.fetch,
749
+ } = {}) {
750
+ const options = parseArgs(argv);
751
+ if (options.dashboard) {
752
+ const dashboard = buildGrafanaDashboard({ uid: options.uid || DEFAULT_DASHBOARD_UID });
753
+ const outputPath = writeJson(options.out, dashboard);
754
+ return {
755
+ status: 'dashboard_prepared_not_published',
756
+ dashboard,
757
+ outputPath,
758
+ claimBoundary: CLAIM_BOUNDARY,
759
+ };
760
+ }
761
+
762
+ const targetFile = readJsonFile(options.target, 'Target-control evidence');
763
+ const remediationFile = readJsonFile(options.remediation, 'Remediation evidence');
764
+ const billingFile = options.billing
765
+ ? readJsonFile(options.billing, 'Hosted billing evidence')
766
+ : null;
767
+ const stripeCatalogFile = options.stripeCatalog
768
+ ? readJsonFile(options.stripeCatalog, 'Stripe catalog audit')
769
+ : null;
770
+ const snapshot = buildRevenueEvidenceSnapshot({
771
+ target: targetFile.value,
772
+ remediation: remediationFile.value,
773
+ billing: billingFile?.value,
774
+ stripeCatalog: stripeCatalogFile?.value,
775
+ targetDigest: targetFile.digest,
776
+ remediationDigest: remediationFile.digest,
777
+ billingDigest: billingFile?.digest,
778
+ stripeCatalogDigest: stripeCatalogFile?.digest,
779
+ generatedAt: options.now || new Date().toISOString(),
780
+ });
781
+ const payload = buildLokiPayload(snapshot);
782
+ const payloadSha256 = sha256(JSON.stringify(payload));
783
+ const outputPath = writeJson(options.out, { snapshot, lokiPayload: payload });
784
+ let delivery = null;
785
+ if (options.send) {
786
+ if (env.THUMBGATE_GRAFANA_ZERO_SPEND_CONFIRMED !== '1') {
787
+ throw new Error('Set THUMBGATE_GRAFANA_ZERO_SPEND_CONFIRMED=1 only after confirming the stack remains on the free plan.');
788
+ }
789
+ delivery = await sendLokiPayload(payload, {
790
+ endpoint: options.endpoint || env.THUMBGATE_GRAFANA_LOKI_URL,
791
+ userId: options.userId || env.THUMBGATE_GRAFANA_LOKI_USER_ID,
792
+ token: env.THUMBGATE_GRAFANA_LOKI_TOKEN,
793
+ fetchImpl,
794
+ });
795
+ }
796
+ return {
797
+ status: delivery ? 'sent' : 'prepared_not_sent',
798
+ snapshot,
799
+ payloadSha256,
800
+ outputPath,
801
+ delivery,
802
+ claimBoundary: CLAIM_BOUNDARY,
803
+ };
804
+ }
805
+
806
+ function isCliInvocation(argv = process.argv) {
807
+ const invokedPath = argv[1];
808
+ if (!invokedPath) return false;
809
+ try {
810
+ return fs.realpathSync(path.resolve(invokedPath)) === fs.realpathSync(__filename);
811
+ } catch {
812
+ return path.resolve(invokedPath) === path.resolve(__filename);
813
+ }
814
+ }
815
+
816
+ if (isCliInvocation()) {
817
+ runCli().then((result) => {
818
+ const safeResult = {
819
+ status: result.status,
820
+ outputPath: result.outputPath,
821
+ payloadSha256: result.payloadSha256 || null,
822
+ delivery: result.delivery,
823
+ snapshot: result.snapshot || null,
824
+ dashboardUid: result.dashboard?.uid || null,
825
+ claimBoundary: result.claimBoundary,
826
+ };
827
+ process.stdout.write(`${JSON.stringify(safeResult, null, 2)}\n`);
828
+ }).catch((error) => {
829
+ console.error(error?.message || error);
830
+ process.exitCode = 1;
831
+ });
832
+ }
833
+
834
+ module.exports = {
835
+ CLAIM_BOUNDARY,
836
+ DEFAULT_DASHBOARD_UID,
837
+ LOKI_HOST_PATTERN,
838
+ LOKI_PUSH_PATH,
839
+ SCHEMA_VERSION,
840
+ buildGrafanaDashboard,
841
+ buildLokiPayload,
842
+ buildObservedFunnelFields,
843
+ buildStripeCatalogFields,
844
+ buildRevenueEvidenceSnapshot,
845
+ buildStatPanel,
846
+ isCliInvocation,
847
+ parseArgs,
848
+ readJsonFile,
849
+ runCli,
850
+ sendLokiPayload,
851
+ sha256,
852
+ validateLokiEndpoint,
853
+ validateRevenueEvidenceLokiPayload,
854
+ validateRevenueEvidenceSnapshot,
855
+ writeJson,
856
+ };