@thinkingsage/kanon 0.8.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 (199) hide show
  1. package/CHANGELOG.md +410 -0
  2. package/LICENSE +21 -0
  3. package/README.md +168 -0
  4. package/bridge/mcp-server.cjs +14171 -0
  5. package/package.json +98 -0
  6. package/src/adapters/capabilities.ts +178 -0
  7. package/src/adapters/claude-code.ts +110 -0
  8. package/src/adapters/cline.ts +98 -0
  9. package/src/adapters/codex.ts +173 -0
  10. package/src/adapters/copilot.ts +106 -0
  11. package/src/adapters/cursor.ts +97 -0
  12. package/src/adapters/degradation.ts +95 -0
  13. package/src/adapters/index.ts +324 -0
  14. package/src/adapters/kiro-frontmatter.ts +139 -0
  15. package/src/adapters/kiro-inclusion.ts +86 -0
  16. package/src/adapters/kiro.ts +412 -0
  17. package/src/adapters/qdeveloper.ts +115 -0
  18. package/src/adapters/types.ts +81 -0
  19. package/src/adapters/windsurf.ts +96 -0
  20. package/src/admin.ts +283 -0
  21. package/src/asset-conventions.ts +118 -0
  22. package/src/attribution-backfill.ts +319 -0
  23. package/src/attribution-report.ts +95 -0
  24. package/src/attribution.ts +239 -0
  25. package/src/backends/github.ts +194 -0
  26. package/src/backends/http.ts +122 -0
  27. package/src/backends/index.ts +39 -0
  28. package/src/backends/local.ts +47 -0
  29. package/src/backends/s3.ts +157 -0
  30. package/src/backends/types.ts +59 -0
  31. package/src/base-cache.ts +270 -0
  32. package/src/browse-ui.ts +3754 -0
  33. package/src/browse.ts +1038 -0
  34. package/src/build.ts +1108 -0
  35. package/src/catalog.ts +204 -0
  36. package/src/cli-deprecated.ts +29 -0
  37. package/src/cli.ts +773 -0
  38. package/src/collection-admin.ts +287 -0
  39. package/src/collection-builder.ts +464 -0
  40. package/src/collections.ts +116 -0
  41. package/src/compatibility.ts +105 -0
  42. package/src/config.ts +743 -0
  43. package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
  44. package/src/eval.ts +1169 -0
  45. package/src/file-writer.ts +61 -0
  46. package/src/format-registry.ts +141 -0
  47. package/src/guild/auto-updater.ts +163 -0
  48. package/src/guild/backend-resolver.ts +49 -0
  49. package/src/guild/cli.ts +592 -0
  50. package/src/guild/collection-expander.ts +47 -0
  51. package/src/guild/global-cache.ts +247 -0
  52. package/src/guild/hook-generator.ts +100 -0
  53. package/src/guild/manifest.ts +154 -0
  54. package/src/guild/path-utils.ts +12 -0
  55. package/src/guild/sync.ts +622 -0
  56. package/src/guild/version-resolver.ts +42 -0
  57. package/src/help/metadata.ts +445 -0
  58. package/src/help/renderer.ts +265 -0
  59. package/src/help/typo-suggester.ts +25 -0
  60. package/src/hooks/expression.ts +493 -0
  61. package/src/hooks/pipeline.ts +141 -0
  62. package/src/import.ts +773 -0
  63. package/src/importers/claude-code.ts +134 -0
  64. package/src/importers/cline.ts +103 -0
  65. package/src/importers/codex.ts +140 -0
  66. package/src/importers/copilot.ts +103 -0
  67. package/src/importers/cursor.ts +105 -0
  68. package/src/importers/index.ts +390 -0
  69. package/src/importers/kiro.ts +110 -0
  70. package/src/importers/qdeveloper.ts +103 -0
  71. package/src/importers/types.ts +54 -0
  72. package/src/importers/windsurf.ts +104 -0
  73. package/src/install.ts +1005 -0
  74. package/src/manifest-admin.ts +306 -0
  75. package/src/mcp-bridge.ts +240 -0
  76. package/src/mutation/delta.ts +50 -0
  77. package/src/mutation/history.ts +66 -0
  78. package/src/mutation/operators.ts +524 -0
  79. package/src/mutation/runner.ts +332 -0
  80. package/src/new.ts +106 -0
  81. package/src/outcomes/collision.ts +127 -0
  82. package/src/outcomes/normalize.ts +208 -0
  83. package/src/outcomes/registry.ts +173 -0
  84. package/src/parser.ts +446 -0
  85. package/src/provenance-backfill-cli.ts +319 -0
  86. package/src/provenance-backfill.ts +520 -0
  87. package/src/publish.ts +354 -0
  88. package/src/reconcile-orchestrator.ts +502 -0
  89. package/src/reconcile-report-renderer.ts +176 -0
  90. package/src/resolve-body.ts +15 -0
  91. package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
  92. package/src/rosetta/builtins/contracts.ts +1033 -0
  93. package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
  94. package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
  95. package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
  96. package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
  97. package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
  98. package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
  99. package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
  100. package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
  101. package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
  102. package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
  103. package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
  104. package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
  105. package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
  106. package/src/rosetta/builtins/sources/cline-native.ts +176 -0
  107. package/src/rosetta/builtins/sources/codex-native.ts +343 -0
  108. package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
  109. package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
  110. package/src/rosetta/builtins/sources/index.ts +95 -0
  111. package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
  112. package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
  113. package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
  114. package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
  115. package/src/rosetta/builtins/sources/superpowers.ts +240 -0
  116. package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
  117. package/src/rosetta/builtins/targets/claude-code.ts +181 -0
  118. package/src/rosetta/builtins/targets/cline.ts +87 -0
  119. package/src/rosetta/builtins/targets/codex.ts +226 -0
  120. package/src/rosetta/builtins/targets/copilot.ts +103 -0
  121. package/src/rosetta/builtins/targets/cursor.ts +87 -0
  122. package/src/rosetta/builtins/targets/index.ts +60 -0
  123. package/src/rosetta/builtins/targets/kiro.ts +278 -0
  124. package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
  125. package/src/rosetta/builtins/targets/windsurf.ts +87 -0
  126. package/src/rosetta/canonical.ts +729 -0
  127. package/src/rosetta/compatibility.ts +432 -0
  128. package/src/rosetta/contracts.ts +329 -0
  129. package/src/rosetta/detector.ts +724 -0
  130. package/src/rosetta/diagnostics.ts +630 -0
  131. package/src/rosetta/engine-bootstrap.ts +103 -0
  132. package/src/rosetta/engine.ts +744 -0
  133. package/src/rosetta/index.ts +381 -0
  134. package/src/rosetta/inspection.ts +530 -0
  135. package/src/rosetta/plan.ts +448 -0
  136. package/src/rosetta/provenance-digest.ts +369 -0
  137. package/src/rosetta/reconcile.ts +812 -0
  138. package/src/rosetta/redaction.ts +467 -0
  139. package/src/rosetta/registry.ts +712 -0
  140. package/src/rosetta/renderers.ts +571 -0
  141. package/src/rosetta/request-guard.ts +335 -0
  142. package/src/rosetta/resolution.ts +419 -0
  143. package/src/rosetta/source-accounting.ts +233 -0
  144. package/src/rosetta/templates.ts +129 -0
  145. package/src/rosetta-cli.ts +717 -0
  146. package/src/rosetta-docs-generator.ts +793 -0
  147. package/src/rosetta-profiles-cli.ts +367 -0
  148. package/src/schemas.ts +1712 -0
  149. package/src/spec-coordination.ts +1141 -0
  150. package/src/temper.ts +747 -0
  151. package/src/template-bundle-loader.ts +312 -0
  152. package/src/template-engine.ts +53 -0
  153. package/src/translation-application-policy.ts +496 -0
  154. package/src/translation-orchestrator.ts +1013 -0
  155. package/src/translation-plan-applier.ts +473 -0
  156. package/src/tutorial.ts +305 -0
  157. package/src/validate.ts +1093 -0
  158. package/src/versioning.ts +553 -0
  159. package/src/wizard.ts +660 -0
  160. package/src/workspace.ts +237 -0
  161. package/templates/eval-contexts/claude-code.md.njk +6 -0
  162. package/templates/eval-contexts/cline.md.njk +6 -0
  163. package/templates/eval-contexts/copilot.md.njk +6 -0
  164. package/templates/eval-contexts/cursor.md.njk +6 -0
  165. package/templates/eval-contexts/kiro.md.njk +10 -0
  166. package/templates/eval-contexts/qdeveloper.md.njk +6 -0
  167. package/templates/eval-contexts/windsurf.md.njk +6 -0
  168. package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
  169. package/templates/harness-adapters/_base/base.md.njk +16 -0
  170. package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
  171. package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
  172. package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
  173. package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
  174. package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
  175. package/templates/harness-adapters/cline/hook.sh.njk +4 -0
  176. package/templates/harness-adapters/cline/mcp.json.njk +1 -0
  177. package/templates/harness-adapters/cline/rule.md.njk +1 -0
  178. package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
  179. package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
  180. package/templates/harness-adapters/codex/skill.md.njk +27 -0
  181. package/templates/harness-adapters/copilot/agents.md.njk +1 -0
  182. package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
  183. package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
  184. package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
  185. package/templates/harness-adapters/cursor/rule.md.njk +6 -0
  186. package/templates/harness-adapters/kiro/hook.json.njk +1 -0
  187. package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
  188. package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
  189. package/templates/harness-adapters/kiro/power.md.njk +12 -0
  190. package/templates/harness-adapters/kiro/steering.md.njk +16 -0
  191. package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
  192. package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
  193. package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
  194. package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
  195. package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
  196. package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
  197. package/templates/knowledge/hooks.yaml.njk +4 -0
  198. package/templates/knowledge/knowledge.md.njk +53 -0
  199. package/templates/knowledge/mcp-servers.yaml.njk +2 -0
