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
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { ValidationError } = require("./errors");
|
|
6
|
+
const {
|
|
7
|
+
assertWikiWorkspaceWritePath,
|
|
8
|
+
getPreferredWikiContentPath,
|
|
9
|
+
getWikiReportsPath,
|
|
10
|
+
isPathInsideOrEqual,
|
|
11
|
+
resolveProjectPath,
|
|
12
|
+
} = require("./wiki-paths");
|
|
13
|
+
|
|
14
|
+
const REPORT_PREFIX = "github-enrichment-review";
|
|
15
|
+
|
|
16
|
+
function toPosix(value) {
|
|
17
|
+
return String(value || "").replace(/\\/g, "/");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function todayStamp() {
|
|
21
|
+
return new Date().toISOString().slice(0, 10);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseFrontmatter(text) {
|
|
25
|
+
const source = String(text || "");
|
|
26
|
+
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
27
|
+
if (!match) return { fields: {}, body: source };
|
|
28
|
+
const fields = {};
|
|
29
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
30
|
+
const separator = line.indexOf(":");
|
|
31
|
+
if (separator === -1) continue;
|
|
32
|
+
const key = line.slice(0, separator).trim();
|
|
33
|
+
const value = line.slice(separator + 1).trim();
|
|
34
|
+
fields[key] = value.replace(/^["']|["']$/g, "");
|
|
35
|
+
}
|
|
36
|
+
return { fields, body: source.slice(match[0].length) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseInlineList(value) {
|
|
40
|
+
const text = String(value || "").trim();
|
|
41
|
+
if (!text || text === "[]") return [];
|
|
42
|
+
const inner = text.startsWith("[") && text.endsWith("]") ? text.slice(1, -1) : text;
|
|
43
|
+
return inner
|
|
44
|
+
.split(",")
|
|
45
|
+
.map((item) => item.trim().replace(/^["']|["']$/g, ""))
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function collectMarkdownFiles(rootPath) {
|
|
50
|
+
const files = [];
|
|
51
|
+
if (!fs.existsSync(rootPath)) return files;
|
|
52
|
+
function visit(current) {
|
|
53
|
+
const stat = fs.statSync(current);
|
|
54
|
+
if (stat.isDirectory()) {
|
|
55
|
+
for (const child of fs.readdirSync(current).sort()) {
|
|
56
|
+
visit(path.join(current, child));
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (stat.isFile() && current.toLowerCase().endsWith(".md")) {
|
|
61
|
+
files.push(current);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
visit(rootPath);
|
|
65
|
+
return files.sort((a, b) => toPosix(a).localeCompare(toPosix(b)));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractGithubRepos(text) {
|
|
69
|
+
const repos = [];
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
const matcher = /https?:\/\/github\.com\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9._-]+)/gi;
|
|
72
|
+
let match;
|
|
73
|
+
while ((match = matcher.exec(String(text || ""))) !== null) {
|
|
74
|
+
const owner = match[1];
|
|
75
|
+
const repo = match[2].replace(/[).,;:]+$/g, "").replace(/\.git$/i, "");
|
|
76
|
+
if (!repo || repo.includes("...")) continue;
|
|
77
|
+
const url = `https://github.com/${owner}/${repo}`;
|
|
78
|
+
const key = url.toLowerCase();
|
|
79
|
+
if (seen.has(key)) continue;
|
|
80
|
+
seen.add(key);
|
|
81
|
+
repos.push({ owner, repo, url });
|
|
82
|
+
}
|
|
83
|
+
return repos;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function firstSection(body, heading) {
|
|
87
|
+
const escaped = String(heading).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
88
|
+
const matcher = new RegExp(`^##\\s+${escaped}\\s*$([\\s\\S]*?)(?=^##\\s+|$)`, "im");
|
|
89
|
+
const match = String(body || "").match(matcher);
|
|
90
|
+
if (!match) return "";
|
|
91
|
+
return match[1].replace(/\s+/g, " ").trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function extractProvenanceRefs(body) {
|
|
95
|
+
const section = firstSection(body, "Provenance");
|
|
96
|
+
if (!section) return [];
|
|
97
|
+
return section
|
|
98
|
+
.split(/\s*-\s+/)
|
|
99
|
+
.map((item) => item.trim())
|
|
100
|
+
.filter((item) => item.startsWith("prov_"))
|
|
101
|
+
.slice(0, 12);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function recordFromPage(filePath, contentRoot) {
|
|
105
|
+
const text = fs.readFileSync(filePath, "utf-8");
|
|
106
|
+
const parsed = parseFrontmatter(text);
|
|
107
|
+
const fields = parsed.fields;
|
|
108
|
+
const repos = extractGithubRepos(text);
|
|
109
|
+
if (repos.length === 0) return [];
|
|
110
|
+
|
|
111
|
+
const relPath = toPosix(path.relative(contentRoot.path, filePath));
|
|
112
|
+
const type = String(fields.type || "");
|
|
113
|
+
const title = String(fields.title || path.basename(filePath, ".md"));
|
|
114
|
+
const tags = parseInlineList(fields.tags);
|
|
115
|
+
const sourceRefs = parseInlineList(fields.source_refs);
|
|
116
|
+
const summary = firstSection(parsed.body, "Summary");
|
|
117
|
+
const provenanceRefs = extractProvenanceRefs(parsed.body);
|
|
118
|
+
|
|
119
|
+
return repos.map((repo) => ({
|
|
120
|
+
record_type: "sdtk_wiki_github_enrichment_candidate",
|
|
121
|
+
status: "review_only_local_identity",
|
|
122
|
+
page_path: `${toPosix(contentRoot.relative)}/${relPath}`,
|
|
123
|
+
page_type: type || "unknown",
|
|
124
|
+
page_title: title,
|
|
125
|
+
repo_url: repo.url,
|
|
126
|
+
source_url: repo.url,
|
|
127
|
+
owner: repo.owner,
|
|
128
|
+
repo: repo.repo,
|
|
129
|
+
local_summary: summary || null,
|
|
130
|
+
local_topics: tags,
|
|
131
|
+
source_refs: sourceRefs,
|
|
132
|
+
provenance_refs: provenanceRefs,
|
|
133
|
+
confidence: fields.confidence || "unknown",
|
|
134
|
+
metadata: {
|
|
135
|
+
stars: null,
|
|
136
|
+
license: null,
|
|
137
|
+
description: null,
|
|
138
|
+
current_owner: null,
|
|
139
|
+
default_branch: null,
|
|
140
|
+
last_verified_at: null,
|
|
141
|
+
},
|
|
142
|
+
fetch_timestamp: null,
|
|
143
|
+
fetch_status: "not_fetched_local_review_mode",
|
|
144
|
+
failure_status: "not_fetched_local_review_mode",
|
|
145
|
+
failure_reason: "Network metadata was not requested in review-only local mode.",
|
|
146
|
+
enrichment_mode: "review",
|
|
147
|
+
mutation: "none",
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function uniqueRecords(records) {
|
|
152
|
+
const byRepo = new Map();
|
|
153
|
+
for (const record of records) {
|
|
154
|
+
const key = record.repo_url.toLowerCase();
|
|
155
|
+
const existing = byRepo.get(key);
|
|
156
|
+
if (!existing) {
|
|
157
|
+
byRepo.set(key, record);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
existing.page_path = existing.page_path || record.page_path;
|
|
161
|
+
existing.local_topics = Array.from(new Set([...existing.local_topics, ...record.local_topics])).sort();
|
|
162
|
+
existing.source_refs = Array.from(new Set([...existing.source_refs, ...record.source_refs])).sort();
|
|
163
|
+
existing.provenance_refs = Array.from(new Set([...existing.provenance_refs, ...record.provenance_refs])).sort();
|
|
164
|
+
}
|
|
165
|
+
return [...byRepo.values()].sort((a, b) => a.repo_url.localeCompare(b.repo_url));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function renderMarkdownReport({ projectPath, reportJsonPath, records, contentRoot }) {
|
|
169
|
+
const lines = [
|
|
170
|
+
"# SDTK-WIKI GitHub Enrichment Review",
|
|
171
|
+
"",
|
|
172
|
+
`Date: ${todayStamp()}`,
|
|
173
|
+
`Project root: \`${projectPath}\``,
|
|
174
|
+
`Wiki content root: \`${contentRoot.path}\``,
|
|
175
|
+
`Wiki content mode: \`${contentRoot.mode}\``,
|
|
176
|
+
`JSON report: \`${reportJsonPath}\``,
|
|
177
|
+
"",
|
|
178
|
+
"Report-only and non-destructive: this command does not fetch network metadata, rewrite pages, mutate sources, or touch `.sdtk/atlas`.",
|
|
179
|
+
"Network-backed GitHub verification is deferred to a later controller-approved slice.",
|
|
180
|
+
"",
|
|
181
|
+
"## Summary",
|
|
182
|
+
"",
|
|
183
|
+
`- candidates: ${records.length}`,
|
|
184
|
+
"- source: github",
|
|
185
|
+
"- mode: review",
|
|
186
|
+
"- fetch status: not_fetched_local_review_mode",
|
|
187
|
+
"",
|
|
188
|
+
"## Candidates",
|
|
189
|
+
"",
|
|
190
|
+
"| Repo | Page | Confidence | Source refs | Fetch status | Pending metadata |",
|
|
191
|
+
"|---|---|---|---:|---|---|",
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
for (const record of records) {
|
|
195
|
+
lines.push(
|
|
196
|
+
`| [${record.owner}/${record.repo}](${record.repo_url}) | \`${record.page_path}\` | ${record.confidence} | ${record.source_refs.length} | ${record.fetch_status} | stars, license, description, current owner |`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (records.length === 0) {
|
|
200
|
+
lines.push("| none | - | - | 0 | - | - |");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
lines.push(
|
|
204
|
+
"",
|
|
205
|
+
"## Review Notes",
|
|
206
|
+
"",
|
|
207
|
+
"- Treat every candidate as locally identified only until a future explicit network/enrichment apply issue verifies it.",
|
|
208
|
+
"- Do not copy stars, license, or activity claims into generated pages from this report unless those fields are later verified.",
|
|
209
|
+
""
|
|
210
|
+
);
|
|
211
|
+
return lines.join("\n");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function runWikiGithubEnrichmentReview(options = {}) {
|
|
215
|
+
const projectPath = resolveProjectPath(options.projectPath || process.cwd());
|
|
216
|
+
if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) {
|
|
217
|
+
throw new ValidationError(`--project-path is not a valid directory: ${projectPath}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const contentRoot = getPreferredWikiContentPath(projectPath);
|
|
221
|
+
if (!isPathInsideOrEqual(contentRoot.path, projectPath)) {
|
|
222
|
+
throw new ValidationError("Refusing to read local wiki pages outside the project root.");
|
|
223
|
+
}
|
|
224
|
+
if (!fs.existsSync(contentRoot.path) || !fs.statSync(contentRoot.path).isDirectory()) {
|
|
225
|
+
throw new ValidationError(
|
|
226
|
+
`No SDTK-WIKI local wiki found at ${contentRoot.path}. Run "sdtk-wiki ingest <source-root>" and "sdtk-wiki compile --mode safe --apply" first.`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const records = uniqueRecords(collectMarkdownFiles(contentRoot.path).flatMap((filePath) =>
|
|
231
|
+
recordFromPage(filePath, contentRoot)
|
|
232
|
+
));
|
|
233
|
+
|
|
234
|
+
const reportsRoot = getWikiReportsPath(projectPath);
|
|
235
|
+
assertWikiWorkspaceWritePath(reportsRoot, projectPath);
|
|
236
|
+
fs.mkdirSync(reportsRoot, { recursive: true });
|
|
237
|
+
const jsonPath = path.join(reportsRoot, `${REPORT_PREFIX}-${todayStamp()}.json`);
|
|
238
|
+
const markdownPath = path.join(reportsRoot, `${REPORT_PREFIX}-${todayStamp()}.md`);
|
|
239
|
+
assertWikiWorkspaceWritePath(jsonPath, projectPath);
|
|
240
|
+
assertWikiWorkspaceWritePath(markdownPath, projectPath);
|
|
241
|
+
|
|
242
|
+
const payload = {
|
|
243
|
+
schema_version: 1,
|
|
244
|
+
record_type: "sdtk_wiki_github_enrichment_review",
|
|
245
|
+
generated_at: new Date().toISOString(),
|
|
246
|
+
project_path: projectPath,
|
|
247
|
+
wiki_content_path: contentRoot.path,
|
|
248
|
+
wiki_content_mode: contentRoot.mode,
|
|
249
|
+
source: "github",
|
|
250
|
+
mode: "review",
|
|
251
|
+
network_fetch: false,
|
|
252
|
+
mutation: "none",
|
|
253
|
+
candidate_count: records.length,
|
|
254
|
+
records,
|
|
255
|
+
};
|
|
256
|
+
fs.writeFileSync(jsonPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
257
|
+
fs.writeFileSync(markdownPath, renderMarkdownReport({ projectPath, reportJsonPath: jsonPath, records, contentRoot }), "utf-8");
|
|
258
|
+
|
|
259
|
+
return { markdownPath, jsonPath, records, projectPath };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = {
|
|
263
|
+
runWikiGithubEnrichmentReview,
|
|
264
|
+
};
|