@gitwand/core 1.6.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 (219) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/dist/__tests__/bench.bench.d.ts +14 -0
  4. package/dist/__tests__/bench.bench.d.ts.map +1 -0
  5. package/dist/__tests__/bench.bench.js +137 -0
  6. package/dist/__tests__/bench.bench.js.map +1 -0
  7. package/dist/__tests__/confidence-v14.test.d.ts +13 -0
  8. package/dist/__tests__/confidence-v14.test.d.ts.map +1 -0
  9. package/dist/__tests__/confidence-v14.test.js +284 -0
  10. package/dist/__tests__/confidence-v14.test.js.map +1 -0
  11. package/dist/__tests__/config.test.d.ts +2 -0
  12. package/dist/__tests__/config.test.d.ts.map +1 -0
  13. package/dist/__tests__/config.test.js +317 -0
  14. package/dist/__tests__/config.test.js.map +1 -0
  15. package/dist/__tests__/corpus.d.ts +36 -0
  16. package/dist/__tests__/corpus.d.ts.map +1 -0
  17. package/dist/__tests__/corpus.js +541 -0
  18. package/dist/__tests__/corpus.js.map +1 -0
  19. package/dist/__tests__/corpus.test.d.ts +17 -0
  20. package/dist/__tests__/corpus.test.d.ts.map +1 -0
  21. package/dist/__tests__/corpus.test.js +179 -0
  22. package/dist/__tests__/corpus.test.js.map +1 -0
  23. package/dist/__tests__/diff.test.d.ts +10 -0
  24. package/dist/__tests__/diff.test.d.ts.map +1 -0
  25. package/dist/__tests__/diff.test.js +178 -0
  26. package/dist/__tests__/diff.test.js.map +1 -0
  27. package/dist/__tests__/format-resolvers.test.d.ts +2 -0
  28. package/dist/__tests__/format-resolvers.test.d.ts.map +1 -0
  29. package/dist/__tests__/format-resolvers.test.js +577 -0
  30. package/dist/__tests__/format-resolvers.test.js.map +1 -0
  31. package/dist/__tests__/imports-extended.test.d.ts +2 -0
  32. package/dist/__tests__/imports-extended.test.d.ts.map +1 -0
  33. package/dist/__tests__/imports-extended.test.js +94 -0
  34. package/dist/__tests__/imports-extended.test.js.map +1 -0
  35. package/dist/__tests__/lockfile-resolvers.test.d.ts +2 -0
  36. package/dist/__tests__/lockfile-resolvers.test.d.ts.map +1 -0
  37. package/dist/__tests__/lockfile-resolvers.test.js +200 -0
  38. package/dist/__tests__/lockfile-resolvers.test.js.map +1 -0
  39. package/dist/__tests__/patterns/insertion-at-boundary.test.d.ts +10 -0
  40. package/dist/__tests__/patterns/insertion-at-boundary.test.d.ts.map +1 -0
  41. package/dist/__tests__/patterns/insertion-at-boundary.test.js +185 -0
  42. package/dist/__tests__/patterns/insertion-at-boundary.test.js.map +1 -0
  43. package/dist/__tests__/patterns/reorder-only.test.d.ts +10 -0
  44. package/dist/__tests__/patterns/reorder-only.test.d.ts.map +1 -0
  45. package/dist/__tests__/patterns/reorder-only.test.js +181 -0
  46. package/dist/__tests__/patterns/reorder-only.test.js.map +1 -0
  47. package/dist/__tests__/phase-7-2-3b.test.d.ts +6 -0
  48. package/dist/__tests__/phase-7-2-3b.test.d.ts.map +1 -0
  49. package/dist/__tests__/phase-7-2-3b.test.js +730 -0
  50. package/dist/__tests__/phase-7-2-3b.test.js.map +1 -0
  51. package/dist/__tests__/resolver.test.d.ts +2 -0
  52. package/dist/__tests__/resolver.test.d.ts.map +1 -0
  53. package/dist/__tests__/resolver.test.js +927 -0
  54. package/dist/__tests__/resolver.test.js.map +1 -0
  55. package/dist/__tests__/resolvers/cargo.test.d.ts +10 -0
  56. package/dist/__tests__/resolvers/cargo.test.d.ts.map +1 -0
  57. package/dist/__tests__/resolvers/cargo.test.js +158 -0
  58. package/dist/__tests__/resolvers/cargo.test.js.map +1 -0
  59. package/dist/__tests__/resolvers/dockerfile.test.d.ts +8 -0
  60. package/dist/__tests__/resolvers/dockerfile.test.d.ts.map +1 -0
  61. package/dist/__tests__/resolvers/dockerfile.test.js +120 -0
  62. package/dist/__tests__/resolvers/dockerfile.test.js.map +1 -0
  63. package/dist/__tests__/resolvers/dotenv.test.d.ts +9 -0
  64. package/dist/__tests__/resolvers/dotenv.test.d.ts.map +1 -0
  65. package/dist/__tests__/resolvers/dotenv.test.js +113 -0
  66. package/dist/__tests__/resolvers/dotenv.test.js.map +1 -0
  67. package/dist/__tests__/resolvers/improvements-v14.test.d.ts +8 -0
  68. package/dist/__tests__/resolvers/improvements-v14.test.d.ts.map +1 -0
  69. package/dist/__tests__/resolvers/improvements-v14.test.js +306 -0
  70. package/dist/__tests__/resolvers/improvements-v14.test.js.map +1 -0
  71. package/dist/__tests__/validation.test.d.ts +12 -0
  72. package/dist/__tests__/validation.test.d.ts.map +1 -0
  73. package/dist/__tests__/validation.test.js +136 -0
  74. package/dist/__tests__/validation.test.js.map +1 -0
  75. package/dist/classifier.d.ts +21 -0
  76. package/dist/classifier.d.ts.map +1 -0
  77. package/dist/classifier.js +127 -0
  78. package/dist/classifier.js.map +1 -0
  79. package/dist/config.d.ts +108 -0
  80. package/dist/config.d.ts.map +1 -0
  81. package/dist/config.js +200 -0
  82. package/dist/config.js.map +1 -0
  83. package/dist/diff.d.ts +69 -0
  84. package/dist/diff.d.ts.map +1 -0
  85. package/dist/diff.js +328 -0
  86. package/dist/diff.js.map +1 -0
  87. package/dist/index.d.ts +47 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +38 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/parser.d.ts +39 -0
  92. package/dist/parser.d.ts.map +1 -0
  93. package/dist/parser.js +164 -0
  94. package/dist/parser.js.map +1 -0
  95. package/dist/patterns/complex.d.ts +5 -0
  96. package/dist/patterns/complex.d.ts.map +1 -0
  97. package/dist/patterns/complex.js +27 -0
  98. package/dist/patterns/complex.js.map +1 -0
  99. package/dist/patterns/delete-no-change.d.ts +4 -0
  100. package/dist/patterns/delete-no-change.d.ts.map +1 -0
  101. package/dist/patterns/delete-no-change.js +75 -0
  102. package/dist/patterns/delete-no-change.js.map +1 -0
  103. package/dist/patterns/insertion-at-boundary.d.ts +22 -0
  104. package/dist/patterns/insertion-at-boundary.d.ts.map +1 -0
  105. package/dist/patterns/insertion-at-boundary.js +164 -0
  106. package/dist/patterns/insertion-at-boundary.js.map +1 -0
  107. package/dist/patterns/non-overlapping.d.ts +4 -0
  108. package/dist/patterns/non-overlapping.d.ts.map +1 -0
  109. package/dist/patterns/non-overlapping.js +28 -0
  110. package/dist/patterns/non-overlapping.js.map +1 -0
  111. package/dist/patterns/one-side-change.d.ts +4 -0
  112. package/dist/patterns/one-side-change.d.ts.map +1 -0
  113. package/dist/patterns/one-side-change.js +45 -0
  114. package/dist/patterns/one-side-change.js.map +1 -0
  115. package/dist/patterns/reorder-only.d.ts +14 -0
  116. package/dist/patterns/reorder-only.d.ts.map +1 -0
  117. package/dist/patterns/reorder-only.js +81 -0
  118. package/dist/patterns/reorder-only.js.map +1 -0
  119. package/dist/patterns/same-change.d.ts +4 -0
  120. package/dist/patterns/same-change.d.ts.map +1 -0
  121. package/dist/patterns/same-change.js +25 -0
  122. package/dist/patterns/same-change.js.map +1 -0
  123. package/dist/patterns/utils.d.ts +70 -0
  124. package/dist/patterns/utils.d.ts.map +1 -0
  125. package/dist/patterns/utils.js +206 -0
  126. package/dist/patterns/utils.js.map +1 -0
  127. package/dist/patterns/value-only-change.d.ts +4 -0
  128. package/dist/patterns/value-only-change.d.ts.map +1 -0
  129. package/dist/patterns/value-only-change.js +34 -0
  130. package/dist/patterns/value-only-change.js.map +1 -0
  131. package/dist/patterns/whitespace-only.d.ts +4 -0
  132. package/dist/patterns/whitespace-only.d.ts.map +1 -0
  133. package/dist/patterns/whitespace-only.js +32 -0
  134. package/dist/patterns/whitespace-only.js.map +1 -0
  135. package/dist/resolver/assemble.d.ts +25 -0
  136. package/dist/resolver/assemble.d.ts.map +1 -0
  137. package/dist/resolver/assemble.js +170 -0
  138. package/dist/resolver/assemble.js.map +1 -0
  139. package/dist/resolver/format-dispatch.d.ts +40 -0
  140. package/dist/resolver/format-dispatch.d.ts.map +1 -0
  141. package/dist/resolver/format-dispatch.js +51 -0
  142. package/dist/resolver/format-dispatch.js.map +1 -0
  143. package/dist/resolver/generated-detection.d.ts +48 -0
  144. package/dist/resolver/generated-detection.d.ts.map +1 -0
  145. package/dist/resolver/generated-detection.js +123 -0
  146. package/dist/resolver/generated-detection.js.map +1 -0
  147. package/dist/resolver/index.d.ts +26 -0
  148. package/dist/resolver/index.d.ts.map +1 -0
  149. package/dist/resolver/index.js +147 -0
  150. package/dist/resolver/index.js.map +1 -0
  151. package/dist/resolver/policy.d.ts +53 -0
  152. package/dist/resolver/policy.d.ts.map +1 -0
  153. package/dist/resolver/policy.js +99 -0
  154. package/dist/resolver/policy.js.map +1 -0
  155. package/dist/resolver/validation.d.ts +28 -0
  156. package/dist/resolver/validation.d.ts.map +1 -0
  157. package/dist/resolver/validation.js +96 -0
  158. package/dist/resolver/validation.js.map +1 -0
  159. package/dist/resolver.d.ts +18 -0
  160. package/dist/resolver.d.ts.map +1 -0
  161. package/dist/resolver.js +18 -0
  162. package/dist/resolver.js.map +1 -0
  163. package/dist/resolvers/cargo.d.ts +34 -0
  164. package/dist/resolvers/cargo.d.ts.map +1 -0
  165. package/dist/resolvers/cargo.js +262 -0
  166. package/dist/resolvers/cargo.js.map +1 -0
  167. package/dist/resolvers/css.d.ts +60 -0
  168. package/dist/resolvers/css.d.ts.map +1 -0
  169. package/dist/resolvers/css.js +531 -0
  170. package/dist/resolvers/css.js.map +1 -0
  171. package/dist/resolvers/dispatcher.d.ts +78 -0
  172. package/dist/resolvers/dispatcher.d.ts.map +1 -0
  173. package/dist/resolvers/dispatcher.js +290 -0
  174. package/dist/resolvers/dispatcher.js.map +1 -0
  175. package/dist/resolvers/dockerfile.d.ts +24 -0
  176. package/dist/resolvers/dockerfile.d.ts.map +1 -0
  177. package/dist/resolvers/dockerfile.js +221 -0
  178. package/dist/resolvers/dockerfile.js.map +1 -0
  179. package/dist/resolvers/dotenv.d.ts +27 -0
  180. package/dist/resolvers/dotenv.d.ts.map +1 -0
  181. package/dist/resolvers/dotenv.js +114 -0
  182. package/dist/resolvers/dotenv.js.map +1 -0
  183. package/dist/resolvers/imports.d.ts +63 -0
  184. package/dist/resolvers/imports.d.ts.map +1 -0
  185. package/dist/resolvers/imports.js +513 -0
  186. package/dist/resolvers/imports.js.map +1 -0
  187. package/dist/resolvers/json.d.ts +48 -0
  188. package/dist/resolvers/json.d.ts.map +1 -0
  189. package/dist/resolvers/json.js +363 -0
  190. package/dist/resolvers/json.js.map +1 -0
  191. package/dist/resolvers/lockfile-npm.d.ts +38 -0
  192. package/dist/resolvers/lockfile-npm.d.ts.map +1 -0
  193. package/dist/resolvers/lockfile-npm.js +267 -0
  194. package/dist/resolvers/lockfile-npm.js.map +1 -0
  195. package/dist/resolvers/lockfile-pnpm.d.ts +44 -0
  196. package/dist/resolvers/lockfile-pnpm.d.ts.map +1 -0
  197. package/dist/resolvers/lockfile-pnpm.js +277 -0
  198. package/dist/resolvers/lockfile-pnpm.js.map +1 -0
  199. package/dist/resolvers/lockfile-yarn.d.ts +40 -0
  200. package/dist/resolvers/lockfile-yarn.d.ts.map +1 -0
  201. package/dist/resolvers/lockfile-yarn.js +184 -0
  202. package/dist/resolvers/lockfile-yarn.js.map +1 -0
  203. package/dist/resolvers/markdown.d.ts +64 -0
  204. package/dist/resolvers/markdown.d.ts.map +1 -0
  205. package/dist/resolvers/markdown.js +335 -0
  206. package/dist/resolvers/markdown.js.map +1 -0
  207. package/dist/resolvers/vue.d.ts +65 -0
  208. package/dist/resolvers/vue.d.ts.map +1 -0
  209. package/dist/resolvers/vue.js +258 -0
  210. package/dist/resolvers/vue.js.map +1 -0
  211. package/dist/resolvers/yaml.d.ts +65 -0
  212. package/dist/resolvers/yaml.d.ts.map +1 -0
  213. package/dist/resolvers/yaml.js +405 -0
  214. package/dist/resolvers/yaml.js.map +1 -0
  215. package/dist/types.d.ts +256 -0
  216. package/dist/types.d.ts.map +1 -0
  217. package/dist/types.js +8 -0
  218. package/dist/types.js.map +1 -0
  219. package/package.json +57 -0
