openplanr 1.13.2 → 1.14.0

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 (159) hide show
  1. package/README.md +46 -4
  2. package/dist/cli/commands/artifact.d.ts.map +1 -1
  3. package/dist/cli/commands/artifact.js +17 -6
  4. package/dist/cli/commands/artifact.js.map +1 -1
  5. package/dist/cli/commands/operate.d.ts +3 -0
  6. package/dist/cli/commands/operate.d.ts.map +1 -0
  7. package/dist/cli/commands/operate.js +568 -0
  8. package/dist/cli/commands/operate.js.map +1 -0
  9. package/dist/cli/index.js +42 -2
  10. package/dist/cli/index.js.map +1 -1
  11. package/dist/services/ai-service.d.ts +9 -2
  12. package/dist/services/ai-service.d.ts.map +1 -1
  13. package/dist/services/ai-service.js +14 -6
  14. package/dist/services/ai-service.js.map +1 -1
  15. package/dist/services/artifact-pipeline-service.d.ts +1 -0
  16. package/dist/services/artifact-pipeline-service.d.ts.map +1 -1
  17. package/dist/services/artifact-pipeline-service.js.map +1 -1
  18. package/dist/services/delta-apply-service.js +1 -1
  19. package/dist/services/delta-apply-service.js.map +1 -1
  20. package/dist/services/operate/advisors.d.ts +219 -0
  21. package/dist/services/operate/advisors.d.ts.map +1 -0
  22. package/dist/services/operate/advisors.js +745 -0
  23. package/dist/services/operate/advisors.js.map +1 -0
  24. package/dist/services/operate/artifact-route-generation.d.ts +111 -0
  25. package/dist/services/operate/artifact-route-generation.d.ts.map +1 -0
  26. package/dist/services/operate/artifact-route-generation.js +459 -0
  27. package/dist/services/operate/artifact-route-generation.js.map +1 -0
  28. package/dist/services/operate/artifacts.d.ts +26 -0
  29. package/dist/services/operate/artifacts.d.ts.map +1 -0
  30. package/dist/services/operate/artifacts.js +92 -0
  31. package/dist/services/operate/artifacts.js.map +1 -0
  32. package/dist/services/operate/canonical.d.ts +18 -0
  33. package/dist/services/operate/canonical.d.ts.map +1 -0
  34. package/dist/services/operate/canonical.js +157 -0
  35. package/dist/services/operate/canonical.js.map +1 -0
  36. package/dist/services/operate/config.d.ts +56 -0
  37. package/dist/services/operate/config.d.ts.map +1 -0
  38. package/dist/services/operate/config.js +644 -0
  39. package/dist/services/operate/config.js.map +1 -0
  40. package/dist/services/operate/consolidation.d.ts +18 -0
  41. package/dist/services/operate/consolidation.d.ts.map +1 -0
  42. package/dist/services/operate/consolidation.js +539 -0
  43. package/dist/services/operate/consolidation.js.map +1 -0
  44. package/dist/services/operate/doctor.d.ts +12 -0
  45. package/dist/services/operate/doctor.d.ts.map +1 -0
  46. package/dist/services/operate/doctor.js +307 -0
  47. package/dist/services/operate/doctor.js.map +1 -0
  48. package/dist/services/operate/engine.d.ts +53 -0
  49. package/dist/services/operate/engine.d.ts.map +1 -0
  50. package/dist/services/operate/engine.js +1097 -0
  51. package/dist/services/operate/engine.js.map +1 -0
  52. package/dist/services/operate/event-store.d.ts +57 -0
  53. package/dist/services/operate/event-store.d.ts.map +1 -0
  54. package/dist/services/operate/event-store.js +232 -0
  55. package/dist/services/operate/event-store.js.map +1 -0
  56. package/dist/services/operate/evidence-cache.d.ts +17 -0
  57. package/dist/services/operate/evidence-cache.d.ts.map +1 -0
  58. package/dist/services/operate/evidence-cache.js +96 -0
  59. package/dist/services/operate/evidence-cache.js.map +1 -0
  60. package/dist/services/operate/evidence-import.d.ts +40 -0
  61. package/dist/services/operate/evidence-import.d.ts.map +1 -0
  62. package/dist/services/operate/evidence-import.js +340 -0
  63. package/dist/services/operate/evidence-import.js.map +1 -0
  64. package/dist/services/operate/evidence-readiness.d.ts +8 -0
  65. package/dist/services/operate/evidence-readiness.d.ts.map +1 -0
  66. package/dist/services/operate/evidence-readiness.js +78 -0
  67. package/dist/services/operate/evidence-readiness.js.map +1 -0
  68. package/dist/services/operate/evidence.d.ts +71 -0
  69. package/dist/services/operate/evidence.d.ts.map +1 -0
  70. package/dist/services/operate/evidence.js +1022 -0
  71. package/dist/services/operate/evidence.js.map +1 -0
  72. package/dist/services/operate/ids.d.ts +7 -0
  73. package/dist/services/operate/ids.d.ts.map +1 -0
  74. package/dist/services/operate/ids.js +23 -0
  75. package/dist/services/operate/ids.js.map +1 -0
  76. package/dist/services/operate/index.d.ts +14 -0
  77. package/dist/services/operate/index.d.ts.map +1 -0
  78. package/dist/services/operate/index.js +881 -0
  79. package/dist/services/operate/index.js.map +1 -0
  80. package/dist/services/operate/journal.d.ts +34 -0
  81. package/dist/services/operate/journal.d.ts.map +1 -0
  82. package/dist/services/operate/journal.js +229 -0
  83. package/dist/services/operate/journal.js.map +1 -0
  84. package/dist/services/operate/legacy-import-service.d.ts +58 -0
  85. package/dist/services/operate/legacy-import-service.d.ts.map +1 -0
  86. package/dist/services/operate/legacy-import-service.js +922 -0
  87. package/dist/services/operate/legacy-import-service.js.map +1 -0
  88. package/dist/services/operate/lifecycle.d.ts +88 -0
  89. package/dist/services/operate/lifecycle.d.ts.map +1 -0
  90. package/dist/services/operate/lifecycle.js +556 -0
  91. package/dist/services/operate/lifecycle.js.map +1 -0
  92. package/dist/services/operate/lock-service.d.ts +37 -0
  93. package/dist/services/operate/lock-service.d.ts.map +1 -0
  94. package/dist/services/operate/lock-service.js +208 -0
  95. package/dist/services/operate/lock-service.js.map +1 -0
  96. package/dist/services/operate/maintenance.d.ts +38 -0
  97. package/dist/services/operate/maintenance.d.ts.map +1 -0
  98. package/dist/services/operate/maintenance.js +699 -0
  99. package/dist/services/operate/maintenance.js.map +1 -0
  100. package/dist/services/operate/outcomes.d.ts +48 -0
  101. package/dist/services/operate/outcomes.d.ts.map +1 -0
  102. package/dist/services/operate/outcomes.js +312 -0
  103. package/dist/services/operate/outcomes.js.map +1 -0
  104. package/dist/services/operate/pipeline-handoff.d.ts +138 -0
  105. package/dist/services/operate/pipeline-handoff.d.ts.map +1 -0
  106. package/dist/services/operate/pipeline-handoff.js +175 -0
  107. package/dist/services/operate/pipeline-handoff.js.map +1 -0
  108. package/dist/services/operate/projection-persistence.d.ts +55 -0
  109. package/dist/services/operate/projection-persistence.d.ts.map +1 -0
  110. package/dist/services/operate/projection-persistence.js +393 -0
  111. package/dist/services/operate/projection-persistence.js.map +1 -0
  112. package/dist/services/operate/projection.d.ts +4 -0
  113. package/dist/services/operate/projection.d.ts.map +1 -0
  114. package/dist/services/operate/projection.js +128 -0
  115. package/dist/services/operate/projection.js.map +1 -0
  116. package/dist/services/operate/protocol.d.ts +60 -0
  117. package/dist/services/operate/protocol.d.ts.map +1 -0
  118. package/dist/services/operate/protocol.js +62 -0
  119. package/dist/services/operate/protocol.js.map +1 -0
  120. package/dist/services/operate/read-only-providers.d.ts +38 -0
  121. package/dist/services/operate/read-only-providers.d.ts.map +1 -0
  122. package/dist/services/operate/read-only-providers.js +286 -0
  123. package/dist/services/operate/read-only-providers.js.map +1 -0
  124. package/dist/services/operate/redaction.d.ts +47 -0
  125. package/dist/services/operate/redaction.d.ts.map +1 -0
  126. package/dist/services/operate/redaction.js +279 -0
  127. package/dist/services/operate/redaction.js.map +1 -0
  128. package/dist/services/operate/reducer.d.ts +10 -0
  129. package/dist/services/operate/reducer.d.ts.map +1 -0
  130. package/dist/services/operate/reducer.js +11 -0
  131. package/dist/services/operate/reducer.js.map +1 -0
  132. package/dist/services/operate/routes.d.ts +49 -0
  133. package/dist/services/operate/routes.d.ts.map +1 -0
  134. package/dist/services/operate/routes.js +996 -0
  135. package/dist/services/operate/routes.js.map +1 -0
  136. package/dist/services/operate/shipment-observer.d.ts +14 -0
  137. package/dist/services/operate/shipment-observer.d.ts.map +1 -0
  138. package/dist/services/operate/shipment-observer.js +321 -0
  139. package/dist/services/operate/shipment-observer.js.map +1 -0
  140. package/dist/services/operate/stalled-item-service.d.ts +18 -0
  141. package/dist/services/operate/stalled-item-service.d.ts.map +1 -0
  142. package/dist/services/operate/stalled-item-service.js +121 -0
  143. package/dist/services/operate/stalled-item-service.js.map +1 -0
  144. package/dist/services/operate/subprocess-env.d.ts +4 -0
  145. package/dist/services/operate/subprocess-env.d.ts.map +1 -0
  146. package/dist/services/operate/subprocess-env.js +45 -0
  147. package/dist/services/operate/subprocess-env.js.map +1 -0
  148. package/dist/services/operate/types.d.ts +729 -0
  149. package/dist/services/operate/types.d.ts.map +1 -0
  150. package/dist/services/operate/types.js +13 -0
  151. package/dist/services/operate/types.js.map +1 -0
  152. package/dist/services/operate/workspace.d.ts +59 -0
  153. package/dist/services/operate/workspace.d.ts.map +1 -0
  154. package/dist/services/operate/workspace.js +316 -0
  155. package/dist/services/operate/workspace.js.map +1 -0
  156. package/dist/services/runtime-manager-service.d.ts.map +1 -1
  157. package/dist/services/runtime-manager-service.js +71 -2
  158. package/dist/services/runtime-manager-service.js.map +1 -1
  159. package/package.json +10 -4
