@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,143 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>RFC-0347 + ADR-0019: property-based tests for text normalization idempotency and HTML structural invariance.</purpose>
4
+ <keywords>RFC-0347, ADR-0019, PBT, fast-check, text-normalize, idempotency, tag balance, comment isolation, no content creation</keywords>
5
+ <responsibilities>
6
+ <item>Verify normalizeText is idempotent: normalize(normalize(x)) === normalize(x).</item>
7
+ <item>Verify normalizeHtml is idempotent.</item>
8
+ <item>Verify normalizeMarkdown is idempotent.</item>
9
+ <item>ADR-0019: Verify normalizeHtml preserves tag balance — structural tags in input remain balanced in output.</item>
10
+ <item>ADR-0019: Verify normalizeHtml preserves comment isolation — HTML comments are not created or destroyed.</item>
11
+ <item>ADR-0019: Verify normalizeHtml does not create HTML tags not present in input.</item>
12
+ </responsibilities>
13
+ </MODULE_CONTRACT>
14
+ <MODULE_MAP><entry key="pbt-text-normalize">Property-based tests for normalization idempotency and structural invariance.</entry></MODULE_MAP>
15
+ <CHANGE_SUMMARY>
16
+ <item>RFC-0347: initial PBT illustrative example for text-normalize idempotency.</item>
17
+ <item>ADR-0019: added tag balance, comment isolation, and no-content-creation properties for normalizeHtml.</item>
18
+ </CHANGE_SUMMARY>
19
+ */
20
+
21
+ import { test, expect } from "vitest";
22
+ import fc from "fast-check";
23
+ import {
24
+ normalizeText,
25
+ normalizeHtml,
26
+ normalizeMarkdown,
27
+ DEFAULT_NORMALIZE_CONFIG,
28
+ } from "../text-normalize.ts";
29
+
30
+ const textArbitrary = fc
31
+ .string({ minLength: 0, maxLength: 200 })
32
+ .filter((s) => !s.includes("\uFFFD"));
33
+
34
+ test("PBT: normalizeText is idempotent — normalize(normalize(x)) === normalize(x)", () => {
35
+ fc.assert(
36
+ fc.property(textArbitrary, (s) => {
37
+ const once = normalizeText(s, DEFAULT_NORMALIZE_CONFIG);
38
+ expect(normalizeText(once, DEFAULT_NORMALIZE_CONFIG)).toBe(once);
39
+ }),
40
+ );
41
+ });
42
+
43
+ test("PBT: normalizeHtml is idempotent — normalize(normalize(x)) === normalize(x)", () => {
44
+ fc.assert(
45
+ fc.property(textArbitrary, (s) => {
46
+ const once = normalizeHtml(s, DEFAULT_NORMALIZE_CONFIG);
47
+ expect(normalizeHtml(once, DEFAULT_NORMALIZE_CONFIG)).toBe(once);
48
+ }),
49
+ );
50
+ });
51
+
52
+ test("PBT: normalizeMarkdown is idempotent — normalize(normalize(x)) === normalize(x)", () => {
53
+ fc.assert(
54
+ fc.property(textArbitrary, (s) => {
55
+ const once = normalizeMarkdown(s, DEFAULT_NORMALIZE_CONFIG);
56
+ expect(normalizeMarkdown(once, DEFAULT_NORMALIZE_CONFIG)).toBe(once);
57
+ }),
58
+ );
59
+ });
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // ADR-0019: structural invariance properties for normalizeHtml
63
+ // ---------------------------------------------------------------------------
64
+
65
+ const STRUCTURAL_TAGS = [
66
+ "div",
67
+ "span",
68
+ "p",
69
+ "main",
70
+ "section",
71
+ "article",
72
+ "header",
73
+ "footer",
74
+ "ul",
75
+ "li",
76
+ ] as const;
77
+
78
+ const htmlTextSegment = fc
79
+ .string({ minLength: 0, maxLength: 50 })
80
+ .filter((s) => !s.includes("<") && !s.includes(">") && !s.includes("\uFFFD"));
81
+
82
+ const htmlComment = fc.tuple(htmlTextSegment).map(([text]) => `<!-- ${text} -->`);
83
+
84
+ const htmlBalancedTag = fc
85
+ .tuple(fc.constantFrom(...STRUCTURAL_TAGS), htmlTextSegment)
86
+ .map(([tag, text]) => `<${tag}>${text}</${tag}>`);
87
+
88
+ const htmlSegment = fc.oneof(htmlTextSegment, htmlComment, htmlBalancedTag);
89
+
90
+ const htmlArbitrary = fc
91
+ .array(htmlSegment, { minLength: 1, maxLength: 8 })
92
+ .map((segments) => segments.join("\n"));
93
+
94
+ function countTagOccurrences(html: string, tag: string): number {
95
+ const openRe = new RegExp(`<${tag}\\b[^>]*>`, "gi");
96
+ const closeRe = new RegExp(`</${tag}>`, "gi");
97
+ return (html.match(openRe) ?? []).length + (html.match(closeRe) ?? []).length;
98
+ }
99
+
100
+ test("PBT: normalizeHtml preserves tag balance — structural tags in input remain balanced in output", () => {
101
+ fc.assert(
102
+ fc.property(htmlArbitrary, (input) => {
103
+ const output = normalizeHtml(input, DEFAULT_NORMALIZE_CONFIG);
104
+ for (const tag of STRUCTURAL_TAGS) {
105
+ const inputCount = countTagOccurrences(input, tag);
106
+ const outputCount = countTagOccurrences(output, tag);
107
+ expect(outputCount).toBe(inputCount);
108
+ }
109
+ }),
110
+ );
111
+ });
112
+
113
+ test("PBT: normalizeHtml preserves comment isolation — comments are not created or destroyed", () => {
114
+ fc.assert(
115
+ fc.property(htmlArbitrary, (input) => {
116
+ const output = normalizeHtml(input, DEFAULT_NORMALIZE_CONFIG);
117
+ const inputCommentCount = (input.match(/<!--[\s\S]*?-->/g) ?? []).length;
118
+ const outputCommentCount = (output.match(/<!--[\s\S]*?-->/g) ?? []).length;
119
+ expect(outputCommentCount).toBe(inputCommentCount);
120
+ }),
121
+ );
122
+ });
123
+
124
+ test("PBT: normalizeHtml does not create HTML tags not present in input", () => {
125
+ fc.assert(
126
+ fc.property(htmlArbitrary, (input) => {
127
+ const output = normalizeHtml(input, DEFAULT_NORMALIZE_CONFIG);
128
+ const tagRe = /<([a-zA-Z][a-zA-Z0-9]*)\b/g;
129
+ const inputTagNames = new Set<string>();
130
+ let m: RegExpExecArray | null;
131
+ while ((m = tagRe.exec(input)) !== null) {
132
+ inputTagNames.add(m[1].toLowerCase());
133
+ }
134
+ const outputTagNames = new Set<string>();
135
+ while ((m = tagRe.exec(output)) !== null) {
136
+ outputTagNames.add(m[1].toLowerCase());
137
+ }
138
+ for (const tag of outputTagNames) {
139
+ expect(inputTagNames.has(tag)).toBe(true);
140
+ }
141
+ }),
142
+ );
143
+ });
@@ -0,0 +1,285 @@
1
+ import { test, expect } from "vitest";
2
+ import {
3
+ DEFAULT_NORMALIZE_CONFIG,
4
+ SIGNAL_IDS,
5
+ SIGNAL_REGISTRY,
6
+ resolveNormalizeConfig,
7
+ normalizeText,
8
+ normalizeHtml,
9
+ normalizeJson,
10
+ normalizeXml,
11
+ normalizeMarkdown,
12
+ detectResidual,
13
+ createDevNormalizeMiddleware,
14
+ type NormalizeConfig,
15
+ } from "../text-normalize.ts";
16
+
17
+ // All special/invisible inputs use \u escapes so the test is canonical and the
18
+ // module's matchers must hit the real codepoints. Expected outputs are ASCII.
19
+ const ALL_ON = DEFAULT_NORMALIZE_CONFIG;
20
+
21
+ function only(id: (typeof SIGNAL_IDS)[number]): NormalizeConfig {
22
+ const signals = {} as NormalizeConfig["signals"];
23
+ for (const s of SIGNAL_IDS) signals[s] = s === id;
24
+ return { enabled: true, signals };
25
+ }
26
+
27
+ // --- char signals -----------------------------------------------------------
28
+
29
+ test("dashes: every special dash becomes a hyphen, spaces preserved", () => {
30
+ const input = "a—b — c–d‒e―f‑g−h";
31
+ expect(normalizeText(input, ALL_ON)).toBe("a-b - c-d-e-f-g-h");
32
+ });
33
+
34
+ test("quotes: curly + guillemets become straight", () => {
35
+ const input = "“hi” «yo» it’s ‘a’ „de“";
36
+ expect(normalizeText(input, ALL_ON)).toBe(`"hi" "yo" it's 'a' "de"`);
37
+ });
38
+
39
+ test("ellipsis: single char becomes three dots", () => {
40
+ expect(normalizeText("wait…", ALL_ON)).toBe("wait...");
41
+ });
42
+
43
+ test("spaces: nbsp and exotic spaces become a regular space (no collapsing)", () => {
44
+ const input = "a b c d e f g";
45
+ expect(normalizeText(input, ALL_ON)).toBe("a b c d e f g");
46
+ });
47
+
48
+ test("zero-width cruft is removed", () => {
49
+ const input = "a​b‌c⁠de­f؜g";
50
+ expect(normalizeText(input, ALL_ON)).toBe("abcdefg");
51
+ });
52
+
53
+ test("ZWJ inside an emoji sequence is preserved; standalone ZWJ is stripped", () => {
54
+ const family = "\u{1F469}‍\u{1F469}‍\u{1F466}"; // woman+woman+boy
55
+ expect(normalizeText(family, ALL_ON)).toBe(family);
56
+ expect(normalizeText("a‍b", ALL_ON)).toBe("ab");
57
+ });
58
+
59
+ // --- html entities -----------------------------------------------------------
60
+
61
+ test("typographic entities are decoded then normalized; structural entities survive", () => {
62
+ const input = "A&mdash;B&nbsp;C&hellip; &amp; &lt;tag&gt; &quot;q&quot;";
63
+ expect(normalizeText(input, ALL_ON)).toBe("A-B C... &amp; &lt;tag&gt; &quot;q&quot;");
64
+ });
65
+
66
+ test("numeric typographic entities decode; structural numeric entities do not", () => {
67
+ // &#8212; = em dash (typographic) ; &#38; = & (structural)
68
+ expect(normalizeText("x&#8212;y &#38; z", ALL_ON)).toBe("x-y &#38; z");
69
+ });
70
+
71
+ test("entity lens respects the dash toggle (decode keeps em-dash when dashes off)", () => {
72
+ const cfg: NormalizeConfig = { enabled: true, signals: { ...ALL_ON.signals, dashes: false } };
73
+ expect(normalizeText("a&mdash;b", cfg)).toBe("a—b");
74
+ });
75
+
76
+ // --- per-signal isolation ----------------------------------------------------
77
+
78
+ test("disabling spaces keeps nbsp while other signals still fire", () => {
79
+ const cfg: NormalizeConfig = { enabled: true, signals: { ...ALL_ON.signals, spaces: false } };
80
+ expect(normalizeText("a b—c", cfg)).toBe("a b-c");
81
+ });
82
+
83
+ test("master switch off is a no-op", () => {
84
+ const off: NormalizeConfig = { enabled: false, signals: { ...ALL_ON.signals } };
85
+ const input = "a—b“c”";
86
+ expect(normalizeText(input, off)).toBe(input);
87
+ });
88
+
89
+ // --- html --------------------------------------------------------------------
90
+
91
+ test("html: text nodes normalized, code/script/pre skipped", () => {
92
+ const input = "<p>he said “hi”—ok</p><code>a—b</code><pre>x—y</pre>";
93
+ expect(normalizeHtml(input, ALL_ON)).toBe(`<p>he said "hi"-ok</p><code>a—b</code><pre>x—y</pre>`);
94
+ });
95
+
96
+ test("html: whitelisted attribute normalized and quote re-escaped", () => {
97
+ const input = `<img alt="say “hi”" src="/x.png">`;
98
+ expect(normalizeHtml(input, ALL_ON)).toBe(`<img alt="say &quot;hi&quot;" src="/x.png">`);
99
+ });
100
+
101
+ test("html: non-whitelisted attribute (href) is left untouched", () => {
102
+ const input = `<a href="/a—b">t—t</a>`;
103
+ expect(normalizeHtml(input, ALL_ON)).toBe(`<a href="/a—b">t-t</a>`);
104
+ });
105
+
106
+ test("html: inline JSON-LD string values normalized and stay valid JSON", () => {
107
+ const input = '<script type="application/ld+json">{"name":"He said “hi”—ok"}</script>';
108
+ const out = normalizeHtml(input, ALL_ON);
109
+ const json = out.replace(/^<script[^>]*>/, "").replace(/<\/script>$/, "");
110
+ const parsed = JSON.parse(json);
111
+ expect(parsed.name).toBe(`He said "hi"-ok`);
112
+ });
113
+
114
+ // --- json --------------------------------------------------------------------
115
+
116
+ test("json: clean input returns byte-identical (no reformat)", () => {
117
+ const input = '{"a":"plain","b":1}';
118
+ expect(normalizeJson(input, ALL_ON)).toBe(input);
119
+ });
120
+
121
+ test("json: values normalized, keys untouched", () => {
122
+ const out = normalizeJson('{"a\\u2014key":"v\\u2014v"}', ALL_ON);
123
+ const parsed = JSON.parse(out);
124
+ expect(Object.keys(parsed)).toEqual(["a—key"]);
125
+ expect(parsed["a—key"]).toBe("v-v");
126
+ });
127
+
128
+ // --- xml ---------------------------------------------------------------------
129
+
130
+ test("xml: element text normalized, tags preserved", () => {
131
+ expect(normalizeXml("<title>A—B</title>", ALL_ON)).toBe("<title>A-B</title>");
132
+ });
133
+
134
+ test("xml: CDATA body normalized as html", () => {
135
+ const input = "<description><![CDATA[<p>a—b</p>]]></description>";
136
+ expect(normalizeXml(input, ALL_ON)).toBe("<description><![CDATA[<p>a-b</p>]]></description>");
137
+ });
138
+
139
+ // --- markdown ----------------------------------------------------------------
140
+
141
+ test("markdown: prose normalized, fenced + inline code protected", () => {
142
+ const input = "He—said\n```\nx—y\n```\nand `a—b` end";
143
+ expect(normalizeMarkdown(input, ALL_ON)).toBe("He-said\n```\nx—y\n```\nand `a—b` end");
144
+ });
145
+
146
+ // --- CSS < in style block (regression) ---------------------------------------
147
+
148
+ test("html: CSS media query < in <style> does not trap the skip stack", () => {
149
+ const input = "<style>@media (width<=768px){.x{color:red}}</style><p>a\u2014b</p>";
150
+ expect(normalizeHtml(input, ALL_ON)).toBe(
151
+ "<style>@media (width<=768px){.x{color:red}}</style><p>a-b</p>",
152
+ );
153
+ });
154
+
155
+ test("html: text after <style> with CSS < is still normalized", () => {
156
+ const input =
157
+ "<style>.a{color:red}@media (width<=768px){.a{color:blue}}</style>" +
158
+ '<span aria-label="Warpgogol \u2014 Home">link</span>' +
159
+ "<p>\u0426\u0438\u0444\u0440\u043e\u0432\u0438\u0439 \u0444\u0443\u043d\u0434\u0430\u043c\u0435\u043d\u0442 \u2014 \u043f\u0440\u043e\u0434\u0443\u043a\u0442</p>";
160
+ const out = normalizeHtml(input, ALL_ON);
161
+ expect(out).toContain('aria-label="Warpgogol - Home"');
162
+ expect(out).not.toContain("\u2014");
163
+ });
164
+
165
+ // --- idempotency -------------------------------------------------------------
166
+
167
+ test("all transforms are idempotent", () => {
168
+ const html = `<p>“q”—<a title="t…">l</a></p>`;
169
+ const once = normalizeHtml(html, ALL_ON);
170
+ expect(normalizeHtml(once, ALL_ON)).toBe(once);
171
+ const md = "a—b “c”…";
172
+ const mdOnce = normalizeMarkdown(md, ALL_ON);
173
+ expect(normalizeMarkdown(mdOnce, ALL_ON)).toBe(mdOnce);
174
+ });
175
+
176
+ // --- config resolution -------------------------------------------------------
177
+
178
+ test("resolveNormalizeConfig: absent block ⇒ all on", () => {
179
+ expect(resolveNormalizeConfig({})).toEqual(ALL_ON);
180
+ expect(resolveNormalizeConfig(null)).toEqual(ALL_ON);
181
+ });
182
+
183
+ test("resolveNormalizeConfig: explicit per-signal off respected, others default on", () => {
184
+ const cfg = resolveNormalizeConfig({ text: { normalize: { signals: { spaces: false } } } });
185
+ expect(cfg.enabled).toBe(true);
186
+ expect(cfg.signals.spaces).toBe(false);
187
+ expect(cfg.signals.dashes).toBe(true);
188
+ });
189
+
190
+ test("resolveNormalizeConfig: enabled:false honored", () => {
191
+ const cfg = resolveNormalizeConfig({ text: { normalize: { enabled: false } } });
192
+ expect(cfg.enabled).toBe(false);
193
+ });
194
+
195
+ // --- backstop detection ------------------------------------------------------
196
+
197
+ test("detectResidual: clean content returns null", () => {
198
+ expect(detectResidual("<p>clean</p>", "html", ALL_ON)).toBe(null);
199
+ });
200
+
201
+ test("detectResidual: reports the responsible signal and a line", () => {
202
+ const finding = detectResidual("line1\nse—cond", "txt", ALL_ON);
203
+ expect(finding).toBeTruthy();
204
+ expect(finding!.line).toBe(2);
205
+ expect(finding!.signals).toEqual(["dashes"]);
206
+ });
207
+
208
+ // --- registry ----------------------------------------------------------------
209
+
210
+ test("SIGNAL_REGISTRY covers exactly the six signal ids", () => {
211
+ expect(SIGNAL_REGISTRY.map((s) => s.id).sort()).toEqual([...SIGNAL_IDS].sort());
212
+ });
213
+
214
+ test("only() isolates a single signal", () => {
215
+ expect(normalizeText("a—b“c”", only("dashes"))).toBe(`a-b“c”`);
216
+ });
217
+
218
+ // Codepoint-built inputs — independent of how source glyphs are stored, this proves
219
+ // the matchers hit the exact Unicode points named in the RFC (no false pass).
220
+ test("codepoints: spaces + zero-width matched by exact U+ value", () => {
221
+ const cp = (...codes: number[]) => String.fromCodePoint(...codes);
222
+ // U+00A0 U+2009 U+2003 U+202F U+3000 U+1680 U+205F all → single space
223
+ const spaces = `a${cp(0x00a0)}b${cp(0x2009)}c${cp(0x2003)}d${cp(0x202f)}e${cp(0x3000)}f${cp(0x1680)}g${cp(0x205f)}h`;
224
+ expect(normalizeText(spaces, ALL_ON)).toBe("a b c d e f g h");
225
+ // U+200B U+200C U+2060 U+FEFF U+00AD U+061C U+2061 → removed
226
+ const zw = `a${cp(0x200b)}b${cp(0x200c)}c${cp(0x2060)}d${cp(0xfeff)}e${cp(0x00ad)}f${cp(0x061c)}g${cp(0x2061)}h`;
227
+ expect(normalizeText(zw, ALL_ON)).toBe("abcdefgh");
228
+ // U+2014 em dash, U+201C/U+201D quotes, U+2026 ellipsis
229
+ expect(normalizeText(`x${cp(0x2014)}y ${cp(0x201c)}z${cp(0x201d)}${cp(0x2026)}`, ALL_ON)).toBe(
230
+ `x-y "z"...`,
231
+ );
232
+ });
233
+
234
+ // --- dev middleware (RFC-0569) -----------------------------------------------
235
+
236
+ function makeResponse(body: string, contentType: string): Response {
237
+ return new Response(body, { headers: { "content-type": contentType } });
238
+ }
239
+
240
+ test("dev middleware: enabled config normalizes HTML response", async () => {
241
+ const mw = createDevNormalizeMiddleware(ALL_ON);
242
+ const html = "<p>he said \u201chi\u201d\u2014ok</p>";
243
+ const next = async () => makeResponse(html, "text/html; charset=utf-8");
244
+ const res = await mw({} as unknown, next);
245
+ if (!(res instanceof Response)) throw new Error("expected Response");
246
+ const body = await res.text();
247
+ expect(body).toBe(`<p>he said "hi"-ok</p>`);
248
+ });
249
+
250
+ test("dev middleware: disabled config is pass-through", async () => {
251
+ const off: NormalizeConfig = { enabled: false, signals: { ...ALL_ON.signals } };
252
+ const mw = createDevNormalizeMiddleware(off);
253
+ const html = "<p>a\u2014b</p>";
254
+ const next = async () => makeResponse(html, "text/html");
255
+ const res = await mw({} as unknown, next);
256
+ if (!(res instanceof Response)) throw new Error("expected Response");
257
+ const body = await res.text();
258
+ expect(body).toBe(html);
259
+ });
260
+
261
+ test("dev middleware: non-HTML response is pass-through", async () => {
262
+ const mw = createDevNormalizeMiddleware(ALL_ON);
263
+ const json = '{"a":"b\u2014c"}';
264
+ const next = async () => makeResponse(json, "application/json");
265
+ const res = await mw({} as unknown, next);
266
+ if (!(res instanceof Response)) throw new Error("expected Response");
267
+ const body = await res.text();
268
+ expect(body).toBe(json);
269
+ });
270
+
271
+ test("dev middleware: try/catch falls back to original on error", async () => {
272
+ const mw = createDevNormalizeMiddleware(ALL_ON);
273
+ const html = "<p>valid</p>";
274
+ let threw = false;
275
+ const res = await mw({} as unknown, async () => {
276
+ const r = makeResponse(html, "text/html");
277
+ r.text = async () => {
278
+ threw = true;
279
+ throw new Error("simulated read error");
280
+ };
281
+ return r;
282
+ });
283
+ expect(threw).toBe(true);
284
+ expect(res).toBeInstanceOf(Response);
285
+ });
@@ -0,0 +1,43 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for RFC-0306 Markdown twin path helpers.</purpose>
4
+ <keywords>markdown twin, path helper, RFC-0306</keywords>
5
+ <responsibilities>
6
+ <item>Lock root, language-root, nested, trailing-slash, and non-language slug behavior.</item>
7
+ </responsibilities>
8
+ <non-goals>
9
+ <item>Do not inspect generated app files.</item>
10
+ </non-goals>
11
+ </MODULE_CONTRACT>
12
+ <MODULE_MAP>
13
+ <entry key="tests">node:test assertions for markdownTwinRelPath and markdownTwinUrlPath.</entry>
14
+ </MODULE_MAP>
15
+ <CHANGE_SUMMARY>
16
+ <item>RFC-0306: Added Markdown twin path helper coverage.</item>
17
+ </CHANGE_SUMMARY>
18
+ */
19
+
20
+ import { test, expect } from "vitest";
21
+ import { markdownTwinRelPath, markdownTwinUrlPath } from "../semantic/ids.ts";
22
+
23
+ const supportedLangs = ["de", "en"] as const;
24
+
25
+ test("markdownTwinRelPath keeps root and language roots at index.md", () => {
26
+ expect(markdownTwinRelPath("/", { supportedLangs })).toBe("index.md");
27
+ expect(markdownTwinRelPath("/en", { supportedLangs })).toBe("en/index.md");
28
+ expect(markdownTwinRelPath("/en/", { supportedLangs })).toBe("en/index.md");
29
+ });
30
+
31
+ test("markdownTwinRelPath maps non-home pages to sibling md files", () => {
32
+ expect(markdownTwinRelPath("/team/andrii-syrokomskyi", { supportedLangs })).toBe(
33
+ "team/andrii-syrokomskyi.md",
34
+ );
35
+ expect(markdownTwinRelPath("/en/team/andrii", { supportedLangs })).toBe("en/team/andrii.md");
36
+ expect(markdownTwinRelPath("/it", { supportedLangs })).toBe("it.md");
37
+ });
38
+
39
+ test("markdownTwinUrlPath returns the public URL form", () => {
40
+ expect(markdownTwinUrlPath("/team/andrii-syrokomskyi", { supportedLangs })).toBe(
41
+ "/team/andrii-syrokomskyi.md",
42
+ );
43
+ });