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