@prestack/thread 0.0.1 → 0.0.2

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 (150) hide show
  1. package/dist/core/actor.ts +20 -0
  2. package/dist/core/asking.d.ts +0 -1
  3. package/dist/core/asking.js +0 -1
  4. package/dist/core/asking.ts +1342 -0
  5. package/dist/core/attach.js +2 -2
  6. package/dist/core/attach.ts +585 -0
  7. package/dist/core/chain.js +8 -1
  8. package/dist/core/chain.ts +148 -0
  9. package/dist/core/cli.js +97 -18
  10. package/dist/core/cli.ts +2004 -0
  11. package/dist/core/condition.ts +227 -0
  12. package/dist/core/descend.d.ts +1 -0
  13. package/dist/core/descend.js +36 -0
  14. package/dist/core/descend.ts +1001 -0
  15. package/dist/core/finding.ts +38 -0
  16. package/dist/core/generators/meta.js +6 -0
  17. package/dist/core/generators/meta.ts +1006 -0
  18. package/dist/core/init.ts +170 -0
  19. package/dist/core/invoked.ts +47 -0
  20. package/dist/core/round.ts +57 -0
  21. package/dist/core/rows.ts +369 -0
  22. package/dist/core/scope.ts +18 -0
  23. package/dist/core/thread.ts +29 -0
  24. package/dist/core/tree.ts +67 -0
  25. package/dist/core/types.ts +350 -0
  26. package/dist/derivation/attach.js +1 -1
  27. package/dist/derivation/attach.ts +68 -0
  28. package/dist/derivation/generators/build.ts +232 -0
  29. package/dist/derivation/generators/check.ts +118 -0
  30. package/dist/derivation/generators/derivation.ts +347 -0
  31. package/dist/derivation/generators/determinism.ts +145 -0
  32. package/dist/derivation/generators/release.js +19 -9
  33. package/dist/derivation/generators/release.ts +213 -0
  34. package/dist/derivation/makers/release.ts +135 -0
  35. package/dist/derivation/release.ts +23 -0
  36. package/dist/integrations/attach.js +2 -2
  37. package/dist/integrations/attach.ts +207 -0
  38. package/dist/integrations/generators/read.ts +127 -0
  39. package/dist/integrations/generators/secret.ts +289 -0
  40. package/dist/integrations/generators/target.js +16 -6
  41. package/dist/integrations/generators/target.ts +944 -0
  42. package/dist/integrations/generators/vault.js +46 -14
  43. package/dist/integrations/generators/vault.ts +212 -0
  44. package/dist/integrations/{sdk.js → module.js} +4 -4
  45. package/dist/integrations/module.ts +189 -0
  46. package/dist/integrations/secret.ts +50 -0
  47. package/dist/integrations/target.d.ts +3 -1
  48. package/dist/integrations/target.js +14 -11
  49. package/dist/integrations/target.ts +340 -0
  50. package/dist/integrations/vault.d.ts +2 -0
  51. package/dist/integrations/vault.js +4 -2
  52. package/dist/integrations/vault.ts +136 -0
  53. package/dist/integrity/attach.ts +234 -0
  54. package/dist/integrity/check-threads.js +2 -26
  55. package/dist/integrity/check-threads.ts +166 -0
  56. package/dist/integrity/constraint.ts +1202 -0
  57. package/dist/integrity/generators/compiler.ts +161 -0
  58. package/dist/integrity/generators/gate.ts +201 -0
  59. package/dist/integrity/generators/issues.ts +160 -0
  60. package/dist/integrity/generators/publish.js +14 -5
  61. package/dist/integrity/generators/publish.ts +247 -0
  62. package/dist/integrity/generators/rule.ts +271 -0
  63. package/dist/integrity/generators/seal.ts +117 -0
  64. package/dist/integrity/generators/trial.ts +320 -0
  65. package/dist/integrity/lint.ts +227 -0
  66. package/dist/integrity/lsp/bin.ts +42 -0
  67. package/dist/integrity/lsp/diagnostics.ts +92 -0
  68. package/dist/integrity/lsp/findings.ts +63 -0
  69. package/dist/integrity/lsp/lint.ts +140 -0
  70. package/dist/integrity/lsp/protocol.ts +78 -0
  71. package/dist/integrity/lsp/server.ts +224 -0
  72. package/dist/integrity/sealed.js +2 -2
  73. package/dist/integrity/sealed.ts +56 -0
  74. package/dist/integrity/slopnator.ts +350 -0
  75. package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
  76. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
  77. package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
  78. package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
  79. package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
  80. package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
  81. package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
  82. package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
  83. package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
  84. package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
  85. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
  86. package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
  87. package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
  88. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
  89. package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
  90. package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
  91. package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
  92. package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
  93. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
  94. package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
  95. package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
  96. package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
  97. package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
  98. package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
  99. package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
  100. package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
  101. package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
  102. package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
  103. package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
  104. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
  105. package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
  106. package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
  107. package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
  108. package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
  109. package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
  110. package/dist/integrity/sweep.ts +307 -0
  111. package/dist/knowledge/attach.ts +350 -0
  112. package/dist/knowledge/generators/docsgen.ts +174 -0
  113. package/dist/knowledge/generators/glossary.ts +73 -0
  114. package/dist/knowledge/generators/skills.ts +263 -0
  115. package/dist/knowledge/glossary.ts +6 -0
  116. package/dist/operations/access.ts +88 -0
  117. package/dist/operations/attach.ts +475 -0
  118. package/dist/operations/capabilities.ts +65 -0
  119. package/dist/operations/cost.ts +58 -0
  120. package/dist/operations/dispatch.ts +348 -0
  121. package/dist/operations/error-results.ts +31 -0
  122. package/dist/operations/errors/catalog.ts +168 -0
  123. package/dist/operations/errors/classes.ts +51 -0
  124. package/dist/operations/errors/errors.ts +314 -0
  125. package/dist/operations/generators/capability.js +29 -19
  126. package/dist/operations/generators/capability.ts +704 -0
  127. package/dist/operations/generators/compile.js +20 -1
  128. package/dist/operations/generators/compile.ts +643 -0
  129. package/dist/operations/generators/grant.ts +336 -0
  130. package/dist/operations/generators/operation.ts +972 -0
  131. package/dist/operations/generators/permits.ts +223 -0
  132. package/dist/operations/generators/seed.ts +347 -0
  133. package/dist/operations/generators/trial.js +4 -1
  134. package/dist/operations/generators/trial.ts +529 -0
  135. package/dist/operations/http.ts +164 -0
  136. package/dist/operations/makers/capability.ts +166 -0
  137. package/dist/operations/makers/trial.ts +520 -0
  138. package/dist/operations/memory.ts +470 -0
  139. package/dist/operations/metering.ts +328 -0
  140. package/dist/operations/nodes.ts +54 -0
  141. package/dist/operations/operations.ts +32 -0
  142. package/dist/operations/queue-policy.ts +10 -0
  143. package/dist/operations/result.ts +28 -0
  144. package/dist/operations/seams.d.ts +4 -4
  145. package/dist/operations/seams.ts +364 -0
  146. package/dist/operations/store.ts +29 -0
  147. package/package.json +8 -1
  148. package/dist/integrations/generators/sdk.d.ts +0 -1
  149. package/dist/integrations/generators/sdk.js +0 -148
  150. /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
