@prestack/thread 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/core/actor.ts +20 -0
  2. package/dist/core/asking.d.ts +0 -1
  3. package/dist/core/asking.js +0 -1
  4. package/dist/core/asking.ts +1342 -0
  5. package/dist/core/attach.js +2 -2
  6. package/dist/core/attach.ts +585 -0
  7. package/dist/core/chain.js +8 -1
  8. package/dist/core/chain.ts +148 -0
  9. package/dist/core/cli.js +97 -18
  10. package/dist/core/cli.ts +2004 -0
  11. package/dist/core/condition.ts +227 -0
  12. package/dist/core/descend.d.ts +1 -0
  13. package/dist/core/descend.js +36 -0
  14. package/dist/core/descend.ts +1001 -0
  15. package/dist/core/finding.ts +38 -0
  16. package/dist/core/generators/meta.js +6 -0
  17. package/dist/core/generators/meta.ts +1006 -0
  18. package/dist/core/init.ts +170 -0
  19. package/dist/core/invoked.ts +47 -0
  20. package/dist/core/round.ts +57 -0
  21. package/dist/core/rows.ts +369 -0
  22. package/dist/core/scope.ts +18 -0
  23. package/dist/core/thread.ts +29 -0
  24. package/dist/core/tree.ts +67 -0
  25. package/dist/core/types.ts +350 -0
  26. package/dist/derivation/attach.js +1 -1
  27. package/dist/derivation/attach.ts +68 -0
  28. package/dist/derivation/generators/build.ts +232 -0
  29. package/dist/derivation/generators/check.ts +118 -0
  30. package/dist/derivation/generators/derivation.ts +347 -0
  31. package/dist/derivation/generators/determinism.ts +145 -0
  32. package/dist/derivation/generators/release.js +19 -9
  33. package/dist/derivation/generators/release.ts +213 -0
  34. package/dist/derivation/makers/release.ts +135 -0
  35. package/dist/derivation/release.ts +23 -0
  36. package/dist/integrations/attach.js +2 -2
  37. package/dist/integrations/attach.ts +207 -0
  38. package/dist/integrations/generators/read.ts +127 -0
  39. package/dist/integrations/generators/secret.ts +289 -0
  40. package/dist/integrations/generators/target.js +16 -6
  41. package/dist/integrations/generators/target.ts +944 -0
  42. package/dist/integrations/generators/vault.js +46 -14
  43. package/dist/integrations/generators/vault.ts +212 -0
  44. package/dist/integrations/{sdk.js → module.js} +4 -4
  45. package/dist/integrations/module.ts +189 -0
  46. package/dist/integrations/secret.ts +50 -0
  47. package/dist/integrations/target.d.ts +3 -1
  48. package/dist/integrations/target.js +14 -11
  49. package/dist/integrations/target.ts +340 -0
  50. package/dist/integrations/vault.d.ts +2 -0
  51. package/dist/integrations/vault.js +4 -2
  52. package/dist/integrations/vault.ts +136 -0
  53. package/dist/integrity/attach.ts +234 -0
  54. package/dist/integrity/check-threads.js +2 -26
  55. package/dist/integrity/check-threads.ts +166 -0
  56. package/dist/integrity/constraint.ts +1202 -0
  57. package/dist/integrity/generators/compiler.ts +161 -0
  58. package/dist/integrity/generators/gate.ts +201 -0
  59. package/dist/integrity/generators/issues.ts +160 -0
  60. package/dist/integrity/generators/publish.js +14 -5
  61. package/dist/integrity/generators/publish.ts +247 -0
  62. package/dist/integrity/generators/rule.ts +271 -0
  63. package/dist/integrity/generators/seal.ts +117 -0
  64. package/dist/integrity/generators/trial.ts +320 -0
  65. package/dist/integrity/lint.ts +227 -0
  66. package/dist/integrity/lsp/bin.ts +42 -0
  67. package/dist/integrity/lsp/diagnostics.ts +92 -0
  68. package/dist/integrity/lsp/findings.ts +63 -0
  69. package/dist/integrity/lsp/lint.ts +140 -0
  70. package/dist/integrity/lsp/protocol.ts +78 -0
  71. package/dist/integrity/lsp/server.ts +224 -0
  72. package/dist/integrity/sealed.js +2 -2
  73. package/dist/integrity/sealed.ts +56 -0
  74. package/dist/integrity/slopnator.ts +350 -0
  75. package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
  76. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
  77. package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
  78. package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
  79. package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
  80. package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
  81. package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
  82. package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
  83. package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
  84. package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
  85. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
  86. package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
  87. package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
  88. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
  89. package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
  90. package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
  91. package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
  92. package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
  93. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
  94. package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
  95. package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
  96. package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
  97. package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
  98. package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
  99. package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
  100. package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
  101. package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
  102. package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
  103. package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
  104. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
  105. package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
  106. package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
  107. package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
  108. package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
  109. package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
  110. package/dist/integrity/sweep.ts +307 -0
  111. package/dist/knowledge/attach.ts +350 -0
  112. package/dist/knowledge/generators/docsgen.ts +174 -0
  113. package/dist/knowledge/generators/glossary.ts +73 -0
  114. package/dist/knowledge/generators/skills.ts +263 -0
  115. package/dist/knowledge/glossary.ts +6 -0
  116. package/dist/operations/access.ts +88 -0
  117. package/dist/operations/attach.ts +475 -0
  118. package/dist/operations/capabilities.ts +65 -0
  119. package/dist/operations/cost.ts +58 -0
  120. package/dist/operations/dispatch.ts +348 -0
  121. package/dist/operations/error-results.ts +31 -0
  122. package/dist/operations/errors/catalog.ts +168 -0
  123. package/dist/operations/errors/classes.ts +51 -0
  124. package/dist/operations/errors/errors.ts +314 -0
  125. package/dist/operations/generators/capability.js +29 -19
  126. package/dist/operations/generators/capability.ts +704 -0
  127. package/dist/operations/generators/compile.js +20 -1
  128. package/dist/operations/generators/compile.ts +643 -0
  129. package/dist/operations/generators/grant.ts +336 -0
  130. package/dist/operations/generators/operation.ts +972 -0
  131. package/dist/operations/generators/permits.ts +223 -0
  132. package/dist/operations/generators/seed.ts +347 -0
  133. package/dist/operations/generators/trial.js +4 -1
  134. package/dist/operations/generators/trial.ts +529 -0
  135. package/dist/operations/http.ts +164 -0
  136. package/dist/operations/makers/capability.ts +166 -0
  137. package/dist/operations/makers/trial.ts +520 -0
  138. package/dist/operations/memory.ts +470 -0
  139. package/dist/operations/metering.ts +328 -0
  140. package/dist/operations/nodes.ts +54 -0
  141. package/dist/operations/operations.ts +32 -0
  142. package/dist/operations/queue-policy.ts +10 -0
  143. package/dist/operations/result.ts +28 -0
  144. package/dist/operations/seams.d.ts +4 -4
  145. package/dist/operations/seams.ts +364 -0
  146. package/dist/operations/store.ts +29 -0
  147. package/package.json +8 -1
  148. package/dist/integrations/generators/sdk.d.ts +0 -1
  149. package/dist/integrations/generators/sdk.js +0 -148
  150. /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
