@cruba/crubal 0.0.0-stage → 0.2.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 (99) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +97 -2
  3. package/dist/bin/crubal.d.ts +2 -0
  4. package/dist/bin/crubal.js +103 -0
  5. package/dist/bin/crubal.js.map +1 -0
  6. package/dist/src/access.d.ts +126 -0
  7. package/dist/src/access.js +517 -0
  8. package/dist/src/access.js.map +1 -0
  9. package/dist/src/arrival.d.ts +55 -0
  10. package/dist/src/arrival.js +124 -0
  11. package/dist/src/arrival.js.map +1 -0
  12. package/dist/src/browser.d.ts +10 -0
  13. package/dist/src/browser.js +13 -0
  14. package/dist/src/browser.js.map +1 -0
  15. package/dist/src/bundle.d.ts +66 -0
  16. package/dist/src/bundle.js +753 -0
  17. package/dist/src/bundle.js.map +1 -0
  18. package/dist/src/canonical.d.ts +6 -0
  19. package/dist/src/canonical.js +47 -0
  20. package/dist/src/canonical.js.map +1 -0
  21. package/dist/src/conditions.d.ts +28 -0
  22. package/dist/src/conditions.js +216 -0
  23. package/dist/src/conditions.js.map +1 -0
  24. package/dist/src/corpus.d.ts +8 -0
  25. package/dist/src/corpus.js +1207 -0
  26. package/dist/src/corpus.js.map +1 -0
  27. package/dist/src/defs.d.ts +151 -0
  28. package/dist/src/defs.js +611 -0
  29. package/dist/src/defs.js.map +1 -0
  30. package/dist/src/dictionary.d.ts +65 -0
  31. package/dist/src/dictionary.js +557 -0
  32. package/dist/src/dictionary.js.map +1 -0
  33. package/dist/src/exchange.d.ts +236 -0
  34. package/dist/src/exchange.js +1035 -0
  35. package/dist/src/exchange.js.map +1 -0
  36. package/dist/src/forms.d.ts +16 -0
  37. package/dist/src/forms.js +169 -0
  38. package/dist/src/forms.js.map +1 -0
  39. package/dist/src/index.d.ts +27 -0
  40. package/dist/src/index.js +29 -0
  41. package/dist/src/index.js.map +1 -0
  42. package/dist/src/lex.d.ts +21 -0
  43. package/dist/src/lex.js +142 -0
  44. package/dist/src/lex.js.map +1 -0
  45. package/dist/src/lifecycle.d.ts +84 -0
  46. package/dist/src/lifecycle.js +415 -0
  47. package/dist/src/lifecycle.js.map +1 -0
  48. package/dist/src/log.d.ts +78 -0
  49. package/dist/src/log.js +506 -0
  50. package/dist/src/log.js.map +1 -0
  51. package/dist/src/migrate.d.ts +111 -0
  52. package/dist/src/migrate.js +286 -0
  53. package/dist/src/migrate.js.map +1 -0
  54. package/dist/src/model.d.ts +68 -0
  55. package/dist/src/model.js +188 -0
  56. package/dist/src/model.js.map +1 -0
  57. package/dist/src/packages.d.ts +69 -0
  58. package/dist/src/packages.js +232 -0
  59. package/dist/src/packages.js.map +1 -0
  60. package/dist/src/print-text.d.ts +7 -0
  61. package/dist/src/print-text.js +216 -0
  62. package/dist/src/print-text.js.map +1 -0
  63. package/dist/src/publish.d.ts +27 -0
  64. package/dist/src/publish.js +275 -0
  65. package/dist/src/publish.js.map +1 -0
  66. package/dist/src/read-text.d.ts +10 -0
  67. package/dist/src/read-text.js +385 -0
  68. package/dist/src/read-text.js.map +1 -0
  69. package/dist/src/records.d.ts +31 -0
  70. package/dist/src/records.js +72 -0
  71. package/dist/src/records.js.map +1 -0
  72. package/dist/src/review.d.ts +22 -0
  73. package/dist/src/review.js +198 -0
  74. package/dist/src/review.js.map +1 -0
  75. package/dist/src/rules.d.ts +96 -0
  76. package/dist/src/rules.js +611 -0
  77. package/dist/src/rules.js.map +1 -0
  78. package/dist/src/schedule.d.ts +24 -0
  79. package/dist/src/schedule.js +126 -0
  80. package/dist/src/schedule.js.map +1 -0
  81. package/dist/src/std-packages.d.ts +9 -0
  82. package/dist/src/std-packages.js +268 -0
  83. package/dist/src/std-packages.js.map +1 -0
  84. package/dist/src/store.d.ts +164 -0
  85. package/dist/src/store.js +538 -0
  86. package/dist/src/store.js.map +1 -0
  87. package/dist/src/tree.d.ts +35 -0
  88. package/dist/src/tree.js +159 -0
  89. package/dist/src/tree.js.map +1 -0
  90. package/dist/src/units-table.d.ts +1 -0
  91. package/dist/src/units-table.js +200 -0
  92. package/dist/src/units-table.js.map +1 -0
  93. package/dist/src/units.d.ts +42 -0
  94. package/dist/src/units.js +98 -0
  95. package/dist/src/units.js.map +1 -0
  96. package/dist/src/views.d.ts +216 -0
  97. package/dist/src/views.js +761 -0
  98. package/dist/src/views.js.map +1 -0
  99. package/package.json +60 -4
