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,347 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { buildDocs as buildDocsImpl, loadDocsBundle } from './docs-catalog.js';
|
|
4
|
+
import { DOCS_TOPICS as topics } from './docs-types.js';
|
|
5
|
+
const rootHelp = `pdks docs\nUsage: pdks docs [topic]\n pdks docs search <query> [--lang en|ko] [--limit N] [--json]\n pdks docs show <document-id> [--lang en|ko] [--section <section-id>] [--json]\n\nTopics:\n${topics.map((topic) => ` ${topic}`).join('\n')}\n`;
|
|
6
|
+
function fail(message) {
|
|
7
|
+
throw new Error(message);
|
|
8
|
+
}
|
|
9
|
+
function isDocumentId(value) {
|
|
10
|
+
return /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
11
|
+
}
|
|
12
|
+
function isSectionId(value) {
|
|
13
|
+
return /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
14
|
+
}
|
|
15
|
+
function quoteShellWord(value) {
|
|
16
|
+
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
17
|
+
}
|
|
18
|
+
function shellCommand(parts) {
|
|
19
|
+
return parts.map(quoteShellWord).join(' ');
|
|
20
|
+
}
|
|
21
|
+
function languageFrom(value) {
|
|
22
|
+
if (value === 'en' || value === 'ko')
|
|
23
|
+
return value;
|
|
24
|
+
fail(`unsupported language: ${value ?? '(missing)'}`);
|
|
25
|
+
}
|
|
26
|
+
function sectionSource(path, sectionId) {
|
|
27
|
+
return sectionId ? `${path}#${sectionId}` : path;
|
|
28
|
+
}
|
|
29
|
+
function normalizeQuery(value) {
|
|
30
|
+
const query = value.trim();
|
|
31
|
+
if (query === '')
|
|
32
|
+
fail('empty search query');
|
|
33
|
+
return query;
|
|
34
|
+
}
|
|
35
|
+
function excerptFor(markdown, query) {
|
|
36
|
+
const needle = query.toLowerCase();
|
|
37
|
+
const haystack = markdown.toLowerCase();
|
|
38
|
+
const at = haystack.indexOf(needle);
|
|
39
|
+
if (at === -1)
|
|
40
|
+
return markdown.slice(0, 160);
|
|
41
|
+
const start = Math.max(0, at - 40);
|
|
42
|
+
const end = Math.min(markdown.length, at + query.length + 80);
|
|
43
|
+
return markdown.slice(start, end);
|
|
44
|
+
}
|
|
45
|
+
function scoreField(field, query, weight) {
|
|
46
|
+
const lowerField = field.toLowerCase();
|
|
47
|
+
const lowerQuery = query.toLowerCase();
|
|
48
|
+
if (!lowerField.includes(lowerQuery))
|
|
49
|
+
return 0;
|
|
50
|
+
if (lowerField === lowerQuery)
|
|
51
|
+
return weight * 4;
|
|
52
|
+
if (lowerField.startsWith(lowerQuery) || lowerField.endsWith(lowerQuery))
|
|
53
|
+
return weight * 3;
|
|
54
|
+
return weight * 2;
|
|
55
|
+
}
|
|
56
|
+
function sectionScore(document, language, sectionMarkdown, query, sectionTitle) {
|
|
57
|
+
const translation = document.translations[language];
|
|
58
|
+
const tokens = [...new Set(query.normalize('NFC').toLowerCase().split(/\s+/u))];
|
|
59
|
+
let score = 0;
|
|
60
|
+
for (const token of tokens) {
|
|
61
|
+
const tokenScore = scoreField(sectionTitle, token, 100) +
|
|
62
|
+
scoreField(translation.title, token, 30) +
|
|
63
|
+
scoreField(translation.summary, token, 20) +
|
|
64
|
+
scoreField((translation.terms ?? []).join(' '), token, 15) +
|
|
65
|
+
scoreField(sectionMarkdown, token, 10);
|
|
66
|
+
if (tokenScore === 0)
|
|
67
|
+
return 0;
|
|
68
|
+
score += tokenScore;
|
|
69
|
+
}
|
|
70
|
+
return score;
|
|
71
|
+
}
|
|
72
|
+
function sectionFromDocument(document, language, sectionId) {
|
|
73
|
+
const translation = document.translations[language];
|
|
74
|
+
if (sectionId === null) {
|
|
75
|
+
return { markdown: translation.markdown, source: translation.path, title: translation.title };
|
|
76
|
+
}
|
|
77
|
+
const section = translation.sections.find((entry) => entry.id === sectionId);
|
|
78
|
+
if (!section)
|
|
79
|
+
fail(`unknown section: ${sectionId}`);
|
|
80
|
+
return {
|
|
81
|
+
markdown: section.text,
|
|
82
|
+
source: sectionSource(translation.path, section.id),
|
|
83
|
+
title: section.title,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function parseSearchArgs(args) {
|
|
87
|
+
if (args.length === 0)
|
|
88
|
+
fail('missing search query');
|
|
89
|
+
const query = normalizeQuery(args[0]);
|
|
90
|
+
let language = 'en';
|
|
91
|
+
let limit = 5;
|
|
92
|
+
let json = false;
|
|
93
|
+
let seenLang = false;
|
|
94
|
+
let seenLimit = false;
|
|
95
|
+
let seenJson = false;
|
|
96
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
97
|
+
const arg = args[index];
|
|
98
|
+
if (arg === '--lang') {
|
|
99
|
+
if (seenLang)
|
|
100
|
+
fail('duplicate --lang');
|
|
101
|
+
seenLang = true;
|
|
102
|
+
const value = args[index + 1];
|
|
103
|
+
if (value === undefined || value.startsWith('--'))
|
|
104
|
+
fail('missing --lang value');
|
|
105
|
+
language = languageFrom(value);
|
|
106
|
+
index += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (arg === '--limit') {
|
|
110
|
+
if (seenLimit)
|
|
111
|
+
fail('duplicate --limit');
|
|
112
|
+
seenLimit = true;
|
|
113
|
+
const value = args[index + 1];
|
|
114
|
+
if (value === undefined || value.startsWith('--'))
|
|
115
|
+
fail('missing --limit value');
|
|
116
|
+
if (!/^[1-9]\d*$/.test(value))
|
|
117
|
+
fail(`invalid limit: ${value}`);
|
|
118
|
+
const parsed = Number(value);
|
|
119
|
+
if (parsed < 1 || parsed > 50)
|
|
120
|
+
fail(`invalid limit: ${value}`);
|
|
121
|
+
limit = parsed;
|
|
122
|
+
index += 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (arg === '--json') {
|
|
126
|
+
if (seenJson)
|
|
127
|
+
fail('duplicate --json');
|
|
128
|
+
seenJson = true;
|
|
129
|
+
json = true;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (arg.startsWith('--'))
|
|
133
|
+
fail(`unknown search flag: ${arg}`);
|
|
134
|
+
fail(`unexpected search argument: ${arg}`);
|
|
135
|
+
}
|
|
136
|
+
return { query, language, limit, json };
|
|
137
|
+
}
|
|
138
|
+
function parseShowArgs(args) {
|
|
139
|
+
if (args.length === 0)
|
|
140
|
+
fail('missing document id');
|
|
141
|
+
const documentId = args[0];
|
|
142
|
+
if (!isDocumentId(documentId))
|
|
143
|
+
fail(`invalid document id: ${documentId}`);
|
|
144
|
+
let language = 'en';
|
|
145
|
+
let sectionId = null;
|
|
146
|
+
let json = false;
|
|
147
|
+
let seenLang = false;
|
|
148
|
+
let seenSection = false;
|
|
149
|
+
let seenJson = false;
|
|
150
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
151
|
+
const arg = args[index];
|
|
152
|
+
if (arg === '--lang') {
|
|
153
|
+
if (seenLang)
|
|
154
|
+
fail('duplicate --lang');
|
|
155
|
+
seenLang = true;
|
|
156
|
+
const value = args[index + 1];
|
|
157
|
+
if (value === undefined || value.startsWith('--'))
|
|
158
|
+
fail('missing --lang value');
|
|
159
|
+
language = languageFrom(value);
|
|
160
|
+
index += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (arg === '--section') {
|
|
164
|
+
if (seenSection)
|
|
165
|
+
fail('duplicate --section');
|
|
166
|
+
seenSection = true;
|
|
167
|
+
const value = args[index + 1];
|
|
168
|
+
if (value === undefined || value.startsWith('--'))
|
|
169
|
+
fail('missing --section value');
|
|
170
|
+
if (!isSectionId(value))
|
|
171
|
+
fail(`invalid section id: ${value}`);
|
|
172
|
+
sectionId = value;
|
|
173
|
+
index += 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (arg === '--json') {
|
|
177
|
+
if (seenJson)
|
|
178
|
+
fail('duplicate --json');
|
|
179
|
+
seenJson = true;
|
|
180
|
+
json = true;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (arg.startsWith('--'))
|
|
184
|
+
fail(`unknown show flag: ${arg}`);
|
|
185
|
+
fail(`unexpected show argument: ${arg}`);
|
|
186
|
+
}
|
|
187
|
+
return { documentId, language, sectionId, json };
|
|
188
|
+
}
|
|
189
|
+
function parseTopicArgs(args) {
|
|
190
|
+
if (args.length === 0)
|
|
191
|
+
fail('missing docs topic');
|
|
192
|
+
const topic = args[0];
|
|
193
|
+
if (!topics.includes(topic))
|
|
194
|
+
fail(`unknown docs topic: ${topic}`);
|
|
195
|
+
let language = 'en';
|
|
196
|
+
let seenLang = false;
|
|
197
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
198
|
+
const arg = args[index];
|
|
199
|
+
if (arg === '--lang') {
|
|
200
|
+
if (seenLang)
|
|
201
|
+
fail('duplicate --lang');
|
|
202
|
+
seenLang = true;
|
|
203
|
+
const value = args[index + 1];
|
|
204
|
+
if (value === undefined || value.startsWith('--'))
|
|
205
|
+
fail('missing --lang value');
|
|
206
|
+
language = languageFrom(value);
|
|
207
|
+
index += 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (arg.startsWith('--'))
|
|
211
|
+
fail(`unknown docs flag: ${arg}`);
|
|
212
|
+
fail(`unexpected docs argument: ${arg}`);
|
|
213
|
+
}
|
|
214
|
+
return { topic, language };
|
|
215
|
+
}
|
|
216
|
+
function loadBundleRoot(docsRoot) {
|
|
217
|
+
if (!existsSync(join(docsRoot, 'catalog.json')) || !existsSync(join(docsRoot, 'index.json'))) {
|
|
218
|
+
fail('missing docs bundle');
|
|
219
|
+
}
|
|
220
|
+
return loadDocsBundle(docsRoot);
|
|
221
|
+
}
|
|
222
|
+
function searchBundle(bundleRoot, query, language, limit, version) {
|
|
223
|
+
const bundle = loadBundleRoot(bundleRoot);
|
|
224
|
+
const results = [];
|
|
225
|
+
for (const document of bundle.documents.values()) {
|
|
226
|
+
if (!document.bundled)
|
|
227
|
+
continue;
|
|
228
|
+
const translation = document.translations[language];
|
|
229
|
+
for (const section of translation.sections) {
|
|
230
|
+
const markdown = section.text;
|
|
231
|
+
const score = sectionScore(document, language, markdown, query, section.title);
|
|
232
|
+
if (score <= 0)
|
|
233
|
+
continue;
|
|
234
|
+
results.push({
|
|
235
|
+
documentId: document.id,
|
|
236
|
+
sectionId: section.id,
|
|
237
|
+
title: section.title,
|
|
238
|
+
excerpt: excerptFor(markdown, query),
|
|
239
|
+
source: sectionSource(translation.path, section.id),
|
|
240
|
+
command: shellCommand([
|
|
241
|
+
'pdks',
|
|
242
|
+
'docs',
|
|
243
|
+
'show',
|
|
244
|
+
document.id,
|
|
245
|
+
'--lang',
|
|
246
|
+
language,
|
|
247
|
+
'--section',
|
|
248
|
+
section.id,
|
|
249
|
+
]),
|
|
250
|
+
score,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
results.sort((left, right) => {
|
|
255
|
+
if (right.score !== left.score)
|
|
256
|
+
return right.score - left.score;
|
|
257
|
+
if (left.documentId !== right.documentId)
|
|
258
|
+
return left.documentId < right.documentId ? -1 : 1;
|
|
259
|
+
return left.sectionId < right.sectionId ? -1 : left.sectionId > right.sectionId ? 1 : 0;
|
|
260
|
+
});
|
|
261
|
+
return {
|
|
262
|
+
schemaVersion: 1,
|
|
263
|
+
packageVersion: version,
|
|
264
|
+
language,
|
|
265
|
+
query,
|
|
266
|
+
count: Math.min(results.length, limit),
|
|
267
|
+
results: results.slice(0, limit),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function renderSearchResultText(search) {
|
|
271
|
+
if (search.results.length === 0) {
|
|
272
|
+
return `0 results for ${search.query}\n[]\n`;
|
|
273
|
+
}
|
|
274
|
+
const lines = [
|
|
275
|
+
`${search.count} results for ${search.query} (${search.language})`,
|
|
276
|
+
...search.results.map((result, index) => [
|
|
277
|
+
`${index + 1}. ${result.documentId}#${result.sectionId} ${result.title} [${result.score}]`,
|
|
278
|
+
` ${result.excerpt}`,
|
|
279
|
+
` ${result.source}`,
|
|
280
|
+
` ${result.command}`,
|
|
281
|
+
].join('\n')),
|
|
282
|
+
];
|
|
283
|
+
return `${lines.join('\n\n')}\n`;
|
|
284
|
+
}
|
|
285
|
+
function renderTopic(bundleRoot, topic, language) {
|
|
286
|
+
const bundle = loadBundleRoot(bundleRoot);
|
|
287
|
+
const entry = bundle.topics[topic];
|
|
288
|
+
if (!entry)
|
|
289
|
+
fail(`unknown docs topic: ${topic}`);
|
|
290
|
+
const parts = [];
|
|
291
|
+
for (const reference of entry.references) {
|
|
292
|
+
const document = bundle.documents.get(reference.documentId);
|
|
293
|
+
if (!document)
|
|
294
|
+
fail(`unknown topic document: ${reference.documentId}`);
|
|
295
|
+
const section = sectionFromDocument(document, language, reference.sectionId ?? null);
|
|
296
|
+
parts.push(section.markdown);
|
|
297
|
+
}
|
|
298
|
+
parts.push(`See also: ${shellCommand(['pdks', 'docs', 'show', entry.seeAlso, '--lang', language])}`);
|
|
299
|
+
return `${parts.join('\n\n')}\n`;
|
|
300
|
+
}
|
|
301
|
+
/** Validate the source collection and replace the bundle, removing retired files. */
|
|
302
|
+
export function buildDocs(spec) {
|
|
303
|
+
return buildDocsImpl(spec);
|
|
304
|
+
}
|
|
305
|
+
/** Answer a docs-only command without loading project configuration or the judge. */
|
|
306
|
+
export function runDocs(spec) {
|
|
307
|
+
const args = spec.args;
|
|
308
|
+
if (args.length === 0)
|
|
309
|
+
return { text: rootHelp };
|
|
310
|
+
if (args.length === 1 && args[0] === '--help')
|
|
311
|
+
return { text: rootHelp };
|
|
312
|
+
if (args[0] === 'search') {
|
|
313
|
+
if (args.length === 2 && args[1] === '--help')
|
|
314
|
+
return { text: rootHelp };
|
|
315
|
+
const { query, language, limit, json } = parseSearchArgs(args.slice(1));
|
|
316
|
+
const search = searchBundle(spec.docsRoot, query, language, limit, spec.version);
|
|
317
|
+
return { text: json ? `${JSON.stringify(search)}\n` : renderSearchResultText(search) };
|
|
318
|
+
}
|
|
319
|
+
if (args[0] === 'show') {
|
|
320
|
+
if (args.length === 2 && args[1] === '--help')
|
|
321
|
+
return { text: rootHelp };
|
|
322
|
+
const { documentId, language, sectionId, json } = parseShowArgs(args.slice(1));
|
|
323
|
+
const bundle = loadBundleRoot(spec.docsRoot);
|
|
324
|
+
const document = bundle.documents.get(documentId);
|
|
325
|
+
if (!document)
|
|
326
|
+
fail(`unknown document: ${documentId}`);
|
|
327
|
+
const section = sectionFromDocument(document, language, sectionId);
|
|
328
|
+
if (json) {
|
|
329
|
+
const payload = {
|
|
330
|
+
schemaVersion: 1,
|
|
331
|
+
packageVersion: spec.version,
|
|
332
|
+
language,
|
|
333
|
+
documentId,
|
|
334
|
+
sectionId,
|
|
335
|
+
source: section.source,
|
|
336
|
+
markdown: section.markdown,
|
|
337
|
+
};
|
|
338
|
+
return { text: `${JSON.stringify(payload)}\n` };
|
|
339
|
+
}
|
|
340
|
+
return { text: section.markdown };
|
|
341
|
+
}
|
|
342
|
+
if (topics.includes(args[0])) {
|
|
343
|
+
const { topic, language } = parseTopicArgs(args);
|
|
344
|
+
return { text: renderTopic(spec.docsRoot, topic, language) };
|
|
345
|
+
}
|
|
346
|
+
fail(`unknown docs command: ${args[0]}`);
|
|
347
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { DocsSection } from './docs-types.ts';
|
|
2
|
+
/** Compute a hex SHA-256 digest of the exact UTF-8 Markdown without normalizing whitespace. */
|
|
3
|
+
export declare function hashMarkdown(markdown: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Retrieve original Markdown by stable anchor ID, including the anchor and child headings.
|
|
6
|
+
*
|
|
7
|
+
* @param markdown - Complete document whose section anchors must be valid.
|
|
8
|
+
* @param sectionId - Anchor ID without a leading hash sign.
|
|
9
|
+
* @returns Unmodified section text up to the next same-level or higher-level heading.
|
|
10
|
+
* @throws If the section is absent or the document has missing or duplicate section anchors.
|
|
11
|
+
*/
|
|
12
|
+
export declare function extractSection(markdown: string, sectionId: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Find anchored H2–H6 sections outside code fences, preserving their original Markdown.
|
|
15
|
+
*
|
|
16
|
+
* @param markdown - Complete source document to divide into retrievable sections.
|
|
17
|
+
* @param options - Require anchors for every H2–H6 heading, or omit unanchored headings.
|
|
18
|
+
* @returns Sections in document order, including nested content and their anchor lines.
|
|
19
|
+
* @throws If section IDs repeat or a required anchor is missing or not adjacent to its heading.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseSections(markdown: string, options: {
|
|
22
|
+
requireAnchors: boolean;
|
|
23
|
+
}): DocsSection[];
|
|
24
|
+
/**
|
|
25
|
+
* Look up a stable section ID after validating the document's section anchors.
|
|
26
|
+
*
|
|
27
|
+
* @param markdown - Complete source document, not just the target section.
|
|
28
|
+
* @param sectionId - Anchor ID without a leading hash sign.
|
|
29
|
+
* @returns The section and its source bounds, or undefined when the ID is absent.
|
|
30
|
+
* @throws If the document has missing or duplicate section anchors.
|
|
31
|
+
*/
|
|
32
|
+
export declare function headingMatch(markdown: string, sectionId: string): DocsSection | undefined;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
const headingPattern = /^(#{1,6})\s+(.*)$/;
|
|
3
|
+
const anchorPattern = /^<a id="([a-z0-9][a-z0-9-]*)"><\/a>$/;
|
|
4
|
+
const sectionHeadingLevel = 2;
|
|
5
|
+
function fenceMatch(line) {
|
|
6
|
+
const match = /^(`{3,}|~{3,})/.exec(line.trim());
|
|
7
|
+
if (!match)
|
|
8
|
+
return null;
|
|
9
|
+
return { marker: match[0][0], length: match[0].length };
|
|
10
|
+
}
|
|
11
|
+
function headingLevel(line) {
|
|
12
|
+
return headingPattern.exec(line)?.[1].length ?? 0;
|
|
13
|
+
}
|
|
14
|
+
function headingText(line) {
|
|
15
|
+
return headingPattern.exec(line)?.[2] ?? '';
|
|
16
|
+
}
|
|
17
|
+
function anchorId(line) {
|
|
18
|
+
return anchorPattern.exec(line.trim())?.[1] ?? null;
|
|
19
|
+
}
|
|
20
|
+
/** Compute a hex SHA-256 digest of the exact UTF-8 Markdown without normalizing whitespace. */
|
|
21
|
+
export function hashMarkdown(markdown) {
|
|
22
|
+
return createHash('sha256').update(markdown, 'utf8').digest('hex');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Retrieve original Markdown by stable anchor ID, including the anchor and child headings.
|
|
26
|
+
*
|
|
27
|
+
* @param markdown - Complete document whose section anchors must be valid.
|
|
28
|
+
* @param sectionId - Anchor ID without a leading hash sign.
|
|
29
|
+
* @returns Unmodified section text up to the next same-level or higher-level heading.
|
|
30
|
+
* @throws If the section is absent or the document has missing or duplicate section anchors.
|
|
31
|
+
*/
|
|
32
|
+
export function extractSection(markdown, sectionId) {
|
|
33
|
+
const sections = parseSections(markdown, { requireAnchors: true });
|
|
34
|
+
const section = sections.find((entry) => entry.id === sectionId);
|
|
35
|
+
if (!section) {
|
|
36
|
+
throw new Error(`section not found: ${sectionId}`);
|
|
37
|
+
}
|
|
38
|
+
return section.text;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Find anchored H2–H6 sections outside code fences, preserving their original Markdown.
|
|
42
|
+
*
|
|
43
|
+
* @param markdown - Complete source document to divide into retrievable sections.
|
|
44
|
+
* @param options - Require anchors for every H2–H6 heading, or omit unanchored headings.
|
|
45
|
+
* @returns Sections in document order, including nested content and their anchor lines.
|
|
46
|
+
* @throws If section IDs repeat or a required anchor is missing or not adjacent to its heading.
|
|
47
|
+
*/
|
|
48
|
+
export function parseSections(markdown, options) {
|
|
49
|
+
const lines = markdown.split('\n');
|
|
50
|
+
const lineStarts = [0];
|
|
51
|
+
for (let index = 0; index < lines.length - 1; index += 1) {
|
|
52
|
+
lineStarts[index + 1] = lineStarts[index] + lines[index].length + 1;
|
|
53
|
+
}
|
|
54
|
+
const starts = [];
|
|
55
|
+
const topLevelBoundaries = [];
|
|
56
|
+
let fence;
|
|
57
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
58
|
+
const line = lines[index];
|
|
59
|
+
const match = fenceMatch(line);
|
|
60
|
+
if (match) {
|
|
61
|
+
if (fence === undefined) {
|
|
62
|
+
fence = match;
|
|
63
|
+
}
|
|
64
|
+
else if (fence.marker === match.marker && match.length >= fence.length) {
|
|
65
|
+
fence = undefined;
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (fence !== undefined) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (headingLevel(line) === 1) {
|
|
73
|
+
let previous = index - 1;
|
|
74
|
+
while (previous >= 0 && lines[previous].trim() === '')
|
|
75
|
+
previous -= 1;
|
|
76
|
+
topLevelBoundaries.push(previous >= 0 && anchorId(lines[previous]) ? previous : index);
|
|
77
|
+
}
|
|
78
|
+
if (headingLevel(line) < sectionHeadingLevel) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
let anchorLine = index - 1;
|
|
82
|
+
while (anchorLine >= 0 && lines[anchorLine].trim() === '') {
|
|
83
|
+
anchorLine -= 1;
|
|
84
|
+
}
|
|
85
|
+
if (anchorLine < 0) {
|
|
86
|
+
if (options.requireAnchors) {
|
|
87
|
+
throw new Error(`missing anchor for heading: ${line}`);
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const id = anchorId(lines[anchorLine]);
|
|
92
|
+
if (!id) {
|
|
93
|
+
if (options.requireAnchors) {
|
|
94
|
+
throw new Error(`missing anchor for heading: ${line}`);
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const blankSpan = lines.slice(anchorLine + 1, index).every((entry) => entry.trim() === '');
|
|
99
|
+
if (!blankSpan) {
|
|
100
|
+
if (options.requireAnchors) {
|
|
101
|
+
throw new Error(`anchor not adjacent to heading: ${line}`);
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
starts.push({
|
|
106
|
+
id,
|
|
107
|
+
title: headingText(line),
|
|
108
|
+
level: headingLevel(line),
|
|
109
|
+
startLine: anchorLine,
|
|
110
|
+
startOffset: lineStarts[anchorLine],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const sections = starts.map((start, index) => {
|
|
114
|
+
let endLine = topLevelBoundaries.find((line) => line > start.startLine) ?? lines.length;
|
|
115
|
+
for (let candidate = index + 1; candidate < starts.length; candidate += 1) {
|
|
116
|
+
if (starts[candidate].level <= start.level) {
|
|
117
|
+
endLine = Math.min(endLine, starts[candidate].startLine);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const endOffset = endLine >= lines.length ? markdown.length : lineStarts[endLine];
|
|
122
|
+
return {
|
|
123
|
+
id: start.id,
|
|
124
|
+
title: start.title,
|
|
125
|
+
level: start.level,
|
|
126
|
+
startLine: start.startLine,
|
|
127
|
+
endLine,
|
|
128
|
+
text: markdown.slice(start.startOffset, endOffset),
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
const ids = new Set();
|
|
132
|
+
for (const section of sections) {
|
|
133
|
+
if (ids.has(section.id)) {
|
|
134
|
+
throw new Error(`duplicate section id: ${section.id}`);
|
|
135
|
+
}
|
|
136
|
+
ids.add(section.id);
|
|
137
|
+
}
|
|
138
|
+
return sections;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Look up a stable section ID after validating the document's section anchors.
|
|
142
|
+
*
|
|
143
|
+
* @param markdown - Complete source document, not just the target section.
|
|
144
|
+
* @param sectionId - Anchor ID without a leading hash sign.
|
|
145
|
+
* @returns The section and its source bounds, or undefined when the ID is absent.
|
|
146
|
+
* @throws If the document has missing or duplicate section anchors.
|
|
147
|
+
*/
|
|
148
|
+
export function headingMatch(markdown, sectionId) {
|
|
149
|
+
return parseSections(markdown, { requireAnchors: true }).find((section) => section.id === sectionId);
|
|
150
|
+
}
|
package/dist/docs-query.d.ts
CHANGED
|
@@ -1,46 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* The bundled English guides, answered from the installed version. An AI partner that
|
|
5
|
-
* searches the web gets whatever release the internet indexed; this returns the document
|
|
6
|
-
* that shipped with the code doing the judging, with no network at all.
|
|
7
|
-
*
|
|
8
|
-
* The domain is the five topics below and nothing else. An unknown topic throws instead of
|
|
9
|
-
* resolving to something near it: an answer to a question we never mapped is
|
|
10
|
-
* indistinguishable from a real one by the time it reaches a reader.
|
|
11
|
-
*
|
|
12
|
-
* Every failure throws so the bin can leave stdout at zero bytes and exit 2. Text written
|
|
13
|
-
* halfway is read as the document and quoted as the document — the same direction the
|
|
14
|
-
* judging surface fails in, for the same reason.
|
|
15
|
-
*/
|
|
16
|
-
/** The finite query domain — the topic list `pdks docs` prints with no argument. */
|
|
17
|
-
export declare const TOPICS: readonly ['install', 'config', 'discipline', 'covenant', 'witness'];
|
|
18
|
-
/**
|
|
19
|
-
* The body of one section of `markdown`: from the line equal to `heading` up to just before
|
|
20
|
-
* the next heading of the same or a higher level, returned verbatim.
|
|
21
|
-
*
|
|
22
|
-
* `heading` is matched by exact string equality. A document that renames its heading kills
|
|
23
|
-
* the query here rather than letting a normalizing matcher hand back a neighbouring section
|
|
24
|
-
* with full confidence.
|
|
25
|
-
*
|
|
26
|
-
* Both scans — for the start and for the boundary — run outside code fences. `#` lines
|
|
27
|
-
* inside a fence are content: the guides really carry them, and a fence-blind scanner cuts
|
|
28
|
-
* the answer at one of those lines while still looking like a success.
|
|
29
|
-
*/
|
|
30
|
-
export declare function extractSection(markdown: string, heading: string): string;
|
|
31
|
-
/** `queryDocs` input — the bundle to read from, and which topic to answer. */
|
|
1
|
+
export { DOCS_TOPICS as TOPICS } from './docs-types.ts';
|
|
2
|
+
/** Inputs for the original five-topic query interface. */
|
|
32
3
|
export type QueryDocsSpec = {
|
|
33
|
-
/** Root of the bundled documents (`dist/docs`), the only tree read here. */
|
|
34
4
|
docsRoot: string;
|
|
35
|
-
/** ABSENT lists the topics; anything not in {@link TOPICS} throws, empty string included. */
|
|
36
5
|
topic?: string;
|
|
37
6
|
};
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
*
|
|
41
|
-
* With no topic the result is the listing — how an AI discovers what it may ask at all.
|
|
42
|
-
* With one, it is the mapped section body followed by the bundled reference to read next.
|
|
43
|
-
*/
|
|
44
|
-
export declare function queryDocs(spec: QueryDocsSpec): {
|
|
7
|
+
/** Complete topic output, including the command for further reading. */
|
|
8
|
+
export type QueryDocsOutcome = {
|
|
45
9
|
text: string;
|
|
46
10
|
};
|
|
11
|
+
/** Answer a legacy topic using the same catalog as search and show. */
|
|
12
|
+
export declare function queryDocs(spec: QueryDocsSpec): QueryDocsOutcome;
|
|
13
|
+
/**
|
|
14
|
+
* Extract a section by its exact heading, retaining the original internal helper's contract.
|
|
15
|
+
* Topic retrieval uses stable IDs instead; this helper remains for existing internal callers.
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractSection(markdown: string, heading: string): string;
|