@prestack/thread 0.1.7 → 0.1.9

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 (34) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/dist/core/attach.js +40 -6
  3. package/dist/core/attach.ts +38 -6
  4. package/dist/core/descend.js +13 -17
  5. package/dist/core/descend.ts +13 -15
  6. package/dist/integrations/attach.js +5 -3
  7. package/dist/integrations/attach.ts +8 -4
  8. package/dist/integrations/cli.d.ts +2 -0
  9. package/dist/integrations/cli.js +55 -11
  10. package/dist/integrations/cli.ts +62 -11
  11. package/dist/integrations/disk.js +4 -3
  12. package/dist/integrations/disk.ts +4 -3
  13. package/dist/integrations/generators/vault.ts +0 -1
  14. package/dist/integrations/target.d.ts +1 -1
  15. package/dist/integrations/target.ts +1 -1
  16. package/dist/integrity/generators/gate.ts +1 -2
  17. package/dist/integrity/generators/publish.ts +0 -1
  18. package/dist/integrity/lint.js +0 -6
  19. package/dist/integrity/lint.ts +0 -10
  20. package/dist/integrity/slopnator.ts +1 -1
  21. package/dist/integrity/slopnators/sl_derived_drift.js +0 -2
  22. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -3
  23. package/dist/integrity/slopnators/sl_unthreaded.js +0 -2
  24. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -5
  25. package/dist/integrity/sweep.ts +0 -2
  26. package/dist/knowledge/generators/docsgen.ts +1 -1
  27. package/dist/operations/generators/capability.js +20 -9
  28. package/dist/operations/generators/capability.ts +19 -12
  29. package/dist/operations/generators/compile.js +41 -28
  30. package/dist/operations/generators/compile.ts +48 -33
  31. package/dist/operations/generators/permits.js +6 -3
  32. package/dist/operations/generators/permits.ts +7 -5
  33. package/dist/operations/generators/trial.ts +1 -1
  34. package/package.json +1 -1
@@ -535,6 +535,7 @@ export function reaching(targets, calls, stores, vaulted, carried) {
535
535
  const file = (one.declares ?? [])[0]?.at;
536
536
  return file === undefined ? [] : [{ from: beside(REACHING_AT, file), as: 'c' + String(at), of: one.id }];
537
537
  });
538
+ const used = new Set();
538
539
  const opened = targets
539
540
  .map((one) => {
540
541
  const reach = reachOf(one.id);
@@ -545,32 +546,43 @@ export function reaching(targets, calls, stores, vaulted, carried) {
545
546
  const each = calls
546
547
  .filter((call) => call.of === one.id && one.takes.includes(call.id))
547
548
  .map((call) => quoted(call.id.replace(/^cl_/, '')) + ': ' + held.as + '[' + quoted(call.path) + ']');
549
+ if (each.length > 0)
550
+ used.add(held.as);
548
551
  return ' ' + one.id + ': { ' + each.join(', ') + ' },';
549
552
  }
550
553
  if (reach === 'disk') {
551
554
  const through = one.requires.find((each) => reachOf(each) !== undefined);
552
- return through === undefined ? ' ' + one.id + ': HERE,' : '';
555
+ if (through !== undefined)
556
+ return '';
557
+ used.add('HERE');
558
+ return ' ' + one.id + ': HERE,';
553
559
  }
554
560
  if (reach === 'cli') {
555
561
  const each = calls
556
562
  .filter((call) => call.of === one.id && one.takes.includes(call.id))
557
563
  .map((call) => quoted(call.id.replace(/^cl_/, '')) + ': ' + quoted(call.path));
564
+ used.add('running');
558
565
  return ' ' + one.id + ': running(' + quoted(one.id.replace(/^cli_/, '')) + ', { ' + each.join(', ') + ' }, process.cwd()),';
559
566
  }
560
- if (reach === 'https')
567
+ if (reach === 'https') {
568
+ used.add('sendingHttp');
561
569
  return ' ' + one.id + ': sendingHttp(),';
570
+ }
562
571
  const held = named.find((each) => each.of === one.id);
563
- return held === undefined ? '' : ' ' + one.id + ': ' + held.as + ',';
572
+ if (held === undefined)
573
+ return '';
574
+ used.add(held.as);
575
+ return ' ' + one.id + ': ' + held.as + ',';
564
576
  })
565
577
  .filter((each) => each !== '');
