@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.
Files changed (119) hide show
  1. package/LICENSE-APACHE +201 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +54 -0
  4. package/dist/alias.d.ts +4 -0
  5. package/dist/alias.d.ts.map +1 -0
  6. package/dist/alias.js +34 -0
  7. package/dist/alias.js.map +1 -0
  8. package/dist/cbor.d.ts +24 -0
  9. package/dist/cbor.d.ts.map +1 -0
  10. package/dist/cbor.js +267 -0
  11. package/dist/cbor.js.map +1 -0
  12. package/dist/delta.d.ts +8 -0
  13. package/dist/delta.d.ts.map +1 -0
  14. package/dist/delta.js +92 -0
  15. package/dist/delta.js.map +1 -0
  16. package/dist/derivation.d.ts +29 -0
  17. package/dist/derivation.d.ts.map +1 -0
  18. package/dist/derivation.js +183 -0
  19. package/dist/derivation.js.map +1 -0
  20. package/dist/eval.d.ts +91 -0
  21. package/dist/eval.d.ts.map +1 -0
  22. package/dist/eval.js +318 -0
  23. package/dist/eval.js.map +1 -0
  24. package/dist/hash.d.ts +4 -0
  25. package/dist/hash.d.ts.map +1 -0
  26. package/dist/hash.js +17 -0
  27. package/dist/hash.js.map +1 -0
  28. package/dist/http.d.ts +21 -0
  29. package/dist/http.d.ts.map +1 -0
  30. package/dist/http.js +110 -0
  31. package/dist/http.js.map +1 -0
  32. package/dist/hview.d.ts +15 -0
  33. package/dist/hview.d.ts.map +1 -0
  34. package/dist/hview.js +72 -0
  35. package/dist/hview.js.map +1 -0
  36. package/dist/index.d.ts +23 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +22 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/json-profile.d.ts +4 -0
  41. package/dist/json-profile.d.ts.map +1 -0
  42. package/dist/json-profile.js +97 -0
  43. package/dist/json-profile.js.map +1 -0
  44. package/dist/pack.d.ts +5 -0
  45. package/dist/pack.d.ts.map +1 -0
  46. package/dist/pack.js +227 -0
  47. package/dist/pack.js.map +1 -0
  48. package/dist/peer.d.ts +26 -0
  49. package/dist/peer.d.ts.map +1 -0
  50. package/dist/peer.js +111 -0
  51. package/dist/peer.js.map +1 -0
  52. package/dist/policy.d.ts +46 -0
  53. package/dist/policy.d.ts.map +1 -0
  54. package/dist/policy.js +186 -0
  55. package/dist/policy.js.map +1 -0
  56. package/dist/pred.d.ts +78 -0
  57. package/dist/pred.d.ts.map +1 -0
  58. package/dist/pred.js +228 -0
  59. package/dist/pred.js.map +1 -0
  60. package/dist/reactor.d.ts +67 -0
  61. package/dist/reactor.d.ts.map +1 -0
  62. package/dist/reactor.js +433 -0
  63. package/dist/reactor.js.map +1 -0
  64. package/dist/schema-deltas.d.ts +14 -0
  65. package/dist/schema-deltas.d.ts.map +1 -0
  66. package/dist/schema-deltas.js +87 -0
  67. package/dist/schema-deltas.js.map +1 -0
  68. package/dist/schema.d.ts +17 -0
  69. package/dist/schema.d.ts.map +1 -0
  70. package/dist/schema.js +102 -0
  71. package/dist/schema.js.map +1 -0
  72. package/dist/set.d.ts +18 -0
  73. package/dist/set.d.ts.map +1 -0
  74. package/dist/set.js +83 -0
  75. package/dist/set.js.map +1 -0
  76. package/dist/sign.d.ts +8 -0
  77. package/dist/sign.d.ts.map +1 -0
  78. package/dist/sign.js +44 -0
  79. package/dist/sign.js.map +1 -0
  80. package/dist/term-io.d.ts +13 -0
  81. package/dist/term-io.d.ts.map +1 -0
  82. package/dist/term-io.js +216 -0
  83. package/dist/term-io.js.map +1 -0
  84. package/dist/term-json.d.ts +7 -0
  85. package/dist/term-json.d.ts.map +1 -0
  86. package/dist/term-json.js +362 -0
  87. package/dist/term-json.js.map +1 -0
  88. package/dist/types.d.ts +34 -0
  89. package/dist/types.d.ts.map +1 -0
  90. package/dist/types.js +4 -0
  91. package/dist/types.js.map +1 -0
  92. package/dist/vocab.d.ts +2 -0
  93. package/dist/vocab.d.ts.map +1 -0
  94. package/dist/vocab.js +4 -0
  95. package/dist/vocab.js.map +1 -0
  96. package/package.json +83 -0
  97. package/src/alias.ts +36 -0
  98. package/src/cbor.ts +280 -0
  99. package/src/delta.ts +89 -0
  100. package/src/derivation.ts +229 -0
  101. package/src/eval.ts +401 -0
  102. package/src/hash.ts +19 -0
  103. package/src/http.ts +124 -0
  104. package/src/hview.ts +91 -0
  105. package/src/index.ts +83 -0
  106. package/src/json-profile.ts +96 -0
  107. package/src/pack.ts +239 -0
  108. package/src/peer.ts +126 -0
  109. package/src/policy.ts +216 -0
  110. package/src/pred.ts +307 -0
  111. package/src/reactor.ts +490 -0
  112. package/src/schema-deltas.ts +100 -0
  113. package/src/schema.ts +111 -0
  114. package/src/set.ts +98 -0
  115. package/src/sign.ts +48 -0
  116. package/src/term-io.ts +228 -0
  117. package/src/term-json.ts +364 -0
  118. package/src/types.ts +38 -0
  119. package/src/vocab.ts +3 -0