@@ -0,0 +1,1001 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'
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 { ignored } from './init.ts'
9
+ import { NAMED, ORGANISATION } 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
+ const OWN_IGNORES: readonly string[] | undefined = (() => {
39
+ if (spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: ROOT, stdio: 'ignore' }).status === 0) {
40
+ return undefined
41
+ }
42
+ try {
43
+ return readFileSync(join(ROOT, '.gitignore'), 'utf8')
44
+ .split('\n')
45
+ .map((one) => one.trim())
46
+ .filter((one) => one !== '' && !one.startsWith('#') && !one.startsWith('!'))
47
+ .map((one) => one.replace(/^\/+|\/+$/g, ''))
48
+ .filter((one) => one !== '' && !one.includes('*'))
49
+ } catch {
50
+ return []
51
+ }
52
+ })()
53
+
54
+ const asked = new Map<string, boolean>()
55
+
56
+ export function unkept(at: string): boolean {
57
+ const held = asked.get(at)
58
+ if (held !== undefined) return held
59
+ const found =
60
+ OWN_IGNORES === undefined
61
+ ? spawnSync('git', ['check-ignore', '-q', '--', at], { cwd: ROOT, stdio: 'ignore' }).status === 0
62
+ : OWN_IGNORES.some((one) => at === one || at.startsWith(`${one}/`) || at.split('/').includes(one))
63
+ asked.set(at, found)
64
+ return found
65
+ }
66
+
67
+ export const LISTS = ['declares', 'derives', 'seeds'] as const
68
+
69
+ const SOURCE = /\.(ts|tsx|mts|cts)$/
70
+ const RUNNER = /^(?:pnpm|npm|yarn) (?:run )?([\w:.-]+)/
71
+
72
+ export interface Row {
73
+ readonly at?: string
74
+ readonly id?: string
75
+ readonly describes?: string
76
+ readonly by?: string
77
+ readonly runs?: string
78
+
79
+ readonly requires?: readonly string[]
80
+ readonly takes?: readonly string[]
81
+
82
+ readonly type: string
83
+ readonly shape?: ts.Type
84
+ readonly fields: ReadonlyMap<string, ts.Expression>
85
+ readonly where: string
86
+ }
87
+
88
+ export interface Pass {
89
+
90
+ readonly threaded: ReadonlyMap<
91
+ string,
92
+ readonly { readonly list: 'declares' | 'derives' | 'seeds'; readonly by: string; readonly row: Row }[]
93
+ >
94
+ readonly mentioned: ReadonlyMap<string, readonly string[]>
95
+ readonly derived: ReadonlyMap<string, Row>
96
+
97
+ readonly stated: ReadonlyMap<string, Row>
98
+ readonly coined: ReadonlyMap<string, { readonly said: readonly string[]; readonly under: string; readonly from: readonly string[] }>
99
+ readonly broken: readonly Finding[]
100
+ readonly findings: readonly Finding[]
101
+ readonly declarations: number
102
+ readonly rows: number
103
+ readonly sources: readonly string[]
104
+ }
105
+
106
+ export type { Finding }
107
+
108
+ export function under(pass: Pass, id: string, root: string, seen: Set<string> = new Set()): boolean {
109
+ if (id === root) return true
110
+ if (seen.has(id)) return false
111
+ seen.add(id)
112
+ return (pass.coined.get(id)?.from ?? []).some((one) => under(pass, one, root, seen))
113
+ }
114
+
115
+ function here(path: string): string {
116
+ return relative(ROOT, path).split('\\').join('/')
117
+ }
118
+
119
+ const CANONICAL = 'threads/'
120
+
121
+ const BESIDE = /^\.\//
122
+
123
+ function settle(at: string, where: string): string {
124
+ if (BESIDE.test(at)) return here(resolve(ROOT, dirname(where), at.slice(2)))
125
+ if (at.startsWith(CANONICAL)) return here(resolve(ROOT, TREE, at.slice(CANONICAL.length)))
126
+ return here(resolve(ROOT, at))
127
+ }
128
+
129
+ function itself(at: string): string {
130
+ try {
131
+ return realpathSync(resolve(ROOT, at))
132
+ } catch {
133
+ return resolve(ROOT, at)
134
+ }
135
+ }
136
+
137
+ function walk(dir: string, out: string[]): string[] {
138
+ let entries: readonly string[]
139
+ try {
140
+ entries = readdirSync(dir)
141
+ } catch {
142
+ return out
143
+ }
144
+ for (const name of [...entries].sort()) {
145
+ if (name === '.git' || BUILT.has(name)) continue
146
+ const path = join(dir, name)
147
+ let held
148
+ try {
149
+ held = statSync(path)
150
+ } catch {
151
+ continue
152
+ }
153
+ if (held.isDirectory()) walk(path, out)
154
+ else out.push(here(path))
155
+ }
156
+ return out
157
+ }
158
+
159
+ let scanned: readonly string[] | undefined
160
+
161
+ export function sources(): readonly string[] {
162
+ rooted()
163
+ scanned ??= walk(ROOT, []).sort()
164
+ return scanned
165
+ }
166
+
167
+ function shipped(): readonly string[] {
168
+ const out: string[] = []
169
+ const base = join(ROOT, 'node_modules', ORGANISATION)
170
+ let entries: readonly string[]
171
+ try {
172
+ entries = readdirSync(base)
173
+ } catch {
174
+ return out
175
+ }
176
+ for (const name of [...entries].sort()) walk(join(base, name, 'src'), out)
177
+ return out
178
+ }
179
+
180
+ const FALLBACK: ts.CompilerOptions = {
181
+ target: ts.ScriptTarget.Latest,
182
+ module: ts.ModuleKind.ESNext,
183
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
184
+ jsx: ts.JsxEmit.ReactJSX,
185
+ strict: true,
186
+ allowImportingTsExtensions: true,
187
+ allowJs: true,
188
+ resolveJsonModule: true,
189
+ }
190
+
191
+ const CONFIGS = ['tsconfig.json', 'tsconfig.base.json'] as const
192
+
193
+ const INVARIANT: ts.CompilerOptions = {
194
+ noEmit: true,
195
+ skipLibCheck: true,
196
+ allowImportingTsExtensions: true,
197
+ allowJs: true,
198
+ jsx: ts.JsxEmit.ReactJSX,
199
+ }
200
+
201
+ export function configured(): { readonly options: ts.CompilerOptions; readonly from: string; readonly trouble?: string } {
202
+ for (const name of CONFIGS) {
203
+ const at = join(ROOT, name)
204
+ if (!ts.sys.fileExists(at)) continue
205
+
206
+ const held = ts.readConfigFile(at, ts.sys.readFile.bind(ts.sys))
207
+ if (held.error !== undefined) {
208
+ return {
209
+ options: { ...FALLBACK, ...INVARIANT },
210
+ from: '',
211
+ trouble: `${name} is there and cannot be read: ${ts.flattenDiagnosticMessageText(held.error.messageText, ' ')}`,
212
+ }
213
+ }
214
+
215
+ const parsed = ts.parseJsonConfigFileContent(held.config as object, ts.sys, dirname(at))
216
+ const complained = parsed.errors[0]
217
+ if (complained !== undefined) {
218
+ return {
219
+ options: { ...FALLBACK, ...INVARIANT },
220
+ from: '',
221
+ trouble: `${name} is there and states no options this gate can use: ${ts.flattenDiagnosticMessageText(complained.messageText, ' ')}`,
222
+ }
223
+ }
224
+
225
+ return { options: { ...parsed.options, ...INVARIANT }, from: name }
226
+ }
227
+ return { options: { ...FALLBACK, ...INVARIANT }, from: '' }
228
+ }
229
+
230
+ let built: { program: ts.Program; checker: ts.TypeChecker } | undefined
231
+
232
+ function compiled(): { program: ts.Program; checker: ts.TypeChecker } {
233
+ rooted()
234
+ if (built !== undefined) return built
235
+ const roots = [THREAD_AT, ...sources(), ...shipped()].filter((one) => SOURCE.test(one)).map((one) => resolve(ROOT, one))
236
+ const program = ts.createProgram([...new Set(roots)], configured().options)
237
+ built = { program, checker: program.getTypeChecker() }
238
+ return built
239
+ }
240
+
241
+ export function uncompiled(): readonly Finding[] {
242
+ const { program } = compiled()
243
+ const found: Finding[] = []
244
+ const INSTALLED = 'node_' + 'modules'
245
+ for (const file of program.getSourceFiles()) {
246
+ if (file.isDeclarationFile) continue
247
+ const at = here(file.fileName)
248
+ if (at.startsWith('..') || at.split('/').includes(INSTALLED)) continue
249
+ for (const one of [...program.getSyntacticDiagnostics(file), ...program.getSemanticDiagnostics(file)]) {
250
+ const start = one.start === undefined ? undefined : file.getLineAndCharacterOfPosition(one.start)
251
+ found.push({
252
+ file: at,
253
+ at: start === undefined ? undefined : { line: start.line + 1, column: start.character + 1 },
254
+ detail: `will not compile — TS${String(one.code)}: ${ts.flattenDiagnosticMessageText(one.messageText, ' ')}`,
255
+ instead:
256
+ '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',
257
+ })
258
+ }
259
+ }
260
+ return found
261
+ }
262
+
263
+ function threadedType(checker: ts.TypeChecker, program: ts.Program): ts.Type | undefined {
264
+ const root = program.getSourceFile(resolve(ROOT, THREAD_AT))
265
+ if (root === undefined) return undefined
266
+ for (const statement of root.statements) {
267
+ if (!ts.isImportDeclaration(statement)) continue
268
+ const bindings = statement.importClause?.namedBindings
269
+ if (bindings === undefined || !ts.isNamedImports(bindings)) continue
270
+ for (const one of bindings.elements) {
271
+ const symbol = checker.getSymbolAtLocation(one.name)
272
+ if (symbol === undefined) continue
273
+ const real = (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol
274
+ if (real.name === 'Threaded') return checker.getDeclaredTypeOfSymbol(real)
275
+ }
276
+ }
277
+ return undefined
278
+ }
279
+
280
+ const source = new Map<string, string | undefined>()
281
+
282
+ export function read(at: string): string | undefined {
283
+ if (source.has(at)) return source.get(at)
284
+ let text: string | undefined
285
+ try {
286
+ text = readFileSync(resolve(ROOT, at), 'utf8')
287
+ } catch {
288
+ text = undefined
289
+ }
290
+ source.set(at, text)
291
+ return text
292
+ }
293
+
294
+ export function shapesNamed(): ReadonlyMap<string, readonly string[]> {
295
+ const { program, checker } = compiled()
296
+ const shape = threadedType(checker, program)
297
+ const found = new Map<string, string[]>()
298
+ if (shape === undefined) return found
299
+ for (const at of [...sources(), ...shipped()]) {
300
+ if (!SOURCE.test(at)) continue
301
+ const file = program.getSourceFile(resolve(ROOT, at))
302
+ if (file === undefined) continue
303
+ for (const statement of file.statements) {
304
+ if (!ts.isInterfaceDeclaration(statement) && !ts.isTypeAliasDeclaration(statement)) continue
305
+ if ((statement.typeParameters ?? []).length > 0) continue
306
+ const symbol = checker.getSymbolAtLocation(statement.name)
307
+ if (symbol === undefined) continue
308
+ const shown = (statement.modifiers ?? []).some((one) => one.kind === ts.SyntaxKind.ExportKeyword)
309
+ if (!shown && statement.name.text !== 'Threaded') continue
310
+ const held = ts.isInterfaceDeclaration(statement)
311
+ ? checker.getDeclaredTypeOfSymbol(symbol)
312
+ : checker.getTypeAtLocation(statement.name)
313
+ const named = statement.name.text
314
+ const shadows = named === 'Threaded'
315
+ const kind = elementOf(checker, held)
316
+ if (!shadows && (kind.flags & ts.TypeFlags.Never) !== 0) continue
317
+ if (!shadows && !checker.isTypeAssignableTo(kind, shape)) continue
318
+ const where = found.get(named) ?? []
319
+ const real = itself(at)
320
+ if (where.some((one) => itself(one) === real)) continue
321
+ found.set(named, [...where, at])
322
+ }
323
+ }
324
+ return found
325
+ }
326
+
327
+ export function threading(): ReadonlySet<string> {
328
+ return new Set(['Threaded', ...shapesNamed().keys()])
329
+ }
330
+
331
+ export function shapeFields(
332
+ named: string,
333
+ inside?: string,
334
+ ): readonly { readonly name: string; readonly required: boolean; readonly list: boolean; readonly map: boolean; readonly worded: boolean; readonly guide: string }[] {
335
+ const { program, checker } = compiled()
336
+ for (const at of [...sources(), ...shipped()]) {
337
+ if (!SOURCE.test(at)) continue
338
+ if (inside !== undefined && !at.split('\\').join('/').endsWith(inside)) continue
339
+ const file = program.getSourceFile(resolve(ROOT, at))
340
+ if (file === undefined) continue
341
+ for (const statement of file.statements) {
342
+ if (!ts.isInterfaceDeclaration(statement) && !ts.isTypeAliasDeclaration(statement)) continue
343
+ if (statement.name.text !== named) continue
344
+ const symbol = checker.getSymbolAtLocation(statement.name)
345
+ if (symbol === undefined) continue
346
+ const held = ts.isInterfaceDeclaration(statement)
347
+ ? checker.getDeclaredTypeOfSymbol(symbol)
348
+ : checker.getTypeAtLocation(statement.name)
349
+ const mapped = (kind: ts.Type): boolean =>
350
+ (kind.isUnion() ? kind.types : [kind]).some(
351
+ (each) =>
352
+ (each.flags & ts.TypeFlags.StringLike) === 0 &&
353
+ checker.getIndexTypeOfType(each, ts.IndexKind.Number) === undefined &&
354
+ checker.getIndexTypeOfType(each, ts.IndexKind.String) !== undefined,
355
+ )
356
+ const listed = (kind: ts.Type): boolean =>
357
+ (kind.isUnion() ? kind.types : [kind]).some(
358
+ (each) =>
359
+ (each.flags & ts.TypeFlags.StringLike) === 0 &&
360
+ checker.getIndexTypeOfType(each, ts.IndexKind.Number) !== undefined,
361
+ )
362
+ return checker.getPropertiesOfType(held).map((one) => {
363
+ const kind = checker.getTypeOfSymbolAtLocation(one, statement)
364
+ return {
365
+ name: one.getName(),
366
+ required: (one.flags & ts.SymbolFlags.Optional) === 0,
367
+ list: listed(kind),
368
+ map: mapped(kind),
369
+ worded: (kind.isUnion() ? kind.types : [kind]).some((each) => (each.flags & ts.TypeFlags.StringLike) !== 0),
370
+ guide: ts.displayPartsToString(one.getDocumentationComment(checker)).trim(),
371
+ }
372
+ })
373
+ }
374
+ }
375
+ return []
376
+ }
377
+
378
+ export interface Field {
379
+ readonly name: string
380
+ readonly required: boolean
381
+ readonly list: boolean
382
+ readonly map: boolean
383
+ readonly boolean: boolean
384
+ readonly worded: boolean
385
+ readonly guide: string
386
+ }
387
+
388
+ function elementAt(at: string, list: string): { readonly type: ts.Type; readonly node: ts.Node } | undefined {
389
+ const { program, checker } = compiled()
390
+ const file = program.getSourceFile(resolve(ROOT, at))
391
+ if (file === undefined) return undefined
392
+ const threaded = threadedType(checker, program)
393
+
394
+ let held: { readonly type: ts.Type; readonly node: ts.Node } | undefined
395
+
396
+ const consider = (node: ts.ArrayLiteralExpression): void => {
397
+ if (held !== undefined) return
398
+ const wanted = checker.getContextualType(node) ?? checker.getTypeAtLocation(node)
399
+ const kinds = (wanted.isUnionOrIntersection() ? wanted.types : [wanted])
400
+ .filter((one) => checker.getIndexTypeOfType(one, ts.IndexKind.Number) !== undefined)
401
+ .map((one) => elementOf(checker, one))
402
+ const type =
403
+ kinds.find((one) => threaded !== undefined && checker.isTypeAssignableTo(one, threaded)) ??
404
+ (kinds.length === 1 ? kinds[0] : undefined)
405
+ if (type !== undefined) held = { type, node }
406
+ }
407
+
408
+ const visit = (node: ts.Node): void => {
409
+ if (held !== undefined) return
410
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === list) {
411
+ const initial = node.initializer
412
+ if (initial !== undefined && ts.isArrayLiteralExpression(initial)) consider(initial)
413
+ }
414
+ if (ts.isPropertyAssignment(node)) {
415
+ const key = ts.isIdentifier(node.name) || ts.isStringLiteralLike(node.name) ? node.name.text : undefined
416
+ if (key === list && ts.isArrayLiteralExpression(node.initializer)) consider(node.initializer)
417
+ }
418
+ ts.forEachChild(node, visit)
419
+ }
420
+
421
+ visit(file)
422
+ return held
423
+ }
424
+
425
+ export function listShape(at: string, list: string): string | undefined {
426
+ const held = elementAt(at, list)
427
+ return held === undefined ? undefined : compiled().checker.typeToString(held.type)
428
+ }
429
+
430
+ export function listFields(at: string, list: string): readonly Field[] {
431
+ const held = elementAt(at, list)
432
+ if (held === undefined) return []
433
+ const { checker } = compiled()
434
+ const bare = (kind: ts.Type): readonly ts.Type[] =>
435
+ (kind.isUnion() ? kind.types : [kind]).filter((each) => (each.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) === 0)
436
+ const mapped = (kind: ts.Type): boolean =>
437
+ bare(kind).some(
438
+ (each) =>
439
+ (each.flags & ts.TypeFlags.StringLike) === 0 &&
440
+ checker.getIndexTypeOfType(each, ts.IndexKind.Number) === undefined &&
441
+ checker.getIndexTypeOfType(each, ts.IndexKind.String) !== undefined,
442
+ )
443
+ const listed = (kind: ts.Type): boolean =>
444
+ bare(kind).some(
445
+ (each) => (each.flags & ts.TypeFlags.StringLike) === 0 && checker.getIndexTypeOfType(each, ts.IndexKind.Number) !== undefined,
446
+ )
447
+ const flagged = (kind: ts.Type): boolean => {
448
+ const kinds = bare(kind)
449
+ return kinds.length > 0 && kinds.every((each) => (each.flags & ts.TypeFlags.BooleanLike) !== 0)
450
+ }
451
+ const worded = (kind: ts.Type): boolean => bare(kind).some((each) => (each.flags & ts.TypeFlags.StringLike) !== 0)
452
+ return checker.getPropertiesOfType(held.type).map((one) => {
453
+ const kind = checker.getTypeOfSymbolAtLocation(one, held.node)
454
+ return {
455
+ name: one.getName(),
456
+ required: (one.flags & ts.SymbolFlags.Optional) === 0,
457
+ list: listed(kind),
458
+ map: mapped(kind),
459
+ boolean: flagged(kind),
460
+ worded: worded(kind),
461
+ guide: ts.displayPartsToString(one.getDocumentationComment(checker)).trim(),
462
+ }
463
+ })
464
+ }
465
+
466
+ function elementOf(checker: ts.TypeChecker, type: ts.Type): ts.Type {
467
+ return checker.getIndexTypeOfType(type, ts.IndexKind.Number) ?? type
468
+ }
469
+
470
+ export function rowShape(at: string, list: string): string | undefined {
471
+ const { program, checker } = compiled()
472
+ const file = program.getSourceFile(resolve(ROOT, at))
473
+ const threaded = threadedType(checker, program)
474
+ if (file === undefined || threaded === undefined) return undefined
475
+
476
+ let held: ts.Type | undefined
477
+
478
+ const consider = (node: ts.ArrayLiteralExpression): void => {
479
+ if (held !== undefined) return
480
+ const wanted = checker.getContextualType(node) ?? checker.getTypeAtLocation(node)
481
+ const each = (wanted.isUnionOrIntersection() ? wanted.types : [wanted]).map((one) => elementOf(checker, one))
482
+ held = each.find((one) => checker.isTypeAssignableTo(one, threaded))
483
+ }
484
+
485
+ const visit = (node: ts.Node): void => {
486
+ if (held !== undefined) return
487
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === list) {
488
+ const initial = node.initializer
489
+ if (initial !== undefined && ts.isArrayLiteralExpression(initial)) consider(initial)
490
+ }
491
+ if (ts.isPropertyAssignment(node)) {
492
+ const key = ts.isIdentifier(node.name) || ts.isStringLiteralLike(node.name) ? node.name.text : undefined
493
+ if (key === list && ts.isArrayLiteralExpression(node.initializer)) consider(node.initializer)
494
+ }
495
+ ts.forEachChild(node, visit)
496
+ }
497
+
498
+ visit(file)
499
+ return held === undefined ? undefined : checker.typeToString(held)
500
+ }
501
+
502
+ function literal(node: ts.Expression | undefined): string | undefined {
503
+ if (node === undefined) return undefined
504
+ return ts.isStringLiteralLike(node) ? node.text : undefined
505
+ }
506
+
507
+ function fieldsOf(node: ts.ObjectLiteralExpression): Map<string, ts.Expression> {
508
+ const found = new Map<string, ts.Expression>()
509
+ for (const property of node.properties) {
510
+ if (!ts.isPropertyAssignment(property)) continue
511
+ const key = ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name) ? property.name.text : undefined
512
+ if (key !== undefined) found.set(key, property.initializer)
513
+ }
514
+ return found
515
+ }
516
+
517
+ function wordsOf(node: ts.Expression | undefined): readonly string[] {
518
+ if (node === undefined || !ts.isArrayLiteralExpression(node)) return []
519
+ return node.elements.map((one) => literal(one)).filter((one): one is string => one !== undefined)
520
+ }
521
+
522
+ function rowOf(node: ts.ObjectLiteralExpression, type: string, where: string, shape?: ts.Type): Row {
523
+ const fields = fieldsOf(node)
524
+ const named = literal(fields.get('at'))
525
+ const at = named === undefined ? undefined : settle(named, where)
526
+ const id = literal(fields.get('id'))
527
+ const describes = literal(fields.get('describes'))
528
+ const by = literal(fields.get('by'))
529
+ const runs = literal(fields.get('runs'))
530
+ const requires = wordsOf(fields.get('requires'))
531
+ const takes = wordsOf(fields.get('takes'))
532
+ return {
533
+ ...(at === undefined ? {} : { at }),
534
+ ...(id === undefined ? {} : { id }),
535
+ ...(describes === undefined ? {} : { describes }),
536
+ ...(by === undefined ? {} : { by }),
537
+ ...(runs === undefined ? {} : { runs }),
538
+ ...(requires.length === 0 ? {} : { requires }),
539
+ ...(takes.length === 0 ? {} : { takes }),
540
+ ...(shape === undefined ? {} : { shape }),
541
+ type,
542
+ fields,
543
+ where,
544
+ }
545
+ }
546
+
547
+ function unwrap(node: ts.Expression | undefined): ts.Expression | undefined {
548
+ let held = node
549
+ for (;;) {
550
+ if (held === undefined) return undefined
551
+ if (ts.isParenthesizedExpression(held) || ts.isSatisfiesExpression(held) || ts.isAsExpression(held)) {
552
+ held = held.expression
553
+ continue
554
+ }
555
+ return held
556
+ }
557
+ }
558
+
559
+ interface Held {
560
+ readonly name: string
561
+ readonly type: string
562
+ readonly shape: ts.Type
563
+ readonly node: ts.ObjectLiteralExpression
564
+ }
565
+
566
+ export function nodesIn(at: string): readonly Held[] {
567
+ const { program, checker } = compiled()
568
+ const threaded = threadedType(checker, program)
569
+ const file = program.getSourceFile(resolve(ROOT, at))
570
+ if (file === undefined || threaded === undefined) return []
571
+
572
+ const found: Held[] = []
573
+
574
+ const take = (name: string, where: ts.Node, initial: ts.Expression | undefined): void => {
575
+ const held = unwrap(initial)
576
+ if (held === undefined) return
577
+
578
+ const recognised = (one: ts.Type): boolean => (one.flags & ts.TypeFlags.Any) === 0 && checker.isTypeAssignableTo(one, threaded)
579
+ const declared = checker.getTypeAtLocation(where)
580
+ const each = (declared.isUnionOrIntersection() ? declared.types : [declared]).map((one) => elementOf(checker, one))
581
+ const stated = each.find((one) => recognised(one))
582
+
583
+ const parts = ts.isArrayLiteralExpression(held) ? held.elements : [held]
584
+ for (const one of parts) {
585
+ const object = unwrap(one)
586
+ if (object === undefined || !ts.isObjectLiteralExpression(object)) continue
587
+ const mine = checker.getWidenedType(checker.getTypeAtLocation(object))
588
+ const known = stated ?? (recognised(mine) ? mine : undefined)
589
+ if (known === undefined) continue
590
+ found.push({ name, type: checker.typeToString(known), shape: known, node: object })
591
+ }
592
+ }
593
+
594
+ const step = (statements: readonly ts.Statement[]): void => {
595
+ for (const statement of statements) {
596
+ if (ts.isModuleDeclaration(statement) && statement.body !== undefined && ts.isModuleBlock(statement.body)) {
597
+ step(statement.body.statements)
598
+ continue
599
+ }
600
+ if (ts.isExportAssignment(statement)) {
601
+ take('default', statement.expression, statement.expression)
602
+ continue
603
+ }
604
+ if (!ts.isVariableStatement(statement)) continue
605
+ for (const one of statement.declarationList.declarations) {
606
+ if (!ts.isIdentifier(one.name) || one.initializer === undefined) continue
607
+ take(one.name.text, one.name, one.initializer)
608
+ }
609
+ }
610
+ }
611
+
612
+ step(file.statements)
613
+ return found
614
+ }
615
+
616
+ let commands: ReadonlySet<string> | undefined
617
+
618
+ function scripts(): ReadonlySet<string> {
619
+ if (commands !== undefined) return commands
620
+ const found = new Set<string>()
621
+ try {
622
+ const held = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf8')) as { scripts?: Record<string, unknown> }
623
+ for (const name of Object.keys(held.scripts ?? {})) found.add(name)
624
+ } catch {
625
+ found.clear()
626
+ }
627
+ commands = found
628
+ return found
629
+ }
630
+
631
+ function mentionsIn(at: string): readonly string[] {
632
+ const found: string[] = []
633
+ const step = (node: ts.ObjectLiteralExpression): void => {
634
+ const fields = fieldsOf(node)
635
+ for (const list of LISTS) {
636
+ const value = fields.get(list)
637
+ if (value === undefined || !ts.isArrayLiteralExpression(value)) continue
638
+ for (const one of value.elements) {
639
+ if (!ts.isObjectLiteralExpression(one)) continue
640
+ const held = literal(fieldsOf(one).get('at'))
641
+ if (held !== undefined) found.push(settle(held, at))
642
+ step(one)
643
+ }
644
+ }
645
+ }
646
+ for (const one of nodesIn(at)) step(one.node)
647
+ return found
648
+ }
649
+
650
+ let passed: Pass | undefined
651
+
652
+ export function reached(): readonly string[] {
653
+ return [THREAD_AT, ...descend().threaded.keys()]
654
+ }
655
+
656
+ export function forget(): void {
657
+ passed = undefined
658
+ built = undefined
659
+ scanned = undefined
660
+ commands = undefined
661
+ source.clear()
662
+ }
663
+
664
+ export function descend(): Pass {
665
+ rooted()
666
+ if (passed !== undefined) return passed
667
+
668
+ const { program, checker } = compiled()
669
+ const threaded = new Map<string, { list: 'declares' | 'derives' | 'seeds'; by: string; row: Row }[]>()
670
+ const mentioned = new Map<string, string[]>()
671
+ const derived = new Map<string, Row>()
672
+ const stated = new Map<string, Row>()
673
+ const coined = new Map<string, { said: string[]; under: string; from: string[] }>()
674
+ const broken: Finding[] = []
675
+ const findings: Finding[] = []
676
+ const opened = new Set<string>()
677
+ const shaped = new Map<string, { type: string; shape?: ts.Type; by: string }>()
678
+ const made = new Map<string, { by: Set<string>; rows: string[]; where: string }>()
679
+ const absent = new Map<string, { at: number; by: Set<string>; rows: number }>()
680
+ const demanded: { row: Row; word: string; list: string }[] = []
681
+ const thread: { row: Row; command: string }[] = []
682
+ const offered = new Set<string>()
683
+ let declarations = 0
684
+ let rows = 0
685
+
686
+ const root = program.getSourceFile(resolve(ROOT, THREAD_AT))
687
+ if (root === undefined) {
688
+ broken.push({
689
+ file: THREAD_AT,
690
+ detail: 'the root of the tree cannot be read, so nothing below it is reached and every file here reads as unthreaded',
691
+ instead: 'write the root, or run `thread init`',
692
+ })
693
+ passed = { threaded, mentioned, derived, stated, coined, broken, findings, declarations, rows, sources: sources() }
694
+ return passed
695
+ }
696
+
697
+ const shape = threadedType(checker, program)
698
+ if (shape === undefined) {
699
+ broken.push({
700
+ file: THREAD_AT,
701
+ detail: 'the root does not resolve `Threaded`, so nothing here can be recognised as a declaration and every file reads as unthreaded',
702
+ instead: `import \`Threaded\` from \`${NAMED}/thread\` in the root, or run \`thread init\`. Without the shape there is nothing to hold anything to`,
703
+ })
704
+ passed = { threaded, mentioned, derived, stated, coined, broken, findings, declarations, rows, sources: sources() }
705
+ return passed
706
+ }
707
+
708
+ const said = (by: string): string => (by === '' ? 'the root' : by)
709
+ const names = (row: Row): string => row.id ?? row.at ?? 'a row stating neither an id nor an at'
710
+
711
+ function coin(id: string | undefined, describes: string | undefined, under: string, parent: string): void {
712
+ if (id === undefined) return
713
+ const held = coined.get(id) ?? { said: [], under, from: [] }
714
+ const meaning = describes ?? ''
715
+ if (!held.said.includes(meaning)) held.said.push(meaning)
716
+ if (parent !== '' && parent !== id && !held.from.includes(parent)) held.from.push(parent)
717
+ coined.set(id, held)
718
+ }
719
+
720
+ function judge(row: Row, by: string, list: (typeof LISTS)[number], own: boolean): void {
721
+ const at = row.where
722
+
723
+ for (const field of ['by', 'runs'] as const) {
724
+ const command = row[field]
725
+ if (command === undefined) continue
726
+ if (command.startsWith('thread ')) {
727
+ if (field === 'by') offered.add(command)
728
+ else thread.push({ row, command })
729
+ continue
730
+ }
731
+ const held = RUNNER.exec(command)
732
+ if (held === null || scripts().has(held[1] ?? '')) continue
733
+ findings.push({
734
+ file: at,
735
+ detail: `\`${names(row)}\` states \`${field}: '${command}'\`, and package.json states no \`${held[1] ?? ''}\` script`,
736
+ instead: 'add the script, or fix the name. A row naming a command is a promise that the command exists',
737
+ })
738
+ }
739
+
740
+ for (const field of ['requires', 'takes'] as const) {
741
+ for (const word of wordsOf(row.fields.get(field))) demanded.push({ row, word, list: field })
742
+ }
743
+
744
+ if (list === 'derives' && row.at !== undefined && !own) {
745
+ const held = made.get(row.at) ?? { by: new Set<string>(), rows: [] as string[], where: at }
746
+ if (row.by !== undefined) held.by.add(row.by)
747
+ held.rows.push(names(row))
748
+ made.set(row.at, held)
749
+ }
750
+
751
+ if (row.at === THREAD_AT && !own) {
752
+ findings.push({
753
+ file: at,
754
+ detail: `\`${names(row)}\` declares \`${THREAD_AT}\`, which is the root`,
755
+ instead:
756
+ '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',
757
+ })
758
+ }
759
+
760
+ if (row.at === undefined) return
761
+
762
+ if (row.at.startsWith('../') || row.at.startsWith('/')) {
763
+ findings.push({
764
+ file: at,
765
+ detail: `\`${names(row)}\` names \`${row.at}\`, which is outside this repository`,
766
+ instead:
767
+ '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',
768
+ })
769
+ return
770
+ }
771
+
772
+ const target = resolve(ROOT, row.at)
773
+ if (unkept(row.at)) {
774
+ findings.push({
775
+ file: at,
776
+ detail: `\`${names(row)}\` names \`${row.at}\`, which git ignores, so it is on this machine and in no clone of it`,
777
+ instead:
778
+ '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',
779
+ })
780
+ return
781
+ }
782
+ if (!existsSync(target)) {
783
+
784
+ const derived = list === 'derives' || row.by !== undefined
785
+ const said = absent.get(row.at)
786
+ if (said !== undefined) {
787
+ said.by.add(names(row))
788
+ said.rows += 1
789
+ return
790
+ }
791
+ absent.set(row.at, { at: findings.length, by: new Set([names(row)]), rows: 1 })
792
+ findings.push({
793
+ file: at,
794
+ detail: derived
795
+ ? `\`${names(row)}\` ${list === 'derives' ? 'derives' : 'names'} \`${row.at}\`, and nothing is there yet`
796
+ : `\`${names(row)}\` names \`${row.at}\`, and nothing is there`,
797
+ instead: derived
798
+ ? 'run the generator the row names in `by`. A derived file is absent until it is derived, so this is a warning'
799
+ : 'write the file, fix the path, or delete the row. A row naming nothing brings nothing into existence',
800
+ ...(derived ? { severity: 'warning' as const } : {}),
801
+ })
802
+ } else if (statSync(target).isDirectory()) {
803
+ findings.push({
804
+ file: at,
805
+ detail: `\`${names(row)}\` names \`${row.at}\`, which is a directory`,
806
+ instead:
807
+ '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',
808
+ })
809
+ }
810
+
811
+ const already = shaped.get(row.at)
812
+ if (already === undefined) {
813
+ shaped.set(row.at, { type: row.type, shape: row.shape, by })
814
+ return
815
+ }
816
+ if (row.shape === undefined || already.shape === undefined) return
817
+ if (checker.isTypeAssignableTo(row.shape, already.shape) || checker.isTypeAssignableTo(already.shape, row.shape)) return
818
+ findings.push({
819
+ file: at,
820
+ detail: `\`${row.at}\` is declared \`${row.type}\` by \`${said(by)}\` and \`${already.type}\` by \`${said(already.by)}\`, and neither shape satisfies the other`,
821
+ instead: 'one address has one shape. Decide what the child is and let both rows say it',
822
+ })
823
+ }
824
+
825
+ function into(node: ts.ObjectLiteralExpression, type: string, at: string, under: string, parent: string, judged = false): void {
826
+ declarations += 1
827
+ const held = rowOf(node, type, at)
828
+ if (!judged) {
829
+ coin(held.id, held.describes, under, parent)
830
+ judge(held, parent, 'declares', true)
831
+ if (held.id !== undefined && !stated.has(held.id)) stated.set(held.id, held)
832
+ }
833
+
834
+ const by = held.id ?? at
835
+
836
+ for (const list of LISTS) {
837
+ const stated = held.fields.get(list)
838
+ if (stated === undefined) continue
839
+
840
+ const only = ts.isCallExpression(stated) && stated.arguments.length === 1 ? stated.arguments[0] : undefined
841
+ const value = only !== undefined && ts.isArrayLiteralExpression(only) ? only : stated
842
+
843
+ if (!ts.isArrayLiteralExpression(value)) {
844
+ findings.push({ file: at, detail: `\`${by}\` states a \`${list}\` that is not a list`, instead: `write \`${list}\` as a list of rows` })
845
+ continue
846
+ }
847
+
848
+ for (const one of value.elements) {
849
+ if (!ts.isObjectLiteralExpression(one)) {
850
+ findings.push({
851
+ file: at,
852
+ detail: `\`${by}\` holds something in \`${list}\` that is not a row written out here`,
853
+ instead: 'write the row inline. A row reached through a name is a row the descent would have to run code to read',
854
+ })
855
+ continue
856
+ }
857
+ rows += 1
858
+ const wanted = checker.getContextualType(one)
859
+ const kind = wanted === undefined ? undefined : elementOf(checker, wanted)
860
+ const row = rowOf(one, kind === undefined ? type : checker.typeToString(kind), at, kind)
861
+ coin(row.id, row.describes, under, by)
862
+ judge(row, by, list, false)
863
+ if (row.at !== undefined) {
864
+ threaded.set(row.at, [...(threaded.get(row.at) ?? []), { list, by, row }])
865
+ if (list === 'derives' && row.id !== undefined) derived.set(row.id, row)
866
+ }
867
+ if (LISTS.some((name) => row.fields.has(name))) into(one, row.type, at, under, by, true)
868
+ if (row.at !== undefined) open(row.at, wanted === undefined ? undefined : elementOf(checker, wanted), under, row.id ?? by, by)
869
+ }
870
+ }
871
+ }
872
+
873
+ function open(at: string, expected: ts.Type | undefined, under: string, parent: string, by: string): void {
874
+ const already = opened.has(at)
875
+ opened.add(at)
876
+ if (!SOURCE.test(at)) return
877
+ const file = program.getSourceFile(resolve(ROOT, at))
878
+ if (file === undefined) return
879
+
880
+ if (!already) {
881
+ for (const one of program.getSyntacticDiagnostics(file)) {
882
+ broken.push({
883
+ file: at,
884
+ 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, ' ')}`,
885
+ instead: 'fix the syntax. A file the descent cannot read declares nothing, and nothing below it can be found missing',
886
+ })
887
+ break
888
+ }
889
+ }
890
+
891
+ for (const one of nodesIn(at)) {
892
+ if (expected !== undefined && !checker.isTypeAssignableTo(one.shape, expected)) {
893
+ findings.push({
894
+ file: at,
895
+ detail: `\`${one.name}\` here is \`${one.type}\`, and \`${said(by)}\` states its child is \`${checker.typeToString(expected)}\``,
896
+ 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`,
897
+ })
898
+ continue
899
+ }
900
+ if (already) continue
901
+ into(one.node, one.type, at, under, parent)
902
+ }
903
+ }
904
+
905
+ opened.add(THREAD_AT)
906
+ for (const one of nodesIn(THREAD_AT)) into(one.node, one.type, THREAD_AT, '', '')
907
+
908
+ for (const at of [...opened].filter((one) => SOURCE.test(one))) {
909
+ const file = program.getSourceFile(resolve(ROOT, at))
910
+ if (file === undefined) continue
911
+ for (const one of program.getSemanticDiagnostics(file)) {
912
+ findings.push({
913
+ file: at,
914
+ detail: `the compiler refuses this declaration: ${ts.flattenDiagnosticMessageText(one.messageText, ' ')}`,
915
+ instead:
916
+ 'make the declaration the shape its own type states. A row the compiler will not accept is a row nothing downstream can read',
917
+ })
918
+ }
919
+ }
920
+
921
+ for (const [what, held] of made) {
922
+ if (held.by.size === 0) {
923
+ findings.push({
924
+ file: held.where,
925
+ 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\``,
926
+ instead:
927
+ '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',
928
+ })
929
+ continue
930
+ }
931
+ if (held.by.size < 2) continue
932
+ findings.push({
933
+ file: held.where,
934
+ detail: `\`${what}\` is derived by ${[...held.by].map((one) => `\`${one}\``).join(' and ')}`,
935
+ instead: 'one file has one generator. Two commands writing one address means whichever runs last wins and the other is overwritten in silence',
936
+ })
937
+ }
938
+
939
+ const answers = (command: string): boolean =>
940
+ offered.has(command) || [...offered].some((one) => command.startsWith(`${one} --`))
941
+
942
+ for (const one of thread) {
943
+ if (answers(one.command)) continue
944
+ findings.push({
945
+ file: one.row.where,
946
+ detail: `\`${names(one.row)}\` states \`runs: '${one.command}'\`, and no row declares a \`by\` it is a mode of`,
947
+ instead:
948
+ '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',
949
+ })
950
+ }
951
+
952
+ for (const one of demanded) {
953
+ if (coined.has(one.word)) continue
954
+ findings.push({
955
+ file: one.row.where,
956
+ detail: `\`${names(one.row)}\` ${one.list} \`${one.word}\`, and nothing in this tree coins that word`,
957
+ instead: `declare something with \`id: '${one.word}'\`, or fix the name. A demand naming nothing is met by nothing, and nothing tells you so`,
958
+ })
959
+ }
960
+
961
+ for (const [word, held] of coined) {
962
+ const meanings = [...new Set(held.said.filter((one) => one !== ''))]
963
+ if (meanings.length < 2) continue
964
+ findings.push({
965
+ file: THREAD_AT,
966
+ detail: `\`${word}\` is coined ${String(meanings.length)} times with different meanings: ${meanings.map((one) => `"${one.slice(0, 60)}${one.length > 60 ? '…' : ''}"`).join(' and ')}`,
967
+ 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',
968
+ })
969
+ }
970
+
971
+ for (const [name, where] of shapesNamed()) {
972
+ if (where.length < 2) continue
973
+ findings.push({
974
+ file: where[1] ?? THREAD_AT,
975
+ detail: `\`${name}\` is a shape the tree threads by, and it is declared in ${String(where.length)} places: ${where.join(', ')}`,
976
+ 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',
977
+ })
978
+ }
979
+
980
+ for (const one of [...sources(), ...shipped()]) {
981
+ if (!SOURCE.test(one)) continue
982
+ for (const at of mentionsIn(one)) {
983
+ const held = mentioned.get(at) ?? []
984
+ if (!held.includes(one)) mentioned.set(at, [...held, one])
985
+ }
986
+ }
987
+
988
+ for (const [path, held] of absent) {
989
+ if (held.rows < 2) continue
990
+ const one = findings[held.at]
991
+ if (one === undefined) continue
992
+
993
+ findings[held.at] = {
994
+ ...one,
995
+ detail: `${String(held.rows)} rows derive \`${path}\`, and nothing is there yet — ${[...held.by].sort().join(', ')}`,
996
+ }
997
+ }
998
+
999
+ passed = { threaded, mentioned, derived, stated, coined, broken, findings, declarations, rows, sources: sources() }
1000
+ return passed
1001
+ }