@lapxo/bound 0.1.1 → 0.1.3

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.
package/dist/cli/verb.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { resolveSources } from "../host/source-content.js";
2
+ import { withoutEffects } from "../host/read-only.js";
3
+ import { resolveSources, resolveRequested } from "../host/source-content.js";
3
4
  import { cellsWithReceipts } from "../host/cell-receipts.js";
5
+ import { renderCells } from '@lapxo/topos/cells-view';
4
6
  import { releasePolicyLines } from "../fold/release-policy.js";
5
7
  import { admittedObjects } from "../host/objects.js";
6
8
  import { objectFold } from "../fold/object.js";
@@ -15,7 +17,7 @@ import { eraOf, signaturesOf } from "../fold/digests.js";
15
17
  import { fullDigest as bootstrapDigest } from "../host/digest.js";
16
18
  import { canonical } from '@lapxo/topos/wire';
17
19
  import { EXTENSION, LOCK, fieldOf, foldClaims, isWire, selfName } from "../fold/claims.js";
18
- import { observedClaims } from "../fold/observed.js";
20
+ import { observedClaims, readerRefusals } from "../fold/observed.js";
19
21
  import { rolesOf } from "../fold/roles.js";
20
22
  import { authorityFor, releaseSigner, rootSigner, writerFor } from "../fold/signers.js";
21
23
  import { land, ledgerLines, storeOf } from "../land/ledger.js";
