spexcode 0.6.1 → 0.6.5

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 (480) hide show
  1. package/README.md +99 -149
  2. package/bin/spex.mjs +4 -0
  3. package/node_modules/@spexcode/spec-cli/bin/spex.mjs +76 -0
  4. package/node_modules/@spexcode/spec-cli/dist/attach.d.ts +2 -0
  5. package/node_modules/@spexcode/spec-cli/dist/attach.js +48 -0
  6. package/node_modules/@spexcode/spec-cli/dist/claude-headless.d.ts +33 -0
  7. package/node_modules/@spexcode/spec-cli/dist/claude-headless.js +307 -0
  8. package/node_modules/@spexcode/spec-cli/dist/cli.d.ts +1 -0
  9. package/node_modules/@spexcode/spec-cli/dist/cli.js +1859 -0
  10. package/node_modules/@spexcode/spec-cli/dist/client.d.ts +111 -0
  11. package/node_modules/@spexcode/spec-cli/dist/client.js +476 -0
  12. package/node_modules/@spexcode/spec-cli/dist/cockpit.d.ts +7 -0
  13. package/node_modules/@spexcode/spec-cli/dist/cockpit.js +26 -0
  14. package/node_modules/@spexcode/spec-cli/dist/codex-headless.d.ts +1 -0
  15. package/{spec-cli/src/codex-headless.ts → node_modules/@spexcode/spec-cli/dist/codex-headless.js} +3 -9
  16. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +53 -0
  17. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +686 -0
  18. package/node_modules/@spexcode/spec-cli/dist/commit-surgery.d.ts +1 -0
  19. package/node_modules/@spexcode/spec-cli/dist/commit-surgery.js +90 -0
  20. package/node_modules/@spexcode/spec-cli/dist/contract-filter.d.ts +14 -0
  21. package/node_modules/@spexcode/spec-cli/dist/contract-filter.js +219 -0
  22. package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.d.ts +6 -0
  23. package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.js +50 -0
  24. package/node_modules/@spexcode/spec-cli/dist/delivery-queue.d.ts +23 -0
  25. package/node_modules/@spexcode/spec-cli/dist/delivery-queue.js +179 -0
  26. package/node_modules/@spexcode/spec-cli/dist/doctor.d.ts +9 -0
  27. package/node_modules/@spexcode/spec-cli/dist/doctor.js +547 -0
  28. package/node_modules/@spexcode/spec-cli/dist/eval-host.d.ts +1 -0
  29. package/node_modules/@spexcode/spec-cli/dist/eval-host.js +19 -0
  30. package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +26 -0
  31. package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +475 -0
  32. package/node_modules/@spexcode/spec-cli/dist/file-write.d.ts +2 -0
  33. package/node_modules/@spexcode/spec-cli/dist/file-write.js +26 -0
  34. package/node_modules/@spexcode/spec-cli/dist/flat.d.ts +89 -0
  35. package/node_modules/@spexcode/spec-cli/dist/flat.js +1076 -0
  36. package/node_modules/@spexcode/spec-cli/dist/gateway-auth.d.ts +56 -0
  37. package/node_modules/@spexcode/spec-cli/dist/gateway-auth.js +168 -0
  38. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.d.ts +28 -0
  39. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +361 -0
  40. package/node_modules/@spexcode/spec-cli/dist/gateway.d.ts +37 -0
  41. package/node_modules/@spexcode/spec-cli/dist/gateway.js +503 -0
  42. package/node_modules/@spexcode/spec-cli/dist/graphCache.d.ts +27 -0
  43. package/node_modules/@spexcode/spec-cli/dist/graphCache.js +716 -0
  44. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.d.ts +14 -0
  45. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +35 -0
  46. package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +85 -0
  47. package/node_modules/@spexcode/spec-cli/dist/graphStream.js +1195 -0
  48. package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.d.ts +64 -0
  49. package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.js +166 -0
  50. package/node_modules/@spexcode/spec-cli/dist/guide.d.ts +8 -0
  51. package/{spec-cli/src/guide.ts → node_modules/@spexcode/spec-cli/dist/guide.js} +115 -22
  52. package/node_modules/@spexcode/spec-cli/dist/harness-select.d.ts +18 -0
  53. package/node_modules/@spexcode/spec-cli/dist/harness-select.js +62 -0
  54. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +325 -0
  55. package/node_modules/@spexcode/spec-cli/dist/harness.js +3026 -0
  56. package/node_modules/@spexcode/spec-cli/dist/headless-controller.d.ts +9 -0
  57. package/node_modules/@spexcode/spec-cli/dist/headless-controller.js +47 -0
  58. package/node_modules/@spexcode/spec-cli/dist/help.d.ts +10 -0
  59. package/node_modules/@spexcode/spec-cli/dist/help.js +574 -0
  60. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.d.ts +18 -0
  61. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +122 -0
  62. package/node_modules/@spexcode/spec-cli/dist/hooks.d.ts +1 -0
  63. package/{spec-cli/src/hooks.ts → node_modules/@spexcode/spec-cli/dist/hooks.js} +15 -15
  64. package/node_modules/@spexcode/spec-cli/dist/host-resources.d.ts +97 -0
  65. package/node_modules/@spexcode/spec-cli/dist/host-resources.js +795 -0
  66. package/node_modules/@spexcode/spec-cli/dist/host.d.ts +88 -0
  67. package/node_modules/@spexcode/spec-cli/dist/host.js +663 -0
  68. package/node_modules/@spexcode/spec-cli/dist/index.d.ts +1 -0
  69. package/node_modules/@spexcode/spec-cli/dist/index.js +899 -0
  70. package/node_modules/@spexcode/spec-cli/dist/init.d.ts +1 -0
  71. package/node_modules/@spexcode/spec-cli/dist/init.js +284 -0
  72. package/node_modules/@spexcode/spec-cli/dist/issues-cli.d.ts +6 -0
  73. package/node_modules/@spexcode/spec-cli/dist/issues-cli.js +334 -0
  74. package/node_modules/@spexcode/spec-cli/dist/issues.d.ts +69 -0
  75. package/node_modules/@spexcode/spec-cli/dist/issues.js +189 -0
  76. package/node_modules/@spexcode/spec-cli/dist/lint.d.ts +34 -0
  77. package/node_modules/@spexcode/spec-cli/dist/lint.js +476 -0
  78. package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +7 -0
  79. package/{spec-cli/src/listen.ts → node_modules/@spexcode/spec-cli/dist/listen.js} +13 -17
  80. package/node_modules/@spexcode/spec-cli/dist/localIssues.d.ts +65 -0
  81. package/node_modules/@spexcode/spec-cli/dist/localIssues.js +582 -0
  82. package/node_modules/@spexcode/spec-cli/dist/login-page.d.ts +5 -0
  83. package/{spec-cli/src/login-page.ts → node_modules/@spexcode/spec-cli/dist/login-page.js} +7 -7
  84. package/node_modules/@spexcode/spec-cli/dist/loop-in.d.ts +23 -0
  85. package/{spec-cli/src/loop-in.ts → node_modules/@spexcode/spec-cli/dist/loop-in.js} +35 -45
  86. package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +69 -0
  87. package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +603 -0
  88. package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +18 -0
  89. package/node_modules/@spexcode/spec-cli/dist/materialize.js +468 -0
  90. package/node_modules/@spexcode/spec-cli/dist/mentions.d.ts +50 -0
  91. package/node_modules/@spexcode/spec-cli/dist/mentions.js +117 -0
  92. package/node_modules/@spexcode/spec-cli/dist/opencode-headless.d.ts +4 -0
  93. package/node_modules/@spexcode/spec-cli/dist/opencode-headless.js +208 -0
  94. package/node_modules/@spexcode/spec-cli/dist/opencode.d.ts +3 -0
  95. package/{spec-cli/src/opencode.ts → node_modules/@spexcode/spec-cli/dist/opencode.js} +9 -13
  96. package/node_modules/@spexcode/spec-cli/dist/pi-harness.d.ts +3 -0
  97. package/{spec-cli/src/pi-harness.ts → node_modules/@spexcode/spec-cli/dist/pi-harness.js} +45 -40
  98. package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +25 -0
  99. package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +196 -0
  100. package/node_modules/@spexcode/spec-cli/dist/plugin-harness.d.ts +21 -0
  101. package/node_modules/@spexcode/spec-cli/dist/plugin-harness.js +146 -0
  102. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.d.ts +10 -0
  103. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +404 -0
  104. package/node_modules/@spexcode/spec-cli/dist/pty-helper.d.mts +1 -0
  105. package/node_modules/@spexcode/spec-cli/dist/pty-helper.mjs +101 -0
  106. package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.d.mts +2 -0
  107. package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.mjs +23 -0
  108. package/node_modules/@spexcode/spec-cli/dist/public-graph.d.ts +46 -0
  109. package/node_modules/@spexcode/spec-cli/dist/public-graph.js +56 -0
  110. package/node_modules/@spexcode/spec-cli/dist/ranker.d.ts +15 -0
  111. package/node_modules/@spexcode/spec-cli/dist/ranker.js +188 -0
  112. package/node_modules/@spexcode/spec-cli/dist/reaper.d.ts +6 -0
  113. package/node_modules/@spexcode/spec-cli/dist/reaper.js +81 -0
  114. package/node_modules/@spexcode/spec-cli/dist/reviews.d.ts +189 -0
  115. package/node_modules/@spexcode/spec-cli/dist/reviews.js +350 -0
  116. package/node_modules/@spexcode/spec-cli/dist/runtime-guard.d.ts +6 -0
  117. package/node_modules/@spexcode/spec-cli/dist/runtime-guard.js +37 -0
  118. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +21 -0
  119. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +84 -0
  120. package/node_modules/@spexcode/spec-cli/dist/search.bench.d.mts +1 -0
  121. package/node_modules/@spexcode/spec-cli/dist/search.bench.mjs +75 -0
  122. package/node_modules/@spexcode/spec-cli/dist/search.d.ts +20 -0
  123. package/node_modules/@spexcode/spec-cli/dist/search.js +61 -0
  124. package/node_modules/@spexcode/spec-cli/dist/session-cursors.d.ts +14 -0
  125. package/node_modules/@spexcode/spec-cli/dist/session-cursors.js +82 -0
  126. package/node_modules/@spexcode/spec-cli/dist/session-declarations.d.ts +13 -0
  127. package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +126 -0
  128. package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +10 -0
  129. package/node_modules/@spexcode/spec-cli/dist/session-execution.js +70 -0
  130. package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +28 -0
  131. package/node_modules/@spexcode/spec-cli/dist/session-files.js +130 -0
  132. package/node_modules/@spexcode/spec-cli/dist/session-follow.d.ts +31 -0
  133. package/node_modules/@spexcode/spec-cli/dist/session-follow.js +179 -0
  134. package/node_modules/@spexcode/spec-cli/dist/session-reparent.d.ts +10 -0
  135. package/node_modules/@spexcode/spec-cli/dist/session-reparent.js +33 -0
  136. package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +51 -0
  137. package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +233 -0
  138. package/node_modules/@spexcode/spec-cli/dist/session-web.d.ts +24 -0
  139. package/node_modules/@spexcode/spec-cli/dist/session-web.js +141 -0
  140. package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +412 -0
  141. package/node_modules/@spexcode/spec-cli/dist/sessions.js +4239 -0
  142. package/node_modules/@spexcode/spec-cli/dist/sh.d.ts +1 -0
  143. package/node_modules/@spexcode/spec-cli/dist/sh.js +3 -0
  144. package/node_modules/@spexcode/spec-cli/dist/shim-runtime.d.ts +1 -0
  145. package/{spec-cli/src/shim-runtime.ts → node_modules/@spexcode/spec-cli/dist/shim-runtime.js} +3 -4
  146. package/node_modules/@spexcode/spec-cli/dist/slash-commands.d.ts +9 -0
  147. package/node_modules/@spexcode/spec-cli/dist/slash-commands.js +307 -0
  148. package/node_modules/@spexcode/spec-cli/dist/source-files.d.ts +9 -0
  149. package/node_modules/@spexcode/spec-cli/dist/source-files.js +113 -0
  150. package/node_modules/@spexcode/spec-cli/dist/supervise.d.ts +1 -0
  151. package/node_modules/@spexcode/spec-cli/dist/supervise.js +308 -0
  152. package/node_modules/@spexcode/spec-cli/dist/tree.d.ts +25 -0
  153. package/node_modules/@spexcode/spec-cli/dist/tree.js +98 -0
  154. package/node_modules/@spexcode/spec-cli/dist/tsx-bin.d.ts +3 -0
  155. package/node_modules/@spexcode/spec-cli/dist/tsx-bin.js +41 -0
  156. package/node_modules/@spexcode/spec-cli/dist/uninstall.d.ts +3 -0
  157. package/node_modules/@spexcode/spec-cli/dist/uninstall.js +177 -0
  158. package/node_modules/@spexcode/spec-cli/dist/uploads.d.ts +36 -0
  159. package/node_modules/@spexcode/spec-cli/dist/uploads.js +243 -0
  160. package/node_modules/@spexcode/spec-cli/dist/worktree-sources.d.ts +1 -0
  161. package/node_modules/@spexcode/spec-cli/dist/worktree-sources.js +47 -0
  162. package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/dispatch.sh +5 -5
  163. package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/harness.sh +27 -7
  164. package/node_modules/@spexcode/spec-cli/package.json +47 -0
  165. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
  166. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +9 -0
  167. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec-first.sh +1 -1
  168. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec-of-file.sh +1 -0
  169. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +22 -0
  170. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/spec.md +2 -2
  171. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +31 -29
  172. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +15 -0
  173. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +10 -0
  174. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +13 -0
  175. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +12 -0
  176. package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +94 -0
  177. package/node_modules/@spexcode/spec-core/dist/anchors.js +730 -0
  178. package/node_modules/@spexcode/spec-core/dist/git.d.ts +166 -0
  179. package/node_modules/@spexcode/spec-core/dist/git.js +2736 -0
  180. package/node_modules/@spexcode/spec-core/dist/graph-delta.d.ts +44 -0
  181. package/node_modules/@spexcode/spec-core/dist/graph-delta.js +72 -0
  182. package/node_modules/@spexcode/spec-core/dist/graph.d.ts +34 -0
  183. package/node_modules/@spexcode/spec-core/dist/graph.js +237 -0
  184. package/node_modules/@spexcode/spec-core/dist/graphDelta.d.ts +3 -0
  185. package/node_modules/@spexcode/spec-core/dist/graphDelta.js +13 -0
  186. package/node_modules/@spexcode/spec-core/dist/harness-identity.d.ts +31 -0
  187. package/node_modules/@spexcode/spec-core/dist/harness-identity.js +20 -0
  188. package/node_modules/@spexcode/spec-core/dist/identity-presets.d.ts +152 -0
  189. package/node_modules/@spexcode/spec-core/dist/identity-presets.js +132 -0
  190. package/node_modules/@spexcode/spec-core/dist/index.d.ts +45 -0
  191. package/node_modules/@spexcode/spec-core/dist/index.js +19 -0
  192. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +183 -0
  193. package/node_modules/@spexcode/spec-core/dist/layout.js +548 -0
  194. package/node_modules/@spexcode/spec-core/dist/process-identity.d.ts +37 -0
  195. package/node_modules/@spexcode/spec-core/dist/process-identity.js +214 -0
  196. package/node_modules/@spexcode/spec-core/dist/project-identity.d.ts +12 -0
  197. package/node_modules/@spexcode/spec-core/dist/project-identity.js +71 -0
  198. package/node_modules/@spexcode/spec-core/dist/project-store.d.ts +3 -0
  199. package/node_modules/@spexcode/spec-core/dist/project-store.js +14 -0
  200. package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +2 -0
  201. package/node_modules/@spexcode/spec-core/dist/resilience.js +40 -0
  202. package/node_modules/@spexcode/spec-core/dist/review/index.d.ts +3 -0
  203. package/node_modules/@spexcode/spec-core/dist/review/index.js +5 -0
  204. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +77 -0
  205. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +308 -0
  206. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +66 -0
  207. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +180 -0
  208. package/node_modules/@spexcode/spec-core/dist/review/session.d.ts +4 -0
  209. package/node_modules/@spexcode/spec-core/dist/review/session.js +8 -0
  210. package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.d.ts +15 -0
  211. package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.js +12 -0
  212. package/node_modules/@spexcode/spec-core/dist/root-lru.d.ts +4 -0
  213. package/{spec-cli/src/root-lru.ts → node_modules/@spexcode/spec-core/dist/root-lru.js} +26 -30
  214. package/node_modules/@spexcode/spec-core/dist/specs.d.ts +117 -0
  215. package/node_modules/@spexcode/spec-core/dist/specs.js +489 -0
  216. package/node_modules/@spexcode/spec-core/package.json +28 -0
  217. package/node_modules/@spexcode/spec-eval/dist/cache.d.ts +10 -0
  218. package/node_modules/@spexcode/spec-eval/dist/cache.js +50 -0
  219. package/node_modules/@spexcode/spec-eval/dist/cli.d.ts +22 -0
  220. package/node_modules/@spexcode/spec-eval/dist/cli.js +981 -0
  221. package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +98 -0
  222. package/node_modules/@spexcode/spec-eval/dist/evaltab.js +176 -0
  223. package/node_modules/@spexcode/spec-eval/dist/filing.d.ts +16 -0
  224. package/node_modules/@spexcode/spec-eval/dist/filing.js +36 -0
  225. package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +48 -0
  226. package/node_modules/@spexcode/spec-eval/dist/freshness.js +799 -0
  227. package/node_modules/@spexcode/spec-eval/dist/host.d.ts +46 -0
  228. package/node_modules/@spexcode/spec-eval/dist/host.js +53 -0
  229. package/node_modules/@spexcode/spec-eval/dist/humanok.d.ts +11 -0
  230. package/node_modules/@spexcode/spec-eval/dist/humanok.js +27 -0
  231. package/node_modules/@spexcode/spec-eval/dist/index.d.ts +4 -0
  232. package/node_modules/@spexcode/spec-eval/dist/index.js +4 -0
  233. package/node_modules/@spexcode/spec-eval/dist/remarks.d.ts +31 -0
  234. package/node_modules/@spexcode/spec-eval/dist/remarks.js +1 -0
  235. package/node_modules/@spexcode/spec-eval/dist/scenariofresh.d.ts +13 -0
  236. package/node_modules/@spexcode/spec-eval/dist/scenariofresh.js +303 -0
  237. package/node_modules/@spexcode/spec-eval/dist/scenarios.d.ts +99 -0
  238. package/node_modules/@spexcode/spec-eval/dist/scenarios.js +650 -0
  239. package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +307 -0
  240. package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +1883 -0
  241. package/node_modules/@spexcode/spec-eval/dist/sidecar.d.ts +55 -0
  242. package/node_modules/@spexcode/spec-eval/dist/sidecar.js +82 -0
  243. package/node_modules/@spexcode/spec-eval/dist/timeline.d.ts +25 -0
  244. package/node_modules/@spexcode/spec-eval/dist/timeline.js +65 -0
  245. package/node_modules/@spexcode/spec-eval/dist/ui-path.d.ts +1 -0
  246. package/node_modules/@spexcode/spec-eval/dist/ui-path.js +2 -0
  247. package/node_modules/@spexcode/spec-eval/package.json +36 -0
  248. package/node_modules/@spexcode/spec-forge/dist/cache.d.ts +29 -0
  249. package/node_modules/@spexcode/spec-forge/dist/cache.js +59 -0
  250. package/node_modules/@spexcode/spec-forge/dist/cli.d.ts +1 -0
  251. package/node_modules/@spexcode/spec-forge/dist/cli.js +100 -0
  252. package/node_modules/@spexcode/spec-forge/dist/drivers/github.d.ts +2 -0
  253. package/node_modules/@spexcode/spec-forge/dist/drivers/github.js +116 -0
  254. package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.d.ts +7 -0
  255. package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.js +144 -0
  256. package/node_modules/@spexcode/spec-forge/dist/drivers.d.ts +10 -0
  257. package/node_modules/@spexcode/spec-forge/dist/drivers.js +83 -0
  258. package/node_modules/@spexcode/spec-forge/dist/index.d.ts +4 -0
  259. package/node_modules/@spexcode/spec-forge/dist/index.js +4 -0
  260. package/node_modules/@spexcode/spec-forge/dist/links.d.ts +15 -0
  261. package/node_modules/@spexcode/spec-forge/dist/links.js +65 -0
  262. package/node_modules/@spexcode/spec-forge/dist/needs-eval.d.ts +10 -0
  263. package/node_modules/@spexcode/spec-forge/dist/needs-eval.js +20 -0
  264. package/node_modules/@spexcode/spec-forge/dist/port.d.ts +53 -0
  265. package/node_modules/@spexcode/spec-forge/dist/port.js +1 -0
  266. package/node_modules/@spexcode/spec-forge/dist/resident.d.ts +7 -0
  267. package/node_modules/@spexcode/spec-forge/dist/resident.js +43 -0
  268. package/node_modules/@spexcode/spec-forge/package.json +32 -0
  269. package/package.json +33 -19
  270. package/spec-cli/bin/spex.mjs +0 -69
  271. package/spec-cli/src/anchors.ts +0 -706
  272. package/spec-cli/src/attach.ts +0 -43
  273. package/spec-cli/src/claude-headless.ts +0 -321
  274. package/spec-cli/src/cli.ts +0 -1115
  275. package/spec-cli/src/client.ts +0 -315
  276. package/spec-cli/src/cockpit.ts +0 -43
  277. package/spec-cli/src/codex-runtime-generations.ts +0 -646
  278. package/spec-cli/src/commit-surgery.ts +0 -68
  279. package/spec-cli/src/contract-filter.ts +0 -202
  280. package/spec-cli/src/delivery-queue.ts +0 -107
  281. package/spec-cli/src/doctor.ts +0 -516
  282. package/spec-cli/src/file-write.ts +0 -22
  283. package/spec-cli/src/gateway-auth.ts +0 -181
  284. package/spec-cli/src/gateway-hub.ts +0 -356
  285. package/spec-cli/src/gateway.ts +0 -368
  286. package/spec-cli/src/git.ts +0 -2180
  287. package/spec-cli/src/graph.ts +0 -221
  288. package/spec-cli/src/graphCache.ts +0 -641
  289. package/spec-cli/src/graphDelta.ts +0 -77
  290. package/spec-cli/src/graphStream.ts +0 -1019
  291. package/spec-cli/src/harness-select.ts +0 -77
  292. package/spec-cli/src/harness.ts +0 -2747
  293. package/spec-cli/src/headless-controller.ts +0 -63
  294. package/spec-cli/src/help.ts +0 -480
  295. package/spec-cli/src/host-resources.ts +0 -707
  296. package/spec-cli/src/host.ts +0 -611
  297. package/spec-cli/src/identity-presets.d.ts +0 -13
  298. package/spec-cli/src/identity-presets.js +0 -128
  299. package/spec-cli/src/index.ts +0 -730
  300. package/spec-cli/src/init.ts +0 -258
  301. package/spec-cli/src/issues-cli.ts +0 -276
  302. package/spec-cli/src/issues.ts +0 -237
  303. package/spec-cli/src/layout.ts +0 -557
  304. package/spec-cli/src/lint.ts +0 -458
  305. package/spec-cli/src/localIssues.ts +0 -538
  306. package/spec-cli/src/materialize.ts +0 -412
  307. package/spec-cli/src/mentions.ts +0 -81
  308. package/spec-cli/src/opencode-headless.ts +0 -205
  309. package/spec-cli/src/pi-headless.ts +0 -153
  310. package/spec-cli/src/plugin-harness.ts +0 -157
  311. package/spec-cli/src/process-identity.ts +0 -207
  312. package/spec-cli/src/project-identity.ts +0 -73
  313. package/spec-cli/src/project-store.ts +0 -17
  314. package/spec-cli/src/pty-bridge.ts +0 -396
  315. package/spec-cli/src/pty-helper.mjs +0 -96
  316. package/spec-cli/src/pty-native-helper.mjs +0 -22
  317. package/spec-cli/src/ranker.ts +0 -191
  318. package/spec-cli/src/reaper.ts +0 -125
  319. package/spec-cli/src/resilience.ts +0 -41
  320. package/spec-cli/src/reviewSnapshot.ts +0 -28
  321. package/spec-cli/src/reviews.ts +0 -405
  322. package/spec-cli/src/runtime-guard.ts +0 -35
  323. package/spec-cli/src/runtime-ownership.ts +0 -88
  324. package/spec-cli/src/search.bench.mjs +0 -80
  325. package/spec-cli/src/search.ts +0 -63
  326. package/spec-cli/src/session-cursors.ts +0 -91
  327. package/spec-cli/src/session-follow.ts +0 -182
  328. package/spec-cli/src/session-timeline.ts +0 -98
  329. package/spec-cli/src/sessions.ts +0 -3403
  330. package/spec-cli/src/sh.ts +0 -3
  331. package/spec-cli/src/slash-commands.ts +0 -318
  332. package/spec-cli/src/source-files.ts +0 -104
  333. package/spec-cli/src/specs.ts +0 -498
  334. package/spec-cli/src/supervise.ts +0 -245
  335. package/spec-cli/src/tree.ts +0 -118
  336. package/spec-cli/src/tsx-bin.ts +0 -18
  337. package/spec-cli/src/uninstall.ts +0 -171
  338. package/spec-cli/src/uploads.ts +0 -253
  339. package/spec-cli/src/worktree-sources.ts +0 -42
  340. package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +0 -8
  341. package/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +0 -28
  342. package/spec-cli/templates/spec/project/.plugins/core/spec.md +0 -23
  343. package/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +0 -32
  344. package/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +0 -9
  345. package/spec-cli/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +0 -15
  346. package/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +0 -18
  347. package/spec-dashboard/dist/assets/App-F9uaAVcH.js +0 -2
  348. package/spec-dashboard/dist/assets/Dashboard-Ba_jhxp1.js +0 -27
  349. package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +0 -1
  350. package/spec-dashboard/dist/assets/EvalsPage-FixoOg_n.js +0 -2
  351. package/spec-dashboard/dist/assets/IssuesPage-CuKLFhH3.js +0 -1
  352. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  353. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  354. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  355. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  356. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  357. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  358. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  359. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  360. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  361. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  362. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  363. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  364. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  365. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  366. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  367. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  368. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  369. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  370. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  371. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  372. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  373. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  374. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  375. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  376. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  377. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  378. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  379. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  380. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  381. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  382. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  383. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  384. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  385. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  386. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  387. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  388. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  389. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  390. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  391. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  392. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  393. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  394. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  395. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  396. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  397. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  398. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  399. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  400. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  401. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  402. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  403. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  404. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  405. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  406. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  407. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  408. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  409. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  410. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  411. package/spec-dashboard/dist/assets/MobileApp-CHgEHORJ.js +0 -2
  412. package/spec-dashboard/dist/assets/Modal-CQgYymmr.js +0 -1
  413. package/spec-dashboard/dist/assets/PageScroll-hT7UTLvD.js +0 -1
  414. package/spec-dashboard/dist/assets/ProjectsPage-CtXxakF9.js +0 -1
  415. package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +0 -32
  416. package/spec-dashboard/dist/assets/SessionInterface-Bpie-9fs.js +0 -39
  417. package/spec-dashboard/dist/assets/SessionWindow-CixDi4PI.js +0 -1
  418. package/spec-dashboard/dist/assets/Settings-C2MsucfE.js +0 -1
  419. package/spec-dashboard/dist/assets/Thread-C6Go8HRh.js +0 -13
  420. package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +0 -1
  421. package/spec-dashboard/dist/assets/data-B-RQmit6.js +0 -1
  422. package/spec-dashboard/dist/assets/index-CsI8DElI.css +0 -1
  423. package/spec-dashboard/dist/assets/index-DrVao0Ep.js +0 -41
  424. package/spec-dashboard/dist/assets/launch-BBH02b1v.js +0 -276
  425. package/spec-dashboard/dist/assets/launch-Cp579UoJ.css +0 -1
  426. package/spec-dashboard/dist/assets/terminalFont-C6oGNKrf.js +0 -1
  427. package/spec-dashboard/dist/index.html +0 -29
  428. package/spec-dashboard/src/reviewFilters.js +0 -322
  429. package/spec-dashboard/src/reviewQuery.js +0 -174
  430. package/spec-dashboard/src/session.js +0 -199
  431. package/spec-eval/src/cache.ts +0 -57
  432. package/spec-eval/src/cli.ts +0 -889
  433. package/spec-eval/src/evaltab.ts +0 -262
  434. package/spec-eval/src/filing.ts +0 -39
  435. package/spec-eval/src/freshness.ts +0 -557
  436. package/spec-eval/src/humanok.ts +0 -28
  437. package/spec-eval/src/scenariofresh.ts +0 -244
  438. package/spec-eval/src/scenarios.ts +0 -672
  439. package/spec-eval/src/sessioneval.ts +0 -2226
  440. package/spec-eval/src/sidecar.ts +0 -93
  441. package/spec-eval/src/timeline.ts +0 -54
  442. package/spec-forge/src/__fixtures__/github-forge.json +0 -63
  443. package/spec-forge/src/cache.ts +0 -66
  444. package/spec-forge/src/cli.ts +0 -99
  445. package/spec-forge/src/drivers/github.ts +0 -132
  446. package/spec-forge/src/drivers/gitlab.ts +0 -153
  447. package/spec-forge/src/drivers.ts +0 -73
  448. package/spec-forge/src/links.ts +0 -72
  449. package/spec-forge/src/needs-eval.ts +0 -29
  450. package/spec-forge/src/port.ts +0 -36
  451. package/spec-forge/src/resident.ts +0 -46
  452. /package/{spec-cli → node_modules/@spexcode/spec-cli}/README.md +0 -0
  453. /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -0
  454. /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-sed-v0.fixture +0 -0
  455. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/commit-msg +0 -0
  456. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-checkout +0 -0
  457. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-merge +0 -0
  458. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/pre-commit +0 -0
  459. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/prepare-commit-msg +0 -0
  460. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/reference-transaction +0 -0
  461. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/extract/spec.md +0 -0
  462. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/regroup/spec.md +0 -0
  463. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/rename/spec.md +0 -0
  464. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/spec.md +0 -0
  465. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/tidy/spec.md +0 -0
  466. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/idle.sh +0 -0
  467. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/spec.md +0 -0
  468. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/mark-active.sh +0 -0
  469. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/spec.md +0 -0
  470. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/fail.sh +0 -0
  471. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/spec.md +0 -0
  472. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec.md +0 -0
  473. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec.md +0 -0
  474. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/spec.md +0 -0
  475. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/digest.mjs +0 -0
  476. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/spec.md +0 -0
  477. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/spec.md +0 -0
  478. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/spec.md +0 -0
  479. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/spec.md +0 -0
  480. /package/{spec-cli → node_modules/@spexcode/spec-core}/templates/spexcode.json +0 -0
