@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,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impact / Effort / Confidence (ICE) scoring matrix for Citable findings and initiatives.
|
|
3
|
+
*
|
|
4
|
+
* All factors are explicitly scored on a transparent 1-10 scale.
|
|
5
|
+
* Formula: ICE Score = (Impact × Confidence) / Effort
|
|
6
|
+
* Quadrants:
|
|
7
|
+
* - Quick Wins: High Impact (>=6), Low Effort (<=4), High Confidence (>=6)
|
|
8
|
+
* - Strategic Bets: High Impact (>=6), High Effort (>=5)
|
|
9
|
+
* - Low-Hanging Fruit: Low Impact (<6), Low Effort (<=4)
|
|
10
|
+
* - Deprioritize: Low Impact (<6), High Effort (>=5) or Low Confidence (<5)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const SEVERITY_IMPACT_MAP = {
|
|
14
|
+
critical: 10,
|
|
15
|
+
high: 8,
|
|
16
|
+
medium: 5,
|
|
17
|
+
low: 2,
|
|
18
|
+
informational: 1,
|
|
19
|
+
experimental: 2,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const DETECTOR_EFFORT_MAP = {
|
|
23
|
+
TECH: 4,
|
|
24
|
+
CRAWL: 3,
|
|
25
|
+
CWV: 5,
|
|
26
|
+
ARCH: 6,
|
|
27
|
+
PAGE: 3,
|
|
28
|
+
ANS: 4,
|
|
29
|
+
GEO: 5,
|
|
30
|
+
RECO: 6,
|
|
31
|
+
SCHEMA: 3,
|
|
32
|
+
LINK: 4,
|
|
33
|
+
ENTITY: 4,
|
|
34
|
+
CLAIM: 5,
|
|
35
|
+
EVD: 6,
|
|
36
|
+
SEC: 4,
|
|
37
|
+
MOBILE: 3,
|
|
38
|
+
CRO: 4,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const CONFIDENCE_MAP = {
|
|
42
|
+
deterministic: 10,
|
|
43
|
+
verified: 9,
|
|
44
|
+
validated: 8,
|
|
45
|
+
proxy: 6,
|
|
46
|
+
modeled: 6,
|
|
47
|
+
anecdote: 4,
|
|
48
|
+
assumption: 3,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Score an audit finding for the ICE matrix
|
|
53
|
+
*/
|
|
54
|
+
export function scoreFinding(finding) {
|
|
55
|
+
const severity = finding.classification?.severity || 'medium';
|
|
56
|
+
const impact = SEVERITY_IMPACT_MAP[severity] ?? 5;
|
|
57
|
+
|
|
58
|
+
const ns = (finding.detector_id || '').split('-')[0];
|
|
59
|
+
let effort = DETECTOR_EFFORT_MAP[ns] ?? 4;
|
|
60
|
+
if (finding.remediation?.review_required) effort += 2;
|
|
61
|
+
effort = Math.min(10, Math.max(1, effort));
|
|
62
|
+
|
|
63
|
+
let confidence = 8;
|
|
64
|
+
if (finding.classification?.deterministic) {
|
|
65
|
+
confidence = 10;
|
|
66
|
+
} else if (finding.fact_status === 'observed_telemetry') {
|
|
67
|
+
confidence = 9;
|
|
68
|
+
} else if (finding.fact_status === 'modeled_rubric_evaluation') {
|
|
69
|
+
confidence = 6;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const iceScore = Math.round(((impact * confidence) / effort) * 10) / 10;
|
|
73
|
+
|
|
74
|
+
let quadrant = 'low_hanging_fruit';
|
|
75
|
+
if (impact >= 6 && effort <= 4 && confidence >= 6) {
|
|
76
|
+
quadrant = 'quick_wins';
|
|
77
|
+
} else if (impact >= 6 && effort >= 5) {
|
|
78
|
+
quadrant = 'strategic_bets';
|
|
79
|
+
} else if (impact < 6 && effort <= 4) {
|
|
80
|
+
quadrant = 'low_hanging_fruit';
|
|
81
|
+
} else {
|
|
82
|
+
quadrant = 'deprioritize';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
id: finding.detector_id || finding.finding_id,
|
|
87
|
+
finding_id: finding.finding_id,
|
|
88
|
+
detector_id: finding.detector_id,
|
|
89
|
+
title: finding.observation?.summary || finding.name || finding.detector_id,
|
|
90
|
+
subject: finding.subject?.identifier || finding.subject?.url || 'global',
|
|
91
|
+
severity,
|
|
92
|
+
impact,
|
|
93
|
+
effort,
|
|
94
|
+
confidence,
|
|
95
|
+
ice_score: iceScore,
|
|
96
|
+
quadrant,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Score an initiative from initiatives.yaml for the ICE matrix
|
|
102
|
+
*/
|
|
103
|
+
export function scoreInitiative(initiative) {
|
|
104
|
+
const SCALE = { none: 0, low: 2, medium: 5, high: 8, critical: 10, validated: 9, transformative: 10, trivial: 1, very_high: 9, negligible: 1 };
|
|
105
|
+
|
|
106
|
+
const demand = SCALE[initiative.customer_demand] ?? 5;
|
|
107
|
+
const rev = SCALE[initiative.revenue_potential] ?? 5;
|
|
108
|
+
const diff = SCALE[initiative.strategic_differentiation] ?? 5;
|
|
109
|
+
const impact = Math.min(10, Math.max(1, Math.round((demand + rev + diff) / 3)));
|
|
110
|
+
|
|
111
|
+
const engCost = SCALE[initiative.engineering_cost] ?? 5;
|
|
112
|
+
const opCost = SCALE[initiative.operating_cost || 'none'] ?? 0;
|
|
113
|
+
const effort = Math.min(10, Math.max(1, Math.round(engCost + opCost * 0.3)));
|
|
114
|
+
|
|
115
|
+
const confidence = CONFIDENCE_MAP[initiative.evidence_strength] ?? 5;
|
|
116
|
+
const iceScore = Math.round(((impact * confidence) / effort) * 10) / 10;
|
|
117
|
+
|
|
118
|
+
let quadrant = 'low_hanging_fruit';
|
|
119
|
+
if (impact >= 6 && effort <= 4 && confidence >= 6) {
|
|
120
|
+
quadrant = 'quick_wins';
|
|
121
|
+
} else if (impact >= 6 && effort >= 5) {
|
|
122
|
+
quadrant = 'strategic_bets';
|
|
123
|
+
} else if (impact < 6 && effort <= 4) {
|
|
124
|
+
quadrant = 'low_hanging_fruit';
|
|
125
|
+
} else {
|
|
126
|
+
quadrant = 'deprioritize';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
id: initiative.initiative_id,
|
|
131
|
+
title: initiative.title,
|
|
132
|
+
owner: initiative.owner || 'unassigned',
|
|
133
|
+
status: initiative.status,
|
|
134
|
+
impact,
|
|
135
|
+
effort,
|
|
136
|
+
confidence,
|
|
137
|
+
ice_score: iceScore,
|
|
138
|
+
quadrant,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Build the full ICE Scoring Matrix from a list of findings or initiatives
|
|
144
|
+
*/
|
|
145
|
+
export function buildIceMatrix(items = [], { type = 'findings' } = {}) {
|
|
146
|
+
const scored = items.map((item) => (type === 'initiatives' ? scoreInitiative(item) : scoreFinding(item)));
|
|
147
|
+
scored.sort((a, b) => b.ice_score - a.ice_score || b.impact - a.impact);
|
|
148
|
+
|
|
149
|
+
const quadrants = {
|
|
150
|
+
quick_wins: scored.filter((i) => i.quadrant === 'quick_wins'),
|
|
151
|
+
strategic_bets: scored.filter((i) => i.quadrant === 'strategic_bets'),
|
|
152
|
+
low_hanging_fruit: scored.filter((i) => i.quadrant === 'low_hanging_fruit'),
|
|
153
|
+
deprioritize: scored.filter((i) => i.quadrant === 'deprioritize'),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
fact_status: 'transparent_prioritization_matrix',
|
|
158
|
+
scoring_method: 'ICE: (Impact [1-10] × Confidence [1-10]) / Effort [1-10]',
|
|
159
|
+
total_items: scored.length,
|
|
160
|
+
summary: {
|
|
161
|
+
quick_wins_count: quadrants.quick_wins.length,
|
|
162
|
+
strategic_bets_count: quadrants.strategic_bets.length,
|
|
163
|
+
low_hanging_fruit_count: quadrants.low_hanging_fruit.length,
|
|
164
|
+
deprioritize_count: quadrants.deprioritize.length,
|
|
165
|
+
},
|
|
166
|
+
quadrants,
|
|
167
|
+
ranked_items: scored,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Format ICE Matrix as human-readable terminal table / markdown
|
|
173
|
+
*/
|
|
174
|
+
export function formatIceMatrixOutput(matrix) {
|
|
175
|
+
const lines = [
|
|
176
|
+
`Impact / Effort / Confidence (ICE) Prioritization Matrix`,
|
|
177
|
+
`=======================================================`,
|
|
178
|
+
`Methodology: ${matrix.scoring_method}`,
|
|
179
|
+
`Total Items Scored: ${matrix.total_items}`,
|
|
180
|
+
``,
|
|
181
|
+
`QUADRANT SUMMARY:`,
|
|
182
|
+
` Quadrant I [Quick Wins]: ${matrix.summary.quick_wins_count} item(s) (High Impact, Low Effort, High Confidence)`,
|
|
183
|
+
` Quadrant II [Strategic Bets]: ${matrix.summary.strategic_bets_count} item(s) (High Impact, High Effort)`,
|
|
184
|
+
` Quadrant III [Low-Hanging Fruit]: ${matrix.summary.low_hanging_fruit_count} item(s) (Low Impact, Low Effort)`,
|
|
185
|
+
` Quadrant IV [Deprioritize]: ${matrix.summary.deprioritize_count} item(s) (Low Impact or Low Confidence)`,
|
|
186
|
+
``,
|
|
187
|
+
`RANKED ACTION MATRIX:`,
|
|
188
|
+
` ${'Rank'.padEnd(6)} | ${'ID'.padEnd(16)} | ${'Impact'.padEnd(8)} | ${'Effort'.padEnd(8)} | ${'Conf'.padEnd(6)} | ${'ICE Score'.padEnd(10)} | Quadrant`,
|
|
189
|
+
` ${'-'.repeat(80)}`,
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
matrix.ranked_items.forEach((item, idx) => {
|
|
193
|
+
const rank = `#${idx + 1}`.padEnd(6);
|
|
194
|
+
const id = (item.id || '').slice(0, 16).padEnd(16);
|
|
195
|
+
const impact = `${item.impact}/10`.padEnd(8);
|
|
196
|
+
const effort = `${item.effort}/10`.padEnd(8);
|
|
197
|
+
const conf = `${item.confidence}/10`.padEnd(6);
|
|
198
|
+
const score = `${item.ice_score}`.padEnd(10);
|
|
199
|
+
const q = item.quadrant.toUpperCase();
|
|
200
|
+
lines.push(` ${rank} | ${id} | ${impact} | ${effort} | ${conf} | ${score} | ${q}`);
|
|
201
|
+
lines.push(` ${item.title.slice(0, 70)}`);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return lines.join('\n');
|
|
205
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { URL } from 'node:url';
|
|
2
|
+
|
|
3
|
+
const SPAM_TLDS = new Set([
|
|
4
|
+
'top', 'work', 'gdn', 'click', 'monster', 'buzz', 'cfd', 'sbs',
|
|
5
|
+
'rest', 'cam', 'fit', 'surf', 'stream', 'tk', 'ml', 'ga', 'cf',
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
const COMMERCIAL_SPAM_KEYWORDS = /\b(casino|viagra|cialis|payday\s*loans?|replica|cheap\s*essay|free\s*download|warez|crack|hack|gambling|betting|slots|crypto\s*airdrop)\b/i;
|
|
9
|
+
|
|
10
|
+
function extractHostname(urlString) {
|
|
11
|
+
try {
|
|
12
|
+
return new URL(urlString).hostname.toLowerCase();
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function extractRootDomain(hostname) {
|
|
19
|
+
if (!hostname) return null;
|
|
20
|
+
const parts = hostname.split('.');
|
|
21
|
+
if (parts.length <= 2) return hostname;
|
|
22
|
+
return parts.slice(-2).join('.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function extractTld(hostname) {
|
|
26
|
+
if (!hostname) return null;
|
|
27
|
+
const parts = hostname.split('.');
|
|
28
|
+
return parts.at(-1) || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Audit an inbound backlink profile for authority distribution and toxic domain risks.
|
|
33
|
+
*/
|
|
34
|
+
export function auditBacklinkProfile(backlinks, { targetDomain = null } = {}) {
|
|
35
|
+
const links = Array.isArray(backlinks) ? backlinks : (backlinks?.backlinks || []);
|
|
36
|
+
const normalizedTarget = targetDomain ? targetDomain.toLowerCase().replace(/^www\./, '') : null;
|
|
37
|
+
|
|
38
|
+
const referringDomains = new Map();
|
|
39
|
+
const cClassClusters = new Map();
|
|
40
|
+
let dofollowCount = 0;
|
|
41
|
+
let nofollowCount = 0;
|
|
42
|
+
let ugcCount = 0;
|
|
43
|
+
let sponsoredCount = 0;
|
|
44
|
+
let homepageLinksCount = 0;
|
|
45
|
+
let deepLinksCount = 0;
|
|
46
|
+
|
|
47
|
+
const anchorTypes = {
|
|
48
|
+
branded: 0,
|
|
49
|
+
naked_url: 0,
|
|
50
|
+
generic: 0,
|
|
51
|
+
exact_match_commercial: 0,
|
|
52
|
+
other: 0,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const toxicDomains = [];
|
|
56
|
+
const GENERIC_ANCHORS = /^(?:click here|here|website|link|read more|learn more|source|this site|visit|more info)$/i;
|
|
57
|
+
|
|
58
|
+
// 1. Process each backlink
|
|
59
|
+
for (const link of links) {
|
|
60
|
+
const srcHost = extractHostname(link.source_url);
|
|
61
|
+
if (!srcHost) continue;
|
|
62
|
+
const rootDomain = extractRootDomain(srcHost) || srcHost;
|
|
63
|
+
const tld = extractTld(srcHost);
|
|
64
|
+
const anchor = (link.anchor_text || '').trim();
|
|
65
|
+
const rel = (link.rel || '').toLowerCase();
|
|
66
|
+
const isNofollow = rel.includes('nofollow');
|
|
67
|
+
const isUgc = rel.includes('ugc');
|
|
68
|
+
const isSponsored = rel.includes('sponsored');
|
|
69
|
+
|
|
70
|
+
if (isNofollow) nofollowCount++;
|
|
71
|
+
else if (isUgc) ugcCount++;
|
|
72
|
+
else if (isSponsored) sponsoredCount++;
|
|
73
|
+
else dofollowCount++;
|
|
74
|
+
|
|
75
|
+
// Deep link vs homepage
|
|
76
|
+
const targetPath = link.target_url ? (() => { try { return new URL(link.target_url).pathname; } catch { return '/'; } })() : '/';
|
|
77
|
+
if (targetPath === '/' || targetPath === '') homepageLinksCount++;
|
|
78
|
+
else deepLinksCount++;
|
|
79
|
+
|
|
80
|
+
// Anchor categorization
|
|
81
|
+
if (normalizedTarget && (anchor.toLowerCase().includes(normalizedTarget) || normalizedTarget.includes(anchor.toLowerCase()))) {
|
|
82
|
+
anchorTypes.branded++;
|
|
83
|
+
} else if (/^https?:\/\//i.test(anchor) || anchor.includes('www.') || anchor.includes('.com') || anchor.includes('.org')) {
|
|
84
|
+
anchorTypes.naked_url++;
|
|
85
|
+
} else if (GENERIC_ANCHORS.test(anchor)) {
|
|
86
|
+
anchorTypes.generic++;
|
|
87
|
+
} else if (COMMERCIAL_SPAM_KEYWORDS.test(anchor) || /\b(buy|best|cheap|discount|order)\b/i.test(anchor)) {
|
|
88
|
+
anchorTypes.exact_match_commercial++;
|
|
89
|
+
} else {
|
|
90
|
+
anchorTypes.other++;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// C-class IP clustering
|
|
94
|
+
if (link.source_ip) {
|
|
95
|
+
const parts = link.source_ip.split('.');
|
|
96
|
+
if (parts.length === 4) {
|
|
97
|
+
const cClass = parts.slice(0, 3).join('.');
|
|
98
|
+
const cluster = cClassClusters.get(cClass) || new Set();
|
|
99
|
+
cluster.add(rootDomain);
|
|
100
|
+
cClassClusters.set(cClass, cluster);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Domain tracking
|
|
105
|
+
const domainData = referringDomains.get(rootDomain) || {
|
|
106
|
+
domain: rootDomain,
|
|
107
|
+
sample_source_url: link.source_url,
|
|
108
|
+
total_links: 0,
|
|
109
|
+
anchors: [],
|
|
110
|
+
tld,
|
|
111
|
+
ip: link.source_ip || null,
|
|
112
|
+
dofollow: false,
|
|
113
|
+
};
|
|
114
|
+
domainData.total_links++;
|
|
115
|
+
if (!isNofollow && !isSponsored) domainData.dofollow = true;
|
|
116
|
+
if (anchor && !domainData.anchors.includes(anchor)) domainData.anchors.push(anchor);
|
|
117
|
+
referringDomains.set(rootDomain, domainData);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2. Toxic domain risk analysis per domain
|
|
121
|
+
for (const [domain, data] of referringDomains.entries()) {
|
|
122
|
+
const reasons = [];
|
|
123
|
+
let riskTier = 'clean';
|
|
124
|
+
|
|
125
|
+
// Spam TLD rule
|
|
126
|
+
if (data.tld && SPAM_TLDS.has(data.tld)) {
|
|
127
|
+
reasons.push(`Known high-abuse spam TLD (.${data.tld})`);
|
|
128
|
+
riskTier = 'high';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Spam anchor rule
|
|
132
|
+
const hasSpamAnchor = data.anchors.some((a) => COMMERCIAL_SPAM_KEYWORDS.test(a));
|
|
133
|
+
if (hasSpamAnchor) {
|
|
134
|
+
reasons.push('Anchor text matches known high-risk commercial spam patterns');
|
|
135
|
+
riskTier = 'critical';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// PBN IP clustering rule (3+ distinct domains on the exact same /24 C-block)
|
|
139
|
+
if (data.ip) {
|
|
140
|
+
const parts = data.ip.split('.');
|
|
141
|
+
if (parts.length === 4) {
|
|
142
|
+
const cClass = parts.slice(0, 3).join('.');
|
|
143
|
+
const cluster = cClassClusters.get(cClass);
|
|
144
|
+
if (cluster && cluster.size >= 3) {
|
|
145
|
+
reasons.push(`Part of hosting cluster on ${cClass}.0/24 with ${cluster.size} domains (PBN footprint)`);
|
|
146
|
+
riskTier = 'critical';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Excessive sitewide link density without nofollow
|
|
152
|
+
if (data.total_links > 50 && data.dofollow && (data.tld && SPAM_TLDS.has(data.tld))) {
|
|
153
|
+
reasons.push(`Sitewide link farm pattern (${data.total_links} links without rel="nofollow")`);
|
|
154
|
+
riskTier = 'critical';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (riskTier !== 'clean') {
|
|
158
|
+
toxicDomains.push({
|
|
159
|
+
domain,
|
|
160
|
+
risk_tier: riskTier,
|
|
161
|
+
reasons,
|
|
162
|
+
sample_url: data.sample_source_url,
|
|
163
|
+
anchors: data.anchors.slice(0, 5),
|
|
164
|
+
total_links: data.total_links,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Sort toxic domains by severity (critical first)
|
|
170
|
+
toxicDomains.sort((a, b) => (a.risk_tier === 'critical' ? -1 : 1));
|
|
171
|
+
|
|
172
|
+
const totalLinks = links.length;
|
|
173
|
+
const totalDomains = referringDomains.size;
|
|
174
|
+
const commercialAnchorPct = totalLinks > 0 ? Math.round((anchorTypes.exact_match_commercial / totalLinks) * 100) : 0;
|
|
175
|
+
const brandedAnchorPct = totalLinks > 0 ? Math.round((anchorTypes.branded / totalLinks) * 100) : 0;
|
|
176
|
+
const deepLinkPct = totalLinks > 0 ? Math.round((deepLinksCount / totalLinks) * 100) : 0;
|
|
177
|
+
|
|
178
|
+
// Authority health assessment
|
|
179
|
+
let profileHealth = 'natural';
|
|
180
|
+
if (toxicDomains.some((d) => d.risk_tier === 'critical') || commercialAnchorPct > 30) {
|
|
181
|
+
profileHealth = 'high_risk_footprint';
|
|
182
|
+
} else if (toxicDomains.length > 0 || commercialAnchorPct > 15) {
|
|
183
|
+
profileHealth = 'moderate_risk';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Generate GSC-compatible disavow lines
|
|
187
|
+
const disavowLines = [
|
|
188
|
+
`# Citable Google Disavow Candidate Export`,
|
|
189
|
+
`# Generated at: ${new Date().toISOString()}`,
|
|
190
|
+
`# Flagged domains: ${toxicDomains.length} (critical: ${toxicDomains.filter((d) => d.risk_tier === 'critical').length})`,
|
|
191
|
+
``,
|
|
192
|
+
];
|
|
193
|
+
for (const td of toxicDomains) {
|
|
194
|
+
disavowLines.push(`# Risk: ${td.risk_tier.toUpperCase()} — ${td.reasons.join('; ')}`);
|
|
195
|
+
disavowLines.push(`domain:${td.domain}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
fact_status: 'observable_risk_indicators',
|
|
200
|
+
target_domain: targetDomain,
|
|
201
|
+
profile_health: profileHealth,
|
|
202
|
+
summary: {
|
|
203
|
+
total_backlinks: totalLinks,
|
|
204
|
+
total_referring_domains: totalDomains,
|
|
205
|
+
toxic_domains_count: toxicDomains.length,
|
|
206
|
+
critical_risk_domains: toxicDomains.filter((d) => d.risk_tier === 'critical').length,
|
|
207
|
+
high_risk_domains: toxicDomains.filter((d) => d.risk_tier === 'high').length,
|
|
208
|
+
dofollow_count: dofollowCount,
|
|
209
|
+
nofollow_count: nofollowCount,
|
|
210
|
+
ugc_count: ugcCount,
|
|
211
|
+
sponsored_count: sponsoredCount,
|
|
212
|
+
deep_link_ratio_pct: deepLinkPct,
|
|
213
|
+
},
|
|
214
|
+
anchor_profile: {
|
|
215
|
+
branded_pct: brandedAnchorPct,
|
|
216
|
+
commercial_exact_match_pct: commercialAnchorPct,
|
|
217
|
+
naked_url_count: anchorTypes.naked_url,
|
|
218
|
+
generic_count: anchorTypes.generic,
|
|
219
|
+
over_optimization_risk: commercialAnchorPct > 25 ? 'elevated' : 'normal',
|
|
220
|
+
},
|
|
221
|
+
toxic_domains: toxicDomains,
|
|
222
|
+
disavow_export: disavowLines.join('\n'),
|
|
223
|
+
limitations: [
|
|
224
|
+
'Search engines do not disclose algorithmic penalty thresholds; toxic classifications reflect observable risk patterns.',
|
|
225
|
+
'Manual verification is required before submitting disavow files to Google Search Console.',
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { parse as parseHtml } from 'node-html-parser';
|
|
2
|
+
import { registryPageFor, safePath } from '../detectors/framework.js';
|
|
3
|
+
|
|
4
|
+
const QUESTION_RX = /^(?:how|what|why|when|where|who|can|is|does|which|should)\b|\?$/i;
|
|
5
|
+
const COPULA_RX = /\b(?:is|are|refers\s+to|denotes|represents|means|provides|enables)\b/i;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Evaluates a page's extraction, retrieval, and citation readiness across
|
|
9
|
+
* Perplexity, Bing Copilot, and ChatGPT / SearchGPT.
|
|
10
|
+
*/
|
|
11
|
+
export function evaluateAnswerEngineReadiness(page, ctx = {}) {
|
|
12
|
+
const html = page.html || '';
|
|
13
|
+
const root = parseHtml(html);
|
|
14
|
+
const text = (page.text || root.textContent || '').replace(/\s+/g, ' ').trim();
|
|
15
|
+
const headings = page.headings || [];
|
|
16
|
+
const paragraphs = page.paragraphs || root.querySelectorAll('p').map((p) => p.text.trim()).filter(Boolean);
|
|
17
|
+
const jsonLd = page.jsonLd || [];
|
|
18
|
+
const tablesCount = (page.tables ?? root.querySelectorAll('table').length);
|
|
19
|
+
const listsCount = (page.orderedLists ?? root.querySelectorAll('ol').length) + root.querySelectorAll('ul').length;
|
|
20
|
+
const wordCount = page.rawVisibleWordCount || page.wordCount || text.split(/\s+/).filter(Boolean).length;
|
|
21
|
+
|
|
22
|
+
// Crawler directives check (from robots Directives or headers)
|
|
23
|
+
const robotsDirectives = Array.from(page.robotsDirectives || []);
|
|
24
|
+
const isNoindex = robotsDirectives.some((d) => d.includes('noindex'));
|
|
25
|
+
|
|
26
|
+
// -----------------------------------------------------------------
|
|
27
|
+
// 1. PERPLEXITY READINESS
|
|
28
|
+
// -----------------------------------------------------------------
|
|
29
|
+
const perplexityChecks = [];
|
|
30
|
+
let perplexityScore = 0;
|
|
31
|
+
|
|
32
|
+
// Check 1: Crawler policy (PerplexityBot)
|
|
33
|
+
const perplexityBotBlocked = robotsDirectives.some((d) => d.includes('perplexitybot: noindex') || d.includes('perplexitybot: none'));
|
|
34
|
+
if (!perplexityBotBlocked && !isNoindex) {
|
|
35
|
+
perplexityScore += 25;
|
|
36
|
+
perplexityChecks.push({ name: 'Crawler Policy', passed: true, note: 'PerplexityBot allowed' });
|
|
37
|
+
} else {
|
|
38
|
+
perplexityChecks.push({ name: 'Crawler Policy', passed: false, note: 'Blocked by robots directive' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check 2: Direct Definitional Lead (Copula in first paragraph)
|
|
42
|
+
const firstParagraph = paragraphs[0] || '';
|
|
43
|
+
const hasCopularLead = COPULA_RX.test(firstParagraph.slice(0, 160));
|
|
44
|
+
if (hasCopularLead) {
|
|
45
|
+
perplexityScore += 25;
|
|
46
|
+
perplexityChecks.push({ name: 'Definitional Lead', passed: true, note: 'Concise copular definition in opening passage' });
|
|
47
|
+
} else {
|
|
48
|
+
perplexityChecks.push({ name: 'Definitional Lead', passed: false, note: 'Opening passage lacks concise definitional copula ("X is Y")' });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check 3: Structured question headings
|
|
52
|
+
const questionHeadings = headings.filter((h) => QUESTION_RX.test(h.text.trim()));
|
|
53
|
+
if (questionHeadings.length >= 2) {
|
|
54
|
+
perplexityScore += 25;
|
|
55
|
+
perplexityChecks.push({ name: 'Question Density', passed: true, note: `${questionHeadings.length} interrogative headings found` });
|
|
56
|
+
} else if (questionHeadings.length === 1) {
|
|
57
|
+
perplexityScore += 15;
|
|
58
|
+
perplexityChecks.push({ name: 'Question Density', passed: true, note: '1 interrogative heading found' });
|
|
59
|
+
} else {
|
|
60
|
+
perplexityChecks.push({ name: 'Question Density', passed: false, note: 'No interrogative headings found (Q&A format optimizes extraction)' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check 4: Structured data & tables for citation synthesis
|
|
64
|
+
if (tablesCount >= 1 || listsCount >= 2) {
|
|
65
|
+
perplexityScore += 25;
|
|
66
|
+
perplexityChecks.push({ name: 'Tabular / List Data', passed: true, note: `${tablesCount} table(s), ${listsCount} list(s) for direct citation` });
|
|
67
|
+
} else {
|
|
68
|
+
perplexityScore += 10;
|
|
69
|
+
perplexityChecks.push({ name: 'Tabular / List Data', passed: false, note: 'Limited tables or structured lists for bulleted answers' });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// -----------------------------------------------------------------
|
|
73
|
+
// 2. BING COPILOT READINESS
|
|
74
|
+
// -----------------------------------------------------------------
|
|
75
|
+
const bingChecks = [];
|
|
76
|
+
let bingScore = 0;
|
|
77
|
+
|
|
78
|
+
// Check 1: Bingbot allowed
|
|
79
|
+
const bingbotBlocked = robotsDirectives.some((d) => d.includes('bingbot: noindex'));
|
|
80
|
+
if (!bingbotBlocked && !isNoindex) {
|
|
81
|
+
bingScore += 25;
|
|
82
|
+
bingChecks.push({ name: 'Crawler Policy', passed: true, note: 'Bingbot allowed' });
|
|
83
|
+
} else {
|
|
84
|
+
bingChecks.push({ name: 'Crawler Policy', passed: false, note: 'Bingbot blocked' });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check 2: IndexNow protocol support / fast-indexing
|
|
88
|
+
const hasIndexNow = Boolean(
|
|
89
|
+
ctx.registries?.connections?.entries?.some((c) => c.provider === 'indexnow') ||
|
|
90
|
+
root.querySelector('link[rel="indexnow"]') ||
|
|
91
|
+
ctx.config?.indexnow
|
|
92
|
+
);
|
|
93
|
+
if (hasIndexNow) {
|
|
94
|
+
bingScore += 25;
|
|
95
|
+
bingChecks.push({ name: 'IndexNow Protocol', passed: true, note: 'IndexNow instant discovery integration detected' });
|
|
96
|
+
} else {
|
|
97
|
+
bingScore += 10;
|
|
98
|
+
bingChecks.push({ name: 'IndexNow Protocol', passed: false, note: 'IndexNow not configured; submission relies on standard crawl schedule' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check 3: Schema.org JSON-LD completeness
|
|
102
|
+
const validSchemaBlocks = jsonLd.flatMap((j) => j.blocks || []).filter((b) => b['@type']);
|
|
103
|
+
const schemaTypes = validSchemaBlocks.map((b) => [].concat(b['@type']).join(',')).join('; ');
|
|
104
|
+
if (validSchemaBlocks.length >= 2) {
|
|
105
|
+
bingScore += 25;
|
|
106
|
+
bingChecks.push({ name: 'Structured Data', passed: true, note: `Rich schema graphs (${schemaTypes})` });
|
|
107
|
+
} else if (validSchemaBlocks.length === 1) {
|
|
108
|
+
bingScore += 15;
|
|
109
|
+
bingChecks.push({ name: 'Structured Data', passed: true, note: `Basic schema present (${schemaTypes})` });
|
|
110
|
+
} else {
|
|
111
|
+
bingChecks.push({ name: 'Structured Data', passed: false, note: 'No valid JSON-LD schema detected; Bing Copilot cards hindered' });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check 4: Deep page internal linking and canonical clarity
|
|
115
|
+
const canonicals = page.canonicals || [];
|
|
116
|
+
if (canonicals.length === 1) {
|
|
117
|
+
bingScore += 25;
|
|
118
|
+
bingChecks.push({ name: 'Canonical Authority', passed: true, note: `Canonical unambiguously specified (${canonicals[0]})` });
|
|
119
|
+
} else {
|
|
120
|
+
bingChecks.push({ name: 'Canonical Authority', passed: false, note: canonicals.length === 0 ? 'Missing canonical URL' : 'Multiple contradictory canonical tags' });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// -----------------------------------------------------------------
|
|
124
|
+
// 3. CHATGPT / SEARCHGPT READINESS
|
|
125
|
+
// -----------------------------------------------------------------
|
|
126
|
+
const chatGptChecks = [];
|
|
127
|
+
let chatGptScore = 0;
|
|
128
|
+
|
|
129
|
+
// Check 1: OAI-SearchBot / ChatGPT-User allowed
|
|
130
|
+
const oaiBlocked = robotsDirectives.some((d) => d.includes('oai-searchbot: noindex') || d.includes('chatgpt-user: noindex'));
|
|
131
|
+
if (!oaiBlocked && !isNoindex) {
|
|
132
|
+
chatGptScore += 25;
|
|
133
|
+
chatGptScore = Math.min(100, chatGptScore);
|
|
134
|
+
chatGptChecks.push({ name: 'SearchBot Policy', passed: true, note: 'OAI-SearchBot and ChatGPT-User allowed for search retrieval' });
|
|
135
|
+
} else {
|
|
136
|
+
chatGptChecks.push({ name: 'SearchBot Policy', passed: false, note: 'OAI-SearchBot / ChatGPT-User blocked by robots directives' });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check 2: RAG Chunkability (H2/H3 headers under 350 words per chunk)
|
|
140
|
+
const estimatedChunks = Math.max(1, Math.ceil(wordCount / 300));
|
|
141
|
+
const wordsPerHeading = headings.length > 0 ? Math.round(wordCount / headings.length) : wordCount;
|
|
142
|
+
if (headings.length >= 2 && wordsPerHeading >= 80 && wordsPerHeading <= 400) {
|
|
143
|
+
chatGptScore += 25;
|
|
144
|
+
chatGptChecks.push({ name: 'RAG Chunkability', passed: true, note: `Optimal chunk sizes (${wordsPerHeading} words/heading across ${headings.length} headings)` });
|
|
145
|
+
} else if (headings.length >= 1) {
|
|
146
|
+
chatGptScore += 15;
|
|
147
|
+
chatGptChecks.push({ name: 'RAG Chunkability', passed: true, note: `Sub-optimal chunking (${wordsPerHeading} words/heading)` });
|
|
148
|
+
} else {
|
|
149
|
+
chatGptChecks.push({ name: 'RAG Chunkability', passed: false, note: 'Monolithic content without H2/H3 hierarchy risks retrieval severance' });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check 3: Direct answer lead in first 100 words of section
|
|
153
|
+
const SUMMARY_RX = /\b(?:executive\s+summary|key\s+takeaways?|summary|overview|quick\s+facts|highlights?)\b/i;
|
|
154
|
+
const hasSummary = headings.some((h) => SUMMARY_RX.test(h.text));
|
|
155
|
+
if (hasSummary || hasCopularLead) {
|
|
156
|
+
chatGptScore += 25;
|
|
157
|
+
chatGptChecks.push({ name: 'Direct Answer Synthesis', passed: true, note: hasSummary ? 'Executive summary / key takeaways section present' : 'Immediate answer thesis in opening paragraph' });
|
|
158
|
+
} else {
|
|
159
|
+
chatGptChecks.push({ name: 'Direct Answer Synthesis', passed: false, note: 'Missing executive summary or lead key takeaways section' });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check 4: Quantitative claim grounding
|
|
163
|
+
const hasQuantitativeData = /(?:\b\d+(?:\.\d+)?%|\b\d+\s*ms\b|\b\d+\s*s\b|\$\d+(?:,\d{3})*)/.test(text);
|
|
164
|
+
if (hasQuantitativeData && wordCount >= 300) {
|
|
165
|
+
chatGptScore += 25;
|
|
166
|
+
chatGptChecks.push({ name: 'Claim Grounding', passed: true, note: 'Specific numerical metrics and quantitative facts present' });
|
|
167
|
+
} else if (wordCount >= 300) {
|
|
168
|
+
chatGptScore += 15;
|
|
169
|
+
chatGptChecks.push({ name: 'Claim Grounding', passed: false, note: 'Qualitative assertions without empirical statistics' });
|
|
170
|
+
} else {
|
|
171
|
+
chatGptChecks.push({ name: 'Claim Grounding', passed: false, note: 'Thin content lacks verifiable data' });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Posture per engine
|
|
175
|
+
const posture = (s) => (s >= 80 ? 'optimal' : s >= 50 ? 'needs_optimization' : 'obstructed');
|
|
176
|
+
const perplexityStatus = posture(perplexityScore);
|
|
177
|
+
const bingStatus = posture(bingScore);
|
|
178
|
+
const chatGptStatus = posture(chatGptScore);
|
|
179
|
+
|
|
180
|
+
const compositeScore = Math.round((perplexityScore + bingScore + chatGptScore) / 3);
|
|
181
|
+
|
|
182
|
+
// Cross-engine prioritized recommendations
|
|
183
|
+
const recommendations = [];
|
|
184
|
+
if (!hasCopularLead) {
|
|
185
|
+
recommendations.push({
|
|
186
|
+
priority: 'high',
|
|
187
|
+
engine: 'all',
|
|
188
|
+
action: 'Add a concise definitional sentence ("X is Y") within the first 140 characters of the page',
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (!hasSummary) {
|
|
192
|
+
recommendations.push({
|
|
193
|
+
priority: 'medium',
|
|
194
|
+
engine: 'chatgpt',
|
|
195
|
+
action: 'Include a "Key Takeaways" or "Executive Summary" section at the top of long-form content',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (questionHeadings.length < 2) {
|
|
199
|
+
recommendations.push({
|
|
200
|
+
priority: 'medium',
|
|
201
|
+
engine: 'perplexity',
|
|
202
|
+
action: 'Rephrase section headings as explicit user questions (e.g. "How does X work?", "What are the benefits of Y?")',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (validSchemaBlocks.length < 2) {
|
|
206
|
+
recommendations.push({
|
|
207
|
+
priority: 'medium',
|
|
208
|
+
engine: 'bing_copilot',
|
|
209
|
+
action: 'Deploy structured JSON-LD schema (FAQPage, Article, or Product) to power Copilot card extraction',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (!hasIndexNow) {
|
|
213
|
+
recommendations.push({
|
|
214
|
+
priority: 'low',
|
|
215
|
+
engine: 'bing_copilot',
|
|
216
|
+
action: 'Configure IndexNow protocol to notify Bing immediately of content updates',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
url: page.url,
|
|
222
|
+
fact_status: 'deterministic_engine_readiness_audit',
|
|
223
|
+
composite_readiness_score: compositeScore,
|
|
224
|
+
engines: {
|
|
225
|
+
perplexity: {
|
|
226
|
+
score: perplexityScore,
|
|
227
|
+
status: perplexityStatus,
|
|
228
|
+
primary_bot: 'PerplexityBot',
|
|
229
|
+
checks: perplexityChecks,
|
|
230
|
+
},
|
|
231
|
+
bing_copilot: {
|
|
232
|
+
score: bingScore,
|
|
233
|
+
status: bingStatus,
|
|
234
|
+
primary_bot: 'Bingbot',
|
|
235
|
+
checks: bingChecks,
|
|
236
|
+
},
|
|
237
|
+
chatgpt: {
|
|
238
|
+
score: chatGptScore,
|
|
239
|
+
status: chatGptStatus,
|
|
240
|
+
primary_bot: 'OAI-SearchBot',
|
|
241
|
+
checks: chatGptChecks,
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
comparison_matrix: [
|
|
245
|
+
{ capability: 'Direct Definitional Lead', perplexity: hasCopularLead ? 'SUPPORTED' : 'MISSING', bing_copilot: hasCopularLead ? 'SUPPORTED' : 'OPTIONAL', chatgpt: hasCopularLead ? 'SUPPORTED' : 'MISSING' },
|
|
246
|
+
{ capability: 'Q&A Heading Structure', perplexity: questionHeadings.length >= 2 ? 'HIGH' : 'LOW', bing_copilot: questionHeadings.length >= 2 ? 'SUPPORTED' : 'OPTIONAL', chatgpt: questionHeadings.length >= 2 ? 'OPTIMAL' : 'MODERATE' },
|
|
247
|
+
{ capability: 'Structured Data (JSON-LD)', perplexity: validSchemaBlocks.length ? 'PARSED' : 'FALLBACK', bing_copilot: validSchemaBlocks.length ? 'REQUIRED_CARD' : 'OBSTRUCTED', chatgpt: validSchemaBlocks.length ? 'GROUNDED' : 'PLAIN_TEXT' },
|
|
248
|
+
{ capability: 'RAG Section Chunkability', perplexity: wordsPerHeading <= 400 ? 'OPTIMAL' : 'LONG', bing_copilot: 'STANDARD', chatgpt: wordsPerHeading <= 400 ? 'OPTIMAL' : 'SEVERED' },
|
|
249
|
+
{ capability: 'Crawler Directives', perplexity: perplexityBotBlocked ? 'BLOCKED' : 'ALLOWED', bing_copilot: bingbotBlocked ? 'BLOCKED' : 'ALLOWED', chatgpt: oaiBlocked ? 'BLOCKED' : 'ALLOWED' },
|
|
250
|
+
],
|
|
251
|
+
recommendations,
|
|
252
|
+
};
|
|
253
|
+
}
|