@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,320 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+ import { pathToFileURL } from 'node:url'
4
+
5
+ import { exit, invoked } from '../../core/invoked.ts'
6
+
7
+ import type { Linter as LinterType, Rule } from 'eslint'
8
+ import { Linter } from 'eslint'
9
+
10
+ import type { Constraint } from '../constraint.ts'
11
+
12
+ const TREE = findTree()
13
+ import { CONSTRAINTS } from '../constraint.ts'
14
+ import { NOT_WRITTEN, ROOT, governs, load, proofsOf, reads, rules, slopnatorName, sweeps, write, type Byproduct, type Proof, type Slopnator, type Sourced } from '../slopnator.ts'
15
+ import { forget } from '../slopnators/sl_unthreaded.ts'
16
+ import { findTree, inTree } from '../../core/tree.ts'
17
+
18
+ export const TRIALS_PATH = `${findTree().name}/_generated/trials.ts`
19
+
20
+ const PROBE = 'probe.tsx'
21
+
22
+ const WRAPPINGS: readonly ((source: string) => string)[] = [
23
+ (source) => source,
24
+ (source) => `const _probe = { ${source} }`,
25
+ (source) => `const _probe = <Probe ${source} />`,
26
+ (source) => `const _probe = () => { ${source} }`,
27
+ (source) => `const _probe = <Probe>{${source}}</Probe>`,
28
+ ]
29
+
30
+ export interface Scenario {
31
+ readonly files: readonly Sourced[]
32
+ readonly at: string
33
+ }
34
+
35
+ export interface Trial {
36
+ readonly id: string
37
+ readonly rule: string
38
+ readonly name: string
39
+ readonly scenario: Scenario
40
+ readonly verdict: { readonly reports: boolean }
41
+ readonly requires: string
42
+ }
43
+
44
+ export interface Derivation {
45
+ readonly trials: readonly Trial[]
46
+ }
47
+
48
+ export interface Outcome {
49
+ readonly id: string
50
+ readonly rule: string
51
+ readonly passed: boolean
52
+ readonly failures: readonly string[]
53
+ readonly parsed: boolean
54
+ }
55
+
56
+ export interface Run {
57
+ readonly outcomes: readonly Outcome[]
58
+ readonly passed: number
59
+ readonly failed: number
60
+ readonly measured: number
61
+ }
62
+
63
+ export function probeAt(rule: Constraint): string {
64
+ const path = rule.where.paths[0] ?? 'src/'
65
+ if (path === '.') return PROBE
66
+ return path.endsWith('/') ? `${path}${PROBE}` : `${path}/${PROBE}`
67
+ }
68
+
69
+ function requiresOf(rule: Constraint, reports: boolean): string {
70
+ return reports
71
+ ? `${rule.id} reports on this source, at a path it governs`
72
+ : `${rule.id} stays silent on this source — the fix it recommends is not itself refused`
73
+ }
74
+
75
+ function scenarioOf(rule: Constraint, proof: Proof): Scenario {
76
+ if (typeof proof !== 'string') return proof
77
+ const path = probeAt(rule)
78
+ return { files: [{ at: path, source: proof }], at: path }
79
+ }
80
+
81
+ function spoken(scenario: Scenario): string {
82
+ const target = scenario.files.find((file) => file.at === scenario.at) ?? scenario.files[0]
83
+ const rest = scenario.files.length - 1
84
+ return `${target?.source ?? ''}${rest > 0 ? ` (beside ${String(rest)} more file(s))` : ''}`
85
+ }
86
+
87
+ export function deriveTrials(all: readonly Constraint[], slopnators: ReadonlyMap<string, Slopnator>): Derivation {
88
+ const trials: Trial[] = []
89
+
90
+ for (const rule of all) {
91
+ const proofs = proofsOf(rule, slopnators)
92
+ const put = (proof: Proof, index: number, reports: boolean): void => {
93
+ const scenario = scenarioOf(rule, proof)
94
+ trials.push({
95
+ id: `trl_rule_${rule.id.replace(/-/g, '_')}_${reports ? 'refuses' : 'accepts'}_${String(index)}`,
96
+ rule: rule.id,
97
+ name: `${rule.id} ${reports ? 'refuses' : 'accepts'}: ${spoken(scenario)}`,
98
+ scenario,
99
+ verdict: { reports },
100
+ requires: requiresOf(rule, reports),
101
+ })
102
+ }
103
+ proofs.wrong.forEach((proof, index) => { put(proof, index, true) })
104
+ proofs.right.forEach((proof, index) => { put(proof, index, false) })
105
+ }
106
+
107
+ return { trials: trials.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) }
108
+ }
109
+
110
+ export function trialsByproduct(derivation: Derivation): Byproduct {
111
+ return {
112
+ path: TRIALS_PATH,
113
+ content: `export const RULE_TRIALS = ${JSON.stringify(derivation.trials, null, 2)} as const
114
+ `,
115
+ }
116
+ }
117
+
118
+ function config(rule: Constraint, module: Rule.RuleModule, parser: unknown): Parameters<LinterType['verify']>[1] {
119
+ return [
120
+ {
121
+ files: ['**/*.ts', '**/*.tsx'],
122
+ languageOptions: {
123
+ parser,
124
+ parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true } },
125
+ },
126
+ plugins: { rule: { rules: { [rule.id]: module } } },
127
+ rules: { [`rule/`]: 'error' },
128
+ },
129
+ ] as unknown as Parameters<LinterType['verify']>[1]
130
+ }
131
+
132
+ export function runTrial(trial: Trial, rule: Constraint, module: Rule.RuleModule, parser: unknown, slopnator?: Slopnator): Outcome {
133
+ const linter = new Linter()
134
+ const failures: string[] = []
135
+
136
+ if (!governs(rule, trial.scenario.at)) {
137
+ return {
138
+ id: trial.id,
139
+ rule: rule.id,
140
+ passed: false,
141
+ parsed: false,
142
+ failures: [`the trial's own path "${trial.scenario.at}" is not one the rule governs, so nothing could have run`],
143
+ }
144
+ }
145
+
146
+ const target = trial.scenario.files.find((file) => file.at === trial.scenario.at)
147
+ if (target === undefined) {
148
+ return {
149
+ id: trial.id,
150
+ rule: rule.id,
151
+ passed: false,
152
+ parsed: false,
153
+ failures: [`the scenario writes no file at "${trial.scenario.at}", the path it lints, so nothing could have been measured`],
154
+ }
155
+ }
156
+
157
+ for (const file of trial.scenario.files) {
158
+ if (!existsSync(join(ROOT, file.at))) continue
159
+ return {
160
+ id: trial.id,
161
+ rule: rule.id,
162
+ passed: false,
163
+ parsed: false,
164
+ failures: [`the scenario would overwrite "${file.at}", which is a real file in this repository — a trial may not stand on a path somebody authored`],
165
+ }
166
+ }
167
+
168
+ const written: string[] = []
169
+ const created: string[] = []
170
+ let parsed = false
171
+ let satisfied = false
172
+
173
+ try {
174
+ for (const file of trial.scenario.files) {
175
+ const path = join(ROOT, file.at)
176
+ const made = mkdirSync(dirname(path), { recursive: true })
177
+ if (made !== undefined) created.push(made)
178
+ writeFileSync(path, file.source)
179
+ written.push(path)
180
+ }
181
+ forget()
182
+
183
+ const path = join(ROOT, trial.scenario.at)
184
+
185
+ if (slopnator !== undefined && !reads(slopnator)) {
186
+ parsed = true
187
+ satisfied = slopnator.checks(ROOT).length > 0 === trial.verdict.reports
188
+ } else if (slopnator !== undefined && sweeps(slopnator)) {
189
+ const held = slopnator.holds(undefined, {
190
+ filename: trial.scenario.at,
191
+ text: target.source,
192
+ ancestors: () => [],
193
+ comments: () => [],
194
+ })
195
+ parsed = true
196
+ satisfied = (held !== false && held !== NOT_WRITTEN) === trial.verdict.reports
197
+ } else {
198
+ for (const wrap of WRAPPINGS) {
199
+ const text = wrap(target.source)
200
+ writeFileSync(path, text)
201
+ const messages = linter.verify(text, config(rule, module, parser), trial.scenario.at)
202
+ if (messages.some((message) => message.fatal === true)) continue
203
+ parsed = true
204
+ satisfied = messages.some((message) => message.ruleId === `rule/`) === trial.verdict.reports
205
+ break
206
+ }
207
+ }
208
+ } finally {
209
+ for (const path of written) if (existsSync(path)) rmSync(path)
210
+ for (const path of created) if (existsSync(path)) rmSync(path, { recursive: true })
211
+ forget()
212
+ }
213
+
214
+ if (!parsed) {
215
+ failures.push('no wrapping of this source parsed, so nothing was measured — a trial that cannot run is not a trial that passed')
216
+ } else if (!satisfied) {
217
+ failures.push(
218
+ trial.verdict.reports
219
+ ? `expected to report on this source and it stayed silent — the rule documents a form its own slopnator does not refuse`
220
+ : `expected to stay silent on this source and it reported — the rule refuses the fix it recommends`,
221
+ )
222
+ }
223
+
224
+ return { id: trial.id, rule: rule.id, passed: failures.length === 0, parsed, failures }
225
+ }
226
+
227
+ export function runTrials(
228
+ trials: readonly Trial[],
229
+ all: readonly Constraint[],
230
+ rules: Readonly<Record<string, Rule.RuleModule>>,
231
+ parser: unknown,
232
+ slopnators?: ReadonlyMap<string, Slopnator>,
233
+ ): Run {
234
+ const byName = new Map(all.map((rule) => [rule.id, rule]))
235
+ const outcomes = trials.map((trial): Outcome => {
236
+ const rule = byName.get(trial.rule)
237
+ const module = rules[trial.rule]
238
+ if (rule === undefined || module === undefined) {
239
+ return {
240
+ id: trial.id,
241
+ rule: trial.rule,
242
+ passed: false,
243
+ parsed: false,
244
+ failures: [`no rule named "${trial.rule}" is registered, so this trial is about nothing — regenerate with \`thread trials\``],
245
+ }
246
+ }
247
+ return runTrial(trial, rule, module, parser, slopnators?.get(slopnatorName(rule.id)))
248
+ })
249
+
250
+ return {
251
+ outcomes,
252
+ passed: outcomes.filter((outcome) => outcome.passed).length,
253
+ failed: outcomes.filter((outcome) => !outcome.passed).length,
254
+ measured: outcomes.filter((outcome) => outcome.parsed).length,
255
+ }
256
+ }
257
+
258
+ if (invoked(import.meta.url)) {
259
+ const own = await rules()
260
+ const governing = own ?? CONSTRAINTS
261
+ const source =
262
+ own === undefined
263
+ ? `the ${String(CONSTRAINTS.length)} constraint(s) this tree authors`
264
+ : `the ${String(own.length)} rule(s) this tree declares — a shipped constraint is proved in the package that ships it, against the paths that package governs`
265
+ const slopnators = await load(ROOT, governing)
266
+ const derivation = deriveTrials(governing, slopnators)
267
+ const byproduct = trialsByproduct(derivation)
268
+
269
+ if (process.argv.includes('--check')) {
270
+ const path = join(ROOT, byproduct.path)
271
+ const current = existsSync(path) ? readFileSync(path, 'utf8') : ''
272
+ const drifted = current !== byproduct.content
273
+ if (drifted) console.error(`trial: ${byproduct.path} is not what the rules derive. Run \`thread trials\` and commit the result.`)
274
+ console.log(`trial: ${String(derivation.trials.length)} derived from ${source}, ${String(drifted ? 1 : 0)} findings.`)
275
+ exit(drifted ? 1 : 0)
276
+ }
277
+
278
+ if (process.argv.includes('--try')) {
279
+ const at = process.argv.indexOf('--rule')
280
+ const only = at === -1 ? undefined : (process.argv[at + 1] ?? '').split(',').map((one) => one.trim()).filter(Boolean)
281
+ const unknown = (only ?? []).filter((id) => !governing.some((rule) => rule.id === id))
282
+ if (unknown.length > 0) {
283
+ console.error(`trial: no rule is called ${unknown.join(', ')} — a filter naming nothing would measure nothing and exit 0`)
284
+ exit(1)
285
+ }
286
+ const chosen = only === undefined ? derivation.trials : derivation.trials.filter((trial) => only.includes(trial.rule))
287
+ if (chosen.length === 0) {
288
+ if (only !== undefined) {
289
+ console.error('trial: the filter selected no trials, and measuring nothing must not exit 0')
290
+ exit(1)
291
+ }
292
+ if (governing.length > 0) {
293
+ console.error(
294
+ `trial: ${String(governing.length)} rule(s) govern and none derived a trial — every rule owes a \`wrong\` and a \`right\`, so measuring nothing must not exit 0`,
295
+ )
296
+ exit(1)
297
+ }
298
+ console.log(`trial: nothing to prove — ${source}.`)
299
+ exit(0)
300
+ }
301
+
302
+ const { default: plugin } = await import('../lint.ts')
303
+ const parser = (await import('typescript-eslint')).parser
304
+ const run = runTrials(chosen, governing, plugin.rules, parser, slopnators)
305
+ for (const outcome of run.outcomes) {
306
+ if (outcome.passed) continue
307
+ for (const failure of outcome.failures) console.error(`trial: ${outcome.id} — ${failure}`)
308
+ }
309
+ console.log(`trial: ${String(run.passed)} of ${String(run.outcomes.length)} passed, ${String(run.measured)} measured.`)
310
+ if (only !== undefined) {
311
+ console.log(
312
+ `trial: PARTIAL RUN — ${String(derivation.trials.length - chosen.length)} of ${String(derivation.trials.length)} derived trials did not run. \`thread trials\` with no arguments runs every one.`,
313
+ )
314
+ }
315
+ exit(run.failed === 0 ? 0 : 1)
316
+ }
317
+
318
+ write(ROOT, byproduct)
319
+ console.log(`trial: wrote ${byproduct.path} — ${String(derivation.trials.length)} trials from ${source}.`)
320
+ }
@@ -0,0 +1,227 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { fileURLToPath } from 'node:url'
3
+
4
+ import { Linter, type Rule } from 'eslint'
5
+ import tseslint from 'typescript-eslint'
6
+
7
+ import { THING } from '../core/scope.ts'
8
+ import { CONSTRAINTS, type Constraint } from './constraint.ts'
9
+ import {
10
+ NOT_WRITTEN,
11
+ governs,
12
+ reads,
13
+ visits,
14
+ load,
15
+ ROOT,
16
+ rules as appRules,
17
+ slopnatorName,
18
+ type Slopnator,
19
+ type SlopnatorContext,
20
+ } from './slopnator.ts'
21
+
22
+
23
+
24
+ const PROBE: SlopnatorContext = {
25
+ filename: 'probe.ts',
26
+ text: '',
27
+ ancestors: () => [],
28
+ comments: () => [],
29
+ }
30
+
31
+ export function ruleMessage(rule: Constraint): string {
32
+ const cites = (rule.cites ?? []).map((cite) => `${cite.on} — ${cite.said}`).join(' · ')
33
+ return cites === '' ? `${rule.bans} — ${rule.because}. Instead: ${rule.instead}.` : `${rule.bans} — ${rule.because}. Instead: ${rule.instead}. (${cites})`
34
+ }
35
+
36
+ const oneLine = (text: string): string => text.replace(/\s*\n\s*/g, ' ').trim()
37
+
38
+ export function agentInstructions(): readonly string[] {
39
+ return [...CONSTRAINTS]
40
+ .sort((one, two) => (one.id < two.id ? -1 : one.id > two.id ? 1 : 0))
41
+ .map(
42
+ (rule) =>
43
+ `${oneLine(rule.instead)} — never ${oneLine(rule.bans)}. Wrong: \`${oneLine(rule.wrong)}\`. Right: \`${oneLine(rule.right)}\`. [${rule.id}]`,
44
+ )
45
+ }
46
+
47
+ export function defineRule(rule: Constraint, slopnators: ReadonlyMap<string, Slopnator>): Constraint {
48
+ const expected = slopnatorName(rule.id)
49
+
50
+ if (rule.where.paths.length === 0) {
51
+ throw new Error(
52
+ `rule "${rule.id}" names no path, so it governs nothing. A rule that cannot fire is indistinguishable from a rule being obeyed.`,
53
+ )
54
+ }
55
+
56
+ const found = slopnators.get(expected)
57
+ if (found === undefined) {
58
+ throw new Error(
59
+ `"${rule.id}" names "${expected}", and no such file is beside the tree or shipped with the package. ` +
60
+ 'One that cannot run reads as enforced, reports nothing, and is indistinguishable from one being obeyed. ' +
61
+ 'Seed it.',
62
+ )
63
+ }
64
+
65
+ return rule
66
+ }
67
+
68
+ export function defineRules(slopnators: ReadonlyMap<string, Slopnator>): readonly Constraint[] {
69
+ const seen = new Set<string>()
70
+ for (const rule of GOVERNING) {
71
+ if (seen.has(rule.id)) throw new Error(`rule "${rule.id}" is declared twice in packages/core/src/integrity/constraint.ts`)
72
+ seen.add(rule.id)
73
+ defineRule(rule, slopnators)
74
+ }
75
+ return GOVERNING
76
+ }
77
+
78
+ export function ruleRule(rule: Constraint, slopnators: ReadonlyMap<string, Slopnator>): Rule.RuleModule {
79
+ const slopnator = slopnators.get(slopnatorName(rule.id))
80
+ return {
81
+ meta: {
82
+ type: 'problem',
83
+ docs: { description: rule.bans },
84
+ schema: [],
85
+ ...(slopnator !== undefined && reads(slopnator) && slopnator.fixes !== undefined ? { fixable: 'code' as const } : {}),
86
+ messages: { broken: `${ruleMessage(rule)} Found: {{found}}.` },
87
+ },
88
+ create(context) {
89
+ if (!governs(rule, context.filename)) return {}
90
+ const found = slopnator
91
+ if (found === undefined || !visits(found)) return {}
92
+ const visit = (node: unknown): void => {
93
+ const at: SlopnatorContext = {
94
+ filename: context.filename,
95
+ text: context.sourceCode.getText(),
96
+ ancestors: () => context.sourceCode.getAncestors(node as never),
97
+ comments: () =>
98
+ context.sourceCode.getAllComments().map((comment) => ({
99
+ value: comment.value,
100
+ range: (comment.range ?? [0, 0]) as readonly [number, number],
101
+ })),
102
+ }
103
+ const held = found.holds(node, at)
104
+ if (held === false || held === NOT_WRITTEN) return
105
+ const mend = found.fixes
106
+ const mended = mend === undefined ? undefined : mend(node, at)
107
+ context.report({
108
+ node: node as never,
109
+ messageId: 'broken',
110
+ data: { found: typeof held === 'string' ? held : slopnatorName(rule.id) },
111
+ ...(mended === undefined ? {} : { fix: (fixer) => fixer.replaceTextRange([0, at.text.length], mended) }),
112
+ })
113
+ }
114
+ return Object.fromEntries(found.on.map((type) => [type, visit]))
115
+ },
116
+ }
117
+ }
118
+
119
+ export function ruleRules(slopnators: ReadonlyMap<string, Slopnator>): Readonly<Record<string, Rule.RuleModule>> {
120
+ return Object.fromEntries(GOVERNING.map((rule) => [rule.id, ruleRule(rule, slopnators)]))
121
+ }
122
+
123
+ export function ruleSeverities(): Readonly<Record<string, 'error' | 'warn'>> {
124
+ return Object.fromEntries(
125
+ GOVERNING.map((rule) => [`lint/${rule.id}`, rule.severity === 'warning' ? ('warn' as const) : ('error' as const)]),
126
+ )
127
+ }
128
+
129
+ const GOVERNING = [...CONSTRAINTS, ...((await appRules()) ?? [])]
130
+ const SLOPNATORS = await load(ROOT, GOVERNING)
131
+
132
+ defineRules(SLOPNATORS)
133
+
134
+ export const plugin = { rules: ruleRules(SLOPNATORS) }
135
+
136
+ export default plugin
137
+
138
+ function ignored(): readonly string[] {
139
+ try {
140
+ return execFileSync('git', ['ls-files', '--others', '--ignored', '--exclude-standard', '--directory'], {
141
+ cwd: ROOT,
142
+ encoding: 'utf8',
143
+ stdio: ['ignore', 'pipe', 'ignore'],
144
+ })
145
+ .split('\n')
146
+ .filter(Boolean)
147
+ .map((one) => (one.endsWith('/') ? one + '**' : one))
148
+ } catch {
149
+ return []
150
+ }
151
+ }
152
+
153
+ export const rules = {
154
+ files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.mjs', '**/*.js'],
155
+ ignores: ignored(),
156
+ languageOptions: {
157
+ parser: tseslint.parser,
158
+ parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true } },
159
+ },
160
+ plugins: { lint: plugin },
161
+ rules: ruleSeverities(),
162
+ }
163
+
164
+ export function ruleDeclarations(): readonly (Constraint & { readonly name: string })[] {
165
+ return [...CONSTRAINTS]
166
+ .sort((one, two) => (one.id < two.id ? -1 : one.id > two.id ? 1 : 0))
167
+ .map((rule) => ({ ...rule, name: rule.id }))
168
+ }
169
+
170
+ export interface RuleViolation {
171
+ readonly file: string
172
+ readonly line: number
173
+ readonly column: number
174
+ readonly rule: string
175
+
176
+ readonly message: string
177
+
178
+ readonly bans?: string
179
+
180
+ readonly instead?: string
181
+
182
+ readonly wrong?: string
183
+
184
+ readonly right?: string
185
+ }
186
+
187
+ export type RuleRunner = (file: string, source: string) => readonly RuleViolation[]
188
+
189
+ export function theRules(): RuleRunner {
190
+ const linter = new Linter()
191
+ const declared = new Map(ruleDeclarations().map((one) => [one.name, one]))
192
+
193
+ const config = [
194
+ {
195
+ files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],
196
+
197
+ linterOptions: { noInlineConfig: true },
198
+ languageOptions: {
199
+ parser: tseslint.parser,
200
+ parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true } },
201
+ },
202
+ plugins: { [`@${THING}`]: plugin },
203
+ rules: Object.fromEntries(
204
+ GOVERNING.map((rule) => [`@${THING}/${rule.id}`, rule.severity === 'warning' ? 'warn' : 'error']),
205
+ ),
206
+ },
207
+ ] as unknown as Parameters<InstanceType<typeof Linter>['verify']>[1]
208
+
209
+ return (file, source) =>
210
+ linter
211
+ .verify(source, config, file)
212
+ .filter((message) => message.ruleId != null)
213
+ .map((message) => {
214
+ const rule = String(message.ruleId).replace(`@${THING}/`, '')
215
+ const found = declared.get(rule)
216
+ return {
217
+ file,
218
+ line: message.line,
219
+ column: message.column,
220
+ rule,
221
+ message: message.message,
222
+ ...(found === undefined
223
+ ? {}
224
+ : { bans: found.bans, instead: found.instead, wrong: found.wrong, right: found.right }),
225
+ }
226
+ })
227
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cwd, exit, stderr, stdin, stdout } from 'node:process'
4
+
5
+ import { repoSource } from './diagnostics.ts'
6
+ import { encode, MessageBuffer } from './protocol.ts'
7
+ import { LspServer } from './server.ts'
8
+
9
+ const server = new LspServer({
10
+ root: cwd(),
11
+ sourceFor: repoSource,
12
+ send: (message) => stdout.write(encode(message)),
13
+ })
14
+
15
+ const buffer = new MessageBuffer()
16
+
17
+ let queue: Promise<void> = Promise.resolve()
18
+
19
+ const fatal = (error: unknown): never => {
20
+ stderr.write(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`)
21
+ exit(1)
22
+ }
23
+
24
+ stdin.on('data', (chunk: Buffer) => {
25
+ let messages
26
+ try {
27
+ messages = buffer.push(chunk)
28
+ } catch (error) {
29
+ fatal(error)
30
+ return
31
+ }
32
+
33
+ for (const message of messages) {
34
+ queue = queue.then(async () => {
35
+ await server.handle(message)
36
+ if (server.exited) exit(server.exitCode)
37
+ })
38
+ queue.catch(fatal)
39
+ }
40
+ })
41
+
42
+ stdin.on('end', () => exit(server.exitCode))
@@ -0,0 +1,92 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import { detailOf, rangeOf, type Finding } from './findings.ts'
5
+ import { lintFindings } from './lint.ts'
6
+
7
+ export interface DiagnosticSource {
8
+
9
+ lint(relative: string, text: string): Promise<readonly Finding[]>
10
+
11
+ read(relative: string): string | null
12
+ }
13
+
14
+ export function repoSource(root: string): DiagnosticSource {
15
+ return {
16
+ lint: (relative, text) => lintFindings(root, relative, text),
17
+ read: (relative) => {
18
+ try {
19
+ return readFileSync(join(root, relative), 'utf8')
20
+ } catch {
21
+
22
+ return null
23
+ }
24
+ },
25
+ }
26
+ }
27
+
28
+ export const ERROR = 1
29
+ export const WARNING = 2
30
+
31
+ export interface Position {
32
+ readonly line: number
33
+ readonly character: number
34
+ }
35
+
36
+ export interface Diagnostic {
37
+ readonly range: { readonly start: Position; readonly end: Position }
38
+ readonly severity: typeof ERROR | typeof WARNING
39
+ readonly source: string
40
+ readonly code: string
41
+ readonly message: string
42
+ }
43
+
44
+ export const SOURCE_NAME = 'thread'
45
+
46
+ export function toDiagnostic(finding: Finding, text: string): Diagnostic {
47
+ const at = rangeOf(finding, text)
48
+ return {
49
+ range: {
50
+ start: { line: at.line - 1, character: at.column - 1 },
51
+ end: { line: (at.endLine ?? at.line) - 1, character: (at.endColumn ?? at.column) - 1 },
52
+ },
53
+ severity: finding.severity === 'error' ? ERROR : WARNING,
54
+ source: SOURCE_NAME,
55
+ code: finding.check,
56
+ message: detailOf(finding),
57
+ }
58
+ }
59
+
60
+ export async function report(
61
+ source: DiagnosticSource,
62
+ open: ReadonlyMap<string, string>,
63
+ previous: ReadonlySet<string> = new Set(),
64
+ ): Promise<ReadonlyMap<string, readonly Diagnostic[]>> {
65
+ const findings: Finding[] = []
66
+
67
+ for (const [relative, text] of open) {
68
+ findings.push(...(await source.lint(relative, text)))
69
+ }
70
+
71
+ const byFile = new Map<string, Finding[]>()
72
+ for (const finding of findings) {
73
+ const bucket = byFile.get(finding.file)
74
+ if (bucket === undefined) byFile.set(finding.file, [finding])
75
+ else bucket.push(finding)
76
+ }
77
+
78
+ const published = new Map<string, readonly Diagnostic[]>()
79
+ for (const [relative, group] of byFile) {
80
+ const text = open.get(relative) ?? source.read(relative) ?? ''
81
+ published.set(
82
+ relative,
83
+ group.map((finding) => toDiagnostic(finding, text)),
84
+ )
85
+ }
86
+
87
+ for (const relative of [...previous, ...open.keys()]) {
88
+ if (!published.has(relative)) published.set(relative, [])
89
+ }
90
+
91
+ return published
92
+ }