spexcode 0.6.2 → 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 (479) hide show
  1. package/README.md +3 -1
  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} +36 -32
  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 +1 -1
  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/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec-first.sh +1 -1
  167. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec-of-file.sh +1 -0
  168. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/spec.md +2 -2
  169. package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +31 -29
  170. package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +94 -0
  171. package/node_modules/@spexcode/spec-core/dist/anchors.js +730 -0
  172. package/node_modules/@spexcode/spec-core/dist/git.d.ts +166 -0
  173. package/node_modules/@spexcode/spec-core/dist/git.js +2736 -0
  174. package/node_modules/@spexcode/spec-core/dist/graph-delta.d.ts +44 -0
  175. package/node_modules/@spexcode/spec-core/dist/graph-delta.js +72 -0
  176. package/node_modules/@spexcode/spec-core/dist/graph.d.ts +34 -0
  177. package/node_modules/@spexcode/spec-core/dist/graph.js +237 -0
  178. package/node_modules/@spexcode/spec-core/dist/graphDelta.d.ts +3 -0
  179. package/node_modules/@spexcode/spec-core/dist/graphDelta.js +13 -0
  180. package/node_modules/@spexcode/spec-core/dist/harness-identity.d.ts +31 -0
  181. package/node_modules/@spexcode/spec-core/dist/harness-identity.js +20 -0
  182. package/node_modules/@spexcode/spec-core/dist/identity-presets.d.ts +152 -0
  183. package/node_modules/@spexcode/spec-core/dist/identity-presets.js +132 -0
  184. package/node_modules/@spexcode/spec-core/dist/index.d.ts +45 -0
  185. package/node_modules/@spexcode/spec-core/dist/index.js +19 -0
  186. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +183 -0
  187. package/node_modules/@spexcode/spec-core/dist/layout.js +548 -0
  188. package/node_modules/@spexcode/spec-core/dist/process-identity.d.ts +37 -0
  189. package/node_modules/@spexcode/spec-core/dist/process-identity.js +214 -0
  190. package/node_modules/@spexcode/spec-core/dist/project-identity.d.ts +12 -0
  191. package/node_modules/@spexcode/spec-core/dist/project-identity.js +71 -0
  192. package/node_modules/@spexcode/spec-core/dist/project-store.d.ts +3 -0
  193. package/node_modules/@spexcode/spec-core/dist/project-store.js +14 -0
  194. package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +2 -0
  195. package/node_modules/@spexcode/spec-core/dist/resilience.js +40 -0
  196. package/node_modules/@spexcode/spec-core/dist/review/index.d.ts +3 -0
  197. package/node_modules/@spexcode/spec-core/dist/review/index.js +5 -0
  198. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +77 -0
  199. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +308 -0
  200. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +66 -0
  201. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +180 -0
  202. package/node_modules/@spexcode/spec-core/dist/review/session.d.ts +4 -0
  203. package/node_modules/@spexcode/spec-core/dist/review/session.js +8 -0
  204. package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.d.ts +15 -0
  205. package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.js +12 -0
  206. package/node_modules/@spexcode/spec-core/dist/root-lru.d.ts +4 -0
  207. package/{spec-cli/src/root-lru.ts → node_modules/@spexcode/spec-core/dist/root-lru.js} +26 -30
  208. package/node_modules/@spexcode/spec-core/dist/specs.d.ts +117 -0
  209. package/node_modules/@spexcode/spec-core/dist/specs.js +489 -0
  210. package/node_modules/@spexcode/spec-core/package.json +28 -0
  211. package/node_modules/@spexcode/spec-eval/dist/cache.d.ts +10 -0
  212. package/node_modules/@spexcode/spec-eval/dist/cache.js +50 -0
  213. package/node_modules/@spexcode/spec-eval/dist/cli.d.ts +22 -0
  214. package/node_modules/@spexcode/spec-eval/dist/cli.js +981 -0
  215. package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +98 -0
  216. package/node_modules/@spexcode/spec-eval/dist/evaltab.js +176 -0
  217. package/node_modules/@spexcode/spec-eval/dist/filing.d.ts +16 -0
  218. package/node_modules/@spexcode/spec-eval/dist/filing.js +36 -0
  219. package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +48 -0
  220. package/node_modules/@spexcode/spec-eval/dist/freshness.js +799 -0
  221. package/node_modules/@spexcode/spec-eval/dist/host.d.ts +46 -0
  222. package/node_modules/@spexcode/spec-eval/dist/host.js +53 -0
  223. package/node_modules/@spexcode/spec-eval/dist/humanok.d.ts +11 -0
  224. package/node_modules/@spexcode/spec-eval/dist/humanok.js +27 -0
  225. package/node_modules/@spexcode/spec-eval/dist/index.d.ts +4 -0
  226. package/node_modules/@spexcode/spec-eval/dist/index.js +4 -0
  227. package/node_modules/@spexcode/spec-eval/dist/remarks.d.ts +31 -0
  228. package/node_modules/@spexcode/spec-eval/dist/remarks.js +1 -0
  229. package/node_modules/@spexcode/spec-eval/dist/scenariofresh.d.ts +13 -0
  230. package/node_modules/@spexcode/spec-eval/dist/scenariofresh.js +303 -0
  231. package/node_modules/@spexcode/spec-eval/dist/scenarios.d.ts +99 -0
  232. package/node_modules/@spexcode/spec-eval/dist/scenarios.js +650 -0
  233. package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +307 -0
  234. package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +1883 -0
  235. package/node_modules/@spexcode/spec-eval/dist/sidecar.d.ts +55 -0
  236. package/node_modules/@spexcode/spec-eval/dist/sidecar.js +82 -0
  237. package/node_modules/@spexcode/spec-eval/dist/timeline.d.ts +25 -0
  238. package/node_modules/@spexcode/spec-eval/dist/timeline.js +65 -0
  239. package/node_modules/@spexcode/spec-eval/dist/ui-path.d.ts +1 -0
  240. package/node_modules/@spexcode/spec-eval/dist/ui-path.js +2 -0
  241. package/node_modules/@spexcode/spec-eval/package.json +36 -0
  242. package/node_modules/@spexcode/spec-forge/dist/cache.d.ts +29 -0
  243. package/node_modules/@spexcode/spec-forge/dist/cache.js +59 -0
  244. package/node_modules/@spexcode/spec-forge/dist/cli.d.ts +1 -0
  245. package/node_modules/@spexcode/spec-forge/dist/cli.js +100 -0
  246. package/node_modules/@spexcode/spec-forge/dist/drivers/github.d.ts +2 -0
  247. package/node_modules/@spexcode/spec-forge/dist/drivers/github.js +116 -0
  248. package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.d.ts +7 -0
  249. package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.js +144 -0
  250. package/node_modules/@spexcode/spec-forge/dist/drivers.d.ts +10 -0
  251. package/node_modules/@spexcode/spec-forge/dist/drivers.js +83 -0
  252. package/node_modules/@spexcode/spec-forge/dist/index.d.ts +4 -0
  253. package/node_modules/@spexcode/spec-forge/dist/index.js +4 -0
  254. package/node_modules/@spexcode/spec-forge/dist/links.d.ts +15 -0
  255. package/node_modules/@spexcode/spec-forge/dist/links.js +65 -0
  256. package/node_modules/@spexcode/spec-forge/dist/needs-eval.d.ts +10 -0
  257. package/node_modules/@spexcode/spec-forge/dist/needs-eval.js +20 -0
  258. package/node_modules/@spexcode/spec-forge/dist/port.d.ts +53 -0
  259. package/node_modules/@spexcode/spec-forge/dist/port.js +1 -0
  260. package/node_modules/@spexcode/spec-forge/dist/resident.d.ts +7 -0
  261. package/node_modules/@spexcode/spec-forge/dist/resident.js +43 -0
  262. package/node_modules/@spexcode/spec-forge/package.json +32 -0
  263. package/package.json +33 -19
  264. package/spec-cli/bin/spex.mjs +0 -69
  265. package/spec-cli/src/anchors.ts +0 -728
  266. package/spec-cli/src/attach.ts +0 -43
  267. package/spec-cli/src/claude-headless.ts +0 -321
  268. package/spec-cli/src/cli.ts +0 -1258
  269. package/spec-cli/src/client.ts +0 -414
  270. package/spec-cli/src/cockpit.ts +0 -43
  271. package/spec-cli/src/codex-runtime-generations.ts +0 -646
  272. package/spec-cli/src/commit-surgery.ts +0 -68
  273. package/spec-cli/src/contract-filter.ts +0 -202
  274. package/spec-cli/src/delivery-queue.ts +0 -165
  275. package/spec-cli/src/doctor.ts +0 -516
  276. package/spec-cli/src/execution-trace.ts +0 -444
  277. package/spec-cli/src/file-write.ts +0 -22
  278. package/spec-cli/src/gateway-auth.ts +0 -181
  279. package/spec-cli/src/gateway-hub.ts +0 -358
  280. package/spec-cli/src/gateway.ts +0 -461
  281. package/spec-cli/src/git.ts +0 -2556
  282. package/spec-cli/src/graph.ts +0 -271
  283. package/spec-cli/src/graphCache.ts +0 -710
  284. package/spec-cli/src/graphDelta.ts +0 -77
  285. package/spec-cli/src/graphStream.ts +0 -1019
  286. package/spec-cli/src/harness-select.ts +0 -77
  287. package/spec-cli/src/harness.ts +0 -3025
  288. package/spec-cli/src/headless-controller.ts +0 -63
  289. package/spec-cli/src/help.ts +0 -492
  290. package/spec-cli/src/host-resources.ts +0 -762
  291. package/spec-cli/src/host.ts +0 -611
  292. package/spec-cli/src/identity-presets.d.ts +0 -13
  293. package/spec-cli/src/identity-presets.js +0 -138
  294. package/spec-cli/src/index.ts +0 -818
  295. package/spec-cli/src/init.ts +0 -258
  296. package/spec-cli/src/issues-cli.ts +0 -276
  297. package/spec-cli/src/issues.ts +0 -237
  298. package/spec-cli/src/layout.ts +0 -640
  299. package/spec-cli/src/lint.ts +0 -488
  300. package/spec-cli/src/localIssues.ts +0 -538
  301. package/spec-cli/src/materialize.ts +0 -417
  302. package/spec-cli/src/mentions.ts +0 -81
  303. package/spec-cli/src/opencode-headless.ts +0 -205
  304. package/spec-cli/src/pi-headless.ts +0 -153
  305. package/spec-cli/src/plugin-harness.ts +0 -157
  306. package/spec-cli/src/process-identity.ts +0 -207
  307. package/spec-cli/src/project-identity.ts +0 -73
  308. package/spec-cli/src/project-store.ts +0 -17
  309. package/spec-cli/src/pty-bridge.ts +0 -396
  310. package/spec-cli/src/pty-helper.mjs +0 -96
  311. package/spec-cli/src/pty-native-helper.mjs +0 -22
  312. package/spec-cli/src/ranker.ts +0 -191
  313. package/spec-cli/src/reaper.ts +0 -125
  314. package/spec-cli/src/resilience.ts +0 -41
  315. package/spec-cli/src/reviewSnapshot.ts +0 -28
  316. package/spec-cli/src/reviews.ts +0 -434
  317. package/spec-cli/src/runtime-guard.ts +0 -35
  318. package/spec-cli/src/runtime-ownership.ts +0 -88
  319. package/spec-cli/src/search.bench.mjs +0 -80
  320. package/spec-cli/src/search.ts +0 -63
  321. package/spec-cli/src/session-cursors.ts +0 -91
  322. package/spec-cli/src/session-execution.ts +0 -68
  323. package/spec-cli/src/session-files.ts +0 -121
  324. package/spec-cli/src/session-follow.ts +0 -182
  325. package/spec-cli/src/session-reparent.ts +0 -33
  326. package/spec-cli/src/session-timeline.ts +0 -247
  327. package/spec-cli/src/session-web.ts +0 -135
  328. package/spec-cli/src/sessions.ts +0 -3883
  329. package/spec-cli/src/sh.ts +0 -3
  330. package/spec-cli/src/slash-commands.ts +0 -318
  331. package/spec-cli/src/source-files.ts +0 -104
  332. package/spec-cli/src/specs.ts +0 -498
  333. package/spec-cli/src/supervise.ts +0 -245
  334. package/spec-cli/src/tree.ts +0 -118
  335. package/spec-cli/src/tsx-bin.ts +0 -18
  336. package/spec-cli/src/uninstall.ts +0 -171
  337. package/spec-cli/src/uploads.ts +0 -253
  338. package/spec-cli/src/worktree-sources.ts +0 -42
  339. package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +0 -8
  340. package/spec-dashboard/dist/assets/App-B6IbFbGu.js +0 -2
  341. package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +0 -1
  342. package/spec-dashboard/dist/assets/Dashboard-_sDk7Rcg.js +0 -27
  343. package/spec-dashboard/dist/assets/EvalsPage-BT357Lyt.js +0 -2
  344. package/spec-dashboard/dist/assets/IssuesPage-BG9rNOgV.js +0 -1
  345. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  346. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  347. package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  348. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  349. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  350. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  351. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  352. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  353. package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  354. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  355. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  356. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  357. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  358. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  359. package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  360. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  361. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  362. package/spec-dashboard/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  363. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  364. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  365. package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  366. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  367. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  368. package/spec-dashboard/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  369. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  370. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  371. package/spec-dashboard/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  372. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  373. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  374. package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  375. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  376. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  377. package/spec-dashboard/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  378. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  379. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  380. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  381. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  382. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  383. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  384. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  385. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  386. package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  387. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  388. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  389. package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  390. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  391. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  392. package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  393. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  394. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  395. package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  396. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  397. package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  398. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  399. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  400. package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  401. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  402. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  403. package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  404. package/spec-dashboard/dist/assets/MobileApp-D9yPMR3A.js +0 -2
  405. package/spec-dashboard/dist/assets/Modal-B3EcgeA3.js +0 -1
  406. package/spec-dashboard/dist/assets/PageScroll-CNZOugWV.js +0 -1
  407. package/spec-dashboard/dist/assets/ProjectsPage-C2CRHmvs.js +0 -1
  408. package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +0 -32
  409. package/spec-dashboard/dist/assets/SessionInterface-DOEjuT3l.js +0 -40
  410. package/spec-dashboard/dist/assets/SessionWindow-1nYgZ4CL.js +0 -1
  411. package/spec-dashboard/dist/assets/Settings-7WeTJn0V.js +0 -1
  412. package/spec-dashboard/dist/assets/Thread-CEbzAFQn.js +0 -13
  413. package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +0 -1
  414. package/spec-dashboard/dist/assets/data-SlvxNA2r.js +0 -1
  415. package/spec-dashboard/dist/assets/index-DLVeOHL-.js +0 -41
  416. package/spec-dashboard/dist/assets/index-DYLGfPjx.css +0 -1
  417. package/spec-dashboard/dist/assets/launch-Cp579UoJ.css +0 -1
  418. package/spec-dashboard/dist/assets/launch-yxCYr64x.js +0 -276
  419. package/spec-dashboard/dist/assets/sessionSurface--7l69h4F.js +0 -1
  420. package/spec-dashboard/dist/index.html +0 -29
  421. package/spec-dashboard/src/reviewFilters.js +0 -322
  422. package/spec-dashboard/src/reviewQuery.js +0 -174
  423. package/spec-dashboard/src/session.js +0 -199
  424. package/spec-eval/src/cache.ts +0 -57
  425. package/spec-eval/src/cli.ts +0 -894
  426. package/spec-eval/src/evaltab.ts +0 -264
  427. package/spec-eval/src/filing.ts +0 -39
  428. package/spec-eval/src/freshness.ts +0 -836
  429. package/spec-eval/src/humanok.ts +0 -28
  430. package/spec-eval/src/scenariofresh.ts +0 -244
  431. package/spec-eval/src/scenarios.ts +0 -672
  432. package/spec-eval/src/sessioneval.ts +0 -2220
  433. package/spec-eval/src/sidecar.ts +0 -93
  434. package/spec-eval/src/timeline.ts +0 -54
  435. package/spec-forge/src/__fixtures__/github-forge.json +0 -63
  436. package/spec-forge/src/cache.ts +0 -66
  437. package/spec-forge/src/cli.ts +0 -99
  438. package/spec-forge/src/drivers/github.ts +0 -132
  439. package/spec-forge/src/drivers/gitlab.ts +0 -153
  440. package/spec-forge/src/drivers.ts +0 -73
  441. package/spec-forge/src/links.ts +0 -72
  442. package/spec-forge/src/needs-eval.ts +0 -29
  443. package/spec-forge/src/port.ts +0 -36
  444. package/spec-forge/src/resident.ts +0 -46
  445. /package/{spec-cli → node_modules/@spexcode/spec-cli}/README.md +0 -0
  446. /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -0
  447. /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-sed-v0.fixture +0 -0
  448. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/commit-msg +0 -0
  449. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-checkout +0 -0
  450. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-merge +0 -0
  451. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/pre-commit +0 -0
  452. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/prepare-commit-msg +0 -0
  453. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/reference-transaction +0 -0
  454. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/extract/spec.md +0 -0
  455. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/regroup/spec.md +0 -0
  456. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/rename/spec.md +0 -0
  457. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/spec.md +0 -0
  458. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/tidy/spec.md +0 -0
  459. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/comment-altitude/spec.md +0 -0
  460. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/idle.sh +0 -0
  461. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/spec.md +0 -0
  462. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/mark-active.sh +0 -0
  463. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/spec.md +0 -0
  464. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/fail.sh +0 -0
  465. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/spec.md +0 -0
  466. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec.md +0 -0
  467. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec.md +0 -0
  468. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec.md +0 -0
  469. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +0 -0
  470. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/forge-link/spec.md +0 -0
  471. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +0 -0
  472. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +0 -0
  473. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/spec.md +0 -0
  474. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/digest.mjs +0 -0
  475. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/spec.md +0 -0
  476. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/spec.md +0 -0
  477. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/spec.md +0 -0
  478. /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/spec.md +0 -0
  479. /package/{spec-cli → node_modules/@spexcode/spec-core}/templates/spexcode.json +0 -0
