@nebulacomponents/citable 1.17.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 (84) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/universal/.agents/skills/citable/SKILL.md +20 -1
  3. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  4. package/dist/universal/.agents/skills/citable/manifest.json +6 -5
  5. package/dist/universal/.agents/skills/citable/schemas/sow.schema.json +92 -0
  6. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  7. package/dist/universal/.claude/skills/citable/SKILL.md +20 -1
  8. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  9. package/dist/universal/.claude/skills/citable/manifest.json +6 -5
  10. package/dist/universal/.claude/skills/citable/schemas/sow.schema.json +92 -0
  11. package/dist/universal/.cursor/skills/citable/SKILL.md +20 -1
  12. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  13. package/dist/universal/.cursor/skills/citable/manifest.json +6 -5
  14. package/dist/universal/.cursor/skills/citable/schemas/sow.schema.json +92 -0
  15. package/dist/universal/.gemini/skills/citable/SKILL.md +20 -1
  16. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  17. package/dist/universal/.gemini/skills/citable/manifest.json +6 -5
  18. package/dist/universal/.gemini/skills/citable/schemas/sow.schema.json +92 -0
  19. package/dist/universal/.github/skills/citable/SKILL.md +20 -1
  20. package/dist/universal/.github/skills/citable/VERSION +1 -1
  21. package/dist/universal/.github/skills/citable/manifest.json +6 -5
  22. package/dist/universal/.github/skills/citable/schemas/sow.schema.json +92 -0
  23. package/dist/universal/.kiro/skills/citable/SKILL.md +20 -1
  24. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  25. package/dist/universal/.kiro/skills/citable/manifest.json +6 -5
  26. package/dist/universal/.kiro/skills/citable/schemas/sow.schema.json +92 -0
  27. package/dist/universal/.opencode/skills/citable/SKILL.md +20 -1
  28. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  29. package/dist/universal/.opencode/skills/citable/manifest.json +6 -5
  30. package/dist/universal/.opencode/skills/citable/schemas/sow.schema.json +92 -0
  31. package/dist/universal/.pi/agent/skills/citable/SKILL.md +20 -1
  32. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  33. package/dist/universal/.pi/agent/skills/citable/manifest.json +6 -5
  34. package/dist/universal/.pi/agent/skills/citable/schemas/sow.schema.json +92 -0
  35. package/dist/universal/.qoder/skills/citable/SKILL.md +20 -1
  36. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  37. package/dist/universal/.qoder/skills/citable/manifest.json +6 -5
  38. package/dist/universal/.qoder/skills/citable/schemas/sow.schema.json +92 -0
  39. package/dist/universal/.rovodev/skills/citable/SKILL.md +20 -1
  40. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  41. package/dist/universal/.rovodev/skills/citable/manifest.json +6 -5
  42. package/dist/universal/.rovodev/skills/citable/schemas/sow.schema.json +92 -0
  43. package/dist/universal/.trae/skills/citable/SKILL.md +20 -1
  44. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  45. package/dist/universal/.trae/skills/citable/manifest.json +6 -5
  46. package/dist/universal/.trae/skills/citable/schemas/sow.schema.json +92 -0
  47. package/dist/universal/.trae-cn/skills/citable/SKILL.md +20 -1
  48. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  49. package/dist/universal/.trae-cn/skills/citable/manifest.json +6 -5
  50. package/dist/universal/.trae-cn/skills/citable/schemas/sow.schema.json +92 -0
  51. package/dist/universal/manifest.json +76 -64
  52. package/package.json +1 -1
  53. package/schemas/sow.schema.json +92 -0
  54. package/skill/SKILL.md +20 -1
  55. package/src/analysis/behavioral.js +126 -0
  56. package/src/analysis/croAudit.js +302 -0
  57. package/src/analysis/croRoadmap.js +121 -0
  58. package/src/analysis/eeat.js +300 -0
  59. package/src/analysis/funnelAnalysis.js +171 -0
  60. package/src/analysis/iceMatrix.js +205 -0
  61. package/src/analysis/offpage.js +228 -0
  62. package/src/analysis/readiness.js +253 -0
  63. package/src/analysis/schemaRecommendations.js +173 -0
  64. package/src/analysis/strategicRoadmap.js +148 -0
  65. package/src/cli/index.js +149 -3
  66. package/src/commands/answerEngineReadiness.js +48 -0
  67. package/src/commands/auditBacklinks.js +79 -0
  68. package/src/commands/croBacklog.js +182 -0
  69. package/src/commands/croSuite.js +156 -0
  70. package/src/commands/executive.js +6 -0
  71. package/src/commands/executiveCro.js +30 -0
  72. package/src/commands/executiveSearch.js +28 -0
  73. package/src/commands/inspectCro.js +7 -0
  74. package/src/commands/inspectEeat.js +53 -0
  75. package/src/commands/prioritize.js +42 -6
  76. package/src/commands/roadmapCmd.js +69 -0
  77. package/src/commands/schemaCmd.js +10 -2
  78. package/src/commands/sowCmd.js +67 -0
  79. package/src/commands/sweep.js +357 -0
  80. package/src/reporting/executiveCroReport.js +731 -0
  81. package/src/reporting/executiveExport.js +62 -0
  82. package/src/reporting/executiveSearchReport.js +704 -0
  83. package/src/sow/admissibilityGate.js +194 -0
  84. package/src/sow/generateSow.js +762 -0
