@nebulacomponents/citable 1.15.1 → 1.17.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 (264) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +18 -5
  3. package/dist/universal/.agents/skills/citable/SKILL.md +51 -1
  4. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  5. package/dist/universal/.agents/skills/citable/commands/README.md +3 -1
  6. package/dist/universal/.agents/skills/citable/commands/governance.md +12 -0
  7. package/dist/universal/.agents/skills/citable/commands/measurement.md +20 -0
  8. package/dist/universal/.agents/skills/citable/commands/reporting.md +16 -0
  9. package/dist/universal/.agents/skills/citable/manifest.json +17 -12
  10. package/dist/universal/.agents/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  11. package/dist/universal/.agents/skills/citable/schemas/experiment.schema.json +1 -1
  12. package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +69 -0
  13. package/dist/universal/.agents/skills/citable/schemas/fleet.schema.json +93 -0
  14. package/dist/universal/.agents/skills/citable/schemas/funnel.schema.json +116 -0
  15. package/dist/universal/.agents/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  16. package/dist/universal/.agents/skills/citable/schemas/observation.schema.json +1 -1
  17. package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +98 -0
  18. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  19. package/dist/universal/.claude/skills/citable/SKILL.md +51 -1
  20. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  21. package/dist/universal/.claude/skills/citable/commands/README.md +3 -1
  22. package/dist/universal/.claude/skills/citable/commands/governance.md +12 -0
  23. package/dist/universal/.claude/skills/citable/commands/measurement.md +20 -0
  24. package/dist/universal/.claude/skills/citable/commands/reporting.md +16 -0
  25. package/dist/universal/.claude/skills/citable/manifest.json +17 -12
  26. package/dist/universal/.claude/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  27. package/dist/universal/.claude/skills/citable/schemas/experiment.schema.json +1 -1
  28. package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +69 -0
  29. package/dist/universal/.claude/skills/citable/schemas/fleet.schema.json +93 -0
  30. package/dist/universal/.claude/skills/citable/schemas/funnel.schema.json +116 -0
  31. package/dist/universal/.claude/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  32. package/dist/universal/.claude/skills/citable/schemas/observation.schema.json +1 -1
  33. package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +98 -0
  34. package/dist/universal/.cursor/skills/citable/SKILL.md +51 -1
  35. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  36. package/dist/universal/.cursor/skills/citable/commands/README.md +3 -1
  37. package/dist/universal/.cursor/skills/citable/commands/governance.md +12 -0
  38. package/dist/universal/.cursor/skills/citable/commands/measurement.md +20 -0
  39. package/dist/universal/.cursor/skills/citable/commands/reporting.md +16 -0
  40. package/dist/universal/.cursor/skills/citable/manifest.json +17 -12
  41. package/dist/universal/.cursor/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  42. package/dist/universal/.cursor/skills/citable/schemas/experiment.schema.json +1 -1
  43. package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +69 -0
  44. package/dist/universal/.cursor/skills/citable/schemas/fleet.schema.json +93 -0
  45. package/dist/universal/.cursor/skills/citable/schemas/funnel.schema.json +116 -0
  46. package/dist/universal/.cursor/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  47. package/dist/universal/.cursor/skills/citable/schemas/observation.schema.json +1 -1
  48. package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +98 -0
  49. package/dist/universal/.gemini/skills/citable/SKILL.md +51 -1
  50. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  51. package/dist/universal/.gemini/skills/citable/commands/README.md +3 -1
  52. package/dist/universal/.gemini/skills/citable/commands/governance.md +12 -0
  53. package/dist/universal/.gemini/skills/citable/commands/measurement.md +20 -0
  54. package/dist/universal/.gemini/skills/citable/commands/reporting.md +16 -0
  55. package/dist/universal/.gemini/skills/citable/manifest.json +17 -12
  56. package/dist/universal/.gemini/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  57. package/dist/universal/.gemini/skills/citable/schemas/experiment.schema.json +1 -1
  58. package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +69 -0
  59. package/dist/universal/.gemini/skills/citable/schemas/fleet.schema.json +93 -0
  60. package/dist/universal/.gemini/skills/citable/schemas/funnel.schema.json +116 -0
  61. package/dist/universal/.gemini/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  62. package/dist/universal/.gemini/skills/citable/schemas/observation.schema.json +1 -1
  63. package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +98 -0
  64. package/dist/universal/.github/skills/citable/SKILL.md +51 -1
  65. package/dist/universal/.github/skills/citable/VERSION +1 -1
  66. package/dist/universal/.github/skills/citable/commands/README.md +3 -1
  67. package/dist/universal/.github/skills/citable/commands/governance.md +12 -0
  68. package/dist/universal/.github/skills/citable/commands/measurement.md +20 -0
  69. package/dist/universal/.github/skills/citable/commands/reporting.md +16 -0
  70. package/dist/universal/.github/skills/citable/manifest.json +17 -12
  71. package/dist/universal/.github/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  72. package/dist/universal/.github/skills/citable/schemas/experiment.schema.json +1 -1
  73. package/dist/universal/.github/skills/citable/schemas/finding.schema.json +69 -0
  74. package/dist/universal/.github/skills/citable/schemas/fleet.schema.json +93 -0
  75. package/dist/universal/.github/skills/citable/schemas/funnel.schema.json +116 -0
  76. package/dist/universal/.github/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  77. package/dist/universal/.github/skills/citable/schemas/observation.schema.json +1 -1
  78. package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +98 -0
  79. package/dist/universal/.kiro/skills/citable/SKILL.md +51 -1
  80. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  81. package/dist/universal/.kiro/skills/citable/commands/README.md +3 -1
  82. package/dist/universal/.kiro/skills/citable/commands/governance.md +12 -0
  83. package/dist/universal/.kiro/skills/citable/commands/measurement.md +20 -0
  84. package/dist/universal/.kiro/skills/citable/commands/reporting.md +16 -0
  85. package/dist/universal/.kiro/skills/citable/manifest.json +17 -12
  86. package/dist/universal/.kiro/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  87. package/dist/universal/.kiro/skills/citable/schemas/experiment.schema.json +1 -1
  88. package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +69 -0
  89. package/dist/universal/.kiro/skills/citable/schemas/fleet.schema.json +93 -0
  90. package/dist/universal/.kiro/skills/citable/schemas/funnel.schema.json +116 -0
  91. package/dist/universal/.kiro/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  92. package/dist/universal/.kiro/skills/citable/schemas/observation.schema.json +1 -1
  93. package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +98 -0
  94. package/dist/universal/.opencode/skills/citable/SKILL.md +51 -1
  95. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  96. package/dist/universal/.opencode/skills/citable/commands/README.md +3 -1
  97. package/dist/universal/.opencode/skills/citable/commands/governance.md +12 -0
  98. package/dist/universal/.opencode/skills/citable/commands/measurement.md +20 -0
  99. package/dist/universal/.opencode/skills/citable/commands/reporting.md +16 -0
  100. package/dist/universal/.opencode/skills/citable/manifest.json +17 -12
  101. package/dist/universal/.opencode/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  102. package/dist/universal/.opencode/skills/citable/schemas/experiment.schema.json +1 -1
  103. package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +69 -0
  104. package/dist/universal/.opencode/skills/citable/schemas/fleet.schema.json +93 -0
  105. package/dist/universal/.opencode/skills/citable/schemas/funnel.schema.json +116 -0
  106. package/dist/universal/.opencode/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  107. package/dist/universal/.opencode/skills/citable/schemas/observation.schema.json +1 -1
  108. package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +98 -0
  109. package/dist/universal/.pi/agent/skills/citable/SKILL.md +51 -1
  110. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  111. package/dist/universal/.pi/agent/skills/citable/commands/README.md +3 -1
  112. package/dist/universal/.pi/agent/skills/citable/commands/governance.md +12 -0
  113. package/dist/universal/.pi/agent/skills/citable/commands/measurement.md +20 -0
  114. package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +16 -0
  115. package/dist/universal/.pi/agent/skills/citable/manifest.json +17 -12
  116. package/dist/universal/.pi/agent/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  117. package/dist/universal/.pi/agent/skills/citable/schemas/experiment.schema.json +1 -1
  118. package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +69 -0
  119. package/dist/universal/.pi/agent/skills/citable/schemas/fleet.schema.json +93 -0
  120. package/dist/universal/.pi/agent/skills/citable/schemas/funnel.schema.json +116 -0
  121. package/dist/universal/.pi/agent/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  122. package/dist/universal/.pi/agent/skills/citable/schemas/observation.schema.json +1 -1
  123. package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +98 -0
  124. package/dist/universal/.qoder/skills/citable/SKILL.md +51 -1
  125. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  126. package/dist/universal/.qoder/skills/citable/commands/README.md +3 -1
  127. package/dist/universal/.qoder/skills/citable/commands/governance.md +12 -0
  128. package/dist/universal/.qoder/skills/citable/commands/measurement.md +20 -0
  129. package/dist/universal/.qoder/skills/citable/commands/reporting.md +16 -0
  130. package/dist/universal/.qoder/skills/citable/manifest.json +17 -12
  131. package/dist/universal/.qoder/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  132. package/dist/universal/.qoder/skills/citable/schemas/experiment.schema.json +1 -1
  133. package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +69 -0
  134. package/dist/universal/.qoder/skills/citable/schemas/fleet.schema.json +93 -0
  135. package/dist/universal/.qoder/skills/citable/schemas/funnel.schema.json +116 -0
  136. package/dist/universal/.qoder/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  137. package/dist/universal/.qoder/skills/citable/schemas/observation.schema.json +1 -1
  138. package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +98 -0
  139. package/dist/universal/.rovodev/skills/citable/SKILL.md +51 -1
  140. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  141. package/dist/universal/.rovodev/skills/citable/commands/README.md +3 -1
  142. package/dist/universal/.rovodev/skills/citable/commands/governance.md +12 -0
  143. package/dist/universal/.rovodev/skills/citable/commands/measurement.md +20 -0
  144. package/dist/universal/.rovodev/skills/citable/commands/reporting.md +16 -0
  145. package/dist/universal/.rovodev/skills/citable/manifest.json +17 -12
  146. package/dist/universal/.rovodev/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  147. package/dist/universal/.rovodev/skills/citable/schemas/experiment.schema.json +1 -1
  148. package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +69 -0
  149. package/dist/universal/.rovodev/skills/citable/schemas/fleet.schema.json +93 -0
  150. package/dist/universal/.rovodev/skills/citable/schemas/funnel.schema.json +116 -0
  151. package/dist/universal/.rovodev/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  152. package/dist/universal/.rovodev/skills/citable/schemas/observation.schema.json +1 -1
  153. package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +98 -0
  154. package/dist/universal/.trae/skills/citable/SKILL.md +51 -1
  155. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  156. package/dist/universal/.trae/skills/citable/commands/README.md +3 -1
  157. package/dist/universal/.trae/skills/citable/commands/governance.md +12 -0
  158. package/dist/universal/.trae/skills/citable/commands/measurement.md +20 -0
  159. package/dist/universal/.trae/skills/citable/commands/reporting.md +16 -0
  160. package/dist/universal/.trae/skills/citable/manifest.json +17 -12
  161. package/dist/universal/.trae/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  162. package/dist/universal/.trae/skills/citable/schemas/experiment.schema.json +1 -1
  163. package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +69 -0
  164. package/dist/universal/.trae/skills/citable/schemas/fleet.schema.json +93 -0
  165. package/dist/universal/.trae/skills/citable/schemas/funnel.schema.json +116 -0
  166. package/dist/universal/.trae/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  167. package/dist/universal/.trae/skills/citable/schemas/observation.schema.json +1 -1
  168. package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +98 -0
  169. package/dist/universal/.trae-cn/skills/citable/SKILL.md +51 -1
  170. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  171. package/dist/universal/.trae-cn/skills/citable/commands/README.md +3 -1
  172. package/dist/universal/.trae-cn/skills/citable/commands/governance.md +12 -0
  173. package/dist/universal/.trae-cn/skills/citable/commands/measurement.md +20 -0
  174. package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +16 -0
  175. package/dist/universal/.trae-cn/skills/citable/manifest.json +17 -12
  176. package/dist/universal/.trae-cn/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  177. package/dist/universal/.trae-cn/skills/citable/schemas/experiment.schema.json +1 -1
  178. package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +69 -0
  179. package/dist/universal/.trae-cn/skills/citable/schemas/fleet.schema.json +93 -0
  180. package/dist/universal/.trae-cn/skills/citable/schemas/funnel.schema.json +116 -0
  181. package/dist/universal/.trae-cn/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  182. package/dist/universal/.trae-cn/skills/citable/schemas/observation.schema.json +1 -1
  183. package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +98 -0
  184. package/dist/universal/manifest.json +208 -148
  185. package/package.json +1 -1
  186. package/schemas/cli-output-envelope.schema.json +30 -0
  187. package/schemas/experiment.schema.json +1 -1
  188. package/schemas/finding.schema.json +69 -0
  189. package/schemas/fleet.schema.json +93 -0
  190. package/schemas/funnel.schema.json +116 -0
  191. package/schemas/mcp-transport-envelope.schema.json +244 -0
  192. package/schemas/observation.schema.json +1 -1
  193. package/schemas/remediation-verification.schema.json +98 -0
  194. package/skill/SKILL.md +51 -1
  195. package/skill/commands/README.md +3 -1
  196. package/skill/commands/governance.md +12 -0
  197. package/skill/commands/measurement.md +20 -0
  198. package/skill/commands/reporting.md +16 -0
  199. package/src/analysis/saliency.js +119 -0
  200. package/src/cli/index.js +651 -13
  201. package/src/commands/attributeImpact.js +54 -0
  202. package/src/commands/audit.js +2 -2
  203. package/src/commands/ciWorkflow.js +63 -0
  204. package/src/commands/compatibility.js +205 -0
  205. package/src/commands/connect.js +3 -0
  206. package/src/commands/context.js +2 -1
  207. package/src/commands/edgeRules.js +196 -0
  208. package/src/commands/edgeSecurity.js +192 -0
  209. package/src/commands/exceptions.js +241 -0
  210. package/src/commands/experimentGuardrails.js +213 -0
  211. package/src/commands/fleet.js +104 -0
  212. package/src/commands/generateLlmsTxt.js +111 -0
  213. package/src/commands/goldenCorpus.js +155 -0
  214. package/src/commands/implementationKit.js +202 -0
  215. package/src/commands/inspectAeo.js +74 -0
  216. package/src/commands/inspectCro.js +142 -0
  217. package/src/commands/inspectGeo.js +52 -0
  218. package/src/commands/inspectSerp.js +317 -0
  219. package/src/commands/lintComponents.js +132 -0
  220. package/src/commands/monitor.js +138 -0
  221. package/src/commands/observe.js +6 -1
  222. package/src/commands/planExperiment.js +120 -0
  223. package/src/commands/previewCro.js +241 -0
  224. package/src/commands/probe.js +70 -0
  225. package/src/commands/remediate.js +461 -0
  226. package/src/commands/reportConsensus.js +48 -0
  227. package/src/commands/testFunnel.js +107 -0
  228. package/src/commands/verifyRemediation.js +204 -0
  229. package/src/commands/visualRegression.js +165 -0
  230. package/src/components/index.js +556 -0
  231. package/src/connectors/index.js +6 -1
  232. package/src/connectors/indexnow.js +431 -0
  233. package/src/connectors/mcp/allowlist.js +107 -0
  234. package/src/connectors/mcp/client.js +239 -0
  235. package/src/connectors/mcp/diagnostics.js +214 -0
  236. package/src/connectors/mcp/gscServer.js +155 -0
  237. package/src/connectors/mcp/http.js +130 -0
  238. package/src/connectors/mcp/index.js +7 -0
  239. package/src/connectors/mcp/pilot.js +138 -0
  240. package/src/connectors/mcp/pilotServer.js +80 -0
  241. package/src/connectors/mcp/sanitizer.js +38 -0
  242. package/src/connectors/mcp/stdio.js +178 -0
  243. package/src/crawler/ipRanges.js +161 -0
  244. package/src/detectors/agent.js +127 -0
  245. package/src/detectors/ans.js +282 -0
  246. package/src/detectors/crawl.js +46 -0
  247. package/src/detectors/cro.js +825 -0
  248. package/src/detectors/cwv.js +38 -1
  249. package/src/detectors/evd.js +101 -0
  250. package/src/detectors/framework.js +32 -2
  251. package/src/detectors/geoReco.js +408 -0
  252. package/src/detectors/index.js +4 -2
  253. package/src/detectors/link.js +112 -1
  254. package/src/detectors/page.js +101 -0
  255. package/src/detectors/schemaData.js +529 -0
  256. package/src/detectors/tech.js +306 -0
  257. package/src/extractor/page.js +186 -2
  258. package/src/extractor/site.js +9 -2
  259. package/src/installer/index.js +45 -2
  260. package/src/monitoring/alertDelivery.js +101 -7
  261. package/src/observations/attribution.js +217 -0
  262. package/src/registries/index.js +14 -0
  263. package/src/reporting/consensus.js +416 -0
  264. package/src/reporting/executiveExport.js +244 -0
