sdtk-wiki-kit 0.2.0 → 0.2.2
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 +8 -0
- package/assets/atlas/build_atlas.py +775 -775
- package/assets/atlas/doc_atlas_viewer_template.html +4054 -3796
- package/assets/atlas/vendor/mermaid.min.js +2029 -2029
- package/bin/sdtk-wiki.js +0 -0
- package/package.json +45 -45
- package/src/commands/ask.js +2 -0
- package/src/commands/context.js +67 -67
- package/src/commands/help.js +7 -0
- package/src/commands/init.js +1 -1
- package/src/commands/update.js +11 -0
- package/src/index.js +111 -107
- package/src/lib/update.js +217 -0
- package/src/lib/wiki-ask.js +254 -19
- package/src/lib/wiki-config.js +3 -1
- package/src/lib/wiki-context-pack.js +267 -267
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawn } = require("child_process");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { parseFlags } = require("./args");
|
|
6
|
+
const { CliError, ValidationError } = require("./errors");
|
|
7
|
+
|
|
8
|
+
const PACKAGE_NAME = "sdtk-wiki-kit";
|
|
9
|
+
const PRODUCT_NAME = "SDTK-WIKI";
|
|
10
|
+
const NPM_BIN = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
11
|
+
const NPM_DISPLAY = "npm";
|
|
12
|
+
const NPM_VIEW_ARGS = ["view", PACKAGE_NAME, "version"];
|
|
13
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
14
|
+
const FLAG_DEFS = {
|
|
15
|
+
version: { type: "string" },
|
|
16
|
+
"project-path": { type: "string" },
|
|
17
|
+
"check-only": { type: "boolean" },
|
|
18
|
+
"skip-project-files": { type: "boolean" },
|
|
19
|
+
verbose: { type: "boolean" },
|
|
20
|
+
};
|
|
21
|
+
const pkg = require("../../package.json");
|
|
22
|
+
|
|
23
|
+
let commandExecutor = defaultCommandExecutor;
|
|
24
|
+
|
|
25
|
+
function defaultCommandExecutor(command, args, options = {}) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const child = spawn(command, args, {
|
|
28
|
+
cwd: options.cwd || process.cwd(),
|
|
29
|
+
env: options.env || process.env,
|
|
30
|
+
shell: options.shell || false,
|
|
31
|
+
windowsHide: true,
|
|
32
|
+
});
|
|
33
|
+
let stdout = "";
|
|
34
|
+
let stderr = "";
|
|
35
|
+
|
|
36
|
+
child.stdout.on("data", (chunk) => {
|
|
37
|
+
const text = chunk.toString();
|
|
38
|
+
stdout += text;
|
|
39
|
+
if (options.verbose) process.stdout.write(text);
|
|
40
|
+
});
|
|
41
|
+
child.stderr.on("data", (chunk) => {
|
|
42
|
+
const text = chunk.toString();
|
|
43
|
+
stderr += text;
|
|
44
|
+
if (options.verbose) process.stderr.write(text);
|
|
45
|
+
});
|
|
46
|
+
child.on("error", (error) => {
|
|
47
|
+
if (error && error.code === "ENOENT") {
|
|
48
|
+
reject(new CliError(`Required command not found in PATH: ${command}`));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
reject(error);
|
|
52
|
+
});
|
|
53
|
+
child.on("close", (exitCode) => {
|
|
54
|
+
resolve({ exitCode: typeof exitCode === "number" ? exitCode : 1, stdout, stderr });
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function setCommandExecutorForTests(executor) {
|
|
60
|
+
commandExecutor = executor || defaultCommandExecutor;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resetCommandExecutorForTests() {
|
|
64
|
+
commandExecutor = defaultCommandExecutor;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function quote(value) {
|
|
68
|
+
const text = String(value);
|
|
69
|
+
return /[\s"]/u.test(text) ? JSON.stringify(text) : text;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatCommand(command, args) {
|
|
73
|
+
return [command, ...args].map((value) => quote(value)).join(" ");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateVersion(targetVersion) {
|
|
77
|
+
if (targetVersion !== "latest" && !VERSION_PATTERN.test(targetVersion)) {
|
|
78
|
+
throw new ValidationError(`Invalid value for --version: "${targetVersion}". Must be "latest" or x.y.z.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function extractResolvedVersion(stdout) {
|
|
83
|
+
const lines = String(stdout || "")
|
|
84
|
+
.split(/\r?\n/u)
|
|
85
|
+
.map((line) => line.trim())
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
const candidate = (lines[lines.length - 1] || "").replace(/^['"]|['"]$/gu, "");
|
|
88
|
+
if (!VERSION_PATTERN.test(candidate)) {
|
|
89
|
+
throw new CliError(`npm registry lookup returned an invalid version for ${PACKAGE_NAME}: "${candidate || "<empty>"}"`);
|
|
90
|
+
}
|
|
91
|
+
return candidate;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function resolveTargetVersion(options) {
|
|
95
|
+
if (options.requestedVersion !== "latest") {
|
|
96
|
+
return options.requestedVersion;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let result;
|
|
100
|
+
try {
|
|
101
|
+
result = await commandExecutor(NPM_BIN, NPM_VIEW_ARGS, {
|
|
102
|
+
verbose: options.verbose,
|
|
103
|
+
shell: process.platform === "win32",
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw new CliError(`npm registry lookup failed for ${PACKAGE_NAME} while resolving --version latest.\n${error.message}`, error.exitCode || 4);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (result.exitCode !== 0) {
|
|
110
|
+
const detail = (result.stderr || result.stdout || "").trim();
|
|
111
|
+
throw new CliError(`npm registry lookup failed for ${PACKAGE_NAME} while resolving --version latest (exit code ${result.exitCode}).${detail ? `\n${detail}` : ""}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
return extractResolvedVersion(result.stdout);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw new CliError(`npm registry lookup failed for ${PACKAGE_NAME} while resolving --version latest.\n${error.message}`, error.exitCode || 4);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildPlan(options) {
|
|
122
|
+
const npmArgs = ["install", "-g", `${PACKAGE_NAME}@${options.targetVersion}`];
|
|
123
|
+
return {
|
|
124
|
+
installedVersion: pkg.version,
|
|
125
|
+
requestedVersion: options.requestedVersion,
|
|
126
|
+
targetVersion: options.targetVersion,
|
|
127
|
+
updateNeeded: pkg.version !== options.targetVersion,
|
|
128
|
+
npmArgs,
|
|
129
|
+
npmCommand: formatCommand(NPM_DISPLAY, npmArgs),
|
|
130
|
+
projectPath: options.projectPath,
|
|
131
|
+
checkOnly: options.checkOnly,
|
|
132
|
+
skipProjectFiles: options.skipProjectFiles,
|
|
133
|
+
projectRefreshCommand: options.skipProjectFiles
|
|
134
|
+
? "skipped (--skip-project-files)"
|
|
135
|
+
: "skipped (R1 package-only update; project files are never mutated)",
|
|
136
|
+
runtimeRefreshCommand: "skipped (no runtime asset update in R1)",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseUpdateOptions(args) {
|
|
141
|
+
const { flags, positional } = parseFlags(args || [], FLAG_DEFS);
|
|
142
|
+
|
|
143
|
+
if (positional.length > 0) {
|
|
144
|
+
throw new ValidationError(`Unexpected arguments: ${positional.join(" ")}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const requestedVersion = flags.version || "latest";
|
|
148
|
+
validateVersion(requestedVersion);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
requestedVersion,
|
|
152
|
+
projectPath: path.resolve(flags["project-path"] || process.cwd()),
|
|
153
|
+
checkOnly: Boolean(flags["check-only"]),
|
|
154
|
+
skipProjectFiles: Boolean(flags["skip-project-files"]),
|
|
155
|
+
verbose: Boolean(flags.verbose),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function printPlan(plan) {
|
|
160
|
+
console.log(`${PRODUCT_NAME} update plan`);
|
|
161
|
+
console.log(` Installed package version: ${plan.installedVersion}`);
|
|
162
|
+
console.log(` Requested package version: ${plan.requestedVersion}`);
|
|
163
|
+
console.log(` Target package version: ${plan.targetVersion}`);
|
|
164
|
+
console.log(` Package update needed: ${plan.updateNeeded ? "yes" : `no (already installed: ${plan.installedVersion})`}`);
|
|
165
|
+
console.log(` Package refresh command: ${plan.npmCommand}`);
|
|
166
|
+
console.log(` Project path: ${plan.projectPath}`);
|
|
167
|
+
console.log(` Project file refresh: ${plan.projectRefreshCommand}`);
|
|
168
|
+
console.log(` Runtime asset refresh: ${plan.runtimeRefreshCommand}`);
|
|
169
|
+
console.log(` Mode: ${plan.checkOnly ? "check-only (no changes applied)" : "apply"}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function runCommand(label, command, args, options) {
|
|
173
|
+
const result = await commandExecutor(command, args, options);
|
|
174
|
+
if (result.exitCode !== 0) {
|
|
175
|
+
const detail = (result.stderr || result.stdout || "").trim();
|
|
176
|
+
throw new CliError(`${label} failed (exit code ${result.exitCode}).${detail ? `\n${detail}` : ""}`);
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function applyPlan(plan, options) {
|
|
182
|
+
console.log("");
|
|
183
|
+
console.log(`Applying ${PRODUCT_NAME} update...`);
|
|
184
|
+
console.log(` npm refresh: ${plan.npmCommand}`);
|
|
185
|
+
await runCommand("npm package refresh", NPM_BIN, plan.npmArgs, {
|
|
186
|
+
verbose: options.verbose,
|
|
187
|
+
shell: process.platform === "win32",
|
|
188
|
+
});
|
|
189
|
+
console.log(` project refresh: ${plan.projectRefreshCommand}`);
|
|
190
|
+
console.log(` runtime refresh: ${plan.runtimeRefreshCommand}`);
|
|
191
|
+
console.log("");
|
|
192
|
+
console.log(`${PRODUCT_NAME} update completed successfully.`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function executeUpdate(args) {
|
|
196
|
+
const options = parseUpdateOptions(args);
|
|
197
|
+
const targetVersion = await resolveTargetVersion(options);
|
|
198
|
+
const plan = buildPlan({ ...options, targetVersion });
|
|
199
|
+
printPlan(plan);
|
|
200
|
+
|
|
201
|
+
if (options.checkOnly) {
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await applyPlan(plan, options);
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
buildPlan,
|
|
211
|
+
executeUpdate,
|
|
212
|
+
formatCommand,
|
|
213
|
+
parseUpdateOptions,
|
|
214
|
+
resetCommandExecutorForTests,
|
|
215
|
+
resolveTargetVersion,
|
|
216
|
+
setCommandExecutorForTests,
|
|
217
|
+
};
|
package/src/lib/wiki-ask.js
CHANGED
|
@@ -9,6 +9,11 @@ const { loadWikiAskHandler } = require("./wiki-premium-loader");
|
|
|
9
9
|
const INDEX_FILE = "SDTK_DOC_INDEX.json";
|
|
10
10
|
const GRAPH_FILE = "SDTK_DOC_GRAPH.json";
|
|
11
11
|
const DEFAULT_MAX_SOURCES = 6;
|
|
12
|
+
// BK-269 hyperedge-aware context expansion (--source filter only).
|
|
13
|
+
// Mirrors run_doc_atlas_server.py so both grounding surfaces behave identically.
|
|
14
|
+
const DEFAULT_MAX_HYPEREDGE_EXPANSION = 6;
|
|
15
|
+
const DEFAULT_MIN_HYPEREDGE_CO_MEMBERSHIP = 1;
|
|
16
|
+
const DEFAULT_MAX_FAMILY_HYPEREDGE_CONTRIB = 3;
|
|
12
17
|
|
|
13
18
|
function readJsonFile(filePath, label) {
|
|
14
19
|
try {
|
|
@@ -69,28 +74,251 @@ function extractDocumentText(document) {
|
|
|
69
74
|
return "";
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
function
|
|
77
|
+
function docId(document) {
|
|
78
|
+
return document.id || extractDocumentPath(document);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function docFacetKeys(document) {
|
|
82
|
+
const keys = [];
|
|
83
|
+
const groups = [
|
|
84
|
+
["issue", document.issues],
|
|
85
|
+
["knowledge", document.knowledge_ids],
|
|
86
|
+
["skill", document.skill_refs],
|
|
87
|
+
["lane", document.lane_refs],
|
|
88
|
+
];
|
|
89
|
+
for (const [namespace, values] of groups) {
|
|
90
|
+
if (!Array.isArray(values)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
for (const value of values) {
|
|
94
|
+
const text = String(value == null ? "" : value).trim();
|
|
95
|
+
if (text) {
|
|
96
|
+
keys.push(`${namespace}:${text}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const family = String(document.family == null ? "" : document.family).trim();
|
|
101
|
+
if (family) {
|
|
102
|
+
keys.push(`family:${family}`);
|
|
103
|
+
}
|
|
104
|
+
return keys;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Invert shared reference facets into hyperedges (groups with >=2 docs). Pure and
|
|
108
|
+
// deterministic; mirrors _derive_hyperedges in run_doc_atlas_server.py.
|
|
109
|
+
function deriveHyperedges(documents) {
|
|
110
|
+
const facetToDocs = new Map();
|
|
111
|
+
for (const document of documents) {
|
|
112
|
+
if (!document || typeof document !== "object") {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const id = docId(document);
|
|
116
|
+
if (!id) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
for (const facetKey of docFacetKeys(document)) {
|
|
120
|
+
let members = facetToDocs.get(facetKey);
|
|
121
|
+
if (!members) {
|
|
122
|
+
members = [];
|
|
123
|
+
facetToDocs.set(facetKey, members);
|
|
124
|
+
}
|
|
125
|
+
if (!members.includes(id)) {
|
|
126
|
+
members.push(id);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const hyperedges = new Map();
|
|
131
|
+
for (const [facet, ids] of facetToDocs) {
|
|
132
|
+
if (ids.length >= 2) {
|
|
133
|
+
hyperedges.set(facet, ids.slice().sort());
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const docHyperedges = new Map();
|
|
137
|
+
for (const [facet, ids] of hyperedges) {
|
|
138
|
+
for (const id of ids) {
|
|
139
|
+
let facets = docHyperedges.get(id);
|
|
140
|
+
if (!facets) {
|
|
141
|
+
facets = new Set();
|
|
142
|
+
docHyperedges.set(id, facets);
|
|
143
|
+
}
|
|
144
|
+
facets.add(facet);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { hyperedges, docHyperedges };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function computeNodeDegree(graph, docIdSet) {
|
|
151
|
+
const degree = new Map();
|
|
152
|
+
const edges = graph && Array.isArray(graph.edges) ? graph.edges : [];
|
|
153
|
+
for (const edge of edges) {
|
|
154
|
+
const source = edge && edge.source;
|
|
155
|
+
const target = edge && edge.target;
|
|
156
|
+
if (docIdSet.has(source) && docIdSet.has(target)) {
|
|
157
|
+
degree.set(source, (degree.get(source) || 0) + 1);
|
|
158
|
+
degree.set(target, (degree.get(target) || 0) + 1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return degree;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Bounded hyperedge co-member expansion. Returns ranked expansion entries
|
|
165
|
+
// ({ id, facets, relation }). Mirrors _expand_via_hyperedges in the Python server:
|
|
166
|
+
// shared-facet-count desc -> node_degree desc -> title/id lexical, with a family cap.
|
|
167
|
+
function expandSourcesViaHyperedges(seedIds, derived, nodeDegree, docsById, options) {
|
|
168
|
+
const opts = options || {};
|
|
169
|
+
const maxExpansion = Number.isInteger(opts.maxExpansion)
|
|
170
|
+
? opts.maxExpansion
|
|
171
|
+
: DEFAULT_MAX_HYPEREDGE_EXPANSION;
|
|
172
|
+
const minCoMembership = Number.isInteger(opts.minCoMembership)
|
|
173
|
+
? opts.minCoMembership
|
|
174
|
+
: DEFAULT_MIN_HYPEREDGE_CO_MEMBERSHIP;
|
|
175
|
+
const familyCap = Number.isInteger(opts.familyCap)
|
|
176
|
+
? opts.familyCap
|
|
177
|
+
: DEFAULT_MAX_FAMILY_HYPEREDGE_CONTRIB;
|
|
178
|
+
if (maxExpansion <= 0) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
const seedSet = new Set(seedIds);
|
|
182
|
+
const seedFacets = new Set();
|
|
183
|
+
for (const seedId of seedIds) {
|
|
184
|
+
const facets = derived.docHyperedges.get(seedId);
|
|
185
|
+
if (facets) {
|
|
186
|
+
for (const facet of facets) {
|
|
187
|
+
seedFacets.add(facet);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (seedFacets.size === 0) {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
const candidateFacets = new Map();
|
|
195
|
+
for (const facet of seedFacets) {
|
|
196
|
+
const members = derived.hyperedges.get(facet) || [];
|
|
197
|
+
for (const id of members) {
|
|
198
|
+
if (seedSet.has(id)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
let facets = candidateFacets.get(id);
|
|
202
|
+
if (!facets) {
|
|
203
|
+
facets = new Set();
|
|
204
|
+
candidateFacets.set(id, facets);
|
|
205
|
+
}
|
|
206
|
+
facets.add(facet);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const candidates = [];
|
|
210
|
+
for (const [id, facets] of candidateFacets) {
|
|
211
|
+
if (facets.size >= minCoMembership) {
|
|
212
|
+
candidates.push({ id, facets });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
candidates.sort((a, b) => {
|
|
216
|
+
if (b.facets.size !== a.facets.size) {
|
|
217
|
+
return b.facets.size - a.facets.size;
|
|
218
|
+
}
|
|
219
|
+
const degreeDelta = (nodeDegree.get(b.id) || 0) - (nodeDegree.get(a.id) || 0);
|
|
220
|
+
if (degreeDelta !== 0) {
|
|
221
|
+
return degreeDelta;
|
|
222
|
+
}
|
|
223
|
+
const titleA = String((docsById.get(a.id) || {}).title || a.id).toLowerCase();
|
|
224
|
+
const titleB = String((docsById.get(b.id) || {}).title || b.id).toLowerCase();
|
|
225
|
+
if (titleA !== titleB) {
|
|
226
|
+
return titleA < titleB ? -1 : 1;
|
|
227
|
+
}
|
|
228
|
+
const idA = a.id.toLowerCase();
|
|
229
|
+
const idB = b.id.toLowerCase();
|
|
230
|
+
if (idA !== idB) {
|
|
231
|
+
return idA < idB ? -1 : 1;
|
|
232
|
+
}
|
|
233
|
+
return 0;
|
|
234
|
+
});
|
|
235
|
+
const selected = [];
|
|
236
|
+
let familyOnlyAdmitted = 0;
|
|
237
|
+
for (const candidate of candidates) {
|
|
238
|
+
if (selected.length >= maxExpansion) {
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
let hasNonFamily = false;
|
|
242
|
+
for (const facet of candidate.facets) {
|
|
243
|
+
if (!facet.startsWith("family:")) {
|
|
244
|
+
hasNonFamily = true;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!hasNonFamily) {
|
|
249
|
+
// OQ-C: cap how many docs can enter via a family facet alone.
|
|
250
|
+
if (familyOnlyAdmitted >= familyCap) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
familyOnlyAdmitted += 1;
|
|
254
|
+
}
|
|
255
|
+
const sortedFacets = Array.from(candidate.facets).sort();
|
|
256
|
+
selected.push({
|
|
257
|
+
id: candidate.id,
|
|
258
|
+
facets: sortedFacets,
|
|
259
|
+
relation: `shares ${sortedFacets.join(", ")}`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return selected;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function buildSources(index, sourceFilters, maxSources, graph) {
|
|
73
266
|
const documents = Array.isArray(index.documents) ? index.documents : [];
|
|
74
267
|
const filters = (sourceFilters || []).map((item) => item.trim()).filter(Boolean);
|
|
75
268
|
const limit = Number.isInteger(maxSources) && maxSources > 0 ? maxSources : DEFAULT_MAX_SOURCES;
|
|
76
269
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
270
|
+
const mapped = documents.map((document) => {
|
|
271
|
+
const sourcePath = extractDocumentPath(document);
|
|
272
|
+
return {
|
|
273
|
+
id: document.id || sourcePath,
|
|
274
|
+
path: sourcePath,
|
|
275
|
+
title: document.title || sourcePath,
|
|
276
|
+
text: extractDocumentText(document),
|
|
277
|
+
relation: "primary",
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// No explicit --source filter: ground on all docs, unchanged (no expansion).
|
|
282
|
+
if (filters.length === 0) {
|
|
283
|
+
return mapped.slice(0, limit);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const primaries = mapped.filter((source) =>
|
|
287
|
+
filters.some((filter) => source.id === filter || source.path === filter)
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// BK-269: append the filtered docs' hyperedge co-members (capped, labelled),
|
|
291
|
+
// so `--source <doc>` also grounds on its directly-related siblings.
|
|
292
|
+
const sourceById = new Map(mapped.map((source) => [source.id, source]));
|
|
293
|
+
const docsById = new Map();
|
|
294
|
+
for (const document of documents) {
|
|
295
|
+
if (!document || typeof document !== "object") {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const id = docId(document);
|
|
299
|
+
if (id && !docsById.has(id)) {
|
|
300
|
+
docsById.set(id, document);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const derived = deriveHyperedges(documents);
|
|
304
|
+
const nodeDegree = computeNodeDegree(graph, new Set(docsById.keys()));
|
|
305
|
+
const seedIds = primaries.map((source) => source.id);
|
|
306
|
+
const expansion = expandSourcesViaHyperedges(seedIds, derived, nodeDegree, docsById, {});
|
|
307
|
+
|
|
308
|
+
const result = primaries.slice();
|
|
309
|
+
const seen = new Set(seedIds);
|
|
310
|
+
for (const item of expansion) {
|
|
311
|
+
if (seen.has(item.id)) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const base = sourceById.get(item.id);
|
|
315
|
+
if (!base) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
seen.add(item.id);
|
|
319
|
+
result.push({ ...base, relation: item.relation });
|
|
320
|
+
}
|
|
321
|
+
return result.slice(0, limit);
|
|
94
322
|
}
|
|
95
323
|
|
|
96
324
|
function normalizeCitations(citations) {
|
|
@@ -126,6 +354,7 @@ function normalizeAskResult(rawResult, context) {
|
|
|
126
354
|
citations: normalizeCitations(result.citations),
|
|
127
355
|
confidence: result.confidence,
|
|
128
356
|
graphPath: context.graphPath,
|
|
357
|
+
expandedDocCount: Number.isInteger(context.expandedDocCount) ? context.expandedDocCount : 0,
|
|
129
358
|
};
|
|
130
359
|
}
|
|
131
360
|
|
|
@@ -139,7 +368,10 @@ async function runWikiAsk(options) {
|
|
|
139
368
|
const graphInfo = assertWikiGraphReady(projectPath);
|
|
140
369
|
const index = readJsonFile(graphInfo.indexPath, INDEX_FILE);
|
|
141
370
|
const graph = readJsonFile(graphInfo.graphFilePath, GRAPH_FILE);
|
|
142
|
-
const sources = buildSources(index, options.sources, options.maxSources);
|
|
371
|
+
const sources = buildSources(index, options.sources, options.maxSources, graph);
|
|
372
|
+
const expandedDocCount = sources.filter(
|
|
373
|
+
(source) => source.relation && source.relation !== "primary"
|
|
374
|
+
).length;
|
|
143
375
|
|
|
144
376
|
const handlerState = await loadWikiAskHandler();
|
|
145
377
|
if (!handlerState.ok) {
|
|
@@ -155,6 +387,7 @@ async function runWikiAsk(options) {
|
|
|
155
387
|
graphFilePath: graphInfo.graphFilePath,
|
|
156
388
|
graph,
|
|
157
389
|
sources,
|
|
390
|
+
expandedDocCount,
|
|
158
391
|
maxSources: options.maxSources,
|
|
159
392
|
};
|
|
160
393
|
|
|
@@ -170,6 +403,8 @@ async function runWikiAsk(options) {
|
|
|
170
403
|
|
|
171
404
|
module.exports = {
|
|
172
405
|
buildSources,
|
|
406
|
+
deriveHyperedges,
|
|
407
|
+
expandSourcesViaHyperedges,
|
|
173
408
|
normalizeAskResult,
|
|
174
409
|
runWikiAsk,
|
|
175
410
|
};
|
package/src/lib/wiki-config.js
CHANGED
|
@@ -71,7 +71,9 @@ function resolveWikiConfig(flags = {}) {
|
|
|
71
71
|
) {
|
|
72
72
|
scanRoots = persisted.scanRoots.map((r) => path.resolve(r));
|
|
73
73
|
} else {
|
|
74
|
-
|
|
74
|
+
const docsDir = path.join(projectPath, "docs");
|
|
75
|
+
const hasDocsDir = fs.existsSync(docsDir) && fs.statSync(docsDir).isDirectory();
|
|
76
|
+
scanRoots = [hasDocsDir ? docsDir : projectPath];
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
const excludes =
|