lazycodex-ai 5.1.44 → 5.1.45

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 (273) hide show
  1. package/dist/cli/index.js +948 -416
  2. package/dist/cli/service-auth/keystore.d.ts +4 -2
  3. package/dist/cli-node/index.js +948 -416
  4. package/docs/reference/web-terminal-visual-qa.md +8 -0
  5. package/package.json +1 -1
  6. package/packages/lsp-daemon/dist/cli.js +4 -4
  7. package/packages/lsp-daemon/dist/client.js +4 -4
  8. package/packages/lsp-daemon/dist/index.js +4 -4
  9. package/packages/lsp-daemon/package.json +3 -3
  10. package/packages/lsp-tools-mcp/dist/cli.js +4 -4
  11. package/packages/lsp-tools-mcp/dist/mcp.js +4 -4
  12. package/packages/lsp-tools-mcp/dist/tools.js +4 -4
  13. package/packages/lsp-tools-mcp/package.json +3 -3
  14. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  15. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +62 -21
  16. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  17. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/bootstrap/src/hook.ts +1 -1
  19. package/packages/omo-codex/plugin/components/comment-checker/biome.json +1 -21
  20. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  21. package/packages/omo-codex/plugin/components/comment-checker/package.json +4 -4
  22. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  23. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/biome.json +1 -21
  25. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  26. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +4 -4
  27. package/packages/omo-codex/plugin/components/lsp/biome.json +1 -21
  28. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  29. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +4 -4
  30. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  31. package/packages/omo-codex/plugin/components/lsp/package.json +4 -4
  32. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +0 -1
  33. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-not-installed.test.ts +1 -1
  34. package/packages/omo-codex/plugin/components/rules/biome.json +1 -21
  35. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  36. package/packages/omo-codex/plugin/components/rules/package.json +4 -4
  37. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  38. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  39. package/packages/omo-codex/plugin/components/telemetry/biome.json +1 -21
  40. package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +90 -1
  41. package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +91 -1
  42. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  43. package/packages/omo-codex/plugin/components/telemetry/package.json +4 -4
  44. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +2 -2
  45. package/packages/omo-codex/plugin/components/ultrawork/biome.json +1 -21
  46. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  47. package/packages/omo-codex/plugin/components/ultrawork/package.json +4 -4
  48. package/packages/omo-codex/plugin/components/ulw-execute-continuation/biome.json +1 -21
  49. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
  50. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  51. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +4 -4
  52. package/packages/omo-codex/plugin/components/ulw-loop/biome.json +1 -21
  53. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +1 -1
  54. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +7 -7
  55. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +5 -5
  56. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +1 -1
  57. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  58. package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -4
  59. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +1 -1
  60. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +5 -5
  61. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +1 -1
  62. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +2 -0
  63. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  64. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  65. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  66. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  67. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  68. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  69. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  70. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  71. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  72. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  73. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  74. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  75. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  76. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  77. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  78. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  79. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  80. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  81. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  82. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  83. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  84. package/packages/omo-codex/plugin/package-lock.json +12 -12
  85. package/packages/omo-codex/plugin/package.json +1 -1
  86. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +7 -7
  87. package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +2 -0
  88. package/packages/omo-codex/plugin/skills/browser/SKILL.md +21 -22
  89. package/packages/omo-codex/plugin/skills/browser/references/commands.md +2 -9
  90. package/packages/omo-codex/plugin/skills/browser/references/install.md +1 -2
  91. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  92. package/packages/omo-codex/plugin/skills/browser/scripts/browser-doctor.mjs +2 -2
  93. package/packages/omo-codex/plugin/skills/browser/scripts/browser-engine-guard.mjs +63 -487
  94. package/packages/omo-codex/plugin/skills/browser/scripts/browser-install.mjs +1 -1
  95. package/packages/omo-codex/plugin/skills/browser/scripts/omowright.mjs +1 -10
  96. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +79 -118
  97. package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +75 -224
  98. package/packages/omo-codex/plugin/skills/frontend/references/design/stylegallery.md +32 -27
  99. package/packages/omo-codex/plugin/skills/frontend/references/perfection/README.md +2 -2
  100. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
  101. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
  102. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/collect-files.ts +53 -0
  103. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
  104. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
  105. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
  106. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
  107. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
  108. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
  109. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
  110. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
  111. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
  112. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
  113. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
  114. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
  115. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
  116. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
  117. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
  118. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
  119. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
  120. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
  121. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
  122. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +1 -1
  123. package/packages/omo-codex/plugin/skills/programming/SKILL.md +10 -7
  124. package/packages/omo-codex/plugin/skills/programming/references/typescript/README.md +10 -7
  125. package/packages/omo-codex/plugin/skills/programming/references/typescript/bootstrap.md +9 -23
  126. package/packages/omo-codex/plugin/skills/programming/references/typescript/oxc-lint.md +130 -0
  127. package/packages/omo-codex/plugin/skills/programming/references/typescript/tsconfig-strict.md +6 -62
  128. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
  129. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/new-project.ts +20 -81
  130. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
  131. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
  132. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
  133. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
  134. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
  135. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
  136. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
  137. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +14 -14
  138. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +1 -1
  139. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +2 -2
  140. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +2 -2
  141. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  142. package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +9 -3
  143. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +2 -2
  144. package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +7 -6
  145. package/packages/omo-codex/plugin/skills/ulw-research/references/fact-ledger-grammar.md +116 -0
  146. package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +51 -3
  147. package/packages/omo-codex/plugin/skills/ulw-research/scripts/claims-index.mjs +125 -0
  148. package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +68 -3
  149. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-kinds.mjs +136 -0
  150. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger-schema.mjs +204 -0
  151. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-ledger.mjs +103 -0
  152. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-brief.mjs +145 -0
  153. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-state.mjs +63 -0
  154. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +1 -1
  155. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-structure.mjs +1 -1
  156. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-text.mjs +1 -27
  157. package/packages/omo-codex/plugin/skills/ulw-research/scripts/lexicon.mjs +47 -0
  158. package/packages/omo-codex/plugin/skills/ulw-research/scripts/numerals.mjs +226 -0
  159. package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +2 -3
  160. package/packages/omo-codex/plugin/skills/ulw-research/scripts/particles-ko.mjs +108 -0
  161. package/packages/omo-codex/plugin/skills/ulw-research/scripts/rational.mjs +139 -0
  162. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +2 -2
  163. package/packages/omo-codex/plugin/skills/ulw-research/scripts/sentence-split.mjs +77 -0
  164. package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-normalize.mjs +82 -0
  165. package/packages/omo-codex/plugin/skills/ulw-research/scripts/text-similarity.mjs +28 -0
  166. package/packages/omo-codex/plugin/skills/ulw-research/scripts/units.mjs +105 -0
  167. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +71 -64
  168. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +4 -1
  169. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +6 -6
  170. package/packages/omo-codex/scripts/install-dist/install-local.mjs +152 -23
  171. package/packages/shared-skills/skills/browser/SKILL.md +21 -22
  172. package/packages/shared-skills/skills/browser/references/commands.md +2 -9
  173. package/packages/shared-skills/skills/browser/references/install.md +1 -2
  174. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  175. package/packages/shared-skills/skills/browser/scripts/browser-doctor.mjs +2 -2
  176. package/packages/shared-skills/skills/browser/scripts/browser-engine-guard.mjs +63 -487
  177. package/packages/shared-skills/skills/browser/scripts/browser-install.mjs +1 -1
  178. package/packages/shared-skills/skills/browser/scripts/omowright.mjs +1 -10
  179. package/packages/shared-skills/skills/frontend/SKILL.md +79 -118
  180. package/packages/shared-skills/skills/frontend/references/design/README.md +75 -224
  181. package/packages/shared-skills/skills/frontend/references/design/stylegallery.md +32 -27
  182. package/packages/shared-skills/skills/frontend/references/perfection/README.md +2 -2
  183. package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.ts +8 -24
  184. package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.test.ts +101 -0
  185. package/packages/shared-skills/skills/frontend/scripts/perfection/check-draft-guard.ts +40 -0
  186. package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.test.ts +50 -0
  187. package/packages/shared-skills/skills/frontend/scripts/perfection/collect-files.ts +53 -0
  188. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/findings.ts +16 -0
  189. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/lint.ts +38 -0
  190. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/rules.ts +69 -0
  191. package/packages/shared-skills/skills/frontend/scripts/perfection/draft-guard/text-nodes.ts +121 -0
  192. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/attr-lookalikes.tsx +10 -0
  193. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/bound-values.tsx +12 -0
  194. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/const-generics.tsx +3 -0
  195. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/generics.tsx +7 -0
  196. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/measured-stats.html +6 -0
  197. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/plain-copy.html +9 -0
  198. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/negative/state-copy.html +11 -0
  199. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/attribute-edges.html +7 -0
  200. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/components.tsx +16 -0
  201. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/decorative-badge.vue +7 -0
  202. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/emoji-icons.tsx +9 -0
  203. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/jsx-edges.tsx +1 -0
  204. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.html +6 -0
  205. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/promo-copy.tsx +8 -0
  206. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/draft-guard/positive/vanity-stats.html +5 -0
  207. package/packages/shared-skills/skills/programming/SKILL.md +10 -7
  208. package/packages/shared-skills/skills/programming/references/typescript/README.md +10 -7
  209. package/packages/shared-skills/skills/programming/references/typescript/bootstrap.md +9 -23
  210. package/packages/shared-skills/skills/programming/references/typescript/oxc-lint.md +130 -0
  211. package/packages/shared-skills/skills/programming/references/typescript/tsconfig-strict.md +6 -62
  212. package/packages/shared-skills/skills/programming/scripts/typescript/check-no-excuse-rules.ts +12 -12
  213. package/packages/shared-skills/skills/programming/scripts/typescript/new-project.test.ts +162 -0
  214. package/packages/shared-skills/skills/programming/scripts/typescript/new-project.ts +20 -81
  215. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxfmtrc.json +9 -0
  216. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/.oxlintrc.json +46 -0
  217. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/LICENSE +21 -0
  218. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/index.js +10 -0
  219. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/no-em-dash-copy.js +28 -0
  220. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/lint/oxlint-plugin/require-suppression-reason.js +62 -0
  221. package/packages/shared-skills/skills/programming/scripts/typescript/oxc/tsconfig.json +29 -0
  222. package/packages/shared-skills/skills/refactor/SKILL.md +13 -13
  223. package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
  224. package/packages/shared-skills/skills/ulw-research/AGENTS.md +9 -3
  225. package/packages/shared-skills/skills/ulw-research/SKILL.md +2 -2
  226. package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +7 -6
  227. package/packages/shared-skills/skills/ulw-research/references/fact-ledger-grammar.md +116 -0
  228. package/packages/shared-skills/skills/ulw-research/references/report-gates.md +51 -3
  229. package/packages/shared-skills/skills/ulw-research/scripts/claims-index.mjs +125 -0
  230. package/packages/shared-skills/skills/ulw-research/scripts/claims-index.test.ts +149 -0
  231. package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +68 -3
  232. package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +97 -12
  233. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-grammar.test.ts +190 -0
  234. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.mjs +136 -0
  235. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-kinds.test.ts +141 -0
  236. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.mjs +204 -0
  237. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger-schema.test.ts +43 -0
  238. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.mjs +103 -0
  239. package/packages/shared-skills/skills/ulw-research/scripts/fact-ledger.test.ts +252 -0
  240. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.mjs +145 -0
  241. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-brief.test.ts +179 -0
  242. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.mjs +63 -0
  243. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-state.test.ts +46 -0
  244. package/packages/shared-skills/skills/ulw-research/scripts/gates-context.test.ts +64 -0
  245. package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +1 -1
  246. package/packages/shared-skills/skills/ulw-research/scripts/gates-structure.mjs +1 -1
  247. package/packages/shared-skills/skills/ulw-research/scripts/gates-text.mjs +1 -27
  248. package/packages/shared-skills/skills/ulw-research/scripts/lexicon.mjs +47 -0
  249. package/packages/shared-skills/skills/ulw-research/scripts/lexicon.test.ts +57 -0
  250. package/packages/shared-skills/skills/ulw-research/scripts/numerals.mjs +226 -0
  251. package/packages/shared-skills/skills/ulw-research/scripts/numerals.test.ts +120 -0
  252. package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +2 -3
  253. package/packages/shared-skills/skills/ulw-research/scripts/outcome.test.ts +29 -0
  254. package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.mjs +108 -0
  255. package/packages/shared-skills/skills/ulw-research/scripts/particles-ko.test.ts +86 -0
  256. package/packages/shared-skills/skills/ulw-research/scripts/rational.mjs +139 -0
  257. package/packages/shared-skills/skills/ulw-research/scripts/rational.test.ts +46 -0
  258. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +2 -2
  259. package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.mjs +77 -0
  260. package/packages/shared-skills/skills/ulw-research/scripts/sentence-split.test.ts +109 -0
  261. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/claim-graph.md +13 -0
  262. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-locked.md +17 -0
  263. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity-brief-pending.md +17 -0
  264. package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.mjs +82 -0
  265. package/packages/shared-skills/skills/ulw-research/scripts/text-normalize.test.ts +73 -0
  266. package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.mjs +28 -0
  267. package/packages/shared-skills/skills/ulw-research/scripts/text-similarity.test.ts +38 -0
  268. package/packages/shared-skills/skills/ulw-research/scripts/units.mjs +105 -0
  269. package/packages/shared-skills/skills/ulw-research/scripts/units.test.ts +83 -0
  270. package/packages/shared-skills/skills/visual-qa/SKILL.md +70 -63
  271. package/packages/shared-skills/skills/visual-qa/scripts/skill-doc-fields.test.ts +34 -0
  272. package/script/qa/web-terminal-visual-qa.mjs +6 -2
  273. package/script/qa/xterm-live-terminal.mjs +26 -3
