@prestack/thread 0.0.1 → 0.0.2

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 (150) hide show
  1. package/dist/core/actor.ts +20 -0
  2. package/dist/core/asking.d.ts +0 -1
  3. package/dist/core/asking.js +0 -1
  4. package/dist/core/asking.ts +1342 -0
  5. package/dist/core/attach.js +2 -2
  6. package/dist/core/attach.ts +585 -0
  7. package/dist/core/chain.js +8 -1
  8. package/dist/core/chain.ts +148 -0
  9. package/dist/core/cli.js +97 -18
  10. package/dist/core/cli.ts +2004 -0
  11. package/dist/core/condition.ts +227 -0
  12. package/dist/core/descend.d.ts +1 -0
  13. package/dist/core/descend.js +36 -0
  14. package/dist/core/descend.ts +1001 -0
  15. package/dist/core/finding.ts +38 -0
  16. package/dist/core/generators/meta.js +6 -0
  17. package/dist/core/generators/meta.ts +1006 -0
  18. package/dist/core/init.ts +170 -0
  19. package/dist/core/invoked.ts +47 -0
  20. package/dist/core/round.ts +57 -0
  21. package/dist/core/rows.ts +369 -0
  22. package/dist/core/scope.ts +18 -0
  23. package/dist/core/thread.ts +29 -0
  24. package/dist/core/tree.ts +67 -0
  25. package/dist/core/types.ts +350 -0
  26. package/dist/derivation/attach.js +1 -1
  27. package/dist/derivation/attach.ts +68 -0
  28. package/dist/derivation/generators/build.ts +232 -0
  29. package/dist/derivation/generators/check.ts +118 -0
  30. package/dist/derivation/generators/derivation.ts +347 -0
  31. package/dist/derivation/generators/determinism.ts +145 -0
  32. package/dist/derivation/generators/release.js +19 -9
  33. package/dist/derivation/generators/release.ts +213 -0
  34. package/dist/derivation/makers/release.ts +135 -0
  35. package/dist/derivation/release.ts +23 -0
  36. package/dist/integrations/attach.js +2 -2
  37. package/dist/integrations/attach.ts +207 -0
  38. package/dist/integrations/generators/read.ts +127 -0
  39. package/dist/integrations/generators/secret.ts +289 -0
  40. package/dist/integrations/generators/target.js +16 -6
  41. package/dist/integrations/generators/target.ts +944 -0
  42. package/dist/integrations/generators/vault.js +46 -14
  43. package/dist/integrations/generators/vault.ts +212 -0
  44. package/dist/integrations/{sdk.js → module.js} +4 -4
  45. package/dist/integrations/module.ts +189 -0
  46. package/dist/integrations/secret.ts +50 -0
  47. package/dist/integrations/target.d.ts +3 -1
  48. package/dist/integrations/target.js +14 -11
  49. package/dist/integrations/target.ts +340 -0
  50. package/dist/integrations/vault.d.ts +2 -0
  51. package/dist/integrations/vault.js +4 -2
  52. package/dist/integrations/vault.ts +136 -0
  53. package/dist/integrity/attach.ts +234 -0
  54. package/dist/integrity/check-threads.js +2 -26
  55. package/dist/integrity/check-threads.ts +166 -0
  56. package/dist/integrity/constraint.ts +1202 -0
  57. package/dist/integrity/generators/compiler.ts +161 -0
  58. package/dist/integrity/generators/gate.ts +201 -0
  59. package/dist/integrity/generators/issues.ts +160 -0
  60. package/dist/integrity/generators/publish.js +14 -5
  61. package/dist/integrity/generators/publish.ts +247 -0
  62. package/dist/integrity/generators/rule.ts +271 -0
  63. package/dist/integrity/generators/seal.ts +117 -0
  64. package/dist/integrity/generators/trial.ts +320 -0
  65. package/dist/integrity/lint.ts +227 -0
  66. package/dist/integrity/lsp/bin.ts +42 -0
  67. package/dist/integrity/lsp/diagnostics.ts +92 -0
  68. package/dist/integrity/lsp/findings.ts +63 -0
  69. package/dist/integrity/lsp/lint.ts +140 -0
  70. package/dist/integrity/lsp/protocol.ts +78 -0
  71. package/dist/integrity/lsp/server.ts +224 -0
  72. package/dist/integrity/sealed.js +2 -2
  73. package/dist/integrity/sealed.ts +56 -0
  74. package/dist/integrity/slopnator.ts +350 -0
  75. package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
  76. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
  77. package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
  78. package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
  79. package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
  80. package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
  81. package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
  82. package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
  83. package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
  84. package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
  85. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
  86. package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
  87. package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
  88. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
  89. package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
  90. package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
  91. package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
  92. package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
  93. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
  94. package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
  95. package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
  96. package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
  97. package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
  98. package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
  99. package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
  100. package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
  101. package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
  102. package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
  103. package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
  104. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
  105. package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
  106. package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
  107. package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
  108. package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
  109. package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
  110. package/dist/integrity/sweep.ts +307 -0
  111. package/dist/knowledge/attach.ts +350 -0
  112. package/dist/knowledge/generators/docsgen.ts +174 -0
  113. package/dist/knowledge/generators/glossary.ts +73 -0
  114. package/dist/knowledge/generators/skills.ts +263 -0
  115. package/dist/knowledge/glossary.ts +6 -0
  116. package/dist/operations/access.ts +88 -0
  117. package/dist/operations/attach.ts +475 -0
  118. package/dist/operations/capabilities.ts +65 -0
  119. package/dist/operations/cost.ts +58 -0
  120. package/dist/operations/dispatch.ts +348 -0
  121. package/dist/operations/error-results.ts +31 -0
  122. package/dist/operations/errors/catalog.ts +168 -0
  123. package/dist/operations/errors/classes.ts +51 -0
  124. package/dist/operations/errors/errors.ts +314 -0
  125. package/dist/operations/generators/capability.js +29 -19
  126. package/dist/operations/generators/capability.ts +704 -0
  127. package/dist/operations/generators/compile.js +20 -1
  128. package/dist/operations/generators/compile.ts +643 -0
  129. package/dist/operations/generators/grant.ts +336 -0
  130. package/dist/operations/generators/operation.ts +972 -0
  131. package/dist/operations/generators/permits.ts +223 -0
  132. package/dist/operations/generators/seed.ts +347 -0
  133. package/dist/operations/generators/trial.js +4 -1
  134. package/dist/operations/generators/trial.ts +529 -0
  135. package/dist/operations/http.ts +164 -0
  136. package/dist/operations/makers/capability.ts +166 -0
  137. package/dist/operations/makers/trial.ts +520 -0
  138. package/dist/operations/memory.ts +470 -0
  139. package/dist/operations/metering.ts +328 -0
  140. package/dist/operations/nodes.ts +54 -0
  141. package/dist/operations/operations.ts +32 -0
  142. package/dist/operations/queue-policy.ts +10 -0
  143. package/dist/operations/result.ts +28 -0
  144. package/dist/operations/seams.d.ts +4 -4
  145. package/dist/operations/seams.ts +364 -0
  146. package/dist/operations/store.ts +29 -0
  147. package/package.json +8 -1
  148. package/dist/integrations/generators/sdk.d.ts +0 -1
  149. package/dist/integrations/generators/sdk.js +0 -148
  150. /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
