@prestack/thread 0.1.21 → 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.
- package/CHANGELOG.md +109 -0
- package/dist/core/attach.js +21 -0
- package/dist/core/descend.js +6 -3
- package/dist/core/generators/commit.js +55 -3
- package/dist/core/generators/dev.d.ts +1 -0
- package/dist/core/generators/dev.js +68 -0
- package/dist/core/tree.d.ts +1 -0
- package/dist/core/tree.js +7 -1
- package/dist/derivation/generators/build.d.ts +3 -1
- package/dist/derivation/generators/build.js +103 -22
- package/dist/derivation/generators/release.js +74 -26
- package/dist/derivation/release.js +3 -3
- package/dist/integrations/cli.js +69 -10
- package/dist/integrations/pglite.js +13 -3
- package/dist/integrations/target.d.ts +3 -0
- package/dist/integrity/attach.js +1 -5
- package/dist/integrity/slopnators/sl_stale_path.js +10 -0
- package/dist/operations/access.js +3 -2
- package/dist/operations/capabilities.d.ts +2 -0
- package/dist/operations/capabilities.js +2 -0
- package/dist/operations/generators/capability.js +234 -24
- package/dist/operations/generators/compile.js +150 -50
- package/dist/operations/generators/seed.d.ts +3 -0
- package/dist/operations/generators/seed.js +25 -3
- package/dist/operations/nodes.d.ts +2 -2
- package/package.json +1 -1
- package/dist/core/actor.ts +0 -21
- package/dist/core/attach.ts +0 -613
- package/dist/core/chain.ts +0 -145
- package/dist/core/condition.ts +0 -227
- package/dist/core/descend.ts +0 -1185
- package/dist/core/errors/catalogue.ts +0 -314
- package/dist/core/errors/classes.ts +0 -51
- package/dist/core/errors/error.ts +0 -152
- package/dist/core/errors/namespaces.ts +0 -19
- package/dist/core/finding.ts +0 -37
- package/dist/core/generators/commit.ts +0 -157
- package/dist/core/init.ts +0 -222
- package/dist/core/invoked.ts +0 -52
- package/dist/core/round.ts +0 -74
- package/dist/core/rows.ts +0 -506
- package/dist/core/scope.ts +0 -20
- package/dist/core/thread.ts +0 -29
- package/dist/core/tree.ts +0 -90
- package/dist/core/types.ts +0 -351
- package/dist/derivation/attach.ts +0 -57
- package/dist/derivation/generators/build.ts +0 -371
- package/dist/derivation/generators/check.ts +0 -118
- package/dist/derivation/generators/determinism.ts +0 -145
- package/dist/derivation/generators/release.ts +0 -236
- package/dist/derivation/release.ts +0 -43
- package/dist/integrations/attach.ts +0 -187
- package/dist/integrations/cli.ts +0 -606
- package/dist/integrations/declared.ts +0 -34
- package/dist/integrations/disk.ts +0 -466
- package/dist/integrations/generators/read.ts +0 -127
- package/dist/integrations/generators/vault.ts +0 -210
- package/dist/integrations/https.ts +0 -216
- package/dist/integrations/pglite.ts +0 -206
- package/dist/integrations/secret.ts +0 -54
- package/dist/integrations/target.ts +0 -672
- package/dist/integrations/vault.ts +0 -148
- package/dist/integrity/attach.ts +0 -150
- package/dist/integrity/check-threads.ts +0 -166
- package/dist/integrity/constraint.ts +0 -1246
- package/dist/integrity/generators/compiler.ts +0 -138
- package/dist/integrity/generators/gate.ts +0 -227
- package/dist/integrity/generators/issues.ts +0 -160
- package/dist/integrity/generators/publish.ts +0 -233
- package/dist/integrity/generators/trial.ts +0 -359
- package/dist/integrity/lint.ts +0 -214
- package/dist/integrity/lsp/bin.ts +0 -44
- package/dist/integrity/lsp/diagnostics.ts +0 -92
- package/dist/integrity/lsp/findings.ts +0 -63
- package/dist/integrity/lsp/lint.ts +0 -124
- package/dist/integrity/lsp/protocol.ts +0 -78
- package/dist/integrity/lsp/server.ts +0 -224
- package/dist/integrity/slopnator.ts +0 -324
- package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
- package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
- package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
- package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
- package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
- package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
- package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
- package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
- package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
- package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
- package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
- package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
- package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
- package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
- package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
- package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
- package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
- package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
- package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
- package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
- package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
- package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
- package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
- package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
- package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
- package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
- package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
- package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
- package/dist/integrity/sweep.ts +0 -395
- package/dist/knowledge/attach.ts +0 -327
- package/dist/knowledge/generators/docsgen.ts +0 -186
- package/dist/knowledge/generators/glossary.ts +0 -73
- package/dist/knowledge/glossary.ts +0 -6
- package/dist/operations/access.ts +0 -131
- package/dist/operations/attach.ts +0 -408
- package/dist/operations/capabilities.ts +0 -88
- package/dist/operations/dispatch.ts +0 -294
- package/dist/operations/error-results.ts +0 -31
- package/dist/operations/generators/capability.ts +0 -1054
- package/dist/operations/generators/compile.ts +0 -701
- package/dist/operations/generators/permits.ts +0 -229
- package/dist/operations/generators/seed.ts +0 -359
- package/dist/operations/generators/trial.ts +0 -666
- package/dist/operations/nodes.ts +0 -53
- package/dist/operations/operations.ts +0 -91
- package/dist/operations/result.ts +0 -28
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import { starts } from '../../integrations/cli.ts'
|
|
2
|
-
import { calls } from '../../integrations/declared.ts'
|
|
3
|
-
import { existsSync, readFileSync } from '../../integrations/disk.ts'
|
|
4
|
-
import { valuesIn } from '../../core/rows.ts'
|
|
5
|
-
import { join } from 'node:path'
|
|
6
|
-
import { argv, env, execPath, stderr, stdout } from 'node:process'
|
|
7
|
-
import { CLI, exit } from '../../core/invoked.ts'
|
|
8
|
-
import { THING as TYPED, named } from '../../core/scope.ts'
|
|
9
|
-
import { findTree, inTree } from '../../core/tree.ts'
|
|
10
|
-
|
|
11
|
-
import type { Secret } from '../../integrations/secret.ts'
|
|
12
|
-
import { placedAt, type Target } from '../../integrations/target.ts'
|
|
13
|
-
import { opened, sealedIn } from '../../integrations/vault.ts'
|
|
14
|
-
|
|
15
|
-
const TREE = findTree()
|
|
16
|
-
const ROOT = TREE.at
|
|
17
|
-
|
|
18
|
-
const STEP_TIMEOUT = 900_000
|
|
19
|
-
|
|
20
|
-
const REGISTRY = 'ht_registry'
|
|
21
|
-
|
|
22
|
-
interface Check {
|
|
23
|
-
readonly id: string
|
|
24
|
-
readonly runs: string
|
|
25
|
-
readonly because: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const CHECKS: readonly Check[] = [
|
|
29
|
-
{
|
|
30
|
-
id: 'gate',
|
|
31
|
-
runs: 'thread gate',
|
|
32
|
-
because: 'every step this tree declares, in the order it declares them, which is where the descent and the sweep are',
|
|
33
|
-
},
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
const PREFIX = /^(?:npx|pnpm)$/
|
|
37
|
-
|
|
38
|
-
const words = (said: string): readonly string[] => said.split(/\s+/).filter((one) => one !== '')
|
|
39
|
-
|
|
40
|
-
function run(given: readonly string[], extra?: Record<string, string>): number {
|
|
41
|
-
const mine = given[0] === 'thread' ? 1 : PREFIX.test(given[0] ?? '') && given[1] === 'thread' ? 2 : 0
|
|
42
|
-
const how = { at: ROOT, shows: true, within: STEP_TIMEOUT, holds: extra === undefined ? env : { ...env, ...extra } }
|
|
43
|
-
const went = mine === 0 ? starts(given[0] ?? '', given.slice(1), how) : starts(execPath, [...CLI, ...given.slice(mine)], how)
|
|
44
|
-
return went.there ? went.code : 1
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function refuse(id: string, code: number, because: string, runs: string): never {
|
|
48
|
-
stderr.write(`\npublish: REFUSED — \`${id}\` exited ${String(code)}. NOTHING was published.\n`)
|
|
49
|
-
stderr.write(`publish: ${because}.\n`)
|
|
50
|
-
stderr.write(`publish: what refused is above, in that command's own words — this does not restate it, because a\n`)
|
|
51
|
-
stderr.write(`publish: second copy of a refusal drifts from the first. Run \`${runs}\` alone to work on it.\n`)
|
|
52
|
-
exit(1)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const given = argv.indexOf('--registry')
|
|
56
|
-
|
|
57
|
-
if (given !== -1) {
|
|
58
|
-
stderr.write('thread publish takes no url. Where the packages go is the address on the ht_registry row in _threads/targets.ts.\n')
|
|
59
|
-
stderr.write('\nit builds, then runs every check, then publishes. A check that fails publishes nothing.\n')
|
|
60
|
-
exit(1)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const TOKEN = 'sec_registry_token'
|
|
64
|
-
|
|
65
|
-
const PRODUCTION = 'production'
|
|
66
|
-
|
|
67
|
-
const secrets = inTree(TREE, 'secrets.ts')
|
|
68
|
-
|
|
69
|
-
const declared = existsSync(secrets)
|
|
70
|
-
? ((valuesIn(readFileSync(secrets)) as { SECRETS?: readonly Secret[] }).SECRETS ?? [])
|
|
71
|
-
: []
|
|
72
|
-
|
|
73
|
-
const vaulted = declared.some((one) => one.id === TOKEN)
|
|
74
|
-
|
|
75
|
-
const targets = inTree(TREE, 'targets.ts')
|
|
76
|
-
|
|
77
|
-
const registry = placedAt(
|
|
78
|
-
existsSync(targets) ? ((valuesIn(readFileSync(targets)) as { TARGETS?: readonly Target[] }).TARGETS ?? []) : [],
|
|
79
|
-
REGISTRY,
|
|
80
|
-
).replace(/[/]$/, '')
|
|
81
|
-
|
|
82
|
-
if (registry === '') {
|
|
83
|
-
stderr.write('\npublish: no target row says where the registry is, so there is nowhere for these packages to go. Give ' + REGISTRY + ' an address in _threads/targets.ts, the absolute url of the registry. NOTHING was published.\n')
|
|
84
|
-
exit(1)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (vaulted && !sealedIn(ROOT, PRODUCTION).includes(TOKEN)) {
|
|
88
|
-
stderr.write(`\npublish: ${TOKEN} is declared in _threads/secrets.ts and has no value sealed for ${PRODUCTION}, so nothing was checked and NOTHING was published.\n`)
|
|
89
|
-
stderr.write(`publish: run \`thread create secret set ${TOKEN}\` at a terminal and paste the registry token, or take the row out to publish with the login this machine holds.\n`)
|
|
90
|
-
exit(1)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const setting = (): string => {
|
|
94
|
-
const at = new URL(registry)
|
|
95
|
-
return `npm_config_//${at.host}${at.pathname.endsWith('/') ? at.pathname : `${at.pathname}/`}:_authToken`
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function tokened(): Promise<string> {
|
|
99
|
-
const values = await opened(ROOT, declared.map((one) => one.id), PRODUCTION)([TOKEN]).catch((thrown: unknown) => {
|
|
100
|
-
stderr.write(`\npublish: ${TOKEN} is sealed here and would not open: ${thrown instanceof Error ? thrown.message : String(thrown)}. NOTHING was published.\n`)
|
|
101
|
-
exit(1)
|
|
102
|
-
})
|
|
103
|
-
const token = values[TOKEN]
|
|
104
|
-
if (token === undefined) {
|
|
105
|
-
stderr.write(`\npublish: the vault opened and answered nothing for ${TOKEN}. NOTHING was published.\n`)
|
|
106
|
-
exit(1)
|
|
107
|
-
}
|
|
108
|
-
return token
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const signedAs = env['GITHUB_ACTIONS'] === 'true' ? 'the identity npm trusts this GitHub Actions run under' : 'the login this machine already holds'
|
|
112
|
-
|
|
113
|
-
stdout.write(
|
|
114
|
-
vaulted
|
|
115
|
-
? `publish: ${TOKEN} is declared and sealed for ${PRODUCTION}. It is opened only after every check passes, and handed to pnpm as its auth setting for ${registry}.\n`
|
|
116
|
-
: `publish: no row declares ${TOKEN}, so this publish uses ${signedAs}. Declare it with \`thread create secret\` to publish with a token sealed in this repository instead.\n`,
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
stdout.write('publish: deriving and building what the packaging rows state first, so every check below reads the files this release ships rather than the source they were written from.\n')
|
|
120
|
-
|
|
121
|
-
const made = run(words(`${TYPED} packaged --build`))
|
|
122
|
-
stdout.write(`publish: packaged exited ${String(made)}\n`)
|
|
123
|
-
if (made !== 0) {
|
|
124
|
-
refuse(
|
|
125
|
-
'packaged',
|
|
126
|
-
made,
|
|
127
|
-
'a package whose manifest and release note are not what its rows state would go out describing itself as something nobody declared, and one that did not compile would go out promising files it does not have',
|
|
128
|
-
`${TYPED} packaged --build`,
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
stdout.write(`\npublish: ${String(CHECKS.length)} check(s) stand between this tree and ${registry}. Any one of them stops it.\n`)
|
|
133
|
-
for (const check of CHECKS) stdout.write(` ${check.id} — ${check.runs}\n`)
|
|
134
|
-
|
|
135
|
-
for (const [index, check] of CHECKS.entries()) {
|
|
136
|
-
const at = `check ${String(index + 1)} of ${String(CHECKS.length)}`
|
|
137
|
-
stdout.write(`\npublish: ${at} — ${check.runs} (${check.id})\n`)
|
|
138
|
-
|
|
139
|
-
const code = run(words(check.runs))
|
|
140
|
-
stdout.write(`publish: ${at} — exited ${String(code)}\n`)
|
|
141
|
-
if (code !== 0) refuse(check.id, code, check.because, check.runs)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const numbered = run(words(`${TYPED} packaged --release`))
|
|
145
|
-
stdout.write(`publish: packaged --release exited ${String(numbered)}\n`)
|
|
146
|
-
if (numbered !== 0) {
|
|
147
|
-
refuse(
|
|
148
|
-
'packaged',
|
|
149
|
-
numbered,
|
|
150
|
-
'the next release number is written onto the manifests only once every check has passed, and writing it refused, so nothing here carries a number to go out under',
|
|
151
|
-
`${TYPED} packaged --release`,
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const handed = vaulted ? { [setting()]: await tokened() } : undefined
|
|
156
|
-
|
|
157
|
-
stdout.write(`\npublish: every check passed. Publishing to ${registry} with ${vaulted ? `${TOKEN} from this repository's vault` : signedAs}.\n`)
|
|
158
|
-
|
|
159
|
-
const sent = run(words(`pnpm -r publish --registry ${registry} --no-git-checks --access public`), handed)
|
|
160
|
-
|
|
161
|
-
if (sent !== 0) {
|
|
162
|
-
stderr.write(`\npublish: the checks passed and the publish itself exited ${String(sent)}.\n`)
|
|
163
|
-
const back = run(words(`${TYPED} packaged`))
|
|
164
|
-
stderr.write(
|
|
165
|
-
back === 0
|
|
166
|
-
? 'publish: the manifests carry the last recorded release again and no note was written, so the next `thread publish` numbers this release once more.\n'
|
|
167
|
-
: `publish: the manifests still carry the number that did not go out. Run \`${TYPED} packaged\` to put the last recorded release back.\n`,
|
|
168
|
-
)
|
|
169
|
-
exit(1)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const recorded = run(words(`${TYPED} packaged --record`))
|
|
173
|
-
stdout.write(`publish: packaged --record exited ${String(recorded)}\n`)
|
|
174
|
-
if (recorded !== 0) {
|
|
175
|
-
stderr.write(`\npublish: the packages went out and the note recording them refused to be written, so the next publish would number this release again. Run \`${TYPED} packaged --record\` and read what it says.\n`)
|
|
176
|
-
exit(1)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const SHEET = 'packaging.ts'
|
|
180
|
-
|
|
181
|
-
interface Packaged {
|
|
182
|
-
readonly id?: string
|
|
183
|
-
readonly folder?: string
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const sheet = inTree(TREE, SHEET)
|
|
187
|
-
|
|
188
|
-
const stated = existsSync(sheet)
|
|
189
|
-
? (valuesIn(readFileSync(sheet)) as { PACKAGES?: readonly Packaged[] })
|
|
190
|
-
: {}
|
|
191
|
-
|
|
192
|
-
const shipped = stated.PACKAGES ?? []
|
|
193
|
-
|
|
194
|
-
function versionAt(folder: string): string {
|
|
195
|
-
const at = join(ROOT, folder, 'package.json')
|
|
196
|
-
if (!existsSync(at)) return 'no manifest'
|
|
197
|
-
try {
|
|
198
|
-
return (JSON.parse(readFileSync(at, 'utf8')) as { version?: string }).version ?? 'no version'
|
|
199
|
-
} catch {
|
|
200
|
-
return 'no version'
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const git = calls('cli_git', ROOT)
|
|
205
|
-
|
|
206
|
-
const asked = async (what: readonly string[]): Promise<string> => (await git.rev_parse(what)).out.trim()
|
|
207
|
-
|
|
208
|
-
if ((await asked(['--is-inside-work-tree'])) !== 'true') {
|
|
209
|
-
stdout.write("\npublish: it's not a repo, so nothing was committed.\n")
|
|
210
|
-
} else if (shipped.length === 0) {
|
|
211
|
-
stdout.write('\npublish: no packaging row names a folder, so there is nothing derived here to commit.\n')
|
|
212
|
-
} else {
|
|
213
|
-
const going = shipped.map((one) => `${named(one.id ?? '')} ${versionAt(one.folder ?? '')}`).join(', ')
|
|
214
|
-
const branch = await asked(['--abbrev-ref', 'HEAD'])
|
|
215
|
-
|
|
216
|
-
for (const one of shipped) await git.stage([one.folder ?? ''], true, STEP_TIMEOUT)
|
|
217
|
-
|
|
218
|
-
if ((await git.diff(['--cached', '--quiet'])).ok) {
|
|
219
|
-
stdout.write(`\npublish: ${going} — what is on disk is already what is committed, so this release added no artifact.\n`)
|
|
220
|
-
} else {
|
|
221
|
-
const wrote = (await git.commit(['-m', `thread publish — ${going}`], true, STEP_TIMEOUT)).code
|
|
222
|
-
stdout.write(`publish: commit exited ${String(wrote)}\n`)
|
|
223
|
-
if (wrote !== 0) {
|
|
224
|
-
stderr.write('\npublish: the packages went out and the files that describe them are not committed, so this release has no record in git.\n')
|
|
225
|
-
stderr.write('publish: commit them by hand — everything is staged.\n')
|
|
226
|
-
exit(1)
|
|
227
|
-
}
|
|
228
|
-
stdout.write(`publish: committed the derived artifacts to ${branch} — ${going}.\n`)
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
stdout.write(`\npublish: done. What went out was derived, then built, then passed the gate, in that order. The compile inside \`thread packaged --build\` is the typecheck.\n`)
|
|
233
|
-
exit(0)
|
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
import { answeredBy, standing } from '../../integrations/disk.ts'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
3
|
-
|
|
4
|
-
import type { Linter as LinterType, Rule } from 'eslint'
|
|
5
|
-
import { Linter } from 'eslint'
|
|
6
|
-
|
|
7
|
-
import type { Constraint, Proof, Sourced } from '../constraint.ts'
|
|
8
|
-
|
|
9
|
-
const TREE = findTree()
|
|
10
|
-
import { NOT_WRITTEN, ROOT, governs, proofsOf, reads, slopnatorName, sweeps, visits, type Slopnator } from '../slopnator.ts'
|
|
11
|
-
import { forget } from '../slopnators/sl_unthreaded.ts'
|
|
12
|
-
import { findTree } from '../../core/tree.ts'
|
|
13
|
-
|
|
14
|
-
const PROBE = 'probe.tsx'
|
|
15
|
-
|
|
16
|
-
const WRAPPINGS: readonly ((source: string) => string)[] = [
|
|
17
|
-
(source) => source,
|
|
18
|
-
(source) => `const _probe = { ${source} }`,
|
|
19
|
-
(source) => `const _probe = <Probe ${source} />`,
|
|
20
|
-
(source) => `const _probe = () => { ${source} }`,
|
|
21
|
-
(source) => `const _probe = <Probe>{${source}}</Probe>`,
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
export interface Scenario {
|
|
25
|
-
readonly files: readonly Sourced[]
|
|
26
|
-
readonly at: string
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface Trial {
|
|
30
|
-
readonly id: string
|
|
31
|
-
readonly rule: string
|
|
32
|
-
readonly name: string
|
|
33
|
-
readonly scenario: Scenario
|
|
34
|
-
readonly verdict: { readonly reports: boolean }
|
|
35
|
-
readonly requires: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface Derivation {
|
|
39
|
-
readonly trials: readonly Trial[]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface Outcome {
|
|
43
|
-
readonly id: string
|
|
44
|
-
readonly rule: string
|
|
45
|
-
readonly passed: boolean
|
|
46
|
-
readonly failures: readonly string[]
|
|
47
|
-
readonly parsed: boolean
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface Run {
|
|
51
|
-
readonly outcomes: readonly Outcome[]
|
|
52
|
-
readonly passed: number
|
|
53
|
-
readonly failed: number
|
|
54
|
-
readonly measured: number
|
|
55
|
-
readonly spent: ReadonlyMap<string, number>
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const canonical = (at: string): string => at.replace(/^threads\//, `${TREE.name}/`)
|
|
59
|
-
|
|
60
|
-
export function probeAt(rule: Constraint): string {
|
|
61
|
-
const path = canonical(rule.where.paths[0] ?? 'src/')
|
|
62
|
-
if (path === '.') return PROBE
|
|
63
|
-
return path.endsWith('/') ? `${path}${PROBE}` : `${path}/${PROBE}`
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function requiresOf(rule: Constraint, reports: boolean): string {
|
|
67
|
-
return reports
|
|
68
|
-
? `${rule.id} reports on this source, at a path it governs`
|
|
69
|
-
: `${rule.id} stays silent on this source — the fix it recommends is not itself refused`
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function scenarioOf(rule: Constraint, proof: Proof): Scenario {
|
|
73
|
-
if (typeof proof !== 'string') {
|
|
74
|
-
return { at: canonical(proof.at), files: proof.files.map((one) => ({ ...one, at: canonical(one.at) })) }
|
|
75
|
-
}
|
|
76
|
-
const path = probeAt(rule)
|
|
77
|
-
return { files: [{ at: path, source: proof }], at: path }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function spoken(scenario: Scenario): string {
|
|
81
|
-
const target = scenario.files.find((file) => file.at === scenario.at) ?? scenario.files[0]
|
|
82
|
-
const rest = scenario.files.length - 1
|
|
83
|
-
return `${target?.source ?? ''}${rest > 0 ? ` (beside ${String(rest)} more file(s))` : ''}`
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function deriveTrials(all: readonly Constraint[], slopnators: ReadonlyMap<string, Slopnator>): Derivation {
|
|
87
|
-
const trials: Trial[] = []
|
|
88
|
-
|
|
89
|
-
for (const rule of all) {
|
|
90
|
-
const proofs = proofsOf(rule, slopnators)
|
|
91
|
-
const put = (proof: Proof, index: number, reports: boolean): void => {
|
|
92
|
-
const scenario = scenarioOf(rule, proof)
|
|
93
|
-
trials.push({
|
|
94
|
-
id: `trl_rule_${rule.id.replace(/-/g, '_')}_${reports ? 'refuses' : 'accepts'}_${String(index)}`,
|
|
95
|
-
rule: rule.id,
|
|
96
|
-
name: `${rule.id} ${reports ? 'refuses' : 'accepts'}: ${spoken(scenario)}`,
|
|
97
|
-
scenario,
|
|
98
|
-
verdict: { reports },
|
|
99
|
-
requires: requiresOf(rule, reports),
|
|
100
|
-
})
|
|
101
|
-
}
|
|
102
|
-
proofs.wrong.forEach((proof, index) => { put(proof, index, true) })
|
|
103
|
-
proofs.right.forEach((proof, index) => { put(proof, index, false) })
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return { trials: trials.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function config(rule: Constraint, module: Rule.RuleModule, parser: unknown): Parameters<LinterType['verify']>[1] {
|
|
110
|
-
return [
|
|
111
|
-
{
|
|
112
|
-
files: ['**/*.ts', '**/*.tsx'],
|
|
113
|
-
languageOptions: {
|
|
114
|
-
parser,
|
|
115
|
-
parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true } },
|
|
116
|
-
},
|
|
117
|
-
plugins: { rule: { rules: { [rule.id]: module } } },
|
|
118
|
-
rules: { [`rule/${rule.id}`]: 'error' },
|
|
119
|
-
},
|
|
120
|
-
] as unknown as Parameters<LinterType['verify']>[1]
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function runTrial(trial: Trial, rule: Constraint, module: Rule.RuleModule, parser: unknown, slopnator?: Slopnator): Outcome {
|
|
124
|
-
const linter = new Linter()
|
|
125
|
-
const failures: string[] = []
|
|
126
|
-
|
|
127
|
-
if (!governs(rule, trial.scenario.at)) {
|
|
128
|
-
return {
|
|
129
|
-
id: trial.id,
|
|
130
|
-
rule: rule.id,
|
|
131
|
-
passed: false,
|
|
132
|
-
parsed: false,
|
|
133
|
-
failures: [`the trial's own path "${trial.scenario.at}" is not one the rule governs, so nothing could have run`],
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const target = trial.scenario.files.find((file) => file.at === trial.scenario.at)
|
|
138
|
-
|
|
139
|
-
let parsed = false
|
|
140
|
-
let satisfied = false
|
|
141
|
-
|
|
142
|
-
answeredBy(standing(ROOT, new Map<string, string>(trial.scenario.files.map((one) => [resolve(ROOT, one.at), one.source]))))
|
|
143
|
-
forget()
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
if (slopnator !== undefined && !reads(slopnator)) {
|
|
147
|
-
parsed = true
|
|
148
|
-
satisfied = slopnator.checks(ROOT).length > 0 === trial.verdict.reports
|
|
149
|
-
} else if (target === undefined) {
|
|
150
|
-
failures.push(`the scenario writes no file at "${trial.scenario.at}", the path it lints, so nothing could have been measured`)
|
|
151
|
-
} else if (slopnator !== undefined && sweeps(slopnator)) {
|
|
152
|
-
const held = slopnator.holds(undefined, {
|
|
153
|
-
filename: trial.scenario.at,
|
|
154
|
-
text: target.source,
|
|
155
|
-
ancestors: () => [],
|
|
156
|
-
comments: () => [],
|
|
157
|
-
})
|
|
158
|
-
parsed = true
|
|
159
|
-
satisfied = (held !== false && held !== NOT_WRITTEN) === trial.verdict.reports
|
|
160
|
-
} else {
|
|
161
|
-
for (const wrap of WRAPPINGS) {
|
|
162
|
-
const text = wrap(target.source)
|
|
163
|
-
const messages = linter.verify(text, config(rule, module, parser), trial.scenario.at)
|
|
164
|
-
if (messages.some((message) => message.fatal === true)) continue
|
|
165
|
-
parsed = true
|
|
166
|
-
satisfied = messages.some((message) => message.ruleId === `rule/${rule.id}`) === trial.verdict.reports
|
|
167
|
-
break
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
} catch (thrown) {
|
|
171
|
-
failures.push(
|
|
172
|
-
`this trial could not run: ${thrown instanceof Error ? thrown.message : String(thrown)} — its scenario does not describe a world this rule can be measured against`,
|
|
173
|
-
)
|
|
174
|
-
} finally {
|
|
175
|
-
answeredBy(undefined)
|
|
176
|
-
forget()
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (failures.length === 0 && !parsed) {
|
|
180
|
-
failures.push('no wrapping of this source parsed, so nothing was measured — a trial that cannot run is not a trial that passed')
|
|
181
|
-
} else if (failures.length === 0 && !satisfied) {
|
|
182
|
-
failures.push(
|
|
183
|
-
trial.verdict.reports
|
|
184
|
-
? `expected ${rule.id} to report on this source and it stayed silent — the rule documents a form its own slopnator does not refuse`
|
|
185
|
-
: `expected ${rule.id} to stay silent on this source and it reported — the rule refuses the fix it recommends`,
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return { id: trial.id, rule: rule.id, passed: failures.length === 0, parsed, failures }
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const judged = (who: readonly Constraint[], rules: Readonly<Record<string, Rule.RuleModule>>, parser: unknown): Parameters<LinterType['verify']>[1] =>
|
|
193
|
-
[
|
|
194
|
-
{
|
|
195
|
-
files: ['**/*.ts', '**/*.tsx'],
|
|
196
|
-
languageOptions: {
|
|
197
|
-
parser,
|
|
198
|
-
parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true } },
|
|
199
|
-
},
|
|
200
|
-
plugins: { rule: { rules: Object.fromEntries(who.map((one) => [one.id, rules[one.id] as Rule.RuleModule])) } },
|
|
201
|
-
rules: Object.fromEntries(who.map((one) => ['rule/' + one.id, 'error'])),
|
|
202
|
-
},
|
|
203
|
-
] as unknown as Parameters<LinterType['verify']>[1]
|
|
204
|
-
|
|
205
|
-
const failed = (thrown: unknown): string => 'could not run on it: ' + (thrown instanceof Error ? thrown.message : String(thrown))
|
|
206
|
-
|
|
207
|
-
const parseable = (source: string, at: string, parser: unknown): ((said: string) => string) =>
|
|
208
|
-
WRAPPINGS.find((wrap) => !new Linter().verify(wrap(source), judged([], {}, parser), at).some((one) => one.fatal === true)) ??
|
|
209
|
-
((said: string): string => said)
|
|
210
|
-
|
|
211
|
-
function refusals(
|
|
212
|
-
scenario: Scenario,
|
|
213
|
-
source: string,
|
|
214
|
-
judges: readonly Constraint[],
|
|
215
|
-
rules: Readonly<Record<string, Rule.RuleModule>>,
|
|
216
|
-
parser: unknown,
|
|
217
|
-
slopnators: ReadonlyMap<string, Slopnator>,
|
|
218
|
-
): ReadonlyMap<string, ReadonlySet<string>> {
|
|
219
|
-
const found = new Map<string, Set<string>>()
|
|
220
|
-
const note = (who: string, said: string): void => {
|
|
221
|
-
const held = found.get(who) ?? new Set<string>()
|
|
222
|
-
held.add(said)
|
|
223
|
-
found.set(who, held)
|
|
224
|
-
}
|
|
225
|
-
const walking = judges.filter((one) => {
|
|
226
|
-
const held = slopnators.get(slopnatorName(one.id))
|
|
227
|
-
return held !== undefined && visits(held) && rules[one.id] !== undefined
|
|
228
|
-
})
|
|
229
|
-
const linted = (who: readonly Constraint[]): void => {
|
|
230
|
-
const messages = new Linter().verify(source, judged(who, rules, parser), scenario.at)
|
|
231
|
-
if (messages.some((one) => one.fatal === true)) return
|
|
232
|
-
for (const one of messages) {
|
|
233
|
-
const by = String(one.ruleId ?? '').replace('rule/', '')
|
|
234
|
-
if (by !== '') note(by, one.message)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const files = scenario.files.map((one) => (one.at === scenario.at ? { at: one.at, source } : one))
|
|
239
|
-
answeredBy(standing(ROOT, new Map<string, string>(files.map((one) => [resolve(ROOT, one.at), one.source]))))
|
|
240
|
-
forget()
|
|
241
|
-
try {
|
|
242
|
-
try {
|
|
243
|
-
linted(walking)
|
|
244
|
-
} catch {
|
|
245
|
-
for (const one of walking) {
|
|
246
|
-
try {
|
|
247
|
-
linted([one])
|
|
248
|
-
} catch (thrown) {
|
|
249
|
-
note(one.id, failed(thrown))
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
for (const one of judges) {
|
|
254
|
-
const held = slopnators.get(slopnatorName(one.id))
|
|
255
|
-
if (held === undefined) continue
|
|
256
|
-
if (visits(held)) continue
|
|
257
|
-
try {
|
|
258
|
-
if (sweeps(held)) {
|
|
259
|
-
const said = held.holds(undefined, { filename: scenario.at, text: source, ancestors: () => [], comments: () => [] })
|
|
260
|
-
if (said !== false && said !== NOT_WRITTEN) note(one.id, typeof said === 'string' ? said : slopnatorName(one.id))
|
|
261
|
-
continue
|
|
262
|
-
}
|
|
263
|
-
if (!reads(held)) for (const said of held.checks(ROOT)) note(one.id, said)
|
|
264
|
-
} catch (thrown) {
|
|
265
|
-
note(one.id, failed(thrown))
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
} finally {
|
|
269
|
-
answeredBy(undefined)
|
|
270
|
-
forget()
|
|
271
|
-
}
|
|
272
|
-
return found
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export function crossed(
|
|
276
|
-
trials: readonly Trial[],
|
|
277
|
-
all: readonly Constraint[],
|
|
278
|
-
rules: Readonly<Record<string, Rule.RuleModule>>,
|
|
279
|
-
parser: unknown,
|
|
280
|
-
slopnators: ReadonlyMap<string, Slopnator> = new Map<string, Slopnator>(),
|
|
281
|
-
spent: Map<string, number> = new Map<string, number>(),
|
|
282
|
-
): readonly Outcome[] {
|
|
283
|
-
const found: Outcome[] = []
|
|
284
|
-
const blanks = new Map<string, ReadonlyMap<string, ReadonlySet<string>>>()
|
|
285
|
-
|
|
286
|
-
for (const trial of trials) {
|
|
287
|
-
if (trial.verdict.reports) continue
|
|
288
|
-
const target = trial.scenario.files.find((one) => one.at === trial.scenario.at)
|
|
289
|
-
if (target === undefined) continue
|
|
290
|
-
|
|
291
|
-
const judges = all.filter((one) => governs(one, trial.scenario.at))
|
|
292
|
-
if (!judges.some((one) => one.id !== trial.rule)) continue
|
|
293
|
-
const began = Date.now()
|
|
294
|
-
|
|
295
|
-
const wrap = parseable(target.source, trial.scenario.at, parser)
|
|
296
|
-
const emptied = JSON.stringify([wrap(''), trial.scenario.files.map((one) => (one.at === trial.scenario.at ? { at: one.at, source: '' } : one))])
|
|
297
|
-
const blank = blanks.get(emptied) ?? refusals(trial.scenario, wrap(''), judges, rules, parser, slopnators)
|
|
298
|
-
blanks.set(emptied, blank)
|
|
299
|
-
const said = refusals(trial.scenario, wrap(target.source), judges, rules, parser, slopnators)
|
|
300
|
-
|
|
301
|
-
spent.set(trial.rule, (spent.get(trial.rule) ?? 0) + Date.now() - began)
|
|
302
|
-
|
|
303
|
-
for (const [who, messages] of said) {
|
|
304
|
-
if (who === trial.rule) continue
|
|
305
|
-
for (const message of messages) {
|
|
306
|
-
if (blank.get(who)?.has(message) === true) continue
|
|
307
|
-
found.push({
|
|
308
|
-
id: trial.id + '_under_' + who.replace(/-/g, '_'),
|
|
309
|
-
rule: trial.rule,
|
|
310
|
-
passed: false,
|
|
311
|
-
parsed: true,
|
|
312
|
-
failures: [
|
|
313
|
-
'the form it recommends is refused by ' + who + ', so this law tells somebody to write what another law bans. ' +
|
|
314
|
-
who + ' said: ' + (message.split('Found: ').pop() ?? message),
|
|
315
|
-
],
|
|
316
|
-
})
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return found
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export function runTrials(
|
|
325
|
-
trials: readonly Trial[],
|
|
326
|
-
all: readonly Constraint[],
|
|
327
|
-
rules: Readonly<Record<string, Rule.RuleModule>>,
|
|
328
|
-
parser: unknown,
|
|
329
|
-
slopnators?: ReadonlyMap<string, Slopnator>,
|
|
330
|
-
): Run {
|
|
331
|
-
const byName = new Map(all.map((rule) => [rule.id, rule]))
|
|
332
|
-
const spent = new Map<string, number>()
|
|
333
|
-
const outcomes = trials.map((trial): Outcome => {
|
|
334
|
-
const rule = byName.get(trial.rule)
|
|
335
|
-
const module = rules[trial.rule]
|
|
336
|
-
if (rule === undefined || module === undefined) {
|
|
337
|
-
return {
|
|
338
|
-
id: trial.id,
|
|
339
|
-
rule: trial.rule,
|
|
340
|
-
passed: false,
|
|
341
|
-
parsed: false,
|
|
342
|
-
failures: [`the rule "${trial.rule}" has no slopnator on disk, so this trial is about nothing — the file to write is the one its row names in \`at\``],
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
const began = Date.now()
|
|
346
|
-
const outcome = runTrial(trial, rule, module, parser, slopnators?.get(slopnatorName(rule.id)))
|
|
347
|
-
spent.set(trial.rule, (spent.get(trial.rule) ?? 0) + Date.now() - began)
|
|
348
|
-
return outcome
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
return {
|
|
352
|
-
outcomes,
|
|
353
|
-
passed: outcomes.filter((outcome) => outcome.passed).length,
|
|
354
|
-
failed: outcomes.filter((outcome) => !outcome.passed).length,
|
|
355
|
-
measured: outcomes.filter((outcome) => outcome.parsed).length,
|
|
356
|
-
spent,
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|