@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
@@ -13,8 +13,9 @@ import lifeMeas from './lifeMeas.js';
13
13
  import { hreflangDetectors } from './hreflang.js';
14
14
  import { cwvDetectors } from './cwv.js';
15
15
  import { AGENT_DETECTORS } from './agent.js';
16
+ import cro from './cro.js';
16
17
 
17
- export const ALL_DETECTORS = [...tech, ...crawl, ...arch, ...page, ...ans, ...entity, ...claim, ...evd, ...schemaData, ...link, ...geoReco, ...lifeMeas, ...hreflangDetectors, ...cwvDetectors, ...AGENT_DETECTORS];
18
+ export const ALL_DETECTORS = [...tech, ...crawl, ...arch, ...page, ...ans, ...entity, ...claim, ...evd, ...schemaData, ...link, ...geoReco, ...lifeMeas, ...hreflangDetectors, ...cwvDetectors, ...AGENT_DETECTORS, ...cro];
18
19
 
19
20
  const ids = new Set();
20
21
  for (const d of ALL_DETECTORS) {
@@ -45,9 +46,10 @@ export function selectDetectors({ scope, namespaces, discipline } = {}) {
45
46
  schema: ['SCHEMA'],
46
47
  lifecycle: ['LIFE'],
47
48
  corroboration: ['EXT'],
49
+ cro: ['CRO'],
48
50
  };
49
51
  if (scopeMap[scope]) ds = ds.filter((d) => scopeMap[scope].includes(d.namespace));
50
- else if (['seo', 'aeo', 'geo'].includes(scope)) ds = ds.filter((d) => d.discipline.includes(scope));
52
+ else if (['seo', 'aeo', 'geo', 'cro'].includes(scope)) ds = ds.filter((d) => d.discipline.includes(scope));
51
53
  }
52
54
  return ds;
53
55
  }
@@ -1,4 +1,4 @@
1
- import { defineDetector, pageSubject } from './framework.js';
1
+ import { defineDetector, indexTargets, pageSubject } from './framework.js';
2
2
 
3
3
  const D = [];
4
4
 
@@ -118,4 +118,115 @@ D.push(defineDetector({
118
118
  },
119
119
  }));
120
120
 
