@shipispec/tsfix 0.1.0 → 0.3.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/CHANGELOG.md +104 -0
- package/README.md +144 -86
- package/dist/cli.js +724 -0
- package/dist/index.d.ts +176 -0
- package/dist/index.js +576 -0
- package/dist/types/index.d.ts +176 -0
- package/dist/types/tsLanguageServiceFixer.d.ts +124 -0
- package/dist/types/validatorInProcess.d.ts +64 -0
- package/package.json +19 -16
- package/bin/tsfix.mjs +0 -49
- package/cli/run-stack.ts +0 -195
- package/src/index.ts +0 -202
- package/src/tsLanguageServiceFixer.ts +0 -486
- package/src/validatorInProcess.ts +0 -276
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
* ## Public types for downstream LLM-mend integrations
|
|
35
|
+
*
|
|
36
|
+
* - `Diagnostic` — single tsc error (re-exported from `runInProcessTsc`)
|
|
37
|
+
* - `MendContext` — input contract for a Layer 2–4 LLM-mend agent
|
|
38
|
+
* - `LayerEvent` — per-layer event shape for streaming telemetry
|
|
39
|
+
*
|
|
40
|
+
* The mend agents themselves (`@shipispec/tsmend`, planned) consume these
|
|
41
|
+
* types but are not shipped from this package — `tsfix` stays Layer 0–1
|
|
42
|
+
* deterministic.
|
|
43
|
+
*/
|
|
44
|
+
export { runInProcessTsc, isInProcessTscEnabled, resetInProcessTscCache } from "./validatorInProcess.js";
|
|
45
|
+
export type { InProcessTscOptions, InProcessTscResult } from "./validatorInProcess.js";
|
|
46
|
+
export { runLSPFixerPass, isLSPFixerEnabled, resetLSPFixerCache } from "./tsLanguageServiceFixer.js";
|
|
47
|
+
export type { LSPFixerOptions, LSPFixerResult, LSPFixerLogger } from "./tsLanguageServiceFixer.js";
|
|
48
|
+
import { type InProcessTscResult } from "./validatorInProcess.js";
|
|
49
|
+
/** Logger shape required by the validation/fix loop. Plain object with three methods. */
|
|
50
|
+
export interface Logger {
|
|
51
|
+
info(msg: string): void;
|
|
52
|
+
warn(msg: string): void;
|
|
53
|
+
error(msg: string): void;
|
|
54
|
+
}
|
|
55
|
+
export interface ValidationLoopOptions {
|
|
56
|
+
/** Absolute path to the workspace (must contain `tsconfig.json`). */
|
|
57
|
+
workspaceRoot: string;
|
|
58
|
+
/**
|
|
59
|
+
* Files to scope the type-check + fix to. If omitted, all .ts/.tsx files
|
|
60
|
+
* under `workspaceRoot` (excluding node_modules, .next, dist, build, .git)
|
|
61
|
+
* are discovered.
|
|
62
|
+
*/
|
|
63
|
+
targetFiles?: string[];
|
|
64
|
+
/** Skip Layer 0 LSP auto-fixer. Default false. */
|
|
65
|
+
skipLSPFixer?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Run the LSP fixer in memory but do NOT persist edits to disk. The
|
|
68
|
+
* returned `lspFixer.filesEdited` lists files that *would* have been
|
|
69
|
+
* written. Useful for previewing changes before letting tsfix mutate a
|
|
70
|
+
* workspace. Default false.
|
|
71
|
+
*/
|
|
72
|
+
dryRun?: boolean;
|
|
73
|
+
/** Default: a no-op logger. Pass your own to capture layer events. */
|
|
74
|
+
logger?: Logger;
|
|
75
|
+
}
|
|
76
|
+
export interface ValidationLoopResult {
|
|
77
|
+
passed: boolean;
|
|
78
|
+
errorsBefore: number;
|
|
79
|
+
errorsAfter: number;
|
|
80
|
+
lspFixer: {
|
|
81
|
+
ran: boolean;
|
|
82
|
+
fixesApplied: number;
|
|
83
|
+
filesEdited: string[];
|
|
84
|
+
iterations: number;
|
|
85
|
+
};
|
|
86
|
+
remainingByCode: Record<string, number>;
|
|
87
|
+
remainingByFile: Record<string, number>;
|
|
88
|
+
diagnostics: InProcessTscResult["diagnostics"];
|
|
89
|
+
elapsedMs: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Discover all `.ts` / `.tsx` files under a workspace, excluding common
|
|
93
|
+
* non-source dirs. Skips `.d.ts` declaration files.
|
|
94
|
+
*/
|
|
95
|
+
export declare function discoverTsFiles(workspaceRoot: string): string[];
|
|
96
|
+
/**
|
|
97
|
+
* Run the full deterministic validation + fix loop:
|
|
98
|
+
*
|
|
99
|
+
* 1. In-process tsc → capture baseline diagnostics
|
|
100
|
+
* 2. If errors AND not `skipLSPFixer`, run Layer 0 LSP auto-fix
|
|
101
|
+
* 3. If fixes were applied, re-run in-process tsc to capture post-fix state
|
|
102
|
+
* 4. Return aggregated result
|
|
103
|
+
*
|
|
104
|
+
* Throws on missing `tsconfig.json` or workspace path.
|
|
105
|
+
*/
|
|
106
|
+
export declare function runValidationLoop(opts: ValidationLoopOptions): ValidationLoopResult;
|
|
107
|
+
/**
|
|
108
|
+
* Single tsc diagnostic. Re-exported from `runInProcessTsc`'s result type
|
|
109
|
+
* so consumers building a `MendContext` don't have to dig the shape out of
|
|
110
|
+
* `InProcessTscResult["diagnostics"][number]`.
|
|
111
|
+
*/
|
|
112
|
+
export type Diagnostic = InProcessTscResult["diagnostics"][number];
|
|
113
|
+
/**
|
|
114
|
+
* Input contract for a Layer 2–4 LLM-mend agent.
|
|
115
|
+
*
|
|
116
|
+
* Pattern:
|
|
117
|
+
* 1. Run `runValidationLoop` (Layer 0/1).
|
|
118
|
+
* 2. If `result.errorsAfter > 0`, build a `MendContext` from the
|
|
119
|
+
* surviving diagnostics + whatever task/spec context your pipeline has.
|
|
120
|
+
* 3. Hand off to a mend agent (e.g. `@shipispec/tsmend`).
|
|
121
|
+
*
|
|
122
|
+
* Required fields: `workspaceRoot`, `diagnostics`, `erroredFiles`.
|
|
123
|
+
* Everything else is optional — leave fields out if your pipeline doesn't
|
|
124
|
+
* carry them.
|
|
125
|
+
*/
|
|
126
|
+
export interface MendContext {
|
|
127
|
+
/** Absolute path to the workspace (must contain `tsconfig.json`). */
|
|
128
|
+
workspaceRoot: string;
|
|
129
|
+
/** Diagnostics that survived Layer 0/1 and need higher-layer repair. */
|
|
130
|
+
diagnostics: Diagnostic[];
|
|
131
|
+
/** Absolute paths of files containing the surviving diagnostics. */
|
|
132
|
+
erroredFiles: string[];
|
|
133
|
+
/** Optional one-line summary of what the failing code was supposed to do. */
|
|
134
|
+
taskDescription?: string;
|
|
135
|
+
/** Optional Markdown spec the code is implementing. Helps the LLM understand intent. */
|
|
136
|
+
featureSpecText?: string;
|
|
137
|
+
/** Optional testable acceptance criteria from the spec. */
|
|
138
|
+
acceptanceCriteria?: string;
|
|
139
|
+
/** Other tasks in the same feature, with their files and current status. */
|
|
140
|
+
siblingTasks?: Array<{
|
|
141
|
+
description: string;
|
|
142
|
+
files: string[];
|
|
143
|
+
status: "pending" | "completed" | "failed";
|
|
144
|
+
}>;
|
|
145
|
+
/** Public API surface from earlier completed tasks (helps prevent re-defining symbols). */
|
|
146
|
+
priorTaskExports?: string;
|
|
147
|
+
/** Compact type signatures of installed npm dependencies (helps prevent API hallucination). */
|
|
148
|
+
installedTypes?: string;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Per-layer event for streaming telemetry across the validate → fix → mend
|
|
152
|
+
* chain. Designed for an `onLayerEvent` callback (added in a future minor
|
|
153
|
+
* release) rather than accumulating in a result array — a workspace with
|
|
154
|
+
* 200 errors emits ~1000 events.
|
|
155
|
+
*
|
|
156
|
+
* Layer assignments:
|
|
157
|
+
* 0 = prevention (prompt rules, exported-API injection — caller's problem)
|
|
158
|
+
* 1 = tsfix LSP fixer (this package)
|
|
159
|
+
* 2 = single-file LLM mend
|
|
160
|
+
* 3 = multi-file LLM mend (blast-radius search/replace)
|
|
161
|
+
* 4 = stub-and-continue (escape hatch)
|
|
162
|
+
*/
|
|
163
|
+
export interface LayerEvent {
|
|
164
|
+
/** Which layer ran. */
|
|
165
|
+
layer: 0 | 1 | 2 | 3 | 4;
|
|
166
|
+
/** TypeScript error code being acted on (e.g. 2304, 2339, 7006). */
|
|
167
|
+
errorCode: number;
|
|
168
|
+
/** True if the error was resolved by this layer. */
|
|
169
|
+
fixed: boolean;
|
|
170
|
+
/** Wall-clock time spent on this attempt. */
|
|
171
|
+
latencyMs: number;
|
|
172
|
+
/** USD cost (LLM tokens). Undefined for deterministic layers. */
|
|
173
|
+
costUsd?: number;
|
|
174
|
+
/** `Date.now()` at emission. */
|
|
175
|
+
ts: number;
|
|
176
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TS Language Service Fixer — Sprint G / Sprint J (2026-05-03).
|
|
3
|
+
*
|
|
4
|
+
* Layer 0 of the mend stack. Uses TypeScript's built-in `LanguageService.getCodeFixesAtPosition`
|
|
5
|
+
* (the same engine VS Code's Quick Fix uses) to resolve common errors *deterministically*,
|
|
6
|
+
* before we spend a single LLM call on them.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: ~80% of generated-code TS errors fall into a small set of
|
|
9
|
+
* boring categories that the compiler already knows how to fix:
|
|
10
|
+
*
|
|
11
|
+
* - TS2304 "Cannot find name X" → auto-import
|
|
12
|
+
* - TS2305 "no exported member named X" → did-you-mean rename
|
|
13
|
+
* - TS2551 "Property X does not exist on Y. Did you mean Z?" → spelling fix
|
|
14
|
+
* - TS2552 "Cannot find name X. Did you mean Y?" → spelling fix
|
|
15
|
+
* - TS2724 "no exported member, did you mean Y?" → import rename
|
|
16
|
+
*
|
|
17
|
+
* For these, the fixer is free (no LLM), fast (~ms), and deterministic.
|
|
18
|
+
* The LLM mend stack only gets called for *interesting* errors that require
|
|
19
|
+
* semantic reasoning (signature drift, missing logic, package gotchas).
|
|
20
|
+
*
|
|
21
|
+
* Conservative coverage: we only apply fixes for codes whose auto-fixes are
|
|
22
|
+
* unambiguous. Codes like TS7006 (implicit any) and TS2741 (missing property)
|
|
23
|
+
* are skipped — those need human intent to choose the right type or default
|
|
24
|
+
* value, and a wrong auto-fix introduces silent bugs.
|
|
25
|
+
*
|
|
26
|
+
* Iteration cap: 5 passes. After each pass we re-validate; cascades like
|
|
27
|
+
* "rename import → rename type annotation → rename method call" can need 3-4
|
|
28
|
+
* hops to converge. The signature-set progress check stops sooner if no new
|
|
29
|
+
* errors appear. If errors remain after pass 5, escalate to LLM mend.
|
|
30
|
+
*
|
|
31
|
+
* Feature flag: `SPECTOSHIP_TS_LSP_FIXER=false` opts out (default: ON).
|
|
32
|
+
*/
|
|
33
|
+
import * as ts from "typescript";
|
|
34
|
+
export interface LSPFixerLogger {
|
|
35
|
+
info(msg: string): void;
|
|
36
|
+
warn(msg: string): void;
|
|
37
|
+
error(msg: string): void;
|
|
38
|
+
}
|
|
39
|
+
export interface LSPFixerOptions {
|
|
40
|
+
workspaceRoot: string;
|
|
41
|
+
/** Files where errors were detected. Limits the fix scope. */
|
|
42
|
+
targetFiles: string[];
|
|
43
|
+
logger: LSPFixerLogger;
|
|
44
|
+
/** Max iterations (default 5). Signature-set progress check stops sooner. */
|
|
45
|
+
maxIterations?: number;
|
|
46
|
+
/**
|
|
47
|
+
* When true, run the full fix loop in memory but do NOT persist edits to
|
|
48
|
+
* disk. Returned `LSPFixerResult` is identical otherwise — `filesEdited`
|
|
49
|
+
* lists the files that *would* have been written, `fixesApplied` is the
|
|
50
|
+
* count of fixes the loop computed. Use to preview what tsfix would do
|
|
51
|
+
* before letting it modify a workspace.
|
|
52
|
+
*/
|
|
53
|
+
dryRun?: boolean;
|
|
54
|
+
}
|
|
55
|
+
export interface LSPFixerResult {
|
|
56
|
+
/** Number of fixes successfully applied across all iterations. */
|
|
57
|
+
fixesApplied: number;
|
|
58
|
+
/** Files whose contents were modified on disk. */
|
|
59
|
+
filesEdited: string[];
|
|
60
|
+
/** Iteration count when fixer stopped (1 if it converged on first pass). */
|
|
61
|
+
iterations: number;
|
|
62
|
+
/** When true, every diagnostic was auto-fixable and resolved. Caller can skip LLM mend. */
|
|
63
|
+
allResolved: boolean;
|
|
64
|
+
/** Errors remaining after the last iteration (caller passes these to LLM mend). */
|
|
65
|
+
remainingErrors: Array<{
|
|
66
|
+
file: string;
|
|
67
|
+
line: number;
|
|
68
|
+
column: number;
|
|
69
|
+
code: string;
|
|
70
|
+
message: string;
|
|
71
|
+
}>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Apply LSP code-fixes to all diagnostics in the workspace whose error code
|
|
75
|
+
* is in SAFE_FIXABLE_CODES. Writes edits back to disk. Re-runs ts diagnostics
|
|
76
|
+
* after each pass; stops when no further fixable errors remain or
|
|
77
|
+
* maxIterations is reached.
|
|
78
|
+
*
|
|
79
|
+
* Throws on host setup failure (missing tsconfig, etc.) — callers should
|
|
80
|
+
* catch and fall through to LLM mend.
|
|
81
|
+
*/
|
|
82
|
+
export declare function runLSPFixerPass(opts: LSPFixerOptions): LSPFixerResult;
|
|
83
|
+
/**
|
|
84
|
+
* When the LanguageService returns multiple code-fix candidates, only apply
|
|
85
|
+
* if they're textually equivalent (same edits on the same files). This
|
|
86
|
+
* conservatively skips ambiguous cases (e.g., import from `lib/foo` vs
|
|
87
|
+
* `lib/bar` where both export `Foo`) where guessing wrong is worse than
|
|
88
|
+
* deferring to the LLM.
|
|
89
|
+
*/
|
|
90
|
+
/**
|
|
91
|
+
* @internal Compute a stable `(file, start, code)` signature for each fixable
|
|
92
|
+
* error. Used by the iteration loop's stuck-loop detector.
|
|
93
|
+
*/
|
|
94
|
+
export declare function computeErrorSignatures(errors: readonly {
|
|
95
|
+
file: string;
|
|
96
|
+
start: number;
|
|
97
|
+
code: number;
|
|
98
|
+
}[]): Set<string>;
|
|
99
|
+
/**
|
|
100
|
+
* @internal True if `a` and `b` contain the same members. Used to decide
|
|
101
|
+
* whether the iteration loop is stuck (same error set across passes) vs.
|
|
102
|
+
* making genuine progress (set membership changed even if size didn't).
|
|
103
|
+
*/
|
|
104
|
+
export declare function signatureSetsEqual(a: Set<string>, b: Set<string>): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* @internal True if every fix in `fixes` produces identical text edits.
|
|
107
|
+
* Used to decide whether multiple candidate fixes for one error are safe to
|
|
108
|
+
* pick automatically (identical = unambiguous; different = abstain).
|
|
109
|
+
*/
|
|
110
|
+
export declare function fixesAreEquivalent(fixes: readonly ts.CodeFixAction[]): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* @internal Apply a CodeFixAction's text changes to in-memory snapshots.
|
|
113
|
+
* Returns the number of changes successfully applied. Bumps script versions
|
|
114
|
+
* so the LanguageService re-parses on next call. Skips edits to files not
|
|
115
|
+
* already in `snapshots` (defensive — won't create new files unbeknownst).
|
|
116
|
+
*/
|
|
117
|
+
export declare function applyFixToSnapshots(fix: ts.CodeFixAction, snapshots: Map<string, {
|
|
118
|
+
content: string;
|
|
119
|
+
version: number;
|
|
120
|
+
}>): number;
|
|
121
|
+
/** Whether the LSP fixer is enabled (env-flag opt-out). Default ON. */
|
|
122
|
+
export declare function isLSPFixerEnabled(): boolean;
|
|
123
|
+
/** Reset internal caches (for tests). No-op currently — service is created per-call. */
|
|
124
|
+
export declare function resetLSPFixerCache(): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process TypeScript validator (Phase 5 — TSC iron-clad).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the `tsc --noEmit` shell exec with `ts.createProgram` +
|
|
5
|
+
* `getPreEmitDiagnostics()`. Three wins over shelling:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Structured diagnostics** — `{file, start, length, code, messageText,
|
|
8
|
+
* category, relatedInformation}` natively, no regex parsing of tsc text
|
|
9
|
+
* output. Feeds Layer 2's symbol tracer directly.
|
|
10
|
+
*
|
|
11
|
+
* 2. **Speed** — long-lived Program per workspace caches lib files,
|
|
12
|
+
* transitive deps, and tsconfig parse. Cold start is comparable; warm
|
|
13
|
+
* validation is ~5-10× faster than spawning a fresh tsc process.
|
|
14
|
+
*
|
|
15
|
+
* 3. **Diagnostic enrichment** — we have the AST in hand, so each error
|
|
16
|
+
* can carry the offending node's source span and aliased symbol info,
|
|
17
|
+
* which the shell tsc doesn't provide.
|
|
18
|
+
*
|
|
19
|
+
* Backwards compatibility: feature-flagged via `SPECTOSHIP_TSC_INPROCESS`
|
|
20
|
+
* env var. Shell tsc remains the default until we measure parity on real
|
|
21
|
+
* projects.
|
|
22
|
+
*/
|
|
23
|
+
export interface InProcessTscResult {
|
|
24
|
+
passed: boolean;
|
|
25
|
+
/** Diagnostic messages formatted in the same shape as `tsc` output. */
|
|
26
|
+
output: string;
|
|
27
|
+
/** Structured per-error data — drives Layer 2 cross-file tracer. */
|
|
28
|
+
diagnostics: Array<{
|
|
29
|
+
file: string;
|
|
30
|
+
line: number;
|
|
31
|
+
column: number;
|
|
32
|
+
code: string;
|
|
33
|
+
message: string;
|
|
34
|
+
category: "error" | "warning" | "message" | "suggestion";
|
|
35
|
+
}>;
|
|
36
|
+
/** Number of lines of output for log truncation. */
|
|
37
|
+
lineCount: number;
|
|
38
|
+
}
|
|
39
|
+
export interface InProcessTscOptions {
|
|
40
|
+
workspaceRoot: string;
|
|
41
|
+
/** Optional list of files to filter diagnostics to (matches shell tsc filter). */
|
|
42
|
+
generatedFiles?: string[];
|
|
43
|
+
logger: {
|
|
44
|
+
info(msg: string): void;
|
|
45
|
+
warn(msg: string): void;
|
|
46
|
+
error(msg: string): void;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Run tsc in-process. Compatible with the shell-based ToolResult shape
|
|
51
|
+
* so the caller can swap implementations transparently.
|
|
52
|
+
*/
|
|
53
|
+
export declare function runInProcessTsc(opts: InProcessTscOptions): InProcessTscResult;
|
|
54
|
+
/** Reset cache (for tests + when workspace switches). */
|
|
55
|
+
export declare function resetInProcessTscCache(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Whether the in-process path is enabled. Defaults to ON as of Sprint J
|
|
58
|
+
* (2026-05-03) — shell tsc consistently times out at 60s on Node 23 due to
|
|
59
|
+
* the documented tsc startup-pause bug, blocking every code-gen task.
|
|
60
|
+
* In-process is also 5-10× faster on warm runs because the Program is reused.
|
|
61
|
+
*
|
|
62
|
+
* Set `SPECTOSHIP_TSC_INPROCESS=false` to opt out.
|
|
63
|
+
*/
|
|
64
|
+
export declare function isInProcessTscEnabled(): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipispec/tsfix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -16,38 +16,40 @@
|
|
|
16
16
|
],
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"author": "owgreen-dev <ogreenowow@gmail.com>",
|
|
19
|
-
"homepage": "https://github.com/owgreen-dev/
|
|
19
|
+
"homepage": "https://github.com/owgreen-dev/tsfix#readme",
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
|
-
"url": "git+https://github.com/owgreen-dev/
|
|
23
|
-
"directory": "tsc-defense-stack"
|
|
22
|
+
"url": "git+https://github.com/owgreen-dev/tsfix.git"
|
|
24
23
|
},
|
|
25
24
|
"bugs": {
|
|
26
|
-
"url": "https://github.com/owgreen-dev/
|
|
25
|
+
"url": "https://github.com/owgreen-dev/tsfix/issues"
|
|
27
26
|
},
|
|
28
27
|
"engines": {
|
|
29
28
|
"node": ">=20.9.0"
|
|
30
29
|
},
|
|
31
30
|
"type": "module",
|
|
32
|
-
"main": "
|
|
33
|
-
"types": "
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
34
33
|
"exports": {
|
|
35
|
-
".":
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/index.js"
|
|
37
|
+
}
|
|
38
38
|
},
|
|
39
39
|
"bin": {
|
|
40
|
-
"tsfix": "./
|
|
40
|
+
"tsfix": "./dist/cli.js"
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
43
|
-
"
|
|
44
|
-
"!src/**/*.test.ts",
|
|
45
|
-
"cli",
|
|
46
|
-
"bin",
|
|
43
|
+
"dist",
|
|
47
44
|
"README.md",
|
|
48
|
-
"LICENSE"
|
|
45
|
+
"LICENSE",
|
|
46
|
+
"CHANGELOG.md"
|
|
49
47
|
],
|
|
50
48
|
"scripts": {
|
|
49
|
+
"build": "node scripts/build.mjs",
|
|
50
|
+
"matrix": "node scripts/run-matrix.mjs",
|
|
51
|
+
"capture": "node scripts/capture-fixture.mjs",
|
|
52
|
+
"prepack": "npm run build",
|
|
51
53
|
"setup-fixtures": "node -e \"require('fs').existsSync('fixtures/_shared/node_modules')||require('child_process').execSync('npm install --prefix fixtures/_shared',{stdio:'inherit'})\"",
|
|
52
54
|
"prebenchmark": "npm run setup-fixtures",
|
|
53
55
|
"pretest": "npm run setup-fixtures",
|
|
@@ -58,6 +60,7 @@
|
|
|
58
60
|
},
|
|
59
61
|
"devDependencies": {
|
|
60
62
|
"@types/node": "^20.0.0",
|
|
63
|
+
"esbuild": "^0.28.0",
|
|
61
64
|
"tsx": "^4.20.6",
|
|
62
65
|
"typescript": "^5.9.3",
|
|
63
66
|
"vitest": "^3.2.4"
|
package/bin/tsfix.mjs
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
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
|
-
);
|