oh-my-opencode 5.1.45 → 5.1.46

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 (352) hide show
  1. package/.agents/command/publish.md +9 -9
  2. package/.agents/skills/publish/SKILL.md +9 -9
  3. package/.opencode/command/publish.md +9 -9
  4. package/README.de.md +0 -5
  5. package/README.es.md +0 -5
  6. package/README.fr.md +0 -5
  7. package/README.it.md +0 -5
  8. package/README.ja.md +0 -5
  9. package/README.ko.md +0 -5
  10. package/README.md +4 -5
  11. package/README.pt-br.md +0 -5
  12. package/README.ru.md +0 -6
  13. package/README.zh-cn.md +0 -5
  14. package/dist/cli/index.js +76 -156
  15. package/dist/cli-node/index.js +76 -156
  16. package/dist/config-migration/index.d.ts +2 -2
  17. package/dist/config-migration/migration-plans.d.ts +0 -10
  18. package/dist/index.js +1166 -1203
  19. package/dist/skills/ast-grep/AGENTS.md +1 -1
  20. package/dist/skills/browser/runtime/omowright/manifest.json +1 -1
  21. package/dist/skills/frontend/ATTRIBUTION.md +2 -2
  22. package/dist/skills/ultimate-browsing/engine/AGENTS.md +1 -1
  23. package/dist/skills/ulw-research/AGENTS.md +14 -2
  24. package/dist/skills/ulw-research/SKILL.md +37 -12
  25. package/dist/skills/ulw-research/references/deliverable-phase.md +73 -13
  26. package/dist/skills/ulw-research/references/fact-ledger-grammar.md +19 -1
  27. package/dist/skills/ulw-research/references/fact-ledger.md +95 -0
  28. package/dist/skills/ulw-research/references/fidelity-claim-kinds.md +19 -0
  29. package/dist/skills/ulw-research/references/report-gates.md +21 -4
  30. package/dist/skills/ulw-research/references/report-visualization.md +2 -0
  31. package/dist/skills/ulw-research/references/unit-rules.md +157 -0
  32. package/dist/skills/ulw-research/scripts/claim-kinds.mjs +116 -0
  33. package/dist/skills/ulw-research/scripts/claim-kinds.test.ts +193 -0
  34. package/dist/skills/ulw-research/scripts/cli-support.mjs +19 -5
  35. package/dist/skills/ulw-research/scripts/cli-support.test.ts +13 -2
  36. package/dist/skills/ulw-research/scripts/contracts.mjs +27 -6
  37. package/dist/skills/ulw-research/scripts/contracts.test.ts +3 -0
  38. package/dist/skills/ulw-research/scripts/copy-returns.test.ts +183 -0
  39. package/dist/skills/ulw-research/scripts/delivery-barrier.test.ts +245 -0
  40. package/dist/skills/ulw-research/scripts/fact-commands-meaning.mjs +191 -0
  41. package/dist/skills/ulw-research/scripts/fact-commands-verify.mjs +164 -0
  42. package/dist/skills/ulw-research/scripts/fact-commands.mjs +239 -0
  43. package/dist/skills/ulw-research/scripts/fact-derive.mjs +66 -0
  44. package/dist/skills/ulw-research/scripts/fact-derive.test.ts +375 -0
  45. package/dist/skills/ulw-research/scripts/fact-expr.mjs +108 -0
  46. package/dist/skills/ulw-research/scripts/fact-fill.mjs +194 -0
  47. package/dist/skills/ulw-research/scripts/fact-fill.test.ts +221 -0
  48. package/dist/skills/ulw-research/scripts/fact-ledger-grammar.test.ts +9 -2
  49. package/dist/skills/ulw-research/scripts/fact-ledger-kinds.mjs +127 -13
  50. package/dist/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +18 -3
  51. package/dist/skills/ulw-research/scripts/fact-ledger-legacy.test.ts +160 -0
  52. package/dist/skills/ulw-research/scripts/fact-ledger-schema.mjs +33 -16
  53. package/dist/skills/ulw-research/scripts/fact-ledger.mjs +29 -9
  54. package/dist/skills/ulw-research/scripts/fact-ledger.test.ts +2 -0
  55. package/dist/skills/ulw-research/scripts/fact-locate.mjs +231 -0
  56. package/dist/skills/ulw-research/scripts/fact-locate.test.ts +118 -0
  57. package/dist/skills/ulw-research/scripts/fact-pairs.mjs +187 -0
  58. package/dist/skills/ulw-research/scripts/fact-pairs.test.ts +167 -0
  59. package/dist/skills/ulw-research/scripts/fact-recompute.mjs +89 -0
  60. package/dist/skills/ulw-research/scripts/fact-unit-dimensions.test.ts +409 -0
  61. package/dist/skills/ulw-research/scripts/fact-unit-rules.test.ts +388 -0
  62. package/dist/skills/ulw-research/scripts/fidelity-binding.mjs +130 -0
  63. package/dist/skills/ulw-research/scripts/fidelity-binding.test.ts +81 -0
  64. package/dist/skills/ulw-research/scripts/fidelity-brief.mjs +5 -0
  65. package/dist/skills/ulw-research/scripts/fidelity-claims.mjs +89 -0
  66. package/dist/skills/ulw-research/scripts/fidelity-lexicon.mjs +145 -0
  67. package/dist/skills/ulw-research/scripts/fidelity-lexicon.test.ts +117 -0
  68. package/dist/skills/ulw-research/scripts/fidelity-records.mjs +163 -0
  69. package/dist/skills/ulw-research/scripts/fidelity-state.mjs +2 -1
  70. package/dist/skills/ulw-research/scripts/fidelity-trace.mjs +85 -0
  71. package/dist/skills/ulw-research/scripts/fidelity-trace.test.ts +58 -0
  72. package/dist/skills/ulw-research/scripts/fidelity-values.mjs +229 -0
  73. package/dist/skills/ulw-research/scripts/fidelity-values.test.ts +270 -0
  74. package/dist/skills/ulw-research/scripts/gates-fidelity.mjs +113 -0
  75. package/dist/skills/ulw-research/scripts/gates-fidelity.test.ts +73 -0
  76. package/dist/skills/ulw-research/scripts/gates-static.mjs +28 -8
  77. package/dist/skills/ulw-research/scripts/html-lite.mjs +2 -2
  78. package/dist/skills/ulw-research/scripts/html-markup.mjs +182 -0
  79. package/dist/skills/ulw-research/scripts/md-lite.mjs +193 -0
  80. package/dist/skills/ulw-research/scripts/md-lite.test.ts +271 -0
  81. package/dist/skills/ulw-research/scripts/meaning-check.mjs +196 -0
  82. package/dist/skills/ulw-research/scripts/meaning-check.test.ts +228 -0
  83. package/dist/skills/ulw-research/scripts/outcome.mjs +6 -5
  84. package/dist/skills/ulw-research/scripts/placeholder.mjs +184 -0
  85. package/dist/skills/ulw-research/scripts/placeholder.test.ts +77 -0
  86. package/dist/skills/ulw-research/scripts/polish-apply.mjs +115 -0
  87. package/dist/skills/ulw-research/scripts/polish-apply.test.ts +363 -0
  88. package/dist/skills/ulw-research/scripts/polish-diff.mjs +31 -0
  89. package/dist/skills/ulw-research/scripts/polish-diff.test.ts +168 -0
  90. package/dist/skills/ulw-research/scripts/polish-markup.test.ts +187 -0
  91. package/dist/skills/ulw-research/scripts/polish-project.mjs +125 -0
  92. package/dist/skills/ulw-research/scripts/polish-rules.mjs +170 -0
  93. package/dist/skills/ulw-research/scripts/report-tools-commands.mjs +19 -8
  94. package/dist/skills/ulw-research/scripts/report-tools.mjs +15 -0
  95. package/dist/skills/ulw-research/scripts/report-tools.test.ts +458 -2
  96. package/dist/skills/ulw-research/scripts/source-commands.mjs +140 -0
  97. package/dist/skills/ulw-research/scripts/source-snapshot.mjs +176 -0
  98. package/dist/skills/ulw-research/scripts/source-snapshot.test.ts +113 -0
  99. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/assets/share.csv +3 -0
  100. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/gen-legacy-dev.mjs +89 -0
  101. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-basic.json +223 -0
  102. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-legacy-dev.json +859 -0
  103. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-verify.json +420 -0
  104. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/report-bad.html +19 -0
  105. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/report-good.html +39 -0
  106. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/acronyms.json +3 -0
  107. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/meta.json +9 -0
  108. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/sentences.jsonl +7 -0
  109. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/text.txt +7 -0
  110. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4.html +19 -0
  111. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/acronyms.json +1 -0
  112. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/meta.json +9 -0
  113. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/sentences.jsonl +1 -0
  114. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/text.txt +1 -0
  115. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S9.txt +1 -0
  116. package/dist/skills/ulw-research/scripts/unit-algebra.mjs +101 -0
  117. package/dist/skills/ulw-research/scripts/unit-errors.mjs +23 -0
  118. package/dist/skills/ulw-research/scripts/unit-metadata.mjs +31 -0
  119. package/dist/skills/ulw-research/scripts/unit-pairs.mjs +72 -0
  120. package/dist/skills/ulw-research/scripts/unit-precision.mjs +44 -0
  121. package/dist/skills/ulw-research/scripts/unit-rules-shapes.mjs +64 -0
  122. package/dist/skills/ulw-research/scripts/unit-rules.mjs +173 -0
  123. package/dist/skills/ulw-research/scripts/units.mjs +4 -4
  124. package/dist/testing/module-mock-lifecycle.d.ts +1 -1
  125. package/dist/tui.js +32 -34
  126. package/package.json +13 -13
  127. package/packages/lsp-core/src/mcp-protocol-pin.test.ts +29 -0
  128. package/packages/lsp-core/src/mcp.ts +5 -2
  129. package/packages/lsp-daemon/dist/cli.js +3 -3
  130. package/packages/lsp-daemon/dist/client.js +3 -3
  131. package/packages/lsp-daemon/dist/index.js +3 -3
  132. package/packages/lsp-tools-mcp/dist/cli.js +3 -3
  133. package/packages/lsp-tools-mcp/dist/mcp.js +3 -3
  134. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  135. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +22 -12
  136. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  137. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  138. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  139. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  140. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  141. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  142. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  143. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  144. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  145. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  146. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  147. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  148. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  149. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  150. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  151. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  152. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  153. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  154. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  155. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  156. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  157. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  158. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  159. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  160. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  161. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  162. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  163. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  164. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  165. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  166. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  167. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  168. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  169. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  170. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  171. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  172. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  173. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  174. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  175. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  176. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  177. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  178. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  179. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  180. package/packages/omo-codex/plugin/package-lock.json +12 -12
  181. package/packages/omo-codex/plugin/package.json +1 -1
  182. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +8 -3
  183. package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +0 -2
  184. package/packages/omo-codex/plugin/skills/ast-grep/AGENTS.md +1 -1
  185. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  186. package/packages/omo-codex/plugin/skills/frontend/ATTRIBUTION.md +2 -2
  187. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +1 -1
  188. package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +14 -2
  189. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +36 -11
  190. package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +73 -13
  191. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger-grammar.md +19 -1
  192. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger.md +95 -0
  193. package/packages/omo-codex/plugin/skills/ulw-research/references/fidelity-claim-kinds.md +19 -0
  194. package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +21 -4
  195. package/packages/omo-codex/plugin/skills/ulw-research/references/report-visualization.md +2 -0
  196. package/packages/omo-codex/plugin/skills/ulw-research/references/unit-rules.md +157 -0
  197. package/packages/omo-codex/plugin/skills/ulw-research/scripts/claim-kinds.mjs +116 -0
  198. package/packages/omo-codex/plugin/skills/ulw-research/scripts/cli-support.mjs +19 -5
  199. package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +27 -6
  200. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-meaning.mjs +191 -0
  201. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-verify.mjs +164 -0
  202. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands.mjs +239 -0
  203. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-derive.mjs +66 -0
  204. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-expr.mjs +108 -0
  205. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-fill.mjs +194 -0
  206. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-kinds.mjs +127 -13
  207. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-schema.mjs +33 -16
  208. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger.mjs +29 -9
  209. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-locate.mjs +231 -0
  210. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-pairs.mjs +187 -0
  211. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-recompute.mjs +89 -0
  212. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-binding.mjs +130 -0
  213. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-brief.mjs +5 -0
  214. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-claims.mjs +89 -0
  215. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-lexicon.mjs +145 -0
  216. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-records.mjs +163 -0
  217. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-state.mjs +2 -1
  218. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-trace.mjs +85 -0
  219. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-values.mjs +229 -0
  220. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-fidelity.mjs +113 -0
  221. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +28 -8
  222. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-lite.mjs +2 -2
  223. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-markup.mjs +182 -0
  224. package/packages/omo-codex/plugin/skills/ulw-research/scripts/md-lite.mjs +193 -0
  225. package/packages/omo-codex/plugin/skills/ulw-research/scripts/meaning-check.mjs +196 -0
  226. package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +6 -5
  227. package/packages/omo-codex/plugin/skills/ulw-research/scripts/placeholder.mjs +184 -0
  228. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-apply.mjs +115 -0
  229. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-diff.mjs +31 -0
  230. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-project.mjs +125 -0
  231. package/packages/omo-codex/plugin/skills/ulw-research/scripts/polish-rules.mjs +170 -0
  232. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +19 -8
  233. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools.mjs +15 -0
  234. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +140 -0
  235. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-snapshot.mjs +176 -0
  236. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-algebra.mjs +101 -0
  237. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-errors.mjs +23 -0
  238. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-metadata.mjs +31 -0
  239. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-pairs.mjs +72 -0
  240. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-precision.mjs +44 -0
  241. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-rules-shapes.mjs +64 -0
  242. package/packages/omo-codex/plugin/skills/ulw-research/scripts/unit-rules.mjs +173 -0
  243. package/packages/omo-codex/plugin/skills/ulw-research/scripts/units.mjs +4 -4
  244. package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +19 -0
  245. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +7 -3
  246. package/packages/omo-codex/scripts/install-dist/install-local.mjs +26 -16
  247. package/packages/shared-skills/skills/ast-grep/AGENTS.md +1 -1
  248. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  249. package/packages/shared-skills/skills/frontend/ATTRIBUTION.md +2 -2
  250. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +1 -1
  251. package/packages/shared-skills/skills/ulw-research/AGENTS.md +14 -2
  252. package/packages/shared-skills/skills/ulw-research/SKILL.md +37 -12
  253. package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +73 -13
  254. package/packages/shared-skills/skills/ulw-research/references/fact-ledger-grammar.md +19 -1
  255. package/packages/shared-skills/skills/ulw-research/references/fact-ledger.md +95 -0
  256. package/packages/shared-skills/skills/ulw-research/references/fidelity-claim-kinds.md +19 -0
  257. package/packages/shared-skills/skills/ulw-research/references/report-gates.md +21 -4
  258. package/packages/shared-skills/skills/ulw-research/references/report-visualization.md +2 -0
  259. package/packages/shared-skills/skills/ulw-research/references/unit-rules.md +157 -0
  260. package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.mjs +116 -0
  261. package/packages/shared-skills/skills/ulw-research/scripts/claim-kinds.test.ts +193 -0
  262. package/packages/shared-skills/skills/ulw-research/scripts/cli-support.mjs +19 -5
  263. package/packages/shared-skills/skills/ulw-research/scripts/cli-support.test.ts +13 -2
  264. package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +27 -6
  265. package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +3 -0
  266. package/packages/shared-skills/skills/ulw-research/scripts/copy-returns.test.ts +183 -0
  267. package/packages/shared-skills/skills/ulw-research/scripts/delivery-barrier.test.ts +245 -0
  268. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-meaning.mjs +191 -0
  269. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-verify.mjs +164 -0
  270. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands.mjs +239 -0
  271. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.mjs +66 -0
  272. package/packages/shared-skills/skills/ulw-research/scripts/fact-derive.test.ts +375 -0
  273. package/packages/shared-skills/skills/ulw-research/scripts/fact-expr.mjs +108 -0
  274. package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.mjs +194 -0
  275. package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.test.ts +221 -0
  276. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-grammar.test.ts +9 -2
  277. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.mjs +127 -13
  278. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +18 -3
  279. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-legacy.test.ts +160 -0
  280. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.mjs +33 -16
  281. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.mjs +29 -9
  282. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.test.ts +2 -0
  283. package/packages/shared-skills/skills/ulw-research/scripts/fact-locate.mjs +231 -0
  284. package/packages/shared-skills/skills/ulw-research/scripts/fact-locate.test.ts +118 -0
  285. package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.mjs +187 -0
  286. package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.test.ts +167 -0
  287. package/packages/shared-skills/skills/ulw-research/scripts/fact-recompute.mjs +89 -0
  288. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-dimensions.test.ts +409 -0
  289. package/packages/shared-skills/skills/ulw-research/scripts/fact-unit-rules.test.ts +388 -0
  290. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-binding.mjs +130 -0
  291. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-binding.test.ts +81 -0
  292. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.mjs +5 -0
  293. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-claims.mjs +89 -0
  294. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-lexicon.mjs +145 -0
  295. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-lexicon.test.ts +117 -0
  296. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-records.mjs +163 -0
  297. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.mjs +2 -1
  298. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-trace.mjs +85 -0
  299. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-trace.test.ts +58 -0
  300. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.mjs +229 -0
  301. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-values.test.ts +270 -0
  302. package/packages/shared-skills/skills/ulw-research/scripts/gates-fidelity.mjs +113 -0
  303. package/packages/shared-skills/skills/ulw-research/scripts/gates-fidelity.test.ts +73 -0
  304. package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +28 -8
  305. package/packages/shared-skills/skills/ulw-research/scripts/html-lite.mjs +2 -2
  306. package/packages/shared-skills/skills/ulw-research/scripts/html-markup.mjs +182 -0
  307. package/packages/shared-skills/skills/ulw-research/scripts/md-lite.mjs +193 -0
  308. package/packages/shared-skills/skills/ulw-research/scripts/md-lite.test.ts +271 -0
  309. package/packages/shared-skills/skills/ulw-research/scripts/meaning-check.mjs +196 -0
  310. package/packages/shared-skills/skills/ulw-research/scripts/meaning-check.test.ts +228 -0
  311. package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +6 -5
  312. package/packages/shared-skills/skills/ulw-research/scripts/placeholder.mjs +184 -0
  313. package/packages/shared-skills/skills/ulw-research/scripts/placeholder.test.ts +77 -0
  314. package/packages/shared-skills/skills/ulw-research/scripts/polish-apply.mjs +115 -0
  315. package/packages/shared-skills/skills/ulw-research/scripts/polish-apply.test.ts +363 -0
  316. package/packages/shared-skills/skills/ulw-research/scripts/polish-diff.mjs +31 -0
  317. package/packages/shared-skills/skills/ulw-research/scripts/polish-diff.test.ts +168 -0
  318. package/packages/shared-skills/skills/ulw-research/scripts/polish-markup.test.ts +187 -0
  319. package/packages/shared-skills/skills/ulw-research/scripts/polish-project.mjs +125 -0
  320. package/packages/shared-skills/skills/ulw-research/scripts/polish-rules.mjs +170 -0
  321. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +19 -8
  322. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.mjs +15 -0
  323. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +458 -2
  324. package/packages/shared-skills/skills/ulw-research/scripts/source-commands.mjs +140 -0
  325. package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.mjs +176 -0
  326. package/packages/shared-skills/skills/ulw-research/scripts/source-snapshot.test.ts +113 -0
  327. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/assets/share.csv +3 -0
  328. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/gen-legacy-dev.mjs +89 -0
  329. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-basic.json +223 -0
  330. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-legacy-dev.json +859 -0
  331. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/ledger-verify.json +420 -0
  332. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/report-bad.html +19 -0
  333. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/report-good.html +39 -0
  334. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/acronyms.json +3 -0
  335. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/meta.json +9 -0
  336. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/sentences.jsonl +7 -0
  337. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4/text.txt +7 -0
  338. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S4.html +19 -0
  339. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/acronyms.json +1 -0
  340. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/meta.json +9 -0
  341. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/sentences.jsonl +1 -0
  342. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S5/text.txt +1 -0
  343. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/sources/S9.txt +1 -0
  344. package/packages/shared-skills/skills/ulw-research/scripts/unit-algebra.mjs +101 -0
  345. package/packages/shared-skills/skills/ulw-research/scripts/unit-errors.mjs +23 -0
  346. package/packages/shared-skills/skills/ulw-research/scripts/unit-metadata.mjs +31 -0
  347. package/packages/shared-skills/skills/ulw-research/scripts/unit-pairs.mjs +72 -0
  348. package/packages/shared-skills/skills/ulw-research/scripts/unit-precision.mjs +44 -0
  349. package/packages/shared-skills/skills/ulw-research/scripts/unit-rules-shapes.mjs +64 -0
  350. package/packages/shared-skills/skills/ulw-research/scripts/unit-rules.mjs +173 -0
  351. package/packages/shared-skills/skills/ulw-research/scripts/units.mjs +4 -4
  352. package/dist/config-migration/dori-section-rename.d.ts +0 -3