@@ -0,0 +1,300 @@
1
+ import { parse as parseHtml } from 'node-html-parser';
2
+ import { registryPageFor, safePath } from '../detectors/framework.js';
3
+
4
+ /**
5
+ * Evaluates on-page content against Google's E-E-A-T rubric on a 0.0 - 5.0 scale.
6
+ *
7
+ * Premise 2 compliance:
8
+ * - fact_status: 'modeled_rubric_evaluation'
9
+ * - E-E-A-T is an evaluative framework from the Search Quality Rater Guidelines,
10
+ * NOT a direct algorithmic ranking factor or search engine API metric.
11
+ */
12
+ export function evaluateEeat(page, ctx = {}) {
13
+ const html = page.html || '';
14
+ const root = parseHtml(html);
15
+ const text = (page.text || root.textContent || '').replace(/\s+/g, ' ').trim();
16
+ const lowerText = text.toLowerCase();
17
+ const reg = registryPageFor(ctx, page);
18
+
19
+ // -------------------------------------------------------------
20
+ // 1. EXPERIENCE (0.0 - 5.0)
21
+ // -------------------------------------------------------------
22
+ const expSignals = [];
23
+ const expMissing = [];
24
+ let expScore = 1.0;
25
+
26
+ // First-person practitioner indicators
27
+ const FIRST_PERSON_RX = /\b(?:we tested|in our testing|we evaluated|our evaluation|we measured|our analysis|we observed|our findings|in our experience|our team found|hands-on|case study|we verified|we deployed)\b/i;
28
+ if (FIRST_PERSON_RX.test(text)) {
29
+ expScore += 1.2;
30
+ expSignals.push('First-person practitioner observations and testing language detected');
31
+ } else {
32
+ expMissing.push('Add first-hand testing observations, case studies, or practitioner findings');
33
+ }
34
+
35
+ // Original media / demonstrations / code artifacts
36
+ const codeBlocks = root.querySelectorAll('pre, code, table');
37
+ if (codeBlocks.length >= 2) {
38
+ expScore += 1.0;
39
+ expSignals.push(`Structured demonstration artifacts present (${codeBlocks.length} code/table blocks)`);
40
+ } else {
41
+ expMissing.push('Include original demonstration tables, code snippets, or walkthroughs');
42
+ }
43
+
44
+ // Original images with descriptive captions/alt text
45
+ const images = page.images || root.querySelectorAll('img');
46
+ const validImages = images.filter((img) => {
47
+ const alt = typeof img === 'object' && img.getAttribute ? img.getAttribute('alt') : img.alt;
48
+ return Boolean(alt && alt.length > 5);
49
+ });
50
+ if (validImages.length >= 1) {
51
+ expScore += 0.8;
52
+ expSignals.push(`Descriptive contextual images present (${validImages.length} with detailed alt text)`);
53
+ }
54
+
55
+ // Quantitative measurement / benchmark data
56
+ const METRIC_DATA_RX = /(?:\b\d+(?:\.\d+)?%|\b\d+\s*ms\b|\b\d+\s*s\b|\$\d+(?:,\d{3})*|\bbenchmark\b|\bresults showed\b)/i;
57
+ if (METRIC_DATA_RX.test(text)) {
58
+ expScore += 1.0;
59
+ expSignals.push('Quantitative experimental or benchmark metrics presented');
60
+ } else {
61
+ expMissing.push('Include verifiable quantitative benchmark data or empirical results');
62
+ }
63
+
64
+ expScore = Math.min(5.0, Math.max(0.0, Math.round(expScore * 10) / 10));
65
+
66
+ // -------------------------------------------------------------
67
+ // 2. EXPERTISE (0.0 - 5.0)
68
+ // -------------------------------------------------------------
69
+ const expertSignals = [];
70
+ const expertMissing = [];
71
+ let expertScore = 1.0;
72
+
73
+ // Author attribution in page or schema
74
+ let authorName = null;
75
+ let authorSameAs = null;
76
+ for (const j of (page.jsonLd || [])) {
77
+ for (const b of (j.blocks || [])) {
78
+ if (b.author) {
79
+ const authorObj = Array.isArray(b.author) ? b.author[0] : b.author;
80
+ authorName = authorObj?.name || (typeof authorObj === 'string' ? authorObj : null);
81
+ authorSameAs = authorObj?.sameAs || null;
82
+ }
83
+ }
84
+ }
85
+
86
+ // Visible byline check
87
+ const BYLINE_RX = /\b(?:written by|authored by|author:|by)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)/i;
88
+ const bylineMatch = text.match(BYLINE_RX);
89
+ if (!authorName && bylineMatch) {
90
+ authorName = bylineMatch[1];
91
+ }
92
+
93
+ if (authorName) {
94
+ expertScore += 1.5;
95
+ expertSignals.push(`Attributed author identified: "${authorName}"`);
96
+ } else {
97
+ expertMissing.push('Add explicit author byline with credentials or Person schema');
98
+ }
99
+
100
+ // Author credentials / biography / authoritative profiles
101
+ const BIO_RX = /\b(?:ph\.?d|m\.?d|engineer|architect|researcher|director|specialist|consultant|analyst|professor|founder|lead)\b/i;
102
+ if (authorSameAs || BIO_RX.test(text)) {
103
+ expertScore += 1.2;
104
+ expertSignals.push('Author credentials, professional title, or external disambiguation profiles verified');
105
+ } else {
106
+ expertMissing.push('Link author to authoritative profile (sameAs: LinkedIn, ORCID, GitHub) and list credentials');
107
+ }
108
+
109
+ // Content depth & semantic heading hierarchy
110
+ const wordCount = page.rawVisibleWordCount || page.wordCount || text.split(/\s+/).filter(Boolean).length;
111
+ const headings = page.headings || [];
112
+ if (wordCount >= 600 && headings.length >= 3) {
113
+ expertScore += 1.3;
114
+ expertSignals.push(`Substantive technical depth (${wordCount} words, ${headings.length} headings)`);
115
+ } else if (wordCount < 300) {
116
+ expertMissing.push('Expand content depth to address topic comprehensively (currently thin <300 words)');
117
+ }
118
+
119
+ expertScore = Math.min(5.0, Math.max(0.0, Math.round(expertScore * 10) / 10));
120
+
121
+ // -------------------------------------------------------------
122
+ // 3. AUTHORITATIVENESS (0.0 - 5.0)
123
+ // -------------------------------------------------------------
124
+ const authSignals = [];
125
+ const authMissing = [];
126
+ let authScore = 1.0;
127
+
128
+ // External primary source citations (.edu, .gov, standards, docs)
129
+ const links = page.links || root.querySelectorAll('a').map((a) => ({
130
+ href: a.getAttribute('href') || '',
131
+ text: a.text.trim(),
132
+ }));
133
+
134
+ const externalLinks = links.filter((l) => /^https?:\/\//i.test(l.href));
135
+ const authoritativeDomains = externalLinks.filter((l) => {
136
+ const u = l.href.toLowerCase();
137
+ return u.includes('.edu') || u.includes('.gov') || u.includes('.org') ||
138
+ u.includes('w3.org') || u.includes('rfc-editor.org') || u.includes('github.com') ||
139
+ u.includes('arxiv.org') || u.includes('developer.mozilla.org') || u.includes('web.dev');
140
+ });
141
+
142
+ if (authoritativeDomains.length >= 2) {
143
+ authScore += 1.5;
144
+ authSignals.push(`Primary authority citations present (${authoritativeDomains.length} citations to academic/standard/industry domains)`);
145
+ } else if (externalLinks.length >= 1) {
146
+ authScore += 0.8;
147
+ authSignals.push(`External citations present (${externalLinks.length} external links)`);
148
+ authMissing.push('Cite recognized primary sources (standards organizations, academic research, official documentation)');
149
+ } else {
150
+ authMissing.push('Add outbound citations to primary sources and industry standards');
151
+ }
152
+
153
+ // Registry claim grounding
154
+ const claims = ctx.registries?.claims?.entries || [];
155
+ const pageClaims = claims.filter((c) => c.canonical_url === page.url || c.claim_id);
156
+ const verifiedClaims = pageClaims.filter((c) => c.status === 'verified');
157
+ if (verifiedClaims.length >= 1) {
158
+ authScore += 1.5;
159
+ authSignals.push(`Page claims backed by verified evidence records (${verifiedClaims.length} verified claims)`);
160
+ } else {
161
+ authMissing.push('Ground material claims in evidence registry with primary citations');
162
+ }
163
+
164
+ // Publisher / Organization entity graph
165
+ let hasOrgSchema = false;
166
+ for (const j of (page.jsonLd || [])) {
167
+ for (const b of (j.blocks || [])) {
168
+ const type = [].concat(b['@type'] || []).join(',');
169
+ const pubType = [].concat(b.publisher?.["@type"] || []).join(",");
170
+ if (/Organization|Corporation/i.test(type) || /Organization|Corporation/i.test(pubType)) hasOrgSchema = true;
171
+ }
172
+ }
173
+ if (hasOrgSchema) {
174
+ authScore += 1.0;
175
+ authSignals.push('Publisher Organization schema graph present');
176
+ } else {
177
+ authMissing.push('Add Organization schema with publisher credentials and sameAs properties');
178
+ }
179
+
180
+ authScore = Math.min(5.0, Math.max(0.0, Math.round(authScore * 10) / 10));
181
+
182
+ // -------------------------------------------------------------
183
+ // 4. TRUSTWORTHINESS (0.0 - 5.0) — The foundational pillar
184
+ // -------------------------------------------------------------
185
+ const trustSignals = [];
186
+ const trustMissing = [];
187
+ let trustScore = 1.0;
188
+
189
+ // Editorial freshness / visible timestamps
190
+ const DATE_RX = /\b(?:published|updated|last modified|reviewed|date):\s*([a-zA-Z]+\s+\d{1,2},\s*\d{4}|\d{4}-\d{2}-\d{2})\b/i;
191
+ const hasVisibleDate = DATE_RX.test(text);
192
+ let hasSchemaDate = false;
193
+ for (const j of (page.jsonLd || [])) {
194
+ for (const b of (j.blocks || [])) {
195
+ if (b.datePublished || b.dateModified) hasSchemaDate = true;
196
+ }
197
+ }
198
+ if (hasVisibleDate || hasSchemaDate) {
199
+ trustScore += 1.0;
200
+ trustSignals.push('Transparent publication and modification dates disclosed');
201
+ } else {
202
+ trustMissing.push('Disclose explicit publication and last-updated dates');
203
+ }
204
+
205
+ // Contact / About / Support transparency
206
+ const hasContact = links.some((l) => /contact|about|support|team|company/i.test(l.href) || /contact|about|support/i.test(l.text));
207
+ if (hasContact) {
208
+ trustScore += 1.0;
209
+ trustSignals.push('Direct contact, about, or support channels accessible');
210
+ } else {
211
+ trustMissing.push('Provide clear navigation links to About Us, Contact, and Support pages');
212
+ }
213
+
214
+ // Legal transparency: Privacy policy & Terms
215
+ const hasPrivacy = links.some((l) => /privacy/i.test(l.href) || /privacy/i.test(l.text));
216
+ const hasTerms = links.some((l) => /terms/i.test(l.href) || /terms/i.test(l.text));
217
+ if (hasPrivacy && hasTerms) {
218
+ trustScore += 1.0;
219
+ trustSignals.push('Privacy policy and terms of service disclosed');
220
+ } else {
221
+ trustMissing.push('Provide clear links to Privacy Policy and Terms of Service');
222
+ }
223
+
224
+ // Security & protocol
225
+ if (page.url && page.url.startsWith('https://')) {
226
+ trustScore += 1.0;
227
+ trustSignals.push('Secure HTTPS transport confirmed');
228
+ } else if (!page.url.startsWith('http://')) {
229
+ trustScore += 0.5; // relative / local test
230
+ }
231
+
232
+ // Commercial / sponsorship transparency
233
+ const sponsoredLinks = links.filter((l) => (l.rel || '').includes('sponsored') || (l.rel || '').includes('nofollow'));
234
+ if (sponsoredLinks.length > 0) {
235
+ trustScore += 0.5;
236
+ trustSignals.push('Commercial and affiliate links properly attributed with rel="sponsored"');
237
+ }
238
+
239
+ trustScore = Math.min(5.0, Math.max(0.0, Math.round(trustScore * 10) / 10));
240
+
241
+ // -------------------------------------------------------------
242
+ // COMPOSITE E-E-A-T SCORE (0.0 - 5.0)
243
+ // In Google Quality Rater Guidelines, Trust is the foundational base:
244
+ // untrusted content cannot achieve a high overall rating.
245
+ // -------------------------------------------------------------
246
+ let composite = (trustScore * 0.40) + (expertScore * 0.25) + (expScore * 0.20) + (authScore * 0.15);
247
+ if (trustScore < 2.0) {
248
+ composite = Math.min(composite, trustScore); // Trust cap
249
+ }
250
+ composite = Math.round(composite * 10) / 10;
251
+
252
+ // Grade rating
253
+ let rating = 'medium';
254
+ if (composite >= 4.0) rating = 'very_high';
255
+ else if (composite >= 3.0) rating = 'high';
256
+ else if (composite >= 2.0) rating = 'medium';
257
+ else rating = 'low';
258
+
259
+ return {
260
+ url: page.url,
261
+ fact_status: 'modeled_rubric_evaluation',
262
+ methodology: 'Google Search Quality Rater Guidelines (E-E-A-T Evaluative Rubric)',
263
+ disclaimer: 'E-E-A-T is an evaluative heuristic rubric modeled on Quality Rater Guidelines, not an algorithmic ranking score. Google does not calculate a single E-E-A-T metric.',
264
+ composite_score: composite,
265
+ overall_rating: rating,
266
+ dimensions: {
267
+ experience: {
268
+ score: expScore,
269
+ scale: '0.0 - 5.0',
270
+ signals: expSignals,
271
+ gaps: expMissing,
272
+ },
273
+ expertise: {
274
+ score: expertScore,
275
+ scale: '0.0 - 5.0',
276
+ author: authorName,
277
+ signals: expertSignals,
278
+ gaps: expertMissing,
279
+ },
280
+ authoritativeness: {
281
+ score: authScore,
282
+ scale: '0.0 - 5.0',
283
+ signals: authSignals,
284
+ gaps: authMissing,
285
+ },
286
+ trustworthiness: {
287
+ score: trustScore,
288
+ scale: '0.0 - 5.0',
289
+ signals: trustSignals,
290
+ gaps: trustMissing,
291
+ },
292
+ },
293
+ actionable_improvements: [
294
+ ...expMissing.map((g) => ({ dimension: 'experience', recommendation: g })),
295
+ ...expertMissing.map((g) => ({ dimension: 'expertise', recommendation: g })),
296
+ ...authMissing.map((g) => ({ dimension: 'authoritativeness', recommendation: g })),
297
+ ...trustMissing.map((g) => ({ dimension: 'trustworthiness', recommendation: g })),
298
+ ],
299
+ };
300
+ }
@@ -0,0 +1,171 @@
1
+ import { safePath } from '../detectors/framework.js';
2
+
3
+ /**
4
+ * Perform End-to-End Funnel and Conversion-Path Analysis across site pages.
5
+ */
6
+ export function analyzeConversionFunnel(sitePages = [], declaredFunnel = null) {
7
+ let steps = [];
8
+
9
+ if (declaredFunnel?.steps?.length) {
10
+ steps = declaredFunnel.steps;
11
+ } else {
12
+ // Auto-discover standard conversion path from site pages
13
+ const roleOrder = ['landing', 'product', 'pricing', 'checkout', 'confirmation'];
14
+ const discovered = [];
15
+
16
+ for (const role of roleOrder) {
17
+ const p = sitePages.find((page) => {
18
+ const u = (page.url || '').toLowerCase();
19
+ if (role === 'landing' && (u.endsWith('/') || u.includes('home') || u.includes('landing'))) return true;
20
+ if (role === 'product' && (u.includes('product') || u.includes('feature') || u.includes('solution'))) return true;
21
+ if (role === 'pricing' && (u.includes('pricing') || u.includes('plan'))) return true;
22
+ if (role === 'checkout' && (u.includes('checkout') || u.includes('signup') || u.includes('demo') || u.includes('contact'))) return true;
23
+ if (role === 'confirmation' && (u.includes('thank-you') || u.includes('success') || u.includes('confirmed'))) return true;
24
+ return false;
25
+ });
26
+ if (p) {
27
+ discovered.push({
28
+ step_id: `STEP-${role.toUpperCase()}`,
29
+ name: `${role[0].toUpperCase()}${role.slice(1)} Page`,
30
+ url_pattern: p.url,
31
+ role,
32
+ });
33
+ }
34
+ }
35
+ steps = discovered.length >= 2 ? discovered : [];
36
+ }
37
+
38
+ if (steps.length === 0) {
39
+ return {
40
+ funnel_id: declaredFunnel?.funnel_id || 'AUTO-DISCOVERED',
41
+ funnel_name: declaredFunnel?.name || 'Conversion Funnel',
42
+ status: 'insufficient_steps',
43
+ message: 'Could not establish conversion funnel: declare steps in .citable/funnels.yaml or provide multi-step pages.',
44
+ steps: [],
45
+ funnel_health_score: 0,
46
+ leaks: [],
47
+ };
48
+ }
49
+
50
+ const stepEvaluations = [];
51
+ const leaks = [];
52
+ let continuityScore = 100;
53
+
54
+ for (let i = 0; i < steps.length; i++) {
55
+ const step = steps[i];
56
+ const page = sitePages.find((p) => (p.url || '').includes(step.url_pattern) || safePath(p.url) === safePath(step.url_pattern));
57
+
58
+ if (!page) {
59
+ continuityScore -= 25;
60
+ leaks.push({
61
+ step_id: step.step_id,
62
+ severity: 'critical',
63
+ type: 'missing_page',
64
+ description: `Funnel step page matching "${step.url_pattern}" was not found in audited site output`,
65
+ });
66
+ stepEvaluations.push({
67
+ step: i + 1,
68
+ step_id: step.step_id,
69
+ name: step.name,
70
+ url: step.url_pattern,
71
+ role: step.role,
72
+ status_code: null,
73
+ continuity_intact: false,
74
+ drop_off_risk: 'critical',
75
+ });
76
+ continue;
77
+ }
78
+
79
+ const ctas = page.ctas || [];
80
+ const forms = page.forms || [];
81
+ let nextStepLinked = true;
82
+ let nextStepTarget = null;
83
+ let paramPreserved = true;
84
+
85
+ if (i < steps.length - 1) {
86
+ const nextStep = steps[i + 1];
87
+ const linkToNext = ctas.find((c) => c.target && c.target.includes(nextStep.url_pattern));
88
+ if (!linkToNext) {
89
+ nextStepLinked = false;
90
+ continuityScore -= 20;
91
+ leaks.push({
92
+ step_id: step.step_id,
93
+ severity: 'high',
94
+ type: 'missing_next_step_cta',
95
+ description: `Step ${i + 1} (${step.name}) has no interactive CTA linking directly to step ${i + 2} (${nextStep.name})`,
96
+ });
97
+ } else {
98
+ nextStepTarget = linkToNext.target;
99
+ // Check attribution parameter preservation
100
+ if (linkToNext.target.includes('?') && !linkToNext.target.includes('utm_')) {
101
+ paramPreserved = false;
102
+ continuityScore -= 10;
103
+ leaks.push({
104
+ step_id: step.step_id,
105
+ severity: 'medium',
106
+ type: 'attribution_parameter_loss',
107
+ description: `CTA link to "${nextStepTarget}" strips campaign attribution query parameters`,
108
+ });
109
+ }
110
+ }
111
+ }
112
+
113
+ // Post-conversion confirmation page indexing check
114
+ if (step.role === 'confirmation' || i === steps.length - 1) {
115
+ const robots = Array.from(page.robotsDirectives || []);
116
+ const hasNoindex = robots.some((r) => r.includes('noindex'));
117
+ if (!hasNoindex && (step.role === 'confirmation' || page.url.includes('thank-you') || page.url.includes('success'))) {
118
+ continuityScore -= 10;
119
+ leaks.push({
120
+ step_id: step.step_id,
121
+ severity: 'medium',
122
+ type: 'unprotected_conversion_page',
123
+ description: 'Post-conversion thank-you page lacks a noindex directive, risking conversion goal contamination in analytics',
124
+ });
125
+ }
126
+ }
127
+
128
+ // Distraction leaks on checkout / high-intent steps
129
+ if (['checkout', 'pricing', 'signup'].includes(step.role) && (page.navLinksCount || 0) > 12) {
130
+ continuityScore -= 15;
131
+ leaks.push({
132
+ step_id: step.step_id,
133
+ severity: 'medium',
134
+ type: 'enclosed_checkout_leak',
135
+ description: `Conversion step "${step.name}" contains ${page.navLinksCount} header/footer links, leaking visitors out of the funnel`,
136
+ });
137
+ }
138
+
139
+ // Drop-off risk assessment
140
+ let dropOffRisk = 'low';
141
+ if (!nextStepLinked || page.status >= 400) dropOffRisk = 'critical';
142
+ else if (forms.some((f) => f.fieldCount > 6)) dropOffRisk = 'high';
143
+ else if ((page.navLinksCount || 0) > 12) dropOffRisk = 'moderate';
144
+
145
+ stepEvaluations.push({
146
+ step: i + 1,
147
+ step_id: step.step_id,
148
+ name: step.name,
149
+ url: page.url,
150
+ role: step.role,
151
+ status_code: page.status,
152
+ continuity_intact: nextStepLinked && page.status === 200,
153
+ next_step_target: nextStepTarget,
154
+ drop_off_risk: dropOffRisk,
155
+ form_count: forms.length,
156
+ cta_count: ctas.length,
157
+ });
158
+ }
159
+
160
+ const finalScore = Math.min(100, Math.max(0, continuityScore));
161
+
162
+ return {
163
+ funnel_id: declaredFunnel?.funnel_id || 'DISCOVERED-FUNNEL',
164
+ funnel_name: declaredFunnel?.name || 'Primary Conversion Path',
165
+ funnel_health_score: finalScore,
166
+ status: finalScore >= 80 ? 'healthy' : finalScore >= 50 ? 'needs_optimization' : 'critical_leaks',
167
+ steps: stepEvaluations,
168
+ total_steps: steps.length,
169
+ leaks,
170
+ };
171
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Impact / Effort / Confidence (ICE) scoring matrix for Citable findings and initiatives.
3
+ *
4
+ * All factors are explicitly scored on a transparent 1-10 scale.
5
+ * Formula: ICE Score = (Impact × Confidence) / Effort
6
+ * Quadrants:
7
+ * - Quick Wins: High Impact (>=6), Low Effort (<=4), High Confidence (>=6)
8
+ * - Strategic Bets: High Impact (>=6), High Effort (>=5)
9
+ * - Low-Hanging Fruit: Low Impact (<6), Low Effort (<=4)
10
+ * - Deprioritize: Low Impact (<6), High Effort (>=5) or Low Confidence (<5)
11
+ */
12
+
13
+ const SEVERITY_IMPACT_MAP = {
14
+ critical: 10,
15
+ high: 8,
16
+ medium: 5,
17
+ low: 2,
18
+ informational: 1,
19
+ experimental: 2,
20
+ };
21
+
22
+ const DETECTOR_EFFORT_MAP = {
23
+ TECH: 4,
24
+ CRAWL: 3,
25
+ CWV: 5,
26
+ ARCH: 6,
27
+ PAGE: 3,
28
+ ANS: 4,
29
+ GEO: 5,
30
+ RECO: 6,
31
+ SCHEMA: 3,
32
+ LINK: 4,
33
+ ENTITY: 4,
34
+ CLAIM: 5,
35
+ EVD: 6,
36
+ SEC: 4,
37
+ MOBILE: 3,
38
+ CRO: 4,
39
+ };
40
+
41
+ const CONFIDENCE_MAP = {
42
+ deterministic: 10,
43
+ verified: 9,
44
+ validated: 8,
45
+ proxy: 6,
46
+ modeled: 6,
47
+ anecdote: 4,
48
+ assumption: 3,
49
+ };
50
+
51
+ /**
52
+ * Score an audit finding for the ICE matrix
53
+ */
54
+ export function scoreFinding(finding) {
55
+ const severity = finding.classification?.severity || 'medium';
56
+ const impact = SEVERITY_IMPACT_MAP[severity] ?? 5;
57
+
58
+ const ns = (finding.detector_id || '').split('-')[0];
59
+ let effort = DETECTOR_EFFORT_MAP[ns] ?? 4;
60
+ if (finding.remediation?.review_required) effort += 2;
61
+ effort = Math.min(10, Math.max(1, effort));
62
+
63
+ let confidence = 8;
64
+ if (finding.classification?.deterministic) {
65
+ confidence = 10;
66
+ } else if (finding.fact_status === 'observed_telemetry') {
67
+ confidence = 9;
68
+ } else if (finding.fact_status === 'modeled_rubric_evaluation') {
69
+ confidence = 6;
70
+ }
71
+
72
+ const iceScore = Math.round(((impact * confidence) / effort) * 10) / 10;
73
+
74
+ let quadrant = 'low_hanging_fruit';
75
+ if (impact >= 6 && effort <= 4 && confidence >= 6) {
76
+ quadrant = 'quick_wins';
77
+ } else if (impact >= 6 && effort >= 5) {
78
+ quadrant = 'strategic_bets';
79
+ } else if (impact < 6 && effort <= 4) {
80
+ quadrant = 'low_hanging_fruit';
81
+ } else {
82
+ quadrant = 'deprioritize';
83
+ }
84
+
85
+ return {
86
+ id: finding.detector_id || finding.finding_id,
87
+ finding_id: finding.finding_id,
88
+ detector_id: finding.detector_id,
89
+ title: finding.observation?.summary || finding.name || finding.detector_id,
90
+ subject: finding.subject?.identifier || finding.subject?.url || 'global',
91
+ severity,
92
+ impact,
93
+ effort,
94
+ confidence,
95
+ ice_score: iceScore,
96
+ quadrant,
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Score an initiative from initiatives.yaml for the ICE matrix
102
+ */
103
+ export function scoreInitiative(initiative) {
104
+ const SCALE = { none: 0, low: 2, medium: 5, high: 8, critical: 10, validated: 9, transformative: 10, trivial: 1, very_high: 9, negligible: 1 };
105
+
106
+ const demand = SCALE[initiative.customer_demand] ?? 5;
107
+ const rev = SCALE[initiative.revenue_potential] ?? 5;
108
+ const diff = SCALE[initiative.strategic_differentiation] ?? 5;
109
+ const impact = Math.min(10, Math.max(1, Math.round((demand + rev + diff) / 3)));
110
+
111
+ const engCost = SCALE[initiative.engineering_cost] ?? 5;
112
+ const opCost = SCALE[initiative.operating_cost || 'none'] ?? 0;
113
+ const effort = Math.min(10, Math.max(1, Math.round(engCost + opCost * 0.3)));
114
+
115
+ const confidence = CONFIDENCE_MAP[initiative.evidence_strength] ?? 5;
116
+ const iceScore = Math.round(((impact * confidence) / effort) * 10) / 10;
117
+
118
+ let quadrant = 'low_hanging_fruit';
119
+ if (impact >= 6 && effort <= 4 && confidence >= 6) {
120
+ quadrant = 'quick_wins';
121
+ } else if (impact >= 6 && effort >= 5) {
122
+ quadrant = 'strategic_bets';
123
+ } else if (impact < 6 && effort <= 4) {
124
+ quadrant = 'low_hanging_fruit';
125
+ } else {
126
+ quadrant = 'deprioritize';
127
+ }
128
+
129
+ return {
130
+ id: initiative.initiative_id,
131
+ title: initiative.title,
132
+ owner: initiative.owner || 'unassigned',
133
+ status: initiative.status,
134
+ impact,
135
+ effort,
136
+ confidence,
137
+ ice_score: iceScore,
138
+ quadrant,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Build the full ICE Scoring Matrix from a list of findings or initiatives
144
+ */
145
+ export function buildIceMatrix(items = [], { type = 'findings' } = {}) {
146
+ const scored = items.map((item) => (type === 'initiatives' ? scoreInitiative(item) : scoreFinding(item)));
147
+ scored.sort((a, b) => b.ice_score - a.ice_score || b.impact - a.impact);
148
+
149
+ const quadrants = {
150
+ quick_wins: scored.filter((i) => i.quadrant === 'quick_wins'),
151
+ strategic_bets: scored.filter((i) => i.quadrant === 'strategic_bets'),
152
+ low_hanging_fruit: scored.filter((i) => i.quadrant === 'low_hanging_fruit'),
153
+ deprioritize: scored.filter((i) => i.quadrant === 'deprioritize'),
154
+ };
155
+
156
+ return {
157
+ fact_status: 'transparent_prioritization_matrix',
158
+ scoring_method: 'ICE: (Impact [1-10] × Confidence [1-10]) / Effort [1-10]',
159
+ total_items: scored.length,
160
+ summary: {
161
+ quick_wins_count: quadrants.quick_wins.length,
162
+ strategic_bets_count: quadrants.strategic_bets.length,
163
+ low_hanging_fruit_count: quadrants.low_hanging_fruit.length,
164
+ deprioritize_count: quadrants.deprioritize.length,
165
+ },
166
+ quadrants,
167
+ ranked_items: scored,
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Format ICE Matrix as human-readable terminal table / markdown
173
+ */
174
+ export function formatIceMatrixOutput(matrix) {
175
+ const lines = [
176
+ `Impact / Effort / Confidence (ICE) Prioritization Matrix`,
177
+ `=======================================================`,
178
+ `Methodology: ${matrix.scoring_method}`,
179
+ `Total Items Scored: ${matrix.total_items}`,
180
+ ``,
181
+ `QUADRANT SUMMARY:`,
182
+ ` Quadrant I [Quick Wins]: ${matrix.summary.quick_wins_count} item(s) (High Impact, Low Effort, High Confidence)`,
183
+ ` Quadrant II [Strategic Bets]: ${matrix.summary.strategic_bets_count} item(s) (High Impact, High Effort)`,
184
+ ` Quadrant III [Low-Hanging Fruit]: ${matrix.summary.low_hanging_fruit_count} item(s) (Low Impact, Low Effort)`,
185
+ ` Quadrant IV [Deprioritize]: ${matrix.summary.deprioritize_count} item(s) (Low Impact or Low Confidence)`,
186
+ ``,
187
+ `RANKED ACTION MATRIX:`,
188
+ ` ${'Rank'.padEnd(6)} | ${'ID'.padEnd(16)} | ${'Impact'.padEnd(8)} | ${'Effort'.padEnd(8)} | ${'Conf'.padEnd(6)} | ${'ICE Score'.padEnd(10)} | Quadrant`,
189
+ ` ${'-'.repeat(80)}`,
190
+ ];
191
+
192
+ matrix.ranked_items.forEach((item, idx) => {
193
+ const rank = `#${idx + 1}`.padEnd(6);
194
+ const id = (item.id || '').slice(0, 16).padEnd(16);
195
+ const impact = `${item.impact}/10`.padEnd(8);
196
+ const effort = `${item.effort}/10`.padEnd(8);
197
+ const conf = `${item.confidence}/10`.padEnd(6);
198
+ const score = `${item.ice_score}`.padEnd(10);
199
+ const q = item.quadrant.toUpperCase();
200
+ lines.push(` ${rank} | ${id} | ${impact} | ${effort} | ${conf} | ${score} | ${q}`);
201
+ lines.push(` ${item.title.slice(0, 70)}`);
202
+ });
203
+
204
+ return lines.join('\n');
205
+ }