sdtk-wiki-kit 0.1.3 → 0.2.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.
@@ -4,7 +4,10 @@ const fs = require("fs");
4
4
  const path = require("path");
5
5
  const { CliError, ValidationError } = require("./errors");
6
6
  const {
7
+ assertCanonicalWikiWritePath,
7
8
  assertWikiWorkspaceWritePath,
9
+ getCanonicalWikiPath,
10
+ getLegacyPersonalBrainPath,
8
11
  getWikiReportsPath,
9
12
  getWikiWorkspacePath,
10
13
  isPathInsideOrEqual,
@@ -15,7 +18,7 @@ const REPORT_PREFIX = "compile-dry-run-preview";
15
18
  const APPLY_PLAN_PREFIX = "compile-apply-plan";
16
19
  const APPLY_PLAN_RECORD_TYPE = "sdtk_wiki_compile_apply_plan";
17
20
  const APPLY_PLAN_SCHEMA_VERSION = 1;
18
- const PERSONAL_BRAIN_RELATIVE = path.join(".sdtk", "wiki", "personal-brain");
21
+ const CANONICAL_WIKI_RELATIVE = "wiki";
19
22
  const APPLY_MODE = "create_only_or_same_content_noop";
20
23
  const ALLOWED_OPERATION_TYPES = new Set([
21
24
  "append_section",
@@ -138,6 +141,7 @@ function relatedRepoList(entity) {
138
141
 
139
142
  function relativePersonalBrainLink(targetPagePath) {
140
143
  const text = toPosix(targetPagePath || "");
144
+ if (text.startsWith("wiki/")) return text.slice("wiki/".length);
141
145
  const marker = ".sdtk/wiki/personal-brain/";
142
146
  const idx = text.indexOf(marker);
143
147
  return idx >= 0 ? text.slice(idx + marker.length) : text;
@@ -204,6 +208,464 @@ function candidateToolList(records) {
204
208
  }).join("\n");
205
209
  }
206
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
+
207
669
  function yamlScalar(value) {
208
670
  return `"${String(value ?? "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r?\n/g, " ").trim()}"`;
209
671
  }
@@ -268,6 +730,10 @@ function renderSourcePage(source, generatedAt) {
268
730
  "",
269
731
  mdList(source.source_quality && source.source_quality.quality_flags),
270
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
+ "",
271
737
  "## Provenance",
272
738
  "",
273
739
  mdList(provenanceRefsFor(source)),
@@ -281,6 +747,7 @@ function renderSourcePage(source, generatedAt) {
281
747
  updated_at: generatedAt,
282
748
  aliases: source.source_logical_path ? [source.source_logical_path] : [],
283
749
  tags: ["source", source.source_type || "local"],
750
+ thinness_policy: "accepted_source_provenance_anchor",
284
751
  source_refs: sourceRefsFor(source),
285
752
  confidence: source.source_quality && source.source_quality.low_confidence_extraction ? "low" : "medium",
286
753
  review_status: source.source_quality && source.source_quality.low_confidence_extraction ? "needs_review" : "ready_for_review",
@@ -399,6 +866,10 @@ function renderConceptPage(concept, generatedAt) {
399
866
  "",
400
867
  summary,
401
868
  "",
869
+ "## Decision Brief",
870
+ "",
871
+ conceptDecisionBrief(concept),
872
+ "",
402
873
  "## Key Axes",
403
874
  "",
404
875
  conceptAxisList(concept),
@@ -464,6 +935,10 @@ function renderComparisonPage(comparison, generatedAt) {
464
935
  "",
465
936
  comparison.summary || `Local comparison for ${comparison.topic || comparison.comparison_id}.`,
466
937
  "",
938
+ "## Decision Guidance",
939
+ "",
940
+ comparisonDecisionGuidance(comparison),
941
+ "",
467
942
  "## Decision Axes",
468
943
  "",
469
944
  conceptAxisList({ key_axes: comparison.decision_axes }),
@@ -527,6 +1002,10 @@ function renderSynthesisPage(synthesis, generatedAt) {
527
1002
  "",
528
1003
  synthesis.summary || `Generated synthesis page for ${synthesis.topic || synthesis.synthesis_id}.`,
529
1004
  "",
1005
+ "## Decision Narrative",
1006
+ "",
1007
+ synthesisDecisionNarrative(synthesis),
1008
+ "",
530
1009
  "## Landscape Snapshot",
531
1010
  "",
532
1011
  candidateToolList(synthesis.candidate_tools),
@@ -587,15 +1066,133 @@ function renderSynthesisPage(synthesis, generatedAt) {
587
1066
 
588
1067
  function renderRootPage(name, payload) {
589
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
+ ];
590
1080
  const titles = {
591
- index: "Personal Brain Index",
592
- overview: "Personal Brain Overview",
593
- ontology: "Personal Brain Ontology",
594
- taxonomy: "Personal Brain Taxonomy",
595
- graph: "Personal Brain Graph",
596
- log: "Personal Brain Generation Log",
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
+ ],
597
1195
  };
598
- const title = titles[name] || "Personal Brain";
599
1196
  const body = [
600
1197
  `# ${title}`,
601
1198
  "",
@@ -615,8 +1212,15 @@ function renderRootPage(name, payload) {
615
1212
  "- concepts/",
616
1213
  "- comparisons/",
617
1214
  "- syntheses/",
1215
+ "- dashboards/",
618
1216
  "- maintenance/",
619
1217
  "",
1218
+ ...(pageSpecificSections[name] || []),
1219
+ "",
1220
+ "## Source Coverage",
1221
+ "",
1222
+ sourceLinks,
1223
+ "",
620
1224
  ].join("\n");
621
1225
  return withPersonalBrainFrontmatter({
622
1226
  id: `personal_brain_${name}`,
@@ -625,7 +1229,8 @@ function renderRootPage(name, payload) {
625
1229
  created_at: payload.generated_at || "",
626
1230
  updated_at: payload.generated_at || "",
627
1231
  aliases: [name],
628
- tags: ["personal-brain", name],
1232
+ tags: ["local-wiki", name],
1233
+ related_pages: dashboards.map(([, target]) => target),
629
1234
  source_refs: asArray(payload.sources).map((source) => source.source_id).filter(Boolean).slice(0, 50),
630
1235
  confidence: "medium",
631
1236
  review_status: "needs_review",
@@ -694,7 +1299,7 @@ function operationsFromSemanticExtraction(payload) {
694
1299
  source_hash: source.source_hash,
695
1300
  target_page_path: source.target_page_path,
696
1301
  content: renderSourcePage(source, payload.generated_at),
697
- proposed_content_summary: `Create personal-brain source page for ${source.source_logical_path || source.source_relative_path || source.source_id}.`,
1302
+ proposed_content_summary: `Create local wiki source page for ${source.source_logical_path || source.source_relative_path || source.source_id}.`,
698
1303
  source_refs: sourceRefsFor(source),
699
1304
  provenance_refs: provenanceRefsFor(source),
700
1305
  evidence_refs: refsFor(source),
@@ -791,12 +1396,12 @@ function operationsFromSemanticExtraction(payload) {
791
1396
  }
792
1397
 
793
1398
  const rootPages = [
794
- ["index", ".sdtk/wiki/personal-brain/index.md", "Create personal-brain index from semantic extraction output."],
795
- ["overview", ".sdtk/wiki/personal-brain/overview.md", "Create personal-brain overview summarizing extracted local sources."],
796
- ["ontology", ".sdtk/wiki/personal-brain/ontology.md", "Create ontology page describing source, tool_entity, concept, claim, relation, comparison, and synthesis records."],
797
- ["taxonomy", ".sdtk/wiki/personal-brain/taxonomy.md", "Create taxonomy page for extracted categories and concepts."],
798
- ["graph", ".sdtk/wiki/personal-brain/graph.md", "Create semantic graph summary page from extracted relations."],
799
- ["log", ".sdtk/wiki/personal-brain/log.md", "Create generation log page for extraction and compile preview evidence."],
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."],
800
1405
  ];
801
1406
  for (const [name, target, summary] of rootPages) {
802
1407
  pushCreatePageOperation(operations, {
@@ -812,13 +1417,34 @@ function operationsFromSemanticExtraction(payload) {
812
1417
  });
813
1418
  }
814
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
+
815
1441
  for (const relation of asArray(payload.relations)) {
816
1442
  operations.push({
817
1443
  operation_id: `pb-relation-${String(operations.length + 1).padStart(3, "0")}`,
818
1444
  operation_type: "add_relation",
819
1445
  source_id: relation.source_id || rootSourceId,
820
1446
  source_hash: rootSourceHash,
821
- target_page_path: ".sdtk/wiki/personal-brain/graph.md",
1447
+ target_page_path: "wiki/graph.md",
822
1448
  content_sections: [
823
1449
  {
824
1450
  title: `Relation: ${relation.relation_type || relation.relation_id}`,
@@ -848,7 +1474,7 @@ function operationsFromSemanticExtraction(payload) {
848
1474
  operation_id: "pb-maintenance-extraction-quality-review",
849
1475
  source_id: rootSourceId,
850
1476
  source_hash: rootSourceHash,
851
- target_page_path: ".sdtk/wiki/personal-brain/maintenance/extraction-quality-review.md",
1477
+ target_page_path: "wiki/maintenance/extraction-quality-review.md",
852
1478
  content: renderMaintenancePage(payload),
853
1479
  proposed_content_summary: `Create maintenance review page for ${qualityFindingCount} source-quality finding(s) and ${unsupportedCount} unsupported extraction item(s).`,
854
1480
  evidence_refs: [`source_quality_findings:${qualityFindingCount}`, `unsupported_items:${unsupportedCount}`],
@@ -1000,7 +1626,7 @@ function fallbackOperationContent(operation) {
1000
1626
  "",
1001
1627
  "## Generated Operation",
1002
1628
  "",
1003
- operation.proposed_content_summary || "Generated personal-brain content.",
1629
+ operation.proposed_content_summary || "Generated local wiki content.",
1004
1630
  "",
1005
1631
  "## Evidence",
1006
1632
  "",
@@ -1010,16 +1636,21 @@ function fallbackOperationContent(operation) {
1010
1636
  }
1011
1637
 
1012
1638
  function normalizeTargetPath(rawTargetPath, projectPath) {
1013
- const personalBrainRoot = path.join(getWikiWorkspacePath(projectPath), "personal-brain");
1639
+ const canonicalWikiRoot = getCanonicalWikiPath(projectPath);
1640
+ const legacyPersonalBrainRoot = getLegacyPersonalBrainPath(projectPath);
1014
1641
  const targetValue = String(rawTargetPath || "");
1015
1642
  const resolvedTarget = targetValue ? path.resolve(projectPath, targetValue) : "";
1016
1643
  const withinProject = Boolean(resolvedTarget) && isPathInsideOrEqual(resolvedTarget, projectPath);
1017
- const withinPersonalBrain = Boolean(resolvedTarget) && isPathInsideOrEqual(resolvedTarget, personalBrainRoot);
1644
+ const withinCanonicalWiki = Boolean(resolvedTarget) && isPathInsideOrEqual(resolvedTarget, canonicalWikiRoot);
1645
+ const withinLegacyPersonalBrain = Boolean(resolvedTarget) && isPathInsideOrEqual(resolvedTarget, legacyPersonalBrainRoot);
1646
+ const withinWikiContent = withinCanonicalWiki || withinLegacyPersonalBrain;
1018
1647
  return {
1019
1648
  resolvedTarget,
1020
1649
  targetPathNormalized: withinProject ? toPosix(path.relative(projectPath, resolvedTarget)) : toPosix(resolvedTarget),
1021
1650
  withinProject,
1022
- withinPersonalBrain,
1651
+ withinCanonicalWiki,
1652
+ withinLegacyPersonalBrain,
1653
+ withinWikiContent,
1023
1654
  };
1024
1655
  }
1025
1656
 
@@ -1036,7 +1667,9 @@ function normalizeOperation(rawOperation, index, projectPath) {
1036
1667
  target_page_path: String(rawOperation.target_page_path || rawOperation.targetPagePath || ""),
1037
1668
  target_path_normalized: target.targetPathNormalized,
1038
1669
  target_path_within_project: target.withinProject,
1039
- target_path_within_personal_brain: target.withinPersonalBrain,
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,
1040
1673
  operation_type: operationType || "unsupported_operation",
1041
1674
  requested_operation_type: operationType || "(missing)",
1042
1675
  proposed_content_summary: String(rawOperation.proposed_content_summary || rawOperation.proposedContentSummary || ""),
@@ -1153,6 +1786,8 @@ function renderApplyPlan({ projectPath, planPath, reportPath, generatedAt, opera
1153
1786
  target_path_normalized: operation.target_path_normalized,
1154
1787
  target_path_within_project: operation.target_path_within_project,
1155
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,
1156
1791
  content: operation.content,
1157
1792
  content_sections: operation.content_sections,
1158
1793
  source_refs: operation.source_refs,
@@ -1214,12 +1849,14 @@ function aggregateApplyTargets(operations, projectPath) {
1214
1849
  const targets = new Map();
1215
1850
  for (const operation of operations) {
1216
1851
  const target = normalizeTargetPath(operation.target_page_path, projectPath);
1217
- if (!target.withinProject || !target.withinPersonalBrain) {
1218
- throw new ValidationError(`Unsafe personal-brain target path: ${operation.target_page_path}. No project files were changed.`);
1852
+ if (!target.withinProject || !target.withinWikiContent) {
1853
+ throw new ValidationError(`Unsafe wiki target path: ${operation.target_page_path}. No project files were changed.`);
1219
1854
  }
1220
1855
  const existing = targets.get(target.resolvedTarget) || {
1221
1856
  path: target.resolvedTarget,
1222
1857
  normalized: target.targetPathNormalized,
1858
+ withinCanonicalWiki: target.withinCanonicalWiki,
1859
+ withinLegacyPersonalBrain: target.withinLegacyPersonalBrain,
1223
1860
  sections: [],
1224
1861
  operationIds: [],
1225
1862
  };
@@ -1233,6 +1870,111 @@ function aggregateApplyTargets(operations, projectPath) {
1233
1870
  return [...targets.values()].sort((a, b) => a.normalized.localeCompare(b.normalized));
1234
1871
  }
1235
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 `.sdtk/wiki/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-WIKI output. Internal state, reports, provenance, raw registry, graph, and logs stay under `.sdtk/wiki`."],
1963
+ ["AGENTS.md", "# Agent Operating Notes\n\nCodex agents should read `wiki/index.md` first, then use `wiki/dashboards/` for navigation. Do not mutate `.sdtk/wiki` internal state by hand."],
1964
+ ["CLAUDE.md", "# Claude Operating Notes\n\nClaude agents should use `wiki/` as the canonical local wiki and treat `.sdtk/wiki` as SDTK-WIKI 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
+
1236
1978
  function runWikiCompileApply({ projectPath, planArg }) {
1237
1979
  const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
1238
1980
  if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
@@ -1251,9 +1993,12 @@ function runWikiCompileApply({ projectPath, planArg }) {
1251
1993
  const toCreate = [];
1252
1994
 
1253
1995
  for (const target of targets) {
1254
- assertWikiWorkspaceWritePath(target.path, resolvedProjectPath);
1255
- if (!isPathInsideOrEqual(target.path, path.join(getWikiWorkspacePath(resolvedProjectPath), "personal-brain"))) {
1256
- throw new ValidationError(`Refusing to write outside .sdtk/wiki/personal-brain: ${target.normalized}. No project files were changed.`);
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.`);
1257
2002
  }
1258
2003
  if (!fs.existsSync(target.path)) {
1259
2004
  toCreate.push(target);
@@ -1272,6 +2017,7 @@ function runWikiCompileApply({ projectPath, planArg }) {
1272
2017
  }
1273
2018
 
1274
2019
  const created = [];
2020
+ const scaffold = scaffoldCanonicalWikiStructure(resolvedProjectPath);
1275
2021
  for (const target of toCreate) {
1276
2022
  fs.mkdirSync(path.dirname(target.path), { recursive: true });
1277
2023
  fs.writeFileSync(target.path, target.content, "utf-8");
@@ -1283,6 +2029,8 @@ function runWikiCompileApply({ projectPath, planArg }) {
1283
2029
  planPath,
1284
2030
  created,
1285
2031
  unchanged,
2032
+ scaffoldCreated: scaffold.created,
2033
+ scaffoldSkipped: scaffold.skipped,
1286
2034
  sourceQualityWarningCount: Number(sourceQualitySummary.warning_count || sourceQualitySummary.findings_count || 0),
1287
2035
  };
1288
2036
  } catch (error) {