@hraness/kb 0.17.3 → 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 +27 -14
- package/dist/index.js +309 -0
- package/package.json +3 -1
- package/skills/kb/SKILL.md +1 -1
- package/src/index.ts +1 -0
- package/src/oh-adoption.ts +444 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ system.
|
|
|
14
14
|
Bun 1.3.14 or newer is required.
|
|
15
15
|
|
|
16
16
|
```sh
|
|
17
|
-
bun add --global @hraness/kb@0.
|
|
17
|
+
bun add --global @hraness/kb@0.18.0
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
## Why kb
|
|
@@ -193,6 +193,18 @@ Start with a short inherited `AGENTS.md` path for rules whose omission would mak
|
|
|
193
193
|
|
|
194
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.
|
|
195
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
|
+
|
|
196
208
|
## Upgrade to v0.17.3
|
|
197
209
|
|
|
198
210
|
Version 0.17.3 restructures the README around an inspectable first task,
|
|
@@ -247,9 +259,9 @@ audit projection disposable.
|
|
|
247
259
|
Copy this prompt into Codex, Claude Code, or another coding agent:
|
|
248
260
|
|
|
249
261
|
```text
|
|
250
|
-
Install the `kb` Agent Skill from `hraness/kb#v0.
|
|
262
|
+
Install the `kb` Agent Skill from `hraness/kb#v0.18.0` with the standard skills
|
|
251
263
|
CLI. Use the skill's runtime instructions to install the exact
|
|
252
|
-
`@hraness/kb@0.
|
|
264
|
+
`@hraness/kb@0.18.0` registry release only when the command is missing. Verify it
|
|
253
265
|
with `kb doctor` and `kb --help`, but do not initialize or modify a vault until
|
|
254
266
|
I ask.
|
|
255
267
|
```
|
|
@@ -257,25 +269,25 @@ I ask.
|
|
|
257
269
|
Install the single public skill with either runner:
|
|
258
270
|
|
|
259
271
|
```sh
|
|
260
|
-
npx skills add hraness/kb#v0.
|
|
261
|
-
bunx skills add hraness/kb#v0.
|
|
272
|
+
npx skills add hraness/kb#v0.18.0
|
|
273
|
+
bunx skills add hraness/kb#v0.18.0
|
|
262
274
|
```
|
|
263
275
|
|
|
264
276
|
Both commands discover the same `kb` skill and install it into the selected
|
|
265
277
|
agent runner. Skill installation is inert: it does not initialize a vault,
|
|
266
278
|
refresh a catalog, or edit Markdown. When invoked, the skill uses an existing
|
|
267
279
|
`kb` command or, when the command is missing, checks for Bun and installs the
|
|
268
|
-
CLI from the immutable `@hraness/kb@0.
|
|
280
|
+
CLI from the immutable `@hraness/kb@0.18.0` npm version.
|
|
269
281
|
|
|
270
282
|
The public skills CLI reads `skills/kb/` from the repository. The immutable
|
|
271
|
-
`0.
|
|
283
|
+
`0.18.0` npm package includes the same tree under
|
|
272
284
|
`node_modules/@hraness/kb/skills/kb/`, and the package check verifies that the
|
|
273
285
|
installed skill is byte-identical to the repository source.
|
|
274
286
|
|
|
275
287
|
Install the two global commands with Bun:
|
|
276
288
|
|
|
277
289
|
```sh
|
|
278
|
-
bun add --global @hraness/kb@0.
|
|
290
|
+
bun add --global @hraness/kb@0.18.0
|
|
279
291
|
kb --help
|
|
280
292
|
kb-evaluation-builder --help
|
|
281
293
|
```
|
|
@@ -283,7 +295,7 @@ kb-evaluation-builder --help
|
|
|
283
295
|
The same registry package can be installed with npm:
|
|
284
296
|
|
|
285
297
|
```sh
|
|
286
|
-
npm install --global --ignore-scripts @hraness/kb@0.
|
|
298
|
+
npm install --global --ignore-scripts @hraness/kb@0.18.0
|
|
287
299
|
kb --help
|
|
288
300
|
```
|
|
289
301
|
|
|
@@ -296,7 +308,7 @@ reviewed and enabled; run `kb doctor` to inspect the resulting capabilities.
|
|
|
296
308
|
For programmatic use, add the exact npm version to a Bun project:
|
|
297
309
|
|
|
298
310
|
```sh
|
|
299
|
-
bun add --exact @hraness/kb@0.
|
|
311
|
+
bun add --exact @hraness/kb@0.18.0
|
|
300
312
|
```
|
|
301
313
|
|
|
302
314
|
The resulting dependency should remain exact:
|
|
@@ -304,12 +316,13 @@ The resulting dependency should remain exact:
|
|
|
304
316
|
```json
|
|
305
317
|
{
|
|
306
318
|
"dependencies": {
|
|
307
|
-
"@hraness/kb": "0.
|
|
319
|
+
"@hraness/kb": "0.18.0"
|
|
308
320
|
}
|
|
309
321
|
}
|
|
310
322
|
```
|
|
311
323
|
|
|
312
|
-
Version 0.
|
|
324
|
+
Version 0.18.0 retains three public GitHub dependencies: `@hraness/oh` at
|
|
325
|
+
immutable release `v0.2.0` for closure verification,
|
|
313
326
|
`@steipete/sweet-cookie` at Hraness release `v0.4.2` for the cookie-scope safety
|
|
314
327
|
fork, and `@tobilu/qmd` at commit
|
|
315
328
|
`aa993dceb3ef8cfb71d470554ca437570f5a2b3c` for store-local model behavior. A
|
|
@@ -572,9 +585,9 @@ a vault. The package smoke test keeps future tagged packages byte-identical to
|
|
|
572
585
|
that source tree.
|
|
573
586
|
|
|
574
587
|
```sh
|
|
575
|
-
npx skills add hraness/kb#v0.
|
|
588
|
+
npx skills add hraness/kb#v0.18.0
|
|
576
589
|
# or
|
|
577
|
-
bunx skills add hraness/kb#v0.
|
|
590
|
+
bunx skills add hraness/kb#v0.18.0
|
|
578
591
|
```
|
|
579
592
|
|
|
580
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.
|
|
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",
|
package/skills/kb/SKILL.md
CHANGED
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
|
+
}
|