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,210 @@
1
+ import { fromMarkdown } from "mdast-util-from-markdown";
2
+ import { gfmFootnoteFromMarkdown } from "mdast-util-gfm-footnote";
3
+ import { gfmTableFromMarkdown } from "mdast-util-gfm-table";
4
+ import { gfmFootnote } from "micromark-extension-gfm-footnote";
5
+ import { gfmTable } from "micromark-extension-gfm-table";
6
+ const SENTENCE_CAP = 200;
7
+ export const ANALYSIS_BUDGET = 256 * 1024;
8
+ const MAX_NESTING = 256;
9
+ const MAX_EMPHASIS_RUNS = 2000;
10
+ const MAX_DEFINITIONS = 1000;
11
+ const SCRIPT_LIKE = /<\s*\/?\s*(script|style|iframe)\b/i;
12
+ const DELIMITER_RUN = /[*_]+/g;
13
+ const DEFINITION_LINE = /^ {0,3}\[[^\]]{1,999}\]:/;
14
+ /** An emphasis delimiter run that could open or close: one not wedged between two word characters. */
15
+ function isFlankingRun(text, start, end) {
16
+ const before = start === 0 ? " " : (text[start - 1] ?? " ");
17
+ const after = end >= text.length ? " " : (text[end] ?? " ");
18
+ const wordy = (c) => /[\p{L}\p{N}]/u.test(c);
19
+ return !(wordy(before) && wordy(after));
20
+ }
21
+ /**
22
+ * Why a body must not be parsed, or `undefined` when it may be. The parser's cost grows with the square of the
23
+ * number of emphasis delimiter runs and of link reference definitions, and its stack with nesting depth, so
24
+ * each is counted before any parse. The counts are deterministic and cheap.
25
+ */
26
+ export function analysisBounds(text) {
27
+ if (nestingDepth(text) > MAX_NESTING)
28
+ return "nesting";
29
+ let runs = 0;
30
+ for (const match of text.matchAll(DELIMITER_RUN)) {
31
+ if (isFlankingRun(text, match.index, match.index + match[0].length))
32
+ runs += 1;
33
+ if (runs > MAX_EMPHASIS_RUNS)
34
+ return "emphasis";
35
+ }
36
+ let definitions = 0;
37
+ for (const line of text.split("\n")) {
38
+ if (DEFINITION_LINE.test(line))
39
+ definitions += 1;
40
+ if (definitions > MAX_DEFINITIONS)
41
+ return "definitions";
42
+ }
43
+ return undefined;
44
+ }
45
+ /** The deepest run of block-quote markers or unclosed brackets, measured before any parse so the bound is deterministic. */
46
+ export function nestingDepth(body) {
47
+ let deepest = 0;
48
+ let brackets = 0;
49
+ for (const line of body.split("\n")) {
50
+ const quotes = (/^[ \t>]*/.exec(line)?.[0].match(/>/g) ?? []).length;
51
+ if (quotes > deepest)
52
+ deepest = quotes;
53
+ for (const ch of line) {
54
+ if (ch === "[")
55
+ brackets += 1;
56
+ else if (ch === "]" && brackets > 0)
57
+ brackets -= 1;
58
+ if (brackets > deepest)
59
+ deepest = brackets;
60
+ }
61
+ }
62
+ return deepest;
63
+ }
64
+ function isParent(node) {
65
+ return "children" in node && Array.isArray(node.children);
66
+ }
67
+ /** The prose of a node, leaving out HTML and footnote marks, whitespace collapsed. */
68
+ function proseOf(node) {
69
+ const parts = [];
70
+ const stack = [node];
71
+ while (stack.length > 0) {
72
+ const current = stack.pop();
73
+ if (current.type === "html" || current.type === "footnoteReference")
74
+ continue;
75
+ if ("value" in current && typeof current.value === "string")
76
+ parts.push(current.value);
77
+ else if (isParent(current))
78
+ for (let i = current.children.length - 1; i >= 0; i--)
79
+ stack.push(current.children[i]);
80
+ }
81
+ return parts.join("").replace(/\s+/g, " ").trim();
82
+ }
83
+ const BLOCKS = new Set([
84
+ "paragraph",
85
+ "heading",
86
+ "blockquote",
87
+ "list",
88
+ "listItem",
89
+ "code",
90
+ "table",
91
+ "tableRow",
92
+ "tableCell",
93
+ "thematicBreak",
94
+ "footnoteDefinition",
95
+ ]);
96
+ /** The whole tree's prose, one space between blocks, HTML and footnote marks left out, whitespace collapsed. */
97
+ function proseWithBlocks(tree) {
98
+ const parts = [];
99
+ const stack = [tree];
100
+ while (stack.length > 0) {
101
+ const current = stack.pop();
102
+ if (current.type === "html" || current.type === "footnoteReference")
103
+ continue;
104
+ if (BLOCKS.has(current.type))
105
+ parts.push(" ");
106
+ if ("value" in current && typeof current.value === "string")
107
+ parts.push(current.value);
108
+ else if (isParent(current)) {
109
+ for (let i = current.children.length - 1; i >= 0; i--)
110
+ stack.push(current.children[i]);
111
+ }
112
+ if (BLOCKS.has(current.type))
113
+ parts.push(" ");
114
+ }
115
+ return parts.join("").replace(/\s+/g, " ").trim();
116
+ }
117
+ function firstSentenceOf(text) {
118
+ const match = /^(.*?[.!?])(?=\s|$)/.exec(text);
119
+ const sentence = match?.[1] ?? text;
120
+ return sentence.length > SENTENCE_CAP ? sentence.slice(0, SENTENCE_CAP).trimEnd() : sentence;
121
+ }
122
+ const EMPTY = {
123
+ firstHeading: undefined,
124
+ firstSentence: undefined,
125
+ links: [],
126
+ footnoteReferences: [],
127
+ htmlBlocks: 0,
128
+ inlineHtml: 0,
129
+ hasScriptLike: false,
130
+ };
131
+ /**
132
+ * Reads the facts the OKF layer needs from a Markdown body: CommonMark with footnotes and tables, never the
133
+ * autolink extension, whose cost grows without bound on nested brackets. A body nested past the limit is left
134
+ * unanalysed rather than parsed; a body past the budget is analysed up to it. The tree is walked iteratively.
135
+ */
136
+ export function readBody(body) {
137
+ const truncated = body.length > ANALYSIS_BUDGET;
138
+ const text = truncated ? body.slice(0, ANALYSIS_BUDGET) : body;
139
+ if (analysisBounds(text) !== undefined)
140
+ return { ...EMPTY, links: [], footnoteReferences: [], unanalysed: true, truncated };
141
+ const tree = fromMarkdown(text, {
142
+ extensions: [gfmFootnote(), gfmTable()],
143
+ mdastExtensions: [gfmFootnoteFromMarkdown(), gfmTableFromMarkdown()],
144
+ });
145
+ const facts = {
146
+ ...EMPTY,
147
+ links: [],
148
+ footnoteReferences: [],
149
+ unanalysed: false,
150
+ truncated,
151
+ };
152
+ const definitions = new Map();
153
+ const references = [];
154
+ const stack = [
155
+ { node: tree, parent: undefined, skipped: false },
156
+ ];
157
+ while (stack.length > 0) {
158
+ const { node, parent, skipped } = stack.pop();
159
+ switch (node.type) {
160
+ case "definition":
161
+ definitions.set(node.identifier, node.url);
162
+ break;
163
+ case "heading":
164
+ if (facts.firstHeading === undefined)
165
+ facts.firstHeading = proseOf(node);
166
+ break;
167
+ case "paragraph":
168
+ if (!skipped && facts.firstSentence === undefined) {
169
+ const sentence = firstSentenceOf(proseOf(node));
170
+ if (sentence.length > 0)
171
+ facts.firstSentence = sentence;
172
+ }
173
+ break;
174
+ case "link":
175
+ facts.links.push({ url: node.url, text: proseOf(node) });
176
+ break;
177
+ case "linkReference":
178
+ references.push({ identifier: node.identifier, slot: facts.links.length });
179
+ facts.links.push({ url: "", text: proseOf(node) });
180
+ break;
181
+ case "footnoteReference":
182
+ facts.footnoteReferences.push(node.identifier);
183
+ break;
184
+ case "html":
185
+ if (parent !== undefined && parent.type === "paragraph")
186
+ facts.inlineHtml += 1;
187
+ else
188
+ facts.htmlBlocks += 1;
189
+ if (SCRIPT_LIKE.test(node.value))
190
+ facts.hasScriptLike = true;
191
+ break;
192
+ default:
193
+ break;
194
+ }
195
+ if (isParent(node)) {
196
+ const skip = skipped || node.type === "footnoteDefinition" || node.type === "table";
197
+ for (let i = node.children.length - 1; i >= 0; i--)
198
+ stack.push({ node: node.children[i], parent: node, skipped: skip });
199
+ }
200
+ }
201
+ for (const ref of references) {
202
+ const slot = facts.links[ref.slot];
203
+ if (slot !== undefined)
204
+ slot.url = definitions.get(ref.identifier) ?? "";
205
+ }
206
+ facts.links = facts.links.filter((l) => l.url !== "");
207
+ facts.prose = proseWithBlocks(tree);
208
+ return facts;
209
+ }
210
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../src/bundle/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Nodes, Parent } from \"mdast\";\nimport { fromMarkdown } from \"mdast-util-from-markdown\";\nimport { gfmFootnoteFromMarkdown } from \"mdast-util-gfm-footnote\";\nimport { gfmTableFromMarkdown } from \"mdast-util-gfm-table\";\nimport { gfmFootnote } from \"micromark-extension-gfm-footnote\";\nimport { gfmTable } from \"micromark-extension-gfm-table\";\n\nexport interface BodyFacts {\n firstHeading: string | undefined;\n firstSentence: string | undefined;\n links: Array<{ url: string; text: string }>;\n footnoteReferences: string[];\n htmlBlocks: number;\n inlineHtml: number;\n hasScriptLike: boolean;\n /** The body nests deeper than the analyser allows; no facts were taken from it. */\n unanalysed: boolean;\n /** Only the first part of the body was analysed. */\n truncated: boolean;\n /** The body's text with blocks separated by one space, HTML and footnote marks left out; absent when unanalysed. */\n prose?: string;\n}\n\nconst SENTENCE_CAP = 200;\nexport const ANALYSIS_BUDGET = 256 * 1024;\nconst MAX_NESTING = 256;\nconst MAX_EMPHASIS_RUNS = 2000;\nconst MAX_DEFINITIONS = 1000;\nconst SCRIPT_LIKE = /<\\s*\\/?\\s*(script|style|iframe)\\b/i;\nconst DELIMITER_RUN = /[*_]+/g;\nconst DEFINITION_LINE = /^ {0,3}\\[[^\\]]{1,999}\\]:/;\n\n/** An emphasis delimiter run that could open or close: one not wedged between two word characters. */\nfunction isFlankingRun(text: string, start: number, end: number): boolean {\n const before = start === 0 ? \" \" : (text[start - 1] ?? \" \");\n const after = end >= text.length ? \" \" : (text[end] ?? \" \");\n const wordy = (c: string): boolean => /[\\p{L}\\p{N}]/u.test(c);\n return !(wordy(before) && wordy(after));\n}\n\n/**\n * Why a body must not be parsed, or `undefined` when it may be. The parser's cost grows with the square of the\n * number of emphasis delimiter runs and of link reference definitions, and its stack with nesting depth, so\n * each is counted before any parse. The counts are deterministic and cheap.\n */\nexport function analysisBounds(text: string): \"nesting\" | \"emphasis\" | \"definitions\" | undefined {\n if (nestingDepth(text) > MAX_NESTING) return \"nesting\";\n let runs = 0;\n for (const match of text.matchAll(DELIMITER_RUN)) {\n if (isFlankingRun(text, match.index, match.index + match[0].length)) runs += 1;\n if (runs > MAX_EMPHASIS_RUNS) return \"emphasis\";\n }\n let definitions = 0;\n for (const line of text.split(\"\\n\")) {\n if (DEFINITION_LINE.test(line)) definitions += 1;\n if (definitions > MAX_DEFINITIONS) return \"definitions\";\n }\n return undefined;\n}\n\n/** The deepest run of block-quote markers or unclosed brackets, measured before any parse so the bound is deterministic. */\nexport function nestingDepth(body: string): number {\n let deepest = 0;\n let brackets = 0;\n for (const line of body.split(\"\\n\")) {\n const quotes = (/^[ \\t>]*/.exec(line)?.[0].match(/>/g) ?? []).length;\n if (quotes > deepest) deepest = quotes;\n for (const ch of line) {\n if (ch === \"[\") brackets += 1;\n else if (ch === \"]\" && brackets > 0) brackets -= 1;\n if (brackets > deepest) deepest = brackets;\n }\n }\n return deepest;\n}\n\nfunction isParent(node: Nodes): node is Nodes & Parent {\n return \"children\" in node && Array.isArray((node as Parent).children);\n}\n\n/** The prose of a node, leaving out HTML and footnote marks, whitespace collapsed. */\nfunction proseOf(node: Nodes): string {\n const parts: string[] = [];\n const stack: Nodes[] = [node];\n while (stack.length > 0) {\n const current = stack.pop() as Nodes;\n if (current.type === \"html\" || current.type === \"footnoteReference\") continue;\n if (\"value\" in current && typeof current.value === \"string\") parts.push(current.value);\n else if (isParent(current))\n for (let i = current.children.length - 1; i >= 0; i--)\n stack.push(current.children[i] as Nodes);\n }\n return parts.join(\"\").replace(/\\s+/g, \" \").trim();\n}\n\nconst BLOCKS = new Set([\n \"paragraph\",\n \"heading\",\n \"blockquote\",\n \"list\",\n \"listItem\",\n \"code\",\n \"table\",\n \"tableRow\",\n \"tableCell\",\n \"thematicBreak\",\n \"footnoteDefinition\",\n]);\n\n/** The whole tree's prose, one space between blocks, HTML and footnote marks left out, whitespace collapsed. */\nfunction proseWithBlocks(tree: Nodes): string {\n const parts: string[] = [];\n const stack: Nodes[] = [tree];\n while (stack.length > 0) {\n const current = stack.pop() as Nodes;\n if (current.type === \"html\" || current.type === \"footnoteReference\") continue;\n if (BLOCKS.has(current.type)) parts.push(\" \");\n if (\"value\" in current && typeof current.value === \"string\") parts.push(current.value);\n else if (isParent(current)) {\n for (let i = current.children.length - 1; i >= 0; i--)\n stack.push(current.children[i] as Nodes);\n }\n if (BLOCKS.has(current.type)) parts.push(\" \");\n }\n return parts.join(\"\").replace(/\\s+/g, \" \").trim();\n}\n\nfunction firstSentenceOf(text: string): string {\n const match = /^(.*?[.!?])(?=\\s|$)/.exec(text);\n const sentence = match?.[1] ?? text;\n return sentence.length > SENTENCE_CAP ? sentence.slice(0, SENTENCE_CAP).trimEnd() : sentence;\n}\n\nconst EMPTY: Omit<BodyFacts, \"unanalysed\" | \"truncated\"> = {\n firstHeading: undefined,\n firstSentence: undefined,\n links: [],\n footnoteReferences: [],\n htmlBlocks: 0,\n inlineHtml: 0,\n hasScriptLike: false,\n};\n\n/**\n * Reads the facts the OKF layer needs from a Markdown body: CommonMark with footnotes and tables, never the\n * autolink extension, whose cost grows without bound on nested brackets. A body nested past the limit is left\n * unanalysed rather than parsed; a body past the budget is analysed up to it. The tree is walked iteratively.\n */\nexport function readBody(body: string): BodyFacts {\n const truncated = body.length > ANALYSIS_BUDGET;\n const text = truncated ? body.slice(0, ANALYSIS_BUDGET) : body;\n if (analysisBounds(text) !== undefined)\n return { ...EMPTY, links: [], footnoteReferences: [], unanalysed: true, truncated };\n const tree = fromMarkdown(text, {\n extensions: [gfmFootnote(), gfmTable()],\n mdastExtensions: [gfmFootnoteFromMarkdown(), gfmTableFromMarkdown()],\n });\n const facts: BodyFacts = {\n ...EMPTY,\n links: [],\n footnoteReferences: [],\n unanalysed: false,\n truncated,\n };\n const definitions = new Map<string, string>();\n const references: Array<{ identifier: string; slot: number }> = [];\n const stack: Array<{ node: Nodes; parent: Nodes | undefined; skipped: boolean }> = [\n { node: tree, parent: undefined, skipped: false },\n ];\n while (stack.length > 0) {\n const { node, parent, skipped } = stack.pop() as {\n node: Nodes;\n parent: Nodes | undefined;\n skipped: boolean;\n };\n switch (node.type) {\n case \"definition\":\n definitions.set(node.identifier, node.url);\n break;\n case \"heading\":\n if (facts.firstHeading === undefined) facts.firstHeading = proseOf(node);\n break;\n case \"paragraph\":\n if (!skipped && facts.firstSentence === undefined) {\n const sentence = firstSentenceOf(proseOf(node));\n if (sentence.length > 0) facts.firstSentence = sentence;\n }\n break;\n case \"link\":\n facts.links.push({ url: node.url, text: proseOf(node) });\n break;\n case \"linkReference\":\n references.push({ identifier: node.identifier, slot: facts.links.length });\n facts.links.push({ url: \"\", text: proseOf(node) });\n break;\n case \"footnoteReference\":\n facts.footnoteReferences.push(node.identifier);\n break;\n case \"html\":\n if (parent !== undefined && parent.type === \"paragraph\") facts.inlineHtml += 1;\n else facts.htmlBlocks += 1;\n if (SCRIPT_LIKE.test(node.value)) facts.hasScriptLike = true;\n break;\n default:\n break;\n }\n if (isParent(node)) {\n const skip = skipped || node.type === \"footnoteDefinition\" || node.type === \"table\";\n for (let i = node.children.length - 1; i >= 0; i--)\n stack.push({ node: node.children[i] as Nodes, parent: node, skipped: skip });\n }\n }\n for (const ref of references) {\n const slot = facts.links[ref.slot];\n if (slot !== undefined) slot.url = definitions.get(ref.identifier) ?? \"\";\n }\n facts.links = facts.links.filter((l) => l.url !== \"\");\n facts.prose = proseWithBlocks(tree);\n return facts;\n}\n"]}
@@ -0,0 +1,7 @@
1
+ /** Core types. Pure data; nothing here touches the file system, the engine or the protocol. */
2
+ export const DEFAULT_CAPS = {
3
+ fileBytes: 2 * 1024 * 1024,
4
+ files: 20_000,
5
+ treeBytes: 512 * 1024 * 1024,
6
+ };
7
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/bundle/model.ts"],"names":[],"mappings":"AAAA,+FAA+F;AA+K/F,MAAM,CAAC,MAAM,YAAY,GAAS;IAChC,SAAS,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;IAC1B,KAAK,EAAE,MAAM;IACb,SAAS,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI;CAC7B,CAAC","sourcesContent":["/** Core types. Pure data; nothing here touches the file system, the engine or the protocol. */\n\n/** A bundle-relative path with POSIX separators and no leading slash. */\nexport type PagePath = string;\n\nexport interface BundleFile {\n path: string;\n bytes: Uint8Array;\n}\n\nexport type Status = \"draft\" | \"stable\" | \"deprecated\";\nexport type Trust = \"unverified\" | \"machine-confirmed\" | \"human-reviewed\";\nexport type SpecText = \"2026-08-15\" | \"2026-08-21\";\n\n/** A timestamp as written, with its instant when it parsed. */\nexport interface Timestamp {\n raw: string;\n at?: Date;\n}\n\nexport interface StaleAfter {\n raw: string;\n /** date: overdue from the start of that UTC day; datetime: overdue from that instant; unparseable: never overdue. */\n form: \"date\" | \"datetime\" | \"unparseable\";\n at?: Date;\n}\n\nexport interface Source {\n resource: string;\n id?: string;\n title?: string;\n author?: string;\n usageCount?: number;\n lastModified?: string;\n usageWindow?: { from: string; to: string };\n}\n\nexport interface Verification {\n by: string;\n at?: Timestamp;\n}\n\nexport type LinkKind =\n | \"page\"\n | \"folder\"\n | \"reserved\"\n | \"attachment\"\n | \"anchor\"\n | \"external\"\n | \"broken\";\n\nexport interface Link {\n raw: string;\n kind: LinkKind;\n /** The bundle path the link resolved to, for page, folder, reserved and attachment links. */\n target?: string;\n}\n\nexport type DegradationCode =\n | \"title-from-heading\"\n | \"title-from-filename\"\n | \"description-from-body\"\n | \"description-missing\"\n | \"scalar-coerced\"\n | \"field-ignored\"\n | \"tags-not-list\"\n | \"status-unknown\"\n | \"stale-after-unparseable\"\n | \"stale-after-no-offset\"\n | \"stale-after-unexpected-form\"\n | \"timestamp-invalid\"\n | \"generated-malformed\"\n | \"verified-entry-malformed\"\n | \"source-malformed\"\n | \"footnote-without-source\"\n | \"verification-without-at\"\n | \"body-html\"\n | \"body-unanalysed\"\n | \"body-truncated\"\n | \"index-lists-unserved\"\n | \"replacement-missing\"\n | \"replacement-broken\"\n | \"replacement-external\"\n | \"replacement-not-served\"\n | \"replacement-self\"\n | \"replacement-not-a-page\"\n | \"frontmatter-warning\"\n | \"reserved-frontmatter-unparseable\"\n | \"reserved-unanalysed\"\n | \"okf-version-unknown\";\n\nexport interface Degradation {\n path: string;\n code: DegradationCode;\n field: string;\n detail: string;\n}\n\nexport type RefusalRule =\n | \"no-frontmatter\"\n | \"frontmatter-unparseable\"\n | \"body-unreadable\"\n | \"no-type\"\n | \"not-utf8\"\n | \"symlink\"\n | \"gitlink\"\n | \"path-escape\"\n | \"special-file\"\n | \"unreadable\"\n | \"engine-config\"\n | \"hash-mismatch\"\n | \"size-mismatch\"\n | \"not-in-manifest\"\n | \"manifest-missing\"\n | \"manifest-invalid\"\n | \"oversize\"\n | \"too-many-files\"\n | \"tree-too-large\"\n | \"bundle-path-missing\";\n\nexport interface Refusal {\n path: string;\n rule: RefusalRule;\n detail: string;\n}\n\nexport interface Page {\n path: PagePath;\n folder: string;\n hash: string;\n type: string;\n title: string;\n titleSource: \"frontmatter\" | \"heading\" | \"filename\";\n description?: string;\n descriptionSource: \"frontmatter\" | \"body\" | \"none\";\n tags: string[];\n status: Status;\n statusSource: \"frontmatter\" | \"default\";\n statusRaw?: string;\n staleAfter?: StaleAfter;\n generated?: { by: string; at?: Timestamp };\n verified: Verification[];\n /** The verification with the latest instant; among entries without one, the last listed. */\n latestVerification?: Verification;\n trust: Trust;\n sources: Source[];\n usageWindow?: { from: string; to: string };\n resource?: string;\n replacement?: PagePath;\n links: Link[];\n footnoteReferences: string[];\n frontmatter: Record<string, unknown>;\n body: string;\n /** The body's prose for snippets, captured once at load; absent when the body could not be analysed. */\n prose?: string;\n degradations: Degradation[];\n}\n\nexport interface ReservedFile {\n kind: \"index\" | \"log\";\n path: string;\n folder: string;\n frontmatter?: Record<string, unknown>;\n body: string;\n text: string;\n okfVersion?: string;\n degradations: Degradation[];\n}\n\nexport interface Caps {\n fileBytes: number;\n files: number;\n treeBytes: number;\n}\n\nexport const DEFAULT_CAPS: Caps = {\n fileBytes: 2 * 1024 * 1024,\n files: 20_000,\n treeBytes: 512 * 1024 * 1024,\n};\n\nexport interface LoadOptions {\n admit: Status[];\n dev: boolean;\n integrity: \"require-manifest\" | \"none\";\n specText: SpecText;\n caps: Caps;\n types?: string[];\n /** Refusals the file walker produced before the core saw the files (symbolic links, escapes, oversize). */\n walkRefusals?: Refusal[];\n /** Dot-leading paths the walker found and never read; counted, present for the manifest, refused when they are the engine's. */\n hiddenPaths?: string[];\n /** The hidden paths that are folders: a manifest entry beneath one is present even though no file arrived. */\n hiddenFolders?: string[];\n /** The walker's own bundle-level refusal, when the caps stopped it. */\n walkFatal?: Refusal;\n}\n\nexport interface Report {\n loadedAt: Date;\n commit?: string;\n /** A bundle-level refusal; when set, nothing should be served. */\n fatal?: Refusal;\n /** Whether the manifest was verified against the files, or integrity was not required. */\n integrity: \"checked\" | \"skipped\";\n admitted: number;\n excludedByStatus: number;\n attachments: number;\n hidden: number;\n refusals: Refusal[];\n degradations: Degradation[];\n unknownTypes: string[];\n unknownStatuses: Array<{ path: string; value: string }>;\n brokenLinks: Array<{ from: PagePath; raw: string }>;\n linksToUnserved: Array<{ from: PagePath; raw: string; target: string }>;\n missingOnDisk: string[];\n foldersWithoutIndex: string[];\n encodedFolders: string[];\n}\n"]}
@@ -0,0 +1,374 @@
1
+ import { decodeUtf8, parseFrontmatter, splitFrontmatter } from "./frontmatter.js";
2
+ import { resolveLink } from "./links.js";
3
+ import { sha256Hex } from "./manifest.js";
4
+ import { readBody } from "./markdown.js";
5
+ import { folderOf } from "./paths.js";
6
+ import { parseTimestamp } from "./timestamp.js";
7
+ const STATUSES = new Set(["draft", "stable", "deprecated"]);
8
+ function isRecord(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+ /** What a value is, for a sentence: "a list", "a mapping", "a number", "empty". */
12
+ function kindOf(value) {
13
+ if (value === null || value === undefined)
14
+ return "empty";
15
+ if (Array.isArray(value))
16
+ return "a list";
17
+ if (typeof value === "object")
18
+ return "a mapping";
19
+ return `a ${typeof value}`;
20
+ }
21
+ function stemOf(path) {
22
+ return path.slice(path.lastIndexOf("/") + 1).replace(/\.md$/, "");
23
+ }
24
+ /** The verification with the latest instant; among entries without one, the last listed. */
25
+ export function latestVerification(verified) {
26
+ let best;
27
+ let bestAt = Number.NEGATIVE_INFINITY;
28
+ for (const v of verified) {
29
+ const at = v.at?.at?.getTime();
30
+ if (at === undefined) {
31
+ if (best === undefined)
32
+ best = v;
33
+ continue;
34
+ }
35
+ if (best === undefined || best.at?.at === undefined || at >= bestAt) {
36
+ best = v;
37
+ bestAt = at;
38
+ }
39
+ }
40
+ if (best !== undefined && best.at?.at === undefined) {
41
+ for (const v of verified)
42
+ if (v.at?.at === undefined)
43
+ best = v;
44
+ }
45
+ return best;
46
+ }
47
+ /** Turns one file into a page, deriving every field with its source and a degradation wherever a fallback was used. */
48
+ export function parsePage(file, ctx) {
49
+ const path = file.path;
50
+ const degradations = [];
51
+ const degrade = (code, field, detail) => {
52
+ degradations.push({ path, code, field, detail });
53
+ };
54
+ const refuse = (rule, detail) => ({
55
+ ok: false,
56
+ refusal: { path, rule, detail },
57
+ });
58
+ const decoded = decodeUtf8(file.bytes);
59
+ if (!decoded.ok)
60
+ return refuse("not-utf8", "the file is not valid UTF-8");
61
+ const split = splitFrontmatter(decoded.text);
62
+ if (split.block === undefined) {
63
+ return refuse("no-frontmatter", split.unclosed === true
64
+ ? "the frontmatter block opened on line 1 is never closed"
65
+ : "no frontmatter block opens the file");
66
+ }
67
+ const parsed = parseFrontmatter(split.block, 1);
68
+ if (!parsed.ok)
69
+ return refuse("frontmatter-unparseable", parsed.error);
70
+ const { data, sources: scalarText } = parsed;
71
+ for (const warning of parsed.warnings)
72
+ degrade("frontmatter-warning", "frontmatter", warning);
73
+ /** A top-level value read as text: strings as they are, numbers and booleans by their source text, reported. */
74
+ const text = (key) => {
75
+ const value = data[key];
76
+ if (typeof value === "string")
77
+ return value;
78
+ if (typeof value === "number" || typeof value === "boolean") {
79
+ const asText = scalarText[key] ?? String(value);
80
+ degrade("scalar-coerced", key, `${key} is a ${typeof value}, read as "${asText}"`);
81
+ return asText;
82
+ }
83
+ return undefined;
84
+ };
85
+ if (!Object.hasOwn(data, "type"))
86
+ return refuse("no-type", "the frontmatter has no type");
87
+ const type = text("type")?.trim();
88
+ if (type === undefined)
89
+ return refuse("no-type", `type is ${kindOf(data.type)}, not text`);
90
+ if (type.length === 0)
91
+ return refuse("no-type", "type is empty");
92
+ const facts = readBody(split.body);
93
+ if (facts.unanalysed) {
94
+ degrade("body-unanalysed", "body", "the body nests deeper than the analyser allows; no title, description or links were taken from it");
95
+ }
96
+ if (facts.truncated)
97
+ degrade("body-truncated", "body", "only the first 256 KiB of the body were analysed");
98
+ let title = text("title")?.trim();
99
+ let titleSource = "frontmatter";
100
+ if (Object.hasOwn(data, "title") && title === undefined) {
101
+ degrade("field-ignored", "title", `title is ${kindOf(data.title)}, not text; ignored`);
102
+ }
103
+ if (title === undefined || title.length === 0) {
104
+ if (facts.firstHeading !== undefined && facts.firstHeading.length > 0) {
105
+ title = facts.firstHeading;
106
+ titleSource = "heading";
107
+ degrade("title-from-heading", "title", `no title field; the first heading "${title}" is used`);
108
+ }
109
+ else {
110
+ title = stemOf(path);
111
+ titleSource = "filename";
112
+ degrade("title-from-filename", "title", `no title field and no heading; the file name "${title}" is used`);
113
+ }
114
+ }
115
+ let description = text("description")?.trim();
116
+ let descriptionSource = "frontmatter";
117
+ if (Object.hasOwn(data, "description") && description === undefined) {
118
+ degrade("field-ignored", "description", `description is ${kindOf(data.description)}, not text; ignored`);
119
+ }
120
+ if (description === undefined || description.length === 0) {
121
+ if (facts.firstSentence !== undefined) {
122
+ description = facts.firstSentence;
123
+ descriptionSource = "body";
124
+ degrade("description-from-body", "description", "no description field; the body's first sentence is used");
125
+ }
126
+ else {
127
+ description = undefined;
128
+ descriptionSource = "none";
129
+ degrade("description-missing", "description", "no description field and no body text to take one from");
130
+ }
131
+ }
132
+ const tags = [];
133
+ const rawTags = data.tags;
134
+ if (Array.isArray(rawTags)) {
135
+ rawTags.forEach((tag, i) => {
136
+ if (typeof tag === "string")
137
+ tags.push(tag);
138
+ else if (typeof tag === "number" || typeof tag === "boolean")
139
+ tags.push(String(tag));
140
+ else
141
+ degrade("field-ignored", "tags", `tags[${i}] is ${kindOf(tag)}, not text; ignored`);
142
+ });
143
+ }
144
+ else if (typeof rawTags === "string") {
145
+ tags.push(rawTags);
146
+ degrade("tags-not-list", "tags", "tags is a single string, read as one tag");
147
+ }
148
+ else if (rawTags !== undefined && rawTags !== null) {
149
+ degrade("tags-not-list", "tags", `tags is ${kindOf(rawTags)}, not a list; ignored`);
150
+ }
151
+ const statusResult = normaliseStatus(data.status, Object.hasOwn(data, "status"), degrade);
152
+ let staleAfter;
153
+ if (Object.hasOwn(data, "stale_after") &&
154
+ data.stale_after !== null &&
155
+ data.stale_after !== undefined) {
156
+ const raw = text("stale_after") ?? JSON.stringify(data.stale_after);
157
+ const ts = parseTimestamp(raw);
158
+ const expected = ctx.specText === "2026-08-15" ? "date" : "datetime";
159
+ if (ts.kind === "date")
160
+ staleAfter = { raw, form: "date", at: ts.at };
161
+ else if (ts.kind === "datetime") {
162
+ staleAfter = { raw, form: "datetime", at: ts.at };
163
+ if (!ts.offset)
164
+ degrade("stale-after-no-offset", "stale_after", `${raw} has no offset and is read as UTC`);
165
+ }
166
+ else {
167
+ staleAfter = { raw, form: "unparseable" };
168
+ degrade("stale-after-unparseable", "stale_after", `${raw} is not a date or a datetime; the page is never overdue`);
169
+ }
170
+ if (staleAfter.form !== "unparseable" && staleAfter.form !== expected) {
171
+ degrade("stale-after-unexpected-form", "stale_after", `the ${ctx.specText} text expects a ${expected}; ${raw} is a ${staleAfter.form}`);
172
+ }
173
+ }
174
+ const timestamp = (value, field) => {
175
+ const raw = typeof value === "string" ? value : JSON.stringify(value);
176
+ const ts = parseTimestamp(raw);
177
+ if (ts.kind === "invalid") {
178
+ degrade("timestamp-invalid", field, `${raw} is not a date or a datetime`);
179
+ return { raw };
180
+ }
181
+ return { raw, at: ts.at };
182
+ };
183
+ let generated;
184
+ if (Object.hasOwn(data, "generated")) {
185
+ const g = data.generated;
186
+ if (isRecord(g) && typeof g.by === "string" && g.by.trim().length > 0) {
187
+ generated = { by: g.by.trim() };
188
+ if (Object.hasOwn(g, "at"))
189
+ generated.at = timestamp(g.at, "generated.at");
190
+ }
191
+ else {
192
+ degrade("generated-malformed", "generated", `generated is ${kindOf(g)} without a by actor; ignored`);
193
+ }
194
+ }
195
+ const verified = [];
196
+ if (Object.hasOwn(data, "verified")) {
197
+ const v = data.verified;
198
+ const entries = Array.isArray(v) ? v : isRecord(v) ? [v] : [];
199
+ if (!Array.isArray(v) && !isRecord(v)) {
200
+ degrade("verified-entry-malformed", "verified", `verified is ${kindOf(v)}, neither a list nor a mapping; ignored`);
201
+ }
202
+ entries.forEach((entry, i) => {
203
+ if (isRecord(entry) && typeof entry.by === "string" && entry.by.trim().length > 0) {
204
+ const verification = { by: entry.by.trim() };
205
+ if (Object.hasOwn(entry, "at") && entry.at !== null)
206
+ verification.at = timestamp(entry.at, `verified[${i}].at`);
207
+ else
208
+ degrade("verification-without-at", "verified", `verified[${i}] by ${verification.by} has no at`);
209
+ verified.push(verification);
210
+ }
211
+ else {
212
+ degrade("verified-entry-malformed", "verified", `verified[${i}] has no by actor; ignored`);
213
+ }
214
+ });
215
+ }
216
+ const trust = verified.length === 0
217
+ ? "unverified"
218
+ : verified.some((v) => v.by.startsWith("human:"))
219
+ ? "human-reviewed"
220
+ : "machine-confirmed";
221
+ const sources = [];
222
+ if (Object.hasOwn(data, "sources")) {
223
+ const s = data.sources;
224
+ if (Array.isArray(s)) {
225
+ s.forEach((entry, i) => {
226
+ if (isRecord(entry) && typeof entry.resource === "string" && entry.resource.length > 0) {
227
+ const source = { resource: entry.resource };
228
+ if (typeof entry.id === "string")
229
+ source.id = entry.id;
230
+ else if (typeof entry.id === "number" || typeof entry.id === "boolean")
231
+ source.id = String(entry.id);
232
+ if (typeof entry.title === "string")
233
+ source.title = entry.title;
234
+ if (typeof entry.author === "string")
235
+ source.author = entry.author;
236
+ if (typeof entry.usage_count === "number") {
237
+ if (Number.isFinite(entry.usage_count))
238
+ source.usageCount = entry.usage_count;
239
+ else
240
+ degrade("source-malformed", "sources", `sources[${i}].usage_count is not a finite number; ignored`);
241
+ }
242
+ if (typeof entry.last_modified === "string")
243
+ source.lastModified = entry.last_modified;
244
+ const w = entry.usage_window;
245
+ if (isRecord(w) && typeof w.from === "string" && typeof w.to === "string")
246
+ source.usageWindow = { from: w.from, to: w.to };
247
+ sources.push(source);
248
+ }
249
+ else {
250
+ degrade("source-malformed", "sources", `sources[${i}] has no resource; ignored`);
251
+ }
252
+ });
253
+ }
254
+ else {
255
+ degrade("source-malformed", "sources", `sources is ${kindOf(s)}, not a list; ignored`);
256
+ }
257
+ }
258
+ let usageWindow;
259
+ const w = data.usage_window;
260
+ if (isRecord(w) && typeof w.from === "string" && typeof w.to === "string")
261
+ usageWindow = { from: w.from, to: w.to };
262
+ const resource = text("resource");
263
+ const links = facts.links.map((l) => {
264
+ const resolved = resolveLink(l.url, path, ctx.linkIndex);
265
+ const link = { raw: l.url, kind: resolved.kind };
266
+ if (resolved.target !== undefined)
267
+ link.target = resolved.target;
268
+ return link;
269
+ });
270
+ // GFM lower-cases footnote identifiers; the join with sources[].id is case-insensitive, and each missing id is reported once.
271
+ const sourceIds = new Set(sources.map((s) => s.id?.toLowerCase()).filter((id) => id !== undefined));
272
+ const missing = new Set();
273
+ for (const id of facts.footnoteReferences)
274
+ if (!sourceIds.has(id.toLowerCase()))
275
+ missing.add(id);
276
+ for (const id of missing)
277
+ degrade("footnote-without-source", "sources", `footnote ${id} has no matching sources entry`);
278
+ if (facts.htmlBlocks > 0 || facts.inlineHtml > 0 || facts.hasScriptLike) {
279
+ const parts = [];
280
+ if (facts.htmlBlocks > 0)
281
+ parts.push(`${facts.htmlBlocks} HTML block${facts.htmlBlocks === 1 ? "" : "s"}`);
282
+ if (facts.inlineHtml > 0)
283
+ parts.push(`${facts.inlineHtml} inline HTML element${facts.inlineHtml === 1 ? "" : "s"}`);
284
+ if (facts.hasScriptLike)
285
+ parts.push("including a script-like element");
286
+ degrade("body-html", "body", `${parts.join(", ")}; indexed as text, never executed, kept out of the description`);
287
+ }
288
+ const page = {
289
+ path,
290
+ folder: folderOf(path),
291
+ hash: sha256Hex(file.bytes),
292
+ type,
293
+ title,
294
+ titleSource,
295
+ descriptionSource,
296
+ tags,
297
+ status: statusResult.status,
298
+ statusSource: statusResult.source,
299
+ verified,
300
+ trust,
301
+ sources,
302
+ links,
303
+ footnoteReferences: facts.footnoteReferences,
304
+ frontmatter: data,
305
+ body: split.body,
306
+ ...(facts.prose === undefined ? {} : { prose: facts.prose }),
307
+ degradations,
308
+ };
309
+ if (description !== undefined)
310
+ page.description = description;
311
+ if (statusResult.raw !== undefined)
312
+ page.statusRaw = statusResult.raw;
313
+ if (staleAfter !== undefined)
314
+ page.staleAfter = staleAfter;
315
+ if (generated !== undefined)
316
+ page.generated = generated;
317
+ const latest = latestVerification(verified);
318
+ if (latest !== undefined)
319
+ page.latestVerification = latest;
320
+ if (usageWindow !== undefined)
321
+ page.usageWindow = usageWindow;
322
+ if (resource !== undefined)
323
+ page.resource = resource;
324
+ return { ok: true, page };
325
+ }
326
+ function normaliseStatus(value, present, degrade) {
327
+ if (!present || value === undefined || value === null)
328
+ return { status: "stable", source: "default" };
329
+ const raw = typeof value === "string" ? value : JSON.stringify(value);
330
+ const normalised = raw.trim().toLowerCase();
331
+ if (normalised.length === 0)
332
+ return { status: "stable", source: "default" };
333
+ if (STATUSES.has(normalised))
334
+ return { status: normalised, source: "frontmatter", raw };
335
+ degrade("status-unknown", "status", `status "${raw}" is not draft, stable or deprecated; treated as draft`);
336
+ return { status: "draft", source: "frontmatter", raw };
337
+ }
338
+ const ARTICLE = {
339
+ folder: "a folder",
340
+ reserved: "an index or log file",
341
+ attachment: "an attachment",
342
+ };
343
+ /**
344
+ * The replacement for a deprecated page: the first body link that is not a same-page anchor decides. It is the
345
+ * replacement when it resolves to an admitted page other than this one; otherwise there is none, and the reason is
346
+ * recorded. Nothing is decided for a page that is not deprecated.
347
+ */
348
+ export function decideReplacement(page, admitted) {
349
+ if (page.status !== "deprecated")
350
+ return {};
351
+ const reason = (code, detail) => ({
352
+ degradation: { path: page.path, code, field: "replacement", detail },
353
+ });
354
+ const first = page.links.find((l) => l.kind !== "anchor");
355
+ if (first === undefined)
356
+ return reason("replacement-missing", "a deprecated page should link to the page that replaced it; no link was found");
357
+ switch (first.kind) {
358
+ case "external":
359
+ return reason("replacement-external", `the first link, ${first.raw}, points outside the bundle`);
360
+ case "broken":
361
+ return reason("replacement-broken", `the first link, ${first.raw}, points at nothing in the bundle`);
362
+ case "page": {
363
+ const target = first.target ?? "";
364
+ if (target === page.path)
365
+ return reason("replacement-self", "the first link points at the page itself");
366
+ if (!admitted.has(target))
367
+ return reason("replacement-not-served", `the first link points at ${target}, which is not served`);
368
+ return { replacement: target };
369
+ }
370
+ default:
371
+ return reason("replacement-not-a-page", `the first link, ${first.raw}, points at ${ARTICLE[first.kind] ?? first.kind}, not a page`);
372
+ }
373
+ }
374
+ //# sourceMappingURL=page.js.map