@biaoo/tiangong-wiki 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.
Files changed (136) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/README.zh-CN.md +167 -0
  4. package/SKILL.md +116 -0
  5. package/agents/openai.yaml +4 -0
  6. package/assets/config.example.env +18 -0
  7. package/assets/templates/achievement.md +32 -0
  8. package/assets/templates/bridge.md +33 -0
  9. package/assets/templates/concept.md +47 -0
  10. package/assets/templates/faq.md +31 -0
  11. package/assets/templates/lesson.md +31 -0
  12. package/assets/templates/method.md +31 -0
  13. package/assets/templates/misconception.md +35 -0
  14. package/assets/templates/person.md +31 -0
  15. package/assets/templates/research-note.md +34 -0
  16. package/assets/templates/resume.md +34 -0
  17. package/assets/templates/source-summary.md +35 -0
  18. package/assets/vllm/qwen3_5_openai_developer.jinja +182 -0
  19. package/assets/wiki.config.default.json +193 -0
  20. package/dist/commands/check-config.js +77 -0
  21. package/dist/commands/create.js +32 -0
  22. package/dist/commands/daemon.js +186 -0
  23. package/dist/commands/dashboard.js +112 -0
  24. package/dist/commands/doctor.js +22 -0
  25. package/dist/commands/export-graph.js +28 -0
  26. package/dist/commands/export-index.js +31 -0
  27. package/dist/commands/find.js +36 -0
  28. package/dist/commands/fts.js +32 -0
  29. package/dist/commands/graph.js +35 -0
  30. package/dist/commands/init.js +48 -0
  31. package/dist/commands/lint.js +35 -0
  32. package/dist/commands/list.js +28 -0
  33. package/dist/commands/page-info.js +24 -0
  34. package/dist/commands/search.js +32 -0
  35. package/dist/commands/setup.js +15 -0
  36. package/dist/commands/stat.js +20 -0
  37. package/dist/commands/sync.js +38 -0
  38. package/dist/commands/template.js +71 -0
  39. package/dist/commands/type.js +88 -0
  40. package/dist/commands/vault.js +64 -0
  41. package/dist/core/agent.js +201 -0
  42. package/dist/core/cli-env.js +129 -0
  43. package/dist/core/codex-workflow.js +233 -0
  44. package/dist/core/config.js +126 -0
  45. package/dist/core/db.js +292 -0
  46. package/dist/core/embedding.js +104 -0
  47. package/dist/core/frontmatter.js +287 -0
  48. package/dist/core/indexer.js +241 -0
  49. package/dist/core/onboarding.js +967 -0
  50. package/dist/core/page-files.js +91 -0
  51. package/dist/core/paths.js +161 -0
  52. package/dist/core/presenters.js +23 -0
  53. package/dist/core/query.js +58 -0
  54. package/dist/core/runtime.js +20 -0
  55. package/dist/core/sync.js +235 -0
  56. package/dist/core/synology.js +412 -0
  57. package/dist/core/template-evolution.js +38 -0
  58. package/dist/core/vault-processing.js +742 -0
  59. package/dist/core/vault.js +594 -0
  60. package/dist/core/workflow-context.js +188 -0
  61. package/dist/core/workflow-result.js +162 -0
  62. package/dist/core/workspace-bootstrap.js +30 -0
  63. package/dist/core/workspace-skills.js +220 -0
  64. package/dist/daemon/client.js +147 -0
  65. package/dist/daemon/server.js +807 -0
  66. package/dist/daemon/state.js +53 -0
  67. package/dist/dashboard/assets/index-1FgAUZ28.css +1 -0
  68. package/dist/dashboard/assets/index-6A0PWT4X.js +154 -0
  69. package/dist/dashboard/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  70. package/dist/dashboard/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  71. package/dist/dashboard/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  72. package/dist/dashboard/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  73. package/dist/dashboard/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
  74. package/dist/dashboard/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
  75. package/dist/dashboard/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  76. package/dist/dashboard/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  77. package/dist/dashboard/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  78. package/dist/dashboard/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  79. package/dist/dashboard/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
  80. package/dist/dashboard/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
  81. package/dist/dashboard/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  82. package/dist/dashboard/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  83. package/dist/dashboard/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  84. package/dist/dashboard/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  85. package/dist/dashboard/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
  86. package/dist/dashboard/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
  87. package/dist/dashboard/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  88. package/dist/dashboard/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  89. package/dist/dashboard/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  90. package/dist/dashboard/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  91. package/dist/dashboard/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
  92. package/dist/dashboard/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
  93. package/dist/dashboard/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  94. package/dist/dashboard/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  95. package/dist/dashboard/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
  96. package/dist/dashboard/assets/space-grotesk-latin-400-normal-BnQMeOim.woff +0 -0
  97. package/dist/dashboard/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
  98. package/dist/dashboard/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
  99. package/dist/dashboard/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
  100. package/dist/dashboard/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
  101. package/dist/dashboard/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
  102. package/dist/dashboard/assets/space-grotesk-latin-ext-400-normal-CfP_5XZW.woff2 +0 -0
  103. package/dist/dashboard/assets/space-grotesk-latin-ext-400-normal-DRPE3kg4.woff +0 -0
  104. package/dist/dashboard/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
  105. package/dist/dashboard/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
  106. package/dist/dashboard/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
  107. package/dist/dashboard/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
  108. package/dist/dashboard/assets/space-grotesk-vietnamese-400-normal-B7xT_GF5.woff2 +0 -0
  109. package/dist/dashboard/assets/space-grotesk-vietnamese-400-normal-BIWiOVfw.woff +0 -0
  110. package/dist/dashboard/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
  111. package/dist/dashboard/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
  112. package/dist/dashboard/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
  113. package/dist/dashboard/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
  114. package/dist/dashboard/index.html +18 -0
  115. package/dist/index.js +86 -0
  116. package/dist/operations/dashboard.js +1231 -0
  117. package/dist/operations/export.js +110 -0
  118. package/dist/operations/query.js +649 -0
  119. package/dist/operations/type-template.js +210 -0
  120. package/dist/operations/write.js +143 -0
  121. package/dist/types/config.js +1 -0
  122. package/dist/types/page.js +1 -0
  123. package/dist/utils/case.js +22 -0
  124. package/dist/utils/errors.js +26 -0
  125. package/dist/utils/fs.js +77 -0
  126. package/dist/utils/output.js +33 -0
  127. package/dist/utils/process.js +60 -0
  128. package/dist/utils/segmenter.js +24 -0
  129. package/dist/utils/slug.js +10 -0
  130. package/dist/utils/time.js +24 -0
  131. package/package.json +64 -0
  132. package/references/cli-interface.md +312 -0
  133. package/references/env.md +122 -0
  134. package/references/template-design-guide.md +271 -0
  135. package/references/vault-to-wiki-instruction.md +110 -0
  136. package/references/wiki-maintenance-instruction.md +190 -0
