@hraness/kb 0.17.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.
Files changed (200) hide show
  1. package/DISCLOSURE +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +545 -0
  4. package/dist/agent-context.js +35 -0
  5. package/dist/agent-guide-audit.js +18 -0
  6. package/dist/attachments.js +19 -0
  7. package/dist/authoring.js +33 -0
  8. package/dist/benchmark.js +19 -0
  9. package/dist/capture.js +49 -0
  10. package/dist/cli.js +3646 -0
  11. package/dist/clip/acquire.js +57 -0
  12. package/dist/clip/args.js +21 -0
  13. package/dist/clip/bounded-byte-buffer.js +9 -0
  14. package/dist/clip/browser-profiles.js +17 -0
  15. package/dist/clip/bundle-reader.js +11 -0
  16. package/dist/clip/cli.js +27 -0
  17. package/dist/clip/cookies.js +21 -0
  18. package/dist/clip/defuddle-worker.js +84 -0
  19. package/dist/clip/doctor.js +32 -0
  20. package/dist/clip/jobs.js +784 -0
  21. package/dist/clip/network-proxy.js +9 -0
  22. package/dist/clip/network.js +28 -0
  23. package/dist/clip/persist.js +30 -0
  24. package/dist/clip/refresh.js +11 -0
  25. package/dist/clip/terminal.js +11 -0
  26. package/dist/evaluation-builder.js +12238 -0
  27. package/dist/evaluation-kb.js +26 -0
  28. package/dist/evaluation.js +35 -0
  29. package/dist/git.js +39 -0
  30. package/dist/graph.js +39 -0
  31. package/dist/index-01jj6rbv.js +1103 -0
  32. package/dist/index-06c9ctr6.js +738 -0
  33. package/dist/index-0kavxzqj.js +2480 -0
  34. package/dist/index-1gwbassd.js +743 -0
  35. package/dist/index-1n418kb9.js +827 -0
  36. package/dist/index-1vrd1rmn.js +56 -0
  37. package/dist/index-1xxnjn0d.js +171 -0
  38. package/dist/index-3rm7cz6h.js +373 -0
  39. package/dist/index-3v2z4f0q.js +328 -0
  40. package/dist/index-48pz4jpc.js +392 -0
  41. package/dist/index-4j3tt0c3.js +234 -0
  42. package/dist/index-5n05se68.js +307 -0
  43. package/dist/index-5vwpzb5a.js +764 -0
  44. package/dist/index-6g2pv9d2.js +370 -0
  45. package/dist/index-7fzc32gf.js +712 -0
  46. package/dist/index-84x0vjjp.js +432 -0
  47. package/dist/index-adx6khj5.js +488 -0
  48. package/dist/index-b88v3vtm.js +805 -0
  49. package/dist/index-cv6fh7z5.js +462 -0
  50. package/dist/index-cxfrakt7.js +1067 -0
  51. package/dist/index-d13v9ckt.js +150 -0
  52. package/dist/index-dycjbhw2.js +1943 -0
  53. package/dist/index-dyqwejk5.js +531 -0
  54. package/dist/index-e5fbsywq.js +658 -0
  55. package/dist/index-f984hw45.js +927 -0
  56. package/dist/index-gh719d91.js +63 -0
  57. package/dist/index-hgve9rh2.js +2046 -0
  58. package/dist/index-hya40gb2.js +474 -0
  59. package/dist/index-j4zgmzjr.js +152 -0
  60. package/dist/index-jsmvyyvf.js +1166 -0
  61. package/dist/index-kh0pwvdd.js +1974 -0
  62. package/dist/index-mqx4nd6v.js +119 -0
  63. package/dist/index-mxxxytys.js +1254 -0
  64. package/dist/index-n5dd7r0v.js +711 -0
  65. package/dist/index-npg9z1a4.js +431 -0
  66. package/dist/index-pj501bh1.js +140 -0
  67. package/dist/index-qry4vhxk.js +1041 -0
  68. package/dist/index-s2gw5aw9.js +199 -0
  69. package/dist/index-vvdj6kca.js +70 -0
  70. package/dist/index-w2zc0vwa.js +571 -0
  71. package/dist/index-x3fthpsc.js +585 -0
  72. package/dist/index-zr53sf63.js +44 -0
  73. package/dist/index-zxdy5pby.js +2728 -0
  74. package/dist/index-zzhgcwyt.js +966 -0
  75. package/dist/index.js +453 -0
  76. package/dist/navigation.js +13 -0
  77. package/dist/pdf.js +85 -0
  78. package/dist/percolate.js +24 -0
  79. package/dist/portfolio.js +77 -0
  80. package/dist/query.js +29 -0
  81. package/dist/repository-memory.js +60 -0
  82. package/dist/sdk.js +36 -0
  83. package/dist/search-rules.js +34 -0
  84. package/dist/search.js +22 -0
  85. package/dist/semantic.js +39 -0
  86. package/dist/source-inbox.js +17 -0
  87. package/dist/untrusted-content.js +32 -0
  88. package/dist/url-intelligence.js +74 -0
  89. package/dist/workflow.js +23 -0
  90. package/dist/workflows/decision-context.js +20 -0
  91. package/dist/workflows/explain-change.js +8 -0
  92. package/dist/workflows/index.js +28 -0
  93. package/dist/workflows/plan-radar.js +10 -0
  94. package/package.json +409 -0
  95. package/skills/kb/AGENTS.md +19 -0
  96. package/skills/kb/SKILL.md +82 -0
  97. package/skills/kb/agents/openai.yaml +4 -0
  98. package/skills/kb/references/pdf-review.md +51 -0
  99. package/skills/kb/references/percolate.md +144 -0
  100. package/skills/kb/references/plan-structure.md +105 -0
  101. package/skills/kb/references/plan.md +115 -0
  102. package/skills/kb/references/query.md +231 -0
  103. package/skills/kb/references/refresh.md +133 -0
  104. package/skills/kb/references/save-pdf.md +125 -0
  105. package/skills/kb/references/save-url.md +178 -0
  106. package/skills/kb/references/url-authentication.md +75 -0
  107. package/skills/kb/references/url-platforms.md +25 -0
  108. package/src/agent-context.ts +1241 -0
  109. package/src/agent-guide-audit.ts +750 -0
  110. package/src/attachments.ts +723 -0
  111. package/src/authoring.ts +1725 -0
  112. package/src/benchmark.ts +362 -0
  113. package/src/capture.ts +68 -0
  114. package/src/cli.ts +3660 -0
  115. package/src/clip/acquire.ts +1273 -0
  116. package/src/clip/archive-today.ts +402 -0
  117. package/src/clip/args.ts +423 -0
  118. package/src/clip/assets.ts +262 -0
  119. package/src/clip/bounded-byte-buffer.ts +75 -0
  120. package/src/clip/browser-profiles.ts +372 -0
  121. package/src/clip/bundle-reader.ts +608 -0
  122. package/src/clip/capture.ts +1054 -0
  123. package/src/clip/cli.ts +181 -0
  124. package/src/clip/cookies.ts +512 -0
  125. package/src/clip/doctor.ts +1110 -0
  126. package/src/clip/extract.ts +868 -0
  127. package/src/clip/jobs.ts +959 -0
  128. package/src/clip/lib.ts +861 -0
  129. package/src/clip/media.ts +1474 -0
  130. package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
  131. package/src/clip/metadata-search-tool/Cargo.toml +18 -0
  132. package/src/clip/metadata-search-tool/runner.ts +685 -0
  133. package/src/clip/metadata-search-tool/src/main.rs +389 -0
  134. package/src/clip/metadata-search.ts +687 -0
  135. package/src/clip/network-proxy.ts +627 -0
  136. package/src/clip/network.ts +950 -0
  137. package/src/clip/package-root.ts +54 -0
  138. package/src/clip/persist.ts +1472 -0
  139. package/src/clip/platforms.ts +1824 -0
  140. package/src/clip/refresh.ts +197 -0
  141. package/src/clip/structured.ts +432 -0
  142. package/src/clip/terminal.ts +197 -0
  143. package/src/clip/url-intelligence.ts +581 -0
  144. package/src/clip/url-metadata-backfill.ts +616 -0
  145. package/src/clip/url-metadata-cli.ts +251 -0
  146. package/src/clip/url-metadata.ts +1397 -0
  147. package/src/evaluation-analysis-v2.ts +1440 -0
  148. package/src/evaluation-builder.ts +2539 -0
  149. package/src/evaluation-corpus-authoring.ts +2052 -0
  150. package/src/evaluation-evidence.ts +1147 -0
  151. package/src/evaluation-execution-v2.ts +426 -0
  152. package/src/evaluation-implementation.ts +131 -0
  153. package/src/evaluation-kb-closure.ts +1430 -0
  154. package/src/evaluation-kb-runner-v2.ts +735 -0
  155. package/src/evaluation-kb-v2.ts +2567 -0
  156. package/src/evaluation-kb.ts +1018 -0
  157. package/src/evaluation-measurement-v2.ts +1462 -0
  158. package/src/evaluation-packing-v2.ts +452 -0
  159. package/src/evaluation-redaction.ts +28 -0
  160. package/src/evaluation-v2.ts +3857 -0
  161. package/src/evaluation.ts +1220 -0
  162. package/src/git.ts +1276 -0
  163. package/src/graph.ts +1487 -0
  164. package/src/index.ts +20 -0
  165. package/src/init.ts +123 -0
  166. package/src/navigation.ts +277 -0
  167. package/src/note-lock.ts +529 -0
  168. package/src/pdf/args.ts +202 -0
  169. package/src/pdf/capture.ts +298 -0
  170. package/src/pdf/cli.ts +159 -0
  171. package/src/pdf/extract.ts +303 -0
  172. package/src/pdf/layout.ts +618 -0
  173. package/src/pdf/markdown.ts +154 -0
  174. package/src/pdf/model.ts +234 -0
  175. package/src/pdf/ocr.ts +190 -0
  176. package/src/pdf/persist.ts +314 -0
  177. package/src/pdf/source.ts +135 -0
  178. package/src/pdf/tools.ts +146 -0
  179. package/src/pdf.ts +92 -0
  180. package/src/percolate.ts +877 -0
  181. package/src/portfolio-audit.ts +574 -0
  182. package/src/portfolio-identity.ts +162 -0
  183. package/src/portfolio-registry.ts +473 -0
  184. package/src/portfolio.ts +765 -0
  185. package/src/query.ts +646 -0
  186. package/src/repository-memory.ts +1150 -0
  187. package/src/sdk.ts +1452 -0
  188. package/src/search-rules.ts +712 -0
  189. package/src/search.ts +710 -0
  190. package/src/semantic-runtime.ts +1139 -0
  191. package/src/semantic.ts +2480 -0
  192. package/src/source-inbox.ts +232 -0
  193. package/src/untrusted-content.ts +401 -0
  194. package/src/url-intelligence.ts +104 -0
  195. package/src/vault.ts +636 -0
  196. package/src/workflow.ts +653 -0
  197. package/src/workflows/decision-context.ts +81 -0
  198. package/src/workflows/explain-change.ts +73 -0
  199. package/src/workflows/index.ts +3 -0
  200. package/src/workflows/plan-radar.ts +101 -0
