@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,103 @@
|
|
|
1
|
+
import { verdictDemand } from '@lapxo/topos/authority';
|
|
2
|
+
import { matches, parse } from '@lapxo/topos/wire';
|
|
3
|
+
const fields = (line) => {
|
|
4
|
+
const got = parse(line);
|
|
5
|
+
return got.kind === 'fact' ? got.value.fields : {};
|
|
6
|
+
};
|
|
7
|
+
const f = (line, name) => fields(line)[name] ?? '';
|
|
8
|
+
const words = (value) => [...new Set(value.split('|').filter(Boolean))];
|
|
9
|
+
const active = (line) => f(line, 'value') !== 'withdraw';
|
|
10
|
+
const declaration = (line) => active(line) && f(line, 'role') === 'writes' && f(line, 'form') === 'alphabet' && f(line, 'measure') === 'id';
|
|
11
|
+
/** Preserve actual fold faults; none gets a release category from its spelling. */
|
|
12
|
+
export function releaseFaults(fold) {
|
|
13
|
+
return [
|
|
14
|
+
...fold.forks.map((one) => ({ kind: 'fork', evidence: [one.key, ...one.lines] })),
|
|
15
|
+
...fold.refused.map((line) => ({ kind: 'refused', evidence: [line] })),
|
|
16
|
+
...fold.grey.map((line) => ({ kind: 'grey', evidence: [line] })),
|
|
17
|
+
...fold.orphans.map((line) => ({ kind: 'orphan', evidence: [line] })),
|
|
18
|
+
...(fold.second.verdict === 'forks' ? [{ kind: 'second-fork', evidence: [fold.second.verdict] }] : []),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
/** Select declared obligations, never categories inferred from their names or paths. */
|
|
22
|
+
export function releasePolicy(input) {
|
|
23
|
+
const policy = [...new Set(input.standing.filter((line) => f(line, 'scope') === 'release/blocks' && declaration(line)))];
|
|
24
|
+
const debt = [...new Set([...input.demands, ...input.ceilings])];
|
|
25
|
+
const gateLines = input.standing.filter(line => active(line) && f(line, 'role') === 'demands' && f(line, 'form') === 'alphabet' && f(line, 'measure') === 'verdict');
|
|
26
|
+
const gates = gateLines.map(line => verdictDemand(fields(line), input.verdicts ?? []));
|
|
27
|
+
const base = { snapshot: input.snapshot, policy, debt, gates };
|
|
28
|
+
if (!policy.length)
|
|
29
|
+
return { ...base, status: 'absent', categories: [], unresolved: [] };
|
|
30
|
+
const values = new Set(policy.map((line) => [...words(f(line, 'value'))].sort().join('|')));
|
|
31
|
+
const unresolved = [];
|
|
32
|
+
for (const fault of input.faults ?? []) {
|
|
33
|
+
if (fault.obligation === undefined || !debt.includes(fault.obligation))
|
|
34
|
+
unresolved.push(`unclassified ${fault.kind}: ${fault.evidence.join(' | ')}`);
|
|
35
|
+
}
|
|
36
|
+
if (values.size !== 1)
|
|
37
|
+
unresolved.push('release/blocks has conflicting standing values');
|
|
38
|
+
const labels = words(f(policy[0], 'value'));
|
|
39
|
+
if (!labels.length)
|
|
40
|
+
unresolved.push('release/blocks selects no category');
|
|
41
|
+
const categories = labels.map((name) => {
|
|
42
|
+
const links = [...new Set(input.standing.filter((line) => f(line, 'scope') === `release/category/${name}` && declaration(line)))];
|
|
43
|
+
if (!links.length)
|
|
44
|
+
unresolved.push(`release/category/${name} has no admitted link`);
|
|
45
|
+
if (new Set(links.map((line) => [...words(f(line, 'value'))].sort().join('|'))).size > 1)
|
|
46
|
+
unresolved.push(`release/category/${name} has conflicting standing values`);
|
|
47
|
+
const selected = new Set();
|
|
48
|
+
for (const selector of words(f(links[0] ?? '', 'value'))) {
|
|
49
|
+
const met = debt.filter((line) => matches(selector, f(line, 'scope')));
|
|
50
|
+
if (!met.length)
|
|
51
|
+
unresolved.push(`release/category/${name} selector ${selector} names no obligation`);
|
|
52
|
+
met.forEach((line) => selected.add(line));
|
|
53
|
+
}
|
|
54
|
+
if (!selected.size)
|
|
55
|
+
unresolved.push(`release/category/${name} selects no obligation`);
|
|
56
|
+
const failures = [];
|
|
57
|
+
for (const line of selected) {
|
|
58
|
+
for (const fault of input.faults ?? [])
|
|
59
|
+
if (fault.obligation === line)
|
|
60
|
+
failures.push({ line, reason: fault.kind, evidence: [...fault.evidence] });
|
|
61
|
+
if (input.demands.includes(line)) {
|
|
62
|
+
const gate = gateLines.includes(line) ? verdictDemand(fields(line), input.verdicts ?? []) : undefined;
|
|
63
|
+
if (gate) {
|
|
64
|
+
if (gate.status !== 'met')
|
|
65
|
+
failures.push({ line, reason: gate.status, evidence: gate.evidence.map(e => `${e.coordinate}=${e.value}`) });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!input.paid.includes(line))
|
|
69
|
+
failures.push({ line, reason: 'missing', evidence: [] });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const scope = f(line, 'scope');
|
|
73
|
+
const over = input.readings.over.filter((one) => one.ceiling === scope);
|
|
74
|
+
if (over.length && input.ceilings.filter((one) => f(one, 'scope') === scope).length !== 1)
|
|
75
|
+
unresolved.push(`ceiling evidence for ${scope} does not identify one obligation`);
|
|
76
|
+
if (over.length)
|
|
77
|
+
failures.push({ line, reason: 'outside', evidence: over.map((one) => `${one.side} ${one.question} ${one.got}`) });
|
|
78
|
+
if (input.readings.unread.includes(line))
|
|
79
|
+
failures.push({ line, reason: 'unread', evidence: [] });
|
|
80
|
+
if (input.readings.vacuous.includes(line))
|
|
81
|
+
failures.push({ line, reason: 'vacuous', evidence: [] });
|
|
82
|
+
if (!input.readings.read.includes(line) && !input.readings.unread.includes(line) && !input.readings.vacuous.includes(line)) {
|
|
83
|
+
unresolved.push(`ceiling ${scope} has no result in this fold`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { name, links, selected: [...selected], failures };
|
|
87
|
+
});
|
|
88
|
+
return { ...base, categories, unresolved: [...new Set(unresolved)], status: unresolved.length ? 'incomplete' : categories.some((one) => one.failures.length) ? 'blocked' : 'ready' };
|
|
89
|
+
}
|
|
90
|
+
/** Existing fold output can show the policy without a new view or a release action. */
|
|
91
|
+
export function releasePolicyLines(result) {
|
|
92
|
+
if (result === undefined)
|
|
93
|
+
return [];
|
|
94
|
+
const gates = (result.gates ?? []).map(g => `GATE ${g.scope} ${g.status} · requires=${g.required.join('|')} · ${g.evidence.map(e => `${e.coordinate}=${e.value}`).join(' | ') || g.reason || 'no evidence'}`);
|
|
95
|
+
if (result.status === 'absent')
|
|
96
|
+
return gates;
|
|
97
|
+
return [
|
|
98
|
+
...gates,
|
|
99
|
+
`RELEASE policy ${result.status} · fold ${result.snapshot.fold} · instrument ${result.snapshot.instrument}`,
|
|
100
|
+
...result.unresolved.map((one) => `UNRESOLVED release · ${one}`),
|
|
101
|
+
...result.categories.flatMap((one) => one.failures.map((failure) => `BLOCK ${one.name} · ${f(failure.line, 'scope')} · ${failure.reason}${failure.evidence.length ? ` · ${failure.evidence.join(' | ')}` : ''}`)),
|
|
102
|
+
];
|
|
103
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PlaceFold } from '../cli/place.ts';
|
|
2
|
+
export declare const bytesOnly: (line: string) => string;
|
|
3
|
+
/**
|
|
4
|
+
* Every receipt of a place, @8: the bytes of what it observed that a reading ran over, those readings, the verdicts its
|
|
5
|
+
* paid demands were taken with, and at the root the digest of each place's own receipts. The attest is none of them:
|
|
6
|
+
* it stands in the root lock and the release, naming the root these lines resolve to. The fold keeps the set in the
|
|
7
|
+
* root's store under the digest of its bytes, which every line that resolves it names, and what a place with no reader
|
|
8
|
+
* carries is read back from there, never from a file the place ships.
|
|
9
|
+
*/
|
|
10
|
+
export declare function receiptsOf(fold: PlaceFold): readonly string[];
|
|
11
|
+
export declare function keepCarried(store: string, lines: readonly string[], from?: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* What a copy of a place reads back of its receipts: the lines it shipped when they are the whole set, or, when they
|
|
14
|
+
* name a root at the digest of its bytes, the set the store keeps there; a root the store does not keep is no set, and
|
|
15
|
+
* the caller refuses rather than read the lines that only name it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function carriedFrom(store: string, shipped: readonly string[]): readonly string[] | undefined;
|
|
18
|
+
export declare const carriedIn: (store: string) => readonly string[];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createHash, dirname, existsSync, join, linkSync, mkdirSync, readdirSync, renameSync, writeFileSync } from "../host/io.js";
|
|
2
|
+
import { canonical, parse } from '@lapxo/topos/wire';
|
|
3
|
+
import { RECEIPTS, fieldOf, isWire } from "./claims.js";
|
|
4
|
+
import { bytesDigest } from "./digests.js";
|
|
5
|
+
import { carriesOnly } from "./observed.js";
|
|
6
|
+
import { writerFor } from "./signers.js";
|
|
7
|
+
import { viewsOf } from "./views.js";
|
|
8
|
+
import { zoomed } from "./zoom.js";
|
|
9
|
+
import { ledgerLines, storeAt } from "../land/ledger.js";
|
|
10
|
+
import { sourced } from "../land/vouched.js";
|
|
11
|
+
import { entriesIn, observeFile, observeText } from "../observe/files.js";
|
|
12
|
+
export const bytesOnly = (line) => {
|
|
13
|
+
const got = parse(line);
|
|
14
|
+
return got.kind === 'fact' && fieldOf(line, 'measure') === 'observed' ? canonical({ ...got.value.fields, value: fieldOf(line, 'at').replace(/^.*:/, '').slice(0, 16) }) : line;
|
|
15
|
+
};
|
|
16
|
+
const held = new WeakMap();
|
|
17
|
+
/**
|
|
18
|
+
* Every receipt of a place, @8: the bytes of what it observed that a reading ran over, those readings, the verdicts its
|
|
19
|
+
* paid demands were taken with, and at the root the digest of each place's own receipts. The attest is none of them:
|
|
20
|
+
* it stands in the root lock and the release, naming the root these lines resolve to. The fold keeps the set in the
|
|
21
|
+
* root's store under the digest of its bytes, which every line that resolves it names, and what a place with no reader
|
|
22
|
+
* carries is read back from there, never from a file the place ships.
|
|
23
|
+
*/
|
|
24
|
+
export function receiptsOf(fold) {
|
|
25
|
+
const kept = held.get(fold);
|
|
26
|
+
if (kept !== undefined)
|
|
27
|
+
return kept;
|
|
28
|
+
if (!fold.observed.length && carriesOnly(fold.root, fold.store))
|
|
29
|
+
return held.set(fold, [...new Set(carriedIn(fold.store).flatMap((file) => (observeText(file) ?? '').split('\n').filter(isWire)))]).get(fold);
|
|
30
|
+
const under = fold.under ?? '';
|
|
31
|
+
const shape = [...viewsOf(fold.standing).values()].flatMap((view) => ('regions' in view && view.regions.some((one) => one.name === 'receipts') ? [view.shape] : []))[0] ?? '';
|
|
32
|
+
const closed = under || !shape ? [] : entriesIn(fold.root).filter((entry) => entry.dir && observeText(join(fold.root, entry.name, shape)) !== undefined).map((entry) => `${entry.name}/`).sort();
|
|
33
|
+
const own = (place) => place.startsWith(under) && !closed.some((one) => place.startsWith(one)) && (!shape || !place.endsWith(shape));
|
|
34
|
+
const mine = new Set(fold.observed.filter((line) => fieldOf(line, 'measure') === 'observed' && own(fieldOf(line, 'scope')))
|
|
35
|
+
.map((line) => `${fieldOf(line, 'by')} ${fieldOf(line, 'at')}`));
|
|
36
|
+
const ran = [...new Set(fold.observed.filter((line) => fieldOf(line, 'measure') !== 'observed'
|
|
37
|
+
&& mine.has(`${fieldOf(line, 'by')} ${fieldOf(line, 'at')}`)))];
|
|
38
|
+
const ranAt = new Set(ran.map((line) => `${fieldOf(line, 'by')} ${fieldOf(line, 'at')}`));
|
|
39
|
+
const cones = new Set(fold.observed.filter((line) => fieldOf(line, 'measure') === 'observed' && own(fieldOf(line, 'scope')) && ranAt.has(`${fieldOf(line, 'by')} ${fieldOf(line, 'at')}`)).map(bytesOnly));
|
|
40
|
+
const paid = ledgerLines(fold.store, 'judge').filter((line) => fold.paid.some((demand) => fieldOf(demand, 'scope') === fieldOf(line, 'scope')
|
|
41
|
+
&& !(closed.length > 0 && fieldOf(demand, 'needs').split('|').filter(Boolean).length > 0
|
|
42
|
+
&& fieldOf(demand, 'needs').split('|').filter(Boolean).every((need) => closed.some((one) => need.startsWith(one))))));
|
|
43
|
+
const named = closed.map((place) => {
|
|
44
|
+
const digest = bytesDigest(fold.store, observeFile(join(fold.root, place, shape)) ?? new Uint8Array());
|
|
45
|
+
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
|
+
});
|
|
47
|
+
const place = under.replace(/\/$/, '');
|
|
48
|
+
const lines = [...named, ...[...cones].sort(), ...[...ran].sort(), ...paid].map((line) => (place ? zoomed(line, place) : line));
|
|
49
|
+
if (!lines.length) {
|
|
50
|
+
const shipped = (observeText(join(fold.root, under, RECEIPTS)) ?? '').split('\n').filter(isWire);
|
|
51
|
+
const carried = carriedFrom(fold.store, shipped);
|
|
52
|
+
if (carried !== undefined && carried.length)
|
|
53
|
+
return held.set(fold, carried).get(fold);
|
|
54
|
+
}
|
|
55
|
+
held.set(fold, lines);
|
|
56
|
+
keepCarried(fold.store, lines);
|
|
57
|
+
return lines;
|
|
58
|
+
}
|
|
59
|
+
export function keepCarried(store, lines, from) {
|
|
60
|
+
const text = lines.map((line) => `${line}\n`).join('');
|
|
61
|
+
const digest = createHash('sha256').update(text).digest('hex');
|
|
62
|
+
const at = storeAt(store, 'cas', 'carried', digest);
|
|
63
|
+
if (existsSync(at)) {
|
|
64
|
+
verifiedCarried(at, digest);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
68
|
+
const src = from === undefined ? '' : storeAt(from, 'cas', 'carried', digest);
|
|
69
|
+
if (src && existsSync(src)) {
|
|
70
|
+
verifiedCarried(src, digest);
|
|
71
|
+
try {
|
|
72
|
+
linkSync(src, at);
|
|
73
|
+
return `sha256:${digest}`;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Cross-device stores still keep the authenticated bytes below.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
writeFileSync(`${at}.${process.pid}`, text);
|
|
80
|
+
renameSync(`${at}.${process.pid}`, at);
|
|
81
|
+
}
|
|
82
|
+
return `sha256:${digest}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* What a copy of a place reads back of its receipts: the lines it shipped when they are the whole set, or, when they
|
|
86
|
+
* name a root at the digest of its bytes, the set the store keeps there; a root the store does not keep is no set, and
|
|
87
|
+
* the caller refuses rather than read the lines that only name it.
|
|
88
|
+
*/
|
|
89
|
+
export function carriedFrom(store, shipped) {
|
|
90
|
+
const named = shipped.find((line) => fieldOf(line, 'scope') === 'receipts' && fieldOf(line, 'measure') === 'digest');
|
|
91
|
+
if (named === undefined)
|
|
92
|
+
return shipped;
|
|
93
|
+
// Resolution 8 may precede its authenticated summary in the same public file.
|
|
94
|
+
// It is evidence only if its exact bytes meet the summary's carrier digest.
|
|
95
|
+
for (let i = 1; i < shipped.length; i += 1) {
|
|
96
|
+
const header = shipped[i];
|
|
97
|
+
if (fieldOf(header, 'scope') !== 'receipts' || fieldOf(header, 'measure') !== 'digest')
|
|
98
|
+
continue;
|
|
99
|
+
const digest = fieldOf(header, 'at').replace(/^place:sha256:/, '');
|
|
100
|
+
const body = shipped.slice(0, i);
|
|
101
|
+
if (/^[0-9a-f]{64}$/.test(digest) && createHash('sha256').update(body.map(line => `${line}\n`).join('')).digest('hex') === digest)
|
|
102
|
+
return body;
|
|
103
|
+
}
|
|
104
|
+
const hex = fieldOf(named, 'at').replace(/^place:(?:sha256:)?/, '');
|
|
105
|
+
const text = /^[0-9a-f]{64}$/.test(hex) ? verifiedCarried(storeAt(store, 'cas', 'carried', hex), hex) : undefined;
|
|
106
|
+
return text === undefined ? undefined : text.split('\n').filter(isWire);
|
|
107
|
+
}
|
|
108
|
+
export const carriedIn = (store) => ((dir) => (existsSync(dir)
|
|
109
|
+
? readdirSync(dir).filter((name) => /^[0-9a-f]{64}$/.test(name)).sort().map((name) => {
|
|
110
|
+
const at = join(dir, name);
|
|
111
|
+
verifiedCarried(at, name);
|
|
112
|
+
return at;
|
|
113
|
+
}) : []))(storeAt(store, 'cas', 'carried'));
|
|
114
|
+
/** Authenticate the existing SHA-256 receipt carrier before interpreting any lines. */
|
|
115
|
+
function verifiedCarried(at, expected) {
|
|
116
|
+
const bytes = observeFile(at);
|
|
117
|
+
if (bytes === undefined)
|
|
118
|
+
return undefined;
|
|
119
|
+
const actual = createHash('sha256').update(bytes).digest('hex');
|
|
120
|
+
if (actual !== expected)
|
|
121
|
+
throw Error(`REFUSE·receipt carried bytes mismatch · expected sha256:${expected} · got sha256:${actual}`);
|
|
122
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
123
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RoleClaim } from '@lapxo/topos/wire';
|
|
2
|
+
import type { CoordinateRole } from './wire.ts';
|
|
3
|
+
/** A lock line that gives a coordinate its role: every walk of the tree reads it. */
|
|
4
|
+
export declare function isRoleLock(line: string): boolean;
|
|
5
|
+
/** A lock the owner signs by digest and a capsule writes: its lines stand while the bytes are the ones that were signed. */
|
|
6
|
+
export declare function inheritedRoles(store: string): {
|
|
7
|
+
readonly claims: readonly RoleClaim[];
|
|
8
|
+
readonly stale: readonly string[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* What each coordinate of the tree is: the capsule that knows a world writes the roles of that world, the owner signs the
|
|
12
|
+
* capsule's lock by digest, and this fold reads both. The instrument reads claims and never a name, and a coordinate no
|
|
13
|
+
* standing claim names is source. The steps a walk must not enter are read the same way: the claims that name a whole
|
|
14
|
+
* folder foreign or derived, as a set the walk can ask in one step instead of matching every claim at every step.
|
|
15
|
+
*/
|
|
16
|
+
export declare function stepsOutside(store: string): ReadonlySet<string>;
|
|
17
|
+
export declare function rolesOf(store: string): (coordinate: string) => CoordinateRole;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHash, dirname, join } from "../host/io.js";
|
|
2
|
+
import { parse, roleClaimsOf } from '@lapxo/topos/wire';
|
|
3
|
+
import { namesAt, roleAt } from "./wire.js";
|
|
4
|
+
import { lockStanding } from "./keys.js";
|
|
5
|
+
import { placesIn } from "./places.js";
|
|
6
|
+
import { ownLockOf } from "./signed.js";
|
|
7
|
+
import { observeText } from "../observe/files.js";
|
|
8
|
+
import { fieldOf, isWire } from "./claims.js";
|
|
9
|
+
/** A lock line that gives a coordinate its role: every walk of the tree reads it. */
|
|
10
|
+
export function isRoleLock(line) {
|
|
11
|
+
const got = parse(line);
|
|
12
|
+
return got.kind === 'fact' && roleClaimsOf([got.value.fields]).length > 0;
|
|
13
|
+
}
|
|
14
|
+
/** A lock the owner signs by digest and a capsule writes: its lines stand while the bytes are the ones that were signed. */
|
|
15
|
+
export function inheritedRoles(store) {
|
|
16
|
+
const root = dirname(store);
|
|
17
|
+
const claims = [];
|
|
18
|
+
const stale = [];
|
|
19
|
+
for (const line of lockStanding(store)) {
|
|
20
|
+
if (fieldOf(line, 'measure') !== 'role' || !fieldOf(line, 'scope').startsWith('roles/'))
|
|
21
|
+
continue;
|
|
22
|
+
const at = fieldOf(line, 'needs');
|
|
23
|
+
const held = at ? observeText(join(root, at)) : undefined;
|
|
24
|
+
const digest = held === undefined ? '' : `sha256:${createHash('sha256').update(held).digest('hex')}`;
|
|
25
|
+
if (held === undefined || digest !== fieldOf(line, 'restsOn')) {
|
|
26
|
+
stale.push(fieldOf(line, 'scope'));
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
for (const one of held.split('\n').filter(isWire).map((row) => parse(row))) {
|
|
30
|
+
if (one.kind === 'fact')
|
|
31
|
+
claims.push(one.value.fields);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { claims: roleClaimsOf(claims), stale };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* What each coordinate of the tree is: the capsule that knows a world writes the roles of that world, the owner signs the
|
|
38
|
+
* capsule's lock by digest, and this fold reads both. The instrument reads claims and never a name, and a coordinate no
|
|
39
|
+
* standing claim names is source. The steps a walk must not enter are read the same way: the claims that name a whole
|
|
40
|
+
* folder foreign or derived, as a set the walk can ask in one step instead of matching every claim at every step.
|
|
41
|
+
*/
|
|
42
|
+
export function stepsOutside(store) {
|
|
43
|
+
const own = roleClaimsOf(lockStanding(store)
|
|
44
|
+
.map((line) => parse(line))
|
|
45
|
+
.flatMap((got) => (got.kind === 'fact' ? [got.value.fields] : [])));
|
|
46
|
+
return new Set([...own, ...inheritedRoles(store).claims]
|
|
47
|
+
.filter((claim) => (claim.role === 'foreign' || claim.role === 'derived') && !claim.name.includes('/') && !claim.name.includes('*'))
|
|
48
|
+
.map((claim) => claim.name));
|
|
49
|
+
}
|
|
50
|
+
export function rolesOf(store) {
|
|
51
|
+
const fields = (lines) => lines.map((line) => parse(line)).flatMap((got) => (got.kind === 'fact' ? [got.value.fields] : []));
|
|
52
|
+
const claims = [...roleClaimsOf(fields(lockStanding(store))), ...inheritedRoles(store).claims];
|
|
53
|
+
const root = dirname(store);
|
|
54
|
+
const placed = new Map(placesIn(root).map((place) => [place, roleClaimsOf(fields(ownLockOf(root, place)))]));
|
|
55
|
+
return (coordinate) => {
|
|
56
|
+
const own = coordinate.includes('/') ? placed.get(coordinate.slice(0, coordinate.indexOf('/'))) ?? [] : [];
|
|
57
|
+
return roleAt(coordinate, own.some((claim) => namesAt(coordinate, claim.name)) ? own : claims);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SecondHead {
|
|
2
|
+
/** What its latest word says of this fold: the same lines stand, other lines stand, it read another ledger, its key is not admitted, or it never spoke. */
|
|
3
|
+
readonly verdict: 'agrees' | 'forks' | 'stale' | 'grey' | 'none';
|
|
4
|
+
/**
|
|
5
|
+
* A head counts as an origin of its own only once it has disagreed: until then it may be this one again (L16); and
|
|
6
|
+
* the epoch of its latest word, wherever it said it, which a clone reads as history and not as its own.
|
|
7
|
+
*/
|
|
8
|
+
readonly external: boolean;
|
|
9
|
+
readonly epoch: number;
|
|
10
|
+
readonly apart?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The second head's word, read against this fold. It attests the digest of the lines it found standing and the digest
|
|
14
|
+
* of the lines of the ledger it folded, in whatever order a store landed them, so a clone that lands the same lines
|
|
15
|
+
* agrees; agreement is an encounter, disagreement a fork. A word about another lock is not read as this one's: the head
|
|
16
|
+
* bound's own lock names is asked now over the ledger as it stands, with no key, and what it says is read, never kept.
|
|
17
|
+
*/
|
|
18
|
+
export declare function secondHead(store: string, standing: readonly string[], admitted: (line: string) => boolean, owned?: readonly string[]): SecondHead;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { snapshotVerdict } from "./evidence.js";
|
|
2
|
+
import { dirname, resolve, spawnSync } from "../host/io.js";
|
|
3
|
+
import { ledgerLines } from "../land/ledger.js";
|
|
4
|
+
import { ownLock } from "../observe/runner.js";
|
|
5
|
+
import { fieldOf } from "./claims.js";
|
|
6
|
+
import { keyOf, lockLines } from "./keys.js";
|
|
7
|
+
import { digestsOf, finerOf, regionsOf } from "./region.js";
|
|
8
|
+
import { said } from "./wire.js";
|
|
9
|
+
import { sha } from "../host/hash.js";
|
|
10
|
+
const asked = new Map();
|
|
11
|
+
function askedNow(store, rests) {
|
|
12
|
+
const root = dirname(store);
|
|
13
|
+
const key = `${root} ${rests}`;
|
|
14
|
+
if (asked.has(key))
|
|
15
|
+
return asked.get(key);
|
|
16
|
+
const argv = said(ownLock(), 'head/second');
|
|
17
|
+
const got = argv.length ? spawnSync(process.execPath, ['--disable-warning=ExperimentalWarning', '--disable-warning=MODULE_TYPELESS_PACKAGE_JSON',
|
|
18
|
+
resolve(root, argv[0]), ...argv.slice(1), root], { cwd: root, encoding: 'utf8', maxBuffer: 1 << 26 }) : undefined;
|
|
19
|
+
const line = got === undefined || got.status !== 0 || typeof got.stdout !== 'string' ? undefined : got.stdout.split('\n').find((one) => one.startsWith('VERIFY'));
|
|
20
|
+
const word = line === undefined ? undefined : line.slice('VERIFY'.length).trim().split(' ')[0];
|
|
21
|
+
const verdict = word === 'agrees' || word === 'forks' ? word : undefined;
|
|
22
|
+
asked.set(key, verdict);
|
|
23
|
+
return verdict;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The second head's word, read against this fold. It attests the digest of the lines it found standing and the digest
|
|
27
|
+
* of the lines of the ledger it folded, in whatever order a store landed them, so a clone that lands the same lines
|
|
28
|
+
* agrees; agreement is an encounter, disagreement a fork. A word about another lock is not read as this one's: the head
|
|
29
|
+
* bound's own lock names is asked now over the ledger as it stands, with no key, and what it says is read, never kept.
|
|
30
|
+
*/
|
|
31
|
+
export function secondHead(store, standing, admitted, owned = lockLines(store)) {
|
|
32
|
+
const head = keyOf(store, 'attest');
|
|
33
|
+
const said = head === undefined ? [] : ledgerLines(store, head).filter((line) => fieldOf(line, 'scope') === `${head}/target`);
|
|
34
|
+
const last = said.at(-1);
|
|
35
|
+
const lines = owned.filter((line) => /\bsig=/.test(line));
|
|
36
|
+
const rests = `sha256:${sha([...new Set(lines)].sort().join('\n'))}`;
|
|
37
|
+
const epoch = lines.reduce((most, line) => Math.max(most, Number(fieldOf(line, 'epoch')) || 0), 0);
|
|
38
|
+
const input = {
|
|
39
|
+
declared: head !== undefined,
|
|
40
|
+
expected: { set: lines.length ? rests : undefined, standing: `place:sha256:${sha([...new Set(standing)].sort().join('\n'))}`, epoch },
|
|
41
|
+
...(last ? { statement: { admitted: admitted(last), set: fieldOf(last, 'restsOn'), standing: fieldOf(last, 'at'), epoch: Number(fieldOf(last, 'epoch')) || 0 } } : {}),
|
|
42
|
+
priorDisagreement: said.some(line => fieldOf(line, 'value') === 'forks' && admitted(line)),
|
|
43
|
+
};
|
|
44
|
+
let result = snapshotVerdict(input);
|
|
45
|
+
if (result.verdict === 'stale' && last && lines.length) {
|
|
46
|
+
const now = askedNow(store, rests);
|
|
47
|
+
if (now !== undefined)
|
|
48
|
+
result = snapshotVerdict({ ...input, comparison: { admitted: true, set: rests, verdict: now, epoch } });
|
|
49
|
+
}
|
|
50
|
+
if (result.verdict !== 'forks' || head === undefined || last === undefined || fieldOf(last, 'restsOn') !== rests)
|
|
51
|
+
return result;
|
|
52
|
+
const theirs = new Map(ledgerLines(store, head).filter((line) => fieldOf(line, 'scope').startsWith(`${head}/region/`) && fieldOf(line, 'restsOn') === fieldOf(last, 'restsOn'))
|
|
53
|
+
.map((line) => [fieldOf(line, 'scope').slice(`${head}/region/`.length).replace(/^\.$/, ''), fieldOf(line, 'value')]));
|
|
54
|
+
if (!theirs.size)
|
|
55
|
+
return result;
|
|
56
|
+
const regions = regionsOf(standing, standing, []);
|
|
57
|
+
const [ours, finer] = [digestsOf(regions), finerOf(regions)];
|
|
58
|
+
const apart = [];
|
|
59
|
+
const descend = (region) => {
|
|
60
|
+
if (theirs.get(region) === ours.get(region))
|
|
61
|
+
return;
|
|
62
|
+
apart.push(region || '.');
|
|
63
|
+
for (const one of finer.get(region) ?? [])
|
|
64
|
+
descend(one);
|
|
65
|
+
};
|
|
66
|
+
descend('');
|
|
67
|
+
return { ...result, apart };
|
|
68
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const OBSERVED: RegExp;
|
|
2
|
+
export declare const LEVEL: RegExp;
|
|
3
|
+
export declare const readerOf: (line: string) => string | undefined;
|
|
4
|
+
type Latest = {
|
|
5
|
+
readonly at: string;
|
|
6
|
+
readonly line: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const trimmed: (where: string) => string;
|
|
9
|
+
export { sha } from '../host/hash.ts';
|
|
10
|
+
/**
|
|
11
|
+
* What a reader said is kept by region: the region a place lies in is the longest a reader's lock names that holds it,
|
|
12
|
+
* the stamps and digests it was observed at are lines of that region's shard, and what the reader answered for the
|
|
13
|
+
* region is one observation, stored under its reader's extension, the region, the digest of the region's places and
|
|
14
|
+
* the resolution it was read at. A fold opens the shards of the regions it reaches and reads each observation by its
|
|
15
|
+
* key: nothing is scanned, the same bytes are never read twice, and an observation found under its key is the reading.
|
|
16
|
+
*/
|
|
17
|
+
export declare function regionIn(regions: readonly string[], place: string): string;
|
|
18
|
+
export declare const keyOf: (by: string, region: string, pairs: readonly string[], resolution?: string) => string;
|
|
19
|
+
export declare const observed: (store: string, key: string) => boolean;
|
|
20
|
+
export declare function observation(store: string, key: string): readonly string[] | undefined;
|
|
21
|
+
export declare function keepObservation(store: string, key: string, lines: readonly string[]): void;
|
|
22
|
+
export declare function opened(store: string, speaker: string, region: string, alive: ReadonlySet<string>): {
|
|
23
|
+
readonly region: string;
|
|
24
|
+
readonly places: ReadonlyMap<string, Latest>;
|
|
25
|
+
readonly keys: Map<string, string>;
|
|
26
|
+
};
|
|
27
|
+
export declare const pointer: (speaker: string, by: string, region: string, key: string, pairs: readonly string[]) => string;
|
|
28
|
+
export declare function legacyOf(store: string, speaker: string, regions: readonly string[]): ReadonlyMap<string, readonly string[]> | undefined;
|
|
29
|
+
export declare function setAside(store: string, speaker: string): void;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { dirname, existsSync, mkdirSync, renameSync, writeFileSync } from "../host/io.js";
|
|
2
|
+
import { canonical } from '@lapxo/topos/wire';
|
|
3
|
+
import { bytesDigest } from "./digests.js";
|
|
4
|
+
import { blobAt, landShard, ledgerLines, shardFile, shardLines, storeAt, writerFile } from "../land/ledger.js";
|
|
5
|
+
import { fieldOf, isWire } from "./claims.js";
|
|
6
|
+
import { observeFile, observeText } from "../observe/files.js";
|
|
7
|
+
export const OBSERVED = / measure=observed /;
|
|
8
|
+
export const LEVEL = /(?:^|\s)scope=(?:cost\/)?reader\//;
|
|
9
|
+
export const readerOf = (line) => /(?:^|\s)by=reader:([0-9a-f]+)/.exec(line)?.[1];
|
|
10
|
+
export const trimmed = (where) => where.replace(/\/+$/, '');
|
|
11
|
+
import { sha } from "../host/hash.js";
|
|
12
|
+
export { sha } from "../host/hash.js";
|
|
13
|
+
/**
|
|
14
|
+
* What a reader said is kept by region: the region a place lies in is the longest a reader's lock names that holds it,
|
|
15
|
+
* the stamps and digests it was observed at are lines of that region's shard, and what the reader answered for the
|
|
16
|
+
* region is one observation, stored under its reader's extension, the region, the digest of the region's places and
|
|
17
|
+
* the resolution it was read at. A fold opens the shards of the regions it reaches and reads each observation by its
|
|
18
|
+
* key: nothing is scanned, the same bytes are never read twice, and an observation found under its key is the reading.
|
|
19
|
+
*/
|
|
20
|
+
export function regionIn(regions, place) {
|
|
21
|
+
let best = '';
|
|
22
|
+
for (const region of regions)
|
|
23
|
+
if (region.length > best.length && (place === region || place.startsWith(`${region}/`)))
|
|
24
|
+
best = region;
|
|
25
|
+
return best;
|
|
26
|
+
}
|
|
27
|
+
const observationAt = (store, key) => storeAt(store, 'cas', 'observations', key);
|
|
28
|
+
export const keyOf = (by, region, pairs, resolution = '1') => sha(`${by}\n${region}\n${sha([...pairs].sort().join('\n'))}\n${resolution}`);
|
|
29
|
+
export const observed = (store, key) => existsSync(observationAt(store, key));
|
|
30
|
+
export function observation(store, key) {
|
|
31
|
+
return observeText(observationAt(store, key))?.split('\n').filter(isWire);
|
|
32
|
+
}
|
|
33
|
+
export function keepObservation(store, key, lines) {
|
|
34
|
+
const at = observationAt(store, key);
|
|
35
|
+
if (existsSync(at))
|
|
36
|
+
return;
|
|
37
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
38
|
+
writeFileSync(`${at}.${process.pid}`, `${[...new Set(lines)].sort().join('\n')}\n`);
|
|
39
|
+
renameSync(`${at}.${process.pid}`, at);
|
|
40
|
+
}
|
|
41
|
+
export function opened(store, speaker, region, alive) {
|
|
42
|
+
const places = new Map();
|
|
43
|
+
const keys = new Map();
|
|
44
|
+
const pointers = new Map();
|
|
45
|
+
const lines = shardLines(store, speaker, region);
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
if (OBSERVED.test(line))
|
|
48
|
+
places.set(`${readerOf(line) ?? ''} ${fieldOf(line, 'scope')}`, { at: fieldOf(line, 'at'), line });
|
|
49
|
+
else if (line.includes(' measure=observation ')) {
|
|
50
|
+
keys.set(readerOf(line) ?? '', fieldOf(line, 'value'));
|
|
51
|
+
pointers.set(readerOf(line) ?? '', line);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const live = [...lines.filter((line) => LEVEL.test(line)), ...[...places].filter(([name]) => alive.has(name.slice(0, name.indexOf(' ')))).map(([, one]) => one.line),
|
|
55
|
+
...[...pointers].filter(([by]) => alive.has(by)).map(([, line]) => line)];
|
|
56
|
+
if (lines.length > 2 * live.length + 500) {
|
|
57
|
+
const at = shardFile(store, speaker, region);
|
|
58
|
+
writeFileSync(`${at}.${process.pid}`, `${live.join('\n')}\n`);
|
|
59
|
+
renameSync(`${at}.${process.pid}`, at);
|
|
60
|
+
}
|
|
61
|
+
return { region, places, keys };
|
|
62
|
+
}
|
|
63
|
+
export const pointer = (speaker, by, region, key, pairs) => canonical({ scope: region || '.', role: 'writes', form: 'alphabet', measure: 'observation', value: key, by: `${speaker}:${by}`, at: `place:${sha([...pairs].sort().join('\n'))}` });
|
|
64
|
+
export function legacyOf(store, speaker, regions) {
|
|
65
|
+
if (!existsSync(writerFile(store, speaker)))
|
|
66
|
+
return undefined;
|
|
67
|
+
const latest = new Map();
|
|
68
|
+
const readings = new Map();
|
|
69
|
+
const level = new Map();
|
|
70
|
+
for (const line of ledgerLines(store, speaker)) {
|
|
71
|
+
if (OBSERVED.test(line))
|
|
72
|
+
latest.set(`${readerOf(line) ?? ''} ${fieldOf(line, 'scope')}`, line);
|
|
73
|
+
else if (LEVEL.test(line)) {
|
|
74
|
+
const region = regionIn(regions, fieldOf(line, 'scope').replace(/^(?:cost\/)?reader\//, ''));
|
|
75
|
+
level.set(region, [...(level.get(region) ?? []), line]);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const key = `${readerOf(line) ?? ''} ${fieldOf(line, 'at')}`;
|
|
79
|
+
readings.set(key, [...(readings.get(key) ?? []), line]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const shards = new Map(level);
|
|
83
|
+
for (const line of latest.values()) {
|
|
84
|
+
const region = regionIn(regions, fieldOf(line, 'scope'));
|
|
85
|
+
shards.set(region, [...(shards.get(region) ?? []), line]);
|
|
86
|
+
}
|
|
87
|
+
for (const [region, lines] of shards)
|
|
88
|
+
landShard(store, speaker, region, lines);
|
|
89
|
+
return readings;
|
|
90
|
+
}
|
|
91
|
+
export function setAside(store, speaker) {
|
|
92
|
+
const at = writerFile(store, speaker);
|
|
93
|
+
if (!existsSync(at))
|
|
94
|
+
return;
|
|
95
|
+
const bytes = observeFile(at) ?? new Uint8Array();
|
|
96
|
+
const kept = blobAt(store, bytesDigest(store, bytes));
|
|
97
|
+
mkdirSync(dirname(kept), { recursive: true });
|
|
98
|
+
renameSync(at, kept);
|
|
99
|
+
process.stderr.write(`SHARDED ${speaker} · one file of ${bytes.length} bytes laid out by region · kept as ${bytesDigest(store, bytes)}\n`);
|
|
100
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Lines told to a place's own lock for a preview: folded as if the file held them, and standing only as its signed lines do. */
|
|
2
|
+
export declare const tellOwn: (place: string, lines: readonly string[]) => void;
|
|
3
|
+
/**
|
|
4
|
+
* A place's own lock, folded as the tree folds its own: a line that carries a signature stands only when a key the
|
|
5
|
+
* tree's lock admits signed it, inside the window that key held at the epoch it signed and inside its coverage of the
|
|
6
|
+
* place, the line's scope read as a region of the place; a line that carries none is the place's target text until the
|
|
7
|
+
* place signs its lock, and never stands where only signed lines are asked for.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ownLockOf(root: string, place: string, signedOnly?: boolean): readonly string[];
|
|
10
|
+
/**
|
|
11
|
+
* What a place's own lock holds by signature — a key the tree admits signed the line, inside its window and its coverage
|
|
12
|
+
* of the place — withdrawal or not: whether one line stands there so, the lines that do, and how many were written into
|
|
13
|
+
* the lock rather than applied to it.
|
|
14
|
+
*/
|
|
15
|
+
export declare const admittedIn: (root: string, place: string, line: string) => boolean;
|
|
16
|
+
export declare const appliedIn: (root: string, place: string) => readonly string[];
|
|
17
|
+
export declare const forksIn: (root: string, place: string) => readonly string[];
|
|
18
|
+
export declare const writtenIn: (root: string, place: string) => number;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { join } from "../host/io.js";
|
|
2
|
+
import { region, within } from '@lapxo/obligations/field';
|
|
3
|
+
import { LOCK, parse, windowAt } from '@lapxo/topos/wire';
|
|
4
|
+
import { fieldOf, foldClaims, isWire, sayingOf } from "./claims.js";
|
|
5
|
+
import { signaturesOf } from "./digests.js";
|
|
6
|
+
import { lockLines, lockStanding } from "./keys.js";
|
|
7
|
+
import { wordOf } from "./wire.js";
|
|
8
|
+
import { authorityFor, rootSigner } from "./signers.js";
|
|
9
|
+
import { shardLines, storeOf } from "../land/ledger.js";
|
|
10
|
+
import { verifiesFields } from "../land/sign.js";
|
|
11
|
+
import { observeText } from "../observe/files.js";
|
|
12
|
+
const held = new Map();
|
|
13
|
+
const told = new Map();
|
|
14
|
+
/** Lines told to a place's own lock for a preview: folded as if the file held them, and standing only as its signed lines do. */
|
|
15
|
+
export const tellOwn = (place, lines) => void told.set(place, lines);
|
|
16
|
+
function verified(root, place) {
|
|
17
|
+
const store = storeOf(root);
|
|
18
|
+
const lock = lockLines(store);
|
|
19
|
+
const publicText = observeText(join(root, place, LOCK)) ?? '';
|
|
20
|
+
const hasAuthority = lock.some(line => fieldOf(line, 'sig') !== '');
|
|
21
|
+
const algorithms = hasAuthority ? signaturesOf(store).admitted : [];
|
|
22
|
+
const signers = algorithms.length ? authorityFor(lock, rootSigner(root), algorithms).admitted : [];
|
|
23
|
+
const history = signers.flatMap(signer => shardLines(store, signer.id, `${place}/${LOCK}`));
|
|
24
|
+
const text = [publicText, ...history, ...(told.get(place) ?? [])].join('\n');
|
|
25
|
+
const key = `${root} ${place}`;
|
|
26
|
+
const last = held.get(key);
|
|
27
|
+
if (last !== undefined && last.text === text && last.lock === lock)
|
|
28
|
+
return last;
|
|
29
|
+
const lines = [...new Set(text.split('\n').filter(isWire))];
|
|
30
|
+
const family = signers.length ? wordOf(lockStanding(store), 'families', 'region') : '';
|
|
31
|
+
const stands = (line) => {
|
|
32
|
+
const got = parse(line);
|
|
33
|
+
if (got.kind !== 'fact' || got.value.fields['sig'] === undefined)
|
|
34
|
+
return false;
|
|
35
|
+
const fields = got.value.fields;
|
|
36
|
+
const signer = signers.find((one) => one.id === fields['by']);
|
|
37
|
+
const epoch = Number(fields['epoch']) || 0;
|
|
38
|
+
if (signer === undefined || !verifiesFields(fields, signer.publicKey, algorithms))
|
|
39
|
+
return false;
|
|
40
|
+
const window = windowAt(signer, epoch);
|
|
41
|
+
return epoch >= window.start && (window.close === null || epoch <= window.close)
|
|
42
|
+
&& signer.coverage.some((prefix) => prefix === '*' || within(region(`${family}/${place}/${fields['scope'] ?? ''}`, './'), region(prefix, './')));
|
|
43
|
+
};
|
|
44
|
+
const applied = new Set(lines.filter(stands));
|
|
45
|
+
// A published copy of an authenticated delivery is not another inscription.
|
|
46
|
+
const projected = new Set([...applied].map(sayingOf));
|
|
47
|
+
const got = { text, lock, lines, applied, projected, folded: new Map() };
|
|
48
|
+
held.set(key, got);
|
|
49
|
+
return got;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A place's own lock, folded as the tree folds its own: a line that carries a signature stands only when a key the
|
|
53
|
+
* tree's lock admits signed it, inside the window that key held at the epoch it signed and inside its coverage of the
|
|
54
|
+
* place, the line's scope read as a region of the place; a line that carries none is the place's target text until the
|
|
55
|
+
* place signs its lock, and never stands where only signed lines are asked for.
|
|
56
|
+
*/
|
|
57
|
+
export function ownLockOf(root, place, signedOnly = false) {
|
|
58
|
+
const got = verified(root, place);
|
|
59
|
+
const kept = got.folded.get(signedOnly);
|
|
60
|
+
if (kept !== undefined)
|
|
61
|
+
return kept;
|
|
62
|
+
const standing = foldClaims(got.lines.filter((line) => got.applied.has(line) || (!signedOnly && !/ sig=/.test(line) && !got.projected.has(sayingOf(line))))).standing
|
|
63
|
+
.filter((line) => fieldOf(line, 'value') !== 'withdraw');
|
|
64
|
+
got.folded.set(signedOnly, standing);
|
|
65
|
+
return standing;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* What a place's own lock holds by signature — a key the tree admits signed the line, inside its window and its coverage
|
|
69
|
+
* of the place — withdrawal or not: whether one line stands there so, the lines that do, and how many were written into
|
|
70
|
+
* the lock rather than applied to it.
|
|
71
|
+
*/
|
|
72
|
+
export const admittedIn = (root, place, line) => verified(root, place).applied.has(line);
|
|
73
|
+
export const appliedIn = (root, place) => [...verified(root, place).applied];
|
|
74
|
+
export const forksIn = (root, place) => ((got) => foldClaims(got.lines.filter((line) => got.applied.has(line) || (!/ sig=/.test(line) && !got.projected.has(sayingOf(line))))).forks.map((fork) => fork.key))(verified(root, place));
|
|
75
|
+
export const writtenIn = (root, place) => ((got) => got.lines.filter((line) => !got.applied.has(line)).length)(verified(root, place));
|