thumbgate 1.27.16 → 1.27.18

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 (165) hide show
  1. package/.claude-plugin/marketplace.json +85 -0
  2. package/.claude-plugin/plugin.json +3 -4
  3. package/.well-known/llms.txt +12 -33
  4. package/.well-known/mcp/server-card.json +8 -8
  5. package/README.md +30 -215
  6. package/adapters/chatgpt/openapi.yaml +1695 -0
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/gemini/function-declarations.json +0 -1
  9. package/adapters/mcp/server-stdio.js +11 -263
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bench/thumbgate-bench.json +2 -2
  12. package/bin/cli.js +122 -1421
  13. package/bin/postinstall.js +27 -11
  14. package/config/gate-templates.json +0 -216
  15. package/config/gates/claim-verification.json +0 -12
  16. package/config/gates/default.json +2 -31
  17. package/config/github-about.json +2 -2
  18. package/config/mcp-allowlists.json +13 -23
  19. package/config/merge-quality-checks.json +1 -0
  20. package/config/model-candidates.json +6 -121
  21. package/openapi/openapi.yaml +0 -12
  22. package/package.json +98 -228
  23. package/public/blog.html +4 -4
  24. package/public/codex-plugin.html +20 -72
  25. package/public/compare.html +8 -31
  26. package/public/dashboard.html +166 -930
  27. package/public/federal.html +2 -2
  28. package/public/guide.html +9 -33
  29. package/public/index.html +116 -495
  30. package/public/learn.html +18 -191
  31. package/public/lessons.html +10 -168
  32. package/public/numbers.html +7 -7
  33. package/public/pro.html +11 -34
  34. package/scripts/agent-memory-lifecycle.js +0 -211
  35. package/scripts/agent-readiness.js +3 -167
  36. package/scripts/agent-reward-model.js +1 -53
  37. package/scripts/auto-promote-gates.js +10 -82
  38. package/scripts/auto-wire-hooks.js +0 -14
  39. package/scripts/billing.js +1 -93
  40. package/scripts/build-metadata.js +10 -50
  41. package/scripts/cli-feedback.js +6 -18
  42. package/scripts/cli-schema.js +0 -90
  43. package/scripts/cli-telemetry.js +1 -6
  44. package/scripts/commercial-offer.js +2 -82
  45. package/scripts/context-manager.js +6 -74
  46. package/scripts/dashboard.js +2 -68
  47. package/scripts/durability/step.js +3 -3
  48. package/scripts/export-databricks-bundle.js +1 -5
  49. package/scripts/export-dpo-pairs.js +2 -7
  50. package/scripts/feedback-loop.js +1 -123
  51. package/scripts/feedback-quality.js +0 -87
  52. package/scripts/filesystem-search.js +10 -35
  53. package/scripts/gate-stats.js +0 -89
  54. package/scripts/gates-engine.js +85 -1105
  55. package/scripts/gemini-embedding-policy.js +1 -2
  56. package/scripts/hook-runtime.js +14 -20
  57. package/scripts/hook-thumbgate-cache-updater.js +2 -18
  58. package/scripts/hybrid-feedback-context.js +7 -142
  59. package/scripts/lesson-inference.js +3 -8
  60. package/scripts/lesson-search.js +1 -17
  61. package/scripts/license.js +1 -0
  62. package/scripts/llm-client.js +9 -390
  63. package/scripts/local-model-profile.js +8 -15
  64. package/scripts/memory-scope-readiness.js +0 -159
  65. package/scripts/meta-agent-loop.js +0 -36
  66. package/scripts/operational-integrity.js +5 -39
  67. package/scripts/oss-pr-opportunity-scout.js +5 -35
  68. package/scripts/plausible-server-events.js +6 -9
  69. package/scripts/pro-local-dashboard.js +4 -4
  70. package/scripts/proxy-pointer-rag-guardrails.js +1 -42
  71. package/scripts/rate-limiter.js +13 -64
  72. package/scripts/secret-scanner.js +5 -44
  73. package/scripts/security-scanner.js +10 -260
  74. package/scripts/self-distill-agent.js +1 -3
  75. package/scripts/seo-gsd.js +2 -916
  76. package/scripts/statusline-cache-path.js +2 -17
  77. package/scripts/statusline-local-stats.js +1 -9
  78. package/scripts/statusline-meta.js +2 -28
  79. package/scripts/statusline.sh +4 -20
  80. package/scripts/telemetry-analytics.js +0 -357
  81. package/scripts/thompson-sampling.js +10 -31
  82. package/scripts/thumbgate-bench.js +1 -16
  83. package/scripts/thumbgate-search.js +19 -85
  84. package/scripts/tool-registry.js +1 -169
  85. package/scripts/vector-store.js +0 -45
  86. package/scripts/workflow-sentinel.js +53 -286
  87. package/scripts/workspace-evolver.js +2 -62
  88. package/src/api/server.js +290 -2578
  89. package/.claude/commands/dashboard.md +0 -15
  90. package/.claude/commands/thumbgate-blocked.md +0 -27
  91. package/.claude/commands/thumbgate-dashboard.md +0 -15
  92. package/.claude/commands/thumbgate-doctor.md +0 -30
  93. package/.claude/commands/thumbgate-guard.md +0 -36
  94. package/.claude/commands/thumbgate-protect.md +0 -30
  95. package/.claude/commands/thumbgate-rules.md +0 -30
  96. package/.well-known/agentic-verify.txt +0 -1
  97. package/adapters/gcp/dfcx-webhook-gate.js +0 -295
  98. package/adapters/letta/README.md +0 -41
  99. package/adapters/letta/thumbgate-letta-adapter.js +0 -133
  100. package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
  101. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
  102. package/bench/observability-eval-suite.json +0 -26
  103. package/bin/dashboard-cli.js +0 -7
  104. package/commands/dashboard.md +0 -15
  105. package/commands/thumbgate-dashboard.md +0 -15
  106. package/config/builtin-lessons.json +0 -23
  107. package/config/gate-classifier-routing.json +0 -98
  108. package/config/post-deploy-marketing-pages.json +0 -61
  109. package/public/about.html +0 -162
  110. package/public/agent-manager.html +0 -179
  111. package/public/agents-cost-savings.html +0 -153
  112. package/public/ai-malpractice-prevention.html +0 -818
  113. package/public/assets/brand/github-social-preview.png +0 -0
  114. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  115. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  116. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  117. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  118. package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
  119. package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
  120. package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
  121. package/public/assets/brand/thumbgate-mark-team.svg +0 -26
  122. package/public/assets/brand/thumbgate-mark.svg +0 -21
  123. package/public/assets/brand/thumbgate-wordmark.svg +0 -20
  124. package/public/assets/claude-thumbgate-statusbar.svg +0 -8
  125. package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
  126. package/public/assets/legal-intake-control-flow.svg +0 -66
  127. package/public/brand/thumbgate-mark.svg +0 -19
  128. package/public/brand/thumbgate-og.svg +0 -16
  129. package/public/chatgpt-app.html +0 -330
  130. package/public/codex-enterprise.html +0 -123
  131. package/public/js/buyer-intent.js +0 -672
  132. package/public/pricing.html +0 -399
  133. package/scripts/action-receipts.js +0 -324
  134. package/scripts/activation-quickstart.js +0 -187
  135. package/scripts/agent-operations-planner.js +0 -621
  136. package/scripts/ai-component-inventory.js +0 -367
  137. package/scripts/async-eval-observability.js +0 -236
  138. package/scripts/audit.js +0 -65
  139. package/scripts/aws-blocks-guardrails.js +0 -228
  140. package/scripts/classifier-routing.js +0 -130
  141. package/scripts/dashboard-chat.js +0 -332
  142. package/scripts/feedback-aggregate.js +0 -281
  143. package/scripts/feedback-sanitizer.js +0 -105
  144. package/scripts/hob-pack.js +0 -591
  145. package/scripts/hook-stop-anti-claim.js +0 -301
  146. package/scripts/install-shim.js +0 -87
  147. package/scripts/mcp-oauth.js +0 -293
  148. package/scripts/noop-detect.js +0 -285
  149. package/scripts/omlx-smoke.js +0 -192
  150. package/scripts/parallel-workflow-orchestrator.js +0 -293
  151. package/scripts/plan-gate.js +0 -243
  152. package/scripts/plausible-domain-config.js +0 -99
  153. package/scripts/qa-scenario-planner.js +0 -136
  154. package/scripts/repeat-metric.js +0 -137
  155. package/scripts/secret-fixture-tokens.js +0 -61
  156. package/scripts/secret-redaction.js +0 -166
  157. package/scripts/self-harness-optimizer.js +0 -141
  158. package/scripts/self-healing-check.js +0 -193
  159. package/scripts/silent-failure-cluster.js +0 -531
  160. package/scripts/statusline-cache-read.js +0 -57
  161. package/scripts/sync-telemetry-from-prod.js +0 -374
  162. package/scripts/tool-contract-validator.js +0 -76
  163. package/scripts/trajectory-scorer.js +0 -63
  164. package/scripts/verify-marketing-pages-deployed.js +0 -195
  165. package/scripts/visitor-journey.js +0 -172
