@imenam/mcp-documentor 1.0.99 → 1.0.101

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 (75) hide show
  1. package/README.md +9 -1
  2. package/dist/src/core/comment-store.d.ts +13 -2
  3. package/dist/src/core/comment-store.js +44 -6
  4. package/dist/src/core/embedding-store.js +12 -12
  5. package/dist/src/core/history-store.js +14 -14
  6. package/dist/src/gui-worker.js +17 -8
  7. package/dist/src/index.js +17 -4
  8. package/dist/src/tools/comments.d.ts +1 -1
  9. package/dist/src/tools/comments.js +51 -8
  10. package/gui/README.md +75 -0
  11. package/gui/dist/assets/{DocContent-ByGAvCaA.js → DocContent-D5CLsUKq.js} +51 -51
  12. package/gui/dist/assets/{abnfDiagram-WEYOIA3C-C9H-rU6M.js → abnfDiagram-WEYOIA3C-BqeAt-Gs.js} +1 -1
  13. package/gui/dist/assets/{arc-vCYNE25X.js → arc-Btzq4ujL.js} +1 -1
  14. package/gui/dist/assets/{architectureDiagram-ZHVYKT53-BKQrc7ec.js → architectureDiagram-ZHVYKT53-64qym6UB.js} +1 -1
  15. package/gui/dist/assets/{blockDiagram-RJAE7QA3-D_dicGXX.js → blockDiagram-RJAE7QA3-WAx3357U.js} +1 -1
  16. package/gui/dist/assets/{c4Diagram-XDPPPE54-CZFnurNd.js → c4Diagram-XDPPPE54-JfnC4lFj.js} +1 -1
  17. package/gui/dist/assets/channel-CAKB_FDX.js +1 -0
  18. package/gui/dist/assets/{chunk-2E7WHVMD-B-MISuU2.js → chunk-2E7WHVMD-ChRTmoE5.js} +1 -1
  19. package/gui/dist/assets/{chunk-2Q5K7J3B-2a52SE3x.js → chunk-2Q5K7J3B-BLmYMU05.js} +1 -1
  20. package/gui/dist/assets/{chunk-4EMNKR2T-DPfeHeMi.js → chunk-4EMNKR2T-ByrvV5eh.js} +1 -1
  21. package/gui/dist/assets/{chunk-55XCU76K-CuJqVzJg.js → chunk-55XCU76K-CR5a5uPD.js} +1 -1
  22. package/gui/dist/assets/{chunk-5VM5RSS4-CjT3LcWh.js → chunk-5VM5RSS4-Pitn_Zvk.js} +1 -1
  23. package/gui/dist/assets/{chunk-JWPE2WC7-pBWIoivI.js → chunk-JWPE2WC7-DGhyzwFs.js} +1 -1
  24. package/gui/dist/assets/{chunk-VQ5W62QI-B2vesLrV.js → chunk-VQ5W62QI-DgcZxhBM.js} +1 -1
  25. package/gui/dist/assets/{chunk-XXDRQBXY-COcLsneb.js → chunk-XXDRQBXY-CZUV9J1S.js} +1 -1
  26. package/gui/dist/assets/{chunk-ZCHMQSOR-Dmi8Z1BU.js → chunk-ZCHMQSOR-CNNcMwIE.js} +2 -2
  27. package/gui/dist/assets/{classDiagram-v2-4MYWR3UA-D2osY65Q.js → classDiagram-v2-4MYWR3UA-CqjmyUA_.js} +1 -1
  28. package/gui/dist/assets/{cose-bilkent-JH36ORCC-D-LBPHYI.js → cose-bilkent-JH36ORCC-CEo1fweo.js} +1 -1
  29. package/gui/dist/assets/{cynefin-EF2NZ3EQ-u1hU8acG.js → cynefin-EF2NZ3EQ-DJXHpx7E.js} +1 -1
  30. package/gui/dist/assets/{cynefinDiagram-WF24UDRP-BkfQkkkP.js → cynefinDiagram-WF24UDRP-DV_NDSud.js} +1 -1
  31. package/gui/dist/assets/{dagre-NK4GIZVX-C-EzM8in.js → dagre-NK4GIZVX-DQqk6wyz.js} +1 -1
  32. package/gui/dist/assets/{diagram-CBK3MWTC-BOKTtHCl.js → diagram-CBK3MWTC-jqLTDlwa.js} +1 -1
  33. package/gui/dist/assets/{diagram-I5XIIV2D-ngRhV4vV.js → diagram-I5XIIV2D-DX8lfXfI.js} +1 -1
  34. package/gui/dist/assets/{diagram-ODIL2UCW-DwTFpM7V.js → diagram-ODIL2UCW-DeXMUnn8.js} +1 -1
  35. package/gui/dist/assets/{diagram-PFBEJUXE-CNhBo08V.js → diagram-PFBEJUXE-BmNTodoc.js} +1 -1
  36. package/gui/dist/assets/{diagram-RJA3I4VV-qNCeztWZ.js → diagram-RJA3I4VV-Cy6P24cC.js} +1 -1
  37. package/gui/dist/assets/{diagram-TYUNFSNR-DLMiUXhU.js → diagram-TYUNFSNR-DHzYjDGy.js} +1 -1
  38. package/gui/dist/assets/{ebnfDiagram-V4P3TTPT-2EvohYBp.js → ebnfDiagram-V4P3TTPT-B4zOXNTZ.js} +1 -1
  39. package/gui/dist/assets/{elk-QONTBCTQ-DWMzZmm4.js → elk-QONTBCTQ-C4QICnsH.js} +1 -1
  40. package/gui/dist/assets/{erDiagram-KYQIGQAA-BgT-YhVQ.js → erDiagram-KYQIGQAA-p2xJfOWB.js} +1 -1
  41. package/gui/dist/assets/{flowDiagram-IUGLLWBU-CtGI0l2V.js → flowDiagram-IUGLLWBU-DFghgvui.js} +1 -1
  42. package/gui/dist/assets/{ganttDiagram-R7GPNTJY-DEQo6jBk.js → ganttDiagram-R7GPNTJY-HV4LZlu4.js} +1 -1
  43. package/gui/dist/assets/{gitGraphDiagram-PDYL7XHY-idf8MV3F.js → gitGraphDiagram-PDYL7XHY-DXtnta89.js} +1 -1
  44. package/gui/dist/assets/{infoDiagram-MVDRKAX5-m0KC07GR.js → infoDiagram-MVDRKAX5-D0VueLNb.js} +1 -1
  45. package/gui/dist/assets/{ishikawaDiagram-HPEHV2GE-CQzdMlUE.js → ishikawaDiagram-HPEHV2GE-D7K9eBsu.js} +1 -1
  46. package/gui/dist/assets/{journeyDiagram-BOSHW7O7-Bn-ydjkw.js → journeyDiagram-BOSHW7O7-B4bvZn8i.js} +1 -1
  47. package/gui/dist/assets/{kanban-definition-FNSA4HC5-CIPHomZ_.js → kanban-definition-FNSA4HC5-DFUTIwXB.js} +1 -1
  48. package/gui/dist/assets/{linear-BtDtYBvG.js → linear-DNC-wMVK.js} +1 -1
  49. package/gui/dist/assets/main-BTLYod5c.js +10 -0
  50. package/gui/dist/assets/{mindmap-definition-6OY57UG3-Dq-MAgfA.js → mindmap-definition-6OY57UG3-DoOaHbpM.js} +1 -1
  51. package/gui/dist/assets/{pegDiagram-ECIUJNBX-DkN63ahL.js → pegDiagram-ECIUJNBX-ryLSl0Ah.js} +1 -1
  52. package/gui/dist/assets/{pieDiagram-GNMPY74T-DMxFYL7W.js → pieDiagram-GNMPY74T-Bq8sW6iW.js} +1 -1
  53. package/gui/dist/assets/{public-BWV0VJ0G.js → public-3iVzlO1l.js} +1 -1
  54. package/gui/dist/assets/{quadrantDiagram-D57K4MC3-DvCAV95z.js → quadrantDiagram-D57K4MC3-DgbEH3GR.js} +1 -1
  55. package/gui/dist/assets/{railroadDiagram-Y72NRBL7-DIqa4oYb.js → railroadDiagram-Y72NRBL7-CPohPlF3.js} +1 -1
  56. package/gui/dist/assets/{requirementDiagram-EQYSW5RA-CiiuKxrT.js → requirementDiagram-EQYSW5RA-DWFB0c6H.js} +1 -1
  57. package/gui/dist/assets/{sankeyDiagram-SY24JHO5-D7Pqsvht.js → sankeyDiagram-SY24JHO5-Dy_Tjcb0.js} +1 -1
  58. package/gui/dist/assets/{sequenceDiagram-BLEHNHQU-CNOJD6dZ.js → sequenceDiagram-BLEHNHQU-CrojUX5d.js} +1 -1
  59. package/gui/dist/assets/{sizeCapture-INFHLROL-CZUHdjoX.js → sizeCapture-INFHLROL-CgsMV-GT.js} +1 -1
  60. package/gui/dist/assets/{stateDiagram-v2-A6IESKWZ-DwJxREKr.js → stateDiagram-v2-A6IESKWZ-CrIqIMW1.js} +1 -1
  61. package/gui/dist/assets/{swimlanes-B3ENRKFQ-CmwpC4o5.js → swimlanes-B3ENRKFQ-B4GJZt_W.js} +1 -1
  62. package/gui/dist/assets/swimlanesDiagram-AYQRV4AK-Da3hBJCE.js +8 -0
  63. package/gui/dist/assets/{timeline-definition-JXYKGLGB-BYqVvD1n.js → timeline-definition-JXYKGLGB-CEGP9Rfk.js} +1 -1
  64. package/gui/dist/assets/{usecaseDiagram-WPOLBNNX-w3DqfbYF.js → usecaseDiagram-WPOLBNNX-DVDVukRl.js} +1 -1
  65. package/gui/dist/assets/{vennDiagram-Q3Y3IDCW-apvvK_7Z.js → vennDiagram-Q3Y3IDCW-CEr2cVXx.js} +1 -1
  66. package/gui/dist/assets/{wardleyDiagram-IVAIDKWA-DJCHxKD3.js → wardleyDiagram-IVAIDKWA-C1f76sDQ.js} +1 -1
  67. package/gui/dist/assets/{xychartDiagram-4URNHDU4-CBbKJ4ms.js → xychartDiagram-4URNHDU4-DdnVSssZ.js} +1 -1
  68. package/gui/dist/index.html +2 -2
  69. package/gui/dist/public.html +2 -2
  70. package/package.json +72 -69
  71. package/scripts/migrate-links.js +258 -258
  72. package/scripts/migrate-paths-to-relative.js +196 -196
  73. package/gui/dist/assets/channel-CXrvaRTy.js +0 -1
  74. package/gui/dist/assets/main-Cs4w8XB_.js +0 -10
  75. package/gui/dist/assets/swimlanesDiagram-AYQRV4AK-DDXQdzqq.js +0 -8
