@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 +21 -0
- package/README.md +120 -0
- package/bin/tsfix.mjs +49 -0
- package/cli/run-stack.ts +195 -0
- package/package.json +68 -0
- package/src/index.ts +202 -0
- package/src/tsLanguageServiceFixer.ts +486 -0
- package/src/validatorInProcess.ts +276 -0
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
|
+
});
|
package/cli/run-stack.ts
ADDED
|
@@ -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
|
+
}
|