@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
@@ -15,6 +15,7 @@ const ARRIVES = 'Which header a value comes in on. A webhook says which one carr
15
15
  const SIGNS = 'What a secret does to a body on its way out: its value is the key a signature is computed with, and the digest goes on the header named here. It is `sends` turned around — `sends` puts the value itself on a header, and this puts the value nowhere at all.';
16
16
  const ADDRESS = 'Where a target reached over https is: the one absolute url every call on it is written from, stated on the row because a place is a fact anybody may read. Only the address of a database is sealed instead, because a connection string carries a password.';
17
17
  const HEADERS = 'The headers every call on a target carries, stated on its row as name and value: the version of an API this app is written against, or a fixed way of asking. A credential never goes here, because anybody may read a row; a value that has to be withheld is a secret with a sends.';
18
+ const NEEDS = 'The packages that must be installed for this to work, each named with the versions it works with: a range from npm, a git link or a tarball. Attaching a package, or creating any row that states them, adds the ones the app does not already name to what it depends on and fetches them. A name the app already carries is left alone, so nothing overwrites a choice somebody made.';
18
19
  const SSOT = { on: '2026-08-29', said: 'SSOT for the integrations, as a whole meta inside thread core' };
19
20
  const RULED = { on: '2026-09-22', said: 'remove pub then as an acceptable thing, that\'s definitely slop and it\'s not the first time such a fucking thing has surfaced. Delete and redo it properly.' };