@@ -0,0 +1,130 @@
1
+ // fidelity-binding.mjs - sentence binding and claim-status checks of the fidelity gate.
2
+ //
3
+ // proseSentences walks every leaf prose element (p, li) outside figures, tables and page chrome,
4
+ // keeps the offsets of each data-fact element inside its text, and splits it into sentences, so
5
+ // every check sees which ledger rows a sentence cites. bindingChecks raises unbound_sentence (a
6
+ // findings sentence with no fact, placeholder, citation, or data-claim), claim_status_invalid (a
7
+ // data-claim id that is unknown, refuted or unresolved outside a Gaps or Contradictions section,
8
+ // or high-risk and missing from verified-claims), and conflicting_readings (two active rows with
9
+ // one value and unit but different periods or qualifiers cited in one sentence). Plain Node ESM.
10
+
11
+ import { proseText } from "./html-lite.mjs"
12
+ import { fromDecimal, eq } from "./rational.mjs"
13
+ import { splitSentences } from "./sentence-split.mjs"
14
+ import { normalizeUnit } from "./units.mjs"
15
+
16
+ const SKIP = new Set(["code", "pre", "kbd", "samp", "script", "style", "svg", "template", "noscript"])
17
+ const CONTAINERS = new Set(["figure", "table", "nav", "header", "footer"])
18
+ /** data-kind values whose sentences need no binding (opinion and recommendation blocks). */
19
+ export const EXEMPT_KINDS = Object.freeze(["opinion", "recommendation"])
20
+ export const isExemptKind = (kind) => EXEMPT_KINDS.includes(kind)
21
+ const DECIMAL = /^-?\d+(?:\.\d+)?$/
22
+ /** Gaps and Contradictions headings (English, and the Korean gap/limit/contradiction/conflict words). */
23
+ const GAPS_HEADING = /\b(?:gaps?|contradictions?|unresolved)\b|\uACF5\uBC31|\uD55C\uACC4|\uBAA8\uC21C|\uC0C1\uCDA9/i
24
+
25
+ /** Canonical unit token, or the raw unit without spaces, lowercased; "" for none. */
26
+ export function unitKey(unit) {
27
+ if (unit === null || unit === undefined) return ""
28
+ return normalizeUnit(String(unit)) ?? String(unit).replace(/\s+/g, "").toLowerCase()
29
+ }
30
+
31
+ /** Exact decimal equality; false when either side is not a plain decimal. */
32
+ export function sameValue(a, b) {
33
+ const [x, y] = [String(a ?? "").replace(/,/g, ""), String(b ?? "").replace(/,/g, "")]
34
+ return DECIMAL.test(x) && DECIMAL.test(y) && eq(fromDecimal(x), fromDecimal(y))
35
+ }
36
+
37
+ function serialize(node, out) {
38
+ for (const child of node.children) {
39
+ if (child.tag === "#text") out.text += child.text
40
+ else if (child.tag === "br") out.text += " "
41
+ else if (SKIP.has(child.tag)) continue
42
+ else if (child.attrs["data-fact"] !== undefined) {
43
+ const text = proseText(child)
44
+ out.facts.push({ id: child.attrs["data-fact"], start: out.text.length, end: out.text.length + text.length, text, node: child })
45
+ out.text += text
46
+ } else serialize(child, out)
47
+ }
48
+ return out
49
+ }
50
+
51
+ /**
52
+ * Leaf prose elements with their sentences and the facts each sentence cites.
53
+ * @returns {{ owner: object, scope: object, kind: string|null, sentences: { text: string, start: number, end: number, facts: object[] }[] }[]}
54
+ */
55
+ export function proseSentences(ctx) {
56
+ const { closest, descendants } = ctx.util
57
+ const blocks = []
58
+ for (const { node, scope } of ctx.elements) {
59
+ if (node.tag !== "p" && node.tag !== "li") continue
60
+ if (node.tag === "li" && descendants(node).some((child) => child.tag === "p" || child.tag === "li")) continue
61
+ if (closest(node, (cur) => CONTAINERS.has(cur.tag))) continue
62
+ const { text, facts } = serialize(node, { text: "", facts: [] })
63
+ const sentences = splitSentences(text).map((s) => ({ ...s, facts: facts.filter((f) => f.start < s.end && f.end > s.start) }))
64
+ blocks.push({ owner: node, scope, kind: ctx.kindOf(scope), sentences })
65
+ }
66
+ return blocks
67
+ }
68
+
69
+ function claimList(claims) {
70
+ const list = Array.isArray(claims) ? claims : (claims?.claims ?? [])
71
+ return new Map(list.map((claim) => [claim.claim_id, claim]))
72
+ }
73
+
74
+ const selfOrUp = (ctx, node, test) => test(node) || Boolean(ctx.util.closest(node, test))
75
+
76
+ function claimDefects(ctx, claims) {
77
+ const byId = claimList(claims)
78
+ const out = []
79
+ for (const { node, scope } of ctx.elements) {
80
+ const attr = node.attrs["data-claim"]
81
+ if (attr === undefined) continue
82
+ const heading = scope.section?.heading
83
+ const inGaps = Boolean(heading) && GAPS_HEADING.test(proseText(heading))
84
+ for (const id of attr.split(/[\s,]+/).filter(Boolean)) {
85
+ const claim = byId.get(id)
86
+ let why = null
87
+ if (!claim) why = `data-claim ${id} is not in the claim index`
88
+ else if (/refuted|unresolved/i.test(claim.status) && !inGaps) why = `claim ${id} is ${claim.status} but asserted outside Gaps or Contradictions`
89
+ else if (!inGaps && /high/i.test(claim.risk) && !claim.in_verified) why = `high-risk claim ${id} is not in verified-claims`
90
+ if (why) out.push(ctx.defect("claim_status_invalid", node, why))
91
+ }
92
+ }
93
+ return out
94
+ }
95
+
96
+ function conflict(rows, facts) {
97
+ const cited = [...new Set(facts.map((f) => f.id))].map((id) => rows.get(id)).filter((row) => row && row.status === "active" && row.kind !== "quote")
98
+ for (let i = 0; i < cited.length; i++) {
99
+ for (let k = i + 1; k < cited.length; k++) {
100
+ const [a, b] = [cited[i], cited[k]]
101
+ if (!sameValue(a.value, b.value) || unitKey(a.unit) !== unitKey(b.unit)) continue
102
+ if (a.valid_at !== b.valid_at || a.qualifier !== b.qualifier) return [a, b]
103
+ }
104
+ }
105
+ return null
106
+ }
107
+
108
+ /**
109
+ * @param {object} ctx buildContext(...) from gates-static.mjs
110
+ * @param {{ claims: object[] } | object[] | null} claims indexClaims(...) output or its claims array
111
+ * @param {{ rows: object[] } | null} ledger
112
+ */
113
+ export function bindingChecks(ctx, claims, ledger) {
114
+ const rows = new Map((ledger?.rows ?? []).map((row) => [row.fact_id, row]))
115
+ const out = []
116
+ for (const block of proseSentences(ctx)) {
117
+ const findings = Boolean(block.scope.section) && block.kind === null
118
+ const exempt = selfOrUp(ctx, block.owner, (cur) => isExemptKind(cur.attrs["data-kind"]) || cur.attrs["data-claim"] !== undefined)
119
+ for (const sentence of block.sentences) {
120
+ const bound = sentence.facts.length > 0 || ctx.util.citationMarker.test(sentence.text) || sentence.text.includes("{{")
121
+ if (findings && !exempt && !bound) out.push(ctx.defect("unbound_sentence", block.owner, `no fact, citation, or data-claim binds: "${sentence.text.slice(0, 80)}"`))
122
+ const pair = conflict(rows, sentence.facts)
123
+ if (pair) {
124
+ const [a, b] = pair
125
+ out.push(ctx.defect("conflicting_readings", block.owner, `${a.fact_id} (${a.valid_at ?? "no period"}) and ${b.fact_id} (${b.valid_at ?? "no period"}) share ${a.value} ${a.unit ?? ""} in one sentence`))
126
+ }
127
+ }
128
+ }
129
+ return [...out, ...claimDefects(ctx, claims)]
130
+ }
@@ -109,6 +109,11 @@ export function isLocked(parsed) {
109
109
  return FIDELITY_FIELDS.every((field) => Object.hasOwn(parsed.fields, field) && LOCKED_ANSWERED_BY.has(parsed.answered_by[field]))
110
110
  }
