@lapxo/bound 0.1.3 → 0.1.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.
@@ -0,0 +1,40 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ // This is a host mechanism, not a wire reader or a provider. The parent supplies its already selected policy.
4
+ const request = JSON.parse(readFileSync(0, 'utf8'));
5
+ if (!Number.isSafeInteger(request.limits.timeoutMs) || request.limits.timeoutMs <= 0 || !Number.isSafeInteger(request.limits.responseBytes) || request.limits.responseBytes <= 0)
6
+ throw Error('REFUSE·reader invalid process limits');
7
+ const grouped = process.platform !== 'win32';
8
+ const child = spawn(request.command, [...request.args], { cwd: request.cwd, env: request.env, detached: grouped, stdio: ['pipe', 'pipe', 'pipe'] });
9
+ const stdout = [], stderr = [];
10
+ let size = 0, failure, finished = false;
11
+ const cancel = () => { if (child.pid === undefined)
12
+ return; try {
13
+ if (grouped)
14
+ process.kill(-child.pid, 'SIGKILL');
15
+ else
16
+ child.kill('SIGKILL');
17
+ }
18
+ catch { } };
19
+ const refuse = (why) => { failure ?? (failure = why); cancel(); };
20
+ const receive = (chunks, bytes) => { size += bytes.length; if (size > request.limits.responseBytes) {
21
+ refuse('response exceeds declared byte bound');
22
+ return;
23
+ } chunks.push(bytes); };
24
+ child.stdout.on('data', (bytes) => receive(stdout, bytes));
25
+ child.stderr.on('data', (bytes) => receive(stderr, bytes));
26
+ child.stdin.on('error', error => { if (!finished)
27
+ refuse(`input failed: ${error.message}`); });
28
+ const timer = setTimeout(() => refuse('declared timeout exhausted'), request.limits.timeoutMs);
29
+ const finish = (status, signal) => {
30
+ if (finished)
31
+ return;
32
+ finished = true;
33
+ clearTimeout(timer);
34
+ cancel();
35
+ const result = { stdout: failure ? '' : Buffer.concat(stdout).toString('utf8'), stdoutBytes: failure ? '' : Buffer.concat(stdout).toString('base64'), stderr: failure ? '' : Buffer.concat(stderr).toString('utf8'), status, signal, ...(failure ? { failure } : {}) };
36
+ process.stdout.write(JSON.stringify(result));
37
+ };
38
+ child.on('error', error => { failure = error.message; finish(null, null); });
39
+ child.on('close', finish);
40
+ child.stdin.end(request.input);
@@ -10,7 +10,7 @@ export async function admittedObjects(root, lines, emit = () => { }) {
10
10
  if (history.kind !== 'fact')
11
11
  throw Error(`REFUSE·wire ${history.why}`);
12
12
  const authority = authorityFor(lines, rootSigner(root), signaturesOf(storeOf(root)).admitted);
13
- const context = await objectProvider(history.value.objects, history.value.configuration, { admit: fields => authority.of(canonical(fields)).kind === 'admitted', emit, topos: toposAt, module: contextModuleAt });
13
+ const context = await objectProvider(history.value.objects, history.value.configuration, { admit: fields => authority.of(canonical(fields)).kind === 'admitted', emit, topos: digest => toposAt(digest, storeOf(root)), module: digest => contextModuleAt(digest, storeOf(root)) });
14
14
  const checked = validateObjectContext(history.value.objects, context);
15
15
  if (checked.kind !== 'fact')
16
16
  throw Error(`REFUSE·context ${checked.why}`);
@@ -0,0 +1 @@
1
+ export declare function transportBytes(location: string, digest: string, lines: readonly string[]): Promise<Uint8Array>;
@@ -0,0 +1,48 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { contentRequest, transportSelection, parse } from '@lapxo/topos/wire';
3
+ import { boundedProcess } from "../process-lifetime.js";
4
+ import { fetchBytes } from "../adapters/transport/http.js";
5
+ export async function transportBytes(location, digest, lines) {
6
+ const scheme = new URL(location).protocol.slice(0, -1);
7
+ const fields = lines.flatMap(line => { const p = parse(line); return p.kind === 'fact' ? [p.value.fields] : []; });
8
+ const selected = transportSelection(fields, scheme);
9
+ if (selected === undefined)
10
+ throw Error(`REFUSE·transport ${scheme} no admitted content-request/1 policy`);
11
+ if (selected.name === 'fetch') {
12
+ if (!['http', 'https'].includes(scheme))
13
+ throw Error(`REFUSE·transport ${scheme} is not supported by this host adapter`);
14
+ return fetchBytes(location, selected);
15
+ }
16
+ const registryPath = process.env['BOUND_TRANSPORTS'];
17
+ if (!registryPath)
18
+ throw Error(`REFUSE·transport ${scheme} no host binding for ${selected.name}`);
19
+ let record;
20
+ try {
21
+ const registry = JSON.parse(readFileSync(registryPath, 'utf8'));
22
+ if (!registry || typeof registry !== 'object' || !Object.prototype.hasOwnProperty.call(registry, selected.name))
23
+ throw Error();
24
+ const value = registry[selected.name];
25
+ if (!value || typeof value !== 'object')
26
+ throw Error();
27
+ const f = value;
28
+ if (f.kind !== 'command' || typeof f.executable !== 'string' || !f.executable || !Array.isArray(f.args) || !f.args.every(x => typeof x === 'string'))
29
+ throw Error();
30
+ if (f.environment !== undefined && (!Array.isArray(f.environment) || !f.environment.every(x => typeof x === 'string')))
31
+ throw Error();
32
+ const names = (f.environment ?? []);
33
+ const env = {};
34
+ if (process.env.PATH !== undefined)
35
+ env.PATH = process.env.PATH;
36
+ for (const name of names)
37
+ if (process.env[name] !== undefined)
38
+ env[name] = process.env[name];
39
+ record = { executable: f.executable, args: f.args, env };
40
+ }
41
+ catch {
42
+ throw Error(`REFUSE·transport ${scheme} host binding unavailable or invalid`);
43
+ }
44
+ const result = boundedProcess(record.executable, record.args, contentRequest(location, digest, selected), selected, undefined, record.env);
45
+ if (result.status !== 0 || result.signal !== null)
46
+ throw Error(`REFUSE·transport ${scheme} failed · status ${result.status ?? result.signal}`);
47
+ return Buffer.from(result.stdoutBytes ?? '', 'base64');
48
+ }
@@ -0,0 +1,11 @@
1
+ import type { ReaderLifetime } from '@lapxo/topos/wire';
2
+ export interface ProcessResult {
3
+ readonly stdout: string;
4
+ readonly stdoutBytes?: string;
5
+ readonly stderr: string;
6
+ readonly status: number | null;
7
+ readonly signal: NodeJS.Signals | null;
8
+ readonly failure?: string;
9
+ }
10
+ /** A synchronous fold waits on a supervising host process; the supervisor owns cancellation of its child group. */
11
+ export declare function boundedProcess(command: string, args: readonly string[], input: string, limits: ReaderLifetime, cwd?: string, env?: Readonly<Record<string, string>>): ProcessResult;
@@ -0,0 +1,12 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { processOf } from "../observe/runner.js";
3
+ /** A synchronous fold waits on a supervising host process; the supervisor owns cancellation of its child group. */
4
+ export function boundedProcess(command, args, input, limits, cwd, env) {
5
+ const run = spawnSync(process.execPath, [processOf('lifetime', 'host')], { input: JSON.stringify({ command, args, input, limits, cwd, env }), encoding: 'utf8', maxBuffer: limits.responseBytes * 12 + 4096 });
6
+ if (run.error || run.status !== 0)
7
+ throw Error(`REFUSE·reader lifetime supervisor ${run.error?.message ?? run.status}`);
8
+ const result = JSON.parse(run.stdout);
9
+ if (result.failure)
10
+ throw Error(`REFUSE·reader ${result.failure} · status ${result.status ?? result.signal ?? 'cancelled'}`);
11
+ return result;
12
+ }
@@ -1,5 +1,7 @@
1
1
  import { capsuleAt } from "./capsule.js";
2
- import { capsuleOffers } from '@lapxo/topos/standing';
2
+ import { capsuleOffers, providerInputs, providerInputRegions, capsuleDescriptor, assertCapsuleDescriptor } from '@lapxo/topos/standing';
3
+ import { fullDigest } from "./digest.js";
4
+ import { wireAt, parse } from '@lapxo/topos/wire';
3
5
  import { selectedTopoi, toposAt } from "./selected-topos.js";
4
6
  import { canonical } from '@lapxo/topos/wire';
5
7
  import { fieldOf } from "../fold/claims.js";
@@ -8,16 +10,40 @@ import { archiveCoordinate } from "./archive.js";
8
10
  /** Only explicit capsule offers request execution. Forms/classes/views do not need an executable alias. */
9
11
  export function selectedCapsules(pins, resolve = capsuleAt, select = toposAt, host = ownLock()) {
10
12
  return selectedTopoi(pins, select).flatMap(({ digest, topos }) => capsuleOffers(topos).map(offer => {
11
- const projections = host.filter(line => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') === 'writes' && fieldOf(line, 'value') === offer.value);
12
- if (!projections.length || projections.some(line => !fieldOf(line, 'shape')))
13
- throw Error(`REFUSE·pin ${digest} capsule ${offer.value} needs one host projection`);
14
- const entries = [...new Set(projections.map(line => archiveCoordinate(fieldOf(line, 'shape'))))];
15
- if (entries.length !== 1)
16
- throw Error(`REFUSE·pin ${digest} capsule ${offer.value} has conflicting host projections`);
17
- const projected = { ...offer, shape: entries[0] };
18
- const capsule = resolve(offer.value, projected.shape);
19
- if (capsule === undefined)
20
- throw Error(`REFUSE·pin ${digest} · ${offer.scope} · offered capsule ${offer.value} unavailable`);
21
- return { capsule, offer: canonical(projected) };
13
+ const load = () => {
14
+ const projections = host.filter(line => fieldOf(line, 'scope').startsWith('dep/') && fieldOf(line, 'role') === 'writes' && fieldOf(line, 'value') === offer.value);
15
+ if (!projections.length || projections.some(line => !fieldOf(line, 'shape')))
16
+ throw Error(`REFUSE·pin ${digest} capsule ${offer.value} needs one host projection`);
17
+ const entries = [...new Set(projections.map(line => archiveCoordinate(fieldOf(line, 'shape'))))];
18
+ if (entries.length !== 1)
19
+ throw Error(`REFUSE·pin ${digest} capsule ${offer.value} has conflicting host projections`);
20
+ const loaded = resolve(offer.value, entries[0]);
21
+ if (loaded === undefined)
22
+ throw Error(`REFUSE·pin ${digest} · ${offer.scope} · offered capsule ${offer.value} unavailable`);
23
+ return { capsule: loaded, offer: canonical({ ...offer, shape: entries[0] }) };
24
+ };
25
+ const descriptor = capsuleDescriptor(topos, offer.scope ?? '');
26
+ const loaded = descriptor === undefined ? load() : undefined;
27
+ const capsule = loaded?.capsule ?? {
28
+ digest: offer.value, selection: `${digest} ${offer.scope}`, ...descriptor, ask: requests => {
29
+ if (!requests.length)
30
+ return [];
31
+ const actual = load();
32
+ assertCapsuleDescriptor(descriptor.lines, actual.capsule.lines);
33
+ return actual.capsule.ask(requests);
34
+ },
35
+ };
36
+ const projected = loaded?.offer ?? canonical(offer);
37
+ if (!providerInputRegions(topos, offer.scope ?? '').length)
38
+ return { capsule, offer: projected };
39
+ const fields = ownLock().flatMap(line => { const p = parse(line); return p.kind === 'fact' ? [p.value.fields] : []; });
40
+ const wire = wireAt(fields, Number.MAX_SAFE_INTEGER), algorithm = wire && [...wire.digests][0];
41
+ if (!algorithm)
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 => {
44
+ const provider = providerInputs(topos, digest, offer.scope ?? '', offer.value, request.region ?? '', bytes => fullDigest(bytes, algorithm));
45
+ return provider === undefined ? request : { ...request, provider };
46
+ })) };
47
+ return { capsule: selected, offer: projected };
22
48
  }));
