@nebulacomponents/citable 1.16.0 → 1.18.0

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 (142) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +16 -3
  3. package/dist/universal/.agents/skills/citable/SKILL.md +66 -1
  4. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  5. package/dist/universal/.agents/skills/citable/manifest.json +9 -6
  6. package/dist/universal/.agents/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  7. package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +69 -0
  8. package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +98 -0
  9. package/dist/universal/.agents/skills/citable/schemas/sow.schema.json +92 -0
  10. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  11. package/dist/universal/.claude/skills/citable/SKILL.md +66 -1
  12. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  13. package/dist/universal/.claude/skills/citable/manifest.json +9 -6
  14. package/dist/universal/.claude/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  15. package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +69 -0
  16. package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +98 -0
  17. package/dist/universal/.claude/skills/citable/schemas/sow.schema.json +92 -0
  18. package/dist/universal/.cursor/skills/citable/SKILL.md +66 -1
  19. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  20. package/dist/universal/.cursor/skills/citable/manifest.json +9 -6
  21. package/dist/universal/.cursor/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  22. package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +69 -0
  23. package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +98 -0
  24. package/dist/universal/.cursor/skills/citable/schemas/sow.schema.json +92 -0
  25. package/dist/universal/.gemini/skills/citable/SKILL.md +66 -1
  26. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  27. package/dist/universal/.gemini/skills/citable/manifest.json +9 -6
  28. package/dist/universal/.gemini/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  29. package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +69 -0
  30. package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +98 -0
  31. package/dist/universal/.gemini/skills/citable/schemas/sow.schema.json +92 -0
  32. package/dist/universal/.github/skills/citable/SKILL.md +66 -1
  33. package/dist/universal/.github/skills/citable/VERSION +1 -1
  34. package/dist/universal/.github/skills/citable/manifest.json +9 -6
  35. package/dist/universal/.github/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  36. package/dist/universal/.github/skills/citable/schemas/finding.schema.json +69 -0
  37. package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +98 -0
  38. package/dist/universal/.github/skills/citable/schemas/sow.schema.json +92 -0
  39. package/dist/universal/.kiro/skills/citable/SKILL.md +66 -1
  40. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  41. package/dist/universal/.kiro/skills/citable/manifest.json +9 -6
  42. package/dist/universal/.kiro/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  43. package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +69 -0
  44. package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +98 -0
  45. package/dist/universal/.kiro/skills/citable/schemas/sow.schema.json +92 -0
  46. package/dist/universal/.opencode/skills/citable/SKILL.md +66 -1
  47. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  48. package/dist/universal/.opencode/skills/citable/manifest.json +9 -6
  49. package/dist/universal/.opencode/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  50. package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +69 -0
  51. package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +98 -0
  52. package/dist/universal/.opencode/skills/citable/schemas/sow.schema.json +92 -0
  53. package/dist/universal/.pi/agent/skills/citable/SKILL.md +66 -1
  54. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  55. package/dist/universal/.pi/agent/skills/citable/manifest.json +9 -6
  56. package/dist/universal/.pi/agent/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  57. package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +69 -0
  58. package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +98 -0
  59. package/dist/universal/.pi/agent/skills/citable/schemas/sow.schema.json +92 -0
  60. package/dist/universal/.qoder/skills/citable/SKILL.md +66 -1
  61. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  62. package/dist/universal/.qoder/skills/citable/manifest.json +9 -6
  63. package/dist/universal/.qoder/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  64. package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +69 -0
  65. package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +98 -0
  66. package/dist/universal/.qoder/skills/citable/schemas/sow.schema.json +92 -0
  67. package/dist/universal/.rovodev/skills/citable/SKILL.md +66 -1
  68. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  69. package/dist/universal/.rovodev/skills/citable/manifest.json +9 -6
  70. package/dist/universal/.rovodev/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  71. package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +69 -0
  72. package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +98 -0
  73. package/dist/universal/.rovodev/skills/citable/schemas/sow.schema.json +92 -0
  74. package/dist/universal/.trae/skills/citable/SKILL.md +66 -1
  75. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  76. package/dist/universal/.trae/skills/citable/manifest.json +9 -6
  77. package/dist/universal/.trae/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  78. package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +69 -0
  79. package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +98 -0
  80. package/dist/universal/.trae/skills/citable/schemas/sow.schema.json +92 -0
  81. package/dist/universal/.trae-cn/skills/citable/SKILL.md +66 -1
  82. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  83. package/dist/universal/.trae-cn/skills/citable/manifest.json +9 -6
  84. package/dist/universal/.trae-cn/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  85. package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +69 -0
  86. package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +98 -0
  87. package/dist/universal/.trae-cn/skills/citable/schemas/sow.schema.json +92 -0
  88. package/dist/universal/manifest.json +112 -76
  89. package/package.json +1 -1
  90. package/schemas/cli-output-envelope.schema.json +30 -0
  91. package/schemas/finding.schema.json +69 -0
  92. package/schemas/remediation-verification.schema.json +98 -0
  93. package/schemas/sow.schema.json +92 -0
  94. package/skill/SKILL.md +66 -1
  95. package/src/analysis/behavioral.js +126 -0
  96. package/src/analysis/croAudit.js +302 -0
  97. package/src/analysis/croRoadmap.js +121 -0
  98. package/src/analysis/eeat.js +300 -0
  99. package/src/analysis/funnelAnalysis.js +171 -0
  100. package/src/analysis/iceMatrix.js +205 -0
  101. package/src/analysis/offpage.js +228 -0
  102. package/src/analysis/readiness.js +253 -0
  103. package/src/analysis/saliency.js +119 -0
  104. package/src/analysis/schemaRecommendations.js +173 -0
  105. package/src/analysis/strategicRoadmap.js +148 -0
  106. package/src/cli/index.js +418 -9
  107. package/src/commands/answerEngineReadiness.js +48 -0
  108. package/src/commands/audit.js +2 -2
  109. package/src/commands/auditBacklinks.js +79 -0
  110. package/src/commands/ciWorkflow.js +63 -0
  111. package/src/commands/compatibility.js +205 -0
  112. package/src/commands/context.js +2 -1
  113. package/src/commands/croBacklog.js +182 -0
  114. package/src/commands/croSuite.js +156 -0
  115. package/src/commands/edgeRules.js +80 -4
  116. package/src/commands/edgeSecurity.js +192 -0
  117. package/src/commands/executive.js +6 -0
  118. package/src/commands/executiveCro.js +30 -0
  119. package/src/commands/executiveSearch.js +28 -0
  120. package/src/commands/experimentGuardrails.js +213 -0
  121. package/src/commands/goldenCorpus.js +155 -0
  122. package/src/commands/implementationKit.js +202 -0
  123. package/src/commands/inspectCro.js +67 -0
  124. package/src/commands/inspectEeat.js +53 -0
  125. package/src/commands/planExperiment.js +52 -0
  126. package/src/commands/previewCro.js +241 -0
  127. package/src/commands/prioritize.js +42 -6
  128. package/src/commands/remediate.js +461 -0
  129. package/src/commands/roadmapCmd.js +69 -0
  130. package/src/commands/schemaCmd.js +10 -2
  131. package/src/commands/sowCmd.js +67 -0
  132. package/src/commands/sweep.js +357 -0
  133. package/src/commands/verifyRemediation.js +204 -0
  134. package/src/commands/visualRegression.js +165 -0
  135. package/src/components/index.js +556 -0
  136. package/src/detectors/cro.js +48 -0
  137. package/src/detectors/framework.js +31 -1
  138. package/src/reporting/executiveCroReport.js +731 -0
  139. package/src/reporting/executiveExport.js +175 -1
  140. package/src/reporting/executiveSearchReport.js +704 -0
  141. package/src/sow/admissibilityGate.js +194 -0
  142. package/src/sow/generateSow.js +762 -0
