@lapxo/bound 0.1.3 → 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 (70) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +26 -5
  3. package/TARGET.bound +101 -22
  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 +43 -3
  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 +197 -174
  14. package/dist/fold/readers.d.ts +2 -0
  15. package/dist/fold/readers.js +3 -3
  16. package/dist/fold/resolved.js +6 -2
  17. package/dist/host/act-cost.d.ts +13 -0
  18. package/dist/host/act-cost.js +25 -0
  19. package/dist/host/adapters/modules/javascript.d.ts +1 -0
  20. package/dist/host/adapters/modules/javascript.js +48 -0
  21. package/dist/host/adapters/transport/http.d.ts +5 -0
  22. package/dist/host/adapters/transport/http.js +28 -0
  23. package/dist/host/capsule-process.d.ts +2 -1
  24. package/dist/host/capsule-process.js +24 -20
  25. package/dist/host/capsule.d.ts +1 -0
  26. package/dist/host/capsule.js +13 -16
  27. package/dist/host/cell-receipts.js +1 -1
  28. package/dist/host/content-store.d.ts +8 -0
  29. package/dist/host/content-store.js +31 -0
  30. package/dist/host/lifetime.d.ts +1 -0
  31. package/dist/host/lifetime.js +40 -0
  32. package/dist/host/objects.js +1 -1
  33. package/dist/host/ports/ingress.d.ts +24 -0
  34. package/dist/host/ports/ingress.js +81 -0
  35. package/dist/host/ports/transport.d.ts +1 -0
  36. package/dist/host/ports/transport.js +48 -0
  37. package/dist/host/process-lifetime.d.ts +11 -0
  38. package/dist/host/process-lifetime.js +12 -0
  39. package/dist/host/reading-closures.d.ts +28 -0
  40. package/dist/host/reading-closures.js +146 -0
  41. package/dist/host/release.js +24 -2
  42. package/dist/host/selected-capsules.js +38 -12
  43. package/dist/host/selected-topos.d.ts +4 -4
  44. package/dist/host/selected-topos.js +8 -30
  45. package/dist/host/source-content.d.ts +1 -1
  46. package/dist/host/source-content.js +15 -15
  47. package/dist/host/walk.d.ts +6 -0
  48. package/dist/host/walk.js +193 -0
  49. package/dist/observe/identity.d.ts +2 -0
  50. package/dist/observe/identity.js +3 -3
  51. package/dist/observe/regions.js +3 -5
  52. package/dist/observe/run.d.ts +2 -1
  53. package/dist/observe/run.js +5 -3
  54. package/dist/render/field.d.ts +1 -7
  55. package/dist/render/field.js +6 -12
  56. package/dist/render/names.d.ts +1 -1
  57. package/dist/render/names.js +1 -1
  58. package/dist/render/receipts.js +2 -1
  59. package/dist/render/view.js +16 -11
  60. package/docs/contracts/archive.md +11 -0
  61. package/docs/contracts/cli.md +11 -0
  62. package/docs/contracts/documents.md +35 -0
  63. package/docs/contracts/evidence-view.md +6 -0
  64. package/docs/contracts/publication.md +17 -0
  65. package/docs/contracts/typed-object.md +27 -0
  66. package/docs/contracts/view-walk.md +7 -0
  67. package/docs/contracts/views.md +29 -0
  68. package/docs/usage.md +6 -0
  69. package/package.json +6 -4
  70. package/receipts.bound +314 -94
@@ -1,3 +1,6 @@
1
+ import { actCosts } from "../host/act-cost.js";
2
+ import { readingClosures } from "../host/reading-closures.js";
3
+ import { readerLifetime, readerAllowsEmpty, assertReaderInputs, parse as parseLifetime } from '@lapxo/topos/wire';
1
4
  import { readOnly } from "../host/read-only.js";
2
5
  import { basename, dirname, existsSync, mkdirSync, resolve, rmSync, spawn, statSync, writeFileSync } from "../host/io.js";
3
6
  import { bundleOf, processOf } from "../observe/runner.js";
@@ -264,202 +267,222 @@ export function observedClaims(root, store, options = {}) {
264
267
  }
