@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
@@ -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,
@@ -1,8 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from '../../integrations/disk.ts'
2
2
  import { valuesIn } from '../../core/rows.ts'
3
- import { dirname, resolve } from 'node:path'
3
+ import { resolve } from 'node:path'
4
4
  import { argv, stderr, stdout } from 'node:process'
5
- import { pathToFileURL } from 'node:url'
6
5
 
7
6
  import { exit } from '../../core/invoked.ts'
8
7
  import { PUBLIC_ROLE as PUBLIC } from '../../core/actor.ts'
@@ -81,6 +80,8 @@ if (open.length > 0) {
81
80
 
82
81
  const said = (held: unknown): string => JSON.stringify(held)
83
82
 
83
+ const used = new Set<readonly string[]>()
84
+
84
85
  function reads(one: Operand, grant: Grant): string {
85
86
  if ('value' in one) return said(one.value)
86
87
  if ('now' in one) return 'Date.now()'
@@ -106,6 +107,7 @@ function reads(one: Operand, grant: Grant): string {
106
107
  '" itself, or state the rule as an invariant where the rows are',
107
108
  )
108
109
  }
110
+ used.add(READS)
109
111
  return `at(resource, ${said(one.field)})`
110
112
  }
111
113
  stop(`\`${grant.id}\` states an operand this gate cannot read`)
@@ -119,6 +121,7 @@ function predicate(condition: Condition, grant: Grant): string {
119
121
  return condition.any.length === 0 ? 'false' : `(${condition.any.map((one) => predicate(one, grant)).join(' || ')})`
120
122
  }
121
123
  if ('not' in condition) return `(!${predicate(condition.not, grant)})`
124
+ used.add(COMPARES)
122
125
  return `held(${said(condition.op)}, ${reads(condition.is, grant)}, ${reads(condition.than, grant)})`
123
126
  }
124
127
 
@@ -127,6 +130,7 @@ function wears(grant: Grant): readonly string[] {
127
130
  const roles = grant.to?.roles
128
131
  if (roles !== undefined && !roles.includes(PUBLIC)) held.push(`some(actor.roles, ${said(roles)})`)
129
132
  if (grant.to?.entitlements !== undefined) held.push(`some(actor.entitlements, ${said(grant.to.entitlements)})`)
133
+ if (held.length > 0) used.add(WEARS)
130
134
  return held
131
135
  }
132
136
 
@@ -181,9 +185,7 @@ function permitting(): string {
181
185
  `import type { Actor } from '${runtime(PERMITS_AT, '../../core/actor.ts')}'`,
182
186
  "import type { Row } from '" + runtime(PERMITS_AT, '../../integrations/target.ts') + "'",
183
187
  '',
184
- ...(grants.some((one) => one.when !== undefined) ? [...READS, ''] : []),
185
- ...(grants.some((one) => one.to !== undefined) ? [...WEARS, ''] : []),
186
- ...(grants.some((one) => one.when !== undefined) ? [...COMPARES, ''] : []),
188
+ ...[READS, WEARS, COMPARES].filter((one) => used.has(one)).flatMap((one) => [...one, '']),
187
189
 
188
190
  'export function permits(actor: Actor, action: string, resource: Row | null): { allow: boolean; because?: string } {',
189
191
  ...(grants.length === 0 ? [' void actor', ' void resource'] : []),
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, writeFileSync, type Answering, type Listed } 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
  import { pathToFileURL } from 'node:url'
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prestack/thread",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",