@lapxo/bound 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 (186) hide show
  1. package/CONTRIBUTING.md +44 -0
  2. package/LICENSE +22 -0
  3. package/README.md +97 -0
  4. package/TARGET.bound +361 -0
  5. package/dist/cli/accept.d.ts +23 -0
  6. package/dist/cli/accept.js +423 -0
  7. package/dist/cli/args.d.ts +10 -0
  8. package/dist/cli/args.js +34 -0
  9. package/dist/cli/host.d.ts +1 -0
  10. package/dist/cli/host.js +32 -0
  11. package/dist/cli/names.d.ts +25 -0
  12. package/dist/cli/names.js +12 -0
  13. package/dist/cli/observe.d.ts +1 -0
  14. package/dist/cli/observe.js +6 -0
  15. package/dist/cli/owner.d.ts +35 -0
  16. package/dist/cli/owner.js +119 -0
  17. package/dist/cli/pass.d.ts +6 -0
  18. package/dist/cli/pass.js +309 -0
  19. package/dist/cli/place.d.ts +74 -0
  20. package/dist/cli/place.js +290 -0
  21. package/dist/cli/takes.d.ts +30 -0
  22. package/dist/cli/takes.js +108 -0
  23. package/dist/cli/verb.d.ts +2 -0
  24. package/dist/cli/verb.js +478 -0
  25. package/dist/fold/ceilings.d.ts +24 -0
  26. package/dist/fold/ceilings.js +129 -0
  27. package/dist/fold/claims.d.ts +25 -0
  28. package/dist/fold/claims.js +107 -0
  29. package/dist/fold/closed.d.ts +15 -0
  30. package/dist/fold/closed.js +168 -0
  31. package/dist/fold/configures.d.ts +4 -0
  32. package/dist/fold/configures.js +22 -0
  33. package/dist/fold/digests.d.ts +29 -0
  34. package/dist/fold/digests.js +211 -0
  35. package/dist/fold/evidence.d.ts +32 -0
  36. package/dist/fold/evidence.js +22 -0
  37. package/dist/fold/fold-worker.d.ts +1 -0
  38. package/dist/fold/fold-worker.js +13 -0
  39. package/dist/fold/kept.d.ts +29 -0
  40. package/dist/fold/kept.js +142 -0
  41. package/dist/fold/keys.d.ts +17 -0
  42. package/dist/fold/keys.js +84 -0
  43. package/dist/fold/laid.d.ts +17 -0
  44. package/dist/fold/laid.js +90 -0
  45. package/dist/fold/nos.d.ts +8 -0
  46. package/dist/fold/nos.js +48 -0
  47. package/dist/fold/object.d.ts +17 -0
  48. package/dist/fold/object.js +38 -0
  49. package/dist/fold/observed.d.ts +33 -0
  50. package/dist/fold/observed.js +444 -0
  51. package/dist/fold/opened.d.ts +5 -0
  52. package/dist/fold/opened.js +33 -0
  53. package/dist/fold/own.d.ts +24 -0
  54. package/dist/fold/own.js +215 -0
  55. package/dist/fold/paid.d.ts +11 -0
  56. package/dist/fold/paid.js +92 -0
  57. package/dist/fold/place-inputs.d.ts +17 -0
  58. package/dist/fold/place-inputs.js +70 -0
  59. package/dist/fold/places.d.ts +20 -0
  60. package/dist/fold/places.js +84 -0
  61. package/dist/fold/rank.d.ts +3 -0
  62. package/dist/fold/rank.js +9 -0
  63. package/dist/fold/reach.d.ts +13 -0
  64. package/dist/fold/reach.js +65 -0
  65. package/dist/fold/readers.d.ts +44 -0
  66. package/dist/fold/readers.js +132 -0
  67. package/dist/fold/receipts.d.ts +8 -0
  68. package/dist/fold/receipts.js +47 -0
  69. package/dist/fold/region.d.ts +72 -0
  70. package/dist/fold/region.js +286 -0
  71. package/dist/fold/release-policy.d.ts +67 -0
  72. package/dist/fold/release-policy.js +103 -0
  73. package/dist/fold/resolved.d.ts +18 -0
  74. package/dist/fold/resolved.js +123 -0
  75. package/dist/fold/roles.d.ts +17 -0
  76. package/dist/fold/roles.js +59 -0
  77. package/dist/fold/second.d.ts +18 -0
  78. package/dist/fold/second.js +68 -0
  79. package/dist/fold/shards.d.ts +29 -0
  80. package/dist/fold/shards.js +100 -0
  81. package/dist/fold/signed.d.ts +18 -0
  82. package/dist/fold/signed.js +75 -0
  83. package/dist/fold/signers.d.ts +24 -0
  84. package/dist/fold/signers.js +58 -0
  85. package/dist/fold/stranger.d.ts +3 -0
  86. package/dist/fold/stranger.js +24 -0
  87. package/dist/fold/views.d.ts +27 -0
  88. package/dist/fold/views.js +66 -0
  89. package/dist/fold/wire.d.ts +19 -0
  90. package/dist/fold/wire.js +41 -0
  91. package/dist/fold/zoom.d.ts +16 -0
  92. package/dist/fold/zoom.js +80 -0
  93. package/dist/host/adapters/keys/ed25519.d.ts +7 -0
  94. package/dist/host/adapters/keys/ed25519.js +30 -0
  95. package/dist/host/archive.d.ts +11 -0
  96. package/dist/host/archive.js +152 -0
  97. package/dist/host/capsule-process.d.ts +20 -0
  98. package/dist/host/capsule-process.js +54 -0
  99. package/dist/host/capsule.d.ts +13 -0
  100. package/dist/host/capsule.js +149 -0
  101. package/dist/host/cell-receipts.d.ts +2 -0
  102. package/dist/host/cell-receipts.js +124 -0
  103. package/dist/host/digest.d.ts +3 -0
  104. package/dist/host/digest.js +15 -0
  105. package/dist/host/hash.d.ts +2 -0
  106. package/dist/host/hash.js +3 -0
  107. package/dist/host/io.d.ts +9 -0
  108. package/dist/host/io.js +9 -0
  109. package/dist/host/located.d.ts +4 -0
  110. package/dist/host/located.js +105 -0
  111. package/dist/host/objects.d.ts +5 -0
  112. package/dist/host/objects.js +18 -0
  113. package/dist/host/ports/store.d.ts +2 -0
  114. package/dist/host/ports/store.js +18 -0
  115. package/dist/host/provide.d.ts +23 -0
  116. package/dist/host/provide.js +81 -0
  117. package/dist/host/release.d.ts +12 -0
  118. package/dist/host/release.js +213 -0
  119. package/dist/host/selected-capsules.d.ts +8 -0
  120. package/dist/host/selected-capsules.js +19 -0
  121. package/dist/host/selected-topos.d.ts +17 -0
  122. package/dist/host/selected-topos.js +61 -0
  123. package/dist/host/source-content.d.ts +10 -0
  124. package/dist/host/source-content.js +94 -0
  125. package/dist/host/timing.d.ts +2 -0
  126. package/dist/host/timing.js +7 -0
  127. package/dist/judge/late.d.ts +7 -0
  128. package/dist/judge/late.js +33 -0
  129. package/dist/judge/take.d.ts +57 -0
  130. package/dist/judge/take.js +75 -0
  131. package/dist/judge/widening.d.ts +1 -0
  132. package/dist/judge/widening.js +25 -0
  133. package/dist/land/act.d.ts +4 -0
  134. package/dist/land/act.js +209 -0
  135. package/dist/land/alias.d.ts +10 -0
  136. package/dist/land/alias.js +41 -0
  137. package/dist/land/held.d.ts +7 -0
  138. package/dist/land/held.js +23 -0
  139. package/dist/land/inert.d.ts +26 -0
  140. package/dist/land/inert.js +33 -0
  141. package/dist/land/leaving.d.ts +14 -0
  142. package/dist/land/leaving.js +51 -0
  143. package/dist/land/ledger.d.ts +28 -0
  144. package/dist/land/ledger.js +148 -0
  145. package/dist/land/owned.d.ts +23 -0
  146. package/dist/land/owned.js +65 -0
  147. package/dist/land/sign.d.ts +16 -0
  148. package/dist/land/sign.js +30 -0
  149. package/dist/land/vouched.d.ts +21 -0
  150. package/dist/land/vouched.js +112 -0
  151. package/dist/land/withdraw.d.ts +14 -0
  152. package/dist/land/withdraw.js +24 -0
  153. package/dist/observe/files.d.ts +14 -0
  154. package/dist/observe/files.js +68 -0
  155. package/dist/observe/identity.d.ts +19 -0
  156. package/dist/observe/identity.js +61 -0
  157. package/dist/observe/match.d.ts +1 -0
  158. package/dist/observe/match.js +8 -0
  159. package/dist/observe/regions.d.ts +29 -0
  160. package/dist/observe/regions.js +200 -0
  161. package/dist/observe/run.d.ts +30 -0
  162. package/dist/observe/run.js +34 -0
  163. package/dist/observe/runner.d.ts +16 -0
  164. package/dist/observe/runner.js +83 -0
  165. package/dist/render/capsule.d.ts +24 -0
  166. package/dist/render/capsule.js +30 -0
  167. package/dist/render/field.d.ts +9 -0
  168. package/dist/render/field.js +46 -0
  169. package/dist/render/names.d.ts +3 -0
  170. package/dist/render/names.js +4 -0
  171. package/dist/render/open.d.ts +27 -0
  172. package/dist/render/open.js +61 -0
  173. package/dist/render/page.d.ts +3 -0
  174. package/dist/render/page.js +7 -0
  175. package/dist/render/receipts.d.ts +3 -0
  176. package/dist/render/receipts.js +46 -0
  177. package/dist/render/shape.d.ts +16 -0
  178. package/dist/render/shape.js +74 -0
  179. package/dist/render/target.d.ts +18 -0
  180. package/dist/render/target.js +52 -0
  181. package/dist/render/view.d.ts +22 -0
  182. package/dist/render/view.js +191 -0
  183. package/docs/readme-assets/atlas.svg +169 -0
  184. package/docs/usage.md +53 -0
  185. package/package.json +48 -0
  186. package/receipts.bound +59 -0
