@ddtcorex/dsh-maestro-memory 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +21 -0
  3. package/README.md +448 -0
  4. package/cordis.patch.yml +13 -0
  5. package/lib/client.js +756 -0
  6. package/lib/index.d.ts +10 -0
  7. package/lib/index.d.ts.map +1 -0
  8. package/lib/index.js +627 -0
  9. package/lib/index.js.map +1 -0
  10. package/lib/memory/batch.d.ts +44 -0
  11. package/lib/memory/batch.d.ts.map +1 -0
  12. package/lib/memory/batch.js +75 -0
  13. package/lib/memory/batch.js.map +1 -0
  14. package/lib/memory/feedback.d.ts +18 -0
  15. package/lib/memory/feedback.d.ts.map +1 -0
  16. package/lib/memory/feedback.js +29 -0
  17. package/lib/memory/feedback.js.map +1 -0
  18. package/lib/memory/store.d.ts +154 -0
  19. package/lib/memory/store.d.ts.map +1 -0
  20. package/lib/memory/store.js +606 -0
  21. package/lib/memory/store.js.map +1 -0
  22. package/lib/migration/cli.d.ts +18 -0
  23. package/lib/migration/cli.d.ts.map +1 -0
  24. package/lib/migration/cli.js +143 -0
  25. package/lib/migration/cli.js.map +1 -0
  26. package/lib/migration/fixture.d.ts +45 -0
  27. package/lib/migration/fixture.d.ts.map +1 -0
  28. package/lib/migration/fixture.js +177 -0
  29. package/lib/migration/fixture.js.map +1 -0
  30. package/lib/migration/service.d.ts +75 -0
  31. package/lib/migration/service.d.ts.map +1 -0
  32. package/lib/migration/service.js +713 -0
  33. package/lib/migration/service.js.map +1 -0
  34. package/lib/prompt/snapshot.d.ts +26 -0
  35. package/lib/prompt/snapshot.d.ts.map +1 -0
  36. package/lib/prompt/snapshot.js +74 -0
  37. package/lib/prompt/snapshot.js.map +1 -0
  38. package/lib/review/queue.d.ts +72 -0
  39. package/lib/review/queue.d.ts.map +1 -0
  40. package/lib/review/queue.js +306 -0
  41. package/lib/review/queue.js.map +1 -0
  42. package/lib/skills-browser.d.ts +67 -0
  43. package/lib/skills-browser.d.ts.map +1 -0
  44. package/lib/skills-browser.js +185 -0
  45. package/lib/skills-browser.js.map +1 -0
  46. package/lib/storage/atomic-store.d.ts +211 -0
  47. package/lib/storage/atomic-store.d.ts.map +1 -0
  48. package/lib/storage/atomic-store.js +582 -0
  49. package/lib/storage/atomic-store.js.map +1 -0
  50. package/lib/storage/layout.d.ts +81 -0
  51. package/lib/storage/layout.d.ts.map +1 -0
  52. package/lib/storage/layout.js +162 -0
  53. package/lib/storage/layout.js.map +1 -0
  54. package/lib/storage/legacy-format.d.ts +104 -0
  55. package/lib/storage/legacy-format.d.ts.map +1 -0
  56. package/lib/storage/legacy-format.js +262 -0
  57. package/lib/storage/legacy-format.js.map +1 -0
  58. package/lib/sync/config.d.ts +22 -0
  59. package/lib/sync/config.d.ts.map +1 -0
  60. package/lib/sync/config.js +57 -0
  61. package/lib/sync/config.js.map +1 -0
  62. package/lib/sync/git.d.ts +97 -0
  63. package/lib/sync/git.d.ts.map +1 -0
  64. package/lib/sync/git.js +282 -0
  65. package/lib/sync/git.js.map +1 -0
  66. package/lib/sync/index.d.ts +6 -0
  67. package/lib/sync/index.d.ts.map +1 -0
  68. package/lib/sync/index.js +6 -0
  69. package/lib/sync/index.js.map +1 -0
  70. package/lib/sync/layout.d.ts +15 -0
  71. package/lib/sync/layout.d.ts.map +1 -0
  72. package/lib/sync/layout.js +33 -0
  73. package/lib/sync/layout.js.map +1 -0
  74. package/lib/sync/merge.d.ts +42 -0
  75. package/lib/sync/merge.d.ts.map +1 -0
  76. package/lib/sync/merge.js +159 -0
  77. package/lib/sync/merge.js.map +1 -0
  78. package/lib/sync/service.d.ts +70 -0
  79. package/lib/sync/service.d.ts.map +1 -0
  80. package/lib/sync/service.js +508 -0
  81. package/lib/sync/service.js.map +1 -0
  82. package/lib/todo/store.d.ts +142 -0
  83. package/lib/todo/store.d.ts.map +1 -0
  84. package/lib/todo/store.js +452 -0
  85. package/lib/todo/store.js.map +1 -0
  86. package/lib/types/client/index.d.ts +7 -0
  87. package/lib/types/client/index.d.ts.map +1 -0
  88. package/package.json +63 -0
  89. package/src/client/index.tsx +994 -0
  90. package/src/host/index.ts +591 -0
  91. package/src/host/memory/batch.ts +107 -0
  92. package/src/host/memory/feedback.ts +36 -0
  93. package/src/host/memory/store.ts +650 -0
  94. package/src/host/migration/cli.ts +139 -0
  95. package/src/host/migration/fixture.ts +218 -0
  96. package/src/host/migration/service.ts +786 -0
  97. package/src/host/prompt/snapshot.ts +95 -0
  98. package/src/host/review/queue.ts +325 -0
  99. package/src/host/skills-browser.ts +191 -0
  100. package/src/host/storage/atomic-store.ts +605 -0
  101. package/src/host/storage/layout.ts +191 -0
  102. package/src/host/storage/legacy-format.ts +308 -0
  103. package/src/host/sync/config.ts +67 -0
  104. package/src/host/sync/git.ts +280 -0
  105. package/src/host/sync/index.ts +5 -0
  106. package/src/host/sync/layout.ts +39 -0
  107. package/src/host/sync/merge.ts +188 -0
  108. package/src/host/sync/service.ts +494 -0
  109. package/src/host/todo/store.ts +480 -0
