@lapxo/bound 0.1.4 → 0.1.6

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 (66) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +26 -5
  3. package/TARGET.bound +84 -19
  4. package/dist/cli/accept.d.ts +5 -0
  5. package/dist/cli/accept.js +60 -14
  6. package/dist/cli/act-result.d.ts +3 -0
  7. package/dist/cli/act-result.js +48 -0
  8. package/dist/cli/owner.d.ts +2 -0
  9. package/dist/cli/owner.js +18 -2
  10. package/dist/cli/pass.js +15 -10
  11. package/dist/cli/place.js +8 -1
  12. package/dist/cli/verb.js +48 -2
  13. package/dist/fold/admission-history.d.ts +6 -0
  14. package/dist/fold/admission-history.js +22 -0
  15. package/dist/fold/claims.js +5 -1
  16. package/dist/fold/closed.js +44 -13
  17. package/dist/fold/digests.js +18 -11
  18. package/dist/fold/kept.js +9 -4
  19. package/dist/fold/keys.js +9 -6
  20. package/dist/fold/observed.js +196 -174
  21. package/dist/fold/resolved.js +6 -2
  22. package/dist/fold/signed.d.ts +5 -0
  23. package/dist/fold/signed.js +19 -15
  24. package/dist/host/act-cost.d.ts +13 -0
  25. package/dist/host/act-cost.js +25 -0
  26. package/dist/host/adapters/modules/javascript.d.ts +1 -0
  27. package/dist/host/adapters/modules/javascript.js +48 -0
  28. package/dist/host/capsule-process.js +22 -19
  29. package/dist/host/capsule.d.ts +2 -1
  30. package/dist/host/capsule.js +13 -12
  31. package/dist/host/io.d.ts +1 -1
  32. package/dist/host/io.js +1 -1
  33. package/dist/host/ports/ingress.d.ts +24 -0
  34. package/dist/host/ports/ingress.js +57 -0
  35. package/dist/host/ports/local-act.d.ts +34 -0
  36. package/dist/host/ports/local-act.js +100 -0
  37. package/dist/host/ports/publication.d.ts +10 -0
  38. package/dist/host/ports/publication.js +56 -0
  39. package/dist/host/reading-closures.d.ts +31 -0
  40. package/dist/host/reading-closures.js +146 -0
  41. package/dist/host/release.js +24 -2
  42. package/dist/host/selected-capsules.js +4 -4
  43. package/dist/host/walk.d.ts +6 -0
  44. package/dist/host/walk.js +193 -0
  45. package/dist/land/ledger.d.ts +1 -0
  46. package/dist/land/ledger.js +21 -2
  47. package/dist/land/owned.d.ts +1 -1
  48. package/dist/land/owned.js +5 -4
  49. package/dist/observe/files.js +12 -2
  50. package/dist/observe/regions.js +3 -5
  51. package/dist/render/field.d.ts +1 -7
  52. package/dist/render/field.js +6 -12
  53. package/dist/render/names.d.ts +1 -1
  54. package/dist/render/names.js +1 -1
  55. package/dist/render/view.js +16 -11
  56. package/docs/contracts/archive.md +11 -0
  57. package/docs/contracts/cli.md +19 -0
  58. package/docs/contracts/documents.md +35 -0
  59. package/docs/contracts/evidence-view.md +6 -0
  60. package/docs/contracts/publication.md +17 -0
  61. package/docs/contracts/typed-object.md +27 -0
  62. package/docs/contracts/view-walk.md +7 -0
  63. package/docs/contracts/views.md +29 -0
  64. package/docs/usage.md +6 -0
  65. package/package.json +6 -4
  66. package/receipts.bound +367 -77
@@ -1,6 +1,8 @@
1
+ import { actCosts } from "../host/act-cost.js";
2
+ import { verifiedReadingReceipts } from "../host/reading-closures.js";
1
3
  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 { byBytes, canonical, LOCK, parse, publicationOf, roleAt, roleClaimsOf } from '@lapxo/topos/wire';
5
+ import { RECEIPTS, fieldOf, foldClaims, isConfig, isWire } from "./claims.js";
4
6
  import { bytesDigest as hashBytes, fullDigest as hashFull } from "../host/digest.js";
5
7
  import { worldAt } from "./places.js";
6
8
  import { ownLockOf } from "./signed.js";
