agentic-api 2.0.684 → 2.0.885
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/agents/prompts.d.ts +2 -3
- package/dist/src/agents/prompts.js +13 -109
- package/dist/src/agents/reducer.loaders.d.ts +46 -15
- package/dist/src/agents/reducer.loaders.js +76 -21
- package/dist/src/agents/reducer.types.d.ts +30 -3
- package/dist/src/agents/simulator.d.ts +3 -2
- package/dist/src/agents/simulator.executor.d.ts +8 -2
- package/dist/src/agents/simulator.executor.js +62 -26
- package/dist/src/agents/simulator.js +100 -11
- package/dist/src/agents/simulator.prompts.d.ts +48 -21
- package/dist/src/agents/simulator.prompts.js +289 -122
- package/dist/src/agents/simulator.types.d.ts +33 -1
- package/dist/src/agents/subagent.d.ts +128 -0
- package/dist/src/agents/subagent.js +231 -0
- package/dist/src/agents/worker.executor.d.ts +48 -0
- package/dist/src/agents/worker.executor.js +152 -0
- package/dist/src/execute/helpers.d.ts +3 -0
- package/dist/src/execute/helpers.js +221 -15
- package/dist/src/execute/responses.js +78 -51
- package/dist/src/execute/shared.d.ts +5 -0
- package/dist/src/execute/shared.js +27 -0
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +3 -1
- package/dist/src/llm/openai.js +8 -1
- package/dist/src/llm/pricing.js +2 -0
- package/dist/src/llm/xai.js +11 -6
- package/dist/src/prompts.d.ts +14 -0
- package/dist/src/prompts.js +41 -1
- package/dist/src/rag/rag.manager.d.ts +18 -3
- package/dist/src/rag/rag.manager.js +91 -5
- package/dist/src/rules/git/git.e2e.helper.js +3 -0
- package/dist/src/rules/git/git.health.js +88 -57
- package/dist/src/rules/git/index.d.ts +1 -1
- package/dist/src/rules/git/index.js +13 -5
- package/dist/src/rules/git/repo.d.ts +25 -6
- package/dist/src/rules/git/repo.js +430 -146
- package/dist/src/rules/git/repo.pr.js +45 -13
- package/dist/src/rules/git/repo.tools.d.ts +5 -0
- package/dist/src/rules/git/repo.tools.js +6 -1
- package/dist/src/rules/types.d.ts +0 -2
- package/dist/src/rules/utils.matter.js +1 -5
- package/dist/src/scrapper.d.ts +138 -25
- package/dist/src/scrapper.js +538 -160
- package/dist/src/stategraph/stategraph.d.ts +4 -0
- package/dist/src/stategraph/stategraph.js +16 -0
- package/dist/src/stategraph/types.d.ts +13 -1
- package/dist/src/types.d.ts +21 -0
- package/dist/src/utils.d.ts +24 -0
- package/dist/src/utils.js +84 -86
- package/package.json +3 -2
- package/dist/src/agents/semantic.d.ts +0 -4
- package/dist/src/agents/semantic.js +0 -19
- package/dist/src/execute/legacy.d.ts +0 -46
- package/dist/src/execute/legacy.js +0 -460
- package/dist/src/pricing.llm.d.ts +0 -5
- package/dist/src/pricing.llm.js +0 -14
|
@@ -41,6 +41,8 @@ exports.gitAllocateNextPRNumber = gitAllocateNextPRNumber;
|
|
|
41
41
|
exports.gitReloadIDRegistry = gitReloadIDRegistry;
|
|
42
42
|
exports.gitRegisterExistingID = gitRegisterExistingID;
|
|
43
43
|
exports.gitIDRegistryRename = gitIDRegistryRename;
|
|
44
|
+
exports.gitGetMatterCache = gitGetMatterCache;
|
|
45
|
+
exports.gitSetMatterCache = gitSetMatterCache;
|
|
44
46
|
exports.gitEnsureMatterID = gitEnsureMatterID;
|
|
45
47
|
exports.gitFileStrictMatter = gitFileStrictMatter;
|
|
46
48
|
exports.gitEnsureRepositoryConfiguration = gitEnsureRepositoryConfiguration;
|
|
@@ -51,8 +53,7 @@ exports.gitShowConfiguration = gitShowConfiguration;
|
|
|
51
53
|
exports.gitCheckConfiguration = gitCheckConfiguration;
|
|
52
54
|
exports.gitCreateOrEditFile = gitCreateOrEditFile;
|
|
53
55
|
exports.gitEditFile = gitEditFile;
|
|
54
|
-
exports.
|
|
55
|
-
exports.gitRenameFile_fs = gitRenameFile_fs;
|
|
56
|
+
exports.gitUpdateMatter = gitUpdateMatter;
|
|
56
57
|
exports.gitRenameFile = gitRenameFile;
|
|
57
58
|
exports.gitDeleteFile = gitDeleteFile;
|
|
58
59
|
exports.gitGetBranchHealth = gitGetBranchHealth;
|
|
@@ -63,6 +64,8 @@ const path_1 = require("path");
|
|
|
63
64
|
const fs = __importStar(require("fs/promises"));
|
|
64
65
|
const repo_tools_1 = require("./repo.tools");
|
|
65
66
|
const utils_matter_1 = require("../utils.matter");
|
|
67
|
+
const utils_slug_1 = require("../utils.slug");
|
|
68
|
+
const utils_1 = require("../../utils");
|
|
66
69
|
/**
|
|
67
70
|
* Service singleton pour gérer le registre d'IDs en mémoire
|
|
68
71
|
*/
|
|
@@ -83,6 +86,110 @@ class IDRegistryService {
|
|
|
83
86
|
this.repoPath = '';
|
|
84
87
|
this.isDirty = false;
|
|
85
88
|
}
|
|
89
|
+
createEmptyRegistry() {
|
|
90
|
+
return {
|
|
91
|
+
schemaVersion: 2,
|
|
92
|
+
last: 980,
|
|
93
|
+
lastPR: 60,
|
|
94
|
+
used: [],
|
|
95
|
+
updated: new Date().toISOString(),
|
|
96
|
+
matters: {}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
parseMatterKey(key) {
|
|
100
|
+
const separatorIndex = key.indexOf(':');
|
|
101
|
+
if (separatorIndex < 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
branch: key.slice(0, separatorIndex),
|
|
106
|
+
suffix: key.slice(separatorIndex + 1)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
createMatterKey(branch, id) {
|
|
110
|
+
return `${branch}:${id}`;
|
|
111
|
+
}
|
|
112
|
+
normalizeMatterCache(branch, keySuffix, rawMatter) {
|
|
113
|
+
if (!rawMatter || typeof rawMatter !== 'object') {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const id = typeof rawMatter.id === 'number' ? rawMatter.id : undefined;
|
|
117
|
+
if (!id || !Number.isFinite(id)) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const file = typeof rawMatter.file === 'string' && rawMatter.file.trim() !== ''
|
|
121
|
+
? rawMatter.file
|
|
122
|
+
: keySuffix;
|
|
123
|
+
const normalized = {
|
|
124
|
+
id,
|
|
125
|
+
file,
|
|
126
|
+
title: rawMatter.title,
|
|
127
|
+
service: rawMatter.service,
|
|
128
|
+
updated: typeof rawMatter.updated === 'string' ? rawMatter.updated : new Date().toISOString()
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
cacheKey: this.createMatterKey(branch, id),
|
|
132
|
+
cacheValue: normalized
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
mergeMatterCache(existing, incoming) {
|
|
136
|
+
if (!existing) {
|
|
137
|
+
return incoming;
|
|
138
|
+
}
|
|
139
|
+
const existingUpdated = Date.parse(existing.updated || '');
|
|
140
|
+
const incomingUpdated = Date.parse(incoming.updated || '');
|
|
141
|
+
const preferIncoming = !Number.isNaN(incomingUpdated) && (Number.isNaN(existingUpdated) || incomingUpdated >= existingUpdated);
|
|
142
|
+
const primary = preferIncoming ? incoming : existing;
|
|
143
|
+
const secondary = preferIncoming ? existing : incoming;
|
|
144
|
+
return {
|
|
145
|
+
id: primary.id ?? secondary.id,
|
|
146
|
+
file: primary.file || secondary.file,
|
|
147
|
+
title: primary.title ?? secondary.title,
|
|
148
|
+
service: primary.service ?? secondary.service,
|
|
149
|
+
updated: primary.updated || secondary.updated
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
migrateRegistry(data) {
|
|
153
|
+
const fallback = this.createEmptyRegistry();
|
|
154
|
+
const migrated = {
|
|
155
|
+
schemaVersion: 2,
|
|
156
|
+
last: Number.isInteger(data?.last) ? data.last : fallback.last,
|
|
157
|
+
lastPR: Number.isInteger(data?.lastPR) ? data.lastPR : fallback.lastPR,
|
|
158
|
+
used: Array.isArray(data?.used) ? data.used.filter((value) => typeof value === 'number') : [],
|
|
159
|
+
updated: typeof data?.updated === 'string' ? data.updated : fallback.updated,
|
|
160
|
+
matters: {}
|
|
161
|
+
};
|
|
162
|
+
let migrationApplied = data?.schemaVersion !== 2;
|
|
163
|
+
const rawMatters = data?.matters && typeof data.matters === 'object' ? data.matters : {};
|
|
164
|
+
for (const [rawKey, rawMatter] of Object.entries(rawMatters)) {
|
|
165
|
+
const parsed = this.parseMatterKey(rawKey);
|
|
166
|
+
if (!parsed) {
|
|
167
|
+
migrationApplied = true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const normalized = this.normalizeMatterCache(parsed.branch, parsed.suffix, rawMatter);
|
|
171
|
+
if (!normalized) {
|
|
172
|
+
migrationApplied = true;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (normalized.cacheKey !== rawKey) {
|
|
176
|
+
migrationApplied = true;
|
|
177
|
+
}
|
|
178
|
+
migrated.matters[normalized.cacheKey] = this.mergeMatterCache(migrated.matters[normalized.cacheKey], normalized.cacheValue);
|
|
179
|
+
}
|
|
180
|
+
const used = new Set(migrated.used);
|
|
181
|
+
for (const matter of Object.values(migrated.matters)) {
|
|
182
|
+
if (typeof matter.id === 'number') {
|
|
183
|
+
used.add(matter.id);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
migrated.used = Array.from(used).sort((left, right) => left - right);
|
|
187
|
+
migrated.last = Math.max(migrated.last, migrated.used.length ? migrated.used[migrated.used.length - 1] : fallback.last);
|
|
188
|
+
if (migrationApplied) {
|
|
189
|
+
this.isDirty = true;
|
|
190
|
+
}
|
|
191
|
+
return migrated;
|
|
192
|
+
}
|
|
86
193
|
/**
|
|
87
194
|
* Initialise le service avec le chemin du repository
|
|
88
195
|
*/
|
|
@@ -95,14 +202,12 @@ class IDRegistryService {
|
|
|
95
202
|
// Si le registre existe déjà, le recharger depuis le nouveau chemin
|
|
96
203
|
if (this.registry) {
|
|
97
204
|
this.registry = this.loadFromDisk();
|
|
98
|
-
this.isDirty = false;
|
|
99
205
|
return;
|
|
100
206
|
}
|
|
101
207
|
}
|
|
102
208
|
// Si le registre n'existe pas encore, le créer
|
|
103
209
|
if (!this.registry) {
|
|
104
210
|
this.registry = this.loadFromDisk();
|
|
105
|
-
this.isDirty = false;
|
|
106
211
|
}
|
|
107
212
|
}
|
|
108
213
|
/**
|
|
@@ -114,43 +219,24 @@ class IDRegistryService {
|
|
|
114
219
|
throw new Error('IDRegistryService not initialized. Call init() first.');
|
|
115
220
|
}
|
|
116
221
|
this.registry = this.loadFromDisk();
|
|
117
|
-
this.isDirty = false;
|
|
118
222
|
}
|
|
119
223
|
/**
|
|
120
224
|
* Charge le registre depuis le disque
|
|
121
225
|
*/
|
|
122
226
|
loadFromDisk() {
|
|
123
227
|
const registryPath = (0, path_1.join)(this.repoPath, '.git', 'with-ids.json');
|
|
228
|
+
this.isDirty = false;
|
|
124
229
|
if (!(0, fs_1.existsSync)(registryPath)) {
|
|
125
|
-
return
|
|
126
|
-
last: 980,
|
|
127
|
-
lastPR: 60,
|
|
128
|
-
used: [],
|
|
129
|
-
updated: new Date().toISOString(),
|
|
130
|
-
matters: {}
|
|
131
|
-
};
|
|
230
|
+
return this.createEmptyRegistry();
|
|
132
231
|
}
|
|
133
232
|
try {
|
|
134
233
|
const data = JSON.parse((0, fs_1.readFileSync)(registryPath, 'utf8'));
|
|
135
|
-
|
|
136
|
-
if (!data.matters) {
|
|
137
|
-
data.matters = {};
|
|
138
|
-
}
|
|
139
|
-
// Migration : compteur PR indépendant (valeur initiale explicite)
|
|
140
|
-
if (!Number.isInteger(data.lastPR)) {
|
|
141
|
-
data.lastPR = 60;
|
|
142
|
-
}
|
|
143
|
-
return data;
|
|
234
|
+
return this.migrateRegistry(data);
|
|
144
235
|
}
|
|
145
236
|
catch (error) {
|
|
146
237
|
console.warn('⚠️ Registre d\'IDs corrompu, création d\'un nouveau');
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
lastPR: 60,
|
|
150
|
-
used: [],
|
|
151
|
-
updated: new Date().toISOString(),
|
|
152
|
-
matters: {}
|
|
153
|
-
};
|
|
238
|
+
this.isDirty = false;
|
|
239
|
+
return this.createEmptyRegistry();
|
|
154
240
|
}
|
|
155
241
|
}
|
|
156
242
|
/**
|
|
@@ -241,14 +327,23 @@ class IDRegistryService {
|
|
|
241
327
|
if (!this.registry) {
|
|
242
328
|
return undefined;
|
|
243
329
|
}
|
|
244
|
-
// Chercher le fichier dans toutes les branches
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
330
|
+
// Chercher le fichier dans toutes les branches.
|
|
331
|
+
// Si plusieurs IDs existent (état incohérent), retourner un ID canonique déterministe
|
|
332
|
+
// pour éviter les oscillations entre redémarrages.
|
|
333
|
+
const ids = new Set();
|
|
334
|
+
for (const matter of Object.values(this.registry.matters)) {
|
|
335
|
+
if (matter.file === file && typeof matter.id === 'number') {
|
|
336
|
+
ids.add(matter.id);
|
|
249
337
|
}
|
|
250
338
|
}
|
|
251
|
-
|
|
339
|
+
if (ids.size === 0) {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
if (ids.size === 1) {
|
|
343
|
+
return [...ids][0];
|
|
344
|
+
}
|
|
345
|
+
// Politique d'arbitrage: conserver le plus petit ID observé pour ce fichier.
|
|
346
|
+
return Math.min(...ids);
|
|
252
347
|
}
|
|
253
348
|
/**
|
|
254
349
|
* Vérifie si un ID appartient déjà à un fichier spécifique
|
|
@@ -274,18 +369,14 @@ class IDRegistryService {
|
|
|
274
369
|
* @param id L'ID à chercher
|
|
275
370
|
* @returns Le nom du fichier qui possède cet ID, ou undefined
|
|
276
371
|
*/
|
|
277
|
-
getFileByID(id) {
|
|
372
|
+
getFileByID(branch, id) {
|
|
373
|
+
return this.getMatterCache(branch, id)?.file;
|
|
374
|
+
}
|
|
375
|
+
getMatterCachesByID(id) {
|
|
278
376
|
if (!this.registry) {
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
// Chercher l'ID dans le cache
|
|
282
|
-
for (const [cacheKey, matter] of Object.entries(this.registry.matters)) {
|
|
283
|
-
if (matter.id === id) {
|
|
284
|
-
const [, file] = cacheKey.split(':');
|
|
285
|
-
return file;
|
|
286
|
-
}
|
|
377
|
+
return [];
|
|
287
378
|
}
|
|
288
|
-
return
|
|
379
|
+
return Object.values(this.registry.matters).filter(matter => matter.id === id);
|
|
289
380
|
}
|
|
290
381
|
/**
|
|
291
382
|
* Enregistre un ID existant
|
|
@@ -327,23 +418,24 @@ class IDRegistryService {
|
|
|
327
418
|
* Récupère un matter depuis le cache
|
|
328
419
|
* Note: Le champ 'updated' est filtré pour ne pas être exposé à l'extérieur
|
|
329
420
|
*/
|
|
330
|
-
getMatterCache(branch,
|
|
421
|
+
getMatterCache(branch, id) {
|
|
331
422
|
if (!this.registry) {
|
|
332
423
|
throw new Error('IDRegistryService not initialized. Call init() first.');
|
|
333
424
|
}
|
|
334
|
-
const cacheKey =
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
425
|
+
const cacheKey = this.createMatterKey(branch, id);
|
|
426
|
+
return this.registry.matters[cacheKey];
|
|
427
|
+
}
|
|
428
|
+
getMatterCacheByFile(branch, file) {
|
|
429
|
+
if (!this.registry) {
|
|
430
|
+
throw new Error('IDRegistryService not initialized. Call init() first.');
|
|
338
431
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
};
|
|
432
|
+
for (const [cacheKey, matter] of Object.entries(this.registry.matters)) {
|
|
433
|
+
const parsed = this.parseMatterKey(cacheKey);
|
|
434
|
+
if (parsed?.branch === branch && matter.file === file) {
|
|
435
|
+
return matter;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return undefined;
|
|
347
439
|
}
|
|
348
440
|
/**
|
|
349
441
|
* Met à jour le cache d'un matter
|
|
@@ -352,13 +444,17 @@ class IDRegistryService {
|
|
|
352
444
|
if (!this.registry) {
|
|
353
445
|
throw new Error('IDRegistryService not initialized. Call init() first.');
|
|
354
446
|
}
|
|
355
|
-
|
|
447
|
+
if (!matter.id || !Number.isFinite(matter.id)) {
|
|
448
|
+
this.deleteMatterCacheByFile(branch, file);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
this.deleteMatterCacheByFile(branch, file);
|
|
452
|
+
const cacheKey = this.createMatterKey(branch, matter.id);
|
|
356
453
|
this.registry.matters[cacheKey] = {
|
|
357
454
|
id: matter.id,
|
|
455
|
+
file,
|
|
358
456
|
title: matter.title,
|
|
359
457
|
service: matter.service,
|
|
360
|
-
// FIXME(oldfile): compat temporaire; à retirer quand le client ne dépend plus de ce signal.
|
|
361
|
-
oldfile: matter.oldfile,
|
|
362
458
|
updated: new Date().toISOString()
|
|
363
459
|
};
|
|
364
460
|
this.isDirty = true;
|
|
@@ -366,14 +462,21 @@ class IDRegistryService {
|
|
|
366
462
|
/**
|
|
367
463
|
* Supprime une entrée du cache
|
|
368
464
|
*/
|
|
369
|
-
deleteMatterCache(branch,
|
|
465
|
+
deleteMatterCache(branch, id) {
|
|
370
466
|
if (!this.registry) {
|
|
371
467
|
throw new Error('IDRegistryService not initialized. Call init() first.');
|
|
372
468
|
}
|
|
373
|
-
const cacheKey =
|
|
469
|
+
const cacheKey = this.createMatterKey(branch, id);
|
|
374
470
|
delete this.registry.matters[cacheKey];
|
|
375
471
|
this.isDirty = true;
|
|
376
472
|
}
|
|
473
|
+
deleteMatterCacheByFile(branch, file) {
|
|
474
|
+
const cached = this.getMatterCacheByFile(branch, file);
|
|
475
|
+
if (!cached?.id) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
this.deleteMatterCache(branch, cached.id);
|
|
479
|
+
}
|
|
377
480
|
/**
|
|
378
481
|
* Renomme une entrée dans le cache du registre
|
|
379
482
|
*
|
|
@@ -392,27 +495,18 @@ class IDRegistryService {
|
|
|
392
495
|
* idRegistryService.rename('old-name.md', 'new-name.md', 'rule-validation-1');
|
|
393
496
|
* ```
|
|
394
497
|
*/
|
|
395
|
-
rename(
|
|
498
|
+
rename(id, newFile, branch) {
|
|
396
499
|
if (!this.registry) {
|
|
397
500
|
throw new Error('IDRegistryService not initialized. Call init() first.');
|
|
398
501
|
}
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
// Récupérer le cache existant
|
|
402
|
-
const cached = this.registry.matters[oldKey];
|
|
502
|
+
const cacheKey = this.createMatterKey(branch, id);
|
|
503
|
+
const cached = this.registry.matters[cacheKey];
|
|
403
504
|
if (!cached) {
|
|
404
|
-
// Pas de cache pour l'ancien fichier, rien à renommer
|
|
405
505
|
return;
|
|
406
506
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
// Créer la nouvelle clé avec les mêmes données
|
|
411
|
-
// ⚠️ NE PAS inclure oldfile - c'est temporaire uniquement pour notification client
|
|
412
|
-
this.registry.matters[newKey] = {
|
|
413
|
-
id: cached.id,
|
|
414
|
-
title: cached.title,
|
|
415
|
-
service: cached.service,
|
|
507
|
+
this.registry.matters[cacheKey] = {
|
|
508
|
+
...cached,
|
|
509
|
+
file: newFile,
|
|
416
510
|
updated: new Date().toISOString()
|
|
417
511
|
};
|
|
418
512
|
this.isDirty = true;
|
|
@@ -552,6 +646,9 @@ function gitReloadIDRegistry(config) {
|
|
|
552
646
|
/**
|
|
553
647
|
* Enregistre un ID existant dans le registre
|
|
554
648
|
*
|
|
649
|
+
* @deprecated Préférer `gitCreateOrEditFile(...)`, `gitUpdateMatter(...)` ou
|
|
650
|
+
* `gitRenameFile(...)` qui synchronisent le registre dans le même flux documentaire.
|
|
651
|
+
*
|
|
555
652
|
* @param matter Le matter contenant l'ID à enregistrer
|
|
556
653
|
* @param branch Branche du fichier (optionnel, pour vérification de propriété)
|
|
557
654
|
* @param file Nom du fichier (optionnel, pour vérification de propriété)
|
|
@@ -575,8 +672,9 @@ function gitRegisterExistingID(matter, branch, file, config) {
|
|
|
575
672
|
// Si l'ID existe déjà, vérifier qu'il appartient au MÊME fichier
|
|
576
673
|
const registry = idRegistryService.getRegistry();
|
|
577
674
|
if (registry.used.includes(matter.id)) {
|
|
578
|
-
const
|
|
579
|
-
|
|
675
|
+
const conflictingOwner = idRegistryService.getMatterCachesByID(matter.id)
|
|
676
|
+
.find(owner => owner.file !== file);
|
|
677
|
+
if (conflictingOwner?.file) {
|
|
580
678
|
// ID utilisé par un AUTRE fichier → Erreur
|
|
581
679
|
const error = new Error(`ID ${matter.id} est déjà utilisé`);
|
|
582
680
|
error.code = 'id_already_used';
|
|
@@ -595,6 +693,8 @@ function gitRegisterExistingID(matter, branch, file, config) {
|
|
|
595
693
|
/**
|
|
596
694
|
* Renomme un fichier dans le cache du registre d'IDs
|
|
597
695
|
*
|
|
696
|
+
* @deprecated Le cache de matter/ID est maintenant mis à jour par `gitRenameFile(...)`.
|
|
697
|
+
*
|
|
598
698
|
* Cette fonction met à jour le cache lorsqu'un fichier est renommé:
|
|
599
699
|
* - Supprime l'entrée avec l'ancien nom
|
|
600
700
|
* - Crée une nouvelle entrée avec le nouveau nom
|
|
@@ -614,7 +714,102 @@ function gitRegisterExistingID(matter, branch, file, config) {
|
|
|
614
714
|
function gitIDRegistryRename(oldFile, newFile, branch, config) {
|
|
615
715
|
const gitConf = (0, repo_tools_1.gitLoad)(config);
|
|
616
716
|
idRegistryService.init(gitConf.repoPath);
|
|
617
|
-
idRegistryService.
|
|
717
|
+
const cached = idRegistryService.getMatterCacheByFile(branch, oldFile);
|
|
718
|
+
if (!cached?.id) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
idRegistryService.rename(cached.id, newFile, branch);
|
|
722
|
+
idRegistryService.save();
|
|
723
|
+
}
|
|
724
|
+
function gitGetMatterCache(branch, id, config) {
|
|
725
|
+
const gitConf = (0, repo_tools_1.gitLoad)(config);
|
|
726
|
+
idRegistryService.init(gitConf.repoPath);
|
|
727
|
+
const cached = idRegistryService.getMatterCache(branch, id);
|
|
728
|
+
if (!cached) {
|
|
729
|
+
return undefined;
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
id: cached.id,
|
|
733
|
+
file: cached.file,
|
|
734
|
+
title: cached.title,
|
|
735
|
+
service: cached.service
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function gitSetMatterCache(branch, file, matter, config) {
|
|
739
|
+
const gitConf = (0, repo_tools_1.gitLoad)(config);
|
|
740
|
+
idRegistryService.init(gitConf.repoPath);
|
|
741
|
+
idRegistryService.setMatterCache(branch, file, matter);
|
|
742
|
+
idRegistryService.save();
|
|
743
|
+
}
|
|
744
|
+
function isRulesGitConfig(value) {
|
|
745
|
+
return !!value
|
|
746
|
+
&& typeof value === 'object'
|
|
747
|
+
&& 'instance' in value
|
|
748
|
+
&& 'repoPath' in value
|
|
749
|
+
&& 'mainBranch' in value;
|
|
750
|
+
}
|
|
751
|
+
function resolveDocumentMutationOptions(options) {
|
|
752
|
+
if (isRulesGitConfig(options)) {
|
|
753
|
+
return { config: options };
|
|
754
|
+
}
|
|
755
|
+
return options ?? {};
|
|
756
|
+
}
|
|
757
|
+
function buildDocumentMutationContent(currentContent, nextContent, matterPatch) {
|
|
758
|
+
const currentParsed = (0, utils_matter_1.matterParse)(currentContent);
|
|
759
|
+
const hasMatterPatch = !!matterPatch && Object.keys(matterPatch).length > 0;
|
|
760
|
+
if (!nextContent && !hasMatterPatch) {
|
|
761
|
+
return {
|
|
762
|
+
content: currentContent,
|
|
763
|
+
matter: currentParsed.matter
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
const candidateParsed = nextContent ? (0, utils_matter_1.matterParse)(nextContent) : currentParsed;
|
|
767
|
+
const matter = {
|
|
768
|
+
...currentParsed.matter,
|
|
769
|
+
...candidateParsed.matter,
|
|
770
|
+
...(matterPatch ?? {})
|
|
771
|
+
};
|
|
772
|
+
return {
|
|
773
|
+
content: (0, utils_matter_1.matterSerialize)(candidateParsed.content, matter),
|
|
774
|
+
matter
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function assertUpdateMatterDoesNotRequireRename(filePath, matterPatch, finalMatter) {
|
|
778
|
+
if (!matterPatch || !Object.prototype.hasOwnProperty.call(matterPatch, 'title')) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const nextTitle = finalMatter.title?.trim();
|
|
782
|
+
if (!nextTitle) {
|
|
783
|
+
throw new errors_1.GitOperationError('Le matter doit contenir un champ "title" de type string', 'missing_title', { filePath, matter: finalMatter });
|
|
784
|
+
}
|
|
785
|
+
const currentSlug = (0, utils_slug_1.slugFromFile)(filePath);
|
|
786
|
+
const nextSlug = (0, utils_1.toSlug)(nextTitle);
|
|
787
|
+
if (nextSlug !== currentSlug) {
|
|
788
|
+
throw new errors_1.GitOperationError(`La mutation du matter nécessite un rename explicite: ${filePath} -> ${nextSlug}.md`, 'rename_required', { filePath, currentSlug, nextSlug, title: nextTitle });
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function assertRenameMatterMatchesFile(newFileName, finalMatter) {
|
|
792
|
+
const nextTitle = finalMatter.title?.trim();
|
|
793
|
+
if (!nextTitle) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const expectedSlug = (0, utils_1.toSlug)(nextTitle);
|
|
797
|
+
const targetSlug = (0, utils_slug_1.slugFromFile)(newFileName);
|
|
798
|
+
if (expectedSlug !== targetSlug) {
|
|
799
|
+
throw new errors_1.GitOperationError(`Le titre final "${nextTitle}" ne correspond pas au fichier "${newFileName}"`, 'rename_matter_mismatch', { newFileName, expectedSlug, targetSlug, title: nextTitle });
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function syncMatterCacheAfterRename(branch, oldFileName, newFileName, matter, config) {
|
|
803
|
+
const gitConf = (0, repo_tools_1.gitLoad)(config);
|
|
804
|
+
idRegistryService.init(gitConf.repoPath);
|
|
805
|
+
idRegistryService.deleteMatterCacheByFile(branch, oldFileName);
|
|
806
|
+
if (typeof matter.id === 'number' && Number.isFinite(matter.id)) {
|
|
807
|
+
idRegistryService.setMatterCache(branch, newFileName, {
|
|
808
|
+
id: matter.id,
|
|
809
|
+
title: matter.title,
|
|
810
|
+
service: matter.service
|
|
811
|
+
});
|
|
812
|
+
}
|
|
618
813
|
idRegistryService.save();
|
|
619
814
|
}
|
|
620
815
|
/**
|
|
@@ -636,17 +831,21 @@ function gitEnsureMatterID(matter, config, branch, file) {
|
|
|
636
831
|
// ✅ NOUVEAU: Chercher d'abord l'ID existant du fichier (peu importe la branche)
|
|
637
832
|
const existingFileID = file ? idRegistryService.getFileID(file) : undefined;
|
|
638
833
|
if (existingFileID) {
|
|
639
|
-
// Le fichier a déjà un ID
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
if (
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
834
|
+
// Le fichier a déjà un ID candidat dans le cache.
|
|
835
|
+
// Ne le réutiliser que s'il est bien arbitré pour ce fichier.
|
|
836
|
+
const owner = branch ? idRegistryService.getMatterCache(branch, existingFileID) : undefined;
|
|
837
|
+
if (!owner?.file || owner.file === file) {
|
|
838
|
+
matter.id = existingFileID;
|
|
839
|
+
// Mettre à jour le cache si branch est définie (sinon ce sera fait plus tard)
|
|
840
|
+
if (branch && file && matter.title) {
|
|
841
|
+
idRegistryService.setMatterCache(branch, file, {
|
|
842
|
+
id: matter.id,
|
|
843
|
+
title: matter.title,
|
|
844
|
+
service: matter.service
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
return matter;
|
|
648
848
|
}
|
|
649
|
-
return matter;
|
|
650
849
|
}
|
|
651
850
|
// Si matter a un ID fourni manuellement (ex: import ou test)
|
|
652
851
|
// FIXME 999 should be a constant in config
|
|
@@ -703,7 +902,7 @@ function validateMatter(matter) {
|
|
|
703
902
|
*/
|
|
704
903
|
function extractMatterFromContent(content) {
|
|
705
904
|
if (!content) {
|
|
706
|
-
return { id: 0, title: undefined, service: undefined
|
|
905
|
+
return { id: 0, title: undefined, service: undefined };
|
|
707
906
|
}
|
|
708
907
|
// Regex pour extraire id (format: "id: 1234" ou "id: '1234'")
|
|
709
908
|
const idMatch = content.match(/^id:\s*['"]?(\d+)['"]?/m);
|
|
@@ -713,11 +912,7 @@ function extractMatterFromContent(content) {
|
|
|
713
912
|
const title = titleMatch ? titleMatch[1].trim() : undefined;
|
|
714
913
|
const serviceMatch = content.match(/^service:\s*["']?([^"'\n]+)["']?/m);
|
|
715
914
|
const service = serviceMatch ? serviceMatch[1].trim() : undefined;
|
|
716
|
-
|
|
717
|
-
// FIXME(oldfile): extraction legacy pour compat UI rename; migration cible: ID-only.
|
|
718
|
-
const oldfileMatch = content.match(/^oldfile:\s*["']?([^"'\n]+)["']?/m);
|
|
719
|
-
const oldfile = oldfileMatch ? oldfileMatch[1].trim() : undefined;
|
|
720
|
-
return { id, title, service, oldfile };
|
|
915
|
+
return { id, title, service };
|
|
721
916
|
}
|
|
722
917
|
/**
|
|
723
918
|
* Lecture rapide et stricte du matter d'un fichier (id + title uniquement)
|
|
@@ -747,19 +942,18 @@ async function gitFileStrictMatter(git, filePath, branch, config) {
|
|
|
747
942
|
idRegistryService.init(gitConf.repoPath);
|
|
748
943
|
try {
|
|
749
944
|
// ✅ ÉTAPE 1: Vérifier le cache
|
|
750
|
-
const cached = idRegistryService.
|
|
945
|
+
const cached = idRegistryService.getMatterCacheByFile(branch, filePath);
|
|
751
946
|
if (cached) {
|
|
752
947
|
// Cache trouvé, retourner directement
|
|
753
948
|
return {
|
|
754
949
|
id: cached.id,
|
|
755
|
-
title: cached.title
|
|
756
|
-
oldfile: cached.oldfile // ✅ Inclure oldfile temporaire
|
|
950
|
+
title: cached.title
|
|
757
951
|
};
|
|
758
952
|
}
|
|
759
953
|
// ❌ ÉTAPE 2: Cache absent, lire le fichier
|
|
760
954
|
const content = await git.show([`${branch}:${filePath}`]);
|
|
761
955
|
if (!content) {
|
|
762
|
-
return { id: undefined, title: undefined
|
|
956
|
+
return { id: undefined, title: undefined };
|
|
763
957
|
}
|
|
764
958
|
// ✅ ÉTAPE 3: Extraction rapide avec regex (sans matterParse complet)
|
|
765
959
|
const matter = extractMatterFromContent(content);
|
|
@@ -772,7 +966,7 @@ async function gitFileStrictMatter(git, filePath, branch, config) {
|
|
|
772
966
|
if (gitConf.verbose) {
|
|
773
967
|
console.warn(`⚠️ Erreur lecture matter de ${filePath}:`, error);
|
|
774
968
|
}
|
|
775
|
-
return { id: undefined, title: undefined
|
|
969
|
+
return { id: undefined, title: undefined };
|
|
776
970
|
}
|
|
777
971
|
}
|
|
778
972
|
/**
|
|
@@ -1207,8 +1401,8 @@ async function gitCreateOrEditFile(git, filePath, PR, content, user, config) {
|
|
|
1207
1401
|
idRegistryService.init(gitConf.repoPath);
|
|
1208
1402
|
idRegistryService.setMatterCache(PR, filePath, {
|
|
1209
1403
|
id: parsed.matter.id,
|
|
1210
|
-
title: parsed.matter.title
|
|
1211
|
-
|
|
1404
|
+
title: parsed.matter.title,
|
|
1405
|
+
service: parsed.matter.service
|
|
1212
1406
|
});
|
|
1213
1407
|
idRegistryService.save();
|
|
1214
1408
|
}
|
|
@@ -1232,8 +1426,18 @@ async function gitCreateOrEditFile(git, filePath, PR, content, user, config) {
|
|
|
1232
1426
|
}
|
|
1233
1427
|
// Determine the current head after the commit attempt.
|
|
1234
1428
|
const newHead = (await git.revparse(['HEAD'])).trim();
|
|
1235
|
-
|
|
1236
|
-
|
|
1429
|
+
// IMPORTANT:
|
|
1430
|
+
// Le scope d'une PR doit rester le delta métier déclaré (fichiers déjà trackés + fichier courant),
|
|
1431
|
+
// pas un recalcul global de diff qui peut gonfler metadata.files si mergeBase est absent/ancien.
|
|
1432
|
+
const previousFiles = Array.isArray(oldNote.files) ? oldNote.files : [];
|
|
1433
|
+
const mergedFiles = [...previousFiles, filePath];
|
|
1434
|
+
let newFiles = await sanitizePRFiles(git, mergedFiles, PR);
|
|
1435
|
+
// Fallback de compat pour anciennes notes sans files
|
|
1436
|
+
if (newFiles.length === 0) {
|
|
1437
|
+
const safeBase = oldNote.mergeBase || gitConf.draftBranch;
|
|
1438
|
+
newFiles = await (0, repo_tools_1.gitGetDiffFiles)(git, PR, safeBase);
|
|
1439
|
+
newFiles = await sanitizePRFiles(git, newFiles, PR);
|
|
1440
|
+
}
|
|
1237
1441
|
const updatedNote = {
|
|
1238
1442
|
...oldNote,
|
|
1239
1443
|
files: newFiles,
|
|
@@ -1296,7 +1500,57 @@ async function gitEditFile(git, filePath, PR, content, user, config) {
|
|
|
1296
1500
|
}
|
|
1297
1501
|
return await gitCreateOrEditFile(git, filePath, PR, content, user, config);
|
|
1298
1502
|
}
|
|
1299
|
-
|
|
1503
|
+
/**
|
|
1504
|
+
* Met à jour le front-matter d'un document sans changer son nom de fichier.
|
|
1505
|
+
*
|
|
1506
|
+
* Cette API refuse explicitement toute mutation du `title` qui nécessiterait
|
|
1507
|
+
* un changement de slug / filename. Dans ce cas, utiliser `gitRenameFile(...)`.
|
|
1508
|
+
*/
|
|
1509
|
+
async function gitUpdateMatter(git, filePath, branch, user, options) {
|
|
1510
|
+
const resolvedOptions = resolveDocumentMutationOptions(options);
|
|
1511
|
+
const gitConf = (0, repo_tools_1.gitLoad)(resolvedOptions.config);
|
|
1512
|
+
let currentContent;
|
|
1513
|
+
if (branch.toLowerCase() === 'new') {
|
|
1514
|
+
if (!gitConf.uploadPath) {
|
|
1515
|
+
throw new errors_1.GitOperationError('uploadPath is required for NEW document mutations', 'missing_upload_path', { filePath, branch });
|
|
1516
|
+
}
|
|
1517
|
+
const fullPath = (0, path_1.join)(gitConf.uploadPath, filePath);
|
|
1518
|
+
if (!(0, fs_1.existsSync)(fullPath)) {
|
|
1519
|
+
throw new errors_1.FileNotFoundError(filePath, branch);
|
|
1520
|
+
}
|
|
1521
|
+
currentContent = await fs.readFile(fullPath, 'utf8');
|
|
1522
|
+
}
|
|
1523
|
+
else {
|
|
1524
|
+
const currentDocument = await (0, repo_tools_1.gitGetFileContent)(git, filePath, branch);
|
|
1525
|
+
currentContent = currentDocument?.content;
|
|
1526
|
+
}
|
|
1527
|
+
if (!currentContent) {
|
|
1528
|
+
throw new errors_1.FileNotFoundError(filePath, branch);
|
|
1529
|
+
}
|
|
1530
|
+
const finalDocument = buildDocumentMutationContent(currentContent, undefined, resolvedOptions.matter);
|
|
1531
|
+
assertUpdateMatterDoesNotRequireRename(filePath, resolvedOptions.matter, finalDocument.matter);
|
|
1532
|
+
if (branch.toLowerCase() === 'new') {
|
|
1533
|
+
const fullPath = (0, path_1.join)(gitConf.uploadPath, filePath);
|
|
1534
|
+
await fs.writeFile(fullPath, finalDocument.content, { flag: 'w', encoding: 'utf8' });
|
|
1535
|
+
return {
|
|
1536
|
+
hash: '',
|
|
1537
|
+
date: new Date(),
|
|
1538
|
+
message: `update matter: ${filePath}`,
|
|
1539
|
+
author: {
|
|
1540
|
+
name: user.name,
|
|
1541
|
+
email: user.email
|
|
1542
|
+
},
|
|
1543
|
+
branch,
|
|
1544
|
+
content: finalDocument.content
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
const commit = await gitCreateOrEditFile(git, filePath, branch, finalDocument.content, user, gitConf);
|
|
1548
|
+
return {
|
|
1549
|
+
...commit,
|
|
1550
|
+
content: finalDocument.content
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
async function gitRenameFile_git(git, oldFileName, newFileName, branch, user, finalContent, config) {
|
|
1300
1554
|
// Si les noms sont identiques, pas besoin de renommer
|
|
1301
1555
|
if (oldFileName === newFileName) {
|
|
1302
1556
|
return {
|
|
@@ -1324,6 +1578,12 @@ async function gitRenameFile_git(git, oldFileName, newFileName, branch, user, co
|
|
|
1324
1578
|
await git.checkout(branch);
|
|
1325
1579
|
// Rename file with git mv (atomique dans Git)
|
|
1326
1580
|
await git.raw(['mv', oldFileName, newFileName]);
|
|
1581
|
+
if (typeof finalContent === 'string') {
|
|
1582
|
+
const fullNewPath = (0, path_1.join)(gitConf.repoPath, newFileName);
|
|
1583
|
+
await fs.mkdir((0, path_1.dirname)(fullNewPath), { recursive: true });
|
|
1584
|
+
await fs.writeFile(fullNewPath, finalContent, { flag: 'w', encoding: 'utf8' });
|
|
1585
|
+
await git.add(newFileName);
|
|
1586
|
+
}
|
|
1327
1587
|
// Commit - git mv automatically stages changes
|
|
1328
1588
|
const commit = await git.commit(`rename: ${oldFileName} → ${newFileName}`, {
|
|
1329
1589
|
'--author': `${user.name} <${user.email}>`
|
|
@@ -1336,17 +1596,11 @@ async function gitRenameFile_git(git, oldFileName, newFileName, branch, user, co
|
|
|
1336
1596
|
name: user.name,
|
|
1337
1597
|
email: user.email
|
|
1338
1598
|
},
|
|
1339
|
-
branch: branch
|
|
1599
|
+
branch: branch,
|
|
1600
|
+
content: finalContent
|
|
1340
1601
|
};
|
|
1341
1602
|
}
|
|
1342
1603
|
catch (error) {
|
|
1343
|
-
// En cas d'erreur, essayer de restaurer l'état initial
|
|
1344
|
-
try {
|
|
1345
|
-
await git.reset(['--hard']);
|
|
1346
|
-
}
|
|
1347
|
-
catch (resetError) {
|
|
1348
|
-
console.warn('Could not reset after failed rename:', resetError);
|
|
1349
|
-
}
|
|
1350
1604
|
throw error;
|
|
1351
1605
|
}
|
|
1352
1606
|
finally {
|
|
@@ -1356,7 +1610,7 @@ async function gitRenameFile_git(git, oldFileName, newFileName, branch, user, co
|
|
|
1356
1610
|
(0, repo_tools_1.unlock)(`checkout`);
|
|
1357
1611
|
}
|
|
1358
1612
|
}
|
|
1359
|
-
async function gitRenameFile_fs(git, oldFileName, newFileName, branch, user, config) {
|
|
1613
|
+
async function gitRenameFile_fs(git, oldFileName, newFileName, branch, user, finalContent, config) {
|
|
1360
1614
|
// Si les noms sont identiques, pas besoin de renommer
|
|
1361
1615
|
if (oldFileName === newFileName) {
|
|
1362
1616
|
return {
|
|
@@ -1370,6 +1624,9 @@ async function gitRenameFile_fs(git, oldFileName, newFileName, branch, user, con
|
|
|
1370
1624
|
const gitConf = (0, repo_tools_1.gitLoad)(config);
|
|
1371
1625
|
try {
|
|
1372
1626
|
await (0, repo_tools_1.lock)(`checkout`);
|
|
1627
|
+
if (!gitConf.uploadPath) {
|
|
1628
|
+
throw new errors_1.GitOperationError('uploadPath is required for filesystem rename', 'missing_upload_path', { oldFileName, newFileName, branch });
|
|
1629
|
+
}
|
|
1373
1630
|
const fullNewPath = (0, path_1.join)(gitConf.uploadPath, newFileName);
|
|
1374
1631
|
const fullOldPath = (0, path_1.join)(gitConf.uploadPath, oldFileName);
|
|
1375
1632
|
// Vérifier que l'ancien fichier existe
|
|
@@ -1383,10 +1640,13 @@ async function gitRenameFile_fs(git, oldFileName, newFileName, branch, user, con
|
|
|
1383
1640
|
throw new Error(`Le fichier de destination "${newFileName}" existe déjà sur le système de fichiers`);
|
|
1384
1641
|
}
|
|
1385
1642
|
// Renommage atomique filesystem
|
|
1386
|
-
// Utiliser copyFile + unlink pour plus de sécurité que rename() sur certains systèmes
|
|
1387
1643
|
try {
|
|
1644
|
+
await fs.mkdir((0, path_1.dirname)(fullNewPath), { recursive: true });
|
|
1388
1645
|
await fs.copyFile(fullOldPath, fullNewPath);
|
|
1389
1646
|
await fs.unlink(fullOldPath);
|
|
1647
|
+
if (typeof finalContent === 'string') {
|
|
1648
|
+
await fs.writeFile(fullNewPath, finalContent, { flag: 'w', encoding: 'utf8' });
|
|
1649
|
+
}
|
|
1390
1650
|
}
|
|
1391
1651
|
catch (copyError) {
|
|
1392
1652
|
// En cas d'erreur, nettoyer le nouveau fichier s'il a été créé
|
|
@@ -1408,7 +1668,8 @@ async function gitRenameFile_fs(git, oldFileName, newFileName, branch, user, con
|
|
|
1408
1668
|
name: user.name,
|
|
1409
1669
|
email: user.email
|
|
1410
1670
|
},
|
|
1411
|
-
branch: branch
|
|
1671
|
+
branch: branch,
|
|
1672
|
+
content: finalContent
|
|
1412
1673
|
};
|
|
1413
1674
|
}
|
|
1414
1675
|
catch (error) {
|
|
@@ -1428,7 +1689,7 @@ async function gitRenameFile_fs(git, oldFileName, newFileName, branch, user, con
|
|
|
1428
1689
|
* @param config Configuration Git optionnelle
|
|
1429
1690
|
* @returns Historique du commit de renommage
|
|
1430
1691
|
*/
|
|
1431
|
-
async function gitRenameFile(git, oldFileName, newFileName, branch, user,
|
|
1692
|
+
async function gitRenameFile(git, oldFileName, newFileName, branch, user, options, nextContent) {
|
|
1432
1693
|
// Si les noms sont identiques, pas besoin de renommer
|
|
1433
1694
|
if (oldFileName === newFileName) {
|
|
1434
1695
|
return {
|
|
@@ -1439,37 +1700,44 @@ async function gitRenameFile(git, oldFileName, newFileName, branch, user, config
|
|
|
1439
1700
|
branch: branch
|
|
1440
1701
|
};
|
|
1441
1702
|
}
|
|
1442
|
-
|
|
1443
|
-
const gitConf = (0, repo_tools_1.gitLoad)(config);
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1703
|
+
const resolvedOptions = resolveDocumentMutationOptions(options);
|
|
1704
|
+
const gitConf = (0, repo_tools_1.gitLoad)(resolvedOptions.config);
|
|
1705
|
+
if (branch.toLowerCase() === 'new' && !gitConf.uploadPath) {
|
|
1706
|
+
throw new errors_1.GitOperationError('uploadPath is required for NEW renames', 'missing_upload_path', { oldFileName, newFileName, branch });
|
|
1707
|
+
}
|
|
1708
|
+
const fullOldPath = gitConf.uploadPath ? (0, path_1.join)(gitConf.uploadPath, oldFileName) : '';
|
|
1709
|
+
const useFilesystem = branch.toLowerCase() === 'new'
|
|
1710
|
+
|| (!!gitConf.uploadPath && (0, fs_1.existsSync)(fullOldPath) && !(await (0, repo_tools_1.gitFileExistsInBranch)(git, oldFileName, branch)));
|
|
1711
|
+
const currentContent = useFilesystem
|
|
1712
|
+
? await fs.readFile(fullOldPath, 'utf8')
|
|
1713
|
+
: (await (0, repo_tools_1.gitGetFileContent)(git, oldFileName, branch))?.content;
|
|
1714
|
+
if (!currentContent) {
|
|
1715
|
+
throw new errors_1.FileNotFoundError(oldFileName, branch);
|
|
1716
|
+
}
|
|
1717
|
+
const finalDocument = buildDocumentMutationContent(currentContent, nextContent, resolvedOptions.matter);
|
|
1718
|
+
assertRenameMatterMatchesFile(newFileName, finalDocument.matter);
|
|
1719
|
+
let result;
|
|
1720
|
+
if (useFilesystem) {
|
|
1721
|
+
result = await gitRenameFile_fs(git, oldFileName, newFileName, branch, user, finalDocument.content, gitConf);
|
|
1722
|
+
}
|
|
1723
|
+
else {
|
|
1724
|
+
result = await gitRenameFile_git(git, oldFileName, newFileName, branch, user, finalDocument.content, gitConf);
|
|
1725
|
+
if (branch.startsWith(gitConf.validationPrefix)) {
|
|
1726
|
+
try {
|
|
1727
|
+
await gitPRReplaceFile(git, branch, { remove: oldFileName, add: newFileName }, gitConf);
|
|
1728
|
+
}
|
|
1729
|
+
catch (metadataError) {
|
|
1730
|
+
if (gitConf.verbose) {
|
|
1731
|
+
console.warn('⚠️ Failed to update PR metadata after rename:', metadataError);
|
|
1463
1732
|
}
|
|
1464
1733
|
}
|
|
1465
1734
|
}
|
|
1466
|
-
|
|
1467
|
-
}
|
|
1468
|
-
catch (error) {
|
|
1469
|
-
// Pour les vraies branches Git, ne pas faire de fallback filesystem
|
|
1470
|
-
// Seules les branches NEW utilisent le filesystem
|
|
1471
|
-
throw error;
|
|
1735
|
+
syncMatterCacheAfterRename(branch, oldFileName, newFileName, finalDocument.matter, gitConf);
|
|
1472
1736
|
}
|
|
1737
|
+
return {
|
|
1738
|
+
...result,
|
|
1739
|
+
content: finalDocument.content
|
|
1740
|
+
};
|
|
1473
1741
|
}
|
|
1474
1742
|
/**
|
|
1475
1743
|
* Met à jour la liste des fichiers d'une PR après un renommage.
|
|
@@ -1555,7 +1823,18 @@ async function gitDeleteFile(git, filePath, branch, user, config) {
|
|
|
1555
1823
|
// Mettre à jour la note si elle existe (pour les branches PR)
|
|
1556
1824
|
if (oldNote) {
|
|
1557
1825
|
const newHead = (await git.revparse(['HEAD'])).trim();
|
|
1558
|
-
|
|
1826
|
+
// IMPORTANT:
|
|
1827
|
+
// Conserver metadata.files piloté par les opérations métier explicites.
|
|
1828
|
+
// Ici: suppression => on retire seulement le fichier supprimé, puis on sanitize.
|
|
1829
|
+
const previousFiles = Array.isArray(oldNote.files) ? oldNote.files : [];
|
|
1830
|
+
const mergedFiles = previousFiles.filter((f) => f !== filePath);
|
|
1831
|
+
let newFiles = await sanitizePRFiles(git, mergedFiles, branch);
|
|
1832
|
+
// Fallback de compat pour anciennes notes sans files
|
|
1833
|
+
if (newFiles.length === 0 && previousFiles.length === 0) {
|
|
1834
|
+
const safeBase = oldNote.mergeBase || gitConf.draftBranch;
|
|
1835
|
+
newFiles = await (0, repo_tools_1.gitGetDiffFiles)(git, branch, safeBase);
|
|
1836
|
+
newFiles = await sanitizePRFiles(git, newFiles, branch);
|
|
1837
|
+
}
|
|
1559
1838
|
const updatedNote = {
|
|
1560
1839
|
...oldNote,
|
|
1561
1840
|
files: newFiles,
|
|
@@ -1847,6 +2126,11 @@ async function checkMergeInProgress(repoPath) {
|
|
|
1847
2126
|
return mergeFiles.some(file => (0, fs_1.existsSync)((0, path_1.join)(gitDir, file)));
|
|
1848
2127
|
}
|
|
1849
2128
|
async function sanitizePRFiles(git, files, branch) {
|
|
2129
|
+
// TODO(PR scope): Clarifier le besoin métier.
|
|
2130
|
+
// - Cette fonction valide seulement "existe dans la branche" + déduplication.
|
|
2131
|
+
// - Sur une branche créée depuis rule-editor (checkout -b), presque tous les fichiers existent déjà.
|
|
2132
|
+
// - Donc ce helper NE peut PAS, à lui seul, déterminer le vrai périmètre PR.
|
|
2133
|
+
// Source de vérité attendue: metadata.files maintenu par opérations explicites (add/edit/rename/delete).
|
|
1850
2134
|
if (!files || files.length === 0) {
|
|
1851
2135
|
return [];
|
|
1852
2136
|
}
|