scrumrun 2.6.7 → 2.6.9
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 +1 -1
- package/lib/commands/normalize-legacy.js +3 -2
- package/lib/commands/repair.js +59 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
ScrumRun gives an agent a small command surface and a precise project memory: what should be done, how each attempt happened, which decisions constrain the code, and why the architecture exists in its current form.
|
|
6
6
|
|
|
7
|
-
**Package:** `2.6.
|
|
7
|
+
**Package:** `2.6.9` · **Method target:** `2.0.0` · **Runtime:** Node.js `>=22.13.0` · **License:** MIT
|
|
8
8
|
|
|
9
9
|
**New here?** Read the [Quickstart](docs/QUICKSTART.md) — first Run in under 10 minutes, no `SPEC.md` reading required. Full docs map in [`docs/INDEX.md`](docs/INDEX.md).
|
|
10
10
|
|
|
@@ -83,12 +83,13 @@ function needsNormalization(record, body) {
|
|
|
83
83
|
function buildNormalizedBody({ record, originalBody, backupRelative, reason, errors }) {
|
|
84
84
|
const title = extractTitle(originalBody, `Run ${record.id}`);
|
|
85
85
|
const sha = crypto.createHash("sha256").update(originalBody).digest("hex");
|
|
86
|
+
const snapshotDate = record.created || new Date().toISOString().slice(0, 10);
|
|
86
87
|
const normalizedRecord = {
|
|
87
88
|
id: record.id,
|
|
88
89
|
kind: "run",
|
|
89
90
|
status: record.status || "partial",
|
|
90
91
|
created: record.created,
|
|
91
|
-
updated:
|
|
92
|
+
updated: snapshotDate,
|
|
92
93
|
method: "2.0.0",
|
|
93
94
|
task: TASK_REF_PATTERN.test(record.task || "") ? record.task : null,
|
|
94
95
|
sprint: record.sprint || null,
|
|
@@ -108,7 +109,7 @@ function buildNormalizedBody({ record, originalBody, backupRelative, reason, err
|
|
|
108
109
|
id: `${record.id}-EVT-001`,
|
|
109
110
|
sequence: 1,
|
|
110
111
|
type: "snapshot",
|
|
111
|
-
occurred_at: `${
|
|
112
|
+
occurred_at: `${snapshotDate}T00:00:00.000Z`,
|
|
112
113
|
timestamp_precision: "date",
|
|
113
114
|
actor: "migration",
|
|
114
115
|
from: null,
|
package/lib/commands/repair.js
CHANGED
|
@@ -102,6 +102,29 @@ function scanFeatureIds(scrumDir) {
|
|
|
102
102
|
return ids;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
function scanTaskIds(scrumDir) {
|
|
106
|
+
const dir = path.join(scrumDir, "tasks");
|
|
107
|
+
const ids = new Set();
|
|
108
|
+
for (const file of listDir(dir, TASK_FILE)) {
|
|
109
|
+
ids.add(path.basename(file, ".md"));
|
|
110
|
+
}
|
|
111
|
+
return ids;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function planOrphanRuns(scrumDir, taskIds) {
|
|
115
|
+
const orphans = [];
|
|
116
|
+
for (const file of listDir(path.join(scrumDir, "runs"), RUN_FILE)) {
|
|
117
|
+
const raw = readIf(file);
|
|
118
|
+
if (raw === null) continue;
|
|
119
|
+
const split = splitFrontmatter(raw);
|
|
120
|
+
if (!split) continue;
|
|
121
|
+
const taskRef = extractField(split.header, "task");
|
|
122
|
+
if (taskRef && taskRef !== "null" && TASK_REF.test(taskRef) && taskIds.has(taskRef)) continue;
|
|
123
|
+
orphans.push({ file, reason: taskRef ? `task ${taskRef} is null or missing from .scrumrun/tasks/` : "task field missing", originalText: raw });
|
|
124
|
+
}
|
|
125
|
+
return orphans;
|
|
126
|
+
}
|
|
127
|
+
|
|
105
128
|
function planFile(file, kind, ctx) {
|
|
106
129
|
const original = readIf(file);
|
|
107
130
|
if (original === null) return null;
|
|
@@ -164,6 +187,23 @@ function planFile(file, kind, ctx) {
|
|
|
164
187
|
}
|
|
165
188
|
|
|
166
189
|
if (kind === "run") {
|
|
190
|
+
// Reconcile `updated` with the last event's occurred_at date. Fixes Runs
|
|
191
|
+
// normalized by an older CLI that stamped `updated: today` instead of
|
|
192
|
+
// matching the snapshot event, which produced the "Run updated date
|
|
193
|
+
// disagrees with final event timestamp" migration blocker.
|
|
194
|
+
const currentUpdated = extractField(header, "updated");
|
|
195
|
+
const eventDates = [...split.body.matchAll(/"occurred_at":\s*"(\d{4}-\d{2}-\d{2})/g)].map((m) => m[1]);
|
|
196
|
+
if (eventDates.length && currentUpdated && ISO_DATE.test(currentUpdated)) {
|
|
197
|
+
const lastEventDate = eventDates[eventDates.length - 1];
|
|
198
|
+
if (currentUpdated !== lastEventDate) {
|
|
199
|
+
const next = replaceField(header, "updated", lastEventDate);
|
|
200
|
+
if (next !== header) {
|
|
201
|
+
changes.push({ field: "updated", from: currentUpdated, to: lastEventDate });
|
|
202
|
+
header = next;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
167
207
|
const taskRef = extractField(header, "task");
|
|
168
208
|
if (taskRef === undefined) {
|
|
169
209
|
if (hasField(header, "id")) {
|
|
@@ -223,8 +263,10 @@ function planGuardrails(scrumDir) {
|
|
|
223
263
|
|
|
224
264
|
function analyze(scrumDir) {
|
|
225
265
|
const featureIds = scanFeatureIds(scrumDir);
|
|
266
|
+
const taskIds = scanTaskIds(scrumDir);
|
|
226
267
|
const ctx = { featureIds };
|
|
227
268
|
const entries = [];
|
|
269
|
+
const orphanRuns = planOrphanRuns(scrumDir, taskIds);
|
|
228
270
|
|
|
229
271
|
const guardrailPlan = planGuardrails(scrumDir);
|
|
230
272
|
if (guardrailPlan) entries.push(guardrailPlan);
|
|
@@ -258,7 +300,7 @@ function analyze(scrumDir) {
|
|
|
258
300
|
totals.byField[change.field] = (totals.byField[change.field] || 0) + 1;
|
|
259
301
|
}
|
|
260
302
|
}
|
|
261
|
-
return { entries, totals };
|
|
303
|
+
return { entries, totals, orphanRuns };
|
|
262
304
|
}
|
|
263
305
|
|
|
264
306
|
function apply(scrumDir, plan) {
|
|
@@ -280,7 +322,18 @@ function apply(scrumDir, plan) {
|
|
|
280
322
|
} catch {
|
|
281
323
|
ledgerResult = { plan: { malformed: 0 }, applied: [] };
|
|
282
324
|
}
|
|
283
|
-
|
|
325
|
+
// Quarantine orphan Runs (Task ref missing/invalid — schema requires TASK-NNN). Move byte-exact
|
|
326
|
+
// to backup so nothing is deleted; the orphan can be manually re-linked or discarded later.
|
|
327
|
+
const orphanBackup = path.join(scrumDir, ".migration-backup", "repair", "orphan-runs");
|
|
328
|
+
const quarantined = [];
|
|
329
|
+
for (const orphan of plan.orphanRuns || []) {
|
|
330
|
+
fs.mkdirSync(orphanBackup, { recursive: true });
|
|
331
|
+
const target = path.join(orphanBackup, path.basename(orphan.file));
|
|
332
|
+
if (!fs.existsSync(target)) fs.writeFileSync(target, orphan.originalText);
|
|
333
|
+
fs.rmSync(orphan.file, { force: true });
|
|
334
|
+
quarantined.push({ id: path.basename(orphan.file, ".md"), reason: orphan.reason, backup: path.relative(scrumDir, target) });
|
|
335
|
+
}
|
|
336
|
+
return { applied, ledger: ledgerResult, quarantined };
|
|
284
337
|
}
|
|
285
338
|
|
|
286
339
|
function renderReport(plan, applied) {
|
|
@@ -288,6 +341,7 @@ function renderReport(plan, applied) {
|
|
|
288
341
|
lines.push("## Repair plan\n");
|
|
289
342
|
lines.push(`Files needing frontmatter repair: ${plan.entries.length}`);
|
|
290
343
|
if (plan.ledgerMalformed) lines.push(`Run ledgers to normalize: ${plan.ledgerMalformed}`);
|
|
344
|
+
if (plan.orphanRuns && plan.orphanRuns.length) lines.push(`Orphan Runs to quarantine: ${plan.orphanRuns.length}`);
|
|
291
345
|
const byField = plan.totals.byField;
|
|
292
346
|
if (Object.keys(byField).length) {
|
|
293
347
|
lines.push("\nBy field:");
|
|
@@ -307,9 +361,11 @@ function renderReport(plan, applied) {
|
|
|
307
361
|
lines.push("");
|
|
308
362
|
const frontmatterCount = applied.applied ? applied.applied.length : 0;
|
|
309
363
|
const ledgerCount = applied.ledger && applied.ledger.applied ? applied.ledger.applied.length : 0;
|
|
310
|
-
|
|
364
|
+
const quarantinedCount = applied.quarantined ? applied.quarantined.length : 0;
|
|
365
|
+
if (frontmatterCount || ledgerCount || quarantinedCount) {
|
|
311
366
|
if (frontmatterCount) lines.push(`Applied: ${frontmatterCount} frontmatter file(s) rewritten.`);
|
|
312
367
|
if (ledgerCount) lines.push(`Normalized: ${ledgerCount} Run ledger(s) collapsed to snapshot events.`);
|
|
368
|
+
if (quarantinedCount) lines.push(`Quarantined: ${quarantinedCount} orphan Run(s) moved to .scrumrun/.migration-backup/repair/orphan-runs/.`);
|
|
313
369
|
lines.push("Backups under .scrumrun/.migration-backup/repair/ and .scrumrun/.migration-backup/runs/.");
|
|
314
370
|
} else {
|
|
315
371
|
lines.push("Applied: no changes.");
|