@@ -0,0 +1,53 @@
1
+ import { buildContext } from './context.js';
2
+ import { safePath } from '../detectors/framework.js';
3
+ import { evaluateEeat } from '../analysis/eeat.js';
4
+
5
+ /**
6
+ * `citable inspect eeat <page>` — evaluate on-page content against the E-E-A-T rubric (0-5 scale).
7
+ */
8
+ export async function inspectEeat(root, pageRef, { target, baseUrl, refDate } = {}) {
9
+ const ctx = await buildContext(root, { target, baseUrl, refDate });
10
+ if (!ctx.site) throw new Error('inspect eeat requires a site target (built output directory or URL)');
11
+ const wanted = safePath(pageRef, ctx.site.baseUrl);
12
+ const page = ctx.site.pages.find((p) => safePath(p.url) === wanted || p.sourceFile === pageRef);
13
+ if (!page) throw new Error(`page not found in audited output: ${pageRef}`);
14
+
15
+ return evaluateEeat(page, ctx);
16
+ }
17
+
18
+ /**
19
+ * Format terminal output for `citable inspect eeat <page>`
20
+ */
21
+ export function formatEeatOutput(r) {
22
+ const lines = [
23
+ `Inspect E-E-A-T: ${r.url}`,
24
+ `========================================`,
25
+ `Composite Score: ${r.composite_score} / 5.0 [${r.overall_rating.toUpperCase()}]`,
26
+ `Status: ${r.fact_status} (Google Search Quality Rater Guidelines rubric)`,
27
+ ``,
28
+ `DIMENSIONS (0.0 - 5.0 scale):`,
29
+ ` Experience: ${r.dimensions.experience.score} / 5.0`,
30
+ ` Expertise: ${r.dimensions.expertise.score} / 5.0 (Author: ${r.dimensions.expertise.author || 'unattributed'})`,
31
+ ` Authoritativeness: ${r.dimensions.authoritativeness.score} / 5.0`,
32
+ ` Trustworthiness: ${r.dimensions.trustworthiness.score} / 5.0 (Foundational Base)`,
33
+ ``,
34
+ `OBSERVED POSITIVE SIGNALS:`,
35
+ ];
36
+
37
+ for (const [dim, val] of Object.entries(r.dimensions)) {
38
+ if (val.signals.length) {
39
+ lines.push(` ${dim.toUpperCase()}:`);
40
+ for (const s of val.signals) lines.push(` ✓ ${s}`);
41
+ }
42
+ }
43
+
44
+ if (r.actionable_improvements.length) {
45
+ lines.push(``, `ACTIONABLE IMPROVEMENTS:`);
46
+ for (const item of r.actionable_improvements) {
47
+ lines.push(` - [${item.dimension.toUpperCase()}] ${item.recommendation}`);
48
+ }
49
+ }
50
+
51
+ lines.push(``, `Note: ${r.disclaimer}`);
52
+ return lines.join('\n');
53
+ }
@@ -52,7 +52,56 @@ export async function planExperiment(root, options = {}) {
52
52
  recommendations.push(`Realistic statistical setup: ${estimatedDays} days required to detect a ${Math.round(mde * 100)}% lift with 80% power at 95% confidence.`);
53
53
  }
