@prestack/thread 0.1.21 → 0.1.23

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 (138) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/dist/core/attach.js +21 -0
  3. package/dist/core/descend.js +6 -3
  4. package/dist/core/generators/commit.js +55 -3
  5. package/dist/core/generators/dev.d.ts +1 -0
  6. package/dist/core/generators/dev.js +69 -0
  7. package/dist/derivation/generators/build.d.ts +3 -1
  8. package/dist/derivation/generators/build.js +104 -22
  9. package/dist/derivation/generators/release.js +74 -25
  10. package/dist/derivation/release.js +3 -3
  11. package/dist/integrations/cli.d.ts +0 -1
  12. package/dist/integrations/cli.js +69 -11
  13. package/dist/integrations/declared.d.ts +2 -0
  14. package/dist/integrations/declared.js +6 -1
  15. package/dist/integrations/pglite.js +13 -3
  16. package/dist/integrations/target.d.ts +3 -0
  17. package/dist/integrity/attach.js +1 -5
  18. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  19. package/dist/operations/access.js +3 -2
  20. package/dist/operations/capabilities.d.ts +2 -0
  21. package/dist/operations/capabilities.js +2 -0
  22. package/dist/operations/generators/capability.js +237 -24
  23. package/dist/operations/generators/compile.js +150 -50
  24. package/dist/operations/generators/seed.d.ts +3 -0
  25. package/dist/operations/generators/seed.js +25 -3
  26. package/dist/operations/nodes.d.ts +2 -2
  27. package/package.json +1 -1
  28. package/dist/core/actor.ts +0 -21
  29. package/dist/core/attach.ts +0 -613
  30. package/dist/core/chain.ts +0 -145
  31. package/dist/core/condition.ts +0 -227
  32. package/dist/core/descend.ts +0 -1185
  33. package/dist/core/errors/catalogue.ts +0 -314
  34. package/dist/core/errors/classes.ts +0 -51
  35. package/dist/core/errors/error.ts +0 -152
  36. package/dist/core/errors/namespaces.ts +0 -19
  37. package/dist/core/finding.ts +0 -37
  38. package/dist/core/generators/commit.ts +0 -157
  39. package/dist/core/init.ts +0 -222
  40. package/dist/core/invoked.ts +0 -52
  41. package/dist/core/round.ts +0 -74
  42. package/dist/core/rows.ts +0 -506
  43. package/dist/core/scope.ts +0 -20
  44. package/dist/core/thread.ts +0 -29
  45. package/dist/core/tree.ts +0 -90
  46. package/dist/core/types.ts +0 -351
  47. package/dist/derivation/attach.ts +0 -57
  48. package/dist/derivation/generators/build.ts +0 -371
  49. package/dist/derivation/generators/check.ts +0 -118
  50. package/dist/derivation/generators/determinism.ts +0 -145
  51. package/dist/derivation/generators/release.ts +0 -236
  52. package/dist/derivation/release.ts +0 -43
  53. package/dist/integrations/attach.ts +0 -187
  54. package/dist/integrations/cli.ts +0 -606
  55. package/dist/integrations/declared.ts +0 -34
  56. package/dist/integrations/disk.ts +0 -466
  57. package/dist/integrations/generators/read.ts +0 -127
  58. package/dist/integrations/generators/vault.ts +0 -210
  59. package/dist/integrations/https.ts +0 -216
  60. package/dist/integrations/pglite.ts +0 -206
  61. package/dist/integrations/secret.ts +0 -54
  62. package/dist/integrations/target.ts +0 -672
  63. package/dist/integrations/vault.ts +0 -148
  64. package/dist/integrity/attach.ts +0 -150
  65. package/dist/integrity/check-threads.ts +0 -166
  66. package/dist/integrity/constraint.ts +0 -1246
  67. package/dist/integrity/generators/compiler.ts +0 -138
  68. package/dist/integrity/generators/gate.ts +0 -227
  69. package/dist/integrity/generators/issues.ts +0 -160
  70. package/dist/integrity/generators/publish.ts +0 -233
  71. package/dist/integrity/generators/trial.ts +0 -359
  72. package/dist/integrity/lint.ts +0 -214
  73. package/dist/integrity/lsp/bin.ts +0 -44
  74. package/dist/integrity/lsp/diagnostics.ts +0 -92
  75. package/dist/integrity/lsp/findings.ts +0 -63
  76. package/dist/integrity/lsp/lint.ts +0 -124
  77. package/dist/integrity/lsp/protocol.ts +0 -78
  78. package/dist/integrity/lsp/server.ts +0 -224
  79. package/dist/integrity/slopnator.ts +0 -324
  80. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  81. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  82. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  83. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  84. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  85. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  86. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  87. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  88. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  89. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  90. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  91. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  92. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  93. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  94. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  95. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  96. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  97. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  98. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  99. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  100. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  101. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  102. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  103. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  104. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  105. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  106. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  107. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  108. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  109. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  110. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  111. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  112. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  113. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  114. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  115. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  116. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  117. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  118. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  119. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  120. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  121. package/dist/integrity/sweep.ts +0 -395
  122. package/dist/knowledge/attach.ts +0 -327
  123. package/dist/knowledge/generators/docsgen.ts +0 -186
  124. package/dist/knowledge/generators/glossary.ts +0 -73
  125. package/dist/knowledge/glossary.ts +0 -6
  126. package/dist/operations/access.ts +0 -131
  127. package/dist/operations/attach.ts +0 -408
  128. package/dist/operations/capabilities.ts +0 -88
  129. package/dist/operations/dispatch.ts +0 -294
  130. package/dist/operations/error-results.ts +0 -31
  131. package/dist/operations/generators/capability.ts +0 -1054
  132. package/dist/operations/generators/compile.ts +0 -701
  133. package/dist/operations/generators/permits.ts +0 -229
  134. package/dist/operations/generators/seed.ts +0 -359
  135. package/dist/operations/generators/trial.ts +0 -666
  136. package/dist/operations/nodes.ts +0 -53
  137. package/dist/operations/operations.ts +0 -91
  138. 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
- }