Progressive `// @ts-check` per-leaf rollout playbook for legacy JS bundles. Covers the three-list legacy-globals sync (ESLint globals / TypeScript ambient .d.ts / JSDoc typedef), tsconfig exclude strategy for type-check-resistant files, leaf classification (typedef-widening-fixable vs permanent-exclude), line-anchored progress-grep traps, and Phase-2 exit-gate semantics. Use when planning a per-leaf cleanup PR, introducing a new leaf-level global, editing tsconfig''s exclude list, interpretin...
Scanned 9/2/2026
Install to Claude Code
npx -y skills add hmj1026/dhpk --skill dhpk-js-static-check-strategy --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Dhpk Js Static Check Strategy?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/hmj1026-dhpk-js-static-check-strategy)More formats (shields.io, HTML) on the badges page.
---
name: dhpk-js-static-check-strategy
description: 'Progressive `// @ts-check` per-leaf rollout playbook for legacy JS bundles. Covers the three-list legacy-globals sync (ESLint globals / TypeScript ambient .d.ts / JSDoc typedef), tsconfig exclude strategy for type-check-resistant files, leaf classification (typedef-widening-fixable vs permanent-exclude), line-anchored progress-grep traps, and Phase-2 exit-gate semantics. Use when planning a per-leaf cleanup PR, introducing a new leaf-level global, editing tsconfig''s exclude list, interpreting /ts-check-status output, or validating the strict-checkJs exit gate. Not for everyday JavaScript feature work or isolated lint-rule lookups; use the sibling dhpk-js-lint-config and references/static-checks.md for those. Output: a per-leaf classification, three-list change set, and exit-gate evidence.'
metadata:
dhpk-invocation-class: implicit-eligible
---
# JS static-check strategy — progressive rollout playbook
> Always-loaded SSOT rule: `modules/js/references/static-checks.md`.
This skill carries the long-form execution detail so the always-loaded rule
stays short. Load this when you are actually doing the rollout work.
## Legacy-globals — three-list sync
When a leaf introduces a new global, **all three lists must update together**
(no automatic derivation — three independent maintained files):
1. **`eslint.config.js`** `<projectName>LegacyGlobals` constant — readonly /
writable annotation. SSOT for `no-undef`.
2. **`<frontend-root>/<project>-ambient.d.ts`** — `declare var X: any;`
block. SSOT for `tsc --noEmit` bare-identifier resolution.
3. **`<frontend-root>/jsdoc-globals.js`** — `@typedef` (only when the
default `any` from the ambient `.d.ts` is too loose and a real shape
adds value).
A leaf PR that misses any of the three surfaces later as `no-undef` or
TS2304. CI catches drift after the fact; reviewers should still verify
three-way sync proactively.
## `// @ts-check` progressive strategy
- One leaf per PR: add `// @ts-check` + clean up the JSDoc + run the
unit / contract tests to confirm no regression.
- Track the rollout against your mechanical-extraction cadence (Stage A,
B, C…). Each stage produces a batch of leaves ready for type-check
enablement.
- A stuck leaf can land `// @ts-nocheck` with a TODO comment as a transitional
state — but every transitional file should have a tracked exit PR.
Measure progress with the **line-anchored** grep (anything looser is
gameable — see the trap below):
```bash
# Step 1 — strict opt-in (exit gate: this list must end up empty)
find <frontend-root> -maxdepth 1 -name '*.js' -exec \
grep -lE '^[[:space:]]*//[[:space:]]*@ts-check[[:space:]]*$' {} \;
# Step 2 — files still on `// @ts-nocheck` (governance indicator;
# should trend toward zero, but some files may be permanent exclusions —
# see classification below).
find <frontend-root> -maxdepth 1 -name '*.js' -exec \
grep -lE '^[[:space:]]*//[[:space:]]*@ts-nocheck' {} \;
```
`/ts-check-status` runs both queries plus a summary.
### Leaf classification template
When a leaf gets stuck on `// @ts-nocheck`, classify which exit path applies:
- **Typedef-widening fixable** — blocker is a too-narrow ambient `@typedef`
conflicting with the leaf's constructor / inference pattern. **Exit
path:** widen the typedef (or write a more precise JSDoc) in the
per-leaf cleanup PR, then flip back to `// @ts-check`.
- **Permanent exclude** — blocker is cross-leaf late-init globals
(typically the core monolith and a couple of helper modules sitting
next to it). Widening the typedef doesn't help — the issue is runtime
semantics, not a type definition. **Exit path:** add the file to
`tsconfig.json`'s `exclude` array (same treatment as the truly massive
leaves that resist any single-PR cleanup). Expected to live on
`// @ts-nocheck` indefinitely.
Misclassifying a permanent-exclude file as "typical leaf cleanup" wastes
work. Look at the leaf's relationship to the monolith first.
### Trap — grep gameable
A bare `grep -l '@ts-check'` is a substring match. During the transitional
period, files often carry **both** `// @ts-nocheck` (the active directive)
**and** a comment mentioning `@ts-check` (e.g. `// TODO: enable @ts-check
after widening User typedef`). The bare grep matches the comment and
reports the file as already opted into strict mode.
The line-anchored form (`^\s*//\s*@ts-check\s*$`) only matches a dedicated
leading directive, which is the actual TypeScript opt-in trigger.
## `tsconfig.json` strategy
```jsonc
{
"compilerOptions": {
"allowJs": true,
"checkJs": false, // see note below
"noEmit": true,
"strict": false,
"target": "ES2017", // adjust to project's runtime baseline
"module": "CommonJS", // or ESNext for modern bundler projects
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": false,
"lib": ["ES2017", "DOM", "DOM.Iterable"]
},
"include": [
"<frontend-root>/**/*.js",
"<frontend-root>/**/*.d.ts",
"<frontend-root>/jsdoc-globals.js"
],
"exclude": [
// The monolith bundle files and any leaf classified as "permanent exclude"
"<frontend-root>/<monolith-entry>.js",
"<frontend-root>/<helper-1>.js",
"<frontend-root>/<helper-2>.js",
"node_modules"
]
}
```
**`checkJs` design choice.** Two reasonable interpretations:
- **Strict project-wide** (`checkJs: true`) — every `.js` file is checked
unless it carries `// @ts-nocheck`. Suitable when ≥ ~80% of leaves
already have `// @ts-check`.
- **Per-leaf opt-in** (`checkJs: false`) — only files with `// @ts-check`
are checked. Suitable when most leaves are still being audited.
Mid-rollout, **per-leaf opt-in wins**. Flip to project-wide once the
opt-in count hits the threshold above and the `// @ts-nocheck` set has
stabilised (only the permanent-exclude files remain).
## Ambient typedef standard
`<frontend-root>/jsdoc-globals.js` is where structural typedef SSOT lives:
- `@typedef` for the project namespace + its main sub-objects.
- `@typedef` for the canonical AJAX wrapper (`MyApp.api.request`,
`MyApp.list.ajaxPromise`, etc.) — gives leaves accurate parameter /
return types without per-leaf JSDoc.
- `@typedef` for major domain modules.
Leaf PRs annotate call sites via JSDoc `@type`, `@param`, `@returns`; TS
resolves cross-leaf types through the ambient typedef.
## Future evolution
| Work | Trigger |
|---|---|
| Migrating Tier 1.5 / Tier 1.7 files toward Tier 1 | One-by-one as their `$.ajax` / `axios` callsites get migrated |
| Promoting `checkJs` to `true` | When per-leaf strict opt-in reaches the threshold above |
| Linting `<script>` blocks inside server-side templates | Optional Phase-3 — usually scoped to the view files holding inline config / page-config SSOT |
| Bundler / ESM migration | Separate effort — once the modular structure is stable and the SSOT-facade has matured |
## Related rule
- `modules/js/references/static-checks.md` — always-loaded index pointing at
this skill and the lint-config skill.
- `skills/dhpk-js-lint-config/SKILL.md` — companion skill with the
ESLint tier model, custom AST selectors, and the legacy-globals
three-list sync detail.
## When NOT to Use
Not for day-to-day lint-rule lookups (use the sibling `dhpk-js-lint-config` and
`references/static-checks.md`), and not for ESLint tier design itself. Load this
when actually executing the per-leaf `// @ts-check` rollout.
## Output
A per-leaf cleanup PR: one leaf flipped to `// @ts-check` with its three-list
globals updated, a correct `tsconfig.json` `exclude` for any permanent-exclude
leaf, and each stuck leaf classified (typedef-widening-fixable vs
permanent-exclude) with a tracked exit.
## Verification
- The line-anchored `^\s*//\s*@ts-check\s*$` grep (via `/ts-check-status`)
counts the flipped leaf; the `// @ts-nocheck` set trends toward only the
permanent-exclude files.
- `tsc --noEmit` passes after the flip; contract/unit tests show no regression.
- Any new global appears in all three lists (ESLint globals / ambient `.d.ts` /
JSDoc typedef).
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!