@hraness/kb 0.17.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,31 +3,55 @@
3
3
 
4
4
  [![skills.sh](https://skills.sh/b/hraness/kb)](https://skills.sh/hraness/kb)
5
5
 
6
- a knowledge base for coding agents, built from Markdown, backlinks, semantic
6
+ A knowledge base for coding agents, built from Markdown, backlinks, semantic
7
7
  search, and Git context.
8
+ It turns sources, plans, and decisions into inspectable context that agents can
9
+ recover across sessions without coupling application code to the knowledge
10
+ system.
8
11
 
9
- ## install
12
+ ## Install
13
+
14
+ Bun 1.3.14 or newer is required.
10
15
 
11
16
  ```sh
12
- bun add --global @hraness/kb@0.17.2
17
+ bun add --global @hraness/kb@0.18.0
13
18
  ```
14
19
 
15
- ## about
20
+ ## Why kb
21
+
22
+ - **Inspect what agents recover.** Markdown and Git stay authoritative,
23
+ retrieval signals stay distinct, and indexes, embeddings, and graph views
24
+ remain replaceable.
25
+ - **Keep the application independent.** Application code imports neither the
26
+ vault nor a hosted knowledge service. Capture and semantic adapters declare
27
+ their network, browser, native-tool, and model-download effects.
28
+
29
+ ## Create one durable note
30
+
31
+ From a directory without an existing `kb/` path, this local task creates a
32
+ vault, writes one typed note, and finds it without a network request or
33
+ embedding model:
34
+
35
+ ```sh
36
+ kb init kb
37
+ kb note create notes/parser-contract \
38
+ --title "Parser contract" --type concept --tag architecture --root kb
39
+ kb search "parser contract" --root kb --mode exact
40
+ ```
16
41
 
17
- Turn research, plans, and decisions into reusable context for coding agents.
18
- Keep sources and repository context beside your code in Markdown and Git. Your
19
- application stays independent.
42
+ The note is stored at `kb/notes/parser-contract.md`. Exact search reads the
43
+ current Markdown and returns that record. Commit the vault when it should
44
+ travel with the repository.
20
45
 
21
- Search identifiers and metadata. Find meaning locally with QMD. Follow
22
- backlinks and typed relations. Inspect Git provenance. Search an explicitly
23
- authorized portfolio of vaults without merging their Markdown or comparing
24
- their local QMD scores.
46
+ <!-- hraness:kb-landing:end -->
25
47
 
26
- Capture signed-in pages and PDFs. Use the TypeScript SDK and bounded workflows.
48
+ ## Links
27
49
 
28
- Markdown and Git stay authoritative. Indexes and graph views are replaceable.
50
+ [Install `@hraness/kb` from npm](https://www.npmjs.com/package/@hraness/kb) ·
51
+ [KB source on GitHub](https://github.com/hraness/kb) ·
52
+ [KB overview](https://hraness.com/kb)
29
53
 
30
- ## use
54
+ ## Use
31
55
 
32
56
  ```sh
33
57
  kb init kb
@@ -43,13 +67,6 @@ kb portfolio search "parser-v2" --registry kb-portfolio.json \
43
67
  kb history search packages/parser --root . --repo .. --json
44
68
  ```
45
69
 
46
- ## links
47
-
48
- [npm](https://www.npmjs.com/package/@hraness/kb) ·
49
- [github](https://github.com/hraness/kb) ·
50
- [overview](https://hraness.com/kb)
51
- <!-- hraness:kb-landing:end -->
52
-
53
70
  ## A knowledge base for your coding agents
54
71
 
55
72
  > Give coding agents durable, searchable memory beside the repository with plain Markdown, Git history, and replaceable local search.
@@ -113,7 +130,15 @@ kb history search packages/parser/src/index.ts --root kb --repo . --json
113
130
 
114
131
  ### Preserve evidence and plans as working records
115
132
 
116
- Durable reasoning needs inspectable evidence. `kb clip` can read a public URL, saved HTML, rendered page, a page already open in an authenticated browser, or an existing exact Archive.today snapshot after the direct routes fail. Archive fallback is read-only and always partial. The [capture documentation](<https://github.com/hraness/kb/blob/main/docs/capture.md>) defines the supported routes. A capture writes readable Markdown beside localized assets and `capture.json`, whose manifest records where the material came from, how it was extracted, what was saved, and any warnings. “Complete” describes the selected page surface, not every hidden branch or future version of the site.
133
+ Durable reasoning needs inspectable evidence. `kb clip` can read a public URL,
134
+ saved HTML, rendered page, a page already open in an authenticated browser, or
135
+ an existing exact Archive.today snapshot after the direct routes fail. Archive
136
+ fallback is read-only and always partial. The
137
+ [capture documentation](docs/capture.md) defines the supported routes. A capture
138
+ writes readable Markdown beside localized assets and `capture.json`, whose
139
+ manifest records where the material came from, how it was extracted, what was
140
+ saved, and any warnings. “Complete” describes the selected page surface, not
141
+ every hidden branch or future version of the site.
117
142
 
118
143
  **Capture a web source or local PDF**
119
144
 
@@ -168,6 +193,24 @@ Start with a short inherited `AGENTS.md` path for rules whose omission would mak
168
193
 
169
194
  Treat the knowledge base as repository-adjacent durable memory. Authored Markdown and Git are the record; catalogs, indexes, embeddings, and graph views are replaceable ways to find and inspect it. Checks can validate structure, captures can preserve a selected surface, and similarity can suggest candidates. None of those mechanisms proves that a source is trustworthy or an explanation is still true. People and agents must revise the knowledge as the repository changes.
170
195
 
196
+ ## Upgrade to v0.18.0
197
+
198
+ Version 0.18.0 adds a review-only adoption seam for exact dependency closures
199
+ from an Oh working authority. Trusted host code creates a
200
+ `createOhAdoptionPreparerV1` facade with the expected binding and head,
201
+ destination, rights clearance, review route, and conflict policy. The narrow
202
+ `prepare` call accepts only a capsule plus transformation and redaction
203
+ disclosures, returns deeply immutable deterministic Markdown and manifest
204
+ bytes with status `prepared`, and has no vault, Git, Oh-store, or promotion
205
+ capability. KB pins `@hraness/oh` v0.2.0 and delegates closure integrity to its
206
+ official store verifier.
207
+
208
+ ## Upgrade to v0.17.3
209
+
210
+ Version 0.17.3 restructures the README around an inspectable first task,
211
+ explicit operating boundaries, and a shorter path from installation to useful
212
+ output. Runtime APIs and package behavior are unchanged.
213
+
171
214
  ## Upgrade to v0.17.2
172
215
 
173
216
  Version 0.17.2 improves package discovery through focused npm keywords, a more
@@ -207,7 +250,7 @@ Existing Markdown is not rewritten automatically. Add IDs to maintained legacy
207
250
  notes only through reviewed edits, and keep every QMD, graph, portfolio, and
208
251
  audit projection disposable.
209
252
 
210
- ## Install
253
+ ## Installation reference
211
254
 
212
255
  [Bun](https://bun.sh/docs/installation) is the required runtime.
213
256
 
@@ -216,34 +259,35 @@ audit projection disposable.
216
259
  Copy this prompt into Codex, Claude Code, or another coding agent:
217
260
 
218
261
  ```text
219
- Install the `kb` Agent Skill from hraness/kb with the standard skills CLI. Use
220
- the skill's runtime instructions to install the exact `@hraness/kb@0.17.2`
221
- registry release only when the command is missing. Verify it with `kb doctor`
222
- and `kb --help`, but do not initialize or modify a vault until I ask.
262
+ Install the `kb` Agent Skill from `hraness/kb#v0.18.0` with the standard skills
263
+ CLI. Use the skill's runtime instructions to install the exact
264
+ `@hraness/kb@0.18.0` registry release only when the command is missing. Verify it
265
+ with `kb doctor` and `kb --help`, but do not initialize or modify a vault until
266
+ I ask.
223
267
  ```
224
268
 
225
269
  Install the single public skill with either runner:
226
270
 
227
271
  ```sh
228
- npx skills add hraness/kb
229
- bunx skills add hraness/kb
272
+ npx skills add hraness/kb#v0.18.0
273
+ bunx skills add hraness/kb#v0.18.0
230
274
  ```
231
275
 
232
276
  Both commands discover the same `kb` skill and install it into the selected
233
277
  agent runner. Skill installation is inert: it does not initialize a vault,
234
278
  refresh a catalog, or edit Markdown. When invoked, the skill uses an existing
235
279
  `kb` command or, when the command is missing, checks for Bun and installs the
236
- CLI from the immutable `@hraness/kb@0.17.2` npm version.
280
+ CLI from the immutable `@hraness/kb@0.18.0` npm version.
237
281
 
238
282
  The public skills CLI reads `skills/kb/` from the repository. The immutable
239
- `0.17.2` npm package includes the same tree under
283
+ `0.18.0` npm package includes the same tree under
240
284
  `node_modules/@hraness/kb/skills/kb/`, and the package check verifies that the
241
285
  installed skill is byte-identical to the repository source.
242
286
 
243
287
  Install the two global commands with Bun:
244
288
 
245
289
  ```sh
246
- bun add --global @hraness/kb@0.17.2
290
+ bun add --global @hraness/kb@0.18.0
247
291
  kb --help
248
292
  kb-evaluation-builder --help
249
293
  ```
@@ -251,7 +295,7 @@ kb-evaluation-builder --help
251
295
  The same registry package can be installed with npm:
252
296
 
253
297
  ```sh
254
- npm install --global --ignore-scripts @hraness/kb@0.17.2
298
+ npm install --global --ignore-scripts @hraness/kb@0.18.0
255
299
  kb --help
256
300
  ```
257
301
 
@@ -264,7 +308,7 @@ reviewed and enabled; run `kb doctor` to inspect the resulting capabilities.
264
308
  For programmatic use, add the exact npm version to a Bun project:
265
309
 
266
310
  ```sh
267
- bun add --exact @hraness/kb@0.17.2
311
+ bun add --exact @hraness/kb@0.18.0
268
312
  ```
269
313
 
270
314
  The resulting dependency should remain exact:
@@ -272,12 +316,13 @@ The resulting dependency should remain exact:
272
316
  ```json
273
317
  {
274
318
  "dependencies": {
275
- "@hraness/kb": "0.17.2"
319
+ "@hraness/kb": "0.18.0"
276
320
  }
277
321
  }
278
322
  ```
279
323
 
280
- Version 0.17.2 retains two public GitHub dependencies:
324
+ Version 0.18.0 retains three public GitHub dependencies: `@hraness/oh` at
325
+ immutable release `v0.2.0` for closure verification,
281
326
  `@steipete/sweet-cookie` at Hraness release `v0.4.2` for the cookie-scope safety
282
327
  fork, and `@tobilu/qmd` at commit
283
328
  `aa993dceb3ef8cfb71d470554ca437570f5a2b3c` for store-local model behavior. A
@@ -540,9 +585,9 @@ a vault. The package smoke test keeps future tagged packages byte-identical to
540
585
  that source tree.
541
586
 
542
587
  ```sh
543
- npx skills add hraness/kb
588
+ npx skills add hraness/kb#v0.18.0
544
589
  # or
545
- bunx skills add hraness/kb
590
+ bunx skills add hraness/kb#v0.18.0
546
591
  ```
547
592
 
548
593
  The skill invokes the installed `kb` command without depending on a repository
package/dist/index.js CHANGED
@@ -245,6 +245,314 @@ import {
245
245
  wikiLinks
246
246
  } from "./index-cxfrakt7.js";
247
247
  import"./index-1xxnjn0d.js";
248
+ // src/oh-adoption.ts
249
+ import { createHash } from "crypto";
250
+ import { posix } from "path";
251
+ import { canonicalJson, canonicalSha256 } from "@hraness/oh";
252
+ import {
253
+ parseOhHeadV1,
254
+ parseOhStoreBindingV1,
255
+ verifyOhDependencyClosureAgainstV1
256
+ } from "@hraness/oh/store";
257
+ var CODE_PATTERN = /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u;
258
+ var RECORD_KEY_PATTERN = /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u;
259
+ var MAX_CAPSULE_BYTES = 16 * 1024 * 1024;
260
+ var MAX_RECORDS = 1024;
261
+ var MAX_ROOTS = 256;
262
+ var MAX_TEXT_BYTES = 4096;
263
+ var MAX_STRUCTURAL_NODES = 262144;
264
+ var MAX_STRUCTURAL_DEPTH = 128;
265
+ function isRecord(value) {
266
+ if (typeof value !== "object" || value === null || Array.isArray(value))
267
+ return false;
268
+ const prototype = Object.getPrototypeOf(value);
269
+ return prototype === Object.prototype || prototype === null;
270
+ }
271
+ function exactKeys(value, keys) {
272
+ const actual = Reflect.ownKeys(value);
273
+ return actual.length === keys.length && actual.every((key) => {
274
+ if (typeof key !== "string" || !keys.includes(key))
275
+ return false;
276
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
277
+ return descriptor !== undefined && descriptor.enumerable && "value" in descriptor;
278
+ });
279
+ }
280
+ function validUnicode(value) {
281
+ for (let index = 0;index < value.length; index += 1) {
282
+ const code = value.charCodeAt(index);
283
+ if (code >= 55296 && code <= 56319) {
284
+ const next = value.charCodeAt(index + 1);
285
+ if (next < 56320 || next > 57343)
286
+ return false;
287
+ index += 1;
288
+ } else if (code >= 56320 && code <= 57343)
289
+ return false;
290
+ }
291
+ return true;
292
+ }
293
+ function structurallyBounded(value) {
294
+ const pending = [[value, 0]];
295
+ const seen = new Set;
296
+ let nodes = 0;
297
+ let scalarBytes = 0;
298
+ while (pending.length > 0) {
299
+ const [candidate, depth] = pending.pop();
300
+ nodes += 1;
301
+ scalarBytes += 4;
302
+ if (nodes > MAX_STRUCTURAL_NODES || depth > MAX_STRUCTURAL_DEPTH || scalarBytes > MAX_CAPSULE_BYTES)
303
+ return false;
304
+ if (typeof candidate === "string") {
305
+ if (!validUnicode(candidate))
306
+ return false;
307
+ scalarBytes += Buffer.byteLength(candidate, "utf8");
308
+ if (scalarBytes > MAX_CAPSULE_BYTES)
309
+ return false;
310
+ }
311
+ if (typeof candidate !== "object" || candidate === null)
312
+ continue;
313
+ if (seen.has(candidate))
314
+ return false;
315
+ seen.add(candidate);
316
+ if (Array.isArray(candidate)) {
317
+ if (candidate.length > MAX_STRUCTURAL_NODES)
318
+ return false;
319
+ const keys = Reflect.ownKeys(candidate);
320
+ if (keys.some((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= candidate.length)))
321
+ return false;
322
+ for (let index = 0;index < candidate.length; index += 1) {
323
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, String(index));
324
+ if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor))
325
+ return false;
326
+ pending.push([descriptor.value, depth + 1]);
327
+ }
328
+ } else if (isRecord(candidate)) {
329
+ const keys = Reflect.ownKeys(candidate);
330
+ if (keys.length > MAX_STRUCTURAL_NODES || keys.some((key) => typeof key !== "string"))
331
+ return false;
332
+ for (const key of keys) {
333
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
334
+ if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor) || !validUnicode(key))
335
+ return false;
336
+ scalarBytes += Buffer.byteLength(key, "utf8");
337
+ if (scalarBytes > MAX_CAPSULE_BYTES)
338
+ return false;
339
+ pending.push([descriptor.value, depth + 1]);
340
+ }
341
+ } else
342
+ return false;
343
+ }
344
+ return true;
345
+ }
346
+ function immutableClone(value) {
347
+ if (Array.isArray(value)) {
348
+ return Object.freeze(value.map((item) => immutableClone(item)));
349
+ }
350
+ if (isRecord(value)) {
351
+ const clone = {};
352
+ for (const key of Object.keys(value))
353
+ clone[key] = immutableClone(value[key]);
354
+ return Object.freeze(clone);
355
+ }
356
+ return value;
357
+ }
358
+ function code(value, maximum = 256) {
359
+ return typeof value === "string" && value.length <= maximum && CODE_PATTERN.test(value) ? value : null;
360
+ }
361
+ function recordKey(value) {
362
+ return typeof value === "string" && value.length <= 512 && RECORD_KEY_PATTERN.test(value) ? value : null;
363
+ }
364
+ function orderedUnique(values) {
365
+ return values.every((value, index) => index === 0 || values[index - 1] < value);
366
+ }
367
+ function unsafeReviewCodePoint(codePoint) {
368
+ return codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint === 1564 || codePoint === 8206 || codePoint === 8207 || codePoint >= 8232 && codePoint <= 8238 || codePoint >= 8294 && codePoint <= 8297 || codePoint === 65279;
369
+ }
370
+ function singleLine(value) {
371
+ if (typeof value !== "string" || value.length < 1 || value.normalize("NFC") !== value || !validUnicode(value) || [...value].some((character) => unsafeReviewCodePoint(character.codePointAt(0) ?? 0)) || Buffer.byteLength(value, "utf8") > MAX_TEXT_BYTES)
372
+ return null;
373
+ return value;
374
+ }
375
+ function parseExpectedSource(value) {
376
+ if (!isRecord(value) || !exactKeys(value, ["authorityId", "binding", "head", "v"]) || value.v !== 1)
377
+ return null;
378
+ const authorityId = code(value.authorityId);
379
+ const binding = parseOhStoreBindingV1(value.binding);
380
+ const head = parseOhHeadV1(value.head);
381
+ return authorityId !== null && binding !== null && binding.profile.profileKind === "working" && head !== null ? immutableClone({ authorityId, binding, head, v: 1 }) : null;
382
+ }
383
+ function parseDestination(value) {
384
+ if (!isRecord(value) || !exactKeys(value, ["purpose", "targetPath", "v"]) || value.v !== 1)
385
+ return null;
386
+ const purpose = code(value.purpose);
387
+ if (purpose === null || typeof value.targetPath !== "string" || value.targetPath.length > 512 || value.targetPath.includes("\\") || value.targetPath.startsWith("/") || posix.normalize(value.targetPath) !== value.targetPath || !/^notes\/[a-z0-9][a-z0-9._/-]*\.md$/u.test(value.targetPath) || value.targetPath.split("/").some((segment) => segment === "." || segment === ".." || segment.startsWith("."))) {
388
+ return null;
389
+ }
390
+ return { purpose, targetPath: value.targetPath, v: 1 };
391
+ }
392
+ function parseRights(value, purpose) {
393
+ if (!isRecord(value) || !exactKeys(value, ["decisionId", "disposition", "purpose", "v"]) || value.v !== 1 || value.disposition !== "cleared-for-purpose" || value.purpose !== purpose)
394
+ return null;
395
+ const decisionId = code(value.decisionId);
396
+ return decisionId === null ? null : { decisionId, disposition: "cleared-for-purpose", purpose, v: 1 };
397
+ }
398
+ function parseReview(value) {
399
+ if (!isRecord(value) || !exactKeys(value, ["route", "status", "v"]) || value.v !== 1 || value.status !== "required")
400
+ return null;
401
+ const route = code(value.route);
402
+ return route === null ? null : { route, status: "required", v: 1 };
403
+ }
404
+ function parseConflicts(value) {
405
+ if (!isRecord(value) || !exactKeys(value, ["notes", "status", "v"]) || value.v !== 1 || value.status !== "none-observed" && value.status !== "requires-resolution" || !Array.isArray(value.notes) || value.notes.length < 1 || value.notes.length > 64)
406
+ return null;
407
+ const notes = value.notes.map(singleLine);
408
+ if (notes.some((note) => note === null))
409
+ return null;
410
+ const sorted = [...notes].sort();
411
+ return orderedUnique(sorted) ? { notes: sorted, status: value.status, v: 1 } : null;
412
+ }
413
+ function parseHostPolicy(value) {
414
+ if (!structurallyBounded(value) || !isRecord(value) || !exactKeys(value, ["conflicts", "destination", "expectedSource", "review", "rights", "v"]) || value.v !== 1)
415
+ return null;
416
+ const destination = parseDestination(value.destination);
417
+ const expectedSource = parseExpectedSource(value.expectedSource);
418
+ const conflicts = parseConflicts(value.conflicts);
419
+ const review = parseReview(value.review);
420
+ const rights = destination === null ? null : parseRights(value.rights, destination.purpose);
421
+ return destination !== null && expectedSource !== null && conflicts !== null && review !== null && rights !== null ? immutableClone({ conflicts, destination, expectedSource, review, rights, v: 1 }) : null;
422
+ }
423
+ function verifyCapsule(value, expectedSource) {
424
+ try {
425
+ if (!structurallyBounded(value) || !isRecord(value) || !exactKeys(value, ["binding", "closureSha256", "head", "records", "roots", "v"]) || value.v !== 1 || !Array.isArray(value.records) || !Array.isArray(value.roots) || value.records.length < 1 || value.records.length > MAX_RECORDS || value.roots.length < 1 || value.roots.length > MAX_ROOTS || Buffer.byteLength(canonicalJson(value), "utf8") > MAX_CAPSULE_BYTES)
426
+ return null;
427
+ const verified = verifyOhDependencyClosureAgainstV1(value, {
428
+ binding: expectedSource.binding,
429
+ head: expectedSource.head
430
+ });
431
+ return verified.ok && verified.closure.binding.profile.profileKind === "working" ? verified.closure : null;
432
+ } catch {
433
+ return null;
434
+ }
435
+ }
436
+ function parseDisclosures(value, keys) {
437
+ if (!Array.isArray(value) || value.length > 256)
438
+ return null;
439
+ const parsed = [];
440
+ for (const item of value) {
441
+ if (!isRecord(item) || !exactKeys(item, ["id", "recordKey", "summary", "v"]) || item.v !== 1)
442
+ return null;
443
+ const id = code(item.id);
444
+ const key = recordKey(item.recordKey);
445
+ const summary = singleLine(item.summary);
446
+ if (id === null || key === null || summary === null || !keys.has(key))
447
+ return null;
448
+ parsed.push({ id, recordKey: key, summary, v: 1 });
449
+ }
450
+ parsed.sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
451
+ return orderedUnique(parsed.map((item) => item.id)) ? parsed : null;
452
+ }
453
+ function markdownEscape(value) {
454
+ return value.replace(/[\\`*_{}\[\]<>()#+.!|>-]/gu, "\\$&");
455
+ }
456
+ function renderMarkdown(manifest, candidateSha256) {
457
+ const lines = [
458
+ "# Oh adoption candidate",
459
+ "",
460
+ `- Status: \`${manifest.status}\``,
461
+ `- Candidate: \`sha256:${candidateSha256}\``,
462
+ `- Destination: \`${manifest.destination.targetPath}\``,
463
+ `- Purpose: \`${manifest.destination.purpose}\``,
464
+ `- Source authority: \`${manifest.source.authorityId}\``,
465
+ `- Source binding: \`${manifest.source.binding.bindingSha256}\``,
466
+ `- Source head sequence: \`${manifest.source.head.sequence}\``,
467
+ `- Source head operation: \`${manifest.source.head.operationSha256 ?? "empty"}\``,
468
+ `- Source graph revision: \`${manifest.source.head.graphRevisionSha256 ?? "empty"}\``,
469
+ `- Source records digest: \`${manifest.source.head.recordsSha256}\``,
470
+ `- Closure: \`${manifest.source.closureSha256}\``,
471
+ "",
472
+ "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.",
473
+ "",
474
+ "## Required decisions",
475
+ "",
476
+ `- Rights: \`${manifest.rights.disposition}\` via \`${manifest.rights.decisionId}\` for \`${manifest.rights.purpose}\``,
477
+ `- Review: \`${manifest.review.status}\` via \`${manifest.review.route}\``,
478
+ `- Conflicts: \`${manifest.conflicts.status}\``,
479
+ ...manifest.conflicts.notes.map((note) => ` - ${markdownEscape(note)}`),
480
+ "",
481
+ "## Selected roots",
482
+ "",
483
+ ...manifest.source.roots.map((root) => `- \`${root}\``),
484
+ "",
485
+ "## Exact source records",
486
+ ""
487
+ ];
488
+ for (const record of manifest.source.records) {
489
+ lines.push(`### \`${record.key}\``, "", `- Kind: \`${record.kind}\``, `- Digest: \`${record.recordSha256}\``, `- Dependencies: ${record.dependencies.length === 0 ? "none" : record.dependencies.map((key) => `\`${key}\``).join(", ")}`, "");
490
+ }
491
+ lines.push("## Transformations", "", ...manifest.transformations.length === 0 ? ["- None declared."] : manifest.transformations.map((item) => `- \`${item.id}\` on \`${item.recordKey}\`: ${markdownEscape(item.summary)}`), "", "## Redactions", "", ...manifest.redactions.length === 0 ? ["- None declared."] : manifest.redactions.map((item) => `- \`${item.id}\` on \`${item.recordKey}\`: ${markdownEscape(item.summary)}`), "");
492
+ return `${lines.join(`
493
+ `)}
494
+ `;
495
+ }
496
+ function prepareWithPolicy(value, policy) {
497
+ if (!structurallyBounded(value) || !isRecord(value) || !exactKeys(value, ["capsule", "redactions", "transformations", "v"]) || value.v !== 1) {
498
+ throw new TypeError("Invalid Oh adoption preparation input.");
499
+ }
500
+ const capsule = verifyCapsule(value.capsule, policy.expectedSource);
501
+ if (capsule === null)
502
+ throw new TypeError("The source capsule is invalid for the bound authority and head.");
503
+ const recordKeys = new Set(capsule.records.map((record) => record.key));
504
+ const transformations = parseDisclosures(value.transformations, recordKeys);
505
+ const redactions = parseDisclosures(value.redactions, recordKeys);
506
+ const roots = new Set(capsule.roots);
507
+ if (transformations === null || redactions === null || capsule.records.filter((record) => roots.has(record.key)).every((record) => record.kind === "view")) {
508
+ throw new TypeError("Adoption requires valid disclosures and an authoritative root.");
509
+ }
510
+ const source = {
511
+ authorityId: policy.expectedSource.authorityId,
512
+ binding: { bindingSha256: capsule.binding.bindingSha256, v: 1 },
513
+ closureSha256: capsule.closureSha256,
514
+ head: capsule.head,
515
+ records: capsule.records.map((record) => ({
516
+ dependencies: record.dependencies,
517
+ key: record.key,
518
+ kind: record.kind,
519
+ recordSha256: record.recordSha256,
520
+ v: 1
521
+ })),
522
+ roots: capsule.roots,
523
+ v: 1
524
+ };
525
+ const manifest = {
526
+ conflicts: policy.conflicts,
527
+ destination: policy.destination,
528
+ format: "hraness.kb.oh-adoption-candidate.v1",
529
+ redactions,
530
+ review: policy.review,
531
+ rights: policy.rights,
532
+ source,
533
+ status: "prepared",
534
+ transformations,
535
+ v: 1
536
+ };
537
+ const candidateSha256 = canonicalSha256(manifest);
538
+ const markdown = renderMarkdown(manifest, candidateSha256);
539
+ if (Buffer.byteLength(markdown, "utf8") > MAX_CAPSULE_BYTES) {
540
+ throw new RangeError("The adoption candidate exceeds its Markdown byte limit.");
541
+ }
542
+ return immutableClone({
543
+ artifactSha256: createHash("sha256").update(markdown).digest("hex"),
544
+ candidateSha256,
545
+ manifest,
546
+ markdown,
547
+ v: 1
548
+ });
549
+ }
550
+ function createOhAdoptionPreparerV1(value) {
551
+ const policy = parseHostPolicy(value);
552
+ if (policy === null)
553
+ throw new TypeError("Invalid Oh adoption host policy.");
554
+ return Object.freeze({ prepare: (input) => prepareWithPolicy(input, policy) });
555
+ }
248
556
  export {
249
557
  workflowFromUnknown,
250
558
  wikiLinks,
@@ -330,6 +638,7 @@ export {
330
638
  createVerifiedEmbeddingModelLease,
331
639
  createSyntheticRankFusionFixture,
332
640
  createRepresentativeRetrievalFixture,
641
+ createOhAdoptionPreparerV1,
333
642
  createNote,
334
643
  createConceptNote,
335
644
  compareAgentGuideAudits,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/kb",
3
- "version": "0.17.2",
3
+ "version": "0.18.0",
4
4
  "description": "A knowledge base for coding agents, built from Markdown, backlinks, semantic search, and Git context.",
5
5
  "license": "MIT",
6
6
  "contentPolicy": {
@@ -335,6 +335,7 @@
335
335
  "src/init.ts",
336
336
  "src/navigation.ts",
337
337
  "src/note-lock.ts",
338
+ "src/oh-adoption.ts",
338
339
  "src/percolate.ts",
339
340
  "src/portfolio.ts",
340
341
  "src/portfolio-audit.ts",
@@ -391,6 +392,7 @@
391
392
  "prepack": "bun run check"
392
393
  },
393
394
  "dependencies": {
395
+ "@hraness/oh": "github:hraness/oh#v0.2.0",
394
396
  "@steipete/sweet-cookie": "github:hraness/sweet-cookie#v0.4.2",
395
397
  "@tobilu/qmd": "git+https://github.com/hraness/qmd.git#aa993dceb3ef8cfb71d470554ca437570f5a2b3c",
396
398
  "agent-browser": "0.32.3",
@@ -31,7 +31,7 @@ missing:
31
31
  ```sh
32
32
  command -v kb >/dev/null 2>&1 || {
33
33
  command -v bun >/dev/null 2>&1 || exit 1
34
- bun add --global @hraness/kb@0.17.2
34
+ bun add --global @hraness/kb@0.18.0
35
35
  }
36
36
  kb --help
37
37
  ```
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export * from "./git.js";
9
9
  export * from "./graph.js";
10
10
  export * from "./init.js";
11
11
  export * from "./navigation.js";
12
+ export * from "./oh-adoption.js";
12
13
  export * from "./percolate.js";
13
14
  export * from "./query.js";
14
15
  export * from "./repository-memory.js";
@@ -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
+ }