sdtk-wiki-kit 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -23
- package/package.json +1 -1
- package/src/commands/enrich.js +51 -0
- package/src/commands/help.js +16 -11
- package/src/commands/lint.js +1 -0
- package/src/commands/operations.js +6 -5
- package/src/commands/search.js +5 -4
- package/src/commands/wiki.js +8 -7
- package/src/index.js +4 -0
- package/src/lib/wiki-compile.js +1201 -68
- package/src/lib/wiki-enrich.js +264 -0
- package/src/lib/wiki-extract.js +685 -9
- package/src/lib/wiki-lint.js +293 -11
- package/src/lib/wiki-paths.js +55 -0
- package/src/lib/wiki-search.js +17 -10
package/src/lib/wiki-lint.js
CHANGED
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
assertWikiWorkspaceWritePath,
|
|
8
8
|
getWikiGraphPath,
|
|
9
9
|
getWikiPagesPath,
|
|
10
|
+
getPreferredWikiContentPath,
|
|
10
11
|
getWikiProvenanceSourcesPath,
|
|
11
12
|
getWikiRawSourcesPath,
|
|
12
13
|
getWikiReportsPath,
|
|
@@ -27,6 +28,21 @@ const REQUIRED_PAGE_FIELDS = [
|
|
|
27
28
|
"role",
|
|
28
29
|
];
|
|
29
30
|
|
|
31
|
+
const REQUIRED_PERSONAL_BRAIN_FIELDS = [
|
|
32
|
+
"id",
|
|
33
|
+
"title",
|
|
34
|
+
"type",
|
|
35
|
+
"status",
|
|
36
|
+
"created_at",
|
|
37
|
+
"updated_at",
|
|
38
|
+
"aliases",
|
|
39
|
+
"tags",
|
|
40
|
+
"related_pages",
|
|
41
|
+
"source_refs",
|
|
42
|
+
"confidence",
|
|
43
|
+
"review_status",
|
|
44
|
+
];
|
|
45
|
+
|
|
30
46
|
const CATEGORY_DEFS = [
|
|
31
47
|
["schema", "Frontmatter and schema"],
|
|
32
48
|
["duplicates", "Duplicate page IDs"],
|
|
@@ -40,8 +56,23 @@ const CATEGORY_DEFS = [
|
|
|
40
56
|
["markers", "TODO/Open Questions/Gaps"],
|
|
41
57
|
["contradictions", "Candidate contradictions"],
|
|
42
58
|
["sourceQuality", "Source quality"],
|
|
59
|
+
["personalBrainQuality", "Local wiki quality gate"],
|
|
43
60
|
];
|
|
44
61
|
|
|
62
|
+
const REQUIRED_PERSONAL_BRAIN_SECTIONS = {
|
|
63
|
+
source: ["Summary", "Source Metadata", "Source Quality", "Provenance"],
|
|
64
|
+
tool_entity: ["Summary", "Key Facts", "Discovery Source", "Extracted Snippet", "Topic Labels", "Why It Matters", "When To Use", "Related Repos", "Overlaps / Differences", "Open Questions", "Provenance"],
|
|
65
|
+
concept: ["Summary", "Key Axes", "Implementations / Examples", "Patterns", "Recommendations / Caveats", "Open Questions", "Related Pages", "Source References"],
|
|
66
|
+
comparison: ["Summary", "Decision Axes", "Comparison Matrix", "Candidate Tools / Repos", "Recommendations", "Caveats", "Source Confidence", "Open Questions", "Source References"],
|
|
67
|
+
synthesis: ["Summary", "Landscape Snapshot", "Key Patterns", "Decision Axes", "Recommended Review Path", "Caveats", "Source Confidence", "Related Comparisons", "Open Questions", "Source References"],
|
|
68
|
+
maintenance: ["Source Quality Findings", "Unsupported Items"],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const PERSONAL_BRAIN_STUB_CHAR_LIMIT = 600;
|
|
72
|
+
const PERSONAL_BRAIN_GIANT_PAGE_BYTES = 50000;
|
|
73
|
+
const LINT_CONTEXT_CHAR_LIMIT = 96;
|
|
74
|
+
const STRICT_SEMANTIC_PERSONAL_BRAIN_TYPES = new Set(["tool_entity", "concept", "comparison", "synthesis"]);
|
|
75
|
+
|
|
45
76
|
function toPosix(value) {
|
|
46
77
|
return String(value || "").replace(/\\/g, "/");
|
|
47
78
|
}
|
|
@@ -130,24 +161,68 @@ function listMarkdownPages(rootPath) {
|
|
|
130
161
|
return files;
|
|
131
162
|
}
|
|
132
163
|
|
|
164
|
+
function truncateForReport(value, limit = LINT_CONTEXT_CHAR_LIMIT) {
|
|
165
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
166
|
+
if (text.length <= limit) return text;
|
|
167
|
+
return `${text.slice(0, Math.max(0, limit - 3)).trim()}...`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isExternalMarkdownTarget(rawTarget) {
|
|
171
|
+
const target = String(rawTarget || "").trim();
|
|
172
|
+
return (
|
|
173
|
+
target.startsWith("#") ||
|
|
174
|
+
/^(?:https?:|mailto:|tel:)/i.test(target) ||
|
|
175
|
+
/^(?:h|ht|htt|http|https)\.{3,}(?:\s|$)/i.test(target)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
133
179
|
function extractMarkdownLinks(body) {
|
|
134
180
|
const links = [];
|
|
135
|
-
const matcher = /\[[^\]]
|
|
181
|
+
const matcher = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
136
182
|
let match;
|
|
137
183
|
while ((match = matcher.exec(body || "")) !== null) {
|
|
138
|
-
const
|
|
184
|
+
const rawLabel = String(match[1] || "").trim();
|
|
185
|
+
const rawTarget = String(match[2] || "").trim();
|
|
139
186
|
if (!rawTarget) continue;
|
|
140
|
-
if (
|
|
141
|
-
rawTarget.startsWith("#") ||
|
|
142
|
-
/^(?:https?:|mailto:|tel:)/i.test(rawTarget)
|
|
143
|
-
) {
|
|
187
|
+
if (isExternalMarkdownTarget(rawTarget)) {
|
|
144
188
|
continue;
|
|
145
189
|
}
|
|
146
|
-
links.push(
|
|
190
|
+
links.push({
|
|
191
|
+
target: rawTarget,
|
|
192
|
+
label: rawLabel,
|
|
193
|
+
context: truncateForReport(rawLabel || rawTarget),
|
|
194
|
+
});
|
|
147
195
|
}
|
|
148
196
|
return links;
|
|
149
197
|
}
|
|
150
198
|
|
|
199
|
+
function formatBrokenLinkFinding(pageRelPath, link) {
|
|
200
|
+
const target = typeof link === "string" ? link : link.target;
|
|
201
|
+
const context = typeof link === "string" ? "" : link.context;
|
|
202
|
+
const parts = [
|
|
203
|
+
`page: \`${pageRelPath}\``,
|
|
204
|
+
`missing target: \`${truncateForReport(target)}\``,
|
|
205
|
+
];
|
|
206
|
+
if (context) {
|
|
207
|
+
parts.push(`context: \`${context}\``);
|
|
208
|
+
}
|
|
209
|
+
return parts.join("; ") + ".";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function hasHeading(body, heading) {
|
|
213
|
+
const escaped = String(heading).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
214
|
+
return new RegExp(`^##\\s+${escaped}\\s*$`, "im").test(String(body || ""));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function hasSourceRefs(fields) {
|
|
218
|
+
const raw = String(fields.source_refs || "").trim();
|
|
219
|
+
return Boolean(raw && raw !== "[]" && raw !== "[\"\"]");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function formatPercent(value) {
|
|
223
|
+
return `${Math.round(value * 100)}%`;
|
|
224
|
+
}
|
|
225
|
+
|
|
151
226
|
function createFindings() {
|
|
152
227
|
return Object.fromEntries(CATEGORY_DEFS.map(([key]) => [key, []]));
|
|
153
228
|
}
|
|
@@ -426,11 +501,12 @@ function analyzePages(projectPath) {
|
|
|
426
501
|
const pagesByRelPath = new Map(pages.map((page) => [toPosix(path.normalize(page.relPath)), page]));
|
|
427
502
|
for (const page of pages) {
|
|
428
503
|
for (const link of page.links) {
|
|
429
|
-
const
|
|
504
|
+
const target = typeof link === "string" ? link : link.target;
|
|
505
|
+
const rawPath = target.split("#")[0].trim();
|
|
430
506
|
if (!rawPath) continue;
|
|
431
507
|
const resolved = path.resolve(path.dirname(page.filePath), rawPath);
|
|
432
508
|
if (!isPathInsideOrEqual(resolved, inputs.pagesRoot) || !fs.existsSync(resolved)) {
|
|
433
|
-
appendFinding(findings, "brokenLinks",
|
|
509
|
+
appendFinding(findings, "brokenLinks", formatBrokenLinkFinding(page.relPath, link));
|
|
434
510
|
continue;
|
|
435
511
|
}
|
|
436
512
|
const targetRel = toPosix(path.relative(inputs.pagesRoot, resolved));
|
|
@@ -550,7 +626,173 @@ function analyzePages(projectPath) {
|
|
|
550
626
|
|
|
551
627
|
analyzeSourceQuality(projectPath, inputs, findings);
|
|
552
628
|
|
|
553
|
-
|
|
629
|
+
const personalBrainAnalysis = analyzePersonalBrainPages(projectPath, findings);
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
findings,
|
|
633
|
+
pageCount: pages.length + personalBrainAnalysis.count,
|
|
634
|
+
personalBrainMetrics: personalBrainAnalysis.metrics,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function analyzePersonalBrainPages(projectPath, findings) {
|
|
639
|
+
const contentRoot = getPreferredWikiContentPath(projectPath);
|
|
640
|
+
const personalBrainRoot = contentRoot.path;
|
|
641
|
+
const contentRelative = toPosix(contentRoot.relative);
|
|
642
|
+
const pageFiles = listMarkdownPages(personalBrainRoot);
|
|
643
|
+
const pages = [];
|
|
644
|
+
const metrics = {
|
|
645
|
+
pageCount: pageFiles.length,
|
|
646
|
+
contentRoot: personalBrainRoot,
|
|
647
|
+
contentMode: contentRoot.mode,
|
|
648
|
+
contentRelative,
|
|
649
|
+
byType: {},
|
|
650
|
+
frontmatterCoverage: 0,
|
|
651
|
+
requiredSectionCoverage: 0,
|
|
652
|
+
sourceRefsCoverage: 0,
|
|
653
|
+
stubRatio: 0,
|
|
654
|
+
sourcePageCount: 0,
|
|
655
|
+
sourceThinAnchorCount: 0,
|
|
656
|
+
sourceThinAnchorRatio: 0,
|
|
657
|
+
semanticPageCount: 0,
|
|
658
|
+
semanticStubCount: 0,
|
|
659
|
+
semanticStubRatio: 0,
|
|
660
|
+
giantPageCount: 0,
|
|
661
|
+
brokenInternalLinks: 0,
|
|
662
|
+
conceptCount: 0,
|
|
663
|
+
entityCount: 0,
|
|
664
|
+
comparisonCount: 0,
|
|
665
|
+
synthesisCount: 0,
|
|
666
|
+
sourceEvidenceCoverage: 0,
|
|
667
|
+
};
|
|
668
|
+
let frontmatterCount = 0;
|
|
669
|
+
let sectionRequiredTotal = 0;
|
|
670
|
+
let sectionPresentTotal = 0;
|
|
671
|
+
let sourceRefsEligible = 0;
|
|
672
|
+
let sourceRefsPresent = 0;
|
|
673
|
+
let sourceEvidenceEligible = 0;
|
|
674
|
+
let sourceEvidencePresent = 0;
|
|
675
|
+
let stubCount = 0;
|
|
676
|
+
|
|
677
|
+
for (const filePath of pageFiles) {
|
|
678
|
+
const relPath = toPosix(path.relative(personalBrainRoot, filePath));
|
|
679
|
+
const parsed = parseFrontmatter(fs.readFileSync(filePath, "utf-8"));
|
|
680
|
+
const fields = parsed.fields;
|
|
681
|
+
const type = String(fields.type || "unknown");
|
|
682
|
+
pages.push({ filePath, relPath, fields, body: parsed.body, hasFrontmatter: parsed.hasFrontmatter, type });
|
|
683
|
+
metrics.byType[type] = (metrics.byType[type] || 0) + 1;
|
|
684
|
+
if (type === "source") metrics.sourcePageCount += 1;
|
|
685
|
+
if (STRICT_SEMANTIC_PERSONAL_BRAIN_TYPES.has(type)) metrics.semanticPageCount += 1;
|
|
686
|
+
if (type === "concept") metrics.conceptCount += 1;
|
|
687
|
+
if (type === "tool_entity") metrics.entityCount += 1;
|
|
688
|
+
if (type === "comparison") metrics.comparisonCount += 1;
|
|
689
|
+
if (type === "synthesis") metrics.synthesisCount += 1;
|
|
690
|
+
|
|
691
|
+
if (!parsed.hasFrontmatter) {
|
|
692
|
+
appendFinding(findings, "schema", `${contentRelative}/${relPath} is missing parseable frontmatter.`);
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
frontmatterCount += 1;
|
|
696
|
+
|
|
697
|
+
const missingFields = REQUIRED_PERSONAL_BRAIN_FIELDS.filter((field) => !fields[field]);
|
|
698
|
+
if (missingFields.length > 0) {
|
|
699
|
+
appendFinding(
|
|
700
|
+
findings,
|
|
701
|
+
"schema",
|
|
702
|
+
`${contentRelative}/${relPath} is missing required local wiki fields: ${missingFields.join(", ")}.`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const requiredSections = REQUIRED_PERSONAL_BRAIN_SECTIONS[type] || [];
|
|
707
|
+
sectionRequiredTotal += requiredSections.length;
|
|
708
|
+
const missingSections = requiredSections.filter((section) => !hasHeading(parsed.body, section));
|
|
709
|
+
sectionPresentTotal += requiredSections.length - missingSections.length;
|
|
710
|
+
if (missingSections.length > 0) {
|
|
711
|
+
appendFinding(
|
|
712
|
+
findings,
|
|
713
|
+
"personalBrainQuality",
|
|
714
|
+
`${contentRelative}/${relPath} is missing required sections for ${type}: ${missingSections.join(", ")}.`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (!["root"].includes(type)) {
|
|
719
|
+
sourceRefsEligible += 1;
|
|
720
|
+
if (hasSourceRefs(fields)) sourceRefsPresent += 1;
|
|
721
|
+
else appendFinding(findings, "personalBrainQuality", `${contentRelative}/${relPath} has no source_refs coverage.`);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (["tool_entity", "concept", "comparison", "synthesis"].includes(type)) {
|
|
725
|
+
sourceEvidenceEligible += 1;
|
|
726
|
+
if (hasSourceRefs(fields) && /(?:source|provenance|evidence)/i.test(parsed.body)) {
|
|
727
|
+
sourceEvidencePresent += 1;
|
|
728
|
+
} else {
|
|
729
|
+
appendFinding(findings, "personalBrainQuality", `${contentRelative}/${relPath} lacks source evidence coverage in body/frontmatter.`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const compactBody = parsed.body.replace(/\s+/g, " ").trim();
|
|
734
|
+
if (type === "source") {
|
|
735
|
+
metrics.sourceThinAnchorCount += 1;
|
|
736
|
+
} else if (compactBody.length < PERSONAL_BRAIN_STUB_CHAR_LIMIT && STRICT_SEMANTIC_PERSONAL_BRAIN_TYPES.has(type)) {
|
|
737
|
+
stubCount += 1;
|
|
738
|
+
metrics.semanticStubCount += 1;
|
|
739
|
+
appendFinding(findings, "personalBrainQuality", `${contentRelative}/${relPath} appears stub-like (${compactBody.length} normalized characters).`);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const byteSize = fs.statSync(filePath).size;
|
|
743
|
+
if (byteSize > PERSONAL_BRAIN_GIANT_PAGE_BYTES) {
|
|
744
|
+
metrics.giantPageCount += 1;
|
|
745
|
+
appendFinding(findings, "personalBrainQuality", `${contentRelative}/${relPath} is very large (${byteSize} bytes) and may need splitting.`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
for (const page of pages) {
|
|
750
|
+
for (const link of extractMarkdownLinks(page.body)) {
|
|
751
|
+
const target = typeof link === "string" ? link : link.target;
|
|
752
|
+
const rawPath = target.split("#")[0].trim();
|
|
753
|
+
if (!rawPath) continue;
|
|
754
|
+
const resolved = path.resolve(path.dirname(page.filePath), rawPath);
|
|
755
|
+
if (!isPathInsideOrEqual(resolved, personalBrainRoot) || !fs.existsSync(resolved)) {
|
|
756
|
+
appendFinding(findings, "brokenLinks", formatBrokenLinkFinding(`${contentRelative}/${page.relPath}`, link));
|
|
757
|
+
metrics.brokenInternalLinks += 1;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
metrics.frontmatterCoverage = pageFiles.length > 0 ? frontmatterCount / pageFiles.length : 1;
|
|
763
|
+
metrics.requiredSectionCoverage = sectionRequiredTotal > 0 ? sectionPresentTotal / sectionRequiredTotal : 1;
|
|
764
|
+
metrics.sourceRefsCoverage = sourceRefsEligible > 0 ? sourceRefsPresent / sourceRefsEligible : 1;
|
|
765
|
+
metrics.stubRatio = pageFiles.length > 0 ? stubCount / pageFiles.length : 0;
|
|
766
|
+
metrics.sourceThinAnchorRatio = metrics.sourcePageCount > 0 ? metrics.sourceThinAnchorCount / metrics.sourcePageCount : 0;
|
|
767
|
+
metrics.semanticStubRatio = metrics.semanticPageCount > 0 ? metrics.semanticStubCount / metrics.semanticPageCount : 0;
|
|
768
|
+
metrics.sourceEvidenceCoverage = sourceEvidenceEligible > 0 ? sourceEvidencePresent / sourceEvidenceEligible : 1;
|
|
769
|
+
|
|
770
|
+
if (pageFiles.length === 0) {
|
|
771
|
+
appendFinding(findings, "personalBrainQuality", "No local wiki pages were found under wiki/ or legacy .sdtk/wiki/personal-brain.");
|
|
772
|
+
}
|
|
773
|
+
if (metrics.frontmatterCoverage < 1) {
|
|
774
|
+
appendFinding(findings, "personalBrainQuality", `Frontmatter coverage is ${formatPercent(metrics.frontmatterCoverage)}; expected 100%.`);
|
|
775
|
+
}
|
|
776
|
+
if (metrics.requiredSectionCoverage < 0.95) {
|
|
777
|
+
appendFinding(findings, "personalBrainQuality", `Required section coverage is ${formatPercent(metrics.requiredSectionCoverage)}; expected at least 95%.`);
|
|
778
|
+
}
|
|
779
|
+
if (metrics.sourceRefsCoverage < 0.9) {
|
|
780
|
+
appendFinding(findings, "personalBrainQuality", `Source refs coverage is ${formatPercent(metrics.sourceRefsCoverage)}; expected at least 90%.`);
|
|
781
|
+
}
|
|
782
|
+
if (metrics.semanticStubRatio > 0.1) {
|
|
783
|
+
appendFinding(findings, "personalBrainQuality", `Semantic stub ratio is ${formatPercent(metrics.semanticStubRatio)}; expected at most 10% for tool/concept/comparison/synthesis pages.`);
|
|
784
|
+
}
|
|
785
|
+
if (metrics.entityCount > 0 && metrics.conceptCount === 0) {
|
|
786
|
+
appendFinding(findings, "personalBrainQuality", "Tool/entity pages exist but no concept pages were generated.");
|
|
787
|
+
}
|
|
788
|
+
if (metrics.conceptCount > 0 && metrics.comparisonCount === 0) {
|
|
789
|
+
appendFinding(findings, "personalBrainQuality", "Concept pages exist but no comparison pages were generated.");
|
|
790
|
+
}
|
|
791
|
+
if (metrics.comparisonCount > 0 && metrics.synthesisCount === 0) {
|
|
792
|
+
appendFinding(findings, "personalBrainQuality", "Comparison pages exist but no synthesis pages were generated.");
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return { count: pageFiles.length, metrics };
|
|
554
796
|
}
|
|
555
797
|
|
|
556
798
|
function todayStamp() {
|
|
@@ -561,7 +803,43 @@ function totalFindings(findings) {
|
|
|
561
803
|
return CATEGORY_DEFS.reduce((sum, [key]) => sum + findings[key].length, 0);
|
|
562
804
|
}
|
|
563
805
|
|
|
564
|
-
function
|
|
806
|
+
function renderPersonalBrainMetrics(metrics) {
|
|
807
|
+
if (!metrics) return ["## Local Wiki Quality Metrics", "", "- No local wiki metrics available.", ""];
|
|
808
|
+
const typeRows = Object.entries(metrics.byType || {})
|
|
809
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
810
|
+
.map(([type, count]) => `| ${type} | ${count} |`);
|
|
811
|
+
return [
|
|
812
|
+
"## Local Wiki Quality Metrics",
|
|
813
|
+
"",
|
|
814
|
+
`- wiki content root: ${metrics.contentRoot}`,
|
|
815
|
+
`- wiki content mode: ${metrics.contentMode}`,
|
|
816
|
+
`- local wiki pages: ${metrics.pageCount}`,
|
|
817
|
+
`- frontmatter coverage: ${formatPercent(metrics.frontmatterCoverage)}`,
|
|
818
|
+
`- required section coverage: ${formatPercent(metrics.requiredSectionCoverage)}`,
|
|
819
|
+
`- source refs coverage: ${formatPercent(metrics.sourceRefsCoverage)}`,
|
|
820
|
+
`- source evidence coverage: ${formatPercent(metrics.sourceEvidenceCoverage)}`,
|
|
821
|
+
`- source pages: ${metrics.sourcePageCount}`,
|
|
822
|
+
`- source pages accepted as provenance anchors: ${metrics.sourceThinAnchorCount}`,
|
|
823
|
+
`- source-page thin-anchor ratio: ${formatPercent(metrics.sourceThinAnchorRatio)}`,
|
|
824
|
+
`- strict semantic pages: ${metrics.semanticPageCount}`,
|
|
825
|
+
`- strict semantic stub pages: ${metrics.semanticStubCount}`,
|
|
826
|
+
`- semantic stub ratio: ${formatPercent(metrics.semanticStubRatio)}`,
|
|
827
|
+
`- stub ratio policy: source pages are measured separately; strict stub findings apply to tool_entity, concept, comparison, and synthesis pages.`,
|
|
828
|
+
`- giant page warnings: ${metrics.giantPageCount}`,
|
|
829
|
+
`- broken local wiki internal links: ${metrics.brokenInternalLinks}`,
|
|
830
|
+
`- entity pages: ${metrics.entityCount}`,
|
|
831
|
+
`- concept pages: ${metrics.conceptCount}`,
|
|
832
|
+
`- comparison pages: ${metrics.comparisonCount}`,
|
|
833
|
+
`- synthesis pages: ${metrics.synthesisCount}`,
|
|
834
|
+
"",
|
|
835
|
+
"| Page type | Count |",
|
|
836
|
+
"|---|---:|",
|
|
837
|
+
...(typeRows.length > 0 ? typeRows : ["| none | 0 |"]),
|
|
838
|
+
"",
|
|
839
|
+
];
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function renderReport({ projectPath, workspaceRoot, findings, pageCount, personalBrainMetrics }) {
|
|
565
843
|
const summaryRows = CATEGORY_DEFS.map(
|
|
566
844
|
([key, label]) => `| ${label} | ${findings[key].length} |`
|
|
567
845
|
);
|
|
@@ -593,6 +871,7 @@ function renderReport({ projectPath, workspaceRoot, findings, pageCount }) {
|
|
|
593
871
|
"|---|---:|",
|
|
594
872
|
...summaryRows,
|
|
595
873
|
"",
|
|
874
|
+
...renderPersonalBrainMetrics(personalBrainMetrics),
|
|
596
875
|
...detailSections,
|
|
597
876
|
].join("\n");
|
|
598
877
|
}
|
|
@@ -622,12 +901,14 @@ function runWikiLint(options = {}) {
|
|
|
622
901
|
workspaceRoot,
|
|
623
902
|
findings: analysis.findings,
|
|
624
903
|
pageCount: analysis.pageCount,
|
|
904
|
+
personalBrainMetrics: analysis.personalBrainMetrics,
|
|
625
905
|
});
|
|
626
906
|
fs.writeFileSync(reportPath, report + "\n", "utf-8");
|
|
627
907
|
return {
|
|
628
908
|
reportPath,
|
|
629
909
|
totalFindings: totalFindings(analysis.findings),
|
|
630
910
|
findings: analysis.findings,
|
|
911
|
+
personalBrainMetrics: analysis.personalBrainMetrics,
|
|
631
912
|
};
|
|
632
913
|
} catch (error) {
|
|
633
914
|
if (error instanceof CliError) throw error;
|
|
@@ -637,6 +918,7 @@ function runWikiLint(options = {}) {
|
|
|
637
918
|
|
|
638
919
|
module.exports = {
|
|
639
920
|
CATEGORY_DEFS,
|
|
921
|
+
REQUIRED_PERSONAL_BRAIN_FIELDS,
|
|
640
922
|
REQUIRED_PAGE_FIELDS,
|
|
641
923
|
analyzePages,
|
|
642
924
|
parseFrontmatter,
|
package/src/lib/wiki-paths.js
CHANGED
|
@@ -15,6 +15,8 @@ const WIKI_PROVENANCE_SOURCES_RELATIVE = path.join(".sdtk", "wiki", "provenance"
|
|
|
15
15
|
const WIKI_QUERIES_RELATIVE = path.join(".sdtk", "wiki", "queries");
|
|
16
16
|
const WIKI_REPORTS_RELATIVE = path.join(".sdtk", "wiki", "reports");
|
|
17
17
|
const WIKI_LOGS_RELATIVE = path.join(".sdtk", "wiki", "logs");
|
|
18
|
+
const CANONICAL_WIKI_RELATIVE = "wiki";
|
|
19
|
+
const LEGACY_PERSONAL_BRAIN_RELATIVE = path.join(".sdtk", "wiki", "personal-brain");
|
|
18
20
|
const LEGACY_ATLAS_RELATIVE = path.join(".sdtk", "atlas");
|
|
19
21
|
|
|
20
22
|
function resolveProjectPath(projectPath) {
|
|
@@ -51,6 +53,43 @@ function getWikiWorkspacePath(projectPath) {
|
|
|
51
53
|
return path.join(resolveProjectPath(projectPath), WIKI_WORKSPACE_RELATIVE);
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
function getCanonicalWikiPath(projectPath) {
|
|
57
|
+
return path.join(resolveProjectPath(projectPath), CANONICAL_WIKI_RELATIVE);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getLegacyPersonalBrainPath(projectPath) {
|
|
61
|
+
return path.join(resolveProjectPath(projectPath), LEGACY_PERSONAL_BRAIN_RELATIVE);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getPreferredWikiContentPath(projectPath) {
|
|
65
|
+
const canonical = getCanonicalWikiPath(projectPath);
|
|
66
|
+
const legacy = getLegacyPersonalBrainPath(projectPath);
|
|
67
|
+
try {
|
|
68
|
+
const fs = require("fs");
|
|
69
|
+
if (fs.existsSync(canonical) && fs.statSync(canonical).isDirectory()) {
|
|
70
|
+
return {
|
|
71
|
+
path: canonical,
|
|
72
|
+
mode: "canonical_project_wiki",
|
|
73
|
+
relative: CANONICAL_WIKI_RELATIVE,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (fs.existsSync(legacy) && fs.statSync(legacy).isDirectory()) {
|
|
77
|
+
return {
|
|
78
|
+
path: legacy,
|
|
79
|
+
mode: "legacy_personal_brain_fallback",
|
|
80
|
+
relative: LEGACY_PERSONAL_BRAIN_RELATIVE,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
} catch (_) {
|
|
84
|
+
// Callers perform their own existence validation and error reporting.
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
path: canonical,
|
|
88
|
+
mode: "canonical_project_wiki",
|
|
89
|
+
relative: CANONICAL_WIKI_RELATIVE,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
54
93
|
function getWikiGraphPath(projectPath) {
|
|
55
94
|
return path.join(resolveProjectPath(projectPath), WIKI_GRAPH_RELATIVE);
|
|
56
95
|
}
|
|
@@ -111,9 +150,19 @@ function assertWikiWorkspaceWritePath(targetPath, projectPath) {
|
|
|
111
150
|
);
|
|
112
151
|
}
|
|
113
152
|
|
|
153
|
+
function assertCanonicalWikiWritePath(targetPath, projectPath) {
|
|
154
|
+
return assertPathInsideOrEqual(
|
|
155
|
+
targetPath,
|
|
156
|
+
getCanonicalWikiPath(projectPath),
|
|
157
|
+
"Refusing to write outside project-local wiki output"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
114
161
|
function describeWikiPaths(projectPath) {
|
|
115
162
|
return {
|
|
116
163
|
projectPath: resolveProjectPath(projectPath),
|
|
164
|
+
canonicalWikiPath: getCanonicalWikiPath(projectPath),
|
|
165
|
+
legacyPersonalBrainPath: getLegacyPersonalBrainPath(projectPath),
|
|
117
166
|
wikiWorkspacePath: getWikiWorkspacePath(projectPath),
|
|
118
167
|
wikiGraphPath: getWikiGraphPath(projectPath),
|
|
119
168
|
wikiManifestPath: getWikiManifestPath(projectPath),
|
|
@@ -132,7 +181,9 @@ function describeWikiPaths(projectPath) {
|
|
|
132
181
|
}
|
|
133
182
|
|
|
134
183
|
module.exports = {
|
|
184
|
+
CANONICAL_WIKI_RELATIVE,
|
|
135
185
|
LEGACY_ATLAS_RELATIVE,
|
|
186
|
+
LEGACY_PERSONAL_BRAIN_RELATIVE,
|
|
136
187
|
WIKI_GRAPH_RELATIVE,
|
|
137
188
|
WIKI_LOGS_RELATIVE,
|
|
138
189
|
WIKI_MANIFEST_RELATIVE,
|
|
@@ -147,9 +198,13 @@ module.exports = {
|
|
|
147
198
|
WIKI_REPORTS_RELATIVE,
|
|
148
199
|
WIKI_WORKSPACE_RELATIVE,
|
|
149
200
|
assertPathInsideOrEqual,
|
|
201
|
+
assertCanonicalWikiWritePath,
|
|
150
202
|
assertWikiWorkspaceWritePath,
|
|
151
203
|
describeWikiPaths,
|
|
204
|
+
getCanonicalWikiPath,
|
|
205
|
+
getLegacyPersonalBrainPath,
|
|
152
206
|
getLegacyAtlasPath,
|
|
207
|
+
getPreferredWikiContentPath,
|
|
153
208
|
getWikiGraphPath,
|
|
154
209
|
getWikiLogsPath,
|
|
155
210
|
getWikiManifestPath,
|
package/src/lib/wiki-search.js
CHANGED
|
@@ -4,13 +4,14 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const { ValidationError } = require("./errors");
|
|
6
6
|
const {
|
|
7
|
-
|
|
7
|
+
getPreferredWikiContentPath,
|
|
8
8
|
isPathInsideOrEqual,
|
|
9
9
|
resolveProjectPath,
|
|
10
10
|
} = require("./wiki-paths");
|
|
11
11
|
|
|
12
12
|
const DEFAULT_LIMIT = 10;
|
|
13
|
-
const
|
|
13
|
+
const CANONICAL_WIKI_RELATIVE = "wiki";
|
|
14
|
+
const LEGACY_PERSONAL_BRAIN_RELATIVE = path.join(".sdtk", "wiki", "personal-brain");
|
|
14
15
|
|
|
15
16
|
function toPosix(value) {
|
|
16
17
|
return String(value || "").replace(/\\/g, "/");
|
|
@@ -120,18 +121,19 @@ function runWikiSearch({ projectPath, query, limit = DEFAULT_LIMIT }) {
|
|
|
120
121
|
|
|
121
122
|
const parsedLimit = Number.parseInt(limit, 10);
|
|
122
123
|
const safeLimit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 50) : DEFAULT_LIMIT;
|
|
123
|
-
const
|
|
124
|
-
|
|
124
|
+
const contentRoot = getPreferredWikiContentPath(resolvedProjectPath);
|
|
125
|
+
const wikiContentPath = contentRoot.path;
|
|
126
|
+
if (!isPathInsideOrEqual(wikiContentPath, resolvedProjectPath)) {
|
|
125
127
|
throw new ValidationError("Refusing to search outside the project root.");
|
|
126
128
|
}
|
|
127
|
-
if (!fs.existsSync(
|
|
129
|
+
if (!fs.existsSync(wikiContentPath) || !fs.statSync(wikiContentPath).isDirectory()) {
|
|
128
130
|
throw new ValidationError(
|
|
129
|
-
`No SDTK-WIKI
|
|
131
|
+
`No SDTK-WIKI local wiki found at ${wikiContentPath}. Run "sdtk-wiki ingest <source-root>" and "sdtk-wiki compile --mode safe --apply" first. Legacy .sdtk/wiki/personal-brain workspaces are still readable when present.`
|
|
130
132
|
);
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
const tokens = tokenize(normalizedQuery);
|
|
134
|
-
const files = collectMarkdownFiles(
|
|
136
|
+
const files = collectMarkdownFiles(wikiContentPath);
|
|
135
137
|
const matches = [];
|
|
136
138
|
|
|
137
139
|
for (const filePath of files) {
|
|
@@ -157,19 +159,24 @@ function runWikiSearch({ projectPath, query, limit = DEFAULT_LIMIT }) {
|
|
|
157
159
|
return {
|
|
158
160
|
query: normalizedQuery,
|
|
159
161
|
projectPath: resolvedProjectPath,
|
|
160
|
-
|
|
162
|
+
wikiContentPath,
|
|
163
|
+
wikiContentMode: contentRoot.mode,
|
|
164
|
+
personalBrainPath: wikiContentPath,
|
|
161
165
|
scannedFiles: files.length,
|
|
162
166
|
matches: matches.slice(0, safeLimit),
|
|
163
167
|
totalMatches: matches.length,
|
|
164
168
|
limit: safeLimit,
|
|
165
|
-
searchMode: "
|
|
169
|
+
searchMode: contentRoot.mode === "canonical_project_wiki"
|
|
170
|
+
? "local_deterministic_project_wiki_markdown"
|
|
171
|
+
: "local_deterministic_legacy_personal_brain_markdown",
|
|
166
172
|
premiumRequired: false,
|
|
167
173
|
mutated: false,
|
|
168
174
|
};
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
module.exports = {
|
|
172
|
-
|
|
178
|
+
CANONICAL_WIKI_RELATIVE,
|
|
179
|
+
LEGACY_PERSONAL_BRAIN_RELATIVE,
|
|
173
180
|
runWikiSearch,
|
|
174
181
|
tokenize,
|
|
175
182
|
};
|