@@ -1,836 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import { createHash } from 'node:crypto'
3
- import { join, resolve } from 'node:path'
4
- import { batchRevisionOids, gitA, gitTry, headSha, gitObjectInterpretation, currentGitBuildAbortSignal, gitAbortError, ancestorsOf, inAncestors, commitReachable, pathEvents, primeAncestorClosures, unionTopology, type DriftIndex, type DriftPathEvent, type Reachability, eventsSince } from '../../spec-cli/src/git.js'
5
- import { anchorHitExists, extOf, extractorFor, extractors, resolveAnchor, resolveSelectors, type AnchorHitQuery, type Extractor, type RelationEntry, type Unit } from '../../spec-cli/src/anchors.js'
6
- import type { Reading } from './sidecar.js'
7
- import { scenarioCodeAxis, scenarioHash, type Scenario, type ScenarioCodeAxisSource } from './scenarios.js'
8
- import { scenarioChangeCommits, scenarioBlocksAt, primeScenarioBlocks, type ScenarioIndex } from './scenariofresh.js'
9
-
10
- // the CODE axis is touch-based (DriftIndex), so a code-file rename is out of scope — the same blind spot lint's code-drift has
11
-
12
- export type StaleAxis = 'code' | 'scenario' | 'remark' | 'anchor'
13
-
14
- export type ContentProbeDemand = { anchorSha: string; paths: readonly string[]; evalPath: string }
15
-
16
- // @@@ off-history content fallback - ancestry can't testify for a codeSha that isn't reachable from HEAD
17
- // (fold/rebase/squash-merge/cherry-pick all orphan the anchor), but the TREES still can: while the anchor
18
- // commit object exists locally, a pathspec-scoped `git diff <anchor> HEAD -- :(literal)<path>…` answers,
19
- // for exactly the governed paths a caller asks about, whether content differs — so a history rewrite that
20
- // left governed content byte-identical reads FRESH instead of false-positive stale. The question is per
21
- // REQUESTED path on purpose: one whole-repo changed-path set per anchor is what melted a production fold
22
- // (522 anchors × ~6k retained paths ≈ 520MB of Node heap), while the verdicts actually consumed are a
23
- // handful of governed files plus the eval.md. The probe is fed at the call sites (like the remark track) so
24
- // the decision functions stay pure over their inputs and the in-history fast path pays no extra git call;
25
- // only when the commit object is truly gone (gc'd orphan) does the conservative rule remain — surfaced as
26
- // the 'anchor' axis, so "anchor lost" reads differently from "content moved". No probe fed → the old
27
- // always-conservative rule.
28
- export type ContentProbe = {
29
- // did THIS path's content change between the anchor tree and HEAD's? null = can't testify (anchor gone,
30
- // or the path never primed) — callers stay conservative. Only primed paths are ever retained.
31
- changed(anchorSha: string, path: string): boolean | null
32
- // content CAN testify for this anchor: the object is readable and at least one verdict has settled
33
- canTestify(anchorSha: string): boolean
34
- // did THIS scenario's semantic block (description+expected) move between anchor and HEAD ([[scenariofresh]])
35
- scenarioDiffers(anchorSha: string, evalPath: string, scenario: string): boolean
36
- // codeDrift's display detail, the half HEAD's index cannot answer: is `commit` already in the ANCHOR's own
37
- // past? false when the anchor's topology can't testify — the caller then counts conservatively.
38
- inAnchorPast(anchorSha: string, commit: string): boolean
39
- prime?(anchorSha: string, paths: string[], evalPath: string): Promise<void>
40
- primeMany?(demands: readonly ContentProbeDemand[]): Promise<void>
41
- // answer every scenario-block demand `prime` recorded, in ONE batched pair of children. Optional and
42
- // idempotent: an unflushed demand degrades to the singular sync lookup, never to a wrong block.
43
- primeBlocks?(): Promise<void>
44
- }
45
-
46
- // (anchor, HEAD) name two immutable trees, so a settled verdict never invalidates — but "never invalidates"
47
- // is not "keep forever". A checkout answers freshness questions at ONE head at a time, so a root owns
48
- // exactly one head's verdicts ([[source-of-truth]]'s current-root rule, the same one the history and drift
49
- // indices follow). Keying the head into a shared memo instead made every rebuild ADD a generation: three
50
- // full invalidations left three heads' worth of anchors resident, and the cache grew with rebuild count
51
- // rather than with corpus size. A head move therefore swaps the root's scope atomically. An in-flight batch
52
- // keeps the entry object its caller already holds, so that caller still settles, but a detached entry can
53
- // never be read back through the root's current scope — an old flight cannot backfill the new head.
54
- // An anchor entry holds only PER-REQUESTED-PATH verdicts (plus the gone bit and the batch bookkeeping),
55
- // never a whole-repo path set.
56
- type AnchorVerdicts = {
57
- imageOid: string | null | undefined // undefined = interpretation resolution is still in flight
58
- verdicts: Map<string, boolean> // requested path → content differs between the two immutable trees
59
- gone: boolean // the anchor commit object is locally unreadable — content can't testify
60
- pending: Set<string> // paths awaiting the next batch child
61
- flight: Promise<void> | null // the single in-flight batch for this anchor
62
- }
63
- type RootScope = {
64
- head: string // raw HEAD + the full Git interpretation identity
65
- currentOid: string | null // exact current image; null only while first resolution is in flight
66
- anchors: Map<string, AnchorVerdicts>
67
- topology: Reachability | null // the anchors' OWN ancestry: one walk per scope, never per pair
68
- topologyRoster: Set<string> // the anchor images that walk already carries
69
- topologyFlight: Promise<void> | null // the single in-flight walk
70
- blockDemands: Set<string> // (rev, evalPath) awaiting the one batched scenario-block read
71
- // the SELECTOR-anchor probe's verdicts, in the same scope for the same reason ([[selector-anchor-scope]]):
72
- // a `sinceSha..HEAD` window lengthens as HEAD advances, so these are per-head immutable, never forever
73
- // immutable. One scope object means one head move drops both probes' answers at once, and the cardinality
74
- // invariant this scope exists to hold is observable for both.
75
- anchorVerdicts: Map<string, boolean>
76
- }
77
- const rootScopes = new Map<string, RootScope>()
78
- // Roots come and go (a closed worktree never asks again), so cap how many stay warm — the same bounded-slot
79
- // guard the index caches use, and the only bound needed once per-root cardinality is the corpus, not history.
80
- const ROOT_SLOTS = Math.max(4, Number(process.env.SPEXCODE_FRESHNESS_ROOT_SLOTS || 64))
81
-
82
- function scopeFor(rootKey: string, head: string, currentOid: string | null): RootScope {
83
- const current = rootScopes.get(rootKey)
84
- if (current?.head === head && (current.currentOid === currentOid || currentOid === null || current.currentOid === null)) {
85
- if (currentOid !== null) current.currentOid = currentOid
86
- rootScopes.delete(rootKey)
87
- rootScopes.set(rootKey, current)
88
- return current
89
- }
90
- const scope: RootScope = { head, currentOid, anchors: new Map(), topology: null, topologyRoster: new Set(), topologyFlight: null, blockDemands: new Set(), anchorVerdicts: new Map() }
91
- rootScopes.set(rootKey, scope)
92
- while (rootScopes.size > ROOT_SLOTS) {
93
- const oldest = rootScopes.keys().next().value
94
- if (oldest === undefined || oldest === rootKey) break
95
- rootScopes.delete(oldest)
96
- }
97
- return scope
98
- }
99
- // a read only ever sees the root's CURRENT head — a probe pinned to a superseded head can't testify
100
- function currentScope(rootKey: string, head: string): RootScope | undefined {
101
- const scope = rootScopes.get(rootKey)
102
- return scope?.head === head ? scope : undefined
103
- }
104
-
105
- // the cardinality invariant, made observable: how many anchor entries are resident at a root's CURRENT head
106
- // (every root when none is named). It must track the corpus's anchors, never how many times the board has
107
- // been rebuilt — one retained generation, not one per rebuild.
108
- export function freshnessCacheSize(root?: string): number {
109
- if (root !== undefined) return rootScopes.get(resolve(root))?.anchors.size ?? 0
110
- let total = 0
111
- for (const scope of rootScopes.values()) total += scope.anchors.size
112
- return total
113
- }
114
-
115
- // The same invariant for the OTHER probe in this scope. It needs its own counter rather than a bigger number
116
- // from the one above: the two hold different things (content anchor entries vs selector-anchor verdicts), and
117
- // a counter that cannot see a case is exactly how "one retained generation, not one per rebuild" came to be
118
- // declared, observable, and still violated ([[selector-anchor-scope]]).
119
- export function anchorVerdictCacheSize(root?: string): number {
120
- if (root !== undefined) return rootScopes.get(resolve(root))?.anchorVerdicts.size ?? 0
121
- let total = 0
122
- for (const scope of rootScopes.values()) total += scope.anchorVerdicts.size
123
- return total
124
- }
125
-
126
- function touchAnchor(scope: RootScope, sha: string, imageOid: string | null | undefined): AnchorVerdicts {
127
- const hit = scope.anchors.get(sha)
128
- if (hit && imageOid === undefined) return hit
129
- if (hit && hit.imageOid == null && imageOid !== undefined) {
130
- hit.imageOid = imageOid
131
- hit.gone = imageOid === null
132
- return hit
133
- }
134
- if (hit && hit.imageOid === imageOid) return hit
135
- const entry: AnchorVerdicts = { imageOid, verdicts: new Map(), gone: imageOid === null, pending: new Set(), flight: null }
136
- scope.anchors.set(sha, entry)
137
- return entry
138
- }
139
-
140
- type HeavyDiffWaiter = {
141
- signal?: AbortSignal
142
- resolve: (release: () => void) => void
143
- reject: (error: Error) => void
144
- onAbort: () => void
145
- }
146
- type HeavyDiffScope = { active: boolean; waiting: HeavyDiffWaiter[] }
147
- const heavyDiffScopes = new Map<string, HeavyDiffScope>()
148
-
149
- // One tree comparison at a time is enough for a production repo's pack/index memory. The graph-wide git
150
- // permits still bound every child; this narrower domain scheduler keeps these immutable-tree
151
- // comparisons serial per (repo, HEAD), without teaching the git transport what a content fallback is.
152
- function acquireHeavyDiff(scopeKey: string, signal?: AbortSignal): Promise<() => void> {
153
- if (signal?.aborted) return Promise.reject(gitAbortError())
154
- let scope = heavyDiffScopes.get(scopeKey)
155
- if (!scope) {
156
- scope = { active: false, waiting: [] }
157
- heavyDiffScopes.set(scopeKey, scope)
158
- }
159
-
160
- const releaseFor = (): (() => void) => {
161
- let released = false
162
- return () => {
163
- if (released) return
164
- released = true
165
- scope!.active = false
166
- while (scope!.waiting.length) {
167
- const waiter = scope!.waiting.shift()!
168
- waiter.signal?.removeEventListener('abort', waiter.onAbort)
169
- if (waiter.signal?.aborted) {
170
- waiter.reject(gitAbortError())
171
- continue
172
- }
173
- scope!.active = true
174
- waiter.resolve(releaseFor())
175
- return
176
- }
177
- if (heavyDiffScopes.get(scopeKey) === scope) heavyDiffScopes.delete(scopeKey)
178
- }
179
- }
180
-
181
- if (!scope.active) {
182
- scope.active = true
183
- return Promise.resolve(releaseFor())
184
- }
185
- return new Promise((resolvePermit, reject) => {
186
- const waiter: HeavyDiffWaiter = {
187
- signal,
188
- resolve: resolvePermit,
189
- reject,
190
- onAbort: () => {
191
- const index = scope!.waiting.indexOf(waiter)
192
- if (index < 0) return
193
- scope!.waiting.splice(index, 1)
194
- signal?.removeEventListener('abort', waiter.onAbort)
195
- reject(gitAbortError())
196
- if (!scope!.active && scope!.waiting.length === 0 && heavyDiffScopes.get(scopeKey) === scope)
197
- heavyDiffScopes.delete(scopeKey)
198
- },
199
- }
200
- scope!.waiting.push(waiter)
201
- signal?.addEventListener('abort', waiter.onAbort, { once: true })
202
- })
203
- }
204
-
205
- // @@@ one bounded batch per planned population, only the paths actually asked about - the tree comparison is pathspec-scoped
206
- // (`:(literal)` so a path holding a glob char, a space or a leading colon is matched verbatim, `-z` so the
207
- // answer needs no unquoting), and the answer retained is one boolean per REQUESTED path. Concurrent primes
208
- // on the same anchor therefore union their paths BEFORE the child starts: a caller records what it needs in
209
- // `pending`, then either starts the batch that drains pending or joins the running one and re-checks after —
210
- // so a path requested mid-flight rides the NEXT batch instead of racing this one, and a settled path is
211
- // never asked again. Every bounded chunk stays serial through the same scope permit.
212
- const CONTENT_BATCH_RECORD_LIMIT = 8_192
213
- const CONTENT_BATCH_ARGV_BYTE_LIMIT = 16 * 1_024
214
- const CONTENT_BATCH_MARKER = '\x1espex-content:'
215
- type ContentBatchRow = { sha: string; oid: string; entry: AnchorVerdicts; requested: string[] }
216
-
217
- function contentBatchArgs(root: string, paths: readonly string[]): string[] {
218
- return [
219
- '-C', root, '-c', 'core.quotePath=false', 'diff-tree', '--stdin', '--always', '-r', '--name-status', '-z',
220
- '--no-renames', '--no-color', '--no-ext-diff', '--no-textconv', `--format=${CONTENT_BATCH_MARKER}%H`,
221
- '--', ...paths.map((path) => `:(literal)${path}`),
222
- ]
223
- }
224
- function argvBytes(args: readonly string[]): number {
225
- return args.reduce((total, arg) => total + Buffer.byteLength(arg) + 1, 0)
226
- }
227
-
228
- function contentBatchChunks(root: string, rows: ContentBatchRow[]): ContentBatchRow[][] {
229
- const chunks: ContentBatchRow[][] = []
230
- let byRow = new Map<ContentBatchRow, ContentBatchRow>()
231
- let paths = new Set<string>()
232
- const flush = () => {
233
- if (byRow.size) chunks.push([...byRow.values()])
234
- byRow = new Map()
235
- paths = new Set()
236
- }
237
- for (const row of rows) {
238
- for (const path of row.requested) {
239
- for (;;) {
240
- const nextPaths = paths.has(path) ? paths : new Set(paths).add(path)
241
- const nextRows = byRow.size + (byRow.has(row) ? 0 : 1)
242
- const withinRecords = nextRows * nextPaths.size <= CONTENT_BATCH_RECORD_LIMIT
243
- const withinArgv = argvBytes(contentBatchArgs(root, [...nextPaths])) <= CONTENT_BATCH_ARGV_BYTE_LIMIT
244
- if (withinRecords && withinArgv) break
245
- if (!byRow.size)
246
- throw new Error(`git content diff path exceeds the ${CONTENT_BATCH_ARGV_BYTE_LIMIT}-byte argv bound: ${path}`)
247
- flush()
248
- }
249
- let slice = byRow.get(row)
250
- if (!slice) {
251
- slice = { ...row, requested: [] }
252
- byRow.set(row, slice)
253
- }
254
- slice.requested.push(path)
255
- paths.add(path)
256
- }
257
- }
258
- flush()
259
- return chunks
260
- }
261
-
262
- function parseContentBatch(out: string, rows: ContentBatchRow[]): Set<string>[] {
263
- let rowIndex = -1
264
- let status: string | null = null
265
- const changed = rows.map(() => new Set<string>())
266
- for (const raw of out.split('\0')) {
267
- if (!raw) continue
268
- const token = raw.startsWith('\n') ? raw.slice(1) : raw
269
- if (status !== null) {
270
- if (rowIndex < 0 || rowIndex >= rows.length) throw new Error('git content diff batch emitted a path before its pair marker')
271
- changed[rowIndex].add(token)
272
- status = null
273
- continue
274
- }
275
- if (token.startsWith(CONTENT_BATCH_MARKER)) {
276
- rowIndex++
277
- if (rowIndex >= rows.length) throw new Error('git content diff batch emitted too many pair markers')
278
- continue
279
- }
280
- if (!/^[A-Z](?:\d+)?$/.test(token)) throw new Error(`git content diff batch emitted malformed status '${token}'`)
281
- status = token
282
- }
283
- if (status !== null) throw new Error(`git content diff batch ended after status '${status}'`)
284
- if (rowIndex + 1 !== rows.length)
285
- throw new Error(`git content diff batch emitted ${rowIndex + 1} pair markers for ${rows.length} pairs`)
286
- return changed
287
- }
288
-
289
- async function resolvedContentImages(
290
- root: string,
291
- raws: readonly string[],
292
- replacements: ReadonlyMap<string, string>,
293
- ): Promise<(string | null)[]> {
294
- const targets = raws.map((raw) => {
295
- let target = raw
296
- const seen = new Set<string>()
297
- while (replacements.has(target)) {
298
- if (seen.has(target)) throw new Error(`refs/replace cycle while resolving content image '${raw}'`)
299
- seen.add(target)
300
- target = replacements.get(target)!
301
- }
302
- return target
303
- })
304
- // One no-replace child proves both the names callers supplied and the exact final replacement targets.
305
- // A target cannot make a nonexistent raw id look available, and a missing replacement target is loud.
306
- const queries = [...new Set([...raws, ...targets])]
307
- const answers = await batchRevisionOids(root, queries, { replaceObjects: false })
308
- const checked = new Map(queries.map((query, index) => [query, answers[index]]))
309
- return raws.map((raw, index) => {
310
- if (!checked.get(raw)) return null
311
- const target = checked.get(targets[index])
312
- if (!target) throw new Error(`replacement target '${targets[index]}' for content image '${raw}' is unreadable`)
313
- return target
314
- })
315
- }
316
-
317
- async function runContentBatch(root: string, currentOid: string, rows: ContentBatchRow[]): Promise<void> {
318
- const settled: { row: ContentBatchRow; changed: Set<string> }[] = []
319
- for (const chunk of contentBatchChunks(root, rows)) {
320
- const paths = [...new Set(chunk.flatMap((row) => row.requested))].sort()
321
- const result = await gitTry(contentBatchArgs(root, paths), {
322
- input: chunk.map((row) => `${row.oid} ${currentOid}`).join('\n') + '\n',
323
- // batchRevisionOids already froze replacements/grafts into exact object ids. Do not reinterpret those
324
- // ids if refs/replace moves between the resolution child and this content child.
325
- extraEnv: { GIT_NO_REPLACE_OBJECTS: '1' },
326
- })
327
- if (!result.ok)
328
- throw new Error(`git content diff batch failed (${result.failure ?? 'unknown'}): ${result.stderr.trim() || 'unknown git error'}`)
329
- parseContentBatch(result.stdout, chunk).forEach((changed, index) => settled.push({ row: chunk[index], changed }))
330
- }
331
- // Publish only after every chunk succeeds. A late transport failure leaves the entire planned population
332
- // retryable instead of turning its early chunks into an accidental partial freshness answer.
333
- for (const { row, changed } of settled)
334
- for (const path of row.requested) row.entry.verdicts.set(path, changed.has(path))
335
- }
336
-
337
- function startPluralContentBatch(root: string, rootKey: string, scope: RootScope, rows: { sha: string; entry: AnchorVerdicts }[]): Promise<void> {
338
- const run = async (): Promise<void> => {
339
- const release = await acquireHeavyDiff(`${rootKey}\x1f${scope.head}`, currentGitBuildAbortSignal())
340
- const batch = rows.map((row) => ({ ...row, oid: row.entry.imageOid!, requested: [...row.entry.pending] }))
341
- .filter((row) => row.requested.length)
342
- for (const row of batch) row.entry.pending.clear()
343
- try { await runContentBatch(root, scope.currentOid!, batch) }
344
- finally { release() }
345
- }
346
- let flight!: Promise<void>
347
- flight = run().finally(() => {
348
- for (const row of rows) if (row.entry.flight === flight) row.entry.flight = null
349
- })
350
- for (const row of rows) row.entry.flight = flight
351
- return flight
352
- }
353
-
354
- // @@@ the drift COUNT is a reachability question, so ONE walk answers the whole roster - counting it as
355
- // `rev-list --count <anchor>..<HEAD> -- <path>` names an OFF-HISTORY range, so Git can never cut the walk
356
- // short: every (anchor, path) pair traverses the entire history, and the pairs multiply. Measured cold on a
357
- // 437-anchor deployment scope: 1748 of the read's 1750 git children were those counts. What HEAD's index
358
- // genuinely lacks is only the ANCHORS' own ancestry, and one `rev-list --parents` walk over the whole roster
359
- // carries it; the count is then the same in-memory rule `eventsSince` applies on the in-history side. The
360
- // walk is a SEPARATE structure from the shared drift index on purpose: grafting off-history tips into that
361
- // index would make `ancestorsOf` stop answering undefined for them and silently switch the freshness
362
- // DECISION from this content probe to ancestry. A rejected walk caches nothing and every joiner sees the
363
- // same failure, matching the anchor batch above.
364
- async function primeAnchorTopology(root: string, scope: RootScope, oids: readonly string[]): Promise<void> {
365
- for (;;) {
366
- const missing = oids.filter((oid) => !scope.topologyRoster.has(oid))
367
- if (!missing.length) return
368
- // an anchor that arrives mid-walk rides the NEXT one instead of racing this one, exactly as a
369
- // mid-flight path rides the next content batch.
370
- if (scope.topologyFlight) { await scope.topologyFlight; continue }
371
- const roster = [...new Set([...scope.topologyRoster, ...missing])]
372
- const run = async (): Promise<void> => {
373
- const reach = await unionTopology(root, roster)
374
- // one child-before-parent pass for the whole roster; the bytes are identical to asking one by one.
375
- primeAncestorClosures(reach, roster)
376
- scope.topology = reach
377
- scope.topologyRoster = new Set(roster)
378
- }
379
- let flight!: Promise<void>
380
- flight = run().finally(() => { if (scope.topologyFlight === flight) scope.topologyFlight = null })
381
- scope.topologyFlight = flight
382
- await flight
383
- }
384
- }
385
-
386
- export function contentProbeFor(root: string): ContentProbe {
387
- const rootKey = resolve(root)
388
- let head: string | undefined
389
- let activeImageKey: string | undefined
390
- const headOf = () => (head ??= headSha(root))
391
- const imageOf = () => {
392
- const rawHead = headOf()
393
- const interpretation = gitObjectInterpretation(root)
394
- return { rawHead, key: `${rawHead}\x1f${interpretation.identity}`, interpretation }
395
- }
396
- const primeMany = async (demands: readonly ContentProbeDemand[]): Promise<void> => {
397
- if (!demands.length) return
398
- const planned = new Map<string, { paths: Set<string>; codePaths: Set<string>; evalPaths: Set<string> }>()
399
- for (const demand of demands) {
400
- const row = planned.get(demand.anchorSha) ?? { paths: new Set(), codePaths: new Set(), evalPaths: new Set() }
401
- for (const path of demand.paths) { if (path) { row.paths.add(path); row.codePaths.add(path) } }
402
- if (demand.evalPath) { row.paths.add(demand.evalPath); row.evalPaths.add(demand.evalPath) }
403
- planned.set(demand.anchorSha, row)
404
- }
405
- let scope!: RootScope
406
- for (;;) {
407
- const image = imageOf()
408
- activeImageKey = image.key
409
- const current = scopeFor(rootKey, image.key, null)
410
- for (const [sha, row] of planned) {
411
- const entry = touchAnchor(current, sha, undefined)
412
- for (const path of row.paths) if (!entry.verdicts.has(path)) entry.pending.add(path)
413
- }
414
- const needsResolution = current.currentOid === null || [...planned].some(([sha]) => {
415
- const entry = current.anchors.get(sha)!
416
- return entry.imageOid === undefined || entry.gone
417
- })
418
- if (!needsResolution) { scope = current; break }
419
- // Resolve HEAD and every anchor in ONE child, then require the interpretation inputs to remain stable.
420
- // The returned object ids become the only images downstream commands may execute against.
421
- const shas = [...planned.keys()]
422
- const resolved = await resolvedContentImages(root, [image.rawHead, ...shas], image.interpretation.replacements)
423
- if (imageOf().key !== image.key) continue
424
- const currentOid = resolved[0]
425
- if (!currentOid) throw new Error(`git content diff current image '${image.rawHead}' is unreadable`)
426
- scope = scopeFor(rootKey, image.key, currentOid)
427
- shas.forEach((sha, index) => touchAnchor(scope, sha, resolved[index + 1]))
428
- break
429
- }
430
- const grouped = new Map<string, { entry: AnchorVerdicts; paths: Set<string>; codePaths: Set<string>; evalPaths: Set<string> }>()
431
- for (const [sha, row] of planned) {
432
- const entry = scope.anchors.get(sha)
433
- if (!entry) throw new Error(`git content diff did not resolve planned anchor '${sha}'`)
434
- grouped.set(sha, { entry, ...row })
435
- }
436
- for (;;) {
437
- const waiting = new Set<Promise<void>>()
438
- const ready: { sha: string; entry: AnchorVerdicts }[] = []
439
- for (const [sha, row] of grouped) {
440
- if (row.entry.gone) continue
441
- const missing = [...row.paths].filter((path) => !row.entry.verdicts.has(path))
442
- if (!missing.length) continue
443
- for (const path of missing) row.entry.pending.add(path)
444
- if (row.entry.flight) waiting.add(row.entry.flight)
445
- else ready.push({ sha, entry: row.entry })
446
- }
447
- if (waiting.size) { await Promise.all(waiting); continue }
448
- if (!ready.length) break
449
- await startPluralContentBatch(root, rootKey, scope, ready)
450
- }
451
- // only anchors whose governed content actually differs need their past: those are the readings that
452
- // will render a drift count, and a byte-identical population walks nothing at all.
453
- const drifted = [...grouped.values()]
454
- .filter((row) => !row.entry.gone && [...row.codePaths].some((path) => row.entry.verdicts.get(path) === true))
455
- .map((row) => row.entry.imageOid!)
456
- if (drifted.length) await primeAnchorTopology(root, scope, drifted)
457
- for (const [sha, row] of grouped) {
458
- if (row.entry.gone) continue
459
- for (const evalPath of row.evalPaths) if (row.entry.verdicts.get(evalPath) === true) {
460
- scope.blockDemands.add(`${row.entry.imageOid}\x1f${evalPath}`)
461
- scope.blockDemands.add(`${scope.currentOid}\x1f${evalPath}`)
462
- }
463
- }
464
- }
465
- return {
466
- async prime(sha, paths, evalPath) {
467
- await primeMany([{ anchorSha: sha, paths, evalPath }])
468
- },
469
- primeMany,
470
- async primeBlocks() {
471
- const image = imageOf()
472
- activeImageKey = image.key
473
- const scope = currentScope(rootKey, image.key)
474
- if (!scope?.blockDemands.size) return
475
- const demands = [...scope.blockDemands].map((k) => {
476
- const cut = k.indexOf('\x1f')
477
- return { rev: k.slice(0, cut), path: k.slice(cut + 1) }
478
- })
479
- scope.blockDemands.clear()
480
- await primeScenarioBlocks(root, demands)
481
- },
482
- changed(sha, path) {
483
- // The async prime owns all I/O. An unprimed path is 'can't testify', never a fresh sync diff: a miss
484
- // stays conservative and no abort/transient failure can turn into an unbounded synchronous fallback.
485
- const entry = activeImageKey ? currentScope(rootKey, activeImageKey)?.anchors.get(sha) : undefined
486
- if (!entry || entry.gone) return null
487
- return entry.verdicts.get(path) ?? null
488
- },
489
- canTestify(sha) {
490
- // a settled verdict — of either polarity — is the proof the anchor's tree was readable
491
- const entry = activeImageKey ? currentScope(rootKey, activeImageKey)?.anchors.get(sha) : undefined
492
- return !!entry && !entry.gone && entry.verdicts.size > 0
493
- },
494
- scenarioDiffers(sha, evalPath, scenario) {
495
- const scope = activeImageKey ? currentScope(rootKey, activeImageKey) : undefined
496
- const entry = scope?.anchors.get(sha)
497
- const a = entry?.imageOid ? scenarioBlocksAt(root, entry.imageOid, evalPath) : null
498
- if (!a) return true // eval.md unreadable at the anchor (renamed/absent) → can't prove → stale
499
- return a.get(scenario) !== scenarioBlocksAt(root, scope!.currentOid!, evalPath)?.get(scenario)
500
- },
501
- inAnchorPast(sha, commit) {
502
- const scope = activeImageKey ? currentScope(rootKey, activeImageKey) : undefined
503
- const oid = scope?.anchors.get(sha)?.imageOid
504
- if (!scope?.topology || !oid) return false
505
- const bits = ancestorsOf(scope.topology, oid)
506
- return !!bits && inAncestors(scope.topology, bits, commit)
507
- },
508
- }
509
- }
510
-
511
- // @@@ the code axis's SPATIAL narrowing ([[code-anchor]]'s path#symbol, reused whole) - a shared FILE is not a
512
- // shared BEHAVIOUR: harness.ts carries eight adapters, so a one-adapter edit re-flagged every other adapter's
513
- // reading, and those refresh only through a real dispatched session of that harness. An anchored entry
514
- // therefore asks the spatial question instead of the file question — did a commit in codeSha..HEAD intersect
515
- // one of the named units? — through the SAME parse/extract/resolve/hunk∩range engine spec drift runs. Two
516
- // deliberate differences from that engine: the window carries NO ack filter (an ack vindicates a spec, not a
517
- // reading), and it never widens — the file question runs FIRST and the anchor can only subtract from it.
518
- // Fed at the call sites like the ContentProbe, so the decision functions stay pure over their inputs.
519
- export type AnchorProbe = {
520
- // did any commit in sinceSha..HEAD touch one of THESE anchored units?
521
- // null = cannot testify (unprimed, off-history, no usable extractor) — callers stay conservatively stale.
522
- hit(sinceSha: string, path: string, selectors: readonly string[]): boolean | null
523
- // The demand set is PLURAL because the engine underneath is: every window's Git images and ordinary hunks
524
- // are immutable, so one call owns them once for the whole read. A caller that primes one reading at a time
525
- // re-forks that batch per reading, which on this corpus was ~2.5k children for ~800 verdicts.
526
- prime?(demands: readonly AnchorDemand[]): Promise<void>
527
- }
528
- export type AnchorDemand = { sinceSha: string; entries: readonly RelationEntry[] }
529
-
530
- // a verdict answers ONE selector set, so the set is part of its identity: several scenarios anchoring
531
- // DIFFERENT units of one shared file is the whole point of narrowing, and keying only by (sha, path) would
532
- // hand the first one's answer to all the others — silently, and in the fresh direction.
533
- const anchorKey = (sinceSha: string, path: string, selectors: readonly string[]) =>
534
- `${sinceSha}\x1f${path}\x1f${[...selectors].sort().join('\x1e')}`
535
-
536
- // @@@ one parse per CONTENT, not per selector entry - a node's `code:` entries are asked one at a time, and
537
- // the same working-tree file backs many of them, so the unmemoized read parsed 46 distinct files 922 times
538
- // per build (40.4 MB through the TypeScript parser) and did it again on every rebuild. `extract` is a pure
539
- // function of (text, path, extractor), so its result is reusable exactly as far as the CONTENT is unchanged.
540
- // The key is a content digest and never mtime/size: this gate decides whether a reading may testify, so a
541
- // stale unit list would let a dead selector read as alive — the precise failure the comment below warns
542
- // about. Digesting is ~10x cheaper than parsing, so the read stays and only the parse is saved. Bounded like
543
- // the historical-revision memo it mirrors ([[code-anchor]]), and it caches the extractor's REJECTION too, so
544
- // an unparseable file does not re-parse once per entry.
545
- const CURRENT_TREE_MEMO_MAX = 4096
546
- const currentTreeUnitMemo = new Map<string, { units: Unit[] } | { failed: string }>()
547
- // The memo key doubles as the IMAGE's name — the extractor that answered plus the exact bytes it read — so a
548
- // caller retaining a verdict derived from these units names it by the same identity that decides the parse
549
- // ([[selector-anchor-scope]]). Returning the key rather than re-deriving it elsewhere is what keeps the two
550
- // from drifting apart.
551
- function currentTreeImage(root: string, x: Extractor, path: string): { key: string; units: Unit[] } {
552
- const source = readFileSync(join(root, path), 'utf8')
553
- const key = `${x.memoKey(path)}\0${createHash('sha1').update(source).digest('hex')}`
554
- const hit = currentTreeUnitMemo.get(key)
555
- if (hit) { if ('failed' in hit) throw new Error(hit.failed); return { key, units: hit.units } }
556
- let entry: { units: Unit[] } | { failed: string }
557
- try { entry = { units: x.extract(source, path) } }
558
- catch (err: any) { entry = { failed: err?.message ?? String(err) } }
559
- if (currentTreeUnitMemo.size >= CURRENT_TREE_MEMO_MAX) currentTreeUnitMemo.clear()
560
- currentTreeUnitMemo.set(key, entry)
561
- if ('failed' in entry) throw new Error(entry.failed)
562
- return { key, units: entry.units }
563
- }
564
-
565
- // what selector resolution READS for one path, named by its identity — or the reason nothing could be read.
566
- // Split out from the verdict below so a caller can name the image before deciding whether it still has to
567
- // derive the verdict at all; a `problem` image has no name, and an unnameable input is never retained.
568
- type EntryImage = { key: string; units: Unit[] } | { problem: string }
569
- function entryImage(root: string, regs: Extractor[], path: string, selector: string | undefined): EntryImage {
570
- const x = extractorFor(regs, extOf(path))
571
- if (!x) return { problem: `\`code\` selector \`${path}#${selector}\` — no designated extractor for that language; drop the #anchor or add a language row` }
572
- const ready = x.ready()
573
- if (ready !== true) return { problem: `\`code\` anchors on ${path} are unverified: ${ready}` }
574
- try { return currentTreeImage(root, x, path) }
575
- catch (err: any) { return { problem: `\`code\` anchors on ${path} are unverified: ${err?.message ?? String(err)}` } }
576
- }
577
-
578
- // every selector of one entry resolves to exactly one unit in the CURRENT tree, or the entry cannot testify.
579
- // This gate is what stops a DEAD selector from reading fresh: the hit engine answers "no commit touched a
580
- // unit of that name", which for a name that exists nowhere is a vacuous no — true of spec drift, where the
581
- // dead anchor is a separate blocking error, and dangerously false here, where the same silence would retire
582
- // a reading's whole code axis. `problem` names the repair for lint; null means the entry is verifiable.
583
- function selectorProblem(units: Unit[], path: string, selectors: readonly string[]): string | null {
584
- // the SAME classifier the gate uses ([[code-anchor]]'s resolveSelectors) — only the wording is ours, so a
585
- // selector can never be verifiable to one reader and dead to the other.
586
- for (const r of resolveSelectors(units, selectors)) {
587
- if ('dead' in r) return `\`code\` selector \`${path}#${r.selector}\` names no unit in that file — follow the rename or drop the selector (evals stay stale until then)`
588
- if ('ambiguous' in r) return `\`code\` selector \`${path}#${r.selector}\` is ambiguous — ${r.ambiguous} units share that name; pin a unique one`
589
- }
590
- return null
591
- }
592
- function entryUnverifiable(root: string, regs: Extractor[], entry: RelationEntry): string | null {
593
- const image = entryImage(root, regs, entry.path, entry.selectors[0])
594
- return 'problem' in image ? image.problem : selectorProblem(image.units, entry.path, entry.selectors)
595
- }
596
-
597
- // @@@ the verify sweep must reach the MACROTASK queue - resolving every reading's `code:` selectors against
598
- // the working tree is a doubly-nested SYNCHRONOUS sweep (4,384 demands on this corpus), and nothing in it
599
- // awaits, so it ran as one uninterruptible stretch: measured, it held the loop for 1,104ms, which is the
600
- // `/health` p99 this gate is judged by. The bound defended here is a LIVENESS signal, not a latency taste —
601
- // a probe that cannot answer is indistinguishable from a dead backend, and the CLI allows it 600ms while the
602
- // supervisor allows 1,000ms before it keeps the old child. `setImmediate` is what actually returns to the
603
- // loop's I/O phase (awaiting a synchronous-bodied async fn only drains microtasks). A time budget rather
604
- // than every-iteration keeps the common cheap iteration from paying a turn: the sweep's own longest single
605
- // step is one file parse, so the worst hold stays near budget + that step.
606
- const VERIFY_YIELD_BUDGET_MS = Number(process.env.SPEXCODE_VERIFY_YIELD_BUDGET_MS || 50)
607
- const yieldToEventLoop = (): Promise<void> => new Promise<void>((resolve) => { setImmediate(resolve) })
608
-
609
- // A verdict's window is `sinceSha..HEAD`, and that window LENGTHENS as HEAD advances — the same
610
- // (sinceSha, path, selectors) that answers false at one head can legitimately answer true at the next. So
611
- // these are per-head immutable, never content-addressed-forever, and they belong in the root's current-head
612
- // scope above rather than in a memo of their own ([[selector-anchor-scope]]).
613
- // The working tree, however, does NOT stand still at one head: a session re-images the files a selector
614
- // resolves against, and each image is its own key. Bound that with the same drop-wholesale idiom the
615
- // current-tree parse memo uses instead of inventing a second eviction policy.
616
- const ANCHOR_VERDICT_MAX = Math.max(1024, Number(process.env.SPEXCODE_ANCHOR_VERDICT_SLOTS || 8192))
617
-
618
- export function anchorProbeFor(root: string, idx: DriftIndex): AnchorProbe {
619
- const rootKey = resolve(root)
620
- const regs = extractors(root)
621
- const verdicts = new Map<string, boolean>()
622
- // The scope is the one the content probe already owns, entered only when the index's own tip IS the
623
- // current head. An index built at another tip cannot borrow this head's answers, and — the reason this is
624
- // a guard rather than a rotation — a head that moved mid-read must not make THIS probe displace the
625
- // content probe's just-settled scope. Either the two probes agree on the head and share one scope, or this
626
- // one keeps nothing and recomputes.
627
- const scopeOf = (): RootScope | null => {
628
- if (!idx.tip) return null
629
- const rawHead = headSha(root)
630
- if (idx.tip !== rawHead) return null
631
- return scopeFor(rootKey, `${rawHead}\x1f${gitObjectInterpretation(root).identity}`, null)
632
- }
633
- // which extractor BUILD answered is part of a verdict's identity too — a host TypeScript resolving to a
634
- // different module or version must not inherit the previous one's units.
635
- const registry = regs.map((x) => x.memoKey('\0registry')).join('\x1e')
636
- return {
637
- async prime(demands) {
638
- const scope = scopeOf()
639
- const keys: string[] = []
640
- const scoped: (string | null)[] = []
641
- const queries: AnchorHitQuery[] = []
642
- const queued = new Set<string>()
643
- // one coherent read of each source per sweep: every entry on a path resolves against the same bytes,
644
- // and the read that names the verdict is the read the verdict was derived from.
645
- const images = new Map<string, EntryImage>()
646
- const imageOf = (path: string, selector: string | undefined): EntryImage => {
647
- let hit = images.get(path)
648
- if (hit === undefined) { hit = entryImage(root, regs, path, selector); images.set(path, hit) }
649
- return hit
650
- }
651
- const remember = (name: string | null, hit: boolean) => { if (scope && name !== null) {
652
- if (scope.anchorVerdicts.size >= ANCHOR_VERDICT_MAX) scope.anchorVerdicts.clear()
653
- scope.anchorVerdicts.set(name, hit)
654
- } }
655
- let sinceYield = Date.now()
656
- for (const { sinceSha, entries } of demands) for (const e of entries) {
657
- if (Date.now() - sinceYield >= VERIFY_YIELD_BUDGET_MS) { await yieldToEventLoop(); sinceYield = Date.now() }
658
- if (!e.selectors.length) continue
659
- const key = anchorKey(sinceSha, e.path, e.selectors)
660
- if (verdicts.has(key) || queued.has(key)) continue
661
- const image = imageOf(e.path, e.selectors[0])
662
- const name = 'problem' in image ? null : `${registry}\x1f${image.key}\x1f${key}`
663
- const remembered = scope && name !== null ? scope.anchorVerdicts.get(name) : undefined
664
- if (remembered !== undefined) { verdicts.set(key, remembered); continue }
665
- // a miss runs the FULL derivation below, unchanged — it is what a verdict MEANS, not a fallback
666
- if ('problem' in image || selectorProblem(image.units, e.path, e.selectors)) continue // no verdict → conservative stale (lint says why)
667
- const win = eventsSince(idx, sinceSha, e.path)
668
- if (win === null) continue
669
- if (!win.length) { verdicts.set(key, false); remember(name, false); continue }
670
- queued.add(key)
671
- keys.push(key)
672
- scoped.push(name)
673
- queries.push({ win, symbols: [...e.selectors] })
674
- }
675
-
676
- if (!queries.length) return
677
- const results = await anchorHitExists(root, queries, regs)
678
- results.forEach((hit, index) => { verdicts.set(keys[index], hit); remember(scoped[index], hit) })
679
- },
680
- hit(sinceSha, path, selectors) {
681
- return verdicts.get(anchorKey(sinceSha, path, selectors)) ?? null
682
- },
683
- }
684
- }
685
-
686
- // the LOUD half: a selector is a claim that a named unit EXISTS, held to the same standard as a ghost path.
687
- // Dead, ambiguous, unparseable, or no designated extractor — each names itself and its repair, and until
688
- // repaired the probe issues no verdict, so the reading stays stale. Over-warn, never a silent pass.
689
- export function anchorProblems(root: string, entries: readonly RelationEntry[]): string[] {
690
- const regs = extractors(root)
691
- const out: string[] = []
692
- for (const e of entries) {
693
- if (!e.selectors.length) continue
694
- const problem = entryUnverifiable(root, regs, e)
695
- if (problem) out.push(problem)
696
- }
697
- return out
698
- }
699
-
700
- // the REMARK axis's input ([[remark-teeth]]): the teeth read only the resolvable bit + when it was resolved,
701
- // not the whole remark — so freshness stays a PURE function, fed the scenario's remark track at the call
702
- // sites (never reaching into the issue store). One signal per remark on the (node, scenario).
703
- export type RemarkSignal = { resolved: boolean; resolvedAt?: string }
704
-
705
- // the teeth (T1): a scenario is remark-stale unless EVERY remark is resolved AND this reading post-dates
706
- // every resolution. So an UNRESOLVED remark ages it; a RESOLVED remark keeps it stale until a reading taken
707
- // strictly after the resolve (reading.ts > resolvedAt) exists — you can't out-run a remark by re-measuring
708
- // before the resolve, nor clear it by passive receipt. A resolved bit with no timestamp stays conservatively
709
- // stale (defensive: resolveRemark always stamps one).
710
- export function remarkStale(reading: { ts: string }, remarks: RemarkSignal[]): boolean {
711
- return remarks.some((r) => !r.resolved || !(r.resolvedAt && reading.ts > r.resolvedAt))
712
- }
713
-
714
- // true iff some commit touched `path` that is NOT an ancestor of `sinceSha` — i.e. it lies in
715
- // `sinceSha..HEAD` by true DAG reachability, never a log-position/date compare (which under-reports on
716
- // branchy history). An off-history `sinceSha` falls back to the content probe when one is fed (see
717
- // ContentProbe above); without a probe — or when the anchor object is gone — freshness can't be proven
718
- // from HEAD's history, so it reads stale rather than silently pass.
719
- export function changedSince(idx: DriftIndex, sinceSha: string, path: string, probe?: ContentProbe): boolean {
720
- const events = eventsSince(idx, sinceSha, path)
721
- // null = ancestry cannot testify for this anchor; only then does content get a say ([[root-lru]]'s sibling
722
- // rule: one meaning of changed-since, each layer's own fallback on top).
723
- if (events) return events.length > 0
724
- return probe?.changed(sinceSha, path) ?? true
725
- }
726
-
727
- // the code axis's DISPLAY detail: which governed files drifted since a reading, and by HOW MANY commits — so
728
- // a stale eval can say "EvalsFeed.jsx +3" instead of a bare "code moved". Same DAG reachability as
729
- // changedSince (a commit touching the file that is NOT an ancestor of the reading's sha lies in sinceSha..HEAD);
730
- // an off-history sinceSha reports through the same content fallback (only files whose content differs, counted
731
- // against the anchor's own ancestry); with no probe or a gone anchor it counts every touch (conservative,
732
- // matching changedSince).
733
- // Reporting only — it never decides freshness (staleAxes does); it explains a decision already made.
734
- export function codeDrift(idx: DriftIndex, sinceSha: string, codeAxis: ScenarioCodeAxisSource, probe?: ContentProbe): { file: string; behind: number }[] {
735
- // an entry may be anchored (`path#symbol`); drift is reported per BASE FILE — a raw selector string names
736
- // no real path, so counting commits against it would silently report nothing.
737
- const codeFiles = scenarioCodeAxis(undefined, codeAxis).paths
738
- const out: { file: string; behind: number }[] = []
739
- for (const f of codeFiles) {
740
- const since = eventsSince(idx, sinceSha, f)
741
- const events = pathEvents(idx, f)
742
- const differs = since ? undefined : probe?.changed(sinceSha, f)
743
- const behind = since ? new Set(since.map((event) => event.commit)).size
744
- // off-history and the content differs: the SAME rule as the in-history branch above — the path's events
745
- // this anchor has not already seen — read against the anchor's own ancestry instead of HEAD's. Floored
746
- // at one: the trees demonstrably differ, so 'drifted by 0 commits' would be a lie whatever the walk
747
- // could and could not project.
748
- : differs === true ? Math.max(1, new Set(events.filter((event) => !probe!.inAnchorPast(sinceSha, event.commit)).map((event) => event.commit)).size)
749
- : differs === false ? 0
750
- : new Set(events.map((event) => event.commit)).size
751
- if (behind > 0) out.push({ file: f, behind })
752
- }
753
- return out
754
- }
755
-
756
- // @@@scenario axis decides by stored contract hash - a reading filed since #61 carries `scenarioHash`, the
757
- // content hash of the semantic text it was measured against (scenarios.ts scenarioHash — normalized
758
- // description+expected). For such a reading the scenario axis is a PURE TEXT COMPARE: recorded hash vs the
759
- // CURRENT declaration's hash — no git walk, no chain, no ancestry. That is what makes it converge under
760
- // fleet-parallel filing+merging: a sibling scenario's edit, a sidecar-only commit, a merge's textual shift,
761
- // a whitespace re-wrap — none of them move THIS scenario's hash, so none can re-stale its reading; only the
762
- // contract actually changing (or the scenario disappearing from eval.md — `current` undefined) does. A
763
- // LEGACY reading without the hash is decided by the git-derived per-scenario rule below, unchanged — the
764
- // one-shot degradation: exactly ONE track decides each reading (hash if present, else git), never both
765
- // OR-ed together.
766
- function scenarioStaleByHash(reading: Reading, current: Scenario | undefined): boolean | undefined {
767
- if (reading.scenarioHash === undefined) return undefined // legacy → the git rule decides
768
- return current ? scenarioHash(current) !== reading.scenarioHash : true
769
- }
770
-
771
- // LEGACY scenario freshness (readings filed before the stored contract hash) — PER-SCENARIO and SEMANTIC,
772
- // not per-file: a reading stales only when ITS OWN scenario's semantic block (description+expected —
773
- // [[scenariofresh]]'s blockContent projection) moved in scenarioSha..HEAD — never when a sibling in the
774
- // same eval.md did, and never on a metadata-only edit (tags/test/code/related). Reads like the code axis's
775
- // changedSince — the per-scenario change-commits ([[scenariofresh]], rename-followed so a bare git-mv
776
- // reparent isn't a change) tested for ancestry — and an off-history codeSha takes the same content fallback
777
- // at the same granularity and the same projection. Known limit (#61, why the hash replaced it for new
778
- // readings): the change-commit chain is built off a LINEARIZED log walk, so parallel branches editing one
779
- // eval.md cross-attribute each other's edits and can false-stale a sibling's reading across a merge.
780
- function scenarioMoved(scIdx: ScenarioIndex, didx: DriftIndex, sinceSha: string, evalPath: string, scenario: string, probe?: ContentProbe): boolean {
781
- const anc = ancestorsOf(didx, sinceSha)
782
- if (anc) return scenarioChangeCommits(scIdx, evalPath, scenario).some((h) => !inAncestors(didx, anc, h))
783
- const differs = probe?.changed(sinceSha, evalPath)
784
- if (differs == null) return true
785
- if (!differs) return false // whole file byte-identical → this block too
786
- return probe!.scenarioDiffers(sinceSha, evalPath, scenario)
787
- }
788
-
789
- // one declared entry's contribution to the code axis. The FILE question runs first and is unchanged; only an
790
- // ANCHORED entry whose file really moved asks the narrower spatial one, so an anchor can subtract from the
791
- // file verdict but never add to it. No verdict (unprimed, off-history, unverifiable selector) → conservative.
792
- function entryMoved(idx: DriftIndex, sinceSha: string, entry: RelationEntry, probe?: ContentProbe, anchors?: AnchorProbe): boolean {
793
- if (!changedSince(idx, sinceSha, entry.path, probe)) return false
794
- if (!entry.selectors.length) return true
795
- return anchors?.hit(sinceSha, entry.path, entry.selectors) ?? true
796
- }
797
-
798
- export function staleAxes(
799
- reading: Reading,
800
- codeAxis: ScenarioCodeAxisSource,
801
- evalPath: string,
802
- didx: DriftIndex,
803
- scIdx: ScenarioIndex,
804
- remarks: RemarkSignal[] = [],
805
- probe?: ContentProbe,
806
- current?: Scenario, // the scenario's CURRENT declaration (undefined = gone from eval.md) — the hash compare's other side
807
- anchors?: AnchorProbe,
808
- ): StaleAxis[] {
809
- const axes: StaleAxis[] = []
810
- const byHash = scenarioStaleByHash(reading, current)
811
- if (probe && !commitReachable(didx, reading.codeSha) && !probe.canTestify(reading.codeSha)) {
812
- // the anchor commit object is GONE — neither git axis can testify; say that, not "content changed".
813
- // The stored contract hash needs no anchor, so it still decides the scenario axis when present.
814
- axes.push('anchor')
815
- if (byHash) axes.push('scenario')
816
- } else {
817
- if (scenarioCodeAxis(undefined, codeAxis).entries.some((e) => entryMoved(didx, reading.codeSha, e, probe, anchors))) axes.push('code')
818
- if (byHash ?? scenarioMoved(scIdx, didx, reading.codeSha, evalPath, reading.scenario, probe)) axes.push('scenario')
819
- }
820
- if (remarkStale(reading, remarks)) axes.push('remark')
821
- return axes
822
- }
823
-
824
- export function isStale(
825
- reading: Reading,
826
- codeAxis: ScenarioCodeAxisSource,
827
- evalPath: string,
828
- didx: DriftIndex,
829
- scIdx: ScenarioIndex,
830
- remarks: RemarkSignal[] = [],
831
- probe?: ContentProbe,
832
- current?: Scenario,
833
- anchors?: AnchorProbe,
834
- ): boolean {
835
- return staleAxes(reading, codeAxis, evalPath, didx, scIdx, remarks, probe, current, anchors).length > 0
836
- }