@@ -0,0 +1,649 @@
1
+ import path from "node:path";
2
+ import { getMeta } from "../core/db.js";
3
+ import { parsePage } from "../core/frontmatter.js";
4
+ import { normalizePageId, resolvePagePath } from "../core/paths.js";
5
+ import { compactPageSummary } from "../core/presenters.js";
6
+ import { listPageColumns, mapPageRow, selectPageById } from "../core/query.js";
7
+ import { openRuntimeDb } from "../core/runtime.js";
8
+ import { readAllPages } from "../core/sync.js";
9
+ import { getVaultQueueSnapshot } from "../core/vault-processing.js";
10
+ import { camelToSnake } from "../utils/case.js";
11
+ import { AppError } from "../utils/errors.js";
12
+ import { listFilesRecursiveSync } from "../utils/fs.js";
13
+ import { normalizeFtsQuery } from "../utils/segmenter.js";
14
+ function parsePositiveLimit(value, label, fallback) {
15
+ const normalized = value ?? fallback;
16
+ const limit = Number.parseInt(String(normalized), 10);
17
+ if (!Number.isFinite(limit) || limit <= 0) {
18
+ throw new AppError(`Invalid ${label} value: ${value}`, "config");
19
+ }
20
+ return limit;
21
+ }
22
+ function distanceToSimilarity(distance) {
23
+ return 1 / (1 + distance);
24
+ }
25
+ function resolveListSortColumn(sort, config) {
26
+ const sortColumn = camelToSnake(sort ?? "updatedAt");
27
+ const allowedSortColumns = new Set(["updated_at", "created_at", "title", "page_type", ...config.allColumnNames]);
28
+ if (!allowedSortColumns.has(sortColumn)) {
29
+ throw new AppError(`Unsupported --sort column: ${sort}`, "config");
30
+ }
31
+ return sortColumn;
32
+ }
33
+ function resolveFindSortColumn(sort, config) {
34
+ if (!sort) {
35
+ return "updated_at";
36
+ }
37
+ const normalized = camelToSnake(sort.replace(/-/g, "_"));
38
+ const allowed = new Set([
39
+ "id",
40
+ "node_id",
41
+ "title",
42
+ "page_type",
43
+ "status",
44
+ "visibility",
45
+ "updated_at",
46
+ "created_at",
47
+ ...config.allColumnNames,
48
+ ]);
49
+ if (!allowed.has(normalized)) {
50
+ throw new AppError(`Unsupported sort column: ${sort}`, "config");
51
+ }
52
+ return normalized;
53
+ }
54
+ function normalizeStringArray(value) {
55
+ if (Array.isArray(value)) {
56
+ return value.map((item) => String(item).trim()).filter(Boolean);
57
+ }
58
+ if (typeof value === "string" && value.trim()) {
59
+ return [value.trim()];
60
+ }
61
+ return [];
62
+ }
63
+ function normalizeOptionalString(value) {
64
+ if (typeof value !== "string") {
65
+ return null;
66
+ }
67
+ const normalized = value.trim();
68
+ return normalized ? normalized : null;
69
+ }
70
+ function isAbsoluteLikePath(value) {
71
+ return path.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value);
72
+ }
73
+ function olderThanSixMonths(value) {
74
+ if (!value) {
75
+ return false;
76
+ }
77
+ const updatedAt = new Date(value);
78
+ if (Number.isNaN(updatedAt.getTime())) {
79
+ return false;
80
+ }
81
+ const threshold = new Date();
82
+ threshold.setMonth(threshold.getMonth() - 6);
83
+ return updatedAt < threshold;
84
+ }
85
+ function addLintItem(target, page, check, message) {
86
+ target.push({ page, check, message });
87
+ }
88
+ function buildGraphQuery(direction, edgeType) {
89
+ if (direction === "outgoing") {
90
+ return `
91
+ WITH RECURSIVE walk(depth, node, source, target, edge_type, trail) AS (
92
+ SELECT
93
+ 1,
94
+ e.target,
95
+ e.source,
96
+ e.target,
97
+ e.edge_type,
98
+ '|' || ? || '|' || e.target || '|'
99
+ FROM edges e
100
+ WHERE e.source = ?
101
+ ${edgeType ? "AND e.edge_type = ?" : ""}
102
+ UNION ALL
103
+ SELECT
104
+ walk.depth + 1,
105
+ e.target,
106
+ e.source,
107
+ e.target,
108
+ e.edge_type,
109
+ walk.trail || e.target || '|'
110
+ FROM walk
111
+ JOIN edges e ON e.source = walk.node
112
+ WHERE walk.depth < ?
113
+ ${edgeType ? "AND e.edge_type = ?" : ""}
114
+ AND instr(walk.trail, '|' || e.target || '|') = 0
115
+ )
116
+ SELECT DISTINCT source, target, edge_type AS edgeType FROM walk
117
+ `;
118
+ }
119
+ if (direction === "incoming") {
120
+ return `
121
+ WITH RECURSIVE walk(depth, node, source, target, edge_type, trail) AS (
122
+ SELECT
123
+ 1,
124
+ e.source,
125
+ e.source,
126
+ e.target,
127
+ e.edge_type,
128
+ '|' || ? || '|' || e.source || '|'
129
+ FROM edges e
130
+ WHERE e.target = ?
131
+ ${edgeType ? "AND e.edge_type = ?" : ""}
132
+ UNION ALL
133
+ SELECT
134
+ walk.depth + 1,
135
+ e.source,
136
+ e.source,
137
+ e.target,
138
+ e.edge_type,
139
+ walk.trail || e.source || '|'
140
+ FROM walk
141
+ JOIN edges e ON e.target = walk.node
142
+ WHERE walk.depth < ?
143
+ ${edgeType ? "AND e.edge_type = ?" : ""}
144
+ AND instr(walk.trail, '|' || e.source || '|') = 0
145
+ )
146
+ SELECT DISTINCT source, target, edge_type AS edgeType FROM walk
147
+ `;
148
+ }
149
+ return `
150
+ WITH RECURSIVE walk(depth, node, source, target, edge_type, trail) AS (
151
+ SELECT
152
+ 1,
153
+ CASE WHEN e.source = ? THEN e.target ELSE e.source END,
154
+ e.source,
155
+ e.target,
156
+ e.edge_type,
157
+ '|' || ? || '|' || CASE WHEN e.source = ? THEN e.target ELSE e.source END || '|'
158
+ FROM edges e
159
+ WHERE (e.source = ? OR e.target = ?)
160
+ ${edgeType ? "AND e.edge_type = ?" : ""}
161
+ UNION ALL
162
+ SELECT
163
+ walk.depth + 1,
164
+ CASE WHEN e.source = walk.node THEN e.target ELSE e.source END,
165
+ e.source,
166
+ e.target,
167
+ e.edge_type,
168
+ walk.trail || CASE WHEN e.source = walk.node THEN e.target ELSE e.source END || '|'
169
+ FROM walk
170
+ JOIN edges e ON (e.source = walk.node OR e.target = walk.node)
171
+ WHERE walk.depth < ?
172
+ ${edgeType ? "AND e.edge_type = ?" : ""}
173
+ AND instr(
174
+ walk.trail,
175
+ '|' || CASE WHEN e.source = walk.node THEN e.target ELSE e.source END || '|'
176
+ ) = 0
177
+ )
178
+ SELECT DISTINCT source, target, edge_type AS edgeType FROM walk
179
+ `;
180
+ }
181
+ function normalizePageInfoId(input, wikiPath) {
182
+ if (input.endsWith(".md") || path.isAbsolute(input)) {
183
+ return normalizePageId(input, wikiPath);
184
+ }
185
+ return input;
186
+ }
187
+ export function listPages(env = process.env, options = {}) {
188
+ const { db, config } = openRuntimeDb(env);
189
+ try {
190
+ const limit = parsePositiveLimit(options.limit, "--limit", 50);
191
+ const sortColumn = resolveListSortColumn(options.sort, config);
192
+ const rows = db
193
+ .prepare(`
194
+ SELECT ${listPageColumns(config).join(", ")}
195
+ FROM pages
196
+ ${options.type ? "WHERE page_type = ?" : ""}
197
+ ORDER BY ${sortColumn} DESC, title ASC
198
+ LIMIT ?
199
+ `)
200
+ .all(...(options.type ? [options.type, limit] : [limit]));
201
+ return rows.map((row) => compactPageSummary(mapPageRow(row, config), config));
202
+ }
203
+ finally {
204
+ db.close();
205
+ }
206
+ }
207
+ export function findPages(env = process.env, options = {}) {
208
+ const { db, config: runtimeConfig } = openRuntimeDb(env);
209
+ try {
210
+ const dynamicFields = [
211
+ ...new Set([
212
+ ...Object.keys(runtimeConfig.customColumns),
213
+ ...Object.values(runtimeConfig.templates).flatMap((template) => Object.keys(template.columns)),
214
+ ]),
215
+ ];
216
+ const where = [];
217
+ const params = [];
218
+ if (options.type) {
219
+ where.push("page_type = ?");
220
+ params.push(options.type);
221
+ }
222
+ if (options.status) {
223
+ where.push("status = ?");
224
+ params.push(options.status);
225
+ }
226
+ if (options.visibility) {
227
+ where.push("visibility = ?");
228
+ params.push(options.visibility);
229
+ }
230
+ if (options.nodeId) {
231
+ where.push("node_id = ?");
232
+ params.push(options.nodeId);
233
+ }
234
+ if (options.updatedAfter) {
235
+ where.push("updated_at >= ?");
236
+ params.push(options.updatedAfter);
237
+ }
238
+ if (options.tag) {
239
+ where.push("EXISTS (SELECT 1 FROM json_each(pages.tags) WHERE json_each.value = ?)");
240
+ params.push(options.tag);
241
+ }
242
+ for (const field of dynamicFields) {
243
+ const value = options[field];
244
+ if (value !== undefined) {
245
+ where.push(`${camelToSnake(field)} = ?`);
246
+ params.push(value);
247
+ }
248
+ }
249
+ const limit = parsePositiveLimit(options.limit, "--limit", 50);
250
+ const sortColumn = resolveFindSortColumn(options.sort, runtimeConfig);
251
+ const query = `
252
+ SELECT ${listPageColumns(runtimeConfig).join(", ")}
253
+ FROM pages
254
+ ${where.length > 0 ? `WHERE ${where.join(" AND ")}` : ""}
255
+ ORDER BY ${sortColumn} DESC, title ASC
256
+ LIMIT ?
257
+ `;
258
+ const rows = db.prepare(query).all(...params, limit);
259
+ return rows.map((row) => compactPageSummary(mapPageRow(row, runtimeConfig), runtimeConfig));
260
+ }
261
+ finally {
262
+ db.close();
263
+ }
264
+ }
265
+ export async function searchPages(env = process.env, options) {
266
+ const { EmbeddingClient } = await import("../core/embedding.js");
267
+ const embeddingClient = EmbeddingClient.fromEnv(env);
268
+ if (!embeddingClient) {
269
+ throw new AppError("Embedding not configured", "not_configured");
270
+ }
271
+ const limit = parsePositiveLimit(options.limit, "--limit", 10);
272
+ const [queryEmbedding] = await embeddingClient.embedBatch([options.query]);
273
+ const { db, config } = openRuntimeDb(env);
274
+ try {
275
+ const rows = db
276
+ .prepare(`
277
+ SELECT ${listPageColumns(config).map((column) => `pages.${column}`).join(", ")}, vec_pages.distance AS distance
278
+ FROM vec_pages
279
+ JOIN pages ON pages.id = vec_pages.page_id
280
+ WHERE vec_pages.embedding MATCH ?
281
+ AND k = ?
282
+ ${options.type ? "AND pages.page_type = ?" : ""}
283
+ ORDER BY vec_pages.distance
284
+ LIMIT ?
285
+ `)
286
+ .all(...(options.type
287
+ ? [new Float32Array(queryEmbedding), limit, options.type, limit]
288
+ : [new Float32Array(queryEmbedding), limit, limit]));
289
+ return rows.map((row) => ({
290
+ ...compactPageSummary(mapPageRow(row, config), config),
291
+ summaryText: row.summary_text,
292
+ similarity: distanceToSimilarity(Number(row.distance)),
293
+ }));
294
+ }
295
+ finally {
296
+ db.close();
297
+ }
298
+ }
299
+ export function ftsSearchPages(env = process.env, options) {
300
+ const { db, config } = openRuntimeDb(env);
301
+ try {
302
+ const limit = parsePositiveLimit(options.limit, "--limit", 20);
303
+ const normalizedQuery = normalizeFtsQuery(options.query);
304
+ const rows = db
305
+ .prepare(`
306
+ SELECT ${listPageColumns(config).map((column) => `pages.${column}`).join(", ")}, bm25(pages_fts) AS rank
307
+ FROM pages_fts
308
+ JOIN pages ON pages.rowid = pages_fts.rowid
309
+ WHERE pages_fts MATCH ?
310
+ ${options.type ? "AND pages.page_type = ?" : ""}
311
+ ORDER BY rank
312
+ LIMIT ?
313
+ `)
314
+ .all(...(options.type ? [normalizedQuery, options.type, limit] : [normalizedQuery, limit]));
315
+ return rows.map((row) => ({
316
+ ...compactPageSummary(mapPageRow(row, config), config),
317
+ summaryText: row.summary_text,
318
+ rank: row.rank,
319
+ }));
320
+ }
321
+ finally {
322
+ db.close();
323
+ }
324
+ }
325
+ export function traverseGraph(env = process.env, options) {
326
+ const { db } = openRuntimeDb(env);
327
+ try {
328
+ const depth = Number.parseInt(String(options.depth ?? "1"), 10);
329
+ if (!Number.isFinite(depth) || depth < 1) {
330
+ throw new AppError(`Invalid --depth value: ${options.depth}`, "config");
331
+ }
332
+ const direction = (options.direction ?? "both");
333
+ if (!["outgoing", "incoming", "both"].includes(direction)) {
334
+ throw new AppError(`Invalid --direction value: ${options.direction}`, "config");
335
+ }
336
+ const rootRow = db
337
+ .prepare("SELECT id, node_id AS nodeId, title, page_type AS pageType FROM pages WHERE node_id = ? OR id = ? LIMIT 1")
338
+ .get(options.root, options.root);
339
+ const rootKey = rootRow?.nodeId ?? rootRow?.id ?? options.root;
340
+ const sql = buildGraphQuery(direction, options.edgeType);
341
+ let params;
342
+ if (direction === "both") {
343
+ params = options.edgeType
344
+ ? [rootKey, rootKey, rootKey, rootKey, rootKey, options.edgeType, depth, options.edgeType]
345
+ : [rootKey, rootKey, rootKey, rootKey, rootKey, depth];
346
+ }
347
+ else {
348
+ params = options.edgeType
349
+ ? [rootKey, rootKey, options.edgeType, depth, options.edgeType]
350
+ : [rootKey, rootKey, depth];
351
+ }
352
+ const edges = db.prepare(sql).all(...params);
353
+ const identifiers = [...new Set([rootKey, ...edges.flatMap((edge) => [edge.source, edge.target])])];
354
+ const lookupPage = db.prepare("SELECT id, node_id AS nodeId, title, page_type AS pageType, file_path AS filePath FROM pages WHERE node_id = ? OR id = ? LIMIT 1");
355
+ const nodes = identifiers.map((identifier) => {
356
+ const row = lookupPage.get(identifier, identifier);
357
+ if (!row) {
358
+ return { nodeId: identifier };
359
+ }
360
+ return {
361
+ id: row.id,
362
+ nodeId: row.nodeId ?? row.id,
363
+ title: row.title,
364
+ pageType: row.pageType,
365
+ filePath: row.filePath,
366
+ };
367
+ });
368
+ return { root: rootKey, nodes, edges };
369
+ }
370
+ finally {
371
+ db.close();
372
+ }
373
+ }
374
+ export function getPageInfo(env = process.env, inputPageId) {
375
+ const { db, config, paths } = openRuntimeDb(env);
376
+ try {
377
+ const pageId = normalizePageInfoId(inputPageId, paths.wikiPath);
378
+ const page = selectPageById(db, config, pageId);
379
+ if (!page) {
380
+ throw new AppError(`Page not found: ${pageId}`, "not_found");
381
+ }
382
+ const identifiers = [page.id, page.nodeId].filter(Boolean);
383
+ const outgoing = db
384
+ .prepare(`
385
+ SELECT source, target, edge_type AS edgeType, source_page AS sourcePage, metadata
386
+ FROM edges
387
+ WHERE source_page = ?
388
+ ORDER BY edge_type, target
389
+ `)
390
+ .all(page.id);
391
+ const incoming = db
392
+ .prepare(`
393
+ SELECT source, target, edge_type AS edgeType, source_page AS sourcePage, metadata
394
+ FROM edges
395
+ WHERE target IN (${identifiers.map(() => "?").join(", ")})
396
+ ORDER BY edge_type, source
397
+ `)
398
+ .all(...identifiers);
399
+ return {
400
+ ...page,
401
+ outgoingEdges: outgoing.map((edge) => ({
402
+ ...edge,
403
+ metadata: edge.metadata ? JSON.parse(String(edge.metadata)) : {},
404
+ })),
405
+ incomingEdges: incoming.map((edge) => ({
406
+ ...edge,
407
+ metadata: edge.metadata ? JSON.parse(String(edge.metadata)) : {},
408
+ })),
409
+ };
410
+ }
411
+ finally {
412
+ db.close();
413
+ }
414
+ }
415
+ export function getWikiStat(env = process.env) {
416
+ const { db, config } = openRuntimeDb(env);
417
+ try {
418
+ const pages = readAllPages(db);
419
+ const edges = db
420
+ .prepare("SELECT source, target, source_page AS sourcePage FROM edges")
421
+ .all();
422
+ const byType = {};
423
+ const byStatus = {};
424
+ const embeddingStatus = {};
425
+ for (const page of pages) {
426
+ byType[page.pageType] = (byType[page.pageType] ?? 0) + 1;
427
+ byStatus[page.status] = (byStatus[page.status] ?? 0) + 1;
428
+ embeddingStatus[page.embeddingStatus] = (embeddingStatus[page.embeddingStatus] ?? 0) + 1;
429
+ }
430
+ const orphanPages = pages.filter((page) => {
431
+ const identifiers = [page.id, page.nodeId].filter(Boolean);
432
+ const hasOutgoing = edges.some((edge) => edge.sourcePage === page.id);
433
+ const hasIncoming = edges.some((edge) => identifiers.includes(edge.target));
434
+ return !hasOutgoing && !hasIncoming;
435
+ }).length;
436
+ const vaultFiles = db.prepare("SELECT COUNT(*) AS count FROM vault_files").get();
437
+ return {
438
+ totalPages: pages.length,
439
+ byType,
440
+ byStatus,
441
+ totalEdges: edges.length,
442
+ orphanPages,
443
+ embeddingStatus,
444
+ vaultFiles: vaultFiles.count,
445
+ lastSyncAt: getMeta(db, "last_sync_at"),
446
+ registeredTemplates: Object.keys(config.templates).length,
447
+ };
448
+ }
449
+ finally {
450
+ db.close();
451
+ }
452
+ }
453
+ export function listVaultFiles(env = process.env, options = {}) {
454
+ const { db } = openRuntimeDb(env);
455
+ try {
456
+ const clauses = [];
457
+ const params = [];
458
+ if (options.path) {
459
+ clauses.push("id LIKE ?");
460
+ params.push(`${options.path}%`);
461
+ }
462
+ if (options.ext) {
463
+ clauses.push("file_ext = ?");
464
+ params.push(String(options.ext).replace(/^\./, ""));
465
+ }
466
+ return db
467
+ .prepare(`
468
+ SELECT
469
+ id,
470
+ file_name AS fileName,
471
+ file_ext AS fileExt,
472
+ source_type AS sourceType,
473
+ file_size AS fileSize,
474
+ file_path AS filePath,
475
+ content_hash AS contentHash,
476
+ file_mtime AS fileMtime,
477
+ indexed_at AS indexedAt
478
+ FROM vault_files
479
+ ${clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : ""}
480
+ ORDER BY id
481
+ `)
482
+ .all(...params);
483
+ }
484
+ finally {
485
+ db.close();
486
+ }
487
+ }
488
+ export function diffVaultFiles(env = process.env, options = {}) {
489
+ const { db } = openRuntimeDb(env);
490
+ try {
491
+ const clauses = [];
492
+ const params = [];
493
+ if (options.since) {
494
+ clauses.push("detected_at >= ?");
495
+ params.push(options.since);
496
+ }
497
+ else {
498
+ const lastSyncId = getMeta(db, "last_sync_id");
499
+ if (lastSyncId) {
500
+ clauses.push("sync_id = ?");
501
+ params.push(lastSyncId);
502
+ }
503
+ }
504
+ if (options.path) {
505
+ clauses.push("file_id LIKE ?");
506
+ params.push(`${options.path}%`);
507
+ }
508
+ const rows = db
509
+ .prepare(`
510
+ SELECT
511
+ file_id AS fileId,
512
+ action,
513
+ detected_at AS detectedAt,
514
+ sync_id AS syncId
515
+ FROM vault_changelog
516
+ ${clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : ""}
517
+ ORDER BY detected_at DESC, id DESC
518
+ `)
519
+ .all(...params);
520
+ return {
521
+ changes: rows,
522
+ since: options.since ?? null,
523
+ totalChanges: rows.length,
524
+ };
525
+ }
526
+ finally {
527
+ db.close();
528
+ }
529
+ }
530
+ export function getVaultQueue(env = process.env, options = {}) {
531
+ const status = normalizeQueueStatus(options.status);
532
+ return getVaultQueueSnapshot(env, status);
533
+ }
534
+ function normalizeQueueStatus(value) {
535
+ if (!value) {
536
+ return undefined;
537
+ }
538
+ if (value === "pending" || value === "processing" || value === "done" || value === "skipped" || value === "error") {
539
+ return value;
540
+ }
541
+ throw new AppError(`Unsupported queue status: ${value}`, "config");
542
+ }
543
+ export function runLint(env = process.env, options = {}) {
544
+ const level = options.level ?? "info";
545
+ const { db, config, paths } = openRuntimeDb(env);
546
+ try {
547
+ const pageFiles = options.path
548
+ ? [resolvePagePath(normalizePageId(options.path, paths.wikiPath), paths.wikiPath)]
549
+ : listFilesRecursiveSync(paths.wikiPath, ".md");
550
+ const indexedPages = readAllPages(db);
551
+ const pageIdSet = new Set(indexedPages.map((page) => page.id));
552
+ const nodeIdSet = new Set(indexedPages.map((page) => page.nodeId).filter(Boolean));
553
+ const archivedIds = new Set(indexedPages.filter((page) => page.status === "archived").map((page) => page.id));
554
+ const archivedNodeIds = new Set(indexedPages.filter((page) => page.status === "archived" && page.nodeId).map((page) => page.nodeId));
555
+ const vaultIds = new Set(db.prepare("SELECT id FROM vault_files").all().map((row) => row.id));
556
+ const edges = db
557
+ .prepare("SELECT source, target, source_page AS sourcePage FROM edges")
558
+ .all();
559
+ const result = { pages: pageFiles.length, errors: [], warnings: [], info: [] };
560
+ for (const filePath of pageFiles) {
561
+ const parsed = parsePage(filePath, paths.wikiPath, config);
562
+ const pageId = path.relative(paths.wikiPath, filePath).split(path.sep).join("/");
563
+ if (!parsed.ok) {
564
+ addLintItem(result.errors, pageId, parsed.error.code, parsed.error.message);
565
+ continue;
566
+ }
567
+ const { parsed: page } = parsed;
568
+ const sourceRefs = normalizeStringArray(page.rawData.sourceRefs);
569
+ const relatedPages = normalizeStringArray(page.rawData.relatedPages);
570
+ const vaultPath = normalizeOptionalString(page.rawData.vaultPath);
571
+ for (const reference of sourceRefs) {
572
+ if (reference.startsWith("vault/") && !vaultIds.has(reference.replace(/^vault\//, ""))) {
573
+ addLintItem(result.errors, page.page.id, "vault_ref_exists", `sourceRefs: ${reference} does not exist in vault`);
574
+ }
575
+ }
576
+ for (const edge of page.edges) {
577
+ const isPathTarget = edge.target.endsWith(".md");
578
+ if (isPathTarget && !pageIdSet.has(edge.target)) {
579
+ addLintItem(result.errors, page.page.id, "page_ref_exists", `${edge.edgeType}: ${edge.target} not found`);
580
+ }
581
+ if (!isPathTarget && !nodeIdSet.has(edge.target)) {
582
+ addLintItem(result.errors, page.page.id, "node_ref_exists", `${edge.edgeType}: ${edge.target} not found`);
583
+ }
584
+ if (isPathTarget && archivedIds.has(edge.target)) {
585
+ addLintItem(result.warnings, page.page.id, "archived_page_ref", `${edge.target} is archived`);
586
+ }
587
+ if (!isPathTarget && archivedNodeIds.has(edge.target)) {
588
+ addLintItem(result.warnings, page.page.id, "archived_node_ref", `${edge.target} is archived`);
589
+ }
590
+ }
591
+ if (sourceRefs.length === 0) {
592
+ addLintItem(result.warnings, page.page.id, "source_refs_empty", "sourceRefs is empty");
593
+ }
594
+ if (vaultPath && isAbsoluteLikePath(vaultPath)) {
595
+ addLintItem(result.errors, page.page.id, "vault_path_absolute", `vaultPath is an absolute path: "${vaultPath}", should be relative to vault root`);
596
+ }
597
+ if (page.page.pageType === "source-summary" && relatedPages.length === 0) {
598
+ addLintItem(result.warnings, page.page.id, "related_pages_empty", "relatedPages is empty for source-summary — page has no explicit knowledge connections");
599
+ }
600
+ if (page.page.status === "active" && olderThanSixMonths(page.page.updatedAt)) {
601
+ addLintItem(result.warnings, page.page.id, "stale_page", "updatedAt is older than six months");
602
+ }
603
+ const identifiers = [page.page.id, page.page.nodeId].filter(Boolean);
604
+ const hasOutgoing = page.edges.length > 0 || edges.some((edge) => edge.sourcePage === page.page.id);
605
+ const hasIncoming = edges.some((edge) => identifiers.includes(edge.target));
606
+ if (!hasOutgoing && !hasIncoming) {
607
+ addLintItem(result.warnings, page.page.id, "orphan_page", "No incoming or outgoing links");
608
+ }
609
+ if (page.unregisteredFields.length > 0) {
610
+ addLintItem(result.info, page.page.id, "unregistered_fields", `Unregistered fields: ${page.unregisteredFields.join(", ")}`);
611
+ }
612
+ }
613
+ const draftCount = indexedPages.filter((page) => page.status === "draft").length;
614
+ const pendingEmbeddings = indexedPages.filter((page) => page.embeddingStatus !== "done").length;
615
+ addLintItem(result.info, "*", "draft_count", `${draftCount} pages in draft status`);
616
+ addLintItem(result.info, "*", "embedding_pending", `${pendingEmbeddings} pages with pending embedding`);
617
+ return {
618
+ errors: result.errors,
619
+ warnings: level === "warning" || level === "info" ? result.warnings : [],
620
+ info: level === "info" ? result.info : [],
621
+ summary: {
622
+ pages: result.pages,
623
+ errors: result.errors.length,
624
+ warnings: level === "warning" || level === "info" ? result.warnings.length : 0,
625
+ info: level === "info" ? result.info.length : 0,
626
+ },
627
+ };
628
+ }
629
+ finally {
630
+ db.close();
631
+ }
632
+ }
633
+ export function renderLintResult(result) {
634
+ const lines = [`tiangong-wiki lint: ${result.summary.pages} pages checked`, ""];
635
+ const sections = [
636
+ { label: "ERROR", items: result.errors },
637
+ { label: "WARN", items: result.warnings },
638
+ { label: "INFO", items: result.info },
639
+ ];
640
+ for (const section of sections) {
641
+ for (const item of section.items) {
642
+ lines.push(` ${section.label.padEnd(5)} ${item.page}`);
643
+ lines.push(` ${item.message}`);
644
+ lines.push("");
645
+ }
646
+ }
647
+ lines.push(`Summary: ${result.summary.errors} errors, ${result.summary.warnings} warnings, ${result.summary.info} info`);
648
+ return lines.join("\n");
649
+ }