@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,150 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { afterEach, expect, it, vi } from "vitest";
4
+ import { BASE, OUT, STRYKER_PKG, at, fakes, sim } from "./fixtures/mutate-fakes.ts";
5
+ import { git, removePlanted, repoOnMain, tempDir } from "./fixtures/planted-repo.ts";
6
+ import { main, runMutate } from "./mutate.ts";
7
+ import { realDeps, realFs } from "./mutate-real-deps.ts";
8
+
9
+ afterEach(removePlanted);
10
+
11
+ it("runs git in its own repo with a hook's GIT_DIR and GIT_INDEX_FILE planted, and leaves the other repo alone", () => {
12
+ const other = repoOnMain({ "a.txt": "a\n" });
13
+ const source = repoOnMain({ "b.txt": "b\n" });
14
+ const planted = path.join(other, ".git", "planted-index");
15
+ vi.stubEnv("GIT_DIR", path.join(other, ".git"));
16
+ vi.stubEnv("GIT_INDEX_FILE", planted);
17
+ const deps = realDeps(source);
18
+ const clone = path.join(tempDir(), "clone");
19
+ const sha = deps.git(source, ["rev-parse", "HEAD"]).stdout.trim();
20
+ expect(sha).toBe(git(source, "rev-parse", "HEAD").trim());
21
+ expect(deps.git(source, ["clone", "-q", "--shared", source, clone]).code).toBe(0);
22
+ expect(deps.git(source, ["-C", clone, "checkout", "-q", "--detach", sha]).code).toBe(0);
23
+ expect(fs.readFileSync(path.join(clone, "b.txt"), "utf8").replaceAll("\r\n", "\n")).toBe("b\n"); // autocrlf may check out CRLF
24
+ expect(fs.existsSync(planted)).toBe(false);
25
+ vi.unstubAllEnvs();
26
+ expect(git(other, "status", "--porcelain")).toBe("");
27
+ expect(git(source, "status", "--porcelain")).toBe("");
28
+ });
29
+
30
+ it("reports a missing git or dir as a failed run instead of throwing", () => {
31
+ const run = realDeps("/").git(path.join(tempDir(), "missing"), ["status"]);
32
+ expect(run.code).not.toBe(0);
33
+ expect(run.stderr).not.toBe("");
34
+ });
35
+
36
+ it("reads and writes files, lists a dir, and tells a missing file and dir from an empty one", () => {
37
+ const real = realFs;
38
+ const dir = tempDir();
39
+ expect([real.read(path.join(dir, "x/y.json")), real.list(path.join(dir, "x"))]).toEqual([null, null]);
40
+ real.write(path.join(dir, "x/y.json"), "{}");
41
+ expect([real.read(path.join(dir, "x/y.json")), real.list(path.join(dir, "x"))]).toEqual(["{}", ["y.json"]]);
42
+ const base = real.mkdtemp("workerstack-mutate-test-");
43
+ expect(path.basename(base)).toMatch(/^workerstack-mutate-test-/);
44
+ real.rm(base);
45
+ expect(fs.existsSync(base)).toBe(false);
46
+ });
47
+
48
+ it("finds the config of the repo above cwd, its errors, and no config", async () => {
49
+ const root = tempDir();
50
+ fs.writeFileSync(path.join(root, "workerstack.config.ts"), 'export default { coverage: { mutate: ["lib/**"] } };\n');
51
+ fs.mkdirSync(path.join(root, "deep"));
52
+ expect(await realDeps(path.join(root, "deep")).config(path.join(root, "deep"))).toEqual({
53
+ root: fs.realpathSync(root),
54
+ mutate: ["lib/**"],
55
+ floors: "mutation-floors.json",
56
+ });
57
+ fs.writeFileSync(path.join(root, "workerstack.config.ts"), "export default { coverage: 5 };\n");
58
+ const bad = await realDeps(root).config(root);
59
+ expect(Array.isArray(bad) && bad[0]).toMatch(/^workerstack.config.ts: coverage/);
60
+ const none = await realDeps(tempDir()).config(tempDir());
61
+ expect(Array.isArray(none) && none[0]).toMatch(/^no workerstack.config.ts in/);
62
+ });
63
+
64
+ it("lists workspace package dirs, none for an unreadable manifest", () => {
65
+ const root = tempDir();
66
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ workspaces: ["packages/*"] }));
67
+ fs.mkdirSync(path.join(root, "packages/a"), { recursive: true });
68
+ fs.writeFileSync(path.join(root, "packages/a/package.json"), "{}");
69
+ expect(realDeps(root).packages(root)).toEqual(["packages/a"]);
70
+ fs.writeFileSync(path.join(root, "package.json"), "[");
71
+ expect(realDeps(root).packages(root)).toEqual([]);
72
+ });
73
+
74
+ const args = ["--shard", "1/2", "--report", OUT];
75
+
76
+ it.each(["{", "{}"])("treats a Stryker manifest %j as a missing install", async (manifest) => {
77
+ const f = fakes(sim());
78
+ const read = f.deps.fs.read;
79
+ f.deps.fs.read = (file) => (file === STRYKER_PKG ? manifest : read(file));
80
+ expect(await runMutate(args, f.deps)).toBe(1);
81
+ expect(f.err.join("")).toContain("install the mutation tool first");
82
+ });
83
+
84
+ it("takes a bin written as a string", async () => {
85
+ const f = fakes(sim());
86
+ const read = f.deps.fs.read;
87
+ f.deps.fs.read = (file) => (file === STRYKER_PKG ? '{"bin":"bin/s.js"}' : read(file));
88
+ expect(await runMutate(args, f.deps)).toBe(0);
89
+ expect(f.calls.some((c) => c.includes(path.join("core", "bin", "s.js")))).toBe(true);
90
+ });
91
+
92
+ it("refuses a raw report that is no JSON", async () => {
93
+ const f = fakes(sim());
94
+ f.deps.run = (a) => {
95
+ if (a[0] !== "install") f.deps.fs.write(path.join(BASE, "stryker-report.json"), "{");
96
+ return { code: 0, stdout: "", stderr: "" };
97
+ };
98
+ expect(await runMutate(args, f.deps)).toBe(1);
99
+ expect(f.err.join("")).toMatch(/Stryker wrote no readable report: .*JSON/);
100
+ });
101
+
102
+ it("says a failed install's stdout when its stderr is empty", async () => {
103
+ const f = fakes(sim());
104
+ f.deps.run = () => ({ code: 1, stdout: "lockfile had changes\n", stderr: "" });
105
+ expect(await runMutate(args, f.deps)).toBe(1);
106
+ expect(f.err.join("")).toBe("repo mutate: bun install --frozen-lockfile failed in the clone: lockfile had changes\n");
107
+ });
108
+
109
+ it("counts a tracked file the clone lacks as one empty line", async () => {
110
+ const f = fakes(sim({ tracked: ["src/a.ts", "src/b.ts", "src/gone.ts"] }));
111
+ expect(await runMutate(["--shard", "2/2", "--report", OUT], f.deps)).toBe(0);
112
+ expect(f.out.join("")).toContain("2 files, shard 2/2");
113
+ });
114
+
115
+ it("writes through the process streams, and main runs in the current directory", async () => {
116
+ const stdout = vi.spyOn(process.stdout, "write").mockReturnValue(true);
117
+ const stderr = vi.spyOn(process.stderr, "write").mockReturnValue(true);
118
+ try {
119
+ const deps = realDeps("/");
120
+ deps.out("to stdout\n");
121
+ deps.err("to stderr\n");
122
+ expect(await main(["--fast"])).toBe(1);
123
+ expect(stdout.mock.calls).toEqual([["to stdout\n"]]);
124
+ expect(stderr.mock.calls.map(([text]) => String(text))).toEqual([
125
+ "to stderr\n",
126
+ 'repo mutate: unknown argument "--fast"; use --shard i/n, --report <file> or check <dir>\n',
127
+ ]);
128
+ } finally {
129
+ stdout.mockRestore();
130
+ stderr.mockRestore();
131
+ }
132
+ });
133
+
134
+ it("resolves a typed --report and check dir against the current directory, the default report against the root", async () => {
135
+ const run = fakes(sim());
136
+ run.deps.cwd = at("sub");
137
+ expect(await runMutate(["--shard", "1/2", "--report", "out/m.json"], run.deps)).toBe(0);
138
+ expect(run.files.has(at("sub", "out", "m.json"))).toBe(true);
139
+ expect(run.out.join("")).toContain("in out/m.json");
140
+ const whole = fakes(sim());
141
+ whole.deps.cwd = at("sub");
142
+ await runMutate([], whole.deps);
143
+ expect(whole.files.has(at(".tmp/repo/mutation/report.json"))).toBe(true);
144
+ const report = JSON.stringify({ head: "09ed564aaaa", shard: { index: 1, total: 1 }, files: {} });
145
+ const check = fakes(sim());
146
+ check.deps.cwd = at("sub");
147
+ check.files.set(at("sub", "reports", "mutation-1.json"), report);
148
+ expect(await runMutate(["check", "reports"], check.deps)).toBe(0);
149
+ expect(check.out.join("")).toContain("1 shard of 09ed564");
150
+ });
@@ -0,0 +1,100 @@
1
+ import { expect, it } from "vitest";
2
+ import { parseArgs } from "./mutate-args.ts";
3
+ import { selectFiles, splitShards } from "./mutate-files.ts";
4
+
5
+ it("keeps the tracked files matching a glob, minus tests, fixtures and declaration files, sorted", () => {
6
+ const tracked = [
7
+ "src/z.ts",
8
+ "src/a.ts",
9
+ "src/a.test.ts",
10
+ "src/a.spec.tsx",
11
+ "src/fixtures/c.ts",
12
+ "src/deep/fixtures/d.ts",
13
+ "src/types.d.ts",
14
+ "lib/x.ts",
15
+ "docs/y.ts",
16
+ ];
17
+ expect(selectFiles(tracked, ["src/**/*.ts", "lib/*.ts"])).toEqual(["lib/x.ts", "src/a.ts", "src/z.ts"]);
18
+ });
19
+
20
+ it("returns nothing when no glob matches", () => {
21
+ expect(selectFiles(["a.ts"], ["src/**/*.ts"])).toEqual([]);
22
+ });
23
+
24
+ const lines = new Map([
25
+ ["a.ts", 50],
26
+ ["b.ts", 30],
27
+ ["c.ts", 30],
28
+ ["d.ts", 20],
29
+ ["e.ts", 10],
30
+ ]);
31
+
32
+ it("balances the shards by line count and keeps each shard sorted", () => {
33
+ const shards = [1, 2].map((index) => splitShards([...lines.keys()], lines, { index, total: 2 }));
34
+ expect(shards).toEqual([
35
+ ["a.ts", "d.ts"],
36
+ ["b.ts", "c.ts", "e.ts"],
37
+ ]);
38
+ });
39
+
40
+ it("gives every file to exactly one shard, whatever the order it arrives in", () => {
41
+ const names = [...lines.keys()].reverse();
42
+ const all = [1, 2, 3].flatMap((index) => splitShards(names, lines, { index, total: 3 }));
43
+ expect(all.toSorted()).toEqual([...lines.keys()]);
44
+ expect(splitShards(names, lines, { index: 1, total: 3 })).toEqual(
45
+ splitShards([...lines.keys()], lines, { index: 1, total: 3 }),
46
+ );
47
+ });
48
+
49
+ it("hands one shard everything when there is one, and nothing to a shard past the files", () => {
50
+ expect(splitShards([...lines.keys()], lines, { index: 1, total: 1 })).toHaveLength(5);
51
+ expect(splitShards(["a.ts"], lines, { index: 2, total: 2 })).toEqual([]);
52
+ });
53
+
54
+ it("defaults to one full run with the default report", () => {
55
+ expect(parseArgs([])).toEqual({
56
+ kind: "run",
57
+ shard: { index: 1, total: 1 },
58
+ report: undefined,
59
+ });
60
+ });
61
+
62
+ it("reads --shard and --report, as two words or with =", () => {
63
+ expect(parseArgs(["--shard", "2/8", "--report", "r/m.json"])).toEqual({
64
+ kind: "run",
65
+ shard: { index: 2, total: 8 },
66
+ report: "r/m.json",
67
+ });
68
+ expect(parseArgs(["--shard=3/4", "--report=x.json"])).toMatchObject({ shard: { index: 3, total: 4 } });
69
+ });
70
+
71
+ it.each(["0/8", "9/8", "a/b", "2", "2/0", "1/2/3", ""])("refuses --shard %j", (value) => {
72
+ expect(parseArgs(["--shard", value])).toEqual({
73
+ kind: "error",
74
+ line: "--shard takes i/n with 1 <= i <= n, such as 2/8",
75
+ });
76
+ });
77
+
78
+ it("refuses --shard and --report without a value", () => {
79
+ expect(parseArgs(["--shard"])).toMatchObject({ kind: "error" });
80
+ expect(parseArgs(["--report"])).toEqual({ kind: "error", line: "--report takes a file path" });
81
+ });
82
+
83
+ it("reads check <dir> with and without --write", () => {
84
+ expect(parseArgs(["check", "reports"])).toEqual({ kind: "check", dir: "reports", write: false });
85
+ expect(parseArgs(["check", "reports", "--write"])).toEqual({ kind: "check", dir: "reports", write: true });
86
+ expect(parseArgs(["check", "--write", "reports"])).toEqual({ kind: "check", dir: "reports", write: true });
87
+ });
88
+
89
+ it("refuses check without a dir and anything it does not know", () => {
90
+ expect(parseArgs(["check"])).toEqual({ kind: "error", line: "check takes the dir of the shard reports" });
91
+ expect(parseArgs(["--fast"])).toEqual({
92
+ kind: "error",
93
+ line: 'unknown argument "--fast"; use --shard i/n, --report <file> or check <dir>',
94
+ });
95
+ });
96
+
97
+ it("weighs a file with no line count as nothing", () => {
98
+ const known = new Map([["a.ts", 5]]);
99
+ expect(splitShards(["a.ts", "ghost.ts"], known, { index: 2, total: 2 })).toEqual(["ghost.ts"]);
100
+ });
@@ -0,0 +1,31 @@
1
+ /** Which files `repo mutate` mutates, and how a run splits them into shards. */
2
+ import path from "node:path";
3
+ import { EXCLUDED } from "./mutate.constants.ts";
4
+ import type { Shard } from "./mutate-args.ts";
5
+
6
+ /** The tracked files matching one of `globs`, minus tests, fixtures and declaration files, sorted. */
7
+ export function selectFiles(tracked: readonly string[], globs: readonly string[]): string[] {
8
+ return tracked
9
+ .filter((file) => globs.some((glob) => path.matchesGlob(file, glob)))
10
+ .filter((file) => !EXCLUDED.some((pattern) => pattern.test(file)))
11
+ .toSorted();
12
+ }
13
+
14
+ /** A shard being filled: its place, the lines handed to it so far and its files. */
15
+ type Bin = { index: number; load: number; taken: string[] };
16
+
17
+ /**
18
+ * The files of one shard: the sorted files, heaviest first, each handed to the lightest shard so far (the lowest
19
+ * index on a tie), so the same list always splits the same way and the shards stay near equal in lines.
20
+ */
21
+ export function splitShards(files: readonly string[], lines: ReadonlyMap<string, number>, shard: Shard): string[] {
22
+ const size = (file: string) => lines.get(file) ?? 0;
23
+ const heaviest = files.toSorted((a, b) => size(b) - size(a) || a.localeCompare(b));
24
+ const shards = Array.from({ length: shard.total }, (_, at): Bin => ({ index: at + 1, load: 0, taken: [] }));
25
+ for (const file of heaviest) {
26
+ const lightest = shards.reduce((best, next) => (next.load < best.load ? next : best));
27
+ lightest.load += size(file);
28
+ lightest.taken.push(file);
29
+ }
30
+ return shards.flatMap((one) => (one.index === shard.index ? one.taken.toSorted() : []));
31
+ }
@@ -0,0 +1,91 @@
1
+ import { expect, it } from "vitest";
2
+ import { ROOT, at, fakes, rawReport, sim } from "./fixtures/mutate-fakes.ts";
3
+ import { runMutate } from "./mutate.ts";
4
+ import { packageOf, percentOf, tally } from "./mutate-floors.ts";
5
+
6
+ const survivor = { line: 1, column: 1, mutator: "m", status: "Survived" } as const;
7
+
8
+ it("scores killed over killed plus survivors, so errors, ignored and compile errors weigh nothing", () => {
9
+ const files = {
10
+ "a.ts": { killed: 8, survivors: [survivor, { ...survivor, status: "NoCoverage" as const }] },
11
+ };
12
+ const [score] = [...tally(files, []).values()];
13
+ expect(percentOf(score ?? { killed: 0, survivors: 0 })).toBe(80);
14
+ });
15
+
16
+ it("scores a package with no scored mutant as 100", () => {
17
+ expect(percentOf({ killed: 0, survivors: 0 })).toBe(100);
18
+ });
19
+
20
+ it("attributes a file to the deepest package dir holding it, else the root", () => {
21
+ const packages = ["packages/a", "packages/a/inner", "packages/ab"];
22
+ expect(packageOf("packages/a/src/x.ts", packages)).toBe("packages/a");
23
+ expect(packageOf("packages/a/inner/x.ts", packages)).toBe("packages/a/inner");
24
+ expect(packageOf("packages/ab/x.ts", packages)).toBe("packages/ab");
25
+ expect(packageOf("packages/other/x.ts", packages)).toBe(".");
26
+ const files = { "packages/a/x.ts": { killed: 1, survivors: [] }, "y.ts": { killed: 3, survivors: [survivor] } };
27
+ expect(Object.fromEntries(tally(files, packages))).toEqual({
28
+ "packages/a": { killed: 1, survivors: 0 },
29
+ ".": { killed: 3, survivors: 1 },
30
+ });
31
+ });
32
+
33
+ const TRACKED = ["packages/a/src/x.ts", "src/y.ts"];
34
+ const FILES = { "packages/a/src/x.ts": "x\n", "src/y.ts": "y\n" };
35
+ const REPORT = ".tmp/repo/mutation/report.json";
36
+
37
+ function unsharded(floors: Record<string, number> | string | null) {
38
+ const report = rawReport({
39
+ "packages/a/src/x.ts": ["Killed", "Killed", "Timeout", "Survived", "CompileError"],
40
+ "src/y.ts": ["Killed", "NoCoverage"],
41
+ });
42
+ const config = { root: ROOT, mutate: ["**/src/*.ts"], floors: "mf.json" };
43
+ const f = fakes(sim({ tracked: TRACKED, sources: FILES, config, stryker: { code: 0, output: "", report } }), [
44
+ "packages/a",
45
+ ]);
46
+ if (floors !== null) f.files.set(at("mf.json"), typeof floors === "string" ? floors : JSON.stringify(floors));
47
+ return f;
48
+ }
49
+
50
+ it("prints each package's score against its floor and passes at or above every floor", async () => {
51
+ const f = unsharded({ ".": 50, "packages/a": 75 });
52
+ expect(await runMutate([], f.deps)).toBe(0);
53
+ expect(f.out.slice(1)).toEqual([
54
+ "mutate: 2 files, shard 1/1\n",
55
+ `mutate: . 50.00% (floor 50), 1 survivor in ${REPORT}\n`,
56
+ `mutate: packages/a 75.00% (floor 75), 1 survivor in ${REPORT}\n`,
57
+ ]);
58
+ expect(f.err).toEqual([]);
59
+ expect(JSON.parse(f.files.get(at(REPORT)) ?? "")).toMatchObject({ shard: { index: 1, total: 1 } });
60
+ });
61
+
62
+ it("fails a package below its floor, naming the floors file and the report", async () => {
63
+ const f = unsharded({ ".": 51, "packages/a": 75 });
64
+ expect(await runMutate([], f.deps)).toBe(1);
65
+ expect(f.err).toEqual([
66
+ `repo mutate: . scored 50.00%, below its floor of 51 in mf.json; see the survivors in ${REPORT}\n`,
67
+ ]);
68
+ });
69
+
70
+ it("holds a package with no floor to none, and runs with no floors file", async () => {
71
+ const f = unsharded(null);
72
+ expect(await runMutate([], f.deps)).toBe(0);
73
+ expect(f.out[2]).toBe(`mutate: . 50.00% (no floor), 1 survivor in ${REPORT}\n`);
74
+ });
75
+
76
+ it("refuses a floors file that is not a table of numbers", async () => {
77
+ const bad = unsharded('{ ".": "high" }');
78
+ expect(await runMutate([], bad.deps)).toBe(1);
79
+ expect(bad.err.join("")).toMatch(/^repo mutate: mf.json is not a floors file: .+\n$/);
80
+ const json = unsharded("{");
81
+ expect(await runMutate([], json.deps)).toBe(1);
82
+ expect(json.err.join("")).toContain("mf.json is not a floors file");
83
+ });
84
+
85
+ it("treats --shard 1/1 as the whole run and checks no floor for a real shard", async () => {
86
+ const whole = unsharded({ ".": 99 });
87
+ expect(await runMutate(["--shard", "1/1"], whole.deps)).toBe(1);
88
+ const part = unsharded({ ".": 99 });
89
+ expect(await runMutate(["--shard", "1/2"], part.deps)).toBe(0);
90
+ expect(part.err).toEqual([]);
91
+ });
@@ -0,0 +1,104 @@
1
+ /** Scores per package and the floors they are held to: the formula, the floors file, and one run's verdict. */
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import { fail, type MutateConfig, type MutateDeps } from "./mutate-deps.ts";
5
+ import { parseJson, type FileScore, type Parsed, type ShardReport } from "./mutate-report.ts";
6
+ import { ARROW, NEWLINE, OK, PERCENT, PERCENT_DIGITS, ROOT_PACKAGE } from "./mutate.constants.ts";
7
+
8
+ /** A package's mutants: how many tests killed and how many lived. */
9
+ export type Tally = { killed: number; survivors: number };
10
+ /** The mutation score floors by package dir. */
11
+ export type Floors = Record<string, number>;
12
+ /** A package's score against its floor. */
13
+ export type Verdict = { pkg: string; percent: number; floor: number | undefined; survivors: number };
14
+
15
+ const buildFloorsSchema = () => z.record(z.string(), z.number());
16
+ const floorsSchema = /* @__PURE__ */ buildFloorsSchema();
17
+
18
+ /** (killed) / (killed + survivors) as a percent; a package with no scored mutant scores 100. */
19
+ export function percentOf({ killed, survivors }: Tally): number {
20
+ const scored = killed + survivors;
21
+ return scored === 0 ? PERCENT : (killed / scored) * PERCENT;
22
+ }
23
+
24
+ /** The deepest package dir holding `file`, else the root. */
25
+ export function packageOf(file: string, packages: readonly string[]): string {
26
+ const holding = packages.filter((dir) => file.startsWith(`${dir}/`));
27
+ return holding.toSorted((a, b) => b.length - a.length)[0] ?? ROOT_PACKAGE;
28
+ }
29
+
30
+ /** Each package's totals over `files`. */
31
+ export function tally(files: Record<string, FileScore>, packages: readonly string[]): Map<string, Tally> {
32
+ const totals = new Map<string, Tally>();
33
+ for (const [file, score] of Object.entries(files)) {
34
+ const pkg = packageOf(file, packages);
35
+ const was = totals.get(pkg) ?? { killed: 0, survivors: 0 };
36
+ totals.set(pkg, { killed: was.killed + score.killed, survivors: was.survivors + score.survivors.length });
37
+ }
38
+ return totals;
39
+ }
40
+
41
+ /** The floors in `file` under `root`: none when it is missing, the reason when it is no table of numbers. */
42
+ export function readFloors(deps: MutateDeps, root: string, file: string): Parsed<Floors> {
43
+ const text = deps.fs.read(path.join(root, file));
44
+ if (text === null) return { ok: true, value: {} };
45
+ const json = parseJson(text);
46
+ if (!json.ok) return json;
47
+ const parsed = floorsSchema.safeParse(json.value);
48
+ return parsed.success
49
+ ? { ok: true, value: parsed.data }
50
+ : { ok: false, reason: parsed.error.issues.map((issue) => issue.message).join("; ") };
51
+ }
52
+
53
+ /** Every package's score against its floor, sorted by package. */
54
+ export function verdicts(totals: Map<string, Tally>, floors: Floors): Verdict[] {
55
+ return [...totals]
56
+ .toSorted(([a], [b]) => a.localeCompare(b))
57
+ .map(([pkg, t]) => ({ pkg, percent: percentOf(t), floor: floors[pkg], survivors: t.survivors }));
58
+ }
59
+
60
+ const isBelow = (v: Verdict): boolean => v.floor !== undefined && v.percent < v.floor;
61
+ const percentText = (v: Verdict): string => `${v.percent.toFixed(PERCENT_DIGITS)}%`;
62
+ const floorOf = (v: Verdict): number => Math.floor(v.percent);
63
+
64
+ /** `pkg 89.86% (floor 89)`, with the floor's move when it is `raise`d: `(floor 85 → 89)`. */
65
+ export function label(v: Verdict, raise: boolean): string {
66
+ const moved = raise && v.floor !== floorOf(v) && !isBelow(v);
67
+ const floor = v.floor === undefined ? "no floor" : `floor ${v.floor}`;
68
+ return `${v.pkg} ${percentText(v)} (${moved ? `floor ${v.floor ?? "new"} ${ARROW} ${floorOf(v)}` : floor})`;
69
+ }
70
+
71
+ /** One line per package below its floor, naming the floors file and where the survivors are. */
72
+ export function failures(vs: Verdict[], file: string, where: string): string[] {
73
+ return vs
74
+ .filter(isBelow)
75
+ .map(
76
+ (v) =>
77
+ `${v.pkg} scored ${percentText(v)}, below its floor of ${v.floor} in ${file}; see the survivors in ${where}`,
78
+ );
79
+ }
80
+
81
+ /** The floors with each package raised to its whole score; none lowered, none dropped. */
82
+ export function raised(floors: Floors, vs: Verdict[]): Floors {
83
+ return { ...floors, ...Object.fromEntries(vs.map((v) => [v.pkg, Math.max(v.floor ?? 0, floorOf(v))])) };
84
+ }
85
+
86
+ /** `1 survivor`, `2 survivors`. */
87
+ export const plural = (n: number, word: string): string => `${n} ${word}${n === 1 ? "" : "s"}`;
88
+
89
+ /** A whole run: its report, the config it ran under and the report path as the person typed it. */
90
+ export type Judged = { report: ShardReport; config: MutateConfig; shown: string };
91
+
92
+ /** An unsharded run's verdict: each package's line, then a line per package below its floor; 1 if any. */
93
+ export function judgeRun(run: Judged, deps: MutateDeps): number {
94
+ const { report, config, shown } = run;
95
+ const floors = readFloors(deps, config.root, config.floors);
96
+ if (!floors.ok) return fail(deps, `${config.floors} is not a floors file: ${floors.reason}`);
97
+ const totals = tally(report.files, deps.packages(config.root));
98
+ const vs = verdicts(totals, floors.value);
99
+ for (const v of vs) {
100
+ deps.out(`mutate: ${label(v, false)}, ${plural(v.survivors, "survivor")} in ${shown}${NEWLINE}`);
101
+ }
102
+ const lines = failures(vs, config.floors, shown);
103
+ return lines.length > 0 ? fail(deps, ...lines) : OK;
104
+ }
@@ -0,0 +1,57 @@
1
+ /** The real collaborators of `repo mutate`: git, bun, the file system, the config and the workspace. */
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { CONFIG_FILE, findRoot, loadConfig } from "./config.ts";
6
+ import { childEnv } from "./env.ts";
7
+ import type { MutateDeps, MutateFs } from "./mutate-deps.ts";
8
+ import { captureBun, toolEnv, type Captured } from "./scripts/lint.ts";
9
+ import { spawnSync } from "./spawn.ts";
10
+ import { readWorkspace } from "./sync/workspace.ts";
11
+
12
+ /** `git <args>` in `cwd`, run without any `GIT_*` variable a hook set, which would aim it at another repo. */
13
+ function git(cwd: string, args: string[]): Captured {
14
+ const run = spawnSync("git", args, { cwd, encoding: "utf8", env: toolEnv(childEnv()) });
15
+ return { code: run.status ?? 1, stdout: run.stdout, stderr: run.error?.message ?? run.stderr };
16
+ }
17
+
18
+ export const realFs: MutateFs = {
19
+ mkdtemp: (prefix) => fs.mkdtempSync(path.join(os.tmpdir(), prefix)),
20
+ rm: (target) => fs.rmSync(target, { recursive: true, force: true }),
21
+ read: (file) => (fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null),
22
+ write: (file, text) => {
23
+ fs.mkdirSync(path.dirname(file), { recursive: true });
24
+ fs.writeFileSync(file, text);
25
+ },
26
+ list: (dir) => (fs.existsSync(dir) ? fs.readdirSync(dir) : null),
27
+ };
28
+
29
+ /** The root and the `coverage` keys of the repo holding `cwd`, or the lines that say why there is none. */
30
+ async function config(cwd: string): ReturnType<MutateDeps["config"]> {
31
+ const root = findRoot(cwd);
32
+ if (root === null) return [`no ${CONFIG_FILE} in ${cwd} or any parent; run \`bun repo init\``];
33
+ const loaded = await loadConfig(root);
34
+ if (!loaded.ok) return loaded.errors;
35
+ return { root, mutate: loaded.config.coverage.mutate, floors: loaded.config.coverage.mutationFloors };
36
+ }
37
+
38
+ /** The workspace package dirs of `root`; an unreadable manifest is `repo check`'s to report, so none here. */
39
+ function packages(root: string): string[] {
40
+ const read = readWorkspace(root);
41
+ return read.ok ? read.workspace.packages.map((pkg) => pkg.dir) : [];
42
+ }
43
+
44
+ /** The collaborators of a run in `cwd`. */
45
+ export function realDeps(cwd: string): MutateDeps {
46
+ return {
47
+ cwd,
48
+ config,
49
+ packages,
50
+ git,
51
+ run: (args, where) => captureBun(args, where, childEnv()),
52
+ fs: realFs,
53
+ preload: path.join(import.meta.dirname, "stryker-preload.ts"),
54
+ out: (text) => process.stdout.write(text),
55
+ err: (text) => process.stderr.write(text),
56
+ };
57
+ }
@@ -0,0 +1,96 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, expect, it } from "vitest";
5
+ import { childEnv } from "./env.ts";
6
+ import { git, removePlanted, tempDir, write } from "./fixtures/planted-repo.ts";
7
+ import { runMutate } from "./mutate.ts";
8
+ import { realDeps } from "./mutate-real-deps.ts";
9
+ import { toolEnv } from "./scripts/lint.ts";
10
+ import { spawnSync } from "./spawn.ts";
11
+
12
+ afterEach(removePlanted);
13
+
14
+ const SLOW = 120_000;
15
+ const REPORT = ".tmp/repo/mutation/report.json";
16
+ const TOOL = "10.1.1";
17
+ const VITEST = "^5.1.0";
18
+ const MATH = [
19
+ "export const add = (a: number, b: number): number => a + b;",
20
+ "export const sub = (a: number, b: number): number => a - b;",
21
+ "",
22
+ ].join("\n");
23
+ const MATH_TEST = [
24
+ 'import { expect, it } from "vitest";',
25
+ 'import { add, sub } from "./math.ts";',
26
+ 'it("adds", () => expect(add(2, 3)).toBe(5));',
27
+ 'it("subtracts, weakly", () => expect(typeof sub(5, 3)).toBe("number"));',
28
+ "",
29
+ ].join("\n");
30
+ const STR = "export const shout = (text: string): string => text.toUpperCase();\n";
31
+ const STR_TEST = [
32
+ 'import { expect, it } from "vitest";',
33
+ 'import { shout } from "./str.ts";',
34
+ 'it("shouts", () => expect(shout("a")).toBe("A"));',
35
+ "",
36
+ ].join("\n");
37
+
38
+ /** Two sources and one weak test, committed with an installed lockfile, so a clone installs frozen from bun's cache. */
39
+ function plantedRepo(): string {
40
+ const dir = tempDir();
41
+ const manifest = {
42
+ name: "planted",
43
+ private: true,
44
+ type: "module",
45
+ devDependencies: {
46
+ vitest: VITEST,
47
+ "@stryker-mutator/core": TOOL,
48
+ "@stryker-mutator/vitest-runner": TOOL,
49
+ },
50
+ };
51
+ write(dir, {
52
+ "package.json": manifest,
53
+ ".gitignore": "node_modules\n.tmp/\n",
54
+ "workerstack.config.ts": 'export default { packs: [], coverage: { mutate: ["src/**/*.ts"] } };\n',
55
+ "mutation-floors.json": { ".": 99 },
56
+ "src/math.ts": MATH,
57
+ "src/math.test.ts": MATH_TEST,
58
+ "src/str.ts": STR,
59
+ "src/str.test.ts": STR_TEST,
60
+ });
61
+ const installed = spawnSync(process.execPath, ["install"], { cwd: dir, encoding: "utf8", env: toolEnv(childEnv()) });
62
+ expect(installed.status, installed.stderr).toBe(0);
63
+ git(dir, "init", "-q", "-b", "main");
64
+ git(dir, "add", "-A");
65
+ git(dir, "commit", "-q", "-m", "planted");
66
+ return dir;
67
+ }
68
+
69
+ type Report = { files: Record<string, { survivors: { line: number; mutator: string }[] }> };
70
+
71
+ const clones = (): string[] => fs.readdirSync(os.tmpdir()).filter((name) => name.startsWith("workerstack-mutate-"));
72
+
73
+ it(
74
+ "runs real Stryker through repo mutate: reports the weak test's survivor and score, fails a floor above it, leaves the tree clean",
75
+ { timeout: SLOW },
76
+ async () => {
77
+ const dir = plantedRepo();
78
+ const before = clones();
79
+ const out: string[] = [];
80
+ const err: string[] = [];
81
+ const deps = { ...realDeps(dir), out: (t: string) => out.push(t), err: (t: string) => err.push(t) };
82
+ const code = await runMutate([], deps);
83
+ expect(err.join(""), out.join("")).toMatch(
84
+ /^repo mutate: \. scored \d+\.\d\d%, below its floor of 99 in mutation-floors\.json/,
85
+ );
86
+ expect(code).toBe(1);
87
+ expect(out.join("")).toMatch(/mutate: 2 files, shard 1\/1\nmutate: \. \d+\.\d\d% \(floor 99\), 1 survivor in /);
88
+ const text = fs.readFileSync(path.join(dir, REPORT), "utf8");
89
+ const report = JSON.parse(text) /* the shard report this command wrote */ as Report;
90
+ const survivors = (file: string) => report.files[file]?.survivors.map((s) => [s.line, s.mutator]);
91
+ expect(survivors("src/math.ts")).toEqual([[2, "ArithmeticOperator"]]);
92
+ expect(survivors("src/str.ts")).toEqual([]);
93
+ expect(git(dir, "status", "--porcelain")).toBe("");
94
+ expect(clones()).toEqual(before);
95
+ },
96
+ );