@prestack/thread 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/actor.ts +20 -0
- package/dist/core/asking.d.ts +0 -1
- package/dist/core/asking.js +0 -1
- package/dist/core/asking.ts +1342 -0
- package/dist/core/attach.js +2 -2
- package/dist/core/attach.ts +585 -0
- package/dist/core/chain.js +8 -1
- package/dist/core/chain.ts +148 -0
- package/dist/core/cli.js +97 -18
- package/dist/core/cli.ts +2004 -0
- package/dist/core/condition.ts +227 -0
- package/dist/core/descend.d.ts +1 -0
- package/dist/core/descend.js +36 -0
- package/dist/core/descend.ts +1001 -0
- package/dist/core/finding.ts +38 -0
- package/dist/core/generators/meta.js +6 -0
- package/dist/core/generators/meta.ts +1006 -0
- package/dist/core/init.ts +170 -0
- package/dist/core/invoked.ts +47 -0
- package/dist/core/round.ts +57 -0
- package/dist/core/rows.ts +369 -0
- package/dist/core/scope.ts +18 -0
- package/dist/core/thread.ts +29 -0
- package/dist/core/tree.ts +67 -0
- package/dist/core/types.ts +350 -0
- package/dist/derivation/attach.js +1 -1
- package/dist/derivation/attach.ts +68 -0
- package/dist/derivation/generators/build.ts +232 -0
- package/dist/derivation/generators/check.ts +118 -0
- package/dist/derivation/generators/derivation.ts +347 -0
- package/dist/derivation/generators/determinism.ts +145 -0
- package/dist/derivation/generators/release.js +19 -9
- package/dist/derivation/generators/release.ts +213 -0
- package/dist/derivation/makers/release.ts +135 -0
- package/dist/derivation/release.ts +23 -0
- package/dist/integrations/attach.js +2 -2
- package/dist/integrations/attach.ts +207 -0
- package/dist/integrations/generators/read.ts +127 -0
- package/dist/integrations/generators/secret.ts +289 -0
- package/dist/integrations/generators/target.js +16 -6
- package/dist/integrations/generators/target.ts +944 -0
- package/dist/integrations/generators/vault.js +46 -14
- package/dist/integrations/generators/vault.ts +212 -0
- package/dist/integrations/{sdk.js → module.js} +4 -4
- package/dist/integrations/module.ts +189 -0
- package/dist/integrations/secret.ts +50 -0
- package/dist/integrations/target.d.ts +3 -1
- package/dist/integrations/target.js +14 -11
- package/dist/integrations/target.ts +340 -0
- package/dist/integrations/vault.d.ts +2 -0
- package/dist/integrations/vault.js +4 -2
- package/dist/integrations/vault.ts +136 -0
- package/dist/integrity/attach.ts +234 -0
- package/dist/integrity/check-threads.js +2 -26
- package/dist/integrity/check-threads.ts +166 -0
- package/dist/integrity/constraint.ts +1202 -0
- package/dist/integrity/generators/compiler.ts +161 -0
- package/dist/integrity/generators/gate.ts +201 -0
- package/dist/integrity/generators/issues.ts +160 -0
- package/dist/integrity/generators/publish.js +14 -5
- package/dist/integrity/generators/publish.ts +247 -0
- package/dist/integrity/generators/rule.ts +271 -0
- package/dist/integrity/generators/seal.ts +117 -0
- package/dist/integrity/generators/trial.ts +320 -0
- package/dist/integrity/lint.ts +227 -0
- package/dist/integrity/lsp/bin.ts +42 -0
- package/dist/integrity/lsp/diagnostics.ts +92 -0
- package/dist/integrity/lsp/findings.ts +63 -0
- package/dist/integrity/lsp/lint.ts +140 -0
- package/dist/integrity/lsp/protocol.ts +78 -0
- package/dist/integrity/lsp/server.ts +224 -0
- package/dist/integrity/sealed.js +2 -2
- package/dist/integrity/sealed.ts +56 -0
- package/dist/integrity/slopnator.ts +350 -0
- package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
- package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
- package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
- package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
- package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
- package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
- package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
- package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
- package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
- package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
- package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
- package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
- package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
- package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
- package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
- package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
- package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
- package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
- package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
- package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
- package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
- package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
- package/dist/integrity/sweep.ts +307 -0
- package/dist/knowledge/attach.ts +350 -0
- package/dist/knowledge/generators/docsgen.ts +174 -0
- package/dist/knowledge/generators/glossary.ts +73 -0
- package/dist/knowledge/generators/skills.ts +263 -0
- package/dist/knowledge/glossary.ts +6 -0
- package/dist/operations/access.ts +88 -0
- package/dist/operations/attach.ts +475 -0
- package/dist/operations/capabilities.ts +65 -0
- package/dist/operations/cost.ts +58 -0
- package/dist/operations/dispatch.ts +348 -0
- package/dist/operations/error-results.ts +31 -0
- package/dist/operations/errors/catalog.ts +168 -0
- package/dist/operations/errors/classes.ts +51 -0
- package/dist/operations/errors/errors.ts +314 -0
- package/dist/operations/generators/capability.js +29 -19
- package/dist/operations/generators/capability.ts +704 -0
- package/dist/operations/generators/compile.js +20 -1
- package/dist/operations/generators/compile.ts +643 -0
- package/dist/operations/generators/grant.ts +336 -0
- package/dist/operations/generators/operation.ts +972 -0
- package/dist/operations/generators/permits.ts +223 -0
- package/dist/operations/generators/seed.ts +347 -0
- package/dist/operations/generators/trial.js +4 -1
- package/dist/operations/generators/trial.ts +529 -0
- package/dist/operations/http.ts +164 -0
- package/dist/operations/makers/capability.ts +166 -0
- package/dist/operations/makers/trial.ts +520 -0
- package/dist/operations/memory.ts +470 -0
- package/dist/operations/metering.ts +328 -0
- package/dist/operations/nodes.ts +54 -0
- package/dist/operations/operations.ts +32 -0
- package/dist/operations/queue-policy.ts +10 -0
- package/dist/operations/result.ts +28 -0
- package/dist/operations/seams.d.ts +4 -4
- package/dist/operations/seams.ts +364 -0
- package/dist/operations/store.ts +29 -0
- package/package.json +8 -1
- package/dist/integrations/generators/sdk.d.ts +0 -1
- package/dist/integrations/generators/sdk.js +0 -148
- /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { inCode, listBounds, matched } from '../../core/rows.ts'
|
|
2
|
+
import { PREFIXES } from '../../core/thread.ts'
|
|
3
|
+
|
|
4
|
+
import type { Slopnator } from '../slopnator.ts'
|
|
5
|
+
import { here, parses, unreadable } from './sl_unthreaded.ts'
|
|
6
|
+
|
|
7
|
+
const MEMBERS = 'MEMBERS'
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* Which prefixes a tree file's member list may hold.
|
|
11
|
+
*
|
|
12
|
+
* The file is the key because the list is named the same in both: `MEMBERS` in the operations tree
|
|
13
|
+
* holds what an app can be asked to do, and `MEMBERS` in the nodes tree holds the spans an operation
|
|
14
|
+
* is built from. Nothing in TypeScript separates them — both are arrays of rows carrying a string id
|
|
15
|
+
* — so an operation declared among the nodes, or a node among the operations, typechecks, attaches,
|
|
16
|
+
* seeds a body and reads as agreed by everything downstream.
|
|
17
|
+
*
|
|
18
|
+
* What an operation may wear is not written here. `@thread/core/thread` pairs each word an
|
|
19
|
+
* operation can be with the prefix its id takes, and `thread create` puts that prefix on — so the
|
|
20
|
+
* command that makes them and the law that refuses them read one list. Written out again here,
|
|
21
|
+
* a kind added over there would be a kind this refuses.
|
|
22
|
+
*/
|
|
23
|
+
const TAKES: Readonly<Record<string, readonly string[]>> = {
|
|
24
|
+
'operations.ts': Object.values(PREFIXES),
|
|
25
|
+
'nodes.ts': ['nd_', 'sm_'],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ID = /\bid:\s*'((?:[^'\\]|\\.)*)'/
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* The id of every row sitting directly in the list, and no deeper.
|
|
32
|
+
*
|
|
33
|
+
* Depth is the whole of it: a member row states its own id, and the `seeds` and `derives` rows under
|
|
34
|
+
* it state theirs, which are neither operations nor nodes — `wf_send_invoice_body` is a body, not
|
|
35
|
+
* something an app can be asked to do. Reading every id in the list would refuse those children for
|
|
36
|
+
* lacking a prefix they were never meant to wear.
|
|
37
|
+
*
|
|
38
|
+
* The braces are read off `inCode`, not off the raw text. Counting characters directly desynced on a
|
|
39
|
+
* `describes:` holding a brace, and this package already ships the mask and the matcher for exactly
|
|
40
|
+
* that — a second, weaker walker beside them is the drift they exist to prevent.
|
|
41
|
+
*/
|
|
42
|
+
function directly(text: string, open: number, close: number): readonly string[] {
|
|
43
|
+
const code = inCode(text)
|
|
44
|
+
const found: string[] = []
|
|
45
|
+
for (let at = open; at < close; at += 1) {
|
|
46
|
+
if (code[at] !== true || text[at] !== '{') continue
|
|
47
|
+
const ends = matched(text, code, at, '{', '}')
|
|
48
|
+
if (ends === -1 || ends > close) break
|
|
49
|
+
const said = ID.exec(text.slice(at, ends))?.[1]
|
|
50
|
+
if (said !== undefined) found.push(said)
|
|
51
|
+
at = ends
|
|
52
|
+
}
|
|
53
|
+
return found
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const slopnator: Slopnator = {
|
|
57
|
+
holds: (_node, at) => {
|
|
58
|
+
const path = here(at.filename)
|
|
59
|
+
if (unreadable(path, at.text) !== undefined) return false
|
|
60
|
+
if (!parses(path)) return false
|
|
61
|
+
|
|
62
|
+
const takes = TAKES[path.slice(path.lastIndexOf('/') + 1)]
|
|
63
|
+
if (takes === undefined) return false
|
|
64
|
+
|
|
65
|
+
const text = String(at.text)
|
|
66
|
+
const where = listBounds(text, MEMBERS)
|
|
67
|
+
if (where === undefined) return false
|
|
68
|
+
|
|
69
|
+
for (const said of directly(text, where.open, where.close)) {
|
|
70
|
+
if (takes.some((one) => said.startsWith(one))) continue
|
|
71
|
+
return (
|
|
72
|
+
`\`${said}\` sits in \`${MEMBERS}\` here, and this list takes ${takes.map((one) => `\`${one}\``).join(', ')}. ` +
|
|
73
|
+
'A prefix is what says which of the two a row is, and a row wearing the wrong one is declared as ' +
|
|
74
|
+
'something it is not, seeds a body of the wrong shape, and is read as agreed by everything downstream'
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return false
|
|
79
|
+
},
|
|
80
|
+
proves: {
|
|
81
|
+
wrong: [
|
|
82
|
+
{
|
|
83
|
+
files: [
|
|
84
|
+
{
|
|
85
|
+
at: '_threads/operations.ts',
|
|
86
|
+
source:
|
|
87
|
+
"import type { Member, Threaded } from '../../core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n at: '_threads/operations.ts',\n describes: 'what an app can be asked to do',\n version: 1,\n}\n\nexport const MEMBERS: readonly Member[] = [\n {\n id: 'nd_teleport',\n at: '_threads/operations.ts',\n describes: 'a node wearing an operation list',\n version: 1,\n },\n]\n",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
at: '_threads/operations.ts',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
files: [
|
|
94
|
+
{
|
|
95
|
+
at: '_threads/nodes.ts',
|
|
96
|
+
source:
|
|
97
|
+
"import type { Member, Threaded } from '../../core/thread.ts'\n\nexport const NODES: Threaded = {\n id: 'nodes',\n at: '_threads/nodes.ts',\n describes: 'the spans an operation is built from',\n version: 1,\n}\n\nexport const MEMBERS: readonly Member[] = [\n {\n id: 'wf_send_invoice',\n at: '_threads/nodes.ts',\n describes: 'an operation wearing a node list',\n version: 1,\n },\n]\n",
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
at: '_threads/nodes.ts',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
files: [
|
|
104
|
+
{
|
|
105
|
+
at: '_threads/operations.ts',
|
|
106
|
+
source:
|
|
107
|
+
"import type { Member, Threaded } from '../../core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n at: '_threads/operations.ts',\n describes: 'what an app can be asked to do',\n version: 1,\n}\n\nexport const MEMBERS: readonly Member[] = [\n {\n id: 'sendInvoice',\n at: '_threads/operations.ts',\n describes: 'no prefix at all',\n version: 1,\n },\n]\n",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
at: '_threads/operations.ts',
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
right: [
|
|
114
|
+
{
|
|
115
|
+
files: [
|
|
116
|
+
{
|
|
117
|
+
at: '_threads/operations.ts',
|
|
118
|
+
source:
|
|
119
|
+
"import type { Member, Threaded } from '../../core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n at: '_threads/operations.ts',\n describes: 'what an app can be asked to do',\n version: 1,\n}\n\nexport const MEMBERS: readonly Member[] = [\n {\n id: 'wf_send_invoice',\n at: '_threads/operations.ts',\n describes: 'sends an invoice',\n version: 1,\n seeds: [\n {\n id: 'wf_send_invoice_body',\n at: 'operations/wf_send_invoice.ts',\n describes: 'the operation itself',\n },\n ],\n },\n]\n",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
at: '_threads/operations.ts',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
files: [
|
|
126
|
+
{
|
|
127
|
+
at: '_threads/nodes.ts',
|
|
128
|
+
source:
|
|
129
|
+
"import type { Member, Threaded } from '../../core/thread.ts'\n\nexport const NODES: Threaded = {\n id: 'nodes',\n at: '_threads/nodes.ts',\n describes: 'the spans an operation is built from',\n version: 1,\n}\n\nexport const MEMBERS: readonly Member[] = [\n {\n id: 'nd_teleport',\n at: '_threads/nodes.ts',\n describes: 'a step this app added',\n version: 1,\n },\n]\n",
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
at: '_threads/nodes.ts',
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isBuiltin } from 'node:module'
|
|
2
|
+
|
|
3
|
+
import { parser } from 'typescript-eslint'
|
|
4
|
+
|
|
5
|
+
import type { Slopnator } from '../slopnator.ts'
|
|
6
|
+
|
|
7
|
+
const CODE = /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$/
|
|
8
|
+
|
|
9
|
+
const MENTIONS = /\bimport\b|\brequire\s*\(/
|
|
10
|
+
|
|
11
|
+
function reached(node: unknown, seen: WeakSet<object>, found: (one: string) => void): void {
|
|
12
|
+
if (typeof node !== 'object' || node === null) return
|
|
13
|
+
if (seen.has(node)) return
|
|
14
|
+
seen.add(node)
|
|
15
|
+
|
|
16
|
+
if (Array.isArray(node)) {
|
|
17
|
+
for (const each of node) reached(each, seen, found)
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const one = node as Record<string, unknown>
|
|
22
|
+
const type = one['type']
|
|
23
|
+
|
|
24
|
+
if (type === 'ImportDeclaration' || type === 'ImportExpression' || type === 'ExportNamedDeclaration' || type === 'ExportAllDeclaration') {
|
|
25
|
+
const source = one['source'] as { readonly value?: unknown } | null | undefined
|
|
26
|
+
if (typeof source?.value === 'string') found(source.value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (type === 'CallExpression') {
|
|
30
|
+
const callee = one['callee'] as { readonly type?: unknown; readonly name?: unknown } | null | undefined
|
|
31
|
+
if (callee?.type === 'Identifier' && callee.name === 'require') {
|
|
32
|
+
const first = (one['arguments'] as readonly { readonly value?: unknown }[] | undefined)?.[0]
|
|
33
|
+
if (typeof first?.value === 'string') found(first.value)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const held of Object.values(one)) reached(held, seen, found)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const slopnator: Slopnator = {
|
|
41
|
+
holds: (_node, at) => {
|
|
42
|
+
if (!CODE.test(at.filename)) return false
|
|
43
|
+
|
|
44
|
+
const text = String(at.text)
|
|
45
|
+
if (!MENTIONS.test(text)) return false
|
|
46
|
+
|
|
47
|
+
let ast: unknown
|
|
48
|
+
try {
|
|
49
|
+
ast = (parser.parseForESLint as (text: string, options: unknown) => { readonly ast: unknown })(text, {
|
|
50
|
+
range: true,
|
|
51
|
+
ecmaVersion: 'latest',
|
|
52
|
+
sourceType: 'module',
|
|
53
|
+
ecmaFeatures: { jsx: true },
|
|
54
|
+
}).ast
|
|
55
|
+
} catch {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const taken = new Set<string>()
|
|
60
|
+
reached(ast, new WeakSet<object>(), (one) => {
|
|
61
|
+
if (isBuiltin(one)) taken.add(one)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if (taken.size === 0) return false
|
|
65
|
+
|
|
66
|
+
return `this reaches ${[...taken].sort().join(', ')} straight from the runtime, and a node is handed everything it may touch`
|
|
67
|
+
},
|
|
68
|
+
proves: {
|
|
69
|
+
wrong: [
|
|
70
|
+
"import { readFileSync } from 'node:fs'\nexport const node = readFileSync\n",
|
|
71
|
+
"const { spawn } = require('child_process')\nexport const node = spawn\n",
|
|
72
|
+
"export const node = async () => await import('node:net')\n",
|
|
73
|
+
],
|
|
74
|
+
right: [
|
|
75
|
+
"import { nd_read } from '../../operations/nodes.ts'\nexport const node = nd_read(async (hands) => await hands.store.get('a', 'b'))\n",
|
|
76
|
+
"import type { In } from './input.ts'\nexport const node = null as unknown as In\n",
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
}
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { parser } from 'typescript-eslint'
|
|
5
|
+
|
|
6
|
+
import type { Slopnator } from '../slopnator.ts'
|
|
7
|
+
|
|
8
|
+
import { descend, forget as forgetDescent, read, reached as fromThread, threading } from '../../core/descend.ts'
|
|
9
|
+
import { findTree } from '../../core/tree.ts'
|
|
10
|
+
|
|
11
|
+
const TREE_AT = findTree()
|
|
12
|
+
const ROOT = `${TREE_AT.at.split('\\').join('/')}/`
|
|
13
|
+
const TREE = TREE_AT.name
|
|
14
|
+
const THREAD_AT = `${TREE_AT.name}/thread.ts`
|
|
15
|
+
const FOLLOWABLE = /\.(ts|tsx|mts|cts|mjs|cjs|js)$/
|
|
16
|
+
const LISTS = new Set(['declares', 'derives', 'seeds'])
|
|
17
|
+
|
|
18
|
+
let threaders: ReadonlySet<string> | undefined
|
|
19
|
+
let tree: Index | undefined
|
|
20
|
+
const parsed = new Map<string, readonly Reach[]>()
|
|
21
|
+
const broke = new Map<string, string>()
|
|
22
|
+
|
|
23
|
+
export interface Reach {
|
|
24
|
+
readonly list?: 'declares' | 'derives' | 'seeds'
|
|
25
|
+
readonly at?: string
|
|
26
|
+
readonly id?: string
|
|
27
|
+
readonly describes?: string
|
|
28
|
+
readonly version?: number
|
|
29
|
+
readonly governs?: readonly string[]
|
|
30
|
+
readonly wears?: readonly string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Meaning {
|
|
34
|
+
readonly id: string
|
|
35
|
+
readonly describes: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SAID = new Set(['at', 'id', 'describes'])
|
|
39
|
+
|
|
40
|
+
interface Held {
|
|
41
|
+
readonly type?: string
|
|
42
|
+
readonly kind?: string
|
|
43
|
+
readonly name?: string
|
|
44
|
+
readonly value?: unknown
|
|
45
|
+
readonly key?: Held
|
|
46
|
+
readonly computed?: boolean
|
|
47
|
+
readonly id?: Held
|
|
48
|
+
readonly typeAnnotation?: Held
|
|
49
|
+
readonly typeName?: Held
|
|
50
|
+
readonly elementType?: Held
|
|
51
|
+
readonly properties?: readonly Held[]
|
|
52
|
+
readonly elements?: readonly (Held | null)[]
|
|
53
|
+
readonly declarations?: readonly Held[]
|
|
54
|
+
readonly body?: readonly Held[]
|
|
55
|
+
readonly declaration?: Held
|
|
56
|
+
readonly init?: Held
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const keyOf = (property: Held): string | undefined => {
|
|
60
|
+
if (property.computed === true) return undefined
|
|
61
|
+
const key = property.key
|
|
62
|
+
if (key?.type === 'Identifier') return key.name
|
|
63
|
+
if (key?.type === 'Literal' && typeof key.value === 'string') return key.value
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function annotated(node: Held): string | undefined {
|
|
68
|
+
let held = node.id?.typeAnnotation?.typeAnnotation
|
|
69
|
+
while (held !== undefined && (held.type === 'TSArrayType' || held.type === 'TSTypeOperator')) {
|
|
70
|
+
held = held.elementType ?? held.typeAnnotation
|
|
71
|
+
}
|
|
72
|
+
return held?.typeName?.name
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const here = (filename: string): string => {
|
|
76
|
+
const path = filename.split('\\').join('/')
|
|
77
|
+
return path.startsWith(ROOT) ? path.slice(ROOT.length) : path
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
function rows(node: Held, list: Reach['list'], found: Reach[], hoisted: ReadonlyMap<string, Held>): void {
|
|
83
|
+
for (const one of node.elements ?? []) {
|
|
84
|
+
const element = one?.type === 'Identifier' && one.name !== undefined ? hoisted.get(one.name) : one
|
|
85
|
+
if (element === null || element === undefined || element.type !== 'ObjectExpression') continue
|
|
86
|
+
const said: { at?: string; id?: string; describes?: string; governs?: readonly string[]; wears?: readonly string[] } = {}
|
|
87
|
+
for (const property of element.properties ?? []) {
|
|
88
|
+
const key = keyOf(property)
|
|
89
|
+
const held = property.value as Held | undefined
|
|
90
|
+
if (key !== undefined && SAID.has(key) && held?.type === 'Literal' && typeof held.value === 'string') {
|
|
91
|
+
said[key as 'at' | 'id' | 'describes'] = held.value
|
|
92
|
+
}
|
|
93
|
+
if ((key === 'governs' || key === 'wears') && held?.type === 'ArrayExpression') {
|
|
94
|
+
said[key] = (held.elements ?? [])
|
|
95
|
+
.map((one) => (one?.type === 'Literal' && typeof one.value === 'string' ? one.value : undefined))
|
|
96
|
+
.filter((one): one is string => one !== undefined)
|
|
97
|
+
}
|
|
98
|
+
if (key !== undefined && LISTS.has(key) && held?.type === 'ArrayExpression') rows(held, key as Reach['list'], found, hoisted)
|
|
99
|
+
}
|
|
100
|
+
if (said.at !== undefined || said.id !== undefined) found.push({ list, ...said })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function reaching(text: string): readonly Reach[] {
|
|
105
|
+
const already = parsed.get(text)
|
|
106
|
+
if (already !== undefined) return already
|
|
107
|
+
|
|
108
|
+
const types = threading()
|
|
109
|
+
const found: Reach[] = []
|
|
110
|
+
const parse = (jsx: boolean): { ast: unknown } =>
|
|
111
|
+
(parser.parseForESLint as (t: string, o: unknown) => { ast: unknown })(text, {
|
|
112
|
+
range: true,
|
|
113
|
+
ecmaVersion: 'latest',
|
|
114
|
+
sourceType: 'module',
|
|
115
|
+
ecmaFeatures: { jsx },
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const read = ((): { ast: unknown } | undefined => {
|
|
119
|
+
try {
|
|
120
|
+
return parse(true)
|
|
121
|
+
} catch {
|
|
122
|
+
try {
|
|
123
|
+
return parse(false)
|
|
124
|
+
} catch (error) {
|
|
125
|
+
broke.set(text, error instanceof Error ? (error.message.split('\n')[0] ?? error.message) : String(error))
|
|
126
|
+
return undefined
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
})()
|
|
130
|
+
if (read === undefined) {
|
|
131
|
+
parsed.set(text, found)
|
|
132
|
+
return found
|
|
133
|
+
}
|
|
134
|
+
const body = (read.ast as unknown as Held).body ?? []
|
|
135
|
+
const hoisted = new Map<string, Held>()
|
|
136
|
+
for (const statement of body) {
|
|
137
|
+
const held = statement.declaration ?? statement
|
|
138
|
+
if (held.type !== 'VariableDeclaration') continue
|
|
139
|
+
for (const one of held.declarations ?? []) {
|
|
140
|
+
const name = one.id?.name
|
|
141
|
+
if (name !== undefined && one.init?.type === 'ObjectExpression') hoisted.set(name, one.init)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const statement of body) {
|
|
146
|
+
const held = statement.declaration ?? statement
|
|
147
|
+
if (held.type !== 'VariableDeclaration' || held.kind !== 'const') continue
|
|
148
|
+
for (const one of held.declarations ?? []) {
|
|
149
|
+
if (!types.has(annotated(one) ?? '')) continue
|
|
150
|
+
const init = one.init
|
|
151
|
+
for (const node of init?.type === 'ArrayExpression' ? (init.elements ?? []) : [init]) {
|
|
152
|
+
if (node === null || node === undefined || node.type !== 'ObjectExpression') continue
|
|
153
|
+
const said: { id?: string; describes?: string; version?: number } = {}
|
|
154
|
+
for (const property of node.properties ?? []) {
|
|
155
|
+
const key = keyOf(property)
|
|
156
|
+
const value = property.value as Held | undefined
|
|
157
|
+
if ((key === 'id' || key === 'describes') && value?.type === 'Literal' && typeof value.value === 'string') said[key] = value.value
|
|
158
|
+
if (key === 'version' && value?.type === 'Literal' && typeof value.value === 'number') said.version = value.value
|
|
159
|
+
if (key !== undefined && LISTS.has(key) && value?.type === 'ArrayExpression') rows(value, key as Reach['list'], found, hoisted)
|
|
160
|
+
}
|
|
161
|
+
if (said.id !== undefined) found.push({ ...said })
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
parsed.set(text, found)
|
|
167
|
+
return found
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function atsIn(text: string, only?: ReadonlySet<string>): readonly string[] {
|
|
171
|
+
const found: string[] = []
|
|
172
|
+
for (const one of reaching(text)) {
|
|
173
|
+
if (only !== undefined && (one.list === undefined || !only.has(one.list))) continue
|
|
174
|
+
if (one.at !== undefined) found.push(one.at)
|
|
175
|
+
}
|
|
176
|
+
return found
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const rowsIn = (text: string): readonly Reach[] => reaching(text)
|
|
180
|
+
|
|
181
|
+
const SCHEMA = /^export const ([A-Za-z]+)IslandSchema\b/m
|
|
182
|
+
const DEFINE = /^export function (define[A-Za-z]+)\s*\(/m
|
|
183
|
+
|
|
184
|
+
export type Governed =
|
|
185
|
+
| { readonly kind: 'file'; readonly at: string }
|
|
186
|
+
| { readonly kind: 'folder'; readonly under: string }
|
|
187
|
+
| { readonly kind: 'pattern'; readonly matching: string; readonly under: string }
|
|
188
|
+
|
|
189
|
+
export function governedBy(one: string): Governed {
|
|
190
|
+
if (one.includes('*')) {
|
|
191
|
+
const cut = one.lastIndexOf('/', one.indexOf('*'))
|
|
192
|
+
return { kind: 'pattern', matching: one, under: cut === -1 ? '' : one.slice(0, cut + 1) }
|
|
193
|
+
}
|
|
194
|
+
if (one.endsWith('/')) return { kind: 'folder', under: one }
|
|
195
|
+
return { kind: 'file', at: one }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const filesGoverned = (all: readonly string[]): readonly string[] =>
|
|
199
|
+
all.map(governedBy).flatMap((one) => (one.kind === 'file' ? [one.at] : []))
|
|
200
|
+
|
|
201
|
+
export const foldersGoverned = (all: readonly string[]): readonly string[] =>
|
|
202
|
+
all.map(governedBy).flatMap((one) => (one.kind === 'file' ? [] : [one.under]))
|
|
203
|
+
|
|
204
|
+
export function governs(all: readonly string[], path: string): boolean {
|
|
205
|
+
return all.map(governedBy).some((one) => {
|
|
206
|
+
if (one.kind === 'file') return path === one.at
|
|
207
|
+
if (one.kind === 'folder') return path.startsWith(one.under)
|
|
208
|
+
const tail = one.matching.slice(one.under.length).split('*').filter(Boolean)
|
|
209
|
+
const rest = path.startsWith(one.under) ? path.slice(one.under.length) : undefined
|
|
210
|
+
return rest !== undefined && tail.every((part) => rest.includes(part))
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface Declared {
|
|
215
|
+
readonly id: string
|
|
216
|
+
readonly at: string
|
|
217
|
+
readonly describes: string
|
|
218
|
+
readonly governs: readonly string[]
|
|
219
|
+
readonly define?: string
|
|
220
|
+
readonly tree?: string
|
|
221
|
+
readonly member?: string
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function islandsDeclared(): readonly Declared[] {
|
|
225
|
+
const found: Declared[] = []
|
|
226
|
+
const seen = new Set<string>()
|
|
227
|
+
const all: Reach[] = []
|
|
228
|
+
|
|
229
|
+
for (const at of fromThread()) {
|
|
230
|
+
const held = read(at)
|
|
231
|
+
if (held === undefined) continue
|
|
232
|
+
if (unreadable(at, held) !== undefined) continue
|
|
233
|
+
all.push(...reaching(held))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const row of all) {
|
|
237
|
+
if (row.id === undefined || row.at === undefined || seen.has(row.at)) continue
|
|
238
|
+
const source = read(row.at)
|
|
239
|
+
if (source === undefined || !SCHEMA.test(source)) continue
|
|
240
|
+
seen.add(row.at)
|
|
241
|
+
|
|
242
|
+
const tree = `${TREE}/${row.id}.ts`
|
|
243
|
+
const named = all.find((one) => one.at === tree)?.id
|
|
244
|
+
|
|
245
|
+
found.push({
|
|
246
|
+
id: row.id,
|
|
247
|
+
at: row.at,
|
|
248
|
+
describes: row.describes ?? '',
|
|
249
|
+
governs: row.governs ?? [],
|
|
250
|
+
define: DEFINE.exec(source)?.[1],
|
|
251
|
+
...(named === undefined ? {} : { tree, member: named }),
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return found
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export const declaresIsland = (text: string): string | undefined => SCHEMA.exec(text)?.[1]
|
|
259
|
+
|
|
260
|
+
export function meaningsIn(text: string): readonly Meaning[] {
|
|
261
|
+
const found: Meaning[] = []
|
|
262
|
+
for (const one of reaching(text)) {
|
|
263
|
+
if (one.id === undefined) continue
|
|
264
|
+
found.push({ id: one.id, describes: one.describes ?? '' })
|
|
265
|
+
}
|
|
266
|
+
return found
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface Index {
|
|
270
|
+
readonly reached: ReadonlySet<string>
|
|
271
|
+
readonly declared: ReadonlyMap<string, number>
|
|
272
|
+
readonly meanings: ReadonlyMap<string, ReadonlySet<string>>
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
function descended(): Index {
|
|
277
|
+
const pass = descend()
|
|
278
|
+
const reached = new Set<string>([THREAD_AT, ...pass.threaded.keys()])
|
|
279
|
+
const declared = new Map<string, number>()
|
|
280
|
+
for (const [at, held] of pass.threaded) declared.set(at, held.filter((one) => one.list === 'declares').length)
|
|
281
|
+
const meanings = new Map<string, ReadonlySet<string>>()
|
|
282
|
+
for (const [word, held] of pass.coined) meanings.set(word, new Set(held.said))
|
|
283
|
+
return { reached, declared, meanings }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function index(): Index {
|
|
287
|
+
tree ??= descended()
|
|
288
|
+
return tree
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function forget(): void {
|
|
292
|
+
parsed.clear()
|
|
293
|
+
broke.clear()
|
|
294
|
+
threaders = undefined
|
|
295
|
+
tree = undefined
|
|
296
|
+
forgetDescent()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export const threaded = (): ReadonlySet<string> => index().reached
|
|
300
|
+
|
|
301
|
+
export const parses = (path: string): boolean => FOLLOWABLE.test(path)
|
|
302
|
+
|
|
303
|
+
export function unreadable(path: string, text: string): string | undefined {
|
|
304
|
+
if (!parses(path)) return undefined
|
|
305
|
+
atsIn(text)
|
|
306
|
+
const said = broke.get(text)
|
|
307
|
+
if (said === undefined) return undefined
|
|
308
|
+
return `this file cannot be parsed, so no law that reads its structure measured anything on it — ${said}`
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export const names = (path: string, text: string): readonly string[] => (parses(path) ? atsIn(text) : [])
|
|
312
|
+
|
|
313
|
+
export const slopnator: Slopnator = {
|
|
314
|
+
holds: (_node, at) => {
|
|
315
|
+
const path = here(at.filename)
|
|
316
|
+
if (path === THREAD_AT) return false
|
|
317
|
+
const why = unreadable(path, at.text)
|
|
318
|
+
if (why !== undefined) return why
|
|
319
|
+
const pass = descend()
|
|
320
|
+
if (pass.threaded.has(path)) return false
|
|
321
|
+
if (names(path, at.text).length > 0) return false
|
|
322
|
+
const by = pass.mentioned.get(path) ?? []
|
|
323
|
+
const cut = by.filter((one) => one !== THREAD_AT && !pass.threaded.has(one))
|
|
324
|
+
if (cut.length > 0) {
|
|
325
|
+
return `broken thread — ${cut.join(' and ')} names ${path}, and nothing declares ${cut.length === 1 ? 'it' : 'them'}, so the descent from ${THREAD_AT} never arrives`
|
|
326
|
+
}
|
|
327
|
+
if (by.length > 0) {
|
|
328
|
+
return `broken thread — ${by.join(' and ')} names ${path} and is itself declared, and the descent still never arrived, so the row was read and never walked`
|
|
329
|
+
}
|
|
330
|
+
return `nothing that traces back to ${TREE}/ names ${path}, so the descent from ${THREAD_AT} never reaches it`
|
|
331
|
+
},
|
|
332
|
+
proves: {
|
|
333
|
+
wrong: [
|
|
334
|
+
'export const ORPHAN = 2',
|
|
335
|
+
'export const LOOSE = { id: 2 }',
|
|
336
|
+
{
|
|
337
|
+
files: [
|
|
338
|
+
{ at: 'threads/_names_it.ts', source: "export const OWNER = {\n declares: [\n {\n at: 'scripts/_probe_bare.ts',\n },\n ],\n}\n" },
|
|
339
|
+
{ at: 'scripts/_probe_bare.ts', source: 'export const CHILD = 1\n' },
|
|
340
|
+
],
|
|
341
|
+
at: 'scripts/_probe_bare.ts',
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
files: [
|
|
345
|
+
{
|
|
346
|
+
at: 'threads/_names_it.ts',
|
|
347
|
+
source:
|
|
348
|
+
"export const REAL: Threaded = {\n declares: [\n {\n at: 'threads/_names_it.ts',\n },\n ],\n}\n\nexport const BARE = {\n declares: [\n {\n at: 'scripts/_probe_after.ts',\n },\n ],\n}\n",
|
|
349
|
+
},
|
|
350
|
+
{ at: 'scripts/_probe_after.ts', source: 'export const CHILD = 1\n' },
|
|
351
|
+
],
|
|
352
|
+
at: 'scripts/_probe_after.ts',
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
files: [
|
|
356
|
+
{ at: 'threads/_names_it.ts', source: "export let LOOSE = {\n declares: [\n {\n at: 'scripts/_probe_let.ts',\n },\n ],\n}\n" },
|
|
357
|
+
{ at: 'scripts/_probe_let.ts', source: 'export const CHILD = 1\n' },
|
|
358
|
+
],
|
|
359
|
+
at: 'scripts/_probe_let.ts',
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
files: [
|
|
363
|
+
{ at: 'threads/_names_it.ts', source: "export var LOOSE = {\n declares: [\n {\n at: 'scripts/_probe_var.ts',\n },\n ],\n}\n" },
|
|
364
|
+
{ at: 'scripts/_probe_var.ts', source: 'export const CHILD = 1\n' },
|
|
365
|
+
],
|
|
366
|
+
at: 'scripts/_probe_var.ts',
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
files: [
|
|
370
|
+
{ at: 'threads/_names_it.ts', source: "export default {\n declares: [\n {\n at: 'scripts/_probe_default.ts',\n },\n ],\n}\n" },
|
|
371
|
+
{ at: 'scripts/_probe_default.ts', source: 'export const CHILD = 1\n' },
|
|
372
|
+
],
|
|
373
|
+
at: 'scripts/_probe_default.ts',
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
files: [
|
|
377
|
+
{ at: 'threads/_names_it.ts', source: "export function held() {\n return {\n declares: [\n {\n at: 'scripts/_probe_fn.ts',\n },\n ],\n }\n}\n" },
|
|
378
|
+
{ at: 'scripts/_probe_fn.ts', source: 'export const CHILD = 1\n' },
|
|
379
|
+
],
|
|
380
|
+
at: 'scripts/_probe_fn.ts',
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
files: [
|
|
384
|
+
{ at: 'threads/_names_it.ts', source: "export class Held {\n held = {\n declares: [\n {\n at: 'scripts/_probe_class.ts',\n },\n ],\n }\n}\n" },
|
|
385
|
+
{ at: 'scripts/_probe_class.ts', source: 'export const CHILD = 1\n' },
|
|
386
|
+
],
|
|
387
|
+
at: 'scripts/_probe_class.ts',
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
files: [
|
|
391
|
+
{
|
|
392
|
+
at: 'threads/_names_it.ts',
|
|
393
|
+
source: "interface Held { declares: unknown[] }\nexport const FAKE: Held = {\n declares: [\n {\n at: 'scripts/_probe_fake.ts',\n },\n ],\n}\n",
|
|
394
|
+
},
|
|
395
|
+
{ at: 'scripts/_probe_fake.ts', source: 'export const CHILD = 1\n' },
|
|
396
|
+
],
|
|
397
|
+
at: 'scripts/_probe_fake.ts',
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
right: [
|
|
401
|
+
"export const HELD: Threaded = {\n declares: [\n {\n at: 'threads/probe.tsx',\n },\n ],\n}",
|
|
402
|
+
{
|
|
403
|
+
files: [
|
|
404
|
+
{ at: 'threads/_names_it.ts', source: "export const OWNER: Threaded = {\n declares: [\n {\n at: 'scripts/_probe_child.ts',\n },\n ],\n}\n" },
|
|
405
|
+
{ at: 'scripts/_probe_child.ts', source: 'export const CHILD = 1\n' },
|
|
406
|
+
],
|
|
407
|
+
at: 'scripts/_probe_child.ts',
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
files: [
|
|
411
|
+
{ at: 'threads/_names_it.ts', source: "export const OWNER: Threaded = {\n declares: [\n {\n at: 'scripts/_probe_parent.ts',\n },\n ],\n}\n" },
|
|
412
|
+
{ at: 'scripts/_probe_parent.ts', source: "export const MID: Threaded = {\n declares: [\n {\n at: 'scripts/_probe_leaf.ts',\n },\n ],\n}\n" },
|
|
413
|
+
{ at: 'scripts/_probe_leaf.ts', source: 'export const LEAF = 1\n' },
|
|
414
|
+
],
|
|
415
|
+
at: 'scripts/_probe_leaf.ts',
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
},
|
|
419
|
+
}
|