121
+ D.push(defineDetector({
122
+ id: 'LINK-005', name: 'Internal redirect hop chain or circular redirect loop', namespace: 'LINK',
123
+ description: 'An internal link targets a URL that initiates a multi-hop redirect chain (>=2 hops) or a circular redirect loop, dissipating link equity and wasting crawler budget.',
124
+ discipline: ['seo', 'tech'], severity: 'medium', deterministic: true, requires: ['site'],
125
+ impact: { retrieval: 'medium', ranking: 'low' },
126
+ applicable_requirement: 'SEO §3 eliminate multi-hop internal redirects; prevent crawler trap redirect loops',
127
+ remediation: 'Update internal links to point directly to the terminal 200 destination URL, bypassing redirect hops.',
128
+ verification: 'Verify all internal links resolve to terminal 200 destinations in 0 redirect hops.',
129
+ check(ctx) {
130
+ const hits = [];
131
+ for (const p of ctx.site.pages) {
132
+ const seenLinks = new Set();
133
+ for (const e of ctx.site.outbound.get(ctx.site.normalize(p.url)) || []) {
134
+ if (!e.to || seenLinks.has(e.to)) continue;
135
+ seenLinks.add(e.to);
136
+
137
+ let curr = ctx.site.byUrl.get(e.to);
138
+ if (!curr || curr.status < 300 || curr.status >= 400) continue;
139
+
140
+ let hops = 0;
141
+ const visited = [e.to];
142
+ const visitedSet = new Set([e.to]);
143
+ let loopDetected = false;
144
+
145
+ while (curr && curr.status >= 300 && curr.status < 400 && hops < 10) {
146
+ hops++;
147
+ const loc = curr.headers?.location || curr.headers?.Location || curr.redirectLocation;
148
+ if (!loc) break;
149
+ const nextUrl = ctx.site.normalize ? ctx.site.normalize(loc) : loc;
150
+ if (visitedSet.has(nextUrl)) {
151
+ loopDetected = true;
152
+ visited.push(nextUrl);
153
+ break;
154
+ }
155
+ visitedSet.add(nextUrl);
156
+ visited.push(nextUrl);
157
+ curr = ctx.site.byUrl.get(nextUrl);
158
+ }
159
+
160
+ if (loopDetected) {
161
+ hits.push({
162
+ subject: pageSubject(p),
163
+ summary: `Internal link "${e.text || e.href}" targets circular redirect loop (${visited.join(' → ')})`,
164
+ evidence: [
165
+ `link: "${e.text || e.href}"`,
166
+ `redirect loop: ${visited.join(' → ')}`,
167
+ ],
168
+ });
169
+ } else if (hops >= 2) {
170
+ hits.push({
171
+ subject: pageSubject(p),
172
+ summary: `Internal link "${e.text || e.href}" targets multi-hop redirect chain (${hops} hops: ${visited.join(' → ')})`,
173
+ evidence: [
174
+ `link: "${e.text || e.href}"`,
175
+ `redirect chain: ${visited.join(' → ')}`,
176
+ `hops: ${hops}`,
177
+ ],
178
+ captured: hops,
179
+ expected: 0,
180
+ });
181
+ }
182
+ }
183
+ }
184
+ return hits;
185
+ },
186
+ }));
187
+
188
+ D.push(defineDetector({
189
+ id: 'LINK-006', name: 'Excessive naked URL or uninformative internal anchor text ratio', namespace: 'LINK',
190
+ description: 'Over 25% of internal links on a page use naked URLs or uninformative anchors, diluting semantic topic graph signals for search engines and generative models.',
191
+ discipline: ['seo', 'aeo'], severity: 'low', deterministic: true, requires: ['site'],
192
+ impact: { ranking: 'medium', representation: 'low' },
193
+ applicable_requirement: 'SEO §3 descriptive anchor text; AEO §2 semantic entity linking; premise 3.3 topic graph integrity',
194
+ remediation: 'Replace naked URLs and symbol-only anchors with descriptive, keyword-aligned topic text.',
195
+ verification: 'Confirm less than 25% of internal anchors on any page are naked URLs or non-descriptive tokens.',
196
+ check(ctx) {
197
+ const hits = [];
198
+ const UNINFORMATIVE_RX = /^(\s*|https?:\/\/.*|\/.*|[0-9]+|[•→>»*#\-_|~]+|click\s+here|read\s+more|learn\s+more|view\s+more|here|link|more|details|page)$/i;
199
+
200
+ for (const p of indexTargets(ctx)) {
201
+ const outbound = ctx.site.outbound.get(ctx.site.normalize(p.url)) || [];
202
+ if (outbound.length < 4) continue;
203
+
204
+ let uninformativeCount = 0;
205
+ for (const e of outbound) {
206
+ const text = (e.text || '').trim();
207
+ if (!text || UNINFORMATIVE_RX.test(text) || text === e.href || text === e.to) {
208
+ uninformativeCount++;
209
+ }
210
+ }
211
+
212
+ const ratio = uninformativeCount / outbound.length;
213
+ if (ratio > 0.25) {
214
+ hits.push({
215
+ subject: pageSubject(p),
216
+ summary: `Page has ${Math.round(ratio * 100)}% uninformative internal anchors (${uninformativeCount}/${outbound.length})`,
217
+ evidence: [
218
+ `uninformative anchors: ${uninformativeCount}`,
219
+ `total internal links: ${outbound.length}`,
220
+ `ratio: ${(ratio * 100).toFixed(1)}%`,
221
+ ],
222
+ captured: `${Math.round(ratio * 100)}%`,
223
+ expected: '<= 25%',
224
+ });
225
+ }
226
+ }
227
+ return hits;
228
+ },
229
+ }));
230
+
121
231
  export default D;
232
+
@@ -1,4 +1,5 @@
1
1
  import { defineDetector, htmlIndexTargets, pageSubject } from './framework.js';
2
+ import { parse } from 'node-html-parser';
2
3
 
3
4
  const D = [];
4
5
 
@@ -199,4 +200,104 @@ D.push(defineDetector({
199
200
  },
200
201
  }));
201
202
 
203
+ D.push(defineDetector({
204
+ id: 'PAGE-010', name: 'High boilerplate-to-content ratio in extracted answer passages', namespace: 'PAGE',
205
+ description: 'An index-target page has a high ratio of boilerplate (navigation, header, footer, chrome) relative to substantive primary content, diluting extracted answer passages.',
206
+ discipline: ['aeo', 'seo'], severity: 'medium', deterministic: true, requires: ['site'],
207
+ impact: { ranking: 'low', citation: 'medium' },
208
+ applicable_requirement: 'SEO §5 main content extraction; AEO §4 concise and focused answer passages',
209
+ false_positive_conditions: ['minimal navigation shells intentionally designed as search gateways'],
210
+ remediation: 'Reduce header/footer/chrome weight or enrich main body content so substantive answer passages dominate the document.',
211
+ verification: 'Confirm boilerplate-to-content ratio is below the defined threshold.',
212
+ check(ctx) {
213
+ const threshold = ctx.config?.audit?.max_boilerplate_ratio ?? 0.70;
214
+ const hits = [];
215
+ for (const p of htmlIndexTargets(ctx)) {
216
+ if (p.status !== 200) continue;
217
+ const boilerplateWords = (p.structuralRegions || []).reduce((sum, r) => sum + (r.token_count || 0), 0);
218
+ const mainWords = p.wordCount || 0;
219
+ const total = boilerplateWords + mainWords;
220
+ if (boilerplateWords > 0 && mainWords > 0 && total >= 50) {
221
+ const ratio = boilerplateWords / total;
222
+ if (ratio >= threshold) {
223
+ hits.push({
224
+ subject: pageSubject(p),
225
+ summary: `High boilerplate-to-content ratio: ${(100 * ratio).toFixed(1)}% of visible text is chrome/navigation/footer (threshold ${(100 * threshold).toFixed(0)}%)`,
226
+ evidence: [`boilerplate tokens: ${boilerplateWords}`, `main content tokens: ${mainWords}`, `ratio: ${(100 * ratio).toFixed(1)}%`],
227
+ captured: Number(ratio.toFixed(4)),
228
+ threshold,
229
+ });
230
+ }
231
+ }
232
+ }
233
+ return hits;
234
+ },
235
+ }));
236
+
237
+ D.push(defineDetector({
238
+ id: 'PAGE-011', name: 'Critical entity claims rendered inside transient/collapsible UI containers without fallback', namespace: 'PAGE',
239
+ description: 'Registered claim or critical entity statement is rendered exclusively inside a collapsible or transient UI container (<details>, accordion, hidden tab) without an uncollapsed fallback or crawlable representation.',
240
+ discipline: ['aeo', 'geo'], severity: 'medium', deterministic: true, requires: ['site', 'registries'],
241
+ impact: { citation: 'medium', representation: 'medium' },
242
+ applicable_requirement: 'AEO §2 crawlable and indexable answer surfaces; premise 3.5: claims must be verified and crawlable',
243
+ false_positive_conditions: ['supplemental FAQs where claims are also summarized in main prose'],
244
+ remediation: 'Expose key entity statements and primary claim assertions directly in main crawlable prose or provide server-rendered uncollapsed fallbacks.',
245
+ verification: 'Confirm registered claims are accessible outside closed collapsible containers.',
246
+ check(ctx) {
247
+ const claims = (ctx.registries?.claims?.entries || []).filter((c) => !['retired', 'expired', 'prohibited'].includes(c.status));
248
+ if (claims.length === 0) return [];
249
+ const hits = [];
250
+
251
+ for (const p of htmlIndexTargets(ctx)) {
252
+ if (p.status !== 200 || !p.rawHtml) continue;
253
+ const doc = parse(p.rawHtml);
254
+
255
+ // Identify transient/collapsible containers
256
+ const collapsibleContainers = doc.querySelectorAll('details:not([open]), [aria-expanded="false"], .collapse:not(.show), [data-state="closed"]');
257
+ if (collapsibleContainers.length === 0) continue;
258
+
259
+ const containerTexts = collapsibleContainers.map((el) => ({
260
+ tag: el.tagName ? el.tagName.toLowerCase() : 'container',
261
+ text: el.text.toLowerCase().replace(/\s+/g, ' ').trim(),
262
+ })).filter((c) => c.text.length > 0);
263
+
264
+ if (containerTexts.length === 0) continue;
265
+
266
+ // Compute document text outside collapsible containers
267
+ const docClone = parse(p.rawHtml);
268
+ for (const el of docClone.querySelectorAll('details:not([open]), [aria-expanded="false"], .collapse:not(.show), [data-state="closed"]')) {
269
+ el.remove();
270
+ }
271
+ const exteriorText = docClone.text.toLowerCase().replace(/\s+/g, ' ').trim();
272
+
273
+ for (const c of claims) {
274
+ const keywords = (c.claim.toLowerCase().match(/[a-z0-9][a-z0-9-]{3,}/g) || []).slice(0, 8);
275
+ if (keywords.length < 2) continue;
276
+
277
+ for (const container of containerTexts) {
278
+ const matchedInContainer = keywords.filter((k) => container.text.includes(k)).length;
279
+ if (matchedInContainer / keywords.length >= 0.7) {
280
+ const matchedInExterior = keywords.filter((k) => exteriorText.includes(k)).length;
281
+ if (matchedInExterior / keywords.length < 0.4) {
282
+ hits.push({
283
+ subject: pageSubject(p),
284
+ summary: `Claim "${c.claim_id}" rendered inside collapsible <${container.tag}> container without uncollapsed fallback`,
285
+ evidence: [
286
+ `claim_id: ${c.claim_id}`,
287
+ `claim: "${c.claim}"`,
288
+ `container: <${container.tag}>`,
289
+ `keywords matched in container: ${matchedInContainer}/${keywords.length}`,
290
+ `keywords outside container: ${matchedInExterior}/${keywords.length}`,
291
+ ],
292
+ });
293
+ break;
294
+ }
295
+ }
296
+ }
297
+ }
298
+ }
299
+ return hits;
300
+ },
301
+ }));
302
+
202
303
  export default D;