@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,44 @@
|
|
|
1
|
+
import type { CoordinateRole } from './wire.ts';
|
|
2
|
+
import { capsulesFor } from './reach.ts';
|
|
3
|
+
import type { Kind } from '../observe/run.ts';
|
|
4
|
+
export interface ReaderClaim {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly shape: readonly string[];
|
|
7
|
+
readonly where: readonly string[];
|
|
8
|
+
readonly module: string;
|
|
9
|
+
readonly kind: Kind;
|
|
10
|
+
readonly capsule?: {
|
|
11
|
+
readonly digest: string;
|
|
12
|
+
readonly entry: string;
|
|
13
|
+
readonly region: string;
|
|
14
|
+
readonly reads: readonly string[];
|
|
15
|
+
readonly globs: ReadonlyMap<string, readonly string[]>;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export type Offer = {
|
|
19
|
+
readonly place: string;
|
|
20
|
+
readonly seen: string;
|
|
21
|
+
readonly bytes: () => Uint8Array;
|
|
22
|
+
readonly digest?: () => string;
|
|
23
|
+
};
|
|
24
|
+
export type Look = {
|
|
25
|
+
readonly under: (dir: string, pruned: boolean) => readonly string[];
|
|
26
|
+
readonly stamp: (at: string) => string;
|
|
27
|
+
};
|
|
28
|
+
export declare function lookOf(root: string, roleOf: (coordinate: string) => CoordinateRole): Look;
|
|
29
|
+
/**
|
|
30
|
+
* The reader regions of the capsules a place can run, where the place's own lock offers them or, at a world, the wire's offers/world list does for a
|
|
31
|
+
* region its own lock does not name: `region/<name>` under a measure the wire's region-measures list names gives the region and its coordinates, and the first capsule, pinned before carried, that writes the region
|
|
32
|
+
* reads them, handed the coordinates it declares it reads among those the place offers. A capsule region takes over what a
|
|
33
|
+
* place's declared region. A standing reader for that same provider and region keeps ownership, so nothing is read twice.
|
|
34
|
+
* A newly declared region does not need a retired reader in private history to be observed.
|
|
35
|
+
*/
|
|
36
|
+
export declare function capsuleReadersOf(root: string, store: string, standing: readonly string[], lines: readonly ReaderClaim[], resolve?: typeof capsulesFor): readonly ReaderClaim[];
|
|
37
|
+
export declare function handedAt(root: string, reader: ReaderClaim, roleOf: (coordinate: string) => CoordinateRole, look: Look, place: string): readonly string[];
|
|
38
|
+
/**
|
|
39
|
+
* What a reader is offered at the places it reads: a coordinate for a reader of coordinates, the listing for one that reads a
|
|
40
|
+
* directory, and for one that runs over its place its sources and every coordinate its shape names besides — a coordinate a view
|
|
41
|
+
* writes from what readers said is offered only when named, so no reading moves its own offer — each by the stamp it
|
|
42
|
+
* was seen at and the bytes that key what it said; a capsule reader is offered the coordinates it is handed.
|
|
43
|
+
*/
|
|
44
|
+
export declare function offersOf(root: string, store: string, reader: ReaderClaim, roleOf: (coordinate: string) => CoordinateRole, look: Look, into: (where: string) => boolean): readonly Offer[];
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { resolve } from "../host/io.js";
|
|
2
|
+
import { canonical, fields, matches } from '@lapxo/topos/wire';
|
|
3
|
+
import { namesAt, wordOn, wordsOf, wireLine } from "./wire.js";
|
|
4
|
+
import { fits } from "../observe/match.js";
|
|
5
|
+
import { bytesDigest, coordinateDigest } from "./digests.js";
|
|
6
|
+
import { fieldOf } from "./claims.js";
|
|
7
|
+
import { capsuleAt, placesIn } from "./places.js";
|
|
8
|
+
import { capsulesFor } from "./reach.js";
|
|
9
|
+
import { ownLockOf } from "./signed.js";
|
|
10
|
+
import { ownLock } from "../observe/runner.js";
|
|
11
|
+
import { entriesIn, coordinatesUnder, observeFile, stampOf } from "../observe/files.js";
|
|
12
|
+
export function lookOf(root, roleOf) {
|
|
13
|
+
const walked = new Map();
|
|
14
|
+
const stamped = new Map();
|
|
15
|
+
return {
|
|
16
|
+
under: (dir, pruned) => walked.get(`${pruned} ${dir}`) ?? walked.set(`${pruned} ${dir}`, coordinatesUnder(dir, root, pruned ? (one) => roleOf(one) !== 'foreign' : undefined)).get(`${pruned} ${dir}`),
|
|
17
|
+
stamp: (at) => stamped.get(at) ?? stamped.set(at, stampOf(at)?.stamp ?? '').get(at),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function entriesOf(root, where, roleOf) {
|
|
21
|
+
const dir = where.replace(/\/?$/, '/');
|
|
22
|
+
const at = resolve(root, dir);
|
|
23
|
+
if (stampOf(at)?.kind !== 'dir')
|
|
24
|
+
return undefined;
|
|
25
|
+
const names = entriesIn(at)
|
|
26
|
+
.filter((entry) => roleOf(`${dir}${entry.name}`) === 'source')
|
|
27
|
+
.map((entry) => (entry.dir ? `${entry.name}/` : entry.name))
|
|
28
|
+
.sort();
|
|
29
|
+
return new TextEncoder().encode(names.join('\n'));
|
|
30
|
+
}
|
|
31
|
+
const reachOfLine = (module, where) => {
|
|
32
|
+
const [pkg = '', ...rest] = module.split('/');
|
|
33
|
+
const stem = (rest.at(-1) ?? '').replace(/\.[^.]+$/, '');
|
|
34
|
+
return where.flatMap((one) => [`${pkg} ${stem} ${one.replace(/\/?$/, '/')}`, `${pkg} ${stem}s ${one.replace(/\/?$/, '/')}`]);
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The reader regions of the capsules a place can run, where the place's own lock offers them or, at a world, the wire's offers/world list does for a
|
|
38
|
+
* region its own lock does not name: `region/<name>` under a measure the wire's region-measures list names gives the region and its coordinates, and the first capsule, pinned before carried, that writes the region
|
|
39
|
+
* reads them, handed the coordinates it declares it reads among those the place offers. A capsule region takes over what a
|
|
40
|
+
* place's declared region. A standing reader for that same provider and region keeps ownership, so nothing is read twice.
|
|
41
|
+
* A newly declared region does not need a retired reader in private history to be observed.
|
|
42
|
+
*/
|
|
43
|
+
export function capsuleReadersOf(root, store, standing, lines, resolve = capsulesFor) {
|
|
44
|
+
const reaching = new Set(lines.flatMap((reader) => reachOfLine(reader.module, reader.where)));
|
|
45
|
+
const measures = wordsOf(standing, 'region-measures');
|
|
46
|
+
const world = fields(fieldOf(wireLine(standing, 'offers/world') ?? '', 'value')).map(([region, globs]) => canonical({ scope: `region/${region}`, measure: wordOn(standing, 'region-coordinate', ownLock()), value: globs }));
|
|
47
|
+
const groups = new Map();
|
|
48
|
+
for (const place of ['', ...placesIn(root)]) {
|
|
49
|
+
const where = place ? `${place}/` : '';
|
|
50
|
+
const own = ownLockOf(root, place).filter((line) => fieldOf(line, 'scope').startsWith('region/') && measures.includes(fieldOf(line, 'measure')) && fieldOf(line, 'value') !== 'withdraw');
|
|
51
|
+
const offered = [...own, ...(capsuleAt(root, place) ? world.filter((line) => !own.some((one) => fieldOf(one, 'scope') === fieldOf(line, 'scope'))) : [])];
|
|
52
|
+
if (!offered.length)
|
|
53
|
+
continue;
|
|
54
|
+
const capsules = resolve(standing, place, root);
|
|
55
|
+
for (const line of offered) {
|
|
56
|
+
const region = fieldOf(line, 'scope').slice('region/'.length);
|
|
57
|
+
const by = capsules.find(({ capsule }) => capsule.lines.some((one) => fieldOf(one, 'scope') === `region/${region}` && fieldOf(one, 'measure') === 'writes'));
|
|
58
|
+
const pkg = fieldOf(by?.offer ?? '', 'scope').split('/')[1] ?? '';
|
|
59
|
+
if (by === undefined || reaching.has(`${pkg} ${region} ${where || '/'}`))
|
|
60
|
+
continue;
|
|
61
|
+
const key = `${by.capsule.digest} ${region}`;
|
|
62
|
+
const group = groups.get(key) ?? groups.set(key, { module: `${pkg}/${region}`, digest: by.capsule.digest, entry: fieldOf(by.offer, 'shape'), region, reads: by.capsule.declaration.regions[region] ?? [], globs: new Map() }).get(key);
|
|
63
|
+
group.globs.set(where, fieldOf(line, 'value').split('|').filter(Boolean));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return [...groups.values()].map((group) => ({ id: group.module, shape: ['run'], where: [...group.globs.keys()], module: group.module, kind: 'process',
|
|
67
|
+
capsule: { digest: group.digest, entry: group.entry, region: group.region, reads: group.reads, globs: group.globs } }));
|
|
68
|
+
}
|
|
69
|
+
export function handedAt(root, reader, roleOf, look, place) {
|
|
70
|
+
const { reads, globs } = reader.capsule;
|
|
71
|
+
const run = reads.some((read) => read.includes('@'));
|
|
72
|
+
return look.under(resolve(root, place), true).filter((coordinate) => {
|
|
73
|
+
const inside = coordinate.slice(place.length);
|
|
74
|
+
return (!run || roleOf(coordinate) === 'source') && reads.some((read) => fits(read, inside)) && (globs.get(place) ?? []).some((glob) => matches(glob, inside));
|
|
75
|
+
}).sort();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* What a reader is offered at the places it reads: a coordinate for a reader of coordinates, the listing for one that reads a
|
|
79
|
+
* directory, and for one that runs over its place its sources and every coordinate its shape names besides — a coordinate a view
|
|
80
|
+
* writes from what readers said is offered only when named, so no reading moves its own offer — each by the stamp it
|
|
81
|
+
* was seen at and the bytes that key what it said; a capsule reader is offered the coordinates it is handed.
|
|
82
|
+
*/
|
|
83
|
+
export function offersOf(root, store, reader, roleOf, look, into) {
|
|
84
|
+
const offered = [];
|
|
85
|
+
const wheres = reader.where.filter(into);
|
|
86
|
+
if (reader.capsule !== undefined && reader.capsule.reads.some((read) => read.includes('@'))) {
|
|
87
|
+
for (const where of wheres) {
|
|
88
|
+
const held = handedAt(root, reader, roleOf, look, where);
|
|
89
|
+
const stamps = held.map((coordinate) => `${coordinate} ${look.stamp(resolve(root, coordinate))}`).join('\n');
|
|
90
|
+
offered.push({
|
|
91
|
+
place: where,
|
|
92
|
+
seen: bytesDigest(store, new TextEncoder().encode(stamps)).replace(/^[^:]*:/, '').slice(0, 16),
|
|
93
|
+
bytes: () => new TextEncoder().encode(held.map((coordinate) => `${coordinate} ${coordinateDigest(store, resolve(root, coordinate)) ?? bytesDigest(store, new Uint8Array())}`).join('\n')),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else if (reader.capsule !== undefined) {
|
|
98
|
+
for (const coordinate of [...new Set(wheres.flatMap((where) => handedAt(root, reader, roleOf, look, where)))].sort()) {
|
|
99
|
+
offered.push({ place: coordinate, seen: look.stamp(resolve(root, coordinate)), bytes: () => observeFile(resolve(root, coordinate)) ?? new Uint8Array(), digest: () => coordinateDigest(store, resolve(root, coordinate)) ?? bytesDigest(store, new Uint8Array()) });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (reader.kind === 'process') {
|
|
103
|
+
for (const where of wheres) {
|
|
104
|
+
const dir = resolve(root, where);
|
|
105
|
+
if (stampOf(dir)?.kind !== 'dir')
|
|
106
|
+
continue;
|
|
107
|
+
const held = look.under(dir, true).filter((coordinate) => roleOf(coordinate) === 'source' || (roleOf(coordinate) !== 'foreign' && reader.shape.some((name) => namesAt(coordinate, name) || fits(name, coordinate)))).sort();
|
|
108
|
+
const stamps = held.map((coordinate) => `${coordinate} ${look.stamp(resolve(root, coordinate))}`).join('\n');
|
|
109
|
+
offered.push({
|
|
110
|
+
place: where.replace(/\/?$/, '/'),
|
|
111
|
+
seen: bytesDigest(store, new TextEncoder().encode(stamps)).replace(/^[^:]*:/, '').slice(0, 16),
|
|
112
|
+
bytes: () => new TextEncoder().encode(held.map((coordinate) => `${coordinate} ${coordinateDigest(store, resolve(root, coordinate)) ?? bytesDigest(store, new Uint8Array())}`).join('\n')),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (reader.shape.some((name) => name.endsWith('/'))) {
|
|
117
|
+
for (const where of wheres) {
|
|
118
|
+
const listed = entriesOf(root, where, roleOf);
|
|
119
|
+
if (listed)
|
|
120
|
+
offered.push({ place: where.replace(/\/?$/, '/'), seen: bytesDigest(store, listed).replace(/^[^:]*:/, '').slice(0, 16), bytes: () => listed });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const paths = wheres.flatMap((where) => look.under(resolve(root, where), true));
|
|
125
|
+
for (const path of [...new Set(paths)].sort()) {
|
|
126
|
+
if (!reader.shape.some((name) => namesAt(path, name)))
|
|
127
|
+
continue;
|
|
128
|
+
offered.push({ place: path, seen: look.stamp(resolve(root, path)), bytes: () => observeFile(resolve(root, path)) ?? new Uint8Array(), digest: () => coordinateDigest(store, resolve(root, path)) ?? bytesDigest(store, new Uint8Array()) });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return offered;
|
|
132
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** A receipt as the fold sees it: the scope it paid and the coordinates it moved. */
|
|
2
|
+
interface Receipt {
|
|
3
|
+
readonly scope: string;
|
|
4
|
+
readonly digest: string;
|
|
5
|
+
readonly moved: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function receiptsIn(store: string, scopes?: readonly string[]): readonly Receipt[];
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { join } from "../host/io.js";
|
|
2
|
+
import { wordOf } from "./wire.js";
|
|
3
|
+
import { ledgerLines, storeAt } from "../land/ledger.js";
|
|
4
|
+
import { lockStanding } from "./keys.js";
|
|
5
|
+
import { fieldOf } from "./claims.js";
|
|
6
|
+
import { entriesIn, exists, observeText } from "../observe/files.js";
|
|
7
|
+
/** The writer of the index that names, per scope, the receipts judged before the ledger existed. */
|
|
8
|
+
/** The receipt files the session index names for these scopes; without scopes, every receipt file. The index is a ledger. */
|
|
9
|
+
function receiptNames(store, dir, scopes) {
|
|
10
|
+
if (!scopes)
|
|
11
|
+
return entriesIn(dir).map((entry) => entry.name);
|
|
12
|
+
const wanted = new Set(ledgerLines(store, wordOf(lockStanding(store), 'families', 'session'))
|
|
13
|
+
.filter((line) => scopes.includes(fieldOf(line, 'scope')))
|
|
14
|
+
.map((line) => fieldOf(line, 'value').replace(/^sha256:/, '')));
|
|
15
|
+
return [...wanted].flatMap((hex) => [`${hex}.json`, `sha256:${hex}.json`]).filter((name) => exists(join(dir, name)));
|
|
16
|
+
}
|
|
17
|
+
export function receiptsIn(store, scopes) {
|
|
18
|
+
const out = [];
|
|
19
|
+
const dir = storeAt(store, 'cas', 'receipts');
|
|
20
|
+
for (const name of receiptNames(store, dir, scopes)) {
|
|
21
|
+
if (!/^(sha256:)?[0-9a-f]{64}\.json$/.test(name))
|
|
22
|
+
continue;
|
|
23
|
+
const parsed = JSON.parse(observeText(join(dir, name)) ?? '{}');
|
|
24
|
+
const r = parsed.receipt;
|
|
25
|
+
if (r?.kind !== 'fact')
|
|
26
|
+
continue;
|
|
27
|
+
const floor = typeof r.floor === 'string' ? r.floor : r.floor?.scope ?? '';
|
|
28
|
+
if (!floor)
|
|
29
|
+
continue;
|
|
30
|
+
const moved = Array.isArray(r.moved) ? r.moved.filter((m) => typeof m === 'string') : [];
|
|
31
|
+
out.push({ scope: floor.replace(/\.(status|count|id|ms|order|kind)$/, ''), digest: name.replace(/\.json$/, ''), moved });
|
|
32
|
+
}
|
|
33
|
+
for (const line of ledgerLines(store, 'judge')) {
|
|
34
|
+
const at = fieldOf(line, 'at');
|
|
35
|
+
if (fieldOf(line, 'value') !== 'present' || !at.startsWith('receipt:'))
|
|
36
|
+
continue;
|
|
37
|
+
if (scopes && !scopes.includes(fieldOf(line, 'scope')))
|
|
38
|
+
continue;
|
|
39
|
+
const digest = at.slice('receipt:'.length);
|
|
40
|
+
const envelope = JSON.parse(observeText(storeAt(store, 'cas', 'envelopes', `${digest.slice(digest.indexOf(':') + 1)}.json`)) ?? '{}');
|
|
41
|
+
const before = envelope.before ?? {};
|
|
42
|
+
const after = envelope.after ?? {};
|
|
43
|
+
const moved = [...new Set([...Object.keys(before), ...Object.keys(after)])].filter((coordinate) => before[coordinate] !== after[coordinate]);
|
|
44
|
+
out.push({ scope: fieldOf(line, 'scope'), digest, moved });
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Interval } from '@lapxo/obligations/forms';
|
|
2
|
+
/**
|
|
3
|
+
* A region is a prefix of the place, read at a resolution: the prefixes a lock already names — the steps a role, a
|
|
4
|
+
* directory or a need is given at — are its regions, a cell belongs to the longest of them that holds it, and a region
|
|
5
|
+
* rests on the region its prefix extends. A signature or a requirement is a line at the region its scope names, never a
|
|
6
|
+
* value copied into the cells there; a cell's poles are the meet of the live bounds on its chain of rest, met in the
|
|
7
|
+
* lattice of their form. Each region carries a summary of what lies in it, summed over the regions that rest on it —
|
|
8
|
+
* cells, the sums and the values they read at the lattice's own resolution, the bounds signed at it — so what a bound
|
|
9
|
+
* would bite is read off a summary without visiting a cell, and two readings of one place differ exactly in the regions
|
|
10
|
+
* whose summaries differ. Nothing here is kept between folds; a region is read from the lines that stand and the cells seen.
|
|
11
|
+
*/
|
|
12
|
+
export interface Bound {
|
|
13
|
+
readonly line: string;
|
|
14
|
+
readonly question: string;
|
|
15
|
+
readonly region: string;
|
|
16
|
+
readonly measure: string;
|
|
17
|
+
readonly form: string;
|
|
18
|
+
readonly value: string;
|
|
19
|
+
}
|
|
20
|
+
export interface Regions {
|
|
21
|
+
readonly named: readonly string[];
|
|
22
|
+
readonly bounds: readonly Bound[];
|
|
23
|
+
readonly summaries: ReadonlyMap<string, {
|
|
24
|
+
readonly region: string;
|
|
25
|
+
readonly cells: number;
|
|
26
|
+
readonly sums: ReadonlyMap<string, number>;
|
|
27
|
+
readonly values: ReadonlyMap<string, ReadonlyMap<string, number>>;
|
|
28
|
+
readonly bounds: readonly Bound[];
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
export declare function regionsNamed(standing: readonly string[]): readonly string[];
|
|
32
|
+
export declare function boundOf(named: readonly string[], line: string): Bound;
|
|
33
|
+
/**
|
|
34
|
+
* The poles of a region, question by question: each live bound is signed at the region it names and travels by rest, as
|
|
35
|
+
* the object signs, so the ceiling is what the region reads up its chain of rest, the meet in the lattice of their form, and beside it what the region's cells hold under that question,
|
|
36
|
+
* value by count. Read off the summaries alone; no cell is visited.
|
|
37
|
+
*/
|
|
38
|
+
export declare function polesOf(regions: Regions, region: string): ReadonlyMap<string, {
|
|
39
|
+
readonly ceiling: Interval | readonly string[] | undefined;
|
|
40
|
+
readonly held: ReadonlyMap<string, number> | undefined;
|
|
41
|
+
}>;
|
|
42
|
+
export declare function regionsOf(standing: readonly string[], ceilings: readonly string[], cells: readonly {
|
|
43
|
+
readonly question: string;
|
|
44
|
+
readonly measure: string;
|
|
45
|
+
readonly value: string;
|
|
46
|
+
readonly place: string;
|
|
47
|
+
}[]): Regions;
|
|
48
|
+
/**
|
|
49
|
+
* What a bound bites, read off the summary of its region and never off a cell: the values its region's cells read that
|
|
50
|
+
* lie outside it, and of those the ones every bound above it still admits. A question finer than a summary is not asked.
|
|
51
|
+
*/
|
|
52
|
+
export declare function rankOf(regions: Regions, bound: Bound, own?: Readonly<Record<string, number>>): {
|
|
53
|
+
readonly bars: number;
|
|
54
|
+
readonly cells: number;
|
|
55
|
+
readonly fresh: number;
|
|
56
|
+
readonly met: number;
|
|
57
|
+
readonly sums: number;
|
|
58
|
+
};
|
|
59
|
+
export declare function finerOf(regions: Regions): ReadonlyMap<string, readonly string[]>;
|
|
60
|
+
export declare function digestsOf(regions: Regions): ReadonlyMap<string, string>;
|
|
61
|
+
export declare function differing(a: Regions, b: Regions): {
|
|
62
|
+
readonly regions: readonly string[];
|
|
63
|
+
readonly compared: number;
|
|
64
|
+
};
|
|
65
|
+
export interface Packed {
|
|
66
|
+
readonly named: readonly string[];
|
|
67
|
+
readonly bounds: readonly string[];
|
|
68
|
+
readonly summaries: readonly (readonly [string, number, readonly (readonly [string, number])[], readonly (readonly [string, readonly (readonly [string, number])[]])[]])[];
|
|
69
|
+
}
|
|
70
|
+
export declare const packRegions: (regions: Regions) => Packed;
|
|
71
|
+
export declare function unpackRegions(packed: Packed): Regions;
|
|
72
|
+
export declare function unlikeReplay(regions: Regions, red: ReadonlySet<string>, own: Readonly<Record<string, number>>): number;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { createHash } from "../host/io.js";
|
|
2
|
+
import { alphabetForm } from '@lapxo/obligations/forms';
|
|
3
|
+
import { cell, parts, sign } from '@lapxo/obligations/views/field';
|
|
4
|
+
import { point, region as coordinates, within } from '@lapxo/obligations/field';
|
|
5
|
+
import { matches, parse, roleClaimsOf } from '@lapxo/topos/wire';
|
|
6
|
+
import { fieldOf } from "./claims.js";
|
|
7
|
+
import { INTERVALS, spanOf } from '@lapxo/topos/forms';
|
|
8
|
+
const trimmed = (prefix) => prefix.replace(/(?:\/\*\*|\/\*|\/)+$/, '');
|
|
9
|
+
const globbed = (prefix) => prefix.includes('*');
|
|
10
|
+
export function regionsNamed(standing) {
|
|
11
|
+
const out = new Set(['']);
|
|
12
|
+
for (const line of standing) {
|
|
13
|
+
if (fieldOf(line, 'value') === 'withdraw')
|
|
14
|
+
continue;
|
|
15
|
+
const scope = fieldOf(line, 'scope');
|
|
16
|
+
const got = ['role', 'paths'].includes(fieldOf(line, 'measure')) ? parse(line) : undefined;
|
|
17
|
+
const roled = got?.kind === 'fact' ? roleClaimsOf([got.value.fields]).map((claim) => claim.name) : [];
|
|
18
|
+
const said = roled.length ? roled
|
|
19
|
+
: scope.startsWith('dir/') ? [scope.slice('dir/'.length)]
|
|
20
|
+
: scope.startsWith('region/') && fieldOf(line, 'res') ? [fieldOf(line, 'value')] : [];
|
|
21
|
+
for (const prefix of [...said, ...fieldOf(line, 'needs').split('|')].map(trimmed))
|
|
22
|
+
if (prefix && !globbed(prefix))
|
|
23
|
+
out.add(prefix);
|
|
24
|
+
}
|
|
25
|
+
return [...out].sort();
|
|
26
|
+
}
|
|
27
|
+
function restsOn(named, prefix) {
|
|
28
|
+
let best = '';
|
|
29
|
+
for (const one of named)
|
|
30
|
+
if (one.length > best.length && one !== prefix && prefix.startsWith(`${one}/`))
|
|
31
|
+
best = one;
|
|
32
|
+
return best;
|
|
33
|
+
}
|
|
34
|
+
function regionOf(named, place) {
|
|
35
|
+
const at = trimmed(place);
|
|
36
|
+
let best = '';
|
|
37
|
+
for (const one of named)
|
|
38
|
+
if (one.length > best.length && (at === one || at.startsWith(`${one}/`)))
|
|
39
|
+
best = one;
|
|
40
|
+
return best;
|
|
41
|
+
}
|
|
42
|
+
const names = (named, prefix) => (globbed(prefix) ? named.some((one) => matches(prefix, one)) : named.includes(prefix));
|
|
43
|
+
export function boundOf(named, line) {
|
|
44
|
+
const scope = fieldOf(line, 'scope');
|
|
45
|
+
const steps = scope.split('/');
|
|
46
|
+
const of = (question, region) => ({ line, question, region, measure: fieldOf(line, 'measure'), form: fieldOf(line, 'form'), value: fieldOf(line, 'value') });
|
|
47
|
+
for (let i = 1; i < steps.length; i += 1) {
|
|
48
|
+
const tail = trimmed(steps.slice(i).join('/'));
|
|
49
|
+
if (tail && names(named, tail))
|
|
50
|
+
return of(steps.slice(0, i).join('/'), tail);
|
|
51
|
+
}
|
|
52
|
+
for (let j = steps.length - 1; j > 0; j -= 1) {
|
|
53
|
+
const head = steps.slice(0, j).join('/');
|
|
54
|
+
if (names(named, head))
|
|
55
|
+
return of(steps.slice(j).join('/'), head);
|
|
56
|
+
}
|
|
57
|
+
return of(scope, '');
|
|
58
|
+
}
|
|
59
|
+
function poleAt(named, region, bounds) {
|
|
60
|
+
const chain = [];
|
|
61
|
+
for (let at = region; !chain.includes(at); at = restsOn(named, at))
|
|
62
|
+
chain.push(at);
|
|
63
|
+
const signedIn = (L, spans) => {
|
|
64
|
+
let world = new Map(chain.map((_, i) => [`c${i}`, cell(`c${i}`, 0, [], i + 1 < chain.length ? [`c${i + 1}`] : [])]));
|
|
65
|
+
for (const [one, span] of spans)
|
|
66
|
+
world = sign(L, world, `c${Math.max(0, chain.indexOf(one.region))}`, span);
|
|
67
|
+
return parts(L, world.get('c0'), world).signed;
|
|
68
|
+
};
|
|
69
|
+
const spans = bounds.filter((one) => one.form === 'interval').flatMap((one) => ((got) => (got ? [[one, got]] : []))(spanOf(one.value)));
|
|
70
|
+
if (spans.length)
|
|
71
|
+
return signedIn(INTERVALS, spans);
|
|
72
|
+
const alphabets = bounds.filter((one) => one.form === 'alphabet');
|
|
73
|
+
if (!alphabets.length)
|
|
74
|
+
return undefined;
|
|
75
|
+
const words = (value) => (value.startsWith('not:') ? value.slice(4) : value).split('|').filter((one) => one && one !== 'none');
|
|
76
|
+
const tokens = [...new Set(['', ...alphabets.flatMap((one) => words(one.value))])].sort();
|
|
77
|
+
const mask = (value) => alphabetForm.parse({ tokens }, {
|
|
78
|
+
want: value.startsWith('not:') ? tokens.filter((one) => !words(value).includes(one)) : words(value),
|
|
79
|
+
});
|
|
80
|
+
const held = signedIn(alphabetForm.lattice({ tokens }), alphabets.map((one) => [one, mask(one.value)])).members(tokens);
|
|
81
|
+
return held.includes('') ? tokens.filter((one) => one !== '' && !held.includes(one)).map((one) => `not:${one}`) : held;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The poles of a region, question by question: each live bound is signed at the region it names and travels by rest, as
|
|
85
|
+
* the object signs, so the ceiling is what the region reads up its chain of rest, the meet in the lattice of their form, and beside it what the region's cells hold under that question,
|
|
86
|
+
* value by count. Read off the summaries alone; no cell is visited.
|
|
87
|
+
*/
|
|
88
|
+
export function polesOf(regions, region) {
|
|
89
|
+
const chain = [];
|
|
90
|
+
for (let at = region;; at = restsOn(regions.named, at)) {
|
|
91
|
+
chain.push(at);
|
|
92
|
+
if (at === '')
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
const asked = new Map();
|
|
96
|
+
for (const bound of regions.bounds.filter((one) => chain.includes(one.region))) {
|
|
97
|
+
const key = `${bound.question} ${bound.measure}`;
|
|
98
|
+
asked.set(key, [...(asked.get(key) ?? []), bound]);
|
|
99
|
+
}
|
|
100
|
+
const summary = regions.summaries.get(region);
|
|
101
|
+
const heldAt = (question, measure) => {
|
|
102
|
+
const out = new Map();
|
|
103
|
+
for (const [key, bar] of summary?.values ?? []) {
|
|
104
|
+
const [asked_, at] = [key.slice(0, key.lastIndexOf(' ')), key.slice(key.lastIndexOf(' ') + 1)];
|
|
105
|
+
if (at !== measure || !within(point(asked_), coordinates(question)))
|
|
106
|
+
continue;
|
|
107
|
+
for (const [value, count] of bar)
|
|
108
|
+
out.set(value, (out.get(value) ?? 0) + count);
|
|
109
|
+
}
|
|
110
|
+
return out.size ? out : undefined;
|
|
111
|
+
};
|
|
112
|
+
return new Map([...asked].map(([key, bounds]) => [key, { ceiling: poleAt(regions.named, region, bounds), held: heldAt(bounds[0].question, bounds[0].measure) }]));
|
|
113
|
+
}
|
|
114
|
+
export function regionsOf(standing, ceilings, cells) {
|
|
115
|
+
const named = regionsNamed(standing);
|
|
116
|
+
const bounds = ceilings.map((line) => boundOf(named, line));
|
|
117
|
+
const summaries = new Map();
|
|
118
|
+
const at = (region) => summaries.get(region)
|
|
119
|
+
?? summaries.set(region, { region, cells: 0, sums: new Map(), values: new Map(), bounds: [] }).get(region);
|
|
120
|
+
for (const region of [...named, ...bounds.map((one) => one.region)])
|
|
121
|
+
at(region);
|
|
122
|
+
for (const bound of bounds)
|
|
123
|
+
at(bound.region).bounds.push(bound);
|
|
124
|
+
const chains = new Map();
|
|
125
|
+
const chainOf = (region) => chains.get(region) ?? chains.set(region, region === '' ? [''] : [region, ...chainOf(restsOn(named, region))]).get(region);
|
|
126
|
+
const globs = [...summaries.keys()].filter(globbed);
|
|
127
|
+
const placed = new Map();
|
|
128
|
+
for (const cell of cells) {
|
|
129
|
+
const key = `${cell.question} ${cell.measure}`;
|
|
130
|
+
const span = /^(-?\d+(?:\.\d+)?)\.\./.exec(cell.value);
|
|
131
|
+
const held = placed.get(`${cell.place}\u0000${key}`) ?? placed.set(`${cell.place}\u0000${key}`, { place: cell.place, key, ids: new Set() }).get(`${cell.place}\u0000${key}`);
|
|
132
|
+
if (span)
|
|
133
|
+
held.sum = (held.sum ?? 0) + Number(span[1]);
|
|
134
|
+
else
|
|
135
|
+
for (const one of cell.value.split('|'))
|
|
136
|
+
if (one && one !== 'none')
|
|
137
|
+
held.ids.add(one);
|
|
138
|
+
}
|
|
139
|
+
for (const { place, key, sum, ids } of placed.values()) {
|
|
140
|
+
const home = regionOf(named, place);
|
|
141
|
+
const reached = [...chainOf(home), ...globs.filter((glob) => chainOf(home).some((one) => one && matches(glob, one)))];
|
|
142
|
+
for (const region of new Set(reached)) {
|
|
143
|
+
const summary = at(region);
|
|
144
|
+
summary.cells += 1;
|
|
145
|
+
if (sum !== undefined)
|
|
146
|
+
summary.sums.set(key, (summary.sums.get(key) ?? 0) + sum);
|
|
147
|
+
const bars = summary.values.get(key) ?? summary.values.set(key, new Map()).get(key);
|
|
148
|
+
for (const one of sum !== undefined ? [String(sum)] : ids)
|
|
149
|
+
bars.set(one, (bars.get(one) ?? 0) + 1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { named, bounds, summaries };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* What a bound bites, read off the summary of its region and never off a cell: the values its region's cells read that
|
|
156
|
+
* lie outside it, and of those the ones every bound above it still admits. A question finer than a summary is not asked.
|
|
157
|
+
*/
|
|
158
|
+
export function rankOf(regions, bound, own = {}) {
|
|
159
|
+
const mine_ = own[bound.measure] ?? (bound.measure === 'count' ? own[fieldOf(bound.line, 'scope')] : undefined);
|
|
160
|
+
if (mine_ !== undefined) {
|
|
161
|
+
const span = poleAt(regions.named, bound.region, [bound]);
|
|
162
|
+
const off = span !== undefined && !Array.isArray(span) && (mine_ < span.lo || mine_ > span.hi) ? 1 : 0;
|
|
163
|
+
return { bars: off, cells: 0, fresh: off, met: 1, sums: off };
|
|
164
|
+
}
|
|
165
|
+
const summary = regions.summaries.get(bound.region);
|
|
166
|
+
if (!summary)
|
|
167
|
+
return { bars: 0, cells: 0, fresh: 0, met: 0, sums: 0 };
|
|
168
|
+
const above = regions.bounds.filter((one) => one !== bound && one.measure === bound.measure && one.form === bound.form
|
|
169
|
+
&& within(point(bound.question), coordinates(one.question))
|
|
170
|
+
&& (one.region === '' || bound.region === one.region || bound.region.startsWith(`${one.region}/`) || (globbed(one.region) && matches(one.region, bound.region))));
|
|
171
|
+
const alone = bound.form === 'alphabet' || bound.measure === 'per-file' || fieldOf(bound.line, 'scope').endsWith('/*');
|
|
172
|
+
const outside = (value, of) => {
|
|
173
|
+
if (of === undefined)
|
|
174
|
+
return false;
|
|
175
|
+
if (!Array.isArray(of)) {
|
|
176
|
+
const n = Number(value);
|
|
177
|
+
return !INTERVALS.leq({ lo: n, hi: n }, of);
|
|
178
|
+
}
|
|
179
|
+
const list = of;
|
|
180
|
+
return list.some((one) => one.startsWith('not:')) ? list.includes(`not:${value}`) : !list.includes(value);
|
|
181
|
+
};
|
|
182
|
+
const mine = poleAt(regions.named, bound.region, [bound]);
|
|
183
|
+
const held = poleAt(regions.named, bound.region, above);
|
|
184
|
+
let bars = 0;
|
|
185
|
+
let cells = 0;
|
|
186
|
+
let fresh = 0;
|
|
187
|
+
let met = 0;
|
|
188
|
+
const whole = coordinates(fieldOf(bound.line, 'scope'));
|
|
189
|
+
const top = regions.summaries.get('') ?? summary;
|
|
190
|
+
const picked = new Map();
|
|
191
|
+
const take = (from, holds) => {
|
|
192
|
+
for (const [key, bar] of from.values) {
|
|
193
|
+
const [question, measure] = [key.slice(0, key.lastIndexOf(' ')), key.slice(key.lastIndexOf(' ') + 1)];
|
|
194
|
+
if (!picked.has(key) && measure === bound.measure && holds(question))
|
|
195
|
+
picked.set(key, { bar, sum: from.sums.get(key) });
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
take(top, (question) => within(point(question), whole));
|
|
199
|
+
take(summary, (question) => within(point(question), coordinates(bound.question)));
|
|
200
|
+
for (const { bar } of picked.values()) {
|
|
201
|
+
for (const [value, count] of bar) {
|
|
202
|
+
met += count;
|
|
203
|
+
if (!alone || !outside(value, mine))
|
|
204
|
+
continue;
|
|
205
|
+
bars += 1;
|
|
206
|
+
cells += count;
|
|
207
|
+
if (!outside(value, held))
|
|
208
|
+
fresh += count;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const sums = alone ? 0 : [...picked.values()].filter(({ sum }) => sum !== undefined && outside(String(sum), mine)).length;
|
|
212
|
+
return { bars, cells, fresh, met, sums };
|
|
213
|
+
}
|
|
214
|
+
function ownDigest(summary) {
|
|
215
|
+
const hash = createHash('sha256');
|
|
216
|
+
hash.update(`${summary.region}\n${summary.cells}\n`);
|
|
217
|
+
for (const line of summary.bounds.map((one) => one.line).sort())
|
|
218
|
+
hash.update(`${line}\n`);
|
|
219
|
+
for (const key of [...summary.sums.keys()].sort())
|
|
220
|
+
hash.update(`${key}=${summary.sums.get(key)}\n`);
|
|
221
|
+
for (const key of [...summary.values.keys()].sort())
|
|
222
|
+
hash.update(`${key}:${[...summary.values.get(key)].sort(([a], [b]) => (a < b ? -1 : 1)).map(([v, n]) => `${v}*${n}`).join(',')}\n`);
|
|
223
|
+
return hash.digest('hex');
|
|
224
|
+
}
|
|
225
|
+
export function finerOf(regions) {
|
|
226
|
+
const finer = new Map();
|
|
227
|
+
for (const region of regions.summaries.keys())
|
|
228
|
+
if (region !== '') {
|
|
229
|
+
const parent = globbed(region) ? '' : restsOn(regions.named, region);
|
|
230
|
+
finer.set(parent, [...(finer.get(parent) ?? []), region]);
|
|
231
|
+
}
|
|
232
|
+
return finer;
|
|
233
|
+
}
|
|
234
|
+
export function digestsOf(regions) {
|
|
235
|
+
const finer = finerOf(regions);
|
|
236
|
+
const out = new Map();
|
|
237
|
+
const digest = (region) => out.get(region) ?? out.set(region, createHash('sha256').update(`${ownDigest(regions.summaries.get(region))}\n${[...(finer.get(region) ?? [])].sort().map(digest).join('\n')}`).digest('hex')).get(region);
|
|
238
|
+
digest('');
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
export function differing(a, b) {
|
|
242
|
+
const [x, y] = [digestsOf(a), digestsOf(b)];
|
|
243
|
+
const named = [...new Set([...a.named, ...b.named])];
|
|
244
|
+
const out = [];
|
|
245
|
+
let compared = 0;
|
|
246
|
+
const descend = (region) => {
|
|
247
|
+
compared += 1;
|
|
248
|
+
if (x.get(region) === y.get(region))
|
|
249
|
+
return;
|
|
250
|
+
const [one, two] = [a.summaries.get(region), b.summaries.get(region)];
|
|
251
|
+
if (!one || !two || ownDigest(one) !== ownDigest(two))
|
|
252
|
+
out.push(region);
|
|
253
|
+
const finer = new Set([...a.summaries.keys(), ...b.summaries.keys()].filter((name) => name !== '' && name !== region
|
|
254
|
+
&& (globbed(name) ? region === '' : restsOn(named, name) === region)));
|
|
255
|
+
for (const name of [...finer].sort())
|
|
256
|
+
descend(name);
|
|
257
|
+
};
|
|
258
|
+
descend('');
|
|
259
|
+
return { regions: out, compared };
|
|
260
|
+
}
|
|
261
|
+
export const packRegions = (regions) => ({
|
|
262
|
+
named: regions.named,
|
|
263
|
+
bounds: regions.bounds.map((one) => one.line),
|
|
264
|
+
summaries: [...regions.summaries.values()].map((one) => [one.region, one.cells, [...one.sums], [...one.values].map(([key, bars]) => [key, [...bars]])]),
|
|
265
|
+
});
|
|
266
|
+
export function unpackRegions(packed) {
|
|
267
|
+
const bounds = packed.bounds.map((line) => boundOf(packed.named, line));
|
|
268
|
+
return {
|
|
269
|
+
named: packed.named,
|
|
270
|
+
bounds,
|
|
271
|
+
summaries: new Map(packed.summaries.map(([region, cells, sums, values]) => [region, {
|
|
272
|
+
region, cells, sums: new Map(sums), values: new Map(values.map(([key, bars]) => [key, new Map(bars)])), bounds: bounds.filter((one) => one.region === region),
|
|
273
|
+
}])),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
export function unlikeReplay(regions, red, own) {
|
|
277
|
+
const read = new Map();
|
|
278
|
+
for (const bound of regions.bounds) {
|
|
279
|
+
const got = rankOf(regions, bound, own);
|
|
280
|
+
if (got.met === 0)
|
|
281
|
+
continue;
|
|
282
|
+
const scope = fieldOf(bound.line, 'scope');
|
|
283
|
+
read.set(scope, (read.get(scope) ?? false) || got.cells > 0 || got.sums > 0);
|
|
284
|
+
}
|
|
285
|
+
return [...read].filter(([scope, bitten]) => bitten !== red.has(scope)).length;
|
|
286
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { VerdictDemand, VerdictObservation } from '@lapxo/topos/authority';
|
|
2
|
+
type CeilingReading = {
|
|
3
|
+
readonly read: readonly string[];
|
|
4
|
+
readonly unread: readonly string[];
|
|
5
|
+
readonly vacuous: readonly string[];
|
|
6
|
+
readonly over: readonly {
|
|
7
|
+
readonly ceiling: string;
|
|
8
|
+
readonly question: string;
|
|
9
|
+
readonly got: string;
|
|
10
|
+
readonly side: 'short' | 'over';
|
|
11
|
+
}[];
|
|
12
|
+
};
|
|
13
|
+
export interface ReleasePolicyInput {
|
|
14
|
+
/** Standing already admitted by the same fold. This function does not admit lines. */
|
|
15
|
+
readonly verdicts?: readonly VerdictObservation[];
|
|
16
|
+
readonly standing: readonly string[];
|
|
17
|
+
readonly demands: readonly string[];
|
|
18
|
+
readonly paid: readonly string[];
|
|
19
|
+
readonly ceilings: readonly string[];
|
|
20
|
+
readonly readings: CeilingReading;
|
|
21
|
+
readonly snapshot: {
|
|
22
|
+
readonly fold: string;
|
|
23
|
+
readonly instrument: string;
|
|
24
|
+
};
|
|
25
|
+
readonly faults?: readonly {
|
|
26
|
+
readonly kind: string;
|
|
27
|
+
readonly evidence: readonly string[];
|
|
28
|
+
readonly obligation?: string;
|
|
29
|
+
}[];
|
|
30
|
+
}
|
|
31
|
+
export interface ReleasePolicyResult {
|
|
32
|
+
readonly gates?: readonly VerdictDemand[];
|
|
33
|
+
readonly status: 'absent' | 'incomplete' | 'blocked' | 'ready';
|
|
34
|
+
readonly snapshot: ReleasePolicyInput['snapshot'];
|
|
35
|
+
readonly policy: readonly string[];
|
|
36
|
+
readonly categories: readonly {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly links: readonly string[];
|
|
39
|
+
readonly selected: readonly string[];
|
|
40
|
+
readonly failures: readonly {
|
|
41
|
+
readonly line: string;
|
|
42
|
+
readonly reason: string;
|
|
43
|
+
readonly evidence: readonly string[];
|
|
44
|
+
}[];
|
|
45
|
+
}[];
|
|
46
|
+
readonly unresolved: readonly string[];
|
|
47
|
+
/** All obligations, including those not selected; the normal fold keeps their verdicts. */
|
|
48
|
+
readonly debt: readonly string[];
|
|
49
|
+
}
|
|
50
|
+
/** Preserve actual fold faults; none gets a release category from its spelling. */
|
|
51
|
+
export declare function releaseFaults(fold: {
|
|
52
|
+
readonly forks: readonly {
|
|
53
|
+
readonly key: string;
|
|
54
|
+
readonly lines: readonly string[];
|
|
55
|
+
}[];
|
|
56
|
+
readonly refused: readonly string[];
|
|
57
|
+
readonly grey: readonly string[];
|
|
58
|
+
readonly orphans: readonly string[];
|
|
59
|
+
readonly second: {
|
|
60
|
+
readonly verdict: string;
|
|
61
|
+
};
|
|
62
|
+
}): NonNullable<ReleasePolicyInput['faults']>;
|
|
63
|
+
/** Select declared obligations, never categories inferred from their names or paths. */
|
|
64
|
+
export declare function releasePolicy(input: ReleasePolicyInput): ReleasePolicyResult;
|
|
65
|
+
/** Existing fold output can show the policy without a new view or a release action. */
|
|
66
|
+
export declare function releasePolicyLines(result: ReleasePolicyResult | undefined): readonly string[];
|
|
67
|
+
export {};
|