@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.
- package/CHANGELOG.md +22 -0
- package/dist/universal/.agents/skills/citable/SKILL.md +20 -1
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/manifest.json +6 -5
- package/dist/universal/.agents/skills/citable/schemas/sow.schema.json +92 -0
- package/dist/universal/.claude/agents/citable/manifest.json +2 -2
- package/dist/universal/.claude/skills/citable/SKILL.md +20 -1
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/manifest.json +6 -5
- package/dist/universal/.claude/skills/citable/schemas/sow.schema.json +92 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +20 -1
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/manifest.json +6 -5
- package/dist/universal/.cursor/skills/citable/schemas/sow.schema.json +92 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +20 -1
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/manifest.json +6 -5
- package/dist/universal/.gemini/skills/citable/schemas/sow.schema.json +92 -0
- package/dist/universal/.github/skills/citable/SKILL.md +20 -1
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/manifest.json +6 -5
- package/dist/universal/.github/skills/citable/schemas/sow.schema.json +92 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +20 -1
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/manifest.json +6 -5
- package/dist/universal/.kiro/skills/citable/schemas/sow.schema.json +92 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +20 -1
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/manifest.json +6 -5
- package/dist/universal/.opencode/skills/citable/schemas/sow.schema.json +92 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +20 -1
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/manifest.json +6 -5
- package/dist/universal/.pi/agent/skills/citable/schemas/sow.schema.json +92 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +20 -1
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/manifest.json +6 -5
- package/dist/universal/.qoder/skills/citable/schemas/sow.schema.json +92 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +20 -1
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/manifest.json +6 -5
- package/dist/universal/.rovodev/skills/citable/schemas/sow.schema.json +92 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +20 -1
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/manifest.json +6 -5
- package/dist/universal/.trae/skills/citable/schemas/sow.schema.json +92 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +20 -1
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/manifest.json +6 -5
- package/dist/universal/.trae-cn/skills/citable/schemas/sow.schema.json +92 -0
- package/dist/universal/manifest.json +76 -64
- package/package.json +1 -1
- package/schemas/sow.schema.json +92 -0
- package/skill/SKILL.md +20 -1
- package/src/analysis/behavioral.js +126 -0
- package/src/analysis/croAudit.js +302 -0
- package/src/analysis/croRoadmap.js +121 -0
- package/src/analysis/eeat.js +300 -0
- package/src/analysis/funnelAnalysis.js +171 -0
- package/src/analysis/iceMatrix.js +205 -0
- package/src/analysis/offpage.js +228 -0
- package/src/analysis/readiness.js +253 -0
- package/src/analysis/schemaRecommendations.js +173 -0
- package/src/analysis/strategicRoadmap.js +148 -0
- package/src/cli/index.js +149 -3
- package/src/commands/answerEngineReadiness.js +48 -0
- package/src/commands/auditBacklinks.js +79 -0
- package/src/commands/croBacklog.js +182 -0
- package/src/commands/croSuite.js +156 -0
- package/src/commands/executive.js +6 -0
- package/src/commands/executiveCro.js +30 -0
- package/src/commands/executiveSearch.js +28 -0
- package/src/commands/inspectCro.js +7 -0
- package/src/commands/inspectEeat.js +53 -0
- package/src/commands/prioritize.js +42 -6
- package/src/commands/roadmapCmd.js +69 -0
- package/src/commands/schemaCmd.js +10 -2
- package/src/commands/sowCmd.js +67 -0
- package/src/commands/sweep.js +357 -0
- package/src/reporting/executiveCroReport.js +731 -0
- package/src/reporting/executiveExport.js +62 -0
- package/src/reporting/executiveSearchReport.js +704 -0
- package/src/sow/admissibilityGate.js +194 -0
- 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
|
+
}
|