@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,302 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import {
3
+ normalizeSegment,
4
+ matchesRecord,
5
+ countMatching,
6
+ buildRecordIndex,
7
+ countMatchingIndexed,
8
+ pathKey,
9
+ enumerateCandidateTuples,
10
+ buildEligibilityMatrix,
11
+ nearestLiveAncestor,
12
+ liveChildrenOf,
13
+ liveSiblingsOf,
14
+ type AxisFieldMap,
15
+ } from "../eligibility.ts";
16
+ import type { SurfaceAxis, SurfaceRecord, EligibilityPolicy } from "../types.ts";
17
+
18
+ const SEGMENT_PATTERN = /^[a-z0-9-]+$/;
19
+
20
+ const defaultPolicy: EligibilityPolicy = {
21
+ minRecordsPerDepth: { 0: 1, 1: 1, 2: 1 },
22
+ noindexBelowPerDepth: {},
23
+ redirectPolicy: "nearest-ancestor",
24
+ trailingSlash: true,
25
+ segmentPattern: SEGMENT_PATTERN,
26
+ };
27
+
28
+ const axes: SurfaceAxis[] = [
29
+ { id: "trade", universe: ["elektriker", "friseur"] },
30
+ { id: "region", universe: ["berlin", "hamburg"] },
31
+ ];
32
+
33
+ const axisFieldMap: AxisFieldMap = { trade: "trade", region: "region" };
34
+
35
+ const records: SurfaceRecord[] = [
36
+ { trade: "elektriker", region: "berlin", status: "active" },
37
+ { trade: "elektriker", region: "hamburg", status: "active" },
38
+ { trade: "friseur", region: "berlin", status: "active" },
39
+ { trade: "friseur", region: "hamburg", status: "archived" },
40
+ ];
41
+
42
+ describe("normalizeSegment", () => {
43
+ test("lowercases and converts underscores to dashes", () => {
44
+ expect(normalizeSegment("Hello_World", SEGMENT_PATTERN)).toBe("hello-world");
45
+ });
46
+
47
+ test("trims whitespace", () => {
48
+ expect(normalizeSegment(" berlin ", SEGMENT_PATTERN)).toBe("berlin");
49
+ });
50
+
51
+ test("throws on invalid characters", () => {
52
+ expect(() => normalizeSegment("hello world!", SEGMENT_PATTERN)).toThrow();
53
+ });
54
+
55
+ test("passes through already-normalized segment", () => {
56
+ expect(normalizeSegment("elektriker", SEGMENT_PATTERN)).toBe("elektriker");
57
+ });
58
+ });
59
+
60
+ describe("matchesRecord", () => {
61
+ test("matches when all constrained axes match", () => {
62
+ expect(
63
+ matchesRecord(records[0]!, { trade: "elektriker", region: "berlin" }, axisFieldMap),
64
+ ).toBe(true);
65
+ });
66
+
67
+ test("does not match when one axis differs", () => {
68
+ expect(matchesRecord(records[0]!, { trade: "friseur", region: "berlin" }, axisFieldMap)).toBe(
69
+ false,
70
+ );
71
+ });
72
+
73
+ test("matches when tuple is empty (no constraints)", () => {
74
+ expect(matchesRecord(records[0]!, {}, axisFieldMap)).toBe(true);
75
+ });
76
+
77
+ test("matches when only one axis is constrained", () => {
78
+ expect(matchesRecord(records[0]!, { trade: "elektriker" }, axisFieldMap)).toBe(true);
79
+ expect(matchesRecord(records[0]!, { trade: "friseur" }, axisFieldMap)).toBe(false);
80
+ });
81
+
82
+ test("returns false when axis field is missing from map", () => {
83
+ expect(matchesRecord(records[0]!, { unknown: "x" }, axisFieldMap)).toBe(false);
84
+ });
85
+
86
+ test("matches array field values", () => {
87
+ const rec: SurfaceRecord = { tags: ["a", "b"], status: "active" };
88
+ expect(matchesRecord(rec, { tags: "a" }, { tags: "tags" })).toBe(true);
89
+ expect(matchesRecord(rec, { tags: "c" }, { tags: "tags" })).toBe(false);
90
+ });
91
+ });
92
+
93
+ describe("countMatching", () => {
94
+ test("counts all active records for empty tuple", () => {
95
+ expect(countMatching(records, {}, axisFieldMap)).toBe(3);
96
+ });
97
+
98
+ test("counts matching records for a specific tuple", () => {
99
+ expect(countMatching(records, { trade: "elektriker" }, axisFieldMap)).toBe(2);
100
+ expect(countMatching(records, { trade: "friseur" }, axisFieldMap)).toBe(1);
101
+ });
102
+
103
+ test("excludes archived records", () => {
104
+ expect(countMatching(records, { trade: "friseur", region: "hamburg" }, axisFieldMap)).toBe(0);
105
+ });
106
+ });
107
+
108
+ describe("buildRecordIndex + countMatchingIndexed", () => {
109
+ const index = buildRecordIndex(records, axisFieldMap);
110
+
111
+ test("index count matches linear count for empty tuple", () => {
112
+ expect(countMatchingIndexed({}, axisFieldMap, index)).toBe(3);
113
+ });
114
+
115
+ test("index count matches linear count for specific tuple", () => {
116
+ expect(countMatchingIndexed({ trade: "elektriker" }, axisFieldMap, index)).toBe(2);
117
+ expect(countMatchingIndexed({ trade: "friseur", region: "berlin" }, axisFieldMap, index)).toBe(
118
+ 1,
119
+ );
120
+ });
121
+
122
+ test("returns 0 for non-existent axis", () => {
123
+ expect(countMatchingIndexed({ unknown: "x" }, axisFieldMap, index)).toBe(0);
124
+ });
125
+
126
+ test("returns 0 for non-existent value", () => {
127
+ expect(countMatchingIndexed({ trade: "nonexistent" }, axisFieldMap, index)).toBe(0);
128
+ });
129
+ });
130
+
131
+ describe("pathKey", () => {
132
+ test("depth 0 produces just '0'", () => {
133
+ expect(pathKey(0, {}, ["trade", "region"])).toBe("0");
134
+ });
135
+
136
+ test("depth 1 includes first axis value", () => {
137
+ expect(pathKey(1, { trade: "elektriker" }, ["trade", "region"])).toBe("1|elektriker");
138
+ });
139
+
140
+ test("depth 2 includes both axis values", () => {
141
+ expect(pathKey(2, { trade: "elektriker", region: "berlin" }, ["trade", "region"])).toBe(
142
+ "2|elektriker|berlin",
143
+ );
144
+ });
145
+
146
+ test("missing axis value becomes empty string", () => {
147
+ expect(pathKey(2, { trade: "elektriker" }, ["trade", "region"])).toBe("2|elektriker|");
148
+ });
149
+ });
150
+
151
+ describe("enumerateCandidateTuples", () => {
152
+ test("depth 0 yields single empty tuple", () => {
153
+ const tuples = [...enumerateCandidateTuples(axes, 0)];
154
+ expect(tuples).toEqual([{}]);
155
+ });
156
+
157
+ test("depth 1 yields all first-axis values", () => {
158
+ const tuples = [...enumerateCandidateTuples(axes, 1)];
159
+ expect(tuples).toHaveLength(2);
160
+ expect(tuples[0]).toEqual({ trade: "elektriker" });
161
+ expect(tuples[1]).toEqual({ trade: "friseur" });
162
+ });
163
+
164
+ test("depth 2 yields cartesian product", () => {
165
+ const tuples = [...enumerateCandidateTuples(axes, 2)];
166
+ expect(tuples).toHaveLength(4);
167
+ expect(tuples.map((t) => `${t.trade}/${t.region}`).sort()).toEqual([
168
+ "elektriker/berlin",
169
+ "elektriker/hamburg",
170
+ "friseur/berlin",
171
+ "friseur/hamburg",
172
+ ]);
173
+ });
174
+
175
+ test("empty universe yields nothing", () => {
176
+ const emptyAxes: SurfaceAxis[] = [{ id: "trade", universe: [] }];
177
+ expect([...enumerateCandidateTuples(emptyAxes, 1)]).toHaveLength(0);
178
+ });
179
+ });
180
+
181
+ describe("buildEligibilityMatrix", () => {
182
+ const matrix = buildEligibilityMatrix(axes, axisFieldMap, records, defaultPolicy);
183
+
184
+ test("has correct axis order", () => {
185
+ expect(matrix.axisOrder).toEqual(["trade", "region"]);
186
+ });
187
+
188
+ test("root entry (depth 0) is live with 3 records", () => {
189
+ const root = matrix.byKey.get("0");
190
+ expect(root).toBeDefined();
191
+ expect(root!.recordCount).toBe(3);
192
+ expect(root!.indexable).toBe(true);
193
+ });
194
+
195
+ test("elektriker depth 1 is live with 2 records", () => {
196
+ const entry = matrix.byKey.get("1|elektriker");
197
+ expect(entry).toBeDefined();
198
+ expect(entry!.recordCount).toBe(2);
199
+ expect(entry!.indexable).toBe(true);
200
+ });
201
+
202
+ test("friseur depth 1 is live with 1 record", () => {
203
+ const entry = matrix.byKey.get("1|friseur");
204
+ expect(entry).toBeDefined();
205
+ expect(entry!.recordCount).toBe(1);
206
+ expect(entry!.indexable).toBe(true);
207
+ });
208
+
209
+ test("friseur/hamburg depth 2 is not live (archived only)", () => {
210
+ const entry = matrix.byKey.get("2|friseur|hamburg");
211
+ expect(entry).toBeUndefined();
212
+ });
213
+
214
+ test("elektriker/berlin depth 2 is live", () => {
215
+ const entry = matrix.byKey.get("2|elektriker|berlin");
216
+ expect(entry).toBeDefined();
217
+ expect(entry!.indexable).toBe(true);
218
+ });
219
+
220
+ test("forceNonIndexableDepths gates a depth as regional-gated", () => {
221
+ const matrix2 = buildEligibilityMatrix(axes, axisFieldMap, records, defaultPolicy, {
222
+ forceNonIndexableDepths: [2],
223
+ maxStubDepth: 2,
224
+ });
225
+ const entry = matrix2.byKey.get("2|elektriker|berlin");
226
+ expect(entry).toBeDefined();
227
+ expect(entry!.indexable).toBe(false);
228
+ expect(entry!.decision.reason).toBe("regional-gated");
229
+ });
230
+ });
231
+
232
+ describe("nearestLiveAncestor", () => {
233
+ const matrix = buildEligibilityMatrix(axes, axisFieldMap, records, defaultPolicy);
234
+
235
+ test("returns root for depth 1 entry", () => {
236
+ const entry = matrix.byKey.get("1|elektriker")!;
237
+ const ancestor = nearestLiveAncestor(entry, matrix, defaultPolicy);
238
+ expect(ancestor?.depth).toBe(0);
239
+ });
240
+
241
+ test("returns nearest live ancestor for depth 2 entry", () => {
242
+ const entry = matrix.byKey.get("2|elektriker|berlin")!;
243
+ const ancestor = nearestLiveAncestor(entry, matrix, defaultPolicy);
244
+ expect(ancestor?.depth).toBe(1);
245
+ expect(ancestor?.tuple.trade).toBe("elektriker");
246
+ });
247
+
248
+ test("redirectPolicy root always returns root", () => {
249
+ const rootPolicy = { ...defaultPolicy, redirectPolicy: "root" as const };
250
+ const entry = matrix.byKey.get("2|elektriker|berlin")!;
251
+ const ancestor = nearestLiveAncestor(entry, matrix, rootPolicy);
252
+ expect(ancestor?.depth).toBe(0);
253
+ });
254
+ });
255
+
256
+ describe("liveChildrenOf", () => {
257
+ const matrix = buildEligibilityMatrix(axes, axisFieldMap, records, defaultPolicy);
258
+
259
+ test("root has live children at depth 1", () => {
260
+ const root = matrix.byKey.get("0")!;
261
+ const children = liveChildrenOf(root, matrix);
262
+ expect(children).toHaveLength(2);
263
+ expect(children.map((c) => c.tuple.trade).sort()).toEqual(["elektriker", "friseur"]);
264
+ });
265
+
266
+ test("elektriker depth 1 has live children at depth 2", () => {
267
+ const parent = matrix.byKey.get("1|elektriker")!;
268
+ const children = liveChildrenOf(parent, matrix);
269
+ expect(children).toHaveLength(2);
270
+ expect(children.map((c) => c.tuple.region).sort()).toEqual(["berlin", "hamburg"]);
271
+ });
272
+
273
+ test("friseur depth 1 has one live child (berlin)", () => {
274
+ const parent = matrix.byKey.get("1|friseur")!;
275
+ const children = liveChildrenOf(parent, matrix);
276
+ expect(children).toHaveLength(1);
277
+ expect(children[0]!.tuple.region).toBe("berlin");
278
+ });
279
+ });
280
+
281
+ describe("liveSiblingsOf", () => {
282
+ const matrix = buildEligibilityMatrix(axes, axisFieldMap, records, defaultPolicy);
283
+
284
+ test("depth 0 has no siblings", () => {
285
+ const root = matrix.byKey.get("0")!;
286
+ expect(liveSiblingsOf(root, matrix)).toHaveLength(0);
287
+ });
288
+
289
+ test("elektriker at depth 1 has friseur as sibling", () => {
290
+ const entry = matrix.byKey.get("1|elektriker")!;
291
+ const siblings = liveSiblingsOf(entry, matrix);
292
+ expect(siblings).toHaveLength(1);
293
+ expect(siblings[0]!.tuple.trade).toBe("friseur");
294
+ });
295
+
296
+ test("elektriker/berlin at depth 2 has elektriker/hamburg as sibling", () => {
297
+ const entry = matrix.byKey.get("2|elektriker|berlin")!;
298
+ const siblings = liveSiblingsOf(entry, matrix);
299
+ expect(siblings).toHaveLength(1);
300
+ expect(siblings[0]!.tuple.region).toBe("hamburg");
301
+ });
302
+ });
@@ -0,0 +1,289 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ [RFC-0192] Closed contracts for the Programmatic Surface route-source port: the axis/record
5
+ model, the eligibility policy, the materialized virtual route entry, and the PageSurfaceProvider
6
+ interface. Framework-free — no Astro, no Node I/O. These types are the seam shared by the
7
+ build-time generator (kernel command) and the runtime registry merge.
8
+ </purpose>
9
+ <non-goals>
10
+ <item>Do not read content or the filesystem — all I/O lives in the kernel command wrapper.</item>
11
+ <item>Do not define the Blueprint schema (see blueprint.ts) or substance/budget/freshness (later RFCs).</item>
12
+ </non-goals>
13
+ </MODULE_CONTRACT>
14
+ <CHANGE_SUMMARY>
15
+ <item>RFC-0192: initial contracts.</item>
16
+ <item>RFC-0207: SurfaceNarrative (bespoke per-page hero/section prose) + SurfaceRecordImage (per-record image token + alt).</item>
17
+ </CHANGE_SUMMARY>
18
+ */
19
+
20
+ /**
21
+ * RFC-0207: the bespoke per-page narrative that replaces template-glued headings for an indexable
22
+ * page. Produced once per live tuple (or per record) by surface.enrich, stored frozen + approved as
23
+ * a content entry, and read by the baker. The h1/lead/tagline land in the hero; bridges become
24
+ * connective section prose between the field-driven blocks. Transcreated natively per language.
25
+ */
26
+ export interface SurfaceNarrative {
27
+ /** Grammatically complete page H1 (not a token-glued title). */
28
+ h1: string;
29
+ /** Hero lead — distinct from any record field and from the tagline. */
30
+ lead: string;
31
+ /** Hero tagline — distinct from the lead. */
32
+ tagline?: string;
33
+ /** Connective section prose woven between the field-driven blocks. */
34
+ bridges?: Array<{ heading: string; body: string }>;
35
+ }
36
+
37
+ /**
38
+ * RFC-0207: a surface dataset record's image. `image` is a content-asset token resolved exactly like
39
+ * a hero `leadImage.src` / `portraitImage` (bare filename under the app's content assets, RFC-0167);
40
+ * `imageAlt` is the meaningful alt text (per-language record carries the per-language alt). Mixed into
41
+ * SurfaceRecord so the baker can read it without a separate load.
42
+ */
43
+ export interface SurfaceRecordImage {
44
+ image?: string;
45
+ imageAlt?: string;
46
+ }
47
+
48
+ /**
49
+ * RFC-0192: a block in a baked page. Structurally compatible with `@warpgogol/werkstatt-shared/share` `BlockEntry`, but
50
+ * declared here so `@warpgogol/werkstatt-shared/surface` stays the lower-level package with no dependency on share
51
+ * (avoids a workspace cycle — share consumes surface's route types).
52
+ */
53
+ export interface SurfaceBlock {
54
+ id?: string;
55
+ /** CMS-facing archetype slug (e.g. "hero", "markdown", "final-cta"). */
56
+ type?: string;
57
+ /** PlanetName/MoonName (normalized from `type` for internal resolution). */
58
+ use?: string;
59
+ props: Record<string, unknown>;
60
+ layer?: "shell" | "section";
61
+ }
62
+
63
+ /** RFC-0192: a baked block-declarative page. Structurally compatible with `@warpgogol/werkstatt-shared/share` `PageEntry`. */
64
+ export interface PageEntry {
65
+ kind: "page";
66
+ cosmicStar: string;
67
+ title: string;
68
+ description: string;
69
+ lang: string;
70
+ blocks: SurfaceBlock[];
71
+ }
72
+
73
+ /** A single combinatorial axis and the closed universe of value slugs it ranges over. */
74
+ export interface SurfaceAxis {
75
+ id: string;
76
+ /** Value slugs, sourced from a content-source collection by the generator. */
77
+ universe: readonly string[];
78
+ }
79
+
80
+ /** A partial assignment of axis ids → chosen value (undefined ⇒ axis not yet constrained). */
81
+ export type AxisTuple = Record<string, string | undefined>;
82
+
83
+ /**
84
+ * RFC-0199: a value's slug, optionally localized per language. The `neutral` slug is the
85
+ * language-neutral identity key (filename stem / `slug` field) and the fallback; `byLang`
86
+ * supplies a per-language URL segment. Languages absent from `byLang` fall back to `neutral`.
87
+ */
88
+ export interface LocalizedSlug {
89
+ neutral: string;
90
+ byLang?: Record<string, string>;
91
+ }
92
+
93
+ /**
94
+ * RFC-0199: per-axis localized slug map — axisId → (neutral value slug → its LocalizedSlug).
95
+ * An empty/absent universe makes slug rendering fall back to the neutral tuple value, so the
96
+ * generator output is byte-identical to pre-RFC-0199 behavior when no localized slugs exist.
97
+ */
98
+ export type LocalizedUniverse = Record<string, Map<string, LocalizedSlug>>;
99
+
100
+ /**
101
+ * A dataset row projected into axis-addressable fields. Each axis declares which record field
102
+ * carries its membership (a value or an array of values). `status` gates live vs archived rows.
103
+ */
104
+ export type SurfaceRecord = Record<string, string | string[] | undefined> & {
105
+ slug?: string;
106
+ status?: "active" | "archived";
107
+ };
108
+
109
+ export type RedirectPolicy = "nearest-ancestor" | "root";
110
+
111
+ /**
112
+ * The eligibility policy: how many records a combination needs to go live at each depth, when a
113
+ * live page is demoted to noindex, what empty combinations redirect to, and URL-segment rules.
114
+ * Axis-generic generalization of the legacy foerderung policy.
115
+ */
116
+ export interface EligibilityPolicy {
117
+ /** depth → minimum active records required to emit a live page. */
118
+ minRecordsPerDepth: Readonly<Record<number, number>>;
119
+ /** depth → live-but-`noindex` threshold (live page tagged noindex below this count). */
120
+ noindexBelowPerDepth: Readonly<Record<number, number>>;
121
+ redirectPolicy: RedirectPolicy;
122
+ trailingSlash: boolean;
123
+ /** Allowed URL-segment shape (single lowercase dash segment by default). */
124
+ segmentPattern: RegExp;
125
+ }
126
+
127
+ /**
128
+ * The reason a generated page is not indexable. Extended by later RFCs (substance/budget/freshness);
129
+ * RFC-0192 emits only the record-count reasons.
130
+ */
131
+ export type IndexReason =
132
+ | "too-few-records"
133
+ | "thin" // RFC-0194
134
+ | "over-budget" // RFC-0196
135
+ | "decayed" // RFC-0196
136
+ | "regional-gated" // RFC-0240
137
+ | "invalid-freshness" // RFC-0274
138
+ | "missing-evidence" // RFC-0274
139
+ | "missing-demand" // RFC-0280
140
+ | "missing-werk-evidence" // RFC-0281
141
+ | "navigation-noindex" // RFC-0324
142
+ | "redirect-stub"
143
+ | "duplicate"; // RFC-0274
144
+
145
+ /** The composed indexability decision for a generated page. Later RFCs fill the optional gates. */
146
+ export interface IndexDecision {
147
+ recordGate: boolean;
148
+ substanceGate?: boolean;
149
+ evidenceGate?: boolean;
150
+ demandGate?: boolean;
151
+ duplicateGate?: boolean;
152
+ withinBudget?: boolean;
153
+ fresh?: boolean;
154
+ indexable: boolean;
155
+ noindex: boolean;
156
+ reason?: IndexReason;
157
+ /** RFC-0194: the page substance score (0..100) that informed the substance gate. */
158
+ substanceScore?: number;
159
+ /** RFC-0281: count of consented Werk records backing this tuple. */
160
+ werkEvidenceCount?: number;
161
+ }
162
+
163
+ /** One materialized generated route. The single unit the registry merge consumes. */
164
+ export interface VirtualRouteEntry {
165
+ /** Owning Blueprint id (e.g. "website-local"). */
166
+ surfaceId: string;
167
+ /** Synthetic stable page id, e.g. "website-local:elektriker:berlin". */
168
+ pageId: string;
169
+ /** lang → localized slug. */
170
+ routes: Record<string, string>;
171
+ axes: AxisTuple;
172
+ depth: number;
173
+ recordCount: number;
174
+ indexable: boolean;
175
+ noindex: boolean;
176
+ /** For non-live (redirect-stub) entries: the live ancestor pageId to 301 to. */
177
+ redirectToPageId?: string;
178
+ /** For live noindex navigation pages: pageId whose URL should be rendered as canonical. */
179
+ canonicalPageId?: string;
180
+ /** Semantic page type for JSON-LD / OG meta (defaults to "content"). */
181
+ semanticType?: string;
182
+ /**
183
+ * RFC-0325: dated-editorial article metadata, present when `semanticType` is "article". Mirrors
184
+ * the system.md `pages[].article` shape exactly so the RFC-0317 update-stamp resolver
185
+ * (`resolveAuthoredStamp`) and the Astro render layer's Article/BlogPosting JSON-LD wiring work
186
+ * for a Programmatic Surface page without any source-specific branching.
187
+ */
188
+ article?: {
189
+ publishedAt: string;
190
+ updatedAt?: string;
191
+ author?: string;
192
+ tags?: string[];
193
+ };
194
+ /** RFC-0195: per-depth GEO contribution — "full" (llms.txt + twin), "twin-only", "off". */
195
+ geo?: "full" | "twin-only" | "off";
196
+ /**
197
+ * Optional page-level site-background image name (resolved via the app's content assets, same as
198
+ * an authored page's `shell.background` imageName in system.md). When set, the page handler renders
199
+ * a shell-layer background instead of the generic `home-bg` default. Omit for the default.
200
+ */
201
+ backgroundImage?: string;
202
+ /**
203
+ * RFC-0192: the resolved page. In "inline" bake mode it carries the full blocks. In "lazy" mode it
204
+ * carries only metadata (title/description, empty blocks) and the full page lives in its own file
205
+ * (`.surface-cache/<pageId>.yaml`), loaded on demand at render.
206
+ */
207
+ page?: PageEntry;
208
+ /**
209
+ * RFC-0193: per-language baked pages (multilingual surfaces). Keyed by lang. `page` mirrors the
210
+ * default-language entry here. Language-neutral consumers (substance, counts) use `page`; per-lang
211
+ * consumers (render, Markdown twins, llms.txt) use `pages[lang] ?? page`.
212
+ */
213
+ pages?: Record<string, PageEntry>;
214
+ /** RFC-0192: true when the full page blocks live in a per-page file (lazy bake). */
215
+ lazy?: boolean;
216
+ /**
217
+ * RFC-0207: non-default languages whose localized page was omitted because its baked content fell
218
+ * back to the default language (no native record fields and no approved narrative). Their `routes`
219
+ * + `pages` entries are dropped, so the de-fallback page never renders under a localized URL, is
220
+ * never indexed, and never enters the sitemap/hreflang/twins. surface.validate reports each as an
221
+ * `untranslated-route` error. Reversible: supply native content (or an approved narrative) and the
222
+ * route reappears on the next generate.
223
+ */
224
+ untranslatedLangs?: string[];
225
+ /** Decision provenance for the manifest (RFC-0194/0196 enrich this). */
226
+ decision?: IndexDecision;
227
+ }
228
+
229
+ /**
230
+ * The closed route-source port. One implementation per Blueprint (RFC-0193).
231
+ * `enumerate` runs at build time (Node); `resolve` may run at build time (baked) or render time.
232
+ */
233
+ export interface PageSurfaceProvider {
234
+ readonly id: string;
235
+ enumerate(lang: string): Promise<VirtualRouteEntry[]>;
236
+ resolve(entry: VirtualRouteEntry, lang: string): Promise<PageEntry>;
237
+ }
238
+
239
+ /** Per-surface generation counts (written into pseo-manifest.json). */
240
+ export interface SurfaceCounts {
241
+ surfaceId: string;
242
+ generated: number;
243
+ indexable: number;
244
+ noindex: number;
245
+ redirected: number;
246
+ /** RFC-0194: live pages suppressed by the substance gate. */
247
+ thin?: number;
248
+ /** RFC-0194: median Page Substance Score across live pages. */
249
+ medianSubstance?: number;
250
+ /** RFC-0324: legacy composite score retained for diagnostics while medianSubstance is unique-share. */
251
+ legacyMedianSubstanceScore?: number;
252
+ /** RFC-0324: distribution of unique token share across non-redirect generated pages. */
253
+ uniqueTokenShareBands?: {
254
+ lt030: number;
255
+ gte030lt050: number;
256
+ gte050: number;
257
+ };
258
+ /** RFC-0195: Markdown twins emitted (GEO corpus). */
259
+ twins?: number;
260
+ /** RFC-0195: pages contributing to llms.txt (GEO depth "full"). */
261
+ inLlms?: number;
262
+ }
263
+
264
+ /** The pseo-manifest.json payload. */
265
+ export interface SurfaceManifest {
266
+ generatedAt: string | null;
267
+ surfaces: SurfaceCounts[];
268
+ }
269
+
270
+ /** The src/surface.generated.yaml payload consumed by the runtime registry merge. */
271
+ export interface SurfaceArtifact {
272
+ generatedAt: string | null;
273
+ entries: VirtualRouteEntry[];
274
+ }
275
+
276
+ /** RFC-0492: structured dossier fields for depth-1 industry records. */
277
+ export interface IndustryDossierFields {
278
+ customerQuestions?: string[];
279
+ customerJourneys?: Array<{ title: string; stages: string[] }>;
280
+ serviceTaxonomy?: Array<{ category: string; description: string }>;
281
+ trustSignals?: string[];
282
+ evidenceRequirements?: string[];
283
+ contactModes?: string[];
284
+ serviceAreaModel?: string;
285
+ recommendedArchitecture?: Array<{ page: string; description: string }>;
286
+ suitableModules?: Array<{ module: string; applicability: string }>;
287
+ notdienst?: boolean;
288
+ industryFaq?: Array<{ question: string; answer: string }>;
289
+ }
@@ -0,0 +1,57 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Maintains packages/surface/src/visibility.ts as an authored surface authored module so agents can evolve it without rediscovering local boundaries.</purpose>
4
+ <non-goals>
5
+ <item>Do not model per-user analytics rows or external API clients.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0282: initial implementation of visibility feedback-loop contracts.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ import { z } from "zod";
14
+
15
+ export const visibilitySourceSchema = z.enum(["gsc", "manual"]);
16
+
17
+ export const clusterActionSchema = z.enum(["expand", "hold", "prune", "enrich", "escalate"]);
18
+
19
+ export const visibilitySnapshotSchema = z.object({
20
+ clusterId: z.string().min(1),
21
+ windowStart: z.string().datetime(),
22
+ windowEnd: z.string().datetime(),
23
+ indexedPages: z.number().int().nonnegative(),
24
+ eligiblePages: z.number().int().nonnegative(),
25
+ impressions: z.number().int().nonnegative(),
26
+ clicks: z.number().int().nonnegative(),
27
+ uniqueQueries: z.number().int().nonnegative(),
28
+ avgPosition: z.number().positive().optional(),
29
+ source: visibilitySourceSchema,
30
+ importedAt: z.string().datetime(),
31
+ cannibalizingQueries: z.array(z.string().min(1)).optional(),
32
+ coreClicksBaseline: z.number().int().nonnegative().optional(),
33
+ coreClicksCurrent: z.number().int().nonnegative().optional(),
34
+ });
35
+
36
+ export const clusterOutcomeSchema = z.object({
37
+ clusterId: z.string().min(1),
38
+ surfaceId: z.string().min(1).optional(),
39
+ depth: z.number().int().nonnegative().optional(),
40
+ eligiblePages: z.number().int().nonnegative(),
41
+ indexedPages: z.number().int().nonnegative(),
42
+ impressions: z.number().int().nonnegative(),
43
+ clicks: z.number().int().nonnegative(),
44
+ uniqueQueries: z.number().int().nonnegative(),
45
+ indexationRate: z.number().min(0),
46
+ medianImpressionsPerPage: z.number().min(0),
47
+ queryDiversityShare: z.number().min(0),
48
+ positiveDemand: z.boolean(),
49
+ anomalies: z.array(z.string().min(1)),
50
+ proposedAction: clusterActionSchema,
51
+ rationale: z.string().min(1),
52
+ });
53
+
54
+ export type VisibilitySource = z.infer<typeof visibilitySourceSchema>;
55
+ export type ClusterAction = z.infer<typeof clusterActionSchema>;
56
+ export type VisibilitySnapshot = z.infer<typeof visibilitySnapshotSchema>;
57
+ export type ClusterOutcome = z.infer<typeof clusterOutcomeSchema>;
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "node",
6
+ include: ["src/**/*.test.ts"],
7
+ },
8
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "../../tsconfig/node-lib.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "outDir": "dist",
6
+ "noEmit": true,
7
+ "allowImportingTsExtensions": true
8
+ },
9
+ "include": ["src/**/*.ts"],
10
+ "exclude": [
11
+ "src/**/*.template.*",
12
+ "src/**/*.test.ts",
13
+ "src/**/tests/**",
14
+ "dist/**",
15
+ "node_modules/**"
16
+ ]
17
+ }