@@ -0,0 +1,136 @@
1
+ import { inCode, listBounds, matched } from '../../core/rows.ts'
2
+ import { PREFIXES } from '../../core/thread.ts'
3
+
4
+ import type { Slopnator } from '../slopnator.ts'
5
+ import { here, parses, unreadable } from './sl_unthreaded.ts'
6
+
7
+ const MEMBERS = 'MEMBERS'
8
+
9
+ /*
10
+ * Which prefixes a tree file's member list may hold.
11
+ *
12
+ * The file is the key because the list is named the same in both: `MEMBERS` in the operations tree
13
+ * holds what an app can be asked to do, and `MEMBERS` in the nodes tree holds the spans an operation
14
+ * is built from. Nothing in TypeScript separates them — both are arrays of rows carrying a string id
15
+ * — so an operation declared among the nodes, or a node among the operations, typechecks, attaches,
16
+ * seeds a body and reads as agreed by everything downstream.
17
+ *
18
+ * What an operation may wear is not written here. `@thread/core/thread` pairs each word an
19
+ * operation can be with the prefix its id takes, and `thread create` puts that prefix on — so the
20
+ * command that makes them and the law that refuses them read one list. Written out again here,
21
+ * a kind added over there would be a kind this refuses.
22
+ */
23
+ const TAKES: Readonly<Record<string, readonly string[]>> = {
24
+ 'operations.ts': Object.values(PREFIXES),
25
+ 'nodes.ts': ['nd_', 'sm_'],
26
+ }
27
+
28
+ const ID = /\bid:\s*'((?:[^'\\]|\\.)*)'/
29
+
30
+ /*
31
+ * The id of every row sitting directly in the list, and no deeper.
32
+ *
33
+ * Depth is the whole of it: a member row states its own id, and the `seeds` and `derives` rows under
34
+ * it state theirs, which are neither operations nor nodes — `wf_send_invoice_body` is a body, not
35
+ * something an app can be asked to do. Reading every id in the list would refuse those children for
36
+ * lacking a prefix they were never meant to wear.
37
+ *
38
+ * The braces are read off `inCode`, not off the raw text. Counting characters directly desynced on a
39
+ * `describes:` holding a brace, and this package already ships the mask and the matcher for exactly
40
+ * that — a second, weaker walker beside them is the drift they exist to prevent.
41
+ */
42
+ function directly(text: string, open: number, close: number): readonly string[] {
43
+ const code = inCode(text)
44
+ const found: string[] = []
45
+ for (let at = open; at < close; at += 1) {
46
+ if (code[at] !== true || text[at] !== '{') continue
47
+ const ends = matched(text, code, at, '{', '}')
48
+ if (ends === -1 || ends > close) break
49
+ const said = ID.exec(text.slice(at, ends))?.[1]
50
+ if (said !== undefined) found.push(said)
51
+ at = ends
52
+ }
53
+ return found
54
+ }
55
+
56
+ export const slopnator: Slopnator = {
57
+ holds: (_node, at) => {
58
+ const path = here(at.filename)
59
+ if (unreadable(path, at.text) !== undefined) return false
60
+ if (!parses(path)) return false
61
+
62
+ const takes = TAKES[path.slice(path.lastIndexOf('/') + 1)]
63
+ if (takes === undefined) return false
64
+
65
+ const text = String(at.text)
66
+ const where = listBounds(text, MEMBERS)
67
+ if (where === undefined) return false
68
+
69
+ for (const said of directly(text, where.open, where.close)) {
70
+ if (takes.some((one) => said.startsWith(one))) continue
71
+ return (
72
+ `\`${said}\` sits in \`${MEMBERS}\` here, and this list takes ${takes.map((one) => `\`${one}\``).join(', ')}. ` +
73
+ 'A prefix is what says which of the two a row is, and a row wearing the wrong one is declared as ' +
74
+ 'something it is not, seeds a body of the wrong shape, and is read as agreed by everything downstream'
75
+ )
76
+ }
77
+
78
+ return false
79
+ },
80
+ proves: {
81
+ wrong: [
82
+ {
83
+ files: [
84
+ {
85
+ at: '_threads/operations.ts',
86
+ source:
87
+ "import type { Member, Threaded } from '../../core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n at: '_threads/operations.ts',\n describes: 'what an app can be asked to do',\n version: 1,\n}\n\nexport const MEMBERS: readonly Member[] = [\n {\n id: 'nd_teleport',\n at: '_threads/operations.ts',\n describes: 'a node wearing an operation list',\n version: 1,\n },\n]\n",
88
+ },
89
+ ],
90
+ at: '_threads/operations.ts',
91
+ },
92
+ {
93
+ files: [
94
+ {
95
+ at: '_threads/nodes.ts',
96
+ source:
97
+ "import type { Member, Threaded } from '../../core/thread.ts'\n\nexport const NODES: Threaded = {\n id: 'nodes',\n at: '_threads/nodes.ts',\n describes: 'the spans an operation is built from',\n version: 1,\n}\n\nexport const MEMBERS: readonly Member[] = [\n {\n id: 'wf_send_invoice',\n at: '_threads/nodes.ts',\n describes: 'an operation wearing a node list',\n version: 1,\n },\n]\n",
98
+ },
99
+ ],
100
+ at: '_threads/nodes.ts',
101
+ },
102
+ {
103
+ files: [
104
+ {
105
+ at: '_threads/operations.ts',
106
+ source:
107
+ "import type { Member, Threaded } from '../../core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n at: '_threads/operations.ts',\n describes: 'what an app can be asked to do',\n version: 1,\n}\n\nexport const MEMBERS: readonly Member[] = [\n {\n id: 'sendInvoice',\n at: '_threads/operations.ts',\n describes: 'no prefix at all',\n version: 1,\n },\n]\n",
108
+ },
109
+ ],
110
+ at: '_threads/operations.ts',
111
+ },
112
+ ],
113
+ right: [
114
+ {
115
+ files: [
116
+ {
117
+ at: '_threads/operations.ts',
118
+ source:
119
+ "import type { Member, Threaded } from '../../core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n at: '_threads/operations.ts',\n describes: 'what an app can be asked to do',\n version: 1,\n}\n\nexport const MEMBERS: readonly Member[] = [\n {\n id: 'wf_send_invoice',\n at: '_threads/operations.ts',\n describes: 'sends an invoice',\n version: 1,\n seeds: [\n {\n id: 'wf_send_invoice_body',\n at: 'operations/wf_send_invoice.ts',\n describes: 'the operation itself',\n },\n ],\n },\n]\n",
120
+ },
121
+ ],
122
+ at: '_threads/operations.ts',
123
+ },
124
+ {
125
+ files: [
126
+ {
127
+ at: '_threads/nodes.ts',
128
+ source:
129
+ "import type { Member, Threaded } from '../../core/thread.ts'\n\nexport const NODES: Threaded = {\n id: 'nodes',\n at: '_threads/nodes.ts',\n describes: 'the spans an operation is built from',\n version: 1,\n}\n\nexport const MEMBERS: readonly Member[] = [\n {\n id: 'nd_teleport',\n at: '_threads/nodes.ts',\n describes: 'a step this app added',\n version: 1,\n },\n]\n",
130
+ },
131
+ ],
132
+ at: '_threads/nodes.ts',
133
+ },
134
+ ],
135
+ },
136
+ }
@@ -0,0 +1,79 @@
1
+ import { isBuiltin } from 'node:module'
2
+
3
+ import { parser } from 'typescript-eslint'
4
+
5
+ import type { Slopnator } from '../slopnator.ts'
6
+
7
+ const CODE = /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$/
8
+
9
+ const MENTIONS = /\bimport\b|\brequire\s*\(/
10
+
11
+ function reached(node: unknown, seen: WeakSet<object>, found: (one: string) => void): void {
12
+ if (typeof node !== 'object' || node === null) return
13
+ if (seen.has(node)) return
14
+ seen.add(node)
15
+
16
+ if (Array.isArray(node)) {
17
+ for (const each of node) reached(each, seen, found)
18
+ return
19
+ }
20
+
21
+ const one = node as Record<string, unknown>
22
+ const type = one['type']
23
+
24
+ if (type === 'ImportDeclaration' || type === 'ImportExpression' || type === 'ExportNamedDeclaration' || type === 'ExportAllDeclaration') {
25
+ const source = one['source'] as { readonly value?: unknown } | null | undefined
26
+ if (typeof source?.value === 'string') found(source.value)
27
+ }
28
+
29
+ if (type === 'CallExpression') {
30
+ const callee = one['callee'] as { readonly type?: unknown; readonly name?: unknown } | null | undefined
31
+ if (callee?.type === 'Identifier' && callee.name === 'require') {
32
+ const first = (one['arguments'] as readonly { readonly value?: unknown }[] | undefined)?.[0]
33
+ if (typeof first?.value === 'string') found(first.value)
34
+ }
35
+ }
36
+
37
+ for (const held of Object.values(one)) reached(held, seen, found)
38
+ }
39
+
40
+ export const slopnator: Slopnator = {
41
+ holds: (_node, at) => {
42
+ if (!CODE.test(at.filename)) return false
43
+
44
+ const text = String(at.text)
45
+ if (!MENTIONS.test(text)) return false
46
+
47
+ let ast: unknown
48
+ try {
49
+ ast = (parser.parseForESLint as (text: string, options: unknown) => { readonly ast: unknown })(text, {
50
+ range: true,
51
+ ecmaVersion: 'latest',
52
+ sourceType: 'module',
53
+ ecmaFeatures: { jsx: true },
54
+ }).ast
55
+ } catch {
56
+ return false
57
+ }
58
+
59
+ const taken = new Set<string>()
60
+ reached(ast, new WeakSet<object>(), (one) => {
61
+ if (isBuiltin(one)) taken.add(one)
62
+ })
63
+
64
+ if (taken.size === 0) return false
65
+
66
+ return `this reaches ${[...taken].sort().join(', ')} straight from the runtime, and a node is handed everything it may touch`
67
+ },
68
+ proves: {
69
+ wrong: [
70
+ "import { readFileSync } from 'node:fs'\nexport const node = readFileSync\n",
71
+ "const { spawn } = require('child_process')\nexport const node = spawn\n",
72
+ "export const node = async () => await import('node:net')\n",
73
+ ],
74
+ right: [
75
+ "import { nd_read } from '../../operations/nodes.ts'\nexport const node = nd_read(async (hands) => await hands.store.get('a', 'b'))\n",
76
+ "import type { In } from './input.ts'\nexport const node = null as unknown as In\n",
77
+ ],
78
+ },
79
+ }
@@ -0,0 +1,419 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+
4
+ import { parser } from 'typescript-eslint'
5
+
6
+ import type { Slopnator } from '../slopnator.ts'
7
+
8
+ import { descend, forget as forgetDescent, read, reached as fromThread, threading } from '../../core/descend.ts'
9
+ import { findTree } from '../../core/tree.ts'
10
+
11
+ const TREE_AT = findTree()
12
+ const ROOT = `${TREE_AT.at.split('\\').join('/')}/`
13
+ const TREE = TREE_AT.name
14
+ const THREAD_AT = `${TREE_AT.name}/thread.ts`
15
+ const FOLLOWABLE = /\.(ts|tsx|mts|cts|mjs|cjs|js)$/
16
+ const LISTS = new Set(['declares', 'derives', 'seeds'])
17
+
18
+ let threaders: ReadonlySet<string> | undefined
19
+ let tree: Index | undefined
20
+ const parsed = new Map<string, readonly Reach[]>()
21
+ const broke = new Map<string, string>()
22
+
23
+ export interface Reach {
24
+ readonly list?: 'declares' | 'derives' | 'seeds'
25
+ readonly at?: string
26
+ readonly id?: string
27
+ readonly describes?: string
28
+ readonly version?: number
29
+ readonly governs?: readonly string[]
30
+ readonly wears?: readonly string[]
31
+ }
32
+
33
+ export interface Meaning {
34
+ readonly id: string
35
+ readonly describes: string
36
+ }
37
+
38
+ const SAID = new Set(['at', 'id', 'describes'])
39
+
40
+ interface Held {
41
+ readonly type?: string
42
+ readonly kind?: string
43
+ readonly name?: string
44
+ readonly value?: unknown
45
+ readonly key?: Held
46
+ readonly computed?: boolean
47
+ readonly id?: Held
48
+ readonly typeAnnotation?: Held
49
+ readonly typeName?: Held
50
+ readonly elementType?: Held
51
+ readonly properties?: readonly Held[]
52
+ readonly elements?: readonly (Held | null)[]
53
+ readonly declarations?: readonly Held[]
54
+ readonly body?: readonly Held[]
55
+ readonly declaration?: Held
56
+ readonly init?: Held
57
+ }
58
+
59
+ const keyOf = (property: Held): string | undefined => {
60
+ if (property.computed === true) return undefined
61
+ const key = property.key
62
+ if (key?.type === 'Identifier') return key.name
63
+ if (key?.type === 'Literal' && typeof key.value === 'string') return key.value
64
+ return undefined
65
+ }
66
+
67
+ function annotated(node: Held): string | undefined {
68
+ let held = node.id?.typeAnnotation?.typeAnnotation
69
+ while (held !== undefined && (held.type === 'TSArrayType' || held.type === 'TSTypeOperator')) {
70
+ held = held.elementType ?? held.typeAnnotation
71
+ }
72
+ return held?.typeName?.name
73
+ }
74
+
75
+ export const here = (filename: string): string => {
76
+ const path = filename.split('\\').join('/')
77
+ return path.startsWith(ROOT) ? path.slice(ROOT.length) : path
78
+ }
79
+
80
+
81
+
82
+ function rows(node: Held, list: Reach['list'], found: Reach[], hoisted: ReadonlyMap<string, Held>): void {
83
+ for (const one of node.elements ?? []) {
84
+ const element = one?.type === 'Identifier' && one.name !== undefined ? hoisted.get(one.name) : one
85
+ if (element === null || element === undefined || element.type !== 'ObjectExpression') continue
86
+ const said: { at?: string; id?: string; describes?: string; governs?: readonly string[]; wears?: readonly string[] } = {}
87
+ for (const property of element.properties ?? []) {
88
+ const key = keyOf(property)
89
+ const held = property.value as Held | undefined
90
+ if (key !== undefined && SAID.has(key) && held?.type === 'Literal' && typeof held.value === 'string') {
91
+ said[key as 'at' | 'id' | 'describes'] = held.value
92
+ }
93
+ if ((key === 'governs' || key === 'wears') && held?.type === 'ArrayExpression') {
94
+ said[key] = (held.elements ?? [])
95
+ .map((one) => (one?.type === 'Literal' && typeof one.value === 'string' ? one.value : undefined))
96
+ .filter((one): one is string => one !== undefined)
97
+ }
98
+ if (key !== undefined && LISTS.has(key) && held?.type === 'ArrayExpression') rows(held, key as Reach['list'], found, hoisted)
99
+ }
100
+ if (said.at !== undefined || said.id !== undefined) found.push({ list, ...said })
101
+ }
102
+ }
103
+
104
+ function reaching(text: string): readonly Reach[] {
105
+ const already = parsed.get(text)
106
+ if (already !== undefined) return already
107
+
108
+ const types = threading()
109
+ const found: Reach[] = []
110
+ const parse = (jsx: boolean): { ast: unknown } =>
111
+ (parser.parseForESLint as (t: string, o: unknown) => { ast: unknown })(text, {
112
+ range: true,
113
+ ecmaVersion: 'latest',
114
+ sourceType: 'module',
115
+ ecmaFeatures: { jsx },
116
+ })
117
+
118
+ const read = ((): { ast: unknown } | undefined => {
119
+ try {
120
+ return parse(true)
121
+ } catch {
122
+ try {
123
+ return parse(false)
124
+ } catch (error) {
125
+ broke.set(text, error instanceof Error ? (error.message.split('\n')[0] ?? error.message) : String(error))
126
+ return undefined
127
+ }
128
+ }
129
+ })()
130
+ if (read === undefined) {
131
+ parsed.set(text, found)
132
+ return found
133
+ }
134
+ const body = (read.ast as unknown as Held).body ?? []
135
+ const hoisted = new Map<string, Held>()
136
+ for (const statement of body) {
137
+ const held = statement.declaration ?? statement
138
+ if (held.type !== 'VariableDeclaration') continue
139
+ for (const one of held.declarations ?? []) {
140
+ const name = one.id?.name
141
+ if (name !== undefined && one.init?.type === 'ObjectExpression') hoisted.set(name, one.init)
142
+ }
143
+ }
144
+
145
+ for (const statement of body) {
146
+ const held = statement.declaration ?? statement
147
+ if (held.type !== 'VariableDeclaration' || held.kind !== 'const') continue
148
+ for (const one of held.declarations ?? []) {
149
+ if (!types.has(annotated(one) ?? '')) continue
150
+ const init = one.init
151
+ for (const node of init?.type === 'ArrayExpression' ? (init.elements ?? []) : [init]) {
152
+ if (node === null || node === undefined || node.type !== 'ObjectExpression') continue
153
+ const said: { id?: string; describes?: string; version?: number } = {}
154
+ for (const property of node.properties ?? []) {
155
+ const key = keyOf(property)
156
+ const value = property.value as Held | undefined
157
+ if ((key === 'id' || key === 'describes') && value?.type === 'Literal' && typeof value.value === 'string') said[key] = value.value
158
+ if (key === 'version' && value?.type === 'Literal' && typeof value.value === 'number') said.version = value.value
159
+ if (key !== undefined && LISTS.has(key) && value?.type === 'ArrayExpression') rows(value, key as Reach['list'], found, hoisted)
160
+ }
161
+ if (said.id !== undefined) found.push({ ...said })
162
+ }
163
+ }
164
+ }
165
+
166
+ parsed.set(text, found)
167
+ return found
168
+ }
169
+
170
+ export function atsIn(text: string, only?: ReadonlySet<string>): readonly string[] {
171
+ const found: string[] = []
172
+ for (const one of reaching(text)) {
173
+ if (only !== undefined && (one.list === undefined || !only.has(one.list))) continue
174
+ if (one.at !== undefined) found.push(one.at)
175
+ }
176
+ return found
177
+ }
178
+
179
+ export const rowsIn = (text: string): readonly Reach[] => reaching(text)
180
+
181
+ const SCHEMA = /^export const ([A-Za-z]+)IslandSchema\b/m
182
+ const DEFINE = /^export function (define[A-Za-z]+)\s*\(/m
183
+
184
+ export type Governed =
185
+ | { readonly kind: 'file'; readonly at: string }
186
+ | { readonly kind: 'folder'; readonly under: string }
187
+ | { readonly kind: 'pattern'; readonly matching: string; readonly under: string }
188
+
189
+ export function governedBy(one: string): Governed {
190
+ if (one.includes('*')) {
191
+ const cut = one.lastIndexOf('/', one.indexOf('*'))
192
+ return { kind: 'pattern', matching: one, under: cut === -1 ? '' : one.slice(0, cut + 1) }
193
+ }
194
+ if (one.endsWith('/')) return { kind: 'folder', under: one }
195
+ return { kind: 'file', at: one }
196
+ }
197
+
198
+ export const filesGoverned = (all: readonly string[]): readonly string[] =>
199
+ all.map(governedBy).flatMap((one) => (one.kind === 'file' ? [one.at] : []))
200
+
201
+ export const foldersGoverned = (all: readonly string[]): readonly string[] =>
202
+ all.map(governedBy).flatMap((one) => (one.kind === 'file' ? [] : [one.under]))
203
+
204
+ export function governs(all: readonly string[], path: string): boolean {
205
+ return all.map(governedBy).some((one) => {
206
+ if (one.kind === 'file') return path === one.at
207
+ if (one.kind === 'folder') return path.startsWith(one.under)
208
+ const tail = one.matching.slice(one.under.length).split('*').filter(Boolean)
209
+ const rest = path.startsWith(one.under) ? path.slice(one.under.length) : undefined
210
+ return rest !== undefined && tail.every((part) => rest.includes(part))
211
+ })
212
+ }
213
+
214
+ export interface Declared {
215
+ readonly id: string
216
+ readonly at: string
217
+ readonly describes: string
218
+ readonly governs: readonly string[]
219
+ readonly define?: string
220
+ readonly tree?: string
221
+ readonly member?: string
222
+ }
223
+
224
+ export function islandsDeclared(): readonly Declared[] {
225
+ const found: Declared[] = []
226
+ const seen = new Set<string>()
227
+ const all: Reach[] = []
228
+
229
+ for (const at of fromThread()) {
230
+ const held = read(at)
231
+ if (held === undefined) continue
232
+ if (unreadable(at, held) !== undefined) continue
233
+ all.push(...reaching(held))
234
+ }
235
+
236
+ for (const row of all) {
237
+ if (row.id === undefined || row.at === undefined || seen.has(row.at)) continue
238
+ const source = read(row.at)
239
+ if (source === undefined || !SCHEMA.test(source)) continue
240
+ seen.add(row.at)
241
+
242
+ const tree = `${TREE}/${row.id}.ts`
243
+ const named = all.find((one) => one.at === tree)?.id
244
+
245
+ found.push({
246
+ id: row.id,
247
+ at: row.at,
248
+ describes: row.describes ?? '',
249
+ governs: row.governs ?? [],
250
+ define: DEFINE.exec(source)?.[1],
251
+ ...(named === undefined ? {} : { tree, member: named }),
252
+ })
253
+ }
254
+
255
+ return found
256
+ }
257
+
258
+ export const declaresIsland = (text: string): string | undefined => SCHEMA.exec(text)?.[1]
259
+
260
+ export function meaningsIn(text: string): readonly Meaning[] {
261
+ const found: Meaning[] = []
262
+ for (const one of reaching(text)) {
263
+ if (one.id === undefined) continue
264
+ found.push({ id: one.id, describes: one.describes ?? '' })
265
+ }
266
+ return found
267
+ }
268
+
269
+ export interface Index {
270
+ readonly reached: ReadonlySet<string>
271
+ readonly declared: ReadonlyMap<string, number>
272
+ readonly meanings: ReadonlyMap<string, ReadonlySet<string>>
273
+ }
274
+
275
+
276
+ function descended(): Index {
277
+ const pass = descend()
278
+ const reached = new Set<string>([THREAD_AT, ...pass.threaded.keys()])
279
+ const declared = new Map<string, number>()
280
+ for (const [at, held] of pass.threaded) declared.set(at, held.filter((one) => one.list === 'declares').length)
281
+ const meanings = new Map<string, ReadonlySet<string>>()
282
+ for (const [word, held] of pass.coined) meanings.set(word, new Set(held.said))
283
+ return { reached, declared, meanings }
284
+ }
285
+
286
+ export function index(): Index {
287
+ tree ??= descended()
288
+ return tree
289
+ }
290
+
291
+ export function forget(): void {
292
+ parsed.clear()
293
+ broke.clear()
294
+ threaders = undefined
295
+ tree = undefined
296
+ forgetDescent()
297
+ }
298
+
299
+ export const threaded = (): ReadonlySet<string> => index().reached
300
+
301
+ export const parses = (path: string): boolean => FOLLOWABLE.test(path)
302
+
303
+ export function unreadable(path: string, text: string): string | undefined {
304
+ if (!parses(path)) return undefined
305
+ atsIn(text)
306
+ const said = broke.get(text)
307
+ if (said === undefined) return undefined
308
+ return `this file cannot be parsed, so no law that reads its structure measured anything on it — ${said}`
309
+ }
310
+
311
+ export const names = (path: string, text: string): readonly string[] => (parses(path) ? atsIn(text) : [])
312
+
313
+ export const slopnator: Slopnator = {
314
+ holds: (_node, at) => {
315
+ const path = here(at.filename)
316
+ if (path === THREAD_AT) return false
317
+ const why = unreadable(path, at.text)
318
+ if (why !== undefined) return why
319
+ const pass = descend()
320
+ if (pass.threaded.has(path)) return false
321
+ if (names(path, at.text).length > 0) return false
322
+ const by = pass.mentioned.get(path) ?? []
323
+ const cut = by.filter((one) => one !== THREAD_AT && !pass.threaded.has(one))
324
+ if (cut.length > 0) {
325
+ return `broken thread — ${cut.join(' and ')} names ${path}, and nothing declares ${cut.length === 1 ? 'it' : 'them'}, so the descent from ${THREAD_AT} never arrives`
326
+ }
327
+ if (by.length > 0) {
328
+ return `broken thread — ${by.join(' and ')} names ${path} and is itself declared, and the descent still never arrived, so the row was read and never walked`
329
+ }
330
+ return `nothing that traces back to ${TREE}/ names ${path}, so the descent from ${THREAD_AT} never reaches it`
331
+ },
332
+ proves: {
333
+ wrong: [
334
+ 'export const ORPHAN = 2',
335
+ 'export const LOOSE = { id: 2 }',
336
+ {
337
+ files: [
338
+ { at: 'threads/_names_it.ts', source: "export const OWNER = {\n declares: [\n {\n at: 'scripts/_probe_bare.ts',\n },\n ],\n}\n" },
339
+ { at: 'scripts/_probe_bare.ts', source: 'export const CHILD = 1\n' },
340
+ ],
341
+ at: 'scripts/_probe_bare.ts',
342
+ },
343
+ {
344
+ files: [
345
+ {
346
+ at: 'threads/_names_it.ts',
347
+ source:
348
+ "export const REAL: Threaded = {\n declares: [\n {\n at: 'threads/_names_it.ts',\n },\n ],\n}\n\nexport const BARE = {\n declares: [\n {\n at: 'scripts/_probe_after.ts',\n },\n ],\n}\n",
349
+ },
350
+ { at: 'scripts/_probe_after.ts', source: 'export const CHILD = 1\n' },
351
+ ],
352
+ at: 'scripts/_probe_after.ts',
353
+ },
354
+ {
355
+ files: [
356
+ { at: 'threads/_names_it.ts', source: "export let LOOSE = {\n declares: [\n {\n at: 'scripts/_probe_let.ts',\n },\n ],\n}\n" },
357
+ { at: 'scripts/_probe_let.ts', source: 'export const CHILD = 1\n' },
358
+ ],
359
+ at: 'scripts/_probe_let.ts',
360
+ },
361
+ {
362
+ files: [
363
+ { at: 'threads/_names_it.ts', source: "export var LOOSE = {\n declares: [\n {\n at: 'scripts/_probe_var.ts',\n },\n ],\n}\n" },
364
+ { at: 'scripts/_probe_var.ts', source: 'export const CHILD = 1\n' },
365
+ ],
366
+ at: 'scripts/_probe_var.ts',
367
+ },
368
+ {
369
+ files: [
370
+ { at: 'threads/_names_it.ts', source: "export default {\n declares: [\n {\n at: 'scripts/_probe_default.ts',\n },\n ],\n}\n" },
371
+ { at: 'scripts/_probe_default.ts', source: 'export const CHILD = 1\n' },
372
+ ],
373
+ at: 'scripts/_probe_default.ts',
374
+ },
375
+ {
376
+ files: [
377
+ { at: 'threads/_names_it.ts', source: "export function held() {\n return {\n declares: [\n {\n at: 'scripts/_probe_fn.ts',\n },\n ],\n }\n}\n" },
378
+ { at: 'scripts/_probe_fn.ts', source: 'export const CHILD = 1\n' },
379
+ ],
380
+ at: 'scripts/_probe_fn.ts',
381
+ },
382
+ {
383
+ files: [
384
+ { at: 'threads/_names_it.ts', source: "export class Held {\n held = {\n declares: [\n {\n at: 'scripts/_probe_class.ts',\n },\n ],\n }\n}\n" },
385
+ { at: 'scripts/_probe_class.ts', source: 'export const CHILD = 1\n' },
386
+ ],
387
+ at: 'scripts/_probe_class.ts',
388
+ },
389
+ {
390
+ files: [
391
+ {
392
+ at: 'threads/_names_it.ts',
393
+ source: "interface Held { declares: unknown[] }\nexport const FAKE: Held = {\n declares: [\n {\n at: 'scripts/_probe_fake.ts',\n },\n ],\n}\n",
394
+ },
395
+ { at: 'scripts/_probe_fake.ts', source: 'export const CHILD = 1\n' },
396
+ ],
397
+ at: 'scripts/_probe_fake.ts',
398
+ },
399
+ ],
400
+ right: [
401
+ "export const HELD: Threaded = {\n declares: [\n {\n at: 'threads/probe.tsx',\n },\n ],\n}",
402
+ {
403
+ files: [
404
+ { at: 'threads/_names_it.ts', source: "export const OWNER: Threaded = {\n declares: [\n {\n at: 'scripts/_probe_child.ts',\n },\n ],\n}\n" },
405
+ { at: 'scripts/_probe_child.ts', source: 'export const CHILD = 1\n' },
406
+ ],
407
+ at: 'scripts/_probe_child.ts',
408
+ },
409
+ {
410
+ files: [
411
+ { at: 'threads/_names_it.ts', source: "export const OWNER: Threaded = {\n declares: [\n {\n at: 'scripts/_probe_parent.ts',\n },\n ],\n}\n" },
412
+ { at: 'scripts/_probe_parent.ts', source: "export const MID: Threaded = {\n declares: [\n {\n at: 'scripts/_probe_leaf.ts',\n },\n ],\n}\n" },
413
+ { at: 'scripts/_probe_leaf.ts', source: 'export const LEAF = 1\n' },
414
+ ],
415
+ at: 'scripts/_probe_leaf.ts',
416
+ },
417
+ ],
418
+ },
419
+ }