1 import Base 2 import bend-net-json@0.3.0.0/json.bend as Json 3 import ../../relationship/type.bend as Rel 4 import ../../category/type.bend as K 5 import ../../environment/external/type.bend as Ex 6 import ./type.bend as Ct 7 import ./component/type.bend as Co 8 9 # A container's stack against its repository: the frameworks and libraries it declares are its 10 # dependencies, its source files are in its declared languages, and it has the SDK of each model API 11 # it calls. What it lists as `migrating` may be absent. 12 13 def key_of(c: Ct.Container) -> String: 14 match c: 15 case Ct.Container{k, _, _, _, _, _, _, _, _, _, _, _, _, _, _}: 16 k 17 18 def source_of(c: Ct.Container) -> String: 19 match c: 20 case Ct.Container{_, _, _, _, _, _, _, s, _, _, _, _, _, _, _}: 21 s 22 23 def keep_built(+c: Ct.Container, +rest: List<&2, Ct.Container>) -> List<&2, Ct.Container>: 24 Bool.pick(List<&2, Ct.Container>, String.is_empty(source_of(c)), rest, c <> rest) 25 26 # The containers built from this repository: those with a source folder. 27 def built(cs: List<&2, Ct.Container>) -> List<&2, Ct.Container>: 28 match cs: 29 case []: 30 [] 31 case c <> rest: 32 keep_built(c, built(rest)) 33 34 # ---- names ---- 35 # Declared names are readable names; dependencies are package names. 36 def package_name(+name: String) -> String: 37 Bool.pick(String, String.eq(name, "Next.js"), "next", 38 Bool.pick(String, String.eq(name, "Tailwind CSS"), "tailwindcss", 39 Bool.pick(String, String.eq(name, "React"), "react", 40 Bool.pick(String, String.eq(name, "Convex"), "convex", 41 Bool.pick(String, String.eq(name, "OpenRouter SDK"), "@openrouter/sdk", 42 Bool.pick(String, String.eq(name, "Sentry SDK"), "@sentry/nextjs", 43 Bool.pick(String, String.eq(name, "whatsmeow"), "go.mau.fi/whatsmeow", name))))))) 44 45 # Whether a language is written in files with this extension. 46 def language_extension(+name: String, +ext: String) -> Bool: 47 (String.eq(name, "TypeScript") && (String.eq(ext, ".ts") || String.eq(ext, ".tsx"))) || 48 (String.eq(name, "Go") && String.eq(ext, ".go")) || 49 (String.eq(name, "JavaScript") && (String.eq(ext, ".js") || String.eq(ext, ".mjs") || String.eq(ext, ".jsx"))) || 50 (String.eq(name, "Python") && String.eq(ext, ".py")) || 51 (String.eq(name, "Bend") && String.eq(ext, ".bend")) || 52 (String.eq(name, "C") && (String.eq(ext, ".c") || String.eq(ext, ".h"))) 53 54 def any_language(+ext: String, xs: List<&2, String>) -> Bool: 55 match xs: 56 case []: 57 False{} 58 case name <> rest: 59 language_extension(name, ext) || any_language(ext, rest) 60 61 # The extensions of every language V knows; files with other extensions are not source. 62 def known(+ext: String) -> Bool: 63 any_language(ext, ["TypeScript", "Go", "JavaScript", "Python", "Bend", "C"]) 64 65 # ---- what is missing ---- 66 # The declared names that are neither dependencies nor migrating. 67 def absent(xs: List<&2, String>, +deps: List<&2, String>, +migrating: List<&2, String>) -> List<&2, String>: 68 match xs: 69 case []: 70 [] 71 case +x <> rest: 72 +more = absent(rest, deps, migrating) 73 Bool.pick(List<&2, String>, List.contains(~String, ~String.eq, deps, package_name(x)) || List.contains(~String, ~String.eq, migrating, x), more, x <> more) 74 75 # The frameworks and libraries a built container declares but does not depend on. 76 def stack_missing(c: Ct.Container, +deps: List<&2, String>) -> List<&2, String>: 77 match c: 78 case Ct.Container{_, _, _, _, _, _, _, source, _, fs, ls, _, +ms, _, _}: 79 Bool.pick(List<&2, String>, String.is_empty(source), [], absent(List.append(&2, String, fs, ls), deps, ms)) 80 81 # The extensions in a built container's source that no declared or migrating language is written in, 82 # and that are not themselves migrating. 83 def unwritten(xs: List<&2, String>, +languages: List<&2, String>, +migrating: List<&2, String>) -> List<&2, String>: 84 match xs: 85 case []: 86 [] 87 case +x <> rest: 88 +more = unwritten(rest, languages, migrating) 89 Bool.pick(List<&2, String>, any_language(x, languages) || any_language(x, migrating) || List.contains(~String, ~String.eq, migrating, x), more, x <> more) 90 91 def languages_missing(c: Ct.Container, +extensions: List<&2, String>) -> List<&2, String>: 92 match c: 93 case Ct.Container{_, _, _, _, _, _, _, source, _, _, _, langs, +ms, _, _}: 94 Bool.pick(List<&2, String>, String.is_empty(source), [], unwritten(extensions, langs, ms)) 95 96 # ---- the SDK of each model API a container calls ---- 97 def destinations(rs: List<&2, Rel.Relationship>) -> List<&2, String>: 98 match rs: 99 case []: 100 [] 101 case r <> rest: 102 match r: 103 case Rel.Relationship{d, _, _, _}: 104 d <> destinations(rest) 105 106 def component_destinations(cos: List<&2, Co.Component>) -> List<&2, String>: 107 match cos: 108 case []: 109 [] 110 case co <> rest: 111 match co: 112 case Co.Component{_, _, _, _, us}: 113 List.append(&2, String, destinations(us), component_destinations(rest)) 114 115 def model_libraries(cat: K.Category, ls: List<&2, String>) -> List<&2, String>: 116 match cat: 117 case K.ModelApi{}: 118 ls 119 case _: 120 [] 121 122 # The SDK of the model API with this key, among the externals. 123 def sdk_of(+dest: String, xs: List<&2, Ex.External>) -> List<&2, String>: 124 match xs: 125 case []: 126 [] 127 case x <> rest: 128 match x: 129 case Ex.External{k, _, _, cat, _, _, ls, _}: 130 List.append(&2, String, Bool.pick(List<&2, String>, String.eq(k, dest), model_libraries(cat, ls), []), sdk_of(dest, rest)) 131 132 def sdks(ds: List<&2, String>, +xs: List<&2, Ex.External>) -> List<&2, String>: 133 match ds: 134 case []: 135 [] 136 case d <> rest: 137 List.append(&2, String, sdk_of(d, xs), sdks(rest, xs)) 138 139 # The SDK libraries of the model APIs a built container (or one of its components) calls that it does 140 # not depend on. 141 def sdk_missing(c: Ct.Container, +xs: List<&2, Ex.External>, +deps: List<&2, String>) -> List<&2, String>: 142 match c: 143 case Ct.Container{_, _, _, _, _, _, _, source, _, _, _, _, +ms, cos, us}: 144 Bool.pick(List<&2, String>, String.is_empty(source), [], 145 absent(Set.to_list(Set.from_list(sdks(List.append(&2, String, destinations(us), component_destinations(cos)), xs))), deps, ms)) 146 147 # ---- reading the repository's evidence ---- 148 def filled(xs: List<&2, String>) -> List<&2, String>: 149 match xs: 150 case []: 151 [] 152 case +x <> rest: 153 +more = filled(rest) 154 Bool.pick(List<&2, String>, String.is_empty(x) || String.eq(x, "."), more, x <> more) 155 156 def nth(+xs: List<&2, String>, n: Nat) -> String: 157 Maybe.default(&2, String, List.get(&2, String, xs, n), "") 158 159 def last(+xs: List<&2, String>) -> String: 160 Maybe.default(&2, String, List.head(&2, String, List.reverse(&2, String, xs)), "") 161 162 def prefixes(segs: List<&2, String>, +acc: String) -> List<&2, String>: 163 match segs: 164 case []: 165 [acc] 166 case +s <> rest: 167 acc <> prefixes(rest, Bool.pick(String, String.is_empty(acc), s, acc ++ "/" ++ s)) 168 169 # A folder and each folder above it, nearest first, ending with the repository root (""). 170 def folders(+dir: String) -> List<&2, String>: 171 List.reverse(&2, String, prefixes(filled(String.split(dir, '/')), "")) 172 173 def json_or_null(m: Maybe<&2, Json.Val>) -> Json.Val: 174 match m: 175 case None{}: 176 Json.Null{} 177 case Some{v}: 178 v 179 180 def keys(v: Maybe<&2, Json.Val>) -> List<&2, String>: 181 match v: 182 case Some{Json.Obj{m}}: 183 Map.keys(&2, Json.Val, m) 184 case _: 185 [] 186 187 # A package.json's dependencies and devDependencies. 188 def package_deps(+text: String) -> List<&2, String>: 189 +v = json_or_null(Json.parse(text)) 190 List.append(&2, String, keys(Json.get(v, "dependencies")), keys(Json.get(v, "devDependencies"))) 191 192 # The module a go.mod line requires, or "". 193 def module_name(+line: String) -> String: 194 +words = filled(String.split(String.join(String.split(String.trim(line), '\t'), " "), ' ')) 195 +first = nth(words, 0n) 196 +second = nth(words, 1n) 197 Bool.pick(String, String.eq(first, "require") && String.starts_with(nth(words, 2n), "v"), second, 198 Bool.pick(String, String.starts_with(second, "v") && Bool.not(String.eq(first, "module")), first, "")) 199 200 def go_deps(ls: List<&2, String>) -> List<&2, String>: 201 match ls: 202 case []: 203 [] 204 case +l <> rest: 205 +more = go_deps(rest) 206 Bool.pick(List<&2, String>, String.is_empty(module_name(l)), more, module_name(l) <> more) 207 208 # The source extension of a repository path, or "" when it is not source: generated files, and files 209 # under node_modules, _generated or .next. 210 def source_ext(+path: String) -> String: 211 +parts = String.split(path, '/') 212 +name = last(parts) 213 +ext = "." ++ last(String.split(name, '.')) 214 Bool.pick(String, String.contains(name, ".") && Bool.not(String.contains(name, ".generated.")) && known(ext) && 215 Bool.not(List.contains(~String, ~String.eq, parts, "node_modules") || List.contains(~String, ~String.eq, parts, "_generated") || List.contains(~String, ~String.eq, parts, ".next")), 216 ext, "") 217 218 def source_exts(paths: List<&2, String>) -> List<&2, String>: 219 match paths: 220 case []: 221 [] 222 case +p <> rest: 223 +more = source_exts(rest) 224 Bool.pick(List<&2, String>, String.is_empty(source_ext(p)), more, source_ext(p) <> more) 225 226 # The distinct source extensions among repository paths. 227 def source_extensions(paths: List<&2, String>) -> List<&2, String>: 228 Set.to_list(Set.from_list(source_exts(paths)))