265
268
  function observeOne(reader, started) {
266
269
  const module = resolve(root, reader.module);
267
- const asked = reader.capsule === undefined ? undefined : capsuleAt(reader.capsule.digest, reader.capsule.entry);
268
- const source = reader.capsule === undefined ? observeFile(module) : asked === undefined ? undefined : new Uint8Array();
269
- const bundle = source && reader.capsule === undefined ? bundleOf(module) ?? bytesDigest(store, source) : 'none';
270
- const offered = offersOf(root, store, reader, roleOf, look, (where) => reached(trimmed(where)));
271
- const vectors = reader.capsule === undefined ? observeText(resolve(root, vectorsOf(reader.module, standing))) : undefined;
272
- const kept = whole && legacy === undefined && answered === undefined && source ? sha([
273
- speaker, reader.module, reader.kind, reader.shape.join('|'), reader.where.join('|'), bundle, vectors === undefined ? 'no vectors' : sha(vectors),
274
- reader.capsule === undefined ? '' : `${reader.capsule.digest} ${reader.capsule.region} ${reader.capsule.reads.join('|')} ${[...reader.capsule.globs].map(([place, globs]) => `${place}${globs.join('|')}`).join(' ')}`,
275
- ...offered.map((offer) => `${offer.place} ${offer.digest?.() ?? bytesDigest(store, offer.bytes())}`),
276
- ].join('\n')) : undefined;
277
- const stored = kept === undefined ? undefined : observeText(memoAt(store, `observed/${kept}`));
278
- if (stored !== undefined) {
279
- out.push(...stored.split('\n').filter(isWire));
280
- read += offered.length;
281
- whole_ += 1;
270
+ const asked = reader.capsule === undefined ? undefined : reader.capsule.selected ?? capsuleAt(reader.capsule.digest, reader.capsule.entry);
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;
282
280
  return false;
283
281
  }
284
- const mark = out.length;
285
- const home = regionIn(regions, reader.module);
286
- const own = shardLines(store, speaker, home).filter((line) => fieldOf(line, 'scope') === `reader/${reader.module}`);
287
- const lineage = own.filter((line) => fieldOf(line, 'measure') === 'identity').map((line) => fieldOf(line, 'value'));
288
- const sum = source && reader.capsule === undefined ? bytesDigest(store, new TextEncoder().encode(`${reader.module} ${bundle}`)).replace(/^sha256:/, '').slice(0, 12) : 'none';
289
- const measured = reader.capsule !== undefined ? { by: source ? sha(`${reader.capsule.digest}\n${reader.capsule.region}`).slice(0, 12) : 'none', lines: [] }
290
- : source ? identityOf({ store, root, speaker, module: reader.module, kind: reader.kind, code: sum, lineage, kept: own.filter((line) => /(?:^|\s)measure=(?:extension|emits)\s/.test(line)) })
291
- : { by: 'none', lines: [] };
292
- const by = measured.by;
293
- const level = [...measured.lines];
294
- const identified = Date.now();
295
- const lines = [...lineage, by];
296
- const before = [...lineage].reverse().find((one) => one !== by);
297
- const regionsOf = new Map();
298
- for (const offer of offered) {
299
- const region = regionIn(regions, offer.place);
300
- if (reached(region))
301
- regionsOf.set(region, [...(regionsOf.get(region) ?? []), offer]);
302
- }
303
- const planned = [...regionsOf].map(([region, offers]) => {
304
- const shard = open(region);
305
- const places = offers.map((offer) => offer.place);
306
- const pairs = [];
307
- const kept = [];
308
- const moved = [];
309
- const all = [];
310
- for (const offer of offers) {
311
- const known = shard.places.get(`${by} ${offer.place}`);
312
- if (known && fieldOf(known.line, 'value') === offer.seen) {
313
- pairs.push(`${offer.place} ${known.at}`);
314
- kept.push(known.line);
315
- all.push({ ...offer, digest: known.at.slice('place:'.length) });
316
- read += 1;
317
- 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);
318
356
  }
319
- const bytes = offer.digest ? undefined : offer.bytes();
320
- const digest = offer.digest?.() ?? bytesDigest(store, bytes);
321
- pairs.push(`${offer.place} place:${digest}`);
322
- kept.push(canonical({ scope: offer.place, role: 'writes', form: 'alphabet', measure: 'observed', value: offer.seen, by: `${speaker}:${by}`, at: `place:${digest}` }));
323
- all.push({ ...offer, digest, bytes: () => bytes ?? offer.bytes() });
324
- if (known?.at === `place:${digest}`)
325
- read += 1;
326
- else
327
- 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;
328
360
  }
329
- return { region, shard, places, pairs, kept, moved, all, key: keyOf(by, region, pairs) };
330
- });
331
- if (answered !== undefined && planned.every((plan) => !plan.moved.length && observedAt(store, plan.key))) {
332
- for (const plan of planned) {
333
- for (const [at, digest] of digestsAt(plan.key))
334
- answered.set(`${by} ${at}`, digest);
335
- if (plan.shard.keys.get(by) !== plan.key || plan.kept.some((line) => plan.shard.places.get(`${by} ${fieldOf(line, 'scope')}`)?.line !== line)) {
336
- 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')]);
337
379
  }
338
- 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;
339
385
  }
340
- if (level.length)
341
- landShard(store, speaker, home, level);
342
- return true;
343
- }
344
- const plans = planned.map(({ region, shard, places, pairs, kept, moved, all, key }) => {
345
- const prior = lastOf(shard, by, places);
346
- if (before !== undefined)
347
- lastOf(shard, before, places);
348
- const ats = [...new Set(pairs.map((pair) => pair.slice(pair.indexOf(' ') + 1)))];
349
- const found = byAt(key);
350
- const said = found !== undefined ? ats.flatMap((at) => found.get(at) ?? [])
351
- : moved.length || prior === undefined ? undefined : ats.flatMap((at) => prior.get(at) ?? []);
352
- const due = said === undefined && !moved.length ? all.map((one) => ({ place: one.place, seen: one.seen, digest: one.digest, bytes: one.bytes() })) : moved;
353
- return { region, shard, places, pairs, kept, due, key, said, prior, found: found !== undefined };
354
- });
355
- const due = plans.flatMap((plan) => (plan.said === undefined ? plan.due.map((one) => ({ ...one, region: plan.region })) : []));
356
- const heldOf = (place, region) => {
357
- for (const one of [...lines].reverse()) {
358
- const shard = open(region);
359
- const mine = servedAt(shard, one, place, lastOf(shard, one, [place])).slice(1);
360
- if (mine.length)
361
- 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;
362
413
  }
363
- return [];
364
- };
365
- if (due.length && !source) {
366
- refused.push(`REFUSE·reader ${reader.module} unavailable · ${due.length} readings remain due`);
367
- return false;
368
- }
369
- if (due.length && !wait) {
370
- for (const plan of plans) {
371
- if (plan.said !== undefined) {
372
- out.push(...plan.kept, ...plan.said);
373
- 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));
374
428
  }
