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
@@ -1,405 +0,0 @@
1
- import { createHash } from 'node:crypto'
2
- import { listSessions } from './sessions.js'
3
- import { getBoard, getBoardForForgeRevision } from './graphCache.js'
4
- import { type SessionEvalOrderRow, buildSessionEvals, type SessionEvals } from '../../spec-eval/src/sessioneval.js'
5
- import { evalTimeline } from '../../spec-eval/src/evaltab.js'
6
- import { issuesEnabled as issuesEnabledForReview } from './localIssues.js'
7
- import { issueStores as issueStoresForReview } from './issues.js'
8
- import { hasReviewSnapshot, readReviewSnapshot } from './reviewSnapshot.js'
9
- import { residentForgeRevision, residentForgeState } from '../../spec-forge/src/resident.js'
10
- // @ts-expect-error The dashboard module is deliberately plain JS so the browser and server execute the
11
- // exact same tokenizer/matcher. It is shipped beside the built dashboard by the root package manifest.
12
- import { EVAL_FILTER_KIND, evalFilterModel, evalReviewState, issueFilterModel, tokenFilterState } from '../../spec-dashboard/src/reviewFilters.js'
13
- // @ts-expect-error See the shared-domain note above.
14
- import { EVAL_QUERY_DEFAULT, ISSUE_QUERY_DEFAULT, readToken } from '../../spec-dashboard/src/reviewQuery.js'
15
-
16
- export const REVIEW_PER_PAGE = 25
17
-
18
- type ReviewItem = Record<string, unknown>
19
- // A section count is one number, or — when the domain adapter splits that section — its named buckets,
20
- // which re-add to the same whole population ([[review-filters]] owns the split; Evals splits its measured
21
- // verdicts into {fresh,stale} so the remeasurement debt travels with the count instead of being recomputed
22
- // per surface). The fold happens ONCE, here on the server, over the complete filtered population.
23
- export type ReviewCount = number | Record<string, number>
24
- type ReviewOption = { value: string; label?: string; count?: number }
25
- type ReviewFacet = { key: string; label?: string; value: string; meaningful?: boolean; options: ReviewOption[] }
26
- type EvalNeighbor = { node: string; scenario: string; state: string }
27
-
28
- export type PagedReview<T extends ReviewItem = ReviewItem> = {
29
- items: T[]
30
- page: number
31
- perPage: number
32
- total: number
33
- sourceTotal: number
34
- pageCount: number
35
- prev: number | null
36
- next: number | null
37
- revision: string
38
- counts: Record<string, ReviewCount>
39
- facets: Record<string, ReviewFacet>
40
- section: { key: string; value: string; options: ReviewOption[] } | null
41
- }
42
-
43
- export type EvalDetailReview = {
44
- scope: string | null
45
- selected: ReviewItem | null
46
- history: ReviewItem[]
47
- neighbors: {
48
- prev: EvalNeighbor[]
49
- next: EvalNeighbor[]
50
- total: number
51
- index: number | null
52
- order: 'default'
53
- }
54
- revision: string
55
- summary?: SessionEvals['summary']
56
- evalRevision?: SessionEvals['evalRevision']
57
- }
58
-
59
- const revisionOf = (value: unknown): string => createHash('sha256').update(JSON.stringify(value)).digest('hex')
60
-
61
- export function reviewPageNumber(value: unknown): number {
62
- const raw = typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : Number.NaN
63
- return Number.isSafeInteger(raw) && raw > 0 ? raw : 1
64
- }
65
-
66
- function responseModel(model: any): Pick<PagedReview, 'counts' | 'facets' | 'section'> {
67
- const facets = Object.fromEntries(Object.entries(model.facets ?? {}).map(([key, raw]) => {
68
- const facet = raw as any
69
- return [key, {
70
- key,
71
- ...(facet.label != null ? { label: String(facet.label) } : {}),
72
- value: String(facet.value ?? ''),
73
- ...(facet.meaningful != null ? { meaningful: !!facet.meaningful } : {}),
74
- options: (facet.options ?? []).map((option: any) => ({
75
- value: String(option.value ?? ''),
76
- ...(option.label != null ? { label: String(option.label) } : {}),
77
- ...(Number.isFinite(option.count) ? { count: Number(option.count) } : {}),
78
- })),
79
- }]
80
- })) as Record<string, ReviewFacet>
81
- const section = model.section ? {
82
- key: String(model.section.key),
83
- ...(model.section.label != null ? { label: String(model.section.label) } : {}),
84
- value: String(model.section.value ?? ''),
85
- ...(model.section.meaningful != null ? { meaningful: !!model.section.meaningful } : {}),
86
- options: (model.section.options ?? []).map((option: any) => ({
87
- value: String(option.value ?? ''),
88
- ...(option.label != null ? { label: String(option.label) } : {}),
89
- ...(Number.isFinite(option.count) ? { count: Number(option.count) } : {}),
90
- })),
91
- } : null
92
- return { counts: { ...(model.sections ?? {}) }, facets, section }
93
- }
94
-
95
- export function paginateReview<T extends ReviewItem>(
96
- source: T[],
97
- shown: T[],
98
- model: any,
99
- requestedPage: unknown,
100
- revisionInputs: unknown,
101
- ): PagedReview<T> {
102
- const page = reviewPageNumber(requestedPage)
103
- const total = shown.length
104
- const pageCount = Math.ceil(total / REVIEW_PER_PAGE)
105
- const start = (page - 1) * REVIEW_PER_PAGE
106
- const overflow = pageCount > 0 ? page > pageCount : page > 1
107
- return {
108
- items: shown.slice(start, start + REVIEW_PER_PAGE),
109
- page,
110
- perPage: REVIEW_PER_PAGE,
111
- total,
112
- sourceTotal: source.length,
113
- pageCount,
114
- prev: page > 1 ? page - 1 : null,
115
- next: page < pageCount || overflow ? page + 1 : null,
116
- revision: revisionOf(revisionInputs),
117
- ...responseModel(model),
118
- }
119
- }
120
-
121
- const issueOrder = (a: any, b: any): number => String(b.created ?? '').localeCompare(String(a.created ?? ''))
122
- || String(a.id ?? '').localeCompare(String(b.id ?? ''))
123
-
124
- export async function issuesReview(query: string | undefined, requestedPage: unknown) {
125
- // The first request must wait for the first atomic publication. Once one exists, a graph refresh may be
126
- // rebuilding unrelated board/session state; the published review source remains a valid answer and its
127
- // revision/poll path will deliver the next generation without making this page join that flight.
128
- residentForgeState()
129
- if (!hasReviewSnapshot()) await getBoard()
130
- else if (readReviewSnapshot().forgeRevision < residentForgeRevision()) {
131
- await getBoardForForgeRevision(residentForgeRevision())
132
- }
133
- const sessions = await listSessions()
134
- const issues = readReviewSnapshot().issues.slice().sort(issueOrder)
135
- const text = String(query ?? '').trim() || ISSUE_QUERY_DEFAULT
136
- const model = issueFilterModel(issues, tokenFilterState(text, 'issue'), { sessions, defaultSection: '' })
137
- return {
138
- enabled: issuesEnabledForReview(),
139
- stores: issueStoresForReview(),
140
- ...paginateReview(issues, model.shown, model, requestedPage, {
141
- domain: 'issues', issues, sessions: sessions.map((session) => session.id),
142
- }),
143
- }
144
- }
145
-
146
- const byNewest = (a: any, b: any): number => Number(b.filterKind === EVAL_FILTER_KIND.RESULT) - Number(a.filterKind === EVAL_FILTER_KIND.RESULT)
147
- || String(b.ts ?? '').localeCompare(String(a.ts ?? ''))
148
- || String(a.node ?? '').localeCompare(String(b.node ?? ''))
149
- || String(a.scenario ?? '').localeCompare(String(b.scenario ?? ''))
150
-
151
- export function trunkEvalReviewItems(nodes: any[]): ReviewItem[] {
152
- const items: any[] = []
153
- for (const node of nodes ?? []) {
154
- const latest = new Map<string, any>()
155
- for (const reading of node.evals ?? []) if (!latest.has(reading.scenario)) latest.set(reading.scenario, reading)
156
- for (const scenario of node.scenarios ?? []) {
157
- const reading = latest.get(scenario.name)
158
- if (!reading) {
159
- items.push({
160
- scenario: scenario.name,
161
- expected: scenario.expected,
162
- tags: scenario.tags,
163
- node: node.id,
164
- hue: node.hue,
165
- filterKind: EVAL_FILTER_KIND.BLIND,
166
- })
167
- continue
168
- }
169
- items.push({
170
- ...reading,
171
- expected: scenario.expected ?? reading.expected,
172
- tags: scenario.tags,
173
- state: evalReviewState(reading),
174
- node: node.id,
175
- hue: node.hue,
176
- filterKind: EVAL_FILTER_KIND.RESULT,
177
- })
178
- }
179
- }
180
- return items.sort(byNewest)
181
- }
182
-
183
- export function scopedEvalReviewItems(model: SessionEvals): ReviewItem[] {
184
- const items: any[] = []
185
- for (const node of model.nodes ?? []) {
186
- const latest = new Map<string, any>()
187
- for (const reading of node.evals ?? []) if (!latest.has(reading.scenario)) latest.set(reading.scenario, reading)
188
- for (const scenario of node.scenarios ?? []) {
189
- const reading = latest.get(scenario.name)
190
- if (!reading) {
191
- items.push({
192
- scenario: scenario.name,
193
- expected: scenario.expected,
194
- tags: scenario.tags,
195
- impact: scenario.impact,
196
- node: node.id,
197
- hue: node.hue,
198
- filterKind: EVAL_FILTER_KIND.BLIND,
199
- })
200
- continue
201
- }
202
- const item = {
203
- ...reading,
204
- expected: scenario.expected ?? reading.expected,
205
- tags: scenario.tags,
206
- impact: scenario.impact,
207
- state: evalReviewState(reading),
208
- node: node.id,
209
- hue: node.hue,
210
- filterKind: EVAL_FILTER_KIND.RESULT,
211
- }
212
- items.push(item)
213
- }
214
- }
215
- return items.sort(byNewest)
216
- }
217
-
218
- const evalItemKey = (item: any): string => `${String(item?.node ?? '')}\0${String(item?.scenario ?? '')}`
219
-
220
- function evalNeighbor(item: any, stateOf: (row: { node: string; scenario: string }) => string): EvalNeighbor {
221
- return {
222
- node: String(item.node),
223
- scenario: String(item.scenario),
224
- state: stateOf(item),
225
- }
226
- }
227
-
228
- // `sequence` is the whole measured population in list order; `stateOf` answers only for rows whose freshness
229
- // was actually computed. On a focused build those are two different sets — the sequence spans the scope, the
230
- // states cover the selected row and its window — which is exactly why the state is looked up rather than
231
- // carried: a row this response does not render contributes its POSITION and nothing else.
232
- export function boundedEvalNeighbors(
233
- sequence: { node: string; scenario: string }[],
234
- node: string,
235
- scenario: string,
236
- stateOf: (row: { node: string; scenario: string }) => string,
237
- want = 5,
238
- ) {
239
- const items = sequence
240
- const key = `${node}\0${scenario}`
241
- const index = items.findIndex((item) => evalItemKey(item) === key)
242
- if (index < 0) return { prev: [], next: [], total: items.length, index: null, order: 'default' as const }
243
- const before = index
244
- const after = items.length - index - 1
245
- const take = Math.min(want, before + after)
246
- const nextN = Math.min(after, Math.max(Math.ceil(take / 2), take - before))
247
- const prevN = Math.min(before, take - nextN)
248
- return {
249
- prev: items.slice(index - prevN, index).reverse().map((item) => evalNeighbor(item, stateOf)),
250
- next: items.slice(index + 1, index + 1 + nextN).map((item) => evalNeighbor(item, stateOf)),
251
- total: items.length,
252
- index,
253
- order: 'default' as const,
254
- }
255
- }
256
-
257
- export function projectEvalDetail(
258
- items: ReviewItem[],
259
- historySource: ReviewItem[],
260
- node: string,
261
- scenario: string,
262
- metadata: {
263
- scope?: string | null
264
- summary?: SessionEvals['summary']
265
- evalRevision?: SessionEvals['evalRevision']
266
- // the scope's whole measured sequence, when `items` deliberately holds only the rendered window
267
- sequence?: { node: string; scenario: string }[]
268
- } = {},
269
- ): EvalDetailReview {
270
- const results = items.filter((item: any) => item.filterKind === EVAL_FILTER_KIND.RESULT)
271
- const selected = results.find((item) => evalItemKey(item) === `${node}\0${scenario}`) ?? null
272
- const history = historySource.filter((reading: any) => String(reading.scenario) === scenario)
273
- const stateByKey = new Map(results.map((item: any) => [evalItemKey(item), String(item.state ?? evalReviewState(item))]))
274
- const sequence = metadata.sequence
275
- ?? results.map((item: any) => ({ node: String(item.node), scenario: String(item.scenario) }))
276
- const neighbors = boundedEvalNeighbors(sequence, node, scenario,
277
- (row) => stateByKey.get(evalItemKey(row)) ?? 'empty')
278
- const scope = metadata.scope ?? null
279
- return {
280
- scope,
281
- selected,
282
- history,
283
- neighbors,
284
- revision: revisionOf({ scope, selected, history, neighbors, summary: metadata.summary, evalRevision: metadata.evalRevision }),
285
- ...(metadata.summary ? { summary: metadata.summary } : {}),
286
- ...(metadata.evalRevision ? { evalRevision: metadata.evalRevision } : {}),
287
- }
288
- }
289
-
290
- // the measured population in list order, from the freshness-free rows: a filed reading leads (newest first),
291
- // and the tie-breaks are the identity ones — the SAME comparison `byNewest` applies, over the only fields it
292
- // actually reads. Blind rows never enter, exactly as the detail's own `results` filter excludes them.
293
- export function measuredSequence(order: SessionEvalOrderRow[]): { node: string; scenario: string }[] {
294
- return order.filter((row) => row.ts)
295
- .sort((a, b) => String(b.ts ?? '').localeCompare(String(a.ts ?? ''))
296
- || a.node.localeCompare(b.node) || a.scenario.localeCompare(b.scenario))
297
- .map((row) => ({ node: row.node, scenario: row.scenario }))
298
- }
299
-
300
- // the nodes whose verdicts the response will publish: the selected row's, plus a window wide enough to
301
- // contain any neighbour boundedEvalNeighbors can choose (it takes at most five, split around the index).
302
- export function focusNodes(order: SessionEvalOrderRow[], node: string, scenario: string): string[] {
303
- const sequence = measuredSequence(order)
304
- const index = sequence.findIndex((row) => row.node === node && row.scenario === scenario)
305
- if (index < 0) return [node]
306
- return [...new Set([node, ...sequence.slice(Math.max(0, index - 6), index + 7).map((row) => row.node)])]
307
- }
308
-
309
- export async function evalDetailReview(node: string, scenario: string, scope?: string | null): Promise<EvalDetailReview | null> {
310
- if (scope) {
311
- // A detail renders ONE row plus at most five neighbours, but owes the whole population's index/total.
312
- // So name the window from the freshness-free sequence and let only those nodes pay the freshness pass;
313
- // a build that finds a full cached model ignores the pick and answers from it instead.
314
- const model = await buildSessionEvals(scope, (order) => focusNodes(order, node, scenario))
315
- if (!model) return null
316
- const sourceNode = model.nodes.find((candidate) => candidate.id === node)
317
- return projectEvalDetail(scopedEvalReviewItems(model), sourceNode?.evals ?? [], node, scenario, {
318
- scope,
319
- summary: model.summary,
320
- evalRevision: model.evalRevision,
321
- ...(model.order ? { sequence: measuredSequence(model.order) } : {}),
322
- })
323
- }
324
- await getBoard()
325
- const snapshot = readReviewSnapshot()
326
- const sourceNode = snapshot.evalNodes.find((candidate) => candidate.id === node)
327
- return projectEvalDetail(trunkEvalReviewItems(snapshot.evalNodes), sourceNode?.readings ?? [], node, scenario)
328
- }
329
-
330
- async function timelineEvalReview(text: string, requestedPage: unknown) {
331
- const node = readToken(text, 'node')
332
- if (!node) return null
333
- const [timeline, sessions] = await Promise.all([evalTimeline(node), listSessions()])
334
- const measured = new Set(timeline.readings.map((reading) => reading.scenario))
335
- const items = [
336
- ...timeline.scenarios.filter((scenario) => !measured.has(scenario.name)).map((scenario) => ({
337
- ...scenario,
338
- scenario: scenario.name,
339
- node,
340
- filterKind: EVAL_FILTER_KIND.UNMEASURED,
341
- })),
342
- ...timeline.readings.map((reading, index) => ({
343
- ...reading,
344
- state: evalReviewState(reading),
345
- node,
346
- filterKind: EVAL_FILTER_KIND.RESULT,
347
- filterKey: `${EVAL_FILTER_KIND.RESULT}:${index}`,
348
- })),
349
- ...(timeline.dangling ?? []).map((track) => ({
350
- ...track,
351
- node,
352
- filterKind: EVAL_FILTER_KIND.DANGLING,
353
- filterKey: `${EVAL_FILTER_KIND.DANGLING}:${track.threadId}`,
354
- })),
355
- ]
356
- const filtered = evalFilterModel(items, tokenFilterState(text, 'eval'), { sessions, defaultKind: 'all', defaultSection: '' })
357
- return {
358
- scope: null,
359
- view: 'timeline',
360
- node,
361
- hasEvalFile: timeline.hasEvalFile,
362
- gates: [],
363
- unknown: 0,
364
- ...paginateReview(items, filtered.shown, filtered, requestedPage, {
365
- domain: 'evals', view: 'timeline', node, timeline, sessions: sessions.map((session) => session.id),
366
- }),
367
- }
368
- }
369
-
370
- export async function evalsReview(query: string | undefined, requestedPage: unknown, options: { view?: string } = {}) {
371
- const text = String(query ?? '').trim() || EVAL_QUERY_DEFAULT
372
- if (options.view === 'timeline') return timelineEvalReview(text, requestedPage)
373
- const scope = readToken(text, 'scope') || null
374
- if (scope) {
375
- const model = await buildSessionEvals(scope)
376
- if (!model) return null
377
- const items = scopedEvalReviewItems(model)
378
- const sessions = await listSessions()
379
- const filtered = evalFilterModel(items, tokenFilterState(text, 'eval'), { sessions, defaultKind: 'all', defaultSection: '' })
380
- return {
381
- scope,
382
- gates: model.gates ?? [],
383
- unknown: model.nodes.reduce((count, node) => count + (node.unknownCoverage?.length ?? 0), 0),
384
- summary: model.summary,
385
- evalRevision: model.evalRevision,
386
- impact: model.impact,
387
- ...paginateReview(items, filtered.shown, filtered, requestedPage, {
388
- domain: 'evals', scope, items, gates: model.gates ?? [], summary: model.summary,
389
- evalRevision: model.evalRevision, impact: model.impact, sessions: sessions.map((session) => session.id),
390
- }),
391
- }
392
- }
393
- if (!hasReviewSnapshot()) await getBoard()
394
- const sessions = await listSessions()
395
- const items = trunkEvalReviewItems(readReviewSnapshot().evalNodes)
396
- const filtered = evalFilterModel(items, tokenFilterState(text, 'eval'), { sessions, defaultKind: 'all', defaultSection: '' })
397
- return {
398
- scope: null,
399
- gates: [],
400
- unknown: 0,
401
- ...paginateReview(items, filtered.shown, filtered, requestedPage, {
402
- domain: 'evals', items, sessions: sessions.map((session) => session.id),
403
- }),
404
- }
405
- }
@@ -1,35 +0,0 @@
1
- import { spawnSync } from 'node:child_process'
2
-
3
- // tmux presence is the primitive we actually depend on, so probe THAT rather than assuming by platform:
4
- // this also catches a bare POSIX box that simply hasn't installed tmux, not only native Windows.
5
- export function hasTmux(): boolean {
6
- try {
7
- return spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0
8
- } catch {
9
- return false
10
- }
11
- }
12
-
13
- // Pure so it is unit-testable without spawning or exiting: null = runtime OK; otherwise the stderr lines.
14
- // The pointer branches on platform because the honest repair differs — WSL2 on Windows (no POSIX analog at
15
- // all), install-tmux on a POSIX host that merely lacks it.
16
- export function sessionRuntimeBlock(env: { hasTmux: boolean; platform: string }): string[] | null {
17
- if (env.hasTmux) 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
- } else {
23
- lines.push('tmux is not on PATH — install it and retry (e.g. `apt install tmux`, `brew install tmux`).')
24
- }
25
- return lines
26
- }
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(): void {
31
- const block = sessionRuntimeBlock({ hasTmux: hasTmux(), platform: process.platform })
32
- if (!block) return
33
- for (const line of block) console.error(line)
34
- process.exit(69)
35
- }
@@ -1,88 +0,0 @@
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 './git.js'
6
- import { readJsonConfig, runtimeRoot } from './layout.js'
7
- import { processStartToken, writeDetachedRuntimeReceipt, type ProcessIdentity } from './process-identity.js'
8
-
9
- export type BackendInstanceRecord = {
10
- version: 1
11
- instanceId: string
12
- pid: number
13
- startToken: string
14
- projectRoot: string
15
- startedAt: string
16
- }
17
-
18
- const backendInstancesDir = () => join(runtimeRoot(), 'backend-instances')
19
- const backendInstancePath = (instanceId: string) => join(backendInstancesDir(), `${instanceId}.json`)
20
-
21
- const atomicJson = (path: string, value: unknown) => {
22
- mkdirSync(dirname(path), { recursive: true })
23
- const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`
24
- writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 })
25
- renameSync(tmp, path)
26
- }
27
-
28
- export function readBackendInstanceRecords(root = repoRoot()): BackendInstanceRecord[] {
29
- let names: string[]
30
- try { names = readdirSync(backendInstancesDir()) } catch { return [] }
31
- return names.flatMap((name) => {
32
- try {
33
- const rec = readJsonConfig(join(backendInstancesDir(), name)) as BackendInstanceRecord
34
- return rec?.version === 1 && rec.projectRoot === root ? [rec] : []
35
- } catch { return [] }
36
- })
37
- }
38
-
39
- export function registerBackendInstance(instanceId: string, pid = process.pid, root = repoRoot()): BackendInstanceRecord {
40
- const startToken = processStartToken(pid)
41
- if (!startToken) throw new Error(`cannot identify backend supervisor PID ${pid}`)
42
- const record: BackendInstanceRecord = { version: 1, instanceId, pid, startToken, projectRoot: root, startedAt: new Date().toISOString() }
43
- atomicJson(backendInstancePath(instanceId), record)
44
- return record
45
- }
46
-
47
- export function unregisterBackendInstance(instanceId: string, pid = process.pid): void {
48
- try {
49
- const record = readJsonConfig(backendInstancePath(instanceId)) as BackendInstanceRecord
50
- if (record.instanceId === instanceId && record.pid === pid && record.startToken === processStartToken(pid)) rmSync(backendInstancePath(instanceId), { force: true })
51
- } catch { /* not ours / already removed */ }
52
- }
53
-
54
- export function spawnDetachedRuntime(opts: {
55
- cwd: string
56
- logFile: string
57
- pidFile: string
58
- receiptFile: string
59
- command: string
60
- args: string[]
61
- env?: NodeJS.ProcessEnv
62
- }): ProcessIdentity {
63
- mkdirSync(dirname(opts.logFile), { recursive: true })
64
- mkdirSync(dirname(opts.pidFile), { recursive: true })
65
- const log = openSync(opts.logFile, 'a', 0o600)
66
- let child: ReturnType<typeof spawn>
67
- try {
68
- child = spawn(opts.command, opts.args, {
69
- cwd: opts.cwd,
70
- env: opts.env ?? process.env,
71
- detached: true,
72
- stdio: ['ignore', log, log],
73
- })
74
- } finally { closeSync(log) }
75
- child.once('error', () => {})
76
- if (!child.pid) throw new Error(`could not spawn detached shared runtime: ${opts.command}`)
77
- child.unref()
78
- try {
79
- const identity = writeDetachedRuntimeReceipt(child.pid, opts.receiptFile)
80
- writeFileSync(opts.pidFile, `${child.pid}\n`, { mode: 0o600 })
81
- return { pid: identity.pid, startToken: identity.startToken }
82
- } catch (error) {
83
- try { child.kill('SIGTERM') } catch { /* exact just-spawned child already exited */ }
84
- rmSync(opts.pidFile, { force: true })
85
- rmSync(opts.receiptFile, { force: true })
86
- throw error
87
- }
88
- }
@@ -1,80 +0,0 @@
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
-
12
- const BIN = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'spex.mjs')
13
-
14
- const CASES = [
15
- ['exit-cleanup', "does /exit remove the session's worktree and tmux, or just orphan them?", ['session-console']],
16
- ['owner-at-edit', 'how does an agent learn which spec governs a file it just edited?', ['spec-of-file']],
17
- ['main-block', 'what stops an agent from committing or merging straight into main?', ['main-guard']],
18
- ['main-escape', 'the escape hatch that lets seeding run on the main branch', ['main-guard']],
19
- ['inter-agent-msg', 'how do two running agent sessions send messages to each other?', ['agent-reply-channel', 'comms']],
20
- ['search-hidden-node', 'keyboard shortcut to find a node hidden inside a collapsed subtree', ['keyboard-nav']],
21
- ['session-order', 'how is the order of sessions in the session list decided?', ['session-console']],
22
- ['node-status', 'what makes a node show as pending vs active vs merged vs drift?', ['spec-node-states']],
23
- ['dashboard-backend', 'how does the dashboard reach the backend API and on which port?', ['api-endpoint']],
24
- ['loss-measured', "how is a node's loss measured and its scenarios scored?", ['eval-core']],
25
- ['launch-injection', "what context gets injected into a freshly launched agent's prompt?", ['injected-context']],
26
- ['read-before-code', 'the one-shot nudge that makes an agent read its spec before touching code', ['spec-first']],
27
- ['hot-reload', 'zero-downtime backend reload without dropping connections', ['supervisor']],
28
- ['many-owners', 'can several specs own the same code file, and what happens if too many do?', ['governed-related']],
29
- ['active-spec-search', 'an injected sub-agent that searches specs for the agent, the spec analog of Explore', ['spec-search']],
30
- ['declare-done', 'how does a worker declare it is done', ['state']],
31
- ]
32
-
33
- // de-collision-aware label match: exact id, or the id's trailing `_`-suffix is the label.
34
- const matches = (id, label) => id === label || id.endsWith('_' + label)
35
-
36
- let r1 = 0, r3 = 0, mrr = 0
37
- const rows = []
38
- for (const [name, query, expect] of CASES) {
39
- const out = execFileSync('node', [BIN, 'spec', 'search', query, '--json', '--limit', '10'], { encoding: 'utf8' })
40
- const results = JSON.parse(out)
41
- const ids = results.map((x) => x.id)
42
- const rank = ids.findIndex((id) => expect.some((label) => matches(id, label))) + 1 // 1-based; 0 = not found
43
- const hit1 = rank === 1
44
- const hit3 = rank >= 1 && rank <= 3
45
- if (hit1) r1++
46
- if (hit3) r3++
47
- if (rank >= 1) mrr += 1 / rank
48
- rows.push({ name, rank: rank || '—', top: ids.slice(0, 3).join(', '), expect: expect.join('|') })
49
- }
50
- const n = CASES.length
51
- for (const row of rows) {
52
- const mark = row.rank === 1 ? '✓1' : (typeof row.rank === 'number' && row.rank <= 3 ? '·3' : (row.rank === '—' ? '✗ ' : `·${row.rank}`))
53
- console.log(`${mark} ${row.name.padEnd(20)} want=${row.expect.padEnd(22)} rank=${String(row.rank).padStart(2)} top3: ${row.top}`)
54
- }
55
-
56
- // CJK-positive: the corpus is mostly English but a few nodes carry Chinese prose — the root `spexcode`
57
- // node's body repeats 节点 throughout. A per-CJK-character tokenizer must let a Chinese content word reach
58
- // that node; `spex spec search "节点"` MUST return `spexcode` in results (the bug: the old tokenizer split on
59
- // [^a-z0-9]+ and discarded all CJK, so every Chinese query dead-ended at zero results).
60
- const cjkPos = JSON.parse(execFileSync('node', [BIN, 'spec', 'search', '节点', '--json', '--limit', '10'], { encoding: 'utf8' }))
61
- const cjkPosRank = cjkPos.map((x) => x.id).findIndex((id) => matches(id, 'spexcode')) + 1
62
- const cjkPosPass = cjkPosRank >= 1
63
- console.log(`${cjkPosPass ? `✓${cjkPosRank}` : '✗ '} cjk-positive want=spexcode(节点) rank=${cjkPosRank || '—'} top3: ${cjkPos.slice(0, 3).map((x) => x.id).join(', ')}`)
64
-
65
- // zero-result fail-loud regression: a CJK query whose characters appear NOWHERE in the corpus (会/话) returns
66
- // nothing, and the zero-result message must still carry the corpus-is-English translate-and-retry fact
67
- // (fail-loud) plus the browse-all next step — CJK support does not suppress the honest zero-result route.
68
- // (No nearest titles: a CJK query has nothing to be lexically near.)
69
- const cjk = execFileSync('node', [BIN, 'spec', 'search', '会话'], { encoding: 'utf8' })
70
- const cjkPass = cjk.includes('corpus is English') && cjk.includes('spex graph')
71
- console.log(`${cjkPass ? '✓ ' : '✗ '} cjk-zero-result want=corpus-is-English + spex-tree ${cjkPass ? 'both present' : 'MISSING: ' + cjk.trim()}`)
72
-
73
- // zero-result typo routing: an English typo that matches nothing must surface the nearest node titles
74
- // (per-word edit distance) plus the browse-all next step, so a typo routes forward instead of dead-ending.
75
- const typo = execFileSync('node', [BIN, 'spec', 'search', 'kyeboard'], { encoding: 'utf8' })
76
- const typoPass = typo.includes('nearest titles') && typo.includes('keyboard-nav') && typo.includes('spex graph')
77
- console.log(`${typoPass ? '✓ ' : '✗ '} typo-zero-result want=nearest-titles(keyboard-nav) + spex-tree ${typoPass ? 'both present' : 'MISSING: ' + typo.trim()}`)
78
-
79
- console.log('—'.repeat(72))
80
- 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'}`)
@@ -1,63 +0,0 @@
1
- import { loadSpecsLite } from './specs.js'
2
- import { rankDocs, type RankInput } from './ranker.js'
3
-
4
- export type SearchResult = { id: string; title: string; path: string; score: number; snippet: string }
5
- export type SearchStats = { nodes: number; tokens: number; ms: number }
6
-
7
- // zero-result fallback: nearest node titles by per-word edit distance over title+id — the lightweight
8
- // string distance that routes a typo'd query to a next step. Each query word takes its best normalised
9
- // Levenshtein similarity against any title/id word (≥0.5, so unrelated words don't accumulate), then the
10
- // words sum. Same loadSpecsLite read as the ranker; deliberately NOT part of rankDocs scoring (this
11
- // tolerates typos, the ranker must not).
12
- export function nearestTitles(query: string, n = 3): { id: string; title: string }[] {
13
- const words = (s: string) => s.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 1)
14
- const lev = (a: string, b: string) => {
15
- const dp = Array.from({ length: b.length + 1 }, (_, i) => i)
16
- for (let i = 1; i <= a.length; i++) {
17
- let prev = dp[0]
18
- dp[0] = i
19
- for (let j = 1; j <= b.length; j++) {
20
- const cur = dp[j]
21
- dp[j] = Math.min(dp[j] + 1, dp[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1))
22
- prev = cur
23
- }
24
- }
25
- return dp[b.length]
26
- }
27
- const qws = words(query)
28
- if (!qws.length) return [] // nothing to be "near" (e.g. a pure-CJK query) — the caller still points at spex graph
29
- return loadSpecsLite()
30
- .map((s) => {
31
- const tws = [...new Set(words(`${s.title} ${s.id}`))]
32
- let score = 0
33
- for (const q of qws) {
34
- let best = 0
35
- for (const t of tws) best = Math.max(best, 1 - lev(q, t) / Math.max(q.length, t.length))
36
- if (best >= 0.5) score += best
37
- }
38
- return { id: s.id, title: s.title, score }
39
- })
40
- .filter((s) => s.score > 0)
41
- .sort((a, b) => b.score - a.score)
42
- .slice(0, n)
43
- .map(({ id, title }) => ({ id, title }))
44
- }
45
-
46
- export async function searchSpecs(query: string, opts: { limit?: number; onStats?: (s: SearchStats) => void } = {}): Promise<SearchResult[]> {
47
- const t0 = performance.now()
48
- // pre-sort by (shorter id, then id): rankDocs sorts stably, so for equal-scored nodes this IS the tiebreak.
49
- const nodes = loadSpecsLite()
50
- .slice()
51
- .sort((a, b) => a.id.length - b.id.length || a.id.localeCompare(b.id))
52
- const inputs: RankInput<(typeof nodes)[number]>[] = nodes.map((s) => ({
53
- ref: s, name: `${s.title} ${s.id}`, desc: s.desc, body: s.body,
54
- }))
55
- const out = rankDocs(query, inputs, { limit: opts.limit }).map((r) => ({
56
- id: r.ref.id, title: r.ref.title, path: r.ref.path, score: r.score, snippet: r.snippet,
57
- }))
58
- if (opts.onStats) {
59
- const tokens = inputs.reduce((a, i) => a + `${i.name} ${i.desc} ${i.body}`.split(/[^a-z0-9]+/i).filter(Boolean).length, 0)
60
- opts.onStats({ nodes: inputs.length, tokens, ms: performance.now() - t0 })
61
- }
62
- return out
63
- }