@lapxo/bound 0.1.2 → 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,16 +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);
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());
390
425
  // Public content is authenticated by its digest, independently of owner history.
391
426
  // A named view does not request mounting every release of the instrument.
392
- if (as !== NAMED_VIEWS.help && lockLines(storeOf(root)).length)
393
- await resolveSources(standingOf(root, under).standing, as === undefined);
427
+ const receiptCheck = as === undefined && argv.includes(FLAGS.check);
394
428
  if (as === OBJECT_VIEW) {
395
429
  const inputs = positionalsOf(argv).filter(one => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
396
430
  if (inputs.length > 1)
@@ -406,6 +440,8 @@ async function foldAt(root, under, argv) {
406
440
  const key = valueOf(argv, FLAGS.key);
407
441
  if (as === undefined) {
408
442
  const exit = await pass(root, entryOf(root), under, argv.includes(FLAGS.check));
443
+ if (receiptCheck)
444
+ return exit;
409
445
  if (exit !== 0)
410
446
  return exit;
411
447
  const history = signedOwnerLines(storeOf(root));
@@ -421,7 +457,7 @@ async function foldAt(root, under, argv) {
421
457
  }
422
458
  return exit;
423
459
  }
424
- const named = positionalsOf(argv).filter((one) => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
460
+ const named = inputs;
425
461
  const coord = positionalsOf(argv).find((one) => one !== under?.replace(/\/$/, '') && !one.startsWith('-') && !(existsSync(resolve(one)) && lstatSync(resolve(one)).isDirectory()));
426
462
  if ((as === 'why' || as === NAMED_VIEWS.because || as.split('@')[0] === 'why') && coord !== undefined)
427
463
  return view(root, under, as === NAMED_VIEWS.because ? 'why' : as, [], coord, key);
@@ -432,7 +468,7 @@ async function foldAt(root, under, argv) {
432
468
  return batch.code;
433
469
  if (batch.kind === 'once')
434
470
  process.stdout.write(`ONCE ${batch.proposed.length} lines · every one already landed: the view folds the tree as it stands\n`);
435
- 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));
436
472
  }
437
473
  const VERBS = {
438
474
  fold: (root, rest) => foldVerb(root, rest),
@@ -4,6 +4,8 @@ import { isReaderLock } from "./observed.js";
4
4
  import { isRoleLock } from "./roles.js";
5
5
  import { isSignerLock } from "./signers.js";
6
6
  import { isReleaseBinding } from "../host/release.js";
7
+ import { isRegionDeclaration } from "./region-declarations.js";
8
+ import { ownLock } from "../observe/runner.js";
7
9
  function isEffectLock(line) {
8
10
  const got = parse(line);
9
11
  return got.kind === 'fact' && got.value.fields['measure'] === 'effect';
@@ -11,7 +13,7 @@ function isEffectLock(line) {
11
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. */
12
14
  function configures(line) {
13
15
  const got = parse(line);
14
- return isReleaseBinding(line) || 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());
15
17
  }
16
18
  /** Every reads line that configures nothing is a ceiling: a bound some reading has to meet. */
17
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,3 +1,4 @@
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";
@@ -129,8 +130,11 @@ export function keptPlace(store, under) {
129
130
  };
130
131
  }
131
132
  export const keptRender = (store, key, name) => observeText(memoAt(store, `render/${key}/${name}`));
132
- 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_)); };
133
135
  export function keepFold(store, key, fold, points, instrument = '') {
136
+ if (readOnly())
137
+ return;
134
138
  const packed = {
135
139
  ...fold,
136
140
  standing: joined(fold.standing),
@@ -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;
@@ -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,3 +1,4 @@
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';
@@ -143,7 +144,7 @@ export function layReleases(store, own, lock, contentStore = own) {
143
144
  if (bytes !== undefined && current !== undefined && !dir && !sameSnapshot(snapshotOf(current), members(bytes).map((one) => [one.name, digestOf(one.bytes)])))
144
145
  throw new Error(`REFUSE·release snapshot for ${digest} omits or changes archive members; explicit rebuild required`);
145
146
  if (bytes === undefined && !dir && current === undefined)
146
- throw new Error(`REFUSE·release ${fieldOf(line, 'scope')} has no available archive or snapshot`);
147
+ throw new ContentNeeded(digest, `REFUSE·release ${fieldOf(line, 'scope')} has no available archive or snapshot`);
147
148
  const root = fieldOf(line, 'shape');
148
149
  const place = join(own, archiveCoordinate(needs));
149
150
  const link = lstatSync(place, { throwIfNoEntry: false });
@@ -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.
@@ -1,3 +1,4 @@
1
+ import { ContentNeeded } from "./content-needed.js";
1
2
  import { createHash } from 'node:crypto';
2
3
  import { readStanding } from '@lapxo/topos/standing';
3
4
  import { parse, wireAt } from '@lapxo/topos/wire';
@@ -5,13 +6,9 @@ import { fieldOf } from "../fold/claims.js";
5
6
  import { blobAt } from "../land/ledger.js";
6
7
  import { observeFile } from "../observe/files.js";
7
8
  import { ownStore, ownLock } from "../observe/runner.js";
8
- import { replaceWhole } from "../land/ledger.js";
9
- import { join } from 'node:path';
10
9
  /** Resolve content using the instrument wire's admitted digest algorithms. No transport interpretation. */
11
10
  export function toposAt(digest) {
12
11
  const topos = readStanding(Buffer.from(contentAt(digest)).toString('utf8'));
13
- for (const dependency of topos.dependencies)
14
- contentAt(dependency);
15
12
  return topos;
16
13
  }
17
14
  export function contentAt(digest) {
@@ -27,7 +24,7 @@ export function contentAt(digest) {
27
24
  throw Error(`REFUSE·pin ${name} digest is not admitted`);
28
25
  const bytes = observeFile(blobAt(ownStore(), name));
29
26
  if (bytes === undefined)
30
- throw Error(`REFUSE·pin ${name} content unavailable · not laid`);
27
+ throw new ContentNeeded(name);
31
28
  if (createHash(algorithm).update(bytes).digest('hex') !== hex)
32
29
  throw Error(`REFUSE·pin ${name} content hash mismatch · not laid`);
33
30
  return bytes;
@@ -36,12 +33,10 @@ export function contentAt(digest) {
36
33
  }
37
34
  /** A declared JS context offer uses this host adapter only after selection; execution is not selection. */
38
35
  export function contextModuleAt(digest) {
39
- const bytes = contentAt(digest);
40
- const at = join(ownStore(), 'cas', 'runtime', digest.replace(':', '_') + '.mjs');
41
- replaceWhole(at, bytes);
42
- return at;
36
+ contentAt(digest);
37
+ return blobAt(ownStore(), digest);
43
38
  }
44
- /** Selection verifies the standing and its directly declared closure. It does not request any capability. */
39
+ /** Selection verifies standing identity. Requested capabilities verify their artifact bytes separately. */
45
40
  export function selectedTopoi(pins, resolve = toposAt) {
46
41
  return [...new Set(pins.map(pin => fieldOf(pin, 'value')))].map(digest => ({ digest, topos: resolve(digest) }));
47
42
  }
@@ -5,6 +5,8 @@ export interface ContentPort {
5
5
  readonly fetch: (url: string) => Promise<Uint8Array>;
6
6
  }
7
7
  /** Transport uses declared locations; only the requested digest admits bytes into the cache. */
8
- export declare function resolveSelectedContent(lines: readonly string[], port: ContentPort): Promise<readonly string[]>;
8
+ export declare function resolveSelectedContent(lines: readonly string[], port: ContentPort, requested: readonly string[]): Promise<readonly string[]>;
9
9
  /** Exact resource URLs; no repository checkout or inferred release. */
10
- export declare function resolveSources(standing: readonly string[], materializeInstrument?: boolean): Promise<void>;
10
+ export declare function resolveSources(standing: readonly string[], requested: readonly string[]): Promise<void>;
11
+ /** A changed verified store is the only reason to retry. A repeated miss never loops or changes a named pin. */
12
+ export declare function resolveRequested<T>(resolve: (digest: string) => Promise<void>, fold: () => Promise<T>): Promise<T>;
@@ -1,11 +1,11 @@
1
+ import { ContentNeeded } from "./content-needed.js";
1
2
  import { createHash } from 'node:crypto';
2
- import { readStanding } from '@lapxo/topos/standing';
3
3
  import { parse, wireAt } from '@lapxo/topos/wire';
4
4
  import { blobAt, landBlob } from "../land/ledger.js";
5
5
  import { observeFile } from "../observe/files.js";
6
6
  import { ownLock, ownStore } from "../observe/runner.js";
7
7
  /** Transport uses declared locations; only the requested digest admits bytes into the cache. */
8
- export async function resolveSelectedContent(lines, port) {
8
+ export async function resolveSelectedContent(lines, port, requested) {
9
9
  const fields = lines.flatMap(line => { const p = parse(line); return p.kind === 'fact' && p.value.fields.value !== 'withdraw' ? [p.value.fields] : []; });
10
10
  const selectionIndex = (scope) => { const match = /(^|\/)uses\//.exec(scope); return match === null ? -1 : match.index + match[1].length; };
11
11
  // A dependency constraint is not a content binding. Historical artifact records
@@ -21,7 +21,6 @@ export async function resolveSelectedContent(lines, port) {
21
21
  if (urls.length)
22
22
  locations.set(binding.value ?? '', [...new Set([...(locations.get(binding.value ?? '') ?? []), ...urls])]);
23
23
  }
24
- const roots = [...new Set(bindings.filter(f => selectionIndex(f.scope ?? '') >= 0).map(f => f.value ?? ''))];
25
24
  const received = [];
26
25
  const validate = (digest) => {
27
26
  const split = digest.indexOf(':');
@@ -30,9 +29,6 @@ export async function resolveSelectedContent(lines, port) {
30
29
  throw Error(`REFUSE·pin ${digest} digest is not admitted`);
31
30
  return { algorithm, hex };
32
31
  };
33
- // Refuse malformed declared selections before materializing any dependency.
34
- for (const binding of bindings)
35
- validate(binding.value ?? '');
36
32
  const ensure = async (digest) => {
37
33
  const { algorithm, hex } = validate(digest);
38
34
  const verify = (bytes) => {
@@ -66,17 +62,12 @@ export async function resolveSelectedContent(lines, port) {
66
62
  }
67
63
  throw Error(`REFUSE·source ${digest} ${last instanceof Error ? last.message : 'declared sources unavailable'}`);
68
64
  };
69
- for (const digest of new Set(bindings.filter(f => f.scope?.startsWith('dep/') && f.role === 'reads').map(f => f.value ?? '')))
65
+ for (const digest of new Set(requested))
70
66
  await ensure(digest);
71
- for (const digest of roots) {
72
- const topos = readStanding(Buffer.from(await ensure(digest)).toString('utf8'));
73
- for (const dependency of topos.dependencies)
74
- await ensure(dependency);
75
- }
76
67
  return received;
77
68
  }
78
69
  /** Exact resource URLs; no repository checkout or inferred release. */
79
- export async function resolveSources(standing, materializeInstrument = false) {
70
+ export async function resolveSources(standing, requested) {
80
71
  const instrument = ownLock().flatMap(line => { const p = parse(line); return p.kind === 'fact' ? [p.value.fields] : []; });
81
72
  const wire = wireAt(instrument, Number.MAX_SAFE_INTEGER);
82
73
  if (wire === null)
@@ -86,7 +77,7 @@ export async function resolveSources(standing, materializeInstrument = false) {
86
77
  if (p.kind !== 'fact')
87
78
  return false;
88
79
  const fields = p.value.fields;
89
- return fields.scope?.startsWith('sources/') || (fields.scope?.startsWith('dep/') && (fields.role !== 'reads' || (materializeInstrument && !!fields.needs)));
80
+ return fields.scope?.startsWith('sources/') || fields.scope?.startsWith('dep/');
90
81
  });
91
82
  const received = await resolveSelectedContent([...standing, ...bindings], {
92
83
  algorithms: wire.digests,
@@ -98,7 +89,22 @@ export async function resolveSources(standing, materializeInstrument = false) {
98
89
  throw Error(`HTTP ${response.status}`);
99
90
  return new Uint8Array(await response.arrayBuffer());
100
91
  },
101
- });
92
+ }, requested);
102
93
  for (const digest of received)
103
94
  process.stdout.write(`FETCHED ${digest} · verified\n`);
104
95
  }
96
+ /** A changed verified store is the only reason to retry. A repeated miss never loops or changes a named pin. */
97
+ export async function resolveRequested(resolve, fold) {
98
+ const attempted = new Set();
99
+ for (;;) {
100
+ try {
101
+ return await fold();
102
+ }
103
+ catch (error) {
104
+ if (!(error instanceof ContentNeeded) || attempted.has(error.digest))
105
+ throw error;
106
+ attempted.add(error.digest);
107
+ await resolve(error.digest);
108
+ }
109
+ }
110
+ }
@@ -1,3 +1,5 @@
1
+ import { readOnly } from "../host/read-only.js";
2
+ import { meets } from "../fold/closed.js";
1
3
  import { fieldOf } from "../fold/claims.js";
2
4
  import { sourced } from "./vouched.js";
3
5
  import { observedClaims } from "../fold/observed.js";
@@ -14,6 +16,8 @@ export function coordinatesWithdrawn(root, store, signed, standing) {
14
16
  .map((line) => fieldOf(line, 'scope').slice(family.length + 1));
15
17
  if (!asked.length)
16
18
  return { taken: [], refused: [] };
19
+ if (readOnly() && !meets(root, store, ''))
20
+ throw Error('REFUSE·withdraw current native receipts are required to judge file removal; fold the declared inputs first');
17
21
  /** A run rewrites every block of a test file under one observation, so a block whose title moved still holds a row
18
22
  * from an older one: what reads a file now is what the latest run of each test file wrote, and nothing older. */
19
23
  const memo = observedClaims(root, store, { wait: false }).filter((line) => fieldOf(line, 'measure') === 'memo');
@@ -1,3 +1,4 @@
1
+ import { requireEffects } from "../host/read-only.js";
1
2
  import { appendFileSync, closeSync, dirname, existsSync, join, linkSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync } from "../host/io.js";
2
3
  import { storeRoot } from "../host/ports/store.js";
3
4
  import { canonical, EXTENSION, PROTOCOL } from '@lapxo/topos/wire';
@@ -81,6 +82,7 @@ function landWhole(at, bytes) {
81
82
  }
82
83
  /** A file replaced whole: the bytes land in a temp beside it and the name moves in one rename, so a killed write keeps the temp, never a torn file under the name a later read parses. */
83
84
  export function replaceWhole(at, bytes) {
85
+ requireEffects('persistent replacement');
84
86
  mkdirSync(dirname(at), { recursive: true });
85
87
  const part = `${at}.${process.pid}.part`;
86
88
  writeFileSync(part, bytes);
@@ -124,6 +126,7 @@ export function landMemo(store, key, bytes) {
124
126
  }
125
127
  export const landEnvelope = (store, digest, bytes) => landWhole(storeAt(store, 'cas', 'envelopes', `${digest.slice(digest.indexOf(':') + 1)}.json`), bytes);
126
128
  export function land(store, writer, lines) {
129
+ requireEffects('ledger admission');
127
130
  const at = writerFile(store, writer);
128
131
  return { at, appended: appendNew(at, lines) };
129
132
  }
@@ -23,11 +23,11 @@ import { coordinatesUnder, observeText } from "./files.js";
23
23
  import { ownLock } from "./runner.js";
24
24
  import { nameOf } from "../render/page.js";
25
25
  import { placeLines, takenFor } from "../fold/place-inputs.js";
26
+ import { isRegionDeclaration } from "../fold/region-declarations.js";
26
27
  const fact = (scope, value, form = 'alphabet', measure = 'fact') => canonical({ scope, role: 'writes', form, measure, value, by: 'fold', at: 'place:fold' });
27
28
  const described = (local = []) => {
28
29
  const lock = ownLock();
29
- const measures = ['lines', 'receipts', 'reads', wordOn(lock, 'region-coordinate', lock), wordOn(lock, 'region-leaf', lock)];
30
- const eligible = (line) => fieldOf(line, 'scope').startsWith('region/') && measures.includes(fieldOf(line, 'measure')) && fieldOf(line, 'value') !== 'withdraw';
30
+ const eligible = (line) => isRegionDeclaration(line, lock);
31
31
  const key = (line) => `${fieldOf(line, 'scope')} ${fieldOf(line, 'measure')}`;
32
32
  const supplied = local.filter(eligible);
33
33
  const overridden = new Set(supplied.map(key));