domain/architecture/system/effects.bend
near-v-framework@0.4.0.0 · 457 lines · 53 definitions
1import Base
2import bend-kit-files@0.1.0.0/files.bend as Fs
3import bend-net-json@0.3.0.0/json.bend as Json
4import near-v-framework@0.4.0.0/domain/architecture/system/effs/io.bend as X
5import near-v-framework@0.4.0.0/domain/architecture/type.bend as A
6import near-v-framework@0.4.0.0/domain/architecture/ops.bend as O
7import near-v-framework@0.4.0.0/domain/architecture/system/container/effects.bend as CtE
8import near-v-framework@0.4.0.0/domain/architecture/system/deployment/type.bend as D
9import near-v-framework@0.4.0.0/domain/architecture/system/deployment/ops.bend as DO
10import near-v-framework@0.4.0.0/domain/architecture/system/deployment/azure/effects.bend as AzE
11import near-v-framework@0.4.0.0/domain/architecture/system/deployment/nix_darwin/type.bend as ND
12import near-v-framework@0.4.0.0/domain/architecture/system/deployment/nix_darwin/ops.bend as NDO
13import near-v-framework@0.4.0.0/domain/architecture/system/deployment/nix_darwin/effects.bend as NDE
14import near-v-framework@0.4.0.0/domain/architecture/system/deployment/nix_darwin/setting/type.bend as St
15import near-v-framework@0.4.0.0/domain/architecture/system/deployment/nix_darwin/setting/ops.bend as StO
16import near-v-framework@0.4.0.0/domain/architecture/system/deployment/nix_darwin/brew/type.bend as Br
17import near-v-framework@0.4.0.0/domain/architecture/system/deployment/nix_darwin/setting/effects.bend as StE
18import near-v-framework@0.4.0.0/domain/architecture/system/deployment/nix_darwin/brew/effects.bend as BrE
19import near-v-framework@0.4.0.0/domain/architecture/system/deployment/nix_darwin/service/ops.bend as SvO
20import near-v-framework@0.4.0.0/domain/architecture/system/release/type.bend as Rl
21import near-v-framework@0.4.0.0/domain/architecture/system/release/ops.bend as RlO
22import near-v-framework@0.4.0.0/domain/architecture/system/plan/type.bend as P
23import near-v-framework@0.4.0.0/domain/architecture/system/plan/ops.bend as PO
24import near-v-framework@0.4.0.0/domain/architecture/system/ops.bend as SO
25
26
27
28
29
30
31def files(+root: String) -> IO(List<&2, String>):
32 do IO<List<&2, String>>:
33 out : String <- X.run("git", ["-C", root, "ls-files", "-co", "--exclude-standard"])
34 return X.nonempty(String.lines(out))
35
36
37def second_word(+t: String) -> String:
38 Maybe.default(&2, String, List.get(&2, String, X.nonempty(String.split(t, ' ')), 1n), "")
39
40def unquote(+w: String) -> String:
41 Bool.pick(String, String.starts_with(w, "\""), Maybe.default(&2, String, List.get(&2, String, String.split(w, '"'), 1n), ""), w)
42
43
44def import_of(+line: String, +dir: String) -> String:
45 +w = unquote(second_word(String.trim(line)))
46 Bool.pick(String, String.starts_with(String.trim(line), "import ") && (String.starts_with(w, "./") || String.starts_with(w, "../")),
47 Fs.path.normalize(Fs.path.join(dir, w)), "")
48
49def imports(ls: List<&2, String>, +dir: String) -> List<&2, String>:
50 match ls:
51 case []:
52 []
53 case l <> rest:
54 X.nonempty(import_of(l, dir) <> imports(rest, dir))
55
56
57def reach(fuel: Nat, +todo: List<&2, String>, +seen: List<&2, String>, +root: String) -> IO(List<&2, String>):
58 match fuel:
59 case 0n:
60 IO.pure(List<&2, String>, seen)
61 case 1n+k:
62 match todo:
63 case []:
64 IO.pure(List<&2, String>, seen)
65 case +f <> rest:
66 do IO<List<&2, String>>:
67 text : String <- X.read_head(root ++ "/" ++ f, 4096)
68 +known : Bool = List.contains(~String, ~String.eq, seen, f)
69 reach(k, List.append(&2, String, rest, Bool.pick(List<&2, String>, known, [], imports(String.lines(text), Fs.path.parent(f)))), Bool.pick(List<&2, String>, known, seen, f <> seen), root)
70
71
72def differing(fs: List<&2, String>, +local: String, +hub: String) -> IO(String):
73 match fs:
74 case []:
75 IO.pure(String, "")
76 case +f <> rest:
77 do IO<String>:
78 same : Bool <- X.succeeds("cmp", ["-s", local ++ "/" ++ f, hub ++ "/" ++ f])
79 more : String <- differing(rest, local, hub)
80 return Bool.pick(String, same, more, f)
81
82def hub_of(+published: String, +f: String) -> P.Hub:
83 Bool.pick(P.Hub, String.is_empty(published), P.Unpublished{}, Bool.pick(P.Hub, String.is_empty(f), P.Same{}, P.Differs{f}))
84
85
86
87def fetch_if(missing: Bool, +home: String, +n: String, +v: String, +e: String, +cached: String) -> IO(String):
88 match missing:
89 case False{}:
90 IO.pure(String, cached)
91 case True{}:
92 do IO<String>:
93 X.write("/tmp/v-probe.bend", "import Base\nimport " ++ n ++ "@" ++ v ++ "/" ++ Fs.path.file_name(e) ++ " as P\n")
94 _ : Bool <- X.succeeds(home ++ "/.bend/bin/bend", ["/tmp/v-probe.bend", "--check-only"])
95 X.read(home ++ "/.bend/lib/names/" ++ n ++ "@" ++ v)
96
97def fetched(+home: String, +n: String, +v: String, +e: String, +cached: String) -> IO(String):
98 fetch_if(String.is_empty(String.trim(cached)), home, n, v, e, cached)
99
100def hub_state(+home: String, +root: String, p: Rl.Package) -> IO(P.Hub):
101 match p:
102 case Rl.Package{+n, +v, +e, _}:
103 do IO<P.Hub>:
104 cached : String <- X.read(home ++ "/.bend/lib/names/" ++ n ++ "@" ++ v)
105 +hash : String <- fetched(home, n, v, e, cached)
106 +files : List<&2, String> <- reach(1000n, [e], [], root)
107 f : String <- differing(files, root, home ++ "/.bend/lib/" ++ String.trim(hash))
108 return hub_of(String.trim(hash), f)
109
110def hub_states(+home: String, +root: String, ps: List<&2, Rl.Package>) -> IO(List<&2, P.Hub>):
111 match ps:
112 case []:
113 IO.pure(List<&2, P.Hub>, [])
114 case p <> rest:
115 do IO<List<&2, P.Hub>>:
116 h : P.Hub <- hub_state(home, root, p)
117 hs : List<&2, P.Hub> <- hub_states(home, root, rest)
118 return h <> hs
119
120
121def first_word(+t: String) -> String:
122 Maybe.default(&2, String, List.head(&2, String, X.nonempty(String.split(t, '\t'))), "")
123
124def pushed(+root: String, +sha: String) -> IO(Bool):
125 Bool.pick(IO(Bool), String.is_empty(sha), IO.pure(Bool, False{}), X.succeeds("git", ["-C", root, "merge-base", "--is-ancestor", "HEAD", sha]))
126
127def remote(+root: String, r: Rl.Repository) -> IO(P.Remote):
128 match r:
129 case Rl.Repository{+name, _}:
130 do IO<P.Remote>:
131 origin : String <- X.run("git", ["-C", root, "remote", "get-url", "origin"])
132 vis : String <- X.run("gh", ["repo", "view", name, "--json", "visibility", "--jq", ".visibility"])
133 status : String <- X.run("git", ["-C", root, "status", "--porcelain"])
134 head : String <- X.run("git", ["-C", root, "ls-remote", "origin", "refs/heads/main"])
135 up : Bool <- pushed(root, first_word(head))
136 return P.Remote{String.trim(origin), String.to_lower(String.trim(vis)), String.is_empty(String.trim(status)), up}
137
138
139
140
141def plugin_dir(+root: String, +n: String) -> String:
142 root ++ "/.git/v-plugin/" ++ n
143
144def tag_of(+n: String, +v: String) -> String:
145 n ++ "-v" ++ v
146
147def last_segment(xs: List<&2, String>) -> String:
148 Maybe.default(&2, String, List.last(&2, String, xs), "")
149
150def owner_of(+r: String) -> String:
151 Maybe.default(&2, String, List.head(&2, String, String.split(r, '/')), "")
152
153def str_of(m: Maybe<&2, Json.Val>) -> String:
154 match m:
155 case Some{Json.Str{s}}:
156 s
157 case _:
158 ""
159
160def version_in(m: Maybe<&2, Json.Val>) -> String:
161 match m:
162 case Some{v}:
163 str_of(Json.get(v, "version"))
164 case None{}:
165 ""
166
167def obj.go(kvs: List<&2, Sigma<&2, &2, String, _ => Json.Val>>, m: Map<&2, Json.Val>) -> Map<&2, Json.Val>:
168 match kvs:
169 case []:
170 m
171 case (k, v) <> rest:
172 obj.go(rest, Map.set(&2, Json.Val, m, k, v))
173
174def obj(kvs: List<&2, Sigma<&2, &2, String, _ => Json.Val>>) -> Json.Val:
175 Json.Obj{obj.go(kvs, Map.new(&2, Json.Val))}
176
177
178def others(vs: List<&2, Json.Val>, +n: String) -> List<&2, Json.Val>:
179 match vs:
180 case []:
181 []
182 case +v <> rest:
183 +kept = others(rest, n)
184 Bool.pick(List<&2, Json.Val>, String.eq(str_of(Json.get(v, "name")), n), kept, v <> kept)
185
186def listed(m: Maybe<&2, Json.Val>) -> List<&2, Json.Val>:
187 match m:
188 case Some{Json.Arr{vs}}:
189 vs
190 case _:
191 []
192
193
194def marketplace(+now: String, +n: String, +v: String, +r: String) -> String:
195 +m = Json.parse(now)
196 Json.encode(obj([
197 ("name", Json.Str{last_segment(String.split(r, '/'))}),
198 ("owner", obj([("name", Json.Str{owner_of(r)})])),
199 ("plugins", Json.Arr{List.append(&2, Json.Val, others(listed(Bool.pick(Maybe<&2, Json.Val>, Maybe.is_some(&2, Json.Val, m), Json.get(Maybe.default(&2, Json.Val, m, Json.Null{}), "plugins"), None{})), n),
200 [obj([("name", Json.Str{n}), ("source", Json.Str{"./plugins/" ++ n}), ("version", Json.Str{v})])])})])) ++ "\n"
201
202def build_plugin(+root: String, +bend: String, +n: String, +e: String) -> IO(String):
203 do IO<String>:
204 +build : String = plugin_dir(root, n) ++ "/build"
205 _ : Bool <- X.succeeds("rm", ["-rf", plugin_dir(root, n)])
206 _ : Bool <- X.succeeds("mkdir", ["-p", build])
207 ok : Bool <- X.succeeds(bend, [root ++ "/" ++ e, "plugin", build])
208 manifest : String <- X.read(build ++ "/.claude-plugin/plugin.json")
209 return Bool.pick(String, ok, version_in(Json.parse(manifest)), "")
210
211def tag_differs(tagged: Bool, +root: String, +n: String, +v: String, +r: String) -> IO(Bool):
212 match tagged:
213 case False{}:
214 IO.pure(Bool, False{})
215 case True{}:
216 do IO<Bool>:
217 _ : Bool <- X.succeeds("git", ["clone", "-q", "--depth", "1", "--branch", tag_of(n, v), "https://github.com/" ++ r, plugin_dir(root, n) ++ "/tag"])
218 same : Bool <- X.succeeds("diff", ["-rq", plugin_dir(root, n) ++ "/build", plugin_dir(root, n) ++ "/tag/plugins/" ++ n])
219 return Bool.not(same)
220
221def tag_state(+home: String, +root: String, g: Rl.Plugin) -> IO(P.Tag):
222 match g:
223 case Rl.Plugin{+n, +v, +e, _, Rl.Repository{+r, _}}:
224 do IO<P.Tag>:
225 built : String <- build_plugin(root, home ++ "/.bend/bin/bend", n, e)
226 +repo : Bool <- X.succeeds("gh", ["repo", "view", r, "--json", "name"])
227 tags : String <- X.run("git", ["ls-remote", "--tags", "https://github.com/" ++ r, "refs/tags/" ++ tag_of(n, v)])
228 +tagged : Bool = repo && Bool.not(String.is_empty(String.trim(tags)))
229 differs : Bool <- tag_differs(tagged, root, n, v, r)
230 return P.Tag{repo, tagged, built, differs}
231
232def tag_states(+home: String, +root: String, gs: List<&2, Rl.Plugin>) -> IO(List<&2, P.Tag>):
233 match gs:
234 case []:
235 IO.pure(List<&2, P.Tag>, [])
236 case g <> rest:
237 do IO<List<&2, P.Tag>>:
238 t : P.Tag <- tag_state(home, root, g)
239 ts : List<&2, P.Tag> <- tag_states(home, root, rest)
240 return t <> ts
241
242
243def release_plugin(+root: String, +n: String, +v: String, +r: String, create: Bool) -> IO(Bool):
244 do IO<Bool>:
245 +repo : String = plugin_dir(root, n) ++ "/repo"
246 _ : Bool <- Bool.pick(IO(Bool), create, X.succeeds("gh", ["repo", "create", r, "--public", "--description", "Claude Code plugins, built and released by V."]), IO.pure(Bool, True{}))
247 _ : Bool <- X.succeeds("rm", ["-rf", repo])
248 _ : Bool <- X.succeeds("git", ["clone", "-q", "https://github.com/" ++ r, repo])
249 _ : Bool <- X.succeeds("mkdir", ["-p", repo ++ "/plugins/" ++ n, repo ++ "/.claude-plugin"])
250 _ : Bool <- X.succeeds("rsync", ["-a", "--delete", plugin_dir(root, n) ++ "/build/", repo ++ "/plugins/" ++ n ++ "/"])
251 now : String <- X.read(repo ++ "/.claude-plugin/marketplace.json")
252 X.write(repo ++ "/.claude-plugin/marketplace.json", marketplace(now, n, v, r))
253 _ : Bool <- X.succeeds("git", ["-C", repo, "add", "-A"])
254 _ : Bool <- X.succeeds("git", ["-C", repo, "commit", "-q", "-m", n ++ " " ++ v])
255 _ : Bool <- X.succeeds("git", ["-C", repo, "tag", tag_of(n, v)])
256 X.succeeds("git", ["-C", repo, "push", "-q", "origin", "HEAD:main", tag_of(n, v)])
257
258
259def root() -> IO(String):
260 do IO<String>:
261 t : String <- X.run("git", ["rev-parse", "--show-toplevel"])
262 return String.trim(t)
263
264
265def observe(+root: String, +entry: String, r: Rl.Release) -> IO(P.Observed):
266 match r:
267 case Rl.Release{repo, +ps, +gs}:
268 do IO<P.Observed>:
269 +h : String <- X.home()
270 +fs : List<&2, String> <- files(root)
271 reached : List<&2, String> <- reach(Nat.mul(1n+List.length(&2, String, fs), 1n+List.length(&2, String, fs)), entry <> List.append(&2, String, RlO.entries(ps), RlO.plugin_entries(gs)), [], root)
272 hs : List<&2, P.Hub> <- hub_states(h, root, ps)
273 rm : P.Remote <- remote(root, repo)
274 ts : List<&2, P.Tag> <- tag_states(h, root, gs)
275 return P.Observed{fs, reached, hs, rm, ts}
276
277
278def apply(+root: String, +bend: String, s: P.Step) -> IO(Unit):
279 match s:
280 case P.Current{_}:
281 IO.print(PO.show(s))
282 case P.Publish{+n, +v, +e}:
283 do IO<Unit>:
284 out : String <- X.run(bend, [root ++ "/" ++ e, "--publish", n ++ "@" ++ v])
285 IO.print(Bool.pick(String, String.contains(out, "published"), "published " ++ n ++ "@" ++ v, "failed publish " ++ n ++ "@" ++ v))
286 case P.Push{+r}:
287 do IO<Unit>:
288 ok : Bool <- X.succeeds("git", ["-C", root, "push", "-q", "origin", "HEAD:main"])
289 IO.print(Bool.pick(String, ok, "pushed github " ++ r, "failed push github " ++ r))
290 case P.ReleasePlugin{+n, +v, +r, create}:
291 do IO<Unit>:
292 ok : Bool <- release_plugin(root, n, v, r, create)
293 IO.print(Bool.pick(String, ok, "released plugin " ++ n ++ "@" ++ v, "failed release plugin " ++ n ++ "@" ++ v))
294 case P.Rebuild{+h, +fl, +mac}:
295 do IO<Unit>:
296 _ : Result<&1, &1, U32 & String, Unit> <- Fs.mkdir(root ++ "/nix")
297 X.write(root ++ "/nix/flake.nix", fl)
298 X.write(root ++ "/nix/mac.nix", mac)
299 ok : Bool <- X.succeeds("sudo", ["-n", "/run/current-system/sw/bin/darwin-rebuild", "switch", "--flake", root ++ "/nix#" ++ h])
300 IO.print(Bool.pick(String, ok, "rebuilt nix-darwin " ++ h, "failed darwin-rebuild switch " ++ h))
301 case P.ServePort{+p, +t, ts}:
302 do IO<Unit>:
303 ok : Bool <- X.succeeds(ts, ["serve", "--bg", "--yes", "--https=" ++ U32.show(p), t])
304 IO.print(Bool.pick(String, ok, "served :" ++ U32.show(p) ++ " -> " ++ t, "failed serve :" ++ U32.show(p)))
305 case P.Change{_, _, _}:
306 IO.pure(Unit, Unit{})
307 case P.Blocked{_, _}:
308 IO.print(PO.show(s))
309
310def apply_all(+root: String, +bend: String, ss: List<&2, P.Step>) -> IO(Unit):
311 match ss:
312 case []:
313 IO.pure(Unit, Unit{})
314 case s <> rest:
315 do IO<Unit>:
316 apply(root, bend, s)
317 apply_all(root, bend, rest)
318
319def deploy_if(blocked: Bool, +root: String, +ss: List<&2, P.Step>) -> IO(Unit):
320 match blocked:
321 case True{}:
322 IO.die(Unit, 1, PO.show_all(ss) ++ "nothing deployed: the plan is blocked")
323 case False{}:
324 do IO<Unit>:
325 +h : String <- X.home()
326 apply_all(root, h ++ "/.bend/bin/bend", ss)
327
328def deploy(+root: String, +ss: List<&2, P.Step>) -> IO(Unit):
329 deploy_if(PO.any_blocked(ss), root, ss)
330
331
332
333def has_change(ss: List<&2, P.Step>) -> Bool:
334 match ss:
335 case []:
336 False{}
337 case s <> rest:
338 match s:
339 case P.Change{_, _, _}:
340 True{}
341 case _:
342 has_change(rest)
343
344def mac_of(nd: ND.NixDarwin) -> String:
345 match nd:
346 case ND.NixDarwin{_, _, _, _, _, st, fw, br, _, _, _, _, _, _}:
347 StO.mac_nix(st, fw, br)
348
349def host_of(nd: ND.NixDarwin) -> String:
350 match nd:
351 case ND.NixDarwin{h, _, _, _, _, _, _, _, _, _, _, _, _, _}:
352 h
353
354def rebuild_if(c: Bool, +h: String, +fl: String, +mac: String) -> List<&2, P.Step>:
355 match c:
356 case True{}:
357 [P.Rebuild{h, fl, mac}]
358 case False{}:
359 [P.Current{"nix-darwin " ++ h}]
360
361
362def differs(+path: String, +text: String) -> IO(Bool):
363 do IO<Bool>:
364 X.write("/tmp/v-generated", text)
365 same : Bool <- X.succeeds("cmp", ["-s", "/tmp/v-generated", path])
366 return Bool.not(same)
367
368def darwin_steps(+root: String, +k: String, +nd: ND.NixDarwin) -> IO(List<&2, P.Step>):
369 match nd:
370 case ND.NixDarwin{_, _, _, _, _, +st, +fw, +br, _, _, _, _, _, _}:
371 do IO<List<&2, P.Step>>:
372 checks : List<&2, P.Step> <- NDE.checks(k, nd)
373 +settings : List<&2, P.Step> <- StE.changes(st, fw)
374 +brews : List<&2, P.Step> <- BrE.changes(br)
375 flake_new : Bool <- differs(root ++ "/nix/flake.nix", NDO.flake(nd))
376 mac_new : Bool <- differs(root ++ "/nix/mac.nix", mac_of(nd))
377 return List.append(&2, P.Step, checks, List.append(&2, P.Step, settings, List.append(&2, P.Step, brews,
378 rebuild_if(flake_new || mac_new || has_change(settings) || has_change(brews), host_of(nd), NDO.flake(nd), mac_of(nd)))))
379
380def platform_steps(p: D.Platform, +root: String, +k: String, +a: A.Architecture) -> IO(List<&2, P.Step>):
381 match p:
382 case D.Unmanaged{}:
383 IO.pure(List<&2, P.Step>, [])
384 case D.Darwin{+nd}:
385 darwin_steps(root, k, nd)
386 case D.AzureCloud{az}:
387 AzE.checks(az, O.agreement(a))
388
389def node_steps(ns: List<&2, D.Node>, +root: String, +a: A.Architecture) -> IO(List<&2, P.Step>):
390 match ns:
391 case []:
392 IO.pure(List<&2, P.Step>, [])
393 case n <> rest:
394 match n:
395 case D.Node{+k, _, _, _, _, pf, kids, _}:
396 do IO<List<&2, P.Step>>:
397 here : List<&2, P.Step> <- platform_steps(pf, root, k, a)
398 below : List<&2, P.Step> <- node_steps(kids, root, a)
399 after : List<&2, P.Step> <- node_steps(rest, root, a)
400 return List.append(&2, P.Step, here, List.append(&2, P.Step, below, after))
401
402def deployed_path(+root: String) -> String:
403 root ++ "/.git/v-deployed"
404
405def breaks_step(+bs: List<&2, D.Break>, +d: D.Deployment) -> List<&2, P.Step>:
406 Bool.pick(List<&2, P.Step>, DO.breaks_eq(bs, DO.accepted(d)), [],
407 [P.Blocked{"deployment", DO.show_breaks(bs) ++ " (declare it as accepted to deploy)"}])
408
409
410def deployed_steps(+root: String, +d: D.Deployment) -> IO(List<&2, P.Step>):
411 do IO<List<&2, P.Step>>:
412 +text : String <- X.read(deployed_path(root))
413 return breaks_step(DO.breaks(DO.recorded_address(text), DO.recorded_stores(text), d), d)
414
415def plan_all(+root: String, +entry: String, +a: A.Architecture) -> IO(List<&2, P.Step>):
416 do IO<List<&2, P.Step>>:
417 o : P.Observed <- observe(root, entry, SO.release(O.system(a)))
418 stack : List<&2, P.Step> <- CtE.checks(root, O.containers(a), O.externals(a))
419 nodes : List<&2, P.Step> <- node_steps(O.nodes(a), root, a)
420 breaks : List<&2, P.Step> <- deployed_steps(root, O.deployment(a))
421 return List.append(&2, P.Step, stack, List.append(&2, P.Step, nodes, List.append(&2, P.Step, breaks, PO.plan(SO.release(O.system(a)), o))))
422
423
424def deploy_all(+root: String, +entry: String, +a: A.Architecture) -> IO(Unit):
425 do IO<Unit>:
426 +steps : List<&2, P.Step> <- plan_all(root, entry, a)
427 deploy(root, steps)
428 X.write(deployed_path(root), DO.record(O.deployment(a)))
429
430
431def import_node(p: D.Platform, +root: String) -> IO(Unit):
432 match p:
433 case D.Darwin{nd}:
434 match nd:
435 case ND.NixDarwin{h, _, _, _, _, _, _, _, svs, _, _, _, _, _}:
436 do IO<Unit>:
437 st : List<&2, St.Setting> <- StE.observed(root ++ "/nix/options.tsv", SvO.labels(svs))
438 fw : St.Firewall <- StE.firewall_now()
439 br : List<&2, Br.Brew> <- BrE.observed()
440 IO.print("# " ++ h ++ "\n" ++ StO.import_text(st, fw, br))
441 case _:
442 IO.pure(Unit, Unit{})
443
444def import_nodes(ns: List<&2, D.Node>, +root: String) -> IO(Unit):
445 match ns:
446 case []:
447 IO.pure(Unit, Unit{})
448 case n <> rest:
449 match n:
450 case D.Node{_, _, _, _, _, pf, kids, _}:
451 do IO<Unit>:
452 import_node(pf, root)
453 import_nodes(kids, root)
454 import_nodes(rest, root)
455
456def import_all(+root: String, a: A.Architecture) -> IO(Unit):
457 import_nodes(O.nodes(a), root)