project-librarian 0.2.0 → 0.3.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.
package/dist/migration.js CHANGED
@@ -33,9 +33,18 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.generatedMigrationInboxFiles = void 0;
36
37
  exports.classifyMarkdown = classifyMarkdown;
38
+ exports.formOnlyMigrationDocumentReason = formOnlyMigrationDocumentReason;
39
+ exports.extractMigrationUnits = extractMigrationUnits;
40
+ exports.collectMigrationCoverageDiagnostics = collectMigrationCoverageDiagnostics;
41
+ exports.collectMigrationUnitMapDiagnostics = collectMigrationUnitMapDiagnostics;
42
+ exports.collectMigrationSplitPlanDiagnostics = collectMigrationSplitPlanDiagnostics;
37
43
  exports.markdownTableRows = markdownTableRows;
38
44
  exports.buildInbox = buildInbox;
45
+ exports.isPrunableGeneratedMigrationInbox = isPrunableGeneratedMigrationInbox;
46
+ exports.migrationSemanticReviewComplete = migrationSemanticReviewComplete;
47
+ exports.buildMigrationBulkReviewPlan = buildMigrationBulkReviewPlan;
39
48
  exports.timestampSuffix = timestampSuffix;
40
49
  exports.prepareMigrationMode = prepareMigrationMode;
41
50
  exports.migrationTargetForKind = migrationTargetForKind;
@@ -43,9 +52,11 @@ exports.runMigrationMode = runMigrationMode;
43
52
  exports.normalizeMigrationStatus = normalizeMigrationStatus;
44
53
  exports.isMigrationInboxStatus = isMigrationInboxStatus;
45
54
  exports.migrationInboxStatusMap = migrationInboxStatusMap;
55
+ exports.migrationCoverageStatusMap = migrationCoverageStatusMap;
46
56
  exports.semanticStatusForInboxStatus = semanticStatusForInboxStatus;
47
57
  exports.runReviewMigrationMode = runReviewMigrationMode;
48
58
  const fs = __importStar(require("node:fs"));
59
+ const taxonomy_1 = require("./taxonomy");
49
60
  const workspace_1 = require("./workspace");
50
61
  const templates_1 = require("./templates");
51
62
  const wiki_files_1 = require("./wiki-files");
