okf-catalog 0.1.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.
Files changed (107) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +10 -0
  4. package/README.md +87 -0
  5. package/dist/bundle/cat-file.js +94 -0
  6. package/dist/bundle/cat-file.js.map +1 -0
  7. package/dist/bundle/contract.js +60 -0
  8. package/dist/bundle/contract.js.map +1 -0
  9. package/dist/bundle/frontmatter.js +106 -0
  10. package/dist/bundle/frontmatter.js.map +1 -0
  11. package/dist/bundle/git-tree.js +109 -0
  12. package/dist/bundle/git-tree.js.map +1 -0
  13. package/dist/bundle/index-file.js +90 -0
  14. package/dist/bundle/index-file.js.map +1 -0
  15. package/dist/bundle/links.js +57 -0
  16. package/dist/bundle/links.js.map +1 -0
  17. package/dist/bundle/load.js +363 -0
  18. package/dist/bundle/load.js.map +1 -0
  19. package/dist/bundle/manifest.js +95 -0
  20. package/dist/bundle/manifest.js.map +1 -0
  21. package/dist/bundle/markdown.js +210 -0
  22. package/dist/bundle/markdown.js.map +1 -0
  23. package/dist/bundle/model.js +7 -0
  24. package/dist/bundle/model.js.map +1 -0
  25. package/dist/bundle/page.js +374 -0
  26. package/dist/bundle/page.js.map +1 -0
  27. package/dist/bundle/paths.js +36 -0
  28. package/dist/bundle/paths.js.map +1 -0
  29. package/dist/bundle/reserved.js +56 -0
  30. package/dist/bundle/reserved.js.map +1 -0
  31. package/dist/bundle/timestamp.js +49 -0
  32. package/dist/bundle/timestamp.js.map +1 -0
  33. package/dist/catalog/model.js +32 -0
  34. package/dist/catalog/model.js.map +1 -0
  35. package/dist/catalog/nearest.js +56 -0
  36. package/dist/catalog/nearest.js.map +1 -0
  37. package/dist/catalog/outputs.js +438 -0
  38. package/dist/catalog/outputs.js.map +1 -0
  39. package/dist/catalog/provenance.js +37 -0
  40. package/dist/catalog/provenance.js.map +1 -0
  41. package/dist/catalog/runtime.js +2 -0
  42. package/dist/catalog/runtime.js.map +1 -0
  43. package/dist/catalog/text.js +140 -0
  44. package/dist/catalog/text.js.map +1 -0
  45. package/dist/cli.js +81 -0
  46. package/dist/cli.js.map +1 -0
  47. package/dist/commands/check.js +155 -0
  48. package/dist/commands/check.js.map +1 -0
  49. package/dist/commands/pack.js +182 -0
  50. package/dist/commands/pack.js.map +1 -0
  51. package/dist/commands/serve.js +452 -0
  52. package/dist/commands/serve.js.map +1 -0
  53. package/dist/config/company-config.js +272 -0
  54. package/dist/config/company-config.js.map +1 -0
  55. package/dist/derive/derived-document.js +42 -0
  56. package/dist/derive/derived-document.js.map +1 -0
  57. package/dist/engine/qmd-render.js +54 -0
  58. package/dist/engine/qmd-render.js.map +1 -0
  59. package/dist/engine/qmd.js +241 -0
  60. package/dist/engine/qmd.js.map +1 -0
  61. package/dist/fs/cache-dir.js +140 -0
  62. package/dist/fs/cache-dir.js.map +1 -0
  63. package/dist/fs/company-lock.js +162 -0
  64. package/dist/fs/company-lock.js.map +1 -0
  65. package/dist/fs/walk.js +168 -0
  66. package/dist/fs/walk.js.map +1 -0
  67. package/dist/log.js +65 -0
  68. package/dist/log.js.map +1 -0
  69. package/dist/mcp/server.js +30 -0
  70. package/dist/mcp/server.js.map +1 -0
  71. package/dist/mcp/stdio.js +45 -0
  72. package/dist/mcp/stdio.js.map +1 -0
  73. package/dist/mcp/tools.js +249 -0
  74. package/dist/mcp/tools.js.map +1 -0
  75. package/dist/report/report.js +43 -0
  76. package/dist/report/report.js.map +1 -0
  77. package/dist/search/engine.js +2 -0
  78. package/dist/search/engine.js.map +1 -0
  79. package/dist/search/query.js +91 -0
  80. package/dist/search/query.js.map +1 -0
  81. package/dist/search/search.js +224 -0
  82. package/dist/search/search.js.map +1 -0
  83. package/dist/search/snippet.js +95 -0
  84. package/dist/search/snippet.js.map +1 -0
  85. package/dist/serve/poller.js +119 -0
  86. package/dist/serve/poller.js.map +1 -0
  87. package/dist/serve/runtime.js +390 -0
  88. package/dist/serve/runtime.js.map +1 -0
  89. package/dist/source/git-runner.js +232 -0
  90. package/dist/source/git-runner.js.map +1 -0
  91. package/dist/source/git.js +457 -0
  92. package/dist/source/git.js.map +1 -0
  93. package/dist/source/local.js +20 -0
  94. package/dist/source/local.js.map +1 -0
  95. package/dist/source/source.js +2 -0
  96. package/dist/source/source.js.map +1 -0
  97. package/npm-shrinkwrap.json +5482 -0
  98. package/package.json +80 -0
  99. package/plugin/claude-code/.claude-plugin/plugin.json +15 -0
  100. package/plugin/claude-code/.mcp.json +10 -0
  101. package/plugin/claude-code/skills/okf-catalog/SKILL.md +15 -0
  102. package/recipes/publish/README.md +32 -0
  103. package/recipes/publish/checkers.lock +435 -0
  104. package/recipes/publish/checkers.txt +5 -0
  105. package/recipes/publish/pack.sh +60 -0
  106. package/recipes/publish/publish.yml +88 -0
  107. package/recipes/publish/push.sh +63 -0
