@automatelab/ai-seo-mcp 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/index.js +20 -1
  2. package/dist/index.js.map +1 -1
  3. package/dist/lib/agentic.d.ts +1 -1
  4. package/dist/lib/agentic.d.ts.map +1 -1
  5. package/dist/lib/agentic.js +8 -5
  6. package/dist/lib/agentic.js.map +1 -1
  7. package/dist/lib/deprecated-schema-types.json +10 -0
  8. package/dist/lib/fetch.d.ts.map +1 -1
  9. package/dist/lib/fetch.js +18 -0
  10. package/dist/lib/fetch.js.map +1 -1
  11. package/dist/lib/schema-types.json +38 -1
  12. package/dist/lib/schema.d.ts +2 -0
  13. package/dist/lib/schema.d.ts.map +1 -1
  14. package/dist/lib/schema.js +16 -4
  15. package/dist/lib/schema.js.map +1 -1
  16. package/dist/lib/score-citability.d.ts +9 -0
  17. package/dist/lib/score-citability.d.ts.map +1 -0
  18. package/dist/lib/score-citability.js +94 -0
  19. package/dist/lib/score-citability.js.map +1 -0
  20. package/dist/lib/score-content.d.ts +8 -0
  21. package/dist/lib/score-content.d.ts.map +1 -0
  22. package/dist/lib/score-content.js +52 -0
  23. package/dist/lib/score-content.js.map +1 -0
  24. package/dist/lib/score-eeat.d.ts +16 -0
  25. package/dist/lib/score-eeat.d.ts.map +1 -0
  26. package/dist/lib/score-eeat.js +149 -0
  27. package/dist/lib/score-eeat.js.map +1 -0
  28. package/dist/lib/score-evidence.d.ts +10 -0
  29. package/dist/lib/score-evidence.d.ts.map +1 -0
  30. package/dist/lib/score-evidence.js +84 -0
  31. package/dist/lib/score-evidence.js.map +1 -0
  32. package/dist/lib/score.d.ts +48 -7
  33. package/dist/lib/score.d.ts.map +1 -1
  34. package/dist/lib/score.js +46 -10
  35. package/dist/lib/score.js.map +1 -1
  36. package/dist/lib/ssrf.d.ts +6 -0
  37. package/dist/lib/ssrf.d.ts.map +1 -0
  38. package/dist/lib/ssrf.js +71 -0
  39. package/dist/lib/ssrf.js.map +1 -0
  40. package/dist/output-schemas.d.ts +212 -31
  41. package/dist/output-schemas.d.ts.map +1 -1
  42. package/dist/output-schemas.js +31 -2
  43. package/dist/output-schemas.js.map +1 -1
  44. package/dist/tools/audit-page.d.ts +6 -10
  45. package/dist/tools/audit-page.d.ts.map +1 -1
  46. package/dist/tools/audit-page.js +59 -4
  47. package/dist/tools/audit-page.js.map +1 -1
  48. package/dist/tools/check-sitemap.d.ts +11 -0
  49. package/dist/tools/check-sitemap.d.ts.map +1 -1
  50. package/dist/tools/check-sitemap.js +1 -1
  51. package/dist/tools/check-sitemap.js.map +1 -1
  52. package/dist/tools/generate-llms-txt.d.ts +1 -0
  53. package/dist/tools/generate-llms-txt.d.ts.map +1 -1
  54. package/dist/tools/generate-llms-txt.js +50 -26
  55. package/dist/tools/generate-llms-txt.js.map +1 -1
  56. package/dist/tools/generate-pricing-md.d.ts +24 -0
  57. package/dist/tools/generate-pricing-md.d.ts.map +1 -0
  58. package/dist/tools/generate-pricing-md.js +167 -0
  59. package/dist/tools/generate-pricing-md.js.map +1 -0
  60. package/dist/tools/validate-llms-txt.d.ts +4 -4
  61. package/dist/types.d.ts +9 -1
  62. package/dist/types.d.ts.map +1 -1
  63. package/package.json +1 -1
  64. package/src/lib/schema-types.json +38 -1
