@lapxo/bound 0.1.4 → 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 (44) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +26 -5
  3. package/TARGET.bound +83 -19
  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 +35 -1
  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 +195 -174
  14. package/dist/fold/resolved.js +6 -2
  15. package/dist/host/act-cost.d.ts +13 -0
  16. package/dist/host/act-cost.js +25 -0
  17. package/dist/host/adapters/modules/javascript.d.ts +1 -0
  18. package/dist/host/adapters/modules/javascript.js +48 -0
  19. package/dist/host/capsule-process.js +22 -19
  20. package/dist/host/capsule.js +5 -6
  21. package/dist/host/ports/ingress.d.ts +24 -0
  22. package/dist/host/ports/ingress.js +81 -0
  23. package/dist/host/reading-closures.d.ts +28 -0
  24. package/dist/host/reading-closures.js +146 -0
  25. package/dist/host/release.js +24 -2
  26. package/dist/host/walk.d.ts +6 -0
  27. package/dist/host/walk.js +193 -0
  28. package/dist/observe/regions.js +3 -5
  29. package/dist/render/field.d.ts +1 -7
  30. package/dist/render/field.js +6 -12
  31. package/dist/render/names.d.ts +1 -1
  32. package/dist/render/names.js +1 -1
  33. package/dist/render/view.js +16 -11
  34. package/docs/contracts/archive.md +11 -0
  35. package/docs/contracts/cli.md +11 -0
  36. package/docs/contracts/documents.md +35 -0
  37. package/docs/contracts/evidence-view.md +6 -0
  38. package/docs/contracts/publication.md +17 -0
  39. package/docs/contracts/typed-object.md +27 -0
  40. package/docs/contracts/view-walk.md +7 -0
  41. package/docs/contracts/views.md +29 -0
  42. package/docs/usage.md +6 -0
  43. package/package.json +6 -4
  44. package/receipts.bound +314 -92
@@ -38,9 +38,9 @@ function unpacked(digest, bytes) {
38
38
  }
39
39
  /**
40
40
  * A capsule is the bytes a digest names in the instrument's own store, and only when they hash to it: its own lock is
41
- * read from them, and the coordinate a lock names runs as a process of its own, like a reader. Each answer is kept in one
42
- * slot of the capsule, the place and the region; the body of the request is the hit, so a file that moved misses and
43
- * overwrites the slot, and a request asked again for the same bytes is read back and never run twice. Operational failures never enter the cache; refusals and abstentions are asked again.
41
+ * read from them, and the coordinate a lock names runs as a process of its own, like a reader. Each answer is kept by the
42
+ * digest of its complete declared request and implementation; distinct views retain distinct answers, and the same
43
+ * request reads its verified answer without running again. Operational failures never enter the cache; refusals and abstentions are asked again.
44
44
  */
45
45
  const capsules = new Map();
46
46
  /** A blob is loaded once in an act. A later ask for the same digest reads that capsule, and does not hash or unpack it again. */
@@ -93,10 +93,9 @@ function answered(digest, bytes, requests, names, tree, cacheStore = ownStore(),
93
93
  return requests.map(() => undefined);
94
94
  }
95
95
  const loader = entry !== undefined ? `${createHash('sha256').update(`${relative(tree, entry)}\n`).digest('hex')}\n` : child.length > 1 ? `${createHash('sha256').update(observeFile(child[0]) ?? new Uint8Array()).digest('hex')}\n` : '';
96
- const slotOf = (request) => sha(`${digest}\n${loader}${request.verb}\n${request.rootScope}\n${request.region ?? ''}`);
97
96
  const hitOf = (request) => sha(`${digest}\n${loader}${JSON.stringify(request)}`);
98
- const kept = requests.map((request) => storeAt(cacheStore, 'cas', 'answers', slotOf(request)));
99
97
  const hits = requests.map(hitOf);