54
54
 
55
+ const experimentId = options.id || options.experimentId || 'EXP-CRO-AUTO';
56
+ const targetPage = options.page || options.targetPage || '/pricing';
57
+ const primaryMetric = options.metric || 'conversion_rate';
58
+
59
+ const blueprint = {
60
+ experiment_id: experimentId,
61
+ discipline: 'cro',
62
+ target_page: targetPage,
63
+ status: 'planned',
64
+ hypothesis: options.hypothesis || 'Replacing high-friction hero with NebulaHeroCTA increases primary CTA conversion.',
65
+ primary_metric: primaryMetric,
66
+ evaluation_window: `${Math.max(estimatedDays, 7)} days`,
67
+ sample_size_per_variant: nPerVariant,
68
+ variants: [
69
+ { id: 'control', name: 'Original Page (Baseline)', traffic_pct: 50 },
70
+ { id: 'variant_nebula', name: 'Nebula Remediated Component', traffic_pct: 50 },
71
+ ],
72
+ };
73
+
74
+ const edgeVariantRouterCode = `// Zero-Flicker Edge A/B Variant Router (MurmurHash3 / FNV-1a)
75
+ export function routeExperiment(request) {
76
+ const visitorId = request.headers.get('cf-connecting-ip') || 'anon';
77
+ let hash = 2166136261;
78
+ const str = visitorId + ':${experimentId}';
79
+ for (let i = 0; i < str.length; i++) {
80
+ hash ^= str.charCodeAt(i);
81
+ hash = Math.imul(hash, 16777619);
82
+ }
83
+ const bucket = Math.abs(hash % 100);
84
+ return bucket < 50 ? 'control' : 'variant_nebula';
85
+ }`;
86
+
87
+ const telemetryCode = `// Schema-Valid Conversion Event Telemetry Envelope
88
+ export function trackConversion(experimentId, variantId, actionName) {
89
+ const payload = {
90
+ kind: 'cro_conversion_event',
91
+ experiment_id: experimentId,
92
+ variant_id: variantId,
93
+ action: actionName,
94
+ timestamp: new Date().toISOString(),
95
+ };
96
+ if (navigator.sendBeacon) {
97
+ navigator.sendBeacon('/api/citable/events', JSON.stringify(payload));
98
+ } else {
99
+ fetch('/api/citable/events', { method: 'POST', body: JSON.stringify(payload), keepalive: true });
100
+ }
101
+ }`;
102
+
55
103
  return {
104
+ experiment_id: experimentId,
56
105
  baseline_conversion_rate: p1,
57
106
  target_conversion_rate: Number(p2.toFixed(4)),
58
107
  minimum_detectable_effect_relative: mde,
@@ -63,6 +112,9 @@ export async function planExperiment(root, options = {}) {
63
112
  total_sample_size: totalSample,
64
113
  estimated_duration_days: estimatedDays,
65
114
  risk_level: riskLevel,
115
+ blueprint,
116
+ edge_variant_router_code: edgeVariantRouterCode,
117
+ telemetry_code: telemetryCode,
66
118
  recommendations,
67
119
  };
68
120
  }