@@ -0,0 +1,431 @@
1
+ // @bun
2
+ import {
3
+ CAPTURE_MANIFEST_FILENAME,
4
+ CAPTURE_SOURCE_EVIDENCE_PATH
5
+ } from "./index-mxxxytys.js";
6
+
7
+ // src/clip/bundle-reader.ts
8
+ import { createHash } from "crypto";
9
+ import { constants as fsConstants } from "fs";
10
+ import { lstat, open, opendir, realpath } from "fs/promises";
11
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from "path";
12
+ var MAX_MANIFEST_BYTES = 1 * 1024 * 1024;
13
+ var DEFAULT_MAX_DOCUMENT_BYTES = 64 * 1024 * 1024;
14
+ var MAX_DOCUMENT_BYTES = 256 * 1024 * 1024;
15
+ var DEFAULT_MAX_SOURCE_BYTES = 32 * 1024 * 1024;
16
+ var MAX_SOURCE_BYTES = 128 * 1024 * 1024;
17
+ var MAX_ASSETS = 1e4;
18
+ var MAX_LEGACY_DIRECTORY_ENTRIES = 1000;
19
+ var DEFAULT_MAX_VERIFIED_ASSETS = 1000;
20
+ var DEFAULT_MAX_ASSET_BYTES = 100 * 1024 * 1024;
21
+ var DEFAULT_MAX_TOTAL_ASSET_BYTES = 500 * 1024 * 1024;
22
+ var MAX_ASSET_BYTES = 8 * 1024 ** 3;
23
+ var MAX_TOTAL_ASSET_BYTES = 8 * 1024 ** 3;
24
+ var DEFAULT_ASSET_VERIFICATION_MS = 30000;
25
+ var MAX_ASSET_VERIFICATION_MS = 5 * 60 * 1000;
26
+ var SHA256_PATTERN = /^[0-9a-f]{64}$/u;
27
+ function object(value, label) {
28
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
29
+ throw new Error(`${label} must be an object`);
30
+ }
31
+ return value;
32
+ }
33
+ function optionalString(value) {
34
+ return typeof value === "string" ? value : null;
35
+ }
36
+ function boundedInteger(value, fallback, maximum, label) {
37
+ const selected = value ?? fallback;
38
+ if (!Number.isSafeInteger(selected) || selected < 1 || selected > maximum) {
39
+ throw new RangeError(`${label} must be an integer from 1 through ${maximum}`);
40
+ }
41
+ return selected;
42
+ }
43
+ function confinedRelativePath(value, label) {
44
+ if (typeof value !== "string" || value.length === 0 || value.length > 4096 || value.includes("\x00")) {
45
+ throw new Error(`${label} must be a bounded non-empty path`);
46
+ }
47
+ if (isAbsolute(value) || value.includes("\\"))
48
+ throw new Error(`${label} must be a relative POSIX path`);
49
+ const segments = value.split("/");
50
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
51
+ throw new Error(`${label} contains an unsafe segment`);
52
+ }
53
+ return value;
54
+ }
55
+
56
+ class MissingCaptureAssetError extends Error {
57
+ }
58
+ function isMissingPathError(error) {
59
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
60
+ }
61
+ function assertAssetVerificationDeadline(deadline) {
62
+ if (deadline !== undefined && performance.now() >= deadline) {
63
+ throw new Error("capture asset verification exceeded its time budget");
64
+ }
65
+ }
66
+ function confined(root, relativePath, label) {
67
+ const path = resolve(root.path, relativePath);
68
+ const lexical = relative(root.path, path);
69
+ if (lexical === "" || lexical === ".." || lexical.startsWith(`..${sep}`) || isAbsolute(lexical)) {
70
+ throw new Error(`${label} escapes the capture bundle`);
71
+ }
72
+ return path;
73
+ }
74
+ async function safeDirectory(pathInput) {
75
+ if (pathInput.trim() === "")
76
+ throw new Error("capture bundle path is required");
77
+ const path = resolve(pathInput);
78
+ const canonical = await realpath(path);
79
+ if (canonical !== path)
80
+ throw new Error("capture bundle path must be canonical and cannot traverse a symbolic link");
81
+ const stats = await lstat(path, { bigint: true });
82
+ if (!stats.isDirectory() || stats.isSymbolicLink())
83
+ throw new Error("capture bundle path must be a real directory");
84
+ if (dirname(path) === path)
85
+ throw new Error("refusing a filesystem root as a capture bundle");
86
+ const handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
87
+ try {
88
+ const opened = await handle.stat({ bigint: true });
89
+ if (!opened.isDirectory() || opened.dev !== stats.dev || opened.ino !== stats.ino) {
90
+ throw new Error("capture bundle root changed while it was opened");
91
+ }
92
+ return { path, handle, device: opened.dev, inode: opened.ino };
93
+ } catch (error) {
94
+ await handle.close();
95
+ throw error;
96
+ }
97
+ }
98
+ async function assertBundleRoot(root, deadline) {
99
+ assertAssetVerificationDeadline(deadline);
100
+ const [opened, named, canonical] = await Promise.all([
101
+ root.handle.stat({ bigint: true }),
102
+ lstat(root.path, { bigint: true }),
103
+ realpath(root.path)
104
+ ]);
105
+ assertAssetVerificationDeadline(deadline);
106
+ if (canonical !== root.path || !opened.isDirectory() || !named.isDirectory() || named.isSymbolicLink() || opened.dev !== root.device || opened.ino !== root.inode || named.dev !== root.device || named.ino !== root.inode)
107
+ throw new Error("capture bundle root changed during inspection");
108
+ }
109
+ async function assertAncestorChain(root, relativePath, label, deadline) {
110
+ await assertBundleRoot(root, deadline);
111
+ const segments = confinedRelativePath(relativePath, label).split("/");
112
+ let current = root.path;
113
+ for (const segment of segments.slice(0, -1)) {
114
+ current = resolve(current, segment);
115
+ assertAssetVerificationDeadline(deadline);
116
+ const metadata = await lstat(current, { bigint: true });
117
+ assertAssetVerificationDeadline(deadline);
118
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
119
+ throw new Error(`${label} must not traverse a linked or non-directory ancestor`);
120
+ }
121
+ const canonical = await realpath(current);
122
+ assertAssetVerificationDeadline(deadline);
123
+ if (canonical !== current) {
124
+ throw new Error(`${label} must not traverse a filesystem alias`);
125
+ }
126
+ }
127
+ await assertBundleRoot(root, deadline);
128
+ }
129
+ async function withRegularFile(root, relativePath, maximumBytes, label, read, deadline, missingError) {
130
+ await assertAncestorChain(root, relativePath, label, deadline);
131
+ const path = confined(root, relativePath, label);
132
+ assertAssetVerificationDeadline(deadline);
133
+ let before;
134
+ try {
135
+ before = await lstat(path, { bigint: true });
136
+ } catch (error) {
137
+ assertAssetVerificationDeadline(deadline);
138
+ if (missingError !== undefined && isMissingPathError(error))
139
+ throw missingError;
140
+ throw error;
141
+ }
142
+ assertAssetVerificationDeadline(deadline);
143
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n) {
144
+ throw new Error(`${label} must be a regular single-link file`);
145
+ }
146
+ if (before.size > BigInt(maximumBytes))
147
+ throw new Error(`${label} exceeds ${maximumBytes} bytes`);
148
+ const canonical = await realpath(path);
149
+ assertAssetVerificationDeadline(deadline);
150
+ if (canonical !== path)
151
+ throw new Error(`${label} must not traverse a filesystem alias`);
152
+ const handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
153
+ assertAssetVerificationDeadline(deadline);
154
+ try {
155
+ const opened = await handle.stat({ bigint: true });
156
+ assertAssetVerificationDeadline(deadline);
157
+ if (!opened.isFile() || opened.nlink !== 1n || opened.dev !== before.dev || opened.ino !== before.ino || opened.size !== before.size)
158
+ throw new Error(`${label} changed while it was opened`);
159
+ await assertAncestorChain(root, relativePath, label, deadline);
160
+ const result = await read(handle, Number(opened.size));
161
+ assertAssetVerificationDeadline(deadline);
162
+ const after = await handle.stat({ bigint: true });
163
+ assertAssetVerificationDeadline(deadline);
164
+ const named = await lstat(path, { bigint: true });
165
+ assertAssetVerificationDeadline(deadline);
166
+ await assertAncestorChain(root, relativePath, label, deadline);
167
+ const finalCanonical = await realpath(path);
168
+ assertAssetVerificationDeadline(deadline);
169
+ if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.mtimeNs !== before.mtimeNs || after.nlink !== 1n || named.dev !== before.dev || named.ino !== before.ino || named.size !== before.size || named.mtimeNs !== before.mtimeNs || named.nlink !== 1n || named.isSymbolicLink() || finalCanonical !== path)
170
+ throw new Error(`${label} changed while it was read`);
171
+ return result;
172
+ } finally {
173
+ await handle.close();
174
+ assertAssetVerificationDeadline(deadline);
175
+ }
176
+ }
177
+ async function readRegularFile(root, relativePath, maximumBytes, label) {
178
+ return withRegularFile(root, relativePath, maximumBytes, label, async (handle, size) => {
179
+ const bytes = Buffer.alloc(size);
180
+ let offset = 0;
181
+ while (offset < bytes.byteLength) {
182
+ const result = await handle.read(bytes, offset, bytes.byteLength - offset, offset);
183
+ if (result.bytesRead === 0)
184
+ throw new Error(`${label} changed while it was read`);
185
+ offset += result.bytesRead;
186
+ }
187
+ return bytes;
188
+ });
189
+ }
190
+ async function hashRegularFile(root, relativePath, maximumBytes, deadline, label) {
191
+ assertAssetVerificationDeadline(deadline);
192
+ await assertBundleRoot(root, deadline);
193
+ assertAssetVerificationDeadline(deadline);
194
+ const path = confined(root, relativePath, label);
195
+ assertAssetVerificationDeadline(deadline);
196
+ try {
197
+ await lstat(path, { bigint: true });
198
+ } catch (error) {
199
+ assertAssetVerificationDeadline(deadline);
200
+ if (isMissingPathError(error)) {
201
+ await assertBundleRoot(root, deadline);
202
+ throw new MissingCaptureAssetError(`capture asset ${relativePath} is missing`);
203
+ }
204
+ throw error;
205
+ }
206
+ assertAssetVerificationDeadline(deadline);
207
+ return withRegularFile(root, relativePath, maximumBytes, label, async (handle, size) => {
208
+ assertAssetVerificationDeadline(deadline);
209
+ const hash = createHash("sha256");
210
+ const chunk = Buffer.allocUnsafe(Math.min(1 * 1024 * 1024, Math.max(1, size)));
211
+ let offset = 0;
212
+ while (offset < size) {
213
+ assertAssetVerificationDeadline(deadline);
214
+ const result = await handle.read(chunk, 0, Math.min(chunk.byteLength, size - offset), offset);
215
+ assertAssetVerificationDeadline(deadline);
216
+ if (result.bytesRead === 0)
217
+ throw new Error(`${label} changed while it was read`);
218
+ hash.update(chunk.subarray(0, result.bytesRead));
219
+ offset += result.bytesRead;
220
+ }
221
+ assertAssetVerificationDeadline(deadline);
222
+ return { bytes: size, sha256: hash.digest("hex") };
223
+ }, deadline, new MissingCaptureAssetError(`capture asset ${relativePath} is missing`));
224
+ }
225
+ function digest(bytes) {
226
+ return createHash("sha256").update(bytes).digest("hex");
227
+ }
228
+ function manifestVersion(manifest) {
229
+ const version = manifest.schemaVersion;
230
+ if (version === 1 || version === 2 || version === 3 || version === 4)
231
+ return version;
232
+ throw new Error("capture manifest schemaVersion must be 1, 2, 3, or 4");
233
+ }
234
+ async function legacyMarkdownPath(root) {
235
+ await assertBundleRoot(root);
236
+ const expected = `${basename(root.path)}.md`;
237
+ const directory = await opendir(root.path);
238
+ const markdown = [];
239
+ let observedEntries = 0;
240
+ try {
241
+ while (true) {
242
+ const entry = await directory.read();
243
+ if (entry === null)
244
+ break;
245
+ observedEntries += 1;
246
+ if (observedEntries > MAX_LEGACY_DIRECTORY_ENTRIES) {
247
+ throw new Error(`legacy capture bundle contains more than ${MAX_LEGACY_DIRECTORY_ENTRIES} top-level entries`);
248
+ }
249
+ if (entry.isFile() && !entry.isSymbolicLink() && entry.name.endsWith(".md")) {
250
+ markdown.push(entry.name);
251
+ }
252
+ }
253
+ } finally {
254
+ await directory.close();
255
+ }
256
+ await assertBundleRoot(root);
257
+ if (markdown.includes(expected))
258
+ return expected;
259
+ if (markdown.length === 1 && markdown[0] !== undefined)
260
+ return markdown[0];
261
+ throw new Error("legacy capture bundle must contain exactly one identifiable top-level Markdown document");
262
+ }
263
+ function v4Document(manifest) {
264
+ const document = object(manifest.document, "capture manifest document");
265
+ const path = confinedRelativePath(document.path, "capture manifest document.path");
266
+ if (!path.endsWith(".md"))
267
+ throw new Error("capture manifest document.path must name Markdown");
268
+ if (!Number.isSafeInteger(document.bytes) || document.bytes < 0) {
269
+ throw new Error("capture manifest document.bytes must be a non-negative safe integer");
270
+ }
271
+ if (typeof document.sha256 !== "string" || !SHA256_PATTERN.test(document.sha256)) {
272
+ throw new Error("capture manifest document.sha256 must be a lowercase SHA-256 digest");
273
+ }
274
+ return { path, bytes: document.bytes, sha256: document.sha256 };
275
+ }
276
+ function manifestAssets(manifest) {
277
+ if (manifest.assets === undefined)
278
+ return [];
279
+ if (!Array.isArray(manifest.assets) || manifest.assets.length > MAX_ASSETS) {
280
+ throw new Error(`capture manifest assets must contain at most ${MAX_ASSETS} entries`);
281
+ }
282
+ const seen = new Set;
283
+ return Object.freeze(manifest.assets.map((raw, index) => {
284
+ const asset = object(raw, `capture manifest assets[${index}]`);
285
+ const path = confinedRelativePath(asset.path, `capture manifest assets[${index}].path`);
286
+ if (seen.has(path))
287
+ throw new Error(`capture manifest repeats asset path ${path}`);
288
+ seen.add(path);
289
+ const bytes = Number.isSafeInteger(asset.bytes) && asset.bytes >= 0 ? asset.bytes : null;
290
+ const sha256 = typeof asset.sha256 === "string" && SHA256_PATTERN.test(asset.sha256) ? asset.sha256 : null;
291
+ return { path, bytes, sha256 };
292
+ }));
293
+ }
294
+ function sourceHtmlPath(manifest) {
295
+ if (manifest.evidence === undefined)
296
+ return null;
297
+ const evidence = object(manifest.evidence, "capture manifest evidence");
298
+ const raw = evidence.sourceHtmlPath;
299
+ if (raw === null || raw === undefined)
300
+ return null;
301
+ const path = confinedRelativePath(raw, "capture manifest evidence.sourceHtmlPath");
302
+ if (path !== CAPTURE_SOURCE_EVIDENCE_PATH) {
303
+ throw new Error("capture manifest source HTML path is not the owned evidence path");
304
+ }
305
+ return path;
306
+ }
307
+ async function readCaptureBundle(path, options = {}) {
308
+ const root = await safeDirectory(path);
309
+ try {
310
+ const manifestBytes = await readRegularFile(root, CAPTURE_MANIFEST_FILENAME, MAX_MANIFEST_BYTES, "capture manifest");
311
+ let parsed;
312
+ try {
313
+ parsed = JSON.parse(manifestBytes.toString("utf8"));
314
+ } catch {
315
+ throw new Error("capture manifest is not valid JSON");
316
+ }
317
+ const manifest = object(parsed, "capture manifest");
318
+ const schemaVersion = manifestVersion(manifest);
319
+ if (typeof manifest.sourceUrl !== "string")
320
+ throw new Error("capture manifest sourceUrl is required");
321
+ const expectedDocument = schemaVersion === 4 ? v4Document(manifest) : null;
322
+ const documentPath = expectedDocument?.path ?? await legacyMarkdownPath(root);
323
+ const documentBytes = await readRegularFile(root, documentPath, boundedInteger(options.maxDocumentBytes, DEFAULT_MAX_DOCUMENT_BYTES, MAX_DOCUMENT_BYTES, "document byte limit"), "capture document");
324
+ const documentSha256 = digest(documentBytes);
325
+ const documentIntegrity = expectedDocument === null ? "unavailable" : expectedDocument.bytes === documentBytes.byteLength && expectedDocument.sha256 === documentSha256 ? "verified" : "mismatch";
326
+ const configuredAssets = manifestAssets(manifest);
327
+ const maximumVerifiedAssets = boundedInteger(options.maxVerifiedAssets, DEFAULT_MAX_VERIFIED_ASSETS, MAX_ASSETS, "verified asset count limit");
328
+ if (options.verifyAssets && configuredAssets.length > maximumVerifiedAssets) {
329
+ throw new Error(`capture asset verification exceeds the ${maximumVerifiedAssets}-file limit`);
330
+ }
331
+ const maximumAssetBytes = boundedInteger(options.maxAssetBytes, DEFAULT_MAX_ASSET_BYTES, MAX_ASSET_BYTES, "asset byte limit");
332
+ const maximumTotalAssetBytes = boundedInteger(options.maxTotalAssetBytes, DEFAULT_MAX_TOTAL_ASSET_BYTES, MAX_TOTAL_ASSET_BYTES, "total asset byte limit");
333
+ if (maximumTotalAssetBytes < maximumAssetBytes) {
334
+ throw new RangeError("total asset byte limit cannot be smaller than the per-asset byte limit");
335
+ }
336
+ const verificationDeadline = performance.now() + boundedInteger(options.maxAssetVerificationMs, DEFAULT_ASSET_VERIFICATION_MS, MAX_ASSET_VERIFICATION_MS, "asset verification time limit");
337
+ let verifiedAssetBytes = 0;
338
+ const assets = [];
339
+ for (const asset of configuredAssets) {
340
+ if (!options.verifyAssets) {
341
+ assets.push(Object.freeze({
342
+ path: asset.path,
343
+ expectedBytes: asset.bytes,
344
+ expectedSha256: asset.sha256,
345
+ integrity: "unavailable"
346
+ }));
347
+ continue;
348
+ }
349
+ assertAssetVerificationDeadline(verificationDeadline);
350
+ const remainingBytes = maximumTotalAssetBytes - verifiedAssetBytes;
351
+ if (remainingBytes < 0)
352
+ throw new Error("capture asset verification exceeds its total byte limit");
353
+ let verified;
354
+ try {
355
+ verified = await hashRegularFile(root, asset.path, Math.min(maximumAssetBytes, remainingBytes), verificationDeadline, `capture asset ${asset.path}`);
356
+ } catch (error) {
357
+ assertAssetVerificationDeadline(verificationDeadline);
358
+ if (!(error instanceof MissingCaptureAssetError))
359
+ throw error;
360
+ assets.push(Object.freeze({
361
+ path: asset.path,
362
+ expectedBytes: asset.bytes,
363
+ expectedSha256: asset.sha256,
364
+ integrity: "mismatch"
365
+ }));
366
+ continue;
367
+ }
368
+ verifiedAssetBytes += verified.bytes;
369
+ const integrity = asset.bytes !== null && asset.sha256 !== null && asset.bytes === verified.bytes && asset.sha256 === verified.sha256 ? "verified" : "mismatch";
370
+ assets.push(Object.freeze({
371
+ path: asset.path,
372
+ expectedBytes: asset.bytes,
373
+ expectedSha256: asset.sha256,
374
+ integrity
375
+ }));
376
+ assertAssetVerificationDeadline(verificationDeadline);
377
+ }
378
+ let sourceHtml;
379
+ if (options.includeSourceHtml) {
380
+ const evidencePath = sourceHtmlPath(manifest);
381
+ if (evidencePath !== null) {
382
+ sourceHtml = (await readRegularFile(root, evidencePath, boundedInteger(options.maxSourceHtmlBytes, DEFAULT_MAX_SOURCE_BYTES, MAX_SOURCE_BYTES, "source HTML byte limit"), "capture source HTML")).toString("utf8");
383
+ }
384
+ }
385
+ await assertBundleRoot(root);
386
+ return Object.freeze({
387
+ root: root.path,
388
+ schemaVersion,
389
+ sourceUrl: manifest.sourceUrl,
390
+ canonicalUrl: optionalString(manifest.canonicalUrl),
391
+ status: optionalString(manifest.status),
392
+ capturedAt: optionalString(manifest.capturedAt),
393
+ document: Object.freeze({
394
+ path: documentPath,
395
+ bytes: documentBytes.byteLength,
396
+ sha256: documentSha256,
397
+ expectedBytes: expectedDocument?.bytes ?? null,
398
+ expectedSha256: expectedDocument?.sha256 ?? null,
399
+ integrity: documentIntegrity,
400
+ markdown: documentBytes.toString("utf8")
401
+ }),
402
+ assets: Object.freeze(assets),
403
+ ...sourceHtml === undefined ? {} : { sourceHtml }
404
+ });
405
+ } finally {
406
+ await root.handle.close();
407
+ }
408
+ }
409
+ async function verifyCaptureBundle(path, options = {}) {
410
+ const inspection = await readCaptureBundle(path, options);
411
+ const issues = [];
412
+ if (inspection.document.integrity !== "verified") {
413
+ issues.push(Object.freeze({
414
+ kind: "document-integrity",
415
+ path: inspection.document.path,
416
+ message: inspection.document.integrity === "mismatch" ? "Stored Markdown bytes do not match the v4 capture manifest." : "Legacy capture manifest does not contain an authoritative Markdown digest."
417
+ }));
418
+ }
419
+ for (const asset of inspection.assets) {
420
+ if (asset.integrity === "mismatch") {
421
+ issues.push(Object.freeze({
422
+ kind: "asset-integrity",
423
+ path: asset.path,
424
+ message: "Stored asset bytes do not match the capture manifest."
425
+ }));
426
+ }
427
+ }
428
+ return Object.freeze({ ok: issues.length === 0, inspection, issues: Object.freeze(issues) });
429
+ }
430
+
431
+ export { readCaptureBundle, verifyCaptureBundle };
@@ -0,0 +1,140 @@
1
+ // @bun
2
+ // src/source-inbox.ts
3
+ var MAX_SOURCE_INBOX_NOTES = 1e4;
4
+ var MAX_SOURCE_INBOX_RESULTS = 1000;
5
+ var MAX_SOURCE_INBOX_PREFIXES = 16;
6
+ var MAX_SOURCE_INBOX_CONNECTIONS = 250000;
7
+ var MAX_SOURCE_DISPOSITION_EVIDENCE = 20;
8
+ var windowsAbsolutePattern = /^[a-z]:[\\/]/iu;
9
+ function normalizedPrefix(value) {
10
+ const normalized = value.replaceAll("\\", "/").normalize("NFC").replace(/^\.\//u, "").replace(/\/+$/u, "");
11
+ if (normalized === "" || normalized.startsWith("/") || windowsAbsolutePattern.test(normalized) || normalized.split("/").some((part) => part === ".." || part === "")) {
12
+ throw new TypeError("Source-inbox prefixes must be confined vault-relative directories.");
13
+ }
14
+ return normalized.endsWith("/") ? normalized : `${normalized}/`;
15
+ }
16
+ function validDate(value) {
17
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/u.test(value))
18
+ return null;
19
+ const date = new Date(`${value}T00:00:00.000Z`);
20
+ return Number.isNaN(date.valueOf()) || date.toISOString().slice(0, 10) !== value ? null : value;
21
+ }
22
+ function clippedReason(note) {
23
+ const raw = note.metadata.clipped;
24
+ const clipped = validDate(raw);
25
+ if (clipped !== null)
26
+ return { clipped, reason: "no-maintained-disposition" };
27
+ return {
28
+ clipped: null,
29
+ reason: raw === undefined ? "missing-clipped-date" : "invalid-clipped-date"
30
+ };
31
+ }
32
+ function defaultMaintained(note) {
33
+ const type = note.metadata.type;
34
+ return note.path.startsWith("notes/") || type === "note" || type === "concept";
35
+ }
36
+ function compareInbox(left, right) {
37
+ if (left.clipped !== null && right.clipped === null)
38
+ return -1;
39
+ if (left.clipped === null && right.clipped !== null)
40
+ return 1;
41
+ if (left.clipped !== null && right.clipped !== null && left.clipped !== right.clipped) {
42
+ return right.clipped.localeCompare(left.clipped);
43
+ }
44
+ return left.path.localeCompare(right.path);
45
+ }
46
+ function sourceInbox(notes, analysis, options = {}) {
47
+ const maxNotes = options.maxNotes ?? MAX_SOURCE_INBOX_NOTES;
48
+ const maxConnections = options.maxConnections ?? MAX_SOURCE_INBOX_CONNECTIONS;
49
+ const limit = options.limit ?? 100;
50
+ if (!Number.isSafeInteger(maxNotes) || maxNotes < 1 || maxNotes > MAX_SOURCE_INBOX_NOTES) {
51
+ throw new RangeError(`Source inbox accepts from 1 through ${MAX_SOURCE_INBOX_NOTES} notes.`);
52
+ }
53
+ if (notes.length > maxNotes) {
54
+ throw new RangeError(`Source inbox received ${notes.length} notes, above its ${maxNotes}-note limit.`);
55
+ }
56
+ if (!Number.isSafeInteger(maxConnections) || maxConnections < 0 || maxConnections > MAX_SOURCE_INBOX_CONNECTIONS) {
57
+ throw new RangeError(`Source inbox connection limit must be from 0 through ${MAX_SOURCE_INBOX_CONNECTIONS}.`);
58
+ }
59
+ const observedConnections = analysis.contextualLinks.length + analysis.authoredRelations.length;
60
+ if (observedConnections > maxConnections) {
61
+ throw new RangeError(`Source inbox received ${observedConnections} connections, above its ${maxConnections}-connection limit.`);
62
+ }
63
+ if (!Number.isSafeInteger(limit) || limit < 0 || limit > MAX_SOURCE_INBOX_RESULTS) {
64
+ throw new RangeError(`Source inbox limit must be from 0 through ${MAX_SOURCE_INBOX_RESULTS}.`);
65
+ }
66
+ const prefixInputs = options.sourcePrefixes ?? ["articles/"];
67
+ if (prefixInputs.length < 1 || prefixInputs.length > MAX_SOURCE_INBOX_PREFIXES) {
68
+ throw new RangeError(`Source inbox accepts from 1 through ${MAX_SOURCE_INBOX_PREFIXES} source prefixes.`);
69
+ }
70
+ const sourcePrefixes = Object.freeze([...new Set(prefixInputs.map(normalizedPrefix))].toSorted());
71
+ const catalogs = new Set(options.catalogNoteIds ?? ["index"]);
72
+ const noteByReference = new Map(notes.flatMap((note) => [
73
+ [note.id, note],
74
+ [note.path, note]
75
+ ]));
76
+ const isSource = (note) => sourcePrefixes.some((prefix) => note.path.startsWith(prefix));
77
+ const maintained = options.isMaintained ?? defaultMaintained;
78
+ const sources = notes.filter(isSource).toSorted((left, right) => left.path.localeCompare(right.path));
79
+ const evidence = new Map;
80
+ const acceptSource = (sourceId, targetId) => {
81
+ const source = noteByReference.get(sourceId);
82
+ const target = noteByReference.get(targetId);
83
+ if (source === undefined || target === undefined || catalogs.has(source.id) || isSource(source) || !isSource(target) || !maintained(source))
84
+ return null;
85
+ return { source, target };
86
+ };
87
+ for (const link of analysis.contextualLinks) {
88
+ const accepted = acceptSource(link.source, link.target);
89
+ if (accepted === null)
90
+ continue;
91
+ const entries = evidence.get(accepted.target.id) ?? [];
92
+ entries.push(Object.freeze({ kind: "link", source: accepted.source.id, line: link.line }));
93
+ evidence.set(accepted.target.id, entries);
94
+ }
95
+ for (const relation of analysis.authoredRelations) {
96
+ const accepted = acceptSource(relation.source, relation.target);
97
+ if (accepted === null)
98
+ continue;
99
+ const entries = evidence.get(accepted.target.id) ?? [];
100
+ entries.push(Object.freeze({
101
+ kind: "relation",
102
+ source: accepted.source.id,
103
+ line: relation.provenance.line,
104
+ predicate: relation.predicate
105
+ }));
106
+ evidence.set(accepted.target.id, entries);
107
+ }
108
+ const dispositions = sources.flatMap((source) => {
109
+ const entries = evidence.get(source.id);
110
+ if (entries === undefined || entries.length === 0)
111
+ return [];
112
+ const sorted = entries.toSorted((left, right) => left.source.localeCompare(right.source) || left.line - right.line || left.kind.localeCompare(right.kind));
113
+ return [Object.freeze({
114
+ id: source.id,
115
+ path: source.path,
116
+ evidence: Object.freeze(sorted.slice(0, MAX_SOURCE_DISPOSITION_EVIDENCE)),
117
+ ...sorted.length > MAX_SOURCE_DISPOSITION_EVIDENCE ? { evidenceTruncated: true } : {}
118
+ })];
119
+ });
120
+ const pending = sources.filter(({ id }) => !evidence.has(id)).map((source) => Object.freeze({
121
+ id: source.id,
122
+ path: source.path,
123
+ title: source.title,
124
+ ...clippedReason(source)
125
+ })).toSorted(compareInbox);
126
+ const items = pending.slice(0, limit);
127
+ return Object.freeze({
128
+ advisory: true,
129
+ sourcePrefixes,
130
+ totalSources: sources.length,
131
+ disposedSources: dispositions.length,
132
+ pendingSources: pending.length,
133
+ returnedSources: items.length,
134
+ truncated: items.length < pending.length,
135
+ items: Object.freeze(items),
136
+ dispositions: Object.freeze(dispositions)
137
+ });
138
+ }
139
+
140
+ export { MAX_SOURCE_INBOX_NOTES, MAX_SOURCE_INBOX_RESULTS, MAX_SOURCE_INBOX_PREFIXES, MAX_SOURCE_INBOX_CONNECTIONS, MAX_SOURCE_DISPOSITION_EVIDENCE, sourceInbox };