@lapxo/bound 0.1.0
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/CONTRIBUTING.md +44 -0
- package/LICENSE +22 -0
- package/README.md +97 -0
- package/TARGET.bound +361 -0
- package/dist/cli/accept.d.ts +23 -0
- package/dist/cli/accept.js +423 -0
- package/dist/cli/args.d.ts +10 -0
- package/dist/cli/args.js +34 -0
- package/dist/cli/host.d.ts +1 -0
- package/dist/cli/host.js +32 -0
- package/dist/cli/names.d.ts +25 -0
- package/dist/cli/names.js +12 -0
- package/dist/cli/observe.d.ts +1 -0
- package/dist/cli/observe.js +6 -0
- package/dist/cli/owner.d.ts +35 -0
- package/dist/cli/owner.js +119 -0
- package/dist/cli/pass.d.ts +6 -0
- package/dist/cli/pass.js +309 -0
- package/dist/cli/place.d.ts +74 -0
- package/dist/cli/place.js +290 -0
- package/dist/cli/takes.d.ts +30 -0
- package/dist/cli/takes.js +108 -0
- package/dist/cli/verb.d.ts +2 -0
- package/dist/cli/verb.js +478 -0
- package/dist/fold/ceilings.d.ts +24 -0
- package/dist/fold/ceilings.js +129 -0
- package/dist/fold/claims.d.ts +25 -0
- package/dist/fold/claims.js +107 -0
- package/dist/fold/closed.d.ts +15 -0
- package/dist/fold/closed.js +168 -0
- package/dist/fold/configures.d.ts +4 -0
- package/dist/fold/configures.js +22 -0
- package/dist/fold/digests.d.ts +29 -0
- package/dist/fold/digests.js +211 -0
- package/dist/fold/evidence.d.ts +32 -0
- package/dist/fold/evidence.js +22 -0
- package/dist/fold/fold-worker.d.ts +1 -0
- package/dist/fold/fold-worker.js +13 -0
- package/dist/fold/kept.d.ts +29 -0
- package/dist/fold/kept.js +142 -0
- package/dist/fold/keys.d.ts +17 -0
- package/dist/fold/keys.js +84 -0
- package/dist/fold/laid.d.ts +17 -0
- package/dist/fold/laid.js +90 -0
- package/dist/fold/nos.d.ts +8 -0
- package/dist/fold/nos.js +48 -0
- package/dist/fold/object.d.ts +17 -0
- package/dist/fold/object.js +38 -0
- package/dist/fold/observed.d.ts +33 -0
- package/dist/fold/observed.js +444 -0
- package/dist/fold/opened.d.ts +5 -0
- package/dist/fold/opened.js +33 -0
- package/dist/fold/own.d.ts +24 -0
- package/dist/fold/own.js +215 -0
- package/dist/fold/paid.d.ts +11 -0
- package/dist/fold/paid.js +92 -0
- package/dist/fold/place-inputs.d.ts +17 -0
- package/dist/fold/place-inputs.js +70 -0
- package/dist/fold/places.d.ts +20 -0
- package/dist/fold/places.js +84 -0
- package/dist/fold/rank.d.ts +3 -0
- package/dist/fold/rank.js +9 -0
- package/dist/fold/reach.d.ts +13 -0
- package/dist/fold/reach.js +65 -0
- package/dist/fold/readers.d.ts +44 -0
- package/dist/fold/readers.js +132 -0
- package/dist/fold/receipts.d.ts +8 -0
- package/dist/fold/receipts.js +47 -0
- package/dist/fold/region.d.ts +72 -0
- package/dist/fold/region.js +286 -0
- package/dist/fold/release-policy.d.ts +67 -0
- package/dist/fold/release-policy.js +103 -0
- package/dist/fold/resolved.d.ts +18 -0
- package/dist/fold/resolved.js +123 -0
- package/dist/fold/roles.d.ts +17 -0
- package/dist/fold/roles.js +59 -0
- package/dist/fold/second.d.ts +18 -0
- package/dist/fold/second.js +68 -0
- package/dist/fold/shards.d.ts +29 -0
- package/dist/fold/shards.js +100 -0
- package/dist/fold/signed.d.ts +18 -0
- package/dist/fold/signed.js +75 -0
- package/dist/fold/signers.d.ts +24 -0
- package/dist/fold/signers.js +58 -0
- package/dist/fold/stranger.d.ts +3 -0
- package/dist/fold/stranger.js +24 -0
- package/dist/fold/views.d.ts +27 -0
- package/dist/fold/views.js +66 -0
- package/dist/fold/wire.d.ts +19 -0
- package/dist/fold/wire.js +41 -0
- package/dist/fold/zoom.d.ts +16 -0
- package/dist/fold/zoom.js +80 -0
- package/dist/host/adapters/keys/ed25519.d.ts +7 -0
- package/dist/host/adapters/keys/ed25519.js +30 -0
- package/dist/host/archive.d.ts +11 -0
- package/dist/host/archive.js +152 -0
- package/dist/host/capsule-process.d.ts +20 -0
- package/dist/host/capsule-process.js +54 -0
- package/dist/host/capsule.d.ts +13 -0
- package/dist/host/capsule.js +149 -0
- package/dist/host/cell-receipts.d.ts +2 -0
- package/dist/host/cell-receipts.js +124 -0
- package/dist/host/digest.d.ts +3 -0
- package/dist/host/digest.js +15 -0
- package/dist/host/hash.d.ts +2 -0
- package/dist/host/hash.js +3 -0
- package/dist/host/io.d.ts +9 -0
- package/dist/host/io.js +9 -0
- package/dist/host/located.d.ts +4 -0
- package/dist/host/located.js +105 -0
- package/dist/host/objects.d.ts +5 -0
- package/dist/host/objects.js +18 -0
- package/dist/host/ports/store.d.ts +2 -0
- package/dist/host/ports/store.js +18 -0
- package/dist/host/provide.d.ts +23 -0
- package/dist/host/provide.js +81 -0
- package/dist/host/release.d.ts +12 -0
- package/dist/host/release.js +213 -0
- package/dist/host/selected-capsules.d.ts +8 -0
- package/dist/host/selected-capsules.js +19 -0
- package/dist/host/selected-topos.d.ts +17 -0
- package/dist/host/selected-topos.js +61 -0
- package/dist/host/source-content.d.ts +10 -0
- package/dist/host/source-content.js +94 -0
- package/dist/host/timing.d.ts +2 -0
- package/dist/host/timing.js +7 -0
- package/dist/judge/late.d.ts +7 -0
- package/dist/judge/late.js +33 -0
- package/dist/judge/take.d.ts +57 -0
- package/dist/judge/take.js +75 -0
- package/dist/judge/widening.d.ts +1 -0
- package/dist/judge/widening.js +25 -0
- package/dist/land/act.d.ts +4 -0
- package/dist/land/act.js +209 -0
- package/dist/land/alias.d.ts +10 -0
- package/dist/land/alias.js +41 -0
- package/dist/land/held.d.ts +7 -0
- package/dist/land/held.js +23 -0
- package/dist/land/inert.d.ts +26 -0
- package/dist/land/inert.js +33 -0
- package/dist/land/leaving.d.ts +14 -0
- package/dist/land/leaving.js +51 -0
- package/dist/land/ledger.d.ts +28 -0
- package/dist/land/ledger.js +148 -0
- package/dist/land/owned.d.ts +23 -0
- package/dist/land/owned.js +65 -0
- package/dist/land/sign.d.ts +16 -0
- package/dist/land/sign.js +30 -0
- package/dist/land/vouched.d.ts +21 -0
- package/dist/land/vouched.js +112 -0
- package/dist/land/withdraw.d.ts +14 -0
- package/dist/land/withdraw.js +24 -0
- package/dist/observe/files.d.ts +14 -0
- package/dist/observe/files.js +68 -0
- package/dist/observe/identity.d.ts +19 -0
- package/dist/observe/identity.js +61 -0
- package/dist/observe/match.d.ts +1 -0
- package/dist/observe/match.js +8 -0
- package/dist/observe/regions.d.ts +29 -0
- package/dist/observe/regions.js +200 -0
- package/dist/observe/run.d.ts +30 -0
- package/dist/observe/run.js +34 -0
- package/dist/observe/runner.d.ts +16 -0
- package/dist/observe/runner.js +83 -0
- package/dist/render/capsule.d.ts +24 -0
- package/dist/render/capsule.js +30 -0
- package/dist/render/field.d.ts +9 -0
- package/dist/render/field.js +46 -0
- package/dist/render/names.d.ts +3 -0
- package/dist/render/names.js +4 -0
- package/dist/render/open.d.ts +27 -0
- package/dist/render/open.js +61 -0
- package/dist/render/page.d.ts +3 -0
- package/dist/render/page.js +7 -0
- package/dist/render/receipts.d.ts +3 -0
- package/dist/render/receipts.js +46 -0
- package/dist/render/shape.d.ts +16 -0
- package/dist/render/shape.js +74 -0
- package/dist/render/target.d.ts +18 -0
- package/dist/render/target.js +52 -0
- package/dist/render/view.d.ts +22 -0
- package/dist/render/view.js +191 -0
- package/docs/readme-assets/atlas.svg +169 -0
- package/docs/usage.md +53 -0
- package/package.json +48 -0
- package/receipts.bound +59 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as nodeModule from 'node:module';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { join, relative, isAbsolute, sep } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { fieldOf } from "../fold/claims.js";
|
|
6
|
+
import { observeText } from "../observe/files.js";
|
|
7
|
+
import { ownLock, ownStore, ownRoot } from "../observe/runner.js";
|
|
8
|
+
import { runtimeTreeAt } from "./release.js";
|
|
9
|
+
import { theWord } from "../fold/wire.js";
|
|
10
|
+
const registerHooks = nodeModule.registerHooks;
|
|
11
|
+
const taken = (exp) => (typeof exp === 'string' ? exp : exp && typeof exp === 'object' && 'default' in exp && typeof exp.default === 'string' ? exp.default : '');
|
|
12
|
+
/**
|
|
13
|
+
* Where a named package or world lies: `dep/<name>` for a library the instrument installs, `uses/<name>` for a world
|
|
14
|
+
* pinned by its GitHub release. Both are the digest, laid. Nothing here spells a path.
|
|
15
|
+
*/
|
|
16
|
+
export function pinPlace(name) {
|
|
17
|
+
const lock = ownLock();
|
|
18
|
+
const line = lock.find((one) => fieldOf(one, 'scope') === `dep/${name}` && fieldOf(one, 'role') === 'reads')
|
|
19
|
+
?? lock.find((one) => fieldOf(one, 'scope') === `uses/${name}`);
|
|
20
|
+
if (line === undefined)
|
|
21
|
+
return '';
|
|
22
|
+
const digest = fieldOf(line, 'value');
|
|
23
|
+
return digest ? runtimeTreeAt(ownStore(), ownRoot(), digest, ownStore()) ?? '' : '';
|
|
24
|
+
}
|
|
25
|
+
export function pinExport(name, sub) {
|
|
26
|
+
const at = pinPlace(name);
|
|
27
|
+
const file = taken(JSON.parse(observeText(join(at, manifestOf())) ?? '{}').exports?.[sub]);
|
|
28
|
+
return at && file ? join(at, file) : '';
|
|
29
|
+
}
|
|
30
|
+
export function pinName(name) {
|
|
31
|
+
const at = pinPlace(name);
|
|
32
|
+
return at ? (JSON.parse(observeText(join(at, manifestOf())) ?? '{}').name ?? '') : '';
|
|
33
|
+
}
|
|
34
|
+
const manifestOf = () => `${theWord(ownLock(), 'pin-shape')}.json`;
|
|
35
|
+
/** Node package resolution at a digest-laid library; exports remain authoritative. */
|
|
36
|
+
export function resolvePinned(specifier, context, nextResolve, pin) {
|
|
37
|
+
if (pin.exports)
|
|
38
|
+
return nextResolve(specifier, { ...context, parentURL: pin.json });
|
|
39
|
+
const sub = specifier === pin.pkg ? '' : specifier.slice(pin.pkg.length + 1);
|
|
40
|
+
const requested = join(pin.at, sub);
|
|
41
|
+
const coordinate = relative(pin.at, requested);
|
|
42
|
+
if (isAbsolute(coordinate) || coordinate === '..' || coordinate.startsWith(`..${sep}`))
|
|
43
|
+
throw new Error('REFUSE·pin library import leaves its laid tree');
|
|
44
|
+
const file = nodeModule.createRequire(pin.json).resolve(requested);
|
|
45
|
+
const entry = relative(realpathSync(pin.at), realpathSync(file));
|
|
46
|
+
if (isAbsolute(entry) || entry === '..' || entry.startsWith(`..${sep}`))
|
|
47
|
+
throw new Error('REFUSE·pin library entry leaves its laid tree');
|
|
48
|
+
return nextResolve(pathToFileURL(file).href, context);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A blob stays self-contained. The one thing it does not carry is the SDK the host provides: an import whose package
|
|
52
|
+
* the instrument's own lock names at dep/<name> resolves at the place pinPlace delivers. Nothing is linked into the
|
|
53
|
+
* blob's folder.
|
|
54
|
+
*/
|
|
55
|
+
let hooked = false;
|
|
56
|
+
export function provide() {
|
|
57
|
+
if (hooked)
|
|
58
|
+
return;
|
|
59
|
+
hooked = true;
|
|
60
|
+
const pins = ownLock()
|
|
61
|
+
.filter((line) => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') === 'reads')
|
|
62
|
+
.flatMap((line) => {
|
|
63
|
+
const name = fieldOf(line, 'scope').slice('dep/'.length);
|
|
64
|
+
const at = pinPlace(name);
|
|
65
|
+
const pkg = pinName(name);
|
|
66
|
+
const file = manifestOf();
|
|
67
|
+
const manifest = JSON.parse(observeText(join(at, file)) ?? '{}');
|
|
68
|
+
return at && pkg ? [{ at, pkg, exports: Object.prototype.hasOwnProperty.call(manifest, 'exports'), href: pathToFileURL(at).href, json: pathToFileURL(join(at, file)).href }] : [];
|
|
69
|
+
});
|
|
70
|
+
registerHooks({
|
|
71
|
+
resolve(specifier, context, nextResolve) {
|
|
72
|
+
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.includes(':'))
|
|
73
|
+
return nextResolve(specifier, context);
|
|
74
|
+
const parent = context.parentURL ?? '';
|
|
75
|
+
const pin = pins.find((one) => specifier === one.pkg || specifier.startsWith(`${one.pkg}/`));
|
|
76
|
+
if (pin === undefined || parent.startsWith(pin.href))
|
|
77
|
+
return nextResolve(specifier, context);
|
|
78
|
+
return resolvePinned(specifier, context, nextResolve, pin);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const laidAt: (store: string, digest: string) => string;
|
|
2
|
+
export declare const releasesOf: (lock: readonly string[]) => readonly string[];
|
|
3
|
+
/**
|
|
4
|
+
* What the host does before anything runs: each release the instrument's own lock names by digest is a snapshot of
|
|
5
|
+
* lines and digests, kept by digest, and the place the line needs is a materialization of verified CAS bytes. Materialized files do not share writable inodes with the store. A blob is laid only when its bytes hash to the digest that names it. What stands laid is
|
|
6
|
+
* said by resolvedOf: a line for each release whose blob hashes to its name and whose place, or the cas when its line
|
|
7
|
+
* needs none, holds it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function layReleases(store: string, own: string, lock: readonly string[], contentStore?: string): number;
|
|
10
|
+
export declare const resolvedOf: (store: string, own: string, lock: readonly string[], contentStore?: string) => readonly string[];
|
|
11
|
+
/** A host loads verified release bytes from a runtime tree, never interprets a snapshot file as a directory. */
|
|
12
|
+
export declare function runtimeTreeAt(store: string, own: string, digest: string, contentStore?: string): string | undefined;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, sep } from 'node:path';
|
|
4
|
+
import { canonical } from '@lapxo/topos/wire';
|
|
5
|
+
import { fieldOf, isWire } from "../fold/claims.js";
|
|
6
|
+
import { archiveCoordinate, archiveDestination, members } from "./archive.js";
|
|
7
|
+
import { blobAt, holdStore, landBlob, replaceWhole, storeAt } from "../land/ledger.js";
|
|
8
|
+
import { coordinatesUnder, observeFile, observeText } from "../observe/files.js";
|
|
9
|
+
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
|
+
const laidMark = '.laid';
|
|
12
|
+
const digestOf = (bytes) => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
13
|
+
const hashed = (digest, bytes) => createHash(digest.includes(':') ? digest.slice(0, digest.indexOf(':')) : 'sha256').update(bytes).digest('hex') === digest.replace(/^[^:]*:/, '');
|
|
14
|
+
const snapshotOf = (text) => text.split('\n').filter(isWire).filter((line) => fieldOf(line, 'measure') === 'digest' && fieldOf(line, 'scope').startsWith('file/'))
|
|
15
|
+
.map((line) => [fieldOf(line, 'scope').slice('file/'.length), fieldOf(line, 'value')]);
|
|
16
|
+
function snapshotLines(files) {
|
|
17
|
+
return files.map(([coord, digest]) => canonical({
|
|
18
|
+
scope: `file/${coord}`, role: 'writes', form: 'alphabet', measure: 'digest', value: digest, at: `place:${digest}`, by: 'target',
|
|
19
|
+
})).join('\n') + (files.length ? '\n' : '');
|
|
20
|
+
}
|
|
21
|
+
function layDir(from, into) {
|
|
22
|
+
for (const coord of coordinatesUnder(from, from)) {
|
|
23
|
+
if (coord === laidMark)
|
|
24
|
+
continue;
|
|
25
|
+
const src = join(from, coord);
|
|
26
|
+
const dest = archiveDestination(into, coord);
|
|
27
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
28
|
+
if (existsSync(dest))
|
|
29
|
+
continue;
|
|
30
|
+
const bytes = observeFile(src);
|
|
31
|
+
if (bytes === undefined)
|
|
32
|
+
throw new Error(`REFUSE·release unreadable historical member ${coord}`);
|
|
33
|
+
writeFileSync(dest, bytes, { flag: 'wx' });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function swapPlace(place, next, digest) {
|
|
37
|
+
const old = `${next}.old`;
|
|
38
|
+
if (lstatSync(old, { throwIfNoEntry: false }) !== undefined)
|
|
39
|
+
throw new Error(`REFUSE·release recovery required for ${old}`);
|
|
40
|
+
writeFileSync(join(next, laidMark), digest, { flag: 'wx' });
|
|
41
|
+
let moved = false;
|
|
42
|
+
try {
|
|
43
|
+
if (lstatSync(place, { throwIfNoEntry: false }) !== undefined) {
|
|
44
|
+
renameSync(place, old);
|
|
45
|
+
moved = true;
|
|
46
|
+
}
|
|
47
|
+
renameSync(next, place);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (moved && lstatSync(place, { throwIfNoEntry: false }) === undefined)
|
|
51
|
+
renameSync(old, place);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
if (moved)
|
|
55
|
+
rmSync(old, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
function nextPlace(place) {
|
|
58
|
+
const next = `${place}.${process.pid}`;
|
|
59
|
+
mkdirSync(dirname(place), { recursive: true });
|
|
60
|
+
mkdirSync(next); // An abandoned staging directory is never silently reused.
|
|
61
|
+
return next;
|
|
62
|
+
}
|
|
63
|
+
function layFiles(store, own, files, into) {
|
|
64
|
+
const held = files.map(([coordinate, digest]) => {
|
|
65
|
+
const coord = archiveCoordinate(coordinate);
|
|
66
|
+
const dest = archiveDestination(into, coord);
|
|
67
|
+
const src = [blobAt(own, digest), blobAt(store, digest)].find(existsSync);
|
|
68
|
+
if (src === undefined)
|
|
69
|
+
throw new Error(`REFUSE·release missing blob ${digest} for ${coord}`);
|
|
70
|
+
const bytes = observeFile(src);
|
|
71
|
+
if (bytes === undefined || !hashed(digest, bytes))
|
|
72
|
+
throw new Error(`REFUSE·release blob ${digest} for ${coord} does not hold`);
|
|
73
|
+
return { dest, bytes };
|
|
74
|
+
});
|
|
75
|
+
for (const { dest, bytes } of held) {
|
|
76
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
77
|
+
// Materialized files may be edited by their consumer; never share writable inodes with the CAS.
|
|
78
|
+
writeFileSync(dest, bytes, { flag: 'wx' });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function layoutMatches(place, files) {
|
|
82
|
+
return files.every(([coord, digest]) => {
|
|
83
|
+
const path = join(place, archiveCoordinate(coord));
|
|
84
|
+
const st = lstatSync(path, { throwIfNoEntry: false });
|
|
85
|
+
const bytes = st?.isFile() ? observeFile(path) : undefined;
|
|
86
|
+
return bytes !== undefined && hashed(digest, bytes);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function sameSnapshot(actual, expected) {
|
|
90
|
+
const named = new Map(actual);
|
|
91
|
+
return named.size === actual.length && actual.length === expected.length && expected.every(([name, digest]) => named.get(name) === digest);
|
|
92
|
+
}
|
|
93
|
+
function keepSnapshot(store, digest, bytes) {
|
|
94
|
+
const files = members(bytes).map((one) => {
|
|
95
|
+
const file = digestOf(one.bytes);
|
|
96
|
+
landBlob(store, file, one.bytes);
|
|
97
|
+
return [one.name, file];
|
|
98
|
+
});
|
|
99
|
+
const text = snapshotLines(files);
|
|
100
|
+
replaceWhole(laidAt(store, digest), text);
|
|
101
|
+
holdStore(store, 'laid', digest, Buffer.byteLength(text));
|
|
102
|
+
return files;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* What the host does before anything runs: each release the instrument's own lock names by digest is a snapshot of
|
|
106
|
+
* lines and digests, kept by digest, and the place the line needs is a materialization of verified CAS bytes. Materialized files do not share writable inodes with the store. A blob is laid only when its bytes hash to the digest that names it. What stands laid is
|
|
107
|
+
* said by resolvedOf: a line for each release whose blob hashes to its name and whose place, or the cas when its line
|
|
108
|
+
* needs none, holds it.
|
|
109
|
+
*/
|
|
110
|
+
export function layReleases(store, own, lock, contentStore = own) {
|
|
111
|
+
let laid = 0;
|
|
112
|
+
for (const line of releasesOf(lock)) {
|
|
113
|
+
const digest = fieldOf(line, 'value');
|
|
114
|
+
const at = laidAt(store, digest);
|
|
115
|
+
const dir = lstatSync(at, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
116
|
+
const bytes = observeFile(blobAt(contentStore, digest));
|
|
117
|
+
const named = digest.replace(/^[^:]*:/, '');
|
|
118
|
+
if (bytes !== undefined && !hashed(digest, bytes)) {
|
|
119
|
+
throw new Error(`REFUSE·release ${fieldOf(line, 'scope')} · blob ${named.slice(0, 12)} hashes to ${digestOf(bytes).slice(7, 19)} · not laid`);
|
|
120
|
+
}
|
|
121
|
+
if (bytes !== undefined && !dir && observeText(at) === undefined)
|
|
122
|
+
keepSnapshot(store, digest, bytes);
|
|
123
|
+
const current = observeText(at);
|
|
124
|
+
if (bytes !== undefined && current !== undefined && !dir && !sameSnapshot(snapshotOf(current), members(bytes).map((one) => [one.name, digestOf(one.bytes)])))
|
|
125
|
+
throw new Error(`REFUSE·release snapshot for ${digest} omits or changes archive members; explicit rebuild required`);
|
|
126
|
+
if (bytes === undefined && !dir && current === undefined)
|
|
127
|
+
throw new Error(`REFUSE·release ${fieldOf(line, 'scope')} has no available archive or snapshot`);
|
|
128
|
+
const needs = fieldOf(line, 'needs');
|
|
129
|
+
if (!needs)
|
|
130
|
+
continue;
|
|
131
|
+
const place = join(own, archiveCoordinate(needs));
|
|
132
|
+
const link = lstatSync(place, { throwIfNoEntry: false });
|
|
133
|
+
if (link?.isSymbolicLink() && !realpathSync(place).includes(`${sep}cas${sep}laid${sep}`))
|
|
134
|
+
continue;
|
|
135
|
+
if (dir) {
|
|
136
|
+
const files = bytes === undefined ? undefined : members(bytes).map((one) => [one.name, digestOf(one.bytes)]);
|
|
137
|
+
if (!lstatSync(place, { throwIfNoEntry: false })?.isSymbolicLink() && observeText(join(place, laidMark)) === digest && files !== undefined && layoutMatches(place, files))
|
|
138
|
+
continue;
|
|
139
|
+
const next = nextPlace(place);
|
|
140
|
+
try {
|
|
141
|
+
layDir(at, next);
|
|
142
|
+
swapPlace(place, next, digest);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
rmSync(next, { recursive: true, force: true });
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
laid += 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const snap = observeText(at);
|
|
152
|
+
if (snap === undefined)
|
|
153
|
+
throw new Error(`REFUSE·release ${fieldOf(line, 'scope')} has no available snapshot`);
|
|
154
|
+
if (!lstatSync(place, { throwIfNoEntry: false })?.isSymbolicLink() && observeText(join(place, laidMark)) === digest && layoutMatches(place, snapshotOf(snap)))
|
|
155
|
+
continue;
|
|
156
|
+
const next = nextPlace(place);
|
|
157
|
+
try {
|
|
158
|
+
layFiles(store, contentStore, snapshotOf(snap), next);
|
|
159
|
+
swapPlace(place, next, digest);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
rmSync(next, { recursive: true, force: true });
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
laid += 1;
|
|
166
|
+
}
|
|
167
|
+
return laid;
|
|
168
|
+
}
|
|
169
|
+
export const resolvedOf = (store, own, lock, contentStore = own) => releasesOf(lock).flatMap((line) => {
|
|
170
|
+
const digest = fieldOf(line, 'value');
|
|
171
|
+
const bytes = observeFile(blobAt(contentStore, digest));
|
|
172
|
+
const ok = bytes !== undefined && hashed(digest, bytes);
|
|
173
|
+
const needs = fieldOf(line, 'needs');
|
|
174
|
+
const place = needs ? join(own, archiveCoordinate(needs)) : '';
|
|
175
|
+
const at = laidAt(store, digest);
|
|
176
|
+
const laid = needs
|
|
177
|
+
? observeText(join(place, laidMark)) === digest || (existsSync(place) && existsSync(at) && realpathSync(place) === realpathSync(at))
|
|
178
|
+
: existsSync(at);
|
|
179
|
+
const expected = ok ? members(bytes).map((one) => [one.name, digestOf(one.bytes)]) : undefined;
|
|
180
|
+
const historicalDirectory = lstatSync(at, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
181
|
+
const snapshot = observeText(at);
|
|
182
|
+
const complete = expected !== undefined && (needs
|
|
183
|
+
? layoutMatches(place, expected)
|
|
184
|
+
: historicalDirectory ? layoutMatches(at, expected) : snapshot !== undefined && sameSnapshot(snapshotOf(snapshot), expected));
|
|
185
|
+
return ok && laid && complete ? [canonical({ scope: `resolved/${fieldOf(line, 'scope').slice('dep/'.length)}`, role: 'writes', form: 'alphabet', measure: 'digest', value: digest, at: `place:${digest}`, by: 'target' })] : [];
|
|
186
|
+
});
|
|
187
|
+
/** A host loads verified release bytes from a runtime tree, never interprets a snapshot file as a directory. */
|
|
188
|
+
export function runtimeTreeAt(store, own, digest, contentStore = own) {
|
|
189
|
+
const named = /^([a-zA-Z0-9-]+):([a-fA-F0-9]+)$/.exec(digest);
|
|
190
|
+
if (named === null)
|
|
191
|
+
throw new Error(`REFUSE·release invalid digest ${digest}`);
|
|
192
|
+
const bytes = observeFile(blobAt(contentStore, digest));
|
|
193
|
+
if (bytes === undefined)
|
|
194
|
+
return undefined;
|
|
195
|
+
if (!hashed(digest, bytes))
|
|
196
|
+
throw new Error(`REFUSE·release ${digest} does not name its available bytes`);
|
|
197
|
+
const files = members(bytes).map((one) => [one.name, digestOf(one.bytes)]);
|
|
198
|
+
const place = storeAt(store, 'cas', 'runtime', named[1], named[2]);
|
|
199
|
+
if (observeText(join(place, laidMark)) === digest && layoutMatches(place, files))
|
|
200
|
+
return place;
|
|
201
|
+
for (const member of members(bytes))
|
|
202
|
+
landBlob(store, digestOf(member.bytes), member.bytes);
|
|
203
|
+
const next = nextPlace(place);
|
|
204
|
+
try {
|
|
205
|
+
layFiles(store, own, files, next);
|
|
206
|
+
swapPlace(place, next, digest);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
rmSync(next, { recursive: true, force: true });
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
return place;
|
|
213
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { capsuleAt } from './capsule.ts';
|
|
2
|
+
import type { Capsule } from './capsule.ts';
|
|
3
|
+
import type { ToposResolver } from './selected-topos.ts';
|
|
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 {
|
|
6
|
+
readonly capsule: Capsule;
|
|
7
|
+
readonly offer: string;
|
|
8
|
+
}[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { capsuleAt } from "./capsule.js";
|
|
2
|
+
import { capsuleOffers } from '@lapxo/topos/standing';
|
|
3
|
+
import { selectedTopoi, toposAt } from "./selected-topos.js";
|
|
4
|
+
import { canonical } from '@lapxo/topos/wire';
|
|
5
|
+
import { fieldOf } from "../fold/claims.js";
|
|
6
|
+
import { ownLock } from "../observe/runner.js";
|
|
7
|
+
/** 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
|
+
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'))
|
|
12
|
+
throw Error(`REFUSE·pin ${digest} capsule ${offer.value} needs one host projection`);
|
|
13
|
+
const projected = { ...offer, shape: fieldOf(projections[0], 'shape') };
|
|
14
|
+
const capsule = resolve(offer.value, projected.shape);
|
|
15
|
+
if (capsule === undefined)
|
|
16
|
+
throw Error(`REFUSE·pin ${digest} · ${offer.scope} · offered capsule ${offer.value} unavailable`);
|
|
17
|
+
return { capsule, offer: canonical(projected) };
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StandingTopos } from '@lapxo/topos/standing';
|
|
2
|
+
export interface SelectedTopos {
|
|
3
|
+
readonly digest: string;
|
|
4
|
+
readonly topos: StandingTopos;
|
|
5
|
+
}
|
|
6
|
+
export type ToposResolver = (digest: string) => StandingTopos;
|
|
7
|
+
/** Resolve content using the instrument wire's admitted digest algorithms. No transport interpretation. */
|
|
8
|
+
export declare function toposAt(digest: string): StandingTopos;
|
|
9
|
+
export declare function contentAt(digest: string): Uint8Array;
|
|
10
|
+
/** A declared JS context offer uses this host adapter only after selection; execution is not selection. */
|
|
11
|
+
export declare function contextModuleAt(digest: string): string;
|
|
12
|
+
/** Selection verifies the standing and its directly declared closure. It does not request any capability. */
|
|
13
|
+
export declare function selectedTopoi(pins: readonly string[], resolve?: ToposResolver): readonly SelectedTopos[];
|
|
14
|
+
/** Read a declared wire list without copying a world's claims or executing its offers.
|
|
15
|
+
* Independent declarations must agree; selection order cannot choose a contract.
|
|
16
|
+
*/
|
|
17
|
+
export declare function selectedWireList(local: readonly string[], epoch: number, pins: readonly string[], list: string, resolve?: ToposResolver): readonly string[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readStanding } from '@lapxo/topos/standing';
|
|
3
|
+
import { parse, wireAt } from '@lapxo/topos/wire';
|
|
4
|
+
import { fieldOf } from "../fold/claims.js";
|
|
5
|
+
import { blobAt } from "../land/ledger.js";
|
|
6
|
+
import { observeFile } from "../observe/files.js";
|
|
7
|
+
import { ownStore, ownLock } from "../observe/runner.js";
|
|
8
|
+
import { replaceWhole } from "../land/ledger.js";
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
/** Resolve content using the instrument wire's admitted digest algorithms. No transport interpretation. */
|
|
11
|
+
export function toposAt(digest) {
|
|
12
|
+
const topos = readStanding(Buffer.from(contentAt(digest)).toString('utf8'));
|
|
13
|
+
for (const dependency of topos.dependencies)
|
|
14
|
+
contentAt(dependency);
|
|
15
|
+
return topos;
|
|
16
|
+
}
|
|
17
|
+
export function contentAt(digest) {
|
|
18
|
+
const fields = ownLock().flatMap(line => { const got = parse(line); return got.kind === 'fact' ? [got.value.fields] : []; });
|
|
19
|
+
const wire = wireAt(fields, Number.MAX_SAFE_INTEGER);
|
|
20
|
+
if (wire === null)
|
|
21
|
+
throw Error('REFUSE·pin instrument has no admitted wire');
|
|
22
|
+
const algorithms = wire.digests;
|
|
23
|
+
const bytesAt = (name) => {
|
|
24
|
+
const split = name.indexOf(':');
|
|
25
|
+
const algorithm = name.slice(0, split), hex = name.slice(split + 1);
|
|
26
|
+
if (split < 1 || !algorithms.has(algorithm) || !/^[a-f0-9]+$/.test(hex))
|
|
27
|
+
throw Error(`REFUSE·pin ${name} digest is not admitted`);
|
|
28
|
+
const bytes = observeFile(blobAt(ownStore(), name));
|
|
29
|
+
if (bytes === undefined)
|
|
30
|
+
throw Error(`REFUSE·pin ${name} content unavailable · not laid`);
|
|
31
|
+
if (createHash(algorithm).update(bytes).digest('hex') !== hex)
|
|
32
|
+
throw Error(`REFUSE·pin ${name} content hash mismatch · not laid`);
|
|
33
|
+
return bytes;
|
|
34
|
+
};
|
|
35
|
+
return bytesAt(digest);
|
|
36
|
+
}
|
|
37
|
+
/** A declared JS context offer uses this host adapter only after selection; execution is not selection. */
|
|
38
|
+
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;
|
|
43
|
+
}
|
|
44
|
+
/** Selection verifies the standing and its directly declared closure. It does not request any capability. */
|
|
45
|
+
export function selectedTopoi(pins, resolve = toposAt) {
|
|
46
|
+
return [...new Set(pins.map(pin => fieldOf(pin, 'value')))].map(digest => ({ digest, topos: resolve(digest) }));
|
|
47
|
+
}
|
|
48
|
+
/** Read a declared wire list without copying a world's claims or executing its offers.
|
|
49
|
+
* Independent declarations must agree; selection order cannot choose a contract.
|
|
50
|
+
*/
|
|
51
|
+
export function selectedWireList(local, epoch, pins, list, resolve = toposAt) {
|
|
52
|
+
const records = local.flatMap(line => { const got = parse(line); return got.kind === 'fact' ? [got.value.fields] : []; });
|
|
53
|
+
const contracts = [{ name: 'place', wire: wireAt(records, epoch) }, ...selectedTopoi(pins, resolve).map(({ digest, topos }) => ({ name: digest, wire: wireAt(topos.parts, Number.MAX_SAFE_INTEGER) }))]
|
|
54
|
+
.flatMap(({ name, wire }) => { const words = wire?.lists.get(list); return words === undefined ? [] : [{ name, words: [...words] }]; });
|
|
55
|
+
if (!contracts.length)
|
|
56
|
+
throw Error(`REFUSE·wire wire/${list} has no declared contract in the place or its selected topoi`);
|
|
57
|
+
const first = contracts[0];
|
|
58
|
+
if (contracts.some(({ words }) => words.length !== first.words.length || words.some(word => !first.words.includes(word))))
|
|
59
|
+
throw Error(`REFUSE·wire wire/${list} has conflicting contracts · ${contracts.map(one => one.name).join(' · ')}`);
|
|
60
|
+
return first.words;
|
|
61
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ContentPort {
|
|
2
|
+
readonly algorithms: ReadonlySet<string>;
|
|
3
|
+
readonly read: (digest: string) => Uint8Array | undefined;
|
|
4
|
+
readonly write: (digest: string, bytes: Uint8Array) => void;
|
|
5
|
+
readonly fetch: (url: string) => Promise<Uint8Array>;
|
|
6
|
+
}
|
|
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[]>;
|
|
9
|
+
/** Exact resource URLs; no repository checkout or inferred release. */
|
|
10
|
+
export declare function resolveSources(standing: readonly string[]): Promise<void>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readStanding } from '@lapxo/topos/standing';
|
|
3
|
+
import { parse, wireAt } from '@lapxo/topos/wire';
|
|
4
|
+
import { blobAt, landBlob } from "../land/ledger.js";
|
|
5
|
+
import { observeFile } from "../observe/files.js";
|
|
6
|
+
import { ownLock, ownStore } from "../observe/runner.js";
|
|
7
|
+
/** Transport uses declared locations; only the requested digest admits bytes into the cache. */
|
|
8
|
+
export async function resolveSelectedContent(lines, port) {
|
|
9
|
+
const fields = lines.flatMap(line => { const p = parse(line); return p.kind === 'fact' && p.value.fields.value !== 'withdraw' ? [p.value.fields] : []; });
|
|
10
|
+
const selectionIndex = (scope) => { const match = /(^|\/)uses\//.exec(scope); return match === null ? -1 : match.index + match[1].length; };
|
|
11
|
+
const bindings = fields.filter(f => selectionIndex(f.scope ?? '') >= 0 || f.scope?.startsWith('dep/'));
|
|
12
|
+
const locations = new Map();
|
|
13
|
+
for (const binding of bindings) {
|
|
14
|
+
const scope = binding.scope ?? '';
|
|
15
|
+
const index = selectionIndex(scope);
|
|
16
|
+
const sourceScope = index >= 0 ? scope.slice(0, index) + 'sources/' + scope.slice(index + 5) : 'sources/' + scope.slice(4);
|
|
17
|
+
const urls = fields.filter(f => f.scope === sourceScope).map(f => f.value ?? '');
|
|
18
|
+
if (urls.length)
|
|
19
|
+
locations.set(binding.value ?? '', [...new Set([...(locations.get(binding.value ?? '') ?? []), ...urls])]);
|
|
20
|
+
}
|
|
21
|
+
const roots = [...new Set(bindings.filter(f => selectionIndex(f.scope ?? '') >= 0).map(f => f.value ?? ''))];
|
|
22
|
+
const received = [];
|
|
23
|
+
const validate = (digest) => {
|
|
24
|
+
const split = digest.indexOf(':');
|
|
25
|
+
const algorithm = digest.slice(0, split), hex = digest.slice(split + 1);
|
|
26
|
+
if (split < 1 || !port.algorithms.has(algorithm) || !/^[a-f0-9]+$/.test(hex))
|
|
27
|
+
throw Error(`REFUSE·pin ${digest} digest is not admitted`);
|
|
28
|
+
return { algorithm, hex };
|
|
29
|
+
};
|
|
30
|
+
// Refuse malformed declared selections before materializing any dependency.
|
|
31
|
+
for (const binding of bindings)
|
|
32
|
+
validate(binding.value ?? '');
|
|
33
|
+
const ensure = async (digest) => {
|
|
34
|
+
const { algorithm, hex } = validate(digest);
|
|
35
|
+
const verify = (bytes) => {
|
|
36
|
+
if (createHash(algorithm).update(bytes).digest('hex') !== hex)
|
|
37
|
+
throw Error(`REFUSE·pin ${digest} content hash mismatch · not laid`);
|
|
38
|
+
return bytes;
|
|
39
|
+
};
|
|
40
|
+
const cached = port.read(digest);
|
|
41
|
+
if (cached !== undefined)
|
|
42
|
+
return verify(cached);
|
|
43
|
+
const urls = locations.get(digest) ?? [];
|
|
44
|
+
if (!urls.length)
|
|
45
|
+
throw Error(`REFUSE·pin ${digest} content unavailable · no declared source · not laid`);
|
|
46
|
+
let last;
|
|
47
|
+
for (const url of urls) {
|
|
48
|
+
const parsed = new URL(url);
|
|
49
|
+
if (!['https:', 'http:'].includes(parsed.protocol))
|
|
50
|
+
throw Error(`REFUSE·source ${url} needs an explicit transport adapter`);
|
|
51
|
+
let bytes;
|
|
52
|
+
try {
|
|
53
|
+
bytes = await port.fetch(url);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
last = error;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
verify(bytes);
|
|
60
|
+
port.write(digest, bytes);
|
|
61
|
+
received.push(digest);
|
|
62
|
+
return bytes;
|
|
63
|
+
}
|
|
64
|
+
throw Error(`REFUSE·source ${digest} ${last instanceof Error ? last.message : 'declared sources unavailable'}`);
|
|
65
|
+
};
|
|
66
|
+
for (const digest of new Set(bindings.filter(f => f.scope?.startsWith('dep/') && f.role === 'reads').map(f => f.value ?? '')))
|
|
67
|
+
await ensure(digest);
|
|
68
|
+
for (const digest of roots) {
|
|
69
|
+
const topos = readStanding(Buffer.from(await ensure(digest)).toString('utf8'));
|
|
70
|
+
for (const dependency of topos.dependencies)
|
|
71
|
+
await ensure(dependency);
|
|
72
|
+
}
|
|
73
|
+
return received;
|
|
74
|
+
}
|
|
75
|
+
/** Exact resource URLs; no repository checkout or inferred release. */
|
|
76
|
+
export async function resolveSources(standing) {
|
|
77
|
+
const instrument = ownLock().flatMap(line => { const p = parse(line); return p.kind === 'fact' ? [p.value.fields] : []; });
|
|
78
|
+
const wire = wireAt(instrument, Number.MAX_SAFE_INTEGER);
|
|
79
|
+
if (wire === null)
|
|
80
|
+
throw Error('REFUSE·pin instrument has no admitted wire');
|
|
81
|
+
const received = await resolveSelectedContent([...standing, ...ownLock().filter(line => { const p = parse(line); return p.kind === 'fact' && (p.value.fields.scope?.startsWith('dep/') || p.value.fields.scope?.startsWith('sources/')); })], {
|
|
82
|
+
algorithms: wire.digests,
|
|
83
|
+
read: digest => observeFile(blobAt(ownStore(), digest)),
|
|
84
|
+
write: (digest, bytes) => { landBlob(ownStore(), digest, bytes); },
|
|
85
|
+
fetch: async (url) => {
|
|
86
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(30000) });
|
|
87
|
+
if (!response.ok)
|
|
88
|
+
throw Error(`HTTP ${response.status}`);
|
|
89
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
for (const digest of received)
|
|
93
|
+
process.stdout.write(`FETCHED ${digest} · verified\n`);
|
|
94
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A demand that landed after its coordinates. A take judges what moved, because it assumes the demand came first and the
|
|
3
|
+
* tree after it; a demand that arrives second has nothing left to move — its cone is already in the ledger, every coordinate
|
|
4
|
+
* of it vouched at a digest. Judging that is reading, not observing, so it is answered where it lands and no file is
|
|
5
|
+
* touched to force it. A demand whose cone holds no vouched coordinate is not late, it is unlanded, and it waits for a take.
|
|
6
|
+
*/
|
|
7
|
+
export declare function lateFacts(store: string, signed: readonly string[], answered: ReadonlySet<string>): readonly string[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { canonical } from '@lapxo/topos/wire';
|
|
2
|
+
import { fieldOf } from "../fold/claims.js";
|
|
3
|
+
import { fullDigest } from "../fold/digests.js";
|
|
4
|
+
import { inside, locationsOf, vouchedCoordinates } from "../land/vouched.js";
|
|
5
|
+
import { wordOf } from "../fold/wire.js";
|
|
6
|
+
import { holdsCases } from "../fold/paid.js";
|
|
7
|
+
/**
|
|
8
|
+
* A demand that landed after its coordinates. A take judges what moved, because it assumes the demand came first and the
|
|
9
|
+
* tree after it; a demand that arrives second has nothing left to move — its cone is already in the ledger, every coordinate
|
|
10
|
+
* of it vouched at a digest. Judging that is reading, not observing, so it is answered where it lands and no file is
|
|
11
|
+
* touched to force it. A demand whose cone holds no vouched coordinate is not late, it is unlanded, and it waits for a take.
|
|
12
|
+
*/
|
|
13
|
+
export function lateFacts(store, signed, answered) {
|
|
14
|
+
const vouched = [...vouchedCoordinates(store)];
|
|
15
|
+
const views = `${wordOf(signed, 'families', 'view')}/`;
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const line of signed) {
|
|
18
|
+
const scope = fieldOf(line, 'scope');
|
|
19
|
+
const value = fieldOf(line, 'value') || 'present';
|
|
20
|
+
if (fieldOf(line, 'role') !== 'demands' || value === 'withdraw' || answered.has(scope))
|
|
21
|
+
continue;
|
|
22
|
+
const asked = locationsOf(fieldOf(line, 'needs')).locations;
|
|
23
|
+
if (!asked.length || (!scope.startsWith(views) && !asked.some(holdsCases)))
|
|
24
|
+
continue;
|
|
25
|
+
const cone = asked.map((where) => vouched.filter(([coordinate]) => inside(coordinate, where)));
|
|
26
|
+
if (cone.some((held) => !held.length))
|
|
27
|
+
continue;
|
|
28
|
+
const held = cone.flat();
|
|
29
|
+
const at = `cone:${fullDigest(store, held.map(([coordinate, digest]) => `${coordinate} ${digest}`).sort()).replace(/^sha256:/, '')}`;
|
|
30
|
+
out.push(canonical({ scope, role: 'writes', form: 'alphabet', measure: fieldOf(line, 'measure') || 'status', value, by: 'judge', at }), canonical({ scope, role: 'writes', form: 'alphabet', measure: 'cone', value: held.map(([coordinate]) => coordinate).join('|'), by: 'judge', at }));
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface Gate {
|
|
2
|
+
readonly kind: string;
|
|
3
|
+
readonly status: 'green' | 'red';
|
|
4
|
+
readonly ms: number;
|
|
5
|
+
readonly at: string;
|
|
6
|
+
readonly admitted: boolean;
|
|
7
|
+
readonly why: string;
|
|
8
|
+
}
|
|
9
|
+
interface Effect {
|
|
10
|
+
readonly kind: string;
|
|
11
|
+
readonly status: 'green' | 'red';
|
|
12
|
+
readonly ms: number;
|
|
13
|
+
readonly at: string;
|
|
14
|
+
readonly admitted: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** E: the demand, its cone, the coordinates before and after by digest, and the effects that ran over the cone; an effect the ledger has never seen green answers for nothing yet: it runs, it lands, and it refuses nothing. */
|
|
17
|
+
interface Envelope {
|
|
18
|
+
readonly scope: string;
|
|
19
|
+
readonly cone: readonly string[];
|
|
20
|
+
readonly before: Readonly<Record<string, string>>;
|
|
21
|
+
readonly after: Readonly<Record<string, string>>;
|
|
22
|
+
readonly effects: readonly Effect[];
|
|
23
|
+
}
|
|
24
|
+
type Verdict = {
|
|
25
|
+
readonly kind: 'fact';
|
|
26
|
+
readonly why: string;
|
|
27
|
+
readonly envelope: Envelope;
|
|
28
|
+
readonly digest: string;
|
|
29
|
+
readonly changed: readonly string[];
|
|
30
|
+
} | {
|
|
31
|
+
readonly kind: 'refuse';
|
|
32
|
+
readonly why: string;
|
|
33
|
+
readonly envelope: Envelope;
|
|
34
|
+
readonly digest: string;
|
|
35
|
+
readonly changed: readonly string[];
|
|
36
|
+
};
|
|
37
|
+
export declare function judgeTake(input: {
|
|
38
|
+
readonly root: string;
|
|
39
|
+
readonly scope: string;
|
|
40
|
+
readonly value: string;
|
|
41
|
+
readonly locations: readonly string[];
|
|
42
|
+
readonly before: ReadonlyMap<string, string>;
|
|
43
|
+
readonly after: ReadonlyMap<string, string>;
|
|
44
|
+
readonly law?: readonly string[];
|
|
45
|
+
readonly template: string;
|
|
46
|
+
readonly shape?: string;
|
|
47
|
+
readonly restsOn?: string;
|
|
48
|
+
readonly witnessOf?: (coordinate: string) => string | undefined;
|
|
49
|
+
}): Verdict & {
|
|
50
|
+
readonly notOwned: readonly string[];
|
|
51
|
+
};
|
|
52
|
+
/** A take that met its cone carries the gates its batch passed: their runs join its envelope, and its receipt is sealed with them. */
|
|
53
|
+
export declare function sealed<V extends {
|
|
54
|
+
readonly envelope: Envelope;
|
|
55
|
+
readonly digest: string;
|
|
56
|
+
}>(root: string, verdict: V, gates: readonly Gate[]): V;
|
|
57
|
+
export {};
|