@lapxo/bound 0.1.4 → 0.1.6
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/LICENSE +0 -1
- package/README.md +26 -5
- package/TARGET.bound +84 -19
- package/dist/cli/accept.d.ts +5 -0
- package/dist/cli/accept.js +60 -14
- package/dist/cli/act-result.d.ts +3 -0
- package/dist/cli/act-result.js +48 -0
- package/dist/cli/owner.d.ts +2 -0
- package/dist/cli/owner.js +18 -2
- package/dist/cli/pass.js +15 -10
- package/dist/cli/place.js +8 -1
- package/dist/cli/verb.js +48 -2
- package/dist/fold/admission-history.d.ts +6 -0
- package/dist/fold/admission-history.js +22 -0
- package/dist/fold/claims.js +5 -1
- package/dist/fold/closed.js +44 -13
- package/dist/fold/digests.js +18 -11
- package/dist/fold/kept.js +9 -4
- package/dist/fold/keys.js +9 -6
- package/dist/fold/observed.js +196 -174
- package/dist/fold/resolved.js +6 -2
- package/dist/fold/signed.d.ts +5 -0
- package/dist/fold/signed.js +19 -15
- package/dist/host/act-cost.d.ts +13 -0
- package/dist/host/act-cost.js +25 -0
- package/dist/host/adapters/modules/javascript.d.ts +1 -0
- package/dist/host/adapters/modules/javascript.js +48 -0
- package/dist/host/capsule-process.js +22 -19
- package/dist/host/capsule.d.ts +2 -1
- package/dist/host/capsule.js +13 -12
- package/dist/host/io.d.ts +1 -1
- package/dist/host/io.js +1 -1
- package/dist/host/ports/ingress.d.ts +24 -0
- package/dist/host/ports/ingress.js +57 -0
- package/dist/host/ports/local-act.d.ts +34 -0
- package/dist/host/ports/local-act.js +100 -0
- package/dist/host/ports/publication.d.ts +10 -0
- package/dist/host/ports/publication.js +56 -0
- package/dist/host/reading-closures.d.ts +31 -0
- package/dist/host/reading-closures.js +146 -0
- package/dist/host/release.js +24 -2
- package/dist/host/selected-capsules.js +4 -4
- package/dist/host/walk.d.ts +6 -0
- package/dist/host/walk.js +193 -0
- package/dist/land/ledger.d.ts +1 -0
- package/dist/land/ledger.js +21 -2
- package/dist/land/owned.d.ts +1 -1
- package/dist/land/owned.js +5 -4
- package/dist/observe/files.js +12 -2
- package/dist/observe/regions.js +3 -5
- package/dist/render/field.d.ts +1 -7
- package/dist/render/field.js +6 -12
- package/dist/render/names.d.ts +1 -1
- package/dist/render/names.js +1 -1
- package/dist/render/view.js +16 -11
- package/docs/contracts/archive.md +11 -0
- package/docs/contracts/cli.md +19 -0
- package/docs/contracts/documents.md +35 -0
- package/docs/contracts/evidence-view.md +6 -0
- package/docs/contracts/publication.md +17 -0
- package/docs/contracts/typed-object.md +27 -0
- package/docs/contracts/view-walk.md +7 -0
- package/docs/contracts/views.md +29 -0
- package/docs/usage.md +6 -0
- package/package.json +6 -4
- package/receipts.bound +367 -77
package/dist/fold/closed.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { actCosts } from "../host/act-cost.js";
|
|
2
|
+
import { verifiedReadingReceipts } from "../host/reading-closures.js";
|
|
1
3
|
import { dirname, existsSync, join, mkdirSync, relative, resolve, statSync, writeFileSync } from "../host/io.js";
|
|
2
|
-
import { canonical, LOCK, parse, roleAt, roleClaimsOf } from '@lapxo/topos/wire';
|
|
3
|
-
import { RECEIPTS, fieldOf, isWire } from "./claims.js";
|
|
4
|
+
import { byBytes, canonical, LOCK, parse, publicationOf, roleAt, roleClaimsOf } from '@lapxo/topos/wire';
|
|
5
|
+
import { RECEIPTS, fieldOf, foldClaims, isConfig, isWire } from "./claims.js";
|
|
4
6
|
import { bytesDigest as hashBytes, fullDigest as hashFull } from "../host/digest.js";
|
|
5
7
|
import { worldAt } from "./places.js";
|
|
6
8
|
import { ownLockOf } from "./signed.js";
|
|
@@ -9,15 +11,14 @@ import { storeAt, storeOf } from "../land/ledger.js";
|
|
|
9
11
|
import { coordinatesUnder, observeFile, observeText } from "../observe/files.js";
|
|
10
12
|
import { carriedFrom } from "./resolved.js";
|
|
11
13
|
import { takenFor } from "../fold/place-inputs.js";
|
|
12
|
-
/** A region is closed when the digest of its bytes is the one its receipt names. Folding a closed region is idle: idleCount is how many were already closed, actSeconds
|
|
14
|
+
/** A region is closed when the digest of its bytes is the one its receipt names. Folding a closed region is idle: idleCount is how many were already closed, actSeconds the largest measured act or unpartitioned host work, receiptsSeen how many receipts were read and how long that took. */
|
|
13
15
|
let idle = 0;
|
|
14
|
-
const began = Date.now();
|
|
15
16
|
let seen = { count: 0, ms: 0 };
|
|
16
17
|
export const idleCount = () => idle;
|
|
17
18
|
export const noteIdle = () => {
|
|
18
19
|
idle += 1;
|
|
19
20
|
};
|
|
20
|
-
export const actSeconds = () =>
|
|
21
|
+
export const actSeconds = () => actCosts.seconds();
|
|
21
22
|
export const receiptsSeen = () => seen;
|
|
22
23
|
const algorithmIn = (text) => text.match(/(?:value=|place:)([a-z0-9]+):[0-9a-f]{64}/)?.[1] ?? '';
|
|
23
24
|
const lockStamp = (root, place) => {
|
|
@@ -55,6 +56,14 @@ function filesOf(root, place, region, inputs) {
|
|
|
55
56
|
return [...new Set([...owned, ...observed, ...own])].filter(rel => rel !== receipt && !rel.split('/').some(part => part === '.bound' || part === '.cache' || part === '.git'));
|
|
56
57
|
}
|
|
57
58
|
const rooted = new Map();
|
|
59
|
+
function publicConfiguration(lines) {
|
|
60
|
+
if (!lines.length || !lines.every(isConfig))
|
|
61
|
+
return lines;
|
|
62
|
+
const folded = foldClaims(lines);
|
|
63
|
+
if (folded.forks.length)
|
|
64
|
+
throw Error('REFUSE·receipt disputed configuration standing');
|
|
65
|
+
return publicationOf(folded.standing.filter(line => fieldOf(line, 'value') !== 'withdraw'));
|
|
66
|
+
}
|
|
58
67
|
function rootLines(root, place) {
|
|
59
68
|
const name = place.replace(/\/$/, '');
|
|
60
69
|
if (!name)
|
|
@@ -63,7 +72,7 @@ function rootLines(root, place) {
|
|
|
63
72
|
const held = rooted.get(key);
|
|
64
73
|
if (held !== undefined)
|
|
65
74
|
return held;
|
|
66
|
-
const standing = (observeText(join(root, LOCK)) ?? '').split('\n').filter(isWire);
|
|
75
|
+
const standing = publicConfiguration((observeText(join(root, LOCK)) ?? '').split('\n').filter(isWire));
|
|
67
76
|
// Authority validates admission separately; it is not a rendered file input.
|
|
68
77
|
const lines = standing.filter((line) => fieldOf(line, 'value') !== 'withdraw' && !fieldOf(line, 'scope').startsWith('keys/'));
|
|
69
78
|
const take = takenFor(lines, `${name}/`, worldAt(root, name) ? { own: ownLockOf(root, name), tree: viewsOf(standing) } : undefined);
|
|
@@ -73,8 +82,20 @@ function rootLines(root, place) {
|
|
|
73
82
|
}
|
|
74
83
|
/** File-fold identity uses public inputs; admission history stays in its separate ledger guard. */
|
|
75
84
|
export function fileRegionDigest(root, place, region, algorithm, inputs = []) {
|
|
76
|
-
const files = filesOf(root, place, region, inputs);
|
|
77
|
-
|
|
85
|
+
const files = [...filesOf(root, place, region, inputs)].sort(byBytes);
|
|
86
|
+
const own = join(root, place, LOCK);
|
|
87
|
+
const bytesOf = (rel) => {
|
|
88
|
+
const at = join(root, rel), bytes = observeFile(at) ?? new Uint8Array();
|
|
89
|
+
if (resolve(at) !== resolve(own))
|
|
90
|
+
return bytes;
|
|
91
|
+
const lines = new TextDecoder().decode(bytes).split('\n').filter(isWire);
|
|
92
|
+
// Object history keeps its own contract. Configuration's public identity is
|
|
93
|
+
// the existing Topos publication projection, not its admission envelopes.
|
|
94
|
+
if (!lines.length || !lines.every(isConfig))
|
|
95
|
+
return bytes;
|
|
96
|
+
return new TextEncoder().encode(publicConfiguration(lines).join('\n') + '\n');
|
|
97
|
+
};
|
|
98
|
+
return hashFull([...files.map((rel) => `${relative(join(root, place), join(root, rel))} ${hashBytes(bytesOf(rel), algorithm)}`), ...rootLines(root, place)], algorithm);
|
|
78
99
|
}
|
|
79
100
|
function namedReceipts(root, store) {
|
|
80
101
|
const text = observeText(join(root, RECEIPTS));
|
|
@@ -89,13 +110,15 @@ function namedReceipts(root, store) {
|
|
|
89
110
|
}
|
|
90
111
|
return out;
|
|
91
112
|
}
|
|
113
|
+
// Only the instrument's file seals describe file-region bytes. Reader receipts keep their own semantic identity.
|
|
114
|
+
const fileSeal = (line) => isWire(line) && fieldOf(line, 'measure') === 'bytes' && fieldOf(line, 'by') === 'bound';
|
|
92
115
|
function ownBytes(root) {
|
|
93
|
-
return (observeText(join(root, RECEIPTS)) ?? '').split('\n').filter(
|
|
116
|
+
return (observeText(join(root, RECEIPTS)) ?? '').split('\n').filter(fileSeal);
|
|
94
117
|
}
|
|
95
118
|
function bytesMeet(root, place) {
|
|
96
119
|
const shipped = (observeText(join(root, place, RECEIPTS)) ?? '').split('\n').filter(isWire);
|
|
97
120
|
const inputs = carriedFrom(storeOf(root), shipped) ?? [];
|
|
98
|
-
const lines = shipped.filter(
|
|
121
|
+
const lines = shipped.filter(fileSeal);
|
|
99
122
|
const algorithm = algorithmIn(lines.join('\n'));
|
|
100
123
|
return lines.length > 0 && algorithm !== '' && lines.map((line) => fieldOf(line, 'value')).join('\n') === lines.map((line) => fileRegionDigest(root, place, fieldOf(line, 'scope'), algorithm, inputs)).join('\n');
|
|
101
124
|
}
|
|
@@ -115,8 +138,8 @@ function rewritePlace(root, place) {
|
|
|
115
138
|
const algorithm = algorithmIn(text);
|
|
116
139
|
if (algorithm === '')
|
|
117
140
|
return;
|
|
118
|
-
const kept = text.split('\n').filter((line) => line === '' || !
|
|
119
|
-
const regions = kept.filter((line) => isWire(line) && fieldOf(line, 'measure') === 'count' && fieldOf(line, 'scope').startsWith('receipts/'));
|
|
141
|
+
const kept = text.split('\n').filter((line) => line === '' || !fileSeal(line));
|
|
142
|
+
const regions = kept.filter((line) => isWire(line) && fieldOf(line, 'measure') === 'count' && fieldOf(line, 'by') === 'bound' && fieldOf(line, 'scope').startsWith('receipts/'));
|
|
120
143
|
const bytes = regions.map((line) => {
|
|
121
144
|
const scope = fieldOf(line, 'scope');
|
|
122
145
|
const inputs = carriedFrom(storeOf(root), kept.filter(isWire)) ?? [];
|
|
@@ -146,11 +169,19 @@ export function meets(root, store, place) {
|
|
|
146
169
|
const own = ownBytes(root);
|
|
147
170
|
const carried = own.length ? carriedFrom(store, (observeText(join(root, RECEIPTS)) ?? '').split('\n').filter(isWire)) : undefined;
|
|
148
171
|
const ownClosed = own.length > 0 && carried !== undefined && carried.length > 0 && bytesMeet(root, '');
|
|
149
|
-
|
|
172
|
+
let closed = (own.length > 0 ? ownClosed : named.size > 0) && [...named].every(([one, expected]) => {
|
|
150
173
|
const bytes = observeFile(join(root, one, RECEIPTS));
|
|
151
174
|
const algorithm = expected.split(':')[0];
|
|
152
175
|
return bytes !== undefined && hashBytes(bytes, algorithm) === expected && meets(root, store, `${one}/`);
|
|
153
176
|
});
|
|
177
|
+
if (closed && carried) {
|
|
178
|
+
const algorithm = algorithmIn(own.join('\n'));
|
|
179
|
+
const readings = verifiedReadingReceipts(store, carried, text => hashFull(text, algorithm));
|
|
180
|
+
if (readings === undefined)
|
|
181
|
+
closed = false;
|
|
182
|
+
else if (readings)
|
|
183
|
+
process.stderr.write(`RECEIPTS ${readings} read · 0 opened · reading closures\n`);
|
|
184
|
+
}
|
|
154
185
|
seen = { count: named.size + (own.length > 0 ? 1 : 0), ms: Date.now() - at };
|
|
155
186
|
return closed;
|
|
156
187
|
}
|
package/dist/fold/digests.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { ledgerLines, ledgerWriters } from "../land/ledger.js";
|
|
1
2
|
import { readOnly } from "../host/read-only.js";
|
|
2
|
-
import { appendFileSync, dirname, join, mkdirSync, readdirSync, relative, resolve } from "../host/io.js";
|
|
3
|
+
import { appendFileSync, dirname, join, mkdirSync, readdirSync, realpathSync, relative, resolve } from "../host/io.js";
|
|
3
4
|
import { bytesDigest as bytesWith, fullDigest as fullWith } from "../host/digest.js";
|
|
4
5
|
import { coordinatesUnder, observeFile, observeText, stampOf } from "../observe/files.js";
|
|
5
6
|
import { releaseOf, signedLockLines } from "./keys.js";
|
|
@@ -7,6 +8,7 @@ import { EXTENSION, fieldOf, foldClaims, selfName } from "./claims.js";
|
|
|
7
8
|
import { wireLine } from "./wire.js";
|
|
8
9
|
import { publicLock, LOCK } from '@lapxo/topos/wire';
|
|
9
10
|
import { builtinModules, createRequire } from 'node:module';
|
|
11
|
+
import { moduleImports } from "../host/adapters/modules/javascript.js";
|
|
10
12
|
const held = new Map();
|
|
11
13
|
let algorithmMs = 0;
|
|
12
14
|
export const algorithmCost = () => algorithmMs;
|
|
@@ -31,10 +33,8 @@ function namedAlgorithm(store) {
|
|
|
31
33
|
const contract = published === undefined ? undefined : wireLine(published, 'digest-algorithms');
|
|
32
34
|
return contract === undefined ? undefined : fieldOf(contract, 'value').split('|')[0]?.split(':')[0];
|
|
33
35
|
}
|
|
34
|
-
for (const
|
|
35
|
-
|
|
36
|
-
continue;
|
|
37
|
-
const text = observeText(join(store, 'ledger', entry.name)) ?? '';
|
|
36
|
+
for (const writer of ledgerWriters(store)) {
|
|
37
|
+
const text = ledgerLines(store, writer).join('\n');
|
|
38
38
|
let from = 0;
|
|
39
39
|
while (from < text.length) {
|
|
40
40
|
const at = (() => {
|
|
@@ -110,6 +110,8 @@ export function coordinateDigest(store, at) {
|
|
|
110
110
|
const stamp = stampOf(at);
|
|
111
111
|
if (stamp?.kind !== 'file')
|
|
112
112
|
return stamp === undefined ? undefined : bytesDigest(store, observeFile(at) ?? new Uint8Array());
|
|
113
|
+
if (readOnly())
|
|
114
|
+
return bytesDigest(store, observeFile(at) ?? new Uint8Array());
|
|
113
115
|
const key = `${at} ${stamp.stamp}`;
|
|
114
116
|
const memo = memoOf(store);
|
|
115
117
|
const held = memo.get(key);
|
|
@@ -140,10 +142,10 @@ const imported = new Map();
|
|
|
140
142
|
const importsOf = (store, at, digest) => {
|
|
141
143
|
const key = `imports ${fullDigest(store, importsOf.toString())} ${digest}`;
|
|
142
144
|
const memo = memoOf(store);
|
|
143
|
-
const held = imported.get(digest) ?? memo.get(key)?.split('|').filter(Boolean);
|
|
145
|
+
const held = imported.get(digest) ?? (readOnly() ? undefined : memo.get(key)?.split('|').filter(Boolean));
|
|
144
146
|
if (held !== undefined)
|
|
145
147
|
return held;
|
|
146
|
-
const found =
|
|
148
|
+
const found = moduleImports(new TextDecoder().decode(observeFile(at) ?? new Uint8Array()), at);
|
|
147
149
|
imported.set(digest, found);
|
|
148
150
|
memo.set(key, found.join('|'));
|
|
149
151
|
if (!readOnly())
|
|
@@ -173,14 +175,19 @@ export function instrumentCoordinates(store, entry, standing = [], root = dirnam
|
|
|
173
175
|
const digest = coordinateDigest(store, at);
|
|
174
176
|
if (digest === undefined)
|
|
175
177
|
return;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
+
// The host loader resolves links before loading a module. Count that module
|
|
179
|
+
// once, while retaining distinct modules even when their bytes are equal.
|
|
180
|
+
const module = realpathSync(at);
|
|
181
|
+
if (seen.has(module))
|
|
182
|
+
return;
|
|
183
|
+
seen.set(module, digest);
|
|
184
|
+
for (const one of importsOf(store, module, digest)) {
|
|
178
185
|
if (one.startsWith('node:') || builtinModules.includes(one))
|
|
179
186
|
continue;
|
|
180
187
|
if (one.startsWith('.'))
|
|
181
|
-
walk(resolve(dirname(
|
|
188
|
+
walk(resolve(dirname(module), one));
|
|
182
189
|
else
|
|
183
|
-
walk(createRequire(
|
|
190
|
+
walk(createRequire(module).resolve(one));
|
|
184
191
|
}
|
|
185
192
|
};
|
|
186
193
|
walk(resolve(entry));
|
package/dist/fold/kept.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { actLines, committedLocalActs } from "../host/ports/local-act.js";
|
|
1
2
|
import { readOnly } from "../host/read-only.js";
|
|
2
3
|
import { createHash, dirname, existsSync, join, mkdirSync, readdirSync, renameSync, writeFileSync } from "../host/io.js";
|
|
3
4
|
import { bytesDigest, coordinateDigest } from "./digests.js";
|
|
4
5
|
import { viewsOf } from "./views.js";
|
|
5
|
-
import { landMemo, ledgerLines, memoAt, shardFile, writerFile } from "../land/ledger.js";
|
|
6
|
+
import { landMemo, ledgerLines, ledgerWriters, memoAt, shardFile, writerFile } from "../land/ledger.js";
|
|
6
7
|
import { coordinatesUnder, observeFile, observeText } from "../observe/files.js";
|
|
7
8
|
import { EXTENSION, fieldOf } from "./claims.js";
|
|
8
9
|
import { keyFor, keyOf, lockSigners } from "./keys.js";
|
|
@@ -36,7 +37,7 @@ export function foldPoints(store, standing, under, keeper, instrument = '', obse
|
|
|
36
37
|
`take ${digestOf(ledgerLines(store, 'take').filter((line) => reads.scopes.has(fieldOf(line, 'scope').replace(/^take\//, ''))))}`,
|
|
37
38
|
`tree ${digestOf(ledgerLines(store, tree ?? '').filter((line) => (sourceOf(line) ?? '').startsWith(reads.place)))}`,
|
|
38
39
|
];
|
|
39
|
-
const heads = (
|
|
40
|
+
const heads = ledgerWriters(store)
|
|
40
41
|
.filter((writer) => writer !== keeper && writer !== 'gate' && !writer.startsWith('reader') && !(reads && (signers.has(writer) || writer === 'judge' || writer === 'take' || writer === tree)))
|
|
41
42
|
.map((writer) => `${writer} ${writer === liveWriter(store, standing) ? lastWord(store, writer) : bytesDigest(store, text(ledgerLines(store, writer).join('\n')))}`);
|
|
42
43
|
const written = [...new Set([...viewsOf(standing).values()].flatMap((view) => ('regions' in view && view.shape ? [view.shape] : [])))].sort()
|
|
@@ -100,8 +101,12 @@ function readingContext(store) {
|
|
|
100
101
|
const speaker = keyOf(store, 'read');
|
|
101
102
|
const files = speaker === undefined ? [] : [writerFile(store, speaker), ...coordinatesUnder(dirname(shardFile(store, speaker, '')), store)
|
|
102
103
|
.filter(file => file.endsWith(EXTENSION)).map(file => join(store, file))];
|
|
103
|
-
|
|
104
|
-
.
|
|
104
|
+
const segments = speaker === undefined ? [] : committedLocalActs(store).flatMap(act => act.bundle.placements?.flatMap(placement => {
|
|
105
|
+
const records = actLines(placement.records).filter(line => fieldOf(line, 'by') === speaker);
|
|
106
|
+
return records.length ? [placement.place + '\n' + records.join('\n')] : [];
|
|
107
|
+
}) ?? []);
|
|
108
|
+
return bytesDigest(store, text([...files.filter(file => existsSync(file)).sort()
|
|
109
|
+
.map(file => bytesDigest(store, observeFile(file) ?? new Uint8Array())), ...segments].join('\n')));
|
|
105
110
|
}
|
|
106
111
|
export function keptPlace(store, under) {
|
|
107
112
|
const key = observeText(memoAt(store, lastOf([`place ${under ?? '.'}`])))?.trim();
|
package/dist/fold/keys.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { dirname, existsSync, join } from "../host/io.js";
|
|
2
|
-
import { ledgerLines, signedLines } from "../land/ledger.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { ledgerLines, ledgerWriters, signedLines } from "../land/ledger.js";
|
|
3
|
+
import { fieldOf, foldClaims, isWire, LOCK, selfName } from "./claims.js";
|
|
4
|
+
import { observeText } from "../observe/files.js";
|
|
5
5
|
const held = new Map();
|
|
6
|
+
const keyInputs = new Map();
|
|
6
7
|
const merged = new Map();
|
|
7
8
|
/**
|
|
8
9
|
* Whose lines carry a class of writing, as the lock says it. Every ledger the store keeps is read, the key claims
|
|
@@ -47,13 +48,15 @@ export function signedLockLines(store, told = []) {
|
|
|
47
48
|
return lockSigners(store, told).flatMap((id) => signedLines(store, id));
|
|
48
49
|
}
|
|
49
50
|
function keysOf(store, told) {
|
|
50
|
-
const
|
|
51
|
+
const input = ledgerKeyLines(store).join('\n');
|
|
52
|
+
const mine = told.length ? read(store, told) : keyInputs.get(store) === input ? held.get(store) ?? read(store) : read(store);
|
|
53
|
+
if (!told.length)
|
|
54
|
+
keyInputs.set(store, input);
|
|
51
55
|
if (!told.length)
|
|
52
56
|
held.set(store, mine);
|
|
53
57
|
return mine;
|
|
54
58
|
}
|
|
55
|
-
const ledgerKeyLines = (store) => (
|
|
56
|
-
.filter((name) => name.endsWith(EXTENSION)).flatMap((name) => ledgerLines(store, name.slice(0, -EXTENSION.length)))
|
|
59
|
+
const ledgerKeyLines = (store) => ledgerWriters(store).flatMap(writer => ledgerLines(store, writer))
|
|
57
60
|
.filter((line) => /\bsig=/.test(line) && /(?:^|\s)scope=keys\/[^/\s]+\s/.test(line));
|
|
58
61
|
const releases = new Map();
|
|
59
62
|
/**
|