@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.
Files changed (67) hide show
  1. package/.github/workflows/release.yml +67 -0
  2. package/.github/workflows/test.yml +35 -0
  3. package/CITATION.cff +19 -0
  4. package/TARGET.bound +71 -20
  5. package/dist/cli/accept.js +103 -71
  6. package/dist/cli/place.js +3 -2
  7. package/dist/cli/standing-result.js +3 -5
  8. package/dist/cli/takes.js +4 -3
  9. package/dist/cli/verb.js +2 -1
  10. package/dist/engine/ports.d.ts +6 -1
  11. package/dist/engine/ports.js +23 -9
  12. package/dist/fold/claims.d.ts +29 -0
  13. package/dist/fold/claims.js +2 -1
  14. package/dist/fold/closed.js +2 -1
  15. package/dist/fold/digests.js +7 -7
  16. package/dist/host/admitted-place.js +3 -31
  17. package/dist/host/call-context.d.ts +20 -0
  18. package/dist/host/call-context.js +14 -0
  19. package/dist/host/capsule-process.js +2 -1
  20. package/dist/host/capsule.js +7 -4
  21. package/dist/host/commit-result.d.ts +6 -0
  22. package/dist/host/commit-result.js +5 -0
  23. package/dist/host/contextual-place.js +9 -14
  24. package/dist/host/host-controls.js +19 -0
  25. package/dist/host/in-process-act.d.ts +82 -0
  26. package/dist/host/in-process-act.js +84 -0
  27. package/dist/host/in-process-context.d.ts +46 -0
  28. package/dist/host/in-process-context.js +91 -0
  29. package/dist/host/in-process-root.d.ts +117 -0
  30. package/dist/host/in-process-root.js +84 -0
  31. package/dist/host/in-process.d.ts +41 -0
  32. package/dist/host/in-process.js +55 -0
  33. package/dist/host/index.d.ts +15 -0
  34. package/dist/host/index.js +7 -0
  35. package/dist/host/native-receiver.d.ts +19 -0
  36. package/dist/host/native-receiver.js +25 -0
  37. package/dist/host/place-act-transaction.d.ts +24 -0
  38. package/dist/host/place-act-transaction.js +78 -0
  39. package/dist/host/place-clock-selection.d.ts +55 -0
  40. package/dist/host/place-clock-selection.js +1 -1
  41. package/dist/host/ports/local-act.d.ts +44 -0
  42. package/dist/host/ports/local-act.js +13 -2
  43. package/dist/host/ports/place-act.d.ts +49 -0
  44. package/dist/host/ports/place-act.js +19 -54
  45. package/dist/host/ports/publication.d.ts +10 -0
  46. package/dist/host/root-clock-source.d.ts +86 -0
  47. package/dist/host/root-clock-source.js +7 -5
  48. package/dist/host/signature-era.js +12 -0
  49. package/dist/host/signing.js +1 -1
  50. package/dist/host/source-content.js +2 -1
  51. package/dist/host/standing-carrier.js +18 -0
  52. package/dist/host/standing-projection.js +50 -0
  53. package/dist/land/act.js +60 -26
  54. package/dist/observe/run.js +2 -1
  55. package/dist/observe/runner.js +16 -8
  56. package/docs/contracts/native-host.md +21 -0
  57. package/docs/contracts/native-host.publication.json +25 -0
  58. package/docs/contracts/native-publication.inventory.json +2 -1
  59. package/docs/contracts/publication.inventory.json +29 -0
  60. package/docs/guide.md +10 -0
  61. package/docs/reference.md +39 -0
  62. package/docs/why.md +109 -0
  63. package/package.json +32 -2
  64. package/receipts.bound +10369 -9723
  65. package/samples/native-host/no.json +10 -0
  66. package/samples/native-host/yes.json +20 -0
  67. 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, verifyPlaceActContext, placeActReceiptsOf } from '@lapxo/topos/core';
4
- import { underAuthorityRegions, underTheRegions, underRootProjection } from "../../land/act.js";
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 => { clock(h.epoch); if (!h.identity)
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 = clock(prior.epoch) + 1;
104
- if (!Number.isSafeInteger(epoch) || clock(p.epoch) !== 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
- budget(limits);
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 "../fold/digests.js";
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 declaredRootClockSource(root) {
162
- const live = authenticatedRootStanding(root), rows = live.filter(l => fieldOf(l, 'scope') === 'wire/place-act-context-source' && fieldOf(l, 'measure') === 'text' && fieldOf(l, 'value') !== 'withdraw');
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], anchor = admissionAnchor(root), algorithms = eraOf(live).admitted;
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
- return rootClockSource(root, value);
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
+ }
@@ -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
- process.stdout.write(`FETCHED ${digest} · verified\n`);
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
- process.stderr.write(`RED wait · ${named(at)} is held by ${who} · a wait over a minute is a red, not a pause\n`);
24
- throw new Error(`${selfName()}: REFUSE·stale nothing landed · a wait over a minute is a red, not a pause`);
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(`${selfName()}: REFUSE·lock invalid region coordinate`);
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(`${selfName()}: REFUSE·lock no act owns this request`);
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(`${selfName()}: REFUSE·stale ${named(at)} belongs to exited process ${holder.pid}; host recovery required`);
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
- process.stderr.write(`WAIT ${named(at)} is held by ${holder.pid} · ${holder.what} since ${holder.since}\n`);
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
- process.stderr.write(`HELD ${named(at)} after ${Date.now() - began} ms waiting\n`);
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.once('SIGTERM', () => {
129
- process.stderr.write(`${selfName()}: REFUSE·crash signal\n`);
130
- releaseAll();
131
- process.exit(1);
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(`${selfName()}: REFUSE·lock authority dependencies and child regions must be declared together`);
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(`${selfName()}: REFUSE·lock declare every region before entering an act`);
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(`${selfName()}: REFUSE·stale ${named(at)} belongs to exited process ${holder.pid}; host recovery required`);
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
- process.stderr.write(`WAIT ${named(at)} is held by ${holder.pid} · ${holder.what} since ${holder.since}\n`);
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
- process.stderr.write(`HELD ${named(at)} after ${Date.now() - began} ms waiting\n`);
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(`${selfName()}: REFUSE·lock authority dependencies must be declared before entering a region act`);
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(`${selfName()}: REFUSE·lock root authority is not protected`);
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(`${selfName()}: REFUSE·lock authority protection changed`);
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(`${selfName()}: REFUSE·lock invalid inherited authority protection`);
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(`${selfName()}: REFUSE·lock inherited authority protection ended`);
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(`${selfName()}: REFUSE·lock root projection requires authority protection`);
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(`${selfName()}: REFUSE·lock a region act cannot upgrade to the tree; declare the tree before entering`);
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); };