@lapxo/bound 0.1.5 → 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.
@@ -0,0 +1,6 @@
1
+ import type { Signer } from '@lapxo/topos/wire';
2
+ /** Local admission history only. Foreign ingress is evidence, never coverage.
3
+ * The clock cutoff is meaningful for configuration; object clocks stay intact. */
4
+ export declare function admissionHistoryAt(root: string, epoch: number): readonly string[];
5
+ /** The existing bootstrap's public root of trust, not today's key selection. */
6
+ export declare function admissionAnchor(root: string): Signer;
@@ -0,0 +1,22 @@
1
+ import { join } from 'node:path';
2
+ import { readFileSync } from 'node:fs';
3
+ import { ledgerLines, ledgerWriters, storeOf } from "../land/ledger.js";
4
+ import { fieldOf, isConfig, selfName } from "./claims.js";
5
+ /** Local admission history only. Foreign ingress is evidence, never coverage.
6
+ * The clock cutoff is meaningful for configuration; object clocks stay intact. */
7
+ export function admissionHistoryAt(root, epoch) {
8
+ const store = storeOf(root);
9
+ return [...new Set(ledgerWriters(store).flatMap(writer => ledgerLines(store, writer)))]
10
+ .filter(line => fieldOf(line, 'sig') !== '' && (!isConfig(line) || Number(fieldOf(line, 'epoch')) <= epoch));
11
+ }
12
+ /** The existing bootstrap's public root of trust, not today's key selection. */
13
+ export function admissionAnchor(root) {
14
+ const at = join(storeOf(root), `${selfName()}.keys`);
15
+ const keys = JSON.parse(readFileSync(at, 'utf8'));
16
+ const anchors = Object.values(keys);
17
+ if (anchors.length !== 1 || !anchors[0]?.publicKey || !anchors[0].coverage.length)
18
+ throw Error('REFUSE·act historical admission requires one bootstrap anchor');
19
+ const anchor = anchors[0];
20
+ return { id: anchor.id, keyClass: 'authorize', publicKey: anchor.publicKey, coverage: anchor.coverage,
21
+ depth: { lo: 1, hi: 16 }, admittedBy: [] };
22
+ }
@@ -1,5 +1,6 @@
1
+ import { ledgerLines, ledgerWriters } from "../land/ledger.js";
1
2
  import { readOnly } from "../host/read-only.js";
2
- import { appendFileSync, dirname, join, mkdirSync, readdirSync, relative, resolve } from "../host/io.js";
3
+ import { appendFileSync, dirname, join, mkdirSync, readdirSync, realpathSync, relative, resolve } from "../host/io.js";
3
4
  import { bytesDigest as bytesWith, fullDigest as fullWith } from "../host/digest.js";
4
5
  import { coordinatesUnder, observeFile, observeText, stampOf } from "../observe/files.js";
5
6
  import { releaseOf, signedLockLines } from "./keys.js";
@@ -32,10 +33,8 @@ function namedAlgorithm(store) {
32
33
  const contract = published === undefined ? undefined : wireLine(published, 'digest-algorithms');
33
34
  return contract === undefined ? undefined : fieldOf(contract, 'value').split('|')[0]?.split(':')[0];
34
35
  }
