@prestack/thread 0.1.21 → 0.1.22

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 (137) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/dist/core/attach.js +21 -0
  3. package/dist/core/descend.js +6 -3
  4. package/dist/core/generators/commit.js +55 -3
  5. package/dist/core/generators/dev.d.ts +1 -0
  6. package/dist/core/generators/dev.js +68 -0
  7. package/dist/core/tree.d.ts +1 -0
  8. package/dist/core/tree.js +7 -1
  9. package/dist/derivation/generators/build.d.ts +3 -1
  10. package/dist/derivation/generators/build.js +103 -22
  11. package/dist/derivation/generators/release.js +74 -26
  12. package/dist/derivation/release.js +3 -3
  13. package/dist/integrations/cli.js +69 -10
  14. package/dist/integrations/pglite.js +13 -3
  15. package/dist/integrations/target.d.ts +3 -0
  16. package/dist/integrity/attach.js +1 -5
  17. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  18. package/dist/operations/access.js +3 -2
  19. package/dist/operations/capabilities.d.ts +2 -0
  20. package/dist/operations/capabilities.js +2 -0
  21. package/dist/operations/generators/capability.js +234 -24
  22. package/dist/operations/generators/compile.js +150 -50
  23. package/dist/operations/generators/seed.d.ts +3 -0
  24. package/dist/operations/generators/seed.js +25 -3
  25. package/dist/operations/nodes.d.ts +2 -2
  26. package/package.json +1 -1
  27. package/dist/core/actor.ts +0 -21
  28. package/dist/core/attach.ts +0 -613
  29. package/dist/core/chain.ts +0 -145
  30. package/dist/core/condition.ts +0 -227
  31. package/dist/core/descend.ts +0 -1185
  32. package/dist/core/errors/catalogue.ts +0 -314
  33. package/dist/core/errors/classes.ts +0 -51
  34. package/dist/core/errors/error.ts +0 -152
  35. package/dist/core/errors/namespaces.ts +0 -19
  36. package/dist/core/finding.ts +0 -37
  37. package/dist/core/generators/commit.ts +0 -157
  38. package/dist/core/init.ts +0 -222
  39. package/dist/core/invoked.ts +0 -52
  40. package/dist/core/round.ts +0 -74
  41. package/dist/core/rows.ts +0 -506
  42. package/dist/core/scope.ts +0 -20
  43. package/dist/core/thread.ts +0 -29
  44. package/dist/core/tree.ts +0 -90
  45. package/dist/core/types.ts +0 -351
  46. package/dist/derivation/attach.ts +0 -57
  47. package/dist/derivation/generators/build.ts +0 -371
  48. package/dist/derivation/generators/check.ts +0 -118
  49. package/dist/derivation/generators/determinism.ts +0 -145
  50. package/dist/derivation/generators/release.ts +0 -236
  51. package/dist/derivation/release.ts +0 -43
  52. package/dist/integrations/attach.ts +0 -187
  53. package/dist/integrations/cli.ts +0 -606
  54. package/dist/integrations/declared.ts +0 -34
  55. package/dist/integrations/disk.ts +0 -466
  56. package/dist/integrations/generators/read.ts +0 -127
  57. package/dist/integrations/generators/vault.ts +0 -210
  58. package/dist/integrations/https.ts +0 -216
  59. package/dist/integrations/pglite.ts +0 -206
  60. package/dist/integrations/secret.ts +0 -54
  61. package/dist/integrations/target.ts +0 -672
  62. package/dist/integrations/vault.ts +0 -148
  63. package/dist/integrity/attach.ts +0 -150
  64. package/dist/integrity/check-threads.ts +0 -166
  65. package/dist/integrity/constraint.ts +0 -1246
  66. package/dist/integrity/generators/compiler.ts +0 -138
  67. package/dist/integrity/generators/gate.ts +0 -227
  68. package/dist/integrity/generators/issues.ts +0 -160
  69. package/dist/integrity/generators/publish.ts +0 -233
  70. package/dist/integrity/generators/trial.ts +0 -359
  71. package/dist/integrity/lint.ts +0 -214
  72. package/dist/integrity/lsp/bin.ts +0 -44
  73. package/dist/integrity/lsp/diagnostics.ts +0 -92
  74. package/dist/integrity/lsp/findings.ts +0 -63
  75. package/dist/integrity/lsp/lint.ts +0 -124
  76. package/dist/integrity/lsp/protocol.ts +0 -78
  77. package/dist/integrity/lsp/server.ts +0 -224
  78. package/dist/integrity/slopnator.ts +0 -324
  79. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  80. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  81. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  82. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  83. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  84. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  85. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  86. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  87. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  88. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  89. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  90. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  91. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  92. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  93. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  94. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  95. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  96. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  97. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  98. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  99. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  100. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  101. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  102. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  103. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  104. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  105. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  106. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  107. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  108. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  109. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  110. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  111. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  112. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  113. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  114. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  115. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  116. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  117. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  118. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  119. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  120. package/dist/integrity/sweep.ts +0 -395
  121. package/dist/knowledge/attach.ts +0 -327
  122. package/dist/knowledge/generators/docsgen.ts +0 -186
  123. package/dist/knowledge/generators/glossary.ts +0 -73
  124. package/dist/knowledge/glossary.ts +0 -6
  125. package/dist/operations/access.ts +0 -131
  126. package/dist/operations/attach.ts +0 -408
  127. package/dist/operations/capabilities.ts +0 -88
  128. package/dist/operations/dispatch.ts +0 -294
  129. package/dist/operations/error-results.ts +0 -31
  130. package/dist/operations/generators/capability.ts +0 -1054
  131. package/dist/operations/generators/compile.ts +0 -701
  132. package/dist/operations/generators/permits.ts +0 -229
  133. package/dist/operations/generators/seed.ts +0 -359
  134. package/dist/operations/generators/trial.ts +0 -666
  135. package/dist/operations/nodes.ts +0 -53
  136. package/dist/operations/operations.ts +0 -91
  137. package/dist/operations/result.ts +0 -28
