pumuki 6.3.39 → 6.3.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +21 -12
  2. package/VERSION +1 -1
  3. package/core/gate/evaluateRules.test.ts +40 -0
  4. package/core/gate/evaluateRules.ts +7 -1
  5. package/core/rules/Consequence.ts +1 -0
  6. package/docs/CONFIGURATION.md +50 -0
  7. package/docs/INSTALLATION.md +38 -11
  8. package/docs/MCP_SERVERS.md +1 -1
  9. package/docs/README.md +1 -0
  10. package/docs/RELEASE_NOTES.md +44 -0
  11. package/docs/USAGE.md +191 -9
  12. package/docs/registro-maestro-de-seguimiento.md +2 -2
  13. package/docs/seguimiento-activo-pumuki-saas-supermercados.md +1592 -1
  14. package/docs/validation/README.md +2 -1
  15. package/docs/validation/ast-intelligence-roadmap.md +96 -0
  16. package/integrations/config/skillsCustomRules.ts +14 -0
  17. package/integrations/config/skillsDetectorRegistry.ts +11 -1
  18. package/integrations/config/skillsLock.ts +30 -0
  19. package/integrations/config/skillsMarkdownRules.ts +14 -3
  20. package/integrations/config/skillsRuleSet.ts +25 -3
  21. package/integrations/evidence/readEvidence.test.ts +3 -2
  22. package/integrations/evidence/readEvidence.ts +14 -4
  23. package/integrations/evidence/repoState.ts +10 -2
  24. package/integrations/evidence/schema.test.ts +3 -2
  25. package/integrations/evidence/schema.ts +3 -0
  26. package/integrations/evidence/writeEvidence.test.ts +3 -2
  27. package/integrations/gate/evaluateAiGate.ts +511 -2
  28. package/integrations/git/GitService.ts +5 -1
  29. package/integrations/git/astIntelligenceDualValidation.ts +275 -0
  30. package/integrations/git/gitAtomicity.ts +42 -9
  31. package/integrations/git/resolveGitRefs.ts +37 -0
  32. package/integrations/git/runPlatformGate.ts +228 -1
  33. package/integrations/git/runPlatformGateEvaluation.ts +4 -0
  34. package/integrations/git/stageRunners.ts +116 -2
  35. package/integrations/lifecycle/cli.ts +759 -22
  36. package/integrations/lifecycle/doctor.ts +62 -0
  37. package/integrations/lifecycle/index.ts +1 -0
  38. package/integrations/lifecycle/packageInfo.ts +25 -3
  39. package/integrations/lifecycle/policyReconcile.ts +304 -0
  40. package/integrations/lifecycle/preWriteAutomation.ts +42 -2
  41. package/integrations/lifecycle/watch.ts +365 -0
  42. package/integrations/mcp/aiGateCheck.ts +59 -2
  43. package/integrations/mcp/autoExecuteAiStart.ts +25 -1
  44. package/integrations/mcp/preFlightCheck.ts +13 -0
  45. package/integrations/sdd/evidenceScaffold.ts +223 -0
  46. package/integrations/sdd/index.ts +2 -0
  47. package/integrations/sdd/stateSync.ts +400 -0
  48. package/integrations/sdd/syncDocs.ts +97 -2
  49. package/package.json +4 -1
  50. package/scripts/backlog-action-reasons-lib.ts +38 -0
  51. package/scripts/backlog-id-issue-map-lib.ts +69 -0
  52. package/scripts/backlog-json-contract-lib.ts +3 -0
  53. package/scripts/framework-menu-consumer-preflight-lib.ts +6 -0
  54. package/scripts/package-install-smoke-command-resolution-lib.ts +64 -0
  55. package/scripts/package-install-smoke-consumer-npm-lib.ts +43 -0
  56. package/scripts/package-install-smoke-consumer-repo-setup-lib.ts +2 -0
  57. package/scripts/package-install-smoke-execution-steps-lib.ts +27 -9
  58. package/scripts/package-install-smoke-lifecycle-lib.ts +15 -4
  59. package/scripts/package-install-smoke-workspace-factory-lib.ts +4 -1
  60. package/scripts/reconcile-consumer-backlog-issues-lib.ts +651 -0
  61. package/scripts/reconcile-consumer-backlog-issues.ts +348 -0
  62. package/scripts/watch-consumer-backlog-lib.ts +465 -0
  63. package/scripts/watch-consumer-backlog.ts +326 -0
