geo-checker 0.3.1 → 0.3.2

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/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { CheerioAPI } from 'cheerio';
2
2
 
3
3
  type Status = 'pass' | 'warn' | 'fail' | 'skip';
4
- type Category = 'crawler' | 'structured-data' | 'citation' | 'content';
4
+ type Category = 'crawler' | 'structured-data' | 'citation' | 'content' | 'aeo';
5
5
  interface RobotsRuleGroup {
6
6
  userAgent: string;
7
7
  allow: string[];
@@ -46,6 +46,8 @@ interface AuditContext {
46
46
  renderMode: 'static' | 'rendered';
47
47
  fetchedAt: string;
48
48
  warnings: string[];
49
+ skillMd: string | null;
50
+ agentPermissions: unknown | null;
49
51
  }
50
52
  type Impact = 'critical' | 'high' | 'medium' | 'low';
51
53
  type Effort = 'low' | 'medium' | 'high';
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { CheerioAPI } from 'cheerio';
2
2
 
3
3
  type Status = 'pass' | 'warn' | 'fail' | 'skip';
4
- type Category = 'crawler' | 'structured-data' | 'citation' | 'content';
4
+ type Category = 'crawler' | 'structured-data' | 'citation' | 'content' | 'aeo';
5
5
  interface RobotsRuleGroup {
6
6
  userAgent: string;
7
7
  allow: string[];
@@ -46,6 +46,8 @@ interface AuditContext {
46
46
  renderMode: 'static' | 'rendered';
47
47
  fetchedAt: string;
48
48
  warnings: string[];
49
+ skillMd: string | null;
50
+ agentPermissions: unknown | null;
49
51
  }
50
52
  type Impact = 'critical' | 'high' | 'medium' | 'low';
51
53
  type Effort = 'low' | 'medium' | 'high';
package/dist/index.js CHANGED
@@ -255,10 +255,12 @@ async function buildContext(url, opts = {}) {
255
255
  "Site appears to be JS-rendered (sparse body + SPA root element). Re-run with --render for accurate results."
256
256
  );
257
257
  }
258
- const [robotsRaw, llmsRaw, llmsFullRaw] = await Promise.all([
258
+ const [robotsRaw, llmsRaw, llmsFullRaw, skillMdRaw, agentPermissionsRaw] = await Promise.all([
259
259
  fetchText(`${origin}/robots.txt`, opts),
260
260
  fetchText(`${origin}/llms.txt`, opts),
261
- fetchText(`${origin}/llms-full.txt`, opts)
261
+ fetchText(`${origin}/llms-full.txt`, opts),
262
+ fetchText(`${origin}/skill.md`, opts),
263
+ fetchText(`${origin}/agent-permissions.json`, opts)
262
264
  ]);
263
265
  let sitemapUrl = null;
264
266
  const robots = robotsRaw ? parseRobots(robotsRaw) : null;
@@ -266,6 +268,13 @@ async function buildContext(url, opts = {}) {
266
268
  if (!sitemapUrl) sitemapUrl = `${origin}/sitemap.xml`;
267
269
  const sitemapRaw = await fetchText(sitemapUrl, opts);
268
270
  const sitemap = sitemapRaw ? parseSitemap(sitemapRaw) : null;
271
+ let agentPermissions = null;
272
+ if (agentPermissionsRaw && agentPermissionsRaw.trim().length > 0) {
273
+ try {
274
+ agentPermissions = JSON.parse(agentPermissionsRaw);
275
+ } catch {
276
+ }
277
+ }
269
278
  return {
270
279
  url,
271
280
  finalUrl,
@@ -280,7 +289,9 @@ async function buildContext(url, opts = {}) {
280
289
  jsonLd: extractJsonLd($),
281
290
  renderMode,
282
291
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
283
- warnings
292
+ warnings,
293
+ skillMd: skillMdRaw && skillMdRaw.trim().length > 0 ? skillMdRaw : null,
294
+ agentPermissions
284
295
  };
285
296
  }
286
297
 
@@ -289,10 +300,11 @@ function defineRule(rule) {
289
300
  return rule;
290
301
  }
291
302
  var CATEGORY_WEIGHTS = {
292
- crawler: 25,
293
- "structured-data": 30,
294
- citation: 25,
295
- content: 20
303
+ crawler: 20,
304
+ "structured-data": 25,
305
+ citation: 20,
306
+ content: 15,
307
+ aeo: 20
296
308
  };
297
309
 
298
310
  // src/engine.ts
@@ -306,7 +318,8 @@ async function runRules(ctx, rules, opts = {}) {
306
318
  crawler: { score: 0, weight: weights.crawler, results: [] },
307
319
  "structured-data": { score: 0, weight: weights["structured-data"], results: [] },
308
320
  citation: { score: 0, weight: weights.citation, results: [] },
309
- content: { score: 0, weight: weights.content, results: [] }
321
+ content: { score: 0, weight: weights.content, results: [] },
322
+ aeo: { score: 0, weight: weights.aeo, results: [] }
310
323
  };
311
324
  for (const rule of rules) {
312
325
  if (onlySet && !onlySet.has(rule.id) && (!rule.stableId || !onlySet.has(rule.stableId))) continue;
@@ -1896,12 +1909,130 @@ var contentRules = [
1896
1909
  externalCitationsRule
1897
1910
  ];
1898
1911
 
1912
+ // src/rules/aeo/skill-md.ts
1913
+ var aeoSkillMdRule = defineRule({
1914
+ id: "aeo.skill-md",
1915
+ stableId: "aeo.skill-md",
1916
+ category: "aeo",
1917
+ group: "opportunity",
1918
+ weight: 3,
1919
+ impact: "high",
1920
+ effort: "low",
1921
+ docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeoskill-md",
1922
+ title: "skill.md is present",
1923
+ title_ko: "skill.md \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
1924
+ description: "A /skill.md file describes site capabilities so AI agents know what this site can do for them.",
1925
+ run(ctx) {
1926
+ if (ctx.skillMd !== null) {
1927
+ return {
1928
+ status: "pass",
1929
+ score: 1,
1930
+ rationale: "skill.md found at site root.",
1931
+ rationale_ko: "skill.md\uAC00 \uC0AC\uC774\uD2B8 \uB8E8\uD2B8\uC5D0 \uC874\uC7AC\uD569\uB2C8\uB2E4."
1932
+ };
1933
+ }
1934
+ return {
1935
+ status: "warn",
1936
+ score: 0,
1937
+ rationale: "No /skill.md found. Add one to describe your site capabilities to AI agents.",
1938
+ rationale_ko: "/skill.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. AI \uC5D0\uC774\uC804\uD2B8\uAC00 \uC0AC\uC774\uD2B8 \uAE30\uB2A5\uC744 \uD30C\uC545\uD560 \uC218 \uC788\uB3C4\uB85D \uCD94\uAC00\uD558\uC138\uC694.",
1939
+ fixHint: "Create /skill.md listing what services, products, and capabilities this site offers."
1940
+ };
1941
+ }
1942
+ });
1943
+
1944
+ // src/rules/aeo/agent-permissions.ts
1945
+ var aeoAgentPermissionsRule = defineRule({
1946
+ id: "aeo.agent-permissions",
1947
+ stableId: "aeo.agent-permissions",
1948
+ category: "aeo",
1949
+ group: "opportunity",
1950
+ weight: 3,
1951
+ impact: "medium",
1952
+ effort: "low",
1953
+ docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeoagent-permissions",
1954
+ title: "agent-permissions.json is present",
1955
+ title_ko: "agent-permissions.json \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
1956
+ description: "Declares explicit read/summarize/cite/train permissions for AI agents.",
1957
+ run(ctx) {
1958
+ if (ctx.agentPermissions !== null) {
1959
+ return {
1960
+ status: "pass",
1961
+ score: 1,
1962
+ rationale: "agent-permissions.json found at site root.",
1963
+ rationale_ko: "agent-permissions.json\uC774 \uC0AC\uC774\uD2B8 \uB8E8\uD2B8\uC5D0 \uC874\uC7AC\uD569\uB2C8\uB2E4.",
1964
+ evidence: ctx.agentPermissions
1965
+ };
1966
+ }
1967
+ return {
1968
+ status: "warn",
1969
+ score: 0,
1970
+ rationale: "No /agent-permissions.json found. Add one to declare AI agent access policy.",
1971
+ rationale_ko: "/agent-permissions.json\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. AI \uC5D0\uC774\uC804\uD2B8 \uC811\uADFC \uC815\uCC45\uC744 \uBA85\uC2DC\uD558\uB824\uBA74 \uCD94\uAC00\uD558\uC138\uC694.",
1972
+ fixHint: "Create /agent-permissions.json with read, summarize, cite, and train permission flags."
1973
+ };
1974
+ }
1975
+ });
1976
+
1977
+ // src/rules/aeo/token-length.ts
1978
+ var THRESHOLD_OPTIMAL = 15e3;
1979
+ var THRESHOLD_MAX = 25e3;
1980
+ var aeoTokenLengthRule = defineRule({
1981
+ id: "aeo.token-length",
1982
+ stableId: "aeo.token-length",
1983
+ category: "aeo",
1984
+ group: "diagnostic",
1985
+ weight: 4,
1986
+ impact: "medium",
1987
+ effort: "medium",
1988
+ docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeotoken-length",
1989
+ title: "Content token length within AI agent limits",
1990
+ title_ko: "\uCF58\uD150\uCE20 \uD1A0\uD070 \uC218 AI \uC5D0\uC774\uC804\uD2B8 \uAD8C\uC7A5 \uBC94\uC704",
1991
+ description: "Pages under 15K tokens are optimal for AI agents (per Addy Osmani's AEO guidance).",
1992
+ run(ctx) {
1993
+ const text = ctx.$("body").text();
1994
+ const tokenEstimate = Math.round(text.length / 3);
1995
+ const evidence = { tokenEstimate, thresholds: { optimal: THRESHOLD_OPTIMAL, max: THRESHOLD_MAX } };
1996
+ if (tokenEstimate <= THRESHOLD_OPTIMAL) {
1997
+ return {
1998
+ status: "pass",
1999
+ score: 1,
2000
+ rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 within optimal range.`,
2001
+ rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 \uAD8C\uC7A5 \uBC94\uC704(15K) \uC774\uB0B4\uC785\uB2C8\uB2E4.`,
2002
+ evidence
2003
+ };
2004
+ }
2005
+ if (tokenEstimate <= THRESHOLD_MAX) {
2006
+ return {
2007
+ status: "warn",
2008
+ score: 0.5,
2009
+ rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 exceeds 15K recommendation.`,
2010
+ rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 15K \uAD8C\uC7A5\uCE58\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4.`,
2011
+ fixHint: "Consider splitting content into shorter, focused pages.",
2012
+ evidence
2013
+ };
2014
+ }
2015
+ return {
2016
+ status: "fail",
2017
+ score: 0,
2018
+ rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 exceeds 25K agent processing limit.`,
2019
+ rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 AI \uC5D0\uC774\uC804\uD2B8 \uCC98\uB9AC \uD55C\uACC4(25K)\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4.`,
2020
+ fixHint: "Split this page into multiple focused pages under 15K tokens.",
2021
+ evidence
2022
+ };
2023
+ }
2024
+ });
2025
+
2026
+ // src/rules/aeo/index.ts
2027
+ var aeoRules = [aeoSkillMdRule, aeoAgentPermissionsRule, aeoTokenLengthRule];
2028
+
1899
2029
  // src/rules/index.ts
1900
2030
  var defaultRules = [
1901
2031
  ...crawlerRules,
1902
2032
  ...structuredDataRules,
1903
2033
  ...citationRules,
1904
- ...contentRules
2034
+ ...contentRules,
2035
+ ...aeoRules
1905
2036
  ];
1906
2037
 
1907
2038
  // src/config.ts
@@ -2097,7 +2228,8 @@ var CATEGORY_LABELS = {
2097
2228
  crawler: "AI Crawler Access",
2098
2229
  "structured-data": "Structured Data",
2099
2230
  citation: "Citation Signals",
2100
- content: "Content Structure"
2231
+ content: "Content Structure",
2232
+ aeo: "AEO Stack"
2101
2233
  };
2102
2234
  function scoreBadge(score) {
2103
2235
  const color = score >= 85 ? "brightgreen" : score >= 60 ? "yellow" : "red";