@@ -0,0 +1,241 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { inspectCro } from './inspectCro.js';
4
+
5
+ export function generateCroPreviewHtml({
6
+ url = 'https://example.com/pricing',
7
+ pageType = 'pricing',
8
+ findings = [],
9
+ fsaBefore = 35,
10
+ fsaAfter = 0,
11
+ }) {
12
+ const findingItems = findings.length > 0 ? findings : [
13
+ { detector_id: 'CRO-007', summary: 'Input fields missing W3C autocomplete tokens', severity: 'medium' },
14
+ { detector_id: 'CRO-010', summary: 'Hero section contains choice overload (3 competing CTAs)', severity: 'high' },
15
+ { detector_id: 'CRO-011', summary: 'Primary CTA buried on long-form mobile viewport', severity: 'medium' },
16
+ { detector_id: 'CRO-015', summary: 'Button touch targets below 44px threshold (measured 36px)', severity: 'high' },
17
+ ];
18
+
19
+ return `<!DOCTYPE html>
20
+ <html lang="en">
21
+ <head>
22
+ <meta charset="utf-8" />
23
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
24
+ <title>Nebula CRO Interactive Preview — ${url}</title>
25
+ <style>
26
+ * { box-sizing: border-box; margin: 0; padding: 0; }
27
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #f8fafc; }
28
+ header { background: #1e293b; border-bottom: 1px solid #334155; padding: 1rem 1.5rem; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 1rem; }
29
+ .brand { display: flex; align-items: center; gap: 0.75rem; font-weight: 700; font-size: 1.15rem; color: #38bdf8; }
30
+ .metrics { display: flex; gap: 1.5rem; }
31
+ .metric-pill { background: #0f172a; padding: 0.5rem 0.85rem; border-radius: 0.5rem; border: 1px solid #334155; font-size: 0.85rem; }
32
+ .metric-pill strong { color: #38bdf8; }
33
+ .metric-pill.good strong { color: #4ade80; }
34
+ .viewports { display: flex; gap: 0.5rem; align-items: center; }
35
+ .vp-btn { background: #334155; color: #f8fafc; border: none; padding: 0.4rem 0.75rem; border-radius: 0.375rem; font-size: 0.8rem; font-weight: 600; cursor: pointer; transition: all 0.2s; }
36
+ .vp-btn:hover { background: #475569; }
37
+ .vp-btn.active { background: #2563eb; color: #fff; }
38
+ main { padding: 1.5rem; max-width: 1600px; margin: 0 auto; }
39
+ .split-container { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; transition: max-width 0.3s; margin: 0 auto; }
40
+ .preview-card { background: #1e293b; border-radius: 0.75rem; border: 1px solid #334155; overflow: hidden; display: flex; flex-direction: column; }
41
+ .card-header { padding: 0.85rem 1.25rem; font-weight: 600; display: flex; justify-content: space-between; align-items: center; font-size: 0.95rem; }
42
+ .card-header.before { background: #450a0a; color: #fca5a5; border-bottom: 1px solid #7f1d1d; }
43
+ .card-header.after { background: #052e16; color: #86efac; border-bottom: 1px solid #14532d; }
44
+ .frame-wrapper { background: #fff; color: #0f172a; padding: 1.5rem; flex: 1; min-height: 520px; font-size: 0.9rem; position: relative; }
45
+ .hotspot { border: 2px dashed #dc2626; background: rgba(239, 68, 68, 0.08); padding: 0.75rem; border-radius: 0.5rem; position: relative; margin-bottom: 1rem; }
46
+ .hotspot-tag { position: absolute; top: -10px; right: 10px; background: #dc2626; color: #fff; font-size: 0.65rem; font-weight: 700; padding: 0.15rem 0.4rem; border-radius: 0.25rem; text-transform: uppercase; }
47
+ .remediation-box { border: 2px solid #16a34a; background: rgba(34, 197, 94, 0.08); padding: 0.75rem; border-radius: 0.5rem; position: relative; margin-bottom: 1rem; }
48
+ .remediation-tag { position: absolute; top: -10px; right: 10px; background: #16a34a; color: #fff; font-size: 0.65rem; font-weight: 700; padding: 0.15rem 0.4rem; border-radius: 0.25rem; text-transform: uppercase; }
49
+ .btn-mock { display: inline-flex; align-items: center; justify-content: center; font-weight: 600; border-radius: 0.375rem; text-decoration: none; cursor: pointer; }
50
+ .btn-broken { height: 32px; padding: 0 12px; font-size: 11px; background: #64748b; color: #fff; border: none; }
51
+ .btn-nebula { min-height: 48px; min-width: 160px; padding: 0 24px; font-size: 16px; background: #2563eb; color: #fff; border: none; border-radius: 0.5rem; box-shadow: 0 4px 6px -1px rgba(37,99,235,0.2); }
52
+ .btn-secondary { min-height: 48px; padding: 0 16px; font-size: 14px; background: transparent; color: #475569; text-decoration: underline; border: none; }
53
+ .findings-list { margin-top: 1.5rem; background: #1e293b; border-radius: 0.75rem; padding: 1.25rem; border: 1px solid #334155; }
54
+ .findings-list h3 { font-size: 1rem; margin-bottom: 0.75rem; color: #38bdf8; }
55
+ .finding-row { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #334155; font-size: 0.85rem; }
56
+ .finding-badge { font-weight: 700; font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 0.25rem; }
57
+ .badge-high { background: #7f1d1d; color: #fca5a5; }
58
+ .badge-medium { background: #713f12; color: #fde047; }
59
+ .badge-low { background: #1e293b; color: #94a3b8; }
60
+ /* Keyboard navigation: visible focus indicators on all interactive elements */
61
+ .vp-btn:focus-visible, .btn-mock:focus-visible { outline: 3px solid #38bdf8; outline-offset: 2px; }
62
+ input:focus-visible { outline: 3px solid #2563eb; outline-offset: 1px; }
63
+ /* Reduced motion: disable transitions/animations when requested */
64
+ @media (prefers-reduced-motion: reduce) {
65
+ *, *::before, *::after { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
66
+ }
67
+ /* Narrow mobile: collapse the split view below 480px regardless of the toggle */
68
+ @media (max-width: 480px) {
69
+ .split-container { grid-template-columns: 1fr !important; }
70
+ .metrics { flex-wrap: wrap; }
71
+ main { padding: 0.75rem; }
72
+ }
73
+ /* Text zoom and long translations must wrap, not clip */
74
+ .frame-wrapper, .finding-row span:first-child { overflow-wrap: break-word; word-break: break-word; }
75
+ /* Dark/light browser preference: adapt the preview chrome, keep frames readable */
76
+ @media (prefers-color-scheme: light) {
77
+ body { background: #e2e8f0; color: #0f172a; }
78
+ header { background: #ffffff; border-bottom-color: #cbd5e1; }
79
+ .preview-card, .findings-list { background: #ffffff; border-color: #cbd5e1; }
80
+ .metric-pill { background: #f1f5f9; border-color: #cbd5e1; }
81
+ }
82
+ </style>
83
+ </head>
84
+ <body>
85
+ <header>
86
+ <div class="brand">
87
+ <span>⚡ Nebula Components</span>
88
+ <span style="color:#94a3b8; font-weight:400; font-size:0.9rem;">| Interactive CRO Sandbox</span>
89
+ </div>
90
+ <div class="metrics">
91
+ <div class="metric-pill">Target: <strong>${url}</strong></div>
92
+ <div class="metric-pill">Friction Surface Area (Before): <strong style="color:#f87171;">${fsaBefore}</strong></div>
93
+ <div class="metric-pill good">Friction Surface Area (Modeled Target): <strong>${fsaAfter}</strong> <span style="color:#94a3b8;">modeled index, not a measured outcome</span></div>
94
+ </div>
95
+ <div class="viewports">
96
+ <span style="font-size:0.8rem; color:#94a3b8; margin-right:0.25rem;">Viewport:</span>
97
+ <button type="button" class="vp-btn" aria-pressed="false" onclick="setVp('375px', this)">Mobile (375px)</button>
98
+ <button type="button" class="vp-btn" aria-pressed="false" onclick="setVp('768px', this)">Tablet (768px)</button>
99
+ <button type="button" class="vp-btn active" aria-pressed="true" onclick="setVp('100%', this)">Desktop (Full)</button>
100
+ </div>
101
+ </header>
102
+
103
+ <main>
104
+ <div id="splitContainer" class="split-container">
105
+ <!-- Original Side -->
106
+ <div class="preview-card">
107
+ <div class="card-header before">
108
+ <span>❌ Current Audited State (Friction Hotspots)</span>
109
+ <span>FSA: ${fsaBefore}</span>
110
+ </div>
111
+ <div class="frame-wrapper">
112
+ <h2 style="font-size:1.25rem; font-weight:700; margin-bottom:0.75rem;">Scale Your Enterprise Architecture</h2>
113
+ <p style="color:#64748b; font-size:0.85rem; margin-bottom:1rem;">The leading cloud platform for distributed teams and mission-critical workflows.</p>
114
+
115
+ <div class="hotspot">
116
+ <span class="hotspot-tag">CRO-010 • Choice Overload</span>
117
+ <div style="display:flex; gap:0.5rem; flex-wrap:wrap;">
118
+ <button class="btn-broken" style="background:#dc2626;">Get Started</button>
119
+ <button class="btn-broken" style="background:#475569;">Contact Sales</button>
120
+ <button class="btn-broken" style="background:#0284c7;">Read Whitepaper</button>
121
+ </div>
122
+ </div>
123
+
124
+ <div class="hotspot">
125
+ <span class="hotspot-tag">CRO-007 / CRO-008 • Form Friction</span>
126
+ <div style="display:flex; flex-direction:column; gap:0.5rem;">
127
+ <input type="text" placeholder="Your Name" style="height:32px; padding:0 8px; border:1px solid #cbd5e1; border-radius:4px; font-size:12px;" />
128
+ <input type="text" placeholder="Email Address" style="height:32px; padding:0 8px; border:1px solid #cbd5e1; border-radius:4px; font-size:12px;" />
129
+ <button class="btn-broken" style="background:#2563eb; width:100px;">Submit</button>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </div>
134
+
135
+ <!-- Remediated Side -->
136
+ <div class="preview-card">
137
+ <div class="card-header after">
138
+ <span>✅ Remediated with Nebula Components</span>
139
+ <span>FSA modeled target: ${fsaAfter}</span>
140
+ </div>
141
+ <div class="frame-wrapper">
142
+ <h2 style="font-size:1.25rem; font-weight:700; margin-bottom:0.75rem;">Scale Your Enterprise Architecture</h2>
143
+ <p style="color:#64748b; font-size:0.85rem; margin-bottom:1rem;">The leading cloud platform for distributed teams and mission-critical workflows.</p>
144
+
145
+ <div class="remediation-box">
146
+ <span class="remediation-tag">NebulaHeroCTA</span>
147
+ <div style="display:flex; align-items:center; gap:0.75rem;">
148
+ <a href="/signup" class="btn-mock btn-nebula">Get Started Free</a>
149
+ <a href="#demo" class="btn-mock btn-secondary">View Live Demo</a>
150
+ </div>
151
+ </div>
152
+
153
+ <div class="remediation-box">
154
+ <span class="remediation-tag">NebulaFrictionlessForm</span>
155
+ <div style="display:flex; flex-direction:column; gap:0.75rem;">
156
+ <div style="position:relative;">
157
+ <input id="demo-name" type="text" autocomplete="name" placeholder="Full Name" style="width:100%; min-height:48px; padding:0 12px; border:1px solid #cbd5e1; border-radius:6px; font-size:15px;" />
158
+ </div>
159
+ <div style="position:relative;">
160
+ <input id="demo-email" type="email" inputmode="email" autocomplete="email" placeholder="Work Email" style="width:100%; min-height:48px; padding:0 12px; border:1px solid #cbd5e1; border-radius:6px; font-size:15px;" />
161
+ </div>
162
+ <button class="btn-mock btn-nebula" style="width:100%;">Complete Signup</button>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <div class="findings-list">
170
+ <h3>Resolved CRO Defects (${findingItems.length})</h3>
171
+ ${findingItems.map((f) => `
172
+ <div class="finding-row">
173
+ <span><strong>${f.detector_id}</strong>: ${f.summary}</span>
174
+ <span class="finding-badge badge-${f.severity || 'medium'}">${(f.severity || 'medium').toUpperCase()}</span>
175
+ </div>
176
+ `).join('')}
177
+ </div>
178
+ </main>
179
+
180
+ <script>
181
+ function setVp(width, btn) {
182
+ const container = document.getElementById('splitContainer');
183
+ document.querySelectorAll('.vp-btn').forEach(b => { b.classList.remove('active'); b.setAttribute('aria-pressed', 'false'); });
184
+ btn.classList.add('active');
185
+ btn.setAttribute('aria-pressed', 'true');
186
+ if (width === '100%') {
187
+ container.style.maxWidth = '100%';
188
+ container.style.gridTemplateColumns = '1fr 1fr';
189
+ } else {
190
+ container.style.maxWidth = width;
191
+ container.style.gridTemplateColumns = '1fr';
192
+ }
193
+ }
194
+ </script>
195
+ </body>
196
+ </html>`;
197
+ }
198
+
199
+ export async function previewCroCommand(root, page = 'home', options = {}) {
200
+ const { target, export: exportPath } = options;
201
+ let croData = null;
202
+
203
+ try {
204
+ croData = await inspectCro(root, page, { target });
205
+ } catch (err) {
206
+ // Graceful fallback for mock preview
207
+ croData = { url: page, findings: [], declared_conversion_action: 'Get Started' };
208
+ }
209
+
210
+ const findings = croData?.findings || [];
211
+ const fsaBefore = findings.reduce((acc, f) => acc + (f.severity === 'high' ? 10 : f.severity === 'medium' ? 5 : 2), 0) || 30;
212
+ const fsaAfter = 0;
213
+
214
+ const html = generateCroPreviewHtml({
215
+ url: croData.url || page,
216
+ pageType: croData.page_type || 'commercial',
217
+ findings,
218
+ fsaBefore,
219
+ fsaAfter,
220
+ });
221
+
222
+ let savedFile = null;
223
+ if (exportPath) {
224
+ const outFull = path.resolve(root, exportPath);
225
+ fs.mkdirSync(path.dirname(outFull), { recursive: true });
226
+ fs.writeFileSync(outFull, html, 'utf8');
227
+ savedFile = outFull;
228
+ }
229
+
230
+ return {
231
+ ok: true,
232
+ url: croData.url || page,
233
+ findings_count: findings.length,
234
+ fsa_before: fsaBefore,
235
+ fsa_after: fsaAfter,
236
+ fsa_eliminated_pct: null,
237
+ fsa_note: 'FSA is a modeled heuristic index; the remediated value is a projection of applied components, not a measured outcome',
238
+ saved_file: savedFile,
239
+ html,
240
+ };
241
+ }
@@ -1,19 +1,21 @@
1
1
  /**
2
- * prioritize command — Initiative Prioritization
2
+ * prioritize command — Initiative and Finding Prioritization (ICE Matrix)
3
3
  *
4
- * Ranks roadmap and investment choices. Weights must be visible when
5
- * weighted scoring is used — no opaque scores.
4
+ * Ranks roadmap, audit findings, and investment choices.
5
+ * Weights and factors must be visible when weighted scoring is used — no opaque scores.
6
6
  *
7
7
  * Usage:
8
8
  * citable prioritize [--status proposed|approved]
9
9
  * citable prioritize show <initiative_id>
10
10
  * citable prioritize validate
11
- * citable prioritize rank — ranked list with explicit criteria
11
+ * citable prioritize rank — ranked list with explicit criteria
12
+ * citable prioritize matrix — Impact/Effort/Confidence (ICE) scoring matrix
12
13
  */
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
13
16
  import { contextDir, loadRegistryFile, registryLoadProblems } from '../registries/index.js';
