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