20
21
  export const CONCEPTS = [
@@ -28,6 +29,7 @@ export const CONCEPTS = [
28
29
  { id: 'call', describes: CALL, cites: [SSOT] },
29
30
  { id: 'reach', describes: REACH, cites: [SSOT] },
30
31
  { id: 'integration', describes: INTEGRATION, cites: [SSOT] },
32
+ { id: 'needs', describes: NEEDS, cites: [] },
31
33
  ];
32
34
  import { quoted } from '../core/rows.js';
33
35
  const concept = (one) => [
@@ -36,9 +38,9 @@ const concept = (one) => [
36
38
  " at: '_threads/concepts.ts',",
37
39
  ` describes: ${quoted(one.describes)},`,
38
40
  ` version: ${String(VERSION)},`,
39
- ' cites: [',
40
- ...one.cites.map((each) => " { on: '" + each.on + "', said: " + quoted(each.said) + ' },'),
41
- ' ],',
41
+ ...(one.cites.length === 0
42
+ ? [' cites: [],']
43
+ : [' cites: [', ...one.cites.map((each) => " { on: '" + each.on + "', said: " + quoted(each.said) + ' },'), ' ],']),
42
44
  ' },',
43
45
  ].join('\n');
44
46
  const INTEGRATIONS_SOURCE = `import type { Threaded } from '${SHAPED}'
@@ -1,6 +1,6 @@
1
1
  import type { Attaching } from '../core/attach.ts'
2
2
  import { SHAPED } from '../core/init.ts'
3
- import { NAMED, named } from '../core/scope.ts'
3
+ import { NAMED } from '../core/scope.ts'
4
4
 
5
5
  const VERSION = 1
6
6
 
@@ -46,6 +46,9 @@ const ADDRESS =
46
46
  const HEADERS =
47
47
  'The headers every call on a target carries, stated on its row as name and value: the version of an API this app is written against, or a fixed way of asking. A credential never goes here, because anybody may read a row; a value that has to be withheld is a secret with a sends.'
48
48
 
49
+ const NEEDS =
50
+ 'The packages that must be installed for this to work, each named with the versions it works with: a range from npm, a git link or a tarball. Attaching a package, or creating any row that states them, adds the ones the app does not already name to what it depends on and fetches them. A name the app already carries is left alone, so nothing overwrites a choice somebody made.'
51
+
49
52
  const SSOT = { on: '2026-08-29', said: 'SSOT for the integrations, as a whole meta inside thread core' }
50
53
 
51
54
  const RULED = { on: '2026-09-22', said: 'remove pub then as an acceptable thing, that\'s definitely slop and it\'s not the first time such a fucking thing has surfaced. Delete and redo it properly.' }
@@ -61,6 +64,7 @@ export const CONCEPTS: readonly { readonly id: string; readonly describes: strin
61
64
  { id: 'call', describes: CALL, cites: [SSOT] },
62
65
  { id: 'reach', describes: REACH, cites: [SSOT] },
63
66
  { id: 'integration', describes: INTEGRATION, cites: [SSOT] },
67
+ { id: 'needs', describes: NEEDS, cites: [] },
64
68
  ]
65
69
 
66
70
  import { quoted } from '../core/rows.ts'
@@ -72,9 +76,9 @@ const concept = (one: (typeof CONCEPTS)[number]): string =>
72
76
  " at: '_threads/concepts.ts',",
73
77
  ` describes: ${quoted(one.describes)},`,
74
78
  ` version: ${String(VERSION)},`,
75
- ' cites: [',
76
- ...one.cites.map((each) => " { on: '" + each.on + "', said: " + quoted(each.said) + ' },'),
77
- ' ],',
79
+ ...(one.cites.length === 0
80
+ ? [' cites: [],']
81
+ : [' cites: [', ...one.cites.map((each) => " { on: '" + each.on + "', said: " + quoted(each.said) + ' },'), ' ],']),
78
82
  ' },',
79
83
  ].join('\n')
80
84
 
@@ -6,7 +6,7 @@ import { allowedNodeEnvironmentFlags, env, execPath, kill, pid, stderr, stdout }
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { CHANNEL, FOLDER, across, attached, createFileSync, existsSync, here, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from '../integrations/disk.js';
8
8
  import { VAULT_DIR } from '../integrations/vault.js';
9
- import { CALL_PREFIX, LOCKFILES, PACKAGER, REACH, managerOf, programOf, unlisted } from '../integrations/target.js';
9
+ import { CALL_PREFIX, LOCKFILES, ORDERS, PACKAGER, REACH, managerOf, programOf, unlisted } from '../integrations/target.js';
10
10
  import { CLI } from '../core/invoked.js';
11
11
  import { valuesIn } from '../core/rows.js';
12
12
  import { THING } from '../core/scope.js';
@@ -201,6 +201,11 @@ const still = (one, asks) => ({ there: true, ok: false, code: -1, out: '', err:
201
201
  const nowhere = (why) => ({ there: false, ok: false, code: 1, out: '', err: '', killed: null, why });
202
202
  const lost = (of) => ({ there: true, ok: false, code: -1, out: '', err: '', killed: null, why: '', of, lost: of });
203
203
  const told = (one) => JSON.stringify([one.order, one.at, one.said ?? null, one.mode ?? null, one.words ?? null]);
204
+ const READS = new Set(Object.entries(ORDERS).filter(([order, answer]) => order === answer).map(([order]) => order));
205
+ function kept(one, asked, held) {
206
+ if (READS.has(asked.order) && held.answered !== 'refused')
207
+ one.known.set(told(asked), held);
208
+ }
204
209
  const plainly = (one) => one.order + ' ' + (one.at === '' ? '.' : one.at);
205
210
  const outside = (root, at) => {
206
211
  const step = relative(root, at);
@@ -248,10 +253,18 @@ function watching(one) {
248
253
  continue;
249
254
  }
250
255
  const asked = { ...order, at: relative(one.root, order.at) };
256
+ const known = one.known.get(told(asked));
257
+ if (known !== undefined) {
258
+ reply(one.dir, short, known);
259
+ continue;
260
+ }
261
+ if (!READS.has(asked.order))
262
+ one.known.clear();
251
263
  const due = one.replaying.shift();
252
264
  if (due !== undefined) {
253
265
  if (told(due.order) === told(asked)) {
254
266
  reply(one.dir, short, due.answered);
267
+ kept(one, asked, due.answered);
255
268
  continue;
256
269
  }
257
270
  one.replaying = [];
@@ -263,6 +276,7 @@ function watching(one) {
263
276
  continue;
264
277
  }
265
278
  one.pending = short;
279
+ one.asking = asked;
266
280
  hand(one, still(one, [asked]));
267
281
  }
268
282
  }, TICK);
@@ -295,7 +309,7 @@ function begun(word, args, root, hears, called, recording = []) {
295
309
  const of = called ?? randomBytes(6).toString('hex');
296
310
  const dir = join(tmpdir(), SERVED, randomBytes(6).toString('hex'));
297
311
  writeFileSync(join(dir, 'channel'), '', 'utf8');
298
- const one = { of, dir, root, out: '', err: '', why: '', pending: '', waiting: [], parked: [], replaying: [...recording], handed: recording.length };
312
+ const one = { of, dir, root, out: '', err: '', why: '', pending: '', waiting: [], parked: [], replaying: [...recording], handed: recording.length, known: new Map() };
299
313
  const child = spawn(execPath, [...CLI, word, ...args], {
300
314
  cwd: OWN,
301
315
  shell: false,
@@ -346,6 +360,8 @@ async function served(word, args, within, how) {
346
360
  const given = (how?.answered ?? [])[0];
347
361
  if (one.pending !== '' && given !== undefined) {
348
362
  reply(one.dir, one.pending, given);
363
+ if (one.asking !== undefined)
364
+ kept(one, one.asking, given);
349
365
  one.pending = '';
350
366
  }
351
367
  return counted(one, await next(one, within));
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url'
8
8
  import { CHANNEL, FOLDER, across, attached, createFileSync, existsSync, here, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from '../integrations/disk.ts'
9
9
  import { VAULT_DIR } from '../integrations/vault.ts'
10
10
 
11
- import { CALL_PREFIX, LOCKFILES, PACKAGER, REACH, managerOf, programOf, unlisted, type Answer, type Call, type How, type Order, type Ran, type Recorded, type Running, type Target } from '../integrations/target.ts'
11
+ import { CALL_PREFIX, LOCKFILES, ORDERS, PACKAGER, REACH, managerOf, programOf, unlisted, type Answer, type Call, type How, type Order, type Ran, type Recorded, type Running, type Target } from '../integrations/target.ts'
12
12
  import { CLI } from '../core/invoked.ts'
13
13
  import { valuesIn } from '../core/rows.ts'
14
14
  import { THING } from '../core/scope.ts'
@@ -267,6 +267,8 @@ interface Serving {
267
267
  parked: Ran[]
268
268
  replaying: Recorded[]
269
269
  handed: number
270
+ readonly known: Map<string, Answer>
271
+ asking?: Order
270
272
  last?: Ran
271
273
  stop?: () => void
272
274
  watch?: ReturnType<typeof setInterval>
@@ -282,6 +284,12 @@ const lost = (of: string): Ran => ({ there: true, ok: false, code: -1, out: '',
282
284
 
283
285
  const told = (one: Order): string => JSON.stringify([one.order, one.at, one.said ?? null, one.mode ?? null, one.words ?? null])
284
286
 
287
+ const READS: ReadonlySet<string> = new Set(Object.entries(ORDERS).filter(([order, answer]) => order === answer).map(([order]) => order))
288
+
289
+ function kept(one: Serving, asked: Order, held: Answer): void {
290
+ if (READS.has(asked.order) && held.answered !== 'refused') one.known.set(told(asked), held)
291
+ }
292
+
285
293
  const plainly = (one: Order): string => one.order + ' ' + (one.at === '' ? '.' : one.at)
286
294
 
287
295
  const outside = (root: string, at: string): boolean => {
@@ -328,10 +336,17 @@ function watching(one: Serving): void {
328
336
  continue
329
337
  }
330
338
  const asked: Order = { ...order, at: relative(one.root, order.at) }
339
+ const known = one.known.get(told(asked))
340
+ if (known !== undefined) {
341
+ reply(one.dir, short, known)
342
+ continue
343
+ }
344
+ if (!READS.has(asked.order)) one.known.clear()
331
345
  const due = one.replaying.shift()
332
346
  if (due !== undefined) {
333
347
  if (told(due.order) === told(asked)) {
334
348
  reply(one.dir, short, due.answered)
349
+ kept(one, asked, due.answered)
335
350
  continue
336
351
  }
337
352
  one.replaying = []
@@ -343,6 +358,7 @@ function watching(one: Serving): void {
343
358
  continue
344
359
  }
345
360
  one.pending = short
361
+ one.asking = asked
346
362
  hand(one, still(one, [asked]))
347
363
  }
348
364
  }, TICK)
@@ -373,7 +389,7 @@ function begun(word: string, args: readonly string[], root: string, hears?: stri
373
389
  const of = called ?? randomBytes(6).toString('hex')
374
390
  const dir = join(tmpdir(), SERVED, randomBytes(6).toString('hex'))
375
391
  writeFileSync(join(dir, 'channel'), '', 'utf8')
376
- const one: Serving = { of, dir, root, out: '', err: '', why: '', pending: '', waiting: [], parked: [], replaying: [...recording], handed: recording.length }
392
+ const one: Serving = { of, dir, root, out: '', err: '', why: '', pending: '', waiting: [], parked: [], replaying: [...recording], handed: recording.length, known: new Map() }
377
393
  const child = spawn(execPath, [...CLI, word, ...args], {
378
394
  cwd: OWN,
379
395
  shell: false,
@@ -422,6 +438,7 @@ async function served(word: string, args: readonly string[], within?: number, ho
422
438
  const given = (how?.answered ?? [])[0]
423
439
  if (one.pending !== '' && given !== undefined) {
424
440
  reply(one.dir, one.pending, given)
441
+ if (one.asking !== undefined) kept(one, one.asking, given)
425
442
  one.pending = ''
426
443
  }
427
444
  return counted(one, await next(one, within))
@@ -132,9 +132,9 @@ function overFiles(dir) {
132
132
  reals.set(at, found);
133
133
  return found;
134
134
  },
135
- fetch: (at) => {
135
+ fetch: (at, words) => {
136
136
  try {
137
- return String(took({ order: 'fetch', at }).said);
137
+ return String(took(words === undefined ? { order: 'fetch', at } : { order: 'fetch', at, words }).said);
138
138
  }
139
139
  finally {
140
140
  unknown();
@@ -308,7 +308,8 @@ export const HERE = {
308
308
  held = disk.statSync(at, { throwIfNoEntry: false });
309
309
  }
310
310
  catch (thrown) {
311
- if (thrown.code === 'ERR_ACCESS_DENIED')
311
+ const code = thrown.code;
312
+ if (code === 'ERR_ACCESS_DENIED' || code === 'ENAMETOOLONG')
312
313
  return 'none';
313
314
  throw thrown;
314
315
  }
@@ -146,9 +146,9 @@ function overFiles(dir: string): Attached {
146
146
  reals.set(at, found)
147
147
  return found
148
148
  },
149
- fetch: (at) => {
149
+ fetch: (at, words) => {
150
150
  try {
151
- return String(took({ order: 'fetch', at }).said)
151
+ return String(took(words === undefined ? { order: 'fetch', at } : { order: 'fetch', at, words }).said)
152
152
  } finally {
153
153
  unknown()
154
154
  }
@@ -349,7 +349,8 @@ export const HERE: Attached = {
349
349
  try {
350
350
  held = disk.statSync(at, { throwIfNoEntry: false })
351
351
  } catch (thrown) {
352
- if ((thrown as NodeJS.ErrnoException).code === 'ERR_ACCESS_DENIED') return 'none'
352
+ const code = (thrown as NodeJS.ErrnoException).code
353
+ if (code === 'ERR_ACCESS_DENIED' || code === 'ENAMETOOLONG') return 'none'
353
354
  throw thrown
354
355
  }
355
356
  if (held === undefined) return 'none'
@@ -2,7 +2,6 @@ import { existsSync, readFileSync, writeFileSync } from '../disk.ts'
2
2
  import { valuesIn } from '../../core/rows.ts'
3
3
  import { dirname, join, relative } from 'node:path'
4
4
  import { argv, stderr, stdout } from 'node:process'
5
- import { fileURLToPath } from 'node:url'
6
5
 
7
6
  import ts from 'typescript'
8
7
 
@@ -48,7 +48,7 @@ export interface Answering {
48
48
  readonly real: (at: string) => string;
49
49
  }
50
50
  export interface Attached extends Answering {
51
- readonly fetch: (at: string) => string;
51
+ readonly fetch: (at: string, words?: readonly string[]) => string;
52
52
  readonly run: (at: string, words: readonly string[]) => Ran;
53
53
  }
54
54
  export declare const ORDERS: {
@@ -127,7 +127,7 @@ export interface Answering {
127
127
 
128
128
  export interface Attached extends Answering {
129
129
 
130
- readonly fetch: (at: string) => string
130
+ readonly fetch: (at: string, words?: readonly string[]) => string
131
131
 
132
132
  readonly run: (at: string, words: readonly string[]) => Ran
133
133
  }
@@ -1,8 +1,7 @@
1
1
  import { starts } from '../../integrations/cli.ts'
2
2
  import { createHash } from 'node:crypto'
3
- import { dirname, join } from 'node:path'
3
+ import { join } from 'node:path'
4
4
  import { argv, execPath, stderr, stdout } from 'node:process'
5
- import { fileURLToPath } from 'node:url'
6
5
  import { descend } from '../../core/descend.ts'
7
6
  import { attached, existsSync, here, readdirSync, readFileSync, rmSync, writeFileSync } from '../../integrations/disk.ts'
8
7
  import { LOADER, exit } from '../../core/invoked.ts'
@@ -3,7 +3,6 @@ import { existsSync, readFileSync } from '../../integrations/disk.ts'
3
3
  import { valuesIn } from '../../core/rows.ts'
4
4
  import { join } from 'node:path'
5
5
  import { argv, env, execPath, stderr, stdout } from 'node:process'
6
- import { pathToFileURL } from 'node:url'
7
6
  import { CLI, exit } from '../../core/invoked.ts'
8
7
  import { THING as TYPED, named } from '../../core/scope.ts'
9
8
  import { findTree, inTree } from '../../core/tree.ts'
@@ -4,12 +4,6 @@ import tseslint from 'typescript-eslint';
4
4
  import { THING } from '../core/scope.js';
5
5
  import { CONSTRAINTS, shown } from './constraint.js';
6
6
  import { NOT_WRITTEN, governs, subject, reads, visits, load, ROOT, rules as appRules, slopnatorName, } from './slopnator.js';
7
- const PROBE = {
8
- filename: 'probe.ts',
9
- text: '',
10
- ancestors: () => [],
11
- comments: () => [],
12
- };
13
7
  export function ruleMessage(rule) {
14
8
  const cites = (rule.cites ?? []).map((cite) => `${cite.on} — ${cite.said}`).join(' · ');
15
9
  return cites === '' ? `${rule.bans} — ${rule.because}. Instead: ${rule.instead}.` : `${rule.bans} — ${rule.because}. Instead: ${rule.instead}. (${cites})`;
@@ -1,5 +1,4 @@
1
1
  import { starts } from '../integrations/cli.ts'
2
- import { fileURLToPath } from 'node:url'
3
2
 
4
3
  import { Linter, type Rule } from 'eslint'
5
4
  import tseslint from 'typescript-eslint'
@@ -20,15 +19,6 @@ import {
20
19
  type SlopnatorContext,
21
20
  } from './slopnator.ts'
22
21
 
23
-
24
-
25
- const PROBE: SlopnatorContext = {
26
- filename: 'probe.ts',
27
- text: '',
28
- ancestors: () => [],
29
- comments: () => [],
30
- }
31
-
32
22
  export function ruleMessage(rule: Constraint): string {
33
23
  const cites = (rule.cites ?? []).map((cite) => `${cite.on} — ${cite.said}`).join(' · ')
34
24
  return cites === '' ? `${rule.bans} — ${rule.because}. Instead: ${rule.instead}.` : `${rule.bans} — ${rule.because}. Instead: ${rule.instead}. (${cites})`
@@ -8,7 +8,7 @@ import { invoked } from '../core/invoked.ts'
8
8
 
9
9
  import { listBounds, quoted, withSeed } from '../core/rows.ts'
10
10
 
11
- import { CONSTRAINTS, type Constraint, type Proof, type Sourced } from './constraint.ts'
11
+ import { CONSTRAINTS, type Constraint, type Proof } from './constraint.ts'
12
12
  import { findTree, inTree } from '../core/tree.ts'
13
13
 
14
14
  export const NOT_WRITTEN = Symbol.for('thread.slopnator.not_written')
@@ -2,8 +2,6 @@ import { readFileSync } from '../../integrations/disk.js';
2
2
  import { resolve } from 'node:path';
3
3
  import { atsIn } from './sl_unthreaded.js';
4
4
  import { reached } from '../../core/descend.js';
5
- import { findTree } from '../../core/tree.js';
6
- const ROOT = `${findTree().at}/`;
7
5
  export function derivations(root) {
8
6
  const found = [];
9
7
  for (const at of reached()) {
@@ -4,9 +4,6 @@ import { resolve } from 'node:path'
4
4
  import type { Slopnator } from '../slopnator.ts'
5
5
  import { atsIn } from './sl_unthreaded.ts'
6
6
  import { reached } from '../../core/descend.ts'
7
- import { findTree } from '../../core/tree.ts'
8
-
9
- const ROOT = `${findTree().at}/`
10
7
 
11
8
  export interface Derivation {
12
9
  readonly at: string
@@ -6,10 +6,14 @@ const ROOT = findTree().at;
6
6
  const TREE = findTree().name;
7
7
  const canonical = (at) => (at.startsWith('threads/') ? `${TREE}/${at.slice('threads/'.length)}` : at);
8
8
  const NAMES_A_FILE = /\.[a-z]{1,5}$/;
9
- const derived = (() => {
9
+ const known = new WeakMap();
10
+ const derived = () => {
10
11
  const found = new Set();
11
12
  try {
12
13
  const pass = descend();
14
+ const cached = known.get(pass);
15
+ if (cached !== undefined)
16
+ return cached;
13
17
  for (const [at, held] of pass.threaded) {
14
18
  if (held.some((one) => one.row.by !== undefined))
15
19
  found.add(at);
@@ -17,11 +21,12 @@ const derived = (() => {
17
21
  for (const one of pass.derived.values())
18
22
  if (one.at !== undefined)
19
23
  found.add(one.at);
24
+ known.set(pass, found);
20
25
  }
21
26
  catch {
22
27
  }
23
28
  return found;
24
- })();
29
+ };
25
30
  const ours = () => {
26
31
  try {
27
32
  return new Set(readdirSync(ROOT).filter((one) => one !== 'node_modules' && statSync(resolve(ROOT, one), { throwIfNoEntry: false })?.isDirectory() === true));
@@ -69,7 +74,7 @@ export const slopnator = {
69
74
  return false;
70
75
  if (existsSync(resolve(ROOT, path)))
71
76
  return false;
72
- if (derived.has(path))
77
+ if (derived().has(path))
73
78
  return false;
74
79
  return `\`${value}\` is not on disk`;
75
80
  },
@@ -137,6 +142,8 @@ export const slopnator = {
137
142
  "const said = 'a sentence about packages/core/integrity/src/constraint.ts and nothing else'",
138
143
  {
139
144
  files: [
145
+ { at: 'thread/core/thread.ts', source: 'export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly by?: string\n readonly runs?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n' },
146
+ { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n derives: [\n {\n id: 'layout',\n at: 'app/layout.tsx',\n by: 'thread routes',\n runs: 'thread routes --check',\n },\n ],\n}\n" },
140
147
  { at: 'app/_probe_there.tsx', source: 'export const THERE = 1\n' },
141
148
  { at: 'probe.tsx', source: "const at = 'app/layout.tsx'\n" },
142
149
  ],
@@ -10,18 +10,23 @@ const TREE = findTree().name
10
10
  const canonical = (at: string): string => (at.startsWith('threads/') ? `${TREE}/${at.slice('threads/'.length)}` : at)
11
11
  const NAMES_A_FILE = /\.[a-z]{1,5}$/
12
12
 
13
- const derived = ((): ReadonlySet<string> => {
13
+ const known = new WeakMap<object, ReadonlySet<string>>()
14
+
15
+ const derived = (): ReadonlySet<string> => {
14
16
  const found = new Set<string>()
15
17
  try {
16
18
  const pass = descend()
19
+ const cached = known.get(pass)
20
+ if (cached !== undefined) return cached
17
21
  for (const [at, held] of pass.threaded) {
18
22
  if (held.some((one) => one.row.by !== undefined)) found.add(at)
19
23
  }
20
24
  for (const one of pass.derived.values()) if (one.at !== undefined) found.add(one.at)
25
+ known.set(pass, found)
21
26
  } catch {
22
27
  }
23
28
  return found
24
- })()
29
+ }
25
30
 
26
31
  const ours = (): ReadonlySet<string> => {
27
32
  try {
@@ -79,7 +84,7 @@ export const slopnator: Slopnator = {
79
84
 
80
85
  if (!NAMES_A_FILE.test(path)) return false
81
86
  if (existsSync(resolve(ROOT, path))) return false
82
- if (derived.has(path)) return false
87
+ if (derived().has(path)) return false
83
88
  return `\`${value}\` is not on disk`
84
89
  },
85
90
  proves: {
@@ -146,6 +151,8 @@ export const slopnator: Slopnator = {
146
151
  "const said = 'a sentence about packages/core/integrity/src/constraint.ts and nothing else'",
147
152
  {
148
153
  files: [
154
+ { at: 'thread/core/thread.ts', source: 'export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly by?: string\n readonly runs?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n' },
155
+ { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n derives: [\n {\n id: 'layout',\n at: 'app/layout.tsx',\n by: 'thread routes',\n runs: 'thread routes --check',\n },\n ],\n}\n" },
149
156
  { at: 'app/_probe_there.tsx', source: 'export const THERE = 1\n' },
150
157
  { at: 'probe.tsx', source: "const at = 'app/layout.tsx'\n" },
151
158
  ],
@@ -6,7 +6,11 @@ import { packageOf } from '../../core/attach.js';
6
6
  import { PREFIXES } from '../../core/thread.js';
7
7
  import { HOLDS, OPENS } from '../../integrations/target.js';
8
8
  const FROM_SOURCE = import.meta.url.endsWith('.ts');
9
- const OPENED_AT = fileURLToPath(new URL(FROM_SOURCE ? '../../operations/operations.ts' : '../../operations/operations.d.ts', import.meta.url));
9
+ const SHAPE = 'operations/operations';
10
+ const OPENED_AT = fileURLToPath(new URL('../../' + SHAPE + (FROM_SOURCE ? '.ts' : '.d.ts'), import.meta.url));
11
+ const FRAMEWORK = 'thread';
12
+ const STATED_AT = resolve(ROOT, FRAMEWORK, SHAPE + '.ts');
13
+ const SHAPES = [...new Set([STATED_AT, OPENED_AT])];
10
14
  const HANDED = 'Opened';
11
15
  const EVERY = '';
12
16
  const IMPORTED = 'import ';
@@ -147,15 +151,14 @@ function compiled() {
147
151
  return built;
148
152
  const options = configured().options;
149
153
  const roots = [THREAD_AT, ...sources()].filter((one) => SOURCE.test(one)).map((one) => resolve(ROOT, one));
150
- const program = ts.createProgram([...new Set([OPENED_AT, ...roots])], options, hosting(options));
154
+ const program = ts.createProgram([...new Set([...SHAPES, ...roots])], options, hosting(options));
151
155
  const checker = program.getTypeChecker();
152
156
  const hands = new Map();
153
157
  const own = (symbol) => (symbol.getDeclarations() ?? []).some((one) => {
154
158
  const from = one.getSourceFile();
155
159
  return !program.isSourceFileDefaultLibrary(from) && !program.isSourceFileFromExternalLibrary(from);
156
160
  });
157
- const shape = program.getSourceFile(OPENED_AT);
158
- for (const statement of shape?.statements ?? []) {
161
+ for (const statement of SHAPES.flatMap((one) => program.getSourceFile(one)?.statements ?? [])) {
159
162
  if (!ts.isTypeAliasDeclaration(statement) || statement.name.text !== HANDED)
160
163
  continue;
161
164
  const alias = checker.getSymbolAtLocation(statement.name);
@@ -376,7 +379,7 @@ export function judging(at, wanted, closing) {
376
379
  if (!SOURCE.test(path))
377
380
  return false;
378
381
  const held = compiled();
379
- if (held.hands.size === 0 && resolve(ROOT, path) === OPENED_AT) {
382
+ if (held.hands.size === 0 && SHAPES.includes(resolve(ROOT, path))) {
380
383
  return 'no type called ' + HANDED + ' can be read here, so nothing can tell what an operation was opened with from any other value, and a call on it outside an operation goes unmeasured';
381
384
  }
382
385
  const found = judged(path, held, operational(held.of, path), wanted);
@@ -12,7 +12,15 @@ import type { Slopnator, SlopnatorContext } from '../slopnator.ts'
12
12
 
13
13
  const FROM_SOURCE = import.meta.url.endsWith('.ts')
14
14
 
15
- const OPENED_AT = fileURLToPath(new URL(FROM_SOURCE ? '../../operations/operations.ts' : '../../operations/operations.d.ts', import.meta.url))
15
+ const SHAPE = 'operations/operations'
16
+
17
+ const OPENED_AT = fileURLToPath(new URL('../../' + SHAPE + (FROM_SOURCE ? '.ts' : '.d.ts'), import.meta.url))
18
+
19
+ const FRAMEWORK = 'thread'
20
+
21
+ const STATED_AT = resolve(ROOT, FRAMEWORK, SHAPE + '.ts')
22
+
23
+ const SHAPES = [...new Set([STATED_AT, OPENED_AT])]
16
24
 
17
25
 
18
26
  const HANDED = 'Opened'
@@ -184,7 +192,7 @@ function compiled(): Built {
184
192
  if (built !== undefined && built.of === pass) return built
185
193
  const options = configured().options
186
194
  const roots = [THREAD_AT, ...sources()].filter((one) => SOURCE.test(one)).map((one) => resolve(ROOT, one))
187
- const program = ts.createProgram([...new Set([OPENED_AT, ...roots])], options, hosting(options))
195
+ const program = ts.createProgram([...new Set([...SHAPES, ...roots])], options, hosting(options))
188
196
  const checker = program.getTypeChecker()
189
197
  const hands = new Map<ts.Symbol, string>()
190
198
  const own = (symbol: ts.Symbol): boolean =>
@@ -192,8 +200,7 @@ function compiled(): Built {
192
200
  const from = one.getSourceFile()
193
201
  return !program.isSourceFileDefaultLibrary(from) && !program.isSourceFileFromExternalLibrary(from)
194
202
  })
195
- const shape = program.getSourceFile(OPENED_AT)
196
- for (const statement of shape?.statements ?? []) {
203
+ for (const statement of SHAPES.flatMap((one) => program.getSourceFile(one)?.statements ?? [])) {
197
204
  if (!ts.isTypeAliasDeclaration(statement) || statement.name.text !== HANDED) continue
198
205
  const alias = checker.getSymbolAtLocation(statement.name)
199
206
  if (alias !== undefined) hands.set(alias, EVERY)
@@ -400,7 +407,7 @@ export function judging(at: SlopnatorContext, wanted: string, closing: string):
400
407
  const path = here(at.filename)
401
408
  if (!SOURCE.test(path)) return false
402
409
  const held = compiled()
403
- if (held.hands.size === 0 && resolve(ROOT, path) === OPENED_AT) {
410
+ if (held.hands.size === 0 && SHAPES.includes(resolve(ROOT, path))) {
404
411
  return 'no type called ' + HANDED + ' can be read here, so nothing can tell what an operation was opened with from any other value, and a call on it outside an operation goes unmeasured'
405
412
  }
406
413
  const found = judged(path, held, operational(held.of, path), wanted)
@@ -30,7 +30,13 @@ export const slopnator = {
30
30
  "export const TARGETS: readonly Target[] = [{ id: 'cli_bash', of: 'in_shell', describes: 'The shell on the machine a command runs on.', version: 1, takes: [], requires: [] }]",
31
31
  ],
32
32
  right: [
33
- "export const TARGETS: readonly Target[] = [{ id: 'cli_tsc', of: 'in_typescript', describes: 'The compiler this tree is checked and built with.', version: 1, takes: [], requires: [] }]",
33
+ {
34
+ files: [
35
+ { at: 'node_modules/.bin/tsc', source: '' },
36
+ { at: 'threads/_probe_targets.ts', source: "export const TARGETS: readonly Target[] = [{ id: 'cli_tsc', of: 'in_typescript', describes: 'The compiler this tree is checked and built with.', version: 1, takes: [], requires: [] }]\n" },
37
+ ],
38
+ at: 'threads/_probe_targets.ts',
39
+ },
34
40
  "export const TARGETS: readonly Target[] = [{ id: 'cli_packager', of: 'in_packager', describes: 'The package manager the lockfile beside this tree picks.', version: 1, takes: [], requires: [] }]",
35
41
  "export const TARGETS: readonly Target[] = [{ id: 'cli_node', of: 'in_node', describes: 'The runtime this framework runs on.', version: 1, takes: [], requires: [] }]",
36
42
  "export const TARGETS: readonly Target[] = [{ id: 'ht_registry', of: 'in_registry', describes: 'The registry every package goes to.', version: 1, takes: [], requires: [], address: 'https://registry.npmjs.org' }]",
@@ -31,7 +31,13 @@ export const slopnator: Slopnator = {
31
31
  "export const TARGETS: readonly Target[] = [{ id: 'cli_bash', of: 'in_shell', describes: 'The shell on the machine a command runs on.', version: 1, takes: [], requires: [] }]",
32
32
  ],
33
33
  right: [
34
- "export const TARGETS: readonly Target[] = [{ id: 'cli_tsc', of: 'in_typescript', describes: 'The compiler this tree is checked and built with.', version: 1, takes: [], requires: [] }]",
34
+ {
35
+ files: [
36
+ { at: 'node_modules/.bin/tsc', source: '' },
37
+ { at: 'threads/_probe_targets.ts', source: "export const TARGETS: readonly Target[] = [{ id: 'cli_tsc', of: 'in_typescript', describes: 'The compiler this tree is checked and built with.', version: 1, takes: [], requires: [] }]\n" },
38
+ ],
39
+ at: 'threads/_probe_targets.ts',
40
+ },
35
41
  "export const TARGETS: readonly Target[] = [{ id: 'cli_packager', of: 'in_packager', describes: 'The package manager the lockfile beside this tree picks.', version: 1, takes: [], requires: [] }]",
36
42
  "export const TARGETS: readonly Target[] = [{ id: 'cli_node', of: 'in_node', describes: 'The runtime this framework runs on.', version: 1, takes: [], requires: [] }]",
37
43
  "export const TARGETS: readonly Target[] = [{ id: 'ht_registry', of: 'in_registry', describes: 'The registry every package goes to.', version: 1, takes: [], requires: [], address: 'https://registry.npmjs.org' }]",
@@ -7,7 +7,6 @@ const TREE = TREE_AT.name;
7
7
  const THREAD_AT = `${TREE_AT.name}/thread.ts`;
8
8
  const FOLLOWABLE = /\.(ts|tsx|mts|cts|mjs|cjs|js)$/;
9
9
  const LISTS = new Set(LINEAGE);
10
- let threaders;
11
10
  let tree;
12
11
  const parsed = new Map();
13
12
  const broke = new Map();
@@ -233,7 +232,6 @@ export function index() {
233
232
  export function forget() {
234
233
  parsed.clear();
235
234
  broke.clear();
236
- threaders = undefined;
237
235
  tree = undefined;
238
236
  forgetDescent();
239
237
  }
@@ -1,6 +1,3 @@
1
- import { readFileSync } from '../../integrations/disk.ts'
2
- import { resolve } from 'node:path'
3
-
4
1
  import { parser } from 'typescript-eslint'
5
2
 
6
3
  import type { Slopnator } from '../slopnator.ts'
@@ -15,7 +12,6 @@ const THREAD_AT = `${TREE_AT.name}/thread.ts`
15
12
  const FOLLOWABLE = /\.(ts|tsx|mts|cts|mjs|cjs|js)$/
16
13
  const LISTS: ReadonlySet<string> = new Set(LINEAGE)
17
14
 
18
- let threaders: ReadonlySet<string> | undefined
19
15
  let tree: Index | undefined
20
16
  const parsed = new Map<string, readonly Reach[]>()
21
17
  const broke = new Map<string, string>()
@@ -295,7 +291,6 @@ export function index(): Index {
295
291
  export function forget(): void {
296
292
  parsed.clear()
297
293
  broke.clear()
298
- threaders = undefined
299
294
  tree = undefined
300
295
  forgetDescent()
301
296
  }
@@ -2,7 +2,6 @@ import { starts } from '../integrations/cli.ts'
2
2
  import { attached, existsSync, readFileSync, readdirSync } from '../integrations/disk.ts'
3
3
  import { join } from 'node:path'
4
4
  import { argv, stdout } from 'node:process'
5
- import { pathToFileURL } from 'node:url'
6
5
 
7
6
  import { runChecker, type Finding } from '../core/finding.ts'
8
7
  import { exit, invoked } from '../core/invoked.ts'
@@ -11,7 +10,6 @@ import { parser } from 'typescript-eslint'
11
10
 
12
11
  import { CONSTRAINTS, type Constraint, type Proof } from './constraint.ts'
13
12
  import { NOT_WRITTEN, ROOT, derivedHere, governs, load, proofsOf, reads, rules, slopnatorName, subject, sweeps, visits, type Reading, type Slopnator, type Spoken, type Visiting } from './slopnator.ts'
14
- import { ruleMessage } from './lint.ts'
15
13
  import { crossed, deriveTrials, runTrials } from './generators/trial.ts'
16
14
 
17
15
  const PARSEABLE = /\.(ts|tsx|mts|cts|mjs|cjs|js|jsx)$/