@prestack/thread 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/core/actor.ts +20 -0
  2. package/dist/core/asking.d.ts +0 -1
  3. package/dist/core/asking.js +0 -1
  4. package/dist/core/asking.ts +1342 -0
  5. package/dist/core/attach.js +2 -2
  6. package/dist/core/attach.ts +585 -0
  7. package/dist/core/chain.js +8 -1
  8. package/dist/core/chain.ts +148 -0
  9. package/dist/core/cli.js +97 -18
  10. package/dist/core/cli.ts +2004 -0
  11. package/dist/core/condition.ts +227 -0
  12. package/dist/core/descend.d.ts +1 -0
  13. package/dist/core/descend.js +36 -0
  14. package/dist/core/descend.ts +1001 -0
  15. package/dist/core/finding.ts +38 -0
  16. package/dist/core/generators/meta.js +6 -0
  17. package/dist/core/generators/meta.ts +1006 -0
  18. package/dist/core/init.ts +170 -0
  19. package/dist/core/invoked.ts +47 -0
  20. package/dist/core/round.ts +57 -0
  21. package/dist/core/rows.ts +369 -0
  22. package/dist/core/scope.ts +18 -0
  23. package/dist/core/thread.ts +29 -0
  24. package/dist/core/tree.ts +67 -0
  25. package/dist/core/types.ts +350 -0
  26. package/dist/derivation/attach.js +1 -1
  27. package/dist/derivation/attach.ts +68 -0
  28. package/dist/derivation/generators/build.ts +232 -0
  29. package/dist/derivation/generators/check.ts +118 -0
  30. package/dist/derivation/generators/derivation.ts +347 -0
  31. package/dist/derivation/generators/determinism.ts +145 -0
  32. package/dist/derivation/generators/release.js +19 -9
  33. package/dist/derivation/generators/release.ts +213 -0
  34. package/dist/derivation/makers/release.ts +135 -0
  35. package/dist/derivation/release.ts +23 -0
  36. package/dist/integrations/attach.js +2 -2
  37. package/dist/integrations/attach.ts +207 -0
  38. package/dist/integrations/generators/read.ts +127 -0
  39. package/dist/integrations/generators/secret.ts +289 -0
  40. package/dist/integrations/generators/target.js +16 -6
  41. package/dist/integrations/generators/target.ts +944 -0
  42. package/dist/integrations/generators/vault.js +46 -14
  43. package/dist/integrations/generators/vault.ts +212 -0
  44. package/dist/integrations/{sdk.js → module.js} +4 -4
  45. package/dist/integrations/module.ts +189 -0
  46. package/dist/integrations/secret.ts +50 -0
  47. package/dist/integrations/target.d.ts +3 -1
  48. package/dist/integrations/target.js +14 -11
  49. package/dist/integrations/target.ts +340 -0
  50. package/dist/integrations/vault.d.ts +2 -0
  51. package/dist/integrations/vault.js +4 -2
  52. package/dist/integrations/vault.ts +136 -0
  53. package/dist/integrity/attach.ts +234 -0
  54. package/dist/integrity/check-threads.js +2 -26
  55. package/dist/integrity/check-threads.ts +166 -0
  56. package/dist/integrity/constraint.ts +1202 -0
  57. package/dist/integrity/generators/compiler.ts +161 -0
  58. package/dist/integrity/generators/gate.ts +201 -0
  59. package/dist/integrity/generators/issues.ts +160 -0
  60. package/dist/integrity/generators/publish.js +14 -5
  61. package/dist/integrity/generators/publish.ts +247 -0
  62. package/dist/integrity/generators/rule.ts +271 -0
  63. package/dist/integrity/generators/seal.ts +117 -0
  64. package/dist/integrity/generators/trial.ts +320 -0
  65. package/dist/integrity/lint.ts +227 -0
  66. package/dist/integrity/lsp/bin.ts +42 -0
  67. package/dist/integrity/lsp/diagnostics.ts +92 -0
  68. package/dist/integrity/lsp/findings.ts +63 -0
  69. package/dist/integrity/lsp/lint.ts +140 -0
  70. package/dist/integrity/lsp/protocol.ts +78 -0
  71. package/dist/integrity/lsp/server.ts +224 -0
  72. package/dist/integrity/sealed.js +2 -2
  73. package/dist/integrity/sealed.ts +56 -0
  74. package/dist/integrity/slopnator.ts +350 -0
  75. package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
  76. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
  77. package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
  78. package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
  79. package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
  80. package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
  81. package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
  82. package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
  83. package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
  84. package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
  85. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
  86. package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
  87. package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
  88. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
  89. package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
  90. package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
  91. package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
  92. package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
  93. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
  94. package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
  95. package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
  96. package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
  97. package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
  98. package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
  99. package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
  100. package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
  101. package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
  102. package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
  103. package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
  104. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
  105. package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
  106. package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
  107. package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
  108. package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
  109. package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
  110. package/dist/integrity/sweep.ts +307 -0
  111. package/dist/knowledge/attach.ts +350 -0
  112. package/dist/knowledge/generators/docsgen.ts +174 -0
  113. package/dist/knowledge/generators/glossary.ts +73 -0
  114. package/dist/knowledge/generators/skills.ts +263 -0
  115. package/dist/knowledge/glossary.ts +6 -0
  116. package/dist/operations/access.ts +88 -0
  117. package/dist/operations/attach.ts +475 -0
  118. package/dist/operations/capabilities.ts +65 -0
  119. package/dist/operations/cost.ts +58 -0
  120. package/dist/operations/dispatch.ts +348 -0
  121. package/dist/operations/error-results.ts +31 -0
  122. package/dist/operations/errors/catalog.ts +168 -0
  123. package/dist/operations/errors/classes.ts +51 -0
  124. package/dist/operations/errors/errors.ts +314 -0
  125. package/dist/operations/generators/capability.js +29 -19
  126. package/dist/operations/generators/capability.ts +704 -0
  127. package/dist/operations/generators/compile.js +20 -1
  128. package/dist/operations/generators/compile.ts +643 -0
  129. package/dist/operations/generators/grant.ts +336 -0
  130. package/dist/operations/generators/operation.ts +972 -0
  131. package/dist/operations/generators/permits.ts +223 -0
  132. package/dist/operations/generators/seed.ts +347 -0
  133. package/dist/operations/generators/trial.js +4 -1
  134. package/dist/operations/generators/trial.ts +529 -0
  135. package/dist/operations/http.ts +164 -0
  136. package/dist/operations/makers/capability.ts +166 -0
  137. package/dist/operations/makers/trial.ts +520 -0
  138. package/dist/operations/memory.ts +470 -0
  139. package/dist/operations/metering.ts +328 -0
  140. package/dist/operations/nodes.ts +54 -0
  141. package/dist/operations/operations.ts +32 -0
  142. package/dist/operations/queue-policy.ts +10 -0
  143. package/dist/operations/result.ts +28 -0
  144. package/dist/operations/seams.d.ts +4 -4
  145. package/dist/operations/seams.ts +364 -0
  146. package/dist/operations/store.ts +29 -0
  147. package/package.json +8 -1
  148. package/dist/integrations/generators/sdk.d.ts +0 -1
  149. package/dist/integrations/generators/sdk.js +0 -148
  150. /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
