scrumrun 2.6.7 → 2.6.8
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 +42 -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.8` · **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;
|
|
@@ -223,8 +246,10 @@ function planGuardrails(scrumDir) {
|
|
|
223
246
|
|
|
224
247
|
function analyze(scrumDir) {
|
|
225
248
|
const featureIds = scanFeatureIds(scrumDir);
|
|
249
|
+
const taskIds = scanTaskIds(scrumDir);
|
|
226
250
|
const ctx = { featureIds };
|
|
227
251
|
const entries = [];
|
|
252
|
+
const orphanRuns = planOrphanRuns(scrumDir, taskIds);
|
|
228
253
|
|
|
229
254
|
const guardrailPlan = planGuardrails(scrumDir);
|
|
230
255
|
if (guardrailPlan) entries.push(guardrailPlan);
|
|
@@ -258,7 +283,7 @@ function analyze(scrumDir) {
|
|
|
258
283
|
totals.byField[change.field] = (totals.byField[change.field] || 0) + 1;
|
|
259
284
|
}
|
|
260
285
|
}
|
|
261
|
-
return { entries, totals };
|
|
286
|
+
return { entries, totals, orphanRuns };
|
|
262
287
|
}
|
|
263
288
|
|
|
264
289
|
function apply(scrumDir, plan) {
|
|
@@ -280,7 +305,18 @@ function apply(scrumDir, plan) {
|
|
|
280
305
|
} catch {
|
|
281
306
|
ledgerResult = { plan: { malformed: 0 }, applied: [] };
|
|
282
307
|
}
|
|
283
|
-
|
|
308
|
+
// Quarantine orphan Runs (Task ref missing/invalid — schema requires TASK-NNN). Move byte-exact
|
|
309
|
+
// to backup so nothing is deleted; the orphan can be manually re-linked or discarded later.
|
|
310
|
+
const orphanBackup = path.join(scrumDir, ".migration-backup", "repair", "orphan-runs");
|
|
311
|
+
const quarantined = [];
|
|
312
|
+
for (const orphan of plan.orphanRuns || []) {
|
|
313
|
+
fs.mkdirSync(orphanBackup, { recursive: true });
|
|
314
|
+
const target = path.join(orphanBackup, path.basename(orphan.file));
|
|
315
|
+
if (!fs.existsSync(target)) fs.writeFileSync(target, orphan.originalText);
|
|
316
|
+
fs.rmSync(orphan.file, { force: true });
|
|
317
|
+
quarantined.push({ id: path.basename(orphan.file, ".md"), reason: orphan.reason, backup: path.relative(scrumDir, target) });
|
|
318
|
+
}
|
|
319
|
+
return { applied, ledger: ledgerResult, quarantined };
|
|
284
320
|
}
|
|
285
321
|
|
|
286
322
|
function renderReport(plan, applied) {
|
|
@@ -288,6 +324,7 @@ function renderReport(plan, applied) {
|
|
|
288
324
|
lines.push("## Repair plan\n");
|
|
289
325
|
lines.push(`Files needing frontmatter repair: ${plan.entries.length}`);
|
|
290
326
|
if (plan.ledgerMalformed) lines.push(`Run ledgers to normalize: ${plan.ledgerMalformed}`);
|
|
327
|
+
if (plan.orphanRuns && plan.orphanRuns.length) lines.push(`Orphan Runs to quarantine: ${plan.orphanRuns.length}`);
|
|
291
328
|
const byField = plan.totals.byField;
|
|
292
329
|
if (Object.keys(byField).length) {
|
|
293
330
|
lines.push("\nBy field:");
|
|
@@ -307,9 +344,11 @@ function renderReport(plan, applied) {
|
|
|
307
344
|
lines.push("");
|
|
308
345
|
const frontmatterCount = applied.applied ? applied.applied.length : 0;
|
|
309
346
|
const ledgerCount = applied.ledger && applied.ledger.applied ? applied.ledger.applied.length : 0;
|
|
310
|
-
|
|
347
|
+
const quarantinedCount = applied.quarantined ? applied.quarantined.length : 0;
|
|
348
|
+
if (frontmatterCount || ledgerCount || quarantinedCount) {
|
|
311
349
|
if (frontmatterCount) lines.push(`Applied: ${frontmatterCount} frontmatter file(s) rewritten.`);
|
|
312
350
|
if (ledgerCount) lines.push(`Normalized: ${ledgerCount} Run ledger(s) collapsed to snapshot events.`);
|
|
351
|
+
if (quarantinedCount) lines.push(`Quarantined: ${quarantinedCount} orphan Run(s) moved to .scrumrun/.migration-backup/repair/orphan-runs/.`);
|
|
313
352
|
lines.push("Backups under .scrumrun/.migration-backup/repair/ and .scrumrun/.migration-backup/runs/.");
|
|
314
353
|
} else {
|
|
315
354
|
lines.push("Applied: no changes.");
|