@@ -9,15 +11,14 @@ import { storeAt, storeOf } from "../land/ledger.js";
9
11
  import { coordinatesUnder, observeFile, observeText } from "../observe/files.js";
10
12
  import { carriedFrom } from "./resolved.js";
11
13
  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. */
14
+ /** 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 the largest measured act or unpartitioned host work, receiptsSeen how many receipts were read and how long that took. */
13
15
  let idle = 0;
14
- const began = Date.now();
15
16
  let seen = { count: 0, ms: 0 };
16
17
  export const idleCount = () => idle;
17
18
  export const noteIdle = () => {
18
19
  idle += 1;
19
20
  };
20
- export const actSeconds = () => Math.max(0, Math.round((Date.now() - began) / 1000));
21
+ export const actSeconds = () => actCosts.seconds();
21
22
  export const receiptsSeen = () => seen;
22
23
  const algorithmIn = (text) => text.match(/(?:value=|place:)([a-z0-9]+):[0-9a-f]{64}/)?.[1] ?? '';
23
24
  const lockStamp = (root, place) => {
@@ -55,6 +56,14 @@ function filesOf(root, place, region, inputs) {
55
56
  return [...new Set([...owned, ...observed, ...own])].filter(rel => rel !== receipt && !rel.split('/').some(part => part === '.bound' || part === '.cache' || part === '.git'));
56
57
  }
57
58
  const rooted = new Map();
59
+ function publicConfiguration(lines) {
60
+ if (!lines.length || !lines.every(isConfig))
61
+ return lines;
62
+ const folded = foldClaims(lines);
63
+ if (folded.forks.length)
64
+ throw Error('REFUSE·receipt disputed configuration standing');
65
+ return publicationOf(folded.standing.filter(line => fieldOf(line, 'value') !== 'withdraw'));
66
+ }
58
67
  function rootLines(root, place) {
59
68
  const name = place.replace(/\/$/, '');
60
69
  if (!name)
@@ -63,7 +72,7 @@ function rootLines(root, place) {
63
72
  const held = rooted.get(key);
64
73
  if (held !== undefined)
65
74
  return held;
66
- const standing = (observeText(join(root, LOCK)) ?? '').split('\n').filter(isWire);
75
+ const standing = publicConfiguration((observeText(join(root, LOCK)) ?? '').split('\n').filter(isWire));
67
76
  // Authority validates admission separately; it is not a rendered file input.
68
77
  const lines = standing.filter((line) => fieldOf(line, 'value') !== 'withdraw' && !fieldOf(line, 'scope').startsWith('keys/'));
69
78
  const take = takenFor(lines, `${name}/`, worldAt(root, name) ? { own: ownLockOf(root, name), tree: viewsOf(standing) } : undefined);
@@ -73,8 +82,20 @@ function rootLines(root, place) {
73
82
  }
74
83
  /** File-fold identity uses public inputs; admission history stays in its separate ledger guard. */
75
84
  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);
85
+ const files = [...filesOf(root, place, region, inputs)].sort(byBytes);
86
+ const own = join(root, place, LOCK);
87
+ const bytesOf = (rel) => {
88
+ const at = join(root, rel), bytes = observeFile(at) ?? new Uint8Array();
89
+ if (resolve(at) !== resolve(own))
90
+ return bytes;
91
+ const lines = new TextDecoder().decode(bytes).split('\n').filter(isWire);
92
+ // Object history keeps its own contract. Configuration's public identity is
93
+ // the existing Topos publication projection, not its admission envelopes.
94
+ if (!lines.length || !lines.every(isConfig))
95
+ return bytes;
96
+ return new TextEncoder().encode(publicConfiguration(lines).join('\n') + '\n');
97
+ };
98
+ return hashFull([...files.map((rel) => `${relative(join(root, place), join(root, rel))} ${hashBytes(bytesOf(rel), algorithm)}`), ...rootLines(root, place)], algorithm);
78
99
  }
79
100
  function namedReceipts(root, store) {
80
101
  const text = observeText(join(root, RECEIPTS));
@@ -89,13 +110,15 @@ function namedReceipts(root, store) {
89
110
  }
90
111
  return out;
91
112
  }
113
+ // Only the instrument's file seals describe file-region bytes. Reader receipts keep their own semantic identity.
114
+ const fileSeal = (line) => isWire(line) && fieldOf(line, 'measure') === 'bytes' && fieldOf(line, 'by') === 'bound';
92
115
  function ownBytes(root) {
93
- return (observeText(join(root, RECEIPTS)) ?? '').split('\n').filter(line => isWire(line) && fieldOf(line, 'measure') === 'bytes');
116
+ return (observeText(join(root, RECEIPTS)) ?? '').split('\n').filter(fileSeal);
94
117
  }
95
118
  function bytesMeet(root, place) {
96
119
  const shipped = (observeText(join(root, place, RECEIPTS)) ?? '').split('\n').filter(isWire);
97
120
  const inputs = carriedFrom(storeOf(root), shipped) ?? [];
98
- const lines = shipped.filter(line => fieldOf(line, 'measure') === 'bytes');
121
+ const lines = shipped.filter(fileSeal);
99
122
  const algorithm = algorithmIn(lines.join('\n'));
100
123
  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
124
  }
@@ -115,8 +138,8 @@ function rewritePlace(root, place) {
115
138
  const algorithm = algorithmIn(text);
116
139
  if (algorithm === '')
117
140
  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/'));
141
+ const kept = text.split('\n').filter((line) => line === '' || !fileSeal(line));
142
+ const regions = kept.filter((line) => isWire(line) && fieldOf(line, 'measure') === 'count' && fieldOf(line, 'by') === 'bound' && fieldOf(line, 'scope').startsWith('receipts/'));
120
143
  const bytes = regions.map((line) => {
121
144
  const scope = fieldOf(line, 'scope');
122
145
  const inputs = carriedFrom(storeOf(root), kept.filter(isWire)) ?? [];
@@ -146,11 +169,19 @@ export function meets(root, store, place) {
146
169
  const own = ownBytes(root);
147
170
  const carried = own.length ? carriedFrom(store, (observeText(join(root, RECEIPTS)) ?? '').split('\n').filter(isWire)) : undefined;
148
171
  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]) => {
172
+ let closed = (own.length > 0 ? ownClosed : named.size > 0) && [...named].every(([one, expected]) => {
150
173
  const bytes = observeFile(join(root, one, RECEIPTS));
151
174
  const algorithm = expected.split(':')[0];
152
175
  return bytes !== undefined && hashBytes(bytes, algorithm) === expected && meets(root, store, `${one}/`);
153
176
  });
177
+ if (closed && carried) {
178
+ const algorithm = algorithmIn(own.join('\n'));
179
+ const readings = verifiedReadingReceipts(store, carried, text => hashFull(text, algorithm));
180
+ if (readings === undefined)
181
+ closed = false;
182
+ else if (readings)
183
+ process.stderr.write(`RECEIPTS ${readings} read · 0 opened · reading closures\n`);
184
+ }
154
185
  seen = { count: named.size + (own.length > 0 ? 1 : 0), ms: Date.now() - at };
155
186
  return closed;
156
187
  }
@@ -1,5 +1,6 @@
1
+ import { ledgerLines, ledgerWriters } from "../land/ledger.js";
1
2
  import { readOnly } from "../host/read-only.js";
2
- import { appendFileSync, dirname, join, mkdirSync, readdirSync, relative, resolve } from "../host/io.js";
3
+ import { appendFileSync, dirname, join, mkdirSync, readdirSync, realpathSync, relative, resolve } from "../host/io.js";
3
4
  import { bytesDigest as bytesWith, fullDigest as fullWith } from "../host/digest.js";
4
5
  import { coordinatesUnder, observeFile, observeText, stampOf } from "../observe/files.js";
5
6
  import { releaseOf, signedLockLines } from "./keys.js";
@@ -7,6 +8,7 @@ import { EXTENSION, fieldOf, foldClaims, selfName } from "./claims.js";
7
8
  import { wireLine } from "./wire.js";
8
9
  import { publicLock, LOCK } from '@lapxo/topos/wire';
9
10
  import { builtinModules, createRequire } from 'node:module';
11
+ import { moduleImports } from "../host/adapters/modules/javascript.js";
10
12
  const held = new Map();
11
13
  let algorithmMs = 0;
12
14
  export const algorithmCost = () => algorithmMs;
@@ -31,10 +33,8 @@ function namedAlgorithm(store) {
31
33
  const contract = published === undefined ? undefined : wireLine(published, 'digest-algorithms');
32
34
  return contract === undefined ? undefined : fieldOf(contract, 'value').split('|')[0]?.split(':')[0];
33
35
  }
34
- for (const entry of readdirSync(join(store, 'ledger'), { withFileTypes: true })) {
35
- if (!entry.isFile() || !entry.name.endsWith(EXTENSION))
36
- continue;
37
- const text = observeText(join(store, 'ledger', entry.name)) ?? '';
36
+ for (const writer of ledgerWriters(store)) {
37
+ const text = ledgerLines(store, writer).join('\n');
38
38
  let from = 0;
39
39
  while (from < text.length) {
40
40
  const at = (() => {
@@ -110,6 +110,8 @@ export function coordinateDigest(store, at) {
110
110
  const stamp = stampOf(at);
111
111
  if (stamp?.kind !== 'file')
112
112
  return stamp === undefined ? undefined : bytesDigest(store, observeFile(at) ?? new Uint8Array());
113
+ if (readOnly())
114
+ return bytesDigest(store, observeFile(at) ?? new Uint8Array());
113
115
  const key = `${at} ${stamp.stamp}`;
114
116
  const memo = memoOf(store);
115
117
  const held = memo.get(key);
@@ -140,10 +142,10 @@ const imported = new Map();
140
142
  const importsOf = (store, at, digest) => {
141
143
  const key = `imports ${fullDigest(store, importsOf.toString())} ${digest}`;
142
144
  const memo = memoOf(store);
143
- const held = imported.get(digest) ?? memo.get(key)?.split('|').filter(Boolean);
145
+ const held = imported.get(digest) ?? (readOnly() ? undefined : memo.get(key)?.split('|').filter(Boolean));
144
146
  if (held !== undefined)
145
147
  return held;
146
- const found = [...new TextDecoder().decode(observeFile(at) ?? new Uint8Array()).matchAll(/(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s*)['"]([^'"]*)['"]/g)].map((m) => m[1] ?? '');
148
+ const found = moduleImports(new TextDecoder().decode(observeFile(at) ?? new Uint8Array()), at);
147
149
  imported.set(digest, found);
148
150
  memo.set(key, found.join('|'));
149
151
  if (!readOnly())
@@ -173,14 +175,19 @@ export function instrumentCoordinates(store, entry, standing = [], root = dirnam
173
175
  const digest = coordinateDigest(store, at);
174
176
  if (digest === undefined)
175
177
  return;
176
- seen.set(at, digest);
177
- for (const one of importsOf(store, at, digest)) {
178
+ // The host loader resolves links before loading a module. Count that module
179
+ // once, while retaining distinct modules even when their bytes are equal.
180
+ const module = realpathSync(at);
181
+ if (seen.has(module))
182
+ return;
183
+ seen.set(module, digest);
184
+ for (const one of importsOf(store, module, digest)) {
178
185
  if (one.startsWith('node:') || builtinModules.includes(one))
179
186
  continue;
180
187
  if (one.startsWith('.'))
181
- walk(resolve(dirname(at), one));
188
+ walk(resolve(dirname(module), one));
182
189
  else
183
- walk(createRequire(at).resolve(one));
190
+ walk(createRequire(module).resolve(one));
184
191
  }
185
192
  };
186
193
  walk(resolve(entry));
package/dist/fold/kept.js CHANGED
@@ -1,8 +1,9 @@
1
+ import { actLines, committedLocalActs } from "../host/ports/local-act.js";
1
2
  import { readOnly } from "../host/read-only.js";
2
3
  import { createHash, dirname, existsSync, join, mkdirSync, readdirSync, renameSync, writeFileSync } from "../host/io.js";
3
4
  import { bytesDigest, coordinateDigest } from "./digests.js";
4
5
  import { viewsOf } from "./views.js";
5
- import { landMemo, ledgerLines, memoAt, shardFile, writerFile } from "../land/ledger.js";
6
+ import { landMemo, ledgerLines, ledgerWriters, memoAt, shardFile, writerFile } from "../land/ledger.js";
6
7
  import { coordinatesUnder, observeFile, observeText } from "../observe/files.js";
7
8
  import { EXTENSION, fieldOf } from "./claims.js";
8
9
  import { keyFor, keyOf, lockSigners } from "./keys.js";
@@ -36,7 +37,7 @@ export function foldPoints(store, standing, under, keeper, instrument = '', obse
36
37
  `take ${digestOf(ledgerLines(store, 'take').filter((line) => reads.scopes.has(fieldOf(line, 'scope').replace(/^take\//, ''))))}`,
37
38
  `tree ${digestOf(ledgerLines(store, tree ?? '').filter((line) => (sourceOf(line) ?? '').startsWith(reads.place)))}`,
38
39
  ];
39
- const heads = (existsSync(join(store, 'ledger')) ? readdirSync(join(store, 'ledger')) : []).filter((name) => name.endsWith(EXTENSION)).map((name) => name.slice(0, -EXTENSION.length))
40
+ const heads = ledgerWriters(store)
40
41
  .filter((writer) => writer !== keeper && writer !== 'gate' && !writer.startsWith('reader') && !(reads && (signers.has(writer) || writer === 'judge' || writer === 'take' || writer === tree)))
41
42
  .map((writer) => `${writer} ${writer === liveWriter(store, standing) ? lastWord(store, writer) : bytesDigest(store, text(ledgerLines(store, writer).join('\n')))}`);
42
43
  const written = [...new Set([...viewsOf(standing).values()].flatMap((view) => ('regions' in view && view.shape ? [view.shape] : [])))].sort()
@@ -100,8 +101,12 @@ function readingContext(store) {
100
101
  const speaker = keyOf(store, 'read');
101
102
  const files = speaker === undefined ? [] : [writerFile(store, speaker), ...coordinatesUnder(dirname(shardFile(store, speaker, '')), store)
102
103
  .filter(file => file.endsWith(EXTENSION)).map(file => join(store, file))];
103
- return bytesDigest(store, text(files.filter(file => existsSync(file)).sort()
104
- .map(file => bytesDigest(store, observeFile(file) ?? new Uint8Array())).join('\n')));
104
+ const segments = speaker === undefined ? [] : committedLocalActs(store).flatMap(act => act.bundle.placements?.flatMap(placement => {
105
+ const records = actLines(placement.records).filter(line => fieldOf(line, 'by') === speaker);
106
+ return records.length ? [placement.place + '\n' + records.join('\n')] : [];
107
+ }) ?? []);
108
+ return bytesDigest(store, text([...files.filter(file => existsSync(file)).sort()
109
+ .map(file => bytesDigest(store, observeFile(file) ?? new Uint8Array())), ...segments].join('\n')));
105
110
  }
106
111
  export function keptPlace(store, under) {
107
112
  const key = observeText(memoAt(store, lastOf([`place ${under ?? '.'}`])))?.trim();
package/dist/fold/keys.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { dirname, existsSync, join } from "../host/io.js";
2
- import { ledgerLines, signedLines } from "../land/ledger.js";
3
- import { EXTENSION, fieldOf, foldClaims, isWire, LOCK, selfName } from "./claims.js";
4
- import { entriesIn, observeText } from "../observe/files.js";
2
+ import { ledgerLines, ledgerWriters, signedLines } from "../land/ledger.js";
3
+ import { fieldOf, foldClaims, isWire, LOCK, selfName } from "./claims.js";
4
+ import { observeText } from "../observe/files.js";
5
5
  const held = new Map();
6
+ const keyInputs = new Map();
6
7
  const merged = new Map();
7
8
  /**
8
9
  * Whose lines carry a class of writing, as the lock says it. Every ledger the store keeps is read, the key claims
@@ -47,13 +48,15 @@ export function signedLockLines(store, told = []) {
47
48
  return lockSigners(store, told).flatMap((id) => signedLines(store, id));
48
49
  }
49
50
  function keysOf(store, told) {
50
- const mine = told.length ? read(store, told) : held.get(store) ?? read(store);
51
+ const input = ledgerKeyLines(store).join('\n');
52
+ const mine = told.length ? read(store, told) : keyInputs.get(store) === input ? held.get(store) ?? read(store) : read(store);
53
+ if (!told.length)
54
+ keyInputs.set(store, input);
51
55
  if (!told.length)
52
56
  held.set(store, mine);
53
57
  return mine;
54
58
  }
55
- const ledgerKeyLines = (store) => (existsSync(join(store, 'ledger')) ? entriesIn(join(store, 'ledger')).map((entry) => entry.name) : [])
56
- .filter((name) => name.endsWith(EXTENSION)).flatMap((name) => ledgerLines(store, name.slice(0, -EXTENSION.length)))
59
+ const ledgerKeyLines = (store) => ledgerWriters(store).flatMap(writer => ledgerLines(store, writer))
57
60
  .filter((line) => /\bsig=/.test(line) && /(?:^|\s)scope=keys\/[^/\s]+\s/.test(line));
58
61
  const releases = new Map();
59
62
  /**