@prestack/thread 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/dist/core/attach.js +21 -0
  3. package/dist/core/descend.d.ts +2 -0
  4. package/dist/core/descend.js +18 -3
  5. package/dist/core/generators/commit.js +55 -3
  6. package/dist/core/generators/dev.d.ts +1 -0
  7. package/dist/core/generators/dev.js +68 -0
  8. package/dist/core/rows.d.ts +5 -0
  9. package/dist/core/rows.js +45 -0
  10. package/dist/core/tree.d.ts +1 -0
  11. package/dist/core/tree.js +7 -1
  12. package/dist/derivation/generators/build.d.ts +3 -1
  13. package/dist/derivation/generators/build.js +103 -22
  14. package/dist/derivation/generators/release.js +74 -26
  15. package/dist/derivation/release.js +3 -3
  16. package/dist/integrations/cli.d.ts +3 -9
  17. package/dist/integrations/cli.js +96 -36
  18. package/dist/integrations/declared.d.ts +8 -0
  19. package/dist/integrations/declared.js +22 -0
  20. package/dist/integrations/pglite.js +13 -3
  21. package/dist/integrations/target.d.ts +4 -1
  22. package/dist/integrations/target.js +20 -6
  23. package/dist/integrity/attach.js +1 -5
  24. package/dist/integrity/generators/publish.js +2 -1
  25. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  26. package/dist/integrity/slopnators/sl_unlisted_program.js +2 -2
  27. package/dist/operations/access.js +3 -2
  28. package/dist/operations/capabilities.d.ts +2 -0
  29. package/dist/operations/capabilities.js +2 -0
  30. package/dist/operations/generators/capability.js +234 -24
  31. package/dist/operations/generators/compile.d.ts +21 -0
  32. package/dist/operations/generators/compile.js +151 -51
  33. package/dist/operations/generators/permits.d.ts +2 -1
  34. package/dist/operations/generators/permits.js +52 -47
  35. package/dist/operations/generators/seed.d.ts +3 -0
  36. package/dist/operations/generators/seed.js +25 -3
  37. package/dist/operations/nodes.d.ts +2 -2
  38. package/package.json +1 -1
  39. package/dist/core/actor.ts +0 -21
  40. package/dist/core/attach.ts +0 -613
  41. package/dist/core/chain.ts +0 -145
  42. package/dist/core/condition.ts +0 -227
  43. package/dist/core/descend.ts +0 -1173
  44. package/dist/core/errors/catalogue.ts +0 -314
  45. package/dist/core/errors/classes.ts +0 -51
  46. package/dist/core/errors/error.ts +0 -152
  47. package/dist/core/errors/namespaces.ts +0 -19
  48. package/dist/core/finding.ts +0 -37
  49. package/dist/core/generators/commit.ts +0 -157
  50. package/dist/core/init.ts +0 -222
  51. package/dist/core/invoked.ts +0 -52
  52. package/dist/core/round.ts +0 -74
  53. package/dist/core/rows.ts +0 -459
  54. package/dist/core/scope.ts +0 -20
  55. package/dist/core/thread.ts +0 -29
  56. package/dist/core/tree.ts +0 -90
  57. package/dist/core/types.ts +0 -351
  58. package/dist/derivation/attach.ts +0 -57
  59. package/dist/derivation/generators/build.ts +0 -371
  60. package/dist/derivation/generators/check.ts +0 -118
  61. package/dist/derivation/generators/determinism.ts +0 -145
  62. package/dist/derivation/generators/release.ts +0 -236
  63. package/dist/derivation/release.ts +0 -43
  64. package/dist/integrations/attach.ts +0 -187
  65. package/dist/integrations/cli.ts +0 -615
  66. package/dist/integrations/disk.ts +0 -466
  67. package/dist/integrations/generators/read.ts +0 -127
  68. package/dist/integrations/generators/vault.ts +0 -210
  69. package/dist/integrations/https.ts +0 -216
  70. package/dist/integrations/pglite.ts +0 -206
  71. package/dist/integrations/secret.ts +0 -54
  72. package/dist/integrations/target.ts +0 -656
  73. package/dist/integrations/vault.ts +0 -148
  74. package/dist/integrity/attach.ts +0 -150
  75. package/dist/integrity/check-threads.ts +0 -166
  76. package/dist/integrity/constraint.ts +0 -1246
  77. package/dist/integrity/generators/compiler.ts +0 -138
  78. package/dist/integrity/generators/gate.ts +0 -227
  79. package/dist/integrity/generators/issues.ts +0 -160
  80. package/dist/integrity/generators/publish.ts +0 -232
  81. package/dist/integrity/generators/trial.ts +0 -359
  82. package/dist/integrity/lint.ts +0 -214
  83. package/dist/integrity/lsp/bin.ts +0 -44
  84. package/dist/integrity/lsp/diagnostics.ts +0 -92
  85. package/dist/integrity/lsp/findings.ts +0 -63
  86. package/dist/integrity/lsp/lint.ts +0 -124
  87. package/dist/integrity/lsp/protocol.ts +0 -78
  88. package/dist/integrity/lsp/server.ts +0 -224
  89. package/dist/integrity/slopnator.ts +0 -324
  90. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  91. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  92. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  93. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  94. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  95. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  96. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  97. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  98. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  99. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  100. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  101. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  102. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  103. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  104. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  105. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  106. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  107. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  108. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  109. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  110. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  111. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  112. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  113. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  114. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  115. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  116. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  117. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  118. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  119. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  120. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  121. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  122. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  123. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  124. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  125. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  126. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  127. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  128. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  129. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  130. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  131. package/dist/integrity/sweep.ts +0 -395
  132. package/dist/knowledge/attach.ts +0 -327
  133. package/dist/knowledge/generators/docsgen.ts +0 -186
  134. package/dist/knowledge/generators/glossary.ts +0 -73
  135. package/dist/knowledge/glossary.ts +0 -6
  136. package/dist/operations/access.ts +0 -131
  137. package/dist/operations/attach.ts +0 -408
  138. package/dist/operations/capabilities.ts +0 -88
  139. package/dist/operations/dispatch.ts +0 -294
  140. package/dist/operations/error-results.ts +0 -31
  141. package/dist/operations/generators/capability.ts +0 -1054
  142. package/dist/operations/generators/compile.ts +0 -701
  143. package/dist/operations/generators/permits.ts +0 -224
  144. package/dist/operations/generators/seed.ts +0 -359
  145. package/dist/operations/generators/trial.ts +0 -666
  146. package/dist/operations/nodes.ts +0 -53
  147. package/dist/operations/operations.ts +0 -91
  148. package/dist/operations/result.ts +0 -28
