@akagilnc/pi-workflow-roles 0.1.4035 → 0.1.4193

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 (108) hide show
  1. package/dist/acp-host/production-host.js +2368 -1155
  2. package/dist/archivist-record-entry.js +204 -20
  3. package/dist/archivist-record-topology.js +36 -16
  4. package/dist/auditor-dossier-tool.js +2 -1
  5. package/dist/diarist-contracts.js +96 -0
  6. package/dist/doctor-auditor.js +1 -1
  7. package/dist/doctor-evidence.js +3 -2
  8. package/dist/dossier-resolution.js +21 -8
  9. package/dist/gatekeeper-role.js +34 -27
  10. package/dist/headless-host/description.js +7 -8
  11. package/dist/headless-host/production-host.js +2375 -1149
  12. package/dist/host-contracts.js +16 -0
  13. package/dist/inspector-contracts.js +8 -0
  14. package/dist/migrate-book-topology.js +278917 -0
  15. package/dist/navigator-public-session.js +22 -25
  16. package/dist/notary-source-run.js +7 -4
  17. package/dist/packaged-role-registry.js +1 -1
  18. package/dist/pi/durable-principal.js +9 -6
  19. package/dist/pi/role-turn-host.js +43 -13
  20. package/dist/public-cli/auto-resume.js +2 -5
  21. package/dist/public-cli/case-dossier-delivery.js +60 -1
  22. package/dist/public-cli/diarist-run.js +27 -25
  23. package/dist/public-cli/inspector-run.js +17 -3
  24. package/dist/public-cli/instruction-seat-run.js +17 -17
  25. package/dist/public-cli/invocation.js +127 -63
  26. package/dist/public-cli/main.js +1685 -696
  27. package/dist/public-cli/notary-run.js +7 -4
  28. package/dist/public-cli/option-definitions.js +2 -2
  29. package/dist/public-cli/post-admission.js +113 -71
  30. package/dist/public-cli/run-lifecycle.js +209 -58
  31. package/dist/public-cli/seat-ticket-binding.js +3 -6
  32. package/dist/public-cli/settlement.js +29 -10
  33. package/dist/public-role-summons.js +8 -0
  34. package/dist/role-run-placement.js +84 -0
  35. package/dist/role-run-relocation.js +358 -0
  36. package/dist/run-terminal-artifacts.js +2 -1
  37. package/dist/run-ticket-number.js +51 -32
  38. package/dist/sitian-appender.js +11 -26
  39. package/dist/submission-ledger.js +106 -25
  40. package/dist/ticket-provenance.js +3 -1
  41. package/dist/user-dialogue-stdin.js +33 -0
  42. package/extensions/role-runtime.ts +1 -0
  43. package/package.json +3 -2
  44. package/resources/836-deleted-machine-instruction-inventory.md +1 -1
  45. package/scripts/build-package.mjs +34 -2
  46. package/souls/fixer.md +3 -2
  47. package/src/analyst-ledger.ts +14 -15
  48. package/src/archivist-record-entry.ts +285 -38
  49. package/src/archivist-record-topology.ts +43 -18
  50. package/src/auditor-dossier-tool.ts +2 -1
  51. package/src/book-topology-migration-placement.ts +493 -0
  52. package/src/book-topology-migration.ts +220 -0
  53. package/src/book-topology-migrators-t11.ts +856 -0
  54. package/src/book-topology-mixed-volume-migrators.ts +470 -0
  55. package/src/book-topology-partition-migrators.ts +25 -0
  56. package/src/book-topology-record-class-migrators.ts +779 -0
  57. package/src/book-topology-runs-migrator.ts +244 -0
  58. package/src/diarist-contracts.ts +116 -0
  59. package/src/diarist-role.ts +6 -0
  60. package/src/diarist.ts +2 -0
  61. package/src/doctor-auditor.ts +1 -1
  62. package/src/doctor-evidence.ts +4 -1
  63. package/src/doctor-role.ts +1 -1
  64. package/src/dossier-resolution.ts +26 -8
  65. package/src/external-host-turn-loop.ts +9 -0
  66. package/src/gatekeeper-pass-envelope.ts +6 -0
  67. package/src/gatekeeper-role.ts +47 -28
  68. package/src/headless-host/description.ts +8 -11
  69. package/src/headless-host/role-turn-host.ts +20 -5
  70. package/src/host-contracts.ts +22 -3
  71. package/src/inspector-contracts.ts +7 -0
  72. package/src/judge-role.ts +4 -0
  73. package/src/navigator-public-session.ts +33 -30
  74. package/src/navigator-work-context.ts +3 -4
  75. package/src/notary-role.ts +1 -0
  76. package/src/notary-source-run.ts +9 -4
  77. package/src/packaged-role-registry.ts +1 -1
  78. package/src/pi/adapter.ts +19 -4
  79. package/src/pi/durable-principal.ts +9 -13
  80. package/src/pi/role-turn-host.ts +48 -14
  81. package/src/public-cli/auto-resume.ts +2 -6
  82. package/src/public-cli/case-dossier-delivery.ts +86 -1
  83. package/src/public-cli/countersign-run.ts +182 -32
  84. package/src/public-cli/diarist-run.ts +34 -26
  85. package/src/public-cli/inspector-run.ts +26 -2
  86. package/src/public-cli/instruction-seat-run.ts +24 -20
  87. package/src/public-cli/invocation.ts +194 -58
  88. package/src/public-cli/notary-run.ts +13 -4
  89. package/src/public-cli/option-definitions.ts +2 -2
  90. package/src/public-cli/post-admission.ts +147 -78
  91. package/src/public-cli/run-lifecycle.ts +258 -65
  92. package/src/public-cli/seat-ticket-binding.ts +3 -6
  93. package/src/public-cli/settlement.ts +38 -10
  94. package/src/public-role-summons.ts +31 -7
  95. package/src/role-envelope.ts +9 -30
  96. package/src/role-run-placement.ts +128 -0
  97. package/src/role-run-relocation.ts +491 -0
  98. package/src/role-runtime-dependencies.ts +1 -0
  99. package/src/role-runtime.ts +106 -24
  100. package/src/run-terminal-artifacts.ts +3 -1
  101. package/src/run-ticket-number.ts +80 -20
  102. package/src/sitian-appender.ts +25 -28
  103. package/src/submission-ledger.ts +148 -30
  104. package/src/ticket-provenance.ts +4 -1
  105. package/src/ticket-trajectory.ts +22 -35
  106. package/src/user-dialogue-stdin.ts +37 -0
  107. package/src/worker-role.ts +8 -4
  108. package/src/worker-submission-gates.ts +6 -6
