src/ws/joint.bend

ezx@1.10.0.0 · 930 lines · 0 definitions

1# ws/joint: the one ledger a workspace is locked from (EZ-WS-3). `ez lock` in
2# a workspace writes one ez.lock.toml at the root, so it reads every member's
3# ledger; rather than teach the lock planner a second kind of World, the
4# members' ledgers are joined into one, and the lock planner runs on a World
5# whose ledger is that one. Every law the lock planner has is about every
6# World, so it holds of a workspace's lock as it does of a project's. The
7# design is docs/rfc/ez-workspaces.md.
8#
9# The joint ledger is the root's `[package]` table with the dependencies and
10# tools of the root and of every member, a member's relative path sources
11# taken from the member's directory, as the root reads them. A name is one
12# dependency, or one tool, across the workspace: the first declaration of it
13# is the one the joint ledger holds, and every other must agree with it.
14#
15# A dependency section with `workspace = true` names another member by its
16# `[package] name` (EZ-WS-5). Before the ledgers are joined, each such section
17# is given the member's package: its hash, the `0x` name of the walk from the
18# member's entry over the committed files, its entry as a path from the root,
19# and the directory the walk writes its paths from. No ledger a person keeps
20# records any of that; only the joint ledger the lock reads does.
21import Base
22import ezx@1.10.0.0/src/ledger/manifest.bend as M
23import ezx@1.10.0.0/src/ledger/render.bend as Rend
24import ezx@1.10.0.0/src/pkg/path.bend as P
25import ezx@1.10.0.0/src/pkg/pkg.bend as K
26import ezx@1.10.0.0/src/lock/lock.bend as L
27import ezx@1.10.0.0/src/lock/world.bend as LW
28import ezx@1.10.0.0/src/pub/plan.bend as PP
29import ezx@1.10.0.0/src/ws/workspace.bend as W
30
31# one ez.toml git lists under the root, other than the root's own: its path
32# from the root, and its text
33type Found is Data:
34  Found{path: String, text: String}
35
36# a member's ledger, read: its directory and what it holds
37type Member is Data:
38  Member{dir: List<&2, String>, m: M.Manifest}
39
40# what a workspace is locked from: as a project of its own, which is not a
41# workspace; nothing, because something about the workspace was refused; or
42# the joint ledger
43type Joint is Data:
44  Lone{}
45  Clash{why: String}
46  Joint{m: M.Manifest}
47
48# ---------------------------------------------------------------------------
49# a member's sources, as the root reads them
50
51# a source read from a member's directory: a relative path joined to it, and
52# a URL or an absolute path as it is (`P.anchor`, EZ-LED-8)
53def source.at(+dir: String, src: M.Source) -> M.Source:
54  match src:
55    case M.Hub{nv}:
56      M.Hub{nv}
57    case M.Git{url, rev, tag, root, nar, vend}:
58      M.Git{P.anchor(dir, url), rev, tag, root, nar, vend}
59    case M.Member{path, named}:
60      M.Member{path, named}
61
62# a dependency read from a member's directory
63def dep.at(+dir: String, dependency: M.Dep) -> M.Dep:
64  M.Dep{nn, hh, ee, ss} = dependency
65  M.Dep{nn, hh, ee, source.at(dir, ss)}
66
67# every dependency read from a member's directory
68def deps.at(+dir: String, ds: List<&2, M.Dep>) -> List<&2, M.Dep>:
69  match ds:
70    case []:
71      []
72    case hd <> tl:
73      dep.at(dir, hd) <> deps.at(dir, tl)
74
75# a tool read from a member's directory
76def tool.at(+dir: String, pin: M.Tool) -> M.Tool:
77  M.Tool{nn, ee, bb, ss} = pin
78  M.Tool{nn, ee, bb, source.at(dir, ss)}
79
80# every tool read from a member's directory
81def tools.at(+dir: String, ts: List<&2, M.Tool>) -> List<&2, M.Tool>:
82  match ts:
83    case []:
84      []
85    case hd <> tl:
86      tool.at(dir, hd) <> tools.at(dir, tl)
87
88# every member's dependencies, in member order, each as the root reads it
89def members.deps(ms: List<&2, Member>) -> List<&2, M.Dep>:
90  match ms:
91    case []:
92      []
93    case Member{ds, mm} <> tl:
94      M.Manifest{_n, _e, _b, _h, _pa, _pv, xs, _ts} = mm
95      List.append(&2, M.Dep, deps.at(W.segs.text(ds), xs), members.deps(tl))
96
97# every member's tools, in member order, each as the root reads it
98def members.tools(ms: List<&2, Member>) -> List<&2, M.Tool>:
99  match ms:
100    case []:
101      []
102    case Member{ds, mm} <> tl:
103      M.Manifest{_n, _e, _b, _h, _pa, _pv, _xs, ts} = mm
104      List.append(&2, M.Tool, tools.at(W.segs.text(ds), ts), members.tools(tl))
105
106# the root's dependencies, then every member's
107def all.deps(root: M.Manifest, ms: List<&2, Member>) -> List<&2, M.Dep>:
108  M.Manifest{_n, _e, _b, _h, _pa, _pv, ds, _ts} = root
109  List.append(&2, M.Dep, ds, members.deps(ms))
110
111# the root's tools, then every member's
112def all.tools(root: M.Manifest, ms: List<&2, Member>) -> List<&2, M.Tool>:
113  M.Manifest{_n, _e, _b, _h, _pa, _pv, _ds, ts} = root
114  List.append(&2, M.Tool, ts, members.tools(ms))
115
116# ---------------------------------------------------------------------------
117# one declaration per name
118
119# every dependency, each name once, at its first declaration
120def firsts(ds: List<&2, M.Dep>) -> List<&2, M.Dep>:
121  match ds:
122    case []:
123      []
124    case +hd <> tl:
125      hd <> Rend.without(firsts(tl), M.dep.name(hd))
126
127# a tool kept or dropped
128def tools.without.put(hit: Bool, pin: M.Tool, rest: List<&2, M.Tool>) -> List<&2, M.Tool>:
129  match hit:
130    case True{}:
131      rest
132    case False{}:
133      pin <> rest
134
135# every tool but the one named
136def tools.without(ts: List<&2, M.Tool>, +name: String) -> List<&2, M.Tool>:
137  match ts:
138    case []:
139      []
140    case +hd <> tl:
141      tools.without.put(String.eq(M.tool.name(hd), name), hd, tools.without(tl, name))
142
143# every tool, each name once, at its first declaration
144def tools.firsts(ts: List<&2, M.Tool>) -> List<&2, M.Tool>:
145  match ts:
146    case []:
147      []
148    case +hd <> tl:
149      hd <> tools.without(tools.firsts(tl), M.tool.name(hd))
150
151# whether two bits are one
152def bit.eq(aa: Bool, bb: Bool) -> Bool:
153  match aa:
154    case True{}:
155      bb
156    case False{}:
157      Bool.not(bb)
158
159# whether two sources are one
160def source.eq(aa: M.Source, bb: M.Source) -> Bool:
161  match aa:
162    case M.Hub{an}:
163      match bb:
164        case M.Hub{bn}:
165          String.eq(an, bn)
166        case M.Git{_u, _r, _t, _o, _n, _v}:
167          False{}
168        case M.Member{_p, _m}:
169          False{}
170    case M.Git{au, ar, at, ao, an, av}:
171      match bb:
172        case M.Hub{_bn}:
173          False{}
174        case M.Git{bu, br, bt, bo, bn, bv}:
175          Bool.and(Bool.and(Bool.and(String.eq(au, bu), String.eq(ar, br)),
176            Bool.and(String.eq(at, bt), String.eq(ao, bo))),
177            Bool.and(String.eq(an, bn), bit.eq(av, bv)))
178        case M.Member{_p, _m}:
179          False{}
180    case M.Member{ap, am}:
181      match bb:
182        case M.Hub{_bn}:
183          False{}
184        case M.Git{_u, _r, _t, _o, _n, _v}:
185          False{}
186        case M.Member{bp, bm}:
187          Bool.and(String.eq(ap, bp), String.eq(am, bm))
188
189# whether two dependencies are one
190def dep.eq(aa: M.Dep, bb: M.Dep) -> Bool:
191  M.Dep{an, ah, ae, asrc} = aa
192  M.Dep{bn, bh, be, bsrc} = bb
193  Bool.and(Bool.and(String.eq(an, bn), String.eq(ah, bh)),
194    Bool.and(String.eq(ae, be), source.eq(asrc, bsrc)))
195
196# whether two tools are one
197def tool.eq(aa: M.Tool, bb: M.Tool) -> Bool:
198  M.Tool{an, ae, ab, asrc} = aa
199  M.Tool{bn, be, bb2, bsrc} = bb
200  Bool.and(Bool.and(String.eq(an, bn), String.eq(ae, be)),
201    Bool.and(String.eq(ab, bb2), source.eq(asrc, bsrc)))
202
203# whether every dependency of a list is the first declaration of its name in
204# the whole list
205def agree.go(ds: List<&2, M.Dep>, +all: List<&2, M.Dep>) -> Bool:
206  match ds:
207    case []:
208      True{}
209    case +hd <> tl:
210      rest = agree.go(tl, all)
211      Bool.and(dep.eq(hd, M.find(all, M.dep.name(hd))), rest)
212
213# whether every declaration of a dependency agrees with the first
214def agree(+ds: List<&2, M.Dep>) -> Bool:
215  agree.go(ds, ds)
216
217# the first dependency that does not agree with the first declaration of its
218# name, "" for none
219def clash.go(ds: List<&2, M.Dep>, +all: List<&2, M.Dep>) -> String:
220  match ds:
221    case []:
222      ""
223    case +hd <> tl:
224      rest = clash.go(tl, all)
225      Bool.pick(String, dep.eq(hd, M.find(all, M.dep.name(hd))), rest, M.dep.name(hd))
226
227# whether every declaration of a tool agrees with the first
228def tools.agree.go(ts: List<&2, M.Tool>, +all: List<&2, M.Tool>) -> Bool:
229  match ts:
230    case []:
231      True{}
232    case +hd <> tl:
233      rest = tools.agree.go(tl, all)
234      Bool.and(tool.eq(hd, M.tool.find(all, M.tool.name(hd))), rest)
235
236# whether every declaration of a tool agrees with the first
237def tools.agree(+ts: List<&2, M.Tool>) -> Bool:
238  tools.agree.go(ts, ts)
239
240# the first tool that does not agree with the first declaration of its name,
241# "" for none
242def tools.clash.go(ts: List<&2, M.Tool>, +all: List<&2, M.Tool>) -> String:
243  match ts:
244    case []:
245      ""
246    case +hd <> tl:
247      rest = tools.clash.go(tl, all)
248      Bool.pick(String, tool.eq(hd, M.tool.find(all, M.tool.name(hd))), rest, M.tool.name(hd))
249
250# ---------------------------------------------------------------------------
251# one pin per git source (EZ-WS-4)
252
253# the pin a source is recorded at, once it is known whether it is the url
254def pin.at(hit: Bool, src: M.Source, rest: M.Source) -> M.Source:
255  match hit:
256    case True{}:
257      src
258    case False{}:
259      rest
260
261# the first git source among the dependencies with this url, as the root
262# reads it, or a hub source when there is none
263def pin.of(ds: List<&2, M.Dep>, +url: String) -> M.Source:
264  match ds:
265    case []:
266      M.Hub{""}
267    case M.Dep{_n, _h, _e, +src} <> tl:
268      rest = pin.of(tl, url)
269      pin.at(String.eq(M.source.url(src), url), src, rest)
270
271# whether two git sources are pinned alike: one commit and one tag
272def pin.same(aa: M.Source, bb: M.Source) -> Bool:
273  match aa:
274    case M.Hub{_an}:
275      True{}
276    case M.Git{_au, ar, at, _ao, _an, _av}:
277      match bb:
278        case M.Hub{_bn}:
279          True{}
280        case M.Git{_bu, br, bt, _bo, _bn, _bv}:
281          Bool.and(String.eq(ar, br), String.eq(at, bt))
282        case M.Member{_bp, _bm}:
283          True{}
284    case M.Member{_ap, _am}:
285      True{}
286
287# whether every git dependency is pinned as the first declaration of its url
288def pins.go(ds: List<&2, M.Dep>, +all: List<&2, M.Dep>) -> Bool:
289  match ds:
290    case []:
291      True{}
292    case M.Dep{_n, _h, _e, +src} <> tl:
293      rest = pins.go(tl, all)
294      Bool.and(pin.same(src, pin.of(all, M.source.url(src))), rest)
295
296# whether every git source has one pin across the workspace
297def pins(+ds: List<&2, M.Dep>) -> Bool:
298  pins.go(ds, ds)
299
300# the first git source pinned two ways, "" for none
301def pins.clash.go(ds: List<&2, M.Dep>, +all: List<&2, M.Dep>) -> String:
302  match ds:
303    case []:
304      ""
305    case M.Dep{_n, _h, _e, +src} <> tl:
306      rest = pins.clash.go(tl, all)
307      Bool.pick(String, pin.same(src, pin.of(all, M.source.url(src))), rest, M.source.url(src))
308
309# ---------------------------------------------------------------------------
310# the hub
311
312# whether a member's ledger names the root's hub: it names none, which is the
313# root's, or the same one
314def hub.one(+hub: String, mm: M.Manifest) -> Bool:
315  M.Manifest{_n, _e, _b, +hh, _pa, _pv, _ds, _ts} = mm
316  Bool.or(String.is_empty(hh), String.eq(hh, hub))
317
318# whether every member names the root's hub
319def hubs(+hub: String, ms: List<&2, Member>) -> Bool:
320  match ms:
321    case []:
322      True{}
323    case Member{_ds, mm} <> tl:
324      rest = hubs(hub, tl)
325      Bool.and(hub.one(hub, mm), rest)
326
327# the first member naming another hub, "" for none
328def hubs.other(+hub: String, ms: List<&2, Member>) -> String:
329  match ms:
330    case []:
331      ""
332    case Member{+ds, mm} <> tl:
333      rest = hubs.other(hub, tl)
334      Bool.pick(String, hub.one(hub, mm), rest, W.segs.text(ds))
335
336# ---------------------------------------------------------------------------
337# the joint ledger
338
339# the root's hub
340def root.hub(root: M.Manifest) -> String:
341  M.Manifest{_n, _e, _b, hh, _pa, _pv, _ds, _ts} = root
342  hh
343
344# whether the members can be joined: every one names the root's hub, every
345# declaration of a dependency and of a tool agrees with the first, and every
346# git source is pinned at one commit and one tag
347def ok(+root: M.Manifest, +ms: List<&2, Member>) -> Bool:
348  Bool.and(hubs(root.hub(root), ms),
349    Bool.and(Bool.and(agree(all.deps(root, ms)), pins(all.deps(root, ms))),
350      tools.agree(all.tools(root, ms))))
351
352# why the members cannot be joined
353def why(+root: M.Manifest, +ms: List<&2, Member>) -> String:
354  +other = hubs.other(root.hub(root), ms)
355  +dep = clash.go(all.deps(root, ms), all.deps(root, ms))
356  +pin = pins.clash.go(all.deps(root, ms), all.deps(root, ms))
357  +tool = tools.clash.go(all.tools(root, ms), all.tools(root, ms))
358  Bool.pick(String, String.is_empty(other),
359    Bool.pick(String, String.is_empty(dep),
360      Bool.pick(String, String.is_empty(pin),
361        "ez: workspace members pin the tool " ++ tool ++ " differently",
362        "ez: workspace members pin " ++ pin ++ " at different commits; " ++
363          "move it for all of them with ez lock --upgrade --package"),
364      "ez: workspace members depend on " ++ dep ++ " differently"),
365    "ez: workspace member " ++ other ++ " names another hub than the root's")
366
367# the joint ledger: the root's package, and every dependency and tool, each
368# name at its first declaration
369def joined(root: M.Manifest, +ms: List<&2, Member>) -> M.Manifest:
370  M.Manifest{nn, ee, bb, hh, pa, pv, ds, ts} = root
371  M.Manifest{nn, ee, bb, hh, pa, pv,
372    firsts(List.append(&2, M.Dep, ds, members.deps(ms))),
373    tools.firsts(List.append(&2, M.Tool, ts, members.tools(ms)))}
374
375# the members joined, once it is known whether they can be
376def combine.go(fine: Bool, +root: M.Manifest, +ms: List<&2, Member>) -> Joint:
377  match fine:
378    case True{}:
379      Joint{joined(root, ms)}
380    case False{}:
381      Clash{why(root, ms)}
382
383# the root's ledger and every member's, joined, or why they cannot be
384def combine(+root: M.Manifest, +ms: List<&2, Member>) -> Joint:
385  combine.go(ok(root, ms), root, ms)
386
387# the members' ledgers read, or why one could not be
388type Read is Data:
389  Unread{why: String}
390  Read{ms: List<&2, Member>}
391
392# ---------------------------------------------------------------------------
393# a member as a dependency (EZ-WS-5)
394
395# what a member's section resolves to: the dependency on the member's
396# package, or why there is none
397type Res is Data:
398  Got{dep: M.Dep}
399  Bad{why: String}
400
401# a member's entry, as a path from the root
402def entry.at(+dir: List<&2, String>, +entry: String) -> String:
403  P.join(W.segs.text(dir), entry)
404
405# the ledger a member's section is written in, as a path from the root
406def ledger.at(+dir: List<&2, String>) -> String:
407  P.join(W.segs.text(dir), "ez.toml")
408
409# a member's `[package] name`
410def member.name(mb: Member) -> String:
411  Member{_dir, mm} = mb
412  M.Manifest{nn, _e, _b, _h, _pa, _pv, _ds, _ts} = mm
413  nn
414
415# the member found, once it is known whether the head is it
416def named.at(hit: Bool, hd: Member, rest: Maybe<&2, Member>) -> Maybe<&2, Member>:
417  match hit:
418    case True{}:
419      Some{hd}
420    case False{}:
421      rest
422
423# the first member whose `[package] name` is this one, or none
424def named(ms: List<&2, Member>, +name: String) -> Maybe<&2, Member>:
425  match ms:
426    case []:
427      None{}
428    case +hd <> tl:
429      rest = named(tl, name)
430      named.at(String.eq(member.name(hd), name), hd, rest)
431
432# a member's package, walked, as the dependency on it: the walk's hash, the
433# entry it walked from, the directory it writes its paths from, and the name
434# it is imported by, "" when it is imported by its hash
435def walked(got: K.Walked, +name: String, +entry: String, +nv: String) -> Res:
436  match got:
437    case K.Walked{hash, root, _fs, _sums}:
438      Got{M.Dep{name, hash, entry, M.Member{root, nv}}}
439    case K.Asks{at}:
440      Bad{"ez: workspace member " ++ name ++ " imports " ++ at ++ ", which git does not track"}
441    case K.Refused{why}:
442      Bad{why}
443
444# a member's package, by the name `ez publish` would publish it under
445# (EZ-WS-6): imported by that name when it has one, by its hash when its
446# ledger gives neither `publish-as` nor `version`, and refused when it gives
447# a name publish refuses
448def by.name(named: PP.Named, got: K.Walked, +name: String, +entry: String) -> Res:
449  match named:
450    case PP.ByHash{}:
451      walked(got, name, entry, "")
452    case PP.ByName{nv}:
453      walked(got, name, entry, nv)
454    case PP.Unnamed{why}:
455      Bad{why ++ "; workspace member " ++ name ++ " is imported by the name ez publish would give it"}
456
457# a member's package, as the dependency on it
458def member.dep(mm: M.Manifest, +dir: List<&2, String>, +name: String, +files: List<&2, K.Source>) -> Res:
459  M.Manifest{_n, ee, _b, _h, pa, pv, _ds, _ts} = mm
460  +entry = entry.at(dir, ee)
461  by.name(PP.naming(pa, pv), K.of(entry, files), name, entry)
462
463# the member a section names, once it is known whether it is the member
464# that names it
465def target.at(
466  own: Bool,
467  +name: String,
468  mm: M.Manifest,
469  +dir: List<&2, String>,
470  +files: List<&2, K.Source>,
471  +self: List<&2, String>
472) -> Res:
473  match own:
474    case True{}:
475      Bad{"ez: " ++ ledger.at(self) ++ " names " ++ name ++ " as a workspace member, and that member is itself"}
476    case False{}:
477      member.dep(mm, dir, name, files)
478
479# the member a section names, once it was looked for
480def target(
481  found: Maybe<&2, Member>,
482  +self: List<&2, String>,
483  +name: String,
484  +files: List<&2, K.Source>
485) -> Res:
486  match found:
487    case None{}:
488      Bad{"ez: " ++ ledger.at(self) ++ " names " ++ name ++ " as a workspace member, and no member's " ++
489        "[package] is named " ++ name}
490    case Some{mb}:
491      Member{+dir, mm} = mb
492      target.at(W.segs.eq(dir, self), name, mm, dir, files, self)
493
494# whether a member's section is `workspace = true` and nothing else
495def bare(+hash: String, +entry: String, +path: String, +nv: String) -> Bool:
496  Bool.and(Bool.and(String.is_empty(hash), String.is_empty(entry)),
497    Bool.and(String.is_empty(path), String.is_empty(nv)))
498
499# a member's section, once it is known whether it says nothing else
500def section(
501  plain: Bool,
502  found: Maybe<&2, Member>,
503  +self: List<&2, String>,
504  +name: String,
505  +files: List<&2, K.Source>
506) -> Res:
507  match plain:
508    case True{}:
509      target(found, self, name, files)
510    case False{}:
511      Bad{"ez: " ++ ledger.at(self) ++ " gives the workspace member " ++ name ++
512        " a hash, an entry or a source; its section is `workspace = true` alone"}
513
514# a dependency of the member at `self`, by its source: a member's section
515# given the member's package, and any other as it is
516def dep.res.src(
517  src: M.Source,
518  +name: String,
519  +hash: String,
520  +entry: String,
521  +ms: List<&2, Member>,
522  +self: List<&2, String>,
523  +files: List<&2, K.Source>
524) -> Res:
525  match src:
526    case M.Hub{nv}:
527      Got{M.Dep{name, hash, entry, M.Hub{nv}}}
528    case M.Git{url, rev, tag, root, nar, vend}:
529      Got{M.Dep{name, hash, entry, M.Git{url, rev, tag, root, nar, vend}}}
530    case M.Member{+path, +nv}:
531      section(bare(hash, entry, path, nv), named(ms, name), self, name, files)
532
533# a dependency of the member at `self`, resolved against every member
534def dep.res(
535  dependency: M.Dep,
536  +ms: List<&2, Member>,
537  +self: List<&2, String>,
538  +files: List<&2, K.Source>
539) -> Res:
540  M.Dep{nn, hh, ee, src} = dependency
541  dep.res.src(src, nn, hh, ee, ms, self, files)
542
543# a member's dependencies resolved, or why one could not be
544type Deps is Data:
545  Deps{ds: List<&2, M.Dep>}
546  Unmet{why: String}
547
548# a dependency resolved in front of the rest, unless the rest could not be
549def deps.cons(got: Res, rest: Deps) -> Deps:
550  match got:
551    case Bad{why}:
552      Unmet{why}
553    case Got{dd}:
554      match rest:
555        case Unmet{why}:
556          Unmet{why}
557        case Deps{ds}:
558          Deps{dd <> ds}
559
560# every dependency of the member at `self`, resolved, the first refusal in
561# the order written winning
562def deps.res(
563  ds: List<&2, M.Dep>,
564  +ms: List<&2, Member>,
565  +self: List<&2, String>,
566  +files: List<&2, K.Source>
567) -> Deps:
568  match ds:
569    case []:
570      Deps{[]}
571    case hd <> tl:
572      rest = deps.res(tl, ms, self, files)
573      deps.cons(dep.res(hd, ms, self, files), rest)
574
575# a member's ledger with these dependencies
576def with.deps(mm: M.Manifest, ds: List<&2, M.Dep>) -> M.Manifest:
577  M.Manifest{nn, ee, bb, hh, pa, pv, _ds, ts} = mm
578  M.Manifest{nn, ee, bb, hh, pa, pv, ds, ts}
579
580# a member's ledger with its dependencies resolved, in front of the rest
581def res.put(got: Deps, +dir: List<&2, String>, mm: M.Manifest, rest: Read) -> Read:
582  match got:
583    case Unmet{why}:
584      Unread{why}
585    case Deps{ds}:
586      match rest:
587        case Unread{why}:
588          Unread{why}
589        case Read{xs}:
590          Read{Member{dir, with.deps(mm, ds)} <> xs}
591
592# the dependencies a member's ledger records
593def deps.of(mm: M.Manifest) -> List<&2, M.Dep>:
594  M.Manifest{_n, _e, _b, _h, _pa, _pv, ds, _ts} = mm
595  ds
596
597# one member's ledger resolved against every member
598def res.one(mb: Member, +ms: List<&2, Member>, +files: List<&2, K.Source>, rest: Read) -> Read:
599  Member{+dir, +mm} = mb
600  res.put(deps.res(deps.of(mm), ms, dir, files), dir, mm, rest)
601
602# every member's ledger resolved against every member, `ms`
603def res.all(xs: List<&2, Member>, +ms: List<&2, Member>, +files: List<&2, K.Source>) -> Read:
604  match xs:
605    case []:
606      Read{[]}
607    case hd <> tl:
608      rest = res.all(tl, ms, files)
609      res.one(hd, ms, files, rest)
610
611# the name `ez publish` would publish a member under, or that it has none
612def member.naming(mb: Member) -> PP.Named:
613  Member{_dir, mm} = mb
614  M.Manifest{_n, _e, _b, _h, pa, pv, _ds, _ts} = mm
615  PP.naming(pa, pv)
616
617# a member imported by name, as a dependency of the joint ledger, in front of
618# the rest; any other member adds none
619def named.one(named: PP.Named, mb: Member, +files: List<&2, K.Source>, rest: Deps) -> Deps:
620  match named:
621    case PP.ByHash{}:
622      rest
623    case PP.Unnamed{_why}:
624      rest
625    case PP.ByName{_nv}:
626      Member{+dir, +mm} = mb
627      deps.cons(member.dep(mm, dir, member.name(Member{dir, mm}), files), rest)
628
629# every member imported by name, as a dependency of the joint ledger, whether
630# or not a section names it, so that the lock resolves its name from the
631# ledger and never puts it to the hub or to the lock being rewritten
632# (EZ-WS-6). A member whose name publish refuses is imported by none, and
633# adds none.
634def names.deps(xs: List<&2, Member>, +files: List<&2, K.Source>) -> Deps:
635  match xs:
636    case []:
637      Deps{[]}
638    case +hd <> tl:
639      rest = names.deps(tl, files)
640      named.one(member.naming(hd), hd, files, rest)
641
642# the root's ledger and the members', resolved, joined, the members imported
643# by name after the root's own dependencies
644def res.joint(xs: List<&2, Member>, +ns: List<&2, M.Dep>) -> Joint:
645  match xs:
646    case []:
647      Clash{"ez: the workspace root has no ledger"}
648    case Member{_dir, +root} <> ms:
649      combine(with.deps(root, List.append(&2, M.Dep, deps.of(root), ns)), ms)
650
651# the members imported by name, once every ledger was resolved
652def res.named(xs: List<&2, Member>, got: Deps) -> Joint:
653  match got:
654    case Unmet{why}:
655      Clash{why}
656    case Deps{ns}:
657      res.joint(xs, ns)
658
659# the resolved ledgers joined, or why one could not be resolved
660def res.read(rd: Read, got: Deps) -> Joint:
661  match rd:
662    case Unread{why}:
663      Clash{why}
664    case Read{xs}:
665      res.named(xs, got)
666
667# the root, as the member at the root's own directory, ahead of the members
668def with.root(root: M.Manifest, ms: List<&2, Member>) -> List<&2, Member>:
669  Member{[], root} <> ms
670
671# the root's ledger and every member's, resolved and joined, or why they
672# cannot be. The root is the member at the root's own directory, so a member
673# may name it when it has a `[package]`, and its sections are resolved as a
674# member's are.
675def resolve(+root: M.Manifest, ms: List<&2, Member>, +files: List<&2, K.Source>) -> Joint:
676  +all = with.root(root, ms)
677  res.read(res.all(all, all, files), names.deps(all, files))
678
679# ---------------------------------------------------------------------------
680# the answers the lock reads a member's package from
681
682# a file's text in a walk's files, "" when it holds none at that path
683def text.at(fs: List<&2, K.Source>, +at: String) -> String:
684  match fs:
685    case []:
686      ""
687    case K.Source{+fa, ft} <> tl:
688      rest = text.at(tl, at)
689      Bool.pick(String, String.eq(fa, at), ft, rest)
690
691# the texts of a walk's files, in its manifest's order
692def texts(sums: List<&2, K.Item>, +fs: List<&2, K.Source>) -> List<&2, String>:
693  match sums:
694    case []:
695      []
696    case K.Item{at, _sum} <> tl:
697      text.at(fs, at) <> texts(tl, fs)
698
699# a member's package as the lock is to read it: fresh from the committed
700# tree, as a clone arrives, so the lock lays it under BEND_LIB
701def seed.walked(got: K.Walked, +hash: String, +path: String, +entry: String) -> List<&2, LW.Reply>:
702  match got:
703    case K.Walked{_h, _root, fs, +sums}:
704      [LW.Reply{LW.Pkg{hash, L.Member{path, entry}, ""},
705        LW.Got{LW.Clone{""}, K.manifest_of(sums), texts(sums, fs)}}]
706    case K.Asks{_at}:
707      []
708    case K.Refused{_why}:
709      []
710
711# the answer for one dependency of the joint ledger, by its source: a
712# member's package walked again from its entry, and nothing for any other
713def seed.src(src: M.Source, +hash: String, +entry: String, +files: List<&2, K.Source>) -> List<&2, LW.Reply>:
714  match src:
715    case M.Hub{_nv}:
716      []
717    case M.Git{_url, _rev, _tag, _root, _nar, _vend}:
718      []
719    case M.Member{path, _nv}:
720      seed.walked(K.of(entry, files), hash, path, entry)
721
722# the answer for one dependency of the joint ledger
723def seed(dependency: M.Dep, +files: List<&2, K.Source>) -> List<&2, LW.Reply>:
724  M.Dep{_n, hh, ee, src} = dependency
725  seed.src(src, hh, ee, files)
726
727# the answers for every member the joint ledger depends on
728def seeds.of(ds: List<&2, M.Dep>, +files: List<&2, K.Source>) -> List<&2, LW.Reply>:
729  match ds:
730    case []:
731      []
732    case hd <> tl:
733      List.append(&2, LW.Reply, seed(hd, files), seeds.of(tl, files))
734
735# the answers the lock reads the members it depends on from: none for a
736# project of its own, or for a workspace that cannot be joined
737def seeds(jj: Joint, +files: List<&2, K.Source>) -> List<&2, LW.Reply>:
738  match jj:
739    case Lone{}:
740      []
741    case Clash{_why}:
742      []
743    case Joint{mm}:
744      seeds.of(deps.of(mm), files)
745
746# the committed files a listing names, as the walk takes them
747def files.of(ls: List<&2, LW.Source>) -> List<&2, K.Source>:
748  match ls:
749    case []:
750      []
751    case LW.Source{at, text} <> tl:
752      K.Source{at, text} <> files.of(tl)
753
754# the committed files git listed, or none when it could not list them
755def files(listing: LW.Listing) -> List<&2, K.Source>:
756  match listing:
757    case LW.Listed{ls}:
758      files.of(ls)
759    case LW.Unlisted{_why}:
760      []
761
762# ---------------------------------------------------------------------------
763# from the texts git lists
764
765# a ledger kept, when it is one under a directory
766def ledgers.put(lg: Maybe<&2, W.Ledger>, rest: List<&2, W.Ledger>) -> List<&2, W.Ledger>:
767  match lg:
768    case None{}:
769      rest
770    case Some{ll}:
771      ll <> rest
772
773# every ledger git lists under the root, read for the member set
774def ledgers(fs: List<&2, Found>) -> List<&2, W.Ledger>:
775  match fs:
776    case []:
777      []
778    case Found{+pp, +tt} <> tl:
779      ledgers.put(W.ledger.of(pp, tt), ledgers(tl))
780
781# a member read in front of the rest, unless the rest could not be read
782def read.cons(+dir: List<&2, String>, rd: M.Read, rest: Read) -> Read:
783  match rd:
784    case M.Bad{why}:
785      Unread{"ez: workspace member " ++ W.segs.text(dir) ++ " has an ez.toml that does not parse: " ++ why}
786    case M.Good{mm}:
787      match rest:
788        case Unread{why}:
789          Unread{why}
790        case Read{ms}:
791          Read{Member{dir, mm} <> ms}
792
793# a ledger git listed, kept when it is a member's
794def read.pick(hit: Bool, +dir: List<&2, String>, +text: String, rest: Read) -> Read:
795  match hit:
796    case True{}:
797      read.cons(dir, M.parse(text), rest)
798    case False{}:
799      rest
800
801# a ledger git listed, when it is one under a directory
802def read.one(lg: Maybe<&2, W.Ledger>, +dirs: List<&2, List<&2, String>>, +text: String, rest: Read) -> Read:
803  match lg:
804    case None{}:
805      rest
806    case Some{ll}:
807      +dir = W.ledger.dir(ll)
808      read.pick(W.has(dirs, dir), dir, text, rest)
809
810# every member's ledger, read in the order git listed them
811def read.all(fs: List<&2, Found>, +dirs: List<&2, List<&2, String>>) -> Read:
812  match fs:
813    case []:
814      Read{[]}
815    case Found{+pp, +tt} <> tl:
816      rest = read.all(tl, dirs)
817      read.one(W.ledger.of(pp, tt), dirs, tt, rest)
818
819# the members joined with the root, once each was read and every member's
820# section resolved
821def joint.read(rd: Read, +root: M.Manifest, +files: List<&2, K.Source>) -> Joint:
822  match rd:
823    case Unread{why}:
824      Clash{why}
825    case Read{ms}:
826      resolve(root, ms, files)
827
828# the members joined with the root, once the root's ledger was read
829def joint.root(
830  rd: M.Read,
831  +fs: List<&2, Found>,
832  +dirs: List<&2, List<&2, String>>,
833  +files: List<&2, K.Source>
834) -> Joint:
835  match rd:
836    case M.Bad{why}:
837      Clash{why}
838    case M.Good{root}:
839      joint.read(read.all(fs, dirs), root, files)
840
841# the members joined, once the member set is known
842def joint.members(
843  ms: W.Members,
844  +text: String,
845  +fs: List<&2, Found>,
846  +files: List<&2, K.Source>
847) -> Joint:
848  match ms:
849    case W.Refused{why}:
850      Clash{why}
851    case W.Members{_root, dirs}:
852      joint.root(M.parse(text), fs, dirs, files)
853
854# what the root's `[workspace]` table says, against the ledgers git lists
855def joint.spec(rd: W.Read, +text: String, +fs: List<&2, Found>, +files: List<&2, K.Source>) -> Joint:
856  match rd:
857    case W.Lone{}:
858      Lone{}
859    case W.Bad{why}:
860      Clash{why}
861    case W.Root{+spec}:
862      joint.members(W.members(spec, W.root.of(text), ledgers(fs)), text, fs, files)
863
864# ---------------------------------------------------------------------------
865# the members, resolved, for `ez doctor` (EZ-WS-9)
866
867# the members resolved, once each was read: the root first, as the member at
868# its own directory
869def resolved.read(rd: Read, +root: M.Manifest, +files: List<&2, K.Source>) -> Read:
870  match rd:
871    case Unread{why}:
872      Unread{why}
873    case Read{ms}:
874      +all = with.root(root, ms)
875      res.all(all, all, files)
876
877# the members resolved, once the root's ledger was read
878def resolved.root(
879  rd: M.Read,
880  +fs: List<&2, Found>,
881  +dirs: List<&2, List<&2, String>>,
882  +files: List<&2, K.Source>
883) -> Read:
884  match rd:
885    case M.Bad{why}:
886      Unread{why}
887    case M.Good{+root}:
888      resolved.read(read.all(fs, dirs), root, files)
889
890# the members resolved, once the member set is known
891def resolved.members(ms: W.Members, +text: String, +fs: List<&2, Found>, +files: List<&2, K.Source>) -> Read:
892  match ms:
893    case W.Refused{why}:
894      Unread{why}
895    case W.Members{_root, dirs}:
896      resolved.root(M.parse(text), fs, dirs, files)
897
898# the members resolved, by what the root's `[workspace]` table says
899def resolved.spec(rd: W.Read, +text: String, +fs: List<&2, Found>, +files: List<&2, K.Source>) -> Read:
900  match rd:
901    case W.Lone{}:
902      Read{[]}
903    case W.Bad{why}:
904      Unread{why}
905    case W.Root{+spec}:
906      resolved.members(W.members(spec, W.root.of(text), ledgers(fs)), text, fs, files)
907
908# every member's ledger, the root's first, with each section resolved
909# against every member, as the joint ledger resolves them before it joins
910# them; none for a project of its own
911def resolved(+text: String, +fs: List<&2, Found>, +files: List<&2, K.Source>) -> Read:
912  resolved.spec(W.spec.of(text), text, fs, files)
913
914# what a project is locked from: its own ledger when it is no workspace root,
915# the joint ledger of the root and every member when it is one, or why there
916# is none. `text` is the root's ez.toml, `fs` every other ez.toml git lists
917# under it, and `files` the committed `.bend` files a member's package is
918# walked over.
919def joint(+text: String, +fs: List<&2, Found>, +files: List<&2, K.Source>) -> Joint:
920  joint.spec(W.spec.of(text), text, fs, files)
921
922# the ledger text the lock planner reads: the joint ledger, rendered
923def joint.text(+jj: Joint, +own: Maybe<&2, String>) -> Maybe<&2, String>:
924  match jj:
925    case Lone{}:
926      own
927    case Clash{_why}:
928      own
929    case Joint{mm}:
930      Some{Rend.show(mm)}