@lapxo/bound 0.2.3 → 0.2.4
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/.github/workflows/release.yml +67 -0
- package/.github/workflows/test.yml +35 -0
- package/CITATION.cff +19 -0
- package/TARGET.bound +71 -20
- package/dist/cli/accept.js +103 -71
- package/dist/cli/place.js +3 -2
- package/dist/cli/standing-result.js +3 -5
- package/dist/cli/takes.js +4 -3
- package/dist/cli/verb.js +2 -1
- package/dist/engine/ports.d.ts +6 -1
- package/dist/engine/ports.js +23 -9
- package/dist/fold/claims.d.ts +29 -0
- package/dist/fold/claims.js +2 -1
- package/dist/fold/closed.js +2 -1
- package/dist/fold/digests.js +7 -7
- package/dist/host/admitted-place.js +3 -31
- package/dist/host/call-context.d.ts +20 -0
- package/dist/host/call-context.js +14 -0
- package/dist/host/capsule-process.js +2 -1
- package/dist/host/capsule.js +7 -4
- package/dist/host/commit-result.d.ts +6 -0
- package/dist/host/commit-result.js +5 -0
- package/dist/host/contextual-place.js +9 -14
- package/dist/host/host-controls.js +19 -0
- package/dist/host/in-process-act.d.ts +82 -0
- package/dist/host/in-process-act.js +84 -0
- package/dist/host/in-process-context.d.ts +46 -0
- package/dist/host/in-process-context.js +91 -0
- package/dist/host/in-process-root.d.ts +117 -0
- package/dist/host/in-process-root.js +84 -0
- package/dist/host/in-process.d.ts +41 -0
- package/dist/host/in-process.js +55 -0
- package/dist/host/index.d.ts +15 -0
- package/dist/host/index.js +7 -0
- package/dist/host/native-receiver.d.ts +19 -0
- package/dist/host/native-receiver.js +25 -0
- package/dist/host/place-act-transaction.d.ts +24 -0
- package/dist/host/place-act-transaction.js +78 -0
- package/dist/host/place-clock-selection.d.ts +55 -0
- package/dist/host/place-clock-selection.js +1 -1
- package/dist/host/ports/local-act.d.ts +44 -0
- package/dist/host/ports/local-act.js +13 -2
- package/dist/host/ports/place-act.d.ts +49 -0
- package/dist/host/ports/place-act.js +19 -54
- package/dist/host/ports/publication.d.ts +10 -0
- package/dist/host/root-clock-source.d.ts +86 -0
- package/dist/host/root-clock-source.js +7 -5
- package/dist/host/signature-era.js +12 -0
- package/dist/host/signing.js +1 -1
- package/dist/host/source-content.js +2 -1
- package/dist/host/standing-carrier.js +18 -0
- package/dist/host/standing-projection.js +50 -0
- package/dist/land/act.js +60 -26
- package/dist/observe/run.js +2 -1
- package/dist/observe/runner.js +16 -8
- package/docs/contracts/native-host.md +21 -0
- package/docs/contracts/native-host.publication.json +25 -0
- package/docs/contracts/native-publication.inventory.json +2 -1
- package/docs/contracts/publication.inventory.json +29 -0
- package/docs/guide.md +10 -0
- package/docs/reference.md +39 -0
- package/docs/why.md +109 -0
- package/package.json +32 -2
- package/receipts.bound +10369 -9723
- package/samples/native-host/no.json +10 -0
- package/samples/native-host/yes.json +20 -0
- package/vectors/native/native-host.vector.json +62 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type TrustedClockContract, type PlaceReceipt, type RootContext } from '@lapxo/topos/core';
|
|
2
|
+
import { type PlaceActPersistence } from '../place-act-transaction.ts';
|
|
3
|
+
import { type PublicationStorage } from './publication.ts';
|
|
4
|
+
export interface PlaceActBudget {
|
|
5
|
+
readonly acts: number;
|
|
6
|
+
readonly bytes: number;
|
|
7
|
+
}
|
|
8
|
+
export interface PlaceActHead {
|
|
9
|
+
readonly place: string;
|
|
10
|
+
readonly identity: string;
|
|
11
|
+
readonly epoch: string;
|
|
12
|
+
}
|
|
13
|
+
export interface PlaceActSnapshot {
|
|
14
|
+
readonly root: RootContext;
|
|
15
|
+
readonly heads: readonly PlaceActHead[];
|
|
16
|
+
}
|
|
17
|
+
/** Fixed instrument construction, never supplied by world JSON. Historical
|
|
18
|
+
* authentication permits inspection only; current() is read under Root guards. */
|
|
19
|
+
export interface PlaceActTrust {
|
|
20
|
+
readonly contract: TrustedClockContract;
|
|
21
|
+
readonly initialHeads: readonly PlaceActHead[];
|
|
22
|
+
readonly current: () => PlaceActSnapshot;
|
|
23
|
+
readonly historical: (encoded: string) => TrustedClockContract;
|
|
24
|
+
}
|
|
25
|
+
export interface PlaceActBundle {
|
|
26
|
+
readonly context: string;
|
|
27
|
+
readonly receipts: readonly PlaceReceipt[];
|
|
28
|
+
readonly originals: readonly (readonly string[])[];
|
|
29
|
+
}
|
|
30
|
+
/** Historical result reads never produce a current authorization permit. */
|
|
31
|
+
export declare function placeActResult(store: string, id: string, trust: Pick<PlaceActTrust, 'historical'>, limits: PlaceActBudget): PlaceActBundle | undefined;
|
|
32
|
+
/** Each receiver chain advances independently. Initial heads are explicit,
|
|
33
|
+
* authenticated host inputs, not inferred zero or legacy global epochs. */
|
|
34
|
+
export declare function placeActHeads(store: string, initial: readonly PlaceActHead[], trust: Pick<PlaceActTrust, 'historical' | 'contract'>, limits: PlaceActBudget): readonly PlaceActHead[];
|
|
35
|
+
/** Context, exact original bytes and complete signed receipts have one visibility
|
|
36
|
+
* point. Real Root guard exclusion spans final authority/head comparison and
|
|
37
|
+
* publication; this does not activate a CLI profile or reinterpret local-act@1. */
|
|
38
|
+
/** Native persistence adapter. The transaction below is also the CLI path. */
|
|
39
|
+
export declare function nativePlaceActPersistence(store: string, storage?: PublicationStorage): PlaceActPersistence;
|
|
40
|
+
export declare function commitPlaceAct(store: string, encoded: string, receipts: readonly PlaceReceipt[], trust: PlaceActTrust, limits: PlaceActBudget, storage?: PublicationStorage): Promise<PlaceActBundle & {
|
|
41
|
+
readonly kind: "landed" | "once";
|
|
42
|
+
readonly identity: string;
|
|
43
|
+
}>;
|
|
44
|
+
/** Enumerates only committed, historically authenticated carriers. Namespace
|
|
45
|
+
* projection is a caller's separate pure operation, never current permission. */
|
|
46
|
+
export declare function committedPlaceActResults(store: string, trust: Pick<PlaceActTrust, 'historical'>, limits: PlaceActBudget): readonly {
|
|
47
|
+
identity: string;
|
|
48
|
+
bundle: PlaceActBundle;
|
|
49
|
+
}[];
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync, openSync, fstatSync, readSync, closeSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { readPlaceActContext,
|
|
4
|
-
import {
|
|
3
|
+
import { readPlaceActContext, placeActReceiptsOf } from '@lapxo/topos/core';
|
|
4
|
+
import { transactPlaceAct } from "../place-act-transaction.js";
|
|
5
|
+
import { canonicalActStore, underAuthorityRegions, underTheRegions, underRootProjection } from "../../land/act.js";
|
|
5
6
|
import { requireEffects } from "../read-only.js";
|
|
7
|
+
import { receiverClock } from "../host-controls.js";
|
|
6
8
|
import { publishBundle, nativePublicationStorage } from "./publication.js";
|
|
7
9
|
const fail = (why) => { throw Error('REFUSE·place act ' + why); };
|
|
8
|
-
const clock = (n) => { if (!/^(0|[1-9][0-9]*)$/.test(n) || !Number.isSafeInteger(Number(n)))
|
|
9
|
-
fail('unknown receiver clock'); return Number(n); };
|
|
10
|
-
const rootBytes = (r) => JSON.stringify([r.namespace, r.snapshot, r.epoch, r.prefix, r.head, r.policies]);
|
|
11
10
|
const budget = (b) => { if (!b || !Number.isSafeInteger(b.acts) || b.acts < 1 || !Number.isSafeInteger(b.bytes) || b.bytes < 1)
|
|
12
11
|
fail('missing storage budget'); };
|
|
13
12
|
const base = (store) => join(store, 'ledger', 'place-acts');
|
|
@@ -86,7 +85,7 @@ export function placeActHeads(store, initial, trust, limits) {
|
|
|
86
85
|
budget(limits);
|
|
87
86
|
if (!initial.length || new Set(initial.map(h => h.place)).size !== initial.length)
|
|
88
87
|
fail('missing/ambiguous initial heads');
|
|
89
|
-
const heads = new Map(initial.map(h => {
|
|
88
|
+
const heads = new Map(initial.map(h => { receiverClock(h.epoch, fail); if (!h.identity)
|
|
90
89
|
fail('missing initial head'); return [h.place, { ...h }]; }));
|
|
91
90
|
if (!existsSync(base(store)))
|
|
92
91
|
return [...heads.values()];
|
|
@@ -100,8 +99,8 @@ export function placeActHeads(store, initial, trust, limits) {
|
|
|
100
99
|
const act = pending.splice(next, 1)[0];
|
|
101
100
|
for (const p of act.context.places) {
|
|
102
101
|
const prior = heads.get(p.place);
|
|
103
|
-
const epoch =
|
|
104
|
-
if (!Number.isSafeInteger(epoch) ||
|
|
102
|
+
const epoch = receiverClock(prior.epoch, fail) + 1;
|
|
103
|
+
if (!Number.isSafeInteger(epoch) || receiverClock(p.epoch, fail) !== epoch)
|
|
105
104
|
fail('receiver chain clock mismatch');
|
|
106
105
|
heads.set(p.place, { place: p.place, identity: act.id, epoch: p.epoch });
|
|
107
106
|
}
|
|
@@ -111,54 +110,20 @@ export function placeActHeads(store, initial, trust, limits) {
|
|
|
111
110
|
/** Context, exact original bytes and complete signed receipts have one visibility
|
|
112
111
|
* point. Real Root guard exclusion spans final authority/head comparison and
|
|
113
112
|
* publication; this does not activate a CLI profile or reinterpret local-act@1. */
|
|
113
|
+
/** Native persistence adapter. The transaction below is also the CLI path. */
|
|
114
|
+
export function nativePlaceActPersistence(store, storage = nativePublicationStorage) {
|
|
115
|
+
store = canonicalActStore(store);
|
|
116
|
+
return {
|
|
117
|
+
exclusive: (regions, run) => { const enter = regions.includes('') ? underTheRegions : underAuthorityRegions; return enter(store, regions, 'place act', () => underRootProjection(store, 'place act publication', run)); },
|
|
118
|
+
result: (identity, trust, limits) => placeActResult(store, identity, trust, limits),
|
|
119
|
+
inventory: limits => storedInventory(store, limits),
|
|
120
|
+
heads: (trust, limits) => placeActHeads(store, trust.initialHeads, trust, limits),
|
|
121
|
+
publish: (identity, files) => publishBundle(at(store, identity), files, storage),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
114
124
|
export async function commitPlaceAct(store, encoded, receipts, trust, limits, storage = nativePublicationStorage) {
|
|
115
125
|
requireEffects('place act commitment');
|
|
116
|
-
|
|
117
|
-
const decoded = readPlaceActContext(encoded, trust.contract.budget), id = trust.contract.digest(encoded);
|
|
118
|
-
const regions = decoded.places.map(p => { if (p.place === '.')
|
|
119
|
-
return ''; const parts = p.place.split('/'); if (parts.length < 2 || parts.pop() !== 'TARGET.bound' || parts.some(s => s === '' || s === '.' || s === '..'))
|
|
120
|
-
fail('unknown place namespace'); return parts.join('/'); });
|
|
121
|
-
const enter = regions.includes('') ? underTheRegions : underAuthorityRegions;
|
|
122
|
-
return enter(store, regions, 'place act', () => underRootProjection(store, 'place act publication', () => {
|
|
123
|
-
const prior = placeActResult(store, id, trust, limits);
|
|
124
|
-
if (prior) {
|
|
125
|
-
if (prior.context !== encoded)
|
|
126
|
-
fail('replay bytes differ');
|
|
127
|
-
// Replay authenticates the supplied evidence too. Historical authentication
|
|
128
|
-
// grants no current authority and leaves the committed result unchanged.
|
|
129
|
-
placeActReceiptsOf(encoded, receipts, trust.historical(encoded));
|
|
130
|
-
return { ...prior, identity: id, kind: 'once' };
|
|
131
|
-
}
|
|
132
|
-
const verified = verifyPlaceActContext(encoded, trust.contract);
|
|
133
|
-
placeActReceiptsOf(encoded, receipts, trust.contract);
|
|
134
|
-
const originals = decoded.places.map(p => p.records), files = { 'context.json': encoded, 'receipts.json': JSON.stringify(receipts) + '\n', 'originals.json': JSON.stringify(originals) + '\n' };
|
|
135
|
-
const inventory = storedInventory(store, limits);
|
|
136
|
-
if (inventory.bytes + Object.values(files).reduce((n, v) => n + Buffer.byteLength(v), 0) > limits.bytes)
|
|
137
|
-
fail('publication byte budget');
|
|
138
|
-
if (inventory.names.length >= limits.acts)
|
|
139
|
-
fail('publication act budget');
|
|
140
|
-
const current = trust.current();
|
|
141
|
-
if (rootBytes(current.root) !== rootBytes(verified.dependencies))
|
|
142
|
-
fail('authority changed before publication');
|
|
143
|
-
if (new Set(current.heads.map(h => h.place)).size !== current.heads.length)
|
|
144
|
-
fail('ambiguous receiver heads');
|
|
145
|
-
const storedHeads = placeActHeads(store, trust.initialHeads, trust, limits);
|
|
146
|
-
for (const p of decoded.places) {
|
|
147
|
-
const actual = storedHeads.find(h => h.place === p.place), selected = current.heads.find(h => h.place === p.place);
|
|
148
|
-
if (!actual || !selected || actual.identity !== selected.identity || actual.epoch !== selected.epoch)
|
|
149
|
-
fail('receiver snapshot is not committed head ' + p.place);
|
|
150
|
-
}
|
|
151
|
-
for (const p of decoded.places) {
|
|
152
|
-
const head = current.heads.find(h => h.place === p.place) ?? fail('missing receiver head ' + p.place);
|
|
153
|
-
if (head.identity !== p.previous)
|
|
154
|
-
fail('receiver head moved ' + p.place);
|
|
155
|
-
const next = clock(head.epoch) + 1;
|
|
156
|
-
if (!Number.isSafeInteger(next) || clock(p.epoch) !== next)
|
|
157
|
-
fail('receiver epoch not next ' + p.place);
|
|
158
|
-
}
|
|
159
|
-
publishBundle(at(store, id), files, storage);
|
|
160
|
-
return { context: encoded, receipts, originals, identity: id, kind: 'landed' };
|
|
161
|
-
}));
|
|
126
|
+
return transactPlaceAct(encoded, receipts, trust, limits, nativePlaceActPersistence(store, storage));
|
|
162
127
|
}
|
|
163
128
|
/** Enumerates only committed, historically authenticated carriers. Namespace
|
|
164
129
|
* projection is a caller's separate pure operation, never current permission. */
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface PublicationStorage {
|
|
2
|
+
publish(staged: string, committed: string): void;
|
|
3
|
+
}
|
|
4
|
+
export declare const nativePublicationStorage: PublicationStorage;
|
|
5
|
+
/** Publish prepared bytes as one directory. No identity, grammar or authority
|
|
6
|
+
* is inferred here. Readers address the committed name, never staging files.
|
|
7
|
+
* A failure after rename is not evidence of rollback: the caller must retain
|
|
8
|
+
* the uncertainty and allow a verified read of the committed name.
|
|
9
|
+
*/
|
|
10
|
+
export declare function publishBundle(committed: string, files: Readonly<Record<string, string>>, storage?: PublicationStorage): void;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type PlaceReceipt, type ClockBudget } from '@lapxo/topos/core';
|
|
2
|
+
import type { PlaceActBudget } from './ports/place-act.ts';
|
|
3
|
+
export interface RootClockSourceSelection {
|
|
4
|
+
readonly selector: string;
|
|
5
|
+
readonly profileSelector: string;
|
|
6
|
+
readonly bootstrapCheckpoint: {
|
|
7
|
+
scope: string;
|
|
8
|
+
measure: string;
|
|
9
|
+
};
|
|
10
|
+
readonly resources: {
|
|
11
|
+
bytes: number;
|
|
12
|
+
lines: number;
|
|
13
|
+
places: number;
|
|
14
|
+
acts: number;
|
|
15
|
+
};
|
|
16
|
+
readonly inherited: {
|
|
17
|
+
clock: ClockBudget;
|
|
18
|
+
storage: PlaceActBudget;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare function rootClockSource(root: string, selected: RootClockSourceSelection): {
|
|
22
|
+
observation: Readonly<{
|
|
23
|
+
history: readonly string[];
|
|
24
|
+
anchor: import("@lapxo/topos/wire").Signer;
|
|
25
|
+
algorithms: readonly string[];
|
|
26
|
+
}>;
|
|
27
|
+
resources: Readonly<{
|
|
28
|
+
bytes: number;
|
|
29
|
+
lines: number;
|
|
30
|
+
places: number;
|
|
31
|
+
acts: number;
|
|
32
|
+
}>;
|
|
33
|
+
authority: (encoded: string) => {
|
|
34
|
+
anchor: import("@lapxo/topos/wire").Signer;
|
|
35
|
+
history: readonly string[];
|
|
36
|
+
checkpoint: string;
|
|
37
|
+
};
|
|
38
|
+
selection: import("./place-clock-selection.ts").HostClockSelection;
|
|
39
|
+
trust: import("./ports/place-act.ts").PlaceActTrust;
|
|
40
|
+
};
|
|
41
|
+
/** Source mapping is an explicit Root declaration; absence never means a zero
|
|
42
|
+
* clock or an inferred cursor. Existing legacy callers do not request it. */
|
|
43
|
+
export declare function rootClockSelectionOf(live: readonly string[], anchor: import('@lapxo/topos/wire').Signer): RootClockSourceSelection;
|
|
44
|
+
export declare function declaredRootClockSource(root: string): {
|
|
45
|
+
observation: Readonly<{
|
|
46
|
+
history: readonly string[];
|
|
47
|
+
anchor: import("@lapxo/topos/wire").Signer;
|
|
48
|
+
algorithms: readonly string[];
|
|
49
|
+
}>;
|
|
50
|
+
resources: Readonly<{
|
|
51
|
+
bytes: number;
|
|
52
|
+
lines: number;
|
|
53
|
+
places: number;
|
|
54
|
+
acts: number;
|
|
55
|
+
}>;
|
|
56
|
+
authority: (encoded: string) => {
|
|
57
|
+
anchor: import("@lapxo/topos/wire").Signer;
|
|
58
|
+
history: readonly string[];
|
|
59
|
+
checkpoint: string;
|
|
60
|
+
};
|
|
61
|
+
selection: import("./place-clock-selection.ts").HostClockSelection;
|
|
62
|
+
trust: import("./ports/place-act.ts").PlaceActTrust;
|
|
63
|
+
};
|
|
64
|
+
export declare function hasRootClockSource(root: string): boolean;
|
|
65
|
+
export interface PortablePlaceContext {
|
|
66
|
+
readonly identity: string;
|
|
67
|
+
readonly context: string;
|
|
68
|
+
readonly receipts: readonly PlaceReceipt[];
|
|
69
|
+
readonly authority: {
|
|
70
|
+
anchor: import('@lapxo/topos/wire').Signer;
|
|
71
|
+
history: readonly string[];
|
|
72
|
+
checkpoint: string;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Historical inspection ONLY. The expected acting Root anchor and resource
|
|
76
|
+
* selection come from the consumer's selected place, never from this packet.
|
|
77
|
+
* This function returns data and cannot construct a current command permit. */
|
|
78
|
+
export declare function inspectPortablePlaceContexts(encoded: string, expectedAnchor: import('@lapxo/topos/wire').Signer, selected: RootClockSourceSelection): {
|
|
79
|
+
identity: string;
|
|
80
|
+
context: import("@lapxo/topos/core").PlaceAct;
|
|
81
|
+
}[];
|
|
82
|
+
/** Prospective configuration is checked as authenticated Root history, never
|
|
83
|
+
* against a prose birth condition. Initial heads are explicit typed selections;
|
|
84
|
+
* stored committed chains cannot disappear when a selection is activated or
|
|
85
|
+
* replaced. Preview reads only; the real admission repeats under its Root lock. */
|
|
86
|
+
export declare function assertProspectiveRootClockSelection(root: string, signed: readonly string[]): void;
|
|
@@ -6,7 +6,7 @@ import { committedLocalActs, actLines } from "./ports/local-act.js";
|
|
|
6
6
|
import { verifiedLegacyActResult } from "../cli/act-result.js";
|
|
7
7
|
import { admissionAnchor, admissionHistoryAt } from "../fold/admission-history.js";
|
|
8
8
|
import { authorityFor } from "../fold/signers.js";
|
|
9
|
-
import { eraOf } from "
|
|
9
|
+
import { signatureEraOf as eraOf } from "./signature-era.js";
|
|
10
10
|
import { fieldOf, foldClaims, isConfig, selfName } from "../fold/claims.js";
|
|
11
11
|
import { authenticatedRootStanding } from "./admitted-place.js";
|
|
12
12
|
import { verifiesOwnerLine } from "../land/sign.js";
|
|
@@ -158,11 +158,11 @@ export function rootClockSource(root, selected) {
|
|
|
158
158
|
}
|
|
159
159
|
/** Source mapping is an explicit Root declaration; absence never means a zero
|
|
160
160
|
* clock or an inferred cursor. Existing legacy callers do not request it. */
|
|
161
|
-
export function
|
|
162
|
-
const
|
|
161
|
+
export function rootClockSelectionOf(live, anchor) {
|
|
162
|
+
const rows = live.filter(l => fieldOf(l, 'scope') === 'wire/place-act-context-source' && fieldOf(l, 'measure') === 'text' && fieldOf(l, 'value') !== 'withdraw');
|
|
163
163
|
if (rows.length !== 1)
|
|
164
164
|
fail('requires one admitted source mapping');
|
|
165
|
-
const line = rows[0],
|
|
165
|
+
const line = rows[0], algorithms = eraOf(live).admitted;
|
|
166
166
|
if (fieldOf(line, 'by') !== anchor.id || fieldOf(line, 'role') !== 'writes' || fieldOf(line, 'form') !== 'alphabet' || fieldOf(line, 'value') !== 'lock' || !verifiesOwnerLine(line, anchor.publicKey, algorithms))
|
|
167
167
|
fail('mapping is not Root signed');
|
|
168
168
|
const about = fieldOf(line, 'about');
|
|
@@ -175,8 +175,10 @@ export function declaredRootClockSource(root) {
|
|
|
175
175
|
}
|
|
176
176
|
if (!value || Object.keys(value).sort().join(',') !== 'bootstrapCheckpoint,inherited,profile,profileSelector,resources,selector' || value.profile !== 'place-act-context@1' || JSON.stringify(value) !== about)
|
|
177
177
|
fail('unknown/noncanonical mapping');
|
|
178
|
-
|
|
178
|
+
sourceSelection(value);
|
|
179
|
+
return value;
|
|
179
180
|
}
|
|
181
|
+
export function declaredRootClockSource(root) { return rootClockSource(root, rootClockSelectionOf(authenticatedRootStanding(root), admissionAnchor(root))); }
|
|
180
182
|
export function hasRootClockSource(root) { return authenticatedRootStanding(root).some(l => fieldOf(l, 'scope') === 'wire/place-act-context-source' && fieldOf(l, 'measure') === 'text' && fieldOf(l, 'value') !== 'withdraw'); }
|
|
181
183
|
/** Historical inspection ONLY. The expected acting Root anchor and resource
|
|
182
184
|
* selection come from the consumer's selected place, never from this packet.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { wireLine } from "../fold/wire.js";
|
|
2
|
+
import { fieldOf } from "../fold/claims.js";
|
|
3
|
+
/** Pure signature vocabulary; no instrument discovery for diagnostics. */
|
|
4
|
+
export function signatureEraOf(standing) {
|
|
5
|
+
const said = wireLine(standing, 'signature-algorithms');
|
|
6
|
+
const admitted = (said === undefined ? [] : fieldOf(said, 'value').split('|')).filter(Boolean);
|
|
7
|
+
const current = wireLine(standing, 'era');
|
|
8
|
+
const era = (current === undefined ? undefined : admitted.find((one) => one.endsWith(`:${fieldOf(current, 'value')}`))) ?? admitted.find((one) => one.includes(':'));
|
|
9
|
+
if (era === undefined)
|
|
10
|
+
throw new Error(`REFUSE·runtime the lock names no signature algorithm by digest: nothing can be signed here`);
|
|
11
|
+
return { admitted, era };
|
|
12
|
+
}
|
package/dist/host/signing.js
CHANGED
|
@@ -40,7 +40,7 @@ export function signerFor(lines, keyId, selected, keyFile, injected) {
|
|
|
40
40
|
if (injected) {
|
|
41
41
|
if (injected.name !== declaration.name || keyFile)
|
|
42
42
|
throw new SigningFailure('REFUSE·signer injected host binding conflicts with declaration or key file');
|
|
43
|
-
return (lot) => invokeSigningPort(lot, injected.sign, { timeoutMs: declaration.timeoutMs, maxBytes: declaration.responseBytes });
|
|
43
|
+
return (lot) => invokeSigningPort(lot, injected.sign, { timeoutMs: declaration.timeoutMs, maxBytes: declaration.responseBytes }, injected.signal);
|
|
44
44
|
}
|
|
45
45
|
const mechanism = binding(declaration.name, keyFile);
|
|
46
46
|
const protocol = { request: signerRequestBytes, response: signerResponseBytes, reply: signerResponseOf };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hostOutput } from "./call-context.js";
|
|
1
2
|
import { transportBytes } from "./ports/transport.js";
|
|
2
3
|
import { contentStore, verifiedContent } from "./content-store.js";
|
|
3
4
|
import { ContentNeeded } from "./content-needed.js";
|
|
@@ -91,7 +92,7 @@ export async function resolveSources(standing, requested) {
|
|
|
91
92
|
fetch: async (url, digest) => transportBytes(url, digest, [...ownLock(), ...standing]),
|
|
92
93
|
}, requested);
|
|
93
94
|
for (const digest of received)
|
|
94
|
-
|
|
95
|
+
hostOutput('out', `FETCHED ${digest} · verified\n`);
|
|
95
96
|
}
|
|
96
97
|
/** A changed verified store is the only reason to retry. A repeated miss never loops or changes a named pin. */
|
|
97
98
|
export async function resolveRequested(resolve, fold) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PLACE_ACT_CONTEXT } from '@lapxo/topos/core';
|
|
2
|
+
import { foldClaims, fieldOf } from "../fold/claims.js";
|
|
3
|
+
/** Projection only, after the caller authenticates the complete carrier. The
|
|
4
|
+
* CLI and in-process reader use this same fold and output shape. */
|
|
5
|
+
export function projectContextualStanding(root, legacy, contexts, decoded) {
|
|
6
|
+
const groups = new Map();
|
|
7
|
+
for (const act of contexts) {
|
|
8
|
+
const context = decoded.find(one => one.identity === act.identity).context;
|
|
9
|
+
for (const p of context.places) {
|
|
10
|
+
const key = JSON.stringify([context.root.namespace, p.place]), group = groups.get(key) ?? { namespace: context.root.namespace, place: p.place, lines: [], contexts: [] };
|
|
11
|
+
group.lines.push(...p.records);
|
|
12
|
+
group.contexts.push({ identity: act.identity, context: act.context, receipts: act.receipts });
|
|
13
|
+
groups.set(key, group);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const legacyPlaces = legacy.map(place => ({ namespace: 'legacy-global', place: place.place, standing: foldClaims(place.history).standing.filter(line => fieldOf(line, 'value') !== 'withdraw'), history: place.history }));
|
|
17
|
+
return { profile: PLACE_ACT_CONTEXT, root, legacyPlaces, contexts, places: [...groups.values()].map(group => { const folded = foldClaims(group.lines); return { namespace: group.namespace, place: group.place, standing: folded.standing.filter(line => fieldOf(line, 'value') !== 'withdraw'), retracted: folded.retracted, superseded: folded.superseded, forks: folded.forks, contexts: group.contexts }; }) };
|
|
18
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { viewsOf } from "../fold/views.js";
|
|
2
|
+
import { BoundRefusal } from "./refusals.js";
|
|
3
|
+
import { authorityFor } from "../fold/signers.js";
|
|
4
|
+
import { signatureEraOf as eraOf } from "./signature-era.js";
|
|
5
|
+
import { fieldOf, foldClaims, isConfig } from "../fold/claims.js";
|
|
6
|
+
import { foldedEvidence } from '@lapxo/topos/contract';
|
|
7
|
+
/** Project the supplied signed history against the host's original trust anchor.
|
|
8
|
+
* No current-root selection, storage access, effects or retained authority.
|
|
9
|
+
* The file host and the in-process host use this same admission-snapshot fold. */
|
|
10
|
+
export function projectAuthenticatedStanding(history, anchor) {
|
|
11
|
+
const snapshots = new Map();
|
|
12
|
+
const admitted = history.filter(line => {
|
|
13
|
+
const epoch = Number(fieldOf(line, 'epoch')) || 0;
|
|
14
|
+
let authority = snapshots.get(epoch);
|
|
15
|
+
if (!authority) {
|
|
16
|
+
// This invocation has freshly read the complete signed inventory. Derive
|
|
17
|
+
// the same historical cutoff without reopening it for every epoch; the
|
|
18
|
+
// next invocation still rereads every authority input.
|
|
19
|
+
const snapshot = history.filter(record => !isConfig(record) || Number(fieldOf(record, 'epoch')) <= epoch);
|
|
20
|
+
// The signature vocabulary is configuration, not the as-yet
|
|
21
|
+
// unauthenticated evidence whose values are about to be folded.
|
|
22
|
+
const wire = snapshot.filter(record => {
|
|
23
|
+
const scope = fieldOf(record, 'scope');
|
|
24
|
+
return scope.startsWith('wire/') || scope.startsWith('audit/wire/');
|
|
25
|
+
});
|
|
26
|
+
authority = authorityFor(snapshot, anchor, eraOf(foldClaims(wire).standing).admitted);
|
|
27
|
+
snapshots.set(epoch, authority);
|
|
28
|
+
}
|
|
29
|
+
return authority.of(line).kind === 'admitted';
|
|
30
|
+
});
|
|
31
|
+
// The host authenticates against historical authority; Topos projects the
|
|
32
|
+
// authenticated history using the instrument's existing fold.
|
|
33
|
+
const { standing } = foldedEvidence(admitted, {
|
|
34
|
+
classification: () => 'local',
|
|
35
|
+
fold: lines => foldClaims(lines).standing,
|
|
36
|
+
});
|
|
37
|
+
return [...standing];
|
|
38
|
+
}
|
|
39
|
+
/** One check shared by the CLI and the read-only native host. */
|
|
40
|
+
export function assertStandingView(lines) {
|
|
41
|
+
const view = viewsOf(lines).get('standing');
|
|
42
|
+
if (!view || view.shape || !view.regions.some(region => region.name === 'standing'))
|
|
43
|
+
throw new BoundRefusal('standing/view', 'REFUSE·view standing requires a declared standing view');
|
|
44
|
+
}
|
|
45
|
+
export function publicStandingProjection(history, anchor) {
|
|
46
|
+
return publishedStandingLines(projectAuthenticatedStanding(history, anchor));
|
|
47
|
+
}
|
|
48
|
+
export function publishedStandingLines(standing) {
|
|
49
|
+
return standing.filter(line => !isConfig(line) || fieldOf(line, 'value') !== 'withdraw');
|
|
50
|
+
}
|
package/dist/land/act.js
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { basename, dirname, existsSync, join, mkdirSync, rmSync, writeFileSync } from "../host/io.js";
|
|
3
|
+
import { basename, dirname, existsSync, join, mkdirSync, rmSync, writeFileSync, realpathSync, resolve } from "../host/io.js";
|
|
4
|
+
import { BoundRefusal } from "../host/refusals.js";
|
|
4
5
|
import { selfName } from "../fold/claims.js";
|
|
5
6
|
import { placesIn } from "../fold/places.js";
|
|
6
7
|
import { coordinatesUnder, observeText } from "../observe/files.js";
|
|
8
|
+
const hostContext = new AsyncLocalStorage();
|
|
9
|
+
export const withActHost = (host, run) => hostContext.run(host, run);
|
|
10
|
+
const actName = () => hostContext.getStore()?.name ?? selfName();
|
|
11
|
+
const report = (text) => { const host = hostContext.getStore(); if (host)
|
|
12
|
+
host.report(text);
|
|
13
|
+
else
|
|
14
|
+
process.stderr.write(text); };
|
|
15
|
+
const checkCancellation = () => { if (hostContext.getStore()?.signal?.aborted)
|
|
16
|
+
throw new BoundRefusal('host/cancelled', 'REFUSE·host/cancelled'); };
|
|
17
|
+
/** Resolve aliases before acquiring the same native lock protocol as the CLI. */
|
|
18
|
+
export function canonicalActStore(store) {
|
|
19
|
+
let at = resolve(store);
|
|
20
|
+
const tail = [];
|
|
21
|
+
while (!existsSync(at)) {
|
|
22
|
+
const parent = dirname(at);
|
|
23
|
+
if (parent === at)
|
|
24
|
+
break;
|
|
25
|
+
tail.unshift(basename(at));
|
|
26
|
+
at = parent;
|
|
27
|
+
}
|
|
28
|
+
return join(realpathSync(at), ...tail);
|
|
29
|
+
}
|
|
7
30
|
const context = new AsyncLocalStorage();
|
|
8
31
|
const held = new Map();
|
|
9
32
|
const pauseFor = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
@@ -20,8 +43,8 @@ function overAMinute(at, began, holder) {
|
|
|
20
43
|
if (Date.now() - began < MINUTE)
|
|
21
44
|
return;
|
|
22
45
|
const who = holder === undefined ? 'a process' : `${holder.pid} · ${holder.what} since ${holder.since}`;
|
|
23
|
-
|
|
24
|
-
throw new Error(`${
|
|
46
|
+
report(`RED wait · ${named(at)} is held by ${who} · a wait over a minute is a red, not a pause\n`);
|
|
47
|
+
throw new Error(`${actName()}: REFUSE·stale nothing landed · a wait over a minute is a red, not a pause`);
|
|
25
48
|
}
|
|
26
49
|
const alive = (pid) => {
|
|
27
50
|
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
@@ -37,7 +60,7 @@ const alive = (pid) => {
|
|
|
37
60
|
const regionAt = (store, region) => {
|
|
38
61
|
const coordinate = region.replace(/\/+$/, '');
|
|
39
62
|
if ((coordinate && coordinate.split('/').some((step) => step === '..' || step === '.' || step === '')) || coordinate.startsWith('/'))
|
|
40
|
-
throw new Error(`${
|
|
63
|
+
throw new Error(`${actName()}: REFUSE·lock invalid region coordinate`);
|
|
41
64
|
return join(store, 'locks', coordinate ? encodeURIComponent(coordinate).replace(/^\./, '%2E').replace(/^_root$/, '%5Froot') : ROOT);
|
|
42
65
|
};
|
|
43
66
|
const coordinateOf = (file) => {
|
|
@@ -77,12 +100,13 @@ async function hold(at, what) {
|
|
|
77
100
|
return;
|
|
78
101
|
const frame = context.getStore();
|
|
79
102
|
if (frame === undefined)
|
|
80
|
-
throw new Error(`${
|
|
103
|
+
throw new Error(`${actName()}: REFUSE·lock no act owns this request`);
|
|
81
104
|
const began = Date.now();
|
|
82
105
|
const token = randomUUID();
|
|
83
106
|
let told = false;
|
|
84
107
|
mkdirSync(dirname(at), { recursive: true });
|
|
85
108
|
for (;;) {
|
|
109
|
+
checkCancellation();
|
|
86
110
|
try {
|
|
87
111
|
writeFileSync(at, `${process.pid} ${what.replace(/\s+/g, '_')} ${new Date().toISOString()} ${token}\n`, { flag: 'wx' });
|
|
88
112
|
break;
|
|
@@ -97,17 +121,17 @@ async function hold(at, what) {
|
|
|
97
121
|
const holder = holderOf(at);
|
|
98
122
|
// Removing a dead owner's lock here would race with another process's recovery. Recovery is a separate host act.
|
|
99
123
|
if (holder !== undefined && !alive(holder.pid)) {
|
|
100
|
-
throw new Error(`${
|
|
124
|
+
throw new Error(`${actName()}: REFUSE·stale ${named(at)} belongs to exited process ${holder.pid}; host recovery required`);
|
|
101
125
|
}
|
|
102
126
|
overAMinute(at, began, holder);
|
|
103
127
|
if (!told && holder !== undefined) {
|
|
104
|
-
|
|
128
|
+
report(`WAIT ${named(at)} is held by ${holder.pid} · ${holder.what} since ${holder.since}\n`);
|
|
105
129
|
told = true;
|
|
106
130
|
}
|
|
107
131
|
await pauseFor(250);
|
|
108
132
|
}
|
|
109
133
|
if (told)
|
|
110
|
-
|
|
134
|
+
report(`HELD ${named(at)} after ${Date.now() - began} ms waiting\n`);
|
|
111
135
|
held.set(at, token);
|
|
112
136
|
frame.locks.set(at, token);
|
|
113
137
|
if (!process.listeners('exit').includes(releaseAll))
|
|
@@ -125,16 +149,17 @@ function releaseAll() {
|
|
|
125
149
|
for (const [at, token] of [...held])
|
|
126
150
|
release(at, token);
|
|
127
151
|
}
|
|
128
|
-
process.
|
|
129
|
-
|
|
130
|
-
releaseAll();
|
|
131
|
-
|
|
132
|
-
});
|
|
152
|
+
/** Only the CLI installs process termination behavior. Importing a host does not. */
|
|
153
|
+
export function installActSignalHandler() {
|
|
154
|
+
process.once('SIGTERM', () => { report(`${actName()}: REFUSE·crash signal\n`); releaseAll(); process.exit(1); });
|
|
155
|
+
}
|
|
133
156
|
async function framed(takes, act) {
|
|
134
157
|
const frame = { parent: context.getStore(), locks: new Map() };
|
|
135
158
|
return context.run(frame, async () => {
|
|
136
159
|
try {
|
|
160
|
+
checkCancellation();
|
|
137
161
|
await takes();
|
|
162
|
+
checkCancellation();
|
|
138
163
|
return await act();
|
|
139
164
|
}
|
|
140
165
|
finally {
|
|
@@ -149,14 +174,14 @@ function guardedRegions(store, regions, what, act, rootAuthority) {
|
|
|
149
174
|
const tree = treeAt(store);
|
|
150
175
|
const asked = [...new Set((regions.length ? regions : ['']).map((one) => one.replace(/\/+$/, '')))];
|
|
151
176
|
if (rootAuthority && (asked.includes('') || (ownsRegion(store) && !owns(tree) && !ownsAuthority(store))))
|
|
152
|
-
throw new Error(`${
|
|
177
|
+
throw new Error(`${actName()}: REFUSE·lock authority dependencies and child regions must be declared together`);
|
|
153
178
|
if (owns(tree) || asked.every((region) => owns(regionAt(store, region)))) {
|
|
154
179
|
for (const region of asked)
|
|
155
180
|
await hold(regionAt(store, region), what);
|
|
156
181
|
return;
|
|
157
182
|
}
|
|
158
183
|
if (ownsRegion(store))
|
|
159
|
-
throw new Error(`${
|
|
184
|
+
throw new Error(`${actName()}: REFUSE·lock declare every region before entering an act`);
|
|
160
185
|
const already = owns(tree);
|
|
161
186
|
// The tree lock is the acquisition gate. A whole-tree act cannot enter between checking it and taking regions.
|
|
162
187
|
await hold(tree, what);
|
|
@@ -172,17 +197,18 @@ function guardedRegions(store, regions, what, act, rootAuthority) {
|
|
|
172
197
|
const began = Date.now();
|
|
173
198
|
let told = false;
|
|
174
199
|
for (let holder = holderOf(at); holder !== undefined; holder = holderOf(at)) {
|
|
200
|
+
checkCancellation();
|
|
175
201
|
if (!alive(holder.pid))
|
|
176
|
-
throw new Error(`${
|
|
202
|
+
throw new Error(`${actName()}: REFUSE·stale ${named(at)} belongs to exited process ${holder.pid}; host recovery required`);
|
|
177
203
|
overAMinute(at, began, holder);
|
|
178
204
|
if (!told) {
|
|
179
|
-
|
|
205
|
+
report(`WAIT ${named(at)} is held by ${holder.pid} · ${holder.what} since ${holder.since}\n`);
|
|
180
206
|
told = true;
|
|
181
207
|
}
|
|
182
208
|
await pauseFor(250);
|
|
183
209
|
}
|
|
184
210
|
if (told)
|
|
185
|
-
|
|
211
|
+
report(`HELD ${named(at)} after ${Date.now() - began} ms waiting\n`);
|
|
186
212
|
}
|
|
187
213
|
// Registered under the same acquisition gate as every legacy writer.
|
|
188
214
|
// Root writes overlap all locks; siblings keep distinct region locks.
|
|
@@ -207,12 +233,14 @@ function guardedRegions(store, regions, what, act, rootAuthority) {
|
|
|
207
233
|
}, act);
|
|
208
234
|
}
|
|
209
235
|
export function underTheRegions(store, regions, what, act) {
|
|
236
|
+
store = canonicalActStore(store);
|
|
210
237
|
return guardedRegions(store, regions, what, act, false);
|
|
211
238
|
}
|
|
212
239
|
/** Protect the root authority/policy read through child publication. This is
|
|
213
240
|
* exclusion only: callers must authenticate contexts and compare prepared heads
|
|
214
241
|
* while held. It creates no grant, clock advance, lease or automatic recovery. */
|
|
215
242
|
export function underAuthorityRegions(store, regions, what, act) {
|
|
243
|
+
store = canonicalActStore(store);
|
|
216
244
|
return guardedRegions(store, regions, what, act, true);
|
|
217
245
|
}
|
|
218
246
|
/** Protect an authority snapshot without taking a child region. The acquisition
|
|
@@ -220,12 +248,13 @@ export function underAuthorityRegions(store, regions, what, act) {
|
|
|
220
248
|
* shared read; existing child acts continue independently. This is exclusion,
|
|
221
249
|
* never authorization or a clock advance. */
|
|
222
250
|
export function underRootAuthority(store, what, act) {
|
|
251
|
+
store = canonicalActStore(store);
|
|
223
252
|
return framed(async () => {
|
|
224
253
|
const tree = treeAt(store), root = regionAt(store, '');
|
|
225
254
|
if (owns(tree) || owns(root) || ownsAuthority(store))
|
|
226
255
|
return;
|
|
227
256
|
if (ownsRegion(store))
|
|
228
|
-
throw new Error(`${
|
|
257
|
+
throw new Error(`${actName()}: REFUSE·lock authority dependencies must be declared before entering a region act`);
|
|
229
258
|
await hold(tree, what);
|
|
230
259
|
try {
|
|
231
260
|
await hold(root, what);
|
|
@@ -246,49 +275,54 @@ export function underRootAuthority(store, what, act) {
|
|
|
246
275
|
/** The fixed host may hand its child evidence of an actual held exclusion.
|
|
247
276
|
* It is an operational envelope, not a grant, signer or receipt identity. */
|
|
248
277
|
export function rootAuthorityProtection(store) {
|
|
278
|
+
store = canonicalActStore(store);
|
|
249
279
|
const locks = [treeAt(store), regionAt(store, ''), ...held.keys()].filter(at => at === treeAt(store) || at === regionAt(store, '') || dirname(at) === join(store, 'locks') && basename(at).startsWith('@authority-'));
|
|
250
280
|
const lock = locks.find(owns);
|
|
251
281
|
if (lock === undefined)
|
|
252
|
-
throw Error(`${
|
|
282
|
+
throw Error(`${actName()}: REFUSE·lock root authority is not protected`);
|
|
253
283
|
const holder = holderOf(lock);
|
|
254
284
|
if (holder?.pid !== process.pid || holder.token !== held.get(lock))
|
|
255
|
-
throw Error(`${
|
|
285
|
+
throw Error(`${actName()}: REFUSE·lock authority protection changed`);
|
|
256
286
|
return Object.freeze({ store, lock, pid: holder.pid, token: holder.token });
|
|
257
287
|
}
|
|
258
288
|
/** expectedStore and expectedParent come from the fixed host's inheritance
|
|
259
289
|
* channel, never from world data. Matching a live lock proves exclusion only;
|
|
260
290
|
* signature, coverage and current context still require verification. */
|
|
261
291
|
export function assertRootAuthorityProtection(proof, expectedStore, expectedParent) {
|
|
292
|
+
expectedStore = canonicalActStore(expectedStore);
|
|
262
293
|
if (!proof || proof.store !== expectedStore || !Number.isSafeInteger(expectedParent) || expectedParent <= 0 || proof.pid !== expectedParent
|
|
263
294
|
|| typeof proof.lock !== 'string' || typeof proof.token !== 'string' || !proof.token
|
|
264
295
|
|| !(proof.lock === treeAt(expectedStore) || proof.lock === regionAt(expectedStore, '')
|
|
265
296
|
|| dirname(proof.lock) === join(expectedStore, 'locks') && basename(proof.lock).startsWith('@authority-'))) {
|
|
266
|
-
throw Error(`${
|
|
297
|
+
throw Error(`${actName()}: REFUSE·lock invalid inherited authority protection`);
|
|
267
298
|
}
|
|
268
299
|
const holder = holderOf(proof.lock);
|
|
269
300
|
if (!alive(expectedParent) || holder?.pid !== proof.pid || holder.token !== proof.token)
|
|
270
|
-
throw Error(`${
|
|
301
|
+
throw Error(`${actName()}: REFUSE·lock inherited authority protection ended`);
|
|
271
302
|
}
|
|
272
303
|
/** A child can publish independently, but the joined root projection and its
|
|
273
304
|
* derived ledger appends have one writer. Authority protection already excludes
|
|
274
305
|
* root mutations, so this lock needs no acquisition-gate upgrade. */
|
|
275
306
|
export function underRootProjection(store, what, act) {
|
|
307
|
+
store = canonicalActStore(store);
|
|
276
308
|
return framed(async () => {
|
|
277
309
|
if (owns(treeAt(store)) || owns(regionAt(store, '')))
|
|
278
310
|
return;
|
|
279
311
|
if (!ownsAuthority(store))
|
|
280
|
-
throw new Error(`${
|
|
312
|
+
throw new Error(`${actName()}: REFUSE·lock root projection requires authority protection`);
|
|
281
313
|
await hold(projectionAt(store), what);
|
|
282
314
|
}, act);
|
|
283
315
|
}
|
|
284
316
|
export const holdRoot = async (store, what) => {
|
|
317
|
+
store = canonicalActStore(store);
|
|
285
318
|
if (!owns(treeAt(store)))
|
|
286
319
|
await hold(regionAt(store, ''), what);
|
|
287
320
|
};
|
|
288
321
|
export function underTheLock(store, what, act) {
|
|
322
|
+
store = canonicalActStore(store);
|
|
289
323
|
return framed(async () => {
|
|
290
324
|
if (!owns(treeAt(store)) && ownsRegion(store))
|
|
291
|
-
throw new Error(`${
|
|
325
|
+
throw new Error(`${actName()}: REFUSE·lock a region act cannot upgrade to the tree; declare the tree before entering`);
|
|
292
326
|
await hold(treeAt(store), what);
|
|
293
327
|
const locks = join(store, 'locks');
|
|
294
328
|
const active = lockFiles(locks);
|
|
@@ -299,4 +333,4 @@ export function underTheLock(store, what, act) {
|
|
|
299
333
|
await hold(regionAt(store, ''), what);
|
|
300
334
|
}, act);
|
|
301
335
|
}
|
|
302
|
-
export const holds = (store, region) => [treeAt(store), regionAt(store, region)].some(owns);
|
|
336
|
+
export const holds = (store, region) => { store = canonicalActStore(store); return [treeAt(store), regionAt(store, region)].some(owns); };
|