@@ -0,0 +1,470 @@
1
+ /**
2
+ * #868 T12: book-root mixed single volumes. Line-by-line (or per session
3
+ * volume) attribution into the owning run; undecidable rows/volumes go to
4
+ * unbound/. Never discard, never silent-merge, never leave at book root.
5
+ */
6
+ import type { Dirent, Stats } from "node:fs";
7
+ import { appendFile, cp, lstat, mkdir, readFile, writeFile } from "node:fs/promises";
8
+ import { dirname, join, relative, resolve, sep } from "node:path";
9
+
10
+ import { WORKER_SUBMISSION_GATE_KIND } from "./archivist-record-entry.ts";
11
+ import {
12
+ reconcileMigrationPartition,
13
+ type BookTopologyMigrationContext,
14
+ type BookTopologyPartitionMigrator,
15
+ type MigrationItemOutcome,
16
+ } from "./book-topology-migration.ts";
17
+ import {
18
+ bookHistoricalRoots,
19
+ findPlacedMigratingRun,
20
+ isMigrationEnoent,
21
+ listMigrationBookKeys,
22
+ listMigrationDirents,
23
+ runRefFromBoundPath,
24
+ } from "./book-topology-migration-placement.ts";
25
+
26
+ const SITIAN_MIXED_VOLUME_PARTITIONS = [
27
+ "attendance",
28
+ "auditor",
29
+ "dispatch-error",
30
+ "gate",
31
+ ] as const;
32
+
33
+ const CURRENT_SESSION_LEDGER = "current-session.json";
34
+
35
+ function isRecord(value: unknown): value is Record<string, unknown> {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+
39
+ function posixRelative(from: string, to: string): string {
40
+ return relative(from, to).split(sep).join("/");
41
+ }
42
+
43
+ function addPathCandidate(paths: string[], seen: Set<string>, value: unknown): void {
44
+ if (typeof value !== "string" || value.length === 0 || seen.has(value)) return;
45
+ seen.add(value);
46
+ paths.push(value);
47
+ }
48
+
49
+ /** Path fields that can carry a decidable run leaf (`.../runs/<leaf>/...`). */
50
+ function pathCandidatesFromRecord(record: Record<string, unknown>): string[] {
51
+ const paths: string[] = [];
52
+ const seen = new Set<string>();
53
+ addPathCandidate(paths, seen, record.sessionParent);
54
+ addPathCandidate(paths, seen, record.parentSession);
55
+ if (!isRecord(record.payload)) return paths;
56
+ const payload = record.payload;
57
+ addPathCandidate(paths, seen, payload.sessionParent);
58
+ addPathCandidate(paths, seen, payload.parentSession);
59
+ addPathCandidate(paths, seen, payload.sessionFile);
60
+ if (isRecord(payload.parent)) {
61
+ addPathCandidate(paths, seen, payload.parent.sessionFile);
62
+ addPathCandidate(paths, seen, payload.parent.sessionParent);
63
+ addPathCandidate(paths, seen, payload.parent.parentSession);
64
+ }
65
+ if (isRecord(payload.data)) {
66
+ addPathCandidate(paths, seen, payload.data.sessionParent);
67
+ addPathCandidate(paths, seen, payload.data.parentSession);
68
+ addPathCandidate(paths, seen, payload.data.sessionFile);
69
+ addPathCandidate(paths, seen, payload.data.file);
70
+ }
71
+ return paths;
72
+ }
73
+
74
+ function describeFsKind(entry: Dirent | Stats): string {
75
+ if (entry.isFile()) return "file";
76
+ if (entry.isDirectory()) return "directory";
77
+ if (entry.isSymbolicLink()) return "symbolic link";
78
+ if (entry.isFIFO()) return "FIFO";
79
+ if (entry.isSocket()) return "socket";
80
+ if (entry.isCharacterDevice()) return "character device";
81
+ if (entry.isBlockDevice()) return "block device";
82
+ return "unknown filesystem object";
83
+ }
84
+
85
+ function runRefFromRecord(
86
+ record: Record<string, unknown>,
87
+ bookRoots: readonly string[],
88
+ ): ReturnType<typeof runRefFromBoundPath> {
89
+ for (const path of pathCandidatesFromRecord(record)) {
90
+ const ref = runRefFromBoundPath(path, bookRoots);
91
+ if (ref !== undefined) return ref;
92
+ }
93
+ return undefined;
94
+ }
95
+
96
+ async function readRegularBackupFile(path: string): Promise<string | undefined> {
97
+ let info: Stats;
98
+ try {
99
+ info = await lstat(path);
100
+ } catch (error) {
101
+ if (isMigrationEnoent(error)) return undefined;
102
+ throw error;
103
+ }
104
+ if (!info.isFile()) {
105
+ throw new Error(
106
+ `cannot migrate mixed volume from ${path}: source is a ${describeFsKind(info)}, not a regular file`,
107
+ );
108
+ }
109
+ return readFile(path, "utf8");
110
+ }
111
+
112
+ /**
113
+ * Worker-submission-gate volumes are Pi session files. Ownership is only the
114
+ * session header's parentSession — never later message/payload path fields.
115
+ * Malformed JSONL rows are reported separately; they do not select the leaf.
116
+ */
117
+ function inspectWorkerSubmissionGateVolume(
118
+ text: string,
119
+ bookRoots: readonly string[],
120
+ ): {
121
+ readonly leaf: string | undefined;
122
+ readonly sourceRelative: string | undefined;
123
+ readonly malformedRows: readonly { readonly lineNumber: number; readonly raw: string }[];
124
+ } {
125
+ const malformedRows: { lineNumber: number; raw: string }[] = [];
126
+ let firstRecord = true;
127
+ let leaf: string | undefined;
128
+ let sourceRelative: string | undefined;
129
+
130
+ for (const row of jsonlRows(text)) {
131
+ const isHeader = firstRecord;
132
+ firstRecord = false;
133
+ let parsed: unknown;
134
+ try {
135
+ parsed = JSON.parse(row.raw);
136
+ } catch {
137
+ malformedRows.push({ lineNumber: row.lineNumber, raw: row.raw });
138
+ continue;
139
+ }
140
+ if (!isRecord(parsed)) {
141
+ malformedRows.push({ lineNumber: row.lineNumber, raw: row.raw });
142
+ continue;
143
+ }
144
+ // Sole attribution evidence: the first JSONL record's parentSession.
145
+ if (
146
+ isHeader &&
147
+ parsed.type === "session" &&
148
+ typeof parsed.parentSession === "string"
149
+ ) {
150
+ const ref = runRefFromBoundPath(parsed.parentSession, bookRoots);
151
+ leaf = ref?.leaf;
152
+ sourceRelative = ref?.sourceRelative;
153
+ }
154
+ }
155
+
156
+ return { leaf, sourceRelative, malformedRows };
157
+ }
158
+
159
+ function volumeDestinationFile(
160
+ booksDirectory: string,
161
+ bookKey: string,
162
+ destRun: { readonly runDirectory: string } | undefined,
163
+ kind: string,
164
+ fileName: string,
165
+ ): string {
166
+ if (destRun === undefined) {
167
+ return join(booksDirectory, bookKey, "unbound", kind, fileName);
168
+ }
169
+ return join(destRun.runDirectory, "session", kind, fileName);
170
+ }
171
+
172
+ async function placedRunForLeaf(
173
+ cache: Map<string, { runDirectory: string; disposition: "placed" | "unbound" } | undefined>,
174
+ booksDirectory: string,
175
+ bookKey: string,
176
+ leaf: string,
177
+ sourceRelative?: string,
178
+ ): Promise<{ runDirectory: string; disposition: "placed" | "unbound" } | undefined> {
179
+ const cacheKey = `${bookKey}\0${sourceRelative ?? ""}\0${leaf}`;
180
+ if (cache.has(cacheKey)) return cache.get(cacheKey);
181
+ const placed = await findPlacedMigratingRun(
182
+ booksDirectory,
183
+ bookKey,
184
+ leaf,
185
+ sourceRelative,
186
+ );
187
+ cache.set(cacheKey, placed);
188
+ return placed;
189
+ }
190
+
191
+ async function appendRawLine(file: string, raw: string): Promise<void> {
192
+ await mkdir(dirname(file), { recursive: true });
193
+ await appendFile(file, `${raw}\n`, "utf8");
194
+ }
195
+
196
+ function jsonlRows(text: string): readonly { lineNumber: number; raw: string }[] {
197
+ const lines = text.split("\n");
198
+ const rows: { lineNumber: number; raw: string }[] = [];
199
+ for (let index = 0; index < lines.length; index += 1) {
200
+ const raw = lines[index]!;
201
+ if (index === lines.length - 1 && raw.length === 0) continue;
202
+ if (!raw.trim()) continue;
203
+ rows.push({ lineNumber: index + 1, raw });
204
+ }
205
+ return rows;
206
+ }
207
+
208
+ async function migrateCurrentSessionPointer(input: {
209
+ readonly sourceDir: string;
210
+ readonly volumeDestinations: ReadonlyMap<string, string>;
211
+ readonly unboundPointerFile: string;
212
+ }): Promise<void> {
213
+ const sourceFile = join(input.sourceDir, CURRENT_SESSION_LEDGER);
214
+ const raw = await readRegularBackupFile(sourceFile);
215
+ if (raw === undefined) return;
216
+
217
+ let sessionFile: string | undefined;
218
+ try {
219
+ const parsed: unknown = JSON.parse(raw);
220
+ if (isRecord(parsed) && typeof parsed.sessionFile === "string" && parsed.sessionFile.length > 0) {
221
+ sessionFile = parsed.sessionFile;
222
+ }
223
+ } catch {
224
+ sessionFile = undefined;
225
+ }
226
+
227
+ let destinationSessionFile: string | undefined;
228
+ if (sessionFile !== undefined) {
229
+ const pointed = resolve(input.sourceDir, sessionFile);
230
+ destinationSessionFile = input.volumeDestinations.get(pointed)
231
+ ?? input.volumeDestinations.get(sessionFile);
232
+ }
233
+
234
+ if (destinationSessionFile === undefined) {
235
+ await mkdir(dirname(input.unboundPointerFile), { recursive: true });
236
+ await writeFile(
237
+ input.unboundPointerFile,
238
+ raw.endsWith("\n") ? raw : `${raw}\n`,
239
+ "utf8",
240
+ );
241
+ return;
242
+ }
243
+ const pointerFile = join(dirname(destinationSessionFile), CURRENT_SESSION_LEDGER);
244
+ await mkdir(dirname(pointerFile), { recursive: true });
245
+ await writeFile(
246
+ pointerFile,
247
+ `${JSON.stringify({ sessionFile: destinationSessionFile })}\n`,
248
+ "utf8",
249
+ );
250
+ }
251
+
252
+ async function migrateSitianMixedVolume(
253
+ partition: (typeof SITIAN_MIXED_VOLUME_PARTITIONS)[number],
254
+ context: BookTopologyMigrationContext,
255
+ ): Promise<ReturnType<typeof reconcileMigrationPartition>> {
256
+ const outcomes: MigrationItemOutcome[] = [];
257
+ const placedCache = new Map<string, { runDirectory: string; disposition: "placed" | "unbound" } | undefined>();
258
+ const { backupBooksDirectory, booksDirectory } = context;
259
+
260
+ for (const bookKey of await listMigrationBookKeys(backupBooksDirectory)) {
261
+ const sourceDir = join(backupBooksDirectory, bookKey, partition);
262
+ const recordsFile = join(sourceDir, "records.jsonl");
263
+ const bookRoots = bookHistoricalRoots(booksDirectory, backupBooksDirectory, bookKey);
264
+ const text = await readRegularBackupFile(recordsFile);
265
+ if (text === undefined) {
266
+ await migrateCurrentSessionPointer({
267
+ sourceDir,
268
+ volumeDestinations: new Map(),
269
+ unboundPointerFile: join(
270
+ booksDirectory,
271
+ bookKey,
272
+ "unbound",
273
+ partition,
274
+ CURRENT_SESSION_LEDGER,
275
+ ),
276
+ });
277
+ continue;
278
+ }
279
+
280
+ // Line placement builds the sole volume→dest map for current-session rewrite.
281
+ const placedDests = new Set<string>();
282
+ const sourceIdentity = posixRelative(backupBooksDirectory, recordsFile);
283
+ for (const row of jsonlRows(text)) {
284
+ const source = `${sourceIdentity}:${row.lineNumber}`;
285
+ let parsed: unknown;
286
+ try {
287
+ parsed = JSON.parse(row.raw);
288
+ } catch {
289
+ const dest = volumeDestinationFile(
290
+ booksDirectory,
291
+ bookKey,
292
+ undefined,
293
+ partition,
294
+ "records.jsonl",
295
+ );
296
+ await appendRawLine(dest, row.raw);
297
+ placedDests.add(dest);
298
+ outcomes.push({
299
+ disposition: "unbound",
300
+ source,
301
+ malformed: true,
302
+ malformedRaw: row.raw,
303
+ });
304
+ continue;
305
+ }
306
+ if (!isRecord(parsed)) {
307
+ const dest = volumeDestinationFile(
308
+ booksDirectory,
309
+ bookKey,
310
+ undefined,
311
+ partition,
312
+ "records.jsonl",
313
+ );
314
+ await appendRawLine(dest, row.raw);
315
+ placedDests.add(dest);
316
+ outcomes.push({
317
+ disposition: "unbound",
318
+ source,
319
+ malformed: true,
320
+ malformedRaw: row.raw,
321
+ });
322
+ continue;
323
+ }
324
+
325
+ const ref = runRefFromRecord(parsed, bookRoots);
326
+ const destRun = ref === undefined
327
+ ? undefined
328
+ : await placedRunForLeaf(
329
+ placedCache,
330
+ booksDirectory,
331
+ bookKey,
332
+ ref.leaf,
333
+ ref.sourceRelative,
334
+ );
335
+ const dest = volumeDestinationFile(
336
+ booksDirectory,
337
+ bookKey,
338
+ destRun,
339
+ partition,
340
+ "records.jsonl",
341
+ );
342
+ await appendRawLine(dest, row.raw);
343
+ placedDests.add(dest);
344
+ outcomes.push({
345
+ disposition: destRun?.disposition === "placed" ? "placed" : "unbound",
346
+ source,
347
+ });
348
+ }
349
+
350
+ const volumeDestinations = new Map<string, string>();
351
+ if (placedDests.size === 1) {
352
+ const onlyDest = placedDests.values().next().value!;
353
+ volumeDestinations.set(resolve(recordsFile), onlyDest);
354
+ volumeDestinations.set(
355
+ resolve(join(booksDirectory, bookKey, partition, "records.jsonl")),
356
+ onlyDest,
357
+ );
358
+ }
359
+ await migrateCurrentSessionPointer({
360
+ sourceDir,
361
+ volumeDestinations,
362
+ unboundPointerFile: join(
363
+ booksDirectory,
364
+ bookKey,
365
+ "unbound",
366
+ partition,
367
+ CURRENT_SESSION_LEDGER,
368
+ ),
369
+ });
370
+ }
371
+
372
+ return reconcileMigrationPartition(partition, "lines", outcomes);
373
+ }
374
+
375
+ async function migrateWorkerSubmissionGate(
376
+ context: BookTopologyMigrationContext,
377
+ ): Promise<ReturnType<typeof reconcileMigrationPartition>> {
378
+ const outcomes: MigrationItemOutcome[] = [];
379
+ const volumeMalformedRows: { source: string; raw: string }[] = [];
380
+ const placedCache = new Map<string, { runDirectory: string; disposition: "placed" | "unbound" } | undefined>();
381
+ const { backupBooksDirectory, booksDirectory } = context;
382
+ const kind = WORKER_SUBMISSION_GATE_KIND;
383
+
384
+ for (const bookKey of await listMigrationBookKeys(backupBooksDirectory)) {
385
+ const sourceDir = join(backupBooksDirectory, bookKey, kind);
386
+ const entries = await listMigrationDirents(sourceDir);
387
+ if (entries.length === 0) continue;
388
+
389
+ const bookRoots = bookHistoricalRoots(booksDirectory, backupBooksDirectory, bookKey);
390
+ const volumes = entries
391
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
392
+ .map((entry) => entry.name)
393
+ .sort();
394
+ const volumeDestinations = new Map<string, string>();
395
+
396
+ for (const name of volumes) {
397
+ const sourcePath = join(sourceDir, name);
398
+ const source = posixRelative(backupBooksDirectory, sourcePath);
399
+ const text = await readRegularBackupFile(sourcePath);
400
+ if (text === undefined) continue;
401
+ const inspected = inspectWorkerSubmissionGateVolume(text, bookRoots);
402
+ const destRun = inspected.leaf === undefined
403
+ ? undefined
404
+ : await placedRunForLeaf(
405
+ placedCache,
406
+ booksDirectory,
407
+ bookKey,
408
+ inspected.leaf,
409
+ inspected.sourceRelative,
410
+ );
411
+ const dest = volumeDestinationFile(
412
+ booksDirectory,
413
+ bookKey,
414
+ destRun,
415
+ kind,
416
+ name,
417
+ );
418
+ await mkdir(dirname(dest), { recursive: true });
419
+ await cp(sourcePath, dest, { preserveTimestamps: true });
420
+ volumeDestinations.set(resolve(sourcePath), dest);
421
+ volumeDestinations.set(resolve(join(booksDirectory, bookKey, kind, name)), dest);
422
+ // One outcome per volume keeps the entries closure; bad rows attach below.
423
+ outcomes.push({
424
+ disposition: destRun?.disposition === "placed" ? "placed" : "unbound",
425
+ source,
426
+ });
427
+ for (const bad of inspected.malformedRows) {
428
+ volumeMalformedRows.push({
429
+ source: `${source}:${bad.lineNumber}`,
430
+ raw: bad.raw,
431
+ });
432
+ }
433
+ }
434
+
435
+ await migrateCurrentSessionPointer({
436
+ sourceDir,
437
+ volumeDestinations,
438
+ unboundPointerFile: join(
439
+ booksDirectory,
440
+ bookKey,
441
+ "unbound",
442
+ kind,
443
+ CURRENT_SESSION_LEDGER,
444
+ ),
445
+ });
446
+ }
447
+
448
+ const report = reconcileMigrationPartition(kind, "entries", outcomes);
449
+ return {
450
+ ...report,
451
+ // Malformed rows are evidence on the volume bytes, not extra entry units.
452
+ malformedRows: [...report.malformedRows, ...volumeMalformedRows],
453
+ };
454
+ }
455
+
456
+ const bookTopologySitianMixedVolumeMigrators: readonly BookTopologyPartitionMigrator[] =
457
+ SITIAN_MIXED_VOLUME_PARTITIONS.map((partition) => ({
458
+ partition,
459
+ migrate: (context) => migrateSitianMixedVolume(partition, context),
460
+ }));
461
+
462
+ const bookTopologyWorkerSubmissionGateMigrator: BookTopologyPartitionMigrator = {
463
+ partition: WORKER_SUBMISSION_GATE_KIND,
464
+ migrate: migrateWorkerSubmissionGate,
465
+ };
466
+
467
+ export const BOOK_TOPOLOGY_MIXED_VOLUME_MIGRATORS: readonly BookTopologyPartitionMigrator[] = [
468
+ ...bookTopologySitianMixedVolumeMigrators,
469
+ bookTopologyWorkerSubmissionGateMigrator,
470
+ ];
@@ -0,0 +1,25 @@
1
+ import { BOOK_TOPOLOGY_MIXED_VOLUME_MIGRATORS } from "./book-topology-mixed-volume-migrators.ts";
2
+ import type { BookTopologyPartitionMigrator } from "./book-topology-migration.ts";
3
+ import { BOOK_TOPOLOGY_T11_MIGRATORS } from "./book-topology-migrators-t11.ts";
4
+ import { BOOK_TOPOLOGY_RECORD_CLASS_MIGRATORS } from "./book-topology-record-class-migrators.ts";
5
+ import { bookTopologyRunsMigrator } from "./book-topology-runs-migrator.ts";
6
+
7
+ /**
8
+ * One assembly point for the independently implemented legacy partitions.
9
+ * #865–#868 populate this list; the orchestrator refuses an empty migration.
10
+ * #866 contributes record-class migrators (ticket-provenance / submission-ledger /
11
+ * attempt-history / misplaced-record-class). Runs (#865) should precede run-owned
12
+ * record classes when both are assembled.
13
+ *
14
+ * T11 (#867) registration order: auditor-roles, issues, deprecated-kinds,
15
+ * deprecated-run-pages, navigator, collector-handbook, manual-archives.
16
+ * auditor-roles prefers a run already placed by T9 when that nest exists.
17
+ * deprecated-run-pages must run after the runs migrator (T9): it deletes
18
+ * matching pages from already-placed destination runs.
19
+ */
20
+ export const BOOK_TOPOLOGY_PARTITION_MIGRATORS: readonly BookTopologyPartitionMigrator[] = [
21
+ bookTopologyRunsMigrator,
22
+ ...BOOK_TOPOLOGY_RECORD_CLASS_MIGRATORS,
23
+ ...BOOK_TOPOLOGY_T11_MIGRATORS,
24
+ ...BOOK_TOPOLOGY_MIXED_VOLUME_MIGRATORS,
25
+ ];