@lapxo/bound 0.1.3 → 0.1.5

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 (70) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +26 -5
  3. package/TARGET.bound +101 -22
  4. package/dist/cli/accept.js +21 -9
  5. package/dist/cli/owner.d.ts +2 -0
  6. package/dist/cli/owner.js +17 -2
  7. package/dist/cli/pass.js +15 -10
  8. package/dist/cli/place.js +8 -1
  9. package/dist/cli/verb.js +43 -3
  10. package/dist/fold/claims.js +5 -1
  11. package/dist/fold/closed.js +44 -13
  12. package/dist/fold/digests.js +5 -2
  13. package/dist/fold/observed.js +197 -174
  14. package/dist/fold/readers.d.ts +2 -0
  15. package/dist/fold/readers.js +3 -3
  16. package/dist/fold/resolved.js +6 -2
  17. package/dist/host/act-cost.d.ts +13 -0
  18. package/dist/host/act-cost.js +25 -0
  19. package/dist/host/adapters/modules/javascript.d.ts +1 -0
  20. package/dist/host/adapters/modules/javascript.js +48 -0
  21. package/dist/host/adapters/transport/http.d.ts +5 -0
  22. package/dist/host/adapters/transport/http.js +28 -0
  23. package/dist/host/capsule-process.d.ts +2 -1
  24. package/dist/host/capsule-process.js +24 -20
  25. package/dist/host/capsule.d.ts +1 -0
  26. package/dist/host/capsule.js +13 -16
  27. package/dist/host/cell-receipts.js +1 -1
  28. package/dist/host/content-store.d.ts +8 -0
  29. package/dist/host/content-store.js +31 -0
  30. package/dist/host/lifetime.d.ts +1 -0
  31. package/dist/host/lifetime.js +40 -0
  32. package/dist/host/objects.js +1 -1
  33. package/dist/host/ports/ingress.d.ts +24 -0
  34. package/dist/host/ports/ingress.js +81 -0
  35. package/dist/host/ports/transport.d.ts +1 -0
  36. package/dist/host/ports/transport.js +48 -0
  37. package/dist/host/process-lifetime.d.ts +11 -0
  38. package/dist/host/process-lifetime.js +12 -0
  39. package/dist/host/reading-closures.d.ts +28 -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 +38 -12
  43. package/dist/host/selected-topos.d.ts +4 -4
  44. package/dist/host/selected-topos.js +8 -30
  45. package/dist/host/source-content.d.ts +1 -1
  46. package/dist/host/source-content.js +15 -15
  47. package/dist/host/walk.d.ts +6 -0
  48. package/dist/host/walk.js +193 -0
  49. package/dist/observe/identity.d.ts +2 -0
  50. package/dist/observe/identity.js +3 -3
  51. package/dist/observe/regions.js +3 -5
  52. package/dist/observe/run.d.ts +2 -1
  53. package/dist/observe/run.js +5 -3
  54. package/dist/render/field.d.ts +1 -7
  55. package/dist/render/field.js +6 -12
  56. package/dist/render/names.d.ts +1 -1
  57. package/dist/render/names.js +1 -1
  58. package/dist/render/receipts.js +2 -1
  59. package/dist/render/view.js +16 -11
  60. package/docs/contracts/archive.md +11 -0
  61. package/docs/contracts/cli.md +11 -0
  62. package/docs/contracts/documents.md +35 -0
  63. package/docs/contracts/evidence-view.md +6 -0
  64. package/docs/contracts/publication.md +17 -0
  65. package/docs/contracts/typed-object.md +27 -0
  66. package/docs/contracts/view-walk.md +7 -0
  67. package/docs/contracts/views.md +29 -0
  68. package/docs/usage.md +6 -0
  69. package/package.json +6 -4
  70. package/receipts.bound +314 -94
