@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-0168: the Integration Port. Vendor-agnostic contracts for delivering a captured
4
+ lead to outbound channels (Telegram, email, WhatsApp) and upserting it into a CRM (Pipedrive),
5
+ mirroring the growth (RFC-0027) and content-source (RFC-0141) port/adapter pattern. Pure — takes
6
+ secrets as parameters; never imports astro:env (the section handler reads secrets and passes them).</purpose>
7
+ <non-goals>
8
+ <item>Do not import vendor SDKs or astro:env here — adapters use fetch + injected secrets.</item>
9
+ <item>Do not log or return secrets.</item>
10
+ </non-goals>
11
+ </MODULE_CONTRACT>
12
+ <CHANGE_SUMMARY>
13
+ <item>RFC-0168: initial implementation.</item>
14
+ </CHANGE_SUMMARY>
15
+ */
16
+
17
+ import type { LifecycleEventPayload } from "./lifecycle.ts";
18
+
19
+ /** Secrets bag (env name → value). Adapters read only their declared requiredSecrets. */
20
+ export type IntegrationSecrets = Record<string, string | undefined>;
21
+
22
+ export interface LeadMessage {
23
+ message: string;
24
+ formId: string;
25
+ locale: string;
26
+ /** ISO-8601 submission time. */
27
+ submittedAt: string;
28
+ contact?: { email?: string; phone?: string };
29
+ }
30
+
31
+ export interface Lead {
32
+ name?: string;
33
+ email?: string;
34
+ phone?: string;
35
+ note: string;
36
+ source: string;
37
+ }
38
+
39
+ export interface IntegrationChannelAdapter {
40
+ readonly id: string;
41
+ /** Env secret names required for this adapter to be active. */
42
+ readonly requiredSecrets: readonly string[];
43
+ /** Deliver the message. Throws on transport failure. */
44
+ deliver(message: LeadMessage, secrets: IntegrationSecrets): Promise<void>;
45
+ }
46
+
47
+ export interface CrmAdapter {
48
+ readonly id: string;
49
+ readonly requiredSecrets: readonly string[];
50
+ /** Upsert a lead. Returns the CRM id, or null. Throws on transport failure. */
51
+ upsertLead(lead: Lead, secrets: IntegrationSecrets): Promise<{ id: string } | null>;
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // RFC-0176: source-agnostic destination hub
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /**
59
+ * Closed catalog of destination kinds the hub can route to (RFC-0176). The hub
60
+ * runs on the client's site with the client's tokens; "channel"/"crm" from
61
+ * RFC-0168 are subsumed here (a channel is a message/email destination).
62
+ */
63
+ export const DESTINATION_KINDS = ["crm", "calendar", "email", "scheduler"] as const;
64
+ export type DestinationKind = (typeof DESTINATION_KINDS)[number];
65
+
66
+ /**
67
+ * How a configured destination is executed (RFC-0176):
68
+ * - `gogol-adapter` (default): an adapter runs on the client's site with the
69
+ * client's tokens — the studio's code routes, but on the client's deploy.
70
+ * - `vendor-native` (optional): an upstream vendor's own flow executes it; the
71
+ * studio holds no secret and runs no code for it (declared for validation +
72
+ * the RFC-0177 processor disclosure only).
73
+ */
74
+ export const EXECUTION_MODES = ["gogol-adapter", "vendor-native"] as const;
75
+ export type ExecutionMode = (typeof EXECUTION_MODES)[number];
76
+
77
+ /**
78
+ * Normalized event produced by any IntegrationSource (the send-message form, the
79
+ * chat widget, future sources). Generalizes LeadMessage/Lead. `eventId` is the
80
+ * idempotency key the queue consumer dedups on (RFC-0176).
81
+ */
82
+ export interface IntegrationEvent {
83
+ /** Idempotency key (source-provided) — dedup on redelivery. */
84
+ eventId: string;
85
+ kind: "lead" | "message" | "appointment";
86
+ /** Source id, e.g. "send-message" | "uchat" | "stripe". */
87
+ source: string;
88
+ locale: string;
89
+ /** ISO-8601 occurrence time. */
90
+ occurredAt: string;
91
+ contact?: { name?: string; email?: string; phone?: string };
92
+ payload: Record<string, unknown>;
93
+ /** RFC-0191: optional typed lifecycle payload for Stripe billing events. */
94
+ lifecycle?: LifecycleEventPayload;
95
+ }
96
+
97
+ /**
98
+ * A destination executed on the client's site (client tokens) — only for mode
99
+ * `gogol-adapter`. `vendor-native` destinations have no executed adapter.
100
+ */
101
+ export interface DestinationAdapter {
102
+ readonly kind: DestinationKind;
103
+ readonly vendor: string;
104
+ readonly requiredSecrets: readonly string[];
105
+ /** Route the event to the destination. Returns its id, or null. Throws on transport failure. */
106
+ route(event: IntegrationEvent, secrets: IntegrationSecrets): Promise<{ id: string } | null>;
107
+ }
108
+
109
+ /** Map a normalized event onto the RFC-0168 LeadMessage shape (for channel fan-out). */
110
+ export function eventToLeadMessage(event: IntegrationEvent): LeadMessage {
111
+ const message =
112
+ typeof event.payload.message === "string"
113
+ ? event.payload.message
114
+ : typeof event.payload.note === "string"
115
+ ? (event.payload.note as string)
116
+ : "";
117
+ const email = event.contact?.email;
118
+ const phone = event.contact?.phone;
119
+ return {
120
+ message,
121
+ formId: event.source,
122
+ locale: event.locale,
123
+ submittedAt: event.occurredAt,
124
+ ...(email || phone
125
+ ? { contact: { ...(email ? { email } : {}), ...(phone ? { phone } : {}) } }
126
+ : {}),
127
+ };
128
+ }
129
+
130
+ /** Map a normalized `lead`-kind event onto the RFC-0168 Lead shape (back-compat). */
131
+ export function eventToLead(event: IntegrationEvent): Lead {
132
+ const note =
133
+ typeof event.payload.note === "string"
134
+ ? event.payload.note
135
+ : ((event.payload.message as string) ?? "");
136
+ return {
137
+ name: event.contact?.name,
138
+ email: event.contact?.email,
139
+ phone: event.contact?.phone,
140
+ note,
141
+ source: event.source,
142
+ };
143
+ }
@@ -0,0 +1,134 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>RFC-0181: EU-resident delivery substrate. buildQstashPublish constructs the HTTPS request that
4
+ publishes an IntegrationEvent to Upstash QStash in the EU region (eu-central-1, Frankfurt) — with
5
+ content dedup and a retry count — for reliable webhook delivery back to the site's own callback. The
6
+ IdempotencyLedger (Upstash Redis EU) is the durable first-seen guard the callback consults before
7
+ routing. Pure: buildQstashPublish does no I/O; the ledger is an injectable interface. Replaces the
8
+ Cloudflare Queues/KV substrate (RFC-0179), which cannot be EU-pinned.</purpose>
9
+ <non-goals>
10
+ <item>Do not log or embed secrets (UPSTASH_QSTASH_TOKEN, Redis token) — caller injects them.</item>
11
+ <item>Do not publish lead PII to the US region or a non-EU base URL.</item>
12
+ <item>Do not verify QStash webhook signatures here — the route uses @upstash/qstash Receiver.</item>
13
+ </non-goals>
14
+ </MODULE_CONTRACT>
15
+ <CHANGE_SUMMARY>
16
+ <item>RFC-0181: initial EU delivery substrate (QStash + Redis, eu-central-1).</item>
17
+ </CHANGE_SUMMARY>
18
+ */
19
+
20
+ import type { IntegrationEvent } from "./port.ts";
21
+
22
+ /** EU-region QStash base URL — the residency pin (Frankfurt, eu-central-1). RFC-0181. */
23
+ export const QSTASH_EU_BASE = "https://qstash-eu-central-1.upstash.io" as const;
24
+
25
+ /**
26
+ * Canonical server-secret NAMES for the EU delivery substrate (RFC-0181). Single
27
+ * source of truth for the env schema, the .env.example generator, and validators.
28
+ * Values are read via `astro:env/server`; never logged, returned, or committed.
29
+ * - `UPSTASH_QSTASH_URL` — the EU QStash base (must be the eu-central-1 endpoint).
30
+ * - `UPSTASH_QSTASH_TOKEN` — publish token (Bearer).
31
+ * - `UPSTASH_QSTASH_CURRENT_SIGNING_KEY` / `…_NEXT_SIGNING_KEY` — webhook signature verification.
32
+ * - `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` — the EU Redis idempotency ledger.
33
+ */
34
+ export const UPSTASH_QSTASH_SECRETS = [
35
+ "UPSTASH_QSTASH_URL",
36
+ "UPSTASH_QSTASH_TOKEN",
37
+ "UPSTASH_QSTASH_CURRENT_SIGNING_KEY",
38
+ "UPSTASH_QSTASH_NEXT_SIGNING_KEY",
39
+ ] as const;
40
+
41
+ export const UPSTASH_REDIS_SECRETS = [
42
+ "UPSTASH_REDIS_REST_URL",
43
+ "UPSTASH_REDIS_REST_TOKEN",
44
+ ] as const;
45
+
46
+ /** Every EU-delivery secret name (QStash + Redis). RFC-0181. */
47
+ export const UPSTASH_DELIVERY_SECRETS = [
48
+ ...UPSTASH_QSTASH_SECRETS,
49
+ ...UPSTASH_REDIS_SECRETS,
50
+ ] as const;
51
+
52
+ export interface QstashPublishConfig {
53
+ /** UPSTASH_QSTASH_TOKEN — server secret. Never logged. */
54
+ token: string;
55
+ /** The site's own absolute callback URL (its /internal/integration-route). */
56
+ callbackUrl: string;
57
+ /** QStash retry count (exponential backoff). Default 3. */
58
+ retries?: number;
59
+ /** Base URL — defaults to the EU region. Overriding to a non-EU base breaks residency. */
60
+ baseUrl?: string;
61
+ }
62
+
63
+ /**
64
+ * Pure (RFC-0181): build the HTTPS request that publishes `event` to QStash EU
65
+ * for reliable webhook delivery to `config.callbackUrl`. QStash dedups on
66
+ * `Upstash-Deduplication-Id` (the eventId) and retries to the callback, then
67
+ * dead-letters. No I/O — the caller performs the fetch with this Request.
68
+ */
69
+ export function buildQstashPublish(event: IntegrationEvent, config: QstashPublishConfig): Request {
70
+ const base = config.baseUrl ?? QSTASH_EU_BASE;
71
+ // QStash v2: POST /v2/publish/{destination-url}; the body is forwarded to the destination.
72
+ // Do NOT encode the callback URL — QStash expects the raw URL in the path segment.
73
+ // encodeURIComponent breaks scheme detection because %3A is not decoded back to :.
74
+ const url = `${base}/v2/publish/${config.callbackUrl}`;
75
+ return new Request(url, {
76
+ method: "POST",
77
+ headers: {
78
+ authorization: `Bearer ${config.token}`,
79
+ "content-type": "application/json",
80
+ "upstash-deduplication-id": event.eventId,
81
+ "upstash-retries": String(config.retries ?? 3),
82
+ },
83
+ body: JSON.stringify(event),
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Durable idempotency ledger (RFC-0181) backed by Upstash Redis in the EU.
89
+ * `firstSeen` returns true exactly once per eventId (SET NX), so a QStash
90
+ * redelivery never double-writes into the client's CRM. Stores only the eventId
91
+ * key with a short TTL — never PII.
92
+ */
93
+ export interface IdempotencyLedger {
94
+ firstSeen(eventId: string, ttlSeconds?: number): Promise<boolean>;
95
+ }
96
+
97
+ /** Minimal structural type for the Upstash Redis REST endpoint (no hard dep). */
98
+ export interface RestRedisConfig {
99
+ /** UPSTASH_REDIS_REST_URL — must be the eu-central-1 endpoint for EU residency. */
100
+ url: string;
101
+ /** UPSTASH_REDIS_REST_TOKEN — server secret. Never logged. */
102
+ token: string;
103
+ /** Key prefix; defaults to the integration dedup namespace. */
104
+ prefix?: string;
105
+ }
106
+
107
+ /**
108
+ * Adapt an Upstash Redis REST endpoint to the IdempotencyLedger (RFC-0181). Uses
109
+ * `SET key 1 NX EX ttl`; a `{"result":"OK"}` means first-seen, `null` means the
110
+ * key already existed (a redelivery). Fails closed on a transport error by
111
+ * throwing, so the callback retries rather than risk a double-write.
112
+ */
113
+ export function restRedisLedger(
114
+ config: RestRedisConfig,
115
+ fetchImpl: typeof fetch = fetch,
116
+ ): IdempotencyLedger {
117
+ const prefix = config.prefix ?? "gogol-int-dedup";
118
+ return {
119
+ async firstSeen(eventId: string, ttlSeconds = 86400): Promise<boolean> {
120
+ const key = `${prefix}:${eventId}`;
121
+ const command = ["SET", key, "1", "NX", "EX", String(ttlSeconds)];
122
+ const response = await fetchImpl(config.url, {
123
+ method: "POST",
124
+ headers: { authorization: `Bearer ${config.token}`, "content-type": "application/json" },
125
+ body: JSON.stringify(command),
126
+ });
127
+ if (!response.ok) {
128
+ throw new Error(`redis ledger SET failed: ${response.status}`);
129
+ }
130
+ const data = (await response.json()) as { result?: string | null };
131
+ return data.result === "OK";
132
+ },
133
+ };
134
+ }
@@ -0,0 +1,133 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>RFC-0179: deterministic placement of a site on the shared, sharded delivery backbone.
4
+ resolveShard maps a stable siteId + region + tier onto a concrete queue / DLQ / dedup-namespace
5
+ set, so the shared queues replace RFC-0176's per-client queue without a central registry — the
6
+ mapping is recomputable from content alone. Pure: no I/O, no platform bindings.</purpose>
7
+ <non-goals>
8
+ <item>Do not create or call Cloudflare resources — this is a pure naming/placement function.</item>
9
+ <item>Do not read system.md or any file — callers pass region/tier explicitly.</item>
10
+ </non-goals>
11
+ </MODULE_CONTRACT>
12
+ <CHANGE_SUMMARY>
13
+ <item>RFC-0179: initial implementation.</item>
14
+ <item>RFC-0181: the Cloudflare-queue naming here is SUPERSEDED for the EU delivery path (Upstash QStash/Redis, see qstash.ts). Retained for the deferred WfP/CF-hosted topology; not used for EU lead delivery.</item>
15
+ </CHANGE_SUMMARY>
16
+ */
17
+
18
+ /** Closed catalog of delivery jurisdictions (RFC-0179). Drives residency sharding. */
19
+ export const DELIVERY_REGIONS = ["eu", "us"] as const;
20
+ export type DeliveryRegion = (typeof DELIVERY_REGIONS)[number];
21
+
22
+ /** Execution tier on the shared backbone (RFC-0179). */
23
+ export const DELIVERY_TIERS = ["shared", "dedicated"] as const;
24
+ export type DeliveryTier = (typeof DELIVERY_TIERS)[number];
25
+
26
+ /** Resolved placement of a site on the shared delivery backbone (RFC-0179). */
27
+ export interface ShardAssignment {
28
+ siteId: string;
29
+ region: DeliveryRegion;
30
+ tier: DeliveryTier;
31
+ /** Shared-pool shard index, or -1 for a dedicated queue. */
32
+ shardIndex: number;
33
+ /** Queue name, e.g. "gogol-int-eu-shared-02" or "gogol-int-eu-ded-acme". */
34
+ queue: string;
35
+ /** Dead-letter queue name (queue + "-dlq"). */
36
+ dlq: string;
37
+ /** Region dedup KV namespace title, e.g. "gogol-int-dedup-eu". */
38
+ dedupNamespace: string;
39
+ /** Consumer Worker name bound to `queue` (one consumer per queue). */
40
+ consumer: string;
41
+ }
42
+
43
+ /** Options for resolveShard. `shardCount` is the size of the region's shared pool. */
44
+ export interface ResolveShardOptions {
45
+ tier?: DeliveryTier;
46
+ /** Number of queues in the region's shared pool. Must be ≥ 1. Default 4. */
47
+ shardCount?: number;
48
+ }
49
+
50
+ /**
51
+ * Deterministic 32-bit FNV-1a hash of a string. Stable across runtimes (no
52
+ * dependency on Math.random or hash seeds), so a siteId always lands on the same
53
+ * shard — the property re-sharding safety and registry-free placement rely on.
54
+ */
55
+ export function fnv1a(input: string): number {
56
+ let hash = 0x811c9dc5;
57
+ for (let i = 0; i < input.length; i += 1) {
58
+ hash ^= input.charCodeAt(i);
59
+ // hash *= 16777619, kept in 32-bit unsigned space.
60
+ hash = Math.imul(hash, 0x01000193) >>> 0;
61
+ }
62
+ return hash >>> 0;
63
+ }
64
+
65
+ /** Two-digit shard suffix ("00".."99"+) for stable, sortable queue names. */
66
+ function shardSuffix(index: number): string {
67
+ return index < 10 ? `0${index}` : String(index);
68
+ }
69
+
70
+ /**
71
+ * Sanitize a siteId into a Cloudflare-safe resource-name segment: lowercase,
72
+ * alphanumeric + single dashes. Keeps names deterministic for dedicated queues.
73
+ */
74
+ function safeSegment(siteId: string): string {
75
+ return siteId
76
+ .toLowerCase()
77
+ .replace(/[^a-z0-9]+/g, "-")
78
+ .replace(/^-+|-+$/g, "")
79
+ .slice(0, 40);
80
+ }
81
+
82
+ /**
83
+ * Resolve a site's place on the shared, sharded delivery backbone (RFC-0179).
84
+ *
85
+ * - `shared` tier: the site is hashed into one of `shardCount` queues in the
86
+ * region's shared pool — `gogol-int-{region}-shared-{NN}`. Blast radius is
87
+ * bounded to 1/shardCount of the pool.
88
+ * - `dedicated` tier: the site gets its own queue `gogol-int-{region}-ded-{siteId}`
89
+ * so a noisy/high-volume client cannot exhaust a shared pool's backlog.
90
+ *
91
+ * Dedup is a single region-scoped KV namespace in both tiers (keys are
92
+ * `{siteId}:{eventId}:{dest}`, unlimited per namespace).
93
+ *
94
+ * Pure + deterministic: the same inputs always yield the same assignment, so the
95
+ * placement is recomputable from content with no central registry.
96
+ */
97
+ export function resolveShard(
98
+ siteId: string,
99
+ region: DeliveryRegion,
100
+ opts: ResolveShardOptions = {},
101
+ ): ShardAssignment {
102
+ if (!siteId) throw new Error("resolveShard: siteId is required");
103
+ const tier: DeliveryTier = opts.tier ?? "shared";
104
+ const shardCount = Math.max(1, opts.shardCount ?? 4);
105
+ const dedupNamespace = `gogol-int-dedup-${region}`;
106
+
107
+ if (tier === "dedicated") {
108
+ const queue = `gogol-int-${region}-ded-${safeSegment(siteId)}`;
109
+ return {
110
+ siteId,
111
+ region,
112
+ tier,
113
+ shardIndex: -1,
114
+ queue,
115
+ dlq: `${queue}-dlq`,
116
+ dedupNamespace,
117
+ consumer: `${queue}-consumer`,
118
+ };
119
+ }
120
+
121
+ const shardIndex = fnv1a(siteId) % shardCount;
122
+ const queue = `gogol-int-${region}-shared-${shardSuffix(shardIndex)}`;
123
+ return {
124
+ siteId,
125
+ region,
126
+ tier,
127
+ shardIndex,
128
+ queue,
129
+ dlq: `${queue}-dlq`,
130
+ dedupNamespace,
131
+ consumer: `${queue}-consumer`,
132
+ };
133
+ }
@@ -0,0 +1,52 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>RFC-0181: verify the unified deliverEvent fan-out (channels + destinations) and the
4
+ eventToLeadMessage mapping. With no secrets every sink is skipped (not failed), so a misconfigured
5
+ site never spuriously retries. No network — adapters self-skip on absent secrets.</purpose>
6
+ <responsibilities>
7
+ <item>eventToLeadMessage maps source/locale/occurredAt/contact + payload.message.</item>
8
+ <item>deliverEvent with no secrets skips all channels + the CRM destination (no failures).</item>
9
+ </responsibilities>
10
+ <non-goals><item>No network.</item></non-goals>
11
+ </MODULE_CONTRACT>
12
+ <CHANGE_SUMMARY><item>RFC-0181: initial deliverEvent test.</item></CHANGE_SUMMARY>
13
+ */
14
+
15
+ import { test, expect } from "vitest";
16
+ import { deliverEvent, eventToLeadMessage } from "../index.ts";
17
+ import type { IntegrationEvent } from "../port.ts";
18
+
19
+ const EVENT: IntegrationEvent = {
20
+ eventId: "evt-1",
21
+ kind: "lead",
22
+ source: "send-message",
23
+ locale: "de",
24
+ occurredAt: "2026-06-08T00:00:00.000Z",
25
+ contact: { email: "a@example.de" },
26
+ payload: { message: "Hallo" },
27
+ };
28
+
29
+ test("eventToLeadMessage maps the event onto a LeadMessage", () => {
30
+ const msg = eventToLeadMessage(EVENT);
31
+ expect(msg.message).toBe("Hallo");
32
+ expect(msg.formId).toBe("send-message");
33
+ expect(msg.locale).toBe("de");
34
+ expect(msg.submittedAt).toBe("2026-06-08T00:00:00.000Z");
35
+ expect(msg.contact?.email).toBe("a@example.de");
36
+ });
37
+
38
+ test("deliverEvent with no secrets skips every sink (no failures, no retry)", async () => {
39
+ const result = await deliverEvent(EVENT, {});
40
+ expect(result.channels.delivered.length).toBe(0);
41
+ expect(result.channels.failed.length).toBe(0);
42
+ expect(result.channels.skipped.includes("telegram")).toBeTruthy();
43
+ expect(result.channels.skipped.includes("whatsapp")).toBeTruthy();
44
+ expect(result.destinations.routed.length).toBe(0);
45
+ expect(result.destinations.failed.length).toBe(0);
46
+ expect(result.destinations.skipped.includes("crm:pipedrive")).toBeTruthy();
47
+ });
48
+
49
+ test("email is no longer a fetch channel adapter (Cloudflare Email Routing handles it)", async () => {
50
+ const result = await deliverEvent(EVENT, {});
51
+ expect(!result.channels.skipped.includes("email")).toBeTruthy();
52
+ });
@@ -0,0 +1,88 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>RFC-0179: verify the dynamic-dispatch seam. executeDispatch (tenant-side) runs ready
4
+ destinations with local secrets; dispatchToTenant (consumer-side) posts into the tenant, never
5
+ carries a destination token, and maps the reply to ack vs retry. fetch/dispatcher are stubbed.</purpose>
6
+ <responsibilities>
7
+ <item>executeDispatch skips destinations when the tenant has no secrets (no network).</item>
8
+ <item>dispatchToTenant returns ok on a routed reply and retry on all-failed / non-2xx / throw.</item>
9
+ <item>dispatchToTenant sends the inbound secret header and never the destination token.</item>
10
+ </responsibilities>
11
+ <non-goals><item>No network — fetch + dispatcher are stubbed.</item></non-goals>
12
+ </MODULE_CONTRACT>
13
+ <CHANGE_SUMMARY><item>RFC-0179: initial dispatch-seam test.</item></CHANGE_SUMMARY>
14
+ */
15
+
16
+ import { test, expect } from "vitest";
17
+ import {
18
+ dispatchToTenant,
19
+ executeDispatch,
20
+ type DispatchExecuteResult,
21
+ type DispatchNamespaceBinding,
22
+ } from "../dispatch.ts";
23
+ import type { IntegrationEvent } from "../port.ts";
24
+
25
+ const EVENT: IntegrationEvent = {
26
+ eventId: "evt-1",
27
+ kind: "lead",
28
+ source: "uchat",
29
+ locale: "de",
30
+ occurredAt: new Date().toISOString(),
31
+ contact: { name: "A", email: "a@example.org" },
32
+ payload: { message: "hi" },
33
+ };
34
+
35
+ test("executeDispatch skips destinations when the tenant holds no secrets", async () => {
36
+ const result = await executeDispatch({ siteId: "warpgogol-com", event: EVENT }, {});
37
+ expect(result.routed.length).toBe(0);
38
+ expect(result.skipped.includes("crm:pipedrive")).toBeTruthy();
39
+ });
40
+
41
+ function stubDispatcher(
42
+ reply: DispatchExecuteResult,
43
+ status = 200,
44
+ capture?: (r: Request) => void,
45
+ ) {
46
+ const binding: DispatchNamespaceBinding = {
47
+ get() {
48
+ return {
49
+ async fetch(request: Request): Promise<Response> {
50
+ capture?.(request);
51
+ return new Response(JSON.stringify(reply), { status });
52
+ },
53
+ };
54
+ },
55
+ };
56
+ return binding;
57
+ }
58
+
59
+ test("dispatchToTenant acks on a routed reply and forwards only the inbound secret", async () => {
60
+ let seen: Request | undefined;
61
+ const dispatcher = stubDispatcher(
62
+ { routed: ["crm:pipedrive"], failed: [], skipped: [] },
63
+ 200,
64
+ (r) => (seen = r),
65
+ );
66
+ const outcome = await dispatchToTenant(
67
+ dispatcher,
68
+ { siteId: "warpgogol-com", event: EVENT },
69
+ "inbound-secret",
70
+ );
71
+ expect(outcome.ok).toBe(true);
72
+ expect(seen?.headers.get("x-integration-secret")).toBe("inbound-secret");
73
+ const body = await seen!.text();
74
+ expect(!body.includes("PIPEDRIVE")).toBeTruthy();
75
+ });
76
+
77
+ test("dispatchToTenant retries when nothing routed but something failed", async () => {
78
+ const dispatcher = stubDispatcher({ routed: [], failed: ["crm:pipedrive"], skipped: [] });
79
+ const outcome = await dispatchToTenant(dispatcher, { siteId: "x", event: EVENT }, "s");
80
+ expect(outcome.ok).toBe(false);
81
+ });
82
+
83
+ test("dispatchToTenant retries on a non-2xx tenant response", async () => {
84
+ const dispatcher = stubDispatcher({ routed: [], failed: [], skipped: [] }, 503);
85
+ const outcome = await dispatchToTenant(dispatcher, { siteId: "x", event: EVENT }, "s");
86
+ expect(outcome.ok).toBe(false);
87
+ expect(outcome.transportError ?? "").toMatch(/503/);
88
+ });
@@ -0,0 +1,46 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>RFC-0188 Phase 3: prove the canonical→generic stage bridge is total and lands only on
4
+ valid generic stages, so the Pipedrive sync worker (STAGE_MAP) keeps working while the precise
5
+ funnel_stage is preserved. Pure — no I/O.</purpose>
6
+ <responsibilities>
7
+ <item>FUNNEL_STAGE_TO_BUFFER_STAGE covers every canonical stage and maps into BUFFER_DEAL_STAGES.</item>
8
+ <item>Terminal stages bridge to their generic equivalents; isFunnelStage guards the catalog.</item>
9
+ </responsibilities>
10
+ <non-goals><item>No DB — the bridge is a pure mapping.</item></non-goals>
11
+ </MODULE_CONTRACT>
12
+ <CHANGE_SUMMARY><item>RFC-0188 Phase 3: initial stage-bridge test.</item></CHANGE_SUMMARY>
13
+ */
14
+
15
+ import { test, expect } from "vitest";
16
+ import {
17
+ BUFFER_DEAL_STAGES,
18
+ FUNNEL_STAGE_TO_BUFFER_STAGE,
19
+ bridgeFunnelStage,
20
+ isFunnelStage,
21
+ } from "../crm-buffer.ts";
22
+ import { VISITOR_FUNNEL_STAGES } from "../funnel.ts";
23
+
24
+ test("the bridge maps every canonical funnel stage onto a valid generic stage", () => {
25
+ const generic = new Set<string>(BUFFER_DEAL_STAGES);
26
+ for (const stage of VISITOR_FUNNEL_STAGES) {
27
+ const mapped = FUNNEL_STAGE_TO_BUFFER_STAGE[stage];
28
+ expect(mapped).toBeTruthy();
29
+ expect(generic.has(mapped)).toBeTruthy();
30
+ }
31
+ // Totality: no extra keys beyond the canonical catalog.
32
+ expect(Object.keys(FUNNEL_STAGE_TO_BUFFER_STAGE).length).toBe(VISITOR_FUNNEL_STAGES.length);
33
+ });
34
+
35
+ test("terminal funnel stages bridge to their generic equivalents", () => {
36
+ expect(bridgeFunnelStage("won")).toBe("won");
37
+ expect(bridgeFunnelStage("lost")).toBe("lost");
38
+ expect(bridgeFunnelStage("offer_presented")).toBe("proposal");
39
+ expect(bridgeFunnelStage("new_session")).toBe("new");
40
+ });
41
+
42
+ test("isFunnelStage guards the canonical catalog", () => {
43
+ expect(isFunnelStage("offer_presented")).toBe(true);
44
+ expect(isFunnelStage("q_website_tier")).toBe(false); // legacy UChat string
45
+ expect(isFunnelStage("new")).toBe(false); // generic buffer stage, not a funnel stage
46
+ });