@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.
- package/dist/core/actor.ts +20 -0
- package/dist/core/asking.d.ts +0 -1
- package/dist/core/asking.js +0 -1
- package/dist/core/asking.ts +1342 -0
- package/dist/core/attach.js +2 -2
- package/dist/core/attach.ts +585 -0
- package/dist/core/chain.js +8 -1
- package/dist/core/chain.ts +148 -0
- package/dist/core/cli.js +97 -18
- package/dist/core/cli.ts +2004 -0
- package/dist/core/condition.ts +227 -0
- package/dist/core/descend.d.ts +1 -0
- package/dist/core/descend.js +36 -0
- package/dist/core/descend.ts +1001 -0
- package/dist/core/finding.ts +38 -0
- package/dist/core/generators/meta.js +6 -0
- package/dist/core/generators/meta.ts +1006 -0
- package/dist/core/init.ts +170 -0
- package/dist/core/invoked.ts +47 -0
- package/dist/core/round.ts +57 -0
- package/dist/core/rows.ts +369 -0
- package/dist/core/scope.ts +18 -0
- package/dist/core/thread.ts +29 -0
- package/dist/core/tree.ts +67 -0
- package/dist/core/types.ts +350 -0
- package/dist/derivation/attach.js +1 -1
- package/dist/derivation/attach.ts +68 -0
- package/dist/derivation/generators/build.ts +232 -0
- package/dist/derivation/generators/check.ts +118 -0
- package/dist/derivation/generators/derivation.ts +347 -0
- package/dist/derivation/generators/determinism.ts +145 -0
- package/dist/derivation/generators/release.js +19 -9
- package/dist/derivation/generators/release.ts +213 -0
- package/dist/derivation/makers/release.ts +135 -0
- package/dist/derivation/release.ts +23 -0
- package/dist/integrations/attach.js +2 -2
- package/dist/integrations/attach.ts +207 -0
- package/dist/integrations/generators/read.ts +127 -0
- package/dist/integrations/generators/secret.ts +289 -0
- package/dist/integrations/generators/target.js +16 -6
- package/dist/integrations/generators/target.ts +944 -0
- package/dist/integrations/generators/vault.js +46 -14
- package/dist/integrations/generators/vault.ts +212 -0
- package/dist/integrations/{sdk.js → module.js} +4 -4
- package/dist/integrations/module.ts +189 -0
- package/dist/integrations/secret.ts +50 -0
- package/dist/integrations/target.d.ts +3 -1
- package/dist/integrations/target.js +14 -11
- package/dist/integrations/target.ts +340 -0
- package/dist/integrations/vault.d.ts +2 -0
- package/dist/integrations/vault.js +4 -2
- package/dist/integrations/vault.ts +136 -0
- package/dist/integrity/attach.ts +234 -0
- package/dist/integrity/check-threads.js +2 -26
- package/dist/integrity/check-threads.ts +166 -0
- package/dist/integrity/constraint.ts +1202 -0
- package/dist/integrity/generators/compiler.ts +161 -0
- package/dist/integrity/generators/gate.ts +201 -0
- package/dist/integrity/generators/issues.ts +160 -0
- package/dist/integrity/generators/publish.js +14 -5
- package/dist/integrity/generators/publish.ts +247 -0
- package/dist/integrity/generators/rule.ts +271 -0
- package/dist/integrity/generators/seal.ts +117 -0
- package/dist/integrity/generators/trial.ts +320 -0
- package/dist/integrity/lint.ts +227 -0
- package/dist/integrity/lsp/bin.ts +42 -0
- package/dist/integrity/lsp/diagnostics.ts +92 -0
- package/dist/integrity/lsp/findings.ts +63 -0
- package/dist/integrity/lsp/lint.ts +140 -0
- package/dist/integrity/lsp/protocol.ts +78 -0
- package/dist/integrity/lsp/server.ts +224 -0
- package/dist/integrity/sealed.js +2 -2
- package/dist/integrity/sealed.ts +56 -0
- package/dist/integrity/slopnator.ts +350 -0
- package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
- package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
- package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
- package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
- package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
- package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
- package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
- package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
- package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
- package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
- package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
- package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
- package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
- package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
- package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
- package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
- package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
- package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
- package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
- package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
- package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
- package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
- package/dist/integrity/sweep.ts +307 -0
- package/dist/knowledge/attach.ts +350 -0
- package/dist/knowledge/generators/docsgen.ts +174 -0
- package/dist/knowledge/generators/glossary.ts +73 -0
- package/dist/knowledge/generators/skills.ts +263 -0
- package/dist/knowledge/glossary.ts +6 -0
- package/dist/operations/access.ts +88 -0
- package/dist/operations/attach.ts +475 -0
- package/dist/operations/capabilities.ts +65 -0
- package/dist/operations/cost.ts +58 -0
- package/dist/operations/dispatch.ts +348 -0
- package/dist/operations/error-results.ts +31 -0
- package/dist/operations/errors/catalog.ts +168 -0
- package/dist/operations/errors/classes.ts +51 -0
- package/dist/operations/errors/errors.ts +314 -0
- package/dist/operations/generators/capability.js +29 -19
- package/dist/operations/generators/capability.ts +704 -0
- package/dist/operations/generators/compile.js +20 -1
- package/dist/operations/generators/compile.ts +643 -0
- package/dist/operations/generators/grant.ts +336 -0
- package/dist/operations/generators/operation.ts +972 -0
- package/dist/operations/generators/permits.ts +223 -0
- package/dist/operations/generators/seed.ts +347 -0
- package/dist/operations/generators/trial.js +4 -1
- package/dist/operations/generators/trial.ts +529 -0
- package/dist/operations/http.ts +164 -0
- package/dist/operations/makers/capability.ts +166 -0
- package/dist/operations/makers/trial.ts +520 -0
- package/dist/operations/memory.ts +470 -0
- package/dist/operations/metering.ts +328 -0
- package/dist/operations/nodes.ts +54 -0
- package/dist/operations/operations.ts +32 -0
- package/dist/operations/queue-policy.ts +10 -0
- package/dist/operations/result.ts +28 -0
- package/dist/operations/seams.d.ts +4 -4
- package/dist/operations/seams.ts +364 -0
- package/dist/operations/store.ts +29 -0
- package/package.json +8 -1
- package/dist/integrations/generators/sdk.d.ts +0 -1
- package/dist/integrations/generators/sdk.js +0 -148
- /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
|
+
}
|