package/src/set.ts ADDED
@@ -0,0 +1,98 @@
1
+ // The delta set and its algebra (SPEC-1 §8): a mathematical set of deltas, deduplicated by id.
2
+ // merge is union (grow-only set CRDT), fork is filter, federate is merge of a filtered fork.
3
+ // There is no conflict at this layer — contradiction lives in superposition until evaluation.
4
+
5
+ import { array, encode, tstr } from "./cbor.js";
6
+ import { computeId } from "./delta.js";
7
+ import { contentAddress } from "./hash.js";
8
+ import type { Claims, Delta, Pointer } from "./types.js";
9
+
10
+ // Build a complete delta from claims (id computed, optional detached sig attached).
11
+ export function makeDelta(claims: Claims, sig?: string): Delta {
12
+ const id = computeId(claims);
13
+ return sig === undefined ? { id, claims } : { id, claims, sig };
14
+ }
15
+
16
+ // The negation vocabulary convention (SPEC-1 §7): an ordinary delta whose pointer targets the
17
+ // negated delta by content address under role "negates". Meaning is given at evaluation (mask).
18
+ export function makeNegationClaims(
19
+ author: string,
20
+ timestamp: number,
21
+ targetDeltaId: string,
22
+ reason?: string,
23
+ ): Claims {
24
+ const pointers: Pointer[] = [
25
+ { role: "negates", target: { kind: "delta", deltaRef: { delta: targetDeltaId } } },
26
+ ];
27
+ if (reason !== undefined) {
28
+ pointers.push({ role: "reason", target: { kind: "primitive", value: reason } });
29
+ }
30
+ return { timestamp, author, pointers };
31
+ }
32
+
33
+ export class DeltaSet implements Iterable<Delta> {
34
+ private readonly byId = new Map<string, Delta>();
35
+
36
+ static from(deltas: Iterable<Delta>): DeltaSet {
37
+ const s = new DeltaSet();
38
+ for (const d of deltas) s.add(d);
39
+ return s;
40
+ }
41
+
42
+ // Idempotent insert; returns false when the id was already present. Verifies content
43
+ // addressing on the way in (P6): a delta whose id does not recompute is rejected, never
44
+ // repaired (SPEC-4 §2) — set semantics depend on true ids.
45
+ add(delta: Delta): boolean {
46
+ if (this.byId.has(delta.id)) return false;
47
+ if (computeId(delta.claims) !== delta.id) {
48
+ throw new Error(`delta id ${delta.id} does not match its claims (content addressing, P6)`);
49
+ }
50
+ this.byId.set(delta.id, delta);
51
+ return true;
52
+ }
53
+
54
+ has(id: string): boolean {
55
+ return this.byId.has(id);
56
+ }
57
+
58
+ get(id: string): Delta | undefined {
59
+ return this.byId.get(id);
60
+ }
61
+
62
+ get size(): number {
63
+ return this.byId.size;
64
+ }
65
+
66
+ [Symbol.iterator](): Iterator<Delta> {
67
+ return this.byId.values();
68
+ }
69
+
70
+ // Sorted lexicographically — the canonical enumeration order.
71
+ ids(): string[] {
72
+ return [...this.byId.keys()].sort();
73
+ }
74
+
75
+ // Canonical membership fingerprint (ERRATA D10, provisional helper — not the SPEC-6 digest).
76
+ digest(): string {
77
+ return contentAddress(encode(array(this.ids().map(tstr))));
78
+ }
79
+ }
80
+
81
+ // merge(A, B) = A ∪ B — commutative, associative, idempotent (SPEC-1 §8).
82
+ export function merge(a: DeltaSet, b: DeltaSet): DeltaSet {
83
+ const s = DeltaSet.from(a);
84
+ for (const d of b) s.add(d);
85
+ return s;
86
+ }
87
+
88
+ // fork(A, p) = { d ∈ A : p(d) } — any filter yields a valid delta set (SPEC-1 §8).
89
+ export function fork(a: DeltaSet, p: (d: Delta) => boolean): DeltaSet {
90
+ const s = new DeltaSet();
91
+ for (const d of a) if (p(d)) s.add(d);
92
+ return s;
93
+ }
94
+
95
+ // federate(A, B, p) = A ∪ fork(B, p) — merge of a filtered fork (SPEC-1 §8).
96
+ export function federate(a: DeltaSet, b: DeltaSet, p: (d: Delta) => boolean): DeltaSet {
97
+ return merge(a, fork(b, p));
98
+ }
package/src/sign.ts ADDED
@@ -0,0 +1,48 @@
1
+ // Ed25519 signing & verification (SPEC-1 §5, ERRATA D8-D9). Deterministic (RFC 8032), so
2
+ // signatures are reproducible across implementations and pinned in vectors.
3
+
4
+ import { ed25519 } from "@noble/curves/ed25519";
5
+ import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
6
+ import { computeId } from "./delta.js";
7
+ import type { Claims, Delta } from "./types.js";
8
+
9
+ export const AUTHOR_PREFIX = "ed25519:";
10
+
11
+ export function publicKeyFromSeed(seedHex: string): string {
12
+ return bytesToHex(ed25519.getPublicKey(hexToBytes(seedHex)));
13
+ }
14
+
15
+ // The author string a signed delta MUST carry for this seed (ERRATA D8).
16
+ export function authorForSeed(seedHex: string): string {
17
+ return AUTHOR_PREFIX + publicKeyFromSeed(seedHex);
18
+ }
19
+
20
+ // Sign claims, producing a complete delta. Refuses to sign claims whose author does not match
21
+ // the signing key — a signature contradicting its own author field is born broken (ERRATA D8).
22
+ export function signClaims(claims: Claims, seedHex: string): Delta {
23
+ const expected = authorForSeed(seedHex);
24
+ if (claims.author !== expected) {
25
+ throw new Error(`author must be ${expected} for this signing key, got ${claims.author}`);
26
+ }
27
+ const id = computeId(claims);
28
+ const sig = bytesToHex(ed25519.sign(hexToBytes(id), hexToBytes(seedHex)));
29
+ return { id, claims, sig };
30
+ }
31
+
32
+ export type Verification = "verified" | "unsigned" | "invalid";
33
+
34
+ // Full verification per ERRATA D9: content addressing must hold, then the signature must verify
35
+ // over the raw id bytes against the key named in `author`.
36
+ export function verifyDelta(delta: Delta): Verification {
37
+ if (computeId(delta.claims) !== delta.id) return "invalid";
38
+ if (delta.sig === undefined) return "unsigned";
39
+ if (!delta.claims.author.startsWith(AUTHOR_PREFIX)) return "invalid";
40
+ const pubHex = delta.claims.author.slice(AUTHOR_PREFIX.length);
41
+ try {
42
+ return ed25519.verify(hexToBytes(delta.sig), hexToBytes(delta.id), hexToBytes(pubHex))
43
+ ? "verified"
44
+ : "invalid";
45
+ } catch {
46
+ return "invalid";
47
+ }
48
+ }
package/src/term-io.ts ADDED
@@ -0,0 +1,228 @@
1
+ // Term serialization back to the JSON profile, the generic JSON<->CBOR bridge, and term hashing
2
+ // (ERRATA-2 E12). parse(termToJson(t)) is identity on the AST, so semantically identical terms
3
+ // hash identically regardless of authored spelling.
4
+
5
+ import { type CborValue, array, bool, encode, float, map, tstr } from "./cbor.js";
6
+ import type { Term } from "./eval.js";
7
+ import { bytesToHex, contentAddress } from "./hash.js";
8
+ import type { Order, Policy, PropPolicy } from "./policy.js";
9
+ import type { Hole, Pred, PPred, StrMatch, ValMatch } from "./pred.js";
10
+ import type { Primitive } from "./types.js";
11
+
12
+ // --- AST -> JSON profile ---------------------------------------------------------------------------
13
+
14
+ // Holes serialize to their profile spelling (E15); primitives pass through.
15
+ function paramToJson(v: Primitive | Hole): unknown {
16
+ return typeof v === "object" ? { hole: v.name } : v;
17
+ }
18
+
19
+ function strMatchToJson(m: StrMatch): unknown {
20
+ switch (m.kind) {
21
+ case "exact":
22
+ return { exact: m.value };
23
+ case "prefix":
24
+ return { prefix: m.value };
25
+ case "inSet":
26
+ return { inSet: [...m.values] };
27
+ case "aliased": {
28
+ // Optional fields omitted when absent (SPEC-2 §7); the closure never enters the hash.
29
+ const a: Record<string, unknown> = { name: m.name };
30
+ if (m.via !== undefined) a["via"] = m.via;
31
+ if (m.trust !== undefined) a["trust"] = predToJson(m.trust);
32
+ return { aliased: a };
33
+ }
34
+ }
35
+ }
36
+
37
+ function valMatchToJson(m: ValMatch): unknown {
38
+ switch (m.kind) {
39
+ case "vcmp":
40
+ return { vcmp: { cmp: m.cmp, value: paramToJson(m.value) } };
41
+ case "between":
42
+ return { between: [m.lo, m.hi] };
43
+ case "inSet":
44
+ return { inSet: [...m.values] };
45
+ }
46
+ }
47
+
48
+ function ppredToJson(p: PPred): unknown {
49
+ const out: Record<string, unknown> = {};
50
+ if (p.role !== undefined) out["role"] = strMatchToJson(p.role);
51
+ if (p.targetEntity !== undefined) {
52
+ out["targetEntity"] =
53
+ p.targetEntity.kind === "const"
54
+ ? p.targetEntity.id
55
+ : p.targetEntity.kind === "hole"
56
+ ? { hole: p.targetEntity.name }
57
+ : { var: "root" };
58
+ }
59
+ if (p.targetDelta !== undefined) out["targetDelta"] = p.targetDelta;
60
+ if (p.context !== undefined) out["context"] = strMatchToJson(p.context);
61
+ if (p.targetIsPrimitive !== undefined) out["targetIsPrimitive"] = p.targetIsPrimitive;
62
+ if (p.targetValue !== undefined) out["targetValue"] = valMatchToJson(p.targetValue);
63
+ return out;
64
+ }
65
+
66
+ export function predToJson(pred: Pred): unknown {
67
+ switch (pred.kind) {
68
+ case "true":
69
+ return "true";
70
+ case "false":
71
+ return "false";
72
+ case "match":
73
+ return {
74
+ match: {
75
+ field: pred.field,
76
+ cmp: pred.cmp,
77
+ const: Array.isArray(pred.constant)
78
+ ? [...pred.constant]
79
+ : paramToJson(pred.constant as Primitive | Hole),
80
+ },
81
+ };
82
+ case "hasPointer":
83
+ return { hasPointer: ppredToJson(pred.ppred) };
84
+ case "and":
85
+ return { and: [predToJson(pred.left), predToJson(pred.right)] };
86
+ case "or":
87
+ return { or: [predToJson(pred.left), predToJson(pred.right)] };
88
+ case "not":
89
+ return { not: predToJson(pred.pred) };
90
+ }
91
+ }
92
+
93
+ function orderToJson(o: Order): unknown {
94
+ switch (o.kind) {
95
+ case "byTimestamp":
96
+ return { byTimestamp: o.dir };
97
+ case "byAuthorRank":
98
+ return { byAuthorRank: [...o.authors] };
99
+ case "byPred":
100
+ return { byPred: { pred: predToJson(o.pred), then: orderToJson(o.then) } };
101
+ case "lexById":
102
+ return "lexById";
103
+ }
104
+ }
105
+
106
+ function propPolicyToJson(pp: PropPolicy): unknown {
107
+ switch (pp.kind) {
108
+ case "pick":
109
+ return { pick: { order: orderToJson(pp.order) } };
110
+ case "all":
111
+ return { all: { order: orderToJson(pp.order) } };
112
+ case "merge":
113
+ return { merge: pp.fn };
114
+ case "conflicts":
115
+ return { conflicts: { order: orderToJson(pp.order) } };
116
+ case "absentAs":
117
+ return { absentAs: { const: pp.constant, then: propPolicyToJson(pp.then) } };
118
+ }
119
+ }
120
+
121
+ export function policyToJson(p: Policy): unknown {
122
+ const props: Record<string, unknown> = {};
123
+ for (const [k, v] of p.props) props[k] = propPolicyToJson(v);
124
+ return { props, default: propPolicyToJson(p.default) };
125
+ }
126
+
127
+ export function termToJson(term: Term): unknown {
128
+ switch (term.kind) {
129
+ case "input":
130
+ return "input";
131
+ case "select":
132
+ return { op: "select", pred: predToJson(term.pred), in: termToJson(term.of) };
133
+ case "union":
134
+ return { op: "union", left: termToJson(term.left), right: termToJson(term.right) };
135
+ case "mask": {
136
+ const policy =
137
+ term.policy.kind === "trust" ? { trust: predToJson(term.policy.pred) } : term.policy.kind;
138
+ return { op: "mask", policy, in: termToJson(term.of) };
139
+ }
140
+ case "group": {
141
+ const key = term.key.kind === "const" ? { const: term.key.prop } : term.key.kind;
142
+ return { op: "group", key, in: termToJson(term.of) };
143
+ }
144
+ case "prune":
145
+ return {
146
+ op: "prune",
147
+ keep: term.keep === "all" ? "all" : strMatchToJson(term.keep),
148
+ in: termToJson(term.of),
149
+ };
150
+ case "expand":
151
+ return {
152
+ op: "expand",
153
+ role: strMatchToJson(term.role),
154
+ schema: schemaRefToJson(term.schema),
155
+ in: termToJson(term.of),
156
+ };
157
+ case "fix": {
158
+ const out: Record<string, unknown> = {
159
+ op: "fix",
160
+ schema: schemaRefToJson(term.schema),
161
+ entity: term.entity,
162
+ };
163
+ if (term.bindings !== undefined && term.bindings.size > 0) {
164
+ const bindings: Record<string, Primitive> = {};
165
+ for (const key of [...term.bindings.keys()].sort()) {
166
+ bindings[key] = term.bindings.get(key)!;
167
+ }
168
+ out["bindings"] = bindings;
169
+ }
170
+ return out;
171
+ }
172
+ case "resolve":
173
+ return { op: "resolve", policy: policyToJson(term.policy), in: termToJson(term.of) };
174
+ }
175
+ }
176
+
177
+ function schemaRefToJson(ref: import("./eval.js").SchemaRefT): unknown {
178
+ return ref.kind === "name" ? ref.name : { pinned: ref.hash };
179
+ }
180
+
181
+ // --- generic JSON <-> CBOR bridge ------------------------------------------------------------------
182
+
183
+ export function jsonToCbor(v: unknown): CborValue {
184
+ if (typeof v === "string") return tstr(v);
185
+ if (typeof v === "number") return float(v);
186
+ if (typeof v === "boolean") return bool(v);
187
+ if (Array.isArray(v)) return array(v.map(jsonToCbor));
188
+ if (typeof v === "object" && v !== null) {
189
+ return map(
190
+ Object.entries(v as Record<string, unknown>).map(([k, x]): readonly [string, CborValue] => [
191
+ k,
192
+ jsonToCbor(x),
193
+ ]),
194
+ );
195
+ }
196
+ throw new Error("json value outside the CBOR profile (null/undefined are not representable)");
197
+ }
198
+
199
+ export function cborToJson(v: CborValue): unknown {
200
+ switch (v.t) {
201
+ case "tstr":
202
+ case "float":
203
+ case "bool":
204
+ return v.v;
205
+ case "array":
206
+ return v.v.map(cborToJson);
207
+ case "map": {
208
+ const out: Record<string, unknown> = {};
209
+ for (const [k, x] of v.v) out[k] = cborToJson(x);
210
+ return out;
211
+ }
212
+ }
213
+ }
214
+
215
+ // --- term hashing (E12) ----------------------------------------------------------------------------
216
+
217
+ export function termCanonicalBytes(term: Term): Uint8Array {
218
+ return encode(jsonToCbor(termToJson(term)));
219
+ }
220
+
221
+ export function termCanonicalHex(term: Term): string {
222
+ return bytesToHex(termCanonicalBytes(term));
223
+ }
224
+
225
+ // A term's content address: same multihash as deltas (E12).
226
+ export function termHash(term: Term): string {
227
+ return contentAddress(termCanonicalBytes(term));
228
+ }