@warpgogol/werkstatt-shared 0.2.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 (403) hide show
  1. package/package.json +352 -0
  2. package/src/checks/index.ts +24 -0
  3. package/src/checks/lib/astro-site-url.ts +30 -0
  4. package/src/checks/lib/i18n.ts +30 -0
  5. package/src/checks/result-helpers.ts +102 -0
  6. package/src/checks/suppressions-config.ts +195 -0
  7. package/src/index.ts +14 -0
  8. package/src/integration/README.md +38 -0
  9. package/src/integration/adapters.ts +143 -0
  10. package/src/integration/crm-buffer.ts +491 -0
  11. package/src/integration/dispatch.ts +106 -0
  12. package/src/integration/funnel.ts +379 -0
  13. package/src/integration/index.ts +56 -0
  14. package/src/integration/lifecycle.ts +155 -0
  15. package/src/integration/orchestration.ts +439 -0
  16. package/src/integration/port-barrel.ts +124 -0
  17. package/src/integration/port.ts +143 -0
  18. package/src/integration/qstash.ts +134 -0
  19. package/src/integration/sharding.ts +133 -0
  20. package/src/integration/tests/deliver-event.test.ts +52 -0
  21. package/src/integration/tests/dispatch.test.ts +88 -0
  22. package/src/integration/tests/funnel-bridge.test.ts +46 -0
  23. package/src/integration/tests/hub-runtime.test.ts +128 -0
  24. package/src/integration/tests/lifecycle.test.ts +69 -0
  25. package/src/integration/tests/qstash.test.ts +83 -0
  26. package/src/integration/tests/sharding.test.ts +64 -0
  27. package/src/integration/vitest.config.ts +8 -0
  28. package/src/integration-adapter-supabase-crm/adapter.ts +437 -0
  29. package/src/integration-adapter-supabase-crm/client.ts +605 -0
  30. package/src/integration-adapter-supabase-crm/index.ts +28 -0
  31. package/src/integration-adapter-supabase-crm/pipedrive-sync-target.ts +482 -0
  32. package/src/integration-adapter-supabase-crm/supabase-rest.ts +89 -0
  33. package/src/integration-adapter-supabase-crm/tenant-registry.ts +334 -0
  34. package/src/integration-adapter-supabase-crm/tests/change-balance.pbt.test.ts +83 -0
  35. package/src/integration-adapter-supabase-crm/tests/funnel-persistence.test.ts +409 -0
  36. package/src/integration-adapter-supabase-crm/tests/lifecycle-sync.test.ts +288 -0
  37. package/src/integration-adapter-supabase-crm/tests/stage-map.test.ts +80 -0
  38. package/src/integration-adapter-supabase-crm/vitest.config.ts +8 -0
  39. package/src/integration-adapter-supabase-crm/worker.ts +223 -0
  40. package/src/observability/conventions.ts +93 -0
  41. package/src/observability/env-resolver.ts +57 -0
  42. package/src/observability/index.ts +57 -0
  43. package/src/observability/metric-registry.ts +184 -0
  44. package/src/observability/otlp-converter.ts +125 -0
  45. package/src/observability/otlp-json.ts +135 -0
  46. package/src/observability/otlp-transport.ts +68 -0
  47. package/src/observability/pusher.ts +142 -0
  48. package/src/observability/redact.ts +27 -0
  49. package/src/observability/tests/conventions.test.ts +100 -0
  50. package/src/observability/tests/metric-registry.test.ts +117 -0
  51. package/src/observability/tests/otlp-converter.pbt.test.ts +77 -0
  52. package/src/observability/tests/otlp-converter.test.ts +157 -0
  53. package/src/observability/tests/otlp-json.test.ts +120 -0
  54. package/src/observability/tests/pusher.test.ts +75 -0
  55. package/src/observability/tests/redact.pbt.test.ts +71 -0
  56. package/src/observability/tests/redact.test.ts +37 -0
  57. package/src/observability/typed-refs.ts +137 -0
  58. package/src/observability/vitest.config.ts +8 -0
  59. package/src/ontology/analytics/matomo-binding.yaml +172 -0
  60. package/src/ontology/analytics/matomo-fleet.registry.yaml +26 -0
  61. package/src/ontology/analytics/messkanon.yaml +151 -0
  62. package/src/ontology/archetype-registry.ts +61 -0
  63. package/src/ontology/archetypes/components/brand-label.yaml +19 -0
  64. package/src/ontology/archetypes/components/copyright.yaml +19 -0
  65. package/src/ontology/archetypes/components/currency-aware-price-display.yaml +19 -0
  66. package/src/ontology/archetypes/components/currency-selector.yaml +19 -0
  67. package/src/ontology/archetypes/components/footer-promo.yaml +19 -0
  68. package/src/ontology/archetypes/components/lang-switcher.yaml +19 -0
  69. package/src/ontology/archetypes/components/layout.yaml +19 -0
  70. package/src/ontology/archetypes/components/live-photo.yaml +20 -0
  71. package/src/ontology/archetypes/components/material-credit.yaml +20 -0
  72. package/src/ontology/archetypes/components/media.yaml +21 -0
  73. package/src/ontology/archetypes/components/nachweis-card.yaml +34 -0
  74. package/src/ontology/archetypes/components/nachweis-detail.yaml +43 -0
  75. package/src/ontology/archetypes/components/nachweis-list.yaml +37 -0
  76. package/src/ontology/archetypes/components/nachweis-verify.yaml +31 -0
  77. package/src/ontology/archetypes/components/not-found.yaml +22 -0
  78. package/src/ontology/archetypes/components/offer-capacity.yaml +21 -0
  79. package/src/ontology/archetypes/components/passport-header.yaml +19 -0
  80. package/src/ontology/archetypes/components/passport-provenance.yaml +20 -0
  81. package/src/ontology/archetypes/components/passport-score-grid.yaml +20 -0
  82. package/src/ontology/archetypes/components/passport-star-map.yaml +20 -0
  83. package/src/ontology/archetypes/components/person-profile.yaml +20 -0
  84. package/src/ontology/archetypes/components/pulsar.yaml +20 -0
  85. package/src/ontology/archetypes/components/responsive-image.yaml +19 -0
  86. package/src/ontology/archetypes/components/scroll-to-top.yaml +22 -0
  87. package/src/ontology/archetypes/components/social-meta.yaml +20 -0
  88. package/src/ontology/archetypes/components/structured-data.yaml +19 -0
  89. package/src/ontology/archetypes/index.json +1003 -0
  90. package/src/ontology/archetypes/index.yaml +821 -0
  91. package/src/ontology/archetypes/sections/approach.yaml +23 -0
  92. package/src/ontology/archetypes/sections/article-list.yaml +30 -0
  93. package/src/ontology/archetypes/sections/audience-cards.yaml +30 -0
  94. package/src/ontology/archetypes/sections/breadcrumbs.yaml +21 -0
  95. package/src/ontology/archetypes/sections/chat-widget.yaml +37 -0
  96. package/src/ontology/archetypes/sections/comparison-cards.yaml +31 -0
  97. package/src/ontology/archetypes/sections/controlled-responsibility-block.yaml +34 -0
  98. package/src/ontology/archetypes/sections/credits.yaml +29 -0
  99. package/src/ontology/archetypes/sections/donation-card.yaml +53 -0
  100. package/src/ontology/archetypes/sections/donation-use.yaml +23 -0
  101. package/src/ontology/archetypes/sections/dynamic-status-block.yaml +35 -0
  102. package/src/ontology/archetypes/sections/faq-list.yaml +26 -0
  103. package/src/ontology/archetypes/sections/final-cta.yaml +53 -0
  104. package/src/ontology/archetypes/sections/hero-decision-card.yaml +65 -0
  105. package/src/ontology/archetypes/sections/hero.yaml +50 -0
  106. package/src/ontology/archetypes/sections/impact.yaml +23 -0
  107. package/src/ontology/archetypes/sections/markdown.yaml +43 -0
  108. package/src/ontology/archetypes/sections/mountain-journey.yaml +43 -0
  109. package/src/ontology/archetypes/sections/navigation.yaml +20 -0
  110. package/src/ontology/archetypes/sections/notausgang-block.yaml +41 -0
  111. package/src/ontology/archetypes/sections/open-source-registry.yaml +26 -0
  112. package/src/ontology/archetypes/sections/ownership-block.yaml +57 -0
  113. package/src/ontology/archetypes/sections/people.yaml +48 -0
  114. package/src/ontology/archetypes/sections/price-card.yaml +50 -0
  115. package/src/ontology/archetypes/sections/problem.yaml +23 -0
  116. package/src/ontology/archetypes/sections/send-message.yaml +63 -0
  117. package/src/ontology/archetypes/sections/service-metadata-block.yaml +40 -0
  118. package/src/ontology/archetypes/sections/social-proof.yaml +27 -0
  119. package/src/ontology/archetypes/sections/transparency.yaml +32 -0
  120. package/src/ontology/archetypes/sections/trust-strip.yaml +42 -0
  121. package/src/ontology/archetypes/sections/video-section.yaml +27 -0
  122. package/src/ontology/archetypes/sections/women.yaml +29 -0
  123. package/src/ontology/biome-token-projection.ts +229 -0
  124. package/src/ontology/biomes/check-concrete-blueprint.yaml +118 -0
  125. package/src/ontology/biomes/handwerk-material-warm.yaml +135 -0
  126. package/src/ontology/biomes/nonprofit-trust.yaml +123 -0
  127. package/src/ontology/blueprints/offer.yaml +39 -0
  128. package/src/ontology/blueprints/prompts/local-market-signal.md +13 -0
  129. package/src/ontology/blueprints/prompts/website-local-narrative.md +24 -0
  130. package/src/ontology/blueprints/ratgeber.yaml +62 -0
  131. package/src/ontology/blueprints/website-local.yaml +309 -0
  132. package/src/ontology/blueprints/website-service.yaml +84 -0
  133. package/src/ontology/capabilities/lead.submit.yaml +44 -0
  134. package/src/ontology/cosmic/index.ts +48 -0
  135. package/src/ontology/cosmic/moon-catalog.ts +173 -0
  136. package/src/ontology/cosmic/planet-catalog.ts +82 -0
  137. package/src/ontology/cosmic/star-catalog.ts +337 -0
  138. package/src/ontology/enums.ts +159 -0
  139. package/src/ontology/external-surfaces/index.ts +158 -0
  140. package/src/ontology/external-surfaces/jsonld-types.yaml +135 -0
  141. package/src/ontology/external-surfaces/sitemap-shape.yaml +6 -0
  142. package/src/ontology/external-surfaces/url-schema.yaml +95 -0
  143. package/src/ontology/growth/events/contact-email-click.yaml +21 -0
  144. package/src/ontology/growth/events/contact-form-submit.yaml +25 -0
  145. package/src/ontology/growth/events/contact-phone-click.yaml +21 -0
  146. package/src/ontology/growth/events/contact-route-click.yaml +21 -0
  147. package/src/ontology/growth/events/contact-submit.yaml +28 -0
  148. package/src/ontology/growth/events/contact-whatsapp-click.yaml +21 -0
  149. package/src/ontology/growth/events/cta-click.yaml +30 -0
  150. package/src/ontology/growth/events/donation-confirmed.yaml +32 -0
  151. package/src/ontology/growth/events/donation-form-start.yaml +21 -0
  152. package/src/ontology/growth/events/donation-form-submit.yaml +29 -0
  153. package/src/ontology/growth/events/donation-intent.yaml +29 -0
  154. package/src/ontology/growth/events/form-error.yaml +27 -0
  155. package/src/ontology/growth/events/form-start.yaml +21 -0
  156. package/src/ontology/growth/events/form-submit.yaml +21 -0
  157. package/src/ontology/growth/events/outbound-click.yaml +25 -0
  158. package/src/ontology/growth/events/page-view.yaml +24 -0
  159. package/src/ontology/growth/events/passport-view.yaml +28 -0
  160. package/src/ontology/growth/events/star-map-navigate.yaml +30 -0
  161. package/src/ontology/index.ts +165 -0
  162. package/src/ontology/manifest.ts +344 -0
  163. package/src/ontology/operations/index.ts +1 -0
  164. package/src/ontology/schemas/biome.ts +303 -0
  165. package/src/ontology/schemas/capability.ts +104 -0
  166. package/src/ontology/schemas/constellation.ts +114 -0
  167. package/src/ontology/schemas/dns-records.ts +53 -0
  168. package/src/ontology/schemas/index.ts +95 -0
  169. package/src/ontology/schemas/manifest-resolver.ts +123 -0
  170. package/src/ontology/schemas/page-entry.ts +213 -0
  171. package/src/ontology/schemas/section-archetype.ts +109 -0
  172. package/src/ontology/schemas/site-family.ts +98 -0
  173. package/src/ontology/schemas/system/growth.ts +80 -0
  174. package/src/ontology/schemas/system/integrations.ts +143 -0
  175. package/src/ontology/schemas/system/manifest.ts +492 -0
  176. package/src/ontology/schemas/system/page-output.ts +99 -0
  177. package/src/ontology/schemas/system/release.ts +58 -0
  178. package/src/ontology/schemas/system/text.ts +58 -0
  179. package/src/ontology/schemas/system.ts +53 -0
  180. package/src/ontology/shared-section-props/body-fragments.ts +182 -0
  181. package/src/ontology/shared-section-props/catalog.ts +195 -0
  182. package/src/ontology/shared-section-props/common.ts +178 -0
  183. package/src/ontology/shared-section-props/index.ts +34 -0
  184. package/src/ontology/shared-section-props/visual-header.ts +163 -0
  185. package/src/ontology/site-families/charity-donation-trust/cultural-rules.yaml +6 -0
  186. package/src/ontology/site-families/charity-donation-trust/family.yaml +44 -0
  187. package/src/ontology/site-families/charity-donation-trust/linguistic-rules.yaml +6 -0
  188. package/src/ontology/site-families/check-quality-operator/cultural-rules.yaml +6 -0
  189. package/src/ontology/site-families/check-quality-operator/family.yaml +40 -0
  190. package/src/ontology/site-families/check-quality-operator/linguistic-rules.yaml +6 -0
  191. package/src/ontology/site-families/handwerk-trust-engineering/cultural-rules.yaml +6 -0
  192. package/src/ontology/site-families/handwerk-trust-engineering/family.yaml +53 -0
  193. package/src/ontology/site-families/handwerk-trust-engineering/linguistic-rules.yaml +6 -0
  194. package/src/ontology/tests/catalogs.pbt.test.ts +89 -0
  195. package/src/ontology/tests/enums.test.ts +203 -0
  196. package/src/ontology/tests/sternsystem-owner.test.ts +63 -0
  197. package/src/ontology/vitest.config.ts +8 -0
  198. package/src/passport/dht-sign.test.ts +99 -0
  199. package/src/passport/dht-sign.ts +73 -0
  200. package/src/passport/identity-sign.test.ts +134 -0
  201. package/src/passport/identity-sign.ts +85 -0
  202. package/src/passport/index.ts +53 -0
  203. package/src/passport/key-rotate.ts +133 -0
  204. package/src/passport/schema.ts +198 -0
  205. package/src/passport/sign.ts +225 -0
  206. package/src/passport/tests/schema.pbt.test.ts +93 -0
  207. package/src/passport/tests/schema.test.ts +222 -0
  208. package/src/passport/vitest.config.ts +8 -0
  209. package/src/share/agent/api-catalog.ts +77 -0
  210. package/src/share/agent/capability.ts +68 -0
  211. package/src/share/agent/dns-aid.test.ts +74 -0
  212. package/src/share/agent/dns-aid.ts +53 -0
  213. package/src/share/agent/fleet-catalog.ts +149 -0
  214. package/src/share/agent/index.ts +29 -0
  215. package/src/share/agent/knowledge.ts +96 -0
  216. package/src/share/agent/manifest.ts +126 -0
  217. package/src/share/agent/mcp-card.ts +63 -0
  218. package/src/share/agent/omit-empty.ts +77 -0
  219. package/src/share/agent/openapi.ts +157 -0
  220. package/src/share/agent/proof.ts +61 -0
  221. package/src/share/attribution-display.ts +86 -0
  222. package/src/share/check-hints.ts +38 -0
  223. package/src/share/content/entity-id.ts +65 -0
  224. package/src/share/content/index.ts +16 -0
  225. package/src/share/content/merge.ts +86 -0
  226. package/src/share/content/resolve-field-path.ts +39 -0
  227. package/src/share/content/substitute-deep.ts +47 -0
  228. package/src/share/content-discipline/index.ts +14 -0
  229. package/src/share/content-discipline/parsers.ts +66 -0
  230. package/src/share/content-discipline/types.ts +118 -0
  231. package/src/share/content-reference.ts +333 -0
  232. package/src/share/counter-utils.ts +75 -0
  233. package/src/share/css-value-normalize.ts +48 -0
  234. package/src/share/dev-props-validator.ts +131 -0
  235. package/src/share/entitlement.ts +172 -0
  236. package/src/share/env.d.ts +73 -0
  237. package/src/share/feature-policy.ts +367 -0
  238. package/src/share/formula-eval.ts +364 -0
  239. package/src/share/fs/index.ts +96 -0
  240. package/src/share/i18n/localization.ts +57 -0
  241. package/src/share/image-provider.ts +314 -0
  242. package/src/share/index.ts +86 -0
  243. package/src/share/knowledge/claim.ts +129 -0
  244. package/src/share/knowledge/derivation.ts +55 -0
  245. package/src/share/knowledge/freshness.ts +127 -0
  246. package/src/share/knowledge/index.ts +23 -0
  247. package/src/share/knowledge/ledger.ts +222 -0
  248. package/src/share/knowledge/plan.ts +122 -0
  249. package/src/share/knowledge/source.ts +170 -0
  250. package/src/share/legal/translation-policy.ts +133 -0
  251. package/src/share/material-credits.ts +181 -0
  252. package/src/share/middleware/language-redirect.ts +49 -0
  253. package/src/share/offer-capacity.ts +149 -0
  254. package/src/share/onboarding-yaml/index.ts +162 -0
  255. package/src/share/page.ts +351 -0
  256. package/src/share/redirects.test.ts +62 -0
  257. package/src/share/redirects.ts +54 -0
  258. package/src/share/rfc0042-utils.ts +95 -0
  259. package/src/share/runtime-context.ts +70 -0
  260. package/src/share/schemas/claim-records.ts +69 -0
  261. package/src/share/schemas/claims.ts +150 -0
  262. package/src/share/schemas/effects.ts +212 -0
  263. package/src/share/schemas/features.ts +147 -0
  264. package/src/share/schemas/horizontal-align.ts +22 -0
  265. package/src/share/schemas/icon-color.ts +27 -0
  266. package/src/share/schemas/index.ts +43 -0
  267. package/src/share/schemas/live-photo.ts +53 -0
  268. package/src/share/schemas/material-credit.ts +292 -0
  269. package/src/share/schemas/media.ts +220 -0
  270. package/src/share/schemas/navigation.ts +74 -0
  271. package/src/share/schemas/page-base.ts +33 -0
  272. package/src/share/schemas/participant.ts +247 -0
  273. package/src/share/schemas/person.ts +24 -0
  274. package/src/share/schemas/print.ts +93 -0
  275. package/src/share/schemas/section-background.ts +106 -0
  276. package/src/share/schemas/section-body.ts +138 -0
  277. package/src/share/schemas/section-cards.ts +37 -0
  278. package/src/share/schemas/section-cta.ts +74 -0
  279. package/src/share/schemas/section-header.ts +77 -0
  280. package/src/share/schemas/section-image.ts +77 -0
  281. package/src/share/schemas/section-motion.ts +52 -0
  282. package/src/share/schemas/section-shell.ts +63 -0
  283. package/src/share/schemas/section-stats.ts +37 -0
  284. package/src/share/schemas/site-background.ts +78 -0
  285. package/src/share/schemas/standard-list-item.ts +38 -0
  286. package/src/share/scripts/external-links.ts +45 -0
  287. package/src/share/scripts/gsap-counter.ts +168 -0
  288. package/src/share/scripts/gsap-mountain-journey.ts +236 -0
  289. package/src/share/scripts/gsap-parallax.ts +50 -0
  290. package/src/share/scripts/gsap-reveal.ts +65 -0
  291. package/src/share/scripts/gsap-stagger.ts +54 -0
  292. package/src/share/scripts/index.ts +25 -0
  293. package/src/share/scripts/inline-number-animation.ts +120 -0
  294. package/src/share/scripts/lenis.ts +109 -0
  295. package/src/share/scripts/live-photos.ts +250 -0
  296. package/src/share/scripts/lordicon.ts +94 -0
  297. package/src/share/scripts/orchestrator.ts +138 -0
  298. package/src/share/scripts/scheduler.ts +53 -0
  299. package/src/share/scripts/video-player.ts +116 -0
  300. package/src/share/semantic/ai.ts +140 -0
  301. package/src/share/semantic/block-extraction.ts +52 -0
  302. package/src/share/semantic/block-extractors/index.ts +502 -0
  303. package/src/share/semantic/breadcrumbs.ts +120 -0
  304. package/src/share/semantic/build-page.ts +309 -0
  305. package/src/share/semantic/business-projection.ts +278 -0
  306. package/src/share/semantic/derived-prices-loader.ts +36 -0
  307. package/src/share/semantic/extract.ts +118 -0
  308. package/src/share/semantic/feed.ts +124 -0
  309. package/src/share/semantic/ids.ts +76 -0
  310. package/src/share/semantic/image-sitemap.ts +153 -0
  311. package/src/share/semantic/index.ts +36 -0
  312. package/src/share/semantic/jsonld/article.ts +47 -0
  313. package/src/share/semantic/jsonld/breadcrumb.ts +47 -0
  314. package/src/share/semantic/jsonld/collection-list.ts +30 -0
  315. package/src/share/semantic/jsonld/context.ts +46 -0
  316. package/src/share/semantic/jsonld/faq.ts +49 -0
  317. package/src/share/semantic/jsonld/initiative.ts +53 -0
  318. package/src/share/semantic/jsonld/organization.ts +101 -0
  319. package/src/share/semantic/jsonld/person.ts +71 -0
  320. package/src/share/semantic/jsonld/service.ts +80 -0
  321. package/src/share/semantic/jsonld/shared.ts +31 -0
  322. package/src/share/semantic/jsonld/types.ts +18 -0
  323. package/src/share/semantic/jsonld/webpage.ts +98 -0
  324. package/src/share/semantic/jsonld/website.ts +27 -0
  325. package/src/share/semantic/jsonld.ts +75 -0
  326. package/src/share/semantic/llms-policy.ts +74 -0
  327. package/src/share/semantic/llms.ts +394 -0
  328. package/src/share/semantic/markdown-hygiene.ts +68 -0
  329. package/src/share/semantic/markdown-twin-provenance.ts +242 -0
  330. package/src/share/semantic/models.ts +402 -0
  331. package/src/share/semantic/organization-profile.ts +160 -0
  332. package/src/share/semantic/output-projection.ts +131 -0
  333. package/src/share/semantic/page-builders/markdown-page.ts +84 -0
  334. package/src/share/semantic/page-markdown.ts +234 -0
  335. package/src/share/semantic/page-utils.ts +125 -0
  336. package/src/share/semantic/price-marker-resolver.ts +92 -0
  337. package/src/share/semantic/robots.ts +127 -0
  338. package/src/share/semantic/update-stamp.ts +136 -0
  339. package/src/share/shared-context.ts +177 -0
  340. package/src/share/string-utils.ts +19 -0
  341. package/src/share/tests/api-catalog.test.ts +97 -0
  342. package/src/share/tests/breadcrumbs.test.ts +126 -0
  343. package/src/share/tests/build-page-price-markers.test.ts +160 -0
  344. package/src/share/tests/capability.test.ts +107 -0
  345. package/src/share/tests/claim.test.ts +90 -0
  346. package/src/share/tests/content-ref-index.test.ts +328 -0
  347. package/src/share/tests/create-dev-props-validator.test.ts +38 -0
  348. package/src/share/tests/derivation.test.ts +39 -0
  349. package/src/share/tests/dev-props-validator.test.ts +88 -0
  350. package/src/share/tests/fleet-catalog.test.ts +134 -0
  351. package/src/share/tests/formula-eval.test.ts +619 -0
  352. package/src/share/tests/freshness.test.ts +60 -0
  353. package/src/share/tests/hero-section-numbering-parity.test.ts +51 -0
  354. package/src/share/tests/image-sitemap.test.ts +101 -0
  355. package/src/share/tests/jsonld-service.test.ts +282 -0
  356. package/src/share/tests/knowledge.test.ts +86 -0
  357. package/src/share/tests/ledger.test.ts +125 -0
  358. package/src/share/tests/llms-0789.test.ts +63 -0
  359. package/src/share/tests/manifest.test.ts +100 -0
  360. package/src/share/tests/markdown-twin-provenance.test.ts +108 -0
  361. package/src/share/tests/mcp-card.test.ts +57 -0
  362. package/src/share/tests/onboarding-yaml.test.ts +111 -0
  363. package/src/share/tests/ontology-role-by-cosmic-name.test.ts +28 -0
  364. package/src/share/tests/openapi.test.ts +116 -0
  365. package/src/share/tests/organization-jsonld.test.ts +74 -0
  366. package/src/share/tests/output-projection.test.ts +61 -0
  367. package/src/share/tests/plan.test.ts +97 -0
  368. package/src/share/tests/price-marker-resolver.test.ts +141 -0
  369. package/src/share/tests/proof.test.ts +72 -0
  370. package/src/share/tests/resolve-field-path.test.ts +52 -0
  371. package/src/share/tests/source.test.ts +81 -0
  372. package/src/share/tests/substitute-deep.test.ts +65 -0
  373. package/src/share/tests/text-normalize.pbt.test.ts +143 -0
  374. package/src/share/tests/text-normalize.test.ts +285 -0
  375. package/src/share/tests/twin-path.test.ts +43 -0
  376. package/src/share/text-normalize.ts +548 -0
  377. package/src/share/text-position.ts +19 -0
  378. package/src/share/types/hls-js-light.d.ts +16 -0
  379. package/src/share/visibility.ts +167 -0
  380. package/src/share/vitest.config.ts +8 -0
  381. package/src/share/wrap-inline-numbers.ts +139 -0
  382. package/src/surface/blueprint-schema.ts +314 -0
  383. package/src/surface/blueprint-types.ts +256 -0
  384. package/src/surface/blueprint.ts +300 -0
  385. package/src/surface/breaker.ts +57 -0
  386. package/src/surface/decision-composer.ts +156 -0
  387. package/src/surface/eligibility.ts +372 -0
  388. package/src/surface/evidence-records.ts +97 -0
  389. package/src/surface/fleet.ts +60 -0
  390. package/src/surface/geo.ts +170 -0
  391. package/src/surface/governance.ts +146 -0
  392. package/src/surface/index.ts +216 -0
  393. package/src/surface/io/index.ts +18 -0
  394. package/src/surface/io/surface-module-context-io.ts +60 -0
  395. package/src/surface/io/visibility-outcomes-io.ts +46 -0
  396. package/src/surface/module-context.ts +120 -0
  397. package/src/surface/substance.ts +161 -0
  398. package/src/surface/tests/eligibility.pbt.test.ts +137 -0
  399. package/src/surface/tests/eligibility.test.ts +302 -0
  400. package/src/surface/types.ts +289 -0
  401. package/src/surface/visibility.ts +57 -0
  402. package/src/surface/vitest.config.ts +8 -0
  403. package/tsconfig.json +17 -0