111
111
 
112
+ /** The brief's `meaning check` field turns the meaning verifier off only when it reads "off". */
113
+ export function meaningCheckEnabled(parsed) {
114
+ return !/^off\b/iu.test(parsed?.fields?.["meaning check"] ?? "")
115
+ }
116
+
112
117
  function incompleteDefect(briefPath, parsed) {
113
118
  const reasons = []
114
119
  if (!parsed.present) reasons.push("no ## Fidelity block")
@@ -0,0 +1,89 @@
1
+ // fidelity-claims.mjs - the claim-kind checks of the deterministic fidelity gate.
2
+ //
3
+ // A mark (the data-claim-kind attribute) binds per cited fact: a data-fact element takes the kind
4
+ // of its nearest mark (its own, else the closest marked ancestor), and only that kind is checked
5
+ // against its ledger row (claim-kinds.mjs CLAIM_KINDS); a row that fails is the kind's defect
6
+ // (unsupported_verification for `verified`). A fact under two marks of different kinds is a
7
+ // claim_kind_mismatch, and a mark that owns no fact of its own kind (every fact under it cites
8
+ // nothing or is shadowed by another kind) is its kind's defect: shadowing never satisfies a parent.
9
+ // An unmarked fact citing a row neither fact nor quote accepts (an assumed row) is a
10
+ // claim_kind_mismatch naming the mark it needs. A sentence that matches a kind's unmarked patterns warns (minor, verification_claim_unmarked)
11
+ // unless every fact it cites is marked that kind; a sentence citing no fact warns unless its block
12
+ // is marked. No model, no network.
13
+
14
+ import { CLAIM_ATTRIBUTE, CLAIM_KINDS, claimSatisfied, unmarkedKinds, unmarkedRequires } from "./claim-kinds.mjs"
15
+ import { proseSentences } from "./fidelity-binding.mjs"
16
+ import { clip } from "./fidelity-trace.mjs"
17
+ import { proseText } from "./html-lite.mjs"
18
+
19
+ const marked = (node) => node.attrs?.[CLAIM_ATTRIBUTE] !== undefined
20
+ const kindOf = (node) => node.attrs[CLAIM_ATTRIBUTE].trim()
21
+ const isFact = (node) => node.attrs["data-fact"] !== undefined
22
+
23
+ /** The marks over a node, nearest first: its own mark, then each marked ancestor. */
24
+ function marksOver(node) {
25
+ const marks = []
26
+ for (let cur = node; cur; cur = cur.parent) if (marked(cur)) marks.push(cur)
27
+ return marks
28
+ }
29
+
30
+ const nearestKind = (node) => {
31
+ const [mark] = marksOver(node)
32
+ return mark ? kindOf(mark) : null
33
+ }
34
+
35
+ /** Whether `mark` binds `fact`: every mark from the fact up to `mark` names the same kind. */
36
+ function binds(mark, fact) {
37
+ const marks = marksOver(fact)
38
+ const at = marks.indexOf(mark)
39
+ return at >= 0 && marks.slice(0, at + 1).every((cur) => kindOf(cur) === kindOf(mark))
40
+ }
41
+
42
+ /**
43
+ * @param {object} ctx buildContext(...) from gates-static.mjs
44
+ * @param {Map<string, object>} rows ledger rows by fact_id
45
+ * @param {(row: object) => string | undefined} provenanceOf meta.json kind of the snapshot holding the row's own locator artifact
46
+ * @param {(code: string, node: object, message: string) => void} flag
47
+ */
48
+ export function claimChecks(ctx, rows, provenanceOf, flag) {
49
+ for (const node of ctx.all.filter(marked)) {
50
+ const name = kindOf(node)
51
+ const entry = CLAIM_KINDS[name]
52
+ if (!entry) {
53
+ flag("claim_kind_mismatch", node, `${CLAIM_ATTRIBUTE}="${name}" is not a claim kind (${Object.keys(CLAIM_KINDS).join(", ")})`)
54
+ continue
55
+ }
56
+ const cited = [node, ...ctx.util.descendants(node)].filter(isFact)
57
+ if (cited.some((fact) => binds(node, fact))) continue
58
+ const why = cited.length === 0 ? "cites no fact" : "cites no fact of its own (a nested mark of another kind never satisfies it)"
59
+ flag(entry.defect, node, `"${clip(proseText(node))}" is marked ${name} but ${why}`)
60
+ }
61
+ for (const fact of ctx.all.filter(isFact)) {
62
+ const [nearest, ...outer] = marksOver(fact)
63
+ const id = fact.attrs["data-fact"]
64
+ if (!nearest) {
65
+ const need = unmarkedRequires(rows.get(id))
66
+ const mark = need && `{{${CLAIM_KINDS[need].prefix}:${id}}} (${CLAIM_ATTRIBUTE}="${need}")`
67
+ if (need) flag("claim_kind_mismatch", fact, `${id} is a ${rows.get(id).kind} row cited with no mark; an unmarked numeral reads as a fact, so mark it ${mark}`)
68
+ continue
69
+ }
70
+ const name = kindOf(nearest)
71
+ const conflict = outer.find((mark) => kindOf(mark) !== name)
72
+ if (conflict) flag("claim_kind_mismatch", fact, `${id} is marked ${name} inside a ${kindOf(conflict)} mark; a fact carries one claim kind`)
73
+ const entry = CLAIM_KINDS[name]
74
+ const row = rows.get(id)
75
+ if (!entry || !row || claimSatisfied(name, row, provenanceOf(row))) continue
76
+ const source = entry.sourceKinds ? ` and not one whose own locator artifact sits in a ${entry.sourceKinds.join(" or ")} snapshot` : ""
77
+ flag(entry.defect, fact, `${row.fact_id} is marked ${name}, but it is a ${row.kind} row${source}`)
78
+ }
79
+ for (const block of proseSentences(ctx)) {
80
+ if (block.kind === "sources" || block.kind === "toc") continue
81
+ for (const sentence of block.sentences) {
82
+ for (const name of unmarkedKinds(sentence.text)) {
83
+ const covered = sentence.facts.length > 0 ? sentence.facts.every((fact) => nearestKind(fact.node) === name) : nearestKind(block.owner) === name
84
+ if (covered) continue
85
+ flag(CLAIM_KINDS[name].unmarked.code, block.owner, `"${clip(sentence.text)}" reads as a ${name} claim but cites a fact with no ${CLAIM_ATTRIBUTE}="${name}" mark`)
86
+ }
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,145 @@
1
+ // fidelity-lexicon.mjs - closed-list lexical checks over (source quote, report sentence) pairs.
2
+ //
3
+ // lexiconChecks compares each pair with the lexicon.mjs lists: hedge_dropped, direction_flipped,
4
+ // causality_added, qualifier_dropped (six-token window around the fact span; a :q rendering puts
5
+ // the qualifier inside the span), scope_widened_warn, and the acronym-aware name_outside_quote.
6
+ // The hedge-position heuristic never yields a code; it only sets force_strong for the meaning
7
+ // verifier, as do name_outside_quote and scope_widened_warn. Latin terms match whole words (terms
8
+ // of four or more letters also match their inflections); Korean terms match as substrings, single
9
+ // syllables as tokens. No model, no IO. Plain Node ESM; Korean strings are \u escapes.
10
+
11
+ import { CAUSAL, CONCESSIVE, DIRECTION, HEDGES, NEGATION, QUALIFIERS, SCOPE_NARROW_TO_BROAD } from "./lexicon.mjs"
12
+ import { normalizeText } from "./text-normalize.mjs"
13
+ import { normalizeUnit } from "./units.mjs"
14
+
15
+ /** Ledger qualifier terms -> their surface forms in either language (closed mapping). */
16
+ export const QUALIFIER_FORMS = Object.freeze({
17
+ upper_inclusive: ["up to", "at most", "\uCD5C\uB300", "\uC774\uD558", "\uAE4C\uC9C0"],
18
+ lower_inclusive: ["at least", "\uCD5C\uC18C", "\uC774\uC0C1"],
19
+ approximate: ["about", "approximately", "\uC57D", "\uB0B4\uC678"],
20
+ near: ["nearly"],
21
+ lower_exclusive: ["over", "more than", "\uCD08\uACFC"],
22
+ upper_exclusive: ["under", "less than", "\uBBF8\uB9CC"],
23
+ })
24
+ /** Irregular and synonym direction forms the stem match cannot reach ("rose", "reduced"). */
25
+ const DIRECTION_FORMS = { up: ["rose", "risen", "rising", "grew", "grown", "gain"], down: ["reduc", "fell", "fallen", "falling", "drop", "lower"] }
26
+ const KOREAN = /[\uAC00-\uD7A3]/
27
+ /** True when the term holds a Hangul syllable (Korean terms match by the Korean rules below). */
28
+ export const isKoreanTerm = (term) => KOREAN.test(term)
29
+ /** The guarded hedge and qualifier vocabulary: the union of HEDGES, QUALIFIERS and every QUALIFIER_FORMS surface form. */
30
+ export const guardedTerms = () => [...new Set([...HEDGES, ...QUALIFIERS, ...Object.values(QUALIFIER_FORMS).flat()])]
31
+ const ACRONYM = /(?<![\p{L}\p{N}])[A-Z]{2,6}(?![\p{L}\p{N}])/gu
32
+ const WINDOW = 6
33
+ const cache = new Map()
34
+
35
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
36
+
37
+ /**
38
+ * The lexical pattern engine. mode: "stem" (inflections, Korean one-syllable token prefix), "exact",
39
+ * "word", "suffix" (Korean attached to a value), "guard" (single Latin words of four or more letters
40
+ * take inflections, multiword terms never do; Korean terms match as plain substrings).
41
+ */
42
+ export function lexicalPattern(term, mode) {
43
+ const key = `${mode}\u0001${term}`
44
+ if (cache.has(key)) return cache.get(key)
45
+ let source
46
+ if (!KOREAN.test(term)) {
47
+ const stems = term.length >= 4 && (mode === "stem" || (mode === "guard" && !term.includes(" ")))
48
+ source = `(?<![\\p{L}\\p{N}])${escape(term).replace(/ /g, "\\s+")}${stems ? "\\p{L}*" : ""}(?![\\p{L}\\p{N}])`
49
+ } else if (mode === "guard") source = escape(term)
50
+ else if (mode === "suffix") source = `(?<=^|[\\s\\p{P}\\p{N}A-Za-z])${escape(term)}(?=$|[\\s\\p{P}])`
51
+ else if ([...term].length >= 2 || mode === "word") source = escape(term)
52
+ else source = `(?<=^|[\\s\\p{P}])${escape(term)}${mode === "stem" ? "" : "(?=$|[\\s\\p{P}])"}`
53
+ const re = new RegExp(source, "giu")
54
+ cache.set(key, re)
55
+ return re
56
+ }
57
+
58
+ function positions(text, terms, mode) {
59
+ const out = []
60
+ for (const term of terms) for (const match of text.matchAll(lexicalPattern(term, mode))) out.push({ term, at: match.index })
61
+ return out.sort((a, b) => a.at - b.at)
62
+ }
63
+
64
+ const has = (text, terms, mode) => positions(text, terms, mode).length > 0
65
+ const tokens = (text) => text.split(/\s+/).filter(Boolean)
66
+ const stripQualifiers = (text) => QUALIFIERS.reduce((rest, term) => rest.replace(lexicalPattern(term, "exact"), " "), text)
67
+
68
+ function directions(text) {
69
+ const rest = stripQualifiers(text)
70
+ return {
71
+ up: has(rest, DIRECTION.up, "stem") || has(rest, DIRECTION_FORMS.up, "stem"),
72
+ down: has(rest, DIRECTION.down, "stem") || has(rest, DIRECTION_FORMS.down, "stem"),
73
+ }
74
+ }
75
+
76
+ /** The fact span plus up to six tokens on each side, never crossing the sentence. */
77
+ function qualifierWindow(sentence, span) {
78
+ const [start, end] = span
79
+ return [...tokens(sentence.slice(0, start)).slice(-WINDOW), sentence.slice(start, end), ...tokens(sentence.slice(end)).slice(0, WINDOW)].join(" ")
80
+ }
81
+
82
+ function qualifierDropped(pair) {
83
+ if (!pair.qualifier || !Array.isArray(pair.span)) return false
84
+ const forms = QUALIFIER_FORMS[pair.qualifier] ?? [pair.qualifier]
85
+ const window = qualifierWindow(pair.sentence, pair.span)
86
+ return !forms.some((form) => has(window, [form], KOREAN.test(form) ? "suffix" : "exact"))
87
+ }
88
+
89
+ /** A hedge before the sentence's last concessive while the quote hedges its own final clause. */
90
+ function hedgeMoved(quote, sentence) {
91
+ const lastConcessive = (text) => positions(text, CONCESSIVE, "word").at(-1)?.at ?? -1
92
+ const sentenceHedge = positions(sentence, HEDGES, "stem")[0]?.at
93
+ const quoteHedge = positions(quote, HEDGES, "stem").at(-1)?.at
94
+ const sentenceConcessive = lastConcessive(sentence)
95
+ return sentenceHedge !== undefined && quoteHedge !== undefined && sentenceHedge < sentenceConcessive && quoteHedge > lastConcessive(quote)
96
+ }
97
+
98
+ function pairFindings(pair) {
99
+ const quote = normalizeText(pair.quote)
100
+ const sentence = normalizeText(pair.sentence)
101
+ const found = []
102
+ const add = (code, message) => found.push({ pair_id: pair.pair_id, code, message })
103
+ const dropped = qualifierDropped(pair)
104
+ if (dropped) add("qualifier_dropped", `the source bounds the value (${pair.qualifier}) and the sentence drops it`)
105
+ const sourceHedges = positions(quote, HEDGES, "stem")
106
+ const qualifierOnly = sourceHedges.every(({ term }) => QUALIFIERS.includes(term))
107
+ if (sourceHedges.length > 0 && !has(sentence, HEDGES, "stem") && !(dropped && qualifierOnly)) {
108
+ add("hedge_dropped", `the source hedges ("${sourceHedges[0].term}") and the sentence does not`)
109
+ }
110
+ const [from, to] = [directions(quote), directions(sentence)]
111
+ const negated = has(quote, NEGATION, "word") || has(sentence, NEGATION, "word")
112
+ if (!negated && ((from.up && !from.down && to.down && !to.up) || (from.down && !from.up && to.up && !to.down))) {
113
+ add("direction_flipped", `the source says ${from.up ? "up" : "down"} and the sentence says ${to.up ? "up" : "down"}`)
114
+ }
115
+ const causal = positions(sentence, CAUSAL, "word").find(({ term }) => !has(quote, [term], "word"))
116
+ if (causal) add("causality_added", `"${causal.term}" states a cause the source does not`)
117
+ const context = [quote, ...(pair.context ?? []).map(normalizeText)]
118
+ for (const [narrow, broad] of SCOPE_NARROW_TO_BROAD) {
119
+ if (has(quote, [narrow], "word") && has(sentence, [broad], "word") && !context.some((text) => has(text, [broad], "word"))) {
120
+ add("scope_widened_warn", `the source says "${narrow}" and the sentence says "${broad}"`)
121
+ }
122
+ }
123
+ const acronyms = pair.acronyms ?? {}
124
+ const quoteNames = new Set(quote.match(ACRONYM) ?? [])
125
+ const stray = (sentence.match(ACRONYM) ?? []).find((name) => !quoteNames.has(name) && !Object.hasOwn(acronyms, name) && !normalizeUnit(name))
126
+ if (stray) add("name_outside_quote", `"${stray}" is in neither the quote nor the source's acronym table`)
127
+ return found
128
+ }
129
+
130
+ /**
131
+ * @param {{ pair_id: string, quote: string, sentence: string, span?: [number, number] | null,
132
+ * qualifier?: string | null, acronyms?: Record<string, string>, context?: string[] }[]} pairs
133
+ * @returns {{ findings: { pair_id: string, code: string, message: string }[], forceStrong: string[] }}
134
+ */
135
+ export function lexiconChecks(pairs) {
136
+ const findings = []
137
+ const forceStrong = []
138
+ for (const pair of pairs) {
139
+ const found = pairFindings(pair)
140
+ findings.push(...found)
141
+ const routed = found.some(({ code }) => code === "name_outside_quote" || code === "scope_widened_warn")
142
+ if (routed || hedgeMoved(normalizeText(pair.quote), normalizeText(pair.sentence))) forceStrong.push(pair.pair_id)
143
+ }
144
+ return { findings, forceStrong }
145
+ }
@@ -0,0 +1,163 @@
1
+ // fidelity-records.mjs - the fidelity record schema and the delivery barrier of `outcome verify`
2
+ // and `outcome finish`.
3
+ //
4
+ // The table and the schema are the spec for the barrier. Every fidelity record is written through
5
+ // writeFidelityRecord, which stamps one envelope (RECORD_ENVELOPE: record_kind, artifact_sha256,
6
+ // report, written_at, defects) plus the fields its kind declares, and read through
7
+ // readFidelityRecord, which refuses a missing, unknown, or ill-typed field (fail closed).
8
+ // FIDELITY_RECORDS holds every per-record rule as a column: where the records are (targets), when a
9
+ // delivered session must carry one (requiredWhen), which envelope field binds it to the delivered
10
+ // bytes (bindsTo), and which defects block delivery (blocking). The barrier is one loop over the
11
+ // table with no record-specific branch; a new fidelity step adds a row here and nothing else.
12
+ //
13
+ // HTML/PDF/DOCX need a gate record or an explicitly logged not_run reason, even without a ledger.
14
+ // That logged exemption covers deliveries this harness never gated: once a ledger or any session
15
+ // fidelity record exists, the barrier refuses the unverified edition regardless of it, and the same
16
+ // exemption is the only way a required record may be absent.
17
+
18
+ import { existsSync, readFileSync } from "node:fs"
19
+ import { join } from "node:path"
20
+
21
+ import { CliError, readJsonFile, sha256Of, writeJsonAtomic } from "./cli-support.mjs"
22
+ import { LEDGER_FILENAME } from "./fact-ledger.mjs"
23
+ import { meaningCheckEnabled, requireLockedBrief } from "./fidelity-brief.mjs"
24
+ import { blocksDelivery, deliveryBlockReason } from "./gates-fidelity.mjs"
25
+
26
+ export const FIDELITY_RECORD = "fidelity-defects.json"
27
+ export const MEANING_RECORD = "fidelity-meaning-defects.json"
28
+
29
+ /** The fields every fidelity record carries, whatever its kind. */
30
+ export const RECORD_ENVELOPE = Object.freeze(["record_kind", "artifact_sha256", "report", "written_at", "defects"])
31
+
32
+ /** Formats that need a fidelity record even in a session with no ledger and no record. */
33
+ const GATED_FORMATS = Object.freeze(["html", "pdf", "docx"])
34
+
35
+ /** The meaning record is required unless the locked brief turns the check off; a malformed block cannot prove off. */
36
+ function meaningRequired({ sessionDir }) {
37
+ const lock = requireLockedBrief(join(sessionDir, "brief.md"))
38
+ return lock.exitCode === 2 || (lock.locked && meaningCheckEnabled(lock.parsed))
39
+ }
40
+
41
+ const subjectOf = (row) => row.renderedFrom ?? row.path ?? null
42
+
43
+ /** One record per delivered edition: the file its row names with --verified-by, bound to that edition's bytes. */
44
+ const perEdition = (sessionDir, delivered) =>
45
+ delivered.map((row) => ({ path: row.verifiedBy ?? null, declared: row.verifiedBy !== undefined, format: row.format, at: `${row.format}: `, subjects: [subjectOf(row)].filter((subject) => subject !== null) }))
46
+
47
+ /** One record per session, in the session directory, bound to any delivered edition's bytes. */
48
+ const perSession = (file) => (sessionDir, delivered) => [{ path: join(sessionDir, file), declared: false, format: null, at: "", subjects: delivered.map(subjectOf).filter((subject) => subject !== null) }]
49
+
50
+ /** Every fidelity record the barrier reads, in check order; each column is one rule the barrier applies. */
51
+ export const FIDELITY_RECORDS = Object.freeze([
52
+ Object.freeze({
53
+ kind: "fidelity",
54
+ file: FIDELITY_RECORD,
55
+ step: "fidelity gate",
56
+ targets: perEdition,
57
+ requiredWhen: ({ hasFidelity }, target) => hasFidelity || GATED_FORMATS.includes(target.format),
58
+ remedy: (target) => `run fact verify on ${target.subjects[0] ?? "the edition"} and set it with --verified-by`,
59
+ bindsTo: "artifact_sha256",
60
+ blocking: blocksDelivery,
61
+ payload: Object.freeze(["command", "status", "exitCode", "format", "counts", "gatesRun"]),
62
+ }),
63
+ Object.freeze({
64
+ kind: "meaning",
65
+ file: MEANING_RECORD,
66
+ step: "meaning gate",
67
+ targets: perSession(MEANING_RECORD),
68
+ requiredWhen: meaningRequired,
69
+ remedy: () => "run fact pairs and fact meaning-check before delivery",
70
+ bindsTo: "artifact_sha256",
71
+ blocking: blocksDelivery,
72
+ payload: Object.freeze(["command", "status", "exitCode", "run_id", "pairs_sha256", "counts", "dropped", "unverified", "calibration_path", "calibration_written", "calibration_replaced", "promotion_eligibility"]),
73
+ }),
74
+ ])
75
+
76
+ const entryOf = (kind) => {
77
+ const entry = FIDELITY_RECORDS.find((row) => row.kind === kind)
78
+ if (entry === undefined) throw new CliError(`unknown fidelity record kind ${kind}`)
79
+ return entry
80
+ }
81
+
82
+ /** Why a value is not a valid record of this kind, or null. */
83
+ function envelopeProblem(record, entry) {
84
+ if (record === null || typeof record !== "object" || Array.isArray(record)) return "not a record object"
85
+ const missing = RECORD_ENVELOPE.filter((key) => !Object.hasOwn(record, key))
86
+ if (missing.length > 0) return `missing ${missing.join(", ")}`
87
+ const unknown = Object.keys(record).filter((key) => !RECORD_ENVELOPE.includes(key) && !entry.payload.includes(key))
88
+ if (unknown.length > 0) return `unknown field(s) ${unknown.join(", ")}`
89
+ if (record.record_kind !== entry.kind) return `record_kind ${JSON.stringify(record.record_kind)} is not ${entry.kind}`
90
+ if (typeof record.artifact_sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(record.artifact_sha256)) return "artifact_sha256 is not a sha256 hex digest"
91
+ if (typeof record.report !== "string" || record.report === "") return "report is not a path"
92
+ if (typeof record.written_at !== "string" || Number.isNaN(Date.parse(record.written_at))) return "written_at is not an ISO time"
93
+ if (!Array.isArray(record.defects) || !record.defects.every((defect) => typeof defect?.code === "string")) return "defects is not a list of coded defects"
94
+ return null
95
+ }
96
+
97
+ /**
98
+ * Write one fidelity record of a kind in the table: the envelope plus that kind's payload fields.
99
+ * @param {string} path
100
+ * @param {string} kind a FIDELITY_RECORDS kind
101
+ * @param {{ artifact_sha256: string, report: string, defects: object[], [field: string]: unknown }} fields the envelope values plus the kind's payload
102
+ */
103
+ export function writeFidelityRecord(path, kind, { artifact_sha256, report, defects, ...payload }, now = new Date()) {
104
+ const entry = entryOf(kind)
105
+ const record = { record_kind: kind, artifact_sha256, report, written_at: now.toISOString(), defects, ...payload }
106
+ const problem = envelopeProblem(record, entry)
107
+ if (problem !== null) throw new CliError(`refusing to write a malformed ${kind} record: ${problem}`)
108
+ writeJsonAtomic(path, record)
109
+ return record
110
+ }
111
+
112
+ /** Read one fidelity record of a kind; anything but a valid envelope is a CliError naming the path. */
113
+ export function readFidelityRecord(path, kind) {
114
+ const entry = entryOf(kind)
115
+ let record
116
+ try {
117
+ record = readJsonFile(path)
118
+ } catch (error) {
119
+ throw new CliError(`${kind} record ${path} is malformed: ${error.message}`)
120
+ }
121
+ const problem = envelopeProblem(record, entry)
122
+ if (problem !== null) throw new CliError(`${kind} record ${path} is malformed: ${problem}`)
123
+ return record
124
+ }
125
+
126
+ const unresolved = (format, codes, path) => ({ code: "fidelity_unresolved", format, codes, message: codes.map((code) => deliveryBlockReason({ code })).join("; "), path })
127
+ const hashOf = (path) => (existsSync(path) ? sha256Of(readFileSync(path)) : null)
128
+
129
+ /** Barrier problems for `outcome verify` and `outcome finish`; empty when nothing blocks delivery. */
130
+ export function fidelityProblems(sessionDir, manifest) {
131
+ const delivered = manifest.deliverables.filter((row) => row.status === "delivered")
132
+ const hasFidelity = existsSync(join(sessionDir, LEDGER_FILENAME)) || FIDELITY_RECORDS.some((entry) => existsSync(join(sessionDir, entry.file)))
133
+ const lastFidelity = manifest.gateHistory?.findLast((entry) => entry.gate === "fidelity")
134
+ const exempt = !hasFidelity && manifest.gates.fidelity === "not_run" && lastFidelity?.status === "not_run" && Boolean(lastFidelity.reason?.trim())
135
+ const context = { sessionDir, hasFidelity }
136
+ const problems = []
137
+ const counted = new Set()
138
+ for (const entry of FIDELITY_RECORDS) {
139
+ for (const target of entry.targets(sessionDir, delivered)) {
140
+ const refuse = (message, path = target.path) => problems.push({ code: "edition_unverified", format: target.format, message: `${target.at}${message}`, path })
141
+ if (target.path === null || !existsSync(target.path)) {
142
+ if (target.declared) refuse(`${entry.kind} record ${target.path} is missing`)
143
+ else if (delivered.length > 0 && !exempt && entry.requiredWhen(context, target)) refuse(`${entry.step} not run; ${entry.remedy(target)}`, target.path ?? target.subjects[0] ?? null)
144
+ continue
145
+ }
146
+ let record
147
+ try {
148
+ record = readFidelityRecord(target.path, entry.kind)
149
+ } catch (error) {
150
+ refuse(error.message)
151
+ continue
152
+ }
153
+ if (target.subjects.length > 0 && !target.subjects.some((subject) => hashOf(subject) === record[entry.bindsTo])) {
154
+ refuse(`${entry.kind} record ${target.path} is for a different edition than ${target.subjects.join(", ")}`)
155
+ }
156
+ if (counted.has(target.path)) continue
157
+ counted.add(target.path)
158
+ const codes = [...new Set(record.defects.filter(entry.blocking).map((defect) => defect.code))].sort()
159
+ if (codes.length > 0) problems.push(unresolved(target.format, codes, target.path))
160
+ }
161
+ }
162
+ return problems
163
+ }
@@ -1,6 +1,7 @@
1
1
  // fidelity-state.mjs - per-sentence failure counts for the fidelity repair loop. A sentence is
2
2
  // keyed by its section slug and the sorted fact ids it cites, so a reworded sentence that
3
- // cites the same facts keeps its count. node: builtins only.
3
+ // cites the same facts keeps its count. The exact slug and id rules are in the comment above
4
+ // slugify in fact-fill.mjs. node: builtins only.
4
5
 
5
6
  import { existsSync } from "node:fs"
6
7
  import { join } from "node:path"
@@ -0,0 +1,85 @@
1
+ // fidelity-trace.mjs - the numeral and quotation trace of the deterministic fidelity gate.
2
+ //
3
+ // Every numeral in prose and table cells outside sources, toc and chart figures (dates included)
4
+ // must sit in a data-fact, data-translates, q or blockquote element, or at the site of a fill-map
5
+ // entry; otherwise untraceable_number. Quoted runs with no Q row are quote_unbound, and leftover
6
+ // {{...}} pointers unresolved_placeholder. No model, no network.
7
+ //
8
+ // Fill-map assumption (fact-fill.mjs fill(), #11141): format "html" wraps every value and quote in
9
+ // <data data-fact> / <q data-fact> / <blockquote data-fact>, which this trace skips by element, so
10
+ // it needs no fill map. Format "md" writes bare text, and each fillMap entry is
11
+ // { fact_id, rendered, start, end } with start/end indexing the filled Markdown, not the HTML this
12
+ // gate reads. fill() pushes entries in output order and the Markdown-to-HTML step keeps text order,
13
+ // so each entry, in array order, is matched to the next untraced occurrence of its rendered text in
14
+ // document order that is a whole numeral and reads as the entry's row there (fits: "1,237 million"
15
+ // is not F2's 1,237), and strips that one occurrence only. A copy of the same text that no entry
16
+ // accounts for stays in the scan; an entry whose text is gone strips nothing.
17
+
18
+ import { NOT_CLAIMS } from "./numerals.mjs"
19
+
20
+ /** Structural numerals: versions, figure/table/section references, times (NOT_CLAIMS 7-11), pages, citations, placeholders. */
21
+ export const STRUCTURAL = [...NOT_CLAIMS.slice(7, 12), /\b(?:p|pp|page|pages)\.?\s*\d+(?:\s*[-\u2013]\s*\d+)?/gi, /\[(?:S|Source\s*)\d+\]|\[\^\d+\]/gi, /\{\{[^}]*\}\}/g]
22
+ const NUMERAL = /\d+(?:[.,\-/:]\d+)*/g
23
+ const TRACED = "\u0000"
24
+ const QUOTE_RUNS = [/"([^"]+)"/g, /\u201C([^\u201C\u201D]+)\u201D/g, /\u300C([^\u300C\u300D]+)\u300D/g, /\u300E([^\u300E\u300F]+)\u300F/g]
25
+
26
+ export const clip = (text) => String(text).replace(/\s+/g, " ").trim().slice(0, 80)
27
+
28
+ function ownerTexts(ctx, charts) {
29
+ const owners = new Map()
30
+ for (const entry of ctx.texts) {
31
+ const kind = ctx.kindOf(entry.scope)
32
+ if (kind === "sources" || kind === "toc") continue
33
+ if (charts.has(ctx.util.closest(entry.node, (cur) => cur.tag === "figure"))) continue
34
+ const hidden = ctx.util.closest(entry.node, (cur) => cur.attrs["data-fact"] !== undefined || cur.attrs["data-translates"] !== undefined || cur.tag === "q" || cur.tag === "blockquote")
35
+ owners.set(entry.owner, (owners.get(entry.owner) ?? "") + (hidden ? ` ${TRACED} ` : entry.node.text))
36
+ }
37
+ return owners
38
+ }
39
+
40
+ function scanOwner(ctx, owner, raw, flag) {
41
+ let text = raw
42
+ if (/^h[1-6]$/.test(owner.tag)) text = text.replace(/^\s*\d+(?:[.-]\d+)*[.):]?\s+/, " ")
43
+ if (text.includes("{{")) flag("unresolved_placeholder", owner, `leftover placeholder: "${clip(text.slice(text.indexOf("{{")))}"`)
44
+ for (const re of QUOTE_RUNS) {
45
+ for (const match of text.matchAll(re)) {
46
+ const inner = match[1].replaceAll(TRACED, " ").trim()
47
+ const after = text.slice(match.index + match[0].length).trim().split(/\s+/).slice(0, 6).join(" ")
48
+ if (inner && (inner.split(/\s+/).length >= 3 || ctx.util.citationMarker.test(after))) flag("quote_unbound", owner, `direct quotation with no Q row: "${clip(match[0])}"`)
49
+ }
50
+ }
51
+ const rest = STRUCTURAL.reduce((acc, re) => acc.replace(re, " "), text)
52
+ for (const match of rest.matchAll(NUMERAL)) flag("untraceable_number", owner, `number "${match[0]}" has no ledger row: "${clip(rest.slice(Math.max(0, match.index - 30), match.index + 30))}"`)
53
+ }
54
+
55
+ /** Strip each fill-map entry at its own fill site: the next untraced occurrence after the previous entry's site. */
56
+ function traceFills(owners, fillMap, fits) {
57
+ const keys = [...owners.keys()]
58
+ const texts = [...owners.values()]
59
+ let [index, from] = [0, 0]
60
+ for (const entry of fillMap ?? []) {
61
+ const { rendered } = entry
62
+ if (!rendered) continue
63
+ for (let i = index, at = from; i < texts.length; i += 1, at = 0) {
64
+ let hit = texts[i].indexOf(rendered, at)
65
+ const glued = (pos) => /\d/.test(texts[i][pos - 1] ?? "") || /\d/.test(texts[i][pos + rendered.length] ?? "")
66
+ while (hit !== -1 && (glued(hit) || !fits(entry, texts[i].slice(hit)))) hit = texts[i].indexOf(rendered, hit + 1)
67
+ if (hit === -1) continue
68
+ texts[i] = `${texts[i].slice(0, hit)}${TRACED}${texts[i].slice(hit + rendered.length)}`
69
+ ;[index, from] = [i, hit + TRACED.length]
70
+ break
71
+ }
72
+ }
73
+ return new Map(keys.map((key, i) => [key, texts[i]]))
74
+ }
75
+
76
+ /**
77
+ * @param {object} ctx buildContext(...) from gates-static.mjs
78
+ * @param {Map<object, unknown>} charts chart figures, whose numerals chart checks own
79
+ * @param {{ fact_id: string, rendered: string, start?: number, end?: number }[] | null} fillMap Markdown fill map, or null
80
+ * @param {(code: string, node: object, message: string) => void} flag
81
+ * @param {(entry: object, tail: string) => boolean} fits whether the text from a candidate site on reads as the entry's row
82
+ */
83
+ export function traceChecks(ctx, charts, fillMap, flag, fits) {
84
+ for (const [owner, text] of traceFills(ownerTexts(ctx, charts), fillMap, fits)) scanOwner(ctx, owner, text, flag)
85
+ }