375
- for (const place of plan.places) {
376
- const mine = servedAt(plan.shard, by, place, plan.prior);
377
- 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;
378
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 });
379
459
  }
380
- const marker = storeAt(store, 'pending', by);
381
- const pid = Number((observeText(marker) ?? '').split('\n')[1]);
382
- const running = existsSync(marker) && Date.now() - statSync(marker).mtimeMs < 30 * 60 * 1000 && Number.isInteger(pid) && pid > 0 && alive(pid);
383
- if (!running) {
384
- mkdirSync(dirname(marker), { recursive: true });
385
- const child = spawn(process.execPath, [...childFlags(), processOf('observe'), root, reader.module], { cwd: root, detached: true, stdio: 'ignore' });
386
- child.unref();
387
- 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);
388
467
  }
389
- behind.push({ module: reader.module, places: due.length });
390
- process.stderr.write(`GREY reading: ${reader.module} · ${due.length} places · runs behind${running ? ', already running' : ''}\n`);
391
468
  if (level.length)
392
469
  landShard(store, speaker, home, level);
393
470
  if (Date.now() - started > 250)
394
- 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')));
395
474
  return false;
396
475
  }
397
- if (due.length) {
398
- const at = Date.now();
399
- let rows;
400
- try {
401
- rows = reader.capsule !== undefined && asked !== undefined
402
- ? asked.ask(due.map((d) => ({
403
- protocol: PROTOCOL, verb: 'read', rootScope: d.place, region: reader.capsule.region, held: heldOf(d.place, d.region),
404
- 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)) ?? '' })),
405
- }))).map((answer) => {
406
- if (answer?.kind !== 'fact')
407
- throw new Error(`REFUSE·reader ${reader.module} · ${answer?.why ?? 'no capsule answer'}`);
408
- return (answer.claims ?? []);
409
- })
410
- : runReader(module, root, due.map((d) => ({ place: d.place, ...(reader.kind === 'js' ? { text: new TextDecoder().decode(d.bytes) } : {}), held: heldOf(d.place, d.region) })));
411
- }
412
- catch (error) {
413
- const why = `REFUSE·reader ${reader.module} · ${error instanceof Error ? error.message : String(error)}`;
414
- refused.push(why);
415
- process.stderr.write(`${why}\n`);
416
- return false;
417
- }
418
- if (rows.every((mine) => !mine.length)) {
419
- const why = `REFUSE·reader ${reader.module} · answered none of ${due.length} places · nothing landed, they stay due`;
420
- refused.push(why);
421
- process.stderr.write(`${why}\n`);
422
- return false;
423
- }
424
- ran += due.length;
425
- process.stderr.write(`READER ${reader.module} · ${due.length} run · ${Date.now() - at} ms\n`);
426
- const fresh = new Map();
427
- due.forEach((d, i) => fresh.set(d.place, (rows[i] ?? []).map((row) => lineOf(row, d.digest, by, speaker))));
428
- for (const plan of plans.filter((one) => one.said === undefined)) {
429
- const mine = new Set(plan.due.map((d) => d.place));
430
- const stay = [...new Set(plan.pairs.filter((pair) => !mine.has(pair.slice(0, pair.indexOf(' ')))).map((pair) => pair.slice(pair.indexOf(' ') + 1)))];
431
- const said = [...stay.flatMap((one) => plan.prior?.get(one) ?? []), ...[...mine].flatMap((place) => fresh.get(place) ?? [])];
432
- keepObservation(store, plan.key, said);
433
- landShard(store, speaker, plan.region, [...plan.kept, pointer(speaker, by, plan.region, plan.key, plan.pairs)]);
434
- out.push(...plan.kept, ...said);
435
- }
436
- const spent = Date.now() - at;
437
- const over = bytesDigest(store, new TextEncoder().encode(due.map((d) => d.digest).sort().join('\n'))).replace(/^sha256:/, '');
438
- level.push(canonical({ scope: `cost/reader/${reader.module}`, role: 'writes', form: 'interval', measure: 'ms', value: `${spent}..${spent}`, by: `${speaker}:${by}`, at: `place:${over}` }));
439
- if (lineage.at(-1) !== by)
440
- level.push(canonical({ scope: `reader/${reader.module}`, role: 'writes', form: 'alphabet', measure: 'identity', value: by, by: `${speaker}:${by}`, at: `place:${by}` }));
441
- rmSync(storeAt(store, 'pending', by), { force: true });
442
- }
443
- for (const plan of plans.filter((one) => one.said !== undefined)) {
444
- if (!plan.found)
445
- keepObservation(store, plan.key, plan.said);
446
- if (plan.shard.keys.get(by) !== plan.key || plan.kept.some((line) => plan.shard.places.get(`${by} ${fieldOf(line, 'scope')}`)?.line !== line)) {
447
- landShard(store, speaker, plan.region, [...plan.kept, pointer(speaker, by, plan.region, plan.key, plan.pairs)]);
448
- }
449
- out.push(...plan.kept, ...plan.said);
476
+ finally {
477
+ endAct();
450
478
  }
451
- if (level.length)
452
- landShard(store, speaker, home, level);
453
- if (Date.now() - started > 250)
454
- spent.push(`${reader.module.split('/').pop()} ${identified - started}+${Date.now() - identified}`);
455
- if (kept !== undefined)
456
- landMemo(store, `observed/${kept}`, new TextEncoder().encode(out.slice(mark).join('\n')));
457
- return false;
458
479
  }
