Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 63 additions & 5 deletions lib/rules/cicd_rules.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
113 changes: 113 additions & 0 deletions test/rules/cicd_rules_typescript_test.exs
Original file line number Diff line number Diff line change
@@ -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
Loading