@gitwand/core 2.3.0 → 2.4.1

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 (113) hide show
  1. package/dist/__tests__/corpus.d.ts.map +1 -1
  2. package/dist/__tests__/corpus.js +115 -0
  3. package/dist/__tests__/corpus.js.map +1 -1
  4. package/dist/__tests__/patterns/complex.test.d.ts +9 -0
  5. package/dist/__tests__/patterns/complex.test.d.ts.map +1 -0
  6. package/dist/__tests__/patterns/complex.test.js +198 -0
  7. package/dist/__tests__/patterns/complex.test.js.map +1 -0
  8. package/dist/__tests__/patterns/delete-no-change.test.d.ts +11 -0
  9. package/dist/__tests__/patterns/delete-no-change.test.d.ts.map +1 -0
  10. package/dist/__tests__/patterns/delete-no-change.test.js +178 -0
  11. package/dist/__tests__/patterns/delete-no-change.test.js.map +1 -0
  12. package/dist/__tests__/patterns/non-overlapping.test.d.ts +11 -0
  13. package/dist/__tests__/patterns/non-overlapping.test.d.ts.map +1 -0
  14. package/dist/__tests__/patterns/non-overlapping.test.js +240 -0
  15. package/dist/__tests__/patterns/non-overlapping.test.js.map +1 -0
  16. package/dist/__tests__/patterns/one-side-change.test.d.ts +10 -0
  17. package/dist/__tests__/patterns/one-side-change.test.d.ts.map +1 -0
  18. package/dist/__tests__/patterns/one-side-change.test.js +191 -0
  19. package/dist/__tests__/patterns/one-side-change.test.js.map +1 -0
  20. package/dist/__tests__/patterns/same-change.test.d.ts +9 -0
  21. package/dist/__tests__/patterns/same-change.test.d.ts.map +1 -0
  22. package/dist/__tests__/patterns/same-change.test.js +173 -0
  23. package/dist/__tests__/patterns/same-change.test.js.map +1 -0
  24. package/dist/__tests__/patterns/value-only-change.test.d.ts +11 -0
  25. package/dist/__tests__/patterns/value-only-change.test.d.ts.map +1 -0
  26. package/dist/__tests__/patterns/value-only-change.test.js +159 -0
  27. package/dist/__tests__/patterns/value-only-change.test.js.map +1 -0
  28. package/dist/__tests__/patterns/whitespace-only.test.d.ts +10 -0
  29. package/dist/__tests__/patterns/whitespace-only.test.d.ts.map +1 -0
  30. package/dist/__tests__/patterns/whitespace-only.test.js +177 -0
  31. package/dist/__tests__/patterns/whitespace-only.test.js.map +1 -0
  32. package/dist/__tests__/resolvers/css.test.d.ts +12 -0
  33. package/dist/__tests__/resolvers/css.test.d.ts.map +1 -0
  34. package/dist/__tests__/resolvers/css.test.js +171 -0
  35. package/dist/__tests__/resolvers/css.test.js.map +1 -0
  36. package/dist/__tests__/resolvers/imports.test.d.ts +12 -0
  37. package/dist/__tests__/resolvers/imports.test.d.ts.map +1 -0
  38. package/dist/__tests__/resolvers/imports.test.js +135 -0
  39. package/dist/__tests__/resolvers/imports.test.js.map +1 -0
  40. package/dist/__tests__/resolvers/json.test.d.ts +12 -0
  41. package/dist/__tests__/resolvers/json.test.d.ts.map +1 -0
  42. package/dist/__tests__/resolvers/json.test.js +184 -0
  43. package/dist/__tests__/resolvers/json.test.js.map +1 -0
  44. package/dist/__tests__/resolvers/lockfile-npm.test.d.ts +12 -0
  45. package/dist/__tests__/resolvers/lockfile-npm.test.d.ts.map +1 -0
  46. package/dist/__tests__/resolvers/lockfile-npm.test.js +187 -0
  47. package/dist/__tests__/resolvers/lockfile-npm.test.js.map +1 -0
  48. package/dist/__tests__/resolvers/lockfile-pnpm.test.d.ts +12 -0
  49. package/dist/__tests__/resolvers/lockfile-pnpm.test.d.ts.map +1 -0
  50. package/dist/__tests__/resolvers/lockfile-pnpm.test.js +175 -0
  51. package/dist/__tests__/resolvers/lockfile-pnpm.test.js.map +1 -0
  52. package/dist/__tests__/resolvers/lockfile-yarn.test.d.ts +12 -0
  53. package/dist/__tests__/resolvers/lockfile-yarn.test.d.ts.map +1 -0
  54. package/dist/__tests__/resolvers/lockfile-yarn.test.js +165 -0
  55. package/dist/__tests__/resolvers/lockfile-yarn.test.js.map +1 -0
  56. package/dist/__tests__/resolvers/markdown.test.d.ts +12 -0
  57. package/dist/__tests__/resolvers/markdown.test.d.ts.map +1 -0
  58. package/dist/__tests__/resolvers/markdown.test.js +188 -0
  59. package/dist/__tests__/resolvers/markdown.test.js.map +1 -0
  60. package/dist/__tests__/resolvers/vue.test.d.ts +12 -0
  61. package/dist/__tests__/resolvers/vue.test.d.ts.map +1 -0
  62. package/dist/__tests__/resolvers/vue.test.js +225 -0
  63. package/dist/__tests__/resolvers/vue.test.js.map +1 -0
  64. package/dist/__tests__/resolvers/yaml.test.d.ts +12 -0
  65. package/dist/__tests__/resolvers/yaml.test.d.ts.map +1 -0
  66. package/dist/__tests__/resolvers/yaml.test.js +203 -0
  67. package/dist/__tests__/resolvers/yaml.test.js.map +1 -0
  68. package/dist/__tests__/v2-core-scenarios.test.d.ts +35 -0
  69. package/dist/__tests__/v2-core-scenarios.test.d.ts.map +1 -0
  70. package/dist/__tests__/v2-core-scenarios.test.js +692 -0
  71. package/dist/__tests__/v2-core-scenarios.test.js.map +1 -0
  72. package/dist/__tests__/validation-parse-tree.test.d.ts +15 -0
  73. package/dist/__tests__/validation-parse-tree.test.d.ts.map +1 -0
  74. package/dist/__tests__/validation-parse-tree.test.js +243 -0
  75. package/dist/__tests__/validation-parse-tree.test.js.map +1 -0
  76. package/dist/config.d.ts +25 -0
  77. package/dist/config.d.ts.map +1 -1
  78. package/dist/config.js +17 -0
  79. package/dist/config.js.map +1 -1
  80. package/dist/diff/index.d.ts.map +1 -1
  81. package/dist/diff/index.js +1 -3
  82. package/dist/diff/index.js.map +1 -1
  83. package/dist/patterns/utils.d.ts +8 -7
  84. package/dist/patterns/utils.d.ts.map +1 -1
  85. package/dist/patterns/utils.js +13 -12
  86. package/dist/patterns/utils.js.map +1 -1
  87. package/dist/resolver/adapters/strict-node.d.ts +32 -0
  88. package/dist/resolver/adapters/strict-node.d.ts.map +1 -0
  89. package/dist/resolver/adapters/strict-node.js +117 -0
  90. package/dist/resolver/adapters/strict-node.js.map +1 -0
  91. package/dist/resolver/index.d.ts +20 -1
  92. package/dist/resolver/index.d.ts.map +1 -1
  93. package/dist/resolver/index.js +89 -5
  94. package/dist/resolver/index.js.map +1 -1
  95. package/dist/resolver/policy.d.ts.map +1 -1
  96. package/dist/resolver/policy.js +3 -0
  97. package/dist/resolver/policy.js.map +1 -1
  98. package/dist/resolver/validate-parse-tree.d.ts +52 -0
  99. package/dist/resolver/validate-parse-tree.d.ts.map +1 -0
  100. package/dist/resolver/validate-parse-tree.js +87 -0
  101. package/dist/resolver/validate-parse-tree.js.map +1 -0
  102. package/dist/resolver/validate-strict.d.ts +27 -0
  103. package/dist/resolver/validate-strict.d.ts.map +1 -0
  104. package/dist/resolver/validate-strict.js +41 -0
  105. package/dist/resolver/validate-strict.js.map +1 -0
  106. package/dist/resolver/validation.d.ts.map +1 -1
  107. package/dist/resolver/validation.js +15 -1
  108. package/dist/resolver/validation.js.map +1 -1
  109. package/dist/resolvers/dispatcher.d.ts.map +1 -1
  110. package/dist/resolvers/dispatcher.js.map +1 -1
  111. package/dist/types.d.ts +60 -3
  112. package/dist/types.d.ts.map +1 -1
  113. package/package.json +1 -1
