@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,148 @@
1
+ import type { Slopnator } from '../slopnator.ts'
2
+
3
+ import { descend, read, reached } from '../../core/descend.ts'
4
+
5
+ import { rowsIn } from './sl_unthreaded.ts'
6
+
7
+ const ELEMENT = 'element'
8
+
9
+ const VARIANT = 'variant'
10
+
11
+ interface Worn {
12
+ readonly elements: ReadonlySet<string>
13
+ readonly variants: ReadonlyMap<string, readonly string[]>
14
+ }
15
+
16
+ function nameOf(id: string): string {
17
+ return id
18
+ .split('-')
19
+ .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
20
+ .join('')
21
+ }
22
+
23
+ function idOf(name: string): string {
24
+ return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
25
+ }
26
+
27
+ function memberOf(id: string): string {
28
+ const [first = '', ...rest] = id.split('-')
29
+ return first + rest.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join('')
30
+ }
31
+
32
+ function declared(): Worn {
33
+ const elements = new Set<string>()
34
+ const variants = new Map<string, readonly string[]>()
35
+
36
+ descend()
37
+
38
+ for (const at of reached()) {
39
+ const text = read(at)
40
+ if (text === undefined) continue
41
+
42
+ let waiting: { readonly id: string; readonly wears?: readonly string[] }[] = []
43
+ for (const one of rowsIn(text)) {
44
+ if (one.id === undefined) continue
45
+ if (one.list === 'declares') {
46
+ waiting.push(one.wears === undefined ? { id: one.id } : { id: one.id, wears: one.wears })
47
+ continue
48
+ }
49
+ if (one.list !== undefined) continue
50
+ if (one.id === ELEMENT) for (const row of waiting) elements.add(nameOf(row.id))
51
+ if (one.id === VARIANT) for (const row of waiting) variants.set(row.id, row.wears ?? [])
52
+ waiting = []
53
+ }
54
+ }
55
+
56
+ return { elements, variants }
57
+ }
58
+
59
+ interface Tag {
60
+ readonly type?: string
61
+ readonly object?: { readonly type?: string; readonly name?: string }
62
+ readonly property?: { readonly type?: string; readonly name?: string }
63
+ }
64
+
65
+ export const slopnator: Slopnator = {
66
+ on: ['JSXOpeningElement'],
67
+ holds: (node) => {
68
+ const name = (node as { name?: Tag } | undefined)?.name
69
+ if (name?.type !== 'JSXMemberExpression') return false
70
+
71
+ const element = name.object?.type === 'JSXIdentifier' ? name.object.name : undefined
72
+ const member = name.property?.type === 'JSXIdentifier' ? name.property.name : undefined
73
+ if (element === undefined || member === undefined) return false
74
+
75
+ const { elements, variants } = declared()
76
+ if (!elements.has(element)) return false
77
+
78
+ const id = idOf(member)
79
+ const wears = variants.get(id)
80
+
81
+ const worn = [...variants]
82
+ .filter(([, all]) => all.includes(element))
83
+ .map(([one]) => `${element}.${memberOf(one)}`)
84
+ .sort()
85
+
86
+ const offered =
87
+ worn.length === 0
88
+ ? `nothing declares itself worn by \`${element}\`, so no member of it would have been accepted — a variant row has to name \`${element}\` first`
89
+ : `\`${element}\` wears ${worn.join(', ')}`
90
+
91
+ if (wears === undefined) {
92
+ return (
93
+ `\`${element}.${member}\` names the variant \`${id}\` and nothing declares it — so it renders carrying a class ` +
94
+ 'the stylesheet never writes, which reads on screen as an element that lost its styling and is reported ' +
95
+ `nowhere. ${offered}`
96
+ )
97
+ }
98
+
99
+ if (!wears.includes(element)) {
100
+ return (
101
+ `\`${element}.${member}\` pairs \`${element}\` with the variant \`${id}\`, and that variant's row does not name ` +
102
+ `\`${element}\` among the elements that wear it — so the pairing is nobody's declaration. ${offered}`
103
+ )
104
+ }
105
+
106
+ return false
107
+ },
108
+ proves: {
109
+ wrong: [
110
+ {
111
+ files: [
112
+ {
113
+ at: 'threads/_probe_worn.ts',
114
+ source:
115
+ "export const ELEMENTS: Threaded = {\n id: 'element',\n declares: [\n {\n id: 'button',\n at: 'app/_probe_button.tsx',\n },\n ],\n}\n\nexport const VARIANTS: Threaded = {\n id: 'variant',\n declares: [\n {\n id: 'button-primary',\n at: 'app/_probe_variant.ts',\n wears: ['Button'],\n },\n ],\n}\n",
116
+ },
117
+ { at: 'app/_probe_undeclared.tsx', source: 'export const HELD = <Button.primary />\n' },
118
+ ],
119
+ at: 'app/_probe_undeclared.tsx',
120
+ },
121
+ {
122
+ files: [
123
+ {
124
+ at: 'threads/_probe_worn.ts',
125
+ source:
126
+ "export const ELEMENTS: Threaded = {\n id: 'element',\n declares: [\n {\n id: 'button',\n at: 'app/_probe_button.tsx',\n },\n {\n id: 'badge',\n at: 'app/_probe_badge.tsx',\n },\n ],\n}\n\nexport const VARIANTS: Threaded = {\n id: 'variant',\n declares: [\n {\n id: 'chip',\n at: 'app/_probe_chip.ts',\n wears: ['Badge'],\n },\n ],\n}\n",
127
+ },
128
+ { at: 'app/_probe_mispaired.tsx', source: 'export const HELD = <Button.chip />\n' },
129
+ ],
130
+ at: 'app/_probe_mispaired.tsx',
131
+ },
132
+ ],
133
+ right: [
134
+ {
135
+ files: [
136
+ {
137
+ at: 'threads/_probe_worn.ts',
138
+ source:
139
+ "export const ELEMENTS: Threaded = {\n id: 'element',\n declares: [\n {\n id: 'button',\n at: 'app/_probe_button.tsx',\n },\n ],\n}\n\nexport const VARIANTS: Threaded = {\n id: 'variant',\n declares: [\n {\n id: 'button-primary',\n at: 'app/_probe_variant.ts',\n wears: ['Button'],\n },\n ],\n}\n",
140
+ },
141
+ { at: 'app/_probe_declared.tsx', source: 'export const HELD = <Button.buttonPrimary />\n' },
142
+ ],
143
+ at: 'app/_probe_declared.tsx',
144
+ },
145
+ '<Unknown.anything />',
146
+ ],
147
+ },
148
+ }
@@ -0,0 +1,65 @@
1
+ import type { Slopnator } from '../slopnator.ts'
2
+
3
+ const COMMON = new Set([
4
+ 'a', 'an', 'the', 'of', 'for', 'to', 'and', 'or', 'is', 'it', 'its', 'this', 'that',
5
+ 'in', 'on', 'at', 'as', 'by', 'with', 'be', 'are', 'was', 'one', 'every', 'each',
6
+ ])
7
+
8
+ type Node = {
9
+ type?: string
10
+ name?: string
11
+ key?: { type?: string; name?: string; value?: unknown }
12
+ value?: { type?: string; value?: unknown }
13
+ computed?: boolean
14
+ properties?: readonly Node[]
15
+ }
16
+
17
+ const keyOf = (property: Node): string | undefined => {
18
+ if (property.computed === true) return undefined
19
+ const key = property.key
20
+ if (key?.type === 'Identifier') return key.name
21
+ if (key?.type === 'Literal' && typeof key.value === 'string') return key.value
22
+ return undefined
23
+ }
24
+
25
+ const said = (property: Node | undefined): string | undefined => {
26
+ const held = property?.value
27
+ return held?.type === 'Literal' && typeof held.value === 'string' ? held.value : undefined
28
+ }
29
+
30
+ const words = (text: string): readonly string[] =>
31
+ text.toLowerCase().split(/[^a-z]+/).filter((one) => one !== '' && !COMMON.has(one))
32
+
33
+ export const slopnator: Slopnator = {
34
+ on: ['Property'],
35
+ holds: (node, at) => {
36
+ const property = node as Node
37
+ const key = keyOf(property)
38
+ if (key !== 'describes' && key !== 'means') return false
39
+
40
+ const sentence = said(property)
41
+ if (sentence === undefined) return false
42
+ if (sentence.trim() === '') return `\`${key}\` is empty, so the word arrived and the meaning never did`
43
+
44
+ const owner = (at.ancestors() as readonly Node[]).at(-1)
45
+ const id = said((owner?.properties ?? []).find((one) => keyOf(one) === 'id' || keyOf(one) === 'name'))
46
+ if (id === undefined) return false
47
+
48
+ const own = new Set(words(id))
49
+ if (words(sentence).some((word) => !own.has(word))) return false
50
+ return `\`${key}\` says "${sentence}", which adds no word its id \`${id}\` does not already carry`
51
+ },
52
+ proves: {
53
+ wrong: [
54
+ "const M = { id: 'law', describes: 'The law.' }",
55
+ "const M = { id: 'law', describes: '' }",
56
+ "const M = { id: 'term', means: '' }",
57
+ "const M = { id: 'thread-check', describes: 'The thread check.' }",
58
+ ],
59
+ right: [
60
+ "const M = { id: 'law', describes: 'A rule that must be guaranteed to be enforced.' }",
61
+ "const M = { id: 'term', means: 'A word this repository coins, and what somebody reading it should understand.' }",
62
+ "const M = { id: 'law', at: 'packages/core/integrity/src/constraint.ts' }",
63
+ ],
64
+ },
65
+ }
@@ -0,0 +1,61 @@
1
+ import type { Slopnator } from '../slopnator.ts'
2
+ import { here, islandsDeclared } from './sl_unthreaded.ts'
3
+ import { findTree } from '../../core/tree.ts'
4
+
5
+ const TREE = `${findTree().name}/`
6
+ const GOVERNS = /^\s*governs:\s*'([^']+)'/gm
7
+
8
+ export const slopnator: Slopnator = {
9
+ holds: (_node, at) => {
10
+ const path = here(at.filename)
11
+ if (!path.startsWith(TREE)) return false
12
+
13
+ let islands: readonly string[]
14
+ try {
15
+ islands = islandsDeclared().map((one) => one.id)
16
+ } catch {
17
+ return 'this file cannot be parsed, so the islands the tree declares were never read and nothing a law threaded to govern was measured'
18
+ }
19
+
20
+ for (const match of at.text.matchAll(GOVERNS)) {
21
+ const governed = match[1] ?? ''
22
+ if (islands.includes(governed)) continue
23
+ return `a law governs "${governed}" and the tree declares no island by that name, so the rule reads as coverage of a kind nobody can declare`
24
+ }
25
+
26
+ return false
27
+ },
28
+ proves: {
29
+ wrong: [
30
+ {
31
+ files: [
32
+ {
33
+ at: 'threads/_governed_probe.ts',
34
+ source: "export const L: Constraint = {\n id: 'a-law',\n governs: 'no-island-by-this-name',\n}\n",
35
+ },
36
+ ],
37
+ at: 'threads/_governed_probe.ts',
38
+ },
39
+ ],
40
+ right: [
41
+ {
42
+ files: [
43
+ {
44
+ at: 'threads/_governed_probe.ts',
45
+ source: "export const L: Constraint = {\n id: 'a-law',\n governs: 'entities',\n}\n",
46
+ },
47
+ ],
48
+ at: 'threads/_governed_probe.ts',
49
+ },
50
+ {
51
+ files: [
52
+ {
53
+ at: 'threads/_governed_probe.ts',
54
+ source: "export const M: Islanding = {\n declares: [\n { id: 'events', at: 'packages/core/spec/src/islands/events.ts', governs: ['app/_declare/events.ts'] },\n ],\n}\n",
55
+ },
56
+ ],
57
+ at: 'threads/_governed_probe.ts',
58
+ },
59
+ ],
60
+ },
61
+ }
@@ -0,0 +1,111 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+
4
+ import type { Slopnator } from '../slopnator.ts'
5
+
6
+ const STORES = 'src/stores'
7
+
8
+ /* What hands an operation its seams, seeded once and then the app's own. */
9
+ const SEAMS = 'src/seams.ts'
10
+
11
+ /* What the declarations settle, which the seams file is expected to hand on. */
12
+ const DECLARED = 'DECLARED'
13
+
14
+ /*
15
+ * WHAT THIS READS, AND WHY IT READS THAT RATHER THAN THE TARGETS SHEET.
16
+ *
17
+ * A target that keeps entities derives a store of its own, written by `thread schema` into
18
+ * `src/stores/<target>.ts`. So one of those files existing IS the fact that this app declares
19
+ * somewhere to keep rows — already resolved, by the command that resolved it, with no second reading
20
+ * of what a reach can hold and no chance of the two disagreeing about it.
21
+ *
22
+ * Where rows go is settled in `src/_generated/seams.ts`, which `thread capabilities` writes from those
23
+ * stores and refuses when it drifts. The one place it can still go wrong is `src/seams.ts`, which is
24
+ * the app's own: an app that stops handing on what was settled there hands its operations a store
25
+ * that keeps nothing, while the tables are written, the migrations are written, and the gate is green.
26
+ */
27
+ function held(root: string, at: string): string {
28
+ const path = resolve(root, at)
29
+ try {
30
+ return existsSync(path) ? readFileSync(path, 'utf8') : ''
31
+ } catch {
32
+ return ''
33
+ }
34
+ }
35
+
36
+ function derived(root: string): readonly string[] {
37
+ const at = resolve(root, STORES)
38
+ try {
39
+ return existsSync(at) ? readdirSync(at).filter((one) => one.endsWith('.ts')) : []
40
+ } catch {
41
+ return []
42
+ }
43
+ }
44
+
45
+ export const slopnator: Slopnator = {
46
+ checks: (root) => {
47
+ const stores = derived(root)
48
+ if (stores.length === 0) return []
49
+
50
+ /*
51
+ * AN APP WITH NOTHING THAT HANDS A STORE TO AN OPERATION IS NOT KEEPING ROWS THROUGH ONE, SO
52
+ * THERE IS NOTHING TO REFUSE.
53
+ *
54
+ * The seams file is seeded by `thread capabilities`, which ships in the operations package — so
55
+ * an app that attached data and integrity and nothing else has no seams and no command that
56
+ * seeds one. Its absence means rows go nowhere yet, which is a state to pass rather than refuse:
57
+ * nothing is being lost, because nothing is being kept.
58
+ */
59
+ const where = held(root, SEAMS)
60
+ if (where === '') return []
61
+
62
+ if (where.includes(DECLARED)) return []
63
+
64
+ return [
65
+ `${STORES} holds ${stores.join(', ')} and ${SEAMS} no longer hands on ${DECLARED}, so every row this app keeps goes ` +
66
+ 'somewhere that is gone the moment it stops. The tables are written, the migrations are written, the gate is ' +
67
+ `green, and the database is never opened. Spread ${DECLARED} back into SEAMS, or name its store there`,
68
+ ]
69
+ },
70
+ proves: {
71
+ wrong: [
72
+ {
73
+ files: [
74
+ { at: 'src/stores/pl_probe.ts', source: 'export const store = {}\n' },
75
+ {
76
+ at: 'src/seams.ts',
77
+ source: "import { inMemorySeams } from '../../operations/memory.ts'\n\nexport const SEAMS = inMemorySeams().seams\n",
78
+ },
79
+ ],
80
+ at: 'src/seams.ts',
81
+ },
82
+ ],
83
+ right: [
84
+ {
85
+ files: [
86
+ { at: 'src/stores/pl_probe.ts', source: 'export const store = {}\n' },
87
+ {
88
+ at: 'src/seams.ts',
89
+ source: "import { DECLARED } from './_generated/seams.ts'\n\nexport const SEAMS = { ...DECLARED }\n",
90
+ },
91
+ ],
92
+ at: 'src/seams.ts',
93
+ },
94
+
95
+ /* An app that derived a store and has nothing handing one to an operation — data attached, operations not. */
96
+ {
97
+ files: [{ at: 'src/stores/pl_probe.ts', source: 'export const store = {}\n' }],
98
+ at: 'src/seams.ts',
99
+ },
100
+ {
101
+ files: [
102
+ {
103
+ at: 'src/seams.ts',
104
+ source: "import { inMemorySeams } from '../../operations/memory.ts'\n\nexport const SEAMS = inMemorySeams().seams\n",
105
+ },
106
+ ],
107
+ at: 'src/seams.ts',
108
+ },
109
+ ],
110
+ },
111
+ }
@@ -0,0 +1,41 @@
1
+ import type { Slopnator } from '../slopnator.ts'
2
+ import { declaresIsland, here, islandsDeclared } from './sl_unthreaded.ts'
3
+
4
+ export const slopnator: Slopnator = {
5
+ holds: (_node, at) => {
6
+ const schema = declaresIsland(at.text)
7
+ if (schema === undefined) return false
8
+
9
+ const path = here(at.filename)
10
+ if (islandsDeclared().some((island) => island.at === path)) return false
11
+
12
+ return (
13
+ `${schema}IslandSchema is an island and no row in the tree names it, so nothing states what it governs and ` +
14
+ 'no law can reach it — the kind is declarable and every rule about where it may be declared is absent'
15
+ )
16
+ },
17
+ proves: {
18
+ wrong: [
19
+ {
20
+ files: [
21
+ {
22
+ at: 'packages/core/spec/src/islands/_unnamed_probe.ts',
23
+ source: 'export const ThingIslandSchema = z.strictObject({\n specVersion: z.literal(1),\n})\n',
24
+ },
25
+ ],
26
+ at: 'packages/core/spec/src/islands/_unnamed_probe.ts',
27
+ },
28
+ ],
29
+ right: [
30
+ {
31
+ files: [
32
+ {
33
+ at: 'packages/core/spec/src/islands/_unnamed_probe.ts',
34
+ source: 'export const ThingSchema = z.strictObject({\n name: z.string(),\n})\n',
35
+ },
36
+ ],
37
+ at: 'packages/core/spec/src/islands/_unnamed_probe.ts',
38
+ },
39
+ ],
40
+ },
41
+ }
@@ -0,0 +1,131 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+
4
+ import type { Slopnator } from '../slopnator.ts'
5
+ import { foldersGoverned, here, islandsDeclared, rowsIn, type Declared } from './sl_unthreaded.ts'
6
+ import { findTree } from '../../core/tree.ts'
7
+
8
+ const ROOT = findTree().at
9
+ const TREE = `${findTree().name}/`
10
+
11
+ interface Seeding {
12
+ readonly island: string
13
+ readonly folder: string
14
+ readonly tree: string
15
+ readonly member: string
16
+ }
17
+
18
+ const seeding = (one: Declared): Seeding | undefined => {
19
+ const folder = foldersGoverned(one.governs)[0]
20
+ if (folder === undefined || one.tree === undefined || one.member === undefined) return undefined
21
+ return { island: one.id, folder, tree: one.tree, member: one.member }
22
+ }
23
+
24
+ const islandsSeeding = (): readonly Seeding[] =>
25
+ islandsDeclared().flatMap((one) => {
26
+ const held = seeding(one)
27
+ return held === undefined ? [] : [held]
28
+ })
29
+
30
+ const declares = (island: string): RegExp => new RegExp(`^export const ${island.toUpperCase()}\\b`, 'm')
31
+
32
+ const exports = (member: string): RegExp => new RegExp(`^export const ${member}\\b`, 'm')
33
+
34
+ function seededNames(text: string): readonly string[] {
35
+ const found: string[] = []
36
+ for (const row of rowsIn(text)) {
37
+ if (row.list !== undefined || row.id === undefined) continue
38
+ found.push(`${row.id}.ts`)
39
+ }
40
+ return found
41
+ }
42
+
43
+ function onDisk(folder: string): readonly string[] {
44
+ const dir = resolve(ROOT, folder)
45
+ if (!existsSync(dir)) return []
46
+ try {
47
+ return readdirSync(dir).filter((name) => name.endsWith('.ts'))
48
+ } catch {
49
+ return []
50
+ }
51
+ }
52
+
53
+ function declaredNames(one: Seeding): readonly string[] {
54
+ const path = resolve(ROOT, one.tree)
55
+ if (!existsSync(path)) return []
56
+ try {
57
+ return seededNames(readFileSync(path, 'utf8'))
58
+ } catch {
59
+ return []
60
+ }
61
+ }
62
+
63
+ export const slopnator: Slopnator = {
64
+ holds: (_node, at) => {
65
+ const path = here(at.filename)
66
+
67
+ for (const one of islandsSeeding()) {
68
+ if (path.startsWith(TREE)) {
69
+ if (!declares(one.island).test(at.text)) continue
70
+
71
+ let wanted: readonly string[]
72
+ try {
73
+ wanted = seededNames(at.text)
74
+ } catch {
75
+ return 'this file cannot be parsed, so the members it declares were never read and no file was measured against them'
76
+ }
77
+
78
+ const found = onDisk(one.folder)
79
+ for (const name of wanted) {
80
+ if (found.includes(name)) continue
81
+ return `a row here declares ${one.member} \`${name.replace(/\.ts$/, '')}\` and ${one.folder}${name} is not on disk, so the row brings nothing into existence — run \`pnpm member:seed\``
82
+ }
83
+
84
+ return false
85
+ }
86
+
87
+ if (!path.startsWith(one.folder)) continue
88
+ const name = path.slice(one.folder.length)
89
+ if (!name.endsWith('.ts') || name.includes('/')) continue
90
+
91
+ if (!declaredNames(one).includes(name)) {
92
+ return `${one.folder} holds ${one.member} \`${name.replace(/\.ts$/, '')}\` and no row in ${one.tree} declares it, so it can act while nothing brought it into existence — declare the row first and let \`pnpm member:seed\` write the file, or delete it`
93
+ }
94
+
95
+ if (!exports(one.member).test(at.text)) {
96
+ return `${name} is declared and exports no \`${one.member}\`, so the row and the file both exist and the ${one.member} still loads as nothing — write it into the template the seeder left`
97
+ }
98
+
99
+ return false
100
+ }
101
+
102
+ return false
103
+ },
104
+ proves: {
105
+ wrong: [
106
+ {
107
+ files: [
108
+ {
109
+ at: 'threads/_member_probe.ts',
110
+ source: "export const OPERATIONS: readonly Member[] = [\n {\n id: 'wf_unseeded_probe',\n at: 'threads/operations.ts',\n },\n]\n",
111
+ },
112
+ ],
113
+ at: 'threads/_member_probe.ts',
114
+ },
115
+ {
116
+ files: [{ at: 'operations/wf_undeclared_probe.ts', source: 'export const operation = { id: 1 }\n' }],
117
+ at: 'operations/wf_undeclared_probe.ts',
118
+ },
119
+ ],
120
+ right: [
121
+ {
122
+ files: [{ at: 'threads/_member_probe.ts', source: 'export const OPERATIONS: readonly Member[] = []\n' }],
123
+ at: 'threads/_member_probe.ts',
124
+ },
125
+ {
126
+ files: [{ at: 'threads/_member_quiet.ts', source: 'export const NOTHING = []\n' }],
127
+ at: 'threads/_member_quiet.ts',
128
+ },
129
+ ],
130
+ },
131
+ }
@@ -0,0 +1,58 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join, resolve } from 'node:path'
3
+
4
+ import { SEEDED, SHIPPED, slopnatorName, type Slopnator } from '../slopnator.ts'
5
+ import { here } from './sl_unthreaded.ts'
6
+ import { findTree } from '../../core/tree.ts'
7
+
8
+ const ROOT = findTree().at
9
+ const TREE = `${findTree().name}/`
10
+ const ID = /^ {4}id:\s*'([^']+)'/gm
11
+ const DECLARES = /^(?:export )?const (CONSTRAINTS|RULES)\b/m
12
+
13
+ const idsIn = (text: string): readonly string[] => [...text.matchAll(ID)].map((one) => one[1] ?? '')
14
+
15
+ export const slopnator: Slopnator = {
16
+ holds: (_node, at) => {
17
+ const path = here(at.filename)
18
+
19
+ if (path.startsWith(TREE) && DECLARES.test(at.text)) {
20
+ for (const id of idsIn(at.text)) {
21
+ const name = `${slopnatorName(id)}.ts`
22
+ if ([SHIPPED, resolve(ROOT, SEEDED)].some((dir) => existsSync(join(dir, name)))) continue
23
+ return `"${id}" names "${slopnatorName(id)}", and no such file is in ${SEEDED}/ or shipped with the package — one that cannot run registers, reports nothing, and is indistinguishable from one being obeyed. Seed it`
24
+ }
25
+ return false
26
+ }
27
+
28
+ const home = [SEEDED].find((dir) => path.startsWith(`${dir}/`))
29
+ if (home === undefined) return false
30
+ const file = path.slice(home.length + 1)
31
+ if (!file.startsWith('sl_') || !file.endsWith('.ts')) return false
32
+
33
+ const declared = [join(SHIPPED, '..', 'constraint.ts'), resolve(ROOT, `${TREE}rules.ts`)]
34
+ .map((one) => { try { return readFileSync(one, 'utf8') } catch { return '' } })
35
+ .join('\n')
36
+ const named = new Set(idsIn(declared).map((id) => slopnatorName(id)))
37
+ if (named.has(file.replace(/\.ts$/, ''))) return false
38
+ return `${path} is named by no law, so it governs nothing while looking like enforcement to anybody reading the directory — delete it, or declare the law that needs it. A law renamed makes this a \`git mv\`, not a second file`
39
+ },
40
+ proves: {
41
+ wrong: [
42
+ "const CONSTRAINTS = [\n {\n id: 'nothing-names-this-slopnator',\n },\n]",
43
+ {
44
+ files: [{ at: 'threads/slopnators/sl_no_law_names_this.ts', source: "export const slopnator = { on: [], holds: () => false }\n" }],
45
+ at: 'threads/slopnators/sl_no_law_names_this.ts',
46
+ },
47
+ ],
48
+ right: [
49
+ "const CONSTRAINTS = [\n {\n id: 'no-comment',\n },\n]",
50
+ "const CONSTRAINTS = [\n {\n id: 'no-comment',\n },\n {\n id: 'unthreaded',\n },\n]",
51
+ 'export const PLAIN = 1',
52
+ {
53
+ files: [{ at: 'threads/slopnators/not_a_walk.ts', source: 'export const HELPER = 1\n' }],
54
+ at: 'threads/slopnators/not_a_walk.ts',
55
+ },
56
+ ],
57
+ },
58
+ }