@@ -0,0 +1,730 @@
1
+ /**
2
+ * Tests Phase 7.2 — Whitespace normalization + threshold tuning
3
+ * Tests Phase 7.3b — YAML, TS/JS imports, Vue SFC, CSS resolvers
4
+ */
5
+ import { describe, it, expect } from "vitest";
6
+ import { resolve } from "../resolver.js";
7
+ import { classifyConflict } from "../parser.js";
8
+ import { tryResolveYamlConflict } from "../resolvers/yaml.js";
9
+ import { tryResolveImportConflict, isImportBlock } from "../resolvers/imports.js";
10
+ import { tryResolveVueConflict, parseSfcBlocks } from "../resolvers/vue.js";
11
+ import { tryResolveCssConflict, parseCssRules } from "../resolvers/css.js";
12
+ import { tryFormatAwareResolve, isYamlFile, isJsFile, isVueFile, isCssFile, } from "../resolvers/dispatcher.js";
13
+ // ─── Helpers ──────────────────────────────────────────────
14
+ function makeHunk(base, ours, theirs) {
15
+ return {
16
+ baseLines: base,
17
+ oursLines: ours,
18
+ theirsLines: theirs,
19
+ startLine: 1,
20
+ type: "complex",
21
+ confidence: {
22
+ score: 20, label: "low",
23
+ dimensions: { typeClassification: 20, dataRisk: 0, scopeImpact: 0, fileFrequency: 0, baseAvailability: 0 },
24
+ boosters: [], penalties: [],
25
+ },
26
+ explanation: "test hunk",
27
+ trace: {
28
+ steps: [{ type: "complex", passed: true, reason: "test" }],
29
+ selected: "complex",
30
+ summary: "test",
31
+ hasBase: base.length > 0,
32
+ },
33
+ };
34
+ }
35
+ // ═══════════════════════════════════════════════════════════════
36
+ // PHASE 7.2 — Whitespace normalization
37
+ // ═══════════════════════════════════════════════════════════════
38
+ describe("Phase 7.2 — Whitespace normalization améliorée", () => {
39
+ describe("Normalisation des tabs", () => {
40
+ it("détecte whitespace_only quand ours a des tabs et theirs a des espaces", () => {
41
+ // Pour déclencher whitespace_only : ours ≠ base ET theirs ≠ base
42
+ // mais normalize(ours) === normalize(theirs)
43
+ const conflict = {
44
+ oursLines: ["\tconst x = 1;", "\tconst y = 2;"], // tabs
45
+ baseLines: [], // pas de base (diff2)
46
+ theirsLines: [" const x = 1;", " const y = 2;"], // 2 espaces
47
+ startLine: 1,
48
+ endLine: 5,
49
+ };
50
+ const result = classifyConflict(conflict);
51
+ // Sans base, same_change est testé en premier : ours ≠ theirs → non
52
+ // Puis whitespace_only : normalize(tabs) === normalize(2 espaces) → oui
53
+ expect(result.type).toBe("whitespace_only");
54
+ });
55
+ it("détecte whitespace_only entre 2 et 4 espaces (sans base)", () => {
56
+ const conflict = {
57
+ oursLines: [" function foo() {", " return 1;", " }"], // 4 espaces
58
+ baseLines: [], // diff2
59
+ theirsLines: [" function foo() {", " return 1;", " }"], // 2 espaces
60
+ startLine: 1,
61
+ endLine: 8,
62
+ };
63
+ const result = classifyConflict(conflict);
64
+ expect(result.type).toBe("whitespace_only");
65
+ });
66
+ });
67
+ describe("Strip des lignes vides en tête/queue", () => {
68
+ it("détecte whitespace_only quand ours a une ligne vide en tête (diff2)", () => {
69
+ // ours a une ligne vide en tête, theirs n'en a pas
70
+ // normalize(ours) retire la ligne vide → identique à theirs
71
+ const conflict = {
72
+ oursLines: ["", "const x = 1;"],
73
+ baseLines: [],
74
+ theirsLines: ["const x = 1;"],
75
+ startLine: 1,
76
+ endLine: 4,
77
+ };
78
+ const result = classifyConflict(conflict);
79
+ // same_change : ours ≠ theirs → non
80
+ // whitespace_only : normalize(["", "const x = 1;"]) = "const x = 1" === normalize(theirs) → oui
81
+ expect(result.type).toBe("whitespace_only");
82
+ });
83
+ it("détecte whitespace_only quand theirs a des lignes vides en queue (diff2)", () => {
84
+ const conflict = {
85
+ oursLines: ["const a = 1;"],
86
+ baseLines: [],
87
+ theirsLines: ["const a = 1;", "", ""],
88
+ startLine: 1,
89
+ endLine: 5,
90
+ };
91
+ const result = classifyConflict(conflict);
92
+ expect(result.type).toBe("whitespace_only");
93
+ });
94
+ });
95
+ describe("Collapse des espaces internes", () => {
96
+ it("détecte whitespace_only avec espaces multiples (diff2)", () => {
97
+ // ours et theirs diffèrent seulement par le nombre d'espaces
98
+ const conflict = {
99
+ oursLines: ["key: value"],
100
+ baseLines: [],
101
+ theirsLines: ["key: value"],
102
+ startLine: 1,
103
+ endLine: 3,
104
+ };
105
+ const result = classifyConflict(conflict);
106
+ // normalize collapse les espaces multiples
107
+ expect(result.type).toBe("whitespace_only");
108
+ });
109
+ });
110
+ describe("value_only_change — seuils affinés", () => {
111
+ it("détecte value_only_change sur un hash Git valide (hex lowercase)", () => {
112
+ const conflict = {
113
+ oursLines: ['"integrity": "sha512-abc123def456"'],
114
+ baseLines: [],
115
+ theirsLines: ['"integrity": "sha512-fed789cba321"'],
116
+ startLine: 1,
117
+ endLine: 3,
118
+ };
119
+ // Avec les nouveaux seuils et patterns, les hashes hex purs sont toujours volatils
120
+ const result = classifyConflict(conflict);
121
+ // Peut être value_only_change ou complex selon le ratio — on vérifie juste que ça ne plante pas
122
+ expect(result.type).toBeDefined();
123
+ });
124
+ it("détecte semver comme volatile", () => {
125
+ // La ligne '"version": "1.2.3"' avec le tokenizer original produit le token "1.2.3"
126
+ // (sans les guillemets → split sur `"`) qui matche le pattern semver ^N.N.N$
127
+ const conflict = {
128
+ oursLines: ['"version": "1.2.3"'],
129
+ baseLines: [],
130
+ theirsLines: ['"version": "2.0.0"'],
131
+ startLine: 1,
132
+ endLine: 3,
133
+ };
134
+ const result = classifyConflict(conflict);
135
+ // Les semvers 1.2.3 et 2.0.0 sont des tokens volatils (après split sur `"`)
136
+ expect(result.type).toBe("value_only_change");
137
+ });
138
+ it("ne considère pas un identifiant camelCase standard comme volatile", () => {
139
+ // myFunction et otherFunction ne matchent pas les patterns ^hex$ ni ^UUID$ ni semver etc.
140
+ const conflict = {
141
+ oursLines: ["const handler = myFunction;"],
142
+ baseLines: [],
143
+ theirsLines: ["const handler = otherFunction;"],
144
+ startLine: 1,
145
+ endLine: 3,
146
+ };
147
+ const result = classifyConflict(conflict);
148
+ // Ces noms ne sont pas volatils → complex
149
+ expect(result.type).toBe("complex");
150
+ });
151
+ });
152
+ });
153
+ // ═══════════════════════════════════════════════════════════════
154
+ // PHASE 7.3b — Résolveur YAML
155
+ // ═══════════════════════════════════════════════════════════════
156
+ describe("Phase 7.3b — Résolveur YAML", () => {
157
+ describe("tryResolveYamlConflict — cas de base", () => {
158
+ it("fusionne deux clés ajoutées indépendamment", () => {
159
+ const base = ["name: app", "version: 1.0.0"];
160
+ const ours = ["name: app", "version: 1.0.0", "author: Alice"];
161
+ const theirs = ["name: app", "version: 1.0.0", "license: MIT"];
162
+ const result = tryResolveYamlConflict(base, ours, theirs);
163
+ expect(result.mergedLines).not.toBeNull();
164
+ const merged = result.mergedLines.join("\n");
165
+ expect(merged).toContain("author: Alice");
166
+ expect(merged).toContain("license: MIT");
167
+ expect(result.resolvedKeys).toBeGreaterThan(0);
168
+ });
169
+ it("résout seul ours a modifié une clé", () => {
170
+ const base = ["name: app", "port: 3000"];
171
+ const ours = ["name: app", "port: 4000"]; // ours a changé port
172
+ const theirs = ["name: app", "port: 3000"]; // theirs n'a pas changé
173
+ const result = tryResolveYamlConflict(base, ours, theirs);
174
+ expect(result.mergedLines).not.toBeNull();
175
+ const merged = result.mergedLines.join("\n");
176
+ expect(merged).toContain("port: 4000");
177
+ });
178
+ it("résout seul theirs a modifié une clé", () => {
179
+ const base = ["name: app", "debug: false"];
180
+ const ours = ["name: app", "debug: false"];
181
+ const theirs = ["name: app", "debug: true"];
182
+ const result = tryResolveYamlConflict(base, ours, theirs);
183
+ expect(result.mergedLines).not.toBeNull();
184
+ const merged = result.mergedLines.join("\n");
185
+ expect(merged).toContain("debug: true");
186
+ });
187
+ it("retourne null si les deux côtés ont modifié la même clé différemment", () => {
188
+ const base = ["version: 1.0.0"];
189
+ const ours = ["version: 2.0.0"];
190
+ const theirs = ["version: 3.0.0"];
191
+ const result = tryResolveYamlConflict(base, ours, theirs);
192
+ expect(result.mergedLines).toBeNull();
193
+ expect(result.unresolvedKeys).toBeGreaterThan(0);
194
+ });
195
+ it("résout le cas same_change", () => {
196
+ const base = ["name: app"];
197
+ const ours = ["name: app", "type: module"];
198
+ const theirs = ["name: app", "type: module"];
199
+ const result = tryResolveYamlConflict(base, ours, theirs);
200
+ expect(result.mergedLines).not.toBeNull();
201
+ expect(result.mergedLines.join("\n")).toContain("type: module");
202
+ });
203
+ it("rejette les blocs avec anchors YAML", () => {
204
+ const base = ["defaults: &defaults", " port: 3000"];
205
+ const ours = ["defaults: &defaults", " port: 4000"];
206
+ const theirs = ["defaults: &defaults", " port: 3000"];
207
+ const result = tryResolveYamlConflict(base, ours, theirs);
208
+ // Anchors non supportés → null
209
+ expect(result.mergedLines).toBeNull();
210
+ expect(result.reason).toMatch(/anchor/i);
211
+ });
212
+ });
213
+ describe("tryResolveYamlConflict — blocs imbriqués", () => {
214
+ it("fusionne des sous-clés ajoutées indépendamment", () => {
215
+ const base = [
216
+ "scripts:",
217
+ " build: tsc",
218
+ " test: jest",
219
+ ];
220
+ const ours = [
221
+ "scripts:",
222
+ " build: tsc",
223
+ " test: jest",
224
+ " lint: eslint .",
225
+ ];
226
+ const theirs = [
227
+ "scripts:",
228
+ " build: tsc",
229
+ " test: jest",
230
+ " format: prettier .",
231
+ ];
232
+ const result = tryResolveYamlConflict(base, ours, theirs);
233
+ expect(result.mergedLines).not.toBeNull();
234
+ const merged = result.mergedLines.join("\n");
235
+ expect(merged).toContain("lint: eslint .");
236
+ expect(merged).toContain("format: prettier .");
237
+ expect(merged).toContain("build: tsc");
238
+ });
239
+ });
240
+ describe("Intégration YAML via resolve()", () => {
241
+ it("résout un conflit YAML via le moteur principal", () => {
242
+ const yamlConflict = `name: my-app
243
+ version: 1.0.0
244
+ <<<<<<< ours
245
+ author: Alice
246
+ port: 3000
247
+ ||||||| base
248
+ port: 3000
249
+ =======
250
+ license: MIT
251
+ port: 3000
252
+ >>>>>>> theirs`;
253
+ const result = resolve(yamlConflict, "config.yaml");
254
+ expect(result.stats.autoResolved).toBe(1);
255
+ expect(result.mergedContent).not.toBeNull();
256
+ const merged = result.mergedContent;
257
+ expect(merged).toContain("author: Alice");
258
+ expect(merged).toContain("license: MIT");
259
+ });
260
+ it("la raison mentionne le résolveur yaml", () => {
261
+ const yamlConflict = `name: app
262
+ <<<<<<< ours
263
+ debug: true
264
+ ||||||| base
265
+ debug: false
266
+ =======
267
+ debug: false
268
+ >>>>>>> theirs`;
269
+ const result = resolve(yamlConflict, "config.yml");
270
+ if (result.stats.autoResolved > 0) {
271
+ expect(result.resolutions[0].resolutionReason).toMatch(/\[yaml\]/i);
272
+ }
273
+ });
274
+ });
275
+ });
276
+ // ═══════════════════════════════════════════════════════════════
277
+ // PHASE 7.3b — Résolveur d'imports TS/JS
278
+ // ═══════════════════════════════════════════════════════════════
279
+ describe("Phase 7.3b — Résolveur d'imports TS/JS", () => {
280
+ describe("isImportBlock", () => {
281
+ it("détecte un bloc entièrement composé d'imports", () => {
282
+ const lines = [
283
+ "import React from 'react';",
284
+ "import { useState } from 'react';",
285
+ "import type { FC } from 'react';",
286
+ ];
287
+ expect(isImportBlock(lines)).toBe(true);
288
+ });
289
+ it("rejette un bloc avec du code non-import", () => {
290
+ const lines = [
291
+ "import React from 'react';",
292
+ "const x = 1;",
293
+ ];
294
+ expect(isImportBlock(lines)).toBe(false);
295
+ });
296
+ it("accepte les lignes vides et commentaires dans un bloc d'imports", () => {
297
+ const lines = [
298
+ "// React",
299
+ "import React from 'react';",
300
+ "",
301
+ "import { useState } from 'react';",
302
+ ];
303
+ expect(isImportBlock(lines)).toBe(true);
304
+ });
305
+ });
306
+ describe("tryResolveImportConflict — cas de base", () => {
307
+ it("fusionne deux imports ajoutés indépendamment", () => {
308
+ const base = ["import React from 'react';"];
309
+ const ours = ["import React from 'react';", "import { useState } from 'react';"];
310
+ const theirs = ["import React from 'react';", "import { useEffect } from 'react';"];
311
+ const result = tryResolveImportConflict(base, ours, theirs);
312
+ expect(result.mergedLines).not.toBeNull();
313
+ const merged = result.mergedLines.join("\n");
314
+ expect(merged).toContain("useState");
315
+ expect(merged).toContain("useEffect");
316
+ });
317
+ it("fusionne des named imports du même module", () => {
318
+ const base = ["import { useState } from 'react';"];
319
+ const ours = ["import { useState, useEffect } from 'react';"];
320
+ const theirs = ["import { useState, useCallback } from 'react';"];
321
+ const result = tryResolveImportConflict(base, ours, theirs);
322
+ expect(result.mergedLines).not.toBeNull();
323
+ const merged = result.mergedLines.join("\n");
324
+ expect(merged).toContain("useState");
325
+ expect(merged).toContain("useEffect");
326
+ expect(merged).toContain("useCallback");
327
+ });
328
+ it("déduplique les imports identiques ajoutés des deux côtés", () => {
329
+ const base = [];
330
+ const ours = ["import React from 'react';"];
331
+ const theirs = ["import React from 'react';"];
332
+ const result = tryResolveImportConflict(base, ours, theirs);
333
+ expect(result.mergedLines).not.toBeNull();
334
+ // Un seul import React dans le résultat
335
+ const count = result.mergedLines.filter((l) => l.includes("import React")).length;
336
+ expect(count).toBe(1);
337
+ });
338
+ it("retourne null si les deux côtés ont modifié le même named import de façon conflictuelle", () => {
339
+ // ours a supprimé 'a' de l'import, theirs a supprimé 'b' → conflit (chacun a supprimé quelque chose)
340
+ const base = ["import { a, b } from 'lib';"];
341
+ const ours = ["import { a } from 'lib';"]; // supprimé b
342
+ const theirs = ["import { b } from 'lib';"]; // supprimé a
343
+ const result = tryResolveImportConflict(base, ours, theirs);
344
+ // ours supprime b que theirs garde, theirs supprime a que ours garde → conflit non résolvable
345
+ expect(result.mergedLines).toBeNull();
346
+ });
347
+ });
348
+ describe("Intégration imports via resolve()", () => {
349
+ it("résout un conflit d'imports TypeScript via le moteur principal", () => {
350
+ const tsConflict = `<<<<<<< ours
351
+ import React from 'react';
352
+ import { useState } from 'react';
353
+ import { MyComponent } from './components';
354
+ ||||||| base
355
+ import React from 'react';
356
+ import { useState } from 'react';
357
+ =======
358
+ import React from 'react';
359
+ import { useState } from 'react';
360
+ import { useEffect } from 'react';
361
+ >>>>>>> theirs`;
362
+ const result = resolve(tsConflict, "src/app.tsx");
363
+ expect(result.stats.autoResolved).toBe(1);
364
+ expect(result.mergedContent).not.toBeNull();
365
+ const merged = result.mergedContent;
366
+ expect(merged).toContain("MyComponent");
367
+ expect(merged).toContain("useEffect");
368
+ });
369
+ it("la raison mentionne le résolveur imports", () => {
370
+ const tsConflict = `<<<<<<< ours
371
+ import { A } from './a';
372
+ import { B } from './b';
373
+ ||||||| base
374
+ import { A } from './a';
375
+ =======
376
+ import { A } from './a';
377
+ import { C } from './c';
378
+ >>>>>>> theirs`;
379
+ const result = resolve(tsConflict, "src/index.ts");
380
+ if (result.stats.autoResolved > 0) {
381
+ expect(result.resolutions[0].resolutionReason).toMatch(/\[imports\]/i);
382
+ }
383
+ });
384
+ });
385
+ });
386
+ // ═══════════════════════════════════════════════════════════════
387
+ // PHASE 7.3b — Résolveur Vue SFC
388
+ // ═══════════════════════════════════════════════════════════════
389
+ describe("Phase 7.3b — Résolveur Vue SFC", () => {
390
+ describe("parseSfcBlocks", () => {
391
+ it("découpe un fichier Vue en blocs", () => {
392
+ const lines = [
393
+ "<template>",
394
+ " <div>Hello</div>",
395
+ "</template>",
396
+ "",
397
+ "<script setup>",
398
+ "const msg = 'world';",
399
+ "</script>",
400
+ "",
401
+ "<style scoped>",
402
+ ".container { color: red; }",
403
+ "</style>",
404
+ ];
405
+ const blocks = parseSfcBlocks(lines);
406
+ const names = blocks.map((b) => b.name);
407
+ expect(names).toContain("template");
408
+ expect(names).toContain("script");
409
+ expect(names).toContain("style");
410
+ });
411
+ it("gère un fichier avec seulement un template", () => {
412
+ const lines = ["<template>", " <p>test</p>", "</template>"];
413
+ const blocks = parseSfcBlocks(lines);
414
+ expect(blocks[0].name).toBe("template");
415
+ expect(blocks[0].lines).toContain(" <p>test</p>");
416
+ });
417
+ });
418
+ describe("tryResolveVueConflict — blocs indépendants", () => {
419
+ const baseVue = [
420
+ "<template>",
421
+ " <div>Base</div>",
422
+ "</template>",
423
+ "",
424
+ "<script setup>",
425
+ "const x = 1;",
426
+ "</script>",
427
+ ];
428
+ it("accepte la modification de script quand seul ours a changé", () => {
429
+ const ours = [
430
+ "<template>",
431
+ " <div>Base</div>",
432
+ "</template>",
433
+ "",
434
+ "<script setup>",
435
+ "const x = 2; // modifié par ours",
436
+ "</script>",
437
+ ];
438
+ const theirs = [...baseVue]; // identique à base
439
+ const result = tryResolveVueConflict(baseVue, ours, theirs);
440
+ expect(result.mergedLines).not.toBeNull();
441
+ const merged = result.mergedLines.join("\n");
442
+ expect(merged).toContain("modifié par ours");
443
+ });
444
+ it("accepte la modification de template quand seul theirs a changé", () => {
445
+ const ours = [...baseVue]; // identique à base
446
+ const theirs = [
447
+ "<template>",
448
+ " <div>Modifié par theirs</div>",
449
+ "</template>",
450
+ "",
451
+ "<script setup>",
452
+ "const x = 1;",
453
+ "</script>",
454
+ ];
455
+ const result = tryResolveVueConflict(baseVue, ours, theirs);
456
+ expect(result.mergedLines).not.toBeNull();
457
+ const merged = result.mergedLines.join("\n");
458
+ expect(merged).toContain("Modifié par theirs");
459
+ });
460
+ it("retourne null si les deux côtés ont modifié le même bloc", () => {
461
+ const ours = [
462
+ "<template>",
463
+ " <div>Version ours</div>",
464
+ "</template>",
465
+ "",
466
+ "<script setup>",
467
+ "const x = 1;",
468
+ "</script>",
469
+ ];
470
+ const theirs = [
471
+ "<template>",
472
+ " <div>Version theirs</div>",
473
+ "</template>",
474
+ "",
475
+ "<script setup>",
476
+ "const x = 1;",
477
+ "</script>",
478
+ ];
479
+ const result = tryResolveVueConflict(baseVue, ours, theirs);
480
+ expect(result.mergedLines).toBeNull();
481
+ expect(result.conflictedBlocks.length).toBeGreaterThan(0);
482
+ });
483
+ it("intègre un nouveau bloc style ajouté par theirs", () => {
484
+ const ours = [...baseVue]; // pas de style
485
+ const theirs = [
486
+ ...baseVue,
487
+ "",
488
+ "<style scoped>",
489
+ ".container { color: blue; }",
490
+ "</style>",
491
+ ];
492
+ const result = tryResolveVueConflict(baseVue, ours, theirs);
493
+ expect(result.mergedLines).not.toBeNull();
494
+ const merged = result.mergedLines.join("\n");
495
+ expect(merged).toContain("color: blue");
496
+ });
497
+ });
498
+ describe("Intégration Vue via resolve()", () => {
499
+ it("résout un conflit Vue SFC avec blocs indépendants", () => {
500
+ const vueConflict = `<template>
501
+ <div>Hello</div>
502
+ </template>
503
+
504
+ <<<<<<< ours
505
+ <script setup>
506
+ import { ref } from 'vue';
507
+ const count = ref(0);
508
+ </script>
509
+ ||||||| base
510
+ <script setup>
511
+ const msg = 'hello';
512
+ </script>
513
+ =======
514
+ <script setup>
515
+ const msg = 'world';
516
+ </script>
517
+ >>>>>>> theirs
518
+
519
+ <style>
520
+ .app { color: red; }
521
+ </style>`;
522
+ const result = resolve(vueConflict, "App.vue");
523
+ // Si les deux ont modifié le script → non résolvable (correct)
524
+ // ou si c'est un one_side_change selon la base
525
+ expect(result.stats).toBeDefined();
526
+ });
527
+ });
528
+ });
529
+ // ═══════════════════════════════════════════════════════════════
530
+ // PHASE 7.3b — Résolveur CSS
531
+ // ═══════════════════════════════════════════════════════════════
532
+ describe("Phase 7.3b — Résolveur CSS", () => {
533
+ describe("parseCssRules", () => {
534
+ it("parse des règles CSS basiques", () => {
535
+ const lines = [
536
+ ".button {",
537
+ " color: red;",
538
+ " font-size: 14px;",
539
+ "}",
540
+ "",
541
+ ".container {",
542
+ " max-width: 1200px;",
543
+ "}",
544
+ ];
545
+ const rules = parseCssRules(lines);
546
+ const ruleNames = rules.filter((r) => r.kind === "rule").map((r) => r.selector);
547
+ expect(ruleNames).toContain(".button");
548
+ expect(ruleNames).toContain(".container");
549
+ });
550
+ it("détecte les at-rules", () => {
551
+ const lines = [
552
+ "@media (max-width: 768px) {",
553
+ " .container { width: 100%; }",
554
+ "}",
555
+ ];
556
+ const rules = parseCssRules(lines);
557
+ const atRules = rules.filter((r) => r.kind === "at-rule");
558
+ expect(atRules.length).toBeGreaterThan(0);
559
+ expect(atRules[0].selector).toContain("@media");
560
+ });
561
+ });
562
+ describe("tryResolveCssConflict — cas de base", () => {
563
+ it("fusionne deux sélecteurs ajoutés indépendamment", () => {
564
+ const base = [".container {", " max-width: 1200px;", "}"];
565
+ const ours = [
566
+ ".container {",
567
+ " max-width: 1200px;",
568
+ "}",
569
+ ".button {",
570
+ " color: blue;",
571
+ "}",
572
+ ];
573
+ const theirs = [
574
+ ".container {",
575
+ " max-width: 1200px;",
576
+ "}",
577
+ ".header {",
578
+ " font-size: 24px;",
579
+ "}",
580
+ ];
581
+ const result = tryResolveCssConflict(base, ours, theirs);
582
+ expect(result.mergedLines).not.toBeNull();
583
+ const merged = result.mergedLines.join("\n");
584
+ expect(merged).toContain(".button");
585
+ expect(merged).toContain(".header");
586
+ });
587
+ it("fusionne des propriétés ajoutées dans le même sélecteur", () => {
588
+ const base = [".btn {", " color: blue;", "}"];
589
+ const ours = [".btn {", " color: blue;", " font-size: 14px;", "}"];
590
+ const theirs = [".btn {", " color: blue;", " padding: 8px;", "}"];
591
+ const result = tryResolveCssConflict(base, ours, theirs);
592
+ expect(result.mergedLines).not.toBeNull();
593
+ const merged = result.mergedLines.join("\n");
594
+ expect(merged).toContain("font-size: 14px");
595
+ expect(merged).toContain("padding: 8px");
596
+ });
597
+ it("retourne null si les deux côtés modifient la même propriété différemment", () => {
598
+ const base = [".btn {", " color: blue;", "}"];
599
+ const ours = [".btn {", " color: red;", "}"];
600
+ const theirs = [".btn {", " color: green;", "}"];
601
+ const result = tryResolveCssConflict(base, ours, theirs);
602
+ expect(result.mergedLines).toBeNull();
603
+ });
604
+ it("accepte un sélecteur inchangé", () => {
605
+ const base = [".container {", " width: 100%;", "}"];
606
+ const ours = [".container {", " width: 100%;", "}", ".new {", " color: red;", "}"];
607
+ const theirs = [...base]; // theirs n'a pas changé
608
+ const result = tryResolveCssConflict(base, ours, theirs);
609
+ expect(result.mergedLines).not.toBeNull();
610
+ const merged = result.mergedLines.join("\n");
611
+ expect(merged).toContain(".new");
612
+ });
613
+ });
614
+ describe("Intégration CSS via resolve()", () => {
615
+ it("résout un conflit CSS avec sélecteurs indépendants", () => {
616
+ const cssConflict = `.container {
617
+ max-width: 1200px;
618
+ }
619
+
620
+ <<<<<<< ours
621
+ .button {
622
+ color: blue;
623
+ padding: 8px;
624
+ }
625
+ ||||||| base
626
+ =======
627
+ .header {
628
+ font-size: 24px;
629
+ }
630
+ >>>>>>> theirs`;
631
+ const result = resolve(cssConflict, "styles.css");
632
+ expect(result.stats.autoResolved).toBe(1);
633
+ expect(result.mergedContent).not.toBeNull();
634
+ const merged = result.mergedContent;
635
+ expect(merged).toContain(".button");
636
+ expect(merged).toContain(".header");
637
+ });
638
+ it("la raison mentionne le résolveur css", () => {
639
+ const cssConflict = `<<<<<<< ours
640
+ .a { color: red; }
641
+ ||||||| base
642
+ =======
643
+ .b { color: blue; }
644
+ >>>>>>> theirs`;
645
+ const result = resolve(cssConflict, "app.scss");
646
+ if (result.stats.autoResolved > 0) {
647
+ expect(result.resolutions[0].resolutionReason).toMatch(/\[css\]/i);
648
+ }
649
+ });
650
+ });
651
+ });
652
+ // ═══════════════════════════════════════════════════════════════
653
+ // PHASE 7.3b — Dispatcher — détection des nouveaux types
654
+ // ═══════════════════════════════════════════════════════════════
655
+ describe("Phase 7.3b — Dispatcher — nouveaux types de fichiers", () => {
656
+ describe("isYamlFile", () => {
657
+ it("détecte .yaml et .yml", () => {
658
+ expect(isYamlFile("config.yaml")).toBe(true);
659
+ expect(isYamlFile("config.yml")).toBe(true);
660
+ expect(isYamlFile(".github/workflows/ci.yml")).toBe(true);
661
+ });
662
+ it("ne détecte pas les non-YAML", () => {
663
+ expect(isYamlFile("config.json")).toBe(false);
664
+ expect(isYamlFile("app.ts")).toBe(false);
665
+ });
666
+ });
667
+ describe("isJsFile", () => {
668
+ it("détecte .ts, .js, .tsx, .jsx, .mjs, .cjs", () => {
669
+ expect(isJsFile("app.ts")).toBe(true);
670
+ expect(isJsFile("App.tsx")).toBe(true);
671
+ expect(isJsFile("index.js")).toBe(true);
672
+ expect(isJsFile("Component.jsx")).toBe(true);
673
+ expect(isJsFile("server.mjs")).toBe(true);
674
+ expect(isJsFile("require.cjs")).toBe(true);
675
+ });
676
+ it("ne détecte pas les non-JS", () => {
677
+ expect(isJsFile("styles.css")).toBe(false);
678
+ expect(isJsFile("App.vue")).toBe(false);
679
+ });
680
+ });
681
+ describe("isVueFile", () => {
682
+ it("détecte .vue", () => {
683
+ expect(isVueFile("App.vue")).toBe(true);
684
+ expect(isVueFile("components/Button.vue")).toBe(true);
685
+ });
686
+ it("ne détecte pas les non-Vue", () => {
687
+ expect(isVueFile("app.ts")).toBe(false);
688
+ });
689
+ });
690
+ describe("isCssFile", () => {
691
+ it("détecte .css, .scss, .less, .sass", () => {
692
+ expect(isCssFile("styles.css")).toBe(true);
693
+ expect(isCssFile("theme.scss")).toBe(true);
694
+ expect(isCssFile("vars.less")).toBe(true);
695
+ expect(isCssFile("mixins.sass")).toBe(true);
696
+ });
697
+ it("ne détecte pas les non-CSS", () => {
698
+ expect(isCssFile("app.ts")).toBe(false);
699
+ });
700
+ });
701
+ describe("tryFormatAwareResolve — nouveaux résolveurs", () => {
702
+ it("utilise le résolveur yaml pour .yaml", () => {
703
+ const hunk = makeHunk(["name: app", "port: 3000"], ["name: app", "port: 4000"], ["name: app", "port: 3000"]);
704
+ const result = tryFormatAwareResolve(hunk, "config.yaml");
705
+ expect(result.resolverUsed).toBe("yaml");
706
+ });
707
+ it("utilise le résolveur vue pour .vue", () => {
708
+ const hunk = makeHunk(["<template>", " <div>Base</div>", "</template>"], ["<template>", " <div>Ours</div>", "</template>"], ["<template>", " <div>Base</div>", "</template>"]);
709
+ const result = tryFormatAwareResolve(hunk, "App.vue");
710
+ expect(result.resolverUsed).toBe("vue");
711
+ });
712
+ it("utilise le résolveur css pour .scss", () => {
713
+ const hunk = makeHunk([".btn { color: blue; }"], [".btn { color: blue; }", ".new { font-size: 14px; }"], [".btn { color: blue; }"]);
714
+ const result = tryFormatAwareResolve(hunk, "styles.scss");
715
+ expect(result.resolverUsed).toBe("css");
716
+ });
717
+ it("utilise le résolveur imports pour .ts avec bloc d'imports", () => {
718
+ const hunk = makeHunk(["import React from 'react';"], ["import React from 'react';", "import { useState } from 'react';"], ["import React from 'react';", "import { useEffect } from 'react';"]);
719
+ const result = tryFormatAwareResolve(hunk, "app.ts");
720
+ expect(result.resolverUsed).toBe("imports");
721
+ });
722
+ it("n'utilise pas le résolveur imports pour .ts avec code non-import", () => {
723
+ const hunk = makeHunk(["const x = 1;"], ["const x = 2;"], ["const x = 3;"]);
724
+ const result = tryFormatAwareResolve(hunk, "app.ts");
725
+ // Pas de résolveur spécialisé pour du code TS général
726
+ expect(result.resolverUsed).toBe("none");
727
+ });
728
+ });
729
+ });
730
+ //# sourceMappingURL=phase-7-2-3b.test.js.map