23
49
  }
@@ -6,10 +6,10 @@ export interface SelectedTopos {
6
6
  }
7
7
  export type ToposResolver = (digest: string) => StandingTopos;
8
8
  /** Resolve content using the instrument wire's admitted digest algorithms. No transport interpretation. */
9
- export declare function toposAt(digest: string): StandingTopos;
10
- export declare function contentAt(digest: string): Uint8Array;
11
- /** A declared JS context offer uses this host adapter only after selection; execution is not selection. */
12
- export declare function contextModuleAt(digest: string): string;
9
+ export declare function toposAt(digest: string, store?: string): StandingTopos;
10
+ export declare function contentAt(digest: string, store?: string): Uint8Array;
11
+ /** The declared module runs from the store that supplied its verified bytes. No copy or live-checkout fallback. */
12
+ export declare function contextModuleAt(digest: string, store?: string): string;
13
13
  /** Selection verifies standing identity. Requested capabilities verify their artifact bytes separately. */
14
14
  export declare function selectedTopoi(pins: readonly string[], resolve?: ToposResolver): readonly SelectedTopos[];
15
15
  /** Read a declared wire list without copying a world's claims or executing its offers.
@@ -1,40 +1,18 @@
1
- import { ContentNeeded } from "./content-needed.js";
2
- import { createHash } from 'node:crypto';
1
+ import { verifiedContent } from "./content-store.js";
3
2
  import { readStanding } from '@lapxo/topos/standing';
4
3
  import { parse, wireAt } from '@lapxo/topos/wire';
5
4
  import { fieldOf } from "../fold/claims.js";
6
- import { blobAt } from "../land/ledger.js";
7
- import { observeFile } from "../observe/files.js";
8
- import { ownStore, ownLock } from "../observe/runner.js";
9
5
  /** Resolve content using the instrument wire's admitted digest algorithms. No transport interpretation. */
