@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.
- package/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +0 -3
- package/agents/global-settings/antigravity.json +3 -0
- package/agents/global-settings/claude.json +6 -0
- package/agents/global-settings/codex.toml +2 -0
- package/agents/global-settings/cursor.json +3 -0
- package/agents/global-settings/grok.toml +36 -0
- package/agents/global-settings/manifest.json +36 -0
- package/agents/grok/hooks/orchestrate-parent.json +60 -0
- package/agents/grok/hooks/rtk.json +16 -0
- package/agents/grok/hooks/rulesync.json +3 -0
- package/agents/grok/roles/scout.toml +5 -0
- package/agents/hooks.jsonc +67 -0
- package/agents/mcp-policy.jsonc +16 -0
- package/agents/rules/overview.md +21 -0
- package/agents/settings/claude.json +5 -0
- package/agents/skills/ask/SKILL.md +13 -0
- package/agents/skills/auto/SKILL.md +13 -0
- package/agents/skills/away/SKILL.md +19 -0
- package/agents/skills/blast-radius/SKILL.md +54 -0
- package/agents/skills/bro/SKILL.md +9 -0
- package/agents/skills/browser/SKILL.md +16 -0
- package/agents/skills/clasp/SKILL.md +11 -0
- package/agents/skills/commit/SKILL.md +35 -0
- package/agents/skills/commit-and-push/SKILL.md +9 -0
- package/agents/skills/commit-push/SKILL.md +59 -0
- package/agents/skills/commit-push-deploy/SKILL.md +74 -0
- package/agents/skills/commit-push-release/SKILL.md +75 -0
- package/agents/skills/correct/SKILL.md +39 -0
- package/agents/skills/do/SKILL.md +21 -0
- package/agents/skills/explain/SKILL.md +13 -0
- package/agents/skills/google-workspace/SKILL.md +12 -0
- package/agents/skills/handoff/SKILL.md +81 -0
- package/agents/skills/implement-plan/SKILL.md +29 -0
- package/agents/skills/implement-roadmap/SKILL.md +27 -0
- package/agents/skills/jev/SKILL.md +17 -0
- package/agents/skills/listen/SKILL.md +15 -0
- package/agents/skills/pickup/SKILL.md +58 -0
- package/agents/skills/principle-attack-the-premise/SKILL.md +24 -0
- package/agents/skills/principle-boundary-discipline/SKILL.md +38 -0
- package/agents/skills/principle-build-the-lever/SKILL.md +28 -0
- package/agents/skills/principle-encode-lessons-in-structure/SKILL.md +15 -0
- package/agents/skills/principle-exhaust-the-design-space/SKILL.md +12 -0
- package/agents/skills/principle-experience-first/SKILL.md +22 -0
- package/agents/skills/principle-explain-the-number/SKILL.md +21 -0
- package/agents/skills/principle-fix-root-causes/SKILL.md +24 -0
- package/agents/skills/principle-foundational-thinking/SKILL.md +25 -0
- package/agents/skills/principle-guard-the-context-window/SKILL.md +16 -0
- package/agents/skills/principle-laziness-protocol/SKILL.md +13 -0
- package/agents/skills/principle-make-operations-idempotent/SKILL.md +25 -0
- package/agents/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +18 -0
- package/agents/skills/principle-minimize-reader-load/SKILL.md +22 -0
- package/agents/skills/principle-model-the-domain/SKILL.md +26 -0
- package/agents/skills/principle-never-block-on-the-human/SKILL.md +17 -0
- package/agents/skills/principle-outcome-oriented-execution/SKILL.md +25 -0
- package/agents/skills/principle-prove-it-works/SKILL.md +25 -0
- package/agents/skills/principle-redesign-from-first-principles/SKILL.md +18 -0
- package/agents/skills/principle-separate-before-serializing-shared-state/SKILL.md +20 -0
- package/agents/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
- package/agents/skills/principle-subtract-before-you-add/SKILL.md +21 -0
- package/agents/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
- package/agents/skills/principle-type-system-discipline/SKILL.md +17 -0
- package/agents/skills/review-plan/SKILL.md +28 -0
- package/agents/skills/review-roadmap/SKILL.md +28 -0
- package/agents/skills/show-me/SKILL.md +21 -0
- package/agents/skills/speak/SKILL.md +15 -0
- package/agents/skills/su/SKILL.md +11 -0
- package/agents/skills/teach/SKILL.md +25 -0
- package/agents/skills/tmp/SKILL.md +54 -0
- package/agents/skills/typescript-best-practices/SKILL.md +32 -0
- package/agents/skills/typescript-best-practices/references/patterns.md +328 -0
- package/agents/skills/unslop/SKILL.md +62 -0
- package/agents/skills/write-plan/SKILL.md +33 -0
- package/agents/skills/write-roadmap/SKILL.md +66 -0
- package/agents/subagents/implementer-deep.md +57 -0
- package/agents/subagents/implementer-fast.md +57 -0
- package/agents/subagents/orchestrator.md +43 -0
- package/agents/subagents/researcher.md +47 -0
- package/agents/subagents/scout.md +44 -0
- package/bin/repo.ts +4 -0
- package/cli/actionlint-default.test.ts +38 -0
- package/cli/actionlint-download.test.ts +162 -0
- package/cli/actionlint-real.test.ts +33 -0
- package/cli/actionlint.constants.ts +43 -0
- package/cli/actionlint.test.ts +108 -0
- package/cli/actionlint.ts +178 -0
- package/cli/bin.test.ts +54 -0
- package/cli/bin.ts +40 -0
- package/cli/check-changelog.test.ts +40 -0
- package/cli/check-changelog.ts +12 -0
- package/cli/check-comments.test.ts +157 -0
- package/cli/check-comments.ts +123 -0
- package/cli/check-env-modules.test.ts +85 -0
- package/cli/check-env-modules.ts +56 -0
- package/cli/check-magic.test.ts +139 -0
- package/cli/check-magic.ts +120 -0
- package/cli/check-manifests.constants.ts +4 -0
- package/cli/check-manifests.test.ts +210 -0
- package/cli/check-manifests.ts +71 -0
- package/cli/check-nightly.test.ts +103 -0
- package/cli/check-nightly.ts +37 -0
- package/cli/check-ratchets-missing.test.ts +46 -0
- package/cli/check-ratchets-mutation.test.ts +49 -0
- package/cli/check-ratchets.constants.ts +20 -0
- package/cli/check-ratchets.test.ts +190 -0
- package/cli/check-ratchets.ts +193 -0
- package/cli/check-rejections.test.ts +220 -0
- package/cli/check-rejections.ts +153 -0
- package/cli/check-reports.ts +37 -0
- package/cli/check-tsconfig.test.ts +507 -0
- package/cli/check-tsconfig.ts +205 -0
- package/cli/commands.test.ts +114 -0
- package/cli/commands.ts +218 -0
- package/cli/commit-changelog.ts +27 -0
- package/cli/commit-msg-changelog.test.ts +93 -0
- package/cli/commit-msg-range.test.ts +126 -0
- package/cli/commit-msg-red-proof.test.ts +147 -0
- package/cli/commit-msg-repo.test.ts +153 -0
- package/cli/commit-msg.test.ts +101 -0
- package/cli/commit-msg.ts +194 -0
- package/cli/commit-range.constants.ts +17 -0
- package/cli/commit-range.ts +57 -0
- package/cli/config-knip.test.ts +70 -0
- package/cli/config-nightly.test.ts +84 -0
- package/cli/config-tdd.test.ts +46 -0
- package/cli/config.test.ts +576 -0
- package/cli/config.ts +261 -0
- package/cli/conventional.ts +9 -0
- package/cli/coverage-config.ts +62 -0
- package/cli/coverage.constants.ts +6 -0
- package/cli/coverage.test.ts +199 -0
- package/cli/coverage.ts +127 -0
- package/cli/env.constants.ts +10 -0
- package/cli/env.test.ts +56 -0
- package/cli/env.ts +72 -0
- package/cli/fixtures/base-config.ts +13 -0
- package/cli/fixtures/gate-world.ts +48 -0
- package/cli/fixtures/knip-run.ts +38 -0
- package/cli/fixtures/mutate-fakes.ts +137 -0
- package/cli/fixtures/planted-repo.ts +50 -0
- package/cli/fixtures/publish-world.ts +71 -0
- package/cli/fixtures/red-proof-repo.ts +72 -0
- package/cli/fixtures/release-context.ts +15 -0
- package/cli/fixtures/release-world.ts +111 -0
- package/cli/fixtures/stub-bin.ts +40 -0
- package/cli/guard-diagnostics.test.ts +157 -0
- package/cli/guard-diagnostics.ts +195 -0
- package/cli/init.test.ts +487 -0
- package/cli/init.ts +275 -0
- package/cli/knip-check-ignores.test.ts +106 -0
- package/cli/knip-check-real.test.ts +65 -0
- package/cli/knip-check.constants.ts +46 -0
- package/cli/knip-check.test.ts +114 -0
- package/cli/knip-check.ts +169 -0
- package/cli/knip-schema.ts +67 -0
- package/cli/known-failures.constants.ts +22 -0
- package/cli/known-failures.test.ts +138 -0
- package/cli/known-failures.ts +131 -0
- package/cli/lint/as-needs-reason.ts +41 -0
- package/cli/lint/boundaries.ts +73 -0
- package/cli/lint/class-file.ts +39 -0
- package/cli/lint/config-file.ts +32 -0
- package/cli/lint/entry-file.ts +54 -0
- package/cli/lint/env-in-config.ts +66 -0
- package/cli/lint/filename-matches-export.ts +63 -0
- package/cli/lint/index-file.ts +22 -0
- package/cli/lint/index.ts +47 -0
- package/cli/lint/is-reason.ts +6 -0
- package/cli/lint/manifest.ts +46 -0
- package/cli/lint/names.ts +59 -0
- package/cli/lint/no-commented-out-code.ts +105 -0
- package/cli/lint/no-mock-of-subject.ts +64 -0
- package/cli/lint/no-module-side-effects.ts +94 -0
- package/cli/lint/no-module-state.ts +58 -0
- package/cli/lint/no-side-effect-import.ts +17 -0
- package/cli/lint/no-v8-ignore.ts +18 -0
- package/cli/lint/script-rules.ts +9 -0
- package/cli/lint/skip-on-platform.constants.ts +8 -0
- package/cli/lint/skip-on-platform.ts +118 -0
- package/cli/lint/todo-needs-id.ts +63 -0
- package/cli/lint/types-file.ts +28 -0
- package/cli/lint-schema.ts +32 -0
- package/cli/magic/fixtures/magic-repo.ts +64 -0
- package/cli/magic/magic-hit.ts +2 -0
- package/cli/magic/magic-nodes.ts +44 -0
- package/cli/magic/magic-numbers.test.ts +99 -0
- package/cli/magic/magic-numbers.ts +117 -0
- package/cli/magic/magic-strings.test.ts +38 -0
- package/cli/magic/magic-strings.ts +81 -0
- package/cli/main.test.ts +853 -0
- package/cli/main.ts +292 -0
- package/cli/mutate-args.ts +79 -0
- package/cli/mutate-check.test.ts +147 -0
- package/cli/mutate-deps.ts +42 -0
- package/cli/mutate-edges.test.ts +150 -0
- package/cli/mutate-files.test.ts +100 -0
- package/cli/mutate-files.ts +31 -0
- package/cli/mutate-floors.test.ts +91 -0
- package/cli/mutate-floors.ts +104 -0
- package/cli/mutate-real-deps.ts +57 -0
- package/cli/mutate-real.test.ts +96 -0
- package/cli/mutate-report.ts +96 -0
- package/cli/mutate-run.ts +128 -0
- package/cli/mutate-shards.ts +64 -0
- package/cli/mutate.constants.ts +67 -0
- package/cli/mutate.test.ts +182 -0
- package/cli/mutate.ts +64 -0
- package/cli/nested-vitest-flags.ts +8 -0
- package/cli/nightly-config.constants.ts +14 -0
- package/cli/nightly-config.ts +62 -0
- package/cli/prompt.test.ts +202 -0
- package/cli/prompt.ts +1 -0
- package/cli/red-proof-edges.test.ts +78 -0
- package/cli/red-proof-failures.test.ts +82 -0
- package/cli/red-proof-tree.test.ts +157 -0
- package/cli/red-proof-verdict.test.ts +44 -0
- package/cli/red-proof.constants.ts +14 -0
- package/cli/red-proof.ts +202 -0
- package/cli/release/changelog-parse.ts +83 -0
- package/cli/release/changelog.test.ts +141 -0
- package/cli/release/changelog.ts +106 -0
- package/cli/release/gate-refusals.test.ts +85 -0
- package/cli/release/gate.test.ts +63 -0
- package/cli/release/gate.ts +70 -0
- package/cli/release/git.ts +66 -0
- package/cli/release/github.test.ts +128 -0
- package/cli/release/github.ts +46 -0
- package/cli/release/list-dir.test.ts +11 -0
- package/cli/release/list-dir.ts +6 -0
- package/cli/release/main.test.ts +48 -0
- package/cli/release/main.ts +51 -0
- package/cli/release/manifest.test.ts +24 -0
- package/cli/release/manifest.ts +34 -0
- package/cli/release/pack.test.ts +107 -0
- package/cli/release/pack.ts +76 -0
- package/cli/release/plan.ts +127 -0
- package/cli/release/publish-run.test.ts +98 -0
- package/cli/release/publish.test.ts +127 -0
- package/cli/release/publish.ts +125 -0
- package/cli/release/refuse.ts +5 -0
- package/cli/release/release-messages.constants.ts +96 -0
- package/cli/release/release-refusals.test.ts +149 -0
- package/cli/release/release.constants.ts +128 -0
- package/cli/release/release.test.ts +199 -0
- package/cli/release/release.ts +145 -0
- package/cli/release/version.test.ts +118 -0
- package/cli/release/version.ts +69 -0
- package/cli/release/wait.test.ts +101 -0
- package/cli/release/wait.ts +95 -0
- package/cli/script-diagnostics.test.ts +76 -0
- package/cli/script-diagnostics.ts +62 -0
- package/cli/scripts/board.ts +519 -0
- package/cli/scripts/browser.ts +994 -0
- package/cli/scripts/change-set.ts +105 -0
- package/cli/scripts/check-globals.ts +199 -0
- package/cli/scripts/ci-skips.constants.ts +17 -0
- package/cli/scripts/ci-skips.ts +118 -0
- package/cli/scripts/ci-wait-run.ts +28 -0
- package/cli/scripts/ci-wait.constants.ts +44 -0
- package/cli/scripts/ci-wait.ts +175 -0
- package/cli/scripts/ci.constants.ts +15 -0
- package/cli/scripts/ci.ts +208 -0
- package/cli/scripts/clean.ts +363 -0
- package/cli/scripts/comments-step.ts +15 -0
- package/cli/scripts/doctor.ts +828 -0
- package/cli/scripts/emit-agent-shell-tools.ts +91 -0
- package/cli/scripts/emit-mcp-policy.ts +402 -0
- package/cli/scripts/folder-map.ts +353 -0
- package/cli/scripts/gate-fat-tools.ts +1095 -0
- package/cli/scripts/gitignore-guard.ts +338 -0
- package/cli/scripts/google-workspace.ts +779 -0
- package/cli/scripts/hook-command.ts +72 -0
- package/cli/scripts/hook-input.ts +178 -0
- package/cli/scripts/hook-io.constants.ts +5 -0
- package/cli/scripts/hook-io.ts +46 -0
- package/cli/scripts/hooks-union.ts +469 -0
- package/cli/scripts/interactive.ts +30 -0
- package/cli/scripts/jev.ts +420 -0
- package/cli/scripts/job.ts +934 -0
- package/cli/scripts/jsonc.ts +9 -0
- package/cli/scripts/lessons.ts +564 -0
- package/cli/scripts/lint-baseline.ts +233 -0
- package/cli/scripts/lint-main.ts +24 -0
- package/cli/scripts/lint-skips.ts +139 -0
- package/cli/scripts/lint.constants.ts +6 -0
- package/cli/scripts/lint.ts +142 -0
- package/cli/scripts/listen.ts +962 -0
- package/cli/scripts/managed-text.ts +47 -0
- package/cli/scripts/orchestrate-parent.ts +1155 -0
- package/cli/scripts/parse-need.ts +81 -0
- package/cli/scripts/persist-handoff.ts +228 -0
- package/cli/scripts/persist-implementation.ts +276 -0
- package/cli/scripts/persist-plan.ts +888 -0
- package/cli/scripts/persist-roadmap.ts +478 -0
- package/cli/scripts/persist-tmp.ts +177 -0
- package/cli/scripts/persist.ts +54 -0
- package/cli/scripts/phase-files.ts +121 -0
- package/cli/scripts/prompt.ts +38 -0
- package/cli/scripts/purge-global-mcps.ts +313 -0
- package/cli/scripts/rejections-step.ts +37 -0
- package/cli/scripts/rtk-grok.ts +102 -0
- package/cli/scripts/scrub-spawn-prompt.ts +103 -0
- package/cli/scripts/scrub-text-lib.ts +58 -0
- package/cli/scripts/session-map.ts +69 -0
- package/cli/scripts/setup.ts +862 -0
- package/cli/scripts/skill-conflicts.ts +82 -0
- package/cli/scripts/speak.ts +370 -0
- package/cli/scripts/strategy.ts +168 -0
- package/cli/scripts/sync-global-settings.ts +316 -0
- package/cli/scripts/turn-steps.ts +249 -0
- package/cli/scripts/turn.ts +157 -0
- package/cli/scripts/typecheck-step.ts +43 -0
- package/cli/scripts/vscode-tasks.ts +175 -0
- package/cli/scripts/workspace-root.ts +160 -0
- package/cli/scripts/worktree.ts +806 -0
- package/cli/spawn.test.ts +34 -0
- package/cli/spawn.ts +27 -0
- package/cli/staged-change.constants.ts +11 -0
- package/cli/staged-change.ts +79 -0
- package/cli/stryker-preload.test.ts +50 -0
- package/cli/stryker-preload.ts +9 -0
- package/cli/sync/agents/frontmatter.test.ts +197 -0
- package/cli/sync/agents/frontmatter.ts +140 -0
- package/cli/sync/agents/hooks.test.ts +517 -0
- package/cli/sync/agents/hooks.ts +401 -0
- package/cli/sync/agents/rules.test.ts +229 -0
- package/cli/sync/agents/rules.ts +84 -0
- package/cli/sync/agents/sources.test.ts +375 -0
- package/cli/sync/agents/sources.ts +244 -0
- package/cli/sync/agents/subagents.test.ts +149 -0
- package/cli/sync/agents/subagents.ts +158 -0
- package/cli/sync/agents-text.test.ts +31 -0
- package/cli/sync/agents-text.ts +36 -0
- package/cli/sync/fixtures/copied.README.md +35 -0
- package/cli/sync/fixtures/copied.tgz +0 -0
- package/cli/sync/fixtures/maps/golden/INDEX.md +22 -0
- package/cli/sync/fixtures/maps-ws.tgz +0 -0
- package/cli/sync/git-files.test.ts +340 -0
- package/cli/sync/git-files.ts +197 -0
- package/cli/sync/git-hooks.test.ts +94 -0
- package/cli/sync/golden.test.ts +15 -0
- package/cli/sync/hooks-mode.test.ts +99 -0
- package/cli/sync/hosts.ts +28 -0
- package/cli/sync/knip-config.constants.ts +4 -0
- package/cli/sync/knip-config.test.ts +147 -0
- package/cli/sync/knip-config.ts +108 -0
- package/cli/sync/manifest.test.ts +78 -0
- package/cli/sync/manifest.ts +60 -0
- package/cli/sync/maps.test.ts +304 -0
- package/cli/sync/maps.ts +297 -0
- package/cli/sync/migrate.test.ts +2000 -0
- package/cli/sync/migrate.ts +1104 -0
- package/cli/sync/nightly-jobs.test.ts +63 -0
- package/cli/sync/nightly.constants.ts +48 -0
- package/cli/sync/nightly.test.ts +100 -0
- package/cli/sync/nightly.ts +75 -0
- package/cli/sync/outputs.test.ts +351 -0
- package/cli/sync/outputs.ts +168 -0
- package/cli/sync/oxlint-config.test.ts +178 -0
- package/cli/sync/oxlint-config.ts +168 -0
- package/cli/sync/release-jobs.test.ts +29 -0
- package/cli/sync/release-reports.test.ts +31 -0
- package/cli/sync/release-workflow.constants.ts +61 -0
- package/cli/sync/release-workflow.test.ts +192 -0
- package/cli/sync/release-workflow.ts +86 -0
- package/cli/sync/release.constants.ts +93 -0
- package/cli/sync/release.test.ts +187 -0
- package/cli/sync/release.ts +112 -0
- package/cli/sync/repo-dir.test.ts +371 -0
- package/cli/sync/repo-dir.ts +205 -0
- package/cli/sync/standards.constants.ts +8 -0
- package/cli/sync/standards.test.ts +206 -0
- package/cli/sync/standards.ts +179 -0
- package/cli/sync/stock-history.json +2796 -0
- package/cli/sync/stock-history.test.ts +66 -0
- package/cli/sync/stock-history.ts +84 -0
- package/cli/sync/stock.test.ts +47 -0
- package/cli/sync/stock.ts +24 -0
- package/cli/sync/trees.test.ts +621 -0
- package/cli/sync/trees.ts +280 -0
- package/cli/sync/validate.test.ts +299 -0
- package/cli/sync/validate.ts +402 -0
- package/cli/sync/workspace.test.ts +180 -0
- package/cli/sync/workspace.ts +138 -0
- package/cli/sync/write.test.ts +341 -0
- package/cli/sync/write.ts +231 -0
- package/cli/sync.test.ts +1495 -0
- package/cli/sync.ts +360 -0
- package/cli/tdd-scope.test.ts +36 -0
- package/cli/tdd-scope.ts +41 -0
- package/cli/test-files.ts +16 -0
- package/cli/ts-program.ts +69 -0
- package/cli/tsconfig.constants.ts +8 -0
- package/cli/workerstack-config.types.ts +21 -0
- package/dist/cli/env.d.ts +10 -0
- package/dist/cli/env.js +64 -0
- package/dist/presets/temp-root-env.d.ts +10 -0
- package/dist/presets/temp-root-env.js +20 -0
- package/dist/presets/temp-root.constants.d.ts +6 -0
- package/dist/presets/temp-root.constants.js +6 -0
- package/dist/presets/temp-root.d.ts +2 -0
- package/dist/presets/temp-root.js +60 -0
- package/dist/presets/vitest.constants.d.ts +16 -0
- package/dist/presets/vitest.constants.js +16 -0
- package/dist/presets/vitest.d.ts +11 -0
- package/dist/presets/vitest.js +67 -0
- package/dist/presets/workers-plugin.d.ts +10 -0
- package/dist/presets/workers-plugin.js +54 -0
- package/package.json +141 -4
- package/references/INDEX.md +100 -0
- package/references/browser.md +66 -0
- package/references/cascade.md +15 -0
- package/references/clasp.md +21 -0
- package/references/cloudflare.md +36 -0
- package/references/google-workspace.md +79 -0
- package/references/hooks.md +103 -0
- package/references/implementation-format.md +119 -0
- package/references/jev.md +56 -0
- package/references/lessons.md +73 -0
- package/references/listen.md +80 -0
- package/references/map.md +107 -0
- package/references/output.md +47 -0
- package/references/plan-format.md +264 -0
- package/references/review-loop.md +48 -0
- package/references/roadmap-format.md +104 -0
- package/references/rtk.md +27 -0
- package/references/scope.md +76 -0
- package/references/setup.md +42 -0
- package/references/speak.md +59 -0
- package/tsconfig/base.json +28 -0
- package/tsconfig/bun.d.ts +1 -0
- package/tsconfig/bun.json +8 -0
- package/tsconfig/reset.d.ts +1 -0
- package/tsconfig/strict.json +12 -0
- package/tsconfig/web.json +7 -0
- package/tsconfig/worker.d.ts +1 -0
- package/tsconfig/worker.json +8 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { releaseMain } from "../release/release.ts";
|
|
3
|
+
import { spawnRunner } from "../scripts/ci-wait-run.ts";
|
|
4
|
+
import type { Runner } from "../scripts/ci-wait.ts";
|
|
5
|
+
import { git as plantedGit, tempDir, write } from "./planted-repo.ts";
|
|
6
|
+
|
|
7
|
+
export const NAME = "@acme/app";
|
|
8
|
+
export const UNRELEASED_ADDED = "# Changelog\n\n## [Unreleased]\n\n### Added\n\n- x\n\n## [0.1.0] - 2026-10-01\n\n### Added\n\n- first\n";
|
|
9
|
+
export const EMPTY_UNRELEASED = "# Changelog\n\n## [Unreleased]\n\n## [0.1.0] - 2026-10-01\n\n### Added\n\n- first\n";
|
|
10
|
+
|
|
11
|
+
/** A repo at 0.1.0 (tagged unless `tagged` is false) with a bare origin, its first release already out. */
|
|
12
|
+
export type World = { work: string; remote: string };
|
|
13
|
+
|
|
14
|
+
/** Files the world starts with, written over by `files`. */
|
|
15
|
+
export function world(files: Record<string, unknown> = {}, tagged = true): World {
|
|
16
|
+
const dir = tempDir();
|
|
17
|
+
const remote = path.join(dir, "remote.git");
|
|
18
|
+
const work = path.join(dir, "work");
|
|
19
|
+
plantedGit(dir, "init", "-q", "--bare", "-b", "main", remote);
|
|
20
|
+
plantedGit(dir, "init", "-q", "-b", "main", work);
|
|
21
|
+
// The release commits in this checkout with the process's git, so the identity lives in the repo, not in a global config a runner lacks.
|
|
22
|
+
plantedGit(work, "config", "user.name", "t");
|
|
23
|
+
plantedGit(work, "config", "user.email", "t@t");
|
|
24
|
+
plantedGit(work, "config", "commit.gpgsign", "false");
|
|
25
|
+
plantedGit(work, "config", "tag.gpgsign", "false");
|
|
26
|
+
plantedGit(work, "remote", "add", "origin", remote);
|
|
27
|
+
write(work, {
|
|
28
|
+
"package.json": { name: NAME, version: "0.1.0" },
|
|
29
|
+
"workerstack.config.ts": 'export default { packs: ["release"] };\n',
|
|
30
|
+
"CHANGELOG.md": EMPTY_UNRELEASED,
|
|
31
|
+
...files,
|
|
32
|
+
});
|
|
33
|
+
plantedGit(work, "add", "-A");
|
|
34
|
+
plantedGit(work, "commit", "-q", "-m", "chore: init");
|
|
35
|
+
plantedGit(work, "push", "-q", "-u", "origin", "main");
|
|
36
|
+
plantedGit(work, "remote", "set-head", "origin", "main");
|
|
37
|
+
if (tagged) {
|
|
38
|
+
plantedGit(work, "tag", "-a", `${NAME}@0.1.0`, "-m", "first");
|
|
39
|
+
plantedGit(work, "push", "-q", "origin", "--tags");
|
|
40
|
+
}
|
|
41
|
+
return { work, remote };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Writes `files`, commits them as `subject` and pushes. */
|
|
45
|
+
export function commit(w: World, subject: string, files: Record<string, unknown>, push = true): void {
|
|
46
|
+
write(w.work, files);
|
|
47
|
+
plantedGit(w.work, "add", "-A");
|
|
48
|
+
plantedGit(w.work, "commit", "-q", "-m", subject);
|
|
49
|
+
if (push) plantedGit(w.work, "push", "-q", "origin", "main");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A pushed feat with an entry under Unreleased. */
|
|
53
|
+
export function feat(w: World, push = true): void {
|
|
54
|
+
commit(w, "feat: add x", { "src/x.ts": "export const x = 1;\n", "CHANGELOG.md": UNRELEASED_ADDED }, push);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const gitIn = (cwd: string, ...args: string[]): string => plantedGit(cwd, ...args).trim();
|
|
58
|
+
/** Where the checkout and its origin stand: enough to prove a refused run wrote nothing. */
|
|
59
|
+
export const snapshot = (w: World) => ({
|
|
60
|
+
head: gitIn(w.work, "rev-parse", "HEAD"),
|
|
61
|
+
status: gitIn(w.work, "status", "--porcelain"),
|
|
62
|
+
tags: gitIn(w.work, "tag", "-l"),
|
|
63
|
+
remoteTags: gitIn(w.remote, "tag", "-l"),
|
|
64
|
+
remoteMain: gitIn(w.remote, "rev-parse", "main"),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/** What the fake gh answers: a ci run for any commit, ending as `conclusion`. */
|
|
68
|
+
export type Behavior = {
|
|
69
|
+
auth?: boolean;
|
|
70
|
+
run?: boolean;
|
|
71
|
+
conclusion?: string;
|
|
72
|
+
/** Called when the run is watched, to see what exists then. */
|
|
73
|
+
onWatch?: () => void;
|
|
74
|
+
};
|
|
75
|
+
const ok = (stdout = "") => ({ code: 0, stdout, stderr: "" });
|
|
76
|
+
const BAD = { code: 1, stdout: "", stderr: "no" };
|
|
77
|
+
const RUN = { databaseId: 7, createdAt: "2026-11-09T10:00:00Z", headBranch: "main" };
|
|
78
|
+
|
|
79
|
+
export function fakeGh(b: Behavior = {}): Runner {
|
|
80
|
+
return (args) => {
|
|
81
|
+
const key = args.join(" ");
|
|
82
|
+
if (key === "auth status") return b.auth === false ? BAD : ok();
|
|
83
|
+
if (key.startsWith("run list --commit")) return ok(JSON.stringify(b.run === false ? [] : [RUN]));
|
|
84
|
+
if (key.startsWith("run list --branch")) {
|
|
85
|
+
return ok(JSON.stringify([RUN, { databaseId: 8, createdAt: "2026-11-09T10:05:00Z" }]));
|
|
86
|
+
}
|
|
87
|
+
if (key.startsWith("run watch")) return (b.onWatch?.(), ok());
|
|
88
|
+
if (key.startsWith("run view 7 --json")) {
|
|
89
|
+
const conclusion = b.conclusion ?? "success";
|
|
90
|
+
return ok(JSON.stringify({ conclusion, jobs: [{ name: "test", conclusion }] }));
|
|
91
|
+
}
|
|
92
|
+
return key.startsWith("run view 7 --log-failed") ? ok("boom\n") : BAD;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Runs `repo release` in the world's checkout; the exit code and what it printed. */
|
|
97
|
+
export async function release(w: World, argv: string[], gh: Runner = fakeGh()) {
|
|
98
|
+
const out: string[] = [];
|
|
99
|
+
const err: string[] = [];
|
|
100
|
+
let clock = Date.UTC(2026, 10, 9);
|
|
101
|
+
const code = await releaseMain(argv, {
|
|
102
|
+
cwd: w.work,
|
|
103
|
+
stdout: (s) => out.push(s),
|
|
104
|
+
stderr: (s) => err.push(s),
|
|
105
|
+
git: spawnRunner("git"),
|
|
106
|
+
gh,
|
|
107
|
+
now: () => clock,
|
|
108
|
+
sleep: (ms) => ((clock += ms), Promise.resolve()),
|
|
109
|
+
});
|
|
110
|
+
return { code, out: out.join(""), err: err.join("") };
|
|
111
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { BUN } from "../../dev/test-paths.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Plants a command `name` in `dir` that a product starts by name or from PATH: an executable shebang script
|
|
8
|
+
* with `sh` as its body, or on win32 `name.cmd` with `cmd` as its batch body (a shebang script cannot start there).
|
|
9
|
+
* Returns the file written.
|
|
10
|
+
*/
|
|
11
|
+
export function plantStub(dir: string, name: string, body: { sh: string; cmd: string }): string {
|
|
12
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
13
|
+
if (process.platform === "win32") {
|
|
14
|
+
const file = path.join(dir, `${name}.cmd`);
|
|
15
|
+
fs.writeFileSync(file, `@echo off\r\n${body.cmd.replaceAll("\n", "\r\n")}`);
|
|
16
|
+
return file;
|
|
17
|
+
}
|
|
18
|
+
const file = path.join(dir, name);
|
|
19
|
+
fs.writeFileSync(file, `#!/bin/sh\n${body.sh}`, { mode: 0o755 });
|
|
20
|
+
return file;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Plants a command `name` in `dir` that a product starts by its path without a shell, so a `.cmd` will not do on
|
|
25
|
+
* win32: the `js` program, as a shebang script run by bun, or on win32 compiled to `name.exe`. Returns the file.
|
|
26
|
+
*/
|
|
27
|
+
export function plantProgram(dir: string, name: string, js: string): string {
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
const source = path.join(dir, `${name}-source.js`);
|
|
30
|
+
fs.writeFileSync(source, js);
|
|
31
|
+
if (process.platform === "win32") {
|
|
32
|
+
const file = path.join(dir, `${name}.exe`);
|
|
33
|
+
const built = spawnSync(BUN, ["build", "--compile", source, "--outfile", file], { encoding: "utf8" });
|
|
34
|
+
if (built.status !== 0) throw new Error(`bun build --compile failed: ${built.stderr}${built.stdout}`);
|
|
35
|
+
return file;
|
|
36
|
+
}
|
|
37
|
+
const file = path.join(dir, name);
|
|
38
|
+
fs.writeFileSync(file, `#!/bin/sh\nexec ${JSON.stringify(BUN)} ${JSON.stringify(source)} "$@"\n`, { mode: 0o755 });
|
|
39
|
+
return file;
|
|
40
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { afterEach, expect, it } from "vitest";
|
|
2
|
+
import { guardDiagnostics } from "./guard-diagnostics.ts";
|
|
3
|
+
import { cleanTemps, plant, SLOW } from "./magic/fixtures/magic-repo.ts";
|
|
4
|
+
import { cleanTemps as cleanRepos, consumer, SLOW as TURN, turn, write } from "./scripts/fixtures/turn-repo.ts";
|
|
5
|
+
import type { Diagnostic } from "./scripts/lint-baseline.ts";
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
cleanTemps();
|
|
9
|
+
cleanRepos();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const RULE = "repo(guard-rejects)";
|
|
13
|
+
const TSCONFIG = JSON.stringify({
|
|
14
|
+
compilerOptions: { strict: true, module: "preserve", moduleResolution: "bundler", allowImportingTsExtensions: true },
|
|
15
|
+
include: ["src"],
|
|
16
|
+
});
|
|
17
|
+
const USER = [
|
|
18
|
+
"export type User = { name: string };",
|
|
19
|
+
"",
|
|
20
|
+
"export function isUser(value: unknown): value is User {",
|
|
21
|
+
' return typeof value === "object" && value !== null && "name" in value;',
|
|
22
|
+
"}",
|
|
23
|
+
"",
|
|
24
|
+
'export const isName = (value: unknown): value is string => typeof value === "string";',
|
|
25
|
+
"",
|
|
26
|
+
].join("\n");
|
|
27
|
+
|
|
28
|
+
/** A guard no test imports. */
|
|
29
|
+
const OTHER = 'export const isOther = (value: unknown): value is number => typeof value === "number";\n';
|
|
30
|
+
|
|
31
|
+
/** The diagnostic for an untested guard `name` at `at`, `<file>:<line>:<column>`. */
|
|
32
|
+
const untested = (at: string, name: string): Diagnostic => ({
|
|
33
|
+
file: at.slice(0, at.indexOf(":")),
|
|
34
|
+
rule: RULE,
|
|
35
|
+
line: `${at}: error ${RULE}: ${name} is never asserted false; add expect(${name}(<an input it must reject>)).toBe(false) to a test`,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const VITEST = 'import { expect, it } from "vitest";\n';
|
|
39
|
+
|
|
40
|
+
/** A test file over `src/user.ts` with `imports` and `body` in one `it`. */
|
|
41
|
+
const test = (imports: string, body: string): string => `${VITEST}${imports}\n\nit("guards", () => {\n${body}\n});\n`;
|
|
42
|
+
|
|
43
|
+
/** The guard hits of `files` planted beside a tsconfig.json, checking `checked` (all of them by default). */
|
|
44
|
+
function guardsOf(files: Record<string, string>, checked: readonly string[] = Object.keys(files)): Diagnostic[] {
|
|
45
|
+
const found = guardDiagnostics(plant({ "tsconfig.json": TSCONFIG, ...files }), checked);
|
|
46
|
+
if (!found.ok) throw new Error(found.errors.join("\n"));
|
|
47
|
+
return found.value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
it("reports each exported guard no test asserts false, a true-only test included", { timeout: SLOW }, () => {
|
|
51
|
+
const inferred = [
|
|
52
|
+
'export const isCount = (value: unknown) => typeof value === "number";',
|
|
53
|
+
'export const isWord = function (value: unknown): value is string { return typeof value === "string"; };',
|
|
54
|
+
"",
|
|
55
|
+
].join("\n");
|
|
56
|
+
const only = test(
|
|
57
|
+
'import { isUser, isName } from "./user.ts";',
|
|
58
|
+
' expect(isUser({ name: "a" })).toBe(true);\n expect(isName("a")).toBeTruthy();',
|
|
59
|
+
);
|
|
60
|
+
expect(guardsOf({ "src/user.ts": `${USER}${inferred}`, "src/user.test.ts": only })).toEqual([
|
|
61
|
+
untested("src/user.ts:3:17", "isUser"),
|
|
62
|
+
untested("src/user.ts:7:14", "isName"),
|
|
63
|
+
untested("src/user.ts:8:14", "isCount"),
|
|
64
|
+
untested("src/user.ts:9:14", "isWord"),
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it.each(["toBe(false)", "toBeFalsy()", "toEqual(false)"])("passes a guard asserted .%s", { timeout: SLOW }, (form) => {
|
|
69
|
+
const body = ` expect(isUser(1)).${form};\n expect(isName(1)).${form};`;
|
|
70
|
+
const files = { "src/user.ts": USER, "src/user.test.ts": test('import { isUser, isName } from "./user.ts";', body) };
|
|
71
|
+
expect(guardsOf(files)).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("follows a renamed import and a namespace import to the guard", { timeout: SLOW }, () => {
|
|
75
|
+
const renamed = test(
|
|
76
|
+
'import { isUser as looksLikeUser } from "./user.ts";',
|
|
77
|
+
" expect(looksLikeUser(1)).toBe(false);",
|
|
78
|
+
);
|
|
79
|
+
const namespace = test('import * as user from "./user.ts";', " expect(user.isName(1)).toBeFalsy();");
|
|
80
|
+
const files = { "src/user.ts": USER, "src/a.test.ts": renamed, "src/b.test.ts": namespace };
|
|
81
|
+
expect(guardsOf(files, ["src/user.ts"])).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("counts only a guard called as the argument of expect(…) itself", { timeout: SLOW }, () => {
|
|
85
|
+
const body = [
|
|
86
|
+
" const rejected = isUser(1);",
|
|
87
|
+
" expect(rejected).toBe(false);",
|
|
88
|
+
" expect.soft(isUser(1)).toBe(false);",
|
|
89
|
+
" check(isUser(1)).toBe(false);",
|
|
90
|
+
" expect().toBe(false);",
|
|
91
|
+
" rejected.toBe(false);",
|
|
92
|
+
" expect(isUser(1)).toBe(false, 1);",
|
|
93
|
+
" expect(isName(1)).toBe(false);",
|
|
94
|
+
].join("\n");
|
|
95
|
+
const files = { "src/user.ts": USER, "src/user.test.ts": test('import { isUser, isName } from "./user.ts";', body) };
|
|
96
|
+
expect(guardsOf(files)).toEqual([untested("src/user.ts:3:17", "isUser")]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it(
|
|
100
|
+
"skips a guard that is not exported or has no name of its own, an assertion function and a plain boolean function",
|
|
101
|
+
{ timeout: SLOW },
|
|
102
|
+
() => {
|
|
103
|
+
const source = [
|
|
104
|
+
"function isText(value: unknown): value is string {",
|
|
105
|
+
' return typeof value === "string";',
|
|
106
|
+
"}",
|
|
107
|
+
"",
|
|
108
|
+
"export function assertText(value: unknown): asserts value is string {",
|
|
109
|
+
' if (!isText(value)) throw new Error("not text");',
|
|
110
|
+
"}",
|
|
111
|
+
"",
|
|
112
|
+
"export const isEmpty = (text: string): boolean => text.length === 0;",
|
|
113
|
+
'const isLocal = (value: unknown): value is string => typeof value === "string";',
|
|
114
|
+
"export const local = isLocal;",
|
|
115
|
+
'export const { length } = "abc";',
|
|
116
|
+
"export let pending: ((value: unknown) => value is string) | undefined;",
|
|
117
|
+
"export default function (value: unknown): value is number {",
|
|
118
|
+
' return typeof value === "number";',
|
|
119
|
+
"}",
|
|
120
|
+
"",
|
|
121
|
+
].join("\n");
|
|
122
|
+
expect(guardsOf({ "src/text.ts": source })).toEqual([]);
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
it("re-checks the guards of the sources a changed test imports, when only the test changed", { timeout: SLOW }, () => {
|
|
127
|
+
const trueOnly = test(
|
|
128
|
+
'import { isUser, isName } from "./user.ts";',
|
|
129
|
+
" expect(isUser(1)).toBe(true);\n expect(isName(1)).toBe(false);",
|
|
130
|
+
);
|
|
131
|
+
const files = { "src/user.ts": USER, "src/user.test.ts": trueOnly, "src/other.ts": OTHER };
|
|
132
|
+
expect(guardsOf(files, ["src/user.test.ts"])).toEqual([untested("src/user.ts:3:17", "isUser")]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("reports a tsconfig.json typescript cannot read", () => {
|
|
136
|
+
const root = plant({ "pkg/tsconfig.json": "{", "pkg/a.ts": USER });
|
|
137
|
+
const found = guardDiagnostics(root, ["pkg/a.ts"]);
|
|
138
|
+
expect(found.ok ? [] : found.errors).toEqual([expect.stringMatching(/^pkg\/tsconfig\.json: .+/)]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("fails a turn that touches only a test and drops the last false assert", { timeout: TURN }, async () => {
|
|
142
|
+
const asserted = test(
|
|
143
|
+
'import { isUser, isName } from "./user.ts";',
|
|
144
|
+
" expect(isUser(1)).toBe(false);\n expect(isName(1)).toBe(false);",
|
|
145
|
+
// The consumer installs no vitest, and the boundaries rule refuses an undeclared import; the globals stand in.
|
|
146
|
+
).replace(VITEST, "");
|
|
147
|
+
const root = consumer({ "src/user.ts": USER, "src/user.test.ts": asserted });
|
|
148
|
+
write(root, {
|
|
149
|
+
"src/user.test.ts": asserted.replace("expect(isUser(1)).toBe(false)", "expect(isUser(1)).toBe(true)"),
|
|
150
|
+
});
|
|
151
|
+
const result = await turn(root, [], { tsc: () => ({ code: 0, stdout: "", stderr: "" }), vitest: () => null });
|
|
152
|
+
expect(result.err).toBe(
|
|
153
|
+
`${untested("src/user.ts:3:17", "isUser").line}\nrepo turn: src/user.ts has 1 repo(guard-rejects), the baseline allows 0; fix the new one\n`,
|
|
154
|
+
);
|
|
155
|
+
expect(result.out).toMatch(/^turn: 1 file · fmt ok · comments ok · lint 1 over baseline · /);
|
|
156
|
+
expect(result.code).toBe(1);
|
|
157
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guards proven with an input they must reject: an exported function whose return type is a type predicate needs
|
|
3
|
+
* a test that asserts it returns false, else a lint diagnostic `repo(guard-rejects)` the baseline counts. A guard
|
|
4
|
+
* tested only on what it accepts still passes a guard that accepts everything. The tests are read with the type
|
|
5
|
+
* checker, so an import renamed or through a namespace still reaches the guard.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import ts from "typescript";
|
|
10
|
+
import { GUARD_REJECTS } from "./lint/script-rules.ts";
|
|
11
|
+
import type { Diagnostic, Result } from "./scripts/lint-baseline.ts";
|
|
12
|
+
import { codeOf } from "./scripts/lint-skips.ts";
|
|
13
|
+
import { RUNTIME_CONFIG } from "./tsconfig.constants.ts";
|
|
14
|
+
import { isChecked, isTest, ownerOf, posix, setupOf } from "./ts-program.ts";
|
|
15
|
+
|
|
16
|
+
/** An exported guard: its declared name node and the symbol a call resolves to. */
|
|
17
|
+
type Guard = { name: ts.Identifier; symbol: ts.Symbol };
|
|
18
|
+
/** One tsconfig's files, as program paths: the sources to report in and the changed tests whose imports join them. */
|
|
19
|
+
type Group = { root: string; dir: string | null; sources: string[]; tests: string[] };
|
|
20
|
+
type FunctionLike = ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression;
|
|
21
|
+
|
|
22
|
+
const RULE = /* @__PURE__ */ codeOf(GUARD_REJECTS);
|
|
23
|
+
/** The asserts that a call returned false; a test file without one cannot prove a guard rejects. */
|
|
24
|
+
const FALSE_ASSERT = /\.(?:toBe|toEqual)\(\s*false\s*\)|\.toBeFalsy\(\s*\)/;
|
|
25
|
+
/** Each matcher that can assert false, by the argument count that does: `toBe(false)` and `toBeFalsy()`. */
|
|
26
|
+
const FALSE_MATCHERS: ReadonlyMap<string, number> = /* @__PURE__ */ new Map([
|
|
27
|
+
["toBe", 1],
|
|
28
|
+
["toEqual", 1],
|
|
29
|
+
["toBeFalsy", 0],
|
|
30
|
+
]);
|
|
31
|
+
/** vitest's `expect`, as the checker spells an identifier. */
|
|
32
|
+
const EXPECT: ts.__String = /* @__PURE__ */ ts.escapeLeadingUnderscores("expect");
|
|
33
|
+
|
|
34
|
+
const message = (name: string): string =>
|
|
35
|
+
`${name} is never asserted false; add expect(${name}(<an input it must reject>)).toBe(false) to a test`;
|
|
36
|
+
|
|
37
|
+
const isExported = (node: ts.Declaration): boolean =>
|
|
38
|
+
(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0;
|
|
39
|
+
|
|
40
|
+
/** Whether `fn` returns `x is T` or `this is T`, written or inferred; an `asserts` function is no guard. */
|
|
41
|
+
function isGuard(fn: FunctionLike, checker: ts.TypeChecker): boolean {
|
|
42
|
+
const signature = checker.getSignatureFromDeclaration(fn);
|
|
43
|
+
const predicate = signature && checker.getTypePredicateOfSignature(signature);
|
|
44
|
+
const kind = predicate?.kind;
|
|
45
|
+
return kind === ts.TypePredicateKind.Identifier || kind === ts.TypePredicateKind.This;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The exported function declarations and `const` function values of a statement, with their names. */
|
|
49
|
+
function functionsOf(statement: ts.Statement): { name: ts.Identifier; fn: FunctionLike }[] {
|
|
50
|
+
if (ts.isFunctionDeclaration(statement) && statement.name && isExported(statement))
|
|
51
|
+
return [{ name: statement.name, fn: statement }];
|
|
52
|
+
if (!ts.isVariableStatement(statement)) return [];
|
|
53
|
+
return statement.declarationList.declarations.flatMap((declaration) => {
|
|
54
|
+
const init = declaration.initializer;
|
|
55
|
+
const isFunction = init !== undefined && (ts.isArrowFunction(init) || ts.isFunctionExpression(init));
|
|
56
|
+
if (!isFunction || !ts.isIdentifier(declaration.name) || !isExported(declaration)) return [];
|
|
57
|
+
return [{ name: declaration.name, fn: init }];
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function guardsOf(file: ts.SourceFile, checker: ts.TypeChecker): Guard[] {
|
|
62
|
+
return file.statements.flatMap((statement) =>
|
|
63
|
+
functionsOf(statement).flatMap(({ name, fn }) => {
|
|
64
|
+
const symbol = checker.getSymbolAtLocation(name);
|
|
65
|
+
return symbol !== undefined && isGuard(fn, checker) ? [{ name, symbol }] : [];
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The symbol an imported name finally declares, through renames, namespaces and re-exports. */
|
|
71
|
+
function resolved(symbol: ts.Symbol, checker: ts.TypeChecker): ts.Symbol {
|
|
72
|
+
return (symbol.flags & ts.SymbolFlags.Alias) === 0 ? symbol : checker.getAliasedSymbol(symbol);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Whether `call` is a matcher asserting false: `.toBe(false)`, `.toEqual(false)` or `.toBeFalsy()`. */
|
|
76
|
+
function assertsFalse(
|
|
77
|
+
call: ts.CallExpression,
|
|
78
|
+
): call is ts.CallExpression & { expression: ts.PropertyAccessExpression } {
|
|
79
|
+
if (!ts.isPropertyAccessExpression(call.expression)) return false;
|
|
80
|
+
const count = FALSE_MATCHERS.get(call.expression.name.text);
|
|
81
|
+
if (count !== call.arguments.length) return false;
|
|
82
|
+
const [arg] = call.arguments;
|
|
83
|
+
return arg === undefined || arg.kind === ts.SyntaxKind.FalseKeyword;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The function `call` asserts false the result of, as in `expect(isUser(input)).toBe(false)`. */
|
|
87
|
+
function falseCallee(call: ts.CallExpression): ts.Node | undefined {
|
|
88
|
+
if (!assertsFalse(call)) return undefined;
|
|
89
|
+
const expected = call.expression.expression;
|
|
90
|
+
if (!ts.isCallExpression(expected) || !ts.isIdentifier(expected.expression)) return undefined;
|
|
91
|
+
if (expected.expression.escapedText !== EXPECT) return undefined;
|
|
92
|
+
const [subject] = expected.arguments;
|
|
93
|
+
if (subject === undefined || !ts.isCallExpression(subject)) return undefined;
|
|
94
|
+
const callee = subject.expression;
|
|
95
|
+
return ts.isPropertyAccessExpression(callee) ? callee.name : callee;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Every symbol a test file asserts a call of returns false. */
|
|
99
|
+
function assertedFalse(file: ts.SourceFile, checker: ts.TypeChecker): ts.Symbol[] {
|
|
100
|
+
const found: ts.Symbol[] = [];
|
|
101
|
+
const visit = (node: ts.Node): void => {
|
|
102
|
+
const callee = ts.isCallExpression(node) ? falseCallee(node) : undefined;
|
|
103
|
+
const symbol = callee && checker.getSymbolAtLocation(callee);
|
|
104
|
+
if (symbol) found.push(resolved(symbol, checker));
|
|
105
|
+
ts.forEachChild(node, visit);
|
|
106
|
+
};
|
|
107
|
+
visit(file);
|
|
108
|
+
return found;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The program paths of the modules `file` imports. */
|
|
112
|
+
function importsOf(file: ts.SourceFile, checker: ts.TypeChecker): string[] {
|
|
113
|
+
return file.statements.flatMap((statement) => {
|
|
114
|
+
if (!ts.isImportDeclaration(statement)) return [];
|
|
115
|
+
const declaration = checker.getSymbolAtLocation(statement.moduleSpecifier)?.valueDeclaration;
|
|
116
|
+
return declaration !== undefined && ts.isSourceFile(declaration) ? [declaration.fileName] : [];
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The tsconfig's tests that assert some call false, read as text first so a program parses only those. */
|
|
121
|
+
function falseTests(group: Group, fileNames: readonly string[]): string[] {
|
|
122
|
+
const owned = fileNames.filter((fileName) => {
|
|
123
|
+
const rel = path.posix.relative(group.root, fileName);
|
|
124
|
+
return isTest(rel) && ownerOf(group.root, rel) === group.dir;
|
|
125
|
+
});
|
|
126
|
+
return owned.filter((fileName) => FALSE_ASSERT.test(fs.readFileSync(fileName, "utf8")));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The sources to report in: the group's, and those its changed tests import from the same tsconfig. */
|
|
130
|
+
function targetsOf(group: Group, program: ts.Program): string[] {
|
|
131
|
+
const checker = program.getTypeChecker();
|
|
132
|
+
const tests = program.getSourceFiles().filter((file) => group.tests.includes(file.fileName));
|
|
133
|
+
const imported = tests.flatMap((file) => importsOf(file, checker));
|
|
134
|
+
const owned = imported.filter((fileName) => {
|
|
135
|
+
const rel = path.posix.relative(group.root, fileName);
|
|
136
|
+
return isChecked(rel) && ownerOf(group.root, rel) === group.dir;
|
|
137
|
+
});
|
|
138
|
+
return [...new Set([...group.sources, ...owned])];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The guards declared in `targets`. */
|
|
142
|
+
function guardsIn(program: ts.Program, targets: readonly string[]): Guard[] {
|
|
143
|
+
const checker = program.getTypeChecker();
|
|
144
|
+
const files = program.getSourceFiles().filter((file) => targets.includes(file.fileName));
|
|
145
|
+
return files.flatMap((file) => guardsOf(file, checker));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* One group's untested guards. A first program finds the targets and whether they declare a guard; only then a second
|
|
150
|
+
* adds the tests that could assert one false, reusing what the first parsed.
|
|
151
|
+
*/
|
|
152
|
+
function groupGuards(group: Group): Result<Guard[]> {
|
|
153
|
+
const setup = setupOf(group.root, group.dir, RUNTIME_CONFIG);
|
|
154
|
+
if (!setup.ok) return setup;
|
|
155
|
+
const { options } = setup.value;
|
|
156
|
+
const first = ts.createProgram({ rootNames: [...group.sources, ...group.tests], options });
|
|
157
|
+
const targets = targetsOf(group, first);
|
|
158
|
+
if (guardsIn(first, targets).length === 0) return { ok: true, value: [] };
|
|
159
|
+
const tests = [...new Set([...falseTests(group, setup.value.fileNames), ...group.tests])];
|
|
160
|
+
const program = ts.createProgram({ rootNames: [...targets, ...tests], options, oldProgram: first });
|
|
161
|
+
const checker = program.getTypeChecker();
|
|
162
|
+
const testFiles = program.getSourceFiles().filter((file) => tests.includes(file.fileName));
|
|
163
|
+
const asserted = new Set(testFiles.flatMap((file) => assertedFalse(file, checker)));
|
|
164
|
+
return { ok: true, value: guardsIn(program, targets).filter((guard) => !asserted.has(guard.symbol)) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function diagnosticOf(root: string, { name }: Guard): Diagnostic {
|
|
168
|
+
const file = name.getSourceFile();
|
|
169
|
+
const rel = path.posix.relative(root, file.fileName);
|
|
170
|
+
const { line, character } = file.getLineAndCharacterOfPosition(name.getStart());
|
|
171
|
+
return { file: rel, rule: RULE, line: `${rel}:${line + 1}:${character + 1}: error ${RULE}: ${message(name.text)}` };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The untested exported guards of the checked sources among `files` (root-relative POSIX paths), and of the sources
|
|
176
|
+
* the tests among them import, in oxlint's line form; or why a tsconfig.json could not be read. A guard passes when a
|
|
177
|
+
* test of its tsconfig asserts a call of it false.
|
|
178
|
+
*/
|
|
179
|
+
export function guardDiagnostics(root: string, files: readonly string[]): Result<Diagnostic[]> {
|
|
180
|
+
const real = posix(fs.realpathSync(root));
|
|
181
|
+
const groups = new Map<string | null, Group>();
|
|
182
|
+
for (const rel of files.filter((file) => isChecked(file) || isTest(file))) {
|
|
183
|
+
const dir = ownerOf(real, rel);
|
|
184
|
+
const group = groups.get(dir) ?? { root: real, dir, sources: [], tests: [] };
|
|
185
|
+
(isTest(rel) ? group.tests : group.sources).push(path.posix.join(real, rel));
|
|
186
|
+
groups.set(dir, group);
|
|
187
|
+
}
|
|
188
|
+
const diagnostics: Diagnostic[] = [];
|
|
189
|
+
for (const group of groups.values()) {
|
|
190
|
+
const found = groupGuards(group);
|
|
191
|
+
if (!found.ok) return found;
|
|
192
|
+
diagnostics.push(...found.value.map((guard) => diagnosticOf(real, guard)));
|
|
193
|
+
}
|
|
194
|
+
return { ok: true, value: diagnostics };
|
|
195
|
+
}
|