35
- for (const entry of readdirSync(join(store, 'ledger'), { withFileTypes: true })) {
36
- if (!entry.isFile() || !entry.name.endsWith(EXTENSION))
37
- continue;
38
- const text = observeText(join(store, 'ledger', entry.name)) ?? '';
36
+ for (const writer of ledgerWriters(store)) {
37
+ const text = ledgerLines(store, writer).join('\n');
39
38
  let from = 0;
40
39
  while (from < text.length) {
41
40
  const at = (() => {
@@ -176,14 +175,19 @@ export function instrumentCoordinates(store, entry, standing = [], root = dirnam
176
175
  const digest = coordinateDigest(store, at);
177
176
  if (digest === undefined)
178
177
  return;
179
- seen.set(at, digest);
180
- for (const one of importsOf(store, at, digest)) {
178
+ // The host loader resolves links before loading a module. Count that module
179
+ // once, while retaining distinct modules even when their bytes are equal.
180
+ const module = realpathSync(at);
181
+ if (seen.has(module))
182
+ return;
183
+ seen.set(module, digest);
184
+ for (const one of importsOf(store, module, digest)) {
181
185
  if (one.startsWith('node:') || builtinModules.includes(one))
182
186
  continue;
183
187
  if (one.startsWith('.'))
184
- walk(resolve(dirname(at), one));
188
+ walk(resolve(dirname(module), one));
185
189
  else
186
- walk(createRequire(at).resolve(one));
190
+ walk(createRequire(module).resolve(one));
187
191
  }
188
192
  };
189
193
  walk(resolve(entry));
package/dist/fold/kept.js CHANGED
@@ -1,8 +1,9 @@
1
+ import { actLines, committedLocalActs } from "../host/ports/local-act.js";
1
2
  import { readOnly } from "../host/read-only.js";
2
3
  import { createHash, dirname, existsSync, join, mkdirSync, readdirSync, renameSync, writeFileSync } from "../host/io.js";
3
4
  import { bytesDigest, coordinateDigest } from "./digests.js";
4
5
  import { viewsOf } from "./views.js";
5
- import { landMemo, ledgerLines, memoAt, shardFile, writerFile } from "../land/ledger.js";
6
+ import { landMemo, ledgerLines, ledgerWriters, memoAt, shardFile, writerFile } from "../land/ledger.js";
6
7
  import { coordinatesUnder, observeFile, observeText } from "../observe/files.js";
7
8
  import { EXTENSION, fieldOf } from "./claims.js";
8
9
  import { keyFor, keyOf, lockSigners } from "./keys.js";
@@ -36,7 +37,7 @@ export function foldPoints(store, standing, under, keeper, instrument = '', obse
36
37
  `take ${digestOf(ledgerLines(store, 'take').filter((line) => reads.scopes.has(fieldOf(line, 'scope').replace(/^take\//, ''))))}`,
37
38
  `tree ${digestOf(ledgerLines(store, tree ?? '').filter((line) => (sourceOf(line) ?? '').startsWith(reads.place)))}`,
38
39
  ];
39
- const heads = (existsSync(join(store, 'ledger')) ? readdirSync(join(store, 'ledger')) : []).filter((name) => name.endsWith(EXTENSION)).map((name) => name.slice(0, -EXTENSION.length))
40
+ const heads = ledgerWriters(store)
40
41
  .filter((writer) => writer !== keeper && writer !== 'gate' && !writer.startsWith('reader') && !(reads && (signers.has(writer) || writer === 'judge' || writer === 'take' || writer === tree)))
41
42
  .map((writer) => `${writer} ${writer === liveWriter(store, standing) ? lastWord(store, writer) : bytesDigest(store, text(ledgerLines(store, writer).join('\n')))}`);
42
43
  const written = [...new Set([...viewsOf(standing).values()].flatMap((view) => ('regions' in view && view.shape ? [view.shape] : [])))].sort()
@@ -100,8 +101,12 @@ function readingContext(store) {
100
101
  const speaker = keyOf(store, 'read');
101
102
  const files = speaker === undefined ? [] : [writerFile(store, speaker), ...coordinatesUnder(dirname(shardFile(store, speaker, '')), store)
102
103
  .filter(file => file.endsWith(EXTENSION)).map(file => join(store, file))];
103
- return bytesDigest(store, text(files.filter(file => existsSync(file)).sort()
104
- .map(file => bytesDigest(store, observeFile(file) ?? new Uint8Array())).join('\n')));
104
+ const segments = speaker === undefined ? [] : committedLocalActs(store).flatMap(act => act.bundle.placements?.flatMap(placement => {
105
+ const records = actLines(placement.records).filter(line => fieldOf(line, 'by') === speaker);
106
+ return records.length ? [placement.place + '\n' + records.join('\n')] : [];
107
+ }) ?? []);
108
+ return bytesDigest(store, text([...files.filter(file => existsSync(file)).sort()
109
+ .map(file => bytesDigest(store, observeFile(file) ?? new Uint8Array())), ...segments].join('\n')));
105
110
  }
106
111
  export function keptPlace(store, under) {
107
112
  const key = observeText(memoAt(store, lastOf([`place ${under ?? '.'}`])))?.trim();
package/dist/fold/keys.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { dirname, existsSync, join } from "../host/io.js";
2
- import { ledgerLines, signedLines } from "../land/ledger.js";
3
- import { EXTENSION, fieldOf, foldClaims, isWire, LOCK, selfName } from "./claims.js";
4
- import { entriesIn, observeText } from "../observe/files.js";
2
+ import { ledgerLines, ledgerWriters, signedLines } from "../land/ledger.js";
3
+ import { fieldOf, foldClaims, isWire, LOCK, selfName } from "./claims.js";
4
+ import { observeText } from "../observe/files.js";
5
5
  const held = new Map();
6
+ const keyInputs = new Map();
6
7
  const merged = new Map();
7
8
  /**
8
9
  * Whose lines carry a class of writing, as the lock says it. Every ledger the store keeps is read, the key claims
@@ -47,13 +48,15 @@ export function signedLockLines(store, told = []) {
47
48
  return lockSigners(store, told).flatMap((id) => signedLines(store, id));
48
49
  }
49
50
  function keysOf(store, told) {
50
- const mine = told.length ? read(store, told) : held.get(store) ?? read(store);
51
+ const input = ledgerKeyLines(store).join('\n');
52
+ const mine = told.length ? read(store, told) : keyInputs.get(store) === input ? held.get(store) ?? read(store) : read(store);
53
+ if (!told.length)
54
+ keyInputs.set(store, input);
51
55
  if (!told.length)
52
56
  held.set(store, mine);
53
57
  return mine;
54
58
  }
55
- const ledgerKeyLines = (store) => (existsSync(join(store, 'ledger')) ? entriesIn(join(store, 'ledger')).map((entry) => entry.name) : [])
56
- .filter((name) => name.endsWith(EXTENSION)).flatMap((name) => ledgerLines(store, name.slice(0, -EXTENSION.length)))
59
+ const ledgerKeyLines = (store) => ledgerWriters(store).flatMap(writer => ledgerLines(store, writer))
57
60
  .filter((line) => /\bsig=/.test(line) && /(?:^|\s)scope=keys\/[^/\s]+\s/.test(line));
58
61
  const releases = new Map();
59
62
  /**
@@ -267,13 +267,14 @@ export function observedClaims(root, store, options = {}) {
267
267
  }
268
268
  function observeOne(reader, started) {
269
269
  const module = resolve(root, reader.module);
270
+ const lifetime = readerLifetime(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), { id: reader.id, module: reader.module });
270
271
  const asked = reader.capsule === undefined ? undefined : reader.capsule.selected ?? capsuleAt(reader.capsule.digest, reader.capsule.entry);
271
272
  const projection = reader.capsule === undefined ? [] : (asked?.lines ?? []).filter(line => fieldOf(line, 'scope') === `region/${reader.capsule.region}` && fieldOf(line, 'measure') === 'input-projection' && fieldOf(line, 'value') !== 'withdraw');
272
273
  if (projection.length) {
273
274
  if (projection.length !== 1 || !asked)
274
275
  throw Error('REFUSE·input projection is ambiguous ' + reader.module);
275
276
  const files = [...new Set(reader.where.flatMap(where => handedAt(root, reader, roleOf, look, where)))].sort().map(place => ({ place, text: observeText(resolve(root, place)) ?? '' }));
276
- const result = readingClosures({ root, store, reader: reader.module, speaker, digest: text => bytesDigest(store, Buffer.from(text)), capsule: asked, region: reader.capsule.region, projection: fieldOf(projection[0], 'value'), allowsEmpty: readerAllowsEmpty(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module), files, emit: line => process.stderr.write(line + '\n') });
277
+ const result = readingClosures({ root, store, reader: reader.module, speaker, digest: text => bytesDigest(store, Buffer.from(text)), capsule: asked, region: reader.capsule.region, projection: fieldOf(projection[0], 'value'), readerReference: { id: reader.id, module: reader.module }, lifetime, allowsEmpty: readerAllowsEmpty(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module), files, emit: line => process.stderr.write(line + '\n') });
277
278
  out.push(...result.lines);
278
279
  ran += result.executed;
279
280
  read += result.read - result.opened;
@@ -287,7 +288,7 @@ export function observedClaims(root, store, options = {}) {
287
288
  assertReaderInputs(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module, offered.length);
288
289
  const vectors = reader.capsule === undefined ? observeText(resolve(root, vectorsOf(reader.module, standing))) : undefined;
289
290
  const kept = whole && legacy === undefined && answered === undefined && source ? sha([
290
- speaker, reader.module, reader.kind, reader.shape.join('|'), reader.where.join('|'), bundle, vectors === undefined ? 'no vectors' : sha(vectors),
291
+ speaker, reader.module, JSON.stringify(lifetime), reader.kind, reader.shape.join('|'), reader.where.join('|'), bundle, vectors === undefined ? 'no vectors' : sha(vectors),
291
292
  reader.capsule === undefined ? '' : `${reader.capsule.selected?.selection ?? reader.capsule.digest} ${reader.capsule.region} ${reader.capsule.reads.join('|')} ${[...reader.capsule.globs].map(([place, globs]) => `${place}${globs.join('|')}`).join(' ')}`,
292
293
  ...offered.map((offer) => `${offer.place} ${offer.digest?.() ?? bytesDigest(store, offer.bytes())}`),
293
294
  ].join('\n')) : undefined;
@@ -302,9 +303,9 @@ export function observedClaims(root, store, options = {}) {
302
303
  const home = regionIn(regions, reader.module);
303
304
  const own = shardLines(store, speaker, home).filter((line) => fieldOf(line, 'scope') === `reader/${reader.module}`);
304
305
  const lineage = own.filter((line) => fieldOf(line, 'measure') === 'identity').map((line) => fieldOf(line, 'value'));
305
- const sum = source && reader.capsule === undefined ? bytesDigest(store, new TextEncoder().encode(`${reader.module} ${bundle}`)).replace(/^sha256:/, '').slice(0, 12) : 'none';
306
- const measured = reader.capsule !== undefined ? { by: source ? sha(`${reader.capsule.selected?.selection ?? reader.capsule.digest}\n${reader.capsule.region}`).slice(0, 12) : 'none', lines: [] }
307
- : source ? identityOf({ store, root, speaker, module: reader.module, kind: reader.kind, code: sum, lineage, lifetime: readerLifetime(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module), kept: own.filter((line) => /(?:^|\s)measure=(?:extension|emits)\s/.test(line)) })
306
+ const sum = source && reader.capsule === undefined ? bytesDigest(store, new TextEncoder().encode(`${reader.module} ${bundle}${lifetime === undefined ? "" : "\n" + JSON.stringify(lifetime)}`)).replace(/^sha256:/, '').slice(0, 12) : 'none';
307
+ const measured = reader.capsule !== undefined ? { by: source ? sha(`${reader.capsule.selected?.selection ?? reader.capsule.digest}\n${reader.capsule.region}${lifetime === undefined ? "" : "\n" + JSON.stringify(lifetime)}`).slice(0, 12) : 'none', lines: [] }
308
+ : source ? identityOf({ store, root, speaker, module: reader.module, kind: reader.kind, code: sum, lineage, lifetime, kept: own.filter((line) => /(?:^|\s)measure=(?:extension|emits)\s/.test(line)) })
308
309
  : { by: 'none', lines: [] };
309
310
  const by = measured.by;
310
311
  const level = [...measured.lines];
@@ -419,12 +420,12 @@ export function observedClaims(root, store, options = {}) {
419
420
  ? asked.ask(due.map((d) => ({
420
421
  protocol: PROTOCOL, verb: 'read', rootScope: d.place, region: reader.capsule.region, held: heldOf(d.place, d.region),
421
422
  files: (reader.capsule.reads.some((read) => read.includes('@')) ? handedAt(root, reader, roleOf, look, d.place) : [d.place]).map((coordinate) => ({ place: coordinate, text: observeText(resolve(root, coordinate)) ?? '' })),
422
- }))).map((answer) => {
423
+ })), { id: reader.id, module: reader.module }).map((answer) => {
423
424
  if (answer?.kind !== 'fact')
424
425
  throw new Error(`REFUSE·reader ${reader.module} · ${answer?.why ?? 'no capsule answer'}`);
425
426
  return (answer.claims ?? []);
426
427
  })
427
- : runReader(module, root, due.map((d) => ({ place: d.place, ...(reader.kind === 'js' ? { text: new TextDecoder().decode(d.bytes) } : {}), held: heldOf(d.place, d.region) })), readerLifetime(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module));
428
+ : runReader(module, root, due.map((d) => ({ place: d.place, ...(reader.kind === 'js' ? { text: new TextDecoder().decode(d.bytes) } : {}), held: heldOf(d.place, d.region) })), lifetime);
428
429
  }
429
430
  catch (error) {
430
431
  const why = `REFUSE·reader ${reader.module} · ${error instanceof Error ? error.message : String(error)}`;
@@ -1,3 +1,4 @@
1
+ import { authorityFor } from './signers.ts';
1
2
  /** Lines told to a place's own lock for a preview: folded as if the file held them, and standing only as its signed lines do. */
2
3
  export declare const tellOwn: (place: string, lines: readonly string[]) => void;
3
4
  /**
@@ -16,3 +17,7 @@ export declare const admittedIn: (root: string, place: string, line: string) =>
16
17
  export declare const appliedIn: (root: string, place: string) => readonly string[];
17
18
  export declare const forksIn: (root: string, place: string) => readonly string[];
18
19
  export declare const writtenIn: (root: string, place: string) => number;
20
+ /** Authenticated history excludes prospective lines told only for admission. */
21
+ export declare const appliedHistoryIn: (root: string, place: string) => readonly string[];
22
+ /** Shared signature/window/coordinate coverage boundary for own history. */
23
+ export declare function ownRecordAdmitted(line: string, place: string, family: string, signers: ReturnType<typeof authorityFor>['admitted'], algorithms: readonly string[]): boolean;
@@ -13,7 +13,7 @@ const held = new Map();
13
13
  const told = new Map();
14
14
  /** Lines told to a place's own lock for a preview: folded as if the file held them, and standing only as its signed lines do. */
15
15
  export const tellOwn = (place, lines) => void told.set(place, lines);
16
- function verified(root, place) {
16
+ function verified(root, place, preview = true) {
17
17
  const store = storeOf(root);
18
18
  const lock = lockLines(store);
19
19
  const publicText = observeText(join(root, place, LOCK)) ?? '';
@@ -21,7 +21,7 @@ function verified(root, place) {
21
21
  const algorithms = hasAuthority ? signaturesOf(store).admitted : [];
22
22
  const signers = algorithms.length ? authorityFor(lock, rootSigner(root), algorithms).admitted : [];
23
23
  const history = signers.flatMap(signer => place ? shardLines(store, signer.id, `${place}/${LOCK}`) : ledgerLines(store, signer.id)).filter(line => fieldOf(line, 'sig') !== '');
24
- const text = [publicText, ...history, ...(told.get(place) ?? [])].join('\n');
24
+ const text = [publicText, ...history, ...(preview ? told.get(place) ?? [] : [])].join('\n');
25
25
  const key = `${root} ${place}`;
26
26
  const last = held.get(key);
27
27
  if (last !== undefined && last.text === text && last.lock === lock)
@@ -30,19 +30,7 @@ function verified(root, place) {
30
30
  // Unrestricted coverage does not construct a region coordinate. Restricted
31
31
  // coverage still requires the lock's declared namespace before comparison.
32
32
  const family = signers.some(signer => !signer.coverage.includes('*')) ? wordOf(lockStanding(store), 'families', 'region') : '';
33
- const stands = (line) => {
34
- const got = parse(line);
35
- if (got.kind !== 'fact' || got.value.fields['sig'] === undefined)
36
- return false;
37
- const fields = got.value.fields;
38
- const signer = signers.find((one) => one.id === fields['by']);
39
- const epoch = Number(fields['epoch']) || 0;
40
- if (signer === undefined || !verifiesFields(fields, signer.publicKey, algorithms))
41
- return false;
42
- const window = windowAt(signer, epoch);
43
- return epoch >= window.start && (window.close === null || epoch <= window.close)
44
- && signer.coverage.some((prefix) => prefix === '*' || within(region(`${family}/${place}/${fields['scope'] ?? ''}`, './'), region(prefix, './')));
45
- };
33
+ const stands = (line) => ownRecordAdmitted(line, place, family, signers, algorithms);
46
34
  const applied = new Set(lines.filter(stands));
47
35
  // A published copy of an authenticated delivery is not another inscription.
48
36
  const projected = new Set([...applied].map(sayingOf));
@@ -75,3 +63,19 @@ export const admittedIn = (root, place, line) => verified(root, place).applied.h
75
63
  export const appliedIn = (root, place) => [...verified(root, place).applied];
76
64
  export const forksIn = (root, place) => ((got) => foldClaims(got.lines.filter((line) => got.applied.has(line) || (!/ sig=/.test(line) && !got.projected.has(sayingOf(line))))).forks.map((fork) => fork.key))(verified(root, place));
77
65
  export const writtenIn = (root, place) => ((got) => got.lines.filter((line) => !got.applied.has(line)).length)(verified(root, place));
66
+ /** Authenticated history excludes prospective lines told only for admission. */
67
+ export const appliedHistoryIn = (root, place) => [...verified(root, place, false).applied];
68
+ /** Shared signature/window/coordinate coverage boundary for own history. */
69
+ export function ownRecordAdmitted(line, place, family, signers, algorithms) {
70
+ const got = parse(line);
71
+ if (got.kind !== 'fact' || got.value.fields['sig'] === undefined)
72
+ return false;
73
+ const fields = got.value.fields;
74
+ const signer = signers.find((one) => one.id === fields['by']);
75
+ const epoch = Number(fields['epoch']) || 0;
76
+ if (signer === undefined || !verifiesFields(fields, signer.publicKey, algorithms))
77
+ return false;
78
+ const window = windowAt(signer, epoch);
79
+ return epoch >= window.start && (window.close === null || epoch <= window.close)
80
+ && signer.coverage.some((prefix) => prefix === '*' || within(region(`${family}/${place}/${fields['scope'] ?? ''}`, './'), region(prefix, './')));
81
+ }
@@ -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;
@@ -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,14 +86,16 @@ 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 hitOf = (request) => sha(`${digest}\n${loader}${JSON.stringify(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)}`);
97
99
  const hits = requests.map(hitOf);
98
100
  const kept = hits.map(hit => storeAt(cacheStore, 'cas', 'answers', hit));
99
101
  const held = kept.map((at, i) => {
@@ -114,7 +116,7 @@ function answered(digest, bytes, requests, names, tree, cacheStore = ownStore(),
114
116
  if (missing.length)
115
117
  process.stderr.write(`CAPSULE ${digest.slice(7, 19)} · ${requests.length} asked · ${missing.length} run · ${missing.map((one) => `${one.rootScope}${one.region}`).join(' ')}\n`);
116
118
  // Execute and validate the entire batch before committing any answer to the cache.
117
- 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);
118
120
  return requests.map((request, i) => {
119
121
  if (held[i] !== undefined)
120
122
  return held[i];
@@ -148,5 +150,5 @@ export function locatedAt(tree, place, cacheStore = ownStore()) {
148
150
  }
149
151
  }
150
152
  const digest = `sha256:${hash.digest('hex')}`;
151
- 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) };
152
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';
@@ -1,23 +1,10 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
3
2
  import { join } from 'node:path';
4
3
  import { requireEffects } from "../read-only.js";
5
4
  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
- } };
5
+ import { nativePublicationStorage, publishBundle } from "./publication.js";
19
6
  /** 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, '..')); } };
7
+ export const nativeIngressStorage = nativePublicationStorage;
21
8
  const directory = (store) => join(store, 'ingress');
22
9
  const nameOf = (identity) => {
23
10
  if (!identity || identity.includes('\0'))
@@ -58,18 +45,7 @@ export async function commitIngress(store, identity, prepare, verify, storage =
58
45
  const bundle = await prepare(ingressReceipts(store));
59
46
  if (!bundle.evidence || !bundle.receipt || verify(bundle) !== true)
60
47
  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
- }
48
+ publishBundle(at, { 'evidence.bound': bundle.evidence, 'receipt.bound': bundle.receipt }, storage);
73
49
  return measured('landed', bundle);
74
50
  });
75
51
  }
@@ -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
+ }
@@ -1,3 +1,4 @@
1
+ import type { ReaderLifetime, ReaderReference } from '@lapxo/topos/wire';
1
2
  import type { Capsule } from './capsule.ts';
2
3
  type File = {
3
4
  readonly place: string;
@@ -14,6 +15,8 @@ export declare function readingClosures(options: {
14
15
  capsule: Capsule;
15
16
  region: string;
16
17
  projection: string;
18
+ readerReference?: ReaderReference;
19
+ lifetime?: ReaderLifetime;
17
20
  allowsEmpty?: boolean;
18
21
  files: readonly File[];
19
22
  emit?: (line: string) => void;