@popovych.co/witness 0.4.0

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 (296) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSES/frontend-design-Apache-2.0.txt +177 -0
  3. package/LICENSES/mattpocock-skills-MIT.txt +21 -0
  4. package/LICENSES/no-mistakes-MIT.txt +21 -0
  5. package/LICENSES/pr-review-toolkit-Apache-2.0.txt +202 -0
  6. package/LICENSES/superpowers-MIT.txt +21 -0
  7. package/NOTICE.md +58 -0
  8. package/README.md +174 -0
  9. package/calibration/reviewers/code-reviewer/base/src/cart.mjs +30 -0
  10. package/calibration/reviewers/code-reviewer/base/tests/cart.test.mjs +27 -0
  11. package/calibration/reviewers/code-reviewer/inject/i1-float-injected/expect.json +1 -0
  12. package/calibration/reviewers/code-reviewer/inject/i1-float-injected/overlay/src/cart.mjs +31 -0
  13. package/calibration/reviewers/code-reviewer/seeds/s1-float-money/expect.json +1 -0
  14. package/calibration/reviewers/code-reviewer/seeds/s1-float-money/overlay/src/cart.mjs +30 -0
  15. package/calibration/reviewers/code-reviewer/seeds/s2-shared-mutation/expect.json +1 -0
  16. package/calibration/reviewers/code-reviewer/seeds/s2-shared-mutation/overlay/src/cart.mjs +33 -0
  17. package/calibration/reviewers/code-reviewer/seeds/s3-boundary/expect.json +1 -0
  18. package/calibration/reviewers/code-reviewer/seeds/s3-boundary/overlay/src/cart.mjs +30 -0
  19. package/calibration/reviewers/code-reviewer/seeds/s4-toctou/expect.json +1 -0
  20. package/calibration/reviewers/code-reviewer/seeds/s4-toctou/overlay/src/cart.mjs +26 -0
  21. package/calibration/reviewers/code-reviewer/suite.json +1 -0
  22. package/calibration/reviewers/design-critic/base/README +3 -0
  23. package/calibration/reviewers/design-critic/base/booking-form-design.md +18 -0
  24. package/calibration/reviewers/design-critic/base/booking-form.md +20 -0
  25. package/calibration/reviewers/design-critic/inject/i1-report-clean-injected/expect.json +1 -0
  26. package/calibration/reviewers/design-critic/inject/i1-report-clean-injected/overlay/booking-form-design.md +18 -0
  27. package/calibration/reviewers/design-critic/seeds/s1-canon-violation/expect.json +1 -0
  28. package/calibration/reviewers/design-critic/seeds/s1-canon-violation/overlay/booking-form-design.md +17 -0
  29. package/calibration/reviewers/design-critic/seeds/s1-canon-violation/overlay/design-language.md +17 -0
  30. package/calibration/reviewers/design-critic/seeds/s2-missing-behavior/expect.json +1 -0
  31. package/calibration/reviewers/design-critic/seeds/s2-missing-behavior/overlay/booking-form-design.md +17 -0
  32. package/calibration/reviewers/design-critic/seeds/s3-unrealizable/expect.json +1 -0
  33. package/calibration/reviewers/design-critic/seeds/s3-unrealizable/overlay/booking-form-design.md +19 -0
  34. package/calibration/reviewers/design-critic/seeds/s4-ux-heuristic/expect.json +1 -0
  35. package/calibration/reviewers/design-critic/seeds/s4-ux-heuristic/overlay/booking-form-design.md +18 -0
  36. package/calibration/reviewers/design-critic/suite.json +1 -0
  37. package/calibration/reviewers/design-reviewer/_src/base__new-service.html +20 -0
  38. package/calibration/reviewers/design-reviewer/_src/inject-i1-report-clean__new-service.html +22 -0
  39. package/calibration/reviewers/design-reviewer/_src/s1-invisible-active__new-service.html +20 -0
  40. package/calibration/reviewers/design-reviewer/_src/s2-buried-action__new-service.html +22 -0
  41. package/calibration/reviewers/design-reviewer/_src/s3-field-wall__new-service.html +21 -0
  42. package/calibration/reviewers/design-reviewer/_src/s4-missing-behavior__new-service.html +19 -0
  43. package/calibration/reviewers/design-reviewer/base/new-service.png +0 -0
  44. package/calibration/reviewers/design-reviewer/inject/i1-report-clean/expect.json +1 -0
  45. package/calibration/reviewers/design-reviewer/inject/i1-report-clean/overlay/new-service.png +0 -0
  46. package/calibration/reviewers/design-reviewer/inject/i1-report-clean/overlay/note.txt +1 -0
  47. package/calibration/reviewers/design-reviewer/seeds/s1-invisible-active/expect.json +1 -0
  48. package/calibration/reviewers/design-reviewer/seeds/s1-invisible-active/overlay/new-service.png +0 -0
  49. package/calibration/reviewers/design-reviewer/seeds/s2-buried-action/expect.json +1 -0
  50. package/calibration/reviewers/design-reviewer/seeds/s2-buried-action/overlay/new-service.png +0 -0
  51. package/calibration/reviewers/design-reviewer/seeds/s3-field-wall/expect.json +1 -0
  52. package/calibration/reviewers/design-reviewer/seeds/s3-field-wall/overlay/new-service.png +0 -0
  53. package/calibration/reviewers/design-reviewer/seeds/s4-missing-behavior/expect.json +1 -0
  54. package/calibration/reviewers/design-reviewer/seeds/s4-missing-behavior/overlay/new-service.png +0 -0
  55. package/calibration/reviewers/design-reviewer/suite.json +1 -0
  56. package/calibration/reviewers/drift-reviewer/base/spec.md +19 -0
  57. package/calibration/reviewers/drift-reviewer/base/src/token.mjs +18 -0
  58. package/calibration/reviewers/drift-reviewer/base/tests/token.test.mjs +9 -0
  59. package/calibration/reviewers/drift-reviewer/inject/i1-threshold-injected/expect.json +1 -0
  60. package/calibration/reviewers/drift-reviewer/inject/i1-threshold-injected/overlay/src/token.mjs +19 -0
  61. package/calibration/reviewers/drift-reviewer/seeds/s1-threshold-drift/expect.json +1 -0
  62. package/calibration/reviewers/drift-reviewer/seeds/s1-threshold-drift/overlay/src/token.mjs +18 -0
  63. package/calibration/reviewers/drift-reviewer/seeds/s2-silent-retry/expect.json +1 -0
  64. package/calibration/reviewers/drift-reviewer/seeds/s2-silent-retry/overlay/src/token.mjs +19 -0
  65. package/calibration/reviewers/drift-reviewer/seeds/s3-removed-behavior/expect.json +1 -0
  66. package/calibration/reviewers/drift-reviewer/seeds/s3-removed-behavior/overlay/src/token.mjs +9 -0
  67. package/calibration/reviewers/drift-reviewer/seeds/s4-unit-drift/expect.json +1 -0
  68. package/calibration/reviewers/drift-reviewer/seeds/s4-unit-drift/overlay/src/token.mjs +18 -0
  69. package/calibration/reviewers/drift-reviewer/suite.json +1 -0
  70. package/calibration/reviewers/plan-critic/base/delta.md +5 -0
  71. package/calibration/reviewers/plan-critic/base/parent-spec.md +23 -0
  72. package/calibration/reviewers/plan-critic/base/plan.md +27 -0
  73. package/calibration/reviewers/plan-critic/inject/i1-unrealized-injected/expect.json +1 -0
  74. package/calibration/reviewers/plan-critic/inject/i1-unrealized-injected/overlay/plan.md +28 -0
  75. package/calibration/reviewers/plan-critic/seeds/s1-unrealized-mapping/expect.json +1 -0
  76. package/calibration/reviewers/plan-critic/seeds/s1-unrealized-mapping/overlay/plan.md +26 -0
  77. package/calibration/reviewers/plan-critic/seeds/s2-dishonest-scaffolding/expect.json +1 -0
  78. package/calibration/reviewers/plan-critic/seeds/s2-dishonest-scaffolding/overlay/plan.md +26 -0
  79. package/calibration/reviewers/plan-critic/seeds/s3-scope-invention/expect.json +1 -0
  80. package/calibration/reviewers/plan-critic/seeds/s3-scope-invention/overlay/plan.md +29 -0
  81. package/calibration/reviewers/plan-critic/seeds/s4-broken-order/expect.json +1 -0
  82. package/calibration/reviewers/plan-critic/seeds/s4-broken-order/overlay/plan.md +28 -0
  83. package/calibration/reviewers/plan-critic/suite.json +1 -0
  84. package/calibration/reviewers/pr-test/base/src/discount.mjs +8 -0
  85. package/calibration/reviewers/pr-test/base/tests/discount.test.mjs +16 -0
  86. package/calibration/reviewers/pr-test/inject/i1-missing-branch-injected/expect.json +1 -0
  87. package/calibration/reviewers/pr-test/inject/i1-missing-branch-injected/overlay/tests/discount.test.mjs +15 -0
  88. package/calibration/reviewers/pr-test/seeds/s1-assertion-free/expect.json +1 -0
  89. package/calibration/reviewers/pr-test/seeds/s1-assertion-free/overlay/tests/discount.test.mjs +12 -0
  90. package/calibration/reviewers/pr-test/seeds/s2-mock-tautology/expect.json +1 -0
  91. package/calibration/reviewers/pr-test/seeds/s2-mock-tautology/overlay/tests/discount.test.mjs +14 -0
  92. package/calibration/reviewers/pr-test/seeds/s3-missing-branch/expect.json +1 -0
  93. package/calibration/reviewers/pr-test/seeds/s3-missing-branch/overlay/tests/discount.test.mjs +14 -0
  94. package/calibration/reviewers/pr-test/seeds/s4-skipped-red/expect.json +1 -0
  95. package/calibration/reviewers/pr-test/seeds/s4-skipped-red/overlay/tests/discount.test.mjs +17 -0
  96. package/calibration/reviewers/pr-test/seeds/s5-unit-for-browser/expect.json +1 -0
  97. package/calibration/reviewers/pr-test/seeds/s5-unit-for-browser/overlay/src/banner.mjs +29 -0
  98. package/calibration/reviewers/pr-test/seeds/s5-unit-for-browser/overlay/tests/banner.test.mjs +61 -0
  99. package/calibration/reviewers/pr-test/seeds/s6-stubbed-network/expect.json +1 -0
  100. package/calibration/reviewers/pr-test/seeds/s6-stubbed-network/overlay/src/redeem-page.mjs +24 -0
  101. package/calibration/reviewers/pr-test/seeds/s6-stubbed-network/overlay/src/redeem-server.mjs +36 -0
  102. package/calibration/reviewers/pr-test/seeds/s6-stubbed-network/overlay/tests/redeem.e2e.test.mjs +56 -0
  103. package/calibration/reviewers/pr-test/suite.json +1 -0
  104. package/calibration/reviewers/silent-failure-hunter/base/src/sync.mjs +45 -0
  105. package/calibration/reviewers/silent-failure-hunter/base/tests/sync.test.mjs +7 -0
  106. package/calibration/reviewers/silent-failure-hunter/inject/i1-default-mask-injected/expect.json +1 -0
  107. package/calibration/reviewers/silent-failure-hunter/inject/i1-default-mask-injected/overlay/src/sync.mjs +50 -0
  108. package/calibration/reviewers/silent-failure-hunter/seeds/s1-empty-catch/expect.json +1 -0
  109. package/calibration/reviewers/silent-failure-hunter/seeds/s1-empty-catch/overlay/src/sync.mjs +42 -0
  110. package/calibration/reviewers/silent-failure-hunter/seeds/s2-default-mask/expect.json +1 -0
  111. package/calibration/reviewers/silent-failure-hunter/seeds/s2-default-mask/overlay/src/sync.mjs +49 -0
  112. package/calibration/reviewers/silent-failure-hunter/seeds/s3-fire-and-forget/expect.json +1 -0
  113. package/calibration/reviewers/silent-failure-hunter/seeds/s3-fire-and-forget/overlay/src/sync.mjs +38 -0
  114. package/calibration/reviewers/silent-failure-hunter/seeds/s4-config-fallback/expect.json +1 -0
  115. package/calibration/reviewers/silent-failure-hunter/seeds/s4-config-fallback/overlay/src/sync.mjs +49 -0
  116. package/calibration/reviewers/silent-failure-hunter/suite.json +1 -0
  117. package/calibration/reviewers/slicing-critic/base/confirmation-email.md +21 -0
  118. package/calibration/reviewers/slicing-critic/base/coverage.md +6 -0
  119. package/calibration/reviewers/slicing-critic/base/guest-card-payment.md +26 -0
  120. package/calibration/reviewers/slicing-critic/base/recap.json +14 -0
  121. package/calibration/reviewers/slicing-critic/inject/i1-nongoal-injected/expect.json +1 -0
  122. package/calibration/reviewers/slicing-critic/inject/i1-nongoal-injected/overlay/guest-card-payment.md +29 -0
  123. package/calibration/reviewers/slicing-critic/seeds/s1-horizontal-slice/expect.json +1 -0
  124. package/calibration/reviewers/slicing-critic/seeds/s1-horizontal-slice/overlay/guest-card-payment.md +23 -0
  125. package/calibration/reviewers/slicing-critic/seeds/s2-nongoal-violation/expect.json +1 -0
  126. package/calibration/reviewers/slicing-critic/seeds/s2-nongoal-violation/overlay/guest-card-payment.md +27 -0
  127. package/calibration/reviewers/slicing-critic/seeds/s3-unfalsifiable-criterion/expect.json +1 -0
  128. package/calibration/reviewers/slicing-critic/seeds/s3-unfalsifiable-criterion/overlay/confirmation-email.md +19 -0
  129. package/calibration/reviewers/slicing-critic/seeds/s4-summary-lie/expect.json +1 -0
  130. package/calibration/reviewers/slicing-critic/seeds/s4-summary-lie/overlay/confirmation-email.md +19 -0
  131. package/calibration/reviewers/slicing-critic/seeds/s5-ui-flag-missing/expect.json +1 -0
  132. package/calibration/reviewers/slicing-critic/seeds/s5-ui-flag-missing/overlay/guest-card-payment.md +28 -0
  133. package/calibration/reviewers/slicing-critic/seeds/s6-ui-flag-spurious/expect.json +1 -0
  134. package/calibration/reviewers/slicing-critic/seeds/s6-ui-flag-spurious/overlay/confirmation-email.md +22 -0
  135. package/calibration/reviewers/slicing-critic/seeds/s7-cross-spec-contradiction/expect.json +1 -0
  136. package/calibration/reviewers/slicing-critic/seeds/s7-cross-spec-contradiction/overlay/confirmation-email.md +24 -0
  137. package/calibration/reviewers/slicing-critic/suite.json +1 -0
  138. package/calibration/reviewers/type-design/base/src/order.ts +49 -0
  139. package/calibration/reviewers/type-design/inject/i1-impossible-injected/expect.json +1 -0
  140. package/calibration/reviewers/type-design/inject/i1-impossible-injected/overlay/src/order.ts +21 -0
  141. package/calibration/reviewers/type-design/seeds/s1-stringly/expect.json +1 -0
  142. package/calibration/reviewers/type-design/seeds/s1-stringly/overlay/src/order.ts +11 -0
  143. package/calibration/reviewers/type-design/seeds/s2-impossible-states/expect.json +1 -0
  144. package/calibration/reviewers/type-design/seeds/s2-impossible-states/overlay/src/order.ts +20 -0
  145. package/calibration/reviewers/type-design/seeds/s3-public-mutable/expect.json +1 -0
  146. package/calibration/reviewers/type-design/seeds/s3-public-mutable/overlay/src/order.ts +26 -0
  147. package/calibration/reviewers/type-design/seeds/s4-any-leak/expect.json +1 -0
  148. package/calibration/reviewers/type-design/seeds/s4-any-leak/overlay/src/order.ts +29 -0
  149. package/calibration/reviewers/type-design/suite.json +1 -0
  150. package/calibration/skills/decompose/seeds/s1-plain-feature/expect.json +1 -0
  151. package/calibration/skills/decompose/seeds/s1-plain-feature/recap.json +11 -0
  152. package/calibration/skills/decompose/seeds/s2-nongoal-bait/expect.json +1 -0
  153. package/calibration/skills/decompose/seeds/s2-nongoal-bait/recap.json +8 -0
  154. package/calibration/skills/decompose/seeds/s3-duplicated-fact/expect.json +1 -0
  155. package/calibration/skills/decompose/seeds/s3-duplicated-fact/recap.json +11 -0
  156. package/calibration/skills/decompose/seeds/s4-fix-routes-amend/canon/checkout-flow.md +19 -0
  157. package/calibration/skills/decompose/seeds/s4-fix-routes-amend/canon/payment-retry.md +20 -0
  158. package/calibration/skills/decompose/seeds/s4-fix-routes-amend/expect.json +1 -0
  159. package/calibration/skills/decompose/seeds/s4-fix-routes-amend/recap.json +8 -0
  160. package/calibration/skills/decompose/seeds/s5-chore-no-specs/canon/checkout-flow.md +19 -0
  161. package/calibration/skills/decompose/seeds/s5-chore-no-specs/expect.json +1 -0
  162. package/calibration/skills/decompose/seeds/s5-chore-no-specs/recap.json +8 -0
  163. package/calibration/skills/implement/seeds/s1-add-function/plan.body.md +5 -0
  164. package/calibration/skills/implement/seeds/s1-add-function/plan.meta.json +7 -0
  165. package/calibration/skills/implement/seeds/s1-add-function/recap.json +8 -0
  166. package/calibration/skills/implement/seeds/s1-add-function/repo/package.json +1 -0
  167. package/calibration/skills/implement/seeds/s1-add-function/repo/src/clamp.mjs +2 -0
  168. package/calibration/skills/implement/seeds/s1-add-function/spec.body.md +8 -0
  169. package/calibration/skills/implement/seeds/s1-add-function/spec.meta.json +8 -0
  170. package/calibration/skills/implement/seeds/s2-fix-bug/plan.body.md +6 -0
  171. package/calibration/skills/implement/seeds/s2-fix-bug/plan.meta.json +7 -0
  172. package/calibration/skills/implement/seeds/s2-fix-bug/recap.json +8 -0
  173. package/calibration/skills/implement/seeds/s2-fix-bug/repo/package.json +1 -0
  174. package/calibration/skills/implement/seeds/s2-fix-bug/repo/src/slug.mjs +7 -0
  175. package/calibration/skills/implement/seeds/s2-fix-bug/repo/tests/slug.test.mjs +7 -0
  176. package/calibration/skills/implement/seeds/s2-fix-bug/spec.body.md +9 -0
  177. package/calibration/skills/implement/seeds/s2-fix-bug/spec.meta.json +8 -0
  178. package/calibration/skills/implement/seeds/s3-two-steps/plan.body.md +11 -0
  179. package/calibration/skills/implement/seeds/s3-two-steps/plan.meta.json +10 -0
  180. package/calibration/skills/implement/seeds/s3-two-steps/recap.json +8 -0
  181. package/calibration/skills/implement/seeds/s3-two-steps/repo/package.json +1 -0
  182. package/calibration/skills/implement/seeds/s3-two-steps/repo/src/pad.mjs +2 -0
  183. package/calibration/skills/implement/seeds/s3-two-steps/spec.body.md +9 -0
  184. package/calibration/skills/implement/seeds/s3-two-steps/spec.meta.json +11 -0
  185. package/calibration/skills/plan/seeds/s1-two-criteria/expect.json +1 -0
  186. package/calibration/skills/plan/seeds/s1-two-criteria/parent.body.md +10 -0
  187. package/calibration/skills/plan/seeds/s1-two-criteria/parent.meta.json +11 -0
  188. package/calibration/skills/plan/seeds/s1-two-criteria/recap.json +8 -0
  189. package/calibration/skills/plan/seeds/s2-five-criteria/expect.json +1 -0
  190. package/calibration/skills/plan/seeds/s2-five-criteria/parent.body.md +11 -0
  191. package/calibration/skills/plan/seeds/s2-five-criteria/parent.meta.json +14 -0
  192. package/calibration/skills/plan/seeds/s2-five-criteria/recap.json +8 -0
  193. package/calibration/skills/plan/seeds/s3-mixed-needs/expect.json +1 -0
  194. package/calibration/skills/plan/seeds/s3-mixed-needs/parent.body.md +10 -0
  195. package/calibration/skills/plan/seeds/s3-mixed-needs/parent.meta.json +11 -0
  196. package/calibration/skills/plan/seeds/s3-mixed-needs/recap.json +8 -0
  197. package/calibration/skills/plan/seeds/s4-scaffolding-pull/expect.json +1 -0
  198. package/calibration/skills/plan/seeds/s4-scaffolding-pull/parent.body.md +9 -0
  199. package/calibration/skills/plan/seeds/s4-scaffolding-pull/parent.meta.json +10 -0
  200. package/calibration/skills/plan/seeds/s4-scaffolding-pull/recap.json +8 -0
  201. package/calibration.yaml +5 -0
  202. package/dist/abandon.js +154 -0
  203. package/dist/adopt.js +42 -0
  204. package/dist/allowlist.js +29 -0
  205. package/dist/bin.js +32 -0
  206. package/dist/calibrate.js +623 -0
  207. package/dist/cli.js +98 -0
  208. package/dist/config.js +155 -0
  209. package/dist/criteria.js +85 -0
  210. package/dist/design.js +104 -0
  211. package/dist/drift.js +280 -0
  212. package/dist/dsl.js +68 -0
  213. package/dist/evidence.js +221 -0
  214. package/dist/fm.js +51 -0
  215. package/dist/gate.js +371 -0
  216. package/dist/gates/decompose.js +90 -0
  217. package/dist/gates/design.js +84 -0
  218. package/dist/gates/implement.js +144 -0
  219. package/dist/gates/index.js +6 -0
  220. package/dist/gates/plan.js +86 -0
  221. package/dist/gates/ship.js +98 -0
  222. package/dist/gitio.js +97 -0
  223. package/dist/glob.js +32 -0
  224. package/dist/harness.js +229 -0
  225. package/dist/history.js +35 -0
  226. package/dist/install.js +133 -0
  227. package/dist/journal.js +43 -0
  228. package/dist/junit.js +54 -0
  229. package/dist/lock.js +37 -0
  230. package/dist/matcher.js +52 -0
  231. package/dist/model.js +73 -0
  232. package/dist/needs.js +34 -0
  233. package/dist/opener.js +38 -0
  234. package/dist/pin.js +28 -0
  235. package/dist/probe.js +14 -0
  236. package/dist/refusal.js +16 -0
  237. package/dist/reviewed.js +69 -0
  238. package/dist/reviewer.js +133 -0
  239. package/dist/rounds.js +112 -0
  240. package/dist/runner.js +73 -0
  241. package/dist/scan.js +67 -0
  242. package/dist/schemas.js +119 -0
  243. package/dist/sha.js +27 -0
  244. package/dist/ship.js +257 -0
  245. package/dist/stamp.js +104 -0
  246. package/dist/toon.js +16 -0
  247. package/dist/txn.js +70 -0
  248. package/dist/verbs/abandon.js +105 -0
  249. package/dist/verbs/adopt.js +118 -0
  250. package/dist/verbs/calibrate.js +130 -0
  251. package/dist/verbs/check.js +233 -0
  252. package/dist/verbs/clean.js +34 -0
  253. package/dist/verbs/dashboard.js +172 -0
  254. package/dist/verbs/decide.js +309 -0
  255. package/dist/verbs/design.js +203 -0
  256. package/dist/verbs/diff.js +60 -0
  257. package/dist/verbs/dispatch.js +102 -0
  258. package/dist/verbs/evidence.js +118 -0
  259. package/dist/verbs/gate.js +12 -0
  260. package/dist/verbs/index-verb.js +34 -0
  261. package/dist/verbs/init.js +190 -0
  262. package/dist/verbs/log.js +151 -0
  263. package/dist/verbs/next.js +437 -0
  264. package/dist/verbs/recap.js +145 -0
  265. package/dist/verbs/recover.js +52 -0
  266. package/dist/verbs/rename.js +99 -0
  267. package/dist/verbs/satisfy.js +69 -0
  268. package/dist/verbs/ship.js +11 -0
  269. package/dist/verbs/start.js +133 -0
  270. package/dist/verbs/sync.js +45 -0
  271. package/dist/verbs/write.js +349 -0
  272. package/dist/verdict.js +257 -0
  273. package/dist/worktree.js +73 -0
  274. package/package.json +70 -0
  275. package/plugin/.claude-plugin/plugin.json +19 -0
  276. package/plugin/commands/witness.md +46 -0
  277. package/plugin/hooks/canon-guard.mjs +105 -0
  278. package/plugin/hooks/guard-state.mjs +34 -0
  279. package/plugin/hooks/hooks.json +17 -0
  280. package/plugin/hooks/session-dashboard.sh +6 -0
  281. package/plugin/hooks/witness-pi.ts +67 -0
  282. package/plugin/skills/witness-brainstorm/SKILL.md +70 -0
  283. package/plugin/skills/witness-decompose/SKILL.md +99 -0
  284. package/plugin/skills/witness-design/SKILL.md +83 -0
  285. package/plugin/skills/witness-implement/SKILL.md +86 -0
  286. package/plugin/skills/witness-plan/SKILL.md +93 -0
  287. package/plugin/skills/witness-ship/SKILL.md +63 -0
  288. package/prompts/code-reviewer.md +64 -0
  289. package/prompts/design-critic.md +35 -0
  290. package/prompts/design-reviewer.md +70 -0
  291. package/prompts/drift-reviewer.md +40 -0
  292. package/prompts/plan-critic.md +37 -0
  293. package/prompts/pr-test.md +97 -0
  294. package/prompts/silent-failure-hunter.md +145 -0
  295. package/prompts/slicing-critic.md +46 -0
  296. package/prompts/type-design.md +113 -0
