@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,350 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { dirname, join, sep } from 'node:path'
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { invoked } from '../core/invoked.ts'
|
|
8
|
+
|
|
9
|
+
import { listBounds, quoted, withSeed } from '../core/rows.ts'
|
|
10
|
+
|
|
11
|
+
import { CONSTRAINTS, type Constraint } from './constraint.ts'
|
|
12
|
+
import { SEALED } from './sealed.ts'
|
|
13
|
+
import { findTree, inTree } from '../core/tree.ts'
|
|
14
|
+
|
|
15
|
+
export const NOT_WRITTEN = Symbol.for('thread.slopnator.not_written')
|
|
16
|
+
|
|
17
|
+
const HOME = dirname(fileURLToPath(import.meta.url))
|
|
18
|
+
|
|
19
|
+
const BUILT = dirname(dirname(HOME))
|
|
20
|
+
|
|
21
|
+
export const SHIPPED = `${HOME}/slopnators`
|
|
22
|
+
|
|
23
|
+
export const SEEDED = `${findTree().name}/slopnators`
|
|
24
|
+
|
|
25
|
+
function homes(root: string): readonly string[] {
|
|
26
|
+
return [SHIPPED, join(root, SEEDED)]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const ROOT = `${findTree().at}/`
|
|
30
|
+
|
|
31
|
+
export const DEPTH = 'THREAD_DEPTH'
|
|
32
|
+
|
|
33
|
+
export const CEILING = 1
|
|
34
|
+
|
|
35
|
+
export const SPAWN_TIMEOUT = 120_000
|
|
36
|
+
|
|
37
|
+
export const SPAWN_BUFFER = 8 * 1024 * 1024
|
|
38
|
+
|
|
39
|
+
export function nested(): boolean {
|
|
40
|
+
return Number(process.env[DEPTH] ?? '0') >= CEILING
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ran(command: string, options: { readonly cwd: string; readonly args?: readonly string[] }): { readonly status: number | null; readonly said: string } | undefined {
|
|
44
|
+
if (nested()) return undefined
|
|
45
|
+
const depth = Number(process.env[DEPTH] ?? '0')
|
|
46
|
+
const out = spawnSync(command, [...(options.args ?? [])], {
|
|
47
|
+
shell: options.args === undefined,
|
|
48
|
+
cwd: options.cwd,
|
|
49
|
+
encoding: 'utf8',
|
|
50
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
51
|
+
timeout: SPAWN_TIMEOUT,
|
|
52
|
+
killSignal: 'SIGKILL',
|
|
53
|
+
maxBuffer: SPAWN_BUFFER,
|
|
54
|
+
env: { ...process.env, [DEPTH]: String(depth + 1) },
|
|
55
|
+
})
|
|
56
|
+
return { status: out.status, said: `${String(out.stdout ?? '')}${String(out.stderr ?? '')}` }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SlopnatorContext {
|
|
60
|
+
readonly filename: string
|
|
61
|
+
readonly text: string
|
|
62
|
+
readonly ancestors: () => readonly unknown[]
|
|
63
|
+
readonly comments: () => readonly Spoken[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface Spoken {
|
|
67
|
+
readonly value: string
|
|
68
|
+
readonly range: readonly [number, number]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface Proves {
|
|
72
|
+
readonly wrong: readonly Proof[]
|
|
73
|
+
readonly right: readonly Proof[]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface Reads {
|
|
77
|
+
readonly holds: (node: unknown, at: SlopnatorContext) => boolean | string | typeof NOT_WRITTEN
|
|
78
|
+
readonly fixes?: (node: unknown, at: SlopnatorContext) => string | undefined
|
|
79
|
+
readonly proves: Proves
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface Reading extends Reads {
|
|
83
|
+
readonly on?: readonly string[]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface Running {
|
|
87
|
+
readonly checks: (root: string) => readonly string[]
|
|
88
|
+
readonly proves: Proves
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type Slopnator = Reading | Running
|
|
92
|
+
|
|
93
|
+
export type Visiting = Reading & { readonly on: readonly string[] }
|
|
94
|
+
|
|
95
|
+
export const reads = (one: Slopnator): one is Reading => 'holds' in one
|
|
96
|
+
|
|
97
|
+
export const visits = (one: Slopnator): one is Visiting => 'holds' in one && one.on !== undefined
|
|
98
|
+
|
|
99
|
+
export const sweeps = (one: Slopnator): one is Reading => 'holds' in one && one.on === undefined
|
|
100
|
+
|
|
101
|
+
export interface Sourced {
|
|
102
|
+
readonly at: string
|
|
103
|
+
readonly source: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type Proof = string | { readonly files: readonly Sourced[]; readonly at: string }
|
|
107
|
+
|
|
108
|
+
export interface Byproduct {
|
|
109
|
+
readonly path: string
|
|
110
|
+
readonly content: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function slopnatorName(rule: string): string {
|
|
114
|
+
return `sl_${rule
|
|
115
|
+
.replace(/[^a-zA-Z0-9]+/g, '_')
|
|
116
|
+
.replace(/^_+|_+$/g, '')
|
|
117
|
+
.toLowerCase()}`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const CANONICAL_TREE = 'threads'
|
|
121
|
+
|
|
122
|
+
const TREE = findTree().name
|
|
123
|
+
|
|
124
|
+
function parts(one: string): readonly string[] {
|
|
125
|
+
return one.split('\\').join('/').split('/').filter((part) => part !== '')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function wanted(fragment: string): readonly string[] {
|
|
129
|
+
const all = parts(fragment)
|
|
130
|
+
return all[0] === CANONICAL_TREE ? [TREE, ...all.slice(1)] : all
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function names(fragment: string, path: readonly string[]): boolean {
|
|
134
|
+
if (fragment === '.') return true
|
|
135
|
+
const want = wanted(fragment)
|
|
136
|
+
if (want.length === 0) return false
|
|
137
|
+
if (!fragment.endsWith('/')) return want.join('/') === path.join('/')
|
|
138
|
+
for (let at = 0; at + want.length < path.length; at += 1) {
|
|
139
|
+
if (want.every((part, step) => part === path[at + step])) return true
|
|
140
|
+
}
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function governs(rule: Constraint, filename: string): boolean {
|
|
145
|
+
const path = parts(filename.startsWith(ROOT) ? filename.slice(ROOT.length) : filename)
|
|
146
|
+
if ((rule.where.except ?? []).some((fragment) => names(fragment, path))) return false
|
|
147
|
+
return rule.where.paths.some((fragment) => names(fragment, path))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export class UnloadableSlopnatorError extends Error {
|
|
151
|
+
constructor(rule: string, at: string, because: string) {
|
|
152
|
+
super(
|
|
153
|
+
`rule "${rule}" names a slopnator at ${at}, the file is on disk, and loading it threw: ${because}. ` +
|
|
154
|
+
'A slopnated that cannot be imported reads from every caller exactly like a slopnator that found nothing — ' +
|
|
155
|
+
'the sweep skips it, the trial falls through, and the rule reports green while enforcing nothing. ' +
|
|
156
|
+
'Fix the import, or delete the rule.',
|
|
157
|
+
)
|
|
158
|
+
this.name = 'UnloadableSlopnatorError'
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export class UnreadableRulesError extends Error {
|
|
163
|
+
constructor(at: string, because: string) {
|
|
164
|
+
super(
|
|
165
|
+
`${at} is where this tree states the rules it authors, and reading it threw: ${because}. ` +
|
|
166
|
+
'A tree whose rules cannot be read reports exactly like a tree that declared none — every surface runs, finds nothing of its own, and exits 0. ' +
|
|
167
|
+
'Fix the file, or delete it if this tree authors no rules of its own.',
|
|
168
|
+
)
|
|
169
|
+
this.name = 'UnreadableRulesError'
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export class BrokenSealError extends Error {
|
|
174
|
+
constructor(at: string, why: string) {
|
|
175
|
+
super(
|
|
176
|
+
`${at} ${why} — this package is not the one that was published, so nothing it refuses or allows can be trusted. ` +
|
|
177
|
+
'Reinstall it (`pnpm install --frozen-lockfile` re-verifies the published bytes), or if you meant to change it, run `thread seal` and publish that.',
|
|
178
|
+
)
|
|
179
|
+
this.name = 'BrokenSealError'
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const INSTALLED = HOME.includes(`${sep}node_modules${sep}`)
|
|
184
|
+
|
|
185
|
+
export function unsealed(home: string = HOME): readonly string[] {
|
|
186
|
+
const broken: string[] = []
|
|
187
|
+
for (const [at, hash] of Object.entries(SEALED)) {
|
|
188
|
+
let held: Buffer
|
|
189
|
+
try {
|
|
190
|
+
held = readFileSync(join(home, at))
|
|
191
|
+
} catch {
|
|
192
|
+
broken.push(`${at} is missing`)
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
if (createHash('sha256').update(held).digest('hex') !== hash) broken.push(`${at} differs from the seal`)
|
|
196
|
+
}
|
|
197
|
+
return broken
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function verify(): void {
|
|
201
|
+
if (!INSTALLED) return
|
|
202
|
+
const broken = unsealed(BUILT)
|
|
203
|
+
const first = broken[0]
|
|
204
|
+
if (first !== undefined) throw new BrokenSealError(first.split(' ')[0] ?? '', first.split(' ').slice(1).join(' '))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function load(root: string, all: readonly Constraint[]): Promise<ReadonlyMap<string, Slopnator>> {
|
|
208
|
+
verify()
|
|
209
|
+
const found = new Map<string, Slopnator>()
|
|
210
|
+
for (const rule of all) {
|
|
211
|
+
const name = slopnatorName(rule.id)
|
|
212
|
+
const at = homes(root)
|
|
213
|
+
.flatMap((dir) => [join(dir, `${name}.ts`), join(dir, `${name}.js`)])
|
|
214
|
+
.find((one) => existsSync(one))
|
|
215
|
+
if (at === undefined) continue
|
|
216
|
+
|
|
217
|
+
let module: unknown
|
|
218
|
+
try {
|
|
219
|
+
module = await import(pathToFileURL(at).href)
|
|
220
|
+
} catch (thrown) {
|
|
221
|
+
throw new UnloadableSlopnatorError(rule.id, at, thrown instanceof Error ? thrown.message : String(thrown))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const one = (module as { slopnator?: Slopnator }).slopnator
|
|
225
|
+
if (one === undefined) {
|
|
226
|
+
throw new UnloadableSlopnatorError(rule.id, at, 'it exports no `slopnator`')
|
|
227
|
+
}
|
|
228
|
+
found.set(name, one)
|
|
229
|
+
}
|
|
230
|
+
return found
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function proofsOf(
|
|
234
|
+
rule: Constraint,
|
|
235
|
+
slopnators: ReadonlyMap<string, Slopnator>,
|
|
236
|
+
): { readonly wrong: readonly Proof[]; readonly right: readonly Proof[] } {
|
|
237
|
+
const slopnated = slopnators.get(slopnatorName(rule.id))?.proves
|
|
238
|
+
return { wrong: [rule.wrong, ...(slopnated?.wrong ?? [])], right: [rule.right, ...(slopnated?.right ?? [])] }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function scaffold(): string {
|
|
242
|
+
return `import { NOT_WRITTEN, type Slopnator } from './slopnator.ts'
|
|
243
|
+
|
|
244
|
+
export const slopnator: Slopnator = {
|
|
245
|
+
on: [],
|
|
246
|
+
holds: () => NOT_WRITTEN,
|
|
247
|
+
proves: { wrong: [], right: [] },
|
|
248
|
+
}
|
|
249
|
+
`
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function write(root: string, byproduct: Byproduct): void {
|
|
253
|
+
const path = join(root, byproduct.path)
|
|
254
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
255
|
+
writeFileSync(path, byproduct.content, 'utf8')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function onDisk(root: string): readonly string[] {
|
|
259
|
+
const found = new Set<string>()
|
|
260
|
+
for (const dir of homes(root)) {
|
|
261
|
+
if (!existsSync(dir)) continue
|
|
262
|
+
for (const file of readdirSync(dir)) {
|
|
263
|
+
if (file.startsWith('sl_') && file.endsWith('.ts')) found.add(file.replace(/\.ts$/, ''))
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return [...found].sort()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function named(all: readonly Constraint[]): readonly string[] {
|
|
270
|
+
return all.map((rule) => slopnatorName(rule.id)).sort()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function rules(): Promise<readonly Constraint[] | undefined> {
|
|
274
|
+
const at = inTree(findTree(), 'rules.ts')
|
|
275
|
+
if (!existsSync(at)) return undefined
|
|
276
|
+
let module: { RULES?: readonly Constraint[] }
|
|
277
|
+
try {
|
|
278
|
+
module = (await import(pathToFileURL(at).href)) as { RULES?: readonly Constraint[] }
|
|
279
|
+
} catch (thrown) {
|
|
280
|
+
throw new UnreadableRulesError(at, thrown instanceof Error ? thrown.message : String(thrown))
|
|
281
|
+
}
|
|
282
|
+
if (module.RULES === undefined) throw new UnreadableRulesError(at, 'it exports no `RULES`')
|
|
283
|
+
return module.RULES
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function seeds(root: string, all: readonly Constraint[]): readonly Byproduct[] {
|
|
287
|
+
const found = onDisk(root)
|
|
288
|
+
return named(all)
|
|
289
|
+
.filter((wanted) => !found.includes(wanted))
|
|
290
|
+
.map((wanted) => ({ path: `${SEEDED}/${wanted}.ts`, content: scaffold() }))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export const GENERALLY =
|
|
294
|
+
'The check the rule beside it refuses with. The rule says what is banned and this finds it, reading every file that rule governs and answering whether each one breaks it.'
|
|
295
|
+
|
|
296
|
+
export function declaring(at: string, says: string): string {
|
|
297
|
+
return [
|
|
298
|
+
' {',
|
|
299
|
+
" id: 'slopnator',",
|
|
300
|
+
` at: '${at}',`,
|
|
301
|
+
` describes: ${quoted(says)},`,
|
|
302
|
+
' },',
|
|
303
|
+
].join('\n')
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function declareSeeded(root: string, ids: readonly string[], says?: string): Promise<readonly string[]> {
|
|
307
|
+
if (ids.length === 0) return []
|
|
308
|
+
const at = inTree(findTree(), 'rules.ts')
|
|
309
|
+
let text: string
|
|
310
|
+
try {
|
|
311
|
+
text = readFileSync(at, 'utf8')
|
|
312
|
+
} catch {
|
|
313
|
+
return []
|
|
314
|
+
}
|
|
315
|
+
const where = listBounds(text, 'RULES')
|
|
316
|
+
if (where === undefined) return []
|
|
317
|
+
|
|
318
|
+
const done: string[] = []
|
|
319
|
+
for (const id of ids) {
|
|
320
|
+
const next = withSeed(
|
|
321
|
+
text,
|
|
322
|
+
listBounds(text, 'RULES') ?? where,
|
|
323
|
+
id,
|
|
324
|
+
declaring(`${SEEDED}/${slopnatorName(id)}.ts`, says ?? GENERALLY),
|
|
325
|
+
)
|
|
326
|
+
if (next === text) continue
|
|
327
|
+
text = next
|
|
328
|
+
done.push(id)
|
|
329
|
+
}
|
|
330
|
+
if (done.length > 0) writeFileSync(at, text, 'utf8')
|
|
331
|
+
return done
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (invoked(import.meta.url)) {
|
|
335
|
+
const own = (await rules()) ?? []
|
|
336
|
+
const seeded = seeds(ROOT, own)
|
|
337
|
+
for (const byproduct of seeded) {
|
|
338
|
+
write(ROOT, byproduct)
|
|
339
|
+
console.log(`slopnator: seeded ${byproduct.path}`)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const held = new Set(onDisk(ROOT))
|
|
343
|
+
const declared = await declareSeeded(ROOT, own.filter((rule) => held.has(slopnatorName(rule.id))).map((rule) => rule.id))
|
|
344
|
+
for (const id of declared) console.log(`slopnator: declared the slopnator for "${id}" in its own row`)
|
|
345
|
+
|
|
346
|
+
console.log(
|
|
347
|
+
`slopnator: ${String(own.length)} rule(s), ${String(seeded.length)} seeded, ${String(declared.length)} declared. ` +
|
|
348
|
+
`${String(CONSTRAINTS.length)} shipped constraint(s) are authored, not seeded — unpaired-slopnator refuses one whose slopnated is missing.`,
|
|
349
|
+
)
|
|
350
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Slopnator } from '../slopnator.ts'
|
|
2
|
+
import { here, names, threaded, unreadable } from './sl_unthreaded.ts'
|
|
3
|
+
import { findTree } from '../../core/tree.ts'
|
|
4
|
+
|
|
5
|
+
const THREAD_AT = `${findTree().name}/thread.ts`
|
|
6
|
+
|
|
7
|
+
export const slopnator: Slopnator = {
|
|
8
|
+
holds: (_node, at) => {
|
|
9
|
+
const path = here(at.filename)
|
|
10
|
+
if (path === THREAD_AT) return false
|
|
11
|
+
|
|
12
|
+
if (unreadable(path, at.text) !== undefined) return false
|
|
13
|
+
const children = names(path, at.text)
|
|
14
|
+
if (children.length === 0) return false
|
|
15
|
+
if (threaded().has(path)) return false
|
|
16
|
+
|
|
17
|
+
return `this file names ${String(children.length)} child(ren) — ${children.slice(0, 3).join(', ')} — and its own origin does not trace back to threads/, so everything it parents is stranded with it`
|
|
18
|
+
},
|
|
19
|
+
proves: {
|
|
20
|
+
wrong: [
|
|
21
|
+
"export const M: Threaded = {\n declares: [\n {\n at: 'scripts/finding.ts',\n },\n ],\n}",
|
|
22
|
+
"export const M: Threaded = {\n derives: [\n {\n at: 'scripts/finding.ts',\n },\n ],\n}",
|
|
23
|
+
],
|
|
24
|
+
right: [
|
|
25
|
+
{
|
|
26
|
+
files: [
|
|
27
|
+
{ at: 'threads/_names_it.ts', source: "export const OWNER: Threaded = {\n declares: [\n {\n at: 'scripts/_probe_mid.ts',\n },\n ],\n}\n" },
|
|
28
|
+
{ at: 'scripts/_probe_mid.ts', source: "export const MID: Threaded = {\n declares: [\n {\n at: 'scripts/_probe_end.ts',\n },\n ],\n}\n" },
|
|
29
|
+
{ at: 'scripts/_probe_end.ts', source: 'export const END = 1\n' },
|
|
30
|
+
],
|
|
31
|
+
at: 'scripts/_probe_mid.ts',
|
|
32
|
+
},
|
|
33
|
+
'export const PLAIN = 1',
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Slopnator } from '../slopnator.ts'
|
|
2
|
+
import { filesGoverned, here, islandsDeclared } from './sl_unthreaded.ts'
|
|
3
|
+
|
|
4
|
+
const sentence = (describes: string): string => `${describes.split(/ — |\. /)[0] ?? describes}.`
|
|
5
|
+
|
|
6
|
+
export const slopnator: Slopnator = {
|
|
7
|
+
holds: (_node, at) => {
|
|
8
|
+
const path = here(at.filename)
|
|
9
|
+
|
|
10
|
+
for (const island of islandsDeclared()) {
|
|
11
|
+
if (island.define === undefined) continue
|
|
12
|
+
const call = new RegExp(`(?<!function\\s)\\b${island.define}\\s*\\(`)
|
|
13
|
+
if (!call.test(at.text)) continue
|
|
14
|
+
|
|
15
|
+
const places = filesGoverned(island.governs)
|
|
16
|
+
if (places.includes(path)) continue
|
|
17
|
+
|
|
18
|
+
const where = places.length === 0 ? 'nowhere — its kind is derived, never authored' : places.join(' or ')
|
|
19
|
+
return (
|
|
20
|
+
`\`${island.define}\` is called here and the "${island.id}" island governs ${where}, so the loader never opens this ` +
|
|
21
|
+
`file for it and the declaration produces nothing. ${sentence(island.describes)}`
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return false
|
|
26
|
+
},
|
|
27
|
+
proves: {
|
|
28
|
+
wrong: [
|
|
29
|
+
"export default defineEvents({ specVersion: 1, events: [] })",
|
|
30
|
+
"export default definePermissions({ specVersion: 1, entities: [] })",
|
|
31
|
+
"export default defineMap({ specVersion: 1, screens: [] })",
|
|
32
|
+
],
|
|
33
|
+
right: [
|
|
34
|
+
"import events from 'app/_declare/events'",
|
|
35
|
+
"import permissions from 'app/_declare/permissions'",
|
|
36
|
+
'const screens = useRead(recentScreens)',
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { Slopnator } from '../slopnator.ts'
|
|
5
|
+
import { atsIn } from './sl_unthreaded.ts'
|
|
6
|
+
import { reached } from '../../core/descend.ts'
|
|
7
|
+
import { findTree } from '../../core/tree.ts'
|
|
8
|
+
|
|
9
|
+
const ROOT = `${findTree().at}/`
|
|
10
|
+
|
|
11
|
+
export interface Derivation {
|
|
12
|
+
readonly at: string
|
|
13
|
+
readonly by?: string
|
|
14
|
+
readonly runs?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function derivations(root: string): readonly Derivation[] {
|
|
18
|
+
const found: Derivation[] = []
|
|
19
|
+
for (const at of reached()) {
|
|
20
|
+
let text: string
|
|
21
|
+
try {
|
|
22
|
+
text = readFileSync(resolve(root, at), 'utf8')
|
|
23
|
+
} catch {
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
const threadedDerives = new Set(atsIn(text, new Set(['derives'])))
|
|
27
|
+
for (const opens of text.matchAll(/^\s*derives:\s*\[/gm)) {
|
|
28
|
+
const from = opens.index ?? 0
|
|
29
|
+
let depth = 1
|
|
30
|
+
let cursor = from + opens[0].length
|
|
31
|
+
while (cursor < text.length && depth > 0) {
|
|
32
|
+
const one = text[cursor]
|
|
33
|
+
if (one === '[') depth += 1
|
|
34
|
+
else if (one === ']') depth -= 1
|
|
35
|
+
cursor += 1
|
|
36
|
+
}
|
|
37
|
+
const own: string[] = []
|
|
38
|
+
let nesting = 0
|
|
39
|
+
for (const line of text.slice(from, cursor).split('\n')) {
|
|
40
|
+
if (nesting === 1) own.push(line)
|
|
41
|
+
for (const one of line) {
|
|
42
|
+
if (one === '[') nesting += 1
|
|
43
|
+
else if (one === ']') nesting -= 1
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const block = own.join('\n')
|
|
47
|
+
for (const row of block.split(/^\s*\{$/m)) {
|
|
48
|
+
const at = /^\s*at:\s*'([^']+)'/m.exec(row)?.[1]
|
|
49
|
+
const by = /^\s*by:\s*'([^']+)'/m.exec(row)?.[1]
|
|
50
|
+
const runs = /^\s*runs:\s*'([^']+)'/m.exec(row)?.[1]
|
|
51
|
+
if (at !== undefined && threadedDerives.has(at)) found.push({ at, by, runs })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return found
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const slopnator: Slopnator = {
|
|
59
|
+
checks: (root) => {
|
|
60
|
+
const findings: string[] = []
|
|
61
|
+
const all = derivations(root)
|
|
62
|
+
const complete = all.filter((one) => one.by !== undefined && one.runs !== undefined)
|
|
63
|
+
const covered = new Set(complete.map((one) => one.at))
|
|
64
|
+
|
|
65
|
+
for (const at of new Set(all.map((one) => one.at))) {
|
|
66
|
+
if (covered.has(at)) continue
|
|
67
|
+
findings.push(
|
|
68
|
+
`${at} sits in a \`derives\` list and no row naming it states both \`by\` and \`runs\` — a byproduct nothing can re-run is a claim, and this check used to drop such a row in silence`,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return findings
|
|
73
|
+
},
|
|
74
|
+
proves: {
|
|
75
|
+
wrong: [
|
|
76
|
+
{
|
|
77
|
+
files: [
|
|
78
|
+
{ at: 'threads/_drift_probe.ts', source: "export const M: Threaded = {\n derives: [\n {\n at: 'scripts/_generated/gate.mjs',\n by: 'pnpm gate:generate',\n runs: 'pnpm no-such-check',\n },\n ],\n}\n" },
|
|
79
|
+
],
|
|
80
|
+
at: 'threads/_drift_probe.ts',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
files: [
|
|
84
|
+
{ at: 'threads/_drift_probe.ts', source: "export const M: Threaded = {\n derives: [\n {\n at: 'scripts/finding.ts',\n },\n ],\n}\n" },
|
|
85
|
+
],
|
|
86
|
+
at: 'threads/_drift_probe.ts',
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
right: [
|
|
90
|
+
{
|
|
91
|
+
files: [
|
|
92
|
+
{ at: 'threads/_drift_probe.ts', source: "export const M: Threaded = {\n derives: [\n {\n at: 'scripts/_generated/gate.mjs',\n by: 'pnpm gate:generate',\n runs: 'pnpm gate:check',\n },\n ],\n}\n" },
|
|
93
|
+
],
|
|
94
|
+
at: 'threads/_drift_probe.ts',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import type { Slopnator } from '../slopnator.ts'
|
|
5
|
+
|
|
6
|
+
const ALONE = /^\s*([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\s*:\s*([^;{}\n]+);\s*$/
|
|
7
|
+
|
|
8
|
+
const NAMED = /var\(\s*--/
|
|
9
|
+
|
|
10
|
+
const PIECES = new Set([
|
|
11
|
+
ts.SyntaxKind.StringLiteral,
|
|
12
|
+
ts.SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
13
|
+
ts.SyntaxKind.TemplateHead,
|
|
14
|
+
ts.SyntaxKind.TemplateMiddle,
|
|
15
|
+
ts.SyntaxKind.TemplateTail,
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
export const slopnator: Slopnator = {
|
|
19
|
+
holds: (_node, at) => {
|
|
20
|
+
const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, ts.LanguageVariant.JSX, at.text)
|
|
21
|
+
let token = scanner.scan()
|
|
22
|
+
while (token !== ts.SyntaxKind.EndOfFileToken) {
|
|
23
|
+
if (PIECES.has(token)) {
|
|
24
|
+
const said = scanner.getTokenValue()
|
|
25
|
+
const found = ALONE.exec(said)
|
|
26
|
+
if (found !== null) {
|
|
27
|
+
const property = found[1] ?? ''
|
|
28
|
+
const value = (found[2] ?? '').trim()
|
|
29
|
+
if (value !== '' && !NAMED.test(value)) {
|
|
30
|
+
return (
|
|
31
|
+
`\`${property}\` is written into the compiler with \`${value}\` typed into it, so a ` +
|
|
32
|
+
'stylesheet carries a decision nothing declared — no rebrand moves it, no mode or theme reaches it, and no sheet knows it is there'
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
token = scanner.scan()
|
|
38
|
+
}
|
|
39
|
+
return false
|
|
40
|
+
},
|
|
41
|
+
proves: {
|
|
42
|
+
wrong: [
|
|
43
|
+
"lines.push(' body {', ' background: #f8fbfa;', ' }')",
|
|
44
|
+
"classes.push(' padding: 8px 16px;')",
|
|
45
|
+
"return [' :where(*) {', ' text-box-trim: trim-both;', ' }']",
|
|
46
|
+
"held.push(' color-scheme: light dark;')",
|
|
47
|
+
],
|
|
48
|
+
right: [
|
|
49
|
+
"lines.push(' body {', ' background: var(--colour-page);', ' }')",
|
|
50
|
+
'lines.push(...block(selector, made, indent))',
|
|
51
|
+
'held.push(` ${nameOf(id)}: ${made[id] ?? String()};`)',
|
|
52
|
+
'const held: readonly string[] = [];',
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { Slopnator } from '../slopnator.ts'
|
|
5
|
+
import { declaresIsland, here, rowsIn } from './sl_unthreaded.ts'
|
|
6
|
+
import { findTree } from '../../core/tree.ts'
|
|
7
|
+
|
|
8
|
+
const ROOT = findTree().at
|
|
9
|
+
const TREE = `${findTree().name}/`
|
|
10
|
+
|
|
11
|
+
function schemaAt(at: string): string | undefined {
|
|
12
|
+
const path = resolve(ROOT, at)
|
|
13
|
+
if (!existsSync(path)) return undefined
|
|
14
|
+
try {
|
|
15
|
+
return declaresIsland(readFileSync(path, 'utf8'))
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const slopnator: Slopnator = {
|
|
22
|
+
holds: (_node, at) => {
|
|
23
|
+
const path = here(at.filename)
|
|
24
|
+
if (!path.startsWith(TREE)) return false
|
|
25
|
+
|
|
26
|
+
let rows
|
|
27
|
+
try {
|
|
28
|
+
rows = rowsIn(at.text)
|
|
29
|
+
} catch {
|
|
30
|
+
return 'this file cannot be parsed, so the islands it declares were never read and nothing about what they govern was measured'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
if (row.at === undefined || row.id === undefined) continue
|
|
35
|
+
const schema = schemaAt(row.at)
|
|
36
|
+
if (schema === undefined) continue
|
|
37
|
+
if ((row.governs ?? []).length > 0) continue
|
|
38
|
+
return (
|
|
39
|
+
`island "${row.id}" states nothing it governs, so ${schema}IslandSchema is a shape nothing is held to — ` +
|
|
40
|
+
'no file is the place its kind is declared, which makes every file the place, and the island is inert'
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return false
|
|
45
|
+
},
|
|
46
|
+
proves: {
|
|
47
|
+
wrong: [
|
|
48
|
+
{
|
|
49
|
+
files: [
|
|
50
|
+
{
|
|
51
|
+
at: 'threads/_inert_probe.ts',
|
|
52
|
+
source:
|
|
53
|
+
"export const M: Islanding = {\n declares: [\n { id: 'entities', at: 'packages/core/spec/src/islands/entities.ts' },\n ],\n}\n",
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
at: 'threads/_inert_probe.ts',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
files: [
|
|
60
|
+
{
|
|
61
|
+
at: 'threads/_inert_probe.ts',
|
|
62
|
+
source:
|
|
63
|
+
"export const M: Islanding = {\n declares: [\n { id: 'events', at: 'packages/core/spec/src/islands/events.ts', governs: [] },\n ],\n}\n",
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
at: 'threads/_inert_probe.ts',
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
right: [
|
|
70
|
+
{
|
|
71
|
+
files: [
|
|
72
|
+
{
|
|
73
|
+
at: 'threads/_inert_probe.ts',
|
|
74
|
+
source:
|
|
75
|
+
"export const M: Islanding = {\n declares: [\n { id: 'events', at: 'packages/core/spec/src/islands/events.ts', governs: ['app/_declare/events.ts'] },\n ],\n}\n",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
at: 'threads/_inert_probe.ts',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
files: [
|
|
82
|
+
{
|
|
83
|
+
at: 'threads/_inert_probe.ts',
|
|
84
|
+
source: "export const M: Threaded = {\n declares: [\n { id: 'a-file', at: 'threads/thread.ts' },\n ],\n}\n",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
at: 'threads/_inert_probe.ts',
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
}
|