@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.
- package/CHANGELOG.md +193 -0
- package/dist/core/attach.js +21 -0
- package/dist/core/descend.d.ts +2 -0
- package/dist/core/descend.js +18 -3
- package/dist/core/generators/commit.js +55 -3
- package/dist/core/generators/dev.d.ts +1 -0
- package/dist/core/generators/dev.js +68 -0
- package/dist/core/rows.d.ts +5 -0
- package/dist/core/rows.js +45 -0
- package/dist/core/tree.d.ts +1 -0
- package/dist/core/tree.js +7 -1
- package/dist/derivation/generators/build.d.ts +3 -1
- package/dist/derivation/generators/build.js +103 -22
- package/dist/derivation/generators/release.js +74 -26
- package/dist/derivation/release.js +3 -3
- package/dist/integrations/cli.d.ts +3 -9
- package/dist/integrations/cli.js +96 -36
- package/dist/integrations/declared.d.ts +8 -0
- package/dist/integrations/declared.js +22 -0
- package/dist/integrations/pglite.js +13 -3
- package/dist/integrations/target.d.ts +4 -1
- package/dist/integrations/target.js +20 -6
- package/dist/integrity/attach.js +1 -5
- package/dist/integrity/generators/publish.js +2 -1
- package/dist/integrity/slopnators/sl_stale_path.js +10 -0
- package/dist/integrity/slopnators/sl_unlisted_program.js +2 -2
- package/dist/operations/access.js +3 -2
- package/dist/operations/capabilities.d.ts +2 -0
- package/dist/operations/capabilities.js +2 -0
- package/dist/operations/generators/capability.js +234 -24
- package/dist/operations/generators/compile.d.ts +21 -0
- package/dist/operations/generators/compile.js +151 -51
- package/dist/operations/generators/permits.d.ts +2 -1
- package/dist/operations/generators/permits.js +52 -47
- package/dist/operations/generators/seed.d.ts +3 -0
- package/dist/operations/generators/seed.js +25 -3
- package/dist/operations/nodes.d.ts +2 -2
- package/package.json +1 -1
- package/dist/core/actor.ts +0 -21
- package/dist/core/attach.ts +0 -613
- package/dist/core/chain.ts +0 -145
- package/dist/core/condition.ts +0 -227
- package/dist/core/descend.ts +0 -1173
- package/dist/core/errors/catalogue.ts +0 -314
- package/dist/core/errors/classes.ts +0 -51
- package/dist/core/errors/error.ts +0 -152
- package/dist/core/errors/namespaces.ts +0 -19
- package/dist/core/finding.ts +0 -37
- package/dist/core/generators/commit.ts +0 -157
- package/dist/core/init.ts +0 -222
- package/dist/core/invoked.ts +0 -52
- package/dist/core/round.ts +0 -74
- package/dist/core/rows.ts +0 -459
- package/dist/core/scope.ts +0 -20
- package/dist/core/thread.ts +0 -29
- package/dist/core/tree.ts +0 -90
- package/dist/core/types.ts +0 -351
- package/dist/derivation/attach.ts +0 -57
- package/dist/derivation/generators/build.ts +0 -371
- package/dist/derivation/generators/check.ts +0 -118
- package/dist/derivation/generators/determinism.ts +0 -145
- package/dist/derivation/generators/release.ts +0 -236
- package/dist/derivation/release.ts +0 -43
- package/dist/integrations/attach.ts +0 -187
- package/dist/integrations/cli.ts +0 -615
- package/dist/integrations/disk.ts +0 -466
- package/dist/integrations/generators/read.ts +0 -127
- package/dist/integrations/generators/vault.ts +0 -210
- package/dist/integrations/https.ts +0 -216
- package/dist/integrations/pglite.ts +0 -206
- package/dist/integrations/secret.ts +0 -54
- package/dist/integrations/target.ts +0 -656
- package/dist/integrations/vault.ts +0 -148
- package/dist/integrity/attach.ts +0 -150
- package/dist/integrity/check-threads.ts +0 -166
- package/dist/integrity/constraint.ts +0 -1246
- package/dist/integrity/generators/compiler.ts +0 -138
- package/dist/integrity/generators/gate.ts +0 -227
- package/dist/integrity/generators/issues.ts +0 -160
- package/dist/integrity/generators/publish.ts +0 -232
- package/dist/integrity/generators/trial.ts +0 -359
- package/dist/integrity/lint.ts +0 -214
- package/dist/integrity/lsp/bin.ts +0 -44
- package/dist/integrity/lsp/diagnostics.ts +0 -92
- package/dist/integrity/lsp/findings.ts +0 -63
- package/dist/integrity/lsp/lint.ts +0 -124
- package/dist/integrity/lsp/protocol.ts +0 -78
- package/dist/integrity/lsp/server.ts +0 -224
- package/dist/integrity/slopnator.ts +0 -324
- package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
- package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
- package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
- package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
- package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
- package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
- package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
- package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
- package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
- package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
- package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
- package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
- package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
- package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
- package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
- package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
- package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
- package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
- package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
- package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
- package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
- package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
- package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
- package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
- package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
- package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
- package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
- package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
- package/dist/integrity/sweep.ts +0 -395
- package/dist/knowledge/attach.ts +0 -327
- package/dist/knowledge/generators/docsgen.ts +0 -186
- package/dist/knowledge/generators/glossary.ts +0 -73
- package/dist/knowledge/glossary.ts +0 -6
- package/dist/operations/access.ts +0 -131
- package/dist/operations/attach.ts +0 -408
- package/dist/operations/capabilities.ts +0 -88
- package/dist/operations/dispatch.ts +0 -294
- package/dist/operations/error-results.ts +0 -31
- package/dist/operations/generators/capability.ts +0 -1054
- package/dist/operations/generators/compile.ts +0 -701
- package/dist/operations/generators/permits.ts +0 -224
- package/dist/operations/generators/seed.ts +0 -359
- package/dist/operations/generators/trial.ts +0 -666
- package/dist/operations/nodes.ts +0 -53
- package/dist/operations/operations.ts +0 -91
- package/dist/operations/result.ts +0 -28
|
@@ -1,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
|
-
}
|