thumbgate 1.13.0 → 1.14.1

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.
@@ -0,0 +1,608 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const DEFAULT_WINDOW_HOURS = 24;
8
+ const MAX_WINDOW_HOURS = 24 * 30;
9
+ const ARTIFACT_TYPES = [
10
+ 'pr-pulse',
11
+ 'reliability-pulse',
12
+ 'revenue-pulse',
13
+ 'release-readiness',
14
+ ];
15
+
16
+ function normalizeWindowHours(value) {
17
+ if (value === null || value === undefined || value === '') return DEFAULT_WINDOW_HOURS;
18
+ const parsed = Number(value);
19
+ if (!Number.isFinite(parsed)) return DEFAULT_WINDOW_HOURS;
20
+ if (parsed < 1) return 1;
21
+ if (parsed > MAX_WINDOW_HOURS) return MAX_WINDOW_HOURS;
22
+ return Math.floor(parsed);
23
+ }
24
+
25
+ function normalizeArtifactType(type) {
26
+ const normalized = String(type || 'reliability-pulse').trim().toLowerCase();
27
+ const aliases = {
28
+ pr: 'pr-pulse',
29
+ prs: 'pr-pulse',
30
+ pull_requests: 'pr-pulse',
31
+ pullrequests: 'pr-pulse',
32
+ reliability: 'reliability-pulse',
33
+ gates: 'reliability-pulse',
34
+ revenue: 'revenue-pulse',
35
+ growth: 'revenue-pulse',
36
+ acquisition: 'revenue-pulse',
37
+ release: 'release-readiness',
38
+ readiness: 'release-readiness',
39
+ };
40
+ const resolved = aliases[normalized] || normalized;
41
+ if (!ARTIFACT_TYPES.includes(resolved)) {
42
+ throw new Error(`Unknown operator artifact type: ${type}`);
43
+ }
44
+ return resolved;
45
+ }
46
+
47
+ function safeNumber(value, fallback = 0) {
48
+ const parsed = Number(value);
49
+ return Number.isFinite(parsed) ? parsed : fallback;
50
+ }
51
+
52
+ function getPath(source, parts, fallback = undefined) {
53
+ let cursor = source;
54
+ for (const part of parts) {
55
+ if (!cursor || typeof cursor !== 'object' || !(part in cursor)) return fallback;
56
+ cursor = cursor[part];
57
+ }
58
+ return cursor === undefined ? fallback : cursor;
59
+ }
60
+
61
+ function formatCurrency(cents) {
62
+ return `$${(safeNumber(cents) / 100).toFixed(2)}`;
63
+ }
64
+
65
+ function compactList(items, limit = 5) {
66
+ return (Array.isArray(items) ? items : []).filter(Boolean).slice(0, limit);
67
+ }
68
+
69
+ function getErrorMessage(error) {
70
+ return String(error?.message || error);
71
+ }
72
+
73
+ function artifactBase(type, options = {}) {
74
+ const generatedAt = options.now instanceof Date
75
+ ? options.now.toISOString()
76
+ : options.now || new Date().toISOString();
77
+ return {
78
+ schemaVersion: 1,
79
+ type,
80
+ generatedAt,
81
+ windowHours: normalizeWindowHours(options.windowHours),
82
+ status: 'watch',
83
+ summary: '',
84
+ decision: {
85
+ label: 'Review',
86
+ rationale: '',
87
+ nextActions: [],
88
+ },
89
+ metrics: {},
90
+ sections: [],
91
+ evidence: [],
92
+ };
93
+ }
94
+
95
+ function buildEvidence(label, value, extra = {}) {
96
+ return {
97
+ label,
98
+ value: value === undefined || value === null ? 'unknown' : value,
99
+ ...extra,
100
+ };
101
+ }
102
+
103
+ function buildReliabilityPulseArtifact(options = {}) {
104
+ const type = 'reliability-pulse';
105
+ const artifact = artifactBase(type, options);
106
+ const dashboard = options.dashboardData || {};
107
+ const session = options.sessionReport || {};
108
+ const gateStats = dashboard.gateStats || {};
109
+ const health = dashboard.health || {};
110
+ const diagnostics = dashboard.diagnostics || {};
111
+ const reviewDelta = dashboard.reviewDelta || {};
112
+ const lessonPipeline = dashboard.lessonPipeline || {};
113
+
114
+ const blocked = safeNumber(gateStats.blocked, safeNumber(getPath(session, ['gates', 'blocked'])));
115
+ const warned = safeNumber(gateStats.warned, safeNumber(getPath(session, ['gates', 'warned'])));
116
+ const feedbackCount = safeNumber(health.feedbackCount);
117
+ const memoryCount = safeNumber(health.memoryCount);
118
+ const negativeAdded = safeNumber(reviewDelta.negativeAdded);
119
+ const staleLessons = safeNumber(lessonPipeline.staleLessons);
120
+ const topDiagnostic = getPath(diagnostics, ['categories', 0, 'key'], null);
121
+
122
+ artifact.title = 'Reliability Pulse';
123
+ artifact.metrics = {
124
+ blocked,
125
+ warned,
126
+ feedbackCount,
127
+ memoryCount,
128
+ negativeAdded,
129
+ staleLessons,
130
+ };
131
+
132
+ if (negativeAdded > 0 || staleLessons > 0 || blocked > 0) {
133
+ artifact.status = 'actionable';
134
+ artifact.decision.label = 'Regenerate and inspect gates';
135
+ artifact.decision.rationale = 'Recent negative signal or gate activity means the prevention layer has learnable work to absorb.';
136
+ artifact.decision.nextActions = compactList([
137
+ negativeAdded > 0 ? `Promote ${negativeAdded} new negative signal(s) into prevention rules.` : null,
138
+ staleLessons > 0 ? `Review ${staleLessons} stale lesson(s) before they age out of useful recall.` : null,
139
+ blocked > 0 ? `Inspect the top blocked gate path before the next risky operation.` : null,
140
+ topDiagnostic ? `Address top diagnostic category: ${topDiagnostic}.` : null,
141
+ ], 4);
142
+ } else {
143
+ artifact.status = 'healthy';
144
+ artifact.decision.label = 'Keep shipping';
145
+ artifact.decision.rationale = 'No fresh reliability pressure is visible in the current window.';
146
+ artifact.decision.nextActions = ['Keep the Reliability Gateway enabled during PR and release work.'];
147
+ }
148
+
149
+ artifact.summary = `${blocked} blocked, ${warned} warned, ${feedbackCount} feedback events, ${memoryCount} memories.`;
150
+ artifact.sections = [
151
+ {
152
+ title: 'Gate Load',
153
+ bullets: [
154
+ `${blocked} blocked actions`,
155
+ `${warned} warnings`,
156
+ `${safeNumber(getPath(session, ['gates', 'pendingApproval']))} pending approvals`,
157
+ ],
158
+ },
159
+ {
160
+ title: 'Learning Queue',
161
+ bullets: compactList([
162
+ `${negativeAdded} new negative signal(s)`,
163
+ `${staleLessons} stale lesson(s)`,
164
+ topDiagnostic ? `Top diagnostic: ${topDiagnostic}` : null,
165
+ ]),
166
+ },
167
+ ];
168
+ artifact.evidence = [
169
+ buildEvidence('dashboard.health.feedbackCount', feedbackCount),
170
+ buildEvidence('dashboard.gateStats.blocked', blocked),
171
+ buildEvidence('session_report.windowHours', artifact.windowHours),
172
+ ];
173
+ return artifact;
174
+ }
175
+
176
+ function buildRevenuePulseArtifact(options = {}) {
177
+ const type = 'revenue-pulse';
178
+ const artifact = artifactBase(type, options);
179
+ const dashboard = options.dashboardData || {};
180
+ const analytics = dashboard.analytics || {};
181
+ const funnel = analytics.funnel || {};
182
+ const revenue = analytics.revenue || {};
183
+ const seo = analytics.seo || {};
184
+ const attribution = analytics.attribution || {};
185
+
186
+ const visitors = safeNumber(funnel.visitors);
187
+ const ctaClicks = safeNumber(funnel.ctaClicks);
188
+ const checkoutStarts = safeNumber(funnel.checkoutStarts);
189
+ const acquisitionLeads = safeNumber(funnel.acquisitionLeads);
190
+ const paidOrders = safeNumber(revenue.paidOrders, safeNumber(funnel.paidOrders));
191
+ const bookedRevenueCents = safeNumber(revenue.bookedRevenueCents);
192
+ const topTrafficChannel = funnel.topTrafficChannel || getPath(seo, ['topSurface', 'key'], null);
193
+ const topPaidSource = Object.entries(attribution.paidBySource || {})
194
+ .sort((a, b) => safeNumber(b[1]) - safeNumber(a[1]))[0];
195
+
196
+ artifact.title = 'Revenue Pulse';
197
+ artifact.metrics = {
198
+ visitors,
199
+ ctaClicks,
200
+ checkoutStarts,
201
+ acquisitionLeads,
202
+ paidOrders,
203
+ bookedRevenueCents,
204
+ bookedRevenue: formatCurrency(bookedRevenueCents),
205
+ visitorToPaidRate: safeNumber(funnel.visitorToPaidRate),
206
+ };
207
+
208
+ if (paidOrders > 0) {
209
+ artifact.status = 'actionable';
210
+ artifact.decision.label = 'Double down on converting source';
211
+ artifact.decision.rationale = 'Revenue is visible; the highest-ROI move is to reuse the source and copy that already converted.';
212
+ artifact.decision.nextActions = compactList([
213
+ topPaidSource ? `Create another offer using the paid source: ${topPaidSource[0]}.` : null,
214
+ topTrafficChannel ? `Fan out the winning acquisition angle on ${topTrafficChannel}.` : null,
215
+ 'Keep checkout proof and pricing copy unchanged until the next conversion batch is measured.',
216
+ ], 3);
217
+ } else if (checkoutStarts > 0 || ctaClicks > 0) {
218
+ artifact.status = 'blocked';
219
+ artifact.decision.label = 'Fix checkout conversion';
220
+ artifact.decision.rationale = 'Intent exists, but the journey is not turning into paid orders.';
221
+ artifact.decision.nextActions = compactList([
222
+ `${checkoutStarts} checkout start(s) and ${ctaClicks} CTA click(s) need buyer-loss review.`,
223
+ 'Audit checkout redirects, pricing objections, and proof placement before adding more traffic.',
224
+ topTrafficChannel ? `Inspect source-specific copy for ${topTrafficChannel}.` : null,
225
+ ], 4);
226
+ } else {
227
+ artifact.status = 'actionable';
228
+ artifact.decision.label = 'Create more acquisition surface';
229
+ artifact.decision.rationale = 'No paid orders or checkout intent are visible, so traffic and discovery injection beat infrastructure work.';
230
+ artifact.decision.nextActions = compactList([
231
+ 'Publish one high-intent ThumbGate proof chunk with DPO, Pre-Action Gates, and Reliability Gateway terms.',
232
+ 'Add one outreach or community distribution action tied to the latest verification evidence.',
233
+ topTrafficChannel ? `Reuse current top channel: ${topTrafficChannel}.` : 'Seed a first measurable traffic channel.',
234
+ ], 3);
235
+ }
236
+
237
+ artifact.summary = `${paidOrders} paid order(s), ${formatCurrency(bookedRevenueCents)} booked, ${visitors} visitors, ${checkoutStarts} checkout starts.`;
238
+ artifact.sections = [
239
+ {
240
+ title: 'Funnel',
241
+ bullets: [
242
+ `${visitors} visitors`,
243
+ `${ctaClicks} CTA clicks`,
244
+ `${checkoutStarts} checkout starts`,
245
+ `${paidOrders} paid orders`,
246
+ ],
247
+ },
248
+ {
249
+ title: 'Acquisition',
250
+ bullets: compactList([
251
+ topTrafficChannel ? `Top traffic channel: ${topTrafficChannel}` : 'No top traffic channel yet',
252
+ `${safeNumber(seo.landingViews)} SEO landing views`,
253
+ `${acquisitionLeads} acquisition leads`,
254
+ ]),
255
+ },
256
+ ];
257
+ artifact.evidence = [
258
+ buildEvidence('analytics.revenue.paidOrders', paidOrders),
259
+ buildEvidence('analytics.revenue.bookedRevenueCents', bookedRevenueCents),
260
+ buildEvidence('analytics.funnel.checkoutStarts', checkoutStarts),
261
+ ];
262
+ return artifact;
263
+ }
264
+
265
+ function classifyPr(pr, checks) {
266
+ const { summarizeChecks } = require('./pr-manager');
267
+ const summary = summarizeChecks(checks || []);
268
+ const mergeState = String(pr.mergeStateStatus || 'UNKNOWN').toUpperCase();
269
+ const mergeable = String(pr.mergeable || 'UNKNOWN').toUpperCase();
270
+ const reviewDecision = String(pr.reviewDecision || '').toUpperCase();
271
+ if (pr.isDraft) return { state: 'draft', blockers: ['draft'] };
272
+ if (mergeState === 'BEHIND') return { state: 'blocked', blockers: ['BEHIND'] };
273
+ if (mergeState === 'DIRTY' || mergeable === 'CONFLICTING') {
274
+ return { state: 'blocked', blockers: ['conflicts'] };
275
+ }
276
+ if (summary.failing.length > 0) return { state: 'blocked', blockers: summary.failing };
277
+ if (summary.pending.length > 0) return { state: 'pending', blockers: summary.pending };
278
+ if (reviewDecision === 'CHANGES_REQUESTED') {
279
+ return { state: 'blocked', blockers: ['changes_requested'] };
280
+ }
281
+ if (reviewDecision === 'REVIEW_REQUIRED') {
282
+ return { state: 'blocked', blockers: ['review_required'] };
283
+ }
284
+ if (['CLEAN', 'HAS_HOOKS'].includes(mergeState) && ['MERGEABLE', 'UNKNOWN'].includes(mergeable)) {
285
+ return { state: 'ready', blockers: [] };
286
+ }
287
+ return { state: 'pending', blockers: [pr.mergeStateStatus || 'unknown_state'] };
288
+ }
289
+
290
+ function createPrRow(pr, checks, classification) {
291
+ return {
292
+ number: pr.number,
293
+ title: pr.title,
294
+ url: pr.url,
295
+ draft: Boolean(pr.isDraft),
296
+ mergeStateStatus: pr.mergeStateStatus || null,
297
+ reviewDecision: pr.reviewDecision || null,
298
+ state: classification.state,
299
+ blockers: classification.blockers,
300
+ checkCount: checks.length,
301
+ };
302
+ }
303
+
304
+ function groupPrRows(rows) {
305
+ return {
306
+ ready: rows.filter((row) => row.state === 'ready'),
307
+ blocked: rows.filter((row) => row.state === 'blocked'),
308
+ pending: rows.filter((row) => row.state === 'pending'),
309
+ drafts: rows.filter((row) => row.state === 'draft'),
310
+ };
311
+ }
312
+
313
+ function getPrPulseStatus(groups) {
314
+ if (groups.blocked.length > 0) return 'blocked';
315
+ if (groups.ready.length > 0) return 'actionable';
316
+ if (groups.pending.length > 0) return 'watch';
317
+ return 'healthy';
318
+ }
319
+
320
+ function getPrPulseDecision(groups) {
321
+ if (groups.ready.length > 0) {
322
+ return {
323
+ label: 'Submit ready PRs through protected merge path',
324
+ rationale: 'Terminal checks and merge state are clean for at least one open PR.',
325
+ };
326
+ }
327
+ if (groups.blocked.length > 0) {
328
+ return {
329
+ label: 'Fix PR blockers',
330
+ rationale: 'One or more PRs have failing checks, draft state, or merge-state blockers.',
331
+ };
332
+ }
333
+ if (groups.pending.length > 0) {
334
+ return {
335
+ label: 'Wait for terminal checks',
336
+ rationale: 'Checks are still running; merging now would violate the protected path.',
337
+ };
338
+ }
339
+ return {
340
+ label: 'No PR action',
341
+ rationale: 'No open PRs require operator action.',
342
+ };
343
+ }
344
+
345
+ function formatPrNumbers(rows) {
346
+ return rows.map((row) => `#${row.number}`).join(', ');
347
+ }
348
+
349
+ function formatBlockedPrs(rows) {
350
+ return rows.map((row) => {
351
+ const blocker = row.blockers[0] || 'blocked';
352
+ return `#${row.number} (${blocker})`;
353
+ }).join(', ');
354
+ }
355
+
356
+ function buildPrNextActions(groups) {
357
+ return compactList([
358
+ groups.ready.length > 0 ? `Run npm run pr:manage for PR(s): ${formatPrNumbers(groups.ready)}.` : null,
359
+ groups.blocked.length > 0 ? `Unblock PR(s): ${formatBlockedPrs(groups.blocked)}.` : null,
360
+ groups.pending.length > 0 ? `Recheck pending PR(s): ${formatPrNumbers(groups.pending)}.` : null,
361
+ groups.drafts.length > 0 ? `Leave draft PR(s) alone until marked ready: ${formatPrNumbers(groups.drafts)}.` : null,
362
+ ], 4);
363
+ }
364
+
365
+ async function buildPrPulseArtifact(options = {}) {
366
+ const type = 'pr-pulse';
367
+ const artifact = artifactBase(type, options);
368
+ artifact.title = 'PR Pulse';
369
+
370
+ const prClient = options.prClient || require('./pr-manager');
371
+ const prs = Array.isArray(options.prs) ? options.prs : await prClient.listOpenPrs();
372
+ const checksByPr = options.checksByPr || {};
373
+ const rows = [];
374
+
375
+ for (const pr of prs) {
376
+ const number = pr.number;
377
+ let checks = checksByPr[number];
378
+ let checkError = null;
379
+ if (!Array.isArray(checks)) {
380
+ try {
381
+ checks = await prClient.getPrChecks(number);
382
+ } catch (err) {
383
+ checks = [];
384
+ checkError = getErrorMessage(err);
385
+ }
386
+ }
387
+ const classification = checkError
388
+ ? { state: 'blocked', blockers: [checkError] }
389
+ : classifyPr(pr, checks);
390
+ rows.push(createPrRow(pr, checks, classification));
391
+ }
392
+
393
+ const groups = groupPrRows(rows);
394
+ const decision = getPrPulseDecision(groups);
395
+
396
+ artifact.metrics = {
397
+ open: rows.length,
398
+ ready: groups.ready.length,
399
+ blocked: groups.blocked.length,
400
+ pending: groups.pending.length,
401
+ draft: groups.drafts.length,
402
+ };
403
+ artifact.status = getPrPulseStatus(groups);
404
+ artifact.summary = `${rows.length} open PR(s): ${groups.ready.length} ready, ${groups.blocked.length} blocked, ${groups.pending.length} pending, ${groups.drafts.length} draft.`;
405
+ artifact.decision.label = decision.label;
406
+ artifact.decision.rationale = decision.rationale;
407
+ artifact.decision.nextActions = buildPrNextActions(groups);
408
+ artifact.sections = [
409
+ {
410
+ title: 'Open PRs',
411
+ bullets: rows.map((row) => `#${row.number} ${row.state}: ${row.title || 'untitled'}`),
412
+ data: rows,
413
+ },
414
+ ];
415
+ artifact.evidence = [
416
+ buildEvidence('openPrs', rows.length),
417
+ buildEvidence('readyPrs', formatPrNumbers(groups.ready) || 'none'),
418
+ buildEvidence('blockedPrs', formatPrNumbers(groups.blocked) || 'none'),
419
+ ];
420
+ return artifact;
421
+ }
422
+
423
+ function buildReleaseReadinessArtifact(options = {}) {
424
+ const type = 'release-readiness';
425
+ const artifact = artifactBase(type, options);
426
+ const dashboard = options.dashboardData || {};
427
+ const packageInfo = options.packageInfo || readPackageInfo(options.packageRoot);
428
+ const health = dashboard.health || {};
429
+ const readiness = dashboard.readiness || {};
430
+ const gateAudit = dashboard.gateAudit || {};
431
+
432
+ const feedbackCount = safeNumber(health.feedbackCount);
433
+ const gateCount = safeNumber(health.gateCount);
434
+ const gateConfigLoaded = health.gateConfigLoaded !== false;
435
+ const warnings = Array.isArray(readiness.warnings) ? readiness.warnings : [];
436
+ const auditWarnings = safeNumber(gateAudit.warnings);
437
+ const version = packageInfo.version || 'unknown';
438
+
439
+ artifact.title = 'Release Readiness';
440
+ artifact.metrics = {
441
+ version,
442
+ feedbackCount,
443
+ gateCount,
444
+ gateConfigLoaded,
445
+ readinessWarnings: warnings.length,
446
+ gateAuditWarnings: auditWarnings,
447
+ };
448
+
449
+ if (!gateConfigLoaded || warnings.length > 0 || auditWarnings > 0) {
450
+ artifact.status = 'blocked';
451
+ artifact.decision.label = 'Hold release until readiness blockers are cleared';
452
+ artifact.decision.rationale = 'Release work needs a loaded gate config and no unresolved readiness warnings.';
453
+ artifact.decision.nextActions = compactList([
454
+ gateConfigLoaded ? null : 'Restore the gate config before release work.',
455
+ warnings[0] ? `Resolve readiness warning: ${warnings[0]}` : null,
456
+ auditWarnings > 0 ? `Clear ${auditWarnings} gate audit warning(s).` : null,
457
+ 'Run the clean-worktree verification suite before publishing.',
458
+ ], 4);
459
+ } else {
460
+ artifact.status = 'actionable';
461
+ artifact.decision.label = 'Verify in a clean worktree';
462
+ artifact.decision.rationale = 'Local readiness inputs look sane; the next gate is exact-commit verification.';
463
+ artifact.decision.nextActions = [
464
+ 'Run npm ci in a dedicated clean verification worktree.',
465
+ 'Run npm test, npm run test:coverage, npm run prove:adapters, npm run prove:automation, and npm run self-heal:check.',
466
+ 'Submit release PRs through npm run pr:manage after checks are terminal.',
467
+ ];
468
+ }
469
+
470
+ artifact.summary = `ThumbGate ${version}: ${gateCount} gates, ${feedbackCount} feedback events, ${warnings.length + auditWarnings} readiness warning(s).`;
471
+ artifact.sections = [
472
+ {
473
+ title: 'Release Inputs',
474
+ bullets: [
475
+ `Package version: ${version}`,
476
+ `Gate config: ${gateConfigLoaded ? 'loaded' : 'missing'}`,
477
+ `${gateCount} configured gates`,
478
+ `${feedbackCount} feedback events`,
479
+ ],
480
+ },
481
+ {
482
+ title: 'Verification',
483
+ bullets: artifact.decision.nextActions,
484
+ },
485
+ ];
486
+ artifact.evidence = [
487
+ buildEvidence('package.version', version, { path: 'package.json' }),
488
+ buildEvidence('dashboard.health.gateConfigLoaded', gateConfigLoaded),
489
+ buildEvidence('dashboard.readiness.warnings', warnings.length),
490
+ ];
491
+ return artifact;
492
+ }
493
+
494
+ function readPackageInfo(packageRoot) {
495
+ const root = packageRoot || path.join(__dirname, '..');
496
+ try {
497
+ return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
498
+ } catch {
499
+ return {};
500
+ }
501
+ }
502
+
503
+ async function resolveDashboardData(options) {
504
+ if (options.dashboardData) return options.dashboardData;
505
+ const { generateDashboard } = require('./dashboard');
506
+ const { getFeedbackPaths } = require('./feedback-loop');
507
+ return generateDashboard(options.feedbackDir || getFeedbackPaths().FEEDBACK_DIR, {
508
+ now: options.now,
509
+ });
510
+ }
511
+
512
+ function resolveSessionReport(options) {
513
+ if (options.sessionReport) return options.sessionReport;
514
+ const { buildSessionReport } = require('./session-report');
515
+ return buildSessionReport({ windowHours: options.windowHours });
516
+ }
517
+
518
+ async function generateOperatorArtifact(options = {}) {
519
+ const type = normalizeArtifactType(options.type);
520
+ const windowHours = normalizeWindowHours(options.windowHours);
521
+ const sharedOptions = { ...options, type, windowHours };
522
+
523
+ if (type === 'pr-pulse') {
524
+ return buildPrPulseArtifact(sharedOptions);
525
+ }
526
+
527
+ const dashboardData = await resolveDashboardData(sharedOptions);
528
+ if (type === 'reliability-pulse') {
529
+ const sessionReport = resolveSessionReport(sharedOptions);
530
+ return buildReliabilityPulseArtifact({ ...sharedOptions, dashboardData, sessionReport });
531
+ }
532
+ if (type === 'revenue-pulse') {
533
+ return buildRevenuePulseArtifact({ ...sharedOptions, dashboardData });
534
+ }
535
+ return buildReleaseReadinessArtifact({ ...sharedOptions, dashboardData });
536
+ }
537
+
538
+ function formatArtifactMarkdown(artifact) {
539
+ const lines = [
540
+ `# ${artifact.title || artifact.type}`,
541
+ '',
542
+ `Status: ${artifact.status}`,
543
+ `Window: ${artifact.windowHours}h`,
544
+ `Generated: ${artifact.generatedAt}`,
545
+ '',
546
+ `Summary: ${artifact.summary}`,
547
+ '',
548
+ `Decision: ${artifact.decision.label}`,
549
+ '',
550
+ artifact.decision.rationale,
551
+ '',
552
+ 'Next actions:',
553
+ ];
554
+ for (const action of artifact.decision.nextActions || []) {
555
+ lines.push(`- ${action}`);
556
+ }
557
+ for (const section of artifact.sections || []) {
558
+ lines.push('', `## ${section.title}`);
559
+ for (const bullet of section.bullets || []) {
560
+ lines.push(`- ${bullet}`);
561
+ }
562
+ }
563
+ if (Array.isArray(artifact.evidence) && artifact.evidence.length > 0) {
564
+ lines.push('', '## Evidence');
565
+ for (const item of artifact.evidence) {
566
+ lines.push(`- ${item.label}: ${item.value}`);
567
+ }
568
+ }
569
+ return `${lines.join('\n')}\n`;
570
+ }
571
+
572
+ module.exports = {
573
+ ARTIFACT_TYPES,
574
+ DEFAULT_WINDOW_HOURS,
575
+ MAX_WINDOW_HOURS,
576
+ buildPrPulseArtifact,
577
+ buildReliabilityPulseArtifact,
578
+ buildRevenuePulseArtifact,
579
+ buildReleaseReadinessArtifact,
580
+ formatArtifactMarkdown,
581
+ generateOperatorArtifact,
582
+ normalizeArtifactType,
583
+ normalizeWindowHours,
584
+ };
585
+
586
+ function isDirectCli() {
587
+ return Boolean(process.argv[1] && path.resolve(process.argv[1]) === __filename);
588
+ }
589
+
590
+ if (isDirectCli()) {
591
+ const args = process.argv.slice(2);
592
+ const typeArg = args.find((arg) => arg.startsWith('--type='));
593
+ const windowArg = args.find((arg) => arg.startsWith('--window-hours='));
594
+ const format = args.includes('--markdown') ? 'markdown' : 'json';
595
+ generateOperatorArtifact({
596
+ type: typeArg ? typeArg.slice('--type='.length) : args.find((arg) => !arg.startsWith('--')),
597
+ windowHours: windowArg ? windowArg.slice('--window-hours='.length) : undefined,
598
+ }).then((artifact) => {
599
+ if (format === 'markdown') {
600
+ process.stdout.write(formatArtifactMarkdown(artifact));
601
+ return;
602
+ }
603
+ process.stdout.write(`${JSON.stringify(artifact, null, 2)}\n`);
604
+ }).catch((err) => {
605
+ console.error(getErrorMessage(err));
606
+ process.exit(1);
607
+ });
608
+ }