@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,193 @@
1
+ /**
2
+ * The ratchets `repo check` holds against main: coverage floors only rise (a rename carries its floor), the
3
+ * known-failures list only shrinks, a mutation floor never drops, and a lint rule's baseline total never rises for a
4
+ * rule main already baselines.
5
+ * Main's copies are read from git; no main ref, or a file absent on main, means nothing to compare.
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { spawnSync } from "node:child_process";
10
+ import {
11
+ COMMON_BRANCHES,
12
+ DEFAULT_FLOORS,
13
+ DEFAULT_MUTATION_FLOORS,
14
+ DETACHED,
15
+ NEWLINE,
16
+ ORIGIN,
17
+ RENAME,
18
+ RENAME_COLUMNS,
19
+ TAB,
20
+ } from "./check-ratchets.constants.ts";
21
+ import { DEFAULT_LIST } from "./known-failures.constants.ts";
22
+ import { childEnv } from "./env.ts";
23
+ import { toolEnv } from "./scripts/lint.ts";
24
+ import { BASELINE_PATH } from "./scripts/lint-baseline.ts";
25
+ import { defaultBranch } from "./scripts/worktree.ts";
26
+
27
+ /** Where the ratcheted files live, as the config's `coverage` section names them. */
28
+ export type RatchetPaths = { floors?: string; knownFailures?: string; mutationFloors?: string };
29
+
30
+ type Table = Record<string, Record<string, number>>;
31
+
32
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
33
+ typeof value === "object" && value !== null && !Array.isArray(value);
34
+
35
+ /** git's stdout, or null when it failed; run without any `GIT_*` variable a hook set. */
36
+ function git(root: string, args: string[]): string | null {
37
+ const run = spawnSync("git", args, { cwd: root, encoding: "utf8", env: toolEnv(childEnv()) });
38
+ return run.status === 0 ? run.stdout : null;
39
+ }
40
+
41
+ const hasRef = (root: string, ref: string) =>
42
+ git(root, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]) !== null;
43
+
44
+ /** The branches a clone without a local main may still have as `origin/<name>`. */
45
+ function originNames(root: string): string[] {
46
+ const head = git(root, ["symbolic-ref", "--short", `refs/remotes/${ORIGIN}/HEAD`]);
47
+ return [head?.trim().replace(`${ORIGIN}/`, "") ?? "", ...COMMON_BRANCHES].filter(Boolean);
48
+ }
49
+
50
+ /**
51
+ * Main's ref: the local default branch, else `origin/<name>`. The default branch falling back to the current branch
52
+ * (or HEAD) is no main, since that compares a branch with itself.
53
+ */
54
+ function mainRef(root: string): string | null {
55
+ const name = defaultBranch(root);
56
+ const current = git(root, ["symbolic-ref", "--short", "HEAD"])?.trim();
57
+ if (name !== DETACHED && name !== current && hasRef(root, name)) return name;
58
+ const remote = [name, ...originNames(root)].map((n) => `${ORIGIN}/${n}`);
59
+ return remote.find((ref) => hasRef(root, ref)) ?? null;
60
+ }
61
+
62
+ function parse(text: string | null): unknown {
63
+ try {
64
+ return text === null ? undefined : JSON.parse(text);
65
+ } catch {
66
+ return undefined;
67
+ }
68
+ }
69
+
70
+ const readMain = (root: string, ref: string, rel: string): unknown => parse(git(root, ["show", `${ref}:${rel}`]));
71
+ const readHere = (root: string, rel: string): unknown => {
72
+ const file = path.join(root, rel);
73
+ return parse(fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null);
74
+ };
75
+
76
+ function asTable(value: unknown): Table | undefined {
77
+ if (!isRecord(value)) return undefined;
78
+ const table: Table = {};
79
+ for (const [key, row] of Object.entries(value)) {
80
+ if (!isRecord(row)) continue;
81
+ table[key] = Object.fromEntries(Object.entries(row).filter((e): e is [string, number] => typeof e[1] === "number"));
82
+ }
83
+ return table;
84
+ }
85
+
86
+ const asList = (value: unknown): string[] | undefined =>
87
+ Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : undefined;
88
+
89
+ /** Old path → new path of the renames between main and the working tree. */
90
+ function renames(root: string, ref: string): Map<string, string> {
91
+ // A failed diff reads as the text "null", which holds no rename.
92
+ const out = String(git(root, ["diff", "-M", "--name-status", ref]));
93
+ const pairs = out
94
+ .split(NEWLINE)
95
+ .map((line) => line.split(TAB))
96
+ .filter(
97
+ (cols): cols is [string, string, string] =>
98
+ cols.length === RENAME_COLUMNS && cols[0]?.startsWith(RENAME) === true,
99
+ );
100
+ return new Map(pairs.map(([, from, to]) => [from, to]));
101
+ }
102
+
103
+ /** One main file's floors against the checkout's, `rel` being its key on main and `key` its key here. */
104
+ type Row = { rel: string; key: string; file: string };
105
+
106
+ function fileFloors({ rel, key, file }: Row, was: Record<string, number>, now: Record<string, number>): string[] {
107
+ const note = key === rel ? "" : ` (renamed from ${rel})`;
108
+ return Object.entries(was).flatMap(([metric, floor]) => {
109
+ const has = now[metric];
110
+ if (has === undefined) return [`${file}: ${key} ${metric} has no floor but main has ${floor}${note}; restore it`];
111
+ return has < floor ? [`${file}: ${key} ${metric} ${has} is below main's ${floor}${note}; floors only rise`] : [];
112
+ });
113
+ }
114
+
115
+ function floorProblems(root: string, ref: string, file: string): string[] {
116
+ const was = asTable(readMain(root, ref, file));
117
+ if (was === undefined) return [];
118
+ const now = asTable(readHere(root, file)) ?? {};
119
+ const moved = renames(root, ref);
120
+ return Object.entries(was).flatMap(([rel, metrics]) => {
121
+ const key = rel in now ? rel : (moved.get(rel) ?? rel);
122
+ const row = now[key];
123
+ if (row !== undefined) return fileFloors({ rel, key, file }, metrics, row);
124
+ return fs.existsSync(path.join(root, key))
125
+ ? [`${file}: ${key} has no floor but main has one and the file exists; restore it`]
126
+ : [];
127
+ });
128
+ }
129
+
130
+ const asNumbers = (value: unknown): Record<string, number> | undefined =>
131
+ isRecord(value)
132
+ ? Object.fromEntries(Object.entries(value).filter((e): e is [string, number] => typeof e[1] === "number"))
133
+ : undefined;
134
+
135
+ /** A mutation floor lowered, or dropped while its package dir exists; a package dir that is gone takes its floor. */
136
+ function mutationProblems(root: string, ref: string, file: string): string[] {
137
+ const was = asNumbers(readMain(root, ref, file));
138
+ if (was === undefined) return [];
139
+ const now = asNumbers(readHere(root, file)) ?? {};
140
+ return Object.entries(was).flatMap(([pkg, floor]) => {
141
+ const has = now[pkg];
142
+ if (has === undefined) {
143
+ return fs.existsSync(path.join(root, pkg)) ? [`${file} drops ${pkg} from ${floor}; floors only rise`] : [];
144
+ }
145
+ return has < floor ? [`${file} lowers ${pkg} from ${floor} to ${has} against main; floors only rise`] : [];
146
+ });
147
+ }
148
+
149
+ function listProblems(root: string, ref: string, file: string): string[] {
150
+ const was = asList(readMain(root, ref, file));
151
+ if (was === undefined) return [];
152
+ const now = asList(readHere(root, file)) ?? [];
153
+ return now
154
+ .filter((entry) => !was.includes(entry))
155
+ .map((entry) => `${file}: "${entry}" is not on main's list; fix the case instead of listing it`);
156
+ }
157
+
158
+ /** Per rule, the count summed over every file. */
159
+ function totals(table: Table): Map<string, number> {
160
+ const sums = new Map<string, number>();
161
+ for (const row of Object.values(table)) {
162
+ for (const [rule, count] of Object.entries(row)) sums.set(rule, (sums.get(rule) ?? 0) + count);
163
+ }
164
+ return sums;
165
+ }
166
+
167
+ function baselineProblems(root: string, ref: string): string[] {
168
+ const was = asTable(readMain(root, ref, BASELINE_PATH));
169
+ if (was === undefined) return [];
170
+ const now = totals(asTable(readHere(root, BASELINE_PATH)) ?? {});
171
+ return [...totals(was)].flatMap(([rule, before]) => {
172
+ const after = now.get(rule) ?? 0;
173
+ return after > before
174
+ ? [`${BASELINE_PATH}: ${rule} totals ${after}, main has ${before}; fix the new hit instead of baselining it`]
175
+ : [];
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Every way the checkout at `root` loosens a ratchet main holds: a lowered or dropped coverage floor, a known-failure
181
+ * entry main's list lacks, a lowered mutation floor, a lint rule total above main's. The paths default to
182
+ * `coverage-floors.json`, `known-failures.json` and `mutation-floors.json`.
183
+ */
184
+ export function ratchetProblems(root: string, paths: RatchetPaths = {}): string[] {
185
+ const ref = mainRef(root);
186
+ if (ref === null) return [];
187
+ return [
188
+ ...floorProblems(root, ref, paths.floors ?? DEFAULT_FLOORS),
189
+ ...listProblems(root, ref, paths.knownFailures ?? DEFAULT_LIST),
190
+ ...mutationProblems(root, ref, paths.mutationFloors ?? DEFAULT_MUTATION_FLOORS),
191
+ ...baselineProblems(root, ref),
192
+ ];
193
+ }
@@ -0,0 +1,220 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { afterEach, expect, it } from "vitest";
6
+ import { rejectionProblems, rejections } from "./check-rejections.ts";
7
+
8
+ const pkgRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ /** Each program parses the default lib, over a second on a fast machine. */
10
+ const SLOW = 60_000;
11
+
12
+ const temps: string[] = [];
13
+ afterEach(() => {
14
+ for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
15
+ });
16
+
17
+ /** A temp root holding `files`, a value that is not a string written as JSON. */
18
+ function plant(files: Record<string, string | object>): string {
19
+ const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "check-rejections-")));
20
+ temps.push(root);
21
+ for (const [rel, content] of Object.entries(files)) {
22
+ fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
23
+ fs.writeFileSync(path.join(root, rel), typeof content === "string" ? content : JSON.stringify(content));
24
+ }
25
+ return root;
26
+ }
27
+
28
+ /** A package at `dir`: a `tsconfig.json` on this checkout's base that excludes reject files, and its strict sibling. */
29
+ function pkg(dir: string, compilerOptions: object = {}): Record<string, object> {
30
+ const exclude = ["**/*.reject.ts", "**/*.reject.tsx"];
31
+ return {
32
+ [path.posix.join(dir, "tsconfig.json")]: {
33
+ extends: path.join(pkgRoot, "tsconfig", "base.json"),
34
+ compilerOptions,
35
+ include: ["src"],
36
+ exclude,
37
+ },
38
+ [path.posix.join(dir, "tsconfig.src.json")]: {
39
+ extends: ["./tsconfig.json", path.join(pkgRoot, "tsconfig", "strict.json")],
40
+ exclude,
41
+ },
42
+ };
43
+ }
44
+
45
+ /** The problems of every planted file. */
46
+ function problemsOf(files: Record<string, string | object>): string[] {
47
+ return rejectionProblems(plant(files), Object.keys(files));
48
+ }
49
+
50
+ it("applies each mark from the next code line to the end of its block", () => {
51
+ const source = [
52
+ "// rejects: first",
53
+ "",
54
+ "// a note",
55
+ "const a = (",
56
+ "\t1",
57
+ ")",
58
+ "",
59
+ "const b = 2",
60
+ "// rejects: second",
61
+ "const c = 3",
62
+ ].join("\n");
63
+ expect(rejections(source)).toEqual([
64
+ { end: 6, line: 4, text: "first" },
65
+ { end: 10, line: 10, text: "second" },
66
+ ]);
67
+ });
68
+
69
+ it("reads a mark written as a block comment and skips block prose", () => {
70
+ const source = [
71
+ "/* A rejection file: prose first,",
72
+ " over two lines. */",
73
+ "",
74
+ "/* rejects: first */",
75
+ 'export const a: number = "x"',
76
+ "",
77
+ "/* a note, not a mark */",
78
+ "/* rejects: second */",
79
+ "export const b = { }.nope",
80
+ ].join("\n");
81
+ expect(rejections(source)).toEqual([
82
+ { end: 5, line: 5, text: "first" },
83
+ { end: 9, line: 9, text: "second" },
84
+ ]);
85
+ });
86
+
87
+ const WIDE_USER = "export type UserId = string;\nexport function load(id: UserId): UserId {\n return id;\n}\n";
88
+ const USER_REJECT = [
89
+ "// src/user.reject.ts — excluded from tsconfig.json and tsconfig.src.json",
90
+ 'import { load } from "./user.ts";',
91
+ "/* rejects: Argument of type 'string' is not assignable to parameter of type 'UserId' */",
92
+ 'load("u1");',
93
+ "",
94
+ 'lod("u2");',
95
+ "",
96
+ '/* rejects: import("/home/me/app/src/user") */',
97
+ "load(1);",
98
+ "",
99
+ ].join("\n");
100
+
101
+ it("prints each Usage problem on its statement's line", { timeout: SLOW }, () => {
102
+ expect(problemsOf({ ...pkg("."), "src/user.ts": WIDE_USER, "src/user.reject.ts": USER_REJECT })).toEqual([
103
+ "src/user.reject.ts: line 4 compiles; it must reject: Argument of type 'string' is not assignable to parameter of type 'UserId'",
104
+ "src/user.reject.ts: line 6 fails unmarked: TS2552: Cannot find name 'lod'. Did you mean 'load'?",
105
+ "src/user.reject.ts: line 9's mark quotes an absolute import path, which differs per checkout: mark the text before it",
106
+ ]);
107
+ });
108
+
109
+ it("passes a file that rejects exactly what it marks, under the owner's strict options", { timeout: SLOW }, () => {
110
+ const branded = WIDE_USER.replace("= string;", '= string & { readonly brand: "UserId" };');
111
+ const source = [
112
+ 'import { load } from "./user.ts";',
113
+ "/* rejects: Argument of type 'string' is not assignable to parameter of type 'UserId' */",
114
+ 'load("u1");',
115
+ "",
116
+ "export type Opt = { a?: number };",
117
+ "/* rejects: with 'exactOptionalPropertyTypes: true' */",
118
+ "export const opt: Opt = { a: undefined };",
119
+ "",
120
+ "/* rejects: Type 'unknown' is not assignable to type 'number' */",
121
+ 'export const parsed: number = JSON.parse("1");',
122
+ "",
123
+ ].join("\n");
124
+ expect(problemsOf({ ...pkg("."), "src/user.ts": branded, "src/user.reject.ts": source })).toEqual([]);
125
+ });
126
+
127
+ it("names the errors of a marked line that fails without its text", { timeout: SLOW }, () => {
128
+ const source = "/* rejects: Property 'nope' does not exist */\nexport const wrong: number = \"two\";\n";
129
+ expect(problemsOf({ ...pkg("."), "src/a.reject.ts": source })).toEqual([
130
+ `src/a.reject.ts: line 2 fails without "Property 'nope' does not exist": TS2322: Type 'string' is not assignable to type 'number'.`,
131
+ ]);
132
+ });
133
+
134
+ it("covers a statement the formatter wrapped through the line before the next blank line", { timeout: SLOW }, () => {
135
+ const source = [
136
+ "/* rejects: Type 'string' is not assignable to type 'number' */",
137
+ "export const list: number[] = [",
138
+ " 1,",
139
+ ' "two",',
140
+ "];",
141
+ "",
142
+ "export const after: number = 3;",
143
+ "",
144
+ ].join("\n");
145
+ expect(problemsOf({ ...pkg("."), "src/a.reject.ts": source })).toEqual([]);
146
+ });
147
+
148
+ it("reports a syntax error as such, never as a marked line that compiles", { timeout: SLOW }, () => {
149
+ const source = "/* rejects: anything */\nexport const a: number = ;\n";
150
+ expect(problemsOf({ ...pkg("."), "src/a.reject.ts": source })).toEqual([
151
+ "src/a.reject.ts: line 2 does not parse, so nothing in the file was type-checked: TS1109: Expression expected.",
152
+ ]);
153
+ });
154
+
155
+ it("checks a .tsx reject file with its owner's jsx option and ambient types", { timeout: SLOW }, () => {
156
+ const jsx =
157
+ "declare namespace JSX {\n interface Element {}\n interface IntrinsicElements {\n div: { id?: string };\n }\n}\n";
158
+ const source = [
159
+ "/* rejects: Type 'number' is not assignable to type 'string' */",
160
+ "export const el: JSX.Element = <div id={1} />;",
161
+ "",
162
+ "/* rejects: anything */",
163
+ 'export const ok: JSX.Element = <div id="a" />;',
164
+ "",
165
+ ].join("\n");
166
+ const files = { ...pkg(".", { jsx: "preserve" }), "src/jsx.d.ts": jsx, "src/view.reject.tsx": source };
167
+ expect(problemsOf(files)).toEqual(["src/view.reject.tsx: line 5 compiles; it must reject: anything"]);
168
+ });
169
+
170
+ it("checks each package's reject files under that package's own options", { timeout: SLOW }, () => {
171
+ const source = [
172
+ "export type Opt = { a?: number };",
173
+ "/* rejects: with 'exactOptionalPropertyTypes: true' */",
174
+ "export const opt: Opt = { a: undefined };",
175
+ "",
176
+ ].join("\n");
177
+ const loose = pkg("packages/loose");
178
+ loose["packages/loose/tsconfig.src.json"] = { extends: "./tsconfig.json" };
179
+ const files = {
180
+ ...pkg("packages/strict"),
181
+ ...loose,
182
+ "packages/strict/src/a.reject.ts": source,
183
+ "packages/loose/src/a.reject.ts": source,
184
+ };
185
+ expect(problemsOf(files)).toEqual([
186
+ "packages/loose/src/a.reject.ts: line 3 compiles; it must reject: with 'exactOptionalPropertyTypes: true'",
187
+ ]);
188
+ });
189
+
190
+ it(
191
+ "falls back to tsconfig.json without a strict sibling, and to strict defaults with no tsconfig",
192
+ { timeout: SLOW },
193
+ () => {
194
+ const source = [
195
+ "export type Opt = { a?: number };",
196
+ "/* rejects: with 'exactOptionalPropertyTypes: true' */",
197
+ "export const opt: Opt = { a: undefined };",
198
+ "",
199
+ ].join("\n");
200
+ const runtime = pkg("packages/runtime");
201
+ delete runtime["packages/runtime/tsconfig.src.json"];
202
+ const bare = "/* rejects: Type 'string' is not assignable to type 'number' */\nexport const n: number = 'x';\n";
203
+ expect(problemsOf({ ...runtime, "packages/runtime/src/a.reject.ts": source, "scratch/a.reject.ts": bare })).toEqual(
204
+ ["packages/runtime/src/a.reject.ts: line 3 compiles; it must reject: with 'exactOptionalPropertyTypes: true'"],
205
+ );
206
+ },
207
+ );
208
+
209
+ it("skips a listed reject file that no longer exists", { timeout: SLOW }, () => {
210
+ expect(rejectionProblems(plant(pkg(".")), ["src/gone.reject.ts"])).toEqual([]);
211
+ });
212
+
213
+ it("names a tsconfig.src.json it cannot read", () => {
214
+ const files = { "tsconfig.json": "{}", "tsconfig.src.json": '{ "include": ["src"]\n', "src/a.reject.ts": "" };
215
+ expect(problemsOf(files)).toEqual(["tsconfig.src.json: '}' expected."]);
216
+ });
217
+
218
+ it("ignores files that are not reject files", () => {
219
+ expect(problemsOf({ ...pkg("."), "src/a.ts": "export const a: number = 'x';\n" })).toEqual([]);
220
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Type-level tests: code that must not compile, without `@ts-` directives. A `*.reject.ts(x)` file sits outside its
3
+ * package's type check (tsconfig `exclude`) and marks each statement that must fail with a `rejects: <text>` comment
4
+ * above it. The check type-checks it in-process with its owner's `tsconfig.src.json` options: every marked statement
5
+ * fails with an error containing its text, and nothing else fails. A mark covers its statement's lines up to the next
6
+ * blank line, so the formatter can wrap it.
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import ts from "typescript";
11
+ import { RUNTIME_CONFIG, SRC_CONFIG } from "./tsconfig.constants.ts";
12
+ import { ownerOf, posix, setupOf } from "./ts-program.ts";
13
+
14
+ /** A statement that must not compile (its first and last one-based lines), and the text its error must contain. */
15
+ export type Rejection = { line: number; end: number; text: string };
16
+ /** A type error on a one-based line, as `TS<code>: <text>`. */
17
+ type TypeError = { line: number; message: string };
18
+ /** A problem's line, for sorting, and its text without the file. */
19
+ type Problem = { line: number; text: string };
20
+
21
+ /* Both comment forms are read: the line form is what the harness was written for, and `repo/no-line-comment` leaves
22
+ only the block form in a repo that bans line comments. */
23
+ const MARK = /^\s*(?:\/\/|\/\*)\s*rejects:\s*(.*?)\s*(?:\*\/)?$/;
24
+ const REJECT_FILE = /\.reject\.tsx?$/;
25
+ const DECLARATION = /\.d\.[cm]?ts$/;
26
+ const LINE_COMMENT = "//";
27
+ const BLOCK_OPEN = "/*";
28
+ const BLOCK_CLOSE = "*/";
29
+ /** The start of a module's printed type, which carries the path of the checkout it was checked in. */
30
+ const ABSOLUTE_IMPORT = 'import("/';
31
+
32
+ /** Where the reader of `rejections` is: the marks waiting for a statement, the statements still growing. */
33
+ type Scan = { found: Rejection[]; pending: string[]; open: Rejection[]; inBlock: boolean };
34
+
35
+ const isCode = (text: string): boolean => text !== "" && !text.startsWith(LINE_COMMENT) && !text.startsWith(BLOCK_OPEN);
36
+
37
+ /** A code line: it extends the open statements, and starts one for the marks waiting. */
38
+ function scanCode(scan: Scan, number: number): void {
39
+ for (const rejection of scan.open) rejection.end = number;
40
+ if (scan.pending.length === 0) return;
41
+ scan.open = scan.pending.map((pending) => ({ end: number, line: number, text: pending }));
42
+ scan.found.push(...scan.open);
43
+ scan.pending = [];
44
+ }
45
+
46
+ function scanLine(scan: Scan, line: string, number: number): void {
47
+ const text = line.trim();
48
+ if (scan.inBlock) {
49
+ scan.inBlock = !text.includes(BLOCK_CLOSE);
50
+ return;
51
+ }
52
+ const mark = MARK.exec(line)?.[1];
53
+ if (mark || text === "") scan.open = [];
54
+ if (text.startsWith(BLOCK_OPEN) && !text.includes(BLOCK_CLOSE)) scan.inBlock = true;
55
+ if (mark) scan.pending.push(mark.trim());
56
+ else if (isCode(text)) scanCode(scan, number);
57
+ }
58
+
59
+ /**
60
+ * The marked statements: each `rejects:` comment applies to the next line that is not a comment or blank, through the
61
+ * line before the next blank line or mark. Prose in a block comment over several lines is not code a mark applies to.
62
+ */
63
+ export function rejections(source: string): Rejection[] {
64
+ const scan: Scan = { found: [], pending: [], open: [], inBlock: false };
65
+ for (const [index, line] of source.split("\n").entries()) scanLine(scan, line, index + 1);
66
+ return scan.found;
67
+ }
68
+
69
+ const lineOf = (diagnostic: ts.DiagnosticWithLocation): number =>
70
+ diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start).line + 1;
71
+
72
+ const messageOf = (diagnostic: ts.Diagnostic): string =>
73
+ `TS${diagnostic.code}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, " ")}`;
74
+
75
+ /** What is wrong with one mark, given the errors of the file. */
76
+ function markProblem(rejection: Rejection, errors: readonly TypeError[]): Problem | undefined {
77
+ const { line, text } = rejection;
78
+ /* The compiler prints a module's type with the path of the checkout it ran in, so a mark quoting one passes in that
79
+ checkout alone: a worktree or CI prints another. */
80
+ if (text.includes(ABSOLUTE_IMPORT)) {
81
+ return {
82
+ line,
83
+ text: `line ${line}'s mark quotes an absolute import path, which differs per checkout: mark the text before it`,
84
+ };
85
+ }
86
+ const onLine = errors.filter((error) => error.line >= line && error.line <= rejection.end);
87
+ if (onLine.length === 0) return { line, text: `line ${line} compiles; it must reject: ${text}` };
88
+ if (onLine.some((error) => error.message.includes(text))) return undefined;
89
+ return { line, text: `line ${line} fails without "${text}": ${onLine.map((error) => error.message).join("; ")}` };
90
+ }
91
+
92
+ /** A file's problems in line order: marks its errors do not match, and errors no mark covers. */
93
+ function fileProblems(source: string, errors: readonly TypeError[]): string[] {
94
+ const marked = rejections(source);
95
+ const problems = marked.map((rejection) => markProblem(rejection, errors)).filter((p) => p !== undefined);
96
+ for (const error of errors) {
97
+ const covered = marked.some((rejection) => error.line >= rejection.line && error.line <= rejection.end);
98
+ if (!covered) problems.push({ line: error.line, text: `line ${error.line} fails unmarked: ${error.message}` });
99
+ }
100
+ return problems.sort((a, b) => a.line - b.line).map((problem) => problem.text);
101
+ }
102
+
103
+ /** The errors tsc would print for `file` under the program's options. */
104
+ function typeErrors(program: ts.Program, file: ts.SourceFile): TypeError[] {
105
+ const declarations = program.getCompilerOptions().declaration === true ? program.getDeclarationDiagnostics(file) : [];
106
+ return [...program.getSemanticDiagnostics(file), ...declarations]
107
+ .filter((diagnostic): diagnostic is ts.DiagnosticWithLocation => diagnostic.start !== undefined)
108
+ .map((diagnostic) => ({ line: lineOf(diagnostic), message: messageOf(diagnostic) }));
109
+ }
110
+
111
+ /** One reject file's problems, each prefixed with its root-relative path. */
112
+ function fileProblemsOf(program: ts.Program, root: string, file: ts.SourceFile): string[] {
113
+ const rel = path.posix.relative(root, file.fileName);
114
+ /* A syntax error leaves no statement checked: report it, never "compiles". */
115
+ const syntax = program.getSyntacticDiagnostics(file);
116
+ if (syntax.length > 0) {
117
+ return syntax.map(
118
+ (d) => `${rel}: line ${lineOf(d)} does not parse, so nothing in the file was type-checked: ${messageOf(d)}`,
119
+ );
120
+ }
121
+ return fileProblems(file.text, typeErrors(program, file)).map((problem) => `${rel}: ${problem}`);
122
+ }
123
+
124
+ /** The owner's strict config when it has one; else its `tsconfig.json`, whose missing sibling `checkTsconfigs` names. */
125
+ function configOf(root: string, dir: string): string {
126
+ return fs.existsSync(path.join(root, dir, SRC_CONFIG)) ? SRC_CONFIG : RUNTIME_CONFIG;
127
+ }
128
+
129
+ /** The problems of one owner's reject files, from one program over them and the declarations its config lists. */
130
+ function groupProblems(root: string, dir: string | null, targets: readonly string[]): string[] {
131
+ const setup = setupOf(root, dir, dir === null ? SRC_CONFIG : configOf(root, dir));
132
+ if (!setup.ok) return setup.errors;
133
+ const ambient = setup.value.fileNames.filter((fileName) => DECLARATION.test(fileName));
134
+ const program = ts.createProgram({ rootNames: [...ambient, ...targets], options: setup.value.options });
135
+ /* A reject file deleted since it was listed is not in the program, and has nothing left to check. */
136
+ const read = program.getSourceFiles().filter((file) => targets.includes(file.fileName));
137
+ return read.flatMap((file) => fileProblemsOf(program, root, file));
138
+ }
139
+
140
+ /**
141
+ * What is wrong with the reject files among `files` (root-relative POSIX paths), one line each, by file then line: a
142
+ * marked statement that compiles or fails without its text, a mark quoting a checkout's path, an error no mark
143
+ * covers, and a file that does not parse. Empty when every file rejects exactly what it marks.
144
+ */
145
+ export function rejectionProblems(root: string, files: readonly string[]): string[] {
146
+ const real = posix(fs.realpathSync(root));
147
+ const groups = new Map<string | null, string[]>();
148
+ for (const rel of files.filter((file) => REJECT_FILE.test(file)).sort()) {
149
+ const dir = ownerOf(real, rel);
150
+ groups.set(dir, [...(groups.get(dir) ?? []), path.posix.join(real, rel)]);
151
+ }
152
+ return [...groups].flatMap(([dir, targets]) => groupProblems(real, dir, targets));
153
+ }
@@ -0,0 +1,37 @@
1
+ /** `repo mutate check <dir>`: merges the shard reports of one run, scores each package and holds it to its floor. */
2
+ import path from "node:path";
3
+ import { fail, type MutateConfig, type MutateDeps } from "./mutate-deps.ts";
4
+ import { failures, label, plural, raised, readFloors, tally, verdicts, type Floors } from "./mutate-floors.ts";
5
+ import type { FileScore, ShardReport } from "./mutate-report.ts";
6
+ import { loadShards, shortOf } from "./mutate-shards.ts";
7
+ import { JSON_INDENT, NEWLINE, OK, SEPARATOR } from "./mutate.constants.ts";
8
+
9
+ const text = (floors: Floors): string => `${JSON.stringify(floors, null, JSON_INDENT)}${NEWLINE}`;
10
+
11
+ type Check = { dir: string; write: boolean; config: MutateConfig; reports: ShardReport[]; floors: Floors };
12
+
13
+ /** The score lines, then a failure line per package below its floor; with `write`, the raised floors file. */
14
+ function settle({ dir, write, config, reports, floors }: Check, deps: MutateDeps): number {
15
+ const files: Record<string, FileScore> = Object.fromEntries(reports.flatMap((r) => Object.entries(r.files)));
16
+ const vs = verdicts(tally(files, deps.packages(config.root)), floors);
17
+ const lines = failures(vs, config.floors, dir);
18
+ const next = raised(floors, vs);
19
+ const writes = write && lines.length === 0 && text(next) !== text(floors);
20
+ const head = `${plural(reports.length, "shard")} of ${shortOf(reports.reduce((_, report) => report.head, ""))}`;
21
+ const scores = vs.map((v) => label(v, write)).join(SEPARATOR);
22
+ const wrote = writes ? `${SEPARATOR}wrote ${config.floors}` : "";
23
+ deps.out(`mutate: ${head}${SEPARATOR}${scores}${wrote}${NEWLINE}`);
24
+ if (writes) deps.fs.write(path.join(config.root, config.floors), text(next));
25
+ return lines.length > 0 ? fail(deps, ...lines) : OK;
26
+ }
27
+
28
+ /** Scores the merged `dir` of reports against the floors; with `write`, raises the floors file when none is below. */
29
+ export async function checkReports(dir: string, write: boolean, deps: MutateDeps): Promise<number> {
30
+ const config = await deps.config(deps.cwd);
31
+ if (Array.isArray(config)) return fail(deps, ...config);
32
+ const reports = loadShards(dir, deps);
33
+ if (typeof reports === "string") return fail(deps, reports);
34
+ const floors = readFloors(deps, config.root, config.floors);
35
+ if (!floors.ok) return fail(deps, `${config.floors} is not a floors file: ${floors.reason}`);
36
+ return settle({ dir, write, config, reports, floors: floors.value }, deps);
37
+ }