10
- export function toposAt(digest) {
11
- const topos = readStanding(Buffer.from(contentAt(digest)).toString('utf8'));
6
+ export function toposAt(digest, store) {
7
+ const topos = readStanding(Buffer.from(contentAt(digest, store)).toString('utf8'));
12
8
  return topos;
13
9
  }
14
- export function contentAt(digest) {
15
- const fields = ownLock().flatMap(line => { const got = parse(line); return got.kind === 'fact' ? [got.value.fields] : []; });
16
- const wire = wireAt(fields, Number.MAX_SAFE_INTEGER);
17
- if (wire === null)
18
- throw Error('REFUSE·pin instrument has no admitted wire');
19
- const algorithms = wire.digests;
20
- const bytesAt = (name) => {
21
- const split = name.indexOf(':');
22
- const algorithm = name.slice(0, split), hex = name.slice(split + 1);
23
- if (split < 1 || !algorithms.has(algorithm) || !/^[a-f0-9]+$/.test(hex))
24
- throw Error(`REFUSE·pin ${name} digest is not admitted`);
25
- const bytes = observeFile(blobAt(ownStore(), name));
26
- if (bytes === undefined)
27
- throw new ContentNeeded(name);
28
- if (createHash(algorithm).update(bytes).digest('hex') !== hex)
29
- throw Error(`REFUSE·pin ${name} content hash mismatch · not laid`);
30
- return bytes;
31
- };
32
- return bytesAt(digest);
10
+ export function contentAt(digest, store) {
11
+ return verifiedContent(digest, store).bytes;
33
12
  }
34
- /** A declared JS context offer uses this host adapter only after selection; execution is not selection. */
35
- export function contextModuleAt(digest) {
36
- contentAt(digest);
37
- return blobAt(ownStore(), digest);
13
+ /** The declared module runs from the store that supplied its verified bytes. No copy or live-checkout fallback. */
14
+ export function contextModuleAt(digest, store) {
15
+ return verifiedContent(digest, store).path;
38
16
  }
39
17
  /** Selection verifies standing identity. Requested capabilities verify their artifact bytes separately. */
40
18
  export function selectedTopoi(pins, resolve = toposAt) {
@@ -2,7 +2,7 @@ export interface ContentPort {
2
2
  readonly algorithms: ReadonlySet<string>;
3
3
  readonly read: (digest: string) => Uint8Array | undefined;
4
4
  readonly write: (digest: string, bytes: Uint8Array) => void;
5
- readonly fetch: (url: string) => Promise<Uint8Array>;
5
+ readonly fetch: (url: string, digest?: string) => Promise<Uint8Array>;
6
6
  }
7
7
  /** Transport uses declared locations; only the requested digest admits bytes into the cache. */
8
8
  export declare function resolveSelectedContent(lines: readonly string[], port: ContentPort, requested: readonly string[]): Promise<readonly string[]>;
@@ -1,9 +1,10 @@
1
+ import { transportBytes } from "./ports/transport.js";
2
+ import { contentStore, verifiedContent } from "./content-store.js";
1
3
  import { ContentNeeded } from "./content-needed.js";
2
4
  import { createHash } from 'node:crypto';
3
5
  import { parse, wireAt } from '@lapxo/topos/wire';
4
- import { blobAt, landBlob } from "../land/ledger.js";
5
- import { observeFile } from "../observe/files.js";
6
- import { ownLock, ownStore } from "../observe/runner.js";
6
+ import { landBlob } from "../land/ledger.js";
7
+ import { ownLock } from "../observe/runner.js";
7
8
  /** Transport uses declared locations; only the requested digest admits bytes into the cache. */
8
9
  export async function resolveSelectedContent(lines, port, requested) {
9
10
  const fields = lines.flatMap(line => { const p = parse(line); return p.kind === 'fact' && p.value.fields.value !== 'withdraw' ? [p.value.fields] : []; });
@@ -44,12 +45,9 @@ export async function resolveSelectedContent(lines, port, requested) {
44
45
  throw Error(`REFUSE·pin ${digest} content unavailable · no declared source · not laid`);
45
46
  let last;
46
47
  for (const url of urls) {
47
- const parsed = new URL(url);
48
- if (!['https:', 'http:'].includes(parsed.protocol))
49
- throw Error(`REFUSE·source ${url} needs an explicit transport adapter`);
50
48
  let bytes;
51
49
  try {
52
- bytes = await port.fetch(url);
50
+ bytes = await port.fetch(url, digest);
53
51
  }
54
52
  catch (error) {
55
53
  last = error;
@@ -81,14 +79,16 @@ export async function resolveSources(standing, requested) {
81
79
  });
82
80
  const received = await resolveSelectedContent([...standing, ...bindings], {
83
81
  algorithms: wire.digests,
84
- read: digest => observeFile(blobAt(ownStore(), digest)),
85
- write: (digest, bytes) => { landBlob(ownStore(), digest, bytes); },
86
- fetch: async (url) => {
87
- const response = await fetch(url, { signal: AbortSignal.timeout(30000) });
88
- if (!response.ok)
89
- throw Error(`HTTP ${response.status}`);
90
- return new Uint8Array(await response.arrayBuffer());
91
- },
82
+ read: digest => { try {
83
+ return verifiedContent(digest).bytes;
84
+ }
85
+ catch (error) {
86
+ if (error instanceof ContentNeeded)
87
+ return undefined;
88
+ throw error;
89
+ } },
90
+ write: (digest, bytes) => { landBlob(contentStore(), digest, bytes); },
91
+ fetch: async (url, digest) => transportBytes(url, digest, [...ownLock(), ...standing]),
92
92
  }, requested);
93
93
  for (const digest of received)
94
94
  process.stdout.write(`FETCHED ${digest} · verified\n`);
@@ -1,3 +1,4 @@
1
+ import type { ReaderLifetime } from '@lapxo/topos/wire';
1
2
  import type { Kind } from './run.ts';
2
3
  /** Where a reader's vectors lie: in its package, beside its source, under the name of its module, the folder the wire's vector family names. */
3
4
  export declare const vectorsOf: (module: string, standing: readonly string[]) => string;
@@ -13,6 +14,7 @@ export declare function identityOf(input: {
13
14
  readonly code: string;
14
15
  readonly lineage: readonly string[];
15
16
  readonly kept: readonly string[];
17
+ readonly lifetime?: ReaderLifetime;
16
18
  }): {
17
19
  readonly by: string;
18
20
  readonly lines: readonly string[];
@@ -19,7 +19,7 @@ export function resetsOnEqualEmits(lines) {
19
19
  }
20
20
  return [...named.values()].filter((ats) => ats.size > 1).length;
21
21
  }
22
- function emitted(store, root, module, kind, cases) {
22
+ function emitted(store, root, module, kind, cases, lifetime) {
23
23
  const laid = kind === 'process' ? mkdtempSync(join(tmpdir(), `${selfName()}-vectors-`)) : root;
24
24
  try {
25
25
  for (const one of kind === 'process' ? cases : []) {
@@ -28,7 +28,7 @@ function emitted(store, root, module, kind, cases) {
28
28
  writeFileSync(join(laid, one.place, name), text);
29
29
  }
30
30
  }
31
- const rows = runReader(resolve(root, module), laid, cases.map((one) => ({ place: one.place, ...(kind === 'js' ? { text: one.text ?? '' } : {}) })));
31
+ const rows = runReader(resolve(root, module), laid, cases.map((one) => ({ place: one.place, ...(kind === 'js' ? { text: one.text ?? '' } : {}) })), lifetime);
32
32
  if (cases.length && rows.every((mine) => !mine.length))
33
33
  return undefined;
34
34
  const said = rows.map((mine) => mine.filter((row) => !/^(cost|derived|vector-memo)\//.test(row.scope)).map((row) => JSON.stringify(row)).sort());
@@ -50,7 +50,7 @@ export function identityOf(input) {
50
50
  const known = mine.find((line) => fieldOf(line, 'measure') === 'extension' && fieldOf(line, 'at') === `place:${key}`);
51
51
  if (known !== undefined && fieldOf(known, 'value') === input.code)
52
52
  return { by: input.code, lines: [] };
53
- const emits = emitted(input.store, input.root, input.module, input.kind, JSON.parse(text).cases ?? []);
53
+ const emits = emitted(input.store, input.root, input.module, input.kind, JSON.parse(text).cases ?? [], input.lifetime);
54
54
  if (emits === undefined)
55
55
  return { by: input.code, lines: [] };
56
56
  const by = input.code;
@@ -1,3 +1,4 @@
1
+ import type { ReaderLifetime } from '@lapxo/topos/wire';
1
2
  export type Kind = 'js' | 'process';
2
3
  /** The kinds this host starts. A reader of any other kind is signed and stays potential: nothing here runs it. */
3
4
  export declare const runs: (kind: string) => kind is Kind;
@@ -26,5 +27,5 @@ export declare const hostOf: () => string;
26
27
  * the file it is handed or alone when it reads the place by running it, as the region it was matched for when its
27
28
  * capsule reads with several, and answers one list of claims per place.
28
29
  */
29
- export declare function runReader(module: string, root: string, inputs: readonly Input[]): readonly (readonly Row[])[];
30
+ export declare function runReader(module: string, root: string, inputs: readonly Input[], limits?: ReaderLifetime): readonly (readonly Row[])[];
30
31
  export {};
@@ -1,3 +1,4 @@
1
+ import { boundedProcess } from "../host/process-lifetime.js";
1
2
  import { spawnSync } from 'node:child_process';
2
3
  import { existsSync } from 'node:fs';
3
4
  import { PROTOCOL } from '@lapxo/topos/wire';
@@ -13,7 +14,7 @@ export const hostOf = () => processOf('host');
13
14
  * the file it is handed or alone when it reads the place by running it, as the region it was matched for when its
14
15
  * capsule reads with several, and answers one list of claims per place.
15
16
  */
16
- export function runReader(module, root, inputs) {
17
+ export function runReader(module, root, inputs, limits) {
17
18
  if (!inputs.length)
18
19
  return [];
19
20
  const requests = inputs.map((input) => ({
@@ -23,8 +24,9 @@ export function runReader(module, root, inputs) {
23
24
  const reader = readerOf(module);
24
25
  if (!existsSync(reader.path))
25
26
  throw new Error(`REFUSE·reader ${module} unavailable; its declared capability needs an admitted provider`);
26
- const ran = spawnSync(process.execPath, [...reader.flags, hostOf(), reader.path, module], { cwd: root, input: JSON.stringify(requests), encoding: 'utf8', maxBuffer: 1 << 28 });
27
- if (ran.error)
27
+ const args = [...reader.flags, hostOf(), reader.path, module];
28
+ const ran = limits === undefined ? spawnSync(process.execPath, args, { cwd: root, input: JSON.stringify(requests), encoding: 'utf8', maxBuffer: 1 << 28 }) : boundedProcess(process.execPath, args, JSON.stringify(requests), limits, root);
29
+ if ('error' in ran && ran.error)
28
30
  throw Object.assign(new Error(`${module}: reader process failed: ${ran.error.message}`), { cause: ran.error });
29
31
  if (ran.status !== 0)
30
32
  throw new Error(`${module}: reader exited ${ran.status ?? `with signal ${ran.signal}`}\n${ran.stderr ?? ''}${ran.stdout ?? ''}`);
@@ -23,7 +23,8 @@ export function receiptRegion(fold, at) {
23
23
  const contract = wireLine(ownLock(), 'receipt-instrument');
24
24
  const coordinate = contract === undefined ? undefined : fieldOf(contract, 'value');
25
25
  const implementation = coordinate === undefined ? undefined : instrumentDigest(fold.store, sourceOf(), [], ownRoot(), ownLock());
26
- const observed = [...receiptsOf(fold), ...(coordinate && implementation ? [canonical({ scope: coordinate, role: 'writes', form: 'alphabet', measure: 'digest', value: implementation, at: `place:${implementation}`, by: 'bound' })] : [])];
26
+ // The current implementation seal is metadata, not an observation carried from a previous fold.
27
+ const observed = [...receiptsOf(fold).filter(line => !coordinate || fieldOf(line, 'scope') !== coordinate), ...(coordinate && implementation ? [canonical({ scope: coordinate, role: 'writes', form: 'alphabet', measure: 'digest', value: implementation, at: `place:${implementation}`, by: 'bound' })] : [])];
27
28
  const hash = (bytes) => bytesDigest(fold.store, new TextEncoder().encode(bytes));
28
29
  const head = keyOf(fold.store, 'attest');
29
30
  const projection = receiptProjection(observed, fields, hash, head ? [head] : []);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lapxo/bound",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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",
@@ -32,7 +32,7 @@
32
32
  "license": "MIT",
33
33
  "dependencies": {
34
34
  "@lapxo/obligations": "0.3.5",
35
- "@lapxo/topos": "0.1.8"
35
+ "@lapxo/topos": "0.1.9"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^20.0.0",