agentic-api 2.0.646 → 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 +21 -118
- package/dist/src/agents/reducer.loaders.d.ts +103 -1
- package/dist/src/agents/reducer.loaders.js +164 -2
- package/dist/src/agents/reducer.types.d.ts +34 -3
- package/dist/src/agents/simulator.d.ts +32 -2
- package/dist/src/agents/simulator.executor.d.ts +15 -5
- package/dist/src/agents/simulator.executor.js +134 -67
- package/dist/src/agents/simulator.js +251 -8
- package/dist/src/agents/simulator.prompts.d.ts +55 -10
- package/dist/src/agents/simulator.prompts.js +305 -61
- package/dist/src/agents/simulator.types.d.ts +62 -1
- package/dist/src/agents/simulator.types.js +5 -0
- 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 +222 -16
- package/dist/src/execute/responses.js +81 -55
- 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 +114 -12
- package/dist/src/rag/types.d.ts +3 -1
- package/dist/src/rules/git/git.e2e.helper.js +51 -4
- package/dist/src/rules/git/git.health.js +89 -56
- package/dist/src/rules/git/index.d.ts +2 -2
- package/dist/src/rules/git/index.js +22 -5
- package/dist/src/rules/git/repo.d.ts +64 -6
- package/dist/src/rules/git/repo.js +572 -141
- package/dist/src/rules/git/repo.pr.d.ts +11 -18
- package/dist/src/rules/git/repo.pr.js +82 -94
- 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 +6 -2
- package/dist/src/stategraph/stategraph.js +21 -6
- package/dist/src/stategraph/types.d.ts +14 -6
- package/dist/src/types.d.ts +22 -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
|
@@ -35,9 +35,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.gitIDRegistryExists = gitIDRegistryExists;
|
|
37
37
|
exports.gitGenerateNextID = gitGenerateNextID;
|
|
38
|
+
exports.gitGenerateNextPRNumber = gitGenerateNextPRNumber;
|
|
39
|
+
exports.gitEnsureMinimumPRCounter = gitEnsureMinimumPRCounter;
|
|
40
|
+
exports.gitAllocateNextPRNumber = gitAllocateNextPRNumber;
|
|
38
41
|
exports.gitReloadIDRegistry = gitReloadIDRegistry;
|
|
39
42
|
exports.gitRegisterExistingID = gitRegisterExistingID;
|
|
40
43
|
exports.gitIDRegistryRename = gitIDRegistryRename;
|
|
44
|
+
exports.gitGetMatterCache = gitGetMatterCache;
|
|
45
|
+
exports.gitSetMatterCache = gitSetMatterCache;
|
|
41
46
|
exports.gitEnsureMatterID = gitEnsureMatterID;
|
|
42
47
|
exports.gitFileStrictMatter = gitFileStrictMatter;
|
|
43
48
|
exports.gitEnsureRepositoryConfiguration = gitEnsureRepositoryConfiguration;
|
|
@@ -48,8 +53,7 @@ exports.gitShowConfiguration = gitShowConfiguration;
|
|
|
48
53
|
exports.gitCheckConfiguration = gitCheckConfiguration;
|
|
49
54
|
exports.gitCreateOrEditFile = gitCreateOrEditFile;
|
|
50
55
|
exports.gitEditFile = gitEditFile;
|
|
51
|
-
exports.
|
|
52
|
-
exports.gitRenameFile_fs = gitRenameFile_fs;
|
|
56
|
+
exports.gitUpdateMatter = gitUpdateMatter;
|
|
53
57
|
exports.gitRenameFile = gitRenameFile;
|
|
54
58
|
exports.gitDeleteFile = gitDeleteFile;
|
|
55
59
|
exports.gitGetBranchHealth = gitGetBranchHealth;
|
|
@@ -59,8 +63,9 @@ const errors_1 = require("../errors");
|
|
|
59
63
|
const path_1 = require("path");
|
|
60
64
|
const fs = __importStar(require("fs/promises"));
|
|
61
65
|
const repo_tools_1 = require("./repo.tools");
|
|
62
|
-
const repo_pr_1 = require("./repo.pr");
|
|
63
66
|
const utils_matter_1 = require("../utils.matter");
|
|
67
|
+
const utils_slug_1 = require("../utils.slug");
|
|
68
|
+
const utils_1 = require("../../utils");
|
|
64
69
|
/**
|
|
65
70
|
* Service singleton pour gérer le registre d'IDs en mémoire
|
|
66
71
|
*/
|
|
@@ -81,6 +86,110 @@ class IDRegistryService {
|
|
|
81
86
|
this.repoPath = '';
|
|
82
87
|
this.isDirty = false;
|
|
83
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
|
+
}
|
|
84
193
|
/**
|
|
85
194
|
* Initialise le service avec le chemin du repository
|
|
86
195
|
*/
|
|
@@ -93,14 +202,12 @@ class IDRegistryService {
|
|
|
93
202
|
// Si le registre existe déjà, le recharger depuis le nouveau chemin
|
|
94
203
|
if (this.registry) {
|
|
95
204
|
this.registry = this.loadFromDisk();
|
|
96
|
-
this.isDirty = false;
|
|
97
205
|
return;
|
|
98
206
|
}
|
|
99
207
|
}
|
|
100
208
|
// Si le registre n'existe pas encore, le créer
|
|
101
209
|
if (!this.registry) {
|
|
102
210
|
this.registry = this.loadFromDisk();
|
|
103
|
-
this.isDirty = false;
|
|
104
211
|
}
|
|
105
212
|
}
|
|
106
213
|
/**
|
|
@@ -112,41 +219,29 @@ class IDRegistryService {
|
|
|
112
219
|
throw new Error('IDRegistryService not initialized. Call init() first.');
|
|
113
220
|
}
|
|
114
221
|
this.registry = this.loadFromDisk();
|
|
115
|
-
this.isDirty = false;
|
|
116
222
|
}
|
|
117
223
|
/**
|
|
118
224
|
* Charge le registre depuis le disque
|
|
119
225
|
*/
|
|
120
226
|
loadFromDisk() {
|
|
121
227
|
const registryPath = (0, path_1.join)(this.repoPath, '.git', 'with-ids.json');
|
|
228
|
+
this.isDirty = false;
|
|
122
229
|
if (!(0, fs_1.existsSync)(registryPath)) {
|
|
123
|
-
return
|
|
124
|
-
last: 980,
|
|
125
|
-
used: [],
|
|
126
|
-
updated: new Date().toISOString(),
|
|
127
|
-
matters: {}
|
|
128
|
-
};
|
|
230
|
+
return this.createEmptyRegistry();
|
|
129
231
|
}
|
|
130
232
|
try {
|
|
131
233
|
const data = JSON.parse((0, fs_1.readFileSync)(registryPath, 'utf8'));
|
|
132
|
-
|
|
133
|
-
if (!data.matters) {
|
|
134
|
-
data.matters = {};
|
|
135
|
-
}
|
|
136
|
-
return data;
|
|
234
|
+
return this.migrateRegistry(data);
|
|
137
235
|
}
|
|
138
236
|
catch (error) {
|
|
139
237
|
console.warn('⚠️ Registre d\'IDs corrompu, création d\'un nouveau');
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
used: [],
|
|
143
|
-
updated: new Date().toISOString(),
|
|
144
|
-
matters: {}
|
|
145
|
-
};
|
|
238
|
+
this.isDirty = false;
|
|
239
|
+
return this.createEmptyRegistry();
|
|
146
240
|
}
|
|
147
241
|
}
|
|
148
242
|
/**
|
|
149
|
-
* Sauvegarde le registre sur le disque (seulement si modifié)
|
|
243
|
+
* Sauvegarde le registre sur le disque (seulement si modifié).
|
|
244
|
+
* L'échec est fatal: on propage une erreur pour éviter un état incohérent.
|
|
150
245
|
*/
|
|
151
246
|
save() {
|
|
152
247
|
if (!this.registry || !this.isDirty) {
|
|
@@ -165,8 +260,7 @@ class IDRegistryService {
|
|
|
165
260
|
this.isDirty = false;
|
|
166
261
|
}
|
|
167
262
|
catch (error) {
|
|
168
|
-
|
|
169
|
-
// Ne pas propager l'erreur, le registre est une optimisation
|
|
263
|
+
throw new errors_1.GitOperationError(`Failed to save ID registry: ${error}`, 'id_registry_save');
|
|
170
264
|
}
|
|
171
265
|
}
|
|
172
266
|
/**
|
|
@@ -191,6 +285,35 @@ class IDRegistryService {
|
|
|
191
285
|
this.isDirty = true;
|
|
192
286
|
return newID;
|
|
193
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Génère le prochain numéro de PR (séquence indépendante des IDs documents)
|
|
290
|
+
*/
|
|
291
|
+
generateNextPRNumber(minValue = 60) {
|
|
292
|
+
if (!this.registry) {
|
|
293
|
+
throw new Error('IDRegistryService not initialized. Call init() first.');
|
|
294
|
+
}
|
|
295
|
+
const baseline = Math.max(Number.isInteger(this.registry.lastPR) ? this.registry.lastPR : minValue, minValue);
|
|
296
|
+
const newPR = baseline + 1;
|
|
297
|
+
this.registry.lastPR = newPR;
|
|
298
|
+
this.isDirty = true;
|
|
299
|
+
return newPR;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Force le compteur PR à une borne minimale (sans allouer de numéro).
|
|
303
|
+
* Utile pour réaligner le registre avec des branches déjà présentes.
|
|
304
|
+
*/
|
|
305
|
+
ensureMinimumPRCounter(minCounter) {
|
|
306
|
+
if (!this.registry) {
|
|
307
|
+
throw new Error('IDRegistryService not initialized. Call init() first.');
|
|
308
|
+
}
|
|
309
|
+
const normalized = Number.isInteger(minCounter) ? minCounter : 60;
|
|
310
|
+
const current = Number.isInteger(this.registry.lastPR) ? this.registry.lastPR : 60;
|
|
311
|
+
const next = Math.max(current, normalized, 60);
|
|
312
|
+
if (next !== this.registry.lastPR) {
|
|
313
|
+
this.registry.lastPR = next;
|
|
314
|
+
this.isDirty = true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
194
317
|
/**
|
|
195
318
|
* Obtient l'ID d'un fichier (peu importe la branche)
|
|
196
319
|
*
|
|
@@ -204,14 +327,23 @@ class IDRegistryService {
|
|
|
204
327
|
if (!this.registry) {
|
|
205
328
|
return undefined;
|
|
206
329
|
}
|
|
207
|
-
// Chercher le fichier dans toutes les branches
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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);
|
|
212
337
|
}
|
|
213
338
|
}
|
|
214
|
-
|
|
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);
|
|
215
347
|
}
|
|
216
348
|
/**
|
|
217
349
|
* Vérifie si un ID appartient déjà à un fichier spécifique
|
|
@@ -237,18 +369,14 @@ class IDRegistryService {
|
|
|
237
369
|
* @param id L'ID à chercher
|
|
238
370
|
* @returns Le nom du fichier qui possède cet ID, ou undefined
|
|
239
371
|
*/
|
|
240
|
-
getFileByID(id) {
|
|
372
|
+
getFileByID(branch, id) {
|
|
373
|
+
return this.getMatterCache(branch, id)?.file;
|
|
374
|
+
}
|
|
375
|
+
getMatterCachesByID(id) {
|
|
241
376
|
if (!this.registry) {
|
|
242
|
-
return
|
|
377
|
+
return [];
|
|
243
378
|
}
|
|
244
|
-
|
|
245
|
-
for (const [cacheKey, matter] of Object.entries(this.registry.matters)) {
|
|
246
|
-
if (matter.id === id) {
|
|
247
|
-
const [, file] = cacheKey.split(':');
|
|
248
|
-
return file;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return undefined;
|
|
379
|
+
return Object.values(this.registry.matters).filter(matter => matter.id === id);
|
|
252
380
|
}
|
|
253
381
|
/**
|
|
254
382
|
* Enregistre un ID existant
|
|
@@ -290,22 +418,24 @@ class IDRegistryService {
|
|
|
290
418
|
* Récupère un matter depuis le cache
|
|
291
419
|
* Note: Le champ 'updated' est filtré pour ne pas être exposé à l'extérieur
|
|
292
420
|
*/
|
|
293
|
-
getMatterCache(branch,
|
|
421
|
+
getMatterCache(branch, id) {
|
|
294
422
|
if (!this.registry) {
|
|
295
423
|
throw new Error('IDRegistryService not initialized. Call init() first.');
|
|
296
424
|
}
|
|
297
|
-
const cacheKey =
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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.');
|
|
301
431
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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;
|
|
309
439
|
}
|
|
310
440
|
/**
|
|
311
441
|
* Met à jour le cache d'un matter
|
|
@@ -314,12 +444,17 @@ class IDRegistryService {
|
|
|
314
444
|
if (!this.registry) {
|
|
315
445
|
throw new Error('IDRegistryService not initialized. Call init() first.');
|
|
316
446
|
}
|
|
317
|
-
|
|
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);
|
|
318
453
|
this.registry.matters[cacheKey] = {
|
|
319
454
|
id: matter.id,
|
|
455
|
+
file,
|
|
320
456
|
title: matter.title,
|
|
321
457
|
service: matter.service,
|
|
322
|
-
oldfile: matter.oldfile, // ✅ Inclure oldfile temporaire
|
|
323
458
|
updated: new Date().toISOString()
|
|
324
459
|
};
|
|
325
460
|
this.isDirty = true;
|
|
@@ -327,14 +462,21 @@ class IDRegistryService {
|
|
|
327
462
|
/**
|
|
328
463
|
* Supprime une entrée du cache
|
|
329
464
|
*/
|
|
330
|
-
deleteMatterCache(branch,
|
|
465
|
+
deleteMatterCache(branch, id) {
|
|
331
466
|
if (!this.registry) {
|
|
332
467
|
throw new Error('IDRegistryService not initialized. Call init() first.');
|
|
333
468
|
}
|
|
334
|
-
const cacheKey =
|
|
469
|
+
const cacheKey = this.createMatterKey(branch, id);
|
|
335
470
|
delete this.registry.matters[cacheKey];
|
|
336
471
|
this.isDirty = true;
|
|
337
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
|
+
}
|
|
338
480
|
/**
|
|
339
481
|
* Renomme une entrée dans le cache du registre
|
|
340
482
|
*
|
|
@@ -353,27 +495,18 @@ class IDRegistryService {
|
|
|
353
495
|
* idRegistryService.rename('old-name.md', 'new-name.md', 'rule-validation-1');
|
|
354
496
|
* ```
|
|
355
497
|
*/
|
|
356
|
-
rename(
|
|
498
|
+
rename(id, newFile, branch) {
|
|
357
499
|
if (!this.registry) {
|
|
358
500
|
throw new Error('IDRegistryService not initialized. Call init() first.');
|
|
359
501
|
}
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
// Récupérer le cache existant
|
|
363
|
-
const cached = this.registry.matters[oldKey];
|
|
502
|
+
const cacheKey = this.createMatterKey(branch, id);
|
|
503
|
+
const cached = this.registry.matters[cacheKey];
|
|
364
504
|
if (!cached) {
|
|
365
|
-
// Pas de cache pour l'ancien fichier, rien à renommer
|
|
366
505
|
return;
|
|
367
506
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// Créer la nouvelle clé avec les mêmes données
|
|
372
|
-
// ⚠️ NE PAS inclure oldfile - c'est temporaire uniquement pour notification client
|
|
373
|
-
this.registry.matters[newKey] = {
|
|
374
|
-
id: cached.id,
|
|
375
|
-
title: cached.title,
|
|
376
|
-
service: cached.service,
|
|
507
|
+
this.registry.matters[cacheKey] = {
|
|
508
|
+
...cached,
|
|
509
|
+
file: newFile,
|
|
377
510
|
updated: new Date().toISOString()
|
|
378
511
|
};
|
|
379
512
|
this.isDirty = true;
|
|
@@ -389,6 +522,9 @@ const idRegistryService = IDRegistryService.get();
|
|
|
389
522
|
* dans le repository. Le fichier `with-ids.json` est créé automatiquement lors du
|
|
390
523
|
* premier appel à `gitGenerateNextID()` ou `gitEnsureMatterID()`.
|
|
391
524
|
*
|
|
525
|
+
* @deprecated API utilitaire de compatibilité. Préférer la résolution via
|
|
526
|
+
* gitEnsureMatterID() / gitFileStrictMatter() sans dépendre d'un check préalable.
|
|
527
|
+
*
|
|
392
528
|
* @param config Configuration Git optionnelle (utilise la config par défaut si non fournie)
|
|
393
529
|
* @returns `true` si le fichier `.git/with-ids.json` existe, `false` sinon
|
|
394
530
|
*
|
|
@@ -426,6 +562,60 @@ function gitGenerateNextID(config) {
|
|
|
426
562
|
idRegistryService.save();
|
|
427
563
|
return newID;
|
|
428
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Génère le prochain numéro de PR via le registre `.git/with-ids.json`.
|
|
567
|
+
* Cette séquence est indépendante des IDs documentaires.
|
|
568
|
+
*
|
|
569
|
+
* @param config Configuration Git optionnelle
|
|
570
|
+
* @param minValue Valeur plancher du compteur PR (60 par défaut)
|
|
571
|
+
* @returns Prochain numéro de PR
|
|
572
|
+
*/
|
|
573
|
+
function gitGenerateNextPRNumber(config, minValue = 60) {
|
|
574
|
+
const gitConf = (0, repo_tools_1.gitLoad)(config);
|
|
575
|
+
idRegistryService.init(gitConf.repoPath);
|
|
576
|
+
const newPR = idRegistryService.generateNextPRNumber(minValue);
|
|
577
|
+
idRegistryService.save();
|
|
578
|
+
return newPR;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Réaligne le compteur PR du registre sur une borne minimale sans allouer de numéro.
|
|
582
|
+
*
|
|
583
|
+
* @param minCounter Valeur minimale à garantir pour `lastPR`
|
|
584
|
+
* @param config Configuration Git optionnelle
|
|
585
|
+
*/
|
|
586
|
+
function gitEnsureMinimumPRCounter(minCounter, config) {
|
|
587
|
+
const gitConf = (0, repo_tools_1.gitLoad)(config);
|
|
588
|
+
idRegistryService.init(gitConf.repoPath);
|
|
589
|
+
idRegistryService.ensureMinimumPRCounter(minCounter);
|
|
590
|
+
idRegistryService.save();
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Alloue le prochain numéro de PR en réalignant d'abord le compteur registre
|
|
594
|
+
* sur les branches de validation existantes.
|
|
595
|
+
*
|
|
596
|
+
* @param git Instance Git
|
|
597
|
+
* @param validationPrefix Préfixe des branches de validation
|
|
598
|
+
* @param config Configuration Git optionnelle
|
|
599
|
+
* @param minValue Valeur plancher du compteur PR (60 par défaut)
|
|
600
|
+
* @returns Prochain numéro de PR
|
|
601
|
+
*/
|
|
602
|
+
async function gitAllocateNextPRNumber(git, validationPrefix, config, minValue = 60) {
|
|
603
|
+
const gitConf = (0, repo_tools_1.gitLoad)(config);
|
|
604
|
+
idRegistryService.init(gitConf.repoPath);
|
|
605
|
+
const allBranches = await (0, repo_tools_1.gitGetAllBranches)(git);
|
|
606
|
+
const numbers = allBranches
|
|
607
|
+
.filter((branch) => branch.startsWith(validationPrefix))
|
|
608
|
+
.map((branch) => {
|
|
609
|
+
const numberPart = branch.substring(validationPrefix.length);
|
|
610
|
+
return parseInt(numberPart.split('-')[0], 10);
|
|
611
|
+
})
|
|
612
|
+
.filter((num) => !isNaN(num));
|
|
613
|
+
const maxBranchNumber = numbers.length > 0 ? Math.max(...numbers) : minValue;
|
|
614
|
+
idRegistryService.ensureMinimumPRCounter(maxBranchNumber);
|
|
615
|
+
const nextPR = idRegistryService.generateNextPRNumber(minValue);
|
|
616
|
+
idRegistryService.save();
|
|
617
|
+
return nextPR;
|
|
618
|
+
}
|
|
429
619
|
/**
|
|
430
620
|
* Force le rechargement du registre d'IDs depuis le disque
|
|
431
621
|
*
|
|
@@ -442,6 +632,8 @@ function gitGenerateNextID(config) {
|
|
|
442
632
|
* await gitCreateOrEditFile(...); // Modifie le registre
|
|
443
633
|
* gitReloadIDRegistry(); // Force le rechargement depuis le disque
|
|
444
634
|
* ```
|
|
635
|
+
*
|
|
636
|
+
* @deprecated Réservé aux tests de bas niveau. Ne pas utiliser dans le workflow applicatif.
|
|
445
637
|
*/
|
|
446
638
|
function gitReloadIDRegistry(config) {
|
|
447
639
|
const gitConf = (0, repo_tools_1.gitLoad)(config);
|
|
@@ -454,6 +646,9 @@ function gitReloadIDRegistry(config) {
|
|
|
454
646
|
/**
|
|
455
647
|
* Enregistre un ID existant dans le registre
|
|
456
648
|
*
|
|
649
|
+
* @deprecated Préférer `gitCreateOrEditFile(...)`, `gitUpdateMatter(...)` ou
|
|
650
|
+
* `gitRenameFile(...)` qui synchronisent le registre dans le même flux documentaire.
|
|
651
|
+
*
|
|
457
652
|
* @param matter Le matter contenant l'ID à enregistrer
|
|
458
653
|
* @param branch Branche du fichier (optionnel, pour vérification de propriété)
|
|
459
654
|
* @param file Nom du fichier (optionnel, pour vérification de propriété)
|
|
@@ -477,8 +672,9 @@ function gitRegisterExistingID(matter, branch, file, config) {
|
|
|
477
672
|
// Si l'ID existe déjà, vérifier qu'il appartient au MÊME fichier
|
|
478
673
|
const registry = idRegistryService.getRegistry();
|
|
479
674
|
if (registry.used.includes(matter.id)) {
|
|
480
|
-
const
|
|
481
|
-
|
|
675
|
+
const conflictingOwner = idRegistryService.getMatterCachesByID(matter.id)
|
|
676
|
+
.find(owner => owner.file !== file);
|
|
677
|
+
if (conflictingOwner?.file) {
|
|
482
678
|
// ID utilisé par un AUTRE fichier → Erreur
|
|
483
679
|
const error = new Error(`ID ${matter.id} est déjà utilisé`);
|
|
484
680
|
error.code = 'id_already_used';
|
|
@@ -497,6 +693,8 @@ function gitRegisterExistingID(matter, branch, file, config) {
|
|
|
497
693
|
/**
|
|
498
694
|
* Renomme un fichier dans le cache du registre d'IDs
|
|
499
695
|
*
|
|
696
|
+
* @deprecated Le cache de matter/ID est maintenant mis à jour par `gitRenameFile(...)`.
|
|
697
|
+
*
|
|
500
698
|
* Cette fonction met à jour le cache lorsqu'un fichier est renommé:
|
|
501
699
|
* - Supprime l'entrée avec l'ancien nom
|
|
502
700
|
* - Crée une nouvelle entrée avec le nouveau nom
|
|
@@ -516,7 +714,102 @@ function gitRegisterExistingID(matter, branch, file, config) {
|
|
|
516
714
|
function gitIDRegistryRename(oldFile, newFile, branch, config) {
|
|
517
715
|
const gitConf = (0, repo_tools_1.gitLoad)(config);
|
|
518
716
|
idRegistryService.init(gitConf.repoPath);
|
|
519
|
-
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
|
+
}
|
|
520
813
|
idRegistryService.save();
|
|
521
814
|
}
|
|
522
815
|
/**
|
|
@@ -538,17 +831,21 @@ function gitEnsureMatterID(matter, config, branch, file) {
|
|
|
538
831
|
// ✅ NOUVEAU: Chercher d'abord l'ID existant du fichier (peu importe la branche)
|
|
539
832
|
const existingFileID = file ? idRegistryService.getFileID(file) : undefined;
|
|
540
833
|
if (existingFileID) {
|
|
541
|
-
// Le fichier a déjà un ID
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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;
|
|
550
848
|
}
|
|
551
|
-
return matter;
|
|
552
849
|
}
|
|
553
850
|
// Si matter a un ID fourni manuellement (ex: import ou test)
|
|
554
851
|
// FIXME 999 should be a constant in config
|
|
@@ -605,7 +902,7 @@ function validateMatter(matter) {
|
|
|
605
902
|
*/
|
|
606
903
|
function extractMatterFromContent(content) {
|
|
607
904
|
if (!content) {
|
|
608
|
-
return { id: 0, title: undefined, service: undefined
|
|
905
|
+
return { id: 0, title: undefined, service: undefined };
|
|
609
906
|
}
|
|
610
907
|
// Regex pour extraire id (format: "id: 1234" ou "id: '1234'")
|
|
611
908
|
const idMatch = content.match(/^id:\s*['"]?(\d+)['"]?/m);
|
|
@@ -615,11 +912,7 @@ function extractMatterFromContent(content) {
|
|
|
615
912
|
const title = titleMatch ? titleMatch[1].trim() : undefined;
|
|
616
913
|
const serviceMatch = content.match(/^service:\s*["']?([^"'\n]+)["']?/m);
|
|
617
914
|
const service = serviceMatch ? serviceMatch[1].trim() : undefined;
|
|
618
|
-
|
|
619
|
-
// ✅ Extraction de oldfile (champ temporaire pour notifier les renames)
|
|
620
|
-
const oldfileMatch = content.match(/^oldfile:\s*["']?([^"'\n]+)["']?/m);
|
|
621
|
-
const oldfile = oldfileMatch ? oldfileMatch[1].trim() : undefined;
|
|
622
|
-
return { id, title, service, oldfile };
|
|
915
|
+
return { id, title, service };
|
|
623
916
|
}
|
|
624
917
|
/**
|
|
625
918
|
* Lecture rapide et stricte du matter d'un fichier (id + title uniquement)
|
|
@@ -649,19 +942,18 @@ async function gitFileStrictMatter(git, filePath, branch, config) {
|
|
|
649
942
|
idRegistryService.init(gitConf.repoPath);
|
|
650
943
|
try {
|
|
651
944
|
// ✅ ÉTAPE 1: Vérifier le cache
|
|
652
|
-
const cached = idRegistryService.
|
|
945
|
+
const cached = idRegistryService.getMatterCacheByFile(branch, filePath);
|
|
653
946
|
if (cached) {
|
|
654
947
|
// Cache trouvé, retourner directement
|
|
655
948
|
return {
|
|
656
949
|
id: cached.id,
|
|
657
|
-
title: cached.title
|
|
658
|
-
oldfile: cached.oldfile // ✅ Inclure oldfile temporaire
|
|
950
|
+
title: cached.title
|
|
659
951
|
};
|
|
660
952
|
}
|
|
661
953
|
// ❌ ÉTAPE 2: Cache absent, lire le fichier
|
|
662
954
|
const content = await git.show([`${branch}:${filePath}`]);
|
|
663
955
|
if (!content) {
|
|
664
|
-
return { id: undefined, title: undefined
|
|
956
|
+
return { id: undefined, title: undefined };
|
|
665
957
|
}
|
|
666
958
|
// ✅ ÉTAPE 3: Extraction rapide avec regex (sans matterParse complet)
|
|
667
959
|
const matter = extractMatterFromContent(content);
|
|
@@ -674,7 +966,7 @@ async function gitFileStrictMatter(git, filePath, branch, config) {
|
|
|
674
966
|
if (gitConf.verbose) {
|
|
675
967
|
console.warn(`⚠️ Erreur lecture matter de ${filePath}:`, error);
|
|
676
968
|
}
|
|
677
|
-
return { id: undefined, title: undefined
|
|
969
|
+
return { id: undefined, title: undefined };
|
|
678
970
|
}
|
|
679
971
|
}
|
|
680
972
|
/**
|
|
@@ -1109,8 +1401,8 @@ async function gitCreateOrEditFile(git, filePath, PR, content, user, config) {
|
|
|
1109
1401
|
idRegistryService.init(gitConf.repoPath);
|
|
1110
1402
|
idRegistryService.setMatterCache(PR, filePath, {
|
|
1111
1403
|
id: parsed.matter.id,
|
|
1112
|
-
title: parsed.matter.title
|
|
1113
|
-
|
|
1404
|
+
title: parsed.matter.title,
|
|
1405
|
+
service: parsed.matter.service
|
|
1114
1406
|
});
|
|
1115
1407
|
idRegistryService.save();
|
|
1116
1408
|
}
|
|
@@ -1134,8 +1426,18 @@ async function gitCreateOrEditFile(git, filePath, PR, content, user, config) {
|
|
|
1134
1426
|
}
|
|
1135
1427
|
// Determine the current head after the commit attempt.
|
|
1136
1428
|
const newHead = (await git.revparse(['HEAD'])).trim();
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
+
}
|
|
1139
1441
|
const updatedNote = {
|
|
1140
1442
|
...oldNote,
|
|
1141
1443
|
files: newFiles,
|
|
@@ -1180,6 +1482,9 @@ async function gitCreateOrEditFile(git, filePath, PR, content, user, config) {
|
|
|
1180
1482
|
}
|
|
1181
1483
|
/**
|
|
1182
1484
|
* Modifie un fichier dans la branche draft (opération bas niveau)
|
|
1485
|
+
*
|
|
1486
|
+
* @deprecated Utiliser gitCreateOrEditFile() qui couvre création + édition.
|
|
1487
|
+
*
|
|
1183
1488
|
* @param git Instance Git
|
|
1184
1489
|
* @param filePath Chemin du fichier
|
|
1185
1490
|
* @param PR Nom de la branche de Pull Request
|
|
@@ -1195,7 +1500,57 @@ async function gitEditFile(git, filePath, PR, content, user, config) {
|
|
|
1195
1500
|
}
|
|
1196
1501
|
return await gitCreateOrEditFile(git, filePath, PR, content, user, config);
|
|
1197
1502
|
}
|
|
1198
|
-
|
|
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) {
|
|
1199
1554
|
// Si les noms sont identiques, pas besoin de renommer
|
|
1200
1555
|
if (oldFileName === newFileName) {
|
|
1201
1556
|
return {
|
|
@@ -1223,6 +1578,12 @@ async function gitRenameFile_git(git, oldFileName, newFileName, branch, user, co
|
|
|
1223
1578
|
await git.checkout(branch);
|
|
1224
1579
|
// Rename file with git mv (atomique dans Git)
|
|
1225
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
|
+
}
|
|
1226
1587
|
// Commit - git mv automatically stages changes
|
|
1227
1588
|
const commit = await git.commit(`rename: ${oldFileName} → ${newFileName}`, {
|
|
1228
1589
|
'--author': `${user.name} <${user.email}>`
|
|
@@ -1235,17 +1596,11 @@ async function gitRenameFile_git(git, oldFileName, newFileName, branch, user, co
|
|
|
1235
1596
|
name: user.name,
|
|
1236
1597
|
email: user.email
|
|
1237
1598
|
},
|
|
1238
|
-
branch: branch
|
|
1599
|
+
branch: branch,
|
|
1600
|
+
content: finalContent
|
|
1239
1601
|
};
|
|
1240
1602
|
}
|
|
1241
1603
|
catch (error) {
|
|
1242
|
-
// En cas d'erreur, essayer de restaurer l'état initial
|
|
1243
|
-
try {
|
|
1244
|
-
await git.reset(['--hard']);
|
|
1245
|
-
}
|
|
1246
|
-
catch (resetError) {
|
|
1247
|
-
console.warn('Could not reset after failed rename:', resetError);
|
|
1248
|
-
}
|
|
1249
1604
|
throw error;
|
|
1250
1605
|
}
|
|
1251
1606
|
finally {
|
|
@@ -1255,7 +1610,7 @@ async function gitRenameFile_git(git, oldFileName, newFileName, branch, user, co
|
|
|
1255
1610
|
(0, repo_tools_1.unlock)(`checkout`);
|
|
1256
1611
|
}
|
|
1257
1612
|
}
|
|
1258
|
-
async function gitRenameFile_fs(git, oldFileName, newFileName, branch, user, config) {
|
|
1613
|
+
async function gitRenameFile_fs(git, oldFileName, newFileName, branch, user, finalContent, config) {
|
|
1259
1614
|
// Si les noms sont identiques, pas besoin de renommer
|
|
1260
1615
|
if (oldFileName === newFileName) {
|
|
1261
1616
|
return {
|
|
@@ -1269,6 +1624,9 @@ async function gitRenameFile_fs(git, oldFileName, newFileName, branch, user, con
|
|
|
1269
1624
|
const gitConf = (0, repo_tools_1.gitLoad)(config);
|
|
1270
1625
|
try {
|
|
1271
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
|
+
}
|
|
1272
1630
|
const fullNewPath = (0, path_1.join)(gitConf.uploadPath, newFileName);
|
|
1273
1631
|
const fullOldPath = (0, path_1.join)(gitConf.uploadPath, oldFileName);
|
|
1274
1632
|
// Vérifier que l'ancien fichier existe
|
|
@@ -1282,10 +1640,13 @@ async function gitRenameFile_fs(git, oldFileName, newFileName, branch, user, con
|
|
|
1282
1640
|
throw new Error(`Le fichier de destination "${newFileName}" existe déjà sur le système de fichiers`);
|
|
1283
1641
|
}
|
|
1284
1642
|
// Renommage atomique filesystem
|
|
1285
|
-
// Utiliser copyFile + unlink pour plus de sécurité que rename() sur certains systèmes
|
|
1286
1643
|
try {
|
|
1644
|
+
await fs.mkdir((0, path_1.dirname)(fullNewPath), { recursive: true });
|
|
1287
1645
|
await fs.copyFile(fullOldPath, fullNewPath);
|
|
1288
1646
|
await fs.unlink(fullOldPath);
|
|
1647
|
+
if (typeof finalContent === 'string') {
|
|
1648
|
+
await fs.writeFile(fullNewPath, finalContent, { flag: 'w', encoding: 'utf8' });
|
|
1649
|
+
}
|
|
1289
1650
|
}
|
|
1290
1651
|
catch (copyError) {
|
|
1291
1652
|
// En cas d'erreur, nettoyer le nouveau fichier s'il a été créé
|
|
@@ -1307,7 +1668,8 @@ async function gitRenameFile_fs(git, oldFileName, newFileName, branch, user, con
|
|
|
1307
1668
|
name: user.name,
|
|
1308
1669
|
email: user.email
|
|
1309
1670
|
},
|
|
1310
|
-
branch: branch
|
|
1671
|
+
branch: branch,
|
|
1672
|
+
content: finalContent
|
|
1311
1673
|
};
|
|
1312
1674
|
}
|
|
1313
1675
|
catch (error) {
|
|
@@ -1327,7 +1689,7 @@ async function gitRenameFile_fs(git, oldFileName, newFileName, branch, user, con
|
|
|
1327
1689
|
* @param config Configuration Git optionnelle
|
|
1328
1690
|
* @returns Historique du commit de renommage
|
|
1329
1691
|
*/
|
|
1330
|
-
async function gitRenameFile(git, oldFileName, newFileName, branch, user,
|
|
1692
|
+
async function gitRenameFile(git, oldFileName, newFileName, branch, user, options, nextContent) {
|
|
1331
1693
|
// Si les noms sont identiques, pas besoin de renommer
|
|
1332
1694
|
if (oldFileName === newFileName) {
|
|
1333
1695
|
return {
|
|
@@ -1338,37 +1700,86 @@ async function gitRenameFile(git, oldFileName, newFileName, branch, user, config
|
|
|
1338
1700
|
branch: branch
|
|
1339
1701
|
};
|
|
1340
1702
|
}
|
|
1341
|
-
|
|
1342
|
-
const gitConf = (0, repo_tools_1.gitLoad)(config);
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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);
|
|
1362
1732
|
}
|
|
1363
1733
|
}
|
|
1364
1734
|
}
|
|
1365
|
-
|
|
1735
|
+
syncMatterCacheAfterRename(branch, oldFileName, newFileName, finalDocument.matter, gitConf);
|
|
1366
1736
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1737
|
+
return {
|
|
1738
|
+
...result,
|
|
1739
|
+
content: finalDocument.content
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Met à jour la liste des fichiers d'une PR après un renommage.
|
|
1744
|
+
* Helper local pour éviter une dépendance circulaire avec repo.pr.ts.
|
|
1745
|
+
*/
|
|
1746
|
+
async function gitPRReplaceFile(git, branch, options = {}, config) {
|
|
1747
|
+
const { remove, add } = options;
|
|
1748
|
+
if (!remove && !add) {
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
const gitConf = (0, repo_tools_1.gitLoad)(config);
|
|
1752
|
+
const metadata = await (0, repo_tools_1.gitReadNote)(git, branch, gitConf.gitNotes.namespace, 20);
|
|
1753
|
+
if (!metadata) {
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
const currentFiles = Array.isArray(metadata.files) ? metadata.files : [];
|
|
1757
|
+
const seen = new Set();
|
|
1758
|
+
const nextFiles = [];
|
|
1759
|
+
for (const file of currentFiles) {
|
|
1760
|
+
if (!file) {
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
if (remove && file === remove) {
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
if (seen.has(file)) {
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
seen.add(file);
|
|
1770
|
+
nextFiles.push(file);
|
|
1771
|
+
}
|
|
1772
|
+
if (add && !seen.has(add)) {
|
|
1773
|
+
nextFiles.push(add);
|
|
1774
|
+
}
|
|
1775
|
+
const changed = currentFiles.length !== nextFiles.length ||
|
|
1776
|
+
currentFiles.some((file, idx) => file !== nextFiles[idx]);
|
|
1777
|
+
if (!changed) {
|
|
1778
|
+
return;
|
|
1371
1779
|
}
|
|
1780
|
+
metadata.files = nextFiles;
|
|
1781
|
+
const headCommitHash = (await git.revparse([branch])).trim();
|
|
1782
|
+
await (0, repo_tools_1.gitWriteNote)(git, headCommitHash, metadata, gitConf.gitNotes.namespace);
|
|
1372
1783
|
}
|
|
1373
1784
|
/**
|
|
1374
1785
|
* Supprime un fichier d'une branche Git (opération bas niveau)
|
|
@@ -1412,7 +1823,18 @@ async function gitDeleteFile(git, filePath, branch, user, config) {
|
|
|
1412
1823
|
// Mettre à jour la note si elle existe (pour les branches PR)
|
|
1413
1824
|
if (oldNote) {
|
|
1414
1825
|
const newHead = (await git.revparse(['HEAD'])).trim();
|
|
1415
|
-
|
|
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
|
+
}
|
|
1416
1838
|
const updatedNote = {
|
|
1417
1839
|
...oldNote,
|
|
1418
1840
|
files: newFiles,
|
|
@@ -1468,6 +1890,8 @@ const _writeFileAndCommit = async (git, filePath, content, user, config, commitM
|
|
|
1468
1890
|
/**
|
|
1469
1891
|
* Diagnostique l'état de santé d'une branche Git et détecte les problèmes bloquants.
|
|
1470
1892
|
*
|
|
1893
|
+
* @deprecated Utiliser GitHealthManager pour les diagnostics et réparations.
|
|
1894
|
+
*
|
|
1471
1895
|
* **Problèmes bloquants détectés :**
|
|
1472
1896
|
* - ❌ **Branche inexistante** : La branche spécifiée n'existe pas dans le dépôt
|
|
1473
1897
|
* - ❌ **Branche inaccessible** : Impossible de faire un checkout vers la branche
|
|
@@ -1593,6 +2017,8 @@ async function gitGetBranchHealth(git, branch) {
|
|
|
1593
2017
|
/**
|
|
1594
2018
|
* Version optimisée de gitGetBranchHealth pour les branches de validation
|
|
1595
2019
|
*
|
|
2020
|
+
* @deprecated Utiliser GitHealthManager.diagnoseValidationBranches() / repairValidationBranches().
|
|
2021
|
+
*
|
|
1596
2022
|
* Cette fonction utilise une approche en cascade pour diagnostiquer rapidement
|
|
1597
2023
|
* l'état d'une branche de validation sans effectuer de checkout coûteux.
|
|
1598
2024
|
*
|
|
@@ -1700,6 +2126,11 @@ async function checkMergeInProgress(repoPath) {
|
|
|
1700
2126
|
return mergeFiles.some(file => (0, fs_1.existsSync)((0, path_1.join)(gitDir, file)));
|
|
1701
2127
|
}
|
|
1702
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).
|
|
1703
2134
|
if (!files || files.length === 0) {
|
|
1704
2135
|
return [];
|
|
1705
2136
|
}
|