@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,133 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ [RFC-0174] Binding-language policy resolver for legal documents.
5
+ Pure functions that turn a page's authored `translation` frontmatter block into
6
+ a render-time decision: is this the binding-language version, what is the locale's
7
+ translation status, must the mandatory language notice + "unofficial translation"
8
+ indicator be shown, and where does the binding document live.
9
+ </purpose>
10
+ <non-goals>
11
+ <item>Do not read content or the route registry — the binding URL is injected by the caller.</item>
12
+ <item>Do not render markup — that is @warpgogol/werkstatt-site/ui's translation-notice component.</item>
13
+ <item>Do not translate copy — the notice component owns localized microcopy.</item>
14
+ </non-goals>
15
+ </MODULE_CONTRACT>
16
+ <CHANGE_SUMMARY>
17
+ <item>RFC-0174: initial binding-language policy resolver.</item>
18
+ </CHANGE_SUMMARY>
19
+ */
20
+
21
+ /** Per-locale translation status for a legal document. */
22
+ export type TranslationStatus = "official" | "unofficial" | "disabled";
23
+
24
+ export const TRANSLATION_STATUSES: readonly TranslationStatus[] = [
25
+ "official",
26
+ "unofficial",
27
+ "disabled",
28
+ ] as const;
29
+
30
+ /**
31
+ * Authored `translation` block, declared once in the binding-language page file
32
+ * (e.g. `pages/de/datenschutz.md`). Lives in the client-editable `pages` surface
33
+ * (DNA-22) so the client toggles it without engineering.
34
+ */
35
+ export interface PageTranslationPolicy {
36
+ /** Legally binding language code for this page (e.g. "de"). */
37
+ binding: string;
38
+ /** pageId of the binding document; defaults to the declaring page's id. */
39
+ bindingPageId?: string;
40
+ /** Localized display name of the binding document, e.g. "Datenschutzerklärung". */
41
+ bindingDocLabel?: string;
42
+ /**
43
+ * Mandatory language notice. Default true. The validator forbids `false` while
44
+ * any non-binding locale is `unofficial`.
45
+ */
46
+ notice?: boolean;
47
+ /** Persistent "unofficial translation" indicator. Default true. */
48
+ indicator?: boolean;
49
+ /**
50
+ * Per-locale status. A locale absent here that has a translation file is treated
51
+ * as "unofficial". The binding language itself is always treated as official.
52
+ */
53
+ locales?: Record<string, TranslationStatus>;
54
+ }
55
+
56
+ /** Render-time decision for the current (lang, pageId). */
57
+ export interface ResolvedTranslationContext {
58
+ /** Is the current render the binding-language version? */
59
+ isBinding: boolean;
60
+ /** Effective status of the current locale. */
61
+ status: TranslationStatus;
62
+ /** Binding language code (for dual-language notice copy). */
63
+ bindingLang: string;
64
+ /** Localized name of the binding document for the notice link text. */
65
+ bindingDocLabel?: string;
66
+ /** Absolute or localized URL of the binding document (notice link + redirect target). */
67
+ bindingUrl: string;
68
+ /** Inject the mandatory language-notice banner. */
69
+ showNotice: boolean;
70
+ /** Render the persistent "unofficial translation" indicator. */
71
+ showIndicator: boolean;
72
+ /**
73
+ * When set, this locale is `disabled` and the route should redirect to the
74
+ * binding-language document instead of serving the untrusted translation.
75
+ */
76
+ redirectTo?: string;
77
+ }
78
+
79
+ function normalizeStatus(value: unknown): TranslationStatus | undefined {
80
+ return TRANSLATION_STATUSES.includes(value as TranslationStatus)
81
+ ? (value as TranslationStatus)
82
+ : undefined;
83
+ }
84
+
85
+ /**
86
+ * Resolve the binding-language decision for one render.
87
+ *
88
+ * Returns `null` when the page declares no `translation.binding` (i.e. it is not a
89
+ * legal page under policy) — callers then render normally with no notice.
90
+ *
91
+ * @param resolveBindingUrl Callback that maps (bindingPageId, bindingLang) to the
92
+ * binding document's URL. Injected so this module stays free of the route
93
+ * registry / content dependencies.
94
+ */
95
+ export function resolveTranslationContext(args: {
96
+ lang: string;
97
+ pageId: string;
98
+ policy: PageTranslationPolicy | undefined | null;
99
+ resolveBindingUrl: (bindingPageId: string, bindingLang: string) => string | null;
100
+ }): ResolvedTranslationContext | null {
101
+ const { lang, pageId, policy, resolveBindingUrl } = args;
102
+ if (!policy || typeof policy.binding !== "string" || policy.binding.length === 0) {
103
+ return null;
104
+ }
105
+
106
+ const bindingLang = policy.binding;
107
+ const bindingPageId = policy.bindingPageId ?? pageId;
108
+ const isBinding = lang === bindingLang;
109
+
110
+ // The binding-language render is the authoritative text — never gets a notice.
111
+ const status: TranslationStatus = isBinding
112
+ ? "official"
113
+ : (normalizeStatus(policy.locales?.[lang]) ?? "unofficial");
114
+
115
+ const bindingUrl = resolveBindingUrl(bindingPageId, bindingLang) ?? "";
116
+ const noticeEnabled = policy.notice !== false;
117
+ const indicatorEnabled = policy.indicator !== false;
118
+
119
+ const showNotice = !isBinding && status === "unofficial" && noticeEnabled;
120
+ const showIndicator = !isBinding && status === "unofficial" && indicatorEnabled;
121
+ const redirectTo = !isBinding && status === "disabled" ? bindingUrl : undefined;
122
+
123
+ return {
124
+ isBinding,
125
+ status,
126
+ bindingLang,
127
+ bindingDocLabel: policy.bindingDocLabel,
128
+ bindingUrl,
129
+ showNotice,
130
+ showIndicator,
131
+ ...(redirectTo ? { redirectTo } : {}),
132
+ };
133
+ }
@@ -0,0 +1,181 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ RFC-0220 Material Credits helpers shared by UI and Site OS commands. Parses sidecar
5
+ YAML records, resolves localized/default-language fallback, and builds simple
6
+ schema.org CreativeWork-style payloads from the same structured source.
7
+ </purpose>
8
+ <non-goals>
9
+ <item>Do not scan the filesystem; callers supply raw sidecar maps.</item>
10
+ <item>Do not render UI; @warpgogol/werkstatt-site/ui owns presentation.</item>
11
+ </non-goals>
12
+ </MODULE_CONTRACT>
13
+ <CHANGE_SUMMARY>
14
+ <item>RFC-0220: initial helper surface for material credit sidecars.</item>
15
+ <item>RFC-0227: buildMaterialCreditJsonLd emits @id for page-graph linking.</item>
16
+ </CHANGE_SUMMARY>
17
+ */
18
+
19
+ import { parse as parseYaml } from "yaml";
20
+ import {
21
+ type MaterialCredit,
22
+ type MaterialCreditLabels,
23
+ type MaterialKind,
24
+ type MaterialTarget,
25
+ materialCreditSchema,
26
+ materialTargetKey,
27
+ formatMaterialCreditLine,
28
+ } from "./schemas/material-credit.ts";
29
+
30
+ // RFC-0488: re-export label mapping helpers for @warpgogol/werkstatt-shared/share/material-credits consumers.
31
+ export {
32
+ labelForSourceType,
33
+ labelForStatus,
34
+ labelForUsageBasis,
35
+ } from "./schemas/material-credit.ts";
36
+ export type {
37
+ MaterialSourceType,
38
+ MaterialCreditStatus,
39
+ UsageBasisType,
40
+ } from "./schemas/material-credit.ts";
41
+
42
+ export interface MaterialCreditRecord {
43
+ file: string;
44
+ lang?: string;
45
+ credit: MaterialCredit;
46
+ }
47
+
48
+ export type RawMaterialCreditMap = Record<string, string | { default?: string }>;
49
+
50
+ function rawToString(raw: string | { default?: string }): string {
51
+ return typeof raw === "string" ? raw : (raw.default ?? "");
52
+ }
53
+
54
+ export function langFromCreditPath(path: string): string | undefined {
55
+ const match = path.match(/\/src\/content\/[^/]+\/([^/]+)\//);
56
+ if (!match) return undefined;
57
+ const segment = match[1];
58
+ // RFC-0220: language-agnostic asset folders (surface/assets/, shared/assets/)
59
+ // carry no lang segment; the sidecar itself is the canonical record.
60
+ return segment === "assets" || segment === "media" ? undefined : segment;
61
+ }
62
+
63
+ export function parseMaterialCreditMap(rawMap: RawMaterialCreditMap): MaterialCreditRecord[] {
64
+ const records: MaterialCreditRecord[] = [];
65
+ for (const [file, raw] of Object.entries(rawMap)) {
66
+ const parsed = parseYaml(rawToString(raw));
67
+ const credit = materialCreditSchema.parse(parsed);
68
+ records.push({ file, lang: langFromCreditPath(file), credit });
69
+ }
70
+ return records;
71
+ }
72
+
73
+ export function findMaterialCredit(
74
+ records: MaterialCreditRecord[],
75
+ target: MaterialTarget,
76
+ lang: string,
77
+ defaultLang: string,
78
+ ): MaterialCredit | null {
79
+ const candidates = lang === defaultLang ? [lang, undefined] : [lang, defaultLang, undefined];
80
+ for (const candidateLang of candidates) {
81
+ const key = materialTargetKey(target, candidateLang);
82
+ const record = records.find((item) => {
83
+ const recordLang = item.credit.target.lang ?? item.lang;
84
+ return materialTargetKey(item.credit.target, recordLang) === key;
85
+ });
86
+ if (record) return record.credit;
87
+ }
88
+ return null;
89
+ }
90
+
91
+ export function creditByTarget(
92
+ rawMap: RawMaterialCreditMap,
93
+ target: MaterialTarget,
94
+ lang: string,
95
+ defaultLang: string,
96
+ ): MaterialCredit | null {
97
+ return findMaterialCredit(parseMaterialCreditMap(rawMap), target, lang, defaultLang);
98
+ }
99
+
100
+ export function schemaTypeForMaterial(
101
+ kind: MaterialKind,
102
+ ): "ImageObject" | "VideoObject" | "CreativeWork" {
103
+ if (kind === "image") return "ImageObject";
104
+ if (kind === "video") return "VideoObject";
105
+ return "CreativeWork";
106
+ }
107
+
108
+ /**
109
+ * RFC-0227: stable @id for a material credit node, derived from the content URL.
110
+ * Used by both the disclosure <script> and any page-graph associatedMedia reference.
111
+ */
112
+ export function materialCreditAtId(contentUrl?: string, targetId?: string): string | undefined {
113
+ const base = contentUrl ?? (targetId ? `urn:material:${targetId}` : undefined);
114
+ return base ? `${base}#material` : undefined;
115
+ }
116
+
117
+ export function buildMaterialCreditJsonLd(
118
+ credit: MaterialCredit,
119
+ contentUrl?: string,
120
+ labels?: MaterialCreditLabels,
121
+ ): Record<string, unknown> {
122
+ const creators = credit.parties.filter(
123
+ (party) => party.role === "creator" || party.role === "coCreator",
124
+ );
125
+ const creator =
126
+ creators.length === 1
127
+ ? {
128
+ "@type": creators[0].kind === "Organization" ? "Organization" : "Person",
129
+ name: creators[0].name,
130
+ url: creators[0].url,
131
+ }
132
+ : creators.map((party) => ({
133
+ "@type": party.kind === "Organization" ? "Organization" : "Person",
134
+ name: party.name,
135
+ ...(party.url ? { url: party.url } : {}),
136
+ }));
137
+
138
+ const copyrightHolder = resolveCopyrightHolder(credit);
139
+ const copyrightYear =
140
+ credit.license.copyrightYear ?? copyrightYearFromNotice(credit.license.copyrightNotice);
141
+ // RFC-0227: stable @id for page-graph linking.
142
+ const atId = materialCreditAtId(contentUrl, credit.target.id);
143
+
144
+ return {
145
+ "@type": schemaTypeForMaterial(credit.target.kind),
146
+ ...(atId ? { "@id": atId } : {}),
147
+ ...(contentUrl ? { contentUrl } : {}),
148
+ name: credit.title ?? credit.target.id,
149
+ ...(labels ? { creditText: formatMaterialCreditLine(credit, labels) } : {}),
150
+ creator,
151
+ ...(copyrightHolder ? { copyrightHolder } : {}),
152
+ ...(copyrightYear ? { copyrightYear } : {}),
153
+ ...(credit.license.copyrightNotice ? { copyrightNotice: credit.license.copyrightNotice } : {}),
154
+ // RFC-0223: only emit a URL as schema.org license; the human label stays in creditText.
155
+ ...(credit.license.url ? { license: credit.license.url } : {}),
156
+ ...(credit.license.acquireLicensePage
157
+ ? { acquireLicensePage: credit.license.acquireLicensePage }
158
+ : {}),
159
+ ...(credit.createdAt ? { dateCreated: credit.createdAt } : {}),
160
+ };
161
+ }
162
+
163
+ function copyrightYearFromNotice(notice?: string): number | undefined {
164
+ const match = notice?.match(/\b(\d{4})\b/);
165
+ return match ? Number(match[1]) : undefined;
166
+ }
167
+
168
+ function resolveCopyrightHolder(credit: MaterialCredit): Record<string, unknown> | undefined {
169
+ const holder = credit.parties.find((party) => party.role === "rightsHolder");
170
+ if (holder) {
171
+ return {
172
+ "@type": holder.kind === "Person" ? "Person" : "Organization",
173
+ name: holder.name,
174
+ ...(holder.url ? { url: holder.url } : {}),
175
+ };
176
+ }
177
+ const fromNotice = credit.license.copyrightNotice
178
+ ?.match(/©\s*\d{0,4}\s*([^.]+?)\.?\s*(?:All rights|$)/i)?.[1]
179
+ ?.trim();
180
+ return fromNotice ? { "@type": "Organization", name: fromNotice } : undefined;
181
+ }
@@ -0,0 +1,49 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Maintains packages/share/src/middleware/language-redirect.ts as an authored share middleware module so agents can evolve it without rediscovering local boundaries.</purpose>
4
+ <non-goals>
5
+ <item>Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file.</item>
6
+ </non-goals>
7
+ <!-- @ai-invariant Generated factory output (RFC-0055) flows through system.md i18n config; do not hand-edit app copies. -->
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0133: backfilled MODULE_MAP and CHANGE_SUMMARY markers for compass.validate compliance.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import { defineMiddleware } from "astro:middleware";
15
+
16
+ /**
17
+ * Factory for language routing middleware.
18
+ *
19
+ * [RFC-0160] The default language is served WITHOUT a URL prefix (`/`,
20
+ * `/<slug>`), and only non-default languages are prefixed (`/<lang>/…`).
21
+ * Therefore unprefixed paths ARE default-language content and MUST NOT be
22
+ * rewritten to `/<defaultLang>/…`. Client-side language detection for the
23
+ * default audience happens exclusively at `/` (see the root entry page's
24
+ * soft redirect, RFC-0159).
25
+ *
26
+ * This middleware also issues a 308 redirect when the default language is
27
+ * accessed with its old prefixed URL (`/<defaultLang>/…`), so bookmarks and
28
+ * search results do not 404. In static-output production this is handled by
29
+ * the `_redirects` file; the middleware covers dev mode and SSR routes.
30
+ */
31
+ export function createLanguageRedirectMiddleware(options: {
32
+ supportedLangs: readonly string[];
33
+ defaultLang: string;
34
+ }) {
35
+ return defineMiddleware((ctx: any, next: any) => {
36
+ const url = new URL(ctx.request.url);
37
+ const segments = url.pathname.split("/").filter(Boolean);
38
+ const firstSegment = segments[0];
39
+
40
+ // Redirect /<defaultLang>/<rest> → /<rest> with a 308 Permanent Redirect.
41
+ if (firstSegment === options.defaultLang) {
42
+ const rest = segments.slice(1).join("/");
43
+ const target = "/" + (rest ? rest + url.search : url.search);
44
+ return ctx.redirect(target, 308);
45
+ }
46
+
47
+ return next();
48
+ });
49
+ }
@@ -0,0 +1,149 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Pure Offer Capacity Wave calculations for structured offer availability policy.</purpose>
4
+ <non-goals>
5
+ <item>Do not fetch CRM, browser, or private operational data.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0322: add structured offer capacity wave helper.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ export interface OfferCapacityPolicy {
14
+ timezone: string;
15
+ startsAt: string;
16
+ cadence: "monthly" | "fixed-days";
17
+ cadenceDays?: number;
18
+ slotRange: { min: number; max: number };
19
+ maxSlotsPerWave: number;
20
+ }
21
+
22
+ export interface OfferCapacityReservation {
23
+ waveId: string;
24
+ slots: number;
25
+ source: "bordbuch" | "manual";
26
+ asOf: string;
27
+ }
28
+
29
+ export interface OfferCapacityState {
30
+ waveId: string;
31
+ waveIndex: number;
32
+ startsAt: string;
33
+ endsAt: string;
34
+ daysRemaining: number;
35
+ progress: number;
36
+ slotRange: { min: number; max: number };
37
+ maxSlots: number;
38
+ reservedSlots?: number;
39
+ openSlots?: number;
40
+ availabilityStatus: "known" | "unknown" | "full";
41
+ }
42
+
43
+ const DAY_MS = 24 * 60 * 60 * 1000;
44
+
45
+ function parseDateOnly(value: string): Date {
46
+ const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
47
+ if (!match) throw new Error(`Invalid date-only value: ${value}`);
48
+ const [, year, month, day] = match;
49
+ return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
50
+ }
51
+
52
+ function dateOnly(date: Date, timezone: string): string {
53
+ const parts = new Intl.DateTimeFormat("en-CA", {
54
+ timeZone: timezone,
55
+ year: "numeric",
56
+ month: "2-digit",
57
+ day: "2-digit",
58
+ }).formatToParts(date);
59
+ const get = (type: string) => parts.find((part) => part.type === type)?.value ?? "";
60
+ return `${get("year")}-${get("month")}-${get("day")}`;
61
+ }
62
+
63
+ function daysInMonth(year: number, monthIndex: number): number {
64
+ return new Date(Date.UTC(year, monthIndex + 1, 0)).getUTCDate();
65
+ }
66
+
67
+ function addMonthsClamped(start: Date, months: number): Date {
68
+ const year = start.getUTCFullYear();
69
+ const month = start.getUTCMonth() + months;
70
+ const targetYear = year + Math.floor(month / 12);
71
+ const targetMonth = ((month % 12) + 12) % 12;
72
+ const day = Math.min(start.getUTCDate(), daysInMonth(targetYear, targetMonth));
73
+ return new Date(Date.UTC(targetYear, targetMonth, day));
74
+ }
75
+
76
+ function addDays(date: Date, days: number): Date {
77
+ return new Date(date.getTime() + days * DAY_MS);
78
+ }
79
+
80
+ function clamp(value: number, min: number, max: number): number {
81
+ return Math.min(max, Math.max(min, value));
82
+ }
83
+
84
+ function monthDistance(from: Date, to: Date): number {
85
+ return (
86
+ (to.getUTCFullYear() - from.getUTCFullYear()) * 12 + (to.getUTCMonth() - from.getUTCMonth())
87
+ );
88
+ }
89
+
90
+ export function calculateOfferCapacityState(
91
+ policy: OfferCapacityPolicy,
92
+ reservations: readonly OfferCapacityReservation[] = [],
93
+ now: Date = new Date(),
94
+ ): OfferCapacityState {
95
+ const start = parseDateOnly(policy.startsAt);
96
+ const today = parseDateOnly(dateOnly(now, policy.timezone));
97
+ let waveIndex = 0;
98
+ let waveStart = start;
99
+ let waveEnd: Date;
100
+
101
+ if (policy.cadence === "monthly") {
102
+ waveIndex = Math.max(0, monthDistance(start, today));
103
+ waveStart = addMonthsClamped(start, waveIndex);
104
+ if (waveStart > today && waveIndex > 0) {
105
+ waveIndex -= 1;
106
+ waveStart = addMonthsClamped(start, waveIndex);
107
+ }
108
+ waveEnd = addMonthsClamped(start, waveIndex + 1);
109
+ } else {
110
+ const cadenceDays = policy.cadenceDays ?? 0;
111
+ if (cadenceDays <= 0) throw new Error("fixed-days cadence requires cadenceDays > 0");
112
+ waveIndex = Math.max(
113
+ 0,
114
+ Math.floor((today.getTime() - start.getTime()) / (cadenceDays * DAY_MS)),
115
+ );
116
+ waveStart = addDays(start, waveIndex * cadenceDays);
117
+ waveEnd = addDays(waveStart, cadenceDays);
118
+ }
119
+
120
+ const waveId =
121
+ policy.cadence === "monthly"
122
+ ? `${waveStart.getUTCFullYear()}-${String(waveStart.getUTCMonth() + 1).padStart(2, "0")}`
123
+ : `${policy.startsAt}+${waveIndex}`;
124
+ const duration = Math.max(1, waveEnd.getTime() - waveStart.getTime());
125
+ const elapsed = clamp(today.getTime() - waveStart.getTime(), 0, duration);
126
+ const activeReservations = reservations.filter((reservation) => reservation.waveId === waveId);
127
+ const reservedSlots =
128
+ activeReservations.length > 0
129
+ ? activeReservations.reduce((sum, reservation) => sum + reservation.slots, 0)
130
+ : undefined;
131
+ const openSlots =
132
+ reservedSlots === undefined
133
+ ? undefined
134
+ : clamp(policy.maxSlotsPerWave - reservedSlots, 0, policy.maxSlotsPerWave);
135
+
136
+ return {
137
+ waveId,
138
+ waveIndex,
139
+ startsAt: dateOnly(waveStart, "UTC"),
140
+ endsAt: dateOnly(waveEnd, "UTC"),
141
+ daysRemaining: Math.max(0, Math.ceil((waveEnd.getTime() - today.getTime()) / DAY_MS)),
142
+ progress: Math.round((elapsed / duration) * 100),
143
+ slotRange: policy.slotRange,
144
+ maxSlots: policy.maxSlotsPerWave,
145
+ ...(reservedSlots === undefined ? {} : { reservedSlots }),
146
+ ...(openSlots === undefined ? {} : { openSlots }),
147
+ availabilityStatus: openSlots === undefined ? "unknown" : openSlots <= 0 ? "full" : "known",
148
+ };
149
+ }
@@ -0,0 +1,162 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ RFC-0082 shared parser for RFC-0076-headed onboarding artifacts.
5
+
6
+ Owns the rule "first YAML document is the RFC-0076 metadata header; last YAML
7
+ document is the artifact payload" so phase validation, audit validators, and
8
+ content-discipline parsers all see the same answer.
9
+ </purpose>
10
+ <non-goals>
11
+ <item>Do not introduce a generic YAML schema framework — scoped to onboarding RFC-0076 metadata only.</item>
12
+ <item>Do not read files from disk.</item>
13
+ <item>Do not own the metadata key set beyond the four declared by RFC-0076.</item>
14
+ </non-goals>
15
+ </MODULE_CONTRACT>
16
+ <CHANGE_SUMMARY>
17
+ <item>RFC-0082: Introduce shared multi-document YAML helper for onboarding artifacts.</item>
18
+ </CHANGE_SUMMARY>
19
+ */
20
+
21
+ import YAML from "yaml";
22
+ import type { ZodType } from "zod";
23
+
24
+ /**
25
+ * The four top-level keys that belong to the RFC-0076 metadata header.
26
+ * Anything else at the top level is part of the artifact payload.
27
+ */
28
+ export const RFC_METADATA_KEYS = [
29
+ "phase",
30
+ "derivedFromInputHash",
31
+ "generatedAt",
32
+ "generator",
33
+ ] as const;
34
+
35
+ export type RfcMetadataKey = (typeof RFC_METADATA_KEYS)[number];
36
+
37
+ /**
38
+ * The RFC-0076 metadata header that every machine-readable file under
39
+ * onboarding/.output/<phase>/ MUST carry.
40
+ *
41
+ * - `phase` — the onboarding phase that produced the artifact (e.g. "04-author").
42
+ * - `derivedFromInputHash` — sha256 of the input manifest the artifact was derived from.
43
+ * - `generatedAt` — ISO 8601 timestamp.
44
+ * - `generator` — author name, typically "agent" or a command name.
45
+ */
46
+ export interface RfcMetadataHeader {
47
+ phase: string;
48
+ derivedFromInputHash: string;
49
+ generatedAt: string;
50
+ generator: string;
51
+ }
52
+
53
+ function isRecord(value: unknown): value is Record<string, unknown> {
54
+ return typeof value === "object" && value !== null && !Array.isArray(value);
55
+ }
56
+
57
+ function asString(value: unknown): string | null {
58
+ return typeof value === "string" ? value : null;
59
+ }
60
+
61
+ function buildHeader(raw: Record<string, unknown>): RfcMetadataHeader | null {
62
+ const phase = asString(raw["phase"]);
63
+ const hash = asString(raw["derivedFromInputHash"]);
64
+ if (phase === null || hash === null) {
65
+ return null;
66
+ }
67
+ return {
68
+ phase,
69
+ derivedFromInputHash: hash,
70
+ generatedAt: asString(raw["generatedAt"]) ?? "",
71
+ generator: asString(raw["generator"]) ?? "",
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Read the RFC-0076 metadata header from an onboarding YAML artifact.
77
+ *
78
+ * Accepts both file shapes:
79
+ * - **Two-doc file**: first doc is the header, second doc is the payload.
80
+ * - **Single-doc file**: header keys and payload keys are merged at top level.
81
+ *
82
+ * Returns `null` when the source does not contain at minimum `phase` and
83
+ * `derivedFromInputHash` — callers can treat that as "no RFC-0076 header
84
+ * present" without inspecting the YAML themselves.
85
+ */
86
+ export function parseOnboardingArtifactHeader(source: string): RfcMetadataHeader | null {
87
+ const docs = YAML.parseAllDocuments(source);
88
+ if (docs.length === 0) return null;
89
+ const first = docs[0].toJSON();
90
+ if (!isRecord(first)) return null;
91
+ return buildHeader(first);
92
+ }
93
+
94
+ /**
95
+ * Read the artifact payload from an onboarding YAML artifact and validate
96
+ * it against the caller-supplied Zod schema.
97
+ *
98
+ * Selection rule:
99
+ * - **Two-doc file** → the LAST document is the payload, returned unchanged
100
+ * (schemas can keep `.strict()` because the header lives in a separate doc).
101
+ * - **Single-doc file** → the document is the payload merged with the header;
102
+ * the four RFC_METADATA_KEYS are stripped before validation so `.strict()`
103
+ * schemas can still accept the file.
104
+ *
105
+ * Throws the Zod validation error on schema mismatch and a plain Error when
106
+ * the source contains zero documents or more than two documents (the second
107
+ * case enforces the "exactly header + payload" contract).
108
+ */
109
+ export function parseOnboardingArtifactPayload<T>(source: string, schema: ZodType<T>): T {
110
+ const docs = YAML.parseAllDocuments(source);
111
+ if (docs.length === 0) {
112
+ throw new Error("parseOnboardingArtifactPayload: source contains no YAML documents.");
113
+ }
114
+ if (docs.length > 2) {
115
+ throw new Error(
116
+ `parseOnboardingArtifactPayload: expected exactly 1 or 2 YAML documents (RFC-0076 header + payload), found ${docs.length}.`,
117
+ );
118
+ }
119
+
120
+ let payload: unknown;
121
+ if (docs.length === 2) {
122
+ payload = docs[1].toJSON();
123
+ } else {
124
+ const merged = docs[0].toJSON();
125
+ if (isRecord(merged)) {
126
+ // Strip RFC-0076 metadata keys from the merged single-doc payload, BUT
127
+ // preserve any key the schema itself declares as a payload field. This
128
+ // matters most for `generatedAt`, which RFC-0076 carries in the header
129
+ // and several content-discipline schemas (e.g. contentAtomsFileSchema)
130
+ // also declare as a strict payload field. Without this carve-out the
131
+ // strip would erase a legitimate payload value and schema parse would
132
+ // fail with a confusing "expected string, received undefined" error.
133
+ const schemaKeys = getSchemaTopLevelKeys(schema);
134
+ const stripped: Record<string, unknown> = { ...merged };
135
+ for (const key of RFC_METADATA_KEYS) {
136
+ if (!schemaKeys.has(key)) {
137
+ delete stripped[key];
138
+ }
139
+ }
140
+ payload = stripped;
141
+ } else {
142
+ payload = merged;
143
+ }
144
+ }
145
+
146
+ return schema.parse(payload);
147
+ }
148
+
149
+ /**
150
+ * Best-effort top-level-key extraction from a Zod schema. Used to decide which
151
+ * RFC-0076 metadata keys to preserve when stripping a merged single-doc
152
+ * payload. Returns an empty set when the schema does not expose a `.shape` —
153
+ * permissive schemas like z.unknown() / z.any() then accept the stripped
154
+ * payload as-is.
155
+ */
156
+ function getSchemaTopLevelKeys(schema: ZodType<unknown>): Set<string> {
157
+ const candidate = schema as unknown as { shape?: Record<string, unknown> };
158
+ if (candidate.shape && typeof candidate.shape === "object") {
159
+ return new Set(Object.keys(candidate.shape));
160
+ }
161
+ return new Set();
162
+ }