@@ -1,421 +0,0 @@
1
- import { parser } from 'typescript-eslint'
2
-
3
- import type { Slopnator } from '../slopnator.ts'
4
-
5
- import { LISTS as LINEAGE, descend, forget as forgetDescent, read, reached as fromThread, threading } from '../../core/descend.ts'
6
- import { findTree } from '../../core/tree.ts'
7
-
8
- const TREE_AT = findTree()
9
- const ROOT = `${TREE_AT.at.split('\\').join('/')}/`
10
- const TREE = TREE_AT.name
11
- const THREAD_AT = `${TREE_AT.name}/thread.ts`
12
- const FOLLOWABLE = /\.(ts|tsx|mts|cts|mjs|cjs|js)$/
13
- const LISTS: ReadonlySet<string> = new Set(LINEAGE)
14
-
15
- let tree: Index | undefined
16
- const parsed = new Map<string, readonly Reach[]>()
17
- const broke = new Map<string, string>()
18
-
19
- export interface Reach {
20
- readonly list?: 'declares' | 'derives' | 'seeds'
21
- readonly at?: string
22
- readonly id?: string
23
- readonly describes?: string
24
- readonly version?: number
25
- readonly governs?: readonly string[]
26
- readonly wears?: readonly string[]
27
- }
28
-
29
- export interface Meaning {
30
- readonly id: string
31
- readonly describes: string
32
- }
33
-
34
- interface Held {
35
- readonly type?: string
36
- readonly kind?: string
37
- readonly name?: string
38
- readonly value?: unknown
39
- readonly key?: Held
40
- readonly computed?: boolean
41
- readonly id?: Held
42
- readonly typeAnnotation?: Held
43
- readonly typeName?: Held
44
- readonly elementType?: Held
45
- readonly properties?: readonly Held[]
46
- readonly elements?: readonly (Held | null)[]
47
- readonly declarations?: readonly Held[]
48
- readonly body?: readonly Held[]
49
- readonly declaration?: Held
50
- readonly init?: Held
51
- }
52
-
53
- const keyOf = (property: Held): string | undefined => {
54
- if (property.computed === true) return undefined
55
- const key = property.key
56
- if (key?.type === 'Identifier') return key.name
57
- if (key?.type === 'Literal' && typeof key.value === 'string') return key.value
58
- return undefined
59
- }
60
-
61
- function annotated(node: Held): string | undefined {
62
- let held = node.id?.typeAnnotation?.typeAnnotation
63
- while (held !== undefined && (held.type === 'TSArrayType' || held.type === 'TSTypeOperator')) {
64
- held = held.elementType ?? held.typeAnnotation
65
- }
66
- return held?.typeName?.name
67
- }
68
-
69
- export const here = (filename: string): string => {
70
- const path = filename.split('\\').join('/')
71
- return path.startsWith(ROOT) ? path.slice(ROOT.length) : path
72
- }
73
-
74
-
75
-
76
- function rows(node: Held, list: Reach['list'], found: Reach[], hoisted: ReadonlyMap<string, Held>): void {
77
- for (const one of node.elements ?? []) {
78
- const element = one?.type === 'Identifier' && one.name !== undefined ? hoisted.get(one.name) : one
79
- if (element === null || element === undefined || element.type !== 'ObjectExpression') continue
80
- const strings: Record<string, string> = {}
81
- const said: { at?: string; id?: string; describes?: string; governs?: readonly string[]; wears?: readonly string[] } = {}
82
- for (const property of element.properties ?? []) {
83
- const key = keyOf(property)
84
- const held = property.value as Held | undefined
85
- if (key !== undefined && held?.type === 'Literal' && typeof held.value === 'string') strings[key] = held.value
86
- if ((key === 'governs' || key === 'wears') && held?.type === 'ArrayExpression') {
87
- said[key] = (held.elements ?? [])
88
- .map((one) => (one?.type === 'Literal' && typeof one.value === 'string' ? one.value : undefined))
89
- .filter((one): one is string => one !== undefined)
90
- }
91
- if (key !== undefined && LISTS.has(key) && held?.type === 'ArrayExpression') rows(held, key as Reach['list'], found, hoisted)
92
- }
93
- if (strings['at'] !== undefined) said.at = strings['at']
94
- if (strings['id'] !== undefined) said.id = strings['id']
95
- if (strings['describes'] !== undefined) said.describes = strings['describes']
96
- if (said.at !== undefined || said.id !== undefined) found.push({ list, ...said })
97
- }
98
- }
99
-
100
- function reaching(text: string): readonly Reach[] {
101
- const already = parsed.get(text)
102
- if (already !== undefined) return already
103
-
104
- const types = threading()
105
- const found: Reach[] = []
106
- const parse = (jsx: boolean): { ast: unknown } =>
107
- (parser.parseForESLint as (t: string, o: unknown) => { ast: unknown })(text, {
108
- range: true,
109
- ecmaVersion: 'latest',
110
- sourceType: 'module',
111
- ecmaFeatures: { jsx },
112
- })
113
-
114
- const read = ((): { ast: unknown } | undefined => {
115
- try {
116
- return parse(true)
117
- } catch {
118
- try {
119
- return parse(false)
120
- } catch (error) {
121
- broke.set(text, error instanceof Error ? (error.message.split('\n')[0] ?? error.message) : String(error))
122
- return undefined
123
- }
124
- }
125
- })()
126
- if (read === undefined) {
127
- parsed.set(text, found)
128
- return found
129
- }
130
- const body = (read.ast as unknown as Held).body ?? []
131
- const hoisted = new Map<string, Held>()
132
- for (const statement of body) {
133
- const held = statement.declaration ?? statement
134
- if (held.type !== 'VariableDeclaration') continue
135
- for (const one of held.declarations ?? []) {
136
- const name = one.id?.name
137
- if (name !== undefined && one.init?.type === 'ObjectExpression') hoisted.set(name, one.init)
138
- }
139
- }
140
-
141
- for (const statement of body) {
142
- const held = statement.declaration ?? statement
143
- if (held.type !== 'VariableDeclaration' || held.kind !== 'const') continue
144
- for (const one of held.declarations ?? []) {
145
- if (!types.has(annotated(one) ?? '')) continue
146
- const init = one.init
147
- for (const node of init?.type === 'ArrayExpression' ? (init.elements ?? []) : [init]) {
148
- if (node === null || node === undefined || node.type !== 'ObjectExpression') continue
149
- const said: { id?: string; describes?: string; version?: number } = {}
150
- for (const property of node.properties ?? []) {
151
- const key = keyOf(property)
152
- const value = property.value as Held | undefined
153
- if ((key === 'id' || key === 'describes') && value?.type === 'Literal' && typeof value.value === 'string') said[key] = value.value
154
- if (key === 'version' && value?.type === 'Literal' && typeof value.value === 'number') said.version = value.value
155
- if (key !== undefined && LISTS.has(key) && value?.type === 'ArrayExpression') rows(value, key as Reach['list'], found, hoisted)
156
- }
157
- if (said.id !== undefined) found.push({ ...said })
158
- }
159
- }
160
- }
161
-
162
- parsed.set(text, found)
163
- return found
164
- }
165
-
166
- export function atsIn(text: string, only?: ReadonlySet<string>): readonly string[] {
167
- const found: string[] = []
168
- for (const one of reaching(text)) {
169
- if (only !== undefined && (one.list === undefined || !only.has(one.list))) continue
170
- if (one.at !== undefined) found.push(one.at)
171
- }
172
- return found
173
- }
174
-
175
- export const rowsIn = (text: string): readonly Reach[] => reaching(text)
176
-
177
- const SCHEMA = /^export const ([A-Za-z]+)IslandSchema\b/m
178
- const DEFINE = /^export function (define[A-Za-z]+)\s*\(/m
179
-
180
- export type Governed =
181
- | { readonly kind: 'file'; readonly at: string }
182
- | { readonly kind: 'folder'; readonly under: string }
183
- | { readonly kind: 'pattern'; readonly matching: string; readonly under: string }
184
-
185
- export function governedBy(one: string): Governed {
186
- if (one.includes('*')) {
187
- const cut = one.lastIndexOf('/', one.indexOf('*'))
188
- return { kind: 'pattern', matching: one, under: cut === -1 ? '' : one.slice(0, cut + 1) }
189
- }
190
- if (one.endsWith('/')) return { kind: 'folder', under: one }
191
- return { kind: 'file', at: one }
192
- }
193
-
194
- export const filesGoverned = (all: readonly string[]): readonly string[] =>
195
- all.map(governedBy).flatMap((one) => (one.kind === 'file' ? [one.at] : []))
196
-
197
- export const foldersGoverned = (all: readonly string[]): readonly string[] =>
198
- all.map(governedBy).flatMap((one) => (one.kind === 'file' ? [] : [one.under]))
199
-
200
- export function governs(all: readonly string[], path: string): boolean {
201
- return all.map(governedBy).some((one) => {
202
- if (one.kind === 'file') return path === one.at
203
- if (one.kind === 'folder') return path.startsWith(one.under)
204
- const tail = one.matching.slice(one.under.length).split('*').filter(Boolean)
205
- const rest = path.startsWith(one.under) ? path.slice(one.under.length) : undefined
206
- return rest !== undefined && tail.every((part) => rest.includes(part))
207
- })
208
- }
209
-
210
- export interface Declared {
211
- readonly id: string
212
- readonly at: string
213
- readonly describes: string
214
- readonly governs: readonly string[]
215
- readonly define?: string
216
- readonly tree?: string
217
- readonly member?: string
218
- }
219
-
220
- export const canonical = (at: string): string => at.replace(/^threads\//, `${TREE}/`)
221
-
222
- export function islandsDeclared(): readonly Declared[] {
223
- const found: Declared[] = []
224
- const seen = new Set<string>()
225
- const all: Reach[] = []
226
-
227
- for (const at of fromThread()) {
228
- const held = read(at)
229
- if (held === undefined) continue
230
- if (unreadable(at, held) !== undefined) continue
231
- all.push(...reaching(held))
232
- }
233
-
234
- for (const row of all) {
235
- if (row.id === undefined || row.at === undefined) continue
236
- const at = canonical(row.at)
237
- if (seen.has(at)) continue
238
- const source = read(at)
239
- if (source === undefined || !SCHEMA.test(source)) continue
240
- seen.add(at)
241
-
242
- const tree = `${TREE}/${row.id}.ts`
243
- const named = all.find((one) => one.at !== undefined && canonical(one.at) === tree)?.id
244
-
245
- found.push({
246
- id: row.id,
247
- 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
- tree = undefined
295
- forgetDescent()
296
- }
297
-
298
- export const threaded = (): ReadonlySet<string> => index().reached
299
-
300
- export const parses = (path: string): boolean => FOLLOWABLE.test(path)
301
-
302
- export function unreadable(path: string, text: string): string | undefined {
303
- if (!parses(path)) return undefined
304
- atsIn(text)
305
- const said = broke.get(text)
306
- if (said === undefined) return undefined
307
- return `this file cannot be parsed, so no law that reads its structure measured anything on it — ${said}`
308
- }
309
-
310
- export const names = (path: string, text: string): readonly string[] => (parses(path) ? atsIn(text) : [])
311
-
312
- export const slopnator: Slopnator = {
313
- holds: (_node, at) => {
314
- const path = here(at.filename)
315
- if (path === THREAD_AT) return false
316
- const why = unreadable(path, at.text)
317
- if (why !== undefined) return why
318
- const pass = descend()
319
- if (pass.threaded.has(path)) return false
320
- if (names(path, at.text).length > 0) return false
321
- const by = pass.mentioned.get(path) ?? []
322
- const cut = by.filter((one) => one !== THREAD_AT && !pass.threaded.has(one))
323
- if (cut.length > 0) {
324
- return `broken thread — ${cut.join(' and ')} names ${path}, and nothing declares ${cut.length === 1 ? 'it' : 'them'}, so the descent from ${THREAD_AT} never arrives`
325
- }
326
- if (by.length > 0) {
327
- 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`
328
- }
329
- return `nothing that traces back to ${TREE}/ names ${path}, so the descent from ${THREAD_AT} never reaches it`
330
- },
331
- proves: {
332
- wrong: [
333
- 'export const ORPHAN = 2',
334
- 'export const LOOSE = { id: 2 }',
335
- {
336
- files: [
337
- { at: 'threads/_names_it.ts', source: "export const OWNER = {\n declares: [\n {\n at: 'scripts/_probe_bare.ts',\n },\n ],\n}\n" },
338
- { at: 'scripts/_probe_bare.ts', source: 'export const CHILD = 1\n' },
339
- ],
340
- at: 'scripts/_probe_bare.ts',
341
- },
342
- {
343
- files: [
344
- {
345
- at: 'threads/_names_it.ts',
346
- source:
347
- "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",
348
- },
349
- { at: 'scripts/_probe_after.ts', source: 'export const CHILD = 1\n' },
350
- ],
351
- at: 'scripts/_probe_after.ts',
352
- },
353
- {
354
- files: [
355
- { at: 'threads/_names_it.ts', source: "export let LOOSE = {\n declares: [\n {\n at: 'scripts/_probe_let.ts',\n },\n ],\n}\n" },
356
- { at: 'scripts/_probe_let.ts', source: 'export const CHILD = 1\n' },
357
- ],
358
- at: 'scripts/_probe_let.ts',
359
- },
360
- {
361
- files: [
362
- { at: 'threads/_names_it.ts', source: "export var LOOSE = {\n declares: [\n {\n at: 'scripts/_probe_var.ts',\n },\n ],\n}\n" },
363
- { at: 'scripts/_probe_var.ts', source: 'export const CHILD = 1\n' },
364
- ],
365
- at: 'scripts/_probe_var.ts',
366
- },
367
- {
368
- files: [
369
- { at: 'threads/_names_it.ts', source: "export default {\n declares: [\n {\n at: 'scripts/_probe_default.ts',\n },\n ],\n}\n" },
370
- { at: 'scripts/_probe_default.ts', source: 'export const CHILD = 1\n' },
371
- ],
372
- at: 'scripts/_probe_default.ts',
373
- },
374
- {
375
- files: [
376
- { 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" },
377
- { at: 'scripts/_probe_fn.ts', source: 'export const CHILD = 1\n' },
378
- ],
379
- at: 'scripts/_probe_fn.ts',
380
- },
381
- {
382
- files: [
383
- { 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" },
384
- { at: 'scripts/_probe_class.ts', source: 'export const CHILD = 1\n' },
385
- ],
386
- at: 'scripts/_probe_class.ts',
387
- },
388
- {
389
- files: [
390
- {
391
- at: 'threads/_names_it.ts',
392
- source: "interface Held { declares: unknown[] }\nexport const FAKE: Held = {\n declares: [\n {\n at: 'scripts/_probe_fake.ts',\n },\n ],\n}\n",
393
- },
394
- { at: 'scripts/_probe_fake.ts', source: 'export const CHILD = 1\n' },
395
- ],
396
- at: 'scripts/_probe_fake.ts',
397
- },
398
- ],
399
- right: [
400
- {
401
- files: [
402
- { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
403
- { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_names_it.ts',\n },\n ],\n}\n" },
404
- { at: 'threads/_names_it.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport 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: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
412
- { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_names_it.ts',\n },\n ],\n}\n" },
413
- { at: 'threads/_names_it.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const OWNER: Threaded = {\n declares: [\n {\n at: 'scripts/_probe_parent.ts',\n },\n ],\n}\n" },
414
- { at: 'scripts/_probe_parent.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const MID: Threaded = {\n declares: [\n {\n at: 'scripts/_probe_leaf.ts',\n },\n ],\n}\n" },
415
- { at: 'scripts/_probe_leaf.ts', source: 'export const LEAF = 1\n' },
416
- ],
417
- at: 'scripts/_probe_leaf.ts',
418
- },
419
- ],
420
- },
421
- }
@@ -1,140 +0,0 @@
1
- import { existsSync, readFileSync, readdirSync } from '../../integrations/disk.ts'
2
- import { join } from 'node:path'
3
-
4
- import type { Slopnator } from '../slopnator.ts'
5
- import { named as packageOf } from '../../core/scope.ts'
6
-
7
- const SKIP = new Set(['node_modules', '.next', '.git', 'dist', '_generated'])
8
-
9
- const CODE = /\.(tsx?|jsx?)$/
10
-
11
- const TYPED = /readonly\s+([A-Za-z][A-Za-z0-9]*)\??\s*:\s*MessageKey\b/g
12
-
13
- function walk(at: string, found: string[] = []): readonly string[] {
14
- let entries: readonly { name: string; isDirectory: () => boolean }[]
15
- try {
16
- entries = readdirSync(at, { withFileTypes: true })
17
- } catch {
18
- return found
19
- }
20
- for (const entry of entries) {
21
- if (SKIP.has(entry.name)) continue
22
- const path = join(at, entry.name)
23
- if (entry.isDirectory()) walk(path, found)
24
- else if (CODE.test(entry.name)) found.push(path)
25
- }
26
- return found
27
- }
28
-
29
- function keyProps(root: string): readonly string[] {
30
- const found = new Set<string>()
31
- const home = join(root, 'node_modules', packageOf('ui'))
32
- if (!existsSync(home)) return []
33
- for (const file of walk(home)) {
34
- for (const one of readFileSync(file, 'utf8').matchAll(TYPED)) found.add(one[1] ?? '')
35
- }
36
- return [...found].sort()
37
- }
38
-
39
- function languagesIn(root: string): { readonly held: readonly string[]; readonly source: string | undefined } {
40
- const at = join(root, '_threads', 'localizations.ts')
41
- if (!existsSync(at)) return { held: [], source: undefined }
42
- const text = readFileSync(at, 'utf8')
43
- const held: string[] = []
44
- let source: string | undefined
45
- for (const one of text.matchAll(/id:\s*(?:SOURCE_ROW|'([^']+)')\s*,[\s\S]{0,200}?is:\s*'([^']*)'/g)) {
46
- const id = one[1]
47
- if (id === undefined || id === 'source') {
48
- source = one[2]
49
- continue
50
- }
51
- held.push(id)
52
- }
53
- return { held, source }
54
- }
55
-
56
- export const slopnator: Slopnator = {
57
- checks: (root) => {
58
- const { held } = languagesIn(root)
59
- if (held.length === 0) return []
60
-
61
- const props = keyProps(root)
62
- if (props.length === 0) return []
63
-
64
- const named = new RegExp(`\\b(?:${props.join('|')})\\s*=\\s*"([^"]+)"`, 'g')
65
-
66
- const used = new Map<string, string>()
67
- for (const file of walk(root)) {
68
- const text = readFileSync(file, 'utf8')
69
- for (const one of text.matchAll(named)) {
70
- const key = one[1] ?? ''
71
- if (!used.has(key)) used.set(key, file.slice(root.length).replace(/^\/+/, ''))
72
- }
73
- }
74
- if (used.size === 0) return []
75
-
76
- const findings: string[] = []
77
- for (const language of held) {
78
- const at = join(root, 'i18n', `${language}.json`)
79
- if (!existsSync(at)) {
80
- findings.push(
81
- `i18n/${language}.json is not there and \`${language}\` is a declared language, so every key this ` +
82
- 'interface names goes unanswered in it — run `thread i18n`',
83
- )
84
- continue
85
- }
86
- let wording: Readonly<Record<string, unknown>>
87
- try {
88
- wording = JSON.parse(readFileSync(at, 'utf8')) as Readonly<Record<string, unknown>>
89
- } catch {
90
- findings.push(
91
- `i18n/${language}.json will not parse, so what this app says in \`${language}\` cannot be known — ` +
92
- 'a reader that cannot read is not a reader that found nothing',
93
- )
94
- continue
95
- }
96
- for (const [key, where] of [...used].sort()) {
97
- const said = wording[key]
98
- if (typeof said === 'string' && said !== '') continue
99
- findings.push(
100
- `\`${key}\` is named by ${where} and says nothing in \`${language}\` — an untranslated key is a ` +
101
- 'blank on the screen, and it is an error rather than a word that comes out in the wrong language',
102
- )
103
- }
104
- }
105
- return findings
106
- },
107
- proves: {
108
- wrong: [
109
- {
110
- files: [
111
- { at: '_threads/localizations.ts', source: "export const LOCALIZATION = {\n holds: [\n { id: 'source', is: 'en' },\n { id: 'en', is: 'English' },\n ],\n}\n" },
112
- { at: `node_modules/${packageOf('ui')}/probe.ts`, source: 'export interface P {\n readonly label: MessageKey\n}\n' },
113
- { at: 'compositions/cmp_probe.tsx', source: 'export const P = () => <Button label="probe.blank" />\n' },
114
- { at: 'i18n/en.json', source: '{\n "probe.blank": ""\n}\n' },
115
- ],
116
- at: 'compositions/cmp_probe.tsx',
117
- },
118
- {
119
- files: [
120
- { at: '_threads/localizations.ts', source: "export const LOCALIZATION = {\n holds: [\n { id: 'source', is: 'en' },\n { id: 'en', is: 'English' },\n ],\n}\n" },
121
- { at: `node_modules/${packageOf('ui')}/probe.ts`, source: 'export interface P {\n readonly label: MessageKey\n}\n' },
122
- { at: 'compositions/cmp_probe.tsx', source: 'export const P = () => <Button label="probe.missing" />\n' },
123
- { at: 'i18n/en.json', source: '{\n "probe.other": "Something"\n}\n' },
124
- ],
125
- at: 'compositions/cmp_probe.tsx',
126
- },
127
- ],
128
- right: [
129
- {
130
- files: [
131
- { at: '_threads/localizations.ts', source: "export const LOCALIZATION = {\n holds: [\n { id: 'source', is: 'en' },\n { id: 'en', is: 'English' },\n ],\n}\n" },
132
- { at: `node_modules/${packageOf('ui')}/probe.ts`, source: 'export interface P {\n readonly label: MessageKey\n}\n' },
133
- { at: 'compositions/cmp_probe.tsx', source: 'export const P = () => <Button label="probe.answered" />\n' },
134
- { at: 'i18n/en.json', source: '{\n "probe.answered": "Answered"\n}\n' },
135
- ],
136
- at: 'compositions/cmp_probe.tsx',
137
- },
138
- ],
139
- },
140
- }
@@ -1,132 +0,0 @@
1
- import type { Slopnator } from '../slopnator.ts'
2
-
3
- import { descend, read, reached } from '../../core/descend.ts'
4
- import { rowsIn } from './sl_unthreaded.ts'
5
-
6
- interface Held {
7
- readonly type?: string
8
- readonly name?: string
9
- readonly value?: unknown
10
- readonly computed?: boolean
11
- readonly object?: Held
12
- readonly property?: Held
13
- readonly init?: Held
14
- readonly id?: Held
15
- readonly declaration?: Held
16
- readonly declarations?: readonly Held[]
17
- readonly left?: Held
18
- }
19
-
20
- const SECRETS = 'secrets'
21
-
22
- function wears(): readonly string[] {
23
- const found = new Set<string>()
24
-
25
- descend()
26
-
27
- for (const at of reached()) {
28
- const text = read(at)
29
- if (text === undefined) continue
30
-
31
- let waiting: string[] = []
32
- for (const one of rowsIn(text)) {
33
- if (one.id === undefined) continue
34
- if (one.list === 'declares') {
35
- waiting.push(one.id)
36
- continue
37
- }
38
- if (one.list !== undefined) continue
39
- if (one.id === SECRETS) {
40
- for (const id of waiting) {
41
- const cut = id.indexOf('_')
42
- if (cut > 0) found.add(id.slice(0, cut + 1))
43
- }
44
- }
45
- waiting = []
46
- }
47
- }
48
-
49
- return [...found]
50
- }
51
-
52
- const HOLDS = 'process'
53
-
54
- const ENV = 'env'
55
-
56
- const isEnv = (one: Held | undefined): boolean =>
57
- one?.type === 'MemberExpression' &&
58
- one.object?.type === 'Identifier' &&
59
- one.object.name === HOLDS &&
60
- one.property?.type === 'Identifier' &&
61
- one.property.name === ENV
62
-
63
- function stated(node: Held | undefined, ancestors: readonly unknown[]): string | undefined {
64
- if (node === undefined) return undefined
65
- if (node.type === 'Literal' && typeof node.value === 'string') return node.value
66
- if (node.type !== 'Identifier' || node.name === undefined) return undefined
67
- for (const one of ancestors) {
68
- const body = (one as { readonly body?: unknown }).body
69
- for (const each of Array.isArray(body) ? (body as readonly unknown[]) : []) {
70
- const line = each as Held
71
- const held = line.type === 'ExportNamedDeclaration' ? line.declaration : line
72
- if (held?.type !== 'VariableDeclaration') continue
73
- for (const said of held.declarations ?? []) {
74
- if (said.id?.type !== 'Identifier' || said.id.name !== node.name) continue
75
- return said.init?.type === 'Literal' && typeof said.init.value === 'string' ? said.init.value : undefined
76
- }
77
- }
78
- }
79
- return undefined
80
- }
81
-
82
- export const slopnator: Slopnator = {
83
- on: ['MemberExpression'],
84
- holds: (node, at) => {
85
- const held = node as Held
86
- if (!isEnv(held.object)) return false
87
-
88
- const up = at.ancestors()
89
- const parent = up[up.length - 1] as Held | undefined
90
- if (parent?.type === 'AssignmentExpression' && parent.left === held) return false
91
-
92
- const name = held.computed === true ? stated(held.property, up) : held.property?.name
93
-
94
- if (name === undefined) {
95
- return (
96
- 'this takes a value out of the environment under a name nobody reading the file can see, so it can name ' +
97
- 'any secret this app declares. A declared value is opened by the vault, which refuses a name no row states ' +
98
- 'and a value nothing sealed, and neither of those refusals can happen here'
99
- )
100
- }
101
-
102
- const wearing = wears().find((one) => name.toLowerCase().startsWith(one))
103
- if (wearing === undefined) return false
104
-
105
- return (
106
- name +
107
- ' wears the prefix ' +
108
- wearing +
109
- ', so it names a secret, and this takes it from the environment instead of the vault — past the check that a ' +
110
- 'row declares it, and past the sealed store the value was written to'
111
- )
112
- },
113
- proves: {
114
- wrong: [
115
- 'const key = process.env[named.toUpperCase()]',
116
- {
117
- files: [
118
- { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
119
- { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_probe_secrets.ts',\n },\n ],\n}\n" },
120
- { at: 'threads/_probe_secrets.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const SECRETS: Threaded = {\n id: 'secrets',\n declares: [\n {\n id: 'sec_model',\n at: 'threads/_probe_secrets.ts',\n },\n ],\n}\n" },
121
- { at: 'probe.tsx', source: "const key = process.env['SEC_MODEL']\n" },
122
- ],
123
- at: 'probe.tsx',
124
- },
125
- ],
126
- right: [
127
- "const mode = process.env['NODE_ENV']",
128
- "const DEPTH = 'THREAD_DEPTH'\n\nconst depth = Number(process.env[DEPTH] ?? '0')",
129
- "const held = opened(['sec_model'])",
130
- ],
131
- },
132
- }