@@ -0,0 +1,1035 @@
1
+ // exchange.ts: the Exchange level (spec chapters 7, 9 at borders, 11). Contracts are checked at publish
2
+ // (7.10) and run in seven steps (7.4): trigger and permission, needs, map out, act, map back, rules,
3
+ // record. Runs are idempotent (7.5). Offer and take share between owners, and only the overlap flows
4
+ // (7.8). Data crossing a border is verified against the presenter's published key (8.5.1).
5
+ import { arrive } from "./arrival.js";
6
+ import { serialize } from "./canonical.js";
7
+ import { evaluate, parseCondition } from "./conditions.js";
8
+ import { MemoryRecords } from "./records.js";
9
+ import { Engine } from "./rules.js";
10
+ import { isSchedule, readSchedule, ticksDue } from "./schedule.js";
11
+ import { fieldPath, land, receive, sha256, verifyChain, proposeSplit } from "./store.js";
12
+ import { CrubalError, readTree } from "./tree.js";
13
+ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
14
+ export function readRoles(source) {
15
+ const out = [];
16
+ for (const node of readTree(source)) {
17
+ if (node.kind !== "pair" || node.key !== "role")
18
+ continue;
19
+ const role = { name: node.value.trim(), audience: "member", may: {}, line: node.line };
20
+ for (const c of node.children) {
21
+ if (c.key === "audience")
22
+ role.audience = c.value;
23
+ if (c.key === "may")
24
+ for (const g of c.children)
25
+ if (g.key)
26
+ role.may[g.key] = g.value.split(",").map((s) => s.trim());
27
+ }
28
+ out.push(role);
29
+ }
30
+ return out;
31
+ }
32
+ /**
33
+ * What a caller is, for `who` (7.6): the classes it wears, its roles' grants, and the menu items it holds
34
+ * on the record by its edges (14.6) when the run has access to consult. A caller with no record of its own
35
+ * here (a hosted person, 14.8) is still what its edges make it.
36
+ */
37
+ function callerStates(caller, roles, held, asked = []) {
38
+ return (path) => {
39
+ const s = new Set();
40
+ if (path !== "caller")
41
+ return s;
42
+ if (held)
43
+ for (const q of asked)
44
+ if (held(q.slice(0, q.indexOf(" ")), q.slice(q.indexOf(" ") + 1)))
45
+ s.add(`may:${q}`);
46
+ if (!caller)
47
+ return s;
48
+ for (const w of caller.wears ?? [])
49
+ s.add(`wears:${w}`);
50
+ const names = (Array.isArray(caller.values.roles) ? caller.values.roles : caller.values.roles ? [caller.values.roles] : []).map((r) => (isObj(r) && "$link" in r ? String(r.$link) : String(r)));
51
+ for (const r of roles.filter((x) => names.includes(x.name))) {
52
+ for (const [verb, targets] of Object.entries(r.may))
53
+ for (const t of targets)
54
+ s.add(`may:${verb} ${t}`);
55
+ }
56
+ return s;
57
+ };
58
+ }
59
+ // ─── Publish checks (7.10) ───────────────────────────────────────────────────
60
+ const PARTS = {
61
+ send: ["needs", "gives", "writes", "reads", "stream", "to", "when", "missed", "who", "audience", "auth", "scope", "credential", "retry", "undo", "for", "label", "describe", "version"],
62
+ receive: ["needs", "gives", "reads", "when", "missed", "who", "audience", "origin", "lands", "evidence", "absorb", "ignore", "keep-raw", "prefix", "ref", "relation", "auth", "scope", "credential", "retry", "undo", "for", "label", "describe", "version"],
63
+ store: ["needs", "stores", "origin", "when", "who", "audience", "for", "label", "describe", "version"],
64
+ form: ["needs", "gives", "writes", "reads", "when", "who", "audience", "origin", "lands", "ref", "relation", "for", "label", "describe", "version"],
65
+ offer: ["needs", "gives", "to", "mode", "terms", "for", "when", "who", "audience", "label", "describe", "version"],
66
+ take: ["needs", "from", "offer", "reads", "mode", "lands", "origin", "absorb", "ignore", "keep-raw", "prefix", "ref", "when", "who", "audience", "for", "label", "describe", "version"],
67
+ };
68
+ const AUDIENCES = ["public", "member", "staff", "owner"];
69
+ /**
70
+ * The lowest audience that may run a contract (7.2): what it says; else public for one that grants the
71
+ * consumer's edge (a sign-up, a first order: how one becomes a member, 14.4), member for any other form,
72
+ * public otherwise.
73
+ */
74
+ const audienceOf = (c) => c.parts.audience ?? (c.kind === "form" && !c.parts.relation ? "member" : "public");
75
+ function ourKey(address) {
76
+ return address.replace(/^\w+::/, "").split(/[.[]/)[0];
77
+ }
78
+ /** Check contracts against a dictionary (7.10). Throws the first problem. `views` lets stream contracts be checked. */
79
+ export function checkContracts(contracts, dict, ownPrefix = "", views) {
80
+ const names = new Set(contracts.map((c) => c.name));
81
+ for (const c of contracts) {
82
+ const allowed = PARTS[c.kind];
83
+ if (!allowed)
84
+ throw new CrubalError(`unknown contract kind ${c.kind}`, c.line);
85
+ if (c.parts.audience && !AUDIENCES.includes(c.parts.audience))
86
+ throw new CrubalError(`${c.name}: audience is public, member, staff or owner, not ${c.parts.audience}`, c.partLines.audience ?? c.line);
87
+ for (const [part, line] of Object.entries(c.partLines)) {
88
+ if (part === "kind" || part.includes(":"))
89
+ continue;
90
+ if (!allowed.includes(part))
91
+ throw new CrubalError(`a ${c.kind} contract has no part ${part}`, line);
92
+ }
93
+ // A stream contract (12.6.2) carries projection entries: nothing is mapped and nothing is needed.
94
+ if (c.stream.length) {
95
+ if (c.writes.length || c.needs.length)
96
+ throw new CrubalError(`${c.name} carries a stream, so it has no writes and no needs`, c.partLines.stream);
97
+ if (!c.parts.to)
98
+ throw new CrubalError(`${c.name} carries a stream, so it names who it goes to`, c.partLines.stream);
99
+ if (views)
100
+ for (const v of c.stream)
101
+ if (!views.has(v))
102
+ throw new CrubalError(`${c.name} streams ${v}, which isn't a view`, c.partLines.stream);
103
+ }
104
+ for (const l of c.needs)
105
+ if (!dict.facets.has(l.facet))
106
+ throw new CrubalError(`${l.facet} is not a defined facet`, l.line);
107
+ const sides = [
108
+ ...c.writes.filter((m) => !m.from.startsWith('"')).map((m) => ({ address: m.from, line: m.line, values: m.values.map(([ours]) => ours), valueLines: m.valueLines, mayBeUnknown: false })),
109
+ ...c.reads.map((m) => ({ address: m.to, line: m.line, values: m.values.map(([, ours]) => ours), valueLines: m.valueLines, mayBeUnknown: true })),
110
+ ];
111
+ for (const s of sides) {
112
+ const key = ourKey(s.address);
113
+ const f = dict.facets.get(key);
114
+ const foreign = key.includes(":") && !(ownPrefix && key.startsWith(`${ownPrefix}:`));
115
+ if (!f) {
116
+ if (s.mayBeUnknown && foreign)
117
+ continue;
118
+ throw new CrubalError(`${key} is not a defined facet`, s.line);
119
+ }
120
+ s.values.forEach((v, i) => {
121
+ if (f.type !== "choice" || v === "-")
122
+ return;
123
+ if ((f.values ?? []).some((x) => x.value === v))
124
+ return;
125
+ const of = (f.values ?? []).find((x) => x.aliases.includes(v));
126
+ throw new CrubalError(of ? `${v} is an alias of ${of.value}; a value map must name the real value` : `${v} is not a value of ${key}`, s.valueLines[i] ?? s.line);
127
+ });
128
+ }
129
+ // `when` is a condition, `asked` (a caller starts it), `entry` (a stream contract runs as its views
130
+ // publish) or a schedule (7.11).
131
+ if (c.who)
132
+ parseCondition(c.who, c.line);
133
+ if (c.when && c.when !== "asked" && c.when !== "entry") {
134
+ if (isSchedule(c.when))
135
+ readSchedule(c.when, c.partLines.when);
136
+ else
137
+ parseCondition(c.when, c.line);
138
+ }
139
+ if (c.when === "entry" && !c.stream.length)
140
+ throw new CrubalError(`${c.name} runs on entry but carries no stream`, c.partLines.when);
141
+ const missed = c.parts.missed;
142
+ if (missed && !["once", "all", "skip"].includes(missed))
143
+ throw new CrubalError(`${c.name}: missed is once, all or skip`, c.partLines.missed);
144
+ if (missed && !isSchedule(c.when))
145
+ throw new CrubalError(`${c.name}: missed belongs to a scheduled contract`, c.partLines.missed);
146
+ if (c.undo && !names.has(c.undo))
147
+ throw new CrubalError(`${c.name}'s undo names ${c.undo}, which isn't a contract here`, c.partLines.undo);
148
+ const cred = c.parts.credential;
149
+ if (cred && /[^A-Za-z0-9._-]/.test(cred))
150
+ throw new CrubalError(`${c.name}'s credential must be a name, never a secret`, c.partLines.credential);
151
+ }
152
+ }
153
+ /** The plain text a value is sent as: an amount for money and quantities, a target for links. */
154
+ function wire(v) {
155
+ if (isObj(v)) {
156
+ if ("$value" in v)
157
+ return wire(v.$value);
158
+ if ("$money" in v)
159
+ return String(v.$money);
160
+ if ("$qty" in v)
161
+ return String(v.$qty);
162
+ if ("$link" in v)
163
+ return String(v.$link);
164
+ if ("$image" in v)
165
+ return String(v.$image);
166
+ if ("$empty" in v)
167
+ return "";
168
+ }
169
+ if (Array.isArray(v))
170
+ return v.map(wire);
171
+ return v;
172
+ }
173
+ function put(payload, path, value) {
174
+ const names = fieldPath(path);
175
+ let at = payload;
176
+ names.slice(0, -1).forEach((n) => {
177
+ if (!isObj(at[n]))
178
+ at[n] = {};
179
+ at = at[n];
180
+ });
181
+ at[names.at(-1)] = value;
182
+ }
183
+ export class Exchange {
184
+ dict;
185
+ contracts;
186
+ roles;
187
+ /** The records, shared with the Access that decides over them (chapter 14). */
188
+ records = new MemoryRecords();
189
+ bindings = new Map();
190
+ /** Set to make the border consult access: `run` needs the caller's `run` item, gated access suggests. */
191
+ access;
192
+ /**
193
+ * How an arrival's new record is named (9.2a). A persistent store sets this to mint the next id of the
194
+ * owner (`P-2` after `P-1`), since an Exchange is built per run and its own counter would start again.
195
+ * Without it the memory border's `<L>-NEW<n>` stands.
196
+ */
197
+ mint;
198
+ /** Set to make every run a transaction (8.5): the border works on the journal's draft and commits each run. */
199
+ journal;
200
+ /** Set to let stream contracts (12.6.2) carry the views' projection streams. */
201
+ views;
202
+ /** Per stream contract, the cursor each view's index has acknowledged, with the version it was for. */
203
+ cursors = new Map();
204
+ /** The rules a run's step 6 applies (7.4): the package's, plus the facet shortcuts the engine adds. */
205
+ engine;
206
+ /** Idempotency keys acted on (7.5); a store that outlives the instance seeds and keeps them. */
207
+ sent = new Set();
208
+ /** Runs so far, which numbers the next run's log; a store continues the count across instances. */
209
+ runs = 0;
210
+ /** Records minted by arrivals (9.2a), likewise. */
211
+ minted = 0;
212
+ /** The versions a run uses (7.4 step 7, 8.5): the package's, set by a store; the contract's is added per run. */
213
+ versions = {};
214
+ /** The review queue the border writes to (a dead run, an answer naming no run); the journal's draft list. */
215
+ review = [];
216
+ /** Observations this instance made (9.7); a store keeps every one in its state. */
217
+ observations = [];
218
+ observed = 0;
219
+ /** The package's thresholds (11.1): instances to propose a version split, witnesses to confirm. */
220
+ thresholds = {};
221
+ /** Review items already open in the store, for a border that loads them apart from the draft's list. */
222
+ knownReview = [];
223
+ /** Mapped-out payloads kept beside the log, by run (docs/design/runs.md): a store keeps them in outtake. */
224
+ outtake = new Map();
225
+ /** Runs the store knows beyond this instance's entries (a border over a store seeds them): run → contract and record. */
226
+ runsKnown = new Map();
227
+ /** Runs started and not yet acted or failed, by run: what a restarted host asks the adapter about. */
228
+ started = new Map();
229
+ constructor(dict, contracts, roles = [], rules = []) {
230
+ this.dict = dict;
231
+ this.contracts = new Map(contracts.map((c) => [c.name, c]));
232
+ this.roles = roles;
233
+ this.engine = new Engine(dict, rules, () => this.records);
234
+ }
235
+ /**
236
+ * Step 6 of a run (7.4): what came in runs through the rules (chapter 6). A `must` the write breaks
237
+ * refuses it and nothing changes; a hold takes the value out of the current values; a fill or derivation
238
+ * lands with `origin: derived` and the rule as its `via`. Returns the record to keep, or the refusal.
239
+ */
240
+ applyRules(landed, touched) {
241
+ const results = this.engine.write({ ...landed, values: touched });
242
+ const refused = results.find((r) => r.result === "refused");
243
+ if (refused)
244
+ return { refused };
245
+ const after = this.engine.get(landed.id);
246
+ if (!after)
247
+ return { record: landed, summary: {} };
248
+ const values = { ...landed.values };
249
+ const sources = { ...(landed.sources ?? {}) };
250
+ const summary = {};
251
+ const held = { ...(landed.held ?? {}) };
252
+ for (const r of results) {
253
+ if ((r.result === "filled" || r.result === "derived") && r.address && r.value !== undefined && r.rule) {
254
+ values[r.address] = r.value;
255
+ sources[r.address] = { origin: "derived", via: r.rule };
256
+ const bag = (summary[r.result] ?? {});
257
+ bag[r.address] = r.value;
258
+ summary[r.result] = bag;
259
+ }
260
+ if (r.result === "held" && r.address && r.rule) {
261
+ const v = values[r.address];
262
+ if (v !== undefined) {
263
+ const src = sources[r.address];
264
+ held[r.address] = { value: v, rule: r.rule, ...(src ? { source: src } : {}) };
265
+ delete values[r.address];
266
+ delete sources[r.address];
267
+ }
268
+ summary.held = [...(summary.held ?? []), r.address];
269
+ }
270
+ if (r.result === "released" && r.address && held[r.address]) {
271
+ // The held value comes back as it was (6.7).
272
+ const h = held[r.address];
273
+ values[r.address] = h.value;
274
+ if (isObj(h.source))
275
+ sources[r.address] = h.source;
276
+ delete held[r.address];
277
+ summary.released = [...(summary.released ?? []), r.address];
278
+ }
279
+ if (r.result === "flagged" && r.rule)
280
+ summary.flagged = [...(summary.flagged ?? []), r.rule];
281
+ }
282
+ // The held bag is rebuilt from what stayed held: a released key leaves it.
283
+ const { held: _was, ...base } = landed;
284
+ const record = { ...base, values, sources, ...(Object.keys(held).length ? { held } : {}) };
285
+ this.engine.put(record);
286
+ // 6b. What the ancestors derive from below is recomputed now (6.4), nearest first, each landing before
287
+ // the next reads it; the record itself is in the set first, so a rollup over it sees this write.
288
+ if (this.engine.readsLowerLevels) {
289
+ this.records.set(record.id, record);
290
+ const recomputed = this.reproduceAncestors(record.id);
291
+ if (recomputed.length)
292
+ summary.recomputed = recomputed;
293
+ }
294
+ return { record, summary };
295
+ }
296
+ /** Run the ancestors' productions again after a write below them; the ids whose values changed. */
297
+ reproduceAncestors(id) {
298
+ const changed = [];
299
+ for (const upId of this.engine.ancestorsOf(id)) {
300
+ const up = this.records.get(upId);
301
+ if (!up)
302
+ break;
303
+ this.engine.put(up);
304
+ const derived = this.engine.reproduce(upId).filter((r) => (r.result === "derived" || r.result === "filled") && r.address && r.value !== undefined && r.rule);
305
+ if (!derived.length)
306
+ continue;
307
+ const values = { ...up.values };
308
+ const sources = { ...(up.sources ?? {}) };
309
+ let moved = false;
310
+ for (const r of derived) {
311
+ const address = r.address;
312
+ if (serialize(values[address] ?? "∅") === serialize(r.value))
313
+ continue;
314
+ values[address] = r.value;
315
+ sources[address] = { origin: "derived", via: r.rule };
316
+ moved = true;
317
+ }
318
+ if (!moved)
319
+ continue;
320
+ const next = { ...up, values, sources };
321
+ this.records.set(upId, next);
322
+ this.engine.put(next);
323
+ changed.push(upId);
324
+ }
325
+ return changed;
326
+ }
327
+ /** Work on a journal's draft, so every run the border makes is one logged entry (8.5). */
328
+ useJournal(journal) {
329
+ this.journal = journal;
330
+ for (const [id, r] of this.records)
331
+ journal.draft.records.set(id, r);
332
+ for (const [k, v] of this.bindings)
333
+ journal.draft.bindings.set(k, v);
334
+ this.records = journal.draft.records;
335
+ this.bindings = journal.draft.bindings;
336
+ if (this.access) {
337
+ for (const [k, e] of this.access.edges)
338
+ journal.draft.edges.set(k, e);
339
+ this.access.edges = journal.draft.edges;
340
+ }
341
+ this.review = journal.draft.review;
342
+ }
343
+ /** Log a run's transaction: what it changed on the draft, the run's own state, and any observations (8.5). */
344
+ commit(by, log, run, more = []) {
345
+ this.journal?.commit(by, () => { }, `2026-01-01T00:00:${String(this.runs).padStart(2, "0")}Z`, [{ run: log, ...run }, ...more]);
346
+ }
347
+ /** Step 1 of a run (7.4): the menu is the permission (14.6); `who` only narrows. */
348
+ permitted(c, recordId, callerId, arrival = false) {
349
+ // A run needs `run` on the record (14.6); an arrival has no record yet, so it needs `run` anywhere by an
350
+ // admin or control edge, or to be an admitted member taking part (14.4), on the parent when it has one.
351
+ const may = (verb, target) => {
352
+ if (!this.access || !callerId)
353
+ return true;
354
+ if (!arrival || recordId)
355
+ return this.access.may(callerId, verb, target, recordId);
356
+ return this.access.mayAnywhere(callerId, verb, target) || (verb === "run" && (audienceOf(c) === "public" || this.access.audienceOf(callerId) !== "public"));
357
+ };
358
+ if (this.access && callerId && !may("run", c.name))
359
+ return "permission";
360
+ // The contract's audience (7.2, 14.4): consume opens only what the package opens to consumers.
361
+ if (this.access && callerId && AUDIENCES.indexOf(this.access.audienceOf(callerId)) < AUDIENCES.indexOf(audienceOf(c)))
362
+ return "permission";
363
+ if (c.who) {
364
+ const caller = callerId ? this.records.get(callerId) : undefined;
365
+ // `caller may edit Deployment`: held by the caller's edges and the menu on this record (14.6).
366
+ const asked = [...c.who.matchAll(/\bcaller may (\w+) (\S+)/g)].map((m) => `${m[1]} ${m[2]}`);
367
+ const held = this.access && callerId ? (verb, target) => may(verb, target) : undefined;
368
+ const ok = evaluate(parseCondition(c.who), caller ? { ...caller.values } : {}, callerStates(caller, this.roles, held, asked));
369
+ if (ok !== true)
370
+ return "who";
371
+ }
372
+ return null;
373
+ }
374
+ /** The gated edge a caller's access comes through, if every granting edge is gated (14.7). */
375
+ gate(recordId, callerId) {
376
+ return this.access && callerId ? this.access.gated(callerId, recordId) : null;
377
+ }
378
+ /**
379
+ * A contract that creates a record for a caller grants their edge on it (14.4): `mode: consume`, the
380
+ * contract's `relation`, under the owner's grant.
381
+ */
382
+ grantConsumer(c, recordId, callerId) {
383
+ const relation = c.parts.relation;
384
+ if (!this.access || !relation)
385
+ return null;
386
+ const edge = { edge: `E-${c.name}-${recordId}`, who: callerId, relation, mode: "consume", on: recordId, grants: [], by: "owner", line: c.line };
387
+ this.access.edges.set(edge.edge, edge);
388
+ return edge;
389
+ }
390
+ put(r) {
391
+ this.records.set(r.id, r);
392
+ this.engine.put(r);
393
+ }
394
+ get(id) {
395
+ return this.records.get(id);
396
+ }
397
+ /** A record's values with everything above it read through (1.7). */
398
+ effective(r) {
399
+ const up = r.of ? this.records.get(r.of) : undefined;
400
+ return { ...(up ? this.effective(up) : {}), ...r.values };
401
+ }
402
+ /** The value an address names from a record, with a level prefix reading that level. */
403
+ read(r, address) {
404
+ const m = address.match(/^(\w+)::(.+)$/);
405
+ // A level prefix reads the record at that level: the run's record itself, when it sits there.
406
+ if (m)
407
+ return r.level === m[1] ? this.effective(r)[m[2]] : undefined;
408
+ return this.effective(r)[address];
409
+ }
410
+ nextLog() {
411
+ this.runs += 1;
412
+ return `L-R${this.runs}`;
413
+ }
414
+ /**
415
+ * A send (or form) contract's run with no act between (the memory border, docs/design/runs.md): prepare,
416
+ * then acted, in two entries. A host puts its adapter between `prepare` and `acted` or `failed`.
417
+ */
418
+ run(name, recordId, callerId) {
419
+ const p = this.prepare(name, recordId, callerId);
420
+ if (p.started !== true)
421
+ return p;
422
+ this.acted(String(p.log));
423
+ const { started: _s, ...rest } = p;
424
+ return { ...rest, acted: true };
425
+ }
426
+ /**
427
+ * Steps 1–3 of a send (7.4): permission, needs, map out; then the run is `started`, its idempotency key
428
+ * and the payload's hash logged, the payload itself kept beside the log. A key already started or acted
429
+ * is never sent again (7.5).
430
+ */
431
+ prepare(name, recordId, callerId, cause) {
432
+ const c = this.contracts.get(name);
433
+ if (!c)
434
+ throw new Error(`no contract ${name}`);
435
+ const r = this.records.get(recordId);
436
+ if (!r)
437
+ throw new Error(`no record ${recordId}`);
438
+ const head = { contract: name, record: recordId, ...(callerId ? { caller: callerId } : {}) };
439
+ const log = this.nextLog();
440
+ // 1. Permission and trigger.
441
+ const stopped = this.permitted(c, recordId, callerId);
442
+ if (stopped)
443
+ return { ...head, stopped };
444
+ // 2. Needs: an unnamed class over the record.
445
+ const missing = [];
446
+ const unfit = [];
447
+ for (const l of this.needsOf(c)) {
448
+ const v = this.read(r, l.level ? `${l.level}::${l.facet}` : l.facet);
449
+ const empty = v === undefined || (isObj(v) && "$empty" in v);
450
+ if (l.required && empty)
451
+ missing.push(l.facet);
452
+ if (!empty && l.values && typeof v === "string" && !l.values.includes(v))
453
+ unfit.push(l.facet);
454
+ if (!empty && l.length !== undefined && typeof v === "string" && [...v].length > l.length)
455
+ unfit.push(l.facet);
456
+ }
457
+ if (missing.length || unfit.length)
458
+ return { ...head, stopped: "needs", ...(missing.length ? { missing } : {}), ...(unfit.length ? { unfit } : {}) };
459
+ if (c.kind === "receive" || c.kind === "take")
460
+ return { ...head, ready: true, log };
461
+ // 3. Map out.
462
+ const payload = {};
463
+ for (const m of c.writes) {
464
+ if (m.from.startsWith('"')) {
465
+ put(payload, m.to, JSON.parse(m.from));
466
+ continue;
467
+ }
468
+ const v = this.read(r, m.from);
469
+ if (v === undefined)
470
+ continue;
471
+ const mapped = typeof v === "string" ? (m.values.find(([ours]) => ours === v)?.[1] ?? v) : v;
472
+ put(payload, m.to, wire(mapped));
473
+ }
474
+ // 4 begins: at most once per contract, record and payload; a started run is not started again.
475
+ const key = `${name}|${recordId}|${sha256(serialize(payload))}`;
476
+ const open = [...this.started].find(([, s]) => s.key === key);
477
+ if (open)
478
+ return { ...head, started: false, reason: "a run with this payload is already started", run: open[0] };
479
+ if (this.sent.has(key))
480
+ return { ...head, acted: false, reason: "the same payload was already sent" };
481
+ this.sent.add(key);
482
+ this.outtake.set(log, payload);
483
+ this.started.set(log, { contract: name, record: recordId, key, attempts: 0, ...(cause ? { cause } : {}) });
484
+ const versions = { ...this.versions, ...(c.parts.version ? { contract: c.parts.version } : {}) };
485
+ this.commit({ contract: name, run: log }, log, { contract: name, record: recordId, key, state: "started", payload: `sha256:${sha256(serialize(payload))}`, ...(Object.keys(versions).length ? { versions } : {}), ...(cause ? { cause } : {}) });
486
+ return { ...head, started: true, payload, key, log, ...(cause ? { cause } : {}) };
487
+ }
488
+ /** The act succeeded: the run is `acted` (docs/design/runs.md). */
489
+ acted(run, at) {
490
+ const s = this.started.get(run);
491
+ if (!s)
492
+ throw new Error(`no started run ${run}`);
493
+ this.started.delete(run);
494
+ this.commit({ contract: s.contract, run }, run, { contract: s.contract, record: s.record, key: s.key, state: "acted", ...(at ? { at } : {}) });
495
+ return { contract: s.contract, record: s.record, run, acted: true };
496
+ }
497
+ /** How many attempts a contract allows (7.5 `retry: N every D`): one without it. */
498
+ attemptsAllowed(c) {
499
+ const m = (c.parts.retry ?? "").match(/^(\d+)\s+every\s+/);
500
+ return m ? 1 + Number(m[1]) : 1;
501
+ }
502
+ /**
503
+ * The act failed: a `failed` entry with the attempt; after the last attempt the contract allows, a
504
+ * review item `run:<contract>` opens, grouped by contract across records, and the run stays failed.
505
+ */
506
+ failed(run, reason) {
507
+ const s = this.started.get(run);
508
+ if (!s)
509
+ throw new Error(`no started run ${run}`);
510
+ const c = this.contracts.get(s.contract);
511
+ s.attempts += 1;
512
+ const allowed = c ? this.attemptsAllowed(c) : 1;
513
+ const dead = s.attempts >= allowed;
514
+ if (dead) {
515
+ this.started.delete(run);
516
+ this.review.push({ key: `run:${s.contract}`, run, record: s.record, reason, attempts: s.attempts });
517
+ }
518
+ this.commit({ contract: s.contract, run }, run, { contract: s.contract, record: s.record, key: s.key, state: "failed", reason, attempt: s.attempts });
519
+ return { contract: s.contract, record: s.record, run, failed: true, attempt: s.attempts, ...(dead ? { dead: true, review: `run:${s.contract}` } : { retry: allowed - s.attempts }) };
520
+ }
521
+ /**
522
+ * A decision on a dead run (runs.md): `retry` releases its key and starts a new run from the record as it
523
+ * now is; `drop` closes the item. Either is a review write.
524
+ */
525
+ decideRun(run, decision, callerId) {
526
+ const w = this.runOf(run);
527
+ if (!w)
528
+ throw new Error(`no run ${run}`);
529
+ const item = this.review.find((it) => it.run === run && it.key === `run:${w.contract}` && it.decision === undefined);
530
+ const key = [...this.sent].find((k) => k.startsWith(`${w.contract}|${w.record}|`));
531
+ if (decision === "retry" && key)
532
+ this.sent.delete(key);
533
+ this.review.push({ ...(item ?? { key: `run:${w.contract}`, run, record: w.record }), decision, ...(decision === "retry" && key ? { releases: key } : {}), ...(callerId ? { by: callerId } : {}) });
534
+ if (decision === "drop")
535
+ return { contract: w.contract, run, decision };
536
+ return { ...this.prepare(w.contract, w.record, callerId), decision, retried: run };
537
+ }
538
+ /** Run the contract that reverses a run (7.5 `undo`): an ordinary send naming its cause. */
539
+ /** The contract and record of a run this instance or its store knows. */
540
+ runOf(run) {
541
+ const known = this.runsKnown.get(run);
542
+ if (known)
543
+ return known;
544
+ for (const x of this.journal?.entries ?? []) {
545
+ const w = Array.isArray(x.writes) ? x.writes.find((y) => isObj(y) && y.run === run) : undefined;
546
+ if (w)
547
+ return { contract: String(w.contract), record: String(w.record ?? "") };
548
+ }
549
+ return undefined;
550
+ }
551
+ undo(run, callerId) {
552
+ const w = this.runOf(run);
553
+ const contract = w ? this.contracts.get(w.contract) : undefined;
554
+ if (!w || !contract)
555
+ throw new Error(`no run ${run}`);
556
+ if (!contract.undo)
557
+ return { contract: contract.name, run, stopped: "undo", reason: `${contract.name} names no undo` };
558
+ return this.prepare(contract.undo, w.record, callerId, run);
559
+ }
560
+ needsOf(c) {
561
+ return c.needs;
562
+ }
563
+ /**
564
+ * Steps 5–6 for a receive contract: map the answer back, compare with the record, and land it. Under a
565
+ * gated edge (14.7) the values land as suggestions for the edge's reviewer, whatever the contract says.
566
+ */
567
+ receiveRun(name, recordId, body, callerId, forRun) {
568
+ const c = this.contracts.get(name);
569
+ const r = this.records.get(recordId);
570
+ if (!c || !r)
571
+ throw new Error(`no contract ${name} or record ${recordId}`);
572
+ const head = { contract: name, record: recordId, ...(callerId ? { caller: callerId } : {}) };
573
+ // An answer that names a run the store doesn't know is never guessed onto a record (docs/design/runs.md).
574
+ if (forRun !== undefined) {
575
+ const known = this.runOf(forRun)?.contract === name;
576
+ if (!known) {
577
+ this.review.push({ key: `run:${name}`, run: forRun, record: recordId, reason: "an answer named a run the store doesn't know" });
578
+ const l = this.nextLog();
579
+ this.commit({ contract: name, run: l }, l, { contract: name, record: recordId, state: "closed", answerFor: forRun });
580
+ return { ...head, stopped: "run", reason: `no run ${forRun}`, log: l };
581
+ }
582
+ }
583
+ const stopped = this.permitted(c, recordId, callerId);
584
+ if (stopped)
585
+ return { ...head, stopped };
586
+ const log = forRun ?? this.nextLog();
587
+ const intake = receive(c, body, recordId, this.dict, `I-${log.slice(2)}`);
588
+ const outcomes = intake.outcomes.map((o) => {
589
+ if (o.outcome !== "absorbed" || !o.to)
590
+ return o;
591
+ // A value a must rule won't have is a contradiction citing the rule (9.3): parked, never landed.
592
+ const broken = this.engine.probe({ ...r, values: { [o.to]: o.value } });
593
+ if (broken)
594
+ return { ...o, outcome: "contradiction", cites: { rule: broken.rule ?? "" } };
595
+ const current = r.values[o.to];
596
+ if (current === undefined || (isObj(current) && "$empty" in current))
597
+ return o;
598
+ if (serialize(current) === serialize(o.value))
599
+ return o;
600
+ // A state facet moves (4.2, 9.3): a later value of equal or higher trust is absorbed, not disputed.
601
+ // Equal trust is the common case here (a form of the same origin); the landing decides the rest.
602
+ if (this.dict.facets.get(o.to)?.transitions && (c.origin === "declared" || callerId))
603
+ return o;
604
+ return { ...o, outcome: "conflict" };
605
+ });
606
+ const source = { origin: c.origin, via: log };
607
+ const gate = this.gate(recordId, callerId);
608
+ let summary = {};
609
+ if (c.lands === "observation") {
610
+ // Evidence (9.7): each reading is an observation on this instance, supporting the record that stores
611
+ // the value; the first observation fills an empty value with origin observed.
612
+ const docs = [];
613
+ let filled;
614
+ for (const o of outcomes) {
615
+ if (!o.to || o.value === undefined || !["absorbed", "conflict", "new"].includes(o.outcome))
616
+ continue;
617
+ const up = r.of ? this.records.get(r.of) : undefined;
618
+ const supports = up && r.values[o.to] === undefined && (up.values[o.to] !== undefined || !this.dict.vary.get(r.level)?.has(o.to)) ? up.id : r.id;
619
+ const doc = { obs: `O-${++this.observed}`, on: recordId, address: o.to, value: o.value, "evidence-type": c.parts.evidence ?? "reading", reading: intake.intake, supports };
620
+ docs.push(doc);
621
+ const target = filled ?? (supports === r.id ? r : up);
622
+ const cur = target.values[o.to];
623
+ if (cur === undefined || (isObj(cur) && "$empty" in cur)) {
624
+ filled = { ...target, values: { ...target.values, [o.to]: o.value }, sources: { ...(target.sources ?? {}), [o.to]: { origin: "observed", via: log } } };
625
+ }
626
+ }
627
+ this.observed = Math.max(this.observed, ...this.observations.map((o) => Number(String(o.obs).replace(/^O-/, "")) || 0));
628
+ for (const d of docs)
629
+ d.obs = `O-${++this.observed}`;
630
+ this.observations.push(...docs);
631
+ if (filled)
632
+ this.put(filled);
633
+ // The version split proposed (9.7.3): at the threshold, a review item names the instances that agree.
634
+ const proposed = [];
635
+ const supported = [...new Set(docs.map((d) => String(d.supports)))].flatMap((id) => { const rec = this.records.get(id); return rec ? [rec] : []; });
636
+ for (const item of proposeSplit(supported, this.observations, this.thresholds.split ?? 3)) {
637
+ const same = (it) => it.key === item.key && it.address === item.address && serialize(it.value) === serialize(item.value) && it.decision === undefined;
638
+ if (!this.review.some(same) && !this.knownReview.some(same)) {
639
+ this.review.push(item);
640
+ proposed.push(item);
641
+ }
642
+ }
643
+ this.commit({ contract: name, run: log }, log, { contract: name, record: recordId, state: "closed" }, docs.map((d) => ({ observation: d })));
644
+ return { ...head, outcomes: outcomes, log, landed: "observation", observations: docs, ...(filled ? { filled: filled.id } : {}), ...(proposed.length ? { proposed } : {}) };
645
+ }
646
+ if (c.lands === "suggestion" || gate)
647
+ this.put(suggest(r, outcomes, source));
648
+ else {
649
+ // A form run by a caller is a person's write (11.3): it ranks as `person`, ahead of what a contract or an earlier declaration left.
650
+ const landed = land(r, { outcomes, contract: name, ...(c.kind === "form" && callerId ? { person: true } : {}) }, source, this.dict.trust, this.dict.facetTrust, new Set([...this.dict.facets.values()].filter((f) => f.transitions).map((f) => f.key)));
651
+ // 6. Rules, over what this run changed.
652
+ const touched = {};
653
+ for (const [k, v] of Object.entries(landed.values))
654
+ if (serialize(r.values[k] ?? "∅") !== serialize(v))
655
+ touched[k] = v;
656
+ const ruled = this.applyRules(landed, touched);
657
+ if ("refused" in ruled)
658
+ return { ...head, stopped: "rule", rule: ruled.refused.rule ?? "", address: ruled.refused.address ?? "", log };
659
+ this.put(ruled.record);
660
+ summary = ruled.summary;
661
+ }
662
+ // A form run by a caller is that person's entry (8.5), as an arrival's is; the run write still names the contract.
663
+ this.commit(callerId && c.kind === "form" ? { person: callerId } : { contract: name, run: log }, log, { contract: name, record: recordId, state: "closed" });
664
+ return { ...head, outcomes: outcomes, sources: { origin: c.origin }, log, landed: gate || c.lands === "suggestion" ? "suggestion" : "value", ...(gate ? { gated: gate.edge, reviewer: gate.reviewer ?? gate.by } : {}), ...(Object.keys(summary).length ? { rules: summary } : {}) };
665
+ }
666
+ /**
667
+ * An arrival through a contract (9.2a): the payload finds or makes its record; a new record made for a
668
+ * caller gets their consumer edge (14.4).
669
+ */
670
+ arriveRun(name, body, level, callerId, of, within) {
671
+ const c = this.contracts.get(name);
672
+ if (!c)
673
+ throw new Error(`no contract ${name}`);
674
+ const log = this.nextLog();
675
+ const domain = this.access?.owner ?? this.journal?.owner ?? this.records.all()[0]?.owner ?? "";
676
+ // A person owns what the package lets a person own (14.1): the record is theirs, written as `domain:id`.
677
+ const kind = this.dict.levels.get(level) ?? level;
678
+ // Permitted on the record the new one sits under or within (14.2), else anywhere (7.4 step 1).
679
+ const refused = this.permitted(c, of ?? within ?? "", callerId, true);
680
+ if (refused)
681
+ return { contract: name, ...(callerId ? { caller: callerId } : {}), stopped: refused, log };
682
+ const person = callerId && !callerId.includes(":") && (this.access?.perms.ownership.get("person") ?? []).includes(kind);
683
+ const owner = person ? `${domain}:${callerId}` : domain;
684
+ // A refusal by the rules puts everything back: the arrival never happened.
685
+ const prevRecords = new Map(this.records);
686
+ const prevBindings = new Map(this.bindings);
687
+ const { arrival, record } = arrive(c, body, level, this, this.dict, { mint: () => (this.mint ? this.mint(level) : `${level.charAt(0).toUpperCase()}-NEW${++this.minted}`), owner, ...(of ? { of } : {}), ...(within ? { within } : {}), intakeId: `I-${log.slice(2)}` });
688
+ const out = { contract: name, ...(callerId ? { caller: callerId } : {}), arrival: arrival, log };
689
+ if (record) {
690
+ // 6. Rules, over what landed (everything, for a new record).
691
+ const was = prevRecords.get(record.id);
692
+ const touched = {};
693
+ for (const [k, v] of Object.entries(record.values))
694
+ if (!was || serialize(was.values[k] ?? "∅") !== serialize(v))
695
+ touched[k] = v;
696
+ const ruled = this.applyRules(record, touched);
697
+ if ("refused" in ruled) {
698
+ this.records.clear();
699
+ for (const [k, v] of prevRecords)
700
+ this.records.set(k, v);
701
+ this.bindings.clear();
702
+ for (const [k, v] of prevBindings)
703
+ this.bindings.set(k, v);
704
+ if (was)
705
+ this.engine.put(was);
706
+ return { contract: name, ...(callerId ? { caller: callerId } : {}), stopped: "rule", rule: ruled.refused.rule ?? "", address: ruled.refused.address ?? "", log };
707
+ }
708
+ this.put(ruled.record);
709
+ if (Object.keys(ruled.summary).length)
710
+ out.rules = ruled.summary;
711
+ }
712
+ if (record && arrival.outcome === "new" && callerId) {
713
+ const edge = this.grantConsumer(c, record.id, callerId);
714
+ if (edge)
715
+ out.granted = { edge: edge.edge, relation: edge.relation, mode: edge.mode, on: edge.on };
716
+ }
717
+ this.commit(callerId && c.kind === "form" ? { person: callerId } : { contract: name, run: log }, log, { contract: name, ...(record ? { record: record.id } : {}), state: arrival.outcome === "held" ? "started" : "closed" });
718
+ return out;
719
+ }
720
+ /** The arrival type, re-exported for callers that read `arrival`. */
721
+ static arrivalOf(r) {
722
+ return r.arrival;
723
+ }
724
+ /**
725
+ * A stream contract's run (12.6.2): the entries of each view since the cursor, in order. Pushed (the
726
+ * stored cursor, acknowledged by the index) or pulled (the caller's cursor). A view above `public` goes
727
+ * only to a party holding `run` for the contract.
728
+ */
729
+ streamRun(name, callerId, cursor) {
730
+ const c = this.contracts.get(name);
731
+ if (!c)
732
+ throw new Error(`no contract ${name}`);
733
+ if (!c.stream.length)
734
+ throw new Error(`${name} carries no stream`);
735
+ if (!this.views)
736
+ throw new Error("no views to stream from");
737
+ const party = callerId ?? c.parts.to ?? "";
738
+ const head = { contract: name, caller: party };
739
+ for (const v of c.stream) {
740
+ const view = this.views.set.views.get(v);
741
+ if (view && view.show !== "public" && !this.access?.mayAnywhere(party, "run", name))
742
+ return { ...head, stopped: "permission", view: v };
743
+ }
744
+ const log = this.nextLog();
745
+ const stored = this.cursors.get(name) ?? {};
746
+ const entries = {};
747
+ const out = {};
748
+ const batches = [];
749
+ for (const v of c.stream) {
750
+ this.views.stream(v);
751
+ const version = this.views.versionOf(v);
752
+ const held = stored[v];
753
+ const since = cursor?.[v] ?? (held && held.version === version ? held.seq : 0);
754
+ const list = this.views.entriesSince(v, since);
755
+ entries[v] = list;
756
+ out[v] = list.at(-1)?.seq ?? since;
757
+ if (list.length)
758
+ batches.push(`${name}|${v}|${version}|${list[0]?.seq}-${list.at(-1)?.seq}`);
759
+ }
760
+ return { ...head, entries, cursor: out, ...(batches.length ? { key: batches.join(" ") } : {}), log };
761
+ }
762
+ lastTick = new Map();
763
+ /**
764
+ * A scheduled contract's tick (7.11): the host offers its clock as `now`; every tick due since the last
765
+ * one runs (or only the latest, or none, by `missed`), each run keyed by the contract and the tick so a
766
+ * restarted host never runs a tick twice. A send contract with `for` runs over every record it selects;
767
+ * a stream contract pushes; any other runs once with no record.
768
+ */
769
+ tick(name, now, callerId) {
770
+ const c = this.contracts.get(name);
771
+ if (!c)
772
+ throw new Error(`no contract ${name}`);
773
+ if (!isSchedule(c.when))
774
+ throw new Error(`${name} isn't scheduled`);
775
+ const sch = readSchedule(c.when);
776
+ // A schedule begins when its contract is published (7.11): here, at the first clock reading.
777
+ if (!this.lastTick.has(name)) {
778
+ this.lastTick.set(name, now);
779
+ return { contract: name, now, ticks: [], runs: [], began: now };
780
+ }
781
+ const due = ticksDue(sch, this.lastTick.get(name), now);
782
+ const missed = c.parts.missed ?? "once";
783
+ const run = missed === "all" ? due : missed === "once" ? due.slice(-1) : [];
784
+ const skipped = due.filter((t) => !run.includes(t));
785
+ const results = [];
786
+ for (const tick of run) {
787
+ const key = `${name}|${tick}`;
788
+ if (this.sent.has(key))
789
+ continue;
790
+ this.sent.add(key);
791
+ if (c.stream.length) {
792
+ const r = this.streamRun(name, callerId);
793
+ results.push({ tick, ...r });
794
+ continue;
795
+ }
796
+ const targets = c.parts.for ? this.records.forTarget(c.parts.for).map((r) => r.id) : [];
797
+ if (!targets.length) {
798
+ const log = this.nextLog();
799
+ this.commit({ contract: name, run: log }, log, { contract: name, tick, key, state: "acted" });
800
+ results.push({ tick, log, acted: true });
801
+ continue;
802
+ }
803
+ for (const id of targets)
804
+ results.push({ tick, ...this.run(name, id, callerId) });
805
+ }
806
+ if (due.length)
807
+ this.lastTick.set(name, due[due.length - 1]);
808
+ return { contract: name, now, ticks: run, ...(skipped.length ? { skipped } : {}), runs: results };
809
+ }
810
+ /** The index acknowledges what it holds: the store resumes from there (12.6.2). */
811
+ ack(name, cursor) {
812
+ const stored = this.cursors.get(name) ?? {};
813
+ for (const [v, seq] of Object.entries(cursor))
814
+ stored[v] = { version: this.views?.versionOf(v) ?? "", seq };
815
+ this.cursors.set(name, stored);
816
+ return { contract: name, acked: cursor };
817
+ }
818
+ /** The classes a record wears, those of the records above it included (5.5). */
819
+ wearsOf(r) {
820
+ const up = r.of ? this.records.get(r.of) : undefined;
821
+ return [...(up ? this.wearsOf(up) : []), ...(r.wears ?? [])];
822
+ }
823
+ /**
824
+ * An offer (7.8): the records the contract's `for` selects, with only the facets it gives, each value with
825
+ * its source; the mode and terms ride along. A value the owner took under `reshare: no` never flows on.
826
+ * Who may see it is the host's decision from `to`.
827
+ */
828
+ offer(name, owner, only) {
829
+ const c = this.contracts.get(name);
830
+ if (!c || c.kind !== "offer")
831
+ throw new Error(`no offer ${name}`);
832
+ const wears = c.parts.for;
833
+ const pool = only ? only.flatMap((id) => this.records.get(id) ?? []) : this.records.all();
834
+ const records = [];
835
+ for (const r of pool) {
836
+ if (r.status?.state === "archived")
837
+ continue;
838
+ if (wears && !this.wearsOf(r).includes(wears))
839
+ continue;
840
+ const values = {};
841
+ const sources = {};
842
+ for (const key of c.gives) {
843
+ const v = r.values[key];
844
+ if (v === undefined || (isObj(v) && "$empty" in v))
845
+ continue;
846
+ const s = r.sources?.[key] ?? r.sources?.["*"];
847
+ if (s?.reshare === false)
848
+ continue;
849
+ values[key] = v;
850
+ if (s)
851
+ sources[key] = { origin: s.origin, via: s.via, ...(s.owner ? { owner: s.owner } : {}) };
852
+ }
853
+ if (!Object.keys(values).length)
854
+ continue;
855
+ records.push({ id: r.id, kind: r.kind, level: r.level, owner: r.owner, values, sources, ...(r.of ? { of: r.of } : {}) });
856
+ }
857
+ return { contract: name, owner, to: c.to ?? "private", mode: c.mode ?? "gathered", terms: { ...c.terms }, gives: [...c.gives], records };
858
+ }
859
+ /**
860
+ * Offer and take (7.8): only what the offer gives and the take reads flows, by the offer's mode.
861
+ * `dump` copies once and never updates; `observed` stores a link and no value; `gathered` copies and
862
+ * takes the presenter's updates as intake, never over a value the consumer entered; `controlled` copies
863
+ * under the presenter's terms, which do override, expire and withdraw. The entry names the presenter's
864
+ * entry the values came from (8.5.1).
865
+ */
866
+ take(takeName, offer, recordId, presenter) {
867
+ const t = this.contracts.get(takeName);
868
+ const o = typeof offer === "string" ? this.localOffer(offer) : offer;
869
+ const r = this.records.get(recordId);
870
+ if (!t || !o || !r)
871
+ throw new Error("a take needs its take contract, the offer and the record");
872
+ if (t.from && t.from !== presenter.owner)
873
+ return { contract: takeName, record: recordId, stopped: "from" };
874
+ const mode = o.mode;
875
+ if (t.mode && t.mode !== mode)
876
+ return { contract: takeName, record: recordId, stopped: "mode", offered: mode };
877
+ const log = this.nextLog();
878
+ const values = { ...r.values };
879
+ const sources = { ...(r.sources ?? {}) };
880
+ const alternatives = { ...(r.alternatives ?? {}) };
881
+ const flows = [];
882
+ let from;
883
+ const fromPresenter = (a) => sources[a]?.owner === presenter.owner;
884
+ if (mode === "observed") {
885
+ // A read-through is never stored (7.8): the record links to the presenter's, naming what it reads.
886
+ for (const m of t.reads)
887
+ if (o.gives.includes(m.from) && presenter.values[m.from] !== undefined)
888
+ flows.push(m.from);
889
+ values["crubal:observes"] = { $link: `${presenter.owner}/${presenter.id}` };
890
+ sources["crubal:observes"] = { origin: "reported", via: log, owner: presenter.owner };
891
+ this.put({ ...r, values, sources });
892
+ this.commit({ take: takeName, from: presenter.owner }, log, { contract: takeName, record: recordId, state: "closed" });
893
+ return { contract: takeName, record: recordId, mode, observes: flows, log };
894
+ }
895
+ for (const m of t.reads) {
896
+ const theirs = m.from;
897
+ if (!o.gives.includes(theirs))
898
+ continue; // not offered: doesn't flow
899
+ const v = presenter.values[theirs];
900
+ if (v === undefined)
901
+ continue;
902
+ const theirSource = presenter.sources?.[theirs] ?? presenter.sources?.["*"];
903
+ from ??= theirSource?.via ? `${presenter.owner}/${theirSource.via}` : presenter.owner;
904
+ const src = { origin: theirSource?.origin ?? "reported", via: log, owner: presenter.owner };
905
+ if (mode === "controlled") {
906
+ if (o.terms.expires)
907
+ src.until = o.terms.expires;
908
+ if (o.terms.reshare === "no")
909
+ src.reshare = false;
910
+ if (o.terms.revocable === "yes")
911
+ src.revocable = true;
912
+ }
913
+ const current = values[m.to];
914
+ const empty = current === undefined || (isObj(current) && "$empty" in current);
915
+ if (empty) {
916
+ values[m.to] = v;
917
+ sources[m.to] = src;
918
+ flows.push(theirs);
919
+ }
920
+ else if (serialize(current) === serialize(v)) {
921
+ if (mode !== "dump" && fromPresenter(m.to))
922
+ sources[m.to] = { ...sources[m.to], ...src, via: sources[m.to]?.via ?? log };
923
+ }
924
+ else if (mode === "dump") {
925
+ // Copied once, never updated.
926
+ }
927
+ else if (mode === "controlled" || fromPresenter(m.to)) {
928
+ // The presenter's update: under controlled terms it wins; gathered, it replaces only its own earlier value.
929
+ values[m.to] = v;
930
+ sources[m.to] = src;
931
+ flows.push(theirs);
932
+ }
933
+ else {
934
+ // A presenter never overrides a value a person entered on the consumer's side.
935
+ alternatives[m.to] = [...(alternatives[m.to] ?? []), { value: v, origin: src.origin, via: log, owner: presenter.owner }];
936
+ }
937
+ }
938
+ // Withdrawal under revocable terms: a value this presenter gave and no longer offers or holds is removed.
939
+ const withdrawn = [];
940
+ for (const [a, s] of Object.entries(sources)) {
941
+ if (!fromPresenter(a) || !s.revocable)
942
+ continue;
943
+ const line = t.reads.find((m) => m.to === a);
944
+ const still = line && o.gives.includes(line.from) && presenter.values[line.from] !== undefined;
945
+ if (still)
946
+ continue;
947
+ delete values[a];
948
+ delete sources[a];
949
+ withdrawn.push(a);
950
+ }
951
+ const next = { ...r, values, sources, ...(Object.keys(alternatives).length ? { alternatives } : {}) };
952
+ this.put(next);
953
+ this.commit({ take: takeName, from: from ?? presenter.owner }, log, { contract: takeName, record: recordId, state: "closed" });
954
+ return { contract: takeName, record: recordId, mode, flows, ...(withdrawn.length ? { withdrawn } : {}), log };
955
+ }
956
+ /** A local offer contract as an offer object, for a take run against a presenter record at hand. */
957
+ localOffer(name) {
958
+ const o = this.contracts.get(name);
959
+ return o ? { contract: name, owner: "", to: o.to ?? "private", mode: o.mode ?? "gathered", terms: { ...o.terms }, gives: [...o.gives], records: [] } : undefined;
960
+ }
961
+ /** Taken values past their terms' `expires` (7.8) are removed, each record one entry; the ids changed. */
962
+ expire(now) {
963
+ const out = [];
964
+ for (const r of this.records.all()) {
965
+ const gone = Object.entries(r.sources ?? {}).filter(([, s]) => typeof s.until === "string" && s.until <= now).map(([a]) => a);
966
+ if (!gone.length)
967
+ continue;
968
+ const values = { ...r.values };
969
+ const sources = { ...(r.sources ?? {}) };
970
+ for (const a of gone) {
971
+ delete values[a];
972
+ delete sources[a];
973
+ }
974
+ this.put({ ...r, values, sources });
975
+ const log = this.nextLog();
976
+ this.commit({ take: "expired", at: now }, log, { contract: "expired", record: r.id, state: "closed" });
977
+ out.push(r.id);
978
+ }
979
+ return out;
980
+ }
981
+ }
982
+ function suggest(r, outcomes, source) {
983
+ const s = { ...(r.suggestions ?? {}) };
984
+ for (const o of outcomes) {
985
+ if (!o.to || o.value === undefined || o.outcome === "unknown" || o.outcome === "ignored" || o.outcome === "contradiction")
986
+ continue;
987
+ s[o.to] = [...(s[o.to] ?? []), { value: o.value, origin: source.origin, via: source.via }];
988
+ }
989
+ return { ...r, suggestions: s };
990
+ }
991
+ /**
992
+ * The version gate at a border (10.1): what a receiver does with data written under another version of a
993
+ * package it holds. Newer than ours: hold until we upgrade. Older: restamp through the recipes between.
994
+ * The same, or a package we don't hold, or no version on the wire: take as is.
995
+ */
996
+ export function versionGate(theirs, installed) {
997
+ if (!theirs)
998
+ return { kind: "take" };
999
+ const at = theirs.lastIndexOf(" /");
1000
+ if (at < 0)
1001
+ return { kind: "take" };
1002
+ const pkg = theirs.slice(0, at).trim();
1003
+ const version = theirs.slice(at + 2).trim();
1004
+ const ours = installed.filter((i) => i.package === pkg).map((i) => i.version).sort().at(-1);
1005
+ if (!ours)
1006
+ return { kind: "take" };
1007
+ if (version > ours)
1008
+ return { kind: "hold", package: pkg, theirs: version, ours };
1009
+ if (version < ours)
1010
+ return { kind: "restamp", package: pkg, theirs: version, ours };
1011
+ return { kind: "take" };
1012
+ }
1013
+ // ─── Borders (8.5.1) ─────────────────────────────────────────────────────────
1014
+ /** Verify a border wrapper from another owner: its log against the owner's key, and its values against the log. */
1015
+ export function verifyBorder(w, keys) {
1016
+ const owner = w.records[0]?.owner ?? "";
1017
+ if (!keys[owner])
1018
+ return { verified: false, reason: "no key for the owner" };
1019
+ const chain = verifyChain(w.log ?? [], keys);
1020
+ if (!chain.valid)
1021
+ return { verified: false, "breaks-at": chain["breaks-at"] ?? "", reason: chain.reason ?? "" };
1022
+ const byLog = new Map((w.log ?? []).map((e) => [String(e.log), e]));
1023
+ for (const r of w.records) {
1024
+ for (const [address, value] of Object.entries(r.values)) {
1025
+ const via = (r.sources?.[address] ?? r.sources?.["*"])?.via;
1026
+ const entry = via ? byLog.get(via) : undefined;
1027
+ const writes = Array.isArray(entry?.writes) ? entry.writes : [];
1028
+ const write = writes.find((x) => x.record === r.id && x.address === address);
1029
+ if (!write || serialize(write.value) !== serialize(value))
1030
+ return { verified: false, record: r.id, address, reason: "value doesn't match the log" };
1031
+ }
1032
+ }
1033
+ return { verified: true, records: w.records.length, owner };
1034
+ }
1035
+ //# sourceMappingURL=exchange.js.map