566
578
  return [
567
- 'import { HERE } from ' + quoted(runtime(REACHING_AT, '../../integrations/disk.ts')),
568
- 'import { running } from ' + quoted(runtime(REACHING_AT, '../../integrations/cli.ts')),
569
- 'import { sendingHttp } from ' + quoted(runtime(REACHING_AT, '../../integrations/https.ts')),
579
+ ...(used.has('HERE') ? ['import { HERE } from ' + quoted(runtime(REACHING_AT, '../../integrations/disk.ts'))] : []),
580
+ ...(used.has('running') ? ['import { running } from ' + quoted(runtime(REACHING_AT, '../../integrations/cli.ts'))] : []),
581
+ ...(used.has('sendingHttp') ? ['import { sendingHttp } from ' + quoted(runtime(REACHING_AT, '../../integrations/https.ts'))] : []),
570
582
  'import type { Counting } from ' +
571
583
  quoted(carried ? packageOf('operations') + '/operations/operations' : runtime(REACHING_AT, '../operations.ts')),
572
- ...named.map((each) => 'import { store as ' + each.as + ' } from ' + quoted(each.from)),
573
- ...adapted.map((each) => 'import { calls as ' + each.as + ' } from ' + quoted(each.from)),
584
+ ...named.filter((each) => used.has(each.as)).map((each) => 'import { store as ' + each.as + ' } from ' + quoted(each.from)),
585
+ ...adapted.filter((each) => used.has(each.as)).map((each) => 'import { calls as ' + each.as + ' } from ' + quoted(each.from)),
574
586
  ...(vaulted ? ['import { vault } from ' + quoted(beside(REACHING_AT, VAULT_AT))] : []),
575
587
  '',
576
588
  'declare module ' +
@@ -807,7 +819,6 @@ if (invoked(import.meta.url)) {
807
819
  }
808
820
  const checking = argv.includes('--check');
809
821
  let adrift = 0;
810
- const keeping = existsSync(resolve(ROOT, ENTITIES_AT));
811
822
  if (found.length === 0 && refusals.length === 0) {
812
823
  console.log('capabilities: this app declares no operation, so nothing is reachable through one yet.');
813
824
  exit(adrift > 0 ? 1 : 0);
@@ -11,7 +11,7 @@ import { named as packageOf } from '../../core/scope.ts'
11
11
  import { PREFIXES } from '../../core/thread.ts'
12
12
 
13
13
  import { secrecyOf, type Secret } from '../../integrations/secret.ts'
14
- import { reachOf, servesBrowser, standsHere, targetOf, type Call, type Target, type Webhook } from '../../integrations/target.ts'
14
+ import { reachOf, servesBrowser, targetOf, type Call, type Target, type Webhook } from '../../integrations/target.ts'
15
15
 
16
16
  import {
17
17
  CAPABILITIES_AT,
@@ -33,7 +33,6 @@ import {
33
33
  type Surface,
34
34
  } from '../capabilities.ts'
35
35
  import { ANONYMOUS, ENVIRONMENT } from '../../core/actor.ts'
36
- import { PERMITS_AT } from '../access.ts'
37
36
  import { DISPATCH_AT } from '../dispatch.ts'
38
37
  import { STREAMED, TOLD } from '../../integrations/https.ts'
39
38
  import { holdsRow, listBounds, marked, nestedBounds, rowsIn, valuesIn, withField, withRow, withTopField, withoutRow } from '../../core/rows.ts'
@@ -642,6 +641,7 @@ export function reaching(
642
641
  return file === undefined ? [] : [{ from: beside(REACHING_AT, file), as: 'c' + String(at), of: one.id }]
643
642
  })
644
643
 
644
+ const used = new Set<string>()
645
645
  const opened = targets
646
646
  .map((one) => {
647
647
  const reach = reachOf(one.id)
@@ -651,32 +651,41 @@ export function reaching(
651
651
  const each = calls
652
652
  .filter((call) => call.of === one.id && one.takes.includes(call.id))
653
653
  .map((call) => quoted(call.id.replace(/^cl_/, '')) + ': ' + held.as + '[' + quoted(call.path) + ']')
654
+ if (each.length > 0) used.add(held.as)
654
655
  return ' ' + one.id + ': { ' + each.join(', ') + ' },'
655
656
  }
656
657
  if (reach === 'disk') {
657
658
  const through = one.requires.find((each) => reachOf(each) !== undefined)
658
- return through === undefined ? ' ' + one.id + ': HERE,' : ''
659
+ if (through !== undefined) return ''
660
+ used.add('HERE')
661
+ return ' ' + one.id + ': HERE,'
659
662
  }
660
663
  if (reach === 'cli') {
661
664
  const each = calls
662
665
  .filter((call) => call.of === one.id && one.takes.includes(call.id))
663
666
  .map((call) => quoted(call.id.replace(/^cl_/, '')) + ': ' + quoted(call.path))
667
+ used.add('running')
664
668
  return ' ' + one.id + ': running(' + quoted(one.id.replace(/^cli_/, '')) + ', { ' + each.join(', ') + ' }, process.cwd()),'
665
669
  }
666
- if (reach === 'https') return ' ' + one.id + ': sendingHttp(),'
670
+ if (reach === 'https') {
671
+ used.add('sendingHttp')
672
+ return ' ' + one.id + ': sendingHttp(),'
673
+ }
667
674
  const held = named.find((each) => each.of === one.id)
668
- return held === undefined ? '' : ' ' + one.id + ': ' + held.as + ','
675
+ if (held === undefined) return ''
676
+ used.add(held.as)
677
+ return ' ' + one.id + ': ' + held.as + ','
669
678
  })
670
679
  .filter((each) => each !== '')
671
680
 
672
681
  return [
673
- 'import { HERE } from ' + quoted(runtime(REACHING_AT, '../../integrations/disk.ts')),
674
- 'import { running } from ' + quoted(runtime(REACHING_AT, '../../integrations/cli.ts')),
675
- 'import { sendingHttp } from ' + quoted(runtime(REACHING_AT, '../../integrations/https.ts')),
682
+ ...(used.has('HERE') ? ['import { HERE } from ' + quoted(runtime(REACHING_AT, '../../integrations/disk.ts'))] : []),
683
+ ...(used.has('running') ? ['import { running } from ' + quoted(runtime(REACHING_AT, '../../integrations/cli.ts'))] : []),
684
+ ...(used.has('sendingHttp') ? ['import { sendingHttp } from ' + quoted(runtime(REACHING_AT, '../../integrations/https.ts'))] : []),
676
685
  'import type { Counting } from ' +
677
686
  quoted(carried ? packageOf('operations') + '/operations/operations' : runtime(REACHING_AT, '../operations.ts')),
678
- ...named.map((each) => 'import { store as ' + each.as + ' } from ' + quoted(each.from)),
679
- ...adapted.map((each) => 'import { calls as ' + each.as + ' } from ' + quoted(each.from)),
687
+ ...named.filter((each) => used.has(each.as)).map((each) => 'import { store as ' + each.as + ' } from ' + quoted(each.from)),
688
+ ...adapted.filter((each) => used.has(each.as)).map((each) => 'import { calls as ' + each.as + ' } from ' + quoted(each.from)),
680
689
  ...(vaulted ? ['import { vault } from ' + quoted(beside(REACHING_AT, VAULT_AT))] : []),
681
690
  '',
682
691
  'declare module ' +
@@ -951,8 +960,6 @@ if (invoked(import.meta.url)) {
951
960
  const checking = argv.includes('--check')
952
961
  let adrift = 0
953
962
 
954
- const keeping = existsSync(resolve(ROOT, ENTITIES_AT))
955
-
956
963
  if (found.length === 0 && refusals.length === 0) {
957
964
  console.log('capabilities: this app declares no operation, so nothing is reachable through one yet.')
958
965
  exit(adrift > 0 ? 1 : 0)
@@ -152,7 +152,8 @@ function answers(node, found) {
152
152
  answers(one, found);
153
153
  });
154
154
  }
155
- function spliced(text, names, mine, roles) {
155
+ function spliced(text, shape, roles) {
156
+ const { names, mine } = shape;
156
157
  const read = arrowIn(text, roles);
157
158
  if (typeof read === 'string')
158
159
  return read;
@@ -190,6 +191,7 @@ function spliced(text, names, mine, roles) {
190
191
  return;
191
192
  }
192
193
  edits.push({ from: up.getStart(), to: up.getEnd(), text: said });
194
+ shape.heard.add(said);
193
195
  return;
194
196
  }
195
197
  because = `reaches for \`${read.held}\` itself rather than one thing on it, and once every node is a local there is no such thing to hand it`;
@@ -232,19 +234,20 @@ function spliced(text, names, mine, roles) {
232
234
  return { form: 'call', text: whole, whole };
233
235
  }
234
236
  const ANSWERS = `${OWN}ANSWER`;
235
- function answered(name, one, opened, kind) {
237
+ const linesOf = (one, name, unread) => (unread ? `${one.text.replace(`${ANSWERS} = `, 'void (')})` : one.text.replace(ANSWERS, name)).split('\n');
238
+ function answered(name, one, opened, kind, unread) {
236
239
  if (one.form === 'expression')
237
- return [`${kind} ${name}: unknown = ${one.text}`];
240
+ return [unread ? `void (${one.text})` : `${kind} ${name}: unknown = ${one.text}`];
238
241
  if (one.form === 'statements') {
239
- return [`let ${name}: unknown`, '{', ...one.text.split('\n').map((line) => ` ${line.replace(ANSWERS, name)}`), '}'];
242
+ return [...(unread ? [] : [`let ${name}: unknown`]), '{', ...linesOf(one, name, unread).map((line) => ` ${line}`), '}'];
240
243
  }
241
- return [`${kind} ${name}: unknown = await ${asFn(one.whole, opened)}(opened)`];
244
+ return [`${unread ? '' : `${kind} ${name}: unknown = `}await ${asFn(one.whole, opened)}(opened)`];
242
245
  }
243
246
  const asFn = (text, opened, more = '') => `((${text}) as (${more}opened: ${opened}) => Promise<unknown>)`;
244
247
  function forked(name, said, shape) {
245
248
  if (said.length !== 2)
246
249
  return `takes a chooser and the ways it chooses between, and was handed ${String(said.length)} thing(s)`;
247
- const chose = spliced(said[0] ?? '', shape.names, shape.mine, ROLES.block);
250
+ const chose = spliced(said[0] ?? '', shape, ROLES.block);
248
251
  if (typeof chose === 'string')
249
252
  return `chooses with a block that ${chose}`;
250
253
  const held = inside(said[1] ?? '', '{', '}');
@@ -252,25 +255,26 @@ function forked(name, said, shape) {
252
255
  const named = ways?.every((one) => one !== undefined && PLAIN.test(one.key)) === true ? ways : undefined;
253
256
  if (named === undefined || named.length === 0)
254
257
  return 'names its ways in a way this cannot read — write each one as `name: block`';
258
+ const unread = shape.unread.has(name);
255
259
  const picked = `${OWN}${name}_said`;
256
260
  const runs = [
257
- `let ${name}: unknown`,
261
+ ...(unread ? [] : [`let ${name}: unknown`]),
258
262
  chose.form === 'expression'
259
263
  ? `const ${picked}: string = await (${chose.text})`
260
264
  : `const ${picked}: string = await ((${said[0] ?? ''}) as (opened: ${shape.reads}) => Promise<string>)(opened)`,
261
265
  `switch (${picked}) {`,
262
266
  ];
263
267
  for (const way of named) {
264
- const each = spliced(way?.held ?? '', shape.names, shape.mine, ROLES.block);
268
+ const each = spliced(way?.held ?? '', shape, ROLES.block);
265
269
  if (typeof each === 'string')
266
270
  return `has a way \`${way?.key ?? ''}\` that ${each}`;
267
271
  runs.push(` case ${(way?.key ?? '').startsWith("'") ? (way?.key ?? '') : quoted(way?.key ?? '')}: {`);
268
272
  if (each.form === 'expression')
269
- runs.push(` ${name} = ${each.text}`);
273
+ runs.push(unread ? ` void (${each.text})` : ` ${name} = ${each.text}`);
270
274
  else if (each.form === 'statements')
271
- runs.push(...each.text.split('\n').map((line) => ` ${line.replace(ANSWERS, name)}`));
275
+ runs.push(...linesOf(each, name, unread).map((line) => ` ${line}`));
272
276
  else
273
- runs.push(` ${name} = await ${asFn(each.whole, shape.all)}(opened)`);
277
+ runs.push(` ${unread ? '' : `${name} = `}await ${asFn(each.whole, shape.all)}(opened)`);
274
278
  runs.push(' break');
275
279
  runs.push(' }');
276
280
  }
@@ -285,23 +289,24 @@ const SHAPES = {
285
289
  nd_each: (name, said, shape) => {
286
290
  if (said.length !== 2)
287
291
  return `takes what to run over and the block to run, and was handed ${String(said.length)} thing(s)`;
288
- const over = spliced(said[0] ?? '', shape.names, shape.mine, ROLES.over);
289
- const block = spliced(said[1] ?? '', shape.names, shape.mine, ROLES.item);
292
+ const over = spliced(said[0] ?? '', shape, ROLES.over);
293
+ const block = spliced(said[1] ?? '', shape, ROLES.item);
290
294
  if (typeof block === 'string')
291
295
  return `runs a block that ${block}`;
292
296
  if (typeof over === 'string')
293
297
  return `runs over a block that ${over}`;
298
+ const unread = shape.unread.has(name);
294
299
  const item = `${OWN}${name}_item`;
295
300
  const all = `${OWN}${name}_all`;
296
301
  const each = `${OWN}${name}_each`;
297
302
  return {
298
303
  runs: [
299
- `const ${all}: unknown[] = []`,
304
+ ...(unread ? [] : [`const ${all}: unknown[] = []`]),
300
305
  `const ${each} = (${block.whole}) as (item: unknown, opened: ${shape.all}) => Promise<unknown>`,
301
306
  `for (const ${item} of ${over.form === 'expression' ? `(${over.text})` : `((${over.whole}) as () => readonly unknown[])()`} as readonly unknown[]) {`,
302
- ` ${all}.push(await ${each}(${item}, opened))`,
307
+ unread ? ` await ${each}(${item}, opened)` : ` ${all}.push(await ${each}(${item}, opened))`,
303
308
  '}',
304
- `const ${name}: unknown = ${all}`,
309
+ ...(unread ? [] : [`const ${name}: unknown = ${all}`]),
305
310
  ],
306
311
  };
307
312
  },
@@ -316,17 +321,17 @@ const SHAPES = {
316
321
  return 'was handed no blocks to run at once';
317
322
  const calls = [];
318
323
  for (const [order, one] of every.entries()) {
319
- const each = spliced(one, shape.names, shape.mine, ROLES.block);
324
+ const each = spliced(one, shape, ROLES.block);
320
325
  if (typeof each === 'string')
321
326
  return `runs a block at position ${String(order + 1)} that ${each}`;
322
327
  calls.push(each.form === 'expression' ? `(async () => ${each.text})()` : `${asFn(each.whole, shape.all)}(opened)`);
323
328
  }
324
- return { runs: [`const ${name}: unknown = await Promise.all([${calls.join(', ')}])`] };
329
+ return { runs: [`${shape.unread.has(name) ? '' : `const ${name}: unknown = `}await Promise.all([${calls.join(', ')}])`] };
325
330
  },
326
331
  nd_repeat: (name, said, shape) => {
327
332
  if (said.length !== 2)
328
333
  return `takes a ceiling and the block to repeat, and was handed ${String(said.length)} thing(s)`;
329
- const block = spliced(said[1] ?? '', shape.names, shape.mine, ROLES.again);
334
+ const block = spliced(said[1] ?? '', shape, ROLES.again);
330
335
  if (typeof block === 'string')
331
336
  return `repeats a block that ${block}`;
332
337
  const ceiling = `${OWN}${name}_ceiling`;
@@ -335,16 +340,17 @@ const SHAPES = {
335
340
  const done = `${OWN}${name}_done`;
336
341
  const pass = `${OWN}${name}_pass`;
337
342
  const answer = `${OWN}${name}_said`;
343
+ const unread = shape.unread.has(name);
338
344
  return {
339
345
  runs: [
340
- `let ${name}: unknown`,
346
+ ...(unread ? [] : [`let ${name}: unknown`]),
341
347
  `const ${ceiling}: number = ${said[0] ?? ''}`,
342
348
  `const ${again} = (${block.whole}) as (pass: number, carried: unknown, opened: ${shape.all}) => Promise<{ readonly done: boolean; readonly value: unknown }>`,
343
349
  `let ${carried}: unknown`,
344
350
  `let ${done} = false`,
345
351
  `for (let ${pass} = 0; ${pass} < ${ceiling}; ${pass} += 1) {`,
346
352
  ` const ${answer} = await ${again}(${pass}, ${carried}, opened)`,
347
- ` if (${answer}.done) { ${name} = ${answer}.value; ${done} = true; break }`,
353
+ ` if (${answer}.done) { ${unread ? '' : `${name} = ${answer}.value; `}${done} = true; break }`,
348
354
  ` ${carried} = ${answer}.value`,
349
355
  '}',
350
356
  `if (!${done}) throw new Error(\`${name} reached its ceiling of \${String(${ceiling})} passes without answering done\`)`,
@@ -357,21 +363,22 @@ const STOPS = "Symbol.for('thread.operation.stopped')";
357
363
  function plainly(name, said, shape, opened, ends) {
358
364
  if (said.length !== 1)
359
365
  return `takes one block, and was handed ${String(said.length)} thing(s)`;
360
- const one = spliced(said[0] ?? '', shape.names, shape.mine, ROLES.block);
366
+ const one = spliced(said[0] ?? '', shape, ROLES.block);
361
367
  if (typeof one === 'string')
362
368
  return one;
369
+ const unread = shape.unread.has(name);
363
370
  if (!ends)
364
- return { runs: answered(name, one, opened, 'const') };
371
+ return { runs: answered(name, one, opened, 'const', unread) };
365
372
  return {
366
373
  runs: [
367
- ...answered(name, one, opened, 'const'),
374
+ ...answered(name, one, opened, 'const', unread),
368
375
  `throw Object.assign(new Error(${quoted(`${name} ended the operation`)}), { [${STOPS}]: true, value: ${name} })`,
369
376
  ],
370
377
  };
371
378
  }
372
379
  const saidIn = (held) => held !== undefined && ts.isStringLiteral(held) ? held.text : undefined;
373
380
  const sourceIn = (held) => held?.getText();
374
- function compiled(at, id, one, known) {
381
+ function compiled(at, id, one, known, unread) {
375
382
  const because = problem(id, one);
376
383
  if (because !== undefined)
377
384
  return { because };
@@ -394,6 +401,7 @@ function compiled(at, id, one, known) {
394
401
  const mine = new Set(['input', ...one.nodes.map((row) => String(row.id))]);
395
402
  const runs = [];
396
403
  const names = new Set();
404
+ const heard = new Set();
397
405
  let last;
398
406
  for (const [order, row] of one.nodes.entries()) {
399
407
  const name = row.id;
@@ -418,7 +426,7 @@ function compiled(at, id, one, known) {
418
426
  if (held === undefined) {
419
427
  return { because: `the node \`${name}\` states no block this can read in ${row.at} — run \`thread seed\` and write it again` };
420
428
  }
421
- const shape = { all: `Opened<${only}>`, reads: `Reads<Opened<${only}>>`, names, mine };
429
+ const shape = { all: `Opened<${only}>`, reads: `Reads<Opened<${only}>>`, names, mine, heard, unread: unread ?? new Set() };
422
430
  const made = (SHAPES[from] ?? ownType)(name, parts(held), shape);
423
431
  if (typeof made === 'string')
424
432
  return { because: `the node \`${name}\` ${made}` };
@@ -428,12 +436,17 @@ function compiled(at, id, one, known) {
428
436
  names.add(name);
429
437
  last = from === 'nd_stop' ? undefined : name;
430
438
  }
439
+ if (unread === undefined) {
440
+ const final = one.nodes.at(-1)?.id;
441
+ return compiled(at, id, one, known, new Set([...mine].filter((name) => name !== final && !heard.has(name))));
442
+ }
443
+ const typed = ['Opened', 'Reads'].filter((word) => runs.some((line) => line.includes(`${word}<`)));
431
444
  return {
432
445
  text: [
433
446
  "import type { z } from 'zod'",
434
447
  '',
435
448
  `import type { Operation } from ${quoted(runtime(at, '../operations.ts'))}`,
436
- 'import type { Opened, Reads } from ' + quoted(runtime(at, '../operations.ts')),
449
+ ...(typed.length === 0 ? [] : [`import type { ${typed.join(', ')} } from ${quoted(runtime(at, '../operations.ts'))}`]),
437
450
  '',
438
451
  `import { input, resource } from ${quoted(beside(at, takes))}`,
439
452
  '',
@@ -448,7 +461,7 @@ function compiled(at, id, one, known) {
448
461
  ...(quota === undefined ? [] : [` quota: ${quota},`]),
449
462
  '',
450
463
  ' run: async (opened, held) => {',
451
- ' const input = held.input',
464
+ ...(unread.has('input') ? [] : [' const input = held.input']),
452
465
  ` let ${ENTERED} = ''`,
453
466
  ' try {',
454
467
  ...runs,
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from '../../integrations/disk.ts'
2
- import { dirname, resolve } from 'node:path'
2
+ import { resolve } from 'node:path'
3
3
  import { argv, stdout } from 'node:process'
4
4
 
5
5
  import ts from 'typescript'
@@ -190,12 +190,8 @@ interface Spliced {
190
190
  readonly whole: string
191
191
  }
192
192
 
193
- function spliced(
194
- text: string,
195
- names: ReadonlySet<string>,
196
- mine: ReadonlySet<string>,
197
- roles: readonly string[],
198
- ): Spliced | string {
193
+ function spliced(text: string, shape: Shape, roles: readonly string[]): Spliced | string {
194
+ const { names, mine } = shape
199
195
  const read = arrowIn(text, roles)
200
196
  if (typeof read === 'string') return read
201
197
 
@@ -243,6 +239,7 @@ function spliced(
243
239
  return
244
240
  }
245
241
  edits.push({ from: up.getStart(), to: up.getEnd(), text: said })
242
+ shape.heard.add(said)
246
243
  return
247
244
  }
248
245
  because = `reaches for \`${read.held}\` itself rather than one thing on it, and once every node is a local there is no such thing to hand it`
@@ -300,14 +297,19 @@ interface Shape {
300
297
  readonly reads: string
301
298
  readonly names: ReadonlySet<string>
302
299
  readonly mine: ReadonlySet<string>
300
+ readonly heard: Set<string>
301
+ readonly unread: ReadonlySet<string>
303
302
  }
304
303
 
305
- function answered(name: string, one: Spliced, opened: string, kind: 'const' | 'let'): readonly string[] {
306
- if (one.form === 'expression') return [`${kind} ${name}: unknown = ${one.text}`]
304
+ const linesOf = (one: Spliced, name: string, unread: boolean): readonly string[] =>
305
+ (unread ? `${one.text.replace(`${ANSWERS} = `, 'void (')})` : one.text.replace(ANSWERS, name)).split('\n')
306
+
307
+ function answered(name: string, one: Spliced, opened: string, kind: 'const' | 'let', unread: boolean): readonly string[] {
308
+ if (one.form === 'expression') return [unread ? `void (${one.text})` : `${kind} ${name}: unknown = ${one.text}`]
307
309
  if (one.form === 'statements') {
308
- return [`let ${name}: unknown`, '{', ...one.text.split('\n').map((line) => ` ${line.replace(ANSWERS, name)}`), '}']
310
+ return [...(unread ? [] : [`let ${name}: unknown`]), '{', ...linesOf(one, name, unread).map((line) => ` ${line}`), '}']
309
311
  }
310
- return [`${kind} ${name}: unknown = await ${asFn(one.whole, opened)}(opened)`]
312
+ return [`${unread ? '' : `${kind} ${name}: unknown = `}await ${asFn(one.whole, opened)}(opened)`]
311
313
  }
312
314
 
313
315
  const asFn = (text: string, opened: string, more: string = ''): string =>
@@ -316,7 +318,7 @@ const asFn = (text: string, opened: string, more: string = ''): string =>
316
318
  function forked(name: string, said: readonly string[], shape: Shape): Made | string {
317
319
  if (said.length !== 2) return `takes a chooser and the ways it chooses between, and was handed ${String(said.length)} thing(s)`
318
320
 
319
- const chose = spliced(said[0] ?? '', shape.names, shape.mine, ROLES.block)
321
+ const chose = spliced(said[0] ?? '', shape, ROLES.block)
320
322
  if (typeof chose === 'string') return `chooses with a block that ${chose}`
321
323
 
322
324
  const held = inside(said[1] ?? '', '{', '}')
@@ -324,9 +326,10 @@ function forked(name: string, said: readonly string[], shape: Shape): Made | str
324
326
  const named = ways?.every((one) => one !== undefined && PLAIN.test(one.key)) === true ? ways : undefined
325
327
  if (named === undefined || named.length === 0) return 'names its ways in a way this cannot read — write each one as `name: block`'
326
328
 
329
+ const unread = shape.unread.has(name)
327
330
  const picked = `${OWN}${name}_said`
328
331
  const runs = [
329
- `let ${name}: unknown`,
332
+ ...(unread ? [] : [`let ${name}: unknown`]),
330
333
  chose.form === 'expression'
331
334
  ? `const ${picked}: string = await (${chose.text})`
332
335
  : `const ${picked}: string = await ((${said[0] ?? ''}) as (opened: ${shape.reads}) => Promise<string>)(opened)`,
@@ -334,12 +337,12 @@ function forked(name: string, said: readonly string[], shape: Shape): Made | str
334
337
  ]
335
338
 
336
339
  for (const way of named) {
337
- const each = spliced(way?.held ?? '', shape.names, shape.mine, ROLES.block)
340
+ const each = spliced(way?.held ?? '', shape, ROLES.block)
338
341
  if (typeof each === 'string') return `has a way \`${way?.key ?? ''}\` that ${each}`
339
342
  runs.push(` case ${(way?.key ?? '').startsWith("'") ? (way?.key ?? '') : quoted(way?.key ?? '')}: {`)
340
- if (each.form === 'expression') runs.push(` ${name} = ${each.text}`)
341
- else if (each.form === 'statements') runs.push(...each.text.split('\n').map((line) => ` ${line.replace(ANSWERS, name)}`))
342
- else runs.push(` ${name} = await ${asFn(each.whole, shape.all)}(opened)`)
343
+ if (each.form === 'expression') runs.push(unread ? ` void (${each.text})` : ` ${name} = ${each.text}`)
344
+ else if (each.form === 'statements') runs.push(...linesOf(each, name, unread).map((line) => ` ${line}`))
345
+ else runs.push(` ${unread ? '' : `${name} = `}await ${asFn(each.whole, shape.all)}(opened)`)
343
346
  runs.push(' break')
344
347
  runs.push(' }')
345
348
  }
@@ -356,21 +359,22 @@ const SHAPES: Readonly<Record<string, (name: string, said: readonly string[], sh
356
359
 
357
360
  nd_each: (name, said, shape) => {
358
361
  if (said.length !== 2) return `takes what to run over and the block to run, and was handed ${String(said.length)} thing(s)`
359
- const over = spliced(said[0] ?? '', shape.names, shape.mine, ROLES.over)
360
- const block = spliced(said[1] ?? '', shape.names, shape.mine, ROLES.item)
362
+ const over = spliced(said[0] ?? '', shape, ROLES.over)
363
+ const block = spliced(said[1] ?? '', shape, ROLES.item)
361
364
  if (typeof block === 'string') return `runs a block that ${block}`
362
365
  if (typeof over === 'string') return `runs over a block that ${over}`
366
+ const unread = shape.unread.has(name)
363
367
  const item = `${OWN}${name}_item`
364
368
  const all = `${OWN}${name}_all`
365
369
  const each = `${OWN}${name}_each`
366
370
  return {
367
371
  runs: [
368
- `const ${all}: unknown[] = []`,
372
+ ...(unread ? [] : [`const ${all}: unknown[] = []`]),
369
373
  `const ${each} = (${block.whole}) as (item: unknown, opened: ${shape.all}) => Promise<unknown>`,
370
374
  `for (const ${item} of ${over.form === 'expression' ? `(${over.text})` : `((${over.whole}) as () => readonly unknown[])()`} as readonly unknown[]) {`,
371
- ` ${all}.push(await ${each}(${item}, opened))`,
375
+ unread ? ` await ${each}(${item}, opened)` : ` ${all}.push(await ${each}(${item}, opened))`,
372
376
  '}',
373
- `const ${name}: unknown = ${all}`,
377
+ ...(unread ? [] : [`const ${name}: unknown = ${all}`]),
374
378
  ],
375
379
  }
376
380
  },
@@ -384,16 +388,16 @@ const SHAPES: Readonly<Record<string, (name: string, said: readonly string[], sh
384
388
 
385
389
  const calls: string[] = []
386
390
  for (const [order, one] of every.entries()) {
387
- const each = spliced(one, shape.names, shape.mine, ROLES.block)
391
+ const each = spliced(one, shape, ROLES.block)
388
392
  if (typeof each === 'string') return `runs a block at position ${String(order + 1)} that ${each}`
389
393
  calls.push(each.form === 'expression' ? `(async () => ${each.text})()` : `${asFn(each.whole, shape.all)}(opened)`)
390
394
  }
391
- return { runs: [`const ${name}: unknown = await Promise.all([${calls.join(', ')}])`] }
395
+ return { runs: [`${shape.unread.has(name) ? '' : `const ${name}: unknown = `}await Promise.all([${calls.join(', ')}])`] }
392
396
  },
393
397
 
394
398
  nd_repeat: (name, said, shape) => {
395
399
  if (said.length !== 2) return `takes a ceiling and the block to repeat, and was handed ${String(said.length)} thing(s)`
396
- const block = spliced(said[1] ?? '', shape.names, shape.mine, ROLES.again)
400
+ const block = spliced(said[1] ?? '', shape, ROLES.again)
397
401
  if (typeof block === 'string') return `repeats a block that ${block}`
398
402
  const ceiling = `${OWN}${name}_ceiling`
399
403
  const again = `${OWN}${name}_again`
@@ -401,16 +405,17 @@ const SHAPES: Readonly<Record<string, (name: string, said: readonly string[], sh
401
405
  const done = `${OWN}${name}_done`
402
406
  const pass = `${OWN}${name}_pass`
403
407
  const answer = `${OWN}${name}_said`
408
+ const unread = shape.unread.has(name)
404
409
  return {
405
410
  runs: [
406
- `let ${name}: unknown`,
411
+ ...(unread ? [] : [`let ${name}: unknown`]),
407
412
  `const ${ceiling}: number = ${said[0] ?? ''}`,
408
413
  `const ${again} = (${block.whole}) as (pass: number, carried: unknown, opened: ${shape.all}) => Promise<{ readonly done: boolean; readonly value: unknown }>`,
409
414
  `let ${carried}: unknown`,
410
415
  `let ${done} = false`,
411
416
  `for (let ${pass} = 0; ${pass} < ${ceiling}; ${pass} += 1) {`,
412
417
  ` const ${answer} = await ${again}(${pass}, ${carried}, opened)`,
413
- ` if (${answer}.done) { ${name} = ${answer}.value; ${done} = true; break }`,
418
+ ` if (${answer}.done) { ${unread ? '' : `${name} = ${answer}.value; `}${done} = true; break }`,
414
419
  ` ${carried} = ${answer}.value`,
415
420
  '}',
416
421
  `if (!${done}) throw new Error(\`${name} reached its ceiling of \${String(${ceiling})} passes without answering done\`)`,
@@ -425,14 +430,15 @@ const STOPS = "Symbol.for('thread.operation.stopped')"
425
430
 
426
431
  function plainly(name: string, said: readonly string[], shape: Shape, opened: string, ends: boolean): Made | string {
427
432
  if (said.length !== 1) return `takes one block, and was handed ${String(said.length)} thing(s)`
428
- const one = spliced(said[0] ?? '', shape.names, shape.mine, ROLES.block)
433
+ const one = spliced(said[0] ?? '', shape, ROLES.block)
429
434
  if (typeof one === 'string') return one
430
435
 
431
- if (!ends) return { runs: answered(name, one, opened, 'const') }
436
+ const unread = shape.unread.has(name)
437
+ if (!ends) return { runs: answered(name, one, opened, 'const', unread) }
432
438
 
433
439
  return {
434
440
  runs: [
435
- ...answered(name, one, opened, 'const'),
441
+ ...answered(name, one, opened, 'const', unread),
436
442
  `throw Object.assign(new Error(${quoted(`${name} ended the operation`)}), { [${STOPS}]: true, value: ${name} })`,
437
443
  ],
438
444
  }
@@ -447,6 +453,7 @@ function compiled(
447
453
  id: string,
448
454
  one: ReturnType<typeof operations> extends ReadonlyMap<string, infer W> ? W : never,
449
455
  known: ReadonlySet<string>,
456
+ unread?: ReadonlySet<string>,
450
457
  ): { readonly text?: string; readonly because?: string } {
451
458
  const because = problem(id, one)
452
459
  if (because !== undefined) return { because }
@@ -474,6 +481,7 @@ function compiled(
474
481
 
475
482
  const runs: string[] = []
476
483
  const names = new Set<string>()
484
+ const heard = new Set<string>()
477
485
  let last: string | undefined
478
486
 
479
487
  for (const [order, row] of one.nodes.entries()) {
@@ -500,7 +508,7 @@ function compiled(
500
508
  return { because: `the node \`${name}\` states no block this can read in ${row.at} — run \`thread seed\` and write it again` }
501
509
  }
502
510
 
503
- const shape: Shape = { all: `Opened<${only}>`, reads: `Reads<Opened<${only}>>`, names, mine }
511
+ const shape: Shape = { all: `Opened<${only}>`, reads: `Reads<Opened<${only}>>`, names, mine, heard, unread: unread ?? new Set<string>() }
504
512
  const made = (SHAPES[from] ?? ownType)(name, parts(held), shape)
505
513
  if (typeof made === 'string') return { because: `the node \`${name}\` ${made}` }
506
514
 
@@ -510,12 +518,19 @@ function compiled(
510
518
  last = from === 'nd_stop' ? undefined : name
511
519
  }
512
520
 
521
+ if (unread === undefined) {
522
+ const final = one.nodes.at(-1)?.id
523
+ return compiled(at, id, one, known, new Set([...mine].filter((name) => name !== final && !heard.has(name))))
524
+ }
525
+
526
+ const typed = ['Opened', 'Reads'].filter((word) => runs.some((line) => line.includes(`${word}<`)))
527
+
513
528
  return {
514
529
  text: [
515
530
  "import type { z } from 'zod'",
516
531
  '',
517
532
  `import type { Operation } from ${quoted(runtime(at, '../operations.ts'))}`,
518
- 'import type { Opened, Reads } from ' + quoted(runtime(at, '../operations.ts')),
533
+ ...(typed.length === 0 ? [] : [`import type { ${typed.join(', ')} } from ${quoted(runtime(at, '../operations.ts'))}`]),
519
534
  '',
520
535
  `import { input, resource } from ${quoted(beside(at, takes))}`,
521
536
  '',
@@ -530,7 +545,7 @@ function compiled(
530
545
  ...(quota === undefined ? [] : [` quota: ${quota},`]),
531
546
  '',
532
547
  ' run: async (opened, held) => {',
533
- ' const input = held.input',
548
+ ...(unread.has('input') ? [] : [' const input = held.input']),
534
549
  ` let ${ENTERED} = ''`,
535
550
  ' try {',
536
551
  ...runs,
@@ -61,6 +61,7 @@ if (open.length > 0) {
61
61
  'that decision in the sheet. Otherwise name the roles that may, or state a `when` saying whose row it is');
62
62
  }
63
63
  const said = (held) => JSON.stringify(held);
64
+ const used = new Set();
64
65
  function reads(one, grant) {
65
66
  if ('value' in one)
66
67
  return said(one.value);
@@ -85,6 +86,7 @@ function reads(one, grant) {
85
86
  grant.on +
86
87
  '" itself, or state the rule as an invariant where the rows are');
87
88
  }
89
+ used.add(READS);
88
90
  return `at(resource, ${said(one.field)})`;
89
91
  }
90
92
  stop(`\`${grant.id}\` states an operand this gate cannot read`);
@@ -98,6 +100,7 @@ function predicate(condition, grant) {
98
100
  }
99
101
  if ('not' in condition)
100
102
  return `(!${predicate(condition.not, grant)})`;
103
+ used.add(COMPARES);
101
104
  return `held(${said(condition.op)}, ${reads(condition.is, grant)}, ${reads(condition.than, grant)})`;
102
105
  }
103
106
  function wears(grant) {
@@ -107,6 +110,8 @@ function wears(grant) {
107
110
  held.push(`some(actor.roles, ${said(roles)})`);
108
111
  if (grant.to?.entitlements !== undefined)
109
112
  held.push(`some(actor.entitlements, ${said(grant.to.entitlements)})`);
113
+ if (held.length > 0)
114
+ used.add(WEARS);
110
115
  return held;
111
116
  }
112
117
  const READS = ['const at = (row: Row | null, name: string): unknown => (row === null ? undefined : row[name])'];
@@ -154,9 +159,7 @@ function permitting() {
154
159
  `import type { Actor } from '${runtime(PERMITS_AT, '../../core/actor.ts')}'`,
155
160
  "import type { Row } from '" + runtime(PERMITS_AT, '../../integrations/target.ts') + "'",
156
161
  '',
157
- ...(grants.some((one) => one.when !== undefined) ? [...READS, ''] : []),
158
- ...(grants.some((one) => one.to !== undefined) ? [...WEARS, ''] : []),
159
- ...(grants.some((one) => one.when !== undefined) ? [...COMPARES, ''] : []),
162
+ ...[READS, WEARS, COMPARES].filter((one) => used.has(one)).flatMap((one) => [...one, '']),
160
163
  'export function permits(actor: Actor, action: string, resource: Row | null): { allow: boolean; because?: string } {',
161
164
  ...(grants.length === 0 ? [' void actor', ' void resource'] : []),
162
165
  ...body,