14
17
  import { validateAgainst } from '../shared/schemaValidator.js';
15
- import path from 'node:path';
16
-
18
+ import { buildIceMatrix, formatIceMatrixOutput } from '../analysis/iceMatrix.js';
17
19
 
18
20
  const SCALE = { none: 0, low: 1, medium: 2, high: 3, critical: 4, validated: 4, transformative: 4, trivial: 0, very_high: 4, negligible: 0, 'n/a': 0 };
19
21
 
@@ -25,6 +27,7 @@ export async function prioritizeCommand(args, root = process.cwd()) {
25
27
  case 'show': return initiativeShow(file, rest[0]);
26
28
  case 'validate': return initiativeValidate(file);
27
29
  case 'rank': return initiativeRank(file);
30
+ case 'matrix': return initiativeMatrix(file, root, rest);
28
31
  case 'list':
29
32
  default: {
30
33
  const statusFilter = rest[rest.indexOf('--status') + 1] ?? null;
@@ -86,6 +89,39 @@ function initiativeRank(file) {
86
89
  };
87
90
  }
88
91
 
92
+ function initiativeMatrix(file, root, rest = []) {
93
+ const runIdx = rest.indexOf('--run');
94
+ const runId = runIdx !== -1 ? rest[runIdx + 1] : null;
95
+
96
+ if (runId) {
97
+ const findingsPath = path.join(root, '.citable', 'runs', runId, 'findings.json');
98
+ if (!fs.existsSync(findingsPath)) throw new Error(`run ${runId} findings.json not found`);
99
+ const findings = JSON.parse(fs.readFileSync(findingsPath, 'utf8'));
100
+ const matrix = buildIceMatrix(findings, { type: 'findings' });
101
+ return { ...matrix, formatted: formatIceMatrixOutput(matrix) };
102
+ }
103
+
104
+ // Check if findings.json exists in latest run
105
+ const runsDir = path.join(root, '.citable', 'runs');
106
+ if (fs.existsSync(runsDir)) {
107
+ const runs = fs.readdirSync(runsDir).filter((r) => fs.existsSync(path.join(runsDir, r, 'findings.json'))).sort().reverse();
108
+ if (runs.length > 0 && rest.includes('--findings')) {
109
+ const findings = JSON.parse(fs.readFileSync(path.join(runsDir, runs[0], 'findings.json'), 'utf8'));
110
+ const matrix = buildIceMatrix(findings, { type: 'findings' });
111
+ return { ...matrix, formatted: formatIceMatrixOutput(matrix) };
112
+ }
113
+ }
114
+
115
+ // Default: score initiatives from initiatives.yaml
116
+ if (fs.existsSync(file)) {
117
+ const data = load(file);
118
+ const matrix = buildIceMatrix(data.entries || [], { type: 'initiatives' });
119
+ return { ...matrix, formatted: formatIceMatrixOutput(matrix) };
120
+ }
121
+
122
+ throw new Error('prioritize matrix requires initiatives.yaml or an audit run (--run <run-id>)');
123
+ }
124
+
89
125
  function initiativeValidate(file) {
90
126
  const data = load(file);
91
127
  const problems = [...registryLoadProblems(data)];