459
480
  if (spent.length)
460
481
  process.stderr.write(`SPENT observe ${spent.join(' · ')} ms\n`);
461
482
  if (legacy)
462
483
  setAside(store, speaker);
463
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'));
464
487
  return [...new Set(out)].sort();
465
488
  }
@@ -1,3 +1,4 @@
1
+ import type { Capsule } from '../host/capsule.ts';
1
2
  import type { CoordinateRole } from './wire.ts';
2
3
  import { capsulesFor } from './reach.ts';
3
4
  import type { Kind } from '../observe/run.ts';
@@ -8,6 +9,7 @@ export interface ReaderClaim {
8
9
  readonly module: string;
9
10
  readonly kind: Kind;
10
11
  readonly capsule?: {
12
+ readonly selected?: Capsule;
11
13
  readonly digest: string;
12
14
  readonly entry: string;
13
15
  readonly region: string;
@@ -62,13 +62,13 @@ export function capsuleReadersOf(root, store, standing, lines, resolve = capsule
62
62
  const pkg = fieldOf(by?.offer ?? '', 'scope').split('/')[1] ?? '';
63
63
  if (by === undefined || reaching.has(`${pkg} ${region} ${where || '/'}`))
64
64
  continue;
65
- const key = `${by.capsule.digest} ${region}`;
66
- const group = groups.get(key) ?? groups.set(key, { module: `${pkg}/${region}`, digest: by.capsule.digest, entry: fieldOf(by.offer, 'shape'), region, reads: by.capsule.declaration.regions[region] ?? [], globs: new Map() }).get(key);
65
+ const key = `${by.capsule.selection ?? by.capsule.digest} ${region}`;
66
+ const group = groups.get(key) ?? groups.set(key, { module: `${pkg}/${region}`, digest: by.capsule.digest, entry: fieldOf(by.offer, 'shape'), selected: by.capsule, region, reads: by.capsule.declaration.regions[region] ?? [], globs: new Map() }).get(key);
67
67
  group.globs.set(where, fieldOf(line, 'value').split('|').filter(Boolean));
68
68
  }
69
69
  }
70
70
  return [...groups.values()].map((group) => ({ id: group.module, shape: ['run'], where: [...group.globs.keys()], module: group.module, kind: 'process',
71
- capsule: { digest: group.digest, entry: group.entry, region: group.region, reads: group.reads, globs: group.globs } }));
71
+ capsule: { selected: group.selected, digest: group.digest, entry: group.entry, region: group.region, reads: group.reads, globs: group.globs } }));
72
72
  }