@@ -0,0 +1,146 @@
1
+ import { actCosts } from "./act-cost.js";
2
+ import { mkdirSync, readFileSync, existsSync } from 'node:fs';
3
+ import { join, dirname } from 'node:path';
4
+ import { canonical, parse, PROTOCOL, byBytes } from '@lapxo/topos/wire';
5
+ import { replaceWhole, landShard, shardLines } from "../land/ledger.js";
6
+ import { fieldOf } from "../fold/claims.js";
7
+ import { readOnly } from "./read-only.js";
8
+ const fields = (line) => { const got = parse(line); if (got.kind !== 'fact')
9
+ throw Error('REFUSE·input projection contains a non-wire line'); return got.value.fields; };
10
+ export const inputLines = (files) => files.map(file => canonical({ scope: file.place, form: 'alphabet', measure: 'text', role: 'writes', value: 'lock', shape: file.place, about: file.text, by: 'host', at: 'policy:handed-input' }));
11
+ /** The selected world owns input projection and reading. The host only verifies closures and keeps native count/bytes receipts. */
12
+ export function readingClosures(options) {
13
+ const { root, store, reader, speaker, digest: hash, capsule, region, projection, files } = options, emit = options.emit ?? (() => { });
14
+ const algorithm = hash('').split(':')[0], handed = new Map(files.map(file => [file.place, file.text]));
15
+ if (handed.size !== files.length)
16
+ throw Error('REFUSE·input projection has duplicate handed coordinates');
17
+ const verify = () => { for (const file of files)
18
+ if (!readFileSync(join(root, file.place)).equals(Buffer.from(file.text)))
19
+ throw Error('REFUSE·input changed ' + file.place); };
20
+ verify();
21
+ const projected = capsule.ask([{ protocol: PROTOCOL, verb: 'render', rootScope: '', region: projection, at: 1, reads: ['**'], lines: inputLines(files), files: [] }])[0];
22
+ if (projected?.kind !== 'fact' || !Array.isArray(projected.lines))
23
+ throw Error('REFUSE·input projection ' + (projected?.why ?? 'no lines'));
24
+ const plans = new Map();
25
+ for (const line of projected.lines) {
26
+ const f = fields(line), scope = f.scope ?? '', resolution = /^receipt:(\d+)$/.exec(f.at ?? '');
27
+ if (!scope || plans.has(scope) || f.form !== 'alphabet' || f.measure !== 'reads' || f.role !== 'reads' || !resolution)
28
+ throw Error('REFUSE·input projection ambiguous reading ' + scope);
29
+ const inputs = (f.value ?? '').split('|').filter(Boolean), needs = (f.needs ?? '').split('|').filter(Boolean);
30
+ if (!inputs.length || new Set(inputs).size !== inputs.length || inputs.some(input => !handed.has(input)) || new Set(needs).size !== needs.length)
31
+ throw Error('REFUSE·input projection has unhanded or duplicate dependencies ' + scope);
32
+ const n = Number(resolution[1]);
33
+ if (!Number.isSafeInteger(n))
34
+ throw Error('REFUSE·input projection invalid resolution ' + scope);
35
+ plans.set(scope, { scope, inputs, needs, resolution: n, line: canonical({ scope, form: f.form, measure: f.measure, role: f.role, value: inputs.join('|'), needs: needs.join('|'), at: f.at }) });
36
+ }
37
+ if (!plans.size)
38
+ throw Error('REFUSE·input projection returned no readings');
39
+ const ordered = [], visiting = new Set(), done = new Set();
40
+ const visit = (scope) => { if (done.has(scope))
41
+ return; if (visiting.has(scope))
42
+ throw Error('REFUSE·input projection dependency cycle ' + scope); const p = plans.get(scope); if (!p)
43
+ throw Error('REFUSE·input projection missing reading ' + scope); visiting.add(scope); for (const need of p.needs)
44
+ visit(need); visiting.delete(scope); done.add(scope); ordered.push(p); };
45
+ for (const scope of plans.keys())
46
+ visit(scope);
47
+ const identity = hash([capsule.selection ?? '', capsule.digest, region, projection].join('\n')), by = speaker + ':' + identity.split(':')[1].slice(0, 12), journal = 'reading-closures/' + identity.split(':')[1];
48
+ const prior = shardLines(store, speaker, journal), results = new Map(), out = [];
49
+ const carried = (digest) => join(store, 'cas', 'carried', digest.split(':')[1] ?? '');
50
+ const saved = (text) => { const digest = hash(text), path = carried(digest); mkdirSync(dirname(path), { recursive: true }); if (!existsSync(path) || hash(readFileSync(path, 'utf8')) !== digest)
51
+ replaceWhole(path, text); return digest; };
52
+ const load = (digest) => { if (!digest.startsWith(algorithm + ':') || !/^[a-f0-9]+$/.test(digest.split(':')[1] ?? ''))
53
+ return; const path = carried(digest); if (!existsSync(path))
54
+ return; const text = readFileSync(path, 'utf8'); return hash(text) === digest ? text : undefined; };
55
+ let read = 0, opened = 0, executed = 0;
56
+ for (const plan of ordered) {
57
+ actCosts.measure(() => {
58
+ const closure = [plan.line, canonical({ scope: 'implementation', role: 'writes', form: 'alphabet', measure: 'digest', value: identity, by: 'host', at: 'policy:input-identity' }), ...plan.inputs.map(input => canonical({ scope: input, role: 'writes', form: 'alphabet', measure: 'digest', value: hash(handed.get(input)), by: 'host', at: 'policy:input-identity' })), ...plan.needs.map(need => canonical({ scope: need, role: 'writes', form: 'alphabet', measure: 'digest', value: results.get(need).digest, by: 'host', at: 'policy:input-identity' }))].sort(byBytes).join('\n') + '\n';
59
+ const input = hash(closure), scope = 'receipts/readings/' + hash(plan.scope).split(':')[1] + '/' + input.split(':')[1], matching = prior.filter(line => fieldOf(line, 'scope') === scope && fieldOf(line, 'measure') === 'bytes' && fieldOf(line, 'value') === input).at(-1);
60
+ const count = matching && prior.filter(line => fieldOf(line, 'scope') === scope && fieldOf(line, 'measure') === 'count' && fieldOf(line, 'by') === by).at(-1);
61
+ const heldOutput = count && load(fieldOf(count, 'at').replace(/^place:/, ''));
62
+ const heldLines = heldOutput?.split('\n').filter(Boolean);
63
+ const output = heldLines !== undefined && fieldOf(count, 'value') === `${heldLines.length}..${heldLines.length}` && heldLines.every(line => { const f = fields(line); return f.by === by && f.at === 'place:' + input; }) ? heldOutput : undefined;
64
+ let lines, payload;
65
+ if (output !== undefined && load(input) === closure) {
66
+ if (output === '' && !options.allowsEmpty)
67
+ throw Error('REFUSE·reading empty response is not admitted ' + plan.scope);
68
+ lines = output.split('\n').filter(Boolean);
69
+ for (const line of lines)
70
+ fields(line);
71
+ payload = fieldOf(count, 'at').replace(/^place:/, '');
72
+ }
73
+ else {
74
+ if (readOnly())
75
+ throw Error('REFUSE·preview reading closure is unpaid ' + plan.scope);
76
+ const started = process.hrtime.bigint();
77
+ const dependencies = plan.needs.flatMap(need => results.get(need).lines), answer = capsule.ask([{ protocol: PROTOCOL, verb: 'read', rootScope: plan.scope, name: plan.scope, region, at: plan.resolution, reads: ['**'], lines: [...inputLines(plan.inputs.map(place => ({ place, text: handed.get(place) }))), ...dependencies], files: [] }])[0];
78
+ if (answer?.kind !== 'fact' || !Array.isArray(answer.claims))
79
+ throw Error('REFUSE·reading ' + plan.scope + ' · ' + (answer?.why ?? 'no claims'));
80
+ lines = answer.claims.map((unknown) => { if (typeof unknown?.scope !== 'string' || typeof unknown?.measure !== 'string' || typeof unknown?.role !== 'string' || !unknown.bound || !['interval', 'enumerated'].includes(unknown.bound.kind))
81
+ throw Error('REFUSE·reading malformed claim ' + plan.scope); const span = unknown.bound; const text = canonical({ scope: unknown.scope, measure: unknown.measure, role: unknown.role, form: span.kind === 'interval' ? 'interval' : 'alphabet', value: span.kind === 'interval' ? `${span.lo}..${span.hi}` : Array.isArray(span.values) ? span.values.join('|') : '', by, at: 'place:' + input }); fields(text); return text; });
82
+ if (!lines.length && !options.allowsEmpty)
83
+ throw Error('REFUSE·reading empty response is not admitted ' + plan.scope);
84
+ executed++;
85
+ verify();
86
+ payload = saved(lines.length ? [...lines].sort(byBytes).join('\n') + '\n' : '');
87
+ if (saved(closure) !== input)
88
+ throw Error('REFUSE·receipt input closure mismatch');
89
+ const seconds = (Number(process.hrtime.bigint() - started) / 1e9).toFixed(6);
90
+ landShard(store, speaker, journal, [canonical({ scope, role: 'writes', form: 'interval', measure: 'count', value: `${lines.length}..${lines.length}`, by, at: 'place:' + payload }), canonical({ scope, role: 'writes', form: 'alphabet', measure: 'bytes', value: input, by, at: 'place:' + input }), canonical({ scope, role: 'writes', form: 'interval', measure: 's', value: seconds + '..' + seconds, by, at: 'place:' + input })]);
91
+ if (plan.resolution === 1)
92
+ opened++;
93
+ }
94
+ results.set(plan.scope, { digest: payload, lines });
95
+ out.push(canonical({ scope: plan.scope, role: 'writes', form: 'alphabet', measure: 'observed', value: input, by, at: 'place:' + input }), ...lines);
96
+ if (plan.resolution === 1) {
97
+ read++;
98
+ const native = shardLines(store, speaker, journal).filter(line => fieldOf(line, 'scope') === scope && ((fieldOf(line, 'measure') === 'bytes' && fieldOf(line, 'value') === input) || (fieldOf(line, 'measure') === 'count') || (fieldOf(line, 'measure') === 's' && fieldOf(line, 'at') === 'place:' + input))).map(line => {
99
+ const f = fields(line), id = f.measure === 'bytes' ? f.value : f.measure === 'count' ? (f.at ?? '').replace(/^place:/, '') : undefined;
100
+ if (id === undefined)
101
+ return line;
102
+ const bytes = load(id);
103
+ if (bytes === undefined)
104
+ throw Error('REFUSE·receipt missing verified carrier ' + id);
105
+ // The full-resolution native row carries the actual verified UTF-8 bytes.
106
+ // Its semantic count/digest is unchanged; a public copy needs no private CAS.
107
+ return canonical({ ...f, about: bytes });
108
+ });
109
+ out.push(canonical({ scope, role: 'writes', form: 'alphabet', measure: 'observed', value: payload, by, at: 'place:' + payload }), ...native);
110
+ }
111
+ });
112
+ }
113
+ verify();
114
+ emit(`RECEIPTS ${read} read · ${opened} opened · reading closures`);
115
+ return { lines: out, read, opened, executed };
116
+ }
117
+ /** Verify the native reading carriers before a file seal may reuse them. These are semantic receipts, not file-region seals. */
118
+ export function verifiedReadingReceipts(store, lines, digest) {
119
+ const units = lines.filter(line => fieldOf(line, 'scope').startsWith('receipts/readings/') && fieldOf(line, 'measure') === 'bytes' && fieldOf(line, 'role') === 'writes');
120
+ const load = (id, row) => {
121
+ const hex = id.split(':')[1];
122
+ if (!hex || !/^[a-f0-9]+$/.test(hex))
123
+ return;
124
+ const supplied = fields(row).about;
125
+ // A supplied carrier must authenticate even if an old local copy still exists.
126
+ if (supplied !== undefined)
127
+ return digest(supplied) === id ? supplied : undefined;
128
+ const path = join(store, 'cas', 'carried', hex);
129
+ if (!existsSync(path))
130
+ return;
131
+ const bytes = readFileSync(path, 'utf8');
132
+ return digest(bytes) === id ? bytes : undefined;
133
+ };
134
+ for (const unit of units) {
135
+ const input = fieldOf(unit, 'value'), scope = fieldOf(unit, 'scope'), by = fieldOf(unit, 'by'), counts = lines.filter(line => fieldOf(line, 'scope') === scope && fieldOf(line, 'measure') === 'count' && fieldOf(line, 'by') === by && fieldOf(line, 'role') === 'writes');
136
+ if (counts.length !== 1 || load(input, unit) === undefined)
137
+ return;
138
+ const count = counts[0], output = load(fieldOf(count, 'at').replace(/^place:/, ''), count);
139
+ if (output === undefined)
140
+ return;
141
+ const rows = output.split('\n').filter(Boolean);
142
+ if (fieldOf(count, 'value') !== `${rows.length}..${rows.length}` || rows.some(line => fieldOf(line, 'by') !== by || fieldOf(line, 'at') !== 'place:' + input))
143
+ return;
144
+ }
145
+ return units.length;
146
+ }
@@ -48,11 +48,33 @@ function layDir(from, into) {
48
48
  writeFileSync(dest, bytes, { flag: 'wx' });
49
49
  }
