@warpgogol/werkstatt-shared 0.14.1 → 0.15.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 (438) hide show
  1. package/README.md +23 -15
  2. package/README.uk.md +1 -1
  3. package/package.json +517 -766
  4. package/src/{share/agent → agent}/ard-catalog.ts +6 -1
  5. package/src/{share/agent → agent}/dns-aid.test.ts +2 -2
  6. package/src/{share/agent → agent}/manifest.ts +15 -1
  7. package/src/{share/agent → agent}/search.ts +6 -1
  8. package/src/{share → agent}/tests/api-catalog.test.ts +2 -2
  9. package/src/{share → agent}/tests/ard-catalog.test.ts +2 -2
  10. package/src/{share → agent}/tests/capability.test.ts +1 -1
  11. package/src/{share → agent}/tests/fleet-catalog.test.ts +1 -1
  12. package/src/{share → agent}/tests/knowledge.test.ts +2 -2
  13. package/src/{share → agent}/tests/manifest.test.ts +1 -1
  14. package/src/{share → agent}/tests/mcp-card.test.ts +2 -2
  15. package/src/{share → agent}/tests/openapi.test.ts +2 -2
  16. package/src/{share → agent}/tests/proof.test.ts +1 -1
  17. package/src/{share → attribution}/material-credits.ts +4 -4
  18. package/src/{share → checks}/check-hints.ts +6 -1
  19. package/src/checks/index.ts +4 -1
  20. package/src/checks/lib/astro-site-url.ts +6 -1
  21. package/src/checks/lib/i18n.ts +14 -0
  22. package/src/checks/result-helpers.ts +2 -2
  23. package/src/checks/tests/remediation-auto-populate.test.ts +1 -1
  24. package/src/{share → client-scripts}/counter-utils.ts +6 -2
  25. package/src/{share/scripts → client-scripts}/external-link-qr.ts +11 -16
  26. package/src/{share/scripts → client-scripts}/external-links.ts +12 -1
  27. package/src/{share/scripts → client-scripts}/gsap-counter.ts +8 -3
  28. package/src/client-scripts/index.ts +35 -0
  29. package/src/{share/scripts → client-scripts}/inline-number-animation.ts +1 -1
  30. package/src/{share/scripts → client-scripts}/lenis.ts +6 -1
  31. package/src/{share/scripts → client-scripts}/lordicon.ts +6 -1
  32. package/src/{share/scripts → client-scripts}/orchestrator.ts +10 -1
  33. package/src/{share/scripts → client-scripts}/scheduler.ts +6 -1
  34. package/src/client-scripts/scroll-spy.ts +106 -0
  35. package/src/client-scripts/tests/orchestrator.test.ts +47 -0
  36. package/src/client-scripts/tests/scroll-spy.test.ts +204 -0
  37. package/src/{share/scripts → client-scripts/tests}/video-player.test.ts +51 -31
  38. package/src/component/contracts.ts +206 -0
  39. package/src/component/index.ts +11 -0
  40. package/src/{share → content}/content-reference.ts +3 -3
  41. package/src/{share/content → content}/entity-id.ts +6 -3
  42. package/src/content/index.ts +3 -0
  43. package/src/{share/content → content}/merge.ts +6 -1
  44. package/src/content/system-manifest.test.ts +107 -0
  45. package/src/content/system-manifest.ts +12 -94
  46. package/src/{share → content}/tests/resolve-field-path.test.ts +1 -1
  47. package/src/{share → content}/tests/substitute-deep.test.ts +1 -1
  48. package/src/content-discipline/index.ts +19 -0
  49. package/src/{share/content-discipline → content-discipline}/parsers.ts +2 -2
  50. package/src/{share/env.d.ts → env.d.ts} +6 -3
  51. package/src/fingerprint/canonical-json.ts +731 -0
  52. package/src/fingerprint/index.ts +12 -0
  53. package/src/fingerprint/primitives.ts +72 -0
  54. package/src/{share/i18n → i18n}/localization.ts +6 -2
  55. package/src/integration/adapters.ts +10 -71
  56. package/src/integration/dispatch.ts +1 -1
  57. package/src/integration/funnel.ts +6 -1
  58. package/src/integration/index.ts +101 -4
  59. package/src/integration/lagebild-destination-adapter.ts +108 -0
  60. package/src/integration/lagebild-ingress.ts +7 -12
  61. package/src/integration/orchestration.ts +11 -10
  62. package/src/integration/tests/deliver-event.test.ts +1 -1
  63. package/src/integration/tests/dispatch.test.ts +4 -4
  64. package/src/integration/tests/hub-runtime.test.ts +22 -14
  65. package/src/integration/vitest.config.ts +1 -0
  66. package/src/kernel/desired-state.ts +227 -0
  67. package/src/kernel/diagnostic.ts +289 -0
  68. package/src/kernel/fs-atomic.ts +131 -0
  69. package/src/kernel/index.ts +15 -0
  70. package/src/kernel/tests/fs-atomic.test.ts +150 -0
  71. package/src/kernel/tests/workspace-io.test.ts +187 -0
  72. package/src/kernel/types.ts +459 -0
  73. package/src/kernel/workspace-io.ts +248 -0
  74. package/src/{share → knowledge}/tests/claim.test.ts +1 -1
  75. package/src/{share → knowledge}/tests/derivation.test.ts +1 -1
  76. package/src/{share → knowledge}/tests/freshness.test.ts +1 -1
  77. package/src/{share → knowledge}/tests/ledger.test.ts +1 -1
  78. package/src/{share → knowledge}/tests/plan.test.ts +1 -1
  79. package/src/{share → knowledge}/tests/source.test.ts +1 -1
  80. package/src/legal/index.ts +15 -0
  81. package/src/{share/middleware → middleware}/access-protection.ts +8 -8
  82. package/src/{share → node}/dev-props-validator.ts +6 -1
  83. package/src/{share → node}/import-scan.ts +23 -3
  84. package/src/{share → node}/semantic/derived-prices-loader.ts +4 -2
  85. package/src/{share → node}/semantic/markdown-twin-provenance.ts +1 -1
  86. package/src/{share/semantic → node}/tests/derived-prices-loader.test.ts +14 -1
  87. package/src/{share/semantic → node}/tests/markdown-twin-provenance.test.ts +1 -1
  88. package/src/observability/env-resolver.ts +6 -1
  89. package/src/observability/index.ts +6 -1
  90. package/src/observability/otlp-converter.ts +6 -1
  91. package/src/observability/otlp-transport.ts +6 -1
  92. package/src/observability/pusher.ts +6 -1
  93. package/src/observability/redact.ts +6 -1
  94. package/src/observability/typed-refs.ts +6 -2
  95. package/src/observability/vitest.config.ts +1 -0
  96. package/src/{share → offers}/formula-eval.ts +3 -3
  97. package/src/{share → offers}/tests/formula-eval.test.ts +2 -2
  98. package/src/onboarding/index.ts +1 -0
  99. package/src/{share → onboarding}/tests/onboarding-yaml.test.ts +1 -1
  100. package/src/ontology/archetype-registry.ts +6 -1
  101. package/src/ontology/archetypes/index.json +33 -1
  102. package/src/ontology/archetypes/index.yaml +27 -1
  103. package/src/ontology/archetypes/sections/entity-grid.yaml +46 -0
  104. package/src/ontology/archetypes/sections/exit-diagram.yaml +43 -0
  105. package/src/ontology/biome-token-projection.ts +6 -1
  106. package/src/ontology/cosmic/index.ts +6 -1
  107. package/src/ontology/cosmic/moon-catalog.ts +6 -1
  108. package/src/ontology/cosmic/planet-catalog.ts +6 -1
  109. package/src/ontology/cosmic/star-catalog.ts +6 -1
  110. package/src/ontology/enums.ts +6 -1
  111. package/src/ontology/external-surfaces/index.ts +1 -1
  112. package/src/ontology/index.ts +6 -2
  113. package/src/ontology/manifest.ts +7 -4
  114. package/src/ontology/operations/artifact-store.ts +45 -0
  115. package/src/ontology/operations/dht.ts +98 -0
  116. package/src/ontology/operations/handoff.ts +92 -0
  117. package/src/ontology/operations/index.ts +187 -1
  118. package/src/ontology/operations/leitstand.ts +99 -0
  119. package/src/ontology/operations/materialization.ts +90 -0
  120. package/src/ontology/operations/mission.ts +111 -0
  121. package/src/ontology/operations/naming-policy.ts +132 -0
  122. package/src/ontology/operations/notausgang.ts +64 -0
  123. package/src/ontology/operations/release.ts +121 -0
  124. package/src/ontology/operations/sternsystem.ts +192 -0
  125. package/src/ontology/operations/werkstatt.ts +48 -0
  126. package/src/ontology/schemas/biome.ts +7 -2
  127. package/src/ontology/schemas/capability.ts +1 -1
  128. package/src/ontology/schemas/constellation.ts +6 -1
  129. package/src/ontology/schemas/index.ts +6 -2
  130. package/src/ontology/schemas/manifest-resolver.ts +6 -1
  131. package/src/ontology/schemas/page-entry.ts +9 -5
  132. package/src/ontology/schemas/system/growth.ts +6 -1
  133. package/src/ontology/schemas/system/manifest.ts +40 -1
  134. package/src/ontology/schemas/system/page-output.ts +7 -6
  135. package/src/ontology/schemas/system/text.ts +15 -0
  136. package/src/ontology/schemas/system.ts +6 -2
  137. package/src/ontology/shared-section-props/body-fragments.ts +2 -0
  138. package/src/ontology/tests/sternsystem-owner.test.ts +1 -1
  139. package/src/ontology/vitest.config.ts +1 -0
  140. package/src/{share → page}/page.ts +13 -9
  141. package/src/{share → page}/tests/dev-props-validator.test.ts +1 -1
  142. package/src/{share → page}/tests/hero-section-numbering-parity.test.ts +1 -1
  143. package/src/passport/index.ts +6 -1
  144. package/src/passport/key-rotate.ts +10 -2
  145. package/src/passport/schema.ts +6 -1
  146. package/src/passport/sign.ts +8 -3
  147. package/src/passport/vitest.config.ts +1 -0
  148. package/src/pbt/index.test.ts +95 -0
  149. package/src/pbt/index.ts +251 -0
  150. package/src/plugin/plugin-contract.ts +1 -1
  151. package/src/{share → policy}/entitlement.ts +24 -4
  152. package/src/{share → policy}/feature-policy.ts +8 -3
  153. package/src/{share → policy}/visibility.ts +7 -2
  154. package/src/redirects/redirects.ts +117 -0
  155. package/src/{share → redirects/tests}/redirects.test.ts +1 -1
  156. package/src/remediation/remediation-catalog.ts +316 -0
  157. package/src/{share/routes → routes}/template-filter.ts +1 -1
  158. package/src/{share → runtime}/runtime-context.ts +7 -2
  159. package/src/{share → runtime}/shared-context.ts +1 -1
  160. package/src/{share/schemas → schemas}/claims.ts +2 -2
  161. package/src/{share/schemas → schemas}/effects.ts +6 -1
  162. package/src/{share/schemas → schemas}/features.ts +1 -1
  163. package/src/{share/schemas → schemas}/index.ts +3 -3
  164. package/src/{share/schemas → schemas}/navigation.ts +6 -1
  165. package/src/{share/schemas → schemas}/page-base.ts +6 -2
  166. package/src/{share/schemas → schemas}/person.ts +1 -1
  167. package/src/{share/schemas → schemas}/standard-list-item.ts +1 -1
  168. package/src/{share → sections}/rfc0042-utils.ts +1 -1
  169. package/src/{share/semantic → semantic}/block-extractors/index.ts +20 -1
  170. package/src/{share/semantic → semantic}/business-projection.ts +95 -1
  171. package/src/semantic/canonical-uri.ts +54 -0
  172. package/src/{share/semantic → semantic}/extract.ts +100 -1
  173. package/src/semantic/fact-extraction.test.ts +47 -0
  174. package/src/semantic/fact-extraction.ts +46 -0
  175. package/src/semantic/freshness.ts +67 -0
  176. package/src/{share/semantic → semantic}/ids.ts +6 -1
  177. package/src/{share/semantic → semantic}/index.ts +9 -2
  178. package/src/{share/semantic → semantic}/jsonld/breadcrumb.ts +6 -2
  179. package/src/{share/semantic → semantic}/jsonld/context.ts +6 -2
  180. package/src/{share/semantic → semantic}/jsonld/faq.ts +6 -2
  181. package/src/{share/semantic → semantic}/jsonld/initiative.ts +6 -1
  182. package/src/{share/semantic → semantic}/jsonld/organization.ts +6 -1
  183. package/src/{share/semantic → semantic}/jsonld/person.ts +6 -2
  184. package/src/{share/semantic → semantic}/jsonld/service.ts +6 -2
  185. package/src/semantic/jsonld/shared.ts +36 -0
  186. package/src/semantic/jsonld/types.ts +24 -0
  187. package/src/{share/semantic → semantic}/jsonld/webpage.ts +6 -1
  188. package/src/semantic/jsonld/website.ts +32 -0
  189. package/src/{share/semantic → semantic}/jsonld.ts +6 -1
  190. package/src/{share/semantic → semantic}/llms.ts +41 -3
  191. package/src/{share/semantic → semantic}/models.ts +26 -38
  192. package/src/{share/semantic → semantic}/organization-profile.ts +3 -0
  193. package/src/{share/semantic → semantic}/page-builders/markdown-page.ts +6 -3
  194. package/src/{share/semantic → semantic}/page-utils.ts +6 -3
  195. package/src/{share/tests/breadcrumbs.test.ts → semantic/tests/breadcrumbs-0229.test.ts} +1 -1
  196. package/src/{share → semantic}/tests/build-page-price-markers.test.ts +2 -2
  197. package/src/{share/semantic → semantic}/tests/business-projection.test.ts +179 -0
  198. package/src/semantic/tests/canonical-uri.test.ts +56 -0
  199. package/src/{share/semantic → semantic}/tests/extract.test.ts +83 -0
  200. package/src/semantic/tests/freshness.pbt.test.ts +108 -0
  201. package/src/{share/tests/image-sitemap.test.ts → semantic/tests/image-sitemap-0150.test.ts} +1 -1
  202. package/src/{share → semantic}/tests/jsonld-service.test.ts +2 -2
  203. package/src/{share → semantic}/tests/jsonld-video.test.ts +3 -3
  204. package/src/{share → semantic}/tests/jsonld-webpage.test.ts +3 -3
  205. package/src/{share → semantic}/tests/llms-0789.test.ts +2 -2
  206. package/src/{share/semantic → semantic}/tests/llms.test.ts +46 -0
  207. package/src/{share → semantic}/tests/organization-jsonld.test.ts +3 -3
  208. package/src/{share/tests/output-projection.test.ts → semantic/tests/output-projection-0143.test.ts} +1 -1
  209. package/src/{share/tests/price-marker-resolver.test.ts → semantic/tests/price-marker-resolver-0767.test.ts} +2 -2
  210. package/src/{share/semantic → semantic}/tests/price-marker-resolver.test.ts +4 -2
  211. package/src/semantic/tests/project-claims.pbt.test.ts +60 -0
  212. package/src/{share/semantic → semantic}/tests/split-sentences.test.ts +1 -1
  213. package/src/{share → semantic}/tests/twin-path.test.ts +1 -1
  214. package/src/signing/index.ts +13 -0
  215. package/src/signing/key.ts +139 -0
  216. package/src/signing/sign.ts +66 -0
  217. package/src/signing/types.ts +29 -0
  218. package/src/{share/slug → slug}/slug-url.ts +4 -1
  219. package/src/stack/run-tool.ts +108 -0
  220. package/src/stack/stack-checks.ts +124 -0
  221. package/src/stack/tests/run-tool.test.ts +138 -0
  222. package/src/stack/tests/stack-checks.test.ts +141 -0
  223. package/src/stack/tests/walk-files.test.ts +131 -0
  224. package/src/stack/walk-files.ts +114 -0
  225. package/src/surface/blueprint.ts +342 -66
  226. package/src/surface/decision-composer.ts +6 -1
  227. package/src/surface/geo.ts +6 -1
  228. package/src/surface/index.ts +11 -7
  229. package/src/surface/types.ts +2 -2
  230. package/src/surface/vitest.config.ts +1 -0
  231. package/src/tests/signing-core.test.ts +129 -0
  232. package/src/{share → text}/css-value-normalize.ts +6 -1
  233. package/src/text/string-utils.ts +24 -0
  234. package/src/{share → text}/tests/text-normalize.test.ts +5 -0
  235. package/src/{share → text}/text-normalize.ts +4 -3
  236. package/src/{share → text}/wrap-inline-numbers.ts +1 -1
  237. package/src/types/axiom-study.d.ts +36 -0
  238. package/src/types/hls-js-light.d.ts +21 -0
  239. package/src/types/qrcode-browser.d.ts +23 -0
  240. package/src/typography/fix.pbt.test.ts +208 -0
  241. package/src/typography/fix.ts +148 -0
  242. package/src/typography/index.ts +54 -0
  243. package/src/typography/locale-defaults.ts +97 -0
  244. package/src/typography/rules-tier1.test.ts +459 -0
  245. package/src/typography/rules-tier1.ts +925 -0
  246. package/src/typography/rules-tier2-locale.pbt.test.ts +190 -0
  247. package/src/typography/rules-tier2-locale.test.ts +286 -0
  248. package/src/typography/rules-tier2-locale.ts +399 -0
  249. package/src/typography/rules-tier2-structure.test.ts +330 -0
  250. package/src/typography/rules-tier2-structure.ts +378 -0
  251. package/src/typography/rules-tier3-advisory.test.ts +290 -0
  252. package/src/typography/rules-tier3-advisory.ts +239 -0
  253. package/src/typography/text-surface.test.ts +244 -0
  254. package/src/typography/text-surface.ts +448 -0
  255. package/src/index.ts +0 -14
  256. package/src/integration/port-barrel.ts +0 -105
  257. package/src/share/agent/tests/api-catalog.test.ts +0 -8
  258. package/src/share/agent/tests/ard-catalog.test.ts +0 -8
  259. package/src/share/agent/tests/capability.test.ts +0 -14
  260. package/src/share/agent/tests/fleet-catalog.test.ts +0 -15
  261. package/src/share/agent/tests/knowledge.test.ts +0 -20
  262. package/src/share/agent/tests/manifest.test.ts +0 -9
  263. package/src/share/agent/tests/mcp-card.test.ts +0 -8
  264. package/src/share/agent/tests/openapi.test.ts +0 -8
  265. package/src/share/agent/tests/proof.test.ts +0 -15
  266. package/src/share/content/index.ts +0 -16
  267. package/src/share/content-discipline/index.ts +0 -14
  268. package/src/share/index.ts +0 -86
  269. package/src/share/knowledge/tests/claim.test.ts +0 -11
  270. package/src/share/knowledge/tests/derivation.test.ts +0 -30
  271. package/src/share/knowledge/tests/freshness.test.ts +0 -18
  272. package/src/share/knowledge/tests/ledger.test.ts +0 -35
  273. package/src/share/knowledge/tests/plan.test.ts +0 -19
  274. package/src/share/knowledge/tests/source.test.ts +0 -19
  275. package/src/share/legal/index.ts +0 -1
  276. package/src/share/redirects.ts +0 -54
  277. package/src/share/remediation/remediation-catalog.ts +0 -196
  278. package/src/share/scripts/index.ts +0 -26
  279. package/src/share/scripts/tests/orchestrator.test.ts +0 -8
  280. package/src/share/semantic/jsonld/shared.ts +0 -31
  281. package/src/share/semantic/jsonld/types.ts +0 -19
  282. package/src/share/semantic/jsonld/website.ts +0 -27
  283. package/src/share/string-utils.ts +0 -19
  284. package/src/share/types/axiom-study.d.ts +0 -22
  285. package/src/share/types/hls-js-light.d.ts +0 -16
  286. package/src/share/types/qrcode-browser.d.ts +0 -18
  287. package/src/share/utility-registry.yaml +0 -50
  288. package/src/share/vitest.config.ts +0 -8
  289. package/src/surface/blueprint-schema.ts +0 -314
  290. package/src/surface/blueprint-types.ts +0 -256
  291. /package/src/{share/agent → agent}/api-catalog.ts +0 -0
  292. /package/src/{share/agent → agent}/capability.ts +0 -0
  293. /package/src/{share/agent → agent}/dns-aid.ts +0 -0
  294. /package/src/{share/agent → agent}/fleet-catalog.ts +0 -0
  295. /package/src/{share/agent → agent}/index.ts +0 -0
  296. /package/src/{share/agent → agent}/knowledge.ts +0 -0
  297. /package/src/{share/agent → agent}/mcp-card.ts +0 -0
  298. /package/src/{share/agent → agent}/omit-empty.ts +0 -0
  299. /package/src/{share/agent → agent}/openapi.ts +0 -0
  300. /package/src/{share/agent → agent}/proof.ts +0 -0
  301. /package/src/{share/agent → agent}/search.test.ts +0 -0
  302. /package/src/{share/agent → agent}/tests/omit-empty.test.ts +0 -0
  303. /package/src/{share → attribution}/attribution-display.ts +0 -0
  304. /package/src/{share/scripts → client-scripts}/gsap-mountain-journey.ts +0 -0
  305. /package/src/{share/scripts → client-scripts}/gsap-parallax.ts +0 -0
  306. /package/src/{share/scripts → client-scripts}/gsap-reveal.ts +0 -0
  307. /package/src/{share/scripts → client-scripts}/gsap-stagger.ts +0 -0
  308. /package/src/{share/scripts → client-scripts}/live-photos.ts +0 -0
  309. /package/src/{share/scripts → client-scripts}/tests/external-link-qr.test.ts +0 -0
  310. /package/src/{share/scripts → client-scripts}/tests/external-links.test.ts +0 -0
  311. /package/src/{share/scripts → client-scripts}/tests/gsap-counter.test.ts +0 -0
  312. /package/src/{share/scripts → client-scripts}/tests/gsap-mountain-journey.test.ts +0 -0
  313. /package/src/{share/scripts → client-scripts}/tests/gsap-parallax.test.ts +0 -0
  314. /package/src/{share/scripts → client-scripts}/tests/gsap-reveal.test.ts +0 -0
  315. /package/src/{share/scripts → client-scripts}/tests/gsap-stagger.test.ts +0 -0
  316. /package/src/{share/scripts → client-scripts}/tests/inline-number-animation.test.ts +0 -0
  317. /package/src/{share/scripts → client-scripts}/tests/lenis.test.ts +0 -0
  318. /package/src/{share/scripts → client-scripts}/tests/live-photos.test.ts +0 -0
  319. /package/src/{share/scripts → client-scripts}/tests/lordicon.test.ts +0 -0
  320. /package/src/{share/scripts → client-scripts}/tests/scheduler.test.ts +0 -0
  321. /package/src/{share/scripts → client-scripts}/video-player.ts +0 -0
  322. /package/src/{share/content → content}/resolve-field-path.ts +0 -0
  323. /package/src/{share/content → content}/substitute-deep.ts +0 -0
  324. /package/src/{share → content}/tests/content-ref-index.test.ts +0 -0
  325. /package/src/{share/content-discipline → content-discipline}/types.ts +0 -0
  326. /package/src/{share → image-provider}/image-provider.ts +0 -0
  327. /package/src/{share/knowledge → knowledge}/claim.ts +0 -0
  328. /package/src/{share/knowledge → knowledge}/derivation.ts +0 -0
  329. /package/src/{share/knowledge → knowledge}/freshness.ts +0 -0
  330. /package/src/{share/knowledge → knowledge}/index.ts +0 -0
  331. /package/src/{share/knowledge → knowledge}/ledger.ts +0 -0
  332. /package/src/{share/knowledge → knowledge}/plan.ts +0 -0
  333. /package/src/{share/knowledge → knowledge}/source.ts +0 -0
  334. /package/src/{share/legal → legal}/translation-policy.ts +0 -0
  335. /package/src/{share/middleware → middleware}/language-redirect.ts +0 -0
  336. /package/src/{share/middleware → middleware}/tests/access-protection.test.ts +0 -0
  337. /package/src/{share → node}/fs/index.ts +0 -0
  338. /package/src/{share/tests/markdown-twin-provenance.test.ts → node/tests/markdown-twin-provenance-0613.test.ts} +0 -0
  339. /package/src/{share → offers}/offer-capacity.ts +0 -0
  340. /package/src/{share/onboarding-yaml/index.ts → onboarding/yaml.ts} +0 -0
  341. /package/src/{share → ontology}/tests/ontology-role-by-cosmic-name.test.ts +0 -0
  342. /package/src/{share/routes → routes}/tests/template-filter.test.ts +0 -0
  343. /package/src/{share/schemas → schemas}/claim-records.ts +0 -0
  344. /package/src/{share/schemas → schemas}/horizontal-align.ts +0 -0
  345. /package/src/{share/schemas → schemas}/icon-color.ts +0 -0
  346. /package/src/{share/schemas → schemas}/live-photo.ts +0 -0
  347. /package/src/{share/schemas → schemas}/material-credit.ts +0 -0
  348. /package/src/{share/schemas → schemas}/media.ts +0 -0
  349. /package/src/{share/schemas → schemas}/participant.ts +0 -0
  350. /package/src/{share/schemas → schemas}/print.ts +0 -0
  351. /package/src/{share/schemas → schemas}/section-background.ts +0 -0
  352. /package/src/{share/schemas → schemas}/section-body.ts +0 -0
  353. /package/src/{share/schemas → schemas}/section-cards.ts +0 -0
  354. /package/src/{share/schemas → schemas}/section-cta.ts +0 -0
  355. /package/src/{share/schemas → schemas}/section-header.ts +0 -0
  356. /package/src/{share/schemas → schemas}/section-image.ts +0 -0
  357. /package/src/{share/schemas → schemas}/section-motion.ts +0 -0
  358. /package/src/{share/schemas → schemas}/section-shell.ts +0 -0
  359. /package/src/{share/schemas → schemas}/section-stats.ts +0 -0
  360. /package/src/{share/schemas → schemas}/site-background.ts +0 -0
  361. /package/src/{share/schemas → schemas}/tests/claim-records.test.ts +0 -0
  362. /package/src/{share/schemas → schemas}/tests/claims.test.ts +0 -0
  363. /package/src/{share/schemas → schemas}/tests/effects.test.ts +0 -0
  364. /package/src/{share/schemas → schemas}/tests/features.test.ts +0 -0
  365. /package/src/{share/schemas → schemas}/tests/horizontal-align.test.ts +0 -0
  366. /package/src/{share/schemas → schemas}/tests/icon-color.test.ts +0 -0
  367. /package/src/{share/schemas → schemas}/tests/live-photo.test.ts +0 -0
  368. /package/src/{share/schemas → schemas}/tests/material-credit.test.ts +0 -0
  369. /package/src/{share/schemas → schemas}/tests/media.test.ts +0 -0
  370. /package/src/{share/schemas → schemas}/tests/navigation.test.ts +0 -0
  371. /package/src/{share/schemas → schemas}/tests/page-base.test.ts +0 -0
  372. /package/src/{share/schemas → schemas}/tests/participant.test.ts +0 -0
  373. /package/src/{share/schemas → schemas}/tests/person.test.ts +0 -0
  374. /package/src/{share/schemas → schemas}/tests/print.test.ts +0 -0
  375. /package/src/{share/schemas → schemas}/tests/section-background.test.ts +0 -0
  376. /package/src/{share/schemas → schemas}/tests/section-body.test.ts +0 -0
  377. /package/src/{share/schemas → schemas}/tests/section-cards.test.ts +0 -0
  378. /package/src/{share/schemas → schemas}/tests/section-cta.test.ts +0 -0
  379. /package/src/{share/schemas → schemas}/tests/section-header.test.ts +0 -0
  380. /package/src/{share/schemas → schemas}/tests/section-image.test.ts +0 -0
  381. /package/src/{share/schemas → schemas}/tests/section-motion.test.ts +0 -0
  382. /package/src/{share/schemas → schemas}/tests/section-shell.test.ts +0 -0
  383. /package/src/{share/schemas → schemas}/tests/section-stats.test.ts +0 -0
  384. /package/src/{share/schemas → schemas}/tests/site-background.test.ts +0 -0
  385. /package/src/{share/schemas → schemas}/tests/standard-list-item.test.ts +0 -0
  386. /package/src/{share/semantic → semantic}/ai.ts +0 -0
  387. /package/src/{share/semantic → semantic}/block-extraction.ts +0 -0
  388. /package/src/{share/semantic → semantic}/breadcrumbs.ts +0 -0
  389. /package/src/{share/semantic → semantic}/build-page.ts +0 -0
  390. /package/src/{share/semantic → semantic}/feed.ts +0 -0
  391. /package/src/{share/semantic → semantic}/image-sitemap.ts +0 -0
  392. /package/src/{share/semantic → semantic}/jsonld/article.ts +0 -0
  393. /package/src/{share/semantic → semantic}/jsonld/collection-list.ts +0 -0
  394. /package/src/{share/semantic → semantic}/jsonld/tests/article.test.ts +0 -0
  395. /package/src/{share/semantic → semantic}/jsonld/tests/breadcrumb.test.ts +0 -0
  396. /package/src/{share/semantic → semantic}/jsonld/tests/collection-list.test.ts +0 -0
  397. /package/src/{share/semantic → semantic}/jsonld/tests/faq.test.ts +0 -0
  398. /package/src/{share/semantic → semantic}/jsonld/tests/initiative.test.ts +0 -0
  399. /package/src/{share/semantic → semantic}/jsonld/tests/organization.test.ts +0 -0
  400. /package/src/{share/semantic → semantic}/jsonld/tests/person.test.ts +0 -0
  401. /package/src/{share/semantic → semantic}/jsonld/tests/service.test.ts +0 -0
  402. /package/src/{share/semantic → semantic}/jsonld/tests/shared.test.ts +0 -0
  403. /package/src/{share/semantic → semantic}/jsonld/tests/video.test.ts +0 -0
  404. /package/src/{share/semantic → semantic}/jsonld/tests/webpage.test.ts +0 -0
  405. /package/src/{share/semantic → semantic}/jsonld/tests/website.test.ts +0 -0
  406. /package/src/{share/semantic → semantic}/jsonld/video.ts +0 -0
  407. /package/src/{share/semantic → semantic}/llms-policy.ts +0 -0
  408. /package/src/{share/semantic → semantic}/markdown-hygiene.ts +0 -0
  409. /package/src/{share/semantic → semantic}/output-projection.ts +0 -0
  410. /package/src/{share/semantic → semantic}/page-markdown.ts +0 -0
  411. /package/src/{share/semantic → semantic}/price-marker-resolver.ts +0 -0
  412. /package/src/{share/semantic → semantic}/robots.ts +0 -0
  413. /package/src/{share/semantic → semantic}/tests/ai.test.ts +0 -0
  414. /package/src/{share/semantic → semantic}/tests/block-extraction.test.ts +0 -0
  415. /package/src/{share/semantic → semantic}/tests/breadcrumbs.test.ts +0 -0
  416. /package/src/{share/semantic → semantic}/tests/build-page.test.ts +0 -0
  417. /package/src/{share/semantic → semantic}/tests/feed.test.ts +0 -0
  418. /package/src/{share/semantic → semantic}/tests/helpers.ts +0 -0
  419. /package/src/{share/semantic → semantic}/tests/ids.test.ts +0 -0
  420. /package/src/{share/semantic → semantic}/tests/image-sitemap.test.ts +0 -0
  421. /package/src/{share/semantic → semantic}/tests/jsonld.test.ts +0 -0
  422. /package/src/{share/semantic → semantic}/tests/llms-policy.test.ts +0 -0
  423. /package/src/{share/semantic → semantic}/tests/markdown-hygiene.test.ts +0 -0
  424. /package/src/{share/semantic → semantic}/tests/markdown-page.test.ts +0 -0
  425. /package/src/{share/semantic → semantic}/tests/organization-profile.test.ts +0 -0
  426. /package/src/{share/semantic → semantic}/tests/output-projection.test.ts +0 -0
  427. /package/src/{share/semantic → semantic}/tests/page-markdown.test.ts +0 -0
  428. /package/src/{share/semantic → semantic}/tests/page-utils.test.ts +0 -0
  429. /package/src/{share/semantic → semantic}/tests/robots.test.ts +0 -0
  430. /package/src/{share/semantic → semantic}/tests/update-stamp.test.ts +0 -0
  431. /package/src/{share/semantic → semantic}/update-stamp.ts +0 -0
  432. /package/src/{share/slug → slug}/heading-slugger.ts +0 -0
  433. /package/src/{share/slug → slug}/index.ts +0 -0
  434. /package/src/{share/slug → slug}/slug-id.ts +0 -0
  435. /package/src/{share/slug → slug}/strategies.ts +0 -0
  436. /package/src/{share/slug → slug}/tests/slug.test.ts +0 -0
  437. /package/src/{share → text}/tests/text-normalize.pbt.test.ts +0 -0
  438. /package/src/{share → text}/text-position.ts +0 -0
