@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,1185 +0,0 @@
1
- import { starts } from '../integrations/cli.ts'
2
- import { attached, beyond, existsSync, here as answering, kind, readdirSync, readFileSync, realpathSync, statSync, writeFileSync, type Kind } from '../integrations/disk.ts'
3
- import { dirname, join, relative, resolve } from 'node:path'
4
-
5
- import ts from 'typescript'
6
-
7
- import type { Finding } from './finding.ts'
8
- import { SHAPED, ignored } from './init.ts'
9
- import { ORGANISATION, installed } from './scope.ts'
10
- import { findTree } from './tree.ts'
11
-
12
- const BUILT: ReadonlySet<string> = new Set(
13
- ignored()
14
- .split('\n')
15
- .map((one) => one.trim())
16
- .filter((one) => one.endsWith('/'))
17
- .map((one) => one.replace(/\/+$/, '')),
18
- )
19
-
20
- const TREE_AT = ((): ReturnType<typeof findTree> | undefined => {
21
- try {
22
- return findTree()
23
- } catch {
24
- return undefined
25
- }
26
- })()
27
-
28
- const NOWHERE = '/this-app-has-no-tree'
29
-
30
- function rooted(): void {
31
- if (TREE_AT === undefined) findTree()
32
- }
33
-
34
- export const ROOT = TREE_AT?.at ?? NOWHERE
35
- export const TREE = `${TREE_AT?.name ?? NOWHERE}/`
36
- export const THREAD_AT = `${TREE_AT?.name ?? NOWHERE}/thread.ts`
37
-
38
- let ignores: readonly string[] | undefined
39
- let readIgnores = false
40
-
41
- function ownIgnores(): readonly string[] | undefined {
42
- if (readIgnores) return ignores
43
- readIgnores = true
44
- if (!attached() && starts('git', ['rev-parse', '--is-inside-work-tree'], { at: ROOT }).ok) {
45
- return ignores
46
- }
47
- try {
48
- ignores = readFileSync(join(ROOT, '.gitignore'), 'utf8')
49
- .split('\n')
50
- .map((one) => one.trim())
51
- .filter((one) => one !== '' && !one.startsWith('#') && !one.startsWith('!'))
52
- .map((one) => one.replace(/^\/+|\/+$/g, ''))
53
- .filter((one) => one !== '' && !one.includes('*'))
54
- } catch {
55
- ignores = []
56
- }
57
- return ignores
58
- }
59
-
60
- const asked = new Map<string, boolean>()
61
-
62
- export function unkept(at: string): boolean {
63
- const held = asked.get(at)
64
- if (held !== undefined) return held
65
- const own = ownIgnores()
66
- const found =
67
- own === undefined
68
- ? starts('git', ['check-ignore', '-q', '--', at], { at: ROOT }).ok
69
- : own.some((one) => at === one || at.startsWith(`${one}/`) || at.split('/').includes(one))
70
- asked.set(at, found)
71
- return found
72
- }
73
-
74
- export const LISTS = ['declares', 'derives', 'seeds'] as const
75
-
76
- export const ROW_LISTS = [...LISTS, 'holds'] as const
77
-
78
- const SOURCE = /\.(ts|tsx|mts|cts)$/
79
- const RUNNER = /^(?:pnpm|npm|yarn) (?:run )?([\w:.-]+)/
80
-
81
- export interface Row {
82
- readonly at?: string
83
- readonly id?: string
84
- readonly describes?: string
85
- readonly by?: string
86
- readonly runs?: string
87
-
88
- readonly requires?: readonly string[]
89
- readonly takes?: readonly string[]
90
-
91
- readonly type: string
92
- readonly shape?: ts.Type
93
- readonly fields: ReadonlyMap<string, ts.Expression>
94
- readonly where: string
95
- }
96
-
97
- export interface Pass {
98
-
99
- readonly threaded: ReadonlyMap<
100
- string,
101
- readonly { readonly list: 'declares' | 'derives' | 'seeds'; readonly by: string; readonly row: Row }[]
102
- >
103
- readonly mentioned: ReadonlyMap<string, readonly string[]>
104
- readonly derived: ReadonlyMap<string, Row>
105
-
106
- readonly stated: ReadonlyMap<string, Row>
107
- readonly coined: ReadonlyMap<string, { readonly said: readonly string[]; readonly under: string; readonly from: readonly string[] }>
108
- readonly broken: readonly Finding[]
109
- readonly findings: readonly Finding[]
110
- readonly declarations: number
111
- readonly rows: number
112
- readonly sources: readonly string[]
113
- }
114
-
115
- export type { Finding }
116
-
117
- export function under(pass: Pass, id: string, root: string, seen: Set<string> = new Set()): boolean {
118
- if (id === root) return true
119
- if (seen.has(id)) return false
120
- seen.add(id)
121
- return (pass.coined.get(id)?.from ?? []).some((one) => under(pass, one, root, seen))
122
- }
123
-
124
- function here(path: string): string {
125
- return relative(ROOT, path).split('\\').join('/')
126
- }
127
-
128
- const CANONICAL = 'threads/'
129
-
130
- const BESIDE = /^\.\//
131
-
132
- function settle(at: string, where: string): string {
133
- if (BESIDE.test(at)) return here(resolve(ROOT, dirname(where), at.slice(2)))
134
- if (at.startsWith(CANONICAL)) return here(resolve(ROOT, TREE, at.slice(CANONICAL.length)))
135
- return here(resolve(ROOT, at))
136
- }
137
-
138
- function itself(at: string): string {
139
- try {
140
- return realpathSync(resolve(ROOT, at))
141
- } catch {
142
- return resolve(ROOT, at)
143
- }
144
- }
145
-
146
- function walk(dir: string, out: string[]): string[] {
147
- let entries: readonly string[]
148
- try {
149
- entries = readdirSync(dir)
150
- } catch {
151
- return out
152
- }
153
- for (const name of [...entries].sort()) {
154
- if (name === '.git' || BUILT.has(name)) continue
155
- const path = join(dir, name)
156
- let held
157
- try {
158
- held = statSync(path)
159
- } catch {
160
- continue
161
- }
162
- if (held.isDirectory()) walk(path, out)
163
- else out.push(here(path))
164
- }
165
- return out
166
- }
167
-
168
- let scanned: readonly string[] | undefined
169
-
170
- export function sources(): readonly string[] {
171
- rooted()
172
- scanned ??= walk(ROOT, []).sort()
173
- return scanned
174
- }
175
-
176
- function imported(program: ts.Program): readonly string[] {
177
- const under = `node_modules/${ORGANISATION}/`
178
- return program
179
- .getSourceFiles()
180
- .map((one) => here(one.fileName))
181
- .filter((one) => one.includes(under))
182
- .sort()
183
- }
184
-
185
- const FALLBACK: ts.CompilerOptions = {
186
- target: ts.ScriptTarget.Latest,
187
- module: ts.ModuleKind.ESNext,
188
- moduleResolution: ts.ModuleResolutionKind.Bundler,
189
- jsx: ts.JsxEmit.ReactJSX,
190
- strict: true,
191
- allowImportingTsExtensions: true,
192
- allowJs: true,
193
- resolveJsonModule: true,
194
- }
195
-
196
- const CONFIGS = ['tsconfig.json', 'tsconfig.base.json'] as const
197
-
198
- const INVARIANT: ts.CompilerOptions = {
199
- noEmit: true,
200
- noUnusedLocals: true,
201
- skipLibCheck: true,
202
- allowImportingTsExtensions: true,
203
- allowJs: true,
204
- jsx: ts.JsxEmit.ReactJSX,
205
- }
206
-
207
- const NEVER: readonly string[] = ['node_modules', 'bower_components', 'jspm_packages']
208
-
209
- interface Wanted {
210
- readonly under: string
211
- readonly ending: string
212
- }
213
-
214
- function bounds(from: string, one: string): Wanted {
215
- const star = one.indexOf('*')
216
- if (star === -1) return { under: resolve(from, one), ending: '' }
217
- const head = one.slice(0, star)
218
- const cut = head.lastIndexOf('/')
219
- const tail = one.slice(star)
220
- const dot = tail.lastIndexOf('.')
221
- const end = dot === -1 ? '' : tail.slice(dot)
222
- return {
223
- under: resolve(from, cut === -1 ? '' : head.slice(0, cut)),
224
- ending: end.includes('/') || end.includes('*') ? '' : end,
225
- }
226
- }
227
-
228
- function wanted(at: string, only: readonly Wanted[]): boolean {
229
- if (only.length === 0) return true
230
- return only.some((one) => (at === one.under || at.startsWith(`${one.under}/`)) && at.endsWith(one.ending))
231
- }
232
-
233
- function listing(at: string, take: readonly string[], skip: readonly string[], only: readonly Wanted[], deep: number): readonly string[] {
234
- const found: string[] = []
235
- if (deep < 0 || !existsSync(at)) return found
236
- for (const one of readdirSync(at, { withFileTypes: true })) {
237
- const full = join(at, one.name)
238
- if (skip.some((each) => full === each || full.startsWith(`${each}/`))) continue
239
- if (one.isDirectory()) {
240
- if (NEVER.includes(one.name)) continue
241
- found.push(...listing(full, take, skip, only, deep - 1))
242
- } else if (take.some((each) => one.name.endsWith(each)) && wanted(full, only)) {
243
- found.push(full)
244
- }
245
- }
246
- return found
247
- }
248
-
249
- function parsing(from: string): ts.ParseConfigHost {
250
- return {
251
- useCaseSensitiveFileNames: true,
252
- fileExists: (at) => !beyond(at) && existsSync(at),
253
- readFile: (at) => {
254
- try {
255
- return readFileSync(at)
256
- } catch {
257
- return undefined
258
- }
259
- },
260
- readDirectory: (at, take, skip, only, deep) =>
261
- listing(
262
- at,
263
- take,
264
- (skip ?? []).map((one) => resolve(from, one.endsWith('/**') ? one.slice(0, -3) : one)),
265
- only.map((one) => bounds(from, one)),
266
- deep ?? Number.MAX_SAFE_INTEGER,
267
- ),
268
- }
269
- }
270
-
271
- export function configured(): { readonly options: ts.CompilerOptions; readonly from: string; readonly trouble?: string } {
272
- for (const name of CONFIGS) {
273
- const at = join(ROOT, name)
274
- if (!existsSync(at)) continue
275
-
276
- const host = parsing(ROOT)
277
- const held = ts.readConfigFile(at, host.readFile)
278
- if (held.error !== undefined) {
279
- return {
280
- options: { ...FALLBACK, ...INVARIANT },
281
- from: '',
282
- trouble: `${name} is there and cannot be read: ${ts.flattenDiagnosticMessageText(held.error.messageText, ' ')}`,
283
- }
284
- }
285
-
286
- const parsed = ts.parseJsonConfigFileContent(held.config as object, host, dirname(at))
287
- const complained = parsed.errors[0]
288
- if (complained !== undefined) {
289
- return {
290
- options: { ...FALLBACK, ...INVARIANT },
291
- from: '',
292
- trouble: `${name} is there and states no options this gate can use: ${ts.flattenDiagnosticMessageText(complained.messageText, ' ')}`,
293
- }
294
- }
295
-
296
- return { options: { ...parsed.options, ...INVARIANT }, from: name }
297
- }
298
- return { options: { ...FALLBACK, ...INVARIANT }, from: '' }
299
- }
300
-
301
- const libraries = new Map<string, ReturnType<ts.CompilerHost['getSourceFile']>>()
302
-
303
- const DEPENDENCIES = '/node_' + 'modules/'
304
-
305
- export function hosting(options: ts.CompilerOptions): ts.CompilerHost {
306
- const host = ts.createCompilerHost(options, true)
307
- const lib = host.getDefaultLibLocation?.()
308
- const settings = JSON.stringify(options)
309
- const toolchain = (at: string): boolean => lib !== undefined && (at === lib || at.startsWith(`${lib}/`))
310
- const seen = (at: string): Kind => (beyond(at) ? 'none' : kind(at))
311
- return {
312
- ...host,
313
- getCurrentDirectory: () => ROOT,
314
- fileExists: (at) => (toolchain(at) ? host.fileExists(at) : seen(at) === 'file'),
315
- directoryExists: (at) => (toolchain(at) ? (host.directoryExists?.(at) ?? false) : seen(at) === 'folder'),
316
- readFile: (at) => (toolchain(at) ? host.readFile(at) : seen(at) === 'file' ? readFileSync(at) : undefined),
317
- getSourceFile: (at, version) => {
318
- const lasting = toolchain(at) ? true : at.includes(DEPENDENCIES)
319
- const key = settings + at + JSON.stringify(version)
320
- if (lasting && libraries.has(key)) return libraries.get(key)
321
- const made = toolchain(at)
322
- ? host.getSourceFile(at, version)
323
- : seen(at) === 'file'
324
- ? ts.createSourceFile(at, readFileSync(at), version, true)
325
- : undefined
326
- if (lasting) libraries.set(key, made)
327
- return made
328
- },
329
- getDirectories: (at) =>
330
- seen(at) === 'folder' ? readdirSync(at, { withFileTypes: true }).filter((one) => one.isDirectory()).map((one) => one.name) : [],
331
- realpath: (at) => (toolchain(at) ? (host.realpath?.(at) ?? at) : realpathSync(at)),
332
- writeFile: (at, said) => {
333
- writeFileSync(at, said)
334
- },
335
- }
336
- }
337
-
338
- let built: { program: ts.Program; checker: ts.TypeChecker } | undefined
339
-
340
- function compiled(): { program: ts.Program; checker: ts.TypeChecker } {
341
- rooted()
342
- if (built !== undefined) return built
343
- const roots = [THREAD_AT, ...sources()].filter((one) => SOURCE.test(one)).map((one) => resolve(ROOT, one))
344
- const options = configured().options
345
- const program = ts.createProgram([...new Set(roots)], options, hosting(options))
346
- built = { program, checker: program.getTypeChecker() }
347
- return built
348
- }
349
-
350
- export function uncompiled(): readonly Finding[] {
351
- const { program } = compiled()
352
- const found: Finding[] = []
353
- const INSTALLED = 'node_' + 'modules'
354
- for (const file of program.getSourceFiles()) {
355
- if (file.isDeclarationFile) continue
356
- const at = here(file.fileName)
357
- if (at.startsWith('..') || at.split('/').includes(INSTALLED)) continue
358
- for (const one of [...program.getSyntacticDiagnostics(file), ...program.getSemanticDiagnostics(file)]) {
359
- const start = one.start === undefined ? undefined : file.getLineAndCharacterOfPosition(one.start)
360
- found.push({
361
- file: at,
362
- at: start === undefined ? undefined : { line: start.line + 1, column: start.character + 1 },
363
- detail: `will not compile — TS${String(one.code)}: ${ts.flattenDiagnosticMessageText(one.messageText, ' ')}`,
364
- instead:
365
- 'fix the type error. Nothing in this tree is built, served or gated while a file it names does not compile, and every other surface here reads the same program',
366
- })
367
- }
368
- }
369
- return found
370
- }
371
-
372
- function threadedType(checker: ts.TypeChecker, program: ts.Program): ts.Type | undefined {
373
- const root = program.getSourceFile(resolve(ROOT, THREAD_AT))
374
- if (root === undefined) return undefined
375
- for (const statement of root.statements) {
376
- if (!ts.isImportDeclaration(statement)) continue
377
- const bindings = statement.importClause?.namedBindings
378
- if (bindings === undefined || !ts.isNamedImports(bindings)) continue
379
- for (const one of bindings.elements) {
380
- const symbol = checker.getSymbolAtLocation(one.name)
381
- if (symbol === undefined) continue
382
- const real = (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol
383
- if (real.name === 'Threaded') return checker.getDeclaredTypeOfSymbol(real)
384
- }
385
- }
386
- return undefined
387
- }
388
-
389
- const source = new Map<string, string | undefined>()
390
-
391
- export function read(at: string): string | undefined {
392
- if (source.has(at)) return source.get(at)
393
- let text: string | undefined
394
- try {
395
- text = readFileSync(resolve(ROOT, at), 'utf8')
396
- } catch {
397
- text = undefined
398
- }
399
- source.set(at, text)
400
- return text
401
- }
402
-
403
- const shapes = new WeakMap<object, ReadonlyMap<string, readonly string[]>>()
404
-
405
- export function shapesNamed(): ReadonlyMap<string, readonly string[]> {
406
- const held = compiled()
407
- const known = shapes.get(held)
408
- if (known !== undefined) return known
409
- const { program, checker } = held
410
- const shape = threadedType(checker, program)
411
- const found = new Map<string, string[]>()
412
- if (shape === undefined) return found
413
- for (const at of [...sources(), ...imported(program)]) {
414
- if (!SOURCE.test(at)) continue
415
- const file = program.getSourceFile(resolve(ROOT, at))
416
- if (file === undefined) continue
417
- for (const statement of file.statements) {
418
- if (!ts.isInterfaceDeclaration(statement) && !ts.isTypeAliasDeclaration(statement)) continue
419
- if ((statement.typeParameters ?? []).length > 0) continue
420
- const symbol = checker.getSymbolAtLocation(statement.name)
421
- if (symbol === undefined) continue
422
- const shown = (statement.modifiers ?? []).some((one) => one.kind === ts.SyntaxKind.ExportKeyword)
423
- if (!shown && statement.name.text !== 'Threaded') continue
424
- const held = ts.isInterfaceDeclaration(statement)
425
- ? checker.getDeclaredTypeOfSymbol(symbol)
426
- : checker.getTypeAtLocation(statement.name)
427
- const named = statement.name.text
428
- const shadows = named === 'Threaded'
429
- const kind = elementOf(checker, held)
430
- if (!shadows && (kind.flags & ts.TypeFlags.Never) !== 0) continue
431
- if (!shadows && !checker.isTypeAssignableTo(kind, shape)) continue
432
- const where = found.get(named) ?? []
433
- const real = itself(at)
434
- if (where.some((one) => itself(one) === real)) continue
435
- found.set(named, [...where, at])
436
- }
437
- }
438
- shapes.set(held, found)
439
- return found
440
- }
441
-
442
- export function threading(): ReadonlySet<string> {
443
- return new Set(['Threaded', ...shapesNamed().keys()])
444
- }
445
-
446
- export function shapeFields(
447
- named: string,
448
- inside?: string,
449
- ): readonly { readonly name: string; readonly required: boolean; readonly list: boolean; readonly map: boolean; readonly worded: boolean; readonly guide: string; readonly record: readonly string[]; readonly objects: boolean }[] {
450
- const { program, checker } = compiled()
451
- for (const at of [...sources(), ...imported(program)]) {
452
- if (!SOURCE.test(at)) continue
453
- if (inside !== undefined && !at.split('\\').join('/').endsWith(inside)) continue
454
- const file = program.getSourceFile(resolve(ROOT, at))
455
- if (file === undefined) continue
456
- for (const statement of file.statements) {
457
- if (!ts.isInterfaceDeclaration(statement) && !ts.isTypeAliasDeclaration(statement)) continue
458
- if (statement.name.text !== named) continue
459
- const symbol = checker.getSymbolAtLocation(statement.name)
460
- if (symbol === undefined) continue
461
- const held = ts.isInterfaceDeclaration(statement)
462
- ? checker.getDeclaredTypeOfSymbol(symbol)
463
- : checker.getTypeAtLocation(statement.name)
464
- const mapped = (kind: ts.Type): boolean =>
465
- (kind.isUnion() ? kind.types : [kind]).some(
466
- (each) =>
467
- (each.flags & ts.TypeFlags.StringLike) === 0 &&
468
- checker.getIndexTypeOfType(each, ts.IndexKind.Number) === undefined &&
469
- checker.getIndexTypeOfType(each, ts.IndexKind.String) !== undefined,
470
- )
471
- const listed = (kind: ts.Type): boolean =>
472
- (kind.isUnion() ? kind.types : [kind]).some(
473
- (each) =>
474
- (each.flags & ts.TypeFlags.StringLike) === 0 &&
475
- checker.getIndexTypeOfType(each, ts.IndexKind.Number) !== undefined,
476
- )
477
- return checker.getPropertiesOfType(held).map((one) => {
478
- const kind = checker.getTypeOfSymbolAtLocation(one, statement)
479
- return {
480
- name: one.getName(),
481
- required: (one.flags & ts.SymbolFlags.Optional) === 0,
482
- list: listed(kind),
483
- map: mapped(kind),
484
- worded: (kind.isUnion() ? kind.types : [kind]).some((each) => (each.flags & ts.TypeFlags.StringLike) !== 0),
485
- guide: ts.displayPartsToString(one.getDocumentationComment(checker)).trim(),
486
- record: recordOf(checker, kind),
487
- objects: objectsOf(checker, kind),
488
- }
489
- })
490
- }
491
- }
492
- return []
493
- }
494
-
495
- export interface Field {
496
- readonly name: string
497
- readonly required: boolean
498
- readonly list: boolean
499
- readonly map: boolean
500
- readonly boolean: boolean
501
- readonly worded: boolean
502
- readonly guide: string
503
- readonly record: readonly string[]
504
- readonly objects: boolean
505
- }
506
-
507
- function elementAt(at: string, list: string, within?: number): { readonly type: ts.Type; readonly node: ts.Node } | undefined {
508
- const { program, checker } = compiled()
509
- const file = program.getSourceFile(resolve(ROOT, at))
510
- if (file === undefined) return undefined
511
- const threaded = threadedType(checker, program)
512
-
513
- let held: { readonly type: ts.Type; readonly node: ts.Node } | undefined
514
-
515
- const consider = (node: ts.ArrayLiteralExpression): void => {
516
- if (held !== undefined) return
517
- if (within !== undefined && node.getStart() !== within) return
518
- const wanted = checker.getContextualType(node) ?? checker.getTypeAtLocation(node)
519
- const kinds = (wanted.isUnionOrIntersection() ? wanted.types : [wanted])
520
- .filter((one) => checker.getIndexTypeOfType(one, ts.IndexKind.Number) !== undefined)
521
- .map((one) => elementOf(checker, one))
522
- const type =
523
- kinds.find((one) => threaded !== undefined && checker.isTypeAssignableTo(one, threaded)) ??
524
- (kinds.length === 1 ? kinds[0] : undefined)
525
- if (type !== undefined) held = { type, node }
526
- }
527
-
528
- const visit = (node: ts.Node): void => {
529
- if (held !== undefined) return
530
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === list) {
531
- const initial = node.initializer
532
- if (initial !== undefined && ts.isArrayLiteralExpression(initial)) consider(initial)
533
- }
534
- if (ts.isPropertyAssignment(node)) {
535
- const key = ts.isIdentifier(node.name) || ts.isStringLiteralLike(node.name) ? node.name.text : undefined
536
- if (key === list && ts.isArrayLiteralExpression(node.initializer)) consider(node.initializer)
537
- }
538
- ts.forEachChild(node, visit)
539
- }
540
-
541
- visit(file)
542
- return held
543
- }
544
-
545
- export function listShape(at: string, list: string): string | undefined {
546
- const held = elementAt(at, list)
547
- return held === undefined ? undefined : compiled().checker.typeToString(held.type)
548
- }
549
-
550
- export function listFields(at: string, list: string, within?: number): readonly Field[] {
551
- const held = elementAt(at, list, within)
552
- if (held === undefined) return []
553
- const { checker } = compiled()
554
- const bare = (kind: ts.Type): readonly ts.Type[] =>
555
- (kind.isUnion() ? kind.types : [kind]).filter((each) => (each.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) === 0)
556
- const mapped = (kind: ts.Type): boolean =>
557
- bare(kind).some(
558
- (each) =>
559
- (each.flags & ts.TypeFlags.StringLike) === 0 &&
560
- checker.getIndexTypeOfType(each, ts.IndexKind.Number) === undefined &&
561
- checker.getIndexTypeOfType(each, ts.IndexKind.String) !== undefined,
562
- )
563
- const listed = (kind: ts.Type): boolean =>
564
- bare(kind).some(
565
- (each) => (each.flags & ts.TypeFlags.StringLike) === 0 && checker.getIndexTypeOfType(each, ts.IndexKind.Number) !== undefined,
566
- )
567
- const flagged = (kind: ts.Type): boolean => {
568
- const kinds = bare(kind)
569
- return kinds.length > 0 && kinds.every((each) => (each.flags & ts.TypeFlags.BooleanLike) !== 0)
570
- }
571
- const worded = (kind: ts.Type): boolean => bare(kind).some((each) => (each.flags & ts.TypeFlags.StringLike) !== 0)
572
- return checker.getPropertiesOfType(held.type).map((one) => {
573
- const kind = checker.getTypeOfSymbolAtLocation(one, held.node)
574
- return {
575
- name: one.getName(),
576
- required: (one.flags & ts.SymbolFlags.Optional) === 0,
577
- list: listed(kind),
578
- map: mapped(kind),
579
- boolean: flagged(kind),
580
- worded: worded(kind),
581
- guide: ts.displayPartsToString(one.getDocumentationComment(checker)).trim(),
582
- record: recordOf(checker, kind),
583
- objects: objectsOf(checker, kind),
584
- }
585
- })
586
- }
587
-
588
- function elementOf(checker: ts.TypeChecker, type: ts.Type): ts.Type {
589
- return checker.getIndexTypeOfType(type, ts.IndexKind.Number) ?? type
590
- }
591
-
592
- function objectsOf(checker: ts.TypeChecker, type: ts.Type): boolean {
593
- for (const each of type.isUnion() ? type.types : [type]) {
594
- if ((each.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.StringLike)) !== 0) continue
595
- const held = checker.getIndexTypeOfType(each, ts.IndexKind.Number) ?? each
596
- if ((held.isUnion() ? held.types : [held]).every((one) => (one.flags & ts.TypeFlags.Object) !== 0)) return true
597
- }
598
- return false
599
- }
600
-
601
- function recordOf(checker: ts.TypeChecker, type: ts.Type): readonly string[] {
602
- for (const each of type.isUnion() ? type.types : [type]) {
603
- if ((each.flags & ts.TypeFlags.StringLike) !== 0 || checker.getIndexTypeOfType(each, ts.IndexKind.Number) === undefined) continue
604
- const element = elementOf(checker, each)
605
- if ((element.flags & ts.TypeFlags.Object) === 0) return []
606
- const fields = checker.getPropertiesOfType(element)
607
- const worded = fields.every((one) => {
608
- const kind = checker.getNonNullableType(checker.getTypeOfSymbol(one))
609
- return (kind.isUnion() ? kind.types : [kind]).every((part) => (part.flags & ts.TypeFlags.StringLike) !== 0)
610
- })
611
- return worded ? fields.map((one) => one.getName()) : []
612
- }
613
- return []
614
- }
615
-
616
- export function rowShape(at: string, list: string): string | undefined {
617
- const { program, checker } = compiled()
618
- const file = program.getSourceFile(resolve(ROOT, at))
619
- const threaded = threadedType(checker, program)
620
- if (file === undefined || threaded === undefined) return undefined
621
-
622
- let held: ts.Type | undefined
623
-
624
- const consider = (node: ts.ArrayLiteralExpression): void => {
625
- if (held !== undefined) return
626
- const wanted = checker.getContextualType(node) ?? checker.getTypeAtLocation(node)
627
- const each = (wanted.isUnionOrIntersection() ? wanted.types : [wanted]).map((one) => elementOf(checker, one))
628
- held = each.find((one) => checker.isTypeAssignableTo(one, threaded))
629
- }
630
-
631
- const visit = (node: ts.Node): void => {
632
- if (held !== undefined) return
633
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === list) {
634
- const initial = node.initializer
635
- if (initial !== undefined && ts.isArrayLiteralExpression(initial)) consider(initial)
636
- }
637
- if (ts.isPropertyAssignment(node)) {
638
- const key = ts.isIdentifier(node.name) || ts.isStringLiteralLike(node.name) ? node.name.text : undefined
639
- if (key === list && ts.isArrayLiteralExpression(node.initializer)) consider(node.initializer)
640
- }
641
- ts.forEachChild(node, visit)
642
- }
643
-
644
- visit(file)
645
- return held === undefined ? undefined : checker.typeToString(held)
646
- }
647
-
648
- function literal(node: ts.Expression | undefined): string | undefined {
649
- if (node === undefined) return undefined
650
- return ts.isStringLiteralLike(node) ? node.text : undefined
651
- }
652
-
653
- function fieldsOf(node: ts.ObjectLiteralExpression): Map<string, ts.Expression> {
654
- const found = new Map<string, ts.Expression>()
655
- for (const property of node.properties) {
656
- if (!ts.isPropertyAssignment(property)) continue
657
- const key = ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name) ? property.name.text : undefined
658
- if (key !== undefined) found.set(key, property.initializer)
659
- }
660
- return found
661
- }
662
-
663
- function wordsOf(node: ts.Expression | undefined): readonly string[] {
664
- if (node === undefined || !ts.isArrayLiteralExpression(node)) return []
665
- return node.elements.map((one) => literal(one)).filter((one): one is string => one !== undefined)
666
- }
667
-
668
- function rowOf(node: ts.ObjectLiteralExpression, type: string, where: string, shape?: ts.Type): Row {
669
- const fields = fieldsOf(node)
670
- const named = literal(fields.get('at'))
671
- const at = named === undefined ? undefined : settle(named, where)
672
- const id = literal(fields.get('id'))
673
- const describes = literal(fields.get('describes'))
674
- const by = literal(fields.get('by'))
675
- const runs = literal(fields.get('runs'))
676
- const requires = wordsOf(fields.get('requires'))
677
- const takes = wordsOf(fields.get('takes'))
678
- return {
679
- ...(at === undefined ? {} : { at }),
680
- ...(id === undefined ? {} : { id }),
681
- ...(describes === undefined ? {} : { describes }),
682
- ...(by === undefined ? {} : { by }),
683
- ...(runs === undefined ? {} : { runs }),
684
- ...(requires.length === 0 ? {} : { requires }),
685
- ...(takes.length === 0 ? {} : { takes }),
686
- ...(shape === undefined ? {} : { shape }),
687
- type,
688
- fields,
689
- where,
690
- }
691
- }
692
-
693
- function unwrap(node: ts.Expression | undefined): ts.Expression | undefined {
694
- let held = node
695
- for (;;) {
696
- if (held === undefined) return undefined
697
- if (ts.isParenthesizedExpression(held) || ts.isSatisfiesExpression(held) || ts.isAsExpression(held)) {
698
- held = held.expression
699
- continue
700
- }
701
- return held
702
- }
703
- }
704
-
705
- interface Held {
706
- readonly name: string
707
- readonly type: string
708
- readonly shape: ts.Type
709
- readonly node: ts.ObjectLiteralExpression
710
- }
711
-
712
- export function nodesIn(at: string): readonly Held[] {
713
- const { program, checker } = compiled()
714
- const threaded = threadedType(checker, program)
715
- const file = program.getSourceFile(resolve(ROOT, at))
716
- if (file === undefined || threaded === undefined) return []
717
-
718
- const found: Held[] = []
719
-
720
- const take = (name: string, where: ts.Node, initial: ts.Expression | undefined): void => {
721
- const held = unwrap(initial)
722
- if (held === undefined) return
723
-
724
- const recognised = (one: ts.Type): boolean => (one.flags & ts.TypeFlags.Any) === 0 && checker.isTypeAssignableTo(one, threaded)
725
- const declared = checker.getTypeAtLocation(where)
726
- const each = (declared.isUnionOrIntersection() ? declared.types : [declared]).map((one) => elementOf(checker, one))
727
- const stated = each.find((one) => recognised(one))
728
-
729
- const parts = ts.isArrayLiteralExpression(held) ? held.elements : [held]
730
- for (const one of parts) {
731
- const object = unwrap(one)
732
- if (object === undefined || !ts.isObjectLiteralExpression(object)) continue
733
- const mine = checker.getWidenedType(checker.getTypeAtLocation(object))
734
- const known = stated ?? (recognised(mine) ? mine : undefined)
735
- if (known === undefined) continue
736
- found.push({ name, type: checker.typeToString(known), shape: known, node: object })
737
- }
738
- }
739
-
740
- const step = (statements: readonly ts.Statement[]): void => {
741
- for (const statement of statements) {
742
- if (ts.isModuleDeclaration(statement) && statement.body !== undefined && ts.isModuleBlock(statement.body)) {
743
- step(statement.body.statements)
744
- continue
745
- }
746
- if (ts.isExportAssignment(statement)) {
747
- take('default', statement.expression, statement.expression)
748
- continue
749
- }
750
- if (!ts.isVariableStatement(statement)) continue
751
- for (const one of statement.declarationList.declarations) {
752
- if (!ts.isIdentifier(one.name) || one.initializer === undefined) continue
753
- take(one.name.text, one.name, one.initializer)
754
- }
755
- }
756
- }
757
-
758
- step(file.statements)
759
- return found
760
- }
761
-
762
- let commands: ReadonlySet<string> | undefined
763
-
764
- function scripts(): ReadonlySet<string> {
765
- if (commands !== undefined) return commands
766
- const found = new Set<string>()
767
- try {
768
- const held = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf8')) as { scripts?: Record<string, unknown> }
769
- for (const name of Object.keys(held.scripts ?? {})) found.add(name)
770
- } catch {
771
- found.clear()
772
- }
773
- commands = found
774
- return found
775
- }
776
-
777
- function mentionsIn(at: string): readonly string[] {
778
- const found: string[] = []
779
- const step = (node: ts.ObjectLiteralExpression): void => {
780
- const fields = fieldsOf(node)
781
- for (const list of LISTS) {
782
- const value = fields.get(list)
783
- if (value === undefined || !ts.isArrayLiteralExpression(value)) continue
784
- for (const one of value.elements) {
785
- if (!ts.isObjectLiteralExpression(one)) continue
786
- const held = literal(fieldsOf(one).get('at'))
787
- if (held !== undefined) found.push(settle(held, at))
788
- step(one)
789
- }
790
- }
791
- }
792
- for (const one of nodesIn(at)) step(one.node)
793
- return found
794
- }
795
-
796
- let passed: Pass | undefined
797
-
798
- export function reached(): readonly string[] {
799
- return [THREAD_AT, ...descend().threaded.keys()]
800
- }
801
-
802
- let world: object = answering()
803
-
804
- const worlds = new WeakMap<object, () => void>()
805
-
806
- function kept(): () => void {
807
- const was = { passed, built, scanned, commands, ignores, readIgnores, source: new Map(source), asked: new Map(asked) }
808
- return () => {
809
- passed = was.passed
810
- built = was.built
811
- scanned = was.scanned
812
- commands = was.commands
813
- ignores = was.ignores
814
- readIgnores = was.readIgnores
815
- source.clear()
816
- for (const [at, text] of was.source) source.set(at, text)
817
- asked.clear()
818
- for (const [at, ignore] of was.asked) asked.set(at, ignore)
819
- }
820
- }
821
-
822
- export function forget(): void {
823
- const now = answering()
824
- if (now === world) libraries.clear()
825
- else {
826
- worlds.set(world, kept())
827
- world = now
828
- const back = worlds.get(now)
829
- if (back !== undefined) {
830
- back()
831
- return
832
- }
833
- }
834
- passed = undefined
835
- built = undefined
836
- scanned = undefined
837
- commands = undefined
838
- source.clear()
839
- asked.clear()
840
- ignores = undefined
841
- readIgnores = false
842
- }
843
-
844
- export function descend(): Pass {
845
- rooted()
846
- if (passed !== undefined) return passed
847
-
848
- const { program, checker } = compiled()
849
- const threaded = new Map<string, { list: 'declares' | 'derives' | 'seeds'; by: string; row: Row }[]>()
850
- const mentioned = new Map<string, string[]>()
851
- const derived = new Map<string, Row>()
852
- const stated = new Map<string, Row>()
853
- const coined = new Map<string, { said: string[]; under: string; from: string[] }>()
854
- const broken: Finding[] = []
855
- const findings: Finding[] = []
856
- const opened = new Set<string>()
857
- const shaped = new Map<string, { type: string; shape?: ts.Type; by: string }>()
858
- const made = new Map<string, { by: Set<string>; rows: string[]; where: string }>()
859
- const absent = new Map<string, { at: number; by: Set<string>; rows: number }>()
860
- const demanded: { row: Row; word: string; list: string }[] = []
861
- const thread: { row: Row; command: string }[] = []
862
- const offered = new Set<string>()
863
- let declarations = 0
864
- let rows = 0
865
-
866
- const root = program.getSourceFile(resolve(ROOT, THREAD_AT))
867
- if (root === undefined) {
868
- broken.push({
869
- file: THREAD_AT,
870
- detail: 'the root of the tree cannot be read, so nothing below it is reached and every file here reads as unthreaded',
871
- instead: 'write the root, or run `thread init`',
872
- })
873
- passed = { threaded, mentioned, derived, stated, coined, broken, findings, declarations, rows, sources: sources() }
874
- return passed
875
- }
876
-
877
- const shape = threadedType(checker, program)
878
- if (shape === undefined) {
879
- broken.push({
880
- file: THREAD_AT,
881
- detail: 'the root does not resolve `Threaded`, so nothing here can be recognised as a declaration and every file reads as unthreaded',
882
- instead: `import \`Threaded\` from \`${SHAPED}\` in the root, or run \`thread init\`. Without the shape there is nothing to hold anything to`,
883
- })
884
- passed = { threaded, mentioned, derived, stated, coined, broken, findings, declarations, rows, sources: sources() }
885
- return passed
886
- }
887
-
888
- const said = (by: string): string => (by === '' ? 'the root' : by)
889
- const names = (row: Row): string => row.id ?? row.at ?? 'a row stating neither an id nor an at'
890
-
891
- function coin(id: string | undefined, describes: string | undefined, under: string, parent: string): void {
892
- if (id === undefined) return
893
- const held = coined.get(id) ?? { said: [], under, from: [] }
894
- const meaning = describes ?? ''
895
- if (!held.said.includes(meaning)) held.said.push(meaning)
896
- if (parent !== '' && parent !== id && !held.from.includes(parent)) held.from.push(parent)
897
- coined.set(id, held)
898
- }
899
-
900
- function judge(row: Row, by: string, list: (typeof LISTS)[number], own: boolean): void {
901
- const at = row.where
902
-
903
- for (const field of ['by', 'runs'] as const) {
904
- const command = row[field]
905
- if (command === undefined) continue
906
- if (command.startsWith('thread ')) {
907
- if (field === 'by') offered.add(command)
908
- else thread.push({ row, command })
909
- continue
910
- }
911
- const held = RUNNER.exec(command)
912
- if (held === null || scripts().has(held[1] ?? '')) continue
913
- findings.push({
914
- file: at,
915
- detail: `\`${names(row)}\` states \`${field}: '${command}'\`, and package.json states no \`${held[1] ?? ''}\` script`,
916
- instead: 'add the script, or fix the name. A row naming a command is a promise that the command exists',
917
- })
918
- }
919
-
920
- for (const field of ['requires', 'takes'] as const) {
921
- for (const word of wordsOf(row.fields.get(field))) demanded.push({ row, word, list: field })
922
- }
923
-
924
- if (list === 'derives' && row.at !== undefined && !own) {
925
- const held = made.get(row.at) ?? { by: new Set<string>(), rows: [] as string[], where: at }
926
- if (row.by !== undefined) held.by.add(row.by)
927
- held.rows.push(names(row))
928
- made.set(row.at, held)
929
- }
930
-
931
- if (row.at === THREAD_AT && !own) {
932
- findings.push({
933
- file: at,
934
- detail: `\`${names(row)}\` declares \`${THREAD_AT}\`, which is the root`,
935
- instead:
936
- 'delete the row. The root exists by fiat and is the one thing nothing declares — giving it a parent makes its origin a thing the tree answers two ways',
937
- })
938
- }
939
-
940
- if (row.at === undefined) return
941
-
942
- if (row.at.startsWith('../') || row.at.startsWith('/')) {
943
- findings.push({
944
- file: at,
945
- detail: `\`${names(row)}\` names \`${row.at}\`, which is outside this repository`,
946
- instead:
947
- 'name something inside the tree. A declaration reaches only what this repository holds, and a row pointing past its edge threads a file nobody here can read, change or delete',
948
- })
949
- return
950
- }
951
-
952
- const target = resolve(ROOT, row.at)
953
- if (!installed(row.at) && unkept(row.at)) {
954
- findings.push({
955
- file: at,
956
- detail: `\`${names(row)}\` names \`${row.at}\`, which git ignores, so it is on this machine and in no clone of it`,
957
- instead:
958
- 'name a file this repository carries, or stop ignoring it. A row naming an ignored path is answered here and nowhere else, so the descent passes on the disk that wrote it and breaks everywhere the work actually runs',
959
- })
960
- return
961
- }
962
- if (!existsSync(target)) {
963
-
964
- const derived = list === 'derives' || row.by !== undefined
965
- const said = absent.get(row.at)
966
- if (said !== undefined) {
967
- said.by.add(names(row))
968
- said.rows += 1
969
- return
970
- }
971
- absent.set(row.at, { at: findings.length, by: new Set([names(row)]), rows: 1 })
972
- findings.push({
973
- file: at,
974
- detail: derived
975
- ? `\`${names(row)}\` ${list === 'derives' ? 'derives' : 'names'} \`${row.at}\`, and nothing is there yet`
976
- : `\`${names(row)}\` names \`${row.at}\`, and nothing is there`,
977
- instead: derived
978
- ? 'run the generator the row names in `by`. A derived file is absent until it is derived, so this is a warning'
979
- : 'write the file, fix the path, or delete the row. A row naming nothing brings nothing into existence',
980
- ...(derived ? { severity: 'warning' as const } : {}),
981
- })
982
- } else if (statSync(target).isDirectory()) {
983
- findings.push({
984
- file: at,
985
- detail: `\`${names(row)}\` names \`${row.at}\`, which is a directory`,
986
- instead:
987
- 'name the file. A directory threads nothing beneath it, so every file under this one still reads as declared by nobody while the row itself looks answered',
988
- })
989
- }
990
-
991
- const already = shaped.get(row.at)
992
- if (already === undefined) {
993
- shaped.set(row.at, { type: row.type, shape: row.shape, by })
994
- return
995
- }
996
- if (row.shape === undefined || already.shape === undefined) return
997
- if (checker.isTypeAssignableTo(row.shape, already.shape) || checker.isTypeAssignableTo(already.shape, row.shape)) return
998
- findings.push({
999
- file: at,
1000
- detail: `\`${row.at}\` is declared \`${row.type}\` by \`${said(by)}\` and \`${already.type}\` by \`${said(already.by)}\`, and neither shape satisfies the other`,
1001
- instead: 'one address has one shape. Decide what the child is and let both rows say it',
1002
- })
1003
- }
1004
-
1005
- function into(node: ts.ObjectLiteralExpression, type: string, at: string, under: string, parent: string, judged = false): void {
1006
- declarations += 1
1007
- const held = rowOf(node, type, at)
1008
- if (!judged) {
1009
- coin(held.id, held.describes, under, parent)
1010
- judge(held, parent, 'declares', true)
1011
- if (held.id !== undefined && !stated.has(held.id)) stated.set(held.id, held)
1012
- }
1013
-
1014
- const by = held.id ?? at
1015
-
1016
- for (const list of LISTS) {
1017
- const stated = held.fields.get(list)
1018
- if (stated === undefined) continue
1019
-
1020
- const only = ts.isCallExpression(stated) && stated.arguments.length === 1 ? stated.arguments[0] : undefined
1021
- const value = only !== undefined && ts.isArrayLiteralExpression(only) ? only : stated
1022
-
1023
- if (!ts.isArrayLiteralExpression(value)) {
1024
- findings.push({ file: at, detail: `\`${by}\` states a \`${list}\` that is not a list`, instead: `write \`${list}\` as a list of rows` })
1025
- continue
1026
- }
1027
-
1028
- for (const one of value.elements) {
1029
- const declaration = ts.isIdentifier(one) ? checker.getSymbolAtLocation(one)?.valueDeclaration : undefined
1030
- const initial = declaration !== undefined && ts.isVariableDeclaration(declaration) ? unwrap(declaration.initializer) : undefined
1031
- const object = ts.isObjectLiteralExpression(one) ? one : initial !== undefined && ts.isObjectLiteralExpression(initial) ? initial : undefined
1032
- if (object === undefined) {
1033
- findings.push({
1034
- file: at,
1035
- detail: `\`${by}\` holds something in \`${list}\` that is not a row written out here`,
1036
- instead: 'write the row inline. A row reached through a name is a row the descent would have to run code to read',
1037
- })
1038
- continue
1039
- }
1040
- rows += 1
1041
- const wanted = checker.getContextualType(one)
1042
- const kind = wanted === undefined ? undefined : elementOf(checker, wanted)
1043
- const row = rowOf(object, kind === undefined ? type : checker.typeToString(kind), at, kind)
1044
- coin(row.id, row.describes, under, by)
1045
- judge(row, by, list, false)
1046
- if (row.at !== undefined) {
1047
- threaded.set(row.at, [...(threaded.get(row.at) ?? []), { list, by, row }])
1048
- if (list === 'derives' && row.id !== undefined) derived.set(row.id, row)
1049
- }
1050
- if (LISTS.some((name) => row.fields.has(name))) into(object, row.type, at, under, by, true)
1051
- if (row.at !== undefined) open(row.at, wanted === undefined ? undefined : elementOf(checker, wanted), under, row.id ?? by, by, list !== 'derives' || row.at.startsWith(TREE))
1052
- }
1053
- }
1054
- }
1055
-
1056
- function open(at: string, expected: ts.Type | undefined, under: string, parent: string, by: string, reads: boolean): void {
1057
- const already = opened.has(at)
1058
- opened.add(at)
1059
- if (!SOURCE.test(at)) return
1060
- const file = program.getSourceFile(resolve(ROOT, at))
1061
- if (file === undefined) return
1062
- if (!reads) return
1063
-
1064
- if (!already) {
1065
- for (const one of program.getSyntacticDiagnostics(file)) {
1066
- broken.push({
1067
- file: at,
1068
- detail: `this file does not parse, so whatever it declares was never read and every child under it reads as absent: ${ts.flattenDiagnosticMessageText(one.messageText, ' ')}`,
1069
- instead: 'fix the syntax. A file the descent cannot read declares nothing, and nothing below it can be found missing',
1070
- })
1071
- break
1072
- }
1073
- }
1074
-
1075
- for (const one of nodesIn(at)) {
1076
- if (expected !== undefined && !checker.isTypeAssignableTo(one.shape, expected)) {
1077
- findings.push({
1078
- file: at,
1079
- detail: `\`${one.name}\` here is \`${one.type}\`, and \`${said(by)}\` states its child is \`${checker.typeToString(expected)}\``,
1080
- instead: `annotate it \`${checker.typeToString(expected)}\`, or a shape that satisfies it, or change the row. A parent states the shape of its child, and a child answering with another shape is a child of something else`,
1081
- })
1082
- continue
1083
- }
1084
- if (already) continue
1085
- into(one.node, one.type, at, under, parent)
1086
- }
1087
- }
1088
-
1089
- opened.add(THREAD_AT)
1090
- for (const one of nodesIn(THREAD_AT)) into(one.node, one.type, THREAD_AT, '', '')
1091
-
1092
- for (const at of [...opened].filter((one) => SOURCE.test(one))) {
1093
- const file = program.getSourceFile(resolve(ROOT, at))
1094
- if (file === undefined) continue
1095
- for (const one of program.getSemanticDiagnostics(file)) {
1096
- findings.push({
1097
- file: at,
1098
- detail: `the compiler refuses this declaration: ${ts.flattenDiagnosticMessageText(one.messageText, ' ')}`,
1099
- instead:
1100
- 'make the declaration the shape its own type states. A row the compiler will not accept is a row nothing downstream can read',
1101
- })
1102
- }
1103
- }
1104
-
1105
- for (const [what, held] of made) {
1106
- if (held.by.size === 0) {
1107
- findings.push({
1108
- file: held.where,
1109
- detail: `\`${what}\` is derived by ${held.rows.length === 1 ? `\`${held.rows[0] ?? ''}\`` : `${String(held.rows.length)} rows`}, and not one of them states a \`by\``,
1110
- instead:
1111
- 'name the command that writes it on one of those rows. A derived file whose generator is unnamed cannot be compared against what it should be, so drift in it is invisible',
1112
- })
1113
- continue
1114
- }
1115
- if (held.by.size < 2) continue
1116
- findings.push({
1117
- file: held.where,
1118
- detail: `\`${what}\` is derived by ${[...held.by].map((one) => `\`${one}\``).join(' and ')}`,
1119
- instead: 'one file has one generator. Two commands writing one address means whichever runs last wins and the other is overwritten in silence',
1120
- })
1121
- }
1122
-
1123
- const answers = (command: string): boolean =>
1124
- offered.has(command) || [...offered].some((one) => command.startsWith(`${one} --`))
1125
-
1126
- for (const one of thread) {
1127
- if (answers(one.command)) continue
1128
- findings.push({
1129
- file: one.row.where,
1130
- detail: `\`${names(one.row)}\` states \`runs: '${one.command}'\`, and no row declares a \`by\` it is a mode of`,
1131
- instead:
1132
- 'name a command some row brings into existence with a `by`, or a `--` mode of one. A row proving a byproduct proves what another row writes, so the writer has to exist',
1133
- })
1134
- }
1135
-
1136
- for (const one of demanded) {
1137
- if (coined.has(one.word)) continue
1138
- findings.push({
1139
- file: one.row.where,
1140
- detail: `\`${names(one.row)}\` ${one.list} \`${one.word}\`, and nothing in this tree coins that word`,
1141
- instead: `declare something with \`id: '${one.word}'\`, or fix the name. A demand naming nothing is met by nothing, and nothing tells you so`,
1142
- })
1143
- }
1144
-
1145
- for (const [word, held] of coined) {
1146
- const meanings = [...new Set(held.said.filter((one) => one !== ''))]
1147
- if (meanings.length < 2) continue
1148
- findings.push({
1149
- file: THREAD_AT,
1150
- detail: `\`${word}\` is coined ${String(meanings.length)} times with different meanings: ${meanings.map((one) => `"${one.slice(0, 60)}${one.length > 60 ? '…' : ''}"`).join(' and ')}`,
1151
- instead: 'one word means one thing. Keep the row that coins it and make the others name what they actually are, or say the same sentence in both',
1152
- })
1153
- }
1154
-
1155
- for (const [name, where] of shapesNamed()) {
1156
- if (where.length < 2) continue
1157
- findings.push({
1158
- file: where[1] ?? THREAD_AT,
1159
- detail: `\`${name}\` is a shape the tree threads by, and it is declared in ${String(where.length)} places: ${where.join(', ')}`,
1160
- instead: 'keep one and delete the rest. Whichever the descent reads first decides what every declaration is held to, so a second one silently rewrites the rules for the whole tree',
1161
- })
1162
- }
1163
-
1164
- for (const one of sources()) {
1165
- if (!SOURCE.test(one)) continue
1166
- for (const at of mentionsIn(one)) {
1167
- const held = mentioned.get(at) ?? []
1168
- if (!held.includes(one)) mentioned.set(at, [...held, one])
1169
- }
1170
- }
1171
-
1172
- for (const [path, held] of absent) {
1173
- if (held.rows < 2) continue
1174
- const one = findings[held.at]
1175
- if (one === undefined) continue
1176
-
1177
- findings[held.at] = {
1178
- ...one,
1179
- detail: `${String(held.rows)} rows derive \`${path}\`, and nothing is there yet — ${[...held.by].sort().join(', ')}`,
1180
- }
1181
- }
1182
-
1183
- passed = { threaded, mentioned, derived, stated, coined, broken, findings, declarations, rows, sources: sources() }
1184
- return passed
1185
- }