@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,3 +1,4 @@
1
+ import { actCosts } from "./act-cost.js";
1
2
  import { boundedProcess } from "./process-lifetime.js";
2
3
  import { spawnSync } from 'node:child_process';
3
4
  import { PROTOCOL } from '@lapxo/topos/wire';
@@ -33,23 +34,25 @@ export function validResponse(value, request) {
33
34
  }
34
35
  /** Last-line JSON is the wire framing. No partial batch, failed exit or synthetic missing response is accepted. */
35
36
  export function executeCapsule(command, args, requests, limits = {}, lifetime) {
36
- const run = lifetime === undefined ? spawnSync(command, args, { input: JSON.stringify(requests), encoding: 'utf8', maxBuffer: 1 << 26, ...limits }) : boundedProcess(command, args, JSON.stringify(requests), lifetime);
37
- if ('error' in run && run.error)
38
- throw new CapsuleProcessError(`failed: ${run.error.message}`, run);
39
- if (run.signal !== null || run.status !== 0)
40
- throw new CapsuleProcessError(`exited ${run.status ?? `with signal ${run.signal}`}`, run);
41
- const last = /(?:^|\n)([^\n]*)$/.exec(run.stdout.trimEnd())?.[1] ?? '';
42
- let values;
43
- try {
44
- values = JSON.parse(last);
45
- }
46
- catch (error) {
47
- throw new CapsuleProcessError('returned malformed or absent JSON', { ...run, error });
48
- }
49
- if (!Array.isArray(values) || values.length !== requests.length || !values.every((value, i) => validResponse(value, requests[i]))) {
50
- throw new CapsuleProcessError(`returned an invalid batch; expected ${requests.length} responses`, run);
51
- }
52
- if (run.stderr)
53
- process.stderr.write(run.stderr);
54
- return values;
37
+ return actCosts.measure(() => {
38
+ const run = lifetime === undefined ? spawnSync(command, args, { input: JSON.stringify(requests), encoding: 'utf8', maxBuffer: 1 << 26, ...limits }) : boundedProcess(command, args, JSON.stringify(requests), lifetime);
39
+ if ('error' in run && run.error)
40
+ throw new CapsuleProcessError(`failed: ${run.error.message}`, run);
41
+ if (run.signal !== null || run.status !== 0)
42
+ throw new CapsuleProcessError(`exited ${run.status ?? `with signal ${run.signal}`}`, run);
43
+ const last = /(?:^|\n)([^\n]*)$/.exec(run.stdout.trimEnd())?.[1] ?? '';
44
+ let values;
45
+ try {
46
+ values = JSON.parse(last);
47
+ }
48
+ catch (error) {
49
+ throw new CapsuleProcessError('returned malformed or absent JSON', { ...run, error });
50
+ }
51
+ if (!Array.isArray(values) || values.length !== requests.length || !values.every((value, i) => validResponse(value, requests[i]))) {
52
+ throw new CapsuleProcessError(`returned an invalid batch; expected ${requests.length} responses`, run);
53
+ }
54
+ if (run.stderr)
55
+ process.stderr.write(run.stderr);
56
+ return values;
57
+ });
55
58
  }
@@ -1,3 +1,4 @@
1
+ import type { ReaderReference } from '@lapxo/topos/wire';
1
2
  import type { Declaration } from '@lapxo/topos/capsule';
2
3
  import type { Request, Response } from '@lapxo/topos/contract';