@@ -1,258 +1,258 @@
1
- #!/usr/bin/env node
2
- /**
3
- * migrate-links.js
4
- *
5
- * Migrates inter-document references from the old slug-only format
6
- * to the new full-path format (folder/name) introduced in Task 10.
7
- *
8
- * Fixes two types of links:
9
- * - related_docs: ["old-slug"] → ["folder/old-slug"]
10
- * - inline links: (doc://old-slug) → (doc://folder/old-slug)
11
- * (doc://old-slug#h) → (doc://folder/old-slug#h)
12
- *
13
- * Usage:
14
- * node scripts/migrate-links.js [--dry-run] [--data-dir /path/to/.docs-data]
15
- *
16
- * Options:
17
- * --dry-run Show what would be changed without modifying any file.
18
- * --data-dir Override the data directory (default: <project-root>/.docs-data).
19
- */
20
-
21
- import fs from "fs";
22
- import path from "path";
23
- import { fileURLToPath } from "url";
24
- import matter from "gray-matter";
25
-
26
- // ---------------------------------------------------------------------------
27
- // Setup
28
- // ---------------------------------------------------------------------------
29
-
30
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
31
- const rootDir = path.join(__dirname, "..");
32
-
33
- const args = process.argv.slice(2);
34
- const dryRun = args.includes("--dry-run");
35
-
36
- const dataDirArgIdx = args.indexOf("--data-dir");
37
- const dataDir =
38
- dataDirArgIdx !== -1 && args[dataDirArgIdx + 1]
39
- ? args[dataDirArgIdx + 1]
40
- : path.join(rootDir, ".docs-data");
41
-
42
- // ---------------------------------------------------------------------------
43
- // Helpers
44
- // ---------------------------------------------------------------------------
45
-
46
- function walkMdFiles(dir) {
47
- if (!fs.existsSync(dir)) return [];
48
- const results = [];
49
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
50
- const fullPath = path.join(dir, entry.name);
51
- if (entry.isDirectory()) {
52
- results.push(...walkMdFiles(fullPath));
53
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
54
- results.push(fullPath);
55
- }
56
- }
57
- return results;
58
- }
59
-
60
- /** Derive the doc path (id) from an absolute file path within a project dir. */
61
- function filePathToDocId(filePath, projectDir) {
62
- return path
63
- .relative(projectDir, filePath)
64
- .replace(/\.md$/, "")
65
- .split(path.sep)
66
- .join("/");
67
- }
68
-
69
- // ---------------------------------------------------------------------------
70
- // Load config
71
- // ---------------------------------------------------------------------------
72
-
73
- const configPath = path.join(dataDir, "config.json");
74
- if (!fs.existsSync(configPath)) {
75
- console.error(`[ERROR] config.json not found at: ${configPath}`);
76
- console.error(" Make sure --data-dir points to the right directory.");
77
- process.exit(1);
78
- }
79
-
80
- const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
81
- const projects = config.projects ?? ["default"];
82
-
83
- console.log(`${dryRun ? "[DRY RUN] " : ""}migrate-links`);
84
- console.log(`Data dir : ${dataDir}`);
85
- console.log(`Projects : ${projects.join(", ")}`);
86
- console.log("");
87
-
88
- // ---------------------------------------------------------------------------
89
- // Step 1 — Build the global map: slug → [{ project, fullId }]
90
- //
91
- // A "slug" is the last segment of a doc path (the filename without .md).
92
- // If a slug appears in multiple projects or folders, the reference is ambiguous.
93
- // ---------------------------------------------------------------------------
94
-
95
- /** @type {Map<string, Array<{ project: string, fullId: string }>>} */
96
- const slugMap = new Map();
97
-
98
- for (const project of projects) {
99
- const projectDir = path.join(dataDir, "projects", project);
100
- for (const filePath of walkMdFiles(projectDir)) {
101
- const fullId = filePathToDocId(filePath, projectDir);
102
- const slug = fullId.split("/").pop();
103
-
104
- if (!slugMap.has(slug)) slugMap.set(slug, []);
105
- slugMap.get(slug).push({ project, fullId });
106
- }
107
- }
108
-
109
- // ---------------------------------------------------------------------------
110
- // Step 2 — Scan every doc and fix links
111
- // ---------------------------------------------------------------------------
112
-
113
- let totalDocsChanged = 0;
114
- let totalLinksFixed = 0;
115
- let totalLinksNotFound = 0;
116
- let totalLinksAmbiguous = 0;
117
-
118
- /**
119
- * Resolve an old slug-only reference to its full path.
120
- * Returns { resolved: string } on success, or { error: string } otherwise.
121
- */
122
- function resolveSlug(slug) {
123
- const matches = slugMap.get(slug) ?? [];
124
- if (matches.length === 0) return { error: "NOT FOUND" };
125
- if (matches.length > 1) {
126
- return { error: `AMBIGUOUS → [${matches.map((m) => m.fullId).join(", ")}]` };
127
- }
128
- return { resolved: matches[0].fullId };
129
- }
130
-
131
- for (const project of projects) {
132
- const projectDir = path.join(dataDir, "projects", project);
133
- const files = walkMdFiles(projectDir);
134
-
135
- if (files.length === 0) continue;
136
-
137
- console.log(`\n### Project: ${project} (${files.length} docs)`);
138
- console.log("");
139
-
140
- for (const filePath of files) {
141
- const fullId = filePathToDocId(filePath, projectDir);
142
- const raw = fs.readFileSync(filePath, "utf-8");
143
- const parsed = matter(raw);
144
-
145
- const docChanges = [];
146
- let docChanged = false;
147
-
148
- // -----------------------------------------------------------------------
149
- // Fix related_docs
150
- // -----------------------------------------------------------------------
151
-
152
- if (Array.isArray(parsed.data.related_docs)) {
153
- const newRelatedDocs = [];
154
-
155
- for (const ref of parsed.data.related_docs) {
156
- // Already a full path — nothing to do
157
- if (ref.includes("/")) {
158
- newRelatedDocs.push(ref);
159
- continue;
160
- }
161
-
162
- const result = resolveSlug(ref);
163
-
164
- if (result.resolved) {
165
- docChanges.push(` [related_docs] "${ref}" → "${result.resolved}"`);
166
- newRelatedDocs.push(result.resolved);
167
- docChanged = true;
168
- totalLinksFixed++;
169
- } else {
170
- docChanges.push(` [related_docs] "${ref}" → ${result.error} (kept as-is)`);
171
- newRelatedDocs.push(ref);
172
- if (result.error === "NOT FOUND") totalLinksNotFound++;
173
- else totalLinksAmbiguous++;
174
- }
175
- }
176
-
177
- if (docChanged) {
178
- parsed.data.related_docs = newRelatedDocs;
179
- }
180
- }
181
-
182
- // -----------------------------------------------------------------------
183
- // Fix inline doc:// links in content
184
- //
185
- // Matches: doc://slug or doc://slug#anchor
186
- // Does NOT touch: doc://folder/slug (already full path)
187
- // -----------------------------------------------------------------------
188
-
189
- // Pattern captures: group1 = slug-or-path, group2 = optional #anchor
190
- const docLinkRegex = /doc:\/\/([^)#\s]+)(#[^)\s]*)?/g;
191
- let newContent = parsed.content;
192
-
193
- // Collect all matches first to avoid mutation during iteration
194
- const contentMatches = [];
195
- let m;
196
- while ((m = docLinkRegex.exec(parsed.content)) !== null) {
197
- contentMatches.push({ full: m[0], ref: m[1], anchor: m[2] ?? "" });
198
- }
199
-
200
- for (const { full, ref, anchor } of contentMatches) {
201
- // Already a full path
202
- if (ref.includes("/")) continue;
203
-
204
- const result = resolveSlug(ref);
205
-
206
- if (result.resolved) {
207
- const newLink = `doc://${result.resolved}${anchor}`;
208
- docChanges.push(` [doc link] "${full}" → "${newLink}"`);
209
- // Replace only this specific occurrence (anchor included for precision)
210
- newContent = newContent.replaceAll(full, newLink);
211
- docChanged = true;
212
- totalLinksFixed++;
213
- } else {
214
- docChanges.push(` [doc link] "${full}" → ${result.error} (kept as-is)`);
215
- if (result.error === "NOT FOUND") totalLinksNotFound++;
216
- else totalLinksAmbiguous++;
217
- }
218
- }
219
-
220
- // -----------------------------------------------------------------------
221
- // Report & write
222
- // -----------------------------------------------------------------------
223
-
224
- if (docChanges.length > 0) {
225
- console.log(` [${fullId}]`);
226
- docChanges.forEach((c) => console.log(c));
227
- console.log("");
228
- }
229
-
230
- if (docChanged) {
231
- totalDocsChanged++;
232
- if (!dryRun) {
233
- const output = matter.stringify(newContent, parsed.data);
234
- fs.writeFileSync(filePath, output, "utf-8");
235
- }
236
- }
237
- }
238
- }
239
-
240
- // ---------------------------------------------------------------------------
241
- // Summary
242
- // ---------------------------------------------------------------------------
243
-
244
- console.log("─".repeat(60));
245
- console.log(`Links fixed : ${totalLinksFixed}`);
246
- if (totalLinksNotFound > 0) {
247
- console.log(`Links not found : ${totalLinksNotFound} ← target doc doesn't exist`);
248
- }
249
- if (totalLinksAmbiguous > 0) {
250
- console.log(`Links ambiguous : ${totalLinksAmbiguous} ← same slug in multiple folders, fix manually`);
251
- }
252
- console.log(`Docs modified : ${totalDocsChanged}`);
253
-
254
- if (dryRun) {
255
- console.log("");
256
- console.log("[DRY RUN] No files were modified.");
257
- console.log(" Run without --dry-run to apply the changes.");
258
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * migrate-links.js
4
+ *
5
+ * Migrates inter-document references from the old slug-only format
6
+ * to the new full-path format (folder/name) introduced in Task 10.
7
+ *
8
+ * Fixes two types of links:
9
+ * - related_docs: ["old-slug"] → ["folder/old-slug"]
10
+ * - inline links: (doc://old-slug) → (doc://folder/old-slug)
11
+ * (doc://old-slug#h) → (doc://folder/old-slug#h)
12
+ *
13
+ * Usage:
14
+ * node scripts/migrate-links.js [--dry-run] [--data-dir /path/to/.docs-data]
15
+ *
16
+ * Options:
17
+ * --dry-run Show what would be changed without modifying any file.
18
+ * --data-dir Override the data directory (default: <project-root>/.docs-data).
19
+ */
20
+
21
+ import fs from "fs";
22
+ import path from "path";
23
+ import { fileURLToPath } from "url";
24
+ import matter from "gray-matter";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Setup
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
31
+ const rootDir = path.join(__dirname, "..");
32
+
33
+ const args = process.argv.slice(2);
34
+ const dryRun = args.includes("--dry-run");
35
+
36
+ const dataDirArgIdx = args.indexOf("--data-dir");
37
+ const dataDir =
38
+ dataDirArgIdx !== -1 && args[dataDirArgIdx + 1]
39
+ ? args[dataDirArgIdx + 1]
40
+ : path.join(rootDir, ".docs-data");
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Helpers
44
+ // ---------------------------------------------------------------------------
45
+
46
+ function walkMdFiles(dir) {
47
+ if (!fs.existsSync(dir)) return [];
48
+ const results = [];
49
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
50
+ const fullPath = path.join(dir, entry.name);
51
+ if (entry.isDirectory()) {
52
+ results.push(...walkMdFiles(fullPath));
53
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
54
+ results.push(fullPath);
55
+ }
56
+ }
57
+ return results;
58
+ }
59
+
60
+ /** Derive the doc path (id) from an absolute file path within a project dir. */
61
+ function filePathToDocId(filePath, projectDir) {
62
+ return path
63
+ .relative(projectDir, filePath)
64
+ .replace(/\.md$/, "")
65
+ .split(path.sep)
66
+ .join("/");
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Load config
71
+ // ---------------------------------------------------------------------------
72
+
73
+ const configPath = path.join(dataDir, "config.json");
74
+ if (!fs.existsSync(configPath)) {
75
+ console.error(`[ERROR] config.json not found at: ${configPath}`);
76
+ console.error(" Make sure --data-dir points to the right directory.");
77
+ process.exit(1);
78
+ }
79
+
80
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
81
+ const projects = config.projects ?? ["default"];
82
+
83
+ console.log(`${dryRun ? "[DRY RUN] " : ""}migrate-links`);
84
+ console.log(`Data dir : ${dataDir}`);
85
+ console.log(`Projects : ${projects.join(", ")}`);
86
+ console.log("");
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Step 1 — Build the global map: slug → [{ project, fullId }]
90
+ //
91
+ // A "slug" is the last segment of a doc path (the filename without .md).
92
+ // If a slug appears in multiple projects or folders, the reference is ambiguous.
93
+ // ---------------------------------------------------------------------------
94
+
95
+ /** @type {Map<string, Array<{ project: string, fullId: string }>>} */
96
+ const slugMap = new Map();
97
+
98
+ for (const project of projects) {
99
+ const projectDir = path.join(dataDir, "projects", project);
100
+ for (const filePath of walkMdFiles(projectDir)) {
101
+ const fullId = filePathToDocId(filePath, projectDir);
102
+ const slug = fullId.split("/").pop();
103
+
104
+ if (!slugMap.has(slug)) slugMap.set(slug, []);
105
+ slugMap.get(slug).push({ project, fullId });
106
+ }
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Step 2 — Scan every doc and fix links
111
+ // ---------------------------------------------------------------------------
112
+
113
+ let totalDocsChanged = 0;
114
+ let totalLinksFixed = 0;
115
+ let totalLinksNotFound = 0;
116
+ let totalLinksAmbiguous = 0;
117
+
118
+ /**
119
+ * Resolve an old slug-only reference to its full path.
120
+ * Returns { resolved: string } on success, or { error: string } otherwise.
121
+ */
122
+ function resolveSlug(slug) {
123
+ const matches = slugMap.get(slug) ?? [];
124
+ if (matches.length === 0) return { error: "NOT FOUND" };
125
+ if (matches.length > 1) {
126
+ return { error: `AMBIGUOUS → [${matches.map((m) => m.fullId).join(", ")}]` };
127
+ }
128
+ return { resolved: matches[0].fullId };
129
+ }
130
+
131
+ for (const project of projects) {
132
+ const projectDir = path.join(dataDir, "projects", project);
133
+ const files = walkMdFiles(projectDir);
134
+
135
+ if (files.length === 0) continue;
136
+
137
+ console.log(`\n### Project: ${project} (${files.length} docs)`);
138
+ console.log("");
139
+
140
+ for (const filePath of files) {
141
+ const fullId = filePathToDocId(filePath, projectDir);
142
+ const raw = fs.readFileSync(filePath, "utf-8");
143
+ const parsed = matter(raw);
144
+
145
+ const docChanges = [];
146
+ let docChanged = false;
147
+
148
+ // -----------------------------------------------------------------------
149
+ // Fix related_docs
150
+ // -----------------------------------------------------------------------
151
+
152
+ if (Array.isArray(parsed.data.related_docs)) {
153
+ const newRelatedDocs = [];
154
+
155
+ for (const ref of parsed.data.related_docs) {
156
+ // Already a full path — nothing to do
157
+ if (ref.includes("/")) {
158
+ newRelatedDocs.push(ref);
159
+ continue;
160
+ }
161
+
162
+ const result = resolveSlug(ref);
163
+
164
+ if (result.resolved) {
165
+ docChanges.push(` [related_docs] "${ref}" → "${result.resolved}"`);
166
+ newRelatedDocs.push(result.resolved);
167
+ docChanged = true;
168
+ totalLinksFixed++;
169
+ } else {
170
+ docChanges.push(` [related_docs] "${ref}" → ${result.error} (kept as-is)`);
171
+ newRelatedDocs.push(ref);
172
+ if (result.error === "NOT FOUND") totalLinksNotFound++;
173
+ else totalLinksAmbiguous++;
174
+ }
175
+ }
176
+
177
+ if (docChanged) {
178
+ parsed.data.related_docs = newRelatedDocs;
179
+ }
180
+ }
181
+
182
+ // -----------------------------------------------------------------------
183
+ // Fix inline doc:// links in content
184
+ //
185
+ // Matches: doc://slug or doc://slug#anchor
186
+ // Does NOT touch: doc://folder/slug (already full path)
187
+ // -----------------------------------------------------------------------
188
+
189
+ // Pattern captures: group1 = slug-or-path, group2 = optional #anchor
190
+ const docLinkRegex = /doc:\/\/([^)#\s]+)(#[^)\s]*)?/g;
191
+ let newContent = parsed.content;
192
+
193
+ // Collect all matches first to avoid mutation during iteration
194
+ const contentMatches = [];
195
+ let m;
196
+ while ((m = docLinkRegex.exec(parsed.content)) !== null) {
197
+ contentMatches.push({ full: m[0], ref: m[1], anchor: m[2] ?? "" });
198
+ }
199
+
200
+ for (const { full, ref, anchor } of contentMatches) {
201
+ // Already a full path
202
+ if (ref.includes("/")) continue;
203
+
204
+ const result = resolveSlug(ref);
205
+
206
+ if (result.resolved) {
207
+ const newLink = `doc://${result.resolved}${anchor}`;
208
+ docChanges.push(` [doc link] "${full}" → "${newLink}"`);
209
+ // Replace only this specific occurrence (anchor included for precision)
210
+ newContent = newContent.replaceAll(full, newLink);
211
+ docChanged = true;
212
+ totalLinksFixed++;
213
+ } else {
214
+ docChanges.push(` [doc link] "${full}" → ${result.error} (kept as-is)`);
215
+ if (result.error === "NOT FOUND") totalLinksNotFound++;
216
+ else totalLinksAmbiguous++;
217
+ }
218
+ }
219
+
220
+ // -----------------------------------------------------------------------
221
+ // Report & write
222
+ // -----------------------------------------------------------------------
223
+
224
+ if (docChanges.length > 0) {
225
+ console.log(` [${fullId}]`);
226
+ docChanges.forEach((c) => console.log(c));
227
+ console.log("");
228
+ }
229
+
230
+ if (docChanged) {
231
+ totalDocsChanged++;
232
+ if (!dryRun) {
233
+ const output = matter.stringify(newContent, parsed.data);
234
+ fs.writeFileSync(filePath, output, "utf-8");
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // Summary
242
+ // ---------------------------------------------------------------------------
243
+
244
+ console.log("─".repeat(60));
245
+ console.log(`Links fixed : ${totalLinksFixed}`);
246
+ if (totalLinksNotFound > 0) {
247
+ console.log(`Links not found : ${totalLinksNotFound} ← target doc doesn't exist`);
248
+ }
249
+ if (totalLinksAmbiguous > 0) {
250
+ console.log(`Links ambiguous : ${totalLinksAmbiguous} ← same slug in multiple folders, fix manually`);
251
+ }
252
+ console.log(`Docs modified : ${totalDocsChanged}`);
253
+
254
+ if (dryRun) {
255
+ console.log("");
256
+ console.log("[DRY RUN] No files were modified.");
257
+ console.log(" Run without --dry-run to apply the changes.");
258
+ }