@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,330 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for Tier 2 structure typography rules (RFC-1069). Tests each
4
+ of the 9 rules (TYPO-HEAD-01..03, TYPO-PAIR-01..03, TYPO-MD-01..03) with
5
+ positive and negative cases, including edge cases for frontmatter heading
6
+ detection, link-stripping interactions, and placeholder exclusions.</purpose>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-1069: initial test suite for 9 Tier 2 structure rules.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ import { describe, it, expect } from "vitest";
14
+ import {
15
+ TIER2_STRUCTURE_RULES,
16
+ createTypographyContext,
17
+ type TextSegment,
18
+ type TypographyFinding,
19
+ } from "./index.ts";
20
+
21
+ function makeSegment(text: string, overrides: Partial<TextSegment> = {}): TextSegment {
22
+ return {
23
+ file: "test.md",
24
+ line: 1,
25
+ path: "$body",
26
+ source: "body",
27
+ text,
28
+ raw: text,
29
+ isTableRow: false,
30
+ locale: "de",
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ function makeFmSegment(
36
+ text: string,
37
+ path: string,
38
+ overrides: Partial<TextSegment> = {},
39
+ ): TextSegment {
40
+ return makeSegment(text, {
41
+ source: "frontmatter",
42
+ path,
43
+ ...overrides,
44
+ });
45
+ }
46
+
47
+ const ctx = createTypographyContext("de", new Set());
48
+
49
+ function runRule(ruleId: string, segment: TextSegment): TypographyFinding[] {
50
+ const rule = TIER2_STRUCTURE_RULES.find((r) => r.id === ruleId);
51
+ if (!rule) throw new Error(`Rule ${ruleId} not found`);
52
+ return rule.check(segment, ctx);
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // TYPO-HEAD-01: Heading ends with a period
57
+ // ---------------------------------------------------------------------------
58
+
59
+ describe("TYPO-HEAD-01", () => {
60
+ it("flags a body heading ending with a period", () => {
61
+ const seg = makeSegment("## My Heading.");
62
+ const findings = runRule("TYPO-HEAD-01", seg);
63
+ expect(findings).toHaveLength(1);
64
+ expect(findings[0].ruleId).toBe("TYPO-HEAD-01");
65
+ });
66
+
67
+ it("does not flag a body heading without trailing period", () => {
68
+ const seg = makeSegment("## My Heading");
69
+ expect(runRule("TYPO-HEAD-01", seg)).toHaveLength(0);
70
+ });
71
+
72
+ it("flags a frontmatter heading ending with a period", () => {
73
+ const seg = makeFmSegment("My Heading.", "$.blocks[0].props.heading");
74
+ expect(runRule("TYPO-HEAD-01", seg)).toHaveLength(1);
75
+ });
76
+
77
+ it("flags a frontmatter subheading ending with a period", () => {
78
+ const seg = makeFmSegment("My Subheading.", "$.blocks[0].props.subheading");
79
+ expect(runRule("TYPO-HEAD-01", seg)).toHaveLength(1);
80
+ });
81
+
82
+ it("does not flag a frontmatter non-heading key ending with a period", () => {
83
+ const seg = makeFmSegment("Some text.", "$.blocks[0].props.title");
84
+ expect(runRule("TYPO-HEAD-01", seg)).toHaveLength(0);
85
+ });
86
+
87
+ it("does not flag a non-heading body line ending with a period", () => {
88
+ const seg = makeSegment("This is a sentence.");
89
+ expect(runRule("TYPO-HEAD-01", seg)).toHaveLength(0);
90
+ });
91
+
92
+ // RFC-1083: multi-line body fields starting with ## are NOT headings
93
+ it("does not flag a multi-line body field starting with ##", () => {
94
+ const seg = makeSegment("## Heading\nMore body content.");
95
+ expect(runRule("TYPO-HEAD-01", seg)).toHaveLength(0);
96
+ });
97
+
98
+ it("still flags a single-line heading ending with a period", () => {
99
+ const seg = makeSegment("## Heading.");
100
+ expect(runRule("TYPO-HEAD-01", seg)).toHaveLength(1);
101
+ });
102
+ });
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // TYPO-HEAD-02: Heading ends with a colon or dash
106
+ // ---------------------------------------------------------------------------
107
+
108
+ describe("TYPO-HEAD-02", () => {
109
+ it("flags a heading ending with a colon", () => {
110
+ const seg = makeSegment("## Heading:");
111
+ expect(runRule("TYPO-HEAD-02", seg)).toHaveLength(1);
112
+ });
113
+
114
+ it("flags a heading ending with an em-dash", () => {
115
+ const seg = makeSegment("## Heading\u2014");
116
+ expect(runRule("TYPO-HEAD-02", seg)).toHaveLength(1);
117
+ });
118
+
119
+ it("flags a heading ending with an en-dash", () => {
120
+ const seg = makeSegment("## Heading\u2013");
121
+ expect(runRule("TYPO-HEAD-02", seg)).toHaveLength(1);
122
+ });
123
+
124
+ it("flags a heading ending with a hyphen", () => {
125
+ const seg = makeSegment("## Heading-");
126
+ expect(runRule("TYPO-HEAD-02", seg)).toHaveLength(1);
127
+ });
128
+
129
+ it("does not flag a heading without trailing colon or dash", () => {
130
+ const seg = makeSegment("## Heading");
131
+ expect(runRule("TYPO-HEAD-02", seg)).toHaveLength(0);
132
+ });
133
+ });
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // TYPO-HEAD-03: Heading ends with other closing punctuation
137
+ // ---------------------------------------------------------------------------
138
+
139
+ describe("TYPO-HEAD-03", () => {
140
+ it("flags a heading ending with !", () => {
141
+ const seg = makeSegment("## Heading!");
142
+ expect(runRule("TYPO-HEAD-03", seg)).toHaveLength(1);
143
+ });
144
+
145
+ it("flags a heading ending with ?", () => {
146
+ const seg = makeSegment("## Heading?");
147
+ expect(runRule("TYPO-HEAD-03", seg)).toHaveLength(1);
148
+ });
149
+
150
+ it("flags a heading ending with )", () => {
151
+ const seg = makeSegment("## Heading)");
152
+ expect(runRule("TYPO-HEAD-03", seg)).toHaveLength(1);
153
+ });
154
+
155
+ it("does not flag a heading without trailing closing punctuation", () => {
156
+ const seg = makeSegment("## Heading");
157
+ expect(runRule("TYPO-HEAD-03", seg)).toHaveLength(0);
158
+ });
159
+ });
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // TYPO-PAIR-01: Unbalanced round parentheses
163
+ // ---------------------------------------------------------------------------
164
+
165
+ describe("TYPO-PAIR-01", () => {
166
+ it("flags 3 opening and 2 closing parentheses", () => {
167
+ const seg = makeSegment("Text (foo (bar) baz");
168
+ const findings = runRule("TYPO-PAIR-01", seg);
169
+ expect(findings).toHaveLength(1);
170
+ expect(findings[0].ruleId).toBe("TYPO-PAIR-01");
171
+ });
172
+
173
+ it("does not flag balanced parentheses", () => {
174
+ const seg = makeSegment("Text (foo) and (bar)");
175
+ expect(runRule("TYPO-PAIR-01", seg)).toHaveLength(0);
176
+ });
177
+
178
+ it("does not flag text with formula placeholder", () => {
179
+ const seg = makeSegment("Text \uE000 end");
180
+ expect(runRule("TYPO-PAIR-01", seg)).toHaveLength(0);
181
+ });
182
+ });
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // TYPO-PAIR-02: Orphaned closing square bracket
186
+ // ---------------------------------------------------------------------------
187
+
188
+ describe("TYPO-PAIR-02", () => {
189
+ it("flags an orphaned ]", () => {
190
+ const seg = makeSegment("Text ] end");
191
+ expect(runRule("TYPO-PAIR-02", seg)).toHaveLength(1);
192
+ });
193
+
194
+ it("does not flag balanced [text]", () => {
195
+ const seg = makeSegment("Text [text] end");
196
+ expect(runRule("TYPO-PAIR-02", seg)).toHaveLength(0);
197
+ });
198
+
199
+ it("does not flag [text from a stripped link (orphaned [ is not a defect)", () => {
200
+ const seg = makeSegment("See [text for details");
201
+ expect(runRule("TYPO-PAIR-02", seg)).toHaveLength(0);
202
+ });
203
+ });
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // TYPO-PAIR-03: Unbalanced curly braces
207
+ // ---------------------------------------------------------------------------
208
+
209
+ describe("TYPO-PAIR-03", () => {
210
+ it("flags unbalanced curly braces (more opening)", () => {
211
+ const seg = makeSegment("Text {foo {bar}");
212
+ expect(runRule("TYPO-PAIR-03", seg)).toHaveLength(1);
213
+ });
214
+
215
+ it("flags unbalanced curly braces (more closing)", () => {
216
+ const seg = makeSegment("Text {foo}}");
217
+ expect(runRule("TYPO-PAIR-03", seg)).toHaveLength(1);
218
+ });
219
+
220
+ it("does not flag balanced curly braces", () => {
221
+ const seg = makeSegment("Text {foo} and {bar}");
222
+ expect(runRule("TYPO-PAIR-03", seg)).toHaveLength(0);
223
+ });
224
+
225
+ it("does not flag CMS placeholder (braces already stripped)", () => {
226
+ const seg = makeSegment("Text \uE002 end");
227
+ expect(runRule("TYPO-PAIR-03", seg)).toHaveLength(0);
228
+ });
229
+ });
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // TYPO-MD-01: Unclosed inline code span (odd backticks)
233
+ // ---------------------------------------------------------------------------
234
+
235
+ describe("TYPO-MD-01", () => {
236
+ it("flags an odd number of backticks", () => {
237
+ const seg = makeSegment("Text `code end");
238
+ expect(runRule("TYPO-MD-01", seg)).toHaveLength(1);
239
+ });
240
+
241
+ it("does not flag an even number of backticks", () => {
242
+ const seg = makeSegment("Text `code` end");
243
+ expect(runRule("TYPO-MD-01", seg)).toHaveLength(0);
244
+ });
245
+
246
+ it("does not flag frontmatter segments", () => {
247
+ const seg = makeFmSegment("Text `code", "$.blocks[0].props.text");
248
+ expect(runRule("TYPO-MD-01", seg)).toHaveLength(0);
249
+ });
250
+ });
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // TYPO-MD-02: Broken link target syntax
254
+ // ---------------------------------------------------------------------------
255
+
256
+ describe("TYPO-MD-02", () => {
257
+ it("flags ]( without preceding [", () => {
258
+ const seg = makeSegment("Text ](url end");
259
+ expect(runRule("TYPO-MD-02", seg)).toHaveLength(1);
260
+ });
261
+
262
+ it("does not flag a complete link after stripping", () => {
263
+ // After link stripping, [text](url) becomes [text — no ]( remains
264
+ const seg = makeSegment("See [text for details");
265
+ expect(runRule("TYPO-MD-02", seg)).toHaveLength(0);
266
+ });
267
+
268
+ it("does not flag frontmatter segments", () => {
269
+ const seg = makeFmSegment("Text ](url", "$.blocks[0].props.text");
270
+ expect(runRule("TYPO-MD-02", seg)).toHaveLength(0);
271
+ });
272
+ });
273
+
274
+ // ---------------------------------------------------------------------------
275
+ // TYPO-MD-03: Unclosed emphasis marker
276
+ // ---------------------------------------------------------------------------
277
+
278
+ describe("TYPO-MD-03", () => {
279
+ it("flags an odd number of asterisks", () => {
280
+ const seg = makeSegment("Text *bold end");
281
+ expect(runRule("TYPO-MD-03", seg)).toHaveLength(1);
282
+ });
283
+
284
+ it("does not flag an even number of asterisks", () => {
285
+ const seg = makeSegment("Text **bold** end");
286
+ expect(runRule("TYPO-MD-03", seg)).toHaveLength(0);
287
+ });
288
+
289
+ it("flags an odd number of underscores", () => {
290
+ const seg = makeSegment("Text _italic end");
291
+ expect(runRule("TYPO-MD-03", seg)).toHaveLength(1);
292
+ });
293
+
294
+ it("does not flag an even number of underscores", () => {
295
+ const seg = makeSegment("Text _italic_ end");
296
+ expect(runRule("TYPO-MD-03", seg)).toHaveLength(0);
297
+ });
298
+
299
+ it("does not flag frontmatter segments", () => {
300
+ const seg = makeFmSegment("Text *bold", "$.blocks[0].props.text");
301
+ expect(runRule("TYPO-MD-03", seg)).toHaveLength(0);
302
+ });
303
+ });
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // TIER2_STRUCTURE_RULES export
307
+ // ---------------------------------------------------------------------------
308
+
309
+ describe("TIER2_STRUCTURE_RULES", () => {
310
+ it("exports exactly 9 rules", () => {
311
+ expect(TIER2_STRUCTURE_RULES).toHaveLength(9);
312
+ });
313
+
314
+ it("all rules have tier 2", () => {
315
+ for (const rule of TIER2_STRUCTURE_RULES) {
316
+ expect(rule.tier).toBe(2);
317
+ }
318
+ });
319
+
320
+ it("all rules have severity error", () => {
321
+ for (const rule of TIER2_STRUCTURE_RULES) {
322
+ expect(rule.severity).toBe("error");
323
+ }
324
+ });
325
+
326
+ it("covers HEAD, PAIR, and MD families", () => {
327
+ const families = new Set(TIER2_STRUCTURE_RULES.map((r) => r.family));
328
+ expect(families).toEqual(new Set(["HEAD", "PAIR", "MD"]));
329
+ });
330
+ });
@@ -0,0 +1,378 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Tier 2 structure typography rules (RFC-1069). Implements 9 rules across
4
+ 3 families: HEAD (3), PAIR (3), MD (3). Rules operate on TextSegment objects
5
+ produced by the text-surface extractor (RFC-1068), never on raw file bytes.
6
+ All regexes use the `u` flag and Unicode property escapes where applicable.</purpose>
7
+ <non-goals>
8
+ <item>Do not extract text — that is text-surface.ts.</item>
9
+ <item>Do not handle file I/O or command registration — that is the command adapter.</item>
10
+ <item>Do not auto-fix — autofix is RFC-1072.</item>
11
+ <item>Do not validate heading hierarchy — that is a semantic concern, not typographic.</item>
12
+ <item>Do not validate list item punctuation — that is RFC-1067 (list.punctuation.validate).</item>
13
+ </non-goals>
14
+ </MODULE_CONTRACT>
15
+ <CHANGE_SUMMARY>
16
+ <item>RFC-1069: initial creation with 9 Tier 2 structure rules.</item>
17
+ </CHANGE_SUMMARY>
18
+ */
19
+
20
+ import type { TextSegment } from "./text-surface.ts";
21
+ import { finding, type TypographyFinding, type TypographyRule } from "./rules-tier1.ts";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Types
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export type Tier2StructureRuleId = `TYPO-${"HEAD" | "PAIR" | "MD"}-${string}`;
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Helper: extract last path segment from a JSON-path
31
+ // e.g. "$.blocks[0].props.heading" → "heading"
32
+ // ---------------------------------------------------------------------------
33
+
34
+ function lastPathKey(path: string): string | undefined {
35
+ const parts = path.split(".");
36
+ const last = parts[parts.length - 1];
37
+ if (!last || last.startsWith("$") || last.includes("[")) return undefined;
38
+ return last;
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Helper: check if a segment is a heading (body or frontmatter)
43
+ // ---------------------------------------------------------------------------
44
+
45
+ function isBodyHeading(text: string): boolean {
46
+ // A body segment is a heading only if it is a single line starting with #
47
+ // (RFC-1083). Multi-line body fields starting with ## are NOT headings —
48
+ // they are body content. The `m` flag is unnecessary because
49
+ // !text.includes("\n") guarantees a single-line string.
50
+ return /^#{1,6}\s+.+$/u.test(text) && !text.includes("\n");
51
+ }
52
+
53
+ function isFrontmatterHeading(segment: TextSegment): boolean {
54
+ if (segment.source !== "frontmatter") return false;
55
+ const key = lastPathKey(segment.path);
56
+ return key === "heading" || key === "subheading";
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // TYPO-HEAD-01: Heading ends with a period
61
+ // ---------------------------------------------------------------------------
62
+
63
+ const HEAD01_PERIOD = /[.]\s*$/u;
64
+
65
+ const head01: TypographyRule = {
66
+ id: "TYPO-HEAD-01",
67
+ family: "HEAD",
68
+ tier: 2,
69
+ severity: "error",
70
+ check(segment) {
71
+ const text = segment.text;
72
+ if (!isBodyHeading(text) && !isFrontmatterHeading(segment)) return [];
73
+ const match = HEAD01_PERIOD.exec(text);
74
+ if (!match) return [];
75
+ const col = match.index;
76
+ return [
77
+ finding(
78
+ "TYPO-HEAD-01",
79
+ segment,
80
+ match[0],
81
+ col,
82
+ "Heading ends with a period — a heading is a title, not a sentence.",
83
+ "Remove the trailing period.",
84
+ ),
85
+ ];
86
+ },
87
+ };
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // TYPO-HEAD-02: Heading ends with a colon or dash
91
+ // ---------------------------------------------------------------------------
92
+
93
+ const HEAD02_COLON_DASH = /[:\u2014\u2013-]\s*$/u;
94
+
95
+ const head02: TypographyRule = {
96
+ id: "TYPO-HEAD-02",
97
+ family: "HEAD",
98
+ tier: 2,
99
+ severity: "error",
100
+ check(segment) {
101
+ const text = segment.text;
102
+ if (!isBodyHeading(text) && !isFrontmatterHeading(segment)) return [];
103
+ const match = HEAD02_COLON_DASH.exec(text);
104
+ if (!match) return [];
105
+ const col = match.index;
106
+ return [
107
+ finding(
108
+ "TYPO-HEAD-02",
109
+ segment,
110
+ match[0],
111
+ col,
112
+ "Heading ends with a colon or dash — trailing punctuation is always wrong in a heading.",
113
+ "Remove the trailing colon or dash.",
114
+ ),
115
+ ];
116
+ },
117
+ };
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // TYPO-HEAD-03: Heading ends with other closing punctuation (!?)])
121
+ // ---------------------------------------------------------------------------
122
+
123
+ const HEAD03_OTHER = /[)!?]\s*$/u;
124
+
125
+ const head03: TypographyRule = {
126
+ id: "TYPO-HEAD-03",
127
+ family: "HEAD",
128
+ tier: 2,
129
+ severity: "error",
130
+ check(segment) {
131
+ const text = segment.text;
132
+ if (!isBodyHeading(text) && !isFrontmatterHeading(segment)) return [];
133
+ const match = HEAD03_OTHER.exec(text);
134
+ if (!match) return [];
135
+ const col = match.index;
136
+ return [
137
+ finding(
138
+ "TYPO-HEAD-03",
139
+ segment,
140
+ match[0],
141
+ col,
142
+ "Heading ends with closing punctuation (!, ?, or )) — a heading should not end with these marks.",
143
+ "Remove the trailing punctuation.",
144
+ ),
145
+ ];
146
+ },
147
+ };
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // TYPO-PAIR-01: Unbalanced round parentheses
151
+ // Exclusions: code spans (already stripped), formulas (replaced with placeholder)
152
+ // ---------------------------------------------------------------------------
153
+
154
+ function countChar(text: string, ch: string): number {
155
+ let count = 0;
156
+ for (const c of text) {
157
+ if (c === ch) count++;
158
+ }
159
+ return count;
160
+ }
161
+
162
+ const pair01: TypographyRule = {
163
+ id: "TYPO-PAIR-01",
164
+ family: "PAIR",
165
+ tier: 2,
166
+ severity: "error",
167
+ check(segment) {
168
+ const text = segment.text;
169
+ const open = countChar(text, "(");
170
+ const close = countChar(text, ")");
171
+ if (open === close) return [];
172
+ const col = open > close ? text.lastIndexOf("(") : text.lastIndexOf(")");
173
+ return [
174
+ finding(
175
+ "TYPO-PAIR-01",
176
+ segment,
177
+ open > close ? "(" : ")",
178
+ col,
179
+ `Unbalanced round parentheses — ${open} opening, ${close} closing.`,
180
+ "Add or remove a parenthesis to balance the pair.",
181
+ ),
182
+ ];
183
+ },
184
+ };
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // TYPO-PAIR-02: Orphaned closing square bracket
188
+ // After removing balanced [...] pairs, any ] remaining is an orphan.
189
+ // Exclusions: markdown link syntax (the link-stripping step removes ](...) leaving
190
+ // [text — orphaned [ is expected and not a defect); code spans
191
+ // ---------------------------------------------------------------------------
192
+
193
+ const pair02: TypographyRule = {
194
+ id: "TYPO-PAIR-02",
195
+ family: "PAIR",
196
+ tier: 2,
197
+ severity: "error",
198
+ check(segment) {
199
+ const text = segment.text;
200
+ // Find orphan ] by scanning the original string and skipping balanced [..] pairs.
201
+ let i = 0;
202
+ let bracketDepth = 0;
203
+ while (i < text.length) {
204
+ if (text[i] === "[") {
205
+ bracketDepth++;
206
+ } else if (text[i] === "]") {
207
+ if (bracketDepth === 0) {
208
+ return [
209
+ finding(
210
+ "TYPO-PAIR-02",
211
+ segment,
212
+ "]",
213
+ i,
214
+ "Orphaned closing square bracket — a ] without a matching [.",
215
+ "Add the opening [ or remove the orphaned ].",
216
+ ),
217
+ ];
218
+ }
219
+ bracketDepth--;
220
+ }
221
+ i++;
222
+ }
223
+ return [];
224
+ },
225
+ };
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // TYPO-PAIR-03: Unbalanced curly braces
229
+ // Exclusions: formula expressions (replaced with placeholder), frontmatter
230
+ // template variables (replaced with CMS placeholder by extractor)
231
+ // ---------------------------------------------------------------------------
232
+
233
+ const pair03: TypographyRule = {
234
+ id: "TYPO-PAIR-03",
235
+ family: "PAIR",
236
+ tier: 2,
237
+ severity: "error",
238
+ check(segment) {
239
+ const text = segment.text;
240
+ const open = countChar(text, "{");
241
+ const close = countChar(text, "}");
242
+ if (open === close) return [];
243
+ const col = open > close ? text.lastIndexOf("{") : text.lastIndexOf("}");
244
+ return [
245
+ finding(
246
+ "TYPO-PAIR-03",
247
+ segment,
248
+ open > close ? "{" : "}",
249
+ col,
250
+ `Unbalanced curly braces — ${open} opening, ${close} closing.`,
251
+ "Add or remove a brace to balance the pair.",
252
+ ),
253
+ ];
254
+ },
255
+ };
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // TYPO-MD-01: Unclosed inline code span (odd number of backticks)
259
+ // Exclusions: fenced code blocks (removed by extractor)
260
+ // ---------------------------------------------------------------------------
261
+
262
+ const md01: TypographyRule = {
263
+ id: "TYPO-MD-01",
264
+ family: "MD",
265
+ tier: 2,
266
+ severity: "error",
267
+ check(segment) {
268
+ if (segment.source !== "body") return [];
269
+ const text = segment.text;
270
+ const backtickCount = countChar(text, "`");
271
+ if (backtickCount % 2 === 0) return [];
272
+ const col = text.lastIndexOf("`");
273
+ return [
274
+ finding(
275
+ "TYPO-MD-01",
276
+ segment,
277
+ "`",
278
+ col,
279
+ `Unclosed inline code span — odd number of backticks (${backtickCount}).`,
280
+ "Add a closing backtick to match the opening.",
281
+ ),
282
+ ];
283
+ },
284
+ };
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // TYPO-MD-02: Broken link target syntax (](… without preceding [)
288
+ // Only checks ]( without a preceding ] on the same segment.
289
+ // Unclosed [text at end of segment is NOT checked because the link-stripping
290
+ // step removes ](...) making [text indistinguishable from a complete link.
291
+ // ---------------------------------------------------------------------------
292
+
293
+ const MD02_PATTERN = /(?<!\])\]\(/u;
294
+
295
+ const md02: TypographyRule = {
296
+ id: "TYPO-MD-02",
297
+ family: "MD",
298
+ tier: 2,
299
+ severity: "error",
300
+ check(segment) {
301
+ if (segment.source !== "body") return [];
302
+ const text = segment.text;
303
+ const match = MD02_PATTERN.exec(text);
304
+ if (!match) return [];
305
+ return [
306
+ finding(
307
+ "TYPO-MD-02",
308
+ segment,
309
+ match[0],
310
+ match.index,
311
+ "Broken link target syntax — ]( without a matching [.",
312
+ "Add the opening [ or remove the orphaned ](.",
313
+ ),
314
+ ];
315
+ },
316
+ };
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // TYPO-MD-03: Unclosed emphasis marker (odd count of * or _)
320
+ // Exclusions: code spans (already stripped); formulas (replaced with placeholder)
321
+ // ---------------------------------------------------------------------------
322
+
323
+ const md03: TypographyRule = {
324
+ id: "TYPO-MD-03",
325
+ family: "MD",
326
+ tier: 2,
327
+ severity: "error",
328
+ check(segment) {
329
+ if (segment.source !== "body") return [];
330
+ const text = segment.text;
331
+ const starCount = countChar(text, "*");
332
+ const underscoreCount = countChar(text, "_");
333
+ const findings: TypographyFinding[] = [];
334
+ if (starCount % 2 !== 0) {
335
+ const col = text.lastIndexOf("*");
336
+ findings.push(
337
+ finding(
338
+ "TYPO-MD-03",
339
+ segment,
340
+ "*",
341
+ col,
342
+ `Unclosed emphasis marker — odd number of asterisks (${starCount}).`,
343
+ "Add a closing * to match the opening.",
344
+ ),
345
+ );
346
+ }
347
+ if (underscoreCount % 2 !== 0) {
348
+ const col = text.lastIndexOf("_");
349
+ findings.push(
350
+ finding(
351
+ "TYPO-MD-03",
352
+ segment,
353
+ "_",
354
+ col,
355
+ `Unclosed emphasis marker — odd number of underscores (${underscoreCount}).`,
356
+ "Add a closing _ to match the opening.",
357
+ ),
358
+ );
359
+ }
360
+ return findings;
361
+ },
362
+ };
363
+
364
+ // ---------------------------------------------------------------------------
365
+ // Export all Tier 2 structure rules
366
+ // ---------------------------------------------------------------------------
367
+
368
+ export const TIER2_STRUCTURE_RULES: readonly TypographyRule[] = [
369
+ head01,
370
+ head02,
371
+ head03,
372
+ pair01,
373
+ pair02,
374
+ pair03,
375
+ md01,
376
+ md02,
377
+ md03,
378
+ ];