@pseolint/core 0.4.3 → 0.5.4

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 (270) hide show
  1. package/README.md +264 -169
  2. package/dist/ai/manifest/diff.d.ts +78 -0
  3. package/dist/ai/manifest/diff.d.ts.map +1 -0
  4. package/dist/ai/manifest/diff.js +139 -0
  5. package/dist/ai/manifest/diff.js.map +1 -0
  6. package/dist/ai/manifest/index.d.ts +18 -0
  7. package/dist/ai/manifest/index.d.ts.map +1 -0
  8. package/dist/ai/manifest/index.js +15 -0
  9. package/dist/ai/manifest/index.js.map +1 -0
  10. package/dist/ai/manifest/validate-manifest.d.ts +37 -0
  11. package/dist/ai/manifest/validate-manifest.d.ts.map +1 -0
  12. package/dist/ai/manifest/validate-manifest.js +67 -0
  13. package/dist/ai/manifest/validate-manifest.js.map +1 -0
  14. package/dist/ai/manifest/validators/domain-patches.d.ts +15 -0
  15. package/dist/ai/manifest/validators/domain-patches.d.ts.map +1 -0
  16. package/dist/ai/manifest/validators/domain-patches.js +110 -0
  17. package/dist/ai/manifest/validators/domain-patches.js.map +1 -0
  18. package/dist/ai/manifest/validators/index.d.ts +5 -0
  19. package/dist/ai/manifest/validators/index.d.ts.map +1 -0
  20. package/dist/ai/manifest/validators/index.js +4 -0
  21. package/dist/ai/manifest/validators/index.js.map +1 -0
  22. package/dist/ai/manifest/validators/page-changes.d.ts +36 -0
  23. package/dist/ai/manifest/validators/page-changes.d.ts.map +1 -0
  24. package/dist/ai/manifest/validators/page-changes.js +221 -0
  25. package/dist/ai/manifest/validators/page-changes.js.map +1 -0
  26. package/dist/ai/manifest/validators/types.d.ts +17 -0
  27. package/dist/ai/manifest/validators/types.d.ts.map +1 -0
  28. package/dist/ai/manifest/validators/types.js +5 -0
  29. package/dist/ai/manifest/validators/types.js.map +1 -0
  30. package/dist/ai/orchestrate.d.ts +74 -0
  31. package/dist/ai/orchestrate.d.ts.map +1 -0
  32. package/dist/ai/orchestrate.js +54 -0
  33. package/dist/ai/orchestrate.js.map +1 -0
  34. package/dist/ai/orchestrator/budget.d.ts +57 -0
  35. package/dist/ai/orchestrator/budget.d.ts.map +1 -0
  36. package/dist/ai/orchestrator/budget.js +114 -0
  37. package/dist/ai/orchestrator/budget.js.map +1 -0
  38. package/dist/ai/orchestrator/finish-tool.d.ts +568 -0
  39. package/dist/ai/orchestrator/finish-tool.d.ts.map +1 -0
  40. package/dist/ai/orchestrator/finish-tool.js +114 -0
  41. package/dist/ai/orchestrator/finish-tool.js.map +1 -0
  42. package/dist/ai/orchestrator/index.d.ts +25 -0
  43. package/dist/ai/orchestrator/index.d.ts.map +1 -0
  44. package/dist/ai/orchestrator/index.js +21 -0
  45. package/dist/ai/orchestrator/index.js.map +1 -0
  46. package/dist/ai/orchestrator/log.d.ts +24 -0
  47. package/dist/ai/orchestrator/log.d.ts.map +1 -0
  48. package/dist/ai/orchestrator/log.js +48 -0
  49. package/dist/ai/orchestrator/log.js.map +1 -0
  50. package/dist/ai/orchestrator/page-cache.d.ts +64 -0
  51. package/dist/ai/orchestrator/page-cache.d.ts.map +1 -0
  52. package/dist/ai/orchestrator/page-cache.js +127 -0
  53. package/dist/ai/orchestrator/page-cache.js.map +1 -0
  54. package/dist/ai/orchestrator/prompt.d.ts +16 -0
  55. package/dist/ai/orchestrator/prompt.d.ts.map +1 -0
  56. package/dist/ai/orchestrator/prompt.js +52 -0
  57. package/dist/ai/orchestrator/prompt.js.map +1 -0
  58. package/dist/ai/orchestrator/runner.d.ts +65 -0
  59. package/dist/ai/orchestrator/runner.d.ts.map +1 -0
  60. package/dist/ai/orchestrator/runner.js +223 -0
  61. package/dist/ai/orchestrator/runner.js.map +1 -0
  62. package/dist/ai/orchestrator/session.d.ts +44 -0
  63. package/dist/ai/orchestrator/session.d.ts.map +1 -0
  64. package/dist/ai/orchestrator/session.js +64 -0
  65. package/dist/ai/orchestrator/session.js.map +1 -0
  66. package/dist/ai/orchestrator/types.d.ts +99 -0
  67. package/dist/ai/orchestrator/types.d.ts.map +1 -0
  68. package/dist/ai/orchestrator/types.js +8 -0
  69. package/dist/ai/orchestrator/types.js.map +1 -0
  70. package/dist/ai/probes/cache.d.ts +12 -0
  71. package/dist/ai/probes/cache.d.ts.map +1 -0
  72. package/dist/ai/probes/cache.js +46 -0
  73. package/dist/ai/probes/cache.js.map +1 -0
  74. package/dist/ai/tools/ask-ai-engine.d.ts +77 -0
  75. package/dist/ai/tools/ask-ai-engine.d.ts.map +1 -0
  76. package/dist/ai/tools/ask-ai-engine.js +253 -0
  77. package/dist/ai/tools/ask-ai-engine.js.map +1 -0
  78. package/dist/ai/tools/check-domain-crawler-access.d.ts +71 -0
  79. package/dist/ai/tools/check-domain-crawler-access.d.ts.map +1 -0
  80. package/dist/ai/tools/check-domain-crawler-access.js +76 -0
  81. package/dist/ai/tools/check-domain-crawler-access.js.map +1 -0
  82. package/dist/ai/tools/check-domain-llms-txt.d.ts +70 -0
  83. package/dist/ai/tools/check-domain-llms-txt.d.ts.map +1 -0
  84. package/dist/ai/tools/check-domain-llms-txt.js +75 -0
  85. package/dist/ai/tools/check-domain-llms-txt.js.map +1 -0
  86. package/dist/ai/tools/check-indexability.d.ts +58 -0
  87. package/dist/ai/tools/check-indexability.d.ts.map +1 -0
  88. package/dist/ai/tools/check-indexability.js +64 -0
  89. package/dist/ai/tools/check-indexability.js.map +1 -0
  90. package/dist/ai/tools/check-robots.d.ts +68 -0
  91. package/dist/ai/tools/check-robots.d.ts.map +1 -0
  92. package/dist/ai/tools/check-robots.js +90 -0
  93. package/dist/ai/tools/check-robots.js.map +1 -0
  94. package/dist/ai/tools/check-rule-answer-first.d.ts +54 -0
  95. package/dist/ai/tools/check-rule-answer-first.d.ts.map +1 -0
  96. package/dist/ai/tools/check-rule-answer-first.js +50 -0
  97. package/dist/ai/tools/check-rule-answer-first.js.map +1 -0
  98. package/dist/ai/tools/check-rule-canonical-consistency.d.ts +66 -0
  99. package/dist/ai/tools/check-rule-canonical-consistency.d.ts.map +1 -0
  100. package/dist/ai/tools/check-rule-canonical-consistency.js +51 -0
  101. package/dist/ai/tools/check-rule-canonical-consistency.js.map +1 -0
  102. package/dist/ai/tools/check-rule-citable-facts.d.ts +58 -0
  103. package/dist/ai/tools/check-rule-citable-facts.d.ts.map +1 -0
  104. package/dist/ai/tools/check-rule-citable-facts.js +41 -0
  105. package/dist/ai/tools/check-rule-citable-facts.js.map +1 -0
  106. package/dist/ai/tools/check-rule-content-modularity.d.ts +58 -0
  107. package/dist/ai/tools/check-rule-content-modularity.d.ts.map +1 -0
  108. package/dist/ai/tools/check-rule-content-modularity.js +45 -0
  109. package/dist/ai/tools/check-rule-content-modularity.js.map +1 -0
  110. package/dist/ai/tools/check-rule-faq-coverage.d.ts +54 -0
  111. package/dist/ai/tools/check-rule-faq-coverage.d.ts.map +1 -0
  112. package/dist/ai/tools/check-rule-faq-coverage.js +39 -0
  113. package/dist/ai/tools/check-rule-faq-coverage.js.map +1 -0
  114. package/dist/ai/tools/check-rule-freshness-signals.d.ts +54 -0
  115. package/dist/ai/tools/check-rule-freshness-signals.d.ts.map +1 -0
  116. package/dist/ai/tools/check-rule-freshness-signals.js +45 -0
  117. package/dist/ai/tools/check-rule-freshness-signals.js.map +1 -0
  118. package/dist/ai/tools/check-rule-json-ld-valid.d.ts +54 -0
  119. package/dist/ai/tools/check-rule-json-ld-valid.d.ts.map +1 -0
  120. package/dist/ai/tools/check-rule-json-ld-valid.js +44 -0
  121. package/dist/ai/tools/check-rule-json-ld-valid.js.map +1 -0
  122. package/dist/ai/tools/check-rule-missing-author.d.ts +54 -0
  123. package/dist/ai/tools/check-rule-missing-author.d.ts.map +1 -0
  124. package/dist/ai/tools/check-rule-missing-author.js +45 -0
  125. package/dist/ai/tools/check-rule-missing-author.js.map +1 -0
  126. package/dist/ai/tools/check-rule-near-duplicate.d.ts +82 -0
  127. package/dist/ai/tools/check-rule-near-duplicate.d.ts.map +1 -0
  128. package/dist/ai/tools/check-rule-near-duplicate.js +63 -0
  129. package/dist/ai/tools/check-rule-near-duplicate.js.map +1 -0
  130. package/dist/ai/tools/check-rule-required-fields.d.ts +50 -0
  131. package/dist/ai/tools/check-rule-required-fields.d.ts.map +1 -0
  132. package/dist/ai/tools/check-rule-required-fields.js +38 -0
  133. package/dist/ai/tools/check-rule-required-fields.js.map +1 -0
  134. package/dist/ai/tools/check-rule-schema-consistency.d.ts +54 -0
  135. package/dist/ai/tools/check-rule-schema-consistency.d.ts.map +1 -0
  136. package/dist/ai/tools/check-rule-schema-consistency.js +44 -0
  137. package/dist/ai/tools/check-rule-schema-consistency.js.map +1 -0
  138. package/dist/ai/tools/check-rule-summary-bait.d.ts +54 -0
  139. package/dist/ai/tools/check-rule-summary-bait.d.ts.map +1 -0
  140. package/dist/ai/tools/check-rule-summary-bait.js +39 -0
  141. package/dist/ai/tools/check-rule-summary-bait.js.map +1 -0
  142. package/dist/ai/tools/check-rule-thin-content.d.ts +66 -0
  143. package/dist/ai/tools/check-rule-thin-content.d.ts.map +1 -0
  144. package/dist/ai/tools/check-rule-thin-content.js +58 -0
  145. package/dist/ai/tools/check-rule-thin-content.js.map +1 -0
  146. package/dist/ai/tools/detect-templates.d.ts +60 -0
  147. package/dist/ai/tools/detect-templates.d.ts.map +1 -0
  148. package/dist/ai/tools/detect-templates.js +43 -0
  149. package/dist/ai/tools/detect-templates.js.map +1 -0
  150. package/dist/ai/tools/fetch-page.d.ts +70 -0
  151. package/dist/ai/tools/fetch-page.d.ts.map +1 -0
  152. package/dist/ai/tools/fetch-page.js +93 -0
  153. package/dist/ai/tools/fetch-page.js.map +1 -0
  154. package/dist/ai/tools/fetch-sitemap.d.ts +60 -0
  155. package/dist/ai/tools/fetch-sitemap.d.ts.map +1 -0
  156. package/dist/ai/tools/fetch-sitemap.js +116 -0
  157. package/dist/ai/tools/fetch-sitemap.js.map +1 -0
  158. package/dist/ai/tools/index.d.ts +1555 -0
  159. package/dist/ai/tools/index.d.ts.map +1 -0
  160. package/dist/ai/tools/index.js +119 -0
  161. package/dist/ai/tools/index.js.map +1 -0
  162. package/dist/ai/tools/parse-page.d.ts +94 -0
  163. package/dist/ai/tools/parse-page.d.ts.map +1 -0
  164. package/dist/ai/tools/parse-page.js +108 -0
  165. package/dist/ai/tools/parse-page.js.map +1 -0
  166. package/dist/ai/tools/query-serp.d.ts +113 -0
  167. package/dist/ai/tools/query-serp.d.ts.map +1 -0
  168. package/dist/ai/tools/query-serp.js +131 -0
  169. package/dist/ai/tools/query-serp.js.map +1 -0
  170. package/dist/ai/tools/sample-template.d.ts +67 -0
  171. package/dist/ai/tools/sample-template.d.ts.map +1 -0
  172. package/dist/ai/tools/sample-template.js +75 -0
  173. package/dist/ai/tools/sample-template.js.map +1 -0
  174. package/dist/ai/tools/types.d.ts +73 -0
  175. package/dist/ai/tools/types.d.ts.map +1 -0
  176. package/dist/ai/tools/types.js +64 -0
  177. package/dist/ai/tools/types.js.map +1 -0
  178. package/dist/ai/tools/validate-jsonld.d.ts +62 -0
  179. package/dist/ai/tools/validate-jsonld.d.ts.map +1 -0
  180. package/dist/ai/tools/validate-jsonld.js +84 -0
  181. package/dist/ai/tools/validate-jsonld.js.map +1 -0
  182. package/dist/auditor.d.ts +4 -0
  183. package/dist/auditor.d.ts.map +1 -1
  184. package/dist/auditor.js +629 -64
  185. package/dist/auditor.js.map +1 -1
  186. package/dist/backpressure.d.ts.map +1 -1
  187. package/dist/backpressure.js +10 -3
  188. package/dist/backpressure.js.map +1 -1
  189. package/dist/enrich-findings.d.ts.map +1 -1
  190. package/dist/enrich-findings.js +15 -1
  191. package/dist/enrich-findings.js.map +1 -1
  192. package/dist/formatters/console.d.ts.map +1 -1
  193. package/dist/formatters/console.js +13 -0
  194. package/dist/formatters/console.js.map +1 -1
  195. package/dist/formatters/markdown.d.ts.map +1 -1
  196. package/dist/formatters/markdown.js +20 -2
  197. package/dist/formatters/markdown.js.map +1 -1
  198. package/dist/index.d.ts +12 -1
  199. package/dist/index.d.ts.map +1 -1
  200. package/dist/index.js +8 -0
  201. package/dist/index.js.map +1 -1
  202. package/dist/rule-references.d.ts.map +1 -1
  203. package/dist/rule-references.js +5 -0
  204. package/dist/rule-references.js.map +1 -1
  205. package/dist/rules/content/heading-structure.d.ts +21 -0
  206. package/dist/rules/content/heading-structure.d.ts.map +1 -0
  207. package/dist/rules/content/heading-structure.js +56 -0
  208. package/dist/rules/content/heading-structure.js.map +1 -0
  209. package/dist/rules/content/image-alt-text.d.ts +18 -0
  210. package/dist/rules/content/image-alt-text.d.ts.map +1 -0
  211. package/dist/rules/content/image-alt-text.js +77 -0
  212. package/dist/rules/content/image-alt-text.js.map +1 -0
  213. package/dist/rules/content/title-uniqueness.d.ts +18 -0
  214. package/dist/rules/content/title-uniqueness.d.ts.map +1 -0
  215. package/dist/rules/content/title-uniqueness.js +70 -0
  216. package/dist/rules/content/title-uniqueness.js.map +1 -0
  217. package/dist/rules/links/host-section-divergence.d.ts +3 -0
  218. package/dist/rules/links/host-section-divergence.d.ts.map +1 -0
  219. package/dist/rules/links/host-section-divergence.js +158 -0
  220. package/dist/rules/links/host-section-divergence.js.map +1 -0
  221. package/dist/rules/links/link-depth.d.ts +12 -1
  222. package/dist/rules/links/link-depth.d.ts.map +1 -1
  223. package/dist/rules/links/link-depth.js +25 -12
  224. package/dist/rules/links/link-depth.js.map +1 -1
  225. package/dist/rules/scope.d.ts.map +1 -1
  226. package/dist/rules/scope.js +5 -0
  227. package/dist/rules/scope.js.map +1 -1
  228. package/dist/rules/spam/doorway-pattern.d.ts.map +1 -1
  229. package/dist/rules/spam/doorway-pattern.js +27 -4
  230. package/dist/rules/spam/doorway-pattern.js.map +1 -1
  231. package/dist/rules/spam/publication-velocity.d.ts +1 -1
  232. package/dist/rules/spam/publication-velocity.d.ts.map +1 -1
  233. package/dist/rules/spam/publication-velocity.js +9 -4
  234. package/dist/rules/spam/publication-velocity.js.map +1 -1
  235. package/dist/rules/spam/template-coverage.js +1 -1
  236. package/dist/rules/spam/template-coverage.js.map +1 -1
  237. package/dist/rules/spam/template-diversity.js +1 -1
  238. package/dist/rules/spam/template-diversity.js.map +1 -1
  239. package/dist/rules/tech/hreflang-consistency.d.ts.map +1 -1
  240. package/dist/rules/tech/hreflang-consistency.js +33 -4
  241. package/dist/rules/tech/hreflang-consistency.js.map +1 -1
  242. package/dist/rules/tech/og-completeness.d.ts +11 -0
  243. package/dist/rules/tech/og-completeness.d.ts.map +1 -1
  244. package/dist/rules/tech/og-completeness.js +22 -23
  245. package/dist/rules/tech/og-completeness.js.map +1 -1
  246. package/dist/ruleset-version.d.ts +8 -0
  247. package/dist/ruleset-version.d.ts.map +1 -0
  248. package/dist/ruleset-version.js +8 -0
  249. package/dist/ruleset-version.js.map +1 -0
  250. package/dist/scrape-strategy.d.ts +42 -0
  251. package/dist/scrape-strategy.d.ts.map +1 -0
  252. package/dist/scrape-strategy.js +101 -0
  253. package/dist/scrape-strategy.js.map +1 -0
  254. package/dist/site-classifier.d.ts.map +1 -1
  255. package/dist/site-classifier.js +1 -0
  256. package/dist/site-classifier.js.map +1 -1
  257. package/dist/state.d.ts +36 -1
  258. package/dist/state.d.ts.map +1 -1
  259. package/dist/state.js +3 -1
  260. package/dist/state.js.map +1 -1
  261. package/dist/stratified-sample.d.ts +9 -1
  262. package/dist/stratified-sample.d.ts.map +1 -1
  263. package/dist/stratified-sample.js +23 -6
  264. package/dist/stratified-sample.js.map +1 -1
  265. package/dist/types.d.ts +135 -2
  266. package/dist/types.d.ts.map +1 -1
  267. package/dist/url-normalize.d.ts.map +1 -1
  268. package/dist/url-normalize.js +13 -1
  269. package/dist/url-normalize.js.map +1 -1
  270. package/package.json +90 -90