@@ -1,224 +0,0 @@
1
- import { relative as relativeTo, resolve } from 'node:path'
2
- import { fileURLToPath, pathToFileURL } from 'node:url'
3
-
4
- import { named } from '../../core/scope.ts'
5
- import { report, SOURCE_NAME, type Diagnostic, type DiagnosticSource } from './diagnostics.ts'
6
- import { METHOD_NOT_FOUND, type RpcMessage } from './protocol.ts'
7
-
8
- const SYNC_FULL = 1
9
-
10
- export interface ServerOptions {
11
-
12
- readonly root: string
13
-
14
- readonly sourceFor: (root: string) => DiagnosticSource
15
- send(message: RpcMessage): void
16
-
17
- settle?(): Promise<void>
18
- }
19
-
20
- interface TextDocumentIdentifier {
21
- readonly uri?: unknown
22
- }
23
-
24
- interface DidOpenParams {
25
- readonly textDocument?: { readonly uri?: unknown; readonly text?: unknown }
26
- }
27
-
28
- interface DidChangeParams {
29
- readonly textDocument?: TextDocumentIdentifier
30
- readonly contentChanges?: readonly { readonly text?: unknown }[]
31
- }
32
-
33
- interface InitializeParams {
34
- readonly rootUri?: unknown
35
- readonly rootPath?: unknown
36
- readonly workspaceFolders?: readonly { readonly uri?: unknown }[] | null
37
- }
38
-
39
- export class LspServer {
40
- #root: string
41
- readonly #options: ServerOptions
42
-
43
- readonly #open = new Map<string, string>()
44
-
45
- #published = new Set<string>()
46
-
47
- #running = false
48
- #again = false
49
- #shutdownRequested = false
50
- #exited = false
51
-
52
- constructor(options: ServerOptions) {
53
- this.#options = options
54
- this.#root = options.root
55
- }
56
-
57
- get exited(): boolean {
58
- return this.#exited
59
- }
60
-
61
- get exitCode(): number {
62
- return this.#shutdownRequested ? 0 : 1
63
- }
64
-
65
- async handle(message: RpcMessage): Promise<void> {
66
-
67
- if (message.method === undefined) return
68
-
69
- switch (message.method) {
70
- case 'initialize':
71
- this.#root = rootIn(message.params as InitializeParams | undefined) ?? this.#root
72
- this.#reply(message.id, {
73
- capabilities: {
74
- textDocumentSync: { openClose: true, change: SYNC_FULL, save: true },
75
-
76
- diagnosticProvider: {
77
- identifier: SOURCE_NAME,
78
-
79
- interFileDependencies: true,
80
- workspaceDiagnostics: false,
81
- },
82
- },
83
- serverInfo: { name: named('integrity') },
84
- })
85
- return
86
-
87
- case 'initialized':
88
-
89
- return
90
-
91
- case 'shutdown':
92
- this.#shutdownRequested = true
93
- this.#reply(message.id, null)
94
- return
95
-
96
- case 'exit':
97
- this.#exited = true
98
- return
99
-
100
- case 'textDocument/didOpen': {
101
- const params = message.params as DidOpenParams | undefined
102
- const path = this.#pathOf(params?.textDocument?.uri)
103
- if (path === undefined) return
104
- this.#open.set(path, typeof params?.textDocument?.text === 'string' ? params.textDocument.text : '')
105
- await this.#recompute()
106
- return
107
- }
108
-
109
- case 'textDocument/didChange': {
110
- const params = message.params as DidChangeParams | undefined
111
- const path = this.#pathOf(params?.textDocument?.uri)
112
- if (path === undefined) return
113
-
114
- const latest = params?.contentChanges?.at(-1)?.text
115
- if (typeof latest === 'string') this.#open.set(path, latest)
116
- await this.#recompute()
117
- return
118
- }
119
-
120
- case 'textDocument/didSave':
121
- await this.#recompute()
122
- return
123
-
124
- case 'textDocument/didClose': {
125
- const path = this.#pathOf((message.params as DidOpenParams | undefined)?.textDocument?.uri)
126
- if (path === undefined) return
127
- this.#open.delete(path)
128
- await this.#recompute()
129
- return
130
- }
131
-
132
- case 'textDocument/diagnostic': {
133
- const path = this.#pathOf((message.params as DidOpenParams | undefined)?.textDocument?.uri)
134
- const computed = await report(this.#sourceForRoot(), this.#open, this.#published)
135
- this.#published = new Set([...computed].filter(([, list]) => list.length > 0).map(([file]) => file))
136
- this.#reply(message.id, {
137
- kind: 'full',
138
- items: path === undefined ? [] : (computed.get(path) ?? []),
139
- })
140
- return
141
- }
142
-
143
- default:
144
-
145
- if (message.id !== undefined && message.id !== null) {
146
- this.#options.send({
147
- jsonrpc: '2.0',
148
- id: message.id,
149
- error: { code: METHOD_NOT_FOUND, message: `${message.method} is not implemented by ${SOURCE_NAME}` },
150
- })
151
- }
152
- }
153
- }
154
-
155
- #sourceForRoot(): DiagnosticSource {
156
- return this.#options.sourceFor(this.#root)
157
- }
158
-
159
- #reply(id: RpcMessage['id'], result: unknown): void {
160
- if (id === undefined || id === null) return
161
- this.#options.send({ jsonrpc: '2.0', id, result })
162
- }
163
-
164
- async #recompute(): Promise<void> {
165
- if (this.#running) {
166
- this.#again = true
167
- return
168
- }
169
-
170
- this.#running = true
171
- try {
172
- do {
173
- this.#again = false
174
- await (this.#options.settle?.() ?? sleep(200))
175
- const computed = await report(this.#sourceForRoot(), this.#open, this.#published)
176
-
177
- const nonEmpty = new Set<string>()
178
- for (const [path, diagnostics] of computed) {
179
- this.#publish(path, diagnostics)
180
- if (diagnostics.length > 0) nonEmpty.add(path)
181
- }
182
- this.#published = nonEmpty
183
- } while (this.#again)
184
- } finally {
185
- this.#running = false
186
- }
187
- }
188
-
189
- #publish(path: string, diagnostics: readonly Diagnostic[]): void {
190
- this.#options.send({
191
- jsonrpc: '2.0',
192
- method: 'textDocument/publishDiagnostics',
193
- params: { uri: pathToFileURL(resolve(this.#root, path)).href, diagnostics },
194
- })
195
- }
196
-
197
- #pathOf(uri: unknown): string | undefined {
198
- if (typeof uri !== 'string' || !uri.startsWith('file://')) return undefined
199
- let absolute: string
200
- try {
201
- absolute = fileURLToPath(uri)
202
- } catch {
203
- return undefined
204
- }
205
- const path = relativeTo(this.#root, absolute).split('\\').join('/')
206
-
207
- return path.startsWith('..') || path.startsWith('/') ? undefined : path
208
- }
209
- }
210
-
211
- const sleep = (ms: number): Promise<void> => new Promise((done) => setTimeout(done, ms))
212
-
213
- function rootIn(params: InitializeParams | undefined): string | undefined {
214
- const folder = params?.workspaceFolders?.[0]?.uri
215
- for (const uri of [folder, params?.rootUri]) {
216
- if (typeof uri !== 'string' || !uri.startsWith('file://')) continue
217
- try {
218
- return fileURLToPath(uri)
219
- } catch {
220
- continue
221
- }
222
- }
223
- return typeof params?.rootPath === 'string' ? params.rootPath : undefined
224
- }
@@ -1,324 +0,0 @@
1
- import { existsSync, readFileSync, readdirSync, writeFileSync } from '../integrations/disk.ts'
2
- import { dirname, join } from 'node:path'
3
- import { fileURLToPath, pathToFileURL } from 'node:url'
4
-
5
- import { descend } from '../core/descend.ts'
6
- import { governs as reaches, islandsDeclared } from './slopnators/sl_unthreaded.ts'
7
- import { invoked } from '../core/invoked.ts'
8
-
9
- import { listBounds, quoted, withSeed } from '../core/rows.ts'
10
-
11
- import { CONSTRAINTS, type Constraint, type Proof } from './constraint.ts'
12
- import { findTree, inTree } from '../core/tree.ts'
13
-
14
- export const NOT_WRITTEN = Symbol.for('thread.slopnator.not_written')
15
-
16
- const HOME = dirname(fileURLToPath(import.meta.url))
17
-
18
- export const SHIPPED = `${HOME}/slopnators`
19
-
20
- export const SEEDED = `${findTree().name}/slopnators`
21
-
22
- function homes(root: string): readonly string[] {
23
- return [SHIPPED, join(root, SEEDED)]
24
- }
25
-
26
- export const ROOT = `${findTree().at}/`
27
-
28
- export interface SlopnatorContext {
29
- readonly filename: string
30
- readonly text: string
31
- readonly ancestors: () => readonly unknown[]
32
- readonly comments: () => readonly Spoken[]
33
- }
34
-
35
- export interface Spoken {
36
- readonly value: string
37
- readonly range: readonly [number, number]
38
- }
39
-
40
- export interface Proves {
41
- readonly wrong: readonly Proof[]
42
- readonly right: readonly Proof[]
43
- }
44
-
45
- export interface Reads {
46
- readonly holds: (node: unknown, at: SlopnatorContext) => boolean | string | typeof NOT_WRITTEN
47
- readonly fixes?: (node: unknown, at: SlopnatorContext) => string | undefined
48
- readonly proves: Proves
49
- }
50
-
51
- export interface Reading extends Reads {
52
- readonly on?: readonly string[]
53
- }
54
-
55
- export interface Running {
56
- readonly checks: (root: string) => readonly string[]
57
- readonly proves: Proves
58
- }
59
-
60
- export type Slopnator = Reading | Running
61
-
62
- export type Visiting = Reading & { readonly on: readonly string[] }
63
-
64
- export const reads = (one: Slopnator): one is Reading => 'holds' in one
65
-
66
- export const visits = (one: Slopnator): one is Visiting => 'holds' in one && one.on !== undefined
67
-
68
- export const sweeps = (one: Slopnator): one is Reading => 'holds' in one && one.on === undefined
69
-
70
- export interface Byproduct {
71
- readonly path: string
72
- readonly content: string
73
- }
74
-
75
- export function slopnatorName(rule: string): string {
76
- return `sl_${rule
77
- .replace(/[^a-zA-Z0-9]+/g, '_')
78
- .replace(/^_+|_+$/g, '')
79
- .toLowerCase()}`
80
- }
81
-
82
- const CANONICAL_TREE = 'threads'
83
-
84
- const TREE = findTree().name
85
-
86
- function parts(one: string): readonly string[] {
87
- return one.split('\\').join('/').split('/').filter((part) => part !== '')
88
- }
89
-
90
- function wanted(fragment: string): readonly string[] {
91
- const all = parts(fragment)
92
- return all[0] === CANONICAL_TREE ? [TREE, ...all.slice(1)] : all
93
- }
94
-
95
- function holding(want: readonly string[], path: readonly string[], from: number): boolean {
96
- return want.every((part, step) => part === path[from + step])
97
- }
98
-
99
- export function region(fragment: string, path: readonly string[]): boolean {
100
- if (fragment === '.') return true
101
- const want = wanted(fragment)
102
- if (want.length === 0) return false
103
- if (!fragment.endsWith('/')) return want.join('/') === path.join('/')
104
- return want.length < path.length && holding(want, path, 0)
105
- }
106
-
107
- let written: ReadonlySet<string> | undefined
108
-
109
- export function derivedHere(): ReadonlySet<string> | undefined {
110
- if (written !== undefined) return written
111
- try {
112
- const found = new Set<string>()
113
- for (const [at, threadings] of descend().threaded) {
114
- if (threadings.some((one) => one.list === 'derives' && (one.row.by ?? one.row.runs) !== undefined)) found.add(at)
115
- }
116
- written = found
117
- return written
118
- } catch {
119
- return undefined
120
- }
121
- }
122
-
123
- const islands = new WeakMap<object, ReadonlyMap<string, readonly string[]>>()
124
-
125
- function islandsHere(): ReadonlyMap<string, readonly string[]> | undefined {
126
- try {
127
- const pass = descend()
128
- const known = islands.get(pass)
129
- if (known !== undefined) return known
130
- const named = new Map(islandsDeclared().map((one) => [one.id, one.governs]))
131
- islands.set(pass, named)
132
- return named
133
- } catch {
134
- return undefined
135
- }
136
- }
137
-
138
- export function subject(rule: Constraint): boolean {
139
- if (rule.governs === undefined) return true
140
- const named = islandsHere()
141
- return named === undefined || named.has(rule.governs)
142
- }
143
-
144
- export function governs(rule: Constraint, filename: string): boolean {
145
- const under = filename.startsWith(ROOT) ? filename.slice(ROOT.length) : filename
146
- const path = parts(under)
147
- if (rule.governs !== undefined) {
148
- const body = islandsHere()?.get(rule.governs)
149
- if (body !== undefined && reaches(body, under)) return false
150
- }
151
- if (!rule.where.paths.some((fragment) => region(fragment, path))) return false
152
- return derivedHere()?.has(under) !== true
153
- }
154
-
155
- export class UnloadableSlopnatorError extends Error {
156
- constructor(rule: string, at: string, because: string) {
157
- super(
158
- `rule "${rule}" names a slopnator at ${at}, the file is on disk, and loading it threw: ${because}. ` +
159
- 'A slopnated that cannot be imported reads from every caller exactly like a slopnator that found nothing — ' +
160
- 'the sweep skips it, the trial falls through, and the rule reports green while enforcing nothing. ' +
161
- 'Fix the import, or delete the rule.',
162
- )
163
- this.name = 'UnloadableSlopnatorError'
164
- }
165
- }
166
-
167
- export class UnreadableRulesError extends Error {
168
- constructor(at: string, because: string) {
169
- super(
170
- `${at} is where this tree states the rules it authors, and reading it threw: ${because}. ` +
171
- '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. ' +
172
- 'Fix the file, or delete it if this tree authors no rules of its own.',
173
- )
174
- this.name = 'UnreadableRulesError'
175
- }
176
- }
177
-
178
- export async function load(root: string, all: readonly Constraint[]): Promise<ReadonlyMap<string, Slopnator>> {
179
- const found = new Map<string, Slopnator>()
180
- for (const rule of all) {
181
- const name = slopnatorName(rule.id)
182
- const at = homes(root)
183
- .flatMap((dir) => [join(dir, `${name}.js`), join(dir, `${name}.ts`)])
184
- .find((one) => existsSync(one))
185
- if (at === undefined) continue
186
-
187
- let module: unknown
188
- try {
189
- module = await import(pathToFileURL(at).href)
190
- } catch (thrown) {
191
- throw new UnloadableSlopnatorError(rule.id, at, thrown instanceof Error ? thrown.message : String(thrown))
192
- }
193
-
194
- const one = (module as { slopnator?: Slopnator }).slopnator
195
- if (one === undefined) {
196
- throw new UnloadableSlopnatorError(rule.id, at, 'it exports no `slopnator`')
197
- }
198
- found.set(name, one)
199
- }
200
- return found
201
- }
202
-
203
- export function proofsOf(
204
- rule: Constraint,
205
- slopnators: ReadonlyMap<string, Slopnator>,
206
- ): { readonly wrong: readonly Proof[]; readonly right: readonly Proof[] } {
207
- const slopnator = slopnators.get(slopnatorName(rule.id))
208
- const slopnated = slopnator?.proves
209
- const own = (slopnated?.wrong.length ?? 0) + (slopnated?.right.length ?? 0)
210
- if (slopnator !== undefined && !reads(slopnator) && own > 0) {
211
- return { wrong: slopnated?.wrong ?? [], right: slopnated?.right ?? [] }
212
- }
213
- return { wrong: [rule.wrong, ...(slopnated?.wrong ?? [])], right: [rule.right, ...(slopnated?.right ?? [])] }
214
- }
215
-
216
- export function scaffold(): string {
217
- return `import { NOT_WRITTEN, type Slopnator } from './slopnator.ts'
218
-
219
- export const slopnator: Slopnator = {
220
- on: [],
221
- holds: () => NOT_WRITTEN,
222
- proves: { wrong: [], right: [] },
223
- }
224
- `
225
- }
226
-
227
- export function write(root: string, byproduct: Byproduct): void {
228
- const path = join(root, byproduct.path)
229
- writeFileSync(path, byproduct.content, 'utf8')
230
- }
231
-
232
- export function onDisk(root: string): readonly string[] {
233
- const found = new Set<string>()
234
- for (const dir of homes(root)) {
235
- if (!existsSync(dir)) continue
236
- for (const file of readdirSync(dir)) {
237
- if (file.startsWith('sl_') && file.endsWith('.ts')) found.add(file.replace(/\.ts$/, ''))
238
- }
239
- }
240
- return [...found].sort()
241
- }
242
-
243
- export function named(all: readonly Constraint[]): readonly string[] {
244
- return all.map((rule) => slopnatorName(rule.id)).sort()
245
- }
246
-
247
- export async function rules(): Promise<readonly Constraint[] | undefined> {
248
- const at = inTree(findTree(), 'rules.ts')
249
- if (!existsSync(at)) return undefined
250
- let module: { RULES?: readonly Constraint[] }
251
- try {
252
- module = (await import(pathToFileURL(at).href)) as { RULES?: readonly Constraint[] }
253
- } catch (thrown) {
254
- throw new UnreadableRulesError(at, thrown instanceof Error ? thrown.message : String(thrown))
255
- }
256
- if (module.RULES === undefined) throw new UnreadableRulesError(at, 'it exports no `RULES`')
257
- return module.RULES
258
- }
259
-
260
- export function seeds(root: string, all: readonly Constraint[]): readonly Byproduct[] {
261
- const found = onDisk(root)
262
- return named(all)
263
- .filter((wanted) => !found.includes(wanted))
264
- .map((wanted) => ({ path: `${SEEDED}/${wanted}.ts`, content: scaffold() }))
265
- }
266
-
267
- export const GENERALLY =
268
- '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.'
269
-
270
- export function declaring(at: string, says: string): string {
271
- return [
272
- ' {',
273
- " id: 'slopnator',",
274
- ` at: '${at}',`,
275
- ` describes: ${quoted(says)},`,
276
- ' },',
277
- ].join('\n')
278
- }
279
-
280
- export async function declareSeeded(root: string, ids: readonly string[], says?: string): Promise<readonly string[]> {
281
- if (ids.length === 0) return []
282
- const at = inTree(findTree(), 'rules.ts')
283
- let text: string
284
- try {
285
- text = readFileSync(at, 'utf8')
286
- } catch {
287
- return []
288
- }
289
- const where = listBounds(text, 'RULES')
290
- if (where === undefined) return []
291
-
292
- const done: string[] = []
293
- for (const id of ids) {
294
- const next = withSeed(
295
- text,
296
- listBounds(text, 'RULES') ?? where,
297
- id,
298
- declaring(`${SEEDED}/${slopnatorName(id)}.ts`, says ?? GENERALLY),
299
- )
300
- if (next === text) continue
301
- text = next
302
- done.push(id)
303
- }
304
- if (done.length > 0) writeFileSync(at, text, 'utf8')
305
- return done
306
- }
307
-
308
- if (invoked(import.meta.url)) {
309
- const own = (await rules()) ?? []
310
- const seeded = seeds(ROOT, own)
311
- for (const byproduct of seeded) {
312
- write(ROOT, byproduct)
313
- console.log(`slopnator: seeded ${byproduct.path}`)
314
- }
315
-
316
- const held = new Set(onDisk(ROOT))
317
- const declared = await declareSeeded(ROOT, own.filter((rule) => held.has(slopnatorName(rule.id))).map((rule) => rule.id))
318
- for (const id of declared) console.log(`slopnator: declared the slopnator for "${id}" in its own row`)
319
-
320
- console.log(
321
- `slopnator: ${String(own.length)} rule(s), ${String(seeded.length)} seeded, ${String(declared.length)} declared. ` +
322
- `${String(CONSTRAINTS.length)} shipped constraint(s) are authored, not seeded — unpaired-slopnator refuses one whose slopnated is missing.`,
323
- )
324
- }
@@ -1,38 +0,0 @@
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: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
28
- { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_names_it.ts',\n },\n ],\n}\n" },
29
- { at: 'threads/_names_it.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const OWNER: Threaded = {\n declares: [\n {\n at: 'scripts/_probe_mid.ts',\n },\n ],\n}\n" },
30
- { at: 'scripts/_probe_mid.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const MID: Threaded = {\n declares: [\n {\n at: 'scripts/_probe_end.ts',\n },\n ],\n}\n" },
31
- { at: 'scripts/_probe_end.ts', source: 'export const END = 1\n' },
32
- ],
33
- at: 'scripts/_probe_mid.ts',
34
- },
35
- 'export const PLAIN = 1',
36
- ],
37
- },
38
- }
@@ -1,60 +0,0 @@
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
- {
30
- files: [
31
- { at: 'threads/thread.ts', source: "export const THREAD: Threaded = {\n declares: [\n { id: 'events', at: 'src/islands/events.ts', governs: ['app/_declare/events.ts'] },\n ],\n}\n" },
32
- { at: 'src/islands/events.ts', source: 'export const EventsIslandSchema = {}\nexport function defineEvents() {}\n' },
33
- { at: 'probe.tsx', source: 'export default defineEvents({ specVersion: 1, events: [] })\n' },
34
- ],
35
- at: 'probe.tsx',
36
- },
37
- {
38
- files: [
39
- { at: 'threads/thread.ts', source: "export const THREAD: Threaded = {\n declares: [\n { id: 'permissions', at: 'src/islands/permissions.ts', governs: ['app/_declare/permissions.ts'] },\n ],\n}\n" },
40
- { at: 'src/islands/permissions.ts', source: 'export const PermissionsIslandSchema = {}\nexport function definePermissions() {}\n' },
41
- { at: 'probe.tsx', source: 'export default definePermissions({ specVersion: 1, entities: [] })\n' },
42
- ],
43
- at: 'probe.tsx',
44
- },
45
- {
46
- files: [
47
- { at: 'threads/thread.ts', source: "export const THREAD: Threaded = {\n declares: [\n { id: 'navigation', at: 'src/islands/navigation.ts', governs: ['app/_declare/map.ts'] },\n ],\n}\n" },
48
- { at: 'src/islands/navigation.ts', source: 'export const NavigationIslandSchema = {}\nexport function defineMap() {}\n' },
49
- { at: 'probe.tsx', source: 'export default defineMap({ specVersion: 1, screens: [] })\n' },
50
- ],
51
- at: 'probe.tsx',
52
- },
53
- ],
54
- right: [
55
- "import events from 'app/_declare/events'",
56
- "import permissions from 'app/_declare/permissions'",
57
- 'const screens = useRead(recentScreens)',
58
- ],
59
- },
60
- }