@@ -0,0 +1,75 @@
1
+ import { createHash, existsSync, join } from "../host/io.js";
2
+ import { fullDigest } from "../fold/digests.js";
3
+ import { answers, point, region } from '@lapxo/obligations/field';
4
+ import { parse, PROTOCOL } from '@lapxo/topos/wire';
5
+ import { observeText } from "../observe/files.js";
6
+ import { storeOf } from "../land/ledger.js";
7
+ import { holdsCases } from "../fold/paid.js";
8
+ /** A demanded location is a region: it is there when some place of the tree answers to it, file or directory alike. */
9
+ function located(root, location) {
10
+ return existsSync(join(root, location.replace(/\/+$/, '')));
11
+ }
12
+ function namedEffects(_root, _changed, _ROOTS) {
13
+ return [];
14
+ }
15
+ export function judgeTake(input) {
16
+ const { root, scope, value, locations, before, after } = input;
17
+ const paths = [...new Set([...before.keys(), ...after.keys()])].sort();
18
+ const moved = paths.filter((p) => before.get(p) !== after.get(p));
19
+ const notOwned = moved.filter((coordinate) => {
20
+ if (before.has(coordinate) || locations.includes(coordinate) || !input.witnessOf || !input.law)
21
+ return false;
22
+ const named = input.witnessOf(coordinate);
23
+ return named !== undefined && !input.law.includes(named);
24
+ });
25
+ const changed = moved.filter((coordinate) => !notOwned.includes(coordinate));
26
+ const absent = value === 'present' ? locations.filter((l) => !located(root, l)) : [];
27
+ const effects = changed.length && !absent.length ? namedEffects(root, changed, input.template) : [];
28
+ const envelope = {
29
+ scope,
30
+ cone: [...locations],
31
+ before: Object.fromEntries([...before].sort()),
32
+ after: Object.fromEntries([...after].filter(([coordinate]) => !notOwned.includes(coordinate)).sort()),
33
+ effects,
34
+ };
35
+ const digest = fullDigest(storeOf(root), JSON.stringify({ ...envelope, effects: effects.map((e) => ({ kind: e.kind, status: e.status, at: e.at })) }));
36
+ const refuse = (why) => ({ kind: 'refuse', why, envelope, digest, changed, notOwned });
37
+ const met = (why) => ({ kind: 'fact', why, envelope, digest, changed, notOwned });
38
+ if (!changed.length)
39
+ return refuse('nothing in the cone moved against what was vouched');
40
+ const cone = locations.map((l) => region(l));
41
+ const outside = changed.filter((coordinate) => !answers([point(coordinate)], cone));
42
+ if (outside.length)
43
+ return refuse(`outside the cone: ${outside.join(', ')}`);
44
+ if (absent.length)
45
+ return refuse(`demanded locations absent: ${absent.join(', ')}`);
46
+ if (input.shape === 'lock' || input.shape === 'render') {
47
+ const lines = locations.map((l) => ({ at: l, text: observeText(join(root, l)) }));
48
+ const absent = lines.filter((one) => one.text === undefined).map((one) => one.at);
49
+ if (absent.length)
50
+ return refuse(`a lock the demand names is not here: ${absent.join(', ')}`);
51
+ const said = lines.flatMap((one) => (one.text ?? '').split('\n').filter((line) => line.startsWith(PROTOCOL)));
52
+ const unread = said.filter((line) => parse(line).kind !== 'fact');
53
+ if (input.shape === 'lock' && (!said.length || unread.length)) {
54
+ return refuse(`a lock the demand names holds ${said.length ? `${unread.length} lines the wire does not read` : 'no claim'}`);
55
+ }
56
+ const rests = input.restsOn ?? '';
57
+ const digest = rests ? `sha256:${createHash('sha256').update(lines.map((one) => one.text ?? '').join('')).digest('hex')}` : rests;
58
+ if (rests && digest !== rests)
59
+ return refuse(`the lock moved: it rests on ${rests} and is ${digest}`);
60
+ return met(input.shape === 'lock' ? `${said.length} lines of the lock read, at the digest it rests on` : 'the region is here, at the digest it rests on');
61
+ }
62
+ const vectors = locations.filter((l) => holdsCases(l) && located(root, l));
63
+ if (!vectors.length)
64
+ return refuse('no vector location in the cone exists');
65
+ const red = effects.filter((e) => e.status === 'red' && e.admitted);
66
+ if (red.length)
67
+ return refuse(`effects red: ${red.map((e) => `${e.kind}@${e.at}`).join(', ')}`);
68
+ return met('demand met in its cone');
69
+ }
70
+ /** A take that met its cone carries the gates its batch passed: their runs join its envelope, and its receipt is sealed with them. */
71
+ export function sealed(root, verdict, gates) {
72
+ const envelope = { ...verdict.envelope, effects: [...verdict.envelope.effects, ...gates.map(({ kind, status, ms, at, admitted }) => ({ kind, status, ms, at, admitted }))] };
73
+ const digest = fullDigest(storeOf(root), JSON.stringify({ ...envelope, effects: envelope.effects.map((e) => ({ kind: e.kind, status: e.status, at: e.at })) }));
74
+ return { ...verdict, envelope, digest };
75
+ }
@@ -0,0 +1 @@
1
+ export declare function widenedWithoutWitness(proposed: readonly string[], standing: readonly string[], isCeiling: (line: string) => boolean, witness: string): readonly string[];
@@ -0,0 +1,25 @@
1
+ import { fieldOf } from "../fold/claims.js";
2
+ import { ALPHABETS, idsOf, INTERVALS, spanOf } from '@lapxo/topos/forms';
3
+ /**
4
+ * A ceiling that admits more than it did is not a revision of the same kind as one that admits less: narrowing costs
5
+ * the signer nothing anyone else can dispute, while widening gives back freedom the tree had already spent. So a line
6
+ * that widens what it replaces is refused unless it says what forced it — an `at` of the witness class, which names
7
+ * the reading or the case that made the old bound wrong. Policy may narrow; only a witness may widen. A list of what is
8
+ * forbidden narrows as it grows.
9
+ */
10
+ function widens(now, before) {
11
+ const [was, held] = [spanOf(fieldOf(before, 'value')), spanOf(fieldOf(now, 'value'))];
12
+ if (was && held)
13
+ return INTERVALS.leq(was, held) && !INTERVALS.leq(held, was);
14
+ const [old, fresh] = [idsOf(fieldOf(before, 'value')), idsOf(fieldOf(now, 'value'))];
15
+ return old.polarity === fresh.polarity && ALPHABETS.leq(old, fresh) && !ALPHABETS.leq(fresh, old);
16
+ }
17
+ export function widenedWithoutWitness(proposed, standing, isCeiling, witness) {
18
+ const held = new Map(standing.filter(isCeiling).map((line) => [fieldOf(line, 'scope'), line]));
19
+ return proposed.filter((line) => {
20
+ const before = held.get(fieldOf(line, 'scope'));
21
+ if (!before || !isCeiling(line) || !widens(line, before))
22
+ return false;
23
+ return !fieldOf(line, 'at').startsWith(`${witness}:`);
24
+ });
25
+ }
@@ -0,0 +1,4 @@
1
+ export declare function underTheRegions<T>(store: string, regions: readonly string[], what: string, act: () => T | Promise<T>): Promise<T>;
2
+ export declare const holdRoot: (store: string, what: string) => Promise<void>;
3
+ export declare function underTheLock<T>(store: string, what: string, act: () => T | Promise<T>): Promise<T>;
4
+ export declare const holds: (store: string, region: string) => boolean;
@@ -0,0 +1,209 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { basename, dirname, existsSync, join, mkdirSync, rmSync, writeFileSync } from "../host/io.js";
4
+ import { selfName } from "../fold/claims.js";
5
+ import { placesIn } from "../fold/places.js";
6
+ import { coordinatesUnder, observeText } from "../observe/files.js";
7
+ const context = new AsyncLocalStorage();
8
+ const held = new Map();
9
+ const pauseFor = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
10
+ const ROOT = '_root';
11
+ const MINUTE = 60000;
12
+ const holderOf = (at) => {
13
+ const text = observeText(at);
14
+ if (text === undefined)
15
+ return undefined;
16
+ const [pid, what, since, token] = text.trim().split(' ');
17
+ return { pid: Number(pid), what: what ?? '', since: since ?? '', token: token ?? '' };
18
+ };
19
+ function overAMinute(at, began, holder) {
20
+ if (Date.now() - began < MINUTE)
21
+ return;
22
+ const who = holder === undefined ? 'a process' : `${holder.pid} · ${holder.what} since ${holder.since}`;
23
+ process.stderr.write(`RED wait · ${named(at)} is held by ${who} · a wait over a minute is a red, not a pause\n`);
24
+ throw new Error(`${selfName()}: REFUSE·stale nothing landed · a wait over a minute is a red, not a pause`);
25
+ }
26
+ const alive = (pid) => {
27
+ if (!Number.isSafeInteger(pid) || pid <= 0)
28
+ return true;
29
+ try {
30
+ process.kill(pid, 0);
31
+ return true;
32
+ }
33
+ catch (error) {
34
+ return error.code !== 'ESRCH';
35
+ }
36
+ };
37
+ const regionAt = (store, region) => {
38
+ const coordinate = region.replace(/\/+$/, '');
39
+ if ((coordinate && coordinate.split('/').some((step) => step === '..' || step === '.' || step === '')) || coordinate.startsWith('/'))
40
+ throw new Error(`${selfName()}: REFUSE·lock invalid region coordinate`);
41
+ return join(store, 'locks', coordinate ? encodeURIComponent(coordinate).replace(/^\./, '%2E').replace(/^_root$/, '%5Froot') : ROOT);
42
+ };
43
+ const coordinateOf = (file) => {
44
+ if (file === ROOT)
45
+ return '';
46
+ try {
47
+ return decodeURIComponent(file);
48
+ }
49
+ catch {
50
+ return file;
51
+ }
52
+ };
53
+ const lockFiles = (locks) => coordinatesUnder(locks, locks).filter((file) => {
54
+ if (file !== '.act')
55
+ return true;
56
+ // The legacy marker lists paths, while an actual region named .act has a numeric lock owner.
57
+ const holder = holderOf(join(locks, file));
58
+ return holder !== undefined && Number.isSafeInteger(holder.pid) && holder.pid > 0;
59
+ });
60
+ const overlaps = (a, b) => !a || !b || a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
61
+ const treeAt = (store) => join(store, 'lock');
62
+ const named = (at) => (basename(at) === 'lock' ? 'the store' : `region ${basename(at) === ROOT ? '.' : basename(at)}`);
63
+ const owns = (at) => {
64
+ for (let frame = context.getStore(); frame !== undefined; frame = frame.parent) {
65
+ const token = frame.locks.get(at);
66
+ if (token !== undefined && held.get(at) === token)
67
+ return true;
68
+ }
69
+ return false;
70
+ };
71
+ const ownsRegion = (store) => [...held.keys()].some((at) => at.startsWith(`${join(store, 'locks')}/`) && owns(at));
72
+ /** Reentry belongs to an awaited act's context. Another process or async act has no inherited authority. */
73
+ async function hold(at, what) {
74
+ if (owns(at))
75
+ return;
76
+ const frame = context.getStore();
77
+ if (frame === undefined)
78
+ throw new Error(`${selfName()}: REFUSE·lock no act owns this request`);
79
+ const began = Date.now();
80
+ const token = randomUUID();
81
+ let told = false;
82
+ mkdirSync(dirname(at), { recursive: true });
83
+ for (;;) {
84
+ try {
85
+ writeFileSync(at, `${process.pid} ${what.replace(/\s+/g, '_')} ${new Date().toISOString()} ${token}\n`, { flag: 'wx' });
86
+ break;
87
+ }
88
+ catch (error) {
89
+ const code = error.code;
90
+ if (code === 'ENOENT')
91
+ mkdirSync(dirname(at), { recursive: true });
92
+ else if (code !== 'EEXIST')
93
+ throw error;
94
+ }
95
+ const holder = holderOf(at);
96
+ // Removing a dead owner's lock here would race with another process's recovery. Recovery is a separate host act.
97
+ if (holder !== undefined && !alive(holder.pid)) {
98
+ throw new Error(`${selfName()}: REFUSE·stale ${named(at)} belongs to exited process ${holder.pid}; host recovery required`);
99
+ }
100
+ overAMinute(at, began, holder);
101
+ if (!told && holder !== undefined) {
102
+ process.stderr.write(`WAIT ${named(at)} is held by ${holder.pid} · ${holder.what} since ${holder.since}\n`);
103
+ told = true;
104
+ }
105
+ await pauseFor(250);
106
+ }
107
+ if (told)
108
+ process.stderr.write(`HELD ${named(at)} after ${Date.now() - began} ms waiting\n`);
109
+ held.set(at, token);
110
+ frame.locks.set(at, token);
111
+ if (!process.listeners('exit').includes(releaseAll))
112
+ process.once('exit', releaseAll);
113
+ }
114
+ function release(at, token) {
115
+ if (held.get(at) !== token)
116
+ return;
117
+ held.delete(at);
118
+ const holder = holderOf(at);
119
+ if (holder?.pid === process.pid && holder.token === token && existsSync(at))
120
+ rmSync(at, { force: true });
121
+ }
122
+ function releaseAll() {
123
+ for (const [at, token] of [...held])
124
+ release(at, token);
125
+ }
126
+ process.once('SIGTERM', () => {
127
+ process.stderr.write(`${selfName()}: REFUSE·crash signal\n`);
128
+ releaseAll();
129
+ process.exit(1);
130
+ });
131
+ async function framed(takes, act) {
132
+ const frame = { parent: context.getStore(), locks: new Map() };
133
+ return context.run(frame, async () => {
134
+ try {
135
+ await takes();
136
+ return await act();
137
+ }
138
+ finally {
139
+ for (const [at, token] of [...frame.locks].reverse())
140
+ release(at, token);
141
+ frame.locks.clear();
142
+ }
143
+ });
144
+ }
145
+ export function underTheRegions(store, regions, what, act) {
146
+ return framed(async () => {
147
+ const tree = treeAt(store);
148
+ const asked = [...new Set((regions.length ? regions : ['']).map((one) => one.replace(/\/+$/, '')))];
149
+ if (owns(tree) || asked.every((region) => owns(regionAt(store, region)))) {
150
+ for (const region of asked)
151
+ await hold(regionAt(store, region), what);
152
+ return;
153
+ }
154
+ if (ownsRegion(store))
155
+ throw new Error(`${selfName()}: REFUSE·lock declare every region before entering an act`);
156
+ const already = owns(tree);
157
+ // The tree lock is the acquisition gate. A whole-tree act cannot enter between checking it and taking regions.
158
+ await hold(tree, what);
159
+ try {
160
+ const locks = join(store, 'locks');
161
+ for (;;) {
162
+ const conflict = lockFiles(locks).find((file) => asked.some((region) => overlaps(region, file) || overlaps(region, coordinateOf(file))) && !owns(join(locks, file)));
163
+ if (conflict === undefined)
164
+ break;
165
+ const at = join(locks, conflict);
166
+ const began = Date.now();
167
+ for (let holder = holderOf(at); holder !== undefined; holder = holderOf(at)) {
168
+ if (!alive(holder.pid))
169
+ throw new Error(`${selfName()}: REFUSE·stale ${named(at)} belongs to exited process ${holder.pid}; host recovery required`);
170
+ overAMinute(at, began, holder);
171
+ await pauseFor(250);
172
+ }
173
+ }
174
+ for (const region of asked.filter(Boolean).sort())
175
+ await hold(regionAt(store, region), what);
176
+ if (asked.includes(''))
177
+ await hold(regionAt(store, ''), what);
178
+ }
179
+ finally {
180
+ if (!already) {
181
+ const frame = context.getStore();
182
+ const token = frame.locks.get(tree);
183
+ if (token !== undefined) {
184
+ release(tree, token);
185
+ frame.locks.delete(tree);
186
+ }
187
+ }
188
+ }
189
+ }, act);
190
+ }
191
+ export const holdRoot = async (store, what) => {
192
+ if (!owns(treeAt(store)))
193
+ await hold(regionAt(store, ''), what);
194
+ };
195
+ export function underTheLock(store, what, act) {
196
+ return framed(async () => {
197
+ if (!owns(treeAt(store)) && ownsRegion(store))
198
+ throw new Error(`${selfName()}: REFUSE·lock a region act cannot upgrade to the tree; declare the tree before entering`);
199
+ await hold(treeAt(store), what);
200
+ const locks = join(store, 'locks');
201
+ const active = lockFiles(locks);
202
+ for (const file of [...active].sort())
203
+ await hold(join(locks, file), what);
204
+ for (const region of [...placesIn(dirname(store))].sort())
205
+ await hold(regionAt(store, region), what);
206
+ await hold(regionAt(store, ''), what);
207
+ }, act);
208
+ }
209
+ export const holds = (store, region) => [treeAt(store), regionAt(store, region)].some(owns);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * An alias is gone only when no pin will still name its digest. The pins are the root's uses lines and each world's own,
3
+ * minus the pins this batch itself withdraws, plus the pins it adds. Checked before the trial, so a batch that would
4
+ * orphan a pin lands nothing.
5
+ */
6
+ export declare function aliasPinned(root: string, signed: readonly string[]): readonly {
7
+ readonly scope: string;
8
+ readonly digest: string;
9
+ readonly by: readonly string[];
10
+ }[];
@@ -0,0 +1,41 @@
1
+ import { ENVELOPE } from '@lapxo/topos/wire';
2
+ import { fieldOf, fieldsOf } from "../fold/claims.js";
3
+ import { lockStanding } from "../fold/keys.js";
4
+ import { worldsIn } from "../fold/places.js";
5
+ import { ownLockOf } from "../fold/signed.js";
6
+ import { storeOf } from "./ledger.js";
7
+ const retracts = (withdraw, line) => Object.keys(withdraw).filter((key) => !ENVELOPE.has(key) && key !== 'value').every((key) => line[key] === withdraw[key]);
8
+ const pinScope = (scope) => scope === 'uses' || scope.startsWith('uses/') || scope.split('/').slice(1, -1).includes('uses');
9
+ const aliasWithdraw = (line) => fieldOf(line, 'value') === 'withdraw' && fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') !== 'reads';
10
+ /**
11
+ * An alias is gone only when no pin will still name its digest. The pins are the root's uses lines and each world's own,
12
+ * minus the pins this batch itself withdraws, plus the pins it adds. Checked before the trial, so a batch that would
13
+ * orphan a pin lands nothing.
14
+ */
15
+ export function aliasPinned(root, signed) {
16
+ const aliases = lockStanding(storeOf(root)).filter((line) => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') !== 'reads' && fieldOf(line, 'value') !== 'withdraw');
17
+ const digests = new Map();
18
+ for (const withdraw of signed.filter(aliasWithdraw)) {
19
+ const fields = fieldsOf(withdraw);
20
+ for (const line of aliases) {
21
+ if (!retracts(fields, fieldsOf(line)))
22
+ continue;
23
+ const digest = fieldOf(line, 'value');
24
+ const kept = signed.some((one) => fieldOf(one, 'scope') === fieldOf(line, 'scope') && fieldOf(one, 'value') === digest);
25
+ if (!kept)
26
+ digests.set(`${fieldOf(line, 'scope')}\0${digest}`, { scope: fieldOf(line, 'scope'), digest });
27
+ }
28
+ }
29
+ if (!digests.size)
30
+ return [];
31
+ const named = [
32
+ ...lockStanding(storeOf(root)).filter((line) => pinScope(fieldOf(line, 'scope')) && fieldOf(line, 'value') !== 'withdraw').map((line) => ({ scope: fieldOf(line, 'scope'), value: fieldOf(line, 'value'), fields: fieldsOf(line) })),
33
+ ...worldsIn(root).flatMap((place) => ownLockOf(root, place).filter((line) => pinScope(fieldOf(line, 'scope')) && fieldOf(line, 'value') !== 'withdraw').map((line) => ({ scope: `${place}/${fieldOf(line, 'scope')}`, value: fieldOf(line, 'value'), fields: fieldsOf(line) }))),
34
+ ];
35
+ const still = named.filter((pin) => !signed.some((line) => fieldOf(line, 'value') === 'withdraw' && retracts(fieldsOf(line), pin.fields)));
36
+ const added = signed.filter((line) => pinScope(fieldOf(line, 'scope')) && fieldOf(line, 'value') !== 'withdraw').map((line) => ({ scope: fieldOf(line, 'scope'), value: fieldOf(line, 'value') }));
37
+ return [...digests.values()].flatMap((alias) => {
38
+ const by = [...still.filter((pin) => pin.value === alias.digest).map((pin) => pin.scope), ...added.filter((pin) => pin.value === alias.digest).map((pin) => pin.scope)];
39
+ return by.length ? [{ scope: alias.scope, digest: alias.digest, by }] : [];
40
+ });
41
+ }
@@ -0,0 +1,7 @@
1
+ export interface Held {
2
+ readonly scope: string;
3
+ readonly bytes: number;
4
+ readonly at: string;
5
+ }
6
+ /** Bytes the store receipts name: a byte lives only while a standing line names it, and the receipts list what is there. */
7
+ export declare const heldBytes: (_root: string, store: string, _standing: readonly string[]) => number;
@@ -0,0 +1,23 @@
1
+ import { fieldOf } from "../fold/claims.js";
2
+ import { spanOf } from '@lapxo/topos/forms';
3
+ import { blobAt, ledgerLines, storeAt } from "./ledger.js";
4
+ const STORE = 'store';
5
+ const pathOf = (store, scope) => {
6
+ const name = scope.split('/').pop() ?? '';
7
+ if (scope.startsWith('cas/blobs/'))
8
+ return blobAt(store, name);
9
+ if (scope.startsWith('cas/laid/'))
10
+ return storeAt(store, 'cas', 'laid', name);
11
+ if (scope.startsWith('cas/answers/'))
12
+ return storeAt(store, 'cas', 'answers', name);
13
+ return storeAt(store, ...scope.split('/'));
14
+ };
15
+ const sizeOf = (line) => spanOf(fieldOf(line, 'value'))?.hi ?? 0;
16
+ function inventory(store) {
17
+ return ledgerLines(store, STORE).filter((line) => fieldOf(line, 'value') !== 'withdraw').map((line) => {
18
+ const scope = fieldOf(line, 'scope');
19
+ return { scope, bytes: sizeOf(line), at: pathOf(store, scope) };
20
+ });
21
+ }
22
+ /** Bytes the store receipts name: a byte lives only while a standing line names it, and the receipts list what is there. */
23
+ export const heldBytes = (_root, store, _standing) => inventory(store).reduce((sum, one) => sum + one.bytes, 0);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The ceilings of a batch that would stand unread: no reading meets them and no reading of the instrument's own
3
+ * carries their measure. A batch that also signs a reader goes through whole, since what a reader reads is known only
4
+ * once it runs, and so does a ceiling a gate of the same act reads, whose reading lands with it or refuses it.
5
+ */
6
+ export declare function inertIn(batch: readonly string[], input: {
7
+ readonly standing: readonly string[];
8
+ readonly ceiling: (line: string) => boolean;
9
+ readonly signsReader: boolean;
10
+ readonly observed: readonly string[];
11
+ readonly own: Readonly<Record<string, number>>;
12
+ readonly epoch: number;
13
+ }): readonly string[];
14
+ /** What each ceiling of a batch would bite, read off the summaries of the cone before anything lands. */
15
+ export declare function rankedIn(batch: readonly string[], fold: {
16
+ readonly standing: readonly string[];
17
+ readonly observed: readonly string[];
18
+ readonly own: Readonly<Record<string, number>>;
19
+ }): readonly string[];
20
+ /** Every ceiling asked, at resolution one: what the fold reads for it, beside its bound, or unmeasured where no reading answers it. */
21
+ export declare function readingsIn(ceilings: readonly string[], fold: {
22
+ readonly standing: readonly string[];
23
+ readonly observed: readonly string[];
24
+ readonly own: Readonly<Record<string, number>>;
25
+ readonly epoch: number;
26
+ }): readonly string[];
@@ -0,0 +1,33 @@
1
+ import { foldCeilings } from "../fold/ceilings.js";
2
+ import { fieldOf } from "../fold/claims.js";
3
+ import { isCeiling } from "../fold/configures.js";
4
+ import { placedReadings } from "../fold/observed.js";
5
+ import { boundOf, rankOf, regionsOf } from "../fold/region.js";
6
+ /**
7
+ * The ceilings of a batch that would stand unread: no reading meets them and no reading of the instrument's own
8
+ * carries their measure. A batch that also signs a reader goes through whole, since what a reader reads is known only
9
+ * once it runs, and so does a ceiling a gate of the same act reads, whose reading lands with it or refuses it.
10
+ */
11
+ export function inertIn(batch, input) {
12
+ if (input.signsReader)
13
+ return [];
14
+ const folded = foldCeilings({ standing: input.standing, ceilings: batch.filter(input.ceiling), observed: input.observed, own: input.own, epoch: input.epoch });
15
+ return [...folded.unread, ...folded.vacuous];
16
+ }
17
+ /** What each ceiling of a batch would bite, read off the summaries of the cone before anything lands. */
18
+ export function rankedIn(batch, fold) {
19
+ const regions = regionsOf(fold.standing, [...fold.standing.filter(isCeiling), ...batch], placedReadings(fold.observed));
20
+ return batch.map((line) => {
21
+ const bound = boundOf(regions.named, line);
22
+ const got = rankOf(regions, bound, fold.own);
23
+ const bite = got.cells ? `bites ${got.cells} cells in ${got.bars} bars, ${got.fresh} no ceiling above bites` : got.sums ? `bites ${got.sums} sums` : `bites nothing of ${got.met} met`;
24
+ return `RANK ${fieldOf(line, 'scope')} · ${bite} · region ${bound.region || '.'}`;
25
+ });
26
+ }
27
+ /** Every ceiling asked, at resolution one: what the fold reads for it, beside its bound, or unmeasured where no reading answers it. */
28
+ export function readingsIn(ceilings, fold) {
29
+ const got = foldCeilings({ standing: fold.standing, ceilings, observed: fold.observed, own: fold.own, epoch: fold.epoch });
30
+ const outside = new Map(got.over.map((one) => [one.ceiling, `${one.side} ${one.got}`]));
31
+ return ceilings.map((line) => ((scope, read) => `RANK@1 ${scope} · ${fieldOf(line, 'measure')} ${fieldOf(line, 'value')} · ${read === undefined ? 'unmeasured'
32
+ : `read ${read.length > 3 ? `${read.slice(0, 3).join(' · ')} +${read.length - 3}` : read.join(' · ')}`}${outside.has(scope) ? ` · ${outside.get(scope)}` : read === undefined ? '' : ' · met'}`)(fieldOf(line, 'scope'), got.readings.get(fieldOf(line, 'scope'))));
33
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A file goes from the tree through a line, never through a hand: its path withdrawn in the family the lock sources files
3
+ * by is the only way a file goes, and the fold refuses it while a block still reads the file — what a run depends on is judged by the run's
4
+ * own memo, which names every file each block read, and not by a search for the file's name in a test. A search misses
5
+ * a name built in a loop; the memo does not. A test file goes with its own blocks, and is refused while one of them
6
+ * is the only run of a vector or a sample, which it names.
7
+ */
8
+ export declare function coordinatesWithdrawn(root: string, store: string, signed: readonly string[], standing: readonly string[]): {
9
+ readonly taken: readonly string[];
10
+ readonly refused: readonly {
11
+ readonly path: string;
12
+ readonly by: string;
13
+ }[];
14
+ };
@@ -0,0 +1,51 @@
1
+ import { fieldOf } from "../fold/claims.js";
2
+ import { sourced } from "./vouched.js";
3
+ import { observedClaims } from "../fold/observed.js";
4
+ /**
5
+ * A file goes from the tree through a line, never through a hand: its path withdrawn in the family the lock sources files
6
+ * by is the only way a file goes, and the fold refuses it while a block still reads the file — what a run depends on is judged by the run's
7
+ * own memo, which names every file each block read, and not by a search for the file's name in a test. A search misses
8
+ * a name built in a loop; the memo does not. A test file goes with its own blocks, and is refused while one of them
9
+ * is the only run of a vector or a sample, which it names.
10
+ */
11
+ export function coordinatesWithdrawn(root, store, signed, standing) {
12
+ const family = sourced(standing);
13
+ const asked = signed.filter((line) => fieldOf(line, 'scope').startsWith(`${family}/`) && fieldOf(line, 'value') === 'withdraw')
14
+ .map((line) => fieldOf(line, 'scope').slice(family.length + 1));
15
+ if (!asked.length)
16
+ return { taken: [], refused: [] };
17
+ /** A run rewrites every block of a test file under one observation, so a block whose title moved still holds a row
18
+ * from an older one: what reads a file now is what the latest run of each test file wrote, and nothing older. */
19
+ const memo = observedClaims(root, store, { wait: false }).filter((line) => fieldOf(line, 'measure') === 'memo');
20
+ const fileOf = (scope) => scope.split('/').slice(0, 2).join('/');
21
+ const last = new Map();
22
+ for (const line of memo)
23
+ last.set(fileOf(fieldOf(line, 'scope')), fieldOf(line, 'at'));
24
+ const latest = new Map();
25
+ for (const line of memo) {
26
+ const scope = fieldOf(line, 'scope');
27
+ if (fieldOf(line, 'at') === last.get(fileOf(scope)))
28
+ latest.set(scope, fieldOf(line, 'value'));
29
+ }
30
+ const refused = [];
31
+ const taken = [];
32
+ const ownOf = (inside) => (/^test\/[^/]+\.test\.ts$/.test(inside) ? `vector-memo/${inside.slice('test/'.length)}/` : undefined);
33
+ const leaving = asked.map((path) => ownOf(path.slice(path.indexOf('/') + 1))).filter((own) => own !== undefined);
34
+ const runs = (value) => value.split('|').filter((token) => token.startsWith('read:') && !token.includes('#')).map((token) => token.slice('read:'.length));
35
+ for (const path of asked) {
36
+ const inside = path.slice(path.indexOf('/') + 1);
37
+ const own = ownOf(inside);
38
+ const reads = (value) => value.split('|').some((token) => token.startsWith(`in:${inside}:`) && /^[0-9a-f]{12}$/.test(token.slice(`in:${inside}:`.length)));
39
+ const by = [...latest].find(([scope, value]) => (own === undefined || !scope.startsWith(own)) && reads(value))?.[0];
40
+ const elsewhere = new Set([...latest].filter(([scope]) => !leaving.some((one) => scope.startsWith(one))).flatMap(([, value]) => runs(value)));
41
+ const only = own === undefined ? undefined : [...latest].filter(([scope]) => scope.startsWith(own))
42
+ .flatMap(([scope, value]) => runs(value).filter((name) => !elsewhere.has(name)).map((name) => `${scope}, the only run of ${name}`))[0];
43
+ if (by !== undefined)
44
+ refused.push({ path, by });
45
+ else if (only !== undefined)
46
+ refused.push({ path, by: only });
47
+ else
48
+ taken.push(path);
49
+ }
50
+ return { taken, refused };
51
+ }
@@ -0,0 +1,28 @@
1
+ export declare function storeOf(root: string): string;
2
+ /**
3
+ * The store of a place: every name under it is read here and nowhere else, whoever needs a place asks for it by its
4
+ * steps, and one append-only file per writer stands beside the bytes kept by digest — one per region for a writer
5
+ * whose lines are about regions, so a fold opens only the regions it reaches. A file is read once per stamp, and a
6
+ * ledger only grows: what it held is kept, and only the bytes appended since, from where it last ended a line, are read.
7
+ */
8
+ export declare function storeAt(store: string, ...steps: readonly string[]): string;
9
+ export declare function writerFile(store: string, writer: string): string;
10
+ export declare function ledgerLines(store: string, writer: string): readonly string[];
11
+ export declare function shardFile(store: string, writer: string, region: string): string;
12
+ export declare const shardLines: (store: string, writer: string, region: string) => readonly string[];
13
+ export declare function landShard(store: string, writer: string, region: string, lines: readonly string[]): number;
14
+ export declare function signedLines(store: string, writer: string): string[];
15
+ export declare function blobAt(store: string, digest: string): string;
16
+ /** A file replaced whole: the bytes land in a temp beside it and the name moves in one rename, so a killed write keeps the temp, never a torn file under the name a later read parses. */
17
+ export declare function replaceWhole(at: string, bytes: Uint8Array | string): void;
18
+ export declare const landBlob: (store: string, digest: string, bytes: Uint8Array) => string;
19
+ export declare function holdStore(store: string, kind: 'blobs' | 'laid' | 'answers', name: string, bytes: number): void;
20
+ export declare const memoAt: (store: string, key: string) => string;
21
+ export declare function landMemo(store: string, key: string, bytes: Uint8Array): string;
22
+ export declare const landEnvelope: (store: string, digest: string, bytes: string) => string;
23
+ export declare function land(store: string, writer: string, lines: readonly string[]): {
24
+ readonly at: string;
25
+ readonly appended: number;
26
+ };
27
+ export declare function keptVerdict(store: string, tree: string, lot: string): unknown[] | undefined;
28
+ export declare function keepVerdict(store: string, tree: string, lot: string, judged: readonly unknown[]): void;