@lapxo/bound 0.1.5 → 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/README.md +8 -8
- package/TARGET.bound +8 -7
- package/dist/cli/accept.d.ts +5 -0
- package/dist/cli/accept.js +40 -6
- package/dist/cli/act-result.d.ts +3 -0
- package/dist/cli/act-result.js +48 -0
- package/dist/cli/owner.js +3 -2
- package/dist/cli/verb.js +13 -1
- package/dist/fold/admission-history.d.ts +6 -0
- package/dist/fold/admission-history.js +22 -0
- package/dist/fold/digests.js +13 -9
- package/dist/fold/kept.js +9 -4
- package/dist/fold/keys.js +9 -6
- package/dist/fold/observed.js +8 -7
- package/dist/fold/signed.d.ts +5 -0
- package/dist/fold/signed.js +19 -15
- package/dist/host/capsule.d.ts +2 -1
- package/dist/host/capsule.js +8 -6
- package/dist/host/io.d.ts +1 -1
- package/dist/host/io.js +1 -1
- package/dist/host/ports/ingress.js +4 -28
- 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 +3 -0
- package/dist/host/reading-closures.js +3 -3
- package/dist/host/selected-capsules.js +4 -4
- 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/docs/contracts/cli.md +8 -0
- package/package.json +2 -2
- package/receipts.bound +382 -314
|
@@ -18,7 +18,7 @@ export function readingClosures(options) {
|
|
|
18
18
|
if (!readFileSync(join(root, file.place)).equals(Buffer.from(file.text)))
|
|
19
19
|
throw Error('REFUSE·input changed ' + file.place); };
|
|
20
20
|
verify();
|
|
21
|
-
const projected = capsule.ask([{ protocol: PROTOCOL, verb: 'render', rootScope: '', region: projection, at: 1, reads: ['**'], lines: inputLines(files), files: [] }])[0];
|
|
21
|
+
const projected = capsule.ask([{ protocol: PROTOCOL, verb: 'render', rootScope: '', region: projection, at: 1, reads: ['**'], lines: inputLines(files), files: [] }], options.readerReference)[0];
|
|
22
22
|
if (projected?.kind !== 'fact' || !Array.isArray(projected.lines))
|
|
23
23
|
throw Error('REFUSE·input projection ' + (projected?.why ?? 'no lines'));
|
|
24
24
|
const plans = new Map();
|
|
@@ -44,7 +44,7 @@ export function readingClosures(options) {
|
|
|
44
44
|
visit(need); visiting.delete(scope); done.add(scope); ordered.push(p); };
|
|
45
45
|
for (const scope of plans.keys())
|
|
46
46
|
visit(scope);
|
|
47
|
-
const identity = hash([capsule.selection ?? '', capsule.digest, region, projection].join('\n')), by = speaker + ':' + identity.split(':')[1].slice(0, 12), journal = 'reading-closures/' + identity.split(':')[1];
|
|
47
|
+
const identity = hash([capsule.selection ?? '', capsule.digest, region, projection, ...(options.lifetime === undefined ? [] : [JSON.stringify(options.lifetime)])].join('\n')), by = speaker + ':' + identity.split(':')[1].slice(0, 12), journal = 'reading-closures/' + identity.split(':')[1];
|
|
48
48
|
const prior = shardLines(store, speaker, journal), results = new Map(), out = [];
|
|
49
49
|
const carried = (digest) => join(store, 'cas', 'carried', digest.split(':')[1] ?? '');
|
|
50
50
|
const saved = (text) => { const digest = hash(text), path = carried(digest); mkdirSync(dirname(path), { recursive: true }); if (!existsSync(path) || hash(readFileSync(path, 'utf8')) !== digest)
|
|
@@ -74,7 +74,7 @@ export function readingClosures(options) {
|
|
|
74
74
|
if (readOnly())
|
|
75
75
|
throw Error('REFUSE·preview reading closure is unpaid ' + plan.scope);
|
|
76
76
|
const started = process.hrtime.bigint();
|
|
77
|
-
const dependencies = plan.needs.flatMap(need => results.get(need).lines), answer = capsule.ask([{ protocol: PROTOCOL, verb: 'read', rootScope: plan.scope, name: plan.scope, region, at: plan.resolution, reads: ['**'], lines: [...inputLines(plan.inputs.map(place => ({ place, text: handed.get(place) }))), ...dependencies], files: [] }])[0];
|
|
77
|
+
const dependencies = plan.needs.flatMap(need => results.get(need).lines), answer = capsule.ask([{ protocol: PROTOCOL, verb: 'read', rootScope: plan.scope, name: plan.scope, region, at: plan.resolution, reads: ['**'], lines: [...inputLines(plan.inputs.map(place => ({ place, text: handed.get(place) }))), ...dependencies], files: [] }], options.readerReference)[0];
|
|
78
78
|
if (answer?.kind !== 'fact' || !Array.isArray(answer.claims))
|
|
79
79
|
throw Error('REFUSE·reading ' + plan.scope + ' · ' + (answer?.why ?? 'no claims'));
|
|
80
80
|
lines = answer.claims.map((unknown) => { if (typeof unknown?.scope !== 'string' || typeof unknown?.measure !== 'string' || typeof unknown?.role !== 'string' || !unknown.bound || !['interval', 'enumerated'].includes(unknown.bound.kind))
|
|
@@ -25,12 +25,12 @@ export function selectedCapsules(pins, resolve = capsuleAt, select = toposAt, ho
|
|
|
25
25
|
const descriptor = capsuleDescriptor(topos, offer.scope ?? '');
|
|
26
26
|
const loaded = descriptor === undefined ? load() : undefined;
|
|
27
27
|
const capsule = loaded?.capsule ?? {
|
|
28
|
-
digest: offer.value, selection: `${digest} ${offer.scope}`, ...descriptor, ask: requests => {
|
|
28
|
+
digest: offer.value, selection: `${digest} ${offer.scope}`, ...descriptor, ask: (requests, reader) => {
|
|
29
29
|
if (!requests.length)
|
|
30
30
|
return [];
|
|
31
31
|
const actual = load();
|
|
32
32
|
assertCapsuleDescriptor(descriptor.lines, actual.capsule.lines);
|
|
33
|
-
return actual.capsule.ask(requests);
|
|
33
|
+
return actual.capsule.ask(requests, reader);
|
|
34
34
|
},
|
|
35
35
|
};
|
|
36
36
|
const projected = loaded?.offer ?? canonical(offer);
|
|
@@ -40,10 +40,10 @@ export function selectedCapsules(pins, resolve = capsuleAt, select = toposAt, ho
|
|
|
40
40
|
const wire = wireAt(fields, Number.MAX_SAFE_INTEGER), algorithm = wire && [...wire.digests][0];
|
|
41
41
|
if (!algorithm)
|
|
42
42
|
throw Error('REFUSE·input instrument has no admitted digest algorithm');
|
|
43
|
-
const selected = { ...capsule, selection: `${digest} ${offer.scope}`, ask: requests => capsule.ask(requests.map(request => {
|
|
43
|
+
const selected = { ...capsule, selection: `${digest} ${offer.scope}`, ask: (requests, reader) => capsule.ask(requests.map(request => {
|
|
44
44
|
const provider = providerInputs(topos, digest, offer.scope ?? '', offer.value, request.region ?? '', bytes => fullDigest(bytes, algorithm));
|
|
45
45
|
return provider === undefined ? request : { ...request, provider };
|
|
46
|
-
})) };
|
|
46
|
+
}), reader) };
|
|
47
47
|
return { capsule: selected, offer: projected };
|
|
48
48
|
}));
|
|
49
49
|
}
|
package/dist/land/ledger.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare function writerFile(store: string, writer: string): string;
|
|
|
10
10
|
export declare function ledgerLines(store: string, writer: string): readonly string[];
|
|
11
11
|
export declare function shardFile(store: string, writer: string, region: string): string;
|
|
12
12
|
export declare const shardLines: (store: string, writer: string, region: string) => readonly string[];
|
|
13
|
+
export declare function ledgerWriters(store: string): readonly string[];
|
|
13
14
|
export declare function landShard(store: string, writer: string, region: string, lines: readonly string[]): number;
|
|
14
15
|
export declare function signedLines(store: string, writer: string): string[];
|
|
15
16
|
export declare function blobAt(store: string, digest: string): string;
|
package/dist/land/ledger.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { actLines, committedLocalActs } from "../host/ports/local-act.js";
|
|
2
|
+
import { readdirSync } from 'node:fs';
|
|
1
3
|
import { requireEffects } from "../host/read-only.js";
|
|
2
4
|
import { appendFileSync, closeSync, dirname, existsSync, join, linkSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync } from "../host/io.js";
|
|
3
5
|
import { storeRoot } from "../host/ports/store.js";
|
|
@@ -21,12 +23,29 @@ export function writerFile(store, writer) {
|
|
|
21
23
|
}
|
|
22
24
|
const held = new Map();
|
|
23
25
|
export function ledgerLines(store, writer) {
|
|
24
|
-
return linesAt(writerFile(store, writer));
|
|
26
|
+
return journalLines(store, writer, '.', linesAt(writerFile(store, writer)));
|
|
25
27
|
}
|
|
26
28
|
export function shardFile(store, writer, region) {
|
|
27
29
|
return storeAt(store, 'ledger', writer.replace(/[^a-zA-Z0-9_-]/g, ''), `${region.replace(/\/+$/, '') || '@'}${EXTENSION}`);
|
|
28
30
|
}
|
|
29
|
-
export const shardLines = (store, writer, region) => linesAt(shardFile(store, writer, region));
|
|
31
|
+
export const shardLines = (store, writer, region) => journalLines(store, writer, region, linesAt(shardFile(store, writer, region)));
|
|
32
|
+
const journalMemo = new Map();
|
|
33
|
+
function journalLines(store, writer, place, legacy) {
|
|
34
|
+
const fresh = committedLocalActs(store).flatMap(act => act.bundle.placements?.filter(p => p.place === place).flatMap(p => actLines(p.records).filter(line => fieldOf(line, 'by') === writer)) ?? []);
|
|
35
|
+
if (!fresh.length)
|
|
36
|
+
return legacy;
|
|
37
|
+
const lines = [...new Set([...legacy, ...fresh])], key = store + '\0' + writer + '\0' + place, last = journalMemo.get(key);
|
|
38
|
+
if (last?.length === lines.length && last.every((line, i) => line === lines[i]))
|
|
39
|
+
return last;
|
|
40
|
+
journalMemo.set(key, lines);
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
export function ledgerWriters(store) {
|
|
44
|
+
const at = join(store, 'ledger');
|
|
45
|
+
const legacy = existsSync(at) ? readdirSync(at).filter(name => name.endsWith(EXTENSION)).map(name => name.slice(0, -EXTENSION.length)) : [];
|
|
46
|
+
const committed = committedLocalActs(store).flatMap(act => act.bundle.placements?.filter(p => p.place === '.').flatMap(p => actLines(p.records).map(line => fieldOf(line, 'by'))) ?? []);
|
|
47
|
+
return [...new Set([...legacy, ...committed])];
|
|
48
|
+
}
|
|
30
49
|
export function landShard(store, writer, region, lines) {
|
|
31
50
|
return appendNew(shardFile(store, writer, region), lines);
|
|
32
51
|
}
|
package/dist/land/owned.d.ts
CHANGED
|
@@ -20,4 +20,4 @@ export type Owned = {
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function ownedOf(root: string, signed: readonly string[], admits: (line: string) => boolean, coverage: (by: string) => readonly string[], named?: readonly string[], placedAt?: (line: string) => readonly string[] | undefined): Owned;
|
|
22
22
|
/** A place's own lock gains the lines a batch landed there, appended whole: a reader never meets half of it, and every line already there keeps its bytes as they were. */
|
|
23
|
-
export declare function landOwned(root: string, place: string, lines: readonly string[]): void;
|
|
23
|
+
export declare function landOwned(root: string, place: string, lines: readonly string[], committed?: boolean): void;
|
package/dist/land/owned.js
CHANGED
|
@@ -51,14 +51,15 @@ export function ownedOf(root, signed, admits, coverage, named = [], placedAt = (
|
|
|
51
51
|
return { ledger, own, nowhere: [...placed, ...rest.filter((line) => !lands(line))], forked };
|
|
52
52
|
}
|
|
53
53
|
/** A place's own lock gains the lines a batch landed there, appended whole: a reader never meets half of it, and every line already there keeps its bytes as they were. */
|
|
54
|
-
export function landOwned(root, place, lines) {
|
|
54
|
+
export function landOwned(root, place, lines, committed = false) {
|
|
55
55
|
if (!lines.length)
|
|
56
56
|
return tellOwn(place, []);
|
|
57
57
|
const at = join(root, place, LOCK);
|
|
58
58
|
const text = observeText(at) ?? '';
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
if (!committed)
|
|
60
|
+
for (const by of new Set(lines.map(line => fieldOf(line, 'by')))) {
|
|
61
|
+
landShard(storeOf(root), by, `${place}/${LOCK}`, lines.filter(line => fieldOf(line, 'by') === by));
|
|
62
|
+
}
|
|
62
63
|
writeFileSync(`${at}.${process.pid}.landing`, `${text.replace(/\n*$/, '\n')}${lines.join('\n')}\n`);
|
|
63
64
|
renameSync(`${at}.${process.pid}.landing`, at);
|
|
64
65
|
tellOwn(place, []);
|
package/dist/observe/files.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, lstatSync, readFileSync, readdirSync, readlinkSync } from 'node:fs';
|
|
1
|
+
import { existsSync, lstatSync, statSync, readFileSync, readdirSync, readlinkSync } from 'node:fs';
|
|
2
2
|
import { join, relative, sep } from 'node:path';
|
|
3
3
|
/**
|
|
4
4
|
* The one place the instrument reads the world: a coordinate's bytes, the entries of a directory, the coordinates under a place,
|
|
@@ -7,7 +7,17 @@ import { join, relative, sep } from 'node:path';
|
|
|
7
7
|
const seen = new Map();
|
|
8
8
|
/** A coordinate read once is kept by its stamp — its inode, size and time — and read again only when the stamp moves. */
|
|
9
9
|
function bytesAt(at) {
|
|
10
|
-
|
|
10
|
+
// Reading follows a link, so its cache stamp must describe the same target.
|
|
11
|
+
// Directory links have no file bytes; traversal keeps its separate link policy.
|
|
12
|
+
let st = stampable(at);
|
|
13
|
+
if (st?.isSymbolicLink()) {
|
|
14
|
+
try {
|
|
15
|
+
st = statSync(at);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
11
21
|
if (st === undefined || st.isDirectory() || !existsSync(at))
|
|
12
22
|
return undefined;
|
|
13
23
|
const stamp = `${st.ino}-${st.size}-${st.mtimeMs}`;
|
package/docs/contracts/cli.md
CHANGED
|
@@ -9,3 +9,11 @@
|
|
|
9
9
|
# sign
|
|
10
10
|
|
|
11
11
|
`bound sign <lot>` returns canonical signed lines after validating the whole lot. The admitted key selects a declared signer; `--signer` selects the same port explicitly. File and command mechanisms use one port. Topos signing-lot/1 frames canonical bytes in lot order, one invocation per lot, and returns signatures and public-key IDs. Verification checks the admitted public key, independently of the mechanism. A command is invoked without a shell and under declared bounds. No secret output is disclosed in refusals. Missing, invalid, partial, timed-out or failed responses refuse; `land` cannot commit their partial output. The sample compares file and command signatures and landed lines, checks invocation count and ledger hashes, and refuses an unknown wire version. The sample uses ephemeral keys outside the place; no owner key is included.
|
|
12
|
+
|
|
13
|
+
# Committed act results
|
|
14
|
+
|
|
15
|
+
The opt-in `local-act@1` contract is selected by `wire/act-result`, `wire/act-result-scope` and `wire/act-result-context`. An admitted `view/act measure=id value=act` makes `bound fold --as act <identity>` available. Missing or conflicting declarations refuse; an absent profile preserves the historical interface. Topos defines the canonical record and result contract.
|
|
16
|
+
|
|
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
|
+
|
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lapxo/bound",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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": "^0.1.
|
|
36
|
+
"@lapxo/topos": "^0.1.12",
|
|
37
37
|
"acorn": "8.19.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|