package/README.md CHANGED
@@ -1,170 +1,265 @@
1
- # @pseolint/core
2
-
3
- > Programmatic SEO audit engine for SpamBrain-risk detection across large template-generated sites.
4
-
5
- The core engine behind [pseolint](https://www.npmjs.com/package/pseolint). Use this package to embed pSEO auditing into your own tools, CI pipelines, or SaaS products.
6
-
7
- ## Install
8
-
9
- ```bash
10
- npm install @pseolint/core
11
- ```
12
-
13
- ## Usage
14
-
15
- ```ts
16
- import { auditSource } from "@pseolint/core";
17
-
18
- const summary = await auditSource("./out");
19
- console.log(`Score: ${summary.score}/100`);
20
- console.log(`Findings: ${summary.findings.length}`);
21
- ```
22
-
23
- `auditSource` accepts a local directory, a single HTML file, a page URL, or a sitemap URL.
24
-
25
- ## What It Checks
26
-
27
- 32 rules grouped into 4 scoring super-categories (v0.4): **Integrity** (spam + content + cannibal, weight 0.50), **Discoverability** (links + tech, 0.20), **Citation** (aeo + schema, 0.25), **Data** (0.05). Source-tree namespaces remain `spam/*`, `aeo/*`, etc. for stable rule IDs.
28
-
29
- - **Spam / SpamBrain risk** (8) — near-duplicate (SimHash), entity-swap doorways, thin content, boilerplate ratio, template diversity, template coverage, publication velocity, doorway pattern
30
- - **Technical SEO** (8) — canonical consistency, canonical/noindex and robots/noindex conflicts, sitemap completeness, robots compliance, redirect chains, soft 404s, Open Graph, hreflang
31
- - **AEO / AI Overview citability** (9, v0.3.0–v0.3.1) — `llms.txt` presence, AI-crawler access in robots.txt, freshness signals, FAQ coverage, answer-first opener, citable-fact density, non-replicable value, content modularity, **summary-bait** (pages optimized for summarization over retention)
32
- - **Content** (5) — unique value, heading / meta uniqueness, author attribution, E-E-A-T signals
33
- - **Internal linking** (5) — orphan pages, dead ends, cluster connectivity, hub pages, link depth
34
- - **Structured data** (3) — JSON-LD validity, required fields, cross-page schema consistency
35
- - **Cannibalization** (3) — title overlap, keyword collision, URL pattern conflicts
36
- - **Data binding** (2) — verify rendered pages expose values from a source dataset (missing or identical-across-pages bindings)
37
-
38
- ## API
39
-
40
- ### `auditSource(source, options?)`
41
-
42
- Returns an `AuditSummary` with composite score, category scores, enriched findings, and optional cache / state / AI-triage metadata.
43
-
44
- Selected options (see `AuditOptions` in `types.ts` for the full surface):
45
-
46
- ```ts
47
- await auditSource("https://example.com/sitemap.xml", {
48
- concurrency: 5,
49
- timeout: 30_000,
50
- sampleSize: 200,
51
- samplingStrategy: "stratified", // or "random"
52
- ignore: ["**/api/**"],
53
- maxFetchBytes: 52_428_800, // 50 MB hard cap per run
54
- cache: { dir: ".pseolint/cache", ttlMs: 7 * 24 * 60 * 60 * 1000 },
55
- state: { path: ".pseolint/state.json", since: true, exitOnRegression: true },
56
- pageGroups: {
57
- blog: { match: "**/blog/**", rules: ["content/*", "spam/*"] },
58
- products: { match: "**/p/**", overrides: { "spam/thin-content": { thinContentMinWords: 200 } } },
59
- },
60
- dataSource: { records: [{ url: "/p/*", data: { price: "$19", stock: 12 } }] },
61
- entityPatterns: [{ placeholder: "[CITY]", pattern: "\\b(NYC|LA|SF)\\b", flags: "gi" }],
62
- ai: { enabled: true, provider: "anthropic", model: "claude-haiku-4-5-20251001", maxCostUsd: 0.1 },
63
- telemetry: { enabled: true, path: ".pseolint/telemetry.jsonl" },
64
- // Safety (v0.3.2–v0.3.3)
65
- safeMode: "saas", // "saas" | "cli" — flips guardSsrf + caps
66
- guardSsrf: true, // DNS-validated SSRF check on every URL
67
- respectRobotsTxt: true, // skip sitemap URLs Disallow'd by target robots.txt
68
- followRedirects: true,
69
- maxCrawlDiscovered: 2000, // hard ceiling on link-discovery fan-out
70
- signal: controller.signal, // AbortSignal — ctrl-C / quota-exhausted cancels cleanly
71
- rules: {
72
- nearDuplicateThreshold: 0.85,
73
- thinContentMinWords: 300,
74
- titleOverlapThreshold: 0.8,
75
- // ...
76
- },
77
- });
78
- ```
79
-
80
- ### Safety primitives (SSRF, abort, crawl-ceiling)
81
-
82
- `@pseolint/core` ships a few primitives for hosts that run audits against
83
- user-submitted URLs. All are opt-in; local CLI use doesn't change.
84
-
85
- ```ts
86
- import {
87
- safeFetch, // SSRF-safe fetch for non-audit use cases
88
- validateTargetHost, // throws SSRFError on private-range / DNS-rebinding targets
89
- isPrivateOrReservedHost,
90
- SSRFError,
91
- DnsResolutionError,
92
- } from "@pseolint/core";
93
-
94
- // Validate a user-submitted URL before enqueuing:
95
- await validateTargetHost(new URL(userUrl).hostname);
96
-
97
- // Fetch with SSRF guard baked in:
98
- const res = await safeFetch(userUrl, { timeoutMs: 10_000, followRedirects: false });
99
- ```
100
-
101
- The full audit picks up the same guard via `auditSource(url, { safeMode: "saas" })`
102
- or via the individual `guardSsrf` / `respectRobotsTxt` / `followRedirects` flags.
103
-
104
- ### Render-mode analytics blocking
105
-
106
- Rendered audits (`options.render = {...}`) block known analytics endpoints
107
- by default so the audit doesn't inject fake sessions into the site owner's
108
- GA / Plausible / PostHog / Mixpanel / Hotjar / Sentry dashboards.
109
-
110
- ```ts
111
- await auditSource(url, {
112
- render: {
113
- analyticsMode: "block", // default — blocks ~40 analytics hosts
114
- // "allow-first-party" block third-party only
115
- // "allow" — don't intercept anything
116
- extraBlockedHosts: ["my-internal-metrics.corp"],
117
- },
118
- });
119
- ```
120
-
121
- ### Formatters
122
-
123
- ```ts
124
- import { formatConsole, formatJson, formatMarkdown, formatHtml } from "@pseolint/core";
125
-
126
- const out = formatConsole(summary);
127
- const json = formatJson(summary);
128
- const md = formatMarkdown(summary);
129
- const html = formatHtml(summary);
130
- ```
131
-
132
- ### AI triage
133
-
134
- When `ai.enabled` is set, findings are clustered into root-causes by an LLM. Providers are loaded lazily from optional peer deps — install only the one you need:
135
-
136
- ```bash
137
- npm install @ai-sdk/anthropic # or @ai-sdk/openai, @ai-sdk/google, @ai-sdk/mistral,
138
- # @ai-sdk/groq, @ai-sdk/xai, @ai-sdk/cohere,
139
- # ollama-ai-provider-v2
140
- ```
141
-
142
- ```ts
143
- import { triageFindings, createLanguageModel, estimateCostUsd } from "@pseolint/core";
144
- ```
145
-
146
- Cost and daily-budget caps are enforced pre-flight; results are cached on disk by default.
147
-
148
- ### Delta runs & regression gating
149
-
150
- Pass `state.since: true` to audit only URLs whose content hash changed since the last run, and `state.exitOnRegression: true` to flag a run where a new rule ID fires on any previously clean URL (`summary.hasRegression`).
151
-
152
- ### Caching
153
-
154
- Setting `cache` enables an ETag/Last-Modified-aware disk cache for HTTP fetches. `summary.cacheStats` reports `{ hits, total, bytesSavedEstimate }`.
155
-
156
- ### Page groups
157
-
158
- Classify pages by glob and apply different rule subsets or threshold overrides per group. Results are surfaced in `summary.groupScores` / `summary.groupPageCounts`.
159
-
160
- ### Rendering
161
-
162
- For client-rendered pages, install `playwright-core` and pass `render: { browserWsEndpoint }` to connect to an existing browser endpoint.
163
-
164
- ## Peer dependencies
165
-
166
- All AI providers and `playwright-core` are optional peers you only install the ones you actually use.
167
-
168
- ## License
169
-
1
+ # @pseolint/core
2
+
3
+ > Programmatic SEO audit engine for SpamBrain-risk detection across large template-generated sites.
4
+
5
+ The core engine behind [pseolint](https://www.npmjs.com/package/pseolint). Use this package to embed pSEO auditing into your own tools, CI pipelines, or SaaS products.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @pseolint/core
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { auditSource } from "@pseolint/core";
17
+
18
+ const summary = await auditSource("./out");
19
+ console.log(`Score: ${summary.score}/100`);
20
+ console.log(`Findings: ${summary.findings.length}`);
21
+ ```
22
+
23
+ `auditSource` accepts a local directory, a single HTML file, a page URL, or a sitemap URL.
24
+
25
+ ## What It Checks
26
+
27
+ 45 rules grouped into 4 scoring super-categories (v0.4): **Integrity** (spam + content + cannibal, weight 0.50), **Discoverability** (links + tech, 0.20), **Citation** (aeo + schema, 0.25), **Data** (0.05). Source-tree namespaces remain `spam/*`, `aeo/*`, etc. for stable rule IDs.
28
+
29
+ - **Spam / SpamBrain risk** (8) — near-duplicate (SimHash), entity-swap doorways, thin content, boilerplate ratio, template diversity, template coverage, publication velocity, doorway pattern (cluster-collapsed since v0.5.2)
30
+ - **Technical SEO** (9) — canonical consistency, canonical/noindex and robots/noindex conflicts, sitemap completeness, robots compliance, redirect chains, soft 404s, hreflang reciprocity, robots-sitemap presence, **og-completeness** (v0.5.2)
31
+ - **AEO / AI Overview citability** (8, v0.3.0–v0.3.1) — `llms.txt` presence, AI-crawler access in robots.txt, freshness signals, FAQ coverage, answer-first opener, citable-fact density, content modularity, **summary-bait** (pages optimized for summarization over retention)
32
+ - **Content** (7) — unique value, meta uniqueness, author attribution, E-E-A-T signals, plus **title-uniqueness**, **heading-structure**, **image-alt-text** (all v0.5.2)
33
+ - **Internal linking** (6) — orphan pages, dead ends, cluster connectivity, link depth, unreachable-from-root (sample-aware), **host-section-divergence** (v0.5.1, site-reputation-abuse detector)
34
+ - **Structured data** (3) — JSON-LD validity, required fields, cross-page schema consistency
35
+ - **Cannibalization** (1) — URL pattern conflicts (`title-overlap` and `keyword-collision` were dropped in v0.4 due to high false-positive rates)
36
+ - **Data binding** (2) — verify rendered pages expose values from a source dataset (missing or identical-across-pages bindings)
37
+
38
+ ## What's new in v0.5.2 — credibility layer
39
+
40
+ - **4 new content-quality rules** addressing the v0.5.1 blind-spot audit's tier-1 gaps: `content/title-uniqueness` (raw, not entity-masked — catalog templates with per-record entity values still pass), `content/heading-structure` (H1 presence, single-H1, hierarchy), `content/image-alt-text` (skips `role="presentation"` / `aria-hidden="true"` / explicit `alt=""`), `tech/og-completeness` (the README-promised rule that finally ships).
41
+ - **`AuditOptions.authorityScore`** (0-100) — bring-your-own-DA. ≥80 shifts the verdict ladder one tier lenient (established brand can absorb shapes a newer site can't). ≤30 shifts one tier stricter (newer/lower-authority operator). Raw `risk` number unchanged so CI gates stay stable. The engine itself remains authority-blind by design — no Moz/Ahrefs/Semrush dependency.
42
+ - **`AuditOptions.sampleSeed`** — deterministic `mulberry32` PRNG plumbed through the stratified sampler. Repeated audits with the same seed pick the same pages and produce reproducible verdicts.
43
+ - **`spam/doorway-pattern` cluster collapse** — emits in the same `pageUrl` + `relatedUrls[0]` shape as `spam/near-duplicate` and is registered in `CLUSTERABLE_RULES`. C(N,2) per-pair findings on entity-swap-heavy catalogs collapse into one cluster finding per template-tied group.
44
+ - **Per-bucket info-severity cap** — a flood of info findings can't fill a category bucket on its own (capped at 50 separately from the 100 cap on warning+ findings).
45
+ - **`summary.appliedSeverityDemotions: string[]`** — engine emits the list of rule IDs whose severity was overridden by the active scoring profile so consumers (formatters, CI) can show *which* rules got demoted and *why*. Pass `--strict` to disable demotions entirely.
46
+ - **Sample-aware rules** — `links/unreachable-from-root` skips on partial-sample audits (it can't distinguish real graph isolation from sample-shape).
47
+ - **Markdown formatter** collapses informational findings under `<details>` so PR comments don't drown actionable items in 100+ info bullets.
48
+
49
+ The full per-round iteration story (9 calibration rounds against a curated reputable-pSEO corpus) and the trade-offs we accepted are at [`docs/superpowers/specs/2026-05-03-calibration-against-reputable-pseo.md`](../../docs/superpowers/specs/2026-05-03-calibration-against-reputable-pseo.md). The honest blind-spot audit (what we still don't detect, including the domain-authority gap that motivated `--authority-score`) is at [`docs/superpowers/specs/2026-05-03-pseolint-blind-spots.md`](../../docs/superpowers/specs/2026-05-03-pseolint-blind-spots.md). The dated user-facing methodology summary is at [pseolint.dev/methodology](https://pseolint.dev/methodology).
50
+
51
+ ## API
52
+
53
+ ### `auditSource(source, options?)`
54
+
55
+ Returns an `AuditSummary` with composite score, category scores, enriched findings, and optional cache / state / AI-triage metadata.
56
+
57
+ Selected options (see `AuditOptions` in `types.ts` for the full surface):
58
+
59
+ ```ts
60
+ await auditSource("https://example.com/sitemap.xml", {
61
+ concurrency: 5,
62
+ timeout: 30_000,
63
+ sampleSize: 200,
64
+ samplingStrategy: "stratified", // or "random"
65
+ ignore: ["**/api/**"],
66
+ maxFetchBytes: 52_428_800, // 50 MB hard cap per run
67
+ cache: { dir: ".pseolint/cache", ttlMs: 7 * 24 * 60 * 60 * 1000 },
68
+ state: {
69
+ path: ".pseolint/state.json",
70
+ mode: "monitoring", // v0.5+: pre-fetch decision matrix; "fresh" forces full re-audit.
71
+ // Omit to auto-monitor when prior state exists.
72
+ ageFloorDays: 7, // v0.5+: forces refetch on URLs older than N days
73
+ exitOnRegression: true,
74
+ since: true, // v0.5+ alias for mode: "monitoring" (back-compat)
75
+ },
76
+ pageGroups: {
77
+ blog: { match: "**/blog/**", rules: ["content/*", "spam/*"] },
78
+ products: { match: "**/p/**", overrides: { "spam/thin-content": { thinContentMinWords: 200 } } },
79
+ },
80
+ dataSource: { records: [{ url: "/p/*", data: { price: "$19", stock: 12 } }] },
81
+ entityPatterns: [{ placeholder: "[CITY]", pattern: "\\b(NYC|LA|SF)\\b", flags: "gi" }],
82
+ ai: { enabled: true, provider: "anthropic", model: "claude-haiku-4-5-20251001", maxCostUsd: 0.1 },
83
+ telemetry: { enabled: true, path: ".pseolint/telemetry.jsonl" },
84
+ // Safety (v0.3.2–v0.3.3)
85
+ safeMode: "saas", // "saas" | "cli" — flips guardSsrf + caps
86
+ guardSsrf: true, // DNS-validated SSRF check on every URL
87
+ respectRobotsTxt: true, // skip sitemap URLs Disallow'd by target robots.txt
88
+ followRedirects: true,
89
+ maxCrawlDiscovered: 2000, // hard ceiling on link-discovery fan-out
90
+ signal: controller.signal, // AbortSignal — ctrl-C / quota-exhausted cancels cleanly
91
+ rules: {
92
+ nearDuplicateThreshold: 0.85,
93
+ thinContentMinWords: 300,
94
+ titleOverlapThreshold: 0.8,
95
+ // ...
96
+ },
97
+ });
98
+ ```
99
+
100
+ ### Safety primitives (SSRF, abort, crawl-ceiling)
101
+
102
+ `@pseolint/core` ships a few primitives for hosts that run audits against
103
+ user-submitted URLs. All are opt-in; local CLI use doesn't change.
104
+
105
+ ```ts
106
+ import {
107
+ safeFetch, // SSRF-safe fetch for non-audit use cases
108
+ validateTargetHost, // throws SSRFError on private-range / DNS-rebinding targets
109
+ isPrivateOrReservedHost,
110
+ SSRFError,
111
+ DnsResolutionError,
112
+ } from "@pseolint/core";
113
+
114
+ // Validate a user-submitted URL before enqueuing:
115
+ await validateTargetHost(new URL(userUrl).hostname);
116
+
117
+ // Fetch with SSRF guard baked in:
118
+ const res = await safeFetch(userUrl, { timeoutMs: 10_000, followRedirects: false });
119
+ ```
120
+
121
+ The full audit picks up the same guard via `auditSource(url, { safeMode: "saas" })`
122
+ or via the individual `guardSsrf` / `respectRobotsTxt` / `followRedirects` flags.
123
+
124
+ ### Render-mode analytics blocking
125
+
126
+ Rendered audits (`options.render = {...}`) block known analytics endpoints
127
+ by default so the audit doesn't inject fake sessions into the site owner's
128
+ GA / Plausible / PostHog / Mixpanel / Hotjar / Sentry dashboards.
129
+
130
+ ```ts
131
+ await auditSource(url, {
132
+ render: {
133
+ analyticsMode: "block", // default — blocks ~40 analytics hosts
134
+ // "allow-first-party" block third-party only
135
+ // "allow" — don't intercept anything
136
+ extraBlockedHosts: ["my-internal-metrics.corp"],
137
+ },
138
+ });
139
+ ```
140
+
141
+ ### Formatters
142
+
143
+ ```ts
144
+ import { formatConsole, formatJson, formatMarkdown, formatHtml } from "@pseolint/core";
145
+
146
+ const out = formatConsole(summary);
147
+ const json = formatJson(summary);
148
+ const md = formatMarkdown(summary);
149
+ const html = formatHtml(summary);
150
+ ```
151
+
152
+ ### AI triage
153
+
154
+ When `ai.enabled` is set, findings are clustered into root-causes by an LLM. Providers are loaded lazily from optional peer deps — install only the one you need:
155
+
156
+ ```bash
157
+ npm install @ai-sdk/anthropic # or @ai-sdk/openai, @ai-sdk/google, @ai-sdk/mistral,
158
+ # @ai-sdk/groq, @ai-sdk/xai, @ai-sdk/cohere,
159
+ # ollama-ai-provider-v2
160
+ ```
161
+
162
+ ```ts
163
+ import { triageFindings, createLanguageModel, estimateCostUsd } from "@pseolint/core";
164
+ ```
165
+
166
+ Cost and daily-budget caps are enforced pre-flight; results are cached on disk by default.
167
+
168
+ ### AI orchestrator (v0.5)
169
+
170
+ Net-new in v0.5. `orchestrate()` drives an LLM through 25 deterministic tools (sitemap fetch, template clustering, per-page rule checks, AEO probes against live answer engines, SerpAPI) and produces a **fix manifest** of concrete patches — not just a list of findings.
171
+
172
+ ```ts
173
+ import { orchestrate } from "@pseolint/core";
174
+
175
+ const { session, manifest, validation, diff } = await orchestrate({
176
+ domain: "https://example.com",
177
+ userId: "demo",
178
+ budget: { maxSessionUsd: 3 }, // optional; default $5
179
+ onEvent: (e) => console.log(e), // optional; SSE-friendly callback
180
+ });
181
+
182
+ if (session.reason === "completed") {
183
+ console.log(`Verdict: ${manifest!.verdict}`);
184
+ console.log(`${validation!.validPatches}/${validation!.totalPatches} patches valid`);
185
+ }
186
+ ```
187
+
188
+ **What you get:**
189
+ - `manifest` — `FixManifest` with verdict, category grades, page/template/domain patches (replace_h1, rewrite_meta, add_jsonld, add_faq_block, rewrite_intro, add_internal_link, remove_thin_block, robots_txt, sitemap_xml, canonical_strategy)
190
+ - `validation` — patch-by-patch `ManifestValidationReport`. Failed patches are dropped from the manifest before it returns; `failures` carries the location + reason.
191
+ - `diff` — `ManifestDiff` of structured `PatchDiff` objects (5 kinds — text_replace, html_insert, html_remove, file_replace, guidance) suitable for direct UI rendering.
192
+
193
+ **Architecture**: rules become tools the LLM calls. The LLM picks order. Budget caps (LLM tokens + external probe USD, pre-flight + reactive) bound spend. Watchdog injects a convergence reminder every N tool calls. AsyncLocalStorage-backed page cache means HTML never travels in conversation history — token cost stays bounded as audits scale.
194
+
195
+ **Lower-level exports** for callers who want individual pieces:
196
+
197
+ ```ts
198
+ import {
199
+ runOrchestrator, // direct runner — bring your own LanguageModel
200
+ orchestratorTools, // the 25-tool registry
201
+ defineTool, // helper to add custom tools
202
+ validateManifest, // walk a manifest, return per-failure report
203
+ diffManifest, // produce a structured-diff projection
204
+ manifestSchema, // Zod schema for FixManifest
205
+ buildSystemPrompt, // canonical orchestrator system prompt
206
+ DEFAULT_BUDGET, // BudgetCaps defaults
207
+ } from "@pseolint/core";
208
+ ```
209
+
210
+ External probes (`query_serp`, `ask_ai_engine`) read API keys from the call's `apiKey` arg or `SERPAPI_API_KEY` / `ANTHROPIC_API_KEY` / `PERPLEXITY_API_KEY` / `GOOGLE_GENERATIVE_AI_API_KEY` env vars.
211
+
212
+ ### Change-driven monitoring (v0.5)
213
+
214
+ When prior state exists, `auditSource` defaults to **monitoring mode**: the decision matrix decides which URLs to fetch BEFORE the network round-trip. URLs without change signals are skipped entirely; their findings are carried forward from prior state with `carriedForward: true` and `lastVerifiedAt` markers.
215
+
216
+ ```ts
217
+ import { planScrapeStrategy, CORE_RULESET_VERSION, DEFAULT_AGE_FLOOR_DAYS } from "@pseolint/core";
218
+
219
+ // The decision matrix is also exposed as a pure function for callers that
220
+ // want to plan their own fetches:
221
+ const plan = planScrapeStrategy({
222
+ candidateUrls,
223
+ priorState,
224
+ sitemapLastmodByUrl, // Map<url, ISO-string>
225
+ currentRulesetVersion: CORE_RULESET_VERSION,
226
+ ageFloorDays: DEFAULT_AGE_FLOOR_DAYS,
227
+ now: new Date(),
228
+ // Optional Pro-only inputs:
229
+ // gscDeltasByUrl, gscThresholds
230
+ });
231
+ // plan.refetch: Map<url, RefetchReason>
232
+ // plan.skip: Map<url, "unchanged">
233
+ ```
234
+
235
+ **Reasons** (first match wins): `new` → `age` → `ruleset` → `recheck` (warning/error/critical only — info findings carry forward) → `lastmod` → `gsc` → `no-signal` → else `unchanged`.
236
+
237
+ `AuditSummary.scrapePlan` reports `{ fetched, intended, carriedForward, reasonCounts, rulesetVersion, lastFullAuditAt }` — populated only on monitoring runs.
238
+
239
+ **Bump `CORE_RULESET_VERSION`** when shipping a new rule or materially changing rule logic so monitoring runs re-evaluate previously-skipped URLs against the new ruleset.
240
+
241
+ **Regression gating.** `state.exitOnRegression: true` flags a run where a new rule ID fires on any previously clean URL (`summary.hasRegression`). Carried-forward findings are excluded from the regression baseline so a regression on a skipped URL isn't masked by stale findings.
242
+
243
+ ### State schema v2
244
+
245
+ `UrlStateEntry` v2 stores full finding records (not just IDs) so future runs can carry them forward. Persists `lastModified`, `etag`, `sitemapLastmodAtAudit`, `rulesetVersion` per URL. `RunState` adds `lastFullAuditAt` and `rulesetVersion`. Existing v1 state files (v0.4) are discarded on read with a warning, triggering one baseline re-audit.
246
+
247
+ ### Caching
248
+
249
+ Setting `cache` enables an ETag/Last-Modified-aware disk cache for HTTP fetches. `summary.cacheStats` reports `{ hits, total, bytesSavedEstimate }`.
250
+
251
+ ### Page groups
252
+
253
+ Classify pages by glob and apply different rule subsets or threshold overrides per group. Results are surfaced in `summary.groupScores` / `summary.groupPageCounts`.
254
+
255
+ ### Rendering
256
+
257
+ For client-rendered pages, install `playwright-core` and pass `render: { browserWsEndpoint }` to connect to an existing browser endpoint.
258
+
259
+ ## Peer dependencies
260
+
261
+ All AI providers and `playwright-core` are optional peers — you only install the ones you actually use.
262
+
263
+ ## License
264
+
170
265
  MIT
@@ -0,0 +1,78 @@
1
+ import type { FixManifest } from "../orchestrator/finish-tool.js";
2
+ type PageChange = FixManifest["pages"][number]["changes"][number];
3
+ type DomainPatch = FixManifest["domainLevel"][number];
4
+ /**
5
+ * Structured patch diff. Each manifest change maps to one of these,
6
+ * which downstream consumers (web UI, CLI, GitHub-PR generator) render
7
+ * differently:
8
+ * - text_replace → before/after blocks side-by-side, copy-paste UI
9
+ * - html_insert → HTML snippet to paste into the named region
10
+ * - html_remove → CSS selector to delete
11
+ * - file_replace → full-file diff (robots.txt / sitemap.xml)
12
+ * - guidance → freeform prose (canonical_strategy)
13
+ *
14
+ * Kept deliberately narrow — not a unified-diff library. The manifest
15
+ * patches are paste-into-CMS-friendly outputs, not source-level diffs.
16
+ */
17
+ export type PatchDiff = {
18
+ kind: "text_replace";
19
+ field: "h1" | "title" | "meta_description" | "intro";
20
+ before: string;
21
+ after: string;
22
+ reason: string;
23
+ } | {
24
+ kind: "html_insert";
25
+ target: "head" | "body" | "after_h1";
26
+ description: string;
27
+ html: string;
28
+ reason: string;
29
+ } | {
30
+ kind: "html_remove";
31
+ selector: string;
32
+ reason: string;
33
+ } | {
34
+ kind: "file_replace";
35
+ path: string;
36
+ before: string | null;
37
+ after: string;
38
+ reason: string;
39
+ } | {
40
+ kind: "guidance";
41
+ topic: "canonical_strategy";
42
+ text: string;
43
+ reason: string;
44
+ };
45
+ /**
46
+ * Convert a single page-change into a `PatchDiff`. Pure transformation —
47
+ * does not validate (use `validatePageChange` for that). The two should
48
+ * be called in sequence at manifest finalization: validate first, drop
49
+ * failures, then diff the survivors.
50
+ */
51
+ export declare function diffPageChange(c: PageChange): PatchDiff;
52
+ /** Convert a single domain patch into a `PatchDiff`. */
53
+ export declare function diffDomainPatch(p: DomainPatch): PatchDiff;
54
+ export interface ManifestDiff {
55
+ pages: Array<{
56
+ url: string;
57
+ diffs: PatchDiff[];
58
+ }>;
59
+ templates: Array<{
60
+ templateId: string;
61
+ affectedUrlCount: number;
62
+ recommendation: string;
63
+ examples: Array<{
64
+ url: string;
65
+ diffs: PatchDiff[];
66
+ }>;
67
+ }>;
68
+ domainLevel: PatchDiff[];
69
+ }
70
+ /**
71
+ * Walk every patch in a manifest and produce a structured-diff projection.
72
+ * Mirrors the manifest's tree structure so the UI can render side-by-side
73
+ * (page list / template clusters / domain-level), each populated with
74
+ * already-typed patch diffs.
75
+ */
76
+ export declare function diffManifest(manifest: FixManifest): ManifestDiff;
77
+ export {};
78
+ //# sourceMappingURL=diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../../src/ai/manifest/diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAElE,KAAK,UAAU,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAClE,KAAK,WAAW,GAAG,WAAW,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtD;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,SAAS,GACjB;IACE,IAAI,EAAE,cAAc,CAAC;IACrB,KAAK,EAAE,IAAI,GAAG,OAAO,GAAG,kBAAkB,GAAG,OAAO,CAAC;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,oBAAoB,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAgBN;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,UAAU,GAAG,SAAS,CAmEvD;AAED,wDAAwD;AACxD,wBAAgB,eAAe,CAAC,CAAC,EAAE,WAAW,GAAG,SAAS,CA0BzD;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,EAAE,CAAA;KAAE,CAAC,CAAC;IAClD,SAAS,EAAE,KAAK,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,gBAAgB,EAAE,MAAM,CAAC;QACzB,cAAc,EAAE,MAAM,CAAC;QACvB,QAAQ,EAAE,KAAK,CAAC;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,SAAS,EAAE,CAAA;SAAE,CAAC,CAAC;KACtD,CAAC,CAAC;IACH,WAAW,EAAE,SAAS,EAAE,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,WAAW,GAAG,YAAY,CAiBhE"}