@@ -0,0 +1,651 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { readFileSync, writeFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+
5
+ export type BacklogIssueState = 'OPEN' | 'CLOSED';
6
+ export type BacklogStatusEmoji = '✅' | '🚧' | '⏳' | '⛔';
7
+
8
+ export type BacklogIssueEntry = {
9
+ issueNumber: number;
10
+ lineNumber: number;
11
+ currentEmoji: BacklogStatusEmoji;
12
+ line: string;
13
+ };
14
+
15
+ export type BacklogIssueChange = {
16
+ issueNumber: number;
17
+ lineNumber: number;
18
+ from: BacklogStatusEmoji;
19
+ to: BacklogStatusEmoji;
20
+ issueState: BacklogIssueState;
21
+ line: string;
22
+ };
23
+
24
+ export type BacklogIssueReferenceChange = {
25
+ id: string;
26
+ issueNumber: number;
27
+ lineNumber: number;
28
+ from: string;
29
+ to: string;
30
+ line: string;
31
+ };
32
+
33
+ export type BacklogSectionHeadingChange = {
34
+ id: string;
35
+ lineNumber: number;
36
+ from: BacklogStatusEmoji;
37
+ to: BacklogStatusEmoji;
38
+ line: string;
39
+ };
40
+
41
+ export type BacklogStatusSummary = {
42
+ closed: number;
43
+ inProgress: number;
44
+ pending: number;
45
+ blocked: number;
46
+ inProgressIds: ReadonlyArray<string>;
47
+ blockedIds: ReadonlyArray<string>;
48
+ };
49
+
50
+ export type BacklogMappingSource = 'none' | 'json' | 'markdown' | 'merged';
51
+
52
+ export type BacklogReferenceResolution = {
53
+ resolvedByProvidedMap: ReadonlyArray<string>;
54
+ resolvedByLookup: ReadonlyArray<string>;
55
+ unresolvedReferenceIds: ReadonlyArray<string>;
56
+ };
57
+
58
+ export type BacklogReconcileResult = {
59
+ filePath: string;
60
+ repo?: string;
61
+ apply: boolean;
62
+ mappingSource: BacklogMappingSource;
63
+ entriesScanned: number;
64
+ issuesResolved: number;
65
+ referenceChanges: ReadonlyArray<BacklogIssueReferenceChange>;
66
+ referenceResolution: BacklogReferenceResolution;
67
+ changes: ReadonlyArray<BacklogIssueChange>;
68
+ summaryUpdated: boolean;
69
+ nextStepUpdated: boolean;
70
+ headingUpdated: boolean;
71
+ summary: BacklogStatusSummary;
72
+ updated: boolean;
73
+ headingChanges: ReadonlyArray<BacklogSectionHeadingChange>;
74
+ };
75
+
76
+ const STATUS_EMOJI_PATTERN = /(✅|🚧|⏳|⛔)/;
77
+ const ISSUE_REF_PATTERN = /#(\d+)/;
78
+ const BACKLOG_ID_PATTERN = /^(PUMUKI-(?:M)?\d+|PUMUKI-INC-\d+|FP-\d+|AST-GAP-\d+)$/;
79
+ const PENDING_REFERENCE_PATTERN = /\|\s*Pendiente(?:\s*\(rel\.\s*#\d+\))?\s*\|/;
80
+ const BACKLOG_SECTION_HEADING_PATTERN =
81
+ /^(\s*###\s*)(✅|🚧|⏳|⛔)(\s+)(PUMUKI-(?:M)?\d+|PUMUKI-INC-\d+|FP-\d+|AST-GAP-\d+)\b/;
82
+
83
+ export type BacklogIssueNumberResolver = (
84
+ backlogId: string,
85
+ repo?: string
86
+ ) => number | null | Promise<number | null>;
87
+
88
+ const parseIssueNumber = (line: string): number | null => {
89
+ const match = ISSUE_REF_PATTERN.exec(line);
90
+ if (!match?.[1]) {
91
+ return null;
92
+ }
93
+ const parsed = Number.parseInt(match[1], 10);
94
+ return Number.isFinite(parsed) ? parsed : null;
95
+ };
96
+
97
+ export const collectBacklogIssueEntries = (markdown: string): ReadonlyArray<BacklogIssueEntry> => {
98
+ const lines = markdown.split(/\r?\n/);
99
+ const entries: BacklogIssueEntry[] = [];
100
+
101
+ lines.forEach((line, index) => {
102
+ if (!line.includes('|')) {
103
+ return;
104
+ }
105
+ const emojiMatch = STATUS_EMOJI_PATTERN.exec(line);
106
+ if (!emojiMatch?.[1]) {
107
+ return;
108
+ }
109
+ const issueNumber = parseIssueNumber(line);
110
+ if (issueNumber === null) {
111
+ return;
112
+ }
113
+ entries.push({
114
+ issueNumber,
115
+ lineNumber: index + 1,
116
+ currentEmoji: emojiMatch[1] as BacklogStatusEmoji,
117
+ line,
118
+ });
119
+ });
120
+
121
+ return entries;
122
+ };
123
+
124
+ const parseBacklogId = (line: string): string | null => {
125
+ const cells = line.split('|').map((cell) => cell.trim());
126
+ const id = cells.find((cell) => BACKLOG_ID_PATTERN.test(cell));
127
+ return id ?? null;
128
+ };
129
+
130
+ export const collectBacklogOperationalStatusEntries = (
131
+ markdown: string
132
+ ): ReadonlyArray<{ id: string; status: BacklogStatusEmoji }> => {
133
+ const lines = markdown.split(/\r?\n/);
134
+ const entries: Array<{ id: string; status: BacklogStatusEmoji }> = [];
135
+ for (const line of lines) {
136
+ if (!line.trimStart().startsWith('|')) {
137
+ continue;
138
+ }
139
+ const cells = line.split('|').map((cell) => cell.trim());
140
+ const id = cells.find((cell) => BACKLOG_ID_PATTERN.test(cell));
141
+ const status = cells.find((cell) => STATUS_EMOJI_PATTERN.test(cell));
142
+ if (!id || !status) {
143
+ continue;
144
+ }
145
+ entries.push({
146
+ id,
147
+ status: status as BacklogStatusEmoji,
148
+ });
149
+ }
150
+ return entries;
151
+ };
152
+
153
+ export const buildBacklogStatusSummary = (markdown: string): BacklogStatusSummary => {
154
+ const entries = collectBacklogOperationalStatusEntries(markdown);
155
+ const closedIds = entries.filter((entry) => entry.status === '✅').map((entry) => entry.id);
156
+ const inProgressIds = entries.filter((entry) => entry.status === '🚧').map((entry) => entry.id);
157
+ const pendingIds = entries.filter((entry) => entry.status === '⏳').map((entry) => entry.id);
158
+ const blockedIds = entries.filter((entry) => entry.status === '⛔').map((entry) => entry.id);
159
+ return {
160
+ closed: closedIds.length,
161
+ inProgress: inProgressIds.length,
162
+ pending: pendingIds.length,
163
+ blocked: blockedIds.length,
164
+ inProgressIds: inProgressIds.sort(),
165
+ blockedIds: blockedIds.sort(),
166
+ };
167
+ };
168
+
169
+ export const applyBacklogIssueReferenceMapping = (params: {
170
+ markdown: string;
171
+ idIssueMap?: ReadonlyMap<string, number>;
172
+ }): {
173
+ updatedMarkdown: string;
174
+ changes: ReadonlyArray<BacklogIssueReferenceChange>;
175
+ } => {
176
+ if (!params.idIssueMap || params.idIssueMap.size === 0) {
177
+ return {
178
+ updatedMarkdown: params.markdown,
179
+ changes: [],
180
+ };
181
+ }
182
+
183
+ const lines = params.markdown.split(/\r?\n/);
184
+ const changes: BacklogIssueReferenceChange[] = [];
185
+
186
+ lines.forEach((line, index) => {
187
+ if (!line.includes('|')) {
188
+ return;
189
+ }
190
+ if (parseIssueNumber(line) !== null) {
191
+ return;
192
+ }
193
+ const id = parseBacklogId(line);
194
+ if (!id) {
195
+ return;
196
+ }
197
+ const issueNumber = params.idIssueMap?.get(id);
198
+ if (!issueNumber) {
199
+ return;
200
+ }
201
+ if (!PENDING_REFERENCE_PATTERN.test(line)) {
202
+ return;
203
+ }
204
+ const next = line.replace(PENDING_REFERENCE_PATTERN, `| #${issueNumber} |`);
205
+ if (next === line) {
206
+ return;
207
+ }
208
+ lines[index] = next;
209
+ changes.push({
210
+ id,
211
+ issueNumber,
212
+ lineNumber: index + 1,
213
+ from: 'Pendiente',
214
+ to: `#${issueNumber}`,
215
+ line,
216
+ });
217
+ });
218
+
219
+ return {
220
+ updatedMarkdown: lines.join('\n'),
221
+ changes,
222
+ };
223
+ };
224
+
225
+ const parseResolvedIssueNumber = (value: unknown): number | null => {
226
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
227
+ return Math.trunc(value);
228
+ }
229
+ if (typeof value === 'string') {
230
+ const parsed = Number.parseInt(value, 10);
231
+ if (Number.isFinite(parsed) && parsed > 0) {
232
+ return parsed;
233
+ }
234
+ }
235
+ return null;
236
+ };
237
+
238
+ const collectPendingReferenceIds = (markdown: string): ReadonlyArray<string> => {
239
+ const lines = markdown.split(/\r?\n/);
240
+ const ids = new Set<string>();
241
+ for (const line of lines) {
242
+ if (!line.includes('|')) {
243
+ continue;
244
+ }
245
+ if (parseIssueNumber(line) !== null) {
246
+ continue;
247
+ }
248
+ if (!PENDING_REFERENCE_PATTERN.test(line)) {
249
+ continue;
250
+ }
251
+ const id = parseBacklogId(line);
252
+ if (!id) {
253
+ continue;
254
+ }
255
+ ids.add(id);
256
+ }
257
+ return Array.from(ids).sort();
258
+ };
259
+
260
+ const formatSummaryLine = (params: {
261
+ emoji: BacklogStatusEmoji;
262
+ label: string;
263
+ count: number;
264
+ ids?: ReadonlyArray<string>;
265
+ }): string => {
266
+ const ids = params.ids ?? [];
267
+ if (ids.length === 0) {
268
+ return `- ${params.emoji} ${params.label}: ${params.count}`;
269
+ }
270
+ return `- ${params.emoji} ${params.label}: ${params.count} (${ids.map((id) => `\`${id}\``).join(', ')})`;
271
+ };
272
+
273
+ export const syncBacklogStatusSummary = (
274
+ markdown: string
275
+ ): { markdown: string; updated: boolean; summary: BacklogStatusSummary } => {
276
+ const lines = markdown.split(/\r?\n/);
277
+ const headerIndex = lines.findIndex((line) => line.trim() === '## Estado de este backlog');
278
+ const summary = buildBacklogStatusSummary(markdown);
279
+ if (headerIndex < 0) {
280
+ return {
281
+ markdown,
282
+ updated: false,
283
+ summary,
284
+ };
285
+ }
286
+
287
+ const start = headerIndex + 1;
288
+ let end = start;
289
+ while (end < lines.length && /^- (✅|🚧|⏳|⛔) /.test(lines[end].trim())) {
290
+ end += 1;
291
+ }
292
+
293
+ const nextLines = [
294
+ formatSummaryLine({
295
+ emoji: '✅',
296
+ label: 'Cerrados',
297
+ count: summary.closed,
298
+ }),
299
+ formatSummaryLine({
300
+ emoji: '🚧',
301
+ label: 'En construcción',
302
+ count: summary.inProgress,
303
+ ids: summary.inProgressIds,
304
+ }),
305
+ formatSummaryLine({
306
+ emoji: '⏳',
307
+ label: 'Pendientes',
308
+ count: summary.pending,
309
+ }),
310
+ formatSummaryLine({
311
+ emoji: '⛔',
312
+ label: 'Bloqueados',
313
+ count: summary.blocked,
314
+ ids: summary.blockedIds,
315
+ }),
316
+ ];
317
+
318
+ const current = lines.slice(start, end);
319
+ const changed = current.join('\n') !== nextLines.join('\n');
320
+ if (!changed) {
321
+ return {
322
+ markdown,
323
+ updated: false,
324
+ summary,
325
+ };
326
+ }
327
+
328
+ const updatedLines = [...lines.slice(0, start), ...nextLines, ...lines.slice(end)];
329
+ return {
330
+ markdown: updatedLines.join('\n'),
331
+ updated: true,
332
+ summary,
333
+ };
334
+ };
335
+
336
+ export const syncBacklogSectionHeadingStatus = (
337
+ markdown: string
338
+ ): {
339
+ markdown: string;
340
+ updated: boolean;
341
+ changes: ReadonlyArray<BacklogSectionHeadingChange>;
342
+ } => {
343
+ const lines = markdown.split(/\r?\n/);
344
+ const statusEntries = collectBacklogOperationalStatusEntries(markdown);
345
+ const idToStatus = new Map<string, BacklogStatusEmoji>();
346
+ for (const entry of statusEntries) {
347
+ if (!idToStatus.has(entry.id)) {
348
+ idToStatus.set(entry.id, entry.status);
349
+ }
350
+ }
351
+
352
+ const changes: BacklogSectionHeadingChange[] = [];
353
+ lines.forEach((line, index) => {
354
+ const match = BACKLOG_SECTION_HEADING_PATTERN.exec(line);
355
+ if (!match?.[2] || !match[4]) {
356
+ return;
357
+ }
358
+ const currentEmoji = match[2] as BacklogStatusEmoji;
359
+ const id = match[4];
360
+ const targetEmoji = idToStatus.get(id);
361
+ if (!targetEmoji || targetEmoji === currentEmoji) {
362
+ return;
363
+ }
364
+ const next = line.replace(BACKLOG_SECTION_HEADING_PATTERN, `$1${targetEmoji}$3$4`);
365
+ if (next === line) {
366
+ return;
367
+ }
368
+ lines[index] = next;
369
+ changes.push({
370
+ id,
371
+ lineNumber: index + 1,
372
+ from: currentEmoji,
373
+ to: targetEmoji,
374
+ line,
375
+ });
376
+ });
377
+
378
+ return {
379
+ markdown: lines.join('\n'),
380
+ updated: changes.length > 0,
381
+ changes,
382
+ };
383
+ };
384
+
385
+ export const syncBacklogNextStepNarrative = (params: {
386
+ markdown: string;
387
+ summary: BacklogStatusSummary;
388
+ }): { markdown: string; updated: boolean } => {
389
+ const heading = '## Próximo paso operativo (sin intervención manual en el seguimiento)';
390
+ if (
391
+ params.summary.inProgress !== 0 ||
392
+ params.summary.pending !== 0 ||
393
+ params.summary.blocked !== 0
394
+ ) {
395
+ return {
396
+ markdown: params.markdown,
397
+ updated: false,
398
+ };
399
+ }
400
+
401
+ const lines = params.markdown.split(/\r?\n/);
402
+ const headerIndex = lines.findIndex((line) => line.trim() === heading);
403
+ if (headerIndex < 0) {
404
+ return {
405
+ markdown: params.markdown,
406
+ updated: false,
407
+ };
408
+ }
409
+
410
+ const start = headerIndex + 1;
411
+ let end = start;
412
+ while (end < lines.length && !lines[end].trimStart().startsWith('## ')) {
413
+ end += 1;
414
+ }
415
+
416
+ const replacement = [
417
+ '- Objetivo inmediato:',
418
+ ' - Backlog cerrado al 100% (sin tareas activas).',
419
+ '- Entregables de este estado:',
420
+ ' - Mantener monitorización de nuevos hallazgos y abrir issue upstream al primer incidente real.',
421
+ ' - Reejecutar reconciliación automática cuando entren nuevas filas en el backlog consumidor.',
422
+ '- Regla de continuidad:',
423
+ ' - Si entra una incidencia nueva, marcar exactamente una `🚧` y mantener el resto en `⏳` o `✅` según estado real.',
424
+ ];
425
+
426
+ const current = lines.slice(start, end);
427
+ const changed = current.join('\n') !== replacement.join('\n');
428
+ if (!changed) {
429
+ return {
430
+ markdown: params.markdown,
431
+ updated: false,
432
+ };
433
+ }
434
+
435
+ const updatedLines = [...lines.slice(0, start), ...replacement, ...lines.slice(end)];
436
+ return {
437
+ markdown: updatedLines.join('\n'),
438
+ updated: true,
439
+ };
440
+ };
441
+
442
+ const deriveTargetEmoji = (params: {
443
+ current: BacklogStatusEmoji;
444
+ issueState: BacklogIssueState;
445
+ }): BacklogStatusEmoji | null => {
446
+ if (params.issueState === 'CLOSED') {
447
+ return params.current === '✅' ? null : '✅';
448
+ }
449
+ if (params.current === '✅') {
450
+ return '⏳';
451
+ }
452
+ return null;
453
+ };
454
+
455
+ const replaceFirstEmoji = (line: string, emoji: BacklogStatusEmoji): string =>
456
+ line.replace(STATUS_EMOJI_PATTERN, emoji);
457
+
458
+ const resolveIssueStateWithGh = (issueNumber: number, repo?: string): BacklogIssueState => {
459
+ const args = ['issue', 'view', String(issueNumber), '--json', 'state'];
460
+ if (typeof repo === 'string' && repo.trim().length > 0) {
461
+ args.push('--repo', repo.trim());
462
+ }
463
+ const stdout = execFileSync('gh', args, {
464
+ encoding: 'utf8',
465
+ stdio: ['ignore', 'pipe', 'pipe'],
466
+ });
467
+ const parsed = JSON.parse(stdout) as { state?: unknown };
468
+ return parsed.state === 'CLOSED' ? 'CLOSED' : 'OPEN';
469
+ };
470
+
471
+ export const reconcileBacklogMarkdown = (params: {
472
+ markdown: string;
473
+ issueStates: ReadonlyMap<number, BacklogIssueState>;
474
+ }): {
475
+ updatedMarkdown: string;
476
+ changes: ReadonlyArray<BacklogIssueChange>;
477
+ summaryUpdated: boolean;
478
+ nextStepUpdated: boolean;
479
+ headingUpdated: boolean;
480
+ summary: BacklogStatusSummary;
481
+ headingChanges: ReadonlyArray<BacklogSectionHeadingChange>;
482
+ } => {
483
+ const entries = collectBacklogIssueEntries(params.markdown);
484
+ const byLine = new Map<number, BacklogIssueChange>();
485
+
486
+ for (const entry of entries) {
487
+ const issueState = params.issueStates.get(entry.issueNumber);
488
+ if (!issueState) {
489
+ continue;
490
+ }
491
+ const targetEmoji = deriveTargetEmoji({
492
+ current: entry.currentEmoji,
493
+ issueState,
494
+ });
495
+ if (!targetEmoji || targetEmoji === entry.currentEmoji) {
496
+ continue;
497
+ }
498
+ byLine.set(entry.lineNumber, {
499
+ issueNumber: entry.issueNumber,
500
+ lineNumber: entry.lineNumber,
501
+ from: entry.currentEmoji,
502
+ to: targetEmoji,
503
+ issueState,
504
+ line: entry.line,
505
+ });
506
+ }
507
+
508
+ if (byLine.size === 0) {
509
+ const syncedOnlySummary = syncBacklogStatusSummary(params.markdown);
510
+ const syncedNarrative = syncBacklogNextStepNarrative({
511
+ markdown: syncedOnlySummary.markdown,
512
+ summary: syncedOnlySummary.summary,
513
+ });
514
+ const syncedHeadings = syncBacklogSectionHeadingStatus(syncedNarrative.markdown);
515
+ return {
516
+ updatedMarkdown: syncedHeadings.markdown,
517
+ changes: [],
518
+ summaryUpdated: syncedOnlySummary.updated,
519
+ nextStepUpdated: syncedNarrative.updated,
520
+ headingUpdated: syncedHeadings.updated,
521
+ summary: syncedOnlySummary.summary,
522
+ headingChanges: syncedHeadings.changes,
523
+ };
524
+ }
525
+
526
+ const lines = params.markdown.split(/\r?\n/);
527
+ for (const change of byLine.values()) {
528
+ const index = change.lineNumber - 1;
529
+ const current = lines[index];
530
+ if (typeof current !== 'string') {
531
+ continue;
532
+ }
533
+ lines[index] = replaceFirstEmoji(current, change.to);
534
+ }
535
+
536
+ const reconciledMarkdown = lines.join('\n');
537
+ const syncedSummary = syncBacklogStatusSummary(reconciledMarkdown);
538
+ const syncedNarrative = syncBacklogNextStepNarrative({
539
+ markdown: syncedSummary.markdown,
540
+ summary: syncedSummary.summary,
541
+ });
542
+ const syncedHeadings = syncBacklogSectionHeadingStatus(syncedNarrative.markdown);
543
+ return {
544
+ updatedMarkdown: syncedHeadings.markdown,
545
+ changes: Array.from(byLine.values()).sort((a, b) => a.lineNumber - b.lineNumber),
546
+ summaryUpdated: syncedSummary.updated,
547
+ nextStepUpdated: syncedNarrative.updated,
548
+ headingUpdated: syncedHeadings.updated,
549
+ summary: syncedSummary.summary,
550
+ headingChanges: syncedHeadings.changes,
551
+ };
552
+ };
553
+
554
+ export const runBacklogIssuesReconcile = async (params: {
555
+ filePath: string;
556
+ repo?: string;
557
+ apply?: boolean;
558
+ mappingSource?: BacklogMappingSource;
559
+ idIssueMap?: ReadonlyMap<string, number>;
560
+ resolveIssueNumberById?: BacklogIssueNumberResolver;
561
+ readFile?: (path: string) => string;
562
+ writeFile?: (path: string, contents: string) => void;
563
+ resolveIssueState?: (issueNumber: number, repo?: string) => BacklogIssueState | Promise<BacklogIssueState>;
564
+ }): Promise<BacklogReconcileResult> => {
565
+ const filePath = resolve(params.filePath);
566
+ const readFile = params.readFile ?? ((path: string) => readFileSync(path, 'utf8'));
567
+ const writeFile = params.writeFile ?? ((path: string, contents: string) => writeFileSync(path, contents, 'utf8'));
568
+ const resolveIssueState = params.resolveIssueState ?? resolveIssueStateWithGh;
569
+
570
+ const markdown = readFile(filePath);
571
+ const providedIssueMap = new Map<string, number>(
572
+ params.idIssueMap ? Array.from(params.idIssueMap.entries()) : []
573
+ );
574
+ const pendingIds = collectPendingReferenceIds(markdown);
575
+ const resolvedByProvidedMapSet = new Set<string>(pendingIds.filter((id) => providedIssueMap.has(id)));
576
+ const idIssueMap = new Map<string, number>(providedIssueMap);
577
+ const resolveIssueNumberById = params.resolveIssueNumberById;
578
+ const resolvedByLookupSet = new Set<string>();
579
+ if (typeof resolveIssueNumberById === 'function') {
580
+ for (const backlogId of pendingIds) {
581
+ if (idIssueMap.has(backlogId)) {
582
+ continue;
583
+ }
584
+ const resolvedIssue = await resolveIssueNumberById(backlogId, params.repo);
585
+ const parsedIssue = parseResolvedIssueNumber(resolvedIssue);
586
+ if (parsedIssue === null) {
587
+ continue;
588
+ }
589
+ idIssueMap.set(backlogId, parsedIssue);
590
+ resolvedByLookupSet.add(backlogId);
591
+ }
592
+ }
593
+ const mapped = applyBacklogIssueReferenceMapping({
594
+ markdown,
595
+ idIssueMap,
596
+ });
597
+ const unresolvedReferenceIds = collectPendingReferenceIds(mapped.updatedMarkdown);
598
+ const referenceResolution: BacklogReferenceResolution = {
599
+ resolvedByProvidedMap: Array.from(resolvedByProvidedMapSet).sort(),
600
+ resolvedByLookup: Array.from(resolvedByLookupSet).sort(),
601
+ unresolvedReferenceIds,
602
+ };
603
+ const entries = collectBacklogIssueEntries(mapped.updatedMarkdown);
604
+ const uniqueIssueNumbers = Array.from(new Set(entries.map((entry) => entry.issueNumber))).sort((a, b) => a - b);
605
+
606
+ const issueStates = new Map<number, BacklogIssueState>();
607
+ for (const issueNumber of uniqueIssueNumbers) {
608
+ issueStates.set(issueNumber, await resolveIssueState(issueNumber, params.repo));
609
+ }
610
+
611
+ const reconciled = reconcileBacklogMarkdown({
612
+ markdown: mapped.updatedMarkdown,
613
+ issueStates,
614
+ });
615
+
616
+ const apply = params.apply === true;
617
+ if (
618
+ apply &&
619
+ (mapped.changes.length > 0 ||
620
+ reconciled.changes.length > 0 ||
621
+ reconciled.summaryUpdated ||
622
+ reconciled.nextStepUpdated ||
623
+ reconciled.headingUpdated)
624
+ ) {
625
+ writeFile(filePath, reconciled.updatedMarkdown);
626
+ }
627
+
628
+ return {
629
+ filePath,
630
+ repo: params.repo,
631
+ apply,
632
+ mappingSource: params.mappingSource ?? (providedIssueMap.size > 0 ? 'json' : 'none'),
633
+ entriesScanned: entries.length,
634
+ issuesResolved: uniqueIssueNumbers.length,
635
+ referenceChanges: mapped.changes,
636
+ referenceResolution,
637
+ changes: reconciled.changes,
638
+ summaryUpdated: reconciled.summaryUpdated,
639
+ nextStepUpdated: reconciled.nextStepUpdated,
640
+ headingUpdated: reconciled.headingUpdated,
641
+ summary: reconciled.summary,
642
+ headingChanges: reconciled.headingChanges,
643
+ updated:
644
+ apply &&
645
+ (mapped.changes.length > 0 ||
646
+ reconciled.changes.length > 0 ||
647
+ reconciled.summaryUpdated ||
648
+ reconciled.nextStepUpdated ||
649
+ reconciled.headingUpdated),
650
+ };
651
+ };