@agjs/tsforge 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.
Files changed (216) hide show
  1. package/bin/tsforge.js +2 -0
  2. package/package.json +35 -0
  3. package/src/agent/agent.constants.ts +382 -0
  4. package/src/agent/agent.types.ts +34 -0
  5. package/src/agent/index.ts +4 -0
  6. package/src/agent/model-agent.ts +297 -0
  7. package/src/agent/tool-repair.ts +194 -0
  8. package/src/agent/tools.ts +190 -0
  9. package/src/browser/checks.ts +96 -0
  10. package/src/browser/index.ts +8 -0
  11. package/src/browser/oracle.ts +303 -0
  12. package/src/classify.ts +48 -0
  13. package/src/cli.ts +1333 -0
  14. package/src/config/config.constants.ts +9 -0
  15. package/src/config/flags.ts +32 -0
  16. package/src/config/index.ts +8 -0
  17. package/src/config/tsforge-config.ts +301 -0
  18. package/src/constitution/baseline.ts +257 -0
  19. package/src/detect-gate.ts +498 -0
  20. package/src/eval/eval.types.ts +36 -0
  21. package/src/eval/index.ts +3 -0
  22. package/src/eval/judge.ts +62 -0
  23. package/src/eval/score.ts +39 -0
  24. package/src/files/create.ts +22 -0
  25. package/src/files/edit.ts +193 -0
  26. package/src/files/files.constants.ts +11 -0
  27. package/src/files/files.types.ts +81 -0
  28. package/src/files/hashline-format.ts +110 -0
  29. package/src/files/hashline.ts +689 -0
  30. package/src/files/index.ts +19 -0
  31. package/src/index.ts +8 -0
  32. package/src/inference/index.ts +6 -0
  33. package/src/inference/inference.constants.ts +34 -0
  34. package/src/inference/inference.types.ts +123 -0
  35. package/src/inference/openai-compatible.ts +113 -0
  36. package/src/inference/stream-guard.ts +161 -0
  37. package/src/inference/stream.ts +370 -0
  38. package/src/inference/transport.ts +78 -0
  39. package/src/inference/wire.ts +0 -0
  40. package/src/lib/fs/fs.ts +126 -0
  41. package/src/lib/fs/fs.types.ts +5 -0
  42. package/src/lib/fs/index.ts +3 -0
  43. package/src/lib/fs/process.ts +146 -0
  44. package/src/lib/guards/guards.ts +9 -0
  45. package/src/lib/guards/index.ts +1 -0
  46. package/src/lib/json/index.ts +1 -0
  47. package/src/lib/json/json.ts +12 -0
  48. package/src/lib/scope/index.ts +2 -0
  49. package/src/lib/scope/scope.constants.ts +3 -0
  50. package/src/lib/scope/scope.ts +40 -0
  51. package/src/loop/astgrep-fix.ts +228 -0
  52. package/src/loop/feedback/feedback.ts +138 -0
  53. package/src/loop/feedback/index.ts +8 -0
  54. package/src/loop/feedback/meta-rule-docs.ts +41 -0
  55. package/src/loop/feedback/meta-rule-feedback.ts +61 -0
  56. package/src/loop/feedback/rule-docs.generated.json +112 -0
  57. package/src/loop/feedback/rule-docs.ts +342 -0
  58. package/src/loop/index.ts +19 -0
  59. package/src/loop/loop.constants.ts +68 -0
  60. package/src/loop/loop.types.ts +99 -0
  61. package/src/loop/prompt/index.ts +2 -0
  62. package/src/loop/prompt/project-map.ts +69 -0
  63. package/src/loop/prompt/prompt.ts +107 -0
  64. package/src/loop/quality.ts +174 -0
  65. package/src/loop/rule-docs.generated.json +367 -0
  66. package/src/loop/run-spec.ts +88 -0
  67. package/src/loop/run.ts +400 -0
  68. package/src/loop/session.ts +1410 -0
  69. package/src/loop/tools/add-dependency.ts +71 -0
  70. package/src/loop/tools/condense.ts +498 -0
  71. package/src/loop/tools/edit-hashline.ts +80 -0
  72. package/src/loop/tools/execute-tool.ts +80 -0
  73. package/src/loop/tools/file-ops.ts +323 -0
  74. package/src/loop/tools/index.ts +2 -0
  75. package/src/loop/tools/lsp-ops.ts +222 -0
  76. package/src/loop/tools/scaffold-routes.ts +68 -0
  77. package/src/loop/tools/scaffold-ui.ts +62 -0
  78. package/src/loop/tools/scaffold-web.ts +35 -0
  79. package/src/loop/tools/tool-context.ts +126 -0
  80. package/src/loop/ttsr-defaults.ts +53 -0
  81. package/src/loop/ttsr.ts +322 -0
  82. package/src/loop/turn.ts +856 -0
  83. package/src/lsp/index.ts +2 -0
  84. package/src/lsp/lsp.types.ts +56 -0
  85. package/src/lsp/service.ts +500 -0
  86. package/src/meta-rules/context.ts +195 -0
  87. package/src/meta-rules/index.ts +9 -0
  88. package/src/meta-rules/meta-rules.types.ts +47 -0
  89. package/src/meta-rules/parsers/package-json-parser.ts +51 -0
  90. package/src/meta-rules/registry.ts +37 -0
  91. package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
  92. package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
  93. package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
  94. package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
  95. package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
  96. package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
  97. package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
  98. package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
  99. package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
  100. package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
  101. package/src/meta-rules/runner.ts +64 -0
  102. package/src/models-config.ts +196 -0
  103. package/src/render/ansi.ts +289 -0
  104. package/src/render/banner.ts +113 -0
  105. package/src/render/box.ts +134 -0
  106. package/src/render/index.ts +7 -0
  107. package/src/render/markdown.ts +123 -0
  108. package/src/render/render.types.ts +21 -0
  109. package/src/render/stream-markdown.ts +128 -0
  110. package/src/render/style.ts +26 -0
  111. package/src/rule-packs/bullmq/index.ts +39 -0
  112. package/src/rule-packs/bullmq/rules/index.ts +7 -0
  113. package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
  114. package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
  115. package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
  116. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
  117. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
  118. package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
  119. package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
  120. package/src/rule-packs/bullmq/utils.ts +334 -0
  121. package/src/rule-packs/code-flow/index.ts +25 -0
  122. package/src/rule-packs/code-flow/rules/index.ts +3 -0
  123. package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
  124. package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
  125. package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
  126. package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
  127. package/src/rule-packs/comment-hygiene/index.ts +25 -0
  128. package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
  129. package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
  130. package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
  131. package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
  132. package/src/rule-packs/create-rule.ts +9 -0
  133. package/src/rule-packs/drizzle/index.ts +41 -0
  134. package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
  135. package/src/rule-packs/drizzle/rules/index.ts +8 -0
  136. package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
  137. package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
  138. package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
  139. package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
  140. package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
  141. package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
  142. package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
  143. package/src/rule-packs/drizzle/utils.ts +115 -0
  144. package/src/rule-packs/elysia/index.ts +43 -0
  145. package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
  146. package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
  147. package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
  148. package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
  149. package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
  150. package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
  151. package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
  152. package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
  153. package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
  154. package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
  155. package/src/rule-packs/env-access/index.ts +23 -0
  156. package/src/rule-packs/env-access/rules/index.ts +2 -0
  157. package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
  158. package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
  159. package/src/rule-packs/i18n-keys/index.ts +19 -0
  160. package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
  161. package/src/rule-packs/index.ts +139 -0
  162. package/src/rule-packs/jwt-cookies/index.ts +25 -0
  163. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
  164. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
  165. package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
  166. package/src/rule-packs/jwt-cookies/utils.ts +188 -0
  167. package/src/rule-packs/oauth-security/index.ts +25 -0
  168. package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
  169. package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
  170. package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
  171. package/src/rule-packs/oauth-security/utils.ts +127 -0
  172. package/src/rule-packs/react-component-architecture/index.ts +35 -0
  173. package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
  174. package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
  175. package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
  176. package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
  177. package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
  178. package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
  179. package/src/rule-packs/react-component-architecture/utils.ts +47 -0
  180. package/src/rule-packs/rule-packs.types.ts +18 -0
  181. package/src/rule-packs/structured-logging/index.ts +26 -0
  182. package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
  183. package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
  184. package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
  185. package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
  186. package/src/rule-packs/tanstack-query/index.ts +20 -0
  187. package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
  188. package/src/rule-packs/test-conventions/index.ts +23 -0
  189. package/src/rule-packs/test-conventions/rules/index.ts +2 -0
  190. package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
  191. package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
  192. package/src/rule-packs/utils.ts +142 -0
  193. package/src/session-store.ts +359 -0
  194. package/src/spec/generate-tests.ts +213 -0
  195. package/src/spec/index.ts +5 -0
  196. package/src/spec/parse.ts +152 -0
  197. package/src/spec/review-tests.ts +162 -0
  198. package/src/spec/spec.constants.ts +13 -0
  199. package/src/spec/spec.types.ts +79 -0
  200. package/src/stack-detection/detect.ts +246 -0
  201. package/src/stack-detection/index.ts +3 -0
  202. package/src/stack-detection/packs.ts +174 -0
  203. package/src/stack-detection/stack-detection.types.ts +47 -0
  204. package/src/validate/accept.ts +49 -0
  205. package/src/validate/errors.ts +35 -0
  206. package/src/validate/index.ts +12 -0
  207. package/src/validate/parse.ts +148 -0
  208. package/src/validate/run-tests.ts +59 -0
  209. package/src/validate/validate.ts +40 -0
  210. package/src/validate/validate.types.ts +52 -0
  211. package/src/web-components.ts +638 -0
  212. package/src/web-coverage.ts +89 -0
  213. package/src/web-routes.ts +151 -0
  214. package/src/web-templates.ts +1011 -0
  215. package/strict.eslint.config.mjs +84 -0
  216. package/strict.web.eslint.config.mjs +185 -0