98
+ const kept = hits.map(hit => storeAt(cacheStore, 'cas', 'answers', hit));
100
99
  const held = kept.map((at, i) => {
101
100
  const text = observeText(at);
102
101
  if (text === undefined)
@@ -123,7 +122,7 @@ function answered(digest, bytes, requests, names, tree, cacheStore = ownStore(),
123
122
  if (said?.kind === 'fact') {
124
123
  const text = JSON.stringify({ format: 'capsule-answer/2', hit: hits[i], said });
125
124
  replaceWhole(kept[i], text);
126
- holdStore(cacheStore, 'answers', slotOf(request), Buffer.byteLength(text));
125
+ holdStore(cacheStore, 'answers', hits[i], Buffer.byteLength(text));
127
126
  }
128
127
  return said;
129
128
  });
@@ -0,0 +1,24 @@
1
+ /** Prepared by the admitted Topos contract and receiver signer. The host does not infer origins, coverage or epochs. */
2
+ export interface IngressBundle {
3
+ readonly evidence: string;
4
+ readonly receipt: string;
5
+ }
6
+ export interface IngressResult extends IngressBundle {
7
+ readonly kind: 'landed' | 'once';
8
+ readonly evidenceBytes: number;
9
+ readonly receiptBytes: number;
10
+ }
11
+ export interface IngressStorage {
12
+ publish(staged: string, committed: string): void;
13
+ }
14
+ /** Directory publication makes the pair visible together; there is no later ledger append that can expose half. */
15
+ export declare const nativeIngressStorage: IngressStorage;
16
+ /** Native signed receipt lines only. Foreign bytes are never added to the local ledger or epoch allocator. */
17
+ export declare function ingressReceipts(store: string): readonly string[];
18
+ /** Private staging directories are invisible to readers and may be discarded after host recovery. */
19
+ export declare function ingressEvidence(store: string, identity: string): IngressBundle | undefined;
20
+ export declare function ingressBundles(store: string): readonly IngressBundle[];
21
+ /** Verification and signing complete before staging. Replay performs no signer invocation or persistent write. */
22
+ export declare function commitIngress(store: string, identity: string, prepare: (localImportReceipts: readonly string[]) => Promise<IngressBundle>, verify: (bundle: IngressBundle) => boolean, storage?: IngressStorage): Promise<IngressResult>;
23
+ /** Costs distinguish delivered bytes from newly committed bytes; no count is called information or freedom. */
24
+ export declare function ingressCost(result: IngressResult, deliveredBytes: number, protocolBytes: number): string;
@@ -0,0 +1,81 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { requireEffects } from "../read-only.js";
5
+ import { underTheLock } from "../../land/act.js";
6
+ const syncDirectory = (path) => { const fd = openSync(path, 'r'); try {
7
+ fsyncSync(fd);
8
+ }
9
+ finally {
10
+ closeSync(fd);
11
+ } };
12
+ const durableFile = (path, text) => { const fd = openSync(path, 'wx', 0o600); try {
13
+ writeFileSync(fd, text);
14
+ fsyncSync(fd);
15
+ }
16
+ finally {
17
+ closeSync(fd);
18
+ } };
19
+ /** Directory publication makes the pair visible together; there is no later ledger append that can expose half. */
20
+ export const nativeIngressStorage = { publish(staged, committed) { renameSync(staged, committed); syncDirectory(join(committed, '..')); } };
21
+ const directory = (store) => join(store, 'ingress');
22
+ const nameOf = (identity) => {
23
+ if (!identity || identity.includes('\0'))
24
+ throw Error('REFUSE·ingress missing import identity');
25
+ return 'committed-' + encodeURIComponent(identity);
26
+ };
27
+ const read = (at) => ({ evidence: readFileSync(join(at, 'evidence.bound'), 'utf8'), receipt: readFileSync(join(at, 'receipt.bound'), 'utf8') });
28
+ const measured = (kind, bundle) => ({ kind, ...bundle, evidenceBytes: Buffer.byteLength(bundle.evidence), receiptBytes: Buffer.byteLength(bundle.receipt) });
29
+ /** Native signed receipt lines only. Foreign bytes are never added to the local ledger or epoch allocator. */
30
+ export function ingressReceipts(store) {
31
+ const root = directory(store);
32
+ if (!existsSync(root))
33
+ return [];
34
+ return readdirSync(root).filter(name => name.startsWith('committed-')).sort().flatMap(name => read(join(root, name)).receipt.split('\n').filter(Boolean));
35
+ }
36
+ /** Private staging directories are invisible to readers and may be discarded after host recovery. */
37
+ export function ingressEvidence(store, identity) {
38
+ const at = join(directory(store), nameOf(identity));
39
+ return existsSync(at) ? read(at) : undefined;
40
+ }
41
+ export function ingressBundles(store) {
42
+ const root = directory(store);
43
+ if (!existsSync(root))
44
+ return [];
45
+ return readdirSync(root).filter(name => name.startsWith('committed-')).sort().map(name => read(join(root, name)));
46
+ }
47
+ /** Verification and signing complete before staging. Replay performs no signer invocation or persistent write. */
48
+ export async function commitIngress(store, identity, prepare, verify, storage = nativeIngressStorage) {
49
+ requireEffects('foreign evidence ingress');
50
+ return underTheLock(store, 'ingress', async () => {
51
+ const root = directory(store), at = join(root, nameOf(identity));
52
+ if (existsSync(at)) {
53
+ const prior = read(at);
54
+ if (verify(prior) !== true)
55
+ throw Error('REFUSE·ingress committed bytes do not verify');
56
+ return measured('once', prior);
57
+ }
58
+ const bundle = await prepare(ingressReceipts(store));
59
+ if (!bundle.evidence || !bundle.receipt || verify(bundle) !== true)
60
+ throw Error('REFUSE·ingress evidence and receipt do not verify');
61
+ mkdirSync(root, { recursive: true });
62
+ const staged = join(root, '.stage-' + randomUUID());
63
+ mkdirSync(staged, { mode: 0o700 });
64
+ try {
65
+ durableFile(join(staged, 'evidence.bound'), bundle.evidence);
66
+ durableFile(join(staged, 'receipt.bound'), bundle.receipt);
67
+ syncDirectory(staged);
68
+ storage.publish(staged, at);
69
+ }
70
+ finally {
71
+ rmSync(staged, { recursive: true, force: true });
72
+ }
73
+ return measured('landed', bundle);
74
+ });
75
+ }
76
+ /** Costs distinguish delivered bytes from newly committed bytes; no count is called information or freedom. */
77
+ export function ingressCost(result, deliveredBytes, protocolBytes) {
78
+ if (!Number.isSafeInteger(deliveredBytes) || deliveredBytes < 0 || !Number.isSafeInteger(protocolBytes) || protocolBytes < 0)
79
+ throw Error('REFUSE·ingress invalid exchange byte measurement');
80
+ return `EXCHANGE ${deliveredBytes} payload bytes · ${protocolBytes} protocol bytes · ${result.kind === 'once' ? 0 : result.evidenceBytes} new evidence bytes · ${result.kind === 'once' ? 0 : result.receiptBytes} new receipt bytes`;
81
+ }
@@ -0,0 +1,28 @@
1
+ import type { Capsule } from './capsule.ts';
2
+ type File = {
3
+ readonly place: string;
4
+ readonly text: string;
5
+ };
6
+ export declare const inputLines: (files: readonly File[]) => readonly string[];
7
+ /** The selected world owns input projection and reading. The host only verifies closures and keeps native count/bytes receipts. */
8
+ export declare function readingClosures(options: {
9
+ root: string;
10
+ store: string;
11
+ reader: string;
12
+ speaker: string;
13
+ digest: (text: string) => string;
14
+ capsule: Capsule;
15
+ region: string;
16
+ projection: string;
17
+ allowsEmpty?: boolean;
18
+ files: readonly File[];
19
+ emit?: (line: string) => void;
20
+ }): {
21
+ lines: readonly string[];
22
+ read: number;
23
+ opened: number;
24
+ executed: number;
25
+ };
26
+ /** Verify the native reading carriers before a file seal may reuse them. These are semantic receipts, not file-region seals. */
27
+ export declare function verifiedReadingReceipts(store: string, lines: readonly string[], digest: (text: string) => string): number | undefined;
28
+ export {};
@@ -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 });
@@ -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
+ }
@@ -5,13 +5,11 @@ import { createHash } from 'node:crypto';
5
5
  import { basename, dirname, join, relative } from 'node:path';
