@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/cli.js ADDED
@@ -0,0 +1,98 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { configPath } from './config.js';
4
+ import { primaryRoot } from './gitio.js';
5
+ export const EXIT = { OK: 0, FINDINGS: 1, REFUSED: 2, BLOCKED: 3 };
6
+ const VERBS = {
7
+ recover: () => import('./verbs/recover.js'),
8
+ init: () => import('./verbs/init.js'),
9
+ recap: () => import('./verbs/recap.js'),
10
+ write: () => import('./verbs/write.js'),
11
+ diff: () => import('./verbs/diff.js'),
12
+ check: () => import('./verbs/check.js'),
13
+ index: () => import('./verbs/index-verb.js'),
14
+ satisfy: () => import('./verbs/satisfy.js'),
15
+ log: () => import('./verbs/log.js'),
16
+ 'test-evidence': () => import('./verbs/evidence.js'),
17
+ 'verify-red': () => import('./verbs/evidence.js').then((m) => ({ run: m.runVerifyRed })),
18
+ 'dispatch-report': () => import('./verbs/dispatch.js'),
19
+ adopt: () => import('./verbs/adopt.js'),
20
+ gate: () => import('./verbs/gate.js'),
21
+ decide: () => import('./verbs/decide.js'),
22
+ design: () => import('./verbs/design.js'),
23
+ start: () => import('./verbs/start.js'),
24
+ clean: () => import('./verbs/clean.js'),
25
+ ship: () => import('./verbs/ship.js'),
26
+ next: () => import('./verbs/next.js'),
27
+ abandon: () => import('./verbs/abandon.js'),
28
+ rename: () => import('./verbs/rename.js'),
29
+ sync: () => import('./verbs/sync.js'),
30
+ calibrate: () => import('./verbs/calibrate.js'),
31
+ };
32
+ // answered centrally so no verb can crash on --help (several parse argv
33
+ // strictly); each line mirrors the verb's own usage refusal where one exists
34
+ const VERB_USAGE = {
35
+ abandon: 'witness abandon <plan-id | effort-slug>',
36
+ adopt: 'witness adopt <specs/... | plans/...>.md',
37
+ calibrate: 'witness calibrate <exact-model-id> [--suite all|reviewers|skills] [--only <name>] [--samples <n>] [--publish]',
38
+ check: 'witness check',
39
+ clean: 'witness clean',
40
+ decide: 'witness decide <gate> <target> --approve|--revise|--stop [--override] [--note <t>] [--upstream <artifact|effort>] [--pin <policy>]… [--show]',
41
+ design: 'witness design <spec-id> --file <html> | --reconfirm | --open',
42
+ diff: 'witness diff <spec-id>',
43
+ 'dispatch-report': 'witness dispatch-report <plan-id> --steps-assigned <n> --steps-completed <n> [--tokens <n>] [--tool-uses <n>] [--duration-ms <n>]',
44
+ gate: 'witness gate <decompose|plan|implement|ship|design> <target> [--fresh] [--manual]',
45
+ index: 'witness index',
46
+ init: 'witness init [--agent claude-code|pi|auto]',
47
+ log: 'witness log <stream> [--all] [--lineage]',
48
+ next: 'witness next',
49
+ recap: 'witness recap --file <recap.json> [--amend]',
50
+ recover: 'witness recover [--complete | --rollback]',
51
+ rename: 'witness rename <old-id> <new-id>',
52
+ satisfy: 'witness satisfy <doc-id> --need <text | index>',
53
+ ship: 'witness ship <plan-id>',
54
+ start: 'witness start <plan-id>',
55
+ sync: 'witness sync',
56
+ 'test-evidence': 'witness test-evidence <plan-id> --phase red|green',
57
+ 'verify-red': 'witness verify-red <plan-id> [--base <ref>]',
58
+ };
59
+ export function version() {
60
+ const pkg = JSON.parse(readFileSync(join(new URL('.', import.meta.url).pathname, '..', 'package.json'), 'utf8'));
61
+ return pkg.version;
62
+ }
63
+ export function usage() {
64
+ return [
65
+ 'witness — spec-led pipeline: specs are state, plans are motion',
66
+ 'usage: witness <verb> [flags]',
67
+ `verbs: ${Object.keys(VERBS).sort().join(' · ') || '(landing slice by slice)'}`,
68
+ 'help: witness <verb> --help',
69
+ ].join('\n');
70
+ }
71
+ export async function main(ctx, argv) {
72
+ const [verb, ...rest] = argv;
73
+ if (verb === '--version' || verb === '-v') {
74
+ ctx.out(version());
75
+ return EXIT.OK;
76
+ }
77
+ if (!verb || verb === 'help' || verb === '--help') {
78
+ if (!verb) {
79
+ const rootRes = primaryRoot(ctx.cwd);
80
+ if (rootRes.ok && existsSync(configPath(rootRes.value))) {
81
+ return (await import('./verbs/dashboard.js')).run(ctx, []);
82
+ }
83
+ }
84
+ ctx.out(usage());
85
+ return EXIT.OK;
86
+ }
87
+ const load = VERBS[verb];
88
+ if (load && (rest.includes('--help') || rest.includes('-h'))) {
89
+ ctx.out(`usage: ${VERB_USAGE[verb] ?? `witness ${verb} [flags]`}`);
90
+ return EXIT.OK;
91
+ }
92
+ if (!load) {
93
+ ctx.err(`unknown verb: ${verb}`);
94
+ ctx.err(usage());
95
+ return EXIT.REFUSED;
96
+ }
97
+ return (await load()).run(ctx, rest);
98
+ }
package/dist/config.js ADDED
@@ -0,0 +1,155 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { isAbsolute, join } from 'node:path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import { ok, refuse, v } from './refusal.js';
5
+ export const SCHEMA_VERSION = 1;
6
+ export const DEFAULT_PATHS = { specs: 'specs', plans: 'plans', designs: 'designs' };
7
+ export const DOC_KEYS = ['conventions', 'design'];
8
+ export function resolvePaths(raw) {
9
+ const conf = (raw.paths ?? {});
10
+ const violations = [];
11
+ const pick = (key) => {
12
+ const val = conf[key];
13
+ if (val === undefined)
14
+ return DEFAULT_PATHS[key];
15
+ if (typeof val !== 'string' || val === '') {
16
+ violations.push(v(`paths.${key}`, 'invalid', String(val), 'a repo-relative directory'));
17
+ return DEFAULT_PATHS[key];
18
+ }
19
+ const norm = val.replace(/\/+$/, '');
20
+ if (isAbsolute(norm) || norm.split('/').some((seg) => seg === '' || seg === '.' || seg === '..')) {
21
+ violations.push(v(`paths.${key}`, 'invalid', norm, 'a repo-relative directory without . or .. segments'));
22
+ return DEFAULT_PATHS[key];
23
+ }
24
+ if (norm === '.witness' || norm.startsWith('.witness/')) {
25
+ violations.push(v(`paths.${key}`, 'reserved', norm, 'a directory outside .witness/'));
26
+ return DEFAULT_PATHS[key];
27
+ }
28
+ return norm;
29
+ };
30
+ const specs = pick('specs');
31
+ const plans = pick('plans');
32
+ const designs = pick('designs');
33
+ if (violations.length === 0) {
34
+ const roots = [['specs', specs], ['plans', plans], ['designs', designs]];
35
+ for (let i = 0; i < roots.length; i++) {
36
+ for (let j = i + 1; j < roots.length; j++) {
37
+ const [ka, a] = roots[i];
38
+ const [kb, b] = roots[j];
39
+ if (a === b) {
40
+ violations.push(v('paths', 'overlap', `${ka}=${kb}=${a}`, 'distinct specs, plans and designs directories'));
41
+ }
42
+ else if (a.startsWith(`${b}/`) || b.startsWith(`${a}/`)) {
43
+ violations.push(v('paths', 'nested', `${a} vs ${b}`, 'directories that do not contain each other (scans are recursive)'));
44
+ }
45
+ }
46
+ }
47
+ }
48
+ return violations.length ? refuse(violations) : ok({ specs, plans, designs });
49
+ }
50
+ export function resolveDocs(raw) {
51
+ const conf = raw.docs;
52
+ if (conf === undefined)
53
+ return ok({});
54
+ if (typeof conf !== 'object' || conf === null || Array.isArray(conf)) {
55
+ return refuse([v('docs', 'invalid', String(conf), 'a map of doc keys to path lists')]);
56
+ }
57
+ const violations = [];
58
+ const out = {};
59
+ for (const [key, val] of Object.entries(conf)) {
60
+ if (!DOC_KEYS.includes(key)) {
61
+ violations.push(v(`docs.${key}`, 'unknown-doc-key', key, `a key with a shipped consumer (${DOC_KEYS.join(' | ')})`));
62
+ continue;
63
+ }
64
+ if (!Array.isArray(val) || val.length === 0 || !val.every((x) => typeof x === 'string' && x !== '')) {
65
+ violations.push(v(`docs.${key}`, 'invalid', JSON.stringify(val), 'a non-empty list of repo-relative file paths'));
66
+ continue;
67
+ }
68
+ const paths = [];
69
+ for (const p of val) {
70
+ const norm = p.replace(/\/+$/, '');
71
+ if (isAbsolute(norm) || norm.split('/').some((seg) => seg === '' || seg === '.' || seg === '..')) {
72
+ violations.push(v(`docs.${key}`, 'invalid', norm, 'a repo-relative file path without . or .. segments'));
73
+ continue;
74
+ }
75
+ if (norm === '.witness' || norm.startsWith('.witness/')) {
76
+ violations.push(v(`docs.${key}`, 'reserved', norm, 'a path outside .witness/'));
77
+ continue;
78
+ }
79
+ paths.push(norm);
80
+ }
81
+ out[key] = paths;
82
+ }
83
+ return violations.length ? refuse(violations) : ok(out);
84
+ }
85
+ export const DEFAULT_STEPS_PER_DISPATCH = 3;
86
+ export function resolveImplement(raw) {
87
+ const conf = raw.implement;
88
+ if (conf === undefined)
89
+ return ok({ stepsPerDispatch: DEFAULT_STEPS_PER_DISPATCH });
90
+ if (typeof conf !== 'object' || conf === null || Array.isArray(conf)) {
91
+ return refuse([v('implement', 'invalid', String(conf), 'a map of implement-stage settings')]);
92
+ }
93
+ const val = conf.stepsPerDispatch;
94
+ if (val === undefined)
95
+ return ok({ stepsPerDispatch: DEFAULT_STEPS_PER_DISPATCH });
96
+ if (typeof val !== 'number' || !Number.isInteger(val) || val < 1) {
97
+ return refuse([v('implement.stepsPerDispatch', 'invalid', String(val), 'an integer >= 1 — steps handed to each fresh implement agent')]);
98
+ }
99
+ return ok({ stepsPerDispatch: val });
100
+ }
101
+ // Lenient path resolution for callers without a loaded Config (canon scan, git
102
+ // state scoping): a missing or broken config falls back to the defaults — the
103
+ // verbs own the refusal messaging via loadConfig, which validates paths strictly.
104
+ export function canonPaths(root) {
105
+ const p = configPath(root);
106
+ if (!existsSync(p))
107
+ return DEFAULT_PATHS;
108
+ try {
109
+ const raw = (parseYaml(readFileSync(p, 'utf8')) ?? {});
110
+ const resolved = resolvePaths(raw);
111
+ return resolved.ok ? resolved.value : DEFAULT_PATHS;
112
+ }
113
+ catch {
114
+ return DEFAULT_PATHS;
115
+ }
116
+ }
117
+ export const configPath = (root) => join(root, 'witness.config.yaml');
118
+ export function loadConfig(root) {
119
+ const p = configPath(root);
120
+ if (!existsSync(p)) {
121
+ return refuse([v('witness.config.yaml', 'missing', 'no config at repo root', 'run witness init')]);
122
+ }
123
+ let raw;
124
+ try {
125
+ raw = parseYaml(readFileSync(p, 'utf8'));
126
+ }
127
+ catch (e) {
128
+ return refuse([v('witness.config.yaml', 'yaml-parse', String(e.message).slice(0, 120), 'valid YAML')]);
129
+ }
130
+ const obj = (raw ?? {});
131
+ const schema = obj.schema;
132
+ if (typeof schema !== 'number' || !Number.isInteger(schema)) {
133
+ return refuse([v('schema', 'required', String(schema ?? 'absent'), 'integer schema version')]);
134
+ }
135
+ if (schema > SCHEMA_VERSION) {
136
+ return refuse([v('schema', 'newer-than-cli', String(schema), `<=${SCHEMA_VERSION} — upgrade witness`)]);
137
+ }
138
+ const paths = resolvePaths(obj);
139
+ if (!paths.ok)
140
+ return refuse(paths.violations);
141
+ const docs = resolveDocs(obj);
142
+ if (!docs.ok)
143
+ return refuse(docs.violations);
144
+ const implement = resolveImplement(obj);
145
+ if (!implement.ok)
146
+ return refuse(implement.violations);
147
+ return ok({
148
+ schema,
149
+ raw: obj,
150
+ paths: paths.value,
151
+ docs: docs.value,
152
+ implement: implement.value,
153
+ warning: schema < SCHEMA_VERSION ? `schema ${schema} < ${SCHEMA_VERSION} — run witness migrate (reserved)` : undefined,
154
+ });
155
+ }
@@ -0,0 +1,85 @@
1
+ import { ensureTrusted } from './allowlist.js';
2
+ import { loadConfig } from './config.js';
3
+ import { matchesTag, sourceTags } from './matcher.js';
4
+ import { ok } from './refusal.js';
5
+ import { criteriaExcludes, execCommand, runFiltered, runFullSuite, runnerConfig } from './runner.js';
6
+ import { canonicalSha } from './sha.js';
7
+ export async function runCriteria(runRoot, ctx, doc, opts = {}) {
8
+ const trustRoot = opts.trustRoot ?? runRoot;
9
+ const cfg = loadConfig(runRoot);
10
+ if (!cfg.ok)
11
+ return cfg;
12
+ const rcRes = runnerConfig(cfg.value);
13
+ if (!rcRes.ok)
14
+ return rcRes;
15
+ const rc = rcRes.value;
16
+ const id = String(doc.meta.id);
17
+ const list = (Array.isArray(doc.meta.criteria) ? doc.meta.criteria : []);
18
+ let suite = opts.suite;
19
+ let suiteProblem;
20
+ if (rc.mode === 'full-suite' && suite === undefined && list.some((c) => typeof c.test === 'string')) {
21
+ const res = await runFullSuite(runRoot, ctx, rc, trustRoot);
22
+ if (res.ok)
23
+ suite = res.value.tests;
24
+ else
25
+ suiteProblem = res.violations.map((x) => `${x.rule}: ${x.field}`).join('; ');
26
+ }
27
+ const src = rc.mode === 'filtered'
28
+ ? sourceTags(runRoot, criteriaExcludes(cfg.value))
29
+ : undefined;
30
+ const criteria = [];
31
+ for (const c of list) {
32
+ const cid = String(c.id);
33
+ if (typeof c.test === 'string') {
34
+ if (rc.mode === 'full-suite') {
35
+ if (!suite) {
36
+ criteria.push({ id: cid, kind: 'test', ok: false, detail: suiteProblem ?? 'suite did not run' });
37
+ continue;
38
+ }
39
+ const mine = suite.filter((t) => matchesTag(t.name, id));
40
+ const passed = mine.filter((t) => t.status === 'passed').length;
41
+ const failed = mine.filter((t) => t.status === 'failed').length;
42
+ criteria.push({
43
+ id: cid, kind: 'test', ok: passed > 0 && failed === 0,
44
+ detail: `${mine.length} tagged · ${passed} passed · ${failed} failed`,
45
+ });
46
+ }
47
+ else {
48
+ const run = await runFiltered(runRoot, ctx, rc.template, id, trustRoot);
49
+ const count = src?.counts.get(id) ?? 0;
50
+ if (!run.ok) {
51
+ criteria.push({ id: cid, kind: 'test', ok: false, detail: run.violations.map((x) => x.rule).join('; ') });
52
+ }
53
+ else if (count === 0) {
54
+ criteria.push({ id: cid, kind: 'test', ok: false, detail: 'no tagged test found in source (grep side)' });
55
+ }
56
+ else {
57
+ criteria.push({
58
+ id: cid, kind: 'test', ok: run.value.exitZero,
59
+ detail: `filtered run ${run.value.exitZero ? 'exit 0' : 'nonzero'} · ${count} tagged in source`,
60
+ });
61
+ }
62
+ }
63
+ }
64
+ else if (typeof c.cmd === 'string') {
65
+ const trust = await ensureTrusted(trustRoot, ctx, c.cmd);
66
+ if (trust !== 'trusted') {
67
+ criteria.push({ id: cid, kind: 'cmd', ok: false, detail: `untrusted-${trust} — allow interactively or set WITNESS_TRUST_CMDS=1` });
68
+ continue;
69
+ }
70
+ const run = execCommand(runRoot, ctx, c.cmd);
71
+ criteria.push({ id: cid, kind: 'cmd', ok: run.exitZero, detail: run.exitZero ? 'exit 0' : 'nonzero exit' });
72
+ }
73
+ }
74
+ const tagCount = rc.mode === 'full-suite'
75
+ ? (suite ?? []).filter((t) => matchesTag(t.name, id)).length
76
+ : (src?.counts.get(id) ?? 0);
77
+ return ok({
78
+ spec: id,
79
+ sha: canonicalSha(doc.meta, doc.body),
80
+ ok: criteria.length > 0 && criteria.every((c) => c.ok),
81
+ mode: rc.mode,
82
+ tagCount,
83
+ criteria,
84
+ });
85
+ }
package/dist/design.js ADDED
@@ -0,0 +1,104 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { latestRecap, readStream } from './journal.js';
5
+ import { v } from './refusal.js';
6
+ import { effortOf } from './reviewed.js';
7
+ import { canonicalSha } from './sha.js';
8
+ export function elementIds(html) {
9
+ const ids = [];
10
+ const re = /\bid\s*=\s*["']([^"']+)["']/g;
11
+ let m;
12
+ while ((m = re.exec(html)) !== null)
13
+ ids.push(m[1].trim());
14
+ return ids;
15
+ }
16
+ // A design artifact is a portable, self-contained mockup: it must render with no
17
+ // network, and it must expose id-attributed sections so the design-critic can anchor
18
+ // findings to concrete parts of the screen (the anchor grammar's targets).
19
+ const EXTERNAL_REFS = [
20
+ [/<script[^>]*\bsrc\s*=/i, 'external <script src> — inline the script'],
21
+ [/<link[^>]*\bhref\s*=\s*["'](?:https?:)?\/\//i, 'external <link href> — inline the stylesheet'],
22
+ [/<img[^>]*\bsrc\s*=\s*["'](?:https?:)?\/\//i, 'external <img src> — embed as a data: URI'],
23
+ [/<[^>]*\bsrcset\s*=\s*["'][^"']*(?:https?:)?\/\//i, 'external srcset — embed as data: URIs'],
24
+ [/url\(\s*["']?(?:https?:)?\/\//i, 'external url() in CSS — embed the asset'],
25
+ ];
26
+ export function validateDesignArtifact(html) {
27
+ const out = [];
28
+ for (const [re, why] of EXTERNAL_REFS) {
29
+ if (re.test(html)) {
30
+ out.push(v('artifact', 'external-ref', why, 'a self-contained artifact — no external resource refs'));
31
+ break;
32
+ }
33
+ }
34
+ const ids = elementIds(html);
35
+ const unique = new Set(ids);
36
+ if (unique.size !== ids.length) {
37
+ out.push(v('artifact', 'duplicate-id', `${ids.length} ids, ${unique.size} unique`, 'unique id attributes — anchors must be unambiguous'));
38
+ }
39
+ if (unique.size < 2) {
40
+ out.push(v('artifact', 'template', `${unique.size} id-attributed element(s)`, '>=2 id-attributed sections — the design-critic anchors to element ids'));
41
+ }
42
+ if (!/<!doctype html/i.test(html) && !/<html[\s>]/i.test(html) && !/<body[\s>]/i.test(html)) {
43
+ out.push(v('artifact', 'not-html', html.slice(0, 40), 'an HTML document (<!doctype html> / <html> / <body>)'));
44
+ }
45
+ return out;
46
+ }
47
+ export function htmlSha(html) {
48
+ return createHash('sha256').update(html.replace(/\s+$/, '') + '\n').digest('hex');
49
+ }
50
+ export function designPairSha(html, spec) {
51
+ return createHash('sha256')
52
+ .update(`design:${htmlSha(html)}\n`)
53
+ .update(`spec:${canonicalSha(spec.meta, spec.body)}`)
54
+ .digest('hex');
55
+ }
56
+ export function designRel(paths, specId) {
57
+ return `${paths.designs}/${specId}.html`;
58
+ }
59
+ export function designStamp(spec) {
60
+ const s = spec.meta.design;
61
+ if (!s || typeof s.sha !== 'string' || typeof s.spec !== 'string')
62
+ return undefined;
63
+ return { sha: s.sha, spec: s.spec };
64
+ }
65
+ // The single home for "this ui spec owes a design": a ui-flagged spec in a
66
+ // feature-class effort whose design stamp is missing or stale (designed against
67
+ // older content). fix/chore efforts never arm it (Q2). next/dashboard/check/gate
68
+ // all consult this — never re-derive the rule.
69
+ export function designPending(root, spec) {
70
+ if (spec.meta.type !== 'spec' || spec.meta.ui !== true)
71
+ return false;
72
+ const effort = effortOf(root, String(spec.meta.id));
73
+ if (!effort)
74
+ return false;
75
+ if (latestRecap(root, effort)?.class !== 'feature')
76
+ return false;
77
+ const stamp = designStamp(spec);
78
+ return !stamp || stamp.spec !== canonicalSha(spec.meta, spec.body);
79
+ }
80
+ // Is an artifact authored for the CURRENT spec content already on disk (a design-write
81
+ // entry whose `spec` sha equals the current canonical sha)? Distinguishes "authored,
82
+ // awaiting gate" from "amended since the last artifact" — routing keys on this instead
83
+ // of file existence, so an amendment (stale artifact) correctly re-enters the design skill.
84
+ export function designArtifactCurrent(root, spec) {
85
+ const cur = canonicalSha(spec.meta, spec.body);
86
+ return readStream(root, String(spec.meta.id)).some((e) => e.t === 'design-write' && e.spec === cur);
87
+ }
88
+ // Pure half: has a human been shown THESE exact bytes? Sha-keyed, because a re-authored
89
+ // artifact is a different thing to look at — the same discipline that makes a re-captured
90
+ // screenshot invalidate its predecessor's witnessed sha (D71).
91
+ export function designShown(entries, sha) {
92
+ return entries.some((e) => e.t === 'design-shown' && e.sha === sha);
93
+ }
94
+ // The single home for "this artifact is registered but nobody has been shown it".
95
+ // Returns the unwitnessed sha, or undefined when there is nothing to see (no artifact)
96
+ // or a design-shown entry already covers the current bytes. gate/decide/next all consult
97
+ // this — never re-derive the rule.
98
+ export function designUnseen(root, paths, specId) {
99
+ const abs = join(root, designRel(paths, specId));
100
+ if (!existsSync(abs))
101
+ return undefined;
102
+ const sha = htmlSha(readFileSync(abs, 'utf8'));
103
+ return designShown(readStream(root, specId), sha) ? undefined : sha;
104
+ }