sdtk-wiki-kit 0.1.1 → 0.1.3

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.
@@ -27,6 +27,21 @@ const REQUIRED_PAGE_FIELDS = [
27
27
  "role",
28
28
  ];
29
29
 
30
+ const REQUIRED_PERSONAL_BRAIN_FIELDS = [
31
+ "id",
32
+ "title",
33
+ "type",
34
+ "status",
35
+ "created_at",
36
+ "updated_at",
37
+ "aliases",
38
+ "tags",
39
+ "related_pages",
40
+ "source_refs",
41
+ "confidence",
42
+ "review_status",
43
+ ];
44
+
30
45
  const CATEGORY_DEFS = [
31
46
  ["schema", "Frontmatter and schema"],
32
47
  ["duplicates", "Duplicate page IDs"],
@@ -40,8 +55,21 @@ const CATEGORY_DEFS = [
40
55
  ["markers", "TODO/Open Questions/Gaps"],
41
56
  ["contradictions", "Candidate contradictions"],
42
57
  ["sourceQuality", "Source quality"],
58
+ ["personalBrainQuality", "Personal-brain quality gate"],
43
59
  ];
44
60
 
61
+ const REQUIRED_PERSONAL_BRAIN_SECTIONS = {
62
+ source: ["Summary", "Source Metadata", "Source Quality", "Provenance"],
63
+ tool_entity: ["Summary", "Key Facts", "Discovery Source", "Extracted Snippet", "Topic Labels", "Why It Matters", "When To Use", "Related Repos", "Overlaps / Differences", "Open Questions", "Provenance"],
64
+ concept: ["Summary", "Key Axes", "Implementations / Examples", "Patterns", "Recommendations / Caveats", "Open Questions", "Related Pages", "Source References"],
65
+ comparison: ["Summary", "Decision Axes", "Comparison Matrix", "Candidate Tools / Repos", "Recommendations", "Caveats", "Source Confidence", "Open Questions", "Source References"],
66
+ synthesis: ["Summary", "Landscape Snapshot", "Key Patterns", "Decision Axes", "Recommended Review Path", "Caveats", "Source Confidence", "Related Comparisons", "Open Questions", "Source References"],
67
+ maintenance: ["Source Quality Findings", "Unsupported Items"],
68
+ };
69
+
70
+ const PERSONAL_BRAIN_STUB_CHAR_LIMIT = 600;
71
+ const PERSONAL_BRAIN_GIANT_PAGE_BYTES = 50000;
72
+
45
73
  function toPosix(value) {
46
74
  return String(value || "").replace(/\\/g, "/");
47
75
  }
@@ -148,6 +176,20 @@ function extractMarkdownLinks(body) {
148
176
  return links;
149
177
  }
150
178
 
179
+ function hasHeading(body, heading) {
180
+ const escaped = String(heading).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
181
+ return new RegExp(`^##\\s+${escaped}\\s*$`, "im").test(String(body || ""));
182
+ }
183
+
184
+ function hasSourceRefs(fields) {
185
+ const raw = String(fields.source_refs || "").trim();
186
+ return Boolean(raw && raw !== "[]" && raw !== "[\"\"]");
187
+ }
188
+
189
+ function formatPercent(value) {
190
+ return `${Math.round(value * 100)}%`;
191
+ }
192
+
151
193
  function createFindings() {
152
194
  return Object.fromEntries(CATEGORY_DEFS.map(([key]) => [key, []]));
153
195
  }
@@ -550,7 +592,154 @@ function analyzePages(projectPath) {
550
592
 
551
593
  analyzeSourceQuality(projectPath, inputs, findings);
552
594
 
553
- return { findings, pageCount: pages.length };
595
+ const personalBrainAnalysis = analyzePersonalBrainPages(projectPath, findings);
596
+
597
+ return {
598
+ findings,
599
+ pageCount: pages.length + personalBrainAnalysis.count,
600
+ personalBrainMetrics: personalBrainAnalysis.metrics,
601
+ };
602
+ }
603
+
604
+ function analyzePersonalBrainPages(projectPath, findings) {
605
+ const personalBrainRoot = path.join(getWikiWorkspacePath(projectPath), "personal-brain");
606
+ const pageFiles = listMarkdownPages(personalBrainRoot);
607
+ const pages = [];
608
+ const metrics = {
609
+ pageCount: pageFiles.length,
610
+ byType: {},
611
+ frontmatterCoverage: 0,
612
+ requiredSectionCoverage: 0,
613
+ sourceRefsCoverage: 0,
614
+ stubRatio: 0,
615
+ giantPageCount: 0,
616
+ brokenInternalLinks: 0,
617
+ conceptCount: 0,
618
+ entityCount: 0,
619
+ comparisonCount: 0,
620
+ synthesisCount: 0,
621
+ sourceEvidenceCoverage: 0,
622
+ };
623
+ let frontmatterCount = 0;
624
+ let sectionRequiredTotal = 0;
625
+ let sectionPresentTotal = 0;
626
+ let sourceRefsEligible = 0;
627
+ let sourceRefsPresent = 0;
628
+ let sourceEvidenceEligible = 0;
629
+ let sourceEvidencePresent = 0;
630
+ let stubCount = 0;
631
+
632
+ for (const filePath of pageFiles) {
633
+ const relPath = toPosix(path.relative(personalBrainRoot, filePath));
634
+ const parsed = parseFrontmatter(fs.readFileSync(filePath, "utf-8"));
635
+ const fields = parsed.fields;
636
+ const type = String(fields.type || "unknown");
637
+ pages.push({ filePath, relPath, fields, body: parsed.body, hasFrontmatter: parsed.hasFrontmatter, type });
638
+ metrics.byType[type] = (metrics.byType[type] || 0) + 1;
639
+ if (type === "concept") metrics.conceptCount += 1;
640
+ if (type === "tool_entity") metrics.entityCount += 1;
641
+ if (type === "comparison") metrics.comparisonCount += 1;
642
+ if (type === "synthesis") metrics.synthesisCount += 1;
643
+
644
+ if (!parsed.hasFrontmatter) {
645
+ appendFinding(findings, "schema", `personal-brain/${relPath} is missing parseable frontmatter.`);
646
+ continue;
647
+ }
648
+ frontmatterCount += 1;
649
+
650
+ const missingFields = REQUIRED_PERSONAL_BRAIN_FIELDS.filter((field) => !fields[field]);
651
+ if (missingFields.length > 0) {
652
+ appendFinding(
653
+ findings,
654
+ "schema",
655
+ `personal-brain/${relPath} is missing required personal-brain fields: ${missingFields.join(", ")}.`
656
+ );
657
+ }
658
+
659
+ const requiredSections = REQUIRED_PERSONAL_BRAIN_SECTIONS[type] || [];
660
+ sectionRequiredTotal += requiredSections.length;
661
+ const missingSections = requiredSections.filter((section) => !hasHeading(parsed.body, section));
662
+ sectionPresentTotal += requiredSections.length - missingSections.length;
663
+ if (missingSections.length > 0) {
664
+ appendFinding(
665
+ findings,
666
+ "personalBrainQuality",
667
+ `personal-brain/${relPath} is missing required sections for ${type}: ${missingSections.join(", ")}.`
668
+ );
669
+ }
670
+
671
+ if (!["root"].includes(type)) {
672
+ sourceRefsEligible += 1;
673
+ if (hasSourceRefs(fields)) sourceRefsPresent += 1;
674
+ else appendFinding(findings, "personalBrainQuality", `personal-brain/${relPath} has no source_refs coverage.`);
675
+ }
676
+
677
+ if (["tool_entity", "concept", "comparison", "synthesis"].includes(type)) {
678
+ sourceEvidenceEligible += 1;
679
+ if (hasSourceRefs(fields) && /(?:source|provenance|evidence)/i.test(parsed.body)) {
680
+ sourceEvidencePresent += 1;
681
+ } else {
682
+ appendFinding(findings, "personalBrainQuality", `personal-brain/${relPath} lacks source evidence coverage in body/frontmatter.`);
683
+ }
684
+ }
685
+
686
+ const compactBody = parsed.body.replace(/\s+/g, " ").trim();
687
+ if (compactBody.length < PERSONAL_BRAIN_STUB_CHAR_LIMIT && !["root"].includes(type)) {
688
+ stubCount += 1;
689
+ appendFinding(findings, "personalBrainQuality", `personal-brain/${relPath} appears stub-like (${compactBody.length} normalized characters).`);
690
+ }
691
+
692
+ const byteSize = fs.statSync(filePath).size;
693
+ if (byteSize > PERSONAL_BRAIN_GIANT_PAGE_BYTES) {
694
+ metrics.giantPageCount += 1;
695
+ appendFinding(findings, "personalBrainQuality", `personal-brain/${relPath} is very large (${byteSize} bytes) and may need splitting.`);
696
+ }
697
+ }
698
+
699
+ for (const page of pages) {
700
+ for (const link of extractMarkdownLinks(page.body)) {
701
+ const rawPath = link.split("#")[0].trim();
702
+ if (!rawPath) continue;
703
+ const resolved = path.resolve(path.dirname(page.filePath), rawPath);
704
+ if (!isPathInsideOrEqual(resolved, personalBrainRoot) || !fs.existsSync(resolved)) {
705
+ appendFinding(findings, "brokenLinks", `personal-brain/${page.relPath} links to missing ${link}.`);
706
+ metrics.brokenInternalLinks += 1;
707
+ }
708
+ }
709
+ }
710
+
711
+ metrics.frontmatterCoverage = pageFiles.length > 0 ? frontmatterCount / pageFiles.length : 1;
712
+ metrics.requiredSectionCoverage = sectionRequiredTotal > 0 ? sectionPresentTotal / sectionRequiredTotal : 1;
713
+ metrics.sourceRefsCoverage = sourceRefsEligible > 0 ? sourceRefsPresent / sourceRefsEligible : 1;
714
+ metrics.stubRatio = pageFiles.length > 0 ? stubCount / pageFiles.length : 0;
715
+ metrics.sourceEvidenceCoverage = sourceEvidenceEligible > 0 ? sourceEvidencePresent / sourceEvidenceEligible : 1;
716
+
717
+ if (pageFiles.length === 0) {
718
+ appendFinding(findings, "personalBrainQuality", "No personal-brain pages were found under .sdtk/wiki/personal-brain.");
719
+ }
720
+ if (metrics.frontmatterCoverage < 1) {
721
+ appendFinding(findings, "personalBrainQuality", `Frontmatter coverage is ${formatPercent(metrics.frontmatterCoverage)}; expected 100%.`);
722
+ }
723
+ if (metrics.requiredSectionCoverage < 0.95) {
724
+ appendFinding(findings, "personalBrainQuality", `Required section coverage is ${formatPercent(metrics.requiredSectionCoverage)}; expected at least 95%.`);
725
+ }
726
+ if (metrics.sourceRefsCoverage < 0.9) {
727
+ appendFinding(findings, "personalBrainQuality", `Source refs coverage is ${formatPercent(metrics.sourceRefsCoverage)}; expected at least 90%.`);
728
+ }
729
+ if (metrics.stubRatio > 0.1) {
730
+ appendFinding(findings, "personalBrainQuality", `Stub ratio is ${formatPercent(metrics.stubRatio)}; expected at most 10%.`);
731
+ }
732
+ if (metrics.entityCount > 0 && metrics.conceptCount === 0) {
733
+ appendFinding(findings, "personalBrainQuality", "Tool/entity pages exist but no concept pages were generated.");
734
+ }
735
+ if (metrics.conceptCount > 0 && metrics.comparisonCount === 0) {
736
+ appendFinding(findings, "personalBrainQuality", "Concept pages exist but no comparison pages were generated.");
737
+ }
738
+ if (metrics.comparisonCount > 0 && metrics.synthesisCount === 0) {
739
+ appendFinding(findings, "personalBrainQuality", "Comparison pages exist but no synthesis pages were generated.");
740
+ }
741
+
742
+ return { count: pageFiles.length, metrics };
554
743
  }
555
744
 
556
745
  function todayStamp() {
@@ -561,7 +750,35 @@ function totalFindings(findings) {
561
750
  return CATEGORY_DEFS.reduce((sum, [key]) => sum + findings[key].length, 0);
562
751
  }
563
752
 
564
- function renderReport({ projectPath, workspaceRoot, findings, pageCount }) {
753
+ function renderPersonalBrainMetrics(metrics) {
754
+ if (!metrics) return ["## Personal-Brain Quality Metrics", "", "- No personal-brain metrics available.", ""];
755
+ const typeRows = Object.entries(metrics.byType || {})
756
+ .sort(([a], [b]) => a.localeCompare(b))
757
+ .map(([type, count]) => `| ${type} | ${count} |`);
758
+ return [
759
+ "## Personal-Brain Quality Metrics",
760
+ "",
761
+ `- personal-brain pages: ${metrics.pageCount}`,
762
+ `- frontmatter coverage: ${formatPercent(metrics.frontmatterCoverage)}`,
763
+ `- required section coverage: ${formatPercent(metrics.requiredSectionCoverage)}`,
764
+ `- source refs coverage: ${formatPercent(metrics.sourceRefsCoverage)}`,
765
+ `- source evidence coverage: ${formatPercent(metrics.sourceEvidenceCoverage)}`,
766
+ `- stub ratio: ${formatPercent(metrics.stubRatio)}`,
767
+ `- giant page warnings: ${metrics.giantPageCount}`,
768
+ `- broken personal-brain internal links: ${metrics.brokenInternalLinks}`,
769
+ `- entity pages: ${metrics.entityCount}`,
770
+ `- concept pages: ${metrics.conceptCount}`,
771
+ `- comparison pages: ${metrics.comparisonCount}`,
772
+ `- synthesis pages: ${metrics.synthesisCount}`,
773
+ "",
774
+ "| Page type | Count |",
775
+ "|---|---:|",
776
+ ...(typeRows.length > 0 ? typeRows : ["| none | 0 |"]),
777
+ "",
778
+ ];
779
+ }
780
+
781
+ function renderReport({ projectPath, workspaceRoot, findings, pageCount, personalBrainMetrics }) {
565
782
  const summaryRows = CATEGORY_DEFS.map(
566
783
  ([key, label]) => `| ${label} | ${findings[key].length} |`
567
784
  );
@@ -593,6 +810,7 @@ function renderReport({ projectPath, workspaceRoot, findings, pageCount }) {
593
810
  "|---|---:|",
594
811
  ...summaryRows,
595
812
  "",
813
+ ...renderPersonalBrainMetrics(personalBrainMetrics),
596
814
  ...detailSections,
597
815
  ].join("\n");
598
816
  }
@@ -622,12 +840,14 @@ function runWikiLint(options = {}) {
622
840
  workspaceRoot,
623
841
  findings: analysis.findings,
624
842
  pageCount: analysis.pageCount,
843
+ personalBrainMetrics: analysis.personalBrainMetrics,
625
844
  });
626
845
  fs.writeFileSync(reportPath, report + "\n", "utf-8");
627
846
  return {
628
847
  reportPath,
629
848
  totalFindings: totalFindings(analysis.findings),
630
849
  findings: analysis.findings,
850
+ personalBrainMetrics: analysis.personalBrainMetrics,
631
851
  };
632
852
  } catch (error) {
633
853
  if (error instanceof CliError) throw error;
@@ -637,6 +857,7 @@ function runWikiLint(options = {}) {
637
857
 
638
858
  module.exports = {
639
859
  CATEGORY_DEFS,
860
+ REQUIRED_PERSONAL_BRAIN_FIELDS,
640
861
  REQUIRED_PAGE_FIELDS,
641
862
  analyzePages,
642
863
  parseFrontmatter,
@@ -126,7 +126,7 @@ function runWikiSearch({ projectPath, query, limit = DEFAULT_LIMIT }) {
126
126
  }
127
127
  if (!fs.existsSync(personalBrainPath) || !fs.statSync(personalBrainPath).isDirectory()) {
128
128
  throw new ValidationError(
129
- `No SDTK-WIKI personal brain found at ${personalBrainPath}. Run extract, compile dry-run, and compile --apply --yes from the generated JSON sidecar first.`
129
+ `No SDTK-WIKI personal brain found at ${personalBrainPath}. Run "sdtk-wiki ingest <source-root>" and "sdtk-wiki compile --mode safe --apply" first. Advanced users can still use wiki extract/compile sidecar commands.`
130
130
  );
131
131
  }
132
132