@@ -189,10 +191,31 @@ async function view(root, under, asked, told = [], coord, key, includePolicy = t
189
191
  const rest = told.filter((line) => !ledger.includes(line));
190
192
  if (place && rest.length)
191
193
  tellOwn(place, rest);
194
+ if (name === NAMED_VIEWS.lines && (told.length || depth === undefined)) {
195
+ const text = standingOf(root, under, ledger).standing.filter(isWire).join('\n');
196
+ process.stdout.write(text ? `${text}\n` : '');
197
+ return EXIT.closed;
198
+ }
199
+ if (told.length) {
200
+ const selected = viewsOf(standingOf(root, under, ledger).standing).get(name);
201
+ if (selected && !selected.shape && selected.regions.some(region => region.name === 'cells')) {
202
+ const history = [...signedOwnerLines(storeOf(root)), ...ledger];
203
+ const held = await admittedObjects(root, history);
204
+ process.stdout.write(`TOLD ${told.length} lines folded as if landed · nothing written\n`);
205
+ for (const line of renderCells(objectFold(held.records, held.context)))
206
+ process.stdout.write(line + '\n');
207
+ return EXIT.closed;
208
+ }
209
+ }
192
210
  const own = place ? rest.filter((line) => admittedIn(root, place, line)) : [];
193
211
  const fold = told.length
194
- ? foldPlace(root, entryOf(root), under, false, { standing: standingOf(root, under, ledger), observed: observedClaims(root, storeOf(root), { wait: false }) })
195
- : foldPlace(root, entryOf(root), under, false);
212
+ ? foldPlace(root, entryOf(root), under, false, { standing: standingOf(root, under, ledger), observed: observedClaims(root, storeOf(root), { wait: false }), free: true })
213
+ : foldPlace(root, entryOf(root), under, true);
214
+ if (!told.length && readerRefusals().length) {
215
+ for (const why of readerRefusals())
216
+ process.stderr.write(`${selfName()}: ${why}\n`);
217
+ return EXIT.refuse;
218
+ }
196
219
  if (coord)
197
220
  askWhy(fold, coord);
198
221
  if (name === NAMED_VIEWS.lines) {
@@ -200,6 +223,9 @@ async function view(root, under, asked, told = [], coord, key, includePolicy = t
200
223
  beat(root, fold, Date.now() - at);
201
224
  const text = fold.standing.filter(isWire).join('\n');
202
225
  process.stdout.write(text ? `${text}\n` : '');
226
+ if (depth === '1')
227
+ for (const line of readingsIn([...fold.standing.filter(isCeiling), ...ownCeilings(under)], fold))
228
+ process.stdout.write(`${line}\n`);
203
229
  return EXIT.closed;
204
230
  }
205
231
  const views = viewsOf(fold.standing);
@@ -371,7 +397,7 @@ function foldFile(at) {
371
397
  process.stdout.write(`FACT ${folded.standing.length} standing · ${folded.forks.length} forks · ${basename(at)}\n`);
372
398
  return EXIT.closed;
373
399
  }
374
- function foldVerb(_root, rest) {
400
+ function foldVerb(root, rest) {
375
401
  const files = positionalsOf(rest).filter((one) => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory() && one.endsWith(EXTENSION));
376
402
  const places = positionalsOf(rest).filter((one) => existsSync(resolve(one)) && lstatSync(resolve(one)).isDirectory());
377
403
  if (files.length === 1 && !places.length && valueOf(rest, FLAGS.as) === undefined) {
@@ -381,14 +407,24 @@ function foldVerb(_root, rest) {
381
407
  return foldAt(dirname(at), undefined, rest);
382
408
  }
383
409
  const under = places[0];
384
- const tree = files.length === 1 ? dirname(resolve(files[0])) : resolve('.');
410
+ // A prospective lot is data, not the place whose authority admits it.
411
+ const tree = files.length ? root : resolve('.');
385
412
  const place = under ? relative(tree, resolve(under)).split(sep).join('/') : '';
386
413
  return foldAt(tree, place ? `${place}/` : undefined, rest);
387
414
  }
388
415
  async function foldAt(root, under, argv) {
389
416
  const as = valueOf(argv, FLAGS.as);
390
- if (as !== NAMED_VIEWS.help && signedOwnerLines(storeOf(root)).length)
391
- await resolveSources(standingOf(root, under).standing);
417
+ const prospective = as !== undefined && positionalsOf(argv).some(one => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
418
+ if (as === NAMED_VIEWS.help || prospective || argv.includes(FLAGS.check))
419
+ return foldResolved(root, under, argv);
420
+ return resolveRequested(digest => resolveSources(standingOf(root, under).standing, [digest]), () => foldResolved(root, under, argv));
421
+ }
422
+ async function foldResolved(root, under, argv) {
423
+ const as = valueOf(argv, FLAGS.as);
424
+ const inputs = positionalsOf(argv).filter(one => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
425
+ // Public content is authenticated by its digest, independently of owner history.
426
+ // A named view does not request mounting every release of the instrument.
427
+ const receiptCheck = as === undefined && argv.includes(FLAGS.check);
392
428
  if (as === OBJECT_VIEW) {
393
429
  const inputs = positionalsOf(argv).filter(one => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
394
430
  if (inputs.length > 1)
@@ -404,6 +440,8 @@ async function foldAt(root, under, argv) {
404
440
  const key = valueOf(argv, FLAGS.key);
405
441
  if (as === undefined) {
406
442
  const exit = await pass(root, entryOf(root), under, argv.includes(FLAGS.check));
443
+ if (receiptCheck)
444
+ return exit;
407
445
  if (exit !== 0)
408
446
  return exit;
409
447
  const history = signedOwnerLines(storeOf(root));
@@ -419,7 +457,7 @@ async function foldAt(root, under, argv) {
419
457
  }
420
458
  return exit;
421
459
  }
422
- const named = positionalsOf(argv).filter((one) => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
460
+ const named = inputs;
423
461
  const coord = positionalsOf(argv).find((one) => one !== under?.replace(/\/$/, '') && !one.startsWith('-') && !(existsSync(resolve(one)) && lstatSync(resolve(one)).isDirectory()));
424
462
  if ((as === 'why' || as === NAMED_VIEWS.because || as.split('@')[0] === 'why') && coord !== undefined)
425
463
  return view(root, under, as === NAMED_VIEWS.because ? 'why' : as, [], coord, key);
@@ -430,7 +468,7 @@ async function foldAt(root, under, argv) {
430
468
  return batch.code;
431
469
  if (batch.kind === 'once')
432
470
  process.stdout.write(`ONCE ${batch.proposed.length} lines · every one already landed: the view folds the tree as it stands\n`);
433
- return view(root, under, as, batch.kind === 'signed' ? batch.signed : [], undefined, key);
471
+ return withoutEffects(() => view(root, under, as, batch.kind === 'signed' ? batch.signed : batch.proposed, undefined, key));
434
472
  }
435
473
  const VERBS = {
436
474
  fold: (root, rest) => foldVerb(root, rest),
@@ -3,6 +3,9 @@ import { fieldOf, isConfig } from "./claims.js";
3
3
  import { isReaderLock } from "./observed.js";
4
4
  import { isRoleLock } from "./roles.js";
5
5
  import { isSignerLock } from "./signers.js";
6
+ import { isReleaseBinding } from "../host/release.js";
7
+ import { isRegionDeclaration } from "./region-declarations.js";
8
+ import { ownLock } from "../observe/runner.js";
6
9
  function isEffectLock(line) {
7
10
  const got = parse(line);
8
11
  return got.kind === 'fact' && got.value.fields['measure'] === 'effect';
@@ -10,7 +13,7 @@ function isEffectLock(line) {
10
13
  /** A lock line the instrument itself consumes — an effect it runs, a reader it loads, a role it walks by, a signer it admits, the wire it reads by. */
11
14
  function configures(line) {
12
15
  const got = parse(line);
13
- return isEffectLock(line) || isReaderLock(line) || isRoleLock(line) || isSignerLock(line) || (got.kind === 'fact' && isWireClaim(got.value.fields));
16
+ return isReleaseBinding(line) || isEffectLock(line) || isReaderLock(line) || isRoleLock(line) || isSignerLock(line) || (got.kind === 'fact' && isWireClaim(got.value.fields)) || isRegionDeclaration(line, ownLock());
14
17
  }
15
18
  /** Every reads line that configures nothing is a ceiling: a bound some reading has to meet. */
16
19
  export function isCeiling(line) {
@@ -1,3 +1,4 @@
1
+ import { readOnly } from "../host/read-only.js";
1
2
  import { appendFileSync, dirname, join, mkdirSync, readdirSync, relative, resolve } from "../host/io.js";
2
3
  import { bytesDigest as bytesWith, fullDigest as fullWith } from "../host/digest.js";
3
4
  import { coordinatesUnder, observeFile, observeText, stampOf } from "../observe/files.js";
@@ -116,6 +117,8 @@ export function coordinateDigest(store, at) {
116
117
  return held;
117
118
  const digest = bytesDigest(store, observeFile(at) ?? new Uint8Array());
118
119
  memo.set(key, digest);
120
+ if (readOnly())
121
+ return digest;
119
122
  mkdirSync(join(store, 'cas'), { recursive: true });
120
123
  appendFileSync(join(store, 'cas', `stamps${EXTENSION}`), `${key}\t${digest}\n`);
121
124
  return digest;
@@ -127,6 +130,8 @@ export function memoized(store, key, compute) {
127
130
  return held;
128
131
  const value = compute();
129
132
  memo.set(key, value);
133
+ if (readOnly())
134
+ return value;
130
135
  mkdirSync(join(store, 'cas'), { recursive: true });
131
136
  appendFileSync(join(store, 'cas', `stamps${EXTENSION}`), `${key}\t${value}\n`);
132
137
  return value;
@@ -141,7 +146,8 @@ const importsOf = (store, at, digest) => {
141
146
  const found = [...new TextDecoder().decode(observeFile(at) ?? new Uint8Array()).matchAll(/(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s*)['"]([^'"]*)['"]/g)].map((m) => m[1] ?? '');
142
147
  imported.set(digest, found);
143
148
  memo.set(key, found.join('|'));
144
- appendFileSync(join(store, 'cas', `stamps${EXTENSION}`), `${key}\t${found.join('|')}\n`);
149
+ if (!readOnly())
150
+ appendFileSync(join(store, 'cas', `stamps${EXTENSION}`), `${key}\t${found.join('|')}\n`);
145
151
  return found;
146
152
  };
147
153
  /**
package/dist/fold/kept.js CHANGED
@@ -1,10 +1,11 @@
1
+ import { readOnly } from "../host/read-only.js";
1
2
  import { createHash, dirname, existsSync, join, mkdirSync, readdirSync, renameSync, writeFileSync } from "../host/io.js";
2
3
  import { bytesDigest, coordinateDigest } from "./digests.js";
3
4
  import { viewsOf } from "./views.js";
4
- import { landMemo, ledgerLines, memoAt } from "../land/ledger.js";
5
- import { observeText } from "../observe/files.js";
5
+ import { landMemo, ledgerLines, memoAt, shardFile, writerFile } from "../land/ledger.js";
6
+ import { coordinatesUnder, observeFile, observeText } from "../observe/files.js";
6
7
  import { EXTENSION, fieldOf } from "./claims.js";
7
- import { keyFor, lockSigners } from "./keys.js";
8
+ import { keyFor, keyOf, lockSigners } from "./keys.js";
8
9
  import { sourceOf, vouching } from "../land/vouched.js";
9
10
  import { wordsOf } from "./wire.js";
10
11
  import { ownLock } from "../observe/runner.js";
@@ -45,6 +46,7 @@ export function foldPoints(store, standing, under, keeper, instrument = '', obse
45
46
  return [
46
47
  `place ${under ?? '.'}`,
47
48
  `instrument ${instrument}`,
49
+ `reading context ${readingContext(store)}`,
48
50
  ...written,
49
51
  ...lined,
50
52
  ...[...new Set(coordinates)].sort(),
@@ -93,6 +95,14 @@ export const foldsElsewhere = () => elsewhere.folds;
93
95
  const joined = (lines) => lines.join('\n');
94
96
  const split = (text) => (text === '' ? [] : text.split('\n'));
95
97
  const linesOf = (value) => value === undefined ? [] : typeof value === 'string' ? split(value) : value;
98
+ /** Operational reader history guards file-fold reuse, never cell-region identity. */
99
+ function readingContext(store) {
100
+ const speaker = keyOf(store, 'read');
101
+ const files = speaker === undefined ? [] : [writerFile(store, speaker), ...coordinatesUnder(dirname(shardFile(store, speaker, '')), store)
102
+ .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')));
105
+ }
96
106
  export function keptPlace(store, under) {
97
107
  const key = observeText(memoAt(store, lastOf([`place ${under ?? '.'}`])))?.trim();
98
108
  if (key === undefined || key === '')
@@ -101,6 +111,8 @@ export function keptPlace(store, under) {
101
111
  if (held === undefined)
102
112
  return undefined;
103
113
  const blob = JSON.parse(held);
114
+ if (blob.readingContext !== readingContext(store))
115
+ return undefined;
104
116
  const fold = blob.fold;
105
117
  return {
106
118
  ...fold,
@@ -118,8 +130,11 @@ export function keptPlace(store, under) {
118
130
  };
119
131
  }
120
132
  export const keptRender = (store, key, name) => observeText(memoAt(store, `render/${key}/${name}`));
121
- export const keepRender = (store, _signer, key, name, _epoch, text_) => { landMemo(store, `render/${key}/${name}`, text(text_)); };
133
+ export const keepRender = (store, _signer, key, name, _epoch, text_) => { if (!readOnly())
134
+ landMemo(store, `render/${key}/${name}`, text(text_)); };
122
135
  export function keepFold(store, key, fold, points, instrument = '') {
136
+ if (readOnly())
137
+ return;
123
138
  const packed = {
124
139
  ...fold,
125
140
  standing: joined(fold.standing),
@@ -134,7 +149,7 @@ export function keepFold(store, key, fold, points, instrument = '') {
134
149
  signed: [...fold.signed],
135
150
  kept: false,
136
151
  };
137
- landMemo(store, `fold/${key}`, text(JSON.stringify({ fold: packed, points, instrument })));
152
+ landMemo(store, `fold/${key}`, text(JSON.stringify({ fold: packed, points, instrument, readingContext: readingContext(store) })));
138
153
  const at = memoAt(store, lastOf(points));
139
154
  mkdirSync(dirname(at), { recursive: true });
140
155
  writeFileSync(`${at}.${process.pid}.part`, `${key}\n`);
@@ -15,6 +15,7 @@ export declare const readersBehind: () => readonly {
15
15
  readonly module: string;
16
16
  readonly places: number;
17
17
  }[];
18
+ export declare const readerRefusals: () => readonly string[];
18
19
  export declare function readerLines(store: string): readonly string[];
19
20
  /**
20
21
  * A reader never blocks a fold that did not ask to wait: what it read before is served at once, and a reader whose
@@ -1,3 +1,4 @@
1
+ import { readOnly } from "../host/read-only.js";
1
2
  import { basename, dirname, existsSync, mkdirSync, resolve, rmSync, spawn, statSync, writeFileSync } from "../host/io.js";
2
3
  import { bundleOf, processOf } from "../observe/runner.js";
3
4
  import { canonical, parse, PROTOCOL } from '@lapxo/topos/wire';
@@ -15,7 +16,7 @@ import { capsuleReadersOf, handedAt, lookOf, offersOf } from "./readers.js";
15
16
  import { runReader, runs } from "../observe/run.js";
16
17
  import { LEVEL, OBSERVED, keepObservation, keyOf, legacyOf, observation, observed as observedAt, opened, pointer, readerOf, regionIn, setAside, sha, trimmed } from "./shards.js";
17
18
  import { saidDigest } from "./kept.js";
18
- import { carriedIn } from "./resolved.js";
19
+ import { carriedIn, carriedFrom } from "./resolved.js";
19
20
  export const cased = (question) => /^(?:vector|sample)\//.test(question);
20
21
  export function isReaderLock(line) {
21
22
  const got = parse(line);
@@ -87,6 +88,8 @@ function takeReadings(observed) {
87
88
  }
88
89
  const behind = [];
89
90
  export const readersBehind = () => behind;
91
+ const refused = [];
92
+ export const readerRefusals = () => refused;
90
93
  const alive = (pid) => { try {
91
94
  return process.kill(pid, 0);
92
95
  }
@@ -170,6 +173,14 @@ export function readerReaches(reach, where) {
170
173
  return where.some((one) => reached(trimmed(one)));
171
174
  }
172
175
  export function observedClaims(root, store, options = {}) {
176
+ refused.length = 0;
177
+ if (readOnly()) {
178
+ const carried = (observeText(resolve(root, 'receipts.bound')) ?? '').split('\n').filter(isWire);
179
+ const evidence = carriedFrom(store, carried);
180
+ if (evidence === undefined)
181
+ throw Error('REFUSE·preview native receipt bytes unavailable; fold the declared inputs first');
182
+ return evidence;
183
+ }
173
184
  const wait = options.wait ?? true;
174
185
  const standing = lockStanding(store);
175
186
  const lines = readersOf(store);
@@ -255,7 +266,7 @@ export function observedClaims(root, store, options = {}) {
255
266
  const module = resolve(root, reader.module);
256
267
  const asked = reader.capsule === undefined ? undefined : capsuleAt(reader.capsule.digest, reader.capsule.entry);
257
268
  const source = reader.capsule === undefined ? observeFile(module) : asked === undefined ? undefined : new Uint8Array();
258
- const bundle = source && reader.capsule === undefined ? bundleOf(module) ?? 'unbundled' : 'none';
269
+ const bundle = source && reader.capsule === undefined ? bundleOf(module) ?? bytesDigest(store, source) : 'none';
259
270
  const offered = offersOf(root, store, reader, roleOf, look, (where) => reached(trimmed(where)));
260
271
  const vectors = reader.capsule === undefined ? observeText(resolve(root, vectorsOf(reader.module, standing))) : undefined;
261
272
  const kept = whole && legacy === undefined && answered === undefined && source ? sha([
@@ -351,8 +362,10 @@ export function observedClaims(root, store, options = {}) {
351
362
  }
352
363
  return [];
353
364
  };
354
- if (due.length && !source)
365
+ if (due.length && !source) {
366
+ refused.push(`REFUSE·reader ${reader.module} unavailable · ${due.length} readings remain due`);
355
367
  return false;
368
+ }
356
369
  if (due.length && !wait) {
357
370
  for (const plan of plans) {
358
371
  if (plan.said !== undefined) {
@@ -397,11 +410,15 @@ export function observedClaims(root, store, options = {}) {
397
410
  : runReader(module, root, due.map((d) => ({ place: d.place, ...(reader.kind === 'js' ? { text: new TextDecoder().decode(d.bytes) } : {}), held: heldOf(d.place, d.region) })));
398
411
  }
399
412
  catch (error) {
400
- process.stderr.write(`REFUSE reader ${reader.module} · ${error instanceof Error ? error.message : String(error)}\n`);
413
+ const why = `REFUSE·reader ${reader.module} · ${error instanceof Error ? error.message : String(error)}`;
414
+ refused.push(why);
415
+ process.stderr.write(`${why}\n`);
401
416
  return false;
402
417
  }
403
418
  if (rows.every((mine) => !mine.length)) {
404
- process.stderr.write(`REFUSE reader ${reader.module} · answered none of ${due.length} places · nothing landed, they stay due\n`);
419
+ const why = `REFUSE·reader ${reader.module} · answered none of ${due.length} places · nothing landed, they stay due`;
420
+ refused.push(why);
421
+ process.stderr.write(`${why}\n`);
405
422
  return false;
406
423
  }
407
424
  ran += due.length;
@@ -7,6 +7,11 @@ export declare const selectionLines: (fold: {
7
7
  readonly standing: readonly string[];
8
8
  readonly under?: string;
9
9
  }) => readonly string[];
10
+ export declare const projectionLines: (fold: {
11
+ readonly root: string;
12
+ readonly standing: readonly string[];
13
+ readonly under?: string;
14
+ }) => readonly string[];
10
15
  export declare function capsulesFor(standing: readonly string[], place: string, root?: string): readonly {
11
16
  readonly capsule: Capsule;
12
17
  readonly offer: string;
@@ -1,6 +1,6 @@
1
1
  import { selectedCapsules } from "../host/selected-capsules.js";
2
2
  import { basename } from "../host/io.js";
3
- import { matches } from '@lapxo/topos/wire';
3
+ import { canonical, matches, parse } from '@lapxo/topos/wire';
4
4
  import { fieldOf } from "./claims.js";
5
5
  import { spoken, worldsIn } from "./places.js";
6
6
  import { wordOf, wordsOf } from "./wire.js";
@@ -57,9 +57,27 @@ export const selectionLines = (fold) => {
57
57
  const qualified = fold.standing.filter((line) => fieldOf(line, 'scope').startsWith(`${fold.under ?? ''}uses/`));
58
58
  return [...new Set([...local, ...qualified])].filter((line) => fieldOf(line, 'value') !== 'withdraw');
59
59
  };
60
+ /** Host bindings belong to the selected place, never to another sibling's lock. */
61
+ const relativeProjections = (standing, prefix) => standing.filter(line => fieldOf(line, 'scope').startsWith(`${prefix}dep/`)).map(line => {
62
+ const got = parse(line, { preserveKeys: true });
63
+ if (got.kind !== 'fact')
64
+ throw Error(`REFUSE·wire ${got.why}`);
65
+ // An unsigned host projection of admitted fields, never a newly signed claim.
66
+ const { sig, by, epoch, expires, repo, ...fields } = got.value.fields;
67
+ return canonical({ ...fields, scope: fields.scope.slice(prefix.length) }, got.value.version);
68
+ });
69
+ export const projectionLines = (fold) => {
70
+ const place = fold.under?.replace(/\/$/, '') ?? '';
71
+ const prefix = place ? `${place}/` : '';
72
+ const local = ownLockOf(fold.root, place).filter(line => fieldOf(line, 'scope').startsWith('dep/'));
73
+ const qualified = relativeProjections(fold.standing, prefix);
74
+ return [...new Set([...ownLock(), ...local, ...qualified])];
75
+ };
60
76
  export function capsulesFor(standing, place, root) {
61
77
  const prefix = place ? `${place}/` : '';
62
78
  const pins = root === undefined ? standing.filter(line => fieldOf(line, 'scope').startsWith(`${prefix}uses/`) && fieldOf(line, 'value') !== 'withdraw')
63
79
  : selectionLines({ root, standing, ...(prefix ? { under: prefix } : {}) });
64
- return selectedCapsules(pins);
80
+ const host = root === undefined ? [...ownLock(), ...relativeProjections(standing, prefix)]
81
+ : projectionLines({ root, standing, ...(prefix ? { under: prefix } : {}) });
82
+ return selectedCapsules(pins, undefined, undefined, host);
65
83
  }
@@ -0,0 +1,2 @@
1
+ /** Region input selectors consumed by the host, rather than measured ceilings. */
2
+ export declare function isRegionDeclaration(line: string, wire: readonly string[]): boolean;
@@ -0,0 +1,13 @@
1
+ import { fieldOf, isConfig } from "./claims.js";
2
+ import { wordsOf } from "./wire.js";
3
+ /** Region input selectors consumed by the host, rather than measured ceilings. */
4
+ export function isRegionDeclaration(line, wire) {
5
+ if (!isConfig(line) || !fieldOf(line, 'scope').startsWith('region/') || fieldOf(line, 'value') === 'withdraw')
6
+ return false;
7
+ const named = ['region-coordinate', 'region-leaf'].flatMap(name => {
8
+ const words = wordsOf(wire, name);
9
+ return words.length === 1 ? words : [];
10
+ });
11
+ const measures = ['lines', 'receipts', 'reads', ...named];
12
+ return measures.includes(fieldOf(line, 'measure'));
13
+ }
@@ -1,3 +1,4 @@
1
+ import { readOnly } from "../host/read-only.js";
1
2
  import { createHash, dirname, existsSync, join, linkSync, mkdirSync, readdirSync, renameSync, writeFileSync } from "../host/io.js";
2
3
  import { canonical, parse } from '@lapxo/topos/wire';
3
4
  import { RECEIPTS, fieldOf, isWire } from "./claims.js";
@@ -11,6 +12,10 @@ import { sourced } from "../land/vouched.js";
11
12
  import { entriesIn, observeFile, observeText } from "../observe/files.js";
12
13
  export const bytesOnly = (line) => {
13
14
  const got = parse(line);
15
+ // Authenticated evidence is delivered verbatim. A shortened observation is
16
+ // a different signed message, even when it describes the same input.
17
+ if (got.kind === 'fact' && got.value.fields.sig)
18
+ return line;
14
19
  return got.kind === 'fact' && fieldOf(line, 'measure') === 'observed' ? canonical({ ...got.value.fields, value: fieldOf(line, 'at').replace(/^.*:/, '').slice(0, 16) }) : line;
15
20
  };
16
21
  const held = new WeakMap();
@@ -45,7 +50,8 @@ export function receiptsOf(fold) {
45
50
  return canonical({ scope: `${sourced(fold.standing)}/${place}${shape}`, role: 'writes', form: 'alphabet', measure: 'digest', value: digest, by: writerFor(fold.standing, 'fold') ?? '', at: `place:${digest}` });
46
51
  });
47
52
  const place = under.replace(/\/$/, '');
48
- const lines = [...named, ...[...cones].sort(), ...[...ran].sort(), ...paid].map((line) => (place ? zoomed(line, place) : line));
53
+ const lines = [...named, ...[...cones].sort(), ...[...ran].sort(), ...paid]
54
+ .map((line) => (place && !fieldOf(line, 'sig') ? zoomed(line, place) : line));
49
55
  if (!lines.length) {
50
56
  const shipped = (observeText(join(fold.root, under, RECEIPTS)) ?? '').split('\n').filter(isWire);
51
57
  const carried = carriedFrom(fold.store, shipped);
@@ -64,6 +70,8 @@ export function keepCarried(store, lines, from) {
64
70
  verifiedCarried(at, digest);
65
71
  }
66
72
  else {
73
+ if (readOnly())
74
+ return `sha256:${digest}`;
67
75
  mkdirSync(dirname(at), { recursive: true });
68
76
  const src = from === undefined ? '' : storeAt(from, 'cas', 'carried', digest);
69
77
  if (src && existsSync(src)) {
@@ -1,3 +1,4 @@
1
+ import { readOnly } from "../host/read-only.js";
1
2
  import { snapshotVerdict } from "./evidence.js";
2
3
  import { dirname, resolve, spawnSync } from "../host/io.js";
3
4
  import { ledgerLines } from "../land/ledger.js";
@@ -9,6 +10,8 @@ import { said } from "./wire.js";
9
10
  import { sha } from "../host/hash.js";
10
11
  const asked = new Map();
11
12
  function askedNow(store, rests) {
13
+ if (readOnly())
14
+ return undefined;
12
15
  const root = dirname(store);
13
16
  const key = `${root} ${rests}`;
14
17
  if (asked.has(key))
@@ -1,3 +1,5 @@
1
+ import { ContentNeeded } from "./content-needed.js";
2
+ import { readOnly } from "./read-only.js";
1
3
  import { sha } from "./hash.js";
2
4
  import { wireLinesOf as linesOf } from "../fold/claims.js";
3
5
  import { createHash } from 'node:crypto';
@@ -47,8 +49,10 @@ export function capsuleAt(digest, entry, store) {
47
49
  if (held !== undefined)
48
50
  return held;
49
51
  const bytes = observeFile(blobAt(store ?? ownStore(), digest));
50
- if (bytes === undefined || `sha256:${createHash('sha256').update(bytes).digest('hex')}` !== digest)
51
- return undefined;
52
+ if (bytes === undefined)
53
+ throw new ContentNeeded(digest, `REFUSE·pin ${digest} offered capsule unavailable · not laid`);
54
+ if (`sha256:${createHash('sha256').update(bytes).digest('hex')}` !== digest)
55
+ throw Error(`REFUSE·pin ${digest} content hash mismatch · not laid`);
52
56
  const known = declared.get(digest) ?? ((lines) => ({ lines, declaration: declarationOf(lines) }))(linesOf(observeText(join(unpacked(digest, bytes), CAPSULE))));
53
57
  declared.set(digest, known);
54
58
  const names = Object.keys(known.declaration.regions);
@@ -108,6 +112,8 @@ function answered(digest, bytes, requests, names, tree, cacheStore = ownStore(),
108
112
  }
109
113
  });
110
114
  const missing = requests.filter((_, i) => held[i] === undefined);
115
+ if (missing.length && readOnly())
116
+ throw Error(`REFUSE·preview capsule ${digest} has no verified answer for the proposed inputs; fold the declared inputs first`);
111
117
  if (missing.length)
112
118
  process.stderr.write(`CAPSULE ${digest.slice(7, 19)} · ${requests.length} asked · ${missing.length} run · ${missing.map((one) => `${one.rootScope}${one.region}`).join(' ')}\n`);
113
119
  // Execute and validate the entire batch before committing any answer to the cache.
@@ -0,0 +1,5 @@
1
+ /** The synchronous host names unavailable content; the enclosing fold resolves exactly that request. */
2
+ export declare class ContentNeeded extends Error {
3
+ readonly digest: string;
4
+ constructor(digest: string, why?: string);
5
+ }
@@ -0,0 +1,8 @@
1
+ /** The synchronous host names unavailable content; the enclosing fold resolves exactly that request. */
2
+ export class ContentNeeded extends Error {
3
+ constructor(digest, why = `REFUSE·pin ${digest} content unavailable · not laid`) {
4
+ super(why);
5
+ this.digest = digest;
6
+ this.name = 'ContentNeeded';
7
+ }
8
+ }
@@ -0,0 +1,3 @@
1
+ export declare const readOnly: () => boolean;
2
+ export declare const withoutEffects: <T>(evaluate: () => T) => T;
3
+ export declare function requireEffects(operation: string): void;
@@ -0,0 +1,9 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ /** Prospective evaluation may read existing evidence and calculate values, but cannot acquire evidence or persist caches. */
3
+ const mode = new AsyncLocalStorage();
4
+ export const readOnly = () => mode.getStore() === true;
5
+ export const withoutEffects = (evaluate) => mode.run(true, evaluate);
6
+ export function requireEffects(operation) {
7
+ if (readOnly())
8
+ throw Error(`REFUSE·preview ${operation} requires an act; fold the declared inputs first`);
9
+ }
@@ -1,4 +1,6 @@
1
1
  export declare const laidAt: (store: string, digest: string) => string;
2
+ /** A host artifact binding, not a measured ceiling or an object act. */
3
+ export declare const isReleaseBinding: (line: string) => boolean;
2
4
  export declare const releasesOf: (lock: readonly string[]) => readonly string[];
3
5
  /**
4
6
  * What the host does before anything runs: each release the instrument's own lock names by digest is a snapshot of
@@ -1,13 +1,19 @@
1
+ import { ContentNeeded } from "./content-needed.js";
1
2
  import { createHash } from 'node:crypto';
2
3
  import { existsSync, lstatSync, mkdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
4
  import { dirname, join, sep } from 'node:path';
4
5
  import { canonical } from '@lapxo/topos/wire';
5
- import { fieldOf, isWire } from "../fold/claims.js";
6
+ import { fieldOf, isWire, isConfig } from "../fold/claims.js";
6
7
  import { archiveCoordinate, archiveDestination, members } from "./archive.js";
7
8
  import { blobAt, holdStore, landBlob, replaceWhole, storeAt } from "../land/ledger.js";
8
9
  import { coordinatesUnder, observeFile, observeText } from "../observe/files.js";
9
10
  export const laidAt = (store, digest) => storeAt(store, 'cas', 'laid', digest.replace(/^[^:]*:/, ''));
10
- export const releasesOf = (lock) => lock.filter((line) => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') === 'reads' && fieldOf(line, 'value') !== 'withdraw');
11
+ /** A host artifact binding, not a measured ceiling or an object act. */
12
+ export const isReleaseBinding = (line) => isConfig(line)
13
+ && fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') === 'reads'
14
+ && fieldOf(line, 'value') !== 'withdraw'
15
+ && (fieldOf(line, 'measure') === 'digest' || /^[a-zA-Z][a-zA-Z0-9-]*:[a-f0-9]+$/.test(fieldOf(line, 'value')));
16
+ export const releasesOf = (lock) => lock.filter(isReleaseBinding);
11
17
  const laidMark = '.laid';
12
18
  const digestOf = (bytes) => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
13
19
  const hashed = (digest, bytes) => createHash(digest.includes(':') ? digest.slice(0, digest.indexOf(':')) : 'sha256').update(bytes).digest('hex') === digest.replace(/^[^:]*:/, '');
@@ -119,6 +125,11 @@ function keepSnapshot(store, digest, bytes) {
119
125
  export function layReleases(store, own, lock, contentStore = own) {
120
126
  let laid = 0;
121
127
  for (const line of releasesOf(lock)) {
128
+ // A release catalog entry without a mount is materialized only when requested.
129
+ // Its absence must not block an unrelated place's fold.
130
+ const needs = fieldOf(line, 'needs');
131
+ if (!needs)
132
+ continue;
122
133
  const digest = fieldOf(line, 'value');
123
134
  const at = laidAt(store, digest);
124
135
  const dir = lstatSync(at, { throwIfNoEntry: false })?.isDirectory() ?? false;
@@ -133,10 +144,7 @@ export function layReleases(store, own, lock, contentStore = own) {
133
144
  if (bytes !== undefined && current !== undefined && !dir && !sameSnapshot(snapshotOf(current), members(bytes).map((one) => [one.name, digestOf(one.bytes)])))
134
145
  throw new Error(`REFUSE·release snapshot for ${digest} omits or changes archive members; explicit rebuild required`);
135
146
  if (bytes === undefined && !dir && current === undefined)
136
- throw new Error(`REFUSE·release ${fieldOf(line, 'scope')} has no available archive or snapshot`);
137
- const needs = fieldOf(line, 'needs');
138
- if (!needs)
139
- continue;
147
+ throw new ContentNeeded(digest, `REFUSE·release ${fieldOf(line, 'scope')} has no available archive or snapshot`);
140
148
  const root = fieldOf(line, 'shape');
141
149
  const place = join(own, archiveCoordinate(needs));
142
150
  const link = lstatSync(place, { throwIfNoEntry: false });
@@ -2,7 +2,7 @@ import { capsuleAt } from './capsule.ts';
2
2
  import type { Capsule } from './capsule.ts';
3
3
  import type { ToposResolver } from './selected-topos.ts';
4
4
  /** Only explicit capsule offers request execution. Forms/classes/views do not need an executable alias. */
5
- export declare function selectedCapsules(pins: readonly string[], resolve?: typeof capsuleAt, select?: ToposResolver): readonly {
5
+ export declare function selectedCapsules(pins: readonly string[], resolve?: typeof capsuleAt, select?: ToposResolver, host?: readonly string[]): readonly {
6
6
  readonly capsule: Capsule;
7
7
  readonly offer: string;
8
8
  }[];
@@ -4,13 +4,17 @@ import { selectedTopoi, toposAt } from "./selected-topos.js";
4
4
  import { canonical } from '@lapxo/topos/wire';
5
5
  import { fieldOf } from "../fold/claims.js";
6
6
  import { ownLock } from "../observe/runner.js";
7
+ import { archiveCoordinate } from "./archive.js";
7
8
  /** Only explicit capsule offers request execution. Forms/classes/views do not need an executable alias. */
8
- export function selectedCapsules(pins, resolve = capsuleAt, select = toposAt) {
9
+ export function selectedCapsules(pins, resolve = capsuleAt, select = toposAt, host = ownLock()) {
9
10
  return selectedTopoi(pins, select).flatMap(({ digest, topos }) => capsuleOffers(topos).map(offer => {
10
- const projections = ownLock().filter(line => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') !== 'reads' && fieldOf(line, 'value') === offer.value);
11
- if (projections.length !== 1 || !fieldOf(projections[0], 'shape'))
11
+ const projections = host.filter(line => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') === 'writes' && fieldOf(line, 'value') === offer.value);
12
+ if (!projections.length || projections.some(line => !fieldOf(line, 'shape')))
12
13
  throw Error(`REFUSE·pin ${digest} capsule ${offer.value} needs one host projection`);
13
- const projected = { ...offer, shape: fieldOf(projections[0], 'shape') };
14
+ const entries = [...new Set(projections.map(line => archiveCoordinate(fieldOf(line, 'shape'))))];
15
+ if (entries.length !== 1)
16
+ throw Error(`REFUSE·pin ${digest} capsule ${offer.value} has conflicting host projections`);
17
+ const projected = { ...offer, shape: entries[0] };
14
18
  const capsule = resolve(offer.value, projected.shape);
15
19
  if (capsule === undefined)
16
20
  throw Error(`REFUSE·pin ${digest} · ${offer.scope} · offered capsule ${offer.value} unavailable`);
@@ -10,7 +10,7 @@ export declare function toposAt(digest: string): StandingTopos;
10
10
  export declare function contentAt(digest: string): Uint8Array;
11
11
  /** A declared JS context offer uses this host adapter only after selection; execution is not selection. */
12
12
  export declare function contextModuleAt(digest: string): string;
13
- /** Selection verifies the standing and its directly declared closure. It does not request any capability. */
13
+ /** Selection verifies standing identity. Requested capabilities verify their artifact bytes separately. */
14
14
  export declare function selectedTopoi(pins: readonly string[], resolve?: ToposResolver): readonly SelectedTopos[];
15
15
  /** Read a declared wire list without copying a world's claims or executing its offers.
16
16
  * Independent declarations must agree; selection order cannot choose a contract.