@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/README.md +5 -5
- package/TARGET.bound +8 -16
- package/dist/cli/accept.js +112 -71
- package/dist/cli/owner.js +3 -0
- package/dist/cli/verb.js +46 -10
- package/dist/fold/configures.js +3 -1
- package/dist/fold/digests.js +7 -1
- package/dist/fold/kept.js +5 -1
- package/dist/fold/observed.d.ts +1 -0
- package/dist/fold/observed.js +22 -5
- package/dist/fold/region-declarations.d.ts +2 -0
- package/dist/fold/region-declarations.js +13 -0
- package/dist/fold/resolved.js +9 -1
- package/dist/fold/second.js +3 -0
- package/dist/host/capsule.js +8 -2
- package/dist/host/content-needed.d.ts +5 -0
- package/dist/host/content-needed.js +8 -0
- package/dist/host/read-only.d.ts +3 -0
- package/dist/host/read-only.js +9 -0
- package/dist/host/release.js +2 -1
- package/dist/host/selected-topos.d.ts +1 -1
- package/dist/host/selected-topos.js +5 -10
- package/dist/host/source-content.d.ts +4 -2
- package/dist/host/source-content.js +21 -15
- package/dist/land/leaving.js +4 -0
- package/dist/land/ledger.js +3 -0
- package/dist/observe/regions.js +2 -2
- package/dist/observe/run.js +5 -1
- package/package.json +1 -1
- package/receipts.bound +73 -70
package/dist/cli/verb.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 :
|
|
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),
|
package/dist/fold/configures.js
CHANGED
|
@@ -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) {
|
package/dist/fold/digests.js
CHANGED
|
@@ -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
|
-
|
|
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_) => {
|
|
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),
|
package/dist/fold/observed.d.ts
CHANGED
|
@@ -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
|
package/dist/fold/observed.js
CHANGED
|
@@ -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) ??
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
+
}
|
package/dist/fold/resolved.js
CHANGED
|
@@ -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]
|
|
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)) {
|
package/dist/fold/second.js
CHANGED
|
@@ -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))
|
package/dist/host/capsule.js
CHANGED
|
@@ -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
|
|
51
|
-
|
|
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,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,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
|
+
}
|
package/dist/host/release.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
replaceWhole(at, bytes);
|
|
42
|
-
return at;
|
|
36
|
+
contentAt(digest);
|
|
37
|
+
return blobAt(ownStore(), digest);
|
|
43
38
|
}
|
|
44
|
-
/** Selection verifies
|
|
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[],
|
|
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(
|
|
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,
|
|
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/') ||
|
|
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
|
+
}
|
package/dist/land/leaving.js
CHANGED
|
@@ -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');
|
package/dist/land/ledger.js
CHANGED
|
@@ -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
|
}
|
package/dist/observe/regions.js
CHANGED
|
@@ -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
|
|
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));
|