sdtk-brain-kit 0.1.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.
@@ -0,0 +1,2108 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { CliError, ValidationError } = require("./errors");
6
+ const {
7
+ assertCanonicalWikiWritePath,
8
+ assertWikiWorkspaceWritePath,
9
+ getCanonicalWikiPath,
10
+ getLegacyPersonalBrainPath,
11
+ getWikiReportsPath,
12
+ getWikiWorkspacePath,
13
+ isPathInsideOrEqual,
14
+ resolveProjectPath,
15
+ } = require("./wiki-paths");
16
+
17
+ const REPORT_PREFIX = "compile-dry-run-preview";
18
+ const APPLY_PLAN_PREFIX = "compile-apply-plan";
19
+ const APPLY_PLAN_RECORD_TYPE = "sdtk_wiki_compile_apply_plan";
20
+ const APPLY_PLAN_SCHEMA_VERSION = 1;
21
+ const CANONICAL_WIKI_RELATIVE = "wiki";
22
+ const APPLY_MODE = "create_only_or_same_content_noop";
23
+ const ALLOWED_OPERATION_TYPES = new Set([
24
+ "append_section",
25
+ "create_page",
26
+ "add_relation",
27
+ "add_source_ref",
28
+ ]);
29
+
30
+ function todayStamp(date = new Date()) {
31
+ return date.toISOString().slice(0, 10);
32
+ }
33
+
34
+ function asArray(value) {
35
+ return Array.isArray(value) ? value : [];
36
+ }
37
+
38
+ function toPosix(value) {
39
+ return String(value || "").replace(/\\/g, "/");
40
+ }
41
+
42
+ function stableHash(value) {
43
+ return require("crypto").createHash("sha256").update(String(value)).digest("hex");
44
+ }
45
+
46
+ function isRemoteUrl(value) {
47
+ return /^(?:https?|ftp):\/\//i.test(String(value || ""));
48
+ }
49
+
50
+ function resolvePlanPath(planArg, projectPath) {
51
+ if (!planArg) {
52
+ throw new ValidationError("sdtk-brain wiki compile requires --plan <path>. No project files were changed.");
53
+ }
54
+ if (isRemoteUrl(planArg)) {
55
+ throw new ValidationError("Remote URL plan inputs are not supported for compile dry-run preview. No project files were changed.");
56
+ }
57
+
58
+ const candidate = path.isAbsolute(planArg) ? path.resolve(planArg) : path.resolve(projectPath, planArg);
59
+ if (!isPathInsideOrEqual(candidate, projectPath)) {
60
+ throw new ValidationError(`--plan must resolve inside the project root: ${candidate}. No project files were changed.`);
61
+ }
62
+ if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
63
+ throw new ValidationError(`--plan is not a valid file: ${candidate}. No project files were changed.`);
64
+ }
65
+ return candidate;
66
+ }
67
+
68
+ function ensureExistingWorkspace(projectPath) {
69
+ const workspacePath = getWikiWorkspacePath(projectPath);
70
+ if (!fs.existsSync(workspacePath) || !fs.statSync(workspacePath).isDirectory()) {
71
+ throw new ValidationError(
72
+ `No SDTK-BRAIN workspace found at ${workspacePath}. Run "sdtk-brain init" or "sdtk-brain atlas build" first. No project files were changed.`
73
+ );
74
+ }
75
+ return workspacePath;
76
+ }
77
+
78
+ function firstSourceRef(record) {
79
+ const refs = asArray(record.source_refs || record.sourceRefs);
80
+ return refs.length > 0 ? String(refs[0]) : "semantic_extraction_report";
81
+ }
82
+
83
+ function sourceHashFor(sourceById, sourceId, fallback = "") {
84
+ const source = sourceById.get(sourceId);
85
+ return source && source.source_hash ? String(source.source_hash) : fallback;
86
+ }
87
+
88
+ function refsFor(record) {
89
+ return [
90
+ ...asArray(record.source_refs || record.sourceRefs),
91
+ ...asArray(record.provenance_refs || record.provenanceRefs),
92
+ record.source_logical_path,
93
+ record.source_relative_path,
94
+ ].filter(Boolean).map(String);
95
+ }
96
+
97
+ function sourceRefsFor(record) {
98
+ const refs = asArray(record.source_refs || record.sourceRefs);
99
+ if (record.source_id) refs.unshift(record.source_id);
100
+ return [...new Set(refs.filter(Boolean).map(String))];
101
+ }
102
+
103
+ function provenanceRefsFor(record) {
104
+ return [...new Set(asArray(record.provenance_refs || record.provenanceRefs).filter(Boolean).map(String))];
105
+ }
106
+
107
+ function mdList(values) {
108
+ const items = asArray(values).filter(Boolean).map(String);
109
+ return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None recorded.";
110
+ }
111
+
112
+ function mdLinkedList(values) {
113
+ const items = asArray(values).filter(Boolean);
114
+ if (items.length === 0) return "- None recorded.";
115
+ return items.map((item) => {
116
+ const text = String(item);
117
+ if (/^https?:\/\//i.test(text)) return `- [${text}](${text})`;
118
+ return `- ${text}`;
119
+ }).join("\n");
120
+ }
121
+
122
+ function quoteBlock(value) {
123
+ const text = String(value || "").replace(/\r?\n/g, " ").trim();
124
+ return text ? `> ${text}` : "> No extracted snippet recorded in the local source.";
125
+ }
126
+
127
+ function topicLabelList(entity) {
128
+ const topics = [...new Set(asArray(entity.topics).filter(Boolean).map(String))];
129
+ return topics.length > 0 ? topics.map((topic) => `- ${topic}`).join("\n") : "- No topic labels extracted from local sources.";
130
+ }
131
+
132
+ function relatedRepoList(entity) {
133
+ const repos = asArray(entity.related_repos);
134
+ if (repos.length === 0) return "- No related repositories inferred from local topic overlap.";
135
+ return repos.map((repo) => {
136
+ const label = repo.name || repo.entity_id || "related repo";
137
+ const suffix = asArray(repo.shared_topics).length > 0 ? ` (shared topics: ${asArray(repo.shared_topics).join(", ")})` : "";
138
+ return repo.github_url ? `- [${label}](${repo.github_url})${suffix}` : `- ${label}${suffix}`;
139
+ }).join("\n");
140
+ }
141
+
142
+ function relativePersonalBrainLink(targetPagePath) {
143
+ const text = toPosix(targetPagePath || "");
144
+ if (text.startsWith("wiki/")) return text.slice("wiki/".length);
145
+ const marker = ".brain/personal-brain/";
146
+ const idx = text.indexOf(marker);
147
+ return idx >= 0 ? text.slice(idx + marker.length) : text;
148
+ }
149
+
150
+ function conceptAxisList(concept) {
151
+ const axes = asArray(concept.key_axes);
152
+ if (axes.length === 0) return "- No key axes were inferred from local evidence.";
153
+ return axes.map((axis) => `- ${axis.name}: ${axis.evidence_count || 1} local evidence reference(s).`).join("\n");
154
+ }
155
+
156
+ function conceptImplementationList(concept) {
157
+ const entities = asArray(concept.related_entity_details);
158
+ if (entities.length === 0) return "- No implementation examples were extracted from local sources.";
159
+ return entities.slice(0, 10).map((entity) => {
160
+ const label = entity.repo_owner && entity.repo_name ? `${entity.repo_owner}/${entity.repo_name}` : (entity.name || "tool candidate");
161
+ const link = entity.target_page_path ? relativePersonalBrainLink(entity.target_page_path) : "";
162
+ const target = link ? `[${label}](../${link})` : label;
163
+ const url = entity.github_url ? `; ${entity.github_url}` : "";
164
+ return `- ${target}: ${entity.summary || `Local candidate in ${entity.category || "uncategorized"}`}${url}`;
165
+ }).join("\n");
166
+ }
167
+
168
+ function conceptPatternList(concept) {
169
+ const patterns = asArray(concept.patterns);
170
+ if (patterns.length === 0) return "- No repeated pattern was inferred from local evidence.";
171
+ return patterns.map((pattern) => `- ${pattern.pattern}: ${pattern.evidence}`).join("\n");
172
+ }
173
+
174
+ function conceptRelatedPagesList(concept) {
175
+ const pages = asArray(concept.related_entity_details)
176
+ .map((entity) => entity.target_page_path)
177
+ .filter(Boolean)
178
+ .map((target) => `../${relativePersonalBrainLink(target)}`);
179
+ return mdList([...new Set(pages)].slice(0, 12));
180
+ }
181
+
182
+ function confidenceSummaryList(summary) {
183
+ if (!summary || typeof summary !== "object") return "- No source confidence summary recorded.";
184
+ const rows = Object.entries(summary).filter(([, count]) => Number(count) > 0);
185
+ return rows.length > 0 ? rows.map(([tier, count]) => `- ${tier}: ${count}`).join("\n") : "- No source confidence summary recorded.";
186
+ }
187
+
188
+ function comparisonMatrix(comparison) {
189
+ const rows = asArray(comparison.matrix_rows);
190
+ if (rows.length === 0) return "| Candidate | Category | Local evidence | Strengths | Caveats | Recommendation |\n|---|---|---:|---|---|---|\n| None recorded | - | 0 | - | - | - |";
191
+ const table = [
192
+ "| Candidate | Category | Local evidence | Strengths | Caveats | Recommendation |",
193
+ "|---|---|---:|---|---|---|",
194
+ ];
195
+ for (const row of rows) {
196
+ const candidate = row.github_url ? `[${row.name || row.entity_id}](${row.github_url})` : (row.name || row.entity_id || "candidate");
197
+ table.push(`| ${candidate} | ${row.category || "uncategorized"} | ${row.source_ref_count || 0} | ${asArray(row.strengths).slice(0, 3).join("<br>") || "-"} | ${asArray(row.caveats).slice(0, 3).join("<br>") || "-"} | ${row.local_recommendation || "review"} |`);
198
+ }
199
+ return table.join("\n");
200
+ }
201
+
202
+ function candidateToolList(records) {
203
+ const rows = asArray(records);
204
+ if (rows.length === 0) return "- No candidate tools recorded.";
205
+ return rows.slice(0, 12).map((row) => {
206
+ const label = row.github_url ? `[${row.name || row.entity_id}](${row.github_url})` : (row.name || row.entity_id || "candidate");
207
+ return `- ${label}: ${row.local_recommendation || "review"}; confidence ${row.source_confidence || row.confidence_tier || "unknown"}.`;
208
+ }).join("\n");
209
+ }
210
+
211
+ function localCandidateLabel(record) {
212
+ if (!record) return "local candidate";
213
+ if (record.repo_owner && record.repo_name) return `${record.repo_owner}/${record.repo_name}`;
214
+ if (record.owner && (record.repo_name || record.repo)) return `${record.owner}/${record.repo_name || record.repo}`;
215
+ return record.name || record.entity_id || "local candidate";
216
+ }
217
+
218
+ function candidateEvidenceCount(record) {
219
+ const explicit = Number(record.source_ref_count || record.local_evidence_count || record.evidence_count || 0);
220
+ if (explicit > 0) return explicit;
221
+ const refs = sourceRefsFor(record);
222
+ return refs.length > 0 ? refs.length : 1;
223
+ }
224
+
225
+ function topLocalCandidates(records, limit = 3) {
226
+ return asArray(records)
227
+ .map((record) => ({
228
+ record,
229
+ label: localCandidateLabel(record),
230
+ evidence: candidateEvidenceCount(record),
231
+ category: record.category || record.local_category || "uncategorized",
232
+ confidence: record.source_confidence || record.confidence_tier || record.confidence || "unknown",
233
+ topics: asArray(record.topics || record.shared_topics).filter(Boolean).map(String),
234
+ recommendation: record.local_recommendation || record.recommendation || "review",
235
+ summary: record.summary || record.evidence || "",
236
+ }))
237
+ .sort((a, b) => (b.evidence - a.evidence) || a.label.localeCompare(b.label))
238
+ .slice(0, limit);
239
+ }
240
+
241
+ function refinedCandidateSentence(candidate) {
242
+ const topicText = candidate.topics.length > 0 ? `; local topics: ${candidate.topics.slice(0, 4).join(", ")}` : "";
243
+ return `${candidate.label} (${candidate.category}) has ${candidate.evidence} local evidence reference(s), confidence ${candidate.confidence}${topicText}.`;
244
+ }
245
+
246
+ function conceptDecisionBrief(concept) {
247
+ const title = recordTitle(concept, "Concept");
248
+ const related = asArray(concept.related_entity_details);
249
+ const candidates = topLocalCandidates(related, 3);
250
+ const sourceCount = sourceRefCount(concept);
251
+ const axisNames = asArray(concept.key_axes).map((axis) => axis.name || axis).filter(Boolean).slice(0, 4);
252
+ const lines = [
253
+ `- Local evidence profile: ${related.length} related tool candidate(s), ${sourceCount} source reference(s), ${axisNames.length} decision axis/axes.`,
254
+ ];
255
+ if (candidates.length > 0) {
256
+ lines.push(`- First review target: ${refinedCandidateSentence(candidates[0])}`);
257
+ if (candidates.length > 1) {
258
+ lines.push(`- Compare next: ${candidates.slice(1).map((candidate) => `${candidate.label} (${candidate.evidence} local ref(s))`).join("; ")}.`);
259
+ }
260
+ } else {
261
+ lines.push("- First review target: no concrete tool candidate was linked to this concept in local evidence.");
262
+ }
263
+ if (axisNames.length > 0) {
264
+ lines.push(`- Decision axes to preserve: ${axisNames.join(", ")}.`);
265
+ }
266
+ lines.push(`- Local-only boundary: treat ${title} as a decision queue from local notes, not as proof of current repository health, license, or adoption readiness.`);
267
+ return lines.join("\n");
268
+ }
269
+
270
+ function comparisonDecisionGuidance(comparison) {
271
+ const rows = asArray(comparison.matrix_rows);
272
+ const candidates = topLocalCandidates(rows, 4);
273
+ const axisNames = asArray(comparison.decision_axes).map((axis) => axis.name || axis).filter(Boolean).slice(0, 5);
274
+ const lines = [
275
+ `- Use this comparison to rank ${rows.length} local candidate(s) across ${axisNames.length || "the recorded"} decision axis/axes.`,
276
+ ];
277
+ if (candidates.length > 0) {
278
+ lines.push(`- Local shortlist: ${candidates.map((candidate) => `${candidate.label} (${candidate.evidence} ref(s), ${candidate.recommendation})`).join("; ")}.`);
279
+ }
280
+ if (axisNames.length > 0) {
281
+ lines.push(`- Review order: score candidates first on ${axisNames.join(", ")}, then verify external facts separately.`);
282
+ }
283
+ lines.push("- Do not convert a local shortlist into an adoption recommendation until license, maintenance, security, and current repository identity are verified outside this local evidence set.");
284
+ return lines.join("\n");
285
+ }
286
+
287
+ function synthesisDecisionNarrative(synthesis) {
288
+ const rows = asArray(synthesis.candidate_tools);
289
+ const candidates = topLocalCandidates(rows, 4);
290
+ const axisNames = asArray(synthesis.landscape_axes).map((axis) => axis.name || axis).filter(Boolean).slice(0, 5);
291
+ const lines = [
292
+ `- This synthesis frames ${rows.length} local candidate(s) as a review backlog, not a final recommendation.`,
293
+ ];
294
+ if (candidates.length > 0) {
295
+ lines.push(`- Best first inspection path: ${candidates.map((candidate) => `${candidate.label} (${candidate.evidence} ref(s), ${candidate.confidence})`).join("; ")}.`);
296
+ }
297
+ if (axisNames.length > 0) {
298
+ lines.push(`- Decision lens: ${axisNames.join(", ")}.`);
299
+ }
300
+ if (synthesis.related_comparison_path) {
301
+ lines.push(`- Use the related comparison page before making a tool choice: ../${relativePersonalBrainLink(synthesis.related_comparison_path)}.`);
302
+ }
303
+ lines.push("- Keep unverified claims labeled: local snippets can explain why a tool entered the queue, but not whether it is currently mature, licensed correctly, or maintained.");
304
+ return lines.join("\n");
305
+ }
306
+
307
+ function mdEscapeTable(value) {
308
+ return String(value ?? "").replace(/\|/g, "\\|").replace(/\r?\n/g, " ").trim();
309
+ }
310
+
311
+ function rootLink(targetPagePath) {
312
+ const rel = relativePersonalBrainLink(targetPagePath);
313
+ return rel ? toPosix(rel) : "";
314
+ }
315
+
316
+ function dashboardLink(targetPagePath) {
317
+ const rel = relativePersonalBrainLink(targetPagePath);
318
+ return rel ? `../${toPosix(rel)}` : "";
319
+ }
320
+
321
+ function sourceRefCount(record) {
322
+ return sourceRefsFor(record).length;
323
+ }
324
+
325
+ function highSignalTools(payload, limit = 12) {
326
+ return asArray(payload.tool_entities)
327
+ .map((tool) => ({
328
+ title: recordTitle(tool, "tool"),
329
+ path: tool.target_page_path,
330
+ github_url: tool.github_url,
331
+ category: tool.category || "uncategorized",
332
+ topics: asArray(tool.topics).filter(Boolean).map(String),
333
+ source_refs: sourceRefCount(tool),
334
+ confidence: tool.confidence_tier || tool.confidence || "medium",
335
+ summary: tool.summary || "",
336
+ }))
337
+ .sort((a, b) => (b.source_refs - a.source_refs) || a.title.localeCompare(b.title))
338
+ .slice(0, limit);
339
+ }
340
+
341
+ function topConcepts(payload, limit = 10) {
342
+ return asArray(payload.concepts)
343
+ .map((concept) => ({
344
+ title: recordTitle(concept, "concept"),
345
+ path: concept.target_page_path,
346
+ related_count: asArray(concept.related_entity_details).length || asArray(concept.related_entities).length,
347
+ source_refs: sourceRefCount(concept),
348
+ axes: asArray(concept.key_axes).map((axis) => axis.name || axis).filter(Boolean),
349
+ }))
350
+ .sort((a, b) => (b.related_count - a.related_count) || (b.source_refs - a.source_refs) || a.title.localeCompare(b.title))
351
+ .slice(0, limit);
352
+ }
353
+
354
+ function comparisonEntries(payload, limit = 10) {
355
+ return asArray(payload.comparisons)
356
+ .map((comparison) => ({
357
+ title: recordTitle(comparison, "comparison"),
358
+ topic: comparison.topic || comparison.comparison_id || recordTitle(comparison, "comparison"),
359
+ path: comparison.target_page_path,
360
+ rows: asArray(comparison.matrix_rows).length,
361
+ source_refs: sourceRefCount(comparison),
362
+ recommendations: asArray(comparison.recommendations),
363
+ }))
364
+ .sort((a, b) => (b.rows - a.rows) || (b.source_refs - a.source_refs) || a.title.localeCompare(b.title))
365
+ .slice(0, limit);
366
+ }
367
+
368
+ function synthesisEntries(payload, limit = 10) {
369
+ return asArray(payload.syntheses)
370
+ .map((synthesis) => ({
371
+ title: recordTitle(synthesis, "synthesis"),
372
+ topic: synthesis.topic || synthesis.synthesis_id || recordTitle(synthesis, "synthesis"),
373
+ path: synthesis.target_page_path,
374
+ candidates: asArray(synthesis.candidate_tools).length,
375
+ source_refs: sourceRefCount(synthesis),
376
+ recommendations: asArray(synthesis.recommendations),
377
+ }))
378
+ .sort((a, b) => (b.candidates - a.candidates) || (b.source_refs - a.source_refs) || a.title.localeCompare(b.title))
379
+ .slice(0, limit);
380
+ }
381
+
382
+ function sourceEntries(payload, limit = 20) {
383
+ return asArray(payload.sources)
384
+ .map((source) => ({
385
+ title: source.source_logical_path || source.source_relative_path || source.source_id || "source",
386
+ path: source.target_page_path,
387
+ source_id: source.source_id,
388
+ quality_flags: asArray(source.source_quality && source.source_quality.quality_flags),
389
+ source_url: source.source_url || "",
390
+ source_hash: source.source_hash || "",
391
+ }))
392
+ .slice(0, limit);
393
+ }
394
+
395
+ function groupToolsByCategory(tools) {
396
+ const byCategory = new Map();
397
+ for (const tool of tools) {
398
+ const key = tool.category || "uncategorized";
399
+ if (!byCategory.has(key)) byCategory.set(key, []);
400
+ byCategory.get(key).push(tool);
401
+ }
402
+ return [...byCategory.entries()]
403
+ .map(([category, rows]) => ({ category, rows }))
404
+ .sort((a, b) => (b.rows.length - a.rows.length) || a.category.localeCompare(b.category));
405
+ }
406
+
407
+ function linkedListFromRoot(entries) {
408
+ const rows = asArray(entries).filter((entry) => entry && entry.path);
409
+ if (rows.length === 0) return "- None recorded.";
410
+ return rows.map((entry) => `- [${entry.title}](${rootLink(entry.path)}): ${entry.source_refs || 0} source reference(s).`).join("\n");
411
+ }
412
+
413
+ function linkedListFromDashboard(entries) {
414
+ const rows = asArray(entries).filter((entry) => entry && entry.path);
415
+ if (rows.length === 0) return "- None recorded.";
416
+ return rows.map((entry) => `- [${entry.title}](${dashboardLink(entry.path)}): ${entry.source_refs || 0} source reference(s).`).join("\n");
417
+ }
418
+
419
+ function dashboardFrontmatter(id, title, payload, generatedAt, relatedPages = []) {
420
+ return {
421
+ id,
422
+ title,
423
+ type: "dashboard",
424
+ created_at: generatedAt,
425
+ updated_at: generatedAt,
426
+ aliases: [title],
427
+ tags: ["dashboard", "navigation"],
428
+ related_pages: relatedPages,
429
+ source_refs: asArray(payload.sources).map((source) => source.source_id).filter(Boolean).slice(0, 50),
430
+ confidence: "medium",
431
+ review_status: "needs_review",
432
+ };
433
+ }
434
+
435
+ function renderResearchToolLandscapeDashboard(payload) {
436
+ const tools = highSignalTools(payload, 16);
437
+ const concepts = topConcepts(payload, 8);
438
+ const comparisons = comparisonEntries(payload, 6);
439
+ const syntheses = synthesisEntries(payload, 6);
440
+ const categoryRows = groupToolsByCategory(tools);
441
+ const table = [
442
+ "| Category | Candidate count | High-signal examples |",
443
+ "|---|---:|---|",
444
+ ...categoryRows.map(({ category, rows }) => {
445
+ const examples = rows.slice(0, 4).map((tool) => tool.path ? `[${mdEscapeTable(tool.title)}](${dashboardLink(tool.path)})` : mdEscapeTable(tool.title)).join("<br>");
446
+ return `| ${mdEscapeTable(category)} | ${rows.length} | ${examples || "-"} |`;
447
+ }),
448
+ ].join("\n");
449
+ const highSignalTable = [
450
+ "| Tool | Category | Local evidence | Topics | Confidence |",
451
+ "|---|---|---:|---|---|",
452
+ ...tools.slice(0, 12).map((tool) => {
453
+ const label = tool.path ? `[${mdEscapeTable(tool.title)}](${dashboardLink(tool.path)})` : mdEscapeTable(tool.title);
454
+ return `| ${label} | ${mdEscapeTable(tool.category)} | ${tool.source_refs} | ${mdEscapeTable(tool.topics.slice(0, 4).join(", ") || "-")} | ${mdEscapeTable(tool.confidence)} |`;
455
+ }),
456
+ ].join("\n");
457
+ const body = [
458
+ "# Research / Tool Landscape Dashboard",
459
+ "",
460
+ "## Purpose",
461
+ "",
462
+ "Use this page as the starting map for browsing locally extracted repository and tool evidence. The links stay inside the generated local wiki and do not imply external verification.",
463
+ "",
464
+ "## Top Concepts",
465
+ "",
466
+ linkedListFromDashboard(concepts),
467
+ "",
468
+ "## Tool Clusters",
469
+ "",
470
+ table,
471
+ "",
472
+ "## High-Signal Tools",
473
+ "",
474
+ highSignalTable,
475
+ "",
476
+ "## Comparison And Synthesis Entry Points",
477
+ "",
478
+ "Comparisons:",
479
+ "",
480
+ linkedListFromDashboard(comparisons),
481
+ "",
482
+ "Syntheses:",
483
+ "",
484
+ linkedListFromDashboard(syntheses),
485
+ "",
486
+ "## Review Notes",
487
+ "",
488
+ "- Prefer tools with multiple local source references before opening an external verification issue.",
489
+ "- Treat topic/category clustering as local evidence only.",
490
+ "- Use the decision-support dashboard before making adoption recommendations.",
491
+ "",
492
+ ].join("\n");
493
+ return withPersonalBrainFrontmatter(
494
+ dashboardFrontmatter(
495
+ "dashboard_research_tool_landscape",
496
+ "Research / Tool Landscape Dashboard",
497
+ payload,
498
+ payload.generated_at || "",
499
+ ["../index.md", ...concepts.map((entry) => dashboardLink(entry.path)).filter(Boolean)]
500
+ ),
501
+ body
502
+ );
503
+ }
504
+
505
+ function renderDecisionSupportDashboard(payload) {
506
+ const comparisons = comparisonEntries(payload, 8);
507
+ const syntheses = synthesisEntries(payload, 8);
508
+ const tools = highSignalTools(payload, 10);
509
+ const comparisonTable = [
510
+ "| Decision surface | Candidates | Local source refs | First recommendation cue |",
511
+ "|---|---:|---:|---|",
512
+ ...comparisons.map((entry) => `| [${mdEscapeTable(entry.title)}](${dashboardLink(entry.path)}) | ${entry.rows} | ${entry.source_refs} | ${mdEscapeTable(entry.recommendations[0] || "review candidates")} |`),
513
+ ].join("\n");
514
+ const shortlist = [
515
+ "| Candidate | Category | Local evidence | Decision note |",
516
+ "|---|---|---:|---|",
517
+ ...tools.slice(0, 8).map((tool) => `| [${mdEscapeTable(tool.title)}](${dashboardLink(tool.path)}) | ${mdEscapeTable(tool.category)} | ${tool.source_refs} | Verify license, maintenance, security, and fit before adoption. |`),
518
+ ].join("\n");
519
+ const body = [
520
+ "# Decision-Support Dashboard",
521
+ "",
522
+ "## Purpose",
523
+ "",
524
+ "Use this page when deciding which locally sourced candidates deserve deeper review. It links comparison, synthesis, and high-signal tool pages without fetching external metadata.",
525
+ "",
526
+ "## Comparison Starting Points",
527
+ "",
528
+ comparisons.length > 0 ? comparisonTable : "- No comparison pages were generated from the local evidence.",
529
+ "",
530
+ "## Synthesis Starting Points",
531
+ "",
532
+ linkedListFromDashboard(syntheses),
533
+ "",
534
+ "## Candidate Shortlist",
535
+ "",
536
+ shortlist,
537
+ "",
538
+ "## Decision Checklist",
539
+ "",
540
+ "- Confirm the page has multiple local source references or a strong extracted snippet.",
541
+ "- Check comparison caveats before adoption.",
542
+ "- Verify license, maintainer activity, security posture, and current repository identity outside this local-only report.",
543
+ "- Record human acceptance criteria before treating a tool as recommended.",
544
+ "",
545
+ ].join("\n");
546
+ return withPersonalBrainFrontmatter(
547
+ dashboardFrontmatter(
548
+ "dashboard_decision_support",
549
+ "Decision-Support Dashboard",
550
+ payload,
551
+ payload.generated_at || "",
552
+ ["../index.md", ...comparisons.map((entry) => dashboardLink(entry.path)).filter(Boolean)]
553
+ ),
554
+ body
555
+ );
556
+ }
557
+
558
+ function renderMaintenanceQualityDashboard(payload) {
559
+ const findings = asArray(payload.source_quality_findings);
560
+ const unsupported = asArray(payload.unsupported_items);
561
+ const sources = sourceEntries(payload, 10);
562
+ const maintenanceLinks = [];
563
+ if (findings.length > 0 || unsupported.length > 0) {
564
+ maintenanceLinks.push("[Extraction Quality Review](../maintenance/extraction-quality-review.md)");
565
+ }
566
+ const findingList = findings.slice(0, 12).map((finding) =>
567
+ `- ${finding.finding_id || finding.source_id || "finding"}: ${asArray(finding.quality_flags).join(", ") || finding.reason || "review required"}`
568
+ ).join("\n") || "- No source-quality findings recorded.";
569
+ const unsupportedList = unsupported.slice(0, 12).map((item) =>
570
+ `- ${item.item_id || "unsupported"}: ${item.reason || item.raw_observation_summary || "review required"}`
571
+ ).join("\n") || "- No unsupported extraction items recorded.";
572
+ const body = [
573
+ "# Maintenance / Quality Dashboard",
574
+ "",
575
+ "## Purpose",
576
+ "",
577
+ "Use this page to triage local extraction quality before relying on generated concept, comparison, synthesis, or tool pages.",
578
+ "",
579
+ "## Quality Snapshot",
580
+ "",
581
+ `- source-quality findings: ${findings.length}`,
582
+ `- unsupported items: ${unsupported.length}`,
583
+ `- source pages tracked: ${asArray(payload.sources).length}`,
584
+ `- tool candidates tracked: ${asArray(payload.tool_entities).length}`,
585
+ "",
586
+ "## Maintenance Entry Points",
587
+ "",
588
+ maintenanceLinks.length > 0 ? maintenanceLinks.map((link) => `- ${link}`).join("\n") : "- No dedicated maintenance review page was required for this extraction.",
589
+ "",
590
+ "## Source Findings",
591
+ "",
592
+ findingList,
593
+ "",
594
+ "## Unsupported Items",
595
+ "",
596
+ unsupportedList,
597
+ "",
598
+ "## Source Pages To Recheck",
599
+ "",
600
+ linkedListFromDashboard(sources.map((source) => ({ title: source.title, path: source.path, source_refs: source.quality_flags.length }))),
601
+ "",
602
+ "## Operating Notes",
603
+ "",
604
+ "- Fix source-quality warnings before treating dashboard rankings as final.",
605
+ "- Keep destructive cleanup, archive, and delete decisions outside this generated dashboard.",
606
+ "- Re-run lint after compile/apply to verify internal links and required fields.",
607
+ "",
608
+ ].join("\n");
609
+ return withPersonalBrainFrontmatter(
610
+ dashboardFrontmatter("dashboard_maintenance_quality", "Maintenance / Quality Dashboard", payload, payload.generated_at || "", ["../index.md"]),
611
+ body
612
+ );
613
+ }
614
+
615
+ function renderSourceCoverageDashboard(payload) {
616
+ const sources = sourceEntries(payload, 30);
617
+ const counts = payload.source_counts || {};
618
+ const sourceTable = [
619
+ "| Source | Quality flags | Source URL | Hash prefix |",
620
+ "|---|---|---|---|",
621
+ ...sources.map((source) => {
622
+ const label = source.path ? `[${mdEscapeTable(source.title)}](${dashboardLink(source.path)})` : mdEscapeTable(source.title);
623
+ const sourceUrl = source.source_url ? `[source](${source.source_url})` : "-";
624
+ return `| ${label} | ${mdEscapeTable(source.quality_flags.join(", ") || "none")} | ${sourceUrl} | ${mdEscapeTable(source.source_hash.slice(0, 12) || "-")} |`;
625
+ }),
626
+ ].join("\n");
627
+ const body = [
628
+ "# Source Coverage Dashboard",
629
+ "",
630
+ "## Purpose",
631
+ "",
632
+ "Use this page to understand which local files fed the generated local wiki and where source-level review should start.",
633
+ "",
634
+ "## Coverage Snapshot",
635
+ "",
636
+ `- sources scanned: ${counts.scanned ?? "(unknown)"}`,
637
+ `- sources indexed: ${counts.indexed ?? "(unknown)"}`,
638
+ `- source records: ${asArray(payload.sources).length}`,
639
+ `- tool candidates: ${asArray(payload.tool_entities).length}`,
640
+ `- concepts: ${asArray(payload.concepts).length}`,
641
+ `- comparisons: ${asArray(payload.comparisons).length}`,
642
+ `- syntheses: ${asArray(payload.syntheses).length}`,
643
+ "",
644
+ "## Source Pages",
645
+ "",
646
+ sourceTable,
647
+ "",
648
+ "## Coverage Notes",
649
+ "",
650
+ "- The dashboard is derived from local source registry and semantic extraction output only.",
651
+ "- Source hashes should remain stable across compile and enrichment/report-only flows.",
652
+ "- Investigate any lint provenance finding before using the generated graph as a decision record.",
653
+ "",
654
+ ].join("\n");
655
+ return withPersonalBrainFrontmatter(
656
+ dashboardFrontmatter("dashboard_source_coverage", "Source Coverage Dashboard", payload, payload.generated_at || "", ["../index.md"]),
657
+ body
658
+ );
659
+ }
660
+
661
+ function renderDashboardPage(name, payload) {
662
+ if (name === "research-tool-landscape") return renderResearchToolLandscapeDashboard(payload);
663
+ if (name === "decision-support") return renderDecisionSupportDashboard(payload);
664
+ if (name === "maintenance-quality") return renderMaintenanceQualityDashboard(payload);
665
+ if (name === "source-coverage") return renderSourceCoverageDashboard(payload);
666
+ return renderResearchToolLandscapeDashboard(payload);
667
+ }
668
+
669
+ function yamlScalar(value) {
670
+ return `"${String(value ?? "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r?\n/g, " ").trim()}"`;
671
+ }
672
+
673
+ function yamlInlineList(values) {
674
+ const items = [...new Set(asArray(values).filter(Boolean).map(String))];
675
+ return `[${items.map(yamlScalar).join(", ")}]`;
676
+ }
677
+
678
+ function frontmatterBlock(fields) {
679
+ const lines = ["---"];
680
+ for (const [key, value] of Object.entries(fields)) {
681
+ if (Array.isArray(value)) {
682
+ lines.push(`${key}: ${yamlInlineList(value)}`);
683
+ } else {
684
+ lines.push(`${key}: ${yamlScalar(value)}`);
685
+ }
686
+ }
687
+ lines.push("---", "");
688
+ return lines.join("\n");
689
+ }
690
+
691
+ function withPersonalBrainFrontmatter(fields, body) {
692
+ return `${frontmatterBlock({
693
+ schema_version: "1",
694
+ product: "SDTK-BRAIN",
695
+ managed_by: "sdtk-wiki",
696
+ status: "generated",
697
+ aliases: [],
698
+ tags: [],
699
+ related_pages: [],
700
+ source_refs: [],
701
+ confidence: "medium",
702
+ review_status: "needs_review",
703
+ ...fields,
704
+ })}${String(body || "").replace(/^\s+/, "")}`;
705
+ }
706
+
707
+ function recordTitle(record, fallback) {
708
+ return String(record.title || record.name || record.topic || record.entity_id || record.concept_id || record.source_id || fallback || "Generated page");
709
+ }
710
+
711
+ function renderSourcePage(source, generatedAt) {
712
+ const title = recordTitle(source, "Source");
713
+ const body = [
714
+ `# ${title}`,
715
+ "",
716
+ "## Summary",
717
+ "",
718
+ `Local source page generated from ${source.source_logical_path || source.source_relative_path || source.source_id}.`,
719
+ "",
720
+ "## Source Metadata",
721
+ "",
722
+ `- source_id: ${source.source_id}`,
723
+ `- source_relative_path: ${source.source_relative_path || "(missing)"}`,
724
+ `- source_logical_path: ${source.source_logical_path || "(missing)"}`,
725
+ `- source_url: ${source.source_url || "(missing)"}`,
726
+ `- source_hash: ${source.source_hash || "(missing)"}`,
727
+ `- encoding_quality: ${source.encoding_quality || "(unknown)"}`,
728
+ "",
729
+ "## Source Quality",
730
+ "",
731
+ mdList(source.source_quality && source.source_quality.quality_flags),
732
+ "",
733
+ "## Thinness Policy",
734
+ "",
735
+ "This page is an accepted source/provenance anchor. Its purpose is to preserve local source identity, hash, quality flags, and provenance rather than to carry semantic analysis. Strict stub quality is measured on tool, concept, comparison, and synthesis pages.",
736
+ "",
737
+ "## Provenance",
738
+ "",
739
+ mdList(provenanceRefsFor(source)),
740
+ "",
741
+ ].join("\n");
742
+ return withPersonalBrainFrontmatter({
743
+ id: source.source_id,
744
+ title,
745
+ type: "source",
746
+ created_at: generatedAt,
747
+ updated_at: generatedAt,
748
+ aliases: source.source_logical_path ? [source.source_logical_path] : [],
749
+ tags: ["source", source.source_type || "local"],
750
+ thinness_policy: "accepted_source_provenance_anchor",
751
+ source_refs: sourceRefsFor(source),
752
+ confidence: source.source_quality && source.source_quality.low_confidence_extraction ? "low" : "medium",
753
+ review_status: source.source_quality && source.source_quality.low_confidence_extraction ? "needs_review" : "ready_for_review",
754
+ }, body);
755
+ }
756
+
757
+ function renderToolEntityPage(entity, generatedAt) {
758
+ const title = recordTitle(entity, "Tool entity");
759
+ const evidenceRecords = asArray(entity.evidence_records);
760
+ const firstSnippet = asArray(entity.evidence_snippets)[0] || (evidenceRecords[0] && evidenceRecords[0].snippet);
761
+ const discoverySources = [
762
+ ...asArray(entity.source_links),
763
+ ...asArray(entity.discovery_sources),
764
+ ...evidenceRecords.map((record) => record.source_link || record.source_logical_path).filter(Boolean),
765
+ ];
766
+ const sourceCount = sourceRefsFor(entity).length;
767
+ const whyItMatters = entity.summary
768
+ ? `Local evidence presents ${title} as relevant to ${entity.category || "the extracted tool landscape"}. ${entity.summary}`
769
+ : `Local evidence links ${title} to ${entity.category || "the extracted tool landscape"} with ${sourceCount} source reference(s).`;
770
+ const whenToUse = asArray(entity.topics).length > 0
771
+ ? `Review when exploring ${asArray(entity.topics).slice(0, 4).join(", ")} options from the local source set.`
772
+ : "Review when evaluating whether this repository should become a maintained tool note.";
773
+ const overlap = asArray(entity.related_repos).length > 0
774
+ ? "Related repositories were inferred from shared local topic labels or category evidence. Treat this as a review cue, not external verification."
775
+ : "No overlap was inferred from local topic labels in this slice.";
776
+ const body = [
777
+ `# ${title}`,
778
+ "",
779
+ "## Summary",
780
+ "",
781
+ entity.summary || `Generated tool entity page for ${entity.name || entity.entity_id}.`,
782
+ "",
783
+ "## Key Facts",
784
+ "",
785
+ `- Canonical URL: ${entity.github_url || "(missing)"}`,
786
+ `- Owner: ${entity.repo_owner || "(missing)"}`,
787
+ `- Repo name: ${entity.repo_name || "(missing)"}`,
788
+ `- Category: ${entity.category || "(unknown)"}`,
789
+ `- Confidence: ${entity.confidence_tier || entity.confidence || "medium"}`,
790
+ `- Local source refs: ${sourceCount}`,
791
+ "",
792
+ "## Discovery Source",
793
+ "",
794
+ mdLinkedList([...new Set(discoverySources.filter(Boolean).map(String))]),
795
+ "",
796
+ "## Extracted Snippet",
797
+ "",
798
+ quoteBlock(firstSnippet),
799
+ "",
800
+ "## Topic Labels",
801
+ "",
802
+ topicLabelList(entity),
803
+ "",
804
+ "## Why It Matters",
805
+ "",
806
+ whyItMatters,
807
+ "",
808
+ "## When To Use",
809
+ "",
810
+ whenToUse,
811
+ "",
812
+ "## Related Repos",
813
+ "",
814
+ relatedRepoList(entity),
815
+ "",
816
+ "## Overlaps / Differences",
817
+ "",
818
+ overlap,
819
+ "",
820
+ "## Open Questions",
821
+ "",
822
+ "- Verify license, maintenance status, and project maturity before relying on this tool.",
823
+ "- Confirm whether the local topic labels match the repository's actual current scope.",
824
+ "- Decide whether this should remain a tool entity or be reclassified after review.",
825
+ "",
826
+ "## Local Evidence Records",
827
+ "",
828
+ evidenceRecords.length > 0
829
+ ? evidenceRecords.map((record) => [
830
+ `- source_id: ${record.source_id || "(missing)"}`,
831
+ ` - source: ${record.source_link || record.source_logical_path || "(missing)"}`,
832
+ ` - topics: ${asArray(record.topics).join(", ") || "(none)"}`,
833
+ ` - confidence: ${record.confidence_tier || record.confidence || "(unknown)"}`,
834
+ ].join("\n")).join("\n")
835
+ : "- No structured evidence records recorded.",
836
+ "",
837
+ "## Provenance",
838
+ "",
839
+ mdList(provenanceRefsFor(entity)),
840
+ "",
841
+ ].join("\n");
842
+ return withPersonalBrainFrontmatter({
843
+ id: entity.entity_id,
844
+ title,
845
+ type: "tool_entity",
846
+ created_at: generatedAt,
847
+ updated_at: generatedAt,
848
+ aliases: [entity.repo_name, entity.github_url].filter(Boolean),
849
+ tags: ["tool", entity.category, ...asArray(entity.topics)].filter(Boolean),
850
+ source_refs: sourceRefsFor(entity),
851
+ confidence: entity.confidence_tier || entity.confidence || "medium",
852
+ review_status: "ready_for_review",
853
+ }, body);
854
+ }
855
+
856
+ function renderConceptPage(concept, generatedAt) {
857
+ const title = recordTitle(concept, "Concept");
858
+ const relatedCount = asArray(concept.related_entity_details).length || asArray(concept.related_entities).length;
859
+ const sourceCount = sourceRefsFor(concept).length;
860
+ const summary = concept.definition
861
+ || `Local sources contain ${relatedCount} tool candidate(s) and ${sourceCount} source reference(s) related to ${title}.`;
862
+ const body = [
863
+ `# ${title}`,
864
+ "",
865
+ "## Summary",
866
+ "",
867
+ summary,
868
+ "",
869
+ "## Decision Brief",
870
+ "",
871
+ conceptDecisionBrief(concept),
872
+ "",
873
+ "## Key Axes",
874
+ "",
875
+ conceptAxisList(concept),
876
+ "",
877
+ "## Implementations / Examples",
878
+ "",
879
+ conceptImplementationList(concept),
880
+ "",
881
+ "## Patterns",
882
+ "",
883
+ conceptPatternList(concept),
884
+ "",
885
+ "## Recommendations / Caveats",
886
+ "",
887
+ "- Treat this concept page as local-evidence synthesis, not external verification.",
888
+ "- Prioritize examples with multiple source references or stronger extraction confidence.",
889
+ "- Review source-quality warnings before making architecture or tool adoption decisions.",
890
+ "",
891
+ "## Open Questions",
892
+ "",
893
+ "- Which extracted implementation is mature enough for real project adoption?",
894
+ "- Are the local topic labels accurate after human review?",
895
+ "- Should this concept be split, merged, or promoted after comparison/synthesis enrichment?",
896
+ "",
897
+ "## Aliases",
898
+ "",
899
+ mdList(concept.aliases),
900
+ "",
901
+ "## Related Pages",
902
+ "",
903
+ conceptRelatedPagesList(concept),
904
+ "",
905
+ "## Source References",
906
+ "",
907
+ mdList(sourceRefsFor(concept)),
908
+ "",
909
+ "## Provenance",
910
+ "",
911
+ mdList(provenanceRefsFor(concept)),
912
+ "",
913
+ ].join("\n");
914
+ return withPersonalBrainFrontmatter({
915
+ id: concept.concept_id,
916
+ title,
917
+ type: "concept",
918
+ created_at: generatedAt,
919
+ updated_at: generatedAt,
920
+ aliases: concept.aliases,
921
+ tags: ["concept"],
922
+ related_pages: asArray(concept.related_entity_details).map((entity) => entity.target_page_path ? `../${relativePersonalBrainLink(entity.target_page_path)}` : null).filter(Boolean).slice(0, 12),
923
+ source_refs: sourceRefsFor(concept),
924
+ confidence: concept.confidence_tier || concept.confidence || "medium",
925
+ review_status: "ready_for_review",
926
+ }, body);
927
+ }
928
+
929
+ function renderComparisonPage(comparison, generatedAt) {
930
+ const title = recordTitle(comparison, "Comparison");
931
+ const body = [
932
+ `# ${title}`,
933
+ "",
934
+ "## Summary",
935
+ "",
936
+ comparison.summary || `Local comparison for ${comparison.topic || comparison.comparison_id}.`,
937
+ "",
938
+ "## Decision Guidance",
939
+ "",
940
+ comparisonDecisionGuidance(comparison),
941
+ "",
942
+ "## Decision Axes",
943
+ "",
944
+ conceptAxisList({ key_axes: comparison.decision_axes }),
945
+ "",
946
+ "## Comparison Matrix",
947
+ "",
948
+ comparisonMatrix(comparison),
949
+ "",
950
+ "## Candidate Tools / Repos",
951
+ "",
952
+ candidateToolList(comparison.matrix_rows),
953
+ "",
954
+ "## Recommendations",
955
+ "",
956
+ mdList(comparison.recommendations),
957
+ "",
958
+ "## Caveats",
959
+ "",
960
+ mdList(comparison.caveats),
961
+ "",
962
+ "## Source Confidence",
963
+ "",
964
+ confidenceSummaryList(comparison.source_confidence_summary),
965
+ "",
966
+ "## Open Questions",
967
+ "",
968
+ "- Which candidate should be verified first with external source checks?",
969
+ "- Which criteria matter most for the target SDTK use case?",
970
+ "- Are any candidates duplicates, abandoned, or misclassified in the local source set?",
971
+ "",
972
+ "## Source References",
973
+ "",
974
+ mdList(sourceRefsFor(comparison)),
975
+ "",
976
+ "## Provenance",
977
+ "",
978
+ mdList(provenanceRefsFor(comparison)),
979
+ "",
980
+ ].join("\n");
981
+ return withPersonalBrainFrontmatter({
982
+ id: comparison.comparison_id,
983
+ title,
984
+ type: "comparison",
985
+ created_at: generatedAt,
986
+ updated_at: generatedAt,
987
+ aliases: [comparison.topic].filter(Boolean),
988
+ tags: ["comparison"],
989
+ related_pages: asArray(comparison.compared_entity_details).map((entity) => entity.target_page_path ? `../${relativePersonalBrainLink(entity.target_page_path)}` : null).filter(Boolean).slice(0, 12),
990
+ source_refs: sourceRefsFor(comparison),
991
+ confidence: comparison.confidence_tier || comparison.confidence || "medium",
992
+ review_status: "needs_review",
993
+ }, body);
994
+ }
995
+
996
+ function renderSynthesisPage(synthesis, generatedAt) {
997
+ const title = recordTitle(synthesis, "Synthesis");
998
+ const body = [
999
+ `# ${title}`,
1000
+ "",
1001
+ "## Summary",
1002
+ "",
1003
+ synthesis.summary || `Generated synthesis page for ${synthesis.topic || synthesis.synthesis_id}.`,
1004
+ "",
1005
+ "## Decision Narrative",
1006
+ "",
1007
+ synthesisDecisionNarrative(synthesis),
1008
+ "",
1009
+ "## Landscape Snapshot",
1010
+ "",
1011
+ candidateToolList(synthesis.candidate_tools),
1012
+ "",
1013
+ "## Key Patterns",
1014
+ "",
1015
+ conceptPatternList({ patterns: synthesis.patterns }),
1016
+ "",
1017
+ "## Decision Axes",
1018
+ "",
1019
+ conceptAxisList({ key_axes: synthesis.landscape_axes }),
1020
+ "",
1021
+ "## Recommended Review Path",
1022
+ "",
1023
+ mdList(synthesis.recommendations),
1024
+ "",
1025
+ "## Caveats",
1026
+ "",
1027
+ mdList(synthesis.caveats),
1028
+ "",
1029
+ "## Source Confidence",
1030
+ "",
1031
+ confidenceSummaryList(synthesis.source_confidence_summary),
1032
+ "",
1033
+ "## Related Comparisons",
1034
+ "",
1035
+ synthesis.related_comparison_path ? mdList([`../${relativePersonalBrainLink(synthesis.related_comparison_path)}`]) : "- None recorded.",
1036
+ "",
1037
+ "## Open Questions",
1038
+ "",
1039
+ "- Which comparison criteria should become product acceptance gates?",
1040
+ "- Which local candidates need external verification before adoption?",
1041
+ "- Does this cluster need a dedicated deeper synthesis pass?",
1042
+ "",
1043
+ "## Source References",
1044
+ "",
1045
+ mdList(sourceRefsFor(synthesis)),
1046
+ "",
1047
+ "## Provenance",
1048
+ "",
1049
+ mdList(provenanceRefsFor(synthesis)),
1050
+ "",
1051
+ ].join("\n");
1052
+ return withPersonalBrainFrontmatter({
1053
+ id: synthesis.synthesis_id,
1054
+ title,
1055
+ type: "synthesis",
1056
+ created_at: generatedAt,
1057
+ updated_at: generatedAt,
1058
+ aliases: [synthesis.topic].filter(Boolean),
1059
+ tags: ["synthesis"],
1060
+ related_pages: synthesis.related_comparison_path ? [`../${relativePersonalBrainLink(synthesis.related_comparison_path)}`] : [],
1061
+ source_refs: sourceRefsFor(synthesis),
1062
+ confidence: synthesis.confidence_tier || synthesis.confidence || "medium",
1063
+ review_status: "needs_review",
1064
+ }, body);
1065
+ }
1066
+
1067
+ function renderRootPage(name, payload) {
1068
+ const extractionCounts = payload && payload.source_counts ? payload.source_counts : {};
1069
+ const concepts = topConcepts(payload, 8);
1070
+ const comparisons = comparisonEntries(payload, 6);
1071
+ const syntheses = synthesisEntries(payload, 6);
1072
+ const tools = highSignalTools(payload, 10);
1073
+ const sources = sourceEntries(payload, 8);
1074
+ const dashboards = [
1075
+ ["Research / Tool Landscape", "dashboards/research-tool-landscape.md"],
1076
+ ["Decision Support", "dashboards/decision-support.md"],
1077
+ ["Maintenance / Quality", "dashboards/maintenance-quality.md"],
1078
+ ["Source Coverage", "dashboards/source-coverage.md"],
1079
+ ];
1080
+ const titles = {
1081
+ index: "Local Wiki Index",
1082
+ overview: "Local Wiki Overview",
1083
+ ontology: "Local Wiki Ontology",
1084
+ taxonomy: "Local Wiki Taxonomy",
1085
+ graph: "Local Wiki Graph",
1086
+ log: "Local Wiki Generation Log",
1087
+ };
1088
+ const title = titles[name] || "Local Wiki";
1089
+ const dashboardLinks = dashboards.map(([label, target]) => `- [${label}](${target})`).join("\n");
1090
+ const topToolLinks = tools.length > 0
1091
+ ? tools.slice(0, 8).map((tool) => `- [${tool.title}](${rootLink(tool.path)}): ${tool.category}; ${tool.source_refs} source reference(s).`).join("\n")
1092
+ : "- None recorded.";
1093
+ const sourceLinks = sources.length > 0
1094
+ ? sources.map((source) => `- [${source.title}](${rootLink(source.path)}): ${source.quality_flags.length > 0 ? source.quality_flags.join(", ") : "no quality flags"}.`).join("\n")
1095
+ : "- None recorded.";
1096
+ const taxonomyRows = [
1097
+ "| Area | Count | Starting points |",
1098
+ "|---|---:|---|",
1099
+ `| Sources | ${asArray(payload.sources).length} | [Source coverage dashboard](dashboards/source-coverage.md) |`,
1100
+ `| Tool entities | ${asArray(payload.tool_entities).length} | [Research / Tool Landscape](dashboards/research-tool-landscape.md) |`,
1101
+ `| Concepts | ${asArray(payload.concepts).length} | ${concepts.slice(0, 4).map((entry) => entry.path ? `[${mdEscapeTable(entry.title)}](${rootLink(entry.path)})` : mdEscapeTable(entry.title)).join("<br>") || "-"} |`,
1102
+ `| Comparisons | ${asArray(payload.comparisons).length} | ${comparisons.slice(0, 4).map((entry) => entry.path ? `[${mdEscapeTable(entry.title)}](${rootLink(entry.path)})` : mdEscapeTable(entry.title)).join("<br>") || "-"} |`,
1103
+ `| Syntheses | ${asArray(payload.syntheses).length} | ${syntheses.slice(0, 4).map((entry) => entry.path ? `[${mdEscapeTable(entry.title)}](${rootLink(entry.path)})` : mdEscapeTable(entry.title)).join("<br>") || "-"} |`,
1104
+ ].join("\n");
1105
+ const graphRows = [
1106
+ "| Graph surface | Count | Navigation |",
1107
+ "|---|---:|---|",
1108
+ `| Relations | ${asArray(payload.relations).length} | Review relation append sections below. |`,
1109
+ `| Concepts | ${asArray(payload.concepts).length} | ${linkedListFromRoot(concepts.slice(0, 5)).replace(/\n/g, "<br>")} |`,
1110
+ `| Comparisons | ${asArray(payload.comparisons).length} | ${linkedListFromRoot(comparisons.slice(0, 5)).replace(/\n/g, "<br>")} |`,
1111
+ `| Syntheses | ${asArray(payload.syntheses).length} | ${linkedListFromRoot(syntheses.slice(0, 5)).replace(/\n/g, "<br>")} |`,
1112
+ ].join("\n");
1113
+ const pageSpecificSections = {
1114
+ index: [
1115
+ "## Start Here",
1116
+ "",
1117
+ dashboardLinks,
1118
+ "",
1119
+ "## High-Signal Tool Entry Points",
1120
+ "",
1121
+ topToolLinks,
1122
+ "",
1123
+ "## Concept / Comparison / Synthesis Entry Points",
1124
+ "",
1125
+ "Concepts:",
1126
+ "",
1127
+ linkedListFromRoot(concepts),
1128
+ "",
1129
+ "Comparisons:",
1130
+ "",
1131
+ linkedListFromRoot(comparisons),
1132
+ "",
1133
+ "Syntheses:",
1134
+ "",
1135
+ linkedListFromRoot(syntheses),
1136
+ ],
1137
+ overview: [
1138
+ "## Session Brief",
1139
+ "",
1140
+ "This generated local wiki is organized for local review. Start with dashboards, inspect top concepts, then use comparison and synthesis pages before adopting any tool.",
1141
+ "",
1142
+ "## Current Landscape",
1143
+ "",
1144
+ `- high-signal tools listed: ${tools.length}`,
1145
+ `- top concepts listed: ${concepts.length}`,
1146
+ `- comparison pages listed: ${comparisons.length}`,
1147
+ `- synthesis pages listed: ${syntheses.length}`,
1148
+ "",
1149
+ "## Recommended Browsing Path",
1150
+ "",
1151
+ "- [Research / Tool Landscape](dashboards/research-tool-landscape.md)",
1152
+ "- [Decision Support](dashboards/decision-support.md)",
1153
+ "- [Source Coverage](dashboards/source-coverage.md)",
1154
+ "- [Maintenance / Quality](dashboards/maintenance-quality.md)",
1155
+ ],
1156
+ taxonomy: [
1157
+ "## Taxonomy Snapshot",
1158
+ "",
1159
+ taxonomyRows,
1160
+ "",
1161
+ "## Concept Clusters",
1162
+ "",
1163
+ concepts.length > 0
1164
+ ? concepts.map((concept) => `- [${concept.title}](${rootLink(concept.path)}): ${concept.related_count} related tool candidate(s); axes: ${concept.axes.slice(0, 4).join(", ") || "none inferred"}.`).join("\n")
1165
+ : "- None recorded.",
1166
+ ],
1167
+ graph: [
1168
+ "## Navigation Graph",
1169
+ "",
1170
+ graphRows,
1171
+ "",
1172
+ "## Tool Cluster Hubs",
1173
+ "",
1174
+ groupToolsByCategory(tools).slice(0, 8).map(({ category, rows }) =>
1175
+ `- ${category}: ${rows.slice(0, 4).map((tool) => `[${tool.title}](${rootLink(tool.path)})`).join(", ")}`
1176
+ ).join("\n") || "- None recorded.",
1177
+ ],
1178
+ log: [
1179
+ "## Review Trail",
1180
+ "",
1181
+ "- Compile operations were derived from the semantic extraction report.",
1182
+ "- Dashboard and navigation pages are generated from the same local evidence as entity/concept/comparison/synthesis pages.",
1183
+ "- Re-run lint after compile/apply to verify internal links and quality gates.",
1184
+ ],
1185
+ ontology: [
1186
+ "## Ontology Navigation",
1187
+ "",
1188
+ "- source: local file/source registry page.",
1189
+ "- tool_entity: extracted GitHub repository or tool candidate page.",
1190
+ "- concept: local thematic grouping across tools and source evidence.",
1191
+ "- comparison: decision matrix for a concept cluster.",
1192
+ "- synthesis: higher-level review path for a cluster.",
1193
+ "- dashboard: curated generated navigation page for browsing and triage.",
1194
+ ],
1195
+ };
1196
+ const body = [
1197
+ `# ${title}`,
1198
+ "",
1199
+ "## Generation Context",
1200
+ "",
1201
+ `- record_type: ${payload.record_type || "(unknown)"}`,
1202
+ `- generated_at: ${payload.generated_at || "(unknown)"}`,
1203
+ `- sources_scanned: ${extractionCounts.scanned ?? "(unknown)"}`,
1204
+ `- sources_indexed: ${extractionCounts.indexed ?? "(unknown)"}`,
1205
+ `- tool_candidates: ${asArray(payload.tool_entities).length}`,
1206
+ `- concept_candidates: ${asArray(payload.concepts).length}`,
1207
+ "",
1208
+ "## Structure",
1209
+ "",
1210
+ "- sources/",
1211
+ "- entities/tools/",
1212
+ "- concepts/",
1213
+ "- comparisons/",
1214
+ "- syntheses/",
1215
+ "- dashboards/",
1216
+ "- maintenance/",
1217
+ "",
1218
+ ...(pageSpecificSections[name] || []),
1219
+ "",
1220
+ "## Source Coverage",
1221
+ "",
1222
+ sourceLinks,
1223
+ "",
1224
+ ].join("\n");
1225
+ return withPersonalBrainFrontmatter({
1226
+ id: `personal_brain_${name}`,
1227
+ title,
1228
+ type: "root",
1229
+ created_at: payload.generated_at || "",
1230
+ updated_at: payload.generated_at || "",
1231
+ aliases: [name],
1232
+ tags: ["local-wiki", name],
1233
+ related_pages: dashboards.map(([, target]) => target),
1234
+ source_refs: asArray(payload.sources).map((source) => source.source_id).filter(Boolean).slice(0, 50),
1235
+ confidence: "medium",
1236
+ review_status: "needs_review",
1237
+ }, body);
1238
+ }
1239
+
1240
+ function renderMaintenancePage(payload) {
1241
+ const body = [
1242
+ "# Extraction Quality Review",
1243
+ "",
1244
+ "## Source Quality Findings",
1245
+ "",
1246
+ mdList(asArray(payload.source_quality_findings).map((finding) => `${finding.finding_id || finding.source_id}: ${asArray(finding.quality_flags).join(", ") || "review required"}`)),
1247
+ "",
1248
+ "## Unsupported Items",
1249
+ "",
1250
+ mdList(asArray(payload.unsupported_items).map((item) => `${item.item_id || "unsupported"}: ${item.reason || item.raw_observation_summary || "review required"}`)),
1251
+ "",
1252
+ ].join("\n");
1253
+ return withPersonalBrainFrontmatter({
1254
+ id: "maintenance_extraction_quality_review",
1255
+ title: "Extraction Quality Review",
1256
+ type: "maintenance",
1257
+ created_at: payload.generated_at || "",
1258
+ updated_at: payload.generated_at || "",
1259
+ aliases: ["source quality", "extraction quality"],
1260
+ tags: ["maintenance", "quality"],
1261
+ source_refs: asArray(payload.sources).map((source) => source.source_id).filter(Boolean).slice(0, 50),
1262
+ confidence: asArray(payload.unsupported_items).length > 0 ? "low" : "medium",
1263
+ review_status: "needs_review",
1264
+ }, body);
1265
+ }
1266
+
1267
+ function pushCreatePageOperation(operations, raw) {
1268
+ if (!raw.target_page_path) return;
1269
+ operations.push({
1270
+ operation_id: raw.operation_id,
1271
+ operation_type: "create_page",
1272
+ source_id: raw.source_id || "semantic_extraction_report",
1273
+ source_hash: raw.source_hash || "",
1274
+ target_page_path: raw.target_page_path,
1275
+ content: raw.content || "",
1276
+ proposed_content_summary: raw.proposed_content_summary,
1277
+ source_refs: raw.source_refs || [],
1278
+ provenance_refs: raw.provenance_refs || [],
1279
+ evidence_refs: raw.evidence_refs || [],
1280
+ confidence: raw.confidence || "medium",
1281
+ review_status: raw.review_status || "needs_review",
1282
+ });
1283
+ }
1284
+
1285
+ function operationsFromSemanticExtraction(payload) {
1286
+ const sourceById = new Map(asArray(payload.sources).map((source) => [String(source.source_id), source]));
1287
+ const operations = [];
1288
+ const reportHash = stableHash(JSON.stringify({
1289
+ generated_at: payload.generated_at,
1290
+ source_counts: payload.source_counts,
1291
+ })).slice(0, 12);
1292
+ const rootSourceId = `semantic_extraction_${reportHash}`;
1293
+ const rootSourceHash = reportHash;
1294
+
1295
+ for (const source of asArray(payload.sources)) {
1296
+ pushCreatePageOperation(operations, {
1297
+ operation_id: `pb-source-${String(operations.length + 1).padStart(3, "0")}`,
1298
+ source_id: source.source_id,
1299
+ source_hash: source.source_hash,
1300
+ target_page_path: source.target_page_path,
1301
+ content: renderSourcePage(source, payload.generated_at),
1302
+ proposed_content_summary: `Create local wiki source page for ${source.source_logical_path || source.source_relative_path || source.source_id}.`,
1303
+ source_refs: sourceRefsFor(source),
1304
+ provenance_refs: provenanceRefsFor(source),
1305
+ evidence_refs: refsFor(source),
1306
+ confidence: source.source_quality && source.source_quality.low_confidence_extraction ? "low" : "medium",
1307
+ review_status: source.source_quality && source.source_quality.low_confidence_extraction ? "needs_review" : "ready_for_review",
1308
+ });
1309
+ }
1310
+
1311
+ for (const entity of asArray(payload.tool_entities)) {
1312
+ const sourceId = firstSourceRef(entity);
1313
+ pushCreatePageOperation(operations, {
1314
+ operation_id: `pb-tool-${String(operations.length + 1).padStart(3, "0")}`,
1315
+ source_id: sourceId,
1316
+ source_hash: sourceHashFor(sourceById, sourceId),
1317
+ target_page_path: entity.target_page_path,
1318
+ content: renderToolEntityPage(entity, payload.generated_at),
1319
+ proposed_content_summary: `Create tool entity page for ${entity.name || entity.entity_id}.`,
1320
+ source_refs: sourceRefsFor(entity),
1321
+ provenance_refs: provenanceRefsFor(entity),
1322
+ evidence_refs: refsFor(entity),
1323
+ confidence: entity.confidence_tier || entity.confidence || "medium",
1324
+ review_status: "ready_for_review",
1325
+ });
1326
+ operations.push({
1327
+ operation_id: `pb-tool-source-ref-${String(operations.length + 1).padStart(3, "0")}`,
1328
+ operation_type: "add_source_ref",
1329
+ source_id: sourceId,
1330
+ source_hash: sourceHashFor(sourceById, sourceId),
1331
+ target_page_path: entity.target_page_path,
1332
+ content_sections: [
1333
+ {
1334
+ title: "Source References",
1335
+ body: mdList(sourceRefsFor(entity)),
1336
+ },
1337
+ ],
1338
+ proposed_content_summary: `Add source references for tool entity ${entity.name || entity.entity_id}.`,
1339
+ source_refs: sourceRefsFor(entity),
1340
+ provenance_refs: provenanceRefsFor(entity),
1341
+ evidence_refs: refsFor(entity),
1342
+ confidence: entity.confidence_tier || entity.confidence || "medium",
1343
+ review_status: "ready_for_review",
1344
+ });
1345
+ }
1346
+
1347
+ for (const concept of asArray(payload.concepts)) {
1348
+ const sourceId = firstSourceRef(concept);
1349
+ pushCreatePageOperation(operations, {
1350
+ operation_id: `pb-concept-${String(operations.length + 1).padStart(3, "0")}`,
1351
+ source_id: sourceId,
1352
+ source_hash: sourceHashFor(sourceById, sourceId),
1353
+ target_page_path: concept.target_page_path,
1354
+ content: renderConceptPage(concept, payload.generated_at),
1355
+ proposed_content_summary: `Create concept page for ${concept.name || concept.concept_id}.`,
1356
+ source_refs: sourceRefsFor(concept),
1357
+ provenance_refs: provenanceRefsFor(concept),
1358
+ evidence_refs: refsFor(concept),
1359
+ confidence: concept.confidence_tier || concept.confidence || "medium",
1360
+ review_status: "ready_for_review",
1361
+ });
1362
+ }
1363
+
1364
+ for (const comparison of asArray(payload.comparisons)) {
1365
+ const sourceId = firstSourceRef(comparison);
1366
+ pushCreatePageOperation(operations, {
1367
+ operation_id: `pb-comparison-${String(operations.length + 1).padStart(3, "0")}`,
1368
+ source_id: sourceId,
1369
+ source_hash: sourceHashFor(sourceById, sourceId),
1370
+ target_page_path: comparison.target_page_path,
1371
+ content: renderComparisonPage(comparison, payload.generated_at),
1372
+ proposed_content_summary: `Create comparison page for ${comparison.topic || comparison.comparison_id}.`,
1373
+ source_refs: sourceRefsFor(comparison),
1374
+ provenance_refs: provenanceRefsFor(comparison),
1375
+ evidence_refs: refsFor(comparison),
1376
+ confidence: comparison.confidence_tier || comparison.confidence || "medium",
1377
+ review_status: "needs_review",
1378
+ });
1379
+ }
1380
+
1381
+ for (const synthesis of asArray(payload.syntheses)) {
1382
+ const sourceId = firstSourceRef(synthesis);
1383
+ pushCreatePageOperation(operations, {
1384
+ operation_id: `pb-synthesis-${String(operations.length + 1).padStart(3, "0")}`,
1385
+ source_id: sourceId,
1386
+ source_hash: sourceHashFor(sourceById, sourceId),
1387
+ target_page_path: synthesis.target_page_path,
1388
+ content: renderSynthesisPage(synthesis, payload.generated_at),
1389
+ proposed_content_summary: `Create synthesis page for ${synthesis.topic || synthesis.synthesis_id}.`,
1390
+ source_refs: sourceRefsFor(synthesis),
1391
+ provenance_refs: provenanceRefsFor(synthesis),
1392
+ evidence_refs: refsFor(synthesis),
1393
+ confidence: synthesis.confidence_tier || synthesis.confidence || "medium",
1394
+ review_status: "needs_review",
1395
+ });
1396
+ }
1397
+
1398
+ const rootPages = [
1399
+ ["index", "wiki/index.md", "Create local wiki index from semantic extraction output."],
1400
+ ["overview", "wiki/overview.md", "Create local wiki overview summarizing extracted local sources."],
1401
+ ["ontology", "wiki/ontology.md", "Create ontology page describing source, tool_entity, concept, claim, relation, comparison, and synthesis records."],
1402
+ ["taxonomy", "wiki/taxonomy.md", "Create taxonomy page for extracted categories and concepts."],
1403
+ ["graph", "wiki/graph.md", "Create semantic graph summary page from extracted relations."],
1404
+ ["log", "wiki/log.md", "Create generation log page for extraction and compile preview evidence."],
1405
+ ];
1406
+ for (const [name, target, summary] of rootPages) {
1407
+ pushCreatePageOperation(operations, {
1408
+ operation_id: `pb-root-${name}`,
1409
+ source_id: rootSourceId,
1410
+ source_hash: rootSourceHash,
1411
+ target_page_path: target,
1412
+ content: renderRootPage(name, payload),
1413
+ proposed_content_summary: summary,
1414
+ evidence_refs: [`record_type:${payload.record_type}`, `generated_at:${payload.generated_at || "(missing)"}`],
1415
+ confidence: "medium",
1416
+ review_status: "needs_review",
1417
+ });
1418
+ }
1419
+
1420
+ const dashboardPages = [
1421
+ ["research-tool-landscape", "wiki/dashboards/research-tool-landscape.md", "Create research/tool landscape dashboard from local semantic extraction output."],
1422
+ ["decision-support", "wiki/dashboards/decision-support.md", "Create decision-support dashboard from local comparison and synthesis evidence."],
1423
+ ["maintenance-quality", "wiki/dashboards/maintenance-quality.md", "Create maintenance/quality dashboard from local extraction findings."],
1424
+ ["source-coverage", "wiki/dashboards/source-coverage.md", "Create source coverage dashboard from local source registry evidence."],
1425
+ ];
1426
+ for (const [name, target, summary] of dashboardPages) {
1427
+ pushCreatePageOperation(operations, {
1428
+ operation_id: `pb-dashboard-${name}`,
1429
+ source_id: rootSourceId,
1430
+ source_hash: rootSourceHash,
1431
+ target_page_path: target,
1432
+ content: renderDashboardPage(name, payload),
1433
+ proposed_content_summary: summary,
1434
+ source_refs: asArray(payload.sources).map((source) => source.source_id).filter(Boolean).slice(0, 50),
1435
+ evidence_refs: [`record_type:${payload.record_type}`, `dashboard:${name}`],
1436
+ confidence: "medium",
1437
+ review_status: "needs_review",
1438
+ });
1439
+ }
1440
+
1441
+ for (const relation of asArray(payload.relations)) {
1442
+ operations.push({
1443
+ operation_id: `pb-relation-${String(operations.length + 1).padStart(3, "0")}`,
1444
+ operation_type: "add_relation",
1445
+ source_id: relation.source_id || rootSourceId,
1446
+ source_hash: rootSourceHash,
1447
+ target_page_path: "wiki/graph.md",
1448
+ content_sections: [
1449
+ {
1450
+ title: `Relation: ${relation.relation_type || relation.relation_id}`,
1451
+ body: [
1452
+ `- source_id: ${relation.source_id || "(missing)"}`,
1453
+ `- target_id: ${relation.target_id || "(missing)"}`,
1454
+ `- relation_type: ${relation.relation_type || "(missing)"}`,
1455
+ `- evidence: ${relation.evidence || "(missing)"}`,
1456
+ `- source_refs: ${sourceRefsFor(relation).join(", ") || "(missing)"}`,
1457
+ `- provenance_refs: ${provenanceRefsFor(relation).join(", ") || "(missing)"}`,
1458
+ ].join("\n"),
1459
+ },
1460
+ ],
1461
+ proposed_content_summary: `Preview semantic relation ${relation.relation_type || relation.relation_id}.`,
1462
+ source_refs: sourceRefsFor(relation),
1463
+ provenance_refs: provenanceRefsFor(relation),
1464
+ evidence_refs: refsFor(relation),
1465
+ confidence: relation.confidence_tier || relation.confidence || "medium",
1466
+ review_status: "ready_for_review",
1467
+ });
1468
+ }
1469
+
1470
+ const qualityFindingCount = asArray(payload.source_quality_findings).length;
1471
+ const unsupportedCount = asArray(payload.unsupported_items).length;
1472
+ if (qualityFindingCount > 0 || unsupportedCount > 0) {
1473
+ pushCreatePageOperation(operations, {
1474
+ operation_id: "pb-maintenance-extraction-quality-review",
1475
+ source_id: rootSourceId,
1476
+ source_hash: rootSourceHash,
1477
+ target_page_path: "wiki/maintenance/extraction-quality-review.md",
1478
+ content: renderMaintenancePage(payload),
1479
+ proposed_content_summary: `Create maintenance review page for ${qualityFindingCount} source-quality finding(s) and ${unsupportedCount} unsupported extraction item(s).`,
1480
+ evidence_refs: [`source_quality_findings:${qualityFindingCount}`, `unsupported_items:${unsupportedCount}`],
1481
+ confidence: unsupportedCount > 0 ? "low" : "medium",
1482
+ review_status: "needs_review",
1483
+ });
1484
+ }
1485
+
1486
+ return operations;
1487
+ }
1488
+
1489
+ function emptySourceQualitySummary() {
1490
+ return {
1491
+ findings_count: 0,
1492
+ unsupported_count: 0,
1493
+ low_confidence_count: 0,
1494
+ warning_count: 0,
1495
+ };
1496
+ }
1497
+
1498
+ function sourceQualitySummaryFromExtraction(payload) {
1499
+ const findings = asArray(payload.source_quality_findings);
1500
+ const unsupported = asArray(payload.unsupported_items);
1501
+ const lowConfidence = findings.filter((finding) => ["low", "unsupported"].includes(String(finding.confidence_tier || finding.confidence || "").toLowerCase())).length;
1502
+ return {
1503
+ findings_count: findings.length,
1504
+ unsupported_count: unsupported.length,
1505
+ low_confidence_count: lowConfidence,
1506
+ warning_count: findings.length + unsupported.length,
1507
+ };
1508
+ }
1509
+
1510
+ function parseJsonPlan(planPath) {
1511
+ let payload;
1512
+ try {
1513
+ payload = JSON.parse(fs.readFileSync(planPath, "utf-8"));
1514
+ } catch (error) {
1515
+ throw new ValidationError(`Invalid JSON compile plan: ${error.message}. No project files were changed.`);
1516
+ }
1517
+ if (payload && payload.record_type === APPLY_PLAN_RECORD_TYPE) {
1518
+ return {
1519
+ operations: asArray(payload.operations),
1520
+ sourceQualitySummary: payload.source_quality_summary || emptySourceQualitySummary(),
1521
+ };
1522
+ }
1523
+ if (payload && payload.record_type === "sdtk_wiki_semantic_extraction") {
1524
+ return {
1525
+ operations: operationsFromSemanticExtraction(payload),
1526
+ sourceQualitySummary: sourceQualitySummaryFromExtraction(payload),
1527
+ };
1528
+ }
1529
+ if (!payload || !Array.isArray(payload.operations)) {
1530
+ throw new ValidationError("Invalid compile plan: JSON plan must contain an operations array. No project files were changed.");
1531
+ }
1532
+ return {
1533
+ operations: payload.operations,
1534
+ sourceQualitySummary: payload.source_quality_summary || emptySourceQualitySummary(),
1535
+ };
1536
+ }
1537
+
1538
+ function parseMarkdownPlan(planPath) {
1539
+ const text = fs.readFileSync(planPath, "utf-8");
1540
+ const lines = text.split(/\r?\n/);
1541
+ const operations = [];
1542
+ let current = null;
1543
+
1544
+ function flush() {
1545
+ if (current && Object.keys(current).length > 1) {
1546
+ operations.push(current);
1547
+ }
1548
+ current = null;
1549
+ }
1550
+
1551
+ for (const line of lines) {
1552
+ const heading = line.match(/^###\s+(.+?)\s*$/);
1553
+ if (heading) {
1554
+ flush();
1555
+ current = { operation_id: heading[1].trim() };
1556
+ continue;
1557
+ }
1558
+ const bullet = line.match(/^\s*-\s*([A-Za-z0-9_]+):\s*(.+?)\s*$/);
1559
+ if (bullet && current) {
1560
+ current[bullet[1]] = bullet[2];
1561
+ }
1562
+ }
1563
+ flush();
1564
+
1565
+ if (operations.length === 0 || operations.every((operation) => !operation.operation_type)) {
1566
+ throw new ValidationError(
1567
+ "Invalid markdown compile plan: expected structured operation blocks with operation_type. No project files were changed."
1568
+ );
1569
+ }
1570
+ return {
1571
+ operations,
1572
+ sourceQualitySummary: emptySourceQualitySummary(),
1573
+ };
1574
+ }
1575
+
1576
+ function parsePlanOperations(planPath) {
1577
+ const ext = path.extname(planPath).toLowerCase();
1578
+ if (ext === ".json") {
1579
+ return parseJsonPlan(planPath);
1580
+ }
1581
+ if (ext === ".md" || ext === ".markdown") {
1582
+ return parseMarkdownPlan(planPath);
1583
+ }
1584
+ throw new ValidationError(`Unsupported plan format: ${ext || "(none)"}. Use .md, .markdown, or .json. No project files were changed.`);
1585
+ }
1586
+
1587
+ function splitRefs(value) {
1588
+ if (Array.isArray(value)) return value.map(String).filter(Boolean);
1589
+ return String(value || "")
1590
+ .split(",")
1591
+ .map((item) => item.trim())
1592
+ .filter(Boolean);
1593
+ }
1594
+
1595
+ function expectedMutationFor(operation) {
1596
+ switch (operation.operation_type) {
1597
+ case "append_section":
1598
+ return `Would append an evidence-backed section to ${operation.target_page_path || operation.target_page_id || "(target missing)"}.`;
1599
+ case "create_page":
1600
+ return `Would create managed page ${operation.target_page_path || operation.target_page_id || "(target missing)"}.`;
1601
+ case "add_relation":
1602
+ return `Would add a related-page relation for ${operation.target_page_path || operation.target_page_id || "(target missing)"}.`;
1603
+ case "add_source_ref":
1604
+ return `Would add source reference metadata for ${operation.target_page_path || operation.target_page_id || "(target missing)"}.`;
1605
+ default:
1606
+ return "No mutation would be allowed; operation type is unsupported in BK-126.";
1607
+ }
1608
+ }
1609
+
1610
+ function renderContentSections(sections) {
1611
+ return asArray(sections)
1612
+ .map((section) => {
1613
+ if (typeof section === "string") return section;
1614
+ const title = section && section.title ? String(section.title) : "Generated Section";
1615
+ const body = section && section.body ? String(section.body) : "";
1616
+ return `## ${title}\n\n${body}`.trimEnd();
1617
+ })
1618
+ .filter(Boolean)
1619
+ .join("\n\n");
1620
+ }
1621
+
1622
+ function fallbackOperationContent(operation) {
1623
+ const title = operation.target_page_id || path.basename(operation.target_page_path || "generated-page.md", path.extname(operation.target_page_path || ""));
1624
+ return [
1625
+ `# ${title}`,
1626
+ "",
1627
+ "## Generated Operation",
1628
+ "",
1629
+ operation.proposed_content_summary || "Generated local wiki content.",
1630
+ "",
1631
+ "## Evidence",
1632
+ "",
1633
+ mdList(operation.evidence_refs),
1634
+ "",
1635
+ ].join("\n");
1636
+ }
1637
+
1638
+ function normalizeTargetPath(rawTargetPath, projectPath) {
1639
+ const canonicalWikiRoot = getCanonicalWikiPath(projectPath);
1640
+ const legacyPersonalBrainRoot = getLegacyPersonalBrainPath(projectPath);
1641
+ const targetValue = String(rawTargetPath || "");
1642
+ const resolvedTarget = targetValue ? path.resolve(projectPath, targetValue) : "";
1643
+ const withinProject = Boolean(resolvedTarget) && isPathInsideOrEqual(resolvedTarget, projectPath);
1644
+ const withinCanonicalWiki = Boolean(resolvedTarget) && isPathInsideOrEqual(resolvedTarget, canonicalWikiRoot);
1645
+ const withinLegacyPersonalBrain = Boolean(resolvedTarget) && isPathInsideOrEqual(resolvedTarget, legacyPersonalBrainRoot);
1646
+ const withinWikiContent = withinCanonicalWiki || withinLegacyPersonalBrain;
1647
+ return {
1648
+ resolvedTarget,
1649
+ targetPathNormalized: withinProject ? toPosix(path.relative(projectPath, resolvedTarget)) : toPosix(resolvedTarget),
1650
+ withinProject,
1651
+ withinCanonicalWiki,
1652
+ withinLegacyPersonalBrain,
1653
+ withinWikiContent,
1654
+ };
1655
+ }
1656
+
1657
+ function normalizeOperation(rawOperation, index, projectPath) {
1658
+ const operationType = String(rawOperation.operation_type || rawOperation.operationType || "").trim();
1659
+ const isSupported = ALLOWED_OPERATION_TYPES.has(operationType);
1660
+ const target = normalizeTargetPath(rawOperation.target_page_path || rawOperation.targetPagePath || "", projectPath);
1661
+ const operation = {
1662
+ operation_id: String(rawOperation.operation_id || rawOperation.operationId || `operation-${String(index + 1).padStart(3, "0")}`),
1663
+ source_id: String(rawOperation.source_id || rawOperation.sourceId || "unknown_source"),
1664
+ source_hash: String(rawOperation.source_hash || rawOperation.sourceHash || ""),
1665
+ revision: String(rawOperation.revision || ""),
1666
+ target_page_id: String(rawOperation.target_page_id || rawOperation.targetPageId || ""),
1667
+ target_page_path: String(rawOperation.target_page_path || rawOperation.targetPagePath || ""),
1668
+ target_path_normalized: target.targetPathNormalized,
1669
+ target_path_within_project: target.withinProject,
1670
+ target_path_within_personal_brain: target.withinWikiContent,
1671
+ target_path_within_canonical_wiki: target.withinCanonicalWiki,
1672
+ target_path_within_legacy_personal_brain: target.withinLegacyPersonalBrain,
1673
+ operation_type: operationType || "unsupported_operation",
1674
+ requested_operation_type: operationType || "(missing)",
1675
+ proposed_content_summary: String(rawOperation.proposed_content_summary || rawOperation.proposedContentSummary || ""),
1676
+ content: String(rawOperation.content || ""),
1677
+ content_sections: asArray(rawOperation.content_sections || rawOperation.contentSections),
1678
+ source_refs: splitRefs(rawOperation.source_refs || rawOperation.sourceRefs || rawOperation.source_id || rawOperation.sourceId),
1679
+ provenance_refs: splitRefs(rawOperation.provenance_refs || rawOperation.provenanceRefs),
1680
+ evidence_refs: splitRefs(rawOperation.evidence_refs || rawOperation.evidenceRefs),
1681
+ confidence: String(rawOperation.confidence || "unknown"),
1682
+ review_status: String(rawOperation.review_status || rawOperation.reviewStatus || "needs_review"),
1683
+ validation_status: isSupported ? "supported" : "unsupported_operation",
1684
+ apply_mode: rawOperation.apply_mode || rawOperation.applyMode || APPLY_MODE,
1685
+ };
1686
+ if (!operation.content && operation.content_sections.length > 0) {
1687
+ operation.content = renderContentSections(operation.content_sections);
1688
+ }
1689
+ if (!operation.content) {
1690
+ operation.content = fallbackOperationContent(operation);
1691
+ }
1692
+ operation.expected_mutation_if_applied = expectedMutationFor(operation);
1693
+ if (!isSupported) {
1694
+ operation.operation_type = "unsupported_operation";
1695
+ operation.review_status = "blocked";
1696
+ }
1697
+ return operation;
1698
+ }
1699
+
1700
+ function summarizeOperations(operations) {
1701
+ const counts = {};
1702
+ for (const operation of operations) {
1703
+ counts[operation.operation_type] = (counts[operation.operation_type] || 0) + 1;
1704
+ }
1705
+ return counts;
1706
+ }
1707
+
1708
+ function renderReport({ projectPath, workspacePath, planPath, applyPlanPath, generatedAt, operations }) {
1709
+ const summary = summarizeOperations(operations);
1710
+ const sortedTypes = Array.from(new Set([...Array.from(ALLOWED_OPERATION_TYPES), "unsupported_operation"]));
1711
+ const unsupported = operations.filter((operation) => operation.validation_status === "unsupported_operation");
1712
+ const lines = [
1713
+ "# SDTK-BRAIN Compile Dry-Run Preview",
1714
+ "",
1715
+ `Generated: ${generatedAt}`,
1716
+ `Project path: ${projectPath}`,
1717
+ `Workspace path: ${workspacePath}`,
1718
+ `Input plan path: ${planPath}`,
1719
+ `Apply JSON sidecar path: ${applyPlanPath}`,
1720
+ `Operations: ${operations.length}`,
1721
+ "",
1722
+ "No wiki pages, raw sources, provenance, or atlas compatibility files were modified.",
1723
+ "Preview-only: apply, page mutation, raw mutation, provenance mutation, discover, prune, query-history, and release work are outside BK-126.",
1724
+ "",
1725
+ "## Operation Counts",
1726
+ "",
1727
+ "| Operation type | Count |",
1728
+ "|---|---:|",
1729
+ ...sortedTypes.map((type) => `| \`${type}\` | ${summary[type] || 0} |`),
1730
+ "",
1731
+ "## Proposed Operations",
1732
+ "",
1733
+ ];
1734
+
1735
+ if (operations.length === 0) {
1736
+ lines.push("No operations were proposed by the input plan.");
1737
+ } else {
1738
+ for (const operation of operations) {
1739
+ lines.push(`### ${operation.operation_id}`);
1740
+ lines.push("");
1741
+ lines.push(`- operation_type: \`${operation.operation_type}\``);
1742
+ if (operation.operation_type === "unsupported_operation") {
1743
+ lines.push(`- requested_operation_type: \`${operation.requested_operation_type}\``);
1744
+ }
1745
+ lines.push(`- Validation status: ${operation.validation_status}`);
1746
+ lines.push(`- source_id: ${operation.source_id}`);
1747
+ lines.push(`- source_hash_or_revision: ${operation.source_hash || operation.revision || "(missing)"}`);
1748
+ lines.push(`- target_page_id: ${operation.target_page_id || "(missing)"}`);
1749
+ lines.push(`- target_page_path: ${operation.target_page_path || "(missing)"}`);
1750
+ lines.push(`- proposed_content_summary: ${operation.proposed_content_summary || "(missing)"}`);
1751
+ lines.push(`- evidence_refs: ${operation.evidence_refs.length > 0 ? operation.evidence_refs.join(", ") : "(missing)"}`);
1752
+ lines.push(`- confidence: ${operation.confidence}`);
1753
+ lines.push(`- review_status: ${operation.review_status}`);
1754
+ lines.push(`- Expected mutation if applied: ${operation.expected_mutation_if_applied}`);
1755
+ lines.push("");
1756
+ }
1757
+ }
1758
+
1759
+ lines.push("## Unsupported / Blocked Operations");
1760
+ lines.push("");
1761
+ if (unsupported.length === 0) {
1762
+ lines.push("- None.");
1763
+ } else {
1764
+ for (const operation of unsupported) {
1765
+ lines.push(`- ${operation.operation_id}: unsupported_operation for requested type \`${operation.requested_operation_type}\`.`);
1766
+ }
1767
+ }
1768
+ lines.push("");
1769
+ return `${lines.join("\n").trimEnd()}\n`;
1770
+ }
1771
+
1772
+ function renderApplyPlan({ projectPath, planPath, reportPath, generatedAt, operations, sourceQualitySummary }) {
1773
+ return {
1774
+ schema_version: APPLY_PLAN_SCHEMA_VERSION,
1775
+ record_type: APPLY_PLAN_RECORD_TYPE,
1776
+ generated_at: generatedAt,
1777
+ project_path: projectPath,
1778
+ source_plan_path: planPath,
1779
+ dry_run_report_path: reportPath,
1780
+ operation_counts: summarizeOperations(operations),
1781
+ source_quality_summary: sourceQualitySummary || emptySourceQualitySummary(),
1782
+ operations: operations.map((operation) => ({
1783
+ operation_id: operation.operation_id,
1784
+ operation_type: operation.operation_type,
1785
+ target_page_path: operation.target_page_path,
1786
+ target_path_normalized: operation.target_path_normalized,
1787
+ target_path_within_project: operation.target_path_within_project,
1788
+ target_path_within_personal_brain: operation.target_path_within_personal_brain,
1789
+ target_path_within_canonical_wiki: operation.target_path_within_canonical_wiki,
1790
+ target_path_within_legacy_personal_brain: operation.target_path_within_legacy_personal_brain,
1791
+ content: operation.content,
1792
+ content_sections: operation.content_sections,
1793
+ source_refs: operation.source_refs,
1794
+ provenance_refs: operation.provenance_refs,
1795
+ evidence_refs: operation.evidence_refs,
1796
+ confidence: operation.confidence,
1797
+ review_status: operation.review_status,
1798
+ validation_status: operation.validation_status,
1799
+ expected_mutation_if_applied: operation.expected_mutation_if_applied,
1800
+ apply_mode: APPLY_MODE,
1801
+ })),
1802
+ };
1803
+ }
1804
+
1805
+ function ensureTrailingNewline(value) {
1806
+ const text = String(value || "").replace(/\r\n/g, "\n").trimEnd();
1807
+ return `${text}\n`;
1808
+ }
1809
+
1810
+ function loadApplyPlan(planPath) {
1811
+ if (path.extname(planPath).toLowerCase() !== ".json") {
1812
+ throw new ValidationError("Apply requires a compile apply JSON sidecar. Markdown plans are rejected for apply. No project files were changed.");
1813
+ }
1814
+ let payload;
1815
+ try {
1816
+ payload = JSON.parse(fs.readFileSync(planPath, "utf-8"));
1817
+ } catch (error) {
1818
+ throw new ValidationError(`Invalid compile apply JSON sidecar: ${error.message}. No project files were changed.`);
1819
+ }
1820
+ if (payload && payload.record_type === "sdtk_wiki_semantic_extraction") {
1821
+ throw new ValidationError("Apply rejects semantic extraction JSON. Run compile --dry-run first and apply the generated compile apply JSON sidecar. No project files were changed.");
1822
+ }
1823
+ if (!payload || payload.record_type !== APPLY_PLAN_RECORD_TYPE) {
1824
+ throw new ValidationError(`Apply requires record_type ${APPLY_PLAN_RECORD_TYPE}. No project files were changed.`);
1825
+ }
1826
+ if (!Array.isArray(payload.operations)) {
1827
+ throw new ValidationError("Compile apply JSON sidecar must contain operations[]. No project files were changed.");
1828
+ }
1829
+ return payload;
1830
+ }
1831
+
1832
+ function validateApplyOperations(operations) {
1833
+ const blocked = operations.filter((operation) => (
1834
+ operation.validation_status !== "supported" ||
1835
+ operation.operation_type === "unsupported_operation" ||
1836
+ !ALLOWED_OPERATION_TYPES.has(operation.operation_type) ||
1837
+ operation.apply_mode !== APPLY_MODE ||
1838
+ !operation.target_path_within_project ||
1839
+ !operation.target_path_within_personal_brain
1840
+ ));
1841
+ if (blocked.length > 0) {
1842
+ throw new ValidationError(
1843
+ `Compile apply plan contains ${blocked.length} blocked or unsafe operation(s). No project files were changed.`
1844
+ );
1845
+ }
1846
+ }
1847
+
1848
+ function aggregateApplyTargets(operations, projectPath) {
1849
+ const targets = new Map();
1850
+ for (const operation of operations) {
1851
+ const target = normalizeTargetPath(operation.target_page_path, projectPath);
1852
+ if (!target.withinProject || !target.withinWikiContent) {
1853
+ throw new ValidationError(`Unsafe wiki target path: ${operation.target_page_path}. No project files were changed.`);
1854
+ }
1855
+ const existing = targets.get(target.resolvedTarget) || {
1856
+ path: target.resolvedTarget,
1857
+ normalized: target.targetPathNormalized,
1858
+ withinCanonicalWiki: target.withinCanonicalWiki,
1859
+ withinLegacyPersonalBrain: target.withinLegacyPersonalBrain,
1860
+ sections: [],
1861
+ operationIds: [],
1862
+ };
1863
+ existing.operationIds.push(operation.operation_id);
1864
+ existing.sections.push(ensureTrailingNewline(operation.content));
1865
+ targets.set(target.resolvedTarget, existing);
1866
+ }
1867
+ for (const target of targets.values()) {
1868
+ target.content = ensureTrailingNewline(target.sections.join("\n"));
1869
+ }
1870
+ return [...targets.values()].sort((a, b) => a.normalized.localeCompare(b.normalized));
1871
+ }
1872
+
1873
+ function scaffoldFileIfMissing(filePath, content, projectPath, rootPath, created, skipped) {
1874
+ if (!isPathInsideOrEqual(filePath, rootPath)) {
1875
+ throw new ValidationError(`Unsafe scaffold target path: ${filePath}. No project files were changed.`);
1876
+ }
1877
+ if (fs.existsSync(filePath)) {
1878
+ skipped.push(toPosix(path.relative(projectPath, filePath)));
1879
+ return;
1880
+ }
1881
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1882
+ fs.writeFileSync(filePath, ensureTrailingNewline(content), "utf-8");
1883
+ created.push(toPosix(path.relative(projectPath, filePath)));
1884
+ }
1885
+
1886
+ function scaffoldWikiPageFrontmatter(fields, body) {
1887
+ const generatedAt = new Date().toISOString();
1888
+ return withPersonalBrainFrontmatter({
1889
+ created_at: generatedAt,
1890
+ updated_at: generatedAt,
1891
+ ...fields,
1892
+ }, body);
1893
+ }
1894
+
1895
+ function scaffoldCanonicalWikiStructure(projectPath) {
1896
+ const canonicalRoot = getCanonicalWikiPath(projectPath);
1897
+ const created = [];
1898
+ const skipped = [];
1899
+ const directories = [
1900
+ "timelines",
1901
+ "queries",
1902
+ "decisions",
1903
+ "meeting-notes",
1904
+ "templates",
1905
+ "maintenance",
1906
+ ];
1907
+ fs.mkdirSync(canonicalRoot, { recursive: true });
1908
+ for (const dir of directories) {
1909
+ const dirPath = path.join(canonicalRoot, dir);
1910
+ if (!isPathInsideOrEqual(dirPath, canonicalRoot)) {
1911
+ throw new ValidationError(`Unsafe wiki directory path: ${dirPath}. No project files were changed.`);
1912
+ }
1913
+ if (!fs.existsSync(dirPath)) {
1914
+ fs.mkdirSync(dirPath, { recursive: true });
1915
+ created.push(toPosix(path.relative(projectPath, dirPath)) + "/");
1916
+ }
1917
+ }
1918
+ scaffoldFileIfMissing(
1919
+ path.join(canonicalRoot, "maintenance.md"),
1920
+ scaffoldWikiPageFrontmatter(
1921
+ {
1922
+ id: "root_maintenance",
1923
+ title: "Maintenance",
1924
+ type: "maintenance",
1925
+ aliases: ["Maintenance"],
1926
+ tags: ["maintenance", "local-wiki"],
1927
+ related_pages: ["index.md", "maintenance/extraction-quality-review.md"],
1928
+ source_refs: ["sdtk_wiki_scaffold"],
1929
+ confidence: "medium",
1930
+ review_status: "needs_review",
1931
+ },
1932
+ "# Maintenance\n\n## Purpose\n\nTrack local wiki maintenance tasks, quality checks, and review follow-ups.\n\n## Source Quality Findings\n\n- Review generated lint, discover, compile, and enrichment reports under `.brain/reports`.\n\n## Unsupported Items\n\n- None recorded in the scaffold.\n\n## Open Items\n\n- Promote maintenance notes into dedicated pages under `wiki/maintenance/` when review produces durable decisions."
1933
+ ),
1934
+ projectPath,
1935
+ canonicalRoot,
1936
+ created,
1937
+ skipped
1938
+ );
1939
+ scaffoldFileIfMissing(
1940
+ path.join(canonicalRoot, "inbox.md"),
1941
+ scaffoldWikiPageFrontmatter(
1942
+ {
1943
+ id: "root_inbox",
1944
+ title: "Inbox",
1945
+ type: "root",
1946
+ aliases: ["Inbox"],
1947
+ tags: ["inbox", "local-wiki"],
1948
+ related_pages: ["index.md"],
1949
+ source_refs: [],
1950
+ confidence: "medium",
1951
+ review_status: "needs_review",
1952
+ },
1953
+ "# Inbox\n\n## Purpose\n\nCapture uncategorized local notes before they are promoted into source, decision, query, timeline, or maintenance pages.\n\n## Inbox\n\n- Add local notes here before structured review."
1954
+ ),
1955
+ projectPath,
1956
+ canonicalRoot,
1957
+ created,
1958
+ skipped
1959
+ );
1960
+
1961
+ const rootDocs = [
1962
+ ["README.md", "# Local Wiki\n\nThis project uses `wiki/` as the human-facing SDTK-BRAIN output. Internal state, reports, provenance, raw registry, graph, and logs stay under `.brain`."],
1963
+ ["AGENTS.md", "# Agent Operating Notes\n\nCodex agents should read `wiki/index.md` first, then use `wiki/dashboards/` for navigation. Do not mutate `.brain` internal state by hand."],
1964
+ ["CLAUDE.md", "# Claude Operating Notes\n\nClaude agents should use `wiki/` as the canonical local wiki and treat `.brain` as SDTK-BRAIN internal state."],
1965
+ ];
1966
+ for (const [name, content] of rootDocs) {
1967
+ const filePath = path.join(projectPath, name);
1968
+ if (fs.existsSync(filePath)) {
1969
+ skipped.push(name);
1970
+ continue;
1971
+ }
1972
+ fs.writeFileSync(filePath, ensureTrailingNewline(content), "utf-8");
1973
+ created.push(name);
1974
+ }
1975
+ return { created, skipped };
1976
+ }
1977
+
1978
+ function runWikiCompileApply({ projectPath, planArg }) {
1979
+ const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
1980
+ if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
1981
+ throw new ValidationError(`--project-path is not a valid directory: ${resolvedProjectPath}`);
1982
+ }
1983
+
1984
+ try {
1985
+ ensureExistingWorkspace(resolvedProjectPath);
1986
+ const planPath = resolvePlanPath(planArg, resolvedProjectPath);
1987
+ const payload = loadApplyPlan(planPath);
1988
+ const operations = payload.operations.map((operation, index) => normalizeOperation(operation, index, resolvedProjectPath));
1989
+ validateApplyOperations(operations);
1990
+ const targets = aggregateApplyTargets(operations, resolvedProjectPath);
1991
+ const conflicts = [];
1992
+ const unchanged = [];
1993
+ const toCreate = [];
1994
+
1995
+ for (const target of targets) {
1996
+ if (target.withinCanonicalWiki) {
1997
+ assertCanonicalWikiWritePath(target.path, resolvedProjectPath);
1998
+ } else if (target.withinLegacyPersonalBrain) {
1999
+ assertWikiWorkspaceWritePath(target.path, resolvedProjectPath);
2000
+ } else {
2001
+ throw new ValidationError(`Refusing to write outside project wiki output: ${target.normalized}. No project files were changed.`);
2002
+ }
2003
+ if (!fs.existsSync(target.path)) {
2004
+ toCreate.push(target);
2005
+ continue;
2006
+ }
2007
+ const current = fs.readFileSync(target.path, "utf-8").replace(/\r\n/g, "\n");
2008
+ if (current === target.content) {
2009
+ unchanged.push(target.normalized);
2010
+ } else {
2011
+ conflicts.push(target.normalized);
2012
+ }
2013
+ }
2014
+
2015
+ if (conflicts.length > 0) {
2016
+ throw new ValidationError(`Compile apply would overwrite ${conflicts.length} existing different file(s): ${conflicts.join(", ")}. No project files were changed.`);
2017
+ }
2018
+
2019
+ const created = [];
2020
+ const scaffold = scaffoldCanonicalWikiStructure(resolvedProjectPath);
2021
+ for (const target of toCreate) {
2022
+ fs.mkdirSync(path.dirname(target.path), { recursive: true });
2023
+ fs.writeFileSync(target.path, target.content, "utf-8");
2024
+ created.push(target.normalized);
2025
+ }
2026
+
2027
+ const sourceQualitySummary = payload.source_quality_summary || emptySourceQualitySummary();
2028
+ return {
2029
+ planPath,
2030
+ created,
2031
+ unchanged,
2032
+ scaffoldCreated: scaffold.created,
2033
+ scaffoldSkipped: scaffold.skipped,
2034
+ sourceQualityWarningCount: Number(sourceQualitySummary.warning_count || sourceQualitySummary.findings_count || 0),
2035
+ };
2036
+ } catch (error) {
2037
+ if (error instanceof CliError) throw error;
2038
+ throw new CliError(`Failed to apply SDTK-BRAIN compile plan: ${error.message}`);
2039
+ }
2040
+ }
2041
+
2042
+ function runWikiCompileDryRun({ projectPath, planArg }) {
2043
+ const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
2044
+ if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
2045
+ throw new ValidationError(`--project-path is not a valid directory: ${resolvedProjectPath}`);
2046
+ }
2047
+
2048
+ try {
2049
+ const workspacePath = ensureExistingWorkspace(resolvedProjectPath);
2050
+ const planPath = resolvePlanPath(planArg, resolvedProjectPath);
2051
+ const parsed = parsePlanOperations(planPath);
2052
+ const rawOperations = parsed.operations;
2053
+ const sourceQualitySummary = parsed.sourceQualitySummary || emptySourceQualitySummary();
2054
+ const operations = rawOperations.map((operation, index) => normalizeOperation(operation, index, resolvedProjectPath));
2055
+ const reportsPath = getWikiReportsPath(resolvedProjectPath);
2056
+ assertWikiWorkspaceWritePath(reportsPath, resolvedProjectPath);
2057
+ const generatedAt = new Date().toISOString();
2058
+ const reportPath = path.join(reportsPath, `${REPORT_PREFIX}-${todayStamp(new Date(generatedAt))}.md`);
2059
+ const applyPlanPath = path.join(reportsPath, `${APPLY_PLAN_PREFIX}-${todayStamp(new Date(generatedAt))}.json`);
2060
+ assertWikiWorkspaceWritePath(reportPath, resolvedProjectPath);
2061
+ assertWikiWorkspaceWritePath(applyPlanPath, resolvedProjectPath);
2062
+ fs.mkdirSync(reportsPath, { recursive: true });
2063
+ fs.writeFileSync(
2064
+ reportPath,
2065
+ renderReport({
2066
+ projectPath: resolvedProjectPath,
2067
+ workspacePath,
2068
+ planPath,
2069
+ applyPlanPath,
2070
+ generatedAt,
2071
+ operations,
2072
+ }),
2073
+ "utf-8"
2074
+ );
2075
+ fs.writeFileSync(
2076
+ applyPlanPath,
2077
+ JSON.stringify(renderApplyPlan({
2078
+ projectPath: resolvedProjectPath,
2079
+ planPath,
2080
+ reportPath,
2081
+ generatedAt,
2082
+ operations,
2083
+ sourceQualitySummary,
2084
+ }), null, 2) + "\n",
2085
+ "utf-8"
2086
+ );
2087
+
2088
+ return {
2089
+ reportPath,
2090
+ applyPlanPath,
2091
+ operations,
2092
+ summary: summarizeOperations(operations),
2093
+ unsupportedCount: operations.filter((operation) => operation.validation_status === "unsupported_operation").length,
2094
+ };
2095
+ } catch (error) {
2096
+ if (error instanceof CliError) throw error;
2097
+ throw new CliError(`Failed to write SDTK-BRAIN compile dry-run preview: ${error.message}`);
2098
+ }
2099
+ }
2100
+
2101
+ module.exports = {
2102
+ APPLY_PLAN_RECORD_TYPE,
2103
+ ALLOWED_OPERATION_TYPES,
2104
+ REPORT_PREFIX,
2105
+ renderReport,
2106
+ runWikiCompileApply,
2107
+ runWikiCompileDryRun,
2108
+ };