Skip to content

build(typescript): eliminate hand-written JavaScript and lock it out with a CI gate - #9534

Merged
JSONbored merged 5 commits into
mainfrom
feat/typescript-lock-9527
Jul 28, 2026
Merged

build(typescript): eliminate hand-written JavaScript and lock it out with a CI gate#9534
JSONbored merged 5 commits into
mainfrom
feat/typescript-lock-9527

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Eliminates every hand-written JavaScript file in the repo and makes the state permanent with a CI gate.

Census (committed in the PR, not the repo): 37 tracked .mjs/.cjs + 3 .js + 7 .d.ts. Classified and resolved as:

Outcome Count What
Ported to .ts 18 Both packages' check-syntax + strip-bin-sourcemap, control-plane-coverage, actionlint-download-attempts, generate-env-reference, both eslint configs, three benchmark/load-test scripts, and control-plane / discovery-index / review-enrichment's gen-cf-typegen, validate-sourcemaps, validate-posthog-release, generate-analyzer-metadata
Allowlisted with a reason 4 + fixtures test/fixtures/** (subprocess doubles spawned by plain node, several deliberately malformed), gittensor-score-preview.mjs (ships in the npm tarball, runs under end users' own node), sw.js (served byte-for-byte to browsers), rees-coverage-chdir.cjs (node --require only loads CommonJS)
Ambient .d.ts, allowlisted 4 env.d.ts ×3 and the gifenc module stub — ambient declaration is what .d.ts is for
Generated, exempt by name 3 worker-configuration.d.tscf-typegen:check already proves these match their generator, a stronger guarantee than a reason string

The gate (scripts/validate-no-hand-written-js.ts, wired into test:ci) fails on any tracked JavaScript or hand-written .d.ts outside the allowlist. Its anti-rot guard: an allowlist entry matching nothing tracked fails the check. A watched path that silently stops existing is exactly how metagraphed's MCP version-sync workflow died unnoticed for months — that lesson is encoded here rather than repeated.

Defects the port surfaced

Typing previously-untyped files found real bugs, all fixed:

  • fetchSymbolSets returns a bare array, but its own call sites treated it as {results} — the untyped version silently worked only because both shapes were handled defensively at one call site.
  • loadPostHogReleaseValidationConfig was implicitly typed against the app's full ProcessEnv once ported, which no caller could satisfy (~50 unrelated vars for a script that reads three). Now typed to exactly what it reads.
  • generate-env-reference dereferenced a regex capture group and a nullable defaultValue without guarding either — both real noUncheckedIndexedAccess violations that were invisible in .mjs.

Regressions avoided

  • check-syntax scanned scripts/*.mjs, so porting those files would have silently removed them from the only check watching them. Both packages' scripts now scan .ts too (node --check accepts it under --experimental-strip-types; verified it still rejects broken syntax). File counts unchanged: 9 and 136.
  • The two upload-sourcemaps spawn sites pass --experimental-strip-types explicitly rather than relying on Node 22.18+ enabling it by default, because engines.node still allows 22.0.
  • generate-env-reference embeds its own filename in both artifacts it emits; regenerated, and --check confirms 48 references unchanged in each — the port is provably behavior-preserving.

Closes #9527

Scope

  • The PR title follows type(scope): short summary Conventional Commit format, for example fix(api): restore profile access checks.
  • This PR is focused and does not mix unrelated backend, UI, MCP, docs, dependency, and deploy changes.
  • This follows CONTRIBUTING.md and does not reintroduce GitHub Pages, VitePress, site/, or CNAME.
  • I linked a currently open issue this PR resolves (e.g. Closes #123) — a linked open issue is required for every contributor PR.

Validation

  • git diff --check
  • npm run actionlint
  • npm run typecheck
  • npm run test:coverage locally (scoped — see note)
  • npm run test:workers
  • npm run build:mcp
  • npm run test:mcp-pack
  • npm run ui:openapi:check
  • npm run ui:lint
  • npm run ui:typecheck
  • npm run ui:build
  • npm audit --audit-level=moderate
  • New or changed behavior has unit/integration tests for new branches, fallback paths, and sanitizer boundaries

Green: 232 tests across the 18 affected vitest suites, the full 1,379-test review-enrichment suite (rees:test), build:mcp + build:miner (both check-syntax runs at full file counts), test:mcp-pack, test:miner-pack, cf-typegen:check, docs:drift-check, miner:env-reference:check, and ui:lint (0 errors on the ported eslint config). The gate has 13 of its own tests covering both sides of every branch — allowlist prefix vs exact match, generated vs hand-written .d.ts, stale-entry detection — plus one that runs the real gate against the real tree.

If any required check was skipped, explain why:

  • Unchecked items cover surfaces this diff does not touch (no OpenAPI, worker, or UI source changes). CI runs them in full.

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized, low-noise, and does not imply compensation guarantees or optimization tactics.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes include negative-path tests. (n/a — none touched)
  • API/OpenAPI/MCP behavior is updated and tested where needed. (No behavior change: renames plus type annotations, with generator output byte-verified.)
  • UI changes use live API data or real empty/error/loading states, not production mock/demo fallbacks. (n/a)
  • Visible UI changes include a UI Evidence section below. (n/a — no visible change)
  • Public docs/changelogs are updated where needed; changelogs are only edited for release-prep PRs.

Notes

Part of #9515 — the language-layer half of "nothing hand-maintained". Independent of every other sub-issue.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui ce4aa1c Commit Preview URL

Branch Preview URL
Jul 28 2026, 08:22 AM

@JSONbored JSONbored self-assigned this Jul 28, 2026
@loopover-orb

loopover-orb Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Warning

⏸️ LoopOver review result - manual review recommended

Review updated: 2026-07-28 08:41:47 UTC

55 files · 1 AI reviewer · no blockers · CI green · clean

⏸️ Suggested Action - Manual Review

  • Touches a guarded path — held for manual review: This PR changes guardrail-protected path(s): .github/workflows/ci.yml (matched .github/workflows/**), .github/workflows/release-selfhost.yml (matched .github/workflows/**).

Review summary
This PR mechanically ports the repo's remaining hand-written .mjs/.js/.cjs files to TypeScript and adds a permanent CI gate (scripts/validate-no-hand-written-js.ts) with directory-prefix allowlisting, a stale-entry anti-rot check, and solid unit test coverage (test/unit/validate-no-hand-written-js.test.ts). The port surfaced and fixed a real latent bug in fetchSymbolSets (bare-array vs {results} response shape mismatch), and every renamed script's callers (package.json scripts, workflow YAML, other scripts' string references, and test assertions) were updated consistently in the same diff. Both check-syntax.ts variants were extended to also `--check` the new `scripts/*.ts` files under `--experimental-strip-types`, closing the gap that porting itself would otherwise have opened in that verification.

Nits — 7 non-blocking
  • scripts/validate-no-hand-written-js.ts:93-102 uses console.log/console.error directly, which is expected for a CLI reporting tool but is worth confirming it's intentionally exempt from any repo console-usage lint rule.
  • packages/discovery-index/scripts/validate-posthog-release.ts:70 (symbolSetsUrl) falls back to `config.projectId ?? ""` with a comment explaining requireConfig already guarantees non-null at runtime — consider a small assertion helper instead of a silent empty-string fallback so a future refactor that removes the requireConfig call ordering fails loudly rather than silently hitting a malformed URL.
  • The GENERATED_BASENAMES exemption in scripts/validate-no-hand-written-js.ts matches by basename anywhere in the tree, not just the three known worker-configuration.d.ts locations — intentional per the comment, but worth a one-line note that this is deliberately broad.
  • CI's validate/validate-tests checks failed with no detail provided, and this PR's branch is 2 commits behind the current default branch — that's the more likely explanation than a defect in this diff; worth rebasing to confirm before merge.
  • Rebase onto the latest default branch to rule out the undetailed validate/validate-tests failures being caused by commits that landed after this branch diverged.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.
  • Touches a guarded path — held for manual review — A maintainer must review and merge this change.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ⚠️ Gate result — Not blocking (Advisory; not blocking this PR.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9527
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ❌ 8/20 High review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 14 registered-repo PR(s), 13 merged, 326 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 326 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Linked issue satisfaction

Addressed
The PR delivers a census in the description, ports the sampled hand-written scripts (env-reference generator, MCP/miner check-syntax and strip-bin-sourcemap, control-plane-coverage, benchmark/cross-repo-evaluation/load-test scripts, discovery-index/review-enrichment scripts) to .ts, adds an unconditional scripts/validate-no-hand-written-js.ts gate wired into test:ci with an existence-checked, reas

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Python, TypeScript, Ruby, Go, MDX, Shell, Solidity, JavaScript
  • Official Gittensor activity: 14 PR(s), 326 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Then work through the remaining 2 steps in the Signals table above.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask <question> answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat <question> answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Decision record
  • action: hold · clause: guardrail_hold
  • config: 7968da4797fbbec1433f2a6d4659b886fd7c55e56ef268b1ccf8e8bf3e6a9015 · pack: oss-anti-slop · ci: failed
  • record: f57e9cf9839b0cc58cfe2df992a6ff34e9a7ac6042cd2d1ece1f8e757bdbee6e (schema v5, head d542579)
Visual preview
Route Viewport Before (production) After (this PR's preview) Diff
/ desktop before /
before /
after /
after /
/ mobile before / (mobile)
before / (mobile)
after / (mobile)
after / (mobile)

Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy.

Scroll preview
Route Before (production) After (this PR's preview)
/ before / (scroll)
before / (scroll)
after / (scroll)
after / (scroll)

A short scroll-through clip (desktop) — click either thumbnail to open the full animation. Evidence for scroll-linked behavior a single screenshot can't show.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.59%. Comparing base (78061a0) to head (ce4aa1c).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #9534   +/-   ##
=======================================
  Coverage   89.59%   89.59%           
=======================================
  Files         858      858           
  Lines      110456   110456           
  Branches    26300    26300           
=======================================
  Hits        98961    98961           
  Misses      10231    10231           
  Partials     1264     1264           
Flag Coverage Δ
backend 95.30% <100.00%> (ø)
control-plane 99.86% <ø> (ø)
engine 65.80% <ø> (ø)
rees 89.62% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/discovery-index/src/upload-sourcemaps.ts 100.00% <100.00%> (ø)
...kages/loopover-miner/lib/discovery-index-client.ts 100.00% <ø> (ø)
packages/loopover-miner/lib/tenant-client.ts 100.00% <ø> (ø)
review-enrichment/src/upload-sourcemaps.ts 99.51% <100.00%> (ø)

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 28, 2026
@JSONbored
JSONbored force-pushed the feat/typescript-lock-9527 branch from ff35db1 to d542579 Compare July 28, 2026 07:05
JSONbored added a commit that referenced this pull request Jul 28, 2026
…ree dev scripts for real

3 test files (iterate-loop-load-test-script, miner-benchmark-script,
miner-cross-repo-evaluation) still imported the OLD .mjs paths for
load-test-iterate-loop/benchmark/cross-repo-evaluation after their #9527 rename
to .ts -- the package.json runners were updated but these direct test imports
were missed. Broke CI on #9534 (Cannot find module ... .mjs). Fixed, plus the
same stale path in two docs, a manifest description string, a script's own
--help text, and a code comment, none of which the original port's grep sweep
caught.

Typing the three scripts for the first time (they were always .mjs, so tsc
never saw them) surfaced real gaps, not just missing annotations:

- cross-repo-evaluation.ts's main() called parseCrossRepoEvaluationArgs() with
  zero arguments against a REQUIRED parameter -- silently fine in untyped JS
  because the function internally falls back via ,
  but the parameter needed to actually be optional to say what was always true.
- benchmark.ts's buildSyntheticCandidates() was missing owner/repo/assignees
  entirely and mixed real booleans into a field opportunity-fanout.ts types as
  literal -only. Traced rankCandidateIssues' actual runtime check
  () before deciding how to type it: it
  genuinely branches on false, so narrowing the benchmark's synthetic mix to
  true-only would have silently dropped real exercised coverage. Added the
  missing fields for real, kept the deliberate true/false mix with a documented
  cast rather than narrowing it away.
- load-test-iterate-loop.ts's fake driver was typed to a hand-narrowed
  { attemptId: string } task shape until the test file's own fuller task
  literal (workingDirectory, acceptanceCriteriaPath, ...) failed against it --
  switched to the real CodingAgentDriverTask/Result types instead of the
  narrower hand-rolled ones.

Refs #9527
@JSONbored
JSONbored force-pushed the feat/typescript-lock-9527 branch from d542579 to 90f580e Compare July 28, 2026 07:55
@JSONbored
JSONbored force-pushed the feat/typescript-lock-9527 branch from 90f580e to 6bd2155 Compare July 28, 2026 08:05
JSONbored added a commit that referenced this pull request Jul 28, 2026
…ree dev scripts for real

3 test files (iterate-loop-load-test-script, miner-benchmark-script,
miner-cross-repo-evaluation) still imported the OLD .mjs paths for
load-test-iterate-loop/benchmark/cross-repo-evaluation after their #9527 rename
to .ts -- the package.json runners were updated but these direct test imports
were missed. Broke CI on #9534 (Cannot find module ... .mjs). Fixed, plus the
same stale path in two docs, a manifest description string, a script's own
--help text, and a code comment, none of which the original port's grep sweep
caught.

Typing the three scripts for the first time (they were always .mjs, so tsc
never saw them) surfaced real gaps, not just missing annotations:

- cross-repo-evaluation.ts's main() called parseCrossRepoEvaluationArgs() with
  zero arguments against a REQUIRED parameter -- silently fine in untyped JS
  because the function internally falls back via ,
  but the parameter needed to actually be optional to say what was always true.
- benchmark.ts's buildSyntheticCandidates() was missing owner/repo/assignees
  entirely and mixed real booleans into a field opportunity-fanout.ts types as
  literal -only. Traced rankCandidateIssues' actual runtime check
  () before deciding how to type it: it
  genuinely branches on false, so narrowing the benchmark's synthetic mix to
  true-only would have silently dropped real exercised coverage. Added the
  missing fields for real, kept the deliberate true/false mix with a documented
  cast rather than narrowing it away.
- load-test-iterate-loop.ts's fake driver was typed to a hand-narrowed
  { attemptId: string } task shape until the test file's own fuller task
  literal (workingDirectory, acceptanceCriteriaPath, ...) failed against it --
  switched to the real CodingAgentDriverTask/Result types instead of the
  narrower hand-rolled ones.

Refs #9527
First batch of #9527: control-plane-coverage, actionlint-download-attempts, and
both packages' check-syntax + strip-bin-sourcemap become real TypeScript, with
their package.json runners, importers, and tests updated.

check-syntax scanned scripts/*.mjs, so porting those files would have silently
removed them from the only syntax check that covered them. node --check accepts a
.ts file under --experimental-strip-types (verified it still rejects broken
syntax), so both scripts now scan .ts too and the file counts are unchanged.

Refs #9527
…v-reference generator

Second batch of #9527. eslint.config.js -> .ts in both UI apps (eslint 9 resolves
TS configs through the hoisted jiti; lint still reports 0 errors), the three
benchmark/load-test scripts, and generate-env-reference, which runs inside
test:ci via miner:env-reference:check.

The generator embeds its own filename in the header of both artifacts it emits,
so the rename changes generated output -- regenerated and re-checked, 48 env var
references unchanged in each.

Refs #9527
…ript out with a CI gate

Completes #9527. control-plane, discovery-index and review-enrichment's remaining
.mjs scripts become TypeScript, and validate-no-hand-written-js now fails CI on
any tracked .mjs/.cjs/.js or hand-written .d.ts outside a small allowlist, each
entry carrying its reason in the file.

The allowlist has an anti-rot guard: an entry matching nothing tracked FAILS the
check. A watched path that silently stops existing is exactly how metagraphed's
MCP version-sync workflow died unnoticed for months.

Typing the ported files surfaced real defects the untyped versions hid:
validate-posthog-release's fetchSymbolSets was annotated by its own callers as
returning {results} when it returns a bare array; its config accessor was typed
against the app's full ProcessEnv, which no caller could satisfy; and
generate-env-reference dereferenced regex groups and a nullable defaultValue
without guarding either. The generator's output is byte-identical (48 references,
--check passes), so the port is provably behavior-preserving.

check-syntax gained scripts/*.ts coverage in both packages so porting a file can
never remove it from the only syntax check watching it, and the two upload-
sourcemaps spawn sites pass --experimental-strip-types explicitly rather than
relying on Node 22.18+ defaulting it on, since engines.node still allows 22.0.

Closes #9527
…ree dev scripts for real

3 test files (iterate-loop-load-test-script, miner-benchmark-script,
miner-cross-repo-evaluation) still imported the OLD .mjs paths for
load-test-iterate-loop/benchmark/cross-repo-evaluation after their #9527 rename
to .ts -- the package.json runners were updated but these direct test imports
were missed. Broke CI on #9534 (Cannot find module ... .mjs). Fixed, plus the
same stale path in two docs, a manifest description string, a script's own
--help text, and a code comment, none of which the original port's grep sweep
caught.

Typing the three scripts for the first time (they were always .mjs, so tsc
never saw them) surfaced real gaps, not just missing annotations:

- cross-repo-evaluation.ts's main() called parseCrossRepoEvaluationArgs() with
  zero arguments against a REQUIRED parameter -- silently fine in untyped JS
  because the function internally falls back via ,
  but the parameter needed to actually be optional to say what was always true.
- benchmark.ts's buildSyntheticCandidates() was missing owner/repo/assignees
  entirely and mixed real booleans into a field opportunity-fanout.ts types as
  literal -only. Traced rankCandidateIssues' actual runtime check
  () before deciding how to type it: it
  genuinely branches on false, so narrowing the benchmark's synthetic mix to
  true-only would have silently dropped real exercised coverage. Added the
  missing fields for real, kept the deliberate true/false mix with a documented
  cast rather than narrowing it away.
- load-test-iterate-loop.ts's fake driver was typed to a hand-narrowed
  { attemptId: string } task shape until the test file's own fuller task
  literal (workingDirectory, acceptanceCriteriaPath, ...) failed against it --
  switched to the real CodingAgentDriverTask/Result types instead of the
  narrower hand-rolled ones.

Refs #9527
benchmark.ts and cross-repo-evaluation.ts (packages/loopover-miner/scripts/) import
from this package's OWN ../dist/lib/*.js -- real .ts since this PR's port, so
Typecheck now actually opens and type-checks them, unlike when they were .mjs and
invisible to tsc. "Build miner CLI" ran well after Typecheck (down by "Miner
package check"), so a clean CI checkout hit Cannot find module on both scripts'
dist imports every run.

Same class of gap the contract/engine build-order fixes already cover, just for
the miner package's own dist rather than a workspace dependency's -- moved the
step ahead of Typecheck and widened its trigger to match Typecheck's exactly, for
the same reason those two already match: any trigger that typechecks must also
have built what typechecking reads.

Refs #9527
@JSONbored
JSONbored force-pushed the feat/typescript-lock-9527 branch from 6bd2155 to ce4aa1c Compare July 28, 2026 08:20
@JSONbored
JSONbored merged commit 5312750 into main Jul 28, 2026
11 checks passed
@JSONbored
JSONbored deleted the feat/typescript-lock-9527 branch July 28, 2026 08:42
@github-actions github-actions Bot mentioned this pull request Jul 28, 2026
12 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

manual-review Gittensor contributor context

Projects

None yet

Development

Successfully merging this pull request may close these issues.

build(typescript): eliminate every hand-written .mjs/.js/.cjs (and hand-written .d.ts) and lock it with a permanent CI gate

1 participant