@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/universal/.agents/skills/citable/SKILL.md +41 -1
  3. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  4. package/dist/universal/.agents/skills/citable/agents/citable-remediator.md +43 -0
  5. package/dist/universal/.agents/skills/citable/agents/citable-sow-architect.md +43 -0
  6. package/dist/universal/.agents/skills/citable/commands/README.md +13 -0
  7. package/dist/universal/.agents/skills/citable/commands/remediation.md +55 -0
  8. package/dist/universal/.agents/skills/citable/commands/reporting.md +28 -0
  9. package/dist/universal/.agents/skills/citable/commands/sow.md +95 -0
  10. package/dist/universal/.agents/skills/citable/manifest.json +12 -7
  11. package/dist/universal/.agents/skills/citable/schemas/sow.schema.json +409 -0
  12. package/dist/universal/.claude/agents/citable/citable-remediator.md +43 -0
  13. package/dist/universal/.claude/agents/citable/citable-sow-architect.md +43 -0
  14. package/dist/universal/.claude/agents/citable/manifest.json +6 -4
  15. package/dist/universal/.claude/skills/citable/SKILL.md +41 -1
  16. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  17. package/dist/universal/.claude/skills/citable/agents/citable-remediator.md +43 -0
  18. package/dist/universal/.claude/skills/citable/agents/citable-sow-architect.md +43 -0
  19. package/dist/universal/.claude/skills/citable/commands/README.md +13 -0
  20. package/dist/universal/.claude/skills/citable/commands/remediation.md +55 -0
  21. package/dist/universal/.claude/skills/citable/commands/reporting.md +28 -0
  22. package/dist/universal/.claude/skills/citable/commands/sow.md +95 -0
  23. package/dist/universal/.claude/skills/citable/manifest.json +16 -9
  24. package/dist/universal/.claude/skills/citable/schemas/sow.schema.json +409 -0
  25. package/dist/universal/.cursor/skills/citable/SKILL.md +41 -1
  26. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  27. package/dist/universal/.cursor/skills/citable/agents/citable-remediator.md +43 -0
  28. package/dist/universal/.cursor/skills/citable/agents/citable-sow-architect.md +43 -0
  29. package/dist/universal/.cursor/skills/citable/commands/README.md +13 -0
  30. package/dist/universal/.cursor/skills/citable/commands/remediation.md +55 -0
  31. package/dist/universal/.cursor/skills/citable/commands/reporting.md +28 -0
  32. package/dist/universal/.cursor/skills/citable/commands/sow.md +95 -0
  33. package/dist/universal/.cursor/skills/citable/manifest.json +12 -7
  34. package/dist/universal/.cursor/skills/citable/schemas/sow.schema.json +409 -0
  35. package/dist/universal/.gemini/skills/citable/SKILL.md +41 -1
  36. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  37. package/dist/universal/.gemini/skills/citable/agents/citable-remediator.md +43 -0
  38. package/dist/universal/.gemini/skills/citable/agents/citable-sow-architect.md +43 -0
  39. package/dist/universal/.gemini/skills/citable/commands/README.md +13 -0
  40. package/dist/universal/.gemini/skills/citable/commands/remediation.md +55 -0
  41. package/dist/universal/.gemini/skills/citable/commands/reporting.md +28 -0
  42. package/dist/universal/.gemini/skills/citable/commands/sow.md +95 -0
  43. package/dist/universal/.gemini/skills/citable/manifest.json +12 -7
  44. package/dist/universal/.gemini/skills/citable/schemas/sow.schema.json +409 -0
  45. package/dist/universal/.github/skills/citable/SKILL.md +41 -1
  46. package/dist/universal/.github/skills/citable/VERSION +1 -1
  47. package/dist/universal/.github/skills/citable/agents/citable-remediator.md +43 -0
  48. package/dist/universal/.github/skills/citable/agents/citable-sow-architect.md +43 -0
  49. package/dist/universal/.github/skills/citable/commands/README.md +13 -0
  50. package/dist/universal/.github/skills/citable/commands/remediation.md +55 -0
  51. package/dist/universal/.github/skills/citable/commands/reporting.md +28 -0
  52. package/dist/universal/.github/skills/citable/commands/sow.md +95 -0
  53. package/dist/universal/.github/skills/citable/manifest.json +12 -7
  54. package/dist/universal/.github/skills/citable/schemas/sow.schema.json +409 -0
  55. package/dist/universal/.kiro/skills/citable/SKILL.md +41 -1
  56. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  57. package/dist/universal/.kiro/skills/citable/agents/citable-remediator.md +43 -0
  58. package/dist/universal/.kiro/skills/citable/agents/citable-sow-architect.md +43 -0
  59. package/dist/universal/.kiro/skills/citable/commands/README.md +13 -0
  60. package/dist/universal/.kiro/skills/citable/commands/remediation.md +55 -0
  61. package/dist/universal/.kiro/skills/citable/commands/reporting.md +28 -0
  62. package/dist/universal/.kiro/skills/citable/commands/sow.md +95 -0
  63. package/dist/universal/.kiro/skills/citable/manifest.json +12 -7
  64. package/dist/universal/.kiro/skills/citable/schemas/sow.schema.json +409 -0
  65. package/dist/universal/.opencode/skills/citable/SKILL.md +41 -1
  66. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  67. package/dist/universal/.opencode/skills/citable/agents/citable-remediator.md +43 -0
  68. package/dist/universal/.opencode/skills/citable/agents/citable-sow-architect.md +43 -0
  69. package/dist/universal/.opencode/skills/citable/commands/README.md +13 -0
  70. package/dist/universal/.opencode/skills/citable/commands/remediation.md +55 -0
  71. package/dist/universal/.opencode/skills/citable/commands/reporting.md +28 -0
  72. package/dist/universal/.opencode/skills/citable/commands/sow.md +95 -0
  73. package/dist/universal/.opencode/skills/citable/manifest.json +12 -7
  74. package/dist/universal/.opencode/skills/citable/schemas/sow.schema.json +409 -0
  75. package/dist/universal/.pi/agent/skills/citable/SKILL.md +41 -1
  76. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  77. package/dist/universal/.pi/agent/skills/citable/agents/citable-remediator.md +43 -0
  78. package/dist/universal/.pi/agent/skills/citable/agents/citable-sow-architect.md +43 -0
  79. package/dist/universal/.pi/agent/skills/citable/commands/README.md +13 -0
  80. package/dist/universal/.pi/agent/skills/citable/commands/remediation.md +55 -0
  81. package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +28 -0
  82. package/dist/universal/.pi/agent/skills/citable/commands/sow.md +95 -0
  83. package/dist/universal/.pi/agent/skills/citable/manifest.json +12 -7
  84. package/dist/universal/.pi/agent/skills/citable/schemas/sow.schema.json +409 -0
  85. package/dist/universal/.qoder/skills/citable/SKILL.md +41 -1
  86. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  87. package/dist/universal/.qoder/skills/citable/agents/citable-remediator.md +43 -0
  88. package/dist/universal/.qoder/skills/citable/agents/citable-sow-architect.md +43 -0
  89. package/dist/universal/.qoder/skills/citable/commands/README.md +13 -0
  90. package/dist/universal/.qoder/skills/citable/commands/remediation.md +55 -0
  91. package/dist/universal/.qoder/skills/citable/commands/reporting.md +28 -0
  92. package/dist/universal/.qoder/skills/citable/commands/sow.md +95 -0
  93. package/dist/universal/.qoder/skills/citable/manifest.json +12 -7
  94. package/dist/universal/.qoder/skills/citable/schemas/sow.schema.json +409 -0
  95. package/dist/universal/.rovodev/skills/citable/SKILL.md +41 -1
  96. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  97. package/dist/universal/.rovodev/skills/citable/agents/citable-remediator.md +43 -0
  98. package/dist/universal/.rovodev/skills/citable/agents/citable-sow-architect.md +43 -0
  99. package/dist/universal/.rovodev/skills/citable/commands/README.md +13 -0
  100. package/dist/universal/.rovodev/skills/citable/commands/remediation.md +55 -0
  101. package/dist/universal/.rovodev/skills/citable/commands/reporting.md +28 -0
  102. package/dist/universal/.rovodev/skills/citable/commands/sow.md +95 -0
  103. package/dist/universal/.rovodev/skills/citable/manifest.json +12 -7
  104. package/dist/universal/.rovodev/skills/citable/schemas/sow.schema.json +409 -0
  105. package/dist/universal/.trae/skills/citable/SKILL.md +41 -1
  106. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  107. package/dist/universal/.trae/skills/citable/agents/citable-remediator.md +43 -0
  108. package/dist/universal/.trae/skills/citable/agents/citable-sow-architect.md +43 -0
  109. package/dist/universal/.trae/skills/citable/commands/README.md +13 -0
  110. package/dist/universal/.trae/skills/citable/commands/remediation.md +55 -0
  111. package/dist/universal/.trae/skills/citable/commands/reporting.md +28 -0
  112. package/dist/universal/.trae/skills/citable/commands/sow.md +95 -0
  113. package/dist/universal/.trae/skills/citable/manifest.json +12 -7
  114. package/dist/universal/.trae/skills/citable/schemas/sow.schema.json +409 -0
  115. package/dist/universal/.trae-cn/skills/citable/SKILL.md +41 -1
  116. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  117. package/dist/universal/.trae-cn/skills/citable/agents/citable-remediator.md +43 -0
  118. package/dist/universal/.trae-cn/skills/citable/agents/citable-sow-architect.md +43 -0
  119. package/dist/universal/.trae-cn/skills/citable/commands/README.md +13 -0
  120. package/dist/universal/.trae-cn/skills/citable/commands/remediation.md +55 -0
  121. package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +28 -0
  122. package/dist/universal/.trae-cn/skills/citable/commands/sow.md +95 -0
  123. package/dist/universal/.trae-cn/skills/citable/manifest.json +12 -7
  124. package/dist/universal/.trae-cn/skills/citable/schemas/sow.schema.json +409 -0
  125. package/dist/universal/manifest.json +150 -88
  126. package/package.json +1 -1
  127. package/schemas/sow.schema.json +409 -0
  128. package/skill/SKILL.md +41 -1
  129. package/skill/agents/citable-remediator.md +43 -0
  130. package/skill/agents/citable-sow-architect.md +43 -0
  131. package/skill/commands/README.md +13 -0
  132. package/skill/commands/remediation.md +55 -0
  133. package/skill/commands/reporting.md +28 -0
  134. package/skill/commands/sow.md +95 -0
  135. package/src/analysis/behavioral.js +126 -0
  136. package/src/analysis/croAudit.js +302 -0
  137. package/src/analysis/croRoadmap.js +121 -0
  138. package/src/analysis/eeat.js +300 -0
  139. package/src/analysis/funnelAnalysis.js +171 -0
  140. package/src/analysis/iceMatrix.js +205 -0
  141. package/src/analysis/offpage.js +228 -0
  142. package/src/analysis/readiness.js +253 -0
  143. package/src/analysis/schemaRecommendations.js +173 -0
  144. package/src/analysis/strategicRoadmap.js +148 -0
  145. package/src/cli/index.js +149 -3
  146. package/src/commands/answerEngineReadiness.js +48 -0
  147. package/src/commands/auditBacklinks.js +79 -0
  148. package/src/commands/croBacklog.js +182 -0
  149. package/src/commands/croSuite.js +156 -0
  150. package/src/commands/executive.js +6 -0
  151. package/src/commands/executiveCro.js +30 -0
  152. package/src/commands/executiveSearch.js +28 -0
  153. package/src/commands/inspectCro.js +7 -0
  154. package/src/commands/inspectEeat.js +53 -0
  155. package/src/commands/prioritize.js +42 -6
  156. package/src/commands/roadmapCmd.js +69 -0
  157. package/src/commands/schemaCmd.js +10 -2
  158. package/src/commands/sowCmd.js +84 -0
  159. package/src/commands/sweep.js +357 -0
  160. package/src/reporting/executiveCroReport.js +731 -0
  161. package/src/reporting/executiveExport.js +62 -0
  162. package/src/reporting/executiveSearchReport.js +704 -0
  163. package/src/sow/admissibilityGate.js +364 -0
  164. 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, { format: args.format, clientName: args.client, output: args.output });
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-brief|markdown-deck>]');
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
+ }