diff --git a/lib/rules/cicd_rules.ex b/lib/rules/cicd_rules.ex index edf35860..56613a11 100644 --- a/lib/rules/cicd_rules.ex +++ b/lib/rules/cicd_rules.ex @@ -64,11 +64,69 @@ defmodule Hypatia.Rules.CicdRules do @blocked_patterns [ # Lang-policy refresh 2026-05-25: TypeScript / ReScript / migrated-JS # all replaced by AffineScript (the estate's go-forward language). - # Existing approved carve-outs (e.g. `.d.ts` declaration files, Deno - # test-runner .ts in `affinescript-deno-test/`, JS shims in - # `affinescript-cli/`) are honoured via ScannerSuppression — these - # entries gate NEW occurrences only. - %{id: :typescript_detected, glob: "*.ts", reason: "TypeScript banned -- use AffineScript"}, + # TS ban (org policy 2026-04-30 for NEW files; existing TS grandfathered + # while in-flight migration to AffineScript proceeds — see project + # tracker `project_estate_ts_to_affinescript_2026_05_28.md`). + # Path-prefix allowlist covers seven classes of legitimate `.ts` presence: + # + # (1) Declaration files (`.d.ts`) — FFI/library type definitions are + # headers, not implementation; they're the boundary, not the code. + # + # (2) Interop targets — directories where we author non-TS code that + # EXPOSES our work to TS/Deno consumers (parallel to V-lang + # v-cartridge/v-adapter/v-bindings/v-client carve-out). + # Pattern: `*/bindings/deno/`, `*/bindings/typescript/`, + # `*/bindings/ts/`. Exemplar: `proven/bindings/deno/` (72 files + # exposing Idris2 ABI to Deno consumers). + # + # (3) PERMANENT exemption — `avow-protocol/telegram-bot/avow-telegram-bot/`: + # Telegraf / node-telegram-bot-api are the canonical TS-native + # Bot API libraries; no AffineScript binding planned. + # + # (4) Tooling configs — `vite.config.ts`, `vitest.config.ts`, + # `tsup.config.ts`, `*.config.ts` are build orchestration, + # not application code. + # + # (5) Bootstrap shims — `affinescript-deno-test/` (Deno test runner) + # and `affinescript-cli/` (CLI bootstrap) carry TS/JS shims that + # bootstrap the AffineScript toolchain itself. + # + # (6) Upstream forks not estate-authored — `rescript/` (ReScript + # compiler), `servers/` (third-party MCP servers), + # `repos-monorepo/` (mass aggregator). + # + # (7) Archived repos — GitHub-archived repos cannot accept PRs; + # their TS is dormant. `hyperpolymath-archive/**`. + %{id: :typescript_detected, glob: "*.ts", + reason: "TypeScript banned in NEW code -- use AffineScript (org policy 2026-04-30; existing TS grandfathered while in-flight migration proceeds, see project_estate_ts_to_affinescript_2026_05_28)", + # check_pattern uses String.contains?/2 so these entries match as + # substrings anywhere in the file path — both directory prefixes + # (e.g., "/bindings/deno/") and suffix patterns (e.g., ".d.ts", + # "vite.config.ts") work uniformly. + path_allow_prefixes: [ + # (1) Declaration files — FFI/library type definitions + ".d.ts", + # (2) Interop targets — TS/Deno consumer-facing bindings + "/bindings/deno/", + "/bindings/typescript/", + "/bindings/ts/", + # (3) PERMANENT exemption — Telegraf + "avow-protocol/telegram-bot/avow-telegram-bot/", + # (4) Tooling configs (matched as suffix substrings) + "vite.config.ts", + "vitest.config.ts", + "tsup.config.ts", + "tsconfig.json", + # (5) Bootstrap shims + "affinescript-deno-test/", + "affinescript-cli/", + # (6) Upstream forks + "rescript/", + "servers/", + "repos-monorepo/", + # (7) Archived repos + "hyperpolymath-archive/" + ]}, %{id: :rescript_detected, glob: "*.res", reason: "ReScript banned -- use AffineScript (org policy 2026-05-25; see #57 migration assistant)"}, %{id: :rescript_interface_detected, glob: "*.resi", reason: "ReScript banned -- use AffineScript (org policy 2026-05-25; see #57 migration assistant)"}, %{id: :nodejs_detected, glob: "package-lock.json", reason: "Node.js banned -- use Deno"}, diff --git a/test/rules/cicd_rules_typescript_test.exs b/test/rules/cicd_rules_typescript_test.exs new file mode 100644 index 00000000..eb811163 --- /dev/null +++ b/test/rules/cicd_rules_typescript_test.exs @@ -0,0 +1,113 @@ +# SPDX-License-Identifier: MPL-2.0 + +defmodule Hypatia.Rules.CicdRules.TypescriptTest do + use ExUnit.Case, async: true + + alias Hypatia.Rules.CicdRules + + describe "typescript_detected rule" do + test "flags TS source files outside the allowlist" do + files = [ + "idaptik/idaptik/idaptik-dlc-iky/core/vm.ts", + "wordpress-tools/praxis/SymbolicEngine/swarm/src/executor.ts", + "lcb-website/src/index.ts" + ] + + results = CicdRules.check_commit_blocks(files) + ts = Enum.find(results, &(&1.rule == :typescript_detected)) + + assert ts, "expected :typescript_detected finding for non-exempt .ts files" + assert length(ts.files) == 3 + assert ts.reason =~ "TypeScript banned in NEW code" + assert ts.reason =~ "AffineScript" + end + + test "exempts .d.ts declaration files (FFI/library headers)" do + files = [ + "rrecord-verity/WebExtensions.d.ts", + "tma-mark2/types/global.d.ts", + "developer-ecosystem/zerotier-k8s-link/index.d.ts" + ] + + results = CicdRules.check_commit_blocks(files) + assert Enum.find(results, &(&1.rule == :typescript_detected)) == nil, + ".d.ts declaration files are exempt (headers, not implementation)" + end + + test "exempts */bindings/{deno,typescript,ts}/ interop targets" do + files = [ + "proven/bindings/deno/mod.ts", + "proven/bindings/deno/src/safe_unit.ts", + "some-lib/bindings/typescript/index.ts", + "another-lib/bindings/ts/client.ts" + ] + + results = CicdRules.check_commit_blocks(files) + assert Enum.find(results, &(&1.rule == :typescript_detected)) == nil, + "consumer-facing TS bindings are exempt (parallel to v-bindings/v-adapter)" + end + + test "exempts avow-protocol/telegram-bot/avow-telegram-bot/ (Telegraf PERMANENT)" do + files = [ + "avow-protocol/telegram-bot/avow-telegram-bot/src/bot.ts", + "avow-protocol/telegram-bot/avow-telegram-bot/test-mock.ts" + ] + + results = CicdRules.check_commit_blocks(files) + assert Enum.find(results, &(&1.rule == :typescript_detected)) == nil + end + + test "exempts tooling configs (vite/vitest/tsup/tsconfig)" do + files = [ + "hyperpolymath-archive/zotero-nesy/vite.config.ts", + "some-repo/vitest.config.ts", + "another-repo/tsup.config.ts" + ] + + results = CicdRules.check_commit_blocks(files) + assert Enum.find(results, &(&1.rule == :typescript_detected)) == nil, + "build orchestration is exempt (not application code)" + end + + test "exempts affinescript-deno-test and affinescript-cli bootstrap shims" do + files = [ + "affinescript-deno-test/runner.ts", + "affinescript-cli/bin/cli.ts" + ] + + results = CicdRules.check_commit_blocks(files) + assert Enum.find(results, &(&1.rule == :typescript_detected)) == nil + end + + test "exempts upstream-fork repos (rescript/servers/repos-monorepo)" do + files = [ + "rescript/jscomp/test/test.ts", + "servers/src/everything/index.ts", + "repos-monorepo/some/path/file.ts" + ] + + results = CicdRules.check_commit_blocks(files) + assert Enum.find(results, &(&1.rule == :typescript_detected)) == nil, + "upstream forks are not estate-authored — vendored as-is" + end + + test "exempts hyperpolymath-archive/** (archived repos)" do + files = [ + "hyperpolymath-archive/avow-telegram-bot/test-mock.ts", + "hyperpolymath-archive/some-old-project/src/main.ts" + ] + + results = CicdRules.check_commit_blocks(files) + assert Enum.find(results, &(&1.rule == :typescript_detected)) == nil + end + + test "flags new TS even with carve-out-like names but outside carve-out paths" do + # `proven/src/something.ts` is NOT in `proven/bindings/deno/`, + # so it MUST still flag. + files = ["proven/src/handler.ts"] + + results = CicdRules.check_commit_blocks(files) + assert Enum.find(results, &(&1.rule == :typescript_detected)) + end + end +end