ts-suppress 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 br0p0p
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # ts-suppress
2
+
3
+ Incremental TypeScript strictness adoption via bulk error suppression.
4
+
5
+ Instead of scattering `@ts-ignore` or `@ts-expect-error` comments throughout your codebase, `ts-suppress` captures all TypeScript errors into a single `.ts-suppressions.json` file. This lets you enable stricter compiler options immediately and fix errors at your own pace.
6
+
7
+ ## How It Works
8
+
9
+ Each suppression is a fingerprint of a TypeScript error, consisting of:
10
+
11
+ - **file** — relative path to the source file
12
+ - **code** — TypeScript error code (e.g. `2322`)
13
+ - **hash** — hex hash of the diagnostic message text
14
+ - **scope** — dot-separated scope chain (e.g. `MyClass.myMethod`)
15
+
16
+ The `check` command diffs the current diagnostics against the suppression file and reports:
17
+
18
+ - **Unsuppressed errors** — new errors not yet in the suppression file
19
+ - **Stale suppressions** — entries that no longer match any current error (i.e. errors that have been fixed)
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ bun add -d ts-suppress
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### `init`
30
+
31
+ Create an empty `.ts-suppressions.json`:
32
+
33
+ ```bash
34
+ bunx ts-suppress init
35
+ ```
36
+
37
+ ### `suppress`
38
+
39
+ Snapshot all current TypeScript errors into `.ts-suppressions.json`:
40
+
41
+ ```bash
42
+ bunx ts-suppress suppress
43
+ ```
44
+
45
+ ### `check`
46
+
47
+ Verify that all errors are suppressed and no suppressions are stale. Exits non-zero on failure — useful in CI:
48
+
49
+ ```bash
50
+ bunx ts-suppress check
51
+ ```
52
+
53
+ ### `update`
54
+
55
+ Add new suppressions and remove stale ones in a single pass:
56
+
57
+ ```bash
58
+ bunx ts-suppress update
59
+ ```
60
+
61
+ Also available as `bunx ts-suppress fix`.
62
+
63
+ ## Typical Workflow
64
+
65
+ 1. Enable a stricter TypeScript option (e.g. `"strict": true`)
66
+ 2. Run `bunx ts-suppress suppress` to baseline all existing errors
67
+ 3. Commit `.ts-suppressions.json`
68
+ 4. Add `bunx ts-suppress check` to CI
69
+ 5. Fix errors over time — `check` will flag stale suppressions as you go
70
+ 6. Run `bunx ts-suppress update` to sync the suppression file after fixing errors
package/dist/cli.js ADDED
@@ -0,0 +1,63 @@
1
+ import { parse } from "@bomb.sh/args";
2
+ import { createProject } from "./project.js";
3
+ import { runCheck } from "./commands/check.js";
4
+ import { runInit } from "./commands/init.js";
5
+ import { runSuppress } from "./commands/suppress.js";
6
+ import { runUpdate } from "./commands/update.js";
7
+ const VERSION = "0.2.0";
8
+ const commands = [
9
+ ["init", "Create an empty .ts-suppressions.json file"],
10
+ ["suppress", "Snapshot all current TypeScript errors into .ts-suppressions.json"],
11
+ ["update", "Add new suppressions and remove stale ones (alias: fix)"],
12
+ ["check", "Check for unsuppressed errors and stale suppressions"],
13
+ ];
14
+ function printHelp() {
15
+ const longest = Math.max(...commands.map(([name]) => name.length));
16
+ const lines = commands.map(([name, desc]) => ` ${name.padEnd(longest + 4)}${desc}`);
17
+ console.log(`ts-suppress v${VERSION}\nIncremental TypeScript strictness adoption via bulk error suppression\n\nCommands:\n${lines.join("\n")}\n\nRun ts-suppress <command> --help for details.`);
18
+ }
19
+ const args = parse(process.argv.slice(2), {
20
+ boolean: ["help", "version"],
21
+ alias: { h: "help", v: "version" },
22
+ });
23
+ const command = args._[0];
24
+ if (args.version) {
25
+ console.log(VERSION);
26
+ }
27
+ else if (args.help || (!command && process.argv.length <= 2)) {
28
+ printHelp();
29
+ }
30
+ else if (command) {
31
+ switch (command) {
32
+ case "init": {
33
+ await runInit();
34
+ break;
35
+ }
36
+ case "suppress": {
37
+ const { project, projectRoot } = createProject(process.cwd());
38
+ await runSuppress(project, projectRoot);
39
+ break;
40
+ }
41
+ case "update":
42
+ case "fix": {
43
+ const { project, projectRoot } = createProject(process.cwd());
44
+ await runUpdate(project, projectRoot);
45
+ break;
46
+ }
47
+ case "check": {
48
+ const { project, projectRoot } = createProject(process.cwd());
49
+ const { exitCode } = await runCheck(project, projectRoot);
50
+ if (exitCode !== 0)
51
+ process.exit(exitCode);
52
+ break;
53
+ }
54
+ default: {
55
+ console.error(`Unknown command: ${command}`);
56
+ printHelp();
57
+ process.exit(1);
58
+ }
59
+ }
60
+ }
61
+ else {
62
+ printHelp();
63
+ }
@@ -0,0 +1,28 @@
1
+ import { collectDiagnostics } from "../diagnostics.js";
2
+ import { readSuppressions, diffSuppressions } from "../suppressions.js";
3
+ /**
4
+ * Core logic, extracted for testability.
5
+ * suppressionsRoot is where the suppression file is read from (may differ from projectRoot in tests).
6
+ */
7
+ export async function runCheck(project, projectRoot, suppressionsRoot = projectRoot) {
8
+ const existing = await readSuppressions(suppressionsRoot);
9
+ const current = collectDiagnostics(project, projectRoot);
10
+ const { unsuppressed, stale } = diffSuppressions(existing, current);
11
+ if (unsuppressed.length > 0) {
12
+ console.error(`\n${unsuppressed.length} unsuppressed error(s):\n`);
13
+ for (const s of unsuppressed) {
14
+ console.error(` TS${s.code} in ${s.file}`);
15
+ }
16
+ }
17
+ if (stale.length > 0) {
18
+ console.error(`\n${stale.length} stale suppression(s):\n`);
19
+ for (const s of stale) {
20
+ console.error(` TS${s.code} in ${s.file}`);
21
+ }
22
+ }
23
+ const exitCode = unsuppressed.length > 0 || stale.length > 0 ? 1 : 0;
24
+ if (exitCode === 0) {
25
+ console.log("No unsuppressed errors or stale suppressions.");
26
+ }
27
+ return { exitCode, unsuppressed, stale };
28
+ }
@@ -0,0 +1,5 @@
1
+ import { writeSuppressions, SUPPRESSIONS_FILENAME } from "../suppressions.js";
2
+ export async function runInit() {
3
+ await writeSuppressions(process.cwd(), []);
4
+ console.log(`Created ${SUPPRESSIONS_FILENAME}`);
5
+ }
@@ -0,0 +1,12 @@
1
+ import { collectDiagnostics } from "../diagnostics.js";
2
+ import { writeSuppressions, SUPPRESSIONS_FILENAME } from "../suppressions.js";
3
+ /**
4
+ * Core logic, extracted for testability.
5
+ * Accepts a ts-morph Project and roots separately so tests can pass in-memory projects.
6
+ * outputRoot is where the suppression file is written (may differ from projectRoot in tests).
7
+ */
8
+ export async function runSuppress(project, projectRoot, outputRoot = projectRoot) {
9
+ const diagnostics = collectDiagnostics(project, projectRoot);
10
+ await writeSuppressions(outputRoot, diagnostics);
11
+ console.log(`Wrote ${diagnostics.length} suppression(s) to ${SUPPRESSIONS_FILENAME}`);
12
+ }
@@ -0,0 +1,23 @@
1
+ import { collectDiagnostics } from "../diagnostics.js";
2
+ import { readSuppressions, writeSuppressions, diffSuppressions } from "../suppressions.js";
3
+ /**
4
+ * Core logic, extracted for testability.
5
+ * Reads existing suppressions, diffs against current diagnostics,
6
+ * writes the updated file, and reports what changed.
7
+ */
8
+ export async function runUpdate(project, projectRoot, outputRoot = projectRoot) {
9
+ const existing = await readSuppressions(outputRoot);
10
+ const current = collectDiagnostics(project, projectRoot);
11
+ const { unsuppressed: added, stale: removed } = diffSuppressions(existing, current);
12
+ await writeSuppressions(outputRoot, current);
13
+ if (added.length > 0) {
14
+ console.log(`Added ${added.length} new suppression(s)`);
15
+ }
16
+ if (removed.length > 0) {
17
+ console.log(`Removed ${removed.length} stale suppression(s)`);
18
+ }
19
+ if (added.length === 0 && removed.length === 0) {
20
+ console.log("Already up to date.");
21
+ }
22
+ return { added, removed, total: current.length };
23
+ }
@@ -0,0 +1,35 @@
1
+ import { relative } from "node:path";
2
+ import { hashMessage } from "./hash.js";
3
+ import { buildScopePath } from "./scope.js";
4
+ /**
5
+ * Collect all pre-emit diagnostics from a ts-morph Project as Suppression fingerprints.
6
+ * Project creation is the caller's responsibility — this enables in-memory testing.
7
+ */
8
+ export function collectDiagnostics(project, projectRoot) {
9
+ const diagnostics = project.getPreEmitDiagnostics();
10
+ const suppressions = [];
11
+ for (const diag of diagnostics) {
12
+ const sourceFile = diag.getSourceFile();
13
+ if (!sourceFile)
14
+ continue;
15
+ const filePath = relative(projectRoot, sourceFile.getFilePath());
16
+ const code = diag.getCode();
17
+ const messageText = diag.getMessageText();
18
+ const message = typeof messageText === "string" ? messageText : messageText.getMessageText();
19
+ const start = diag.getStart();
20
+ let scope = "";
21
+ if (start != null) {
22
+ const node = sourceFile.getDescendantAtPos(start);
23
+ if (node) {
24
+ scope = buildScopePath(node);
25
+ }
26
+ }
27
+ suppressions.push({
28
+ file: filePath,
29
+ code,
30
+ hash: hashMessage(message),
31
+ scope,
32
+ });
33
+ }
34
+ return suppressions;
35
+ }
package/dist/hash.js ADDED
@@ -0,0 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+ /** Hash a diagnostic message text to a deterministic hex string */
3
+ export function hashMessage(message) {
4
+ return createHash("sha256").update(message).digest("hex");
5
+ }
@@ -0,0 +1,24 @@
1
+ import { Project } from "ts-morph";
2
+ import ts from "typescript";
3
+ import { dirname } from "node:path";
4
+ /**
5
+ * Find the nearest tsconfig.json by walking up from the given directory.
6
+ * Uses TypeScript's own findConfigFile for correct resolution behavior.
7
+ */
8
+ export function findTsConfig(cwd) {
9
+ const configPath = ts.findConfigFile(cwd, ts.sys.fileExists, "tsconfig.json");
10
+ if (!configPath) {
11
+ throw new Error(`No tsconfig.json found starting from ${cwd}`);
12
+ }
13
+ return configPath;
14
+ }
15
+ /**
16
+ * Create a ts-morph Project from the nearest tsconfig.json.
17
+ * Returns the Project and the resolved project root (directory containing tsconfig.json).
18
+ */
19
+ export function createProject(cwd) {
20
+ const tsConfigFilePath = findTsConfig(cwd);
21
+ const projectRoot = dirname(tsConfigFilePath);
22
+ const project = new Project({ tsConfigFilePath });
23
+ return { project, projectRoot };
24
+ }
package/dist/scope.js ADDED
@@ -0,0 +1,54 @@
1
+ // src/scope.ts
2
+ import { Node } from "ts-morph";
3
+ /**
4
+ * Build a dot-separated scope path by walking up the AST from a node.
5
+ * Returns empty string for module-level code.
6
+ *
7
+ * Examples:
8
+ * - "MyClass.myMethod" for a method inside a class
9
+ * - "processData" for a top-level function
10
+ * - "handler" for an arrow function assigned to a const
11
+ * - "MyClass.get:name" for a getter
12
+ * - "" for module scope
13
+ */
14
+ export function buildScopePath(node) {
15
+ const parts = [];
16
+ let current = node;
17
+ while (current) {
18
+ const name = getScopeName(current);
19
+ if (name != null) {
20
+ parts.unshift(name);
21
+ }
22
+ current = current.getParent();
23
+ }
24
+ return parts.join(".");
25
+ }
26
+ function getScopeName(node) {
27
+ if (Node.isFunctionDeclaration(node)) {
28
+ return node.getName() ?? null;
29
+ }
30
+ if (Node.isMethodDeclaration(node)) {
31
+ return node.getName();
32
+ }
33
+ if (Node.isClassDeclaration(node)) {
34
+ return node.getName() ?? null;
35
+ }
36
+ if (Node.isGetAccessorDeclaration(node)) {
37
+ return `get:${node.getName()}`;
38
+ }
39
+ if (Node.isSetAccessorDeclaration(node)) {
40
+ return `set:${node.getName()}`;
41
+ }
42
+ if (Node.isConstructorDeclaration(node)) {
43
+ return "constructor";
44
+ }
45
+ // Arrow function or function expression assigned to a variable
46
+ if (Node.isArrowFunction(node) || Node.isFunctionExpression(node)) {
47
+ const parent = node.getParent();
48
+ if (parent && Node.isVariableDeclaration(parent)) {
49
+ return parent.getName();
50
+ }
51
+ return null; // anonymous, no scope name
52
+ }
53
+ return null;
54
+ }
@@ -0,0 +1,101 @@
1
+ import { readFile, writeFile, access } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ export const SUPPRESSIONS_FILENAME = ".ts-suppressions.json";
4
+ /** Compare function for deterministic sorting of suppressions */
5
+ function compareSuppression(a, b) {
6
+ return (a.file.localeCompare(b.file) ||
7
+ a.code - b.code ||
8
+ a.hash.localeCompare(b.hash) ||
9
+ a.scope.localeCompare(b.scope));
10
+ }
11
+ /** Key without scope — used for grouping duplicates */
12
+ function baseKey(s) {
13
+ return `${s.file}\0${s.code}\0${s.hash}`;
14
+ }
15
+ /** Key with scope — used for matching duplicates */
16
+ function fullKey(s) {
17
+ return `${s.file}\0${s.code}\0${s.hash}\0${s.scope}`;
18
+ }
19
+ /** Count occurrences of each base key in a list */
20
+ function countByBaseKey(list) {
21
+ const counts = new Map();
22
+ for (const s of list) {
23
+ const key = baseKey(s);
24
+ counts.set(key, (counts.get(key) ?? 0) + 1);
25
+ }
26
+ return counts;
27
+ }
28
+ /** Read suppressions from .ts-suppressions.json in the given directory */
29
+ export async function readSuppressions(projectRoot) {
30
+ const filePath = resolve(projectRoot, SUPPRESSIONS_FILENAME);
31
+ try {
32
+ await access(filePath);
33
+ }
34
+ catch {
35
+ return [];
36
+ }
37
+ const raw = await readFile(filePath, "utf-8");
38
+ const data = JSON.parse(raw);
39
+ return data.suppressions;
40
+ }
41
+ /** Write suppressions to .ts-suppressions.json, sorted deterministically */
42
+ export async function writeSuppressions(projectRoot, suppressions) {
43
+ const filePath = resolve(projectRoot, SUPPRESSIONS_FILENAME);
44
+ const sorted = [...suppressions].sort(compareSuppression);
45
+ const data = { suppressions: sorted };
46
+ await writeFile(filePath, JSON.stringify(data, null, 2) + "\n");
47
+ }
48
+ /**
49
+ * Diff existing suppressions against current diagnostics.
50
+ *
51
+ * Matching strategy:
52
+ * - For unique { file, code, hash } tuples: match by base key only (scope is informational)
53
+ * - For duplicate { file, code, hash } tuples: match by full key including scope
54
+ *
55
+ * "Became duplicate" edge case: if existing has 1 entry for a base key but current has 2+,
56
+ * the existing suppression covers one occurrence; extras are reported as unsuppressed.
57
+ */
58
+ export function diffSuppressions(existing, current) {
59
+ const existingCounts = countByBaseKey(existing);
60
+ const currentCounts = countByBaseKey(current);
61
+ // A base key is "duplicate" if EITHER list has more than one entry for it
62
+ const isDuplicate = (key) => (existingCounts.get(key) ?? 0) > 1 || (currentCounts.get(key) ?? 0) > 1;
63
+ // Build a pool of existing match counts keyed appropriately
64
+ const existingKeys = new Map();
65
+ for (const s of existing) {
66
+ const key = isDuplicate(baseKey(s)) ? fullKey(s) : baseKey(s);
67
+ existingKeys.set(key, (existingKeys.get(key) ?? 0) + 1);
68
+ }
69
+ // Match current diagnostics against existing suppressions
70
+ const unsuppressed = [];
71
+ const matchedKeys = new Map();
72
+ for (const s of current) {
73
+ const key = isDuplicate(baseKey(s)) ? fullKey(s) : baseKey(s);
74
+ const remaining = (existingKeys.get(key) ?? 0) - (matchedKeys.get(key) ?? 0);
75
+ if (remaining > 0) {
76
+ matchedKeys.set(key, (matchedKeys.get(key) ?? 0) + 1);
77
+ }
78
+ else {
79
+ unsuppressed.push(s);
80
+ }
81
+ }
82
+ // Find stale: existing entries not consumed by any current diagnostic
83
+ const currentKeySet = new Map();
84
+ for (const s of current) {
85
+ const key = isDuplicate(baseKey(s)) ? fullKey(s) : baseKey(s);
86
+ currentKeySet.set(key, (currentKeySet.get(key) ?? 0) + 1);
87
+ }
88
+ const staleConsumed = new Map();
89
+ const stale = [];
90
+ for (const s of existing) {
91
+ const key = isDuplicate(baseKey(s)) ? fullKey(s) : baseKey(s);
92
+ const available = (currentKeySet.get(key) ?? 0) - (staleConsumed.get(key) ?? 0);
93
+ if (available > 0) {
94
+ staleConsumed.set(key, (staleConsumed.get(key) ?? 0) + 1);
95
+ }
96
+ else {
97
+ stale.push(s);
98
+ }
99
+ }
100
+ return { unsuppressed, stale };
101
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ // src/types.ts
2
+ export {};
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "ts-suppress",
3
+ "version": "0.2.0",
4
+ "description": "Incremental TypeScript strictness adoption via bulk error suppression",
5
+ "keywords": [
6
+ "migration",
7
+ "strict",
8
+ "suppression",
9
+ "tanstack-intent",
10
+ "ts-ignore",
11
+ "typescript"
12
+ ],
13
+ "homepage": "https://github.com/br0p0p/ts-suppress#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/br0p0p/ts-suppress/issues"
16
+ },
17
+ "license": "MIT",
18
+ "author": "br0p0p",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/br0p0p/ts-suppress.git"
22
+ },
23
+ "bin": {
24
+ "ts-suppress": "dist/cli.js"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "package.json",
29
+ "README.md",
30
+ "skills",
31
+ "!skills/_artifacts"
32
+ ],
33
+ "type": "module",
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.build.json",
36
+ "test": "bun test",
37
+ "prepublishOnly": "bun run build",
38
+ "fmt": "oxfmt",
39
+ "fmt:check": "oxfmt --check",
40
+ "knip": "knip",
41
+ "lint": "oxlint",
42
+ "lint:fix": "oxlint --fix",
43
+ "prepare": "husky",
44
+ "typecheck": "tsc --noEmit"
45
+ },
46
+ "dependencies": {
47
+ "@bomb.sh/args": "^0.3.1",
48
+ "ts-morph": "^27.0.2"
49
+ },
50
+ "devDependencies": {
51
+ "@types/bun": "latest",
52
+ "@types/node": "^24",
53
+ "husky": "^9.1.7",
54
+ "knip": "^5.87.0",
55
+ "lint-staged": "^16.4.0",
56
+ "oxfmt": "^0.41.0",
57
+ "oxlint": "^1.56.0",
58
+ "oxlint-tsgolint": "^0.17.0",
59
+ "typescript": "^5.9.3"
60
+ },
61
+ "peerDependencies": {
62
+ "typescript": "^5.9.3"
63
+ },
64
+ "lint-staged": {
65
+ "*.{js,jsx,ts,tsx,mjs,cjs}": "bun run lint",
66
+ "*": "oxfmt --no-error-on-unmatched-pattern"
67
+ }
68
+ }
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: ts-suppress
3
+ description: Use when working with TypeScript error suppressions, enabling stricter tsconfig options incrementally, or managing .ts-suppressions.json files
4
+ ---
5
+
6
+ # ts-suppress
7
+
8
+ Incremental TypeScript strictness adoption via bulk error suppression. Instead of scattering `@ts-ignore` everywhere, ts-suppress captures all errors into a single `.ts-suppressions.json` file so you can enable strict options immediately and fix errors at your own pace.
9
+
10
+ ## Commands
11
+
12
+ | Command | Description |
13
+ | ---------------------- | ------------------------------------------------------------------- |
14
+ | `ts-suppress init` | Create an empty `.ts-suppressions.json` file |
15
+ | `ts-suppress suppress` | Snapshot all current TypeScript errors into `.ts-suppressions.json` |
16
+ | `ts-suppress check` | Verify all errors are suppressed and none are stale (use in CI) |
17
+ | `ts-suppress update` | Add new suppressions and remove stale ones (alias: `fix`) |
18
+
19
+ **Flags:** `--help` / `-h`, `--version` / `-v`
20
+
21
+ ## Typical Workflow
22
+
23
+ 1. Enable stricter TypeScript settings in `tsconfig.json` (e.g. `"strict": true`)
24
+ 2. Run `ts-suppress suppress` to capture all resulting errors as a baseline
25
+ 3. Commit `.ts-suppressions.json` to version control
26
+ 4. Add `ts-suppress check` to CI — fails on new unsuppressed errors or stale suppressions
27
+ 5. Fix errors incrementally; run `ts-suppress update` to sync the suppression file
28
+
29
+ ## Suppression File Format
30
+
31
+ `.ts-suppressions.json` contains fingerprinted error entries:
32
+
33
+ ```json
34
+ {
35
+ "suppressions": [
36
+ {
37
+ "file": "src/utils.ts",
38
+ "code": 2322,
39
+ "hash": "a1b2c3...",
40
+ "scope": "MyClass.myMethod"
41
+ }
42
+ ]
43
+ }
44
+ ```
45
+
46
+ - **file** — relative path to source file
47
+ - **code** — TypeScript error code
48
+ - **hash** — SHA256 of the diagnostic message text
49
+ - **scope** — dot-separated scope chain (e.g. `MyClass.myMethod`, empty for module-level)
50
+
51
+ ## Requirements
52
+
53
+ - TypeScript >= 5.9.3
54
+ - A `tsconfig.json` in the project (ts-suppress walks up directories to find it)