@warpgogol/werkstatt-shared 0.14.0 → 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 (437) hide show
  1. package/README.md +31 -13
  2. package/README.uk.md +25 -15
  3. package/package.json +519 -762
  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/agent/tests/omit-empty.test.ts +33 -0
  16. package/src/{share → agent}/tests/openapi.test.ts +2 -2
  17. package/src/{share → agent}/tests/proof.test.ts +1 -1
  18. package/src/{share → attribution}/material-credits.ts +4 -4
  19. package/src/{share → checks}/check-hints.ts +6 -1
  20. package/src/checks/index.ts +4 -1
  21. package/src/checks/lib/astro-site-url.ts +6 -1
  22. package/src/checks/lib/i18n.ts +14 -0
  23. package/src/checks/result-helpers.ts +22 -5
  24. package/src/checks/tests/remediation-auto-populate.test.ts +83 -0
  25. package/src/{share → client-scripts}/counter-utils.ts +6 -2
  26. package/src/{share/scripts → client-scripts}/external-link-qr.ts +11 -16
  27. package/src/{share/scripts → client-scripts}/external-links.ts +12 -1
  28. package/src/{share/scripts → client-scripts}/gsap-counter.ts +8 -3
  29. package/src/client-scripts/index.ts +35 -0
  30. package/src/{share/scripts → client-scripts}/inline-number-animation.ts +1 -1
  31. package/src/{share/scripts → client-scripts}/lenis.ts +6 -1
  32. package/src/{share/scripts → client-scripts}/lordicon.ts +6 -1
  33. package/src/{share/scripts → client-scripts}/orchestrator.ts +10 -1
  34. package/src/{share/scripts → client-scripts}/scheduler.ts +6 -1
  35. package/src/client-scripts/scroll-spy.ts +106 -0
  36. package/src/client-scripts/tests/external-link-qr.test.ts +8 -0
  37. package/src/client-scripts/tests/external-links.test.ts +25 -0
  38. package/src/client-scripts/tests/gsap-counter.test.ts +8 -0
  39. package/src/client-scripts/tests/gsap-mountain-journey.test.ts +8 -0
  40. package/src/client-scripts/tests/gsap-parallax.test.ts +8 -0
  41. package/src/client-scripts/tests/gsap-reveal.test.ts +8 -0
  42. package/src/client-scripts/tests/gsap-stagger.test.ts +8 -0
  43. package/src/client-scripts/tests/inline-number-animation.test.ts +8 -0
  44. package/src/client-scripts/tests/lenis.test.ts +8 -0
  45. package/src/client-scripts/tests/live-photos.test.ts +8 -0
  46. package/src/client-scripts/tests/lordicon.test.ts +8 -0
  47. package/src/client-scripts/tests/orchestrator.test.ts +47 -0
  48. package/src/client-scripts/tests/scheduler.test.ts +23 -0
  49. package/src/client-scripts/tests/scroll-spy.test.ts +204 -0
  50. package/src/{share/scripts → client-scripts/tests}/video-player.test.ts +51 -31
  51. package/src/component/contracts.ts +206 -0
  52. package/src/component/index.ts +11 -0
  53. package/src/{share → content}/content-reference.ts +3 -3
  54. package/src/{share/content → content}/entity-id.ts +6 -3
  55. package/src/content/index.ts +3 -0
  56. package/src/{share/content → content}/merge.ts +6 -1
  57. package/src/content/system-manifest.test.ts +107 -0
  58. package/src/content/system-manifest.ts +12 -94
  59. package/src/{share → content}/tests/resolve-field-path.test.ts +1 -1
  60. package/src/{share → content}/tests/substitute-deep.test.ts +1 -1
  61. package/src/content-discipline/index.ts +19 -0
  62. package/src/{share/content-discipline → content-discipline}/parsers.ts +2 -2
  63. package/src/{share/env.d.ts → env.d.ts} +6 -3
  64. package/src/fingerprint/canonical-json.ts +731 -0
  65. package/src/fingerprint/index.ts +12 -0
  66. package/src/fingerprint/primitives.ts +72 -0
  67. package/src/{share/i18n → i18n}/localization.ts +6 -2
  68. package/src/integration/adapters.ts +10 -71
  69. package/src/integration/dispatch.ts +1 -1
  70. package/src/integration/funnel.ts +6 -1
  71. package/src/integration/index.ts +101 -4
  72. package/src/integration/lagebild-destination-adapter.ts +108 -0
  73. package/src/integration/lagebild-ingress.ts +7 -12
  74. package/src/integration/orchestration.ts +11 -10
  75. package/src/integration/tests/deliver-event.test.ts +1 -1
  76. package/src/integration/tests/dispatch.test.ts +4 -4
  77. package/src/integration/tests/hub-runtime.test.ts +22 -14
  78. package/src/integration/vitest.config.ts +8 -0
  79. package/src/kernel/desired-state.ts +227 -0
  80. package/src/kernel/diagnostic.ts +289 -0
  81. package/src/kernel/fs-atomic.ts +131 -0
  82. package/src/kernel/index.ts +15 -0
  83. package/src/kernel/tests/fs-atomic.test.ts +150 -0
  84. package/src/kernel/tests/workspace-io.test.ts +187 -0
  85. package/src/kernel/types.ts +459 -0
  86. package/src/kernel/workspace-io.ts +248 -0
  87. package/src/{share → knowledge}/tests/claim.test.ts +1 -1
  88. package/src/{share → knowledge}/tests/derivation.test.ts +1 -1
  89. package/src/{share → knowledge}/tests/freshness.test.ts +1 -1
  90. package/src/{share → knowledge}/tests/ledger.test.ts +1 -1
  91. package/src/{share → knowledge}/tests/plan.test.ts +1 -1
  92. package/src/{share → knowledge}/tests/source.test.ts +1 -1
  93. package/src/legal/index.ts +15 -0
  94. package/src/{share/middleware → middleware}/access-protection.ts +8 -8
  95. package/src/{share → node}/dev-props-validator.ts +6 -1
  96. package/src/{share → node}/import-scan.ts +23 -3
  97. package/src/{share → node}/semantic/derived-prices-loader.ts +4 -2
  98. package/src/{share → node}/semantic/markdown-twin-provenance.ts +1 -1
  99. package/src/node/tests/derived-prices-loader.test.ts +59 -0
  100. package/src/node/tests/markdown-twin-provenance.test.ts +152 -0
  101. package/src/observability/env-resolver.ts +6 -1
  102. package/src/observability/index.ts +6 -1
  103. package/src/observability/otlp-converter.ts +6 -1
  104. package/src/observability/otlp-transport.ts +6 -1
  105. package/src/observability/pusher.ts +6 -1
  106. package/src/observability/redact.ts +6 -1
  107. package/src/observability/typed-refs.ts +6 -2
  108. package/src/observability/vitest.config.ts +1 -0
  109. package/src/{share → offers}/formula-eval.ts +3 -3
  110. package/src/{share → offers}/tests/formula-eval.test.ts +2 -2
  111. package/src/onboarding/index.ts +1 -0
  112. package/src/{share → onboarding}/tests/onboarding-yaml.test.ts +1 -1
  113. package/src/ontology/archetype-registry.ts +6 -1
  114. package/src/ontology/archetypes/index.json +33 -1
  115. package/src/ontology/archetypes/index.yaml +27 -1
  116. package/src/ontology/archetypes/sections/entity-grid.yaml +46 -0
  117. package/src/ontology/archetypes/sections/exit-diagram.yaml +43 -0
  118. package/src/ontology/biome-token-projection.ts +6 -1
  119. package/src/ontology/cosmic/index.ts +6 -1
  120. package/src/ontology/cosmic/moon-catalog.ts +6 -1
  121. package/src/ontology/cosmic/planet-catalog.ts +6 -1
  122. package/src/ontology/cosmic/star-catalog.ts +6 -1
  123. package/src/ontology/enums.ts +6 -1
  124. package/src/ontology/external-surfaces/index.ts +1 -1
  125. package/src/ontology/index.ts +6 -2
  126. package/src/ontology/manifest.ts +26 -4
  127. package/src/ontology/operations/artifact-store.ts +45 -0
  128. package/src/ontology/operations/dht.ts +98 -0
  129. package/src/ontology/operations/handoff.ts +92 -0
  130. package/src/ontology/operations/index.ts +187 -1
  131. package/src/ontology/operations/leitstand.ts +99 -0
  132. package/src/ontology/operations/materialization.ts +90 -0
  133. package/src/ontology/operations/mission.ts +111 -0
  134. package/src/ontology/operations/naming-policy.ts +132 -0
  135. package/src/ontology/operations/notausgang.ts +64 -0
  136. package/src/ontology/operations/release.ts +121 -0
  137. package/src/ontology/operations/sternsystem.ts +192 -0
  138. package/src/ontology/operations/werkstatt.ts +48 -0
  139. package/src/ontology/schemas/biome.ts +7 -2
  140. package/src/ontology/schemas/capability.ts +1 -1
  141. package/src/ontology/schemas/constellation.ts +6 -1
  142. package/src/ontology/schemas/index.ts +6 -2
  143. package/src/ontology/schemas/manifest-resolver.ts +6 -1
  144. package/src/ontology/schemas/page-entry.ts +9 -5
  145. package/src/ontology/schemas/system/growth.ts +6 -1
  146. package/src/ontology/schemas/system/manifest.ts +40 -1
  147. package/src/ontology/schemas/system/page-output.ts +7 -6
  148. package/src/ontology/schemas/system/tests/growth.test.ts +22 -0
  149. package/src/ontology/schemas/system/tests/integrations.test.ts +12 -0
  150. package/src/ontology/schemas/system/tests/manifest.test.ts +22 -0
  151. package/src/ontology/schemas/system/tests/page-output.test.ts +13 -0
  152. package/src/ontology/schemas/system/tests/release.test.ts +22 -0
  153. package/src/ontology/schemas/system/tests/text.test.ts +18 -0
  154. package/src/ontology/schemas/system/tests/verification.test.ts +12 -0
  155. package/src/ontology/schemas/system/text.ts +15 -0
  156. package/src/ontology/schemas/system.ts +6 -2
  157. package/src/ontology/schemas/tests/biome.test.ts +19 -0
  158. package/src/ontology/schemas/tests/capability.test.ts +22 -0
  159. package/src/ontology/schemas/tests/constellation.test.ts +22 -0
  160. package/src/ontology/schemas/tests/dns-records.test.ts +13 -0
  161. package/src/ontology/schemas/tests/manifest-resolver.test.ts +13 -0
  162. package/src/ontology/schemas/tests/page-entry.test.ts +22 -0
  163. package/src/ontology/schemas/tests/section-archetype.test.ts +12 -0
  164. package/src/ontology/schemas/tests/site-family.test.ts +18 -0
  165. package/src/ontology/schemas/tests/system.test.ts +20 -0
  166. package/src/ontology/shared-section-props/body-fragments.ts +2 -0
  167. package/src/ontology/tests/sternsystem-owner.test.ts +1 -1
  168. package/src/ontology/vitest.config.ts +1 -0
  169. package/src/{share → page}/page.ts +13 -9
  170. package/src/{share → page}/tests/dev-props-validator.test.ts +1 -1
  171. package/src/{share → page}/tests/hero-section-numbering-parity.test.ts +1 -1
  172. package/src/passport/index.ts +6 -1
  173. package/src/passport/key-rotate.ts +10 -2
  174. package/src/passport/schema.ts +6 -1
  175. package/src/passport/sign.ts +8 -3
  176. package/src/passport/vitest.config.ts +1 -0
  177. package/src/pbt/index.test.ts +95 -0
  178. package/src/pbt/index.ts +251 -0
  179. package/src/plugin/plugin-contract.ts +1 -1
  180. package/src/{share → policy}/entitlement.ts +24 -4
  181. package/src/{share → policy}/feature-policy.ts +8 -3
  182. package/src/{share → policy}/visibility.ts +7 -2
  183. package/src/redirects/redirects.ts +117 -0
  184. package/src/{share → redirects/tests}/redirects.test.ts +1 -1
  185. package/src/remediation/remediation-catalog.ts +316 -0
  186. package/src/{share/routes → routes}/template-filter.ts +1 -1
  187. package/src/{share → runtime}/runtime-context.ts +7 -2
  188. package/src/{share → runtime}/shared-context.ts +1 -1
  189. package/src/{share/schemas → schemas}/claims.ts +2 -2
  190. package/src/{share/schemas → schemas}/effects.ts +6 -1
  191. package/src/{share/schemas → schemas}/features.ts +1 -1
  192. package/src/{share/schemas → schemas}/index.ts +3 -3
  193. package/src/{share/schemas → schemas}/navigation.ts +6 -1
  194. package/src/{share/schemas → schemas}/page-base.ts +6 -2
  195. package/src/{share/schemas → schemas}/person.ts +1 -1
  196. package/src/{share/schemas → schemas}/standard-list-item.ts +1 -1
  197. package/src/schemas/tests/claim-records.test.ts +52 -0
  198. package/src/schemas/tests/claims.test.ts +23 -0
  199. package/src/schemas/tests/effects.test.ts +116 -0
  200. package/src/schemas/tests/features.test.ts +61 -0
  201. package/src/schemas/tests/horizontal-align.test.ts +16 -0
  202. package/src/schemas/tests/icon-color.test.ts +16 -0
  203. package/src/schemas/tests/live-photo.test.ts +40 -0
  204. package/src/schemas/tests/material-credit.test.ts +51 -0
  205. package/src/schemas/tests/media.test.ts +26 -0
  206. package/src/schemas/tests/navigation.test.ts +58 -0
  207. package/src/schemas/tests/page-base.test.ts +19 -0
  208. package/src/schemas/tests/participant.test.ts +26 -0
  209. package/src/schemas/tests/person.test.ts +12 -0
  210. package/src/schemas/tests/print.test.ts +30 -0
  211. package/src/schemas/tests/section-background.test.ts +31 -0
  212. package/src/schemas/tests/section-body.test.ts +16 -0
  213. package/src/schemas/tests/section-cards.test.ts +17 -0
  214. package/src/schemas/tests/section-cta.test.ts +41 -0
  215. package/src/schemas/tests/section-header.test.ts +31 -0
  216. package/src/schemas/tests/section-image.test.ts +24 -0
  217. package/src/schemas/tests/section-motion.test.ts +29 -0
  218. package/src/schemas/tests/section-shell.test.ts +42 -0
  219. package/src/schemas/tests/section-stats.test.ts +17 -0
  220. package/src/schemas/tests/site-background.test.ts +33 -0
  221. package/src/schemas/tests/standard-list-item.test.ts +16 -0
  222. package/src/{share → sections}/rfc0042-utils.ts +1 -1
  223. package/src/{share/semantic → semantic}/block-extractors/index.ts +20 -1
  224. package/src/{share/semantic → semantic}/business-projection.ts +95 -1
  225. package/src/semantic/canonical-uri.ts +54 -0
  226. package/src/{share/semantic → semantic}/extract.ts +100 -1
  227. package/src/semantic/fact-extraction.test.ts +47 -0
  228. package/src/semantic/fact-extraction.ts +46 -0
  229. package/src/semantic/freshness.ts +67 -0
  230. package/src/{share/semantic → semantic}/ids.ts +6 -1
  231. package/src/{share/semantic → semantic}/index.ts +9 -2
  232. package/src/{share/semantic → semantic}/jsonld/breadcrumb.ts +6 -2
  233. package/src/{share/semantic → semantic}/jsonld/context.ts +6 -2
  234. package/src/{share/semantic → semantic}/jsonld/faq.ts +6 -2
  235. package/src/{share/semantic → semantic}/jsonld/initiative.ts +6 -1
  236. package/src/{share/semantic → semantic}/jsonld/organization.ts +6 -1
  237. package/src/{share/semantic → semantic}/jsonld/person.ts +6 -2
  238. package/src/{share/semantic → semantic}/jsonld/service.ts +6 -2
  239. package/src/semantic/jsonld/shared.ts +36 -0
  240. package/src/semantic/jsonld/tests/article.test.ts +89 -0
  241. package/src/semantic/jsonld/tests/breadcrumb.test.ts +62 -0
  242. package/src/semantic/jsonld/tests/collection-list.test.ts +39 -0
  243. package/src/semantic/jsonld/tests/faq.test.ts +60 -0
  244. package/src/semantic/jsonld/tests/initiative.test.ts +56 -0
  245. package/src/semantic/jsonld/tests/organization.test.ts +123 -0
  246. package/src/semantic/jsonld/tests/person.test.ts +89 -0
  247. package/src/semantic/jsonld/tests/service.test.ts +65 -0
  248. package/src/semantic/jsonld/tests/shared.test.ts +41 -0
  249. package/src/semantic/jsonld/tests/video.test.ts +84 -0
  250. package/src/semantic/jsonld/tests/webpage.test.ts +93 -0
  251. package/src/semantic/jsonld/tests/website.test.ts +27 -0
  252. package/src/semantic/jsonld/types.ts +24 -0
  253. package/src/{share/semantic → semantic}/jsonld/webpage.ts +6 -1
  254. package/src/semantic/jsonld/website.ts +32 -0
  255. package/src/{share/semantic → semantic}/jsonld.ts +6 -1
  256. package/src/{share/semantic → semantic}/llms.ts +41 -3
  257. package/src/{share/semantic → semantic}/models.ts +27 -38
  258. package/src/{share/semantic → semantic}/organization-profile.ts +3 -0
  259. package/src/{share/semantic → semantic}/page-builders/markdown-page.ts +6 -3
  260. package/src/{share/semantic → semantic}/page-utils.ts +6 -3
  261. package/src/semantic/tests/ai.test.ts +55 -0
  262. package/src/semantic/tests/block-extraction.test.ts +34 -0
  263. package/src/{share/tests/breadcrumbs.test.ts → semantic/tests/breadcrumbs-0229.test.ts} +1 -1
  264. package/src/semantic/tests/breadcrumbs.test.ts +127 -0
  265. package/src/{share → semantic}/tests/build-page-price-markers.test.ts +2 -2
  266. package/src/semantic/tests/build-page.test.ts +122 -0
  267. package/src/semantic/tests/business-projection.test.ts +367 -0
  268. package/src/semantic/tests/canonical-uri.test.ts +56 -0
  269. package/src/semantic/tests/extract.test.ts +209 -0
  270. package/src/semantic/tests/feed.test.ts +113 -0
  271. package/src/semantic/tests/freshness.pbt.test.ts +108 -0
  272. package/src/semantic/tests/helpers.ts +32 -0
  273. package/src/semantic/tests/ids.test.ts +118 -0
  274. package/src/{share/tests/image-sitemap.test.ts → semantic/tests/image-sitemap-0150.test.ts} +1 -1
  275. package/src/semantic/tests/image-sitemap.test.ts +109 -0
  276. package/src/{share → semantic}/tests/jsonld-service.test.ts +2 -2
  277. package/src/{share → semantic}/tests/jsonld-video.test.ts +3 -3
  278. package/src/{share → semantic}/tests/jsonld-webpage.test.ts +3 -3
  279. package/src/semantic/tests/jsonld.test.ts +61 -0
  280. package/src/{share → semantic}/tests/llms-0789.test.ts +2 -2
  281. package/src/semantic/tests/llms-policy.test.ts +60 -0
  282. package/src/semantic/tests/llms.test.ts +164 -0
  283. package/src/semantic/tests/markdown-hygiene.test.ts +72 -0
  284. package/src/semantic/tests/markdown-page.test.ts +97 -0
  285. package/src/{share → semantic}/tests/organization-jsonld.test.ts +3 -3
  286. package/src/semantic/tests/organization-profile.test.ts +107 -0
  287. package/src/{share/tests/output-projection.test.ts → semantic/tests/output-projection-0143.test.ts} +1 -1
  288. package/src/semantic/tests/output-projection.test.ts +77 -0
  289. package/src/semantic/tests/page-markdown.test.ts +98 -0
  290. package/src/semantic/tests/page-utils.test.ts +88 -0
  291. package/src/{share/tests/price-marker-resolver.test.ts → semantic/tests/price-marker-resolver-0767.test.ts} +2 -2
  292. package/src/semantic/tests/price-marker-resolver.test.ts +90 -0
  293. package/src/semantic/tests/project-claims.pbt.test.ts +60 -0
  294. package/src/semantic/tests/robots.test.ts +70 -0
  295. package/src/{share/semantic → semantic}/tests/split-sentences.test.ts +1 -1
  296. package/src/{share → semantic}/tests/twin-path.test.ts +1 -1
  297. package/src/semantic/tests/update-stamp.test.ts +102 -0
  298. package/src/signing/index.ts +13 -0
  299. package/src/signing/key.ts +139 -0
  300. package/src/signing/sign.ts +66 -0
  301. package/src/signing/types.ts +29 -0
  302. package/src/{share/slug → slug}/slug-url.ts +4 -1
  303. package/src/stack/run-tool.ts +108 -0
  304. package/src/stack/stack-checks.ts +124 -0
  305. package/src/stack/tests/run-tool.test.ts +138 -0
  306. package/src/stack/tests/stack-checks.test.ts +141 -0
  307. package/src/stack/tests/walk-files.test.ts +131 -0
  308. package/src/stack/walk-files.ts +114 -0
  309. package/src/surface/blueprint.ts +342 -66
  310. package/src/surface/decision-composer.ts +6 -1
  311. package/src/surface/geo.ts +6 -1
  312. package/src/surface/index.ts +11 -7
  313. package/src/surface/types.ts +2 -2
  314. package/src/surface/vitest.config.ts +1 -0
  315. package/src/tests/signing-core.test.ts +129 -0
  316. package/src/{share → text}/css-value-normalize.ts +6 -1
  317. package/src/text/string-utils.ts +24 -0
  318. package/src/{share → text}/tests/text-normalize.test.ts +5 -0
  319. package/src/{share → text}/text-normalize.ts +4 -3
  320. package/src/{share → text}/wrap-inline-numbers.ts +1 -1
  321. package/src/types/axiom-study.d.ts +36 -0
  322. package/src/types/hls-js-light.d.ts +21 -0
  323. package/src/types/qrcode-browser.d.ts +23 -0
  324. package/src/typography/fix.pbt.test.ts +208 -0
  325. package/src/typography/fix.ts +148 -0
  326. package/src/typography/index.ts +54 -0
  327. package/src/typography/locale-defaults.ts +97 -0
  328. package/src/typography/rules-tier1.test.ts +459 -0
  329. package/src/typography/rules-tier1.ts +925 -0
  330. package/src/typography/rules-tier2-locale.pbt.test.ts +190 -0
  331. package/src/typography/rules-tier2-locale.test.ts +286 -0
  332. package/src/typography/rules-tier2-locale.ts +399 -0
  333. package/src/typography/rules-tier2-structure.test.ts +330 -0
  334. package/src/typography/rules-tier2-structure.ts +378 -0
  335. package/src/typography/rules-tier3-advisory.test.ts +290 -0
  336. package/src/typography/rules-tier3-advisory.ts +239 -0
  337. package/src/typography/text-surface.test.ts +244 -0
  338. package/src/typography/text-surface.ts +448 -0
  339. package/src/index.ts +0 -14
  340. package/src/integration/port-barrel.ts +0 -105
  341. package/src/share/content/index.ts +0 -16
  342. package/src/share/content-discipline/index.ts +0 -14
  343. package/src/share/index.ts +0 -86
  344. package/src/share/legal/index.ts +0 -1
  345. package/src/share/redirects.ts +0 -54
  346. package/src/share/scripts/index.ts +0 -26
  347. package/src/share/semantic/jsonld/shared.ts +0 -31
  348. package/src/share/semantic/jsonld/types.ts +0 -18
  349. package/src/share/semantic/jsonld/website.ts +0 -27
  350. package/src/share/string-utils.ts +0 -19
  351. package/src/share/types/axiom-study.d.ts +0 -22
  352. package/src/share/types/hls-js-light.d.ts +0 -16
  353. package/src/share/types/qrcode-browser.d.ts +0 -18
  354. package/src/share/utility-registry.yaml +0 -36
  355. package/src/share/vitest.config.ts +0 -8
  356. package/src/surface/blueprint-schema.ts +0 -314
  357. package/src/surface/blueprint-types.ts +0 -256
  358. /package/src/{share/agent → agent}/api-catalog.ts +0 -0
  359. /package/src/{share/agent → agent}/capability.ts +0 -0
  360. /package/src/{share/agent → agent}/dns-aid.ts +0 -0
  361. /package/src/{share/agent → agent}/fleet-catalog.ts +0 -0
  362. /package/src/{share/agent → agent}/index.ts +0 -0
  363. /package/src/{share/agent → agent}/knowledge.ts +0 -0
  364. /package/src/{share/agent → agent}/mcp-card.ts +0 -0
  365. /package/src/{share/agent → agent}/omit-empty.ts +0 -0
  366. /package/src/{share/agent → agent}/openapi.ts +0 -0
  367. /package/src/{share/agent → agent}/proof.ts +0 -0
  368. /package/src/{share/agent → agent}/search.test.ts +0 -0
  369. /package/src/{share → attribution}/attribution-display.ts +0 -0
  370. /package/src/{share/scripts → client-scripts}/gsap-mountain-journey.ts +0 -0
  371. /package/src/{share/scripts → client-scripts}/gsap-parallax.ts +0 -0
  372. /package/src/{share/scripts → client-scripts}/gsap-reveal.ts +0 -0
  373. /package/src/{share/scripts → client-scripts}/gsap-stagger.ts +0 -0
  374. /package/src/{share/scripts → client-scripts}/live-photos.ts +0 -0
  375. /package/src/{share/scripts → client-scripts}/video-player.ts +0 -0
  376. /package/src/{share/content → content}/resolve-field-path.ts +0 -0
  377. /package/src/{share/content → content}/substitute-deep.ts +0 -0
  378. /package/src/{share → content}/tests/content-ref-index.test.ts +0 -0
  379. /package/src/{share/content-discipline → content-discipline}/types.ts +0 -0
  380. /package/src/{share → image-provider}/image-provider.ts +0 -0
  381. /package/src/{share/knowledge → knowledge}/claim.ts +0 -0
  382. /package/src/{share/knowledge → knowledge}/derivation.ts +0 -0
  383. /package/src/{share/knowledge → knowledge}/freshness.ts +0 -0
  384. /package/src/{share/knowledge → knowledge}/index.ts +0 -0
  385. /package/src/{share/knowledge → knowledge}/ledger.ts +0 -0
  386. /package/src/{share/knowledge → knowledge}/plan.ts +0 -0
  387. /package/src/{share/knowledge → knowledge}/source.ts +0 -0
  388. /package/src/{share/legal → legal}/translation-policy.ts +0 -0
  389. /package/src/{share/middleware → middleware}/language-redirect.ts +0 -0
  390. /package/src/{share/middleware → middleware}/tests/access-protection.test.ts +0 -0
  391. /package/src/{share → node}/fs/index.ts +0 -0
  392. /package/src/{share/tests/markdown-twin-provenance.test.ts → node/tests/markdown-twin-provenance-0613.test.ts} +0 -0
  393. /package/src/{share → offers}/offer-capacity.ts +0 -0
  394. /package/src/{share/onboarding-yaml/index.ts → onboarding/yaml.ts} +0 -0
  395. /package/src/{share → ontology}/tests/ontology-role-by-cosmic-name.test.ts +0 -0
  396. /package/src/{share/routes → routes}/tests/template-filter.test.ts +0 -0
  397. /package/src/{share/schemas → schemas}/claim-records.ts +0 -0
  398. /package/src/{share/schemas → schemas}/horizontal-align.ts +0 -0
  399. /package/src/{share/schemas → schemas}/icon-color.ts +0 -0
  400. /package/src/{share/schemas → schemas}/live-photo.ts +0 -0
  401. /package/src/{share/schemas → schemas}/material-credit.ts +0 -0
  402. /package/src/{share/schemas → schemas}/media.ts +0 -0
  403. /package/src/{share/schemas → schemas}/participant.ts +0 -0
  404. /package/src/{share/schemas → schemas}/print.ts +0 -0
  405. /package/src/{share/schemas → schemas}/section-background.ts +0 -0
  406. /package/src/{share/schemas → schemas}/section-body.ts +0 -0
  407. /package/src/{share/schemas → schemas}/section-cards.ts +0 -0
  408. /package/src/{share/schemas → schemas}/section-cta.ts +0 -0
  409. /package/src/{share/schemas → schemas}/section-header.ts +0 -0
  410. /package/src/{share/schemas → schemas}/section-image.ts +0 -0
  411. /package/src/{share/schemas → schemas}/section-motion.ts +0 -0
  412. /package/src/{share/schemas → schemas}/section-shell.ts +0 -0
  413. /package/src/{share/schemas → schemas}/section-stats.ts +0 -0
  414. /package/src/{share/schemas → schemas}/site-background.ts +0 -0
  415. /package/src/{share/semantic → semantic}/ai.ts +0 -0
  416. /package/src/{share/semantic → semantic}/block-extraction.ts +0 -0
  417. /package/src/{share/semantic → semantic}/breadcrumbs.ts +0 -0
  418. /package/src/{share/semantic → semantic}/build-page.ts +0 -0
  419. /package/src/{share/semantic → semantic}/feed.ts +0 -0
  420. /package/src/{share/semantic → semantic}/image-sitemap.ts +0 -0
  421. /package/src/{share/semantic → semantic}/jsonld/article.ts +0 -0
  422. /package/src/{share/semantic → semantic}/jsonld/collection-list.ts +0 -0
  423. /package/src/{share/semantic → semantic}/jsonld/video.ts +0 -0
  424. /package/src/{share/semantic → semantic}/llms-policy.ts +0 -0
  425. /package/src/{share/semantic → semantic}/markdown-hygiene.ts +0 -0
  426. /package/src/{share/semantic → semantic}/output-projection.ts +0 -0
  427. /package/src/{share/semantic → semantic}/page-markdown.ts +0 -0
  428. /package/src/{share/semantic → semantic}/price-marker-resolver.ts +0 -0
  429. /package/src/{share/semantic → semantic}/robots.ts +0 -0
  430. /package/src/{share/semantic → semantic}/update-stamp.ts +0 -0
  431. /package/src/{share/slug → slug}/heading-slugger.ts +0 -0
  432. /package/src/{share/slug → slug}/index.ts +0 -0
  433. /package/src/{share/slug → slug}/slug-id.ts +0 -0
  434. /package/src/{share/slug → slug}/strategies.ts +0 -0
  435. /package/src/{share/slug → slug}/tests/slug.test.ts +0 -0
  436. /package/src/{share → text}/tests/text-normalize.pbt.test.ts +0 -0
  437. /package/src/{share → text}/text-position.ts +0 -0
