@papi-ai/adapter-md 0.1.0-alpha → 0.1.1-alpha
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/LICENSE +98 -0
- package/dist/index.d.ts +368 -187
- package/dist/index.js +278 -417
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -36,8 +36,8 @@ function extractSectionCompat(content, newHeading, legacyHeading) {
|
|
|
36
36
|
const section = extractSection(content, newHeading);
|
|
37
37
|
return section || extractSection(content, legacyHeading);
|
|
38
38
|
}
|
|
39
|
-
function
|
|
40
|
-
const section = extractSectionCompat(content, "Sprint Health", "Session Health");
|
|
39
|
+
function parseCycleHealth(content) {
|
|
40
|
+
const section = extractSectionCompat(content, "Cycle Health", "Sprint Health") || extractSection(content, "Session Health");
|
|
41
41
|
const rows = /* @__PURE__ */ new Map();
|
|
42
42
|
for (const line of section.split("\n")) {
|
|
43
43
|
const match = line.match(/^\|\s*(.+?)\s*\|\s*(.+?)\s*\|$/);
|
|
@@ -48,27 +48,29 @@ function parseSprintHealth(content) {
|
|
|
48
48
|
}
|
|
49
49
|
const get = (key) => rows.get(key) ?? "";
|
|
50
50
|
return {
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
totalCycles: parseInt(get("total cycles") || get("total sprints") || get("total sessions"), 10) || 0,
|
|
52
|
+
cyclesSinceLastStrategyReview: parseInt(get("cycles since last strategy review") || get("sprints since last strategy review") || get("sessions since last strategy review"), 10) || 0,
|
|
53
53
|
strategyReviewDue: get("strategy review due"),
|
|
54
54
|
boardHealth: get("board health"),
|
|
55
55
|
strategicDirection: get("strategic direction"),
|
|
56
56
|
lastFullMode: parseInt(get("last full mode"), 10) || 0
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
|
-
function
|
|
60
|
-
const section = extractSectionCompat(content, "Sprint Health", "Session Health");
|
|
59
|
+
function serializeCycleHealth(health, content) {
|
|
60
|
+
const section = extractSectionCompat(content, "Cycle Health", "Sprint Health") || extractSection(content, "Session Health");
|
|
61
61
|
const fieldMap = {
|
|
62
|
-
"Total
|
|
63
|
-
"
|
|
62
|
+
"Total cycles": String(health.totalCycles),
|
|
63
|
+
"Cycles since last Strategy Review": String(health.cyclesSinceLastStrategyReview),
|
|
64
64
|
"Strategy Review due": health.strategyReviewDue,
|
|
65
65
|
"Board health": health.boardHealth,
|
|
66
66
|
"Strategic direction": health.strategicDirection,
|
|
67
67
|
"Last Full Mode": String(health.lastFullMode)
|
|
68
68
|
};
|
|
69
69
|
const legacyFieldMap = {
|
|
70
|
-
"Total
|
|
71
|
-
"
|
|
70
|
+
"Total sprints": String(health.totalCycles),
|
|
71
|
+
"Sprints since last Strategy Review": String(health.cyclesSinceLastStrategyReview),
|
|
72
|
+
"Total sessions": String(health.totalCycles),
|
|
73
|
+
"Sessions since last Strategy Review": String(health.cyclesSinceLastStrategyReview)
|
|
72
74
|
};
|
|
73
75
|
let updatedSection = section;
|
|
74
76
|
for (const [metric, value] of Object.entries({ ...fieldMap, ...legacyFieldMap })) {
|
|
@@ -86,8 +88,8 @@ function parseActiveDecisions(content) {
|
|
|
86
88
|
);
|
|
87
89
|
if (!headingMatch) return null;
|
|
88
90
|
const metaMatch = block.match(/<!-- papi:(?:created_sprint=(\d+))?\s*(?:modified_sprint=(\d+)\s*)*(?:uuid=(\S+))? -->/);
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
+
const createdCycle = metaMatch?.[1] ? parseInt(metaMatch[1], 10) : void 0;
|
|
92
|
+
const modifiedCycle = metaMatch?.[2] ? parseInt(metaMatch[2], 10) : void 0;
|
|
91
93
|
const uuid = metaMatch?.[3] ?? randomUUID();
|
|
92
94
|
return {
|
|
93
95
|
uuid,
|
|
@@ -97,8 +99,8 @@ function parseActiveDecisions(content) {
|
|
|
97
99
|
confidence: headingMatch[3] ?? "HIGH",
|
|
98
100
|
superseded: !!headingMatch[4],
|
|
99
101
|
supersededBy: headingMatch[4],
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
createdCycle,
|
|
103
|
+
modifiedCycle,
|
|
102
104
|
body: block
|
|
103
105
|
};
|
|
104
106
|
}).filter((d) => d !== null);
|
|
@@ -106,16 +108,16 @@ function parseActiveDecisions(content) {
|
|
|
106
108
|
function stripTemporalMeta(body) {
|
|
107
109
|
return body.replace(/\n?<!-- papi:(?:created_sprint=\d+)?\s*(?:modified_sprint=\d+\s*)*(?:uuid=\S+)? -->/g, "");
|
|
108
110
|
}
|
|
109
|
-
function buildTemporalMeta(
|
|
111
|
+
function buildTemporalMeta(createdCycle, modifiedCycle, uuid) {
|
|
110
112
|
const parts = [];
|
|
111
|
-
if (
|
|
112
|
-
if (
|
|
113
|
+
if (createdCycle != null) parts.push(`created_sprint=${createdCycle}`);
|
|
114
|
+
if (modifiedCycle != null) parts.push(`modified_sprint=${modifiedCycle}`);
|
|
113
115
|
if (uuid) parts.push(`uuid=${uuid}`);
|
|
114
116
|
if (parts.length === 0) return "";
|
|
115
117
|
return `
|
|
116
118
|
<!-- papi:${parts.join(" ")} -->`;
|
|
117
119
|
}
|
|
118
|
-
function
|
|
120
|
+
function extractCreatedCycle(block) {
|
|
119
121
|
const m = block.match(/<!-- papi:(?:created_sprint=(\d+))/);
|
|
120
122
|
return m?.[1] ? parseInt(m[1], 10) : void 0;
|
|
121
123
|
}
|
|
@@ -123,19 +125,19 @@ function extractUuid(block) {
|
|
|
123
125
|
const m = block.match(/<!-- papi:.*?uuid=(\S+)/);
|
|
124
126
|
return m?.[1];
|
|
125
127
|
}
|
|
126
|
-
function updateActiveDecisionInContent(id, newBody, content,
|
|
128
|
+
function updateActiveDecisionInContent(id, newBody, content, cycleNumber) {
|
|
127
129
|
if (!newBody) return content;
|
|
128
130
|
const escapedId = id.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
129
131
|
const pattern = new RegExp(`(### ${escapedId}:.*?)(?=^### AD-\\d+:|^## |$(?![\\s\\S]))`, "ms");
|
|
130
132
|
const cleanBody = stripTemporalMeta(newBody);
|
|
131
133
|
if (pattern.test(content)) {
|
|
132
134
|
const existingMatch = content.match(pattern);
|
|
133
|
-
const existingCreated = existingMatch ?
|
|
135
|
+
const existingCreated = existingMatch ? extractCreatedCycle(existingMatch[0]) : void 0;
|
|
134
136
|
const existingUuid = existingMatch ? extractUuid(existingMatch[0]) : void 0;
|
|
135
|
-
const meta2 =
|
|
137
|
+
const meta2 = cycleNumber != null ? buildTemporalMeta(existingCreated, cycleNumber, existingUuid) : existingUuid ? buildTemporalMeta(existingCreated, void 0, existingUuid) : "";
|
|
136
138
|
return content.replace(pattern, cleanBody.trimEnd() + meta2 + "\n\n");
|
|
137
139
|
}
|
|
138
|
-
const meta =
|
|
140
|
+
const meta = cycleNumber != null ? buildTemporalMeta(cycleNumber) : "";
|
|
139
141
|
const sectionPattern = /^(#{1,2} Active Decisions\n)([\s\S]*?)(?=^#{1,2} |$(?![\s\S]))/m;
|
|
140
142
|
const sectionMatch = content.match(sectionPattern);
|
|
141
143
|
if (sectionMatch) {
|
|
@@ -146,22 +148,22 @@ function updateActiveDecisionInContent(id, newBody, content, sprintNumber) {
|
|
|
146
148
|
}
|
|
147
149
|
return content;
|
|
148
150
|
}
|
|
149
|
-
function
|
|
150
|
-
const section = extractSectionCompat(content, "
|
|
151
|
-
const chunks = section.split(/^(?=### (?:Sprint|Session) \d+ —)/m).map((c) => c.trim()).filter((c) => c.match(/^### (?:Sprint|Session) \d+ —/));
|
|
151
|
+
function parseCycleLog(content, limit) {
|
|
152
|
+
const section = extractSectionCompat(content, "Cycle Log", "Sprint Log");
|
|
153
|
+
const chunks = section.split(/^(?=### (?:Cycle|Sprint|Session) \d+ —)/m).map((c) => c.trim()).filter((c) => c.match(/^### (?:Cycle|Sprint|Session) \d+ —/));
|
|
152
154
|
const entries = chunks.map((block) => {
|
|
153
|
-
const headingMatch = block.match(/^### (?:Sprint|Session) (\d+) — (.+?)$/m);
|
|
154
|
-
const
|
|
155
|
+
const headingMatch = block.match(/^### (?:Cycle|Sprint|Session) (\d+) — (.+?)$/m);
|
|
156
|
+
const cycleNumber = headingMatch ? parseInt(headingMatch[1], 10) : 0;
|
|
155
157
|
const title = headingMatch ? headingMatch[2].trim() : block.split("\n")[0].replace(/^### /, "");
|
|
156
158
|
const carryForwardMatch = block.match(/^- \*\*CARRY FORWARD:\*\*\s*(.+)$/m);
|
|
157
159
|
const uuidMatch = block.match(/<!-- papi:.*?uuid=(\S+)/);
|
|
158
160
|
const uuid = uuidMatch?.[1];
|
|
159
161
|
const blockClean = block.replace(/\n?<!-- papi:.*?-->/g, "");
|
|
160
|
-
const notesMatch = blockClean.match(/\*\*
|
|
162
|
+
const notesMatch = blockClean.match(/\*\*Cycle Notes:\*\*\s*([\s\S]*?)$/);
|
|
161
163
|
const notes = notesMatch ? notesMatch[1].trim() : void 0;
|
|
162
164
|
return {
|
|
163
165
|
uuid: uuid ?? randomUUID(),
|
|
164
|
-
|
|
166
|
+
cycleNumber,
|
|
165
167
|
title,
|
|
166
168
|
content: block,
|
|
167
169
|
carryForward: carryForwardMatch ? carryForwardMatch[1].trim() : void 0,
|
|
@@ -170,11 +172,11 @@ function parseSprintLog(content, limit) {
|
|
|
170
172
|
});
|
|
171
173
|
return limit ? entries.slice(0, limit) : entries;
|
|
172
174
|
}
|
|
173
|
-
function
|
|
174
|
-
const headingPattern = /^## (?:Sprint|Session) Log\s*$/m;
|
|
175
|
+
function prependCycleLogEntry(entry, content) {
|
|
176
|
+
const headingPattern = /^## (?:Cycle|Sprint|Session) Log\s*$/m;
|
|
175
177
|
const headingMatch = content.match(headingPattern);
|
|
176
178
|
if (!headingMatch || headingMatch.index === void 0) {
|
|
177
|
-
throw new Error("
|
|
179
|
+
throw new Error("Cycle Log section not found in Planning Log");
|
|
178
180
|
}
|
|
179
181
|
const insertPos = headingMatch.index + headingMatch[0].length;
|
|
180
182
|
const before = content.slice(0, insertPos);
|
|
@@ -183,7 +185,7 @@ function prependSprintLogEntry(entry, content) {
|
|
|
183
185
|
if (entry.notes) {
|
|
184
186
|
entryContent = `${entryContent}
|
|
185
187
|
|
|
186
|
-
**
|
|
188
|
+
**Cycle Notes:** ${entry.notes}`;
|
|
187
189
|
}
|
|
188
190
|
if (entry.uuid) {
|
|
189
191
|
entryContent = `${entryContent}
|
|
@@ -201,13 +203,13 @@ function parseDeferred(content) {
|
|
|
201
203
|
const section = extractSection(content, "Deferred / Parking Lot");
|
|
202
204
|
return section.split("\n").filter((line) => line.match(/^-\s+/)).map((line) => line.replace(/^-\s+/, "").trim());
|
|
203
205
|
}
|
|
204
|
-
function
|
|
205
|
-
const section = extractSectionCompat(content, "
|
|
206
|
-
const chunks = section.split(/^(?=### (?:Sprint|Session) \d+ —)/m).map((c) => c.trim()).filter((c) => c.match(/^### (?:Sprint|Session) \d+ —/));
|
|
206
|
+
function compressCycleLogInContent(content, threshold, summary) {
|
|
207
|
+
const section = extractSectionCompat(content, "Cycle Log", "Sprint Log");
|
|
208
|
+
const chunks = section.split(/^(?=### (?:Cycle|Sprint|Session) \d+ —)/m).map((c) => c.trim()).filter((c) => c.match(/^### (?:Cycle|Sprint|Session) \d+ —/));
|
|
207
209
|
const keep = [];
|
|
208
210
|
let hasOld = false;
|
|
209
211
|
for (const block of chunks) {
|
|
210
|
-
const match = block.match(/^### (?:Sprint|Session) (\d+) —/);
|
|
212
|
+
const match = block.match(/^### (?:Cycle|Sprint|Session) (\d+) —/);
|
|
211
213
|
const num = match ? parseInt(match[1], 10) : 0;
|
|
212
214
|
if (num >= threshold) {
|
|
213
215
|
keep.push(block);
|
|
@@ -216,27 +218,27 @@ function compressSprintLogInContent(content, threshold, summary) {
|
|
|
216
218
|
}
|
|
217
219
|
}
|
|
218
220
|
if (!hasOld) return content;
|
|
219
|
-
const summaryBlock = `###
|
|
221
|
+
const summaryBlock = `### Cycles 1\u2013${threshold - 1} \u2014 Compressed Summary
|
|
220
222
|
|
|
221
223
|
${summary}`;
|
|
222
224
|
const newEntries = [...keep, summaryBlock].join("\n\n");
|
|
223
|
-
const newSection = `##
|
|
225
|
+
const newSection = `## Cycle Log
|
|
224
226
|
|
|
225
227
|
${newEntries}
|
|
226
228
|
`;
|
|
227
229
|
return content.replace(section, newSection);
|
|
228
230
|
}
|
|
229
|
-
function parsePlanningLog(content, activeDecisionsContent,
|
|
231
|
+
function parsePlanningLog(content, activeDecisionsContent, cycleLogContent) {
|
|
230
232
|
return {
|
|
231
|
-
|
|
233
|
+
cycleHealth: parseCycleHealth(content),
|
|
232
234
|
northStar: parseNorthStar(content),
|
|
233
235
|
activeDecisions: parseActiveDecisions(activeDecisionsContent ?? content),
|
|
234
236
|
deferred: parseDeferred(content),
|
|
235
|
-
|
|
237
|
+
cycleLog: cycleLogContent ? parseCycleLog(cycleLogContent) : []
|
|
236
238
|
};
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
// src/parsers/
|
|
241
|
+
// src/parsers/cycle-board.ts
|
|
240
242
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
241
243
|
import yaml from "js-yaml";
|
|
242
244
|
|
|
@@ -285,11 +287,11 @@ function parseBuildHandoff(markdown) {
|
|
|
285
287
|
if (!markdown.includes("BUILD HANDOFF")) return null;
|
|
286
288
|
const taskIdMatch = markdown.match(/BUILD HANDOFF\s*—\s*(task-\d+)/);
|
|
287
289
|
const taskTitleMatch = markdown.match(/^Task:\s*(.+)$/m);
|
|
288
|
-
const
|
|
290
|
+
const cycleMatch = markdown.match(/^Cycle:\s*(\d+)$/m);
|
|
289
291
|
const whyNowMatch = markdown.match(/^Why now:\s*([\s\S]*?)(?=\n\n|\nSCOPE)/m);
|
|
290
292
|
const taskId = taskIdMatch?.[1] ?? "";
|
|
291
293
|
const taskTitle = taskTitleMatch?.[1]?.trim() ?? "";
|
|
292
|
-
const
|
|
294
|
+
const cycle = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
|
|
293
295
|
const whyNow = whyNowMatch?.[1]?.replace(/\s+/g, " ").trim() ?? "";
|
|
294
296
|
const uuidMatch = markdown.match(/^UUID:\s*(\S+)$/m);
|
|
295
297
|
const uuid = uuidMatch?.[1];
|
|
@@ -306,7 +308,7 @@ function parseBuildHandoff(markdown) {
|
|
|
306
308
|
...createdAt ? { createdAt } : {},
|
|
307
309
|
taskId,
|
|
308
310
|
taskTitle,
|
|
309
|
-
|
|
311
|
+
cycle,
|
|
310
312
|
whyNow,
|
|
311
313
|
scope: parseBulletList(sections.get("SCOPE (DO THIS)") ?? ""),
|
|
312
314
|
scopeBoundary: parseBulletList(sections.get("SCOPE BOUNDARY (DO NOT DO THIS)") ?? ""),
|
|
@@ -323,7 +325,7 @@ function serializeBuildHandoff(handoff) {
|
|
|
323
325
|
if (handoff.displayId) lines.push(`Display ID: ${handoff.displayId}`);
|
|
324
326
|
if (handoff.createdAt) lines.push(`Created: ${handoff.createdAt}`);
|
|
325
327
|
lines.push(`Task: ${handoff.taskTitle}`);
|
|
326
|
-
lines.push(`
|
|
328
|
+
lines.push(`Cycle: ${handoff.cycle}`);
|
|
327
329
|
lines.push(`Why now: ${handoff.whyNow}`);
|
|
328
330
|
lines.push("");
|
|
329
331
|
lines.push("SCOPE (DO THIS)");
|
|
@@ -354,11 +356,11 @@ function serializeBuildHandoff(handoff) {
|
|
|
354
356
|
return lines.join("\n");
|
|
355
357
|
}
|
|
356
358
|
|
|
357
|
-
// src/parsers/
|
|
359
|
+
// src/parsers/cycle-board.ts
|
|
358
360
|
var YAML_MARKER = "<!-- PAPI-ADAPTER: parse the yaml block below -->";
|
|
359
361
|
var YAML_START = "<!-- PAPI-YAML-START -->";
|
|
360
362
|
var YAML_END = "<!-- PAPI-YAML-END -->";
|
|
361
|
-
function
|
|
363
|
+
function toCycleTask(raw) {
|
|
362
364
|
return {
|
|
363
365
|
uuid: raw.uuid || randomUUID3(),
|
|
364
366
|
id: raw.id,
|
|
@@ -372,8 +374,8 @@ function toSprintTask(raw) {
|
|
|
372
374
|
phase: raw.phase,
|
|
373
375
|
owner: raw.owner,
|
|
374
376
|
reviewed: raw.reviewed ?? false,
|
|
375
|
-
|
|
376
|
-
|
|
377
|
+
cycle: raw.cycle != null ? raw.cycle : void 0,
|
|
378
|
+
createdCycle: raw.created_sprint != null ? raw.created_sprint : void 0,
|
|
377
379
|
createdAt: raw.created_at || void 0,
|
|
378
380
|
why: raw.why || void 0,
|
|
379
381
|
dependsOn: raw.depends_on || void 0,
|
|
@@ -387,7 +389,7 @@ function toSprintTask(raw) {
|
|
|
387
389
|
function sanitizeDelimiters(value) {
|
|
388
390
|
return value.replaceAll(YAML_END, "<!-- PAPI-YAML-END (sanitized) -->");
|
|
389
391
|
}
|
|
390
|
-
function
|
|
392
|
+
function fromCycleTask(task) {
|
|
391
393
|
const raw = {
|
|
392
394
|
uuid: task.uuid,
|
|
393
395
|
id: task.id,
|
|
@@ -403,8 +405,8 @@ function fromSprintTask(task) {
|
|
|
403
405
|
depends_on: task.dependsOn ?? "",
|
|
404
406
|
notes: task.notes ? sanitizeDelimiters(task.notes) : ""
|
|
405
407
|
};
|
|
406
|
-
if (task.
|
|
407
|
-
if (task.
|
|
408
|
+
if (task.cycle != null) raw.cycle = task.cycle;
|
|
409
|
+
if (task.createdCycle != null) raw.created_sprint = task.createdCycle;
|
|
408
410
|
if (task.createdAt) raw.created_at = task.createdAt;
|
|
409
411
|
if (task.why) raw.why = task.why;
|
|
410
412
|
if (task.stateHistory?.length) {
|
|
@@ -423,17 +425,17 @@ function mergeConflictHint(content) {
|
|
|
423
425
|
}
|
|
424
426
|
function extractYamlBlock(content) {
|
|
425
427
|
const markerIdx = content.indexOf(YAML_MARKER);
|
|
426
|
-
if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in
|
|
428
|
+
if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in CYCLE_BOARD.md");
|
|
427
429
|
const afterMarker = content.slice(markerIdx + YAML_MARKER.length);
|
|
428
430
|
const startIdx = afterMarker.indexOf(YAML_START);
|
|
429
431
|
if (startIdx !== -1) {
|
|
430
432
|
const yamlStart = startIdx + YAML_START.length;
|
|
431
433
|
const endIdx = afterMarker.indexOf(YAML_END, yamlStart);
|
|
432
|
-
if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in
|
|
434
|
+
if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in CYCLE_BOARD.md");
|
|
433
435
|
return afterMarker.slice(yamlStart, endIdx);
|
|
434
436
|
}
|
|
435
437
|
const blockMatch = afterMarker.match(/```yaml\n([\s\S]*?)```/);
|
|
436
|
-
if (!blockMatch) throw new Error("YAML block not found in
|
|
438
|
+
if (!blockMatch) throw new Error("YAML block not found in CYCLE_BOARD.md");
|
|
437
439
|
return blockMatch[1];
|
|
438
440
|
}
|
|
439
441
|
function parseBoard(content) {
|
|
@@ -446,27 +448,27 @@ function parseBoard(content) {
|
|
|
446
448
|
const lineInfo = yamlErr.mark?.line != null ? ` (near line ${yamlErr.mark.line + 1} of YAML block)` : "";
|
|
447
449
|
const hint = mergeConflictHint(yamlText);
|
|
448
450
|
throw new Error(
|
|
449
|
-
`YAML parse error in
|
|
451
|
+
`YAML parse error in CYCLE_BOARD.md${lineInfo}. Check for syntax errors \u2014 unquoted special characters, bad indentation, or missing colons.${hint}`
|
|
450
452
|
);
|
|
451
453
|
}
|
|
452
|
-
return (data.tasks ?? []).map(
|
|
454
|
+
return (data.tasks ?? []).map(toCycleTask);
|
|
453
455
|
}
|
|
454
456
|
function serializeBoard(tasks, content) {
|
|
455
|
-
const raw = tasks.map(
|
|
457
|
+
const raw = tasks.map(fromCycleTask);
|
|
456
458
|
const yamlStr = yaml.dump({ tasks: raw }, { lineWidth: 120, quotingType: '"' });
|
|
457
459
|
const markerIdx = content.indexOf(YAML_MARKER);
|
|
458
|
-
if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in
|
|
460
|
+
if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in CYCLE_BOARD.md");
|
|
459
461
|
const afterMarker = content.slice(markerIdx + YAML_MARKER.length);
|
|
460
462
|
const htmlStartIdx = afterMarker.indexOf(YAML_START);
|
|
461
463
|
if (htmlStartIdx !== -1) {
|
|
462
464
|
const absStart = markerIdx + YAML_MARKER.length + htmlStartIdx;
|
|
463
465
|
const endIdx = afterMarker.indexOf(YAML_END, htmlStartIdx);
|
|
464
|
-
if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in
|
|
466
|
+
if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in CYCLE_BOARD.md");
|
|
465
467
|
const absEnd = markerIdx + YAML_MARKER.length + endIdx + YAML_END.length;
|
|
466
468
|
return content.slice(0, absStart) + YAML_START + "\n" + yamlStr + YAML_END + content.slice(absEnd);
|
|
467
469
|
}
|
|
468
470
|
const blockMatch = afterMarker.match(/```yaml\n[\s\S]*?```/);
|
|
469
|
-
if (!blockMatch) throw new Error("YAML block not found in
|
|
471
|
+
if (!blockMatch) throw new Error("YAML block not found in CYCLE_BOARD.md");
|
|
470
472
|
const blockStart = markerIdx + YAML_MARKER.length + afterMarker.indexOf(blockMatch[0]);
|
|
471
473
|
const blockEnd = blockStart + blockMatch[0].length;
|
|
472
474
|
return content.slice(0, blockStart) + YAML_START + "\n" + yamlStr + YAML_END + content.slice(blockEnd);
|
|
@@ -508,9 +510,9 @@ function parseEffort(effortLine) {
|
|
|
508
510
|
return match ? { actual: match[1], estimated: match[2] } : { actual: effortLine, estimated: "" };
|
|
509
511
|
}
|
|
510
512
|
function parseBuildReports(content) {
|
|
511
|
-
const chunks = content.split(/^(?=### .+ — .+ — (?:Sprint|Session) \d+)/m).map((c) => c.trim()).filter((c) => c.match(/^### .+ — .+ — (?:Sprint|Session) \d+/));
|
|
513
|
+
const chunks = content.split(/^(?=### .+ — .+ — (?:Cycle|Sprint|Session) \d+)/m).map((c) => c.trim()).filter((c) => c.match(/^### .+ — .+ — (?:Cycle|Sprint|Session) \d+/));
|
|
512
514
|
return chunks.map((block) => {
|
|
513
|
-
const headingMatch = block.match(/^### (.+?) — (.+?) — (?:Sprint|Session) (\d+)/);
|
|
515
|
+
const headingMatch = block.match(/^### (.+?) — (.+?) — (?:Cycle|Sprint|Session) (\d+)/);
|
|
514
516
|
if (!headingMatch) return null;
|
|
515
517
|
const effortLine = parseField(block, "Actual Effort");
|
|
516
518
|
const { actual, estimated } = parseEffort(effortLine);
|
|
@@ -533,7 +535,7 @@ function parseBuildReports(content) {
|
|
|
533
535
|
taskId,
|
|
534
536
|
taskName: headingMatch[1].trim(),
|
|
535
537
|
date: headingMatch[2].trim(),
|
|
536
|
-
|
|
538
|
+
cycle: parseInt(headingMatch[3], 10),
|
|
537
539
|
completed: completedRaw.startsWith("Yes") ? "Yes" : completedRaw.startsWith("No") ? "No" : "Partial",
|
|
538
540
|
actualEffort,
|
|
539
541
|
estimatedEffort,
|
|
@@ -549,7 +551,7 @@ function parseBuildReports(content) {
|
|
|
549
551
|
}
|
|
550
552
|
function serializeBuildReport(report) {
|
|
551
553
|
const lines = [
|
|
552
|
-
`### ${report.taskName} \u2014 ${report.date} \u2014
|
|
554
|
+
`### ${report.taskName} \u2014 ${report.date} \u2014 Cycle ${report.cycle}`
|
|
553
555
|
];
|
|
554
556
|
if (report.uuid) lines.push(`- **UUID:** ${report.uuid}`);
|
|
555
557
|
if (report.displayId) lines.push(`- **Display ID:** ${report.displayId}`);
|
|
@@ -569,14 +571,14 @@ function serializeBuildReport(report) {
|
|
|
569
571
|
}
|
|
570
572
|
return lines.join("\n");
|
|
571
573
|
}
|
|
572
|
-
function formatCompressedSummary(reports,
|
|
574
|
+
function formatCompressedSummary(reports, cycleRange, aiSummary) {
|
|
573
575
|
const dates = reports.map((r) => r.date).filter(Boolean);
|
|
574
576
|
const dateRange = dates.length > 0 ? `${dates[dates.length - 1]} \u2013 ${dates[0]}` : "unknown";
|
|
575
577
|
const completed = reports.filter((r) => r.completed === "Yes");
|
|
576
578
|
const partial = reports.filter((r) => r.completed === "Partial");
|
|
577
579
|
const failed = reports.filter((r) => r.completed === "No");
|
|
578
580
|
const formatTaskList = (list) => list.map((r) => r.taskId !== "unknown" ? `${r.taskId} (${r.taskName})` : r.taskName).join(", ");
|
|
579
|
-
const lines = [`### ${
|
|
581
|
+
const lines = [`### ${cycleRange} \u2014 Compressed Summary`];
|
|
580
582
|
lines.push(`**Date range:** ${dateRange}`);
|
|
581
583
|
lines.push(`**Reports:** ${reports.length}`);
|
|
582
584
|
if (completed.length > 0) {
|
|
@@ -602,11 +604,11 @@ function formatCompressedSummary(reports, sprintRange, aiSummary) {
|
|
|
602
604
|
return lines.join("\n");
|
|
603
605
|
}
|
|
604
606
|
function compressBuildReportsInContent(content, threshold, summary) {
|
|
605
|
-
const chunks = content.split(/^(?=### .+ — .+ — (?:Sprint|Session) \d+)/m).map((c) => c.trim()).filter((c) => c.match(/^### .+ — .+ — (?:Sprint|Session) \d+/));
|
|
607
|
+
const chunks = content.split(/^(?=### .+ — .+ — (?:Cycle|Sprint|Session) \d+)/m).map((c) => c.trim()).filter((c) => c.match(/^### .+ — .+ — (?:Cycle|Sprint|Session) \d+/));
|
|
606
608
|
const keep = [];
|
|
607
609
|
const oldChunks = [];
|
|
608
610
|
for (const block of chunks) {
|
|
609
|
-
const match = block.match(/— (?:Sprint|Session) (\d+)/);
|
|
611
|
+
const match = block.match(/— (?:Cycle|Sprint|Session) (\d+)/);
|
|
610
612
|
const num = match ? parseInt(match[1], 10) : 0;
|
|
611
613
|
if (num >= threshold) {
|
|
612
614
|
keep.push(block);
|
|
@@ -616,8 +618,8 @@ function compressBuildReportsInContent(content, threshold, summary) {
|
|
|
616
618
|
}
|
|
617
619
|
if (oldChunks.length === 0) return content;
|
|
618
620
|
const oldReports = parseBuildReports(oldChunks.join("\n\n---\n\n"));
|
|
619
|
-
const
|
|
620
|
-
const summaryBlock = formatCompressedSummary(oldReports,
|
|
621
|
+
const cycleRange = `Cycles 1\u2013${threshold - 1}`;
|
|
622
|
+
const summaryBlock = formatCompressedSummary(oldReports, cycleRange, summary);
|
|
621
623
|
const firstReportIdx = content.search(/^### /m);
|
|
622
624
|
const header = firstReportIdx === -1 ? content : content.slice(0, firstReportIdx);
|
|
623
625
|
const entries = [...keep, summaryBlock].join("\n\n---\n\n");
|
|
@@ -638,7 +640,7 @@ function mergeBuildReports(existing, incoming) {
|
|
|
638
640
|
taskId: incoming.taskId,
|
|
639
641
|
taskName: incoming.taskName,
|
|
640
642
|
date: incoming.date,
|
|
641
|
-
|
|
643
|
+
cycle: incoming.cycle,
|
|
642
644
|
completed: incoming.completed,
|
|
643
645
|
actualEffort: incoming.actualEffort,
|
|
644
646
|
estimatedEffort: incoming.estimatedEffort,
|
|
@@ -659,8 +661,12 @@ function replaceBuildReport(existing, replacement, content) {
|
|
|
659
661
|
if (idx !== -1) {
|
|
660
662
|
return content.slice(0, idx) + newSerialized + content.slice(idx + oldSerialized.length);
|
|
661
663
|
}
|
|
662
|
-
const headingPattern = `### ${existing.taskName} \u2014 ${existing.date} \u2014
|
|
663
|
-
|
|
664
|
+
const headingPattern = `### ${existing.taskName} \u2014 ${existing.date} \u2014 Cycle ${existing.cycle}`;
|
|
665
|
+
let headingIdx = content.indexOf(headingPattern);
|
|
666
|
+
if (headingIdx === -1) {
|
|
667
|
+
const legacyPattern = `### ${existing.taskName} \u2014 ${existing.date} \u2014 Sprint ${existing.cycle}`;
|
|
668
|
+
headingIdx = content.indexOf(legacyPattern);
|
|
669
|
+
}
|
|
664
670
|
if (headingIdx === -1) throw new Error(`Could not find existing build report for ${existing.taskId}`);
|
|
665
671
|
const afterHeading = content.slice(headingIdx);
|
|
666
672
|
const nextSeparator = afterHeading.indexOf("\n\n---\n");
|
|
@@ -687,9 +693,9 @@ function prependBuildReport(report, content) {
|
|
|
687
693
|
}
|
|
688
694
|
|
|
689
695
|
// src/parsers/metrics.ts
|
|
690
|
-
var TABLE_HEADER = "| Timestamp | Tool | Duration (ms) | Input Tokens | Output Tokens | Cost ($) | Model |
|
|
696
|
+
var TABLE_HEADER = "| Timestamp | Tool | Duration (ms) | Input Tokens | Output Tokens | Cost ($) | Model | Cycle | Context |";
|
|
691
697
|
var TABLE_SEPARATOR = "|-----------|------|---------------|--------------|---------------|----------|-------|--------|---------|";
|
|
692
|
-
var PREV_TABLE_HEADER = "| Timestamp | Tool | Duration (ms) | Input Tokens | Output Tokens | Cost ($) | Model |
|
|
698
|
+
var PREV_TABLE_HEADER = "| Timestamp | Tool | Duration (ms) | Input Tokens | Output Tokens | Cost ($) | Model | Cycle |";
|
|
693
699
|
var LEGACY_TABLE_HEADER = "| Timestamp | Tool | Duration (ms) | Input Tokens | Output Tokens | Cost ($) | Model |";
|
|
694
700
|
var SECTION_HEADING = "## Tool Call Metrics";
|
|
695
701
|
var FILE_TEMPLATE = `# PAPI Metrics
|
|
@@ -728,8 +734,8 @@ function parseToolMetrics(content) {
|
|
|
728
734
|
const outputTokens = cells[4] !== "-" ? parseInt(cells[4].replace(/,/g, ""), 10) : void 0;
|
|
729
735
|
const cost = cells[5] !== "-" ? parseFloat(cells[5]) : void 0;
|
|
730
736
|
const model = cells[6] !== "-" ? cells[6] : void 0;
|
|
731
|
-
const
|
|
732
|
-
const
|
|
737
|
+
const cycleRaw = cells.length >= 8 && cells[7] !== "-" ? parseInt(cells[7], 10) : void 0;
|
|
738
|
+
const cycleNumber = cycleRaw !== void 0 && !isNaN(cycleRaw) ? cycleRaw : void 0;
|
|
733
739
|
const contextRaw = cells.length >= 9 && cells[8] !== "-" ? parseInt(cells[8].replace(/,/g, ""), 10) : void 0;
|
|
734
740
|
const contextBytes = contextRaw !== void 0 && !isNaN(contextRaw) ? contextRaw : void 0;
|
|
735
741
|
const utilisationRaw = cells.length >= 10 && cells[9] !== "-" ? parseFloat(cells[9]) : void 0;
|
|
@@ -742,7 +748,7 @@ function parseToolMetrics(content) {
|
|
|
742
748
|
...outputTokens !== void 0 && !isNaN(outputTokens) ? { outputTokens } : {},
|
|
743
749
|
...cost !== void 0 && !isNaN(cost) ? { estimatedCostUsd: cost } : {},
|
|
744
750
|
...model ? { model } : {},
|
|
745
|
-
...
|
|
751
|
+
...cycleNumber !== void 0 ? { cycleNumber } : {},
|
|
746
752
|
...contextBytes !== void 0 ? { contextBytes } : {},
|
|
747
753
|
...contextUtilisation !== void 0 ? { contextUtilisation } : {}
|
|
748
754
|
});
|
|
@@ -757,10 +763,10 @@ function serializeToolMetric(metric) {
|
|
|
757
763
|
const outputTokens = metric.outputTokens !== void 0 ? formatNumber(metric.outputTokens) : "-";
|
|
758
764
|
const cost = metric.estimatedCostUsd !== void 0 ? metric.estimatedCostUsd.toFixed(4) : "-";
|
|
759
765
|
const model = metric.model ?? "-";
|
|
760
|
-
const
|
|
766
|
+
const cycle = metric.cycleNumber !== void 0 ? String(metric.cycleNumber) : "-";
|
|
761
767
|
const context = metric.contextBytes !== void 0 ? formatNumber(metric.contextBytes) : "-";
|
|
762
768
|
const utilisation = metric.contextUtilisation !== void 0 ? metric.contextUtilisation.toFixed(2) : "-";
|
|
763
|
-
return `| ${metric.timestamp} | ${metric.tool} | ${formatNumber(metric.durationMs)} | ${inputTokens} | ${outputTokens} | ${cost} | ${model} | ${
|
|
769
|
+
return `| ${metric.timestamp} | ${metric.tool} | ${formatNumber(metric.durationMs)} | ${inputTokens} | ${outputTokens} | ${cost} | ${model} | ${cycle} | ${context} | ${utilisation} |`;
|
|
764
770
|
}
|
|
765
771
|
function appendToolMetricToContent(metric, content) {
|
|
766
772
|
if (!content.trim()) {
|
|
@@ -791,8 +797,8 @@ function appendToolMetricToContent(metric, content) {
|
|
|
791
797
|
const after = content.slice(costIdx);
|
|
792
798
|
return before + "\n" + serializeToolMetric(metric) + "\n\n" + after;
|
|
793
799
|
}
|
|
794
|
-
function aggregateCostSummary(metrics,
|
|
795
|
-
const filtered =
|
|
800
|
+
function aggregateCostSummary(metrics, cycleNumber) {
|
|
801
|
+
const filtered = cycleNumber !== void 0 ? metrics.filter((m) => m.cycleNumber === cycleNumber) : metrics;
|
|
796
802
|
let totalCostUsd = 0;
|
|
797
803
|
let totalInputTokens = 0;
|
|
798
804
|
let totalOutputTokens = 0;
|
|
@@ -824,7 +830,7 @@ function aggregateCostSummary(metrics, sprintNumber) {
|
|
|
824
830
|
};
|
|
825
831
|
}
|
|
826
832
|
var COST_SECTION_HEADING = "## Cost Summary";
|
|
827
|
-
var COST_TABLE_HEADER = "|
|
|
833
|
+
var COST_TABLE_HEADER = "| Cycle | Date | Total Cost ($) | Input Tokens | Output Tokens | Calls |";
|
|
828
834
|
var COST_TABLE_SEPARATOR = "|--------|------|----------------|--------------|---------------|-------|";
|
|
829
835
|
function parseCostSnapshots(content) {
|
|
830
836
|
const lines = content.split("\n");
|
|
@@ -843,7 +849,7 @@ function parseCostSnapshots(content) {
|
|
|
843
849
|
const cells = line.split("|").map((c) => c.trim()).filter(Boolean);
|
|
844
850
|
if (cells.length < 6) continue;
|
|
845
851
|
snapshots.push({
|
|
846
|
-
|
|
852
|
+
cycle: parseInt(cells[0], 10),
|
|
847
853
|
date: cells[1],
|
|
848
854
|
totalCostUsd: parseFloat(cells[2]),
|
|
849
855
|
totalInputTokens: parseInt(cells[3].replace(/,/g, ""), 10),
|
|
@@ -854,17 +860,17 @@ function parseCostSnapshots(content) {
|
|
|
854
860
|
return snapshots;
|
|
855
861
|
}
|
|
856
862
|
function serializeCostSnapshot(snapshot) {
|
|
857
|
-
return `| ${snapshot.
|
|
863
|
+
return `| ${snapshot.cycle} | ${snapshot.date} | ${snapshot.totalCostUsd.toFixed(4)} | ${formatNumber(snapshot.totalInputTokens)} | ${formatNumber(snapshot.totalOutputTokens)} | ${formatNumber(snapshot.totalCalls)} |`;
|
|
858
864
|
}
|
|
859
865
|
function writeCostSnapshotToContent(snapshot, content) {
|
|
860
866
|
if (!content.includes(COST_SECTION_HEADING)) {
|
|
861
867
|
return content.trimEnd() + "\n\n" + COST_SECTION_HEADING + "\n\n" + COST_TABLE_HEADER + "\n" + COST_TABLE_SEPARATOR + "\n" + serializeCostSnapshot(snapshot) + "\n";
|
|
862
868
|
}
|
|
863
869
|
const lines = content.split("\n");
|
|
864
|
-
const
|
|
870
|
+
const cyclePrefix = `| ${snapshot.cycle} |`;
|
|
865
871
|
let replaced = false;
|
|
866
872
|
for (let i = 0; i < lines.length; i++) {
|
|
867
|
-
if (lines[i].startsWith(
|
|
873
|
+
if (lines[i].startsWith(cyclePrefix)) {
|
|
868
874
|
lines[i] = serializeCostSnapshot(snapshot);
|
|
869
875
|
replaced = true;
|
|
870
876
|
break;
|
|
@@ -876,11 +882,11 @@ function writeCostSnapshotToContent(snapshot, content) {
|
|
|
876
882
|
return content.trimEnd() + "\n" + serializeCostSnapshot(snapshot) + "\n";
|
|
877
883
|
}
|
|
878
884
|
|
|
879
|
-
// src/parsers/
|
|
880
|
-
var FILE_HEADING = "#
|
|
881
|
-
var ACCURACY_HEADER = "|
|
|
885
|
+
// src/parsers/cycle-metrics.ts
|
|
886
|
+
var FILE_HEADING = "# Cycle Methodology Metrics";
|
|
887
|
+
var ACCURACY_HEADER = "| Cycle | Reports | Match Rate | MAE | Bias |";
|
|
882
888
|
var ACCURACY_SEPARATOR = "|--------|---------|------------|-----|------|";
|
|
883
|
-
var VELOCITY_HEADER = "|
|
|
889
|
+
var VELOCITY_HEADER = "| Cycle | Completed | Partial | Failed | Effort Points |";
|
|
884
890
|
var VELOCITY_SEPARATOR = "|--------|-----------|---------|--------|---------------|";
|
|
885
891
|
var EFFORT_SCALE = {
|
|
886
892
|
XS: 1,
|
|
@@ -893,21 +899,21 @@ function effortOrdinal(effort) {
|
|
|
893
899
|
const normalized = effort.trim().toUpperCase();
|
|
894
900
|
return EFFORT_SCALE[normalized];
|
|
895
901
|
}
|
|
896
|
-
function
|
|
902
|
+
function calculateCycleMetrics(reports, currentCycle, window = 5) {
|
|
897
903
|
const recentReports = reports.filter(
|
|
898
|
-
(r) => r.
|
|
904
|
+
(r) => r.cycle > currentCycle - window && r.cycle <= currentCycle
|
|
899
905
|
);
|
|
900
|
-
const
|
|
906
|
+
const perCycle = /* @__PURE__ */ new Map();
|
|
901
907
|
for (const r of recentReports) {
|
|
902
|
-
const group =
|
|
908
|
+
const group = perCycle.get(r.cycle) ?? [];
|
|
903
909
|
group.push(r);
|
|
904
|
-
|
|
910
|
+
perCycle.set(r.cycle, group);
|
|
905
911
|
}
|
|
906
912
|
const accuracy = [];
|
|
907
913
|
const velocity = [];
|
|
908
|
-
const
|
|
909
|
-
for (const
|
|
910
|
-
const reps =
|
|
914
|
+
const sortedCycles = [...perCycle.keys()].sort((a, b) => a - b);
|
|
915
|
+
for (const cycle of sortedCycles) {
|
|
916
|
+
const reps = perCycle.get(cycle);
|
|
911
917
|
const deltas = [];
|
|
912
918
|
for (const r of reps) {
|
|
913
919
|
const actual = effortOrdinal(r.actualEffort);
|
|
@@ -918,7 +924,7 @@ function calculateSprintMetrics(reports, currentSprint, window = 5) {
|
|
|
918
924
|
}
|
|
919
925
|
if (deltas.length > 0) {
|
|
920
926
|
accuracy.push({
|
|
921
|
-
|
|
927
|
+
cycle,
|
|
922
928
|
reports: deltas.length,
|
|
923
929
|
matchRate: Math.round(deltas.filter((d) => d === 0).length / deltas.length * 100),
|
|
924
930
|
mae: Math.round(deltas.reduce((s, d) => s + Math.abs(d), 0) / deltas.length * 10) / 10,
|
|
@@ -926,7 +932,7 @@ function calculateSprintMetrics(reports, currentSprint, window = 5) {
|
|
|
926
932
|
});
|
|
927
933
|
}
|
|
928
934
|
velocity.push({
|
|
929
|
-
|
|
935
|
+
cycle,
|
|
930
936
|
completed: reps.filter((r) => r.completed === "Yes").length,
|
|
931
937
|
partial: reps.filter((r) => r.completed === "Partial").length,
|
|
932
938
|
failed: reps.filter((r) => r.completed === "No").length,
|
|
@@ -936,23 +942,23 @@ function calculateSprintMetrics(reports, currentSprint, window = 5) {
|
|
|
936
942
|
return { accuracy, velocity };
|
|
937
943
|
}
|
|
938
944
|
function serializeAccuracyRow(a) {
|
|
939
|
-
return `| ${a.
|
|
945
|
+
return `| ${a.cycle} | ${a.reports} | ${a.matchRate}% | ${a.mae} | ${a.bias >= 0 ? "+" : ""}${a.bias} |`;
|
|
940
946
|
}
|
|
941
947
|
function serializeVelocityRow(v) {
|
|
942
|
-
return `| ${v.
|
|
948
|
+
return `| ${v.cycle} | ${v.completed} | ${v.partial} | ${v.failed} | ${v.effortPoints} |`;
|
|
943
949
|
}
|
|
944
950
|
function serializeSnapshot(snapshot) {
|
|
945
951
|
const lines = [];
|
|
946
|
-
lines.push(`##
|
|
952
|
+
lines.push(`## Cycle ${snapshot.cycle} Snapshot \u2014 ${snapshot.date}`);
|
|
947
953
|
lines.push("");
|
|
948
|
-
lines.push("### Estimation Accuracy (last 5
|
|
954
|
+
lines.push("### Estimation Accuracy (last 5 cycles)");
|
|
949
955
|
lines.push(ACCURACY_HEADER);
|
|
950
956
|
lines.push(ACCURACY_SEPARATOR);
|
|
951
957
|
for (const a of snapshot.accuracy) {
|
|
952
958
|
lines.push(serializeAccuracyRow(a));
|
|
953
959
|
}
|
|
954
960
|
lines.push("");
|
|
955
|
-
lines.push("###
|
|
961
|
+
lines.push("### Cycle Velocity");
|
|
956
962
|
lines.push(VELOCITY_HEADER);
|
|
957
963
|
lines.push(VELOCITY_SEPARATOR);
|
|
958
964
|
for (const v of snapshot.velocity) {
|
|
@@ -965,10 +971,13 @@ function appendSnapshotToContent(snapshot, content) {
|
|
|
965
971
|
if (!content.trim()) {
|
|
966
972
|
return FILE_HEADING + "\n\n" + block + "\n";
|
|
967
973
|
}
|
|
968
|
-
const marker = `##
|
|
969
|
-
const
|
|
974
|
+
const marker = `## Cycle ${snapshot.cycle} Snapshot`;
|
|
975
|
+
const legacyMarker = `## Sprint ${snapshot.cycle} Snapshot`;
|
|
976
|
+
let markerIdx = content.indexOf(marker);
|
|
977
|
+
if (markerIdx === -1) markerIdx = content.indexOf(legacyMarker);
|
|
970
978
|
if (markerIdx !== -1) {
|
|
971
|
-
|
|
979
|
+
let nextSnapshotIdx = content.indexOf("\n## Cycle ", markerIdx + marker.length);
|
|
980
|
+
if (nextSnapshotIdx === -1) nextSnapshotIdx = content.indexOf("\n## Sprint ", markerIdx + marker.length);
|
|
972
981
|
const before = content.slice(0, markerIdx).trimEnd();
|
|
973
982
|
const after = nextSnapshotIdx !== -1 ? content.slice(nextSnapshotIdx) : "";
|
|
974
983
|
return before + "\n\n" + block + (after ? after : "\n");
|
|
@@ -978,12 +987,12 @@ function appendSnapshotToContent(snapshot, content) {
|
|
|
978
987
|
function parseSnapshots(content) {
|
|
979
988
|
if (!content.trim()) return [];
|
|
980
989
|
const snapshots = [];
|
|
981
|
-
const headerRegex = /^## Sprint (\d+) Snapshot — (\S+)/gm;
|
|
990
|
+
const headerRegex = /^## (?:Cycle|Sprint) (\d+) Snapshot — (\S+)/gm;
|
|
982
991
|
let match;
|
|
983
992
|
const headers = [];
|
|
984
993
|
while ((match = headerRegex.exec(content)) !== null) {
|
|
985
994
|
headers.push({
|
|
986
|
-
|
|
995
|
+
cycle: parseInt(match[1], 10),
|
|
987
996
|
date: match[2],
|
|
988
997
|
index: match.index
|
|
989
998
|
});
|
|
@@ -995,7 +1004,7 @@ function parseSnapshots(content) {
|
|
|
995
1004
|
const accuracy = parseAccuracyTable(block);
|
|
996
1005
|
const velocity = parseVelocityTable(block);
|
|
997
1006
|
snapshots.push({
|
|
998
|
-
|
|
1007
|
+
cycle: headers[i].cycle,
|
|
999
1008
|
date: headers[i].date,
|
|
1000
1009
|
accuracy,
|
|
1001
1010
|
velocity
|
|
@@ -1011,22 +1020,22 @@ function isRealFinding(text) {
|
|
|
1011
1020
|
const trimmed = text.trim();
|
|
1012
1021
|
return trimmed.length > 0 && !NONE_PATTERN.test(trimmed);
|
|
1013
1022
|
}
|
|
1014
|
-
async function detectBuildPatterns(reports,
|
|
1023
|
+
async function detectBuildPatterns(reports, currentCycle, window = 5, clusterer) {
|
|
1015
1024
|
const recentReports = reports.filter(
|
|
1016
|
-
(r) => r.
|
|
1025
|
+
(r) => r.cycle > currentCycle - window && r.cycle <= currentCycle
|
|
1017
1026
|
);
|
|
1018
1027
|
let recurringSurprises;
|
|
1019
1028
|
const realSurprises = recentReports.filter((r) => isRealFinding(r.surprises));
|
|
1020
1029
|
if (clusterer && realSurprises.length >= 2) {
|
|
1021
1030
|
const entries = realSurprises.map((r) => ({
|
|
1022
1031
|
text: r.surprises.trim(),
|
|
1023
|
-
source: String(r.
|
|
1032
|
+
source: String(r.cycle)
|
|
1024
1033
|
}));
|
|
1025
1034
|
const clusters = await clusterer(entries);
|
|
1026
1035
|
recurringSurprises = clusters.filter((c) => c.entries.length >= 2).map((c) => ({
|
|
1027
1036
|
text: c.theme,
|
|
1028
1037
|
count: c.entries.length,
|
|
1029
|
-
|
|
1038
|
+
cycles: [...new Set(c.entries.map((e) => parseInt(e.source, 10)))].sort((a, b) => a - b)
|
|
1030
1039
|
}));
|
|
1031
1040
|
recurringSurprises.sort((a, b) => b.count - a.count);
|
|
1032
1041
|
} else {
|
|
@@ -1036,9 +1045,9 @@ async function detectBuildPatterns(reports, currentSprint, window = 5, clusterer
|
|
|
1036
1045
|
const existing = surpriseMap.get(key);
|
|
1037
1046
|
if (existing) {
|
|
1038
1047
|
existing.count += 1;
|
|
1039
|
-
existing.
|
|
1048
|
+
existing.cycles.add(r.cycle);
|
|
1040
1049
|
} else {
|
|
1041
|
-
surpriseMap.set(key, { original: r.surprises.trim(), count: 1,
|
|
1050
|
+
surpriseMap.set(key, { original: r.surprises.trim(), count: 1, cycles: /* @__PURE__ */ new Set([r.cycle]) });
|
|
1042
1051
|
}
|
|
1043
1052
|
}
|
|
1044
1053
|
recurringSurprises = [];
|
|
@@ -1047,7 +1056,7 @@ async function detectBuildPatterns(reports, currentSprint, window = 5, clusterer
|
|
|
1047
1056
|
recurringSurprises.push({
|
|
1048
1057
|
text: entry.original,
|
|
1049
1058
|
count: entry.count,
|
|
1050
|
-
|
|
1059
|
+
cycles: [...entry.cycles].sort((a, b) => a - b)
|
|
1051
1060
|
});
|
|
1052
1061
|
}
|
|
1053
1062
|
}
|
|
@@ -1117,7 +1126,7 @@ function parseAccuracyTable(block) {
|
|
|
1117
1126
|
const cells = line.split("|").map((c) => c.trim()).filter(Boolean);
|
|
1118
1127
|
if (cells.length < 5) continue;
|
|
1119
1128
|
rows.push({
|
|
1120
|
-
|
|
1129
|
+
cycle: parseInt(cells[0], 10),
|
|
1121
1130
|
reports: parseInt(cells[1], 10),
|
|
1122
1131
|
matchRate: parseInt(cells[2].replace("%", ""), 10),
|
|
1123
1132
|
mae: parseFloat(cells[3]),
|
|
@@ -1143,7 +1152,7 @@ function parseVelocityTable(block) {
|
|
|
1143
1152
|
const cells = line.split("|").map((c) => c.trim()).filter(Boolean);
|
|
1144
1153
|
if (cells.length < 5) continue;
|
|
1145
1154
|
rows.push({
|
|
1146
|
-
|
|
1155
|
+
cycle: parseInt(cells[0], 10),
|
|
1147
1156
|
completed: parseInt(cells[1], 10),
|
|
1148
1157
|
partial: parseInt(cells[2], 10),
|
|
1149
1158
|
failed: parseInt(cells[3], 10),
|
|
@@ -1178,12 +1187,12 @@ function parseReviews(content) {
|
|
|
1178
1187
|
const verdictRaw = parseField2(block, "Verdict");
|
|
1179
1188
|
if (!VALID_VERDICTS.has(verdictRaw)) return null;
|
|
1180
1189
|
const verdict = verdictRaw;
|
|
1181
|
-
const
|
|
1182
|
-
if (isNaN(
|
|
1190
|
+
const cycle = parseInt(parseField2(block, "Cycle"), 10);
|
|
1191
|
+
if (isNaN(cycle)) return null;
|
|
1183
1192
|
const comments = parseField2(block, "Comments");
|
|
1184
1193
|
const uuidRaw = parseField2(block, "UUID");
|
|
1185
1194
|
const displayIdRaw = parseField2(block, "Display ID");
|
|
1186
|
-
const review = { uuid: uuidRaw ?? randomUUID5(), ...displayIdRaw ? { displayId: displayIdRaw } : {}, taskId, stage, reviewer, verdict,
|
|
1195
|
+
const review = { uuid: uuidRaw ?? randomUUID5(), ...displayIdRaw ? { displayId: displayIdRaw } : {}, taskId, stage, reviewer, verdict, cycle, date, comments };
|
|
1187
1196
|
const handoffRevRaw = parseField2(block, "Handoff Revision");
|
|
1188
1197
|
if (handoffRevRaw) {
|
|
1189
1198
|
const parsed = parseInt(handoffRevRaw, 10);
|
|
@@ -1209,11 +1218,22 @@ function serializeReview(review) {
|
|
|
1209
1218
|
lines.push(
|
|
1210
1219
|
`- **Reviewer:** ${review.reviewer}`,
|
|
1211
1220
|
`- **Verdict:** ${review.verdict}`,
|
|
1212
|
-
`- **
|
|
1221
|
+
`- **Cycle:** ${review.cycle}`,
|
|
1213
1222
|
`- **Comments:** ${review.comments}`
|
|
1214
1223
|
);
|
|
1215
1224
|
if (review.handoffRevision !== void 0) lines.push(`- **Handoff Revision:** ${review.handoffRevision}`);
|
|
1216
1225
|
if (review.buildCommitSha) lines.push(`- **Build Commit SHA:** ${review.buildCommitSha}`);
|
|
1226
|
+
if (review.autoReview) {
|
|
1227
|
+
lines.push("", `#### Auto-Review (${review.autoReview.verdict})`);
|
|
1228
|
+
lines.push(`> ${review.autoReview.summary}`);
|
|
1229
|
+
if (review.autoReview.findings.length > 0) {
|
|
1230
|
+
lines.push("");
|
|
1231
|
+
for (const f of review.autoReview.findings) {
|
|
1232
|
+
const loc = f.file ? f.line ? `${f.file}:${f.line}` : f.file : "";
|
|
1233
|
+
lines.push(`- \`${f.severity}\`${loc ? ` ${loc}` : ""}: ${f.message}`);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1217
1237
|
return lines.join("\n");
|
|
1218
1238
|
}
|
|
1219
1239
|
function prependReview(review, content) {
|
|
@@ -1320,126 +1340,15 @@ ${PHASES_END}`;
|
|
|
1320
1340
|
return content.trimEnd() + "\n\n" + newSection + "\n";
|
|
1321
1341
|
}
|
|
1322
1342
|
|
|
1323
|
-
// src/parsers/
|
|
1324
|
-
var VALID_STATUSES2 = /* @__PURE__ */ new Set(["Not Started", "In Progress", "Done", "Deferred"]);
|
|
1325
|
-
var FEATURES_START = "<!-- FEATURES:START -->";
|
|
1326
|
-
var FEATURES_END = "<!-- FEATURES:END -->";
|
|
1327
|
-
function parseFeatures(content) {
|
|
1328
|
-
const startIdx = content.indexOf(FEATURES_START);
|
|
1329
|
-
const endIdx = content.indexOf(FEATURES_END);
|
|
1330
|
-
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return [];
|
|
1331
|
-
const section = content.slice(startIdx + FEATURES_START.length, endIdx);
|
|
1332
|
-
const yamlMatch = section.match(/```yaml\s*\n([\s\S]*?)```/);
|
|
1333
|
-
if (!yamlMatch) return [];
|
|
1334
|
-
const yamlBody = yamlMatch[1];
|
|
1335
|
-
const features = [];
|
|
1336
|
-
const blocks = yamlBody.split(/^(?=\s*- id:)/m).filter((b) => b.trim());
|
|
1337
|
-
for (const block of blocks) {
|
|
1338
|
-
const feature = parseFeatureBlock(block);
|
|
1339
|
-
if (feature) features.push(feature);
|
|
1340
|
-
}
|
|
1341
|
-
return features.sort((a, b) => a.roadmapPosition - b.roadmapPosition);
|
|
1342
|
-
}
|
|
1343
|
-
function parseYamlField2(block, field) {
|
|
1344
|
-
const pattern = new RegExp(`^\\s*${field}:\\s*(.+)$`, "m");
|
|
1345
|
-
const match = block.match(pattern);
|
|
1346
|
-
if (!match) return "";
|
|
1347
|
-
let value = match[1].trim();
|
|
1348
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1349
|
-
value = value.slice(1, -1);
|
|
1350
|
-
}
|
|
1351
|
-
return value;
|
|
1352
|
-
}
|
|
1353
|
-
function parseOptionalYamlField(block, field) {
|
|
1354
|
-
const value = parseYamlField2(block, field);
|
|
1355
|
-
return value || void 0;
|
|
1356
|
-
}
|
|
1357
|
-
function parseFeatureBlock(block) {
|
|
1358
|
-
const idMatch = block.match(/^\s*- id:\s*(.+)$/m);
|
|
1359
|
-
if (!idMatch) return null;
|
|
1360
|
-
let id = idMatch[1].trim();
|
|
1361
|
-
if (id.startsWith('"') && id.endsWith('"') || id.startsWith("'") && id.endsWith("'")) {
|
|
1362
|
-
id = id.slice(1, -1);
|
|
1363
|
-
}
|
|
1364
|
-
const displayId = parseYamlField2(block, "displayId");
|
|
1365
|
-
const slug = parseYamlField2(block, "slug");
|
|
1366
|
-
const name = parseYamlField2(block, "name");
|
|
1367
|
-
const description = parseYamlField2(block, "description");
|
|
1368
|
-
const statusRaw = parseYamlField2(block, "status");
|
|
1369
|
-
const roadmapPositionRaw = parseYamlField2(block, "roadmapPosition");
|
|
1370
|
-
const createdAt = parseYamlField2(block, "createdAt");
|
|
1371
|
-
const createdSprintRaw = parseYamlField2(block, "createdSprint");
|
|
1372
|
-
if (!displayId || !slug || !name || !statusRaw || !roadmapPositionRaw || !createdAt || !createdSprintRaw) return null;
|
|
1373
|
-
const status = statusRaw;
|
|
1374
|
-
if (!VALID_STATUSES2.has(status)) return null;
|
|
1375
|
-
const roadmapPosition = parseInt(roadmapPositionRaw, 10);
|
|
1376
|
-
if (isNaN(roadmapPosition)) return null;
|
|
1377
|
-
const createdSprint = parseInt(createdSprintRaw, 10);
|
|
1378
|
-
if (isNaN(createdSprint)) return null;
|
|
1379
|
-
const completedAt = parseOptionalYamlField(block, "completedAt");
|
|
1380
|
-
const commitDate = parseOptionalYamlField(block, "commitDate");
|
|
1381
|
-
const completedSprintRaw = parseOptionalYamlField(block, "completedSprint");
|
|
1382
|
-
const completedSprint = completedSprintRaw ? parseInt(completedSprintRaw, 10) : void 0;
|
|
1383
|
-
return {
|
|
1384
|
-
id,
|
|
1385
|
-
displayId,
|
|
1386
|
-
slug,
|
|
1387
|
-
name,
|
|
1388
|
-
description,
|
|
1389
|
-
status,
|
|
1390
|
-
roadmapPosition,
|
|
1391
|
-
createdAt,
|
|
1392
|
-
completedAt,
|
|
1393
|
-
commitDate,
|
|
1394
|
-
createdSprint,
|
|
1395
|
-
completedSprint: completedSprint !== void 0 && !isNaN(completedSprint) ? completedSprint : void 0
|
|
1396
|
-
};
|
|
1397
|
-
}
|
|
1398
|
-
function serializeFeatures(features) {
|
|
1399
|
-
const sorted = [...features].sort((a, b) => a.roadmapPosition - b.roadmapPosition);
|
|
1400
|
-
const yamlLines = ["features:"];
|
|
1401
|
-
for (const f of sorted) {
|
|
1402
|
-
yamlLines.push(` - id: "${f.id}"`);
|
|
1403
|
-
yamlLines.push(` displayId: "${f.displayId}"`);
|
|
1404
|
-
yamlLines.push(` slug: "${f.slug}"`);
|
|
1405
|
-
yamlLines.push(` name: "${f.name}"`);
|
|
1406
|
-
yamlLines.push(` description: "${f.description}"`);
|
|
1407
|
-
yamlLines.push(` status: "${f.status}"`);
|
|
1408
|
-
yamlLines.push(` roadmapPosition: ${f.roadmapPosition}`);
|
|
1409
|
-
yamlLines.push(` createdAt: "${f.createdAt}"`);
|
|
1410
|
-
if (f.completedAt) yamlLines.push(` completedAt: "${f.completedAt}"`);
|
|
1411
|
-
if (f.commitDate) yamlLines.push(` commitDate: "${f.commitDate}"`);
|
|
1412
|
-
yamlLines.push(` createdSprint: ${f.createdSprint}`);
|
|
1413
|
-
if (f.completedSprint !== void 0) yamlLines.push(` completedSprint: ${f.completedSprint}`);
|
|
1414
|
-
}
|
|
1415
|
-
return yamlLines.join("\n");
|
|
1416
|
-
}
|
|
1417
|
-
function writeFeaturesToContent(features, content) {
|
|
1418
|
-
const yaml4 = serializeFeatures(features);
|
|
1419
|
-
const newSection = `${FEATURES_START}
|
|
1420
|
-
|
|
1421
|
-
\`\`\`yaml
|
|
1422
|
-
${yaml4}
|
|
1423
|
-
\`\`\`
|
|
1424
|
-
|
|
1425
|
-
${FEATURES_END}`;
|
|
1426
|
-
const startIdx = content.indexOf(FEATURES_START);
|
|
1427
|
-
const endIdx = content.indexOf(FEATURES_END);
|
|
1428
|
-
if (startIdx !== -1 && endIdx !== -1) {
|
|
1429
|
-
return content.slice(0, startIdx) + newSection + content.slice(endIdx + FEATURES_END.length);
|
|
1430
|
-
}
|
|
1431
|
-
return content.trimEnd() + "\n\n" + newSection + "\n";
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
// src/parsers/sprints.ts
|
|
1343
|
+
// src/parsers/cycles.ts
|
|
1435
1344
|
import yaml2 from "js-yaml";
|
|
1436
1345
|
var YAML_MARKER2 = "<!-- PAPI-ADAPTER: parse the yaml block below -->";
|
|
1437
1346
|
var YAML_START2 = "<!-- PAPI-YAML-START -->";
|
|
1438
1347
|
var YAML_END2 = "<!-- PAPI-YAML-END -->";
|
|
1439
|
-
var
|
|
1440
|
-
function
|
|
1441
|
-
if (!
|
|
1442
|
-
const
|
|
1348
|
+
var VALID_STATUSES2 = /* @__PURE__ */ new Set(["planning", "active", "complete"]);
|
|
1349
|
+
function toCycle(raw) {
|
|
1350
|
+
if (!VALID_STATUSES2.has(raw.status)) return null;
|
|
1351
|
+
const cycle = {
|
|
1443
1352
|
id: raw.id,
|
|
1444
1353
|
number: raw.number,
|
|
1445
1354
|
status: raw.status,
|
|
@@ -1448,71 +1357,71 @@ function toSprint(raw) {
|
|
|
1448
1357
|
boardHealth: raw.board_health ?? "",
|
|
1449
1358
|
taskIds: raw.task_ids ?? []
|
|
1450
1359
|
};
|
|
1451
|
-
if (raw.end_date)
|
|
1452
|
-
return
|
|
1360
|
+
if (raw.end_date) cycle.endDate = raw.end_date;
|
|
1361
|
+
return cycle;
|
|
1453
1362
|
}
|
|
1454
|
-
function
|
|
1363
|
+
function fromCycle(cycle) {
|
|
1455
1364
|
const raw = {
|
|
1456
|
-
id:
|
|
1457
|
-
number:
|
|
1458
|
-
status:
|
|
1459
|
-
start_date:
|
|
1460
|
-
goals:
|
|
1461
|
-
board_health:
|
|
1462
|
-
task_ids:
|
|
1365
|
+
id: cycle.id,
|
|
1366
|
+
number: cycle.number,
|
|
1367
|
+
status: cycle.status,
|
|
1368
|
+
start_date: cycle.startDate,
|
|
1369
|
+
goals: cycle.goals,
|
|
1370
|
+
board_health: cycle.boardHealth,
|
|
1371
|
+
task_ids: cycle.taskIds
|
|
1463
1372
|
};
|
|
1464
|
-
if (
|
|
1373
|
+
if (cycle.endDate) raw.end_date = cycle.endDate;
|
|
1465
1374
|
return raw;
|
|
1466
1375
|
}
|
|
1467
1376
|
function extractYamlBlock2(content) {
|
|
1468
1377
|
const markerIdx = content.indexOf(YAML_MARKER2);
|
|
1469
|
-
if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in
|
|
1378
|
+
if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in CYCLES.md");
|
|
1470
1379
|
const afterMarker = content.slice(markerIdx + YAML_MARKER2.length);
|
|
1471
1380
|
const startIdx = afterMarker.indexOf(YAML_START2);
|
|
1472
|
-
if (startIdx === -1) throw new Error("PAPI-YAML-START marker not found in
|
|
1381
|
+
if (startIdx === -1) throw new Error("PAPI-YAML-START marker not found in CYCLES.md");
|
|
1473
1382
|
const yamlStart = startIdx + YAML_START2.length;
|
|
1474
1383
|
const endIdx = afterMarker.indexOf(YAML_END2, yamlStart);
|
|
1475
|
-
if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in
|
|
1384
|
+
if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in CYCLES.md");
|
|
1476
1385
|
return afterMarker.slice(yamlStart, endIdx);
|
|
1477
1386
|
}
|
|
1478
|
-
function
|
|
1387
|
+
function parseCycles(content) {
|
|
1479
1388
|
if (!content.trim()) return [];
|
|
1480
1389
|
const yamlText = extractYamlBlock2(content);
|
|
1481
1390
|
const data = yaml2.load(yamlText);
|
|
1482
|
-
return (data.
|
|
1391
|
+
return (data.cycles ?? []).map(toCycle).filter((s) => s !== null);
|
|
1483
1392
|
}
|
|
1484
|
-
function
|
|
1485
|
-
const raw =
|
|
1486
|
-
const yamlStr = yaml2.dump({
|
|
1393
|
+
function serializeCycles(cycles, content) {
|
|
1394
|
+
const raw = cycles.map(fromCycle);
|
|
1395
|
+
const yamlStr = yaml2.dump({ cycles: raw }, { lineWidth: 120, quotingType: '"' });
|
|
1487
1396
|
const markerIdx = content.indexOf(YAML_MARKER2);
|
|
1488
|
-
if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in
|
|
1397
|
+
if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in CYCLES.md");
|
|
1489
1398
|
const afterMarker = content.slice(markerIdx + YAML_MARKER2.length);
|
|
1490
1399
|
const startIdx = afterMarker.indexOf(YAML_START2);
|
|
1491
|
-
if (startIdx === -1) throw new Error("PAPI-YAML-START marker not found in
|
|
1400
|
+
if (startIdx === -1) throw new Error("PAPI-YAML-START marker not found in CYCLES.md");
|
|
1492
1401
|
const absStart = markerIdx + YAML_MARKER2.length + startIdx;
|
|
1493
1402
|
const endIdx = afterMarker.indexOf(YAML_END2, startIdx);
|
|
1494
|
-
if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in
|
|
1403
|
+
if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in CYCLES.md");
|
|
1495
1404
|
const absEnd = markerIdx + YAML_MARKER2.length + endIdx + YAML_END2.length;
|
|
1496
1405
|
return content.slice(0, absStart) + YAML_START2 + "\n" + yamlStr + YAML_END2 + content.slice(absEnd);
|
|
1497
1406
|
}
|
|
1498
|
-
function
|
|
1499
|
-
const raw =
|
|
1407
|
+
function serializeCycle(cycle) {
|
|
1408
|
+
const raw = fromCycle(cycle);
|
|
1500
1409
|
return yaml2.dump(raw, { lineWidth: 120, quotingType: '"' }).trim();
|
|
1501
1410
|
}
|
|
1502
|
-
function
|
|
1411
|
+
function prependCycle(cycle, content) {
|
|
1503
1412
|
if (!content.trim()) {
|
|
1504
|
-
const header = `#
|
|
1413
|
+
const header = `# Cycles
|
|
1505
1414
|
|
|
1506
1415
|
<!-- PAPI-ADAPTER: parse the yaml block below -->
|
|
1507
1416
|
|
|
1508
1417
|
`;
|
|
1509
|
-
const raw =
|
|
1510
|
-
const yamlStr = yaml2.dump({
|
|
1418
|
+
const raw = fromCycle(cycle);
|
|
1419
|
+
const yamlStr = yaml2.dump({ cycles: [raw] }, { lineWidth: 120, quotingType: '"' });
|
|
1511
1420
|
return header + YAML_START2 + "\n" + yamlStr + YAML_END2 + "\n";
|
|
1512
1421
|
}
|
|
1513
|
-
const existing =
|
|
1514
|
-
const merged = [
|
|
1515
|
-
return
|
|
1422
|
+
const existing = parseCycles(content);
|
|
1423
|
+
const merged = [cycle, ...existing];
|
|
1424
|
+
return serializeCycles(merged, content);
|
|
1516
1425
|
}
|
|
1517
1426
|
|
|
1518
1427
|
// src/parsers/registries.ts
|
|
@@ -1588,18 +1497,18 @@ var MdFileAdapter = class {
|
|
|
1588
1497
|
await writeFile(this.path(file), content, "utf-8");
|
|
1589
1498
|
}
|
|
1590
1499
|
// --- Planning Log ---
|
|
1591
|
-
/** Parse the full planning context into structured sections (reads from PLANNING_LOG.md + ACTIVE_DECISIONS.md +
|
|
1500
|
+
/** Parse the full planning context into structured sections (reads from PLANNING_LOG.md + ACTIVE_DECISIONS.md + CYCLE_LOG.md). */
|
|
1592
1501
|
async readPlanningLog() {
|
|
1593
|
-
const [planningContent, activeDecisionsContent,
|
|
1502
|
+
const [planningContent, activeDecisionsContent, cycleLogContent] = await Promise.all([
|
|
1594
1503
|
this.read("PLANNING_LOG.md"),
|
|
1595
1504
|
this.readOptional("ACTIVE_DECISIONS.md"),
|
|
1596
1505
|
this.readOptional("SPRINT_LOG.md")
|
|
1597
1506
|
]);
|
|
1598
|
-
return parsePlanningLog(planningContent, activeDecisionsContent,
|
|
1507
|
+
return parsePlanningLog(planningContent, activeDecisionsContent, cycleLogContent);
|
|
1599
1508
|
}
|
|
1600
|
-
/** Read the
|
|
1601
|
-
async
|
|
1602
|
-
return
|
|
1509
|
+
/** Read the Cycle Health table from PLANNING_LOG.md. */
|
|
1510
|
+
async getCycleHealth() {
|
|
1511
|
+
return parseCycleHealth(await this.read("PLANNING_LOG.md"));
|
|
1603
1512
|
}
|
|
1604
1513
|
/** Read all Active Decisions from ACTIVE_DECISIONS.md. */
|
|
1605
1514
|
async getActiveDecisions() {
|
|
@@ -1607,58 +1516,58 @@ var MdFileAdapter = class {
|
|
|
1607
1516
|
if (!content) return [];
|
|
1608
1517
|
return parseActiveDecisions(content);
|
|
1609
1518
|
}
|
|
1610
|
-
/** Read
|
|
1611
|
-
async
|
|
1612
|
-
return
|
|
1519
|
+
/** Read cycle log entries (newest first), optionally limited to {@link limit} entries. */
|
|
1520
|
+
async getCycleLog(limit) {
|
|
1521
|
+
return parseCycleLog(await this.read("SPRINT_LOG.md"), limit);
|
|
1613
1522
|
}
|
|
1614
|
-
async
|
|
1615
|
-
const log = await this.
|
|
1616
|
-
return log.filter((entry) => entry.
|
|
1523
|
+
async getCycleLogSince(cycleNumber) {
|
|
1524
|
+
const log = await this.getCycleLog();
|
|
1525
|
+
return log.filter((entry) => entry.cycleNumber >= cycleNumber);
|
|
1617
1526
|
}
|
|
1618
|
-
/** Merge partial updates into the
|
|
1619
|
-
async
|
|
1527
|
+
/** Merge partial updates into the Cycle Health table and write back. */
|
|
1528
|
+
async setCycleHealth(updates) {
|
|
1620
1529
|
const content = await this.read("PLANNING_LOG.md");
|
|
1621
|
-
const current =
|
|
1530
|
+
const current = parseCycleHealth(content);
|
|
1622
1531
|
const updated = { ...current, ...updates };
|
|
1623
|
-
await this.write("PLANNING_LOG.md",
|
|
1532
|
+
await this.write("PLANNING_LOG.md", serializeCycleHealth(updated, content));
|
|
1624
1533
|
}
|
|
1625
|
-
/** Prepend a new
|
|
1626
|
-
async
|
|
1534
|
+
/** Prepend a new cycle log entry at the top of the Cycle Log section. */
|
|
1535
|
+
async writeCycleLogEntry(entry) {
|
|
1627
1536
|
if (!entry.uuid) {
|
|
1628
1537
|
entry = { ...entry, uuid: randomUUID6() };
|
|
1629
1538
|
}
|
|
1630
1539
|
const content = await this.read("SPRINT_LOG.md");
|
|
1631
|
-
await this.write("SPRINT_LOG.md",
|
|
1540
|
+
await this.write("SPRINT_LOG.md", prependCycleLogEntry(entry, content));
|
|
1632
1541
|
}
|
|
1633
|
-
/** Write a strategy review — for md adapter, delegates to
|
|
1542
|
+
/** Write a strategy review — for md adapter, delegates to cycle log. */
|
|
1634
1543
|
async writeStrategyReview(review) {
|
|
1635
|
-
await this.
|
|
1544
|
+
await this.writeCycleLogEntry({
|
|
1636
1545
|
uuid: randomUUID6(),
|
|
1637
|
-
|
|
1546
|
+
cycleNumber: review.cycleNumber,
|
|
1638
1547
|
title: review.title,
|
|
1639
1548
|
content: review.content,
|
|
1640
1549
|
notes: review.notes
|
|
1641
1550
|
});
|
|
1642
1551
|
}
|
|
1643
|
-
/** Get the
|
|
1644
|
-
async
|
|
1645
|
-
const log = await this.
|
|
1552
|
+
/** Get the cycle number of the last strategy review. */
|
|
1553
|
+
async getLastStrategyReviewCycle() {
|
|
1554
|
+
const log = await this.getCycleLog();
|
|
1646
1555
|
const entry = log.find(
|
|
1647
1556
|
(e) => /strategy.*review|strategic.*shift/i.test(e.title)
|
|
1648
1557
|
);
|
|
1649
|
-
return entry?.
|
|
1558
|
+
return entry?.cycleNumber ?? 0;
|
|
1650
1559
|
}
|
|
1651
|
-
/** Get strategy reviews — md adapter returns empty (reviews live in
|
|
1652
|
-
async getStrategyReviews(_limit) {
|
|
1560
|
+
/** Get strategy reviews — md adapter returns empty (reviews live in cycle log). */
|
|
1561
|
+
async getStrategyReviews(_limit, _includeFullAnalysis) {
|
|
1653
1562
|
return [];
|
|
1654
1563
|
}
|
|
1655
1564
|
/** Update or insert an Active Decision block by ID. */
|
|
1656
|
-
async updateActiveDecision(id, body,
|
|
1565
|
+
async updateActiveDecision(id, body, cycleNumber) {
|
|
1657
1566
|
const content = await this.readOptional("ACTIVE_DECISIONS.md") || "## Active Decisions\n\n";
|
|
1658
|
-
await this.write("ACTIVE_DECISIONS.md", updateActiveDecisionInContent(id, body, content,
|
|
1567
|
+
await this.write("ACTIVE_DECISIONS.md", updateActiveDecisionInContent(id, body, content, cycleNumber));
|
|
1659
1568
|
}
|
|
1660
|
-
// ---
|
|
1661
|
-
/** Query the
|
|
1569
|
+
// --- Cycle Board ---
|
|
1570
|
+
/** Query the cycle board, optionally filtering by status/priority/phase/etc. */
|
|
1662
1571
|
async queryBoard(options) {
|
|
1663
1572
|
const tasks = parseBoard(await this.read("SPRINT_BOARD.md"));
|
|
1664
1573
|
return options ? filterTasks(tasks, options) : tasks;
|
|
@@ -1714,7 +1623,7 @@ var MdFileAdapter = class {
|
|
|
1714
1623
|
const archivedTasks = archiveContent ? parseBoard(archiveContent) : [];
|
|
1715
1624
|
await this.warnInvalidPhase(task.phase);
|
|
1716
1625
|
await this.warnInvalidModule(task.module);
|
|
1717
|
-
await this.warnInvalidEpic(task.epic);
|
|
1626
|
+
if (task.epic) await this.warnInvalidEpic(task.epic);
|
|
1718
1627
|
if (task.dependsOn) {
|
|
1719
1628
|
const allTaskIds = new Set([...tasks, ...archivedTasks].map((t) => t.id));
|
|
1720
1629
|
const depIds = task.dependsOn.split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -1783,6 +1692,8 @@ var MdFileAdapter = class {
|
|
|
1783
1692
|
async updateTaskStatus(id, status) {
|
|
1784
1693
|
return this.updateTask(id, { status });
|
|
1785
1694
|
}
|
|
1695
|
+
async recordTransition(_taskId, _fromStatus, _toStatus, _changedBy) {
|
|
1696
|
+
}
|
|
1786
1697
|
// --- Build Reports ---
|
|
1787
1698
|
/** Insert a new build report at the top of BUILD_REPORTS.md. */
|
|
1788
1699
|
async appendBuildReport(report) {
|
|
@@ -1810,10 +1721,10 @@ var MdFileAdapter = class {
|
|
|
1810
1721
|
const reports = parseBuildReports(await this.read("BUILD_REPORTS.md"));
|
|
1811
1722
|
return reports.slice(0, count);
|
|
1812
1723
|
}
|
|
1813
|
-
/** Return all build reports from
|
|
1814
|
-
async getBuildReportsSince(
|
|
1724
|
+
/** Return all build reports from cycles >= {@link cycleNumber}. */
|
|
1725
|
+
async getBuildReportsSince(cycleNumber) {
|
|
1815
1726
|
const reports = parseBuildReports(await this.read("BUILD_REPORTS.md"));
|
|
1816
|
-
return reports.filter((r) => r.
|
|
1727
|
+
return reports.filter((r) => r.cycle >= cycleNumber);
|
|
1817
1728
|
}
|
|
1818
1729
|
// --- Human Reviews ---
|
|
1819
1730
|
/** Return recent human reviews from REVIEWS.md (newest first), optionally limited to {@link count}. */
|
|
@@ -1845,10 +1756,10 @@ var MdFileAdapter = class {
|
|
|
1845
1756
|
await this.write("REVIEWS.md", prependReview(review, content));
|
|
1846
1757
|
}
|
|
1847
1758
|
// --- Compression ---
|
|
1848
|
-
/** Compress old
|
|
1849
|
-
async
|
|
1759
|
+
/** Compress old cycle log entries below {@link threshold} into a summary block. */
|
|
1760
|
+
async compressCycleLog(threshold, summary) {
|
|
1850
1761
|
const content = await this.read("SPRINT_LOG.md");
|
|
1851
|
-
await this.write("SPRINT_LOG.md",
|
|
1762
|
+
await this.write("SPRINT_LOG.md", compressCycleLogInContent(content, threshold, summary));
|
|
1852
1763
|
}
|
|
1853
1764
|
/** Compress old build reports below {@link threshold} into a summary block. */
|
|
1854
1765
|
async compressBuildReports(threshold, summary) {
|
|
@@ -1870,17 +1781,17 @@ var MdFileAdapter = class {
|
|
|
1870
1781
|
const { buildHandoff, buildReport, ...rest } = task;
|
|
1871
1782
|
return rest;
|
|
1872
1783
|
}
|
|
1873
|
-
/** Append tasks to
|
|
1784
|
+
/** Append tasks to ARCHIVE_CYCLE_BOARD.md, stripping heavy fields and deduplicating by ID. */
|
|
1874
1785
|
async appendToArchive(tasks) {
|
|
1875
1786
|
const existing = await this.readOptional("ARCHIVE_SPRINT_BOARD.md");
|
|
1876
|
-
const archiveContent = existing || "# PAPI
|
|
1787
|
+
const archiveContent = existing || "# PAPI Cycle Board \u2014 Archive\n\n<!-- PAPI-ADAPTER: parse the yaml block below -->\n\n<!-- PAPI-YAML-START -->\ntasks: []\n<!-- PAPI-YAML-END -->\n";
|
|
1877
1788
|
const existingArchived = parseBoard(archiveContent);
|
|
1878
1789
|
const existingIds = new Set(existingArchived.map((t) => t.id));
|
|
1879
1790
|
const newArchive = tasks.filter((t) => !existingIds.has(t.id)).map((t) => this.stripHeavyFields(t));
|
|
1880
1791
|
const merged = [...existingArchived, ...newArchive];
|
|
1881
1792
|
await this.write("ARCHIVE_SPRINT_BOARD.md", serializeBoard(merged, archiveContent));
|
|
1882
1793
|
}
|
|
1883
|
-
/** Archive tasks matching phases and/or statuses to
|
|
1794
|
+
/** Archive tasks matching phases and/or statuses to ARCHIVE_CYCLE_BOARD.md and remove them from active board. */
|
|
1884
1795
|
async archiveTasks(phases, statuses) {
|
|
1885
1796
|
const content = await this.read("SPRINT_BOARD.md");
|
|
1886
1797
|
const tasks = parseBoard(content);
|
|
@@ -1916,6 +1827,11 @@ var MdFileAdapter = class {
|
|
|
1916
1827
|
async updateProductBrief(content) {
|
|
1917
1828
|
await this.write("PRODUCT_BRIEF.md", content);
|
|
1918
1829
|
}
|
|
1830
|
+
async readDiscoveryCanvas() {
|
|
1831
|
+
return {};
|
|
1832
|
+
}
|
|
1833
|
+
async updateDiscoveryCanvas(_canvas) {
|
|
1834
|
+
}
|
|
1919
1835
|
/** Read all phases from PHASES.md (falls back to PRODUCT_BRIEF.md for migration). */
|
|
1920
1836
|
async readPhases() {
|
|
1921
1837
|
const phasesContent = await this.readOptional("PHASES.md");
|
|
@@ -1944,59 +1860,6 @@ ${PHASES_END2}`;
|
|
|
1944
1860
|
} else {
|
|
1945
1861
|
await this.write("PHASES.md", `# Phases
|
|
1946
1862
|
|
|
1947
|
-
${newSection}
|
|
1948
|
-
`);
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
// --- Features ---
|
|
1952
|
-
/** Read all features from FEATURES.md. */
|
|
1953
|
-
async readFeatures() {
|
|
1954
|
-
const content = await this.readOptional("FEATURES.md");
|
|
1955
|
-
return content ? parseFeatures(content) : [];
|
|
1956
|
-
}
|
|
1957
|
-
/** Get a single feature by UUID. */
|
|
1958
|
-
async getFeature(id) {
|
|
1959
|
-
const features = await this.readFeatures();
|
|
1960
|
-
return features.find((f) => f.id === id) ?? null;
|
|
1961
|
-
}
|
|
1962
|
-
/** Create a new feature and append it to FEATURES.md. */
|
|
1963
|
-
async createFeature(feature) {
|
|
1964
|
-
const features = await this.readFeatures();
|
|
1965
|
-
const id = randomUUID6();
|
|
1966
|
-
const newFeature = { id, ...feature };
|
|
1967
|
-
features.push(newFeature);
|
|
1968
|
-
await this.writeFeatures(features);
|
|
1969
|
-
return newFeature;
|
|
1970
|
-
}
|
|
1971
|
-
/** Update an existing feature by UUID. */
|
|
1972
|
-
async updateFeature(id, updates) {
|
|
1973
|
-
const features = await this.readFeatures();
|
|
1974
|
-
const idx = features.findIndex((f) => f.id === id);
|
|
1975
|
-
if (idx === -1) throw new Error(`Feature not found: ${id}`);
|
|
1976
|
-
features[idx] = { ...features[idx], ...updates };
|
|
1977
|
-
await this.writeFeatures(features);
|
|
1978
|
-
}
|
|
1979
|
-
/** Write all features to FEATURES.md. */
|
|
1980
|
-
async writeFeatures(features) {
|
|
1981
|
-
const content = await this.readOptional("FEATURES.md");
|
|
1982
|
-
const existing = content || "";
|
|
1983
|
-
const yaml4 = serializeFeatures(features);
|
|
1984
|
-
const FEATURES_START2 = "<!-- FEATURES:START -->";
|
|
1985
|
-
const FEATURES_END2 = "<!-- FEATURES:END -->";
|
|
1986
|
-
const newSection = `${FEATURES_START2}
|
|
1987
|
-
|
|
1988
|
-
\`\`\`yaml
|
|
1989
|
-
${yaml4}
|
|
1990
|
-
\`\`\`
|
|
1991
|
-
|
|
1992
|
-
${FEATURES_END2}`;
|
|
1993
|
-
const startIdx = existing.indexOf(FEATURES_START2);
|
|
1994
|
-
const endIdx = existing.indexOf(FEATURES_END2);
|
|
1995
|
-
if (startIdx !== -1 && endIdx !== -1) {
|
|
1996
|
-
await this.write("FEATURES.md", existing.slice(0, startIdx) + newSection + existing.slice(endIdx + FEATURES_END2.length));
|
|
1997
|
-
} else {
|
|
1998
|
-
await this.write("FEATURES.md", `# Features
|
|
1999
|
-
|
|
2000
1863
|
${newSection}
|
|
2001
1864
|
`);
|
|
2002
1865
|
}
|
|
@@ -2013,10 +1876,10 @@ ${newSection}
|
|
|
2013
1876
|
if (!content) return [];
|
|
2014
1877
|
return parseToolMetrics(content);
|
|
2015
1878
|
}
|
|
2016
|
-
/** Aggregate tool call metrics into a cost summary, optionally filtered by
|
|
2017
|
-
async getCostSummary(
|
|
1879
|
+
/** Aggregate tool call metrics into a cost summary, optionally filtered by cycle. */
|
|
1880
|
+
async getCostSummary(cycleNumber) {
|
|
2018
1881
|
const metrics = await this.readToolMetrics();
|
|
2019
|
-
return aggregateCostSummary(metrics,
|
|
1882
|
+
return aggregateCostSummary(metrics, cycleNumber);
|
|
2020
1883
|
}
|
|
2021
1884
|
/** Write a cost snapshot to the Cost Summary section of METRICS.md. */
|
|
2022
1885
|
async writeCostSnapshot(snapshot) {
|
|
@@ -2029,29 +1892,29 @@ ${newSection}
|
|
|
2029
1892
|
if (!content) return [];
|
|
2030
1893
|
return parseCostSnapshots(content);
|
|
2031
1894
|
}
|
|
2032
|
-
// ---
|
|
2033
|
-
/** Append a
|
|
2034
|
-
async
|
|
1895
|
+
// --- Cycle Methodology Metrics ---
|
|
1896
|
+
/** Append a cycle metrics snapshot to CYCLE_METRICS.md. */
|
|
1897
|
+
async appendCycleMetrics(snapshot) {
|
|
2035
1898
|
const content = await this.readOptional("SPRINT_METRICS.md");
|
|
2036
1899
|
await this.write("SPRINT_METRICS.md", appendSnapshotToContent(snapshot, content));
|
|
2037
1900
|
}
|
|
2038
|
-
/** Read all
|
|
2039
|
-
async
|
|
1901
|
+
/** Read all cycle metrics snapshots from CYCLE_METRICS.md. */
|
|
1902
|
+
async readCycleMetrics() {
|
|
2040
1903
|
const content = await this.readOptional("SPRINT_METRICS.md");
|
|
2041
1904
|
if (!content) return [];
|
|
2042
1905
|
return parseSnapshots(content);
|
|
2043
1906
|
}
|
|
2044
|
-
// ---
|
|
2045
|
-
/** Read all
|
|
2046
|
-
async
|
|
2047
|
-
const content = await this.readOptional("
|
|
1907
|
+
// --- Cycles ---
|
|
1908
|
+
/** Read all Cycle entities from CYCLES.md (newest first). */
|
|
1909
|
+
async readCycles() {
|
|
1910
|
+
const content = await this.readOptional("CYCLES.md");
|
|
2048
1911
|
if (!content) return [];
|
|
2049
|
-
return
|
|
1912
|
+
return parseCycles(content);
|
|
2050
1913
|
}
|
|
2051
|
-
/** Write a new
|
|
2052
|
-
async
|
|
2053
|
-
const content = await this.readOptional("
|
|
2054
|
-
await this.write("
|
|
1914
|
+
/** Write a new Cycle entity to CYCLES.md. */
|
|
1915
|
+
async createCycle(cycle) {
|
|
1916
|
+
const content = await this.readOptional("CYCLES.md");
|
|
1917
|
+
await this.write("CYCLES.md", prependCycle(cycle, content));
|
|
2055
1918
|
}
|
|
2056
1919
|
// --- Registries ---
|
|
2057
1920
|
/** Read module and epic registries from REGISTRIES.md. */
|
|
@@ -2081,8 +1944,9 @@ ${newSection}
|
|
|
2081
1944
|
` type: ${full.type}`,
|
|
2082
1945
|
` status: ${full.status}`,
|
|
2083
1946
|
` content: ${JSON.stringify(full.content)}`,
|
|
2084
|
-
` created_sprint: ${full.
|
|
2085
|
-
full.
|
|
1947
|
+
` created_sprint: ${full.createdCycle}`,
|
|
1948
|
+
full.actionedCycle != null ? ` actioned_cycle: ${full.actionedCycle}` : null,
|
|
1949
|
+
full.target != null ? ` target: ${JSON.stringify(full.target)}` : null
|
|
2086
1950
|
].filter(Boolean).join("\n");
|
|
2087
1951
|
if (!content) {
|
|
2088
1952
|
await this.write("STRATEGY_RECOMMENDATIONS.md", `${header}${entry}
|
|
@@ -2115,7 +1979,7 @@ ${footer}`);
|
|
|
2115
1979
|
const statusMatch = block.match(/status:\s+(.+)/);
|
|
2116
1980
|
const contentMatch = block.match(/content:\s+(.+)/);
|
|
2117
1981
|
const createdMatch = block.match(/created_sprint:\s+(\d+)/);
|
|
2118
|
-
const actionedMatch = block.match(/
|
|
1982
|
+
const actionedMatch = block.match(/actioned_cycle:\s+(\d+)/);
|
|
2119
1983
|
if (!idMatch || !typeMatch || !statusMatch || !contentMatch || !createdMatch) continue;
|
|
2120
1984
|
const status = statusMatch[1].trim();
|
|
2121
1985
|
if (status !== "pending") continue;
|
|
@@ -2131,24 +1995,24 @@ ${footer}`);
|
|
|
2131
1995
|
type: typeMatch[1].trim(),
|
|
2132
1996
|
status: "pending",
|
|
2133
1997
|
content: parsedContent,
|
|
2134
|
-
|
|
2135
|
-
|
|
1998
|
+
createdCycle: parseInt(createdMatch[1], 10),
|
|
1999
|
+
actionedCycle: actionedMatch ? parseInt(actionedMatch[1], 10) : void 0
|
|
2136
2000
|
});
|
|
2137
2001
|
}
|
|
2138
2002
|
return recs;
|
|
2139
2003
|
}
|
|
2140
2004
|
/** Mark a recommendation as actioned. */
|
|
2141
|
-
async actionRecommendation(id,
|
|
2005
|
+
async actionRecommendation(id, cycleNumber) {
|
|
2142
2006
|
const content = await this.readOptional("STRATEGY_RECOMMENDATIONS.md");
|
|
2143
2007
|
if (!content) return;
|
|
2144
2008
|
const idPattern = new RegExp(`(\\s+-\\s+id:\\s+${id}\\n(?:.*\\n)*?)(\\s+status:\\s+)pending`);
|
|
2145
2009
|
let updated = content.replace(idPattern, `$1$2actioned`);
|
|
2146
2010
|
const entryPattern = new RegExp(`(\\s+-\\s+id:\\s+${id}\\n(?:.*\\n)*?)(?=\\s+-\\s+id:|<!-- PAPI-YAML-END -->)`);
|
|
2147
2011
|
const entryMatch = updated.match(entryPattern);
|
|
2148
|
-
if (entryMatch && !entryMatch[0].includes("
|
|
2149
|
-
const
|
|
2150
|
-
updated = updated.replace(
|
|
2151
|
-
|
|
2012
|
+
if (entryMatch && !entryMatch[0].includes("actioned_cycle:")) {
|
|
2013
|
+
const cyclePattern = new RegExp(`(\\s+-\\s+id:\\s+${id}\\n(?:.*\\n)*?\\s+created_sprint:\\s+\\d+)\\n`);
|
|
2014
|
+
updated = updated.replace(cyclePattern, `$1
|
|
2015
|
+
actioned_cycle: ${cycleNumber}
|
|
2152
2016
|
`);
|
|
2153
2017
|
}
|
|
2154
2018
|
await this.write("STRATEGY_RECOMMENDATIONS.md", updated);
|
|
@@ -2162,7 +2026,7 @@ ${footer}`);
|
|
|
2162
2026
|
const full = { ...event, id, createdAt };
|
|
2163
2027
|
const content = await this.readOptional("DECISION_EVENTS.md");
|
|
2164
2028
|
const header = "# Decision Events\n\n";
|
|
2165
|
-
const entry = `## ${full.decisionId} | ${full.eventType} |
|
|
2029
|
+
const entry = `## ${full.decisionId} | ${full.eventType} | Cycle ${full.cycle}
|
|
2166
2030
|
- **id:** ${full.id}
|
|
2167
2031
|
- **source:** ${full.source}
|
|
2168
2032
|
` + (full.sourceRef ? `- **sourceRef:** ${full.sourceRef}
|
|
@@ -2184,9 +2048,9 @@ ${footer}`);
|
|
|
2184
2048
|
const filtered = all.filter((e) => e.decisionId === decisionId);
|
|
2185
2049
|
return limit ? filtered.slice(0, limit) : filtered;
|
|
2186
2050
|
}
|
|
2187
|
-
async getDecisionEventsSince(
|
|
2051
|
+
async getDecisionEventsSince(cycle) {
|
|
2188
2052
|
const all = await this.parseDecisionEvents();
|
|
2189
|
-
return all.filter((e) => e.
|
|
2053
|
+
return all.filter((e) => e.cycle >= cycle);
|
|
2190
2054
|
}
|
|
2191
2055
|
async parseDecisionEvents() {
|
|
2192
2056
|
const content = await this.readOptional("DECISION_EVENTS.md");
|
|
@@ -2194,7 +2058,7 @@ ${footer}`);
|
|
|
2194
2058
|
const events = [];
|
|
2195
2059
|
const blocks = content.split("---").filter((b) => b.trim());
|
|
2196
2060
|
for (const block of blocks) {
|
|
2197
|
-
const headingMatch = block.match(/^##\s+(\S+)\s+\|\s+(\S+)\s+\|\s+
|
|
2061
|
+
const headingMatch = block.match(/^##\s+(\S+)\s+\|\s+(\S+)\s+\|\s+Cycle\s+(\d+)/m);
|
|
2198
2062
|
if (!headingMatch) continue;
|
|
2199
2063
|
const idMatch = block.match(/\*\*id:\*\*\s+(.+)/);
|
|
2200
2064
|
const sourceMatch = block.match(/\*\*source:\*\*\s+(.+)/);
|
|
@@ -2206,7 +2070,7 @@ ${footer}`);
|
|
|
2206
2070
|
id: idMatch[1].trim(),
|
|
2207
2071
|
decisionId: headingMatch[1],
|
|
2208
2072
|
eventType: headingMatch[2],
|
|
2209
|
-
|
|
2073
|
+
cycle: parseInt(headingMatch[3], 10),
|
|
2210
2074
|
source: sourceMatch[1].trim(),
|
|
2211
2075
|
sourceRef: sourceRefMatch?.[1]?.trim(),
|
|
2212
2076
|
detail: detailMatch?.[1]?.trim(),
|
|
@@ -2222,7 +2086,7 @@ ${footer}`);
|
|
|
2222
2086
|
const full = { ...score, id, totalScore, createdAt };
|
|
2223
2087
|
const content = await this.readOptional("DECISION_SCORES.md");
|
|
2224
2088
|
const header = "# Decision Scores\n\n";
|
|
2225
|
-
const entry = `## ${full.decisionId} |
|
|
2089
|
+
const entry = `## ${full.decisionId} | Cycle ${full.cycle}
|
|
2226
2090
|
- **id:** ${full.id}
|
|
2227
2091
|
- **effort:** ${full.effort}
|
|
2228
2092
|
- **risk:** ${full.risk}
|
|
@@ -2252,7 +2116,7 @@ ${footer}`);
|
|
|
2252
2116
|
const latest = /* @__PURE__ */ new Map();
|
|
2253
2117
|
for (const s of all) {
|
|
2254
2118
|
const existing = latest.get(s.decisionId);
|
|
2255
|
-
if (!existing || s.
|
|
2119
|
+
if (!existing || s.cycle > existing.cycle) {
|
|
2256
2120
|
latest.set(s.decisionId, s);
|
|
2257
2121
|
}
|
|
2258
2122
|
}
|
|
@@ -2264,7 +2128,7 @@ ${footer}`);
|
|
|
2264
2128
|
const scores = [];
|
|
2265
2129
|
const blocks = content.split("---").filter((b) => b.trim());
|
|
2266
2130
|
for (const block of blocks) {
|
|
2267
|
-
const headingMatch = block.match(/^##\s+(\S+)\s+\|\s+
|
|
2131
|
+
const headingMatch = block.match(/^##\s+(\S+)\s+\|\s+Cycle\s+(\d+)/m);
|
|
2268
2132
|
if (!headingMatch) continue;
|
|
2269
2133
|
const field = (name) => block.match(new RegExp(`\\*\\*${name}:\\*\\*\\s+(.+)`))?.[1]?.trim();
|
|
2270
2134
|
const id = field("id");
|
|
@@ -2273,7 +2137,7 @@ ${footer}`);
|
|
|
2273
2137
|
scores.push({
|
|
2274
2138
|
id,
|
|
2275
2139
|
decisionId: headingMatch[1],
|
|
2276
|
-
|
|
2140
|
+
cycle: parseInt(headingMatch[2], 10),
|
|
2277
2141
|
effort: parseInt(field("effort") ?? "0", 10),
|
|
2278
2142
|
risk: parseInt(field("risk") ?? "0", 10),
|
|
2279
2143
|
reversibility: parseInt(field("reversibility") ?? "0", 10),
|
|
@@ -2289,7 +2153,7 @@ ${footer}`);
|
|
|
2289
2153
|
// Entity reference tracking — no-op for md adapter (DB-only feature)
|
|
2290
2154
|
async logEntityReferences(_refs) {
|
|
2291
2155
|
}
|
|
2292
|
-
async getDecisionUsage(
|
|
2156
|
+
async getDecisionUsage(_currentCycle) {
|
|
2293
2157
|
return [];
|
|
2294
2158
|
}
|
|
2295
2159
|
};
|
|
@@ -2303,9 +2167,9 @@ function isRealComment(text) {
|
|
|
2303
2167
|
const trimmed = text.trim();
|
|
2304
2168
|
return trimmed.length > 0 && !NONE_PATTERN2.test(trimmed);
|
|
2305
2169
|
}
|
|
2306
|
-
async function detectReviewPatterns(reviews,
|
|
2170
|
+
async function detectReviewPatterns(reviews, currentCycle, window = 5, clusterer) {
|
|
2307
2171
|
const recentReviews = reviews.filter(
|
|
2308
|
-
(r) => r.
|
|
2172
|
+
(r) => r.cycle > currentCycle - window && r.cycle <= currentCycle
|
|
2309
2173
|
);
|
|
2310
2174
|
let recurringFeedback;
|
|
2311
2175
|
const realComments = recentReviews.filter((r) => isRealComment(r.comments));
|
|
@@ -2367,7 +2231,7 @@ export {
|
|
|
2367
2231
|
VALID_TRANSITIONS,
|
|
2368
2232
|
aggregateCostSummary,
|
|
2369
2233
|
appendToolMetricToContent,
|
|
2370
|
-
|
|
2234
|
+
calculateCycleMetrics,
|
|
2371
2235
|
detectBuildPatterns,
|
|
2372
2236
|
detectReviewPatterns,
|
|
2373
2237
|
hasBuildPatterns,
|
|
@@ -2375,24 +2239,21 @@ export {
|
|
|
2375
2239
|
isValidStatus,
|
|
2376
2240
|
isValidTransition,
|
|
2377
2241
|
parseBuildHandoff,
|
|
2242
|
+
parseCycles,
|
|
2378
2243
|
parseEffortSize,
|
|
2379
|
-
parseFeatures,
|
|
2380
2244
|
parsePhases,
|
|
2381
2245
|
parseRegistries,
|
|
2382
2246
|
parseReviews,
|
|
2383
|
-
parseSprints,
|
|
2384
2247
|
parseToolMetrics,
|
|
2248
|
+
prependCycle,
|
|
2385
2249
|
prependReview,
|
|
2386
|
-
prependSprint,
|
|
2387
2250
|
serializeBuildHandoff,
|
|
2388
|
-
|
|
2251
|
+
serializeCycle,
|
|
2252
|
+
serializeCycles,
|
|
2389
2253
|
serializePhases,
|
|
2390
2254
|
serializeRegistries,
|
|
2391
2255
|
serializeReview,
|
|
2392
|
-
serializeSprint,
|
|
2393
|
-
serializeSprints,
|
|
2394
2256
|
serializeToolMetric,
|
|
2395
2257
|
validateTransition,
|
|
2396
|
-
writeFeaturesToContent,
|
|
2397
2258
|
writePhasesToContent
|
|
2398
2259
|
};
|