@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,333 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Framework-agnostic content reference index resolver — loads a build-time generated index
4
+ and resolves braceless collection.file.field references without astro:content (RFC-0527).</purpose>
5
+ <non-goals>
6
+ <item>Do not generate the index — that is content.ref-index.generate in @warpgogol/site-kernel-codegen.</item>
7
+ <item>Do not validate references — that is content.references.validate in @warpgogol/site-kernel-checks.</item>
8
+ <item>Do not handle brace-delimited {collection.file.field} syntax — legacy, removed by RFC-0529.</item>
9
+ </non-goals>
10
+ </MODULE_CONTRACT>
11
+ <CHANGE_SUMMARY>
12
+ <item>RFC-0045: Initial implementation of content data references in markdown files.</item>
13
+ <item>RFC-0527: Rewrite as framework-agnostic index-based resolver. Remove astro:content dependency.
14
+ Replace parseContentReference, resolveContentReference, substituteContentReferences,
15
+ substituteContentReferencesInData with loadContentRefIndex, resolveReference,
16
+ resolveReferencesInString, resolveReferencesDeep.</item>
17
+ <item>RFC-0602: allow null generatedAt in ContentRefIndex for timestamp determinism.</item>
18
+ <item>RFC-0731: Add SourceRef type and sourceRef optional parameter for this. self-reference syntax.
19
+ this. is expanded to collection.file. before pattern matching. REF-12 for missing sourceRef,
20
+ REF-13 for unresolvable field after expansion.</item>
21
+ </CHANGE_SUMMARY>
22
+ */
23
+
24
+ import { readFileSync } from "node:fs";
25
+ import { parse } from "yaml";
26
+ import { substituteRefsDeep } from "./content/substitute-deep.ts";
27
+ import { resolveFieldPath } from "./content/resolve-field-path.ts";
28
+ import { scanFormulas, resolveFormula } from "./formula-eval.ts";
29
+
30
+ export interface ContentRefIndex {
31
+ version: 1;
32
+ generatedAt: string | null;
33
+ entries: Record<string, Record<string, Record<string, unknown>>>;
34
+ collections: string[];
35
+ }
36
+
37
+ export interface ResolveReferenceResult {
38
+ value: unknown;
39
+ resolved: boolean;
40
+ error?: string;
41
+ }
42
+
43
+ export interface SourceRef {
44
+ collection: string;
45
+ file: string;
46
+ }
47
+
48
+ const REF_PATTERN = /^([a-z][a-z-]*)\.([a-z0-9-/]+)\.(.+)$/;
49
+ const FIELD_PATH_PATTERN = /[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*/;
50
+ const PURE_REF_PATTERN = new RegExp(`^[a-z][a-z-]*\\.[a-z0-9-/]+\\.${FIELD_PATH_PATTERN.source}$`);
51
+ const BRACELESS_SCAN_PATTERN = new RegExp(
52
+ `[a-z][a-z-]*\\.[a-z0-9-/]+\\.${FIELD_PATH_PATTERN.source}`,
53
+ "g",
54
+ );
55
+ const THIS_PREFIX = "this.";
56
+ const THIS_SCAN_PATTERN = new RegExp(`this\\.${FIELD_PATH_PATTERN.source}`, "g");
57
+ const DEFAULT_INDEX_PATH = "src/content-ref-index.generated.yaml";
58
+
59
+ function expandThisRef(ref: string, sourceRef?: SourceRef): string | null {
60
+ if (!ref.startsWith(THIS_PREFIX)) return null;
61
+ if (!sourceRef) return null;
62
+ const fieldPath = ref.slice(THIS_PREFIX.length);
63
+ return `${sourceRef.collection}.${sourceRef.file}.${fieldPath}`;
64
+ }
65
+
66
+ export const EMPTY_CONTENT_REF_INDEX: ContentRefIndex = {
67
+ version: 1,
68
+ generatedAt: "",
69
+ entries: {},
70
+ collections: [],
71
+ };
72
+
73
+ let cachedIndex: ContentRefIndex | null | undefined;
74
+
75
+ export function loadContentRefIndex(indexPath: string): ContentRefIndex | null {
76
+ try {
77
+ const raw = readFileSync(indexPath, "utf8");
78
+ const parsed = parse(raw) as ContentRefIndex;
79
+ if (parsed && parsed.version === 1 && parsed.entries) {
80
+ return parsed;
81
+ }
82
+ return null;
83
+ } catch (err: unknown) {
84
+ if (err instanceof Error && (err as NodeJS.ErrnoException).code === "ENOENT") {
85
+ console.warn(
86
+ `[content-ref-index] ${indexPath} not found — formula references =(...) and braceless content references will not resolve. ` +
87
+ `Run: pnpm exec werkstatt run content.ref-index.generate --site <siteId>`,
88
+ );
89
+ }
90
+ return null;
91
+ }
92
+ }
93
+
94
+ export function getContentRefIndex(indexPath?: string): ContentRefIndex | null {
95
+ if (cachedIndex === undefined) {
96
+ cachedIndex = loadContentRefIndex(indexPath ?? DEFAULT_INDEX_PATH);
97
+ }
98
+ return cachedIndex;
99
+ }
100
+
101
+ export function resetContentRefIndexCache(): void {
102
+ cachedIndex = undefined;
103
+ }
104
+
105
+ export function resolveReference(
106
+ index: ContentRefIndex,
107
+ ref: string,
108
+ lang: string,
109
+ defaultLang: string,
110
+ sourceRef?: SourceRef,
111
+ ): ResolveReferenceResult {
112
+ // RFC-0731: Expand this. prefix before pattern matching
113
+ let effectiveRef = ref;
114
+ if (ref.startsWith(THIS_PREFIX)) {
115
+ if (!sourceRef) {
116
+ return {
117
+ value: undefined,
118
+ resolved: false,
119
+ error: `REF-12: this. reference used without sourceRef context`,
120
+ };
121
+ }
122
+ const expanded = expandThisRef(ref, sourceRef);
123
+ if (!expanded) {
124
+ return {
125
+ value: undefined,
126
+ resolved: false,
127
+ error: `REF-12: this. reference used without sourceRef context`,
128
+ };
129
+ }
130
+ effectiveRef = expanded;
131
+ }
132
+
133
+ const match = effectiveRef.match(REF_PATTERN);
134
+ if (!match) {
135
+ return { value: undefined, resolved: false, error: `Invalid reference syntax: ${ref}` };
136
+ }
137
+
138
+ const [, collection, file, fieldPathStr] = match;
139
+ const fieldPath = fieldPathStr.split(".");
140
+
141
+ const collectionEntries = index.entries[collection];
142
+ if (!collectionEntries) {
143
+ return {
144
+ value: undefined,
145
+ resolved: false,
146
+ error: `REF-01: Collection "${collection}" not found in index`,
147
+ };
148
+ }
149
+
150
+ const fileEntries = collectionEntries[file];
151
+ if (!fileEntries) {
152
+ return {
153
+ value: undefined,
154
+ resolved: false,
155
+ error: `REF-02: File "${file}" not found in collection "${collection}"`,
156
+ };
157
+ }
158
+
159
+ let data = fileEntries[lang];
160
+ if (!data && lang !== defaultLang) {
161
+ data = fileEntries[defaultLang];
162
+ }
163
+ if (!data) {
164
+ return {
165
+ value: undefined,
166
+ resolved: false,
167
+ error: `REF-02: No entry for language "${lang}" or fallback "${defaultLang}" for ${collection}.${file}`,
168
+ };
169
+ }
170
+
171
+ const { value, missingField } = resolveFieldPath(data, fieldPath);
172
+ if (missingField || value === undefined) {
173
+ // Field-level fallback: if the field path fails in the requested language,
174
+ // retry with the default language data before reporting REF-03.
175
+ if (lang !== defaultLang) {
176
+ const fallbackData = fileEntries[defaultLang];
177
+ if (fallbackData) {
178
+ const fallbackResult = resolveFieldPath(fallbackData, fieldPath);
179
+ if (!fallbackResult.missingField && fallbackResult.value !== undefined) {
180
+ return { value: fallbackResult.value, resolved: true };
181
+ }
182
+ }
183
+ }
184
+ // RFC-0731: If this was a this. reference, report REF-13 instead of REF-03
185
+ if (ref.startsWith(THIS_PREFIX)) {
186
+ return {
187
+ value: undefined,
188
+ resolved: false,
189
+ error: `REF-13: this.${fieldPathStr} unresolved — field "${missingField ?? fieldPath[fieldPath.length - 1]}" not found in ${sourceRef?.collection}.${sourceRef?.file}`,
190
+ };
191
+ }
192
+ return {
193
+ value: undefined,
194
+ resolved: false,
195
+ error: `REF-03: Field "${missingField ?? fieldPath[fieldPath.length - 1]}" not found in ${collection}.${file}`,
196
+ };
197
+ }
198
+
199
+ return { value, resolved: true };
200
+ }
201
+
202
+ export function resolveReferencesInString(
203
+ index: ContentRefIndex,
204
+ text: string,
205
+ lang: string,
206
+ defaultLang: string,
207
+ sourceRef?: SourceRef,
208
+ ): string {
209
+ // RFC-0731: Check for this. as a pure reference (entire text is this.field.path)
210
+ if (
211
+ sourceRef &&
212
+ text.startsWith(THIS_PREFIX) &&
213
+ THIS_SCAN_PATTERN.test(text) &&
214
+ text.trim() === text.trim()
215
+ ) {
216
+ const expanded = expandThisRef(text.trim(), sourceRef);
217
+ if (expanded) {
218
+ const result = resolveReference(index, expanded, lang, defaultLang);
219
+ if (result.resolved) {
220
+ return formatValue(result.value);
221
+ }
222
+ }
223
+ }
224
+ THIS_SCAN_PATTERN.lastIndex = 0;
225
+
226
+ if (PURE_REF_PATTERN.test(text)) {
227
+ const result = resolveReference(index, text, lang, defaultLang);
228
+ if (result.resolved) {
229
+ return formatValue(result.value);
230
+ }
231
+ return text;
232
+ }
233
+
234
+ // RFC-0570: resolve =(...) formula expressions first
235
+ const formulas = scanFormulas(text);
236
+ let formulaResult = text;
237
+ if (formulas.length > 0) {
238
+ // Process formulas in reverse order to preserve indices
239
+ for (let i = formulas.length - 1; i >= 0; i--) {
240
+ const { start, end, expression } = formulas[i];
241
+ const result = resolveFormula(index, expression, lang, defaultLang, sourceRef);
242
+ const replacement = result.resolved ? result.value : "";
243
+ formulaResult = formulaResult.slice(0, start) + replacement + formulaResult.slice(end);
244
+ }
245
+ }
246
+
247
+ // If the entire string was a formula, return the formula result
248
+ if (formulas.length > 0 && formulas[0].start === 0 && formulas[0].end === text.length) {
249
+ return formulaResult;
250
+ }
251
+
252
+ let result = formulaResult;
253
+ const pattern = new RegExp(BRACELESS_SCAN_PATTERN.source, "g");
254
+ let match: RegExpExecArray | null;
255
+ const replacements: Array<{ start: number; end: number; resolved: string }> = [];
256
+
257
+ while ((match = pattern.exec(formulaResult)) !== null) {
258
+ const candidate = match[0];
259
+ const collectionMatch = candidate.match(/^([a-z][a-z-]*)\./);
260
+ if (!collectionMatch) continue;
261
+ const collection = collectionMatch[1];
262
+ if (!index.entries[collection]) continue;
263
+
264
+ const resolved = resolveReference(index, candidate, lang, defaultLang, sourceRef);
265
+ if (resolved.resolved) {
266
+ replacements.push({
267
+ start: match.index,
268
+ end: match.index + candidate.length,
269
+ resolved: formatValue(resolved.value),
270
+ });
271
+ }
272
+ }
273
+
274
+ // RFC-0731: Scan for this. references in mixed strings
275
+ if (sourceRef) {
276
+ const thisPattern = new RegExp(THIS_SCAN_PATTERN.source, "g");
277
+ let thisMatch: RegExpExecArray | null;
278
+ while ((thisMatch = thisPattern.exec(formulaResult)) !== null) {
279
+ const candidate = thisMatch[0];
280
+ const expanded = expandThisRef(candidate, sourceRef);
281
+ if (!expanded) continue;
282
+ const resolved = resolveReference(index, expanded, lang, defaultLang);
283
+ if (resolved.resolved) {
284
+ // Check for overlap with existing replacements
285
+ const thisStart = thisMatch.index;
286
+ const thisEnd = thisStart + candidate.length;
287
+ const overlaps = replacements.some((r) => thisStart < r.end && thisEnd > r.start);
288
+ if (!overlaps) {
289
+ replacements.push({
290
+ start: thisStart,
291
+ end: thisEnd,
292
+ resolved: formatValue(resolved.value),
293
+ });
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ for (let i = replacements.length - 1; i >= 0; i--) {
300
+ const { start, end, resolved } = replacements[i];
301
+ result = result.slice(0, start) + resolved + result.slice(end);
302
+ }
303
+
304
+ return result;
305
+ }
306
+
307
+ export async function resolveReferencesDeep(
308
+ index: ContentRefIndex,
309
+ data: unknown,
310
+ lang: string,
311
+ defaultLang: string,
312
+ sourceRef?: SourceRef,
313
+ ): Promise<unknown> {
314
+ return substituteRefsDeep(data, (value) =>
315
+ Promise.resolve(resolveReferencesInString(index, value, lang, defaultLang, sourceRef)),
316
+ );
317
+ }
318
+
319
+ function formatValue(value: unknown): string {
320
+ if (value === null || value === undefined) {
321
+ return "";
322
+ }
323
+ if (typeof value === "string") {
324
+ return value;
325
+ }
326
+ if (typeof value === "number" || typeof value === "boolean") {
327
+ return String(value);
328
+ }
329
+ if (Array.isArray(value)) {
330
+ return value.join(", ");
331
+ }
332
+ return "";
333
+ }
@@ -0,0 +1,75 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Maintains packages/share/src/counter-utils.ts as an authored share authored module so agents can evolve it without rediscovering local boundaries.</purpose>
4
+ <non-goals>
5
+ <item>Do not depend on GSAP or DOM.</item>
6
+ <item>Do not handle animation logic.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>Created shared counter utilities to eliminate duplication between hero and impact sections.</item>
11
+ <item>getStartValue now generates "1" in highest digit (e.g., "1,000" instead of "0,000") for better visual feedback during count-up.</item>
12
+ </CHANGE_SUMMARY>
13
+ */
14
+
15
+ export interface CounterStat {
16
+ value?: string;
17
+ label: string;
18
+ numericValue?: number;
19
+ prefix?: string;
20
+ suffix?: string;
21
+ decimals?: number;
22
+ duration?: number;
23
+ }
24
+
25
+ export interface ResolvedCounterStat extends CounterStat {
26
+ isAnimated: boolean;
27
+ startValue: string;
28
+ finalFormatted: string;
29
+ }
30
+
31
+ export function formatNumber(value: number, locale: string, decimals = 0): string {
32
+ return new Intl.NumberFormat(locale, {
33
+ minimumFractionDigits: decimals,
34
+ maximumFractionDigits: decimals,
35
+ }).format(value);
36
+ }
37
+
38
+ export function parseNumeric(val: string | undefined): number | undefined {
39
+ if (!val) return undefined;
40
+ const cleaned = val.replace(/[^\d.,]/g, "").replace(/,/g, "");
41
+ const num = parseFloat(cleaned);
42
+ return isNaN(num) ? undefined : num;
43
+ }
44
+
45
+ // Create start value with same width as final, starting from "1" in highest digit
46
+ export function getStartValue(formattedFinal: string): string {
47
+ // Replace digits: first digit becomes "1", rest become "0"
48
+ // "1,250" → "1,000", "14" → "10"
49
+ let firstDigit = true;
50
+ return formattedFinal.replace(/\d/g, () => {
51
+ if (firstDigit) {
52
+ firstDigit = false;
53
+ return "1";
54
+ }
55
+ return "0";
56
+ });
57
+ }
58
+
59
+ export function resolveCounterStats(
60
+ stats: CounterStat[],
61
+ animated: boolean | undefined,
62
+ locale: string,
63
+ ): ResolvedCounterStat[] {
64
+ return stats.map((stat) => {
65
+ const numericValue = stat.numericValue ?? parseNumeric(stat.value);
66
+ const isAnimated: boolean = (animated ?? false) && numericValue !== undefined;
67
+ const decimals = stat.decimals ?? 0;
68
+ const finalFormatted =
69
+ numericValue !== undefined
70
+ ? formatNumber(numericValue, locale, decimals)
71
+ : (stat.value ?? "");
72
+ const startValue = isAnimated ? getStartValue(finalFormatted) : finalFormatted;
73
+ return { ...stat, numericValue, isAnimated, startValue, finalFormatted };
74
+ });
75
+ }
@@ -0,0 +1,48 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ Canonical CSS value normalizer shared by biome.css.generate (codegen) and
5
+ biome.tokens.validate (drift detection). Whitespace collapsing, quote
6
+ normalisation, hex lowercasing, and trailing-zero stripping are applied in a
7
+ single deterministic order so that generated CSS and drift comparisons never
8
+ disagree on equality.
9
+ </purpose>
10
+ <non-goals>
11
+ <item>Do not format CSS for emission — line-breaking is the caller's job.</item>
12
+ <item>Do not parse CSS structure — this is a string-level transform only.</item>
13
+ </non-goals>
14
+ </MODULE_CONTRACT>
15
+ <CHANGE_SUMMARY>
16
+ <item>Initial: unified normalizeCssValue from site-kernel-codegen and site-kernel-checks.</item>
17
+ </CHANGE_SUMMARY>
18
+ */
19
+
20
+ /**
21
+ * Normalize a CSS value string for stable comparison and emission.
22
+ *
23
+ * Applies (in order):
24
+ * 1. Whitespace collapse + trim (multi-line gradients become single-line)
25
+ * 2. Parenthesis spacing removal
26
+ * 3. Single → double quote conversion
27
+ * 4. Hex color lowercasing
28
+ * 5. Trailing decimal zero stripping
29
+ */
30
+ export function normalizeCssValue(value: string): string {
31
+ return (
32
+ value
33
+ // Collapse all whitespace runs (handles multi-line gradient formatting)
34
+ .replace(/\s+/g, " ")
35
+ .trim()
36
+ // Remove spaces immediately after ( and before ) so gradient args align
37
+ .replace(/\(\s+/g, "(")
38
+ .replace(/\s+\)/g, ")")
39
+ // CSS strings: double quotes
40
+ .replace(/'([^']*)'/g, '"$1"')
41
+ // Hex colors: lowercase
42
+ .replace(/#([0-9A-Fa-f]+)/g, (_m, hex) => `#${hex.toLowerCase()}`)
43
+ // Strip trailing decimal zeros (e.g. 20.40px → 20.4px, 1.10 → 1.1)
44
+ .replace(/(\d+\.\d*[1-9])0+/g, "$1")
45
+ // Strip redundant .0 suffix (e.g. 1.0 → 1, 0.0 → 0)
46
+ .replace(/(\d+)\.0+(?!\d)/g, "$1")
47
+ );
48
+ }
@@ -0,0 +1,131 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ RFC-0262: the dev-only fail-fast prop validator wired into buildPage's
5
+ `validateProps` hook. Resolves each block's manifest propsSchema at runtime
6
+ (packages/ui/src, workspace-relative, resolved by walking up from cwd to
7
+ pnpm-workspace.yaml — this package never ships a hand-rolled path guess) and
8
+ throws PAGE-PROPS-01 on a genuine shape violation. Infrastructure failures
9
+ (workspace root not found, manifest unreadable, schema unresolved) are
10
+ swallowed with a console.warn — this is a dev convenience feature and must
11
+ never crash rendering for a reason unrelated to the author's actual props.
12
+ </purpose>
13
+ <non-goals>
14
+ <item>Do not replace page.block.validate — that remains the authoritative build.check gate.</item>
15
+ <item>Do not run in production — callers must gate construction behind a dev check.</item>
16
+ </non-goals>
17
+ </MODULE_CONTRACT>
18
+ <CHANGE_SUMMARY>
19
+ <item>RFC-0262: initial implementation.</item>
20
+ <item>Replaced hand-rolled validateShape with ajv standard validator.</item>
21
+ </CHANGE_SUMMARY>
22
+ */
23
+
24
+ import { stat } from "node:fs/promises";
25
+ import { dirname, join } from "node:path";
26
+ import { getSectionPropsSchema } from "@warpgogol/werkstatt-shared/ontology/schemas/manifest-resolver";
27
+ import Ajv, { type ValidateFunction } from "ajv";
28
+
29
+ const ajv = new Ajv({ allErrors: true, strict: false });
30
+
31
+ /**
32
+ * Block-level props consumed by blocks-renderer.astro (NOT by section components).
33
+ * These are valid on every block regardless of section archetype, so the
34
+ * validator strips them before per-section propsSchema validation.
35
+ *
36
+ * Must stay in sync with packages/os/site-kernel-checks/src/page-block.ts
37
+ * UNIVERSAL_BLOCK_PROPS and packages/werkstatt-site/src/domain/ui/blocks-renderer.astro.
38
+ */
39
+ const UNIVERSAL_BLOCK_PROPS: ReadonlySet<string> = new Set(["hideSectionNumber", "anchorId"]);
40
+
41
+ let packagesUiRootPromise: Promise<string | null> | undefined;
42
+
43
+ async function resolvePackagesUiRoot(): Promise<string | null> {
44
+ if (!packagesUiRootPromise) {
45
+ packagesUiRootPromise = (async () => {
46
+ let dir = process.cwd();
47
+ for (let depth = 0; depth < 12; depth += 1) {
48
+ try {
49
+ await stat(join(dir, "pnpm-workspace.yaml"));
50
+ return join(dir, "packages", "werkstatt-site", "src", "domain", "ui");
51
+ } catch {
52
+ const parent = dirname(dir);
53
+ if (parent === dir) return null;
54
+ dir = parent;
55
+ }
56
+ }
57
+ return null;
58
+ })();
59
+ }
60
+ return packagesUiRootPromise;
61
+ }
62
+
63
+ const schemaCache = new Map<string, Record<string, unknown> | null>();
64
+ const validateCache = new Map<string, ValidateFunction | null>();
65
+
66
+ async function resolveSchema(planetName: string): Promise<Record<string, unknown> | null> {
67
+ if (schemaCache.has(planetName)) return schemaCache.get(planetName)!;
68
+ try {
69
+ const packagesUiRoot = await resolvePackagesUiRoot();
70
+ if (!packagesUiRoot) {
71
+ schemaCache.set(planetName, null);
72
+ return null;
73
+ }
74
+ const resolved = await getSectionPropsSchema(planetName, packagesUiRoot);
75
+ const schema = resolved?.propsSchema ?? null;
76
+ schemaCache.set(planetName, schema);
77
+ return schema;
78
+ } catch (error) {
79
+ console.warn(
80
+ `[dev-props-validator] Could not resolve propsSchema for "${planetName}": ${String(error)}`,
81
+ );
82
+ schemaCache.set(planetName, null);
83
+ return null;
84
+ }
85
+ }
86
+
87
+ function resolveValidator(planetName: string, schema: Record<string, unknown>): ValidateFunction {
88
+ const cached = validateCache.get(planetName);
89
+ if (cached) return cached;
90
+ const validate = ajv.compile(schema);
91
+ validateCache.set(planetName, validate);
92
+ return validate;
93
+ }
94
+
95
+ /**
96
+ * RFC-0262: build the `validateProps` callback for `BuildPageOptions`. Callers
97
+ * MUST only pass this in dev contexts (`import.meta.env.DEV` /
98
+ * `process.env.NODE_ENV !== "production"`) — it is never active during
99
+ * `astro build`.
100
+ */
101
+ export function createDevPropsValidator(): (
102
+ planetName: string,
103
+ props: Record<string, unknown>,
104
+ blockId: string | null,
105
+ ) => Promise<void> {
106
+ return async (planetName, props, blockId) => {
107
+ const schema = await resolveSchema(planetName);
108
+ if (!schema) return; // No pinned schema (or resolution failed) — nothing to check.
109
+
110
+ // Strip UNIVERSAL_BLOCK_PROPS before validation — these are consumed by
111
+ // blocks-renderer.astro (not by the section component) and are valid on
112
+ // every block regardless of section archetype. Must stay in sync with
113
+ // packages/os/site-kernel-checks/src/page-block.ts.
114
+ const strippedProps: Record<string, unknown> = {};
115
+ for (const [k, v] of Object.entries(props)) {
116
+ if (!UNIVERSAL_BLOCK_PROPS.has(k)) strippedProps[k] = v;
117
+ }
118
+
119
+ const validate = resolveValidator(planetName, schema);
120
+ if (validate(strippedProps)) return;
121
+
122
+ const errors = (validate.errors ?? []).map(
123
+ (e) => `props${e.instancePath}: ${e.message ?? "validation failed"}`,
124
+ );
125
+ const blockLabel = blockId ? `block "${blockId}"` : "a shell block";
126
+ throw new Error(
127
+ `[PAGE-PROPS-01] ${blockLabel} (${planetName}) violates its pinned propsSchema:\n` +
128
+ errors.map((e) => ` - ${e}`).join("\n"),
129
+ );
130
+ };
131
+ }