@@ -0,0 +1,925 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Tier 1 typography rule engine (RFC-1068). Implements 14 rules across
4
+ 5 families: PUNCT (4), SPACE (4), CASE (2), LOCALE (3), YAML (2). All regexes
5
+ use the `u` flag and Unicode property escapes. Rules operate on TextSegment
6
+ objects produced by the text-surface extractor, never on raw file bytes.
7
+ TypographyRuleId and TypographyRuleFamily include Tier 2 families (HEAD, PAIR,
8
+ MD per RFC-1069; NUM, ABBR, APOS per RFC-1070) and Tier 3 families (UNICODE, LINK,
9
+ SENT per RFC-1071) so that Tier 2 and Tier 3 rule arrays can be typed as
10
+ TypographyRule[].</purpose>
11
+ <non-goals>
12
+ <item>Do not extract text — that is text-surface.ts.</item>
13
+ <item>Do not handle file I/O or command registration — that is the command adapter.</item>
14
+ <item>Do not auto-fix — autofix is RFC-1072.</item>
15
+ </non-goals>
16
+ </MODULE_CONTRACT>
17
+ <CHANGE_SUMMARY>
18
+ <item>RFC-1068: initial creation with 14 Tier 1 rules.</item>
19
+ <item>RFC-1069: extended TypographyRuleId and TypographyRuleFamily with HEAD, PAIR, MD families.</item>
20
+ <item>RFC-1070: extended TypographyRuleId and TypographyRuleFamily with NUM, ABBR, APOS families; added localeDefaults to TypographyContext.</item>
21
+ <item>RFC-1071: extended TypographyRuleId and TypographyRuleFamily with UNICODE, LINK, SENT families.</item>
22
+ <item>RFC-1072: changed TypographyFinding.fix from null to FixAction | null; extended finding() helper with optional FixAction param; populated fix on PUNCT-01, SPACE-01, SPACE-02.</item>
23
+ </CHANGE_SUMMARY>
24
+ */
25
+
26
+ import type { TextSegment } from "./text-surface.ts";
27
+ import type { FixAction } from "./fix.ts";
28
+ import {
29
+ LOCALE_DEFAULTS,
30
+ getLocaleDefaults,
31
+ type LocaleTypographyDefaults,
32
+ } from "./locale-defaults.ts";
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Types (from RFC-1068)
36
+ // ---------------------------------------------------------------------------
37
+
38
+ export type TypographyRuleId = `TYPO-${
39
+ | "PUNCT"
40
+ | "SPACE"
41
+ | "CASE"
42
+ | "LOCALE"
43
+ | "YAML"
44
+ | "HEAD"
45
+ | "PAIR"
46
+ | "MD"
47
+ | "NUM"
48
+ | "ABBR"
49
+ | "APOS"
50
+ | "UNICODE"
51
+ | "LINK"
52
+ | "SENT"}-${string}`;
53
+
54
+ export type TypographyRuleFamily =
55
+ | "PUNCT"
56
+ | "SPACE"
57
+ | "CASE"
58
+ | "LOCALE"
59
+ | "YAML"
60
+ | "HEAD"
61
+ | "PAIR"
62
+ | "MD"
63
+ | "NUM"
64
+ | "ABBR"
65
+ | "APOS"
66
+ | "UNICODE"
67
+ | "LINK"
68
+ | "SENT";
69
+
70
+ export interface TypographyFinding {
71
+ ruleId: TypographyRuleId;
72
+ segment: TextSegment;
73
+ /** 0-based column inside segment.text. */
74
+ column: number;
75
+ /** Offending substring (for messages and fix). */
76
+ match: string;
77
+ message: string;
78
+ fixHint: string;
79
+ /** Set by rules whitelisted in RFC-1072; null for non-fixable rules. */
80
+ fix: FixAction | null;
81
+ }
82
+
83
+ export interface TypographyRule {
84
+ id: TypographyRuleId;
85
+ family: TypographyRuleFamily;
86
+ tier: 1 | 2 | 3;
87
+ severity: "error" | "warning";
88
+ check(segment: TextSegment, ctx: TypographyContext): TypographyFinding[];
89
+ }
90
+
91
+ export interface TypographyContext {
92
+ locale: string;
93
+ allowedTokens: ReadonlySet<string>;
94
+ abbreviations: ReadonlySet<string>;
95
+ /** Locale-specific typography defaults (number format, apostrophe policy). Added by RFC-1070. */
96
+ localeDefaults: LocaleTypographyDefaults;
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Default allowed tokens seed (TYPO-CASE-01 and TYPO-LOCALE-03 exclusions)
101
+ // ---------------------------------------------------------------------------
102
+
103
+ export const DEFAULT_ALLOWED_TOKENS = new Set([
104
+ "iPhone",
105
+ "iPad",
106
+ "eBay",
107
+ "GitHub",
108
+ "YouTube",
109
+ "LinkedIn",
110
+ "WordPress",
111
+ "JavaScript",
112
+ "TypeScript",
113
+ "PayPal",
114
+ "macOS",
115
+ "iOS",
116
+ "OpenAI",
117
+ ]);
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Helper: create a finding
121
+ // ---------------------------------------------------------------------------
122
+
123
+ export function finding(
124
+ ruleId: TypographyRuleId,
125
+ segment: TextSegment,
126
+ match: string,
127
+ column: number,
128
+ message: string,
129
+ fixHint: string,
130
+ fix: FixAction | null = null,
131
+ ): TypographyFinding {
132
+ return { ruleId, segment, match, column, message, fixHint, fix };
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Helper: check if a token is in allowedTokens
137
+ // ---------------------------------------------------------------------------
138
+
139
+ function isAllowedToken(token: string, ctx: TypographyContext): boolean {
140
+ if (ctx.allowedTokens.has(token)) return true;
141
+ // Also check with leading/trailing punctuation stripped
142
+ const stripped = token.replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, "");
143
+ if (stripped !== token && ctx.allowedTokens.has(stripped)) return true;
144
+ // Also check hyphenated parts (e.g. LinkedIn-Profil → LinkedIn)
145
+ for (const part of stripped.split("-")) {
146
+ if (part.length > 0 && ctx.allowedTokens.has(part)) return true;
147
+ }
148
+ return false;
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Helper: extract tokens (maximal non-whitespace sequences) from text
153
+ // ---------------------------------------------------------------------------
154
+
155
+ function tokenize(text: string): string[] {
156
+ return text.split(/\s+/).filter(Boolean);
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Placeholder characters — treated as letters for spacing, skipped for casing/locale
161
+ // ---------------------------------------------------------------------------
162
+
163
+ const FORMULA_PLACEHOLDER = "\uE000";
164
+ const URL_PLACEHOLDER = "\uE001";
165
+ const CMS_PLACEHOLDER = "\uE002";
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // TYPO-PUNCT-01: Doubled or clashing punctuation
169
+ // Pattern: [,;:!?]\. · \.[,;] · :: · ;; · ,, · [!?]{3,}
170
+ // Exclusions: ?! and !? are allowed
171
+ // ---------------------------------------------------------------------------
172
+
173
+ const PUNCT01_PATTERNS: RegExp[] = [/[,;:!?]\./u, /\.[,;]/u, /::/u, /;;/u, /,,/u, /[!?]{3,}/u];
174
+
175
+ /**
176
+ * Deduplicate clashing punctuation for PUNCT-01 fix.
177
+ * [,;:!?]. → . (keep the period)
178
+ * .[,;] → . (keep the period)
179
+ * :: → :
180
+ * ;; → ;
181
+ * ,, → ,
182
+ * [!?]{3,} → !! (keep first two)
183
+ */
184
+ function dedupPunct(matched: string): string {
185
+ // [,;:!?]. — closing mark followed by period → keep period
186
+ if (/^[,;:!?]\.$/.test(matched)) return ".";
187
+ // .[,;] — period followed by comma/semicolon → keep period
188
+ if (/^\.[,;]$/.test(matched)) return ".";
189
+ // :: → :
190
+ if (matched === "::") return ":";
191
+ // ;; → ;
192
+ if (matched === ";;") return ";";
193
+ // ,, → ,
194
+ if (matched === ",,") return ",";
195
+ // [!?]{3,} → keep first two
196
+ if (/^[!?]{3,}$/.test(matched)) return matched.slice(0, 2);
197
+ return matched;
198
+ }
199
+
200
+ const PUNCT01_MESSAGES: Record<string, { message: string; fixHint: string }> = {
201
+ "[,;:!?]\\.": {
202
+ message: "Clashing punctuation — a closing mark followed by a period.",
203
+ fixHint: "Keep exactly one closing mark.",
204
+ },
205
+ "\\.[,;]": {
206
+ message: "Clashing punctuation — a period followed by a comma or semicolon.",
207
+ fixHint: "Keep exactly one closing mark.",
208
+ },
209
+ "::": {
210
+ message: "Doubled colon.",
211
+ fixHint: "Use a single colon.",
212
+ },
213
+ ";;": {
214
+ message: "Doubled semicolon.",
215
+ fixHint: "Use a single semicolon.",
216
+ },
217
+ ",,": {
218
+ message: "Doubled comma.",
219
+ fixHint: "Use a single comma.",
220
+ },
221
+ "[!?]{3,}": {
222
+ message: "Three or more exclamation/question marks in a row.",
223
+ fixHint: "Use at most two exclamation or question marks.",
224
+ },
225
+ };
226
+
227
+ const punct01: TypographyRule = {
228
+ id: "TYPO-PUNCT-01",
229
+ family: "PUNCT",
230
+ tier: 1,
231
+ severity: "error",
232
+ check(segment, ctx) {
233
+ const findings: TypographyFinding[] = [];
234
+ const text = segment.text;
235
+ for (const pattern of PUNCT01_PATTERNS) {
236
+ const match = pattern.exec(text);
237
+ if (match) {
238
+ // Check if it's ?! or !? — those are allowed
239
+ const matched = match[0];
240
+ if (matched === "?!" || matched === "!?") continue;
241
+
242
+ // Exclusion: period followed by comma/semicolon where the period
243
+ // is the last char of a known abbreviation (e.g. "e.V.,", "Inc.,")
244
+ if (pattern.source === "\\.[,;]") {
245
+ const textUpToDot = text.slice(0, match.index + 1);
246
+ let isAbbrev = false;
247
+ for (const abbr of ctx.abbreviations) {
248
+ if (textUpToDot.endsWith(abbr)) {
249
+ isAbbrev = true;
250
+ break;
251
+ }
252
+ }
253
+ if (isAbbrev) continue;
254
+ }
255
+
256
+ const key = pattern.source;
257
+ const msg = PUNCT01_MESSAGES[key] ?? {
258
+ message: `Doubled or clashing punctuation: "${matched}".`,
259
+ fixHint: "Keep exactly one closing mark.",
260
+ };
261
+ findings.push(
262
+ finding(this.id, segment, matched, match.index, msg.message, msg.fixHint, {
263
+ type: "replace",
264
+ old: matched,
265
+ new: dedupPunct(matched),
266
+ }),
267
+ );
268
+ }
269
+ }
270
+ return findings;
271
+ },
272
+ };
273
+
274
+ // ---------------------------------------------------------------------------
275
+ // TYPO-PUNCT-02: Two dots or four-plus dots (not three-dot ellipsis)
276
+ // Pattern: (?<!\.)\.\.(?!\.) · \.{4,}
277
+ // ---------------------------------------------------------------------------
278
+
279
+ const PUNCT02_DOUBLE_DOT = /(?<!\.)\.\.(?!\.)/u;
280
+ const PUNCT02_MANY_DOTS = /\.{4,}/u;
281
+
282
+ const punct02: TypographyRule = {
283
+ id: "TYPO-PUNCT-02",
284
+ family: "PUNCT",
285
+ tier: 1,
286
+ severity: "error",
287
+ check(segment, ctx) {
288
+ const findings: TypographyFinding[] = [];
289
+ const text = segment.text;
290
+ let m = PUNCT02_DOUBLE_DOT.exec(text);
291
+ if (m) {
292
+ // Exclusion: first dot is the last char of a known abbreviation
293
+ const textUpToFirstDot = text.slice(0, m.index + 1);
294
+ let isAbbrev = false;
295
+ for (const abbr of ctx.abbreviations) {
296
+ if (textUpToFirstDot.endsWith(abbr)) {
297
+ isAbbrev = true;
298
+ break;
299
+ }
300
+ }
301
+ if (!isAbbrev) {
302
+ findings.push(
303
+ finding(
304
+ this.id,
305
+ segment,
306
+ m[0],
307
+ m.index,
308
+ "Two consecutive dots (not a three-dot ellipsis).",
309
+ "Use a single period or a three-dot ellipsis (...).",
310
+ ),
311
+ );
312
+ }
313
+ }
314
+ m = PUNCT02_MANY_DOTS.exec(text);
315
+ if (m) {
316
+ findings.push(
317
+ finding(
318
+ this.id,
319
+ segment,
320
+ m[0],
321
+ m.index,
322
+ "Four or more consecutive dots.",
323
+ "Use a three-dot ellipsis (...) or a single period.",
324
+ ),
325
+ );
326
+ }
327
+ return findings;
328
+ },
329
+ };
330
+
331
+ // ---------------------------------------------------------------------------
332
+ // TYPO-PUNCT-03: Whitespace before closing punctuation
333
+ // Pattern: \s[,;:!?] · \s\.(?!\.\.)
334
+ // Exclusions: \s\.\.\. (spaced ellipsis) is allowed
335
+ // ---------------------------------------------------------------------------
336
+
337
+ const PUNCT03_WS_BEFORE_PUNCT = /\s[,;:!?]/u;
338
+ const PUNCT03_WS_BEFORE_DOT = /\s\.(?!\.)/u;
339
+
340
+ const punct03: TypographyRule = {
341
+ id: "TYPO-PUNCT-03",
342
+ family: "PUNCT",
343
+ tier: 1,
344
+ severity: "error",
345
+ check(segment, _ctx) {
346
+ const findings: TypographyFinding[] = [];
347
+ const text = segment.text;
348
+ let m = PUNCT03_WS_BEFORE_PUNCT.exec(text);
349
+ if (m) {
350
+ findings.push(
351
+ finding(
352
+ this.id,
353
+ segment,
354
+ m[0],
355
+ m.index,
356
+ "Whitespace before closing punctuation.",
357
+ "Remove the space before the punctuation mark.",
358
+ ),
359
+ );
360
+ }
361
+ m = PUNCT03_WS_BEFORE_DOT.exec(text);
362
+ if (m) {
363
+ findings.push(
364
+ finding(
365
+ this.id,
366
+ segment,
367
+ m[0],
368
+ m.index,
369
+ "Whitespace before a period.",
370
+ "Remove the space before the period.",
371
+ ),
372
+ );
373
+ }
374
+ return findings;
375
+ },
376
+ };
377
+
378
+ // ---------------------------------------------------------------------------
379
+ // TYPO-PUNCT-04: Missing space after closing punctuation before a letter
380
+ // Pattern: [,;!?]\p{L} · (?<!\b\p{L})\.\p{L}
381
+ // Exclusions: single-letter abbreviation stems (z.B., u.a.) — deferred to TYPO-ABBR (RFC-1070);
382
+ // URL/e-mail/domain tokens; decimal separators followed by digits
383
+ // ---------------------------------------------------------------------------
384
+
385
+ const PUNCT04_COMMA_SEMICOLON_EXCL = /[,;!?]\p{L}/u;
386
+ const PUNCT04_DOT_LETTER = /(?<!\b\p{L})\.\p{L}/u;
387
+
388
+ const punct04: TypographyRule = {
389
+ id: "TYPO-PUNCT-04",
390
+ family: "PUNCT",
391
+ tier: 1,
392
+ severity: "error",
393
+ check(segment, _ctx) {
394
+ const findings: TypographyFinding[] = [];
395
+ const text = segment.text;
396
+
397
+ let m = PUNCT04_COMMA_SEMICOLON_EXCL.exec(text);
398
+ if (m) {
399
+ // Skip decimal separators (digit,digit)
400
+ const before = text[m.index - 1];
401
+ const after = text[m.index + 2];
402
+ if (before && /\p{Nd}/u.test(before) && after && /\p{Nd}/u.test(after)) {
403
+ // decimal separator — skip
404
+ } else {
405
+ findings.push(
406
+ finding(
407
+ this.id,
408
+ segment,
409
+ m[0],
410
+ m.index,
411
+ "Missing space after closing punctuation before a letter.",
412
+ "Add a space after the punctuation mark.",
413
+ ),
414
+ );
415
+ }
416
+ }
417
+
418
+ m = PUNCT04_DOT_LETTER.exec(text);
419
+ if (m) {
420
+ // Version pattern exclusion: \d+\.x\b (e.g. "4.x", "2.0.x") — RFC-1083.
421
+ // match[0] is ".x" (dot + letter), NOT "4.x". Check the character before
422
+ // the dot (must be a digit) and the letter after the dot (must be "x"
423
+ // followed by a non-word character or end of string).
424
+ const beforeChar = text[m.index - 1];
425
+ const afterLetter = text[m.index + 2];
426
+ const isVersion =
427
+ beforeChar !== undefined &&
428
+ /\p{Nd}/u.test(beforeChar) &&
429
+ m[0][1] === "x" &&
430
+ (afterLetter === undefined || /\W/u.test(afterLetter));
431
+ if (isVersion) {
432
+ // version designator like "4.x" — skip
433
+ } else {
434
+ // Skip if preceded by a single letter (abbreviation like z.B.)
435
+ const isSingleLetterAbbrev =
436
+ beforeChar !== undefined &&
437
+ /\p{L}/u.test(beforeChar) &&
438
+ (text[m.index - 2] === undefined || /\s/.test(text[m.index - 2]));
439
+ if (!isSingleLetterAbbrev) {
440
+ findings.push(
441
+ finding(
442
+ this.id,
443
+ segment,
444
+ m[0],
445
+ m.index,
446
+ "Missing space after a period before a letter.",
447
+ "Add a space after the period.",
448
+ ),
449
+ );
450
+ }
451
+ }
452
+ }
453
+
454
+ return findings;
455
+ },
456
+ };
457
+
458
+ // ---------------------------------------------------------------------------
459
+ // TYPO-SPACE-01: Two or more consecutive spaces inside text
460
+ // Pattern: \S {2,}\S
461
+ // Exclusions: markdown table rows
462
+ // ---------------------------------------------------------------------------
463
+
464
+ const SPACE01 = /\S {2,}\S/u;
465
+
466
+ const space01: TypographyRule = {
467
+ id: "TYPO-SPACE-01",
468
+ family: "SPACE",
469
+ tier: 1,
470
+ severity: "error",
471
+ check(segment, _ctx) {
472
+ if (segment.isTableRow) return [];
473
+ const m = SPACE01.exec(segment.text);
474
+ if (m) {
475
+ return [
476
+ finding(
477
+ this.id,
478
+ segment,
479
+ m[0],
480
+ m.index,
481
+ "Two or more consecutive spaces inside text.",
482
+ "Use a single space.",
483
+ { type: "replace", old: m[0], new: m[0].replace(/ {2,}/g, " ") },
484
+ ),
485
+ ];
486
+ }
487
+ return [];
488
+ },
489
+ };
490
+
491
+ // ---------------------------------------------------------------------------
492
+ // TYPO-SPACE-02: Trailing whitespace at end of a frontmatter string or body line
493
+ // Pattern: [ \t]+$
494
+ // Exclusions: none — markdown two-space hard breaks are forbidden
495
+ // ---------------------------------------------------------------------------
496
+
497
+ const SPACE02 = /[ \t]+$/u;
498
+
499
+ const space02: TypographyRule = {
500
+ id: "TYPO-SPACE-02",
501
+ family: "SPACE",
502
+ tier: 1,
503
+ severity: "error",
504
+ check(segment, _ctx) {
505
+ const m = SPACE02.exec(segment.text);
506
+ if (m) {
507
+ return [
508
+ finding(
509
+ this.id,
510
+ segment,
511
+ m[0],
512
+ m.index,
513
+ "Trailing whitespace at end of line.",
514
+ "Remove trailing spaces.",
515
+ { type: "delete", at: m.index, length: m[0].length },
516
+ ),
517
+ ];
518
+ }
519
+ return [];
520
+ },
521
+ };
522
+
523
+ // ---------------------------------------------------------------------------
524
+ // TYPO-SPACE-03: Tab character inside text
525
+ // Pattern: \t
526
+ // Exclusions: fenced code blocks (already stripped by extractor)
527
+ // ---------------------------------------------------------------------------
528
+
529
+ const SPACE03 = /\t/u;
530
+
531
+ const space03: TypographyRule = {
532
+ id: "TYPO-SPACE-03",
533
+ family: "SPACE",
534
+ tier: 1,
535
+ severity: "error",
536
+ check(segment, _ctx) {
537
+ const m = SPACE03.exec(segment.text);
538
+ if (m) {
539
+ return [
540
+ finding(
541
+ this.id,
542
+ segment,
543
+ m[0],
544
+ m.index,
545
+ "Tab character inside text.",
546
+ "Use spaces instead of tabs.",
547
+ ),
548
+ ];
549
+ }
550
+ return [];
551
+ },
552
+ };
553
+
554
+ // ---------------------------------------------------------------------------
555
+ // TYPO-SPACE-04: Leading whitespace in a frontmatter string value
556
+ // Pattern: ^[ \t]+
557
+ // Exclusions: none
558
+ // ---------------------------------------------------------------------------
559
+
560
+ const SPACE04 = /^[ \t]+/u;
561
+
562
+ const space04: TypographyRule = {
563
+ id: "TYPO-SPACE-04",
564
+ family: "SPACE",
565
+ tier: 1,
566
+ severity: "error",
567
+ check(segment, _ctx) {
568
+ if (segment.source !== "frontmatter") return [];
569
+ const m = SPACE04.exec(segment.text);
570
+ if (m) {
571
+ return [
572
+ finding(
573
+ this.id,
574
+ segment,
575
+ m[0],
576
+ m.index,
577
+ "Leading whitespace in a frontmatter string value.",
578
+ "Remove leading spaces from the value.",
579
+ ),
580
+ ];
581
+ }
582
+ return [];
583
+ },
584
+ };
585
+
586
+ // ---------------------------------------------------------------------------
587
+ // TYPO-CASE-01: Uppercase letter between two lowercase letters inside a word
588
+ // Pattern: \p{Ll}\p{Lu}\p{Ll}
589
+ // Exclusions: tokens in typography.allowedTokens; code spans; formulas; URLs
590
+ // ---------------------------------------------------------------------------
591
+
592
+ const CASE01 = /\p{Ll}\p{Lu}\p{Ll}/u;
593
+
594
+ const case01: TypographyRule = {
595
+ id: "TYPO-CASE-01",
596
+ family: "CASE",
597
+ tier: 1,
598
+ severity: "error",
599
+ check(segment, ctx) {
600
+ const text = segment.text;
601
+ const m = CASE01.exec(text);
602
+ if (!m) return [];
603
+
604
+ // Check if the containing token is in allowedTokens
605
+ // Find the token (maximal non-whitespace sequence) that contains this match
606
+ const matchStart = m.index;
607
+ const matchEnd = m.index + m[0].length;
608
+
609
+ // Find token boundaries
610
+ let tokenStart = matchStart;
611
+ while (tokenStart > 0 && !/\s/.test(text[tokenStart - 1])) tokenStart--;
612
+ let tokenEnd = matchEnd;
613
+ while (tokenEnd < text.length && !/\s/.test(text[tokenEnd])) tokenEnd++;
614
+
615
+ const token = text.slice(tokenStart, tokenEnd);
616
+
617
+ // Skip if token contains placeholders (formula/URL)
618
+ if (
619
+ token.includes(FORMULA_PLACEHOLDER) ||
620
+ token.includes(URL_PLACEHOLDER) ||
621
+ token.includes(CMS_PLACEHOLDER)
622
+ )
623
+ return [];
624
+
625
+ if (isAllowedToken(token, ctx)) return [];
626
+
627
+ return [
628
+ finding(
629
+ this.id,
630
+ segment,
631
+ m[0],
632
+ m.index,
633
+ `Uppercase letter between two lowercase letters inside a word: "${m[0]}" in "${token}".`,
634
+ "Fix the casing — this is likely a corruption from a regex edit or translation tool.",
635
+ ),
636
+ ];
637
+ },
638
+ };
639
+
640
+ // ---------------------------------------------------------------------------
641
+ // TYPO-CASE-02: Lowercase sentence start after sentence-ending punctuation inside a paragraph
642
+ // Pattern: [.!?]\s+\p{Ll}
643
+ // Exclusions: preceding token is a known abbreviation for the file locale;
644
+ // preceding token is ...; preceding token is a digit sequence (\d+)
645
+ // ---------------------------------------------------------------------------
646
+
647
+ const CASE02 = /[.!?]\s+\p{Ll}/gu;
648
+
649
+ const case02: TypographyRule = {
650
+ id: "TYPO-CASE-02",
651
+ family: "CASE",
652
+ tier: 1,
653
+ severity: "error",
654
+ check(segment, ctx) {
655
+ const text = segment.text;
656
+ for (const m of text.matchAll(CASE02)) {
657
+ const punctIndex = m.index;
658
+
659
+ // Find the token before the punctuation
660
+ // Scan backwards to find the token (maximal non-whitespace before punct)
661
+ let tokenEnd = punctIndex;
662
+ while (tokenEnd > 0 && /\s/.test(text[tokenEnd - 1])) tokenEnd--;
663
+ let tokenStart = tokenEnd;
664
+ while (tokenStart > 0 && !/\s/.test(text[tokenStart - 1])) tokenStart--;
665
+
666
+ const token = text.slice(tokenStart, tokenEnd);
667
+
668
+ // Exclusion: preceding token is a known abbreviation
669
+ if (ctx.abbreviations.has(token)) continue;
670
+
671
+ // Exclusion: text ending at the punctuation matches a multi-word abbreviation
672
+ // (e.g. "z. B." — the period at punctIndex is part of the abbreviation)
673
+ // Also check text including the matched characters, for abbreviations like
674
+ // "u. a." where the first period triggers the match but the abbreviation
675
+ // continues past the matched lowercase letter
676
+ const textUpToPunct = text.slice(0, punctIndex + 1);
677
+ const textUpToMatch = text.slice(0, m.index + m[0].length + 1);
678
+ let isAbbrev = false;
679
+ for (const abbr of ctx.abbreviations) {
680
+ if (textUpToPunct.endsWith(abbr) || textUpToMatch.endsWith(abbr)) {
681
+ isAbbrev = true;
682
+ break;
683
+ }
684
+ }
685
+ if (isAbbrev) continue;
686
+
687
+ // Exclusion: preceding token is "..."
688
+ if (token === "...") continue;
689
+
690
+ // Exclusion: preceding token is a digit sequence
691
+ if (/^\d+$/.test(token)) continue;
692
+
693
+ // Exclusion: preceding token contains a placeholder (formula/URL/CMS)
694
+ if (
695
+ token.includes(FORMULA_PLACEHOLDER) ||
696
+ token.includes(URL_PLACEHOLDER) ||
697
+ token.includes(CMS_PLACEHOLDER)
698
+ )
699
+ continue;
700
+
701
+ return [
702
+ finding(
703
+ this.id,
704
+ segment,
705
+ m[0],
706
+ m.index,
707
+ "Lowercase sentence start after sentence-ending punctuation.",
708
+ "Capitalize the first letter of the sentence.",
709
+ ),
710
+ ];
711
+ }
712
+ return [];
713
+ },
714
+ };
715
+
716
+ // ---------------------------------------------------------------------------
717
+ // TYPO-LOCALE-01: Mixed-script word (Latin and Cyrillic letters in one token)
718
+ // Pattern: token matches both \p{Script=Latin} and \p{Script=Cyrillic}
719
+ // Exclusions: Latin parts of hyphenated tokens in typography.allowedTokens
720
+ // ---------------------------------------------------------------------------
721
+
722
+ const LATIN_SCRIPT = /\p{Script=Latin}/u;
723
+ const CYRILLIC_SCRIPT = /\p{Script=Cyrillic}/u;
724
+
725
+ const LATIN_ONLY = /^[\p{Script=Latin}0-9.]+$/u;
726
+ const HAS_LATIN_LETTER = /\p{Script=Latin}/u;
727
+
728
+ function extractLatinParts(token: string): string[] {
729
+ const parts = token.split("-");
730
+ return parts
731
+ .map((p) => p.replace(/[^\p{Script=Latin}0-9.]/gu, ""))
732
+ .filter((p) => p.length > 0 && LATIN_ONLY.test(p) && HAS_LATIN_LETTER.test(p));
733
+ }
734
+
735
+ const locale01: TypographyRule = {
736
+ id: "TYPO-LOCALE-01",
737
+ family: "LOCALE",
738
+ tier: 1,
739
+ severity: "error",
740
+ check(segment, ctx) {
741
+ const tokens = tokenize(segment.text);
742
+ for (const token of tokens) {
743
+ // Skip tokens with placeholders
744
+ if (
745
+ token.includes(FORMULA_PLACEHOLDER) ||
746
+ token.includes(URL_PLACEHOLDER) ||
747
+ token.includes(CMS_PLACEHOLDER)
748
+ )
749
+ continue;
750
+ if (LATIN_SCRIPT.test(token) && CYRILLIC_SCRIPT.test(token)) {
751
+ // Check if all Latin parts are in allowedTokens
752
+ const latinParts = extractLatinParts(token);
753
+ if (latinParts.length > 0 && latinParts.every((p) => ctx.allowedTokens.has(p))) {
754
+ continue;
755
+ }
756
+ return [
757
+ finding(
758
+ this.id,
759
+ segment,
760
+ token,
761
+ segment.text.indexOf(token),
762
+ `Mixed-script word (Latin and Cyrillic letters in one token): "${token}".`,
763
+ "Separate scripts — this is likely a homoglyph or translation artifact.",
764
+ ),
765
+ ];
766
+ }
767
+ }
768
+ return [];
769
+ },
770
+ };
771
+
772
+ // ---------------------------------------------------------------------------
773
+ // TYPO-LOCALE-02: Foreign-alphabet letters for a Cyrillic locale
774
+ // Pattern: file locale `uk`: [ыэъёЫЭЪЁ]
775
+ // Exclusions: tokens in typography.allowedTokens
776
+ // ---------------------------------------------------------------------------
777
+
778
+ const UK_FOREIGN_LETTERS = /[ыэъёЫЭЪЁ]/u;
779
+
780
+ const locale02: TypographyRule = {
781
+ id: "TYPO-LOCALE-02",
782
+ family: "LOCALE",
783
+ tier: 1,
784
+ severity: "error",
785
+ check(segment, ctx) {
786
+ if (segment.locale !== "uk") return [];
787
+ const text = segment.text;
788
+ const m = UK_FOREIGN_LETTERS.exec(text);
789
+ if (!m) return [];
790
+
791
+ // Check if the containing token is in allowedTokens
792
+ const matchIndex = m.index;
793
+ let tokenStart = matchIndex;
794
+ while (tokenStart > 0 && !/\s/.test(text[tokenStart - 1])) tokenStart--;
795
+ let tokenEnd = matchIndex + 1;
796
+ while (tokenEnd < text.length && !/\s/.test(text[tokenEnd])) tokenEnd++;
797
+ const token = text.slice(tokenStart, tokenEnd);
798
+
799
+ if (isAllowedToken(token, ctx)) return [];
800
+
801
+ return [
802
+ finding(
803
+ this.id,
804
+ segment,
805
+ m[0],
806
+ m.index,
807
+ `Foreign-alphabet letter "${m[0]}" in a Ukrainian locale (these letters are Russian, not Ukrainian).`,
808
+ "Replace with the Ukrainian equivalent.",
809
+ ),
810
+ ];
811
+ },
812
+ };
813
+
814
+ // ---------------------------------------------------------------------------
815
+ // TYPO-LOCALE-03: Cyrillic letters in a non-Cyrillic locale
816
+ // Pattern: file locale `de`/`en`: \p{Script=Cyrillic}
817
+ // Exclusions: tokens in typography.allowedTokens; markdown blockquote lines (lines starting with >)
818
+ // ---------------------------------------------------------------------------
819
+
820
+ const locale03: TypographyRule = {
821
+ id: "TYPO-LOCALE-03",
822
+ family: "LOCALE",
823
+ tier: 1,
824
+ severity: "error",
825
+ check(segment, ctx) {
826
+ if (segment.locale === "uk") return [];
827
+ const text = segment.text;
828
+ const m = CYRILLIC_SCRIPT.exec(text);
829
+ if (!m) return [];
830
+
831
+ // Check if the containing token is in allowedTokens
832
+ const matchIndex = m.index;
833
+ let tokenStart = matchIndex;
834
+ while (tokenStart > 0 && !/\s/.test(text[tokenStart - 1])) tokenStart--;
835
+ let tokenEnd = matchIndex + 1;
836
+ while (tokenEnd < text.length && !/\s/.test(text[tokenEnd])) tokenEnd++;
837
+ const token = text.slice(tokenStart, tokenEnd);
838
+
839
+ if (isAllowedToken(token, ctx)) return [];
840
+
841
+ // Check if this is a blockquote line (raw starts with >)
842
+ if (segment.source === "body" && /^\s*>/.test(segment.raw)) return [];
843
+
844
+ return [
845
+ finding(
846
+ this.id,
847
+ segment,
848
+ m[0],
849
+ m.index,
850
+ `Cyrillic letter "${m[0]}" in a non-Cyrillic locale (${segment.locale}).`,
851
+ "Replace with the Latin equivalent or wrap in a blockquote/fenced block.",
852
+ ),
853
+ ];
854
+ },
855
+ };
856
+
857
+ // ---------------------------------------------------------------------------
858
+ // TYPO-YAML-02: Odd number of straight double quotes in a text value
859
+ // Pattern: count of " in value is odd
860
+ // Exclusions: none
861
+ // ---------------------------------------------------------------------------
862
+
863
+ const yaml02: TypographyRule = {
864
+ id: "TYPO-YAML-02",
865
+ family: "YAML",
866
+ tier: 1,
867
+ severity: "error",
868
+ check(segment, _ctx) {
869
+ if (segment.source !== "frontmatter") return [];
870
+ const quoteCount = (segment.raw.match(/"/g) ?? []).length;
871
+ if (quoteCount % 2 !== 0) {
872
+ return [
873
+ finding(
874
+ this.id,
875
+ segment,
876
+ '"',
877
+ 0,
878
+ `Odd number of straight double quotes (${quoteCount}) in a text value.`,
879
+ "Balance the quotes — add or remove a double quote.",
880
+ ),
881
+ ];
882
+ }
883
+ return [];
884
+ },
885
+ };
886
+
887
+ // ---------------------------------------------------------------------------
888
+ // Export all Tier 1 rules
889
+ // ---------------------------------------------------------------------------
890
+
891
+ export const TIER1_RULES: readonly TypographyRule[] = [
892
+ punct01,
893
+ punct02,
894
+ punct03,
895
+ punct04,
896
+ space01,
897
+ space02,
898
+ space03,
899
+ space04,
900
+ case01,
901
+ case02,
902
+ locale01,
903
+ locale02,
904
+ locale03,
905
+ yaml02,
906
+ ];
907
+
908
+ // Note: TYPO-YAML-01 is not a rule function — it is emitted by the command
909
+ // adapter when the text-surface extractor returns a non-null yamlError.
910
+
911
+ export function createTypographyContext(
912
+ locale: string,
913
+ allowedTokens: ReadonlySet<string>,
914
+ abbreviationsOverride?: ReadonlySet<string>,
915
+ ): TypographyContext {
916
+ const defaults = getLocaleDefaults(locale);
917
+ return {
918
+ locale,
919
+ allowedTokens: new Set([...DEFAULT_ALLOWED_TOKENS, ...allowedTokens]),
920
+ abbreviations: abbreviationsOverride ?? defaults.abbreviations,
921
+ localeDefaults: defaults,
922
+ };
923
+ }
924
+
925
+ export { LOCALE_DEFAULTS, getLocaleDefaults };