@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,309 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>RFC-0144: the single, framework-agnostic per-page semantic builder. Orchestration that was duplicated between the disk loader (@warpgogol/site-kernel-content) and the Astro path (formerly @warpgogol/business, deleted in RFC-0471) lives here once. Where the bytes come from is injected via a small SemanticContentReader seam; the disk path supplies an fs reader, the Astro path an astro:content reader. (A future RFC-0141 node ContentSourceProvider can back both readers — see RFC-0144 staging.)</purpose>
4
+ <non-goals>
5
+ <item>Do not read files or import astro:content — all I/O flows through the injected reader.</item>
6
+ <item>Do not assemble the organization profile or iterate system.md pages — callers own that.</item>
7
+ <item>Do not resolve output projection (RFC-0143) — callers attach model.output.</item>
8
+ </non-goals>
9
+ </MODULE_CONTRACT>
10
+ <CHANGE_SUMMARY>
11
+ <item>RFC-0144: initial extraction of the shared per-page builder from the two duplicated paths.</item>
12
+ <item>RFC-0372: unified all page types through extractContentBlocks + extractPageHeading; removed home-specific branch and extractMarkdownProps; extractContentBlocks returns SemanticBlock[].</item>
13
+ </CHANGE_SUMMARY>
14
+ */
15
+
16
+ import type {
17
+ SemanticBlock,
18
+ SemanticBreadcrumb,
19
+ SemanticFaqEntry,
20
+ SemanticInitiative,
21
+ SemanticOrganization,
22
+ SemanticPageModel,
23
+ SemanticPageType,
24
+ SemanticPerson,
25
+ } from "./models.ts";
26
+ import { BLOCK_EXTRACTORS } from "./block-extraction.ts";
27
+ import "./block-extractors/index.ts";
28
+ import {
29
+ buildMarkdownPageSemantic,
30
+ type MarkdownPageInput,
31
+ } from "./page-builders/markdown-page.ts";
32
+ import { extractAnswerBlocksFromMarkdown, toSemanticAnswerBlocks } from "./page-utils.ts";
33
+ import type { DerivedPriceEntry } from "./price-marker-resolver.ts";
34
+ import {
35
+ resolvePriceMarkersForSemantic,
36
+ resolveAmountMarkersForSemantic,
37
+ } from "./price-marker-resolver.ts";
38
+
39
+ /** The organization profile + enrichment pools the builder draws from. */
40
+ export interface SemanticBuildProfile {
41
+ organization: SemanticOrganization;
42
+ people: SemanticPerson[];
43
+ initiatives: SemanticInitiative[];
44
+ }
45
+
46
+ /**
47
+ * The injected content-read seam. Each implementation keeps its source's
48
+ * behavior (the fs reader substitutes content references per RFC-0045; the
49
+ * astro:content reader returns raw body). Returning `null` frontmatter means
50
+ * the page does not exist for the language and is skipped by the caller.
51
+ */
52
+ export interface SemanticContentReader {
53
+ getPageFrontmatter(pageId: string, lang: string): Promise<Record<string, unknown> | null>;
54
+ getProseBody(proseSlug: string, lang: string): Promise<string>;
55
+ getHomeLabel(lang: string): Promise<string>;
56
+ getFaqEntries(lang: string): Promise<SemanticFaqEntry[]>;
57
+ /**
58
+ * RFC-0767: load derived prices for price marker resolution. Returns null
59
+ * when the derived prices file does not exist (ENOENT). Synchronous because
60
+ * the underlying loader uses readFileSync — the method is on the reader
61
+ * interface to keep build-page.ts I/O-free per its module contract.
62
+ */
63
+ getDerivedPrices(): Record<string, DerivedPriceEntry[]> | null;
64
+ }
65
+
66
+ export interface SemanticPageBuildArgs {
67
+ pageId: string;
68
+ semanticType: SemanticPageType;
69
+ lang: string;
70
+ url: URL | string;
71
+ profile: SemanticBuildProfile;
72
+ /**
73
+ * RFC-0377: optional audience override for the page. If omitted, consumers fall back to the
74
+ * SemanticPageType derivation map (AUDIENCE_BY_PAGE_TYPE).
75
+ */
76
+ audience?: string;
77
+ /**
78
+ * RFC-0195: synthetic frontmatter used when the reader has no content entry for `pageId`
79
+ * (e.g. a Programmatic Surface page baked into the route artifact rather than a pages/*.md file).
80
+ * Shaped like a page entry: `{ title, description, blocks }`.
81
+ */
82
+ fallbackFrontmatter?: Record<string, unknown>;
83
+ /**
84
+ * RFC-0229: the canonical breadcrumb trail (Home → live ancestors → self) resolved by the render
85
+ * pipeline. When supplied it becomes the page's `breadcrumbs` (driving both the JSON-LD
86
+ * BreadcrumbList and the visible section); when omitted the builder falls back to a flat trail.
87
+ */
88
+ breadcrumbs?: SemanticBreadcrumb[];
89
+ }
90
+
91
+ /** semanticType → which enrichment pool to attach. */
92
+ const PAGE_ENRICHMENT_MAP: Partial<Record<SemanticPageType, "people" | "initiatives">> = {
93
+ about: "people",
94
+ projects: "initiatives",
95
+ };
96
+
97
+ /** semanticType values whose pages embed the FAQ list. */
98
+ const PAGE_INCLUDE_FAQ: ReadonlySet<SemanticPageType> = new Set<SemanticPageType>([
99
+ "donationContact",
100
+ ]);
101
+
102
+ /**
103
+ * Resolve the FAQ entries a page should embed (drives the FAQPage JSON-LD node).
104
+ * A page carries FAQ when its semanticType opts in (PAGE_INCLUDE_FAQ), it is the
105
+ * canonical `faq` page, or it declares a visible `faq-list` block. When `faq-list`
106
+ * blocks declare tags, entries are filtered to those tags (RFC-0208). Returns
107
+ * undefined when the page should carry no FAQPage. Shared by the home and
108
+ * markdown builder paths so a `faq-list` block on the home emits FAQPage too.
109
+ */
110
+ async function resolveFaqEntries(
111
+ reader: SemanticContentReader,
112
+ lang: string,
113
+ blocks: Array<Record<string, unknown>>,
114
+ pageId: string,
115
+ semanticType: SemanticPageType,
116
+ ): Promise<SemanticFaqEntry[] | undefined> {
117
+ const faqBlocks = blocks.filter((b) => b["type"] === "faq-list");
118
+ if (!(PAGE_INCLUDE_FAQ.has(semanticType) || pageId === "faq" || faqBlocks.length > 0)) {
119
+ return undefined;
120
+ }
121
+ const faqEntries = await reader.getFaqEntries(lang);
122
+ if (faqEntries.length === 0) return undefined;
123
+ const tags = new Set(
124
+ faqBlocks
125
+ .map((b) => ((b["props"] ?? b) as Record<string, unknown>)["tag"] as string | undefined)
126
+ .filter((t): t is string => typeof t === "string" && t.length > 0),
127
+ );
128
+ if (tags.size > 0) {
129
+ const filtered = faqEntries.filter((e) => e.tags?.some((t) => tags.has(t)));
130
+ return filtered.length > 0 ? filtered : undefined;
131
+ }
132
+ return faqEntries;
133
+ }
134
+
135
+ /** RFC-0372: extract semantic text from declared blocks into SemanticBlock[]. */
136
+ function extractContentBlocks(
137
+ blocks: Array<Record<string, unknown>>,
138
+ ctx: { pageId: string; lang: string; siteUrl: string },
139
+ ): SemanticBlock[] {
140
+ const result: SemanticBlock[] = [];
141
+ for (const block of blocks) {
142
+ const blockType = String(block["type"] ?? "");
143
+ const blockId = String(block["id"] ?? `block-${result.length}`);
144
+ if (!blockType) continue;
145
+ const extractor = BLOCK_EXTRACTORS.get(blockType);
146
+ if (!extractor) continue;
147
+ const props = (block["props"] ?? block) as Record<string, unknown>;
148
+ const extracted = extractor.extract(props, ctx);
149
+ // RFC-0372: always push the block — no-op extractors return heading: "".
150
+ result.push({
151
+ id: blockId,
152
+ blockType,
153
+ heading: extracted.heading ?? "",
154
+ summary: extracted.lead,
155
+ body: extracted.body,
156
+ items: extracted.items,
157
+ extractedAt: new Date().toISOString(),
158
+ extractorVersion: "1.0.0",
159
+ });
160
+ }
161
+ return result;
162
+ }
163
+
164
+ /**
165
+ * RFC-0372: Scan all frontmatter blocks in declaration order for header.heading / header.subheading.
166
+ * Works with any block type that has header props (hero-decision-card, markdown, etc.) — not
167
+ * limited to type: "markdown". Falls back to frontmatter title/description.
168
+ */
169
+ function extractPageHeading(
170
+ blocks: Array<Record<string, unknown>>,
171
+ fallbackTitle: string,
172
+ fallbackDescription: string,
173
+ ): { heading: string; lead?: string } {
174
+ let heading = fallbackTitle;
175
+ let lead: string | undefined;
176
+ for (const block of blocks) {
177
+ const props = (block["props"] ?? block) as Record<string, unknown>;
178
+ const headerHeading =
179
+ props["header"] && typeof props["header"] === "object"
180
+ ? safeString((props["header"] as Record<string, unknown>)["heading"])
181
+ : safeString(props["heading"]);
182
+ const headerSubheading =
183
+ props["header"] && typeof props["header"] === "object"
184
+ ? safeString((props["header"] as Record<string, unknown>)["subheading"])
185
+ : safeString(props["lead"]);
186
+ if (!heading || heading === fallbackTitle) {
187
+ if (headerHeading) heading = headerHeading;
188
+ }
189
+ if (!lead) {
190
+ if (headerSubheading) lead = headerSubheading;
191
+ }
192
+ if (heading !== fallbackTitle && lead) break;
193
+ }
194
+ if (!lead) lead = fallbackDescription || undefined;
195
+ return { heading, ...(lead ? { lead } : {}) };
196
+ }
197
+
198
+ function safeString(v: unknown): string | undefined {
199
+ return typeof v === "string" && v.length > 0 ? v : undefined;
200
+ }
201
+
202
+ /**
203
+ * RFC-0144/RFC-0372: build a single SemanticPageModel via the injected reader. This is
204
+ * the one place the per-page construction logic lives. All page types (including home)
205
+ * go through the same unified pipeline: extractContentBlocks → extractPageHeading →
206
+ * buildMarkdownPageSemantic with a merged blocks array. `model.organization`
207
+ * and `model.output` (RFC-0143) are attached by the caller.
208
+ */
209
+ export async function buildSemanticPageModelWith(
210
+ reader: SemanticContentReader,
211
+ args: SemanticPageBuildArgs,
212
+ ): Promise<SemanticPageModel | null> {
213
+ const { pageId, semanticType, lang, url, profile } = args;
214
+
215
+ const frontmatter = (await reader.getPageFrontmatter(pageId, lang)) ?? args.fallbackFrontmatter;
216
+ if (!frontmatter) return null;
217
+
218
+ const title = (frontmatter["title"] as string) ?? "";
219
+ const rawDescription = (frontmatter["description"] as string) ?? "";
220
+
221
+ const allBlocks = (frontmatter["blocks"] as Array<Record<string, unknown>> | undefined) ?? [];
222
+
223
+ // RFC-0372: Extract block-derived SemanticBlocks from frontmatter.
224
+ const blockDerived = extractContentBlocks(allBlocks, { pageId, lang, siteUrl: url.toString() });
225
+
226
+ // RFC-0372: Extract prose from contentRef (if any) and convert to SemanticBlock[].
227
+ // Scan frontmatter blocks for a contentRef in any block's props (typically markdown blocks).
228
+ let proseBlocks: SemanticBlock[] = [];
229
+ for (const block of allBlocks) {
230
+ const props = (block["props"] ?? block) as Record<string, unknown>;
231
+ const contentRef = props["contentRef"] as string | undefined;
232
+ if (contentRef) {
233
+ const proseSlug = contentRef.replace(/^prose\//, "");
234
+ const bodyText = await reader.getProseBody(proseSlug, lang);
235
+ const markdownBlocks = extractAnswerBlocksFromMarkdown(bodyText);
236
+ proseBlocks = toSemanticAnswerBlocks(markdownBlocks);
237
+ break;
238
+ }
239
+ }
240
+
241
+ // RFC-0372: Merge block-derived and prose-derived blocks in frontmatter declaration order.
242
+ // For pages with both frontmatter blocks and prose, prose blocks are appended after
243
+ // block-derived blocks (prose contentRef is typically the markdown body following the
244
+ // structured blocks).
245
+ const mergedBlocks = [...blockDerived, ...proseBlocks];
246
+
247
+ // RFC-0372: Extract page heading from frontmatter block props (any block type with
248
+ // header.heading), falling back to title/description.
249
+ const { heading, lead } = extractPageHeading(allBlocks, title, rawDescription);
250
+
251
+ // RFC-0767: resolve {price:offering:chargeRef} markers to source-currency (EUR)
252
+ // strings for JSON-LD and meta tags. Markers in heading, lead, and description
253
+ // are resolved before entering the SemanticPageModel.
254
+ const derivedPrices = reader.getDerivedPrices();
255
+ const resolvedHeading = resolveAmountMarkersForSemantic(
256
+ resolvePriceMarkersForSemantic(heading, lang, derivedPrices),
257
+ lang,
258
+ );
259
+ const resolvedLead = lead
260
+ ? resolveAmountMarkersForSemantic(
261
+ resolvePriceMarkersForSemantic(lead, lang, derivedPrices),
262
+ lang,
263
+ )
264
+ : undefined;
265
+ const resolvedDescription = resolveAmountMarkersForSemantic(
266
+ resolvePriceMarkersForSemantic(rawDescription, lang, derivedPrices),
267
+ lang,
268
+ );
269
+
270
+ const baseInput = { lang, url, title, description: resolvedDescription };
271
+
272
+ const homeLabel = await reader.getHomeLabel(lang);
273
+ const enrichmentKey = PAGE_ENRICHMENT_MAP[semanticType];
274
+
275
+ const input: MarkdownPageInput = {
276
+ type: semanticType,
277
+ ...baseInput,
278
+ ...(args.audience ? { audience: args.audience } : {}),
279
+ heading: resolvedHeading,
280
+ ...(resolvedLead ? { lead: resolvedLead } : {}),
281
+ blocks: mergedBlocks,
282
+ breadcrumbsContent: { homeLabel },
283
+ ...(args.breadcrumbs && args.breadcrumbs.length > 0 ? { breadcrumbs: args.breadcrumbs } : {}),
284
+ ...(enrichmentKey === "people" ? { people: profile.people } : {}),
285
+ ...(enrichmentKey === "initiatives" ? { initiatives: profile.initiatives } : {}),
286
+ // RFC-0200: a person profile page attaches just the profiled person (matched by
287
+ // name) so a Person JSON-LD node emits as the page's subject.
288
+ ...(semanticType === "person"
289
+ ? { people: profile.people.filter((p) => p.name === title) }
290
+ : {}),
291
+ };
292
+
293
+ const faqEntries = await resolveFaqEntries(reader, lang, allBlocks, pageId, semanticType);
294
+ if (faqEntries) {
295
+ input.faqEntries = faqEntries.map((e) => ({
296
+ ...e,
297
+ question: resolveAmountMarkersForSemantic(
298
+ resolvePriceMarkersForSemantic(e.question, lang, derivedPrices),
299
+ lang,
300
+ ),
301
+ answer: resolveAmountMarkersForSemantic(
302
+ resolvePriceMarkersForSemantic(e.answer, lang, derivedPrices),
303
+ lang,
304
+ ),
305
+ }));
306
+ }
307
+
308
+ return buildMarkdownPageSemantic(input);
309
+ }
@@ -0,0 +1,278 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>RFC-0148: pure projectors that map canonical business-schema data into the semantic model, plus the projection visibility boundary. Framework-agnostic and side-effect-free, so the disk llms path and the Astro JSON-LD path project every business file identically. Lives in @warpgogol/werkstatt-shared/share (not @warpgogol/werkstatt-site/pbp) because @warpgogol/werkstatt-site/pbp depends on @warpgogol/site-kernel-content — placing the projectors here keeps the node disk loader cycle-free.</purpose>
4
+ <non-goals>
5
+ <item>Do not read files — pure mapping over already-loaded data.</item>
6
+ <item>Do not project privacy-sensitive domains (external-services, compliance).</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0148: initial projection registry — offer + location projectors and the visibility boundary.</item>
11
+ <item>RFC-0287: added projectWeb for the Agent Surface knowledge tier.</item>
12
+ <item>RFC-0373: added projectServices for business service catalog projection.</item>
13
+ </CHANGE_SUMMARY>
14
+ */
15
+
16
+ import type {
17
+ SemanticOffer,
18
+ SemanticLocation,
19
+ SemanticPerson,
20
+ SemanticService,
21
+ SemanticWeb,
22
+ } from "./models.ts";
23
+
24
+ /**
25
+ * RFC-0148 privacy boundary. `public` projects to AI (llms) + JSON-LD; `pageMeta`
26
+ * is build-time only (e.g. legal-page dates → sitemap lastmod); `none` is never
27
+ * projected to any public output. `externalServices` (vendor names/addresses)
28
+ * and `compliance` (internal GoBD dates) are deliberately `none`.
29
+ */
30
+ export const BUSINESS_DOMAIN_VISIBILITY = {
31
+ company: "public",
32
+ legal: "public",
33
+ contact: "public",
34
+ offer: "public",
35
+ service: "public",
36
+ location: "public",
37
+ web: "public",
38
+ people: "public",
39
+ trust: "public",
40
+ faq: "public",
41
+ meta: "pageMeta",
42
+ compliance: "none",
43
+ externalServices: "none",
44
+ } as const;
45
+
46
+ export type BusinessDomain = keyof typeof BUSINESS_DOMAIN_VISIBILITY;
47
+ export type BusinessVisibility = (typeof BUSINESS_DOMAIN_VISIBILITY)[BusinessDomain];
48
+
49
+ const PRICE_LABELS: ReadonlyArray<readonly [string, string]> = [
50
+ ["monthly", "Monthly"],
51
+ ["yearly", "Yearly"],
52
+ ["setup", "Setup"],
53
+ ];
54
+
55
+ /** RFC-0147/RFC-0148: project the canonical offer (price + guarantees + growth modules + terms). */
56
+ export function projectOffer(data: Record<string, unknown> | undefined): SemanticOffer | undefined {
57
+ if (!data) return undefined;
58
+
59
+ const price = (data["price"] ?? {}) as Record<string, unknown>;
60
+ const currency = typeof data["currency"] === "string" ? (data["currency"] as string) : undefined;
61
+ const prices = PRICE_LABELS.filter(
62
+ ([key]) => typeof price[key] === "string" && (price[key] as string).length > 0,
63
+ ).map(([key, label]) => ({
64
+ id: key,
65
+ label,
66
+ amount: price[key] as string,
67
+ ...(currency ? { currency } : {}),
68
+ }));
69
+
70
+ const rawGuarantees = (data["guarantees"] ?? {}) as Record<
71
+ string,
72
+ { label?: string; detail?: string } | undefined
73
+ >;
74
+ const guarantees = Object.entries(rawGuarantees)
75
+ .filter(([, g]) => g && typeof g.label === "string" && g.label.length > 0)
76
+ .map(([id, g]) => ({
77
+ id,
78
+ label: g!.label as string,
79
+ ...(g!.detail ? { detail: g!.detail } : {}),
80
+ }));
81
+
82
+ const rawGrowthModules = (data["growthModules"] ?? {}) as Record<
83
+ string,
84
+ { label?: string; description?: string; price?: string } | undefined
85
+ >;
86
+ const growthModules = Object.entries(rawGrowthModules)
87
+ .filter(([, m]) => m && typeof m.label === "string" && m.label.length > 0)
88
+ .map(([id, m]) => ({
89
+ id,
90
+ label: m!.label as string,
91
+ ...(m!.description ? { description: m!.description } : {}),
92
+ ...(m!.price ? { price: m!.price } : {}),
93
+ }));
94
+
95
+ const changePrice = typeof data["changePrice"] === "string" ? data["changePrice"] : undefined;
96
+ const hourlyRate = typeof data["hourlyRate"] === "string" ? data["hourlyRate"] : undefined;
97
+ const billingDay = typeof data["billingDay"] === "string" ? data["billingDay"] : undefined;
98
+ const rawCapacity = data["capacity"] as Record<string, unknown> | undefined;
99
+ const rawSlotRange = rawCapacity?.["slotRange"] as Record<string, unknown> | undefined;
100
+ const capacity =
101
+ rawCapacity &&
102
+ rawCapacity["enabled"] === true &&
103
+ typeof rawCapacity["timezone"] === "string" &&
104
+ typeof rawCapacity["startsAt"] === "string" &&
105
+ (rawCapacity["cadence"] === "monthly" || rawCapacity["cadence"] === "fixed-days") &&
106
+ typeof rawSlotRange?.["min"] === "number" &&
107
+ typeof rawSlotRange?.["max"] === "number" &&
108
+ typeof rawCapacity["maxSlotsPerWave"] === "number"
109
+ ? {
110
+ enabled: true,
111
+ timezone: rawCapacity["timezone"],
112
+ startsAt: rawCapacity["startsAt"],
113
+ cadence: rawCapacity["cadence"] as "monthly" | "fixed-days",
114
+ ...(typeof rawCapacity["cadenceDays"] === "number"
115
+ ? { cadenceDays: rawCapacity["cadenceDays"] }
116
+ : {}),
117
+ slotRange: { min: rawSlotRange["min"], max: rawSlotRange["max"] },
118
+ maxSlotsPerWave: rawCapacity["maxSlotsPerWave"],
119
+ availabilityStatus: "unknown" as const,
120
+ }
121
+ : undefined;
122
+
123
+ if (
124
+ prices.length === 0 &&
125
+ guarantees.length === 0 &&
126
+ growthModules.length === 0 &&
127
+ !capacity &&
128
+ !changePrice &&
129
+ !hourlyRate &&
130
+ !billingDay
131
+ )
132
+ return undefined;
133
+
134
+ return {
135
+ ...(prices.length ? { prices } : {}),
136
+ ...(guarantees.length ? { guarantees } : {}),
137
+ ...(growthModules.length ? { growthModules } : {}),
138
+ ...(capacity ? { capacity } : {}),
139
+ ...(changePrice ? { changePrice } : {}),
140
+ ...(hourlyRate ? { hourlyRate } : {}),
141
+ ...(billingDay ? { billingDay } : {}),
142
+ };
143
+ }
144
+
145
+ /** RFC-0200: project canonical Person records into SemanticPerson[]. Bio becomes
146
+ * the person description; `lifespan.died` drives `isDeceased` + `deathDate`;
147
+ * `affiliations`/`sameAs` are carried for governance derivation + entity grounding.
148
+ * Records without a name are dropped. Order preserved (loaders pre-sort by `order`). */
149
+ export function projectPeople(
150
+ records: ReadonlyArray<Record<string, unknown>> | undefined,
151
+ ): SemanticPerson[] {
152
+ if (!records?.length) return [];
153
+ const people: SemanticPerson[] = [];
154
+ for (const r of records) {
155
+ const name = typeof r["name"] === "string" ? (r["name"] as string).trim() : "";
156
+ if (!name) continue;
157
+ const bio = typeof r["bio"] === "string" ? (r["bio"] as string).trim() : "";
158
+ const role = typeof r["role"] === "string" ? (r["role"] as string).trim() : "";
159
+ const affiliations = Array.isArray(r["affiliations"])
160
+ ? (r["affiliations"] as unknown[]).filter((a): a is string => typeof a === "string")
161
+ : [];
162
+ const sameAs = Array.isArray(r["sameAs"])
163
+ ? (r["sameAs"] as unknown[]).filter((s): s is string => typeof s === "string")
164
+ : [];
165
+ const lifespan = (r["lifespan"] ?? undefined) as Record<string, unknown> | undefined;
166
+ const born = lifespan?.["born"];
167
+ const died = lifespan?.["died"];
168
+
169
+ /* RFC-0512: map location string ("City, Region") → SemanticPerson.address. */
170
+ const locationRaw = typeof r["location"] === "string" ? (r["location"] as string).trim() : "";
171
+ let address: SemanticPerson["address"] | undefined;
172
+ if (locationRaw) {
173
+ const parts = locationRaw.split(",").map((s) => s.trim());
174
+ address = {
175
+ addressLocality: parts[0] ?? "",
176
+ ...(parts[1] ? { addressRegion: parts[1] } : {}),
177
+ addressCountry: "DE",
178
+ };
179
+ }
180
+
181
+ /* RFC-0512: map capabilities → SemanticPerson.knowsAbout. */
182
+ const capabilities = Array.isArray(r["capabilities"])
183
+ ? (r["capabilities"] as unknown[]).filter((c): c is string => typeof c === "string")
184
+ : [];
185
+
186
+ /* RFC-0512: map organization name → SemanticPerson.affiliation. */
187
+ const orgName =
188
+ typeof r["organizationName"] === "string" ? (r["organizationName"] as string).trim() : "";
189
+
190
+ people.push({
191
+ name,
192
+ ...(role ? { role } : {}),
193
+ ...(bio ? { description: bio } : {}),
194
+ ...(affiliations.length ? { affiliations } : {}),
195
+ ...(born != null ? { birthDate: String(born) } : {}),
196
+ ...(died != null ? { deathDate: String(died), isDeceased: true } : {}),
197
+ ...(sameAs.length ? { sameAs } : {}),
198
+ ...(address ? { address } : {}),
199
+ ...(capabilities.length ? { knowsAbout: capabilities } : {}),
200
+ ...(orgName ? { affiliation: { name: orgName } } : {}),
201
+ });
202
+ }
203
+ return people;
204
+ }
205
+
206
+ /** RFC-0148: project location (NAP locality/region/country + service area). */
207
+ export function projectLocation(
208
+ data: Record<string, unknown> | undefined,
209
+ ): SemanticLocation | undefined {
210
+ if (!data) return undefined;
211
+
212
+ const place = (key: string): string | undefined => {
213
+ const node = data[key] as { name?: unknown } | undefined;
214
+ return typeof node?.name === "string" && node.name.length > 0 ? node.name : undefined;
215
+ };
216
+
217
+ const locality = place("city");
218
+ const region = place("region");
219
+ const country = place("country");
220
+ const serviceAreaRaw = data["serviceArea"];
221
+ const serviceArea = Array.isArray(serviceAreaRaw)
222
+ ? serviceAreaRaw.filter((s): s is string => typeof s === "string" && s.length > 0)
223
+ : undefined;
224
+
225
+ if (!locality && !region && !country && !(serviceArea && serviceArea.length)) return undefined;
226
+ return {
227
+ ...(locality ? { locality } : {}),
228
+ ...(region ? { region } : {}),
229
+ ...(country ? { country } : {}),
230
+ ...(serviceArea && serviceArea.length ? { serviceArea } : {}),
231
+ };
232
+ }
233
+
234
+ /** RFC-0373: project business service catalog entries into SemanticService[].
235
+ * Records without a name are dropped. Order preserved (loaders pre-sort by `order`). */
236
+ export function projectServices(
237
+ records: ReadonlyArray<Record<string, unknown>> | undefined,
238
+ ): SemanticService[] {
239
+ if (!records?.length) return [];
240
+ const services: SemanticService[] = [];
241
+ for (const r of records) {
242
+ const name = typeof r["name"] === "string" ? (r["name"] as string).trim() : "";
243
+ if (!name) continue;
244
+ const slug = typeof r["slug"] === "string" ? (r["slug"] as string) : "";
245
+ const description =
246
+ typeof r["description"] === "string" ? (r["description"] as string).trim() : "";
247
+ services.push({
248
+ id: slug || name,
249
+ name,
250
+ ...(description ? { description } : {}),
251
+ });
252
+ }
253
+ return services;
254
+ }
255
+
256
+ /**
257
+ * RFC-0287: project the business web/domain identity record (business/{lang}/web.md
258
+ * — primaryUrl, statusUrl, domains.{primary,german}). A thin, faithful passthrough of
259
+ * an already-validated, stable cross-app shape — not a generic business-file dump.
260
+ */
261
+ export function projectWeb(data: Record<string, unknown> | undefined): SemanticWeb | undefined {
262
+ if (!data) return undefined;
263
+
264
+ const primaryUrl = typeof data["primaryUrl"] === "string" ? data["primaryUrl"] : undefined;
265
+ const statusUrl = typeof data["statusUrl"] === "string" ? data["statusUrl"] : undefined;
266
+ const domainsRaw = (data["domains"] ?? {}) as Record<string, unknown>;
267
+ const primary = typeof domainsRaw["primary"] === "string" ? domainsRaw["primary"] : undefined;
268
+ const german = typeof domainsRaw["german"] === "string" ? domainsRaw["german"] : undefined;
269
+
270
+ if (!primaryUrl && !statusUrl && !primary && !german) return undefined;
271
+ return {
272
+ ...(primaryUrl ? { primaryUrl } : {}),
273
+ ...(statusUrl ? { statusUrl } : {}),
274
+ ...(primary || german
275
+ ? { domains: { ...(primary ? { primary } : {}), ...(german ? { german } : {}) } }
276
+ : {}),
277
+ };
278
+ }
@@ -0,0 +1,36 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Node-only loader for derived-prices.generated.json (RFC-0767). Reads the file from process.cwd()/src/derived-prices.generated.json and returns null on ENOENT. NOT exported from the semantic barrel — uses node:fs and would pollute client bundles.</purpose>
4
+ <non-goals>
5
+ <item>Does not parse or validate the JSON shape — just reads and JSON.parse.</item>
6
+ <item>Does not cache — callers are responsible for calling once and passing the result.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0767: relocated loadDerivedPrices from packages/ui to packages/share to break circular dependency. Node-only subpath export.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import { readFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import type { DerivedPriceEntry } from "./price-marker-resolver.ts";
17
+
18
+ export function loadDerivedPrices(
19
+ cwd: string = process.cwd(),
20
+ ): Record<string, DerivedPriceEntry[]> | null {
21
+ const filePath = join(cwd, "src", "derived-prices.generated.json");
22
+ let raw: string;
23
+ try {
24
+ raw = readFileSync(filePath, "utf-8");
25
+ } catch (err: unknown) {
26
+ if (err instanceof Error && (err as NodeJS.ErrnoException).code === "ENOENT") {
27
+ console.warn(
28
+ `[derived-prices] ${filePath} not found — currency-aware price variants will not render. ` +
29
+ `Run: pnpm exec werkstatt run derived-prices.materialize --site <siteId>`,
30
+ );
31
+ return null;
32
+ }
33
+ throw err;
34
+ }
35
+ return JSON.parse(raw) as Record<string, DerivedPriceEntry[]>;
36
+ }