73
73
  export function handedAt(root, reader, roleOf, look, place) {
74
74
  const { reads, globs } = reader.capsule;
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ /** Reference HTTP adapter. Location and budgets are already selected by admitted host policy. */
2
+ export declare function fetchBytes(location: string, limits: {
3
+ readonly timeoutMs: number;
4
+ readonly responseBytes: number;
5
+ }): Promise<Uint8Array>;
@@ -0,0 +1,28 @@
1
+ /** Reference HTTP adapter. Location and budgets are already selected by admitted host policy. */
2
+ export async function fetchBytes(location, limits) {
3
+ const signal = AbortSignal.timeout(limits.timeoutMs), response = await fetch(location, { signal });
4
+ if (!response.ok)
5
+ throw Error(`HTTP ${response.status}`);
6
+ const reader = response.body?.getReader();
7
+ if (!reader)
8
+ return new Uint8Array();
9
+ const chunks = [];
10
+ let size = 0;
11
+ try {
12
+ for (;;) {
13
+ const part = await reader.read();
14
+ if (part.done)
15
+ break;
16
+ size += part.value.length;
17
+ if (size > limits.responseBytes) {
18
+ await reader.cancel();
19
+ throw Error('response exceeds declared byte bound');
20
+ }
21
+ chunks.push(part.value);
22
+ }
23
+ }
24
+ finally {
25
+ reader.releaseLock();
26
+ }
27
+ return Buffer.concat(chunks);
28
+ }