@@ -0,0 +1,692 @@
1
+ /**
2
+ * Tests d'intégration "grandeur nature" v2.x — scénarios de régression
3
+ *
4
+ * Couvre les apports majeurs de chaque release mineure du moteur :
5
+ *
6
+ * v2.1 — Histogram diff & block-move detection
7
+ * · `same_change` (renommage a,b→x,y) isolé malgré des insertions adjacentes
8
+ * · `insertion_at_boundary` (multiply vs divide) résolu sans conflit
9
+ * · `detectBlockMove` détecte un déplacement de bloc
10
+ *
11
+ * v2.2 — Format profile registry + JSON Patch arrays
12
+ * · `package.json` /dependencies, /keywords, /scripts résolu via profil
13
+ * · `tsconfig.json` /include résolu via stratégie "set"
14
+ * · `disableFormatProfiles` rétablit le comportement v2.1
15
+ *
16
+ * v2.3 — Structural TypeScript merge (tree-sitter)
17
+ * · Deux branches ajoutent des fonctions TS différentes → merge structural
18
+ * · Conflit sur le corps d'une même fonction → structural retourne null
19
+ *
20
+ * v2.4 — Validation parse-tree post-merge & rétraction
21
+ * · parseTreeValid: true après résolution propre
22
+ * · parseTreeValid: false + rétraction sur fichier syntaxiquement cassé
23
+ * · parseTreeValid: null sur fichier non supporté (.json)
24
+ * · resolve() synchrone → parseTreeValid toujours null
25
+ *
26
+ * Convention de non-flakiness (héritée de grandeur-nature.test.ts) :
27
+ * Les assertions qui requièrent WASM sont gardées par
28
+ * `if (merged === null) return;` ou `if (!WASM) return;`
29
+ * afin que les tests restent verts en CI sans peer optionnels.
30
+ *
31
+ * Le contenu hardcodé correspond exactement aux fichiers créés dans
32
+ * `_tmp_split_scenario` par `setup-v2-scenarios.sh`.
33
+ */
34
+ import { describe, it, expect, beforeAll } from "vitest";
35
+ import { resolve, resolveAsync } from "../resolver/index.js";
36
+ import { tryStructuralMergeResolve } from "../structural/index.js";
37
+ import { checkParseTreeValid, applyPostMergeRiskPenalty } from "../resolver/validate-parse-tree.js";
38
+ import { tryResolveJsonConflict } from "../resolvers/json.js";
39
+ import { histogramDiff } from "../diff/histogram.js";
40
+ import { detectBlockMove } from "../diff/block-move.js";
41
+ import { _resetCache } from "../structural/parsers/loader.js";
42
+ beforeAll(() => {
43
+ _resetCache();
44
+ });
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+ // Helpers
47
+ // ─────────────────────────────────────────────────────────────────────────────
48
+ /** Construit un conflit au format diff3. */
49
+ function diff3(ours, base, theirs) {
50
+ return `<<<<<<< ours\n${ours}||||||| base\n${base}=======\n${theirs}>>>>>>> theirs\n`;
51
+ }
52
+ // ─────────────────────────────────────────────────────────────────────────────
53
+ // ── v2.1 — Histogram diff & block-move detection ─────────────────────────────
54
+ //
55
+ // Scénario : branche v2.1-ours vs v2.1-theirs depuis split-base.
56
+ //
57
+ // split-base : add(a,b) + subtract(a,b)
58
+ // v2.1-ours : rename add→add(x,y), add multiply(), reorder subtract après multiply
59
+ // v2.1-theirs : rename add→add(x,y), add divide()
60
+ //
61
+ // Conflits attendus :
62
+ // Hunk 1 — same_change : le renommage a,b→x,y dans add()
63
+ // Hunk 2 — insertion_at_boundary : multiply (ours) vs divide (theirs) en fin de fichier
64
+ // ─────────────────────────────────────────────────────────────────────────────
65
+ /** Fichier de base (split-base). */
66
+ const V21_BASE = `// Tiny calculator module — educational demo
67
+ export function add(a, b) {
68
+ return a + b;
69
+ }
70
+
71
+ export function subtract(a, b) {
72
+ return a - b;
73
+ }
74
+ `;
75
+ /** Branche ours : rename + multiply + reorder subtract. */
76
+ const V21_OURS = `// Tiny calculator module — educational demo
77
+ export function add(x, y) {
78
+ return x + y;
79
+ }
80
+
81
+ export function multiply(a, b) {
82
+ return a * b;
83
+ }
84
+
85
+ export function subtract(a, b) {
86
+ return a - b;
87
+ }
88
+ `;
89
+ /** Branche theirs : rename + divide. */
90
+ const V21_THEIRS = `// Tiny calculator module — educational demo
91
+ export function add(x, y) {
92
+ return x + y;
93
+ }
94
+
95
+ export function subtract(a, b) {
96
+ return a - b;
97
+ }
98
+
99
+ export function divide(a, b) {
100
+ if (b === 0) throw new Error("division by zero");
101
+ return a / b;
102
+ }
103
+ `;
104
+ describe("v2.1 — same_change (renommage a,b→x,y isolé par histogram)", () => {
105
+ // Git produit un conflit sur le renommage add(a,b)→add(x,y) que les deux
106
+ // branches ont fait identiquement. Histogram ancre sur les lignes uniques
107
+ // (return / export function subtract) pour isoler ce hunk précisément.
108
+ const CONFLICT = diff3("export function add(x, y) {\n return x + y;\n}\n", "export function add(a, b) {\n return a + b;\n}\n", "export function add(x, y) {\n return x + y;\n}\n");
109
+ it("résout automatiquement le same_change", () => {
110
+ const result = resolve(CONFLICT, "calculator.js");
111
+ expect(result.stats.autoResolved).toBe(1);
112
+ expect(result.stats.remaining).toBe(0);
113
+ expect(result.mergedContent).toContain("add(x, y)");
114
+ });
115
+ it("type classifié same_change", () => {
116
+ const result = resolve(CONFLICT, "calculator.js");
117
+ expect(result.hunks[0]?.type).toBe("same_change");
118
+ });
119
+ it("confiance ≥ high (score ≥ 68)", () => {
120
+ const result = resolve(CONFLICT, "calculator.js");
121
+ const score = result.hunks[0]?.confidence.score ?? 0;
122
+ expect(score).toBeGreaterThanOrEqual(68);
123
+ });
124
+ });
125
+ describe("v2.1 — insertion_at_boundary (multiply vs divide en fin de fichier)", () => {
126
+ // insertion_at_boundary requiert des lignes uniques de chaque côté pour que
127
+ // le LCS détecte correctement les additions sans faux-positifs sur les lignes
128
+ // communes comme `}`. On représente les exports du calculator en lignes uniques.
129
+ // Base : add + subtract ; ours ajoute multiply ; theirs ajoute divide.
130
+ const CONFLICT = diff3(
131
+ // ours : base + multiply
132
+ " add,\n subtract,\n multiply,\n",
133
+ // base
134
+ " add,\n subtract,\n",
135
+ // theirs : base + divide
136
+ " add,\n subtract,\n divide,\n");
137
+ it("résout automatiquement le hunk d'insertion (insertion_at_boundary)", () => {
138
+ const result = resolve(CONFLICT, "calculator.js");
139
+ expect(result.stats.autoResolved).toBeGreaterThanOrEqual(1);
140
+ });
141
+ it("type classifié insertion_at_boundary", () => {
142
+ const result = resolve(CONFLICT, "calculator.js");
143
+ const type = result.hunks[0]?.type;
144
+ expect(type).toBe("insertion_at_boundary");
145
+ });
146
+ it("le résultat contient les deux exports ajoutés", () => {
147
+ const result = resolve(CONFLICT, "calculator.js");
148
+ if (!result.mergedContent)
149
+ return;
150
+ expect(result.mergedContent).toContain("multiply");
151
+ expect(result.mergedContent).toContain("divide");
152
+ });
153
+ });
154
+ describe("v2.1 — histogramDiff ancre sur les lignes rares", () => {
155
+ it("ancre sur la signature unique et non sur les accolades communes", () => {
156
+ const a = V21_BASE.split("\n");
157
+ const b = V21_OURS.split("\n");
158
+ const pairs = histogramDiff(a, b);
159
+ // Les paires doivent être valides
160
+ for (const [i, j] of pairs) {
161
+ expect(a[i]).toBe(b[j]);
162
+ }
163
+ // Les indices doivent être strictement croissants
164
+ for (let k = 1; k < pairs.length; k++) {
165
+ expect(pairs[k][0]).toBeGreaterThan(pairs[k - 1][0]);
166
+ expect(pairs[k][1]).toBeGreaterThan(pairs[k - 1][1]);
167
+ }
168
+ });
169
+ it("ne lève jamais d'exception sur des séquences vides", () => {
170
+ expect(() => histogramDiff([], [])).not.toThrow();
171
+ expect(() => histogramDiff(V21_BASE.split("\n"), [])).not.toThrow();
172
+ });
173
+ });
174
+ describe("v2.1 — detectBlockMove : subtract déplacé après multiply dans ours", () => {
175
+ it("ne lève jamais d'exception", () => {
176
+ expect(() => detectBlockMove(V21_BASE.split("\n"), V21_OURS.split("\n"), V21_THEIRS.split("\n"))).not.toThrow();
177
+ });
178
+ it("détecte le bloc subtract déplacé dans ours quand suffisamment grand", () => {
179
+ // detectBlockMove requiert windowSize lignes consécutives (default 3).
180
+ // Notre bloc subtract fait exactement 3 lignes → résultat possible.
181
+ const moves = detectBlockMove(V21_BASE.split("\n"), V21_OURS.split("\n"), V21_THEIRS.split("\n"));
182
+ // Peut être vide si le bloc est trop petit pour la fenêtre — mais ne doit pas throw.
183
+ expect(Array.isArray(moves)).toBe(true);
184
+ });
185
+ });
186
+ // ─────────────────────────────────────────────────────────────────────────────
187
+ // ── v2.2 — Format profile registry + JSON Patch arrays ───────────────────────
188
+ //
189
+ // Scénario : v2.2-ours vs v2.2-theirs depuis v2.2-base.
190
+ //
191
+ // v2.2-base : package.json avec debug dep, "math" keyword, "test" script
192
+ // tsconfig.json avec include: ["src"]
193
+ // v2.2-ours : + axios, + "calculator" keyword, + "build" script, + "test" include
194
+ // v2.2-theirs : + lodash, + "utility" keyword, + "lint" script, + "types" include
195
+ //
196
+ // Chaque conflict array est résolu via JSON Patch (stratégie "set").
197
+ // ─────────────────────────────────────────────────────────────────────────────
198
+ const PKG_BASE = `{
199
+ "name": "calculator",
200
+ "version": "1.0.0",
201
+ "description": "Tiny calculator module — educational demo",
202
+ "keywords": [
203
+ "math"
204
+ ],
205
+ "scripts": {
206
+ "test": "node test.js"
207
+ },
208
+ "dependencies": {
209
+ "debug": "^4.0.0"
210
+ }
211
+ }`;
212
+ const PKG_OURS = `{
213
+ "name": "calculator",
214
+ "version": "1.0.0",
215
+ "description": "Tiny calculator module — educational demo",
216
+ "keywords": [
217
+ "math",
218
+ "calculator"
219
+ ],
220
+ "scripts": {
221
+ "test": "node test.js",
222
+ "build": "tsc"
223
+ },
224
+ "dependencies": {
225
+ "debug": "^4.0.0",
226
+ "axios": "^1.7.0"
227
+ }
228
+ }`;
229
+ const PKG_THEIRS = `{
230
+ "name": "calculator",
231
+ "version": "1.0.0",
232
+ "description": "Tiny calculator module — educational demo",
233
+ "keywords": [
234
+ "math",
235
+ "utility"
236
+ ],
237
+ "scripts": {
238
+ "test": "node test.js",
239
+ "lint": "eslint ."
240
+ },
241
+ "dependencies": {
242
+ "debug": "^4.0.0",
243
+ "lodash": "^4.17.21"
244
+ }
245
+ }`;
246
+ const TSCONFIG_BASE = `{
247
+ "compilerOptions": {
248
+ "target": "ES2020",
249
+ "module": "ESNext",
250
+ "moduleResolution": "bundler",
251
+ "strict": true,
252
+ "outDir": "dist"
253
+ },
254
+ "include": [
255
+ "src"
256
+ ]
257
+ }`;
258
+ const TSCONFIG_OURS = `{
259
+ "compilerOptions": {
260
+ "target": "ES2020",
261
+ "module": "ESNext",
262
+ "moduleResolution": "bundler",
263
+ "strict": true,
264
+ "outDir": "dist"
265
+ },
266
+ "include": [
267
+ "src",
268
+ "test"
269
+ ]
270
+ }`;
271
+ const TSCONFIG_THEIRS = `{
272
+ "compilerOptions": {
273
+ "target": "ES2020",
274
+ "module": "ESNext",
275
+ "moduleResolution": "bundler",
276
+ "strict": true,
277
+ "outDir": "dist"
278
+ },
279
+ "include": [
280
+ "src",
281
+ "types"
282
+ ]
283
+ }`;
284
+ describe("v2.2 — package.json : profil résout keywords, scripts, dependencies", () => {
285
+ it("/keywords — set merge : math + calculator + utility", () => {
286
+ const result = tryResolveJsonConflict(PKG_BASE.split("\n"), PKG_OURS.split("\n"), PKG_THEIRS.split("\n"), "package.json");
287
+ expect(result.merged).not.toBeNull();
288
+ const parsed = JSON.parse(result.merged);
289
+ expect(parsed.keywords).toContain("math");
290
+ expect(parsed.keywords).toContain("calculator");
291
+ expect(parsed.keywords).toContain("utility");
292
+ });
293
+ it("/dependencies — set merge : debug + axios + lodash", () => {
294
+ const result = tryResolveJsonConflict(PKG_BASE.split("\n"), PKG_OURS.split("\n"), PKG_THEIRS.split("\n"), "package.json");
295
+ expect(result.merged).not.toBeNull();
296
+ const parsed = JSON.parse(result.merged);
297
+ expect(parsed.dependencies["debug"]).toBeDefined();
298
+ expect(parsed.dependencies["axios"]).toBeDefined();
299
+ expect(parsed.dependencies["lodash"]).toBeDefined();
300
+ });
301
+ it("/scripts — merge : test + build + lint présents", () => {
302
+ const result = tryResolveJsonConflict(PKG_BASE.split("\n"), PKG_OURS.split("\n"), PKG_THEIRS.split("\n"), "package.json");
303
+ expect(result.merged).not.toBeNull();
304
+ const parsed = JSON.parse(result.merged);
305
+ expect(parsed.scripts["test"]).toBeDefined();
306
+ expect(parsed.scripts["build"]).toBeDefined();
307
+ expect(parsed.scripts["lint"]).toBeDefined();
308
+ });
309
+ it("le résultat est du JSON valide", () => {
310
+ const result = tryResolveJsonConflict(PKG_BASE.split("\n"), PKG_OURS.split("\n"), PKG_THEIRS.split("\n"), "package.json");
311
+ expect(() => JSON.parse(result.merged)).not.toThrow();
312
+ });
313
+ it("sans profil (filePath inconnu) — fallback : non résolu", () => {
314
+ const result = tryResolveJsonConflict(PKG_BASE.split("\n"), PKG_OURS.split("\n"), PKG_THEIRS.split("\n"), "other-file.json");
315
+ // Pas de profil pour ce fichier → les arrays divergents ne sont pas résolus
316
+ expect(result.merged).toBeNull();
317
+ });
318
+ it("disableFormatProfiles=true — retombe en conflit", () => {
319
+ const conflictedPkg = diff3(PKG_OURS + "\n", PKG_BASE + "\n", PKG_THEIRS + "\n");
320
+ const disabled = resolve(conflictedPkg, "package.json", { disableFormatProfiles: true });
321
+ expect(disabled.stats.autoResolved).toBe(0);
322
+ });
323
+ });
324
+ describe("v2.2 — tsconfig.json : /include résolu par stratégie set", () => {
325
+ it("/include — set merge : src + test + types", () => {
326
+ const result = tryResolveJsonConflict(TSCONFIG_BASE.split("\n"), TSCONFIG_OURS.split("\n"), TSCONFIG_THEIRS.split("\n"), "tsconfig.json");
327
+ expect(result.merged).not.toBeNull();
328
+ const parsed = JSON.parse(result.merged);
329
+ expect(parsed.include).toContain("src");
330
+ expect(parsed.include).toContain("test");
331
+ expect(parsed.include).toContain("types");
332
+ });
333
+ it("le résultat est du JSON valide et compilerOptions est préservé", () => {
334
+ const result = tryResolveJsonConflict(TSCONFIG_BASE.split("\n"), TSCONFIG_OURS.split("\n"), TSCONFIG_THEIRS.split("\n"), "tsconfig.json");
335
+ expect(result.merged).not.toBeNull();
336
+ const parsed = JSON.parse(result.merged);
337
+ expect(parsed.compilerOptions.strict).toBe(true);
338
+ expect(parsed.compilerOptions.outDir).toBe("dist");
339
+ });
340
+ });
341
+ // ─────────────────────────────────────────────────────────────────────────────
342
+ // ── v2.3 — Structural TypeScript merge (tree-sitter) ─────────────────────────
343
+ //
344
+ // Scénario : v2.3-ours vs v2.3-theirs depuis v2.3-base.
345
+ //
346
+ // v2.3-base : calculator.ts — add(typed) + subtract(typed)
347
+ // v2.3-ours : + power(base, exp) + absolute(n)
348
+ // v2.3-theirs : + sqrt(n) + floor(n)
349
+ //
350
+ // Git produit un seul grand hunk en fin de fichier (les deux branches ont
351
+ // inséré leurs fonctions au même endroit). Le merge structurel résout en
352
+ // fusionnant entity-par-entity.
353
+ // ─────────────────────────────────────────────────────────────────────────────
354
+ const TS_BASE = `// Tiny calculator module — TypeScript version
355
+ export function add(a: number, b: number): number {
356
+ return a + b;
357
+ }
358
+
359
+ export function subtract(a: number, b: number): number {
360
+ return a - b;
361
+ }
362
+ `;
363
+ const TS_OURS = `// Tiny calculator module — TypeScript version
364
+ export function add(a: number, b: number): number {
365
+ return a + b;
366
+ }
367
+
368
+ export function subtract(a: number, b: number): number {
369
+ return a - b;
370
+ }
371
+
372
+ export function power(base: number, exp: number): number {
373
+ return Math.pow(base, exp);
374
+ }
375
+
376
+ export function absolute(n: number): number {
377
+ return Math.abs(n);
378
+ }
379
+ `;
380
+ const TS_THEIRS = `// Tiny calculator module — TypeScript version
381
+ export function add(a: number, b: number): number {
382
+ return a + b;
383
+ }
384
+
385
+ export function subtract(a: number, b: number): number {
386
+ return a - b;
387
+ }
388
+
389
+ export function sqrt(n: number): number {
390
+ if (n < 0) throw new RangeError("sqrt: argument must be non-negative");
391
+ return Math.sqrt(n);
392
+ }
393
+
394
+ export function floor(n: number): number {
395
+ return Math.floor(n);
396
+ }
397
+ `;
398
+ // Le conflit Git typique pour ce scénario : un seul grand hunk en fin de fichier.
399
+ const TS_CONFLICT = `// Tiny calculator module — TypeScript version
400
+ export function add(a: number, b: number): number {
401
+ return a + b;
402
+ }
403
+
404
+ export function subtract(a: number, b: number): number {
405
+ return a - b;
406
+ }
407
+
408
+ ` + diff3(`export function power(base: number, exp: number): number {
409
+ return Math.pow(base, exp);
410
+ }
411
+
412
+ export function absolute(n: number): number {
413
+ return Math.abs(n);
414
+ }
415
+ `, "", `export function sqrt(n: number): number {
416
+ if (n < 0) throw new RangeError("sqrt: argument must be non-negative");
417
+ return Math.sqrt(n);
418
+ }
419
+
420
+ export function floor(n: number): number {
421
+ return Math.floor(n);
422
+ }
423
+ `);
424
+ describe("v2.3 — structural TypeScript merge : deux branches ajoutent des fonctions différentes", () => {
425
+ it("tryStructuralMergeResolve résout (quand WASM disponible)", async () => {
426
+ const merged = await tryStructuralMergeResolve(TS_CONFLICT, "calculator.ts");
427
+ if (merged === null)
428
+ return; // WASM non disponible dans cet environnement
429
+ expect(merged).not.toContain("<<<<<<<");
430
+ expect(merged).not.toContain("=======");
431
+ expect(merged).not.toContain(">>>>>>>");
432
+ });
433
+ it("le résultat ne contient aucun marqueur de conflit (quand WASM disponible)", async () => {
434
+ const merged = await tryStructuralMergeResolve(TS_CONFLICT, "calculator.ts");
435
+ if (merged === null)
436
+ return; // WASM non disponible
437
+ expect(merged).not.toContain("<<<<<<<");
438
+ expect(merged).not.toContain("=======");
439
+ expect(merged).not.toContain(">>>>>>>");
440
+ // Les fonctions de base (non conflictuelles) sont toujours préservées
441
+ expect(merged).toContain("function add");
442
+ expect(merged).toContain("function subtract");
443
+ });
444
+ it("resolveAsync résout entièrement le fichier (quand WASM disponible)", async () => {
445
+ const result = await resolveAsync(TS_CONFLICT, "calculator.ts");
446
+ if (!result.mergedContent)
447
+ return; // WASM non disponible — hunk-based laisse un conflit
448
+ expect(result.stats.remaining).toBe(0);
449
+ });
450
+ it("tryStructuralMergeResolve ne lève jamais d'exception", async () => {
451
+ await expect(tryStructuralMergeResolve(TS_CONFLICT, "calculator.ts")).resolves.not.toThrow();
452
+ });
453
+ });
454
+ describe("v2.3 — structural merge : conflit sur le corps d'une même fonction → null", () => {
455
+ // Les deux branches remplacent le corps de add() différemment.
456
+ // Le merge structurel doit retourner null (ne peut pas décider).
457
+ const BODY_CONFLICT = diff3(`// Tiny calculator module — TypeScript version
458
+ export function add(a: number, b: number): number {
459
+ return (a + b);
460
+ }
461
+
462
+ export function subtract(a: number, b: number): number {
463
+ return a - b;
464
+ }
465
+ `, TS_BASE, `// Tiny calculator module — TypeScript version
466
+ export function add(a: number, b: number): number {
467
+ return a + b || 0;
468
+ }
469
+
470
+ export function subtract(a: number, b: number): number {
471
+ return a - b;
472
+ }
473
+ `);
474
+ it("tryStructuralMergeResolve retourne null — conflit réel sur le corps", async () => {
475
+ const merged = await tryStructuralMergeResolve(BODY_CONFLICT, "calculator.ts");
476
+ expect(merged).toBeNull();
477
+ });
478
+ it("resolveAsync a des conflits restants", async () => {
479
+ const result = await resolveAsync(BODY_CONFLICT, "calculator.ts");
480
+ expect(result.stats.remaining).toBeGreaterThan(0);
481
+ expect(result.mergedContent).toBeNull();
482
+ });
483
+ });
484
+ // ─────────────────────────────────────────────────────────────────────────────
485
+ // ── v2.4 — Validation parse-tree post-merge & rétraction ─────────────────────
486
+ //
487
+ // Scénario A : happy path — same_change JSDoc sur calculator.ts
488
+ // Les deux branches ajoutent exactement le même JSDoc sur add().
489
+ // Résolution → same_change → parseTreeValid: true
490
+ //
491
+ // Scénario B : rétraction — fichier à contexte syntaxiquement cassé
492
+ // La résolution du hunk (same_change) produit un fichier mergé dont
493
+ // le parse-tree tree-sitter contient des ERROR nodes.
494
+ // → parseTreeValid: false + rétraction de toutes les résolutions auto.
495
+ //
496
+ // Scénario C : non supporté — .json → parseTreeValid: null
497
+ // Scénario D : resolve() synchrone → parseTreeValid: null (toujours)
498
+ // ─────────────────────────────────────────────────────────────────────────────
499
+ describe("v2.4A — happy path : parseTreeValid: true après résolution propre", () => {
500
+ // same_change : les deux branches ajoutent le même JSDoc sur add()
501
+ const JSDOC_CONFLICT = `// Tiny calculator module — TypeScript version
502
+
503
+ ` + diff3(`/** Adds two numbers. */
504
+ export function add(a: number, b: number): number {
505
+ return a + b;
506
+ }
507
+ `, `export function add(a: number, b: number): number {
508
+ return a + b;
509
+ }
510
+ `, `/** Adds two numbers. */
511
+ export function add(a: number, b: number): number {
512
+ return a + b;
513
+ }
514
+ `) + `
515
+ export function subtract(a: number, b: number): number {
516
+ return a - b;
517
+ }
518
+ `;
519
+ it("resolveAsync : parseTreeValid est true ou null (jamais false sur code valide)", async () => {
520
+ const result = await resolveAsync(JSDOC_CONFLICT, "calculator.ts");
521
+ // null = tree-sitter non disponible ; true = syntaxe valide
522
+ expect(result.validation.parseTreeValid === null || result.validation.parseTreeValid === true).toBe(true);
523
+ });
524
+ it("resolveAsync : le same_change est résolu (code valide → pas de rétraction)", async () => {
525
+ const result = await resolveAsync(JSDOC_CONFLICT, "calculator.ts");
526
+ // Si tree-sitter est disponible et valide, la résolution est maintenue.
527
+ // Si non disponible, le same_change reste résolu également.
528
+ if (result.validation.parseTreeValid !== false) {
529
+ // Pas de rétraction — la résolution doit être présente
530
+ expect(result.stats.autoResolved).toBeGreaterThanOrEqual(0);
531
+ }
532
+ });
533
+ it("checkParseTreeValid retourne true ou null sur du TypeScript valide", async () => {
534
+ const validTs = `/** Adds two numbers. */
535
+ export function add(a: number, b: number): number {
536
+ return a + b;
537
+ }
538
+
539
+ export function subtract(a: number, b: number): number {
540
+ return a - b;
541
+ }
542
+ `;
543
+ const result = await checkParseTreeValid(validTs, "calculator.ts");
544
+ expect(result === null || result === true).toBe(true);
545
+ });
546
+ });
547
+ describe("v2.4B — rétraction : parseTreeValid: false sur fichier cassé", () => {
548
+ // Fichier avec une accolade fermante manquante pour la closure forEach.
549
+ // La résolution du same_change produit du TS syntaxiquement invalide.
550
+ const BROKEN_BASE = `import { EventEmitter } from "events";
551
+
552
+ export class CalculatorEvents extends EventEmitter {
553
+ processAll(ops: string[]) {
554
+ ops.forEach(op => {
555
+ this.emit("before", op);
556
+ // placeholder
557
+ this.emit("after", op);
558
+ // NB: closing }) intentionally omitted
559
+
560
+ export function createCalculatorEvents(): CalculatorEvents {
561
+ return new CalculatorEvents();
562
+ }
563
+ `;
564
+ const BROKEN_OURS = `import { EventEmitter } from "events";
565
+
566
+ export class CalculatorEvents extends EventEmitter {
567
+ processAll(ops: string[]) {
568
+ ops.forEach(op => {
569
+ this.emit("before", op);
570
+ console.log(\`processing: \${op}\`);
571
+ this.emit("after", op);
572
+ // NB: closing }) intentionally omitted
573
+
574
+ export function createCalculatorEvents(): CalculatorEvents {
575
+ return new CalculatorEvents();
576
+ }
577
+ `;
578
+ const BROKEN_THEIRS = BROKEN_OURS; // same_change
579
+ it("checkParseTreeValid retourne false ou null sur du TypeScript cassé", async () => {
580
+ const broken = `function foo( {\n return 1;\n`;
581
+ const result = await checkParseTreeValid(broken, "broken.ts");
582
+ expect(result === null || result === false).toBe(true);
583
+ });
584
+ it("applyPostMergeRiskPenalty : rétraction immutable et complète", () => {
585
+ const resolution = {
586
+ autoResolved: true,
587
+ resolvedLines: ["console.log('ok');"],
588
+ resolutionReason: "same_change",
589
+ hunk: {
590
+ baseLines: ["// placeholder"],
591
+ oursLines: ["console.log('ok');"],
592
+ theirsLines: ["console.log('ok');"],
593
+ startLine: 7,
594
+ type: "same_change",
595
+ confidence: {
596
+ score: 100,
597
+ label: "certain",
598
+ dimensions: {
599
+ typeClassification: 100,
600
+ dataRisk: 0,
601
+ scopeImpact: 0,
602
+ fileFrequency: 0,
603
+ baseAvailability: 100,
604
+ },
605
+ boosters: ["same_change"],
606
+ penalties: [],
607
+ },
608
+ explanation: "same_change",
609
+ trace: {
610
+ steps: [],
611
+ selected: "same_change",
612
+ summary: "same_change",
613
+ hasBase: true,
614
+ },
615
+ },
616
+ };
617
+ const retracted = applyPostMergeRiskPenalty(resolution);
618
+ // Rétraction effective
619
+ expect(retracted.autoResolved).toBe(false);
620
+ expect(retracted.resolvedLines).toBeNull();
621
+ expect(retracted.hunk.confidence.score).toBe(0);
622
+ expect(retracted.hunk.confidence.label).toBe("low");
623
+ expect(retracted.hunk.confidence.dimensions.postMergeRisk).toBe(100);
624
+ expect(retracted.hunk.confidence.penalties.some((p) => /parse-tree/i.test(p))).toBe(true);
625
+ // Immutabilité de l'original
626
+ expect(resolution.autoResolved).toBe(true);
627
+ expect(resolution.resolvedLines).toEqual(["console.log('ok');"]);
628
+ expect(resolution.hunk.confidence.score).toBe(100);
629
+ });
630
+ it("resolveAsync sur fichier cassé : parseTreeValid false → rétraction (quand WASM)", async () => {
631
+ const conflictedBroken = diff3(BROKEN_OURS, BROKEN_BASE, BROKEN_THEIRS);
632
+ const result = await resolveAsync(conflictedBroken, "calculator-events.ts");
633
+ if (result.validation.parseTreeValid === null) {
634
+ // WASM non disponible → pas de validation → la résolution est maintenue ou non
635
+ return;
636
+ }
637
+ if (result.validation.parseTreeValid === false) {
638
+ // Rétraction activée
639
+ expect(result.mergedContent).toBeNull();
640
+ expect(result.stats.autoResolved).toBe(0);
641
+ expect(result.resolutions.every((r) => !r.autoResolved)).toBe(true);
642
+ }
643
+ // parseTreeValid: true = le parse-tree ne voit pas l'erreur dans ce contexte → ok aussi
644
+ });
645
+ });
646
+ describe("v2.4C — non-supporté : parseTreeValid: null sur .json", () => {
647
+ it("checkParseTreeValid retourne null pour .json (pas de grammaire tree-sitter)", async () => {
648
+ const result = await checkParseTreeValid('{"a": 1}', "package.json");
649
+ expect(result).toBeNull();
650
+ });
651
+ it("resolveAsync : parseTreeValid est null sur .json", async () => {
652
+ const conflict = diff3('{"version": "1.0.0"}\n', '{"version": "0.9.0"}\n', '{"version": "1.0.0"}\n');
653
+ const result = await resolveAsync(conflict, "package.json");
654
+ expect(result.validation.parseTreeValid).toBeNull();
655
+ });
656
+ it("checkParseTreeValid retourne null pour .md", async () => {
657
+ const result = await checkParseTreeValid("# Hello\n\nsome text\n", "README.md");
658
+ expect(result).toBeNull();
659
+ });
660
+ });
661
+ describe("v2.4D — resolve() synchrone : parseTreeValid toujours null", () => {
662
+ it("resolve() ne fait pas de validation parse-tree → null", () => {
663
+ const conflict = diff3("const x = 1;\n", "const x = 0;\n", "const x = 1;\n");
664
+ const result = resolve(conflict, "test.ts");
665
+ // resolve() est synchrone : parseTreeValid n'est jamais calculé
666
+ expect(result.validation.parseTreeValid).toBeNull();
667
+ });
668
+ it("resolve() : externalValidation absent par défaut (sync ne lance pas la validation stricte)", () => {
669
+ const conflict = diff3("const x = 1;\n", "const x = 0;\n", "const x = 1;\n");
670
+ const result = resolve(conflict, "test.ts");
671
+ // resolve() synchrone : la validation stricte (tsc/eslint) n'est jamais lancée
672
+ expect(result.validation.externalValidation).toBeUndefined();
673
+ });
674
+ });
675
+ describe("v2.4 — checkParseTreeValid : robustesse et graceful degradation", () => {
676
+ it("ne lève jamais d'exception sur contenu vide ou chemin vide", async () => {
677
+ await expect(checkParseTreeValid("", "")).resolves.not.toThrow();
678
+ });
679
+ it("ne lève jamais d'exception sur contenu binaire", async () => {
680
+ await expect(checkParseTreeValid("\x00\xff\xfe", "weird.ts")).resolves.not.toThrow();
681
+ });
682
+ it("retourne null ou boolean pour du JS valide (.js)", async () => {
683
+ const js = `const add = (a, b) => a + b;\nmodule.exports = { add };\n`;
684
+ const result = await checkParseTreeValid(js, "utils.js");
685
+ expect(result === null || typeof result === "boolean").toBe(true);
686
+ });
687
+ it("resolveAsync ne lève jamais d'exception sur fichier .ts mal formé", async () => {
688
+ const conflict = diff3("function broken( {\n", "function broken() {\n", "function broken() {\n return 1;\n}\n");
689
+ await expect(resolveAsync(conflict, "broken.ts")).resolves.not.toThrow();
690
+ });
691
+ });
692
+ //# sourceMappingURL=v2-core-scenarios.test.js.map