@@ -0,0 +1,2 @@
1
+ export * from "./lsp.types";
2
+ export { TsService } from "./service";
@@ -0,0 +1,56 @@
1
+ import type ts from "typescript";
2
+
3
+ export interface ITsDiagnostic {
4
+ code: number;
5
+ message: string;
6
+ file: string;
7
+ start: number;
8
+ length: number;
9
+ }
10
+
11
+ export interface ITsFix {
12
+ description: string;
13
+ /** The text edits this fix applies (possibly across files). */
14
+ changes: readonly ts.FileTextChanges[];
15
+ }
16
+
17
+ /** A reference / definition location, with a 1-based line for readable output. */
18
+ export interface ITsLocation {
19
+ file: string;
20
+ line: number;
21
+ start: number;
22
+ }
23
+
24
+ /** A workspace symbol hit (from navigate-to). */
25
+ export interface ITsSymbol {
26
+ name: string;
27
+ kind: string;
28
+ file: string;
29
+ line: number;
30
+ }
31
+
32
+ /** Reference sites in one file (the lines that depend on a symbol). */
33
+ export interface ITsImpactFile {
34
+ file: string;
35
+ lines: number[];
36
+ }
37
+
38
+ /** The blast radius of a symbol: which files/lines reference it. Type-EXACT (from
39
+ * the TS LanguageService), not a tree-sitter guess — so it can drive a deliberate
40
+ * cross-file edit instead of discovering breakage at the gate. */
41
+ export interface ITsImpact {
42
+ /** Total reference sites (excluding the declaration itself). */
43
+ total: number;
44
+ /** Number of distinct files that reference the symbol. */
45
+ fileCount: number;
46
+ /** Per-file reference lines, most-referenced file first. */
47
+ files: ITsImpactFile[];
48
+ }
49
+
50
+ /** A 360° view of a symbol: its type, where it's defined, and what references it. */
51
+ export interface ITsContext {
52
+ /** Quick-info type string (what `typeAt` returns). */
53
+ type: string;
54
+ definition: ITsLocation[];
55
+ references: ITsLocation[];
56
+ }
@@ -0,0 +1,500 @@
1
+ import { join, isAbsolute } from "node:path";
2
+ import ts from "typescript";
3
+ import type {
4
+ ITsDiagnostic,
5
+ ITsFix,
6
+ ITsLocation,
7
+ ITsSymbol,
8
+ ITsImpact,
9
+ ITsContext,
10
+ } from "./lsp.types";
11
+
12
+ /**
13
+ * Quick-fixes safe to apply automatically — they align with our strict gate.
14
+ * Deliberately EXCLUDES `addNonNullAssertion` and anything inserting `as`/`!`,
15
+ * which the constitution bans. The gate re-validates regardless, so a bad fix
16
+ * can't ship, but we don't want the harness fighting its own rules.
17
+ */
18
+ const SAFE_FIXES = new Set([
19
+ "import",
20
+ "fixMissingImport",
21
+ "unusedIdentifier",
22
+ "addMissingAwait",
23
+ "fixOverrideModifier",
24
+ "fixMissingMember",
25
+ "fixMissingProperties",
26
+ "fixUnreachableCode",
27
+ "fixAddMissingConstraint",
28
+ ]);
29
+
30
+ /**
31
+ * Thin wrapper over the TypeScript LanguageService — the engine behind tsserver,
32
+ * run in-process. Gives semantic diagnostics, TypeScript's own quick-fixes,
33
+ * semantic rename, and type-at-cursor, scoped to one project's `tsconfig`. The
34
+ * `tsc -p` gate stays the authority; this is for fixes / speed / semantics.
35
+ */
36
+ export class TsService {
37
+ private readonly service: ts.LanguageService;
38
+ private readonly versions = new Map<string, number>();
39
+ private readonly files: string[];
40
+
41
+ constructor(private readonly dir: string) {
42
+ const configPath = join(dir, "tsconfig.json");
43
+ const read = ts.readConfigFile(configPath, (p) => ts.sys.readFile(p));
44
+ const parsed = ts.parseJsonConfigFileContent(
45
+ read.config ?? {},
46
+ ts.sys,
47
+ dir
48
+ );
49
+
50
+ this.files = [...parsed.fileNames];
51
+
52
+ const host: ts.LanguageServiceHost = {
53
+ getScriptFileNames: () => this.files,
54
+ getScriptVersion: (f) => String(this.versions.get(f) ?? 0),
55
+ getScriptSnapshot: (f) => {
56
+ const text = ts.sys.readFile(f);
57
+
58
+ return text === undefined
59
+ ? undefined
60
+ : ts.ScriptSnapshot.fromString(text);
61
+ },
62
+ getCurrentDirectory: () => dir,
63
+ getCompilationSettings: () => parsed.options,
64
+ getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
65
+ fileExists: (p) => ts.sys.fileExists(p),
66
+ readFile: (p) => ts.sys.readFile(p),
67
+ readDirectory: (p, ext, exclude, include, depth) =>
68
+ ts.sys.readDirectory(p, ext, exclude, include, depth),
69
+ directoryExists: (p) => ts.sys.directoryExists(p),
70
+ getDirectories: (p) => ts.sys.getDirectories(p),
71
+ };
72
+
73
+ this.service = ts.createLanguageService(host, ts.createDocumentRegistry());
74
+ }
75
+
76
+ /** Re-read a file the harness changed on disk (bump its version, track new files). */
77
+ refresh(file: string): void {
78
+ const abs = this.toAbs(file);
79
+
80
+ this.versions.set(abs, (this.versions.get(abs) ?? 0) + 1);
81
+
82
+ if (!this.files.includes(abs)) {
83
+ this.files.push(abs);
84
+ }
85
+ }
86
+
87
+ diagnostics(file: string): ITsDiagnostic[] {
88
+ const abs = this.toAbs(file);
89
+ const raw = [
90
+ ...this.service.getSyntacticDiagnostics(abs),
91
+ ...this.service.getSemanticDiagnostics(abs),
92
+ ];
93
+
94
+ const out: ITsDiagnostic[] = [];
95
+
96
+ for (const d of raw) {
97
+ if (d.start === undefined || d.length === undefined) {
98
+ continue;
99
+ }
100
+
101
+ out.push({
102
+ code: d.code,
103
+ message: ts.flattenDiagnosticMessageText(d.messageText, "\n"),
104
+ file: abs,
105
+ start: d.start,
106
+ length: d.length,
107
+ });
108
+ }
109
+
110
+ return out;
111
+ }
112
+
113
+ quickFixes(file: string): ITsFix[] {
114
+ const abs = this.toAbs(file);
115
+ const prefs: ts.UserPreferences = {};
116
+ const fmt = ts.getDefaultFormatCodeSettings("\n");
117
+ const fixes: ITsFix[] = [];
118
+
119
+ for (const d of this.diagnostics(file)) {
120
+ const actions = this.service.getCodeFixesAtPosition(
121
+ abs,
122
+ d.start,
123
+ d.start + d.length,
124
+ [d.code],
125
+ fmt,
126
+ prefs
127
+ );
128
+
129
+ for (const a of actions) {
130
+ fixes.push({ description: a.description, changes: a.changes });
131
+ }
132
+ }
133
+
134
+ return fixes;
135
+ }
136
+
137
+ /**
138
+ * Apply TypeScript's own safe quick-fixes to `file` until none remain — the
139
+ * deterministic "eslint --fix, but for TS" step. Returns how many were applied.
140
+ * Iterative (apply one, re-ground) so cascading fixes resolve cleanly.
141
+ */
142
+ fixAll(file: string, maxPasses = 20): number {
143
+ let applied = 0;
144
+
145
+ for (let pass = 0; pass < maxPasses; pass += 1) {
146
+ const fix = this.firstSafeFix(file);
147
+
148
+ if (fix === undefined) {
149
+ break;
150
+ }
151
+
152
+ this.applyChanges(fix.changes);
153
+ applied += 1;
154
+ }
155
+
156
+ return applied;
157
+ }
158
+
159
+ private firstSafeFix(file: string): ts.CodeFixAction | undefined {
160
+ const abs = this.toAbs(file);
161
+ const fmt = ts.getDefaultFormatCodeSettings("\n");
162
+
163
+ for (const d of this.diagnostics(file)) {
164
+ const actions = this.service.getCodeFixesAtPosition(
165
+ abs,
166
+ d.start,
167
+ d.start + d.length,
168
+ [d.code],
169
+ fmt,
170
+ {}
171
+ );
172
+
173
+ for (const a of actions) {
174
+ if (SAFE_FIXES.has(a.fixName)) {
175
+ return a;
176
+ }
177
+ }
178
+ }
179
+
180
+ return undefined;
181
+ }
182
+
183
+ private applyChanges(changes: readonly ts.FileTextChanges[]): void {
184
+ for (const fc of changes) {
185
+ const original = ts.sys.readFile(fc.fileName);
186
+
187
+ if (original === undefined) {
188
+ continue;
189
+ }
190
+
191
+ // Apply edits back-to-front so earlier spans keep their offsets.
192
+ const sorted = [...fc.textChanges].sort(
193
+ (a, b) => b.span.start - a.span.start
194
+ );
195
+ let text = original;
196
+
197
+ for (const tc of sorted) {
198
+ text =
199
+ text.slice(0, tc.span.start) +
200
+ tc.newText +
201
+ text.slice(tc.span.start + tc.span.length);
202
+ }
203
+
204
+ ts.sys.writeFile(fc.fileName, text);
205
+ this.refresh(fc.fileName);
206
+ }
207
+ }
208
+
209
+ /** Find every reference to rename (Slice 3 adds the new name + applies them). */
210
+ renameLocations(
211
+ file: string,
212
+ position: number
213
+ ): readonly ts.RenameLocation[] | undefined {
214
+ return this.service.findRenameLocations(
215
+ this.toAbs(file),
216
+ position,
217
+ false,
218
+ false,
219
+ {}
220
+ );
221
+ }
222
+
223
+ typeAt(file: string, position: number): string {
224
+ const info = this.service.getQuickInfoAtPosition(
225
+ this.toAbs(file),
226
+ position
227
+ );
228
+
229
+ if (info === undefined) {
230
+ return "";
231
+ }
232
+
233
+ return ts.displayPartsToString(info.displayParts);
234
+ }
235
+
236
+ /** All references to the symbol at `position` (across the project). */
237
+ references(file: string, position: number): ITsLocation[] {
238
+ const refs = this.service.getReferencesAtPosition(
239
+ this.toAbs(file),
240
+ position
241
+ );
242
+
243
+ return (refs ?? []).map((r) => ({
244
+ file: r.fileName,
245
+ line: this.lineOf(r.fileName, r.textSpan.start),
246
+ start: r.textSpan.start,
247
+ }));
248
+ }
249
+
250
+ /** Definition location(s) of the symbol at `position`. */
251
+ definition(file: string, position: number): ITsLocation[] {
252
+ const defs = this.service.getDefinitionAtPosition(
253
+ this.toAbs(file),
254
+ position
255
+ );
256
+
257
+ return (defs ?? []).map((d) => ({
258
+ file: d.fileName,
259
+ line: this.lineOf(d.fileName, d.textSpan.start),
260
+ start: d.textSpan.start,
261
+ }));
262
+ }
263
+
264
+ /**
265
+ * BLAST RADIUS of the symbol at `position`: which files/lines reference it,
266
+ * EXCLUDING its own declaration site. Type-exact (the TS LanguageService resolves
267
+ * it), so it can drive a deliberate cross-file edit — "you changed X, it's used
268
+ * at A:12, B:40" — instead of discovering breakage at the gate. Files are ordered
269
+ * most-referenced first.
270
+ */
271
+ impact(file: string, position: number): ITsImpact {
272
+ const abs = this.toAbs(file);
273
+ const refs = this.service.getReferencesAtPosition(abs, position) ?? [];
274
+ const byFile = new Map<string, number[]>();
275
+
276
+ for (const r of refs) {
277
+ // Drop the declaration occurrence itself — blast radius is the DEPENDANTS.
278
+ if (r.fileName === abs && r.textSpan.start === position) {
279
+ continue;
280
+ }
281
+
282
+ const lines = byFile.get(r.fileName) ?? [];
283
+
284
+ lines.push(this.lineOf(r.fileName, r.textSpan.start));
285
+ byFile.set(r.fileName, lines);
286
+ }
287
+
288
+ const files = [...byFile.entries()]
289
+ .map(([f, lines]) => ({
290
+ file: f,
291
+ lines: [...lines].sort((a, b) => a - b),
292
+ }))
293
+ .sort((a, b) => b.lines.length - a.lines.length);
294
+ const total = files.reduce((n, f) => n + f.lines.length, 0);
295
+
296
+ return { total, fileCount: files.length, files };
297
+ }
298
+
299
+ /** A 360° view of the symbol at `position`: its type, definition site(s), and
300
+ * every reference — the pieces a model needs to reason about it in one call. */
301
+ context(file: string, position: number): ITsContext {
302
+ return {
303
+ type: this.typeAt(file, position),
304
+ definition: this.definition(file, position),
305
+ references: this.references(file, position),
306
+ };
307
+ }
308
+
309
+ /** Start offsets of `file`'s top-level EXPORTED declarations (the symbols other
310
+ * files can depend on) — the entry points for a file-level blast-radius check. */
311
+ private exportedPositions(abs: string): number[] {
312
+ const sf = this.service.getProgram()?.getSourceFile(abs);
313
+
314
+ if (sf === undefined) {
315
+ return [];
316
+ }
317
+
318
+ const out: number[] = [];
319
+
320
+ sf.forEachChild((node) => {
321
+ const mods = ts.canHaveModifiers(node)
322
+ ? ts.getModifiers(node)
323
+ : undefined;
324
+ const exported =
325
+ mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
326
+
327
+ if (!exported) {
328
+ return;
329
+ }
330
+
331
+ if (ts.isVariableStatement(node)) {
332
+ for (const d of node.declarationList.declarations) {
333
+ if (ts.isIdentifier(d.name)) {
334
+ out.push(d.name.getStart(sf));
335
+ }
336
+ }
337
+ } else if (
338
+ (ts.isFunctionDeclaration(node) ||
339
+ ts.isClassDeclaration(node) ||
340
+ ts.isInterfaceDeclaration(node) ||
341
+ ts.isTypeAliasDeclaration(node) ||
342
+ ts.isEnumDeclaration(node)) &&
343
+ node.name !== undefined
344
+ ) {
345
+ out.push(node.name.getStart(sf));
346
+ }
347
+ });
348
+
349
+ return out;
350
+ }
351
+
352
+ /**
353
+ * CROSS-FILE BLAST RADIUS of an edit to `file`: the files that depend on its
354
+ * exports AND now have type errors — i.e. what this edit BROKE downstream. This
355
+ * is the write-guard extended across the import graph: change a signature and the
356
+ * harness can say "you also broke deals.service.ts:12" THIS turn, instead of the
357
+ * model discovering it at the gate. `ignoreCodes` drops expected mid-build noise
358
+ * (e.g. 2307 cannot-find-module). Naturally cheap for a fresh file (no dependants
359
+ * yet → no references → empty).
360
+ */
361
+ dependantErrors(
362
+ file: string,
363
+ ignoreCodes: ReadonlySet<number> = new Set()
364
+ ): { file: string; errors: ITsDiagnostic[] }[] {
365
+ const abs = this.toAbs(file);
366
+ const deps = new Set<string>();
367
+
368
+ for (const pos of this.exportedPositions(abs)) {
369
+ for (const f of this.impact(file, pos).files) {
370
+ if (f.file !== abs) {
371
+ deps.add(f.file);
372
+ }
373
+ }
374
+ }
375
+
376
+ const out: { file: string; errors: ITsDiagnostic[] }[] = [];
377
+
378
+ for (const dep of deps) {
379
+ this.refresh(dep);
380
+
381
+ const errors = this.diagnostics(dep).filter(
382
+ (d) => !ignoreCodes.has(d.code)
383
+ );
384
+
385
+ if (errors.length > 0) {
386
+ out.push({ file: dep, errors });
387
+ }
388
+ }
389
+
390
+ return out;
391
+ }
392
+
393
+ /** Workspace symbol search by name (navigate-to) — find a symbol/file without
394
+ * knowing the path. */
395
+ symbols(query: string, max = 50): ITsSymbol[] {
396
+ return this.service.getNavigateToItems(query, max).map((i) => ({
397
+ name: i.name,
398
+ kind: i.kind,
399
+ file: i.fileName,
400
+ line: this.lineOf(i.fileName, i.textSpan.start),
401
+ }));
402
+ }
403
+
404
+ /**
405
+ * Semantic rename across ALL references, applied to disk. Returns the number
406
+ * of locations changed, or null if the symbol can't be renamed. Callers must
407
+ * enforce scope (a rename can touch read-only files — the loop checks).
408
+ */
409
+ rename(file: string, position: number, newName: string): number | null {
410
+ const locs = this.service.findRenameLocations(
411
+ this.toAbs(file),
412
+ position,
413
+ false,
414
+ false,
415
+ {}
416
+ );
417
+
418
+ if (locs === undefined || locs.length === 0) {
419
+ return null;
420
+ }
421
+
422
+ const byFile = new Map<string, ts.TextChange[]>();
423
+
424
+ for (const loc of locs) {
425
+ const arr = byFile.get(loc.fileName) ?? [];
426
+
427
+ arr.push({ span: loc.textSpan, newText: newName });
428
+ byFile.set(loc.fileName, arr);
429
+ }
430
+
431
+ const changes: ts.FileTextChanges[] = [...byFile].map(
432
+ ([fileName, textChanges]) => ({ fileName, textChanges, isNewFile: false })
433
+ );
434
+
435
+ this.applyChanges(changes);
436
+
437
+ return locs.length;
438
+ }
439
+
440
+ /** Which files a rename at `position` would touch (for scope-checking BEFORE
441
+ * applying). Absolute paths. */
442
+ renameTargets(file: string, position: number): string[] {
443
+ const locs = this.service.findRenameLocations(
444
+ this.toAbs(file),
445
+ position,
446
+ false,
447
+ false,
448
+ {}
449
+ );
450
+
451
+ return [...new Set((locs ?? []).map((l) => l.fileName))];
452
+ }
453
+
454
+ /** Organize imports (dedupe/sort/drop unused) for one file. Returns edits made. */
455
+ organizeImports(file: string): number {
456
+ const changes = this.service.organizeImports(
457
+ { type: "file", fileName: this.toAbs(file) },
458
+ ts.getDefaultFormatCodeSettings("\n"),
459
+ {}
460
+ );
461
+
462
+ if (changes.length === 0) {
463
+ return 0;
464
+ }
465
+
466
+ this.applyChanges(changes);
467
+
468
+ return changes.reduce((n, c) => n + c.textChanges.length, 0);
469
+ }
470
+
471
+ /**
472
+ * Ergonomic position lookup: the model addresses symbols by NAME, not byte
473
+ * offset. Returns the offset of the first word-boundary occurrence of `symbol`
474
+ * in `file`, or undefined. Crude but practical (rename re-validates via the
475
+ * gate; references/typeAt are read-only).
476
+ */
477
+ positionOfSymbol(file: string, symbol: string): number | undefined {
478
+ const text = ts.sys.readFile(this.toAbs(file));
479
+
480
+ if (text === undefined) {
481
+ return undefined;
482
+ }
483
+
484
+ const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
485
+ const match = new RegExp(`\\b${escaped}\\b`).exec(text);
486
+
487
+ return match === null ? undefined : match.index;
488
+ }
489
+
490
+ /** 1-based line number of a byte offset in a file (for readable tool output). */
491
+ private lineOf(file: string, position: number): number {
492
+ const text = ts.sys.readFile(file) ?? "";
493
+
494
+ return text.slice(0, position).split("\n").length;
495
+ }
496
+
497
+ private toAbs(file: string): string {
498
+ return isAbsolute(file) ? file : join(this.dir, file);
499
+ }
500
+ }