@@ -0,0 +1,350 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { listSessions } from './sessions.js';
3
+ import { getBoard, getBoardForForgeRevision } from './graphCache.js';
4
+ import { buildSessionEvals } from '@spexcode/spec-eval/sessioneval';
5
+ import { evalTimeline } from '@spexcode/spec-eval/evaltab';
6
+ import { issuesEnabled as issuesEnabledForReview } from './localIssues.js';
7
+ import { issueStores as issueStoresForReview } from './issues.js';
8
+ import { hasReviewSnapshot, readReviewSnapshot } from '@spexcode/spec-core';
9
+ import { residentForgeRevision, residentForgeState } from '@spexcode/spec-forge/resident';
10
+ import { EVAL_FILTER_KIND, evalFilterModel, evalReviewState, issueFilterModel, tokenFilterState } from '@spexcode/spec-core/review';
11
+ import { EVAL_QUERY_DEFAULT, ISSUE_QUERY_DEFAULT, readToken } from '@spexcode/spec-core/review';
12
+ export const REVIEW_PER_PAGE = 25;
13
+ const revisionOf = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
14
+ export function reviewPageNumber(value) {
15
+ const raw = typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : Number.NaN;
16
+ return Number.isSafeInteger(raw) && raw > 0 ? raw : 1;
17
+ }
18
+ function responseModel(model) {
19
+ const facets = Object.fromEntries(Object.entries(model.facets ?? {}).map(([key, raw]) => {
20
+ const facet = raw;
21
+ return [key, {
22
+ key,
23
+ ...(facet.label != null ? { label: String(facet.label) } : {}),
24
+ value: String(facet.value ?? ''),
25
+ ...(facet.meaningful != null ? { meaningful: !!facet.meaningful } : {}),
26
+ options: (facet.options ?? []).map((option) => ({
27
+ value: String(option.value ?? ''),
28
+ ...(option.label != null ? { label: String(option.label) } : {}),
29
+ ...(Number.isFinite(option.count) ? { count: Number(option.count) } : {}),
30
+ })),
31
+ }];
32
+ }));
33
+ const section = model.section ? {
34
+ key: String(model.section.key),
35
+ ...(model.section.label != null ? { label: String(model.section.label) } : {}),
36
+ value: String(model.section.value ?? ''),
37
+ ...(model.section.meaningful != null ? { meaningful: !!model.section.meaningful } : {}),
38
+ options: (model.section.options ?? []).map((option) => ({
39
+ value: String(option.value ?? ''),
40
+ ...(option.label != null ? { label: String(option.label) } : {}),
41
+ ...(Number.isFinite(option.count) ? { count: Number(option.count) } : {}),
42
+ })),
43
+ } : null;
44
+ return { counts: { ...(model.sections ?? {}) }, facets, section };
45
+ }
46
+ export function paginateReview(source, shown, model, requestedPage, revisionInputs) {
47
+ const page = reviewPageNumber(requestedPage);
48
+ const total = shown.length;
49
+ const pageCount = Math.ceil(total / REVIEW_PER_PAGE);
50
+ const start = (page - 1) * REVIEW_PER_PAGE;
51
+ const overflow = pageCount > 0 ? page > pageCount : page > 1;
52
+ return {
53
+ items: shown.slice(start, start + REVIEW_PER_PAGE),
54
+ page,
55
+ perPage: REVIEW_PER_PAGE,
56
+ total,
57
+ sourceTotal: source.length,
58
+ pageCount,
59
+ prev: page > 1 ? page - 1 : null,
60
+ next: page < pageCount || overflow ? page + 1 : null,
61
+ revision: revisionOf({ page, source: revisionInputs }),
62
+ ...responseModel(model),
63
+ };
64
+ }
65
+ const issueOrder = (a, b) => String(b.created ?? '').localeCompare(String(a.created ?? ''))
66
+ || String(a.id ?? '').localeCompare(String(b.id ?? ''));
67
+ export async function issuesReview(query, requestedPage) {
68
+ // The first request must wait for the first atomic publication. Once one exists, a graph refresh may be
69
+ // rebuilding unrelated board/session state; the published review source remains a valid answer and its
70
+ // revision/poll path will deliver the next generation without making this page join that flight.
71
+ residentForgeState();
72
+ if (!hasReviewSnapshot())
73
+ await getBoard();
74
+ else if (readReviewSnapshot().forgeRevision < residentForgeRevision()) {
75
+ await getBoardForForgeRevision(residentForgeRevision());
76
+ }
77
+ const sessions = await listSessions();
78
+ const issues = readReviewSnapshot().issues.slice().sort(issueOrder);
79
+ const text = String(query ?? '').trim() || ISSUE_QUERY_DEFAULT;
80
+ const model = issueFilterModel(issues, tokenFilterState(text, 'issue'), { sessions, defaultSection: '' });
81
+ return {
82
+ enabled: issuesEnabledForReview(),
83
+ stores: issueStoresForReview(),
84
+ ...paginateReview(issues, model.shown, model, requestedPage, {
85
+ domain: 'issues', issues, sessions: sessions.map((session) => session.id),
86
+ }),
87
+ };
88
+ }
89
+ const byNewest = (a, b) => Number(b.filterKind === EVAL_FILTER_KIND.RESULT) - Number(a.filterKind === EVAL_FILTER_KIND.RESULT)
90
+ || String(b.ts ?? '').localeCompare(String(a.ts ?? ''))
91
+ || String(a.node ?? '').localeCompare(String(b.node ?? ''))
92
+ || String(a.scenario ?? '').localeCompare(String(b.scenario ?? ''));
93
+ export function trunkEvalReviewItems(nodes) {
94
+ const items = [];
95
+ for (const node of nodes ?? []) {
96
+ const latest = new Map();
97
+ for (const reading of node.evals ?? [])
98
+ if (!latest.has(reading.scenario))
99
+ latest.set(reading.scenario, reading);
100
+ for (const scenario of node.scenarios ?? []) {
101
+ const reading = latest.get(scenario.name);
102
+ if (!reading) {
103
+ items.push({
104
+ scenario: scenario.name,
105
+ expected: scenario.expected,
106
+ tags: scenario.tags,
107
+ node: node.id,
108
+ hue: node.hue,
109
+ filterKind: EVAL_FILTER_KIND.BLIND,
110
+ });
111
+ continue;
112
+ }
113
+ items.push({
114
+ ...reading,
115
+ expected: scenario.expected ?? reading.expected,
116
+ tags: scenario.tags,
117
+ state: evalReviewState(reading),
118
+ node: node.id,
119
+ hue: node.hue,
120
+ filterKind: EVAL_FILTER_KIND.RESULT,
121
+ });
122
+ }
123
+ }
124
+ return items.sort(byNewest);
125
+ }
126
+ export function scopedEvalReviewItems(model) {
127
+ const items = [];
128
+ for (const node of model.nodes ?? []) {
129
+ const latest = new Map();
130
+ for (const reading of node.evals ?? [])
131
+ if (!latest.has(reading.scenario))
132
+ latest.set(reading.scenario, reading);
133
+ for (const scenario of node.scenarios ?? []) {
134
+ const reading = latest.get(scenario.name);
135
+ if (!reading) {
136
+ items.push({
137
+ scenario: scenario.name,
138
+ expected: scenario.expected,
139
+ tags: scenario.tags,
140
+ impact: scenario.impact,
141
+ node: node.id,
142
+ hue: node.hue,
143
+ filterKind: EVAL_FILTER_KIND.BLIND,
144
+ });
145
+ continue;
146
+ }
147
+ const item = {
148
+ ...reading,
149
+ expected: scenario.expected ?? reading.expected,
150
+ tags: scenario.tags,
151
+ impact: scenario.impact,
152
+ state: evalReviewState(reading),
153
+ node: node.id,
154
+ hue: node.hue,
155
+ filterKind: EVAL_FILTER_KIND.RESULT,
156
+ };
157
+ items.push(item);
158
+ }
159
+ }
160
+ return items.sort(byNewest);
161
+ }
162
+ const evalItemKey = (item) => `${String(item?.node ?? '')}\0${String(item?.scenario ?? '')}`;
163
+ function evalNeighbor(item, stateOf) {
164
+ return {
165
+ node: String(item.node),
166
+ scenario: String(item.scenario),
167
+ state: stateOf(item),
168
+ };
169
+ }
170
+ // `sequence` is the whole measured population in list order; `stateOf` answers only for rows whose freshness
171
+ // was actually computed. On a focused build those are two different sets — the sequence spans the scope, the
172
+ // states cover the selected row and its window — which is exactly why the state is looked up rather than
173
+ // carried: a row this response does not render contributes its POSITION and nothing else.
174
+ export function boundedEvalNeighbors(sequence, node, scenario, stateOf, want = 5) {
175
+ const items = sequence;
176
+ const key = `${node}\0${scenario}`;
177
+ const index = items.findIndex((item) => evalItemKey(item) === key);
178
+ if (index < 0)
179
+ return { prev: [], next: [], total: items.length, index: null, order: 'default' };
180
+ const before = index;
181
+ const after = items.length - index - 1;
182
+ const take = Math.min(want, before + after);
183
+ const nextN = Math.min(after, Math.max(Math.ceil(take / 2), take - before));
184
+ const prevN = Math.min(before, take - nextN);
185
+ return {
186
+ prev: items.slice(index - prevN, index).reverse().map((item) => evalNeighbor(item, stateOf)),
187
+ next: items.slice(index + 1, index + 1 + nextN).map((item) => evalNeighbor(item, stateOf)),
188
+ total: items.length,
189
+ index,
190
+ order: 'default',
191
+ };
192
+ }
193
+ export function projectEvalDetail(items, historySource, node, scenario, metadata = {}) {
194
+ const results = items.filter((item) => item.filterKind === EVAL_FILTER_KIND.RESULT);
195
+ const selected = results.find((item) => evalItemKey(item) === `${node}\0${scenario}`) ?? null;
196
+ const availability = selected
197
+ ? 'measured'
198
+ : items.some((item) => evalItemKey(item) === `${node}\0${scenario}` && item.filterKind === EVAL_FILTER_KIND.BLIND)
199
+ ? 'unmeasured'
200
+ : 'missing';
201
+ const history = historySource.filter((reading) => String(reading.scenario) === scenario);
202
+ const stateByKey = new Map(results.map((item) => [evalItemKey(item), String(item.state ?? evalReviewState(item))]));
203
+ const sequence = metadata.sequence
204
+ ?? results.map((item) => ({ node: String(item.node), scenario: String(item.scenario) }));
205
+ const neighbors = boundedEvalNeighbors(sequence, node, scenario, (row) => stateByKey.get(evalItemKey(row)) ?? 'empty');
206
+ const scope = metadata.scope ?? null;
207
+ const requestedScope = metadata.requestedScope ?? scope;
208
+ const scopeFallback = metadata.scopeFallback ?? null;
209
+ return {
210
+ scope,
211
+ requestedScope,
212
+ scopeFallback,
213
+ availability,
214
+ selected,
215
+ history,
216
+ neighbors,
217
+ revision: revisionOf({ scope, requestedScope, scopeFallback, availability, selected, history, neighbors, summary: metadata.summary, evalRevision: metadata.evalRevision }),
218
+ ...(metadata.summary ? { summary: metadata.summary } : {}),
219
+ ...(metadata.evalRevision ? { evalRevision: metadata.evalRevision } : {}),
220
+ };
221
+ }
222
+ // the measured population in list order, from the freshness-free rows: a filed reading leads (newest first),
223
+ // and the tie-breaks are the identity ones — the SAME comparison `byNewest` applies, over the only fields it
224
+ // actually reads. Blind rows never enter, exactly as the detail's own `results` filter excludes them.
225
+ export function measuredSequence(order) {
226
+ return order.filter((row) => row.ts)
227
+ .sort((a, b) => String(b.ts ?? '').localeCompare(String(a.ts ?? ''))
228
+ || a.node.localeCompare(b.node) || a.scenario.localeCompare(b.scenario))
229
+ .map((row) => ({ node: row.node, scenario: row.scenario }));
230
+ }
231
+ // the nodes whose verdicts the response will publish: the selected row's, plus a window wide enough to
232
+ // contain any neighbour boundedEvalNeighbors can choose (it takes at most five, split around the index).
233
+ export function focusNodes(order, node, scenario) {
234
+ const sequence = measuredSequence(order);
235
+ const index = sequence.findIndex((row) => row.node === node && row.scenario === scenario);
236
+ if (index < 0)
237
+ return [node];
238
+ return [...new Set([node, ...sequence.slice(Math.max(0, index - 6), index + 7).map((row) => row.node)])];
239
+ }
240
+ async function trunkEvalDetailReview(node, scenario, metadata = {}) {
241
+ await getBoard();
242
+ const snapshot = readReviewSnapshot();
243
+ const sourceNode = snapshot.evalNodes.find((candidate) => candidate.id === node);
244
+ return projectEvalDetail(trunkEvalReviewItems(snapshot.evalNodes), sourceNode?.readings ?? [], node, scenario, metadata);
245
+ }
246
+ export async function evalDetailReview(node, scenario, scope) {
247
+ if (scope) {
248
+ // A detail renders ONE row plus at most five neighbours, but owes the whole population's index/total.
249
+ // So name the window from the freshness-free sequence and let only those nodes pay the freshness pass;
250
+ // a build that finds a full cached model ignores the pick and answers from it instead.
251
+ const model = await buildSessionEvals(scope, (order) => focusNodes(order, node, scenario));
252
+ if (!model)
253
+ return trunkEvalDetailReview(node, scenario, { requestedScope: scope, scopeFallback: 'trunk' });
254
+ const sourceNode = model.nodes.find((candidate) => candidate.id === node);
255
+ return projectEvalDetail(scopedEvalReviewItems(model), sourceNode?.evals ?? [], node, scenario, {
256
+ scope,
257
+ requestedScope: scope,
258
+ summary: model.summary,
259
+ evalRevision: model.evalRevision,
260
+ ...(model.order ? { sequence: measuredSequence(model.order) } : {}),
261
+ });
262
+ }
263
+ return trunkEvalDetailReview(node, scenario);
264
+ }
265
+ export function timelineEvalReviewItems(timeline, node) {
266
+ const declared = new Set(timeline.scenarios.map((scenario) => scenario.name));
267
+ const latest = new Map();
268
+ for (const reading of timeline.readings) {
269
+ if (declared.has(reading.scenario) && !latest.has(reading.scenario))
270
+ latest.set(reading.scenario, reading);
271
+ }
272
+ return [
273
+ ...timeline.scenarios.filter((scenario) => !latest.has(scenario.name)).map((scenario) => ({
274
+ ...scenario,
275
+ scenario: scenario.name,
276
+ node,
277
+ filterKind: EVAL_FILTER_KIND.UNMEASURED,
278
+ })),
279
+ ...[...latest.values()].map((reading) => ({
280
+ ...reading,
281
+ state: evalReviewState(reading),
282
+ node,
283
+ filterKind: EVAL_FILTER_KIND.RESULT,
284
+ filterKey: `${EVAL_FILTER_KIND.RESULT}:${reading.scenario}`,
285
+ })),
286
+ ...(timeline.dangling ?? []).map((track) => ({
287
+ ...track,
288
+ node,
289
+ filterKind: EVAL_FILTER_KIND.DANGLING,
290
+ filterKey: `${EVAL_FILTER_KIND.DANGLING}:${track.threadId}`,
291
+ })),
292
+ ];
293
+ }
294
+ async function timelineEvalReview(text, requestedPage) {
295
+ const node = readToken(text, 'node');
296
+ if (!node)
297
+ return null;
298
+ const [timeline, sessions] = await Promise.all([evalTimeline(node), listSessions()]);
299
+ const items = timelineEvalReviewItems(timeline, node);
300
+ const filtered = evalFilterModel(items, tokenFilterState(text, 'eval'), { sessions, defaultKind: 'all', defaultSection: '' });
301
+ return {
302
+ scope: null,
303
+ view: 'timeline',
304
+ node,
305
+ hasEvalFile: timeline.hasEvalFile,
306
+ gates: [],
307
+ unknown: 0,
308
+ ...paginateReview(items, filtered.shown, filtered, requestedPage, {
309
+ domain: 'evals', view: 'timeline', node, timeline, sessions: sessions.map((session) => session.id),
310
+ }),
311
+ };
312
+ }
313
+ export async function evalsReview(query, requestedPage, options = {}) {
314
+ const text = String(query ?? '').trim() || EVAL_QUERY_DEFAULT;
315
+ if (options.view === 'timeline')
316
+ return timelineEvalReview(text, requestedPage);
317
+ const scope = readToken(text, 'scope') || null;
318
+ if (scope) {
319
+ const model = await buildSessionEvals(scope);
320
+ if (!model)
321
+ return null;
322
+ const items = scopedEvalReviewItems(model);
323
+ const sessions = await listSessions();
324
+ const filtered = evalFilterModel(items, tokenFilterState(text, 'eval'), { sessions, defaultKind: 'all', defaultSection: '' });
325
+ return {
326
+ scope,
327
+ gates: [],
328
+ unknown: model.nodes.reduce((count, node) => count + (node.unknownCoverage?.length ?? 0), 0),
329
+ summary: model.summary,
330
+ evalRevision: model.evalRevision,
331
+ ...paginateReview(items, filtered.shown, filtered, requestedPage, {
332
+ domain: 'evals', scope, query: text, gates: [], summary: model.summary,
333
+ evalRevision: model.evalRevision, sessions: sessions.map((session) => session.id),
334
+ }),
335
+ };
336
+ }
337
+ if (!hasReviewSnapshot())
338
+ await getBoard();
339
+ const sessions = await listSessions();
340
+ const items = trunkEvalReviewItems(readReviewSnapshot().evalNodes);
341
+ const filtered = evalFilterModel(items, tokenFilterState(text, 'eval'), { sessions, defaultKind: 'all', defaultSection: '' });
342
+ return {
343
+ scope: null,
344
+ gates: [],
345
+ unknown: 0,
346
+ ...paginateReview(items, filtered.shown, filtered, requestedPage, {
347
+ domain: 'evals', items, sessions: sessions.map((session) => session.id),
348
+ }),
349
+ };
350
+ }
@@ -0,0 +1,6 @@
1
+ export declare function hasTmux(): boolean;
2
+ export declare function sessionRuntimeBlock(env: {
3
+ hasTmux: boolean;
4
+ platform: string;
5
+ }): string[] | null;
6
+ export declare function assertSessionRuntime(): void;
@@ -0,0 +1,37 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ // tmux presence is the primitive we actually depend on, so probe THAT rather than assuming by platform:
3
+ // this also catches a bare POSIX box that simply hasn't installed tmux, not only native Windows.
4
+ export function hasTmux() {
5
+ try {
6
+ return spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ // Pure so it is unit-testable without spawning or exiting: null = runtime OK; otherwise the stderr lines.
13
+ // The pointer branches on platform because the honest repair differs — WSL2 on Windows (no POSIX analog at
14
+ // all), install-tmux on a POSIX host that merely lacks it.
15
+ export function sessionRuntimeBlock(env) {
16
+ if (env.hasTmux)
17
+ return null;
18
+ const lines = ['spex: the session runtime needs a POSIX host (tmux, bash, and unix-domain sockets).'];
19
+ if (env.platform === 'win32') {
20
+ lines.push('Native Windows has no analog for these — run SpexCode under WSL2 (a real Linux kernel):');
21
+ lines.push(' wsl --install # then, in the Ubuntu shell: nvm install 22 && npm i -g spexcode');
22
+ }
23
+ else {
24
+ lines.push('tmux is not on PATH — install it and retry (e.g. `apt install tmux`, `brew install tmux`).');
25
+ }
26
+ return lines;
27
+ }
28
+ // Called at the top of the session-launching command path (`spex serve`). Exits 69 (EX_UNAVAILABLE: a
29
+ // required support program does not exist) — a distinct, honest code, not a swallowed error or a stacktrace.
30
+ export function assertSessionRuntime() {
31
+ const block = sessionRuntimeBlock({ hasTmux: hasTmux(), platform: process.platform });
32
+ if (!block)
33
+ return;
34
+ for (const line of block)
35
+ console.error(line);
36
+ process.exit(69);
37
+ }
@@ -0,0 +1,21 @@
1
+ import { type ProcessIdentity } from '@spexcode/spec-core';
2
+ export type BackendInstanceRecord = {
3
+ version: 1;
4
+ instanceId: string;
5
+ pid: number;
6
+ startToken: string;
7
+ projectRoot: string;
8
+ startedAt: string;
9
+ };
10
+ export declare function readBackendInstanceRecords(root?: string): BackendInstanceRecord[];
11
+ export declare function registerBackendInstance(instanceId: string, pid?: number, root?: string): BackendInstanceRecord;
12
+ export declare function unregisterBackendInstance(instanceId: string, pid?: number): void;
13
+ export declare function spawnDetachedRuntime(opts: {
14
+ cwd: string;
15
+ logFile: string;
16
+ pidFile: string;
17
+ receiptFile: string;
18
+ command: string;
19
+ args: string[];
20
+ env?: NodeJS.ProcessEnv;
21
+ }): ProcessIdentity;
@@ -0,0 +1,84 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import { closeSync, mkdirSync, openSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { repoRoot } from '@spexcode/spec-core';
6
+ import { readJsonConfig, runtimeRoot } from '@spexcode/spec-core';
7
+ import { processStartToken, writeDetachedRuntimeReceipt } from '@spexcode/spec-core';
8
+ const backendInstancesDir = () => join(runtimeRoot(), 'backend-instances');
9
+ const backendInstancePath = (instanceId) => join(backendInstancesDir(), `${instanceId}.json`);
10
+ const atomicJson = (path, value) => {
11
+ mkdirSync(dirname(path), { recursive: true });
12
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
13
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
14
+ renameSync(tmp, path);
15
+ };
16
+ export function readBackendInstanceRecords(root = repoRoot()) {
17
+ let names;
18
+ try {
19
+ names = readdirSync(backendInstancesDir());
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ return names.flatMap((name) => {
25
+ try {
26
+ const rec = readJsonConfig(join(backendInstancesDir(), name));
27
+ return rec?.version === 1 && rec.projectRoot === root ? [rec] : [];
28
+ }
29
+ catch {
30
+ return [];
31
+ }
32
+ });
33
+ }
34
+ export function registerBackendInstance(instanceId, pid = process.pid, root = repoRoot()) {
35
+ const startToken = processStartToken(pid);
36
+ if (!startToken)
37
+ throw new Error(`cannot identify backend supervisor PID ${pid}`);
38
+ const record = { version: 1, instanceId, pid, startToken, projectRoot: root, startedAt: new Date().toISOString() };
39
+ atomicJson(backendInstancePath(instanceId), record);
40
+ return record;
41
+ }
42
+ export function unregisterBackendInstance(instanceId, pid = process.pid) {
43
+ try {
44
+ const record = readJsonConfig(backendInstancePath(instanceId));
45
+ if (record.instanceId === instanceId && record.pid === pid && record.startToken === processStartToken(pid))
46
+ rmSync(backendInstancePath(instanceId), { force: true });
47
+ }
48
+ catch { /* not ours / already removed */ }
49
+ }
50
+ export function spawnDetachedRuntime(opts) {
51
+ mkdirSync(dirname(opts.logFile), { recursive: true });
52
+ mkdirSync(dirname(opts.pidFile), { recursive: true });
53
+ const log = openSync(opts.logFile, 'a', 0o600);
54
+ let child;
55
+ try {
56
+ child = spawn(opts.command, opts.args, {
57
+ cwd: opts.cwd,
58
+ env: opts.env ?? process.env,
59
+ detached: true,
60
+ stdio: ['ignore', log, log],
61
+ });
62
+ }
63
+ finally {
64
+ closeSync(log);
65
+ }
66
+ child.once('error', () => { });
67
+ if (!child.pid)
68
+ throw new Error(`could not spawn detached shared runtime: ${opts.command}`);
69
+ child.unref();
70
+ try {
71
+ const identity = writeDetachedRuntimeReceipt(child.pid, opts.receiptFile);
72
+ writeFileSync(opts.pidFile, `${child.pid}\n`, { mode: 0o600 });
73
+ return { pid: identity.pid, startToken: identity.startToken };
74
+ }
75
+ catch (error) {
76
+ try {
77
+ child.kill('SIGTERM');
78
+ }
79
+ catch { /* exact just-spawned child already exited */ }
80
+ rmSync(opts.pidFile, { force: true });
81
+ rmSync(opts.receiptFile, { force: true });
82
+ throw error;
83
+ }
84
+ }
@@ -0,0 +1,75 @@
1
+ // throwaway benchmark harness for spec-search — drives the REAL `spex spec search --json` over the holdout
2
+ // cases and reports recall@1, recall@3, MRR. The cases live in the node's eval.md.
3
+ //
4
+ // Labels are node LEAF names, matched with the same de-collision rule the loader applies (specs.ts reId):
5
+ // a returned id matches a label if it IS the label or ends with `_<label>` — so a bare leaf keeps matching
6
+ // after collision-qualification renames it (e.g. a colliding `probe` leaf → `a_probe`). A label may
7
+ // also be written pre-qualified to pin one collision branch.
8
+ import { execFileSync } from 'node:child_process';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { dirname, join } from 'node:path';
11
+ const BIN = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'spex.mjs');
12
+ const CASES = [
13
+ ['exit-cleanup', "does /exit remove the session's worktree and tmux, or just orphan them?", ['session-console']],
14
+ ['owner-at-edit', 'how does an agent learn which spec governs a file it just edited?', ['spec-of-file']],
15
+ ['main-block', 'what stops an agent from committing or merging straight into main?', ['main-guard']],
16
+ ['main-escape', 'the escape hatch that lets seeding run on the main branch', ['main-guard']],
17
+ ['inter-agent-msg', 'how do two running agent sessions send messages to each other?', ['agent-reply-channel', 'comms']],
18
+ ['search-hidden-node', 'keyboard shortcut to find a node hidden inside a collapsed subtree', ['keyboard-nav']],
19
+ ['session-order', 'how is the order of sessions in the session list decided?', ['session-console']],
20
+ ['node-status', 'what makes a node show as pending vs active vs merged vs drift?', ['spec-node-states']],
21
+ ['dashboard-backend', 'how does the dashboard reach the backend API and on which port?', ['api-endpoint']],
22
+ ['loss-measured', "how is a node's loss measured and its scenarios scored?", ['eval-core']],
23
+ ['launch-injection', "what context gets injected into a freshly launched agent's prompt?", ['injected-context']],
24
+ ['read-before-code', 'the one-shot nudge that makes an agent read its spec before touching code', ['spec-first']],
25
+ ['hot-reload', 'zero-downtime backend reload without dropping connections', ['supervisor']],
26
+ ['many-owners', 'can several specs own the same code file, and what happens if too many do?', ['governed-related']],
27
+ ['active-spec-search', 'an injected sub-agent that searches specs for the agent, the spec analog of Explore', ['spec-search']],
28
+ ['declare-done', 'how does a worker declare it is done', ['state']],
29
+ ];
30
+ // de-collision-aware label match: exact id, or the id's trailing `_`-suffix is the label.
31
+ const matches = (id, label) => id === label || id.endsWith('_' + label);
32
+ let r1 = 0, r3 = 0, mrr = 0;
33
+ const rows = [];
34
+ for (const [name, query, expect] of CASES) {
35
+ const out = execFileSync('node', [BIN, 'spec', 'search', query, '--json', '--limit', '10'], { encoding: 'utf8' });
36
+ const results = JSON.parse(out);
37
+ const ids = results.map((x) => x.id);
38
+ const rank = ids.findIndex((id) => expect.some((label) => matches(id, label))) + 1; // 1-based; 0 = not found
39
+ const hit1 = rank === 1;
40
+ const hit3 = rank >= 1 && rank <= 3;
41
+ if (hit1)
42
+ r1++;
43
+ if (hit3)
44
+ r3++;
45
+ if (rank >= 1)
46
+ mrr += 1 / rank;
47
+ rows.push({ name, rank: rank || '—', top: ids.slice(0, 3).join(', '), expect: expect.join('|') });
48
+ }
49
+ const n = CASES.length;
50
+ for (const row of rows) {
51
+ const mark = row.rank === 1 ? '✓1' : (typeof row.rank === 'number' && row.rank <= 3 ? '·3' : (row.rank === '—' ? '✗ ' : `·${row.rank}`));
52
+ console.log(`${mark} ${row.name.padEnd(20)} want=${row.expect.padEnd(22)} rank=${String(row.rank).padStart(2)} top3: ${row.top}`);
53
+ }
54
+ // CJK-positive: the corpus is mostly English but a few nodes carry Chinese prose — the root `spexcode`
55
+ // node's body repeats 节点 throughout. A per-CJK-character tokenizer must let a Chinese content word reach
56
+ // that node; `spex spec search "节点"` MUST return `spexcode` in results (the bug: the old tokenizer split on
57
+ // [^a-z0-9]+ and discarded all CJK, so every Chinese query dead-ended at zero results).
58
+ const cjkPos = JSON.parse(execFileSync('node', [BIN, 'spec', 'search', '节点', '--json', '--limit', '10'], { encoding: 'utf8' }));
59
+ const cjkPosRank = cjkPos.map((x) => x.id).findIndex((id) => matches(id, 'spexcode')) + 1;
60
+ const cjkPosPass = cjkPosRank >= 1;
61
+ console.log(`${cjkPosPass ? `✓${cjkPosRank}` : '✗ '} cjk-positive want=spexcode(节点) rank=${cjkPosRank || '—'} top3: ${cjkPos.slice(0, 3).map((x) => x.id).join(', ')}`);
62
+ // zero-result fail-loud regression: a CJK query whose characters appear NOWHERE in the corpus (会/话) returns
63
+ // nothing, and the zero-result message must still carry the corpus-is-English translate-and-retry fact
64
+ // (fail-loud) plus the browse-all next step — CJK support does not suppress the honest zero-result route.
65
+ // (No nearest titles: a CJK query has nothing to be lexically near.)
66
+ const cjk = execFileSync('node', [BIN, 'spec', 'search', '会话'], { encoding: 'utf8' });
67
+ const cjkPass = cjk.includes('corpus is English') && cjk.includes('spex graph');
68
+ console.log(`${cjkPass ? '✓ ' : '✗ '} cjk-zero-result want=corpus-is-English + spex-tree ${cjkPass ? 'both present' : 'MISSING: ' + cjk.trim()}`);
69
+ // zero-result typo routing: an English typo that matches nothing must surface the nearest node titles
70
+ // (per-word edit distance) plus the browse-all next step, so a typo routes forward instead of dead-ending.
71
+ const typo = execFileSync('node', [BIN, 'spec', 'search', 'kyeboard'], { encoding: 'utf8' });
72
+ const typoPass = typo.includes('nearest titles') && typo.includes('keyboard-nav') && typo.includes('spex graph');
73
+ console.log(`${typoPass ? '✓ ' : '✗ '} typo-zero-result want=nearest-titles(keyboard-nav) + spex-tree ${typoPass ? 'both present' : 'MISSING: ' + typo.trim()}`);
74
+ console.log('—'.repeat(72));
75
+ console.log(`recall@1 = ${r1}/${n} = ${(r1 / n).toFixed(3)} recall@3 = ${r3}/${n} = ${(r3 / n).toFixed(3)} MRR = ${(mrr / n).toFixed(3)} cjk-positive = ${cjkPosPass ? 'PASS' : 'FAIL'} cjk-hint = ${cjkPass ? 'PASS' : 'FAIL'} typo-route = ${typoPass ? 'PASS' : 'FAIL'}`);
@@ -0,0 +1,20 @@
1
+ export type SearchResult = {
2
+ id: string;
3
+ title: string;
4
+ path: string;
5
+ score: number;
6
+ snippet: string;
7
+ };
8
+ export type SearchStats = {
9
+ nodes: number;
10
+ tokens: number;
11
+ ms: number;
12
+ };
13
+ export declare function nearestTitles(query: string, n?: number): {
14
+ id: string;
15
+ title: string;
16
+ }[];
17
+ export declare function searchSpecs(query: string, opts?: {
18
+ limit?: number;
19
+ onStats?: (s: SearchStats) => void;
20
+ }): Promise<SearchResult[]>;
@@ -0,0 +1,61 @@
1
+ import { loadSpecsLite } from '@spexcode/spec-core';
2
+ import { rankDocs } from './ranker.js';
3
+ // zero-result fallback: nearest node titles by per-word edit distance over title+id — the lightweight
4
+ // string distance that routes a typo'd query to a next step. Each query word takes its best normalised
5
+ // Levenshtein similarity against any title/id word (≥0.5, so unrelated words don't accumulate), then the
6
+ // words sum. Same loadSpecsLite read as the ranker; deliberately NOT part of rankDocs scoring (this
7
+ // tolerates typos, the ranker must not).
8
+ export function nearestTitles(query, n = 3) {
9
+ const words = (s) => s.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 1);
10
+ const lev = (a, b) => {
11
+ const dp = Array.from({ length: b.length + 1 }, (_, i) => i);
12
+ for (let i = 1; i <= a.length; i++) {
13
+ let prev = dp[0];
14
+ dp[0] = i;
15
+ for (let j = 1; j <= b.length; j++) {
16
+ const cur = dp[j];
17
+ dp[j] = Math.min(dp[j] + 1, dp[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
18
+ prev = cur;
19
+ }
20
+ }
21
+ return dp[b.length];
22
+ };
23
+ const qws = words(query);
24
+ if (!qws.length)
25
+ return []; // nothing to be "near" (e.g. a pure-CJK query) — the caller still points at spex graph
26
+ return loadSpecsLite()
27
+ .map((s) => {
28
+ const tws = [...new Set(words(`${s.title} ${s.id}`))];
29
+ let score = 0;
30
+ for (const q of qws) {
31
+ let best = 0;
32
+ for (const t of tws)
33
+ best = Math.max(best, 1 - lev(q, t) / Math.max(q.length, t.length));
34
+ if (best >= 0.5)
35
+ score += best;
36
+ }
37
+ return { id: s.id, title: s.title, score };
38
+ })
39
+ .filter((s) => s.score > 0)
40
+ .sort((a, b) => b.score - a.score)
41
+ .slice(0, n)
42
+ .map(({ id, title }) => ({ id, title }));
43
+ }
44
+ export async function searchSpecs(query, opts = {}) {
45
+ const t0 = performance.now();
46
+ // pre-sort by (shorter id, then id): rankDocs sorts stably, so for equal-scored nodes this IS the tiebreak.
47
+ const nodes = loadSpecsLite()
48
+ .slice()
49
+ .sort((a, b) => a.id.length - b.id.length || a.id.localeCompare(b.id));
50
+ const inputs = nodes.map((s) => ({
51
+ ref: s, name: `${s.title} ${s.id}`, desc: s.desc, body: s.body,
52
+ }));
53
+ const out = rankDocs(query, inputs, { limit: opts.limit }).map((r) => ({
54
+ id: r.ref.id, title: r.ref.title, path: r.ref.path, score: r.score, snippet: r.snippet,
55
+ }));
56
+ if (opts.onStats) {
57
+ const tokens = inputs.reduce((a, i) => a + `${i.name} ${i.desc} ${i.body}`.split(/[^a-z0-9]+/i).filter(Boolean).length, 0);
58
+ opts.onStats({ nodes: inputs.length, tokens, ms: performance.now() - t0 });
59
+ }
60
+ return out;
61
+ }