@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,290 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for Tier 3 advisory typography rules (RFC-1071). Tests cover
4
+ all 5 rules: TYPO-UNICODE-01..02, TYPO-LINK-01..02, TYPO-SENT-01. Each rule
5
+ is tested for positive detection, negative (no finding), and key exclusions.</purpose>
6
+ <non-goals>
7
+ <item>Do not test text-surface extraction — that is text-surface.test.ts.</item>
8
+ <item>Do not test command adapter — that is typography.test.ts in werkstatt-site.</item>
9
+ </non-goals>
10
+ </MODULE_CONTRACT>
11
+ <CHANGE_SUMMARY>
12
+ <item>RFC-1071: initial creation with unit tests for 5 Tier 3 advisory rules.</item>
13
+ </CHANGE_SUMMARY>
14
+ */
15
+
16
+ import { describe, it, expect } from "vitest";
17
+ import { TIER3_ADVISORY_RULES, type Tier3AdvisoryRuleId } from "./rules-tier3-advisory.ts";
18
+ import type { TextSegment } from "./text-surface.ts";
19
+ import type { TypographyContext, TypographyRule, TypographyFinding } from "./rules-tier1.ts";
20
+ import { createTypographyContext } from "./rules-tier1.ts";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Helpers
24
+ // ---------------------------------------------------------------------------
25
+
26
+ function makeSegment(
27
+ text: string,
28
+ locale: string,
29
+ source: "frontmatter" | "body" = "body",
30
+ raw?: string,
31
+ ): TextSegment {
32
+ return {
33
+ file: `test.${locale}.md`,
34
+ line: 1,
35
+ path: "$body",
36
+ source,
37
+ text,
38
+ raw: raw ?? text,
39
+ isTableRow: false,
40
+ locale,
41
+ };
42
+ }
43
+
44
+ function makeCtx(
45
+ locale: string,
46
+ allowedTokens: ReadonlySet<string> = new Set(),
47
+ ): TypographyContext {
48
+ return createTypographyContext(locale, allowedTokens);
49
+ }
50
+
51
+ function runRule(
52
+ ruleId: Tier3AdvisoryRuleId,
53
+ text: string,
54
+ locale: string,
55
+ source: "frontmatter" | "body" = "body",
56
+ raw?: string,
57
+ ): TypographyFinding[] {
58
+ const rule = TIER3_ADVISORY_RULES.find((r) => r.id === ruleId) as TypographyRule;
59
+ const segment = makeSegment(text, locale, source, raw);
60
+ const ctx = makeCtx(locale);
61
+ return rule.check(segment, ctx);
62
+ }
63
+
64
+ function expectFinding(
65
+ ruleId: Tier3AdvisoryRuleId,
66
+ text: string,
67
+ locale: string,
68
+ raw?: string,
69
+ ): void {
70
+ const findings = runRule(ruleId, text, locale, "body", raw);
71
+ expect(findings.length).toBeGreaterThanOrEqual(1);
72
+ expect(findings[0].ruleId).toBe(ruleId);
73
+ }
74
+
75
+ function expectNoFinding(
76
+ ruleId: Tier3AdvisoryRuleId,
77
+ text: string,
78
+ locale: string,
79
+ source: "frontmatter" | "body" = "body",
80
+ raw?: string,
81
+ ): void {
82
+ const findings = runRule(ruleId, text, locale, source, raw);
83
+ expect(findings).toHaveLength(0);
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Verify all rules have tier 3 and severity warning
88
+ // ---------------------------------------------------------------------------
89
+
90
+ describe("TIER3_ADVISORY_RULES — metadata", () => {
91
+ it("has 5 rules", () => {
92
+ expect(TIER3_ADVISORY_RULES).toHaveLength(5);
93
+ });
94
+
95
+ it("all rules have tier 3 and severity warning", () => {
96
+ for (const rule of TIER3_ADVISORY_RULES) {
97
+ expect(rule.tier).toBe(3);
98
+ expect(rule.severity).toBe("warning");
99
+ }
100
+ });
101
+
102
+ it("has expected rule IDs", () => {
103
+ const ids = TIER3_ADVISORY_RULES.map((r) => r.id);
104
+ expect(ids).toContain("TYPO-UNICODE-01");
105
+ expect(ids).toContain("TYPO-UNICODE-02");
106
+ expect(ids).toContain("TYPO-LINK-01");
107
+ expect(ids).toContain("TYPO-LINK-02");
108
+ expect(ids).toContain("TYPO-SENT-01");
109
+ });
110
+ });
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // TYPO-UNICODE-01: Zero-width characters
114
+ // ---------------------------------------------------------------------------
115
+
116
+ describe("TYPO-UNICODE-01 — zero-width characters", () => {
117
+ it("detects U+200B (zero-width space)", () => {
118
+ expectFinding("TYPO-UNICODE-01", "Hello\u200BWorld", "en");
119
+ });
120
+
121
+ it("detects U+200C (zero-width non-joiner)", () => {
122
+ expectFinding("TYPO-UNICODE-01", "Hello\u200CWorld", "en");
123
+ });
124
+
125
+ it("detects U+200D (zero-width joiner)", () => {
126
+ expectFinding("TYPO-UNICODE-01", "Hello\u200DWorld", "en");
127
+ });
128
+
129
+ it("detects U+FEFF (BOM)", () => {
130
+ expectFinding("TYPO-UNICODE-01", "Hello\uFEFFWorld", "en");
131
+ });
132
+
133
+ it("does not flag clean text", () => {
134
+ expectNoFinding("TYPO-UNICODE-01", "Hello World", "en");
135
+ });
136
+
137
+ it("reports correct character name in message", () => {
138
+ const findings = runRule("TYPO-UNICODE-01", "Hello\u200BWorld", "en");
139
+ expect(findings[0].message).toContain("U+200B");
140
+ });
141
+ });
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // TYPO-UNICODE-02: Soft hyphen
145
+ // ---------------------------------------------------------------------------
146
+
147
+ describe("TYPO-UNICODE-02 — soft hyphen", () => {
148
+ it("detects U+00AD (soft hyphen)", () => {
149
+ expectFinding("TYPO-UNICODE-02", "Hello\u00ADWorld", "en");
150
+ });
151
+
152
+ it("does not flag clean text", () => {
153
+ expectNoFinding("TYPO-UNICODE-02", "Hello World", "en");
154
+ });
155
+
156
+ it("does not flag HTML entity &shy; (ASCII, not U+00AD)", () => {
157
+ expectNoFinding("TYPO-UNICODE-02", "Hello&shy;World", "en");
158
+ });
159
+ });
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // TYPO-LINK-01: Link text is a bare URL
163
+ // ---------------------------------------------------------------------------
164
+
165
+ describe("TYPO-LINK-01 — bare URL link text", () => {
166
+ it("detects [https://example.com](https://example.com)", () => {
167
+ const raw = "[https://example.com](https://example.com)";
168
+ const text = "[https://example.com]"; // extractor strips ](url)
169
+ expectFinding("TYPO-LINK-01", text, "en", raw);
170
+ });
171
+
172
+ it("detects [http://example.com](http://example.com)", () => {
173
+ const raw = "[http://example.com](http://example.com)";
174
+ const text = "[http://example.com]";
175
+ expectFinding("TYPO-LINK-01", text, "en", raw);
176
+ });
177
+
178
+ it("does not flag descriptive link text", () => {
179
+ const raw = "[Learn more](https://example.com)";
180
+ const text = "[Learn more]";
181
+ expectNoFinding("TYPO-LINK-01", text, "en", "body", raw);
182
+ });
183
+
184
+ it("does not flag frontmatter segments", () => {
185
+ const raw = "[https://example.com](https://example.com)";
186
+ const text = "[https://example.com]";
187
+ expectNoFinding("TYPO-LINK-01", text, "en", "frontmatter", raw);
188
+ });
189
+
190
+ it("does not flag bare URLs inside inline code spans", () => {
191
+ const raw = "`[https://example.com](https://example.com)`";
192
+ const text = "\uE002"; // extractor replaces code spans with placeholder
193
+ expectNoFinding("TYPO-LINK-01", text, "en", "body", raw);
194
+ });
195
+ });
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // TYPO-LINK-02: Link text is a generic phrase
199
+ // ---------------------------------------------------------------------------
200
+
201
+ describe("TYPO-LINK-02 — generic link text", () => {
202
+ it("detects 'click here'", () => {
203
+ const raw = "[click here](https://example.com)";
204
+ const text = "[click here]";
205
+ expectFinding("TYPO-LINK-02", text, "en", raw);
206
+ });
207
+
208
+ it("detects 'hier klicken'", () => {
209
+ const raw = "[hier klicken](https://example.com)";
210
+ const text = "[hier klicken]";
211
+ expectFinding("TYPO-LINK-02", text, "de", raw);
212
+ });
213
+
214
+ it("detects 'тут'", () => {
215
+ const raw = "[тут](https://example.com)";
216
+ const text = "[тут]";
217
+ expectFinding("TYPO-LINK-02", text, "uk", raw);
218
+ });
219
+
220
+ it("detects 'here' case-insensitive", () => {
221
+ const raw = "[HERE](https://example.com)";
222
+ const text = "[HERE]";
223
+ expectFinding("TYPO-LINK-02", text, "en", raw);
224
+ });
225
+
226
+ it("detects 'hier' (German)", () => {
227
+ const raw = "[hier](https://example.com)";
228
+ const text = "[hier]";
229
+ expectFinding("TYPO-LINK-02", text, "de", raw);
230
+ });
231
+
232
+ it("does not flag descriptive link text", () => {
233
+ const raw = "[Learn more about our services](https://example.com)";
234
+ const text = "[Learn more about our services]";
235
+ expectNoFinding("TYPO-LINK-02", text, "en", "body", raw);
236
+ });
237
+
238
+ it("does not flag 'here' within a longer phrase", () => {
239
+ const raw = "[See here for details](https://example.com)";
240
+ const text = "[See here for details]";
241
+ expectNoFinding("TYPO-LINK-02", text, "en", "body", raw);
242
+ });
243
+
244
+ it("does not flag frontmatter segments", () => {
245
+ const raw = "[click here](https://example.com)";
246
+ const text = "[click here]";
247
+ expectNoFinding("TYPO-LINK-02", text, "en", "frontmatter", raw);
248
+ });
249
+
250
+ it("does not flag generic phrases inside inline code spans", () => {
251
+ const raw = "`[click here](https://example.com)`";
252
+ const text = "\uE002";
253
+ expectNoFinding("TYPO-LINK-02", text, "en", "body", raw);
254
+ });
255
+ });
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // TYPO-SENT-01: Sentence longer than 40 words
259
+ // ---------------------------------------------------------------------------
260
+
261
+ function makeLongSentence(wordCount: number): string {
262
+ return Array.from({ length: wordCount }, (_, i) => `word${i}`).join(" ") + ".";
263
+ }
264
+
265
+ describe("TYPO-SENT-01 — long sentence", () => {
266
+ it("detects a sentence with 41 words", () => {
267
+ const text = makeLongSentence(41);
268
+ expectFinding("TYPO-SENT-01", text, "en");
269
+ });
270
+
271
+ it("does not flag a sentence with 40 words", () => {
272
+ const text = makeLongSentence(40);
273
+ expectNoFinding("TYPO-SENT-01", text, "en");
274
+ });
275
+
276
+ it("does not flag a short sentence", () => {
277
+ expectNoFinding("TYPO-SENT-01", "This is a short sentence.", "en");
278
+ });
279
+
280
+ it("does not flag frontmatter segments", () => {
281
+ const text = makeLongSentence(41);
282
+ expectNoFinding("TYPO-SENT-01", text, "en", "frontmatter");
283
+ });
284
+
285
+ it("counts words correctly with Unicode punctuation", () => {
286
+ const words = Array.from({ length: 41 }, (_, i) => `word${i}`).join(", ");
287
+ const text = words + ".";
288
+ expectFinding("TYPO-SENT-01", text, "en");
289
+ });
290
+ });
@@ -0,0 +1,239 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Tier 3 advisory typography rules (RFC-1071). Implements 5 rules across
4
+ 3 families: UNICODE (2), LINK (2), SENT (1). All rules emit severity: "warning"
5
+ and do not block commits. Rules operate on TextSegment objects produced by the
6
+ text-surface extractor (RFC-1068). UNICODE rules check segment.text; LINK rules
7
+ check segment.raw (which preserves markdown link syntax stripped from segment.text);
8
+ SENT rules check segment.text via splitSentences.</purpose>
9
+ <non-goals>
10
+ <item>Do not extract text — that is text-surface.ts.</item>
11
+ <item>Do not handle file I/O or command registration — that is the command adapter.</item>
12
+ <item>Do not auto-fix — autofix is RFC-1072.</item>
13
+ <item>Do not validate Tier 1 or Tier 2 rules — that is rules-tier1.ts and rules-tier2-*.ts.</item>
14
+ </non-goals>
15
+ </MODULE_CONTRACT>
16
+ <CHANGE_SUMMARY>
17
+ <item>RFC-1071: initial creation with 5 Tier 3 advisory rules.</item>
18
+ </CHANGE_SUMMARY>
19
+ */
20
+
21
+ import type { TextSegment } from "./text-surface.ts";
22
+ import { finding, type TypographyFinding, type TypographyRule } from "./rules-tier1.ts";
23
+ import { splitSentences } from "../semantic/extract.ts";
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Types
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export type Tier3AdvisoryRuleId = `TYPO-${"UNICODE" | "LINK" | "SENT"}-${string}`;
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Helpers
33
+ // ---------------------------------------------------------------------------
34
+
35
+ const INLINE_CODE_SPAN = /`[^`]*`/g;
36
+
37
+ function stripInlineCodeSpans(raw: string): string {
38
+ return raw.replace(INLINE_CODE_SPAN, "");
39
+ }
40
+
41
+ function countWords(text: string): number {
42
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
43
+ let count = 0;
44
+ for (const word of words) {
45
+ const stripped = word.replace(/\p{P}/gu, "");
46
+ if (stripped.length > 0) count++;
47
+ }
48
+ return count;
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // TYPO-UNICODE-01: Zero-width characters
53
+ // ---------------------------------------------------------------------------
54
+
55
+ const ZERO_WIDTH = /[\u200B\u200C\u200D\uFEFF]/gu;
56
+
57
+ const UNICODE_01: TypographyRule = {
58
+ id: "TYPO-UNICODE-01",
59
+ family: "UNICODE",
60
+ tier: 3,
61
+ severity: "warning",
62
+ check(segment: TextSegment): TypographyFinding[] {
63
+ const findings: TypographyFinding[] = [];
64
+ let match: RegExpExecArray | null;
65
+ const regex = new RegExp(ZERO_WIDTH.source, "gu");
66
+ while ((match = regex.exec(segment.text)) !== null) {
67
+ const charName =
68
+ match[0] === "\u200B"
69
+ ? "U+200B (zero-width space)"
70
+ : match[0] === "\u200C"
71
+ ? "U+200C (zero-width non-joiner)"
72
+ : match[0] === "\u200D"
73
+ ? "U+200D (zero-width joiner)"
74
+ : "U+FEFF (zero-width no-break space / BOM)";
75
+ findings.push(
76
+ finding(
77
+ "TYPO-UNICODE-01",
78
+ segment,
79
+ match[0],
80
+ match.index,
81
+ `Invisible character ${charName} in text.`,
82
+ "Remove the zero-width character from the source.",
83
+ ),
84
+ );
85
+ }
86
+ return findings;
87
+ },
88
+ };
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // TYPO-UNICODE-02: Soft hyphen (U+00AD)
92
+ // ---------------------------------------------------------------------------
93
+
94
+ const SOFT_HYPHEN = /\u00AD/gu;
95
+
96
+ const UNICODE_02: TypographyRule = {
97
+ id: "TYPO-UNICODE-02",
98
+ family: "UNICODE",
99
+ tier: 3,
100
+ severity: "warning",
101
+ check(segment: TextSegment): TypographyFinding[] {
102
+ const findings: TypographyFinding[] = [];
103
+ let match: RegExpExecArray | null;
104
+ const regex = new RegExp(SOFT_HYPHEN.source, "gu");
105
+ while ((match = regex.exec(segment.text)) !== null) {
106
+ findings.push(
107
+ finding(
108
+ "TYPO-UNICODE-02",
109
+ segment,
110
+ match[0],
111
+ match.index,
112
+ "Soft hyphen (U+00AD) in text — invisible in most editors but affects rendering.",
113
+ "Remove the soft hyphen from the source.",
114
+ ),
115
+ );
116
+ }
117
+ return findings;
118
+ },
119
+ };
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // TYPO-LINK-01: Link text is a bare URL
123
+ // ---------------------------------------------------------------------------
124
+
125
+ const BARE_URL_LINK = /\[https?:\/\/[^\]]+\]\(/giu;
126
+
127
+ const LINK_01: TypographyRule = {
128
+ id: "TYPO-LINK-01",
129
+ family: "LINK",
130
+ tier: 3,
131
+ severity: "warning",
132
+ check(segment: TextSegment): TypographyFinding[] {
133
+ if (segment.source !== "body") return [];
134
+ const findings: TypographyFinding[] = [];
135
+ const cleaned = stripInlineCodeSpans(segment.raw);
136
+ let match: RegExpExecArray | null;
137
+ const regex = new RegExp(BARE_URL_LINK.source, "giu");
138
+ while ((match = regex.exec(cleaned)) !== null) {
139
+ findings.push(
140
+ finding(
141
+ "TYPO-LINK-01",
142
+ segment,
143
+ match[0],
144
+ match.index,
145
+ "Link text is a bare URL — use descriptive link text instead.",
146
+ "Replace the URL in link text with a descriptive phrase.",
147
+ ),
148
+ );
149
+ }
150
+ return findings;
151
+ },
152
+ };
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // TYPO-LINK-02: Link text is a generic phrase
156
+ // ---------------------------------------------------------------------------
157
+
158
+ const LINK_TEXT_EXTRACTOR = /\[([^\]]+)\]\([^)]*\)/gu;
159
+ const GENERIC_LINK_PHRASES = /^(click here|hier klicken|тут|here|hier)$/i;
160
+
161
+ const LINK_02: TypographyRule = {
162
+ id: "TYPO-LINK-02",
163
+ family: "LINK",
164
+ tier: 3,
165
+ severity: "warning",
166
+ check(segment: TextSegment): TypographyFinding[] {
167
+ if (segment.source !== "body") return [];
168
+ const findings: TypographyFinding[] = [];
169
+ const cleaned = stripInlineCodeSpans(segment.raw);
170
+ let match: RegExpExecArray | null;
171
+ const regex = new RegExp(LINK_TEXT_EXTRACTOR.source, "gu");
172
+ while ((match = regex.exec(cleaned)) !== null) {
173
+ const linkText = match[1];
174
+ if (GENERIC_LINK_PHRASES.test(linkText)) {
175
+ findings.push(
176
+ finding(
177
+ "TYPO-LINK-02",
178
+ segment,
179
+ match[0],
180
+ match.index,
181
+ `Link text "${linkText}" is generic — use descriptive link text instead.`,
182
+ `Replace "${linkText}" with a phrase that describes the link target.`,
183
+ ),
184
+ );
185
+ }
186
+ }
187
+ return findings;
188
+ },
189
+ };
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // TYPO-SENT-01: Sentence longer than 40 words
193
+ // ---------------------------------------------------------------------------
194
+
195
+ const SENTENCE_WORD_LIMIT = 40;
196
+
197
+ const SENT_01: TypographyRule = {
198
+ id: "TYPO-SENT-01",
199
+ family: "SENT",
200
+ tier: 3,
201
+ severity: "warning",
202
+ check(segment: TextSegment): TypographyFinding[] {
203
+ if (segment.source !== "body") return [];
204
+ const findings: TypographyFinding[] = [];
205
+ const sentences = splitSentences(segment.text, segment.locale);
206
+ let searchOffset = 0;
207
+ for (const sentence of sentences) {
208
+ const wordCount = countWords(sentence);
209
+ if (wordCount > SENTENCE_WORD_LIMIT) {
210
+ const sentenceStart = segment.text.indexOf(sentence, searchOffset);
211
+ const column = sentenceStart >= 0 ? sentenceStart : 0;
212
+ if (sentenceStart >= 0) searchOffset = sentenceStart + sentence.length;
213
+ findings.push(
214
+ finding(
215
+ "TYPO-SENT-01",
216
+ segment,
217
+ sentence,
218
+ column,
219
+ `Sentence has ${wordCount} words (limit: ${SENTENCE_WORD_LIMIT}) — consider splitting for readability.`,
220
+ "Split the long sentence into shorter ones.",
221
+ ),
222
+ );
223
+ }
224
+ }
225
+ return findings;
226
+ },
227
+ };
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // Export
231
+ // ---------------------------------------------------------------------------
232
+
233
+ export const TIER3_ADVISORY_RULES: readonly TypographyRule[] = [
234
+ UNICODE_01,
235
+ UNICODE_02,
236
+ LINK_01,
237
+ LINK_02,
238
+ SENT_01,
239
+ ];