@prestack/thread 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,247 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { argv, env, execPath, stderr, stdout } from 'node:process'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
import { CLI, exit } from '../../core/invoked.ts'
|
|
7
|
+
import { named } from '../../core/scope.ts'
|
|
8
|
+
import { findTree, inTree } from '../../core/tree.ts'
|
|
9
|
+
|
|
10
|
+
import type { Secret } from '../../integrations/secret.ts'
|
|
11
|
+
import { opened, sealedIn } from '../../integrations/vault.ts'
|
|
12
|
+
|
|
13
|
+
const TREE = findTree()
|
|
14
|
+
const ROOT = TREE.at
|
|
15
|
+
|
|
16
|
+
const STEP_TIMEOUT = 900_000
|
|
17
|
+
|
|
18
|
+
const REGISTRY = '--registry'
|
|
19
|
+
|
|
20
|
+
const DEFAULT = 'https://registry.npmjs.org'
|
|
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
|
+
id: 'lint',
|
|
36
|
+
runs: 'pnpm exec eslint .',
|
|
37
|
+
because: 'the rules that live on the lint surface, which is the only one carrying a fixer the sweep can report but not repair',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'seal',
|
|
41
|
+
runs: 'pnpm seal:check',
|
|
42
|
+
because: 'the package must be the bytes it says it is. CHECKED and never regenerated, because a release that rewrites its own seal can never fail it',
|
|
43
|
+
},
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const MINE = /^(?:npx |pnpm )?thread /
|
|
47
|
+
|
|
48
|
+
function run(what: string, extra?: Record<string, string>): number {
|
|
49
|
+
const ours = MINE.exec(what)
|
|
50
|
+
if (ours !== null) {
|
|
51
|
+
const words = what.slice(ours[0].length).split(/\s+/).filter((one) => one !== '')
|
|
52
|
+
const ran = spawnSync(execPath, [...CLI, ...words], {
|
|
53
|
+
cwd: ROOT,
|
|
54
|
+
stdio: 'inherit',
|
|
55
|
+
timeout: STEP_TIMEOUT,
|
|
56
|
+
killSignal: 'SIGKILL',
|
|
57
|
+
env: extra === undefined ? env : { ...env, ...extra },
|
|
58
|
+
})
|
|
59
|
+
return ran.error !== undefined ? 1 : (ran.status ?? 1)
|
|
60
|
+
}
|
|
61
|
+
const result = spawnSync(what, {
|
|
62
|
+
shell: true,
|
|
63
|
+
cwd: ROOT,
|
|
64
|
+
stdio: 'inherit',
|
|
65
|
+
timeout: STEP_TIMEOUT,
|
|
66
|
+
killSignal: 'SIGKILL',
|
|
67
|
+
env: extra === undefined ? env : { ...env, ...extra },
|
|
68
|
+
})
|
|
69
|
+
return result.error !== undefined ? 1 : (result.status ?? 1)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function refuse(id: string, code: number, because: string, runs: string): never {
|
|
73
|
+
stderr.write(`\npublish: REFUSED — \`${id}\` exited ${String(code)}. NOTHING was published.\n`)
|
|
74
|
+
stderr.write(`publish: ${because}.\n`)
|
|
75
|
+
stderr.write(`publish: what refused is above, in that command's own words — this does not restate it, because a\n`)
|
|
76
|
+
stderr.write(`publish: second copy of a refusal drifts from the first. Run \`${runs}\` alone to work on it.\n`)
|
|
77
|
+
exit(1)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const flag = argv.indexOf(REGISTRY)
|
|
81
|
+
const registry = flag === -1 ? DEFAULT : (argv[flag + 1] ?? '')
|
|
82
|
+
|
|
83
|
+
if (registry === '' || registry.startsWith('-')) {
|
|
84
|
+
stderr.write(`thread publish ${REGISTRY} <url> where the packages go. Leave it off for ${DEFAULT}.\n`)
|
|
85
|
+
stderr.write('\nit builds, then runs every check, then publishes. A check that fails publishes nothing.\n')
|
|
86
|
+
exit(1)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const TOKEN = 'sec_registry_token'
|
|
90
|
+
|
|
91
|
+
const PRODUCTION = 'production'
|
|
92
|
+
|
|
93
|
+
const secrets = inTree(TREE, 'secrets.ts')
|
|
94
|
+
|
|
95
|
+
const declared = existsSync(secrets)
|
|
96
|
+
? (((await import(pathToFileURL(secrets).href)) as { SECRETS?: readonly Secret[] }).SECRETS ?? [])
|
|
97
|
+
: []
|
|
98
|
+
|
|
99
|
+
const vaulted = declared.some((one) => one.id === TOKEN)
|
|
100
|
+
|
|
101
|
+
if (vaulted && !sealedIn(ROOT, PRODUCTION).includes(TOKEN)) {
|
|
102
|
+
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`)
|
|
103
|
+
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`)
|
|
104
|
+
exit(1)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const setting = (): string => {
|
|
108
|
+
const at = new URL(registry)
|
|
109
|
+
return `npm_config_//${at.host}${at.pathname.endsWith('/') ? at.pathname : `${at.pathname}/`}:_authToken`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function tokened(): Promise<string> {
|
|
113
|
+
const values = await opened(ROOT, declared.map((one) => one.id), PRODUCTION)([TOKEN]).catch((thrown: unknown) => {
|
|
114
|
+
stderr.write(`\npublish: ${TOKEN} is sealed here and would not open: ${thrown instanceof Error ? thrown.message : String(thrown)}. NOTHING was published.\n`)
|
|
115
|
+
exit(1)
|
|
116
|
+
})
|
|
117
|
+
const token = values[TOKEN]
|
|
118
|
+
if (token === undefined) {
|
|
119
|
+
stderr.write(`\npublish: the vault opened and answered nothing for ${TOKEN}. NOTHING was published.\n`)
|
|
120
|
+
exit(1)
|
|
121
|
+
}
|
|
122
|
+
return token
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const signedAs = env['GITHUB_ACTIONS'] === 'true' ? 'the identity npm trusts this GitHub Actions run under' : 'the login this machine already holds'
|
|
126
|
+
|
|
127
|
+
stdout.write(
|
|
128
|
+
vaulted
|
|
129
|
+
? `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`
|
|
130
|
+
: `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`,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
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')
|
|
134
|
+
|
|
135
|
+
const made = run('npx thread packaged --build')
|
|
136
|
+
stdout.write(`publish: packaged exited ${String(made)}\n`)
|
|
137
|
+
if (made !== 0) {
|
|
138
|
+
refuse(
|
|
139
|
+
'packaged',
|
|
140
|
+
made,
|
|
141
|
+
'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',
|
|
142
|
+
'npx thread packaged --build',
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
stdout.write(`\npublish: ${String(CHECKS.length)} check(s) stand between this tree and ${registry}. Any one of them stops it.\n`)
|
|
147
|
+
for (const check of CHECKS) stdout.write(` ${check.id} — ${check.runs}\n`)
|
|
148
|
+
|
|
149
|
+
for (const [index, check] of CHECKS.entries()) {
|
|
150
|
+
const at = `check ${String(index + 1)} of ${String(CHECKS.length)}`
|
|
151
|
+
stdout.write(`\npublish: ${at} — ${check.runs} (${check.id})\n`)
|
|
152
|
+
|
|
153
|
+
const code = run(check.runs)
|
|
154
|
+
stdout.write(`publish: ${at} — exited ${String(code)}\n`)
|
|
155
|
+
if (code !== 0) refuse(check.id, code, check.because, check.runs)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const numbered = run('npx thread packaged --release')
|
|
159
|
+
stdout.write(`publish: packaged --release exited ${String(numbered)}\n`)
|
|
160
|
+
if (numbered !== 0) {
|
|
161
|
+
refuse(
|
|
162
|
+
'packaged',
|
|
163
|
+
numbered,
|
|
164
|
+
'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',
|
|
165
|
+
'npx thread packaged --release',
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const handed = vaulted ? { [setting()]: await tokened() } : undefined
|
|
170
|
+
|
|
171
|
+
stdout.write(`\npublish: every check passed. Publishing to ${registry} with ${vaulted ? `${TOKEN} from this repository's vault` : signedAs}.\n`)
|
|
172
|
+
|
|
173
|
+
const sent = run(`pnpm -r publish --registry ${registry} --no-git-checks --access public`, handed)
|
|
174
|
+
|
|
175
|
+
if (sent !== 0) {
|
|
176
|
+
stderr.write(`\npublish: the checks passed and the publish itself exited ${String(sent)}.\n`)
|
|
177
|
+
const back = run('npx thread packaged')
|
|
178
|
+
stderr.write(
|
|
179
|
+
back === 0
|
|
180
|
+
? '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'
|
|
181
|
+
: 'publish: the manifests still carry the number that did not go out. Run `npx thread packaged` to put the last recorded release back.\n',
|
|
182
|
+
)
|
|
183
|
+
exit(1)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const recorded = run('npx thread packaged --record')
|
|
187
|
+
stdout.write(`publish: packaged --record exited ${String(recorded)}\n`)
|
|
188
|
+
if (recorded !== 0) {
|
|
189
|
+
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 `npx thread packaged --record` and read what it says.\n')
|
|
190
|
+
exit(1)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const SHEET = 'packaging.ts'
|
|
194
|
+
|
|
195
|
+
interface Packaged {
|
|
196
|
+
readonly id?: string
|
|
197
|
+
readonly folder?: string
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const sheet = inTree(TREE, SHEET)
|
|
201
|
+
|
|
202
|
+
const stated = existsSync(sheet)
|
|
203
|
+
? ((await import(pathToFileURL(sheet).href)) as { PACKAGES?: readonly Packaged[] })
|
|
204
|
+
: {}
|
|
205
|
+
|
|
206
|
+
const shipped = stated.PACKAGES ?? []
|
|
207
|
+
|
|
208
|
+
function versionAt(folder: string): string {
|
|
209
|
+
const at = join(ROOT, folder, 'package.json')
|
|
210
|
+
if (!existsSync(at)) return 'no manifest'
|
|
211
|
+
try {
|
|
212
|
+
return (JSON.parse(readFileSync(at, 'utf8')) as { version?: string }).version ?? 'no version'
|
|
213
|
+
} catch {
|
|
214
|
+
return 'no version'
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function asked(what: readonly string[]): string {
|
|
219
|
+
return (spawnSync('git', what, { cwd: ROOT, encoding: 'utf8' }).stdout ?? '').trim()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (asked(['rev-parse', '--is-inside-work-tree']) !== 'true') {
|
|
223
|
+
stdout.write("\npublish: it's not a repo, so nothing was committed.\n")
|
|
224
|
+
} else if (shipped.length === 0) {
|
|
225
|
+
stdout.write('\npublish: no packaging row names a folder, so there is nothing derived here to commit.\n')
|
|
226
|
+
} else {
|
|
227
|
+
const going = shipped.map((one) => `${named(one.id ?? '')} ${versionAt(one.folder ?? '')}`).join(', ')
|
|
228
|
+
const branch = asked(['rev-parse', '--abbrev-ref', 'HEAD'])
|
|
229
|
+
|
|
230
|
+
for (const one of shipped) run(`git add ${one.folder ?? ''}`)
|
|
231
|
+
|
|
232
|
+
if (spawnSync('git', ['diff', '--cached', '--quiet'], { cwd: ROOT }).status === 0) {
|
|
233
|
+
stdout.write(`\npublish: ${going} — what is on disk is already what is committed, so this release added no artifact.\n`)
|
|
234
|
+
} else {
|
|
235
|
+
const wrote = run(`git commit -m ${JSON.stringify(`thread publish — ${going}`)}`)
|
|
236
|
+
stdout.write(`publish: commit exited ${String(wrote)}\n`)
|
|
237
|
+
if (wrote !== 0) {
|
|
238
|
+
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')
|
|
239
|
+
stderr.write('publish: commit them by hand — everything is staged.\n')
|
|
240
|
+
exit(1)
|
|
241
|
+
}
|
|
242
|
+
stdout.write(`publish: committed the derived artifacts to ${branch} — ${going}.\n`)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
stdout.write(`\npublish: done. What went out was derived, then built, then passed the gate, the lint and the seal, in that order. The compile inside \`thread packaged --build\` is the typecheck.\n`)
|
|
247
|
+
exit(0)
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { argv, stdout } from 'node:process'
|
|
4
|
+
|
|
5
|
+
import { exit } from '../../core/invoked.ts'
|
|
6
|
+
import { ELSEWHERE, RECEIPT, answer, answering, asking, asksOf, bodied, given as pipedIn, guidance, issue, marked, named as answerNames, needed, only, ownAnswers, refuse, rowLines, seeding, spend, stating, within, wordsIn, type Asked } from '../../core/asking.ts'
|
|
7
|
+
import { holdsRow, listBounds, withRow } from '../../core/rows.ts'
|
|
8
|
+
import { named } from '../../core/scope.ts'
|
|
9
|
+
import { findTree, inTree } from '../../core/tree.ts'
|
|
10
|
+
|
|
11
|
+
import type { Constraint } from '../constraint.ts'
|
|
12
|
+
import { ROOT, SEEDED, declareSeeded, rules, scaffold, slopnatorName, write } from '../slopnator.ts'
|
|
13
|
+
|
|
14
|
+
const TREE = findTree()
|
|
15
|
+
|
|
16
|
+
const FILE = 'rules.ts'
|
|
17
|
+
|
|
18
|
+
const LIST = 'RULES'
|
|
19
|
+
|
|
20
|
+
const at = within(TREE.at, inTree(TREE, FILE))
|
|
21
|
+
|
|
22
|
+
const kebab = (one: string): string => one.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
|
|
23
|
+
|
|
24
|
+
const KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/
|
|
25
|
+
|
|
26
|
+
export const ASKS: readonly Asked[] = asksOf(
|
|
27
|
+
'rule',
|
|
28
|
+
'Constraint',
|
|
29
|
+
{
|
|
30
|
+
name: { guide: 'kebab-case, e.g. no-fetch-in-a-screen', asks: 'name: ' },
|
|
31
|
+
describes: {
|
|
32
|
+
guide: 'one sentence a reader who has not seen the code could act on',
|
|
33
|
+
asks: 'describes — one sentence a reader who has not seen the code could act on: ',
|
|
34
|
+
},
|
|
35
|
+
where: {
|
|
36
|
+
guide: 'comma separated path fragments this governs, e.g. app/, src/nodes/',
|
|
37
|
+
asks: 'where (comma separated path fragments this governs, e.g. app/, src/nodes/): ',
|
|
38
|
+
},
|
|
39
|
+
except: {
|
|
40
|
+
guide: 'optional — comma separated path fragments to leave out',
|
|
41
|
+
asks: 'except (comma separated, blank for none): ',
|
|
42
|
+
},
|
|
43
|
+
bans: { guide: 'the exact form this refuses, in one line', asks: 'bans — the exact form this refuses, in one line: ' },
|
|
44
|
+
because: { guide: 'a fact somebody measured — why this is refused', asks: 'because — a fact somebody measured: ' },
|
|
45
|
+
instead: { guide: 'what to do instead', asks: 'instead — what to do instead: ' },
|
|
46
|
+
wrong: {
|
|
47
|
+
guide: 'one line of the form this refuses — its trial',
|
|
48
|
+
asks: 'wrong — one line of the form this refuses, its trial: ',
|
|
49
|
+
},
|
|
50
|
+
right: {
|
|
51
|
+
guide: 'one line of the form this allows — its trial',
|
|
52
|
+
asks: 'right — one line of the form this allows, its trial: ',
|
|
53
|
+
},
|
|
54
|
+
severity: {
|
|
55
|
+
guide: 'optional — forbidden (default, leave blank) or warning',
|
|
56
|
+
asks: 'severity — forbidden or warning (blank for forbidden): ',
|
|
57
|
+
},
|
|
58
|
+
finds: {
|
|
59
|
+
guide: 'one sentence saying what the check looks for, read by whoever opens it next',
|
|
60
|
+
asks: 'finds — one sentence saying what the check looks for: ',
|
|
61
|
+
},
|
|
62
|
+
body: {
|
|
63
|
+
guide:
|
|
64
|
+
'the check itself, written where it is asked for. Leave it blank and the rule is declared with nothing behind it, which `npx thread trials` reports unproven',
|
|
65
|
+
asks: 'body: ',
|
|
66
|
+
optional: true,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
['id', 'at', 'version'],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const said = (one: string): string => ASKS.find((each) => each.at === one)?.asks ?? `${one}: `
|
|
73
|
+
|
|
74
|
+
import { quoted } from '../../core/rows.ts'
|
|
75
|
+
|
|
76
|
+
const held = await rules()
|
|
77
|
+
if (held === undefined) {
|
|
78
|
+
refuse(
|
|
79
|
+
`there is no ${at} declaring \`${LIST}\` here, so a rule has nowhere to go. ` +
|
|
80
|
+
`Run \`npx thread attach ${named('integrity')}\` — the sheet is the package's to write, not yours.`,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
const existing: readonly Constraint[] = held
|
|
84
|
+
|
|
85
|
+
const mark = (one: Constraint): string =>
|
|
86
|
+
marked([one.id, one.describes, one.bans, one.because, one.instead, one.wrong, one.right, JSON.stringify(one.where), one.severity ?? ''])
|
|
87
|
+
|
|
88
|
+
const listing = [
|
|
89
|
+
existing.length === 0
|
|
90
|
+
? 'no rules of its own yet — this app enforces only what the packages it attached ship.'
|
|
91
|
+
: `${String(existing.length)} rule(s) this app adds of its own:`,
|
|
92
|
+
...existing.map((one) => ` [${mark(one)}] ${one.id} — ${one.bans}${one.severity === 'warning' ? ' (warning)' : ''}`),
|
|
93
|
+
].join('\n')
|
|
94
|
+
|
|
95
|
+
const words = wordsIn(['new'])
|
|
96
|
+
|
|
97
|
+
stdout.write(`${listing}\n`)
|
|
98
|
+
|
|
99
|
+
const flagged = argv.indexOf(RECEIPT)
|
|
100
|
+
|
|
101
|
+
if (flagged === -1) {
|
|
102
|
+
const nonce = issue(TREE.at, listing)
|
|
103
|
+
stdout.write(
|
|
104
|
+
[
|
|
105
|
+
'',
|
|
106
|
+
...answering('rule'),
|
|
107
|
+
'',
|
|
108
|
+
...guidance(ASKS),
|
|
109
|
+
'',
|
|
110
|
+
` npx thread create rule new <name> ${RECEIPT} ${nonce}`,
|
|
111
|
+
'',
|
|
112
|
+
...ELSEWHERE,
|
|
113
|
+
'',
|
|
114
|
+
`${RECEIPT} is spent by one run, and is refused if ${at} changed after it was issued.`,
|
|
115
|
+
'',
|
|
116
|
+
'this writes three things, not one. The row goes in the sheet, the check goes in the file it ' +
|
|
117
|
+
'seeds from the `body` you give, and the trial comes from `wrong` and `right`.',
|
|
118
|
+
'',
|
|
119
|
+
'then it runs every generator this tree declares, that trial among them, so before this command ' +
|
|
120
|
+
'returns you see whether the rule fires at the form it refuses and stays silent at the fix it ' +
|
|
121
|
+
'names. Leave `body` blank and the row is declared with nothing behind it, and `npx thread ' +
|
|
122
|
+
'trials` reports it unproven until somebody writes the check.',
|
|
123
|
+
'',
|
|
124
|
+
].join('\n'),
|
|
125
|
+
)
|
|
126
|
+
stdout.write(`create: receipt ${nonce} — spent on use, and stale the moment ${at} changes.\n`)
|
|
127
|
+
exit(0)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
spend(TREE.at, listing, argv[flagged + 1], at)
|
|
131
|
+
|
|
132
|
+
const all = pipedIn()
|
|
133
|
+
const { piped, ask, line, shut } = asking(all)
|
|
134
|
+
|
|
135
|
+
const own = ownAnswers(all)
|
|
136
|
+
|
|
137
|
+
if (piped) only(own, answerNames(ASKS), '`new`')
|
|
138
|
+
|
|
139
|
+
const name = kebab(words[1] ?? (piped ? needed(own, 'name', '`new`') : await ask('name', said('name'))))
|
|
140
|
+
if (!KEBAB.test(name)) {
|
|
141
|
+
shut()
|
|
142
|
+
refuse(`\`${name}\` cannot be the id of a rule — it is kebab-case, e.g. no-fetch-in-a-screen.`)
|
|
143
|
+
}
|
|
144
|
+
if (existing.some((one) => one.id === name)) {
|
|
145
|
+
shut()
|
|
146
|
+
refuse(`\`${name}\` is already declared in ${at}. One id, one rule — two would silently keep whichever ran second.`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const describes = piped
|
|
150
|
+
? needed(own, 'describes', '`new`')
|
|
151
|
+
: await ask('describes', said('describes'))
|
|
152
|
+
if (describes === '') {
|
|
153
|
+
shut()
|
|
154
|
+
refuse(`\`${name}\` was given no meaning, and a row without one is a name nobody can read.`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const whereSaid = piped ? needed(own, 'where', '`new`') : await ask('where', said('where'))
|
|
158
|
+
const paths = whereSaid.split(',').map((one) => one.trim()).filter((one) => one !== '')
|
|
159
|
+
if (paths.length === 0) {
|
|
160
|
+
shut()
|
|
161
|
+
refuse(`\`${name}\` names no path, so it would govern nothing — a rule that cannot fire is indistinguishable from a rule being obeyed.`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const exceptSaid = piped ? (answer(own, 'except') ?? '') : await ask('except', said('except'))
|
|
165
|
+
const except = exceptSaid.split(',').map((one) => one.trim()).filter((one) => one !== '')
|
|
166
|
+
|
|
167
|
+
const bans = piped ? needed(own, 'bans', '`new`') : await ask('bans', said('bans'))
|
|
168
|
+
if (bans === '') {
|
|
169
|
+
shut()
|
|
170
|
+
refuse(`\`${name}\` names no \`bans\`, and a rule that does not say what form it refuses cannot be told from one refusing nothing.`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const because = piped ? needed(own, 'because', '`new`') : await ask('because', said('because'))
|
|
174
|
+
if (because === '') {
|
|
175
|
+
shut()
|
|
176
|
+
refuse(`\`${name}\` names no \`because\`, and a refusal with no reason is an order rather than a rule.`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const instead = piped ? needed(own, 'instead', '`new`') : await ask('instead', said('instead'))
|
|
180
|
+
if (instead === '') {
|
|
181
|
+
shut()
|
|
182
|
+
refuse(`\`${name}\` names no \`instead\`, so whoever it refuses is told only what they did wrong.`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const wrong = piped ? needed(own, 'wrong', '`new`') : await ask('wrong', said('wrong'))
|
|
186
|
+
if (wrong === '') {
|
|
187
|
+
shut()
|
|
188
|
+
refuse(`\`${name}\` names no \`wrong\`, and a rule with nothing it refuses is a predicate with no trial to prove it fires.`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const right = piped ? needed(own, 'right', '`new`') : await ask('right', said('right'))
|
|
192
|
+
if (right === '') {
|
|
193
|
+
shut()
|
|
194
|
+
refuse(
|
|
195
|
+
`\`${name}\` names no \`right\`, and a rule that only shows what it refuses cannot prove it stays silent on the fix it recommends.`,
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const severity = piped ? (answer(own, 'severity') ?? '') : await ask('severity', said('severity'))
|
|
200
|
+
if (severity !== '' && severity !== 'forbidden' && severity !== 'warning') {
|
|
201
|
+
shut()
|
|
202
|
+
refuse(`\`${severity}\` is not a severity — a rule is \`forbidden\` (the default, leave blank to use it) or \`warning\`.`)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const finds = piped ? needed(own, 'finds', '`new`') : await ask('finds', said('finds'))
|
|
206
|
+
if (finds === '') {
|
|
207
|
+
shut()
|
|
208
|
+
refuse(
|
|
209
|
+
`\`${name}\` says nothing about what its check looks for, and that sentence is the only thing between the ` +
|
|
210
|
+
'next person and opening the file. Say what THIS one looks for, not what a check is.',
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const body = await bodied(`the check for \`${name}\``, scaffold(), piped, all, line)
|
|
215
|
+
|
|
216
|
+
const { stated, kids } = await stating(ASKS, all, piped, ask, line, ['name', 'id', 'at', 'describes', 'version', 'where', 'except', 'bans', 'because', 'instead', 'wrong', 'right', 'severity', 'finds', 'body'])
|
|
217
|
+
|
|
218
|
+
shut()
|
|
219
|
+
|
|
220
|
+
const row = [
|
|
221
|
+
' {',
|
|
222
|
+
` id: ${quoted(name)},`,
|
|
223
|
+
` at: ${quoted(at)},`,
|
|
224
|
+
` describes: ${quoted(describes)},`,
|
|
225
|
+
' version: 1,',
|
|
226
|
+
` where: { paths: [${paths.map(quoted).join(', ')}]${except.length === 0 ? '' : `, except: [${except.map(quoted).join(', ')}]`} },`,
|
|
227
|
+
` bans: ${quoted(bans)},`,
|
|
228
|
+
` because: ${quoted(because)},`,
|
|
229
|
+
` instead: ${quoted(instead)},`,
|
|
230
|
+
` wrong: ${quoted(wrong)},`,
|
|
231
|
+
` right: ${quoted(right)},`,
|
|
232
|
+
...(severity === '' || severity === 'forbidden' ? [] : [` severity: ${quoted(severity)},`]),
|
|
233
|
+
...rowLines(' ', stated, kids),
|
|
234
|
+
' },',
|
|
235
|
+
].join('\n')
|
|
236
|
+
|
|
237
|
+
const path = inTree(TREE, FILE)
|
|
238
|
+
const text = readFileSync(path, 'utf8')
|
|
239
|
+
const where = listBounds(text, LIST)
|
|
240
|
+
if (where === undefined) refuse(`${at} states no \`${LIST}\` list to declare a rule in.`)
|
|
241
|
+
if (holdsRow(text, where, name)) refuse(`\`${name}\` is already declared in ${at}.`)
|
|
242
|
+
|
|
243
|
+
writeFileSync(path, withRow(text, where, row), 'utf8')
|
|
244
|
+
stdout.write(`create: declared ${name} in ${at}.\n`)
|
|
245
|
+
|
|
246
|
+
const slopAt = `${SEEDED}/${slopnatorName(name)}.ts`
|
|
247
|
+
if (existsSync(join(ROOT, slopAt))) {
|
|
248
|
+
stdout.write(`create: ${slopAt} is already on disk — left untouched, and declared below.\n`)
|
|
249
|
+
} else {
|
|
250
|
+
write(ROOT, { path: slopAt, content: body ?? scaffold() })
|
|
251
|
+
stdout.write(
|
|
252
|
+
body === undefined
|
|
253
|
+
? `create: seeded ${slopAt} with nothing behind it — its \`holds\` answers NOT_WRITTEN.\n`
|
|
254
|
+
: `create: seeded ${slopAt} with the check you wrote.\n`,
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
for (const told of seeding(kids, ROOT)) stdout.write(`create: ${told}\n`)
|
|
259
|
+
|
|
260
|
+
const declared = await declareSeeded(ROOT, [name], finds)
|
|
261
|
+
if (declared.length > 0) stdout.write(`create: declared the check for "${name}" in its own row.\n`)
|
|
262
|
+
|
|
263
|
+
stdout.write(
|
|
264
|
+
body === undefined
|
|
265
|
+
? `create: ${name} refuses nothing yet — write ${slopAt}, matching the shape any file already under ` +
|
|
266
|
+
'slopnators/ uses. `npx thread trials` reports it unproven until you do.\n'
|
|
267
|
+
: `create: ${name} is declared and its check is written. The trial derived from \`wrong\` and \`right\` ` +
|
|
268
|
+
'runs below — it proves the rule fires at the form it refuses and stays silent at the fix it names.\n',
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
exit(0)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
4
|
+
import { argv, stderr, stdout } from 'node:process'
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { runChecker, type Finding } from '../../core/finding.ts'
|
|
8
|
+
import { exit, invoked } from '../../core/invoked.ts'
|
|
9
|
+
import { findTree, inTree } from '../../core/tree.ts'
|
|
10
|
+
import { SEALED } from '../sealed.ts'
|
|
11
|
+
|
|
12
|
+
export const SEALED_AT = 'sealed.ts'
|
|
13
|
+
|
|
14
|
+
const HERE = fileURLToPath(import.meta.url)
|
|
15
|
+
|
|
16
|
+
const FROM_SOURCE = HERE.endsWith('.ts')
|
|
17
|
+
|
|
18
|
+
const ROOT = FROM_SOURCE ? findTree().at : resolve(HERE, '../..')
|
|
19
|
+
|
|
20
|
+
const DOMAIN = basename(resolve(HERE, '../..'))
|
|
21
|
+
|
|
22
|
+
const WRITTEN = join(dirname(HERE), '..', SEALED_AT)
|
|
23
|
+
|
|
24
|
+
const BUILT = SEALED_AT.replace(/\.ts$/, '.js')
|
|
25
|
+
|
|
26
|
+
const CODE = /\.js$/
|
|
27
|
+
|
|
28
|
+
const SHEET = 'packaging.ts'
|
|
29
|
+
|
|
30
|
+
interface Packaged {
|
|
31
|
+
readonly id?: string
|
|
32
|
+
readonly folder?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function declared(): Promise<readonly Packaged[]> {
|
|
36
|
+
const sheet = inTree(findTree(), SHEET)
|
|
37
|
+
if (!existsSync(sheet)) return []
|
|
38
|
+
const loaded = (await import(pathToFileURL(sheet).href)) as { readonly PACKAGES?: readonly Packaged[] }
|
|
39
|
+
return loaded.PACKAGES ?? []
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PACKAGES: readonly Packaged[] = FROM_SOURCE ? await declared() : []
|
|
43
|
+
|
|
44
|
+
function built(): string {
|
|
45
|
+
if (!FROM_SOURCE) return ROOT
|
|
46
|
+
const folders = PACKAGES.map((one) => one.folder ?? '').filter((one) => one !== '')
|
|
47
|
+
const holder = folders.find((folder) => existsSync(join(ROOT, folder, 'dist', DOMAIN)))
|
|
48
|
+
if (holder === undefined) {
|
|
49
|
+
stderr.write(
|
|
50
|
+
`seal: no package built here holds \`${DOMAIN}\` — ${folders.length === 0 ? '_threads/packaging.ts declares no package' : `none of ${folders.join(', ')} has a dist/${DOMAIN}`}. ` +
|
|
51
|
+
`Run \`thread packaged --build\` first, and if no package row carries the ${DOMAIN} words, put them on one in _threads/packaging.ts.\n`,
|
|
52
|
+
)
|
|
53
|
+
exit(1)
|
|
54
|
+
}
|
|
55
|
+
return join(ROOT, holder, 'dist', DOMAIN)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function sealable(home: string = built()): readonly string[] {
|
|
59
|
+
const found: string[] = []
|
|
60
|
+
const step = (at: string): void => {
|
|
61
|
+
for (const entry of readdirSync(at).sort()) {
|
|
62
|
+
const here = join(at, entry)
|
|
63
|
+
if (statSync(here).isDirectory()) step(here)
|
|
64
|
+
else if (CODE.test(entry) && relative(home, here) !== BUILT) found.push(here)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
step(home)
|
|
68
|
+
return found.sort()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function sealOf(home: string = built()): Readonly<Record<string, string>> {
|
|
72
|
+
const held: Record<string, string> = {}
|
|
73
|
+
for (const file of sealable(home)) {
|
|
74
|
+
held[relative(home, file)] = createHash('sha256').update(readFileSync(file)).digest('hex')
|
|
75
|
+
}
|
|
76
|
+
return held
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function content(seal: Readonly<Record<string, string>>): string {
|
|
80
|
+
const rows = Object.entries(seal).map(([at, hash]) => ` '${at}': '${hash}',`)
|
|
81
|
+
return [
|
|
82
|
+
'export const SEALED: Readonly<Record<string, string>> = {',
|
|
83
|
+
...rows,
|
|
84
|
+
'}',
|
|
85
|
+
'',
|
|
86
|
+
].join('\n')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (invoked(import.meta.url)) {
|
|
90
|
+
const seal = sealOf()
|
|
91
|
+
const emitted = content(seal)
|
|
92
|
+
|
|
93
|
+
if (argv.includes('--check')) {
|
|
94
|
+
const sealed = Object.keys(seal).length
|
|
95
|
+
const findings: Finding[] =
|
|
96
|
+
content(SEALED) === emitted
|
|
97
|
+
? []
|
|
98
|
+
: [
|
|
99
|
+
{
|
|
100
|
+
detail: `${SEALED_AT} is not the bytes this package is made of`,
|
|
101
|
+
instead: 'run `thread seal` and commit what it writes — the seal is what proves an installed copy is the one that shipped',
|
|
102
|
+
file: relative(ROOT, WRITTEN),
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
runChecker('seal', findings, `${String(sealed)} file(s) sealed`)
|
|
106
|
+
exit(findings.length === 0 ? 0 : 1)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!FROM_SOURCE) {
|
|
110
|
+
stderr.write('seal: this copy is installed, and a seal is written where the package is made. Nothing was rewritten — `thread seal --check` says whether this copy is the one that shipped.\n')
|
|
111
|
+
exit(1)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
mkdirSync(dirname(WRITTEN), { recursive: true })
|
|
115
|
+
writeFileSync(WRITTEN, emitted)
|
|
116
|
+
stdout.write(`seal: wrote ${SEALED_AT} — ${String(Object.keys(seal).length)} file(s).\n`)
|
|
117
|
+
}
|