hdoc-tools 0.57.5 → 0.60.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/LICENSE +21 -0
- package/hdoc-build-db.js +24 -0
- package/hdoc-build-embeddings.js +202 -0
- package/hdoc-build.js +42 -0
- package/hdoc-bump.js +123 -123
- package/hdoc-db.js +7 -0
- package/hdoc-help.js +1 -0
- package/hdoc-validate-config.js +6 -1
- package/hdoc-ver.js +43 -43
- package/hdoc.js +5 -2
- package/npm-shrinkwrap.json +996 -3
- package/package.json +6 -2
- package/schemas/hdocbook.schema.json +5 -0
- package/templates/init/_hdocbook/hdocbook.json +1 -0
- package/validateNodeVer.js +17 -17
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Hornbill Docs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/hdoc-build-db.js
CHANGED
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"doc_lastmod",
|
|
18
18
|
"doc_status",
|
|
19
19
|
"doc_keywords",
|
|
20
|
+
// 0=document, 1=api_ref, 2=db_ref, 3=etl_ref, 4=mcp_ref
|
|
21
|
+
"doc_type UNINDEXED",
|
|
22
|
+
// Raw markdown source of the document (HTML sources are converted)
|
|
23
|
+
"doc_md UNINDEXED",
|
|
20
24
|
],
|
|
21
25
|
hdoc_meta: [
|
|
22
26
|
"resource_url",
|
|
@@ -39,6 +43,16 @@
|
|
|
39
43
|
"location_url",
|
|
40
44
|
"http_code INTEGER UNINDEXED",
|
|
41
45
|
],
|
|
46
|
+
// Semantic search embeddings, generated by hdoc-build-embeddings.js.
|
|
47
|
+
// The docs service merges these into its sqlite-vec index at publish
|
|
48
|
+
hdoc_embeddings: [
|
|
49
|
+
"resource_url",
|
|
50
|
+
"chunk_seq INTEGER",
|
|
51
|
+
"chunk_text",
|
|
52
|
+
"embedding BLOB",
|
|
53
|
+
"model",
|
|
54
|
+
"dims INTEGER",
|
|
55
|
+
],
|
|
42
56
|
};
|
|
43
57
|
|
|
44
58
|
exports.create_table = (db, table, cols, virtual, fts5) => {
|
|
@@ -138,6 +152,14 @@
|
|
|
138
152
|
|
|
139
153
|
if (!book_config.tags) book_config.tags = [];
|
|
140
154
|
|
|
155
|
+
// Book-level content type from hdocbook.json bookType:
|
|
156
|
+
// 0=document, 1=api_ref, 2=db_ref, 3=etl_ref, 4=mcp_ref
|
|
157
|
+
// Bound as BigInt: better-sqlite3 binds JS numbers as REAL, and FTS5
|
|
158
|
+
// columns have no affinity to coerce them back to INTEGER
|
|
159
|
+
const book_doc_type = BigInt(
|
|
160
|
+
Number.isInteger(book_config.bookType) ? book_config.bookType : 0,
|
|
161
|
+
);
|
|
162
|
+
|
|
141
163
|
// Build a prepared statement from a schema entry once, reusing it for every row.
|
|
142
164
|
// Previously insert_record() called db.prepare() on every single insert.
|
|
143
165
|
const make_stmt = (table) => {
|
|
@@ -183,6 +205,8 @@
|
|
|
183
205
|
file.lastmod,
|
|
184
206
|
file.status,
|
|
185
207
|
file.keywords,
|
|
208
|
+
book_doc_type,
|
|
209
|
+
file.doc_md ?? "",
|
|
186
210
|
);
|
|
187
211
|
inserted_row_id = info.lastInsertRowid;
|
|
188
212
|
} catch (e) {
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const crypto = require("node:crypto");
|
|
5
|
+
|
|
6
|
+
// Model identity - must match what the docs service query embedder uses
|
|
7
|
+
// (see embedder.h in esp-docs-service). The service refuses embeddings
|
|
8
|
+
// built with a different model or dimension count.
|
|
9
|
+
const MODEL_ID = "all-MiniLM-L6-v2";
|
|
10
|
+
const HF_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
11
|
+
const DIMS = 384;
|
|
12
|
+
|
|
13
|
+
// SHA-256 of onnx/model.onnx as downloaded from Hugging Face. Matches the
|
|
14
|
+
// vendored copy the docs service runs (search_model/model.onnx). Verified
|
|
15
|
+
// after download, before any embeddings are generated - a hash mismatch
|
|
16
|
+
// (tampered/corrupt/re-published model) aborts the embedding step.
|
|
17
|
+
const MODEL_SHA256 = "759c3cd2b7fe7e93933ad23c4c9181b7396442a2ed746ec7c1d46192c469c46e";
|
|
18
|
+
|
|
19
|
+
// Chunking parameters (word based). MiniLM attends to ~256 wordpiece
|
|
20
|
+
// tokens; 180 words plus the title prefix stays comfortably inside that.
|
|
21
|
+
const CHUNK_WORDS = 180;
|
|
22
|
+
const CHUNK_OVERLAP_WORDS = 30;
|
|
23
|
+
const MAX_CHUNKS_PER_DOC = 40;
|
|
24
|
+
const EMBED_BATCH_SIZE = 16;
|
|
25
|
+
|
|
26
|
+
// Maximum characters of chunk text stored for use as a result snippet
|
|
27
|
+
const STORED_CHUNK_CHARS = 500;
|
|
28
|
+
|
|
29
|
+
// Split document plain text into overlapping word-window chunks
|
|
30
|
+
const chunk_text = (text) => {
|
|
31
|
+
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
32
|
+
if (words.length === 0) return [];
|
|
33
|
+
|
|
34
|
+
const chunks = [];
|
|
35
|
+
let start = 0;
|
|
36
|
+
while (start < words.length && chunks.length < MAX_CHUNKS_PER_DOC) {
|
|
37
|
+
const end = Math.min(start + CHUNK_WORDS, words.length);
|
|
38
|
+
chunks.push(words.slice(start, end).join(" "));
|
|
39
|
+
if (end >= words.length) break;
|
|
40
|
+
start = end - CHUNK_OVERLAP_WORDS;
|
|
41
|
+
}
|
|
42
|
+
return chunks;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Compute the resource_url for an index record. Must stay in step with
|
|
46
|
+
// the equivalent logic in populate_index (hdoc-build-db.js), as the docs
|
|
47
|
+
// service joins hdoc_embeddings to hdoc_index on this value.
|
|
48
|
+
const resource_url_for = (file) => {
|
|
49
|
+
let index_path_name = file.relative_path.replaceAll("\\", "/");
|
|
50
|
+
if (
|
|
51
|
+
index_path_name.endsWith("/index.md") ||
|
|
52
|
+
index_path_name.endsWith("/index.html") ||
|
|
53
|
+
index_path_name.endsWith("/index.htm")
|
|
54
|
+
) {
|
|
55
|
+
index_path_name = index_path_name.substring(0, index_path_name.lastIndexOf("/"));
|
|
56
|
+
}
|
|
57
|
+
index_path_name = `/${index_path_name.replace(path.extname(file.relative_path), "")}`;
|
|
58
|
+
return file.index_html.id !== null
|
|
59
|
+
? `${index_path_name}#${file.index_html.id}`
|
|
60
|
+
: index_path_name;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Generate semantic search embeddings for every indexed document and
|
|
64
|
+
// write them to the hdoc_embeddings table. Non-fatal on failure - a book
|
|
65
|
+
// without embeddings remains fully searchable via keyword search.
|
|
66
|
+
exports.populate_embeddings = async (db, doc_id, index_records, verbose = false) => {
|
|
67
|
+
const response = {
|
|
68
|
+
success: false,
|
|
69
|
+
error: "",
|
|
70
|
+
doc_count: 0,
|
|
71
|
+
chunk_count: 0,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let transformers = null;
|
|
75
|
+
try {
|
|
76
|
+
// @huggingface/transformers is ESM-only, load it dynamically
|
|
77
|
+
transformers = await import("@huggingface/transformers");
|
|
78
|
+
} catch (e) {
|
|
79
|
+
response.error = `Could not load @huggingface/transformers: ${e.message}`;
|
|
80
|
+
return response;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Keep the model cache inside the package, like the puppeteer browser
|
|
84
|
+
transformers.env.cacheDir = path.join(__dirname, ".model-cache");
|
|
85
|
+
|
|
86
|
+
console.log(`\nGenerating semantic search embeddings (${MODEL_ID})...`);
|
|
87
|
+
|
|
88
|
+
let extractor = null;
|
|
89
|
+
try {
|
|
90
|
+
// fp32 to match the model the docs service runs at query time
|
|
91
|
+
extractor = await transformers.pipeline("feature-extraction", HF_MODEL, {
|
|
92
|
+
dtype: "fp32",
|
|
93
|
+
});
|
|
94
|
+
} catch (e) {
|
|
95
|
+
response.error = `Could not load embedding model ${HF_MODEL}: ${e.message}`;
|
|
96
|
+
return response;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Verify the downloaded/cached model file against the pinned hash
|
|
100
|
+
// before generating any embeddings
|
|
101
|
+
const model_file = path.join(transformers.env.cacheDir, HF_MODEL, "onnx", "model.onnx");
|
|
102
|
+
try {
|
|
103
|
+
const hash = crypto
|
|
104
|
+
.createHash("sha256")
|
|
105
|
+
.update(fs.readFileSync(model_file))
|
|
106
|
+
.digest("hex");
|
|
107
|
+
if (hash !== MODEL_SHA256) {
|
|
108
|
+
response.error = `Embedding model checksum mismatch for ${model_file}: got ${hash}, expected ${MODEL_SHA256}. Delete the .model-cache folder to re-download, or update MODEL_SHA256 if the pinned model has intentionally changed.`;
|
|
109
|
+
return response;
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
response.error = `Could not verify embedding model checksum: ${e.message}`;
|
|
113
|
+
return response;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Build the chunk list across all documents
|
|
117
|
+
const chunks = [];
|
|
118
|
+
for (const file of index_records) {
|
|
119
|
+
if (file.inline) continue;
|
|
120
|
+
if (file.status === "draft") continue;
|
|
121
|
+
if (!file.index_html || !file.index_html.text) continue;
|
|
122
|
+
|
|
123
|
+
const url = resource_url_for(file);
|
|
124
|
+
const title = (file.index_html.fm_props && file.index_html.fm_props.title) || "";
|
|
125
|
+
|
|
126
|
+
const doc_chunks = chunk_text(file.index_html.text);
|
|
127
|
+
if (doc_chunks.length === 0) continue;
|
|
128
|
+
|
|
129
|
+
response.doc_count++;
|
|
130
|
+
for (let seq = 0; seq < doc_chunks.length; seq++) {
|
|
131
|
+
chunks.push({
|
|
132
|
+
url: url,
|
|
133
|
+
seq: seq,
|
|
134
|
+
text: doc_chunks[seq],
|
|
135
|
+
// Title provides context for the embedding but is not
|
|
136
|
+
// duplicated into the stored snippet text
|
|
137
|
+
embed_input: title ? `${title}\n${doc_chunks[seq]}` : doc_chunks[seq],
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (chunks.length === 0) {
|
|
143
|
+
response.success = true;
|
|
144
|
+
console.log("No documents to embed.");
|
|
145
|
+
return response;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const stmt = db.prepare(
|
|
149
|
+
"INSERT INTO hdoc_embeddings (resource_url, chunk_seq, chunk_text, embedding, model, dims) VALUES (?, ?, ?, ?, ?, ?)",
|
|
150
|
+
);
|
|
151
|
+
const insert_batch = db.transaction((rows) => {
|
|
152
|
+
for (const row of rows) {
|
|
153
|
+
stmt.run(row.url, row.seq, row.text.substring(0, STORED_CHUNK_CHARS), row.embedding, MODEL_ID, DIMS);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const start_time = Date.now();
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
for (let i = 0; i < chunks.length; i += EMBED_BATCH_SIZE) {
|
|
161
|
+
const batch = chunks.slice(i, i + EMBED_BATCH_SIZE);
|
|
162
|
+
|
|
163
|
+
const output = await extractor(
|
|
164
|
+
batch.map((c) => c.embed_input),
|
|
165
|
+
{ pooling: "mean", normalize: true },
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// output is a [batch, DIMS] tensor over one flat Float32Array
|
|
169
|
+
const data = output.data;
|
|
170
|
+
const dims = output.dims[output.dims.length - 1];
|
|
171
|
+
if (dims !== DIMS) {
|
|
172
|
+
response.error = `Embedding model returned ${dims} dimensions, expected ${DIMS}`;
|
|
173
|
+
return response;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (let j = 0; j < batch.length; j++) {
|
|
177
|
+
// Copy the row out of the shared tensor buffer
|
|
178
|
+
const vec = new Float32Array(data.slice(j * DIMS, (j + 1) * DIMS));
|
|
179
|
+
batch[j].embedding = Buffer.from(vec.buffer, 0, DIMS * 4);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
insert_batch(batch);
|
|
183
|
+
response.chunk_count += batch.length;
|
|
184
|
+
|
|
185
|
+
if (verbose || i % (EMBED_BATCH_SIZE * 10) === 0) {
|
|
186
|
+
process.stdout.write(`\rEmbedding chunks: ${Math.min(i + EMBED_BATCH_SIZE, chunks.length)}/${chunks.length}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
response.error = `Embedding generation failed: ${e.message}`;
|
|
191
|
+
return response;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const secs = Math.round((Date.now() - start_time) / 1000);
|
|
195
|
+
console.log(
|
|
196
|
+
`\rEmbedding Build Complete: ${response.chunk_count} chunks across ${response.doc_count} documents in ${secs}s.`,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
response.success = true;
|
|
200
|
+
return response;
|
|
201
|
+
};
|
|
202
|
+
})();
|
package/hdoc-build.js
CHANGED
|
@@ -8,11 +8,22 @@
|
|
|
8
8
|
const hdoc_validate_config = require(path.join(__dirname, "hdoc-validate-config.js"));
|
|
9
9
|
const hdoc = require(path.join(__dirname, "hdoc-module.js"));
|
|
10
10
|
const hdoc_build_db = require(path.join(__dirname, "hdoc-build-db.js"));
|
|
11
|
+
const hdoc_build_embeddings = require(path.join(__dirname, "hdoc-build-embeddings.js"));
|
|
11
12
|
const hdoc_build_onyx = require(path.join(__dirname, "hdoc-build-onyx.js"));
|
|
12
13
|
const hdoc_build_pdf = require(path.join(__dirname, "hdoc-build-pdf.js"));
|
|
13
14
|
const hdoc_index = require(path.join(__dirname, "hdoc-db.js"));
|
|
14
15
|
const hdoc_mermaid = require(path.join(__dirname, "hdoc-mermaid.js"));
|
|
15
16
|
const archiver = require("archiver");
|
|
17
|
+
const TurndownService = require("turndown");
|
|
18
|
+
const turndown_gfm = require("turndown-plugin-gfm");
|
|
19
|
+
|
|
20
|
+
// HTML -> Markdown converter, used to populate the doc_md index column for
|
|
21
|
+
// static HTML sources (markdown sources store their raw markdown directly).
|
|
22
|
+
const turndown = new TurndownService({
|
|
23
|
+
headingStyle: "atx",
|
|
24
|
+
codeBlockStyle: "fenced",
|
|
25
|
+
});
|
|
26
|
+
turndown.use(turndown_gfm.gfm);
|
|
16
27
|
|
|
17
28
|
const h_tags_to_search = ["h1", "h2", "h3"];
|
|
18
29
|
const image_extensions = ["png", "svg", "jpg"];
|
|
@@ -267,6 +278,7 @@
|
|
|
267
278
|
const fm_headers = [];
|
|
268
279
|
let doc_title = "";
|
|
269
280
|
let doc_type = "Article";
|
|
281
|
+
let doc_md = "";
|
|
270
282
|
let fm_status = false;
|
|
271
283
|
// Used only for static HTML: tracks whether the source file already had a
|
|
272
284
|
// frontmatter comment block that needs replacing in the output.
|
|
@@ -295,6 +307,10 @@
|
|
|
295
307
|
}
|
|
296
308
|
}
|
|
297
309
|
|
|
310
|
+
// Raw markdown for the doc_md index column: full post-include content,
|
|
311
|
+
// minus the YAML frontmatter block.
|
|
312
|
+
doc_md = md_txt.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
|
|
313
|
+
|
|
298
314
|
// Point the shared md instance at the current file before rendering
|
|
299
315
|
// so the highlight callback logs and reports against the right path.
|
|
300
316
|
currentMdFilePath = file_path;
|
|
@@ -410,6 +426,15 @@
|
|
|
410
426
|
html_txt = fs.readFileSync(file_path.path, "utf8");
|
|
411
427
|
html_txt = html_txt.replace(/\r/gm, ""); // Remove CR's so we're just dealing with newlines
|
|
412
428
|
|
|
429
|
+
// Best-guess markdown conversion of the HTML source for the doc_md
|
|
430
|
+
// index column (turndown drops the frontmatter comment automatically)
|
|
431
|
+
try {
|
|
432
|
+
doc_md = turndown.turndown(html_txt).trim();
|
|
433
|
+
} catch (e) {
|
|
434
|
+
console.error(`[WARNING] HTML to Markdown conversion failed for ${file_path.relativePath}: ${e}`);
|
|
435
|
+
doc_md = "";
|
|
436
|
+
}
|
|
437
|
+
|
|
413
438
|
// Check if we have a frontmatter comment
|
|
414
439
|
html_fm = hdoc.getHTMLFrontmatterHeader(html_txt);
|
|
415
440
|
|
|
@@ -797,6 +822,7 @@
|
|
|
797
822
|
inline: inline_content,
|
|
798
823
|
status: index_data.fm_props.status ? index_data.fm_props.status : 'release',
|
|
799
824
|
keywords: index_data.fm_props.keywords ? index_data.fm_props.keywords : '',
|
|
825
|
+
doc_md: doc_md,
|
|
800
826
|
});
|
|
801
827
|
}
|
|
802
828
|
|
|
@@ -1041,6 +1067,7 @@
|
|
|
1041
1067
|
gen_exclude,
|
|
1042
1068
|
build_version = "",
|
|
1043
1069
|
output_links = true,
|
|
1070
|
+
build_embeddings = true,
|
|
1044
1071
|
) => {
|
|
1045
1072
|
if (github_api_token !== "") {
|
|
1046
1073
|
git_token = github_api_token;
|
|
@@ -1533,6 +1560,21 @@
|
|
|
1533
1560
|
}
|
|
1534
1561
|
}
|
|
1535
1562
|
|
|
1563
|
+
// Generate semantic search embeddings into the index. A failure here is
|
|
1564
|
+
// not fatal - the book simply ships without semantic search coverage
|
|
1565
|
+
if (!validate && build_embeddings) {
|
|
1566
|
+
const embeddings = await hdoc_build_embeddings.populate_embeddings(
|
|
1567
|
+
db.db,
|
|
1568
|
+
doc_id,
|
|
1569
|
+
index_records,
|
|
1570
|
+
verbose,
|
|
1571
|
+
);
|
|
1572
|
+
if (!embeddings.success) {
|
|
1573
|
+
console.error(`\nWARNING: Semantic embeddings not generated: ${embeddings.error}`);
|
|
1574
|
+
console.error("The book will be keyword-searchable only. Use --no-embeddings to silence this warning.");
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1536
1578
|
if (!validate) {
|
|
1537
1579
|
try {
|
|
1538
1580
|
const zip_path = path.join(work_path, `${doc_id}.zip`);
|
package/hdoc-bump.js
CHANGED
|
@@ -1,123 +1,123 @@
|
|
|
1
|
-
(() => {
|
|
2
|
-
const fs = require("node:fs");
|
|
3
|
-
const path = require("node:path");
|
|
4
|
-
|
|
5
|
-
exports.run = (source_path, bump_type) => {
|
|
6
|
-
if (
|
|
7
|
-
bump_type !== "patch" &&
|
|
8
|
-
bump_type !== "minor" &&
|
|
9
|
-
bump_type !== "major"
|
|
10
|
-
) {
|
|
11
|
-
console.error(`Unsupported bump type: ${bump_type}`);
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
14
|
-
console.log(`Bumping ${bump_type} book version...\n`);
|
|
15
|
-
|
|
16
|
-
// Get document ID
|
|
17
|
-
const hdocbook_project_config_path = path.join(
|
|
18
|
-
source_path,
|
|
19
|
-
"hdocbook-project.json",
|
|
20
|
-
);
|
|
21
|
-
let hdocbook_project;
|
|
22
|
-
try {
|
|
23
|
-
hdocbook_project = require(hdocbook_project_config_path);
|
|
24
|
-
} catch (e) {
|
|
25
|
-
console.error("File not found: hdocbook-project.json:");
|
|
26
|
-
console.error(e, "\n");
|
|
27
|
-
console.error("hdoc bump needs to be run in the root of a HDoc Book.\n");
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
const doc_id = hdocbook_project.docId;
|
|
31
|
-
|
|
32
|
-
const book_path = path.join(source_path, doc_id);
|
|
33
|
-
const hdocbook_path = path.join(book_path, "hdocbook.json");
|
|
34
|
-
|
|
35
|
-
let hdocbook_config;
|
|
36
|
-
try {
|
|
37
|
-
hdocbook_config = require(hdocbook_path);
|
|
38
|
-
} catch (e) {
|
|
39
|
-
console.error("File not found: hdocbook.json");
|
|
40
|
-
console.error(e, "\n");
|
|
41
|
-
console.error("hdoc bump needs to be run in the root of a HDoc Book.\n");
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
const initial_version = hdocbook_config.version;
|
|
45
|
-
const hdocbook_version = hdocbook_config.version.split(".");
|
|
46
|
-
if (hdocbook_version.length !== 3) {
|
|
47
|
-
console.error(
|
|
48
|
-
`Book version does not appear to be in a semantic versioning format: ${initial_version}`,
|
|
49
|
-
);
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (Number.isNaN(hdocbook_version[0])) {
|
|
54
|
-
console.error(
|
|
55
|
-
`Existing major version is not a number: ${hdocbook_version[0]}`,
|
|
56
|
-
);
|
|
57
|
-
process.exit(1);
|
|
58
|
-
}
|
|
59
|
-
if (Number.isNaN(hdocbook_version[1])) {
|
|
60
|
-
console.error(
|
|
61
|
-
`Existing minor version is not a number: ${hdocbook_version[1]}`,
|
|
62
|
-
);
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
if (Number.isNaN(hdocbook_version[2])) {
|
|
66
|
-
console.error(
|
|
67
|
-
`Existing patch version is not a number: ${hdocbook_version[2]}`,
|
|
68
|
-
);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
switch (bump_type) {
|
|
73
|
-
case "major":
|
|
74
|
-
try {
|
|
75
|
-
hdocbook_version[0] = Number.parseInt(hdocbook_version[0], 10) + 1;
|
|
76
|
-
hdocbook_version[1] = 0;
|
|
77
|
-
hdocbook_version[2] = 0;
|
|
78
|
-
} catch (e) {
|
|
79
|
-
console.error("Failed to update major version:");
|
|
80
|
-
console.error(e);
|
|
81
|
-
process.exit(1);
|
|
82
|
-
}
|
|
83
|
-
break;
|
|
84
|
-
case "minor":
|
|
85
|
-
try {
|
|
86
|
-
hdocbook_version[0] = Number.parseInt(hdocbook_version[0], 10);
|
|
87
|
-
hdocbook_version[1] = Number.parseInt(hdocbook_version[1], 10) + 1;
|
|
88
|
-
hdocbook_version[2] = 0;
|
|
89
|
-
} catch (e) {
|
|
90
|
-
console.error("Failed to update minor version:");
|
|
91
|
-
console.error(e);
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
94
|
-
break;
|
|
95
|
-
default:
|
|
96
|
-
//case "patch" catered for with this default
|
|
97
|
-
try {
|
|
98
|
-
hdocbook_version[0] = Number.parseInt(hdocbook_version[0], 10);
|
|
99
|
-
hdocbook_version[1] = Number.parseInt(hdocbook_version[1], 10);
|
|
100
|
-
hdocbook_version[2] = Number.parseInt(hdocbook_version[2], 10) + 1;
|
|
101
|
-
} catch (e) {
|
|
102
|
-
console.error("Failed to update patch version:");
|
|
103
|
-
console.error(e);
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
hdocbook_config.version = hdocbook_version.join(".");
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
fs.writeFileSync(hdocbook_path, JSON.stringify(hdocbook_config, null, 2));
|
|
113
|
-
} catch (e) {
|
|
114
|
-
console.error("Error writing bumped version to book config:", e);
|
|
115
|
-
process.exit(1);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
console.log(
|
|
119
|
-
`Book version updated from ${initial_version} to ${hdocbook_config.version}\n`,
|
|
120
|
-
);
|
|
121
|
-
return true;
|
|
122
|
-
};
|
|
123
|
-
})();
|
|
1
|
+
(() => {
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
|
|
5
|
+
exports.run = (source_path, bump_type) => {
|
|
6
|
+
if (
|
|
7
|
+
bump_type !== "patch" &&
|
|
8
|
+
bump_type !== "minor" &&
|
|
9
|
+
bump_type !== "major"
|
|
10
|
+
) {
|
|
11
|
+
console.error(`Unsupported bump type: ${bump_type}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
console.log(`Bumping ${bump_type} book version...\n`);
|
|
15
|
+
|
|
16
|
+
// Get document ID
|
|
17
|
+
const hdocbook_project_config_path = path.join(
|
|
18
|
+
source_path,
|
|
19
|
+
"hdocbook-project.json",
|
|
20
|
+
);
|
|
21
|
+
let hdocbook_project;
|
|
22
|
+
try {
|
|
23
|
+
hdocbook_project = require(hdocbook_project_config_path);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error("File not found: hdocbook-project.json:");
|
|
26
|
+
console.error(e, "\n");
|
|
27
|
+
console.error("hdoc bump needs to be run in the root of a HDoc Book.\n");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const doc_id = hdocbook_project.docId;
|
|
31
|
+
|
|
32
|
+
const book_path = path.join(source_path, doc_id);
|
|
33
|
+
const hdocbook_path = path.join(book_path, "hdocbook.json");
|
|
34
|
+
|
|
35
|
+
let hdocbook_config;
|
|
36
|
+
try {
|
|
37
|
+
hdocbook_config = require(hdocbook_path);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error("File not found: hdocbook.json");
|
|
40
|
+
console.error(e, "\n");
|
|
41
|
+
console.error("hdoc bump needs to be run in the root of a HDoc Book.\n");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const initial_version = hdocbook_config.version;
|
|
45
|
+
const hdocbook_version = hdocbook_config.version.split(".");
|
|
46
|
+
if (hdocbook_version.length !== 3) {
|
|
47
|
+
console.error(
|
|
48
|
+
`Book version does not appear to be in a semantic versioning format: ${initial_version}`,
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (Number.isNaN(hdocbook_version[0])) {
|
|
54
|
+
console.error(
|
|
55
|
+
`Existing major version is not a number: ${hdocbook_version[0]}`,
|
|
56
|
+
);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
if (Number.isNaN(hdocbook_version[1])) {
|
|
60
|
+
console.error(
|
|
61
|
+
`Existing minor version is not a number: ${hdocbook_version[1]}`,
|
|
62
|
+
);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
if (Number.isNaN(hdocbook_version[2])) {
|
|
66
|
+
console.error(
|
|
67
|
+
`Existing patch version is not a number: ${hdocbook_version[2]}`,
|
|
68
|
+
);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
switch (bump_type) {
|
|
73
|
+
case "major":
|
|
74
|
+
try {
|
|
75
|
+
hdocbook_version[0] = Number.parseInt(hdocbook_version[0], 10) + 1;
|
|
76
|
+
hdocbook_version[1] = 0;
|
|
77
|
+
hdocbook_version[2] = 0;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.error("Failed to update major version:");
|
|
80
|
+
console.error(e);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case "minor":
|
|
85
|
+
try {
|
|
86
|
+
hdocbook_version[0] = Number.parseInt(hdocbook_version[0], 10);
|
|
87
|
+
hdocbook_version[1] = Number.parseInt(hdocbook_version[1], 10) + 1;
|
|
88
|
+
hdocbook_version[2] = 0;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.error("Failed to update minor version:");
|
|
91
|
+
console.error(e);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
default:
|
|
96
|
+
//case "patch" catered for with this default
|
|
97
|
+
try {
|
|
98
|
+
hdocbook_version[0] = Number.parseInt(hdocbook_version[0], 10);
|
|
99
|
+
hdocbook_version[1] = Number.parseInt(hdocbook_version[1], 10);
|
|
100
|
+
hdocbook_version[2] = Number.parseInt(hdocbook_version[2], 10) + 1;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error("Failed to update patch version:");
|
|
103
|
+
console.error(e);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
hdocbook_config.version = hdocbook_version.join(".");
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
fs.writeFileSync(hdocbook_path, JSON.stringify(hdocbook_config, null, 2));
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.error("Error writing bumped version to book config:", e);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(
|
|
119
|
+
`Book version updated from ${initial_version} to ${hdocbook_config.version}\n`,
|
|
120
|
+
);
|
|
121
|
+
return true;
|
|
122
|
+
};
|
|
123
|
+
})();
|
package/hdoc-db.js
CHANGED
|
@@ -94,6 +94,13 @@
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// Page chrome - the document-header block (breadcrumb bar, edit link,
|
|
98
|
+
// title, "Article / date / N minutes to read / N contributors") is
|
|
99
|
+
// rendered into every page and would otherwise pollute search index
|
|
100
|
+
// term frequencies and embedding chunks. The title is indexed
|
|
101
|
+
// separately from frontmatter, so nothing of value is lost.
|
|
102
|
+
$(".document-header").remove();
|
|
103
|
+
|
|
97
104
|
// Full-document plain text for search indexing
|
|
98
105
|
const text = $("body").text();
|
|
99
106
|
|
package/hdoc-help.js
CHANGED
|
@@ -13,6 +13,7 @@ Commands
|
|
|
13
13
|
- Use the '--set-version 1.2.3' argument to set the version number of the built book.
|
|
14
14
|
- Use the '--no-color' argument to remove any color control characters from the output.
|
|
15
15
|
- Use the '--no-links' argument to skip link output to CLI during validation.
|
|
16
|
+
- Use the '--no-embeddings' argument to skip semantic search embedding generation.
|
|
16
17
|
|
|
17
18
|
- createDocs
|
|
18
19
|
Creates folder structure and markdown documents as defined in the HDocBook navigation item links
|
package/hdoc-validate-config.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const hdocbook_schema = require(path.join(__dirname, "schemas", "hdocbook.schema.json"));
|
|
7
7
|
const valid_audience = hdocbook_schema.properties.audience.items.enum;
|
|
8
8
|
const valid_product_families = hdocbook_schema.properties.productFamily.enum;
|
|
9
|
+
const valid_book_types = hdocbook_schema.properties.bookType.enum;
|
|
9
10
|
|
|
10
11
|
const project_schema = require(path.join(__dirname, "schemas", "hdocbook-project.schema.json"));
|
|
11
12
|
const project_top_keys = Object.keys(project_schema.properties);
|
|
@@ -192,7 +193,7 @@
|
|
|
192
193
|
const file = 'hdocbook.json';
|
|
193
194
|
|
|
194
195
|
check_extra_keys(config,
|
|
195
|
-
['docId', 'title', 'description', 'publicSource', 'version', 'productFamily',
|
|
196
|
+
['docId', 'title', 'description', 'publicSource', 'version', 'productFamily', 'bookType',
|
|
196
197
|
'coverImage', 'tags', 'audience', 'languages', 'readingTime', 'navigation', 'inline'],
|
|
197
198
|
'', file, errors);
|
|
198
199
|
|
|
@@ -267,6 +268,10 @@
|
|
|
267
268
|
}
|
|
268
269
|
}
|
|
269
270
|
|
|
271
|
+
if (config.bookType !== undefined && !valid_book_types.includes(config.bookType)) {
|
|
272
|
+
errors.push(`${file}: "bookType" must be an integer, one of: ${valid_book_types.join(', ')} (0=document, 1=api_ref, 2=db_ref, 3=etl_ref, 4=mcp_ref)`);
|
|
273
|
+
}
|
|
274
|
+
|
|
270
275
|
if (config.coverImage !== undefined && typeof config.coverImage !== 'string') {
|
|
271
276
|
errors.push(`${file}: "coverImage" must be a string`);
|
|
272
277
|
}
|