@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,173 @@
|
|
|
1
|
+
import { parse as parseHtml } from 'node-html-parser';
|
|
2
|
+
|
|
3
|
+
const QUESTION_RX = /^(?:how|what|why|when|where|who|can|is|does|which|should)\b|\?$/i;
|
|
4
|
+
const HOWTO_TITLE_RX = /^(?:how\s+to|how\s+do\s+i|step-by-step|guide\s+to)\b/i;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Scan audited site pages for FAQPage, Speakable, and HowTo markup opportunities.
|
|
8
|
+
*/
|
|
9
|
+
export function recommendSchemaForPages(pages = []) {
|
|
10
|
+
const recommendations = {
|
|
11
|
+
faq: [],
|
|
12
|
+
speakable: [],
|
|
13
|
+
howto: [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
for (const page of pages) {
|
|
17
|
+
if (!page || page.status !== 200) continue;
|
|
18
|
+
const html = page.html || '';
|
|
19
|
+
const root = parseHtml(html);
|
|
20
|
+
const existingTypes = new Set(
|
|
21
|
+
(page.jsonLd || []).flatMap((j) => j.blocks || []).flatMap((b) => [].concat(b['@type'] || []))
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// -------------------------------------------------------------
|
|
25
|
+
// 1. FAQPage Recommendation
|
|
26
|
+
// -------------------------------------------------------------
|
|
27
|
+
if (!existingTypes.has('FAQPage')) {
|
|
28
|
+
const qas = [];
|
|
29
|
+
|
|
30
|
+
// Pattern A: <details><summary>Q</summary><p>A</p></details>
|
|
31
|
+
for (const details of root.querySelectorAll('details')) {
|
|
32
|
+
const summary = details.querySelector('summary');
|
|
33
|
+
if (summary) {
|
|
34
|
+
const questionText = summary.text.trim();
|
|
35
|
+
const clone = parseHtml(details.innerHTML);
|
|
36
|
+
const s = clone.querySelector('summary');
|
|
37
|
+
if (s) s.remove();
|
|
38
|
+
const answerText = clone.text.replace(/\s+/g, ' ').trim();
|
|
39
|
+
if (questionText && answerText) {
|
|
40
|
+
qas.push({ question: questionText, answer: answerText });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Pattern B: Question headings followed by paragraphs
|
|
46
|
+
if (qas.length < 2) {
|
|
47
|
+
const headings = root.querySelectorAll('h2, h3');
|
|
48
|
+
for (const h of headings) {
|
|
49
|
+
const hText = h.text.trim();
|
|
50
|
+
if (QUESTION_RX.test(hText)) {
|
|
51
|
+
// Find next sibling paragraph
|
|
52
|
+
let next = h.nextElementSibling;
|
|
53
|
+
let answerText = '';
|
|
54
|
+
while (next && !['H1', 'H2', 'H3', 'H4'].includes(next.tagName)) {
|
|
55
|
+
if (['P', 'UL', 'OL', 'DIV'].includes(next.tagName)) {
|
|
56
|
+
answerText += (answerText ? ' ' : '') + next.text.replace(/\s+/g, ' ').trim();
|
|
57
|
+
if (answerText.length > 60) break;
|
|
58
|
+
}
|
|
59
|
+
next = next.nextElementSibling;
|
|
60
|
+
}
|
|
61
|
+
if (hText && answerText) {
|
|
62
|
+
qas.push({ question: hText, answer: answerText });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (qas.length >= 2) {
|
|
69
|
+
recommendations.faq.push({
|
|
70
|
+
target_url: page.url,
|
|
71
|
+
source_file: page.sourceFile || null,
|
|
72
|
+
schema_type: 'FAQPage',
|
|
73
|
+
rationale: `Detected ${qas.length} visible question-and-answer pairs without FAQPage schema.`,
|
|
74
|
+
jsonld: {
|
|
75
|
+
'@context': 'https://schema.org',
|
|
76
|
+
'@type': 'FAQPage',
|
|
77
|
+
mainEntity: qas.slice(0, 10).map((qa) => ({
|
|
78
|
+
'@type': 'Question',
|
|
79
|
+
name: qa.question,
|
|
80
|
+
acceptedAnswer: {
|
|
81
|
+
'@type': 'Answer',
|
|
82
|
+
text: qa.answer,
|
|
83
|
+
},
|
|
84
|
+
})),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// -------------------------------------------------------------
|
|
91
|
+
// 2. Speakable Recommendation (SpeakableSpecification)
|
|
92
|
+
// -------------------------------------------------------------
|
|
93
|
+
const hasExistingSpeakable = (page.jsonLd || []).some((j) =>
|
|
94
|
+
(j.blocks || []).some((b) => b.speakable)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (!hasExistingSpeakable) {
|
|
98
|
+
const speakableSelectors = [];
|
|
99
|
+
if (root.querySelector('h1')) speakableSelectors.push('h1');
|
|
100
|
+
if (root.querySelector('#summary, .executive-summary, .summary, .key-takeaways')) {
|
|
101
|
+
speakableSelectors.push(root.querySelector('#summary') ? '#summary' : '.executive-summary');
|
|
102
|
+
} else if (root.querySelector('article > p:first-of-type, main > p:first-of-type, p:first-of-type')) {
|
|
103
|
+
speakableSelectors.push('article > p:first-of-type');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (speakableSelectors.length >= 2) {
|
|
107
|
+
recommendations.speakable.push({
|
|
108
|
+
target_url: page.url,
|
|
109
|
+
source_file: page.sourceFile || null,
|
|
110
|
+
schema_type: 'SpeakableSpecification',
|
|
111
|
+
rationale: 'Page has clear title and executive lead passage suitable for smart speaker / assistant audio playback.',
|
|
112
|
+
jsonld: {
|
|
113
|
+
'@context': 'https://schema.org',
|
|
114
|
+
'@type': 'WebPage',
|
|
115
|
+
'@id': `${page.url}#webpage`,
|
|
116
|
+
url: page.url,
|
|
117
|
+
name: page.title || 'Page',
|
|
118
|
+
speakable: {
|
|
119
|
+
'@type': 'SpeakableSpecification',
|
|
120
|
+
cssSelector: speakableSelectors,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// -------------------------------------------------------------
|
|
128
|
+
// 3. HowTo Recommendation
|
|
129
|
+
// -------------------------------------------------------------
|
|
130
|
+
if (!existingTypes.has('HowTo')) {
|
|
131
|
+
const pageTitle = page.title || '';
|
|
132
|
+
const h1s = (page.headings || []).filter((h) => h.level === 1).map((h) => h.text);
|
|
133
|
+
const isHowToPage = HOWTO_TITLE_RX.test(pageTitle) || h1s.some((h) => HOWTO_TITLE_RX.test(h));
|
|
134
|
+
|
|
135
|
+
if (isHowToPage) {
|
|
136
|
+
const steps = [];
|
|
137
|
+
// Look for ordered list items or step subheadings
|
|
138
|
+
const olItems = root.querySelectorAll('ol > li');
|
|
139
|
+
if (olItems.length >= 2) {
|
|
140
|
+
olItems.forEach((li, idx) => {
|
|
141
|
+
const stepText = li.text.replace(/\s+/g, ' ').trim();
|
|
142
|
+
if (stepText) {
|
|
143
|
+
steps.push({
|
|
144
|
+
'@type': 'HowToStep',
|
|
145
|
+
position: idx + 1,
|
|
146
|
+
name: `Step ${idx + 1}`,
|
|
147
|
+
text: stepText,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (steps.length >= 2) {
|
|
154
|
+
recommendations.howto.push({
|
|
155
|
+
target_url: page.url,
|
|
156
|
+
source_file: page.sourceFile || null,
|
|
157
|
+
schema_type: 'HowTo',
|
|
158
|
+
rationale: `Procedural instructional guide detected with ${steps.length} ordered steps.`,
|
|
159
|
+
jsonld: {
|
|
160
|
+
'@context': 'https://schema.org',
|
|
161
|
+
'@type': 'HowTo',
|
|
162
|
+
name: h1s[0] || pageTitle,
|
|
163
|
+
description: page.paragraphs?.[0] || 'Procedural guide and instructions.',
|
|
164
|
+
step: steps,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return recommendations;
|
|
173
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readJson, writeJson, nowIso } from '../shared/io.js';
|
|
4
|
+
import { scoreFinding, scoreInitiative } from './iceMatrix.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate a 30 / 90 / 180-day Strategic Roadmap from audit findings and initiatives.
|
|
8
|
+
*/
|
|
9
|
+
export function buildStrategicRoadmap({ findings = [], initiatives = [], targetDomain = 'example.com' } = {}) {
|
|
10
|
+
// Score all findings and initiatives with ICE
|
|
11
|
+
const scoredFindings = findings.map(scoreFinding);
|
|
12
|
+
const scoredInitiatives = initiatives.map(scoreInitiative);
|
|
13
|
+
|
|
14
|
+
// Group into horizons
|
|
15
|
+
const horizon30 = {
|
|
16
|
+
horizon: '30_days',
|
|
17
|
+
label: 'Days 1 - 30: Foundation & Unblock',
|
|
18
|
+
objective: 'Eliminate catastrophic crawl, index, and Core Web Vitals blockers; establish foundational crawler access and entity identity.',
|
|
19
|
+
focus_areas: [
|
|
20
|
+
'Technical SEO: Resolve HTTP non-200 responses, accidental noindex, and canonical contradictions',
|
|
21
|
+
'Core Web Vitals: Eliminate render-blocking scripts, specify image dimensions (CLS <= 0.1), preload LCP hero images',
|
|
22
|
+
'Answer Engine Access: Open robots.txt permissions for PerplexityBot, Bingbot, and OAI-SearchBot',
|
|
23
|
+
'Entity Identity: Deploy baseline Organization and primary Person JSON-LD schemas',
|
|
24
|
+
],
|
|
25
|
+
exit_criteria: [
|
|
26
|
+
'Zero critical-severity technical findings in citable audit technical',
|
|
27
|
+
'Static CLS readiness: 100% of body images have explicit dimensions',
|
|
28
|
+
'All major answer engine bots verified allowed in robots directives',
|
|
29
|
+
'Baseline Organization schema live with verified @id',
|
|
30
|
+
],
|
|
31
|
+
actions: [],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const horizon90 = {
|
|
35
|
+
horizon: '90_days',
|
|
36
|
+
label: 'Days 31 - 90: Governance, E-E-A-T & Extractability',
|
|
37
|
+
objective: 'Elevate on-page E-E-A-T, deploy rich structured data (FAQPage, Speakable, HowTo), audit backlinks, and automate fast indexing.',
|
|
38
|
+
focus_areas: [
|
|
39
|
+
'On-Page E-E-A-T: Attribute all content to verified authors, add credentials, visible update dates, and editorial transparency links',
|
|
40
|
+
'Schema Enrichment: Deploy FAQPage Q&A markup, SpeakableSpecification for audio extraction, and HowTo step schemas',
|
|
41
|
+
'Fast Indexing: Configure and verify IndexNow protocol connector for instant Bing / search engine notification',
|
|
42
|
+
'Off-Page Governance: Audit inbound backlink profile, flag toxic PBN/spam TLDs, and compile GSC disavow candidate file',
|
|
43
|
+
'Claim Substantiation: Map material business claims to evidence registry records',
|
|
44
|
+
],
|
|
45
|
+
exit_criteria: [
|
|
46
|
+
'Composite E-E-A-T score >= 4.0 / 5.0 across money and content pages',
|
|
47
|
+
'FAQPage and Speakable schema deployed on all eligible guide/product pages',
|
|
48
|
+
'IndexNow connection tested and submitting on content updates',
|
|
49
|
+
'Google Disavow file reviewed and archived in evidence package',
|
|
50
|
+
'Zero unverified material claims in citable substantiate',
|
|
51
|
+
],
|
|
52
|
+
actions: [],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const horizon180 = {
|
|
56
|
+
horizon: '180_days',
|
|
57
|
+
label: 'Days 91 - 180: GEO Dominance, Corroboration & Scale',
|
|
58
|
+
objective: 'Dominate generative search citations across Perplexity, Copilot, and ChatGPT; build digital PR authority; govern conversion experiments.',
|
|
59
|
+
focus_areas: [
|
|
60
|
+
'Generative Engine Optimization (GEO): Optimize RAG chunk boundaries (150-350 words) and direct copular definitions ("X is Y")',
|
|
61
|
+
'Competitive Share-of-Voice: Benchmark brand citation share and sentiment stance across AI engines vs key competitors',
|
|
62
|
+
'Authority & Corroboration: Earn high-authority primary citations (.edu, .gov, standards bodies, industry reports)',
|
|
63
|
+
'Conversion Rate Optimization (CRO): Implement accessible Nebula UI components and govern A/B experiment guardrails (SRM, stopping)',
|
|
64
|
+
'Continuous Governance: Schedule automated version-pinned audit runs with regression alerts',
|
|
65
|
+
],
|
|
66
|
+
exit_criteria: [
|
|
67
|
+
'Answer-Engine readiness score >= 85/100 across Perplexity, Bing Copilot, and ChatGPT',
|
|
68
|
+
'Weekly automated continuous audit pipeline with regression monitoring active',
|
|
69
|
+
'Dominant citation share on primary category prompts in AI search engine probes',
|
|
70
|
+
'Zero statistically invalid experiment decisions (SRM < 0.001)',
|
|
71
|
+
],
|
|
72
|
+
actions: [],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Map findings into horizons based on urgency and discipline
|
|
76
|
+
for (const f of scoredFindings) {
|
|
77
|
+
const ns = (f.id || '').split('-')[0];
|
|
78
|
+
if (f.severity === 'critical' || ['TECH', 'CRAWL', 'STATUS'].includes(ns)) {
|
|
79
|
+
horizon30.actions.push(f);
|
|
80
|
+
} else if (f.severity === 'high' || ['CWV', 'SCHEMA', 'PAGE', 'LINK'].includes(ns)) {
|
|
81
|
+
if (['CWV-001', 'CWV-004'].includes(f.id)) horizon30.actions.push(f);
|
|
82
|
+
else horizon90.actions.push(f);
|
|
83
|
+
} else if (['ANS', 'ENTITY', 'CLAIM', 'EVD'].includes(ns)) {
|
|
84
|
+
horizon90.actions.push(f);
|
|
85
|
+
} else {
|
|
86
|
+
horizon180.actions.push(f);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Map initiatives into horizons
|
|
91
|
+
for (const i of scoredInitiatives) {
|
|
92
|
+
if (i.impact >= 8 && i.effort <= 4) {
|
|
93
|
+
horizon30.actions.push(i);
|
|
94
|
+
} else if (i.impact >= 6) {
|
|
95
|
+
horizon90.actions.push(i);
|
|
96
|
+
} else {
|
|
97
|
+
horizon180.actions.push(i);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
fact_status: 'strategic_roadmap_projection',
|
|
103
|
+
target_domain: targetDomain,
|
|
104
|
+
generated_at: nowIso(),
|
|
105
|
+
methodology: 'Horizon Phasing: 30-Day (Critical Unblock & Core CWV) -> 90-Day (E-E-A-T, Schema & Off-Page) -> 180-Day (GEO Dominance & PR)',
|
|
106
|
+
horizons: [horizon30, horizon90, horizon180],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Format Strategic Roadmap as GitHub-flavored Markdown
|
|
112
|
+
*/
|
|
113
|
+
export function formatRoadmapMarkdown(roadmap) {
|
|
114
|
+
const lines = [
|
|
115
|
+
`# 30 / 90 / 180-Day Strategic Roadmap`,
|
|
116
|
+
``,
|
|
117
|
+
`- Target Property: \`${roadmap.target_domain}\``,
|
|
118
|
+
`- Generated: ${roadmap.generated_at}`,
|
|
119
|
+
`- Governance Methodology: ${roadmap.methodology}`,
|
|
120
|
+
``,
|
|
121
|
+
`> **Operating Premise Reminder**: This roadmap sequences verified remediation priorities and strategic enhancements. It establishes technical eligibility and extractability, but never guarantees ranking, traffic, or citation outcomes.`,
|
|
122
|
+
``,
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
for (const h of roadmap.horizons) {
|
|
126
|
+
lines.push(`## ${h.label}`);
|
|
127
|
+
lines.push(``);
|
|
128
|
+
lines.push(`**Objective**: ${h.objective}`);
|
|
129
|
+
lines.push(``);
|
|
130
|
+
lines.push(`### Strategic Focus Areas:`);
|
|
131
|
+
for (const f of h.focus_areas) lines.push(`- ${f}`);
|
|
132
|
+
lines.push(``);
|
|
133
|
+
lines.push(`### Exit Criteria & Success Milestones:`);
|
|
134
|
+
for (const c of h.exit_criteria) lines.push(`- [ ] ${c}`);
|
|
135
|
+
lines.push(``);
|
|
136
|
+
if (h.actions.length) {
|
|
137
|
+
lines.push(`### Prioritized Actions (${h.actions.length}):`);
|
|
138
|
+
lines.push(`| Action / Initiative | Impact | Effort | Conf | ICE Score | Quadrant |`);
|
|
139
|
+
lines.push(`| :--- | :--- | :--- | :--- | :--- | :--- |`);
|
|
140
|
+
for (const a of h.actions.slice(0, 10)) {
|
|
141
|
+
lines.push(`| **${a.id}**: ${a.title.slice(0, 45)} | ${a.impact}/10 | ${a.effort}/10 | ${a.confidence}/10 | **${a.ice_score}** | ${a.quadrant} |`);
|
|
142
|
+
}
|
|
143
|
+
lines.push(``);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { auditCroSuite, formatCroSuiteOutput } from "../commands/croSuite.js";
|
|
3
|
+
import { formatBacklogMarkdown } from "../commands/croBacklog.js";
|
|
4
|
+
import { formatCroRoadmapMarkdown } from "../analysis/croRoadmap.js";
|
|
5
|
+
import { sweepTechnical, formatSweepOutput } from "../commands/sweep.js";
|
|
6
|
+
import { inspectEeat, formatEeatOutput } from "../commands/inspectEeat.js";
|
|
7
|
+
import { inspectReadiness, formatReadinessOutput } from "../commands/answerEngineReadiness.js";
|
|
8
|
+
import { auditBacklinks, formatBacklinksOutput } from "../commands/auditBacklinks.js";
|
|
9
|
+
import { roadmapCommand } from "../commands/roadmapCmd.js";
|
|
10
|
+
import { sowCommand } from "../commands/sowCmd.js";
|
|
11
|
+
import { buildIceMatrix, formatIceMatrixOutput } from "../analysis/iceMatrix.js";
|
|
2
12
|
import fs from 'node:fs';
|
|
3
13
|
import path from 'node:path';
|
|
4
14
|
import { pathToFileURL } from 'node:url';
|
|
@@ -97,6 +107,15 @@ Commands
|
|
|
97
107
|
check experiment Experiment guardrails: SRM, stopping, power, contamination, lifecycle status
|
|
98
108
|
map-claims Extract material claim candidates from pages (--write to save)
|
|
99
109
|
substantiate Assess claim/evidence status (--write to apply downgrades)
|
|
110
|
+
cro [audit] Full CRO intelligence suite: funnel, ATF clarity, trust, cognitive load, ICE matrix, roadmap
|
|
111
|
+
cro backlog Generate A/B experimentation backlog with falsifiable hypotheses & guardrails
|
|
112
|
+
cro roadmap 30/90/180-day CRO roadmap tied to measurable conversion outcomes
|
|
113
|
+
sweep [technical] Run technical SEO sweep with Core Web Vitals metrics
|
|
114
|
+
inspect eeat <page> Evaluate on-page content against Google E-E-A-T rubric (0-5 scale)
|
|
115
|
+
inspect readiness <page> Evaluate answer-engine readiness across Perplexity, Bing Copilot, and ChatGPT
|
|
116
|
+
audit backlinks Audit off-page authority and identify toxic domains (--input <file>)
|
|
117
|
+
prioritize matrix Impact/Effort/Confidence (ICE) scoring matrix for roadmap & findings
|
|
118
|
+
roadmap [generate|show] 30/90/180-day strategic roadmap with milestone horizons
|
|
100
119
|
schema Validate deployed JSON-LD and propose registry-derived schema
|
|
101
120
|
validate [mode] registries (default) | claims | evidence | schema | links
|
|
102
121
|
compare-snapshots [a b] Regression diff between two audit runs
|
|
@@ -109,6 +128,8 @@ Commands
|
|
|
109
128
|
report dashboard [--last N] [--since <run-id>] Render a cross-run evidence trend as Markdown + HTML
|
|
110
129
|
report share-of-voice [--last N] Compute first-party and competitor citation share
|
|
111
130
|
report consensus [--last N] [--since <run-id>] Render Canonical Discovery Consensus Matrix (Markdown + HTML)
|
|
131
|
+
report search [--target <dir|url>] [--run <id>] Enterprise Search Intelligence & AEO/GEO Executive Briefing (19 pillars)
|
|
132
|
+
report cro [--target <dir|url>] [--input <file>] Enterprise CRO & Customer Journey Executive Briefing (25 pillars)
|
|
112
133
|
metrics import Import declared metric observations from CSV/JSON
|
|
113
134
|
connect status List optional connectors and configured connections
|
|
114
135
|
connect configure Configure non-secret connection state (--write to save)
|
|
@@ -287,6 +308,12 @@ function parseArgs(argv) {
|
|
|
287
308
|
else if (a === '--days-running') args['days-running'] = argv[++i];
|
|
288
309
|
else if (a === '--baseline-rate') args['baseline-rate'] = argv[++i];
|
|
289
310
|
else if (a === '--p-value') args['p-value'] = argv[++i];
|
|
311
|
+
else if (a === '--matrix') args.matrix = true;
|
|
312
|
+
else if (a === '--roadmap') args.roadmap = true;
|
|
313
|
+
else if (a === '--engines') args.engines = true;
|
|
314
|
+
else if (a === '--funnel') args.funnel = argv[++i];
|
|
315
|
+
else if (a === '--client') args.client = argv[++i];
|
|
316
|
+
else if (a === '--type') args.type = argv[++i];
|
|
290
317
|
else if (a === '--revenue-claimed') args['revenue-claimed'] = true;
|
|
291
318
|
else args._.push(a);
|
|
292
319
|
}
|
|
@@ -360,6 +387,16 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
|
|
360
387
|
break;
|
|
361
388
|
}
|
|
362
389
|
case 'audit': {
|
|
390
|
+
if (args._[0] === 'cro') {
|
|
391
|
+
const r = await auditCroSuite(root, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate, input: args.input, funnelId: args.funnel });
|
|
392
|
+
out(args, formatCroSuiteOutput(r), r);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
if (args._[0] === 'backlinks') {
|
|
396
|
+
const r = await auditBacklinks(root, { input: args.input, target: args.target, minSeverity: args.minSeverity });
|
|
397
|
+
out(args, formatBacklinksOutput(r), r);
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
363
400
|
if (args._[0] === 'edge') {
|
|
364
401
|
const file = args._[1];
|
|
365
402
|
if (!file || !args.format) throw new Error('usage: citable audit edge <file> --format <cloudflare-worker|cloudflare-cro|vercel-middleware|shopify-snippet|html>');
|
|
@@ -386,6 +423,18 @@ ${r.findings.map((f) => ` [${f.rule_id}] (${f.severity}) ${f.summary}\n Fix:
|
|
|
386
423
|
}
|
|
387
424
|
case 'inspect': {
|
|
388
425
|
if (!args._[0]) throw new Error('usage: citable inspect <page> --target <dir|url> OR citable inspect serp <page> OR citable inspect cro <page> OR citable inspect aeo <page> OR citable inspect geo <page>');
|
|
426
|
+
if (args._[0] === 'eeat' || args._[0] === 'content') {
|
|
427
|
+
if (!args._[1]) throw new Error('usage: citable inspect eeat <page> --target <dir|url>');
|
|
428
|
+
const r = await inspectEeat(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
|
|
429
|
+
out(args, formatEeatOutput(r), r);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
if (args._[0] === 'readiness' || args._[0] === 'engines') {
|
|
433
|
+
if (!args._[1]) throw new Error('usage: citable inspect readiness <page> --target <dir|url>');
|
|
434
|
+
const r = await inspectReadiness(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
|
|
435
|
+
out(args, formatReadinessOutput(r), r);
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
389
438
|
if (args._[0] === 'serp') {
|
|
390
439
|
if (!args._[1]) throw new Error('usage: citable inspect serp <page> --target <dir|url>');
|
|
391
440
|
const r = await inspectSerp(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
|
|
@@ -409,7 +458,9 @@ ${richList}
|
|
|
409
458
|
if (!args._[1]) throw new Error('usage: citable inspect cro <page> --target <dir|url>');
|
|
410
459
|
const r = await inspectCro(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
|
|
411
460
|
const txt = `Inspect CRO ${r.url}
|
|
412
|
-
Status: ${r.conversion_status}
|
|
461
|
+
Status: ${r.conversion_status} (Conversion Readiness: ${r.conversion_readiness_score ?? 'N/A'}/100)
|
|
462
|
+
Above-the-Fold Clarity: ${r.atf_clarity?.score ?? 'N/A'}/100 (Scent: ${r.atf_clarity?.scent_match ? 'CONGRUENT' : 'GAP'})
|
|
463
|
+
Trust & Credibility: ${r.trust_and_credibility?.score ?? 'N/A'}/100
|
|
413
464
|
Friction Surface Area: ${r.friction_surface_area}
|
|
414
465
|
Declared Conversion Action: ${r.declared_conversion_action || 'none'}
|
|
415
466
|
Page Type: ${r.page_type || 'unregistered'}; Intent: ${r.primary_intent || 'unregistered'}
|
|
@@ -746,6 +797,16 @@ ${r.recommendations.map((rec) => ` - ${rec}`).join('\n')}`;
|
|
|
746
797
|
break;
|
|
747
798
|
}
|
|
748
799
|
case 'action-plan': {
|
|
800
|
+
if (args.matrix) {
|
|
801
|
+
const r = await prioritizeCommand(['matrix', ...(args._[0] ? ['--run', args._[0]] : ['--findings'])], root);
|
|
802
|
+
out(args, r.formatted || JSON.stringify(r, null, 2), r);
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
if (args.roadmap) {
|
|
806
|
+
const r = await roadmapCommand(root, { runId: args._[0], target: args.target, write: args.write !== false });
|
|
807
|
+
out(args, r.markdown, r);
|
|
808
|
+
break;
|
|
809
|
+
}
|
|
749
810
|
const r = actionPlan(root, { runId: args._[0] });
|
|
750
811
|
out(args, `action-plan: ${r.summary.total_actions} action(s) [ready:${r.summary.ready} blocked:${r.summary.blocked}]\nPlan: ${path.join(r.dir, 'action-plan.md')}\nSource audit: ${r.source_run_id}`, r);
|
|
751
812
|
break;
|
|
@@ -798,11 +859,46 @@ ${r.recommendations.map((rec) => ` - ${rec}`).join('\n')}`;
|
|
|
798
859
|
} else if (sub === 'consensus') {
|
|
799
860
|
const r = reportConsensus(root, { runId: args.runId, since: args.since, last: args.last ? Number(args.last) : undefined });
|
|
800
861
|
out(args, `report consensus: ${r.urls_evaluated} URL(s) evaluated (${r.canonical_consensus_count} consensus, ${r.conflicts_count} conflict(s))\nMarkdown: ${r.path_md}\nHTML: ${r.path_html}`, r);
|
|
862
|
+
} else if (sub === 'search' || sub === 'seo') {
|
|
863
|
+
const r = await exportExecutiveReport(root, args.runId, {
|
|
864
|
+
type: 'search',
|
|
865
|
+
format: args.format || 'markdown',
|
|
866
|
+
clientName: args.client,
|
|
867
|
+
output: args.output,
|
|
868
|
+
target: args.target,
|
|
869
|
+
baseUrl: args.baseUrl,
|
|
870
|
+
refDate: args.refDate,
|
|
871
|
+
input: args.input,
|
|
872
|
+
});
|
|
873
|
+
out(args, r.content, r.data);
|
|
874
|
+
} else if (sub === 'cro') {
|
|
875
|
+
const r = await exportExecutiveReport(root, args.runId, {
|
|
876
|
+
type: 'cro',
|
|
877
|
+
format: args.format || 'markdown',
|
|
878
|
+
clientName: args.client,
|
|
879
|
+
output: args.output,
|
|
880
|
+
target: args.target,
|
|
881
|
+
baseUrl: args.baseUrl,
|
|
882
|
+
refDate: args.refDate,
|
|
883
|
+
input: args.input,
|
|
884
|
+
funnelId: args.funnel,
|
|
885
|
+
});
|
|
886
|
+
out(args, r.content, r.data);
|
|
801
887
|
} else if (sub === 'export') {
|
|
802
|
-
const r = await exportExecutiveReport(root, args.runId, {
|
|
888
|
+
const r = await exportExecutiveReport(root, args.runId, {
|
|
889
|
+
format: args.format,
|
|
890
|
+
clientName: args.client,
|
|
891
|
+
output: args.output,
|
|
892
|
+
type: args.type,
|
|
893
|
+
target: args.target,
|
|
894
|
+
baseUrl: args.baseUrl,
|
|
895
|
+
refDate: args.refDate,
|
|
896
|
+
input: args.input,
|
|
897
|
+
funnelId: args.funnel,
|
|
898
|
+
});
|
|
803
899
|
out(args, `Executive Report Exported (${r.format}) for "${r.client_name}"${r.output_path ? ` to ${r.output_path}` : ''}\n\n${r.content}`, r);
|
|
804
900
|
} else {
|
|
805
|
-
throw new Error('usage: citable report <dashboard|share-of-voice|consensus|export> [--last <n>] [--since <run-id>] [--run <run-id>] [--format <html
|
|
901
|
+
throw new Error('usage: citable report <dashboard|share-of-voice|consensus|search|cro|export> [--last <n>] [--since <run-id>] [--run <run-id>] [--format <md|html|json>]');
|
|
806
902
|
}
|
|
807
903
|
break;
|
|
808
904
|
}
|
|
@@ -1178,6 +1274,56 @@ ${r.findings.map((f) => ` [${f.guardrail_id}] (${f.severity}) ${f.summary}`).jo
|
|
|
1178
1274
|
out(args, JSON.stringify(r, null, 2), r);
|
|
1179
1275
|
break;
|
|
1180
1276
|
}
|
|
1277
|
+
case 'cro': {
|
|
1278
|
+
const sub = args._[0];
|
|
1279
|
+
if (sub === 'backlog') {
|
|
1280
|
+
const r = await auditCroSuite(root, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate, input: args.input });
|
|
1281
|
+
out(args, formatBacklogMarkdown(r.experiment_backlog), r.experiment_backlog);
|
|
1282
|
+
break;
|
|
1283
|
+
}
|
|
1284
|
+
if (sub === 'roadmap') {
|
|
1285
|
+
const r = await auditCroSuite(root, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate, input: args.input });
|
|
1286
|
+
out(args, formatCroRoadmapMarkdown(r.strategic_roadmap), r.strategic_roadmap);
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1289
|
+
const r = await auditCroSuite(root, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate, input: args.input, funnelId: args.funnel });
|
|
1290
|
+
out(args, formatCroSuiteOutput(r), r);
|
|
1291
|
+
break;
|
|
1292
|
+
}
|
|
1293
|
+
case 'sweep': {
|
|
1294
|
+
const sub = args._[0];
|
|
1295
|
+
if (sub && sub !== 'technical') throw new Error('usage: citable sweep technical [--target <dir|url>]');
|
|
1296
|
+
const r = await sweepTechnical(root, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate, page: args._[1] });
|
|
1297
|
+
out(args, formatSweepOutput(r), r);
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
1300
|
+
case 'matrix': {
|
|
1301
|
+
const r = await prioritizeCommand(['matrix', ...argv.slice(1)], root);
|
|
1302
|
+
out(args, r.formatted || JSON.stringify(r, null, 2), r);
|
|
1303
|
+
break;
|
|
1304
|
+
}
|
|
1305
|
+
case 'roadmap': {
|
|
1306
|
+
const r = await roadmapCommand(root, { runId: args.runId, target: args.target, write: args.write !== false });
|
|
1307
|
+
out(args, r.markdown, r);
|
|
1308
|
+
break;
|
|
1309
|
+
}
|
|
1310
|
+
case 'sow': {
|
|
1311
|
+
const r = await sowCommand(argv.slice(1), root);
|
|
1312
|
+
out(args, r.content || r.message || JSON.stringify(r, null, 2), r.data || r);
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1315
|
+
case 'inspect-eeat': {
|
|
1316
|
+
if (!args._[0]) throw new Error('usage: citable inspect-eeat <page> --target <dir|url>');
|
|
1317
|
+
const r = await inspectEeat(root, args._[0], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
|
|
1318
|
+
out(args, formatEeatOutput(r), r);
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
case 'inspect-readiness': {
|
|
1322
|
+
if (!args._[0]) throw new Error('usage: citable inspect-readiness <page> --target <dir|url>');
|
|
1323
|
+
const r = await inspectReadiness(root, args._[0], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
|
|
1324
|
+
out(args, formatReadinessOutput(r), r);
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1181
1327
|
case undefined:
|
|
1182
1328
|
console.log(HELP);
|
|
1183
1329
|
break;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { buildContext } from './context.js';
|
|
2
|
+
import { safePath } from '../detectors/framework.js';
|
|
3
|
+
import { evaluateAnswerEngineReadiness } from '../analysis/readiness.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `citable inspect readiness <page>` — evaluate page extraction and readiness across
|
|
7
|
+
* Perplexity, Bing Copilot, and ChatGPT / SearchGPT.
|
|
8
|
+
*/
|
|
9
|
+
export async function inspectReadiness(root, pageRef, { target, baseUrl, refDate } = {}) {
|
|
10
|
+
const ctx = await buildContext(root, { target, baseUrl, refDate });
|
|
11
|
+
if (!ctx.site) throw new Error('inspect readiness requires a site target (built output directory or URL)');
|
|
12
|
+
const wanted = safePath(pageRef, ctx.site.baseUrl);
|
|
13
|
+
const page = ctx.site.pages.find((p) => safePath(p.url) === wanted || p.sourceFile === pageRef);
|
|
14
|
+
if (!page) throw new Error(`page not found in audited output: ${pageRef}`);
|
|
15
|
+
|
|
16
|
+
return evaluateAnswerEngineReadiness(page, ctx);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format terminal output for `citable inspect readiness <page>`
|
|
21
|
+
*/
|
|
22
|
+
export function formatReadinessOutput(r) {
|
|
23
|
+
const lines = [
|
|
24
|
+
`Answer-Engine Readiness: ${r.url}`,
|
|
25
|
+
`==============================================`,
|
|
26
|
+
`Composite Readiness Score: ${r.composite_readiness_score} / 100`,
|
|
27
|
+
``,
|
|
28
|
+
`ENGINE EVALUATIONS:`,
|
|
29
|
+
` Perplexity: ${r.engines.perplexity.score}/100 [${r.engines.perplexity.status.toUpperCase()}] (Bot: ${r.engines.perplexity.primary_bot})`,
|
|
30
|
+
` Bing Copilot: ${r.engines.bing_copilot.score}/100 [${r.engines.bing_copilot.status.toUpperCase()}] (Bot: ${r.engines.bing_copilot.primary_bot})`,
|
|
31
|
+
` ChatGPT: ${r.engines.chatgpt.score}/100 [${r.engines.chatgpt.status.toUpperCase()}] (Bot: ${r.engines.chatgpt.primary_bot})`,
|
|
32
|
+
``,
|
|
33
|
+
`CROSS-ENGINE CAPABILITY MATRIX:`,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const row of r.comparison_matrix) {
|
|
37
|
+
lines.push(` - ${row.capability.padEnd(28)} | Perplexity: ${row.perplexity.padEnd(10)} | Copilot: ${row.bing_copilot.padEnd(14)} | ChatGPT: ${row.chatgpt}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (r.recommendations.length) {
|
|
41
|
+
lines.push(``, `PRIORITIZED ACTIONS:`);
|
|
42
|
+
for (const rec of r.recommendations) {
|
|
43
|
+
lines.push(` [${rec.priority.toUpperCase()}] (${rec.engine.toUpperCase()}) ${rec.action}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse as parseCsv } from 'csv-parse/sync';
|
|
4
|
+
import { auditBacklinkProfile } from '../analysis/offpage.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `citable audit backlinks` — audit off-page authority and identify toxic domains.
|
|
8
|
+
*/
|
|
9
|
+
export async function auditBacklinks(root, { input, target, minSeverity } = {}) {
|
|
10
|
+
let backlinks = [];
|
|
11
|
+
|
|
12
|
+
if (input) {
|
|
13
|
+
const filePath = path.resolve(root, input);
|
|
14
|
+
if (!fs.existsSync(filePath)) throw new Error(`input file not found: ${input}`);
|
|
15
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
16
|
+
|
|
17
|
+
if (filePath.endsWith('.csv')) {
|
|
18
|
+
const records = parseCsv(raw, { columns: true, skip_empty_lines: true });
|
|
19
|
+
backlinks = records.map((r) => ({
|
|
20
|
+
source_url: r.source_url || r.Source || r.url || r.URL,
|
|
21
|
+
target_url: r.target_url || r.Target || r.target,
|
|
22
|
+
anchor_text: r.anchor_text || r.Anchor || r.anchor || '',
|
|
23
|
+
rel: r.rel || r.Rel || r.type || '',
|
|
24
|
+
source_ip: r.source_ip || r.IP || r.ip || null,
|
|
25
|
+
}));
|
|
26
|
+
} else {
|
|
27
|
+
const parsed = JSON.parse(raw);
|
|
28
|
+
backlinks = Array.isArray(parsed) ? parsed : (parsed.backlinks || []);
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
// Check if there is a competitor or backlink import in .citable/
|
|
32
|
+
const defaultFile = path.join(root, '.citable', 'backlinks.json');
|
|
33
|
+
if (fs.existsSync(defaultFile)) {
|
|
34
|
+
const parsed = JSON.parse(fs.readFileSync(defaultFile, 'utf8'));
|
|
35
|
+
backlinks = Array.isArray(parsed) ? parsed : (parsed.backlinks || []);
|
|
36
|
+
} else {
|
|
37
|
+
throw new Error('audit backlinks requires --input <backlinks.json|csv> or .citable/backlinks.json');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = auditBacklinkProfile(backlinks, { targetDomain: target });
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Format terminal output for `citable audit backlinks`
|
|
47
|
+
*/
|
|
48
|
+
export function formatBacklinksOutput(r) {
|
|
49
|
+
const lines = [
|
|
50
|
+
`Off-Page Authority & Toxic Domain Audit`,
|
|
51
|
+
`=======================================`,
|
|
52
|
+
`Profile Health: ${r.profile_health.toUpperCase()}`,
|
|
53
|
+
`Total Backlinks: ${r.summary.total_backlinks} across ${r.summary.total_referring_domains} referring domain(s)`,
|
|
54
|
+
`Dofollow: ${r.summary.dofollow_count} | Nofollow: ${r.summary.nofollow_count} | UGC: ${r.summary.ugc_count} | Sponsored: ${r.summary.sponsored_count}`,
|
|
55
|
+
`Deep Link Ratio: ${r.summary.deep_link_ratio_pct}%`,
|
|
56
|
+
``,
|
|
57
|
+
`ANCHOR TEXT PROFILE:`,
|
|
58
|
+
` Branded Anchors: ${r.anchor_profile.branded_pct}%`,
|
|
59
|
+
` Commercial Exact Match: ${r.anchor_profile.commercial_exact_match_pct}% (Risk: ${r.anchor_profile.over_optimization_risk.toUpperCase()})`,
|
|
60
|
+
``,
|
|
61
|
+
`TOXIC DOMAIN IDENTIFICATION:`,
|
|
62
|
+
` Flagged Toxic Domains: ${r.summary.toxic_domains_count} (Critical: ${r.summary.critical_risk_domains}, High: ${r.summary.high_risk_domains})`,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
if (r.toxic_domains.length) {
|
|
66
|
+
lines.push(``, `Flagged Risk Candidates:`);
|
|
67
|
+
for (const d of r.toxic_domains.slice(0, 15)) {
|
|
68
|
+
lines.push(` [${d.risk_tier.toUpperCase()}] ${d.domain}`);
|
|
69
|
+
for (const reason of d.reasons) lines.push(` - ${reason}`);
|
|
70
|
+
if (d.anchors.length) lines.push(` - Sample anchors: "${d.anchors.join('", "')}"`);
|
|
71
|
+
}
|
|
72
|
+
lines.push(``, `Google Search Console Disavow Syntax Preview:`);
|
|
73
|
+
lines.push(r.disavow_export.split('\n').slice(0, 10).join('\n'));
|
|
74
|
+
} else {
|
|
75
|
+
lines.push(` No toxic domain risk patterns detected in profile.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return lines.join('\n');
|
|
79
|
+
}
|