@lapxo/bound 0.1.4 → 0.1.5

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 (44) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +26 -5
  3. package/TARGET.bound +83 -19
  4. package/dist/cli/accept.js +21 -9
  5. package/dist/cli/owner.d.ts +2 -0
  6. package/dist/cli/owner.js +17 -2
  7. package/dist/cli/pass.js +15 -10
  8. package/dist/cli/place.js +8 -1
  9. package/dist/cli/verb.js +35 -1
  10. package/dist/fold/claims.js +5 -1
  11. package/dist/fold/closed.js +44 -13
  12. package/dist/fold/digests.js +5 -2
  13. package/dist/fold/observed.js +195 -174
  14. package/dist/fold/resolved.js +6 -2
  15. package/dist/host/act-cost.d.ts +13 -0
  16. package/dist/host/act-cost.js +25 -0
  17. package/dist/host/adapters/modules/javascript.d.ts +1 -0
  18. package/dist/host/adapters/modules/javascript.js +48 -0
  19. package/dist/host/capsule-process.js +22 -19
  20. package/dist/host/capsule.js +5 -6
  21. package/dist/host/ports/ingress.d.ts +24 -0
  22. package/dist/host/ports/ingress.js +81 -0
  23. package/dist/host/reading-closures.d.ts +28 -0
  24. package/dist/host/reading-closures.js +146 -0
  25. package/dist/host/release.js +24 -2
  26. package/dist/host/walk.d.ts +6 -0
  27. package/dist/host/walk.js +193 -0
  28. package/dist/observe/regions.js +3 -5
  29. package/dist/render/field.d.ts +1 -7
  30. package/dist/render/field.js +6 -12
  31. package/dist/render/names.d.ts +1 -1
  32. package/dist/render/names.js +1 -1
  33. package/dist/render/view.js +16 -11
  34. package/docs/contracts/archive.md +11 -0
  35. package/docs/contracts/cli.md +11 -0
  36. package/docs/contracts/documents.md +35 -0
  37. package/docs/contracts/evidence-view.md +6 -0
  38. package/docs/contracts/publication.md +17 -0
  39. package/docs/contracts/typed-object.md +27 -0
  40. package/docs/contracts/view-walk.md +7 -0
  41. package/docs/contracts/views.md +29 -0
  42. package/docs/usage.md +6 -0
  43. package/package.json +6 -4
  44. package/receipts.bound +314 -92
@@ -1,3 +1,5 @@
1
+ import { actCosts } from "../host/act-cost.js";
2
+ import { readingClosures } from "../host/reading-closures.js";
1
3
  import { readerLifetime, readerAllowsEmpty, assertReaderInputs, parse as parseLifetime } from '@lapxo/topos/wire';
2
4
  import { readOnly } from "../host/read-only.js";
3
5
  import { basename, dirname, existsSync, mkdirSync, resolve, rmSync, spawn, statSync, writeFileSync } from "../host/io.js";