@@ -0,0 +1,74 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>RFC-0142: pure resolution of per-page llms inclusion depth. Maps the raw `output.llms` declaration from system.md to an effective SemanticLlmsPolicy, applying a conservative default-by-semanticType map.</purpose>
4
+ <non-goals>
5
+ <item>Do not read files or content — pure function only.</item>
6
+ <item>Do not format llms output — that lives in llms.ts.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0142: Initial implementation.</item>
11
+ <item>RFC-0328: Added legal-page default llms depth "exclude".</item>
12
+ </CHANGE_SUMMARY>
13
+ */
14
+
15
+ import type { SemanticLlmsDepth, SemanticLlmsPolicy } from "./models.ts";
16
+
17
+ /** Raw per-page llms declaration as authored in system.md (`output.llms`). */
18
+ export type RawLlmsPolicy =
19
+ SemanticLlmsDepth | { depth?: SemanticLlmsDepth; sections?: { exclude?: string[] } };
20
+
21
+ const VALID_DEPTHS: ReadonlySet<string> = new Set<SemanticLlmsDepth>([
22
+ "full",
23
+ "summary",
24
+ "index-only",
25
+ "exclude",
26
+ ]);
27
+
28
+ /**
29
+ * RFC-0142 / RFC-0328: conservative default depth by semanticType. `openSource`
30
+ * is unambiguously low-signal across all apps and is demoted by default. Legal
31
+ * pages are first-class statutory pages and are excluded from the public AI
32
+ * feed by default.
33
+ */
34
+ export const LLMS_DEPTH_BY_SEMANTIC_TYPE: Readonly<Record<string, SemanticLlmsDepth>> = {
35
+ openSource: "index-only",
36
+ legal: "exclude",
37
+ };
38
+
39
+ const DEFAULT_DEPTH: SemanticLlmsDepth = "full";
40
+
41
+ function isValidDepth(value: unknown): value is SemanticLlmsDepth {
42
+ return typeof value === "string" && VALID_DEPTHS.has(value);
43
+ }
44
+
45
+ /**
46
+ * Resolve the effective llms policy for a page.
47
+ *
48
+ * Precedence: explicit `output.llms` (string or object) > default-by-type > `full`.
49
+ * An unrecognized explicit depth falls back to the type default (callers may warn).
50
+ */
51
+ export function resolveLlmsPolicy(
52
+ raw: RawLlmsPolicy | undefined,
53
+ semanticType: string,
54
+ ): SemanticLlmsPolicy {
55
+ const typeDefault = LLMS_DEPTH_BY_SEMANTIC_TYPE[semanticType] ?? DEFAULT_DEPTH;
56
+
57
+ if (raw === undefined || raw === null) {
58
+ return { depth: typeDefault };
59
+ }
60
+
61
+ // String shorthand: `output.llms: index-only`
62
+ if (typeof raw === "string") {
63
+ return { depth: isValidDepth(raw) ? raw : typeDefault };
64
+ }
65
+
66
+ // Object form: `output.llms: { depth, sections }`
67
+ const depth = isValidDepth(raw.depth) ? raw.depth : typeDefault;
68
+ const excludeIds = raw.sections?.exclude?.filter((id) => typeof id === "string" && id.length > 0);
69
+
70
+ return {
71
+ depth,
72
+ ...(excludeIds && excludeIds.length > 0 ? { sections: { exclude: excludeIds } } : {}),
73
+ };
74
+ }
@@ -0,0 +1,394 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Maintains packages/share/src/semantic/llms.ts as an authored share authored module so agents can evolve it without rediscovering local boundaries.</purpose>
4
+ <non-goals>
5
+ <item>Do not parse raw content or validate input models.</item>
6
+ <item>Do not manage external data fetching.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>Added full llms.txt generation and optional title-aware page labels.</item>
11
+ <item>RFC-0184: canonical Markdown links, blockquoted summary, absolute URLs, llms-full.txt reference, and empty-section filtering.</item>
12
+ <item>RFC-0372: formatBlocks reads from unified page.blocks instead of answerBlocks + contentBlocks.</item>
13
+ </CHANGE_SUMMARY>
14
+ */
15
+
16
+ import { toPathname } from "./ids.ts";
17
+ import {
18
+ canonicalizeGeneratedMarkdownText,
19
+ formatGeneratedMarkdownListItem,
20
+ normalizeGeneratedMarkdownText,
21
+ } from "./markdown-hygiene.ts";
22
+ import type { SemanticLlmsDepth, SemanticPageModel, SemanticSiteModel } from "./models.ts";
23
+
24
+ function canonicalStaticUrl(artifactPath: string, opts: { baseUrl: string }): string {
25
+ const base = opts.baseUrl.replace(/\/+$/, "");
26
+ const clean = artifactPath.replace(/^\/+/, "");
27
+ return clean === "" ? `${base}/` : `${base}/${clean}`;
28
+ }
29
+
30
+ /** RFC-0142: effective llms depth for a page. Defaults to `full` when unresolved. */
31
+ function pageDepth(page: SemanticPageModel): SemanticLlmsDepth {
32
+ return page.output?.llms?.depth ?? "full";
33
+ }
34
+
35
+ /** RFC-0142: pages that contribute a row to the llms.txt index. */
36
+ function inIndex(page: SemanticPageModel): boolean {
37
+ return pageDepth(page) !== "exclude";
38
+ }
39
+
40
+ /** RFC-0142: pages that contribute a body section to llms-full.txt. */
41
+ function inFull(page: SemanticPageModel): boolean {
42
+ const depth = pageDepth(page);
43
+ return depth === "full" || depth === "summary";
44
+ }
45
+
46
+ function pageLabel(page: SemanticPageModel): string {
47
+ return page.heading ?? page.title;
48
+ }
49
+
50
+ function pageSummary(page: SemanticPageModel): string {
51
+ return canonicalizePageText(page, page.lead ?? page.description);
52
+ }
53
+
54
+ function canonicalizePageText(page: SemanticPageModel, text: string | undefined): string {
55
+ return canonicalizeGeneratedMarkdownText(text, {
56
+ baseUrl: page.url ? new URL(page.url).origin : undefined,
57
+ defaultLanguage: page.defaultLanguage,
58
+ });
59
+ }
60
+
61
+ function formatBlocks(page: SemanticPageModel): string {
62
+ const excludeIds = new Set(page.output?.llms?.sections?.exclude ?? []);
63
+ return page.blocks
64
+ .filter((block) => block.heading && !excludeIds.has(block.id))
65
+ .filter((block) => block.summary || block.body || block.facts?.length || block.items?.length)
66
+ .map((block) => {
67
+ const parts = [`### ${block.heading}`];
68
+ if (block.summary) {
69
+ parts.push(canonicalizePageText(page, block.summary));
70
+ }
71
+ if (block.body) {
72
+ parts.push(canonicalizePageText(page, block.body));
73
+ }
74
+ if (block.facts?.length) {
75
+ parts.push(
76
+ ...block.facts.flatMap((fact) =>
77
+ formatGeneratedMarkdownListItem(canonicalizePageText(page, fact)),
78
+ ),
79
+ );
80
+ }
81
+ if (block.items?.length) {
82
+ for (const item of block.items) {
83
+ const desc = item.description ? `: ${item.description}` : "";
84
+ parts.push(`- ${item.title}${desc}`);
85
+ }
86
+ }
87
+ return parts.join("\n\n");
88
+ })
89
+ .join("\n\n");
90
+ }
91
+
92
+ function formatPeople(page: SemanticPageModel): string {
93
+ if (!page.people?.length) {
94
+ return "";
95
+ }
96
+
97
+ return [
98
+ "### People",
99
+ ...page.people.map((person) => {
100
+ const details = [person.role, person.description].filter(Boolean).join(" — ");
101
+ return details ? `- ${person.name}: ${details}` : `- ${person.name}`;
102
+ }),
103
+ ].join("\n");
104
+ }
105
+
106
+ function formatInitiatives(page: SemanticPageModel): string {
107
+ if (!page.initiatives?.length) {
108
+ return "";
109
+ }
110
+
111
+ return [
112
+ "### Initiatives",
113
+ ...page.initiatives.map((initiative) => {
114
+ const facts = initiative.facts?.length ? ` (${initiative.facts.join("; ")})` : "";
115
+ return `- ${initiative.name}: ${initiative.summary}${facts}`;
116
+ }),
117
+ ].join("\n");
118
+ }
119
+
120
+ function formatOrganizationFacts(site: SemanticSiteModel): string[] {
121
+ return [
122
+ `- Name: ${site.organization.legalName ?? site.organization.name}`,
123
+ ...(site.organization.foundingYear ? [`- Founded: ${site.organization.foundingYear}`] : []),
124
+ ...(site.organization.registration
125
+ ? [`- Registration: ${site.organization.registration}`]
126
+ : []),
127
+ ...(site.organization.representative
128
+ ? [`- Representative: ${site.organization.representative}`]
129
+ : []),
130
+ ...(site.organization.email ? [`- Email: ${site.organization.email}`] : []),
131
+ ...(site.organization.areaServed?.length
132
+ ? [`- Areas served: ${site.organization.areaServed.join("; ")}`]
133
+ : []),
134
+ ...(site.organization.address
135
+ ? [
136
+ `- Address: ${[
137
+ site.organization.address.streetAddress,
138
+ site.organization.address.postalCode,
139
+ site.organization.address.addressLocality,
140
+ site.organization.address.addressCountry,
141
+ ]
142
+ .filter(Boolean)
143
+ .join(", ")}`,
144
+ ]
145
+ : []),
146
+ ...(site.organization.founders?.length
147
+ ? [`- Founders: ${site.organization.founders.map((person) => person.name).join("; ")}`]
148
+ : []),
149
+ ...(site.organization.boardMembers?.length
150
+ ? [
151
+ `- Board: ${site.organization.boardMembers
152
+ .map((person) => (person.role ? `${person.role} — ${person.name}` : person.name))
153
+ .join("; ")}`,
154
+ ]
155
+ : []),
156
+ ...(site.organization.contactPoints?.length
157
+ ? [
158
+ `- Contact points: ${site.organization.contactPoints
159
+ .map((contactPoint) =>
160
+ [contactPoint.contactType, contactPoint.email].filter(Boolean).join(" — "),
161
+ )
162
+ .join("; ")}`,
163
+ ]
164
+ : []),
165
+ ...(site.organization.donationAccount
166
+ ? (() => {
167
+ const { accountHolder, bankName, iban, bic } = site.organization.donationAccount;
168
+ const hasReal = (v: string | undefined) =>
169
+ typeof v === "string" && v.length > 0 && !v.startsWith("NEED_THIS_");
170
+ if (!hasReal(iban) || !hasReal(bic)) {
171
+ return [];
172
+ }
173
+ const parts = [accountHolder, bankName, iban, bic].filter(hasReal);
174
+ return [`- Donation account: ${parts.join("; ")}`];
175
+ })()
176
+ : []),
177
+ ];
178
+ }
179
+
180
+ /** RFC-0147: render the projected offer (prices + written guarantees + growth modules + terms). */
181
+ function formatOffer(site: SemanticSiteModel): string[] {
182
+ const offer = site.organization.offer;
183
+ if (
184
+ !offer ||
185
+ (!offer.prices?.length &&
186
+ !offer.guarantees?.length &&
187
+ !offer.growthModules?.length &&
188
+ !offer.changePrice &&
189
+ !offer.hourlyRate &&
190
+ !offer.billingDay)
191
+ ) {
192
+ return [];
193
+ }
194
+ const lines: string[] = ["## Offer"];
195
+ if (offer.prices?.length) {
196
+ lines.push(...offer.prices.map((price) => `- ${price.label}: ${price.amount}`));
197
+ }
198
+ if (offer.changePrice || offer.hourlyRate || offer.billingDay) {
199
+ const terms: string[] = [];
200
+ if (offer.changePrice) terms.push(`- Change price: ${offer.changePrice} EUR per change`);
201
+ if (offer.hourlyRate) terms.push(`- Hourly rate: ${offer.hourlyRate} EUR per hour`);
202
+ if (offer.billingDay) terms.push(`- Billing day: ${offer.billingDay}`);
203
+ lines.push("", "### Terms", ...terms);
204
+ }
205
+ if (offer.guarantees?.length) {
206
+ lines.push("", "### Guarantees");
207
+ lines.push(
208
+ ...offer.guarantees.map((g) => (g.detail ? `- ${g.label}: ${g.detail}` : `- ${g.label}`)),
209
+ );
210
+ }
211
+ if (offer.growthModules?.length) {
212
+ lines.push("", "### Growth modules");
213
+ lines.push(
214
+ ...offer.growthModules.map((m) => {
215
+ const desc = m.description
216
+ ? `: ${normalizeGeneratedMarkdownText(m.description).replace(/\n+/g, " ")}`
217
+ : "";
218
+ const price = m.price
219
+ ? ` (${normalizeGeneratedMarkdownText(m.price).replace(/\n+/g, " ")})`
220
+ : "";
221
+ return `- ${m.label}${desc}${price}`;
222
+ }),
223
+ );
224
+ }
225
+ if (offer.capacity) {
226
+ lines.push(
227
+ "",
228
+ "### Capacity policy",
229
+ `- Wave cadence: ${offer.capacity.cadence}`,
230
+ `- Starts at: ${offer.capacity.startsAt}`,
231
+ `- Timezone: ${offer.capacity.timezone}`,
232
+ `- Slot range: ${offer.capacity.slotRange.min}-${offer.capacity.slotRange.max}`,
233
+ `- Max slots per wave: ${offer.capacity.maxSlotsPerWave}`,
234
+ `- Availability status: ${offer.capacity.availabilityStatus}`,
235
+ );
236
+ }
237
+ lines.push("");
238
+ return lines;
239
+ }
240
+
241
+ /** RFC-0373: render the projected business service catalog. */
242
+ function formatServices(site: SemanticSiteModel): string[] {
243
+ const services = site.organization.services;
244
+ if (!services?.length) return [];
245
+ const lines: string[] = ["## Services"];
246
+ for (const service of services) {
247
+ const desc = service.description ? `: ${service.description}` : "";
248
+ lines.push(`- ${service.name}${desc}`);
249
+ }
250
+ lines.push("");
251
+ return lines;
252
+ }
253
+
254
+ /** RFC-0148: render the projected location (NAP + service area). */
255
+ function formatLocation(site: SemanticSiteModel): string[] {
256
+ const location = site.organization.location;
257
+ if (!location) return [];
258
+ const facts = [
259
+ ...(location.locality ? [`- Locality: ${location.locality}`] : []),
260
+ ...(location.region ? [`- Region: ${location.region}`] : []),
261
+ ...(location.country ? [`- Country: ${location.country}`] : []),
262
+ ...(location.serviceArea?.length ? [`- Service area: ${location.serviceArea.join("; ")}`] : []),
263
+ ];
264
+ if (facts.length === 0) return [];
265
+ return ["## Location", ...facts, ""];
266
+ }
267
+
268
+ /** RFC-0148: render the projected public team (name, role, bio). */
269
+ function formatTeam(site: SemanticSiteModel): string[] {
270
+ const team = site.organization.team;
271
+ if (!team?.length) return [];
272
+ const lines: string[] = ["## Team"];
273
+ for (const member of team) {
274
+ const role = member.role ? ` (${member.role})` : "";
275
+ const bio = member.description
276
+ ? `: ${member.description.replace(/\s*\n\s*/g, " ").trim()}`
277
+ : "";
278
+ lines.push(`- ${member.name}${role}${bio}`);
279
+ }
280
+ lines.push("");
281
+ return lines;
282
+ }
283
+
284
+ function toAbsoluteUrl(site: SemanticSiteModel, pathname: string): string {
285
+ const rawBase = site.organization.url ?? site.baseUrl;
286
+ const base = (() => {
287
+ try {
288
+ return new URL(rawBase).origin;
289
+ } catch {
290
+ return rawBase.replace(/\/$/, "");
291
+ }
292
+ })();
293
+ const defaultLang = site.defaultLanguage ?? "de";
294
+ const normalizedPathname =
295
+ site.lang === defaultLang
296
+ ? pathname.replace(new RegExp(`^/${defaultLang}(?=/|$)`), "") || "/"
297
+ : pathname;
298
+ // RFC-0317: ensure trailing slash to match canonicalPageUrl output.
299
+ const withTrailingSlash =
300
+ normalizedPathname === "/" ? "/" : normalizedPathname.replace(/\/?$/, "/");
301
+ return `${base}${withTrailingSlash}`;
302
+ }
303
+
304
+ /** RFC-0184: Format a Markdown link row for llms.txt. */
305
+ function formatMarkdownLinkRow(site: SemanticSiteModel, page: SemanticPageModel): string {
306
+ const title = pageLabel(page);
307
+ const url = toAbsoluteUrl(site, toPathname(page.url));
308
+ const summary = pageSummary(page);
309
+ return `- [${title}](${url}): ${summary}`;
310
+ }
311
+
312
+ export function buildLlmsIndex(site: SemanticSiteModel): string {
313
+ const indexPages = site.pages.filter(inIndex);
314
+ const llmsFullUrl = canonicalStaticUrl("/llms-full.txt", { baseUrl: site.baseUrl });
315
+ const siteDescription = site.organization.description ?? "";
316
+
317
+ // RFC-0789: agent discovery links — omitted when agent.enabled is false.
318
+ const agentEnabled = site.agent?.enabled !== false;
319
+ const agentLinks = agentEnabled
320
+ ? [
321
+ `> Machine-readable Agent Surface (structured knowledge + capabilities): [agent.json](${canonicalStaticUrl("/.well-known/agent.json", { baseUrl: site.baseUrl })}).`,
322
+ `> API discovery catalog (RFC 9727): [api-catalog](${canonicalStaticUrl("/.well-known/api-catalog", { baseUrl: site.baseUrl })}).`,
323
+ `> MCP Server Card (SEP-1649): [server-card.json](${canonicalStaticUrl("/.well-known/mcp/server-card.json", { baseUrl: site.baseUrl })}).`,
324
+ `> OpenAPI 3.1 specification: [agent.openapi.json](${canonicalStaticUrl("/.well-known/agent.openapi.json", { baseUrl: site.baseUrl })}).`,
325
+ ]
326
+ : [];
327
+
328
+ // RFC-0184: canonical Markdown link rows with absolute URLs
329
+ const primarySources = indexPages.map((page) => formatMarkdownLinkRow(site, page));
330
+
331
+ return [
332
+ `# ${site.organization.name}`,
333
+ // RFC-0184: blockquoted site description with llms-full.txt reference
334
+ ...(siteDescription ? [`> ${siteDescription}`] : []),
335
+ `> For complete documentation in a single file, see [llms-full.txt](${llmsFullUrl}).`,
336
+ ...agentLinks,
337
+ "",
338
+ "## Primary sources",
339
+ ...primarySources,
340
+ "",
341
+ "## Organization",
342
+ ...formatOrganizationFacts(site),
343
+ ].join("\n");
344
+ }
345
+
346
+ export function buildLlmsFull(site: SemanticSiteModel): string {
347
+ const pages = site.pages
348
+ .filter(inFull)
349
+ .map((page) => {
350
+ // RFC-0184: absolute URL in the header
351
+ const header = [
352
+ `## ${pageLabel(page)}`,
353
+ `URL: ${toAbsoluteUrl(site, toPathname(page.url))}`,
354
+ `Description: ${canonicalizePageText(page, page.description)}`,
355
+ ...(page.lead ? [`Lead: ${canonicalizePageText(page, page.lead)}`] : []),
356
+ ];
357
+
358
+ // RFC-0142: `summary` depth emits only the page header (description / lead),
359
+ // skipping the answer-block dump and entity sections.
360
+ if (pageDepth(page) === "summary") {
361
+ return header.join("\n");
362
+ }
363
+
364
+ // RFC-0184: filter empty body sections to avoid blank placeholder gaps
365
+ const bodySections = [formatBlocks(page), formatPeople(page), formatInitiatives(page)].filter(
366
+ (s) => s.length > 0,
367
+ );
368
+
369
+ if (bodySections.length === 0) {
370
+ return header.join("\n");
371
+ }
372
+
373
+ return [...header, "", ...bodySections].join("\n");
374
+ })
375
+ .join("\n\n");
376
+
377
+ // RFC-0184: filter empty organization-level sections
378
+ const orgSections = [
379
+ ...formatOffer(site),
380
+ ...formatServices(site),
381
+ ...formatLocation(site),
382
+ ...formatTeam(site),
383
+ ].filter((s) => s.length > 0);
384
+
385
+ return [
386
+ `# ${site.organization.name}`,
387
+ site.organization.description,
388
+ "",
389
+ "## Organization facts",
390
+ ...formatOrganizationFacts(site),
391
+ ...(orgSections.length > 0 ? ["", ...orgSections] : []),
392
+ ...(pages.length > 0 ? ["", pages] : []),
393
+ ].join("\n");
394
+ }
@@ -0,0 +1,68 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Normalize generated Markdown fragments before they enter public text artifacts.</purpose>
4
+ <non-goals>
5
+ <item>Do not parse or reformat arbitrary Markdown documents.</item>
6
+ <item>Do not mutate source content records.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0316: Added public text artifact Markdown hygiene helpers.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ function normalizeLine(line: string): string {
15
+ return line
16
+ .replace(/^- ---\s*$/, "---")
17
+ .replace(/^- -\s+/, "- ")
18
+ .replace(/^- (#{1,6})\s+/, "$1 ")
19
+ .replace(/^- (\d+\.)\s+/, "$1 ");
20
+ }
21
+
22
+ export function normalizeGeneratedMarkdownText(text: string | undefined): string {
23
+ if (!text) return "";
24
+ return text
25
+ .replace(/\r\n?/g, "\n")
26
+ .replace(/<br\s*\/?>/gi, "\n")
27
+ .replace(/\b(20\d{2})\/(\d{1,2})\/(\d{1,2})\b/g, (_match, year, month, day) => {
28
+ return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
29
+ })
30
+ .split("\n")
31
+ .map(normalizeLine)
32
+ .join("\n")
33
+ .replace(/[ \t]+\n/g, "\n")
34
+ .trim();
35
+ }
36
+
37
+ function escapeRegExp(value: string): string {
38
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
39
+ }
40
+
41
+ export function canonicalizeGeneratedMarkdownText(
42
+ text: string | undefined,
43
+ options: { baseUrl?: string; defaultLanguage?: string } = {},
44
+ ): string {
45
+ let normalized = normalizeGeneratedMarkdownText(text);
46
+ const { baseUrl, defaultLanguage } = options;
47
+ if (baseUrl && defaultLanguage) {
48
+ const base = baseUrl.replace(/\/+$/, "");
49
+ normalized = normalized.replace(
50
+ new RegExp(`${escapeRegExp(base)}/${escapeRegExp(defaultLanguage)}(?=/)`, "g"),
51
+ base,
52
+ );
53
+ }
54
+ return normalized;
55
+ }
56
+
57
+ function looksLikeMarkdownBlock(text: string): boolean {
58
+ return text.includes("\n") || /^(#{1,6}\s+|[-*]\s+|\d+\.\s+|---$|>\s+)/.test(text.trim());
59
+ }
60
+
61
+ export function formatGeneratedMarkdownListItem(text: string): string[] {
62
+ const normalized = normalizeGeneratedMarkdownText(text);
63
+ if (!normalized) return [];
64
+ if (looksLikeMarkdownBlock(normalized)) {
65
+ return normalized.split("\n");
66
+ }
67
+ return [`- ${normalized}`];
68
+ }