@@ -0,0 +1,161 @@
1
+ import net from 'node:net';
2
+ import { sha256 } from '../shared/io.js';
3
+
4
+ /**
5
+ * Official published IP ranges for search and AI crawlers.
6
+ * Built-in CIDRs provide deterministic offline verification,
7
+ * while fetchProviderIpRanges supports dynamic feed refreshing.
8
+ */
9
+ export const OFFICIAL_PROVIDER_RANGES = {
10
+ google: {
11
+ name: 'Google',
12
+ url: 'https://developers.google.com/search/apis/ipranges/googlebot.json',
13
+ patterns: [/Googlebot/i, /Google-InspectionTool/i, /Storebot-Google/i],
14
+ cidrs: [
15
+ '66.249.64.0/19',
16
+ '64.233.160.0/19',
17
+ '72.14.192.0/18',
18
+ '209.85.128.0/17',
19
+ '216.239.32.0/19',
20
+ '2001:4860:4801::/48',
21
+ '2607:f8b0:4000::/36',
22
+ ],
23
+ },
24
+ bing: {
25
+ name: 'Bing',
26
+ url: 'https://www.bing.com/toolbox/bingbot.json',
27
+ patterns: [/bingbot/i, /BingPreview/i, /adidxbot/i],
28
+ cidrs: [
29
+ '157.55.39.0/24',
30
+ '207.46.13.0/24',
31
+ '40.77.167.0/24',
32
+ '13.66.139.0/24',
33
+ '13.66.144.0/24',
34
+ '20.105.244.0/24',
35
+ '2603:1030::/32',
36
+ ],
37
+ },
38
+ openai: {
39
+ name: 'OpenAI',
40
+ url: 'https://openai.com/gptbot.json',
41
+ patterns: [/GPTBot/i, /OAI-SearchBot/i, /ChatGPT-User/i],
42
+ cidrs: [
43
+ '20.15.240.64/28',
44
+ '20.15.240.80/28',
45
+ '20.15.240.96/28',
46
+ '20.15.240.112/28',
47
+ '52.230.152.0/24',
48
+ '52.230.153.0/24',
49
+ ],
50
+ },
51
+ perplexity: {
52
+ name: 'Perplexity',
53
+ url: 'https://www.perplexity.ai/perplexitybot.json',
54
+ patterns: [/PerplexityBot/i],
55
+ cidrs: [
56
+ '199.59.148.0/22',
57
+ '199.16.156.0/22',
58
+ ],
59
+ },
60
+ anthropic: {
61
+ name: 'Anthropic',
62
+ url: 'https://anthropic.com/robots.txt',
63
+ patterns: [/ClaudeBot/i, /Claude-Web/i],
64
+ cidrs: [
65
+ '160.79.104.0/23',
66
+ '160.79.106.0/23',
67
+ ],
68
+ },
69
+ };
70
+
71
+ export function identifyCrawlerProvider(userAgent) {
72
+ if (!userAgent) return null;
73
+ for (const [key, provider] of Object.entries(OFFICIAL_PROVIDER_RANGES)) {
74
+ if (provider.patterns.some((re) => re.test(userAgent))) {
75
+ return key;
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+
81
+ export function matchIpInCidrs(sourceIp, cidrs) {
82
+ if (!sourceIp || !Array.isArray(cidrs) || cidrs.length === 0) return { matched: false, matchedCidr: null };
83
+ const family = net.isIP(sourceIp);
84
+ if (!family) return { matched: false, matchedCidr: null };
85
+
86
+ const ipType = family === 4 ? 'ipv4' : 'ipv6';
87
+ for (const cidr of cidrs) {
88
+ const [network, prefixStr] = String(cidr).split('/');
89
+ const prefix = Number(prefixStr);
90
+ const rangeFamily = net.isIP(network);
91
+ if (rangeFamily !== family || !Number.isInteger(prefix)) continue;
92
+
93
+ const block = new net.BlockList();
94
+ block.addSubnet(network, prefix, ipType);
95
+ if (block.check(sourceIp, ipType)) {
96
+ return { matched: true, matchedCidr: cidr };
97
+ }
98
+ }
99
+ return { matched: false, matchedCidr: null };
100
+ }
101
+
102
+ export function verifyCrawlerIp(sourceIp, userAgent, options = {}) {
103
+ const providerKey = identifyCrawlerProvider(userAgent);
104
+ if (!providerKey) {
105
+ return {
106
+ knownProvider: false,
107
+ provider: null,
108
+ matched: false,
109
+ matchedCidr: null,
110
+ source: 'unrecognized_user_agent',
111
+ };
112
+ }
113
+
114
+ const providerDef = OFFICIAL_PROVIDER_RANGES[providerKey];
115
+ const customCidrs = options.ranges?.[userAgent] || options.ranges?.[providerDef.name] || options.ranges?.[providerKey];
116
+ const cidrs = customCidrs || providerDef.cidrs;
117
+
118
+ const result = matchIpInCidrs(sourceIp, cidrs);
119
+ return {
120
+ knownProvider: true,
121
+ provider: providerDef.name,
122
+ providerKey,
123
+ matched: result.matched,
124
+ matchedCidr: result.matchedCidr,
125
+ rangeSource: customCidrs ? 'custom_supplied' : 'official_published',
126
+ sourceUrl: providerDef.url,
127
+ };
128
+ }
129
+
130
+ export async function fetchProviderIpRanges(providerKey, { fetchImpl = globalThis.fetch, timeoutMs = 5000 } = {}) {
131
+ const provider = OFFICIAL_PROVIDER_RANGES[providerKey];
132
+ if (!provider || !provider.url) {
133
+ throw new Error(`unknown or unconfigured IP range provider: ${providerKey}`);
134
+ }
135
+
136
+ const controller = new AbortController();
137
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
138
+
139
+ try {
140
+ const res = await fetchImpl(provider.url, { signal: controller.signal });
141
+ clearTimeout(timer);
142
+ if (!res.ok) {
143
+ throw new Error(`HTTP ${res.status} fetching IP ranges for ${providerKey}`);
144
+ }
145
+
146
+ const text = await res.text();
147
+ const data = JSON.parse(text);
148
+ const prefixes = (data.prefixes || []).map((p) => p.ipv4Prefix || p.ipv6Prefix || p.cidr || p).filter(Boolean);
149
+
150
+ return {
151
+ provider: provider.name,
152
+ url: provider.url,
153
+ checksum: sha256(text),
154
+ cidrs: prefixes.length > 0 ? prefixes : provider.cidrs,
155
+ retrieved_at: new Date().toISOString(),
156
+ };
157
+ } catch (err) {
158
+ clearTimeout(timer);
159
+ throw err;
160
+ }
161
+ }
@@ -11,6 +11,8 @@
11
11
  * - Agentic Commerce (x402, MPP, UCP, ACP)
12
12
  */
13
13
 
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
14
16
  import { defineDetector } from './framework.js';
15
17
 
16
18
  // ---------------------------------------------------------------------------
@@ -481,6 +483,130 @@ export const AGENT_010 = defineDetector({
481
483
  },
482
484
  });
483
485
 
486
+ // ---------------------------------------------------------------------------
487
+ // AGENT-011: llms.txt structure and link integrity
488
+ // ---------------------------------------------------------------------------
489
+
490
+ export const AGENT_011 = defineDetector({
491
+ id: 'AGENT-011',
492
+ name: 'llms.txt structure or link integrity broken',
493
+ namespace: 'AGENT',
494
+ discipline: ['agent-readiness', 'geo'],
495
+ severity: 'medium',
496
+ deterministic: true,
497
+ description:
498
+ 'An /llms.txt or /llms-full.txt file exists on the site but violates the llmstxt.org specification ' +
499
+ '(missing H1 title, missing blockquote description, malformed Markdown links) or contains broken links.',
500
+ applicable_requirement: 'llmstxt.org specification; GEO §3 discoverability; agent-readiness §content-accessibility',
501
+ remediation:
502
+ 'Format /llms.txt with an opening H1 title, a blockquote summary, and valid Markdown links pointing to active HTTP 200 URLs. Run `citable generate llms-txt --write` to generate a conforming specification.',
503
+ verification: 'Fetch /llms.txt and verify H1 title, blockquote description, and resolvable link URLs.',
504
+ check(ctx) {
505
+ let raw = null;
506
+ let identifier = '/llms.txt';
507
+
508
+ if (ctx.site?.llmsTxt?.raw) {
509
+ raw = ctx.site.llmsTxt.raw;
510
+ } else {
511
+ const page = ctx.site?.pages?.find((p) => p.path === '/llms.txt' || p.url?.endsWith('/llms.txt'));
512
+ if (page) {
513
+ raw = page.rawHtml || page.text || null;
514
+ identifier = page.url || '/llms.txt';
515
+ } else if (ctx.site?.location) {
516
+ const localPath = path.join(ctx.site.location, 'llms.txt');
517
+ if (fs.existsSync(localPath)) {
518
+ raw = fs.readFileSync(localPath, 'utf8');
519
+ }
520
+ }
521
+ }
522
+
523
+ if (!raw || raw.trim().length === 0) return [];
524
+
525
+ const findings = [];
526
+ const problems = [];
527
+
528
+ if (!/^#\s+.+/m.test(raw)) {
529
+ problems.push('Missing H1 project/site title heading (# Title)');
530
+ }
531
+
532
+ if (!/^>\s+.+/m.test(raw)) {
533
+ problems.push('Missing blockquote description/summary (> Summary)');
534
+ }
535
+
536
+ const linkMatches = [...raw.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)];
537
+ const sitePages = ctx.site?.pages || [];
538
+ const brokenLinks = [];
539
+
540
+ for (const match of linkMatches) {
541
+ const linkText = match[1];
542
+ const linkUrl = match[2].trim();
543
+
544
+ let normalizedPath = null;
545
+ try {
546
+ if (linkUrl.startsWith('http://') || linkUrl.startsWith('https://')) {
547
+ const parsed = new URL(linkUrl);
548
+ const baseOrigin = ctx.config?.site?.base_url
549
+ ? new URL(ctx.config.site.base_url).origin
550
+ : (sitePages[0] ? new URL(sitePages[0].url).origin : null);
551
+ if (baseOrigin && parsed.origin === baseOrigin) {
552
+ normalizedPath = parsed.pathname;
553
+ }
554
+ } else if (linkUrl.startsWith('/')) {
555
+ normalizedPath = linkUrl.split('?')[0].split('#')[0];
556
+ }
557
+ } catch {
558
+ problems.push(`Malformed URL in markdown link: [${linkText}](${linkUrl})`);
559
+ continue;
560
+ }
561
+
562
+ if (normalizedPath && sitePages.length > 0) {
563
+ const existsInPages = sitePages.some((p) => {
564
+ try {
565
+ const pagePath = new URL(p.url).pathname;
566
+ return (
567
+ pagePath === normalizedPath ||
568
+ pagePath === normalizedPath + '/' ||
569
+ normalizedPath === pagePath + '/' ||
570
+ pagePath.replace(/\/index\.html$/, '/') === normalizedPath
571
+ );
572
+ } catch {
573
+ return false;
574
+ }
575
+ });
576
+
577
+ if (!existsInPages && (normalizedPath === '/llms-full.txt' || normalizedPath.endsWith('/llms-full.txt'))) {
578
+ const fullExists = Boolean(
579
+ ctx.site?.llmsFullTxt?.raw ||
580
+ ctx.site?.pages?.some((p) => p.path === '/llms-full.txt' || p.url?.endsWith('/llms-full.txt')) ||
581
+ (ctx.site?.location && fs.existsSync(path.join(ctx.site.location, 'llms-full.txt')))
582
+ );
583
+ if (!fullExists) {
584
+ brokenLinks.push(`[${linkText}](${linkUrl}) -> referenced /llms-full.txt not found`);
585
+ }
586
+ } else if (!existsInPages) {
587
+ brokenLinks.push(`[${linkText}](${linkUrl}) -> URL not found in crawled site pages`);
588
+ }
589
+ }
590
+ }
591
+
592
+ if (brokenLinks.length > 0) {
593
+ problems.push(`${brokenLinks.length} broken/unresolved internal link(s): ${brokenLinks.slice(0, 3).join(', ')}`);
594
+ }
595
+
596
+ if (problems.length > 0) {
597
+ findings.push({
598
+ subject: { type: 'file', identifier },
599
+ summary: `/llms.txt has ${problems.length} specification / link integrity problem(s)`,
600
+ evidence: problems,
601
+ captured: problems.join('; '),
602
+ expected: 'Valid llmstxt.org format with H1 title, blockquote summary, and resolvable link URLs',
603
+ });
604
+ }
605
+
606
+ return findings;
607
+ },
608
+ });
609
+
484
610
  export const AGENT_DETECTORS = [
485
611
  AGENT_001,
486
612
  AGENT_002,
@@ -492,4 +618,5 @@ export const AGENT_DETECTORS = [
492
618
  AGENT_008,
493
619
  AGENT_009,
494
620
  AGENT_010,
621
+ AGENT_011,
495
622
  ];
@@ -1,4 +1,5 @@
1
1
  import { defineDetector, indexTargets, registryPageFor, pageSubject } from './framework.js';
2
+ import { parse } from 'node-html-parser';
2
3
 
3
4
  const D = [];
4
5
 
@@ -259,4 +260,285 @@ D.push(defineDetector({
259
260
  },
260
261
  }));
261
262
 
263
+ D.push(defineDetector({
264
+ id: 'ANS-011', name: 'Over-diluted answer passage (missing direct answer lead)', namespace: 'ANS',
265
+ description: 'A section targeting an informational query or question heading lacks a concise, direct answer passage in its opening sentences, burying key factual answers behind excessive preamble or rambling prose.',
266
+ discipline: ['aeo', 'geo'], severity: 'medium', deterministic: false, requires: ['site'],
267
+ impact: { citation: 'medium', representation: 'low' },
268
+ applicable_requirement: 'AEO §4 direct answer block in first 50–100 words; AEO §5 concise declarative answer passages',
269
+ false_positive_conditions: ['narrative essays or creative fiction where question headings are rhetorical devices'],
270
+ remediation: 'Provide a concise 30–70 word direct declarative answer immediately following the question heading before elaborating with background details.',
271
+ verification: 'Re-inspect question sections to verify the first paragraph delivers a direct factual answer.',
272
+ check(ctx) {
273
+ const hits = [];
274
+ const RAMBLE_RX = /^(to understand|before (exploring|answering|diving|we)|in order to understand|it is (important|essential) to (note|understand|first)|there are many (factors|aspects)|the answer to this question depends on)/i;
275
+
276
+ for (const p of indexTargets(ctx)) {
277
+ if (p.status !== 200 || !p.rawHtml) continue;
278
+ const doc = parse(p.rawHtml);
279
+ const headings = doc.querySelectorAll('h2, h3, h4');
280
+
281
+ for (const h of headings) {
282
+ const text = h.text.trim();
283
+ const isQuestion = /\?$/.test(text) || /^(what|how|why|when|where|who|is|are|can|does|do|which)\b/i.test(text);
284
+ if (!isQuestion) continue;
285
+
286
+ let curr = h.nextElementSibling;
287
+ let firstP = null;
288
+ while (curr && !/^h[1-6]$/i.test(curr.tagName)) {
289
+ if (curr.tagName.toLowerCase() === 'p') {
290
+ const pt = curr.text.trim();
291
+ if (pt.length > 0) {
292
+ firstP = pt;
293
+ break;
294
+ }
295
+ }
296
+ curr = curr.nextElementSibling;
297
+ }
298
+
299
+ if (firstP) {
300
+ const words = firstP.split(/\s+/).filter(Boolean);
301
+ const isRambling = RAMBLE_RX.test(firstP);
302
+ const isOverlyLongWithoutDirectness = words.length > 130;
303
+ if (isRambling || isOverlyLongWithoutDirectness) {
304
+ hits.push({
305
+ subject: pageSubject(p),
306
+ summary: `Question section "${text.slice(0, 60)}" lacks a concise direct answer lead (${isRambling ? 'rambling preamble' : `opening paragraph is ${words.length} words`})`,
307
+ evidence: [
308
+ `heading: "${text}"`,
309
+ `opening: "${firstP.slice(0, 150)}..."`,
310
+ `word_count: ${words.length}`,
311
+ ],
312
+ confidence: 'medium',
313
+ });
314
+ }
315
+ }
316
+ }
317
+ }
318
+ return hits;
319
+ },
320
+ }));
321
+
322
+ D.push(defineDetector({
323
+ id: 'ANS-012', name: 'Ungrounded quantitative metric in answer passage', namespace: 'ANS',
324
+ description: 'An answer-bearing passage asserts high-stakes quantitative metrics, percentages, or multiples without adjacent evidence citation, benchmark reference, or registered evidence link.',
325
+ discipline: ['aeo', 'geo'], severity: 'medium', deterministic: false, requires: ['site'],
326
+ impact: { citation: 'medium', legal: 'low' },
327
+ applicable_requirement: 'AEO §8 version evidence; GEO §5 substantiated factual claims; premise 3.5: quantitative outcomes require grounding',
328
+ false_positive_conditions: ['introductory marketing pages where detailed case studies are linked elsewhere in the navigation'],
329
+ remediation: 'Attach an adjacent citation, benchmark link, methodology note, or registered evidence ID near the metric.',
330
+ verification: 'Confirm every statistical or multiple claim has adjacent attribution or registered evidence.',
331
+ check(ctx) {
332
+ const hits = [];
333
+ const STAT_RX = /\b(\d+(?:\.\d+)?%\s*(?:reduction|increase|faster|decrease|improvement|growth|boost|drop|savings?)|(?:\d+(?:\.\d+)?x)\s*(?:faster|more|performance|throughput|speed|improvement)|\$\d+(?:\.\d+)?[MBK]\b)/i;
334
+ const ATTRIBUTION_RX = /\[\d+\]|\((?:source|benchmark|study|audit|test|report|see)\b|according to|citation|evidence|EVD-|CLAIM-|\bhttps?:\/\/|href=/i;
335
+
336
+ for (const p of indexTargets(ctx)) {
337
+ if (p.status !== 200 || !p.paragraphs) continue;
338
+
339
+ for (const para of p.paragraphs) {
340
+ const statMatch = STAT_RX.exec(para);
341
+ if (statMatch) {
342
+ const hasAttribution = ATTRIBUTION_RX.test(para);
343
+ if (!hasAttribution) {
344
+ hits.push({
345
+ subject: pageSubject(p),
346
+ summary: `Ungrounded quantitative metric "${statMatch[1]}" in answer prose lacks citation or evidence attribution`,
347
+ evidence: [
348
+ `metric: "${statMatch[1]}"`,
349
+ `passage: "${para.slice(0, 150)}..."`,
350
+ ],
351
+ confidence: 'medium',
352
+ });
353
+ }
354
+ }
355
+ }
356
+ }
357
+ return hits;
358
+ },
359
+ }));
360
+
361
+ D.push(defineDetector({
362
+ id: 'ANS-013', name: 'Comparative or procedural section lacking structured table or ordered sequence', namespace: 'ANS',
363
+ description: 'A section covering comparative evaluation (vs, alternatives, comparison) or multi-step execution (how to, steps) lacks structured <table>, <ol>, or <ul> markup, diminishing answer engine extraction fidelity.',
364
+ discipline: ['aeo', 'seo'], severity: 'medium', deterministic: true, requires: ['site'],
365
+ impact: { representation: 'medium', citation: 'low' },
366
+ applicable_requirement: 'AEO §5 structured answer surfaces; GEO §4 extractability; premise 3.3 information gain',
367
+ remediation: 'Represent comparisons with <table> markup and sequential instructions with <ol> step lists.',
368
+ verification: 'Confirm comparative/procedural sections contain <table> or <ol> structured elements.',
369
+ check(ctx) {
370
+ const hits = [];
371
+ const COMPARATIVE_RX = /\b(vs\.?|versus|comparison|alternatives|pros and cons|differences between)\b/i;
372
+ const PROCEDURAL_RX = /\b(how to|steps to|installation steps|quickstart guide|configuration steps|deployment guide)\b/i;
373
+
374
+ for (const p of indexTargets(ctx)) {
375
+ if (p.status !== 200 || !p.rawHtml) continue;
376
+ const doc = parse(p.rawHtml);
377
+ const headings = doc.querySelectorAll('h2, h3');
378
+
379
+ for (const h of headings) {
380
+ const text = h.text.trim();
381
+ const isComp = COMPARATIVE_RX.test(text);
382
+ const isProc = PROCEDURAL_RX.test(text);
383
+ if (!isComp && !isProc) continue;
384
+
385
+ let curr = h.nextElementSibling;
386
+ let hasTable = false;
387
+ let hasList = false;
388
+ let textWordCount = 0;
389
+
390
+ while (curr && !/^h[1-3]$/i.test(curr.tagName)) {
391
+ if (curr.tagName.toLowerCase() === 'table' || curr.querySelector('table')) hasTable = true;
392
+ if (/^(ol|ul)$/i.test(curr.tagName) || curr.querySelector('ol, ul')) hasList = true;
393
+ if (curr.tagName.toLowerCase() === 'p') {
394
+ textWordCount += curr.text.split(/\s+/).filter(Boolean).length;
395
+ }
396
+ curr = curr.nextElementSibling;
397
+ }
398
+
399
+ if (textWordCount > 50) {
400
+ if (isComp && !hasTable) {
401
+ hits.push({
402
+ subject: pageSubject(p),
403
+ summary: `Comparative section "${text.slice(0, 60)}" lacks a structured <table> comparison surface`,
404
+ evidence: [`heading: "${text}"`, `section word count: ${textWordCount}`, 'no <table> detected before next heading'],
405
+ confidence: 'high',
406
+ });
407
+ } else if (isProc && !hasList) {
408
+ hits.push({
409
+ subject: pageSubject(p),
410
+ summary: `Procedural section "${text.slice(0, 60)}" lacks structured <ol> step or list markup`,
411
+ evidence: [`heading: "${text}"`, `section word count: ${textWordCount}`, 'no <ol>/<ul> detected before next heading'],
412
+ confidence: 'high',
413
+ });
414
+ }
415
+ }
416
+ }
417
+ }
418
+ return hits;
419
+ },
420
+ }));
421
+
422
+ D.push(defineDetector({
423
+ id: 'ANS-014', name: 'Low information-gain fluff ratio in answer prose', namespace: 'ANS',
424
+ description: 'An answer passage contains excessive rhetorical filler, clichés, or conversational padding, reducing factual density for generative engine extraction.',
425
+ discipline: ['aeo', 'geo'], severity: 'low', deterministic: true, requires: ['site'],
426
+ impact: { representation: 'low', citation: 'low' },
427
+ applicable_requirement: 'AEO §8 concise factual grounding; GEO §4 information gain; premise 3.3 substantive answer density',
428
+ remediation: 'Remove conversational clichés, tautologies, and promotional fluff; state verifiable facts, properties, and constraints directly.',
429
+ verification: 'Re-audit answer passages to ensure cliché and filler phrases are removed.',
430
+ check(ctx) {
431
+ const hits = [];
432
+ const FLUFF_PATTERNS = [
433
+ /\b(?:as we all know|it goes without saying|needless to say)\b/i,
434
+ /\bat the end of the day\b/i,
435
+ /\b(?:in today's rapidly evolving|in this day and age)\b/i,
436
+ /\b(?:game[- ]changer|revolutionary new|paradigm shift)\b/i,
437
+ /\b(?:delve into|dive deep into|unlock the power of|leverage the power of)\b/i,
438
+ /\b(?:seamlessly integrate|cutting[- ]edge technology)\b/i,
439
+ ];
440
+
441
+ for (const p of indexTargets(ctx)) {
442
+ if (p.status !== 200 || !p.paragraphs) continue;
443
+
444
+ for (const para of p.paragraphs) {
445
+ if (para.length < 60) continue;
446
+ const matchedFluff = [];
447
+ for (const rx of FLUFF_PATTERNS) {
448
+ const m = rx.exec(para);
449
+ if (m) matchedFluff.push(m[0]);
450
+ }
451
+
452
+ if (matchedFluff.length >= 2) {
453
+ hits.push({
454
+ subject: pageSubject(p),
455
+ summary: `Answer passage contains low information-gain fluff clichés (${matchedFluff.join(', ')})`,
456
+ evidence: [
457
+ `clichés detected: ${matchedFluff.join(', ')}`,
458
+ `passage: "${para.slice(0, 150)}..."`,
459
+ ],
460
+ confidence: 'medium',
461
+ });
462
+ }
463
+ }
464
+ }
465
+ return hits;
466
+ },
467
+ }));
468
+
469
+ D.push(defineDetector({
470
+ id: 'ANS-015', name: 'Definitional page lacks direct copular definition', namespace: 'ANS',
471
+ description: 'A page with definitional intent (page_type=definition, /definition/, /glossary/, or "What is" heading) does not provide a direct copular definition ("X is a Y that Z") in the opening paragraph.',
472
+ discipline: ['aeo', 'geo'], severity: 'medium', deterministic: true, requires: ['site'],
473
+ impact: { retrieval: 'medium', citation: 'high' },
474
+ applicable_requirement: 'AEO §4 direct copular answer block; search engine featured snippet & LLM definition synthesis requirements',
475
+ remediation: 'Formulate the opening sentence with a direct copular definition stating what the concept is, its primary class, and its defining capability.',
476
+ verification: 'Confirm the opening paragraph defines the entity using a copular verb ("is", "are", "refers to") within the first 140 characters.',
477
+ check(ctx) {
478
+ const hits = [];
479
+ const COPULA_RX = /\b(?:is|are|refers\s+to|denotes|represents|means)\b/i;
480
+ for (const p of indexTargets(ctx)) {
481
+ if (p.status !== 200 || !p.paragraphs || p.paragraphs.length === 0) continue;
482
+ const reg = registryPageFor(ctx, p);
483
+ const isDefType = reg?.page_type === 'definition' || /\/(definition|glossary)\b/i.test(p.url);
484
+ const firstH1 = (p.headings || []).find((h) => h.level === 1)?.text || '';
485
+ const isWhatIsHeading = /^(what\s+is|definition\s+of|meaning\s+of)\b/i.test(firstH1);
486
+
487
+ if (isDefType || isWhatIsHeading) {
488
+ const firstP = p.paragraphs[0] || '';
489
+ const openingSlice = firstP.slice(0, 140);
490
+ if (!COPULA_RX.test(openingSlice)) {
491
+ hits.push({
492
+ subject: pageSubject(p),
493
+ summary: `Definitional page opening does not provide a direct copular definition ("is", "are", "refers to")`,
494
+ evidence: [
495
+ `opening passage: "${firstP.slice(0, 120)}..."`,
496
+ 'definitional query extractors require immediate copular formulation in lead sentence',
497
+ ],
498
+ captured: firstP.slice(0, 80),
499
+ expected: 'direct copular sentence (e.g. "X is a Y that Z") in first 140 characters',
500
+ });
501
+ }
502
+ }
503
+ }
504
+ return hits;
505
+ },
506
+ }));
507
+
508
+ D.push(defineDetector({
509
+ id: 'ANS-016', name: 'Comprehensive long-form guide lacks executive summary or key takeaways', namespace: 'ANS',
510
+ description: 'An in-depth article or guide exceeding 1,200 words does not provide an executive summary, key takeaways, or TL;DR block, increasing RAG chunk fragmentation and extraction ambiguity.',
511
+ discipline: ['aeo', 'geo'], severity: 'low', deterministic: true, requires: ['site'],
512
+ impact: { representation: 'medium', citation: 'low' },
513
+ applicable_requirement: 'AEO §4 structural answer density; GEO §4 top-level synthesis grounding for RAG embedding retrieval',
514
+ remediation: 'Add an executive summary or bulleted key takeaways block near the top of the article before detailed section walkthroughs.',
515
+ verification: 'Re-audit page to verify a summary, key takeaways, or TL;DR section precedes long-form content.',
516
+ check(ctx) {
517
+ const hits = [];
518
+ const SUMMARY_RX = /\b(?:executive\s+summary|key\s+takeaways?|summary|overview|at\s+a\s+glance|tl;?dr|quick\s+summary|highlights?)\b/i;
519
+ for (const p of indexTargets(ctx)) {
520
+ if (p.status !== 200) continue;
521
+ const wordCount = p.rawVisibleWordCount || p.wordCount || 0;
522
+ if (wordCount < 1200) continue;
523
+
524
+ const hasSummaryHeading = (p.headings || []).some((h) => SUMMARY_RX.test(h.text));
525
+ if (!hasSummaryHeading) {
526
+ hits.push({
527
+ subject: pageSubject(p),
528
+ summary: `Long-form guide (${wordCount} words) lacks an executive summary, key takeaways, or TL;DR section`,
529
+ evidence: [
530
+ `word count: ${wordCount} words`,
531
+ 'no heading matches executive summary, key takeaways, or TL;DR pattern',
532
+ 'sprawling unsummarized documents experience lower RAG retrieval relevance in generative answer engines',
533
+ ],
534
+ captured: { wordCount, hasSummaryHeading: false },
535
+ expected: 'executive summary or key takeaways section for documents > 1,200 words',
536
+ });
537
+ }
538
+ }
539
+ return hits;
540
+ },
541
+ }));
542
+
262
543
  export default D;
544
+
@@ -1,6 +1,7 @@
1
1
  import { defineDetector, entrySubject } from './framework.js';
2
2
  import { isAllowed } from '../crawler/robots.js';
3
3
  import { isPastDate } from '../shared/io.js';
4
+ import { verifyCrawlerIp } from '../crawler/ipRanges.js';
4
5
 
5
6
  const D = [];
6
7
 
@@ -140,4 +141,49 @@ D.push(defineDetector({
140
141
  },
141
142
  }));
142
143
 
144
+ D.push(defineDetector({
145
+ id: 'CRAWL-007', name: 'Spoofed crawler identity in access logs', namespace: 'CRAWL',
146
+ description: 'An observed crawler log row claims an authoritative bot user-agent (Googlebot, Bingbot, GPTBot, ClaudeBot, etc.) but its source IP is outside published provider ranges or reverse-DNS verification failed.',
147
+ discipline: ['seo', 'aeo', 'geo'], severity: 'high', deterministic: true, requires: ['site'],
148
+ impact: { retrieval: 'high', legal: 'medium' },
149
+ applicable_requirement: 'GEO §2 crawler verification; staged identity contract and provider-published IP boundaries',
150
+ remediation: 'Verify edge/WAF logs against official provider IP ranges or reverse-DNS, and block or throttle unverified impersonating crawlers.',
151
+ verification: 'Confirm observed crawler requests originate within official published CIDR ranges and pass rDNS.',
152
+ check(ctx) {
153
+ if (!ctx.observations) return [];
154
+ const hits = [];
155
+ for (const obs of ctx.observations) {
156
+ if (obs.kind !== 'crawler_log' || !obs.data) continue;
157
+ const d = obs.data;
158
+ const ua = d.user_agent;
159
+ const ip = d.source_ip;
160
+ const identity = d.crawler_identity;
161
+ if (!ua) continue;
162
+
163
+ const verification = verifyCrawlerIp(ip, ua);
164
+ const isContradictory = identity?.verification_status === 'contradictory'
165
+ || identity?.cidr_membership === 'not_matched'
166
+ || (verification.knownProvider && !verification.matched);
167
+
168
+ if (isContradictory) {
169
+ hits.push({
170
+ subject: { type: 'crawler', identifier: ua },
171
+ summary: `Spoofed or unverified crawler traffic detected for ${ua} from IP ${ip || 'unknown'}`,
172
+ evidence: [
173
+ `user_agent: ${ua}`,
174
+ `source_ip: ${ip || 'none'}`,
175
+ `provider: ${verification.provider || identity?.provider || 'unknown'}`,
176
+ `verification_status: ${identity?.verification_status || 'contradictory'}`,
177
+ `cidr_membership: ${identity?.cidr_membership || (verification.matched ? 'matched' : 'not_matched')}`,
178
+ `range_source: ${verification.sourceUrl || 'provider_published'}`,
179
+ ],
180
+ captured: { user_agent: ua, source_ip: ip, verification_status: identity?.verification_status || 'contradictory' },
181
+ expected: 'Crawler requests must originate from official provider-published IP ranges',
182
+ });
183
+ }
184
+ }
185
+ return hits;
186
+ },
187
+ }));
188
+
143
189
  export default D;