polydeukes 0.5.0 → 0.6.1
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.ko.md +54 -74
- package/README.md +55 -86
- package/dist/baseline.d.ts +82 -0
- package/dist/baseline.js +166 -0
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +26 -6
- package/dist/claude-code-hook.d.ts +7 -5
- package/dist/claude-code-hook.js +69 -17
- package/dist/claude-code.d.ts +6 -0
- package/dist/claude-code.js +6 -0
- package/dist/covenant-check.d.ts +10 -17
- package/dist/covenant-check.js +44 -12
- package/dist/covenant-module.d.ts +2 -2
- package/dist/covenant-module.js +9 -1
- package/dist/docs/README.ko.md +60 -0
- package/dist/docs/README.md +64 -0
- package/dist/docs/catalog.json +464 -0
- package/dist/docs/concepts/judgment.ko.md +113 -0
- package/dist/docs/concepts/judgment.md +113 -0
- package/dist/docs/how-to/configure-project.ko.md +99 -0
- package/dist/docs/how-to/configure-project.md +95 -0
- package/dist/docs/how-to/connect-surfaces.ko.md +115 -0
- package/dist/docs/how-to/connect-surfaces.md +118 -0
- package/dist/docs/how-to/write-disciplines.ko.md +124 -0
- package/dist/docs/how-to/write-disciplines.md +125 -0
- package/dist/docs/index.json +2046 -0
- package/dist/docs/reference/cli/covenant-check.ko.md +101 -0
- package/dist/docs/reference/cli/covenant-check.md +98 -0
- package/dist/docs/reference/cli/docs.ko.md +97 -0
- package/dist/docs/reference/cli/docs.md +95 -0
- package/dist/docs/reference/cli/explain.ko.md +79 -0
- package/dist/docs/reference/cli/explain.md +84 -0
- package/dist/docs/reference/cli/init.ko.md +119 -0
- package/dist/docs/reference/cli/init.md +131 -0
- package/dist/docs/reference/configuration/index.ko.md +448 -0
- package/dist/docs/reference/configuration/index.md +474 -0
- package/dist/docs/reference/packages/adapter-claude-code.ko.md +83 -0
- package/dist/docs/reference/{adapter-claude-code.md → packages/adapter-claude-code.md} +16 -10
- package/dist/docs/reference/packages/adapter-git.ko.md +101 -0
- package/dist/docs/reference/{adapter-git.md → packages/adapter-git.md} +25 -15
- package/dist/docs/reference/packages/core.ko.md +128 -0
- package/dist/docs/reference/{core.md → packages/core.md} +32 -13
- package/dist/docs/reference/packages/covenant.ko.md +115 -0
- package/dist/docs/reference/{covenant.md → packages/covenant.md} +41 -26
- package/dist/docs/reference/packages/polydeukes.ko.md +134 -0
- package/dist/docs/reference/packages/polydeukes.md +139 -0
- package/dist/docs/troubleshooting.ko.md +142 -0
- package/dist/docs/troubleshooting.md +97 -122
- package/dist/docs/tutorials/first-judgment.ko.md +82 -0
- package/dist/docs/tutorials/first-judgment.md +81 -0
- package/dist/docs-catalog.d.ts +25 -0
- package/dist/docs-catalog.js +450 -0
- package/dist/docs-library.d.ts +23 -0
- package/dist/docs-library.js +347 -0
- package/dist/docs-markdown.d.ts +32 -0
- package/dist/docs-markdown.js +150 -0
- package/dist/docs-query.d.ts +11 -40
- package/dist/docs-query.js +28 -122
- package/dist/docs-types.d.ts +105 -0
- package/dist/docs-types.js +2 -0
- package/dist/explain.d.ts +3 -5
- package/dist/explain.js +48 -47
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -2
- package/dist/init-claude-code.d.ts +5 -3
- package/dist/init-claude-code.js +226 -63
- package/dist/init-grok.d.ts +51 -0
- package/dist/init-grok.js +242 -0
- package/dist/load-config.d.ts +5 -1
- package/dist/load-config.js +2 -1
- package/dist/pre-state-reader.d.ts +22 -0
- package/dist/pre-state-reader.js +32 -0
- package/dist/scaffold-project.js +48 -8
- package/dist/schema/polydeukes.schema.json +38 -91
- package/package.json +7 -7
- package/dist/docs/configuration.md +0 -103
- package/dist/docs/installation.md +0 -212
- package/dist/docs/reference/configuration.md +0 -338
- package/dist/docs/reference/polydeukes.md +0 -287
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
import { hashMarkdown, parseSections } from './docs-markdown.js';
|
|
5
|
+
const catalogSchemaVersion = 1;
|
|
6
|
+
const supportedLanguages = ['en', 'ko'];
|
|
7
|
+
const markdownPattern = /\.md$/i;
|
|
8
|
+
function fail(message) {
|
|
9
|
+
throw new Error(message);
|
|
10
|
+
}
|
|
11
|
+
function isPlainObject(value) {
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function assertString(value, message) {
|
|
15
|
+
if (typeof value !== 'string' || value === '')
|
|
16
|
+
fail(message);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function assertOptionalTerms(value, message) {
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string' || entry === '')) {
|
|
23
|
+
fail(message);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function assertTranslation(value, label) {
|
|
28
|
+
if (!isPlainObject(value))
|
|
29
|
+
fail(`invalid ${label} translation`);
|
|
30
|
+
return {
|
|
31
|
+
path: assertString(value.path, `invalid ${label} path`),
|
|
32
|
+
title: assertString(value.title, `invalid ${label} title`),
|
|
33
|
+
summary: assertString(value.summary, `invalid ${label} summary`),
|
|
34
|
+
terms: assertOptionalTerms(value.terms, `invalid ${label} terms`),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function assertReference(value) {
|
|
38
|
+
if (!isPlainObject(value))
|
|
39
|
+
fail('invalid topic reference');
|
|
40
|
+
const documentId = assertString(value.documentId, 'invalid topic documentId');
|
|
41
|
+
const sectionId = value.sectionId;
|
|
42
|
+
if (sectionId !== undefined && (typeof sectionId !== 'string' || sectionId === '')) {
|
|
43
|
+
fail('invalid topic sectionId');
|
|
44
|
+
}
|
|
45
|
+
return { documentId, ...(sectionId === undefined ? {} : { sectionId }) };
|
|
46
|
+
}
|
|
47
|
+
function assertDocument(value) {
|
|
48
|
+
if (!isPlainObject(value))
|
|
49
|
+
fail('invalid document entry');
|
|
50
|
+
if (typeof value.bundled !== 'boolean')
|
|
51
|
+
fail('invalid bundled flag');
|
|
52
|
+
if (typeof value.order !== 'number')
|
|
53
|
+
fail('invalid document order');
|
|
54
|
+
const document = {
|
|
55
|
+
id: assertString(value.id, 'invalid document id'),
|
|
56
|
+
category: assertString(value.category, 'invalid document category'),
|
|
57
|
+
order: Number(value.order),
|
|
58
|
+
bundled: value.bundled === true,
|
|
59
|
+
en: assertTranslation(value.en, 'en'),
|
|
60
|
+
};
|
|
61
|
+
if (!Number.isInteger(document.order))
|
|
62
|
+
fail(`invalid order for ${document.id}`);
|
|
63
|
+
if (value.ko !== undefined)
|
|
64
|
+
document.ko = assertTranslation(value.ko, 'ko');
|
|
65
|
+
return document;
|
|
66
|
+
}
|
|
67
|
+
function assertCatalog(value) {
|
|
68
|
+
if (!isPlainObject(value))
|
|
69
|
+
fail('invalid catalog json');
|
|
70
|
+
const schemaVersion = value.schemaVersion;
|
|
71
|
+
if (schemaVersion !== catalogSchemaVersion)
|
|
72
|
+
fail('unsupported catalog schema version');
|
|
73
|
+
if (!Array.isArray(value.documents))
|
|
74
|
+
fail('invalid documents list');
|
|
75
|
+
if (!isPlainObject(value.topics))
|
|
76
|
+
fail('invalid topics map');
|
|
77
|
+
const redirects = value.redirects === undefined ? [] : value.redirects;
|
|
78
|
+
if (!Array.isArray(redirects))
|
|
79
|
+
fail('invalid redirects list');
|
|
80
|
+
const catalog = {
|
|
81
|
+
schemaVersion: catalogSchemaVersion,
|
|
82
|
+
documents: value.documents.map(assertDocument),
|
|
83
|
+
topics: Object.fromEntries(Object.entries(value.topics).map(([topicId, topic]) => {
|
|
84
|
+
if (!isPlainObject(topic))
|
|
85
|
+
fail(`invalid topic ${topicId}`);
|
|
86
|
+
return [
|
|
87
|
+
topicId,
|
|
88
|
+
{
|
|
89
|
+
references: Array.isArray(topic.references)
|
|
90
|
+
? topic.references.map(assertReference)
|
|
91
|
+
: fail(`invalid topic references for ${topicId}`),
|
|
92
|
+
seeAlso: assertString(topic.seeAlso, `invalid seeAlso for ${topicId}`),
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
})),
|
|
96
|
+
redirects: redirects.map((entry) => {
|
|
97
|
+
if (!isPlainObject(entry))
|
|
98
|
+
fail('invalid redirect entry');
|
|
99
|
+
return {
|
|
100
|
+
path: assertString(entry.path, 'invalid redirect path'),
|
|
101
|
+
target: assertString(entry.target, 'invalid redirect target'),
|
|
102
|
+
};
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
return catalog;
|
|
106
|
+
}
|
|
107
|
+
function safeRelativePath(path) {
|
|
108
|
+
if (isAbsolute(path) || path.includes('\\') || /^[A-Za-z]:/.test(path)) {
|
|
109
|
+
fail(`absolute or non-portable path rejected: ${path}`);
|
|
110
|
+
}
|
|
111
|
+
const segments = path.split('/');
|
|
112
|
+
if (segments.some((segment) => segment === '' || segment === '.' || segment === '..')) {
|
|
113
|
+
fail(`escaping path rejected: ${path}`);
|
|
114
|
+
}
|
|
115
|
+
const normalized = normalize(path).replace(/\\/g, '/');
|
|
116
|
+
if (normalized === '.' || normalized === '')
|
|
117
|
+
fail('invalid empty path');
|
|
118
|
+
return normalized;
|
|
119
|
+
}
|
|
120
|
+
function physicalPath(path) {
|
|
121
|
+
const absolute = resolve(path);
|
|
122
|
+
if (existsSync(absolute))
|
|
123
|
+
return realpathSync(absolute);
|
|
124
|
+
const parent = dirname(absolute);
|
|
125
|
+
if (parent === absolute)
|
|
126
|
+
return absolute;
|
|
127
|
+
return join(physicalPath(parent), relative(parent, absolute));
|
|
128
|
+
}
|
|
129
|
+
function contains(root, path) {
|
|
130
|
+
const distance = relative(root, path);
|
|
131
|
+
return distance !== '..' && !distance.startsWith(`..${sep}`) && !isAbsolute(distance);
|
|
132
|
+
}
|
|
133
|
+
function validateDocsRootOverlap(sourceRoot, outputRoot) {
|
|
134
|
+
const source = physicalPath(sourceRoot);
|
|
135
|
+
const output = physicalPath(outputRoot);
|
|
136
|
+
if (contains(source, output) || contains(output, source))
|
|
137
|
+
fail('source and output roots overlap');
|
|
138
|
+
}
|
|
139
|
+
function walkMarkdown(root) {
|
|
140
|
+
const result = [];
|
|
141
|
+
const stack = [root];
|
|
142
|
+
while (stack.length > 0) {
|
|
143
|
+
const current = stack.pop();
|
|
144
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
145
|
+
const path = join(current, entry.name);
|
|
146
|
+
if (entry.isDirectory())
|
|
147
|
+
stack.push(path);
|
|
148
|
+
else if (markdownPattern.test(entry.name))
|
|
149
|
+
result.push(relative(root, path).replace(/\\/g, '/'));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return result.sort((left, right) => left.localeCompare(right));
|
|
153
|
+
}
|
|
154
|
+
function readMarkdown(root, path) {
|
|
155
|
+
const absolute = resolve(root, path);
|
|
156
|
+
if (!contains(resolve(root), absolute))
|
|
157
|
+
fail(`path escapes source root: ${path}`);
|
|
158
|
+
if (!existsSync(absolute))
|
|
159
|
+
fail(`missing source markdown: ${path}`);
|
|
160
|
+
if (!contains(realpathSync(root), realpathSync(absolute)))
|
|
161
|
+
fail(`source symlink escapes root: ${path}`);
|
|
162
|
+
return readFileSync(absolute, 'utf8');
|
|
163
|
+
}
|
|
164
|
+
function validateDocumentPaths(document) {
|
|
165
|
+
for (const [language, translation] of Object.entries({ en: document.en, ko: document.ko })) {
|
|
166
|
+
if (!translation)
|
|
167
|
+
fail(`missing ${language} translation for ${document.id}`);
|
|
168
|
+
safeRelativePath(translation.path);
|
|
169
|
+
if (!translation.path.endsWith(language === 'en' ? '.md' : '.ko.md') ||
|
|
170
|
+
(language === 'en' && translation.path.endsWith('.ko.md'))) {
|
|
171
|
+
fail(`translation path suffix mismatch for ${document.id}:${language}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function validateDocumentIds(documents) {
|
|
176
|
+
const ids = new Set();
|
|
177
|
+
for (const document of documents) {
|
|
178
|
+
if (ids.has(document.id))
|
|
179
|
+
fail(`duplicate document id: ${document.id}`);
|
|
180
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(document.id)) {
|
|
181
|
+
fail(`invalid document id: ${document.id}`);
|
|
182
|
+
}
|
|
183
|
+
ids.add(document.id);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function validateTopicMap(catalog) {
|
|
187
|
+
for (const [topicId, topic] of Object.entries(catalog.topics)) {
|
|
188
|
+
if (topic.references.length === 0)
|
|
189
|
+
fail(`empty topic references: ${topicId}`);
|
|
190
|
+
if (typeof topic.seeAlso !== 'string' || topic.seeAlso === '')
|
|
191
|
+
fail(`invalid seeAlso: ${topicId}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function validateRedirects(catalog) {
|
|
195
|
+
const paths = new Set();
|
|
196
|
+
for (const redirect of catalog.redirects ?? []) {
|
|
197
|
+
safeRelativePath(redirect.path);
|
|
198
|
+
safeRelativePath(redirect.target);
|
|
199
|
+
if (paths.has(redirect.path))
|
|
200
|
+
fail(`duplicate redirect path: ${redirect.path}`);
|
|
201
|
+
paths.add(redirect.path);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function assertAllSourceMarkdownRegistered(catalog, sourceRoot) {
|
|
205
|
+
const registered = new Set();
|
|
206
|
+
const register = (path) => {
|
|
207
|
+
const safe = safeRelativePath(path);
|
|
208
|
+
if (registered.has(safe))
|
|
209
|
+
fail(`duplicate document path: ${path}`);
|
|
210
|
+
registered.add(safe);
|
|
211
|
+
};
|
|
212
|
+
for (const document of catalog.documents) {
|
|
213
|
+
register(document.en.path);
|
|
214
|
+
if (document.ko)
|
|
215
|
+
register(document.ko.path);
|
|
216
|
+
}
|
|
217
|
+
for (const redirect of catalog.redirects ?? []) {
|
|
218
|
+
if (!registered.has(redirect.target))
|
|
219
|
+
fail(`unknown redirect target: ${redirect.target}`);
|
|
220
|
+
readMarkdown(sourceRoot, redirect.path);
|
|
221
|
+
}
|
|
222
|
+
for (const redirect of catalog.redirects ?? [])
|
|
223
|
+
register(redirect.path);
|
|
224
|
+
for (const path of walkMarkdown(sourceRoot)) {
|
|
225
|
+
if (!registered.has(safeRelativePath(path))) {
|
|
226
|
+
fail(`unregistered source markdown: ${path}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function validateTopicsAgainstDocuments(catalog) {
|
|
231
|
+
const ids = new Set(catalog.documents.filter((document) => document.bundled).map((document) => document.id));
|
|
232
|
+
for (const [topicId, topic] of Object.entries(catalog.topics)) {
|
|
233
|
+
if (!ids.has(topic.seeAlso))
|
|
234
|
+
fail(`unknown seeAlso for ${topicId}: ${topic.seeAlso}`);
|
|
235
|
+
for (const reference of topic.references) {
|
|
236
|
+
if (!ids.has(reference.documentId))
|
|
237
|
+
fail(`unknown topic reference for ${topicId}: ${reference.documentId}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function validateBundledDocument(document, sourceRoot) {
|
|
242
|
+
const enMarkdown = readMarkdown(sourceRoot, document.en.path);
|
|
243
|
+
const ko = document.ko;
|
|
244
|
+
if (!ko)
|
|
245
|
+
fail(`missing ko translation for ${document.id}`);
|
|
246
|
+
const koMarkdown = readMarkdown(sourceRoot, ko.path);
|
|
247
|
+
const enSections = document.bundled ? parseSections(enMarkdown, { requireAnchors: true }) : [];
|
|
248
|
+
const koSections = document.bundled ? parseSections(koMarkdown, { requireAnchors: true }) : [];
|
|
249
|
+
if (document.bundled) {
|
|
250
|
+
const enIds = enSections.map((section) => section.id).sort();
|
|
251
|
+
const koIds = koSections.map((section) => section.id).sort();
|
|
252
|
+
if (enIds.length !== koIds.length || enIds.some((id, index) => id !== koIds[index])) {
|
|
253
|
+
fail(`bundled section set mismatch for ${document.id}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
id: document.id,
|
|
258
|
+
bundled: document.bundled,
|
|
259
|
+
category: document.category,
|
|
260
|
+
order: document.order,
|
|
261
|
+
translations: {
|
|
262
|
+
en: {
|
|
263
|
+
...document.en,
|
|
264
|
+
terms: document.en.terms ?? [],
|
|
265
|
+
markdown: enMarkdown,
|
|
266
|
+
hash: hashMarkdown(enMarkdown),
|
|
267
|
+
sections: enSections,
|
|
268
|
+
},
|
|
269
|
+
ko: {
|
|
270
|
+
...ko,
|
|
271
|
+
terms: ko.terms ?? [],
|
|
272
|
+
markdown: koMarkdown,
|
|
273
|
+
hash: hashMarkdown(koMarkdown),
|
|
274
|
+
sections: koSections,
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function buildIndexDocument(document) {
|
|
280
|
+
return {
|
|
281
|
+
id: document.id,
|
|
282
|
+
bundled: document.bundled,
|
|
283
|
+
category: document.category,
|
|
284
|
+
order: document.order,
|
|
285
|
+
translations: {
|
|
286
|
+
en: {
|
|
287
|
+
path: document.translations.en.path,
|
|
288
|
+
title: document.translations.en.title,
|
|
289
|
+
summary: document.translations.en.summary,
|
|
290
|
+
...(document.translations.en.terms.length > 0
|
|
291
|
+
? { terms: document.translations.en.terms }
|
|
292
|
+
: {}),
|
|
293
|
+
},
|
|
294
|
+
ko: {
|
|
295
|
+
path: document.translations.ko.path,
|
|
296
|
+
title: document.translations.ko.title,
|
|
297
|
+
summary: document.translations.ko.summary,
|
|
298
|
+
...(document.translations.ko.terms.length > 0
|
|
299
|
+
? { terms: document.translations.ko.terms }
|
|
300
|
+
: {}),
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
hashes: {
|
|
304
|
+
en: document.translations.en.hash,
|
|
305
|
+
ko: document.translations.ko.hash,
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function buildIndexSections(document) {
|
|
310
|
+
if (!document.bundled)
|
|
311
|
+
return [];
|
|
312
|
+
const sections = [];
|
|
313
|
+
for (const language of supportedLanguages) {
|
|
314
|
+
const translation = document.translations[language];
|
|
315
|
+
for (const section of translation.sections) {
|
|
316
|
+
sections.push({
|
|
317
|
+
documentId: document.id,
|
|
318
|
+
language,
|
|
319
|
+
sectionId: section.id,
|
|
320
|
+
title: section.title,
|
|
321
|
+
level: section.level,
|
|
322
|
+
path: translation.path,
|
|
323
|
+
hash: translation.hash,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return sections;
|
|
328
|
+
}
|
|
329
|
+
function copyBundleDocuments(outputRoot, documents) {
|
|
330
|
+
for (const document of documents.values()) {
|
|
331
|
+
for (const language of supportedLanguages) {
|
|
332
|
+
const translation = document.translations[language];
|
|
333
|
+
if (!document.bundled)
|
|
334
|
+
continue;
|
|
335
|
+
const destination = resolve(outputRoot, translation.path);
|
|
336
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
337
|
+
writeFileSync(destination, translation.markdown, 'utf8');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function validateTopicSections(catalog, documents) {
|
|
342
|
+
for (const [topic, entry] of Object.entries(catalog.topics)) {
|
|
343
|
+
for (const reference of entry.references) {
|
|
344
|
+
const document = documents.find((candidate) => candidate.id === reference.documentId);
|
|
345
|
+
if (!document?.bundled)
|
|
346
|
+
fail(`topic ${topic} references an excluded document`);
|
|
347
|
+
if (reference.sectionId !== undefined &&
|
|
348
|
+
!document.translations.en.sections.some((section) => section.id === reference.sectionId))
|
|
349
|
+
fail(`unknown topic section: ${topic}/${reference.sectionId}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function createIndex(documents) {
|
|
354
|
+
const bundled = documents.filter((document) => document.bundled);
|
|
355
|
+
return {
|
|
356
|
+
schemaVersion: catalogSchemaVersion,
|
|
357
|
+
documents: bundled
|
|
358
|
+
.sort((left, right) => left.order - right.order || compare(left.id, right.id))
|
|
359
|
+
.map(buildIndexDocument),
|
|
360
|
+
sections: bundled
|
|
361
|
+
.flatMap(buildIndexSections)
|
|
362
|
+
.sort((left, right) => compare(left.documentId, right.documentId) ||
|
|
363
|
+
compare(left.language, right.language) ||
|
|
364
|
+
compare(left.sectionId, right.sectionId)),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function compare(left, right) {
|
|
368
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
369
|
+
}
|
|
370
|
+
function writeJson(path, value) {
|
|
371
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
372
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
373
|
+
}
|
|
374
|
+
function loadBundle(outputRoot) {
|
|
375
|
+
const catalogPath = join(outputRoot, 'catalog.json');
|
|
376
|
+
const indexPath = join(outputRoot, 'index.json');
|
|
377
|
+
if (!existsSync(catalogPath) || !existsSync(indexPath))
|
|
378
|
+
fail('missing docs bundle metadata');
|
|
379
|
+
const catalog = assertCatalog(JSON.parse(readFileSync(catalogPath, 'utf8')));
|
|
380
|
+
validateDocumentIds(catalog.documents);
|
|
381
|
+
for (const document of catalog.documents)
|
|
382
|
+
validateDocumentPaths(document);
|
|
383
|
+
validateTopicMap(catalog);
|
|
384
|
+
validateTopicsAgainstDocuments(catalog);
|
|
385
|
+
const loaded = catalog.documents
|
|
386
|
+
.filter((document) => document.bundled)
|
|
387
|
+
.map((document) => validateBundledDocument(document, outputRoot));
|
|
388
|
+
validateTopicSections(catalog, loaded);
|
|
389
|
+
const index = createIndex(loaded);
|
|
390
|
+
const stored = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
391
|
+
if (!isDeepStrictEqual(stored, index))
|
|
392
|
+
fail('docs bundle index or integrity hash mismatch');
|
|
393
|
+
const documents = new Map(loaded.map((document) => [document.id, document]));
|
|
394
|
+
return { catalog, index, documents, topics: catalog.topics };
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Validate the source collection before replacing the output with bundled Markdown and metadata.
|
|
398
|
+
*
|
|
399
|
+
* @param spec - Source and non-overlapping output roots, with an optional catalog file path.
|
|
400
|
+
* @returns Metadata for bundled documents and sections; excluded documents are not copied.
|
|
401
|
+
* @throws If the roots overlap or the catalog, source files, or topic references fail validation.
|
|
402
|
+
*/
|
|
403
|
+
export function buildDocs(spec) {
|
|
404
|
+
const catalogPath = spec.catalogPath ?? join(spec.sourceRoot, 'catalog.json');
|
|
405
|
+
validateDocsRootOverlap(spec.sourceRoot, spec.outputRoot);
|
|
406
|
+
if (!existsSync(catalogPath))
|
|
407
|
+
fail(`missing catalog: ${catalogPath}`);
|
|
408
|
+
const catalog = assertCatalog(JSON.parse(readFileSync(catalogPath, 'utf8')));
|
|
409
|
+
validateDocumentIds(catalog.documents);
|
|
410
|
+
for (const document of catalog.documents)
|
|
411
|
+
validateDocumentPaths(document);
|
|
412
|
+
validateTopicMap(catalog);
|
|
413
|
+
validateRedirects(catalog);
|
|
414
|
+
validateTopicsAgainstDocuments(catalog);
|
|
415
|
+
assertAllSourceMarkdownRegistered(catalog, spec.sourceRoot);
|
|
416
|
+
const documents = catalog.documents.map((document) => validateBundledDocument(document, spec.sourceRoot));
|
|
417
|
+
validateTopicSections(catalog, documents);
|
|
418
|
+
const index = createIndex(documents);
|
|
419
|
+
rmSync(spec.outputRoot, { recursive: true, force: true });
|
|
420
|
+
mkdirSync(spec.outputRoot, { recursive: true });
|
|
421
|
+
writeJson(join(spec.outputRoot, 'catalog.json'), catalog);
|
|
422
|
+
writeJson(join(spec.outputRoot, 'index.json'), index);
|
|
423
|
+
const loadedBundle = {
|
|
424
|
+
catalog,
|
|
425
|
+
index,
|
|
426
|
+
documents: new Map(documents.map((document) => [document.id, document])),
|
|
427
|
+
topics: catalog.topics,
|
|
428
|
+
};
|
|
429
|
+
copyBundleDocuments(spec.outputRoot, loadedBundle.documents);
|
|
430
|
+
return {
|
|
431
|
+
documents: index.documents.map((entry) => ({
|
|
432
|
+
id: entry.id,
|
|
433
|
+
bundled: entry.bundled,
|
|
434
|
+
category: entry.category,
|
|
435
|
+
order: entry.order,
|
|
436
|
+
translations: entry.translations,
|
|
437
|
+
})),
|
|
438
|
+
sections: index.sections,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Load a bundle only after its stored index matches the catalog and bundled Markdown.
|
|
443
|
+
*
|
|
444
|
+
* @param outputRoot - Built documentation directory containing catalog.json and index.json.
|
|
445
|
+
* @returns Validated metadata and bundled documents with their original Markdown and sections.
|
|
446
|
+
* @throws If metadata or documents are missing, invalid, or inconsistent with the stored index.
|
|
447
|
+
*/
|
|
448
|
+
export function loadDocsBundle(outputRoot) {
|
|
449
|
+
return loadBundle(outputRoot);
|
|
450
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { buildDocs as buildDocsImpl } from './docs-catalog.ts';
|
|
2
|
+
/** Inputs for one query against the installed documentation bundle. */
|
|
3
|
+
export type RunDocsSpec = {
|
|
4
|
+
docsRoot: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
version: string;
|
|
7
|
+
};
|
|
8
|
+
/** Complete stdout, returned only after validation and reading succeed. */
|
|
9
|
+
export type RunDocsOutcome = {
|
|
10
|
+
text: string;
|
|
11
|
+
};
|
|
12
|
+
/** Source catalog and destination for a reproducible documentation build. */
|
|
13
|
+
export type BuildDocsSpec = {
|
|
14
|
+
sourceRoot: string;
|
|
15
|
+
outputRoot: string;
|
|
16
|
+
catalogPath?: string;
|
|
17
|
+
};
|
|
18
|
+
/** Documents and sections included in a successful bundle. */
|
|
19
|
+
export type BuildDocsOutcome = ReturnType<typeof buildDocsImpl>;
|
|
20
|
+
/** Validate the source collection and replace the bundle, removing retired files. */
|
|
21
|
+
export declare function buildDocs(spec: BuildDocsSpec): BuildDocsOutcome;
|
|
22
|
+
/** Answer a docs-only command without loading project configuration or the judge. */
|
|
23
|
+
export declare function runDocs(spec: RunDocsSpec): RunDocsOutcome;
|