@@ -266,202 +268,221 @@ export function observedClaims(root, store, options = {}) {
266
268
  function observeOne(reader, started) {
267
269
  const module = resolve(root, reader.module);
268
270
  const asked = reader.capsule === undefined ? undefined : reader.capsule.selected ?? capsuleAt(reader.capsule.digest, reader.capsule.entry);
269
- const source = reader.capsule === undefined ? observeFile(module) : asked === undefined ? undefined : new Uint8Array();
270
- const bundle = source && reader.capsule === undefined ? bundleOf(module) ?? bytesDigest(store, source) : 'none';
271
- const offered = offersOf(root, store, reader, roleOf, look, (where) => reached(trimmed(where)));
272
- assertReaderInputs(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module, offered.length);
273
- const vectors = reader.capsule === undefined ? observeText(resolve(root, vectorsOf(reader.module, standing))) : undefined;
274
- const kept = whole && legacy === undefined && answered === undefined && source ? sha([
275
- speaker, reader.module, reader.kind, reader.shape.join('|'), reader.where.join('|'), bundle, vectors === undefined ? 'no vectors' : sha(vectors),
276
- reader.capsule === undefined ? '' : `${reader.capsule.selected?.selection ?? reader.capsule.digest} ${reader.capsule.region} ${reader.capsule.reads.join('|')} ${[...reader.capsule.globs].map(([place, globs]) => `${place}${globs.join('|')}`).join(' ')}`,
277
- ...offered.map((offer) => `${offer.place} ${offer.digest?.() ?? bytesDigest(store, offer.bytes())}`),
278
- ].join('\n')) : undefined;
279
- const stored = kept === undefined ? undefined : observeText(memoAt(store, `observed/${kept}`));
280
- if (stored !== undefined) {
281
- out.push(...stored.split('\n').filter(isWire));
282
- read += offered.length;
283
- whole_ += 1;
271
+ const projection = reader.capsule === undefined ? [] : (asked?.lines ?? []).filter(line => fieldOf(line, 'scope') === `region/${reader.capsule.region}` && fieldOf(line, 'measure') === 'input-projection' && fieldOf(line, 'value') !== 'withdraw');
272
+ if (projection.length) {
273
+ if (projection.length !== 1 || !asked)
274
+ throw Error('REFUSE·input projection is ambiguous ' + reader.module);
275
+ const files = [...new Set(reader.where.flatMap(where => handedAt(root, reader, roleOf, look, where)))].sort().map(place => ({ place, text: observeText(resolve(root, place)) ?? '' }));
276
+ const result = readingClosures({ root, store, reader: reader.module, speaker, digest: text => bytesDigest(store, Buffer.from(text)), capsule: asked, region: reader.capsule.region, projection: fieldOf(projection[0], 'value'), allowsEmpty: readerAllowsEmpty(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module), files, emit: line => process.stderr.write(line + '\n') });
277
+ out.push(...result.lines);
278
+ ran += result.executed;
279
+ read += result.read - result.opened;
284
280
  return false;
285
281
  }
286
- const mark = out.length;
287
- const home = regionIn(regions, reader.module);
288
- const own = shardLines(store, speaker, home).filter((line) => fieldOf(line, 'scope') === `reader/${reader.module}`);
289
- const lineage = own.filter((line) => fieldOf(line, 'measure') === 'identity').map((line) => fieldOf(line, 'value'));
290
- const sum = source && reader.capsule === undefined ? bytesDigest(store, new TextEncoder().encode(`${reader.module} ${bundle}`)).replace(/^sha256:/, '').slice(0, 12) : 'none';
291
- const measured = reader.capsule !== undefined ? { by: source ? sha(`${reader.capsule.selected?.selection ?? reader.capsule.digest}\n${reader.capsule.region}`).slice(0, 12) : 'none', lines: [] }
292
- : source ? identityOf({ store, root, speaker, module: reader.module, kind: reader.kind, code: sum, lineage, lifetime: readerLifetime(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module), kept: own.filter((line) => /(?:^|\s)measure=(?:extension|emits)\s/.test(line)) })
293
- : { by: 'none', lines: [] };
294
- const by = measured.by;
295
- const level = [...measured.lines];
296
- const identified = Date.now();
297
- const lines = [...lineage, by];
298
- const before = [...lineage].reverse().find((one) => one !== by);
299
- const regionsOf = new Map();
300
- for (const offer of offered) {
301
- const region = regionIn(regions, offer.place);
302
- if (reached(region))
303
- regionsOf.set(region, [...(regionsOf.get(region) ?? []), offer]);
304
- }
305
- const planned = [...regionsOf].map(([region, offers]) => {
306
- const shard = open(region);
307
- const places = offers.map((offer) => offer.place);
308
- const pairs = [];
309
- const kept = [];
310
- const moved = [];
311
- const all = [];
312
- for (const offer of offers) {
313
- const known = shard.places.get(`${by} ${offer.place}`);
314
- if (known && fieldOf(known.line, 'value') === offer.seen) {
315
- pairs.push(`${offer.place} ${known.at}`);
316
- kept.push(known.line);
317
- all.push({ ...offer, digest: known.at.slice('place:'.length) });
318
- read += 1;
319
- continue;
282
+ const endAct = actCosts.begin();
283
+ try {
284
+ const source = reader.capsule === undefined ? observeFile(module) : asked === undefined ? undefined : new Uint8Array();
285
+ const bundle = source && reader.capsule === undefined ? bundleOf(module) ?? bytesDigest(store, source) : 'none';
286
+ const offered = offersOf(root, store, reader, roleOf, look, (where) => reached(trimmed(where)));
287
+ assertReaderInputs(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module, offered.length);
288
+ const vectors = reader.capsule === undefined ? observeText(resolve(root, vectorsOf(reader.module, standing))) : undefined;
289
+ const kept = whole && legacy === undefined && answered === undefined && source ? sha([
290
+ speaker, reader.module, reader.kind, reader.shape.join('|'), reader.where.join('|'), bundle, vectors === undefined ? 'no vectors' : sha(vectors),
291
+ reader.capsule === undefined ? '' : `${reader.capsule.selected?.selection ?? reader.capsule.digest} ${reader.capsule.region} ${reader.capsule.reads.join('|')} ${[...reader.capsule.globs].map(([place, globs]) => `${place}${globs.join('|')}`).join(' ')}`,
292
+ ...offered.map((offer) => `${offer.place} ${offer.digest?.() ?? bytesDigest(store, offer.bytes())}`),
293
+ ].join('\n')) : undefined;
294
+ const stored = kept === undefined ? undefined : observeText(memoAt(store, `observed/${kept}`));
295
+ if (stored !== undefined) {
296
+ out.push(...stored.split('\n').filter(isWire));
297
+ read += offered.length;
298
+ whole_ += 1;
299
+ return false;
300
+ }
301
+ const mark = out.length;
302
+ const home = regionIn(regions, reader.module);
303
+ const own = shardLines(store, speaker, home).filter((line) => fieldOf(line, 'scope') === `reader/${reader.module}`);
304
+ const lineage = own.filter((line) => fieldOf(line, 'measure') === 'identity').map((line) => fieldOf(line, 'value'));
305
+ const sum = source && reader.capsule === undefined ? bytesDigest(store, new TextEncoder().encode(`${reader.module} ${bundle}`)).replace(/^sha256:/, '').slice(0, 12) : 'none';
306
+ const measured = reader.capsule !== undefined ? { by: source ? sha(`${reader.capsule.selected?.selection ?? reader.capsule.digest}\n${reader.capsule.region}`).slice(0, 12) : 'none', lines: [] }
307
+ : source ? identityOf({ store, root, speaker, module: reader.module, kind: reader.kind, code: sum, lineage, lifetime: readerLifetime(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module), kept: own.filter((line) => /(?:^|\s)measure=(?:extension|emits)\s/.test(line)) })
308
+ : { by: 'none', lines: [] };
309
+ const by = measured.by;
310
+ const level = [...measured.lines];
311
+ const identified = Date.now();
312
+ const lines = [...lineage, by];
313
+ const before = [...lineage].reverse().find((one) => one !== by);
314
+ const regionsOf = new Map();
315
+ for (const offer of offered) {
316
+ const region = regionIn(regions, offer.place);
317
+ if (reached(region))
318
+ regionsOf.set(region, [...(regionsOf.get(region) ?? []), offer]);
319
+ }
320
+ const planned = [...regionsOf].map(([region, offers]) => {
321
+ const shard = open(region);
322
+ const places = offers.map((offer) => offer.place);
323
+ const pairs = [];
324
+ const kept = [];
325
+ const moved = [];
326
+ const all = [];
327
+ for (const offer of offers) {
328
+ const known = shard.places.get(`${by} ${offer.place}`);
329
+ if (known && fieldOf(known.line, 'value') === offer.seen) {
330
+ pairs.push(`${offer.place} ${known.at}`);
331
+ kept.push(known.line);
332
+ all.push({ ...offer, digest: known.at.slice('place:'.length) });
333
+ read += 1;
334
+ continue;
335
+ }
336
+ const bytes = offer.digest ? undefined : offer.bytes();
337
+ const digest = offer.digest?.() ?? bytesDigest(store, bytes);
338
+ pairs.push(`${offer.place} place:${digest}`);
339
+ kept.push(canonical({ scope: offer.place, role: 'writes', form: 'alphabet', measure: 'observed', value: offer.seen, by: `${speaker}:${by}`, at: `place:${digest}` }));
340
+ all.push({ ...offer, digest, bytes: () => bytes ?? offer.bytes() });
341
+ if (known?.at === `place:${digest}`)
342
+ read += 1;
343
+ else
344
+ moved.push({ place: offer.place, seen: offer.seen, digest, bytes: bytes ?? offer.bytes() });
345
+ }
346
+ return { region, shard, places, pairs, kept, moved, all, key: keyOf(by, region, pairs) };
347
+ });
348
+ if (answered !== undefined && planned.every((plan) => !plan.moved.length && observedAt(store, plan.key))) {
349
+ for (const plan of planned) {
350
+ for (const [at, digest] of digestsAt(plan.key))
351
+ answered.set(`${by} ${at}`, digest);
352
+ if (plan.shard.keys.get(by) !== plan.key || plan.kept.some((line) => plan.shard.places.get(`${by} ${fieldOf(line, 'scope')}`)?.line !== line)) {
353
+ landShard(store, speaker, plan.region, [...plan.kept, pointer(speaker, by, plan.region, plan.key, plan.pairs)]);
354
+ }
355
+ out.push(...plan.kept);
320
356
  }
321
- const bytes = offer.digest ? undefined : offer.bytes();
322
- const digest = offer.digest?.() ?? bytesDigest(store, bytes);
323
- pairs.push(`${offer.place} place:${digest}`);
324
- kept.push(canonical({ scope: offer.place, role: 'writes', form: 'alphabet', measure: 'observed', value: offer.seen, by: `${speaker}:${by}`, at: `place:${digest}` }));
325
- all.push({ ...offer, digest, bytes: () => bytes ?? offer.bytes() });
326
- if (known?.at === `place:${digest}`)
327
- read += 1;
328
- else
329
- moved.push({ place: offer.place, seen: offer.seen, digest, bytes: bytes ?? offer.bytes() });
357
+ if (level.length)
358
+ landShard(store, speaker, home, level);
359
+ return true;
330
360
  }
331
- return { region, shard, places, pairs, kept, moved, all, key: keyOf(by, region, pairs) };
332
- });
333
- if (answered !== undefined && planned.every((plan) => !plan.moved.length && observedAt(store, plan.key))) {
334
- for (const plan of planned) {
335
- for (const [at, digest] of digestsAt(plan.key))
336
- answered.set(`${by} ${at}`, digest);
337
- if (plan.shard.keys.get(by) !== plan.key || plan.kept.some((line) => plan.shard.places.get(`${by} ${fieldOf(line, 'scope')}`)?.line !== line)) {
338
- landShard(store, speaker, plan.region, [...plan.kept, pointer(speaker, by, plan.region, plan.key, plan.pairs)]);
361
+ const plans = planned.map(({ region, shard, places, pairs, kept, moved, all, key }) => {
362
+ const prior = lastOf(shard, by, places);
363
+ if (before !== undefined)
364
+ lastOf(shard, before, places);
365
+ const ats = [...new Set(pairs.map((pair) => pair.slice(pair.indexOf(' ') + 1)))];
366
+ const found = byAt(key);
367
+ const said = found !== undefined ? ats.flatMap((at) => found.get(at) ?? [])
368
+ : moved.length || prior === undefined ? undefined : ats.flatMap((at) => prior.get(at) ?? []);
369
+ const due = said === undefined && !moved.length ? all.map((one) => ({ place: one.place, seen: one.seen, digest: one.digest, bytes: one.bytes() })) : moved;
370
+ return { region, shard, places, pairs, kept, due, key, said, prior, found: found !== undefined };
371
+ });
372
+ const due = plans.flatMap((plan) => (plan.said === undefined ? plan.due.map((one) => ({ ...one, region: plan.region })) : []));
373
+ const heldOf = (place, region) => {
374
+ for (const one of [...lines].reverse()) {
375
+ const shard = open(region);
376
+ const mine = servedAt(shard, one, place, lastOf(shard, one, [place])).slice(1);
377
+ if (mine.length)
378
+ return mine.map((line) => [fieldOf(line, 'scope'), fieldOf(line, 'value')]);
339
379
  }
340
- out.push(...plan.kept);
380
+ return [];
381
+ };
382
+ if (due.length && !source) {
383
+ refused.push(`REFUSE·reader ${reader.module} unavailable · ${due.length} readings remain due`);
384
+ return false;
341
385
  }
342
- if (level.length)
343
- landShard(store, speaker, home, level);
344
- return true;
345
- }
346
- const plans = planned.map(({ region, shard, places, pairs, kept, moved, all, key }) => {
347
- const prior = lastOf(shard, by, places);
348
- if (before !== undefined)
349
- lastOf(shard, before, places);
350
- const ats = [...new Set(pairs.map((pair) => pair.slice(pair.indexOf(' ') + 1)))];
351
- const found = byAt(key);
352
- const said = found !== undefined ? ats.flatMap((at) => found.get(at) ?? [])
353
- : moved.length || prior === undefined ? undefined : ats.flatMap((at) => prior.get(at) ?? []);
354
- const due = said === undefined && !moved.length ? all.map((one) => ({ place: one.place, seen: one.seen, digest: one.digest, bytes: one.bytes() })) : moved;
355
- return { region, shard, places, pairs, kept, due, key, said, prior, found: found !== undefined };
356
- });
357
- const due = plans.flatMap((plan) => (plan.said === undefined ? plan.due.map((one) => ({ ...one, region: plan.region })) : []));
358
- const heldOf = (place, region) => {
359
- for (const one of [...lines].reverse()) {
360
- const shard = open(region);
361
- const mine = servedAt(shard, one, place, lastOf(shard, one, [place])).slice(1);
362
- if (mine.length)
363
- return mine.map((line) => [fieldOf(line, 'scope'), fieldOf(line, 'value')]);
386
+ if (due.length && !wait) {
387
+ for (const plan of plans) {
388
+ if (plan.said !== undefined) {
389
+ out.push(...plan.kept, ...plan.said);
390
+ continue;
391
+ }
392
+ for (const place of plan.places) {
393
+ const mine = servedAt(plan.shard, by, place, plan.prior);
394
+ out.push(...(mine.length || before === undefined ? mine : servedAt(plan.shard, before, place, lastOf(plan.shard, before, plan.places))));
395
+ }
396
+ }
397
+ const marker = storeAt(store, 'pending', by);
398
+ const pid = Number((observeText(marker) ?? '').split('\n')[1]);
399
+ const running = existsSync(marker) && Date.now() - statSync(marker).mtimeMs < 30 * 60 * 1000 && Number.isInteger(pid) && pid > 0 && alive(pid);
400
+ if (!running) {
401
+ mkdirSync(dirname(marker), { recursive: true });
402
+ const child = spawn(process.execPath, [...childFlags(), processOf('observe'), root, reader.module], { cwd: root, detached: true, stdio: 'ignore' });
403
+ child.unref();
404
+ writeFileSync(marker, `${reader.module}\n${child.pid ?? ''}\n`);
405
+ }
406
+ behind.push({ module: reader.module, places: due.length });
407
+ process.stderr.write(`GREY reading: ${reader.module} · ${due.length} places · runs behind${running ? ', already running' : ''}\n`);
408
+ if (level.length)
409
+ landShard(store, speaker, home, level);
410
+ if (Date.now() - started > 250)
411
+ spent.push(`${reader.module.split('/').pop()} behind ${Date.now() - started}`);
412
+ return false;
364
413
  }
365
- return [];
366
- };
367
- if (due.length && !source) {
368
- refused.push(`REFUSE·reader ${reader.module} unavailable · ${due.length} readings remain due`);
369
- return false;
370
- }
371
- if (due.length && !wait) {
372
- for (const plan of plans) {
373
- if (plan.said !== undefined) {
374
- out.push(...plan.kept, ...plan.said);
375
- continue;
414
+ if (due.length) {
415
+ const at = Date.now();
416
+ let rows;
417
+ try {
418
+ rows = reader.capsule !== undefined && asked !== undefined
419
+ ? asked.ask(due.map((d) => ({
420
+ protocol: PROTOCOL, verb: 'read', rootScope: d.place, region: reader.capsule.region, held: heldOf(d.place, d.region),
421
+ files: (reader.capsule.reads.some((read) => read.includes('@')) ? handedAt(root, reader, roleOf, look, d.place) : [d.place]).map((coordinate) => ({ place: coordinate, text: observeText(resolve(root, coordinate)) ?? '' })),
422
+ }))).map((answer) => {
423
+ if (answer?.kind !== 'fact')
424
+ throw new Error(`REFUSE·reader ${reader.module} · ${answer?.why ?? 'no capsule answer'}`);
425
+ return (answer.claims ?? []);
426
+ })
427
+ : runReader(module, root, due.map((d) => ({ place: d.place, ...(reader.kind === 'js' ? { text: new TextDecoder().decode(d.bytes) } : {}), held: heldOf(d.place, d.region) })), readerLifetime(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module));
376
428
  }
377
- for (const place of plan.places) {
378
- const mine = servedAt(plan.shard, by, place, plan.prior);
379
- out.push(...(mine.length || before === undefined ? mine : servedAt(plan.shard, before, place, lastOf(plan.shard, before, plan.places))));
429
+ catch (error) {
430
+ const why = `REFUSE·reader ${reader.module} · ${error instanceof Error ? error.message : String(error)}`;
431
+ refused.push(why);
432
+ process.stderr.write(`${why}\n`);
433
+ return false;
380
434
  }
435
+ if (rows.every((mine) => !mine.length) && !readerAllowsEmpty(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module)) {
436
+ const why = `REFUSE·reader ${reader.module} · answered none of ${due.length} places · nothing landed, they stay due`;
437
+ refused.push(why);
438
+ process.stderr.write(`${why}\n`);
439
+ return false;
440
+ }
441
+ ran += due.length;
442
+ process.stderr.write(`READER ${reader.module} · ${due.length} run · ${Date.now() - at} ms\n`);
443
+ const fresh = new Map();
444
+ due.forEach((d, i) => fresh.set(d.place, (rows[i] ?? []).map((row) => lineOf(row, d.digest, by, speaker))));
445
+ for (const plan of plans.filter((one) => one.said === undefined)) {
446
+ const mine = new Set(plan.due.map((d) => d.place));
447
+ const stay = [...new Set(plan.pairs.filter((pair) => !mine.has(pair.slice(0, pair.indexOf(' ')))).map((pair) => pair.slice(pair.indexOf(' ') + 1)))];
448
+ const said = [...stay.flatMap((one) => plan.prior?.get(one) ?? []), ...[...mine].flatMap((place) => fresh.get(place) ?? [])];
449
+ keepObservation(store, plan.key, said);
450
+ landShard(store, speaker, plan.region, [...plan.kept, pointer(speaker, by, plan.region, plan.key, plan.pairs)]);
451
+ out.push(...plan.kept, ...said);
452
+ }
453
+ const spent = Date.now() - at;
454
+ const over = bytesDigest(store, new TextEncoder().encode(due.map((d) => d.digest).sort().join('\n'))).replace(/^sha256:/, '');
455
+ level.push(canonical({ scope: `cost/reader/${reader.module}`, role: 'writes', form: 'interval', measure: 'ms', value: `${spent}..${spent}`, by: `${speaker}:${by}`, at: `place:${over}` }));
456
+ if (lineage.at(-1) !== by)
457
+ level.push(canonical({ scope: `reader/${reader.module}`, role: 'writes', form: 'alphabet', measure: 'identity', value: by, by: `${speaker}:${by}`, at: `place:${by}` }));
458
+ rmSync(storeAt(store, 'pending', by), { force: true });
381
459
  }
382
- const marker = storeAt(store, 'pending', by);
383
- const pid = Number((observeText(marker) ?? '').split('\n')[1]);
384
- const running = existsSync(marker) && Date.now() - statSync(marker).mtimeMs < 30 * 60 * 1000 && Number.isInteger(pid) && pid > 0 && alive(pid);
385
- if (!running) {
386
- mkdirSync(dirname(marker), { recursive: true });
387
- const child = spawn(process.execPath, [...childFlags(), processOf('observe'), root, reader.module], { cwd: root, detached: true, stdio: 'ignore' });
388
- child.unref();
389
- writeFileSync(marker, `${reader.module}\n${child.pid ?? ''}\n`);
460
+ for (const plan of plans.filter((one) => one.said !== undefined)) {
461
+ if (!plan.found)
462
+ keepObservation(store, plan.key, plan.said);
463
+ if (plan.shard.keys.get(by) !== plan.key || plan.kept.some((line) => plan.shard.places.get(`${by} ${fieldOf(line, 'scope')}`)?.line !== line)) {
464
+ landShard(store, speaker, plan.region, [...plan.kept, pointer(speaker, by, plan.region, plan.key, plan.pairs)]);
465
+ }
466
+ out.push(...plan.kept, ...plan.said);
390
467
  }
391
- behind.push({ module: reader.module, places: due.length });
392
- process.stderr.write(`GREY reading: ${reader.module} · ${due.length} places · runs behind${running ? ', already running' : ''}\n`);
393
468
  if (level.length)
394
469
  landShard(store, speaker, home, level);
395
470
  if (Date.now() - started > 250)
396
- spent.push(`${reader.module.split('/').pop()} behind ${Date.now() - started}`);
471
+ spent.push(`${reader.module.split('/').pop()} ${identified - started}+${Date.now() - identified}`);
472
+ if (kept !== undefined)
473
+ landMemo(store, `observed/${kept}`, new TextEncoder().encode(out.slice(mark).join('\n')));
397
474
  return false;
398
475
  }
399
- if (due.length) {
400
- const at = Date.now();
401
- let rows;
402
- try {
403
- rows = reader.capsule !== undefined && asked !== undefined
404
- ? asked.ask(due.map((d) => ({
405
- protocol: PROTOCOL, verb: 'read', rootScope: d.place, region: reader.capsule.region, held: heldOf(d.place, d.region),
406
- files: (reader.capsule.reads.some((read) => read.includes('@')) ? handedAt(root, reader, roleOf, look, d.place) : [d.place]).map((coordinate) => ({ place: coordinate, text: observeText(resolve(root, coordinate)) ?? '' })),
407
- }))).map((answer) => {
408
- if (answer?.kind !== 'fact')
409
- throw new Error(`REFUSE·reader ${reader.module} · ${answer?.why ?? 'no capsule answer'}`);
410
- return (answer.claims ?? []);
411
- })
412
- : runReader(module, root, due.map((d) => ({ place: d.place, ...(reader.kind === 'js' ? { text: new TextDecoder().decode(d.bytes) } : {}), held: heldOf(d.place, d.region) })), readerLifetime(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module));
413
- }
414
- catch (error) {
415
- const why = `REFUSE·reader ${reader.module} · ${error instanceof Error ? error.message : String(error)}`;
416
- refused.push(why);
417
- process.stderr.write(`${why}\n`);
418
- return false;
419
- }
420
- if (rows.every((mine) => !mine.length) && !readerAllowsEmpty(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module)) {
421
- const why = `REFUSE·reader ${reader.module} · answered none of ${due.length} places · nothing landed, they stay due`;
422
- refused.push(why);
423
- process.stderr.write(`${why}\n`);
424
- return false;
425
- }
426
- ran += due.length;
427
- process.stderr.write(`READER ${reader.module} · ${due.length} run · ${Date.now() - at} ms\n`);
428
- const fresh = new Map();
429
- due.forEach((d, i) => fresh.set(d.place, (rows[i] ?? []).map((row) => lineOf(row, d.digest, by, speaker))));
430
- for (const plan of plans.filter((one) => one.said === undefined)) {
431
- const mine = new Set(plan.due.map((d) => d.place));
432
- const stay = [...new Set(plan.pairs.filter((pair) => !mine.has(pair.slice(0, pair.indexOf(' ')))).map((pair) => pair.slice(pair.indexOf(' ') + 1)))];
433
- const said = [...stay.flatMap((one) => plan.prior?.get(one) ?? []), ...[...mine].flatMap((place) => fresh.get(place) ?? [])];
434
- keepObservation(store, plan.key, said);
435
- landShard(store, speaker, plan.region, [...plan.kept, pointer(speaker, by, plan.region, plan.key, plan.pairs)]);
436
- out.push(...plan.kept, ...said);
437
- }
438
- const spent = Date.now() - at;
439
- const over = bytesDigest(store, new TextEncoder().encode(due.map((d) => d.digest).sort().join('\n'))).replace(/^sha256:/, '');
440
- level.push(canonical({ scope: `cost/reader/${reader.module}`, role: 'writes', form: 'interval', measure: 'ms', value: `${spent}..${spent}`, by: `${speaker}:${by}`, at: `place:${over}` }));
441
- if (lineage.at(-1) !== by)
442
- level.push(canonical({ scope: `reader/${reader.module}`, role: 'writes', form: 'alphabet', measure: 'identity', value: by, by: `${speaker}:${by}`, at: `place:${by}` }));
443
- rmSync(storeAt(store, 'pending', by), { force: true });
444
- }
445
- for (const plan of plans.filter((one) => one.said !== undefined)) {
446
- if (!plan.found)
447
- keepObservation(store, plan.key, plan.said);
448
- if (plan.shard.keys.get(by) !== plan.key || plan.kept.some((line) => plan.shard.places.get(`${by} ${fieldOf(line, 'scope')}`)?.line !== line)) {
449
- landShard(store, speaker, plan.region, [...plan.kept, pointer(speaker, by, plan.region, plan.key, plan.pairs)]);
450
- }
451
- out.push(...plan.kept, ...plan.said);
476
+ finally {
477
+ endAct();
452
478
  }
453
- if (level.length)
454
- landShard(store, speaker, home, level);
455
- if (Date.now() - started > 250)
456
- spent.push(`${reader.module.split('/').pop()} ${identified - started}+${Date.now() - identified}`);
457
- if (kept !== undefined)
458
- landMemo(store, `observed/${kept}`, new TextEncoder().encode(out.slice(mark).join('\n')));
459
- return false;
460
479
  }
461
480
  if (spent.length)
462
481
  process.stderr.write(`SPENT observe ${spent.join(' · ')} ms\n`);
463
482
  if (legacy)
464
483
  setAside(store, speaker);
465
484
  process.stderr.write(`OBSERVE ${ran} run · ${read} read by key${whole_ ? ` · ${whole_} readers kept whole` : ''} · ${Date.now() - began} ms\n`);
485
+ if (wait && refused.length)
486
+ throw new Error(refused.join('\n'));
466
487
  return [...new Set(out)].sort();
467
488
  }
@@ -95,20 +95,24 @@ export function keepCarried(store, lines, from) {
95
95
  * the caller refuses rather than read the lines that only name it.
96
96
  */
97
97
  export function carriedFrom(store, shipped) {
98
- const named = shipped.find((line) => fieldOf(line, 'scope') === 'receipts' && fieldOf(line, 'measure') === 'digest');
98
+ // Signed receipts inside the observed set are evidence, not its framing.
99
+ const headerOf = (line) => fieldOf(line, 'scope') === 'receipts' && fieldOf(line, 'measure') === 'digest' && fieldOf(line, 'by') === 'bound' && !fieldOf(line, 'sig');
100
+ const named = shipped.find(headerOf);
99
101
  if (named === undefined)
100
102
  return shipped;
101
103
  // Resolution 8 may precede its authenticated summary in the same public file.
102
104
  // It is evidence only if its exact bytes meet the summary's carrier digest.
103
105
  for (let i = 1; i < shipped.length; i += 1) {
104
106
  const header = shipped[i];
105
- if (fieldOf(header, 'scope') !== 'receipts' || fieldOf(header, 'measure') !== 'digest')
107
+ if (!headerOf(header))
106
108
  continue;
107
109
  const digest = fieldOf(header, 'at').replace(/^place:sha256:/, '');
108
110
  const body = shipped.slice(0, i);
109
111
  if (/^[0-9a-f]{64}$/.test(digest) && createHash('sha256').update(body.map(line => `${line}\n`).join('')).digest('hex') === digest)
110
112
  return body;
111
113
  }
114
+ if (shipped.indexOf(named) > 0)
115
+ throw Error('REFUSE·receipt public carrier bytes mismatch; a cached carrier cannot replace supplied evidence');
112
116
  const hex = fieldOf(named, 'at').replace(/^place:(?:sha256:)?/, '');
113
117
  const text = /^[0-9a-f]{64}$/.test(hex) ? verifiedCarried(storeAt(store, 'cas', 'carried', hex), hex) : undefined;
114
118
  return text === undefined ? undefined : text.split('\n').filter(isWire);
@@ -0,0 +1,13 @@
1
+ /** A sequence of declared acts is not one act. Unpartitioned host work still has a cost. */
2
+ export declare class ActCosts {
3
+ private readonly began;
4
+ private occupied;
5
+ private largest;
6
+ private depth;
7
+ private readonly clock;
8
+ constructor(clock?: () => number);
9
+ begin(): () => void;
10
+ measure<T>(work: () => T): T;
11
+ seconds(): number;
12
+ }
13
+ export declare const actCosts: ActCosts;
@@ -0,0 +1,25 @@
1
+ /** A sequence of declared acts is not one act. Unpartitioned host work still has a cost. */
2
+ export class ActCosts {
3
+ constructor(clock = () => performance.now()) {
4
+ this.occupied = 0;
5
+ this.largest = 0;
6
+ this.depth = 0;
7
+ this.clock = clock;
8
+ this.began = clock();
9
+ }
10
+ begin() {
11
+ const started = this.clock(), outer = this.depth++ === 0;
12
+ let closed = false;
13
+ return () => { if (closed)
14
+ throw Error('Act measurement closed twice'); closed = true; const elapsed = Math.max(0, this.clock() - started); this.depth--; this.largest = Math.max(this.largest, elapsed); if (outer)
15
+ this.occupied += elapsed; };
16
+ }
17
+ measure(work) { const end = this.begin(); try {
18
+ return work();
19
+ }
20
+ finally {
21
+ end();
22
+ } }
23
+ seconds() { return Math.max(0, Math.round(Math.max(this.largest, this.clock() - this.began - this.occupied) / 1000)); }
24
+ }
25
+ export const actCosts = new ActCosts();
@@ -0,0 +1 @@
1
+ export declare function moduleImports(source: string, coordinate: string): readonly string[];
@@ -0,0 +1,48 @@
1
+ import * as nodeModule from 'node:module';
2
+ import { extname } from 'node:path';
3
+ /** Host module syntax, never wire or domain interpretation. Reading imports executes no module. */
4
+ // The package's minimum Node runtime provides this API; older build-only Node types do not.
5
+ const require = nodeModule.createRequire(import.meta.url);
6
+ const { parse } = require('acorn');
7
+ const stripTypeScriptTypes = nodeModule.stripTypeScriptTypes;
8
+ export function moduleImports(source, coordinate) {
9
+ const code = extname(coordinate) === '.ts' ? stripTypeScriptTypes(source, { mode: 'transform' }) : source;
10
+ let tree;
11
+ try {
12
+ tree = parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowReturnOutsideFunction: true, allowHashBang: true });
13
+ }
14
+ catch {
15
+ throw Error('REFUSE·source module syntax cannot be read: ' + coordinate);
16
+ }
17
+ const found = new Set();
18
+ const literal = (node) => {
19
+ if (!node || typeof node !== 'object')
20
+ return;
21
+ const n = node;
22
+ return n['type'] === 'Literal' && typeof n['value'] === 'string' ? n['value'] : undefined;
23
+ };
24
+ const visit = (node) => {
25
+ if (!node || typeof node !== 'object')
26
+ return;
27
+ if (Array.isArray(node)) {
28
+ for (const child of node)
29
+ visit(child);
30
+ return;
31
+ }
32
+ const n = node;
33
+ let specifier;
34
+ if (n['type'] === 'ImportDeclaration' || n['type'] === 'ExportNamedDeclaration' || n['type'] === 'ExportAllDeclaration' || n['type'] === 'ImportExpression')
35
+ specifier = literal(n['source']);
36
+ if (n['type'] === 'CallExpression') {
37
+ const callee = n['callee'];
38
+ if (callee?.['type'] === 'Identifier' && callee['name'] === 'require')
39
+ specifier = literal(n['arguments'][0]);
40
+ }
41
+ if (specifier !== undefined)
42
+ found.add(specifier);
43
+ for (const child of Object.values(n))
44
+ visit(child);
45
+ };
46
+ visit(tree);
47
+ return [...found];
48
+ }
@@ -1,3 +1,4 @@
1
+ import { actCosts } from "./act-cost.js";
1
2
  import { boundedProcess } from "./process-lifetime.js";
2
3
  import { spawnSync } from 'node:child_process';
3
4
  import { PROTOCOL } from '@lapxo/topos/wire';
@@ -33,23 +34,25 @@ export function validResponse(value, request) {
33
34
  }
34
35
  /** Last-line JSON is the wire framing. No partial batch, failed exit or synthetic missing response is accepted. */
35
36
  export function executeCapsule(command, args, requests, limits = {}, lifetime) {
36
- const run = lifetime === undefined ? spawnSync(command, args, { input: JSON.stringify(requests), encoding: 'utf8', maxBuffer: 1 << 26, ...limits }) : boundedProcess(command, args, JSON.stringify(requests), lifetime);
37
- if ('error' in run && run.error)
38
- throw new CapsuleProcessError(`failed: ${run.error.message}`, run);
39
- if (run.signal !== null || run.status !== 0)
40
- throw new CapsuleProcessError(`exited ${run.status ?? `with signal ${run.signal}`}`, run);
41
- const last = /(?:^|\n)([^\n]*)$/.exec(run.stdout.trimEnd())?.[1] ?? '';
42
- let values;
43
- try {
44
- values = JSON.parse(last);
45
- }
46
- catch (error) {
47
- throw new CapsuleProcessError('returned malformed or absent JSON', { ...run, error });
48
- }
49
- if (!Array.isArray(values) || values.length !== requests.length || !values.every((value, i) => validResponse(value, requests[i]))) {
50
- throw new CapsuleProcessError(`returned an invalid batch; expected ${requests.length} responses`, run);
51
- }
52
- if (run.stderr)
53
- process.stderr.write(run.stderr);
54
- return values;
37
+ return actCosts.measure(() => {
38
+ const run = lifetime === undefined ? spawnSync(command, args, { input: JSON.stringify(requests), encoding: 'utf8', maxBuffer: 1 << 26, ...limits }) : boundedProcess(command, args, JSON.stringify(requests), lifetime);
39
+ if ('error' in run && run.error)
40
+ throw new CapsuleProcessError(`failed: ${run.error.message}`, run);
41
+ if (run.signal !== null || run.status !== 0)
42
+ throw new CapsuleProcessError(`exited ${run.status ?? `with signal ${run.signal}`}`, run);
43
+ const last = /(?:^|\n)([^\n]*)$/.exec(run.stdout.trimEnd())?.[1] ?? '';
44
+ let values;
45
+ try {
46
+ values = JSON.parse(last);
47
+ }
48
+ catch (error) {
49
+ throw new CapsuleProcessError('returned malformed or absent JSON', { ...run, error });
50
+ }
51
+ if (!Array.isArray(values) || values.length !== requests.length || !values.every((value, i) => validResponse(value, requests[i]))) {
52
+ throw new CapsuleProcessError(`returned an invalid batch; expected ${requests.length} responses`, run);
53
+ }
54
+ if (run.stderr)
55
+ process.stderr.write(run.stderr);
56
+ return values;
57
+ });
55
58
  }