@rhizomes/rhizomatic 0.1.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/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +54 -0
- package/dist/alias.d.ts +4 -0
- package/dist/alias.d.ts.map +1 -0
- package/dist/alias.js +34 -0
- package/dist/alias.js.map +1 -0
- package/dist/cbor.d.ts +24 -0
- package/dist/cbor.d.ts.map +1 -0
- package/dist/cbor.js +267 -0
- package/dist/cbor.js.map +1 -0
- package/dist/delta.d.ts +8 -0
- package/dist/delta.d.ts.map +1 -0
- package/dist/delta.js +92 -0
- package/dist/delta.js.map +1 -0
- package/dist/derivation.d.ts +29 -0
- package/dist/derivation.d.ts.map +1 -0
- package/dist/derivation.js +183 -0
- package/dist/derivation.js.map +1 -0
- package/dist/eval.d.ts +91 -0
- package/dist/eval.d.ts.map +1 -0
- package/dist/eval.js +318 -0
- package/dist/eval.js.map +1 -0
- package/dist/hash.d.ts +4 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +17 -0
- package/dist/hash.js.map +1 -0
- package/dist/http.d.ts +21 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +110 -0
- package/dist/http.js.map +1 -0
- package/dist/hview.d.ts +15 -0
- package/dist/hview.d.ts.map +1 -0
- package/dist/hview.js +72 -0
- package/dist/hview.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/json-profile.d.ts +4 -0
- package/dist/json-profile.d.ts.map +1 -0
- package/dist/json-profile.js +97 -0
- package/dist/json-profile.js.map +1 -0
- package/dist/pack.d.ts +5 -0
- package/dist/pack.d.ts.map +1 -0
- package/dist/pack.js +227 -0
- package/dist/pack.js.map +1 -0
- package/dist/peer.d.ts +26 -0
- package/dist/peer.d.ts.map +1 -0
- package/dist/peer.js +111 -0
- package/dist/peer.js.map +1 -0
- package/dist/policy.d.ts +46 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +186 -0
- package/dist/policy.js.map +1 -0
- package/dist/pred.d.ts +78 -0
- package/dist/pred.d.ts.map +1 -0
- package/dist/pred.js +228 -0
- package/dist/pred.js.map +1 -0
- package/dist/reactor.d.ts +67 -0
- package/dist/reactor.d.ts.map +1 -0
- package/dist/reactor.js +433 -0
- package/dist/reactor.js.map +1 -0
- package/dist/schema-deltas.d.ts +14 -0
- package/dist/schema-deltas.d.ts.map +1 -0
- package/dist/schema-deltas.js +87 -0
- package/dist/schema-deltas.js.map +1 -0
- package/dist/schema.d.ts +17 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +102 -0
- package/dist/schema.js.map +1 -0
- package/dist/set.d.ts +18 -0
- package/dist/set.d.ts.map +1 -0
- package/dist/set.js +83 -0
- package/dist/set.js.map +1 -0
- package/dist/sign.d.ts +8 -0
- package/dist/sign.d.ts.map +1 -0
- package/dist/sign.js +44 -0
- package/dist/sign.js.map +1 -0
- package/dist/term-io.d.ts +13 -0
- package/dist/term-io.d.ts.map +1 -0
- package/dist/term-io.js +216 -0
- package/dist/term-io.js.map +1 -0
- package/dist/term-json.d.ts +7 -0
- package/dist/term-json.d.ts.map +1 -0
- package/dist/term-json.js +362 -0
- package/dist/term-json.js.map +1 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/vocab.d.ts +2 -0
- package/dist/vocab.d.ts.map +1 -0
- package/dist/vocab.js +4 -0
- package/dist/vocab.js.map +1 -0
- package/package.json +83 -0
- package/src/alias.ts +36 -0
- package/src/cbor.ts +280 -0
- package/src/delta.ts +89 -0
- package/src/derivation.ts +229 -0
- package/src/eval.ts +401 -0
- package/src/hash.ts +19 -0
- package/src/http.ts +124 -0
- package/src/hview.ts +91 -0
- package/src/index.ts +83 -0
- package/src/json-profile.ts +96 -0
- package/src/pack.ts +239 -0
- package/src/peer.ts +126 -0
- package/src/policy.ts +216 -0
- package/src/pred.ts +307 -0
- package/src/reactor.ts +490 -0
- package/src/schema-deltas.ts +100 -0
- package/src/schema.ts +111 -0
- package/src/set.ts +98 -0
- package/src/sign.ts +48 -0
- package/src/term-io.ts +228 -0
- package/src/term-json.ts +364 -0
- package/src/types.ts +38 -0
- package/src/vocab.ts +3 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type { Primitive, EntityRef, DeltaRef, Target, Pointer, Claims, Delta } from "./types.js";
|
|
2
|
+
export { encode, type CborValue, tstr, float, bool, array, map } from "./cbor.js";
|
|
3
|
+
export { contentAddress } from "./hash.js";
|
|
4
|
+
export {
|
|
5
|
+
claimsToCbor,
|
|
6
|
+
canonicalBytes,
|
|
7
|
+
canonicalHex,
|
|
8
|
+
computeId,
|
|
9
|
+
assertValidClaims,
|
|
10
|
+
} from "./delta.js";
|
|
11
|
+
export { claimsToJson, parseClaims } from "./json-profile.js";
|
|
12
|
+
export {
|
|
13
|
+
AUTHOR_PREFIX,
|
|
14
|
+
authorForSeed,
|
|
15
|
+
publicKeyFromSeed,
|
|
16
|
+
signClaims,
|
|
17
|
+
verifyDelta,
|
|
18
|
+
type Verification,
|
|
19
|
+
} from "./sign.js";
|
|
20
|
+
export { DeltaSet, federate, fork, makeDelta, makeNegationClaims, merge } from "./set.js";
|
|
21
|
+
export {
|
|
22
|
+
comparePrimitives,
|
|
23
|
+
evalPred,
|
|
24
|
+
strMatch,
|
|
25
|
+
type Cmp,
|
|
26
|
+
type PPred,
|
|
27
|
+
type Pred,
|
|
28
|
+
type StrMatch,
|
|
29
|
+
type ValMatch,
|
|
30
|
+
} from "./pred.js";
|
|
31
|
+
export {
|
|
32
|
+
aliasClosure,
|
|
33
|
+
evalTerm,
|
|
34
|
+
expandAliased,
|
|
35
|
+
resultCanonicalHex,
|
|
36
|
+
type AliasedSpec,
|
|
37
|
+
type EvalResult,
|
|
38
|
+
type GroupKey,
|
|
39
|
+
type MaskPolicy,
|
|
40
|
+
type Term,
|
|
41
|
+
} from "./eval.js";
|
|
42
|
+
export { relationSignature, relationSignatureCanonicalHex } from "./alias.js";
|
|
43
|
+
export { hviewCanonicalHex, type HVEntry, type HView } from "./hview.js";
|
|
44
|
+
export { SchemaRegistry, collectRefs, type HyperSchema } from "./schema.js";
|
|
45
|
+
export {
|
|
46
|
+
resolveView,
|
|
47
|
+
viewCanonicalHex,
|
|
48
|
+
type MergeFn,
|
|
49
|
+
type Order,
|
|
50
|
+
type Policy,
|
|
51
|
+
type PropPolicy,
|
|
52
|
+
type View,
|
|
53
|
+
} from "./policy.js";
|
|
54
|
+
export { parsePolicy, parsePred, parseTerm } from "./term-json.js";
|
|
55
|
+
export {
|
|
56
|
+
cborToJson,
|
|
57
|
+
jsonToCbor,
|
|
58
|
+
policyToJson,
|
|
59
|
+
predToJson,
|
|
60
|
+
termCanonicalHex,
|
|
61
|
+
termHash,
|
|
62
|
+
termToJson,
|
|
63
|
+
} from "./term-io.js";
|
|
64
|
+
export { SCHEMA_SCHEMA, VOCAB_PREFIX, loadSchema, publishSchemaClaims } from "./schema-deltas.js";
|
|
65
|
+
export { decode } from "./cbor.js";
|
|
66
|
+
export { packId, packSet, unpackSet } from "./pack.js";
|
|
67
|
+
export { Peer, syncBoth, type SyncReport } from "./peer.js";
|
|
68
|
+
export { offerFor, pullFromUrl, servePeer } from "./http.js";
|
|
69
|
+
export {
|
|
70
|
+
DerivationHost,
|
|
71
|
+
derivedClaims,
|
|
72
|
+
verifyPureDerivation,
|
|
73
|
+
type BindingSpec,
|
|
74
|
+
type DerivedFn,
|
|
75
|
+
} from "./derivation.js";
|
|
76
|
+
export {
|
|
77
|
+
Reactor,
|
|
78
|
+
isRootAnchored,
|
|
79
|
+
makeManifestClaims,
|
|
80
|
+
manifestMemberIds,
|
|
81
|
+
type IngestResult,
|
|
82
|
+
type MaterializationChange,
|
|
83
|
+
} from "./reactor.js";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Parse the JSON debug profile used by the vectors (SPEC-1 §4.1, ERRATA "JSON debug profile")
|
|
2
|
+
// into the logical delta model. The CBOR form is normative; this is for authoring/inspection.
|
|
3
|
+
|
|
4
|
+
import type { Claims, Pointer, Primitive, Target } from "./types.js";
|
|
5
|
+
|
|
6
|
+
function asObject(x: unknown, what: string): Record<string, unknown> {
|
|
7
|
+
if (typeof x !== "object" || x === null || Array.isArray(x)) {
|
|
8
|
+
throw new Error(`expected object for ${what}`);
|
|
9
|
+
}
|
|
10
|
+
return x as Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parsePrimitive(v: unknown): Primitive {
|
|
14
|
+
if (typeof v === "string" || typeof v === "boolean") return v;
|
|
15
|
+
if (typeof v === "number") {
|
|
16
|
+
if (!Number.isFinite(v)) throw new Error("numeric primitive must be finite");
|
|
17
|
+
return v;
|
|
18
|
+
}
|
|
19
|
+
throw new Error("primitive must be string | number | boolean");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The profile mirrors the canonical CBOR exactly: a primitive target is the bare value; an
|
|
23
|
+
// entity ref is {id, context?}; a delta ref is {delta, context?}. Discrimination is structural
|
|
24
|
+
// (SPEC-1 §2.1) — primitives are never objects, and the id/delta key names the ref kind.
|
|
25
|
+
function parseTarget(raw: unknown): Target {
|
|
26
|
+
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
|
|
27
|
+
return { kind: "primitive", value: parsePrimitive(raw) };
|
|
28
|
+
}
|
|
29
|
+
const o = asObject(raw, "target");
|
|
30
|
+
if ("id" in o) {
|
|
31
|
+
const id = o["id"];
|
|
32
|
+
if (typeof id !== "string") throw new Error("entity ref id must be a string");
|
|
33
|
+
const context = o["context"];
|
|
34
|
+
return context === undefined
|
|
35
|
+
? { kind: "entity", entity: { id } }
|
|
36
|
+
: { kind: "entity", entity: { id, context: String(context) } };
|
|
37
|
+
}
|
|
38
|
+
if ("delta" in o) {
|
|
39
|
+
const delta = o["delta"];
|
|
40
|
+
if (typeof delta !== "string") throw new Error("delta ref delta must be a string");
|
|
41
|
+
const context = o["context"];
|
|
42
|
+
return context === undefined
|
|
43
|
+
? { kind: "delta", deltaRef: { delta } }
|
|
44
|
+
: { kind: "delta", deltaRef: { delta, context: String(context) } };
|
|
45
|
+
}
|
|
46
|
+
throw new Error("target must be a primitive, {id, context?}, or {delta, context?}");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parsePointer(raw: unknown): Pointer {
|
|
50
|
+
const o = asObject(raw, "pointer");
|
|
51
|
+
if (typeof o["role"] !== "string") throw new Error("pointer.role must be a string");
|
|
52
|
+
return { role: o["role"], target: parseTarget(o["target"]) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Serialize claims back to the JSON debug profile (the inverse of parseClaims).
|
|
56
|
+
export function claimsToJson(claims: Claims): unknown {
|
|
57
|
+
return {
|
|
58
|
+
timestamp: claims.timestamp,
|
|
59
|
+
author: claims.author,
|
|
60
|
+
pointers: claims.pointers.map((p) => {
|
|
61
|
+
let target: unknown;
|
|
62
|
+
switch (p.target.kind) {
|
|
63
|
+
case "primitive":
|
|
64
|
+
target = p.target.value;
|
|
65
|
+
break;
|
|
66
|
+
case "entity":
|
|
67
|
+
target = {
|
|
68
|
+
id: p.target.entity.id,
|
|
69
|
+
...(p.target.entity.context === undefined ? {} : { context: p.target.entity.context }),
|
|
70
|
+
};
|
|
71
|
+
break;
|
|
72
|
+
case "delta":
|
|
73
|
+
target = {
|
|
74
|
+
delta: p.target.deltaRef.delta,
|
|
75
|
+
...(p.target.deltaRef.context === undefined
|
|
76
|
+
? {}
|
|
77
|
+
: { context: p.target.deltaRef.context }),
|
|
78
|
+
};
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
return { role: p.role, target };
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function parseClaims(raw: unknown): Claims {
|
|
87
|
+
const o = asObject(raw, "claims");
|
|
88
|
+
if (typeof o["timestamp"] !== "number") throw new Error("claims.timestamp must be a number");
|
|
89
|
+
if (typeof o["author"] !== "string") throw new Error("claims.author must be a string");
|
|
90
|
+
if (!Array.isArray(o["pointers"])) throw new Error("claims.pointers must be an array");
|
|
91
|
+
return {
|
|
92
|
+
timestamp: o["timestamp"],
|
|
93
|
+
author: o["author"],
|
|
94
|
+
pointers: o["pointers"].map(parsePointer),
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/pack.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// The pack format (SPEC-8, ERRATA-8): a content-addressed physical container whose logical form
|
|
2
|
+
// is invariant. Never hash the dehydrated form; rehydration is byte-exact by construction because
|
|
3
|
+
// unpacking rebuilds claims through the standard makeDelta path.
|
|
4
|
+
|
|
5
|
+
import { array, bool, decode, encode, float, map, tstr, type CborValue } from "./cbor.js";
|
|
6
|
+
import { contentAddress } from "./hash.js";
|
|
7
|
+
import { manifestMemberIds } from "./reactor.js";
|
|
8
|
+
import { DeltaSet, makeDelta } from "./set.js";
|
|
9
|
+
import type { Claims, Delta, Pointer, Target } from "./types.js";
|
|
10
|
+
|
|
11
|
+
const PACK_VERSION = 1;
|
|
12
|
+
|
|
13
|
+
// --- string interning -------------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
function stringsOf(delta: Delta, out: Set<string>): void {
|
|
16
|
+
out.add(delta.id); // stored ids make rehydration self-verifying (SPEC-8 §4)
|
|
17
|
+
out.add(delta.claims.author);
|
|
18
|
+
if (delta.sig !== undefined) out.add(delta.sig);
|
|
19
|
+
for (const p of delta.claims.pointers) {
|
|
20
|
+
out.add(p.role);
|
|
21
|
+
switch (p.target.kind) {
|
|
22
|
+
case "entity":
|
|
23
|
+
out.add(p.target.entity.id);
|
|
24
|
+
if (p.target.entity.context !== undefined) out.add(p.target.entity.context);
|
|
25
|
+
break;
|
|
26
|
+
case "delta":
|
|
27
|
+
out.add(p.target.deltaRef.delta);
|
|
28
|
+
if (p.target.deltaRef.context !== undefined) out.add(p.target.deltaRef.context);
|
|
29
|
+
break;
|
|
30
|
+
case "primitive":
|
|
31
|
+
if (typeof p.target.value === "string") out.add(p.target.value);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- packing ----------------------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
function ptrToCbor(p: Pointer, idx: (s: string) => number): CborValue {
|
|
40
|
+
const entries: Array<[string, CborValue]> = [["r", float(idx(p.role))]];
|
|
41
|
+
let context: string | undefined;
|
|
42
|
+
switch (p.target.kind) {
|
|
43
|
+
case "entity":
|
|
44
|
+
entries.push(["e", float(idx(p.target.entity.id))]);
|
|
45
|
+
context = p.target.entity.context;
|
|
46
|
+
break;
|
|
47
|
+
case "delta":
|
|
48
|
+
entries.push(["d", float(idx(p.target.deltaRef.delta))]);
|
|
49
|
+
context = p.target.deltaRef.context;
|
|
50
|
+
break;
|
|
51
|
+
case "primitive": {
|
|
52
|
+
const v = p.target.value;
|
|
53
|
+
if (typeof v === "string") entries.push(["s", float(idx(v))]);
|
|
54
|
+
else if (typeof v === "number") entries.push(["n", float(v)]);
|
|
55
|
+
else entries.push(["b", bool(v)]);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (context !== undefined) entries.push(["c", float(idx(context))]);
|
|
60
|
+
return map(entries);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hydratedRecord(d: Delta, idx: (s: string) => number): CborValue {
|
|
64
|
+
const entries: Array<[string, CborValue]> = [
|
|
65
|
+
["i", float(idx(d.id))],
|
|
66
|
+
["a", float(idx(d.claims.author))],
|
|
67
|
+
["t", float(d.claims.timestamp)],
|
|
68
|
+
["p", array(d.claims.pointers.map((p) => ptrToCbor(p, idx)))],
|
|
69
|
+
];
|
|
70
|
+
if (d.sig !== undefined) entries.push(["s", float(idx(d.sig))]);
|
|
71
|
+
return map(entries);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function memberRecord(
|
|
75
|
+
d: Delta,
|
|
76
|
+
manifest: Delta,
|
|
77
|
+
envelopeIdx: number,
|
|
78
|
+
idx: (s: string) => number,
|
|
79
|
+
): CborValue {
|
|
80
|
+
const entries: Array<[string, CborValue]> = [
|
|
81
|
+
["i", float(idx(d.id))],
|
|
82
|
+
["m", float(envelopeIdx)],
|
|
83
|
+
["p", array(d.claims.pointers.map((p) => ptrToCbor(p, idx)))],
|
|
84
|
+
];
|
|
85
|
+
// Dehydrate against the envelope (SPEC-8 §3.1); divergent fields stored explicitly (P2).
|
|
86
|
+
if (d.claims.author !== manifest.claims.author) entries.push(["a", float(idx(d.claims.author))]);
|
|
87
|
+
const dt = d.claims.timestamp - manifest.claims.timestamp;
|
|
88
|
+
if (dt !== 0) entries.push(["dt", float(dt)]);
|
|
89
|
+
if (d.sig !== undefined) entries.push(["s", float(idx(d.sig))]);
|
|
90
|
+
return map(entries);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function packSet(set: DeltaSet): Uint8Array {
|
|
94
|
+
const deltas = [...set].sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
95
|
+
// Manifests: deltas carrying rdb.txn.member pointers, sorted by id.
|
|
96
|
+
const manifests = deltas.filter((d) => manifestMemberIds(d).length > 0);
|
|
97
|
+
// Each member is dehydrated against the lexicographically FIRST claiming manifest (P1).
|
|
98
|
+
const memberToManifest = new Map<string, number>();
|
|
99
|
+
manifests.forEach((m, i) => {
|
|
100
|
+
for (const id of manifestMemberIds(m)) {
|
|
101
|
+
if (set.has(id) && !memberToManifest.has(id)) memberToManifest.set(id, i);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const manifestIds = new Set(manifests.map((m) => m.id));
|
|
105
|
+
const members = deltas.filter((d) => memberToManifest.has(d.id) && !manifestIds.has(d.id));
|
|
106
|
+
const loose = deltas.filter((d) => !memberToManifest.has(d.id) && !manifestIds.has(d.id));
|
|
107
|
+
|
|
108
|
+
const stringSet = new Set<string>();
|
|
109
|
+
for (const d of deltas) stringsOf(d, stringSet);
|
|
110
|
+
const strings = [...stringSet].sort();
|
|
111
|
+
const indexOf = new Map(strings.map((s, i) => [s, i]));
|
|
112
|
+
const idx = (s: string): number => indexOf.get(s)!;
|
|
113
|
+
|
|
114
|
+
const packed = map([
|
|
115
|
+
["version", float(PACK_VERSION)],
|
|
116
|
+
["strings", array(strings.map(tstr))],
|
|
117
|
+
["envelopes", array(manifests.map((m) => hydratedRecord(m, idx)))],
|
|
118
|
+
[
|
|
119
|
+
"members",
|
|
120
|
+
array(
|
|
121
|
+
members.map((d) =>
|
|
122
|
+
memberRecord(
|
|
123
|
+
d,
|
|
124
|
+
manifests[memberToManifest.get(d.id)!]!,
|
|
125
|
+
memberToManifest.get(d.id)!,
|
|
126
|
+
idx,
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
],
|
|
131
|
+
["loose", array(loose.map((d) => hydratedRecord(d, idx)))],
|
|
132
|
+
]);
|
|
133
|
+
return encode(packed);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function packId(bytes: Uint8Array): string {
|
|
137
|
+
return contentAddress(bytes);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- unpacking --------------------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
type Obj = Map<string, CborValue>;
|
|
143
|
+
|
|
144
|
+
function asMap(v: CborValue, what: string): Obj {
|
|
145
|
+
if (v.t !== "map") throw new Error(`pack: expected map for ${what}`);
|
|
146
|
+
return new Map(v.v);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function asArray(v: CborValue | undefined, what: string): readonly CborValue[] {
|
|
150
|
+
if (v === undefined || v.t !== "array") throw new Error(`pack: expected array for ${what}`);
|
|
151
|
+
return v.v;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function asNum(v: CborValue | undefined, what: string): number {
|
|
155
|
+
if (v === undefined || v.t !== "float") throw new Error(`pack: expected number for ${what}`);
|
|
156
|
+
return v.v;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function ptrFromCbor(v: CborValue, strings: readonly string[]): Pointer {
|
|
160
|
+
const o = asMap(v, "pointer");
|
|
161
|
+
const str = (key: string): string => strings[asNum(o.get(key), key)]!;
|
|
162
|
+
const role = str("r");
|
|
163
|
+
const context = o.has("c") ? str("c") : undefined;
|
|
164
|
+
let target: Target;
|
|
165
|
+
if (o.has("e")) {
|
|
166
|
+
target = {
|
|
167
|
+
kind: "entity",
|
|
168
|
+
entity: context === undefined ? { id: str("e") } : { id: str("e"), context },
|
|
169
|
+
};
|
|
170
|
+
} else if (o.has("d")) {
|
|
171
|
+
target = {
|
|
172
|
+
kind: "delta",
|
|
173
|
+
deltaRef: context === undefined ? { delta: str("d") } : { delta: str("d"), context },
|
|
174
|
+
};
|
|
175
|
+
} else if (o.has("s")) {
|
|
176
|
+
target = { kind: "primitive", value: str("s") };
|
|
177
|
+
} else if (o.has("n")) {
|
|
178
|
+
target = { kind: "primitive", value: asNum(o.get("n"), "n") };
|
|
179
|
+
} else if (o.has("b")) {
|
|
180
|
+
const b = o.get("b")!;
|
|
181
|
+
if (b.t !== "bool") throw new Error("pack: expected bool for b");
|
|
182
|
+
target = { kind: "primitive", value: b.v };
|
|
183
|
+
} else {
|
|
184
|
+
throw new Error("pack: pointer record has no target");
|
|
185
|
+
}
|
|
186
|
+
return { role, target };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function hydrateRecord(v: CborValue, strings: readonly string[]): Delta {
|
|
190
|
+
const o = asMap(v, "record");
|
|
191
|
+
const claims: Claims = {
|
|
192
|
+
author: strings[asNum(o.get("a"), "a")]!,
|
|
193
|
+
timestamp: asNum(o.get("t"), "t"),
|
|
194
|
+
pointers: asArray(o.get("p"), "p").map((p) => ptrFromCbor(p, strings)),
|
|
195
|
+
};
|
|
196
|
+
const sig = o.has("s") ? strings[asNum(o.get("s"), "s")]! : undefined;
|
|
197
|
+
return verifiedDelta(claims, sig, strings[asNum(o.get("i"), "i")]!);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// SPEC-8 §4: hydrate -> canonical CBOR -> multihash MUST equal the stored id. Free fsck.
|
|
201
|
+
function verifiedDelta(claims: Claims, sig: string | undefined, storedId: string): Delta {
|
|
202
|
+
const d = makeDelta(claims, sig);
|
|
203
|
+
if (d.id !== storedId) {
|
|
204
|
+
throw new Error(`pack: rehydrated delta ${d.id} does not match stored id ${storedId}`);
|
|
205
|
+
}
|
|
206
|
+
return d;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function unpackSet(bytes: Uint8Array): DeltaSet {
|
|
210
|
+
const top = asMap(decode(bytes), "pack");
|
|
211
|
+
if (asNum(top.get("version"), "version") !== PACK_VERSION) {
|
|
212
|
+
throw new Error("pack: unsupported version");
|
|
213
|
+
}
|
|
214
|
+
const strings = asArray(top.get("strings"), "strings").map((s) => {
|
|
215
|
+
if (s.t !== "tstr") throw new Error("pack: string table entries must be text");
|
|
216
|
+
return s.v;
|
|
217
|
+
});
|
|
218
|
+
const out = new DeltaSet();
|
|
219
|
+
const envelopes = asArray(top.get("envelopes"), "envelopes").map((e) =>
|
|
220
|
+
hydrateRecord(e, strings),
|
|
221
|
+
);
|
|
222
|
+
for (const m of envelopes) out.add(m);
|
|
223
|
+
for (const rec of asArray(top.get("members"), "members")) {
|
|
224
|
+
const o = asMap(rec, "member");
|
|
225
|
+
const manifest = envelopes[asNum(o.get("m"), "m")];
|
|
226
|
+
if (manifest === undefined) throw new Error("pack: member references missing envelope");
|
|
227
|
+
const author = o.has("a") ? strings[asNum(o.get("a"), "a")]! : manifest.claims.author;
|
|
228
|
+
const timestamp = manifest.claims.timestamp + (o.has("dt") ? asNum(o.get("dt"), "dt") : 0);
|
|
229
|
+
const claims: Claims = {
|
|
230
|
+
author,
|
|
231
|
+
timestamp,
|
|
232
|
+
pointers: asArray(o.get("p"), "p").map((p) => ptrFromCbor(p, strings)),
|
|
233
|
+
};
|
|
234
|
+
const sig = o.has("s") ? strings[asNum(o.get("s"), "s")]! : undefined;
|
|
235
|
+
out.add(verifiedDelta(claims, sig, strings[asNum(o.get("i"), "i")]!));
|
|
236
|
+
}
|
|
237
|
+
for (const rec of asArray(top.get("loose"), "loose")) out.add(hydrateRecord(rec, strings));
|
|
238
|
+
return out;
|
|
239
|
+
}
|
package/src/peer.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Federation (SPEC-6, ERRATA-6): a peer is a reactor + keypair + offered lens + admission
|
|
2
|
+
// predicate. Merge is union; this layer is selection and trust. Coordination without conscription.
|
|
3
|
+
|
|
4
|
+
import { evalTerm, type Term } from "./eval.js";
|
|
5
|
+
import { evalPred, type Pred } from "./pred.js";
|
|
6
|
+
import { Reactor, manifestMemberIds, type IngestResult } from "./reactor.js";
|
|
7
|
+
import { authorForSeed, signClaims, verifyDelta } from "./sign.js";
|
|
8
|
+
import type { Claims, Delta } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export interface SyncReport {
|
|
11
|
+
readonly offered: number;
|
|
12
|
+
readonly bundles: number;
|
|
13
|
+
readonly loose: number;
|
|
14
|
+
readonly withheld: number; // unsigned, uncovered: they stay local (F3)
|
|
15
|
+
readonly accepted: number;
|
|
16
|
+
readonly rejected: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ALL: Term = { kind: "input" };
|
|
20
|
+
|
|
21
|
+
export class Peer {
|
|
22
|
+
readonly reactor = new Reactor();
|
|
23
|
+
readonly author: string;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
private readonly seedHex: string,
|
|
27
|
+
// What this peer offers to others (F4). Default: everything.
|
|
28
|
+
public offeredLens: Term = ALL,
|
|
29
|
+
// What this peer accepts (SPEC-6 §5 step 3). Default: everything that verifies.
|
|
30
|
+
public admission: Pred | undefined = undefined,
|
|
31
|
+
) {
|
|
32
|
+
this.author = authorForSeed(seedHex);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Author a claim as this peer: sign and ingest (read-your-writes).
|
|
36
|
+
authorClaims(claims: Omit<Claims, "author">): Delta {
|
|
37
|
+
const signed = signClaims({ ...claims, author: this.author }, this.seedHex);
|
|
38
|
+
const result = this.reactor.ingest(signed);
|
|
39
|
+
if (result.status === "rejected") throw new Error(`own claim rejected: ${result.reason}`);
|
|
40
|
+
return signed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The admission judgment (SPEC-6 §5 step 3), exposed for transport bindings (F5).
|
|
44
|
+
admits(d: Delta): boolean {
|
|
45
|
+
return this.admission === undefined || evalPred(this.admission, d);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The offered set: eval(lens, log) — lens fidelity is a tested invariant (F4).
|
|
49
|
+
offeredSet(): Delta[] {
|
|
50
|
+
const result = evalTerm(this.offeredLens, this.reactor.snapshot());
|
|
51
|
+
if (result.sort !== "dset") throw new Error("a lens must be a DSet-sort term (F4)");
|
|
52
|
+
return [...result.set];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Pull from another peer: WANT(my ids) -> OFFER/BUNDLE -> verify -> admission -> ingest (§5).
|
|
56
|
+
pullFrom(other: Peer): SyncReport {
|
|
57
|
+
const have = new Set<string>();
|
|
58
|
+
for (const d of this.reactor.arrivalLog()) have.add(d.id);
|
|
59
|
+
|
|
60
|
+
const offered = other.offeredSet().filter((d) => !have.has(d.id));
|
|
61
|
+
const offeredIds = new Set(offered.map((d) => d.id));
|
|
62
|
+
|
|
63
|
+
// Partition per the signature boundary (F3): signed manifests carry their present members
|
|
64
|
+
// as bundles; remaining signed deltas travel loose; unsigned uncovered are withheld.
|
|
65
|
+
const isSignedManifest = (d: Delta) =>
|
|
66
|
+
d.sig !== undefined && verifyDelta(d) === "verified" && manifestMemberIds(d).length > 0;
|
|
67
|
+
const bundles: Array<{ manifest: Delta; members: Delta[] }> = [];
|
|
68
|
+
const covered = new Set<string>();
|
|
69
|
+
for (const m of offered.filter(isSignedManifest)) {
|
|
70
|
+
const members = manifestMemberIds(m)
|
|
71
|
+
.filter((id) => offeredIds.has(id))
|
|
72
|
+
.map((id) => offered.find((d) => d.id === id)!)
|
|
73
|
+
.filter((d) => !isSignedManifest(d));
|
|
74
|
+
bundles.push({ manifest: m, members });
|
|
75
|
+
for (const mem of members) covered.add(mem.id);
|
|
76
|
+
covered.add(m.id);
|
|
77
|
+
}
|
|
78
|
+
const loose = offered.filter(
|
|
79
|
+
(d) => !covered.has(d.id) && d.sig !== undefined && verifyDelta(d) === "verified",
|
|
80
|
+
);
|
|
81
|
+
const withheld = offered.length - covered.size - loose.length;
|
|
82
|
+
|
|
83
|
+
let accepted = 0;
|
|
84
|
+
let rejected = 0;
|
|
85
|
+
const admit = (d: Delta): boolean => this.admits(d);
|
|
86
|
+
const count = (r: IngestResult) => {
|
|
87
|
+
if (r.status === "accepted") accepted += 1;
|
|
88
|
+
else if (r.status === "rejected") rejected += 1;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
for (const { manifest, members } of bundles) {
|
|
92
|
+
// Admission applies to the act: if the manifest or any member fails, decline the bundle.
|
|
93
|
+
if (![manifest, ...members].every(admit)) {
|
|
94
|
+
rejected += 1 + members.length;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
count(this.reactor.ingestBundle(manifest, members));
|
|
98
|
+
}
|
|
99
|
+
for (const d of loose) {
|
|
100
|
+
if (!admit(d)) {
|
|
101
|
+
rejected += 1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
count(this.reactor.ingest(d));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
offered: offered.length,
|
|
109
|
+
bundles: bundles.length,
|
|
110
|
+
loose: loose.length,
|
|
111
|
+
withheld,
|
|
112
|
+
accepted,
|
|
113
|
+
rejected,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Anti-entropy both ways; repeat until quiescent (bounded — union is monotone).
|
|
119
|
+
export function syncBoth(a: Peer, b: Peer): void {
|
|
120
|
+
for (let i = 0; i < 4; i++) {
|
|
121
|
+
const before = a.reactor.digest() + b.reactor.digest();
|
|
122
|
+
a.pullFrom(b);
|
|
123
|
+
b.pullFrom(a);
|
|
124
|
+
if (a.reactor.digest() + b.reactor.digest() === before) return;
|
|
125
|
+
}
|
|
126
|
+
}
|