@nebulacomponents/citable 1.17.0 → 1.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/dist/universal/.agents/skills/citable/SKILL.md +41 -1
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.agents/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.agents/skills/citable/commands/README.md +13 -0
- package/dist/universal/.agents/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.agents/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.agents/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.agents/skills/citable/manifest.json +12 -7
- package/dist/universal/.agents/skills/citable/schemas/sow.schema.json +409 -0
- package/dist/universal/.claude/agents/citable/citable-remediator.md +43 -0
- package/dist/universal/.claude/agents/citable/citable-sow-architect.md +43 -0
- package/dist/universal/.claude/agents/citable/manifest.json +6 -4
- package/dist/universal/.claude/skills/citable/SKILL.md +41 -1
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.claude/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.claude/skills/citable/commands/README.md +13 -0
- package/dist/universal/.claude/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.claude/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.claude/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.claude/skills/citable/manifest.json +16 -9
- package/dist/universal/.claude/skills/citable/schemas/sow.schema.json +409 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +41 -1
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.cursor/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.cursor/skills/citable/commands/README.md +13 -0
- package/dist/universal/.cursor/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.cursor/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.cursor/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.cursor/skills/citable/manifest.json +12 -7
- package/dist/universal/.cursor/skills/citable/schemas/sow.schema.json +409 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +41 -1
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.gemini/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.gemini/skills/citable/commands/README.md +13 -0
- package/dist/universal/.gemini/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.gemini/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.gemini/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.gemini/skills/citable/manifest.json +12 -7
- package/dist/universal/.gemini/skills/citable/schemas/sow.schema.json +409 -0
- package/dist/universal/.github/skills/citable/SKILL.md +41 -1
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.github/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.github/skills/citable/commands/README.md +13 -0
- package/dist/universal/.github/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.github/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.github/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.github/skills/citable/manifest.json +12 -7
- package/dist/universal/.github/skills/citable/schemas/sow.schema.json +409 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +41 -1
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.kiro/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.kiro/skills/citable/commands/README.md +13 -0
- package/dist/universal/.kiro/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.kiro/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.kiro/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.kiro/skills/citable/manifest.json +12 -7
- package/dist/universal/.kiro/skills/citable/schemas/sow.schema.json +409 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +41 -1
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.opencode/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.opencode/skills/citable/commands/README.md +13 -0
- package/dist/universal/.opencode/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.opencode/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.opencode/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.opencode/skills/citable/manifest.json +12 -7
- package/dist/universal/.opencode/skills/citable/schemas/sow.schema.json +409 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +41 -1
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.pi/agent/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.pi/agent/skills/citable/commands/README.md +13 -0
- package/dist/universal/.pi/agent/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.pi/agent/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.pi/agent/skills/citable/manifest.json +12 -7
- package/dist/universal/.pi/agent/skills/citable/schemas/sow.schema.json +409 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +41 -1
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.qoder/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.qoder/skills/citable/commands/README.md +13 -0
- package/dist/universal/.qoder/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.qoder/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.qoder/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.qoder/skills/citable/manifest.json +12 -7
- package/dist/universal/.qoder/skills/citable/schemas/sow.schema.json +409 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +41 -1
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.rovodev/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.rovodev/skills/citable/commands/README.md +13 -0
- package/dist/universal/.rovodev/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.rovodev/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.rovodev/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.rovodev/skills/citable/manifest.json +12 -7
- package/dist/universal/.rovodev/skills/citable/schemas/sow.schema.json +409 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +41 -1
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.trae/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.trae/skills/citable/commands/README.md +13 -0
- package/dist/universal/.trae/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.trae/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.trae/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.trae/skills/citable/manifest.json +12 -7
- package/dist/universal/.trae/skills/citable/schemas/sow.schema.json +409 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +41 -1
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/agents/citable-remediator.md +43 -0
- package/dist/universal/.trae-cn/skills/citable/agents/citable-sow-architect.md +43 -0
- package/dist/universal/.trae-cn/skills/citable/commands/README.md +13 -0
- package/dist/universal/.trae-cn/skills/citable/commands/remediation.md +55 -0
- package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +28 -0
- package/dist/universal/.trae-cn/skills/citable/commands/sow.md +95 -0
- package/dist/universal/.trae-cn/skills/citable/manifest.json +12 -7
- package/dist/universal/.trae-cn/skills/citable/schemas/sow.schema.json +409 -0
- package/dist/universal/manifest.json +150 -88
- package/package.json +1 -1
- package/schemas/sow.schema.json +409 -0
- package/skill/SKILL.md +41 -1
- package/skill/agents/citable-remediator.md +43 -0
- package/skill/agents/citable-sow-architect.md +43 -0
- package/skill/commands/README.md +13 -0
- package/skill/commands/remediation.md +55 -0
- package/skill/commands/reporting.md +28 -0
- package/skill/commands/sow.md +95 -0
- 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 +84 -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 +364 -0
- package/src/sow/generateSow.js +1178 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readJson } from '../shared/io.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate a prioritized A/B experiment backlog from CRO audit findings.
|
|
7
|
+
*/
|
|
8
|
+
export function generateExperimentBacklog(findings = [], { dailyVisitors = 500, baselineRate = 0.02, defaultMde = 0.12 } = {}) {
|
|
9
|
+
const croFindings = findings.filter((f) => (f.detector_id || '').startsWith('CRO-') || f.discipline?.includes('cro'));
|
|
10
|
+
|
|
11
|
+
const backlog = [];
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < croFindings.length; i++) {
|
|
14
|
+
const f = croFindings[i];
|
|
15
|
+
const ns = f.detector_id;
|
|
16
|
+
const pageUrl = f.subject?.identifier || f.subject?.url || '/';
|
|
17
|
+
|
|
18
|
+
let title = '';
|
|
19
|
+
let change = '';
|
|
20
|
+
let rationale = f.observation?.summary || 'Friction detected on conversion pathway';
|
|
21
|
+
let expectedMde = defaultMde;
|
|
22
|
+
let primaryMetric = 'conversion_rate';
|
|
23
|
+
let secondaryMetrics = ['bounce_rate', 'form_completion_rate'];
|
|
24
|
+
|
|
25
|
+
switch (ns) {
|
|
26
|
+
case 'CRO-001':
|
|
27
|
+
title = 'Prominent Interactive Hero CTA Deployment';
|
|
28
|
+
change = 'Deploy prominent, high-contrast primary CTA button matching declared conversion intent';
|
|
29
|
+
expectedMde = 0.15;
|
|
30
|
+
break;
|
|
31
|
+
case 'CRO-002':
|
|
32
|
+
title = 'Form Friction Reduction & Progressive Profiling';
|
|
33
|
+
change = 'Condense lead capture form from >6 fields to 3 essential fields with progressive disclosure';
|
|
34
|
+
expectedMde = 0.20;
|
|
35
|
+
primaryMetric = 'lead_submission_rate';
|
|
36
|
+
break;
|
|
37
|
+
case 'CRO-005':
|
|
38
|
+
title = 'Ad-to-Landing Scent Match Alignment';
|
|
39
|
+
change = 'Align above-the-fold H1 headline and value proposition directly with title and ad intent';
|
|
40
|
+
expectedMde = 0.12;
|
|
41
|
+
break;
|
|
42
|
+
case 'CRO-006':
|
|
43
|
+
title = 'Proximate Trust Proof & Security Badges';
|
|
44
|
+
change = 'Position verified customer logo bar and security certification badges immediately below primary CTA';
|
|
45
|
+
expectedMde = 0.10;
|
|
46
|
+
break;
|
|
47
|
+
case 'CRO-007':
|
|
48
|
+
title = 'HTML5 Autocomplete Form Optimization';
|
|
49
|
+
change = 'Inject standard HTML5 autocomplete attributes on all identity, email, and telephone fields';
|
|
50
|
+
expectedMde = 0.08;
|
|
51
|
+
primaryMetric = 'form_completion_speed';
|
|
52
|
+
break;
|
|
53
|
+
case 'CRO-010':
|
|
54
|
+
title = 'Hero Choice Overload Elimination';
|
|
55
|
+
change = 'Consolidate competing hero buttons into exactly ONE dominant primary action and one text link';
|
|
56
|
+
expectedMde = 0.14;
|
|
57
|
+
break;
|
|
58
|
+
case 'CRO-012':
|
|
59
|
+
title = 'High-Intent Value CTA Microcopy';
|
|
60
|
+
change = 'Replace generic "Submit" or "Learn More" with action-oriented value copy ("Get My Free Analysis")';
|
|
61
|
+
expectedMde = 0.10;
|
|
62
|
+
break;
|
|
63
|
+
case 'CRO-013':
|
|
64
|
+
title = 'Enclosed Distraction-Free Checkout Funnel';
|
|
65
|
+
change = 'Remove global header navigation and external outbound links on checkout/pricing steps';
|
|
66
|
+
expectedMde = 0.15;
|
|
67
|
+
break;
|
|
68
|
+
case 'CRO-021':
|
|
69
|
+
title = 'Express Payment Wallet Integration';
|
|
70
|
+
change = 'Implement 1-click Apple Pay, Google Pay, and PayPal express wallet buttons';
|
|
71
|
+
expectedMde = 0.18;
|
|
72
|
+
primaryMetric = 'checkout_conversion_rate';
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
title = `Remediation for ${f.name || ns}`;
|
|
76
|
+
change = f.remediation?.preferred || 'Implement recommended conversion design system component';
|
|
77
|
+
expectedMde = defaultMde;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Falsifiable hypothesis formulation
|
|
81
|
+
const hypothesis = `If we ${change} on ${pageUrl}, then ${primaryMetric} will increase by at least ${Math.round(expectedMde * 100)}%, because ${rationale.toLowerCase()}.`;
|
|
82
|
+
|
|
83
|
+
// Sample size calculation (two-tailed alpha=0.05, power=0.80)
|
|
84
|
+
const p1 = baselineRate;
|
|
85
|
+
const p2 = p1 * (1 + expectedMde);
|
|
86
|
+
const pBar = (p1 + p2) / 2;
|
|
87
|
+
const zAlpha = 1.96;
|
|
88
|
+
const zBeta = 0.8416;
|
|
89
|
+
const num = Math.pow(zAlpha * Math.sqrt(2 * pBar * (1 - pBar)) + zBeta * Math.sqrt(p1 * (1 - p1) + p2 * (1 - p2)), 2);
|
|
90
|
+
const den = Math.pow(p2 - p1, 2);
|
|
91
|
+
const nPerVariant = Math.ceil(num / den);
|
|
92
|
+
const totalSample = nPerVariant * 2;
|
|
93
|
+
const estDays = Math.max(7, Math.ceil(totalSample / dailyVisitors));
|
|
94
|
+
|
|
95
|
+
// Impact, Effort, Confidence (ICE)
|
|
96
|
+
const impact = Math.min(10, Math.max(4, Math.round(expectedMde * 50)));
|
|
97
|
+
const effort = ['CRO-007', 'CRO-012', 'CRO-005'].includes(ns) ? 2 : ['CRO-001', 'CRO-006', 'CRO-010'].includes(ns) ? 4 : 6;
|
|
98
|
+
const confidence = f.classification?.deterministic ? 9 : 7;
|
|
99
|
+
const iceScore = Math.round(((impact * confidence) / effort) * 10) / 10;
|
|
100
|
+
|
|
101
|
+
backlog.push({
|
|
102
|
+
experiment_id: `EXP-CRO-${String(i + 1).padStart(3, '0')}`,
|
|
103
|
+
title,
|
|
104
|
+
detector_id: ns,
|
|
105
|
+
target_page: pageUrl,
|
|
106
|
+
hypothesis,
|
|
107
|
+
discipline: ['cro'],
|
|
108
|
+
primary_metric: primaryMetric,
|
|
109
|
+
secondary_metrics: secondaryMetrics,
|
|
110
|
+
statistical_setup: {
|
|
111
|
+
baseline_rate_pct: Math.round(baselineRate * 1000) / 10,
|
|
112
|
+
expected_mde_pct: Math.round(expectedMde * 100),
|
|
113
|
+
confidence_level_pct: 95,
|
|
114
|
+
power_pct: 80,
|
|
115
|
+
sample_size_per_variant: nPerVariant,
|
|
116
|
+
total_sample_required: totalSample,
|
|
117
|
+
estimated_duration_days: estDays,
|
|
118
|
+
daily_visitors: dailyVisitors,
|
|
119
|
+
},
|
|
120
|
+
guardrails: [
|
|
121
|
+
'SEO Retention: Variant must preserve all canonical tags, JSON-LD structured data, and meta robots tags',
|
|
122
|
+
'Performance Guardrail: Mobile LCP must not degrade by > 250ms; CLS must remain <= 0.10',
|
|
123
|
+
'Secondary Safety Metric: Bounce rate must not increase by > 5% relative to control',
|
|
124
|
+
],
|
|
125
|
+
stopping_criteria: [
|
|
126
|
+
'Minimum Duration Lock: Run for at least 7 full days to account for day-of-week seasonality, regardless of early p-values',
|
|
127
|
+
'Sample Ratio Mismatch (SRM): Terminate immediately if traffic allocation deviates significantly (chi-square p < 0.001)',
|
|
128
|
+
'Maximum Duration Cap: Stop and conclude as inconclusive if required sample size is not reached within 45 days',
|
|
129
|
+
],
|
|
130
|
+
ice_prioritization: {
|
|
131
|
+
impact,
|
|
132
|
+
effort,
|
|
133
|
+
confidence,
|
|
134
|
+
ice_score: iceScore,
|
|
135
|
+
quadrant: impact >= 6 && effort <= 4 ? 'quick_wins' : impact >= 6 ? 'strategic_bets' : 'low_hanging_fruit',
|
|
136
|
+
},
|
|
137
|
+
status: 'draft',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Sort by ICE score descending
|
|
142
|
+
backlog.sort((a, b) => b.ice_prioritization.ice_score - a.ice_prioritization.ice_score);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
fact_status: 'experiment_backlog_projection',
|
|
146
|
+
total_experiments: backlog.length,
|
|
147
|
+
summary: {
|
|
148
|
+
quick_wins: backlog.filter((e) => e.ice_prioritization.quadrant === 'quick_wins').length,
|
|
149
|
+
strategic_bets: backlog.filter((e) => e.ice_prioritization.quadrant === 'strategic_bets').length,
|
|
150
|
+
low_hanging_fruit: backlog.filter((e) => e.ice_prioritization.quadrant === 'low_hanging_fruit').length,
|
|
151
|
+
},
|
|
152
|
+
experiments: backlog,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Format Experiment Backlog as Markdown
|
|
158
|
+
*/
|
|
159
|
+
export function formatBacklogMarkdown(backlog) {
|
|
160
|
+
const lines = [
|
|
161
|
+
`# Conversion Experimentation Backlog & Hypothesis Register`,
|
|
162
|
+
`=========================================================`,
|
|
163
|
+
`Total Hypotheses: ${backlog.total_experiments} | Quick Wins: ${backlog.summary.quick_wins} | Strategic Bets: ${backlog.summary.strategic_bets}`,
|
|
164
|
+
``,
|
|
165
|
+
`Methodology: Every experiment requires a falsifiable hypothesis, statistical power calculations, SEO guardrails, and stopping criteria.`,
|
|
166
|
+
``,
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
for (const exp of backlog.experiments) {
|
|
170
|
+
lines.push(`## [${exp.ice_prioritization.quadrant.toUpperCase()}] ${exp.experiment_id}: ${exp.title}`);
|
|
171
|
+
lines.push(`- **Target Surface**: \`${exp.target_page}\` (Trigger: \`${exp.detector_id}\`)`);
|
|
172
|
+
lines.push(`- **Falsifiable Hypothesis**: "${exp.hypothesis}"`);
|
|
173
|
+
lines.push(`- **Primary Metric**: \`${exp.primary_metric}\` | **Expected MDE**: +${exp.statistical_setup.expected_mde_pct}%`);
|
|
174
|
+
lines.push(`- **Statistical Blueprint**: ${exp.statistical_setup.sample_size_per_variant.toLocaleString()} visitors/variant (~${exp.statistical_setup.estimated_duration_days} days at ${exp.statistical_setup.daily_visitors} visitors/day, 80% power @ 95% conf)`);
|
|
175
|
+
lines.push(`- **ICE Score**: **${exp.ice_prioritization.ice_score}** (Impact: ${exp.ice_prioritization.impact}/10, Effort: ${exp.ice_prioritization.effort}/10, Conf: ${exp.ice_prioritization.confidence}/10)`);
|
|
176
|
+
lines.push(`- **Guardrails**: ${exp.guardrails.join('; ')}`);
|
|
177
|
+
lines.push(`- **Stopping Criteria**: ${exp.stopping_criteria.join('; ')}`);
|
|
178
|
+
lines.push(``);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return lines.join('\n');
|
|
182
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { buildContext } from './context.js';
|
|
4
|
+
import { selectDetectors } from '../detectors/index.js';
|
|
5
|
+
import { runDetectors, indexTargets, pageSubject, safePath } from '../detectors/framework.js';
|
|
6
|
+
import { auditPageCro } from '../analysis/croAudit.js';
|
|
7
|
+
import { analyzeConversionFunnel } from '../analysis/funnelAnalysis.js';
|
|
8
|
+
import { analyzeBehavioralTelemetry } from '../analysis/behavioral.js';
|
|
9
|
+
import { generateExperimentBacklog, formatBacklogMarkdown } from './croBacklog.js';
|
|
10
|
+
import { buildIceMatrix, formatIceMatrixOutput } from '../analysis/iceMatrix.js';
|
|
11
|
+
import { buildCroRoadmap, formatCroRoadmapMarkdown } from '../analysis/croRoadmap.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Full End-to-End CRO Intelligence Suite Audit
|
|
15
|
+
*/
|
|
16
|
+
export async function auditCroSuite(root, { target, baseUrl, refDate, input, funnelId, write = false } = {}) {
|
|
17
|
+
const ctx = await buildContext(root, { target, baseUrl, refDate });
|
|
18
|
+
if (!ctx.site) throw new Error('cro audit requires a site target (built output directory or URL)');
|
|
19
|
+
|
|
20
|
+
// 1. Run CRO detectors across all pages
|
|
21
|
+
const croDetectors = selectDetectors({ namespaces: ['CRO'] });
|
|
22
|
+
const { findings } = runDetectors(croDetectors, ctx);
|
|
23
|
+
|
|
24
|
+
const pages = indexTargets(ctx);
|
|
25
|
+
|
|
26
|
+
// 2. Above-the-fold clarity, trust, cognitive load, offer architecture per page
|
|
27
|
+
const auditedPages = pages.map((page) => auditPageCro(page, ctx));
|
|
28
|
+
|
|
29
|
+
// Average scores across commercial pages
|
|
30
|
+
const commercialPages = auditedPages.filter((p) => {
|
|
31
|
+
const u = (p.url || '').toLowerCase();
|
|
32
|
+
return u.includes('pricing') || u.includes('product') || u.includes('feature') || u.includes('checkout') || u.endsWith('/') || u.includes('landing');
|
|
33
|
+
});
|
|
34
|
+
const targetGroup = commercialPages.length > 0 ? commercialPages : auditedPages;
|
|
35
|
+
|
|
36
|
+
const avgAtfClarity = targetGroup.length > 0
|
|
37
|
+
? Math.round(targetGroup.reduce((acc, p) => acc + p.atf_clarity.score, 0) / targetGroup.length)
|
|
38
|
+
: 0;
|
|
39
|
+
|
|
40
|
+
const avgTrustScore = targetGroup.length > 0
|
|
41
|
+
? Math.round(targetGroup.reduce((acc, p) => acc + p.trust_and_credibility.score, 0) / targetGroup.length)
|
|
42
|
+
: 0;
|
|
43
|
+
|
|
44
|
+
const avgConversionReadiness = targetGroup.length > 0
|
|
45
|
+
? Math.round(targetGroup.reduce((acc, p) => acc + p.conversion_readiness_score, 0) / targetGroup.length)
|
|
46
|
+
: 0;
|
|
47
|
+
|
|
48
|
+
// 3. End-to-end Funnel and Conversion-Path Analysis
|
|
49
|
+
const declaredFunnel = (ctx.registries?.funnels?.entries || []).find((f) => funnelId ? f.funnel_id === funnelId : true) || null;
|
|
50
|
+
const funnelAnalysis = analyzeConversionFunnel(ctx.site.pages, declaredFunnel);
|
|
51
|
+
|
|
52
|
+
// 4. Behavioral Evidence Analysis (if telemetry file provided)
|
|
53
|
+
let behavioralAnalysis = null;
|
|
54
|
+
if (input) {
|
|
55
|
+
const filePath = path.resolve(root, input);
|
|
56
|
+
if (fs.existsSync(filePath)) {
|
|
57
|
+
try {
|
|
58
|
+
const rawTelemetry = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
59
|
+
behavioralAnalysis = analyzeBehavioralTelemetry(rawTelemetry);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
// Non-blocking parse error
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 5. Experiment Backlog with Falsifiable Hypotheses
|
|
67
|
+
const experimentBacklog = generateExperimentBacklog(findings);
|
|
68
|
+
|
|
69
|
+
// 6. Impact / Effort / Confidence (ICE) Prioritization Matrix
|
|
70
|
+
const iceMatrix = buildIceMatrix(findings, { type: 'findings' });
|
|
71
|
+
|
|
72
|
+
// 7. 30 / 90 / 180-Day CRO Strategic Roadmap tied to Measurable Conversion Outcomes
|
|
73
|
+
const targetDomain = ctx.site.baseUrl ? new URL(ctx.site.baseUrl).hostname : 'target-site';
|
|
74
|
+
const croRoadmap = buildCroRoadmap({
|
|
75
|
+
findings,
|
|
76
|
+
targetDomain,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const result = {
|
|
80
|
+
fact_status: 'modeled_cro_evaluation',
|
|
81
|
+
target: ctx.site.baseUrl || target,
|
|
82
|
+
total_pages_audited: pages.length,
|
|
83
|
+
summary: {
|
|
84
|
+
conversion_readiness_score: avgConversionReadiness,
|
|
85
|
+
atf_clarity_score: avgAtfClarity,
|
|
86
|
+
trust_credibility_score: avgTrustScore,
|
|
87
|
+
funnel_health_score: funnelAnalysis.funnel_health_score,
|
|
88
|
+
total_cro_findings: findings.length,
|
|
89
|
+
planned_experiments: experimentBacklog.total_experiments,
|
|
90
|
+
quick_wins_count: iceMatrix.summary.quick_wins_count,
|
|
91
|
+
},
|
|
92
|
+
funnel_analysis: funnelAnalysis,
|
|
93
|
+
pages: auditedPages,
|
|
94
|
+
behavioral_telemetry: behavioralAnalysis,
|
|
95
|
+
experiment_backlog: experimentBacklog,
|
|
96
|
+
ice_matrix: iceMatrix,
|
|
97
|
+
strategic_roadmap: croRoadmap,
|
|
98
|
+
findings: findings.map((f) => ({
|
|
99
|
+
detector_id: f.detector_id,
|
|
100
|
+
severity: f.classification.severity,
|
|
101
|
+
subject: f.subject.identifier || f.subject.url,
|
|
102
|
+
summary: f.observation.summary,
|
|
103
|
+
remediation: f.remediation?.preferred,
|
|
104
|
+
})),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Format Human-Readable CRO Suite Terminal Report
|
|
112
|
+
*/
|
|
113
|
+
export function formatCroSuiteOutput(r) {
|
|
114
|
+
const lines = [
|
|
115
|
+
`Conversion Rate Optimization (CRO) & Journey Intelligence Suite`,
|
|
116
|
+
`==============================================================`,
|
|
117
|
+
`Target: ${r.target}`,
|
|
118
|
+
`Audited Pages: ${r.total_pages_audited} | Conversion Readiness Score: ${r.summary.conversion_readiness_score}/100`,
|
|
119
|
+
``,
|
|
120
|
+
`CORE PILLAR SCORES:`,
|
|
121
|
+
` Above-The-Fold Clarity & Message Match: ${r.summary.atf_clarity_score} / 100`,
|
|
122
|
+
` Trust, Credibility & Risk-Reversal: ${r.summary.trust_credibility_score} / 100`,
|
|
123
|
+
` End-to-End Funnel Health: ${r.summary.funnel_health_score} / 100 (${r.funnel_analysis.status.toUpperCase()})`,
|
|
124
|
+
` Active CRO Findings: ${r.summary.total_cro_findings} defect(s)`,
|
|
125
|
+
``,
|
|
126
|
+
`END-TO-END CONVERSION PATH ANALYSIS (${r.funnel_analysis.funnel_name}):`,
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
for (const s of r.funnel_analysis.steps) {
|
|
130
|
+
const status = s.continuity_intact ? 'INTACT' : 'BROKEN';
|
|
131
|
+
lines.push(` Step ${s.step}: [${s.role.toUpperCase()}] ${s.name} (${s.url}) → ${status} (Drop-off risk: ${s.drop_off_risk.toUpperCase()})`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (r.funnel_analysis.leaks.length) {
|
|
135
|
+
lines.push(``, `Identified Conversion Leaks:`);
|
|
136
|
+
for (const leak of r.funnel_analysis.leaks) {
|
|
137
|
+
lines.push(` - [${leak.severity.toUpperCase()}] (${leak.type}) ${leak.description}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
lines.push(``, `EXPERIMENTATION BACKLOG (${r.experiment_backlog.total_experiments} falsifiable hypotheses generated):`);
|
|
142
|
+
for (const exp of r.experiment_backlog.experiments.slice(0, 5)) {
|
|
143
|
+
lines.push(` [${exp.ice_prioritization.quadrant.toUpperCase()}] ${exp.experiment_id}: ${exp.title} (Expected MDE: +${exp.statistical_setup.expected_mde_pct}%, ICE: ${exp.ice_prioritization.ice_score})`);
|
|
144
|
+
lines.push(` Hypothesis: "${exp.hypothesis}"`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
lines.push(``, `30 / 90 / 180-DAY CRO STRATEGIC ROADMAP:`);
|
|
148
|
+
for (const h of r.strategic_roadmap.horizons) {
|
|
149
|
+
lines.push(` ${h.label}:`);
|
|
150
|
+
lines.push(` Goal: ${h.objective}`);
|
|
151
|
+
lines.push(` Target KPI: ${h.measurable_conversion_kpi}`);
|
|
152
|
+
lines.push(` Sequenced Experiments: ${h.experiments.length} experiment(s)`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return lines.join('\n');
|
|
156
|
+
}
|
|
@@ -45,6 +45,8 @@ const ROUTES = [
|
|
|
45
45
|
{ patterns: [/variance|miss|why.did.we/i], command: 'variance', reason: 'Variance analysis' },
|
|
46
46
|
{ patterns: [/prioriti|roadmap|what.to.build|rank/i], command: 'prioritize', reason: 'Initiative prioritization' },
|
|
47
47
|
{ patterns: [/competitor|competitive|market.change/i], command: 'competitive-intel', reason: 'Competitive intelligence' },
|
|
48
|
+
{ patterns: [/search|seo|aeo|geo|serp/i], command: 'executive-search', reason: 'Enterprise Search & AEO/GEO Executive Report' },
|
|
49
|
+
{ patterns: [/cro|conversion|funnel|journey/i], command: 'executive-cro', reason: 'Enterprise CRO & Journey Intelligence Executive Report' },
|
|
48
50
|
];
|
|
49
51
|
|
|
50
52
|
const MAX_DEPTH = 1;
|
|
@@ -119,6 +121,8 @@ function commandToModule(cmd) {
|
|
|
119
121
|
'variance': 'variance',
|
|
120
122
|
'prioritize': 'prioritize',
|
|
121
123
|
'competitive-intel': 'competitiveIntel',
|
|
124
|
+
'executive-search': 'executiveSearch',
|
|
125
|
+
'executive-cro': 'executiveCro',
|
|
122
126
|
};
|
|
123
127
|
return MAP[cmd] ?? cmd;
|
|
124
128
|
}
|
|
@@ -136,6 +140,8 @@ function commandToExport(cmd) {
|
|
|
136
140
|
'variance': 'varianceCommand',
|
|
137
141
|
'prioritize': 'prioritizeCommand',
|
|
138
142
|
'competitive-intel': 'competitiveIntelCommand',
|
|
143
|
+
'executive-search': 'executiveSearchCommand',
|
|
144
|
+
'executive-cro': 'executiveCroCommand',
|
|
139
145
|
};
|
|
140
146
|
return MAP[cmd] ?? cmd + 'Command';
|
|
141
147
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { exportExecutiveCroReport } from '../reporting/executiveCroReport.js';
|
|
2
|
+
|
|
3
|
+
export async function executiveCroCommand(args = [], root = process.cwd()) {
|
|
4
|
+
const target = argVal(args, '--target');
|
|
5
|
+
const baseUrl = argVal(args, '--base-url');
|
|
6
|
+
const runId = argVal(args, '--run');
|
|
7
|
+
const client = argVal(args, '--client');
|
|
8
|
+
const format = args.includes('--json') ? 'json' : argVal(args, '--format') || 'markdown';
|
|
9
|
+
const output = argVal(args, '--output');
|
|
10
|
+
const input = argVal(args, '--input');
|
|
11
|
+
const funnel = argVal(args, '--funnel');
|
|
12
|
+
|
|
13
|
+
const r = await exportExecutiveCroReport(root, {
|
|
14
|
+
target,
|
|
15
|
+
baseUrl,
|
|
16
|
+
runId,
|
|
17
|
+
clientName: client || 'Enterprise Client',
|
|
18
|
+
format,
|
|
19
|
+
output,
|
|
20
|
+
telemetryInput: input,
|
|
21
|
+
funnelId: funnel,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return format === 'json' ? r.data : r.content;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function argVal(args, flag) {
|
|
28
|
+
const i = args.indexOf(flag);
|
|
29
|
+
return i >= 0 ? args[i + 1] : null;
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { exportExecutiveSearchReport } from '../reporting/executiveSearchReport.js';
|
|
2
|
+
|
|
3
|
+
export async function executiveSearchCommand(args = [], root = process.cwd()) {
|
|
4
|
+
const target = argVal(args, '--target');
|
|
5
|
+
const baseUrl = argVal(args, '--base-url');
|
|
6
|
+
const runId = argVal(args, '--run');
|
|
7
|
+
const client = argVal(args, '--client');
|
|
8
|
+
const format = args.includes('--json') ? 'json' : argVal(args, '--format') || 'markdown';
|
|
9
|
+
const output = argVal(args, '--output');
|
|
10
|
+
const input = argVal(args, '--input');
|
|
11
|
+
|
|
12
|
+
const r = await exportExecutiveSearchReport(root, {
|
|
13
|
+
target,
|
|
14
|
+
baseUrl,
|
|
15
|
+
runId,
|
|
16
|
+
clientName: client || 'Enterprise Client',
|
|
17
|
+
format,
|
|
18
|
+
output,
|
|
19
|
+
backlinksInput: input,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return format === 'json' ? r.data : r.content;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function argVal(args, flag) {
|
|
26
|
+
const i = args.indexOf(flag);
|
|
27
|
+
return i >= 0 ? args[i + 1] : null;
|
|
28
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { auditPageCro } from "../analysis/croAudit.js";
|
|
1
2
|
import { buildContext } from './context.js';
|
|
2
3
|
import { registryPageFor, safePath } from '../detectors/framework.js';
|
|
3
4
|
import { selectDetectors } from '../detectors/index.js';
|
|
@@ -74,8 +75,14 @@ export async function inspectCro(root, pageRef, { target, baseUrl, refDate } = {
|
|
|
74
75
|
return acc + w;
|
|
75
76
|
}, 0);
|
|
76
77
|
|
|
78
|
+
const pageCroAudit = auditPageCro(page, ctx);
|
|
79
|
+
|
|
77
80
|
return {
|
|
78
81
|
url: page.url,
|
|
82
|
+
conversion_readiness_score: pageCroAudit.conversion_readiness_score,
|
|
83
|
+
atf_clarity: pageCroAudit.atf_clarity,
|
|
84
|
+
trust_and_credibility: pageCroAudit.trust_and_credibility,
|
|
85
|
+
offer_architecture: pageCroAudit.offer_architecture,
|
|
79
86
|
sourceFile: page.sourceFile,
|
|
80
87
|
status: page.status,
|
|
81
88
|
declared_conversion_action: declaredAction,
|
|
@@ -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
|
+
}
|
|
@@ -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.
|
|
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
|
|
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
|
|
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)];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { contextDir, loadRegistryFile } from '../registries/index.js';
|
|
4
|
+
import { readJson, writeJson } from '../shared/io.js';
|
|
5
|
+
import { buildStrategicRoadmap, formatRoadmapMarkdown } from '../analysis/strategicRoadmap.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `citable roadmap [generate|show]` — generate or view the 30/90/180-day strategic roadmap.
|
|
9
|
+
*/
|
|
10
|
+
export async function roadmapCommand(root, { runId, target, write = true } = {}) {
|
|
11
|
+
let findings = [];
|
|
12
|
+
let initiatives = [];
|
|
13
|
+
|
|
14
|
+
// 1. Load findings from specified or latest run
|
|
15
|
+
const runsDir = path.join(root, '.citable', 'runs');
|
|
16
|
+
let sourceRun = runId;
|
|
17
|
+
if (!sourceRun && fs.existsSync(runsDir)) {
|
|
18
|
+
const runs = fs.readdirSync(runsDir).filter((r) => fs.existsSync(path.join(runsDir, r, 'findings.json'))).sort();
|
|
19
|
+
if (runs.length) sourceRun = runs.at(-1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (sourceRun) {
|
|
23
|
+
const findingsFile = path.join(runsDir, sourceRun, 'findings.json');
|
|
24
|
+
if (fs.existsSync(findingsFile)) {
|
|
25
|
+
findings = JSON.parse(fs.readFileSync(findingsFile, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 2. Load initiatives if present
|
|
30
|
+
const initFile = path.join(contextDir(root), 'initiatives.yaml');
|
|
31
|
+
if (fs.existsSync(initFile)) {
|
|
32
|
+
try {
|
|
33
|
+
const data = loadRegistryFile(initFile, 'initiatives');
|
|
34
|
+
initiatives = data.entries || [];
|
|
35
|
+
} catch {
|
|
36
|
+
// Ignore non-fatal registry load problems
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Target domain resolution
|
|
41
|
+
let domain = target || 'nebulacomponents.com';
|
|
42
|
+
if (sourceRun) {
|
|
43
|
+
const manifestFile = path.join(runsDir, sourceRun, 'manifest.json');
|
|
44
|
+
if (fs.existsSync(manifestFile)) {
|
|
45
|
+
try {
|
|
46
|
+
const manifest = readJson(manifestFile);
|
|
47
|
+
if (manifest.target?.location) domain = manifest.target.location;
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const roadmap = buildStrategicRoadmap({ findings, initiatives, targetDomain: domain });
|
|
53
|
+
const markdown = formatRoadmapMarkdown(roadmap);
|
|
54
|
+
|
|
55
|
+
let artifactPath = null;
|
|
56
|
+
if (write) {
|
|
57
|
+
const outDir = path.join(root, '.citable', 'roadmap');
|
|
58
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
59
|
+
artifactPath = path.join(outDir, 'strategic-roadmap.md');
|
|
60
|
+
fs.writeFileSync(artifactPath, markdown, 'utf8');
|
|
61
|
+
writeJson(path.join(outDir, 'strategic-roadmap.json'), roadmap);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
...roadmap,
|
|
66
|
+
markdown,
|
|
67
|
+
artifact_path: artifactPath,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -2,9 +2,11 @@ import { buildContext } from './context.js';
|
|
|
2
2
|
import { selectDetectors } from '../detectors/index.js';
|
|
3
3
|
import { runDetectors, sitePageFor } from '../detectors/framework.js';
|
|
4
4
|
import { checklistItem, toStringArray } from '../shared/checklist.js';
|
|
5
|
+
import { recommendSchemaForPages } from '../analysis/schemaRecommendations.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* `citable schema` — derive JSON-LD proposals from registry data
|
|
8
|
+
* `citable schema` — derive JSON-LD proposals from registry data, recommend FAQPage, Speakable,
|
|
9
|
+
* and HowTo markup from visible page content, and validate deployed schema.
|
|
8
10
|
*
|
|
9
11
|
* Generation is registry-driven: schema is only proposed for entities that exist in the
|
|
10
12
|
* entity registry with a canonical URL. Nothing is fabricated; missing fields are listed
|
|
@@ -71,5 +73,11 @@ export async function schemaCommand(root, { target, baseUrl, refDate } = {}) {
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
// 4. Recommend structural schema markup (FAQPage, Speakable, HowTo) from visible content
|
|
77
|
+
let recommendations = { faq: [], speakable: [], howto: [] };
|
|
78
|
+
if (ctx.site?.pages) {
|
|
79
|
+
recommendations = recommendSchemaForPages(ctx.site.pages);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { findings, proposals, blocked, missingDeployment, recommendations };
|
|
75
83
|
}
|