@@ -0,0 +1,244 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for the text-surface extractor (RFC-1068).</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-1068: initial creation.</item>
7
+ </CHANGE_SUMMARY>
8
+ */
9
+
10
+ import { describe, it, expect } from "vitest";
11
+ import { extractTextSurface, deriveFileLocale } from "./text-surface.ts";
12
+
13
+ const LANGS = ["de", "uk", "en"];
14
+
15
+ describe("deriveFileLocale", () => {
16
+ it("derives locale from content path", () => {
17
+ expect(deriveFileLocale("src/content/pages/de/index.md", LANGS, "de")).toBe("de");
18
+ expect(deriveFileLocale("src/content/pages/uk/index.md", LANGS, "de")).toBe("uk");
19
+ expect(deriveFileLocale("src/content/pages/en/index.md", LANGS, "de")).toBe("en");
20
+ });
21
+
22
+ it("derives locale from mission workpiece content path", () => {
23
+ expect(
24
+ deriveFileLocale("missions/wg-m001/workpiece/src/content/pages/de/index.md", LANGS, "de"),
25
+ ).toBe("de");
26
+ expect(
27
+ deriveFileLocale(
28
+ "missions/wg-m001/workpiece/src/content/business-profile/uk/offerings/x.md",
29
+ LANGS,
30
+ "de",
31
+ ),
32
+ ).toBe("uk");
33
+ });
34
+
35
+ it("falls back to default language for unknown locale", () => {
36
+ expect(deriveFileLocale("src/content/pages/fr/index.md", LANGS, "de")).toBe("de");
37
+ });
38
+
39
+ it("falls back to default language for non-content paths", () => {
40
+ expect(deriveFileLocale("src/pages/index.astro", LANGS, "de")).toBe("de");
41
+ });
42
+ });
43
+
44
+ describe("extractTextSurface", () => {
45
+ it("extracts frontmatter strings", () => {
46
+ const source = `---
47
+ title: Hello World
48
+ description: A test page
49
+ ---
50
+ Body text here.
51
+ `;
52
+ const result = extractTextSurface("src/content/pages/de/test.md", source, {
53
+ languages: LANGS,
54
+ defaultLanguage: "de",
55
+ });
56
+
57
+ expect(result.yamlError).toBeNull();
58
+ const fmSegments = result.segments.filter((s) => s.source === "frontmatter");
59
+ expect(fmSegments.length).toBeGreaterThanOrEqual(2);
60
+ expect(fmSegments.some((s) => s.text.includes("Hello World"))).toBe(true);
61
+ expect(fmSegments.some((s) => s.text.includes("A test page"))).toBe(true);
62
+ });
63
+
64
+ it("extracts body lines", () => {
65
+ const source = `---
66
+ title: Test
67
+ ---
68
+ This is a body paragraph.
69
+ Second line of text.
70
+ `;
71
+ const result = extractTextSurface("src/content/pages/de/test.md", source, {
72
+ languages: LANGS,
73
+ defaultLanguage: "de",
74
+ });
75
+
76
+ const bodySegments = result.segments.filter((s) => s.source === "body");
77
+ expect(bodySegments.length).toBe(2);
78
+ expect(bodySegments[0].text).toContain("This is a body paragraph");
79
+ expect(bodySegments[1].text).toContain("Second line of text");
80
+ });
81
+
82
+ it("skips fenced code blocks", () => {
83
+ const source = `---
84
+ title: Test
85
+ ---
86
+ Normal text.
87
+ \`\`\`js
88
+ const x = "code block";
89
+ \`\`\`
90
+ After code.
91
+ `;
92
+ const result = extractTextSurface("src/content/pages/de/test.md", source, {
93
+ languages: LANGS,
94
+ defaultLanguage: "de",
95
+ });
96
+
97
+ const bodySegments = result.segments.filter((s) => s.source === "body");
98
+ expect(bodySegments.length).toBe(2);
99
+ expect(bodySegments.some((s) => s.text.includes("Normal text"))).toBe(true);
100
+ expect(bodySegments.some((s) => s.text.includes("After code"))).toBe(true);
101
+ expect(bodySegments.some((s) => s.text.includes("code block"))).toBe(false);
102
+ });
103
+
104
+ it("skips technical keys", () => {
105
+ const source = `---
106
+ title: Test
107
+ slug: my-page
108
+ id: abc123
109
+ href: https://example.com
110
+ description: Real text
111
+ ---
112
+ Body.
113
+ `;
114
+ const result = extractTextSurface("src/content/pages/de/test.md", source, {
115
+ languages: LANGS,
116
+ defaultLanguage: "de",
117
+ });
118
+
119
+ const fmSegments = result.segments.filter((s) => s.source === "frontmatter");
120
+ expect(fmSegments.some((s) => s.text.includes("Real text"))).toBe(true);
121
+ expect(fmSegments.some((s) => s.text.includes("abc123"))).toBe(false);
122
+ expect(fmSegments.some((s) => s.text.includes("my-page"))).toBe(false);
123
+ });
124
+
125
+ it("strips inline code from body lines", () => {
126
+ const source = `---
127
+ title: Test
128
+ ---
129
+ Use the \`npm install\` command.
130
+ `;
131
+ const result = extractTextSurface("src/content/pages/de/test.md", source, {
132
+ languages: LANGS,
133
+ defaultLanguage: "de",
134
+ });
135
+
136
+ const bodySegments = result.segments.filter((s) => s.source === "body");
137
+ expect(bodySegments.length).toBe(1);
138
+ expect(bodySegments[0].text).toContain("Use the");
139
+ expect(bodySegments[0].text).toContain("command");
140
+ expect(bodySegments[0].text).not.toContain("npm install");
141
+ });
142
+
143
+ it("strips HTML comments from body lines", () => {
144
+ const source = `---
145
+ title: Test
146
+ ---
147
+ Text <!-- comment --> here.
148
+ `;
149
+ const result = extractTextSurface("src/content/pages/de/test.md", source, {
150
+ languages: LANGS,
151
+ defaultLanguage: "de",
152
+ });
153
+
154
+ const bodySegments = result.segments.filter((s) => s.source === "body");
155
+ expect(bodySegments[0].text).not.toContain("comment");
156
+ expect(bodySegments[0].text).toContain("Text");
157
+ expect(bodySegments[0].text).toContain("here");
158
+ });
159
+
160
+ it("strips URLs from body lines", () => {
161
+ const source = `---
162
+ title: Test
163
+ ---
164
+ Visit https://example.com for more.
165
+ `;
166
+ const result = extractTextSurface("src/content/pages/de/test.md", source, {
167
+ languages: LANGS,
168
+ defaultLanguage: "de",
169
+ });
170
+
171
+ const bodySegments = result.segments.filter((s) => s.source === "body");
172
+ expect(bodySegments[0].text).not.toContain("https://example.com");
173
+ expect(bodySegments[0].text).toContain("Visit");
174
+ expect(bodySegments[0].text).toContain("for more");
175
+ });
176
+
177
+ it("detects YAML parse errors", () => {
178
+ const source = `---
179
+ title: Test
180
+ foo: [unclosed
181
+ ---
182
+ Body.
183
+ `;
184
+ const result = extractTextSurface("src/content/pages/de/test.md", source, {
185
+ languages: LANGS,
186
+ defaultLanguage: "de",
187
+ });
188
+
189
+ expect(result.yamlError).not.toBeNull();
190
+ expect(result.yamlError!.message).toBeTruthy();
191
+ });
192
+
193
+ it("marks table rows correctly", () => {
194
+ const source = `---
195
+ title: Test
196
+ ---
197
+ | Col1 | Col2 |
198
+ |------|------|
199
+ | a | b |
200
+ Normal text.
201
+ `;
202
+ const result = extractTextSurface("src/content/pages/de/test.md", source, {
203
+ languages: LANGS,
204
+ defaultLanguage: "de",
205
+ });
206
+
207
+ const bodySegments = result.segments.filter((s) => s.source === "body");
208
+ const tableRows = bodySegments.filter((s) => s.isTableRow);
209
+ const normalLines = bodySegments.filter((s) => !s.isTableRow);
210
+ expect(tableRows.length).toBe(3);
211
+ expect(normalLines.length).toBe(1);
212
+ expect(normalLines[0].text).toContain("Normal text");
213
+ });
214
+
215
+ it("handles files without frontmatter", () => {
216
+ const source = "Just body text.\nSecond line.\n";
217
+ const result = extractTextSurface("src/content/pages/de/test.md", source, {
218
+ languages: LANGS,
219
+ defaultLanguage: "de",
220
+ });
221
+
222
+ expect(result.yamlError).toBeNull();
223
+ const bodySegments = result.segments.filter((s) => s.source === "body");
224
+ expect(bodySegments.length).toBe(2);
225
+ });
226
+
227
+ it("skips identifier-like values in frontmatter", () => {
228
+ const source = `---
229
+ title: Test
230
+ path: src/foo/bar.ts
231
+ description: Real text
232
+ ---
233
+ Body.
234
+ `;
235
+ const result = extractTextSurface("src/content/pages/de/test.md", source, {
236
+ languages: LANGS,
237
+ defaultLanguage: "de",
238
+ });
239
+
240
+ const fmSegments = result.segments.filter((s) => s.source === "frontmatter");
241
+ expect(fmSegments.some((s) => s.text.includes("Real text"))).toBe(true);
242
+ expect(fmSegments.some((s) => s.text.includes("src/foo/bar.ts"))).toBe(false);
243
+ });
244
+ });
@@ -0,0 +1,448 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Text-surface extractor for the typography rule engine (RFC-1068).
4
+ Extracts scannable text segments from Markdown frontmatter strings and body
5
+ lines, applying a normative stripping order so rules see clean text without
6
+ code blocks, HTML tags, formulas, or URLs. Also catches YAML frontmatter
7
+ parse errors and maps them to a structured yamlError field.</purpose>
8
+ <non-goals>
9
+ <item>Do not implement typography rules — that is rules-tier1.ts.</item>
10
+ <item>Do not read files or perform I/O — the caller passes source text.</item>
11
+ <item>Do not validate content semantics — only extracts text for rule scanning.</item>
12
+ </non-goals>
13
+ </MODULE_CONTRACT>
14
+ <CHANGE_SUMMARY>
15
+ <item>RFC-1068: initial creation.</item>
16
+ </CHANGE_SUMMARY>
17
+ */
18
+
19
+ import YAML from "yaml";
20
+ import { parseMarkdownFrontmatter } from "../content/markdown-frontmatter.ts";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Types
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export type TextSegmentSource = "frontmatter" | "body";
27
+
28
+ export interface TextSegment {
29
+ /** Repo-relative file path. */
30
+ file: string;
31
+ /** 1-based file line of the segment start (frontmatter offset already applied). */
32
+ line: number;
33
+ /** JSON-path for frontmatter strings (`$.blocks[2].props.items[0].text`), `$body` for body lines. */
34
+ path: string;
35
+ source: TextSegmentSource;
36
+ /** Segment text after stripping; this is what rules see. */
37
+ text: string;
38
+ /** Original text before stripping; used for fix (RFC-1072) and messages. */
39
+ raw: string;
40
+ /** True when the segment is a markdown table row (some rules skip these). */
41
+ isTableRow: boolean;
42
+ locale: string;
43
+ }
44
+
45
+ export interface TextSurfaceOptions {
46
+ /** Keys whose values are never text (default: TECHNICAL_KEYS). */
47
+ skipKeys?: ReadonlySet<string>;
48
+ /** Site i18n languages; used to derive file locale. */
49
+ languages: readonly string[];
50
+ defaultLanguage: string;
51
+ }
52
+
53
+ export interface YamlError {
54
+ line: number;
55
+ column: number;
56
+ message: string;
57
+ }
58
+
59
+ export interface ExtractTextSurfaceResult {
60
+ segments: TextSegment[];
61
+ yamlError: YamlError | null;
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Technical keys — values under these keys are never treated as text
66
+ // ---------------------------------------------------------------------------
67
+
68
+ export const TECHNICAL_KEYS = new Set([
69
+ "id",
70
+ "slug",
71
+ "href",
72
+ "url",
73
+ "src",
74
+ "path",
75
+ "type",
76
+ "use",
77
+ "kind",
78
+ "variant",
79
+ "layout",
80
+ "anchorId",
81
+ "blockId",
82
+ "ref",
83
+ "key",
84
+ "vendor",
85
+ "collection",
86
+ "lang",
87
+ "locale",
88
+ "currency",
89
+ "email",
90
+ "phone",
91
+ "proseId",
92
+ ]);
93
+
94
+ // When the parent key is "icon", the child "name" is technical
95
+ const TECHNICAL_PARENT_CHILD: Record<string, Set<string>> = {
96
+ icon: new Set(["name"]),
97
+ };
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Identifier heuristic — values without whitespace that contain /, _, or .
101
+ // and no letter-space-letter sequence are treated as identifiers and skipped
102
+ // ---------------------------------------------------------------------------
103
+
104
+ const IDENTIFIER_REGEX = /^[^\s]+$/;
105
+ const HAS_SLASH_OR_UNDERSCORE_OR_DOT = /[/_\.]/;
106
+ const LETTER_SPACE_LETTER = /\p{L}\s\p{L}/u;
107
+
108
+ function isIdentifierValue(value: string): boolean {
109
+ if (!IDENTIFIER_REGEX.test(value)) return false;
110
+ if (!HAS_SLASH_OR_UNDERSCORE_OR_DOT.test(value)) return false;
111
+ if (LETTER_SPACE_LETTER.test(value)) return false;
112
+ return true;
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // File locale derivation
117
+ // ---------------------------------------------------------------------------
118
+
119
+ export function deriveFileLocale(
120
+ file: string,
121
+ languages: readonly string[],
122
+ defaultLanguage: string,
123
+ ): string {
124
+ // file is repo-relative: may be src/content/<collection>/<lang>/... or
125
+ // missions/<id>/workpiece/src/content/<collection>/<lang>/...
126
+ const parts = file.split("/");
127
+ const srcIdx = parts.indexOf("src");
128
+ if (srcIdx >= 0 && srcIdx + 3 < parts.length && parts[srcIdx + 1] === "content") {
129
+ const lang = parts[srcIdx + 3];
130
+ if (languages.includes(lang)) return lang;
131
+ }
132
+ return defaultLanguage;
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Frontmatter string extraction
137
+ // ---------------------------------------------------------------------------
138
+
139
+ interface FrontmatterStringEntry {
140
+ path: string;
141
+ value: string;
142
+ /** 1-based line within the frontmatter block (relative to frontmatter start). */
143
+ lineInFrontmatter: number;
144
+ }
145
+
146
+ function extractFrontmatterStrings(
147
+ data: unknown,
148
+ parentKey: string | null,
149
+ skipKeys: ReadonlySet<string>,
150
+ lineCounter: YAML.LineCounter | null,
151
+ frontmatterText: string,
152
+ ): FrontmatterStringEntry[] {
153
+ const results: FrontmatterStringEntry[] = [];
154
+
155
+ function walk(value: unknown, currentPath: string, parent: string | null): void {
156
+ if (typeof value === "string") {
157
+ // Check if this key is technical
158
+ const lastKey = currentPath.split(".").pop() ?? currentPath;
159
+ const isTechnical =
160
+ skipKeys.has(lastKey) ||
161
+ (parent !== null && TECHNICAL_PARENT_CHILD[parent]?.has(lastKey) === true);
162
+ if (isTechnical) return;
163
+ if (isIdentifierValue(value)) return;
164
+
165
+ // Derive line from lineCounter if available
166
+ let lineInFrontmatter = 0;
167
+ if (lineCounter) {
168
+ const range = lineCounter.lineStarts;
169
+ // Find the position of this value in the source — we approximate by
170
+ // searching for the value in the frontmatter text
171
+ const valueIndex = frontmatterText.indexOf(value);
172
+ if (valueIndex >= 0) {
173
+ lineInFrontmatter = range.findIndex((start, i) => {
174
+ const nextStart = range[i + 1] ?? frontmatterText.length + 1;
175
+ return valueIndex >= start && valueIndex < nextStart;
176
+ });
177
+ if (lineInFrontmatter < 0) lineInFrontmatter = 0;
178
+ lineInFrontmatter += 1; // 1-based
179
+ }
180
+ }
181
+
182
+ results.push({ path: currentPath, value, lineInFrontmatter });
183
+ return;
184
+ }
185
+
186
+ if (Array.isArray(value)) {
187
+ for (let i = 0; i < value.length; i += 1) {
188
+ walk(value[i], `${currentPath}[${i}]`, parent);
189
+ }
190
+ return;
191
+ }
192
+
193
+ if (value !== null && typeof value === "object") {
194
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
195
+ const newParent = key;
196
+ // Check if this key itself is technical (skip entire subtree)
197
+ if (skipKeys.has(key)) continue;
198
+ if (parent !== null && TECHNICAL_PARENT_CHILD[parent]?.has(key) === true) continue;
199
+ walk(child, currentPath === "$" ? `$.${key}` : `${currentPath}.${key}`, newParent);
200
+ }
201
+ return;
202
+ }
203
+ }
204
+
205
+ walk(data, "$", parentKey);
206
+ return results;
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Body text extraction with normative stripping order
211
+ // ---------------------------------------------------------------------------
212
+
213
+ // (1) Inline code spans
214
+ const INLINE_CODE = /`[^`]*`/g;
215
+ // (1b) Markdown autolinks <URL> — must be handled before HTML tag stripping
216
+ const AUTOLINK = /<(https?:\/\/[^\s>]+)>/gi;
217
+ // (2) HTML comments
218
+ const HTML_COMMENT = /<!--[\s\S]*?-->/g;
219
+ // (3) Void HTML elements
220
+ const VOID_HTML =
221
+ /<(?:br|hr|img|input|meta|link|source|area|base|col|embed|param|track|wbr)\b[^>]*\/?>/giu;
222
+ // (3) Non-void HTML elements — strip tags, keep text
223
+ const NON_VOID_HTML = /<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*\/?>/gu;
224
+ const VOID_ELEMENTS = new Set([
225
+ "br",
226
+ "hr",
227
+ "img",
228
+ "input",
229
+ "meta",
230
+ "link",
231
+ "source",
232
+ "area",
233
+ "base",
234
+ "col",
235
+ "embed",
236
+ "param",
237
+ "track",
238
+ "wbr",
239
+ ]);
240
+ // (4) Formula expressions =(…)
241
+ const FORMULA = /=\([^)]*\)/g;
242
+ // (4b) CMS template expression placeholder (same as rules-tier1.ts CMS_PLACEHOLDER)
243
+ const CMS_PLACEHOLDER = "\uE002";
244
+ // (5) Markdown link/image targets — keep link text, remove ](…)
245
+ const LINK_TARGET = /\]\([^)]*\)/g;
246
+ // (6) URL, e-mail, domain tokens
247
+ const URL_PATTERN = /https?:\/\/[^\s)]+/gi;
248
+ const EMAIL_PATTERN = /[\w.+-]+@[\w.-]+\.\w{2,}/gi;
249
+ const DOMAIN_PATTERN = /\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}\b/gi;
250
+
251
+ function stripBodyLine(line: string): string {
252
+ let result = line;
253
+
254
+ // (1) Remove fenced code blocks (they span multiple lines, but for per-line
255
+ // processing we rely on the caller to skip fenced blocks entirely)
256
+ result = result.replace(INLINE_CODE, CMS_PLACEHOLDER);
257
+
258
+ // (1b) Replace markdown autolinks <URL> with URL placeholder
259
+ result = result.replace(AUTOLINK, "\uE001");
260
+
261
+ // (2) Remove HTML comments
262
+ result = result.replace(HTML_COMMENT, "");
263
+
264
+ // (3) Strip inline HTML tags
265
+ result = result.replace(VOID_HTML, "");
266
+ result = result.replace(NON_VOID_HTML, (match, tagName) => {
267
+ if (VOID_ELEMENTS.has(String(tagName).toLowerCase())) return "";
268
+ // Non-void: keep text content (the match is just the tag, not content)
269
+ return "";
270
+ });
271
+
272
+ // (4) Replace formulas with placeholder
273
+ result = result.replace(FORMULA, "\uE000");
274
+
275
+ // (4b) Replace CMS template expressions ({price:...}, {t:...}, etc.) with placeholder
276
+ result = result.replace(/\{[^}]*\}/g, CMS_PLACEHOLDER);
277
+
278
+ // (4c) Replace CMS template expressions (=(path.to.value), =(path/to/value)) with placeholder
279
+ result = result.replace(/=\([^)]*\)/g, "\uE002");
280
+
281
+ // (5) Remove markdown link targets, keep link text
282
+ result = result.replace(LINK_TARGET, "");
283
+
284
+ // (6) Replace URLs, e-mails, domains with placeholder
285
+ result = result.replace(URL_PATTERN, "\uE001");
286
+ result = result.replace(EMAIL_PATTERN, "\uE001");
287
+ result = result.replace(DOMAIN_PATTERN, "\uE001");
288
+
289
+ return result;
290
+ }
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // Main extractor
294
+ // ---------------------------------------------------------------------------
295
+
296
+ export function extractTextSurface(
297
+ file: string,
298
+ source: string,
299
+ options: TextSurfaceOptions,
300
+ ): ExtractTextSurfaceResult {
301
+ const skipKeys = options.skipKeys ?? TECHNICAL_KEYS;
302
+ const locale = deriveFileLocale(file, options.languages, options.defaultLanguage);
303
+
304
+ // Parse frontmatter — catch YAML errors
305
+ // Strip BOM if present so the frontmatter regex matches
306
+ const bomStripped = source.replace(/^\uFEFF/, "");
307
+ const frontmatterMatch = bomStripped.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
308
+ const frontmatterText = frontmatterMatch?.[1] ?? "";
309
+ const frontmatterLineCount = frontmatterMatch ? frontmatterMatch[0].split(/\r?\n/).length - 1 : 0;
310
+
311
+ let data: Record<string, unknown> = {};
312
+ let yamlError: YamlError | null = null;
313
+
314
+ try {
315
+ const parsed = parseMarkdownFrontmatter(source);
316
+ data = parsed.data;
317
+ } catch (err) {
318
+ // YAML parse error — extract line/column from YAMLParseError
319
+ if (err instanceof YAML.YAMLParseError) {
320
+ const linePos = (err as unknown as { linePos?: Array<{ line: number; col: number }> })
321
+ .linePos;
322
+ const line = linePos?.[0]?.line ?? 0;
323
+ const column = linePos?.[0]?.col ?? 0;
324
+ yamlError = { line, column, message: err.message };
325
+ } else {
326
+ yamlError = { line: 0, column: 0, message: err instanceof Error ? err.message : String(err) };
327
+ }
328
+ }
329
+
330
+ const segments: TextSegment[] = [];
331
+
332
+ // Extract frontmatter strings
333
+ if (Object.keys(data).length > 0 && !yamlError) {
334
+ let lineCounter: YAML.LineCounter | null = null;
335
+ try {
336
+ lineCounter = new YAML.LineCounter();
337
+ YAML.parseDocument(frontmatterText, { lineCounter });
338
+ } catch {
339
+ lineCounter = null;
340
+ }
341
+
342
+ const fmStrings = extractFrontmatterStrings(data, null, skipKeys, lineCounter, frontmatterText);
343
+
344
+ for (const entry of fmStrings) {
345
+ const stripped = stripBodyLine(entry.value);
346
+ if (stripped.trim().length === 0) continue;
347
+ segments.push({
348
+ file,
349
+ line: entry.lineInFrontmatter + 1, // +1 because frontmatter starts at line 2 (after ---)
350
+ path: entry.path,
351
+ source: "frontmatter",
352
+ text: stripped,
353
+ raw: entry.value,
354
+ isTableRow: false,
355
+ locale,
356
+ });
357
+ }
358
+ }
359
+
360
+ // Extract body lines
361
+ const body = frontmatterMatch ? bomStripped.slice(frontmatterMatch[0].length) : bomStripped;
362
+ const bodyLines = body.split(/\r?\n/);
363
+ const bodyLineOffset = frontmatterLineCount;
364
+
365
+ let inFencedBlock = false;
366
+ let inHtmlComment = false;
367
+
368
+ for (let i = 0; i < bodyLines.length; i += 1) {
369
+ const rawLine = bodyLines[i];
370
+ const lineNum = i + 1 + bodyLineOffset;
371
+
372
+ // Track fenced code blocks
373
+ if (/^```/.test(rawLine.trim())) {
374
+ inFencedBlock = !inFencedBlock;
375
+ continue;
376
+ }
377
+ if (inFencedBlock) continue;
378
+
379
+ // Track multi-line HTML comments
380
+ if (inHtmlComment) {
381
+ if (rawLine.includes("-->")) {
382
+ inHtmlComment = false;
383
+ // Strip everything up to and including -->, keep the rest
384
+ const after = rawLine.slice(rawLine.indexOf("-->") + 3);
385
+ if (after.trim().length === 0) continue;
386
+ const strippedAfter = stripBodyLine(after);
387
+ if (strippedAfter.trim().length === 0) continue;
388
+ segments.push({
389
+ file,
390
+ line: lineNum,
391
+ path: "$body",
392
+ source: "body",
393
+ text: strippedAfter,
394
+ raw: after,
395
+ isTableRow: /^\|.*\|/.test(after.trim()),
396
+ locale,
397
+ });
398
+ }
399
+ continue;
400
+ }
401
+ // Check if line opens a multi-line HTML comment (<!-- without --> on same line)
402
+ const openIdx = rawLine.indexOf("<!--");
403
+ if (openIdx !== -1) {
404
+ const closeIdx = rawLine.indexOf("-->", openIdx);
405
+ if (closeIdx === -1) {
406
+ // Comment opens but doesn't close on this line — keep content before <!--
407
+ const before = rawLine.slice(0, openIdx);
408
+ inHtmlComment = true;
409
+ if (before.trim().length === 0) continue;
410
+ const strippedBefore = stripBodyLine(before);
411
+ if (strippedBefore.trim().length === 0) continue;
412
+ segments.push({
413
+ file,
414
+ line: lineNum,
415
+ path: "$body",
416
+ source: "body",
417
+ text: strippedBefore,
418
+ raw: before,
419
+ isTableRow: /^\|.*\|/.test(before.trim()),
420
+ locale,
421
+ });
422
+ continue;
423
+ }
424
+ // Both on same line — stripBodyLine handles it via regex
425
+ }
426
+
427
+ // Skip empty lines
428
+ if (rawLine.trim().length === 0) continue;
429
+
430
+ const isTableRow = /^\|.*\|/.test(rawLine.trim());
431
+ const stripped = stripBodyLine(rawLine);
432
+
433
+ if (stripped.trim().length === 0) continue;
434
+
435
+ segments.push({
436
+ file,
437
+ line: lineNum,
438
+ path: "$body",
439
+ source: "body",
440
+ text: stripped,
441
+ raw: rawLine,
442
+ isTableRow,
443
+ locale,
444
+ });
445
+ }
446
+
447
+ return { segments, yamlError };
448
+ }
package/src/index.ts DELETED
@@ -1,14 +0,0 @@
1
- /*
2
- <MODULE_CONTRACT>
3
- <purpose>Package barrel for @warpgogol/werkstatt-shared — stack-agnostic shared infrastructure extracted from werkstatt-site (RFC-0868).</purpose>
4
- <non-goals>
5
- <item>Do not export site-specific validators, Astro components, or stack plugin logic.</item>
6
- <item>Do not import from @warpgogol/werkstatt-site or any stack plugin.</item>
7
- </non-goals>
8
- </MODULE_CONTRACT>
9
- <CHANGE_SUMMARY>
10
- <item>RFC-0868: initial extraction from werkstatt-site.</item>
11
- </CHANGE_SUMMARY>
12
- */
13
-
14
- export * from "./checks/index.ts";