@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.
- 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,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
|
+
}
|