@@ -0,0 +1,164 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import type { Slopnator } from '../slopnator.ts'
5
+ import { named as packageOf } from '../../core/scope.ts'
6
+
7
+ /*
8
+ * A KEY THE INTERFACE NAMES AND A LANGUAGE DOES NOT ANSWER.
9
+ *
10
+ * "i18n keys must be translated OR ITS AN ERROR ... a new constraint.ts row and slopnator that
11
+ * checks i18n entries against the keys in the UI and if there's anything missing in whatever
12
+ * file, it's a build error, compile error, gate error, everything. Cannot be allowed."
13
+ * — founder, 2026-08-29
14
+ *
15
+ * The generator already writes a line into every language file for every key it finds, so a key is
16
+ * never structurally absent — what it cannot do is invent the sentence. This is the half that
17
+ * refuses: a line holding nothing is a key nobody has answered, and an app that ships one shows a
18
+ * person an empty button.
19
+ *
20
+ * Deliberately independent of the generator rather than calling it. A check that runs the thing it
21
+ * is checking passes whenever that thing passes, which is no check at all — this reads the files
22
+ * that are actually on disk and compares them to the markup that is actually there.
23
+ */
24
+
25
+ const SKIP = new Set(['node_modules', '.next', '.git', 'dist', '_generated'])
26
+
27
+ const CODE = /\.(tsx?|jsx?)$/
28
+
29
+ /*
30
+ * Which props carry a key, read off the installed package's own declarations.
31
+ *
32
+ * A list written here would go stale the first time an element gained a prop — silently, by finding
33
+ * fewer keys than there are, which is the failure this whole law exists to prevent.
34
+ */
35
+ const TYPED = /readonly\s+([A-Za-z][A-Za-z0-9]*)\??\s*:\s*MessageKey\b/g
36
+
37
+ function walk(at: string, found: string[] = []): readonly string[] {
38
+ let entries: readonly { name: string; isDirectory: () => boolean }[]
39
+ try {
40
+ entries = readdirSync(at, { withFileTypes: true })
41
+ } catch {
42
+ return found
43
+ }
44
+ for (const entry of entries) {
45
+ if (SKIP.has(entry.name)) continue
46
+ const path = join(at, entry.name)
47
+ if (entry.isDirectory()) walk(path, found)
48
+ else if (CODE.test(entry.name)) found.push(path)
49
+ }
50
+ return found
51
+ }
52
+
53
+ function keyProps(root: string): readonly string[] {
54
+ const found = new Set<string>()
55
+ const home = join(root, 'node_modules', packageOf('ui'))
56
+ if (!existsSync(home)) return []
57
+ for (const file of walk(home)) {
58
+ for (const one of readFileSync(file, 'utf8').matchAll(TYPED)) found.add(one[1] ?? '')
59
+ }
60
+ return [...found].sort()
61
+ }
62
+
63
+ function languagesIn(root: string): { readonly held: readonly string[]; readonly source: string | undefined } {
64
+ const at = join(root, '_threads', 'localizations.ts')
65
+ if (!existsSync(at)) return { held: [], source: undefined }
66
+ const text = readFileSync(at, 'utf8')
67
+ const held: string[] = []
68
+ let source: string | undefined
69
+ for (const one of text.matchAll(/id:\s*(?:SOURCE_ROW|'([^']+)')\s*,[\s\S]{0,200}?is:\s*'([^']*)'/g)) {
70
+ const id = one[1]
71
+ if (id === undefined || id === 'source') {
72
+ source = one[2]
73
+ continue
74
+ }
75
+ held.push(id)
76
+ }
77
+ return { held, source }
78
+ }
79
+
80
+ export const slopnator: Slopnator = {
81
+ checks: (root) => {
82
+ const { held } = languagesIn(root)
83
+ if (held.length === 0) return []
84
+
85
+ const props = keyProps(root)
86
+ if (props.length === 0) return []
87
+
88
+ const named = new RegExp(`\\b(?:${props.join('|')})\\s*=\\s*"([^"]+)"`, 'g')
89
+
90
+ const used = new Map<string, string>()
91
+ for (const file of walk(root)) {
92
+ const text = readFileSync(file, 'utf8')
93
+ for (const one of text.matchAll(named)) {
94
+ const key = one[1] ?? ''
95
+ if (!used.has(key)) used.set(key, file.slice(root.length).replace(/^\/+/, ''))
96
+ }
97
+ }
98
+ if (used.size === 0) return []
99
+
100
+ const findings: string[] = []
101
+ for (const language of held) {
102
+ const at = join(root, 'i18n', `${language}.json`)
103
+ if (!existsSync(at)) {
104
+ findings.push(
105
+ `i18n/${language}.json is not there and \`${language}\` is a declared language, so every key this ` +
106
+ 'interface names goes unanswered in it — run `thread i18n`',
107
+ )
108
+ continue
109
+ }
110
+ let wording: Readonly<Record<string, unknown>>
111
+ try {
112
+ wording = JSON.parse(readFileSync(at, 'utf8')) as Readonly<Record<string, unknown>>
113
+ } catch {
114
+ findings.push(
115
+ `i18n/${language}.json will not parse, so what this app says in \`${language}\` cannot be known — ` +
116
+ 'a reader that cannot read is not a reader that found nothing',
117
+ )
118
+ continue
119
+ }
120
+ for (const [key, where] of [...used].sort()) {
121
+ const said = wording[key]
122
+ if (typeof said === 'string' && said !== '') continue
123
+ findings.push(
124
+ `\`${key}\` is named by ${where} and says nothing in \`${language}\` — an untranslated key is a ` +
125
+ 'blank on the screen, and it is an error rather than a word that comes out in the wrong language',
126
+ )
127
+ }
128
+ }
129
+ return findings
130
+ },
131
+ proves: {
132
+ wrong: [
133
+ {
134
+ files: [
135
+ { at: '_threads/localizations.ts', source: "export const LOCALIZATION = {\n holds: [\n { id: 'source', is: 'en' },\n { id: 'en', is: 'English' },\n ],\n}\n" },
136
+ { at: `node_modules/${packageOf('ui')}/probe.ts`, source: 'export interface P {\n readonly label: MessageKey\n}\n' },
137
+ { at: 'compositions/cmp_probe.tsx', source: 'export const P = () => <Button label="probe.blank" />\n' },
138
+ { at: 'i18n/en.json', source: '{\n "probe.blank": ""\n}\n' },
139
+ ],
140
+ at: 'compositions/cmp_probe.tsx',
141
+ },
142
+ {
143
+ files: [
144
+ { at: '_threads/localizations.ts', source: "export const LOCALIZATION = {\n holds: [\n { id: 'source', is: 'en' },\n { id: 'en', is: 'English' },\n ],\n}\n" },
145
+ { at: `node_modules/${packageOf('ui')}/probe.ts`, source: 'export interface P {\n readonly label: MessageKey\n}\n' },
146
+ { at: 'compositions/cmp_probe.tsx', source: 'export const P = () => <Button label="probe.missing" />\n' },
147
+ { at: 'i18n/en.json', source: '{\n "probe.other": "Something"\n}\n' },
148
+ ],
149
+ at: 'compositions/cmp_probe.tsx',
150
+ },
151
+ ],
152
+ right: [
153
+ {
154
+ files: [
155
+ { at: '_threads/localizations.ts', source: "export const LOCALIZATION = {\n holds: [\n { id: 'source', is: 'en' },\n { id: 'en', is: 'English' },\n ],\n}\n" },
156
+ { at: `node_modules/${packageOf('ui')}/probe.ts`, source: 'export interface P {\n readonly label: MessageKey\n}\n' },
157
+ { at: 'compositions/cmp_probe.tsx', source: 'export const P = () => <Button label="probe.answered" />\n' },
158
+ { at: 'i18n/en.json', source: '{\n "probe.answered": "Answered"\n}\n' },
159
+ ],
160
+ at: 'compositions/cmp_probe.tsx',
161
+ },
162
+ ],
163
+ },
164
+ }
@@ -0,0 +1,36 @@
1
+ import type { Slopnator } from '../slopnator.ts'
2
+ import { here, parses, rowsIn } from './sl_unthreaded.ts'
3
+
4
+ export const slopnator: Slopnator = {
5
+ holds: (_node, at) => {
6
+ const path = here(at.filename)
7
+
8
+ if (!parses(path)) return false
9
+
10
+ let governing: readonly { readonly list?: string; readonly id?: string; readonly version?: number }[]
11
+ try {
12
+ governing = rowsIn(at.text)
13
+ } catch {
14
+ return 'this file cannot be parsed, so the declarations it governs with were never read and nothing about their versions was measured'
15
+ }
16
+
17
+ for (const node of governing) {
18
+ if (node.list !== undefined || node.version !== undefined) continue
19
+ return (
20
+ `"${node.id ?? ''}" is annotated with the interface that makes other things conform to it and states no \`version\`, ` +
21
+ 'so a change to what it governs is a diff nobody downstream can detect and nothing can be carried forward from the shape it used to have'
22
+ )
23
+ }
24
+
25
+ return false
26
+ },
27
+ proves: {
28
+ wrong: [
29
+ "export const L: Constraint = {\n id: 'a-law',\n at: 'packages/core/integrity/src/constraint.ts',\n describes: 'A rule that must be enforced.',\n}",
30
+ ],
31
+ right: [
32
+ "export const L: Constraint = {\n id: 'a-law',\n at: 'packages/core/integrity/src/constraint.ts',\n describes: 'A rule that must be enforced.',\n version: 3,\n}",
33
+ "export const HELPER = {\n id: 'a-helper',\n at: 'threads/meta.ts',\n describes: 'Nothing conforms to this.',\n}",
34
+ ],
35
+ },
36
+ }
@@ -0,0 +1,307 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import { stdout } from 'node:process'
5
+ import { pathToFileURL } from 'node:url'
6
+
7
+ import { descend } from '../core/descend.ts'
8
+ import { runChecker, type Finding } from '../core/finding.ts'
9
+ import { exit, invoked } from '../core/invoked.ts'
10
+
11
+ import { parser } from 'typescript-eslint'
12
+
13
+ import { CONSTRAINTS, type Constraint } from './constraint.ts'
14
+ import { NOT_WRITTEN, ROOT, governs, load, reads, rules, slopnatorName, sweeps, visits, type Reading, type Slopnator, type Spoken, type Visiting } from './slopnator.ts'
15
+ import { ruleMessage } from './lint.ts'
16
+
17
+ const PARSEABLE = /\.(ts|tsx|mts|cts|mjs|cjs|js|jsx)$/
18
+
19
+ interface Node {
20
+ readonly type?: string
21
+ readonly comments?: readonly Spoken[]
22
+ readonly [key: string]: unknown
23
+ }
24
+
25
+ function tree(text: string): Node | undefined {
26
+ const read = (jsx: boolean): { ast: Node } =>
27
+ (parser.parseForESLint as (t: string, o: unknown) => { ast: Node })(text, {
28
+ range: true,
29
+ loc: true,
30
+ comment: true,
31
+ ecmaVersion: 'latest',
32
+ sourceType: 'module',
33
+ ecmaFeatures: { jsx },
34
+ })
35
+ try {
36
+ return read(true).ast
37
+ } catch {
38
+ try {
39
+ return read(false).ast
40
+ } catch {
41
+ return undefined
42
+ }
43
+ }
44
+ }
45
+
46
+ export function visit(root: Node, on: ReadonlySet<string>, each: (node: Node, ancestors: readonly Node[]) => void): void {
47
+ const stack: Node[] = []
48
+ const step = (held: unknown): void => {
49
+ if (held === null || typeof held !== 'object') return
50
+ if (Array.isArray(held)) {
51
+ for (const one of held) step(one)
52
+ return
53
+ }
54
+ const node = held as Node
55
+ const named = typeof node.type === 'string'
56
+ if (named && on.has(node.type as string)) each(node, [...stack])
57
+ if (named) stack.push(node)
58
+ for (const key of Object.keys(node)) {
59
+ if (key === 'parent') continue
60
+ step((node as Record<string, unknown>)[key])
61
+ }
62
+ if (named) stack.pop()
63
+ }
64
+ step(root)
65
+ }
66
+
67
+ export interface Caught {
68
+ readonly rule: string
69
+ readonly file: string
70
+ readonly found: string
71
+ }
72
+
73
+ const ELSEWHERE = new Set(['node_modules', '.git'])
74
+
75
+ function ignoredIn(root: string): ReadonlySet<string> {
76
+ const at = join(root, '.gitignore')
77
+ if (!existsSync(at)) return new Set()
78
+ return new Set(
79
+ readFileSync(at, 'utf8')
80
+ .split('\n')
81
+ .map((line) => line.trim().replace(/^\//, '').replace(/\/$/, ''))
82
+ .filter((line) => line !== '' && !line.startsWith('#') && !line.includes('*') && !line.startsWith('!')),
83
+ )
84
+ }
85
+
86
+ function authored(): ReadonlySet<string> {
87
+ const found = new Set<string>()
88
+ try {
89
+ for (const [at, threadings] of descend().threaded) {
90
+ if (threadings.some((one) => one.list === 'derives' && (one.row.by ?? one.row.runs) !== undefined)) found.add(at)
91
+ }
92
+ } catch {
93
+
94
+ }
95
+ return found
96
+ }
97
+
98
+ export function tracked(root: string): readonly string[] {
99
+ const derived = authored()
100
+ const mine = (one: string): boolean => !derived.has(one)
101
+
102
+ try {
103
+
104
+ const listed = execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard'], {
105
+ cwd: root,
106
+ encoding: 'utf8',
107
+ stdio: ['ignore', 'pipe', 'ignore'],
108
+ })
109
+ .split('\n')
110
+ .filter(Boolean)
111
+ if (listed.length > 0) {
112
+ return [...new Set(listed)].filter((one) => existsSync(join(root, one)) && mine(one)).sort()
113
+ }
114
+ } catch {
115
+
116
+ }
117
+
118
+ const found: string[] = []
119
+ const ignored = ignoredIn(root)
120
+
121
+ const walk = (at: string, under: string): void => {
122
+ for (const one of readdirSync(at, { withFileTypes: true })) {
123
+ if (one.name.startsWith('.')) continue
124
+ if (ELSEWHERE.has(one.name) || ignored.has(one.name) || ignored.has(under === '' ? one.name : `${under}/${one.name}`)) continue
125
+ const here = under === '' ? one.name : `${under}/${one.name}`
126
+ if (one.isDirectory()) walk(join(at, one.name), here)
127
+ else if (mine(here)) found.push(here)
128
+ }
129
+ }
130
+
131
+ walk(root, '')
132
+ return found.sort()
133
+ }
134
+
135
+ export function sweep(
136
+ root: string,
137
+ all: readonly Constraint[],
138
+ slopnators: ReadonlyMap<string, Slopnator>,
139
+ files: readonly string[],
140
+ ): {
141
+ readonly findings: readonly Caught[]
142
+ readonly swept: number
143
+ readonly tracked: number
144
+ readonly rules: number
145
+ readonly unrun: readonly string[]
146
+ readonly unmet: readonly string[]
147
+ readonly linted: readonly string[]
148
+ } {
149
+ const readers = all
150
+ .map((rule) => ({ rule, slopnator: slopnators.get(slopnatorName(rule.id)) }))
151
+ .filter((one): one is { rule: Constraint; slopnator: Reading } => one.slopnator !== undefined && sweeps(one.slopnator))
152
+
153
+ const walkers = all
154
+ .map((rule) => ({ rule, slopnator: slopnators.get(slopnatorName(rule.id)) }))
155
+ .filter((one): one is { rule: Constraint; slopnator: Visiting } => one.slopnator !== undefined && visits(one.slopnator))
156
+
157
+ const findings: Caught[] = []
158
+ let swept = 0
159
+
160
+ const met = new Set<string>()
161
+
162
+ for (const file of files) {
163
+ const reading = readers.filter((one) => governs(one.rule, file))
164
+ const walking = walkers.filter((one) => governs(one.rule, file))
165
+ for (const one of reading) met.add(one.rule.id)
166
+ for (const one of walking) met.add(one.rule.id)
167
+ if (reading.length === 0 && walking.length === 0) continue
168
+
169
+ let text: string
170
+ try {
171
+ text = readFileSync(join(root, file), 'utf8')
172
+ } catch {
173
+ continue
174
+ }
175
+ swept += 1
176
+
177
+ for (const { rule, slopnator } of reading) {
178
+ const held = slopnator.holds(undefined, {
179
+ filename: file,
180
+ text,
181
+ ancestors: () => [],
182
+ comments: () => [],
183
+ })
184
+ if (held === false || held === NOT_WRITTEN) continue
185
+ findings.push({ rule: rule.id, file, found: typeof held === 'string' ? held : slopnatorName(rule.id) })
186
+ }
187
+
188
+ if (walking.length === 0 || !PARSEABLE.test(file)) continue
189
+
190
+ const ast = tree(text)
191
+ if (ast === undefined) {
192
+ for (const { rule } of walking) {
193
+ findings.push({
194
+ rule: rule.id,
195
+ file,
196
+ found:
197
+ 'this file could not be parsed, so nothing about its structure was measured — ' +
198
+ 'a green result here says nothing about this rule on this file',
199
+ })
200
+ }
201
+ continue
202
+ }
203
+
204
+ const comments = ast.comments ?? []
205
+ for (const { rule, slopnator } of walking) {
206
+ const on = new Set(slopnator.on)
207
+ if (on.size === 0) {
208
+ findings.push({
209
+ rule: rule.id,
210
+ file,
211
+ found: 'its slopnator visits no node type, so it measured nothing here rather than finding nothing',
212
+ })
213
+ continue
214
+ }
215
+ visit(ast, on, (node, ancestors) => {
216
+ const held = slopnator.holds(node, {
217
+ filename: file,
218
+ text,
219
+ ancestors: () => ancestors,
220
+ comments: () => comments,
221
+ })
222
+ if (held === false || held === NOT_WRITTEN) return
223
+ findings.push({ rule: rule.id, file, found: typeof held === 'string' ? held : slopnatorName(rule.id) })
224
+ })
225
+ }
226
+ }
227
+
228
+ for (const rule of all) {
229
+ const slopnator = slopnators.get(slopnatorName(rule.id))
230
+ if (slopnator === undefined || reads(slopnator)) continue
231
+ for (const found of slopnator.checks(root)) findings.push({ rule: rule.id, file: '', found })
232
+ }
233
+
234
+ const unrun = all.filter((rule) => slopnators.get(slopnatorName(rule.id)) === undefined).map((rule) => rule.id)
235
+ const linted = walkers.map((one) => one.rule.id)
236
+ const unmet = [...new Set([...readers, ...walkers].map((one) => one.rule.id))].filter((id) => !met.has(id)).sort()
237
+
238
+ return {
239
+ findings,
240
+ swept,
241
+ tracked: files.length,
242
+ unrun,
243
+ unmet,
244
+ linted,
245
+ rules:
246
+ readers.length +
247
+ walkers.length +
248
+ all.filter((rule) => {
249
+ const slopnator = slopnators.get(slopnatorName(rule.id))
250
+ return slopnator !== undefined && !reads(slopnator)
251
+ }).length,
252
+ }
253
+ }
254
+
255
+ if (invoked(import.meta.url)) {
256
+ const governing = [...CONSTRAINTS, ...((await rules()) ?? [])]
257
+ const slopnators = await load(ROOT, governing)
258
+ const named = new Map(governing.map((rule) => [rule.id, rule]))
259
+ const run = sweep(ROOT, governing, slopnators, tracked(ROOT))
260
+
261
+ const warned = new Set<string>()
262
+ const grouped = new Map<string, string[]>()
263
+ const files = new Map<string, Set<string>>()
264
+ for (const finding of run.findings) {
265
+ if (named.get(finding.rule)?.severity === 'warning') warned.add(finding.rule)
266
+ const held = grouped.get(finding.rule) ?? []
267
+ held.push(finding.file === '' ? ` ${finding.found}` : ` ${finding.file} — ${finding.found}`)
268
+ grouped.set(finding.rule, held)
269
+ const where = files.get(finding.rule) ?? new Set<string>()
270
+ where.add(finding.file)
271
+ files.set(finding.rule, where)
272
+ }
273
+ const refused = new Set(run.findings.map((one) => one.file).filter((one) => one !== ''))
274
+
275
+ const findings: Finding[] = [
276
+ ...[...grouped.entries()].map(([id, where]): Finding => {
277
+ const rule = named.get(id)
278
+ const count = files.get(id)?.size ?? 0
279
+ const said = where.length === count ? `${String(count)} file(s)` : `${String(where.length)} finding(s) in ${String(count)} file(s)`
280
+ return {
281
+ detail: `${id} — ${rule?.bans ?? 'refused'}. ${said}:\n${where.sort().join('\n')}`,
282
+ instead: rule?.instead ?? `read what ${id} refuses and stop writing it`,
283
+ ...(rule?.severity === 'warning' ? { severity: 'warning' as const } : {}),
284
+ }
285
+ }),
286
+ ...run.unrun.map((rule): Finding => ({
287
+ detail: `${rule} DID NOT RUN — no slopnator is on disk for it, so this sweep measured nothing for that rule and a green result here says nothing about it`,
288
+ instead: 'write the walk the rule names, or delete the rule and its row together — half a rule reads as a whole one from every direction',
289
+ })),
290
+ ]
291
+
292
+ runChecker(
293
+ 'sweep',
294
+ findings,
295
+ `each naming one rule and the files it refuses — ${String(run.findings.length)} refusal(s) across ${String(refused.size)} file(s), ` +
296
+ `over ${String(run.rules)} rule(s) run across ${String(run.swept)} of ${String(run.tracked)} file(s) in this tree, ` +
297
+ `${String(run.linted.length)} of them reading a syntax tree` +
298
+ `${run.unrun.length === 0 ? '' : `, ${String(run.unrun.length)} that DID NOT RUN AT ALL`}` +
299
+ `${run.unmet.length === 0 ? '' : `, and ${String(run.unmet.length)} that governed no file here and so measured nothing: ${run.unmet.join(', ')}`}`,
300
+ )
301
+ const forbidding = findings.length - run.findings.filter((one) => named.get(one.rule)?.severity === 'warning').length
302
+ if (forbidding > 0) exit(1)
303
+ if (warned.size > 0) {
304
+ stdout.write(`sweep: every finding above is a warning — ${[...warned].sort().join(', ')} state \`severity: 'warning'\`, so the gate does not stop here.\n`)
305
+ }
306
+ exit(0)
307
+ }