@@ -0,0 +1,252 @@
1
+ import { describe, expect, onTestFinished, test } from "bun:test"
2
+ import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
4
+ import { join } from "node:path"
5
+
6
+ import {
7
+ LEDGER_FILENAME,
8
+ LEDGER_SCHEMA_VERSION,
9
+ addRow,
10
+ emptyLedger,
11
+ nextId,
12
+ readLedger,
13
+ renderLedgerMarkdown,
14
+ retractRow,
15
+ supersedeRow,
16
+ validateLedger,
17
+ writeLedger,
18
+ } from "./fact-ledger.mjs"
19
+
20
+ const HASH = "a".repeat(64)
21
+
22
+ function tempDir(): string {
23
+ const dir = mkdtempSync(join(tmpdir(), "ulw-ledger-"))
24
+ onTestFinished(() => rmSync(dir, { recursive: true, force: true }))
25
+ return dir
26
+ }
27
+
28
+ function measured(label: string, value = "320") {
29
+ return {
30
+ kind: "measured",
31
+ value,
32
+ unit: "m",
33
+ raw: `${value} m`,
34
+ quote: `The span is ${value} m.`,
35
+ value_span: [12, 12 + value.length],
36
+ source_id: "S1",
37
+ locator: { artifact: "sources/S1/text.txt", line: 3, char_start: 40, char_end: 62 },
38
+ snapshot_sha256: HASH,
39
+ extracted_by: "fact add --find",
40
+ observed_at: "2026-10-23T08:00:00Z",
41
+ label,
42
+ }
43
+ }
44
+
45
+ function quoteRow(label: string) {
46
+ return { ...measured(label), kind: "quote", value: null, unit: null, value_span: null }
47
+ }
48
+
49
+ function ledgerWith(...rows: object[]) {
50
+ const ledger = emptyLedger()
51
+ for (const row of rows) addRow(ledger, row)
52
+ return ledger
53
+ }
54
+
55
+ describe("validateLedger", () => {
56
+ test("accepts a ledger built by addRow", () => {
57
+ expect(validateLedger(ledgerWith(measured("span"), quoteRow("claim")))).toEqual([])
58
+ })
59
+
60
+ test("rejects a row missing label", () => {
61
+ const ledger = ledgerWith(measured("span"))
62
+ delete (ledger.rows[0] as Record<string, unknown>).label
63
+ expect(validateLedger(ledger).some((e: string) => e.includes("label"))).toBe(true)
64
+ })
65
+
66
+ test("rejects a duplicate label", () => {
67
+ const ledger = ledgerWith(measured("span"))
68
+ ledger.rows.push({ ...ledger.rows[0], fact_id: "F2" })
69
+ expect(validateLedger(ledger).some((e: string) => e.includes("duplicate label"))).toBe(true)
70
+ })
71
+
72
+ test("rejects an assumed row without rationale", () => {
73
+ const ledger = ledgerWith(measured("span"))
74
+ ledger.rows[0].kind = "assumed"
75
+ expect(validateLedger(ledger).some((e: string) => e.includes("rationale"))).toBe(true)
76
+ })
77
+
78
+ test("rejects a value row with a non-decimal value", () => {
79
+ const ledger = ledgerWith(measured("span"))
80
+ ledger.rows[0].value = "320m"
81
+ expect(validateLedger(ledger).some((e: string) => e.includes("value"))).toBe(true)
82
+ })
83
+
84
+ test("rejects an exact with a non-string num", () => {
85
+ const ledger = ledgerWith(measured("span"))
86
+ ledger.rows[0].exact = { num: 1, den: "3" }
87
+ expect(validateLedger(ledger).some((e: string) => e.includes("exact"))).toBe(true)
88
+ })
89
+
90
+ test("rejects operands and derived_from ids with no ledger row", () => {
91
+ const ledger = ledgerWith(measured("span"))
92
+ ledger.rows.push({ ...ledger.rows[0], fact_id: "F2", kind: "derived", label: "ratio", operation: "ratio", operands: ["F9"], derived_from: ["F1", "Q7"] })
93
+ const errors = validateLedger(ledger)
94
+ expect(errors.some((e: string) => e.includes("operands") && e.includes("F9"))).toBe(true)
95
+ expect(errors.some((e: string) => e.includes("derived_from") && e.includes("Q7"))).toBe(true)
96
+ expect(errors.some((e: string) => e.includes("F1"))).toBe(false)
97
+ })
98
+
99
+ test("a bare row with only fact_id returns at least three errors", () => {
100
+ expect(validateLedger({ rows: [{ fact_id: "F1" }] }).length).toBeGreaterThanOrEqual(3)
101
+ })
102
+ })
103
+
104
+ describe("ids and status", () => {
105
+ test("nextId sequences F1, F2 then Q1, and a derived row gets F3", () => {
106
+ const ledger = emptyLedger()
107
+ expect(addRow(ledger, measured("a")).fact_id).toBe("F1")
108
+ expect(addRow(ledger, measured("b")).fact_id).toBe("F2")
109
+ expect(addRow(ledger, quoteRow("c")).fact_id).toBe("Q1")
110
+ expect(nextId(ledger, "derived")).toBe("F3")
111
+ const derived = addRow(ledger, {
112
+ kind: "derived",
113
+ value: "0.333",
114
+ unit: null,
115
+ raw: "0.333",
116
+ quote: "",
117
+ extracted_by: "fact derive",
118
+ observed_at: "2026-10-23T08:00:00Z",
119
+ label: "ratio",
120
+ operation: "ratio",
121
+ operands: ["F1", "F2"],
122
+ derived_from: ["F1", "F2"],
123
+ exact: { num: "1", den: "3" },
124
+ })
125
+ expect(derived.fact_id).toBe("F3")
126
+ expect(nextId(ledger, "quote")).toBe("Q2")
127
+ })
128
+
129
+ test("measured rows store exact from their decimal", () => {
130
+ const ledger = ledgerWith(measured("span", "12.50"))
131
+ expect(ledger.rows[0].exact).toEqual({ num: "25", den: "2" })
132
+ })
133
+
134
+ test("retractRow keeps the row with status retracted", () => {
135
+ const ledger = ledgerWith(measured("a"), measured("b"))
136
+ retractRow(ledger, "F1", "wrong page")
137
+ expect(ledger.rows).toHaveLength(2)
138
+ expect(ledger.rows[0].status).toBe("retracted")
139
+ expect(ledger.events[0]).toMatchObject({ fact_id: "F1", to: "retracted", reason: "wrong page" })
140
+ expect(validateLedger(ledger)).toEqual([])
141
+ expect(() => retractRow(ledger, "F9", "x")).toThrow()
142
+ })
143
+
144
+ test("supersedeRow points at the replacement", () => {
145
+ const ledger = ledgerWith(measured("a"), measured("b"))
146
+ supersedeRow(ledger, "F1", "F2", "newer filing")
147
+ expect(ledger.rows[0].status).toBe("superseded")
148
+ expect(ledger.events[0]).toMatchObject({ fact_id: "F1", replacement: "F2" })
149
+ expect(validateLedger(ledger)).toEqual([])
150
+ })
151
+
152
+ test("supersedeRow refuses a replacement that is not active", () => {
153
+ const ledger = ledgerWith(measured("a"), measured("b"))
154
+ retractRow(ledger, "F2", "wrong page")
155
+ expect(() => supersedeRow(ledger, "F1", "F2", "newer filing")).toThrow(/F2 is retracted/)
156
+ expect(ledger.rows[0].status).toBe("active")
157
+ expect(ledger.events).toHaveLength(1)
158
+ })
159
+
160
+ test("supersedeRow without a replacement throws and points at retract", () => {
161
+ const ledger = ledgerWith(measured("a"))
162
+ expect(() => supersedeRow(ledger, "F1", null, "newer filing")).toThrow(/supersede F1 needs a replacement row; use retract/)
163
+ expect(ledger.rows[0].status).toBe("active")
164
+ expect(ledger.events).toHaveLength(0)
165
+ })
166
+ })
167
+
168
+ describe("read and write", () => {
169
+ test("write then read round-trips byte-identically", () => {
170
+ const dir = tempDir()
171
+ const ledger = ledgerWith(measured("span"), quoteRow("claim"))
172
+ writeLedger(dir, ledger)
173
+ const first = readFileSync(join(dir, LEDGER_FILENAME), "utf8")
174
+ const loaded = readLedger(dir)
175
+ expect(loaded).toEqual(ledger)
176
+ writeLedger(dir, loaded)
177
+ expect(readFileSync(join(dir, LEDGER_FILENAME), "utf8")).toBe(first)
178
+ expect(readdirSync(dir)).toEqual([LEDGER_FILENAME])
179
+ expect(loaded.schemaVersion).toBe(LEDGER_SCHEMA_VERSION)
180
+ })
181
+
182
+ test("a derived row with an exact rational round-trips and validates", () => {
183
+ const dir = tempDir()
184
+ const ledger = ledgerWith(measured("a"), measured("b"))
185
+ addRow(ledger, {
186
+ kind: "derived",
187
+ value: "0.333",
188
+ raw: "0.333",
189
+ quote: "",
190
+ extracted_by: "fact derive",
191
+ observed_at: "2026-10-23T08:00:00Z",
192
+ label: "third",
193
+ operation: "ratio",
194
+ operands: ["F1", "F2"],
195
+ derived_from: ["F1", "F2"],
196
+ exact: { num: "1", den: "3" },
197
+ })
198
+ writeLedger(dir, ledger)
199
+ const loaded = readLedger(dir)
200
+ expect(loaded.rows[2].exact).toEqual({ num: "1", den: "3" })
201
+ expect(loaded.rows[2].value).toBe("0.333")
202
+ expect(validateLedger(loaded)).toEqual([])
203
+ })
204
+
205
+ test("readLedger of a missing file is an empty ledger; writeLedger refuses an invalid one", () => {
206
+ const dir = tempDir()
207
+ expect(readLedger(dir)).toEqual(emptyLedger())
208
+ const ledger = ledgerWith(measured("a"))
209
+ ledger.rows[0].value = "abc"
210
+ expect(() => writeLedger(dir, ledger)).toThrow()
211
+ expect(readdirSync(dir)).toEqual([])
212
+ })
213
+
214
+ test("renderLedgerMarkdown lists every row", () => {
215
+ const text = renderLedgerMarkdown(ledgerWith(measured("span"), quoteRow("claim")))
216
+ expect(text).toContain("| F1 | measured | span |")
217
+ expect(text).toContain("| Q1 | quote | claim |")
218
+ })
219
+ })
220
+
221
+ describe("validateLedger checks events against rows", () => {
222
+ test("a retracted event that carries a replacement is an error", () => {
223
+ const ledger = ledgerWith(measured("a"), measured("b"))
224
+ retractRow(ledger, "F1", "wrong page")
225
+ ledger.events[0].replacement = "F2"
226
+ expect(validateLedger(ledger).some((e: string) => /F1 is retracted but names replacement F2/.test(e))).toBe(true)
227
+ })
228
+
229
+ test("the last event for a fact must match its row status", () => {
230
+ const ledger = ledgerWith(measured("a"), measured("b"))
231
+ retractRow(ledger, "F1", "wrong page")
232
+ ledger.rows[0].status = "active"
233
+ expect(validateLedger(ledger).some((e: string) => /F1 has status active but its last event moves it to retracted/.test(e))).toBe(true)
234
+ })
235
+
236
+ test("a non-active row with no event is an error", () => {
237
+ const ledger = ledgerWith(measured("a"))
238
+ ledger.rows[0].status = "superseded"
239
+ expect(validateLedger(ledger).some((e: string) => /F1 is superseded but has no status event/.test(e))).toBe(true)
240
+ })
241
+
242
+ test("an event with an invalid from or to status is an error naming the fact", () => {
243
+ const ledger = ledgerWith(measured("a"))
244
+ retractRow(ledger, "F1", "wrong page")
245
+ ledger.events[0].from = "gone"
246
+ const errors = validateLedger(ledger)
247
+ expect(errors.some((e: string) => /events\[0\]: F1 is active, but the event moves it from gone/.test(e))).toBe(true)
248
+ ledger.events[0].from = "active"
249
+ ledger.events[0].to = "active"
250
+ expect(validateLedger(ledger).some((e: string) => /events\[0\]: F1 cannot move from active to active/.test(e))).toBe(true)
251
+ })
252
+ })
@@ -0,0 +1,145 @@
1
+ // fidelity-brief.mjs - parse the `## Fidelity` block of brief.md and decide whether it is locked.
2
+ //
3
+ // Grammar (fable-interview.md section 3, "Parser contract"): the block starts at a line
4
+ // `## Fidelity` and ends at the next `## ` heading. It holds the seven field lines below, in any
5
+ // order, each `<field>: <value> (answered_by: user|default|request|pending)`, plus an optional
6
+ // `asked: <field>, <field>` history line. Values carry no parentheses, and `answered_by: pending`
7
+ // is legal only with the value `pending`. Trailing whitespace on a line is ignored; a second
8
+ // `## Fidelity` block is a parse error. This module parses; it never interprets a value, which
9
+ // is each consumer's job. Plain Node ESM, node: builtins only.
10
+
11
+ import { readFileSync } from "node:fs"
12
+
13
+ export const FIDELITY_FIELDS = Object.freeze([
14
+ "as of",
15
+ "meaning check",
16
+ "source floor",
17
+ "derived numbers",
18
+ "languages",
19
+ "identifier classes",
20
+ "fallback",
21
+ ])
22
+ export const FIDELITY_ANSWERED_BY = Object.freeze(["user", "default", "request", "pending"])
23
+ const LOCKED_ANSWERED_BY = new Set(["user", "default", "request"])
24
+
25
+ export const FIDELITY_INCOMPLETE = "fidelity_interview_incomplete"
26
+ const INCOMPLETE_HINT =
27
+ "A fidelity field is pending or missing in brief.md; answer the open question or record its default, then rerun."
28
+
29
+ const BLOCK_HEADING = /^## Fidelity\s*$/
30
+ const NEXT_HEADING = /^## /
31
+ const FIELD_LINE = /^([a-z][a-z ]*[a-z]): ([^()]+) \(answered_by: (user|default|request|pending)\)$/
32
+ const ASKED_LINE = /^asked:(.*)$/
33
+
34
+ function blockLines(markdown) {
35
+ const lines = String(markdown)
36
+ .split(/\r?\n/)
37
+ .map((line) => line.trimEnd())
38
+ const headings = lines.flatMap((line, index) => (BLOCK_HEADING.test(line) ? [index] : []))
39
+ if (headings.length === 0) return null
40
+ const [start, ...duplicates] = headings
41
+ const body = []
42
+ for (let index = start + 1; index < lines.length; index += 1) {
43
+ if (NEXT_HEADING.test(lines[index])) break
44
+ body.push({ text: lines[index], line: index + 1 })
45
+ }
46
+ return { body, duplicates: duplicates.map((index) => index + 1) }
47
+ }
48
+
49
+ function parseAsked(rest, line, text, errors) {
50
+ const names = rest
51
+ .split(",")
52
+ .map((name) => name.trim())
53
+ .filter(Boolean)
54
+ const unknown = names.filter((name) => !FIDELITY_FIELDS.includes(name))
55
+ if (unknown.length > 0) {
56
+ errors.push(`line ${line}: malformed asked line, unknown field ${unknown.join(", ")}: ${text}`)
57
+ return []
58
+ }
59
+ return names
60
+ }
61
+
62
+ function parseFieldLine({ text, line }, parsed, errors) {
63
+ const match = FIELD_LINE.exec(text)
64
+ if (!match) return errors.push(`line ${line}: malformed fidelity line: ${text}`)
65
+ const [, field, rawValue, answeredBy] = match
66
+ const value = rawValue.trim()
67
+ if (!FIDELITY_FIELDS.includes(field)) return errors.push(`line ${line}: malformed fidelity line, unknown field "${field}": ${text}`)
68
+ if (Object.hasOwn(parsed.fields, field)) return errors.push(`line ${line}: malformed fidelity line, duplicate field "${field}": ${text}`)
69
+ if ((answeredBy === "pending") !== (value === "pending")) {
70
+ return errors.push(`line ${line}: malformed fidelity line, value pending and answered_by: pending go together: ${text}`)
71
+ }
72
+ parsed.fields[field] = value
73
+ parsed.answered_by[field] = answeredBy
74
+ }
75
+
76
+ /**
77
+ * @param {string} briefMd the whole brief.md text
78
+ * @returns {{ present: boolean, fields: Record<string, string>, answered_by: Record<string, string>,
79
+ * asked: string[], missing: string[], pending: string[], errors: string[] }}
80
+ */
81
+ export function parseFidelityBlock(briefMd) {
82
+ const parsed = { present: false, fields: {}, answered_by: {}, asked: [], missing: [], pending: [], errors: [] }
83
+ const block = blockLines(briefMd)
84
+ if (block !== null) {
85
+ const { body, duplicates } = block
86
+ parsed.present = true
87
+ for (const line of duplicates) parsed.errors.push(`line ${line}: duplicate ## Fidelity block, only one is allowed`)
88
+ let sawAsked = false
89
+ for (const entry of body) {
90
+ if (entry.text.trim() === "") continue
91
+ const asked = ASKED_LINE.exec(entry.text)
92
+ if (!asked) {
93
+ parseFieldLine(entry, parsed, parsed.errors)
94
+ continue
95
+ }
96
+ if (sawAsked) parsed.errors.push(`line ${entry.line}: malformed asked line, duplicate: ${entry.text}`)
97
+ else parsed.asked = parseAsked(asked[1], entry.line, entry.text, parsed.errors)
98
+ sawAsked = true
99
+ }
100
+ }
101
+ parsed.missing = FIDELITY_FIELDS.filter((field) => !Object.hasOwn(parsed.fields, field))
102
+ parsed.pending = FIDELITY_FIELDS.filter((field) => parsed.answered_by[field] === "pending")
103
+ return parsed
104
+ }
105
+
106
+ /** Locked = no parse error and all seven fields present with answered_by user, default, or request. */
107
+ export function isLocked(parsed) {
108
+ if (!parsed || parsed.errors.length > 0) return false
109
+ return FIDELITY_FIELDS.every((field) => Object.hasOwn(parsed.fields, field) && LOCKED_ANSWERED_BY.has(parsed.answered_by[field]))
110
+ }
111
+
112
+ function incompleteDefect(briefPath, parsed) {
113
+ const reasons = []
114
+ if (!parsed.present) reasons.push("no ## Fidelity block")
115
+ else if (parsed.missing.length > 0) reasons.push(`missing: ${parsed.missing.join(", ")}`)
116
+ if (parsed.pending.length > 0) reasons.push(`pending: ${parsed.pending.join(", ")}`)
117
+ return {
118
+ code: FIDELITY_INCOMPLETE,
119
+ severity: "blocker",
120
+ integrity: true,
121
+ selector: null,
122
+ line: null,
123
+ message: `${briefPath}: fidelity interview incomplete (${reasons.join("; ")})`,
124
+ hint: INCOMPLETE_HINT,
125
+ }
126
+ }
127
+
128
+ /**
129
+ * The lock check the reader-facing CLI stages run before writing anything.
130
+ * exitCode 0 = locked; 1 = pending, missing, or no brief (one `fidelity_interview_incomplete`
131
+ * defect); 2 = malformed block (usage error, `errors` name the lines, no defect).
132
+ * @param {string} briefPath
133
+ */
134
+ export function requireLockedBrief(briefPath) {
135
+ let markdown = ""
136
+ try {
137
+ markdown = readFileSync(briefPath, "utf8")
138
+ } catch (error) {
139
+ if (error?.code !== "ENOENT") throw error
140
+ }
141
+ const parsed = parseFidelityBlock(markdown)
142
+ if (parsed.errors.length > 0) return { locked: false, exitCode: 2, defects: [], errors: parsed.errors, parsed }
143
+ if (isLocked(parsed)) return { locked: true, exitCode: 0, defects: [], errors: [], parsed }
144
+ return { locked: false, exitCode: 1, defects: [incompleteDefect(briefPath, parsed)], errors: [], parsed }
145
+ }
@@ -0,0 +1,179 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
4
+ import { join } from "node:path"
5
+
6
+ import { FIDELITY_FIELDS, isLocked, parseFidelityBlock, requireLockedBrief } from "./fidelity-brief.mjs"
7
+
8
+ const fixture = (name: string) => join(import.meta.dir, "tests", "fixtures", name)
9
+ const lockedMd = await Bun.file(fixture("fidelity-brief-locked.md")).text()
10
+ const pendingMd = await Bun.file(fixture("fidelity-brief-pending.md")).text()
11
+
12
+ function withoutLine(markdown: string, prefix: string): string {
13
+ return markdown
14
+ .split("\n")
15
+ .filter((line) => !line.startsWith(prefix))
16
+ .join("\n")
17
+ }
18
+
19
+ describe("parseFidelityBlock", () => {
20
+ test("#given a complete block #when parsed #then seven fields are read and the brief is locked", () => {
21
+ // when
22
+ const parsed = parseFidelityBlock(lockedMd)
23
+ // then
24
+ expect(parsed.errors).toEqual([])
25
+ expect(Object.keys(parsed.fields).sort()).toEqual([...FIDELITY_FIELDS].sort())
26
+ expect(parsed.fields["as of"]).toBe("latest at run start, 2026-10-23")
27
+ expect(parsed.fields.languages).toBe("report ko; sources ko, en; quotes original with translation beside")
28
+ expect(parsed.answered_by["derived numbers"]).toBe("user")
29
+ expect(parsed.answered_by.fallback).toBe("request")
30
+ expect(parsed.asked).toEqual(["derived numbers"])
31
+ expect(isLocked(parsed)).toBe(true)
32
+ })
33
+
34
+ test("#given a block missing as of #when parsed #then it is not locked and the missing field is named", () => {
35
+ // when
36
+ const parsed = parseFidelityBlock(withoutLine(lockedMd, "as of:"))
37
+ // then
38
+ expect(isLocked(parsed)).toBe(false)
39
+ expect(parsed.missing).toEqual(["as of"])
40
+ expect(parsed.errors).toEqual([])
41
+ })
42
+
43
+ test("#given a field answered_by pending #when parsed #then it is not locked and not malformed", () => {
44
+ // when
45
+ const parsed = parseFidelityBlock(pendingMd)
46
+ // then
47
+ expect(parsed.errors).toEqual([])
48
+ expect(parsed.answered_by["derived numbers"]).toBe("pending")
49
+ expect(parsed.pending).toEqual(["derived numbers"])
50
+ expect(isLocked(parsed)).toBe(false)
51
+ })
52
+
53
+ test("#given a malformed field line #when parsed #then errors name the line as malformed", () => {
54
+ // given
55
+ const markdown = lockedMd.replace("meaning check: on (answered_by: default)", "meaning check: on")
56
+ // when
57
+ const parsed = parseFidelityBlock(markdown)
58
+ // then
59
+ expect(parsed.errors.length).toBe(1)
60
+ expect(parsed.errors[0]).toContain("malformed")
61
+ expect(parsed.errors[0]).toContain("meaning check: on")
62
+ expect(isLocked(parsed)).toBe(false)
63
+ })
64
+
65
+ test("#given grammar violations #when parsed #then each is a malformed error", () => {
66
+ const cases = [
67
+ lockedMd.replace("fallback: block-quote (answered_by: request)", "fallback: block-quote (answered_by: someone)"),
68
+ lockedMd.replace("meaning check: on (answered_by: default)", "meaning check: on (maybe) (answered_by: default)"),
69
+ lockedMd.replace("meaning check: on (answered_by: default)", "meaning check: on (answered_by: pending)"),
70
+ lockedMd.replace("asked: derived numbers", "asked: derived numbers\ntone: formal (answered_by: user)"),
71
+ lockedMd.replace("asked: derived numbers", "asked: derived numbers\nfallback: omit (answered_by: user)"),
72
+ lockedMd.replace("asked: derived numbers", "asked: tone"),
73
+ ]
74
+ for (const markdown of cases) expect(parseFidelityBlock(markdown).errors.length).toBe(1)
75
+ })
76
+
77
+ test("#given a brief with no Fidelity block #when parsed #then it is not locked and every field is missing", () => {
78
+ // when
79
+ const parsed = parseFidelityBlock("# Brief\n\n## Deliverable\nformat: pdf (answered_by: request)\n")
80
+ // then
81
+ expect(parsed.present).toBe(false)
82
+ expect(parsed.missing).toEqual([...FIDELITY_FIELDS])
83
+ expect(isLocked(parsed)).toBe(false)
84
+ })
85
+
86
+ test("#given field lines after the next heading #when parsed #then they are outside the block", () => {
87
+ // given
88
+ const markdown = withoutLine(lockedMd, "fallback:") + "\nfallback: omit (answered_by: user)\n"
89
+ // then
90
+ expect(parseFidelityBlock(markdown).missing).toEqual(["fallback"])
91
+ })
92
+
93
+ test("#given a second Fidelity block #when parsed #then the duplicate is an error", () => {
94
+ // given
95
+ const markdown = lockedMd + "\n## Fidelity\nas of: x (answered_by: user)\n"
96
+ // when
97
+ const parsed = parseFidelityBlock(markdown)
98
+ // then
99
+ expect(parsed.errors.length).toBe(1)
100
+ expect(parsed.errors[0]).toContain("duplicate")
101
+ expect(parsed.fields["as of"]).toBe("latest at run start, 2026-10-23")
102
+ expect(isLocked(parsed)).toBe(false)
103
+ })
104
+ })
105
+
106
+ describe("requireLockedBrief", () => {
107
+ function briefFile(contents: string | null): { path: string; cleanup: () => void } {
108
+ const dir = mkdtempSync(join(tmpdir(), "fidelity-brief-"))
109
+ const path = join(dir, "brief.md")
110
+ if (contents !== null) writeFileSync(path, contents)
111
+ return { path, cleanup: () => rmSync(dir, { recursive: true, force: true }) }
112
+ }
113
+
114
+ test("#given a locked brief #when required #then exit 0 and no defect", () => {
115
+ const brief = briefFile(lockedMd)
116
+ try {
117
+ expect(requireLockedBrief(brief.path)).toMatchObject({ locked: true, exitCode: 0, defects: [], errors: [] })
118
+ } finally {
119
+ brief.cleanup()
120
+ }
121
+ })
122
+
123
+ test("#given field lines with trailing spaces, a tab, and CRLF endings #when required #then they parse like trimmed lines", () => {
124
+ // given
125
+ const markdown = lockedMd
126
+ .replace("meaning check: on (answered_by: default)", "meaning check: on (answered_by: default) ")
127
+ .replace("fallback: block-quote (answered_by: request)", "fallback: block-quote (answered_by: request)\t")
128
+ .replace("as of: latest at run start, 2026-10-23 (answered_by: default)\n", "as of: latest at run start, 2026-10-23 (answered_by: default) \r\n")
129
+ const brief = briefFile(markdown)
130
+ try {
131
+ // when
132
+ const result = requireLockedBrief(brief.path)
133
+ // then
134
+ expect(result).toMatchObject({ locked: true, exitCode: 0, defects: [], errors: [] })
135
+ expect(result.parsed.fields.fallback).toBe("block-quote")
136
+ } finally {
137
+ brief.cleanup()
138
+ }
139
+ })
140
+
141
+ test("#given a second Fidelity block #when required #then exit 2 naming the duplicate", () => {
142
+ const brief = briefFile(lockedMd + "\n## Fidelity\nas of: x (answered_by: user)\n")
143
+ try {
144
+ const result = requireLockedBrief(brief.path)
145
+ expect(result).toMatchObject({ locked: false, exitCode: 2, defects: [] })
146
+ expect(result.errors[0]).toContain("duplicate ## Fidelity block")
147
+ } finally {
148
+ brief.cleanup()
149
+ }
150
+ })
151
+
152
+ test("#given a pending, partial, or absent brief #when required #then exit 1 with the single lock defect", () => {
153
+ for (const contents of [pendingMd, withoutLine(lockedMd, "as of:"), "# Brief\n", null]) {
154
+ const brief = briefFile(contents)
155
+ try {
156
+ const result = requireLockedBrief(brief.path)
157
+ expect(result.locked).toBe(false)
158
+ expect(result.exitCode).toBe(1)
159
+ expect(result.defects.length).toBe(1)
160
+ expect(result.defects[0]).toMatchObject({ code: "fidelity_interview_incomplete", severity: "blocker", integrity: true })
161
+ expect(typeof result.defects[0].message).toBe("string")
162
+ expect(typeof result.defects[0].hint).toBe("string")
163
+ } finally {
164
+ brief.cleanup()
165
+ }
166
+ }
167
+ })
168
+
169
+ test("#given a malformed brief #when required #then exit 2 with the parse errors and no defect", () => {
170
+ const brief = briefFile(lockedMd.replace("meaning check: on (answered_by: default)", "meaning check: on"))
171
+ try {
172
+ const result = requireLockedBrief(brief.path)
173
+ expect(result).toMatchObject({ locked: false, exitCode: 2, defects: [] })
174
+ expect(result.errors[0]).toContain("malformed")
175
+ } finally {
176
+ brief.cleanup()
177
+ }
178
+ })
179
+ })
@@ -0,0 +1,63 @@
1
+ // fidelity-state.mjs - per-sentence failure counts for the fidelity repair loop. A sentence is
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.
4
+
5
+ import { existsSync } from "node:fs"
6
+ import { join } from "node:path"
7
+
8
+ import { CliError, readJsonFile, writeJsonAtomic } from "./cli-support.mjs"
9
+
10
+ export const STATE_FILENAME = "fidelity-state.json"
11
+ export const STATE_SCHEMA_VERSION = 1
12
+
13
+ /** slug + '#' + the fact ids sorted and joined by ','. */
14
+ export function sentenceKey(sectionSlug, factIds) {
15
+ return `${sectionSlug}#${[...factIds].sort().join(",")}`
16
+ }
17
+
18
+ function emptyState() {
19
+ return { schemaVersion: STATE_SCHEMA_VERSION, failures: {}, replaced: [] }
20
+ }
21
+
22
+ function isValid(state) {
23
+ return (
24
+ state !== null &&
25
+ typeof state === "object" &&
26
+ state.schemaVersion === STATE_SCHEMA_VERSION &&
27
+ state.failures !== null &&
28
+ typeof state.failures === "object" &&
29
+ !Array.isArray(state.failures) &&
30
+ Object.values(state.failures).every((count) => Number.isInteger(count) && count >= 0) &&
31
+ Array.isArray(state.replaced) &&
32
+ state.replaced.every((key) => typeof key === "string")
33
+ )
34
+ }
35
+
36
+ export function readState(dir) {
37
+ const file = join(dir, STATE_FILENAME)
38
+ if (!existsSync(file)) return emptyState()
39
+ const state = readJsonFile(file)
40
+ if (!isValid(state)) throw new CliError(`${file} is not a valid fidelity state`)
41
+ return state
42
+ }
43
+
44
+ export function recordFailure(state, key) {
45
+ state.failures[key] = (state.failures[key] ?? 0) + 1
46
+ return state.failures[key]
47
+ }
48
+
49
+ export function failureCount(state, key) {
50
+ return state.failures[key] ?? 0
51
+ }
52
+
53
+ export function markReplaced(state, key) {
54
+ if (!state.replaced.includes(key)) state.replaced.push(key)
55
+ return state
56
+ }
57
+
58
+ export function writeState(dir, state) {
59
+ if (!isValid(state)) throw new CliError("refusing to write an invalid fidelity state")
60
+ const file = join(dir, STATE_FILENAME)
61
+ writeJsonAtomic(file, state)
62
+ return file
63
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, expect, onTestFinished, test } from "bun:test"
2
+ import { mkdtempSync, readdirSync, rmSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
4
+ import { join } from "node:path"
5
+
6
+ import { failureCount, markReplaced, readState, recordFailure, sentenceKey, writeState } from "./fidelity-state.mjs"
7
+
8
+ function tempDir(): string {
9
+ const dir = mkdtempSync(join(tmpdir(), "ulw-fidelity-"))
10
+ onTestFinished(() => rmSync(dir, { recursive: true, force: true }))
11
+ return dir
12
+ }
13
+
14
+ describe("fidelity state", () => {
15
+ test("sentenceKey is order-independent on ids", () => {
16
+ expect(sentenceKey("findings", ["F3", "Q1", "F12"])).toBe(sentenceKey("findings", ["F12", "F3", "Q1"]))
17
+ expect(sentenceKey("findings", ["F2", "F1"])).toBe("findings#F1,F2")
18
+ expect(sentenceKey("findings", ["F1"])).not.toBe(sentenceKey("other", ["F1"]))
19
+ })
20
+
21
+ test("failureCount increments per key", () => {
22
+ const dir = tempDir()
23
+ const state = readState(dir)
24
+ const key = sentenceKey("findings", ["F1"])
25
+ expect(failureCount(state, key)).toBe(0)
26
+ recordFailure(state, key)
27
+ recordFailure(state, key)
28
+ expect(failureCount(state, key)).toBe(2)
29
+ expect(failureCount(state, sentenceKey("findings", ["F2"]))).toBe(0)
30
+ })
31
+
32
+ test("markReplaced records the key and survives a write/read", () => {
33
+ const dir = tempDir()
34
+ const state = readState(dir)
35
+ const key = sentenceKey("findings", ["Q1"])
36
+ recordFailure(state, key)
37
+ markReplaced(state, key)
38
+ markReplaced(state, key)
39
+ expect(state.replaced).toEqual([key])
40
+ writeState(dir, state)
41
+ expect(readdirSync(dir)).toEqual(["fidelity-state.json"])
42
+ const loaded = readState(dir)
43
+ expect(loaded).toEqual(state)
44
+ expect(failureCount(loaded, key)).toBe(1)
45
+ })
46
+ })