@prestack/thread 0.1.20 → 0.1.22

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 (148) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/dist/core/attach.js +21 -0
  3. package/dist/core/descend.d.ts +2 -0
  4. package/dist/core/descend.js +18 -3
  5. package/dist/core/generators/commit.js +55 -3
  6. package/dist/core/generators/dev.d.ts +1 -0
  7. package/dist/core/generators/dev.js +68 -0
  8. package/dist/core/rows.d.ts +5 -0
  9. package/dist/core/rows.js +45 -0
  10. package/dist/core/tree.d.ts +1 -0
  11. package/dist/core/tree.js +7 -1
  12. package/dist/derivation/generators/build.d.ts +3 -1
  13. package/dist/derivation/generators/build.js +103 -22
  14. package/dist/derivation/generators/release.js +74 -26
  15. package/dist/derivation/release.js +3 -3
  16. package/dist/integrations/cli.d.ts +3 -9
  17. package/dist/integrations/cli.js +96 -36
  18. package/dist/integrations/declared.d.ts +8 -0
  19. package/dist/integrations/declared.js +22 -0
  20. package/dist/integrations/pglite.js +13 -3
  21. package/dist/integrations/target.d.ts +4 -1
  22. package/dist/integrations/target.js +20 -6
  23. package/dist/integrity/attach.js +1 -5
  24. package/dist/integrity/generators/publish.js +2 -1
  25. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  26. package/dist/integrity/slopnators/sl_unlisted_program.js +2 -2
  27. package/dist/operations/access.js +3 -2
  28. package/dist/operations/capabilities.d.ts +2 -0
  29. package/dist/operations/capabilities.js +2 -0
  30. package/dist/operations/generators/capability.js +234 -24
  31. package/dist/operations/generators/compile.d.ts +21 -0
  32. package/dist/operations/generators/compile.js +151 -51
  33. package/dist/operations/generators/permits.d.ts +2 -1
  34. package/dist/operations/generators/permits.js +52 -47
  35. package/dist/operations/generators/seed.d.ts +3 -0
  36. package/dist/operations/generators/seed.js +25 -3
  37. package/dist/operations/nodes.d.ts +2 -2
  38. package/package.json +1 -1
  39. package/dist/core/actor.ts +0 -21
  40. package/dist/core/attach.ts +0 -613
  41. package/dist/core/chain.ts +0 -145
  42. package/dist/core/condition.ts +0 -227
  43. package/dist/core/descend.ts +0 -1173
  44. package/dist/core/errors/catalogue.ts +0 -314
  45. package/dist/core/errors/classes.ts +0 -51
  46. package/dist/core/errors/error.ts +0 -152
  47. package/dist/core/errors/namespaces.ts +0 -19
  48. package/dist/core/finding.ts +0 -37
  49. package/dist/core/generators/commit.ts +0 -157
  50. package/dist/core/init.ts +0 -222
  51. package/dist/core/invoked.ts +0 -52
  52. package/dist/core/round.ts +0 -74
  53. package/dist/core/rows.ts +0 -459
  54. package/dist/core/scope.ts +0 -20
  55. package/dist/core/thread.ts +0 -29
  56. package/dist/core/tree.ts +0 -90
  57. package/dist/core/types.ts +0 -351
  58. package/dist/derivation/attach.ts +0 -57
  59. package/dist/derivation/generators/build.ts +0 -371
  60. package/dist/derivation/generators/check.ts +0 -118
  61. package/dist/derivation/generators/determinism.ts +0 -145
  62. package/dist/derivation/generators/release.ts +0 -236
  63. package/dist/derivation/release.ts +0 -43
  64. package/dist/integrations/attach.ts +0 -187
  65. package/dist/integrations/cli.ts +0 -615
  66. package/dist/integrations/disk.ts +0 -466
  67. package/dist/integrations/generators/read.ts +0 -127
  68. package/dist/integrations/generators/vault.ts +0 -210
  69. package/dist/integrations/https.ts +0 -216
  70. package/dist/integrations/pglite.ts +0 -206
  71. package/dist/integrations/secret.ts +0 -54
  72. package/dist/integrations/target.ts +0 -656
  73. package/dist/integrations/vault.ts +0 -148
  74. package/dist/integrity/attach.ts +0 -150
  75. package/dist/integrity/check-threads.ts +0 -166
  76. package/dist/integrity/constraint.ts +0 -1246
  77. package/dist/integrity/generators/compiler.ts +0 -138
  78. package/dist/integrity/generators/gate.ts +0 -227
  79. package/dist/integrity/generators/issues.ts +0 -160
  80. package/dist/integrity/generators/publish.ts +0 -232
  81. package/dist/integrity/generators/trial.ts +0 -359
  82. package/dist/integrity/lint.ts +0 -214
  83. package/dist/integrity/lsp/bin.ts +0 -44
  84. package/dist/integrity/lsp/diagnostics.ts +0 -92
  85. package/dist/integrity/lsp/findings.ts +0 -63
  86. package/dist/integrity/lsp/lint.ts +0 -124
  87. package/dist/integrity/lsp/protocol.ts +0 -78
  88. package/dist/integrity/lsp/server.ts +0 -224
  89. package/dist/integrity/slopnator.ts +0 -324
  90. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  91. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  92. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  93. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  94. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  95. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  96. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  97. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  98. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  99. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  100. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  101. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  102. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  103. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  104. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  105. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  106. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  107. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  108. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  109. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  110. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  111. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  112. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  113. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  114. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  115. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  116. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  117. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  118. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  119. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  120. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  121. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  122. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  123. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  124. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  125. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  126. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  127. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  128. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  129. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  130. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  131. package/dist/integrity/sweep.ts +0 -395
  132. package/dist/knowledge/attach.ts +0 -327
  133. package/dist/knowledge/generators/docsgen.ts +0 -186
  134. package/dist/knowledge/generators/glossary.ts +0 -73
  135. package/dist/knowledge/glossary.ts +0 -6
  136. package/dist/operations/access.ts +0 -131
  137. package/dist/operations/attach.ts +0 -408
  138. package/dist/operations/capabilities.ts +0 -88
  139. package/dist/operations/dispatch.ts +0 -294
  140. package/dist/operations/error-results.ts +0 -31
  141. package/dist/operations/generators/capability.ts +0 -1054
  142. package/dist/operations/generators/compile.ts +0 -701
  143. package/dist/operations/generators/permits.ts +0 -224
  144. package/dist/operations/generators/seed.ts +0 -359
  145. package/dist/operations/generators/trial.ts +0 -666
  146. package/dist/operations/nodes.ts +0 -53
  147. package/dist/operations/operations.ts +0 -91
  148. package/dist/operations/result.ts +0 -28