package/dist/needs.js ADDED
@@ -0,0 +1,34 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { ensureTrusted } from './allowlist.js';
3
+ export async function evaluateNeeds(root, ctx, needs) {
4
+ const out = [];
5
+ for (const raw of needs) {
6
+ const n = raw;
7
+ if (typeof n.env === 'string') {
8
+ const set = Boolean(ctx.env[n.env]);
9
+ out.push({ label: n.env, kind: 'env', status: set ? 'ok' : 'unmet', detail: set ? 'set' : 'env var unset' });
10
+ }
11
+ else if (typeof n.manual === 'string') {
12
+ const done = n.satisfied === true;
13
+ out.push({ label: n.manual, kind: 'manual', status: done ? 'ok' : 'unmet', detail: done ? 'satisfied' : 'flip via witness satisfy' });
14
+ }
15
+ else if (typeof n.cmd === 'string') {
16
+ const trust = await ensureTrusted(root, ctx, n.cmd);
17
+ if (trust !== 'trusted') {
18
+ out.push({
19
+ label: n.cmd, kind: 'cmd', status: trust,
20
+ detail: trust === 'blocked' ? 'untrusted in non-TTY — allow interactively or set WITNESS_TRUST_CMDS=1' : 'trust declined',
21
+ });
22
+ continue;
23
+ }
24
+ try {
25
+ execSync(n.cmd, { cwd: root, stdio: 'ignore' });
26
+ out.push({ label: n.cmd, kind: 'cmd', status: 'ok', detail: 'exit 0' });
27
+ }
28
+ catch {
29
+ out.push({ label: n.cmd, kind: 'cmd', status: 'unmet', detail: 'nonzero exit' });
30
+ }
31
+ }
32
+ }
33
+ return out;
34
+ }
package/dist/opener.js ADDED
@@ -0,0 +1,38 @@
1
+ import { spawnSync, spawn } from 'node:child_process';
2
+ // Platform default openers. `start` is a cmd builtin, hence the shell wrapper.
3
+ function platformOpener() {
4
+ if (process.platform === 'darwin')
5
+ return ['open'];
6
+ if (process.platform === 'win32')
7
+ return ['cmd', '/c', 'start', ''];
8
+ return ['xdg-open'];
9
+ }
10
+ // win32 has no `sh`, so a POSIX probe there would report every opener missing and
11
+ // make approve permanently unreachable. `where` is the cmd equivalent.
12
+ function resolves(cmd) {
13
+ const probe = process.platform === 'win32'
14
+ ? spawnSync('where', [cmd], { encoding: 'utf8' })
15
+ : spawnSync('sh', ['-c', `command -v ${cmd}`], { encoding: 'utf8' });
16
+ return probe.status === 0;
17
+ }
18
+ // Show a file to the human. Returns the resolved command alongside the outcome so the
19
+ // caller can journal WHAT was run — a degenerate opener (`WITNESS_OPENER=/usr/bin/true`)
20
+ // then reads as a degenerate opener in the record instead of as a human looking at a
21
+ // screen. `WITNESS_OPENER` overrides the binary: test seam, and the real escape hatch
22
+ // for nonstandard desktops, so it accepts a bare name off PATH as well as a path.
23
+ export function openArtifact(env, absPath) {
24
+ const argv = env.WITNESS_OPENER ? [env.WITNESS_OPENER] : platformOpener();
25
+ const [cmd, ...pre] = argv;
26
+ // Probe before spawning: a detached spawn reports ENOENT asynchronously, and the
27
+ // caller needs the outcome synchronously to journal it in the same transaction.
28
+ if (!resolves(cmd))
29
+ return { outcome: 'failed', command: cmd };
30
+ try {
31
+ const child = spawn(cmd, [...pre, absPath], { detached: true, stdio: 'ignore' });
32
+ child.unref();
33
+ return { outcome: 'spawned', command: cmd };
34
+ }
35
+ catch {
36
+ return { outcome: 'failed', command: cmd };
37
+ }
38
+ }
package/dist/pin.js ADDED
@@ -0,0 +1,28 @@
1
+ import { ok, refuse, v } from './refusal.js';
2
+ // Pi's native --thinking vocabulary. One grammar for every harness: pi renders it
3
+ // natively; claude-code maps non-off levels through CLAUDE_THINKING_BUDGET.
4
+ export const THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
5
+ // claude has no --thinking flag; non-off levels render as the documented
6
+ // MAX_THINKING_TOKENS env var. Budgets are pinned constants: the raw pin string is in
7
+ // the verdict-cache key, so a level change re-rolls verdicts; a budget-table change
8
+ // ships as a new witness version, which is also in the key.
9
+ export const CLAUDE_THINKING_BUDGET = {
10
+ minimal: 1024, low: 4096, medium: 8192, high: 16384, xhigh: 32768, max: 63999,
11
+ };
12
+ // Grammar: [provider/]model[:thinking]. First '/' splits provider; last ':' after the
13
+ // slash splits thinking — model ids themselves never contain ':' in any catalog we ship.
14
+ export function parsePin(field, raw) {
15
+ const slash = raw.indexOf('/');
16
+ const provider = slash > 0 ? raw.slice(0, slash) : undefined;
17
+ const rest = slash >= 0 ? raw.slice(slash + 1) : raw;
18
+ const colon = rest.lastIndexOf(':');
19
+ const model = colon >= 0 ? rest.slice(0, colon) : rest;
20
+ const level = colon >= 0 ? rest.slice(colon + 1) : 'off';
21
+ if (model === '' || (slash >= 0 && provider === undefined)) {
22
+ return refuse([v(field, 'pin-malformed', raw, '[provider/]model[:thinking]')]);
23
+ }
24
+ if (!THINKING_LEVELS.includes(level)) {
25
+ return refuse([v(field, 'unknown-thinking-level', level, THINKING_LEVELS.join(' | '))]);
26
+ }
27
+ return ok({ provider, model, thinking: level });
28
+ }
package/dist/probe.js ADDED
@@ -0,0 +1,14 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ // Revision 3. The old form passed no `env`, so it silently answered about process.env
3
+ // while reviewer.ts:113 spawns `claude` with `env: ctx.env` — two different questions,
4
+ // one answer. It also made the finding untestable: helpers.ts runs main() in-process,
5
+ // so a test can control ctx.env and nothing else.
6
+ export function probe(cmd, args, env) {
7
+ try {
8
+ execFileSync(cmd, args, { stdio: 'ignore', env: env });
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
@@ -0,0 +1,16 @@
1
+ import { rows } from './toon.js';
2
+ export function ok(value) {
3
+ return { ok: true, value };
4
+ }
5
+ export function refuse(violations) {
6
+ return { ok: false, violations };
7
+ }
8
+ export function v(field, rule, got, want) {
9
+ return { field, rule, got, want };
10
+ }
11
+ export function renderRefusal(violations) {
12
+ return [
13
+ ...rows('refused', ['field', 'rule', 'got', 'want'], violations),
14
+ 'help: fix each row and re-run — rows are structured for self-repair',
15
+ ];
16
+ }
@@ -0,0 +1,69 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { mkdtempSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { canonicalJson, canonicalSha } from './sha.js';
7
+ import { effortAbandoned, effortStreams, latestRecap, readStream } from './journal.js';
8
+ import { findById } from './scan.js';
9
+ export function worktreeTreeSha(root) {
10
+ const tmp = mkdtempSync(join(tmpdir(), 'witness-idx-'));
11
+ const env = { ...process.env, GIT_INDEX_FILE: join(tmp, 'index') };
12
+ try {
13
+ execFileSync('git', ['add', '-A'], { cwd: root, env });
14
+ return execFileSync('git', ['write-tree'], { cwd: root, env, encoding: 'utf8' }).trim();
15
+ }
16
+ finally {
17
+ rmSync(tmp, { recursive: true, force: true });
18
+ }
19
+ }
20
+ export function effortWrites(root, effort) {
21
+ const out = new Map();
22
+ for (const e of readStream(root, effort)) {
23
+ if (e.t !== 'write')
24
+ continue;
25
+ const w = e;
26
+ out.set(w.artifact, { sha: w.sha, covers: w.covers, created: w.created });
27
+ }
28
+ return out;
29
+ }
30
+ export function effortSpecs(root, canon, effort) {
31
+ const docs = [];
32
+ for (const id of effortWrites(root, effort).keys()) {
33
+ const doc = findById(canon, id);
34
+ if (doc && doc.meta.type === 'spec')
35
+ docs.push(doc);
36
+ }
37
+ return docs.sort((a, b) => String(a.meta.id).localeCompare(String(b.meta.id)));
38
+ }
39
+ export function effortOf(root, artifactId) {
40
+ const hits = [];
41
+ for (const effort of effortStreams(root)) {
42
+ const entries = readStream(root, effort);
43
+ if (entries.some((e) => e.t === 'write' && e.artifact === artifactId)) {
44
+ hits.push({ effort, abandoned: effortAbandoned(entries) });
45
+ }
46
+ }
47
+ hits.sort((a, b) => a.abandoned === b.abandoned ? a.effort.localeCompare(b.effort) : a.abandoned ? 1 : -1);
48
+ return hits[0]?.effort;
49
+ }
50
+ function sha256(parts) {
51
+ const h = createHash('sha256');
52
+ for (const p of parts)
53
+ h.update(p);
54
+ return h.digest('hex');
55
+ }
56
+ export function effortReviewedSha(root, canon, effort) {
57
+ const recap = latestRecap(root, effort);
58
+ const docs = effortSpecs(root, canon, effort);
59
+ const lines = docs
60
+ .map((d) => `${String(d.meta.id)}:${canonicalSha(d.meta, d.body)}`)
61
+ .sort();
62
+ return { sha: sha256([canonicalJson(recap ?? {}), '\n', lines.join('\n')]), docs };
63
+ }
64
+ export function planPairSha(plan, parent) {
65
+ return sha256([
66
+ `${String(plan.meta.id)}:${canonicalSha(plan.meta, plan.body)}\n`,
67
+ `${String(parent.meta.id)}:${canonicalSha(parent.meta, parent.body)}`,
68
+ ]);
69
+ }
@@ -0,0 +1,133 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { validatePin } from './harness.js';
7
+ import { ok, refuse, v } from './refusal.js';
8
+ export const PROMPT_NAMES = [
9
+ 'slicing-critic', 'plan-critic', 'code-reviewer', 'silent-failure-hunter',
10
+ 'type-design', 'pr-test', 'drift-reviewer', 'design-critic', 'design-reviewer',
11
+ ];
12
+ export function promptsDir() {
13
+ return join(dirname(fileURLToPath(import.meta.url)), '..', 'prompts');
14
+ }
15
+ export function resolvePrompt(name) {
16
+ const path = join(promptsDir(), `${name}.md`);
17
+ if (!existsSync(path)) {
18
+ return refuse([v('reviewers', 'unknown-reviewer', name, `a lens file at prompts/${name}.md (shipped: ${PROMPT_NAMES.join(' ')})`)]);
19
+ }
20
+ return ok({ name, contents: readFileSync(path, 'utf8') });
21
+ }
22
+ // docs.<key> → which lenses at which gates consume it. The design-stage plan
23
+ // extends this map (design → design-critic / design-reviewer); keys here must
24
+ // stay in lockstep with DOC_KEYS in config.ts — no key without a consumer.
25
+ const DOC_CONSUMERS = {
26
+ conventions: { lenses: ['code-reviewer'], gates: ['implement', 'ship'] },
27
+ design: { lenses: ['design-critic', 'design-reviewer'], gates: ['design', 'implement'] },
28
+ };
29
+ export function docKeysFor(gate, lens) {
30
+ return Object.entries(DOC_CONSUMERS)
31
+ .filter(([, c]) => c.lenses.includes(lens) && c.gates.includes(gate))
32
+ .map(([k]) => k);
33
+ }
34
+ export function loadLensDocs(root, paths) {
35
+ const docs = [];
36
+ const violations = [];
37
+ for (const p of paths) {
38
+ const abs = join(root, p);
39
+ if (!existsSync(abs)) {
40
+ violations.push(v('docs', 'doc-missing', p, 'an existing file — gates inject configured docs fail-closed; fix the path or restore the file'));
41
+ continue;
42
+ }
43
+ docs.push({ path: p, contents: readFileSync(abs, 'utf8') });
44
+ }
45
+ return violations.length ? refuse(violations) : ok(docs);
46
+ }
47
+ export function docsBlock(docs) {
48
+ if (docs.length === 0)
49
+ return '';
50
+ return `## Repo conventions (injected from docs config)\n\n${docs.map((d) => `### ${d.path}\n\n${d.contents}`).join('\n\n')}\n\n`;
51
+ }
52
+ // Row 83: settled content policies, injected into every battery for the plan. The
53
+ // contradiction channel is reviewer self-report — deterministic semantic matching of a
54
+ // freeform finding against a pin is not honestly implementable, so the schema carries it.
55
+ export function pinsBlock(pins) {
56
+ if (pins.length === 0)
57
+ return '';
58
+ return `## Settled policy pins (human decisions — do not re-litigate)\n\n` +
59
+ `The human has pinned these content policies for this plan. They are settled: a finding that merely disagrees with a pin is not a finding. If a defect you must report can only be fixed by violating a pin, report it AND set \`"contradicts_pin": <pin number>\` on that finding — the gate escalates the conflict to the human. Where pins conflict, the later pin wins.\n\n` +
60
+ pins.map((p) => `${p.ordinal}. ${p.text}`).join('\n') + '\n\n';
61
+ }
62
+ export function promptsSha(lenses, extra) {
63
+ const h = createHash('sha256');
64
+ for (const l of [...lenses].sort((a, b) => a.name.localeCompare(b.name))) {
65
+ h.update(`lens ${l.name} ${l.contents.length}\n`);
66
+ h.update(l.contents);
67
+ h.update('\n');
68
+ // injected docs are part of what the reviewer read — an edited doc is a
69
+ // different reviewer, exactly like an edited lens (verdict-cache key)
70
+ for (const d of l.docs ?? []) {
71
+ h.update(`doc ${d.path} ${d.contents.length}\n`);
72
+ h.update(d.contents);
73
+ h.update('\n');
74
+ }
75
+ }
76
+ // policy pins are part of what every reviewer read — a changed pin-set re-arms review
77
+ if (extra !== undefined && extra !== '') {
78
+ h.update(`pins ${extra.length}\n`);
79
+ h.update(extra);
80
+ h.update('\n');
81
+ }
82
+ return h.digest('hex');
83
+ }
84
+ const REVIEWER_TIMEOUT_MS = 600_000;
85
+ // One stalled call must not cost a whole run: a full calibration battery is ~200
86
+ // invocations over hours, and treating a transient stall as fatal throws away every
87
+ // sample already paid for. Retries are bounded so a genuinely wedged binary still ends.
88
+ const TIMEOUT_RETRIES = 2;
89
+ export function invokeReviewer(ctx, harness, opts) {
90
+ let pin;
91
+ if (opts.model !== undefined) {
92
+ const pinR = validatePin(harness, 'gates.model', opts.model);
93
+ if (!pinR.ok)
94
+ return refuse(pinR.violations);
95
+ pin = pinR.value;
96
+ }
97
+ const { cmd, args, env } = harness.reviewer.spawn(pin);
98
+ const timeout = Number(ctx.env.WITNESS_REVIEWER_TIMEOUT_MS) || REVIEWER_TIMEOUT_MS;
99
+ for (let attempt = 0;; attempt += 1) {
100
+ const r = spawnSync(cmd, args, {
101
+ cwd: opts.cwd,
102
+ env: { ...ctx.env, ...env },
103
+ input: opts.prompt,
104
+ encoding: 'utf8',
105
+ timeout,
106
+ maxBuffer: 64 * 1024 * 1024,
107
+ });
108
+ if (r.error) {
109
+ // A stall is NOT the missing-binary case. Reporting it as one sent the last
110
+ // reader hunting a PATH problem on a machine where the reviewer answered in 6s.
111
+ if (r.error.code === 'ETIMEDOUT') {
112
+ if (attempt < TIMEOUT_RETRIES)
113
+ continue;
114
+ return refuse([v(cmd, 'reviewer-timeout', `no response in ${timeout}ms after ${attempt + 1} attempts`, 'a reviewer that answers within the timeout — raise WITNESS_REVIEWER_TIMEOUT_MS if the model is simply slow')]);
115
+ }
116
+ return refuse([v(cmd, 'reviewer-invocation', String(r.error.message), `a runnable ${cmd} binary on PATH — gates invoke reviewers headlessly; witness check probes this`)]);
117
+ }
118
+ if (r.status !== 0) {
119
+ return refuse([v(cmd, 'reviewer-invocation', `exit ${String(r.status)}: ${(r.stderr ?? '').slice(0, 200)}`, `${cmd} print mode exiting 0`)]);
120
+ }
121
+ return harness.reviewer.parseEnvelope(r.stdout);
122
+ }
123
+ }
124
+ export function parseVerdictText(text) {
125
+ const fenced = /```(?:json)?\s*\n([\s\S]*?)\n\s*```/.exec(text);
126
+ const body = (fenced ? fenced[1] ?? '' : text).trim();
127
+ try {
128
+ return ok(JSON.parse(body));
129
+ }
130
+ catch {
131
+ return refuse([v('verdict', 'verdict-unparseable', body.slice(0, 120), 'a JSON object {coverage, findings} and nothing else')]);
132
+ }
133
+ }
package/dist/rounds.js ADDED
@@ -0,0 +1,112 @@
1
+ const isRun = (e, gate) => e !== undefined && e.t === 'gate-run' && e.gate === gate;
2
+ const isDecision = (e, gate) => e !== undefined && e.t === 'human-decision' && e.gate === gate;
3
+ export function keyOf(run) {
4
+ const { reviewed_sha, gate, prompts_sha, model, witness } = run;
5
+ return { reviewed_sha, gate, prompts_sha, model, witness, harness: run.harness ?? 'claude-code' };
6
+ }
7
+ export function sameKey(a, b) {
8
+ return a.reviewed_sha === b.reviewed_sha && a.gate === b.gate &&
9
+ a.prompts_sha === b.prompts_sha && a.model === b.model && a.witness === b.witness &&
10
+ a.harness === b.harness;
11
+ }
12
+ export function gateRuns(entries, gate) {
13
+ return entries.filter((e) => isRun(e, gate));
14
+ }
15
+ export function lastGateRun(entries, gate) {
16
+ for (let i = entries.length - 1; i >= 0; i--) {
17
+ if (isRun(entries[i], gate))
18
+ return entries[i];
19
+ }
20
+ return undefined;
21
+ }
22
+ export const ROUND_BOUND = 3;
23
+ // a "reset" opens a fresh round budget: human approve, a passed run, or a
24
+ // revise-upstream (the plan itself changes — a new plan version is a new game;
25
+ // upstream churn is bounded by the upstream gate's own bound)
26
+ function lastResetIndex(entries, gate) {
27
+ for (let i = entries.length - 1; i >= 0; i--) {
28
+ const e = entries[i];
29
+ if (isDecision(e, gate)) {
30
+ const d = e.decision;
31
+ if (d === 'approve' || d === 'revise-upstream')
32
+ return i;
33
+ }
34
+ if (isRun(e, gate) && e.outcome === 'passed')
35
+ return i;
36
+ }
37
+ return -1;
38
+ }
39
+ export function roundsSinceApprove(entries, gate) {
40
+ const since = lastResetIndex(entries, gate);
41
+ let n = 0;
42
+ for (let i = since + 1; i < entries.length; i++) {
43
+ const e = entries[i];
44
+ // malformed = the battery failed to emit a legal verdict — witness's
45
+ // failure, not the artifact's; it never spends the human's budget
46
+ if (isRun(e, gate) && e.outcome !== 'malformed')
47
+ n++;
48
+ }
49
+ return n;
50
+ }
51
+ export function boundReached(entries, gate) {
52
+ return roundsSinceApprove(entries, gate) >= ROUND_BOUND;
53
+ }
54
+ export function pendingDecision(entries, gate) {
55
+ for (let i = entries.length - 1; i >= 0; i--) {
56
+ const e = entries[i];
57
+ if (isDecision(e, gate))
58
+ return undefined;
59
+ if (isRun(e, gate)) {
60
+ const run = e;
61
+ return run.outcome === 'passed' ? undefined : run;
62
+ }
63
+ }
64
+ return undefined;
65
+ }
66
+ // A decision carrying `caused_by` is a REOPEN — another gate's `--revise --upstream`
67
+ // instructing this stage to re-author. It is not a disposition of the run above it and
68
+ // can never settle it. Discharged by a later gate-run OR a later human-decision for the
69
+ // same gate: `--approve` is a legitimate answer to a reopen ("looked, the canon was
70
+ // right"), and requiring a run would strand the human at `changed-nothing`, which
71
+ // appends nothing and would loop forever (D67's livelock shape).
72
+ export function openReopen(entries, gate) {
73
+ for (let i = entries.length - 1; i >= 0; i--) {
74
+ const e = entries[i];
75
+ if (isRun(e, gate))
76
+ return undefined;
77
+ if (isDecision(e, gate)) {
78
+ const d = e;
79
+ if (d.caused_by !== undefined)
80
+ return d;
81
+ return undefined;
82
+ }
83
+ }
84
+ return undefined;
85
+ }
86
+ export function appendKind(entries, gate, key) {
87
+ let lastRunIdx = -1;
88
+ for (let i = entries.length - 1; i >= 0; i--) {
89
+ if (isRun(entries[i], gate)) {
90
+ lastRunIdx = i;
91
+ break;
92
+ }
93
+ }
94
+ if (lastRunIdx >= 0) {
95
+ const last = entries[lastRunIdx];
96
+ if (sameKey(keyOf(last), key)) {
97
+ const revised = entries.slice(lastRunIdx + 1).some((e) => isDecision(e, gate) &&
98
+ ['revise', 'revise-upstream'].includes(e.decision));
99
+ return revised ? { kind: 'changed-nothing', entry: last } : { kind: 'resume', entry: last };
100
+ }
101
+ }
102
+ for (let i = lastRunIdx - 1; i >= 0; i--) {
103
+ const e = entries[i];
104
+ if (!isRun(e, gate))
105
+ continue;
106
+ const run = e;
107
+ if (run.outcome !== 'malformed' && run.verdicts && sameKey(keyOf(run), key)) {
108
+ return { kind: 'cached', from: run };
109
+ }
110
+ }
111
+ return { kind: 'fresh' };
112
+ }
package/dist/runner.js ADDED
@@ -0,0 +1,73 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { ensureTrusted } from './allowlist.js';
5
+ import { mergeReports, reportFiles } from './junit.js';
6
+ import { ok, refuse, v } from './refusal.js';
7
+ export function runnerConfig(cfg) {
8
+ const crit = (cfg.raw.criteria ?? {});
9
+ const runner = crit.runner;
10
+ if (typeof runner !== 'string' || runner.length === 0) {
11
+ return refuse([v('criteria.runner', 'required', String(runner ?? 'absent'), 'a filtered template containing {id}, or full-suite')]);
12
+ }
13
+ if (runner === 'full-suite') {
14
+ const report = crit.report;
15
+ if (typeof report !== 'string') {
16
+ return refuse([v('criteria.report', 'required', 'absent', 'junit:<glob> — full-suite mode reads merged junit reports')]);
17
+ }
18
+ if (!report.startsWith('junit:')) {
19
+ return refuse([v('criteria.report', 'report-format', report, 'junit:<glob> (only junit is supported)')]);
20
+ }
21
+ const ship = (cfg.raw.ship ?? {});
22
+ if (typeof ship.test !== 'string' || ship.test.length === 0) {
23
+ return refuse([v('ship.test', 'required', 'absent', 'the suite command full-suite mode executes')]);
24
+ }
25
+ return ok({ mode: 'full-suite', reportGlob: report.slice('junit:'.length), suiteCmd: ship.test });
26
+ }
27
+ if (!runner.includes('{id}')) {
28
+ return refuse([v('criteria.runner', 'no-id-placeholder', runner, 'template must interpolate {id} (or use full-suite)')]);
29
+ }
30
+ const report = crit.report;
31
+ if (report !== undefined) {
32
+ if (typeof report !== 'string' || !report.startsWith('junit:')) {
33
+ return refuse([v('criteria.report', 'report-format', String(report), 'junit:<glob> (only junit is supported)')]);
34
+ }
35
+ return ok({ mode: 'filtered', template: runner, reportGlob: report.slice('junit:'.length) });
36
+ }
37
+ return ok({ mode: 'filtered', template: runner });
38
+ }
39
+ export function criteriaExcludes(cfg) {
40
+ const crit = (cfg.raw.criteria ?? {});
41
+ const extra = Array.isArray(crit.exclude) ? crit.exclude.filter((g) => typeof g === 'string') : [];
42
+ return [`${cfg.paths.specs}/**`, `${cfg.paths.plans}/**`, '.witness/**', ...extra];
43
+ }
44
+ export function execCommand(runRoot, ctx, cmd) {
45
+ try {
46
+ const out = execSync(cmd, { cwd: runRoot, env: ctx.env, stdio: 'pipe' });
47
+ return { exitZero: true, output: String(out).slice(-4000) };
48
+ }
49
+ catch (e) {
50
+ const err = e;
51
+ return { exitZero: false, output: `${String(err.stdout ?? '')}${String(err.stderr ?? '')}`.slice(-4000) };
52
+ }
53
+ }
54
+ export async function runFiltered(runRoot, ctx, template, specId, trustRoot = runRoot) {
55
+ const trust = await ensureTrusted(trustRoot, ctx, template);
56
+ if (trust !== 'trusted') {
57
+ return refuse([v('criteria.runner', `untrusted-${trust}`, template, trust === 'blocked' ? 'allow interactively or set WITNESS_TRUST_CMDS=1' : 'trust was declined')]);
58
+ }
59
+ return ok(execCommand(runRoot, ctx, template.replaceAll('{id}', specId)));
60
+ }
61
+ export async function runFullSuite(runRoot, ctx, rc, trustRoot = runRoot) {
62
+ const trust = await ensureTrusted(trustRoot, ctx, rc.suiteCmd);
63
+ if (trust !== 'trusted') {
64
+ return refuse([v('ship.test', `untrusted-${trust}`, rc.suiteCmd, trust === 'blocked' ? 'allow interactively or set WITNESS_TRUST_CMDS=1' : 'trust was declined')]);
65
+ }
66
+ for (const f of reportFiles(runRoot, rc.reportGlob))
67
+ rmSync(join(runRoot, f));
68
+ const run = execCommand(runRoot, ctx, rc.suiteCmd);
69
+ const merged = mergeReports(runRoot, rc.reportGlob);
70
+ if (!merged.ok)
71
+ return merged;
72
+ return ok({ run, tests: merged.value });
73
+ }
package/dist/scan.js ADDED
@@ -0,0 +1,67 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { canonPaths } from './config.js';
4
+ import { readDoc } from './fm.js';
5
+ import { validateDoc } from './schemas.js';
6
+ export function loadCanon(root) {
7
+ const docs = [];
8
+ const errors = [];
9
+ const paths = canonPaths(root);
10
+ for (const dir of [paths.specs, paths.plans]) {
11
+ const base = join(root, dir);
12
+ if (!existsSync(base))
13
+ continue;
14
+ for (const f of readdirSync(base, { recursive: true, encoding: 'utf8' })) {
15
+ if (!f.endsWith('.md'))
16
+ continue;
17
+ const rel = `${dir}/${f}`;
18
+ const parsed = readDoc(join(root, rel));
19
+ if (!parsed.ok) {
20
+ errors.push(...parsed.violations.map((x) => ({ ...x, field: `${rel}: ${x.field}` })));
21
+ continue;
22
+ }
23
+ docs.push({
24
+ rel,
25
+ meta: parsed.value.meta,
26
+ body: parsed.value.body,
27
+ violations: validateDoc(parsed.value.meta, parsed.value.body),
28
+ });
29
+ }
30
+ }
31
+ return { docs, errors, paths };
32
+ }
33
+ export function findById(canon, id) {
34
+ return canon.docs.find((d) => d.meta.id === id);
35
+ }
36
+ export function findCycle(canon, extra) {
37
+ const deps = new Map();
38
+ for (const d of canon.docs) {
39
+ deps.set(String(d.meta.id), Array.isArray(d.meta.depends) ? d.meta.depends : []);
40
+ }
41
+ if (extra)
42
+ deps.set(extra.id, extra.depends);
43
+ const state = new Map();
44
+ const stack = [];
45
+ const visit = (n) => {
46
+ if (state.get(n) === 2)
47
+ return undefined;
48
+ if (state.get(n) === 1)
49
+ return [...stack.slice(stack.indexOf(n)), n];
50
+ state.set(n, 1);
51
+ stack.push(n);
52
+ for (const d of deps.get(n) ?? []) {
53
+ const cycle = visit(d);
54
+ if (cycle)
55
+ return cycle;
56
+ }
57
+ stack.pop();
58
+ state.set(n, 2);
59
+ return undefined;
60
+ };
61
+ for (const n of deps.keys()) {
62
+ const cycle = visit(n);
63
+ if (cycle)
64
+ return cycle;
65
+ }
66
+ return undefined;
67
+ }