@shipispec/tsfix 0.1.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) 2026 owgreen-dev
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,120 @@
1
+ # TSC Defense Stack — `@shipispec/tsfix`
2
+
3
+ Standalone npm package implementing **Layers 0–1** of the TypeScript error-recovery stack: in-process tsc validation + deterministic LSP auto-fix. Layers 2–4 (LLM mend) currently live in `spectoship2/src/pipeline/` and will move to a sister package `@shipispec/tsmend` per the roadmap.
4
+
5
+ Read first:
6
+ - `STATUS.md` — what's working, what's planned, current gaps
7
+ - `ARCHITECTURE.md` — why the package is shaped the way it is
8
+ - `tsc-defense-roadmap.md` — phased plan with open decisions
9
+ - `CLAUDE.md` — working principles (small allowlist, fixture-pinned trust model)
10
+
11
+ ---
12
+
13
+ ## Source-of-truth map
14
+
15
+ This package owns its TypeScript-error handling code outright. The shims in `spectoship2/` re-export from here, not the reverse.
16
+
17
+ | Path | Role |
18
+ |---|---|
19
+ | `src/index.ts` | Public API (`runValidationLoop`, `runInProcessTsc`, `runLSPFixerPass`, `discoverTsFiles`) |
20
+ | `src/validatorInProcess.ts` | In-process tsc with lib-path workaround (Layer 0) |
21
+ | `src/tsLanguageServiceFixer.ts` | LSP auto-fixer using `getCodeFixesAtPosition` (Layer 1) |
22
+ | `cli/run-stack.ts` | CLI: `tsx cli/run-stack.ts --workspace <path>` |
23
+ | `benchmark/run-benchmark.ts` | Fixture harness (auto-discovers `fixtures/*/`) |
24
+ | `fixtures/` | 14 hand-authored synthetic fixtures across 3 tiers |
25
+ | `spectoship2/src/pipeline/validatorInProcess.ts` | **Re-export shim** → `@shipispec/tsfix` |
26
+ | `spectoship2/src/pipeline/tsLanguageServiceFixer.ts` | **Re-export shim** → `@shipispec/tsfix` |
27
+
28
+ ---
29
+
30
+ ## How the layers fit together
31
+
32
+ Per `ARCHITECTURE.md`, a TSC error has up to four chances to die before reaching a user. Layers -1 (prevention) and 2-4 (mend) live outside this package.
33
+
34
+ ```
35
+ ┌─────────────────────────────────────────────────┐
36
+ │ Layer -1: PREVENTION (in spectoship2/, not here)│
37
+ │ packageGotchas, installedExports, priorExports│
38
+ │ codeGenPrompts (rules injected into prompt) │
39
+ └────────────────────┬────────────────────────────┘
40
+ │ files written to disk
41
+
42
+ ┌────── @shipispec/tsfix ───────┴──────────────────────────┐
43
+ │ │
44
+ │ ┌─────────────────────────────────────────────┐ │
45
+ │ │ Layer 0: src/validatorInProcess.ts │ │
46
+ │ │ in-process tsc → structured diagnostics │ │
47
+ │ │ workspace lib-path override │ │
48
+ │ └─────────────────────┬───────────────────────┘ │
49
+ │ │ if errors │
50
+ │ ▼ │
51
+ │ ┌─────────────────────────────────────────────┐ │
52
+ │ │ Layer 1: src/tsLanguageServiceFixer.ts │ │
53
+ │ │ getCodeFixesAtPosition (5 SAFE codes) │ │
54
+ │ │ signature-set progress check, max 5 iters │ │
55
+ │ └─────────────────────┬───────────────────────┘ │
56
+ │ │ re-validate; if errors remain │
57
+ └─────────────────────────┼─────────────────────────────────────────┘
58
+
59
+ ┌─────────────────────────────────┐
60
+ │ Layers 2-4: LLM MEND │
61
+ │ mendAgent / mendArchitect / │
62
+ │ multiFileMend / repairAgent │
63
+ │ (in spectoship2/, not here; │
64
+ │ moves to @shipispec/tsmend│
65
+ │ in v0.2 per roadmap) │
66
+ └─────────────────────────────────┘
67
+ ```
68
+
69
+ ---
70
+
71
+ ## What to read first
72
+
73
+ 1. **`STATUS.md`** — current state, fixture catalog, recent fixes
74
+ 2. **`ARCHITECTURE.md`** — why the package is shaped the way it is (12 sections)
75
+ 3. **`tsc-defense-roadmap.md`** — phased plan with open decisions
76
+ 4. **`src/index.ts`** — public API entry point (`runValidationLoop`)
77
+ 5. **`src/tsLanguageServiceFixer.ts`** — Layer 1 fixer; understand `SAFE_FIXABLE_CODES`, the signature-set progress check, and the iteration loop
78
+ 6. **`src/validatorInProcess.ts`** — in-process tsc with the lib-path workaround that makes the package work inside the VS Code Extension Host
79
+
80
+ ---
81
+
82
+ ## Standalone harness
83
+
84
+ ```
85
+ cli/run-stack.ts # CLI: run stack on any workspace
86
+ benchmark/run-benchmark.ts # benchmark across all fixtures
87
+ fixtures/ # 14 hand-authored synthetic workspaces
88
+ _shared/ # shared node_modules symlink target
89
+ clean-baseline/ # regression check (must stay green)
90
+ synthetic-*/ # 9 LSP-behavior fixtures (positive + negative)
91
+ api-drift-*/ # 4 version-drift fixtures (Zod 3 vs 4, React 18 vs 19, etc.)
92
+ ```
93
+
94
+ ### Run the CLI
95
+
96
+ ```
97
+ cd /Users/ogg/Documents/microservices/Meta/spectoship2
98
+ ./node_modules/.bin/tsx ../tsc-defense-stack/cli/run-stack.ts --workspace <path>
99
+ ```
100
+
101
+ Flags: `--json`, `--no-lsp`, `--verbose`, `--files <comma-list>`. Exit 0 = clean, 1 = errors remain, 2 = bad args.
102
+
103
+ > Note: per Phase 0c of `tsc-defense-roadmap.md`, this package will gain its own local `node_modules` so commands run from inside the package directory. Today it shares `spectoship2/node_modules`.
104
+
105
+ ### Run the benchmark
106
+
107
+ ```
108
+ cd /Users/ogg/Documents/microservices/Meta/spectoship2
109
+ ./node_modules/.bin/tsx ../tsc-defense-stack/benchmark/run-benchmark.ts
110
+ ```
111
+
112
+ `--fixture <name>` to run a single fixture in isolation.
113
+
114
+ ### Current baseline
115
+
116
+ **14/14 synthetic fixtures pass. LSP fixer auto-resolves 14/25 errors (56%).** The remaining errors are intentional non-fixes — TS7006 implicit-any, TS2741 missing prop, API-drift errors that need the mend layer. See `STATUS.md` § Fixture catalog for the full list.
117
+
118
+ ### Capturing real-failure fixtures
119
+
120
+ Phase 3b in the roadmap. When a real spec-pipeline run produces a TSC error Layer 0-1 doesn't fix, snapshot the broken `.ts(x)` files into `fixtures/real-<timestamp>-<hash>/` with an `expected.json`. The fixture set then grows from production failures, not just synthetic ones.
package/bin/tsfix.mjs ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Bin wrapper for `tsfix`. Resolves `tsx` from this package's own
4
+ * dependencies and spawns it against `cli/run-stack.ts`, forwarding all
5
+ * args. Inherits stdio so output/colors look identical to running tsx
6
+ * directly. Propagates the child exit code.
7
+ *
8
+ * Why a wrapper instead of pointing `bin` directly at `cli/run-stack.ts`:
9
+ * the CLI is a `.ts` file. `npm link` symlinks it into PATH, but execution
10
+ * fails because shells parse it as a shell script. A `.mjs` wrapper that
11
+ * Node can run directly fixes the local-dev story.
12
+ *
13
+ * NOT a substitute for the Phase 1a esbuild bundle: this wrapper requires
14
+ * `tsx` to be resolvable from the package's `node_modules`. For a true
15
+ * cold-start `npx @shipispec/tsfix ./project`, we need a bundled
16
+ * .js CLI. See tsc-defense-roadmap.md § Phase 1a.
17
+ */
18
+
19
+ import { spawn } from "node:child_process";
20
+ import { dirname, resolve } from "node:path";
21
+ import { createRequire } from "node:module";
22
+ import { fileURLToPath } from "node:url";
23
+
24
+ const here = dirname(fileURLToPath(import.meta.url));
25
+ const cliEntry = resolve(here, "..", "cli", "run-stack.ts");
26
+
27
+ let tsxCli;
28
+ try {
29
+ tsxCli = createRequire(import.meta.url).resolve("tsx/cli");
30
+ } catch (err) {
31
+ process.stderr.write(
32
+ "error: tsc-defense requires `tsx` in this package's node_modules.\n" +
33
+ " run: npm install\n" +
34
+ " (or wait for the Phase 1a esbuild bundle that drops this dep.)\n",
35
+ );
36
+ process.exit(2);
37
+ }
38
+
39
+ const child = spawn(process.execPath, [tsxCli, cliEntry, ...process.argv.slice(2)], {
40
+ stdio: "inherit",
41
+ });
42
+
43
+ child.on("exit", (code, signal) => {
44
+ if (signal) {
45
+ process.kill(process.pid, signal);
46
+ } else {
47
+ process.exit(code ?? 0);
48
+ }
49
+ });
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Standalone TSC Defense Stack runner.
3
+ *
4
+ * Runs the deterministic layers (in-process tsc + LSP fixer) against an
5
+ * arbitrary workspace and reports per-layer outcomes. No LLM calls.
6
+ *
7
+ * Usage:
8
+ * npx tsx tsc-defense-stack/cli/run-stack.ts --workspace <path>
9
+ * npx tsx tsc-defense-stack/cli/run-stack.ts --workspace <path> --json
10
+ * npx tsx tsc-defense-stack/cli/run-stack.ts --workspace <path> --no-lsp
11
+ *
12
+ * Why standalone: iterate on TSC reliability without running the full
13
+ * SpecToShip pipeline (~$1 per run). See tsc-defense-stack/CLAUDE.md.
14
+ */
15
+
16
+ import * as path from "node:path";
17
+ import * as fs from "node:fs";
18
+ import {
19
+ runValidationLoop,
20
+ discoverTsFiles,
21
+ type ValidationLoopResult,
22
+ } from "../src/index.js";
23
+
24
+ interface CliArgs {
25
+ workspace: string;
26
+ json: boolean;
27
+ noLsp: boolean;
28
+ files: string[] | undefined;
29
+ verbose: boolean;
30
+ }
31
+
32
+ interface StackReport {
33
+ workspace: string;
34
+ errorsBefore: number;
35
+ lspFixer: {
36
+ ran: boolean;
37
+ fixesApplied: number;
38
+ filesEdited: string[];
39
+ iterations: number;
40
+ } | null;
41
+ errorsAfter: number;
42
+ remainingByCode: Record<string, number>;
43
+ remainingByFile: Record<string, number>;
44
+ passed: boolean;
45
+ elapsedMs: number;
46
+ logs?: string[];
47
+ }
48
+
49
+ function parseArgs(argv: string[]): CliArgs {
50
+ const args: CliArgs = {
51
+ workspace: "",
52
+ json: false,
53
+ noLsp: false,
54
+ files: undefined,
55
+ verbose: false,
56
+ };
57
+ for (let i = 0; i < argv.length; i++) {
58
+ const a = argv[i];
59
+ if (a === "--workspace" || a === "-w") {
60
+ args.workspace = argv[++i] ?? "";
61
+ } else if (a === "--json") {
62
+ args.json = true;
63
+ } else if (a === "--no-lsp") {
64
+ args.noLsp = true;
65
+ } else if (a === "--files") {
66
+ args.files = (argv[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
67
+ } else if (a === "--verbose" || a === "-v") {
68
+ args.verbose = true;
69
+ } else if (a === "--help" || a === "-h") {
70
+ printHelp();
71
+ process.exit(0);
72
+ }
73
+ }
74
+ if (!args.workspace) {
75
+ console.error("error: --workspace <path> is required");
76
+ printHelp();
77
+ process.exit(2);
78
+ }
79
+ return args;
80
+ }
81
+
82
+ function printHelp(): void {
83
+ console.error(`
84
+ Usage: run-stack --workspace <path> [options]
85
+
86
+ Options:
87
+ --workspace, -w <path> Workspace root (required)
88
+ --files <list> Comma-separated file paths to scope tsc/lsp to (default: all .ts/.tsx)
89
+ --no-lsp Skip Layer 0 LSP auto-fixer
90
+ --json Emit JSON report on stdout
91
+ --verbose, -v Stream layer logs to stderr
92
+ --help, -h Show this help
93
+
94
+ Exit codes:
95
+ 0 no errors after stack
96
+ 1 errors remain after stack
97
+ 2 bad arguments / harness error
98
+ `.trim());
99
+ }
100
+
101
+ function makeLogger(captureLines: string[], verbose: boolean) {
102
+ const log = (level: string, msg: string) => {
103
+ const line = `[${level}] ${msg}`;
104
+ captureLines.push(line);
105
+ if (verbose) process.stderr.write(line + "\n");
106
+ };
107
+ return {
108
+ info: (m: string) => log("info", m),
109
+ warn: (m: string) => log("warn", m),
110
+ error: (m: string) => log("error", m),
111
+ };
112
+ }
113
+
114
+ function printHumanReport(r: StackReport): void {
115
+ const w = process.stderr;
116
+ w.write(`\nTSC Defense Stack — ${r.workspace}\n`);
117
+ w.write(` errors before: ${r.errorsBefore}\n`);
118
+ if (r.lspFixer?.ran) {
119
+ w.write(
120
+ ` LSP fixer: applied ${r.lspFixer.fixesApplied} fix(es) in ${r.lspFixer.iterations} iter(s); edited ${r.lspFixer.filesEdited.length} file(s)\n`,
121
+ );
122
+ } else {
123
+ w.write(` LSP fixer: skipped\n`);
124
+ }
125
+ w.write(` errors after: ${r.errorsAfter}\n`);
126
+ if (r.errorsAfter > 0) {
127
+ const top = Object.entries(r.remainingByCode)
128
+ .sort((a, b) => b[1] - a[1])
129
+ .slice(0, 8);
130
+ w.write(` top remaining codes:\n`);
131
+ for (const [code, n] of top) {
132
+ w.write(` ${code.padEnd(8)} ${n}\n`);
133
+ }
134
+ }
135
+ w.write(` elapsed: ${r.elapsedMs}ms\n`);
136
+ w.write(` ${r.passed ? "✓ PASS" : "✗ FAIL"}\n\n`);
137
+ }
138
+
139
+ async function main(): Promise<number> {
140
+ const args = parseArgs(process.argv.slice(2));
141
+ const workspaceRoot = path.resolve(args.workspace);
142
+ if (!fs.existsSync(workspaceRoot)) {
143
+ console.error(`error: workspace not found: ${workspaceRoot}`);
144
+ return 2;
145
+ }
146
+ if (!fs.existsSync(path.join(workspaceRoot, "tsconfig.json"))) {
147
+ console.error(`error: no tsconfig.json in ${workspaceRoot}`);
148
+ return 2;
149
+ }
150
+
151
+ const logs: string[] = [];
152
+ const logger = makeLogger(logs, args.verbose);
153
+
154
+ const targetFiles = args.files ?? discoverTsFiles(workspaceRoot);
155
+ if (targetFiles.length === 0) {
156
+ console.error("error: no .ts/.tsx files found in workspace");
157
+ return 2;
158
+ }
159
+
160
+ const loop: ValidationLoopResult = runValidationLoop({
161
+ workspaceRoot,
162
+ targetFiles,
163
+ skipLSPFixer: args.noLsp,
164
+ logger,
165
+ });
166
+
167
+ const report: StackReport = {
168
+ workspace: path.relative(process.cwd(), workspaceRoot) || workspaceRoot,
169
+ errorsBefore: loop.errorsBefore,
170
+ lspFixer: args.noLsp
171
+ ? { ran: false, fixesApplied: 0, filesEdited: [], iterations: 0 }
172
+ : loop.lspFixer,
173
+ errorsAfter: loop.errorsAfter,
174
+ remainingByCode: loop.remainingByCode,
175
+ remainingByFile: loop.remainingByFile,
176
+ passed: loop.passed,
177
+ elapsedMs: loop.elapsedMs,
178
+ };
179
+
180
+ if (args.json) {
181
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
182
+ } else {
183
+ printHumanReport(report);
184
+ }
185
+
186
+ return report.passed ? 0 : 1;
187
+ }
188
+
189
+ main().then(
190
+ (code) => process.exit(code),
191
+ (err) => {
192
+ console.error("harness error:", err instanceof Error ? err.stack : err);
193
+ process.exit(2);
194
+ },
195
+ );
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@shipispec/tsfix",
3
+ "version": "0.1.0",
4
+ "description": "Reusable TypeScript error-recovery agent. Validates LLM-generated TS code, auto-fixes deterministic error classes via the TS Language Service, and exposes hooks for LLM-driven repair.",
5
+ "keywords": [
6
+ "typescript",
7
+ "tsc",
8
+ "error-recovery",
9
+ "language-service",
10
+ "code-fix",
11
+ "auto-fix",
12
+ "llm",
13
+ "ai-codegen",
14
+ "validator",
15
+ "linter"
16
+ ],
17
+ "license": "MIT",
18
+ "author": "owgreen-dev <ogreenowow@gmail.com>",
19
+ "homepage": "https://github.com/owgreen-dev/spectoship-meta/tree/main/tsc-defense-stack#readme",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/owgreen-dev/spectoship-meta.git",
23
+ "directory": "tsc-defense-stack"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/owgreen-dev/spectoship-meta/issues"
27
+ },
28
+ "engines": {
29
+ "node": ">=20.9.0"
30
+ },
31
+ "type": "module",
32
+ "main": "src/index.ts",
33
+ "types": "src/index.ts",
34
+ "exports": {
35
+ ".": "./src/index.ts",
36
+ "./validation": "./src/validatorInProcess.ts",
37
+ "./lsp-fixer": "./src/tsLanguageServiceFixer.ts"
38
+ },
39
+ "bin": {
40
+ "tsfix": "./bin/tsfix.mjs"
41
+ },
42
+ "files": [
43
+ "src",
44
+ "!src/**/*.test.ts",
45
+ "cli",
46
+ "bin",
47
+ "README.md",
48
+ "LICENSE"
49
+ ],
50
+ "scripts": {
51
+ "setup-fixtures": "node -e \"require('fs').existsSync('fixtures/_shared/node_modules')||require('child_process').execSync('npm install --prefix fixtures/_shared',{stdio:'inherit'})\"",
52
+ "prebenchmark": "npm run setup-fixtures",
53
+ "pretest": "npm run setup-fixtures",
54
+ "benchmark": "tsx benchmark/run-benchmark.ts",
55
+ "run-stack": "tsx cli/run-stack.ts",
56
+ "test": "vitest run",
57
+ "check-types": "tsc --noEmit"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^20.0.0",
61
+ "tsx": "^4.20.6",
62
+ "typescript": "^5.9.3",
63
+ "vitest": "^3.2.4"
64
+ },
65
+ "peerDependencies": {
66
+ "typescript": ">=5.0.0"
67
+ }
68
+ }
package/src/index.ts ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * @shipispec/tsfix — public API.
3
+ *
4
+ * A reusable TypeScript error-recovery agent. Validates LLM-generated (or any)
5
+ * TypeScript code via in-process tsc, auto-fixes deterministic error classes
6
+ * (TS2304/2305/2552/2724) via TypeScript's built-in code-fix engine, and
7
+ * exposes hooks for LLM-driven repair (planned, not yet shipped).
8
+ *
9
+ * ## Quick start (library)
10
+ *
11
+ * ```ts
12
+ * import { runValidationLoop } from "@shipispec/tsfix";
13
+ *
14
+ * const result = await runValidationLoop({
15
+ * workspaceRoot: "/path/to/your/project",
16
+ * targetFiles: ["src/index.ts", "src/utils.ts"],
17
+ * });
18
+ *
19
+ * console.log(result.passed, result.errorsAfter, result.lspFixer.fixesApplied);
20
+ * ```
21
+ *
22
+ * ## Quick start (CLI)
23
+ *
24
+ * ```
25
+ * npx @shipispec/tsfix --workspace ./my-project
26
+ * ```
27
+ *
28
+ * ## Layered API
29
+ *
30
+ * - `runValidationLoop` — full deterministic loop (recommended entry point)
31
+ * - `runInProcessTsc` — just type-check, returns structured diagnostics
32
+ * - `runLSPFixerPass` — just the auto-fix pass, edits files in place
33
+ *
34
+ * ## What it doesn't do (yet)
35
+ *
36
+ * LLM-driven repair (the mend-agent layers from the spectoship pipeline) is
37
+ * not exported here yet. They depend on internal types (ParsedTask) that need
38
+ * to be redesigned as opaque interfaces before they can be moved into this
39
+ * package. v0.2 target.
40
+ */
41
+
42
+ export { runInProcessTsc, isInProcessTscEnabled, resetInProcessTscCache } from "./validatorInProcess.js";
43
+ export type { InProcessTscOptions, InProcessTscResult } from "./validatorInProcess.js";
44
+
45
+ export { runLSPFixerPass, isLSPFixerEnabled, resetLSPFixerCache } from "./tsLanguageServiceFixer.js";
46
+ export type { LSPFixerOptions, LSPFixerResult, LSPFixerLogger } from "./tsLanguageServiceFixer.js";
47
+
48
+ import * as fs from "node:fs";
49
+ import * as path from "node:path";
50
+ import {
51
+ runInProcessTsc,
52
+ resetInProcessTscCache,
53
+ type InProcessTscResult,
54
+ } from "./validatorInProcess.js";
55
+ import { runLSPFixerPass } from "./tsLanguageServiceFixer.js";
56
+
57
+ /** Logger shape required by the validation/fix loop. Plain object with three methods. */
58
+ export interface Logger {
59
+ info(msg: string): void;
60
+ warn(msg: string): void;
61
+ error(msg: string): void;
62
+ }
63
+
64
+ export interface ValidationLoopOptions {
65
+ /** Absolute path to the workspace (must contain `tsconfig.json`). */
66
+ workspaceRoot: string;
67
+ /**
68
+ * Files to scope the type-check + fix to. If omitted, all .ts/.tsx files
69
+ * under `workspaceRoot` (excluding node_modules, .next, dist, build, .git)
70
+ * are discovered.
71
+ */
72
+ targetFiles?: string[];
73
+ /** Skip Layer 0 LSP auto-fixer. Default false. */
74
+ skipLSPFixer?: boolean;
75
+ /** Default: a no-op logger. Pass your own to capture layer events. */
76
+ logger?: Logger;
77
+ }
78
+
79
+ export interface ValidationLoopResult {
80
+ passed: boolean;
81
+ errorsBefore: number;
82
+ errorsAfter: number;
83
+ lspFixer: {
84
+ ran: boolean;
85
+ fixesApplied: number;
86
+ filesEdited: string[];
87
+ iterations: number;
88
+ };
89
+ remainingByCode: Record<string, number>;
90
+ remainingByFile: Record<string, number>;
91
+ diagnostics: InProcessTscResult["diagnostics"];
92
+ elapsedMs: number;
93
+ }
94
+
95
+ const noopLogger: Logger = {
96
+ info: () => {},
97
+ warn: () => {},
98
+ error: () => {},
99
+ };
100
+
101
+ /**
102
+ * Discover all `.ts` / `.tsx` files under a workspace, excluding common
103
+ * non-source dirs. Skips `.d.ts` declaration files.
104
+ */
105
+ export function discoverTsFiles(workspaceRoot: string): string[] {
106
+ const out: string[] = [];
107
+ const skip = new Set(["node_modules", ".next", "dist", "build", ".git", "out", "coverage"]);
108
+ const walk = (dir: string): void => {
109
+ let entries: fs.Dirent[];
110
+ try {
111
+ entries = fs.readdirSync(dir, { withFileTypes: true });
112
+ } catch {
113
+ return;
114
+ }
115
+ for (const e of entries) {
116
+ if (e.isDirectory()) {
117
+ if (skip.has(e.name)) {
118
+ continue;
119
+ }
120
+ walk(path.join(dir, e.name));
121
+ } else if (e.isFile() && !e.name.endsWith(".d.ts")) {
122
+ if (e.name.endsWith(".ts") || e.name.endsWith(".tsx")) {
123
+ out.push(path.relative(workspaceRoot, path.join(dir, e.name)));
124
+ }
125
+ }
126
+ }
127
+ };
128
+ walk(workspaceRoot);
129
+ return out;
130
+ }
131
+
132
+ /**
133
+ * Run the full deterministic validation + fix loop:
134
+ *
135
+ * 1. In-process tsc → capture baseline diagnostics
136
+ * 2. If errors AND not `skipLSPFixer`, run Layer 0 LSP auto-fix
137
+ * 3. If fixes were applied, re-run in-process tsc to capture post-fix state
138
+ * 4. Return aggregated result
139
+ *
140
+ * Throws on missing `tsconfig.json` or workspace path.
141
+ */
142
+ export function runValidationLoop(opts: ValidationLoopOptions): ValidationLoopResult {
143
+ const { workspaceRoot, skipLSPFixer = false } = opts;
144
+ const logger = opts.logger ?? noopLogger;
145
+
146
+ if (!fs.existsSync(workspaceRoot)) {
147
+ throw new Error(`workspace not found: ${workspaceRoot}`);
148
+ }
149
+ if (!fs.existsSync(path.join(workspaceRoot, "tsconfig.json"))) {
150
+ throw new Error(`no tsconfig.json in ${workspaceRoot}`);
151
+ }
152
+
153
+ const targetFiles = opts.targetFiles ?? discoverTsFiles(workspaceRoot);
154
+ const startMs = Date.now();
155
+
156
+ resetInProcessTscCache();
157
+ const before = runInProcessTsc({ workspaceRoot, generatedFiles: targetFiles, logger });
158
+ const errorsBefore = before.diagnostics.filter((d) => d.category === "error").length;
159
+
160
+ let after = before;
161
+ let lspFixer = {
162
+ ran: false,
163
+ fixesApplied: 0,
164
+ filesEdited: [] as string[],
165
+ iterations: 0,
166
+ };
167
+
168
+ if (errorsBefore > 0 && !skipLSPFixer) {
169
+ const lsp = runLSPFixerPass({ workspaceRoot, targetFiles, logger });
170
+ lspFixer = {
171
+ ran: true,
172
+ fixesApplied: lsp.fixesApplied,
173
+ filesEdited: lsp.filesEdited,
174
+ iterations: lsp.iterations,
175
+ };
176
+ if (lsp.fixesApplied > 0) {
177
+ resetInProcessTscCache();
178
+ after = runInProcessTsc({ workspaceRoot, generatedFiles: targetFiles, logger });
179
+ }
180
+ }
181
+
182
+ const errorDiags = after.diagnostics.filter((d) => d.category === "error");
183
+ const errorsAfter = errorDiags.length;
184
+
185
+ const remainingByCode: Record<string, number> = {};
186
+ const remainingByFile: Record<string, number> = {};
187
+ for (const d of errorDiags) {
188
+ remainingByCode[d.code] = (remainingByCode[d.code] ?? 0) + 1;
189
+ remainingByFile[d.file] = (remainingByFile[d.file] ?? 0) + 1;
190
+ }
191
+
192
+ return {
193
+ passed: errorsAfter === 0,
194
+ errorsBefore,
195
+ errorsAfter,
196
+ lspFixer,
197
+ remainingByCode,
198
+ remainingByFile,
199
+ diagnostics: after.diagnostics,
200
+ elapsedMs: Date.now() - startMs,
201
+ };
202
+ }