@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
@@ -0,0 +1,364 @@
1
+ // Parse the JSON term profile (ERRATA-2 E1) into Term/Pred. Strings are NFC-normalized at parse
2
+ // time so term-side comparisons are NFC-vs-NFC (data strings are NFC by validation, D11).
3
+
4
+ import type { GroupKey, MaskPolicy, SchemaRefT, Term } from "./eval.js";
5
+ import type { MergeFn, Order, Policy, PropPolicy } from "./policy.js";
6
+ import type { Cmp, EntityMatch, Hole, PPred, Pred, StrMatch, ValMatch } from "./pred.js";
7
+ import type { Primitive } from "./types.js";
8
+
9
+ const CMPS: readonly Cmp[] = ["eq", "neq", "lt", "lte", "gt", "gte", "prefix", "inSet"];
10
+
11
+ function nfc(s: string): string {
12
+ return s.normalize("NFC");
13
+ }
14
+
15
+ function asObject(x: unknown, what: string): Record<string, unknown> {
16
+ if (typeof x !== "object" || x === null || Array.isArray(x)) {
17
+ throw new Error(`expected object for ${what}`);
18
+ }
19
+ return x as Record<string, unknown>;
20
+ }
21
+
22
+ function parsePrimitive(v: unknown, what: string): Primitive {
23
+ if (typeof v === "string") return nfc(v);
24
+ if (typeof v === "boolean") return v;
25
+ if (typeof v === "number") {
26
+ if (!Number.isFinite(v)) throw new Error(`${what}: numeric constant must be finite`);
27
+ return v;
28
+ }
29
+ throw new Error(`${what}: constant must be string | number | boolean`);
30
+ }
31
+
32
+ // A hole in Const position: {"hole": "name"} (E15).
33
+ function parseHole(v: unknown): Hole | undefined {
34
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return undefined;
35
+ const h = (v as Record<string, unknown>)["hole"];
36
+ return typeof h === "string" ? { kind: "hole", name: nfc(h) } : undefined;
37
+ }
38
+
39
+ function parseParam(v: unknown, what: string): Primitive | Hole {
40
+ return parseHole(v) ?? parsePrimitive(v, what);
41
+ }
42
+
43
+ function parseCmp(v: unknown, what: string): Cmp {
44
+ if (typeof v !== "string" || !CMPS.includes(v as Cmp)) {
45
+ throw new Error(`${what}: unknown cmp ${String(v)}`);
46
+ }
47
+ return v as Cmp;
48
+ }
49
+
50
+ function parseStrMatch(raw: unknown, what: string): StrMatch {
51
+ const o = asObject(raw, what);
52
+ if (typeof o["exact"] === "string") return { kind: "exact", value: nfc(o["exact"]) };
53
+ if (typeof o["prefix"] === "string") return { kind: "prefix", value: nfc(o["prefix"]) };
54
+ if (Array.isArray(o["inSet"])) {
55
+ return {
56
+ kind: "inSet",
57
+ values: o["inSet"].map((s) => {
58
+ if (typeof s !== "string") throw new Error(`${what}: inSet members must be strings`);
59
+ return nfc(s);
60
+ }),
61
+ };
62
+ }
63
+ if (o["aliased"] !== undefined) {
64
+ const a = asObject(o["aliased"], `${what}.aliased`);
65
+ if (typeof a["name"] !== "string") throw new Error(`${what}: aliased.name must be a string`);
66
+ const out: { name: string; via?: string; trust?: Pred } = { name: nfc(a["name"]) };
67
+ if (a["via"] !== undefined) {
68
+ if (typeof a["via"] !== "string")
69
+ throw new Error(`${what}: aliased.via must be an entity id`);
70
+ out.via = nfc(a["via"]);
71
+ }
72
+ if (a["trust"] !== undefined) {
73
+ const trust = parsePred(a["trust"]);
74
+ assertClosedTrustPred(trust, `${what}.aliased.trust`);
75
+ out.trust = trust;
76
+ }
77
+ return { kind: "aliased", ...out };
78
+ }
79
+ throw new Error(`${what}: StrMatch must be exact | prefix | inSet | aliased`);
80
+ }
81
+
82
+ // An aliased trust predicate admits no holes and no nested aliased (SPEC-9 §4.1): it is
83
+ // evaluated against alias-vocabulary deltas during closure computation, outside the hole
84
+ // environment and outside any further expansion.
85
+ function assertClosedTrustPred(p: Pred, what: string): void {
86
+ switch (p.kind) {
87
+ case "true":
88
+ case "false":
89
+ return;
90
+ case "match":
91
+ if (typeof p.constant === "object" && !Array.isArray(p.constant)) {
92
+ throw new Error(`${what}: holes are not allowed inside an aliased trust predicate`);
93
+ }
94
+ return;
95
+ case "hasPointer": {
96
+ const pp = p.ppred;
97
+ if (
98
+ pp.targetEntity?.kind === "hole" ||
99
+ (pp.targetValue?.kind === "vcmp" && typeof pp.targetValue.value === "object")
100
+ ) {
101
+ throw new Error(`${what}: holes are not allowed inside an aliased trust predicate`);
102
+ }
103
+ if (pp.role?.kind === "aliased" || pp.context?.kind === "aliased") {
104
+ throw new Error(`${what}: nested aliased is not allowed inside an aliased trust predicate`);
105
+ }
106
+ return;
107
+ }
108
+ case "and":
109
+ case "or":
110
+ assertClosedTrustPred(p.left, what);
111
+ assertClosedTrustPred(p.right, what);
112
+ return;
113
+ case "not":
114
+ assertClosedTrustPred(p.pred, what);
115
+ return;
116
+ }
117
+ }
118
+
119
+ function parseValMatch(raw: unknown, what: string): ValMatch {
120
+ const o = asObject(raw, what);
121
+ if (o["vcmp"] !== undefined) {
122
+ const v = asObject(o["vcmp"], `${what}.vcmp`);
123
+ const cmp = parseCmp(v["cmp"], `${what}.vcmp`);
124
+ if (cmp === "inSet")
125
+ throw new Error(`${what}: vcmp cmp inSet is not allowed; use the inSet arm`);
126
+ const value = parseParam(v["value"], `${what}.vcmp`);
127
+ if (cmp === "prefix" && typeof value !== "string" && typeof value !== "object") {
128
+ throw new Error(`${what}: prefix requires a string constant`);
129
+ }
130
+ return { kind: "vcmp", cmp, value };
131
+ }
132
+ if (Array.isArray(o["between"])) {
133
+ if (o["between"].length !== 2) throw new Error(`${what}: between takes [lo, hi]`);
134
+ return {
135
+ kind: "between",
136
+ lo: parsePrimitive(o["between"][0], `${what}.between`),
137
+ hi: parsePrimitive(o["between"][1], `${what}.between`),
138
+ };
139
+ }
140
+ if (Array.isArray(o["inSet"])) {
141
+ return { kind: "inSet", values: o["inSet"].map((v) => parsePrimitive(v, `${what}.inSet`)) };
142
+ }
143
+ throw new Error(`${what}: ValMatch must be vcmp | between | inSet`);
144
+ }
145
+
146
+ function parsePPred(raw: unknown): PPred {
147
+ const o = asObject(raw, "hasPointer");
148
+ const out: {
149
+ role?: StrMatch;
150
+ targetEntity?: EntityMatch;
151
+ targetDelta?: string;
152
+ context?: StrMatch;
153
+ targetIsPrimitive?: boolean;
154
+ targetValue?: ValMatch;
155
+ } = {};
156
+ if (o["role"] !== undefined) out.role = parseStrMatch(o["role"], "hasPointer.role");
157
+ if (o["targetEntity"] !== undefined) {
158
+ const te = o["targetEntity"];
159
+ if (typeof te === "string") {
160
+ out.targetEntity = { kind: "const", id: nfc(te) };
161
+ } else {
162
+ const hole = parseHole(te);
163
+ if (hole !== undefined) {
164
+ out.targetEntity = hole;
165
+ } else {
166
+ const v = asObject(te, "targetEntity");
167
+ if (v["var"] !== "root") {
168
+ throw new Error('targetEntity must be a string, {var: "root"}, or {hole: "name"}');
169
+ }
170
+ out.targetEntity = { kind: "root" };
171
+ }
172
+ }
173
+ }
174
+ if (o["targetDelta"] !== undefined) {
175
+ if (typeof o["targetDelta"] !== "string") throw new Error("targetDelta must be a string");
176
+ out.targetDelta = o["targetDelta"];
177
+ }
178
+ if (o["context"] !== undefined) out.context = parseStrMatch(o["context"], "hasPointer.context");
179
+ if (o["targetIsPrimitive"] !== undefined) {
180
+ if (typeof o["targetIsPrimitive"] !== "boolean") {
181
+ throw new Error("targetIsPrimitive must be a boolean");
182
+ }
183
+ out.targetIsPrimitive = o["targetIsPrimitive"];
184
+ }
185
+ if (o["targetValue"] !== undefined) {
186
+ out.targetValue = parseValMatch(o["targetValue"], "hasPointer.targetValue");
187
+ }
188
+ if (Object.keys(out).length === 0) throw new Error("hasPointer requires at least one field (E1)");
189
+ return out;
190
+ }
191
+
192
+ export function parsePred(raw: unknown): Pred {
193
+ if (raw === "true") return { kind: "true" };
194
+ if (raw === "false") return { kind: "false" };
195
+ const o = asObject(raw, "pred");
196
+ if (o["match"] !== undefined) {
197
+ const m = asObject(o["match"], "match");
198
+ const field = m["field"];
199
+ if (field !== "author" && field !== "timestamp" && field !== "id") {
200
+ throw new Error(`match: unknown field ${String(field)}`);
201
+ }
202
+ const cmp = parseCmp(m["cmp"], "match");
203
+ const rawConst = m["const"];
204
+ const constant =
205
+ cmp === "inSet"
206
+ ? (() => {
207
+ if (!Array.isArray(rawConst)) throw new Error("match: inSet requires an array const");
208
+ return rawConst.map((v) => parsePrimitive(v, "match.const"));
209
+ })()
210
+ : parseParam(rawConst, "match.const");
211
+ if (cmp === "prefix" && typeof constant !== "string" && typeof constant !== "object") {
212
+ throw new Error("match: prefix requires a string const");
213
+ }
214
+ return { kind: "match", field, cmp, constant };
215
+ }
216
+ if (o["hasPointer"] !== undefined)
217
+ return { kind: "hasPointer", ppred: parsePPred(o["hasPointer"]) };
218
+ if (o["and"] !== undefined || o["or"] !== undefined) {
219
+ const key = o["and"] !== undefined ? "and" : "or";
220
+ const arr = o[key];
221
+ if (!Array.isArray(arr) || arr.length !== 2)
222
+ throw new Error(`${key} takes exactly [Pred, Pred] (E1)`);
223
+ const left = parsePred(arr[0]);
224
+ const right = parsePred(arr[1]);
225
+ return key === "and" ? { kind: "and", left, right } : { kind: "or", left, right };
226
+ }
227
+ if (o["not"] !== undefined) return { kind: "not", pred: parsePred(o["not"]) };
228
+ throw new Error("pred must be true | false | match | hasPointer | and | or | not");
229
+ }
230
+
231
+ function parseMaskPolicy(raw: unknown): MaskPolicy {
232
+ if (raw === "drop") return { kind: "drop" };
233
+ if (raw === "annotate") return { kind: "annotate" };
234
+ const o = asObject(raw, "mask.policy");
235
+ if (o["trust"] !== undefined) return { kind: "trust", pred: parsePred(o["trust"]) };
236
+ throw new Error("mask policy must be drop | annotate | {trust: Pred}");
237
+ }
238
+
239
+ const MERGE_FNS: readonly MergeFn[] = ["max", "min", "sum", "count", "and", "or", "concatSorted"];
240
+
241
+ function parseOrder(raw: unknown): Order {
242
+ if (raw === "lexById") return { kind: "lexById" };
243
+ const o = asObject(raw, "order");
244
+ if (o["byTimestamp"] !== undefined) {
245
+ if (o["byTimestamp"] !== "desc" && o["byTimestamp"] !== "asc") {
246
+ throw new Error("byTimestamp must be desc | asc");
247
+ }
248
+ return { kind: "byTimestamp", dir: o["byTimestamp"] };
249
+ }
250
+ if (Array.isArray(o["byAuthorRank"])) {
251
+ return {
252
+ kind: "byAuthorRank",
253
+ authors: o["byAuthorRank"].map((a) => {
254
+ if (typeof a !== "string") throw new Error("byAuthorRank entries must be strings");
255
+ return nfc(a);
256
+ }),
257
+ };
258
+ }
259
+ if (o["byPred"] !== undefined) {
260
+ const p = asObject(o["byPred"], "byPred");
261
+ return { kind: "byPred", pred: parsePred(p["pred"]), then: parseOrder(p["then"]) };
262
+ }
263
+ throw new Error("order must be lexById | byTimestamp | byAuthorRank | byPred");
264
+ }
265
+
266
+ function parsePropPolicy(raw: unknown): PropPolicy {
267
+ const o = asObject(raw, "propPolicy");
268
+ if (o["pick"] !== undefined) {
269
+ return { kind: "pick", order: parseOrder(asObject(o["pick"], "pick")["order"]) };
270
+ }
271
+ if (o["all"] !== undefined) {
272
+ return { kind: "all", order: parseOrder(asObject(o["all"], "all")["order"]) };
273
+ }
274
+ if (o["merge"] !== undefined) {
275
+ if (!MERGE_FNS.includes(o["merge"] as MergeFn)) {
276
+ throw new Error("unknown merge fn " + String(o["merge"]));
277
+ }
278
+ return { kind: "merge", fn: o["merge"] as MergeFn };
279
+ }
280
+ if (o["conflicts"] !== undefined) {
281
+ return { kind: "conflicts", order: parseOrder(asObject(o["conflicts"], "conflicts")["order"]) };
282
+ }
283
+ if (o["absentAs"] !== undefined) {
284
+ const a = asObject(o["absentAs"], "absentAs");
285
+ return {
286
+ kind: "absentAs",
287
+ constant: parsePrimitive(a["const"], "absentAs.const"),
288
+ then: parsePropPolicy(a["then"]),
289
+ };
290
+ }
291
+ throw new Error("propPolicy must be pick | all | merge | conflicts | absentAs");
292
+ }
293
+
294
+ export function parsePolicy(raw: unknown): Policy {
295
+ const o = asObject(raw, "policy");
296
+ const props = new Map<string, PropPolicy>();
297
+ if (o["props"] !== undefined) {
298
+ for (const [k, v] of Object.entries(asObject(o["props"], "policy.props"))) {
299
+ props.set(nfc(k), parsePropPolicy(v));
300
+ }
301
+ }
302
+ return { props, default: parsePropPolicy(o["default"]) };
303
+ }
304
+
305
+ function parseGroupKey(raw: unknown): GroupKey {
306
+ if (raw === "byTargetContext") return { kind: "byTargetContext" };
307
+ if (raw === "byRole") return { kind: "byRole" };
308
+ const o = asObject(raw, "group.key");
309
+ if (typeof o["const"] === "string") return { kind: "const", prop: nfc(o["const"]) };
310
+ throw new Error("group key must be byTargetContext | byRole | {const: string}");
311
+ }
312
+
313
+ function parseSchemaRef(raw: unknown): SchemaRefT {
314
+ if (typeof raw === "string") return { kind: "name", name: nfc(raw) };
315
+ const o = asObject(raw, "schemaRef");
316
+ if (typeof o["pinned"] === "string") return { kind: "pinned", hash: o["pinned"] };
317
+ throw new Error("schema ref must be a name string or {pinned: hash} (E13)");
318
+ }
319
+
320
+ export function parseTerm(raw: unknown): Term {
321
+ if (raw === "input") return { kind: "input" };
322
+ const o = asObject(raw, "term");
323
+ switch (o["op"]) {
324
+ case "select":
325
+ return { kind: "select", pred: parsePred(o["pred"]), of: parseTerm(o["in"]) };
326
+ case "union":
327
+ return { kind: "union", left: parseTerm(o["left"]), right: parseTerm(o["right"]) };
328
+ case "mask":
329
+ return { kind: "mask", policy: parseMaskPolicy(o["policy"]), of: parseTerm(o["in"]) };
330
+ case "group":
331
+ return { kind: "group", key: parseGroupKey(o["key"]), of: parseTerm(o["in"]) };
332
+ case "expand": {
333
+ return {
334
+ kind: "expand",
335
+ role: parseStrMatch(o["role"], "expand.role"),
336
+ schema: parseSchemaRef(o["schema"]),
337
+ of: parseTerm(o["in"]),
338
+ };
339
+ }
340
+ case "fix": {
341
+ if (typeof o["entity"] !== "string") throw new Error("fix.entity must be a string");
342
+ const fix = {
343
+ kind: "fix" as const,
344
+ schema: parseSchemaRef(o["schema"]),
345
+ entity: nfc(o["entity"]),
346
+ };
347
+ if (o["bindings"] === undefined) return fix;
348
+ const bo = asObject(o["bindings"], "fix.bindings");
349
+ const bindings = new Map<string, Primitive>();
350
+ for (const key of Object.keys(bo).sort()) {
351
+ bindings.set(nfc(key), parsePrimitive(bo[key], `fix.bindings.${key}`));
352
+ }
353
+ return { ...fix, bindings };
354
+ }
355
+ case "resolve":
356
+ return { kind: "resolve", policy: parsePolicy(o["policy"]), of: parseTerm(o["in"]) };
357
+ case "prune": {
358
+ const keep = o["keep"] === "all" ? "all" : parseStrMatch(o["keep"], "prune.keep");
359
+ return { kind: "prune", keep, of: parseTerm(o["in"]) };
360
+ }
361
+ default:
362
+ throw new Error(`unknown term op ${String(o["op"])}`);
363
+ }
364
+ }
package/src/types.ts ADDED
@@ -0,0 +1,38 @@
1
+ // The delta data model (SPEC-1 §2). Bytes are never stored here — these are the logical
2
+ // structures; canonical encoding lives in cbor.ts / delta.ts.
3
+
4
+ export type Primitive = string | number | boolean;
5
+
6
+ export interface EntityRef {
7
+ readonly id: string;
8
+ readonly context?: string;
9
+ }
10
+
11
+ export interface DeltaRef {
12
+ readonly delta: string; // content address (multihash hex) of another delta
13
+ readonly context?: string;
14
+ }
15
+
16
+ // A pointer's target is exactly one of: a primitive value, an entity reference, or a delta
17
+ // reference. The three are kept structurally distinct (SPEC-1 §2.1, ERRATA D5).
18
+ export type Target =
19
+ | { readonly kind: "primitive"; readonly value: Primitive }
20
+ | { readonly kind: "entity"; readonly entity: EntityRef }
21
+ | { readonly kind: "delta"; readonly deltaRef: DeltaRef };
22
+
23
+ export interface Pointer {
24
+ readonly role: string;
25
+ readonly target: Target;
26
+ }
27
+
28
+ export interface Claims {
29
+ readonly timestamp: number; // ms since Unix epoch; a CLAIM, not an authority (SPEC-1 §6)
30
+ readonly author: string; // public key or fingerprint (SPEC-1 §5)
31
+ readonly pointers: readonly Pointer[]; // 1 or more (SPEC-1 §2.1)
32
+ }
33
+
34
+ export interface Delta {
35
+ readonly id: string; // content-derived; "1e20" + hex(blake3-256(canonical_cbor(claims)))
36
+ readonly claims: Claims;
37
+ readonly sig?: string; // detached signature over id (SPEC-1 §5); absent in M0.1
38
+ }
package/src/vocab.ts ADDED
@@ -0,0 +1,3 @@
1
+ // The reserved vocabulary namespace (decided 2026-06-11). One configurable constant, so any
2
+ // future change is a one-line edit plus a vector regen. Leaf module: importable from anywhere.
3
+ export const VOCAB_PREFIX = "rhizomatic";