@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.
- package/LICENSE +0 -1
- package/README.md +26 -5
- package/TARGET.bound +83 -19
- package/dist/cli/accept.js +21 -9
- package/dist/cli/owner.d.ts +2 -0
- package/dist/cli/owner.js +17 -2
- package/dist/cli/pass.js +15 -10
- package/dist/cli/place.js +8 -1
- package/dist/cli/verb.js +35 -1
- package/dist/fold/claims.js +5 -1
- package/dist/fold/closed.js +44 -13
- package/dist/fold/digests.js +5 -2
- package/dist/fold/observed.js +195 -174
- package/dist/fold/resolved.js +6 -2
- package/dist/host/act-cost.d.ts +13 -0
- package/dist/host/act-cost.js +25 -0
- package/dist/host/adapters/modules/javascript.d.ts +1 -0
- package/dist/host/adapters/modules/javascript.js +48 -0
- package/dist/host/capsule-process.js +22 -19
- package/dist/host/capsule.js +5 -6
- package/dist/host/ports/ingress.d.ts +24 -0
- package/dist/host/ports/ingress.js +81 -0
- package/dist/host/reading-closures.d.ts +28 -0
- package/dist/host/reading-closures.js +146 -0
- package/dist/host/release.js +24 -2
- package/dist/host/walk.d.ts +6 -0
- package/dist/host/walk.js +193 -0
- package/dist/observe/regions.js +3 -5
- package/dist/render/field.d.ts +1 -7
- package/dist/render/field.js +6 -12
- package/dist/render/names.d.ts +1 -1
- package/dist/render/names.js +1 -1
- package/dist/render/view.js +16 -11
- package/docs/contracts/archive.md +11 -0
- package/docs/contracts/cli.md +11 -0
- package/docs/contracts/documents.md +35 -0
- package/docs/contracts/evidence-view.md +6 -0
- package/docs/contracts/publication.md +17 -0
- package/docs/contracts/typed-object.md +27 -0
- package/docs/contracts/view-walk.md +7 -0
- package/docs/contracts/views.md +29 -0
- package/docs/usage.md +6 -0
- package/package.json +6 -4
- package/receipts.bound +314 -92
package/dist/fold/observed.js
CHANGED
|
@@ -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
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
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 (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
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
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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()}
|
|
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
|
-
|
|
400
|
-
|
|
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
|
}
|
package/dist/fold/resolved.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
}
|