6
6
  import { CAPSULE, LOCK, canonical, matches, PROTOCOL } from '@lapxo/topos/wire';
7
7
  import { handed, readsOf } from '@lapxo/topos/capsule';
8
- import { STATES } from '@lapxo/obligations';
9
8
  import { foldCeilings } from "../fold/ceilings.js";
10
9
  import { fieldOf, wireLinesOf as linesOf } from "../fold/claims.js";
11
10
  import { isCeiling } from "../fold/configures.js";
12
11
  import { keyFor } from "../fold/keys.js";
13
12
  import { casesHeld, placedReadings } from "../fold/observed.js";
14
- import { verdictsFor } from "../fold/paid.js";
15
13
  import { capsuleAt as isWorld, placesIn, spoken } from "../fold/places.js";
16
14
  import { rolesOf } from "../fold/roles.js";
17
15
  import { ownLockOf } from "../fold/signed.js";
@@ -71,9 +69,8 @@ function askedOf(fold) {
71
69
  return fold.under ? [...new Set(capsuleOf(fold).filter((line) => fieldOf(line, 'scope').startsWith('region/') && fieldOf(line, 'role') === 'render').map((line) => fieldOf(line, 'scope').slice('region/'.length)))]
72
70
  .map((name) => ((n) => fact(`asked/${name}`, `${n}..${n}`, 'interval', 'count'))(pinnersOf(fold).map(at).filter((names) => names.has(name)).length)) : [];
73
71
  }
