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`). 11 import Base 12 import ../doctor/plan.bend as DP 13 import ../doctor/world.bend as DW 14 import ../lock/plan.bend as P 15 import ../lock/world.bend as LW 16 import ../ledger/manifest.bend as M 17 import ../ledger/render.bend as Rend 18 import ../pkg/pkg.bend as K 19 import ../pub/plan.bend as PP 20 import ./workspace.bend as W 21 import ./joint.bend as J 22 import ./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 27 type 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 32 type 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 38 def 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 51 def under(+dir: String, +at: String) -> Bool: 52 String.starts_with(at, dir ++ "/") 53 54 # whether a path is under any member's directory 55 def 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 65 def 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 73 def 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 81 def 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 97 def 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 105 def 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. 117 def 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 127 def 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 132 def 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 140 def 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 149 def 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 163 def 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 169 def 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 178 def 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 190 def 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 196 def 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 212 def 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 220 def 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 233 def 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 250 def 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 264 def 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 268 def 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 277 def 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 285 def 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 294 def step(back: Back, +world: DW.World) -> DP.Step: 295 step.of(DP.step(world), back, world)