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.cjs CHANGED
@@ -305,10 +305,12 @@ async function buildContext(url, opts = {}) {
305
305
  "Site appears to be JS-rendered (sparse body + SPA root element). Re-run with --render for accurate results."
306
306
  );
307
307
  }
308
- const [robotsRaw, llmsRaw, llmsFullRaw] = await Promise.all([
308
+ const [robotsRaw, llmsRaw, llmsFullRaw, skillMdRaw, agentPermissionsRaw] = await Promise.all([
309
309
  fetchText(`${origin}/robots.txt`, opts),
310
310
  fetchText(`${origin}/llms.txt`, opts),
311
- fetchText(`${origin}/llms-full.txt`, opts)
311
+ fetchText(`${origin}/llms-full.txt`, opts),
312
+ fetchText(`${origin}/skill.md`, opts),
313
+ fetchText(`${origin}/agent-permissions.json`, opts)
312
314
  ]);
313
315
  let sitemapUrl = null;
314
316
  const robots = robotsRaw ? parseRobots(robotsRaw) : null;
@@ -316,6 +318,13 @@ async function buildContext(url, opts = {}) {
316
318
  if (!sitemapUrl) sitemapUrl = `${origin}/sitemap.xml`;
317
319
  const sitemapRaw = await fetchText(sitemapUrl, opts);
318
320
  const sitemap = sitemapRaw ? parseSitemap(sitemapRaw) : null;
321
+ let agentPermissions = null;
322
+ if (agentPermissionsRaw && agentPermissionsRaw.trim().length > 0) {
323
+ try {
324
+ agentPermissions = JSON.parse(agentPermissionsRaw);
325
+ } catch {
326
+ }
327
+ }
319
328
  return {
320
329
  url,
321
330
  finalUrl,
@@ -330,7 +339,9 @@ async function buildContext(url, opts = {}) {
330
339
  jsonLd: extractJsonLd($),
331
340
  renderMode,
332
341
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
333
- warnings
342
+ warnings,
343
+ skillMd: skillMdRaw && skillMdRaw.trim().length > 0 ? skillMdRaw : null,
344
+ agentPermissions
334
345
  };
335
346
  }
336
347
 
@@ -339,10 +350,11 @@ function defineRule(rule) {
339
350
  return rule;
340
351
  }
341
352
  var CATEGORY_WEIGHTS = {
342
- crawler: 25,
343
- "structured-data": 30,
344
- citation: 25,
345
- content: 20
353
+ crawler: 20,
354
+ "structured-data": 25,
355
+ citation: 20,
356
+ content: 15,
357
+ aeo: 20
346
358
  };
347
359
 
348
360
  // src/engine.ts
@@ -356,7 +368,8 @@ async function runRules(ctx, rules, opts = {}) {
356
368
  crawler: { score: 0, weight: weights.crawler, results: [] },
357
369
  "structured-data": { score: 0, weight: weights["structured-data"], results: [] },
358
370
  citation: { score: 0, weight: weights.citation, results: [] },
359
- content: { score: 0, weight: weights.content, results: [] }
371
+ content: { score: 0, weight: weights.content, results: [] },
372
+ aeo: { score: 0, weight: weights.aeo, results: [] }
360
373
  };
361
374
  for (const rule of rules) {
362
375
  if (onlySet && !onlySet.has(rule.id) && (!rule.stableId || !onlySet.has(rule.stableId))) continue;
@@ -1946,12 +1959,130 @@ var contentRules = [
1946
1959
  externalCitationsRule
1947
1960
  ];
1948
1961
 
1962
+ // src/rules/aeo/skill-md.ts
1963
+ var aeoSkillMdRule = defineRule({
1964
+ id: "aeo.skill-md",
1965
+ stableId: "aeo.skill-md",
1966
+ category: "aeo",
1967
+ group: "opportunity",
1968
+ weight: 3,
1969
+ impact: "high",
1970
+ effort: "low",
1971
+ docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeoskill-md",
1972
+ title: "skill.md is present",
1973
+ title_ko: "skill.md \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
1974
+ description: "A /skill.md file describes site capabilities so AI agents know what this site can do for them.",
1975
+ run(ctx) {
1976
+ if (ctx.skillMd !== null) {
1977
+ return {
1978
+ status: "pass",
1979
+ score: 1,
1980
+ rationale: "skill.md found at site root.",
1981
+ rationale_ko: "skill.md\uAC00 \uC0AC\uC774\uD2B8 \uB8E8\uD2B8\uC5D0 \uC874\uC7AC\uD569\uB2C8\uB2E4."
1982
+ };
1983
+ }
1984
+ return {
1985
+ status: "warn",
1986
+ score: 0,
1987
+ rationale: "No /skill.md found. Add one to describe your site capabilities to AI agents.",
1988
+ 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.",
1989
+ fixHint: "Create /skill.md listing what services, products, and capabilities this site offers."
1990
+ };
1991
+ }
1992
+ });
1993
+
1994
+ // src/rules/aeo/agent-permissions.ts
1995
+ var aeoAgentPermissionsRule = defineRule({
1996
+ id: "aeo.agent-permissions",
1997
+ stableId: "aeo.agent-permissions",
1998
+ category: "aeo",
1999
+ group: "opportunity",
2000
+ weight: 3,
2001
+ impact: "medium",
2002
+ effort: "low",
2003
+ docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeoagent-permissions",
2004
+ title: "agent-permissions.json is present",
2005
+ title_ko: "agent-permissions.json \uD30C\uC77C \uC874\uC7AC \uC5EC\uBD80",
2006
+ description: "Declares explicit read/summarize/cite/train permissions for AI agents.",
2007
+ run(ctx) {
2008
+ if (ctx.agentPermissions !== null) {
2009
+ return {
2010
+ status: "pass",
2011
+ score: 1,
2012
+ rationale: "agent-permissions.json found at site root.",
2013
+ rationale_ko: "agent-permissions.json\uC774 \uC0AC\uC774\uD2B8 \uB8E8\uD2B8\uC5D0 \uC874\uC7AC\uD569\uB2C8\uB2E4.",
2014
+ evidence: ctx.agentPermissions
2015
+ };
2016
+ }
2017
+ return {
2018
+ status: "warn",
2019
+ score: 0,
2020
+ rationale: "No /agent-permissions.json found. Add one to declare AI agent access policy.",
2021
+ 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.",
2022
+ fixHint: "Create /agent-permissions.json with read, summarize, cite, and train permission flags."
2023
+ };
2024
+ }
2025
+ });
2026
+
2027
+ // src/rules/aeo/token-length.ts
2028
+ var THRESHOLD_OPTIMAL = 15e3;
2029
+ var THRESHOLD_MAX = 25e3;
2030
+ var aeoTokenLengthRule = defineRule({
2031
+ id: "aeo.token-length",
2032
+ stableId: "aeo.token-length",
2033
+ category: "aeo",
2034
+ group: "diagnostic",
2035
+ weight: 4,
2036
+ impact: "medium",
2037
+ effort: "medium",
2038
+ docsUrl: "https://github.com/BaRam-OSS/geo-checker/blob/main/docs/rules.md#aeotoken-length",
2039
+ title: "Content token length within AI agent limits",
2040
+ title_ko: "\uCF58\uD150\uCE20 \uD1A0\uD070 \uC218 AI \uC5D0\uC774\uC804\uD2B8 \uAD8C\uC7A5 \uBC94\uC704",
2041
+ description: "Pages under 15K tokens are optimal for AI agents (per Addy Osmani's AEO guidance).",
2042
+ run(ctx) {
2043
+ const text = ctx.$("body").text();
2044
+ const tokenEstimate = Math.round(text.length / 3);
2045
+ const evidence = { tokenEstimate, thresholds: { optimal: THRESHOLD_OPTIMAL, max: THRESHOLD_MAX } };
2046
+ if (tokenEstimate <= THRESHOLD_OPTIMAL) {
2047
+ return {
2048
+ status: "pass",
2049
+ score: 1,
2050
+ rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 within optimal range.`,
2051
+ rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 \uAD8C\uC7A5 \uBC94\uC704(15K) \uC774\uB0B4\uC785\uB2C8\uB2E4.`,
2052
+ evidence
2053
+ };
2054
+ }
2055
+ if (tokenEstimate <= THRESHOLD_MAX) {
2056
+ return {
2057
+ status: "warn",
2058
+ score: 0.5,
2059
+ rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 exceeds 15K recommendation.`,
2060
+ rationale_ko: `\uC608\uC0C1 \uD1A0\uD070 \uC218 ~${tokenEstimate.toLocaleString()} \u2014 15K \uAD8C\uC7A5\uCE58\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4.`,
2061
+ fixHint: "Consider splitting content into shorter, focused pages.",
2062
+ evidence
2063
+ };
2064
+ }
2065
+ return {
2066
+ status: "fail",
2067
+ score: 0,
2068
+ rationale: `Estimated ~${tokenEstimate.toLocaleString()} tokens \u2014 exceeds 25K agent processing limit.`,
2069
+ 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.`,
2070
+ fixHint: "Split this page into multiple focused pages under 15K tokens.",
2071
+ evidence
2072
+ };
2073
+ }
2074
+ });
2075
+
2076
+ // src/rules/aeo/index.ts
2077
+ var aeoRules = [aeoSkillMdRule, aeoAgentPermissionsRule, aeoTokenLengthRule];
2078
+
1949
2079
  // src/rules/index.ts
1950
2080
  var defaultRules = [
1951
2081
  ...crawlerRules,
1952
2082
  ...structuredDataRules,
1953
2083
  ...citationRules,
1954
- ...contentRules
2084
+ ...contentRules,
2085
+ ...aeoRules
1955
2086
  ];
1956
2087
 
1957
2088
  // src/config.ts
@@ -2147,7 +2278,8 @@ var CATEGORY_LABELS = {
2147
2278
  crawler: "AI Crawler Access",
2148
2279
  "structured-data": "Structured Data",
2149
2280
  citation: "Citation Signals",
2150
- content: "Content Structure"
2281
+ content: "Content Structure",
2282
+ aeo: "AEO Stack"
2151
2283
  };
2152
2284
  function scoreBadge(score) {
2153
2285
  const color = score >= 85 ? "brightgreen" : score >= 60 ? "yellow" : "red";