@@ -0,0 +1,922 @@
1
+ import { lstat, mkdir, open, readdir, readFile, realpath } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { canonicalDigest, canonicalize, sha256Digest } from './canonical.js';
4
+ import { operatingProjectKey } from './config.js';
5
+ import { OperatingEventStore } from './event-store.js';
6
+ import { parseStrictJson } from './evidence-import.js';
7
+ import { applyJournalTransaction, prepareJournalTransaction } from './journal.js';
8
+ import { withOperatingLock } from './lock-service.js';
9
+ import { persistOperatingProjections } from './projection-persistence.js';
10
+ import { assertOperatingArtifact } from './protocol.js';
11
+ import { containsSecret } from './redaction.js';
12
+ import { OPERATE_PROTOCOL_VERSION, OPERATE_SCHEMA_VERSION, OperateError, } from './types.js';
13
+ import { isPathInside, resolveContainedPath, resolveOperatingPaths } from './workspace.js';
14
+ const LEGACY_ROOT = '.planr/board';
15
+ const MAX_FILES = 1_000;
16
+ const MAX_BYTES = 10 * 1024 * 1024;
17
+ const UNKNOWN_CREATED_AT = '1970-01-01T00:00:00.000Z';
18
+ const IMPORT_EVENT_TYPE = 'migration.legacy-imported';
19
+ const IMPORT_ACTOR = { kind: 'migration', id: 'openplanr-operate' };
20
+ const SUPPORTED_EVENT_SOURCE_PATH = /^\.planr\/board(?:\/[A-Za-z0-9._-]+)*$/;
21
+ const STRUCTURED_JSON_NAMES = new Set([
22
+ 'board.json',
23
+ 'register.json',
24
+ 'findings.json',
25
+ 'decisions.json',
26
+ 'gaps.json',
27
+ 'routes.json',
28
+ 'outcomes.json',
29
+ 'backlog.json',
30
+ ]);
31
+ function slashPath(value) {
32
+ return value.split(path.sep).join('/');
33
+ }
34
+ function emptyInspection() {
35
+ return {
36
+ record: null,
37
+ sourcePath: null,
38
+ sourceDigest: null,
39
+ backupManifest: null,
40
+ files: [],
41
+ rows: [],
42
+ candidates: [],
43
+ counts: {
44
+ files: 0,
45
+ bytes: 0,
46
+ importable: 0,
47
+ alreadyImported: 0,
48
+ duplicates: 0,
49
+ conflicts: 0,
50
+ },
51
+ };
52
+ }
53
+ function decodeUtf8(bytes, sourcePath) {
54
+ try {
55
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
56
+ }
57
+ catch {
58
+ throw new OperateError('E_OPERATE_MIGRATION_CONFLICT', `Legacy board file is not valid UTF-8: ${sourcePath}`);
59
+ }
60
+ }
61
+ async function readLegacyFiles(projectRoot) {
62
+ const root = await resolveContainedPath(projectRoot, LEGACY_ROOT);
63
+ const rootInfo = await lstat(root).catch((error) => {
64
+ if (error.code === 'ENOENT')
65
+ return null;
66
+ throw error;
67
+ });
68
+ if (!rootInfo)
69
+ return [];
70
+ if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory()) {
71
+ throw new OperateError('E_OPERATE_MIGRATION_CONFLICT', `${LEGACY_ROOT} must be a real directory, not a file or symlink.`);
72
+ }
73
+ const canonicalProject = await realpath(projectRoot);
74
+ const canonicalRoot = await realpath(root);
75
+ if (!isPathInside(canonicalProject, canonicalRoot)) {
76
+ throw new OperateError('E_OPERATE_PATH_ESCAPE', `${LEGACY_ROOT} resolves outside the project.`);
77
+ }
78
+ const files = [];
79
+ let totalBytes = 0;
80
+ async function visit(directory) {
81
+ const entries = await readdir(directory, { withFileTypes: true });
82
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
83
+ const target = path.join(directory, entry.name);
84
+ const relative = slashPath(path.relative(canonicalProject, target));
85
+ if (containsSecret(entry.name)) {
86
+ throw new OperateError('E_OPERATE_SECRET_DETECTED', 'Legacy board contains a secret-shaped file name; rename or remove it before migration.');
87
+ }
88
+ if (entry.isSymbolicLink()) {
89
+ throw new OperateError('E_OPERATE_MIGRATION_CONFLICT', `Legacy board contains a symlink: ${relative}`);
90
+ }
91
+ if (entry.isDirectory()) {
92
+ await visit(target);
93
+ continue;
94
+ }
95
+ if (!entry.isFile()) {
96
+ throw new OperateError('E_OPERATE_MIGRATION_CONFLICT', `Legacy board contains a non-file entry: ${relative}`);
97
+ }
98
+ if (files.length >= MAX_FILES) {
99
+ throw new OperateError('E_OPERATE_INPUT_TOO_LARGE', `Legacy board exceeds the ${MAX_FILES}-file limit.`);
100
+ }
101
+ const bytes = await readFile(target);
102
+ totalBytes += bytes.byteLength;
103
+ if (totalBytes > MAX_BYTES) {
104
+ throw new OperateError('E_OPERATE_INPUT_TOO_LARGE', `Legacy board exceeds the ${MAX_BYTES}-byte limit.`);
105
+ }
106
+ files.push({
107
+ absolutePath: target,
108
+ path: relative,
109
+ digest: sha256Digest(bytes),
110
+ bytes,
111
+ });
112
+ }
113
+ }
114
+ await visit(canonicalRoot);
115
+ return files.sort((left, right) => left.path.localeCompare(right.path));
116
+ }
117
+ function splitMarkdownTableRow(line) {
118
+ const value = line.trim().replace(/^\|/, '').replace(/\|$/, '');
119
+ const cells = [];
120
+ let current = '';
121
+ let escaped = false;
122
+ for (const character of value) {
123
+ if (escaped) {
124
+ current += character;
125
+ escaped = false;
126
+ }
127
+ else if (character === '\\') {
128
+ escaped = true;
129
+ }
130
+ else if (character === '|') {
131
+ cells.push(current.trim());
132
+ current = '';
133
+ }
134
+ else {
135
+ current += character;
136
+ }
137
+ }
138
+ if (escaped)
139
+ current += '\\';
140
+ cells.push(current.trim());
141
+ return cells;
142
+ }
143
+ function normalizedHeader(value) {
144
+ return value
145
+ .replace(/[*_`]/g, '')
146
+ .trim()
147
+ .toLowerCase()
148
+ .replace(/[^a-z0-9]+/g, '');
149
+ }
150
+ function legacyKindFromId(value) {
151
+ const id = value.trim().toUpperCase();
152
+ if (/^(?:F|FND)-/.test(id))
153
+ return 'finding';
154
+ if (/^(?:D|DEC)-/.test(id))
155
+ return 'decision';
156
+ if (/^GAP-/.test(id))
157
+ return 'gap';
158
+ if (/^(?:A|ACT)-/.test(id))
159
+ return 'route';
160
+ if (/^OUT-/.test(id))
161
+ return 'outcome';
162
+ return 'unknown';
163
+ }
164
+ function legacyKindFromHeaders(headers, sourcePath) {
165
+ if (headers.includes('question')) {
166
+ return sourcePath.includes('gap') ? 'gap' : 'decision';
167
+ }
168
+ if (headers.includes('lane') || headers.includes('title'))
169
+ return 'finding';
170
+ if (sourcePath.includes('decision'))
171
+ return 'decision';
172
+ if (sourcePath.includes('gap'))
173
+ return 'gap';
174
+ if (sourcePath.includes('route'))
175
+ return 'route';
176
+ if (sourcePath.includes('outcome'))
177
+ return 'outcome';
178
+ return 'unknown';
179
+ }
180
+ function boundedLegacyId(value) {
181
+ if (typeof value !== 'string')
182
+ return null;
183
+ const id = value.trim();
184
+ if (containsSecret(id))
185
+ return null;
186
+ return /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(id) ? id : null;
187
+ }
188
+ function candidate(input) {
189
+ const recordDigest = canonicalDigest(input.record);
190
+ const identity = `${input.kind}:${input.legacyId ?? recordDigest}`;
191
+ return {
192
+ sourceId: `${input.sourcePath}:${input.locator}`,
193
+ sourcePath: input.sourcePath,
194
+ sourceDigest: input.sourceDigest,
195
+ legacyKind: input.kind,
196
+ legacyId: input.legacyId,
197
+ recordDigest,
198
+ eventId: `legacy-import-${canonicalDigest(identity).slice('sha256:'.length)}`,
199
+ targetId: `legacy:${identity}`,
200
+ disposition: 'import',
201
+ legacyRecord: input.record,
202
+ };
203
+ }
204
+ function parseMarkdownCandidates(file) {
205
+ if (!file.path.endsWith('.md'))
206
+ return { candidates: [], conflicts: [] };
207
+ const source = decodeUtf8(file.bytes, file.path).replace(/\r\n?/g, '\n');
208
+ const lines = source.split('\n');
209
+ const candidates = [];
210
+ const conflicts = [];
211
+ const claimedLines = new Set();
212
+ for (let index = 0; index + 1 < lines.length; index += 1) {
213
+ if (!lines[index].includes('|') || !lines[index + 1].includes('-'))
214
+ continue;
215
+ const headers = splitMarkdownTableRow(lines[index]).map(normalizedHeader);
216
+ const separator = splitMarkdownTableRow(lines[index + 1]);
217
+ if (headers.length < 2 ||
218
+ separator.length !== headers.length ||
219
+ separator.some((cell) => !/^:?-{3,}:?$/.test(cell.trim()))) {
220
+ continue;
221
+ }
222
+ if (!headers.includes('id'))
223
+ continue;
224
+ const idIndex = headers.indexOf('id');
225
+ const kind = legacyKindFromHeaders(headers, file.path);
226
+ let rowIndex = index + 2;
227
+ while (rowIndex < lines.length && lines[rowIndex].includes('|')) {
228
+ const cells = splitMarkdownTableRow(lines[rowIndex]);
229
+ if (cells.length !== headers.length) {
230
+ conflicts.push(`${file.path}:${rowIndex + 1}:malformed-table-row`);
231
+ claimedLines.add(rowIndex);
232
+ rowIndex += 1;
233
+ continue;
234
+ }
235
+ const fields = Object.fromEntries(headers.map((header, cell) => [header, cells[cell]]));
236
+ const legacyId = boundedLegacyId(cells[idIndex]);
237
+ if (!legacyId) {
238
+ conflicts.push(`${file.path}:${rowIndex + 1}:missing-or-invalid-id`);
239
+ }
240
+ else {
241
+ candidates.push(candidate({
242
+ sourcePath: file.path,
243
+ sourceDigest: file.digest,
244
+ locator: `line-${rowIndex + 1}`,
245
+ kind: kind === 'unknown' ? legacyKindFromId(legacyId) : kind,
246
+ legacyId,
247
+ record: {
248
+ format: 'markdown-table',
249
+ fields,
250
+ },
251
+ }));
252
+ }
253
+ claimedLines.add(rowIndex);
254
+ rowIndex += 1;
255
+ }
256
+ index = rowIndex - 1;
257
+ }
258
+ let block;
259
+ const flushBlock = () => {
260
+ if (!block)
261
+ return;
262
+ const legacyId = boundedLegacyId(block.fields.id);
263
+ if (legacyId && Object.keys(block.fields).length >= 2) {
264
+ candidates.push(candidate({
265
+ sourcePath: file.path,
266
+ sourceDigest: file.digest,
267
+ locator: `line-${block.start + 1}`,
268
+ kind: legacyKindFromId(legacyId),
269
+ legacyId,
270
+ record: {
271
+ format: 'markdown-fields',
272
+ fields: block.fields,
273
+ },
274
+ }));
275
+ }
276
+ else if (block.fields.id) {
277
+ conflicts.push(`${file.path}:${block.start + 1}:missing-or-invalid-id`);
278
+ }
279
+ block = undefined;
280
+ };
281
+ for (let index = 0; index < lines.length; index += 1) {
282
+ if (claimedLines.has(index))
283
+ continue;
284
+ const match = lines[index].match(/^\s*(?:[-*]\s*)?(?:\*\*)?([A-Za-z][A-Za-z0-9 /_-]{0,48})(?:\*\*)?\s*:\s*(.*?)\s*$/);
285
+ if (!match) {
286
+ if (!lines[index].trim())
287
+ flushBlock();
288
+ continue;
289
+ }
290
+ const key = normalizedHeader(match[1]);
291
+ if (key === 'id') {
292
+ flushBlock();
293
+ block = { start: index, fields: { id: match[2].trim() } };
294
+ }
295
+ else if (block) {
296
+ block.fields[key] = match[2].trim();
297
+ }
298
+ }
299
+ flushBlock();
300
+ return { candidates, conflicts };
301
+ }
302
+ function plainObject(value) {
303
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
304
+ }
305
+ function jsonKind(key, value) {
306
+ if (key?.startsWith('finding'))
307
+ return 'finding';
308
+ if (key?.startsWith('decision'))
309
+ return 'decision';
310
+ if (key?.startsWith('gap'))
311
+ return 'gap';
312
+ if (key?.startsWith('route'))
313
+ return 'route';
314
+ if (key?.startsWith('outcome'))
315
+ return 'outcome';
316
+ const id = boundedLegacyId(value.id);
317
+ if (id)
318
+ return legacyKindFromId(id);
319
+ return 'unknown';
320
+ }
321
+ function parseJsonCandidates(file) {
322
+ if (!STRUCTURED_JSON_NAMES.has(path.basename(file.path).toLowerCase())) {
323
+ return { candidates: [], conflicts: [] };
324
+ }
325
+ const source = decodeUtf8(file.bytes, file.path);
326
+ let parsed;
327
+ try {
328
+ parsed = parseStrictJson(source, {
329
+ maxBytes: MAX_BYTES,
330
+ maxDepth: 32,
331
+ maxScalars: 20_000,
332
+ maxStringLength: 100_000,
333
+ });
334
+ }
335
+ catch {
336
+ return { candidates: [], conflicts: [`${file.path}:corrupt-json`] };
337
+ }
338
+ const entries = [];
339
+ if (Array.isArray(parsed)) {
340
+ for (const value of parsed) {
341
+ if (!plainObject(value)) {
342
+ return { candidates: [], conflicts: [`${file.path}:non-object-json-record`] };
343
+ }
344
+ entries.push({ key: null, value });
345
+ }
346
+ }
347
+ else if (plainObject(parsed)) {
348
+ let foundCollection = false;
349
+ for (const key of ['findings', 'decisions', 'gaps', 'routes', 'outcomes', 'records']) {
350
+ const collection = parsed[key];
351
+ if (collection === undefined)
352
+ continue;
353
+ foundCollection = true;
354
+ if (!Array.isArray(collection) || collection.some((value) => !plainObject(value))) {
355
+ return { candidates: [], conflicts: [`${file.path}:invalid-${key}-collection`] };
356
+ }
357
+ entries.push(...collection.map((value) => ({
358
+ key,
359
+ value: value,
360
+ })));
361
+ }
362
+ if (!foundCollection)
363
+ entries.push({ key: null, value: parsed });
364
+ }
365
+ else {
366
+ return { candidates: [], conflicts: [`${file.path}:invalid-json-root`] };
367
+ }
368
+ const candidates = [];
369
+ const conflicts = [];
370
+ for (const [index, entry] of entries.entries()) {
371
+ if (typeof entry.value.id === 'string' && containsSecret(entry.value.id.trim())) {
372
+ conflicts.push(`${file.path}:json-${index + 1}:sensitive-id`);
373
+ continue;
374
+ }
375
+ const legacyId = boundedLegacyId(entry.value.id);
376
+ candidates.push(candidate({
377
+ sourcePath: file.path,
378
+ sourceDigest: file.digest,
379
+ locator: `json-${index + 1}`,
380
+ kind: jsonKind(entry.key, entry.value),
381
+ legacyId,
382
+ record: {
383
+ format: 'json',
384
+ value: entry.value,
385
+ },
386
+ }));
387
+ }
388
+ return {
389
+ candidates,
390
+ conflicts,
391
+ };
392
+ }
393
+ function migrationEventPayload(event) {
394
+ return event.payload;
395
+ }
396
+ async function applyDuplicateAndConflictRules(candidates, store, migrationId) {
397
+ const conflicts = [];
398
+ const byIdentity = new Map();
399
+ for (const row of candidates) {
400
+ const identity = `${row.legacyKind}:${row.legacyId ?? row.recordDigest}`;
401
+ const prior = byIdentity.get(identity);
402
+ if (!prior) {
403
+ byIdentity.set(identity, row);
404
+ continue;
405
+ }
406
+ if (prior.recordDigest === row.recordDigest) {
407
+ row.disposition = 'duplicate';
408
+ row.duplicateOf = prior.sourceId;
409
+ }
410
+ else {
411
+ prior.disposition = 'conflict';
412
+ row.disposition = 'conflict';
413
+ conflicts.push(`${identity}:conflicting-source-records`);
414
+ }
415
+ }
416
+ const replay = await store.replay();
417
+ const imported = replay.events.filter((event) => event.type === IMPORT_EVENT_TYPE);
418
+ const importedByIdentity = new Map();
419
+ const importedByEventId = new Map(imported.map((event) => [event.eventId, event]));
420
+ for (const event of imported) {
421
+ const payload = migrationEventPayload(event);
422
+ const identity = `${payload.legacyKind}:${payload.legacyId ?? payload.sourceDigest}`;
423
+ importedByIdentity.set(identity, event);
424
+ }
425
+ for (const row of candidates) {
426
+ if (row.disposition !== 'import')
427
+ continue;
428
+ const exact = importedByEventId.get(row.eventId);
429
+ if (exact && migrationEventPayload(exact).migrationId === migrationId) {
430
+ const exactPayload = migrationEventPayload(exact);
431
+ if (exactPayload.sourceDigest === row.sourceDigest) {
432
+ row.disposition = 'already-imported';
433
+ }
434
+ else {
435
+ row.disposition = 'conflict';
436
+ conflicts.push(`${row.legacyKind}:${row.legacyId ?? row.recordDigest}:event-payload-mismatch:${exact.eventId}`);
437
+ }
438
+ continue;
439
+ }
440
+ const identity = `${row.legacyKind}:${row.legacyId ?? row.recordDigest}`;
441
+ const prior = importedByIdentity.get(identity);
442
+ if (!prior)
443
+ continue;
444
+ const payload = migrationEventPayload(prior);
445
+ if (payload.sourceDigest === row.sourceDigest) {
446
+ row.disposition = 'duplicate';
447
+ row.duplicateOf = prior.eventId;
448
+ }
449
+ else {
450
+ row.disposition = 'conflict';
451
+ conflicts.push(`${identity}:conflicts-with-imported-event:${prior.eventId}`);
452
+ }
453
+ }
454
+ return [...new Set(conflicts)].sort();
455
+ }
456
+ function backupManifest(migrationId, sourceDigest, files) {
457
+ return {
458
+ implementation: 'openplanr-operate-legacy-backup',
459
+ version: '1.0.0',
460
+ migrationId,
461
+ sourceRoot: LEGACY_ROOT,
462
+ sourceDigest,
463
+ files: files.map((file, index) => ({
464
+ path: file.path,
465
+ digest: file.digest,
466
+ size: file.bytes.byteLength,
467
+ backupFile: `files/${String(index).padStart(4, '0')}`,
468
+ })),
469
+ };
470
+ }
471
+ async function inspectPrepared(input) {
472
+ const files = await readLegacyFiles(input.projectRoot);
473
+ if (files.length === 0)
474
+ return emptyInspection();
475
+ const sourceDigest = canonicalDigest(files.map((file) => ({
476
+ path: file.path,
477
+ digest: file.digest,
478
+ size: file.bytes.byteLength,
479
+ })));
480
+ const migrationId = `MIG-${sourceDigest.slice('sha256:'.length, 'sha256:'.length + 24)}`;
481
+ const store = new OperatingEventStore(input.projectRoot, { localRoot: input.localRoot });
482
+ const candidates = [];
483
+ const conflicts = [];
484
+ for (const file of files) {
485
+ try {
486
+ const parsed = file.path.endsWith('.json')
487
+ ? parseJsonCandidates(file)
488
+ : parseMarkdownCandidates(file);
489
+ candidates.push(...parsed.candidates);
490
+ conflicts.push(...parsed.conflicts);
491
+ }
492
+ catch (error) {
493
+ if (error instanceof OperateError && error.code === 'E_OPERATE_MIGRATION_CONFLICT') {
494
+ conflicts.push(`${file.path}:corrupt-input`);
495
+ }
496
+ else {
497
+ throw error;
498
+ }
499
+ }
500
+ }
501
+ for (const row of candidates) {
502
+ row.eventId = `legacy-import-${canonicalDigest({
503
+ migrationId,
504
+ identity: `${row.legacyKind}:${row.legacyId ?? row.recordDigest}`,
505
+ }).slice('sha256:'.length)}`;
506
+ if (!SUPPORTED_EVENT_SOURCE_PATH.test(row.sourcePath)) {
507
+ row.disposition = 'conflict';
508
+ conflicts.push(`${row.sourcePath}:unsupported-source-path`);
509
+ }
510
+ }
511
+ conflicts.push(...(await applyDuplicateAndConflictRules(candidates, store, migrationId)));
512
+ const manifest = backupManifest(migrationId, sourceDigest, files);
513
+ const backupManifestDigest = canonicalDigest(manifest);
514
+ const mappings = candidates
515
+ .filter((row) => row.disposition === 'import' || row.disposition === 'already-imported')
516
+ .map((row) => ({
517
+ sourceId: row.sourceId,
518
+ targetId: row.targetId,
519
+ eventId: row.eventId,
520
+ }))
521
+ .sort((left, right) => left.sourceId.localeCompare(right.sourceId));
522
+ const stableConflicts = [...new Set(conflicts)].sort();
523
+ const now = input.now ?? new Date().toISOString();
524
+ const previewDigest = canonicalDigest({
525
+ migrationId,
526
+ sourceDigest,
527
+ backupManifestDigest,
528
+ mappings,
529
+ conflicts: stableConflicts,
530
+ });
531
+ const record = {
532
+ kind: 'operating-migration-record',
533
+ schemaVersion: OPERATE_SCHEMA_VERSION,
534
+ protocolVersion: OPERATE_PROTOCOL_VERSION,
535
+ id: migrationId,
536
+ sourceKind: 'prototype-board',
537
+ sourceDigest,
538
+ state: stableConflicts.length > 0 ? 'conflict' : 'previewed',
539
+ previewDigest,
540
+ backupManifestDigest,
541
+ mappings,
542
+ conflicts: stableConflicts,
543
+ createdAt: now,
544
+ updatedAt: now,
545
+ };
546
+ await assertOperatingArtifact('operating-migration-record', record);
547
+ const publicRows = candidates.map(({ legacyRecord: _legacyRecord, ...row }) => row);
548
+ return {
549
+ record,
550
+ sourcePath: LEGACY_ROOT,
551
+ sourceDigest,
552
+ backupManifest: manifest,
553
+ files: files.map((file) => ({
554
+ path: file.path,
555
+ digest: file.digest,
556
+ size: file.bytes.byteLength,
557
+ rows: candidates.filter((candidate) => candidate.sourcePath === file.path).length,
558
+ })),
559
+ rows: publicRows,
560
+ candidates,
561
+ counts: {
562
+ files: files.length,
563
+ bytes: files.reduce((total, file) => total + file.bytes.byteLength, 0),
564
+ importable: candidates.filter((row) => row.disposition === 'import').length,
565
+ alreadyImported: candidates.filter((row) => row.disposition === 'already-imported').length,
566
+ duplicates: candidates.filter((row) => row.disposition === 'duplicate').length,
567
+ conflicts: stableConflicts.length,
568
+ },
569
+ };
570
+ }
571
+ function publicInspection(prepared) {
572
+ return {
573
+ record: prepared.record,
574
+ sourcePath: prepared.sourcePath,
575
+ files: prepared.files,
576
+ rows: prepared.rows,
577
+ counts: prepared.counts,
578
+ };
579
+ }
580
+ async function writePrivateBytes(target, bytes) {
581
+ await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
582
+ try {
583
+ const handle = await open(target, 'wx', 0o600);
584
+ try {
585
+ await handle.writeFile(bytes);
586
+ await handle.sync();
587
+ }
588
+ finally {
589
+ await handle.close();
590
+ }
591
+ }
592
+ catch (error) {
593
+ if (error.code !== 'EEXIST')
594
+ throw error;
595
+ const existing = await readFile(target);
596
+ if (sha256Digest(existing) !== sha256Digest(bytes)) {
597
+ throw new OperateError('E_OPERATE_MIGRATION_CONFLICT', `Migration backup changed unexpectedly: ${path.basename(target)}`);
598
+ }
599
+ }
600
+ }
601
+ function backupRoot(projectRoot, migrationId, localRoot) {
602
+ return path.join(resolveOperatingPaths(projectRoot, { localRoot }).localRoot, 'migration-backups', migrationId);
603
+ }
604
+ async function ensureBackup(input) {
605
+ const { record, backupManifest } = input.prepared;
606
+ if (!record || !backupManifest)
607
+ return;
608
+ const currentFiles = await readLegacyFiles(input.projectRoot);
609
+ const byPath = new Map(currentFiles.map((file) => [file.path, file]));
610
+ if (canonicalDigest(currentFiles.map((file) => ({
611
+ path: file.path,
612
+ digest: file.digest,
613
+ size: file.bytes.byteLength,
614
+ }))) !== record.sourceDigest) {
615
+ throw new OperateError('E_OPERATE_MIGRATION_CONFLICT', 'Legacy board changed after migration preview.');
616
+ }
617
+ const root = backupRoot(input.projectRoot, record.id, input.localRoot);
618
+ for (const file of backupManifest.files) {
619
+ const source = byPath.get(file.path);
620
+ if (!source || source.digest !== file.digest || source.bytes.byteLength !== file.size) {
621
+ throw new OperateError('E_OPERATE_MIGRATION_CONFLICT', `Legacy board changed after preview: ${file.path}`);
622
+ }
623
+ await writePrivateBytes(path.join(root, file.backupFile), source.bytes);
624
+ }
625
+ const manifestBytes = Buffer.from(`${canonicalize(backupManifest)}\n`);
626
+ await writePrivateBytes(path.join(root, 'manifest.json'), manifestBytes);
627
+ if (canonicalDigest(backupManifest) !== record.backupManifestDigest) {
628
+ throw new OperateError('E_OPERATE_STATE_INVALID', 'Migration backup manifest digest does not match the preview.');
629
+ }
630
+ }
631
+ async function verifyBackup(input) {
632
+ const root = backupRoot(input.projectRoot, input.record.id, input.localRoot);
633
+ const manifest = JSON.parse(await readFile(path.join(root, 'manifest.json'), 'utf8'));
634
+ if (manifest.migrationId !== input.record.id ||
635
+ manifest.sourceDigest !== input.record.sourceDigest ||
636
+ canonicalDigest(manifest) !== input.record.backupManifestDigest) {
637
+ throw new OperateError('E_OPERATE_MIGRATION_CONFLICT', `Migration ${input.record.id} backup manifest failed integrity verification.`);
638
+ }
639
+ for (const file of manifest.files) {
640
+ const bytes = await readFile(path.join(root, file.backupFile));
641
+ if (bytes.byteLength !== file.size || sha256Digest(bytes) !== file.digest) {
642
+ throw new OperateError('E_OPERATE_MIGRATION_CONFLICT', `Migration ${input.record.id} backup is corrupt: ${file.path}`);
643
+ }
644
+ }
645
+ return manifest;
646
+ }
647
+ async function readMigrationRecord(projectRoot, migrationId, localRoot) {
648
+ const target = path.join(resolveOperatingPaths(projectRoot, { localRoot }).migrations, `${migrationId}.json`);
649
+ try {
650
+ const record = JSON.parse(await readFile(target, 'utf8'));
651
+ return assertOperatingArtifact('operating-migration-record', record);
652
+ }
653
+ catch (error) {
654
+ if (error.code === 'ENOENT')
655
+ return null;
656
+ throw error;
657
+ }
658
+ }
659
+ async function writeMigrationMetadata(input) {
660
+ const relativePath = `.planr/operate/migrations/${input.record.id}.json`;
661
+ const target = path.join(input.projectRoot, relativePath);
662
+ const current = await readFile(target).catch((error) => {
663
+ if (error.code === 'ENOENT')
664
+ return null;
665
+ throw error;
666
+ });
667
+ const desired = Buffer.from(`${canonicalize(input.record)}\n`);
668
+ if (current?.equals(desired))
669
+ return;
670
+ const journal = await prepareJournalTransaction(input.projectRoot, {
671
+ transactionId: `TXN-${input.record.id}-${input.record.state}-metadata`,
672
+ writes: [
673
+ {
674
+ relativePath,
675
+ operation: current ? 'replace' : 'create',
676
+ content: desired,
677
+ },
678
+ ],
679
+ eventHead: input.eventHead,
680
+ previewDigest: canonicalDigest({
681
+ migrationId: input.record.id,
682
+ state: input.record.state,
683
+ sourceDigest: input.record.sourceDigest,
684
+ eventHead: input.eventHead,
685
+ }),
686
+ localRoot: input.localRoot,
687
+ });
688
+ await applyJournalTransaction(input.projectRoot, journal, {
689
+ currentEventHead: input.eventHead,
690
+ });
691
+ }
692
+ async function persistMigrationHead(projectRoot, store, localRoot) {
693
+ const state = await store.state();
694
+ await store.writeCheckpoint(state);
695
+ await persistOperatingProjections({
696
+ projectRoot,
697
+ localRoot,
698
+ state,
699
+ revalidateEventHead: async () => (await store.replay()).eventHead,
700
+ });
701
+ }
702
+ export async function inspectOperatingMigration(input) {
703
+ return publicInspection(await inspectPrepared(input));
704
+ }
705
+ export async function applyOperatingMigration(input) {
706
+ const preview = await inspectPrepared(input);
707
+ if (!preview.record)
708
+ return null;
709
+ const previewRecord = preview.record;
710
+ if (previewRecord.state === 'conflict') {
711
+ throw new OperateError('E_OPERATE_MIGRATION_CONFLICT', 'Legacy board contains conflicts or corrupt input; no migration was applied.', { preview: publicInspection(preview) });
712
+ }
713
+ if (!input.confirmed) {
714
+ throw new OperateError('E_OPERATE_AUTHORITY_REQUIRED', 'Migration requires explicit confirmation of its preview.', { preview: publicInspection(preview) });
715
+ }
716
+ const store = new OperatingEventStore(input.projectRoot, { localRoot: input.localRoot });
717
+ const initial = await store.replay();
718
+ await input.beforeTransition?.('lock');
719
+ return withOperatingLock(input.projectRoot, {
720
+ projectKey: operatingProjectKey(input.projectRoot),
721
+ expectedEventHead: initial.eventHead,
722
+ currentEventHead: initial.eventHead,
723
+ localRoot: input.localRoot,
724
+ }, async (lock) => {
725
+ const lockedReplay = await store.replay();
726
+ lock.assertEventHead(lockedReplay.eventHead);
727
+ const existing = await readMigrationRecord(input.projectRoot, previewRecord.id, input.localRoot);
728
+ if (existing?.state === 'applied') {
729
+ await verifyBackup({
730
+ projectRoot: input.projectRoot,
731
+ localRoot: input.localRoot,
732
+ record: existing,
733
+ });
734
+ await persistMigrationHead(input.projectRoot, store, input.localRoot);
735
+ return existing;
736
+ }
737
+ if (existing) {
738
+ throw new OperateError('E_OPERATE_MIGRATION_CONFLICT', `Migration ${existing.id} is already ${existing.state}.`);
739
+ }
740
+ const current = await inspectPrepared(input);
741
+ if (!current.record ||
742
+ current.record.previewDigest !== previewRecord.previewDigest ||
743
+ current.record.state === 'conflict') {
744
+ throw new OperateError('E_OPERATE_MIGRATION_CONFLICT', 'Legacy migration input or conflicts changed after preview.');
745
+ }
746
+ await input.beforeTransition?.('backup');
747
+ await ensureBackup({
748
+ projectRoot: input.projectRoot,
749
+ localRoot: input.localRoot,
750
+ prepared: current,
751
+ });
752
+ let replay = await store.replay();
753
+ let head = replay.eventHead;
754
+ const existingEventIds = new Set(replay.events.map((event) => event.eventId));
755
+ const importRows = current.candidates.filter((row) => row.disposition === 'import' || row.disposition === 'already-imported');
756
+ const immutablePlan = {
757
+ ...current.record,
758
+ createdAt: UNKNOWN_CREATED_AT,
759
+ updatedAt: UNKNOWN_CREATED_AT,
760
+ };
761
+ await assertOperatingArtifact('operating-migration-record', immutablePlan);
762
+ await input.beforeTransition?.('record');
763
+ const savedPlan = await store.putRecord('migration', immutablePlan, {
764
+ correlationId: current.record.id,
765
+ createdAt: UNKNOWN_CREATED_AT,
766
+ });
767
+ for (const [index, row] of importRows.entries()) {
768
+ if (existingEventIds.has(row.eventId))
769
+ continue;
770
+ await input.beforeTransition?.('event', index);
771
+ const event = await store.append({
772
+ type: IMPORT_EVENT_TYPE,
773
+ eventId: row.eventId,
774
+ cycleId: 'CYCLE-000',
775
+ entityId: current.record.id,
776
+ actor: IMPORT_ACTOR,
777
+ correlationId: current.record.id,
778
+ evidenceRefs: [],
779
+ timestamp: input.now,
780
+ expectedHead: head.hash,
781
+ payload: {
782
+ migrationId: current.record.id,
783
+ sourcePath: row.sourcePath,
784
+ sourceDigest: row.sourceDigest,
785
+ recordDigest: savedPlan.digest,
786
+ backupManifestDigest: current.record.backupManifestDigest,
787
+ legacyKind: row.legacyKind,
788
+ ...(row.legacyId ? { legacyId: row.legacyId } : {}),
789
+ },
790
+ });
791
+ const next = { sequence: event.sequence, hash: event.eventHash };
792
+ await lock.advanceEventHead(head, next);
793
+ head = next;
794
+ existingEventIds.add(event.eventId);
795
+ }
796
+ replay = await store.replay();
797
+ const appliedAt = input.now ?? new Date().toISOString();
798
+ const applied = {
799
+ ...current.record,
800
+ state: 'applied',
801
+ updatedAt: appliedAt,
802
+ };
803
+ await assertOperatingArtifact('operating-migration-record', applied);
804
+ await input.beforeTransition?.('metadata');
805
+ await writeMigrationMetadata({
806
+ projectRoot: input.projectRoot,
807
+ localRoot: input.localRoot,
808
+ record: applied,
809
+ eventHead: replay.eventHead,
810
+ });
811
+ await input.beforeTransition?.('checkpoint');
812
+ await persistMigrationHead(input.projectRoot, store, input.localRoot);
813
+ await verifyBackup({
814
+ projectRoot: input.projectRoot,
815
+ localRoot: input.localRoot,
816
+ record: applied,
817
+ });
818
+ return applied;
819
+ });
820
+ }
821
+ export async function rollbackOperatingMigration(input) {
822
+ if (!input.confirmed) {
823
+ throw new OperateError('E_OPERATE_AUTHORITY_REQUIRED', 'Migration rollback requires explicit confirmation.');
824
+ }
825
+ const store = new OperatingEventStore(input.projectRoot, { localRoot: input.localRoot });
826
+ const initial = await store.replay();
827
+ return withOperatingLock(input.projectRoot, {
828
+ projectKey: operatingProjectKey(input.projectRoot),
829
+ expectedEventHead: initial.eventHead,
830
+ currentEventHead: initial.eventHead,
831
+ localRoot: input.localRoot,
832
+ }, async (lock) => {
833
+ const lockedReplay = await store.replay();
834
+ lock.assertEventHead(lockedReplay.eventHead);
835
+ const current = await readMigrationRecord(input.projectRoot, input.migrationId, input.localRoot);
836
+ if (!current) {
837
+ throw new OperateError('E_OPERATE_MIGRATION_CONFLICT', `Unknown migration ${input.migrationId}.`);
838
+ }
839
+ if (current.state === 'rolled-back') {
840
+ await verifyBackup({
841
+ projectRoot: input.projectRoot,
842
+ localRoot: input.localRoot,
843
+ record: current,
844
+ });
845
+ await persistMigrationHead(input.projectRoot, store, input.localRoot);
846
+ return current;
847
+ }
848
+ if (current.state !== 'applied') {
849
+ throw new OperateError('E_OPERATE_MIGRATION_CONFLICT', `Migration ${input.migrationId} is not applied.`);
850
+ }
851
+ await verifyBackup({
852
+ projectRoot: input.projectRoot,
853
+ localRoot: input.localRoot,
854
+ record: current,
855
+ });
856
+ const eventId = `legacy-rollback-${current.id}`;
857
+ const replay = lockedReplay;
858
+ let head = replay.eventHead;
859
+ if (!replay.events.some((event) => event.eventId === eventId)) {
860
+ const now = input.now ?? new Date().toISOString();
861
+ const recovery = {
862
+ kind: 'operating-recovery-record',
863
+ schemaVersion: OPERATE_SCHEMA_VERSION,
864
+ protocolVersion: OPERATE_PROTOCOL_VERSION,
865
+ id: `RCV-${current.id.slice('MIG-'.length)}-rollback`,
866
+ transactionId: null,
867
+ action: 'rollback',
868
+ reason: `Compensate legacy Operating Board import ${current.id}; source bytes remain untouched.`,
869
+ previewDigest: canonicalDigest({
870
+ migrationId: current.id,
871
+ from: current.state,
872
+ to: 'rolled-back',
873
+ sourceDigest: current.sourceDigest,
874
+ }),
875
+ fromHead: head,
876
+ toHead: head,
877
+ outcome: 'recovered',
878
+ confirmedBy: 'operate-cli',
879
+ createdAt: now,
880
+ };
881
+ await assertOperatingArtifact('operating-recovery-record', recovery);
882
+ const saved = await store.putRecord('recovery', recovery, {
883
+ correlationId: recovery.id,
884
+ createdAt: now,
885
+ });
886
+ const event = await store.append({
887
+ type: 'recovery.performed',
888
+ eventId,
889
+ cycleId: 'CYCLE-000',
890
+ entityId: recovery.id,
891
+ actor: { kind: 'human', id: 'operate-cli' },
892
+ correlationId: recovery.id,
893
+ timestamp: now,
894
+ expectedHead: head.hash,
895
+ payload: { recordDigest: saved.digest },
896
+ });
897
+ const next = { sequence: event.sequence, hash: event.eventHash };
898
+ await lock.advanceEventHead(head, next);
899
+ head = next;
900
+ }
901
+ const rolledBack = {
902
+ ...current,
903
+ state: 'rolled-back',
904
+ updatedAt: input.now ?? new Date().toISOString(),
905
+ };
906
+ await assertOperatingArtifact('operating-migration-record', rolledBack);
907
+ await writeMigrationMetadata({
908
+ projectRoot: input.projectRoot,
909
+ localRoot: input.localRoot,
910
+ record: rolledBack,
911
+ eventHead: head,
912
+ });
913
+ await persistMigrationHead(input.projectRoot, store, input.localRoot);
914
+ await verifyBackup({
915
+ projectRoot: input.projectRoot,
916
+ localRoot: input.localRoot,
917
+ record: rolledBack,
918
+ });
919
+ return rolledBack;
920
+ });
921
+ }
922
+ //# sourceMappingURL=legacy-import-service.js.map