@prestack/thread 0.1.21 → 0.1.23

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 (138) hide show
  1. package/CHANGELOG.md +227 -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 +69 -0
  7. package/dist/derivation/generators/build.d.ts +3 -1
  8. package/dist/derivation/generators/build.js +104 -22
  9. package/dist/derivation/generators/release.js +74 -25
  10. package/dist/derivation/release.js +3 -3
  11. package/dist/integrations/cli.d.ts +0 -1
  12. package/dist/integrations/cli.js +69 -11
  13. package/dist/integrations/declared.d.ts +2 -0
  14. package/dist/integrations/declared.js +6 -1
  15. package/dist/integrations/pglite.js +13 -3
  16. package/dist/integrations/target.d.ts +3 -0
  17. package/dist/integrity/attach.js +1 -5
  18. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  19. package/dist/operations/access.js +3 -2
  20. package/dist/operations/capabilities.d.ts +2 -0
  21. package/dist/operations/capabilities.js +2 -0
  22. package/dist/operations/generators/capability.js +237 -24
  23. package/dist/operations/generators/compile.js +150 -50
  24. package/dist/operations/generators/seed.d.ts +3 -0
  25. package/dist/operations/generators/seed.js +25 -3
  26. package/dist/operations/nodes.d.ts +2 -2
  27. package/package.json +1 -1
  28. package/dist/core/actor.ts +0 -21
  29. package/dist/core/attach.ts +0 -613
  30. package/dist/core/chain.ts +0 -145
  31. package/dist/core/condition.ts +0 -227
  32. package/dist/core/descend.ts +0 -1185
  33. package/dist/core/errors/catalogue.ts +0 -314
  34. package/dist/core/errors/classes.ts +0 -51
  35. package/dist/core/errors/error.ts +0 -152
  36. package/dist/core/errors/namespaces.ts +0 -19
  37. package/dist/core/finding.ts +0 -37
  38. package/dist/core/generators/commit.ts +0 -157
  39. package/dist/core/init.ts +0 -222
  40. package/dist/core/invoked.ts +0 -52
  41. package/dist/core/round.ts +0 -74
  42. package/dist/core/rows.ts +0 -506
  43. package/dist/core/scope.ts +0 -20
  44. package/dist/core/thread.ts +0 -29
  45. package/dist/core/tree.ts +0 -90
  46. package/dist/core/types.ts +0 -351
  47. package/dist/derivation/attach.ts +0 -57
  48. package/dist/derivation/generators/build.ts +0 -371
  49. package/dist/derivation/generators/check.ts +0 -118
  50. package/dist/derivation/generators/determinism.ts +0 -145
  51. package/dist/derivation/generators/release.ts +0 -236
  52. package/dist/derivation/release.ts +0 -43
  53. package/dist/integrations/attach.ts +0 -187
  54. package/dist/integrations/cli.ts +0 -606
  55. package/dist/integrations/declared.ts +0 -34
  56. package/dist/integrations/disk.ts +0 -466
  57. package/dist/integrations/generators/read.ts +0 -127
  58. package/dist/integrations/generators/vault.ts +0 -210
  59. package/dist/integrations/https.ts +0 -216
  60. package/dist/integrations/pglite.ts +0 -206
  61. package/dist/integrations/secret.ts +0 -54
  62. package/dist/integrations/target.ts +0 -672
  63. package/dist/integrations/vault.ts +0 -148
  64. package/dist/integrity/attach.ts +0 -150
  65. package/dist/integrity/check-threads.ts +0 -166
  66. package/dist/integrity/constraint.ts +0 -1246
  67. package/dist/integrity/generators/compiler.ts +0 -138
  68. package/dist/integrity/generators/gate.ts +0 -227
  69. package/dist/integrity/generators/issues.ts +0 -160
  70. package/dist/integrity/generators/publish.ts +0 -233
  71. package/dist/integrity/generators/trial.ts +0 -359
  72. package/dist/integrity/lint.ts +0 -214
  73. package/dist/integrity/lsp/bin.ts +0 -44
  74. package/dist/integrity/lsp/diagnostics.ts +0 -92
  75. package/dist/integrity/lsp/findings.ts +0 -63
  76. package/dist/integrity/lsp/lint.ts +0 -124
  77. package/dist/integrity/lsp/protocol.ts +0 -78
  78. package/dist/integrity/lsp/server.ts +0 -224
  79. package/dist/integrity/slopnator.ts +0 -324
  80. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  81. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  82. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  83. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  84. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  85. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  86. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  87. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  88. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  89. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  90. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  91. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  92. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  93. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  94. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  95. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  96. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  97. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  98. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  99. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  100. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  101. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  102. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  103. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  104. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  105. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  106. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  107. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  108. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  109. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  110. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  111. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  112. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  113. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  114. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  115. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  116. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  117. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  118. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  119. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  120. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  121. package/dist/integrity/sweep.ts +0 -395
  122. package/dist/knowledge/attach.ts +0 -327
  123. package/dist/knowledge/generators/docsgen.ts +0 -186
  124. package/dist/knowledge/generators/glossary.ts +0 -73
  125. package/dist/knowledge/glossary.ts +0 -6
  126. package/dist/operations/access.ts +0 -131
  127. package/dist/operations/attach.ts +0 -408
  128. package/dist/operations/capabilities.ts +0 -88
  129. package/dist/operations/dispatch.ts +0 -294
  130. package/dist/operations/error-results.ts +0 -31
  131. package/dist/operations/generators/capability.ts +0 -1054
  132. package/dist/operations/generators/compile.ts +0 -701
  133. package/dist/operations/generators/permits.ts +0 -229
  134. package/dist/operations/generators/seed.ts +0 -359
  135. package/dist/operations/generators/trial.ts +0 -666
  136. package/dist/operations/nodes.ts +0 -53
  137. package/dist/operations/operations.ts +0 -91
  138. 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
- }