@hraness/wordcell 0.20.0-bootstrap.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 (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +749 -0
  3. package/dist/agent-context.js +35 -0
  4. package/dist/agent-guide-audit.js +18 -0
  5. package/dist/attachments.js +19 -0
  6. package/dist/authoring.js +33 -0
  7. package/dist/benchmark.js +19 -0
  8. package/dist/capture.js +49 -0
  9. package/dist/cli.js +57 -0
  10. package/dist/clip/acquire.js +57 -0
  11. package/dist/clip/args.js +21 -0
  12. package/dist/clip/bounded-byte-buffer.js +9 -0
  13. package/dist/clip/browser-profiles.js +17 -0
  14. package/dist/clip/bundle-reader.js +11 -0
  15. package/dist/clip/cli.js +27 -0
  16. package/dist/clip/cookies.js +21 -0
  17. package/dist/clip/defuddle-worker.js +84 -0
  18. package/dist/clip/doctor.js +32 -0
  19. package/dist/clip/jobs.js +784 -0
  20. package/dist/clip/network-proxy.js +9 -0
  21. package/dist/clip/network.js +28 -0
  22. package/dist/clip/persist.js +30 -0
  23. package/dist/clip/refresh.js +11 -0
  24. package/dist/clip/terminal.js +11 -0
  25. package/dist/evaluation-builder.js +12238 -0
  26. package/dist/evaluation-kb.js +26 -0
  27. package/dist/evaluation.js +35 -0
  28. package/dist/git.js +39 -0
  29. package/dist/graph.js +41 -0
  30. package/dist/index-054mb7d3.js +1974 -0
  31. package/dist/index-06c9ctr6.js +738 -0
  32. package/dist/index-180zbkkt.js +2480 -0
  33. package/dist/index-1gwbassd.js +743 -0
  34. package/dist/index-1xxnjn0d.js +171 -0
  35. package/dist/index-23z4zxgg.js +119 -0
  36. package/dist/index-2gv8y733.js +432 -0
  37. package/dist/index-3ehtt59c.js +199 -0
  38. package/dist/index-3rm7cz6h.js +373 -0
  39. package/dist/index-48pz4jpc.js +392 -0
  40. package/dist/index-4j3tt0c3.js +234 -0
  41. package/dist/index-5n05se68.js +307 -0
  42. package/dist/index-5vwpzb5a.js +764 -0
  43. package/dist/index-6jcz0m1c.js +712 -0
  44. package/dist/index-6sw24nvv.js +1189 -0
  45. package/dist/index-8a08v6x8.js +56 -0
  46. package/dist/index-adx6khj5.js +488 -0
  47. package/dist/index-b88v3vtm.js +805 -0
  48. package/dist/index-bcknqxrq.js +1943 -0
  49. package/dist/index-bs0yf4pp.js +711 -0
  50. package/dist/index-by0pbant.js +827 -0
  51. package/dist/index-d13v9ckt.js +150 -0
  52. package/dist/index-dfag79p7.js +370 -0
  53. package/dist/index-e4fagfjz.js +44 -0
  54. package/dist/index-e5fbsywq.js +658 -0
  55. package/dist/index-ekpwvbra.js +1070 -0
  56. package/dist/index-f984hw45.js +927 -0
  57. package/dist/index-gh719d91.js +63 -0
  58. package/dist/index-h170byqw.js +371 -0
  59. package/dist/index-hgve9rh2.js +2046 -0
  60. package/dist/index-hya40gb2.js +474 -0
  61. package/dist/index-j4zgmzjr.js +152 -0
  62. package/dist/index-m7et4rz4.js +966 -0
  63. package/dist/index-mxxxytys.js +1254 -0
  64. package/dist/index-n05s3wsb.js +2728 -0
  65. package/dist/index-nd6nynv2.js +1162 -0
  66. package/dist/index-npg9z1a4.js +431 -0
  67. package/dist/index-pj501bh1.js +140 -0
  68. package/dist/index-rdd5f7jj.js +70 -0
  69. package/dist/index-sy0zety7.js +3630 -0
  70. package/dist/index-tkpxp48t.js +1166 -0
  71. package/dist/index-w2zc0vwa.js +571 -0
  72. package/dist/index-why54q5p.js +462 -0
  73. package/dist/index-x3fthpsc.js +585 -0
  74. package/dist/index-x6m57nfc.js +1041 -0
  75. package/dist/index.js +784 -0
  76. package/dist/kb-alias.js +55 -0
  77. package/dist/navigation.js +13 -0
  78. package/dist/pdf.js +85 -0
  79. package/dist/percolate.js +44 -0
  80. package/dist/portfolio.js +77 -0
  81. package/dist/query.js +29 -0
  82. package/dist/repository-memory.js +60 -0
  83. package/dist/sdk.js +36 -0
  84. package/dist/search-rules.js +34 -0
  85. package/dist/search.js +22 -0
  86. package/dist/semantic.js +39 -0
  87. package/dist/source-inbox.js +17 -0
  88. package/dist/untrusted-content.js +32 -0
  89. package/dist/url-intelligence.js +74 -0
  90. package/dist/workflow.js +23 -0
  91. package/dist/workflows/decision-context.js +20 -0
  92. package/dist/workflows/explain-change.js +8 -0
  93. package/dist/workflows/index.js +28 -0
  94. package/dist/workflows/plan-radar.js +10 -0
  95. package/package.json +421 -0
  96. package/skills/wordcell/AGENTS.md +22 -0
  97. package/skills/wordcell/SKILL.md +91 -0
  98. package/skills/wordcell/agents/openai.yaml +4 -0
  99. package/skills/wordcell/references/companion-skills.md +96 -0
  100. package/skills/wordcell/references/customize.md +123 -0
  101. package/skills/wordcell/references/pdf-review.md +51 -0
  102. package/skills/wordcell/references/percolate.md +176 -0
  103. package/skills/wordcell/references/plan-structure.md +105 -0
  104. package/skills/wordcell/references/plan.md +115 -0
  105. package/skills/wordcell/references/query.md +252 -0
  106. package/skills/wordcell/references/refresh.md +133 -0
  107. package/skills/wordcell/references/save-pdf.md +125 -0
  108. package/skills/wordcell/references/save-url.md +178 -0
  109. package/skills/wordcell/references/url-authentication.md +75 -0
  110. package/skills/wordcell/references/url-platforms.md +25 -0
  111. package/skills/wordcell/templates/companion-skill.template.md +57 -0
  112. package/src/agent-context.ts +1241 -0
  113. package/src/agent-guide-audit.ts +750 -0
  114. package/src/attachments.ts +723 -0
  115. package/src/authoring-model.ts +798 -0
  116. package/src/authoring-platform.ts +549 -0
  117. package/src/authoring-program.ts +278 -0
  118. package/src/authoring-runtime.ts +54 -0
  119. package/src/authoring.ts +75 -0
  120. package/src/benchmark.ts +362 -0
  121. package/src/capture.ts +68 -0
  122. package/src/cli-program.ts +3662 -0
  123. package/src/cli.ts +6 -0
  124. package/src/clip/acquire.ts +1273 -0
  125. package/src/clip/archive-today.ts +402 -0
  126. package/src/clip/args.ts +423 -0
  127. package/src/clip/assets.ts +262 -0
  128. package/src/clip/bounded-byte-buffer.ts +75 -0
  129. package/src/clip/browser-profiles.ts +372 -0
  130. package/src/clip/bundle-reader.ts +608 -0
  131. package/src/clip/capture.ts +1054 -0
  132. package/src/clip/cli.ts +181 -0
  133. package/src/clip/cookies.ts +512 -0
  134. package/src/clip/doctor.ts +1110 -0
  135. package/src/clip/extract.ts +868 -0
  136. package/src/clip/jobs.ts +959 -0
  137. package/src/clip/lib.ts +861 -0
  138. package/src/clip/media.ts +1474 -0
  139. package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
  140. package/src/clip/metadata-search-tool/Cargo.toml +18 -0
  141. package/src/clip/metadata-search-tool/runner.ts +685 -0
  142. package/src/clip/metadata-search-tool/src/main.rs +389 -0
  143. package/src/clip/metadata-search.ts +687 -0
  144. package/src/clip/network-proxy.ts +627 -0
  145. package/src/clip/network.ts +950 -0
  146. package/src/clip/package-root.ts +54 -0
  147. package/src/clip/persist.ts +1472 -0
  148. package/src/clip/platforms.ts +1824 -0
  149. package/src/clip/refresh.ts +197 -0
  150. package/src/clip/structured.ts +432 -0
  151. package/src/clip/terminal.ts +197 -0
  152. package/src/clip/url-intelligence.ts +581 -0
  153. package/src/clip/url-metadata-backfill.ts +616 -0
  154. package/src/clip/url-metadata-cli.ts +251 -0
  155. package/src/clip/url-metadata.ts +1397 -0
  156. package/src/evaluation-analysis-v2.ts +1440 -0
  157. package/src/evaluation-builder.ts +2539 -0
  158. package/src/evaluation-corpus-authoring.ts +2052 -0
  159. package/src/evaluation-evidence.ts +1147 -0
  160. package/src/evaluation-execution-v2.ts +426 -0
  161. package/src/evaluation-implementation.ts +131 -0
  162. package/src/evaluation-kb-closure.ts +1430 -0
  163. package/src/evaluation-kb-runner-v2.ts +735 -0
  164. package/src/evaluation-kb-v2.ts +2567 -0
  165. package/src/evaluation-kb.ts +1018 -0
  166. package/src/evaluation-measurement-v2.ts +1462 -0
  167. package/src/evaluation-packing-v2.ts +452 -0
  168. package/src/evaluation-redaction.ts +28 -0
  169. package/src/evaluation-v2.ts +3857 -0
  170. package/src/evaluation.ts +1220 -0
  171. package/src/git.ts +1276 -0
  172. package/src/graph.ts +1494 -0
  173. package/src/index.ts +21 -0
  174. package/src/init.ts +123 -0
  175. package/src/kb-alias.ts +14 -0
  176. package/src/navigation.ts +277 -0
  177. package/src/note-lock.ts +529 -0
  178. package/src/oh-adoption.ts +444 -0
  179. package/src/pdf/args.ts +202 -0
  180. package/src/pdf/capture.ts +298 -0
  181. package/src/pdf/cli.ts +159 -0
  182. package/src/pdf/extract.ts +303 -0
  183. package/src/pdf/layout.ts +618 -0
  184. package/src/pdf/markdown.ts +154 -0
  185. package/src/pdf/model.ts +234 -0
  186. package/src/pdf/ocr.ts +190 -0
  187. package/src/pdf/persist.ts +314 -0
  188. package/src/pdf/source.ts +135 -0
  189. package/src/pdf/tools.ts +146 -0
  190. package/src/pdf.ts +92 -0
  191. package/src/percolate.ts +1948 -0
  192. package/src/portfolio-audit.ts +574 -0
  193. package/src/portfolio-identity.ts +162 -0
  194. package/src/portfolio-registry.ts +473 -0
  195. package/src/portfolio.ts +765 -0
  196. package/src/query.ts +646 -0
  197. package/src/repository-memory.ts +1150 -0
  198. package/src/sdk.ts +1452 -0
  199. package/src/search-rules.ts +712 -0
  200. package/src/search.ts +710 -0
  201. package/src/semantic-runtime.ts +1139 -0
  202. package/src/semantic.ts +2480 -0
  203. package/src/source-inbox.ts +232 -0
  204. package/src/untrusted-content.ts +401 -0
  205. package/src/url-intelligence.ts +104 -0
  206. package/src/vault.ts +636 -0
  207. package/src/workflow-model.ts +444 -0
  208. package/src/workflow-platform.ts +74 -0
  209. package/src/workflow-program.ts +183 -0
  210. package/src/workflow-runtime.ts +51 -0
  211. package/src/workflow.ts +23 -0
  212. package/src/workflows/decision-context.ts +81 -0
  213. package/src/workflows/explain-change.ts +73 -0
  214. package/src/workflows/index.ts +3 -0
  215. package/src/workflows/plan-radar.ts +101 -0
@@ -0,0 +1,444 @@
1
+ import { createHash } from "node:crypto";
2
+ import { posix } from "node:path";
3
+
4
+ import { canonicalJson, canonicalSha256 } from "@hraness/oh";
5
+ import {
6
+ parseOhHeadV1,
7
+ parseOhStoreBindingV1,
8
+ verifyOhDependencyClosureAgainstV1,
9
+ type OhDependencyClosureV1,
10
+ type OhHeadV1,
11
+ type OhStoreBindingV1,
12
+ } from "@hraness/oh/store";
13
+
14
+ const CODE_PATTERN = /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u;
15
+ const RECORD_KEY_PATTERN = /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u;
16
+ const MAX_CAPSULE_BYTES = 16 * 1024 * 1024;
17
+ const MAX_RECORDS = 1_024;
18
+ const MAX_ROOTS = 256;
19
+ const MAX_TEXT_BYTES = 4_096;
20
+ const MAX_STRUCTURAL_NODES = 262_144;
21
+ const MAX_STRUCTURAL_DEPTH = 128;
22
+
23
+ export interface OhAdoptionExpectedSourceV1 {
24
+ readonly authorityId: string;
25
+ readonly binding: OhStoreBindingV1;
26
+ readonly head: OhHeadV1;
27
+ readonly v: 1;
28
+ }
29
+
30
+ export interface OhAdoptionDestinationV1 {
31
+ readonly purpose: string;
32
+ readonly targetPath: string;
33
+ readonly v: 1;
34
+ }
35
+
36
+ export interface OhAdoptionRightsClearanceV1 {
37
+ readonly decisionId: string;
38
+ readonly disposition: "cleared-for-purpose";
39
+ readonly purpose: string;
40
+ readonly v: 1;
41
+ }
42
+
43
+ export interface OhAdoptionReviewRequirementV1 {
44
+ readonly route: string;
45
+ readonly status: "required";
46
+ readonly v: 1;
47
+ }
48
+
49
+ export interface OhAdoptionConflictReviewV1 {
50
+ readonly notes: readonly string[];
51
+ readonly status: "none-observed" | "requires-resolution";
52
+ readonly v: 1;
53
+ }
54
+
55
+ export interface OhAdoptionChangeDisclosureV1 {
56
+ readonly id: string;
57
+ readonly recordKey: string;
58
+ readonly summary: string;
59
+ readonly v: 1;
60
+ }
61
+
62
+ export interface OhAdoptionHostPolicyV1 {
63
+ readonly conflicts: OhAdoptionConflictReviewV1;
64
+ readonly destination: OhAdoptionDestinationV1;
65
+ readonly expectedSource: OhAdoptionExpectedSourceV1;
66
+ readonly review: OhAdoptionReviewRequirementV1;
67
+ readonly rights: OhAdoptionRightsClearanceV1;
68
+ readonly v: 1;
69
+ }
70
+
71
+ export interface OhAdoptionPrepareInputV1 {
72
+ readonly capsule: OhDependencyClosureV1;
73
+ readonly redactions: readonly OhAdoptionChangeDisclosureV1[];
74
+ readonly transformations: readonly OhAdoptionChangeDisclosureV1[];
75
+ readonly v: 1;
76
+ }
77
+
78
+ export interface OhAdoptionCandidateV1 {
79
+ readonly artifactSha256: string;
80
+ readonly candidateSha256: string;
81
+ readonly manifest: Readonly<{
82
+ readonly conflicts: OhAdoptionConflictReviewV1;
83
+ readonly destination: OhAdoptionDestinationV1;
84
+ readonly format: "hraness.kb.oh-adoption-candidate.v1";
85
+ readonly redactions: readonly OhAdoptionChangeDisclosureV1[];
86
+ readonly review: OhAdoptionReviewRequirementV1;
87
+ readonly rights: OhAdoptionRightsClearanceV1;
88
+ readonly source: Readonly<{
89
+ readonly authorityId: string;
90
+ readonly binding: Readonly<{
91
+ readonly bindingSha256: string;
92
+ readonly v: 1;
93
+ }>;
94
+ readonly closureSha256: string;
95
+ readonly head: OhHeadV1;
96
+ readonly records: readonly Readonly<{
97
+ readonly dependencies: readonly string[];
98
+ readonly key: string;
99
+ readonly kind: string;
100
+ readonly recordSha256: string;
101
+ readonly v: 1;
102
+ }>[];
103
+ readonly roots: readonly string[];
104
+ readonly v: 1;
105
+ }>;
106
+ readonly status: "prepared";
107
+ readonly transformations: readonly OhAdoptionChangeDisclosureV1[];
108
+ readonly v: 1;
109
+ }>;
110
+ readonly markdown: string;
111
+ readonly v: 1;
112
+ }
113
+
114
+ export interface OhAdoptionPreparerV1 {
115
+ /** Accepts capsule bytes and disclosures only; all authority and policy are host-bound. */
116
+ prepare(value: unknown): OhAdoptionCandidateV1;
117
+ }
118
+
119
+ function isRecord(value: unknown): value is Record<string, unknown> {
120
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
121
+ const prototype = Object.getPrototypeOf(value);
122
+ return prototype === Object.prototype || prototype === null;
123
+ }
124
+
125
+ function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
126
+ const actual = Reflect.ownKeys(value);
127
+ return actual.length === keys.length && actual.every((key) => {
128
+ if (typeof key !== "string" || !keys.includes(key)) return false;
129
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
130
+ return descriptor !== undefined && descriptor.enumerable && "value" in descriptor;
131
+ });
132
+ }
133
+
134
+ function validUnicode(value: string): boolean {
135
+ for (let index = 0; index < value.length; index += 1) {
136
+ const code = value.charCodeAt(index);
137
+ if (code >= 0xd800 && code <= 0xdbff) {
138
+ const next = value.charCodeAt(index + 1);
139
+ if (next < 0xdc00 || next > 0xdfff) return false;
140
+ index += 1;
141
+ } else if (code >= 0xdc00 && code <= 0xdfff) return false;
142
+ }
143
+ return true;
144
+ }
145
+
146
+ function structurallyBounded(value: unknown): boolean {
147
+ const pending: Array<readonly [unknown, number]> = [[value, 0]];
148
+ const seen = new Set<object>();
149
+ let nodes = 0;
150
+ let scalarBytes = 0;
151
+ while (pending.length > 0) {
152
+ const [candidate, depth] = pending.pop() as readonly [unknown, number];
153
+ nodes += 1;
154
+ scalarBytes += 4;
155
+ if (nodes > MAX_STRUCTURAL_NODES || depth > MAX_STRUCTURAL_DEPTH
156
+ || scalarBytes > MAX_CAPSULE_BYTES) return false;
157
+ if (typeof candidate === "string") {
158
+ if (!validUnicode(candidate)) return false;
159
+ scalarBytes += Buffer.byteLength(candidate, "utf8");
160
+ if (scalarBytes > MAX_CAPSULE_BYTES) return false;
161
+ }
162
+ if (typeof candidate !== "object" || candidate === null) continue;
163
+ if (seen.has(candidate)) return false;
164
+ seen.add(candidate);
165
+ if (Array.isArray(candidate)) {
166
+ if (candidate.length > MAX_STRUCTURAL_NODES) return false;
167
+ const keys = Reflect.ownKeys(candidate);
168
+ if (keys.some((key) => key !== "length" && (typeof key !== "string"
169
+ || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= candidate.length))) return false;
170
+ for (let index = 0; index < candidate.length; index += 1) {
171
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, String(index));
172
+ if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) return false;
173
+ pending.push([descriptor.value, depth + 1]);
174
+ }
175
+ } else if (isRecord(candidate)) {
176
+ const keys = Reflect.ownKeys(candidate);
177
+ if (keys.length > MAX_STRUCTURAL_NODES || keys.some((key) => typeof key !== "string")) return false;
178
+ for (const key of keys as string[]) {
179
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
180
+ if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)
181
+ || !validUnicode(key)) return false;
182
+ scalarBytes += Buffer.byteLength(key, "utf8");
183
+ if (scalarBytes > MAX_CAPSULE_BYTES) return false;
184
+ pending.push([descriptor.value, depth + 1]);
185
+ }
186
+ } else return false;
187
+ }
188
+ return true;
189
+ }
190
+
191
+ function immutableClone<T>(value: T): T {
192
+ if (Array.isArray(value)) {
193
+ return Object.freeze(value.map((item) => immutableClone(item))) as T;
194
+ }
195
+ if (isRecord(value)) {
196
+ const clone: Record<string, unknown> = {};
197
+ for (const key of Object.keys(value)) clone[key] = immutableClone(value[key]);
198
+ return Object.freeze(clone) as T;
199
+ }
200
+ return value;
201
+ }
202
+
203
+ function code(value: unknown, maximum = 256): string | null {
204
+ return typeof value === "string" && value.length <= maximum && CODE_PATTERN.test(value) ? value : null;
205
+ }
206
+
207
+ function recordKey(value: unknown): string | null {
208
+ return typeof value === "string" && value.length <= 512 && RECORD_KEY_PATTERN.test(value) ? value : null;
209
+ }
210
+
211
+ function orderedUnique(values: readonly string[]): boolean {
212
+ return values.every((value, index) => index === 0 || (values[index - 1] as string) < value);
213
+ }
214
+
215
+ function unsafeReviewCodePoint(codePoint: number): boolean {
216
+ return codePoint <= 0x1f
217
+ || (codePoint >= 0x7f && codePoint <= 0x9f)
218
+ || codePoint === 0x061c
219
+ || codePoint === 0x200e
220
+ || codePoint === 0x200f
221
+ || (codePoint >= 0x2028 && codePoint <= 0x202e)
222
+ || (codePoint >= 0x2066 && codePoint <= 0x2069)
223
+ || codePoint === 0xfeff;
224
+ }
225
+
226
+ function singleLine(value: unknown): string | null {
227
+ if (typeof value !== "string" || value.length < 1 || value.normalize("NFC") !== value
228
+ || !validUnicode(value) || [...value].some((character) =>
229
+ unsafeReviewCodePoint(character.codePointAt(0) ?? 0))
230
+ || Buffer.byteLength(value, "utf8") > MAX_TEXT_BYTES) return null;
231
+ return value;
232
+ }
233
+
234
+ function parseExpectedSource(value: unknown): OhAdoptionExpectedSourceV1 | null {
235
+ if (!isRecord(value) || !exactKeys(value, ["authorityId", "binding", "head", "v"])
236
+ || value.v !== 1) return null;
237
+ const authorityId = code(value.authorityId);
238
+ const binding = parseOhStoreBindingV1(value.binding);
239
+ const head = parseOhHeadV1(value.head);
240
+ return authorityId !== null && binding !== null && binding.profile.profileKind === "working" && head !== null
241
+ ? immutableClone({ authorityId, binding, head, v: 1 }) : null;
242
+ }
243
+
244
+ function parseDestination(value: unknown): OhAdoptionDestinationV1 | null {
245
+ if (!isRecord(value) || !exactKeys(value, ["purpose", "targetPath", "v"]) || value.v !== 1) return null;
246
+ const purpose = code(value.purpose);
247
+ if (purpose === null || typeof value.targetPath !== "string" || value.targetPath.length > 512
248
+ || value.targetPath.includes("\\") || value.targetPath.startsWith("/")
249
+ || posix.normalize(value.targetPath) !== value.targetPath
250
+ || !/^notes\/[a-z0-9][a-z0-9._/-]*\.md$/u.test(value.targetPath)
251
+ || value.targetPath.split("/").some((segment) => segment === "." || segment === ".." || segment.startsWith("."))) {
252
+ return null;
253
+ }
254
+ return { purpose, targetPath: value.targetPath, v: 1 };
255
+ }
256
+
257
+ function parseRights(value: unknown, purpose: string): OhAdoptionRightsClearanceV1 | null {
258
+ if (!isRecord(value) || !exactKeys(value, ["decisionId", "disposition", "purpose", "v"])
259
+ || value.v !== 1 || value.disposition !== "cleared-for-purpose" || value.purpose !== purpose) return null;
260
+ const decisionId = code(value.decisionId);
261
+ return decisionId === null ? null
262
+ : { decisionId, disposition: "cleared-for-purpose", purpose, v: 1 };
263
+ }
264
+
265
+ function parseReview(value: unknown): OhAdoptionReviewRequirementV1 | null {
266
+ if (!isRecord(value) || !exactKeys(value, ["route", "status", "v"])
267
+ || value.v !== 1 || value.status !== "required") return null;
268
+ const route = code(value.route);
269
+ return route === null ? null : { route, status: "required", v: 1 };
270
+ }
271
+
272
+ function parseConflicts(value: unknown): OhAdoptionConflictReviewV1 | null {
273
+ if (!isRecord(value) || !exactKeys(value, ["notes", "status", "v"]) || value.v !== 1
274
+ || (value.status !== "none-observed" && value.status !== "requires-resolution")
275
+ || !Array.isArray(value.notes) || value.notes.length < 1 || value.notes.length > 64) return null;
276
+ const notes = value.notes.map(singleLine);
277
+ if (notes.some((note) => note === null)) return null;
278
+ const sorted = [...notes as string[]].sort();
279
+ return orderedUnique(sorted) ? { notes: sorted, status: value.status, v: 1 } : null;
280
+ }
281
+
282
+ function parseHostPolicy(value: unknown): OhAdoptionHostPolicyV1 | null {
283
+ if (!structurallyBounded(value) || !isRecord(value)
284
+ || !exactKeys(value, ["conflicts", "destination", "expectedSource", "review", "rights", "v"])
285
+ || value.v !== 1) return null;
286
+ const destination = parseDestination(value.destination);
287
+ const expectedSource = parseExpectedSource(value.expectedSource);
288
+ const conflicts = parseConflicts(value.conflicts);
289
+ const review = parseReview(value.review);
290
+ const rights = destination === null ? null : parseRights(value.rights, destination.purpose);
291
+ return destination !== null && expectedSource !== null && conflicts !== null && review !== null && rights !== null
292
+ ? immutableClone({ conflicts, destination, expectedSource, review, rights, v: 1 }) : null;
293
+ }
294
+
295
+ function verifyCapsule(
296
+ value: unknown,
297
+ expectedSource: OhAdoptionExpectedSourceV1,
298
+ ): OhDependencyClosureV1 | null {
299
+ try {
300
+ if (!structurallyBounded(value) || !isRecord(value)
301
+ || !exactKeys(value, ["binding", "closureSha256", "head", "records", "roots", "v"])
302
+ || value.v !== 1 || !Array.isArray(value.records) || !Array.isArray(value.roots)
303
+ || value.records.length < 1 || value.records.length > MAX_RECORDS
304
+ || value.roots.length < 1 || value.roots.length > MAX_ROOTS
305
+ || Buffer.byteLength(canonicalJson(value), "utf8") > MAX_CAPSULE_BYTES) return null;
306
+ const verified = verifyOhDependencyClosureAgainstV1(value, {
307
+ binding: expectedSource.binding,
308
+ head: expectedSource.head,
309
+ });
310
+ return verified.ok && verified.closure.binding.profile.profileKind === "working"
311
+ ? verified.closure : null;
312
+ } catch {
313
+ return null;
314
+ }
315
+ }
316
+
317
+ function parseDisclosures(value: unknown, keys: ReadonlySet<string>): readonly OhAdoptionChangeDisclosureV1[] | null {
318
+ if (!Array.isArray(value) || value.length > 256) return null;
319
+ const parsed: OhAdoptionChangeDisclosureV1[] = [];
320
+ for (const item of value) {
321
+ if (!isRecord(item) || !exactKeys(item, ["id", "recordKey", "summary", "v"]) || item.v !== 1) return null;
322
+ const id = code(item.id);
323
+ const key = recordKey(item.recordKey);
324
+ const summary = singleLine(item.summary);
325
+ if (id === null || key === null || summary === null || !keys.has(key)) return null;
326
+ parsed.push({ id, recordKey: key, summary, v: 1 });
327
+ }
328
+ parsed.sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
329
+ return orderedUnique(parsed.map((item) => item.id)) ? parsed : null;
330
+ }
331
+
332
+ function markdownEscape(value: string): string {
333
+ return value.replace(/[\\`*_{}\[\]<>()#+.!|>-]/gu, "\\$&");
334
+ }
335
+
336
+ function renderMarkdown(manifest: OhAdoptionCandidateV1["manifest"], candidateSha256: string): string {
337
+ const lines = [
338
+ "# Oh adoption candidate",
339
+ "",
340
+ `- Status: \`${manifest.status}\``,
341
+ `- Candidate: \`sha256:${candidateSha256}\``,
342
+ `- Destination: \`${manifest.destination.targetPath}\``,
343
+ `- Purpose: \`${manifest.destination.purpose}\``,
344
+ `- Source authority: \`${manifest.source.authorityId}\``,
345
+ `- Source binding: \`${manifest.source.binding.bindingSha256}\``,
346
+ `- Source head sequence: \`${manifest.source.head.sequence}\``,
347
+ `- Source head operation: \`${manifest.source.head.operationSha256 ?? "empty"}\``,
348
+ `- Source graph revision: \`${manifest.source.head.graphRevisionSha256 ?? "empty"}\``,
349
+ `- Source records digest: \`${manifest.source.head.recordsSha256}\``,
350
+ `- Closure: \`${manifest.source.closureSha256}\``,
351
+ "",
352
+ "This is a review candidate, not reviewed knowledge. It does not mutate a vault or adopt the source operation chain, database, projection, or derived tuples.",
353
+ "",
354
+ "## Required decisions",
355
+ "",
356
+ `- Rights: \`${manifest.rights.disposition}\` via \`${manifest.rights.decisionId}\` for \`${manifest.rights.purpose}\``,
357
+ `- Review: \`${manifest.review.status}\` via \`${manifest.review.route}\``,
358
+ `- Conflicts: \`${manifest.conflicts.status}\``,
359
+ ...manifest.conflicts.notes.map((note) => ` - ${markdownEscape(note)}`),
360
+ "",
361
+ "## Selected roots",
362
+ "",
363
+ ...manifest.source.roots.map((root) => `- \`${root}\``),
364
+ "",
365
+ "## Exact source records",
366
+ "",
367
+ ];
368
+ for (const record of manifest.source.records) {
369
+ lines.push(`### \`${record.key}\``, "", `- Kind: \`${record.kind}\``,
370
+ `- Digest: \`${record.recordSha256}\``,
371
+ `- Dependencies: ${record.dependencies.length === 0 ? "none" : record.dependencies.map((key) => `\`${key}\``).join(", ")}`, "");
372
+ }
373
+ lines.push("## Transformations", "",
374
+ ...(manifest.transformations.length === 0 ? ["- None declared."] : manifest.transformations.map((item) =>
375
+ `- \`${item.id}\` on \`${item.recordKey}\`: ${markdownEscape(item.summary)}`)), "",
376
+ "## Redactions", "",
377
+ ...(manifest.redactions.length === 0 ? ["- None declared."] : manifest.redactions.map((item) =>
378
+ `- \`${item.id}\` on \`${item.recordKey}\`: ${markdownEscape(item.summary)}`)), "");
379
+ return `${lines.join("\n")}\n`;
380
+ }
381
+
382
+ function prepareWithPolicy(
383
+ value: unknown,
384
+ policy: OhAdoptionHostPolicyV1,
385
+ ): OhAdoptionCandidateV1 {
386
+ if (!structurallyBounded(value) || !isRecord(value)
387
+ || !exactKeys(value, ["capsule", "redactions", "transformations", "v"]) || value.v !== 1) {
388
+ throw new TypeError("Invalid Oh adoption preparation input.");
389
+ }
390
+ const capsule = verifyCapsule(value.capsule, policy.expectedSource);
391
+ if (capsule === null) throw new TypeError("The source capsule is invalid for the bound authority and head.");
392
+ const recordKeys = new Set(capsule.records.map((record) => record.key));
393
+ const transformations = parseDisclosures(value.transformations, recordKeys);
394
+ const redactions = parseDisclosures(value.redactions, recordKeys);
395
+ const roots = new Set(capsule.roots);
396
+ if (transformations === null || redactions === null
397
+ || capsule.records.filter((record) => roots.has(record.key)).every((record) => record.kind === "view")) {
398
+ throw new TypeError("Adoption requires valid disclosures and an authoritative root.");
399
+ }
400
+ const source = {
401
+ authorityId: policy.expectedSource.authorityId,
402
+ binding: { bindingSha256: capsule.binding.bindingSha256, v: 1 as const },
403
+ closureSha256: capsule.closureSha256,
404
+ head: capsule.head,
405
+ records: capsule.records.map((record) => ({ dependencies: record.dependencies, key: record.key,
406
+ kind: record.kind, recordSha256: record.recordSha256, v: 1 as const })),
407
+ roots: capsule.roots,
408
+ v: 1 as const,
409
+ };
410
+ const manifest = {
411
+ conflicts: policy.conflicts,
412
+ destination: policy.destination,
413
+ format: "hraness.kb.oh-adoption-candidate.v1" as const,
414
+ redactions,
415
+ review: policy.review,
416
+ rights: policy.rights,
417
+ source,
418
+ status: "prepared" as const,
419
+ transformations,
420
+ v: 1 as const,
421
+ };
422
+ const candidateSha256 = canonicalSha256(manifest);
423
+ const markdown = renderMarkdown(manifest, candidateSha256);
424
+ if (Buffer.byteLength(markdown, "utf8") > MAX_CAPSULE_BYTES) {
425
+ throw new RangeError("The adoption candidate exceeds its Markdown byte limit.");
426
+ }
427
+ return immutableClone({
428
+ artifactSha256: createHash("sha256").update(markdown).digest("hex"),
429
+ candidateSha256,
430
+ manifest,
431
+ markdown,
432
+ v: 1,
433
+ });
434
+ }
435
+
436
+ /**
437
+ * Captures all authority, destination, rights, review, and conflict policy in
438
+ * trusted host code. The returned facade can only prepare inert review bytes.
439
+ */
440
+ export function createOhAdoptionPreparerV1(value: unknown): OhAdoptionPreparerV1 {
441
+ const policy = parseHostPolicy(value);
442
+ if (policy === null) throw new TypeError("Invalid Oh adoption host policy.");
443
+ return Object.freeze({ prepare: (input: unknown) => prepareWithPolicy(input, policy) });
444
+ }
@@ -0,0 +1,202 @@
1
+ export const pdfUsage = `wordcell pdf — save a local or public remote PDF as an auditable Markdown bundle
2
+
3
+ Usage:
4
+ wordcell pdf <file-or-url> [--output <directory>] [--slug <slug>] [--annotations <json>] [--force] [--json]
5
+ wordcell pdf save <file-or-url> [capture options]
6
+
7
+ Capture options:
8
+ --output <directory> Bundle parent (default: KB_PDF_OUTPUT or kb/articles)
9
+ --slug <slug> Override the title-derived bundle name
10
+ --annotations <json> Optional hash-bound image interpretations from an agent
11
+ --timeout-ms <milliseconds> Per-tool timeout
12
+ --max-pdf-bytes <size> Input limit, for example 512mb
13
+ --max-pages <count> Maximum pages to process
14
+ --max-images <count> Maximum embedded images
15
+ --max-asset-bytes <size> Per-image limit
16
+ --max-total-asset-bytes <size>
17
+ --force Replace only a compatible PDF capture bundle
18
+ --json Emit a machine-readable result
19
+ --quiet Suppress progress and warning lines
20
+ `;
21
+
22
+ export type PdfCliArguments =
23
+ | { readonly command: "help" }
24
+ | {
25
+ readonly command: "capture";
26
+ readonly input: string;
27
+ readonly outputBase: string;
28
+ readonly slug?: string;
29
+ readonly interpretationsPath?: string;
30
+ readonly force: boolean;
31
+ readonly json: boolean;
32
+ readonly quiet: boolean;
33
+ readonly timeoutMs?: number;
34
+ readonly maxPdfBytes?: number;
35
+ readonly maxPages?: number;
36
+ readonly maxImages?: number;
37
+ readonly maxAssetBytes?: number;
38
+ readonly maxTotalAssetBytes?: number;
39
+ };
40
+
41
+ export type ParsePdfArgumentsResult =
42
+ | { readonly ok: true; readonly value: PdfCliArguments }
43
+ | { readonly ok: false; readonly message: string };
44
+
45
+ const valueOptions = new Set([
46
+ "--output",
47
+ "--slug",
48
+ "--annotations",
49
+ "--timeout-ms",
50
+ "--max-pdf-bytes",
51
+ "--max-pages",
52
+ "--max-images",
53
+ "--max-asset-bytes",
54
+ "--max-total-asset-bytes",
55
+ ]);
56
+
57
+ function optionValue(
58
+ arguments_: readonly string[],
59
+ index: number,
60
+ name: string,
61
+ ): string | ParsePdfArgumentsResult {
62
+ const value = arguments_[index + 1];
63
+ return value === undefined || value.startsWith("--")
64
+ ? { ok: false, message: `${name} requires a value` }
65
+ : value;
66
+ }
67
+
68
+ function positiveInteger(value: string, name: string, maximum: number): number | string {
69
+ if (!/^\d+$/u.test(value)) return `${name} must be a positive integer`;
70
+ const parsed = Number(value);
71
+ return Number.isSafeInteger(parsed) && parsed >= 1 && parsed <= maximum
72
+ ? parsed
73
+ : `${name} must be between 1 and ${maximum}`;
74
+ }
75
+
76
+ function byteSize(value: string, name: string, maximum: number): number | string {
77
+ const match = /^(\d+)(b|kb|mb|gb)?$/iu.exec(value);
78
+ if (match === null || match[1] === undefined) {
79
+ return `${name} must be an integer byte size such as 500000, 25mb, or 1gb`;
80
+ }
81
+ const amount = Number(match[1]);
82
+ const unit = match[2]?.toLowerCase() ?? "b";
83
+ const multiplier = unit === "gb"
84
+ ? 1024 ** 3
85
+ : unit === "mb"
86
+ ? 1024 ** 2
87
+ : unit === "kb"
88
+ ? 1024
89
+ : 1;
90
+ const parsed = amount * multiplier;
91
+ return Number.isSafeInteger(parsed) && parsed >= 1 && parsed <= maximum
92
+ ? parsed
93
+ : `${name} must be between 1 byte and ${maximum} bytes`;
94
+ }
95
+
96
+ /** Parse the delegated `wordcell pdf` surface without touching the filesystem. */
97
+ export function parsePdfArguments(
98
+ rawArguments: readonly string[],
99
+ environment: Readonly<Record<string, string | undefined>> = {},
100
+ ): ParsePdfArgumentsResult {
101
+ if (
102
+ rawArguments.length === 0
103
+ || rawArguments[0] === "help"
104
+ || rawArguments[0] === "--help"
105
+ || rawArguments[0] === "-h"
106
+ ) return { ok: true, value: { command: "help" } };
107
+
108
+ let cursor = rawArguments[0] === "save" || rawArguments[0] === "capture" ? 1 : 0;
109
+ const positional: string[] = [];
110
+ let outputBase = environment.KB_PDF_OUTPUT ?? "kb/articles";
111
+ let slug: string | undefined;
112
+ let interpretationsPath: string | undefined;
113
+ let force = false;
114
+ let json = false;
115
+ let quiet = false;
116
+ let timeoutMs: number | undefined;
117
+ let maxPdfBytes: number | undefined;
118
+ let maxPages: number | undefined;
119
+ let maxImages: number | undefined;
120
+ let maxAssetBytes: number | undefined;
121
+ let maxTotalAssetBytes: number | undefined;
122
+
123
+ for (; cursor < rawArguments.length; cursor += 1) {
124
+ const argument = rawArguments[cursor];
125
+ if (argument === undefined) continue;
126
+ if (!argument.startsWith("--")) {
127
+ positional.push(argument);
128
+ continue;
129
+ }
130
+ if (argument === "--force") {
131
+ force = true;
132
+ continue;
133
+ }
134
+ if (argument === "--json") {
135
+ json = true;
136
+ continue;
137
+ }
138
+ if (argument === "--quiet") {
139
+ quiet = true;
140
+ continue;
141
+ }
142
+ if (!valueOptions.has(argument)) {
143
+ return { ok: false, message: `unknown PDF option: ${argument}` };
144
+ }
145
+ const rawValue = optionValue(rawArguments, cursor, argument);
146
+ if (typeof rawValue !== "string") return rawValue;
147
+ cursor += 1;
148
+ if (argument === "--output") outputBase = rawValue;
149
+ else if (argument === "--slug") slug = rawValue;
150
+ else if (argument === "--annotations") interpretationsPath = rawValue;
151
+ else if (argument === "--timeout-ms") {
152
+ const parsed = positiveInteger(rawValue, argument, 10 * 60_000);
153
+ if (typeof parsed === "string") return { ok: false, message: parsed };
154
+ timeoutMs = parsed;
155
+ } else if (argument === "--max-pages") {
156
+ const parsed = positiveInteger(rawValue, argument, 10_000);
157
+ if (typeof parsed === "string") return { ok: false, message: parsed };
158
+ maxPages = parsed;
159
+ } else if (argument === "--max-images") {
160
+ const parsed = positiveInteger(rawValue, argument, 10_000);
161
+ if (typeof parsed === "string") return { ok: false, message: parsed };
162
+ maxImages = parsed;
163
+ } else {
164
+ const maximum = argument === "--max-asset-bytes" ? 2 * 1024 ** 3 : 8 * 1024 ** 3;
165
+ const parsed = byteSize(rawValue, argument, maximum);
166
+ if (typeof parsed === "string") return { ok: false, message: parsed };
167
+ if (argument === "--max-pdf-bytes") maxPdfBytes = parsed;
168
+ else if (argument === "--max-asset-bytes") maxAssetBytes = parsed;
169
+ else maxTotalAssetBytes = parsed;
170
+ }
171
+ }
172
+
173
+ const input = positional[0];
174
+ if (input === undefined || positional.length !== 1) {
175
+ return { ok: false, message: "wordcell pdf requires exactly one PDF path or public URL" };
176
+ }
177
+ if (input.length > 64 * 1024) {
178
+ return { ok: false, message: "PDF input exceeds the 65536 code-unit limit" };
179
+ }
180
+ if (outputBase.trim() === "") return { ok: false, message: "--output must not be empty" };
181
+ if (slug !== undefined && slug.trim() === "") return { ok: false, message: "--slug must not be empty" };
182
+
183
+ return {
184
+ ok: true,
185
+ value: {
186
+ command: "capture",
187
+ input,
188
+ outputBase,
189
+ ...(slug === undefined ? {} : { slug }),
190
+ ...(interpretationsPath === undefined ? {} : { interpretationsPath }),
191
+ force,
192
+ json,
193
+ quiet,
194
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
195
+ ...(maxPdfBytes === undefined ? {} : { maxPdfBytes }),
196
+ ...(maxPages === undefined ? {} : { maxPages }),
197
+ ...(maxImages === undefined ? {} : { maxImages }),
198
+ ...(maxAssetBytes === undefined ? {} : { maxAssetBytes }),
199
+ ...(maxTotalAssetBytes === undefined ? {} : { maxTotalAssetBytes }),
200
+ },
201
+ };
202
+ }