@@ -0,0 +1,52 @@
1
+ // AI-content / filler heuristic (#10).
2
+ //
3
+ // Generative-engine guidelines (Google QRG §4.6.5/4.6.6 on "filler") and the
4
+ // GEO toolkits flag low-information, hedging, AI-pattern prose. This is a cheap
5
+ // lexical heuristic — it flags density of known filler/hedging phrases and the
6
+ // "delve into / tapestry / ever-evolving" cluster that signals unedited LLM
7
+ // output. It is a DIAGNOSTIC (info-level), never a hard score killer.
8
+ const FILLER_PHRASES = [
9
+ "in today's world", "in today's fast-paced", "it is important to note",
10
+ "it's important to note", "it is worth noting", "it's worth noting",
11
+ "when it comes to", "at the end of the day", "needless to say",
12
+ "as we all know", "in the realm of", "in the world of", "navigating the",
13
+ "plays a crucial role", "plays a vital role", "plays a pivotal role",
14
+ "in order to", "a testament to", "the ever-evolving", "ever-changing landscape",
15
+ "unlock the", "unleash the", "delve into", "dive into the world",
16
+ "rich tapestry", "tapestry of", "game-changer", "game changer",
17
+ "in conclusion", "in summary", "last but not least", "first and foremost",
18
+ "it goes without saying", "the power of", "harness the power",
19
+ ];
20
+ export function scoreContent(bodyText, wordCount) {
21
+ const findings = [];
22
+ if (wordCount < 200) {
23
+ return { score: 100, filler_hits: 0, findings };
24
+ }
25
+ const lower = bodyText.toLowerCase();
26
+ const samples = [];
27
+ let hits = 0;
28
+ for (const p of FILLER_PHRASES) {
29
+ let idx = lower.indexOf(p);
30
+ while (idx !== -1) {
31
+ hits++;
32
+ if (samples.length < 3 && !samples.includes(p))
33
+ samples.push(p);
34
+ idx = lower.indexOf(p, idx + p.length);
35
+ }
36
+ }
37
+ // Density per 1,000 words.
38
+ const density = hits / (wordCount / 1000);
39
+ // 0 hits -> 100; ramps down as density climbs. ~6+ filler phrases / 1k words is heavy.
40
+ const score = Math.round(Math.max(0, Math.min(100, 100 - density * 10)));
41
+ if (density >= 3) {
42
+ findings.push({
43
+ severity: "info",
44
+ category: "content",
45
+ where: "page-level",
46
+ message: `Content shows AI-filler patterns (${hits} filler phrases, e.g. ${samples.map((s) => `"${s}"`).join(", ")}).`,
47
+ fix: "Cut hedging/filler phrasing and lead each section with a concrete, specific claim. Filler dilutes the extractable answer AI engines cite.",
48
+ });
49
+ }
50
+ return { score, filler_hits: hits, findings };
51
+ }
52
+ //# sourceMappingURL=score-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"score-content.js","sourceRoot":"","sources":["../../src/lib/score-content.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,EAAE;AACF,6EAA6E;AAC7E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,sEAAsE;AAItE,MAAM,cAAc,GAAG;IACrB,kBAAkB,EAAE,uBAAuB,EAAE,yBAAyB;IACtE,wBAAwB,EAAE,oBAAoB,EAAE,mBAAmB;IACnE,kBAAkB,EAAE,uBAAuB,EAAE,iBAAiB;IAC9D,gBAAgB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB;IACxE,sBAAsB,EAAE,oBAAoB,EAAE,sBAAsB;IACpE,aAAa,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,yBAAyB;IAC/E,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,qBAAqB;IAChE,eAAe,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc;IAC9D,eAAe,EAAE,YAAY,EAAE,oBAAoB,EAAE,oBAAoB;IACzE,wBAAwB,EAAE,cAAc,EAAE,mBAAmB;CAC9D,CAAC;AAQF,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,SAAiB;IAC9D,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,IAAI,SAAS,GAAG,GAAG,EAAE,CAAC;QACpB,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC;IAClD,CAAC;IAED,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IACrC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,cAAc,EAAE,CAAC;QAC/B,IAAI,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC3B,OAAO,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;YAClB,IAAI,EAAE,CAAC;YACP,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAChE,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;IAED,2BAA2B;IAC3B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAC1C,uFAAuF;IACvF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;IAEzE,IAAI,OAAO,IAAI,CAAC,EAAE,CAAC;QACjB,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,SAAS;YACnB,KAAK,EAAE,YAAY;YACnB,OAAO,EAAE,qCAAqC,IAAI,yBAAyB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YACtH,GAAG,EAAE,2IAA2I;SACjJ,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAChD,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { Finding } from "../types.js";
2
+ import type { ParsedJsonLd } from "./schema.js";
3
+ export interface EeatResult {
4
+ trust_score: number;
5
+ entity_score: number;
6
+ findings: Finding[];
7
+ }
8
+ export declare function scoreEeatEntity(args: {
9
+ html: string;
10
+ url: string;
11
+ foundTypes: string[];
12
+ jsonLdBlocks: ParsedJsonLd[];
13
+ internalLinks: string[];
14
+ externalLinks: string[];
15
+ }): EeatResult;
16
+ //# sourceMappingURL=score-eeat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"score-eeat.d.ts","sourceRoot":"","sources":["../../src/lib/score-eeat.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAShD,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,YAAY,EAAE,YAAY,EAAE,CAAC;IAC7B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB,GAAG,UAAU,CAoIb"}
@@ -0,0 +1,149 @@
1
+ // E-E-A-T trust signals, entity identity, and off-site presence (on-page proxy).
2
+ //
3
+ // Three convergent themes from the GEO toolkits we surveyed:
4
+ // - TRUST (#4): AI engines weight authorship, dates, and policy transparency.
5
+ // - ENTITY (#5): "if an AI cannot identify the publishing entity, it cannot
6
+ // cite it" — Organization/Person schema + sameAs to the knowledge graph.
7
+ // - PRESENCE (#9): off-site brand mentions. We can only PROXY this on-page
8
+ // (does the site link its own authoritative/social profiles); the real
9
+ // off-site mention scan belongs to the citation-intelligence MCP.
10
+ import * as cheerio from "cheerio";
11
+ const AUTHORITATIVE_HOSTS = [
12
+ "wikipedia.org", "wikidata.org", "linkedin.com", "twitter.com", "x.com",
13
+ "github.com", "crunchbase.com", "youtube.com", "facebook.com", "instagram.com",
14
+ ];
15
+ const POLICY_PATHS = ["/privacy", "/terms", "/contact", "/about", "/editorial", "/legal"];
16
+ export function scoreEeatEntity(args) {
17
+ const { html, url, foundTypes, jsonLdBlocks, internalLinks, externalLinks } = args;
18
+ const $ = cheerio.load(html);
19
+ const findings = [];
20
+ const isHttps = (() => {
21
+ try {
22
+ return new URL(url).protocol === "https:";
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ })();
28
+ // --- TRUST ---
29
+ const hasAuthorSchema = jsonLdBlocks.some((b) => b.types.includes("Person") ||
30
+ (b.types.some((t) => ["Article", "BlogPosting", "NewsArticle"].includes(t)) &&
31
+ typeof b.parsed["author"] === "object" && b.parsed["author"] !== null));
32
+ const hasAuthorMarkup = $('[rel="author"], [class*="author" i], [itemprop="author"]').length > 0 ||
33
+ /\bby\s+[A-Z][a-z]+\s+[A-Z][a-z]+/.test($("body").text().slice(0, 4000));
34
+ const hasAuthor = hasAuthorSchema || hasAuthorMarkup;
35
+ const hasVisibleDate = $("time[datetime]").length > 0 ||
36
+ jsonLdBlocks.some((b) => b.parsed["dateModified"] || b.parsed["datePublished"]);
37
+ const allInternal = internalLinks.map((l) => l.toLowerCase());
38
+ const policyHits = POLICY_PATHS.filter((p) => allInternal.some((l) => l.includes(p)));
39
+ const hasContact = policyHits.some((p) => p === "/contact" || p === "/about");
40
+ const hasPolicy = policyHits.some((p) => p === "/privacy" || p === "/terms" || p === "/legal");
41
+ let trust_score = 0;
42
+ if (isHttps)
43
+ trust_score += 20;
44
+ if (hasAuthor)
45
+ trust_score += 25;
46
+ if (hasVisibleDate)
47
+ trust_score += 20;
48
+ if (hasContact)
49
+ trust_score += 15;
50
+ if (hasPolicy)
51
+ trust_score += 20;
52
+ trust_score = Math.min(100, trust_score);
53
+ if (!hasAuthor) {
54
+ findings.push({
55
+ severity: "warning",
56
+ category: "trust",
57
+ where: "page-level",
58
+ message: "No visible author or author schema — AI engines can't attribute expertise.",
59
+ fix: 'Add a byline with the author\'s name and link a bio, plus Article.author as a {"@type":"Person", ...} node.',
60
+ estimated_impact: "high",
61
+ failure_signal: "Re-audit still finds no author markup or Person node.",
62
+ leading_indicator: "Presence of a named, linked author byline on content pages.",
63
+ });
64
+ }
65
+ if (!hasVisibleDate) {
66
+ findings.push({
67
+ severity: "info",
68
+ category: "trust",
69
+ where: "page-level",
70
+ message: "No visible publish/updated date.",
71
+ fix: "Show a published and last-updated date (and set datePublished/dateModified in JSON-LD).",
72
+ estimated_impact: "medium",
73
+ });
74
+ }
75
+ if (!hasPolicy || !hasContact) {
76
+ const missing = [!hasContact ? "contact/about" : null, !hasPolicy ? "privacy/terms" : null]
77
+ .filter(Boolean)
78
+ .join(" and ");
79
+ findings.push({
80
+ severity: "info",
81
+ category: "trust",
82
+ where: "page-level",
83
+ message: `Missing trust pages linked from this page: ${missing}.`,
84
+ fix: "Link a contact/about page and a privacy/terms page in the footer; these are standard trust signals for AI and raters.",
85
+ });
86
+ }
87
+ // --- ENTITY ---
88
+ const hasOrg = foundTypes.includes("Organization") || foundTypes.includes("LocalBusiness");
89
+ const hasPerson = foundTypes.includes("Person");
90
+ const sameAsCount = jsonLdBlocks.reduce((acc, b) => {
91
+ const sa = b.parsed["sameAs"];
92
+ return acc + (Array.isArray(sa) ? sa.length : sa ? 1 : 0);
93
+ }, 0);
94
+ let entity_score = 0;
95
+ if (hasOrg)
96
+ entity_score += 45;
97
+ if (hasPerson)
98
+ entity_score += 20;
99
+ entity_score += Math.min(35, sameAsCount * 12);
100
+ entity_score = Math.min(100, entity_score);
101
+ if (!hasOrg && !hasPerson) {
102
+ findings.push({
103
+ severity: "warning",
104
+ category: "entity",
105
+ where: "page-level",
106
+ message: "No Organization or Person entity in structured data — AI can't identify who publishes this.",
107
+ fix: 'Add an Organization JSON-LD node (name, url, logo, sameAs[]) and, for articles, a Person author. Entity identity is a prerequisite for being cited.',
108
+ estimated_impact: "high",
109
+ failure_signal: "Re-audit still finds no Organization/Person node.",
110
+ leading_indicator: "Whether ChatGPT/Perplexity correctly name the brand when asked (track in citation-intelligence).",
111
+ });
112
+ }
113
+ else if (hasOrg && sameAsCount === 0) {
114
+ findings.push({
115
+ severity: "info",
116
+ category: "entity",
117
+ where: "Organization.sameAs",
118
+ message: "Entity has no sameAs links to the knowledge graph.",
119
+ fix: 'Add sameAs: ["https://www.wikidata.org/...", "https://www.linkedin.com/company/...", official profiles] so engines can disambiguate the entity.',
120
+ estimated_impact: "medium",
121
+ });
122
+ }
123
+ // --- PRESENCE (on-page proxy only) ---
124
+ const offSiteHosts = new Set();
125
+ for (const href of externalLinks) {
126
+ try {
127
+ const h = new URL(href).hostname.replace(/^www\./, "");
128
+ if (AUTHORITATIVE_HOSTS.some((a) => h.endsWith(a)))
129
+ offSiteHosts.add(h);
130
+ }
131
+ catch { /* skip */ }
132
+ }
133
+ const sameAsAuthoritative = jsonLdBlocks.some((b) => {
134
+ const sa = b.parsed["sameAs"];
135
+ const list = Array.isArray(sa) ? sa : sa ? [sa] : [];
136
+ return list.some((u) => typeof u === "string" && AUTHORITATIVE_HOSTS.some((a) => u.includes(a)));
137
+ });
138
+ if (offSiteHosts.size === 0 && !sameAsAuthoritative) {
139
+ findings.push({
140
+ severity: "info",
141
+ category: "presence",
142
+ where: "page-level",
143
+ message: "No links to authoritative/social profiles (Wikipedia, LinkedIn, GitHub, etc.).",
144
+ fix: "Link your verified profiles and any third-party coverage. NOTE: a true off-site brand-mention scan (Reddit/YouTube/Wikipedia citations) lives in the citation-intelligence MCP.",
145
+ });
146
+ }
147
+ return { trust_score, entity_score, findings };
148
+ }
149
+ //# sourceMappingURL=score-eeat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"score-eeat.js","sourceRoot":"","sources":["../../src/lib/score-eeat.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { Finding } from "../types.js";
2
+ export interface EvidenceResult {
3
+ score: number;
4
+ citations: number;
5
+ statistics: number;
6
+ quotes: number;
7
+ findings: Finding[];
8
+ }
9
+ export declare function scoreEvidence(html: string, bodyText: string, externalLinks: string[], wordCount: number): EvidenceResult;
10
+ //# sourceMappingURL=score-evidence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"score-evidence.d.ts","sourceRoot":"","sources":["../../src/lib/score-evidence.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAgBD,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EAAE,EACvB,SAAS,EAAE,MAAM,GAChB,cAAc,CAsEhB"}
@@ -0,0 +1,84 @@
1
+ // Evidence signals (Princeton GEO weighting).
2
+ //
3
+ // The Princeton "GEO" study measured which content changes lift visibility in
4
+ // generative engines: adding CITATIONS to sources (+40%), STATISTICS with
5
+ // numbers (+37%), and EXPERT QUOTATIONS (+30%) were the biggest movers;
6
+ // keyword stuffing was negative. This module counts those three evidence types
7
+ // and emits Princeton-weighted findings when they're absent.
8
+ import * as cheerio from "cheerio";
9
+ // Numbers that carry meaning: percentages, money, multipliers, magnitudes, or a
10
+ // number followed by a unit-ish word. Bare years/list indices are ignored.
11
+ const STAT_RE = /(\b\d[\d,]*(\.\d+)?\s?(%|percent|x\b|×|million|billion|trillion|k\b|bps)\b)|(\$\s?\d[\d,]*(\.\d+)?)|(\b\d[\d,]*(\.\d+)?\s?(users|customers|companies|developers|hours|days|minutes|seconds|requests|queries|tokens|words|pages|sites|points|words\/min)\b)/gi;
12
+ function countStatistics(text) {
13
+ const m = text.match(STAT_RE);
14
+ return m ? m.length : 0;
15
+ }
16
+ // Quotation: a 8+ word span inside double quotes, OR an explicit attribution verb.
17
+ const QUOTE_SPAN_RE = /["“][^"”]{40,}["”]/g;
18
+ const ATTRIBUTION_RE = /\b(said|says|according to|told|notes|argues|explains|writes)\b/gi;
19
+ export function scoreEvidence(html, bodyText, externalLinks, wordCount) {
20
+ const $ = cheerio.load(html);
21
+ $("nav, header, footer, aside, .nav, .menu").remove();
22
+ // Citations: distinct external link hosts (a page that links the same domain
23
+ // 20 times is not 20 citations).
24
+ const hosts = new Set();
25
+ for (const href of externalLinks) {
26
+ try {
27
+ hosts.add(new URL(href).hostname.replace(/^www\./, ""));
28
+ }
29
+ catch {
30
+ /* skip */
31
+ }
32
+ }
33
+ const citations = hosts.size;
34
+ const statistics = countStatistics(bodyText);
35
+ const blockquotes = $("blockquote").length;
36
+ const quoteSpans = (bodyText.match(QUOTE_SPAN_RE) ?? []).length;
37
+ const attributions = (bodyText.match(ATTRIBUTION_RE) ?? []).length;
38
+ const quotes = blockquotes + Math.min(quoteSpans, attributions);
39
+ const findings = [];
40
+ const isSubstantial = wordCount >= 300;
41
+ // Princeton ordering: citations (+40%) > statistics (+37%) > quotes (+30%).
42
+ if (isSubstantial && citations === 0) {
43
+ findings.push({
44
+ severity: "warning",
45
+ category: "evidence",
46
+ where: "page-level",
47
+ message: "No outbound citations to authoritative sources.",
48
+ fix: "Cite 2-3 primary sources (studies, docs, official stats) with outbound links. Adding citations is the single biggest measured GEO lever (~+40% visibility).",
49
+ estimated_impact: "high",
50
+ failure_signal: "Re-audit still finds zero distinct external citation hosts.",
51
+ leading_indicator: "Count of distinct authoritative domains linked in the body.",
52
+ });
53
+ }
54
+ if (isSubstantial && statistics === 0) {
55
+ findings.push({
56
+ severity: "warning",
57
+ category: "evidence",
58
+ where: "page-level",
59
+ message: "No statistics or quantified claims found.",
60
+ fix: "Add concrete numbers with units/percentages and cite their source. Statistics-with-sources measured ~+37% GEO visibility.",
61
+ estimated_impact: "high",
62
+ leading_indicator: "Number of sourced statistics per 1,000 words.",
63
+ });
64
+ }
65
+ if (isSubstantial && quotes === 0) {
66
+ findings.push({
67
+ severity: "info",
68
+ category: "evidence",
69
+ where: "page-level",
70
+ message: "No expert quotations or attributed statements found.",
71
+ fix: "Add 1-2 attributed expert quotes (with name + source). Expert quotations measured ~+30% GEO visibility.",
72
+ estimated_impact: "medium",
73
+ });
74
+ }
75
+ // Score: weighted presence of the three evidence types, scaled by document
76
+ // length so a 100-word stub isn't punished as hard as a 2,000-word article.
77
+ const citeScore = Math.min(1, citations / 3) * 40;
78
+ const statScore = Math.min(1, statistics / 4) * 37;
79
+ const quoteScore = Math.min(1, quotes / 2) * 23;
80
+ const raw = citeScore + statScore + quoteScore; // max 100
81
+ const score = isSubstantial ? Math.round(raw) : Math.round(Math.max(raw, 50));
82
+ return { score, citations, statistics, quotes, findings };
83
+ }
84
+ //# sourceMappingURL=score-evidence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"score-evidence.js","sourceRoot":"","sources":["../../src/lib/score-evidence.ts"],"names":[],"mappings":"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"}
@@ -2,12 +2,7 @@ import type { Finding } from "../types.js";
2
2
  export type Grade = "A" | "B" | "C" | "D" | "F";
3
3
  /** Derive a letter grade from a 0-100 numeric score. */
4
4
  export declare function deriveGrade(score: number): Grade;
5
- /**
6
- * Weighted dimension score for audit_page.
7
- * Weights: schema 25%, technical 20%, structure 20%, robots 10%,
8
- * freshness 10%, authority 10%, entity_density 3%, sitemap 2%.
9
- */
10
- export declare function computeWeightedScore(dimensions: {
5
+ export interface DimensionScores {
11
6
  schema: number;
12
7
  technical: number;
13
8
  structure: number;
@@ -16,7 +11,53 @@ export declare function computeWeightedScore(dimensions: {
16
11
  authority: number;
17
12
  entity_density: number;
18
13
  sitemap: number;
19
- }): number;
14
+ citability: number;
15
+ evidence: number;
16
+ trust: number;
17
+ }
18
+ /**
19
+ * Weighted dimension score for audit_page. Weights re-balanced for AI citation:
20
+ * schema 18%, citability 15%, evidence 12%, structure 12%, technical 12%,
21
+ * trust 8%, authority 7%, robots 7%, freshness 5%, entity_density 2%, sitemap 2%.
22
+ * (Citation-content dimensions now dominate, per converging GEO research.)
23
+ */
24
+ export declare function computeWeightedScore(dimensions: DimensionScores): number;
25
+ /**
26
+ * Veto caps: certain hard blockers make the whole page effectively uncitable,
27
+ * no matter how well the other dimensions score. When any veto is present the
28
+ * composite is capped (default 60 = grade C ceiling). Returns the (possibly
29
+ * capped) score plus the human-readable reasons that triggered the cap.
30
+ */
31
+ export declare function applyVetoCaps(score: number, vetoes: string[], ceiling?: number): {
32
+ score: number;
33
+ capped: boolean;
34
+ reasons: string[];
35
+ };
36
+ export type PlatformLabel = "ready" | "partial" | "weak";
37
+ export interface PlatformReadiness {
38
+ chatgpt: {
39
+ score: number;
40
+ label: PlatformLabel;
41
+ };
42
+ perplexity: {
43
+ score: number;
44
+ label: PlatformLabel;
45
+ };
46
+ google_ai_overview: {
47
+ score: number;
48
+ label: PlatformLabel;
49
+ };
50
+ gemini: {
51
+ score: number;
52
+ label: PlatformLabel;
53
+ };
54
+ }
55
+ /**
56
+ * Derive per-engine readiness from the dimension scores. Different engines
57
+ * reward different signals (only ~11% of domains are cited by both ChatGPT and
58
+ * Google AI Overviews), so a single composite hides real gaps.
59
+ */
60
+ export declare function platformReadiness(d: DimensionScores): PlatformReadiness;
20
61
  /**
21
62
  * Compute a simple score from a findings list.
22
63
  * Starts at 100, deducts per finding severity.
@@ -1 +1 @@
1
- {"version":3,"file":"score.d.ts","sourceRoot":"","sources":["../../src/lib/score.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,KAAK,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAEhD,wDAAwD;AACxD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAMhD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,MAAM,CAWT;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,EAAE,EACnB,SAAS,SAAM,EACf,OAAO;;;;CAAwC,GAC9C,MAAM,CAQR;AAED,+GAA+G;AAC/G,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAUlE"}
1
+ {"version":3,"file":"score.d.ts","sourceRoot":"","sources":["../../src/lib/score.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,KAAK,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAEhD,wDAAwD;AACxD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAMhD;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,eAAe,GAAG,MAAM,CAcxE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,SAAK,GACX;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAGvD;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEzD,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,CAAA;KAAE,CAAC;IACjD,UAAU,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,CAAA;KAAE,CAAC;IACpD,kBAAkB,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,CAAA;KAAE,CAAC;IAC5D,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,CAAA;KAAE,CAAC;CACjD;AAQD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,eAAe,GAAG,iBAAiB,CAWvE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,EAAE,EACnB,SAAS,SAAM,EACf,OAAO;;;;CAAwC,GAC9C,MAAM,CAQR;AAED,+GAA+G;AAC/G,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAUlE"}
package/dist/lib/score.js CHANGED
@@ -12,21 +12,57 @@ export function deriveGrade(score) {
12
12
  return "F";
13
13
  }
14
14
  /**
15
- * Weighted dimension score for audit_page.
16
- * Weights: schema 25%, technical 20%, structure 20%, robots 10%,
17
- * freshness 10%, authority 10%, entity_density 3%, sitemap 2%.
15
+ * Weighted dimension score for audit_page. Weights re-balanced for AI citation:
16
+ * schema 18%, citability 15%, evidence 12%, structure 12%, technical 12%,
17
+ * trust 8%, authority 7%, robots 7%, freshness 5%, entity_density 2%, sitemap 2%.
18
+ * (Citation-content dimensions now dominate, per converging GEO research.)
18
19
  */
19
20
  export function computeWeightedScore(dimensions) {
20
- const score = dimensions.schema * 0.25 +
21
- dimensions.technical * 0.2 +
22
- dimensions.structure * 0.2 +
23
- dimensions.robots * 0.1 +
24
- dimensions.freshness * 0.1 +
25
- dimensions.authority * 0.1 +
26
- dimensions.entity_density * 0.03 +
21
+ const score = dimensions.schema * 0.18 +
22
+ dimensions.citability * 0.15 +
23
+ dimensions.evidence * 0.12 +
24
+ dimensions.structure * 0.12 +
25
+ dimensions.technical * 0.12 +
26
+ dimensions.trust * 0.08 +
27
+ dimensions.authority * 0.07 +
28
+ dimensions.robots * 0.07 +
29
+ dimensions.freshness * 0.05 +
30
+ dimensions.entity_density * 0.02 +
27
31
  dimensions.sitemap * 0.02;
28
32
  return Math.round(Math.max(0, Math.min(100, score)));
29
33
  }
34
+ /**
35
+ * Veto caps: certain hard blockers make the whole page effectively uncitable,
36
+ * no matter how well the other dimensions score. When any veto is present the
37
+ * composite is capped (default 60 = grade C ceiling). Returns the (possibly
38
+ * capped) score plus the human-readable reasons that triggered the cap.
39
+ */
40
+ export function applyVetoCaps(score, vetoes, ceiling = 60) {
41
+ if (vetoes.length === 0)
42
+ return { score, capped: false, reasons: [] };
43
+ return { score: Math.min(score, ceiling), capped: score > ceiling, reasons: vetoes };
44
+ }
45
+ function label(score) {
46
+ return score >= 75 ? "ready" : score >= 50 ? "partial" : "weak";
47
+ }
48
+ const avg = (...xs) => Math.round(xs.reduce((a, b) => a + b, 0) / xs.length);
49
+ /**
50
+ * Derive per-engine readiness from the dimension scores. Different engines
51
+ * reward different signals (only ~11% of domains are cited by both ChatGPT and
52
+ * Google AI Overviews), so a single composite hides real gaps.
53
+ */
54
+ export function platformReadiness(d) {
55
+ const chatgpt = avg(d.citability, d.evidence, d.robots, d.structure);
56
+ const perplexity = avg(d.citability, d.evidence, d.freshness, d.structure);
57
+ const google_ai_overview = avg(d.schema, d.structure, d.technical, d.freshness);
58
+ const gemini = avg(d.schema, d.entity_density, d.robots, d.authority);
59
+ return {
60
+ chatgpt: { score: chatgpt, label: label(chatgpt) },
61
+ perplexity: { score: perplexity, label: label(perplexity) },
62
+ google_ai_overview: { score: google_ai_overview, label: label(google_ai_overview) },
63
+ gemini: { score: gemini, label: label(gemini) },
64
+ };
65
+ }
30
66
  /**
31
67
  * Compute a simple score from a findings list.
32
68
  * Starts at 100, deducts per finding severity.
@@ -1 +1 @@
1
- {"version":3,"file":"score.js","sourceRoot":"","sources":["../../src/lib/score.ts"],"names":[],"mappings":"AAAA,qDAAqD;AAMrD,wDAAwD;AACxD,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,KAAK,IAAI,EAAE;QAAE,OAAO,GAAG,CAAC;IAC5B,IAAI,KAAK,IAAI,EAAE;QAAE,OAAO,GAAG,CAAC;IAC5B,IAAI,KAAK,IAAI,EAAE;QAAE,OAAO,GAAG,CAAC;IAC5B,IAAI,KAAK,IAAI,EAAE;QAAE,OAAO,GAAG,CAAC;IAC5B,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,UASpC;IACC,MAAM,KAAK,GACT,UAAU,CAAC,MAAM,GAAG,IAAI;QACxB,UAAU,CAAC,SAAS,GAAG,GAAG;QAC1B,UAAU,CAAC,SAAS,GAAG,GAAG;QAC1B,UAAU,CAAC,MAAM,GAAG,GAAG;QACvB,UAAU,CAAC,SAAS,GAAG,GAAG;QAC1B,UAAU,CAAC,SAAS,GAAG,GAAG;QAC1B,UAAU,CAAC,cAAc,GAAG,IAAI;QAChC,UAAU,CAAC,OAAO,GAAG,IAAI,CAAC;IAC5B,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;AACvD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAAmB,EACnB,SAAS,GAAG,GAAG,EACf,OAAO,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE;IAE/C,IAAI,KAAK,GAAG,SAAS,CAAC;IACtB,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,IAAI,CAAC,CAAC,QAAQ,KAAK,UAAU;YAAE,KAAK,IAAI,OAAO,CAAC,QAAQ,CAAC;aACpD,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS;YAAE,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC;;YACvD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAC7B,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,+GAA+G;AAC/G,MAAM,UAAU,cAAc,CAAC,YAA2B;IACxD,IAAI,CAAC,YAAY;QAAE,OAAO,EAAE,CAAC,CAAC,qCAAqC;IACnE,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,KAAK,GAAG,GAAG,GAAG,QAAQ,CAAC;IAC7B,MAAM,OAAO,GAAG,KAAK,GAAG,CAAC,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;IAC9C,IAAI,OAAO,IAAI,EAAE;QAAE,OAAO,GAAG,CAAC;IAC9B,IAAI,OAAO,IAAI,GAAG;QAAE,OAAO,CAAC,CAAC;IAC7B,oDAAoD;IACpD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;AAC7D,CAAC"}
1
+ {"version":3,"file":"score.js","sourceRoot":"","sources":["../../src/lib/score.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Returns a human-readable reason if the host must be blocked, else null.
3
+ * Resolves hostnames so DNS-rebinding to a private IP is also caught.
4
+ */
5
+ export declare function ssrfBlockReason(hostname: string): Promise<string | null>;
6
+ //# sourceMappingURL=ssrf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssrf.d.ts","sourceRoot":"","sources":["../../src/lib/ssrf.ts"],"names":[],"mappings":"AAmCA;;;GAGG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoB9E"}
@@ -0,0 +1,71 @@
1
+ // SSRF / DNS-rebinding guard.
2
+ //
3
+ // This server fetches arbitrary user-supplied URLs. Without a guard it is an
4
+ // SSRF vector: a caller could point it at 127.0.0.1, 169.254.169.254 (cloud
5
+ // metadata), or a hostname that RESOLVES to a private IP (DNS rebinding). We
6
+ // block private/loopback/link-local/reserved address space, syntactically for
7
+ // IP literals and after DNS resolution for hostnames.
8
+ import net from "node:net";
9
+ import { lookup } from "node:dns/promises";
10
+ function isPrivateIp(ip) {
11
+ if (net.isIPv4(ip)) {
12
+ const parts = ip.split(".").map(Number);
13
+ const [a, b] = parts;
14
+ if (a === 0 || a === 10 || a === 127)
15
+ return true;
16
+ if (a === 169 && b === 254)
17
+ return true; // link-local + cloud metadata
18
+ if (a === 172 && b >= 16 && b <= 31)
19
+ return true; // private
20
+ if (a === 192 && b === 168)
21
+ return true; // private
22
+ if (a === 100 && b >= 64 && b <= 127)
23
+ return true; // CGNAT
24
+ if (a >= 224)
25
+ return true; // multicast/reserved
26
+ return false;
27
+ }
28
+ if (net.isIPv6(ip)) {
29
+ const low = ip.toLowerCase();
30
+ if (low === "::1" || low === "::")
31
+ return true;
32
+ if (low.startsWith("fe80"))
33
+ return true; // link-local
34
+ if (low.startsWith("fc") || low.startsWith("fd"))
35
+ return true; // unique-local
36
+ const mapped = low.match(/::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/);
37
+ if (mapped)
38
+ return isPrivateIp(mapped[1]);
39
+ return false;
40
+ }
41
+ return false;
42
+ }
43
+ /**
44
+ * Returns a human-readable reason if the host must be blocked, else null.
45
+ * Resolves hostnames so DNS-rebinding to a private IP is also caught.
46
+ */
47
+ export async function ssrfBlockReason(hostname) {
48
+ const h = hostname.toLowerCase().replace(/\.$/, "");
49
+ if (!h)
50
+ return "empty host";
51
+ if (h === "localhost" || h.endsWith(".localhost") || h.endsWith(".internal") || h.endsWith(".local")) {
52
+ return `host "${hostname}" is internal`;
53
+ }
54
+ if (net.isIP(h)) {
55
+ return isPrivateIp(h) ? `IP ${hostname} is in private/reserved space` : null;
56
+ }
57
+ try {
58
+ const addrs = await lookup(h, { all: true });
59
+ for (const a of addrs) {
60
+ if (isPrivateIp(a.address))
61
+ return `host ${hostname} resolves to private IP ${a.address}`;
62
+ }
63
+ }
64
+ catch {
65
+ // Resolution failure — let the actual fetch fail naturally rather than
66
+ // blocking a host we simply couldn't resolve here.
67
+ return null;
68
+ }
69
+ return null;
70
+ }
71
+ //# sourceMappingURL=ssrf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssrf.js","sourceRoot":"","sources":["../../src/lib/ssrf.ts"],"names":[],"mappings":"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"}