@@ -67,10 +78,489 @@ function classifyMarkdown(relativePath, text) {
67
78
  function markdownTableCell(value) {
68
79
  return String(value).replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
69
80
  }
81
+ function plainMarkdownTableCell(value) {
82
+ return markdownTableCell(String(value).replace(/\[/g, "&#91;").replace(/\]/g, "&#93;"));
83
+ }
84
+ function slugPart(value) {
85
+ return value.toLowerCase().replace(/[^a-z0-9가-힣ぁ-んァ-ン一-龥]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "unit";
86
+ }
87
+ function unitSummary(value) {
88
+ return value.replace(/\s+/g, " ").trim().slice(0, 180);
89
+ }
90
+ function nextUnitId(legacyPath, index, summary) {
91
+ return `${legacyPath}#u${String(index).padStart(3, "0")}-${slugPart(summary)}`;
92
+ }
93
+ const generatedEmptyStarterPaths = new Map([
94
+ ["canonical/project-brief.md", "wiki/canonical/project-brief.md"],
95
+ ["canonical/open-questions.md", "wiki/canonical/open-questions.md"],
96
+ ["canonical/assumptions.md", "wiki/canonical/assumptions.md"],
97
+ ["canonical/risks.md", "wiki/canonical/risks.md"],
98
+ ]);
99
+ function normalizedTemplateBody(value) {
100
+ return (0, workspace_1.stripMetadataHeader)(value).replace(/\r\n/g, "\n").trim().replace(/[ \t]+/g, " ");
101
+ }
102
+ function isGeneratedEmptyStarterDocument(legacyPath, text) {
103
+ const starterPath = generatedEmptyStarterPaths.get(legacyPath.replace(/^wiki\//, ""));
104
+ if (!starterPath)
105
+ return false;
106
+ const starter = templates_1.starterFiles[starterPath];
107
+ if (!starter)
108
+ return false;
109
+ return normalizedTemplateBody(text) === normalizedTemplateBody(starter);
110
+ }
111
+ function formOnlyMigrationDocumentReason(legacyPath, text) {
112
+ if ((0, workspace_1.metadataValue)(text, "status").toLowerCase() === "template")
113
+ return "metadata status is template";
114
+ if (isGeneratedEmptyStarterDocument(legacyPath, text))
115
+ return "matches generated empty starter page";
116
+ return "";
117
+ }
118
+ function isFormOnlyTemplateRouteUnit(legacyPath, value) {
119
+ if (legacyPath.replace(/^wiki\//, "") !== "index.md")
120
+ return false;
121
+ return /\bdecisions\/(?:decision-pack-template|full-adr-template)\b/.test(value);
122
+ }
123
+ function extractMigrationUnits(legacyPath, text) {
124
+ if (formOnlyMigrationDocumentReason(legacyPath, text))
125
+ return [];
126
+ const body = (0, workspace_1.stripMetadataHeader)(text);
127
+ const lines = body.split(/\r?\n/);
128
+ const units = [];
129
+ let heading = "";
130
+ let headingPath = [];
131
+ let paragraph = [];
132
+ let inCodeFence = false;
133
+ let codeBlock = [];
134
+ let sourceUnitIndex = 0;
135
+ const pushUnit = (type, value) => {
136
+ const summary = unitSummary(value);
137
+ if (!summary)
138
+ return;
139
+ sourceUnitIndex += 1;
140
+ if (isFormOnlyTemplateRouteUnit(legacyPath, value))
141
+ return;
142
+ const unitHeadingPath = [...headingPath];
143
+ units.push({
144
+ id: nextUnitId(legacyPath, sourceUnitIndex, summary),
145
+ legacyPath,
146
+ type,
147
+ heading,
148
+ headingPath: unitHeadingPath,
149
+ content: value,
150
+ summary,
151
+ classification: (0, taxonomy_1.classifyMigrationUnit)({ legacyPath, heading, headingPath: unitHeadingPath, content: value, summary }),
152
+ });
153
+ };
154
+ const flushParagraph = () => {
155
+ if (paragraph.length === 0)
156
+ return;
157
+ pushUnit("paragraph", paragraph.join(" "));
158
+ paragraph = [];
159
+ };
160
+ for (const line of lines) {
161
+ const trimmed = line.trim();
162
+ if (/^```/.test(trimmed)) {
163
+ if (inCodeFence) {
164
+ codeBlock.push(line);
165
+ pushUnit("code-block", codeBlock.join("\n"));
166
+ codeBlock = [];
167
+ inCodeFence = false;
168
+ }
169
+ else {
170
+ flushParagraph();
171
+ inCodeFence = true;
172
+ codeBlock = [line];
173
+ }
174
+ continue;
175
+ }
176
+ if (inCodeFence) {
177
+ codeBlock.push(line);
178
+ continue;
179
+ }
180
+ if (!trimmed) {
181
+ flushParagraph();
182
+ continue;
183
+ }
184
+ const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
185
+ if (headingMatch?.[2]) {
186
+ flushParagraph();
187
+ heading = headingMatch[2].trim();
188
+ const level = headingMatch[1]?.length ?? 1;
189
+ headingPath = [...headingPath.slice(0, level - 1), heading];
190
+ pushUnit("heading", heading);
191
+ continue;
192
+ }
193
+ if (/^\|.+\|$/.test(trimmed) && !/^\|\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(trimmed)) {
194
+ flushParagraph();
195
+ pushUnit("table-row", trimmed);
196
+ continue;
197
+ }
198
+ if (/^([-*+]|\d+[.)])\s+/.test(trimmed)) {
199
+ flushParagraph();
200
+ pushUnit("list-item", trimmed);
201
+ continue;
202
+ }
203
+ paragraph.push(trimmed);
204
+ }
205
+ if (inCodeFence && codeBlock.length > 0)
206
+ pushUnit("code-block", codeBlock.join("\n"));
207
+ flushParagraph();
208
+ return units;
209
+ }
210
+ function coverageTableRows(units) {
211
+ if (units.length === 0)
212
+ return "| none | - | - | - | - | pending | - | - | - | - | - |\n";
213
+ return units.map((unit) => {
214
+ const status = unit.classification.confidence === "low" ? "needs-human-review" : "pending";
215
+ return `| ${markdownTableCell(unit.id)} | ${markdownTableCell(unit.legacyPath)} | ${unit.type} | ${plainMarkdownTableCell(unit.heading || "-")} | ${plainMarkdownTableCell(unit.summary)} | ${status} | ${markdownTableCell(unit.classification.target)} | - | ${markdownTableCell(unit.classification.label)} | ${unit.classification.confidence} | ${plainMarkdownTableCell(unit.classification.reason)} |`;
216
+ }).join("\n") + "\n";
217
+ }
218
+ function coverageRowsFromUnits(units) {
219
+ return units.map((unit) => ({
220
+ unitId: unit.id,
221
+ legacySource: unit.legacyPath,
222
+ type: unit.type,
223
+ heading: unit.heading || "-",
224
+ summary: unit.summary,
225
+ status: unit.classification.confidence === "low" ? "needs-human-review" : "pending",
226
+ target: unit.classification.target,
227
+ note: "-",
228
+ area: unit.classification.label,
229
+ confidence: unit.classification.confidence,
230
+ reason: unit.classification.reason,
231
+ }));
232
+ }
233
+ function normalizeCoverageStatus(value) {
234
+ const status = String(value || "").trim().toLowerCase();
235
+ if (isMigrationCoverageStatus(status))
236
+ return status;
237
+ return "pending";
238
+ }
239
+ function normalizeConfidence(value) {
240
+ const confidence = String(value || "").trim().toLowerCase();
241
+ if (isMigrationConfidence(confidence))
242
+ return confidence;
243
+ return "low";
244
+ }
245
+ function parseMigrationCoverageRows(text) {
246
+ return (0, wiki_files_1.parseMarkdownTableRows)(text, 11)
247
+ .filter((cells) => cells[0] !== "Unit ID")
248
+ .filter((cells) => !isMarkdownTableSeparatorRow(cells))
249
+ .map((cells) => ({
250
+ unitId: cells[0] ?? "",
251
+ legacySource: cells[1] ?? "",
252
+ type: cells[2] ?? "",
253
+ heading: cells[3] ?? "",
254
+ summary: cells[4] ?? "",
255
+ status: normalizeCoverageStatus(cells[5]),
256
+ target: cells[6] ?? "",
257
+ note: cells[7] ?? "",
258
+ area: cells[8] ?? "",
259
+ confidence: normalizeConfidence(cells[9]),
260
+ reason: cells[10] ?? "",
261
+ }))
262
+ .filter((row) => row.unitId);
263
+ }
264
+ function unitMapRows(units) {
265
+ if (units.length === 0)
266
+ return "| none | - | - | - | - | - | - | - | - | - |\n";
267
+ return units.map((unit) => {
268
+ const status = unit.classification.confidence === "low" ? "needs-human-review" : "pending";
269
+ return `| ${markdownTableCell(unit.id)} | ${markdownTableCell(unit.legacyPath)} | ${plainMarkdownTableCell(unit.headingPath.join(" > ") || "-")} | ${markdownTableCell(unit.classification.label)} | ${unit.classification.storage} | ${unit.classification.confidence} | ${markdownTableCell(unit.classification.target)} | ${plainMarkdownTableCell(unit.classification.reason)} | ${plainMarkdownTableCell(unit.summary)} | ${status} |`;
270
+ }).join("\n") + "\n";
271
+ }
272
+ function confidenceRank(value) {
273
+ if (value === "high")
274
+ return 3;
275
+ if (value === "medium")
276
+ return 2;
277
+ return 1;
278
+ }
279
+ function splitPlanRows(units) {
280
+ if (units.length === 0)
281
+ return "| none | - | - | - | 0 | - |\n";
282
+ const groups = new Map();
283
+ for (const unit of units) {
284
+ const key = unit.classification.target;
285
+ groups.set(key, [...(groups.get(key) ?? []), unit]);
286
+ }
287
+ return Array.from(groups.entries())
288
+ .sort(([left], [right]) => left.localeCompare(right))
289
+ .map(([target, group]) => {
290
+ const first = group[0];
291
+ const confidence = group.map((unit) => unit.classification.confidence).sort((left, right) => confidenceRank(left) - confidenceRank(right))[0] ?? "low";
292
+ const unitIds = group.map((unit) => unit.id).join("<br>");
293
+ return `| ${markdownTableCell(target)} | ${markdownTableCell(first?.classification.label ?? "-")} | ${first?.classification.storage ?? "-"} | ${confidence} | ${group.length} | ${markdownTableCell(unitIds)} |`;
294
+ }).join("\n") + "\n";
295
+ }
296
+ function isMigrationCoverageStatus(value) {
297
+ return ["adopted", "merged", "superseded", "rejected", "resolved", "needs-human-review", "pending"].includes(value);
298
+ }
299
+ function legacyWikiRoots() {
300
+ if (!fs.existsSync(workspace_1.root))
301
+ return [];
302
+ return fs.readdirSync(workspace_1.root, { withFileTypes: true })
303
+ .filter((entry) => entry.isDirectory() && /^wiki_legacy(?:_|$)/.test(entry.name))
304
+ .map((entry) => entry.name)
305
+ .sort();
306
+ }
307
+ function migrationVerificationLegacyRoot() {
308
+ if (!(0, workspace_1.exists)("wiki/migration/verification.md"))
309
+ return null;
310
+ const legacyRoot = ((0, workspace_1.read)("wiki/migration/verification.md").match(/^- legacy root:\s*(.+)$/m) || [])[1]?.trim();
311
+ if (!legacyRoot || ["none", "unknown"].includes(legacyRoot))
312
+ return null;
313
+ return (0, workspace_1.exists)(legacyRoot) ? legacyRoot : null;
314
+ }
315
+ function activeMigrationLegacyRoots() {
316
+ const verifiedRoot = migrationVerificationLegacyRoot();
317
+ return verifiedRoot ? [verifiedRoot] : legacyWikiRoots();
318
+ }
319
+ function expectedMigrationUnits() {
320
+ return activeMigrationLegacyRoots()
321
+ .flatMap((legacyRoot) => (0, wiki_files_1.walkMarkdownFiles)((0, workspace_1.abs)(legacyRoot), [], (0, workspace_1.abs)(legacyRoot)))
322
+ .flatMap((file) => extractMigrationUnits(file.basePath, (0, workspace_1.read)(file.path)));
323
+ }
324
+ function isMigrationConfidence(value) {
325
+ return ["high", "medium", "low"].includes(value);
326
+ }
327
+ function isMigrationStorage(value) {
328
+ return ["canonical", "decisions", "sources", "meta"].includes(value);
329
+ }
330
+ exports.generatedMigrationInboxFiles = [
331
+ "wiki/canonical/migration-inbox.md",
332
+ "wiki/decisions/migration-inbox.md",
333
+ "wiki/sources/migration-inbox.md",
334
+ ];
335
+ function isNewWikiTarget(value) {
336
+ return /^wiki\/(canonical|decisions|sources|meta)\//.test(value);
337
+ }
338
+ function expectedUnitMap(units) {
339
+ return new Map(units.map((unit) => [unit.id, unit]));
340
+ }
341
+ function splitLegacyUnitsCell(value) {
342
+ return value
343
+ .split(/<br\s*\/?>|,/i)
344
+ .map((item) => item.trim())
345
+ .filter(Boolean);
346
+ }
347
+ function isMarkdownTableSeparatorRow(cells) {
348
+ return cells.every((cell) => /^:?-+:?$/.test(cell.replace(/\s/g, "")));
349
+ }
350
+ function isReviewedCoverageRetarget(cells) {
351
+ const note = String(cells[7] || "").toLowerCase();
352
+ const reason = String(cells[10] || "").toLowerCase();
353
+ return note.includes("reviewed low-confidence content; retargeted")
354
+ || reason.includes("reviewed source context; taxonomy target assigned");
355
+ }
356
+ function collectMigrationCoverageDiagnostics() {
357
+ const units = expectedMigrationUnits();
358
+ if (units.length === 0)
359
+ return [];
360
+ if (!(0, workspace_1.exists)("wiki/migration/coverage.md")) {
361
+ return [{
362
+ code: "migration-coverage-missing",
363
+ severity: "error",
364
+ file: "wiki/migration/coverage.md",
365
+ message: "migration unit coverage ledger is missing; run --migrate to account for legacy meaning units",
366
+ }];
367
+ }
368
+ const diagnostics = [];
369
+ const expectedIds = new Set(units.map((unit) => unit.id));
370
+ const unitsById = expectedUnitMap(units);
371
+ const seenIds = new Set();
372
+ const rows = (0, wiki_files_1.parseMarkdownTableRows)((0, workspace_1.read)("wiki/migration/coverage.md"), 8)
373
+ .filter((cells) => cells[0] !== "Unit ID")
374
+ .filter((cells) => !isMarkdownTableSeparatorRow(cells));
375
+ for (const cells of rows) {
376
+ const id = cells[0] || "";
377
+ const status = String(cells[5] || "").trim().toLowerCase();
378
+ const target = String(cells[6] || "").trim();
379
+ const confidence = String(cells[9] || "").trim().toLowerCase();
380
+ const expectedUnit = unitsById.get(id);
381
+ if (cells.length < 11) {
382
+ diagnostics.push({ code: "migration-coverage-schema-drift", severity: "error", file: "wiki/migration/coverage.md", message: `coverage row for ${id || "(blank)"} is missing Area, Confidence, or Reason columns; rerun --migrate` });
383
+ }
384
+ if (seenIds.has(id)) {
385
+ diagnostics.push({ code: "migration-duplicate-unit", severity: "error", file: "wiki/migration/coverage.md", message: `duplicate migration unit row: ${id}` });
386
+ }
387
+ seenIds.add(id);
388
+ if (!expectedIds.has(id)) {
389
+ diagnostics.push({ code: "migration-stale-unit", severity: "warn", file: "wiki/migration/coverage.md", message: `coverage row does not match current legacy units: ${id}` });
390
+ }
391
+ if (!isMigrationCoverageStatus(status)) {
392
+ diagnostics.push({ code: "migration-invalid-status", severity: "error", file: "wiki/migration/coverage.md", message: `unit ${id} has invalid status: ${status || "(blank)"}` });
393
+ }
394
+ if (confidence && !isMigrationConfidence(confidence)) {
395
+ diagnostics.push({ code: "migration-invalid-confidence", severity: "error", file: "wiki/migration/coverage.md", message: `unit ${id} has invalid confidence: ${confidence}` });
396
+ }
397
+ if (["adopted", "merged"].includes(status) && !isNewWikiTarget(target)) {
398
+ diagnostics.push({ code: "migration-missing-target", severity: "error", file: "wiki/migration/coverage.md", message: `unit ${id} is ${status} but target is not a new wiki page` });
399
+ }
400
+ if (status === "pending" && expectedUnit && target && target !== expectedUnit.classification.target && !isReviewedCoverageRetarget(cells)) {
401
+ diagnostics.push({ code: "migration-pending-target-drift", severity: "warn", file: "wiki/migration/coverage.md", message: `pending unit ${id} target differs from generated taxonomy target ${expectedUnit.classification.target}` });
402
+ }
403
+ if (["adopted", "merged"].includes(status) && confidence === "low") {
404
+ diagnostics.push({ code: "migration-low-confidence-adopted", severity: "warn", file: "wiki/migration/coverage.md", message: `unit ${id} is ${status} despite low confidence classification; verify the target manually` });
405
+ }
406
+ if (/\bwiki_legacy(?:_|\b|\/)/.test(target)) {
407
+ diagnostics.push({ code: "migration-legacy-target", severity: "error", file: "wiki/migration/coverage.md", message: `unit ${id} targets wiki_legacy* instead of migrated new-wiki truth` });
408
+ }
409
+ if (status === "pending") {
410
+ diagnostics.push({ code: "migration-pending-unit", severity: "warn", file: "wiki/migration/coverage.md", message: `unit ${id} is still pending migration review` });
411
+ }
412
+ }
413
+ for (const unit of units) {
414
+ if (!seenIds.has(unit.id)) {
415
+ diagnostics.push({ code: "migration-unaccounted-unit", severity: "error", file: unit.legacyPath, message: `legacy meaning unit missing from coverage ledger: ${unit.id}` });
416
+ }
417
+ }
418
+ return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
419
+ }
420
+ function collectMigrationUnitMapDiagnostics() {
421
+ const units = expectedMigrationUnits();
422
+ if (units.length === 0)
423
+ return [];
424
+ const file = "wiki/migration/unit-map.md";
425
+ if (!(0, workspace_1.exists)(file)) {
426
+ return [{
427
+ code: "migration-unit-map-missing",
428
+ severity: "error",
429
+ file,
430
+ message: "migration unit map is missing; run --migrate to classify legacy meaning units",
431
+ }];
432
+ }
433
+ const diagnostics = [];
434
+ const unitsById = expectedUnitMap(units);
435
+ const seenIds = new Set();
436
+ const rows = (0, wiki_files_1.parseMarkdownTableRows)((0, workspace_1.read)(file), 10)
437
+ .filter((cells) => cells[0] !== "Unit ID")
438
+ .filter((cells) => !isMarkdownTableSeparatorRow(cells));
439
+ for (const cells of rows) {
440
+ const id = cells[0] || "";
441
+ const area = String(cells[3] || "").trim();
442
+ const storage = String(cells[4] || "").trim();
443
+ const confidence = String(cells[5] || "").trim().toLowerCase();
444
+ const target = String(cells[6] || "").trim();
445
+ const status = String(cells[9] || "").trim().toLowerCase();
446
+ const expectedUnit = unitsById.get(id);
447
+ if (seenIds.has(id)) {
448
+ diagnostics.push({ code: "migration-unit-map-duplicate-unit", severity: "error", file, message: `duplicate unit-map row: ${id}` });
449
+ }
450
+ seenIds.add(id);
451
+ if (!expectedUnit) {
452
+ diagnostics.push({ code: "migration-unit-map-stale-unit", severity: "warn", file, message: `unit-map row does not match current migration batch: ${id}` });
453
+ }
454
+ if (!isMigrationStorage(storage)) {
455
+ diagnostics.push({ code: "migration-unit-map-invalid-storage", severity: "error", file, message: `unit ${id} has invalid storage: ${storage || "(blank)"}` });
456
+ }
457
+ if (!isMigrationConfidence(confidence)) {
458
+ diagnostics.push({ code: "migration-unit-map-invalid-confidence", severity: "error", file, message: `unit ${id} has invalid confidence: ${confidence || "(blank)"}` });
459
+ }
460
+ if (!isMigrationCoverageStatus(status)) {
461
+ diagnostics.push({ code: "migration-unit-map-invalid-status", severity: "error", file, message: `unit ${id} has invalid status: ${status || "(blank)"}` });
462
+ }
463
+ if (!isNewWikiTarget(target)) {
464
+ diagnostics.push({ code: "migration-unit-map-invalid-target", severity: "error", file, message: `unit ${id} target is not under wiki/canonical, wiki/decisions, wiki/sources, or wiki/meta` });
465
+ }
466
+ if (expectedUnit) {
467
+ if (area !== expectedUnit.classification.label) {
468
+ diagnostics.push({ code: "migration-unit-map-area-drift", severity: "warn", file, message: `unit ${id} area differs from generated classification ${expectedUnit.classification.label}` });
469
+ }
470
+ if (storage !== expectedUnit.classification.storage) {
471
+ diagnostics.push({ code: "migration-unit-map-storage-drift", severity: "warn", file, message: `unit ${id} storage differs from generated classification ${expectedUnit.classification.storage}` });
472
+ }
473
+ if (confidence !== expectedUnit.classification.confidence) {
474
+ diagnostics.push({ code: "migration-unit-map-confidence-drift", severity: "warn", file, message: `unit ${id} confidence differs from generated classification ${expectedUnit.classification.confidence}` });
475
+ }
476
+ if (target !== expectedUnit.classification.target) {
477
+ diagnostics.push({ code: "migration-unit-map-target-drift", severity: "warn", file, message: `unit ${id} target differs from generated classification ${expectedUnit.classification.target}` });
478
+ }
479
+ }
480
+ }
481
+ for (const unit of units) {
482
+ if (!seenIds.has(unit.id)) {
483
+ diagnostics.push({ code: "migration-unit-map-unaccounted-unit", severity: "error", file: unit.legacyPath, message: `legacy meaning unit missing from unit map: ${unit.id}` });
484
+ }
485
+ }
486
+ return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
487
+ }
488
+ function collectMigrationSplitPlanDiagnostics() {
489
+ const units = expectedMigrationUnits();
490
+ if (units.length === 0)
491
+ return [];
492
+ const file = "wiki/migration/split-plan.md";
493
+ if (!(0, workspace_1.exists)(file)) {
494
+ return [{
495
+ code: "migration-split-plan-missing",
496
+ severity: "error",
497
+ file,
498
+ message: "migration split plan is missing; run --migrate to group legacy units by target page",
499
+ }];
500
+ }
501
+ const diagnostics = [];
502
+ const unitsById = expectedUnitMap(units);
503
+ const seenTargets = new Set();
504
+ const seenUnitIds = new Set();
505
+ const rows = (0, wiki_files_1.parseMarkdownTableRows)((0, workspace_1.read)(file), 6)
506
+ .filter((cells) => cells[0] !== "Suggested Target")
507
+ .filter((cells) => !isMarkdownTableSeparatorRow(cells));
508
+ for (const cells of rows) {
509
+ const target = String(cells[0] || "").trim();
510
+ const area = String(cells[1] || "").trim();
511
+ const storage = String(cells[2] || "").trim();
512
+ const confidence = String(cells[3] || "").trim().toLowerCase();
513
+ const count = Number(String(cells[4] || "").trim());
514
+ const unitIds = splitLegacyUnitsCell(cells[5] || "");
515
+ const targetStorage = target.match(/^wiki\/([^/]+)\//)?.[1] ?? "";
516
+ if (seenTargets.has(target)) {
517
+ diagnostics.push({ code: "migration-split-plan-duplicate-target", severity: "error", file, message: `duplicate split-plan target row: ${target}` });
518
+ }
519
+ seenTargets.add(target);
520
+ if (!isNewWikiTarget(target)) {
521
+ diagnostics.push({ code: "migration-split-plan-invalid-target", severity: "error", file, message: `split-plan target is not under wiki/canonical, wiki/decisions, wiki/sources, or wiki/meta: ${target || "(blank)"}` });
522
+ }
523
+ if (!isMigrationStorage(storage)) {
524
+ diagnostics.push({ code: "migration-split-plan-invalid-storage", severity: "error", file, message: `target ${target || "(blank)"} has invalid storage: ${storage || "(blank)"}` });
525
+ }
526
+ if (isMigrationStorage(storage) && targetStorage && storage !== targetStorage) {
527
+ diagnostics.push({ code: "migration-split-plan-storage-target-mismatch", severity: "error", file, message: `target ${target} is under wiki/${targetStorage}/ but split-plan storage is ${storage}` });
528
+ }
529
+ if (!isMigrationConfidence(confidence)) {
530
+ diagnostics.push({ code: "migration-split-plan-invalid-confidence", severity: "error", file, message: `target ${target || "(blank)"} has invalid confidence: ${confidence || "(blank)"}` });
531
+ }
532
+ if (!Number.isInteger(count) || count !== unitIds.length) {
533
+ diagnostics.push({ code: "migration-split-plan-count-mismatch", severity: "error", file, message: `target ${target || "(blank)"} says ${Number.isFinite(count) ? count : "(blank)"} units but lists ${unitIds.length}` });
534
+ }
535
+ for (const unitId of unitIds) {
536
+ const expectedUnit = unitsById.get(unitId);
537
+ if (seenUnitIds.has(unitId)) {
538
+ diagnostics.push({ code: "migration-split-plan-duplicate-unit", severity: "error", file, message: `unit appears in more than one split-plan row: ${unitId}` });
539
+ }
540
+ seenUnitIds.add(unitId);
541
+ if (!expectedUnit) {
542
+ diagnostics.push({ code: "migration-split-plan-stale-unit", severity: "warn", file, message: `split-plan unit does not match current migration batch: ${unitId}` });
543
+ continue;
544
+ }
545
+ if (expectedUnit.classification.target !== target) {
546
+ diagnostics.push({ code: "migration-split-plan-target-drift", severity: "warn", file, message: `unit ${unitId} is listed under ${target} but generated classification targets ${expectedUnit.classification.target}` });
547
+ }
548
+ if (expectedUnit.classification.label !== area) {
549
+ diagnostics.push({ code: "migration-split-plan-area-drift", severity: "warn", file, message: `unit ${unitId} row area differs from generated classification ${expectedUnit.classification.label}` });
550
+ }
551
+ }
552
+ }
553
+ for (const unit of units) {
554
+ if (!seenUnitIds.has(unit.id)) {
555
+ diagnostics.push({ code: "migration-split-plan-unaccounted-unit", severity: "error", file: unit.legacyPath, message: `legacy meaning unit missing from split plan: ${unit.id}` });
556
+ }
557
+ }
558
+ return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
559
+ }
70
560
  function markdownTableRows(items) {
71
561
  if (items.length === 0)
72
562
  return "| none | - | - | - |\n";
73
- return items.map((item) => `| ${markdownTableCell(item.path)} | ${markdownTableCell(item.title)} | ${markdownTableCell(item.summary)} | pending |`).join("\n") + "\n";
563
+ return items.map((item) => `| ${markdownTableCell(item.path)} | ${plainMarkdownTableCell(item.title)} | ${plainMarkdownTableCell(item.summary)} | pending |`).join("\n") + "\n";
74
564
  }
75
565
  function buildInbox(title, description, items) {
76
566
  return `${(0, templates_1.metadata)("migration-inbox", "medium", "wiki/meta/wiki-ops-v1-decisions.md", "migration candidates are adopted or rescanned")}
@@ -87,6 +577,369 @@ function buildInbox(title, description, items) {
87
577
  | --- | --- | --- | --- |
88
578
  ${markdownTableRows(items)}`;
89
579
  }
580
+ function wikiLinkForMigrationPath(relativePath) {
581
+ return `[[${relativePath.replace(/^wiki\//, "").replace(/\.(md|mdx)$/i, "")}]]`;
582
+ }
583
+ function isGeneratedFileLevelMigrationInboxText(text) {
584
+ if ((0, workspace_1.metadataValue)(text, "scope") !== "migration-inbox")
585
+ return false;
586
+ const body = (0, workspace_1.stripMetadataHeader)(text).trim();
587
+ if (!body.includes("| Source | Title | Summary | Status |"))
588
+ return false;
589
+ if (!body.includes("Status values: pending, adopted, rejected, resolved, needs-human-review."))
590
+ return false;
591
+ const headings = Array.from(body.matchAll(/^#{1,6}\s+(.+)$/gm)).map((match) => match[1]?.trim() ?? "");
592
+ if (headings.length !== 2)
593
+ return false;
594
+ return (headings[0]?.endsWith("Migration Inbox") ?? false) && headings[1] === "TL;DR";
595
+ }
596
+ function isPrunableGeneratedMigrationInbox(relativePath) {
597
+ if (!exports.generatedMigrationInboxFiles.includes(relativePath))
598
+ return false;
599
+ if (!(0, workspace_1.exists)(relativePath))
600
+ return false;
601
+ return isGeneratedFileLevelMigrationInboxText((0, workspace_1.read)(relativePath));
602
+ }
603
+ function migrationSemanticReviewComplete() {
604
+ if (!(0, workspace_1.exists)("wiki/migration/verification.md"))
605
+ return false;
606
+ return /^- semantic migration complete:\s*yes, for\b/m.test((0, workspace_1.read)("wiki/migration/verification.md"));
607
+ }
608
+ function migrationBatchScope(legacyRoot) {
609
+ return `${workspace_1.today} migration batch${legacyRoot && legacyRoot !== "none" ? ` from ${legacyRoot}` : ""}`;
610
+ }
611
+ function semanticCompletionValue(complete, batchScope) {
612
+ if (complete)
613
+ return `yes, for the ${batchScope} only`;
614
+ return `no, the ${batchScope} still has unresolved rows`;
615
+ }
616
+ function completionScopeSection(batchScope) {
617
+ return `## Completion Scope
618
+
619
+ - This page records the ${batchScope} only.
620
+ - It does not mean future requests to build a new wiki from the existing wiki should reuse current \`wiki/\` in place.
621
+ - For a fresh rebuild request, treat current \`wiki/\` as the legacy source unless the user says otherwise: preserve it as \`wiki_legacy*\`, create a fresh standard \`wiki/\`, migrate/adopt content from the preserved legacy source, then refresh routing and diagnostics.
622
+ `;
623
+ }
624
+ function completedMigrationStartupBlock(legacyRoot, remainingInboxes) {
625
+ const retainedInboxLine = remainingInboxes.length === 0
626
+ ? "- Generated file-level migration inboxes were pruned after semantic completion."
627
+ : `- Generated file-level migration inboxes were pruned where safe; retained inbox-like pages: ${remainingInboxes.map((file) => `\`${file}\``).join(", ")}.`;
628
+ return `<!-- PROJECT-WIKI-MIGRATION:START -->
629
+ ## Migration State
630
+
631
+ - ${workspace_1.today}: migration review is complete for \`${legacyRoot || "wiki_legacy"}\`; pending 0 and needs-human-review 0.
632
+ ${retainedInboxLine}
633
+ - Migration ledgers under \`wiki/migration/\` remain as audit evidence.
634
+ - \`${legacyRoot || "wiki_legacy"}\` remains preserved as source/rollback archive and is not deleted automatically.
635
+ - Migration completion status is scoped to this batch only. For a future fresh rebuild request, treat current \`wiki/\` as the legacy source unless the user says otherwise.
636
+ <!-- PROJECT-WIKI-MIGRATION:END -->`;
637
+ }
638
+ function completedMigrationIndexBlock(remainingInboxes) {
639
+ const inboxLine = remainingInboxes.length === 0
640
+ ? "- Generated file-level migration inboxes were pruned after semantic completion."
641
+ : `- Retained inbox-like pages: ${remainingInboxes.map(wikiLinkForMigrationPath).join(", ")}.`;
642
+ return `<!-- PROJECT-WIKI-MIGRATION:START -->
643
+ ## Migration
644
+
645
+ - Core ledgers: [[migration/coverage]], [[migration/review]], [[migration/verification]], [[migration/bulk-review]].
646
+ - Planning ledgers: [[migration/plan]], [[migration/inventory]], [[migration/unit-map]], [[migration/split-plan]].
647
+ ${inboxLine}
648
+ - Keep \`wiki_legacy*\` only as source/rollback archive after semantic completion; it is not deleted automatically.
649
+ <!-- PROJECT-WIKI-MIGRATION:END -->`;
650
+ }
651
+ function pruneCompletedMigrationJunk(legacyRoot) {
652
+ const results = [];
653
+ for (const file of exports.generatedMigrationInboxFiles) {
654
+ if (!isPrunableGeneratedMigrationInbox(file))
655
+ continue;
656
+ fs.unlinkSync((0, workspace_1.abs)(file));
657
+ results.push([file, "removed"]);
658
+ }
659
+ const remainingInboxes = exports.generatedMigrationInboxFiles.filter((file) => (0, workspace_1.exists)(file));
660
+ if ((0, workspace_1.exists)("wiki/startup.md")) {
661
+ results.push(["wiki/startup.md migration state", (0, workspace_1.upsertMarkedSection)("wiki/startup.md", "<!-- PROJECT-WIKI-MIGRATION:START -->", "<!-- PROJECT-WIKI-MIGRATION:END -->", completedMigrationStartupBlock(legacyRoot, remainingInboxes))]);
662
+ }
663
+ if ((0, workspace_1.exists)("wiki/index.md")) {
664
+ results.push(["wiki/index.md migration router", (0, workspace_1.upsertMarkedSection)("wiki/index.md", "<!-- PROJECT-WIKI-MIGRATION:START -->", "<!-- PROJECT-WIKI-MIGRATION:END -->", completedMigrationIndexBlock(remainingInboxes))]);
665
+ }
666
+ return results;
667
+ }
668
+ function isOpenMigrationRow(row) {
669
+ return row.inboxStatus === "pending" || row.inboxStatus === "needs-human-review" || row.semanticStatus === "pending semantic rewrite";
670
+ }
671
+ function sourceLooksGeneratedOrRouter(source) {
672
+ return /^(AGENTS|CLAUDE|GEMINI|README|startup|index)\.md$/.test(source)
673
+ || /^meta\/(operating-model|decision-policy|wiki-ops-v1-decisions|document-taxonomy)\.md$/.test(source);
674
+ }
675
+ const structuralHumanReviewHeadings = new Set([
676
+ "Active",
677
+ "Audience",
678
+ "Claim Boundary",
679
+ "Code-Proven Behavior",
680
+ "Code-proven behavior",
681
+ "Constraints",
682
+ "Core Scenarios",
683
+ "Current State",
684
+ "Diagnostics And Search",
685
+ "Discovery Rules",
686
+ "Evidence",
687
+ "Generated Output Ownership",
688
+ "Incremental Update Behavior",
689
+ "Inference",
690
+ "Mode Surface",
691
+ "Parser Backend Registry",
692
+ "Product",
693
+ "Product Direction",
694
+ "Project State",
695
+ "Query And Staleness",
696
+ "Read On Demand",
697
+ "Related Canonical Pages",
698
+ "Resolved",
699
+ "Retired",
700
+ "Routing And Inbox",
701
+ "Skill Installation",
702
+ "Success Criteria",
703
+ "TL;DR",
704
+ "Token Discipline",
705
+ "Tree-Sitter Mode",
706
+ "Workspace And Ownership Adapters",
707
+ ]);
708
+ function normalizedReviewText(value) {
709
+ return value.replace(/\\\|/g, "|").replace(/<br>/g, " ").replace(/\s+/g, " ").trim();
710
+ }
711
+ function isMarkdownTableDividerSummary(summary) {
712
+ return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(summary);
713
+ }
714
+ function isMarkdownTableHeaderSummary(summary) {
715
+ if (!/^\|.+\|$/.test(summary))
716
+ return false;
717
+ if (summary.includes("`"))
718
+ return false;
719
+ if (isMarkdownTableDividerSummary(summary))
720
+ return true;
721
+ const cells = summary.split("|").map((cell) => cell.trim()).filter(Boolean);
722
+ return cells.length > 1 && cells.every((cell) => /^[A-Z가-힣][A-Za-z가-힣 /-]{0,40}$/.test(cell));
723
+ }
724
+ function isStructuralHumanReviewRow(row) {
725
+ const summary = normalizedReviewText(row.summary);
726
+ const heading = normalizedReviewText(row.heading);
727
+ if (row.unitType === "heading" && (summary === heading || structuralHumanReviewHeadings.has(summary)))
728
+ return true;
729
+ if (row.unitType === "paragraph" && /^(Code-proven facts:|Code-proven behavior:|Evidence:|Inference:|Product framing:)$/i.test(summary))
730
+ return true;
731
+ if (isMarkdownTableDividerSummary(summary) || isMarkdownTableHeaderSummary(summary))
732
+ return true;
733
+ return false;
734
+ }
735
+ function confidenceCounts(rows) {
736
+ return {
737
+ high: rows.filter((row) => row.confidence === "high").length,
738
+ medium: rows.filter((row) => row.confidence === "medium").length,
739
+ low: rows.filter((row) => row.confidence === "low").length,
740
+ };
741
+ }
742
+ function groupBulkRows(rows, keyForRow) {
743
+ const groups = new Map();
744
+ for (const row of rows) {
745
+ const key = keyForRow(row);
746
+ groups.set(key, [...(groups.get(key) ?? []), row]);
747
+ }
748
+ return Array.from(groups.entries())
749
+ .map(([key, groupRows]) => {
750
+ const counts = confidenceCounts(groupRows);
751
+ return {
752
+ key,
753
+ rows: groupRows.length,
754
+ ...counts,
755
+ sources: Array.from(new Set(groupRows.map((row) => row.legacySource))).sort(),
756
+ targets: Array.from(new Set(groupRows.map((row) => row.target))).sort(),
757
+ areas: Array.from(new Set(groupRows.map((row) => row.area))).sort(),
758
+ sampleUnitIds: groupRows.slice(0, 5).map((row) => row.unitId),
759
+ sampleSummaries: groupRows.slice(0, 3).map((row) => row.summary),
760
+ };
761
+ })
762
+ .sort((left, right) => right.rows - left.rows || left.key.localeCompare(right.key));
763
+ }
764
+ function groupList(values, limit = 4) {
765
+ const shown = values.slice(0, limit).map((value) => markdownTableCell(value));
766
+ const hidden = values.length - shown.length;
767
+ return hidden > 0 ? `${shown.join("<br>")}<br>+${hidden} more` : shown.join("<br>") || "-";
768
+ }
769
+ function sampleList(values, limit = 3) {
770
+ const shown = values.slice(0, limit).map((value) => plainMarkdownTableCell(value));
771
+ const hidden = values.length - shown.length;
772
+ return hidden > 0 ? `${shown.join("<br>")}<br>+${hidden} more` : shown.join("<br>") || "-";
773
+ }
774
+ function confidenceMix(group) {
775
+ return `high ${group.high}, medium ${group.medium}, low ${group.low}`;
776
+ }
777
+ function renderTargetGroupRows(groups, suggestedStatus, limit = Number.POSITIVE_INFINITY) {
778
+ const selected = groups.slice(0, limit);
779
+ if (selected.length === 0)
780
+ return "| none | - | 0 | - | - | - |\n";
781
+ return selected.map((group) => `| ${markdownTableCell(group.key)} | ${group.rows} | ${confidenceMix(group)} | ${groupList(group.sources)} | ${groupList(group.areas)} | ${markdownTableCell(suggestedStatus)} |`).join("\n") + "\n";
782
+ }
783
+ function renderSourceGroupRows(groups, suggestedStatus, limit = Number.POSITIVE_INFINITY) {
784
+ const selected = groups.slice(0, limit);
785
+ if (selected.length === 0)
786
+ return "| none | 0 | - | - | - | - |\n";
787
+ return selected.map((group) => `| ${markdownTableCell(group.key)} | ${group.rows} | ${confidenceMix(group)} | ${groupList(group.targets, 3)} | ${groupList(group.areas)} | ${markdownTableCell(suggestedStatus)} |`).join("\n") + "\n";
788
+ }
789
+ function renderHumanReviewGroupRows(groups, suggestedStatus, limit = Number.POSITIVE_INFINITY) {
790
+ const selected = groups.slice(0, limit);
791
+ if (selected.length === 0)
792
+ return "| none | 0 | - | - | - | - | - |\n";
793
+ return selected.map((group) => `| ${markdownTableCell(group.key)} | ${group.rows} | ${confidenceMix(group)} | ${groupList(group.targets, 3)} | ${sampleList(group.sampleSummaries)} | ${groupList(group.sampleUnitIds, 3)} | ${markdownTableCell(suggestedStatus)} |`).join("\n") + "\n";
794
+ }
795
+ function buildMigrationBulkReviewPlan(rows) {
796
+ const openRows = rows.filter(isOpenMigrationRow);
797
+ const humanRows = openRows.filter((row) => row.inboxStatus === "needs-human-review" || row.confidence === "low");
798
+ const humanStructuralRows = humanRows.filter(isStructuralHumanReviewRow);
799
+ const humanContentRows = humanRows.filter((row) => !isStructuralHumanReviewRow(row));
800
+ const highRows = openRows.filter((row) => row.inboxStatus === "pending" && row.confidence === "high");
801
+ const mediumRows = openRows.filter((row) => row.inboxStatus === "pending" && row.confidence === "medium");
802
+ const sourceGroups = groupBulkRows(openRows, (row) => row.legacySource);
803
+ const singleTargetSourceGroups = sourceGroups.filter((group) => group.targets.length === 1 && group.low === 0);
804
+ const generatedSourceGroups = sourceGroups.filter((group) => sourceLooksGeneratedOrRouter(group.key));
805
+ return {
806
+ totalRows: rows.length,
807
+ openRows: openRows.length,
808
+ completedRows: rows.length - openRows.length,
809
+ highConfidenceRows: highRows.length,
810
+ mediumConfidenceRows: mediumRows.length,
811
+ humanReviewRows: humanRows.length,
812
+ humanReviewStructuralRows: humanStructuralRows.length,
813
+ humanReviewContentRows: humanContentRows.length,
814
+ highTargetGroups: groupBulkRows(highRows, (row) => row.target),
815
+ mediumTargetGroups: groupBulkRows(mediumRows, (row) => row.target),
816
+ singleTargetSourceGroups,
817
+ humanReviewSourceGroups: groupBulkRows(humanRows, (row) => row.legacySource),
818
+ humanReviewStructuralSourceGroups: groupBulkRows(humanStructuralRows, (row) => row.legacySource),
819
+ humanReviewContentSourceGroups: groupBulkRows(humanContentRows, (row) => row.legacySource),
820
+ generatedSourceGroups,
821
+ };
822
+ }
823
+ function bulkReviewRowsFromCoverage(coverageRows, reviewedRows = []) {
824
+ const reviewedByUnit = new Map(reviewedRows.map((row) => [row.legacyPath, row]));
825
+ return coverageRows.map((row) => {
826
+ const reviewed = reviewedByUnit.get(row.unitId);
827
+ const inboxStatus = reviewed?.inboxStatus ?? normalizeCoverageStatusForReview(row.status);
828
+ const semanticStatus = reviewed?.semanticStatus ?? semanticStatusForInboxStatus(inboxStatus);
829
+ return {
830
+ unitId: row.unitId,
831
+ legacySource: row.legacySource,
832
+ unitType: row.type,
833
+ heading: row.heading,
834
+ summary: row.summary,
835
+ target: reviewed?.target || row.target,
836
+ area: row.area,
837
+ confidence: row.confidence,
838
+ inboxStatus,
839
+ semanticStatus,
840
+ reason: row.reason,
841
+ };
842
+ });
843
+ }
844
+ function bulkReviewSummarySection(plan) {
845
+ return `## Bulk Review Summary
846
+
847
+ - Do not review all ${plan.totalRows} rows one by one; use [[migration/bulk-review]] to process batches.
848
+ - Completed rows: ${plan.completedRows}
849
+ - Open rows: ${plan.openRows}
850
+ - High-confidence bulk candidates: ${plan.highConfidenceRows} rows across ${plan.highTargetGroups.length} target groups.
851
+ - Medium-confidence batch candidates: ${plan.mediumConfidenceRows} rows across ${plan.mediumTargetGroups.length} target groups.
852
+ - Human-review priority: ${plan.humanReviewContentSourceGroups.length} content-bearing source batches (${plan.humanReviewContentRows} rows), plus ${plan.humanReviewStructuralRows} structural/layout rows handled with the same source review.
853
+
854
+ | Queue | Rows | Groups | Recommended handling |
855
+ | --- | ---: | ---: | --- |
856
+ | High-confidence target batches | ${plan.highConfidenceRows} | ${plan.highTargetGroups.length} | Rewrite or confirm target page, then bulk mark rows adopted or merged. |
857
+ | Medium-confidence target batches | ${plan.mediumConfidenceRows} | ${plan.mediumTargetGroups.length} | Review by target page, sample source evidence, then bulk mark rows. |
858
+ | Human-review content batches | ${plan.humanReviewContentRows} | ${plan.humanReviewContentSourceGroups.length} | Inspect by source page; do not handle as isolated rows unless a batch remains ambiguous. |
859
+ | Human-review structural/layout rows | ${plan.humanReviewStructuralRows} | ${plan.humanReviewStructuralSourceGroups.length} | Close with the source rewrite when headings, table headers, or boilerplate carry no standalone project truth. |
860
+ | Single-target source shortcuts | ${plan.singleTargetSourceGroups.reduce((sum, group) => sum + group.rows, 0)} | ${plan.singleTargetSourceGroups.length} | One source maps to one target; a file-level inbox decision can close the batch. |
861
+ | Generated/router source candidates | ${plan.generatedSourceGroups.reduce((sum, group) => sum + group.rows, 0)} | ${plan.generatedSourceGroups.length} | Compare against regenerated operating docs; mark superseded or resolved only when no project truth remains. |
862
+ `;
863
+ }
864
+ function renderMigrationBulkReviewDocument(plan, batchScope) {
865
+ return `${(0, templates_1.metadata)("migration-bulk-review", "on-demand", "wiki/meta/wiki-ops-v1-decisions.md", "migration coverage or confidence grouping changes")}
866
+ # Migration Bulk Review Plan
867
+
868
+ ## TL;DR
869
+
870
+ - Scope: ${batchScope}
871
+ - Total rows: ${plan.totalRows}
872
+ - Open rows: ${plan.openRows}
873
+ - Completed rows: ${plan.completedRows}
874
+ - The low-confidence queue is ${plan.humanReviewContentSourceGroups.length} content-bearing source batches, not ${plan.humanReviewRows} isolated row decisions.
875
+ - Structural/layout low rows: ${plan.humanReviewStructuralRows}; content-bearing low rows: ${plan.humanReviewContentRows}.
876
+ - This page is advisory. It never changes \`wiki/migration/coverage.md\` statuses by itself.
877
+
878
+ ${bulkReviewSummarySection(plan)}
879
+
880
+ ## Human-Review Triage
881
+
882
+ Low-confidence rows are not intended to be reviewed one by one. Start with content-bearing source batches, then close structural/layout rows while rewriting or rejecting that same source.
883
+
884
+ | Queue | Rows | Source batches | What to do |
885
+ | --- | ---: | ---: | --- |
886
+ | Content-bearing low rows | ${plan.humanReviewContentRows} | ${plan.humanReviewContentSourceGroups.length} | Review the legacy source page once, adopt/retarget/reject the useful meaning, then mark its low rows together. |
887
+ | Structural/layout low rows | ${plan.humanReviewStructuralRows} | ${plan.humanReviewStructuralSourceGroups.length} | Treat headings, TL;DR labels, table headers, and separators as source-structure evidence; usually close them with the source page decision. |
888
+ | Total low/needs-human-review rows | ${plan.humanReviewRows} | ${plan.humanReviewSourceGroups.length} | Escalate to row-level review only when a source batch mixes unrelated truths that cannot be split safely. |
889
+
890
+ ## Single-Target Source Shortcuts
891
+
892
+ Use these when a legacy source maps to one target and has no low-confidence rows. A compatible file-level inbox status can close the whole source batch.
893
+
894
+ | Legacy Source | Rows | Confidence Mix | Target | Areas | Suggested status |
895
+ | --- | ---: | --- | --- | --- | --- |
896
+ ${renderSourceGroupRows(plan.singleTargetSourceGroups, "adopted or merged after target rewrite")}
897
+
898
+ ## High-Confidence Target Batches
899
+
900
+ | Suggested Target | Rows | Confidence Mix | Sources | Areas | Suggested status |
901
+ | --- | ---: | --- | --- | --- | --- |
902
+ ${renderTargetGroupRows(plan.highTargetGroups, "adopted or merged after target rewrite")}
903
+
904
+ ## Medium-Confidence Target Batches
905
+
906
+ | Suggested Target | Rows | Confidence Mix | Sources | Areas | Suggested status |
907
+ | --- | ---: | --- | --- | --- | --- |
908
+ ${renderTargetGroupRows(plan.mediumTargetGroups, "review by target page before adopting")}
909
+
910
+ ## Content-Bearing Human-Review Batches
911
+
912
+ These are the practical manual review units. Review each legacy source page once; use the sample rows to find the relevant evidence quickly.
913
+
914
+ | Legacy Source | Rows | Confidence Mix | Targets | Sample summaries | Sample unit ids | Suggested status |
915
+ | --- | ---: | --- | --- | --- | --- | --- |
916
+ ${renderHumanReviewGroupRows(plan.humanReviewContentSourceGroups, "source-batch adopt, retarget, resolve, or reject")}
917
+
918
+ ## Structural Human-Review Batches
919
+
920
+ These rows are low-confidence because they are headings, table scaffolding, or boilerplate labels. They rarely require standalone decisions.
921
+
922
+ | Legacy Source | Rows | Confidence Mix | Targets | Sample summaries | Sample unit ids | Suggested status |
923
+ | --- | ---: | --- | --- | --- | --- | --- |
924
+ ${renderHumanReviewGroupRows(plan.humanReviewStructuralSourceGroups, "close with the same source-page decision")}
925
+
926
+ ## All Human-Review Source Groups
927
+
928
+ These rows have low confidence or are explicitly marked needs-human-review.
929
+
930
+ | Legacy Source | Rows | Confidence Mix | Targets | Areas | Suggested status |
931
+ | --- | ---: | --- | --- | --- | --- |
932
+ ${renderSourceGroupRows(plan.humanReviewSourceGroups, "manual adopt, resolve, reject, or retarget")}
933
+
934
+ ## Generated Or Router Source Candidates
935
+
936
+ These sources often overlap with regenerated bootstrap/wiki operating documents. Do not discard them automatically; compare for project-specific truth first.
937
+
938
+ | Legacy Source | Rows | Confidence Mix | Targets | Areas | Suggested status |
939
+ | --- | ---: | --- | --- | --- | --- |
940
+ ${renderSourceGroupRows(plan.generatedSourceGroups, "superseded or resolved only after comparison")}
941
+ `;
942
+ }
90
943
  function timestampSuffix() {
91
944
  return new Date().toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "_");
92
945
  }
@@ -118,13 +971,19 @@ function migrationTargetForKind(kind) {
118
971
  return "wiki/decisions/migration-inbox.md";
119
972
  if (kind === "source")
120
973
  return "wiki/sources/migration-inbox.md";
974
+ if (kind === "meta")
975
+ return "wiki/meta/migration-inbox.md";
121
976
  return "wiki/canonical/migration-inbox.md";
122
977
  }
123
978
  function runMigrationMode(migrationState) {
124
979
  const legacyPath = migrationState.legacyPath;
125
980
  const markdownFiles = legacyPath && (0, workspace_1.exists)(legacyPath) ? (0, wiki_files_1.walkMarkdownFiles)((0, workspace_1.abs)(legacyPath), [], (0, workspace_1.abs)(legacyPath)) : [];
126
- const items = markdownFiles.map((file) => {
981
+ const fileRecords = markdownFiles.map((file) => {
127
982
  const text = (0, workspace_1.read)(file.path);
983
+ return { file, text, formOnlyReason: formOnlyMigrationDocumentReason(file.basePath, text) };
984
+ });
985
+ const skippedFormOnlyFiles = fileRecords.filter((record) => record.formOnlyReason);
986
+ const items = fileRecords.filter((record) => !record.formOnlyReason).map(({ file, text }) => {
128
987
  return {
129
988
  path: file.path,
130
989
  legacyPath: file.basePath,
@@ -138,11 +997,15 @@ function runMigrationMode(migrationState) {
138
997
  canonical: items.filter((item) => item.kind === "canonical"),
139
998
  decision: items.filter((item) => item.kind === "decision"),
140
999
  source: items.filter((item) => item.kind === "source"),
1000
+ meta: items.filter((item) => item.kind === "meta"),
141
1001
  other: items.filter((item) => item.kind === "other"),
142
1002
  };
143
1003
  const inventoryRows = items.length === 0
144
1004
  ? "| none | - | - | 0 | - |\n"
145
- : items.map((item) => `| ${markdownTableCell(item.path)} | ${item.kind} | ${markdownTableCell(item.title)} | ${item.bytes} | ${markdownTableCell(item.summary)} |`).join("\n") + "\n";
1005
+ : items.map((item) => `| ${markdownTableCell(item.path)} | ${item.kind} | ${plainMarkdownTableCell(item.title)} | ${item.bytes} | ${plainMarkdownTableCell(item.summary)} |`).join("\n") + "\n";
1006
+ const skippedRows = skippedFormOnlyFiles.length === 0
1007
+ ? "| none | - | - |\n"
1008
+ : skippedFormOnlyFiles.map((record) => `| ${markdownTableCell(record.file.path)} | ${markdownTableCell(record.formOnlyReason)} | ${plainMarkdownTableCell((0, wiki_files_1.firstHeading)(record.text, record.file.path))} |`).join("\n") + "\n";
146
1009
  const inventory = `${(0, templates_1.metadata)("migration-inventory", "on-demand", "wiki/meta/wiki-ops-v1-decisions.md", "migration scan is rerun")}
147
1010
  # Migration Inventory
148
1011
 
@@ -150,12 +1013,71 @@ function runMigrationMode(migrationState) {
150
1013
 
151
1014
  - Generated: ${workspace_1.today}
152
1015
  - Legacy root: ${legacyPath || "none"}
153
- - Markdown files: ${items.length}
1016
+ - Legacy markdown files: ${markdownFiles.length}
1017
+ - Migratable markdown files: ${items.length}
1018
+ - Skipped form-only/template files: ${skippedFormOnlyFiles.length}
154
1019
  - Legacy files are not copied directly into the new wiki; they are mapped to rewrite inboxes.
155
1020
 
1021
+ ## Migratable Files
1022
+
156
1023
  | Legacy Source | Classification | Title | Size (bytes) | Summary |
157
1024
  | --- | --- | --- | ---: | --- |
158
- ${inventoryRows}`;
1025
+ ${inventoryRows}
1026
+ ## Skipped Form-Only Files
1027
+
1028
+ | Legacy Source | Reason | Title |
1029
+ | --- | --- | --- |
1030
+ ${skippedRows}`;
1031
+ const units = items.flatMap((item) => extractMigrationUnits(item.legacyPath, (0, workspace_1.read)(item.path)));
1032
+ const unitsByStorage = {
1033
+ canonical: units.filter((unit) => unit.classification.storage === "canonical"),
1034
+ decisions: units.filter((unit) => unit.classification.storage === "decisions"),
1035
+ sources: units.filter((unit) => unit.classification.storage === "sources"),
1036
+ meta: units.filter((unit) => unit.classification.storage === "meta"),
1037
+ };
1038
+ const batchScope = migrationBatchScope(legacyPath || "none");
1039
+ const initialBulkPlan = buildMigrationBulkReviewPlan(bulkReviewRowsFromCoverage(coverageRowsFromUnits(units)));
1040
+ const bulkReview = renderMigrationBulkReviewDocument(initialBulkPlan, batchScope);
1041
+ const coverage = `${(0, templates_1.metadata)("migration-coverage", "on-demand", "wiki/meta/wiki-ops-v1-decisions.md", "migration unit coverage statuses change")}
1042
+ # Migration Coverage Ledger
1043
+
1044
+ ## TL;DR
1045
+
1046
+ - Generated: ${workspace_1.today}
1047
+ - Legacy root: ${legacyPath || "none"}
1048
+ - Legacy meaning units: ${units.length}
1049
+ - Every legacy heading, paragraph, list item, table row, and code block should remain accounted for.
1050
+ - Status values: pending, adopted, merged, superseded, rejected, resolved, needs-human-review.
1051
+ - \`adopted\` and \`merged\` rows require a new-wiki target under \`wiki/canonical/\`, \`wiki/decisions/\`, \`wiki/sources/\`, or \`wiki/meta/\`.
1052
+
1053
+ | Unit ID | Legacy Source | Type | Heading | Summary | Status | Target | Note | Area | Confidence | Reason |
1054
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
1055
+ ${coverageTableRows(units)}`;
1056
+ const unitMap = `${(0, templates_1.metadata)("migration-unit-map", "on-demand", "wiki/meta/wiki-ops-v1-decisions.md", "migration classification or target suggestions change")}
1057
+ # Migration Unit Map
1058
+
1059
+ ## TL;DR
1060
+
1061
+ - Generated: ${workspace_1.today}
1062
+ - Legacy meaning units: ${units.length}
1063
+ - Each row classifies one legacy meaning unit against the service/product document taxonomy.
1064
+ - Low-confidence rows start as needs-human-review.
1065
+
1066
+ | Unit ID | Legacy Source | Heading Path | Area | Storage | Confidence | Suggested Target | Reason | Summary | Status |
1067
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
1068
+ ${unitMapRows(units)}`;
1069
+ const splitPlan = `${(0, templates_1.metadata)("migration-split-plan", "on-demand", "wiki/meta/wiki-ops-v1-decisions.md", "migration split grouping changes")}
1070
+ # Migration Split Plan
1071
+
1072
+ ## TL;DR
1073
+
1074
+ - Generated: ${workspace_1.today}
1075
+ - Suggested target pages: ${new Set(units.map((unit) => unit.classification.target)).size}
1076
+ - Use this as the rewrite plan when one legacy page mixes API specs, features, UX, policy, QA, operations, or source evidence.
1077
+
1078
+ | Suggested Target | Area | Storage | Lowest Confidence | Unit Count | Legacy Units |
1079
+ | --- | --- | --- | --- | ---: | --- |
1080
+ ${splitPlanRows(units)}`;
159
1081
  const plan = `${(0, templates_1.metadata)("migration-plan", "short", "wiki/meta/wiki-ops-v1-decisions.md", "migration procedure or status changes")}
160
1082
  # Migration Plan
161
1083
 
@@ -164,6 +1086,7 @@ ${inventoryRows}`;
164
1086
  - Generated: ${workspace_1.today}
165
1087
  - Preparation: ${migrationState.note}
166
1088
  - The new \`./wiki\` uses the standard structure.
1089
+ - Form-only/template files are recorded in inventory but excluded from meaning-unit migration.
167
1090
  - Next step: review inbox items and absorb useful meaning into canonical, decisions, sources, or meta docs.
168
1091
 
169
1092
  ## Counts
@@ -173,75 +1096,92 @@ ${inventoryRows}`;
173
1096
  | canonical candidates | ${byKind.canonical.length} |
174
1097
  | decision candidates | ${byKind.decision.length} |
175
1098
  | source candidates | ${byKind.source.length} |
1099
+ | meta candidates | ${byKind.meta.length} |
176
1100
  | other candidates | ${byKind.other.length} |
1101
+ | skipped form-only/template files | ${skippedFormOnlyFiles.length} |
1102
+ | canonical units | ${unitsByStorage.canonical.length} |
1103
+ | decision units | ${unitsByStorage.decisions.length} |
1104
+ | source units | ${unitsByStorage.sources.length} |
1105
+ | meta units | ${unitsByStorage.meta.length} |
177
1106
  `;
178
- const verificationRows = items.length === 0
1107
+ const verificationRows = units.length === 0
179
1108
  ? "| none | - | - | pass | - |\n"
180
- : items.map((item) => `| ${markdownTableCell(item.path)} | ${item.kind} | ${migrationTargetForKind(item.kind)} | mapped | pending semantic rewrite |`).join("\n") + "\n";
1109
+ : units.map((unit) => {
1110
+ const semanticStatus = unit.classification.confidence === "low" ? "needs-human-review" : "pending semantic rewrite";
1111
+ return `| ${markdownTableCell(unit.id)} | ${(0, taxonomy_1.storageToMigrationKind)(unit.classification.storage)} | ${markdownTableCell(unit.classification.target)} | mapped | ${semanticStatus} |`;
1112
+ }).join("\n") + "\n";
181
1113
  const verification = `${(0, templates_1.metadata)("migration-verification", "on-demand", "wiki/meta/wiki-ops-v1-decisions.md", "migration inbox items are adopted, rejected, or rescanned")}
182
1114
  # Migration Verification
183
1115
 
184
1116
  ## TL;DR
185
1117
 
186
1118
  - legacy root: ${legacyPath || "none"}
187
- - legacy markdown files: ${items.length}
188
- - mapped files: ${items.length}
189
- - coverage: ${items.length === markdownFiles.length ? "pass" : "fail"}
190
- - This verifies file coverage only. Semantic completeness is confirmed after inbox statuses are resolved.
1119
+ - legacy markdown files: ${markdownFiles.length}
1120
+ - skipped form-only/template files: ${skippedFormOnlyFiles.length}
1121
+ - mapped units: ${units.length}
1122
+ - coverage: ${items.length + skippedFormOnlyFiles.length === markdownFiles.length ? "pass" : "fail"}
1123
+ - This verifies unit coverage. Semantic completeness is confirmed after coverage or inbox statuses are resolved.
1124
+
1125
+ ${completionScopeSection(batchScope)}
191
1126
 
192
1127
  | Legacy Source | Classification | New Wiki Target | Coverage | Semantic Status |
193
1128
  | --- | --- | --- | --- | --- |
194
1129
  ${verificationRows}`;
1130
+ const review = `${(0, templates_1.metadata)("migration-review", "on-demand", "wiki/meta/wiki-ops-v1-decisions.md", "migration inbox statuses change")}
1131
+ # Migration Review
1132
+
1133
+ ## TL;DR
1134
+
1135
+ - generated: ${workspace_1.today}
1136
+ - total legacy rows: ${units.length}
1137
+ - skipped form-only/template files: ${skippedFormOnlyFiles.length}
1138
+ - semantic migration complete: no, the ${batchScope} still has unresolved rows
1139
+ - Run \`--review-migration\` after updating coverage or migration inbox statuses.
1140
+
1141
+ ${completionScopeSection(batchScope)}
1142
+
1143
+ ${bulkReviewSummarySection(initialBulkPlan)}
1144
+
1145
+ | Legacy Source | Classification | Inbox Status | Semantic Status | Evidence |
1146
+ | --- | --- | --- | --- | --- |
1147
+ ${units.length === 0 ? "| none | - | - | - | - |\n" : units.map((unit) => `| ${markdownTableCell(unit.id)} | ${(0, taxonomy_1.storageToMigrationKind)(unit.classification.storage)} | ${unit.classification.confidence === "low" ? "needs-human-review" : "pending"} | ${unit.classification.confidence === "low" ? "needs-human-review" : "pending semantic rewrite"} | ${plainMarkdownTableCell(unit.classification.reason)} |`).join("\n") + "\n"}`;
195
1148
  const migrationStartupBlock = `<!-- PROJECT-WIKI-MIGRATION:START -->
196
1149
  ## Migration State
197
1150
 
198
1151
  - ${workspace_1.today}: preserved existing wiki at \`${legacyPath || "no wiki_legacy"}\` and regenerated the standard wiki structure.
199
- - Scanned ${items.length} legacy markdown files and created migration inventory, plan, verification, and inbox files.
1152
+ - Scanned ${markdownFiles.length} legacy markdown files, skipped ${skippedFormOnlyFiles.length} form-only/template files, and mapped ${units.length} meaning units; created inventory, unit map, split plan, coverage, verification, review, and inbox files.
200
1153
  - Do not delete \`${legacyPath || "wiki_legacy"}\` until all migration inbox items are adopted/rejected/resolved and needs-human-review is 0.
1154
+ - Migration completion status is scoped to this batch only. For a future fresh rebuild request, treat current \`wiki/\` as the legacy source unless the user says otherwise.
201
1155
  <!-- PROJECT-WIKI-MIGRATION:END -->`;
202
1156
  const migrationIndexBlock = `<!-- PROJECT-WIKI-MIGRATION:START -->
203
1157
  ## Migration
204
1158
 
205
- - [[migration/plan]]
206
- - Read when: migration procedure or status matters.
207
- - Update when: migration procedure or state changes.
208
- - Token budget: short.
209
- - [[migration/inventory]]
210
- - Read when: legacy markdown file list and classification matter.
211
- - Update when: migration is rescanned.
212
- - Token budget: on-demand.
213
- - [[migration/verification]]
214
- - Read when: legacy file coverage or semantic migration status matters.
215
- - Update when: migration inbox statuses change.
216
- - Token budget: on-demand.
217
- - [[migration/review]]
218
- - Read when: semantic migration review status matters.
219
- - Update when: \`--review-migration\` syncs migration state.
220
- - Token budget: on-demand.
221
- - [[canonical/migration-inbox]]
222
- - Read when: absorbing legacy canonical candidates.
223
- - Update when: candidates are adopted/rejected/resolved/needs-human-review.
224
- - Token budget: medium.
225
- - [[decisions/migration-inbox]]
226
- - Read when: absorbing legacy decision candidates.
227
- - Update when: candidates are adopted/rejected/resolved/needs-human-review.
228
- - Token budget: medium.
229
- - [[sources/migration-inbox]]
230
- - Read when: absorbing legacy source candidates.
231
- - Update when: candidates are adopted/rejected/resolved/needs-human-review.
232
- - Token budget: medium.
1159
+ - [[migration/plan]]: migration procedure, fresh rebuild procedure, and counts.
1160
+ - [[migration/inventory]]: legacy file list and file-level classification.
1161
+ - [[migration/unit-map]]: meaning-unit taxonomy classification and suggested target pages.
1162
+ - [[migration/split-plan]]: target-page grouping for mixed legacy pages.
1163
+ - [[migration/coverage]]: editable unit status, target, and note ledger.
1164
+ - [[migration/verification]]: current unit coverage and semantic status.
1165
+ - [[migration/review]]: regenerated summary from coverage/inbox status.
1166
+ - [[migration/bulk-review]]: batch review queues so humans do not inspect every unit one by one.
1167
+ - [[canonical/migration-inbox]], [[decisions/migration-inbox]], [[sources/migration-inbox]]: file-level adoption inboxes.
233
1168
  <!-- PROJECT-WIKI-MIGRATION:END -->`;
234
1169
  const results = [];
235
1170
  (0, workspace_1.mkdirp)("wiki/migration");
236
1171
  results.push(["wiki/migration/inventory.md", (0, workspace_1.writeManaged)("wiki/migration/inventory.md", inventory)]);
1172
+ results.push(["wiki/migration/unit-map.md", (0, workspace_1.writeManaged)("wiki/migration/unit-map.md", unitMap)]);
1173
+ results.push(["wiki/migration/split-plan.md", (0, workspace_1.writeManaged)("wiki/migration/split-plan.md", splitPlan)]);
1174
+ results.push(["wiki/migration/coverage.md", (0, workspace_1.writeManaged)("wiki/migration/coverage.md", coverage)]);
237
1175
  results.push(["wiki/migration/plan.md", (0, workspace_1.writeManaged)("wiki/migration/plan.md", plan)]);
1176
+ results.push(["wiki/migration/review.md", (0, workspace_1.writeManaged)("wiki/migration/review.md", review)]);
238
1177
  results.push(["wiki/migration/verification.md", (0, workspace_1.writeManaged)("wiki/migration/verification.md", verification)]);
1178
+ results.push(["wiki/migration/bulk-review.md", (0, workspace_1.writeManaged)("wiki/migration/bulk-review.md", bulkReview)]);
239
1179
  results.push(["wiki/canonical/migration-inbox.md", (0, workspace_1.writeManaged)("wiki/canonical/migration-inbox.md", buildInbox("Canonical Migration Inbox", "Legacy content that may belong in current project truth.", byKind.canonical.concat(byKind.other)))]);
240
1180
  results.push(["wiki/decisions/migration-inbox.md", (0, workspace_1.writeManaged)("wiki/decisions/migration-inbox.md", buildInbox("Decision Migration Inbox", "Legacy content that may belong in project decision history.", byKind.decision))]);
241
1181
  results.push(["wiki/sources/migration-inbox.md", (0, workspace_1.writeManaged)("wiki/sources/migration-inbox.md", buildInbox("Source Migration Inbox", "Legacy content that may belong in source summaries.", byKind.source))]);
242
1182
  results.push(["wiki/startup.md migration state", (0, workspace_1.upsertMarkedSection)("wiki/startup.md", "<!-- PROJECT-WIKI-MIGRATION:START -->", "<!-- PROJECT-WIKI-MIGRATION:END -->", migrationStartupBlock)]);
243
1183
  results.push(["wiki/index.md migration router", (0, workspace_1.upsertMarkedSection)("wiki/index.md", "<!-- PROJECT-WIKI-MIGRATION:START -->", "<!-- PROJECT-WIKI-MIGRATION:END -->", migrationIndexBlock)]);
244
- return { results, total: items.length, legacyPath };
1184
+ return { results, total: markdownFiles.length, legacyPath };
245
1185
  }
246
1186
  function normalizeMigrationStatus(status) {
247
1187
  const value = String(status || "").trim().toLowerCase();
@@ -270,11 +1210,69 @@ function migrationInboxStatusMap() {
270
1210
  const source = cells[0];
271
1211
  if (!source)
272
1212
  continue;
273
- statuses.set(source, { status: normalizeMigrationStatus(cells[3]), inbox: file });
1213
+ const entry = { status: normalizeMigrationStatus(cells[3]), inbox: file };
1214
+ statuses.set(source, entry);
1215
+ const rootStripped = source.replace(/^wiki_legacy(?:_[^/]+)?\//, "");
1216
+ if (rootStripped !== source)
1217
+ statuses.set(rootStripped, entry);
274
1218
  }
275
1219
  }
276
1220
  return statuses;
277
1221
  }
1222
+ function normalizeCoverageStatusForReview(status) {
1223
+ const value = String(status || "").trim().toLowerCase();
1224
+ if (value === "merged")
1225
+ return "adopted";
1226
+ if (value === "superseded")
1227
+ return "resolved";
1228
+ if (isMigrationInboxStatus(value))
1229
+ return value;
1230
+ return "pending";
1231
+ }
1232
+ function migrationCoverageStatusMap() {
1233
+ const statuses = new Map();
1234
+ if (!(0, workspace_1.exists)("wiki/migration/coverage.md"))
1235
+ return statuses;
1236
+ for (const cells of (0, wiki_files_1.parseMarkdownTableRows)((0, workspace_1.read)("wiki/migration/coverage.md"), 8).filter((row) => row[0] !== "Unit ID")) {
1237
+ const source = cells[0];
1238
+ if (!source)
1239
+ continue;
1240
+ statuses.set(source, { status: normalizeCoverageStatusForReview(cells[5]), inbox: "wiki/migration/coverage.md" });
1241
+ }
1242
+ return statuses;
1243
+ }
1244
+ function legacySourceFromUnitId(value) {
1245
+ return value.split("#u", 1)[0] ?? value;
1246
+ }
1247
+ function sourcesSafeForFileLevelFallback(rows) {
1248
+ const targetsBySource = new Map();
1249
+ for (const row of rows) {
1250
+ const source = legacySourceFromUnitId(row.legacyPath);
1251
+ const targets = targetsBySource.get(source) ?? new Set();
1252
+ targets.add(row.target);
1253
+ targetsBySource.set(source, targets);
1254
+ }
1255
+ return new Set(Array.from(targetsBySource.entries()).filter(([, targets]) => targets.size === 1).map(([source]) => source));
1256
+ }
1257
+ function migrationStatusForRow(row, coverageStatuses, inboxStatuses, fileLevelFallbackSources) {
1258
+ const coverage = coverageStatuses.get(row.legacyPath);
1259
+ if (coverage && coverage.status !== "pending")
1260
+ return coverage;
1261
+ const exactInbox = inboxStatuses.get(row.legacyPath);
1262
+ if (exactInbox)
1263
+ return exactInbox;
1264
+ const source = legacySourceFromUnitId(row.legacyPath);
1265
+ const sourceInbox = inboxStatuses.get(source);
1266
+ if (sourceInbox && fileLevelFallbackSources.has(source))
1267
+ return sourceInbox;
1268
+ if (sourceInbox) {
1269
+ const note = `file-level inbox row ignored for mixed-target legacy source: ${sourceInbox.inbox}`;
1270
+ return coverage ? { status: coverage.status, inbox: `${note}; using ${coverage.inbox}` } : { status: "needs-human-review", inbox: note };
1271
+ }
1272
+ if (coverage)
1273
+ return coverage;
1274
+ return { status: "needs-human-review", inbox: "missing migration coverage or inbox row" };
1275
+ }
278
1276
  function semanticStatusForInboxStatus(status) {
279
1277
  if (["adopted", "rejected", "resolved", "needs-human-review"].includes(status))
280
1278
  return status;
@@ -292,11 +1290,12 @@ function runReviewMigrationMode() {
292
1290
  target: cells[2] ?? "",
293
1291
  coverage: cells[3] ?? "",
294
1292
  }));
1293
+ const coverageStatuses = migrationCoverageStatusMap();
295
1294
  const inboxStatuses = migrationInboxStatusMap();
1295
+ const fileLevelFallbackSources = sourcesSafeForFileLevelFallback(verificationRows);
296
1296
  const reviewedRows = verificationRows.map((row) => {
297
- const inbox = inboxStatuses.get(row.legacyPath);
298
- const status = inbox ? inbox.status : "needs-human-review";
299
- return { ...row, inboxStatus: status, semanticStatus: semanticStatusForInboxStatus(status), note: inbox ? inbox.inbox : "missing migration inbox row" };
1297
+ const statusEntry = migrationStatusForRow(row, coverageStatuses, inboxStatuses, fileLevelFallbackSources);
1298
+ return { ...row, inboxStatus: statusEntry.status, semanticStatus: semanticStatusForInboxStatus(statusEntry.status), note: statusEntry.inbox };
300
1299
  });
301
1300
  const counts = reviewedRows.reduce((acc, row) => {
302
1301
  acc[row.inboxStatus] = (acc[row.inboxStatus] || 0) + 1;
@@ -305,6 +1304,12 @@ function runReviewMigrationMode() {
305
1304
  const pending = counts.pending || 0;
306
1305
  const needsHuman = counts["needs-human-review"] || 0;
307
1306
  const complete = pending === 0 && needsHuman === 0;
1307
+ const legacyRoot = (verificationText.match(/^- legacy root:\s*(.+)$/m) || [])[1] || "unknown";
1308
+ const batchScope = migrationBatchScope(legacyRoot);
1309
+ const completionValue = semanticCompletionValue(complete, batchScope);
1310
+ const coverageRows = (0, workspace_1.exists)("wiki/migration/coverage.md") ? parseMigrationCoverageRows((0, workspace_1.read)("wiki/migration/coverage.md")) : [];
1311
+ const bulkPlan = buildMigrationBulkReviewPlan(bulkReviewRowsFromCoverage(coverageRows, reviewedRows));
1312
+ const bulkReview = renderMigrationBulkReviewDocument(bulkPlan, batchScope);
308
1313
  const reviewRows = reviewedRows.length === 0
309
1314
  ? "| none | - | - | - | - |\n"
310
1315
  : reviewedRows.map((row) => `| ${markdownTableCell(row.legacyPath)} | ${row.kind} | ${row.inboxStatus} | ${row.semanticStatus} | ${markdownTableCell(row.note)} |`).join("\n") + "\n";
@@ -320,7 +1325,11 @@ function runReviewMigrationMode() {
320
1325
  - resolved: ${counts.resolved || 0}
321
1326
  - pending: ${pending}
322
1327
  - needs-human-review: ${needsHuman}
323
- - semantic migration complete: ${complete ? "yes" : "no"}
1328
+ - semantic migration complete: ${completionValue}
1329
+
1330
+ ${completionScopeSection(batchScope)}
1331
+
1332
+ ${bulkReviewSummarySection(bulkPlan)}
324
1333
 
325
1334
  | Legacy Source | Classification | Inbox Status | Semantic Status | Evidence |
326
1335
  | --- | --- | --- | --- | --- |
@@ -328,27 +1337,32 @@ ${reviewRows}`;
328
1337
  const verificationRowsText = reviewedRows.length === 0
329
1338
  ? "| none | - | - | pass | - |\n"
330
1339
  : reviewedRows.map((row) => `| ${markdownTableCell(row.legacyPath)} | ${row.kind} | ${row.target} | ${row.coverage} | ${row.semanticStatus} |`).join("\n") + "\n";
331
- const legacyRoot = (verificationText.match(/^- legacy root:\s*(.+)$/m) || [])[1] || "unknown";
332
1340
  const verification = `${(0, templates_1.metadata)("migration-verification", "on-demand", "wiki/meta/wiki-ops-v1-decisions.md", "migration inbox items are adopted, rejected, resolved, or marked needs-human-review")}
333
1341
  # Migration Verification
334
1342
 
335
1343
  ## TL;DR
336
1344
 
337
1345
  - legacy root: ${legacyRoot}
338
- - legacy markdown files: ${reviewedRows.length}
339
- - mapped files: ${reviewedRows.filter((row) => row.coverage === "mapped").length}
1346
+ - legacy rows: ${reviewedRows.length}
1347
+ - mapped rows: ${reviewedRows.filter((row) => row.coverage === "mapped").length}
340
1348
  - coverage: ${reviewedRows.every((row) => row.coverage === "mapped") ? "pass" : "fail"}
341
- - semantic migration complete: ${complete ? "yes" : "no"}
1349
+ - semantic migration complete: ${completionValue}
342
1350
  - pending: ${pending}
343
1351
  - needs-human-review: ${needsHuman}
344
1352
 
1353
+ ${completionScopeSection(batchScope)}
1354
+
345
1355
  | Legacy Source | Classification | New Wiki Target | Coverage | Semantic Status |
346
1356
  | --- | --- | --- | --- | --- |
347
1357
  ${verificationRowsText}`;
348
1358
  const results = [
349
1359
  ["wiki/migration/review.md", (0, workspace_1.writeManaged)("wiki/migration/review.md", review)],
350
1360
  ["wiki/migration/verification.md", (0, workspace_1.writeManaged)("wiki/migration/verification.md", verification)],
1361
+ ["wiki/migration/bulk-review.md", (0, workspace_1.writeManaged)("wiki/migration/bulk-review.md", bulkReview)],
351
1362
  ];
1363
+ if (complete) {
1364
+ results.push(...pruneCompletedMigrationJunk(legacyRoot));
1365
+ }
352
1366
  console.log("Project wiki migration review complete.");
353
1367
  for (const [relativePath, status] of results)
354
1368
  console.log(`${String(status).padEnd(7)} ${relativePath}`);