@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/eval.ts ADDED
@@ -0,0 +1,401 @@
1
+ // Term evaluation: select/union/mask over DSet (SPEC-2 §4.1-4.3), group into HView (§4.4),
2
+ // prune over HView (§4.6). eval is a pure function; order-blind; deterministic (SPEC-2 §5).
3
+ // Sorts are checked at evaluation time in v0 (ERRATA-2 E9).
4
+
5
+ import { array, encode, map, tstr } from "./cbor.js";
6
+ import { bytesToHex } from "./hash.js";
7
+ import { hviewCanonicalHex, type HVEntry, type HView } from "./hview.js";
8
+ import { resolveView, viewCanonicalHex, type Policy, type View } from "./policy.js";
9
+ import {
10
+ comparePrimitives,
11
+ evalPred,
12
+ strMatch,
13
+ substituteHoles,
14
+ type Bindings,
15
+ type Pred,
16
+ type StrMatch,
17
+ } from "./pred.js";
18
+ import { VOCAB_PREFIX } from "./vocab.js";
19
+ import { SchemaRegistry } from "./schema.js";
20
+ import { DeltaSet, fork, merge } from "./set.js";
21
+ import type { Delta } from "./types.js";
22
+
23
+ export type MaskPolicy =
24
+ | { readonly kind: "drop" }
25
+ | { readonly kind: "annotate" }
26
+ | { readonly kind: "trust"; readonly pred: Pred };
27
+
28
+ export type SchemaRefT =
29
+ | { readonly kind: "name"; readonly name: string }
30
+ | { readonly kind: "pinned"; readonly hash: string };
31
+
32
+ export type GroupKey =
33
+ | { readonly kind: "byTargetContext" }
34
+ | { readonly kind: "byRole" }
35
+ | { readonly kind: "const"; readonly prop: string };
36
+
37
+ export type Term =
38
+ | { readonly kind: "input" }
39
+ | { readonly kind: "select"; readonly pred: Pred; readonly of: Term }
40
+ | { readonly kind: "union"; readonly left: Term; readonly right: Term }
41
+ | { readonly kind: "mask"; readonly policy: MaskPolicy; readonly of: Term }
42
+ | { readonly kind: "group"; readonly key: GroupKey; readonly of: Term }
43
+ | { readonly kind: "prune"; readonly keep: "all" | StrMatch; readonly of: Term }
44
+ | {
45
+ readonly kind: "expand";
46
+ readonly role: StrMatch;
47
+ readonly schema: SchemaRefT;
48
+ readonly of: Term;
49
+ }
50
+ | {
51
+ readonly kind: "fix";
52
+ readonly schema: SchemaRefT;
53
+ readonly entity: string;
54
+ readonly bindings?: Bindings;
55
+ }
56
+ | { readonly kind: "resolve"; readonly policy: Policy; readonly of: Term };
57
+
58
+ interface DSetResult {
59
+ readonly sort: "dset";
60
+ readonly set: DeltaSet;
61
+ // Negation tags from mask(annotate); consumed by group (E7) or surfaced at top level (E2).
62
+ readonly negated: ReadonlySet<string>;
63
+ readonly annotated: boolean;
64
+ }
65
+
66
+ interface HViewResult {
67
+ readonly sort: "hview";
68
+ readonly hview: HView;
69
+ }
70
+
71
+ // The terminal sort: no operator consumes a View (SPEC-2 §4.7, ERRATA-5 R7).
72
+ interface ViewResult {
73
+ readonly sort: "view";
74
+ readonly view: View;
75
+ }
76
+
77
+ export type EvalResult = DSetResult | HViewResult | ViewResult;
78
+
79
+ const dsetResult = (set: DeltaSet): DSetResult => ({
80
+ sort: "dset",
81
+ set,
82
+ negated: new Set(),
83
+ annotated: false,
84
+ });
85
+
86
+ function expectDSet(r: EvalResult, op: string): DSetResult {
87
+ if (r.sort !== "dset") throw new Error(`${op} requires a DSet operand (E9)`);
88
+ return r;
89
+ }
90
+
91
+ function expectHView(r: EvalResult, op: string): HViewResult {
92
+ if (r.sort !== "hview") throw new Error(`${op} requires an HView operand (E9)`);
93
+ return r;
94
+ }
95
+
96
+ // negated(d, D) per SPEC-2 §4.3, over candidate negations restricted by `trusted` (E4).
97
+ // Memoized with an in-progress default of "not negated" (E5 recursion guard).
98
+ function computeNegated(d: DeltaSet, trusted?: (n: Delta) => boolean): Set<string> {
99
+ const negators = new Map<string, string[]>(); // target delta id -> negation delta ids
100
+ for (const n of d) {
101
+ if (trusted !== undefined && !trusted(n)) continue;
102
+ for (const ptr of n.claims.pointers) {
103
+ if (ptr.role === "negates" && ptr.target.kind === "delta") {
104
+ const list = negators.get(ptr.target.deltaRef.delta);
105
+ if (list === undefined) negators.set(ptr.target.deltaRef.delta, [n.id]);
106
+ else list.push(n.id);
107
+ }
108
+ }
109
+ }
110
+ const memo = new Map<string, boolean>();
111
+ const isNegated = (id: string): boolean => {
112
+ const cached = memo.get(id);
113
+ if (cached !== undefined) return cached;
114
+ memo.set(id, false); // guard: cycles are impossible with verified ids, but degrade safely
115
+ const result = (negators.get(id) ?? []).some((nid) => !isNegated(nid));
116
+ memo.set(id, result);
117
+ return result;
118
+ };
119
+ const out = new Set<string>();
120
+ for (const delta of d) if (isNegated(delta.id)) out.add(delta.id);
121
+ return out;
122
+ }
123
+
124
+ // --- the aliased closure (SPEC-9 §4.1) -----------------------------------------------------------
125
+
126
+ const ALIAS_FRAGMENT = `${VOCAB_PREFIX}.alias.fragment`;
127
+ const ALIAS_SLOT = `${VOCAB_PREFIX}.alias.slot`;
128
+ const ALIAS_CONCEPT = `${VOCAB_PREFIX}.alias.concept`;
129
+
130
+ export interface AliasedSpec {
131
+ readonly name: string;
132
+ readonly via?: string;
133
+ readonly trust?: Pred;
134
+ }
135
+
136
+ // closure(A, D): name → slots → fragments, one hop, computed against the AMBIENT evaluation
137
+ // input. The trust predicate restricts every participant — mappings, slot declarations, and the
138
+ // negations of both — and negation chains are walked within the trusted set only (mask(trust)
139
+ // semantics). Returns the closure sorted by the canonical string order; the name is always a
140
+ // member, so an aliased with no surviving mappings degrades to exact(name).
141
+ export function aliasClosure(input: DeltaSet, spec: AliasedSpec, root?: string): string[] {
142
+ const trustPred = spec.trust;
143
+ const trusted = trustPred === undefined ? undefined : (n: Delta) => evalPred(trustPred, n, root);
144
+ const negated = computeNegated(input, trusted);
145
+ const mappings: { fragment: string; slot: string }[] = [];
146
+ const slotConcepts = new Map<string, Set<string>>();
147
+ for (const d of input) {
148
+ if (trusted !== undefined && !trusted(d)) continue;
149
+ if (negated.has(d.id)) continue;
150
+ const fragments: string[] = [];
151
+ const slots: string[] = [];
152
+ const concepts: string[] = [];
153
+ for (const ptr of d.claims.pointers) {
154
+ if (ptr.role === ALIAS_FRAGMENT && ptr.target.kind === "primitive") {
155
+ if (typeof ptr.target.value === "string") fragments.push(ptr.target.value);
156
+ } else if (ptr.role === ALIAS_SLOT && ptr.target.kind === "entity") {
157
+ slots.push(ptr.target.entity.id);
158
+ } else if (ptr.role === ALIAS_CONCEPT && ptr.target.kind === "entity") {
159
+ concepts.push(ptr.target.entity.id);
160
+ }
161
+ }
162
+ // Mapping claim: ≥1 fragment × ≥1 slot, cross product (SPEC-9 §3). Anything else with the
163
+ // alias roles is not a mapping and is ignored here (graceful degradation).
164
+ for (const fragment of fragments) for (const slot of slots) mappings.push({ fragment, slot });
165
+ // Slot declaration: ≥1 slot × ≥1 concept (SPEC-9 §2).
166
+ for (const slot of slots) {
167
+ for (const concept of concepts) {
168
+ let set = slotConcepts.get(slot);
169
+ if (set === undefined) {
170
+ set = new Set();
171
+ slotConcepts.set(slot, set);
172
+ }
173
+ set.add(concept);
174
+ }
175
+ }
176
+ }
177
+ const eligible =
178
+ spec.via === undefined
179
+ ? mappings
180
+ : mappings.filter((m) => slotConcepts.get(m.slot)?.has(spec.via!) ?? false);
181
+ const slotsOfName = new Set(eligible.filter((m) => m.fragment === spec.name).map((m) => m.slot));
182
+ const closure = new Set<string>([spec.name]);
183
+ for (const m of eligible) if (slotsOfName.has(m.slot)) closure.add(m.fragment);
184
+ return [...closure].sort(comparePrimitives);
185
+ }
186
+
187
+ // Expand an aliased StrMatch to its inSet form against the ambient input; other forms pass.
188
+ function expandStrMatch(m: StrMatch, input: DeltaSet, root: string | undefined): StrMatch {
189
+ if (m.kind !== "aliased") return m;
190
+ return { kind: "inSet", values: aliasClosure(input, m, root) };
191
+ }
192
+
193
+ // Expand every aliased StrMatch in a predicate (ppred role/context) against the ambient input.
194
+ // Applied where predicates meet data (select / mask-trust), after hole substitution (SPEC-9 §4.1).
195
+ export function expandAliased(pred: Pred, input: DeltaSet, root: string | undefined): Pred {
196
+ switch (pred.kind) {
197
+ case "true":
198
+ case "false":
199
+ case "match":
200
+ return pred;
201
+ case "hasPointer": {
202
+ const p = pred.ppred;
203
+ const role = p.role === undefined ? undefined : expandStrMatch(p.role, input, root);
204
+ const context = p.context === undefined ? undefined : expandStrMatch(p.context, input, root);
205
+ if (role === p.role && context === p.context) return pred;
206
+ return {
207
+ kind: "hasPointer",
208
+ ppred: {
209
+ ...p,
210
+ ...(role === undefined ? {} : { role }),
211
+ ...(context === undefined ? {} : { context }),
212
+ },
213
+ };
214
+ }
215
+ case "and":
216
+ return {
217
+ kind: "and",
218
+ left: expandAliased(pred.left, input, root),
219
+ right: expandAliased(pred.right, input, root),
220
+ };
221
+ case "or":
222
+ return {
223
+ kind: "or",
224
+ left: expandAliased(pred.left, input, root),
225
+ right: expandAliased(pred.right, input, root),
226
+ };
227
+ case "not":
228
+ return { kind: "not", pred: expandAliased(pred.pred, input, root) };
229
+ }
230
+ }
231
+
232
+ // group(key, D) @ root — filing rules per ERRATA-2 E6; annotate tags thread into entries (E7).
233
+ function evalGroup(key: GroupKey, operand: DSetResult, root: string): HView {
234
+ const buckets = new Map<string, Map<string, HVEntry>>(); // prop -> deltaId -> entry
235
+ const file = (prop: string, d: Delta) => {
236
+ let bucket = buckets.get(prop);
237
+ if (bucket === undefined) {
238
+ bucket = new Map();
239
+ buckets.set(prop, bucket);
240
+ }
241
+ if (!bucket.has(d.id)) bucket.set(d.id, { delta: d, negated: operand.negated.has(d.id) });
242
+ };
243
+ for (const d of operand.set) {
244
+ if (key.kind === "const") {
245
+ file(key.prop, d);
246
+ continue;
247
+ }
248
+ for (const ptr of d.claims.pointers) {
249
+ if (ptr.target.kind !== "entity" || ptr.target.entity.id !== root) continue;
250
+ if (key.kind === "byTargetContext") {
251
+ const ctx = ptr.target.entity.context;
252
+ if (ctx !== undefined) file(ctx, d);
253
+ } else {
254
+ file(ptr.role, d);
255
+ }
256
+ }
257
+ }
258
+ const props = new Map<string, HVEntry[]>();
259
+ for (const [prop, bucket] of buckets) {
260
+ props.set(
261
+ prop,
262
+ [...bucket.values()].sort((a, b) => (a.delta.id < b.delta.id ? -1 : 1)),
263
+ );
264
+ }
265
+ return { id: root, props };
266
+ }
267
+
268
+ export function evalTerm(
269
+ term: Term,
270
+ input: DeltaSet,
271
+ root?: string,
272
+ registry?: SchemaRegistry,
273
+ bindings?: Bindings,
274
+ ): EvalResult {
275
+ switch (term.kind) {
276
+ case "input":
277
+ return dsetResult(input);
278
+ case "select": {
279
+ const of = expectDSet(evalTerm(term.of, input, root, registry, bindings), "select");
280
+ const pred = expandAliased(substituteHoles(term.pred, bindings), input, root);
281
+ return dsetResult(fork(of.set, (d) => evalPred(pred, d, root)));
282
+ }
283
+ case "union": {
284
+ const left = expectDSet(evalTerm(term.left, input, root, registry, bindings), "union");
285
+ const right = expectDSet(evalTerm(term.right, input, root, registry, bindings), "union");
286
+ return dsetResult(merge(left.set, right.set));
287
+ }
288
+ case "mask": {
289
+ const of = expectDSet(evalTerm(term.of, input, root, registry, bindings), "mask");
290
+ switch (term.policy.kind) {
291
+ case "drop": {
292
+ const negated = computeNegated(of.set);
293
+ return dsetResult(fork(of.set, (d) => !negated.has(d.id)));
294
+ }
295
+ case "annotate": {
296
+ const negated = computeNegated(of.set);
297
+ return { sort: "dset", set: of.set, negated, annotated: true };
298
+ }
299
+ case "trust": {
300
+ const pred = expandAliased(substituteHoles(term.policy.pred, bindings), input, root);
301
+ const negated = computeNegated(of.set, (n) => evalPred(pred, n, root));
302
+ return dsetResult(fork(of.set, (d) => !negated.has(d.id)));
303
+ }
304
+ }
305
+ break;
306
+ }
307
+ case "group": {
308
+ if (root === undefined) throw new Error("group requires an ambient root entity (E9)");
309
+ const of = expectDSet(evalTerm(term.of, input, root, registry, bindings), "group");
310
+ return { sort: "hview", hview: evalGroup(term.key, of, root) };
311
+ }
312
+ case "prune": {
313
+ const of = expectHView(evalTerm(term.of, input, root, registry, bindings), "prune");
314
+ if (term.keep === "all") return of;
315
+ const keep = expandStrMatch(term.keep, input, root);
316
+ const props = new Map<string, readonly HVEntry[]>();
317
+ for (const [prop, entries] of of.hview.props) {
318
+ if (strMatch(keep, prop)) props.set(prop, entries);
319
+ }
320
+ return { sort: "hview", hview: { id: of.hview.id, props } };
321
+ }
322
+ case "expand": {
323
+ const of = expectHView(evalTerm(term.of, input, root, registry, bindings), "expand");
324
+ const role = expandStrMatch(term.role, input, root);
325
+ const props = new Map<string, readonly HVEntry[]>();
326
+ for (const [prop, entries] of of.hview.props) {
327
+ props.set(
328
+ prop,
329
+ entries.map((e) => {
330
+ let expanded: Map<number, HView> | undefined;
331
+ e.delta.claims.pointers.forEach((ptr, i) => {
332
+ // Only role-matching EntityRef pointers expand; everything else passes through
333
+ // as written (E11, SPEC-3 §7 graceful degradation).
334
+ if (ptr.target.kind !== "entity" || !strMatch(role, ptr.role)) return;
335
+ const nested = evalSchema(
336
+ term.schema,
337
+ input,
338
+ ptr.target.entity.id,
339
+ registry,
340
+ bindings,
341
+ );
342
+ expanded = expanded ?? new Map(e.expanded ?? []);
343
+ expanded.set(i, nested);
344
+ });
345
+ return expanded === undefined ? e : { ...e, expanded };
346
+ }),
347
+ );
348
+ }
349
+ return { sort: "hview", hview: { id: of.hview.id, props } };
350
+ }
351
+ case "fix":
352
+ // The invocation instruction: ambient root is set explicitly (E10); bindings, when
353
+ // present, become the ambient hole environment for the invoked body (E15).
354
+ return {
355
+ sort: "hview",
356
+ hview: evalSchema(term.schema, input, term.entity, registry, term.bindings ?? bindings),
357
+ };
358
+ case "resolve": {
359
+ const of = expectHView(evalTerm(term.of, input, root, registry, bindings), "resolve");
360
+ return { sort: "view", view: resolveView(term.policy, of.hview) };
361
+ }
362
+ }
363
+ }
364
+
365
+ // Evaluate a named schema at a root over the SAME delta set the enclosing evaluation received
366
+ // (SPEC-2 §4.5). Termination is the schema DAG's, enforced at registry build (SPEC-3 §3).
367
+ function evalSchema(
368
+ ref: SchemaRefT,
369
+ input: DeltaSet,
370
+ root: string,
371
+ registry: SchemaRegistry | undefined,
372
+ bindings?: Bindings,
373
+ ): HView {
374
+ const label = ref.kind === "name" ? ref.name : `pinned:${ref.hash.slice(0, 12)}…`;
375
+ if (registry === undefined)
376
+ throw new Error(`schema ${label} referenced but no registry supplied (E10)`);
377
+ const schema = registry.resolve(ref);
378
+ if (schema === undefined) throw new Error(`unknown schema: ${label} (E10/E13)`);
379
+ const result = evalTerm(schema.body, input, root, registry, bindings);
380
+ if (result.sort !== "hview") {
381
+ throw new Error(`schema ${label} body must be an HView-sort term (E10)`);
382
+ }
383
+ return result.hview;
384
+ }
385
+
386
+ // Canonical serialization of an evaluation result (ERRATA-2 E2, E7).
387
+ export function resultCanonicalHex(result: EvalResult): string {
388
+ if (result.sort === "view") return viewCanonicalHex(result.view);
389
+ if (result.sort === "hview") return hviewCanonicalHex(result.hview);
390
+ const ids = result.set.ids().map(tstr);
391
+ if (!result.annotated) return bytesToHex(encode(array(ids)));
392
+ const negated = [...result.negated].sort().map(tstr);
393
+ return bytesToHex(
394
+ encode(
395
+ map([
396
+ ["ids", array(ids)],
397
+ ["negated", array(negated)],
398
+ ]),
399
+ ),
400
+ );
401
+ }
package/src/hash.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { blake3 } from "@noble/hashes/blake3";
2
+ import { bytesToHex } from "@noble/hashes/utils";
3
+
4
+ // BLAKE3 multicodec code (https://github.com/multiformats/multicodec). Both it and the 32-byte
5
+ // length fit in a single-byte varint, so the multihash prefix is simply [0x1e, 0x20].
6
+ const BLAKE3_MULTICODEC = 0x1e;
7
+ const DIGEST_LEN = 32;
8
+
9
+ // id = multihash(BLAKE3-256(data)) as lowercase hex (SPEC-1 §4, ERRATA D7).
10
+ export function contentAddress(data: Uint8Array): string {
11
+ const digest = blake3(data, { dkLen: DIGEST_LEN });
12
+ const mh = new Uint8Array(2 + digest.length);
13
+ mh[0] = BLAKE3_MULTICODEC;
14
+ mh[1] = digest.length;
15
+ mh.set(digest, 2);
16
+ return bytesToHex(mh);
17
+ }
18
+
19
+ export { bytesToHex };
package/src/http.ts ADDED
@@ -0,0 +1,124 @@
1
+ // The blessed HTTP federation binding (ERRATA-6 F5): POST /rhz/v0/sync over the Peer protocol.
2
+ // Transport only — partitioning, the signature boundary, and admission all live in Peer (F3/§5).
3
+
4
+ import { createServer, type Server } from "node:http";
5
+ import { claimsToJson, parseClaims } from "./json-profile.js";
6
+ import { Peer } from "./peer.js";
7
+ import { manifestMemberIds } from "./reactor.js";
8
+ import { makeDelta } from "./set.js";
9
+ import { verifyDelta } from "./sign.js";
10
+ import type { Delta } from "./types.js";
11
+
12
+ interface WireDelta {
13
+ claims: unknown;
14
+ sig?: string;
15
+ }
16
+
17
+ interface SyncResponse {
18
+ bundles: Array<{ manifest: WireDelta; members: WireDelta[] }>;
19
+ loose: WireDelta[];
20
+ }
21
+
22
+ function toWire(d: Delta): WireDelta {
23
+ // No id on the wire: the receiver recomputes content addresses (F5; never trust the wire).
24
+ return d.sig === undefined
25
+ ? { claims: claimsToJson(d.claims) }
26
+ : { claims: claimsToJson(d.claims), sig: d.sig };
27
+ }
28
+
29
+ function fromWire(w: WireDelta): Delta {
30
+ return makeDelta(parseClaims(w.claims), w.sig);
31
+ }
32
+
33
+ // Compute the OFFER for a WANT, partitioned per the signature boundary (F3).
34
+ export function offerFor(peer: Peer, have: ReadonlySet<string>): SyncResponse {
35
+ const offered = peer.offeredSet().filter((d) => !have.has(d.id));
36
+ const offeredIds = new Set(offered.map((d) => d.id));
37
+ const isSignedManifest = (d: Delta) =>
38
+ d.sig !== undefined && verifyDelta(d) === "verified" && manifestMemberIds(d).length > 0;
39
+ const covered = new Set<string>();
40
+ const bundles: SyncResponse["bundles"] = [];
41
+ for (const m of offered.filter(isSignedManifest)) {
42
+ const members = manifestMemberIds(m)
43
+ .filter((id) => offeredIds.has(id))
44
+ .map((id) => offered.find((d) => d.id === id)!)
45
+ .filter((d) => !isSignedManifest(d));
46
+ bundles.push({ manifest: toWire(m), members: members.map(toWire) });
47
+ for (const mem of members) covered.add(mem.id);
48
+ covered.add(m.id);
49
+ }
50
+ const loose = offered
51
+ .filter((d) => !covered.has(d.id) && d.sig !== undefined && verifyDelta(d) === "verified")
52
+ .map(toWire);
53
+ return { bundles, loose };
54
+ }
55
+
56
+ // Serve a peer's offered lens over HTTP. Returns the server; close it when done.
57
+ export function servePeer(peer: Peer, port: number): Promise<Server> {
58
+ const server = createServer((req, res) => {
59
+ if (req.method !== "POST" || req.url !== "/rhz/v0/sync") {
60
+ res.writeHead(404).end();
61
+ return;
62
+ }
63
+ const chunks: Buffer[] = [];
64
+ req.on("data", (c: Buffer) => chunks.push(c));
65
+ req.on("end", () => {
66
+ try {
67
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf8")) as { have?: string[] };
68
+ const have = new Set<string>(Array.isArray(body.have) ? body.have : []);
69
+ const response = offerFor(peer, have);
70
+ res.writeHead(200, { "content-type": "application/json" });
71
+ res.end(JSON.stringify(response));
72
+ } catch (e) {
73
+ res.writeHead(400, { "content-type": "application/json" });
74
+ res.end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
75
+ }
76
+ });
77
+ });
78
+ return new Promise((resolve) => server.listen(port, "127.0.0.1", () => resolve(server)));
79
+ }
80
+
81
+ // Pull from a remote peer over HTTP: WANT(my ids) -> verify -> admission -> ingest (§5).
82
+ export async function pullFromUrl(
83
+ peer: Peer,
84
+ baseUrl: string,
85
+ ): Promise<{ accepted: number; rejected: number }> {
86
+ const have = peer.reactor.arrivalLog().map((d) => d.id);
87
+ const res = await fetch(`${baseUrl}/rhz/v0/sync`, {
88
+ method: "POST",
89
+ headers: { "content-type": "application/json" },
90
+ body: JSON.stringify({ have }),
91
+ });
92
+ if (!res.ok) throw new Error(`sync failed: HTTP ${res.status}`);
93
+ const offer = (await res.json()) as SyncResponse;
94
+ let accepted = 0;
95
+ let rejected = 0;
96
+ // Reuse the local admission machinery by reconstructing deltas and handing them to the
97
+ // same ingest paths pullFrom uses: bundles atomically, loose individually.
98
+ for (const b of offer.bundles) {
99
+ const manifest = fromWire(b.manifest);
100
+ if (verifyDelta(manifest) !== "verified") {
101
+ rejected += 1 + b.members.length;
102
+ continue;
103
+ }
104
+ const members = b.members.map(fromWire);
105
+ if (!peer.admits(manifest) || !members.every((m) => peer.admits(m))) {
106
+ rejected += 1 + members.length;
107
+ continue;
108
+ }
109
+ const result = peer.reactor.ingestBundle(manifest, members);
110
+ if (result.status === "accepted") accepted += 1;
111
+ else if (result.status === "rejected") rejected += 1;
112
+ }
113
+ for (const w of offer.loose) {
114
+ const d = fromWire(w);
115
+ if (verifyDelta(d) !== "verified" || !peer.admits(d)) {
116
+ rejected += 1;
117
+ continue;
118
+ }
119
+ const result = peer.reactor.ingest(d);
120
+ if (result.status === "accepted") accepted += 1;
121
+ else if (result.status === "rejected") rejected += 1;
122
+ }
123
+ return { accepted, rejected };
124
+ }
package/src/hview.ts ADDED
@@ -0,0 +1,91 @@
1
+ // The HyperView: the output sort of group/expand/prune — SPEC-3 §4, encoded per ERRATA-2 E7/E11.
2
+ // Provenance-complete: every entry carries the full delta; expansion is view structure keyed by
3
+ // pointer index, never a mutation of the delta.
4
+
5
+ import { type CborValue, array, bool, encode, float, map, tstr } from "./cbor.js";
6
+ import { bytesToHex } from "./hash.js";
7
+ import type { Claims, Delta, Target } from "./types.js";
8
+
9
+ export interface HVEntry {
10
+ readonly delta: Delta;
11
+ // Annotate tag threaded through group from a mask(annotate) operand (E7).
12
+ readonly negated: boolean;
13
+ // expand replacements: pointer index (authored order) -> nested HView (E11).
14
+ readonly expanded?: ReadonlyMap<number, HView>;
15
+ }
16
+
17
+ export interface HView {
18
+ readonly id: string;
19
+ readonly props: ReadonlyMap<string, readonly HVEntry[]>;
20
+ }
21
+
22
+ function targetToCborWithExpansion(t: Target, expansion: HView | undefined): CborValue {
23
+ if (expansion !== undefined) return hviewToCbor(expansion);
24
+ switch (t.kind) {
25
+ case "primitive": {
26
+ const v = t.value;
27
+ if (typeof v === "string") return tstr(v);
28
+ if (typeof v === "boolean") return bool(v);
29
+ return float(v);
30
+ }
31
+ case "entity": {
32
+ const entries: Array<[string, CborValue]> = [["id", tstr(t.entity.id)]];
33
+ if (t.entity.context !== undefined) entries.push(["context", tstr(t.entity.context)]);
34
+ return map(entries);
35
+ }
36
+ case "delta": {
37
+ const entries: Array<[string, CborValue]> = [["delta", tstr(t.deltaRef.delta)]];
38
+ if (t.deltaRef.context !== undefined) entries.push(["context", tstr(t.deltaRef.context)]);
39
+ return map(entries);
40
+ }
41
+ }
42
+ }
43
+
44
+ // Claims rendered for an HVEntry: identical to the L1 canonical claims encoding, except that
45
+ // expanded pointer targets are replaced by nested HView maps (E11). Never used for hashing.
46
+ function claimsToCborWithExpansions(
47
+ claims: Claims,
48
+ expanded: ReadonlyMap<number, HView> | undefined,
49
+ ): CborValue {
50
+ return map([
51
+ ["author", tstr(claims.author)],
52
+ [
53
+ "pointers",
54
+ array(
55
+ claims.pointers.map((p, i) =>
56
+ map([
57
+ ["role", tstr(p.role)],
58
+ ["target", targetToCborWithExpansion(p.target, expanded?.get(i))],
59
+ ]),
60
+ ),
61
+ ),
62
+ ],
63
+ ["timestamp", float(claims.timestamp)],
64
+ ]);
65
+ }
66
+
67
+ export function hvEntryToCbor(e: HVEntry): CborValue {
68
+ const entries: Array<[string, CborValue]> = [
69
+ ["id", tstr(e.delta.id)],
70
+ ["claims", claimsToCborWithExpansions(e.delta.claims, e.expanded)],
71
+ ];
72
+ if (e.delta.sig !== undefined) entries.push(["sig", tstr(e.delta.sig)]);
73
+ if (e.negated) entries.push(["negated", bool(true)]);
74
+ return map(entries);
75
+ }
76
+
77
+ export function hviewToCbor(h: HView): CborValue {
78
+ const props: Array<[string, CborValue]> = [...h.props.entries()].map(([prop, entries]) => [
79
+ prop,
80
+ array(entries.map(hvEntryToCbor)),
81
+ ]);
82
+ return map([
83
+ ["id", tstr(h.id)],
84
+ ["props", map(props)],
85
+ ]);
86
+ }
87
+
88
+ // HyperViews are content-addressable (SPEC-3 §4): same (schema, DSet) => byte-identical form.
89
+ export function hviewCanonicalHex(h: HView): string {
90
+ return bytesToHex(encode(hviewToCbor(h)));
91
+ }