@lapxo/bound 0.1.6 → 0.1.7
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/README.md +8 -8
- package/TARGET.bound +18 -18
- package/dist/cli/accept.js +18 -3
- package/dist/cli/pass.js +6 -4
- package/dist/cli/place.d.ts +1 -1
- package/dist/cli/place.js +1 -1
- package/dist/cli/release-result.d.ts +5 -0
- package/dist/cli/release-result.js +80 -0
- package/dist/cli/standing-result.d.ts +5 -0
- package/dist/cli/standing-result.js +39 -0
- package/dist/cli/verb.js +21 -2
- package/dist/fold/closed.js +9 -3
- package/dist/fold/kept.d.ts +1 -1
- package/dist/fold/kept.js +12 -2
- package/dist/fold/keys.js +7 -3
- package/dist/fold/places.d.ts +5 -1
- package/dist/fold/places.js +19 -3
- package/dist/fold/readers.js +2 -1
- package/dist/fold/release-policy.js +0 -2
- package/dist/fold/zoom.js +5 -4
- package/dist/host/cell-receipts.js +7 -3
- package/dist/host/reading-closures.js +2 -1
- package/dist/host/selected-topos.d.ts +4 -1
- package/dist/host/selected-topos.js +16 -1
- package/dist/land/standing-pin.d.ts +3 -0
- package/dist/land/standing-pin.js +49 -0
- package/docs/contracts/cli.md +8 -0
- package/docs/contracts/evidence-view.md +4 -0
- package/package.json +2 -2
- package/receipts.bound +575 -364
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { parse } from '@lapxo/topos/wire';
|
|
2
|
+
import { authorityFor, writerFor, releaseSigner } from "../fold/signers.js";
|
|
3
|
+
import { eraOf } from "../fold/digests.js";
|
|
4
|
+
import { fieldOf, foldClaims } from "../fold/claims.js";
|
|
5
|
+
import { observeText } from "../observe/files.js";
|
|
6
|
+
import { claimLinesIn } from "./args.js";
|
|
7
|
+
import { LOCK } from '@lapxo/topos/wire';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
/** Judge an external signed approval without landing it, fetching its source,
|
|
10
|
+
* executing a provider, or changing the release-policy verdict of the place.
|
|
11
|
+
* The anchor is the place's key, never a key offered by the approval. */
|
|
12
|
+
export function releaseApproval(lock, lines) {
|
|
13
|
+
const standing = foldClaims(lock).standing;
|
|
14
|
+
const id = writerFor(standing, 'authorize');
|
|
15
|
+
if (!id)
|
|
16
|
+
throw Error('REFUSE·release place names no authorizer');
|
|
17
|
+
const one = (rows, scope, measure) => {
|
|
18
|
+
const selected = rows.filter(line => fieldOf(line, 'scope') === scope && (!measure || fieldOf(line, 'measure') === measure) && fieldOf(line, 'value') !== 'withdraw');
|
|
19
|
+
if (selected.length !== 1)
|
|
20
|
+
throw Error(`REFUSE·release ${scope} needs one declaration`);
|
|
21
|
+
return fieldOf(selected[0], 'value');
|
|
22
|
+
};
|
|
23
|
+
const anchor = { ...releaseSigner(standing), id, publicKey: one(standing, `keys/${id}`, 'public-key'), coverage: one(standing, `keys/${id}`, 'coverage').split('|') };
|
|
24
|
+
if (!lines.length)
|
|
25
|
+
throw Error('REFUSE·release empty approval');
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
const got = parse(line);
|
|
28
|
+
if (got.kind !== 'fact')
|
|
29
|
+
throw Error('REFUSE·release malformed approval');
|
|
30
|
+
if (got.value.fields['by'] !== id || !got.value.fields['sig'])
|
|
31
|
+
throw Error(`REFUSE·release approval must be signed by ${id}`);
|
|
32
|
+
}
|
|
33
|
+
// A separate release place may declare an era; its configuration must itself
|
|
34
|
+
// authenticate against the existing anchor. No packet key or coverage is used.
|
|
35
|
+
const wire = lines.filter(line => ['wire/signature-algorithms', 'audit/wire/signature-algorithms', 'wire/era', 'audit/wire/era'].includes(fieldOf(line, 'scope')));
|
|
36
|
+
let era;
|
|
37
|
+
try {
|
|
38
|
+
era = eraOf(wire.length ? foldClaims(wire).standing : standing);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw Error('REFUSE·release signature era is absent or conflicting');
|
|
42
|
+
}
|
|
43
|
+
const authority = authorityFor([], anchor, era.admitted);
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
const verdict = authority.of(line);
|
|
46
|
+
if (verdict.kind !== 'admitted')
|
|
47
|
+
throw Error(`REFUSE·release ${fieldOf(line, 'scope')}: ${verdict.why ?? verdict.kind}`);
|
|
48
|
+
}
|
|
49
|
+
const released = lines.filter(line => /^release\/[^/]+$/.test(fieldOf(line, 'scope')) && fieldOf(line, 'measure') === 'digest' && fieldOf(line, 'role') === 'writes');
|
|
50
|
+
if (released.length !== 1)
|
|
51
|
+
throw Error('REFUSE·release needs one signed release/<tag> digest');
|
|
52
|
+
const required = (scope, measure) => {
|
|
53
|
+
const selected = lines.filter(line => fieldOf(line, 'scope') === scope);
|
|
54
|
+
if (selected.some(line => fieldOf(line, 'role') !== 'writes' || fieldOf(line, 'form') !== 'alphabet' || fieldOf(line, 'measure') !== measure))
|
|
55
|
+
throw Error(`REFUSE·release ${scope} is not an approval declaration`);
|
|
56
|
+
return one(selected, scope, measure);
|
|
57
|
+
};
|
|
58
|
+
const digest = required(fieldOf(released[0], 'scope'), 'digest'), name = required('release/artifact', 'id'), source = required('sources/archive', 'id');
|
|
59
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(digest))
|
|
60
|
+
throw Error('REFUSE·release archive needs sha256');
|
|
61
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name))
|
|
62
|
+
throw Error('REFUSE·release artifact needs one safe filename');
|
|
63
|
+
let url;
|
|
64
|
+
try {
|
|
65
|
+
url = new URL(source);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw Error('REFUSE·release archive source needs an absolute URL');
|
|
69
|
+
}
|
|
70
|
+
if (url.username || url.password || /[\s\x00-\x1f\x7f]/.test(source))
|
|
71
|
+
throw Error('REFUSE·release archive source needs a credential-free absolute URL without whitespace');
|
|
72
|
+
return `READY ${digest} ${name} ${source}`;
|
|
73
|
+
}
|
|
74
|
+
export function printReleaseApproval(root, file) {
|
|
75
|
+
const text = observeText(file);
|
|
76
|
+
if (text === undefined)
|
|
77
|
+
throw Error('REFUSE·release approval file is absent');
|
|
78
|
+
const lines = text.split('\n').filter(line => line.trim().length > 0);
|
|
79
|
+
process.stdout.write(releaseApproval(claimLinesIn(join(root, LOCK)), lines) + '\n');
|
|
80
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** An evidence projection, never a new authority lock. Authentication uses
|
|
2
|
+
* the admission snapshot at each local epoch; the existing fold applies
|
|
3
|
+
* supersessions and withdrawals across all declared classes. Storage layout
|
|
4
|
+
* and delivery mode do not enter this public boundary. */
|
|
5
|
+
export declare function printStandingResult(root: string): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { viewsOf } from "../fold/views.js";
|
|
2
|
+
import { lockStanding } from "../fold/keys.js";
|
|
3
|
+
import { admissionAnchor, admissionHistoryAt } from "../fold/admission-history.js";
|
|
4
|
+
import { authorityFor } from "../fold/signers.js";
|
|
5
|
+
import { eraOf } from "../fold/digests.js";
|
|
6
|
+
import { fieldOf, foldClaims } from "../fold/claims.js";
|
|
7
|
+
import { storeOf } from "../land/ledger.js";
|
|
8
|
+
import { foldedEvidence } from '@lapxo/topos/contract';
|
|
9
|
+
/** An evidence projection, never a new authority lock. Authentication uses
|
|
10
|
+
* the admission snapshot at each local epoch; the existing fold applies
|
|
11
|
+
* supersessions and withdrawals across all declared classes. Storage layout
|
|
12
|
+
* and delivery mode do not enter this public boundary. */
|
|
13
|
+
export function printStandingResult(root) {
|
|
14
|
+
const store = storeOf(root), view = viewsOf(lockStanding(store)).get('standing');
|
|
15
|
+
if (!view || view.shape || !view.regions.some(region => region.name === 'standing'))
|
|
16
|
+
throw Error('REFUSE·view standing requires a declared standing view');
|
|
17
|
+
const history = admissionHistoryAt(root, Number.MAX_SAFE_INTEGER), anchor = admissionAnchor(root);
|
|
18
|
+
const snapshots = new Map();
|
|
19
|
+
const admitted = history.filter(line => {
|
|
20
|
+
const epoch = Number(fieldOf(line, 'epoch')) || 0;
|
|
21
|
+
let authority = snapshots.get(epoch);
|
|
22
|
+
if (!authority) {
|
|
23
|
+
const snapshot = admissionHistoryAt(root, epoch);
|
|
24
|
+
// The signature vocabulary is configuration, not the as-yet
|
|
25
|
+
// unauthenticated evidence whose values are about to be folded.
|
|
26
|
+
const wire = snapshot.filter(record => fieldOf(record, 'scope').startsWith('wire/'));
|
|
27
|
+
authority = authorityFor(snapshot, anchor, eraOf(foldClaims(wire).standing).admitted);
|
|
28
|
+
snapshots.set(epoch, authority);
|
|
29
|
+
}
|
|
30
|
+
return authority.of(line).kind === 'admitted';
|
|
31
|
+
});
|
|
32
|
+
// The host authenticates against historical authority; Topos projects the
|
|
33
|
+
// authenticated history using the instrument's existing fold.
|
|
34
|
+
const { standing } = foldedEvidence(admitted, {
|
|
35
|
+
classification: () => 'local',
|
|
36
|
+
fold: lines => foldClaims(lines).standing,
|
|
37
|
+
});
|
|
38
|
+
process.stdout.write(standing.length ? standing.join('\n') + '\n' : '');
|
|
39
|
+
}
|
package/dist/cli/verb.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { printReleaseApproval } from "./release-result.js";
|
|
2
3
|
import { printActResult } from "./act-result.js";
|
|
4
|
+
import { printStandingResult } from "./standing-result.js";
|
|
3
5
|
import { withContentStore } from "../host/content-store.js";
|
|
4
|
-
import { withoutEffects } from "../host/read-only.js";
|
|
6
|
+
import { readOnly, withoutEffects } from "../host/read-only.js";
|
|
7
|
+
import { layReleases } from "../host/release.js";
|
|
5
8
|
import { resolveSources, resolveRequested } from "../host/source-content.js";
|
|
6
9
|
import { cellsWithReceipts } from "../host/cell-receipts.js";
|
|
7
10
|
import { renderCells } from '@lapxo/topos/cells-view';
|
|
@@ -195,6 +198,12 @@ async function view(root, under, asked, told = [], coord, key, includePolicy = t
|
|
|
195
198
|
printActResult(root, coord);
|
|
196
199
|
return EXIT.closed;
|
|
197
200
|
}
|
|
201
|
+
if (name === 'standing') {
|
|
202
|
+
if (under || told.length || coord || depth !== undefined)
|
|
203
|
+
throw Error('REFUSE·standing local committed projection only');
|
|
204
|
+
printStandingResult(root);
|
|
205
|
+
return EXIT.closed;
|
|
206
|
+
}
|
|
198
207
|
if (name === 'evidence' && !under && !told.length && ingressBundles(storeOf(root)).length) {
|
|
199
208
|
const declared = viewsOf(lockStanding(storeOf(root))).get(name);
|
|
200
209
|
if (!declared || declared.shape || !declared.regions.some(region => region.name === 'evidence'))
|
|
@@ -231,6 +240,10 @@ async function view(root, under, asked, told = [], coord, key, includePolicy = t
|
|
|
231
240
|
}
|
|
232
241
|
}
|
|
233
242
|
const own = place ? rest.filter((line) => admittedIn(root, place, line)) : [];
|
|
243
|
+
// Executed views use the place's declared mounts before observation, as pass
|
|
244
|
+
// does. Prospective folds and public standing queries do not lay releases.
|
|
245
|
+
if (!told.length && !readOnly())
|
|
246
|
+
layReleases(storeOf(root), root, standingOf(root, under).standing, storeOf(root));
|
|
234
247
|
const fold = told.length
|
|
235
248
|
? foldPlace(root, entryOf(root), under, false, { standing: standingOf(root, under, ledger), observed: observedClaims(root, storeOf(root), { wait: false }), free: true })
|
|
236
249
|
: foldPlace(root, entryOf(root), under, true);
|
|
@@ -447,12 +460,18 @@ async function foldAt(root, under, argv) {
|
|
|
447
460
|
async function foldRequested(root, under, argv) {
|
|
448
461
|
const as = valueOf(argv, FLAGS.as);
|
|
449
462
|
const prospective = as !== undefined && positionalsOf(argv).some(one => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
|
|
450
|
-
if (as?.split('@')[0] === 'act' || as === NAMED_VIEWS.help || prospective || argv.includes(FLAGS.check))
|
|
463
|
+
if (as === 'release' || as?.split('@')[0] === 'act' || as === NAMED_VIEWS.help || prospective || argv.includes(FLAGS.check))
|
|
451
464
|
return foldResolved(root, under, argv);
|
|
452
465
|
return resolveRequested(digest => resolveSources(standingOf(root, under).standing, [digest]), () => foldResolved(root, under, argv));
|
|
453
466
|
}
|
|
454
467
|
async function foldResolved(root, under, argv) {
|
|
455
468
|
const as = valueOf(argv, FLAGS.as);
|
|
469
|
+
if (as === 'release') {
|
|
470
|
+
const approvals = positionalsOf(argv).filter(one => !(existsSync(resolve(one)) && lstatSync(resolve(one)).isDirectory()));
|
|
471
|
+
if (approvals.length !== 1 || argv.includes(FLAGS.check))
|
|
472
|
+
throw Error('REFUSE·release name one approval file; check does not authorize delivery');
|
|
473
|
+
return withoutEffects(() => { printReleaseApproval(under ? resolve(root, under) : root, resolve(approvals[0])); return EXIT.closed; });
|
|
474
|
+
}
|
|
456
475
|
const inputs = positionalsOf(argv).filter(one => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
|
|
457
476
|
// Public content is authenticated by its digest, independently of owner history.
|
|
458
477
|
// A named view does not request mounting every release of the instrument.
|
package/dist/fold/closed.js
CHANGED
|
@@ -4,7 +4,7 @@ import { dirname, existsSync, join, mkdirSync, relative, resolve, statSync, writ
|
|
|
4
4
|
import { byBytes, canonical, LOCK, parse, publicationOf, roleAt, roleClaimsOf } from '@lapxo/topos/wire';
|
|
5
5
|
import { RECEIPTS, fieldOf, foldClaims, isConfig, isWire } from "./claims.js";
|
|
6
6
|
import { bytesDigest as hashBytes, fullDigest as hashFull } from "../host/digest.js";
|
|
7
|
-
import { worldAt } from "./places.js";
|
|
7
|
+
import { mountedCoordinatesIn, worldAt } from "./places.js";
|
|
8
8
|
import { ownLockOf } from "./signed.js";
|
|
9
9
|
import { viewsOf } from "./views.js";
|
|
10
10
|
import { storeAt, storeOf } from "../land/ledger.js";
|
|
@@ -31,6 +31,7 @@ const lockStamp = (root, place) => {
|
|
|
31
31
|
return 'none';
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
|
+
const publishedMounts = (root) => mountedCoordinatesIn(root, (observeText(join(root, LOCK)) ?? '').split('\n').filter(isWire));
|
|
34
35
|
function filesOf(root, place, region, inputs) {
|
|
35
36
|
const step = region.replace(/^receipts\//, '').replace(/\/$/, '');
|
|
36
37
|
const here = join(root, place);
|
|
@@ -41,7 +42,10 @@ function filesOf(root, place, region, inputs) {
|
|
|
41
42
|
const record = parse(line);
|
|
42
43
|
return record.kind === 'fact' ? [record.value.fields] : [];
|
|
43
44
|
}));
|
|
44
|
-
const
|
|
45
|
+
const localMounts = [...mountedCoordinatesIn(here), ...publishedMounts(here)];
|
|
46
|
+
const mounts = [...new Set([...mountedCoordinatesIn(root), ...publishedMounts(root), ...localMounts.map(coordinate => relative(root, join(here, coordinate)))])];
|
|
47
|
+
const owned = coordinatesUnder(join(here, step), root).filter(rel => roleAt(relative(here, join(root, rel)), claims) !== 'foreign'
|
|
48
|
+
&& !mounts.some(mount => rel === mount || rel.startsWith(`${mount}/`)));
|
|
45
49
|
// Foreign worlds are not the place's tree. Retain the external files actually handed to its readers.
|
|
46
50
|
const observed = inputs.filter(line => fieldOf(line, 'measure') === 'observed').flatMap(line => {
|
|
47
51
|
const coordinate = fieldOf(line, 'scope');
|
|
@@ -101,11 +105,13 @@ function namedReceipts(root, store) {
|
|
|
101
105
|
const text = observeText(join(root, RECEIPTS));
|
|
102
106
|
const carried = carriedFrom(store, (text ?? '').split('\n').filter(isWire));
|
|
103
107
|
const out = new Map();
|
|
108
|
+
const mounted = new Set(mountedCoordinatesIn(root));
|
|
104
109
|
for (const line of carried ?? []) {
|
|
105
110
|
if (!isWire(line) || fieldOf(line, 'measure') !== 'digest')
|
|
106
111
|
continue;
|
|
107
112
|
const place = /^write\/([^/]+)\/receipts\.bound$/.exec(fieldOf(line, 'scope'))?.[1];
|
|
108
|
-
|
|
113
|
+
// A retained dependency receipt is input evidence, not a child-place closure.
|
|
114
|
+
if (place && !mounted.has(place))
|
|
109
115
|
out.set(place, fieldOf(line, 'value'));
|
|
110
116
|
}
|
|
111
117
|
return out;
|
package/dist/fold/kept.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare function movedPoints(store: string, now: readonly string[], key:
|
|
|
23
23
|
readonly came: readonly string[];
|
|
24
24
|
} | undefined;
|
|
25
25
|
export declare const foldsElsewhere: () => number;
|
|
26
|
-
export declare function keptPlace(store: string, under?: string): PlaceFold | undefined;
|
|
26
|
+
export declare function keptPlace(store: string, under?: string, currentInstrument?: string): PlaceFold | undefined;
|
|
27
27
|
export declare const keptRender: (store: string, key: string, name: string) => string | undefined;
|
|
28
28
|
export declare const keepRender: (store: string, _signer: string, key: string, name: string, _epoch: number, text_: string) => void;
|
|
29
29
|
export declare function keepFold(store: string, key: string, fold: PlaceFold, points: readonly string[], instrument?: string): void;
|
package/dist/fold/kept.js
CHANGED
|
@@ -108,7 +108,13 @@ function readingContext(store) {
|
|
|
108
108
|
return bytesDigest(store, text([...files.filter(file => existsSync(file)).sort()
|
|
109
109
|
.map(file => bytesDigest(store, observeFile(file) ?? new Uint8Array())), ...segments].join('\n')));
|
|
110
110
|
}
|
|
111
|
-
|
|
111
|
+
/** A new judgment changes the result even when the files and reader history do not. */
|
|
112
|
+
function judgmentContext(store, demands) {
|
|
113
|
+
const scopes = new Set(demands.map(line => fieldOf(line, 'scope')));
|
|
114
|
+
return bytesDigest(store, text(ledgerLines(store, 'judge')
|
|
115
|
+
.filter(line => scopes.has(fieldOf(line, 'scope'))).join('\n')));
|
|
116
|
+
}
|
|
117
|
+
export function keptPlace(store, under, currentInstrument) {
|
|
112
118
|
const key = observeText(memoAt(store, lastOf([`place ${under ?? '.'}`])))?.trim();
|
|
113
119
|
if (key === undefined || key === '')
|
|
114
120
|
return undefined;
|
|
@@ -119,6 +125,10 @@ export function keptPlace(store, under) {
|
|
|
119
125
|
if (blob.readingContext !== readingContext(store))
|
|
120
126
|
return undefined;
|
|
121
127
|
const fold = blob.fold;
|
|
128
|
+
if (currentInstrument !== undefined && fold.instrument !== currentInstrument)
|
|
129
|
+
return undefined;
|
|
130
|
+
if (blob.judgmentContext !== judgmentContext(store, linesOf(fold.demands)))
|
|
131
|
+
return undefined;
|
|
122
132
|
return {
|
|
123
133
|
...fold,
|
|
124
134
|
standing: linesOf(fold.standing),
|
|
@@ -154,7 +164,7 @@ export function keepFold(store, key, fold, points, instrument = '') {
|
|
|
154
164
|
signed: [...fold.signed],
|
|
155
165
|
kept: false,
|
|
156
166
|
};
|
|
157
|
-
landMemo(store, `fold/${key}`, text(JSON.stringify({ fold: packed, points, instrument, readingContext: readingContext(store) })));
|
|
167
|
+
landMemo(store, `fold/${key}`, text(JSON.stringify({ fold: packed, points, instrument, readingContext: readingContext(store), judgmentContext: judgmentContext(store, fold.demands) })));
|
|
158
168
|
const at = memoAt(store, lastOf(points));
|
|
159
169
|
mkdirSync(dirname(at), { recursive: true });
|
|
160
170
|
writeFileSync(`${at}.${process.pid}.part`, `${key}\n`);
|
package/dist/fold/keys.js
CHANGED
|
@@ -2,6 +2,7 @@ import { dirname, existsSync, join } from "../host/io.js";
|
|
|
2
2
|
import { ledgerLines, ledgerWriters, signedLines } from "../land/ledger.js";
|
|
3
3
|
import { fieldOf, foldClaims, isWire, LOCK, selfName } from "./claims.js";
|
|
4
4
|
import { observeText } from "../observe/files.js";
|
|
5
|
+
import { publicLock } from '@lapxo/topos/wire';
|
|
5
6
|
const held = new Map();
|
|
6
7
|
const keyInputs = new Map();
|
|
7
8
|
const merged = new Map();
|
|
@@ -68,13 +69,16 @@ export function releaseOf(store) {
|
|
|
68
69
|
return releases.get(store);
|
|
69
70
|
const at = join(dirname(store), LOCK);
|
|
70
71
|
const lines = ledgerKeyLines(store).length || !existsSync(at) ? [] : (observeText(at) ?? '').split('\n').filter(isWire);
|
|
71
|
-
const
|
|
72
|
+
const published = lines.some(line => fieldOf(line, 'scope').startsWith('keys/') && fieldOf(line, 'measure') === 'public-key' && fieldOf(line, 'sig'))
|
|
73
|
+
&& publicLock(lines) !== undefined;
|
|
74
|
+
const got = published || lines.some((line) => fieldOf(line, 'scope') === 'publish/bootstrap' && fieldOf(line, 'value') !== 'withdraw') ? lines : undefined;
|
|
72
75
|
releases.set(store, got);
|
|
73
76
|
return got;
|
|
74
77
|
}
|
|
75
78
|
function read(store, told = []) {
|
|
76
|
-
const
|
|
77
|
-
|
|
79
|
+
const release = releaseOf(store) ?? [], published = publicLock(release) === undefined ? [] : release;
|
|
80
|
+
const said = [...ledgerKeyLines(store), ...release, ...told]
|
|
81
|
+
.filter((line) => (/\bsig=/.test(line) || published.includes(line)) && /(?:^|\s)scope=keys\/[^/\s]+\s/.test(line));
|
|
78
82
|
const standing = foldClaims(said).standing.filter((line) => fieldOf(line, 'value') !== 'withdraw')
|
|
79
83
|
.sort((a, b) => (Number(fieldOf(a, 'epoch')) || 0) - (Number(fieldOf(b, 'epoch')) || 0));
|
|
80
84
|
const idOf = (line) => fieldOf(line, 'scope').slice('keys/'.length);
|
package/dist/fold/places.d.ts
CHANGED
|
@@ -2,7 +2,11 @@ type Spoken = {
|
|
|
2
2
|
readonly place: string;
|
|
3
3
|
readonly line: string;
|
|
4
4
|
};
|
|
5
|
-
|
|
5
|
+
/** A dependency's lock describes the dependency, never a child of its host.
|
|
6
|
+
* Retiring a mount does not turn its retained bytes into local declarations. */
|
|
7
|
+
export declare function mountedCoordinatesIn(root: string, history?: readonly string[]): readonly string[];
|
|
8
|
+
export declare function localDirectoriesIn(root: string, history?: readonly string[]): readonly string[];
|
|
9
|
+
export declare function placesIn(root: string, history?: readonly string[]): readonly string[];
|
|
6
10
|
/** A directory with no own lock is founded by the root: a line of its region family names the directory's coordinates, and a covered batch that names the place lands its first own lock. */
|
|
7
11
|
export declare const foundableIn: (root: string, standing: readonly string[], family: string, name: string) => boolean;
|
|
8
12
|
export declare const capsuleAt: (root: string, name: string) => boolean;
|
package/dist/fold/places.js
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import { join } from "../host/io.js";
|
|
2
|
-
import { CAPSULE, canonical, LOCK, matches, parse } from '@lapxo/topos/wire';
|
|
2
|
+
import { CAPSULE, canonical, EXTENSION, LOCK, matches, parse } from '@lapxo/topos/wire';
|
|
3
|
+
import { lockLines } from "./keys.js";
|
|
4
|
+
import { releasesOf } from "../host/release.js";
|
|
5
|
+
import { archiveCoordinate } from "../host/archive.js";
|
|
3
6
|
import { fieldOf } from "./claims.js";
|
|
4
7
|
import { isCeiling } from "./configures.js";
|
|
5
8
|
import { entriesIn, observeText } from "../observe/files.js";
|
|
6
9
|
import { ownLock } from "../observe/runner.js";
|
|
7
10
|
import { said } from "./wire.js";
|
|
8
11
|
import { ownLockOf } from "./signed.js";
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
/** A dependency's lock describes the dependency, never a child of its host.
|
|
13
|
+
* Retiring a mount does not turn its retained bytes into local declarations. */
|
|
14
|
+
export function mountedCoordinatesIn(root, history = lockLines(join(root, EXTENSION))) {
|
|
15
|
+
return releasesOf(history).flatMap(line => {
|
|
16
|
+
const needs = fieldOf(line, 'needs');
|
|
17
|
+
return needs ? [archiveCoordinate(needs)] : [];
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function localDirectoriesIn(root, history = lockLines(join(root, EXTENSION))) {
|
|
21
|
+
const mounts = new Set(mountedCoordinatesIn(root, history));
|
|
22
|
+
return entriesIn(root).filter(entry => entry.dir && !mounts.has(entry.name)).map(entry => entry.name).sort();
|
|
23
|
+
}
|
|
24
|
+
export function placesIn(root, history = lockLines(join(root, EXTENSION))) {
|
|
25
|
+
return localDirectoriesIn(root, history).filter(name => observeText(join(root, name, LOCK)) !== undefined);
|
|
26
|
+
}
|
|
11
27
|
/** A directory with no own lock is founded by the root: a line of its region family names the directory's coordinates, and a covered batch that names the place lands its first own lock. */
|
|
12
28
|
export const foundableIn = (root, standing, family, name) => !placesIn(root).includes(name)
|
|
13
29
|
&& (entriesIn(root).find((entry) => entry.name === name)?.dir ?? false)
|
package/dist/fold/readers.js
CHANGED
|
@@ -43,6 +43,7 @@ const reachOfLine = (module, where) => {
|
|
|
43
43
|
*/
|
|
44
44
|
export function capsuleReadersOf(root, store, standing, lines, resolve = capsulesFor) {
|
|
45
45
|
const reaching = new Set(lines.flatMap((reader) => reachOfLine(reader.module, reader.where)));
|
|
46
|
+
const coordinateMeasure = wordOn(standing, 'region-coordinate', ownLock());
|
|
46
47
|
const localMeasures = wordsOf(standing, 'region-measures');
|
|
47
48
|
const pins = standing.filter(line => fieldOf(line, 'scope').startsWith('uses/') && fieldOf(line, 'value') !== 'withdraw');
|
|
48
49
|
const measures = localMeasures.length || !pins.length ? localMeasures
|
|
@@ -51,7 +52,7 @@ export function capsuleReadersOf(root, store, standing, lines, resolve = capsule
|
|
|
51
52
|
const groups = new Map();
|
|
52
53
|
for (const place of ['', ...placesIn(root)]) {
|
|
53
54
|
const where = place ? `${place}/` : '';
|
|
54
|
-
const own = ownLockOf(root, place).filter((line) => fieldOf(line, 'scope').startsWith('region/') && measures.includes(fieldOf(line, 'measure')) && fieldOf(line, 'value') !== 'withdraw');
|
|
55
|
+
const own = ownLockOf(root, place).filter((line) => fieldOf(line, 'scope').startsWith('region/') && measures.includes(fieldOf(line, 'measure')) && fieldOf(line, 'measure') === coordinateMeasure && fieldOf(line, 'value') !== 'withdraw');
|
|
55
56
|
const offered = [...own, ...(capsuleAt(root, place) ? world.filter((line) => !own.some((one) => fieldOf(one, 'scope') === fieldOf(line, 'scope'))) : [])];
|
|
56
57
|
if (!offered.length)
|
|
57
58
|
continue;
|
|
@@ -92,8 +92,6 @@ export function releasePolicyLines(result) {
|
|
|
92
92
|
if (result === undefined)
|
|
93
93
|
return [];
|
|
94
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
95
|
return [
|
|
98
96
|
...gates,
|
|
99
97
|
`RELEASE policy ${result.status} · fold ${result.snapshot.fold} · instrument ${result.snapshot.instrument}`,
|
package/dist/fold/zoom.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { join } from "../host/io.js";
|
|
2
2
|
import { canonical, LOCK, parse, RECEIPTS } from '@lapxo/topos/wire';
|
|
3
3
|
import { fieldOf, foldClaims, isWire } from "./claims.js";
|
|
4
|
-
import {
|
|
4
|
+
import { observeText } from "../observe/files.js";
|
|
5
|
+
import { placesIn } from "./places.js";
|
|
5
6
|
/**
|
|
6
7
|
* A line of the tree seen from inside a place: every path it names under the place is read from the place's root, and
|
|
7
8
|
* a path outside it is dropped, since the place alone cannot read it. The place's name drops out of the scope, the condition
|
|
@@ -46,10 +47,10 @@ export function zoomed(line, place) {
|
|
|
46
47
|
}
|
|
47
48
|
/** The lines of each place's own lock that still name the place: seen from inside a place, no scope carries its name. */
|
|
48
49
|
export function namingTheirPlace(root) {
|
|
49
|
-
return
|
|
50
|
-
const own = (observeText(join(root,
|
|
50
|
+
return placesIn(root).filter(name => !name.startsWith('.')).flatMap((name) => {
|
|
51
|
+
const own = (observeText(join(root, name, LOCK)) ?? '').split('\n').filter(isWire);
|
|
51
52
|
return foldClaims(own).standing.filter((line) => fieldOf(line, 'value') !== 'withdraw')
|
|
52
|
-
.map((line) => fieldOf(line, 'scope')).filter((scope) => scope.startsWith(`${
|
|
53
|
+
.map((line) => fieldOf(line, 'scope')).filter((scope) => scope.startsWith(`${name}/`) || scope.includes(`/${name}/`));
|
|
53
54
|
}).length;
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
@@ -3,7 +3,7 @@ import { join, dirname } from 'node:path';
|
|
|
3
3
|
import { performance } from 'node:perf_hooks';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { canonical, objectHistory, parse, byBytes, wireAt, RECEIPTS } from '@lapxo/topos/wire';
|
|
6
|
-
import { cellInputs, cellReceiptContract } from '@lapxo/topos/cell-inputs';
|
|
6
|
+
import { cellInputs, cellReceiptContract, cellInputProfile } from '@lapxo/topos/cell-inputs';
|
|
7
7
|
import { renderCells } from '@lapxo/topos/cells-view';
|
|
8
8
|
import { admittedObjects } from "./objects.js";
|
|
9
9
|
import { objectFold } from "../fold/object.js";
|
|
@@ -30,6 +30,7 @@ export async function cellsWithReceipts(root, history, emit) {
|
|
|
30
30
|
throw Error(`REFUSE·authority receipt input ${record.fields.id}`);
|
|
31
31
|
const config = parsed.value.configuration.filter(f => authority.of(canonical(f)).kind === 'admitted');
|
|
32
32
|
cellReceiptContract(config);
|
|
33
|
+
const profile = cellInputProfile(config);
|
|
33
34
|
const instrument = ownLock().map(fields);
|
|
34
35
|
const wire = wireAt(instrument, Number.MAX_SAFE_INTEGER);
|
|
35
36
|
if (!wire)
|
|
@@ -38,7 +39,7 @@ export async function cellsWithReceipts(root, history, emit) {
|
|
|
38
39
|
if (!algorithm)
|
|
39
40
|
throw Error('REFUSE·receipt no digest algorithm');
|
|
40
41
|
const code = implementation(algorithm);
|
|
41
|
-
const inputs = cellInputs(parsed.value.objects), semantic = new Map(), inputBytes = new Map();
|
|
42
|
+
const inputs = cellInputs(parsed.value.objects, profile), semantic = new Map(), inputBytes = new Map();
|
|
42
43
|
const pins = new Map();
|
|
43
44
|
for (const record of parsed.value.objects.filter(r => r.record === 'cell')) {
|
|
44
45
|
const pin = record.fields.topos;
|
|
@@ -47,7 +48,10 @@ export async function cellsWithReceipts(root, history, emit) {
|
|
|
47
48
|
pins.set(pin, [pin, ...top.dependencies]);
|
|
48
49
|
}
|
|
49
50
|
const scope = `receipts/cells/${record.fields.scope}`;
|
|
50
|
-
const
|
|
51
|
+
const ownInputs = inputs.get(record.fields.scope);
|
|
52
|
+
const metadata = [...pins.get(pin), ...code, 'resolution=1'];
|
|
53
|
+
const bytes = profile === 'semantic-live@1' ? [...ownInputs, ...metadata].sort(byBytes).join('\n') :
|
|
54
|
+
[...ownInputs, ...metadata.sort(byBytes), `receipt-inputs=${profile}`].join('\n');
|
|
51
55
|
inputBytes.set(scope, bytes);
|
|
52
56
|
semantic.set(scope, fullDigest(bytes, algorithm));
|
|
53
57
|
}
|
|
@@ -27,7 +27,8 @@ export function readingClosures(options) {
|
|
|
27
27
|
if (!scope || plans.has(scope) || f.form !== 'alphabet' || f.measure !== 'reads' || f.role !== 'reads' || !resolution)
|
|
28
28
|
throw Error('REFUSE·input projection ambiguous reading ' + scope);
|
|
29
29
|
const inputs = (f.value ?? '').split('|').filter(Boolean), needs = (f.needs ?? '').split('|').filter(Boolean);
|
|
30
|
-
|
|
30
|
+
// An aggregate reads its declared receipts; it need not reread a leaf file.
|
|
31
|
+
if ((!inputs.length && !needs.length) || new Set(inputs).size !== inputs.length || inputs.some(input => !handed.has(input)) || new Set(needs).size !== needs.length)
|
|
31
32
|
throw Error('REFUSE·input projection has unhanded or duplicate dependencies ' + scope);
|
|
32
33
|
const n = Number(resolution[1]);
|
|
33
34
|
if (!Number.isSafeInteger(n))
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { StandingTopos } from '@lapxo/topos/standing';
|
|
2
|
-
import { wireAt } from '@lapxo/topos/wire';
|
|
2
|
+
import { wireAt, fromLine } from '@lapxo/topos/wire';
|
|
3
3
|
export interface SelectedTopos {
|
|
4
4
|
readonly digest: string;
|
|
5
5
|
readonly topos: StandingTopos;
|
|
6
6
|
}
|
|
7
7
|
export type ToposResolver = (digest: string) => StandingTopos;
|
|
8
|
+
/** Local grammar is sufficient for locally declared records. Resolve selected
|
|
9
|
+
* contracts only when a reader is missing; a local refusal remains a refusal. */
|
|
10
|
+
export declare function admittedLine(line: string, local: readonly Readonly<Record<string, string>>[], epoch: number, pins: readonly string[], resolve?: ToposResolver): ReturnType<typeof fromLine>;
|
|
8
11
|
/** Resolve content using the instrument wire's admitted digest algorithms. No transport interpretation. */
|
|
9
12
|
export declare function toposAt(digest: string, store?: string): StandingTopos;
|
|
10
13
|
export declare function contentAt(digest: string, store?: string): Uint8Array;
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { verifiedContent } from "./content-store.js";
|
|
2
2
|
import { readStanding } from '@lapxo/topos/standing';
|
|
3
|
-
import { parse, wireAt } from '@lapxo/topos/wire';
|
|
3
|
+
import { parse, wireAt, fromLine } from '@lapxo/topos/wire';
|
|
4
4
|
import { fieldOf } from "../fold/claims.js";
|
|
5
|
+
/** Local grammar is sufficient for locally declared records. Resolve selected
|
|
6
|
+
* contracts only when a reader is missing; a local refusal remains a refusal. */
|
|
7
|
+
export function admittedLine(line, local, epoch, pins, resolve = toposAt) {
|
|
8
|
+
const own = wireAt(local, epoch);
|
|
9
|
+
// A signature era alone is not a configuration grammar. An absent list
|
|
10
|
+
// must be supplied by the selected contract; an explicit local list remains
|
|
11
|
+
// authoritative and may refuse a field without opening unrelated content.
|
|
12
|
+
if (own && ['fields', 'required', 'roles', 'forms', 'at-classes'].every(name => own.lists.has(name))) {
|
|
13
|
+
const got = fromLine(line, own);
|
|
14
|
+
const needsObject = fieldOf(line, 'type') !== '' && !own.object;
|
|
15
|
+
if (got.kind === 'fact' || (got.kind === 'refuse' && !needsObject))
|
|
16
|
+
return got;
|
|
17
|
+
}
|
|
18
|
+
return fromLine(line, selectedWireAt(local, epoch, pins, resolve));
|
|
19
|
+
}
|
|
5
20
|
/** Resolve content using the instrument wire's admitted digest algorithms. No transport interpretation. */
|
|
6
21
|
export function toposAt(digest, store) {
|
|
7
22
|
const topos = readStanding(Buffer.from(contentAt(digest, store)).toString('utf8'));
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** A new world selection names standing bytes. Artifact aliases remain separate.
|
|
2
|
+
* Admission reads laid content only; it never fetches or executes a provider. */
|
|
3
|
+
export declare function admitStandingPins(root: string, lines: readonly string[], read?: (digest: string) => Uint8Array): void;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readStanding } from '@lapxo/topos/standing';
|
|
2
|
+
import { fieldOf } from "../fold/claims.js";
|
|
3
|
+
import { contentAt } from "../host/selected-topos.js";
|
|
4
|
+
import { storeOf } from "./ledger.js";
|
|
5
|
+
import { ContentNeeded } from "../host/content-needed.js";
|
|
6
|
+
import { observeFile } from "../observe/files.js";
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { resolve, relative } from 'node:path';
|
|
9
|
+
/** A declared local file is already held content. Check its bytes before
|
|
10
|
+
* admission; neither fetch it nor write a CAS entry during this inspection. */
|
|
11
|
+
function heldStanding(root, line) {
|
|
12
|
+
const digest = fieldOf(line, 'value');
|
|
13
|
+
try {
|
|
14
|
+
return contentAt(digest, storeOf(root));
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (!(error instanceof ContentNeeded))
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
const name = fieldOf(line, 'needs');
|
|
21
|
+
if (!name || name.includes('|') || name.includes('*'))
|
|
22
|
+
throw new ContentNeeded(digest);
|
|
23
|
+
const path = resolve(root, name), local = relative(resolve(root), path);
|
|
24
|
+
if (!local || local === '..' || local.startsWith('../'))
|
|
25
|
+
throw Error('standing coordinate is outside the place');
|
|
26
|
+
const bytes = observeFile(path);
|
|
27
|
+
if (bytes === undefined)
|
|
28
|
+
throw new ContentNeeded(digest);
|
|
29
|
+
const split = digest.indexOf(':'), algorithm = digest.slice(0, split), hex = digest.slice(split + 1);
|
|
30
|
+
if (createHash(algorithm).update(bytes).digest('hex') !== hex)
|
|
31
|
+
throw Error('standing coordinate content hash mismatch');
|
|
32
|
+
return bytes;
|
|
33
|
+
}
|
|
34
|
+
/** A new world selection names standing bytes. Artifact aliases remain separate.
|
|
35
|
+
* Admission reads laid content only; it never fetches or executes a provider. */
|
|
36
|
+
export function admitStandingPins(root, lines, read) {
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
const scope = fieldOf(line, 'scope'), value = fieldOf(line, 'value');
|
|
39
|
+
const parts = scope.split('/');
|
|
40
|
+
if (!(scope === 'uses' || parts.slice(0, -1).includes('uses')) || value === 'withdraw')
|
|
41
|
+
continue;
|
|
42
|
+
try {
|
|
43
|
+
readStanding(new TextDecoder('utf-8', { fatal: true }).decode(read ? read(value) : heldStanding(root, line)));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw Error(`REFUSE·pin ${scope} ${value} · ${error instanceof Error ? error.message : String(error)} · nothing landed`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
package/docs/contracts/cli.md
CHANGED
|
@@ -17,3 +17,11 @@ The opt-in `local-act@1` contract is selected by `wire/act-result`, `wire/act-re
|
|
|
17
17
|
Admission publishes the original signed records, their logical placements and one native result receipt atomically. A failure before commit writes no partial act. The result names the committed digest and local epoch; replay returns the original result without another epoch or origin. A post-commit error is not a rollback. Read the committed result before deciding whether to retry.
|
|
18
18
|
|
|
19
19
|
The query verifies the record signatures and coverage against the authority snapshot at admission, and the native receipt against its retained provenance. Later key retirement or profile replacement cannot authorize a new act but does not erase a verifiable historical result. Admission is not conformance: the output says `admitted · conformance not asserted`. Missing or corrupt results refuse by name. Host paths and delivery counts are not identity inputs.
|
|
20
|
+
|
|
21
|
+
## Release approval
|
|
22
|
+
|
|
23
|
+
`fold --as release <approval-file>` verifies the external approval against the place authorizer, its admitted public key, coverage and signature era. Exactly one signed release/<tag> digest, release/artifact name and sources/archive URL are required. It prints `READY sha256:<digest> <name> <source>` or refuses with the coordinate and cause. This is an approval judgment, distinct from the place release-policy verdict. It never fetches, executes, signs or lands.
|
|
24
|
+
|
|
25
|
+
## Standing admission
|
|
26
|
+
|
|
27
|
+
A uses/ selection admits canonical standing bytes, never an archive. The bytes must already be held under their verified digest or in the single local coordinate explicitly named by needs. Admission checks identity and canonical form without fetching, executing or writing the content store. Missing or corrupt bytes refuse the whole lot. A corrupt held copy cannot be hidden by another source. Archives remain separate dependency declarations.
|
|
@@ -4,3 +4,7 @@ The declared evidence@8 view projects authenticated delivery history of this pla
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
The no sample specifies an uncovered write and tampered signed bytes. Both must refuse by name. Exact withdrawals remove only covered normative lines; authenticated evidence remains inspectable. The vectors assert status, signature verification and observable CLI output. No private key bytes are printed or shipped.
|
|
7
|
+
|
|
8
|
+
## Folded standing
|
|
9
|
+
|
|
10
|
+
A place declares `view/standing role=demands form=alphabet measure=id value=standing`. `fold --as standing` authenticates each local line against its admission snapshot, then applies revisions and covered exact withdrawals through the existing fold. Every declared key class participates, including attest. Committed acts and historical journals have the same public output. Retiring a key does not erase its previously admitted evidence. Replay adds nothing. Foreign history grants no local authority. This projection is distinct from authenticated delivery history and does not create a new authority lock. An undeclared standing view refuses by name.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lapxo/bound",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Read the bounds you already wrote, fold them, and name the contradictions.",
|
|
6
6
|
"main": "dist/cli/verb.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@lapxo/obligations": "0.3.5",
|
|
36
|
-
"@lapxo/topos": "
|
|
36
|
+
"@lapxo/topos": "0.1.13",
|
|
37
37
|
"acorn": "8.19.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|