@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,94 @@
1
+ import type { Context, ESTree, CreateRule } from "@oxlint/plugins";
2
+
3
+ /*
4
+ * `"sideEffects": false` is only true when importing a module runs nothing. A bundler keeps any top-level code it
5
+ * cannot prove pure, so module scope only declares; a call it needs is marked pure or moved into a function.
6
+ */
7
+ const MESSAGE = "module scope runs no code; move this into a function, or mark a pure call /* @__PURE__ */";
8
+ /** Statements that only declare. A class is walked instead: its decorators, heritage and static parts run. */
9
+ const DECLARATIONS = [
10
+ "EmptyStatement",
11
+ "ExportAllDeclaration",
12
+ "FunctionDeclaration",
13
+ "ImportDeclaration",
14
+ "TSDeclareFunction",
15
+ "TSEnumDeclaration",
16
+ "TSExportAssignment",
17
+ "TSImportEqualsDeclaration",
18
+ "TSInterfaceDeclaration",
19
+ "TSModuleDeclaration",
20
+ "TSTypeAliasDeclaration",
21
+ ];
22
+ const EFFECTS = [
23
+ "AssignmentExpression",
24
+ "AwaitExpression",
25
+ "CallExpression",
26
+ "NewExpression",
27
+ "TaggedTemplateExpression",
28
+ "UpdateExpression",
29
+ "YieldExpression",
30
+ ];
31
+ /** Code inside a function runs when it is called, not at import. */
32
+ const FUNCTIONS = ["ArrowFunctionExpression", "FunctionDeclaration", "FunctionExpression"];
33
+ const PURE = /[@#]__PURE__/;
34
+
35
+ type Node = ESTree.Node;
36
+ type Field = ESTree.PropertyDefinition | ESTree.AccessorProperty;
37
+
38
+ const isNode = (value: unknown): value is Node =>
39
+ typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
40
+ const isField = (node: Node): node is Field =>
41
+ node.type.endsWith("PropertyDefinition") || node.type.endsWith("AccessorProperty");
42
+
43
+ function children(node: Node): Node[] {
44
+ const entries: [string, unknown][] = Object.entries(node);
45
+ return entries.flatMap(([key, value]) => (key === "parent" ? [] : [value].flat().filter(isNode)));
46
+ }
47
+
48
+ function firstOf<T>(items: readonly T[], find: (item: T) => Node | null): Node | null {
49
+ for (const item of items) {
50
+ const found = find(item);
51
+ if (found !== null) return found;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ const pure = (context: Context, node: Node): boolean =>
57
+ context.sourceCode.getCommentsBefore(node).some((comment) => PURE.test(comment.value));
58
+
59
+ /** The first node under `node` that runs at import: a non-pure effect, a decorator, or a static part's code. */
60
+ function firstEffect(context: Context, node: Node | null): Node | null {
61
+ if (node === null || FUNCTIONS.includes(node.type)) return null;
62
+ if (node.type === "Decorator") return node;
63
+ if (node.type === "StaticBlock") return firstOf(node.body, (statement) => statementEffect(context, statement));
64
+ // An instance field's value runs per construction; its decorators and computed key run with the class.
65
+ const parts = isField(node) && !node.static ? [...node.decorators, node.key] : children(node);
66
+ if (EFFECTS.includes(node.type) && !pure(context, node)) return node;
67
+ return firstOf(parts, (child) => firstEffect(context, child));
68
+ }
69
+
70
+ /** What a module-scope statement runs at import, or null when it only declares. */
71
+ function statementEffect(context: Context, statement: Node): Node | null {
72
+ const inner = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
73
+ if (inner === null || DECLARATIONS.includes(inner.type)) return null;
74
+ if (inner.type === "ExpressionStatement" && typeof inner.directive === "string") return null;
75
+ if (inner.type === "VariableDeclaration")
76
+ return firstOf(inner.declarations, (item) => firstEffect(context, item.init));
77
+ if (inner.type === "ExportDefaultDeclaration" || inner.type === "ClassDeclaration")
78
+ return firstEffect(context, inner);
79
+ return inner;
80
+ }
81
+
82
+ /** Fails module-scope code that runs at import, reported on the first such node of each statement. */
83
+ export const noModuleSideEffects: CreateRule = {
84
+ create(context) {
85
+ return {
86
+ Program(program) {
87
+ for (const statement of program.body) {
88
+ const effect = statementEffect(context, statement);
89
+ if (effect !== null) context.report({ message: MESSAGE, node: effect });
90
+ }
91
+ },
92
+ };
93
+ },
94
+ };
@@ -0,0 +1,58 @@
1
+ import type { ESTree, CreateRule } from "@oxlint/plugins";
2
+
3
+ const MESSAGE =
4
+ "module-scope state outlives every caller; move it into a function, or make it a const with a Readonly type";
5
+ /** Wrappers that change a value's type, never the value: `(x)`, `x as T`, `x satisfies T`, `x!` and `<T>x`. */
6
+ const WRAPPERS = [
7
+ "ParenthesizedExpression",
8
+ "TSAsExpression",
9
+ "TSSatisfiesExpression",
10
+ "TSNonNullExpression",
11
+ "TSTypeAssertion",
12
+ ];
13
+ /** A const annotated with one of these is read-only to every caller, so it holds no state they can change. */
14
+ const READONLY = ["ReadonlySet", "ReadonlyMap", "Readonly"];
15
+
16
+ type Expression = ESTree.Expression;
17
+ type Wrapper =
18
+ | ESTree.ParenthesizedExpression
19
+ | ESTree.TSAsExpression
20
+ | ESTree.TSSatisfiesExpression
21
+ | ESTree.TSNonNullExpression
22
+ | ESTree.TSTypeAssertion;
23
+
24
+ const isWrapper = (node: Expression): node is Wrapper => WRAPPERS.includes(node.type);
25
+
26
+ function unwrap(node: Expression | null): Expression | null {
27
+ let current = node;
28
+ while (current !== null && isWrapper(current)) current = current.expression;
29
+ return current;
30
+ }
31
+
32
+ function readonlyAnnotated(declarator: ESTree.VariableDeclarator): boolean {
33
+ const annotation = declarator.id.typeAnnotation?.typeAnnotation;
34
+ if (annotation?.type !== "TSTypeReference" || annotation.typeName.type !== "Identifier") return false;
35
+ return READONLY.includes(annotation.typeName.name);
36
+ }
37
+
38
+ /** A declarator whose value is constructed once at import and shared by every caller. */
39
+ function constructed(declarator: ESTree.VariableDeclarator): boolean {
40
+ return unwrap(declarator.init)?.type === "NewExpression" && !readonlyAnnotated(declarator);
41
+ }
42
+
43
+ function isState(node: ESTree.VariableDeclaration): boolean {
44
+ if (node.declare === true) return false;
45
+ return node.kind !== "const" || node.declarations.some(constructed);
46
+ }
47
+
48
+ /** Fails a module-scope `let`, `var` or `using`, and a `const` holding a `new` object, exported or not. */
49
+ export const noModuleState: CreateRule = {
50
+ create(context) {
51
+ return {
52
+ VariableDeclaration(node) {
53
+ const statement = node.parent.type === "ExportNamedDeclaration" ? node.parent : node;
54
+ if (statement.parent.type === "Program" && isState(node)) context.report({ message: MESSAGE, node });
55
+ },
56
+ };
57
+ },
58
+ };
@@ -0,0 +1,17 @@
1
+ import type { CreateRule } from "@oxlint/plugins";
2
+
3
+ /**
4
+ * Fails an import that binds nothing: it is there only to run the module, so importing it has an effect no caller
5
+ * chose. `import type` is erased and passes.
6
+ */
7
+ export const noSideEffectImport: CreateRule = {
8
+ create(context) {
9
+ return {
10
+ ImportDeclaration(node) {
11
+ if (node.specifiers.length > 0 || node.importKind === "type") return;
12
+ const message = `import "${node.source.value}" runs a module for its effects; import a function from it and call that`;
13
+ context.report({ message, node });
14
+ },
15
+ };
16
+ },
17
+ };
@@ -0,0 +1,18 @@
1
+ import type { CreateRule } from "@oxlint/plugins";
2
+
3
+ /** The v8 coverage provider honors all three tools' hints; each hides uncovered code behind a full score. */
4
+ const HINT = /\b(?:v8|c8|istanbul)\s+ignore\b/i;
5
+ const MESSAGE = "coverage ignore comments hide untested code; delete the code or add a test that runs it";
6
+
7
+ /** Fails every coverage ignore comment, reported on the comment. */
8
+ export const noV8Ignore: CreateRule = {
9
+ create(context) {
10
+ return {
11
+ Program() {
12
+ for (const comment of context.sourceCode.getAllComments()) {
13
+ if (HINT.test(comment.value)) context.report({ message: MESSAGE, node: comment });
14
+ }
15
+ },
16
+ };
17
+ },
18
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The checks `repo check`, `repo turn` and `repo lint --baseline` run beside oxlint as scripts on the TypeScript
3
+ * compiler API, by the names `lint.skip` takes. Kept apart from the checks so the oxlint config can accept the names
4
+ * without loading typescript.
5
+ */
6
+ export const MAGIC_NUMBER = "repo/magic-number";
7
+ export const MAGIC_STRING = "repo/magic-string";
8
+ export const GUARD_REJECTS = "repo/guard-rejects";
9
+ export const SCRIPT_RULES: readonly string[] = [MAGIC_NUMBER, MAGIC_STRING, GUARD_REJECTS];
@@ -0,0 +1,8 @@
1
+ export const SKIP = "skip";
2
+ export const GLOBAL_OBJECT = "globalThis";
3
+ export const PROCESS = "process";
4
+ export const VERSIONS = "versions";
5
+ export const TYPEOF = "typeof";
6
+ export const UNDEFINED = "undefined";
7
+ /** Typed `string` so a node type compares to it without narrowing. */
8
+ export const PAREN: string = "ParenthesizedExpression";
@@ -0,0 +1,118 @@
1
+ import type { CreateRule, ESTree } from "@oxlint/plugins";
2
+ import { GLOBAL_OBJECT, PAREN, PROCESS, SKIP, TYPEOF, UNDEFINED, VERSIONS } from "./skip-on-platform.constants.ts";
3
+
4
+ type Node = ESTree.Node;
5
+
6
+ /** The test functions whose `skipIf` and `runIf` take a skip condition. */
7
+ const TEST_FUNCTIONS = ["it", "test", "describe"];
8
+ /** Chains that keep a test function a test function: `it.concurrent.skipIf`. */
9
+ const CHAINS = ["concurrent", "sequential"];
10
+ const CONDITIONAL = ["skipIf", "runIf"];
11
+ const PLATFORM_READS = ["platform", "arch"];
12
+ const COMPARISONS = ["===", "!==", "==", "!=", "<", ">", "<=", ">="];
13
+ const JOINS = ["&&", "||"];
14
+ const UNARY = ["!", "-"];
15
+ const NOTHING = "no condition";
16
+ const tail =
17
+ "a test may skip only on process.platform, process.arch, process.versions or a runtime global, so make it hermetic instead";
18
+
19
+ const nameOf = (node: Node | null | undefined): string | undefined =>
20
+ node?.type === "Identifier" ? node.name : undefined;
21
+
22
+ /** The plain name of `.name` on a member read, or undefined for `[expr]` and private names. */
23
+ const memberName = (node: ESTree.MemberExpression): string | undefined =>
24
+ node.computed ? undefined : nameOf(node.property);
25
+
26
+ /** `it`, `test` or `describe`, bare or through `.concurrent` and `.sequential`. */
27
+ function isTestFunction(node: Node): boolean {
28
+ if (node.type === "Identifier") return TEST_FUNCTIONS.includes(node.name);
29
+ return node.type === "MemberExpression" && CHAINS.includes(memberName(node) ?? "") && isTestFunction(node.object);
30
+ }
31
+
32
+ /** `it.skipIf` or `describe.concurrent.runIf`: the name of the conditional call, or undefined. */
33
+ function conditionalName(callee: Node): string | undefined {
34
+ if (callee.type !== "MemberExpression") return undefined;
35
+ const name = memberName(callee);
36
+ return name !== undefined && CONDITIONAL.includes(name) && isTestFunction(callee.object) ? name : undefined;
37
+ }
38
+
39
+ /** `process.platform`, `process.arch`, `process.versions.<name>` or `globalThis.<name>`. */
40
+ function isRuntimeRead(node: ESTree.MemberExpression): boolean {
41
+ const name = memberName(node);
42
+ const { object } = node;
43
+ if (name === undefined) return false;
44
+ if (nameOf(object) === GLOBAL_OBJECT) return true;
45
+ if (nameOf(object) === PROCESS) return PLATFORM_READS.includes(name);
46
+ return object.type === "MemberExpression" && nameOf(object.object) === PROCESS && memberName(object) === VERSIONS;
47
+ }
48
+
49
+ type Parenthesized = { type: typeof PAREN; expression: Node };
50
+
51
+ /** `(x)`: oxlint drops the parentheses, but a parser that keeps them (as the test harness's does) yields this node. */
52
+ const isParenthesized = (node: Node): node is Node & Parenthesized => node.type === PAREN;
53
+
54
+ /** The operand a unary read passes through: `!x` and `-x` read what `x` reads, `typeof <identifier>` reads nothing. */
55
+ function unaryLeaf(node: ESTree.UnaryExpression): Node | undefined {
56
+ if (node.operator === TYPEOF) return node.argument.type === "Identifier" ? undefined : node;
57
+ return UNARY.includes(node.operator) ? firstLeaf(node.argument) : node;
58
+ }
59
+
60
+ /** The first leaf of `a <op> b`, when `op` is a comparison or a join. */
61
+ function binaryLeaf(
62
+ node: ESTree.BinaryExpression | ESTree.LogicalExpression | ESTree.PrivateInExpression,
63
+ ): Node | undefined {
64
+ if (!COMPARISONS.includes(node.operator) && !JOINS.includes(node.operator)) return node;
65
+ return firstLeaf(node.left) ?? firstLeaf(node.right);
66
+ }
67
+
68
+ /** The part of a condition that reads anything but the platform or runtime, as the node to name; undefined if none. */
69
+ function firstLeaf(node: Node): Node | undefined {
70
+ if (isParenthesized(node)) return firstLeaf(node.expression);
71
+ if (node.type === "Literal") return undefined;
72
+ if (node.type === "Identifier") return node.name === UNDEFINED ? undefined : node;
73
+ if (node.type === "MemberExpression") return isRuntimeRead(node) ? undefined : node;
74
+ if (node.type === "UnaryExpression") return unaryLeaf(node);
75
+ if (node.type === "BinaryExpression" || node.type === "LogicalExpression") return binaryLeaf(node);
76
+ return node;
77
+ }
78
+
79
+ /** The function a test hands its context to, when `node` is one: `(ctx) => {}` or `function (ctx) {}`. */
80
+ function contextParam(node: Node): string | undefined {
81
+ if (node.type !== "ArrowFunctionExpression" && node.type !== "FunctionExpression") return undefined;
82
+ const callee = node.parent.type === "CallExpression" ? node.parent.callee : undefined;
83
+ if (callee === undefined || !(isTestFunction(callee) || conditionalName(callee) !== undefined)) return undefined;
84
+ return nameOf(node.params[0]);
85
+ }
86
+
87
+ /** Whether `name` is the context parameter of a test callback around `node`: `it("x", (ctx) => { ctx.skip(...) })`. */
88
+ function isTestContext(node: Node, name: string): boolean {
89
+ for (let at: Node | null | undefined = node.parent; at; at = at.parent) {
90
+ if (contextParam(at) === name) return true;
91
+ }
92
+ return false;
93
+ }
94
+
95
+ /** Fails a `skipIf`, `runIf` or `ctx.skip` whose condition reads anything but the platform or the runtime. */
96
+ export const skipOnPlatform: CreateRule = {
97
+ create(context) {
98
+ const { text } = context.sourceCode;
99
+ const render = (node: Node): string => text.slice(node.start, node.end).replaceAll(/\s+/g, " ");
100
+ const check = (node: ESTree.CallExpression, who: string) => {
101
+ const [condition] = node.arguments;
102
+ const leaf = condition === undefined ? undefined : firstLeaf(condition);
103
+ if (condition !== undefined && leaf === undefined) return;
104
+ const what = leaf === undefined ? NOTHING : render(leaf.type === "CallExpression" ? leaf.callee : leaf);
105
+ context.report({ message: `${who} reads ${what}; ${tail}`, node });
106
+ };
107
+ return {
108
+ CallExpression(node) {
109
+ const { callee } = node;
110
+ const conditional = conditionalName(callee);
111
+ if (conditional !== undefined) return check(node, conditional);
112
+ const isSkip = callee.type === "MemberExpression" && memberName(callee) === SKIP;
113
+ const owner = isSkip ? nameOf(callee.object) : undefined;
114
+ if (owner !== undefined && isTestContext(node, owner)) check(node, SKIP);
115
+ },
116
+ };
117
+ },
118
+ };
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { CreateRule } from "@oxlint/plugins";
4
+
5
+ /** A comment line, past its `//`, `/*` or JSDoc `*`, that starts with the marker, and the `(…)` right after it. */
6
+ const MARKER = /^[\s*]*(TODO|FIXME)\b(?:\(([^)]*)\))?/;
7
+ const ISSUE = /^(?:#\d+|[A-Z][A-Z\d]*-\d+|https?:\/\/\S+)$/;
8
+ const SLUG = /^[a-z\d]+(?:-[a-z\d]+)*$/;
9
+ /** `path.join` reads `/` as the separator on every OS. */
10
+ const LESSONS = ".repo/lessons";
11
+
12
+ /** The nearest `.repo/lessons` dir at or above `start`, or null when none is. */
13
+ function nearestLessons(start: string): string | null {
14
+ let dir = start;
15
+ while (!fs.existsSync(path.join(dir, LESSONS))) {
16
+ if (path.dirname(dir) === dir) return null;
17
+ dir = path.dirname(dir);
18
+ }
19
+ return path.join(dir, LESSONS);
20
+ }
21
+
22
+ const shown = (cwd: string, to: string): string => path.relative(cwd, to).replaceAll("\\", "/");
23
+
24
+ /** Where the linted file is, and the dir paths in messages are relative to. */
25
+ type Where = { cwd: string; filename: string };
26
+
27
+ /** The message for a slug that names no lesson file in the nearest lessons dir, or null when one is there. */
28
+ function lessonProblem(where: Where, slug: string): string | null {
29
+ const dir = nearestLessons(path.dirname(where.filename)) ?? path.join(where.cwd, LESSONS);
30
+ if (fs.existsSync(path.join(dir, `${slug}.md`))) return null;
31
+ return `"${slug}" names no lesson: add ${shown(where.cwd, dir)}/${slug}.md, or use an issue ID like TODO(#123)`;
32
+ }
33
+
34
+ /** The message for one comment line, or null when it starts with no marker or carries a good ID. */
35
+ function problemOf(where: Where, line: string): string | null {
36
+ const match = MARKER.exec(line);
37
+ if (match === null) return null;
38
+ const word = String(match[1]);
39
+ const id = match[2] ?? "";
40
+ if (ISSUE.test(id)) return null;
41
+ if (SLUG.test(id)) return lessonProblem(where, id);
42
+ return `${word} needs an issue or lesson ID: ${word}(#123), ${word}(ABC-123) or ${word}(<lesson slug>)`;
43
+ }
44
+
45
+ /**
46
+ * Fails a `TODO` or `FIXME` that starts a comment or a JSDoc line without an ID someone can look up: `#12`, `ABC-12`,
47
+ * an issue URL, or the slug of a lesson in the nearest `.repo/lessons/`. A mention mid-sentence is prose and passes.
48
+ */
49
+ export const todoNeedsId: CreateRule = {
50
+ create(context) {
51
+ const where: Where = { cwd: context.cwd, filename: context.filename };
52
+ return {
53
+ Program() {
54
+ for (const comment of context.sourceCode.getAllComments()) {
55
+ for (const line of comment.value.split("\n")) {
56
+ const message = problemOf(where, line);
57
+ if (message !== null) context.report({ message, node: comment });
58
+ }
59
+ }
60
+ },
61
+ };
62
+ },
63
+ };
@@ -0,0 +1,28 @@
1
+ import type { CreateRule } from "@oxlint/plugins";
2
+ import type { Statement } from "./names.ts";
3
+
4
+ const MESSAGE = "a .types.ts file holds only exported types and interfaces; move this into a module";
5
+ const TYPES = ["TSTypeAliasDeclaration", "TSInterfaceDeclaration"];
6
+
7
+ /** `import type`, an exported type alias or interface, or `export type … from`. */
8
+ function typeOnly(statement: Statement): boolean {
9
+ if (statement.type === "ImportDeclaration") return statement.importKind === "type";
10
+ if (statement.type === "ExportAllDeclaration") return statement.exportKind === "type";
11
+ if (statement.type !== "ExportNamedDeclaration") return false;
12
+ if (statement.declaration !== null) return TYPES.includes(statement.declaration.type);
13
+ return statement.source !== null && statement.exportKind === "type";
14
+ }
15
+
16
+ /** Fails every statement of a `*.types.ts` file that is not a type: such a file is erased whole at build. */
17
+ export const typesFile: CreateRule = {
18
+ create(context) {
19
+ if (!context.filename.endsWith(".types.ts")) return {};
20
+ return {
21
+ Program(program) {
22
+ for (const statement of program.body) {
23
+ if (!typeOnly(statement)) context.report({ message: MESSAGE, node: statement });
24
+ }
25
+ },
26
+ };
27
+ },
28
+ };
@@ -0,0 +1,32 @@
1
+ /** The `lint` section of workerstack.config.ts: the sanctioned rule skips. */
2
+ import { z } from "zod";
3
+ import { isReason } from "./lint/is-reason.ts";
4
+
5
+ const REASON_ERROR = "say why the rules are off, in three words or more";
6
+ const SKIP_LIST = "must be a list of { files, rules, reason } entries";
7
+
8
+ /** One sanctioned exception: oxlint rule keys turned off for the files the globs match, and why. */
9
+ function skipSchema(): SkipSchema {
10
+ const list = (error: string, empty: string): ListSchema =>
11
+ z.array(z.string({ error }), { error }).min(1, { error: empty }).readonly();
12
+ return z.strictObject(
13
+ {
14
+ files: list("must be a list of file globs", "list at least one file glob"),
15
+ rules: list("must be a list of rule names", 'list at least one rule, like "repo/no-module-state"'),
16
+ reason: z.string({ error: REASON_ERROR }).refine(isReason, { error: REASON_ERROR }),
17
+ },
18
+ { error: 'must be an object like { files: ["src/a.ts"], rules: ["repo/no-module-state"], reason: "why" }' },
19
+ );
20
+ }
21
+
22
+ type ListSchema = z.ZodReadonly<z.ZodArray<z.ZodString>>;
23
+ type SkipSchema = z.ZodObject<{ files: ListSchema; rules: ListSchema; reason: z.ZodString }, z.core.$strict>;
24
+ type LintSchema = z.ZodObject<{ skip: z.ZodOptional<z.ZodReadonly<z.ZodArray<SkipSchema>>> }, z.core.$strict>;
25
+
26
+ /** `lint`: the skips, built when the config schema is. */
27
+ export function lintSchema(): LintSchema {
28
+ return z.strictObject(
29
+ { skip: z.array(skipSchema(), { error: SKIP_LIST }).readonly().optional() },
30
+ { error: "must be an object like { skip: [{ files, rules, reason }] }" },
31
+ );
32
+ }
@@ -0,0 +1,64 @@
1
+ /** Temp repos for the magic literal tests, the messages they expect, and a schema type to bound. */
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { scriptDiagnostics } from "../../script-diagnostics.ts";
6
+ import type { Diagnostic } from "../../scripts/lint-baseline.ts";
7
+
8
+ export const SLOW = 60_000;
9
+ export const NUMBER = "repo(magic-number)";
10
+ export const STRING = "repo(magic-string)";
11
+ export const NAMES_NOTHING = (text: string) => `${text} names nothing; give it a named const`;
12
+ export const BOUND = (call: string) =>
13
+ `${call} is a schema bound; import it from a *.constants.ts file or a constants package`;
14
+ export const COMPARED = (text: string) =>
15
+ `"${text}" is compared to a plain string; type the value as a literal union or brand, or import the string from a *.constants.ts file`;
16
+ export const COPIES = (name: string, text: string) =>
17
+ `${name} copies "${text}" locally to compare to a plain string; move it to a *.constants.ts file`;
18
+
19
+ const temps: string[] = [];
20
+
21
+ export function cleanTemps(): void {
22
+ for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
23
+ }
24
+
25
+ /** A temp root holding `files`, removed by `cleanTemps`. */
26
+ export function plant(files: Record<string, string>): string {
27
+ const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "workerstack-magic-")));
28
+ temps.push(root);
29
+ for (const [rel, text] of Object.entries(files)) {
30
+ fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
31
+ fs.writeFileSync(path.join(root, rel), text);
32
+ }
33
+ return root;
34
+ }
35
+
36
+ /** The diagnostic of a hit at `at`, `<file>:<line>:<column>`. */
37
+ export const hit = (at: string, rule: string, message: string): Diagnostic => ({
38
+ file: at.slice(0, at.indexOf(":")),
39
+ rule,
40
+ line: `${at}: error ${rule}: ${message}`,
41
+ });
42
+
43
+ /** Every hit of the planted `files`, all of them checked. */
44
+ export function magicOf(files: Record<string, string>): Diagnostic[] {
45
+ const found = scriptDiagnostics(plant(files), Object.keys(files));
46
+ if (!found.ok) throw new Error(found.errors.join("\n"));
47
+ return found.value;
48
+ }
49
+
50
+ /** A schema type: the receiver of a bound has a `parse` method. */
51
+ export const SCHEMA = [
52
+ "export class Schema {",
53
+ " parse(value: unknown): unknown {",
54
+ " return value;",
55
+ " }",
56
+ " max(_bound: number): Schema {",
57
+ " return this;",
58
+ " }",
59
+ " min(_bound: number): Schema {",
60
+ " return this;",
61
+ " }",
62
+ "}",
63
+ "",
64
+ ].join("\n");
@@ -0,0 +1,2 @@
1
+ /** One magic literal: the file, offset and one-based line and column it is reported at, its rule and the fix. */
2
+ export type MagicHit = { fileName: string; at: number; line: number; column: number; rule: string; message: string };
@@ -0,0 +1,44 @@
1
+ /** What both magic checks ask of a syntax node: a const's string, where a hit sits, and the constants homes. */
2
+ import ts from "typescript";
3
+ import type { MagicHit } from "./magic-hit.ts";
4
+
5
+ /** A `*.constants.ts` file (or its `.js`, `.mts`… spelling), as a path or an import specifier with or without one. */
6
+ export const CONSTANTS_FILE: RegExp = /\.constants(?:\.[cm]?[jt]sx?)?$/;
7
+ /** A package, scoped or not, whose name ends in `constants`, or a subpath of one. */
8
+ const CONSTANTS_PACKAGE = /^(?:@[^/]+\/)?[^/]*constants(?:\/|$)/;
9
+ /** A string with a letter or digit in it; one of punctuation or nothing says nothing a name could. */
10
+ const MEANINGFUL = /[\p{L}\p{N}]/u;
11
+
12
+ /** Whether an import of `specifier` reaches a constants file or a constants package. */
13
+ export const isConstantsModule = (specifier: string): boolean =>
14
+ CONSTANTS_FILE.test(specifier) || CONSTANTS_PACKAGE.test(specifier);
15
+
16
+ export const isConstDeclaration = (declaration: ts.VariableDeclaration): boolean =>
17
+ (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0;
18
+
19
+ /** `node` without the parentheses, `as` and `satisfies` around it, which change a type and never a value. */
20
+ function unwrapped(node: ts.Expression): ts.Expression {
21
+ let bare = node;
22
+ while (ts.isParenthesizedExpression(bare) || ts.isAsExpression(bare) || ts.isSatisfiesExpression(bare))
23
+ bare = bare.expression;
24
+ return bare;
25
+ }
26
+
27
+ /** A string literal that says something: not empty and not punctuation alone. */
28
+ export const isMeaningful = (node: ts.Expression): node is ts.StringLiteralLike =>
29
+ ts.isStringLiteralLike(node) && MEANINGFUL.test(node.text);
30
+
31
+ /** The string a `const` holds, when its initializer is a literal that says something; null for any other. */
32
+ export function constString(declaration: ts.VariableDeclaration): string | null {
33
+ if (declaration.initializer === undefined || !isConstDeclaration(declaration)) return null;
34
+ const value = unwrapped(declaration.initializer);
35
+ return isMeaningful(value) ? value.text : null;
36
+ }
37
+
38
+ /** A hit reported at the start of `node`, with its one-based line and column. */
39
+ export function hitAt(node: ts.Node, rule: string, message: string): MagicHit {
40
+ const file = node.getSourceFile();
41
+ const at = node.getStart(file);
42
+ const { line, character } = file.getLineAndCharacterOfPosition(at);
43
+ return { fileName: file.fileName, at, line: line + 1, column: character + 1, rule, message };
44
+ }
@@ -0,0 +1,99 @@
1
+ import { afterEach, expect, it } from "vitest";
2
+ import {
3
+ BOUND,
4
+ cleanTemps,
5
+ hit,
6
+ magicOf,
7
+ NAMES_NOTHING,
8
+ NUMBER,
9
+ SCHEMA,
10
+ STRING,
11
+ COMPARED,
12
+ } from "./fixtures/magic-repo.ts";
13
+
14
+ afterEach(cleanTemps);
15
+
16
+ const PASS = [
17
+ "export const RETRIES = 3;",
18
+ "export const LIMITS = { name: 64, list: [8, 16] } as const;",
19
+ "export const SIZES = [32, -64] satisfies number[];",
20
+ "export const MINUTE = (60 * 1000) as number;",
21
+ "export enum Level {",
22
+ " Low = 5,",
23
+ " High = 10,",
24
+ "}",
25
+ "export class Pool {",
26
+ " static readonly MAX = 9;",
27
+ " readonly size = 4;",
28
+ "}",
29
+ "export type Port = 8080;",
30
+ "export type Pair = [3, -4];",
31
+ "type Id = string & { __brand: 'Id' };",
32
+ "export function f(x: number, mode: 'fast' | 'slow', at: `/${string}`, id: Id, s: string): number {",
33
+ " if (mode === 'fast' || at === '/a' || id !== 'abc') return 0;",
34
+ " switch (mode) {",
35
+ " case 'slow':",
36
+ " return -0;",
37
+ " }",
38
+ " if (s === '' || s === '/' || s.split(', ').length === 1) return -0;",
39
+ " return x * 2 + x / 2 + (x % 2) + 2 ** x + 1;",
40
+ "}",
41
+ "",
42
+ ].join("\n");
43
+
44
+ it("passes identity values, scaling 2, named numbers, literal types and typed or punctuation strings", () => {
45
+ expect(magicOf({ "src/pass.ts": PASS })).toEqual([]);
46
+ });
47
+
48
+ const REFUSED = [
49
+ 'import { Schema } from "./schema.ts";',
50
+ 'import { NAME_MAX } from "./limits.constants.ts";',
51
+ 'import { WIDTH } from "@acme/constants";',
52
+ 'import * as limits from "./limits.constants.ts";',
53
+ "const LOCAL_MAX = 64;",
54
+ "let loose = 1;",
55
+ "export function g(x: number, s: Schema, kind: string): number {",
56
+ " void s.max(LOCAL_MAX).max(NAME_MAX).max(WIDTH).min(1).max(x).max(limits.NAME_MAX).max(loose).max(missing);",
57
+ " switch (kind) {",
58
+ ' case "deep":',
59
+ " return x + 2;",
60
+ " default:",
61
+ " return Math.max(x, 3) - -1;",
62
+ " }",
63
+ "}",
64
+ "",
65
+ ].join("\n");
66
+
67
+ it("refuses -1, a 2 that does not scale, a local const bound, a string case and a bound off a schema", () => {
68
+ expect(
69
+ magicOf({
70
+ "src/refused.ts": REFUSED,
71
+ "src/schema.ts": SCHEMA,
72
+ "src/limits.constants.ts": "export const NAME_MAX = 2048;\n",
73
+ }),
74
+ ).toEqual([
75
+ hit("src/refused.ts:8:14", NUMBER, BOUND(".max(LOCAL_MAX)")),
76
+ hit("src/refused.ts:10:10", STRING, COMPARED("deep")),
77
+ hit("src/refused.ts:11:18", NUMBER, NAMES_NOTHING("2")),
78
+ hit("src/refused.ts:13:26", NUMBER, NAMES_NOTHING("3")),
79
+ hit("src/refused.ts:13:31", NUMBER, NAMES_NOTHING("-1")),
80
+ ]);
81
+ });
82
+
83
+ it("refuses a bound imported from a module that is not a constants file or package", () => {
84
+ const files = {
85
+ "src/a.ts": [
86
+ 'import { Schema } from "./schema.ts";',
87
+ 'import { MAX } from "./limits.ts";',
88
+ 'import { TOP } from "@acme/limits";',
89
+ "export const a = (s: Schema) => s.max(MAX).max(TOP);",
90
+ "",
91
+ ].join("\n"),
92
+ "src/limits.ts": "export const MAX = 64;\n",
93
+ "src/schema.ts": SCHEMA,
94
+ };
95
+ expect(magicOf(files)).toEqual([
96
+ hit("src/a.ts:4:39", NUMBER, BOUND(".max(MAX)")),
97
+ hit("src/a.ts:4:48", NUMBER, BOUND(".max(TOP)")),
98
+ ]);
99
+ });