@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,107 @@
1
+ import { basename } from "../host/io.js";
2
+ import { contracts, exclusive, point, region } from '@lapxo/obligations/field';
3
+ import { ENVELOPE, PROTOCOL, recordKind, signedBytes, parse } from '@lapxo/topos/wire';
4
+ export { CAPSULE, EXTENSION, LOCK, RECEIPTS } from '@lapxo/topos/wire';
5
+ import { ownRoot } from "../observe/runner.js";
6
+ const decoded = new Map();
7
+ /** Field access uses the Topos wire parser; decoding is shared for this CLI process. */
8
+ function wireFields(line) {
9
+ const held = decoded.get(line);
10
+ if (held !== undefined)
11
+ return held;
12
+ const got = parse(line);
13
+ const fields = got.kind === 'fact' ? got.value.fields : {};
14
+ decoded.set(line, fields);
15
+ return fields;
16
+ }
17
+ export const fieldOf = (line, name) => wireFields(line)[name] ?? '';
18
+ export const fieldsOf = (line) => ({ ...wireFields(line) });
19
+ export function selfName() { return basename(ownRoot()); }
20
+ export function isWire(line) { return line.startsWith(PROTOCOL); }
21
+ export const wireLinesOf = (text) => (text ?? '').split('\n').filter(isWire);
22
+ export const isConfig = (line) => { const k = recordKind(line); return k.kind === 'fact' && k.value === 'config'; };
23
+ export const sayingOf = (line) => !isConfig(line) ? ((p) => p.kind === 'fact' ? signedBytes(p.value.fields) : line)(parse(line)) : Object.entries(fieldsOf(line)).filter(([k]) => !ENVELOPE.has(k)).sort(([a], [b]) => (a < b ? -1 : 1)).map(([k, v]) => `${k}=${v}`).join(' ');
24
+ /** One key per claim: scope, role and measure; what a line says, its envelope set aside, is one however often it landed. */
25
+ export function claimKey(line) {
26
+ const f = fieldsOf(line);
27
+ return `${f['scope'] ?? ''} ${f['role'] ?? ''} ${f['measure'] ?? ''}`;
28
+ }
29
+ function contentOf(fields, at = true) {
30
+ return Object.keys(fields)
31
+ .filter((k) => !ENVELOPE.has(k) && k !== 'value' && (at || k !== 'at'))
32
+ .sort()
33
+ .map((k) => `${k}=${fields[k]}`)
34
+ .join(' ');
35
+ }
36
+ function regionsOf(line) {
37
+ return (fieldsOf(line)['needs'] ?? '').split('|').filter(Boolean).map((need) => region(need));
38
+ }
39
+ function contracted(line, sides, receipts) {
40
+ const scope = fieldsOf(line)['scope'] ?? '';
41
+ const mine = exclusive(regionsOf(line), sides.filter((other) => other !== line).map(regionsOf));
42
+ const touched = receipts.filter((r) => r.scope === scope).flatMap((r) => r.moved.map((coordinate) => point(coordinate)));
43
+ return contracts(mine, touched);
44
+ }
45
+ function retracts(withdraw, line) {
46
+ return Object.keys(withdraw)
47
+ .filter((k) => !ENVELOPE.has(k) && k !== 'value')
48
+ .every((k) => line[k] === withdraw[k]);
49
+ }
50
+ export function foldClaims(lines, receipts, _placed = false) {
51
+ const groups = new Map();
52
+ for (const line of lines.filter(isConfig)) {
53
+ const key = claimKey(line);
54
+ const group = groups.get(key) ?? [];
55
+ if (!group.includes(line))
56
+ group.push(line);
57
+ groups.set(key, group);
58
+ }
59
+ const standing = [...new Set(lines.filter(line => !isConfig(line)))];
60
+ const retracted = [];
61
+ const spent = [];
62
+ const vacuous = [];
63
+ const forks = [];
64
+ for (const [key, group] of groups) {
65
+ const parsed = group.map((line) => ({ line, fields: fieldsOf(line) }));
66
+ const withdraws = parsed.filter((p) => p.fields['value'] === 'withdraw');
67
+ const claims = parsed.filter((p) => p.fields['value'] !== 'withdraw');
68
+ const stands = new Set(claims.flatMap((p, i) => withdraws.some(w => retracts(w.fields, p.fields)) ? [] : [String(i)]));
69
+ retracted.push(...claims.filter((_, i) => !stands.has(String(i))).map((p) => p.line));
70
+ const kept = claims.filter((_, i) => stands.has(String(i)));
71
+ if (!kept.length) {
72
+ const lastByContent = new Map();
73
+ for (const w of withdraws)
74
+ lastByContent.set(contentOf(w.fields), w.line);
75
+ const shownWithdraws = [...lastByContent.values()];
76
+ standing.push(...shownWithdraws);
77
+ spent.push(...withdraws.map((w) => w.line).filter((line) => !shownWithdraws.includes(line)));
78
+ continue;
79
+ }
80
+ spent.push(...withdraws.map((w) => w.line));
81
+ const epochOf = (p) => Number(p.fields['epoch']) || 0;
82
+ const latestOf = new Map();
83
+ const who = (p) => `${p.fields['by'] ?? ''} ${contentOf(p.fields)}`;
84
+ for (const p of kept)
85
+ latestOf.set(who(p), Math.max(latestOf.get(who(p)) ?? 0, epochOf(p)));
86
+ const revised = kept.filter((p) => epochOf(p) < (latestOf.get(who(p)) ?? 0));
87
+ spent.push(...revised.map((p) => p.line));
88
+ // Epoch revisions are host history. Distinct origins remain separate inscriptions.
89
+ const values = kept.filter(p => !revised.includes(p));
90
+ const bySigner = new Map();
91
+ for (const p of values) {
92
+ const by = p.fields['by'] ?? '';
93
+ const identity = who(p);
94
+ const sayings = bySigner.get(identity) ?? new Set();
95
+ sayings.add(sayingOf(p.line));
96
+ bySigner.set(identity, sayings);
97
+ if (sayings.size > 1)
98
+ throw Error(`REFUSE·document ${key} · ${by} has two live inscriptions without withdrawal`);
99
+ }
100
+ const shown = [...new Set(values.map(p => p.line))];
101
+ const resting = receipts && shown.length > 1 ? shown.filter((line) => !contracted(line, shown, receipts)) : shown;
102
+ vacuous.push(...shown.filter((line) => !resting.includes(line)));
103
+ standing.push(...resting);
104
+ }
105
+ const accounted = new Set([...standing, ...retracted, ...spent, ...vacuous]);
106
+ return { standing, superseded: [...new Set([...spent, ...lines.filter((l) => !accounted.has(l))])], retracted, forks, vacuous };
107
+ }
@@ -0,0 +1,15 @@
1
+ export declare const idleCount: () => number;
2
+ export declare const noteIdle: () => void;
3
+ export declare const actSeconds: () => number;
4
+ export declare const receiptsSeen: () => {
5
+ readonly count: number;
6
+ readonly ms: number;
7
+ };
8
+ /** File-fold identity uses public inputs; admission history stays in its separate ledger guard. */
9
+ export declare function fileRegionDigest(root: string, place: string, region: string, algorithm: string, inputs?: readonly string[]): string;
10
+ export declare const instrumentKept: (store: string) => string;
11
+ export declare function instrumentKeep(store: string, id: string): void;
12
+ export declare function receiptPlaces(root: string, store: string): readonly string[];
13
+ export declare function closeReceipts(root: string, store: string, semantic?: ReadonlyMap<string, string>): Promise<readonly string[]>;
14
+ /** One comparison. A place meets its receipt; the root meets when every receipt it names meets. */
15
+ export declare function meets(root: string, store: string, place: string): boolean;
@@ -0,0 +1,168 @@
1
+ import { dirname, existsSync, join, mkdirSync, relative, resolve, statSync, writeFileSync } from "../host/io.js";
2
+ import { canonical, LOCK, parse, roleAt, roleClaimsOf } from '@lapxo/topos/wire';
3
+ import { RECEIPTS, fieldOf, isWire } from "./claims.js";
4
+ import { bytesDigest as hashBytes, fullDigest as hashFull } from "../host/digest.js";
5
+ import { worldAt } from "./places.js";
6
+ import { ownLockOf } from "./signed.js";
7
+ import { viewsOf } from "./views.js";
8
+ import { storeAt, storeOf } from "../land/ledger.js";
9
+ import { coordinatesUnder, observeFile, observeText } from "../observe/files.js";
10
+ import { carriedFrom } from "./resolved.js";
11
+ import { takenFor } from "../fold/place-inputs.js";
12
+ /** A region is closed when the digest of its bytes is the one its receipt names. Folding a closed region is idle: idleCount is how many were already closed, actSeconds how long this act has run, receiptsSeen how many receipts were read and how long that took. */
13
+ let idle = 0;
14
+ const began = Date.now();
15
+ let seen = { count: 0, ms: 0 };
16
+ export const idleCount = () => idle;
17
+ export const noteIdle = () => {
18
+ idle += 1;
19
+ };
20
+ export const actSeconds = () => Math.max(0, Math.round((Date.now() - began) / 1000));
21
+ export const receiptsSeen = () => seen;
22
+ const algorithmIn = (text) => text.match(/(?:value=|place:)([a-z0-9]+):[0-9a-f]{64}/)?.[1] ?? '';
23
+ const lockStamp = (root, place) => {
24
+ const name = place.replace(/\/$/, '');
25
+ try {
26
+ const st = statSync(join(root, name, LOCK));
27
+ return `${st.mtimeMs}:${st.size}`;
28
+ }
29
+ catch {
30
+ return 'none';
31
+ }
32
+ };
33
+ function filesOf(root, place, region, inputs) {
34
+ const step = region.replace(/^receipts\//, '').replace(/\/$/, '');
35
+ const here = join(root, place);
36
+ const name = place.replace(/\/$/, '');
37
+ const own = existsSync(join(here, LOCK)) ? [name ? `${name}/${LOCK}` : LOCK] : [];
38
+ const receipt = `${name ? `${name}/` : ''}${RECEIPTS}`;
39
+ const claims = roleClaimsOf((observeText(join(here, LOCK)) ?? '').split('\n').filter(isWire).flatMap(line => {
40
+ const record = parse(line);
41
+ return record.kind === 'fact' ? [record.value.fields] : [];
42
+ }));
43
+ const owned = coordinatesUnder(join(here, step), root).filter(rel => roleAt(relative(here, join(root, rel)), claims) !== 'foreign');
44
+ // Foreign worlds are not the place's tree. Retain the external files actually handed to its readers.
45
+ const observed = inputs.filter(line => fieldOf(line, 'measure') === 'observed').flatMap(line => {
46
+ const coordinate = fieldOf(line, 'scope');
47
+ const at = resolve(here, coordinate);
48
+ const local = relative(here, at);
49
+ if (local === '..' || local.startsWith('../') || resolve(at) === resolve(here))
50
+ return [];
51
+ if (step !== '.' && step !== '' && local !== step && !local.startsWith(`${step}/`))
52
+ return [];
53
+ return existsSync(at) && statSync(at).isFile() ? [relative(root, at)] : [];
54
+ });
55
+ return [...new Set([...owned, ...observed, ...own])].filter(rel => rel !== receipt && !rel.split('/').some(part => part === '.bound' || part === '.cache' || part === '.git'));
56
+ }
57
+ const rooted = new Map();
58
+ function rootLines(root, place) {
59
+ const name = place.replace(/\/$/, '');
60
+ if (!name)
61
+ return [];
62
+ const key = `${root}\0${name}\0${lockStamp(root, '')}`;
63
+ const held = rooted.get(key);
64
+ if (held !== undefined)
65
+ return held;
66
+ const standing = (observeText(join(root, LOCK)) ?? '').split('\n').filter(isWire);
67
+ // Authority validates admission separately; it is not a rendered file input.
68
+ const lines = standing.filter((line) => fieldOf(line, 'value') !== 'withdraw' && !fieldOf(line, 'scope').startsWith('keys/'));
69
+ const take = takenFor(lines, `${name}/`, worldAt(root, name) ? { own: ownLockOf(root, name), tree: viewsOf(standing) } : undefined);
70
+ const got = lines.filter(take).map((line) => `root ${line}`);
71
+ rooted.set(key, got);
72
+ return got;
73
+ }
74
+ /** File-fold identity uses public inputs; admission history stays in its separate ledger guard. */
75
+ export function fileRegionDigest(root, place, region, algorithm, inputs = []) {
76
+ const files = filesOf(root, place, region, inputs);
77
+ return hashFull([...files.map((rel) => `${relative(join(root, place), join(root, rel))} ${hashBytes(observeFile(join(root, rel)) ?? new Uint8Array(), algorithm)}`), ...rootLines(root, place)], algorithm);
78
+ }
79
+ function namedReceipts(root, store) {
80
+ const text = observeText(join(root, RECEIPTS));
81
+ const carried = carriedFrom(store, (text ?? '').split('\n').filter(isWire));
82
+ const out = new Map();
83
+ for (const line of carried ?? []) {
84
+ if (!isWire(line) || fieldOf(line, 'measure') !== 'digest')
85
+ continue;
86
+ const place = /^write\/([^/]+)\/receipts\.bound$/.exec(fieldOf(line, 'scope'))?.[1];
87
+ if (place)
88
+ out.set(place, fieldOf(line, 'value'));
89
+ }
90
+ return out;
91
+ }
92
+ function ownBytes(root) {
93
+ return (observeText(join(root, RECEIPTS)) ?? '').split('\n').filter(line => isWire(line) && fieldOf(line, 'measure') === 'bytes');
94
+ }
95
+ function bytesMeet(root, place) {
96
+ const shipped = (observeText(join(root, place, RECEIPTS)) ?? '').split('\n').filter(isWire);
97
+ const inputs = carriedFrom(storeOf(root), shipped) ?? [];
98
+ const lines = shipped.filter(line => fieldOf(line, 'measure') === 'bytes');
99
+ const algorithm = algorithmIn(lines.join('\n'));
100
+ return lines.length > 0 && algorithm !== '' && lines.map((line) => fieldOf(line, 'value')).join('\n') === lines.map((line) => fileRegionDigest(root, place, fieldOf(line, 'scope'), algorithm, inputs)).join('\n');
101
+ }
102
+ const stampAt = (store) => storeAt(store, 'cas', 'instrument');
103
+ export const instrumentKept = (store) => (observeText(stampAt(store)) ?? '').trim();
104
+ export function instrumentKeep(store, id) {
105
+ mkdirSync(dirname(stampAt(store)), { recursive: true });
106
+ writeFileSync(stampAt(store), `${id}\n`);
107
+ }
108
+ export function receiptPlaces(root, store) {
109
+ const children = [...namedReceipts(root, store).keys()];
110
+ return ownBytes(root).length ? ['', ...children] : children;
111
+ }
112
+ function rewritePlace(root, place) {
113
+ const at = join(root, place, RECEIPTS);
114
+ const text = observeText(at) ?? '';
115
+ const algorithm = algorithmIn(text);
116
+ if (algorithm === '')
117
+ return;
118
+ const kept = text.split('\n').filter((line) => line === '' || !isWire(line) || fieldOf(line, 'measure') !== 'bytes');
119
+ const regions = kept.filter((line) => isWire(line) && fieldOf(line, 'measure') === 'count' && fieldOf(line, 'scope').startsWith('receipts/'));
120
+ const bytes = regions.map((line) => {
121
+ const scope = fieldOf(line, 'scope');
122
+ const inputs = carriedFrom(storeOf(root), kept.filter(isWire)) ?? [];
123
+ const now = fileRegionDigest(root, place, scope, algorithm, inputs);
124
+ return canonical({ scope, role: 'writes', form: 'alphabet', measure: 'bytes', value: now, at: `place:${now}`, by: fieldOf(line, 'by') || 'bound' });
125
+ });
126
+ writeFileSync(at, `${[...kept.filter((line) => line !== ''), ...bytes].join('\n')}\n`);
127
+ }
128
+ export async function closeReceipts(root, store, semantic) {
129
+ if (semantic) {
130
+ const lines = (observeText(join(root, RECEIPTS)) ?? '').split('\n').filter(isWire);
131
+ return [...semantic].filter(([scope, digest]) => !lines.some(line => fieldOf(line, 'scope') === scope && fieldOf(line, 'measure') === 'bytes' && fieldOf(line, 'value') === digest)).map(([scope]) => scope);
132
+ }
133
+ const open = receiptPlaces(root, store).filter((place) => !meets(root, store, `${place}/`));
134
+ for (const place of open) {
135
+ rewritePlace(root, place);
136
+ await new Promise((resolve) => { setImmediate(resolve); });
137
+ }
138
+ return open;
139
+ }
140
+ /** One comparison. A place meets its receipt; the root meets when every receipt it names meets. */
141
+ export function meets(root, store, place) {
142
+ const name = place.replace(/\/$/, '');
143
+ if (name === '') {
144
+ const at = Date.now();
145
+ const named = namedReceipts(root, store);
146
+ const own = ownBytes(root);
147
+ const carried = own.length ? carriedFrom(store, (observeText(join(root, RECEIPTS)) ?? '').split('\n').filter(isWire)) : undefined;
148
+ const ownClosed = own.length > 0 && carried !== undefined && carried.length > 0 && bytesMeet(root, '');
149
+ const closed = (own.length > 0 ? ownClosed : named.size > 0) && [...named].every(([one, expected]) => {
150
+ const bytes = observeFile(join(root, one, RECEIPTS));
151
+ const algorithm = expected.split(':')[0];
152
+ return bytes !== undefined && hashBytes(bytes, algorithm) === expected && meets(root, store, `${one}/`);
153
+ });
154
+ seen = { count: named.size + (own.length > 0 ? 1 : 0), ms: Date.now() - at };
155
+ return closed;
156
+ }
157
+ const lines = (observeText(join(root, place, RECEIPTS)) ?? '').split('\n').filter((line) => isWire(line) && fieldOf(line, 'measure') === 'bytes');
158
+ const lock = existsSync(join(root, name, LOCK));
159
+ if (lines.length > 0)
160
+ return bytesMeet(root, place);
161
+ if (lock)
162
+ return false;
163
+ const expected = namedReceipts(root, store).get(name);
164
+ const file = expected !== undefined ? observeFile(join(root, place, RECEIPTS)) : undefined;
165
+ const algorithm = file === undefined ? '' : algorithmIn(new TextDecoder().decode(file));
166
+ const closed = file !== undefined && algorithm !== '' && hashBytes(file, algorithm) === expected;
167
+ return closed;
168
+ }
@@ -0,0 +1,4 @@
1
+ /** Every reads line that configures nothing is a ceiling: a bound some reading has to meet. */
2
+ export declare function isCeiling(line: string): boolean;
3
+ /** A demand that names no place asks nothing of the tree: it tells a render what to write, and no take pays it. */
4
+ export declare function asksThePlace(line: string): boolean;
@@ -0,0 +1,22 @@
1
+ import { isWireClaim, parse } from '@lapxo/topos/wire';
2
+ import { fieldOf, isConfig } from "./claims.js";
3
+ import { isReaderLock } from "./observed.js";
4
+ import { isRoleLock } from "./roles.js";
5
+ import { isSignerLock } from "./signers.js";
6
+ function isEffectLock(line) {
7
+ const got = parse(line);
8
+ return got.kind === 'fact' && got.value.fields['measure'] === 'effect';
9
+ }
10
+ /** A lock line the instrument itself consumes — an effect it runs, a reader it loads, a role it walks by, a signer it admits, the wire it reads by. */
11
+ function configures(line) {
12
+ const got = parse(line);
13
+ return isEffectLock(line) || isReaderLock(line) || isRoleLock(line) || isSignerLock(line) || (got.kind === 'fact' && isWireClaim(got.value.fields));
14
+ }
15
+ /** Every reads line that configures nothing is a ceiling: a bound some reading has to meet. */
16
+ export function isCeiling(line) {
17
+ return isConfig(line) && fieldOf(line, 'role') === 'reads' && fieldOf(line, 'value') !== 'withdraw' && !configures(line);
18
+ }
19
+ /** A demand that names no place asks nothing of the tree: it tells a render what to write, and no take pays it. */
20
+ export function asksThePlace(line) {
21
+ return fieldOf(line, 'role') === 'demands' && fieldOf(line, 'needs') !== '';
22
+ }
@@ -0,0 +1,29 @@
1
+ export declare const algorithmCost: () => number;
2
+ /**
3
+ * The signature algorithms this tree admits, and the one it signs with now: each carries its era after its name, the
4
+ * wire's `era` line names the one that is current, and every other is history, read and never written again. A wire
5
+ * that names no era signs with the one that carries a digest, which is how the first era was named.
6
+ */
7
+ export declare function signaturesOf(store: string, told?: readonly string[]): {
8
+ readonly admitted: readonly string[];
9
+ readonly era: string;
10
+ };
11
+ export declare function eraOf(standing: readonly string[]): {
12
+ readonly admitted: readonly string[];
13
+ readonly era: string;
14
+ };
15
+ export declare const bytesDigest: (store: string, bytes: Uint8Array) => string;
16
+ export declare const fullDigest: (store: string, input: string | readonly string[]) => string;
17
+ export declare function coordinateDigest(store: string, at: string): string | undefined;
18
+ export declare function memoized(store: string, key: string, compute: () => string): string;
19
+ /**
20
+ * The digest of the instrument itself: the entry module and everything it imports, by their bytes, and the source of
21
+ * every region its lock says it depends on, followed as far as those regions depend — code it runs as much as its own.
22
+ * A region its own lock names a release for, laid where it needs it, is that release by digest: what it runs, never a
23
+ * sibling rebuilt beside it.
24
+ * A fold is a function of its points and the instrument is one of them, so a fold kept by another bound is another
25
+ * origin's fold — readable as history, never as this instrument's own state.
26
+ */
27
+ export declare function instrumentOf(store: string, entry: string, standing?: readonly string[], root?: string, releases?: readonly string[]): string;
28
+ export declare function instrumentDigest(store: string, entry: string, standing?: readonly string[], root?: string, releases?: readonly string[]): string;
29
+ export declare function instrumentCoordinates(store: string, entry: string, standing?: readonly string[], root?: string, releases?: readonly string[]): ReadonlyMap<string, string>;
@@ -0,0 +1,211 @@
1
+ import { appendFileSync, dirname, join, mkdirSync, readdirSync, relative, resolve } from "../host/io.js";
2
+ import { bytesDigest as bytesWith, fullDigest as fullWith } from "../host/digest.js";
3
+ import { coordinatesUnder, observeFile, observeText, stampOf } from "../observe/files.js";
4
+ import { releaseOf, signedLockLines } from "./keys.js";
5
+ import { EXTENSION, fieldOf, foldClaims, selfName } from "./claims.js";
6
+ import { wireLine } from "./wire.js";
7
+ import { publicLock, LOCK } from '@lapxo/topos/wire';
8
+ import { builtinModules, createRequire } from 'node:module';
9
+ const held = new Map();
10
+ let algorithmMs = 0;
11
+ export const algorithmCost = () => algorithmMs;
12
+ /**
13
+ * The algorithm this tree digests with: the one its wire names, read from the lock and told to the port, which keeps
14
+ * none of its own. A lock that names no algorithm is a tree nothing can be digested in, and it is refused as one.
15
+ */
16
+ function namedAlgorithm(store) {
17
+ const needle = 'scope=wire/digest-algorithms';
18
+ const older = 'scope=audit/wire/digest-algorithms';
19
+ let best = -1;
20
+ let name = '';
21
+ const release = releaseOf(store);
22
+ if (release) {
23
+ const said = release.find((line) => (fieldOf(line, 'scope') === 'wire/digest-algorithms' || fieldOf(line, 'scope') === 'audit/wire/digest-algorithms') && fieldOf(line, 'value') !== 'withdraw');
24
+ return said === undefined ? undefined : fieldOf(said, 'value').split('|').filter(Boolean)[0]?.split(':')[0];
25
+ }
26
+ if (!existsLedger(store)) {
27
+ const text = observeText(join(dirname(store), LOCK));
28
+ const lines = text?.split('\n').filter(line => line.startsWith('bound-lock/'));
29
+ const published = lines === undefined ? undefined : publicLock(lines);
30
+ const contract = published === undefined ? undefined : wireLine(published, 'digest-algorithms');
31
+ return contract === undefined ? undefined : fieldOf(contract, 'value').split('|')[0]?.split(':')[0];
32
+ }
33
+ for (const entry of readdirSync(join(store, 'ledger'), { withFileTypes: true })) {
34
+ if (!entry.isFile() || !entry.name.endsWith(EXTENSION))
35
+ continue;
36
+ const text = observeText(join(store, 'ledger', entry.name)) ?? '';
37
+ let from = 0;
38
+ while (from < text.length) {
39
+ const at = (() => {
40
+ const next = text.indexOf(needle, from);
41
+ const was = text.indexOf(older, from);
42
+ if (next < 0)
43
+ return was;
44
+ if (was < 0)
45
+ return next;
46
+ return Math.min(next, was);
47
+ })();
48
+ if (at < 0)
49
+ break;
50
+ const start = text.lastIndexOf('\n', at) + 1;
51
+ const end = text.indexOf('\n', at);
52
+ const line = text.slice(start, end < 0 ? undefined : end);
53
+ from = end < 0 ? text.length : end + 1;
54
+ if (!line.includes(' sig='))
55
+ continue;
56
+ const epoch = Number(fieldOf(line, 'epoch')) || 0;
57
+ if (epoch < best)
58
+ continue;
59
+ best = epoch;
60
+ const value = fieldOf(line, 'value');
61
+ name = value === 'withdraw' ? '' : (value.split('|').filter(Boolean)[0]?.split(':')[0] ?? '');
62
+ }
63
+ }
64
+ return name || undefined;
65
+ }
66
+ function existsLedger(store) {
67
+ try {
68
+ return readdirSync(join(store, 'ledger')).length > 0;
69
+ }
70
+ catch (error) {
71
+ if (error.code === 'ENOENT')
72
+ return false;
73
+ throw error;
74
+ }
75
+ }
76
+ function algorithmOf(store) {
77
+ const mine = held.get(store);
78
+ if (mine !== undefined)
79
+ return mine;
80
+ const first = namedAlgorithm(store);
81
+ if (first === undefined)
82
+ throw new Error(`${selfName()}: REFUSE·runtime the lock names no \`wire/digest-algorithms\`: nothing can be digested here`);
83
+ held.set(store, first);
84
+ return first;
85
+ }
86
+ /**
87
+ * The signature algorithms this tree admits, and the one it signs with now: each carries its era after its name, the
88
+ * wire's `era` line names the one that is current, and every other is history, read and never written again. A wire
89
+ * that names no era signs with the one that carries a digest, which is how the first era was named.
90
+ */
91
+ export function signaturesOf(store, told = []) {
92
+ return eraOf(foldClaims([...signedLockLines(store, told), ...told]).standing);
93
+ }
94
+ export function eraOf(standing) {
95
+ const said = wireLine(standing, 'signature-algorithms');
96
+ const admitted = (said === undefined ? [] : fieldOf(said, 'value').split('|')).filter(Boolean);
97
+ const current = wireLine(standing, 'era');
98
+ const era = (current === undefined ? undefined : admitted.find((one) => one.endsWith(`:${fieldOf(current, 'value')}`))) ?? admitted.find((one) => one.includes(':'));
99
+ if (era === undefined)
100
+ throw new Error(`${selfName()}: REFUSE·runtime the lock names no signature algorithm by digest: nothing can be signed here`);
101
+ return { admitted, era };
102
+ }
103
+ export const bytesDigest = (store, bytes) => bytesWith(bytes, algorithmOf(store));
104
+ export const fullDigest = (store, input) => fullWith(input, algorithmOf(store));
105
+ const stamped = new Map();
106
+ const memoOf = (store) => stamped.get(store) ?? stamped.set(store, new Map((observeText(join(store, 'cas', `stamps${EXTENSION}`)) ?? '')
107
+ .split('\n').flatMap((row) => (row.indexOf('\t') > 0 ? [[row.slice(0, row.indexOf('\t')), row.slice(row.indexOf('\t') + 1)]] : [])))).get(store);
108
+ export function coordinateDigest(store, at) {
109
+ const stamp = stampOf(at);
110
+ if (stamp?.kind !== 'file')
111
+ return stamp === undefined ? undefined : bytesDigest(store, observeFile(at) ?? new Uint8Array());
112
+ const key = `${at} ${stamp.stamp}`;
113
+ const memo = memoOf(store);
114
+ const held = memo.get(key);
115
+ if (held !== undefined)
116
+ return held;
117
+ const digest = bytesDigest(store, observeFile(at) ?? new Uint8Array());
118
+ memo.set(key, digest);
119
+ mkdirSync(join(store, 'cas'), { recursive: true });
120
+ appendFileSync(join(store, 'cas', `stamps${EXTENSION}`), `${key}\t${digest}\n`);
121
+ return digest;
122
+ }
123
+ export function memoized(store, key, compute) {
124
+ const memo = memoOf(store);
125
+ const held = memo.get(key);
126
+ if (held !== undefined)
127
+ return held;
128
+ const value = compute();
129
+ memo.set(key, value);
130
+ mkdirSync(join(store, 'cas'), { recursive: true });
131
+ appendFileSync(join(store, 'cas', `stamps${EXTENSION}`), `${key}\t${value}\n`);
132
+ return value;
133
+ }
134
+ const imported = new Map();
135
+ const importsOf = (store, at, digest) => {
136
+ const key = `imports ${fullDigest(store, importsOf.toString())} ${digest}`;
137
+ const memo = memoOf(store);
138
+ const held = imported.get(digest) ?? memo.get(key)?.split('|').filter(Boolean);
139
+ if (held !== undefined)
140
+ return held;
141
+ const found = [...new TextDecoder().decode(observeFile(at) ?? new Uint8Array()).matchAll(/(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s*)['"]([^'"]*)['"]/g)].map((m) => m[1] ?? '');
142
+ imported.set(digest, found);
143
+ memo.set(key, found.join('|'));
144
+ appendFileSync(join(store, 'cas', `stamps${EXTENSION}`), `${key}\t${found.join('|')}\n`);
145
+ return found;
146
+ };
147
+ /**
148
+ * The digest of the instrument itself: the entry module and everything it imports, by their bytes, and the source of
149
+ * every region its lock says it depends on, followed as far as those regions depend — code it runs as much as its own.
150
+ * A region its own lock names a release for, laid where it needs it, is that release by digest: what it runs, never a
151
+ * sibling rebuilt beside it.
152
+ * A fold is a function of its points and the instrument is one of them, so a fold kept by another bound is another
153
+ * origin's fold — readable as history, never as this instrument's own state.
154
+ */
155
+ export function instrumentOf(store, entry, standing = [], root = dirname(resolve(entry)), releases = []) {
156
+ return instrumentDigest(store, entry, standing, root, releases).replace(/^[^:]*:/, '').slice(0, 12);
157
+ }
158
+ export function instrumentDigest(store, entry, standing = [], root = dirname(resolve(entry)), releases = []) {
159
+ const seen = instrumentCoordinates(store, entry, standing, root, releases);
160
+ return bytesDigest(store, new TextEncoder().encode([...seen.values()].sort().join('\n')));
161
+ }
162
+ export function instrumentCoordinates(store, entry, standing = [], root = dirname(resolve(entry)), releases = []) {
163
+ const seen = new Map();
164
+ const walk = (at) => {
165
+ if (seen.has(at))
166
+ return;
167
+ const digest = coordinateDigest(store, at);
168
+ if (digest === undefined)
169
+ return;
170
+ seen.set(at, digest);
171
+ for (const one of importsOf(store, at, digest)) {
172
+ if (one.startsWith('node:') || builtinModules.includes(one))
173
+ continue;
174
+ if (one.startsWith('.'))
175
+ walk(resolve(dirname(at), one));
176
+ else
177
+ walk(createRequire(at).resolve(one));
178
+ }
179
+ };
180
+ walk(resolve(entry));
181
+ const deps = standing.filter((line) => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'value') !== 'withdraw')
182
+ .map((line) => ({ region: fieldOf(line, 'scope').slice('dep/'.length).replace(/\*+$/, ''), on: fieldOf(line, 'value').split('|').filter((one) => one && one !== 'none') }));
183
+ const released = new Map(releases.filter((line) => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') === 'reads' && fieldOf(line, 'needs') && fieldOf(line, 'value') !== 'withdraw')
184
+ .map((line) => [fieldOf(line, 'scope').slice('dep/'.length), fieldOf(line, 'value')]));
185
+ const reached = new Set();
186
+ const follow = (region) => {
187
+ for (const name of deps.find((dep) => dep.region === region)?.on ?? []) {
188
+ if (released.has(name)) {
189
+ seen.set(`release:${name}`, released.get(name));
190
+ continue;
191
+ }
192
+ for (const next of deps.filter((dep) => dep.region.startsWith(`${name}/`) && !reached.has(dep.region))) {
193
+ reached.add(next.region);
194
+ for (const coordinate of coordinatesUnder(resolve(root, next.region), root))
195
+ walk(resolve(root, coordinate));
196
+ follow(next.region);
197
+ }
198
+ }
199
+ };
200
+ follow(deps.find((dep) => relative(root, resolve(entry)).startsWith(dep.region))?.region);
201
+ const home = relative(root, resolve(entry)).split('/')[0] ?? '';
202
+ for (const line of standing.filter((one) => fieldOf(one, 'scope').startsWith(`${home}/process/`) && fieldOf(one, 'kind') === 'process' && fieldOf(one, 'value') !== 'withdraw')) {
203
+ for (const need of fieldOf(line, 'needs').split('|').filter(Boolean))
204
+ walk(resolve(root, need));
205
+ }
206
+ for (const line of releases.filter(one => fieldOf(one, 'scope').startsWith('process/') && fieldOf(one, 'kind') === 'process' && fieldOf(one, 'value') !== 'withdraw')) {
207
+ for (const need of fieldOf(line, 'needs').split('|').filter(Boolean))
208
+ walk(resolve(root, need));
209
+ }
210
+ return seen;
211
+ }
@@ -0,0 +1,32 @@
1
+ import type { VerdictObservation } from '@lapxo/topos/authority';
2
+ /** The instrument maps an abstract relation to its declared snapshot vocabulary. Topos has no actor or origin policy. */
3
+ export interface SnapshotInput {
4
+ readonly declared: boolean;
5
+ readonly expected: {
6
+ readonly set?: string;
7
+ readonly standing: string;
8
+ readonly epoch: number;
9
+ };
10
+ readonly statement?: {
11
+ readonly admitted: boolean;
12
+ readonly set: string;
13
+ readonly standing: string;
14
+ readonly epoch: number;
15
+ };
16
+ readonly priorDisagreement?: boolean;
17
+ readonly comparison?: {
18
+ readonly admitted: boolean;
19
+ readonly set: string;
20
+ readonly verdict: 'agrees' | 'forks';
21
+ readonly epoch: number;
22
+ };
23
+ }
24
+ export declare function snapshotVerdict(input: SnapshotInput): {
25
+ verdict: "none" | "grey" | "agrees" | "forks" | "stale";
26
+ external: boolean;
27
+ epoch: number;
28
+ };
29
+ /** This selector belongs to the instrument, not the SDK's abstract evidence contract. */
30
+ export declare function attestationObservations(configuration: readonly Readonly<Record<string, string>>[], result: {
31
+ readonly verdict: string;
32
+ }): readonly VerdictObservation[];
@@ -0,0 +1,22 @@
1
+ import { compareEvidence } from '@lapxo/topos/authority';
2
+ export function snapshotVerdict(input) {
3
+ const statement = input.declared ? input.statement : undefined;
4
+ const comparison = input.comparison;
5
+ const result = compareEvidence({
6
+ expected: { reference: input.expected.set, value: input.expected.standing },
7
+ ...(statement ? { statement: { admitted: statement.admitted, reference: statement.set, value: statement.standing } } : {}),
8
+ ...(comparison ? { comparison: { admitted: comparison.admitted, reference: comparison.set, equal: comparison.verdict === 'agrees' } } : {}),
9
+ });
10
+ const words = { absent: 'none', unadmitted: 'grey', unresolved: 'stale', stale: 'stale', equal: 'agrees', different: 'forks' };
11
+ return { verdict: words[result.relation], external: result.relation !== 'absent' && (input.priorDisagreement === true || result.relation === 'different'),
12
+ epoch: result.source === 'comparison' ? comparison.epoch : statement?.epoch ?? 0 };
13
+ }
14
+ /** This selector belongs to the instrument, not the SDK's abstract evidence contract. */
15
+ export function attestationObservations(configuration, result) {
16
+ const values = [...new Set(configuration.filter(f => ['wire/attestation-result', 'audit/wire/attestation-result'].includes(f.scope ?? '') && f.value !== 'withdraw').map(f => f.value))];
17
+ if (!values.length)
18
+ return [];
19
+ if (values.length !== 1 || !values[0] || /[|=\s\\*]/.test(values[0]) || values[0].startsWith('/') || values[0].split('/').some(s => !s || s === '.' || s === '..'))
20
+ throw Error('REFUSE·authority conflicting or invalid attestation result coordinate');
21
+ return [{ coordinate: values[0], value: result.verdict }];
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import { parentPort, workerData } from "../host/io.js";
2
+ import { foldPlace, standingOf } from "../cli/place.js";
3
+ import { renderedOf, unrendered, viewOf } from "../render/view.js";
4
+ const data = workerData;
5
+ const standing = standingOf(data.root, data.place || undefined, data.told);
6
+ const fold = foldPlace(data.root, data.entry, data.place || undefined, false, { standing, observed: data.observed });
7
+ parentPort?.postMessage({
8
+ place: data.place,
9
+ texts: data.views.map((view) => {
10
+ const missing = unrendered(fold, view);
11
+ return { shape: view.shape, missing, text: missing.length ? '' : viewOf(fold, view), said: missing.length ? [] : renderedOf(fold, view) };
12
+ }),
13
+ });