@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,127 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ RFC-0213: the authored-content freshness model. Given a claim's temporal window
5
+ (asOf / validUntil / reviewEvery) and "today", derive a freshness state and the
6
+ ledger entry shape. Framework-free, deterministic, date-only — the surface
7
+ Freshness Ledger (RFC-0196) shares this state taxonomy + ledger shape so one
8
+ health view can read both, but computes state from page substance, not dates.
9
+ </purpose>
10
+ <non-goals>
11
+ <item>Do not read the filesystem, parse sidecars, or emit Diagnostics — the kernel command does that.</item>
12
+ <item>Do not compute page-substance decay (surface RFC-0196 owns that input).</item>
13
+ </non-goals>
14
+ </MODULE_CONTRACT>
15
+ <CHANGE_SUMMARY>
16
+ <item>RFC-0213: initial freshness state model + ledger shapes.</item>
17
+ </CHANGE_SUMMARY>
18
+ */
19
+
20
+ export type FreshnessState = "fresh" | "review-due" | "expiring-soon" | "expired" | "unsourced";
21
+
22
+ export type FreshnessCriticality = "advisory" | "important" | "blocking";
23
+
24
+ const MS_PER_DAY = 86_400_000;
25
+
26
+ /** Parse an ISO date (YYYY-MM-DD) to a UTC epoch-ms at midnight, or null. */
27
+ function parseIsoDate(date: string): number | null {
28
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return null;
29
+ const ms = Date.parse(`${date}T00:00:00Z`);
30
+ return Number.isNaN(ms) ? null : ms;
31
+ }
32
+
33
+ function toIso(ms: number): string {
34
+ return new Date(ms).toISOString().slice(0, 10);
35
+ }
36
+
37
+ const DURATION_RE =
38
+ /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
39
+
40
+ /**
41
+ * Add an ISO 8601 duration to an ISO date. Calendar-correct for Y/M; W/D add
42
+ * fixed days; the time component is truncated to whole days (cadences are coarse).
43
+ * Returns null on malformed input.
44
+ */
45
+ export function addDuration(dateIso: string, duration: string): string | null {
46
+ const base = parseIsoDate(dateIso);
47
+ if (base === null) return null;
48
+ const m = DURATION_RE.exec(duration);
49
+ if (!m || duration === "P" || duration === "PT") return null;
50
+ const [, y, mo, w, d] = m;
51
+ const dt = new Date(base);
52
+ if (y) dt.setUTCFullYear(dt.getUTCFullYear() + Number(y));
53
+ if (mo) dt.setUTCMonth(dt.getUTCMonth() + Number(mo));
54
+ const extraDays = (w ? Number(w) * 7 : 0) + (d ? Number(d) : 0);
55
+ if (extraDays) dt.setUTCDate(dt.getUTCDate() + extraDays);
56
+ return toIso(dt.getTime());
57
+ }
58
+
59
+ export interface FreshnessWindow {
60
+ asOf: string;
61
+ validUntil?: string;
62
+ reviewEvery?: string;
63
+ }
64
+
65
+ export interface FreshnessEvaluation {
66
+ state: FreshnessState;
67
+ reviewDueAt?: string;
68
+ /** Days until validUntil; negative if past. Computed, not stored. */
69
+ daysToExpiry?: number;
70
+ }
71
+
72
+ /**
73
+ * Derive freshness state from a window relative to `today` (ISO date).
74
+ * `unsourced` is decided by the caller (a NEED_THIS value) and short-circuits here.
75
+ * Precedence: expired > expiring-soon > review-due > fresh.
76
+ */
77
+ export function evaluateFreshness(
78
+ window: FreshnessWindow,
79
+ today: string,
80
+ soonWindowDays = 30,
81
+ ): FreshnessEvaluation {
82
+ const todayMs = parseIsoDate(today);
83
+ const reviewDueAt = window.reviewEvery
84
+ ? (addDuration(window.asOf, window.reviewEvery) ?? undefined)
85
+ : undefined;
86
+
87
+ let daysToExpiry: number | undefined;
88
+ if (window.validUntil) {
89
+ const vu = parseIsoDate(window.validUntil);
90
+ if (vu !== null && todayMs !== null) {
91
+ daysToExpiry = Math.round((vu - todayMs) / MS_PER_DAY);
92
+ }
93
+ }
94
+
95
+ let state: FreshnessState = "fresh";
96
+ if (daysToExpiry !== undefined && daysToExpiry < 0) {
97
+ state = "expired";
98
+ } else if (daysToExpiry !== undefined && daysToExpiry <= soonWindowDays) {
99
+ state = "expiring-soon";
100
+ } else if (reviewDueAt && todayMs !== null) {
101
+ const due = parseIsoDate(reviewDueAt);
102
+ if (due !== null && due <= todayMs) state = "review-due";
103
+ }
104
+
105
+ return { state, reviewDueAt, daysToExpiry };
106
+ }
107
+
108
+ /** One claim's freshness in the ledger. `subject` is the canonical address string. */
109
+ export interface FreshnessLedgerEntry {
110
+ subject: string;
111
+ state: FreshnessState;
112
+ asOf?: string;
113
+ validUntil?: string;
114
+ reviewDueAt?: string;
115
+ criticality: FreshnessCriticality;
116
+ }
117
+
118
+ /** The deterministic per-app authored Freshness Ledger (mirrors the surface ledger shape). */
119
+ export interface AuthoredFreshnessLedger {
120
+ site: string;
121
+ entries: FreshnessLedgerEntry[];
122
+ summary: Record<FreshnessState, number>;
123
+ }
124
+
125
+ export function emptyFreshnessSummary(): Record<FreshnessState, number> {
126
+ return { fresh: 0, "review-due": 0, "expiring-soon": 0, expired: 0, unsourced: 0 };
127
+ }
@@ -0,0 +1,23 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ RFC-0211: barrel for the Content Knowledge Lifecycle (CKL) shared model. Re-exports
5
+ the framework-free claim contracts consumed by the kernel CKL commands and validators
6
+ (RFC-0212..0218). Later CKL RFCs add sibling modules here (freshness, source,
7
+ derivation, plan, ledger) and re-export them from this barrel.
8
+ </purpose>
9
+ <non-goals>
10
+ <item>Do not add logic here — pure re-export barrel.</item>
11
+ </non-goals>
12
+ </MODULE_CONTRACT>
13
+ <CHANGE_SUMMARY>
14
+ <item>RFC-0211: initial barrel exporting the claim model.</item>
15
+ </CHANGE_SUMMARY>
16
+ */
17
+
18
+ export * from "./claim.ts";
19
+ export * from "./freshness.ts";
20
+ export * from "./derivation.ts";
21
+ export * from "./source.ts";
22
+ export * from "./plan.ts";
23
+ export * from "./ledger.ts";
@@ -0,0 +1,222 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ RFC-0217: The Claim Ledger — event schema, append helpers, as-of/lineage
5
+ query, and knowledge-graph/temporal-SEO projectors. The ledger is an
6
+ append-only NDJSON file (`src/content/ledger/claims.ndjson`); each line is
7
+ one immutable ClaimEvent. The projector produces two generated JSON artifacts
8
+ (knowledge.generated.yaml + seo/temporal.generated.yaml) on demand.
9
+ Framework-free; no filesystem access.
10
+ </purpose>
11
+ <keywords>RFC-0217, CKL, ledger, claim event, lineage, temporal SEO, knowledge graph</keywords>
12
+ <responsibilities>
13
+ <item>Define ClaimEvent, ClaimLineage, TemporalSeo, KnowledgeGraph contracts.</item>
14
+ <item>stableEventId — deterministic event identifier.</item>
15
+ <item>buildLineage — assemble lineage from flat event list for a subject.</item>
16
+ <item>queryAsOf — value of a subject as of a given date.</item>
17
+ <item>projectGraph — current claims × lineage as nodes+edges.</item>
18
+ <item>projectTemporalSeo — per-page datePublished/dateModified.</item>
19
+ </responsibilities>
20
+ <non-goals>
21
+ <item>Do not read/write the filesystem — the kernel command does that.</item>
22
+ <item>Do not parse sidecar files — consume pre-parsed events only.</item>
23
+ </non-goals>
24
+ </MODULE_CONTRACT>
25
+ <MODULE_MAP>
26
+ <entry key="ClaimEvent / ClaimEventKind">The immutable ledger record shape.</entry>
27
+ <entry key="stableEventId">sha256-based deterministic event id.</entry>
28
+ <entry key="buildLineage">Lineage for a subject: current + history (newest→oldest).</entry>
29
+ <entry key="queryAsOf">Value as of a given date.</entry>
30
+ <entry key="projectGraph / projectTemporalSeo">Deterministic projections.</entry>
31
+ </MODULE_MAP>
32
+ <CHANGE_SUMMARY>
33
+ <item>RFC-0217: initial ledger contract and projection helpers.</item>
34
+ </CHANGE_SUMMARY>
35
+ */
36
+
37
+ import { createHash } from "node:crypto";
38
+ import type { ClaimProvenanceKind } from "./claim.ts";
39
+
40
+ export type ClaimEventKind =
41
+ "genesis" | "verify-update" | "verify-noop" | "translate" | "supersede" | "retire";
42
+
43
+ export interface ClaimEvent {
44
+ /** Stable deterministic id for this event line. */
45
+ id: string;
46
+ /** ISO timestamp (event time, UTC). */
47
+ ts: string;
48
+ /** Canonical subject address (RFC-0211). */
49
+ subject: string;
50
+ /** String value (omitted for large prose; then valueHash set). */
51
+ value?: string;
52
+ /** sha256:<hex> of the value for large prose. */
53
+ valueHash?: string;
54
+ provenance: ClaimProvenanceKind;
55
+ sourceRef?: string;
56
+ /** ISO date of the fact (when the value was true). */
57
+ asOf: string;
58
+ /** Id of the event this supersedes (linking lineage). */
59
+ supersedes?: string;
60
+ /** Agent or human handle that produced this event. */
61
+ actor: string;
62
+ event: ClaimEventKind;
63
+ }
64
+
65
+ export interface ClaimLineage {
66
+ subject: string;
67
+ current: ClaimEvent;
68
+ /** Full history including current, newest → oldest. */
69
+ history: ClaimEvent[];
70
+ }
71
+
72
+ export interface TemporalSeo {
73
+ /** Route/page slug. */
74
+ page: string;
75
+ /** ISO date of earliest genesis event for claims on this page. */
76
+ datePublished: string;
77
+ /** ISO date of latest verify-update event for claims on this page. */
78
+ dateModified: string;
79
+ /** ISO interval "YYYY-MM-DD/YYYY-MM-DD" when earliest ≠ latest. */
80
+ temporalCoverage?: string;
81
+ }
82
+
83
+ export interface KnowledgeGraphNode {
84
+ subject: string;
85
+ currentValue?: string;
86
+ provenance: ClaimProvenanceKind;
87
+ asOf: string;
88
+ sourceRef?: string;
89
+ latestEventId: string;
90
+ historyCount: number;
91
+ }
92
+
93
+ export interface KnowledgeGraph {
94
+ generatedAt: string;
95
+ app: string;
96
+ nodes: KnowledgeGraphNode[];
97
+ }
98
+
99
+ export const LEDGER_GENERATED_MARKER =
100
+ "GENERATED. Do not change this line unless the file contains project specific changes.";
101
+
102
+ /**
103
+ * Deterministic event id: sha256 of (subject + asOf + event + actor + value).
104
+ * The value (or valueHash for large prose) is part of the identity so that two
105
+ * genuinely distinct corrections on the same day by the same actor produce
106
+ * distinct events, while re-appending an identical event stays idempotent.
107
+ */
108
+ export function stableEventId(
109
+ subject: string,
110
+ asOf: string,
111
+ event: ClaimEventKind,
112
+ actor: string,
113
+ value?: string,
114
+ ): string {
115
+ const key = `${subject}|${asOf}|${event}|${actor}|${value ?? ""}`;
116
+ return `evt_${createHash("sha256").update(key, "utf8").digest("hex").slice(0, 16)}`;
117
+ }
118
+
119
+ /** Parse an NDJSON string into a list of ClaimEvents (skips blank lines). */
120
+ export function parseNdjson(ndjson: string): ClaimEvent[] {
121
+ return ndjson
122
+ .split("\n")
123
+ .filter((l) => l.trim().length > 0)
124
+ .map((l) => JSON.parse(l) as ClaimEvent);
125
+ }
126
+
127
+ /** Serialize a ClaimEvent to a single NDJSON line (no trailing newline). */
128
+ export function serializeEvent(event: ClaimEvent): string {
129
+ return JSON.stringify(event);
130
+ }
131
+
132
+ /** Build a lineage chain for a subject from the flat event list. */
133
+ export function buildLineage(events: ClaimEvent[], subject: string): ClaimLineage | null {
134
+ const relevant = events.filter((e) => e.subject === subject);
135
+ if (relevant.length === 0) return null;
136
+ // Sort newest first by ts.
137
+ const sorted = [...relevant].sort((a, b) => b.ts.localeCompare(a.ts));
138
+ return {
139
+ subject,
140
+ current: sorted[0],
141
+ history: sorted,
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Query the value of a subject as of a given ISO date.
147
+ * Returns the most-recent event whose asOf ≤ queryDate, or null.
148
+ */
149
+ export function queryAsOf(
150
+ events: ClaimEvent[],
151
+ subject: string,
152
+ queryDate: string,
153
+ ): ClaimEvent | null {
154
+ const candidates = events
155
+ .filter((e) => e.subject === subject && e.asOf <= queryDate)
156
+ .sort((a, b) => b.asOf.localeCompare(a.asOf) || b.ts.localeCompare(a.ts));
157
+ return candidates[0] ?? null;
158
+ }
159
+
160
+ /** Project current-state knowledge graph from the ledger. */
161
+ export function projectGraph(events: ClaimEvent[], app: string): KnowledgeGraph {
162
+ const bySubject = new Map<string, ClaimEvent[]>();
163
+ for (const e of events) {
164
+ const list = bySubject.get(e.subject) ?? [];
165
+ list.push(e);
166
+ bySubject.set(e.subject, list);
167
+ }
168
+
169
+ const nodes: KnowledgeGraphNode[] = [];
170
+ for (const [subject, evts] of bySubject) {
171
+ const sorted = [...evts].sort((a, b) => b.ts.localeCompare(a.ts));
172
+ const current = sorted[0];
173
+ nodes.push({
174
+ subject,
175
+ currentValue: current.value,
176
+ provenance: current.provenance,
177
+ asOf: current.asOf,
178
+ sourceRef: current.sourceRef,
179
+ latestEventId: current.id,
180
+ historyCount: evts.length,
181
+ });
182
+ }
183
+
184
+ nodes.sort((a, b) => a.subject.localeCompare(b.subject));
185
+ return {
186
+ generatedAt: new Date().toISOString(),
187
+ app,
188
+ nodes,
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Project temporal SEO metadata per page from the ledger.
194
+ * `pageClaimsMap` maps a page route → array of claim subjects surfaced on that page.
195
+ */
196
+ export function projectTemporalSeo(
197
+ events: ClaimEvent[],
198
+ pageClaimsMap: Map<string, string[]>,
199
+ ): TemporalSeo[] {
200
+ const result: TemporalSeo[] = [];
201
+ for (const [page, subjects] of pageClaimsMap) {
202
+ const relevantEvents = events.filter((e) => subjects.includes(e.subject));
203
+ if (relevantEvents.length === 0) continue;
204
+
205
+ const genesisDates = relevantEvents
206
+ .filter((e) => e.event === "genesis")
207
+ .map((e) => e.asOf)
208
+ .sort();
209
+ const updateDates = relevantEvents
210
+ .filter((e) => e.event === "verify-update" || e.event === "genesis")
211
+ .map((e) => e.asOf)
212
+ .sort();
213
+
214
+ const datePublished = genesisDates[0] ?? updateDates[0] ?? relevantEvents[0].asOf;
215
+ const dateModified = updateDates[updateDates.length - 1] ?? datePublished;
216
+ const temporalCoverage =
217
+ datePublished !== dateModified ? `${datePublished}/${dateModified}` : undefined;
218
+
219
+ result.push({ page, datePublished, dateModified, temporalCoverage });
220
+ }
221
+ return result.sort((a, b) => a.page.localeCompare(b.page));
222
+ }
@@ -0,0 +1,122 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ RFC-0216: Content Maintenance Plan — shared contracts and task-id hashing. The
5
+ planner is a pure consolidator over RFC-0213 (freshness ledger), RFC-0214 (source
6
+ outbox), and RFC-0215 (derivation states). Each signal becomes a deduplicated
7
+ MaintenanceTask with a pre-deadline dueAt, owner, criticality, and the originating
8
+ Diagnostic rule ids. Framework-free; no filesystem access.
9
+ </purpose>
10
+ <keywords>RFC-0216, CKL, maintenance, plan, task, criticality, calendar</keywords>
11
+ <responsibilities>
12
+ <item>Define MaintenanceTask, MaintenancePlan, Criticality, MaintenanceTrigger.</item>
13
+ <item>Stable task id = sha256 of (subject + trigger) so re-runs are idempotent.</item>
14
+ <item>Compute pre-deadline dueAt from validUntil − leadTime or reviewDueAt.</item>
15
+ </responsibilities>
16
+ <non-goals>
17
+ <item>Do not read the filesystem or fetch sources — the kernel command does that.</item>
18
+ <item>Do not emit Diagnostics — tasks are the plan's output; the gate reads plan.gate.</item>
19
+ </non-goals>
20
+ </MODULE_CONTRACT>
21
+ <MODULE_MAP>
22
+ <entry key="MaintenanceTask / MaintenancePlan">Core plan contracts.</entry>
23
+ <entry key="stableTaskId">Idempotent hash for (subject, trigger).</entry>
24
+ <entry key="computeDueAt">Pre-deadline task date from policy + signal.</entry>
25
+ </MODULE_MAP>
26
+ <CHANGE_SUMMARY>
27
+ <item>RFC-0216: initial maintenance plan contracts.</item>
28
+ </CHANGE_SUMMARY>
29
+ */
30
+
31
+ import { createHash } from "node:crypto";
32
+
33
+ export type MaintenanceTrigger =
34
+ "review-due" | "expiring-soon" | "expired" | "source-diverged" | "derived-outdated";
35
+
36
+ export type Criticality = "advisory" | "important" | "blocking";
37
+
38
+ export interface MaintenanceTask {
39
+ /** Stable hash of (subject + trigger) — idempotent across plan builds. */
40
+ id: string;
41
+ subject: string;
42
+ trigger: MaintenanceTrigger;
43
+ /** Pre-deadline: validUntil − leadTime, or reviewDueAt, or today for already-expired/diverged. */
44
+ dueAt: string;
45
+ criticality: Criticality;
46
+ owner: string;
47
+ /** Originating Diagnostic rule ids (e.g. "CKL-FRESH-02 apps/…/offer.claims.yaml:3"). */
48
+ diagnostics: string[];
49
+ status: "open" | "routed" | "done";
50
+ }
51
+
52
+ export interface MaintenancePlan {
53
+ generatedAt: string | null;
54
+ site: string;
55
+ tasks: MaintenanceTask[];
56
+ gate: {
57
+ /** Amber: important + expired, advisory + any, or important + expiring. Ships with flag. */
58
+ amber: number;
59
+ /** Red: blocking + expired/diverged. Blocks build. */
60
+ red: number;
61
+ };
62
+ }
63
+
64
+ /** Deterministic task id — hash of (subject, trigger). */
65
+ export function stableTaskId(subject: string, trigger: MaintenanceTrigger): string {
66
+ return `t_${createHash("sha256").update(`${subject}:${trigger}`, "utf8").digest("hex").slice(0, 16)}`;
67
+ }
68
+
69
+ /**
70
+ * Compute a pre-deadline dueAt for a task.
71
+ * - For time-based signals: validUntil − leadTimeDays (clamped to today if already past).
72
+ * - For already-expired or event-driven signals: today (already overdue).
73
+ */
74
+ export function computeDueAt(opts: {
75
+ validUntil?: string;
76
+ reviewDueAt?: string;
77
+ leadTimeDays: number;
78
+ today: string;
79
+ }): string {
80
+ const { validUntil, reviewDueAt, leadTimeDays, today } = opts;
81
+ if (validUntil) {
82
+ // Subtract leadTimeDays by finding (validUntil in ms) - (leadTimeDays * ms/day).
83
+ const vu = Date.parse(`${validUntil}T00:00:00Z`);
84
+ if (!Number.isNaN(vu)) {
85
+ const early = new Date(vu - leadTimeDays * 86_400_000).toISOString().slice(0, 10);
86
+ if (early > today) return early;
87
+ }
88
+ return today;
89
+ }
90
+ if (reviewDueAt) return reviewDueAt < today ? today : reviewDueAt;
91
+ return today;
92
+ }
93
+
94
+ /** Default gate policy for a trigger × criticality pair. */
95
+ export function defaultCriticality(trigger: MaintenanceTrigger): Criticality {
96
+ if (trigger === "expired") return "important";
97
+ if (trigger === "source-diverged") return "important";
98
+ return "advisory";
99
+ }
100
+
101
+ /**
102
+ * The single source of truth for the red (build-blocking) gate verdict, shared by
103
+ * content.plan.build and (future) APPS_CHECK so the two never diverge. A task is
104
+ * red iff it is `blocking` criticality AND its claim no longer matches reality,
105
+ * has drifted from its source/derivation, or its required comparative-commercial
106
+ * source review is due.
107
+ */
108
+ export function isRedTask(
109
+ task: Pick<MaintenanceTask, "criticality" | "trigger" | "status">,
110
+ ): boolean {
111
+ if (task.status === "done") return false;
112
+ if (task.criticality !== "blocking") return false;
113
+ return (
114
+ task.trigger === "review-due" ||
115
+ task.trigger === "expired" ||
116
+ task.trigger === "source-diverged" ||
117
+ task.trigger === "derived-outdated"
118
+ );
119
+ }
120
+
121
+ export const GENERATED_MARKER =
122
+ "GENERATED. Do not change this line unless the file contains project specific changes.";
@@ -0,0 +1,170 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ RFC-0214: external source binding. A source descriptor names an external authority
5
+ and how to obtain a value (kind, endpoint, extraction, expected type, check cadence,
6
+ tolerance). A claim's sourceRef resolves to a descriptor; the shared Truth Monitor
7
+ fetches it on a cadence and compares the observed value to the asserted one. This
8
+ module is the framework-free contract: the zod descriptor schema, value extraction,
9
+ and tolerance comparison — reused by source.binding.validate and source.monitor.run.
10
+ </purpose>
11
+ <non-goals>
12
+ <item>Do not perform network I/O — the monitor command/worker fetches; this module only shapes + compares.</item>
13
+ <item>Do not parse HTML selectors here — that lives in the worker runtime (browser/DOM); the shared lib handles JSON.</item>
14
+ </non-goals>
15
+ </MODULE_CONTRACT>
16
+ <CHANGE_SUMMARY>
17
+ <item>RFC-0214: initial source descriptor schema, JSON extraction, tolerance compare.</item>
18
+ </CHANGE_SUMMARY>
19
+ */
20
+
21
+ import { z } from "zod";
22
+
23
+ export const SOURCE_KINDS = ["http-json", "http-html-selector", "rest-api", "manual"] as const;
24
+ export type SourceKind = (typeof SOURCE_KINDS)[number];
25
+
26
+ const ISO8601_DURATION = /^P(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+S)?)?$/;
27
+
28
+ export const sourceToleranceSchema = z.object({
29
+ kind: z.enum(["exact", "relative", "absolute"]),
30
+ value: z.number().nonnegative(),
31
+ });
32
+ export type SourceTolerance = z.infer<typeof sourceToleranceSchema>;
33
+
34
+ export const sourceDescriptorSchema = z
35
+ .object({
36
+ id: z.string().min(1),
37
+ title: z.string().min(1),
38
+ kind: z.enum(SOURCE_KINDS),
39
+ endpoint: z.string().url().optional(),
40
+ /** JSONPath-lite (`$.a.b[0].c`) or CSS selector (http-html-selector). */
41
+ extract: z.string().min(1).optional(),
42
+ expectedType: z.enum(["string", "integer", "number", "money", "date", "enum"]),
43
+ checkEvery: z
44
+ .string()
45
+ .regex(ISO8601_DURATION, "must be an ISO 8601 duration (e.g. P3M)")
46
+ .refine((v) => v !== "P" && v !== "PT", "duration must have at least one component"),
47
+ tolerance: sourceToleranceSchema.optional(),
48
+ residency: z.enum(["eu", "any"]).optional().default("eu"),
49
+ /** RFC-0241: the source's license (e.g. "CC-BY-4.0"), for third-party cited datasets. */
50
+ license: z.string().min(1).optional(),
51
+ /**
52
+ * RFC-0241: identity-firewall marker for external public-good sources that must be cited, never
53
+ * branded as a studio project. "ownership-forbidden" is enforced by a dedicated firewall check
54
+ * (e.g. `hdri.firewall.validate`), not by this schema.
55
+ */
56
+ firewall: z.enum(["ownership-forbidden"]).optional(),
57
+ })
58
+ .strict()
59
+ .superRefine((d, ctx) => {
60
+ if (d.kind !== "manual" && !d.endpoint) {
61
+ ctx.addIssue({
62
+ code: "custom",
63
+ message: `kind "${d.kind}" requires an endpoint`,
64
+ path: ["endpoint"],
65
+ });
66
+ }
67
+ if ((d.kind === "http-json" || d.kind === "http-html-selector") && !d.extract) {
68
+ ctx.addIssue({
69
+ code: "custom",
70
+ message: `kind "${d.kind}" requires an extract path`,
71
+ path: ["extract"],
72
+ });
73
+ }
74
+ });
75
+
76
+ export type SourceDescriptor = z.infer<typeof sourceDescriptorSchema>;
77
+
78
+ /**
79
+ * Read a JSONPath-lite expression `$.a.b[0].c` from a parsed JSON value.
80
+ * Supports dotted keys and `[index]` array access. Returns undefined on any miss.
81
+ */
82
+ export function extractJsonPath(payload: unknown, path: string): unknown {
83
+ const trimmed = path.startsWith("$.")
84
+ ? path.slice(2)
85
+ : path.startsWith("$")
86
+ ? path.slice(1)
87
+ : path;
88
+ const tokens: string[] = [];
89
+ for (const seg of trimmed.split(".")) {
90
+ if (seg === "") continue;
91
+ const m = seg.match(/^([^[\]]*)((?:\[\d+\])*)$/);
92
+ if (!m) return undefined;
93
+ if (m[1]) tokens.push(m[1]);
94
+ for (const idx of m[2]!.matchAll(/\[(\d+)\]/g)) tokens.push(idx[1]!);
95
+ }
96
+ let cur: unknown = payload;
97
+ for (const tok of tokens) {
98
+ if (cur === null || cur === undefined) return undefined;
99
+ if (Array.isArray(cur)) {
100
+ const i = Number(tok);
101
+ if (!Number.isInteger(i)) return undefined;
102
+ cur = cur[i];
103
+ } else if (typeof cur === "object") {
104
+ cur = (cur as Record<string, unknown>)[tok];
105
+ } else {
106
+ return undefined;
107
+ }
108
+ }
109
+ return cur;
110
+ }
111
+
112
+ /** Pull the first number out of a string/number (handles "70 € / Monat", "39,120"). */
113
+ export function coerceNumber(value: unknown): number | null {
114
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
115
+ if (typeof value !== "string") return null;
116
+ const cleaned = value
117
+ .replace(/\s/g, "")
118
+ .replace(/[.,](?=\d{3}(?:\D|$))/g, "") // drop thousands separators
119
+ .replace(",", "."); // remaining comma = decimal point
120
+ const m = cleaned.match(/-?\d+(?:\.\d+)?/);
121
+ return m ? Number(m[0]) : null;
122
+ }
123
+
124
+ /**
125
+ * Compare an observed value against the asserted one under a descriptor's tolerance.
126
+ * Numeric types compare numerically (with relative/absolute bands); others compare as
127
+ * normalized strings (exact). Returns whether the observed value is within tolerance.
128
+ */
129
+ export function compareValues(
130
+ asserted: unknown,
131
+ observed: unknown,
132
+ expectedType: SourceDescriptor["expectedType"],
133
+ tolerance?: SourceTolerance,
134
+ ): { withinTolerance: boolean; assertedStr: string; observedStr: string } {
135
+ const assertedStr = String(asserted ?? "");
136
+ const observedStr = String(observed ?? "");
137
+ const numeric =
138
+ expectedType === "integer" || expectedType === "number" || expectedType === "money";
139
+
140
+ if (numeric) {
141
+ const a = coerceNumber(asserted);
142
+ const o = coerceNumber(observed);
143
+ if (a === null || o === null) {
144
+ return { withinTolerance: assertedStr === observedStr, assertedStr, observedStr };
145
+ }
146
+ const t = tolerance ?? { kind: "exact" as const, value: 0 };
147
+ let within: boolean;
148
+ if (t.kind === "exact") within = a === o;
149
+ else if (t.kind === "absolute") within = Math.abs(a - o) <= t.value;
150
+ else within = a === 0 ? o === 0 : Math.abs(a - o) / Math.abs(a) <= t.value; // relative
151
+ return { withinTolerance: within, assertedStr, observedStr };
152
+ }
153
+
154
+ const norm = (s: string) => s.replace(/\s+/g, " ").trim();
155
+ return { withinTolerance: norm(assertedStr) === norm(observedStr), assertedStr, observedStr };
156
+ }
157
+
158
+ /** One Truth Monitor finding, written to the outbox and surfaced by source.binding.validate. */
159
+ export interface DivergenceRecord {
160
+ sourceId: string;
161
+ /** Canonical subject string of the bound claim. */
162
+ subject: string;
163
+ app: string;
164
+ assertedValue: string;
165
+ observedValue: string;
166
+ withinTolerance: boolean;
167
+ observedAt: string;
168
+ /** True when the source could not be fetched (CKL-SRC-04, info — not a divergence). */
169
+ unreachable?: boolean;
170
+ }