@@ -0,0 +1,363 @@
1
+ import { buildCatalog } from "../catalog/model.js";
2
+ import { admit, capRefusals, isEngineConfig, isHidden, unknownTypes } from "./contract.js";
3
+ import { decodeUtf8 } from "./frontmatter.js";
4
+ import { generateIndex, parseIndex } from "./index-file.js";
5
+ import { resolveLink } from "./links.js";
6
+ import { MANIFEST_NAME, parseManifest, verifyManifest } from "./manifest.js";
7
+ import { ANALYSIS_BUDGET, analysisBounds } from "./markdown.js";
8
+ import { decideReplacement, parsePage } from "./page.js";
9
+ import { byCodeUnit, byCodeUnit as byPath, folderOf, isSafeRelativePath } from "./paths.js";
10
+ import { parseReserved, reservedKind } from "./reserved.js";
11
+ function ancestors(folder) {
12
+ const out = [""];
13
+ if (folder === "")
14
+ return out;
15
+ const parts = folder.split("/");
16
+ for (let i = 1; i <= parts.length; i++)
17
+ out.push(parts.slice(0, i).join("/"));
18
+ return out;
19
+ }
20
+ /**
21
+ * Turns the files of a bundle into a catalog and a report. Pure: the files arrive in memory, the clock is a
22
+ * parameter, and nothing here reads a disk or an engine. Caps are measured over every file. Reserved files are
23
+ * routed by name before any page rule runs; non-Markdown files are attachments; hidden paths are skipped and
24
+ * counted; the engine's own configuration is refused per path. Bundle-level refusals set `report.fatal` and
25
+ * leave the catalog empty. A page that fails to parse for any reason is a refusal, never an exception.
26
+ */
27
+ export function loadBundle(company, files, options, now) {
28
+ const report = {
29
+ loadedAt: new Date(now.getTime()),
30
+ integrity: options.integrity === "require-manifest" ? "checked" : "skipped",
31
+ admitted: 0,
32
+ excludedByStatus: 0,
33
+ attachments: 0,
34
+ hidden: 0,
35
+ refusals: [...(options.walkRefusals ?? [])],
36
+ degradations: [],
37
+ unknownTypes: [],
38
+ unknownStatuses: [],
39
+ brokenLinks: [],
40
+ linksToUnserved: [],
41
+ missingOnDisk: [],
42
+ foldersWithoutIndex: [],
43
+ encodedFolders: [],
44
+ };
45
+ const empty = (fatal) => {
46
+ report.fatal = fatal;
47
+ return {
48
+ catalog: buildCatalog({ company, loadedAt: now, pages: new Map(), folders: new Map() }),
49
+ report,
50
+ };
51
+ };
52
+ if (options.walkFatal !== undefined)
53
+ return empty(options.walkFatal);
54
+ // Hidden paths the walker never read: counted, and the engine's own folder refused by name.
55
+ for (const hidden of options.hiddenPaths ?? []) {
56
+ report.hidden += 1;
57
+ if (isEngineConfig(hidden)) {
58
+ report.refusals.push({
59
+ path: hidden,
60
+ rule: "engine-config",
61
+ detail: "the search engine's own configuration has no place in a bundle",
62
+ });
63
+ }
64
+ }
65
+ const presentButUnread = {
66
+ exact: [...(options.hiddenPaths ?? []), ...(options.walkRefusals ?? []).map((r) => r.path)],
67
+ folders: [...(options.hiddenFolders ?? [])],
68
+ };
69
+ // Paths first: a path that is not a safe bundle path is refused before anything reads it.
70
+ const sorted = [];
71
+ for (const file of [...files].sort((a, b) => byPath(a.path, b.path))) {
72
+ if (isSafeRelativePath(file.path))
73
+ sorted.push(file);
74
+ else
75
+ report.refusals.push({
76
+ path: file.path,
77
+ rule: "path-escape",
78
+ detail: "not a bundle-relative path: absolute, empty or parent segments and backslashes are refused",
79
+ });
80
+ }
81
+ // Caps, over everything that arrived.
82
+ const caps = capRefusals(sorted, options.caps);
83
+ if (caps.fatal !== undefined)
84
+ return empty(caps.fatal);
85
+ const refusedPaths = new Set(caps.perFile.map((r) => r.path));
86
+ report.refusals.push(...caps.perFile);
87
+ // Partition.
88
+ let manifestFile;
89
+ const reservedFiles = [];
90
+ const pageFiles = [];
91
+ const attachmentPaths = new Set();
92
+ for (const file of sorted) {
93
+ if (isEngineConfig(file.path)) {
94
+ report.refusals.push({
95
+ path: file.path,
96
+ rule: "engine-config",
97
+ detail: "the search engine's own configuration has no place in a bundle",
98
+ });
99
+ refusedPaths.add(file.path);
100
+ continue;
101
+ }
102
+ if (isHidden(file.path)) {
103
+ report.hidden += 1;
104
+ continue;
105
+ }
106
+ if (file.path === MANIFEST_NAME)
107
+ manifestFile = file;
108
+ else if (reservedKind(file.path) !== undefined)
109
+ reservedFiles.push(file);
110
+ else if (file.path.endsWith(".md"))
111
+ pageFiles.push(file);
112
+ else
113
+ attachmentPaths.add(file.path);
114
+ }
115
+ // Integrity, against every file on disk, hidden and refused ones included: they are present, not missing.
116
+ let manifest;
117
+ if (manifestFile !== undefined) {
118
+ const parsed = parseManifest(manifestFile.bytes);
119
+ if (parsed.ok)
120
+ manifest = parsed.manifest;
121
+ else if (options.integrity === "require-manifest") {
122
+ return empty({ path: MANIFEST_NAME, rule: "manifest-invalid", detail: parsed.error });
123
+ }
124
+ }
125
+ else if (options.integrity === "require-manifest") {
126
+ return empty({
127
+ path: MANIFEST_NAME,
128
+ rule: "manifest-missing",
129
+ detail: "integrity is required and the bundle has no manifest.json",
130
+ });
131
+ }
132
+ if (manifest !== undefined) {
133
+ report.commit = manifest.commit;
134
+ if (options.integrity === "require-manifest") {
135
+ for (const problem of verifyManifest(manifest, sorted, presentButUnread)) {
136
+ if (problem.problem === "missing-on-disk")
137
+ report.missingOnDisk.push(problem.path);
138
+ else if (!isHidden(problem.path)) {
139
+ refusedPaths.add(problem.path);
140
+ report.refusals.push({
141
+ path: problem.path,
142
+ rule: problem.problem,
143
+ detail: problem.problem === "not-in-manifest"
144
+ ? "the manifest does not list this file"
145
+ : `the file does not match its manifest entry (${problem.problem})`,
146
+ });
147
+ }
148
+ }
149
+ }
150
+ }
151
+ const keep = (file) => !refusedPaths.has(file.path);
152
+ const reservedKept = reservedFiles.filter(keep);
153
+ const pagesKept = pageFiles.filter(keep);
154
+ for (const path of refusedPaths)
155
+ attachmentPaths.delete(path);
156
+ report.attachments = attachmentPaths.size;
157
+ // Link index over everything that is still in play.
158
+ const linkIndex = {
159
+ pages: new Set(pagesKept.map((f) => f.path)),
160
+ reserved: new Set(reservedKept.map((f) => f.path)),
161
+ attachments: attachmentPaths,
162
+ folders: new Set(),
163
+ };
164
+ for (const file of sorted) {
165
+ if (isHidden(file.path))
166
+ continue;
167
+ for (const folder of ancestors(folderOf(file.path)))
168
+ linkIndex.folders.add(folder);
169
+ }
170
+ // Reserved files.
171
+ const reserved = new Map();
172
+ for (const file of reservedKept) {
173
+ const decoded = decodeUtf8(file.bytes);
174
+ if (!decoded.ok) {
175
+ report.refusals.push({
176
+ path: file.path,
177
+ rule: "not-utf8",
178
+ detail: "the file is not valid UTF-8",
179
+ });
180
+ continue;
181
+ }
182
+ try {
183
+ const parsed = parseReserved(file.path, decoded.text);
184
+ reserved.set(file.path, parsed);
185
+ report.degradations.push(...parsed.degradations);
186
+ }
187
+ catch (error) {
188
+ report.degradations.push({
189
+ path: file.path,
190
+ code: "reserved-frontmatter-unparseable",
191
+ field: "frontmatter",
192
+ detail: `the file could not be read: ${error.message}`,
193
+ });
194
+ }
195
+ }
196
+ // Pages.
197
+ const pages = [];
198
+ for (const file of pagesKept) {
199
+ try {
200
+ const result = parsePage(file, { linkIndex, specText: options.specText });
201
+ if (result.ok)
202
+ pages.push(result.page);
203
+ else
204
+ report.refusals.push(result.refusal);
205
+ }
206
+ catch (error) {
207
+ report.refusals.push({
208
+ path: file.path,
209
+ rule: "body-unreadable",
210
+ detail: `the page could not be read: ${error.message}`,
211
+ });
212
+ }
213
+ }
214
+ // Admission, then replacements, then what is reported about the admitted pages.
215
+ const admitted = pages.filter((p) => admit(p, options.admit, options.dev));
216
+ report.admitted = admitted.length;
217
+ report.excludedByStatus = pages.length - admitted.length;
218
+ for (const page of pages) {
219
+ if (page.statusRaw !== undefined &&
220
+ page.degradations.some((d) => d.code === "status-unknown")) {
221
+ report.unknownStatuses.push({ path: page.path, value: page.statusRaw });
222
+ }
223
+ }
224
+ const admittedPaths = new Set(admitted.map((p) => p.path));
225
+ const extra = [];
226
+ for (const page of admitted) {
227
+ const decision = decideReplacement(page, admittedPaths);
228
+ if (decision.replacement !== undefined)
229
+ page.replacement = decision.replacement;
230
+ if (decision.degradation !== undefined)
231
+ extra.push(decision.degradation);
232
+ for (const link of page.links) {
233
+ if (link.kind === "broken")
234
+ report.brokenLinks.push({ from: page.path, raw: link.raw });
235
+ else if (link.kind === "page" &&
236
+ link.target !== undefined &&
237
+ !admittedPaths.has(link.target)) {
238
+ report.linksToUnserved.push({ from: page.path, raw: link.raw, target: link.target });
239
+ }
240
+ }
241
+ }
242
+ for (const page of admitted)
243
+ report.degradations.push(...page.degradations);
244
+ report.degradations.push(...extra);
245
+ // A company's own index that lists pages which are not served is reported, since the catalog serves it as written.
246
+ for (const index of reserved.values()) {
247
+ if (index.kind !== "index")
248
+ continue;
249
+ // An index past the analysis bounds is served as written but not read: its parse could take minutes.
250
+ const bound = index.body.length > ANALYSIS_BUDGET ? "size" : analysisBounds(index.body);
251
+ if (bound !== undefined) {
252
+ report.degradations.push({
253
+ path: index.path,
254
+ code: "reserved-unanalysed",
255
+ field: "body",
256
+ detail: `the index was not analysed (${bound} beyond the analysis bounds); it is served as written`,
257
+ });
258
+ continue;
259
+ }
260
+ let entries;
261
+ try {
262
+ entries = parseIndex(index.body).flatMap((s) => s.entries);
263
+ }
264
+ catch (error) {
265
+ report.degradations.push({
266
+ path: index.path,
267
+ code: "reserved-unanalysed",
268
+ field: "body",
269
+ detail: `the index could not be read: ${error.message}`,
270
+ });
271
+ continue;
272
+ }
273
+ const unserved = [];
274
+ for (const entry of entries) {
275
+ const resolved = resolveLink(entry.href, index.path, linkIndex);
276
+ if (resolved.kind === "page" &&
277
+ resolved.target !== undefined &&
278
+ !admittedPaths.has(resolved.target))
279
+ unserved.push(resolved.target);
280
+ }
281
+ if (unserved.length > 0) {
282
+ report.degradations.push({
283
+ path: index.path,
284
+ code: "index-lists-unserved",
285
+ field: "entries",
286
+ detail: `${unserved.length} ${unserved.length === 1 ? "entry points" : "entries point"} at pages that are not served: ${unserved.slice(0, 5).join(", ")}${unserved.length > 5 ? ", …" : ""}`,
287
+ });
288
+ }
289
+ }
290
+ report.unknownTypes = unknownTypes(admitted, options.types);
291
+ // Folders: every ancestor of an admitted page, plus every folder that holds a reserved file.
292
+ const folderNames = new Set([""]);
293
+ for (const page of admitted)
294
+ for (const f of ancestors(page.folder))
295
+ folderNames.add(f);
296
+ for (const file of reservedKept)
297
+ for (const f of ancestors(folderOf(file.path)))
298
+ folderNames.add(f);
299
+ const folders = new Map();
300
+ const indexPages = admitted.map((p) => {
301
+ const entry = { path: p.path, title: p.title };
302
+ if (p.description !== undefined)
303
+ entry.description = p.description;
304
+ return entry;
305
+ });
306
+ for (const name of [...folderNames].sort(byPath)) {
307
+ const prefix = name === "" ? "" : `${name}/`;
308
+ const own = admitted
309
+ .filter((p) => p.folder === name)
310
+ .map((p) => p.path)
311
+ .sort(byPath);
312
+ const subfolders = [...folderNames]
313
+ .filter((f) => f !== "" && f !== name && f.startsWith(prefix) && !f.slice(prefix.length).includes("/"))
314
+ .sort(byPath);
315
+ const indexPath = name === "" ? "index.md" : `${name}/index.md`;
316
+ const logPath = name === "" ? "log.md" : `${name}/log.md`;
317
+ const fileIndex = reserved.get(indexPath);
318
+ const folder = {
319
+ indexSource: fileIndex === undefined ? "generated" : "file",
320
+ pages: own,
321
+ subfolders,
322
+ };
323
+ if (fileIndex !== undefined)
324
+ folder.index = fileIndex;
325
+ else {
326
+ // Subfolder links in a generated index are relative to the folder, as §8 shows them.
327
+ const body = generateIndex(name, indexPages, subfolders.map((f) => f.slice(prefix.length)));
328
+ folder.index = {
329
+ kind: "index",
330
+ path: indexPath,
331
+ folder: name,
332
+ body,
333
+ text: body,
334
+ degradations: [],
335
+ };
336
+ report.foldersWithoutIndex.push(name);
337
+ }
338
+ const log = reserved.get(logPath);
339
+ if (log !== undefined)
340
+ folder.log = log;
341
+ folders.set(name, folder);
342
+ }
343
+ const pageMap = new Map(admitted.map((p) => [p.path, p]));
344
+ const okfVersion = reserved.get("index.md")?.okfVersion;
345
+ const input = {
346
+ company,
347
+ loadedAt: now,
348
+ pages: pageMap,
349
+ folders,
350
+ };
351
+ if (report.commit !== undefined)
352
+ input.commit = report.commit;
353
+ if (okfVersion !== undefined)
354
+ input.okfVersion = okfVersion;
355
+ return { catalog: buildCatalog(input), report: ordered(report) };
356
+ }
357
+ /** Refusals and missing paths in path order, whatever order the walker or the manifest produced them in. */
358
+ function ordered(report) {
359
+ report.refusals.sort((a, b) => byCodeUnit(a.path, b.path) || byCodeUnit(a.rule, b.rule));
360
+ report.missingOnDisk.sort(byCodeUnit);
361
+ return report;
362
+ }
363
+ //# sourceMappingURL=load.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/bundle/load.ts"],"names":[],"mappings":"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","sourcesContent":["import { buildCatalog, type Catalog, type Folder } from \"../catalog/model.js\";\nimport { admit, capRefusals, isEngineConfig, isHidden, unknownTypes } from \"./contract.js\";\nimport { decodeUtf8 } from \"./frontmatter.js\";\nimport { generateIndex, type IndexPage, parseIndex } from \"./index-file.js\";\nimport { type LinkIndex, resolveLink } from \"./links.js\";\nimport { MANIFEST_NAME, type Manifest, parseManifest, verifyManifest } from \"./manifest.js\";\nimport { ANALYSIS_BUDGET, analysisBounds } from \"./markdown.js\";\nimport type {\n BundleFile,\n Degradation,\n LoadOptions,\n Page,\n PagePath,\n Refusal,\n Report,\n ReservedFile,\n} from \"./model.js\";\nimport { decideReplacement, parsePage } from \"./page.js\";\nimport { byCodeUnit, byCodeUnit as byPath, folderOf, isSafeRelativePath } from \"./paths.js\";\nimport { parseReserved, reservedKind } from \"./reserved.js\";\n\nexport interface LoadResult {\n catalog: Catalog;\n report: Report;\n}\n\nfunction ancestors(folder: string): string[] {\n const out = [\"\"];\n if (folder === \"\") return out;\n const parts = folder.split(\"/\");\n for (let i = 1; i <= parts.length; i++) out.push(parts.slice(0, i).join(\"/\"));\n return out;\n}\n\n/**\n * Turns the files of a bundle into a catalog and a report. Pure: the files arrive in memory, the clock is a\n * parameter, and nothing here reads a disk or an engine. Caps are measured over every file. Reserved files are\n * routed by name before any page rule runs; non-Markdown files are attachments; hidden paths are skipped and\n * counted; the engine's own configuration is refused per path. Bundle-level refusals set `report.fatal` and\n * leave the catalog empty. A page that fails to parse for any reason is a refusal, never an exception.\n */\nexport function loadBundle(\n company: string,\n files: BundleFile[],\n options: LoadOptions,\n now: Date,\n): LoadResult {\n const report: Report = {\n loadedAt: new Date(now.getTime()),\n integrity: options.integrity === \"require-manifest\" ? \"checked\" : \"skipped\",\n admitted: 0,\n excludedByStatus: 0,\n attachments: 0,\n hidden: 0,\n refusals: [...(options.walkRefusals ?? [])],\n degradations: [],\n unknownTypes: [],\n unknownStatuses: [],\n brokenLinks: [],\n linksToUnserved: [],\n missingOnDisk: [],\n foldersWithoutIndex: [],\n encodedFolders: [],\n };\n const empty = (fatal: Refusal): LoadResult => {\n report.fatal = fatal;\n return {\n catalog: buildCatalog({ company, loadedAt: now, pages: new Map(), folders: new Map() }),\n report,\n };\n };\n\n if (options.walkFatal !== undefined) return empty(options.walkFatal);\n // Hidden paths the walker never read: counted, and the engine's own folder refused by name.\n for (const hidden of options.hiddenPaths ?? []) {\n report.hidden += 1;\n if (isEngineConfig(hidden)) {\n report.refusals.push({\n path: hidden,\n rule: \"engine-config\",\n detail: \"the search engine's own configuration has no place in a bundle\",\n });\n }\n }\n const presentButUnread = {\n exact: [...(options.hiddenPaths ?? []), ...(options.walkRefusals ?? []).map((r) => r.path)],\n folders: [...(options.hiddenFolders ?? [])],\n };\n\n // Paths first: a path that is not a safe bundle path is refused before anything reads it.\n const sorted: BundleFile[] = [];\n for (const file of [...files].sort((a, b) => byPath(a.path, b.path))) {\n if (isSafeRelativePath(file.path)) sorted.push(file);\n else\n report.refusals.push({\n path: file.path,\n rule: \"path-escape\",\n detail:\n \"not a bundle-relative path: absolute, empty or parent segments and backslashes are refused\",\n });\n }\n\n // Caps, over everything that arrived.\n const caps = capRefusals(sorted, options.caps);\n if (caps.fatal !== undefined) return empty(caps.fatal);\n const refusedPaths = new Set(caps.perFile.map((r) => r.path));\n report.refusals.push(...caps.perFile);\n\n // Partition.\n let manifestFile: BundleFile | undefined;\n const reservedFiles: BundleFile[] = [];\n const pageFiles: BundleFile[] = [];\n const attachmentPaths = new Set<string>();\n for (const file of sorted) {\n if (isEngineConfig(file.path)) {\n report.refusals.push({\n path: file.path,\n rule: \"engine-config\",\n detail: \"the search engine's own configuration has no place in a bundle\",\n });\n refusedPaths.add(file.path);\n continue;\n }\n if (isHidden(file.path)) {\n report.hidden += 1;\n continue;\n }\n if (file.path === MANIFEST_NAME) manifestFile = file;\n else if (reservedKind(file.path) !== undefined) reservedFiles.push(file);\n else if (file.path.endsWith(\".md\")) pageFiles.push(file);\n else attachmentPaths.add(file.path);\n }\n\n // Integrity, against every file on disk, hidden and refused ones included: they are present, not missing.\n let manifest: Manifest | undefined;\n if (manifestFile !== undefined) {\n const parsed = parseManifest(manifestFile.bytes);\n if (parsed.ok) manifest = parsed.manifest;\n else if (options.integrity === \"require-manifest\") {\n return empty({ path: MANIFEST_NAME, rule: \"manifest-invalid\", detail: parsed.error });\n }\n } else if (options.integrity === \"require-manifest\") {\n return empty({\n path: MANIFEST_NAME,\n rule: \"manifest-missing\",\n detail: \"integrity is required and the bundle has no manifest.json\",\n });\n }\n if (manifest !== undefined) {\n report.commit = manifest.commit;\n if (options.integrity === \"require-manifest\") {\n for (const problem of verifyManifest(manifest, sorted, presentButUnread)) {\n if (problem.problem === \"missing-on-disk\") report.missingOnDisk.push(problem.path);\n else if (!isHidden(problem.path)) {\n refusedPaths.add(problem.path);\n report.refusals.push({\n path: problem.path,\n rule: problem.problem,\n detail:\n problem.problem === \"not-in-manifest\"\n ? \"the manifest does not list this file\"\n : `the file does not match its manifest entry (${problem.problem})`,\n });\n }\n }\n }\n }\n\n const keep = (file: BundleFile): boolean => !refusedPaths.has(file.path);\n const reservedKept = reservedFiles.filter(keep);\n const pagesKept = pageFiles.filter(keep);\n for (const path of refusedPaths) attachmentPaths.delete(path);\n report.attachments = attachmentPaths.size;\n\n // Link index over everything that is still in play.\n const linkIndex: LinkIndex = {\n pages: new Set(pagesKept.map((f) => f.path)),\n reserved: new Set(reservedKept.map((f) => f.path)),\n attachments: attachmentPaths,\n folders: new Set<string>(),\n };\n for (const file of sorted) {\n if (isHidden(file.path)) continue;\n for (const folder of ancestors(folderOf(file.path))) linkIndex.folders.add(folder);\n }\n\n // Reserved files.\n const reserved = new Map<string, ReservedFile>();\n for (const file of reservedKept) {\n const decoded = decodeUtf8(file.bytes);\n if (!decoded.ok) {\n report.refusals.push({\n path: file.path,\n rule: \"not-utf8\",\n detail: \"the file is not valid UTF-8\",\n });\n continue;\n }\n try {\n const parsed = parseReserved(file.path, decoded.text);\n reserved.set(file.path, parsed);\n report.degradations.push(...parsed.degradations);\n } catch (error) {\n report.degradations.push({\n path: file.path,\n code: \"reserved-frontmatter-unparseable\",\n field: \"frontmatter\",\n detail: `the file could not be read: ${(error as Error).message}`,\n });\n }\n }\n\n // Pages.\n const pages: Page[] = [];\n for (const file of pagesKept) {\n try {\n const result = parsePage(file, { linkIndex, specText: options.specText });\n if (result.ok) pages.push(result.page);\n else report.refusals.push(result.refusal);\n } catch (error) {\n report.refusals.push({\n path: file.path,\n rule: \"body-unreadable\",\n detail: `the page could not be read: ${(error as Error).message}`,\n });\n }\n }\n\n // Admission, then replacements, then what is reported about the admitted pages.\n const admitted = pages.filter((p) => admit(p, options.admit, options.dev));\n report.admitted = admitted.length;\n report.excludedByStatus = pages.length - admitted.length;\n for (const page of pages) {\n if (\n page.statusRaw !== undefined &&\n page.degradations.some((d) => d.code === \"status-unknown\")\n ) {\n report.unknownStatuses.push({ path: page.path, value: page.statusRaw });\n }\n }\n const admittedPaths = new Set<PagePath>(admitted.map((p) => p.path));\n const extra: Degradation[] = [];\n for (const page of admitted) {\n const decision = decideReplacement(page, admittedPaths);\n if (decision.replacement !== undefined) page.replacement = decision.replacement;\n if (decision.degradation !== undefined) extra.push(decision.degradation);\n for (const link of page.links) {\n if (link.kind === \"broken\") report.brokenLinks.push({ from: page.path, raw: link.raw });\n else if (\n link.kind === \"page\" &&\n link.target !== undefined &&\n !admittedPaths.has(link.target)\n ) {\n report.linksToUnserved.push({ from: page.path, raw: link.raw, target: link.target });\n }\n }\n }\n for (const page of admitted) report.degradations.push(...page.degradations);\n report.degradations.push(...extra);\n // A company's own index that lists pages which are not served is reported, since the catalog serves it as written.\n for (const index of reserved.values()) {\n if (index.kind !== \"index\") continue;\n // An index past the analysis bounds is served as written but not read: its parse could take minutes.\n const bound = index.body.length > ANALYSIS_BUDGET ? \"size\" : analysisBounds(index.body);\n if (bound !== undefined) {\n report.degradations.push({\n path: index.path,\n code: \"reserved-unanalysed\",\n field: \"body\",\n detail: `the index was not analysed (${bound} beyond the analysis bounds); it is served as written`,\n });\n continue;\n }\n let entries: ReturnType<typeof parseIndex>[number][\"entries\"];\n try {\n entries = parseIndex(index.body).flatMap((s) => s.entries);\n } catch (error) {\n report.degradations.push({\n path: index.path,\n code: \"reserved-unanalysed\",\n field: \"body\",\n detail: `the index could not be read: ${(error as Error).message}`,\n });\n continue;\n }\n const unserved: string[] = [];\n for (const entry of entries) {\n const resolved = resolveLink(entry.href, index.path, linkIndex);\n if (\n resolved.kind === \"page\" &&\n resolved.target !== undefined &&\n !admittedPaths.has(resolved.target)\n )\n unserved.push(resolved.target);\n }\n if (unserved.length > 0) {\n report.degradations.push({\n path: index.path,\n code: \"index-lists-unserved\",\n field: \"entries\",\n detail: `${unserved.length} ${unserved.length === 1 ? \"entry points\" : \"entries point\"} at pages that are not served: ${unserved.slice(0, 5).join(\", \")}${unserved.length > 5 ? \", …\" : \"\"}`,\n });\n }\n }\n report.unknownTypes = unknownTypes(admitted, options.types);\n\n // Folders: every ancestor of an admitted page, plus every folder that holds a reserved file.\n const folderNames = new Set<string>([\"\"]);\n for (const page of admitted) for (const f of ancestors(page.folder)) folderNames.add(f);\n for (const file of reservedKept)\n for (const f of ancestors(folderOf(file.path))) folderNames.add(f);\n const folders = new Map<string, Folder>();\n const indexPages: IndexPage[] = admitted.map((p) => {\n const entry: IndexPage = { path: p.path, title: p.title };\n if (p.description !== undefined) entry.description = p.description;\n return entry;\n });\n for (const name of [...folderNames].sort(byPath)) {\n const prefix = name === \"\" ? \"\" : `${name}/`;\n const own = admitted\n .filter((p) => p.folder === name)\n .map((p) => p.path)\n .sort(byPath);\n const subfolders = [...folderNames]\n .filter(\n (f) =>\n f !== \"\" && f !== name && f.startsWith(prefix) && !f.slice(prefix.length).includes(\"/\"),\n )\n .sort(byPath);\n const indexPath = name === \"\" ? \"index.md\" : `${name}/index.md`;\n const logPath = name === \"\" ? \"log.md\" : `${name}/log.md`;\n const fileIndex = reserved.get(indexPath);\n const folder: Folder = {\n indexSource: fileIndex === undefined ? \"generated\" : \"file\",\n pages: own,\n subfolders,\n };\n if (fileIndex !== undefined) folder.index = fileIndex;\n else {\n // Subfolder links in a generated index are relative to the folder, as §8 shows them.\n const body = generateIndex(\n name,\n indexPages,\n subfolders.map((f) => f.slice(prefix.length)),\n );\n folder.index = {\n kind: \"index\",\n path: indexPath,\n folder: name,\n body,\n text: body,\n degradations: [],\n };\n report.foldersWithoutIndex.push(name);\n }\n const log = reserved.get(logPath);\n if (log !== undefined) folder.log = log;\n folders.set(name, folder);\n }\n\n const pageMap = new Map<PagePath, Page>(admitted.map((p) => [p.path, p]));\n const okfVersion = reserved.get(\"index.md\")?.okfVersion;\n const input: Parameters<typeof buildCatalog>[0] = {\n company,\n loadedAt: now,\n pages: pageMap,\n folders,\n };\n if (report.commit !== undefined) input.commit = report.commit;\n if (okfVersion !== undefined) input.okfVersion = okfVersion;\n return { catalog: buildCatalog(input), report: ordered(report) };\n}\n\n/** Refusals and missing paths in path order, whatever order the walker or the manifest produced them in. */\nfunction ordered(report: Report): Report {\n report.refusals.sort((a, b) => byCodeUnit(a.path, b.path) || byCodeUnit(a.rule, b.rule));\n report.missingOnDisk.sort(byCodeUnit);\n return report;\n}\n"]}
@@ -0,0 +1,95 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as z from "zod/v4";
3
+ import { isSafeRelativePath } from "./paths.js";
4
+ export { isSafeRelativePath };
5
+ export const MANIFEST_NAME = "manifest.json";
6
+ const PathKey = z.string().refine(isSafeRelativePath, "not a safe bundle-relative path");
7
+ export const ManifestSchema = z.strictObject({
8
+ okf_catalog: z.literal(1),
9
+ commit: z
10
+ .string()
11
+ .regex(/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/, "a full commit hash, SHA-1 or SHA-256"),
12
+ published_at: z.iso.datetime({ offset: true }),
13
+ files: z.record(PathKey, z.strictObject({
14
+ sha256: z.string().regex(/^[0-9a-f]{64}$/, "a sha256 in lower-case hex"),
15
+ bytes: z.number().int().nonnegative(),
16
+ })),
17
+ });
18
+ export function sha256Hex(bytes) {
19
+ return createHash("sha256").update(bytes).digest("hex");
20
+ }
21
+ export function parseManifest(bytes) {
22
+ let json;
23
+ try {
24
+ json = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
25
+ }
26
+ catch (error) {
27
+ return { ok: false, error: `manifest is not JSON: ${error.message}` };
28
+ }
29
+ if (typeof json === "object" && json !== null && Object.hasOwn(json, "files")) {
30
+ const files = json.files;
31
+ if (typeof files === "object" && files !== null && Object.keys(files).includes("__proto__")) {
32
+ return {
33
+ ok: false,
34
+ error: "manifest invalid at files: a path key named __proto__ is not allowed",
35
+ };
36
+ }
37
+ }
38
+ const result = ManifestSchema.safeParse(json);
39
+ if (!result.success) {
40
+ const first = result.error.issues[0];
41
+ const where = first?.path.map(String).join(".") ?? "";
42
+ return {
43
+ ok: false,
44
+ error: `manifest invalid at ${where || "root"}: ${first?.message ?? "unknown"}`,
45
+ };
46
+ }
47
+ return { ok: true, manifest: result.data };
48
+ }
49
+ /** Builds the manifest for a set of files, the manifest itself excluded, keys sorted by code unit. */
50
+ export function buildManifest(files, meta) {
51
+ for (const file of files) {
52
+ if (!isSafeRelativePath(file.path))
53
+ throw new Error(`cannot build a manifest for an unsafe path: ${file.path}`);
54
+ }
55
+ const entries = files
56
+ .filter((f) => f.path !== MANIFEST_NAME)
57
+ .map((f) => [f.path, { sha256: sha256Hex(f.bytes), bytes: f.bytes.length }])
58
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
59
+ return {
60
+ okf_catalog: 1,
61
+ commit: meta.commit,
62
+ published_at: meta.publishedAt,
63
+ files: Object.fromEntries(entries),
64
+ };
65
+ }
66
+ /** Compares files against a manifest. One problem per path; a size difference is reported before a hash difference. */
67
+ export function verifyManifest(manifest, files, alsoPresent = { exact: [], folders: [] }) {
68
+ const problems = [];
69
+ const seen = new Set(alsoPresent.exact);
70
+ for (const file of files) {
71
+ if (file.path === MANIFEST_NAME)
72
+ continue;
73
+ seen.add(file.path);
74
+ if (!Object.hasOwn(manifest.files, file.path)) {
75
+ problems.push({ path: file.path, problem: "not-in-manifest" });
76
+ continue;
77
+ }
78
+ const entry = manifest.files[file.path];
79
+ if (entry === undefined)
80
+ continue;
81
+ if (entry.bytes !== file.bytes.length)
82
+ problems.push({ path: file.path, problem: "size-mismatch" });
83
+ else if (entry.sha256 !== sha256Hex(file.bytes))
84
+ problems.push({ path: file.path, problem: "hash-mismatch" });
85
+ }
86
+ // A path under a hidden folder the walker never entered is present even though no file arrived. A refused
87
+ // entry (a symbolic link, a pipe, an unreadable file) stands only for itself: nothing beneath it is known.
88
+ const underFolder = (path) => alsoPresent.folders.some((f) => path === f || path.startsWith(`${f}/`));
89
+ for (const path of Object.keys(manifest.files)) {
90
+ if (!seen.has(path) && !underFolder(path))
91
+ problems.push({ path, problem: "missing-on-disk" });
92
+ }
93
+ return problems;
94
+ }
95
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/bundle/manifest.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport * as z from \"zod/v4\";\nimport type { BundleFile } from \"./model.js\";\nimport { isSafeRelativePath } from \"./paths.js\";\n\nexport { isSafeRelativePath };\n\nexport const MANIFEST_NAME = \"manifest.json\";\n\nconst PathKey = z.string().refine(isSafeRelativePath, \"not a safe bundle-relative path\");\n\nexport const ManifestSchema = z.strictObject({\n okf_catalog: z.literal(1),\n commit: z\n .string()\n .regex(/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/, \"a full commit hash, SHA-1 or SHA-256\"),\n published_at: z.iso.datetime({ offset: true }),\n files: z.record(\n PathKey,\n z.strictObject({\n sha256: z.string().regex(/^[0-9a-f]{64}$/, \"a sha256 in lower-case hex\"),\n bytes: z.number().int().nonnegative(),\n }),\n ),\n});\n\nexport type Manifest = z.infer<typeof ManifestSchema>;\n\nexport function sha256Hex(bytes: Uint8Array): string {\n return createHash(\"sha256\").update(bytes).digest(\"hex\");\n}\n\nexport function parseManifest(\n bytes: Uint8Array,\n): { ok: true; manifest: Manifest } | { ok: false; error: string } {\n let json: unknown;\n try {\n json = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes));\n } catch (error) {\n return { ok: false, error: `manifest is not JSON: ${(error as Error).message}` };\n }\n if (typeof json === \"object\" && json !== null && Object.hasOwn(json, \"files\")) {\n const files = (json as { files: unknown }).files;\n if (typeof files === \"object\" && files !== null && Object.keys(files).includes(\"__proto__\")) {\n return {\n ok: false,\n error: \"manifest invalid at files: a path key named __proto__ is not allowed\",\n };\n }\n }\n const result = ManifestSchema.safeParse(json);\n if (!result.success) {\n const first = result.error.issues[0];\n const where = first?.path.map(String).join(\".\") ?? \"\";\n return {\n ok: false,\n error: `manifest invalid at ${where || \"root\"}: ${first?.message ?? \"unknown\"}`,\n };\n }\n return { ok: true, manifest: result.data };\n}\n\n/** Builds the manifest for a set of files, the manifest itself excluded, keys sorted by code unit. */\nexport function buildManifest(\n files: BundleFile[],\n meta: { commit: string; publishedAt: string },\n): Manifest {\n for (const file of files) {\n if (!isSafeRelativePath(file.path))\n throw new Error(`cannot build a manifest for an unsafe path: ${file.path}`);\n }\n const entries = files\n .filter((f) => f.path !== MANIFEST_NAME)\n .map((f) => [f.path, { sha256: sha256Hex(f.bytes), bytes: f.bytes.length }] as const)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n return {\n okf_catalog: 1,\n commit: meta.commit,\n published_at: meta.publishedAt,\n files: Object.fromEntries(entries),\n };\n}\n\nexport type ManifestProblem = {\n path: string;\n problem: \"hash-mismatch\" | \"size-mismatch\" | \"not-in-manifest\" | \"missing-on-disk\";\n};\n\n/** Paths that are on disk but were never read: `exact` entries stand for themselves, `folders` for everything beneath them. */\nexport interface PresentUnread {\n exact: readonly string[];\n folders: readonly string[];\n}\n\n/** Compares files against a manifest. One problem per path; a size difference is reported before a hash difference. */\nexport function verifyManifest(\n manifest: Manifest,\n files: BundleFile[],\n alsoPresent: PresentUnread = { exact: [], folders: [] },\n): ManifestProblem[] {\n const problems: ManifestProblem[] = [];\n const seen = new Set<string>(alsoPresent.exact);\n for (const file of files) {\n if (file.path === MANIFEST_NAME) continue;\n seen.add(file.path);\n if (!Object.hasOwn(manifest.files, file.path)) {\n problems.push({ path: file.path, problem: \"not-in-manifest\" });\n continue;\n }\n const entry = manifest.files[file.path];\n if (entry === undefined) continue;\n if (entry.bytes !== file.bytes.length)\n problems.push({ path: file.path, problem: \"size-mismatch\" });\n else if (entry.sha256 !== sha256Hex(file.bytes))\n problems.push({ path: file.path, problem: \"hash-mismatch\" });\n }\n // A path under a hidden folder the walker never entered is present even though no file arrived. A refused\n // entry (a symbolic link, a pipe, an unreadable file) stands only for itself: nothing beneath it is known.\n const underFolder = (path: string): boolean =>\n alsoPresent.folders.some((f) => path === f || path.startsWith(`${f}/`));\n for (const path of Object.keys(manifest.files)) {\n if (!seen.has(path) && !underFolder(path)) problems.push({ path, problem: \"missing-on-disk\" });\n }\n return problems;\n}\n"]}