@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,784 @@
1
+ // @bun
2
+ import {
3
+ acquireFileLease
4
+ } from "../index-3rm7cz6h.js";
5
+ import {
6
+ redactSensitiveText
7
+ } from "../index-mxxxytys.js";
8
+ import {
9
+ sanitizeTerminalLine,
10
+ sanitizeTerminalText
11
+ } from "../index-1xxnjn0d.js";
12
+
13
+ // src/clip/jobs.ts
14
+ import { Buffer } from "buffer";
15
+ import { randomUUID } from "crypto";
16
+ import { constants } from "fs";
17
+ import {
18
+ link,
19
+ lstat,
20
+ open,
21
+ opendir,
22
+ realpath,
23
+ rename,
24
+ unlink
25
+ } from "fs/promises";
26
+ import { homedir } from "os";
27
+ import { dirname, join, relative, resolve, sep } from "path";
28
+ var CAPTURE_JOB_SCHEMA_VERSION = 1;
29
+ var MAX_CAPTURE_JOB_BYTES = 512 * 1024;
30
+ var MAX_CAPTURE_JOB_ATTEMPTS = 128;
31
+ var MAX_CAPTURE_JOB_WARNINGS = 128;
32
+ var MAX_CAPTURE_JOB_RECORDS = 1e4;
33
+ var MAX_CAPTURE_JOB_LIST_LIMIT = 1000;
34
+ var MAX_REVISION = Number.MAX_SAFE_INTEGER;
35
+ var MAX_TARGET_BYTES = 16 * 1024;
36
+ var MAX_METHOD_BYTES = 256;
37
+ var MAX_MESSAGE_BYTES = 8 * 1024;
38
+ var MAX_WARNING_BYTES = 8 * 1024;
39
+ var MAX_ERROR_BYTES = 16 * 1024;
40
+ var MAX_BUNDLE_PATH_BYTES = 16 * 1024;
41
+ var UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
42
+ var SHA256 = /^[0-9a-f]{64}$/;
43
+ var captureJobPhases = [
44
+ "queued",
45
+ "acquiring",
46
+ "extracting",
47
+ "persisting",
48
+ "finalizing",
49
+ "finished"
50
+ ];
51
+ var captureJobStoreBrand = Symbol("captureJobStore");
52
+
53
+ class CaptureJobSafetyError extends Error {
54
+ constructor(message) {
55
+ super(message);
56
+ this.name = "CaptureJobSafetyError";
57
+ }
58
+ }
59
+
60
+ class CaptureJobNotFoundError extends Error {
61
+ id;
62
+ constructor(id) {
63
+ super(`Capture job ${id} does not exist.`);
64
+ this.name = "CaptureJobNotFoundError";
65
+ this.id = id;
66
+ }
67
+ }
68
+
69
+ class CaptureJobConflictError extends Error {
70
+ id;
71
+ expectedRevision;
72
+ actualRevision;
73
+ constructor(id, expectedRevision, actualRevision) {
74
+ super(`Capture job ${id} revision changed from ${expectedRevision} to ${actualRevision}.`);
75
+ this.name = "CaptureJobConflictError";
76
+ this.id = id;
77
+ this.expectedRevision = expectedRevision;
78
+ this.actualRevision = actualRevision;
79
+ }
80
+ }
81
+
82
+ class CaptureJobChangedDuringReadError extends Error {
83
+ }
84
+ function isErrno(error, code) {
85
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
86
+ }
87
+ function isRecord(value) {
88
+ return typeof value === "object" && value !== null && !Array.isArray(value);
89
+ }
90
+ function hasExactKeys(value, expected) {
91
+ const actual = Object.keys(value).sort();
92
+ const sortedExpected = [...expected].sort();
93
+ return actual.length === sortedExpected.length && actual.every((key, index) => key === sortedExpected[index]);
94
+ }
95
+ function utf8Bytes(value) {
96
+ return Buffer.byteLength(value, "utf8");
97
+ }
98
+ function normalizedText(value, label, maxBytes, line) {
99
+ if (typeof value !== "string")
100
+ throw new Error(`${label} must be a string.`);
101
+ if (utf8Bytes(value) > maxBytes) {
102
+ throw new Error(`${label} exceeds the ${maxBytes}-byte input limit.`);
103
+ }
104
+ const sanitized = (line ? sanitizeTerminalLine : sanitizeTerminalText)(redactSensitiveText(value)).trim();
105
+ if (sanitized === "")
106
+ throw new Error(`${label} must not be empty.`);
107
+ if (utf8Bytes(sanitized) > maxBytes) {
108
+ throw new Error(`${label} exceeds the ${maxBytes}-byte limit.`);
109
+ }
110
+ return sanitized;
111
+ }
112
+ function storedText(value, label, maxBytes, line) {
113
+ const normalized = normalizedText(value, label, maxBytes, line);
114
+ if (normalized !== value) {
115
+ throw new CaptureJobSafetyError(`${label} is not in sanitized canonical form.`);
116
+ }
117
+ return normalized;
118
+ }
119
+ function captureJobId(value) {
120
+ if (typeof value !== "string" || !UUID_V4.test(value)) {
121
+ throw new Error("Capture job id must be a canonical lowercase UUID v4.");
122
+ }
123
+ return value;
124
+ }
125
+ function revision(value, label = "revision") {
126
+ if (!Number.isSafeInteger(value) || value < 1 || value > MAX_REVISION) {
127
+ throw new Error(`${label} must be a positive safe integer.`);
128
+ }
129
+ return value;
130
+ }
131
+ function timestamp(value, label) {
132
+ if (typeof value !== "string" || value.length !== 24) {
133
+ throw new Error(`${label} must be a canonical ISO timestamp.`);
134
+ }
135
+ const parsed = new Date(value);
136
+ if (!Number.isFinite(parsed.valueOf()) || parsed.toISOString() !== value) {
137
+ throw new Error(`${label} must be a canonical ISO timestamp.`);
138
+ }
139
+ return value;
140
+ }
141
+ function inputTimestamp(value) {
142
+ const selected = value ?? new Date;
143
+ if (!(selected instanceof Date) || !Number.isFinite(selected.valueOf())) {
144
+ throw new Error("Capture job timestamp must be a valid Date.");
145
+ }
146
+ return selected.toISOString();
147
+ }
148
+ function phase(value) {
149
+ if (typeof value !== "string" || !captureJobPhases.includes(value)) {
150
+ throw new Error("Capture job phase is invalid.");
151
+ }
152
+ return value;
153
+ }
154
+ function lifecycle(value) {
155
+ if (value !== "running" && value !== "completed" && value !== "failed") {
156
+ throw new Error("Capture job lifecycle is invalid.");
157
+ }
158
+ return value;
159
+ }
160
+ function captureStatus(value) {
161
+ if (value !== "complete" && value !== "partial" && value !== "auth-required" && value !== "blocked" && value !== "unsupported") {
162
+ throw new Error("Capture job status is invalid.");
163
+ }
164
+ return value;
165
+ }
166
+ function attemptOutcome(value) {
167
+ if (value !== "succeeded" && value !== "failed" && value !== "skipped") {
168
+ throw new Error("Capture job attempt outcome is invalid.");
169
+ }
170
+ return value;
171
+ }
172
+ function normalizeAttempt(value, label, stored) {
173
+ if (!isRecord(value) || !hasExactKeys(value, ["method", "outcome", "message"])) {
174
+ throw new Error(`${label} must contain only method, outcome, and message.`);
175
+ }
176
+ const text = stored ? storedText : normalizedText;
177
+ return {
178
+ method: text(value.method, `${label}.method`, MAX_METHOD_BYTES, true),
179
+ outcome: attemptOutcome(value.outcome),
180
+ message: text(value.message, `${label}.message`, MAX_MESSAGE_BYTES, false)
181
+ };
182
+ }
183
+ function normalizeAttempts(value, stored) {
184
+ if (!Array.isArray(value))
185
+ throw new Error("Capture job attempts must be an array.");
186
+ if (value.length > MAX_CAPTURE_JOB_ATTEMPTS) {
187
+ throw new Error(`Capture job attempts exceed the ${MAX_CAPTURE_JOB_ATTEMPTS}-item limit.`);
188
+ }
189
+ return value.map((item, index) => normalizeAttempt(item, `attempts[${index}]`, stored));
190
+ }
191
+ function normalizeWarnings(value, stored) {
192
+ if (!Array.isArray(value))
193
+ throw new Error("Capture job warnings must be an array.");
194
+ if (value.length > MAX_CAPTURE_JOB_WARNINGS) {
195
+ throw new Error(`Capture job warnings exceed the ${MAX_CAPTURE_JOB_WARNINGS}-item limit.`);
196
+ }
197
+ const text = stored ? storedText : normalizedText;
198
+ return value.map((item, index) => text(item, `warnings[${index}]`, MAX_WARNING_BYTES, false));
199
+ }
200
+ function normalizeBundle(value, stored) {
201
+ if (!isRecord(value) || !hasExactKeys(value, ["path", "sha256"])) {
202
+ throw new Error("Capture job bundle must contain only path and sha256.");
203
+ }
204
+ const text = stored ? storedText : normalizedText;
205
+ const digest = typeof value.sha256 === "string" ? value.sha256 : "";
206
+ if (!SHA256.test(digest))
207
+ throw new Error("Capture job bundle sha256 must be a lowercase SHA-256 digest.");
208
+ return {
209
+ path: text(value.path, "bundle.path", MAX_BUNDLE_PATH_BYTES, true),
210
+ sha256: digest
211
+ };
212
+ }
213
+ function parseCaptureJob(value) {
214
+ const keys = [
215
+ "schemaVersion",
216
+ "id",
217
+ "revision",
218
+ "lifecycle",
219
+ "captureStatus",
220
+ "startedAt",
221
+ "updatedAt",
222
+ "finishedAt",
223
+ "phase",
224
+ "target",
225
+ "attempts",
226
+ "warnings",
227
+ "bundle",
228
+ "error"
229
+ ];
230
+ if (!isRecord(value) || !hasExactKeys(value, keys)) {
231
+ throw new Error("Capture job record has an invalid schema.");
232
+ }
233
+ if (value.schemaVersion !== CAPTURE_JOB_SCHEMA_VERSION) {
234
+ throw new Error(`Unsupported capture job schema version ${String(value.schemaVersion)}.`);
235
+ }
236
+ const parsedLifecycle = lifecycle(value.lifecycle);
237
+ const parsedPhase = phase(value.phase);
238
+ const startedAt = timestamp(value.startedAt, "startedAt");
239
+ const updatedAt = timestamp(value.updatedAt, "updatedAt");
240
+ const finishedAt = value.finishedAt === null ? null : timestamp(value.finishedAt, "finishedAt");
241
+ const parsedStatus = value.captureStatus === null ? null : captureStatus(value.captureStatus);
242
+ const bundle = value.bundle === null ? null : normalizeBundle(value.bundle, true);
243
+ const error = value.error === null ? null : storedText(value.error, "error", MAX_ERROR_BYTES, false);
244
+ if (updatedAt < startedAt)
245
+ throw new Error("updatedAt must not precede startedAt.");
246
+ if (finishedAt !== null && (finishedAt !== updatedAt || finishedAt < startedAt)) {
247
+ throw new Error("finishedAt must equal the terminal updatedAt timestamp.");
248
+ }
249
+ if (parsedLifecycle === "running") {
250
+ if (parsedPhase === "finished" || parsedStatus !== null || finishedAt !== null || bundle !== null || error !== null) {
251
+ throw new Error("Running capture job state is inconsistent.");
252
+ }
253
+ } else if (parsedLifecycle === "completed") {
254
+ if (parsedPhase !== "finished" || parsedStatus === null || finishedAt === null || error !== null) {
255
+ throw new Error("Completed capture job state is inconsistent.");
256
+ }
257
+ } else if (parsedPhase !== "finished" || parsedStatus !== null || finishedAt === null || bundle !== null || error === null) {
258
+ throw new Error("Failed capture job state is inconsistent.");
259
+ }
260
+ return {
261
+ schemaVersion: CAPTURE_JOB_SCHEMA_VERSION,
262
+ id: captureJobId(value.id),
263
+ revision: revision(value.revision),
264
+ lifecycle: parsedLifecycle,
265
+ captureStatus: parsedStatus,
266
+ startedAt,
267
+ updatedAt,
268
+ finishedAt,
269
+ phase: parsedPhase,
270
+ target: storedText(value.target, "target", MAX_TARGET_BYTES, true),
271
+ attempts: normalizeAttempts(value.attempts, true),
272
+ warnings: normalizeWarnings(value.warnings, true),
273
+ bundle,
274
+ error
275
+ };
276
+ }
277
+ function renderCaptureJob(record) {
278
+ return `${JSON.stringify(record, null, 2)}
279
+ `;
280
+ }
281
+ function sameIdentity(left, right) {
282
+ return left.dev === right.device && left.ino === right.inode;
283
+ }
284
+ function within(root, candidate) {
285
+ const fromRoot = relative(root, candidate);
286
+ return fromRoot !== "" && fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !fromRoot.includes(sep);
287
+ }
288
+ function pathFor(store, id) {
289
+ const safeId = captureJobId(id);
290
+ const candidate = join(store.root, `${safeId}.json`);
291
+ if (!within(store.root, candidate))
292
+ throw new CaptureJobSafetyError("Capture job path escapes its store.");
293
+ return candidate;
294
+ }
295
+ async function directoryIdentity(path) {
296
+ const before = await lstat(path, { bigint: true });
297
+ if (!before.isDirectory() || before.isSymbolicLink()) {
298
+ throw new CaptureJobSafetyError("Capture job store must be a real directory, not a link.");
299
+ }
300
+ if ((Number(before.mode) & 63) !== 0) {
301
+ throw new CaptureJobSafetyError("Capture job store must not grant group or world access.");
302
+ }
303
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
304
+ try {
305
+ const opened = await handle.stat({ bigint: true });
306
+ if (!opened.isDirectory() || (Number(opened.mode) & 63) !== 0 || !sameIdentity(opened, { device: before.dev, inode: before.ino })) {
307
+ throw new CaptureJobSafetyError("Capture job store changed while it was opened.");
308
+ }
309
+ return { device: opened.dev, inode: opened.ino };
310
+ } finally {
311
+ await handle.close();
312
+ }
313
+ }
314
+ async function openCaptureJobStore(root) {
315
+ if (typeof root !== "string" || root.trim() === "") {
316
+ throw new Error("Capture job store path must not be empty.");
317
+ }
318
+ const selected = resolve(root);
319
+ if (selected === dirname(selected) || selected === resolve(homedir())) {
320
+ throw new CaptureJobSafetyError("Capture job store must be a dedicated subdirectory.");
321
+ }
322
+ let canonical;
323
+ try {
324
+ canonical = await realpath(selected);
325
+ } catch (error) {
326
+ if (isErrno(error, "ENOENT")) {
327
+ throw new CaptureJobSafetyError("Capture job store must already exist.");
328
+ }
329
+ throw error;
330
+ }
331
+ if (canonical !== selected) {
332
+ throw new CaptureJobSafetyError("Capture job store path must not contain filesystem aliases or links.");
333
+ }
334
+ const identity = await directoryIdentity(canonical);
335
+ return {
336
+ root: canonical,
337
+ [captureJobStoreBrand]: identity
338
+ };
339
+ }
340
+ async function assertStore(store) {
341
+ if (!isRecord(store) || typeof store.root !== "string" || !(captureJobStoreBrand in store)) {
342
+ throw new CaptureJobSafetyError("Capture job store was not opened by openCaptureJobStore().");
343
+ }
344
+ const expected = store[captureJobStoreBrand];
345
+ const canonical = await realpath(store.root).catch(() => "");
346
+ if (canonical !== store.root)
347
+ throw new CaptureJobSafetyError("Capture job store path changed or became aliased.");
348
+ const actual = await directoryIdentity(store.root);
349
+ if (actual.device !== expected.device || actual.inode !== expected.inode) {
350
+ throw new CaptureJobSafetyError("Capture job store was replaced after it was opened.");
351
+ }
352
+ }
353
+ async function fsyncStore(store) {
354
+ await assertStore(store);
355
+ const handle = await open(store.root, constants.O_RDONLY | constants.O_NOFOLLOW);
356
+ try {
357
+ const stat = await handle.stat({ bigint: true });
358
+ const expected = store[captureJobStoreBrand];
359
+ if (!stat.isDirectory() || !sameIdentity(stat, expected)) {
360
+ throw new CaptureJobSafetyError("Capture job store changed before synchronization.");
361
+ }
362
+ await handle.sync();
363
+ } finally {
364
+ await handle.close();
365
+ }
366
+ }
367
+ function assertPrivateRegularFile(stat, label) {
368
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n) {
369
+ throw new CaptureJobSafetyError(`${label} must be a single-link regular file.`);
370
+ }
371
+ if ((Number(stat.mode) & 63) !== 0) {
372
+ throw new CaptureJobSafetyError(`${label} must not grant group or world access.`);
373
+ }
374
+ if (stat.size < 1n || stat.size > BigInt(MAX_CAPTURE_JOB_BYTES)) {
375
+ throw new CaptureJobSafetyError(`${label} exceeds the bounded record size.`);
376
+ }
377
+ }
378
+ async function readSnapshotOnce(store, id) {
379
+ await assertStore(store);
380
+ const path = pathFor(store, id);
381
+ let before;
382
+ try {
383
+ before = await lstat(path, { bigint: true });
384
+ } catch (error) {
385
+ if (isErrno(error, "ENOENT"))
386
+ throw new CaptureJobNotFoundError(id);
387
+ throw error;
388
+ }
389
+ assertPrivateRegularFile(before, "Capture job record");
390
+ const identity = { device: before.dev, inode: before.ino };
391
+ let handle;
392
+ try {
393
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
394
+ } catch (error) {
395
+ if (isErrno(error, "ENOENT"))
396
+ throw new CaptureJobChangedDuringReadError;
397
+ throw new CaptureJobSafetyError(`Capture job record could not be opened safely: ${String(error)}`);
398
+ }
399
+ let bytes;
400
+ try {
401
+ const opened = await handle.stat({ bigint: true });
402
+ assertPrivateRegularFile(opened, "Opened capture job record");
403
+ if (!sameIdentity(opened, identity))
404
+ throw new CaptureJobChangedDuringReadError;
405
+ bytes = await handle.readFile();
406
+ const complete = await handle.stat({ bigint: true });
407
+ if (!sameIdentity(complete, identity) || complete.size !== BigInt(bytes.byteLength)) {
408
+ throw new CaptureJobChangedDuringReadError;
409
+ }
410
+ } finally {
411
+ await handle.close();
412
+ }
413
+ if (bytes.byteLength < 1 || bytes.byteLength > MAX_CAPTURE_JOB_BYTES) {
414
+ throw new CaptureJobSafetyError("Capture job record exceeds the bounded record size.");
415
+ }
416
+ const after = await lstat(path, { bigint: true }).catch((error) => {
417
+ if (isErrno(error, "ENOENT"))
418
+ return null;
419
+ throw error;
420
+ });
421
+ if (after === null || !sameIdentity(after, identity))
422
+ throw new CaptureJobChangedDuringReadError;
423
+ assertPrivateRegularFile(after, "Capture job record");
424
+ await assertStore(store);
425
+ let text;
426
+ try {
427
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
428
+ } catch {
429
+ throw new CaptureJobSafetyError("Capture job record is not valid UTF-8.");
430
+ }
431
+ let raw;
432
+ try {
433
+ raw = JSON.parse(text);
434
+ } catch {
435
+ throw new CaptureJobSafetyError("Capture job record is not valid JSON.");
436
+ }
437
+ const record = parseCaptureJob(raw);
438
+ if (record.id !== id)
439
+ throw new CaptureJobSafetyError("Capture job id does not match its filename.");
440
+ if (renderCaptureJob(record) !== text) {
441
+ throw new CaptureJobSafetyError("Capture job record is not canonical JSON.");
442
+ }
443
+ return { record, identity };
444
+ }
445
+ async function readSnapshot(store, id) {
446
+ captureJobId(id);
447
+ for (let attempt = 0;attempt < 3; attempt += 1) {
448
+ try {
449
+ return await readSnapshotOnce(store, id);
450
+ } catch (error) {
451
+ if (!(error instanceof CaptureJobChangedDuringReadError) || attempt === 2)
452
+ throw error;
453
+ }
454
+ }
455
+ throw new CaptureJobChangedDuringReadError;
456
+ }
457
+ async function writeTemporary(store, record) {
458
+ const text = renderCaptureJob(record);
459
+ if (utf8Bytes(text) > MAX_CAPTURE_JOB_BYTES) {
460
+ throw new Error(`Capture job record exceeds the ${MAX_CAPTURE_JOB_BYTES}-byte limit.`);
461
+ }
462
+ await assertStore(store);
463
+ const temporaryPath = join(store.root, `.${record.id}.${randomUUID()}.tmp`);
464
+ if (!within(store.root, temporaryPath))
465
+ throw new CaptureJobSafetyError("Temporary capture job path escapes its store.");
466
+ const handle = await open(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
467
+ let closed = false;
468
+ let identity = null;
469
+ try {
470
+ const created = await handle.stat({ bigint: true });
471
+ if (!created.isFile() || created.nlink !== 1n || (Number(created.mode) & 63) !== 0) {
472
+ throw new CaptureJobSafetyError("Temporary capture job record is not a private regular file.");
473
+ }
474
+ identity = { device: created.dev, inode: created.ino };
475
+ await handle.writeFile(text, "utf8");
476
+ await handle.sync();
477
+ const complete = await handle.stat({ bigint: true });
478
+ if (!complete.isFile() || complete.nlink !== 1n || !sameIdentity(complete, identity)) {
479
+ throw new CaptureJobSafetyError("Temporary capture job record changed before installation.");
480
+ }
481
+ await handle.close();
482
+ closed = true;
483
+ await assertStore(store);
484
+ const named = await lstat(temporaryPath, { bigint: true });
485
+ if (!sameIdentity(named, identity) || named.nlink !== 1n || !named.isFile()) {
486
+ throw new CaptureJobSafetyError("Temporary capture job name changed before installation.");
487
+ }
488
+ return { path: temporaryPath, identity };
489
+ } catch (error) {
490
+ if (!closed)
491
+ await handle.close().catch(() => {
492
+ return;
493
+ });
494
+ if (identity !== null) {
495
+ await unlinkOwnedTemporary(store, temporaryPath, identity).catch(() => {
496
+ return;
497
+ });
498
+ }
499
+ throw error;
500
+ }
501
+ }
502
+ async function unlinkOwnedTemporary(store, path, identity) {
503
+ await assertStore(store);
504
+ const current = await lstat(path, { bigint: true }).catch((error) => {
505
+ if (isErrno(error, "ENOENT"))
506
+ return null;
507
+ throw error;
508
+ });
509
+ if (current !== null && current.isFile() && current.nlink === 1n && sameIdentity(current, identity)) {
510
+ await unlink(path);
511
+ }
512
+ }
513
+ async function assertCurrentSnapshot(store, id, expected) {
514
+ const actual = await readSnapshot(store, id);
515
+ if (actual.record.revision !== expected.record.revision || actual.identity.device !== expected.identity.device || actual.identity.inode !== expected.identity.inode) {
516
+ throw new CaptureJobConflictError(id, expected.record.revision, actual.record.revision);
517
+ }
518
+ }
519
+ async function installCreatedRecord(store, record, lease) {
520
+ const temporary = await writeTemporary(store, record);
521
+ const path = pathFor(store, record.id);
522
+ let temporaryExists = true;
523
+ try {
524
+ await assertStore(store);
525
+ await lease.assertOwned();
526
+ try {
527
+ await link(temporary.path, path);
528
+ } catch (error) {
529
+ if (isErrno(error, "EEXIST"))
530
+ throw new CaptureJobConflictError(record.id, 0, (await readSnapshot(store, record.id)).record.revision);
531
+ throw error;
532
+ }
533
+ await unlink(temporary.path);
534
+ temporaryExists = false;
535
+ const installed = await readSnapshot(store, record.id);
536
+ if (installed.record.revision !== record.revision || installed.record.id !== record.id) {
537
+ throw new CaptureJobSafetyError("Installed capture job record does not match the requested record.");
538
+ }
539
+ if (renderCaptureJob(installed.record) !== renderCaptureJob(record)) {
540
+ throw new CaptureJobSafetyError("Installed capture job record differs from the requested record.");
541
+ }
542
+ await fsyncStore(store);
543
+ } finally {
544
+ if (temporaryExists)
545
+ await unlinkOwnedTemporary(store, temporary.path, temporary.identity).catch(() => {
546
+ return;
547
+ });
548
+ }
549
+ }
550
+ async function installUpdatedRecord(store, previous, record, lease) {
551
+ const temporary = await writeTemporary(store, record);
552
+ const path = pathFor(store, record.id);
553
+ let temporaryExists = true;
554
+ try {
555
+ await assertCurrentSnapshot(store, record.id, previous);
556
+ await assertStore(store);
557
+ await lease.assertOwned();
558
+ await rename(temporary.path, path);
559
+ temporaryExists = false;
560
+ const installed = await readSnapshot(store, record.id);
561
+ if (installed.record.revision !== record.revision || renderCaptureJob(installed.record) !== renderCaptureJob(record)) {
562
+ throw new CaptureJobSafetyError("Capture job update was not installed atomically.");
563
+ }
564
+ await fsyncStore(store);
565
+ } finally {
566
+ if (temporaryExists)
567
+ await unlinkOwnedTemporary(store, temporary.path, temporary.identity).catch(() => {
568
+ return;
569
+ });
570
+ }
571
+ }
572
+ var mutationTails = new Map;
573
+ async function withMutation(store, id, action) {
574
+ const key = `${store.root}\x00${id}`;
575
+ const predecessor = mutationTails.get(key) ?? Promise.resolve();
576
+ let release = () => {
577
+ return;
578
+ };
579
+ const gate = new Promise((resolveGate) => {
580
+ release = resolveGate;
581
+ });
582
+ const tail = predecessor.catch(() => {
583
+ return;
584
+ }).then(() => gate);
585
+ mutationTails.set(key, tail);
586
+ await predecessor.catch(() => {
587
+ return;
588
+ });
589
+ let lease;
590
+ try {
591
+ await assertStore(store);
592
+ lease = await acquireFileLease(join(store.root, `.${captureJobId(id)}.lock`));
593
+ await assertStore(store);
594
+ await lease.assertOwned();
595
+ const result = await action(lease);
596
+ return result;
597
+ } finally {
598
+ await lease?.release().catch(() => {
599
+ return;
600
+ });
601
+ release();
602
+ if (mutationTails.get(key) === tail)
603
+ mutationTails.delete(key);
604
+ }
605
+ }
606
+ function ensureRunning(record) {
607
+ if (record.lifecycle !== "running") {
608
+ throw new Error(`Capture job ${record.id} is terminal and cannot transition again.`);
609
+ }
610
+ if (record.revision >= MAX_REVISION)
611
+ throw new Error("Capture job revision limit reached.");
612
+ }
613
+ function ensureExpectedRevision(record, expected) {
614
+ const normalized = revision(expected, "expectedRevision");
615
+ if (record.revision !== normalized) {
616
+ throw new CaptureJobConflictError(record.id, normalized, record.revision);
617
+ }
618
+ }
619
+ function nextTimestamp(record, selected) {
620
+ const next = inputTimestamp(selected);
621
+ if (next < record.updatedAt)
622
+ throw new Error("Capture job timestamp must not move backwards.");
623
+ return next;
624
+ }
625
+ function appendAttempts(current, appended) {
626
+ const next = [...current, ...normalizeAttempts(appended ?? [], false)];
627
+ if (next.length > MAX_CAPTURE_JOB_ATTEMPTS) {
628
+ throw new Error(`Capture job attempts exceed the ${MAX_CAPTURE_JOB_ATTEMPTS}-item limit.`);
629
+ }
630
+ return next;
631
+ }
632
+ function appendWarnings(current, appended) {
633
+ const next = [...current, ...normalizeWarnings(appended ?? [], false)];
634
+ if (next.length > MAX_CAPTURE_JOB_WARNINGS) {
635
+ throw new Error(`Capture job warnings exceed the ${MAX_CAPTURE_JOB_WARNINGS}-item limit.`);
636
+ }
637
+ return next;
638
+ }
639
+ async function createCaptureJob(store, input) {
640
+ await assertStore(store);
641
+ const id = captureJobId(input.id ?? randomUUID());
642
+ return withMutation(store, id, async (lease) => {
643
+ const at = inputTimestamp(input.at);
644
+ const record = {
645
+ schemaVersion: CAPTURE_JOB_SCHEMA_VERSION,
646
+ id,
647
+ revision: 1,
648
+ lifecycle: "running",
649
+ captureStatus: null,
650
+ startedAt: at,
651
+ updatedAt: at,
652
+ finishedAt: null,
653
+ phase: "queued",
654
+ target: normalizedText(input.target, "target", MAX_TARGET_BYTES, true),
655
+ attempts: [],
656
+ warnings: [],
657
+ bundle: null,
658
+ error: null
659
+ };
660
+ await installCreatedRecord(store, record, lease);
661
+ return record;
662
+ });
663
+ }
664
+ async function readCaptureJob(store, id) {
665
+ return (await readSnapshot(store, id)).record;
666
+ }
667
+ async function listCaptureJobs(store, options = {}) {
668
+ await assertStore(store);
669
+ const limit = options.limit ?? 100;
670
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_CAPTURE_JOB_LIST_LIMIT) {
671
+ throw new Error(`Capture job list limit must be between 1 and ${MAX_CAPTURE_JOB_LIST_LIMIT}.`);
672
+ }
673
+ const ids = [];
674
+ let entries = 0;
675
+ const directory = await opendir(store.root);
676
+ for await (const entry of directory) {
677
+ entries += 1;
678
+ if (entries > MAX_CAPTURE_JOB_RECORDS) {
679
+ throw new Error(`Capture job store exceeds the ${MAX_CAPTURE_JOB_RECORDS}-entry scan limit.`);
680
+ }
681
+ if (!entry.name.endsWith(".json"))
682
+ continue;
683
+ const id = entry.name.slice(0, -5);
684
+ if (UUID_V4.test(id))
685
+ ids.push(id);
686
+ }
687
+ await assertStore(store);
688
+ const records = [];
689
+ for (const id of ids) {
690
+ records.push(await readCaptureJob(store, id));
691
+ records.sort((left, right) => right.startedAt.localeCompare(left.startedAt) || left.id.localeCompare(right.id));
692
+ if (records.length > limit)
693
+ records.pop();
694
+ }
695
+ records.sort((left, right) => right.startedAt.localeCompare(left.startedAt) || left.id.localeCompare(right.id));
696
+ return records;
697
+ }
698
+ async function updateCaptureJob(store, id, input) {
699
+ captureJobId(id);
700
+ return withMutation(store, id, async (lease) => {
701
+ const previous = await readSnapshot(store, id);
702
+ ensureRunning(previous.record);
703
+ ensureExpectedRevision(previous.record, input.expectedRevision);
704
+ const nextPhase = phase(input.phase);
705
+ if (nextPhase === "finished")
706
+ throw new Error("Running capture job phase cannot be finished.");
707
+ if (captureJobPhases.indexOf(nextPhase) < captureJobPhases.indexOf(previous.record.phase)) {
708
+ throw new Error("Capture job phase cannot move backwards.");
709
+ }
710
+ const at = nextTimestamp(previous.record, input.at);
711
+ const record = {
712
+ ...previous.record,
713
+ revision: previous.record.revision + 1,
714
+ updatedAt: at,
715
+ phase: nextPhase,
716
+ attempts: appendAttempts(previous.record.attempts, input.appendAttempts),
717
+ warnings: appendWarnings(previous.record.warnings, input.appendWarnings)
718
+ };
719
+ await installUpdatedRecord(store, previous, record, lease);
720
+ return record;
721
+ });
722
+ }
723
+ async function completeCaptureJob(store, id, input) {
724
+ captureJobId(id);
725
+ return withMutation(store, id, async (lease) => {
726
+ const previous = await readSnapshot(store, id);
727
+ ensureRunning(previous.record);
728
+ ensureExpectedRevision(previous.record, input.expectedRevision);
729
+ const at = nextTimestamp(previous.record, input.at);
730
+ const record = {
731
+ ...previous.record,
732
+ revision: previous.record.revision + 1,
733
+ lifecycle: "completed",
734
+ captureStatus: captureStatus(input.status),
735
+ updatedAt: at,
736
+ finishedAt: at,
737
+ phase: "finished",
738
+ attempts: appendAttempts(previous.record.attempts, input.appendAttempts),
739
+ warnings: appendWarnings(previous.record.warnings, input.appendWarnings),
740
+ bundle: input.bundle === undefined || input.bundle === null ? null : normalizeBundle(input.bundle, false)
741
+ };
742
+ await installUpdatedRecord(store, previous, record, lease);
743
+ return record;
744
+ });
745
+ }
746
+ async function failCaptureJob(store, id, input) {
747
+ captureJobId(id);
748
+ return withMutation(store, id, async (lease) => {
749
+ const previous = await readSnapshot(store, id);
750
+ ensureRunning(previous.record);
751
+ ensureExpectedRevision(previous.record, input.expectedRevision);
752
+ const at = nextTimestamp(previous.record, input.at);
753
+ const record = {
754
+ ...previous.record,
755
+ revision: previous.record.revision + 1,
756
+ lifecycle: "failed",
757
+ updatedAt: at,
758
+ finishedAt: at,
759
+ phase: "finished",
760
+ attempts: appendAttempts(previous.record.attempts, input.appendAttempts),
761
+ warnings: appendWarnings(previous.record.warnings, input.appendWarnings),
762
+ error: normalizedText(input.error, "error", MAX_ERROR_BYTES, false)
763
+ };
764
+ await installUpdatedRecord(store, previous, record, lease);
765
+ return record;
766
+ });
767
+ }
768
+ export {
769
+ updateCaptureJob,
770
+ readCaptureJob,
771
+ openCaptureJobStore,
772
+ listCaptureJobs,
773
+ failCaptureJob,
774
+ createCaptureJob,
775
+ completeCaptureJob,
776
+ captureJobPhases,
777
+ MAX_CAPTURE_JOB_WARNINGS,
778
+ MAX_CAPTURE_JOB_BYTES,
779
+ MAX_CAPTURE_JOB_ATTEMPTS,
780
+ CaptureJobSafetyError,
781
+ CaptureJobNotFoundError,
782
+ CaptureJobConflictError,
783
+ CAPTURE_JOB_SCHEMA_VERSION
784
+ };