@@ -0,0 +1,713 @@
1
+ /**
2
+ * migration/service.ts — inspect/dryRun/run/verify, backup manifest, journal, write-block on mismatch
3
+ *
4
+ * Implements M3-PR-B migration engine: in-place adoption, byte-preserving backup,
5
+ * SHA-256 verified, parser inventory, malformed JSONL tolerant, missing optional files tolerant,
6
+ * noncanonical/lock warnings, and write-block on verify mismatch.
7
+ */
8
+ import { createHash } from 'node:crypto';
9
+ import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, copyFileSync, rmSync, } from 'node:fs';
10
+ import { join, relative, dirname } from 'node:path';
11
+ import { resolveMemoryRoot, maestroMetaDir, schemaPath, journalPath, backupsDir, backupManifestPath, backupFilesDirPath, } from "../storage/layout.js";
12
+ import { parseEntries, isCanonical } from "../storage/legacy-format.js";
13
+ import { parseTodoEntry } from "../storage/legacy-format.js";
14
+ function sha256Hex(buf) {
15
+ return createHash('sha256').update(buf).digest('hex');
16
+ }
17
+ function readBytesAndHash(filePath) {
18
+ const raw = readFileSync(filePath);
19
+ return { bytes: raw.length, sha256: sha256Hex(raw), raw };
20
+ }
21
+ function listDailyFiles(root) {
22
+ const dailyRoot = join(root, 'daily');
23
+ try {
24
+ const names = readdirSync(dailyRoot);
25
+ return names.filter((n) => n.endsWith('.md') || n.endsWith('.todo.md')).map((n) => join(dailyRoot, n));
26
+ }
27
+ catch {
28
+ return [];
29
+ }
30
+ }
31
+ function listProjectFiles(root) {
32
+ const projRoot = join(root, 'projects');
33
+ try {
34
+ const hashes = readdirSync(projRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
35
+ const out = [];
36
+ for (const h of hashes) {
37
+ const dir = join(projRoot, h);
38
+ for (const name of ['MEMORY.md', 'KEY.md', 'KEY-archive.md', 'TODOS.md']) {
39
+ const p = join(dir, name);
40
+ out.push(p); // push even if missing; caller will check exists
41
+ }
42
+ }
43
+ return out;
44
+ }
45
+ catch {
46
+ return [];
47
+ }
48
+ }
49
+ function collectExpectedPaths(root) {
50
+ const expected = [
51
+ join(root, 'MEMORY.md'),
52
+ join(root, 'USER.md'),
53
+ join(root, 'MEMORY-archive.md'),
54
+ join(root, 'USER-archive.md'),
55
+ join(root, 'SUGGESTIONS.jsonl'),
56
+ join(root, 'TODOS-life.md'),
57
+ join(root, 'TODOS-work.md'),
58
+ join(root, 'TODO-archive.md'),
59
+ ];
60
+ expected.push(...listDailyFiles(root));
61
+ expected.push(...listProjectFiles(root));
62
+ // also include any daily files that may exist but we already added; ensure uniqueness
63
+ // Also include any extra files under root for backup completeness: scan all files recursively excluding .maestro-memory
64
+ // But for inspect we want to list expected + known daily/project; extra files will be discovered via full scan for backup
65
+ return expected;
66
+ }
67
+ function scanAllForBackup(root) {
68
+ const out = [];
69
+ function walk(dir) {
70
+ let entries;
71
+ try {
72
+ entries = readdirSync(dir, { withFileTypes: true });
73
+ }
74
+ catch {
75
+ return;
76
+ }
77
+ for (const e of entries) {
78
+ const p = join(dir, e.name);
79
+ if (p.includes('.maestro-memory'))
80
+ continue;
81
+ if (e.isDirectory()) {
82
+ // skip .maestro-memory at any depth
83
+ if (e.name === '.maestro-memory')
84
+ continue;
85
+ walk(p);
86
+ }
87
+ else if (e.isFile()) {
88
+ out.push(p);
89
+ }
90
+ }
91
+ }
92
+ walk(root);
93
+ return out;
94
+ }
95
+ function parseMemoryEntriesSafe(text) {
96
+ const entries = parseEntries(text);
97
+ const canon = text.trim() === '' || isCanonical(text);
98
+ return { entries, isCanonical: canon };
99
+ }
100
+ function parseTodoFileSafe(filePath) {
101
+ let raw;
102
+ try {
103
+ raw = readFileSync(filePath, 'utf8');
104
+ }
105
+ catch {
106
+ return { count: 0, ids: [], valid: 0, malformed: 0 };
107
+ }
108
+ // strip header comment
109
+ const body = raw.replace(/^<!--[\s\S]*?-->\s*/, '').replace(/^\s*§\s*\n?/, '').trim();
110
+ if (body === '')
111
+ return { count: 0, ids: [], valid: 0, malformed: 0 };
112
+ const parts = body.split('\n§\n').map((s) => s.trim()).filter(Boolean);
113
+ const ids = [];
114
+ let valid = 0;
115
+ for (const p of parts) {
116
+ const parsed = parseTodoEntry(p);
117
+ if (parsed && parsed.id) {
118
+ valid += 1;
119
+ ids.push(parsed.id);
120
+ }
121
+ }
122
+ return { count: parts.length, ids, valid, malformed: parts.length - valid };
123
+ }
124
+ function parseQueueSafe(filePath) {
125
+ try {
126
+ const text = readFileSync(filePath, 'utf8');
127
+ const lines = text.split('\n');
128
+ let valid = 0;
129
+ let malformed = 0;
130
+ const entries = [];
131
+ for (const line of lines) {
132
+ if (line.trim() === '')
133
+ continue;
134
+ try {
135
+ const obj = JSON.parse(line);
136
+ if (obj && typeof obj.target === 'string' && typeof obj.content === 'string') {
137
+ valid += 1;
138
+ entries.push(obj);
139
+ }
140
+ else {
141
+ malformed += 1;
142
+ }
143
+ }
144
+ catch {
145
+ malformed += 1;
146
+ }
147
+ }
148
+ return { valid, malformed, entries };
149
+ }
150
+ catch (e) {
151
+ if (e?.code === 'ENOENT')
152
+ return { valid: 0, malformed: 0, entries: [] };
153
+ throw e;
154
+ }
155
+ }
156
+ function kindFor(filePath, root) {
157
+ const rel = relative(root, filePath);
158
+ if (rel === 'MEMORY.md' || rel === 'USER.md')
159
+ return 'memory';
160
+ if (rel.endsWith('-archive.md'))
161
+ return 'archive';
162
+ if (rel.endsWith('SUGGESTIONS.jsonl'))
163
+ return 'queue';
164
+ if (rel.startsWith('daily/') && rel.endsWith('.todo.md'))
165
+ return 'todo-daily';
166
+ if (rel.startsWith('daily/'))
167
+ return 'daily';
168
+ if (rel.includes('projects/') && rel.endsWith('TODOS.md'))
169
+ return 'todo-project';
170
+ if (rel.includes('projects/') && rel.endsWith('KEY.md'))
171
+ return 'key';
172
+ if (rel.includes('projects/') && rel.endsWith('KEY-archive.md'))
173
+ return 'archive';
174
+ if (rel.includes('projects/') && rel.endsWith('MEMORY.md'))
175
+ return 'project';
176
+ if (rel.startsWith('TODOS-') || rel === 'TODOS-life.md' || rel === 'TODOS-work.md')
177
+ return 'todo';
178
+ if (rel === 'TODO-archive.md')
179
+ return 'archive';
180
+ return 'other';
181
+ }
182
+ export async function inspect(memoryDir = null) {
183
+ const root = resolveMemoryRoot(memoryDir);
184
+ const warnings = [];
185
+ const errors = [];
186
+ const files = [];
187
+ const expected = collectExpectedPaths(root);
188
+ // Use set to deduplicate expected
189
+ const seen = new Set();
190
+ const expectedUnique = expected.filter((p) => {
191
+ if (seen.has(p))
192
+ return false;
193
+ seen.add(p);
194
+ return true;
195
+ });
196
+ let totalMemoryEntries = 0;
197
+ let allTodoIds = [];
198
+ let totalTodoCount = 0;
199
+ let queueValid = 0;
200
+ let queueMalformed = 0;
201
+ let dailyFiles = 0;
202
+ let projectDirs = 0;
203
+ // Count project dirs
204
+ try {
205
+ const projRoot = join(root, 'projects');
206
+ const hashes = readdirSync(projRoot, { withFileTypes: true }).filter((d) => d.isDirectory());
207
+ projectDirs = hashes.length;
208
+ }
209
+ catch {
210
+ projectDirs = 0;
211
+ }
212
+ try {
213
+ dailyFiles = readdirSync(join(root, 'daily')).filter((n) => n.endsWith('.md')).length;
214
+ }
215
+ catch {
216
+ dailyFiles = 0;
217
+ }
218
+ for (const abs of expectedUnique) {
219
+ const rel = relative(root, abs);
220
+ const exists = existsSync(abs);
221
+ if (!exists) {
222
+ files.push({
223
+ path: abs,
224
+ relative: rel,
225
+ exists: false,
226
+ bytes: 0,
227
+ sha256: null,
228
+ kind: kindFor(abs, root),
229
+ warning: null,
230
+ });
231
+ continue;
232
+ }
233
+ const { bytes, sha256, raw } = readBytesAndHash(abs);
234
+ const kind = kindFor(abs, root);
235
+ let entriesCount;
236
+ let warning = null;
237
+ let malformedLines;
238
+ let valid;
239
+ let ids;
240
+ if (kind === 'memory' || kind === 'project' || kind === 'key' || kind === 'daily' || kind === 'archive') {
241
+ const text = raw.toString('utf8');
242
+ const { entries, isCanonical: canon } = parseMemoryEntriesSafe(text);
243
+ entriesCount = entries.length;
244
+ totalMemoryEntries += entriesCount;
245
+ if (!canon && text.trim() !== '') {
246
+ warning = `non-canonical content in ${rel} (drift)`;
247
+ warnings.push(`non-canonical: ${rel}`);
248
+ }
249
+ }
250
+ else if (kind === 'todo' || kind === 'todo-project' || kind === 'todo-daily') {
251
+ const info = parseTodoFileSafe(abs);
252
+ entriesCount = info.count;
253
+ ids = info.ids;
254
+ allTodoIds.push(...info.ids);
255
+ totalTodoCount += info.count;
256
+ if (info.malformed > 0) {
257
+ warning = `malformed todo entries in ${rel}: ${info.malformed}`;
258
+ warnings.push(`malformed todo: ${rel} (${info.malformed})`);
259
+ }
260
+ }
261
+ else if (kind === 'queue') {
262
+ const q = parseQueueSafe(abs);
263
+ queueValid = q.valid;
264
+ queueMalformed = q.malformed;
265
+ entriesCount = q.valid + q.malformed;
266
+ malformedLines = q.malformed;
267
+ valid = q.valid;
268
+ if (q.malformed > 0) {
269
+ warning = `malformed JSONL in ${rel}: ${q.malformed} lines`;
270
+ warnings.push(`malformed JSONL: ${rel} (${q.malformed} malformed, ${q.valid} valid)`);
271
+ }
272
+ }
273
+ files.push({
274
+ path: abs,
275
+ relative: rel,
276
+ exists: true,
277
+ bytes,
278
+ sha256,
279
+ kind,
280
+ entries: entriesCount,
281
+ ids,
282
+ malformedLines,
283
+ valid,
284
+ warning,
285
+ });
286
+ }
287
+ // Also detect lock files
288
+ const lockPaths = [];
289
+ function findLocks(dir) {
290
+ try {
291
+ const entries = readdirSync(dir, { withFileTypes: true });
292
+ for (const e of entries) {
293
+ const p = join(dir, e.name);
294
+ if (e.isDirectory()) {
295
+ if (e.name === '.maestro-memory')
296
+ continue;
297
+ findLocks(p);
298
+ }
299
+ else if (e.name === '.maestro.lock') {
300
+ lockPaths.push(p);
301
+ }
302
+ }
303
+ }
304
+ catch { }
305
+ }
306
+ try {
307
+ findLocks(root);
308
+ }
309
+ catch { }
310
+ for (const lp of lockPaths) {
311
+ warnings.push(`lock file present: ${relative(root, lp)}`);
312
+ }
313
+ // For backup completeness, also scan all files not in expected (extra legacy files like advisor/)
314
+ const allFiles = scanAllForBackup(root);
315
+ const expectedSet = new Set(expectedUnique);
316
+ for (const abs of allFiles) {
317
+ if (expectedSet.has(abs))
318
+ continue;
319
+ const rel = relative(root, abs);
320
+ // already handled daily/project extra? but scan will catch additional files like advisor/
321
+ const { bytes, sha256 } = readBytesAndHash(abs);
322
+ files.push({
323
+ path: abs,
324
+ relative: rel,
325
+ exists: true,
326
+ bytes,
327
+ sha256,
328
+ kind: 'other',
329
+ warning: null,
330
+ });
331
+ }
332
+ // ok: inspect never blocks on missing optional or malformed; only errors would be unreadable?
333
+ const ok = errors.length === 0;
334
+ return {
335
+ ok,
336
+ root,
337
+ files,
338
+ inventory: {
339
+ memoryEntries: totalMemoryEntries,
340
+ todoIds: allTodoIds,
341
+ todoIdsCount: allTodoIds.length,
342
+ todoCount: totalTodoCount,
343
+ queueValid,
344
+ queueMalformed,
345
+ dailyFiles,
346
+ projectDirs,
347
+ },
348
+ warnings,
349
+ errors,
350
+ };
351
+ }
352
+ export async function dryRun(memoryDir = null) {
353
+ // dryRun is same as inspect but explicitly read-only; ensure no side effects
354
+ return inspect(memoryDir);
355
+ }
356
+ function utcRunId() {
357
+ const now = new Date();
358
+ const pad = (n, len = 2) => String(n).padStart(len, '0');
359
+ const yyyy = now.getUTCFullYear();
360
+ const mm = pad(now.getUTCMonth() + 1);
361
+ const dd = pad(now.getUTCDate());
362
+ const hh = pad(now.getUTCHours());
363
+ const mi = pad(now.getUTCMinutes());
364
+ const ss = pad(now.getUTCSeconds());
365
+ const ms = pad(now.getUTCMilliseconds(), 3);
366
+ return `${yyyy}${mm}${dd}T${hh}${mi}${ss}.${ms}Z`;
367
+ }
368
+ export async function run(memoryDir = null) {
369
+ const root = resolveMemoryRoot(memoryDir);
370
+ const insp = await inspect(root);
371
+ if (insp.errors.length > 0) {
372
+ return {
373
+ ok: false,
374
+ runId: '',
375
+ manifestPath: '',
376
+ backupFilesDir: '',
377
+ warnings: insp.warnings,
378
+ errors: insp.errors,
379
+ files: insp.files,
380
+ };
381
+ }
382
+ const runId = utcRunId();
383
+ const manifestPath = backupManifestPath(root, runId);
384
+ const backupFilesDir = backupFilesDirPath(root, runId);
385
+ mkdirSync(backupFilesDir, { recursive: true });
386
+ mkdirSync(dirname(manifestPath), { recursive: true });
387
+ // byte-preserving copy
388
+ const manifestFiles = [];
389
+ for (const f of insp.files) {
390
+ if (!f.exists) {
391
+ manifestFiles.push({
392
+ relative: f.relative,
393
+ path: f.path,
394
+ exists: false,
395
+ bytes: 0,
396
+ sha256: null,
397
+ kind: f.kind,
398
+ entries: f.entries ?? 0,
399
+ malformedLines: f.malformedLines,
400
+ });
401
+ continue;
402
+ }
403
+ const dest = join(backupFilesDir, f.relative);
404
+ mkdirSync(dirname(dest), { recursive: true });
405
+ // byte-preserving copy
406
+ copyFileSync(f.path, dest);
407
+ // verify backup matches
408
+ const backupRaw = readFileSync(dest);
409
+ const backupSha = sha256Hex(backupRaw);
410
+ if (backupSha !== f.sha256) {
411
+ throw new Error(`backup sha mismatch for ${f.relative}`);
412
+ }
413
+ manifestFiles.push({
414
+ relative: f.relative,
415
+ path: f.path,
416
+ exists: true,
417
+ bytes: f.bytes,
418
+ sha256: f.sha256,
419
+ kind: f.kind,
420
+ entries: f.entries,
421
+ ids: f.ids,
422
+ malformedLines: f.malformedLines,
423
+ valid: f.valid,
424
+ });
425
+ }
426
+ const manifest = {
427
+ version: 1,
428
+ runId,
429
+ createdAt: new Date().toISOString(),
430
+ root,
431
+ files: manifestFiles,
432
+ inventory: insp.inventory,
433
+ warnings: insp.warnings,
434
+ errors: insp.errors,
435
+ backupFilesDir,
436
+ };
437
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
438
+ // write schema.json (only after all required data parses — we already did)
439
+ const sPath = schemaPath(root);
440
+ mkdirSync(dirname(sPath), { recursive: true });
441
+ const schema = {
442
+ version: 1,
443
+ runId,
444
+ migratedAt: new Date().toISOString(),
445
+ root,
446
+ inventory: insp.inventory,
447
+ manifest: manifestPath,
448
+ };
449
+ writeFileSync(sPath, JSON.stringify(schema, null, 2), 'utf8');
450
+ // journal
451
+ const jPath = journalPath(root);
452
+ mkdirSync(dirname(jPath), { recursive: true });
453
+ const journalEntry = {
454
+ runId,
455
+ at: new Date().toISOString(),
456
+ action: 'run',
457
+ ok: true,
458
+ warnings: insp.warnings,
459
+ inventory: insp.inventory,
460
+ manifest: manifestPath,
461
+ };
462
+ appendFileSync(jPath, JSON.stringify(journalEntry) + '\n', 'utf8');
463
+ // clear any previous write-block on successful run
464
+ const blockPath = join(maestroMetaDir(root), 'write-block.json');
465
+ if (existsSync(blockPath)) {
466
+ try {
467
+ rmSync(blockPath, { force: true });
468
+ }
469
+ catch { }
470
+ }
471
+ return {
472
+ ok: true,
473
+ runId,
474
+ manifestPath,
475
+ backupFilesDir,
476
+ warnings: insp.warnings,
477
+ errors: [],
478
+ files: insp.files,
479
+ };
480
+ }
481
+ function latestRunId(root) {
482
+ const dir = backupsDir(root);
483
+ try {
484
+ const entries = readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
485
+ if (entries.length === 0)
486
+ return null;
487
+ return entries[entries.length - 1];
488
+ }
489
+ catch {
490
+ return null;
491
+ }
492
+ }
493
+ export async function verify(memoryDir = null, runId) {
494
+ const root = resolveMemoryRoot(memoryDir);
495
+ let targetRunId = runId ?? null;
496
+ if (!targetRunId) {
497
+ // try schema.json first
498
+ try {
499
+ const schema = JSON.parse(readFileSync(schemaPath(root), 'utf8'));
500
+ targetRunId = schema.runId ?? null;
501
+ }
502
+ catch { }
503
+ if (!targetRunId)
504
+ targetRunId = latestRunId(root);
505
+ }
506
+ if (!targetRunId) {
507
+ return { ok: false, runId: '', manifestPath: '', mismatches: ['no manifest found'], warnings: [] };
508
+ }
509
+ const manifestPath = backupManifestPath(root, targetRunId);
510
+ let manifest;
511
+ try {
512
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
513
+ }
514
+ catch (e) {
515
+ return { ok: false, runId: targetRunId, manifestPath, mismatches: [`manifest not found: ${e?.message}`], warnings: [] };
516
+ }
517
+ const mismatches = [];
518
+ const warnings = [];
519
+ // check each file in manifest
520
+ for (const f of manifest.files) {
521
+ if (!f.exists) {
522
+ // file was missing at backup time; should still be missing
523
+ const curExists = existsSync(f.path);
524
+ if (curExists) {
525
+ mismatches.push(`unexpected file appeared: ${f.relative}`);
526
+ }
527
+ continue;
528
+ }
529
+ const curExists = existsSync(f.path);
530
+ if (!curExists) {
531
+ mismatches.push(`missing file: ${f.relative} (expected sha ${f.sha256})`);
532
+ continue;
533
+ }
534
+ const cur = readFileSync(f.path);
535
+ const curSha = sha256Hex(cur);
536
+ if (curSha !== f.sha256) {
537
+ mismatches.push(`digest mismatch: ${f.relative} (expected ${f.sha256?.slice(0, 8)}, got ${curSha.slice(0, 8)}, bytes ${f.bytes} -> ${cur.length})`);
538
+ }
539
+ if (cur.length !== f.bytes) {
540
+ mismatches.push(`byte count mismatch: ${f.relative} (${f.bytes} -> ${cur.length})`);
541
+ }
542
+ }
543
+ // also compare inventory via re-parse (entry counts + todo IDs)
544
+ const currentInspect = await inspect(root);
545
+ const manifestInv = manifest.inventory;
546
+ if (manifestInv) {
547
+ if (currentInspect.inventory.memoryEntries !== manifestInv.memoryEntries) {
548
+ mismatches.push(`inventory mismatch: memoryEntries ${manifestInv.memoryEntries} -> ${currentInspect.inventory.memoryEntries}`);
549
+ }
550
+ if (currentInspect.inventory.todoIdsCount !== manifestInv.todoIdsCount) {
551
+ mismatches.push(`inventory mismatch: todoIds ${manifestInv.todoIdsCount} -> ${currentInspect.inventory.todoIdsCount}`);
552
+ }
553
+ if (currentInspect.inventory.queueValid !== manifestInv.queueValid) {
554
+ mismatches.push(`inventory mismatch: queueValid ${manifestInv.queueValid} -> ${currentInspect.inventory.queueValid}`);
555
+ }
556
+ // check todo IDs set equality if counts match but ids differ
557
+ if (manifestInv.todoIds && Array.isArray(manifestInv.todoIds)) {
558
+ const curIdsSorted = [...currentInspect.inventory.todoIds].sort();
559
+ const manIdsSorted = [...manifestInv.todoIds].sort();
560
+ if (curIdsSorted.join(',') !== manIdsSorted.join(',')) {
561
+ // only report if not already reported via count
562
+ if (currentInspect.inventory.todoIdsCount === manifestInv.todoIdsCount) {
563
+ mismatches.push(`todo ID set mismatch: ${manIdsSorted.join(',')} -> ${curIdsSorted.join(',')}`);
564
+ }
565
+ }
566
+ }
567
+ }
568
+ const ok = mismatches.length === 0;
569
+ const blockPath = join(maestroMetaDir(root), 'write-block.json');
570
+ if (!ok) {
571
+ // write-block on mismatch
572
+ mkdirSync(dirname(blockPath), { recursive: true });
573
+ const block = {
574
+ blocked: true,
575
+ runId: targetRunId,
576
+ at: new Date().toISOString(),
577
+ mismatches,
578
+ manifest: manifestPath,
579
+ };
580
+ writeFileSync(blockPath, JSON.stringify(block, null, 2), 'utf8');
581
+ // also append to journal
582
+ try {
583
+ const jPath = journalPath(root);
584
+ mkdirSync(dirname(jPath), { recursive: true });
585
+ appendFileSync(jPath, JSON.stringify({ runId: targetRunId, at: new Date().toISOString(), action: 'verify', ok: false, mismatches }) + '\n', 'utf8');
586
+ }
587
+ catch { }
588
+ }
589
+ else {
590
+ // on success, clear block if exists and log verify success
591
+ if (existsSync(blockPath)) {
592
+ try {
593
+ rmSync(blockPath, { force: true });
594
+ }
595
+ catch { }
596
+ }
597
+ try {
598
+ const jPath = journalPath(root);
599
+ mkdirSync(dirname(jPath), { recursive: true });
600
+ appendFileSync(jPath, JSON.stringify({ runId: targetRunId, at: new Date().toISOString(), action: 'verify', ok: true }) + '\n', 'utf8');
601
+ }
602
+ catch { }
603
+ }
604
+ return { ok, runId: targetRunId, manifestPath, mismatches, warnings };
605
+ }
606
+ export function isWriteBlocked(memoryDir = null) {
607
+ const root = resolveMemoryRoot(memoryDir);
608
+ const blockPath = join(maestroMetaDir(root), 'write-block.json');
609
+ if (!existsSync(blockPath))
610
+ return false;
611
+ try {
612
+ const data = JSON.parse(readFileSync(blockPath, 'utf8'));
613
+ return data.blocked === true;
614
+ }
615
+ catch {
616
+ return true;
617
+ }
618
+ }
619
+ export function clearWriteBlock(memoryDir = null) {
620
+ const root = resolveMemoryRoot(memoryDir);
621
+ const blockPath = join(maestroMetaDir(root), 'write-block.json');
622
+ if (existsSync(blockPath)) {
623
+ try {
624
+ rmSync(blockPath, { force: true });
625
+ }
626
+ catch { }
627
+ }
628
+ }
629
+ /**
630
+ * Restore files from a backup manifest byte-identical.
631
+ * Used for M4-PR-A rehearsal: exercise and test rollback against copied schema.
632
+ * - Restores each existing file from backupFilesDir/<relative> to its original path.
633
+ * - Removes files that appeared after backup if they were absent at backup time.
634
+ * - Clears write-block and appends journal entry on success.
635
+ */
636
+ export async function rollback(memoryDir = null, runId) {
637
+ const root = resolveMemoryRoot(memoryDir);
638
+ let targetRunId = runId ?? null;
639
+ if (!targetRunId) {
640
+ try {
641
+ const schema = JSON.parse(readFileSync(schemaPath(root), 'utf8'));
642
+ targetRunId = schema.runId ?? null;
643
+ }
644
+ catch { }
645
+ if (!targetRunId)
646
+ targetRunId = latestRunId(root);
647
+ }
648
+ if (!targetRunId) {
649
+ return { ok: false, runId: '', manifestPath: '', restored: 0, errors: ['no manifest found'] };
650
+ }
651
+ const manifestPath = backupManifestPath(root, targetRunId);
652
+ let manifest;
653
+ try {
654
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
655
+ }
656
+ catch (e) {
657
+ return { ok: false, runId: targetRunId, manifestPath, restored: 0, errors: [`manifest not found: ${e?.message}`] };
658
+ }
659
+ const backupFilesDir = manifest.backupFilesDir ?? backupFilesDirPath(root, targetRunId);
660
+ let restored = 0;
661
+ const errors = [];
662
+ for (const f of manifest.files) {
663
+ const dest = f.path;
664
+ const src = join(backupFilesDir, f.relative);
665
+ try {
666
+ if (!f.exists) {
667
+ // file was absent at backup time; remove if it appeared
668
+ if (existsSync(dest)) {
669
+ rmSync(dest, { force: true });
670
+ restored += 1;
671
+ }
672
+ continue;
673
+ }
674
+ // file existed; restore byte-identical from backup
675
+ if (!existsSync(src)) {
676
+ errors.push(`backup missing for ${f.relative}`);
677
+ continue;
678
+ }
679
+ mkdirSync(dirname(dest), { recursive: true });
680
+ copyFileSync(src, dest);
681
+ // verify restored sha matches manifest
682
+ const restoredRaw = readFileSync(dest);
683
+ const restoredSha = sha256Hex(restoredRaw);
684
+ if (restoredSha !== f.sha256) {
685
+ errors.push(`restore sha mismatch for ${f.relative} (expected ${f.sha256?.slice(0, 8)}, got ${restoredSha.slice(0, 8)})`);
686
+ }
687
+ else {
688
+ restored += 1;
689
+ }
690
+ }
691
+ catch (e) {
692
+ errors.push(`restore failed for ${f.relative}: ${e?.message ?? String(e)}`);
693
+ }
694
+ }
695
+ // clear write-block if any
696
+ const blockPath = join(maestroMetaDir(root), 'write-block.json');
697
+ if (existsSync(blockPath)) {
698
+ try {
699
+ rmSync(blockPath, { force: true });
700
+ }
701
+ catch { }
702
+ }
703
+ // journal
704
+ try {
705
+ const jPath = journalPath(root);
706
+ mkdirSync(dirname(jPath), { recursive: true });
707
+ appendFileSync(jPath, JSON.stringify({ runId: targetRunId, at: new Date().toISOString(), action: 'rollback', ok: errors.length === 0, restored, errors }) +
708
+ '\n', 'utf8');
709
+ }
710
+ catch { }
711
+ return { ok: errors.length === 0, runId: targetRunId, manifestPath, restored, errors };
712
+ }
713
+ //# sourceMappingURL=service.js.map