@@ -1,214 +0,0 @@
1
- import { starts } from '../integrations/cli.ts'
2
-
3
- import { Linter, type Rule } from 'eslint'
4
- import tseslint from 'typescript-eslint'
5
-
6
- import { THING } from '../core/scope.ts'
7
- import { CONSTRAINTS, shown, type Constraint } from './constraint.ts'
8
- import {
9
- NOT_WRITTEN,
10
- governs,
11
- subject,
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
- export function ruleMessage(rule: Constraint): string {
23
- const cites = (rule.cites ?? []).map((cite) => `${cite.on} — ${cite.said}`).join(' · ')
24
- return cites === '' ? `${rule.bans} — ${rule.because}. Instead: ${rule.instead}.` : `${rule.bans} — ${rule.because}. Instead: ${rule.instead}. (${cites})`
25
- }
26
-
27
- const oneLine = (text: string): string => text.replace(/\s*\n\s*/g, ' ').trim()
28
-
29
- export function agentInstructions(): readonly string[] {
30
- return [...CONSTRAINTS]
31
- .sort((one, two) => (one.id < two.id ? -1 : one.id > two.id ? 1 : 0))
32
- .map(
33
- (rule) =>
34
- `${oneLine(rule.instead)} — never ${oneLine(rule.bans)}. Wrong: \`${oneLine(shown(rule.wrong))}\`. Right: \`${oneLine(shown(rule.right))}\`. [${rule.id}]`,
35
- )
36
- }
37
-
38
- export function defineRule(rule: Constraint, slopnators: ReadonlyMap<string, Slopnator>): Constraint {
39
- const expected = slopnatorName(rule.id)
40
-
41
- if (rule.where.paths.length === 0) {
42
- throw new Error(
43
- `rule "${rule.id}" names no path, so it governs nothing. A rule that cannot fire is indistinguishable from a rule being obeyed.`,
44
- )
45
- }
46
-
47
- const found = slopnators.get(expected)
48
- if (found === undefined) {
49
- throw new Error(
50
- `"${rule.id}" names "${expected}", and no such file is beside the tree or shipped with the package. ` +
51
- 'One that cannot run reads as enforced, reports nothing, and is indistinguishable from one being obeyed. ' +
52
- 'Seed it.',
53
- )
54
- }
55
-
56
- return rule
57
- }
58
-
59
- export function defineRules(slopnators: ReadonlyMap<string, Slopnator>): readonly Constraint[] {
60
- const seen = new Set<string>()
61
- for (const rule of GOVERNING) {
62
- if (seen.has(rule.id)) throw new Error(`rule "${rule.id}" is declared twice in packages/core/src/integrity/constraint.ts`)
63
- seen.add(rule.id)
64
- defineRule(rule, slopnators)
65
- }
66
- return GOVERNING
67
- }
68
-
69
- export function ruleRule(rule: Constraint, slopnators: ReadonlyMap<string, Slopnator>): Rule.RuleModule {
70
- const slopnator = slopnators.get(slopnatorName(rule.id))
71
- return {
72
- meta: {
73
- type: 'problem',
74
- docs: { description: rule.bans },
75
- schema: [],
76
- ...(slopnator !== undefined && reads(slopnator) && slopnator.fixes !== undefined ? { fixable: 'code' as const } : {}),
77
- messages: { broken: `${ruleMessage(rule)} Found: {{found}}.` },
78
- },
79
- create(context) {
80
- if (!governs(rule, context.filename)) return {}
81
- const found = slopnator
82
- if (found === undefined || !visits(found)) return {}
83
- const visit = (node: unknown): void => {
84
- const at: SlopnatorContext = {
85
- filename: context.filename.startsWith(ROOT) ? context.filename.slice(ROOT.length) : context.filename,
86
- text: context.sourceCode.getText(),
87
- ancestors: () => context.sourceCode.getAncestors(node as never),
88
- comments: () =>
89
- context.sourceCode.getAllComments().map((comment) => ({
90
- value: comment.value,
91
- range: (comment.range ?? [0, 0]) as readonly [number, number],
92
- })),
93
- }
94
- const held = found.holds(node, at)
95
- if (held === false || held === NOT_WRITTEN) return
96
- const mend = found.fixes
97
- const mended = mend === undefined ? undefined : mend(node, at)
98
- context.report({
99
- node: node as never,
100
- messageId: 'broken',
101
- data: { found: typeof held === 'string' ? held : slopnatorName(rule.id) },
102
- ...(mended === undefined ? {} : { fix: (fixer) => fixer.replaceTextRange([0, at.text.length], mended) }),
103
- })
104
- }
105
- return Object.fromEntries(found.on.map((type) => [type, visit]))
106
- },
107
- }
108
- }
109
-
110
- export function ruleRules(slopnators: ReadonlyMap<string, Slopnator>): Readonly<Record<string, Rule.RuleModule>> {
111
- return Object.fromEntries(GOVERNING.map((rule) => [rule.id, ruleRule(rule, slopnators)]))
112
- }
113
-
114
- export function ruleSeverities(): Readonly<Record<string, 'error' | 'warn'>> {
115
- return Object.fromEntries(
116
- GOVERNING.filter((rule) => subject(rule)).map((rule) => [`lint/${rule.id}`, rule.severity === 'warning' ? ('warn' as const) : ('error' as const)]),
117
- )
118
- }
119
-
120
- const GOVERNING = [...CONSTRAINTS, ...((await appRules()) ?? [])]
121
- const SLOPNATORS = await load(ROOT, GOVERNING)
122
-
123
- defineRules(SLOPNATORS)
124
-
125
- export const plugin = { rules: ruleRules(SLOPNATORS) }
126
-
127
- export default plugin
128
-
129
- function ignored(): readonly string[] {
130
- try {
131
- return starts('git', ['ls-files', '--others', '--ignored', '--exclude-standard', '--directory'], { at: ROOT })
132
- .out.split('\n')
133
- .filter(Boolean)
134
- .map((one) => (one.endsWith('/') ? one + '**' : one))
135
- } catch {
136
- return []
137
- }
138
- }
139
-
140
- export const rules = {
141
- files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.mjs', '**/*.js'],
142
- ignores: ignored(),
143
- languageOptions: {
144
- parser: tseslint.parser,
145
- parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true } },
146
- },
147
- plugins: { lint: plugin },
148
- rules: ruleSeverities(),
149
- }
150
-
151
- export function ruleDeclarations(): readonly (Constraint & { readonly name: string })[] {
152
- return [...CONSTRAINTS]
153
- .sort((one, two) => (one.id < two.id ? -1 : one.id > two.id ? 1 : 0))
154
- .map((rule) => ({ ...rule, name: rule.id }))
155
- }
156
-
157
- export interface RuleViolation {
158
- readonly file: string
159
- readonly line: number
160
- readonly column: number
161
- readonly rule: string
162
-
163
- readonly message: string
164
-
165
- readonly bans?: string
166
-
167
- readonly instead?: string
168
-
169
- readonly wrong?: string
170
-
171
- readonly right?: string
172
- }
173
-
174
- export type RuleRunner = (file: string, source: string) => readonly RuleViolation[]
175
-
176
- export function theRules(): RuleRunner {
177
- const linter = new Linter()
178
- const declared = new Map(ruleDeclarations().map((one) => [one.name, one]))
179
-
180
- const config = [
181
- {
182
- files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],
183
-
184
- linterOptions: { noInlineConfig: true },
185
- languageOptions: {
186
- parser: tseslint.parser,
187
- parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true } },
188
- },
189
- plugins: { [`@${THING}`]: plugin },
190
- rules: Object.fromEntries(
191
- GOVERNING.filter((rule) => subject(rule)).map((rule) => [`@${THING}/${rule.id}`, rule.severity === 'warning' ? 'warn' : 'error']),
192
- ),
193
- },
194
- ] as unknown as Parameters<InstanceType<typeof Linter>['verify']>[1]
195
-
196
- return (file, source) =>
197
- linter
198
- .verify(source, config, file)
199
- .filter((message) => message.ruleId != null)
200
- .map((message) => {
201
- const rule = String(message.ruleId).replace(`@${THING}/`, '')
202
- const found = declared.get(rule)
203
- return {
204
- file,
205
- line: message.line,
206
- column: message.column,
207
- rule,
208
- message: message.message,
209
- ...(found === undefined
210
- ? {}
211
- : { bans: found.bans, instead: found.instead, wrong: shown(found.wrong), right: shown(found.right) }),
212
- }
213
- })
214
- }
@@ -1,44 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { cwd, stderr, stdin, stdout } from 'node:process'
4
-
5
- import { exit } from '../../core/invoked.ts'
6
-
7
- import { repoSource } from './diagnostics.ts'
8
- import { encode, MessageBuffer } from './protocol.ts'
9
- import { LspServer } from './server.ts'
10
-
11
- const server = new LspServer({
12
- root: cwd(),
13
- sourceFor: repoSource,
14
- send: (message) => stdout.write(encode(message)),
15
- })
16
-
17
- const buffer = new MessageBuffer()
18
-
19
- let queue: Promise<void> = Promise.resolve()
20
-
21
- const fatal = (error: unknown): never => {
22
- stderr.write(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`)
23
- exit(1)
24
- }
25
-
26
- stdin.on('data', (chunk: Buffer) => {
27
- let messages
28
- try {
29
- messages = buffer.push(chunk)
30
- } catch (error) {
31
- fatal(error)
32
- return
33
- }
34
-
35
- for (const message of messages) {
36
- queue = queue.then(async () => {
37
- await server.handle(message)
38
- if (server.exited) exit(server.exitCode)
39
- })
40
- queue.catch(fatal)
41
- }
42
- })
43
-
44
- stdin.on('end', () => exit(server.exitCode))
@@ -1,92 +0,0 @@
1
- import { readFileSync } from '../../integrations/disk.ts'
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
- }
@@ -1,63 +0,0 @@
1
- export interface At {
2
- readonly line: number
3
- readonly column: number
4
- readonly endLine?: number
5
- readonly endColumn?: number
6
- }
7
-
8
- export interface Finding {
9
- readonly severity: 'error' | 'warning'
10
-
11
- readonly check: string
12
- readonly detail: string
13
-
14
- readonly file: string
15
-
16
- readonly at?: At
17
-
18
- readonly anchor?: string
19
-
20
- readonly suggestions?: readonly string[]
21
- }
22
-
23
- export function detailOf(finding: Finding): string {
24
- const offered = finding.suggestions ?? []
25
- return offered.length === 0 ? finding.detail : `${finding.detail} ${offered.join(' ')}`
26
- }
27
-
28
- const DECLARED_ID = /\b[a-z]{2,6}_[a-z][a-z0-9_]*\b/g
29
-
30
- export function anchorIn(detail: string): string | undefined {
31
- const candidates = [
32
- ...(detail.match(DECLARED_ID) ?? []),
33
- ...[...detail.matchAll(/"([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_*]+)+)"/g)].map((m) => m[1] as string),
34
- ]
35
- if (candidates.length === 0) return undefined
36
- return candidates.reduce((longest, one) => (one.length > longest.length ? one : longest))
37
- }
38
-
39
- export function locate(text: string, anchor: string): At | undefined {
40
- const lines = text.split('\n')
41
- for (const [index, line] of lines.entries()) {
42
- const column = line.indexOf(anchor)
43
- if (column === -1) continue
44
- return {
45
- line: index + 1,
46
- column: column + 1,
47
- endLine: index + 1,
48
- endColumn: column + 1 + anchor.length,
49
- }
50
- }
51
- return undefined
52
- }
53
-
54
- export function headOf(text: string): At {
55
- const first = text.split('\n')[0] ?? ''
56
- return { line: 1, column: 1, endLine: 1, endColumn: Math.max(first.length, 1) + 1 }
57
- }
58
-
59
- export function rangeOf(finding: Finding, text: string): At {
60
- if (finding.at !== undefined) return finding.at
61
- const anchor = finding.anchor ?? anchorIn(finding.detail)
62
- return (anchor === undefined ? undefined : locate(text, anchor)) ?? headOf(text)
63
- }
@@ -1,124 +0,0 @@
1
- import { started } from '../../integrations/cli.ts'
2
- import { existsSync } from '../../integrations/disk.ts'
3
- import { extname, join } from 'node:path'
4
-
5
- import { theRules } from '../lint.ts'
6
-
7
- import type { Finding } from './findings.ts'
8
-
9
- interface EslintMessage {
10
- readonly ruleId?: string | null
11
- readonly severity: number
12
- readonly message: string
13
- readonly line?: number
14
- readonly column?: number
15
- readonly endLine?: number
16
- readonly endColumn?: number
17
-
18
- readonly suggestions?: readonly { readonly desc?: string }[]
19
- }
20
-
21
- interface EslintFile {
22
- readonly messages?: readonly EslintMessage[]
23
- }
24
-
25
- const LINTABLE = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs', '.jsx'])
26
-
27
- export const isLintable = (relative: string): boolean => LINTABLE.has(extname(relative))
28
-
29
- export const LINT_CONFIG = 'eslint.config.js'
30
-
31
- export class LintUnavailableError extends Error {
32
- constructor(detail: string) {
33
- super(
34
- `ESLint could not be run over this repository: ${detail}. This server reports what the build ` +
35
- 'computes, so a lint pass that answered "no problems" because it never ran would be ' +
36
- 'indistinguishable from a clean file — which is the one thing it must never be.',
37
- )
38
- this.name = 'LintUnavailableError'
39
- }
40
- }
41
-
42
- function binaryIn(root: string): string | undefined {
43
- const local = join(root, 'node_modules', '.bin', 'eslint')
44
- return existsSync(local) ? local : undefined
45
- }
46
-
47
- async function ruleFindings(relative: string, text: string): Promise<readonly Finding[]> {
48
- const rules = theRules()
49
- return rules(relative, text).map((violation) => ({
50
- severity: 'error' as const,
51
- check: violation.rule,
52
-
53
- detail: violation.bans ?? violation.message,
54
- file: relative,
55
- at: { line: violation.line, column: violation.column },
56
- suggestions: [
57
- ...(violation.instead === undefined ? [] : [violation.instead]),
58
- ...(violation.right === undefined ? [] : [`Write: ${violation.right}`]),
59
- ],
60
- }))
61
- }
62
-
63
- const suggestionsOf = (message: EslintMessage): readonly string[] =>
64
- (message.suggestions ?? []).map((one) => one.desc).filter((desc): desc is string => desc !== undefined)
65
-
66
- export async function lintFindings(root: string, relative: string, text: string): Promise<readonly Finding[]> {
67
- if (!isLintable(relative)) return []
68
-
69
- if (!existsSync(join(root, LINT_CONFIG))) return await ruleFindings(relative, text)
70
-
71
- const binary = binaryIn(root)
72
- if (binary === undefined) {
73
- return [
74
- {
75
- severity: 'error',
76
- check: 'lint:unavailable',
77
- detail:
78
- `${LINT_CONFIG} is here and node_modules/.bin/eslint is not, so none of this repository's ` +
79
- 'rules are being checked while you type. Install the dev dependencies.',
80
- file: LINT_CONFIG,
81
- },
82
- ]
83
- }
84
-
85
- const report = await runEslint(binary, root, relative, text)
86
-
87
- return report.flatMap((file) =>
88
- (file.messages ?? []).map((message) => ({
89
- severity: message.severity === 2 ? ('error' as const) : ('warning' as const),
90
- check: message.ruleId ?? 'lint',
91
- detail: message.message,
92
- file: relative,
93
- at: {
94
- line: message.line ?? 1,
95
- column: message.column ?? 1,
96
- ...(message.endLine === undefined ? {} : { endLine: message.endLine }),
97
- ...(message.endColumn === undefined ? {} : { endColumn: message.endColumn }),
98
- },
99
- ...(suggestionsOf(message).length === 0 ? {} : { suggestions: suggestionsOf(message) }),
100
- })),
101
- )
102
- }
103
-
104
- async function runEslint(
105
- binary: string,
106
- root: string,
107
- relative: string,
108
- text: string,
109
- ): Promise<readonly EslintFile[]> {
110
- const went = await started(binary, ['--format', 'json', '--no-warn-ignored', '--stdin', '--stdin-filename', relative], {
111
- at: root,
112
- hears: text,
113
- })
114
- if (!went.there) throw new LintUnavailableError(went.why)
115
-
116
- let parsed: unknown
117
- try {
118
- parsed = JSON.parse(went.out)
119
- } catch {
120
- throw new LintUnavailableError(went.err.trim() === '' ? `it wrote ${went.out.length} bytes of non-JSON` : went.err.trim())
121
- }
122
- if (!Array.isArray(parsed)) throw new LintUnavailableError('its JSON report was not an array of files')
123
- return parsed as readonly EslintFile[]
124
- }
@@ -1,78 +0,0 @@
1
-
2
-
3
- export interface RpcMessage {
4
- readonly jsonrpc: '2.0'
5
- readonly id?: number | string | null
6
- readonly method?: string
7
- readonly params?: unknown
8
- readonly result?: unknown
9
- readonly error?: { readonly code: number; readonly message: string }
10
- }
11
-
12
- const HEADER_END = '\r\n\r\n'
13
-
14
- export const METHOD_NOT_FOUND = -32601
15
-
16
- export function encode(message: RpcMessage): Buffer {
17
- const body = Buffer.from(JSON.stringify(message), 'utf8')
18
- return Buffer.concat([Buffer.from(`Content-Length: ${body.length}${HEADER_END}`, 'ascii'), body])
19
- }
20
-
21
- export class MalformedMessageError extends Error {
22
- constructor(detail: string) {
23
- super(
24
- `the editor sent something this server cannot parse: ${detail}. ` +
25
- 'A language server that skipped it would answer the next request against a stream it no ' +
26
- 'longer understands, which reads as a file with no problems.',
27
- )
28
- this.name = 'MalformedMessageError'
29
- }
30
- }
31
-
32
- export class MessageBuffer {
33
- #pending: Buffer = Buffer.alloc(0)
34
-
35
- push(chunk: Buffer): readonly RpcMessage[] {
36
- this.#pending = Buffer.concat([this.#pending, chunk])
37
-
38
- const found: RpcMessage[] = []
39
- for (;;) {
40
- const split = this.#pending.indexOf(HEADER_END, 0, 'ascii')
41
- if (split === -1) return found
42
-
43
- const header = this.#pending.subarray(0, split).toString('ascii')
44
- const length = contentLength(header)
45
- const start = split + HEADER_END.length
46
-
47
- if (this.#pending.length < start + length) return found
48
-
49
- const body = this.#pending.subarray(start, start + length).toString('utf8')
50
- this.#pending = this.#pending.subarray(start + length)
51
-
52
- let parsed: unknown
53
- try {
54
- parsed = JSON.parse(body)
55
- } catch {
56
- throw new MalformedMessageError(`a ${length}-byte body that is not JSON`)
57
- }
58
- if (typeof parsed !== 'object' || parsed === null) {
59
- throw new MalformedMessageError(`a ${length}-byte body that is not a JSON object`)
60
- }
61
- found.push(parsed as RpcMessage)
62
- }
63
- }
64
- }
65
-
66
- function contentLength(header: string): number {
67
- for (const line of header.split('\r\n')) {
68
- const [name, value] = line.split(':', 2)
69
-
70
- if (name?.trim().toLowerCase() !== 'content-length') continue
71
- const length = Number.parseInt((value ?? '').trim(), 10)
72
- if (!Number.isInteger(length) || length < 0) {
73
- throw new MalformedMessageError(`Content-Length: ${(value ?? '').trim()}`)
74
- }
75
- return length
76
- }
77
- throw new MalformedMessageError(`a header block with no Content-Length: ${JSON.stringify(header)}`)
78
- }