3
4
  export interface Capsule {
@@ -5,7 +6,7 @@ export interface Capsule {
5
6
  readonly digest: string;
6
7
  readonly declaration: Declaration;
7
8
  readonly lines: readonly string[];
8
- readonly ask: (requests: readonly Request[]) => readonly (Response | undefined)[];
9
+ readonly ask: (requests: readonly Request[], reader?: ReaderReference) => readonly (Response | undefined)[];
9
10
  }
10
11
  /** 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. */
11
12
  export declare function capsuleAt(digest: string, entry: string, store?: string): Capsule | undefined;
@@ -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. */
@@ -60,10 +60,10 @@ export function capsuleAt(digest, entry, store) {
60
60
  if (!entry || isAbsolute(entry) || (coordinate === '..' || coordinate.startsWith(`..${sep}`)) || entry.split(/[\\/]/).includes('..')) {
61
61
  throw new Error(`REFUSE·capsule ${digest} declared entry ${entry || '(absent)'} unavailable`);
62
62
  }
63
- const capsule = { digest, ...known, ask: (requests) => {
63
+ const capsule = { digest, ...known, ask: (requests, reader) => {
64
64
  if (!statSync(module, { throwIfNoEntry: false })?.isFile())
65
65
  throw new Error(`REFUSE·capsule ${digest} declared entry ${entry} unavailable`);
66
- return answered(digest, bytes, requests, names, root, cacheStore, module);
66
+ return answered(digest, bytes, requests, names, root, cacheStore, module, reader);
67
67
  } };
68
68
  capsules.set(identity, capsule);
69
69
  return capsule;
@@ -86,17 +86,18 @@ const spawnOf = (at, names) => {
86
86
  return child;
87
87
  };
88
88
  export const childFlags = (argv = process.execArgv) => argv.filter((flag, i) => !/^(-e|-p|--eval|--print)$/.test(argv[i - 1] ?? '') && !/^(-e|-p|--eval|--print|--input-type)(=|$)/.test(flag));
89
- function answered(digest, bytes, requests, names, tree, cacheStore = ownStore(), entry) {
89
+ function answered(digest, bytes, requests, names, tree, cacheStore = ownStore(), entry, reader) {
90
90
  const child = entry === undefined ? spawnOf(tree ?? unpacked(digest, bytes), names) : [entry];
91
91
  if (!child.length) {
92
92
  process.stderr.write(`LOAD regions · none of ${names.length} under the directories release/layout names\n`);
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
- const hitOf = (request) => sha(`${digest}\n${loader}${JSON.stringify(request)}`);
98
- const kept = requests.map((request) => storeAt(cacheStore, 'cas', 'answers', slotOf(request)));
96
+ const lifetime = readerLifetime(contentPolicy().flatMap(line => { const p = parse(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader ?? digest);
97
+ const policy = lifetime === undefined ? '' : JSON.stringify(lifetime) + '\n';
98
+ const hitOf = (request) => sha(`${digest}\n${loader}${policy}${JSON.stringify(request)}`);
99
99
  const hits = requests.map(hitOf);
100
+ const kept = hits.map(hit => storeAt(cacheStore, 'cas', 'answers', hit));
100
101
  const held = kept.map((at, i) => {
101
102
  const text = observeText(at);
102
103
  if (text === undefined)
@@ -115,7 +116,7 @@ function answered(digest, bytes, requests, names, tree, cacheStore = ownStore(),
115
116
  if (missing.length)
116
117
  process.stderr.write(`CAPSULE ${digest.slice(7, 19)} · ${requests.length} asked · ${missing.length} run · ${missing.map((one) => `${one.rootScope}${one.region}`).join(' ')}\n`);
117
118
  // Execute and validate the entire batch before committing any answer to the cache.
118
- const ran = !missing.length ? [] : executeCapsule(process.execPath, [...childFlags(), hostOf(), ...child], missing, {}, readerLifetime(contentPolicy().flatMap(line => { const p = parse(line); return p.kind === 'fact' ? [p.value.fields] : []; }), digest));
119
+ const ran = !missing.length ? [] : executeCapsule(process.execPath, [...childFlags(), hostOf(), ...child], missing, {}, lifetime);
119
120
  return requests.map((request, i) => {
120
121
  if (held[i] !== undefined)
121
122
  return held[i];
@@ -123,7 +124,7 @@ function answered(digest, bytes, requests, names, tree, cacheStore = ownStore(),
123
124
  if (said?.kind === 'fact') {
124
125
  const text = JSON.stringify({ format: 'capsule-answer/2', hit: hits[i], said });
125
126
  replaceWhole(kept[i], text);
126
- holdStore(cacheStore, 'answers', slotOf(request), Buffer.byteLength(text));
127
+ holdStore(cacheStore, 'answers', hits[i], Buffer.byteLength(text));
127
128
  }
128
129
  return said;
129
130
  });
@@ -149,5 +150,5 @@ export function locatedAt(tree, place, cacheStore = ownStore()) {
149
150
  }
150
151
  }
151
152
  const digest = `sha256:${hash.digest('hex')}`;
152
- return { digest, ...known, ask: (requests) => answered(digest, new Uint8Array(), requests, names, at, cacheStore) };
153
+ return { digest, ...known, ask: (requests, reader) => answered(digest, new Uint8Array(), requests, names, at, cacheStore, undefined, reader) };
153
154
  }
package/dist/host/io.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** The host's node surface: kernel folders import here, never `node:`. World reads still go through observeFile. */
2
2
  export { spawn, spawnSync } from 'node:child_process';
3
3
  export { createHash } from 'node:crypto';
4
- export { appendFileSync, closeSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
4
+ export { appendFileSync, closeSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
5
5
  export { tmpdir } from 'node:os';
6
6
  export { basename, dirname, join, relative, resolve } from 'node:path';
7
7
  export { fileURLToPath } from 'node:url';
package/dist/host/io.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /** The host's node surface: kernel folders import here, never `node:`. World reads still go through observeFile. */
2
2
  export { spawn, spawnSync } from 'node:child_process';
3
3
  export { createHash } from 'node:crypto';
4
- export { appendFileSync, closeSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
4
+ export { appendFileSync, closeSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
5
5
  export { tmpdir } from 'node:os';
6
6
  export { basename, dirname, join, relative, resolve } from 'node:path';
7
7
  export { fileURLToPath } from 'node:url';
@@ -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,57 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { requireEffects } from "../read-only.js";
4
+ import { underTheLock } from "../../land/act.js";
5
+ import { nativePublicationStorage, publishBundle } from "./publication.js";
6
+ /** Directory publication makes the pair visible together; there is no later ledger append that can expose half. */
7
+ export const nativeIngressStorage = nativePublicationStorage;
8
+ const directory = (store) => join(store, 'ingress');
9
+ const nameOf = (identity) => {
10
+ if (!identity || identity.includes('\0'))
11
+ throw Error('REFUSE·ingress missing import identity');
12
+ return 'committed-' + encodeURIComponent(identity);
13
+ };
14
+ const read = (at) => ({ evidence: readFileSync(join(at, 'evidence.bound'), 'utf8'), receipt: readFileSync(join(at, 'receipt.bound'), 'utf8') });
15
+ const measured = (kind, bundle) => ({ kind, ...bundle, evidenceBytes: Buffer.byteLength(bundle.evidence), receiptBytes: Buffer.byteLength(bundle.receipt) });
16
+ /** Native signed receipt lines only. Foreign bytes are never added to the local ledger or epoch allocator. */
17
+ export function ingressReceipts(store) {
18
+ const root = directory(store);
19
+ if (!existsSync(root))
20
+ return [];
21
+ return readdirSync(root).filter(name => name.startsWith('committed-')).sort().flatMap(name => read(join(root, name)).receipt.split('\n').filter(Boolean));
22
+ }
23
+ /** Private staging directories are invisible to readers and may be discarded after host recovery. */
24
+ export function ingressEvidence(store, identity) {
25
+ const at = join(directory(store), nameOf(identity));
26
+ return existsSync(at) ? read(at) : undefined;
27
+ }
28
+ export function ingressBundles(store) {
29
+ const root = directory(store);
30
+ if (!existsSync(root))
31
+ return [];
32
+ return readdirSync(root).filter(name => name.startsWith('committed-')).sort().map(name => read(join(root, name)));
33
+ }
34
+ /** Verification and signing complete before staging. Replay performs no signer invocation or persistent write. */
35
+ export async function commitIngress(store, identity, prepare, verify, storage = nativeIngressStorage) {
36
+ requireEffects('foreign evidence ingress');
37
+ return underTheLock(store, 'ingress', async () => {
38
+ const root = directory(store), at = join(root, nameOf(identity));
39
+ if (existsSync(at)) {
40
+ const prior = read(at);
41
+ if (verify(prior) !== true)
42
+ throw Error('REFUSE·ingress committed bytes do not verify');
43
+ return measured('once', prior);
44
+ }
45
+ const bundle = await prepare(ingressReceipts(store));
46
+ if (!bundle.evidence || !bundle.receipt || verify(bundle) !== true)
47
+ throw Error('REFUSE·ingress evidence and receipt do not verify');
48
+ publishBundle(at, { 'evidence.bound': bundle.evidence, 'receipt.bound': bundle.receipt }, storage);
49
+ return measured('landed', bundle);
50
+ });
51
+ }
52
+ /** Costs distinguish delivered bytes from newly committed bytes; no count is called information or freedom. */
53
+ export function ingressCost(result, deliveredBytes, protocolBytes) {
54
+ if (!Number.isSafeInteger(deliveredBytes) || deliveredBytes < 0 || !Number.isSafeInteger(protocolBytes) || protocolBytes < 0)
55
+ throw Error('REFUSE·ingress invalid exchange byte measurement');
56
+ 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`;
57
+ }
@@ -0,0 +1,34 @@
1
+ import type { PublicationStorage } from './publication.ts';
2
+ /** Prepared and verified by the selected Topos admission/result contract. */
3
+ export interface LocalActBundle {
4
+ readonly records: string;
5
+ readonly receipt: string;
6
+ readonly placements?: readonly {
7
+ place: string;
8
+ records: string;
9
+ }[];
10
+ }
11
+ export type LocalActVerification = (identity: string, bundle: LocalActBundle) => boolean;
12
+ export declare const actLines: (bytes: string) => readonly string[];
13
+ /** Journal integrity is not authority. Signature/coverage checks remain the
14
+ * same admission boundary; the hash algorithm here is the stored seal's named
15
+ * algorithm, not a new selection of the place's wire.
16
+ */
17
+ export declare function intactLocalAct(identity: string, bundle: LocalActBundle): boolean;
18
+ /** Committed segments are part of this ledger, not a second authority source. */
19
+ export declare function committedLocalActs(store: string): readonly {
20
+ identity: string;
21
+ bundle: LocalActBundle;
22
+ }[];
23
+ /** A public result is available only as a verified committed pair.
24
+ * No writer-tail guess, newest-result selection or implicit authority.
25
+ */
26
+ export declare function localActResult(store: string, identity: string, verify: LocalActVerification): LocalActBundle | undefined;
27
+ /** One publication point for records and their native result. Re-delivery
28
+ * reads the original pair without preparation, signing or a new local epoch.
29
+ * The caller must also integrate this boundary into admission-history readers;
30
+ * this storage port alone does not activate a grammar or land authority.
31
+ */
32
+ export declare function commitLocalAct(store: string, identity: string, regions: readonly string[], prepare: () => Promise<LocalActBundle>, verify: LocalActVerification, storage?: PublicationStorage): Promise<LocalActBundle & {
33
+ readonly kind: 'landed' | 'once';
34
+ }>;
@@ -0,0 +1,100 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { requireEffects } from "../read-only.js";
4
+ import { underTheRegions } from "../../land/act.js";
5
+ import { publishBundle, nativePublicationStorage } from "./publication.js";
6
+ import { actIdentity, parse, canonical } from '@lapxo/topos/wire';
7
+ import { fullDigest } from "../digest.js";
8
+ const atOf = (store, identity) => {
9
+ if (!identity || identity.includes('\0'))
10
+ throw Error('REFUSE·act missing identity');
11
+ return join(store, 'ledger', 'acts', 'committed-' + encodeURIComponent(identity));
12
+ };
13
+ const read = (at) => {
14
+ try {
15
+ const placements = readdirSync(at).filter(name => name.startsWith('place-') && name.endsWith('.bound')).map(name => ({
16
+ place: decodeURIComponent(name.slice(6, -6)), records: readFileSync(join(at, name), 'utf8'),
17
+ }));
18
+ return {
19
+ records: readFileSync(join(at, 'records.bound'), 'utf8'),
20
+ receipt: readFileSync(join(at, 'receipt.bound'), 'utf8'),
21
+ ...(placements.length ? { placements } : {}),
22
+ };
23
+ }
24
+ catch (error) {
25
+ throw Error(`REFUSE·act committed result unreadable · ${error.code ?? 'read failed'}`);
26
+ }
27
+ };
28
+ export const actLines = (bytes) => {
29
+ if (!bytes.endsWith('\n'))
30
+ throw Error('REFUSE·act truncated committed lines');
31
+ return bytes.slice(0, -1).split('\n');
32
+ };
33
+ /** Journal integrity is not authority. Signature/coverage checks remain the
34
+ * same admission boundary; the hash algorithm here is the stored seal's named
35
+ * algorithm, not a new selection of the place's wire.
36
+ */
37
+ export function intactLocalAct(identity, bundle) {
38
+ if (!bundle.receipt.endsWith('\n') || bundle.receipt.slice(0, -1).includes('\n'))
39
+ return false;
40
+ const receipt = parse(bundle.receipt.slice(0, -1));
41
+ if (receipt.kind !== 'fact' || !bundle.placements?.length)
42
+ return false;
43
+ const f = receipt.value.fields;
44
+ if (canonical(f) !== bundle.receipt.slice(0, -1) || !/^(0|[1-9][0-9]*)$/.test(f['epoch'] ?? ''))
45
+ return false;
46
+ if (f['value'] !== identity || f['role'] !== 'writes' || f['form'] !== 'alphabet' || f['measure'] !== 'digest')
47
+ return false;
48
+ return actIdentity(actLines(bundle.records), { scope: f['scope'] ?? '', context: f['at'] ?? '',
49
+ localEpoch: Number(f['epoch']), digest: bytes => fullDigest(bytes, identity.slice(0, identity.indexOf(':'))),
50
+ placements: bundle.placements.map(p => ({ place: p.place, records: actLines(p.records) })),
51
+ }) === identity;
52
+ }
53
+ /** Committed segments are part of this ledger, not a second authority source. */
54
+ export function committedLocalActs(store) {
55
+ const root = join(store, 'ledger', 'acts');
56
+ if (!existsSync(root))
57
+ return [];
58
+ return readdirSync(root).filter(name => name.startsWith('committed-')).map(name => {
59
+ const identity = decodeURIComponent(name.slice(10)), bundle = read(join(root, name));
60
+ if (!intactLocalAct(identity, bundle))
61
+ throw Error('REFUSE·act committed journal seal does not verify');
62
+ return { identity, bundle };
63
+ }).sort((a, b) => {
64
+ const epoch = (one) => { const p = parse(one.receipt.trim()); return p.kind === 'fact' ? Number(p.value.fields['epoch']) : 0; };
65
+ return epoch(a.bundle) - epoch(b.bundle);
66
+ });
67
+ }
68
+ /** A public result is available only as a verified committed pair.
69
+ * No writer-tail guess, newest-result selection or implicit authority.
70
+ */
71
+ export function localActResult(store, identity, verify) {
72
+ const at = atOf(store, identity);
73
+ if (!existsSync(at))
74
+ return undefined;
75
+ const bundle = read(at);
76
+ if (verify(identity, bundle) !== true)
77
+ throw Error('REFUSE·act committed result does not verify');
78
+ return bundle;
79
+ }
80
+ /** One publication point for records and their native result. Re-delivery
81
+ * reads the original pair without preparation, signing or a new local epoch.
82
+ * The caller must also integrate this boundary into admission-history readers;
83
+ * this storage port alone does not activate a grammar or land authority.
84
+ */
85
+ export async function commitLocalAct(store, identity, regions, prepare, verify, storage = nativePublicationStorage) {
86
+ requireEffects('local act commitment');
87
+ return underTheRegions(store, regions, 'land', async () => {
88
+ const prior = localActResult(store, identity, verify);
89
+ if (prior)
90
+ return { kind: 'once', ...prior };
91
+ const bundle = await prepare();
92
+ if (!bundle.records || !bundle.receipt || verify(identity, bundle) !== true)
93
+ throw Error('REFUSE·act prepared result does not verify');
94
+ const files = { 'records.bound': bundle.records, 'receipt.bound': bundle.receipt };
95
+ for (const p of bundle.placements ?? [])
96
+ files['place-' + encodeURIComponent(p.place) + '.bound'] = p.records;
97
+ publishBundle(atOf(store, identity), files, storage);
98
+ return { kind: 'landed', ...bundle };
99
+ });
100
+ }
@@ -0,0 +1,10 @@
1
+ export interface PublicationStorage {
2
+ publish(staged: string, committed: string): void;
3
+ }
4
+ export declare const nativePublicationStorage: PublicationStorage;
5
+ /** Publish prepared bytes as one directory. No identity, grammar or authority
6
+ * is inferred here. Readers address the committed name, never staging files.
7
+ * A failure after rename is not evidence of rollback: the caller must retain
8
+ * the uncertainty and allow a verified read of the committed name.
9
+ */
10
+ export declare function publishBundle(committed: string, files: Readonly<Record<string, string>>, storage?: PublicationStorage): void;
@@ -0,0 +1,56 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { closeSync, fsyncSync, mkdirSync, openSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { requireEffects } from "../read-only.js";
5
+ const syncDirectory = (path) => {
6
+ const fd = openSync(path, 'r');
7
+ try {
8
+ fsyncSync(fd);
9
+ }
10
+ finally {
11
+ closeSync(fd);
12
+ }
13
+ };
14
+ export const nativePublicationStorage = {
15
+ publish(staged, committed) { renameSync(staged, committed); syncDirectory(dirname(committed)); },
16
+ };
17
+ /** Publish prepared bytes as one directory. No identity, grammar or authority
18
+ * is inferred here. Readers address the committed name, never staging files.
19
+ * A failure after rename is not evidence of rollback: the caller must retain
20
+ * the uncertainty and allow a verified read of the committed name.
21
+ */
22
+ export function publishBundle(committed, files, storage = nativePublicationStorage) {
23
+ requireEffects('atomic publication');
24
+ const entries = Object.entries(files);
25
+ if (!entries.length || entries.some(([name]) => !name || name === '.' || name === '..' || /[\\/\0]/.test(name))) {
26
+ throw Error('REFUSE·commit invalid publication members');
27
+ }
28
+ const root = dirname(committed);
29
+ const created = mkdirSync(root, { recursive: true });
30
+ // Persist newly created directory entries up to their existing parent too.
31
+ if (created !== undefined)
32
+ for (let at = root;; at = dirname(at)) {
33
+ syncDirectory(at);
34
+ if (at === dirname(created))
35
+ break;
36
+ }
37
+ const staged = join(root, '.stage-' + randomUUID());
38
+ mkdirSync(staged, { mode: 0o700 });
39
+ try {
40
+ for (const [name, bytes] of entries) {
41
+ const fd = openSync(join(staged, name), 'wx', 0o600);
42
+ try {
43
+ writeFileSync(fd, bytes);
44
+ fsyncSync(fd);
45
+ }
46
+ finally {
47
+ closeSync(fd);
48
+ }
49
+ }
50
+ syncDirectory(staged);
51
+ storage.publish(staged, committed);
52
+ }
53
+ finally {
54
+ rmSync(staged, { recursive: true, force: true });
55
+ }
56
+ }
@@ -0,0 +1,31 @@
1
+ import type { ReaderLifetime, ReaderReference } from '@lapxo/topos/wire';
2
+ import type { Capsule } from './capsule.ts';
3
+ type File = {
4
+ readonly place: string;
5
+ readonly text: string;
6
+ };
7
+ export declare const inputLines: (files: readonly File[]) => readonly string[];
8
+ /** The selected world owns input projection and reading. The host only verifies closures and keeps native count/bytes receipts. */
9
+ export declare function readingClosures(options: {
10
+ root: string;
11
+ store: string;
12
+ reader: string;
13
+ speaker: string;
14
+ digest: (text: string) => string;
15
+ capsule: Capsule;
16
+ region: string;
17
+ projection: string;
18
+ readerReference?: ReaderReference;
19
+ lifetime?: ReaderLifetime;
20
+ allowsEmpty?: boolean;
21
+ files: readonly File[];
22
+ emit?: (line: string) => void;
23
+ }): {
24
+ lines: readonly string[];
25
+ read: number;
26
+ opened: number;
27
+ executed: number;
28
+ };
29
+ /** Verify the native reading carriers before a file seal may reuse them. These are semantic receipts, not file-region seals. */
30
+ export declare function verifiedReadingReceipts(store: string, lines: readonly string[], digest: (text: string) => string): number | undefined;
31
+ 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: [] }], options.readerReference)[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, ...(options.lifetime === undefined ? [] : [JSON.stringify(options.lifetime)])].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: [] }], options.readerReference)[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
+ }