sdtk-wiki-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { CliError } = require("./errors");
6
+ const { getWikiGraphPath, resolveProjectPath } = require("./wiki-paths");
7
+ const { loadWikiAskHandler } = require("./wiki-premium-loader");
8
+
9
+ const INDEX_FILE = "SDTK_DOC_INDEX.json";
10
+ const GRAPH_FILE = "SDTK_DOC_GRAPH.json";
11
+ const DEFAULT_MAX_SOURCES = 6;
12
+
13
+ function readJsonFile(filePath, label) {
14
+ try {
15
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
16
+ } catch (err) {
17
+ throw new CliError(`${label} could not be read from ${filePath}: ${err.message}`, 3);
18
+ }
19
+ }
20
+
21
+ function assertWikiGraphReady(projectPath) {
22
+ const graphPath = getWikiGraphPath(projectPath);
23
+ const indexPath = path.join(graphPath, INDEX_FILE);
24
+ const graphFilePath = path.join(graphPath, GRAPH_FILE);
25
+
26
+ if (!fs.existsSync(graphPath) || !fs.statSync(graphPath).isDirectory()) {
27
+ throw new CliError(
28
+ `SDTK-WIKI Ask requires a built wiki graph at .sdtk/wiki/graph. ` +
29
+ `Run "sdtk-wiki atlas build" before running "sdtk-wiki ask".`,
30
+ 1
31
+ );
32
+ }
33
+ if (!fs.existsSync(indexPath) || !fs.existsSync(graphFilePath)) {
34
+ throw new CliError(
35
+ `SDTK-WIKI Ask requires ${INDEX_FILE} and ${GRAPH_FILE} under .sdtk/wiki/graph. ` +
36
+ `Run "sdtk-wiki atlas build" to rebuild the graph.`,
37
+ 1
38
+ );
39
+ }
40
+
41
+ return { graphPath, indexPath, graphFilePath };
42
+ }
43
+
44
+ function extractDocumentPath(document) {
45
+ return (
46
+ document.path ||
47
+ document.sourcePath ||
48
+ document.relativePath ||
49
+ document.id ||
50
+ document.title ||
51
+ "unknown"
52
+ );
53
+ }
54
+
55
+ function extractDocumentText(document) {
56
+ const candidates = [
57
+ document.body_markdown,
58
+ document.markdown,
59
+ document.content,
60
+ document.summary,
61
+ document.excerpt,
62
+ document.text,
63
+ ];
64
+ for (const candidate of candidates) {
65
+ if (typeof candidate === "string" && candidate.trim()) {
66
+ return candidate;
67
+ }
68
+ }
69
+ return "";
70
+ }
71
+
72
+ function buildSources(index, sourceFilters, maxSources) {
73
+ const documents = Array.isArray(index.documents) ? index.documents : [];
74
+ const filters = (sourceFilters || []).map((item) => item.trim()).filter(Boolean);
75
+ const limit = Number.isInteger(maxSources) && maxSources > 0 ? maxSources : DEFAULT_MAX_SOURCES;
76
+
77
+ return documents
78
+ .map((document) => {
79
+ const sourcePath = extractDocumentPath(document);
80
+ return {
81
+ id: document.id || sourcePath,
82
+ path: sourcePath,
83
+ title: document.title || sourcePath,
84
+ text: extractDocumentText(document),
85
+ };
86
+ })
87
+ .filter((source) => {
88
+ if (filters.length === 0) {
89
+ return true;
90
+ }
91
+ return filters.some((filter) => source.id === filter || source.path === filter);
92
+ })
93
+ .slice(0, limit);
94
+ }
95
+
96
+ function normalizeCitations(citations) {
97
+ if (!Array.isArray(citations)) {
98
+ return [];
99
+ }
100
+ return citations
101
+ .map((citation) => {
102
+ if (typeof citation === "string") {
103
+ return citation;
104
+ }
105
+ if (citation && typeof citation === "object") {
106
+ return citation.path || citation.sourcePath || citation.id || citation.title || "";
107
+ }
108
+ return "";
109
+ })
110
+ .filter(Boolean);
111
+ }
112
+
113
+ function normalizeAskResult(rawResult, context) {
114
+ const result = rawResult && typeof rawResult === "object" ? rawResult : {};
115
+ const answer =
116
+ typeof result.answer === "string"
117
+ ? result.answer
118
+ : typeof result.answer_markdown === "string"
119
+ ? result.answer_markdown
120
+ : "";
121
+
122
+ return {
123
+ capability: "wiki.ask",
124
+ question: context.question,
125
+ answer,
126
+ citations: normalizeCitations(result.citations),
127
+ confidence: result.confidence,
128
+ graphPath: context.graphPath,
129
+ };
130
+ }
131
+
132
+ async function runWikiAsk(options) {
133
+ const projectPath = resolveProjectPath(options.projectPath);
134
+ const question = String(options.question || "").trim();
135
+ if (!question) {
136
+ throw new CliError('Missing required question. Use "sdtk-wiki ask --question <text>".', 1);
137
+ }
138
+
139
+ const graphInfo = assertWikiGraphReady(projectPath);
140
+ const index = readJsonFile(graphInfo.indexPath, INDEX_FILE);
141
+ const graph = readJsonFile(graphInfo.graphFilePath, GRAPH_FILE);
142
+ const sources = buildSources(index, options.sources, options.maxSources);
143
+
144
+ const handlerState = await loadWikiAskHandler();
145
+ if (!handlerState.ok) {
146
+ throw new CliError(handlerState.message, handlerState.exitCode);
147
+ }
148
+
149
+ const context = {
150
+ capability: "wiki.ask",
151
+ question,
152
+ projectPath,
153
+ graphPath: graphInfo.graphPath,
154
+ indexPath: graphInfo.indexPath,
155
+ graphFilePath: graphInfo.graphFilePath,
156
+ graph,
157
+ sources,
158
+ maxSources: options.maxSources,
159
+ };
160
+
161
+ let rawResult;
162
+ try {
163
+ rawResult = await handlerState.handler(context);
164
+ } catch (err) {
165
+ throw new CliError(`SDTK-WIKI Ask runtime failed: ${err.message}`, 4);
166
+ }
167
+
168
+ return normalizeAskResult(rawResult, context);
169
+ }
170
+
171
+ module.exports = {
172
+ buildSources,
173
+ normalizeAskResult,
174
+ runWikiAsk,
175
+ };
@@ -0,0 +1,287 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { CliError, ValidationError } = require("./errors");
6
+ const {
7
+ assertWikiWorkspaceWritePath,
8
+ getWikiReportsPath,
9
+ getWikiWorkspacePath,
10
+ isPathInsideOrEqual,
11
+ resolveProjectPath,
12
+ } = require("./wiki-paths");
13
+
14
+ const REPORT_PREFIX = "compile-dry-run-preview";
15
+ const ALLOWED_OPERATION_TYPES = new Set([
16
+ "append_section",
17
+ "create_page",
18
+ "add_relation",
19
+ "add_source_ref",
20
+ ]);
21
+
22
+ function todayStamp(date = new Date()) {
23
+ return date.toISOString().slice(0, 10);
24
+ }
25
+
26
+ function asArray(value) {
27
+ return Array.isArray(value) ? value : [];
28
+ }
29
+
30
+ function isRemoteUrl(value) {
31
+ return /^(?:https?|ftp):\/\//i.test(String(value || ""));
32
+ }
33
+
34
+ function resolvePlanPath(planArg, projectPath) {
35
+ if (!planArg) {
36
+ throw new ValidationError("sdtk-wiki wiki compile requires --plan <path>. No project files were changed.");
37
+ }
38
+ if (isRemoteUrl(planArg)) {
39
+ throw new ValidationError("Remote URL plan inputs are not supported for compile dry-run preview. No project files were changed.");
40
+ }
41
+
42
+ const candidate = path.isAbsolute(planArg) ? path.resolve(planArg) : path.resolve(projectPath, planArg);
43
+ if (!isPathInsideOrEqual(candidate, projectPath)) {
44
+ throw new ValidationError(`--plan must resolve inside the project root: ${candidate}. No project files were changed.`);
45
+ }
46
+ if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
47
+ throw new ValidationError(`--plan is not a valid file: ${candidate}. No project files were changed.`);
48
+ }
49
+ return candidate;
50
+ }
51
+
52
+ function ensureExistingWorkspace(projectPath) {
53
+ const workspacePath = getWikiWorkspacePath(projectPath);
54
+ if (!fs.existsSync(workspacePath) || !fs.statSync(workspacePath).isDirectory()) {
55
+ throw new ValidationError(
56
+ `No SDTK-WIKI workspace found at ${workspacePath}. Run "sdtk-wiki init" or "sdtk-wiki atlas build" first. No project files were changed.`
57
+ );
58
+ }
59
+ return workspacePath;
60
+ }
61
+
62
+ function parseJsonPlan(planPath) {
63
+ let payload;
64
+ try {
65
+ payload = JSON.parse(fs.readFileSync(planPath, "utf-8"));
66
+ } catch (error) {
67
+ throw new ValidationError(`Invalid JSON compile plan: ${error.message}. No project files were changed.`);
68
+ }
69
+ if (!payload || !Array.isArray(payload.operations)) {
70
+ throw new ValidationError("Invalid compile plan: JSON plan must contain an operations array. No project files were changed.");
71
+ }
72
+ return payload.operations;
73
+ }
74
+
75
+ function parseMarkdownPlan(planPath) {
76
+ const text = fs.readFileSync(planPath, "utf-8");
77
+ const lines = text.split(/\r?\n/);
78
+ const operations = [];
79
+ let current = null;
80
+
81
+ function flush() {
82
+ if (current && Object.keys(current).length > 1) {
83
+ operations.push(current);
84
+ }
85
+ current = null;
86
+ }
87
+
88
+ for (const line of lines) {
89
+ const heading = line.match(/^###\s+(.+?)\s*$/);
90
+ if (heading) {
91
+ flush();
92
+ current = { operation_id: heading[1].trim() };
93
+ continue;
94
+ }
95
+ const bullet = line.match(/^\s*-\s*([A-Za-z0-9_]+):\s*(.+?)\s*$/);
96
+ if (bullet && current) {
97
+ current[bullet[1]] = bullet[2];
98
+ }
99
+ }
100
+ flush();
101
+
102
+ if (operations.length === 0 || operations.every((operation) => !operation.operation_type)) {
103
+ throw new ValidationError(
104
+ "Invalid markdown compile plan: expected structured operation blocks with operation_type. No project files were changed."
105
+ );
106
+ }
107
+ return operations;
108
+ }
109
+
110
+ function parsePlanOperations(planPath) {
111
+ const ext = path.extname(planPath).toLowerCase();
112
+ if (ext === ".json") {
113
+ return parseJsonPlan(planPath);
114
+ }
115
+ if (ext === ".md" || ext === ".markdown") {
116
+ return parseMarkdownPlan(planPath);
117
+ }
118
+ throw new ValidationError(`Unsupported plan format: ${ext || "(none)"}. Use .md, .markdown, or .json. No project files were changed.`);
119
+ }
120
+
121
+ function splitRefs(value) {
122
+ if (Array.isArray(value)) return value.map(String).filter(Boolean);
123
+ return String(value || "")
124
+ .split(",")
125
+ .map((item) => item.trim())
126
+ .filter(Boolean);
127
+ }
128
+
129
+ function expectedMutationFor(operation) {
130
+ switch (operation.operation_type) {
131
+ case "append_section":
132
+ return `Would append an evidence-backed section to ${operation.target_page_path || operation.target_page_id || "(target missing)"}.`;
133
+ case "create_page":
134
+ return `Would create managed page ${operation.target_page_path || operation.target_page_id || "(target missing)"}.`;
135
+ case "add_relation":
136
+ return `Would add a related-page relation for ${operation.target_page_path || operation.target_page_id || "(target missing)"}.`;
137
+ case "add_source_ref":
138
+ return `Would add source reference metadata for ${operation.target_page_path || operation.target_page_id || "(target missing)"}.`;
139
+ default:
140
+ return "No mutation would be allowed; operation type is unsupported in BK-126.";
141
+ }
142
+ }
143
+
144
+ function normalizeOperation(rawOperation, index) {
145
+ const operationType = String(rawOperation.operation_type || rawOperation.operationType || "").trim();
146
+ const isSupported = ALLOWED_OPERATION_TYPES.has(operationType);
147
+ const operation = {
148
+ operation_id: String(rawOperation.operation_id || rawOperation.operationId || `operation-${String(index + 1).padStart(3, "0")}`),
149
+ source_id: String(rawOperation.source_id || rawOperation.sourceId || "unknown_source"),
150
+ source_hash: String(rawOperation.source_hash || rawOperation.sourceHash || ""),
151
+ revision: String(rawOperation.revision || ""),
152
+ target_page_id: String(rawOperation.target_page_id || rawOperation.targetPageId || ""),
153
+ target_page_path: String(rawOperation.target_page_path || rawOperation.targetPagePath || ""),
154
+ operation_type: operationType || "unsupported_operation",
155
+ requested_operation_type: operationType || "(missing)",
156
+ proposed_content_summary: String(rawOperation.proposed_content_summary || rawOperation.proposedContentSummary || ""),
157
+ evidence_refs: splitRefs(rawOperation.evidence_refs || rawOperation.evidenceRefs),
158
+ confidence: String(rawOperation.confidence || "unknown"),
159
+ review_status: String(rawOperation.review_status || rawOperation.reviewStatus || "needs_review"),
160
+ validation_status: isSupported ? "supported" : "unsupported_operation",
161
+ };
162
+ operation.expected_mutation_if_applied = expectedMutationFor(operation);
163
+ if (!isSupported) {
164
+ operation.operation_type = "unsupported_operation";
165
+ operation.review_status = "blocked";
166
+ }
167
+ return operation;
168
+ }
169
+
170
+ function summarizeOperations(operations) {
171
+ const counts = {};
172
+ for (const operation of operations) {
173
+ counts[operation.operation_type] = (counts[operation.operation_type] || 0) + 1;
174
+ }
175
+ return counts;
176
+ }
177
+
178
+ function renderReport({ projectPath, workspacePath, planPath, generatedAt, operations }) {
179
+ const summary = summarizeOperations(operations);
180
+ const sortedTypes = Array.from(new Set([...Array.from(ALLOWED_OPERATION_TYPES), "unsupported_operation"]));
181
+ const unsupported = operations.filter((operation) => operation.validation_status === "unsupported_operation");
182
+ const lines = [
183
+ "# SDTK-WIKI Compile Dry-Run Preview",
184
+ "",
185
+ `Generated: ${generatedAt}`,
186
+ `Project path: ${projectPath}`,
187
+ `Workspace path: ${workspacePath}`,
188
+ `Input plan path: ${planPath}`,
189
+ `Operations: ${operations.length}`,
190
+ "",
191
+ "No wiki pages, raw sources, provenance, or atlas compatibility files were modified.",
192
+ "Preview-only: apply, page mutation, raw mutation, provenance mutation, discover, prune, query-history, and release work are outside BK-126.",
193
+ "",
194
+ "## Operation Counts",
195
+ "",
196
+ "| Operation type | Count |",
197
+ "|---|---:|",
198
+ ...sortedTypes.map((type) => `| \`${type}\` | ${summary[type] || 0} |`),
199
+ "",
200
+ "## Proposed Operations",
201
+ "",
202
+ ];
203
+
204
+ if (operations.length === 0) {
205
+ lines.push("No operations were proposed by the input plan.");
206
+ } else {
207
+ for (const operation of operations) {
208
+ lines.push(`### ${operation.operation_id}`);
209
+ lines.push("");
210
+ lines.push(`- operation_type: \`${operation.operation_type}\``);
211
+ if (operation.operation_type === "unsupported_operation") {
212
+ lines.push(`- requested_operation_type: \`${operation.requested_operation_type}\``);
213
+ }
214
+ lines.push(`- Validation status: ${operation.validation_status}`);
215
+ lines.push(`- source_id: ${operation.source_id}`);
216
+ lines.push(`- source_hash_or_revision: ${operation.source_hash || operation.revision || "(missing)"}`);
217
+ lines.push(`- target_page_id: ${operation.target_page_id || "(missing)"}`);
218
+ lines.push(`- target_page_path: ${operation.target_page_path || "(missing)"}`);
219
+ lines.push(`- proposed_content_summary: ${operation.proposed_content_summary || "(missing)"}`);
220
+ lines.push(`- evidence_refs: ${operation.evidence_refs.length > 0 ? operation.evidence_refs.join(", ") : "(missing)"}`);
221
+ lines.push(`- confidence: ${operation.confidence}`);
222
+ lines.push(`- review_status: ${operation.review_status}`);
223
+ lines.push(`- Expected mutation if applied: ${operation.expected_mutation_if_applied}`);
224
+ lines.push("");
225
+ }
226
+ }
227
+
228
+ lines.push("## Unsupported / Blocked Operations");
229
+ lines.push("");
230
+ if (unsupported.length === 0) {
231
+ lines.push("- None.");
232
+ } else {
233
+ for (const operation of unsupported) {
234
+ lines.push(`- ${operation.operation_id}: unsupported_operation for requested type \`${operation.requested_operation_type}\`.`);
235
+ }
236
+ }
237
+ lines.push("");
238
+ return `${lines.join("\n").trimEnd()}\n`;
239
+ }
240
+
241
+ function runWikiCompileDryRun({ projectPath, planArg }) {
242
+ const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
243
+ if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
244
+ throw new ValidationError(`--project-path is not a valid directory: ${resolvedProjectPath}`);
245
+ }
246
+
247
+ try {
248
+ const workspacePath = ensureExistingWorkspace(resolvedProjectPath);
249
+ const planPath = resolvePlanPath(planArg, resolvedProjectPath);
250
+ const rawOperations = parsePlanOperations(planPath);
251
+ const operations = rawOperations.map((operation, index) => normalizeOperation(operation, index));
252
+ const reportsPath = getWikiReportsPath(resolvedProjectPath);
253
+ assertWikiWorkspaceWritePath(reportsPath, resolvedProjectPath);
254
+ const generatedAt = new Date().toISOString();
255
+ const reportPath = path.join(reportsPath, `${REPORT_PREFIX}-${todayStamp(new Date(generatedAt))}.md`);
256
+ assertWikiWorkspaceWritePath(reportPath, resolvedProjectPath);
257
+ fs.mkdirSync(reportsPath, { recursive: true });
258
+ fs.writeFileSync(
259
+ reportPath,
260
+ renderReport({
261
+ projectPath: resolvedProjectPath,
262
+ workspacePath,
263
+ planPath,
264
+ generatedAt,
265
+ operations,
266
+ }),
267
+ "utf-8"
268
+ );
269
+
270
+ return {
271
+ reportPath,
272
+ operations,
273
+ summary: summarizeOperations(operations),
274
+ unsupportedCount: operations.filter((operation) => operation.validation_status === "unsupported_operation").length,
275
+ };
276
+ } catch (error) {
277
+ if (error instanceof CliError) throw error;
278
+ throw new CliError(`Failed to write SDTK-WIKI compile dry-run preview: ${error.message}`);
279
+ }
280
+ }
281
+
282
+ module.exports = {
283
+ ALLOWED_OPERATION_TYPES,
284
+ REPORT_PREFIX,
285
+ renderReport,
286
+ runWikiCompileDryRun,
287
+ };
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { ValidationError } = require("./errors");
6
+ const {
7
+ getLegacyAtlasPath,
8
+ getWikiGraphPath,
9
+ getWikiWorkspacePath,
10
+ isPathInsideOrEqual,
11
+ } = require("./wiki-paths");
12
+
13
+ const CONFIG_SCHEMA_VERSION = 1;
14
+ const DEFAULT_PORT = 8765;
15
+ const DEFAULT_HOST = "127.0.0.1";
16
+
17
+ const DEFAULT_EXCLUDES = [
18
+ ".git",
19
+ ".sdtk/wiki",
20
+ ".sdtk/atlas",
21
+ "node_modules",
22
+ ".venv",
23
+ "venv",
24
+ "dist",
25
+ "build",
26
+ "coverage",
27
+ ".next",
28
+ ".turbo",
29
+ ".cache",
30
+ "__pycache__",
31
+ ];
32
+
33
+ function resolveWikiConfig(flags = {}) {
34
+ const projectPath = flags["project-path"]
35
+ ? path.resolve(flags["project-path"])
36
+ : process.cwd();
37
+
38
+ if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) {
39
+ throw new ValidationError(`--project-path is not a valid directory: ${projectPath}`);
40
+ }
41
+
42
+ const defaultOutputDir = getWikiGraphPath(projectPath);
43
+ const outputDir = flags["output-dir"]
44
+ ? path.resolve(flags["output-dir"])
45
+ : defaultOutputDir;
46
+ const workspaceRoot = getWikiWorkspacePath(projectPath);
47
+ if (!isPathInsideOrEqual(outputDir, workspaceRoot)) {
48
+ throw new ValidationError(
49
+ `--output-dir must stay under project-local .sdtk/wiki workspace: ${outputDir}`
50
+ );
51
+ }
52
+ const configPath = path.join(outputDir, "config.json");
53
+ const legacyAtlasDir = getLegacyAtlasPath(projectPath);
54
+
55
+ let persisted = {};
56
+ if (fs.existsSync(configPath)) {
57
+ try {
58
+ persisted = JSON.parse(fs.readFileSync(configPath, "utf-8"));
59
+ } catch (_) {
60
+ persisted = {};
61
+ }
62
+ }
63
+
64
+ let scanRoots;
65
+ if (flags["scan-root"] && flags["scan-root"].length > 0) {
66
+ scanRoots = flags["scan-root"].map((r) => path.resolve(r));
67
+ } else if (
68
+ persisted.scanRoots &&
69
+ Array.isArray(persisted.scanRoots) &&
70
+ persisted.scanRoots.length > 0
71
+ ) {
72
+ scanRoots = persisted.scanRoots.map((r) => path.resolve(r));
73
+ } else {
74
+ scanRoots = [projectPath];
75
+ }
76
+
77
+ const excludes =
78
+ persisted.excludes && Array.isArray(persisted.excludes)
79
+ ? persisted.excludes
80
+ : DEFAULT_EXCLUDES.slice();
81
+
82
+ const host = flags.host || persisted.host || DEFAULT_HOST;
83
+ const port = flags.port
84
+ ? parseInt(flags.port, 10)
85
+ : persisted.port || DEFAULT_PORT;
86
+
87
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
88
+ throw new ValidationError(`Invalid --port value: ${flags.port}`);
89
+ }
90
+
91
+ return {
92
+ projectPath,
93
+ outputDir,
94
+ configPath,
95
+ legacyAtlasDir,
96
+ scanRoots,
97
+ excludes,
98
+ host,
99
+ port,
100
+ verbose: !!flags.verbose,
101
+ };
102
+ }
103
+
104
+ function writeWikiConfig(config) {
105
+ fs.mkdirSync(config.outputDir, { recursive: true });
106
+
107
+ const payload = {
108
+ schemaVersion: CONFIG_SCHEMA_VERSION,
109
+ product: "SDTK-WIKI",
110
+ projectPath: config.projectPath,
111
+ outputDir: config.outputDir,
112
+ legacyAtlasDir: config.legacyAtlasDir,
113
+ scanRoots: config.scanRoots,
114
+ excludes: config.excludes,
115
+ host: config.host,
116
+ port: config.port,
117
+ };
118
+
119
+ fs.writeFileSync(config.configPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
120
+ }
121
+
122
+ function isWikiInitialized(outputDir) {
123
+ return fs.existsSync(path.join(outputDir, "config.json"));
124
+ }
125
+
126
+ function isWikiBuilt(outputDir) {
127
+ return (
128
+ fs.existsSync(path.join(outputDir, "SDTK_DOC_INDEX.json")) &&
129
+ fs.existsSync(path.join(outputDir, "viewer.html"))
130
+ );
131
+ }
132
+
133
+ function isLegacyAtlasBuilt(projectPath) {
134
+ const legacyDir = getLegacyAtlasPath(projectPath);
135
+ return isWikiBuilt(legacyDir);
136
+ }
137
+
138
+ function readBuildMeta(outputDir) {
139
+ const indexPath = path.join(outputDir, "SDTK_DOC_INDEX.json");
140
+ if (!fs.existsSync(indexPath)) return null;
141
+ try {
142
+ const data = JSON.parse(fs.readFileSync(indexPath, "utf-8"));
143
+ return { generated: data.generated || null, count: data.count || 0 };
144
+ } catch (_) {
145
+ return null;
146
+ }
147
+ }
148
+
149
+ function readGraphMeta(outputDir) {
150
+ const graphPath = path.join(outputDir, "SDTK_DOC_GRAPH.json");
151
+ if (!fs.existsSync(graphPath)) return null;
152
+ try {
153
+ const data = JSON.parse(fs.readFileSync(graphPath, "utf-8"));
154
+ return {
155
+ nodeCount: data.node_count || 0,
156
+ edgeCount: data.edge_count || 0,
157
+ };
158
+ } catch (_) {
159
+ return null;
160
+ }
161
+ }
162
+
163
+ function resolveBuilderPath() {
164
+ return path.resolve(__dirname, "..", "..", "assets", "atlas", "build_atlas.py");
165
+ }
166
+
167
+ module.exports = {
168
+ CONFIG_SCHEMA_VERSION,
169
+ DEFAULT_EXCLUDES,
170
+ DEFAULT_HOST,
171
+ DEFAULT_PORT,
172
+ isLegacyAtlasBuilt,
173
+ isWikiBuilt,
174
+ isWikiInitialized,
175
+ readBuildMeta,
176
+ readGraphMeta,
177
+ resolveBuilderPath,
178
+ resolveWikiConfig,
179
+ writeWikiConfig,
180
+ };