@@ -0,0 +1,93 @@
1
+ import type { Slopnator } from '../slopnator.ts'
2
+
3
+ interface Node {
4
+ readonly type?: string
5
+ readonly name?: string
6
+ readonly key?: { readonly name?: string; readonly value?: unknown }
7
+ readonly value?: unknown
8
+ readonly body?: unknown
9
+ readonly properties?: readonly Node[]
10
+ readonly elements?: readonly unknown[]
11
+ }
12
+
13
+ const keyOf = (node: Node): string | undefined =>
14
+ node.key?.name ?? (typeof node.key?.value === 'string' ? node.key.value : undefined)
15
+
16
+ const answersUnwritten = (held: unknown): boolean => {
17
+ const fn = held as { type?: string; body?: { type?: string; name?: string } } | undefined
18
+ if (fn?.type !== 'ArrowFunctionExpression') return false
19
+ return fn.body?.type === 'Identifier' && fn.body.name === 'NOT_WRITTEN'
20
+ }
21
+
22
+ const statesNothing = (held: unknown): boolean => {
23
+ const proves = held as Node | undefined
24
+ if (proves?.type !== 'ObjectExpression') return false
25
+ return (proves.properties ?? []).some((one) => {
26
+ const key = keyOf(one)
27
+ if (key !== 'wrong' && key !== 'right') return false
28
+ const list = one.value as Node | undefined
29
+ return list?.type === 'ArrayExpression' && (list.elements ?? []).length === 0
30
+ })
31
+ }
32
+
33
+ export const slopnator: Slopnator = {
34
+ on: ['Property'],
35
+ holds: (node) => {
36
+ const held = node as Node
37
+ const key = keyOf(held)
38
+
39
+ if (key === 'holds' && answersUnwritten(held.value)) {
40
+ return 'this is still the scaffold — it answers `NOT_WRITTEN` to everything, so the rule that names it registers, loads, reports nothing, and reads from outside exactly like a rule being obeyed'
41
+ }
42
+
43
+ if (key === 'on') {
44
+ const list = held.value as Node | undefined
45
+ if (list?.type === 'ArrayExpression' && (list.elements ?? []).length === 0) {
46
+ return 'it says it visits syntax and then names no node type, so it is handed nothing and governs nothing inside the files its rule governs'
47
+ }
48
+ }
49
+
50
+ if (key === 'proves' && statesNothing(held.value)) {
51
+ return 'its `proves` states nothing — with no form it refuses and none it accepts, no trial is derived from it, so nothing anywhere shows the slopnator does what its row claims'
52
+ }
53
+
54
+ return false
55
+ },
56
+ proves: {
57
+ wrong: [
58
+ {
59
+ files: [
60
+ {
61
+ at: 'packages/core/src/integrity/slopnators/sl_probe_inert.ts',
62
+ source: 'export const slopnator = {\n on: [],\n holds: () => NOT_WRITTEN,\n proves: { wrong: [], right: [] },\n}\n',
63
+ },
64
+ ],
65
+ at: 'packages/core/src/integrity/slopnators/sl_probe_inert.ts',
66
+ },
67
+ {
68
+ files: [
69
+ {
70
+ at: 'packages/core/src/integrity/slopnators/sl_probe_inert.ts',
71
+ source: "export const slopnator = {\n holds: (_node, at) => at.text.includes('nope') && 'found',\n proves: { wrong: [], right: ['const held = 1'] },\n}\n",
72
+ },
73
+ ],
74
+ at: 'packages/core/src/integrity/slopnators/sl_probe_inert.ts',
75
+ },
76
+ ],
77
+ right: [
78
+ {
79
+ files: [
80
+ {
81
+ at: 'packages/core/src/integrity/slopnators/sl_probe_inert.ts',
82
+ source: "export const slopnator = {\n holds: (_node, at) => at.text.includes('nope') && 'found',\n proves: { wrong: ['nope'], right: ['const held = 1'] },\n}\n",
83
+ },
84
+ ],
85
+ at: 'packages/core/src/integrity/slopnators/sl_probe_inert.ts',
86
+ },
87
+ {
88
+ files: [{ at: 'packages/core/src/integrity/slopnators/sl_probe_inert.ts', source: 'export const held = 1\n' }],
89
+ at: 'packages/core/src/integrity/slopnators/sl_probe_inert.ts',
90
+ },
91
+ ],
92
+ },
93
+ }
@@ -0,0 +1,182 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import type { Slopnator } from '../slopnator.ts'
5
+ import { ORGANISATION, ours } from '../../core/scope.ts'
6
+ import { findTree } from '../../core/tree.ts'
7
+
8
+ /**
9
+ * A row the package still ships and the tree no longer holds.
10
+ *
11
+ * Read as TEXT, deliberately. Eight laws were once added whose walks imported a workspace package,
12
+ * plain Node could not resolve it, `load()` swallowed the failure, and all eight read from every
13
+ * caller exactly like laws finding nothing. Nothing here imports anything but node builtins.
14
+ */
15
+
16
+ const ROOT = findTree().at
17
+
18
+ /** A node: a const annotated `Threaded`. */
19
+ const NODE = /export\s+const\s+[A-Za-z_$][\w$]*\s*:\s*Threaded\b/
20
+
21
+ /**
22
+ * Where a node's rows are written.
23
+ *
24
+ * A node may open the list inline, or name a `declares` array declared beside it and reference it by
25
+ * shorthand. Only the first was read, so `@thread/ui`'s tokens and signals nodes — 107 of its 178
26
+ * shipped rows — reported as shipping nothing, and every one of them could be deleted in silence.
27
+ */
28
+ const SHAPES = [/declares:\s*\[/g, /export\s+const\s+declares\b[^\n]*?=\s*\[/g]
29
+
30
+ /**
31
+ * How a row states its id.
32
+ *
33
+ * A row may be an object literal, or a call to a factory declared beside the list that takes the id
34
+ * first. Only the literal was read, which is the other half of why tokens and signals were invisible.
35
+ */
36
+ const ID = /\bid:\s*'([^']+)'/g
37
+ const CALLED = /(?:^|[\s,[(])[a-zA-Z_$][\w$]*\(\s*'([^']+)'/g
38
+
39
+ /** Strings out first, so a fixture that PRINTS a declaration is not read as being one. */
40
+ const QUOTED = /"(?:[^"\\\n]|\\.)*"|`(?:[^`\\]|\\.)*`/g
41
+
42
+ function closes(code: string, from: number): number {
43
+ let depth = 0
44
+ for (let at = from; at < code.length; at += 1) {
45
+ if (code[at] === '[') depth += 1
46
+ else if (code[at] === ']') {
47
+ depth -= 1
48
+ if (depth === 0) return at
49
+ }
50
+ }
51
+ return -1
52
+ }
53
+
54
+ /**
55
+ * What a package SHIPS: only the ids inside a `declares` list.
56
+ *
57
+ * It was every id anywhere in a file that held one such list, which read an `Appending`'s own `id`
58
+ * as a shipped row — `@thread/operations` was reported as shipping `seed-generator`, the name of a
59
+ * row it asks an app to APPEND into another list, not one it declares.
60
+ */
61
+ function shippedIn(text: string): ReadonlySet<string> {
62
+ const code = text.replace(QUOTED, '""')
63
+ if (!NODE.test(code)) return new Set()
64
+ const found = new Set<string>()
65
+ for (const shape of SHAPES) {
66
+ for (const one of code.matchAll(shape)) {
67
+ const opens = (one.index ?? 0) + one[0].length - 1
68
+ const shut = closes(code, opens)
69
+ if (shut === -1) continue
70
+ const rows = code.slice(opens, shut)
71
+ for (const held of rows.matchAll(ID)) found.add(held[1] ?? '')
72
+ for (const held of rows.matchAll(CALLED)) found.add(held[1] ?? '')
73
+ }
74
+ }
75
+ return found
76
+ }
77
+
78
+ /**
79
+ * What a tree HOLDS: every id in it, whichever list it sits in.
80
+ *
81
+ * Gating this on `declares` made every row in `derivation.ts` invisible, because its node states no
82
+ * `declares` at all and its rows live in `GENERATORS`. A row plainly on disk read as missing, and
83
+ * running the command this law recommends would not have cleared the finding.
84
+ */
85
+ function heldIn(text: string): ReadonlySet<string> {
86
+ const code = text.replace(QUOTED, '""')
87
+ const found = new Set<string>()
88
+ for (const one of code.matchAll(ID)) found.add(one[1] ?? '')
89
+ for (const one of code.matchAll(CALLED)) found.add(one[1] ?? '')
90
+ return found
91
+ }
92
+
93
+ function filesIn(dir: string): readonly string[] {
94
+ try {
95
+ return readdirSync(dir).filter((one) => one.endsWith('.ts')).map((one) => join(dir, one))
96
+ } catch {
97
+ return []
98
+ }
99
+ }
100
+
101
+ /** Every installed thread package that ships nodes of its own. */
102
+ function installed(): readonly { readonly name: string; readonly src: string }[] {
103
+ const at = join(ROOT, 'node_modules', ORGANISATION)
104
+ const found: { name: string; src: string }[] = []
105
+ for (const one of safely(at)) {
106
+ const name = `${ORGANISATION}/${one}`
107
+ if (!ours(name)) continue
108
+ const src = join(at, one, 'dist')
109
+ if (existsSync(src) && statSync(src).isDirectory()) found.push({ name, src })
110
+ }
111
+ return found
112
+ }
113
+
114
+ function safely(dir: string): readonly string[] {
115
+ try {
116
+ return readdirSync(dir)
117
+ } catch {
118
+ return []
119
+ }
120
+ }
121
+
122
+ function read(path: string): string {
123
+ try {
124
+ return readFileSync(path, 'utf8')
125
+ } catch {
126
+ return ''
127
+ }
128
+ }
129
+
130
+ export const slopnator: Slopnator = {
131
+ checks: () => {
132
+ const tree = findTree()
133
+
134
+ /* Every id the tree holds, across every node in it. */
135
+ const held = new Set<string>()
136
+ for (const path of filesIn(tree.dir)) for (const one of heldIn(read(path))) held.add(one)
137
+
138
+ /*
139
+ * Only packages this tree ATTACHED, not every one installed.
140
+ *
141
+ * Measured by running it: the first version named `@thread/knowledge`, which the audit app has
142
+ * in `node_modules` and never attached — so the law demanded rows for a package nobody had
143
+ * asked for. A package is attached when the tree names a file inside it; one nothing points at
144
+ * is a dependency, and a dependency ships nothing this tree promised to hold.
145
+ */
146
+ const wired = new Set<string>()
147
+ for (const path of filesIn(tree.dir)) {
148
+ for (const one of read(path).matchAll(/node_modules\/(@[\w.-]+\/[\w.-]+)\//g)) wired.add(one[1] ?? '')
149
+ }
150
+
151
+ const found: string[] = []
152
+ for (const pkg of installed()) {
153
+ if (!wired.has(pkg.name)) continue
154
+ for (const path of filesIn(pkg.src)) {
155
+ const shipped = shippedIn(read(path))
156
+ if (shipped.size === 0) continue
157
+ for (const id of [...shipped].sort()) {
158
+ /* The node's own id names the list, not a row in it. */
159
+ if (held.has(id)) continue
160
+ found.push(
161
+ `${pkg.name} ships \`${id}\` and this tree does not hold it — run \`thread attach ${pkg.name}\` to put it back`,
162
+ )
163
+ }
164
+ }
165
+ }
166
+ return found
167
+ },
168
+ proves: {
169
+ wrong: [
170
+ {
171
+ files: [{ at: '_threads/_probe_missing.ts', source: "import type { Threaded } from '../../core/thread.ts'\n\nexport const M: Threaded = {\n declares: [],\n}\n" }],
172
+ at: '_threads/_probe_missing.ts',
173
+ },
174
+ ],
175
+ right: [
176
+ {
177
+ files: [{ at: '_threads/_probe_missing.ts', source: "import type { Threaded } from '../../core/thread.ts'\n\nexport const M: Threaded = {\n declares: [\n {\n id: 'colour',\n },\n ],\n}\n" }],
178
+ at: '_threads/_probe_missing.ts',
179
+ },
180
+ ],
181
+ },
182
+ }
@@ -0,0 +1,48 @@
1
+ import type { Slopnator } from '../slopnator.ts'
2
+ import { here } from './sl_unthreaded.ts'
3
+
4
+ const BARREL_NAME = /(^|\/)index\.(ts|tsx|mts|cts|js|mjs|cjs)$/
5
+ const CODE = /\.(ts|tsx|mts|cts|js|mjs|cjs)$/
6
+
7
+ const SAYS = "can't do barrels lazy ass"
8
+
9
+ function reExportsOnly(text: string): boolean {
10
+ const lines = text
11
+ .split('\n')
12
+ .map((one) => one.trim())
13
+ .filter((one) => one !== '')
14
+
15
+ if (lines.length === 0) return false
16
+ return lines.every((one) => one.startsWith('export *') || one.startsWith('export {') || one.startsWith('}') || one.startsWith('} from') || /^[A-Za-z_$][\w$]*,?$/.test(one) || /^type [A-Za-z_$][\w$]*,?$/.test(one))
17
+ }
18
+
19
+ export const slopnator: Slopnator = {
20
+ holds: (_node, at) => {
21
+ const path = here(at.filename)
22
+ if (!CODE.test(path)) return false
23
+
24
+ if (BARREL_NAME.test(path)) {
25
+ return `${SAYS} — this file is named for the folder it sits in, so it exists to list what the folder holds, and the tree already says that`
26
+ }
27
+
28
+ if (reExportsOnly(at.text)) {
29
+ return `${SAYS} — every line of this file is a re-export, so it holds no thing of its own and names nothing the tree has not already named`
30
+ }
31
+
32
+ return false
33
+ },
34
+ proves: {
35
+ wrong: [
36
+ {
37
+ files: [{ at: 'packages/_probe_barrel/index.ts', source: "export const held = 1\n" }],
38
+ at: 'packages/_probe_barrel/index.ts',
39
+ },
40
+ ],
41
+ right: [
42
+ {
43
+ files: [{ at: 'packages/_probe_barrel/held.ts', source: "export const held = 1\n" }],
44
+ at: 'packages/_probe_barrel/held.ts',
45
+ },
46
+ ],
47
+ },
48
+ }
@@ -0,0 +1,74 @@
1
+ import type { Slopnator } from '../slopnator.ts'
2
+
3
+ const STARTS_A_PROCESS = new Set(['node:child_process', 'child_process'])
4
+
5
+ interface Node {
6
+ readonly type?: string
7
+ readonly source?: { readonly type?: string; readonly value?: unknown }
8
+ readonly callee?: { readonly type?: string; readonly name?: string }
9
+ readonly arguments?: readonly { readonly type?: string; readonly value?: unknown }[]
10
+ }
11
+
12
+ const named = (held: unknown): string | undefined => {
13
+ const one = held as { type?: string; value?: unknown } | undefined
14
+ return one?.type === 'Literal' && typeof one.value === 'string' ? one.value : undefined
15
+ }
16
+
17
+ export const slopnator: Slopnator = {
18
+ on: ['ImportDeclaration', 'ImportExpression', 'CallExpression'],
19
+ holds: (node) => {
20
+ const held = node as Node
21
+ const from =
22
+ held.type === 'ImportDeclaration' || held.type === 'ImportExpression'
23
+ ? (named(held.source) ?? named(held.arguments?.[0]))
24
+ : held.callee?.name === 'require'
25
+ ? named(held.arguments?.[0])
26
+ : undefined
27
+
28
+ if (from === undefined || !STARTS_A_PROCESS.has(from)) return false
29
+
30
+ return `it imports \`${from}\`, so this slopnator can start a process — and whatever it starts can reach back into the enforcement that called it`
31
+ },
32
+ proves: {
33
+ wrong: [
34
+ {
35
+ files: [
36
+ {
37
+ at: 'packages/core/src/integrity/slopnators/sl_probe_spawn.ts',
38
+ source: "import { spawnSync } from 'node:child_process'\nexport const slopnator = { holds: () => false, proves: { wrong: ['a'], right: ['b'] } }\n",
39
+ },
40
+ ],
41
+ at: 'packages/core/src/integrity/slopnators/sl_probe_spawn.ts',
42
+ },
43
+ {
44
+ files: [
45
+ {
46
+ at: 'packages/core/src/integrity/slopnators/sl_probe_spawn.ts',
47
+ source: "const held = await import('child_process')\nexport const slopnator = { holds: () => false, proves: { wrong: ['a'], right: ['b'] } }\n",
48
+ },
49
+ ],
50
+ at: 'packages/core/src/integrity/slopnators/sl_probe_spawn.ts',
51
+ },
52
+ ],
53
+ right: [
54
+ {
55
+ files: [
56
+ {
57
+ at: 'packages/core/src/integrity/slopnators/sl_probe_spawn.ts',
58
+ source: "import { readFileSync } from 'node:fs'\nexport const slopnator = { holds: () => false, proves: { wrong: ['a'], right: ['b'] } }\n",
59
+ },
60
+ ],
61
+ at: 'packages/core/src/integrity/slopnators/sl_probe_spawn.ts',
62
+ },
63
+ {
64
+ files: [
65
+ {
66
+ at: 'packages/core/src/integrity/slopnators/sl_probe_spawn.ts',
67
+ source: "export const slopnator = { holds: () => false, proves: { wrong: ['a'], right: ['b'] } }\n",
68
+ },
69
+ ],
70
+ at: 'packages/core/src/integrity/slopnators/sl_probe_spawn.ts',
71
+ },
72
+ ],
73
+ },
74
+ }
@@ -0,0 +1,43 @@
1
+ import type { Slopnator, Spoken } from '../slopnator.ts'
2
+
3
+ const shebang = (text: string): string | undefined => (text.startsWith('#!') ? text.slice(2, text.indexOf('\n')) : undefined)
4
+
5
+ const spoken = (text: string, all: readonly Spoken[]): readonly Spoken[] => {
6
+ const line = shebang(text)
7
+ return all.filter((one, index) => !(index === 0 && one.value === line))
8
+ }
9
+
10
+ export const slopnator: Slopnator = {
11
+ on: ['Program'],
12
+ holds: (_node, at) => {
13
+ const all = spoken(at.text, at.comments())
14
+ if (all.length === 0) return false
15
+ const first = (all[0]?.value ?? '')
16
+ .split('\n')
17
+ .map((line) => line.trim().replace(/^\*+\s?/, ''))
18
+ .find((line) => line !== '') ?? ''
19
+ return `${String(all.length)} comment(s), the first saying "${first.slice(0, 60)}"`
20
+ },
21
+ fixes: (_node, at) => {
22
+ const all = spoken(at.text, at.comments())
23
+ if (all.length === 0) return undefined
24
+ let out = at.text
25
+ for (const one of [...all].sort((a, b) => b.range[0] - a.range[0])) {
26
+ out = out.slice(0, one.range[0]) + out.slice(one.range[1])
27
+ }
28
+ return out.replace(/[ \t]+$/gm, '').replace(/\n{3,}/g, '\n\n')
29
+ },
30
+ proves: {
31
+ wrong: [
32
+ 'const a = 1 // a line comment',
33
+ '/* a block comment */\nconst a = 1',
34
+ '/** JSDoc on the export */\nexport const a = 1',
35
+ ],
36
+ right: [
37
+ 'const a = "// not a comment"',
38
+ 'const url = "https://example.com/docs"',
39
+ 'const a = 1',
40
+ '#!/usr/bin/env node\nconst a = 1',
41
+ ],
42
+ },
43
+ }