src/ws/doctor.bend

ezx@1.11.0.0 · 295 lines · 29 definitions

1# ws/doctor: `ez doctor` at a workspace root (EZ-WS-9). The doctor runs at the
2# root, as `ez lock` does (ws/hop.bend), on a World whose ledger is the joint
3# ledger (ws/joint.bend), so the toolchain, the ledger, the lock, the library
4# and the lock check are judged over every member's ledger as the lock is
5# made from them. The drift report is each member's own: the hashes its import
6# lines name that its ledger does not, and the dependencies of its ledger no
7# import line of its own names, with each `workspace = true` section resolved
8# to the member's package as the joint ledger resolves it. A member whose
9# package walks to a hash the lock does not record for it is reported too.
10# Every report that is a problem fails the command (`DP.plan.notes`).
11import Base
12import ezx@1.11.0.0/src/doctor/plan.bend as DP
13import ezx@1.11.0.0/src/doctor/world.bend as DW
14import ezx@1.11.0.0/src/lock/plan.bend as P
15import ezx@1.11.0.0/src/lock/world.bend as LW
16import ezx@1.11.0.0/src/ledger/manifest.bend as M
17import ezx@1.11.0.0/src/ledger/render.bend as Rend
18import ezx@1.11.0.0/src/pkg/pkg.bend as K
19import ezx@1.11.0.0/src/pub/plan.bend as PP
20import ezx@1.11.0.0/src/ws/workspace.bend as W
21import ezx@1.11.0.0/src/ws/joint.bend as J
22import ezx@1.11.0.0/src/ws/moved.bend as Mv
23
24# what the doctor needs beyond its World: nothing, for a project of its own;
25# or the root's ledger text, every other ez.toml git lists, and why the joint
26# ledger could not be made, "" when it was
27type Back is Data:
28  Alone{}
29  Back{text: String, found: List<&2, J.Found>, why: String}
30
31# the ledger the doctor judges, and what else it needs
32type Doctored is Data:
33  Doctored{ledger: Maybe<&2, String>, back: Back}
34
35# what the doctor judges at a workspace root: the joint ledger, the same text
36# `ez lock` plans the root lock from, or, when it cannot be made, the root's
37# own ledger with why
38def joint(jj: J.Joint, +text: String, +fs: List<&2, J.Found>) -> Doctored:
39  match jj:
40    case J.Lone{}:
41      Doctored{Some{text}, Alone{}}
42    case J.Clash{why}:
43      Doctored{Some{text}, Back{text, fs, why}}
44    case J.Joint{mm}:
45      Doctored{Some{Rend.show(mm)}, Back{text, fs, ""}}
46
47# ---------------------------------------------------------------------------
48# each member's own files
49
50# whether a path is under a directory of the root
51def under(+dir: String, +at: String) -> Bool:
52  String.starts_with(at, dir ++ "/")
53
54# whether a path is under any member's directory
55def under.any(dirs: List<&2, List<&2, String>>, +at: String) -> Bool:
56  match dirs:
57    case []:
58      False{}
59    case +hd <> tl:
60      rest = under.any(tl, at)
61      Bool.or(Bool.and(Bool.not(List.is_empty(&2, String, hd)), under(W.segs.text(hd), at)), rest)
62
63# whether a committed file is a member's own: under its directory, or, for
64# the root, under no member's
65def own.at(dir: List<&2, String>, +dirs: List<&2, List<&2, String>>, +at: String) -> Bool:
66  match dir:
67    case []:
68      Bool.not(under.any(dirs, at))
69    case hd <> tl:
70      under(W.segs.text(hd <> tl), at)
71
72# a file kept, when it is the member's own
73def own.put(hit: Bool, src: LW.Source, rest: List<&2, LW.Source>) -> List<&2, LW.Source>:
74  match hit:
75    case True{}:
76      src <> rest
77    case False{}:
78      rest
79
80# the committed files that are a member's own
81def own.files(
82  +dir: List<&2, String>,
83  +dirs: List<&2, List<&2, String>>,
84  fs: List<&2, LW.Source>
85) -> List<&2, LW.Source>:
86  match fs:
87    case []:
88      []
89    case LW.Source{+at, text} <> tl:
90      rest = own.files(dir, dirs, tl)
91      own.put(own.at(dir, dirs, at), LW.Source{at, text}, rest)
92
93# ---------------------------------------------------------------------------
94# each member's drift
95
96# every member's directory
97def dirs.of(xs: List<&2, J.Member>) -> List<&2, List<&2, String>>:
98  match xs:
99    case []:
100      []
101    case J.Member{dir, _mm} <> tl:
102      dir <> dirs.of(tl)
103
104# the name a member is imported by, when it publishes under one
105def nv.of(named: PP.Named) -> List<&2, String>:
106  match named:
107    case PP.ByName{nv}:
108      [nv]
109    case PP.ByHash{}:
110      []
111    case PP.Unnamed{_why}:
112      []
113
114# every name a member is imported by. Such a name is resolved in the
115# workspace whether or not a section names it (EZ-WS-6), so no member's
116# ledger has to.
117def nvs.of(xs: List<&2, J.Member>) -> List<&2, String>:
118  match xs:
119    case []:
120      []
121    case hd <> tl:
122      List.append(&2, String, nv.of(J.member.naming(hd)), nvs.of(tl))
123
124# both directions of a member's disagreement: a hash its own import lines
125# name that its own ledger does not, and a dependency of its ledger none of
126# them names
127def drift(+hs: List<&2, String>, +ds: List<&2, M.Dep>, +nvs: List<&2, String>) -> List<&2, String>:
128  List.append(&2, String, DP.unrecorded(DP.missing(hs, List.append(&2, String, DP.pinned(ds), nvs))),
129    DP.unused(ds, hs))
130
131# a member's line, said for the member
132def at.lines(+place: String, ls: List<&2, String>) -> List<&2, String>:
133  match ls:
134    case []:
135      []
136    case hd <> tl:
137      (place ++ ": " ++ hd) <> at.lines(place, tl)
138
139# a member's report, or the one line that says there is nothing to report
140def drift.notes(+place: String, +hs: List<&2, String>, lines: List<&2, String>) -> List<&2, DP.Note>:
141  match lines:
142    case []:
143      [DP.Note{False{}, "imports: " ++ place ++ ": " ++
144        DP.plural(List.length(&2, String, hs), "package", "packages") ++ " imported, every one named in it"}]
145    case hd <> tl:
146      DP.flag(at.lines(place, hd <> tl))
147
148# one member's drift report, over its own files
149def member.drift(
150  mb: J.Member,
151  +dirs: List<&2, List<&2, String>>,
152  +nvs: List<&2, String>,
153  +fs: List<&2, LW.Source>
154) -> List<&2, DP.Note>:
155  J.Member{+dir, mm} = mb
156  +hs = DP.imports(own.files(dir, dirs, fs))
157  drift.notes(J.ledger.at(dir), hs, drift(hs, J.deps.of(mm), nvs))
158
159# ---------------------------------------------------------------------------
160# a member against the lock
161
162# how a moved member reads
163def moved.line(+root: String, +now: String, +held: String) -> String:
164  "workspace: member " ++ root ++ " is " ++ now ++ " in the committed files, and the lock records " ++
165    held ++ "; run ez lock"
166
167# a member's walk against what the lock records at its directory, once that
168# is looked up
169def moved.at(found: Maybe<&2, Mv.Held>, +root: String, +now: String) -> List<&2, DP.Note>:
170  match found:
171    case None{}:
172      []
173    case Some{hd}:
174      Mv.Held{_path, _name, +held} = hd
175      Bool.pick(List<&2, DP.Note>, String.eq(now, held), [], [DP.Note{True{}, moved.line(root, now, held)}])
176
177# a member's walk against the lock
178def moved.walked(got: K.Walked, +locked: List<&2, Mv.Held>) -> List<&2, DP.Note>:
179  match got:
180    case K.Walked{+now, +root, _fs, _sums}:
181      moved.at(Mv.at(locked, root), root, now)
182    case K.Asks{_at}:
183      []
184    case K.Refused{_why}:
185      []
186
187# one member against the lock: its package walked from its entry over the
188# committed files, reported when the lock records another hash at its
189# directory
190def member.moved(mb: J.Member, +files: List<&2, K.Source>, +locked: List<&2, Mv.Held>) -> List<&2, DP.Note>:
191  J.Member{+dir, mm} = mb
192  M.Manifest{_n, ee, _b, _h, _pa, _pv, _ds, _ts} = mm
193  moved.walked(K.of(J.entry.at(dir, ee), files), locked)
194
195# every member's report: its drift, then whether it moved
196def members.notes(
197  xs: List<&2, J.Member>,
198  +dirs: List<&2, List<&2, String>>,
199  +nvs: List<&2, String>,
200  +fs: List<&2, LW.Source>,
201  +locked: List<&2, Mv.Held>
202) -> List<&2, DP.Note>:
203  match xs:
204    case []:
205      []
206    case +hd <> tl:
207      rest = members.notes(tl, dirs, nvs, fs, locked)
208      List.append(&2, DP.Note, member.drift(hd, dirs, nvs, fs),
209        List.append(&2, DP.Note, member.moved(hd, J.files.of(fs), locked), rest))
210
211# the members' reports, once they were resolved
212def resolved.notes(rd: J.Read, +fs: List<&2, LW.Source>, +locked: List<&2, Mv.Held>) -> List<&2, DP.Note>:
213  match rd:
214    case J.Unread{why}:
215      [DP.Note{True{}, "workspace: " ++ why}]
216    case J.Read{+xs}:
217      members.notes(xs, dirs.of(xs), nvs.of(xs), fs, locked)
218
219# the members' reports over the sources as git listed them
220def listing.notes(
221  listing: LW.Listing,
222  +text: String,
223  +found: List<&2, J.Found>,
224  +lock: String
225) -> List<&2, DP.Note>:
226  match listing:
227    case LW.Listed{+fs}:
228      resolved.notes(J.resolved(text, found, J.files.of(fs)), fs, Mv.locked(lock))
229    case LW.Unlisted{_why}:
230      [DP.Note{True{}, "imports: git could not list the .bend files it tracks here"}]
231
232# the members' reports, once the sources were asked for
233def sources.notes(got: DW.Heard, +text: String, +found: List<&2, J.Found>, +lock: String) -> List<&2, DP.Note>:
234  match got:
235    case DW.Heard{a}:
236      match a:
237        case DW.Listed{l}:
238          listing.notes(l, text, found, lock)
239        case _:
240          [DP.Note{True{}, "imports: the sources were not read"}]
241    case DW.Open{}:
242      [DP.Note{True{}, "imports: the sources were not read"}]
243
244# ---------------------------------------------------------------------------
245# the plan
246
247# what the doctor says after the toolchain, the ledger, the lock, the
248# library and the lock check: why the joint ledger could not be made, or else
249# every member's report
250def tail.pick(
251  fine: Bool,
252  +why: String,
253  +text: String,
254  +found: List<&2, J.Found>,
255  +world: DW.World
256) -> List<&2, DP.Note>:
257  match fine:
258    case True{}:
259      sources.notes(DW.heard(DW.replies(world), DW.Sources{}), text, found, DP.lock.text(DW.lock(world)))
260    case False{}:
261      [DP.Note{True{}, why}]
262
263# and so, once it is known whether the joint ledger was made
264def tail(+why: String, +text: String, +found: List<&2, J.Found>, +world: DW.World) -> List<&2, DP.Note>:
265  tail.pick(String.is_empty(why), why, text, found, world)
266
267# every line the doctor says at a workspace root
268def notes(+why: String, +text: String, +found: List<&2, J.Found>, +world: DW.World) -> List<&2, DP.Note>:
269  List.append(&2, DP.Note,
270    DP.head.notes(DP.ledger.seen(DW.read(world)), DW.cc(world), DW.lib(world), DW.lock(world),
271      DW.replies(world), DP.checked(world)),
272    tail(why, text, found, world))
273
274# `ez doctor`: for a project of its own, the doctor's plan; at a workspace
275# root, the same lines over the joint ledger, with each member's report in
276# place of the one drift report
277def plan(back: Back, +world: DW.World) -> P.Plan:
278  match back:
279    case Alone{}:
280      DP.plan(world)
281    case Back{+text, +found, +why}:
282      DP.plan.notes(notes(why, text, found, world))
283
284# the step, once the doctor's own is known: its questions, or this plan
285def step.of(st: DP.Step, back: Back, +world: DW.World) -> DP.Step:
286  match st:
287    case DP.Asking{asks}:
288      DP.Asking{asks}
289    case DP.Run{_pl}:
290      DP.Run{plan(back, world)}
291
292# what the interpreter does next: the doctor asks what it always asks, the
293# sources among them, and its plan is this one
294def step(back: Back, +world: DW.World) -> DP.Step:
295  step.of(DP.step(world), back, world)