@@ -1,195 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * verify-marketing-pages-deployed.js — post-deploy probe for top-level
6
- * marketing pages.
7
- *
8
- * Reads `config/post-deploy-marketing-pages.json` and curls every entry
9
- * against the live production URL (default
10
- * https://thumbgate-production.up.railway.app, overridable via
11
- * THUMBGATE_PROD_URL env or --prod-url=…). Each response body must
12
- * contain the configured sentinel string. Any mismatch fails the run
13
- * and the route is included in the failure summary.
14
- *
15
- * Intended to run as a workflow step in .github/workflows/deploy-verify.yml
16
- * after the version sentinel check, so the marketing surface is verified
17
- * in the same gate as /health and /dashboard. Adding a new marketing
18
- * page? Add it to the JSON manifest — no workflow edit required.
19
- *
20
- * Modes:
21
- * --json machine-readable report on stdout, suitable for piping
22
- * into the GitHub Actions PR comment step.
23
- * --quiet suppress per-route lines; only print the final summary.
24
- *
25
- * Exit code is 0 when every page passes, 1 if any sentinel is missing
26
- * or any route returns non-200.
27
- */
28
-
29
- const fs = require('node:fs');
30
- const path = require('node:path');
31
-
32
- const DEFAULT_PROD_URL = 'https://thumbgate-production.up.railway.app';
33
- const DEFAULT_TIMEOUT_MS = 12000;
34
- const DEFAULT_MANIFEST_PATH = path.resolve(__dirname, '..', 'config', 'post-deploy-marketing-pages.json');
35
-
36
- function parseArgs(argv = []) {
37
- const out = {
38
- prodUrl: process.env.THUMBGATE_PROD_URL || DEFAULT_PROD_URL,
39
- manifestPath: DEFAULT_MANIFEST_PATH,
40
- json: false,
41
- quiet: false,
42
- timeoutMs: DEFAULT_TIMEOUT_MS,
43
- };
44
- for (const arg of argv) {
45
- if (arg === '--json') out.json = true;
46
- else if (arg === '--quiet') out.quiet = true;
47
- else if (arg.startsWith('--prod-url=')) out.prodUrl = arg.slice('--prod-url='.length);
48
- else if (arg.startsWith('--manifest=')) out.manifestPath = arg.slice('--manifest='.length);
49
- else if (arg.startsWith('--timeout-ms=')) {
50
- const n = Number(arg.slice('--timeout-ms='.length));
51
- if (Number.isFinite(n) && n > 0) out.timeoutMs = n;
52
- }
53
- }
54
- return out;
55
- }
56
-
57
- function loadManifest(manifestPath) {
58
- const raw = fs.readFileSync(manifestPath, 'utf-8');
59
- const parsed = JSON.parse(raw);
60
- if (!Array.isArray(parsed.pages) || parsed.pages.length === 0) {
61
- throw new Error(`Manifest has no pages: ${manifestPath}`);
62
- }
63
- for (const entry of parsed.pages) {
64
- if (typeof entry.route !== 'string' || !entry.route.startsWith('/')) {
65
- throw new Error(`Invalid route in manifest: ${JSON.stringify(entry)}`);
66
- }
67
- if (typeof entry.sentinel !== 'string' || entry.sentinel.length === 0) {
68
- throw new Error(`Invalid sentinel for route ${entry.route}`);
69
- }
70
- }
71
- return parsed;
72
- }
73
-
74
- async function probePage({ prodUrl, route, sentinel, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS }) {
75
- if (typeof fetchImpl !== 'function') {
76
- return { route, ok: false, error: 'fetch_unavailable' };
77
- }
78
- const controller = new AbortController();
79
- const timer = setTimeout(() => controller.abort(), timeoutMs);
80
- try {
81
- const url = `${prodUrl.replace(/\/$/, '')}${route}`;
82
- const res = await fetchImpl(url, {
83
- signal: controller.signal,
84
- headers: {
85
- // A real browser-shaped UA so any bot-deflection interstitials
86
- // do NOT trigger; we are probing the real visitor's surface.
87
- 'User-Agent': 'thumbgate-deploy-verify/1.0 (+https://github.com/IgorGanapolsky/ThumbGate)',
88
- Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
89
- },
90
- });
91
- const body = await res.text().catch(() => '');
92
- const sentinelPresent = body.includes(sentinel);
93
- return {
94
- route,
95
- url,
96
- status: res.status,
97
- ok: res.ok && sentinelPresent,
98
- sentinelPresent,
99
- bytes: body.length,
100
- };
101
- } catch (error) {
102
- return {
103
- route,
104
- ok: false,
105
- error: error?.name === 'AbortError' ? `timeout_after_${timeoutMs}ms` : (error?.message || String(error)),
106
- };
107
- } finally {
108
- clearTimeout(timer);
109
- }
110
- }
111
-
112
- async function runVerification({ prodUrl, manifestPath, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
113
- const manifest = loadManifest(manifestPath);
114
- const results = [];
115
- // Sequential is fine — manifest has <20 entries; parallelism would
116
- // be over-engineered and would risk masking a per-route rate-limit signal.
117
- for (const entry of manifest.pages) {
118
- // eslint-disable-next-line no-await-in-loop
119
- const result = await probePage({
120
- prodUrl,
121
- route: entry.route,
122
- sentinel: entry.sentinel,
123
- fetchImpl,
124
- timeoutMs,
125
- });
126
- results.push({ ...result, sentinel: entry.sentinel, description: entry.description });
127
- }
128
- const passed = results.filter((r) => r.ok);
129
- const failed = results.filter((r) => !r.ok);
130
- return {
131
- generatedAt: new Date().toISOString(),
132
- prodUrl,
133
- manifestVersion: manifest.version,
134
- totalRoutes: results.length,
135
- passedCount: passed.length,
136
- failedCount: failed.length,
137
- verdict: failed.length === 0 ? 'pass' : 'fail',
138
- results,
139
- };
140
- }
141
-
142
- function renderHuman(report, { quiet = false } = {}) {
143
- const lines = [];
144
- if (!quiet) {
145
- for (const r of report.results) {
146
- if (r.ok) {
147
- lines.push(`✅ ${r.route.padEnd(20)} HTTP ${r.status} bytes=${r.bytes} sentinel-OK`);
148
- } else if (r.error) {
149
- lines.push(`❌ ${r.route.padEnd(20)} ERROR ${r.error}`);
150
- } else if (!r.sentinelPresent) {
151
- lines.push(`❌ ${r.route.padEnd(20)} HTTP ${r.status} sentinel MISSING (expected: ${JSON.stringify(r.sentinel)})`);
152
- } else {
153
- lines.push(`❌ ${r.route.padEnd(20)} HTTP ${r.status}`);
154
- }
155
- }
156
- }
157
- lines.push(`\nSummary: ${report.passedCount}/${report.totalRoutes} pages passed against ${report.prodUrl} (verdict: ${report.verdict.toUpperCase()})`);
158
- return lines.join('\n');
159
- }
160
-
161
- async function main(argv) {
162
- const args = parseArgs(argv);
163
- let report;
164
- try {
165
- report = await runVerification({
166
- prodUrl: args.prodUrl,
167
- manifestPath: args.manifestPath,
168
- timeoutMs: args.timeoutMs,
169
- });
170
- } catch (error) {
171
- process.stderr.write(`verify-marketing-pages-deployed FAILED: ${error.message}\n`);
172
- process.exit(2);
173
- }
174
- if (args.json) {
175
- process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
176
- } else {
177
- process.stdout.write(`${renderHuman(report, { quiet: args.quiet })}\n`);
178
- }
179
- process.exitCode = report.verdict === 'pass' ? 0 : 1;
180
- }
181
-
182
- module.exports = {
183
- DEFAULT_PROD_URL,
184
- DEFAULT_TIMEOUT_MS,
185
- DEFAULT_MANIFEST_PATH,
186
- parseArgs,
187
- loadManifest,
188
- probePage,
189
- runVerification,
190
- renderHuman,
191
- };
192
-
193
- if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
194
- main(process.argv.slice(2));
195
- }
@@ -1,172 +0,0 @@
1
- 'use strict';
2
-
3
- const STAGE_ORDER = [
4
- 'landing',
5
- 'pricing',
6
- 'checkout_viewed',
7
- 'email_submitted',
8
- 'stripe_redirect',
9
- 'purchase',
10
- ];
11
-
12
- function parseTimestamp(row) {
13
- const raw = row && (row.timestamp || row.receivedAt || row.ts);
14
- const ms = raw ? Date.parse(raw) : NaN;
15
- return Number.isFinite(ms) ? ms : null;
16
- }
17
-
18
- function pickText(...values) {
19
- for (const value of values) {
20
- const text = String(value || '').trim();
21
- if (text) return text;
22
- }
23
- return '';
24
- }
25
-
26
- function extractSessionId(row) {
27
- return pickText(
28
- row.sessionId,
29
- row.visitorSessionId,
30
- row.visitor_session_id,
31
- row.attribution && row.attribution.sessionId,
32
- row.metadata && row.metadata.sessionId,
33
- row.stripeSessionId,
34
- row.traceId,
35
- row.acquisitionId,
36
- );
37
- }
38
-
39
- function extractVisitorId(row) {
40
- return pickText(
41
- row.visitorId,
42
- row.visitor_id,
43
- row.installId,
44
- row.attribution && row.attribution.visitorId,
45
- row.metadata && row.metadata.visitorId,
46
- );
47
- }
48
-
49
- function extractPath(row) {
50
- return pickText(
51
- row.path,
52
- row.page,
53
- row.landingPath,
54
- row.landing_path,
55
- row.attribution && row.attribution.landingPath,
56
- );
57
- }
58
-
59
- function classifyStage(row) {
60
- const event = String(row.eventType || row.event || '').toLowerCase();
61
- const stage = String(row.stage || '').toLowerCase();
62
- const path = extractPath(row).toLowerCase();
63
- const cta = String(row.ctaId || row.cta_id || '').toLowerCase();
64
-
65
- if (/purchase|paid|checkout\.session\.completed/.test(event) || stage === 'purchase') return 'purchase';
66
- if (/stripe_redirect|stripe redirect|redirect started/.test(event) || stage === 'stripe_redirect') return 'stripe_redirect';
67
- if (/email_submitted|email submitted|checkout_interstitial_cta_clicked/.test(event)) return 'email_submitted';
68
- if (/checkout/.test(path) || /checkout.*view|checkout pro viewed/.test(event)) return 'checkout_viewed';
69
- if (/pricing/.test(path) || /pricing/.test(cta) || /pricing_cta/.test(event)) return 'pricing';
70
- if (/landing|page_view|landing_page_view|discovery/.test(event) || stage === 'discovery') return 'landing';
71
- return null;
72
- }
73
-
74
- function stageRank(stage) {
75
- const index = STAGE_ORDER.indexOf(stage);
76
- return index === -1 ? -1 : index;
77
- }
78
-
79
- function nextMissingStage(maxStage) {
80
- const index = stageRank(maxStage);
81
- if (index < 0) return 'unknown';
82
- if (maxStage === 'purchase') return 'converted';
83
- return STAGE_ORDER[index + 1] || 'unknown';
84
- }
85
-
86
- function buildVisitorJourneySummary({ telemetryRows = [], funnelRows = [], limit = 100 } = {}) {
87
- const sessions = new Map();
88
- const stageCounts = Object.fromEntries(STAGE_ORDER.map((stage) => [stage, 0]));
89
-
90
- function ingest(row, source) {
91
- const ts = parseTimestamp(row);
92
- if (ts == null) return;
93
- const sessionId = extractSessionId(row) || `anonymous:${extractVisitorId(row) || ts}`;
94
- const visitorId = extractVisitorId(row) || null;
95
- const stage = classifyStage(row);
96
- const path = extractPath(row);
97
- const existing = sessions.get(sessionId) || {
98
- sessionId,
99
- visitorId,
100
- firstSeen: new Date(ts).toISOString(),
101
- lastSeen: new Date(ts).toISOString(),
102
- maxStage: null,
103
- dropoffStage: 'unknown',
104
- events: [],
105
- paths: [],
106
- ctas: [],
107
- sources: [],
108
- visitorType: row.visitorType || null,
109
- utm: {},
110
- referrerHost: pickText(row.referrerHost, row.referrer_host, row.referrer),
111
- };
112
-
113
- existing.visitorId = existing.visitorId || visitorId;
114
- existing.firstSeen = new Date(Math.min(Date.parse(existing.firstSeen), ts)).toISOString();
115
- existing.lastSeen = new Date(Math.max(Date.parse(existing.lastSeen), ts)).toISOString();
116
- existing.sources.push(source);
117
- if (stage) {
118
- stageCounts[stage] += 1;
119
- if (stageRank(stage) > stageRank(existing.maxStage)) existing.maxStage = stage;
120
- }
121
- if (path && !existing.paths.includes(path)) existing.paths.push(path);
122
- const cta = pickText(row.ctaId, row.cta_id);
123
- if (cta && !existing.ctas.includes(cta)) existing.ctas.push(cta);
124
- for (const key of ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']) {
125
- const value = pickText(row[key], row.attribution && row.attribution[key]);
126
- if (value && !existing.utm[key]) existing.utm[key] = value;
127
- }
128
- existing.events.push({
129
- timestamp: new Date(ts).toISOString(),
130
- source,
131
- eventType: pickText(row.eventType, row.event, row.stage, 'unknown'),
132
- stage,
133
- path: path || null,
134
- ctaId: cta || null,
135
- });
136
- sessions.set(sessionId, existing);
137
- }
138
-
139
- for (const row of telemetryRows) ingest(row, 'telemetry');
140
- for (const row of funnelRows) ingest(row, 'funnel');
141
-
142
- const journeys = [...sessions.values()].map((session) => {
143
- session.sources = [...new Set(session.sources)];
144
- session.events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
145
- session.maxStage = session.maxStage || 'unknown';
146
- session.dropoffStage = nextMissingStage(session.maxStage);
147
- session.eventCount = session.events.length;
148
- return session;
149
- }).sort((a, b) => Date.parse(b.lastSeen) - Date.parse(a.lastSeen));
150
-
151
- const dropoffCounts = {};
152
- for (const journey of journeys) {
153
- dropoffCounts[journey.dropoffStage] = (dropoffCounts[journey.dropoffStage] || 0) + 1;
154
- }
155
-
156
- return {
157
- generatedAt: new Date().toISOString(),
158
- sessionCount: journeys.length,
159
- stageCounts,
160
- dropoffCounts,
161
- journeys: journeys.slice(0, Math.max(1, Math.min(Number(limit) || 100, 500))),
162
- truncated: journeys.length > Math.max(1, Math.min(Number(limit) || 100, 500)),
163
- };
164
- }
165
-
166
- module.exports = {
167
- STAGE_ORDER,
168
- buildVisitorJourneySummary,
169
- classifyStage,
170
- extractSessionId,
171
- extractVisitorId,
172
- };