pumuki 6.3.39 → 6.3.41
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/README.md +21 -12
- package/VERSION +1 -1
- package/core/gate/evaluateRules.test.ts +40 -0
- package/core/gate/evaluateRules.ts +7 -1
- package/core/rules/Consequence.ts +1 -0
- package/docs/CONFIGURATION.md +50 -0
- package/docs/INSTALLATION.md +38 -11
- package/docs/MCP_SERVERS.md +1 -1
- package/docs/README.md +1 -0
- package/docs/RELEASE_NOTES.md +58 -0
- package/docs/USAGE.md +191 -9
- package/docs/registro-maestro-de-seguimiento.md +2 -2
- package/docs/seguimiento-activo-pumuki-saas-supermercados.md +1629 -1
- package/docs/validation/README.md +2 -1
- package/docs/validation/ast-intelligence-roadmap.md +96 -0
- package/integrations/config/skillsCustomRules.ts +14 -0
- package/integrations/config/skillsDetectorRegistry.ts +11 -1
- package/integrations/config/skillsLock.ts +30 -0
- package/integrations/config/skillsMarkdownRules.ts +14 -3
- package/integrations/config/skillsRuleSet.ts +25 -3
- package/integrations/evidence/readEvidence.test.ts +3 -2
- package/integrations/evidence/readEvidence.ts +14 -4
- package/integrations/evidence/repoState.ts +10 -2
- package/integrations/evidence/schema.test.ts +3 -2
- package/integrations/evidence/schema.ts +3 -0
- package/integrations/evidence/writeEvidence.test.ts +3 -2
- package/integrations/gate/evaluateAiGate.ts +511 -2
- package/integrations/git/GitService.ts +5 -1
- package/integrations/git/astIntelligenceDualValidation.ts +275 -0
- package/integrations/git/gitAtomicity.ts +42 -9
- package/integrations/git/resolveGitRefs.ts +37 -0
- package/integrations/git/runPlatformGate.ts +228 -1
- package/integrations/git/runPlatformGateEvaluation.ts +4 -0
- package/integrations/git/stageRunners.ts +116 -2
- package/integrations/lifecycle/cli.ts +759 -22
- package/integrations/lifecycle/doctor.ts +62 -0
- package/integrations/lifecycle/index.ts +1 -0
- package/integrations/lifecycle/packageInfo.ts +25 -3
- package/integrations/lifecycle/policyReconcile.ts +304 -0
- package/integrations/lifecycle/preWriteAutomation.ts +42 -2
- package/integrations/lifecycle/watch.ts +365 -0
- package/integrations/mcp/aiGateCheck.ts +59 -2
- package/integrations/mcp/autoExecuteAiStart.ts +25 -1
- package/integrations/mcp/preFlightCheck.ts +13 -0
- package/integrations/sdd/evidenceScaffold.ts +223 -0
- package/integrations/sdd/index.ts +2 -0
- package/integrations/sdd/stateSync.ts +400 -0
- package/integrations/sdd/syncDocs.ts +97 -2
- package/package.json +4 -1
- package/scripts/backlog-action-reasons-lib.ts +38 -0
- package/scripts/backlog-id-issue-map-lib.ts +69 -0
- package/scripts/backlog-json-contract-lib.ts +3 -0
- package/scripts/framework-menu-consumer-preflight-lib.ts +6 -0
- package/scripts/framework-menu-system-notifications-lib.ts +66 -6
- package/scripts/package-install-smoke-command-resolution-lib.ts +64 -0
- package/scripts/package-install-smoke-consumer-npm-lib.ts +43 -0
- package/scripts/package-install-smoke-consumer-repo-setup-lib.ts +2 -0
- package/scripts/package-install-smoke-execution-steps-lib.ts +27 -9
- package/scripts/package-install-smoke-lifecycle-lib.ts +15 -4
- package/scripts/package-install-smoke-workspace-factory-lib.ts +4 -1
- package/scripts/reconcile-consumer-backlog-issues-lib.ts +651 -0
- package/scripts/reconcile-consumer-backlog-issues.ts +348 -0
- package/scripts/watch-consumer-backlog-lib.ts +465 -0
- package/scripts/watch-consumer-backlog.ts +326 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export type BacklogIssueState = 'OPEN' | 'CLOSED';
|
|
6
|
+
export type BacklogStatusEmoji = '✅' | '🚧' | '⏳' | '⛔';
|
|
7
|
+
|
|
8
|
+
export type BacklogWatchEntry = {
|
|
9
|
+
id: string;
|
|
10
|
+
status: BacklogStatusEmoji;
|
|
11
|
+
issueNumber: number | null;
|
|
12
|
+
lineNumber: number;
|
|
13
|
+
line: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type BacklogWatchClassification = {
|
|
17
|
+
needsIssue: ReadonlyArray<BacklogWatchEntry>;
|
|
18
|
+
driftClosedIssue: ReadonlyArray<BacklogWatchEntry>;
|
|
19
|
+
activeIssue: ReadonlyArray<BacklogWatchEntry>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type BacklogWatchResolutionTrace = {
|
|
23
|
+
resolvedByMap: ReadonlyArray<string>;
|
|
24
|
+
resolvedByGhLookup: ReadonlyArray<string>;
|
|
25
|
+
unresolvedIds: ReadonlyArray<string>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type BacklogHeadingDriftEntry = {
|
|
29
|
+
id: string;
|
|
30
|
+
lineNumber: number;
|
|
31
|
+
headingStatus: BacklogStatusEmoji;
|
|
32
|
+
effectiveStatus: BacklogStatusEmoji;
|
|
33
|
+
line: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type BacklogWatchResult = {
|
|
37
|
+
filePath: string;
|
|
38
|
+
repo?: string;
|
|
39
|
+
entriesScanned: number;
|
|
40
|
+
nonClosedEntries: number;
|
|
41
|
+
issueStatesResolved: number;
|
|
42
|
+
classification: BacklogWatchClassification;
|
|
43
|
+
resolution: BacklogWatchResolutionTrace;
|
|
44
|
+
headingDrift: ReadonlyArray<BacklogHeadingDriftEntry>;
|
|
45
|
+
hasActionRequired: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type BacklogWatchIdIssueMap = Readonly<Record<string, number>>;
|
|
49
|
+
export type BacklogIssueNumberResolver = (
|
|
50
|
+
backlogId: string,
|
|
51
|
+
repo?: string
|
|
52
|
+
) => number | null | Promise<number | null>;
|
|
53
|
+
|
|
54
|
+
const STATUS_EMOJI_PATTERN = /(✅|🚧|⏳|⛔)/;
|
|
55
|
+
const ISSUE_REF_PATTERN = /#(\d+)/;
|
|
56
|
+
const BACKLOG_ID_PATTERN = /^(PUMUKI-(?:M)?\d+|PUMUKI-INC-\d+|FP-\d+|AST-GAP-\d+)$/;
|
|
57
|
+
const STATUS_TEXT_PATTERN = /^(OPEN|PENDING|REPORTED|IN_PROGRESS|BLOCKED|FIXED|CLOSED)\b/i;
|
|
58
|
+
const BACKLOG_SECTION_HEADING_PATTERN =
|
|
59
|
+
/^(\s*###\s*)(✅|🚧|⏳|⛔)(\s+)(PUMUKI-(?:M)?\d+|PUMUKI-INC-\d+|FP-\d+|AST-GAP-\d+)\b/;
|
|
60
|
+
const STATUS_TEXT_TO_EMOJI: Record<string, BacklogStatusEmoji> = {
|
|
61
|
+
OPEN: '⏳',
|
|
62
|
+
PENDING: '⏳',
|
|
63
|
+
REPORTED: '🚧',
|
|
64
|
+
IN_PROGRESS: '🚧',
|
|
65
|
+
BLOCKED: '⛔',
|
|
66
|
+
FIXED: '✅',
|
|
67
|
+
CLOSED: '✅',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const parseIssueNumberFromText = (text: string): number | null => {
|
|
71
|
+
const match = ISSUE_REF_PATTERN.exec(text);
|
|
72
|
+
if (!match?.[1]) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const parsed = Number.parseInt(match[1], 10);
|
|
76
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const parseIssueNumberFromCells = (cells: ReadonlyArray<string>, fallbackLine: string): number | null => {
|
|
80
|
+
const candidates: Array<{ issueNumber: number; score: number; index: number }> = [];
|
|
81
|
+
cells.forEach((cell, index) => {
|
|
82
|
+
const issueNumber = parseIssueNumberFromText(cell);
|
|
83
|
+
if (issueNumber === null) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let score = 0;
|
|
87
|
+
if (STATUS_EMOJI_PATTERN.test(cell) || STATUS_TEXT_PATTERN.test(cell)) {
|
|
88
|
+
score += 3;
|
|
89
|
+
}
|
|
90
|
+
if (/\b(ref|upstream|referencia|estado|issue)\b/i.test(cell)) {
|
|
91
|
+
score += 2;
|
|
92
|
+
}
|
|
93
|
+
candidates.push({
|
|
94
|
+
issueNumber,
|
|
95
|
+
score,
|
|
96
|
+
index,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
if (candidates.length > 0) {
|
|
100
|
+
candidates.sort((a, b) => {
|
|
101
|
+
if (b.score !== a.score) {
|
|
102
|
+
return b.score - a.score;
|
|
103
|
+
}
|
|
104
|
+
return b.index - a.index;
|
|
105
|
+
});
|
|
106
|
+
return candidates[0]?.issueNumber ?? null;
|
|
107
|
+
}
|
|
108
|
+
return parseIssueNumberFromText(fallbackLine);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const collectBacklogWatchEntries = (markdown: string): ReadonlyArray<BacklogWatchEntry> => {
|
|
112
|
+
const lines = markdown.split(/\r?\n/);
|
|
113
|
+
const entries: BacklogWatchEntry[] = [];
|
|
114
|
+
|
|
115
|
+
lines.forEach((line, index) => {
|
|
116
|
+
if (!line.trimStart().startsWith('|')) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const cells = line.split('|').map((cell) => cell.trim());
|
|
120
|
+
const id = cells.find((cell) => BACKLOG_ID_PATTERN.test(cell));
|
|
121
|
+
const statusEmojiCell = cells.find((cell) => STATUS_EMOJI_PATTERN.test(cell));
|
|
122
|
+
const statusEmojiMatch =
|
|
123
|
+
typeof statusEmojiCell === 'string' ? STATUS_EMOJI_PATTERN.exec(statusEmojiCell) : null;
|
|
124
|
+
const statusEmoji = statusEmojiMatch?.[1] as BacklogStatusEmoji | undefined;
|
|
125
|
+
const statusTextCell = cells.find((cell) => STATUS_TEXT_PATTERN.test(cell));
|
|
126
|
+
const statusTextMatch =
|
|
127
|
+
typeof statusTextCell === 'string' ? STATUS_TEXT_PATTERN.exec(statusTextCell) : null;
|
|
128
|
+
const normalizedText = statusTextMatch?.[1]?.toUpperCase().replace(/\s+/g, '_') ?? null;
|
|
129
|
+
const mappedTextEmoji = normalizedText ? STATUS_TEXT_TO_EMOJI[normalizedText] : undefined;
|
|
130
|
+
const status = statusEmoji ?? mappedTextEmoji;
|
|
131
|
+
if (!id || !status) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
entries.push({
|
|
135
|
+
id,
|
|
136
|
+
status: (statusEmoji ?? mappedTextEmoji) as BacklogStatusEmoji,
|
|
137
|
+
issueNumber: parseIssueNumberFromCells(cells, line),
|
|
138
|
+
lineNumber: index + 1,
|
|
139
|
+
line,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return entries;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const dedupeBacklogWatchEntriesById = (
|
|
147
|
+
entries: ReadonlyArray<BacklogWatchEntry>
|
|
148
|
+
): ReadonlyArray<BacklogWatchEntry> => {
|
|
149
|
+
const byId = new Map<string, BacklogWatchEntry>();
|
|
150
|
+
|
|
151
|
+
const score = (entry: BacklogWatchEntry): number => {
|
|
152
|
+
let value = 0;
|
|
153
|
+
if (typeof entry.issueNumber === 'number') {
|
|
154
|
+
value += 2;
|
|
155
|
+
}
|
|
156
|
+
if (entry.line.includes('REPORTED (#') || entry.line.includes('FIXED (#')) {
|
|
157
|
+
value += 1;
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
const current = byId.get(entry.id);
|
|
164
|
+
if (!current) {
|
|
165
|
+
byId.set(entry.id, entry);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const currentScore = score(current);
|
|
169
|
+
const nextScore = score(entry);
|
|
170
|
+
if (nextScore > currentScore) {
|
|
171
|
+
byId.set(entry.id, entry);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (nextScore === currentScore && entry.lineNumber > current.lineNumber) {
|
|
175
|
+
byId.set(entry.id, entry);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return Array.from(byId.values()).sort((a, b) => a.lineNumber - b.lineNumber);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const collectBacklogIdIssueMap = (
|
|
183
|
+
markdown: string
|
|
184
|
+
): BacklogWatchIdIssueMap => {
|
|
185
|
+
const entries = dedupeBacklogWatchEntriesById(collectBacklogWatchEntries(markdown));
|
|
186
|
+
const map: Record<string, number> = {};
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
if (typeof entry.issueNumber !== 'number') {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
map[entry.id] = entry.issueNumber;
|
|
192
|
+
}
|
|
193
|
+
return map;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const collectBacklogHeadingEntries = (
|
|
197
|
+
markdown: string
|
|
198
|
+
): ReadonlyArray<{ id: string; status: BacklogStatusEmoji; lineNumber: number; line: string }> => {
|
|
199
|
+
const lines = markdown.split(/\r?\n/);
|
|
200
|
+
const entries: Array<{ id: string; status: BacklogStatusEmoji; lineNumber: number; line: string }> = [];
|
|
201
|
+
lines.forEach((line, index) => {
|
|
202
|
+
const match = BACKLOG_SECTION_HEADING_PATTERN.exec(line);
|
|
203
|
+
if (!match?.[2] || !match[4]) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
entries.push({
|
|
207
|
+
id: match[4],
|
|
208
|
+
status: match[2] as BacklogStatusEmoji,
|
|
209
|
+
lineNumber: index + 1,
|
|
210
|
+
line,
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
return entries;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export const collectBacklogHeadingDrift = (markdown: string): ReadonlyArray<BacklogHeadingDriftEntry> => {
|
|
217
|
+
const effectiveEntries = dedupeBacklogWatchEntriesById(collectBacklogWatchEntries(markdown));
|
|
218
|
+
const effectiveStatusById = new Map<string, BacklogStatusEmoji>();
|
|
219
|
+
for (const entry of effectiveEntries) {
|
|
220
|
+
if (!effectiveStatusById.has(entry.id)) {
|
|
221
|
+
effectiveStatusById.set(entry.id, entry.status);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const headingEntries = collectBacklogHeadingEntries(markdown);
|
|
226
|
+
const drift: BacklogHeadingDriftEntry[] = [];
|
|
227
|
+
for (const heading of headingEntries) {
|
|
228
|
+
const effectiveStatus = effectiveStatusById.get(heading.id);
|
|
229
|
+
if (!effectiveStatus || effectiveStatus === heading.status) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
drift.push({
|
|
233
|
+
id: heading.id,
|
|
234
|
+
lineNumber: heading.lineNumber,
|
|
235
|
+
headingStatus: heading.status,
|
|
236
|
+
effectiveStatus,
|
|
237
|
+
line: heading.line,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return drift;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const resolveIssueStateWithGh = (issueNumber: number, repo?: string): BacklogIssueState => {
|
|
245
|
+
const args = ['issue', 'view', String(issueNumber), '--json', 'state'];
|
|
246
|
+
if (typeof repo === 'string' && repo.trim().length > 0) {
|
|
247
|
+
args.push('--repo', repo.trim());
|
|
248
|
+
}
|
|
249
|
+
const stdout = execFileSync('gh', args, {
|
|
250
|
+
encoding: 'utf8',
|
|
251
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
252
|
+
});
|
|
253
|
+
const parsed = JSON.parse(stdout) as { state?: unknown };
|
|
254
|
+
return parsed.state === 'CLOSED' ? 'CLOSED' : 'OPEN';
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
type GhIssueSearchItem = {
|
|
258
|
+
number?: unknown;
|
|
259
|
+
title?: unknown;
|
|
260
|
+
state?: unknown;
|
|
261
|
+
updatedAt?: unknown;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const escapeRegex = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
265
|
+
|
|
266
|
+
const parseFiniteIssueNumber = (value: unknown): number | null => {
|
|
267
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
268
|
+
return Math.trunc(value);
|
|
269
|
+
}
|
|
270
|
+
if (typeof value === 'string') {
|
|
271
|
+
const parsed = Number.parseInt(value, 10);
|
|
272
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export const resolveIssueNumberByIdWithGh: BacklogIssueNumberResolver = (backlogId, repo) => {
|
|
278
|
+
const normalizedId = backlogId.trim();
|
|
279
|
+
if (normalizedId.length === 0) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const args = [
|
|
283
|
+
'issue',
|
|
284
|
+
'list',
|
|
285
|
+
'--state',
|
|
286
|
+
'all',
|
|
287
|
+
'--limit',
|
|
288
|
+
'20',
|
|
289
|
+
'--search',
|
|
290
|
+
`${normalizedId} in:title,body`,
|
|
291
|
+
'--json',
|
|
292
|
+
'number,title,state,updatedAt',
|
|
293
|
+
];
|
|
294
|
+
if (typeof repo === 'string' && repo.trim().length > 0) {
|
|
295
|
+
args.push('--repo', repo.trim());
|
|
296
|
+
}
|
|
297
|
+
const stdout = execFileSync('gh', args, {
|
|
298
|
+
encoding: 'utf8',
|
|
299
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
300
|
+
});
|
|
301
|
+
const parsed = JSON.parse(stdout) as unknown;
|
|
302
|
+
if (!Array.isArray(parsed)) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const tokenRegex = new RegExp(`\\b${escapeRegex(normalizedId)}\\b`, 'i');
|
|
307
|
+
const candidates = parsed
|
|
308
|
+
.map((item) => item as GhIssueSearchItem)
|
|
309
|
+
.map((item, index) => {
|
|
310
|
+
const issueNumber = parseFiniteIssueNumber(item.number);
|
|
311
|
+
const title = typeof item.title === 'string' ? item.title : '';
|
|
312
|
+
const state = item.state === 'OPEN' ? 'OPEN' : item.state === 'CLOSED' ? 'CLOSED' : 'OPEN';
|
|
313
|
+
const updatedAt = typeof item.updatedAt === 'string' ? item.updatedAt : '';
|
|
314
|
+
const exactTitleMatch = tokenRegex.test(title);
|
|
315
|
+
const updatedAtMs = Number.isFinite(Date.parse(updatedAt)) ? Date.parse(updatedAt) : 0;
|
|
316
|
+
if (issueNumber === null) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
let score = 0;
|
|
320
|
+
if (exactTitleMatch) {
|
|
321
|
+
score += 4;
|
|
322
|
+
}
|
|
323
|
+
if (state === 'OPEN') {
|
|
324
|
+
score += 2;
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
issueNumber,
|
|
328
|
+
score,
|
|
329
|
+
updatedAtMs,
|
|
330
|
+
index,
|
|
331
|
+
};
|
|
332
|
+
})
|
|
333
|
+
.filter(
|
|
334
|
+
(entry): entry is { issueNumber: number; score: number; updatedAtMs: number; index: number } =>
|
|
335
|
+
entry !== null
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if (candidates.length === 0) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
candidates.sort((a, b) => {
|
|
343
|
+
if (b.score !== a.score) {
|
|
344
|
+
return b.score - a.score;
|
|
345
|
+
}
|
|
346
|
+
if (b.updatedAtMs !== a.updatedAtMs) {
|
|
347
|
+
return b.updatedAtMs - a.updatedAtMs;
|
|
348
|
+
}
|
|
349
|
+
if (b.issueNumber !== a.issueNumber) {
|
|
350
|
+
return b.issueNumber - a.issueNumber;
|
|
351
|
+
}
|
|
352
|
+
return a.index - b.index;
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
return candidates[0]?.issueNumber ?? null;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
export const runBacklogWatch = async (params: {
|
|
359
|
+
filePath: string;
|
|
360
|
+
repo?: string;
|
|
361
|
+
idIssueMap?: BacklogWatchIdIssueMap;
|
|
362
|
+
readFile?: (path: string) => string;
|
|
363
|
+
resolveIssueNumberById?: BacklogIssueNumberResolver;
|
|
364
|
+
resolveIssueState?: (issueNumber: number, repo?: string) => BacklogIssueState | Promise<BacklogIssueState>;
|
|
365
|
+
}): Promise<BacklogWatchResult> => {
|
|
366
|
+
const filePath = resolve(params.filePath);
|
|
367
|
+
const readFile = params.readFile ?? ((path: string) => readFileSync(path, 'utf8'));
|
|
368
|
+
const resolveIssueState = params.resolveIssueState ?? resolveIssueStateWithGh;
|
|
369
|
+
|
|
370
|
+
const markdown = readFile(filePath);
|
|
371
|
+
const entries = collectBacklogWatchEntries(markdown);
|
|
372
|
+
const headingDrift = collectBacklogHeadingDrift(markdown);
|
|
373
|
+
const nonClosedRaw = entries.filter((entry) => entry.status !== '✅');
|
|
374
|
+
const nonClosed = dedupeBacklogWatchEntriesById(nonClosedRaw);
|
|
375
|
+
const resolvedByMapSet = new Set<string>();
|
|
376
|
+
const nonClosedWithIssueMap = nonClosed.map((entry) => {
|
|
377
|
+
if (entry.issueNumber !== null) {
|
|
378
|
+
return entry;
|
|
379
|
+
}
|
|
380
|
+
const mappedIssue = params.idIssueMap?.[entry.id];
|
|
381
|
+
if (typeof mappedIssue !== 'number' || !Number.isFinite(mappedIssue)) {
|
|
382
|
+
return entry;
|
|
383
|
+
}
|
|
384
|
+
resolvedByMapSet.add(entry.id);
|
|
385
|
+
return {
|
|
386
|
+
...entry,
|
|
387
|
+
issueNumber: Math.trunc(mappedIssue),
|
|
388
|
+
};
|
|
389
|
+
});
|
|
390
|
+
const resolveIssueNumberById = params.resolveIssueNumberById;
|
|
391
|
+
const resolvedIssueById = new Map<string, number>();
|
|
392
|
+
const resolvedByGhLookupSet = new Set<string>();
|
|
393
|
+
if (typeof resolveIssueNumberById === 'function') {
|
|
394
|
+
const unresolvedIds = Array.from(
|
|
395
|
+
new Set(nonClosedWithIssueMap.filter((entry) => entry.issueNumber === null).map((entry) => entry.id))
|
|
396
|
+
);
|
|
397
|
+
for (const backlogId of unresolvedIds) {
|
|
398
|
+
const resolvedIssue = await resolveIssueNumberById(backlogId, params.repo);
|
|
399
|
+
const parsedIssue = parseFiniteIssueNumber(resolvedIssue);
|
|
400
|
+
if (parsedIssue === null) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
resolvedIssueById.set(backlogId, parsedIssue);
|
|
404
|
+
resolvedByGhLookupSet.add(backlogId);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const nonClosedWithIssueResolution = nonClosedWithIssueMap.map((entry) => {
|
|
408
|
+
if (entry.issueNumber !== null) {
|
|
409
|
+
return entry;
|
|
410
|
+
}
|
|
411
|
+
const resolvedIssue = resolvedIssueById.get(entry.id);
|
|
412
|
+
if (typeof resolvedIssue !== 'number') {
|
|
413
|
+
return entry;
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
...entry,
|
|
417
|
+
issueNumber: resolvedIssue,
|
|
418
|
+
};
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const issueNumbers = Array.from(
|
|
422
|
+
new Set(
|
|
423
|
+
nonClosedWithIssueResolution
|
|
424
|
+
.map((entry) => entry.issueNumber)
|
|
425
|
+
.filter((value): value is number => typeof value === 'number')
|
|
426
|
+
)
|
|
427
|
+
).sort((a, b) => a - b);
|
|
428
|
+
|
|
429
|
+
const issueStates = new Map<number, BacklogIssueState>();
|
|
430
|
+
for (const issueNumber of issueNumbers) {
|
|
431
|
+
issueStates.set(issueNumber, await resolveIssueState(issueNumber, params.repo));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const needsIssue = nonClosedWithIssueResolution.filter((entry) => entry.issueNumber === null);
|
|
435
|
+
const driftClosedIssue = nonClosedWithIssueResolution.filter(
|
|
436
|
+
(entry) => typeof entry.issueNumber === 'number' && issueStates.get(entry.issueNumber) === 'CLOSED'
|
|
437
|
+
);
|
|
438
|
+
const activeIssue = nonClosedWithIssueResolution.filter(
|
|
439
|
+
(entry) => typeof entry.issueNumber === 'number' && issueStates.get(entry.issueNumber) === 'OPEN'
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
const unresolvedIds = Array.from(new Set(needsIssue.map((entry) => entry.id))).sort();
|
|
443
|
+
const resolvedByMap = Array.from(resolvedByMapSet).sort();
|
|
444
|
+
const resolvedByGhLookup = Array.from(resolvedByGhLookupSet).sort();
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
filePath,
|
|
448
|
+
repo: params.repo,
|
|
449
|
+
entriesScanned: entries.length,
|
|
450
|
+
nonClosedEntries: nonClosed.length,
|
|
451
|
+
issueStatesResolved: issueNumbers.length,
|
|
452
|
+
classification: {
|
|
453
|
+
needsIssue,
|
|
454
|
+
driftClosedIssue,
|
|
455
|
+
activeIssue,
|
|
456
|
+
},
|
|
457
|
+
resolution: {
|
|
458
|
+
resolvedByMap,
|
|
459
|
+
resolvedByGhLookup,
|
|
460
|
+
unresolvedIds,
|
|
461
|
+
},
|
|
462
|
+
headingDrift,
|
|
463
|
+
hasActionRequired: needsIssue.length > 0 || driftClosedIssue.length > 0 || headingDrift.length > 0,
|
|
464
|
+
};
|
|
465
|
+
};
|