@workerstack/repo 0.0.0-stage → 0.1.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 (438) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +21 -0
  3. package/README.md +0 -3
  4. package/agents/global-settings/antigravity.json +3 -0
  5. package/agents/global-settings/claude.json +6 -0
  6. package/agents/global-settings/codex.toml +2 -0
  7. package/agents/global-settings/cursor.json +3 -0
  8. package/agents/global-settings/grok.toml +36 -0
  9. package/agents/global-settings/manifest.json +36 -0
  10. package/agents/grok/hooks/orchestrate-parent.json +60 -0
  11. package/agents/grok/hooks/rtk.json +16 -0
  12. package/agents/grok/hooks/rulesync.json +3 -0
  13. package/agents/grok/roles/scout.toml +5 -0
  14. package/agents/hooks.jsonc +67 -0
  15. package/agents/mcp-policy.jsonc +16 -0
  16. package/agents/rules/overview.md +21 -0
  17. package/agents/settings/claude.json +5 -0
  18. package/agents/skills/ask/SKILL.md +13 -0
  19. package/agents/skills/auto/SKILL.md +13 -0
  20. package/agents/skills/away/SKILL.md +19 -0
  21. package/agents/skills/blast-radius/SKILL.md +54 -0
  22. package/agents/skills/bro/SKILL.md +9 -0
  23. package/agents/skills/browser/SKILL.md +16 -0
  24. package/agents/skills/clasp/SKILL.md +11 -0
  25. package/agents/skills/commit/SKILL.md +35 -0
  26. package/agents/skills/commit-and-push/SKILL.md +9 -0
  27. package/agents/skills/commit-push/SKILL.md +59 -0
  28. package/agents/skills/commit-push-deploy/SKILL.md +74 -0
  29. package/agents/skills/commit-push-release/SKILL.md +75 -0
  30. package/agents/skills/correct/SKILL.md +39 -0
  31. package/agents/skills/do/SKILL.md +21 -0
  32. package/agents/skills/explain/SKILL.md +13 -0
  33. package/agents/skills/google-workspace/SKILL.md +12 -0
  34. package/agents/skills/handoff/SKILL.md +81 -0
  35. package/agents/skills/implement-plan/SKILL.md +29 -0
  36. package/agents/skills/implement-roadmap/SKILL.md +27 -0
  37. package/agents/skills/jev/SKILL.md +17 -0
  38. package/agents/skills/listen/SKILL.md +15 -0
  39. package/agents/skills/pickup/SKILL.md +58 -0
  40. package/agents/skills/principle-attack-the-premise/SKILL.md +24 -0
  41. package/agents/skills/principle-boundary-discipline/SKILL.md +38 -0
  42. package/agents/skills/principle-build-the-lever/SKILL.md +28 -0
  43. package/agents/skills/principle-encode-lessons-in-structure/SKILL.md +15 -0
  44. package/agents/skills/principle-exhaust-the-design-space/SKILL.md +12 -0
  45. package/agents/skills/principle-experience-first/SKILL.md +22 -0
  46. package/agents/skills/principle-explain-the-number/SKILL.md +21 -0
  47. package/agents/skills/principle-fix-root-causes/SKILL.md +24 -0
  48. package/agents/skills/principle-foundational-thinking/SKILL.md +25 -0
  49. package/agents/skills/principle-guard-the-context-window/SKILL.md +16 -0
  50. package/agents/skills/principle-laziness-protocol/SKILL.md +13 -0
  51. package/agents/skills/principle-make-operations-idempotent/SKILL.md +25 -0
  52. package/agents/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +18 -0
  53. package/agents/skills/principle-minimize-reader-load/SKILL.md +22 -0
  54. package/agents/skills/principle-model-the-domain/SKILL.md +26 -0
  55. package/agents/skills/principle-never-block-on-the-human/SKILL.md +17 -0
  56. package/agents/skills/principle-outcome-oriented-execution/SKILL.md +25 -0
  57. package/agents/skills/principle-prove-it-works/SKILL.md +25 -0
  58. package/agents/skills/principle-redesign-from-first-principles/SKILL.md +18 -0
  59. package/agents/skills/principle-separate-before-serializing-shared-state/SKILL.md +20 -0
  60. package/agents/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
  61. package/agents/skills/principle-subtract-before-you-add/SKILL.md +21 -0
  62. package/agents/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
  63. package/agents/skills/principle-type-system-discipline/SKILL.md +17 -0
  64. package/agents/skills/review-plan/SKILL.md +28 -0
  65. package/agents/skills/review-roadmap/SKILL.md +28 -0
  66. package/agents/skills/show-me/SKILL.md +21 -0
  67. package/agents/skills/speak/SKILL.md +15 -0
  68. package/agents/skills/su/SKILL.md +11 -0
  69. package/agents/skills/teach/SKILL.md +25 -0
  70. package/agents/skills/tmp/SKILL.md +54 -0
  71. package/agents/skills/typescript-best-practices/SKILL.md +32 -0
  72. package/agents/skills/typescript-best-practices/references/patterns.md +328 -0
  73. package/agents/skills/unslop/SKILL.md +62 -0
  74. package/agents/skills/write-plan/SKILL.md +33 -0
  75. package/agents/skills/write-roadmap/SKILL.md +66 -0
  76. package/agents/subagents/implementer-deep.md +57 -0
  77. package/agents/subagents/implementer-fast.md +57 -0
  78. package/agents/subagents/orchestrator.md +43 -0
  79. package/agents/subagents/researcher.md +47 -0
  80. package/agents/subagents/scout.md +44 -0
  81. package/bin/repo.ts +4 -0
  82. package/cli/actionlint-default.test.ts +38 -0
  83. package/cli/actionlint-download.test.ts +162 -0
  84. package/cli/actionlint-real.test.ts +33 -0
  85. package/cli/actionlint.constants.ts +43 -0
  86. package/cli/actionlint.test.ts +108 -0
  87. package/cli/actionlint.ts +178 -0
  88. package/cli/bin.test.ts +54 -0
  89. package/cli/bin.ts +40 -0
  90. package/cli/check-changelog.test.ts +40 -0
  91. package/cli/check-changelog.ts +12 -0
  92. package/cli/check-comments.test.ts +157 -0
  93. package/cli/check-comments.ts +123 -0
  94. package/cli/check-env-modules.test.ts +85 -0
  95. package/cli/check-env-modules.ts +56 -0
  96. package/cli/check-magic.test.ts +139 -0
  97. package/cli/check-magic.ts +120 -0
  98. package/cli/check-manifests.constants.ts +4 -0
  99. package/cli/check-manifests.test.ts +210 -0
  100. package/cli/check-manifests.ts +71 -0
  101. package/cli/check-nightly.test.ts +103 -0
  102. package/cli/check-nightly.ts +37 -0
  103. package/cli/check-ratchets-missing.test.ts +46 -0
  104. package/cli/check-ratchets-mutation.test.ts +49 -0
  105. package/cli/check-ratchets.constants.ts +20 -0
  106. package/cli/check-ratchets.test.ts +190 -0
  107. package/cli/check-ratchets.ts +193 -0
  108. package/cli/check-rejections.test.ts +220 -0
  109. package/cli/check-rejections.ts +153 -0
  110. package/cli/check-reports.ts +37 -0
  111. package/cli/check-tsconfig.test.ts +507 -0
  112. package/cli/check-tsconfig.ts +205 -0
  113. package/cli/commands.test.ts +114 -0
  114. package/cli/commands.ts +218 -0
  115. package/cli/commit-changelog.ts +27 -0
  116. package/cli/commit-msg-changelog.test.ts +93 -0
  117. package/cli/commit-msg-range.test.ts +126 -0
  118. package/cli/commit-msg-red-proof.test.ts +147 -0
  119. package/cli/commit-msg-repo.test.ts +153 -0
  120. package/cli/commit-msg.test.ts +101 -0
  121. package/cli/commit-msg.ts +194 -0
  122. package/cli/commit-range.constants.ts +17 -0
  123. package/cli/commit-range.ts +57 -0
  124. package/cli/config-knip.test.ts +70 -0
  125. package/cli/config-nightly.test.ts +84 -0
  126. package/cli/config-tdd.test.ts +46 -0
  127. package/cli/config.test.ts +576 -0
  128. package/cli/config.ts +261 -0
  129. package/cli/conventional.ts +9 -0
  130. package/cli/coverage-config.ts +62 -0
  131. package/cli/coverage.constants.ts +6 -0
  132. package/cli/coverage.test.ts +199 -0
  133. package/cli/coverage.ts +127 -0
  134. package/cli/env.constants.ts +10 -0
  135. package/cli/env.test.ts +56 -0
  136. package/cli/env.ts +72 -0
  137. package/cli/fixtures/base-config.ts +13 -0
  138. package/cli/fixtures/gate-world.ts +48 -0
  139. package/cli/fixtures/knip-run.ts +38 -0
  140. package/cli/fixtures/mutate-fakes.ts +137 -0
  141. package/cli/fixtures/planted-repo.ts +50 -0
  142. package/cli/fixtures/publish-world.ts +71 -0
  143. package/cli/fixtures/red-proof-repo.ts +72 -0
  144. package/cli/fixtures/release-context.ts +15 -0
  145. package/cli/fixtures/release-world.ts +111 -0
  146. package/cli/fixtures/stub-bin.ts +40 -0
  147. package/cli/guard-diagnostics.test.ts +157 -0
  148. package/cli/guard-diagnostics.ts +195 -0
  149. package/cli/init.test.ts +487 -0
  150. package/cli/init.ts +275 -0
  151. package/cli/knip-check-ignores.test.ts +106 -0
  152. package/cli/knip-check-real.test.ts +65 -0
  153. package/cli/knip-check.constants.ts +46 -0
  154. package/cli/knip-check.test.ts +114 -0
  155. package/cli/knip-check.ts +169 -0
  156. package/cli/knip-schema.ts +67 -0
  157. package/cli/known-failures.constants.ts +22 -0
  158. package/cli/known-failures.test.ts +138 -0
  159. package/cli/known-failures.ts +131 -0
  160. package/cli/lint/as-needs-reason.ts +41 -0
  161. package/cli/lint/boundaries.ts +73 -0
  162. package/cli/lint/class-file.ts +39 -0
  163. package/cli/lint/config-file.ts +32 -0
  164. package/cli/lint/entry-file.ts +54 -0
  165. package/cli/lint/env-in-config.ts +66 -0
  166. package/cli/lint/filename-matches-export.ts +63 -0
  167. package/cli/lint/index-file.ts +22 -0
  168. package/cli/lint/index.ts +47 -0
  169. package/cli/lint/is-reason.ts +6 -0
  170. package/cli/lint/manifest.ts +46 -0
  171. package/cli/lint/names.ts +59 -0
  172. package/cli/lint/no-commented-out-code.ts +105 -0
  173. package/cli/lint/no-mock-of-subject.ts +64 -0
  174. package/cli/lint/no-module-side-effects.ts +94 -0
  175. package/cli/lint/no-module-state.ts +58 -0
  176. package/cli/lint/no-side-effect-import.ts +17 -0
  177. package/cli/lint/no-v8-ignore.ts +18 -0
  178. package/cli/lint/script-rules.ts +9 -0
  179. package/cli/lint/skip-on-platform.constants.ts +8 -0
  180. package/cli/lint/skip-on-platform.ts +118 -0
  181. package/cli/lint/todo-needs-id.ts +63 -0
  182. package/cli/lint/types-file.ts +28 -0
  183. package/cli/lint-schema.ts +32 -0
  184. package/cli/magic/fixtures/magic-repo.ts +64 -0
  185. package/cli/magic/magic-hit.ts +2 -0
  186. package/cli/magic/magic-nodes.ts +44 -0
  187. package/cli/magic/magic-numbers.test.ts +99 -0
  188. package/cli/magic/magic-numbers.ts +117 -0
  189. package/cli/magic/magic-strings.test.ts +38 -0
  190. package/cli/magic/magic-strings.ts +81 -0
  191. package/cli/main.test.ts +853 -0
  192. package/cli/main.ts +292 -0
  193. package/cli/mutate-args.ts +79 -0
  194. package/cli/mutate-check.test.ts +147 -0
  195. package/cli/mutate-deps.ts +42 -0
  196. package/cli/mutate-edges.test.ts +150 -0
  197. package/cli/mutate-files.test.ts +100 -0
  198. package/cli/mutate-files.ts +31 -0
  199. package/cli/mutate-floors.test.ts +91 -0
  200. package/cli/mutate-floors.ts +104 -0
  201. package/cli/mutate-real-deps.ts +57 -0
  202. package/cli/mutate-real.test.ts +96 -0
  203. package/cli/mutate-report.ts +96 -0
  204. package/cli/mutate-run.ts +128 -0
  205. package/cli/mutate-shards.ts +64 -0
  206. package/cli/mutate.constants.ts +67 -0
  207. package/cli/mutate.test.ts +182 -0
  208. package/cli/mutate.ts +64 -0
  209. package/cli/nested-vitest-flags.ts +8 -0
  210. package/cli/nightly-config.constants.ts +14 -0
  211. package/cli/nightly-config.ts +62 -0
  212. package/cli/prompt.test.ts +202 -0
  213. package/cli/prompt.ts +1 -0
  214. package/cli/red-proof-edges.test.ts +78 -0
  215. package/cli/red-proof-failures.test.ts +82 -0
  216. package/cli/red-proof-tree.test.ts +157 -0
  217. package/cli/red-proof-verdict.test.ts +44 -0
  218. package/cli/red-proof.constants.ts +14 -0
  219. package/cli/red-proof.ts +202 -0
  220. package/cli/release/changelog-parse.ts +83 -0
  221. package/cli/release/changelog.test.ts +141 -0
  222. package/cli/release/changelog.ts +106 -0
  223. package/cli/release/gate-refusals.test.ts +85 -0
  224. package/cli/release/gate.test.ts +63 -0
  225. package/cli/release/gate.ts +70 -0
  226. package/cli/release/git.ts +66 -0
  227. package/cli/release/github.test.ts +128 -0
  228. package/cli/release/github.ts +46 -0
  229. package/cli/release/list-dir.test.ts +11 -0
  230. package/cli/release/list-dir.ts +6 -0
  231. package/cli/release/main.test.ts +48 -0
  232. package/cli/release/main.ts +51 -0
  233. package/cli/release/manifest.test.ts +24 -0
  234. package/cli/release/manifest.ts +34 -0
  235. package/cli/release/pack.test.ts +107 -0
  236. package/cli/release/pack.ts +76 -0
  237. package/cli/release/plan.ts +127 -0
  238. package/cli/release/publish-run.test.ts +98 -0
  239. package/cli/release/publish.test.ts +127 -0
  240. package/cli/release/publish.ts +125 -0
  241. package/cli/release/refuse.ts +5 -0
  242. package/cli/release/release-messages.constants.ts +96 -0
  243. package/cli/release/release-refusals.test.ts +149 -0
  244. package/cli/release/release.constants.ts +128 -0
  245. package/cli/release/release.test.ts +199 -0
  246. package/cli/release/release.ts +145 -0
  247. package/cli/release/version.test.ts +118 -0
  248. package/cli/release/version.ts +69 -0
  249. package/cli/release/wait.test.ts +101 -0
  250. package/cli/release/wait.ts +95 -0
  251. package/cli/script-diagnostics.test.ts +76 -0
  252. package/cli/script-diagnostics.ts +62 -0
  253. package/cli/scripts/board.ts +519 -0
  254. package/cli/scripts/browser.ts +994 -0
  255. package/cli/scripts/change-set.ts +105 -0
  256. package/cli/scripts/check-globals.ts +199 -0
  257. package/cli/scripts/ci-skips.constants.ts +17 -0
  258. package/cli/scripts/ci-skips.ts +118 -0
  259. package/cli/scripts/ci-wait-run.ts +28 -0
  260. package/cli/scripts/ci-wait.constants.ts +44 -0
  261. package/cli/scripts/ci-wait.ts +175 -0
  262. package/cli/scripts/ci.constants.ts +15 -0
  263. package/cli/scripts/ci.ts +208 -0
  264. package/cli/scripts/clean.ts +363 -0
  265. package/cli/scripts/comments-step.ts +15 -0
  266. package/cli/scripts/doctor.ts +828 -0
  267. package/cli/scripts/emit-agent-shell-tools.ts +91 -0
  268. package/cli/scripts/emit-mcp-policy.ts +402 -0
  269. package/cli/scripts/folder-map.ts +353 -0
  270. package/cli/scripts/gate-fat-tools.ts +1095 -0
  271. package/cli/scripts/gitignore-guard.ts +338 -0
  272. package/cli/scripts/google-workspace.ts +779 -0
  273. package/cli/scripts/hook-command.ts +72 -0
  274. package/cli/scripts/hook-input.ts +178 -0
  275. package/cli/scripts/hook-io.constants.ts +5 -0
  276. package/cli/scripts/hook-io.ts +46 -0
  277. package/cli/scripts/hooks-union.ts +469 -0
  278. package/cli/scripts/interactive.ts +30 -0
  279. package/cli/scripts/jev.ts +420 -0
  280. package/cli/scripts/job.ts +934 -0
  281. package/cli/scripts/jsonc.ts +9 -0
  282. package/cli/scripts/lessons.ts +564 -0
  283. package/cli/scripts/lint-baseline.ts +233 -0
  284. package/cli/scripts/lint-main.ts +24 -0
  285. package/cli/scripts/lint-skips.ts +139 -0
  286. package/cli/scripts/lint.constants.ts +6 -0
  287. package/cli/scripts/lint.ts +142 -0
  288. package/cli/scripts/listen.ts +962 -0
  289. package/cli/scripts/managed-text.ts +47 -0
  290. package/cli/scripts/orchestrate-parent.ts +1155 -0
  291. package/cli/scripts/parse-need.ts +81 -0
  292. package/cli/scripts/persist-handoff.ts +228 -0
  293. package/cli/scripts/persist-implementation.ts +276 -0
  294. package/cli/scripts/persist-plan.ts +888 -0
  295. package/cli/scripts/persist-roadmap.ts +478 -0
  296. package/cli/scripts/persist-tmp.ts +177 -0
  297. package/cli/scripts/persist.ts +54 -0
  298. package/cli/scripts/phase-files.ts +121 -0
  299. package/cli/scripts/prompt.ts +38 -0
  300. package/cli/scripts/purge-global-mcps.ts +313 -0
  301. package/cli/scripts/rejections-step.ts +37 -0
  302. package/cli/scripts/rtk-grok.ts +102 -0
  303. package/cli/scripts/scrub-spawn-prompt.ts +103 -0
  304. package/cli/scripts/scrub-text-lib.ts +58 -0
  305. package/cli/scripts/session-map.ts +69 -0
  306. package/cli/scripts/setup.ts +862 -0
  307. package/cli/scripts/skill-conflicts.ts +82 -0
  308. package/cli/scripts/speak.ts +370 -0
  309. package/cli/scripts/strategy.ts +168 -0
  310. package/cli/scripts/sync-global-settings.ts +316 -0
  311. package/cli/scripts/turn-steps.ts +249 -0
  312. package/cli/scripts/turn.ts +157 -0
  313. package/cli/scripts/typecheck-step.ts +43 -0
  314. package/cli/scripts/vscode-tasks.ts +175 -0
  315. package/cli/scripts/workspace-root.ts +160 -0
  316. package/cli/scripts/worktree.ts +806 -0
  317. package/cli/spawn.test.ts +34 -0
  318. package/cli/spawn.ts +27 -0
  319. package/cli/staged-change.constants.ts +11 -0
  320. package/cli/staged-change.ts +79 -0
  321. package/cli/stryker-preload.test.ts +50 -0
  322. package/cli/stryker-preload.ts +9 -0
  323. package/cli/sync/agents/frontmatter.test.ts +197 -0
  324. package/cli/sync/agents/frontmatter.ts +140 -0
  325. package/cli/sync/agents/hooks.test.ts +517 -0
  326. package/cli/sync/agents/hooks.ts +401 -0
  327. package/cli/sync/agents/rules.test.ts +229 -0
  328. package/cli/sync/agents/rules.ts +84 -0
  329. package/cli/sync/agents/sources.test.ts +375 -0
  330. package/cli/sync/agents/sources.ts +244 -0
  331. package/cli/sync/agents/subagents.test.ts +149 -0
  332. package/cli/sync/agents/subagents.ts +158 -0
  333. package/cli/sync/agents-text.test.ts +31 -0
  334. package/cli/sync/agents-text.ts +36 -0
  335. package/cli/sync/fixtures/copied.README.md +35 -0
  336. package/cli/sync/fixtures/copied.tgz +0 -0
  337. package/cli/sync/fixtures/maps/golden/INDEX.md +22 -0
  338. package/cli/sync/fixtures/maps-ws.tgz +0 -0
  339. package/cli/sync/git-files.test.ts +340 -0
  340. package/cli/sync/git-files.ts +197 -0
  341. package/cli/sync/git-hooks.test.ts +94 -0
  342. package/cli/sync/golden.test.ts +15 -0
  343. package/cli/sync/hooks-mode.test.ts +99 -0
  344. package/cli/sync/hosts.ts +28 -0
  345. package/cli/sync/knip-config.constants.ts +4 -0
  346. package/cli/sync/knip-config.test.ts +147 -0
  347. package/cli/sync/knip-config.ts +108 -0
  348. package/cli/sync/manifest.test.ts +78 -0
  349. package/cli/sync/manifest.ts +60 -0
  350. package/cli/sync/maps.test.ts +304 -0
  351. package/cli/sync/maps.ts +297 -0
  352. package/cli/sync/migrate.test.ts +2000 -0
  353. package/cli/sync/migrate.ts +1104 -0
  354. package/cli/sync/nightly-jobs.test.ts +63 -0
  355. package/cli/sync/nightly.constants.ts +48 -0
  356. package/cli/sync/nightly.test.ts +100 -0
  357. package/cli/sync/nightly.ts +75 -0
  358. package/cli/sync/outputs.test.ts +351 -0
  359. package/cli/sync/outputs.ts +168 -0
  360. package/cli/sync/oxlint-config.test.ts +178 -0
  361. package/cli/sync/oxlint-config.ts +168 -0
  362. package/cli/sync/release-jobs.test.ts +29 -0
  363. package/cli/sync/release-reports.test.ts +31 -0
  364. package/cli/sync/release-workflow.constants.ts +61 -0
  365. package/cli/sync/release-workflow.test.ts +192 -0
  366. package/cli/sync/release-workflow.ts +86 -0
  367. package/cli/sync/release.constants.ts +93 -0
  368. package/cli/sync/release.test.ts +187 -0
  369. package/cli/sync/release.ts +112 -0
  370. package/cli/sync/repo-dir.test.ts +371 -0
  371. package/cli/sync/repo-dir.ts +205 -0
  372. package/cli/sync/standards.constants.ts +8 -0
  373. package/cli/sync/standards.test.ts +206 -0
  374. package/cli/sync/standards.ts +179 -0
  375. package/cli/sync/stock-history.json +2796 -0
  376. package/cli/sync/stock-history.test.ts +66 -0
  377. package/cli/sync/stock-history.ts +84 -0
  378. package/cli/sync/stock.test.ts +47 -0
  379. package/cli/sync/stock.ts +24 -0
  380. package/cli/sync/trees.test.ts +621 -0
  381. package/cli/sync/trees.ts +280 -0
  382. package/cli/sync/validate.test.ts +299 -0
  383. package/cli/sync/validate.ts +402 -0
  384. package/cli/sync/workspace.test.ts +180 -0
  385. package/cli/sync/workspace.ts +138 -0
  386. package/cli/sync/write.test.ts +341 -0
  387. package/cli/sync/write.ts +231 -0
  388. package/cli/sync.test.ts +1495 -0
  389. package/cli/sync.ts +360 -0
  390. package/cli/tdd-scope.test.ts +36 -0
  391. package/cli/tdd-scope.ts +41 -0
  392. package/cli/test-files.ts +16 -0
  393. package/cli/ts-program.ts +69 -0
  394. package/cli/tsconfig.constants.ts +8 -0
  395. package/cli/workerstack-config.types.ts +21 -0
  396. package/dist/cli/env.d.ts +10 -0
  397. package/dist/cli/env.js +64 -0
  398. package/dist/presets/temp-root-env.d.ts +10 -0
  399. package/dist/presets/temp-root-env.js +20 -0
  400. package/dist/presets/temp-root.constants.d.ts +6 -0
  401. package/dist/presets/temp-root.constants.js +6 -0
  402. package/dist/presets/temp-root.d.ts +2 -0
  403. package/dist/presets/temp-root.js +60 -0
  404. package/dist/presets/vitest.constants.d.ts +16 -0
  405. package/dist/presets/vitest.constants.js +16 -0
  406. package/dist/presets/vitest.d.ts +11 -0
  407. package/dist/presets/vitest.js +67 -0
  408. package/dist/presets/workers-plugin.d.ts +10 -0
  409. package/dist/presets/workers-plugin.js +54 -0
  410. package/package.json +141 -4
  411. package/references/INDEX.md +100 -0
  412. package/references/browser.md +66 -0
  413. package/references/cascade.md +15 -0
  414. package/references/clasp.md +21 -0
  415. package/references/cloudflare.md +36 -0
  416. package/references/google-workspace.md +79 -0
  417. package/references/hooks.md +103 -0
  418. package/references/implementation-format.md +119 -0
  419. package/references/jev.md +56 -0
  420. package/references/lessons.md +73 -0
  421. package/references/listen.md +80 -0
  422. package/references/map.md +107 -0
  423. package/references/output.md +47 -0
  424. package/references/plan-format.md +264 -0
  425. package/references/review-loop.md +48 -0
  426. package/references/roadmap-format.md +104 -0
  427. package/references/rtk.md +27 -0
  428. package/references/scope.md +76 -0
  429. package/references/setup.md +42 -0
  430. package/references/speak.md +59 -0
  431. package/tsconfig/base.json +28 -0
  432. package/tsconfig/bun.d.ts +1 -0
  433. package/tsconfig/bun.json +8 -0
  434. package/tsconfig/reset.d.ts +1 -0
  435. package/tsconfig/strict.json +12 -0
  436. package/tsconfig/web.json +7 -0
  437. package/tsconfig/worker.d.ts +1 -0
  438. package/tsconfig/worker.json +8 -0