50
50
  }
51
- function swapPlace(place, next, digest) {
51
+ function swapPlace(place, next, digest, ready) {
52
52
  const old = `${next}.old`;
53
53
  if (lstatSync(old, { throwIfNoEntry: false }) !== undefined)
54
54
  throw new Error(`REFUSE·release recovery required for ${old}`);
55
55
  writeFileSync(join(next, laidMark), digest, { flag: 'wx' });
56
+ if (ready !== undefined) {
57
+ // Digest-addressed trees publish once. A verified concurrent winner is reused,
58
+ // never moved aside while another process may be importing its members.
59
+ if (ready()) {
60
+ rmSync(next, { recursive: true, force: true });
61
+ return;
62
+ }
63
+ try {
64
+ renameSync(next, place);
65
+ return;
66
+ }
67
+ catch (error) {
68
+ const code = error.code;
69
+ if (code !== 'EEXIST' && code !== 'ENOTEMPTY')
70
+ throw error;
71
+ if (ready()) {
72
+ rmSync(next, { recursive: true, force: true });
73
+ return;
74
+ }
75
+ // An existing invalid cache retains the repair/rollback path below.
76
+ }
77
+ }
56
78
  let moved = false;
57
79
  try {
58
80
  if (lstatSync(place, { throwIfNoEntry: false }) !== undefined) {
@@ -224,7 +246,7 @@ export function runtimeTreeAt(store, own, digest, contentStore = own, root = '')
224
246
  const next = nextPlace(place);
225
247
  try {
226
248
  layFiles(store, own, files, next);
227
- swapPlace(place, next, digest);
249
+ swapPlace(place, next, digest, () => observeText(join(place, laidMark)) === digest && layoutMatches(place, files));
228
250
  }
229
251
  catch (error) {
230
252
  rmSync(next, { recursive: true, force: true });
@@ -1,5 +1,7 @@
1
1
  import { capsuleAt } from "./capsule.js";
2
- import { capsuleOffers } from '@lapxo/topos/standing';
2
+ import { capsuleOffers, providerInputs, providerInputRegions, capsuleDescriptor, assertCapsuleDescriptor } from '@lapxo/topos/standing';
3
+ import { fullDigest } from "./digest.js";
4
+ import { wireAt, parse } from '@lapxo/topos/wire';
3
5
  import { selectedTopoi, toposAt } from "./selected-topos.js";
4
6
  import { canonical } from '@lapxo/topos/wire';
5
7
  import { fieldOf } from "../fold/claims.js";
@@ -8,16 +10,40 @@ import { archiveCoordinate } from "./archive.js";
8
10
  /** Only explicit capsule offers request execution. Forms/classes/views do not need an executable alias. */
9
11
  export function selectedCapsules(pins, resolve = capsuleAt, select = toposAt, host = ownLock()) {
10
12
  return selectedTopoi(pins, select).flatMap(({ digest, topos }) => capsuleOffers(topos).map(offer => {
11
- const projections = host.filter(line => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') === 'writes' && fieldOf(line, 'value') === offer.value);
12
- if (!projections.length || projections.some(line => !fieldOf(line, 'shape')))
13
- throw Error(`REFUSE·pin ${digest} capsule ${offer.value} needs one host projection`);
14
- const entries = [...new Set(projections.map(line => archiveCoordinate(fieldOf(line, 'shape'))))];
15
- if (entries.length !== 1)
16
- throw Error(`REFUSE·pin ${digest} capsule ${offer.value} has conflicting host projections`);
17
- const projected = { ...offer, shape: entries[0] };
18
- const capsule = resolve(offer.value, projected.shape);
19
- if (capsule === undefined)
20
- throw Error(`REFUSE·pin ${digest} · ${offer.scope} · offered capsule ${offer.value} unavailable`);
21
- return { capsule, offer: canonical(projected) };
13
+ const load = () => {
14
+ const projections = host.filter(line => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') === 'writes' && fieldOf(line, 'value') === offer.value);
15
+ if (!projections.length || projections.some(line => !fieldOf(line, 'shape')))
16
+ throw Error(`REFUSE·pin ${digest} capsule ${offer.value} needs one host projection`);
17
+ const entries = [...new Set(projections.map(line => archiveCoordinate(fieldOf(line, 'shape'))))];
18
+ if (entries.length !== 1)
19
+ throw Error(`REFUSE·pin ${digest} capsule ${offer.value} has conflicting host projections`);
20
+ const loaded = resolve(offer.value, entries[0]);
21
+ if (loaded === undefined)
22
+ throw Error(`REFUSE·pin ${digest} · ${offer.scope} · offered capsule ${offer.value} unavailable`);
23
+ return { capsule: loaded, offer: canonical({ ...offer, shape: entries[0] }) };
24
+ };
25
+ const descriptor = capsuleDescriptor(topos, offer.scope ?? '');
26
+ const loaded = descriptor === undefined ? load() : undefined;
27
+ const capsule = loaded?.capsule ?? {
28
+ digest: offer.value, selection: `${digest} ${offer.scope}`, ...descriptor, ask: requests => {
29
+ if (!requests.length)
30
+ return [];
31
+ const actual = load();
32
+ assertCapsuleDescriptor(descriptor.lines, actual.capsule.lines);
33
+ return actual.capsule.ask(requests);
34
+ },
35
+ };
36
+ const projected = loaded?.offer ?? canonical(offer);
37
+ if (!providerInputRegions(topos, offer.scope ?? '').length)
38
+ return { capsule, offer: projected };
39
+ const fields = ownLock().flatMap(line => { const p = parse(line); return p.kind === 'fact' ? [p.value.fields] : []; });
40
+ const wire = wireAt(fields, Number.MAX_SAFE_INTEGER), algorithm = wire && [...wire.digests][0];
41
+ if (!algorithm)
42
+ throw Error('REFUSE·input instrument has no admitted digest algorithm');
43
+ const selected = { ...capsule, selection: `${digest} ${offer.scope}`, ask: requests => capsule.ask(requests.map(request => {
44
+ const provider = providerInputs(topos, digest, offer.scope ?? '', offer.value, request.region ?? '', bytes => fullDigest(bytes, algorithm));
45
+ return provider === undefined ? request : { ...request, provider };
46
+ })) };
47
+ return { capsule: selected, offer: projected };
22
48
  }));
23
49
  }
@@ -6,10 +6,10 @@ export interface SelectedTopos {
6
6
  }
7
7
  export type ToposResolver = (digest: string) => StandingTopos;
8
8
  /** Resolve content using the instrument wire's admitted digest algorithms. No transport interpretation. */
9
- export declare function toposAt(digest: string): StandingTopos;
10
- export declare function contentAt(digest: string): Uint8Array;
11
- /** A declared JS context offer uses this host adapter only after selection; execution is not selection. */
12
- export declare function contextModuleAt(digest: string): string;
9
+ export declare function toposAt(digest: string, store?: string): StandingTopos;
10
+ export declare function contentAt(digest: string, store?: string): Uint8Array;
11
+ /** The declared module runs from the store that supplied its verified bytes. No copy or live-checkout fallback. */
12
+ export declare function contextModuleAt(digest: string, store?: string): string;
13
13
  /** Selection verifies standing identity. Requested capabilities verify their artifact bytes separately. */
14
14
  export declare function selectedTopoi(pins: readonly string[], resolve?: ToposResolver): readonly SelectedTopos[];
15
15
  /** Read a declared wire list without copying a world's claims or executing its offers.
@@ -1,40 +1,18 @@
1
- import { ContentNeeded } from "./content-needed.js";
2
- import { createHash } from 'node:crypto';
1
+ import { verifiedContent } from "./content-store.js";
3
2
  import { readStanding } from '@lapxo/topos/standing';
4
3
  import { parse, wireAt } from '@lapxo/topos/wire';
5
4
  import { fieldOf } from "../fold/claims.js";
6
- import { blobAt } from "../land/ledger.js";
7
- import { observeFile } from "../observe/files.js";
8
- import { ownStore, ownLock } from "../observe/runner.js";
9
5
  /** Resolve content using the instrument wire's admitted digest algorithms. No transport interpretation. */
10
- export function toposAt(digest) {
11
- const topos = readStanding(Buffer.from(contentAt(digest)).toString('utf8'));
6
+ export function toposAt(digest, store) {
7
+ const topos = readStanding(Buffer.from(contentAt(digest, store)).toString('utf8'));
12
8
  return topos;
13
9
  }
14
- export function contentAt(digest) {
15
- const fields = ownLock().flatMap(line => { const got = parse(line); return got.kind === 'fact' ? [got.value.fields] : []; });
16
- const wire = wireAt(fields, Number.MAX_SAFE_INTEGER);
17
- if (wire === null)
18
- throw Error('REFUSE·pin instrument has no admitted wire');
19
- const algorithms = wire.digests;
20
- const bytesAt = (name) => {
21
- const split = name.indexOf(':');
22
- const algorithm = name.slice(0, split), hex = name.slice(split + 1);
23
- if (split < 1 || !algorithms.has(algorithm) || !/^[a-f0-9]+$/.test(hex))
24
- throw Error(`REFUSE·pin ${name} digest is not admitted`);
25
- const bytes = observeFile(blobAt(ownStore(), name));
26
- if (bytes === undefined)
27
- throw new ContentNeeded(name);
28
- if (createHash(algorithm).update(bytes).digest('hex') !== hex)
29
- throw Error(`REFUSE·pin ${name} content hash mismatch · not laid`);
30
- return bytes;
31
- };
32
- return bytesAt(digest);
10
+ export function contentAt(digest, store) {
11
+ return verifiedContent(digest, store).bytes;
33
12
  }
34
- /** A declared JS context offer uses this host adapter only after selection; execution is not selection. */
35
- export function contextModuleAt(digest) {
36
- contentAt(digest);
37
- return blobAt(ownStore(), digest);
13
+ /** The declared module runs from the store that supplied its verified bytes. No copy or live-checkout fallback. */
14
+ export function contextModuleAt(digest, store) {
15
+ return verifiedContent(digest, store).path;
38
16
  }
39
17
  /** Selection verifies standing identity. Requested capabilities verify their artifact bytes separately. */
40
18
  export function selectedTopoi(pins, resolve = toposAt) {
@@ -2,7 +2,7 @@ export interface ContentPort {
2
2
  readonly algorithms: ReadonlySet<string>;
3
3
  readonly read: (digest: string) => Uint8Array | undefined;
4
4
  readonly write: (digest: string, bytes: Uint8Array) => void;
5
- readonly fetch: (url: string) => Promise<Uint8Array>;
5
+ readonly fetch: (url: string, digest?: string) => Promise<Uint8Array>;
6
6
  }
7
7
  /** Transport uses declared locations; only the requested digest admits bytes into the cache. */
8
8
  export declare function resolveSelectedContent(lines: readonly string[], port: ContentPort, requested: readonly string[]): Promise<readonly string[]>;
@@ -1,9 +1,10 @@
1
+ import { transportBytes } from "./ports/transport.js";
2
+ import { contentStore, verifiedContent } from "./content-store.js";
1
3
  import { ContentNeeded } from "./content-needed.js";
2
4
  import { createHash } from 'node:crypto';
3
5
  import { parse, wireAt } from '@lapxo/topos/wire';
4
- import { blobAt, landBlob } from "../land/ledger.js";
5
- import { observeFile } from "../observe/files.js";
6
- import { ownLock, ownStore } from "../observe/runner.js";
6
+ import { landBlob } from "../land/ledger.js";
7
+ import { ownLock } from "../observe/runner.js";
7
8
  /** Transport uses declared locations; only the requested digest admits bytes into the cache. */
8
9
  export async function resolveSelectedContent(lines, port, requested) {
9
10
  const fields = lines.flatMap(line => { const p = parse(line); return p.kind === 'fact' && p.value.fields.value !== 'withdraw' ? [p.value.fields] : []; });
@@ -44,12 +45,9 @@ export async function resolveSelectedContent(lines, port, requested) {
44
45
  throw Error(`REFUSE·pin ${digest} content unavailable · no declared source · not laid`);
45
46
  let last;
46
47
  for (const url of urls) {
47
- const parsed = new URL(url);
48
- if (!['https:', 'http:'].includes(parsed.protocol))
49
- throw Error(`REFUSE·source ${url} needs an explicit transport adapter`);
50
48
  let bytes;
51
49
  try {
52
- bytes = await port.fetch(url);
50
+ bytes = await port.fetch(url, digest);
53
51
  }
54
52
  catch (error) {
55
53
  last = error;
@@ -81,14 +79,16 @@ export async function resolveSources(standing, requested) {
81
79
  });
82
80
  const received = await resolveSelectedContent([...standing, ...bindings], {
83
81
  algorithms: wire.digests,
84
- read: digest => observeFile(blobAt(ownStore(), digest)),
85
- write: (digest, bytes) => { landBlob(ownStore(), digest, bytes); },
86
- fetch: async (url) => {
87
- const response = await fetch(url, { signal: AbortSignal.timeout(30000) });
88
- if (!response.ok)
89
- throw Error(`HTTP ${response.status}`);
90
- return new Uint8Array(await response.arrayBuffer());
91
- },
82
+ read: digest => { try {
83
+ return verifiedContent(digest).bytes;
84
+ }
85
+ catch (error) {
86
+ if (error instanceof ContentNeeded)
87
+ return undefined;
88
+ throw error;
89
+ } },
90
+ write: (digest, bytes) => { landBlob(contentStore(), digest, bytes); },
91
+ fetch: async (url, digest) => transportBytes(url, digest, [...ownLock(), ...standing]),
92
92
  }, requested);
93
93
  for (const digest of received)
94
94
  process.stdout.write(`FETCHED ${digest} · verified\n`);
@@ -0,0 +1,6 @@
1
+ /** Verified foreign histories stay separate from the local authority and local epoch inputs. */
2
+ export declare function foreignWalkLines(root: string): Promise<readonly string[]>;
3
+ /** A declared origin exports its own signed history. Foreign packets are retained with their contexts, never re-signed as local history. */
4
+ export declare function renderWalk(root: string, resolution: number | undefined, peer: readonly string[], key?: string, keyFile?: string, signer?: string): Promise<readonly string[]>;
5
+ /** The SDK proves the foreign prefix; the host admits the pair at one publication point. */
6
+ export declare function landWalk(root: string, lines: readonly string[], key?: string, keyFile?: string, signer?: string): Promise<readonly string[]>;
@@ -0,0 +1,193 @@
1
+ import { walkProjection, walkAt, walkSnapshot, walkHeaders, readWalkHeaders, readForeignWalk, walkAuthority, walkOrigin, walkInventory, walkSelection, isWalkHeader } from '@lapxo/topos/walk';
2
+ import { canonical, parse, fromLine, wireAt, matches, objectHistory, validateObjectContext } from '@lapxo/topos/wire';
3
+ import { objectProvider } from '@lapxo/topos/object-provider';
4
+ import { standingBytes } from '@lapxo/topos/standing';
5
+ import { fieldOf } from "../fold/claims.js";
6
+ import { lockStanding, lockLines } from "../fold/keys.js";
7
+ import { authorityFor, rootSigner } from "../fold/signers.js";
8
+ import { publicKeyOf } from "../fold/signers.js";
9
+ import { signaturesOf } from "../fold/digests.js";
10
+ import { fullDigest } from "./digest.js";
11
+ import { toposAt, contextModuleAt } from "./selected-topos.js";
12
+ import { signerFor } from "./signing.js";
13
+ import { signConsentLot, verifiesFields } from "../land/sign.js";
14
+ import { storeOf } from "../land/ledger.js";
15
+ import { signedOwnerLines, epochOf, localActLines, ownerOf } from "../cli/owner.js";
16
+ import { commitIngress, ingressBundles, ingressCost } from "./ports/ingress.js";
17
+ const refuse = (why) => { throw Error('REFUSE·walk ' + why); };
18
+ const fields = (line) => { const p = parse(line); if (p.kind !== 'fact')
19
+ return refuse('not a wire line'); return p.value.fields; };
20
+ const one = (lines, scope, measure) => {
21
+ const rows = lines.filter(line => fieldOf(line, 'scope') === scope && fieldOf(line, 'measure') === measure && fieldOf(line, 'value') !== 'withdraw');
22
+ if (rows.length !== 1)
23
+ return refuse('missing or conflicting ' + scope + ' ' + measure);
24
+ return fieldOf(rows[0], 'value');
25
+ };
26
+ function policy(root) {
27
+ const store = storeOf(root), standing = lockStanding(store), contract = walkSelection(standing);
28
+ const view = one(standing, 'view/walk', 'id');
29
+ if (view !== 'walk@*')
30
+ refuse('view/walk must declare walk@*');
31
+ const masks = standing.filter(line => fieldOf(line, 'scope') === 'view/walk').flatMap(line => fieldOf(line, 'needs').split('|')).filter(Boolean);
32
+ if (!masks.length)
33
+ refuse('view/walk has no declared history inputs');
34
+ const wire = wireAt(standing.map(fields), Number.MAX_SAFE_INTEGER), algorithm = wire?.digests.values().next().value;
35
+ if (!algorithm)
36
+ refuse('no admitted digest algorithm');
37
+ return { store, standing, contract, masks, digest: (bytes) => fullDigest(bytes, algorithm) };
38
+ }
39
+ function context(root, pin, own = false) {
40
+ const p = policy(root);
41
+ if (!own && !p.standing.some(line => fieldOf(line, 'scope').startsWith('uses/') && fieldOf(line, 'value') === pin))
42
+ refuse('origin context is not selected by digest');
43
+ const topos = toposAt(pin, p.store), authority = walkAuthority(topos.parts, verifiesFields);
44
+ const configuration = topos.parts.map(f => canonical({ ...f, by: 'target', epoch: '0' }));
45
+ return { pin, topos, authority, configuration, origin: walkOrigin(topos.parts), contract: walkSelection(topos.lines) };
46
+ }
47
+ function local(root) {
48
+ const p = policy(root), authority = authorityFor(lockLines(p.store), rootSigner(root), signaturesOf(p.store).admitted);
49
+ const history = signedOwnerLines(p.store).filter(line => p.masks.some(mask => matches(mask, fieldOf(line, 'scope'))));
50
+ if (history.some(line => authority.of(line).kind !== 'admitted'))
51
+ refuse('local history is not authenticated');
52
+ const selected = context(root, one(p.standing, 'walk/context', 'digest'), true);
53
+ return walkSnapshot(history, selected.contract.fields, p.digest, selected.origin, selected.contract.partition);
54
+ }
55
+ const headerInfo = (lines) => {
56
+ const roots = lines.filter(line => isWalkHeader(line) && fieldOf(line, 'scope') === 'walk/root');
57
+ if (roots.length !== 1)
58
+ return refuse('one signed root is required');
59
+ const at = fieldOf(roots[0], 'at');
60
+ if (!at.startsWith('receipt:'))
61
+ return refuse('root has no declared context');
62
+ return { pin: at.slice(8), base: fieldOf(roots[0], 'condition') };
63
+ };
64
+ async function validateHistory(root, selected, history) {
65
+ if (!selected.authority.authenticate(history))
66
+ refuse('origin history is not authenticated');
67
+ for (const line of history) {
68
+ const f = fields(line), decoded = fromLine(line, wireAt(selected.topos.parts, Number(f.epoch)));
69
+ if (decoded.kind !== 'fact')
70
+ refuse('origin wire ' + decoded.why);
71
+ }
72
+ const decoded = objectHistory([...selected.configuration, ...history]);
73
+ if (decoded.kind !== 'fact')
74
+ return refuse('origin history ' + decoded.why);
75
+ if (!decoded.value.objects.length)
76
+ return;
77
+ const pinned = new Set(selected.topos.lines);
78
+ const admit = (f) => Boolean(f.sig) ? selected.authority.of(canonical(f)).kind === 'admitted' : pinned.has(standingBytes([canonical(f)]).trim());
79
+ const provider = await objectProvider(decoded.value.objects, decoded.value.configuration, { admit, emit: () => { }, topos: pin => toposAt(pin, storeOf(root)), module: pin => contextModuleAt(pin, storeOf(root)) });
80
+ const checked = validateObjectContext(decoded.value.objects, provider);
81
+ if (checked.kind !== 'fact')
82
+ refuse('origin context ' + checked.why);
83
+ }
84
+ async function readBundle(root, bundle, earlier = []) {
85
+ const lines = bundle.evidence.split('\n').filter(Boolean), info = headerInfo(lines), p = policy(root);
86
+ const receipts = bundle.receipt.split('\n').filter(Boolean), authority = authorityFor(lockLines(p.store), rootSigner(root), signaturesOf(p.store).admitted);
87
+ if (receipts.length !== 1 || fieldOf(receipts[0], 'scope') !== 'receipts' || fieldOf(receipts[0], 'role') !== 'writes' || fieldOf(receipts[0], 'form') !== 'alphabet' || fieldOf(receipts[0], 'measure') !== 'digest' || fieldOf(receipts[0], 'at') !== 'receipt:' + info.pin || authority.of(receipts[0]).kind !== 'admitted')
88
+ refuse('retained import receipt is not authenticated');
89
+ const selected = context(root, info.pin);
90
+ const got = readForeignWalk(lines, { origin: selected.origin, regionOrigin: selected.origin, context: info.pin, base: info.base, fields: selected.contract.fields, partition: selected.contract.partition, digest: p.digest, previousPrefixes: [], authenticatePrefix: selected.authority.authenticate, authenticateEvidence: selected.authority.authenticate, validateOriginHistory: () => true });
91
+ if (fieldOf(receipts[0], 'value') !== got.identity)
92
+ refuse('retained import receipt does not authenticate this ingress');
93
+ await validateHistory(root, selected, [...new Set([...earlier, ...got.records])]);
94
+ return { ...got, context: info.pin, regions: walkSnapshot(got.records, selected.contract.fields, p.digest, got.origin, selected.contract.partition).regions };
95
+ }
96
+ async function verifiedIngress(root) {
97
+ const result = [], history = new Map();
98
+ const bundles = [...ingressBundles(storeOf(root))].sort((a, b) => epochOf(a.receipt.split('\n')) - epochOf(b.receipt.split('\n')));
99
+ for (const bundle of bundles) {
100
+ const info = headerInfo(bundle.evidence.split('\n').filter(Boolean)), origin = context(root, info.pin).origin;
101
+ if (result.some(prior => prior.origin === origin && prior.context !== info.pin))
102
+ refuse('origin context changed without declared lineage');
103
+ const prior = history.get(origin) ?? new Set();
104
+ const got = await readBundle(root, bundle, [...prior]);
105
+ for (const line of got.records)
106
+ prior.add(line);
107
+ history.set(origin, prior);
108
+ result.push(got);
109
+ }
110
+ return result;
111
+ }
112
+ async function inventory(root) {
113
+ const p = policy(root), regions = new Map(local(root).regions.map(region => [region.scope, region]));
114
+ for (const got of (await verifiedIngress(root)).sort((a, b) => a.senderEpoch - b.senderEpoch))
115
+ for (const region of got.regions)
116
+ regions.set(region.scope, region);
117
+ return walkInventory([[...regions.values()]], p.digest, context(root, one(p.standing, 'walk/context', 'digest'), true).contract.partition);
118
+ }
119
+ /** Verified foreign histories stay separate from the local authority and local epoch inputs. */
120
+ export async function foreignWalkLines(root) {
121
+ const verified = await verifiedIngress(root);
122
+ return [...new Set(verified.flatMap(got => got.records))];
123
+ }
124
+ /** A declared origin exports its own signed history. Foreign packets are retained with their contexts, never re-signed as local history. */
125
+ export async function renderWalk(root, resolution, peer, key, keyFile, signer) {
126
+ const p = policy(root), pin = one(p.standing, 'walk/context', 'digest'), selected = context(root, pin, true), own = local(root);
127
+ const known = peer.length ? (() => { const info = headerInfo(peer), remote = context(root, info.pin); return readWalkHeaders(peer, info.pin, info.base, remote.authority.authenticate); })() : walkSnapshot([], p.contract.fields, p.digest);
128
+ const projection = walkProjection(selected.contract.projections, resolution);
129
+ const ownInventory = projection === 'history' ? own : await inventory(root);
130
+ const selectedPacket = walkAt(ownInventory, known, resolution, selected.contract.projections), by = key ?? ownerOf(p.store), publicKey = publicKeyOf(root, by);
131
+ if (!publicKey)
132
+ refuse('unknown local signer ' + by);
133
+ const drafted = walkHeaders(selectedPacket, pin, known.root, projection !== 'inventory'), epoch = epochOf(localActLines(root));
134
+ const got = await signConsentLot(drafted, { by, epoch, algorithm: signaturesOf(p.store).era }, publicKey, signaturesOf(p.store).admitted, signerFor(p.standing, by, signer, keyFile));
135
+ if (got.kind !== 'fact')
136
+ return refuse(got.why);
137
+ if (!selected.authority.authenticate(got.lines) || !selected.authority.authenticate(selectedPacket.records))
138
+ refuse('local context does not authenticate exported history');
139
+ return [...got.lines, ...selectedPacket.records, `EXCHANGE ${selectedPacket.touched} touched regions · ${selectedPacket.open} open regions · ${Buffer.byteLength(selectedPacket.records.join('\n') + (selectedPacket.records.length ? '\n' : ''))} payload bytes`];
140
+ }
141
+ /** The SDK proves the foreign prefix; the host admits the pair at one publication point. */
142
+ export async function landWalk(root, lines, key, keyFile, signer) {
143
+ const p = policy(root), info = headerInfo(lines), selected = context(root, info.pin);
144
+ const retained = await verifiedIngress(root);
145
+ if (retained.some(prior => prior.origin === selected.origin && prior.context !== info.pin))
146
+ refuse('origin context changed without declared lineage');
147
+ const previous = (verified) => { const prefixes = new Map(); for (const prior of verified.filter(got => got.origin === selected.origin).sort((a, b) => a.senderEpoch - b.senderEpoch))
148
+ for (const region of prior.regions)
149
+ prefixes.set(region.scope, { ...region, senderEpoch: prior.senderEpoch }); return [...prefixes.values()]; };
150
+ const payload = lines.filter(line => !isWalkHeader(line));
151
+ const proof = (prefixes) => readForeignWalk(lines, { origin: selected.origin, regionOrigin: selected.origin, context: info.pin, base: info.base, fields: selected.contract.fields, partition: selected.contract.partition, digest: p.digest, previousPrefixes: prefixes, authenticatePrefix: selected.authority.authenticate, authenticateEvidence: selected.authority.authenticate, validateOriginHistory: () => true });
152
+ const raw = proof([]), known = retained.find(prior => prior.identity === raw.identity), got = known ? raw : proof(previous(retained));
153
+ const history = (prior) => [...new Set([...prior.filter(item => item.origin === selected.origin).flatMap(item => item.records), ...payload])];
154
+ await validateHistory(root, selected, history(retained));
155
+ if (!known && info.base !== (await inventory(root)).root)
156
+ refuse('stale receiver base');
157
+ if (!payload.length && !readWalkHeaders(lines, info.pin, info.base, selected.authority.authenticate).regions.length)
158
+ return ['ONCE 0 new evidence lines · 0 import receipts · no regions offered', `EXCHANGE 0 payload bytes · ${Buffer.byteLength(lines.join('\n') + '\n')} protocol bytes · 0 new evidence bytes · 0 new receipt bytes`];
159
+ const by = key ?? ownerOf(p.store), pub = publicKeyOf(root, by);
160
+ if (!pub)
161
+ refuse('unknown receiver signer ' + by);
162
+ const result = await commitIngress(p.store, got.identity, async () => {
163
+ if (info.base !== (await inventory(root)).root)
164
+ refuse('receiver changed before ingress');
165
+ const current = await verifiedIngress(root);
166
+ proof(previous(current));
167
+ await validateHistory(root, selected, history(current));
168
+ const next = epochOf(localActLines(root)) + 1;
169
+ const signed = await signConsentLot([got.importReceiptProposal], { by, epoch: next, algorithm: signaturesOf(p.store).era }, pub, signaturesOf(p.store).admitted, signerFor(p.standing, by, signer, keyFile));
170
+ if (signed.kind !== 'fact')
171
+ return refuse(signed.why);
172
+ const authority = authorityFor(lockLines(p.store), rootSigner(root), signaturesOf(p.store).admitted);
173
+ if (authority.of(signed.lines[0]).kind !== 'admitted')
174
+ refuse('receiver signer does not cover the import receipt');
175
+ const grammar = fromLine(signed.lines[0], wireAt(p.standing.map(fields), next));
176
+ if (grammar.kind !== 'fact')
177
+ refuse('receiver receipt wire ' + grammar.why);
178
+ return { evidence: lines.join('\n') + '\n', receipt: signed.lines.join('\n') + '\n' };
179
+ }, bundle => {
180
+ try {
181
+ const retainedLines = bundle.evidence.split('\n').filter(Boolean), retainedInfo = headerInfo(retainedLines);
182
+ if (retainedInfo.pin !== info.pin)
183
+ return false;
184
+ const retainedProof = readForeignWalk(retainedLines, { origin: selected.origin, regionOrigin: selected.origin, context: info.pin, base: retainedInfo.base, fields: selected.contract.fields, partition: selected.contract.partition, digest: p.digest, previousPrefixes: [], authenticatePrefix: selected.authority.authenticate, authenticateEvidence: selected.authority.authenticate, validateOriginHistory: () => true });
185
+ const receipts = bundle.receipt.split('\n').filter(Boolean), receipt = receipts[0] ?? '';
186
+ return receipts.length === 1 && retainedProof.identity === got.identity && fieldOf(receipt, 'scope') === 'receipts' && fieldOf(receipt, 'value') === got.identity && fieldOf(receipt, 'at') === 'receipt:' + info.pin && authorityFor(lockLines(p.store), rootSigner(root), signaturesOf(p.store).admitted).of(receipt).kind === 'admitted';
187
+ }
188
+ catch {
189
+ return false;
190
+ }
191
+ });
192
+ return [`${result.kind === 'once' ? 'ONCE' : 'INGRESS'} ${result.kind === 'once' ? 0 : got.records.length} new evidence lines · 1 ${result.kind === 'once' ? 'retained' : 'signed local'} import receipt`, result.receipt.trim(), ingressCost(result, Buffer.byteLength(payload.join('\n') + (payload.length ? '\n' : '')), Buffer.byteLength(lines.filter(isWalkHeader).join('\n') + '\n'))];
193
+ }
@@ -1,3 +1,4 @@
1
+ import type { ReaderLifetime } from '@lapxo/topos/wire';
1
2
  import type { Kind } from './run.ts';
2
3
  /** Where a reader's vectors lie: in its package, beside its source, under the name of its module, the folder the wire's vector family names. */
3
4
  export declare const vectorsOf: (module: string, standing: readonly string[]) => string;
@@ -13,6 +14,7 @@ export declare function identityOf(input: {
13
14
  readonly code: string;
14
15
  readonly lineage: readonly string[];
15
16
  readonly kept: readonly string[];
17
+ readonly lifetime?: ReaderLifetime;
16
18
  }): {
17
19
  readonly by: string;
18
20
  readonly lines: readonly string[];