@doquflow/server 1.3.1 → 1.5.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.
- package/dist/tools/build-graph.js +111 -0
- package/dist/tools/list-wiki.js +83 -14
- package/package.json +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildGraph = buildGraph;
|
|
4
|
+
const list_wiki_1 = require("./list-wiki");
|
|
5
|
+
const SEP = "\u0000";
|
|
6
|
+
async function buildGraph(input) {
|
|
7
|
+
const generatedAt = new Date().toISOString();
|
|
8
|
+
try {
|
|
9
|
+
const wiki = await (0, list_wiki_1.listWiki)({ project_path: input.project_path });
|
|
10
|
+
if (wiki.error) {
|
|
11
|
+
return {
|
|
12
|
+
nodes: [],
|
|
13
|
+
edges: [],
|
|
14
|
+
meta: {
|
|
15
|
+
total_pages: 0, total_edges: 0, orphans: 0, isolated: 0,
|
|
16
|
+
dangling_refs: [], generated_at: generatedAt,
|
|
17
|
+
},
|
|
18
|
+
error: wiki.error,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const pages = wiki.pages;
|
|
22
|
+
const nodeIds = new Set(pages.map((p) => p.id));
|
|
23
|
+
const edgeMap = new Map();
|
|
24
|
+
const dangling = new Set();
|
|
25
|
+
// Pass 1: outbound
|
|
26
|
+
for (const page of pages) {
|
|
27
|
+
const src = page.id;
|
|
28
|
+
for (const tgt of page.outbound_links ?? []) {
|
|
29
|
+
if (tgt === src)
|
|
30
|
+
continue;
|
|
31
|
+
if (!nodeIds.has(tgt)) {
|
|
32
|
+
dangling.add(tgt);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const key = `${src}${SEP}${tgt}`;
|
|
36
|
+
if (!edgeMap.has(key)) {
|
|
37
|
+
edgeMap.set(key, { source: src, target: tgt, kind: "outbound" });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Pass 2: inbound — `n.inbound_links` lists pages that point AT n.
|
|
42
|
+
for (const page of pages) {
|
|
43
|
+
const tgt = page.id;
|
|
44
|
+
for (const src of page.inbound_links ?? []) {
|
|
45
|
+
if (src === tgt)
|
|
46
|
+
continue;
|
|
47
|
+
if (!nodeIds.has(src)) {
|
|
48
|
+
dangling.add(src);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const key = `${src}${SEP}${tgt}`;
|
|
52
|
+
const existing = edgeMap.get(key);
|
|
53
|
+
if (existing) {
|
|
54
|
+
if (existing.kind === "outbound")
|
|
55
|
+
existing.kind = "both";
|
|
56
|
+
// already 'inbound' or 'both' → keep
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
edgeMap.set(key, { source: src, target: tgt, kind: "inbound" });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const edges = Array.from(edgeMap.values());
|
|
64
|
+
// Compute resolved in/out degree from final edge list.
|
|
65
|
+
const inDeg = new Map();
|
|
66
|
+
const outDeg = new Map();
|
|
67
|
+
for (const e of edges) {
|
|
68
|
+
outDeg.set(e.source, (outDeg.get(e.source) ?? 0) + 1);
|
|
69
|
+
inDeg.set(e.target, (inDeg.get(e.target) ?? 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
const nodes = pages.map((p) => {
|
|
72
|
+
const out_degree = outDeg.get(p.id) ?? 0;
|
|
73
|
+
const in_degree = inDeg.get(p.id) ?? 0;
|
|
74
|
+
return {
|
|
75
|
+
id: p.id,
|
|
76
|
+
title: p.title,
|
|
77
|
+
category: p.category,
|
|
78
|
+
degree: in_degree + out_degree,
|
|
79
|
+
in_degree,
|
|
80
|
+
out_degree,
|
|
81
|
+
stale: p.stale,
|
|
82
|
+
updated_at: p.updated_at,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
const orphans = nodes.filter((n) => n.in_degree === 0).length;
|
|
86
|
+
const isolated = nodes.filter((n) => n.degree === 0).length;
|
|
87
|
+
return {
|
|
88
|
+
nodes,
|
|
89
|
+
edges,
|
|
90
|
+
meta: {
|
|
91
|
+
total_pages: nodes.length,
|
|
92
|
+
total_edges: edges.length,
|
|
93
|
+
orphans,
|
|
94
|
+
isolated,
|
|
95
|
+
dangling_refs: Array.from(dangling).sort(),
|
|
96
|
+
generated_at: generatedAt,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
return {
|
|
102
|
+
nodes: [],
|
|
103
|
+
edges: [],
|
|
104
|
+
meta: {
|
|
105
|
+
total_pages: 0, total_edges: 0, orphans: 0, isolated: 0,
|
|
106
|
+
dangling_refs: [], generated_at: generatedAt,
|
|
107
|
+
},
|
|
108
|
+
error: e?.message ?? String(e),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/tools/list-wiki.js
CHANGED
|
@@ -20,34 +20,98 @@ const SINGULAR_TO_PLURAL = {
|
|
|
20
20
|
synthesis: "syntheses",
|
|
21
21
|
};
|
|
22
22
|
const STALE_DAYS = 30;
|
|
23
|
+
function unquote(s) {
|
|
24
|
+
const t = s.trim();
|
|
25
|
+
if (t.length >= 2) {
|
|
26
|
+
const first = t[0];
|
|
27
|
+
const last = t[t.length - 1];
|
|
28
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
29
|
+
return t.slice(1, -1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return t;
|
|
33
|
+
}
|
|
23
34
|
/**
|
|
24
|
-
* Parse frontmatter from markdown
|
|
35
|
+
* Parse YAML-ish frontmatter from markdown.
|
|
36
|
+
*
|
|
37
|
+
* Supports:
|
|
38
|
+
* - Inline JSON/flow: key: ["a", "b"] or key: {x: 1}
|
|
39
|
+
* - Block-style list: key:\n - "a"\n - "b"
|
|
40
|
+
* - Scalar: key: value (values may contain ':' — split on FIRST ':' only)
|
|
25
41
|
*/
|
|
26
42
|
function parseFrontmatter(content) {
|
|
27
43
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
28
44
|
if (!match)
|
|
29
45
|
return {};
|
|
30
|
-
const
|
|
46
|
+
const lines = match[1].split("\n");
|
|
31
47
|
const result = {};
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
let i = 0;
|
|
49
|
+
while (i < lines.length) {
|
|
50
|
+
const line = lines[i];
|
|
51
|
+
const trimmed = line.trim();
|
|
52
|
+
if (!trimmed) {
|
|
53
|
+
i++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// Block-list continuation handled inside the key branch below; top-level
|
|
57
|
+
// dashes without a preceding key are ignored.
|
|
58
|
+
if (trimmed.startsWith("- ")) {
|
|
59
|
+
i++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const colonIdx = line.indexOf(":");
|
|
63
|
+
if (colonIdx === -1) {
|
|
64
|
+
i++;
|
|
34
65
|
continue;
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
66
|
+
}
|
|
67
|
+
const key = line.slice(0, colonIdx).trim();
|
|
68
|
+
const rawValue = line.slice(colonIdx + 1).trim();
|
|
69
|
+
if (rawValue === "") {
|
|
70
|
+
// Possibly a block-style list on subsequent lines: " - value"
|
|
71
|
+
const items = [];
|
|
72
|
+
let j = i + 1;
|
|
73
|
+
while (j < lines.length) {
|
|
74
|
+
const next = lines[j];
|
|
75
|
+
if (!next.trim()) {
|
|
76
|
+
j++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// A block-list item must be indented and start with "- ".
|
|
80
|
+
const m = next.match(/^(\s+)-\s+(.*)$/);
|
|
81
|
+
if (!m)
|
|
82
|
+
break;
|
|
83
|
+
items.push(unquote(m[2]));
|
|
84
|
+
j++;
|
|
85
|
+
}
|
|
86
|
+
if (items.length > 0) {
|
|
87
|
+
result[key] = items;
|
|
40
88
|
}
|
|
41
89
|
else {
|
|
42
|
-
result[key
|
|
90
|
+
result[key] = "";
|
|
91
|
+
}
|
|
92
|
+
i = j;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (rawValue.startsWith("[") || rawValue.startsWith("{")) {
|
|
96
|
+
try {
|
|
97
|
+
result[key] = JSON.parse(rawValue);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
result[key] = rawValue;
|
|
43
101
|
}
|
|
44
102
|
}
|
|
45
|
-
|
|
46
|
-
result[key
|
|
103
|
+
else {
|
|
104
|
+
result[key] = unquote(rawValue);
|
|
47
105
|
}
|
|
106
|
+
i++;
|
|
48
107
|
}
|
|
49
108
|
return result;
|
|
50
109
|
}
|
|
110
|
+
function toStringArray(v) {
|
|
111
|
+
if (!Array.isArray(v))
|
|
112
|
+
return [];
|
|
113
|
+
return v.filter((x) => typeof x === "string");
|
|
114
|
+
}
|
|
51
115
|
/**
|
|
52
116
|
* Extract title from markdown
|
|
53
117
|
*/
|
|
@@ -89,6 +153,8 @@ async function listWiki(input) {
|
|
|
89
153
|
const updatedAt = fm.updated_at ?? new Date().toISOString();
|
|
90
154
|
const updatedMs = new Date(updatedAt).getTime();
|
|
91
155
|
const stale = !isNaN(updatedMs) && (now - updatedMs) > STALE_DAYS * 86_400_000;
|
|
156
|
+
const outbound_links = toStringArray(fm.outbound_links);
|
|
157
|
+
const inbound_links = toStringArray(fm.inbound_links);
|
|
92
158
|
pages.push({
|
|
93
159
|
id: pageId,
|
|
94
160
|
title,
|
|
@@ -96,9 +162,12 @@ async function listWiki(input) {
|
|
|
96
162
|
path: node_path_1.default.relative(docuDir, filePath),
|
|
97
163
|
created_at: fm.created_at ?? new Date().toISOString(),
|
|
98
164
|
updated_at: updatedAt,
|
|
99
|
-
sources: fm.sources
|
|
100
|
-
tags: fm.tags
|
|
165
|
+
sources: toStringArray(fm.sources),
|
|
166
|
+
tags: toStringArray(fm.tags),
|
|
101
167
|
stale,
|
|
168
|
+
outbound_links,
|
|
169
|
+
inbound_links,
|
|
170
|
+
degree: outbound_links.length + inbound_links.length,
|
|
102
171
|
});
|
|
103
172
|
categoryCount++;
|
|
104
173
|
}
|