@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,148 @@
|
|
|
1
|
+
import { appendFileSync, closeSync, dirname, existsSync, join, linkSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync } from "../host/io.js";
|
|
2
|
+
import { storeRoot } from "../host/ports/store.js";
|
|
3
|
+
import { canonical, EXTENSION, PROTOCOL } from '@lapxo/topos/wire';
|
|
4
|
+
import { fieldOf } from "../fold/claims.js";
|
|
5
|
+
import { observeText } from "../observe/files.js";
|
|
6
|
+
export function storeOf(root) {
|
|
7
|
+
return join(storeRoot(root), EXTENSION);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The store of a place: every name under it is read here and nowhere else, whoever needs a place asks for it by its
|
|
11
|
+
* steps, and one append-only file per writer stands beside the bytes kept by digest — one per region for a writer
|
|
12
|
+
* whose lines are about regions, so a fold opens only the regions it reaches. A file is read once per stamp, and a
|
|
13
|
+
* ledger only grows: what it held is kept, and only the bytes appended since, from where it last ended a line, are read.
|
|
14
|
+
*/
|
|
15
|
+
export function storeAt(store, ...steps) {
|
|
16
|
+
return join(store, ...steps);
|
|
17
|
+
}
|
|
18
|
+
export function writerFile(store, writer) {
|
|
19
|
+
return storeAt(store, 'ledger', `${writer.replace(/[^a-zA-Z0-9_-]/g, '')}${EXTENSION}`);
|
|
20
|
+
}
|
|
21
|
+
const held = new Map();
|
|
22
|
+
export function ledgerLines(store, writer) {
|
|
23
|
+
return linesAt(writerFile(store, writer));
|
|
24
|
+
}
|
|
25
|
+
export function shardFile(store, writer, region) {
|
|
26
|
+
return storeAt(store, 'ledger', writer.replace(/[^a-zA-Z0-9_-]/g, ''), `${region.replace(/\/+$/, '') || '@'}${EXTENSION}`);
|
|
27
|
+
}
|
|
28
|
+
export const shardLines = (store, writer, region) => linesAt(shardFile(store, writer, region));
|
|
29
|
+
export function landShard(store, writer, region, lines) {
|
|
30
|
+
return appendNew(shardFile(store, writer, region), lines);
|
|
31
|
+
}
|
|
32
|
+
function linesAt(at) {
|
|
33
|
+
const st = statSync(at, { throwIfNoEntry: false });
|
|
34
|
+
if (!st)
|
|
35
|
+
return [];
|
|
36
|
+
const stamp = `${Math.floor(st.mtimeMs)}-${st.size}`;
|
|
37
|
+
const mine = held.get(at);
|
|
38
|
+
if (mine?.stamp === stamp)
|
|
39
|
+
return mine.lines;
|
|
40
|
+
const grown = mine !== undefined && mine.size > 0 && st.size > mine.size ? tailOf(at, mine.size, st.size) : undefined;
|
|
41
|
+
const lines = grown !== undefined && mine !== undefined ? [...mine.lines, ...grown.split('\n').filter((line) => line.startsWith(PROTOCOL))] : readFileSync(at, 'utf8').split('\n').filter((line) => line.startsWith(PROTOCOL));
|
|
42
|
+
held.set(at, { stamp, size: st.size, lines });
|
|
43
|
+
return lines;
|
|
44
|
+
}
|
|
45
|
+
function tailOf(at, from, to) {
|
|
46
|
+
const fd = openSync(at, 'r');
|
|
47
|
+
try {
|
|
48
|
+
const bytes = Buffer.alloc(to - from + 1);
|
|
49
|
+
readSync(fd, bytes, 0, bytes.length, from - 1);
|
|
50
|
+
return bytes[0] === 0x0a ? bytes.subarray(1).toString('utf8') : undefined;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
closeSync(fd);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function appendNew(at, lines) {
|
|
57
|
+
const had = new Set(linesAt(at));
|
|
58
|
+
const fresh = [...new Set(lines)].filter((line) => line.startsWith(PROTOCOL) && !had.has(line));
|
|
59
|
+
if (fresh.length) {
|
|
60
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
61
|
+
appendFileSync(at, `${fresh.join('\n')}\n`);
|
|
62
|
+
}
|
|
63
|
+
return fresh.length;
|
|
64
|
+
}
|
|
65
|
+
export function signedLines(store, writer) {
|
|
66
|
+
return ledgerLines(store, writer).filter((line) => line.includes(`by=${writer}`) && /\bsig=/.test(line));
|
|
67
|
+
}
|
|
68
|
+
export function blobAt(store, digest) {
|
|
69
|
+
return storeAt(store, 'cas', 'blobs', digest.slice(digest.indexOf(':') + 1));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Where the bytes of one digest are kept: naming a blob is not reading it. A file named by the digest of its bytes lands
|
|
73
|
+
* whole, once, whoever lands it: every writer writes the same bytes, so a
|
|
74
|
+
* rename over one already there changes nothing, and a reader never meets half of it.
|
|
75
|
+
*/
|
|
76
|
+
function landWhole(at, bytes) {
|
|
77
|
+
if (existsSync(at))
|
|
78
|
+
return at;
|
|
79
|
+
replaceWhole(at, bytes);
|
|
80
|
+
return at;
|
|
81
|
+
}
|
|
82
|
+
/** 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
|
+
export function replaceWhole(at, bytes) {
|
|
84
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
85
|
+
const part = `${at}.${process.pid}.part`;
|
|
86
|
+
writeFileSync(part, bytes);
|
|
87
|
+
renameSync(part, at);
|
|
88
|
+
}
|
|
89
|
+
export const landBlob = (store, digest, bytes) => {
|
|
90
|
+
const at = landWhole(blobAt(store, digest), bytes);
|
|
91
|
+
holdStore(store, 'blobs', digest, bytes.byteLength);
|
|
92
|
+
return at;
|
|
93
|
+
};
|
|
94
|
+
export function holdStore(store, kind, name, bytes) {
|
|
95
|
+
const hex = name.replace(/^[^:]*:/, '');
|
|
96
|
+
const scope = `cas/${kind}/${hex}`;
|
|
97
|
+
const held = ledgerLines(store, 'store').filter((line) => fieldOf(line, 'scope') === scope);
|
|
98
|
+
if (held.length && fieldOf(held[held.length - 1], 'value') !== 'withdraw')
|
|
99
|
+
return;
|
|
100
|
+
land(store, 'store', [canonical({
|
|
101
|
+
scope, role: 'writes', form: 'interval', measure: 'bytes',
|
|
102
|
+
value: `${bytes}..${bytes}`, at: `place:${kind === 'answers' ? hex : `sha256:${hex}`}`, by: 'store',
|
|
103
|
+
})]);
|
|
104
|
+
}
|
|
105
|
+
export const memoAt = (store, key) => storeAt(store, 'cas', 'memo', ...key.split('/').map((step) => step.replace(/[^a-zA-Z0-9._@-]/g, '_')));
|
|
106
|
+
export function landMemo(store, key, bytes) {
|
|
107
|
+
const at = memoAt(store, key);
|
|
108
|
+
if (existsSync(at))
|
|
109
|
+
return at;
|
|
110
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
111
|
+
const part = `${at}.${process.pid}.part`;
|
|
112
|
+
writeFileSync(part, bytes);
|
|
113
|
+
try {
|
|
114
|
+
linkSync(part, at);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error.code !== 'EEXIST')
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
rmSync(part, { force: true });
|
|
122
|
+
}
|
|
123
|
+
return at;
|
|
124
|
+
}
|
|
125
|
+
export const landEnvelope = (store, digest, bytes) => landWhole(storeAt(store, 'cas', 'envelopes', `${digest.slice(digest.indexOf(':') + 1)}.json`), bytes);
|
|
126
|
+
export function land(store, writer, lines) {
|
|
127
|
+
const at = writerFile(store, writer);
|
|
128
|
+
return { at, appended: appendNew(at, lines) };
|
|
129
|
+
}
|
|
130
|
+
const step = (value) => value.replace(/[^a-zA-Z0-9._@-]/g, '_');
|
|
131
|
+
function verdictAt(store, tree, lot) {
|
|
132
|
+
return memoAt(store, `verdict/${step(tree)}/${step(lot)}`);
|
|
133
|
+
}
|
|
134
|
+
export function keptVerdict(store, tree, lot) {
|
|
135
|
+
const text = observeText(verdictAt(store, tree, lot));
|
|
136
|
+
if (text === undefined)
|
|
137
|
+
return undefined;
|
|
138
|
+
try {
|
|
139
|
+
const got = JSON.parse(text);
|
|
140
|
+
return Array.isArray(got) ? got : undefined;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export function keepVerdict(store, tree, lot, judged) {
|
|
147
|
+
landMemo(store, `verdict/${step(tree)}/${step(lot)}`, new TextEncoder().encode(JSON.stringify(judged)));
|
|
148
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type Owned = {
|
|
2
|
+
readonly ledger: readonly string[];
|
|
3
|
+
readonly own: ReadonlyMap<string, readonly string[]>;
|
|
4
|
+
readonly nowhere: readonly string[];
|
|
5
|
+
readonly forked: readonly {
|
|
6
|
+
readonly place: string;
|
|
7
|
+
readonly key: string;
|
|
8
|
+
}[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* A lot named with --place lands in those own locks and not the ledger: a lot never mixes. A line the ledger does not admit is a place's own when that place's own lock admits it: signed by a key the tree admits,
|
|
12
|
+
* inside the window it held, inside its coverage of the place, the line's scope read as a region of the place. An own
|
|
13
|
+
* lock speaks of its place without naming it, so a line whose scope names a place first is the ledger's alone, and the
|
|
14
|
+
* same line can stand in many own locks: named places take every line they admit, and with none named a line lands in
|
|
15
|
+
* the one own lock that admits it. The lines are told to their locks for the gates and land in them signed, beside what
|
|
16
|
+
* the ledger takes. A line no lock admits, or one two locks would take and no place names, is admitted nowhere, and a
|
|
17
|
+
* batch that holds one is refused whole: nothing is ever written into a lock bare. An own lot never forks a lock: lines
|
|
18
|
+
* that would leave a place's lock with two standing claims where it had one are refused whole, so a lot for many places
|
|
19
|
+
* lands the same lines at each, and lines meant for one place go in a lot of their own.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ownedOf(root: string, signed: readonly string[], admits: (line: string) => boolean, coverage: (by: string) => readonly string[], named?: readonly string[], placedAt?: (line: string) => readonly string[] | undefined): Owned;
|
|
22
|
+
/** A place's own lock gains the lines a batch landed there, appended whole: a reader never meets half of it, and every line already there keeps its bytes as they were. */
|
|
23
|
+
export declare function landOwned(root: string, place: string, lines: readonly string[]): void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { join, renameSync, writeFileSync } from "../host/io.js";
|
|
2
|
+
import { fieldOf, LOCK } from "../fold/claims.js";
|
|
3
|
+
import { lockStanding } from "../fold/keys.js";
|
|
4
|
+
import { placesIn } from "../fold/places.js";
|
|
5
|
+
import { admittedIn, forksIn, tellOwn } from "../fold/signed.js";
|
|
6
|
+
import { wordOf } from "../fold/wire.js";
|
|
7
|
+
import { observeText } from "../observe/files.js";
|
|
8
|
+
import { landShard, storeOf } from "./ledger.js";
|
|
9
|
+
/**
|
|
10
|
+
* A lot named with --place lands in those own locks and not the ledger: a lot never mixes. A line the ledger does not admit is a place's own when that place's own lock admits it: signed by a key the tree admits,
|
|
11
|
+
* inside the window it held, inside its coverage of the place, the line's scope read as a region of the place. An own
|
|
12
|
+
* lock speaks of its place without naming it, so a line whose scope names a place first is the ledger's alone, and the
|
|
13
|
+
* same line can stand in many own locks: named places take every line they admit, and with none named a line lands in
|
|
14
|
+
* the one own lock that admits it. The lines are told to their locks for the gates and land in them signed, beside what
|
|
15
|
+
* the ledger takes. A line no lock admits, or one two locks would take and no place names, is admitted nowhere, and a
|
|
16
|
+
* batch that holds one is refused whole: nothing is ever written into a lock bare. An own lot never forks a lock: lines
|
|
17
|
+
* that would leave a place's lock with two standing claims where it had one are refused whole, so a lot for many places
|
|
18
|
+
* lands the same lines at each, and lines meant for one place go in a lot of their own.
|
|
19
|
+
*/
|
|
20
|
+
export function ownedOf(root, signed, admits, coverage, named = [], placedAt = () => undefined) {
|
|
21
|
+
const places = placesIn(root);
|
|
22
|
+
const headed = (line) => places.includes(fieldOf(line, 'scope').split('/')[0] ?? '');
|
|
23
|
+
const ledger = named.length ? [] : signed.filter(admits);
|
|
24
|
+
const rest = [...new Set(signed.filter((line) => (named.length || !admits(line)) && !headed(line)))];
|
|
25
|
+
const placed = signed.filter((line) => !ledger.includes(line) && !rest.includes(line));
|
|
26
|
+
if (!rest.length)
|
|
27
|
+
return { ledger, own: new Map(), nowhere: placed, forked: [] };
|
|
28
|
+
const family = wordOf(lockStanding(storeOf(root)), 'families', 'region');
|
|
29
|
+
const covers = new Set(rest.map((line) => fieldOf(line, 'by')).flatMap((by) => coverage(by)));
|
|
30
|
+
const namedOf = (line) => placedAt(line) ?? named;
|
|
31
|
+
const asked = [...new Set(rest.flatMap((line) => (namedOf(line).length ? namedOf(line) : places)))].filter((place) => [...covers].some((prefix) => prefix === '*' || prefix.startsWith(`${family}/${place}/`) || prefix === `${family}/${place}`));
|
|
32
|
+
const takers = new Map();
|
|
33
|
+
for (const place of asked) {
|
|
34
|
+
tellOwn(place, rest);
|
|
35
|
+
for (const line of rest)
|
|
36
|
+
if ((!namedOf(line).length || namedOf(line).includes(place)) && admittedIn(root, place, line))
|
|
37
|
+
takers.set(line, [...(takers.get(line) ?? []), place]);
|
|
38
|
+
tellOwn(place, []);
|
|
39
|
+
}
|
|
40
|
+
const lands = (line) => (namedOf(line).length ? (takers.get(line)?.length ?? 0) > 0 : takers.get(line)?.length === 1);
|
|
41
|
+
const own = new Map();
|
|
42
|
+
for (const line of rest.filter(lands))
|
|
43
|
+
for (const place of takers.get(line))
|
|
44
|
+
own.set(place, [...(own.get(place) ?? []), line]);
|
|
45
|
+
const forked = [...own].flatMap(([place, lines]) => {
|
|
46
|
+
tellOwn(place, []);
|
|
47
|
+
const before = new Set(forksIn(root, place));
|
|
48
|
+
tellOwn(place, lines);
|
|
49
|
+
return forksIn(root, place).filter((key) => !before.has(key)).map((key) => ({ place, key }));
|
|
50
|
+
});
|
|
51
|
+
return { ledger, own, nowhere: [...placed, ...rest.filter((line) => !lands(line))], forked };
|
|
52
|
+
}
|
|
53
|
+
/** A place's own lock gains the lines a batch landed there, appended whole: a reader never meets half of it, and every line already there keeps its bytes as they were. */
|
|
54
|
+
export function landOwned(root, place, lines) {
|
|
55
|
+
if (!lines.length)
|
|
56
|
+
return tellOwn(place, []);
|
|
57
|
+
const at = join(root, place, LOCK);
|
|
58
|
+
const text = observeText(at) ?? '';
|
|
59
|
+
for (const by of new Set(lines.map(line => fieldOf(line, 'by')))) {
|
|
60
|
+
landShard(storeOf(root), by, `${place}/${LOCK}`, lines.filter(line => fieldOf(line, 'by') === by));
|
|
61
|
+
}
|
|
62
|
+
writeFileSync(`${at}.${process.pid}.landing`, `${text.replace(/\n*$/, '\n')}${lines.join('\n')}\n`);
|
|
63
|
+
renameSync(`${at}.${process.pid}.landing`, at);
|
|
64
|
+
tellOwn(place, []);
|
|
65
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type Signed = {
|
|
2
|
+
readonly kind: 'fact';
|
|
3
|
+
readonly line: string;
|
|
4
|
+
} | {
|
|
5
|
+
readonly kind: 'refuse';
|
|
6
|
+
readonly why: string;
|
|
7
|
+
};
|
|
8
|
+
/** A consent line (by=target, no sig) becomes the owner's claim: stamped with its epoch, then signed over the canonical bytes. */
|
|
9
|
+
export declare function signConsentLine(line: string, privateKeyPem: string, stamp: {
|
|
10
|
+
readonly epoch?: number;
|
|
11
|
+
readonly by: string;
|
|
12
|
+
readonly algorithm: string;
|
|
13
|
+
}): Signed;
|
|
14
|
+
export declare function verifiesOwnerLine(line: string, publicKeyPem: string, admitted: readonly string[]): boolean;
|
|
15
|
+
export declare function verifiesFields(fields: Readonly<Record<string, string>>, publicKey: string, admitted: readonly string[]): boolean;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { canonical, parse, parseForSigning, signedBytes, signedFieldsOf } from '@lapxo/topos/wire';
|
|
2
|
+
import { signBytes, verifyBytes } from "../host/adapters/keys/ed25519.js";
|
|
3
|
+
/** A consent line (by=target, no sig) becomes the owner's claim: stamped with its epoch, then signed over the canonical bytes. */
|
|
4
|
+
export function signConsentLine(line, privateKeyPem, stamp) {
|
|
5
|
+
const got = parseForSigning(line);
|
|
6
|
+
if (got.kind !== 'fact')
|
|
7
|
+
return { kind: 'refuse', why: `REFUSE·accept not a claim line: ${got.why}` };
|
|
8
|
+
const fields = got.value.fields;
|
|
9
|
+
const scope = fields['scope'] ?? '?';
|
|
10
|
+
if (fields['sig'] !== undefined)
|
|
11
|
+
return { kind: 'refuse', why: `REFUSE·accept ${scope} is already signed` };
|
|
12
|
+
if (fields['by'] !== 'target') {
|
|
13
|
+
return { kind: 'refuse', why: `REFUSE·accept ${scope} is by=${fields['by'] ?? '∅'}; only by=target lines are signed` };
|
|
14
|
+
}
|
|
15
|
+
const covered = signedFieldsOf(fields, stamp.by);
|
|
16
|
+
if (stamp.epoch !== undefined)
|
|
17
|
+
covered['epoch'] = String(stamp.epoch);
|
|
18
|
+
return { kind: 'fact', line: canonical({ ...covered, sig: signBytes(signedBytes(covered), privateKeyPem, stamp.algorithm) }) };
|
|
19
|
+
}
|
|
20
|
+
export function verifiesOwnerLine(line, publicKeyPem, admitted) {
|
|
21
|
+
const got = parse(line);
|
|
22
|
+
if (got.kind !== 'fact')
|
|
23
|
+
return false;
|
|
24
|
+
const { sig, ...rest } = got.value.fields;
|
|
25
|
+
return typeof sig === 'string' && verifyBytes(signedBytes(rest), publicKeyPem, sig, admitted);
|
|
26
|
+
}
|
|
27
|
+
export function verifiesFields(fields, publicKey, admitted) {
|
|
28
|
+
const { sig, ...rest } = fields;
|
|
29
|
+
return typeof sig === 'string' && verifyBytes(signedBytes(rest), publicKey, sig, admitted);
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CoordinateRole } from '../fold/wire.ts';
|
|
2
|
+
/** A location is a file or a directory; `x/*` names the directory `x/`. A deeper glob is not a location. */
|
|
3
|
+
export declare function locationsOf(needs: string): {
|
|
4
|
+
readonly locations: readonly string[];
|
|
5
|
+
readonly refused: readonly string[];
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The source coordinates under one location, by digest. A path whose role is not source is never walked, so nothing under
|
|
9
|
+
* it is read. A link is a coordinate whose bytes are its target; it is never followed.
|
|
10
|
+
*/
|
|
11
|
+
export declare function coordinatesOf(root: string, location: string, roleOf: (coordinate: string) => CoordinateRole): Map<string, string>;
|
|
12
|
+
/** The law a witness file says it answers for: its own `witness=<law>` line, or its `"witness"` field. */
|
|
13
|
+
export declare function witnessOf(root: string, coordinate: string): string | undefined;
|
|
14
|
+
export declare const vouching: (standing: readonly string[]) => string;
|
|
15
|
+
export declare const sourced: (standing: readonly string[]) => string;
|
|
16
|
+
export declare const sourceOf: (line: string) => string | undefined;
|
|
17
|
+
export declare function inside(path: string, location: string): boolean;
|
|
18
|
+
export declare function vouchedCoordinates(store: string): Map<string, string>;
|
|
19
|
+
export declare function everLanded(store: string): ReadonlySet<string>;
|
|
20
|
+
export declare function hasVouchedTree(store: string): boolean;
|
|
21
|
+
export declare function landCoordinates(store: string, present: ReadonlyMap<string, string>, gone: readonly string[], at: string): number;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createHash, join } from "../host/io.js";
|
|
2
|
+
import { said, wordOf, wordsOf } from "../fold/wire.js";
|
|
3
|
+
import { fieldOf } from "../fold/claims.js";
|
|
4
|
+
import { point, region, within } from '@lapxo/obligations/field';
|
|
5
|
+
import { canonical } from '@lapxo/topos/wire';
|
|
6
|
+
import { land, ledgerLines } from "./ledger.js";
|
|
7
|
+
import { lockStanding } from "../fold/keys.js";
|
|
8
|
+
import { ownLock } from "../observe/runner.js";
|
|
9
|
+
import { entriesIn, linkOf, observeFile, observeText, stampOf } from "../observe/files.js";
|
|
10
|
+
/** A location is a file or a directory; `x/*` names the directory `x/`. A deeper glob is not a location. */
|
|
11
|
+
export function locationsOf(needs) {
|
|
12
|
+
const locations = [];
|
|
13
|
+
const refused = [];
|
|
14
|
+
for (const raw of needs.split('|').map((n) => n.trim()).filter(Boolean)) {
|
|
15
|
+
const steps = raw.split('/').filter(Boolean);
|
|
16
|
+
while (steps[steps.length - 1] === '*' || steps[steps.length - 1] === '**')
|
|
17
|
+
steps.pop();
|
|
18
|
+
if (!steps.length || raw.includes('#') || steps.some((step) => step.includes('*'))) {
|
|
19
|
+
refused.push(raw);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
locations.push(steps.join('/'));
|
|
23
|
+
}
|
|
24
|
+
return { locations, refused };
|
|
25
|
+
}
|
|
26
|
+
function digestOf(bytes) {
|
|
27
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The source coordinates under one location, by digest. A path whose role is not source is never walked, so nothing under
|
|
31
|
+
* it is read. A link is a coordinate whose bytes are its target; it is never followed.
|
|
32
|
+
*/
|
|
33
|
+
export function coordinatesOf(root, location, roleOf) {
|
|
34
|
+
const out = new Map();
|
|
35
|
+
const visit = (rel) => {
|
|
36
|
+
if (roleOf(rel) !== 'source')
|
|
37
|
+
return;
|
|
38
|
+
const abs = join(root, rel);
|
|
39
|
+
const st = stampOf(abs);
|
|
40
|
+
if (!st)
|
|
41
|
+
return;
|
|
42
|
+
if (st.kind === 'link') {
|
|
43
|
+
out.set(rel, digestOf(`link:${linkOf(abs)}`));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (st.kind === 'dir') {
|
|
47
|
+
for (const name of entriesIn(abs).map((entry) => entry.name).sort())
|
|
48
|
+
visit(join(rel, name));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (st.kind === 'file')
|
|
52
|
+
out.set(rel, digestOf(Buffer.from(observeFile(abs) ?? new Uint8Array())));
|
|
53
|
+
};
|
|
54
|
+
visit(location.replace(/\/$/, ''));
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/** The law a witness file says it answers for: its own `witness=<law>` line, or its `"witness"` field. */
|
|
58
|
+
export function witnessOf(root, coordinate) {
|
|
59
|
+
if (stampOf(join(root, coordinate))?.kind !== 'file')
|
|
60
|
+
return undefined;
|
|
61
|
+
const text = observeText(join(root, coordinate)) ?? '';
|
|
62
|
+
return /^witness=([^\s]+)/m.exec(text)?.[1] ?? /"witness"\s*:\s*"([^"]+)"/.exec(text)?.[1];
|
|
63
|
+
}
|
|
64
|
+
function storeFamily(standing, coordinate) {
|
|
65
|
+
const names = said(ownLock(), coordinate);
|
|
66
|
+
if (!names.length)
|
|
67
|
+
throw new Error(`REFUSE·instrument ${coordinate} has no admitted declaration in the instrument lock`);
|
|
68
|
+
return wordsOf(standing, 'families').length ? wordOf(standing, 'families', names) : names[0];
|
|
69
|
+
}
|
|
70
|
+
export const vouching = (standing) => storeFamily(standing, 'store/vouches');
|
|
71
|
+
export const sourced = (standing) => storeFamily(standing, 'store/source');
|
|
72
|
+
export const sourceOf = (line) => (fieldOf(line, 'measure') === 'digest' && fieldOf(line, 'scope').includes('/') ? fieldOf(line, 'scope').slice(fieldOf(line, 'scope').indexOf('/') + 1) : undefined);
|
|
73
|
+
export function inside(path, location) {
|
|
74
|
+
return within(point(path), region(location));
|
|
75
|
+
}
|
|
76
|
+
export function vouchedCoordinates(store) {
|
|
77
|
+
const out = new Map();
|
|
78
|
+
const standing = lockStanding(store);
|
|
79
|
+
if (!wordsOf(standing, 'states').length)
|
|
80
|
+
return out;
|
|
81
|
+
const absent = wordOf(standing, 'states', 'absent');
|
|
82
|
+
for (const line of ledgerLines(store, vouching(standing))) {
|
|
83
|
+
const path = sourceOf(line);
|
|
84
|
+
const value = fieldOf(line, 'value');
|
|
85
|
+
if (!path || !value)
|
|
86
|
+
continue;
|
|
87
|
+
if (value === absent)
|
|
88
|
+
out.delete(path);
|
|
89
|
+
else
|
|
90
|
+
out.set(path, value);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
export function everLanded(store) {
|
|
95
|
+
const out = new Set();
|
|
96
|
+
for (const line of ledgerLines(store, vouching(lockStanding(store)))) {
|
|
97
|
+
const path = sourceOf(line);
|
|
98
|
+
if (path)
|
|
99
|
+
out.add(path);
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
export function hasVouchedTree(store) {
|
|
104
|
+
return ledgerLines(store, vouching(lockStanding(store))).length > 0;
|
|
105
|
+
}
|
|
106
|
+
export function landCoordinates(store, present, gone, at) {
|
|
107
|
+
const standing = lockStanding(store);
|
|
108
|
+
const line = (path, value) => canonical({
|
|
109
|
+
scope: `${sourced(standing)}/${path}`, role: 'writes', form: 'alphabet', measure: 'digest', value, by: 'land', at,
|
|
110
|
+
});
|
|
111
|
+
return land(store, vouching(standing), [...[...present].map(([path, value]) => line(path, value)), ...gone.map((path) => line(path, wordOf(standing, 'states', 'absent')))]).appended;
|
|
112
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface Withdrawal {
|
|
2
|
+
/** The held lines the withdrawals take. */
|
|
3
|
+
readonly taken: readonly string[];
|
|
4
|
+
/** Taken lines whose scale is distributive: withdrawing them loses nothing and keeps nothing. */
|
|
5
|
+
readonly exact: readonly string[];
|
|
6
|
+
readonly inexact: readonly string[];
|
|
7
|
+
readonly open: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function retractedTogether(signed: readonly string[]): readonly {
|
|
10
|
+
readonly withdraw: string;
|
|
11
|
+
readonly line: string;
|
|
12
|
+
}[];
|
|
13
|
+
export declare function withdrawal(withdraws: readonly string[], held: readonly string[]): Withdrawal;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ENVELOPE } from '@lapxo/topos/wire';
|
|
2
|
+
import { fieldsOf } from "../fold/claims.js";
|
|
3
|
+
import { declaredConfig as declared } from '@lapxo/topos/forms';
|
|
4
|
+
/** A withdrawal takes every held line that states each field it states, its value and envelope aside. The signer's name is the envelope: a key whose coverage holds the scope takes a standing line of it, whoever signed that line. */
|
|
5
|
+
function takes(withdraw, line) {
|
|
6
|
+
const w = fieldsOf(withdraw);
|
|
7
|
+
const f = fieldsOf(line);
|
|
8
|
+
return Object.keys(w).filter((k) => !ENVELOPE.has(k) && k !== 'value').every((k) => f[k] === w[k]);
|
|
9
|
+
}
|
|
10
|
+
export function retractedTogether(signed) {
|
|
11
|
+
const withdraws = signed.filter((line) => fieldsOf(line)['value'] === 'withdraw');
|
|
12
|
+
const claims = signed.filter((line) => fieldsOf(line)['value'] !== 'withdraw');
|
|
13
|
+
return withdraws.flatMap((withdraw) => claims.filter((line) => takes(withdraw, line)).map((line) => ({ withdraw, line })));
|
|
14
|
+
}
|
|
15
|
+
export function withdrawal(withdraws, held) {
|
|
16
|
+
const taken = held.filter((line) => fieldsOf(line)['value'] !== 'withdraw' && withdraws.some((w) => takes(w, line)));
|
|
17
|
+
const scaled = taken.map((line) => ({ line, got: declared(line) }));
|
|
18
|
+
return {
|
|
19
|
+
taken,
|
|
20
|
+
exact: scaled.filter((x) => x.got.distributive === true).map((x) => x.line),
|
|
21
|
+
inexact: scaled.filter((x) => x.got.distributive === false).map((x) => x.line),
|
|
22
|
+
open: scaled.filter((x) => x.got.distributive === null).map((x) => x.line),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const observeFile: (at: string) => Uint8Array | undefined;
|
|
2
|
+
export declare const observeText: (at: string) => string | undefined;
|
|
3
|
+
export declare const exists: (at: string) => boolean;
|
|
4
|
+
export type Kind = 'file' | 'dir' | 'link' | 'other';
|
|
5
|
+
export declare function stampOf(at: string): {
|
|
6
|
+
readonly kind: Kind;
|
|
7
|
+
readonly stamp: string;
|
|
8
|
+
} | undefined;
|
|
9
|
+
export declare const linkOf: (at: string) => string;
|
|
10
|
+
export declare function entriesIn(at: string): readonly {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly dir: boolean;
|
|
13
|
+
}[];
|
|
14
|
+
export declare function coordinatesUnder(dir: string, root: string, into?: (step: string) => boolean): string[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, readlinkSync } from 'node:fs';
|
|
2
|
+
import { join, relative, sep } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* The one place the instrument reads the world: a coordinate's bytes, the entries of a directory, the coordinates under a place,
|
|
5
|
+
* a link's target and a coordinate's stamp. Every fold, render and landing asks here, so each reading is made once and named.
|
|
6
|
+
*/
|
|
7
|
+
const seen = new Map();
|
|
8
|
+
/** A coordinate read once is kept by its stamp — its inode, size and time — and read again only when the stamp moves. */
|
|
9
|
+
function bytesAt(at) {
|
|
10
|
+
const st = stampable(at);
|
|
11
|
+
if (st === undefined || st.isDirectory() || !existsSync(at))
|
|
12
|
+
return undefined;
|
|
13
|
+
const stamp = `${st.ino}-${st.size}-${st.mtimeMs}`;
|
|
14
|
+
const held = seen.get(at);
|
|
15
|
+
if (held?.stamp === stamp)
|
|
16
|
+
return held.bytes;
|
|
17
|
+
const bytes = readFileSync(at);
|
|
18
|
+
seen.set(at, { stamp, bytes });
|
|
19
|
+
return bytes;
|
|
20
|
+
}
|
|
21
|
+
export const observeFile = (at) => ((bytes) => (bytes === undefined ? undefined : new Uint8Array(bytes)))(bytesAt(at));
|
|
22
|
+
export const observeText = (at) => bytesAt(at)?.toString('utf8');
|
|
23
|
+
export const exists = (at) => existsSync(at);
|
|
24
|
+
export function stampOf(at) {
|
|
25
|
+
const st = stampable(at);
|
|
26
|
+
if (!st)
|
|
27
|
+
return undefined;
|
|
28
|
+
const kind = st.isSymbolicLink() ? 'link' : st.isDirectory() ? 'dir' : st.isFile() ? 'file' : 'other';
|
|
29
|
+
return { kind, stamp: `${Math.floor(st.mtimeMs)}-${st.size}` };
|
|
30
|
+
}
|
|
31
|
+
/** A coordinate listed a moment ago can be gone before it is read: a landing renames its file into place. Gone is none. */
|
|
32
|
+
function stampable(at) {
|
|
33
|
+
try {
|
|
34
|
+
return lstatSync(at);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export const linkOf = (at) => readlinkSync(at);
|
|
41
|
+
export function entriesIn(at) {
|
|
42
|
+
if (!stampable(at)?.isDirectory())
|
|
43
|
+
return [];
|
|
44
|
+
return readdirSync(at, { withFileTypes: true }).map((entry) => ({ name: entry.name, dir: entry.isDirectory() }));
|
|
45
|
+
}
|
|
46
|
+
export function coordinatesUnder(dir, root, into = () => true) {
|
|
47
|
+
const out = [];
|
|
48
|
+
const name = (at) => relative(root, at).split(sep).join('/');
|
|
49
|
+
const walk = (at, kind) => {
|
|
50
|
+
if (kind === 'link')
|
|
51
|
+
return;
|
|
52
|
+
if (kind === 'file') {
|
|
53
|
+
out.push(name(at));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (kind !== 'dir' || !into(name(at)))
|
|
57
|
+
return;
|
|
58
|
+
for (const entry of readdirSync(at, { withFileTypes: true })) {
|
|
59
|
+
const here = join(at, entry.name);
|
|
60
|
+
if (entry.isSymbolicLink())
|
|
61
|
+
continue;
|
|
62
|
+
walk(here, entry.isFile() ? 'file' : entry.isDirectory() ? 'dir' : 'other');
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const st = stampable(dir);
|
|
66
|
+
walk(dir, !st ? 'other' : st.isSymbolicLink() ? 'link' : st.isDirectory() ? 'dir' : st.isFile() ? 'file' : 'other');
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Kind } from './run.ts';
|
|
2
|
+
/** Where a reader's vectors lie: in its package, beside its source, under the name of its module, the folder the wire's vector family names. */
|
|
3
|
+
export declare const vectorsOf: (module: string, standing: readonly string[]) => string;
|
|
4
|
+
export declare const withoutVectors: (root: string, modules: readonly string[], standing: readonly string[]) => number;
|
|
5
|
+
export declare function resetsOnEqualEmits(lines: readonly string[]): number;
|
|
6
|
+
/** The executable keys the reader; its vector results describe only the cases measured. */
|
|
7
|
+
export declare function identityOf(input: {
|
|
8
|
+
readonly store: string;
|
|
9
|
+
readonly root: string;
|
|
10
|
+
readonly speaker: string;
|
|
11
|
+
readonly module: string;
|
|
12
|
+
readonly kind: Kind;
|
|
13
|
+
readonly code: string;
|
|
14
|
+
readonly lineage: readonly string[];
|
|
15
|
+
readonly kept: readonly string[];
|
|
16
|
+
}): {
|
|
17
|
+
readonly by: string;
|
|
18
|
+
readonly lines: readonly string[];
|
|
19
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { canonical } from '@lapxo/topos/wire';
|
|
5
|
+
import { bytesDigest } from "../fold/digests.js";
|
|
6
|
+
import { fieldOf, selfName } from "../fold/claims.js";
|
|
7
|
+
import { observeText } from "./files.js";
|
|
8
|
+
import { runReader } from "./run.js";
|
|
9
|
+
import { wordOf } from "../fold/wire.js";
|
|
10
|
+
import { lockStanding } from "../fold/keys.js";
|
|
11
|
+
/** Where a reader's vectors lie: in its package, beside its source, under the name of its module, the folder the wire's vector family names. */
|
|
12
|
+
export const vectorsOf = (module, standing) => join(module.split('/')[0] ?? '', `${wordOf(standing, 'families', 'vector')}s`, `${basename(module).replace(/\.[^.]+$/, '')}.json`);
|
|
13
|
+
export const withoutVectors = (root, modules, standing) => [...new Set(modules)].filter((module) => observeText(resolve(root, vectorsOf(module, standing))) === undefined).length;
|
|
14
|
+
export function resetsOnEqualEmits(lines) {
|
|
15
|
+
const named = new Map();
|
|
16
|
+
for (const line of lines.filter((one) => fieldOf(one, 'measure') === 'emits')) {
|
|
17
|
+
const key = `${fieldOf(line, 'scope')} ${fieldOf(line, 'value')}`;
|
|
18
|
+
named.set(key, (named.get(key) ?? new Set()).add(fieldOf(line, 'at')));
|
|
19
|
+
}
|
|
20
|
+
return [...named.values()].filter((ats) => ats.size > 1).length;
|
|
21
|
+
}
|
|
22
|
+
function emitted(store, root, module, kind, cases) {
|
|
23
|
+
const laid = kind === 'process' ? mkdtempSync(join(tmpdir(), `${selfName()}-vectors-`)) : root;
|
|
24
|
+
try {
|
|
25
|
+
for (const one of kind === 'process' ? cases : []) {
|
|
26
|
+
for (const [name, text] of Object.entries(one.files ?? {})) {
|
|
27
|
+
mkdirSync(dirname(join(laid, one.place, name)), { recursive: true });
|
|
28
|
+
writeFileSync(join(laid, one.place, name), text);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const rows = runReader(resolve(root, module), laid, cases.map((one) => ({ place: one.place, ...(kind === 'js' ? { text: one.text ?? '' } : {}) })));
|
|
32
|
+
if (cases.length && rows.every((mine) => !mine.length))
|
|
33
|
+
return undefined;
|
|
34
|
+
const said = rows.map((mine) => mine.filter((row) => !/^(cost|derived|vector-memo)\//.test(row.scope)).map((row) => JSON.stringify(row)).sort());
|
|
35
|
+
return bytesDigest(store, new TextEncoder().encode(JSON.stringify(said))).replace(/^[^:]*:/, '');
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
if (laid !== root)
|
|
39
|
+
rmSync(laid, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** The executable keys the reader; its vector results describe only the cases measured. */
|
|
43
|
+
export function identityOf(input) {
|
|
44
|
+
const text = observeText(resolve(input.root, vectorsOf(input.module, lockStanding(input.store))));
|
|
45
|
+
if (text === undefined)
|
|
46
|
+
return { by: input.code, lines: [] };
|
|
47
|
+
const short = (value) => bytesDigest(input.store, new TextEncoder().encode(value)).replace(/^[^:]*:/, '').slice(0, 12);
|
|
48
|
+
const key = short(`${input.code} ${short(text)}`);
|
|
49
|
+
const mine = input.kept.filter((line) => fieldOf(line, 'scope') === `reader/${input.module}`);
|
|
50
|
+
const known = mine.find((line) => fieldOf(line, 'measure') === 'extension' && fieldOf(line, 'at') === `place:${key}`);
|
|
51
|
+
if (known !== undefined && fieldOf(known, 'value') === input.code)
|
|
52
|
+
return { by: input.code, lines: [] };
|
|
53
|
+
const emits = emitted(input.store, input.root, input.module, input.kind, JSON.parse(text).cases ?? []);
|
|
54
|
+
if (emits === undefined)
|
|
55
|
+
return { by: input.code, lines: [] };
|
|
56
|
+
const by = input.code;
|
|
57
|
+
const line = (measure, value, at) => canonical({
|
|
58
|
+
scope: `reader/${input.module}`, role: 'writes', form: 'alphabet', measure, value, by: `${input.speaker}:${by}`, at: `place:${at}`,
|
|
59
|
+
});
|
|
60
|
+
return { by, lines: [line('extension', by, key), line('emits', emits, by)] };
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const fits: (read: string, coordinate: string) => boolean;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as wire from '@lapxo/topos/wire';
|
|
2
|
+
const { matches } = wire;
|
|
3
|
+
export const fits = (read, coordinate) => {
|
|
4
|
+
const glob = read.includes('@') ? read.slice(0, read.indexOf('@')) : read;
|
|
5
|
+
const [bare, tail] = coordinate.endsWith('/') ? [coordinate.slice(0, -1), '/'] : [coordinate, ''];
|
|
6
|
+
const steps = bare.split('/');
|
|
7
|
+
return steps.some((_, i) => matches(glob, `${steps.slice(i).join('/')}${tail}`));
|
|
8
|
+
};
|