@@ -0,0 +1,96 @@
1
+ /** The two reports of `repo mutate`: Stryker's json report in, a shard's own report out and read back. */
2
+ import { z } from "zod";
3
+ import { KILLED_STATUSES, SURVIVOR_STATUSES } from "./mutate.constants.ts";
4
+
5
+ /** A mutant no test killed. */
6
+ type Survivor = { line: number; column: number; mutator: string; status: "Survived" | "NoCoverage" };
7
+ /** One file's mutants: how many tests killed and the ones that lived. */
8
+ export type FileScore = { killed: number; survivors: Survivor[] };
9
+ /** What one shard run writes: the commit it mutated, which shard of how many, and its files' scores. */
10
+ export type ShardReport = { head: string; shard: { index: number; total: number }; files: Record<string, FileScore> };
11
+
12
+ function buildSchemas() {
13
+ const stryker = z.object({
14
+ files: z.record(
15
+ z.string(),
16
+ z.object({
17
+ mutants: z.array(
18
+ z.object({
19
+ status: z.string(),
20
+ mutatorName: z.string(),
21
+ location: z.object({ start: z.object({ line: z.number(), column: z.number() }) }),
22
+ }),
23
+ ),
24
+ }),
25
+ ),
26
+ });
27
+ const survivor = z.object({
28
+ line: z.number(),
29
+ column: z.number(),
30
+ mutator: z.string(),
31
+ status: z.enum(["Survived", "NoCoverage"]),
32
+ });
33
+ const report = z.object({
34
+ head: z.string(),
35
+ shard: z.object({ index: z.number().int().min(1), total: z.number().int().min(1) }),
36
+ files: z.record(z.string(), z.object({ killed: z.number(), survivors: z.array(survivor) })),
37
+ });
38
+ return { stryker, report };
39
+ }
40
+
41
+ const schemas = /* @__PURE__ */ buildSchemas();
42
+
43
+ export type Parsed<T> = { ok: true; value: T } | { ok: false; reason: string };
44
+
45
+ const reasonOf = (error: z.ZodError): string =>
46
+ error.issues.map((issue) => `${issue.path.join(".") || "report"}: ${issue.message}`).join("; ");
47
+
48
+ type Mutant = z.output<typeof schemas.stryker>["files"][string]["mutants"][number];
49
+
50
+ function survivorOf(mutant: Mutant): Survivor[] {
51
+ if (!SURVIVOR_STATUSES.includes(mutant.status)) return [];
52
+ const { line, column } = mutant.location.start;
53
+ return [
54
+ {
55
+ line,
56
+ column,
57
+ mutator: mutant.mutatorName,
58
+ status: mutant.status /* SURVIVOR_STATUSES lists exactly the two statuses */ as Survivor["status"],
59
+ },
60
+ ];
61
+ }
62
+
63
+ /** Stryker's report as per-file scores: Killed and Timeout kill, Survived and NoCoverage survive, the rest is left out. */
64
+ export function fromStryker(raw: unknown): Parsed<Record<string, FileScore>> {
65
+ const parsed = schemas.stryker.safeParse(raw);
66
+ if (!parsed.success) return { ok: false, reason: reasonOf(parsed.error) };
67
+ const files = Object.entries(parsed.data.files).map(([file, { mutants }]): [string, FileScore] => [
68
+ file,
69
+ {
70
+ killed: mutants.filter((mutant) => KILLED_STATUSES.includes(mutant.status)).length,
71
+ survivors: mutants.flatMap(survivorOf),
72
+ },
73
+ ]);
74
+ return { ok: true, value: Object.fromEntries(files) };
75
+ }
76
+
77
+ /** JSON text as a value, or why it is no JSON. */
78
+ export function parseJson(text: string): Parsed<unknown> {
79
+ try {
80
+ return { ok: true, value: JSON.parse(text) };
81
+ } catch (error) {
82
+ return { ok: false, reason: String(error) };
83
+ }
84
+ }
85
+
86
+ /** A shard report read from JSON text. */
87
+ export function parseReport(text: string): Parsed<ShardReport> {
88
+ const json = parseJson(text);
89
+ if (!json.ok) return json;
90
+ const parsed = schemas.report.safeParse(json.value);
91
+ return parsed.success ? { ok: true, value: parsed.data } : { ok: false, reason: reasonOf(parsed.error) };
92
+ }
93
+
94
+ /** The survivors of every file of a report. */
95
+ export const survivorCount = (files: Record<string, FileScore>): number =>
96
+ Object.values(files).reduce((sum, file) => sum + file.survivors.length, 0);
@@ -0,0 +1,128 @@
1
+ /** One `repo mutate` run in a throwaway clone of HEAD: clone, install, Stryker, its report; the clone is always removed. */
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import type { Captured } from "./scripts/lint.ts";
5
+ import {
6
+ BUN_FLAGS,
7
+ CLONE_DIR,
8
+ CONFIG_NAME,
9
+ INSTALL_ARGS,
10
+ INSTALL_COMMAND,
11
+ INSTALL_HINT,
12
+ INSTALLED_DIR,
13
+ JSON_INDENT,
14
+ MANIFEST,
15
+ NEWLINE,
16
+ RAW_REPORT_NAME,
17
+ SHORT_SHA,
18
+ STRYKER_BIN_NAME,
19
+ STRYKER_CORE,
20
+ STRYKER_OPTIONS,
21
+ STRYKER_RUNNER,
22
+ STRYKER_RUN,
23
+ TEMP_PREFIX,
24
+ } from "./mutate.constants.ts";
25
+ import type { Shard } from "./mutate-args.ts";
26
+ import type { MutateDeps } from "./mutate-deps.ts";
27
+ import { splitShards } from "./mutate-files.ts";
28
+ import { fromStryker, parseJson, type ShardReport } from "./mutate-report.ts";
29
+
30
+ /** What a run mutates: the root, the commit, every selected file and the shard of them this run takes. */
31
+ export type Plan = { root: string; sha: string; files: string[]; shard: Shard };
32
+
33
+ function buildBinSchema() {
34
+ return z.object({ bin: z.union([z.string(), z.record(z.string(), z.string())]) });
35
+ }
36
+
37
+ const binSchema = /* @__PURE__ */ buildBinSchema();
38
+
39
+ /** A failed command's own words: its stderr, else its stdout. */
40
+ const detail = (run: Captured): string => run.stderr.trim() || run.stdout.trim();
41
+
42
+ /** Prints a refusal and returns null, the failure every step answers with. */
43
+ function refuse(deps: MutateDeps, line: string, extra = ""): null {
44
+ deps.err(`repo mutate: ${line}${NEWLINE}${extra}`);
45
+ return null;
46
+ }
47
+
48
+ /** Clones the root shared, then detaches the clone to the commit; false after saying why when git fails. */
49
+ function checkout(clone: string, plan: Plan, deps: MutateDeps): boolean {
50
+ const cloned = deps.git(plan.root, ["clone", "-q", "--shared", plan.root, clone]);
51
+ const detach = ["-C", clone, "checkout", "-q", "--detach", plan.sha];
52
+ const last = cloned.code === 0 ? deps.git(plan.root, detach) : cloned;
53
+ if (last.code === 0) return true;
54
+ const what = cloned.code === 0 ? `check out ${plan.sha.slice(0, SHORT_SHA)} in the clone` : `clone ${plan.root}`;
55
+ refuse(deps, `could not ${what}: ${detail(last)}`);
56
+ return false;
57
+ }
58
+
59
+ /** The path of Stryker's bin in the clone, or null after printing the install line when the tool is not installed. */
60
+ function strykerBin(clone: string, deps: MutateDeps): string | null {
61
+ const modules = path.join(clone, INSTALLED_DIR);
62
+ const core = deps.fs.read(path.join(modules, STRYKER_CORE, MANIFEST));
63
+ const runner = deps.fs.read(path.join(modules, STRYKER_RUNNER, MANIFEST));
64
+ const manifest = core === null || runner === null ? null : parseJson(core);
65
+ const parsed = manifest?.ok === true ? binSchema.safeParse(manifest.value) : null;
66
+ const bin = parsed?.success ? parsed.data.bin : undefined;
67
+ const relative = typeof bin === "string" ? bin : bin?.[STRYKER_BIN_NAME];
68
+ if (relative === undefined) return refuse(deps, `install the mutation tool first: ${INSTALL_HINT}`);
69
+ return path.join(modules, STRYKER_CORE, relative);
70
+ }
71
+
72
+ /** The Stryker config for `files`, written beside the clone with its raw report path. */
73
+ function configOf(files: string[], rawReport: string): string {
74
+ const config = { ...STRYKER_OPTIONS, mutate: files, jsonReporter: { fileName: rawReport } };
75
+ return JSON.stringify(config, null, JSON_INDENT);
76
+ }
77
+
78
+ function lineCounts(clone: string, files: string[], deps: MutateDeps): Map<string, number> {
79
+ const count = (file: string) => (deps.fs.read(path.join(clone, file)) ?? "").split(NEWLINE).length;
80
+ return new Map(files.map((file) => [file, count(file)]));
81
+ }
82
+
83
+ /** Stryker over `files` in the clone: its per-file scores, or null after printing why it gave none. */
84
+ function stryker(base: string, run: { files: string[]; bin: string }, deps: MutateDeps): ShardReport["files"] | null {
85
+ const { files, bin } = run;
86
+ const clone = path.join(base, CLONE_DIR);
87
+ const config = path.join(base, CONFIG_NAME);
88
+ const raw = path.join(base, RAW_REPORT_NAME);
89
+ deps.fs.write(config, configOf(files, raw));
90
+ const ran = deps.run([...BUN_FLAGS, deps.preload, bin, STRYKER_RUN, config], clone);
91
+ if (ran.code !== 0) {
92
+ return refuse(deps, "the tests failed before any mutant ran; fix them first", ran.stdout + ran.stderr);
93
+ }
94
+ const text = deps.fs.read(raw);
95
+ if (text === null) return refuse(deps, "Stryker wrote no readable report: the report file is missing");
96
+ const json = parseJson(text);
97
+ const scores = json.ok ? fromStryker(json.value) : json;
98
+ return scores.ok ? scores.value : refuse(deps, `Stryker wrote no readable report: ${scores.reason}`);
99
+ }
100
+
101
+ /** A finished run: the shard's report and how many files it took. */
102
+ export type Ran = { report: ShardReport; files: number };
103
+
104
+ function mutateIn(base: string, plan: Plan, deps: MutateDeps): Ran | null {
105
+ const clone = path.join(base, CLONE_DIR);
106
+ if (!checkout(clone, plan, deps)) return null;
107
+ const mine = splitShards(plan.files, lineCounts(clone, plan.files, deps), plan.shard);
108
+ const ran = (files: ShardReport["files"]): Ran => ({
109
+ report: { head: plan.sha, shard: plan.shard, files },
110
+ files: mine.length,
111
+ });
112
+ if (mine.length === 0) return ran({});
113
+ const installed = deps.run([...INSTALL_ARGS], clone);
114
+ if (installed.code !== 0) return refuse(deps, `${INSTALL_COMMAND} failed in the clone: ${detail(installed)}`);
115
+ const bin = strykerBin(clone, deps);
116
+ const scores = bin === null ? null : stryker(base, { files: mine, bin }, deps);
117
+ return scores === null ? null : ran(scores);
118
+ }
119
+
120
+ /** The shard's scores from a run in a fresh clone under the OS temp dir, removed whether the run ends or throws. */
121
+ export function runInClone(plan: Plan, deps: MutateDeps): Ran | null {
122
+ const base = deps.fs.mkdtemp(TEMP_PREFIX);
123
+ try {
124
+ return mutateIn(base, plan, deps);
125
+ } finally {
126
+ deps.fs.rm(base);
127
+ }
128
+ }
@@ -0,0 +1,64 @@
1
+ /** The shard reports in a dir: read, and judged to be one complete run. */
2
+ import path from "node:path";
3
+ import type { MutateDeps } from "./mutate-deps.ts";
4
+ import { parseReport, type ShardReport } from "./mutate-report.ts";
5
+ import { JSON_SUFFIX, SHORT_SHA } from "./mutate.constants.ts";
6
+
7
+ /** `a and b`, `a, b and c`: two items or more. */
8
+ function listOf(items: readonly string[]): string {
9
+ const last = items.length - 1;
10
+ return `${items.slice(0, last).join(", ")} and ${items[last]}`;
11
+ }
12
+
13
+ const unique = <T>(items: readonly T[]): T[] => [...new Set(items)];
14
+ export const shortOf = (head: string): string => head.slice(0, SHORT_SHA);
15
+
16
+ type Check = (dir: string, reports: ShardReport[]) => string | null;
17
+
18
+ const mixedHeads: Check = (dir, reports) => {
19
+ const heads = unique(reports.map((report) => shortOf(report.head)));
20
+ return heads.length > 1 ? `${dir} holds shards of ${listOf(heads)}; use one run's reports` : null;
21
+ };
22
+
23
+ const mixedTotals: Check = (dir, reports) => {
24
+ const totals = unique(reports.map((report) => report.shard.total));
25
+ return totals.length > 1 ? `${dir} mixes runs of ${listOf(totals.map(String))} shards; use one run's reports` : null;
26
+ };
27
+
28
+ const repeated: Check = (dir, reports) => {
29
+ const seen = reports.map((report) => report.shard.index);
30
+ const twice = seen.find((index, at) => seen.indexOf(index) !== at);
31
+ return twice === undefined ? null : `${dir} holds shard ${twice} twice; use one run's reports`;
32
+ };
33
+
34
+ const missing: Check = (dir, reports) => {
35
+ const total = reports.reduce((_, report) => report.shard.total, 0);
36
+ const seen = reports.map((report) => report.shard.index);
37
+ const absent = Array.from({ length: total }, (_, at) => at + 1).filter((index) => !seen.includes(index));
38
+ if (absent.length === 0) return null;
39
+ const noun = absent.length > 1 ? "shards" : "shard";
40
+ return `${dir} is missing ${noun} ${absent.join(", ")} of ${total}; download every shard's report first`;
41
+ };
42
+
43
+ const CHECKS: readonly Check[] = [mixedHeads, mixedTotals, repeated, missing];
44
+
45
+ /** The reports in `dir` when they are one complete run; else the line that says what is wrong with them. */
46
+ export function loadShards(dir: string, deps: MutateDeps): ShardReport[] | string {
47
+ const full = path.resolve(deps.cwd, dir);
48
+ const names = (deps.fs.list(full) ?? []).filter((name) => name.endsWith(JSON_SUFFIX)).toSorted();
49
+ if (names.length === 0) {
50
+ const hint = `repo mutate --shard i/n --report ${dir}/mutation-i${JSON_SUFFIX}`;
51
+ return `${dir} holds no mutation report; run \`${hint}\` first`;
52
+ }
53
+ const reports: ShardReport[] = [];
54
+ for (const name of names) {
55
+ const parsed = parseReport(deps.fs.read(path.join(full, name)) ?? "");
56
+ if (!parsed.ok) return `${path.join(dir, name)} is not a mutation report: ${parsed.reason}`;
57
+ reports.push(parsed.value);
58
+ }
59
+ for (const check of CHECKS) {
60
+ const problem = check(dir, reports);
61
+ if (problem !== null) return problem;
62
+ }
63
+ return reports;
64
+ }
@@ -0,0 +1,67 @@
1
+ /** The names `repo mutate` and its report share. */
2
+
3
+ /** Prefix of the temp dir holding a run's clone, Stryker config and raw report. */
4
+ export const TEMP_PREFIX = "workerstack-mutate-";
5
+ /** The clone's dir inside the temp dir, so the config and the raw report stay outside it. */
6
+ export const CLONE_DIR = "repo";
7
+ export const CONFIG_NAME = "stryker.json";
8
+ export const RAW_REPORT_NAME = "stryker-report.json";
9
+ /** Where an unsharded run writes its report, relative to the repo root. */
10
+ export const DEFAULT_REPORT = ".tmp/repo/mutation/report.json";
11
+ /** The spaces a written JSON file is indented with. */
12
+ export const JSON_INDENT = 2;
13
+ export const OK = 0;
14
+ export const FAILED = 1;
15
+ export const SHORT_SHA = 7;
16
+ /** The words a flag and its value take. */
17
+ export const FLAG_WORDS = 2;
18
+ export const FIRST_SHARD = 1;
19
+ export const NEWLINE = "\n";
20
+ export const JSON_SUFFIX = ".json";
21
+
22
+ export const CHECK_COMMAND = "check";
23
+ export const SHARD_FLAG = "--shard";
24
+ export const REPORT_FLAG = "--report";
25
+ export const WRITE_FLAG = "--write";
26
+ export const FLAG_PREFIX = "--";
27
+ export const ASSIGN = "=";
28
+ export const SHARD_PATTERN: RegExp = /^(\d+)\/(\d+)$/;
29
+
30
+ /** The package manifests of the mutation tool, read from the clone's install. */
31
+ export const STRYKER_CORE = "@stryker-mutator/core";
32
+ export const STRYKER_RUNNER = "@stryker-mutator/vitest-runner";
33
+ export const MANIFEST = "package.json";
34
+ /** The dir `bun install` fills. */
35
+ export const INSTALLED_DIR = "node_modules";
36
+ export const INSTALL_HINT: string = `bun add -d ${STRYKER_CORE}@10.1.1 ${STRYKER_RUNNER}@10.1.1`;
37
+ export const INSTALL_ARGS: readonly string[] = ["install", "--frozen-lockfile"];
38
+ export const INSTALL_COMMAND = "bun install --frozen-lockfile";
39
+ /** The bun flags that run Stryker's node bin under bun with the preload. */
40
+ export const BUN_FLAGS: readonly string[] = ["--bun", "--preload"];
41
+ export const STRYKER_BIN_NAME = "stryker";
42
+ export const STRYKER_RUN = "run";
43
+
44
+ /** The globs a mutated file must not match, with the tests (`*.test.*`, `*.spec.*`) and declaration files. */
45
+ export const EXCLUDED: readonly RegExp[] = [/\.(test|spec)\.[cm]?[jt]sx?$/, /(^|\/)fixtures\//, /\.d\.ts$/];
46
+
47
+ /** The Stryker options this repo needs; see plan 55's Context for why each is set. */
48
+ export const STRYKER_OPTIONS = {
49
+ testRunner: "vitest",
50
+ reporters: ["json"],
51
+ coverageAnalysis: "perTest",
52
+ inPlace: true,
53
+ disableTypeChecks: false,
54
+ ignoreStatic: true,
55
+ vitest: { pool: "forks" },
56
+ } as const;
57
+
58
+ /** Stryker statuses that count as a kill, a survivor, and the rest, which no score counts. */
59
+ export const KILLED_STATUSES: readonly string[] = ["Killed", "Timeout"];
60
+ export const SURVIVOR_STATUSES: readonly string[] = ["Survived", "NoCoverage"];
61
+
62
+ /** The package a file belongs to when no workspace package dir holds it. */
63
+ export const ROOT_PACKAGE = ".";
64
+ export const PERCENT = 100;
65
+ export const PERCENT_DIGITS = 2;
66
+ export const SEPARATOR = "; ";
67
+ export const ARROW = "→";
@@ -0,0 +1,182 @@
1
+ import { expect, it } from "vitest";
2
+ import path from "node:path";
3
+ import { BASE, CLONE, OUT, ROOT, SHA, fakes, rawReport, sim } from "./fixtures/mutate-fakes.ts";
4
+ import { runMutate } from "./mutate.ts";
5
+
6
+ const SHARD = ["--shard", "1/2", "--report", OUT];
7
+ const INSTALL =
8
+ "repo mutate: install the mutation tool first: bun add -d @stryker-mutator/core@10.1.1 @stryker-mutator/vitest-runner@10.1.1\n";
9
+
10
+ it("clones HEAD shared, detaches to its sha, installs frozen, runs Stryker there and removes the clone", async () => {
11
+ const f = fakes();
12
+ expect(await runMutate(SHARD, f.deps)).toBe(0);
13
+ const git = f.calls.filter((c) => c.startsWith("git"));
14
+ expect(git).toEqual([
15
+ "git rev-parse HEAD",
16
+ "git status --porcelain",
17
+ "git ls-tree -r --name-only HEAD",
18
+ `git clone -q --shared ${ROOT} ${CLONE}`,
19
+ `git -C ${CLONE} checkout -q --detach ${SHA}`,
20
+ ]);
21
+ const order = f.calls.map((c) => c.split(" ")[0] + (c.includes("install") ? " install" : ""));
22
+ expect(order.slice(-3)).toEqual(["bun install", "bun", "rm"]);
23
+ expect(f.calls).toContain(`bun install --frozen-lockfile @${CLONE}`);
24
+ expect(f.calls.at(-1)).toBe(`rm ${BASE}`);
25
+ expect(f.calls.filter((c) => c.startsWith("mkdtemp"))).toEqual(["mkdtemp workerstack-mutate-"]);
26
+ });
27
+
28
+ it("runs the bin the clone's package names under bun with the preload, config outside the clone", async () => {
29
+ const f = fakes();
30
+ await runMutate(SHARD, f.deps);
31
+ const run = f.calls.find((c) => c.includes("stryker.js"));
32
+ const bin = path.join(CLONE, "node_modules/@stryker-mutator/core/bin/stryker.js");
33
+ expect(run).toBe(
34
+ `bun --bun --preload /pkg/cli/stryker-preload.ts ${bin} run ${path.join(BASE, "stryker.json")} @${CLONE}`,
35
+ );
36
+ });
37
+
38
+ it("writes a Stryker config with the shard's files and the options this repo needs", async () => {
39
+ const f = fakes();
40
+ let config: unknown;
41
+ const write = f.deps.fs.write;
42
+ f.deps.fs.write = (file, text) => {
43
+ if (file.endsWith("stryker.json")) config = JSON.parse(text);
44
+ write(file, text);
45
+ };
46
+ await runMutate(SHARD, f.deps);
47
+ expect(config).toEqual({
48
+ testRunner: "vitest",
49
+ mutate: ["src/a.ts"],
50
+ reporters: ["json"],
51
+ coverageAnalysis: "perTest",
52
+ inPlace: true,
53
+ disableTypeChecks: false,
54
+ ignoreStatic: true,
55
+ vitest: { pool: "forks" },
56
+ jsonReporter: { fileName: path.join(BASE, "stryker-report.json") },
57
+ });
58
+ });
59
+
60
+ it("removes the clone when Stryker fails, when git fails and when a step throws", async () => {
61
+ const failed = fakes(sim({ stryker: { code: 1, output: "x", report: {} } }));
62
+ expect(await runMutate(SHARD, failed.deps)).toBe(1);
63
+ expect(failed.calls.at(-1)).toBe(`rm ${BASE}`);
64
+ const cloneFails = fakes(sim({ gitFails: "clone" }));
65
+ expect(await runMutate(SHARD, cloneFails.deps)).toBe(1);
66
+ expect(cloneFails.err.join("")).toBe(`repo mutate: could not clone ${ROOT}: boom\n`);
67
+ expect(cloneFails.calls.at(-1)).toBe(`rm ${BASE}`);
68
+ const thrown = fakes();
69
+ thrown.deps.run = () => {
70
+ throw new Error("spawn died");
71
+ };
72
+ await expect(runMutate(SHARD, thrown.deps)).rejects.toThrow("spawn died");
73
+ expect(thrown.calls.at(-1)).toBe(`rm ${BASE}`);
74
+ });
75
+
76
+ it("names a failed detach and a failed install, and removes the clone", async () => {
77
+ const detach = fakes(sim({ gitFails: "--detach" }));
78
+ expect(await runMutate(SHARD, detach.deps)).toBe(1);
79
+ expect(detach.err.join("")).toBe(`repo mutate: could not check out ${SHA.slice(0, 7)} in the clone: boom\n`);
80
+ const install = fakes(sim({ installFails: true }));
81
+ expect(await runMutate(SHARD, install.deps)).toBe(1);
82
+ expect(install.err.join("")).toBe("repo mutate: bun install --frozen-lockfile failed in the clone: lock\n");
83
+ expect(install.calls.at(-1)).toBe(`rm ${BASE}`);
84
+ });
85
+
86
+ it("shards the tracked matches by lines, writes the shard report and prints its line", async () => {
87
+ const f = fakes();
88
+ expect(await runMutate(["--shard", "2/2", "--report", OUT], f.deps)).toBe(0);
89
+ expect(f.out.join("")).toBe(`mutate: HEAD ${SHA.slice(0, 7)}\nmutate: 1 file, shard 2/2, 0 survivors in ${OUT}\n`);
90
+ expect(JSON.parse(f.files.get(OUT) ?? "")).toEqual({
91
+ head: SHA,
92
+ shard: { index: 2, total: 2 },
93
+ files: { "src/b.ts": { killed: 1, survivors: [] } },
94
+ });
95
+ });
96
+
97
+ it("counts Survived and NoCoverage as survivors, Timeout as killed, and leaves out the rest", async () => {
98
+ const report = rawReport({
99
+ "src/a.ts": ["Killed", "Timeout", "Survived", "NoCoverage", "CompileError", "Ignored"],
100
+ });
101
+ const f = fakes(sim({ stryker: { code: 0, output: "", report } }));
102
+ await runMutate(["--shard", "1/2", "--report", OUT], f.deps);
103
+ expect(JSON.parse(f.files.get(OUT) ?? "")).toMatchObject({
104
+ files: {
105
+ "src/a.ts": {
106
+ killed: 2,
107
+ survivors: [
108
+ { line: 3, column: 2, mutator: "ArithmeticOperator", status: "Survived" },
109
+ { line: 4, column: 2, mutator: "ArithmeticOperator", status: "NoCoverage" },
110
+ ],
111
+ },
112
+ },
113
+ });
114
+ expect(f.out.join("")).toContain(`2 survivors in ${OUT}`);
115
+ });
116
+
117
+ it("writes an empty report for a shard past the files without installing or running Stryker", async () => {
118
+ const f = fakes(sim({ tracked: ["src/a.ts"] }));
119
+ expect(await runMutate(["--shard", "2/2", "--report", OUT], f.deps)).toBe(0);
120
+ expect(f.calls.some((c) => c.startsWith("bun"))).toBe(false);
121
+ expect(JSON.parse(f.files.get(OUT) ?? "")).toMatchObject({ shard: { index: 2, total: 2 }, files: {} });
122
+ expect(f.out.join("")).toContain("0 files, shard 2/2, 0 survivors");
123
+ });
124
+
125
+ it("refuses a report Stryker did not write, naming why", async () => {
126
+ const f = fakes(sim({ stryker: { code: 0, output: "", report: { files: 3 } } }));
127
+ expect(await runMutate(SHARD, f.deps)).toBe(1);
128
+ expect(f.err.join("")).toMatch(/^repo mutate: Stryker wrote no readable report: .+\n$/);
129
+ const none = fakes();
130
+ none.deps.run = () => ({ code: 0, stdout: "", stderr: "" });
131
+ expect(await runMutate(SHARD, none.deps)).toBe(1);
132
+ expect(none.err.join("")).toContain("Stryker wrote no readable report: the report file is missing");
133
+ });
134
+
135
+ it("notes that uncommitted changes are not mutated, only when there are some", async () => {
136
+ const dirty = fakes(sim({ dirty: true }));
137
+ await runMutate(SHARD, dirty.deps);
138
+ expect(dirty.out[0]).toBe(`mutate: HEAD ${SHA.slice(0, 7)} (uncommitted changes are not mutated)\n`);
139
+ const clean = fakes();
140
+ await runMutate(SHARD, clean.deps);
141
+ expect(clean.out[0]).toBe(`mutate: HEAD ${SHA.slice(0, 7)}\n`);
142
+ });
143
+
144
+ it("prints the install line when the clone has no Stryker", async () => {
145
+ const f = fakes(sim({ installed: false }));
146
+ expect(await runMutate(SHARD, f.deps)).toBe(1);
147
+ expect(f.err.join("")).toBe(INSTALL);
148
+ expect(f.calls.at(-1)).toBe(`rm ${BASE}`);
149
+ });
150
+
151
+ it("prints Stryker's lines under the failed dry run", async () => {
152
+ const f = fakes(sim({ stryker: { code: 1, output: "FAIL src/a.test.ts > adds\n", report: {} } }));
153
+ expect(await runMutate(SHARD, f.deps)).toBe(1);
154
+ expect(f.err.join("")).toBe(
155
+ "repo mutate: the tests failed before any mutant ran; fix them first\nFAIL src/a.test.ts > adds\n",
156
+ );
157
+ });
158
+
159
+ it("names the glob when no tracked file matches, before any clone", async () => {
160
+ const f = fakes(sim({ tracked: ["docs/x.ts"] }));
161
+ expect(await runMutate(SHARD, f.deps)).toBe(1);
162
+ expect(f.err.join("")).toBe(
163
+ "repo mutate: no tracked file matches coverage.mutate (src/**/*.ts); set coverage.mutate in workerstack.config.ts\n",
164
+ );
165
+ expect(f.calls.some((c) => c.includes("clone") || c.startsWith("mkdtemp"))).toBe(false);
166
+ });
167
+
168
+ it("refuses a bad --shard before touching git, and prints the config's errors", async () => {
169
+ const bad = fakes();
170
+ expect(await runMutate(["--shard", "9/8"], bad.deps)).toBe(1);
171
+ expect(bad.err.join("")).toBe("repo mutate: --shard takes i/n with 1 <= i <= n, such as 2/8\n");
172
+ expect(bad.calls).toEqual([]);
173
+ const noConfig = fakes(sim({ config: ["workerstack.config.ts: no default export"] }));
174
+ expect(await runMutate(SHARD, noConfig.deps)).toBe(1);
175
+ expect(noConfig.err.join("")).toBe("repo mutate: workerstack.config.ts: no default export\n");
176
+ });
177
+
178
+ it("says so when the directory is no git repository", async () => {
179
+ const f = fakes(sim({ gitFails: "rev-parse" }));
180
+ expect(await runMutate(SHARD, f.deps)).toBe(1);
181
+ expect(f.err.join("")).toBe(`repo mutate: ${ROOT} has no commit to mutate: boom\n`);
182
+ });
package/cli/mutate.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `repo mutate`: runs StrykerJS under bun, in place, in a throwaway `git clone --shared` of HEAD so no mutant can touch
3
+ * the checkout, over the tracked files matching `coverage.mutate` or shard `i/n` of them, and writes a report of every
4
+ * file's kills and survivors. A whole run holds each package to its floor in `coverage.mutationFloors`; a shard checks
5
+ * none, and `check <dir>` merges a run's shard reports and does. A typed path is relative to the current directory; the default report is under the repo root.
6
+ */
7
+ import path from "node:path";
8
+ import { parseArgs, type Shard } from "./mutate-args.ts";
9
+ import { checkReports } from "./check-reports.ts";
10
+ import { fail, type MutateConfig, type MutateDeps } from "./mutate-deps.ts";
11
+ import { selectFiles } from "./mutate-files.ts";
12
+ import { judgeRun, plural } from "./mutate-floors.ts";
13
+ import { realDeps } from "./mutate-real-deps.ts";
14
+ import { survivorCount } from "./mutate-report.ts";
15
+ import { runInClone } from "./mutate-run.ts";
16
+ import { DEFAULT_REPORT, FAILED, JSON_INDENT, NEWLINE, OK, SHORT_SHA } from "./mutate.constants.ts";
17
+
18
+ const NOTE = "(uncommitted changes are not mutated)";
19
+ const whole = (shard: Shard): boolean => shard.total === 1;
20
+
21
+ /** The commit to mutate and the tracked files to mutate in it, after saying the commit; or the failing exit code. */
22
+ function target(config: MutateConfig, deps: MutateDeps): { sha: string; files: string[] } | number {
23
+ const head = deps.git(config.root, ["rev-parse", "HEAD"]);
24
+ if (head.code !== 0) return fail(deps, `${config.root} has no commit to mutate: ${head.stderr.trim()}`);
25
+ const sha = head.stdout.trim();
26
+ const dirty = deps.git(config.root, ["status", "--porcelain"]).stdout.trim() !== "";
27
+ deps.out(`mutate: HEAD ${sha.slice(0, SHORT_SHA)}${dirty ? ` ${NOTE}` : ""}${NEWLINE}`);
28
+ const tracked = deps.git(config.root, ["ls-tree", "-r", "--name-only", "HEAD"]).stdout.split(NEWLINE);
29
+ const files = selectFiles(tracked, config.mutate);
30
+ if (files.length > 0) return { sha, files };
31
+ const globs = config.mutate.join(", ");
32
+ return fail(deps, `no tracked file matches coverage.mutate (${globs}); set coverage.mutate in workerstack.config.ts`);
33
+ }
34
+
35
+ async function runShard(shard: Shard, typed: string | undefined, deps: MutateDeps): Promise<number> {
36
+ const config = await deps.config(deps.cwd);
37
+ if (Array.isArray(config)) return fail(deps, ...config);
38
+ const found = target(config, deps);
39
+ if (typeof found === "number") return found;
40
+ if (whole(shard)) deps.out(`mutate: ${plural(found.files.length, "file")}, shard 1/1${NEWLINE}`);
41
+ const report = typed ?? DEFAULT_REPORT;
42
+ const reportFile = typed === undefined ? path.join(config.root, report) : path.resolve(deps.cwd, report);
43
+ const ran = runInClone({ root: config.root, sha: found.sha, files: found.files, shard }, deps);
44
+ if (ran === null) return FAILED;
45
+ const json = `${JSON.stringify(ran.report, null, JSON_INDENT)}${NEWLINE}`;
46
+ deps.fs.write(reportFile, json);
47
+ if (whole(shard)) return judgeRun({ report: ran.report, config, shown: report }, deps);
48
+ const files = plural(ran.files, "file");
49
+ const survivors = plural(survivorCount(ran.report.files), "survivor");
50
+ deps.out(`mutate: ${files}, shard ${shard.index}/${shard.total}, ${survivors} in ${report}${NEWLINE}`);
51
+ return OK;
52
+ }
53
+
54
+ /** `repo mutate` with its collaborators injected; the exit code. */
55
+ export function runMutate(argv: string[], deps: MutateDeps): Promise<number> {
56
+ const args = parseArgs(argv);
57
+ if (args.kind === "error") return Promise.resolve(fail(deps, args.line));
58
+ return args.kind === "check" ? checkReports(args.dir, args.write, deps) : runShard(args.shard, args.report, deps);
59
+ }
60
+
61
+ /** The registry entry: `repo mutate` in the current directory. */
62
+ export function main(argv: string[]): Promise<number> {
63
+ return runMutate(argv, realDeps(process.cwd()));
64
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Passed to every vitest that repo spawns inside a project. Vitest 5 keeps transformed modules in the workspace
3
+ * root's `.vitest-cache` and, at start, wipes the whole directory when the project's lockfile hash differs from the
4
+ * one recorded there (`ensureCacheIntegrity` in vitest's `cli-api` chunk). A project that links the packages of the
5
+ * run that spawned it shares that directory, so the nested run would delete entries the outer run is still reading
6
+ * and fail it with ENOENT at import.
7
+ */
8
+ export const NESTED_VITEST_FLAGS: readonly string[] = ["--fsModuleCache=false"];
@@ -0,0 +1,14 @@
1
+ /** The limits and texts of the `nightly` config section. */
2
+
3
+ export const MIN_SHARDS = 1;
4
+ export const MAX_SHARDS = 32;
5
+ /** A secret name: upper-case letters, digits and underscores, not starting with a digit. */
6
+ export const SECRET_NAME: RegExp = /^[A-Z_][A-Z0-9_]*$/;
7
+ /** Names GitHub reserves for itself. */
8
+ export const RESERVED_PREFIX = "GITHUB_";
9
+ export const SHARDS_ERROR: string = `must be a whole number from ${MIN_SHARDS} to ${MAX_SHARDS}`;
10
+ export const SECRET_ERROR: string = `must be a secret name like CLOUDFLARE_API_TOKEN, not starting with ${RESERVED_PREFIX}`;
11
+ export const SECRETS_ERROR = "must be a list of secret names";
12
+ export const SECTION_ERROR = "must be an object like { shards: 8, secrets: [...] }";
13
+ /** The repeat count that reads as "twice". */
14
+ export const TWICE = 2;