@prestack/thread 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/core/actor.ts +20 -0
  2. package/dist/core/asking.d.ts +0 -1
  3. package/dist/core/asking.js +0 -1
  4. package/dist/core/asking.ts +1342 -0
  5. package/dist/core/attach.js +2 -2
  6. package/dist/core/attach.ts +585 -0
  7. package/dist/core/chain.js +8 -1
  8. package/dist/core/chain.ts +148 -0
  9. package/dist/core/cli.js +97 -18
  10. package/dist/core/cli.ts +2004 -0
  11. package/dist/core/condition.ts +227 -0
  12. package/dist/core/descend.d.ts +1 -0
  13. package/dist/core/descend.js +36 -0
  14. package/dist/core/descend.ts +1001 -0
  15. package/dist/core/finding.ts +38 -0
  16. package/dist/core/generators/meta.js +6 -0
  17. package/dist/core/generators/meta.ts +1006 -0
  18. package/dist/core/init.ts +170 -0
  19. package/dist/core/invoked.ts +47 -0
  20. package/dist/core/round.ts +57 -0
  21. package/dist/core/rows.ts +369 -0
  22. package/dist/core/scope.ts +18 -0
  23. package/dist/core/thread.ts +29 -0
  24. package/dist/core/tree.ts +67 -0
  25. package/dist/core/types.ts +350 -0
  26. package/dist/derivation/attach.js +1 -1
  27. package/dist/derivation/attach.ts +68 -0
  28. package/dist/derivation/generators/build.ts +232 -0
  29. package/dist/derivation/generators/check.ts +118 -0
  30. package/dist/derivation/generators/derivation.ts +347 -0
  31. package/dist/derivation/generators/determinism.ts +145 -0
  32. package/dist/derivation/generators/release.js +19 -9
  33. package/dist/derivation/generators/release.ts +213 -0
  34. package/dist/derivation/makers/release.ts +135 -0
  35. package/dist/derivation/release.ts +23 -0
  36. package/dist/integrations/attach.js +2 -2
  37. package/dist/integrations/attach.ts +207 -0
  38. package/dist/integrations/generators/read.ts +127 -0
  39. package/dist/integrations/generators/secret.ts +289 -0
  40. package/dist/integrations/generators/target.js +16 -6
  41. package/dist/integrations/generators/target.ts +944 -0
  42. package/dist/integrations/generators/vault.js +46 -14
  43. package/dist/integrations/generators/vault.ts +212 -0
  44. package/dist/integrations/{sdk.js → module.js} +4 -4
  45. package/dist/integrations/module.ts +189 -0
  46. package/dist/integrations/secret.ts +50 -0
  47. package/dist/integrations/target.d.ts +3 -1
  48. package/dist/integrations/target.js +14 -11
  49. package/dist/integrations/target.ts +340 -0
  50. package/dist/integrations/vault.d.ts +2 -0
  51. package/dist/integrations/vault.js +4 -2
  52. package/dist/integrations/vault.ts +136 -0
  53. package/dist/integrity/attach.ts +234 -0
  54. package/dist/integrity/check-threads.js +2 -26
  55. package/dist/integrity/check-threads.ts +166 -0
  56. package/dist/integrity/constraint.ts +1202 -0
  57. package/dist/integrity/generators/compiler.ts +161 -0
  58. package/dist/integrity/generators/gate.ts +201 -0
  59. package/dist/integrity/generators/issues.ts +160 -0
  60. package/dist/integrity/generators/publish.js +14 -5
  61. package/dist/integrity/generators/publish.ts +247 -0
  62. package/dist/integrity/generators/rule.ts +271 -0
  63. package/dist/integrity/generators/seal.ts +117 -0
  64. package/dist/integrity/generators/trial.ts +320 -0
  65. package/dist/integrity/lint.ts +227 -0
  66. package/dist/integrity/lsp/bin.ts +42 -0
  67. package/dist/integrity/lsp/diagnostics.ts +92 -0
  68. package/dist/integrity/lsp/findings.ts +63 -0
  69. package/dist/integrity/lsp/lint.ts +140 -0
  70. package/dist/integrity/lsp/protocol.ts +78 -0
  71. package/dist/integrity/lsp/server.ts +224 -0
  72. package/dist/integrity/sealed.js +2 -2
  73. package/dist/integrity/sealed.ts +56 -0
  74. package/dist/integrity/slopnator.ts +350 -0
  75. package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
  76. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
  77. package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
  78. package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
  79. package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
  80. package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
  81. package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
  82. package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
  83. package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
  84. package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
  85. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
  86. package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
  87. package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
  88. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
  89. package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
  90. package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
  91. package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
  92. package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
  93. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
  94. package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
  95. package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
  96. package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
  97. package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
  98. package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
  99. package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
  100. package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
  101. package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
  102. package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
  103. package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
  104. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
  105. package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
  106. package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
  107. package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
  108. package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
  109. package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
  110. package/dist/integrity/sweep.ts +307 -0
  111. package/dist/knowledge/attach.ts +350 -0
  112. package/dist/knowledge/generators/docsgen.ts +174 -0
  113. package/dist/knowledge/generators/glossary.ts +73 -0
  114. package/dist/knowledge/generators/skills.ts +263 -0
  115. package/dist/knowledge/glossary.ts +6 -0
  116. package/dist/operations/access.ts +88 -0
  117. package/dist/operations/attach.ts +475 -0
  118. package/dist/operations/capabilities.ts +65 -0
  119. package/dist/operations/cost.ts +58 -0
  120. package/dist/operations/dispatch.ts +348 -0
  121. package/dist/operations/error-results.ts +31 -0
  122. package/dist/operations/errors/catalog.ts +168 -0
  123. package/dist/operations/errors/classes.ts +51 -0
  124. package/dist/operations/errors/errors.ts +314 -0
  125. package/dist/operations/generators/capability.js +29 -19
  126. package/dist/operations/generators/capability.ts +704 -0
  127. package/dist/operations/generators/compile.js +20 -1
  128. package/dist/operations/generators/compile.ts +643 -0
  129. package/dist/operations/generators/grant.ts +336 -0
  130. package/dist/operations/generators/operation.ts +972 -0
  131. package/dist/operations/generators/permits.ts +223 -0
  132. package/dist/operations/generators/seed.ts +347 -0
  133. package/dist/operations/generators/trial.js +4 -1
  134. package/dist/operations/generators/trial.ts +529 -0
  135. package/dist/operations/http.ts +164 -0
  136. package/dist/operations/makers/capability.ts +166 -0
  137. package/dist/operations/makers/trial.ts +520 -0
  138. package/dist/operations/memory.ts +470 -0
  139. package/dist/operations/metering.ts +328 -0
  140. package/dist/operations/nodes.ts +54 -0
  141. package/dist/operations/operations.ts +32 -0
  142. package/dist/operations/queue-policy.ts +10 -0
  143. package/dist/operations/result.ts +28 -0
  144. package/dist/operations/seams.d.ts +4 -4
  145. package/dist/operations/seams.ts +364 -0
  146. package/dist/operations/store.ts +29 -0
  147. package/package.json +8 -1
  148. package/dist/integrations/generators/sdk.d.ts +0 -1
  149. package/dist/integrations/generators/sdk.js +0 -148
  150. /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
+ }