@prestack/thread 0.1.6 → 0.1.8

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 (41) hide show
  1. package/CHANGELOG.md +174 -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.js +18 -2
  9. package/dist/integrations/cli.ts +19 -2
  10. package/dist/integrations/disk.js +4 -3
  11. package/dist/integrations/disk.ts +4 -3
  12. package/dist/integrations/generators/vault.ts +0 -1
  13. package/dist/integrations/target.d.ts +1 -1
  14. package/dist/integrations/target.ts +1 -1
  15. package/dist/integrity/generators/gate.ts +1 -2
  16. package/dist/integrity/generators/publish.ts +0 -1
  17. package/dist/integrity/lint.js +0 -6
  18. package/dist/integrity/lint.ts +0 -10
  19. package/dist/integrity/slopnator.ts +1 -1
  20. package/dist/integrity/slopnators/sl_derived_drift.js +0 -2
  21. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -3
  22. package/dist/integrity/slopnators/sl_stale_path.js +10 -3
  23. package/dist/integrity/slopnators/sl_stale_path.ts +10 -3
  24. package/dist/integrity/slopnators/sl_undeclared_crossing.js +8 -5
  25. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +12 -5
  26. package/dist/integrity/slopnators/sl_unlisted_program.js +7 -1
  27. package/dist/integrity/slopnators/sl_unlisted_program.ts +7 -1
  28. package/dist/integrity/slopnators/sl_unthreaded.js +0 -2
  29. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -5
  30. package/dist/integrity/sweep.ts +0 -2
  31. package/dist/knowledge/attach.js +1 -18
  32. package/dist/knowledge/attach.ts +1 -19
  33. package/dist/knowledge/generators/docsgen.ts +1 -1
  34. package/dist/operations/generators/capability.js +20 -9
  35. package/dist/operations/generators/capability.ts +19 -12
  36. package/dist/operations/generators/compile.js +41 -28
  37. package/dist/operations/generators/compile.ts +48 -33
  38. package/dist/operations/generators/permits.js +6 -3
  39. package/dist/operations/generators/permits.ts +7 -5
  40. package/dist/operations/generators/trial.ts +1 -1
  41. package/package.json +1 -1
@@ -238,23 +238,6 @@ export const CONCEPTS: readonly Concept[] = [
238
238
  },
239
239
  ]
240
240
  `;
241
- const OWN = [
242
- {
243
- into: 'knowledge.ts',
244
- list: 'declares',
245
- id: 'skills',
246
- source: [
247
- ' {',
248
- " id: 'skills',",
249
- " at: '_threads/_generated/skills.ts',",
250
- " describes: 'One row per skill `thread skills` writes, so every `skills/<noun>.md` is declared by the command that wrote it instead of being refused as a file nothing named. It sits beside the files it names because the nouns are not known until the tree is read, and a list somebody keeps by hand would go stale the first time a package brings a new maker.',",
251
- ' version: 1,',
252
- " by: 'thread skills',",
253
- " runs: 'thread skills --check',",
254
- ' },',
255
- ].join('\n'),
256
- },
257
- ];
258
241
  const SEEDED = {
259
242
  attaches: ['@prestack/thread/derivation/attach'],
260
243
  id: 'knowledge',
@@ -330,5 +313,5 @@ export const ATTACH = {
330
313
  source: CONCEPTS_SOURCE,
331
314
  },
332
315
  ],
333
- rows: [...OWN, ...SHIPPED],
316
+ rows: SHIPPED,
334
317
  };
@@ -246,24 +246,6 @@ export const CONCEPTS: readonly Concept[] = [
246
246
  ]
247
247
  `
248
248
 
249
- const OWN: readonly { readonly into: string; readonly list: string; readonly id: string; readonly source: string }[] = [
250
- {
251
- into: 'knowledge.ts',
252
- list: 'declares',
253
- id: 'skills',
254
- source: [
255
- ' {',
256
- " id: 'skills',",
257
- " at: '_threads/_generated/skills.ts',",
258
- " describes: 'One row per skill `thread skills` writes, so every `skills/<noun>.md` is declared by the command that wrote it instead of being refused as a file nothing named. It sits beside the files it names because the nouns are not known until the tree is read, and a list somebody keeps by hand would go stale the first time a package brings a new maker.',",
259
- ' version: 1,',
260
- " by: 'thread skills',",
261
- " runs: 'thread skills --check',",
262
- ' },',
263
- ].join('\n'),
264
- },
265
- ]
266
-
267
249
  const SEEDED = {
268
250
  attaches: ['@prestack/thread/derivation/attach'] as const,
269
251
  id: 'knowledge',
@@ -341,5 +323,5 @@ export const ATTACH: Attaching = {
341
323
  source: CONCEPTS_SOURCE,
342
324
  },
343
325
  ],
344
- rows: [...OWN, ...SHIPPED],
326
+ rows: SHIPPED,
345
327
  }
@@ -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, stderr, stdout } from 'node:process'
4
4
 
5
5
  import { exit } from '../../core/invoked.ts'
@@ -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,