@@ -0,0 +1,524 @@
1
+ /**
2
+ * Mutation testing — pure mutant generation (Req 5.3).
3
+ *
4
+ * `generateMutants` produces a bounded, deterministic set of single-site
5
+ * mutations for one source file. It is pure (no I/O, no shared state): the same
6
+ * `(filePath, source, operators, cap)` inputs always yield the same `Mutant[]`,
7
+ * and every mutant's `mutatedSource` differs from the original at exactly one
8
+ * site.
9
+ *
10
+ * Operators scan a *masked* view of the source — string-literal and comment
11
+ * characters are blanked to spaces while indices and newlines are preserved — so
12
+ * that relational and arithmetic operators are never matched inside strings or
13
+ * comments. This keeps generation deterministic and well-targeted without
14
+ * pulling in a full TypeScript parser.
15
+ *
16
+ * See ADR-0042 for the architectural rationale.
17
+ */
18
+
19
+ import type { MutationOperator } from "../config";
20
+
21
+ /** A single-site mutation of a source file. */
22
+ export interface Mutant {
23
+ /** Path of the file the mutation applies to. */
24
+ filePath: string;
25
+ /** 1-based line number of the mutation site. */
26
+ line: number;
27
+ /** The operator that produced this mutant. */
28
+ operator: MutationOperator;
29
+ /** The original source text at the mutation site (line-granular). */
30
+ originalSnippet: string;
31
+ /** The mutated source text at the mutation site (line-granular). */
32
+ mutatedSnippet: string;
33
+ /** The full file content with the single mutation applied. */
34
+ mutatedSource: string;
35
+ }
36
+
37
+ /** Default maximum number of mutants generated per file (Req 5.3). */
38
+ export const DEFAULT_MUTANT_CAP = 50;
39
+
40
+ /** Sentinel used by the string-literal operator. */
41
+ const STRING_SENTINEL = "__MUTATED__";
42
+
43
+ /** A located, single-site change before it is materialized into a `Mutant`. */
44
+ interface Candidate {
45
+ /** Inclusive start index into the original source. */
46
+ start: number;
47
+ /** Exclusive end index into the original source (== start for insertions). */
48
+ end: number;
49
+ /** Text that replaces `source.slice(start, end)`. */
50
+ replacement: string;
51
+ operator: MutationOperator;
52
+ }
53
+
54
+ /**
55
+ * Generate mutants for one source file. Pure and deterministic. Produces at most
56
+ * `cap` mutants (default {@link DEFAULT_MUTANT_CAP}). Operators are applied in the
57
+ * order given by `operators`, and candidate sites within each operator are
58
+ * emitted in source order, giving a stable, reproducible ordering.
59
+ */
60
+ export function generateMutants(
61
+ filePath: string,
62
+ source: string,
63
+ operators: MutationOperator[],
64
+ cap: number = DEFAULT_MUTANT_CAP,
65
+ ): Mutant[] {
66
+ if (cap <= 0 || source.length === 0) return [];
67
+
68
+ const scan = scanSource(source);
69
+ const lineStarts = computeLineStarts(source);
70
+
71
+ const candidates: Candidate[] = [];
72
+ for (const operator of operators) {
73
+ switch (operator) {
74
+ case "statement-deletion":
75
+ candidates.push(
76
+ ...findStatementDeletions(source, scan.masked, lineStarts),
77
+ );
78
+ break;
79
+ case "conditional-boundary":
80
+ candidates.push(...findConditionalBoundaries(scan.masked));
81
+ break;
82
+ case "arithmetic-replacement":
83
+ candidates.push(...findArithmeticReplacements(scan.masked));
84
+ break;
85
+ case "string-literal":
86
+ candidates.push(...findStringLiterals(source, scan.strings));
87
+ break;
88
+ case "return-value":
89
+ candidates.push(...findReturnValues(source, scan.masked));
90
+ break;
91
+ }
92
+ }
93
+
94
+ const mutants: Mutant[] = [];
95
+ for (const candidate of candidates) {
96
+ if (mutants.length >= cap) break;
97
+ const mutatedSource =
98
+ source.slice(0, candidate.start) +
99
+ candidate.replacement +
100
+ source.slice(candidate.end);
101
+ // Guarantee a real, single-site change.
102
+ if (mutatedSource === source) continue;
103
+ const { original, mutated } = snippetForRange(
104
+ source,
105
+ lineStarts,
106
+ candidate.start,
107
+ candidate.end,
108
+ candidate.replacement,
109
+ );
110
+ mutants.push({
111
+ filePath,
112
+ line: indexToLine(lineStarts, candidate.start),
113
+ operator: candidate.operator,
114
+ originalSnippet: original,
115
+ mutatedSnippet: mutated,
116
+ mutatedSource,
117
+ });
118
+ }
119
+ return mutants;
120
+ }
121
+
122
+ // --- Operators ---------------------------------------------------------------
123
+
124
+ const BOUNDARY_MAP: Record<string, string> = {
125
+ "<": "<=",
126
+ "<=": "<",
127
+ ">": ">=",
128
+ ">=": ">",
129
+ };
130
+
131
+ /** Conditional-boundary: `<` ↔ `<=`, `>` ↔ `>=` on space-delimited operators. */
132
+ function findConditionalBoundaries(masked: string): Candidate[] {
133
+ const candidates: Candidate[] = [];
134
+ const re = /(?<= )(<=|>=|<|>)(?= )/g;
135
+ let match: RegExpExecArray | null;
136
+ // biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
137
+ while ((match = re.exec(masked)) !== null) {
138
+ const op = match[1];
139
+ candidates.push({
140
+ start: match.index,
141
+ end: match.index + op.length,
142
+ replacement: BOUNDARY_MAP[op],
143
+ operator: "conditional-boundary",
144
+ });
145
+ }
146
+ return candidates;
147
+ }
148
+
149
+ const ARITHMETIC_MAP: Record<string, string> = {
150
+ "+": "-",
151
+ "-": "+",
152
+ "*": "/",
153
+ "/": "*",
154
+ };
155
+
156
+ /** Arithmetic-replacement: `+` ↔ `-`, `*` ↔ `/` on space-delimited operators. */
157
+ function findArithmeticReplacements(masked: string): Candidate[] {
158
+ const candidates: Candidate[] = [];
159
+ const re = /(?<= )([+\-*/])(?= )/g;
160
+ let match: RegExpExecArray | null;
161
+ // biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
162
+ while ((match = re.exec(masked)) !== null) {
163
+ const op = match[1];
164
+ candidates.push({
165
+ start: match.index,
166
+ end: match.index + 1,
167
+ replacement: ARITHMETIC_MAP[op],
168
+ operator: "arithmetic-replacement",
169
+ });
170
+ }
171
+ return candidates;
172
+ }
173
+
174
+ /** String-literal: replace a literal's content with a distinct sentinel. */
175
+ function findStringLiterals(
176
+ source: string,
177
+ strings: StringSpan[],
178
+ ): Candidate[] {
179
+ const candidates: Candidate[] = [];
180
+ for (const span of strings) {
181
+ const contentStart = span.start + 1;
182
+ const contentEnd = span.end; // index of the closing quote
183
+ if (contentEnd < contentStart) continue;
184
+ const content = source.slice(contentStart, contentEnd);
185
+ // Choose a replacement guaranteed to differ from the original content.
186
+ const replacement = content === STRING_SENTINEL ? "" : STRING_SENTINEL;
187
+ candidates.push({
188
+ start: contentStart,
189
+ end: contentEnd,
190
+ replacement,
191
+ operator: "string-literal",
192
+ });
193
+ }
194
+ return candidates;
195
+ }
196
+
197
+ /**
198
+ * Return-value: flip boolean returns, swap null/undefined, otherwise replace the
199
+ * returned expression with `null`.
200
+ */
201
+ function findReturnValues(source: string, masked: string): Candidate[] {
202
+ const candidates: Candidate[] = [];
203
+ const re = /\breturn\b/g;
204
+ let match: RegExpExecArray | null;
205
+ // biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
206
+ while ((match = re.exec(masked)) !== null) {
207
+ const afterKeyword = match.index + "return".length;
208
+ const semicolon = findTopLevelSemicolon(masked, afterKeyword);
209
+ if (semicolon === -1) continue;
210
+
211
+ // Trim surrounding whitespace to locate the expression span.
212
+ let exprStart = afterKeyword;
213
+ while (exprStart < semicolon && /\s/.test(source[exprStart])) exprStart++;
214
+ let exprEnd = semicolon;
215
+ while (exprEnd > exprStart && /\s/.test(source[exprEnd - 1])) exprEnd--;
216
+ if (exprEnd <= exprStart) continue; // bare `return;`
217
+
218
+ const expr = source.slice(exprStart, exprEnd);
219
+ const replacement = mutateReturnExpression(expr);
220
+ if (replacement === expr) continue;
221
+ candidates.push({
222
+ start: exprStart,
223
+ end: exprEnd,
224
+ replacement,
225
+ operator: "return-value",
226
+ });
227
+ }
228
+ return candidates;
229
+ }
230
+
231
+ function mutateReturnExpression(expr: string): string {
232
+ switch (expr) {
233
+ case "true":
234
+ return "false";
235
+ case "false":
236
+ return "true";
237
+ case "null":
238
+ return "undefined";
239
+ case "undefined":
240
+ return "null";
241
+ default:
242
+ return "null";
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Statement-deletion: comment out a complete single-line statement by inserting
248
+ * `// ` at its first non-whitespace character.
249
+ */
250
+ function findStatementDeletions(
251
+ source: string,
252
+ masked: string,
253
+ lineStarts: number[],
254
+ ): Candidate[] {
255
+ const candidates: Candidate[] = [];
256
+ for (let k = 0; k < lineStarts.length; k++) {
257
+ const ls = lineStarts[k];
258
+ const le =
259
+ k + 1 < lineStarts.length ? lineStarts[k + 1] - 1 : source.length;
260
+ const lineText = source.slice(ls, le);
261
+ const trimmed = lineText.trim();
262
+ if (!isDeletableStatement(trimmed)) continue;
263
+ // Require balanced brackets on the (masked) line so we never break a
264
+ // multi-line statement by commenting just one of its lines.
265
+ if (!isBracketBalanced(masked.slice(ls, le))) continue;
266
+
267
+ const lead = lineText.length - lineText.trimStart().length;
268
+ const start = ls + lead;
269
+ candidates.push({
270
+ start,
271
+ end: start, // pure insertion
272
+ replacement: "// ",
273
+ operator: "statement-deletion",
274
+ });
275
+ }
276
+ return candidates;
277
+ }
278
+
279
+ /** Skip declarations/structure whose deletion is never a meaningful mutant. */
280
+ function isDeletableStatement(trimmed: string): boolean {
281
+ if (trimmed.length <= 1) return false;
282
+ if (!trimmed.endsWith(";")) return false;
283
+ const skipPrefixes = [
284
+ "//",
285
+ "/*",
286
+ "*",
287
+ "}",
288
+ "{",
289
+ ")",
290
+ "import ",
291
+ "import(",
292
+ "export type",
293
+ "export {",
294
+ "export *",
295
+ "type ",
296
+ "interface ",
297
+ ];
298
+ for (const prefix of skipPrefixes) {
299
+ if (trimmed.startsWith(prefix)) return false;
300
+ }
301
+ return true;
302
+ }
303
+
304
+ function isBracketBalanced(text: string): boolean {
305
+ let round = 0;
306
+ let curly = 0;
307
+ let square = 0;
308
+ for (const ch of text) {
309
+ switch (ch) {
310
+ case "(":
311
+ round++;
312
+ break;
313
+ case ")":
314
+ round--;
315
+ break;
316
+ case "{":
317
+ curly++;
318
+ break;
319
+ case "}":
320
+ curly--;
321
+ break;
322
+ case "[":
323
+ square++;
324
+ break;
325
+ case "]":
326
+ square--;
327
+ break;
328
+ }
329
+ if (round < 0 || curly < 0 || square < 0) return false;
330
+ }
331
+ return round === 0 && curly === 0 && square === 0;
332
+ }
333
+
334
+ // --- Source scanning ---------------------------------------------------------
335
+
336
+ interface StringSpan {
337
+ /** Index of the opening quote. */
338
+ start: number;
339
+ /** Index of the closing quote. */
340
+ end: number;
341
+ quote: string;
342
+ }
343
+
344
+ interface ScanResult {
345
+ /** Source with string/comment characters replaced by spaces (newlines kept). */
346
+ masked: string;
347
+ /** Spans of single- and double-quoted string literals. */
348
+ strings: StringSpan[];
349
+ }
350
+
351
+ /**
352
+ * Produce a masked view of the source (string and comment characters blanked to
353
+ * spaces, newlines preserved) plus the spans of single/double-quoted strings.
354
+ * Index alignment with the original source is preserved throughout.
355
+ */
356
+ function scanSource(source: string): ScanResult {
357
+ const n = source.length;
358
+ const masked = source.split("");
359
+ const strings: StringSpan[] = [];
360
+
361
+ const blank = (i: number) => {
362
+ if (source[i] !== "\n") masked[i] = " ";
363
+ };
364
+
365
+ let i = 0;
366
+ while (i < n) {
367
+ const ch = source[i];
368
+
369
+ // Line comment.
370
+ if (ch === "/" && source[i + 1] === "/") {
371
+ let j = i;
372
+ while (j < n && source[j] !== "\n") {
373
+ blank(j);
374
+ j++;
375
+ }
376
+ i = j;
377
+ continue;
378
+ }
379
+
380
+ // Block comment.
381
+ if (ch === "/" && source[i + 1] === "*") {
382
+ blank(i);
383
+ blank(i + 1);
384
+ let j = i + 2;
385
+ while (j < n && !(source[j] === "*" && source[j + 1] === "/")) {
386
+ blank(j);
387
+ j++;
388
+ }
389
+ if (j < n) {
390
+ blank(j);
391
+ blank(j + 1);
392
+ j += 2;
393
+ }
394
+ i = j;
395
+ continue;
396
+ }
397
+
398
+ // String / template literal.
399
+ if (ch === '"' || ch === "'" || ch === "`") {
400
+ const quote = ch;
401
+ const openIdx = i;
402
+ blank(i);
403
+ let j = i + 1;
404
+ let closed = false;
405
+ while (j < n) {
406
+ const cj = source[j];
407
+ if (cj === "\\") {
408
+ // Escape: blank both the backslash and the escaped char.
409
+ blank(j);
410
+ if (j + 1 < n) blank(j + 1);
411
+ j += 2;
412
+ continue;
413
+ }
414
+ if (cj === quote) {
415
+ blank(j);
416
+ closed = true;
417
+ break;
418
+ }
419
+ // Single/double quotes do not span lines.
420
+ if (cj === "\n" && quote !== "`") {
421
+ break;
422
+ }
423
+ blank(j);
424
+ j++;
425
+ }
426
+ if (closed && (quote === '"' || quote === "'")) {
427
+ strings.push({ start: openIdx, end: j, quote });
428
+ }
429
+ i = j + 1;
430
+ continue;
431
+ }
432
+
433
+ i++;
434
+ }
435
+
436
+ return { masked: masked.join(""), strings };
437
+ }
438
+
439
+ /** Index of the first top-level `;` at or after `from`, or -1 if none. */
440
+ function findTopLevelSemicolon(masked: string, from: number): number {
441
+ let round = 0;
442
+ let curly = 0;
443
+ let square = 0;
444
+ for (let i = from; i < masked.length; i++) {
445
+ const ch = masked[i];
446
+ switch (ch) {
447
+ case "(":
448
+ round++;
449
+ break;
450
+ case ")":
451
+ round--;
452
+ break;
453
+ case "{":
454
+ curly++;
455
+ break;
456
+ case "}":
457
+ curly--;
458
+ break;
459
+ case "[":
460
+ square++;
461
+ break;
462
+ case "]":
463
+ square--;
464
+ break;
465
+ case ";":
466
+ if (round <= 0 && curly <= 0 && square <= 0) return i;
467
+ break;
468
+ }
469
+ }
470
+ return -1;
471
+ }
472
+
473
+ // --- Line helpers ------------------------------------------------------------
474
+
475
+ /** Start index of each line (index 0 plus every position after a `\n`). */
476
+ function computeLineStarts(source: string): number[] {
477
+ const starts = [0];
478
+ for (let i = 0; i < source.length; i++) {
479
+ if (source[i] === "\n") starts.push(i + 1);
480
+ }
481
+ return starts;
482
+ }
483
+
484
+ /** 1-based line number for a source index. */
485
+ function indexToLine(lineStarts: number[], index: number): number {
486
+ let lo = 0;
487
+ let hi = lineStarts.length - 1;
488
+ let line = 0;
489
+ while (lo <= hi) {
490
+ const mid = (lo + hi) >> 1;
491
+ if (lineStarts[mid] <= index) {
492
+ line = mid;
493
+ lo = mid + 1;
494
+ } else {
495
+ hi = mid - 1;
496
+ }
497
+ }
498
+ return line + 1;
499
+ }
500
+
501
+ /**
502
+ * Build line-granular original/mutated snippets for a `[start, end)` replacement.
503
+ * Spans the full lines touched by the range so the snippet reads as code, and
504
+ * applies the replacement to produce the mutated counterpart.
505
+ */
506
+ function snippetForRange(
507
+ source: string,
508
+ lineStarts: number[],
509
+ start: number,
510
+ end: number,
511
+ replacement: string,
512
+ ): { original: string; mutated: string } {
513
+ const firstLine = indexToLine(lineStarts, start) - 1;
514
+ const lastIdx = end > start ? end - 1 : start;
515
+ const lastLine = indexToLine(lineStarts, lastIdx) - 1;
516
+ const lo = lineStarts[firstLine];
517
+ const hi =
518
+ lastLine + 1 < lineStarts.length
519
+ ? lineStarts[lastLine + 1] - 1
520
+ : source.length;
521
+ const original = source.slice(lo, hi);
522
+ const mutated = source.slice(lo, start) + replacement + source.slice(end, hi);
523
+ return { original: original.trim(), mutated: mutated.trim() };
524
+ }