74
- /** The place's verdict line as counts: what the fold lands, forks and refuses, each of the object's four states, and the ceilings it meets. */
72
+ /** File-fold readings and their ceilings; this path computes no object encounters. */
75
73
  function verdictOf(fold) {
76
- const states = Object.values(verdictsFor(fold));
77
74
  const named = (line) => ((cut) => [line.slice(0, cut < 0 ? line.length : cut).trim(), line.slice(cut < 0 ? line.length : cut + 3).trim() || line])(line.indexOf(' · '));
78
75
  const here = (fold.under ?? '').replace(/\/$/, '');
79
76
  const others = new Set(here ? placesIn(fold.root).filter((place) => place !== here) : []);
@@ -86,7 +83,8 @@ function verdictOf(fold) {
86
83
  return !others.has(head);
87
84
  };
88
85
  return [['claims', fold.claims], ['forks', fold.forks.length + (fold.second.verdict === 'forks' ? 1 : 0)], ['refused', fold.refused.length], ['vacuous', fold.vacuous], ['history', fold.history.length],
89
- ['met', fold.ceilings.read], ['short', fold.ceilings.short.length], ['over', fold.ceilings.over.length], ...STATES.map((one) => [one, states.filter((state) => state === one).length])].map(([name, n]) => fact(`verdict/${name}`, `${n}..${n}`, 'interval', 'count'))
86
+ ['met', fold.ceilings.read], ['short', fold.ceilings.short.length], ['over', fold.ceilings.over.length]].map(([name, n]) => fact(`verdict/${name}`, `${n}..${n}`, 'interval', 'count'))
87
+ .concat([fact('verdict/computation', 'readings'), fact('verdict/encounters', 'uncomputed')])
90
88
  .concat(fold.ceilings.short.filter((one) => mine(named(one)[0])).map((one) => (([scope, reading]) => fact(`unpaid/${scope}`, reading))(named(one))))
91
89
  .concat(fold.ceilings.over.filter((one) => mine(named(one)[0])).map((one) => (([scope, reading]) => fact(`outside/${scope}`, reading))(named(one))))
92
90
  .concat(fold.ceilings.unread.filter(mine).map((scope) => fact(`unpaid/${scope}`, 'unread')))
@@ -1,9 +1,3 @@
1
1
  import type { PlaceFold } from '../cli/place.ts';
2
- /**
3
- * The field of a place at resolution N: every bound placed at the place or at none, in the state its poles give, and
4
- * every coordinate cut to its first N steps in the worst state it holds. Interval poles with no value in common
5
- * conflict, as two signers on one key do; an alphabet that permits none is a ceiling like any other. A value read
6
- * outside the poles is forbidden; a bound nothing read, a reader that refused and a demand unpaid are required; the
7
- * rest is free. It is read off the region summaries and the landed readings, and no reader runs.
8
- */
2
+ /** Host file-region readings, grouped by coordinate prefix. These statuses do not compute object encounters. */
9
3
  export declare function field(fold: PlaceFold, at: number): readonly string[];