~/bend-docscommunity

src/fetch/plan.bend source

src/fetch/plan.bend on the hub · documented module

# fetch/plan: `ez fetch` as a pure planner. It reads a World# (fetch/world.bend) and returns either the questions it still needs# answered (`step`) or a Plan: the effects to run and how the command ends# (`plan`). The interpreter (fetch/run.bend) loops on `step`, answering each# question by IO, until nothing is left to ask, then executes the plan.## `ez fetch` fills BEND_LIB from the lock alone, re-resolving nothing. With no# ez.toml, or no ez.lock.toml, it refuses before it asks anything, as# `uv sync --frozen` does: a fetch never writes a lock, so it has nothing to# fill BEND_LIB from.## For every package the lock records it asks first for the tree already under# BEND_LIB. A tree there that the lock's checks pass is kept as it is. Any# other is fetched: a git package as a shallow checkout at the pinned rev,# from its source anchored at the project (`P.anchor(here, url)`), and a hub# package from the lock's hub. Each file is then taken from what arrived by# its path, from the package's root inside the checkout for a git package,# and the package is checked before anything is laid (`good`): every file's# digest equals the sum the lock records for it (EZ-FETCH-1), no path leaves# the package, and the files hash to the name the lock records them under# (EZ-HASH-3). A git package's checkout must also weigh to the narHash the# lock records (`weighed`), so the files laid come from the tree the lock# pins, the tree nix rebuilds from the same pin. Only then is the tree laid# under BEND_LIB, the files the lock names and their manifest last.## Every name the lock records is laid too, as the file bend reads the name's# hash from, `$BEND_LIB/names/<nv>` holding the hash and a newline, so bend# never asks the hub for it (EZ-HUB-4). A names file that already holds the# lock's hash is kept; any other is written, and one that held another hash# is said to have been rewritten, since the lock wins over it as it wins over# a tree that fails its checks. A name the lock records that bend would not# read back, one the hub rules out or a hash that is not a `0x` name, refuses# the fetch, since its file would not be the one bend reads.## The plan of a fetch that succeeds lays every tree that was fetched and every# names file that is not already right, and nothing else: no ledger, no lock. A fetch that refuses, for any package,# has no effect at all, so a package fetched before the one that failed is# not laid either, and a tree already under BEND_LIB is never removed# (EZ-OUT-2).## The Effect, Plan and Outcome types are the lock's (lock/plan.bend), as for# `ez add`, `ez remove` and `ez init`.import Baseimport ../toml/toml.bend as Timport ./world.bend as Fimport ../lock/plan.bend as Pimport ../lock/world.bend as Wimport ../lock/up.bend as Upimport ../lock/lock.bend as Limport ../pkg/pkg.bend as Kimport ../pkg/path.bend as Pathimport ../share/sha.bend as Sha# ---------------------------------------------------------------------------# the questions# the question that fetches a package from where the lock says it comes from:# a checkout of a git package at its rev, from its source as git is to read# it, a relative path taken from the project; or a hub package from the hubdef fetch.ask(src: L.Src, +here: String, +hub: String, +hash: String) -> F.Ask:  match src:    case L.Hub{}:      F.Hub{W.Pkg{hash, L.Hub{}, hub}}    case L.Git{url, rev, _entry, _root, _nar, _tag}:      F.Git{hash, Up.Clone{Path.anchor(here, url), rev}}# ---------------------------------------------------------------------------# what arrived# a package's files as they arrived, with the directory the lock's paths are# written from inside them, or why nothing arrivedtype Tree is Data:  Tree{root: String, files: List<&2, K.Source>}  Gone{why: String}# the files a manifest names, each with the text read for itdef zip(fs: List<&2, K.Item>, ss: List<&2, String>) -> List<&2, K.Source>:  match fs ss:    case Nil{} Nil{}:      []    case Nil{} Con{_s, _t}:      []    case Con{_f, _g} Nil{}:      []    case Con{f, g} Con{s, t}:      K.Source{K.file.at(f), s} <> zip(g, t)# a manifest and the texts of the files it names, as a tree: what is under# BEND_LIB, or what the hub serveddef tree.read(answer: W.Answer) -> Tree:  match answer:    case W.Got{_how, manifest, ss}:      Tree{"", zip(L.manifest.files(String.lines(manifest)), ss)}    case W.Miss{why}:      Gone{why}    case _:      Gone{"ez: a tree was not answered with one"}# a checkout at the pinned rev, whose paths are the lock's from `root`def tree.said(answer: Up.Answer, +root: String) -> Tree:  match answer:    case Up.Cloned{_nar, fs}:      Tree{root, fs}    case Up.Miss{why}:      Gone{why}    case _:      Gone{"ez: a checkout was not answered with one"}# what arrived for a questiondef tree.of(answer: F.Answer, +root: String) -> Tree:  match answer:    case F.Read{a}:      tree.read(a)    case F.Said{a}:      tree.said(a, root)    case F.Held{_got}:      Gone{"ez: a tree was not answered with one"}# a text looked up, "" when it is not theredef text.or(got: Maybe<&2, String>) -> String:  match got:    case None{}:      ""    case Some{text}:      text# where a path of the lock is in a tree: under its root inside a checkout,# and as it is in a tree a manifest namesdef key(+root: String, +at: String) -> String:  Bool.pick(String, String.is_empty(root), at, Path.join(root, at))# every file the lock names, with the text the tree has at its path. A file# the tree does not have is taken as empty, which does not match the lock# unless the lock records an empty file.def cand(fs: List<&2, K.Item>, +root: String, +tree: List<&2, K.Source>) -> List<&2, W.Source>:  match fs:    case []:      []    case K.Item{+at, _sum} <> t:      W.Source{at, text.or(K.look(tree, key(root, at)))} <> cand(t, root, tree)# ---------------------------------------------------------------------------# the checks# a file laid, weighed by its own textdef weigh(src: W.Source) -> K.Item:  W.Source{at, +text} = src  K.Item{at, Sha.hex(text)}# every file laid, weighed by its own textdef weighs(ls: List<&2, W.Source>) -> List<&2, K.Item>:  match ls:    case []:      []    case h <> t:      weigh(h) <> weighs(t)# whether one file is the one the lock names: the same path, and a text whose# digest is the sum the lock recordsdef fit(want: K.Item, got: W.Source) -> Bool:  K.Item{+at, sum} = want  W.Source{lat, +text} = got  Bool.and(String.eq(lat, at), String.eq(Sha.hex(text), sum))# whether every file is the one the lock names, in the lock's order. A list# longer or shorter than the lock's is not the package.def fits(fs: List<&2, K.Item>, ls: List<&2, W.Source>) -> Bool:  match fs ls:    case Nil{} Nil{}:      True{}    case Nil{} Con{_l, _m}:      False{}    case Con{_f, _g} Nil{}:      False{}    case Con{f, g} Con{l, m}:      +rest = fits(g, m)      Bool.and(fit(f, l), rest)# whether a path stays inside the tree it is laid in: written the way the# path walk writes it, so no `.` or `//` in it, not empty, not absolute and# not climbing out with `..`. A lock is not trusted with where it writes.def path.safe(+at: String) -> Bool:  Bool.and(String.eq(Path.norm(at), at),    Bool.not(Bool.or(K.escapes(at), String.eq(at, "."))))# whether every path stays inside the treedef paths.safe(ls: List<&2, W.Source>) -> Bool:  match ls:    case []:      True{}    case W.Source{+at, _text} <> t:      +rest = paths.safe(t)      Bool.and(path.safe(at), rest)# the manifest of the files laid, weighed by their own textsdef manifest(+ls: List<&2, W.Source>) -> String:  K.manifest_of(K.files_of(weighs(ls)))# the `0x` name of the files laid, weighed by their own textsdef named(+ls: List<&2, W.Source>) -> String:  K.hash_of(K.files_of(weighs(ls)))# whether a package may be laid: every file is the one the lock names, no# path leaves the tree, and the files hash to the name the lock records them# under, so the tree laid under that name is that name'sdef good(fs: List<&2, K.Item>, +hash: String, +ls: List<&2, W.Source>) -> Bool:  +fit = fits(fs, ls)  +safe = paths.safe(ls)  Bool.and(fit, Bool.and(safe, String.eq(hash, named(ls))))# the first file that is not the one the lock names, or "" when none is# wrong. A path is never empty, so "" is never such a path.def unfit.pick(ok: Bool, +at: String, rest: String) -> String:  match ok:    case True{}:      rest    case False{}:      at# the first path that does not fit, "" when every file doesdef unfit(fs: List<&2, K.Item>, ls: List<&2, W.Source>) -> String:  match fs ls:    case Nil{} Nil{}:      ""    case Nil{} Con{l, _m}:      W.Source{at, _text} = l      at    case Con{f, _g} Nil{}:      K.file.at(f)    case Con{+f, g} Con{l, m}:      unfit.pick(fit(f, l), K.file.at(f), unfit(g, m))# why a package may not be laid, naming itdef bad.why(fs: List<&2, K.Item>, +hash: String, +ls: List<&2, W.Source>) -> String:  +at = unfit(fs, ls)  Bool.pick(String, String.is_empty(at),    Bool.pick(String, paths.safe(ls),      "ez: " ++ hash ++ ": its files do not hash to its name",      "ez: " ++ hash ++ ": a path in the lock leaves the package"),    "ez: " ++ hash ++ ": " ++ at ++ " does not match the lock")# ---------------------------------------------------------------------------# one package# where one package stands: its tree under BEND_LIB still to read, its fetch# still to ask, a tree under BEND_LIB that passed and is kept, the files that# arrived, or why nothing arrivedtype Part is Data:  AskLaid{}  AskFetch{}  Have{}  Got{files: List<&2, W.Source>}  No{why: String}# the files that arrived for a package, or why none diddef part.tree(tree: Tree, fs: List<&2, K.Item>) -> Part:  match tree:    case Tree{r, files}:      Got{cand(fs, r, files)}    case Gone{why}:      No{why}# a package fetched, once it is answereddef part.fetch.heard(got: F.Heard, fs: List<&2, K.Item>, +root: String) -> Part:  match got:    case F.Open{}:      AskFetch{}    case F.Heard{answer}:      part.tree(tree.of(answer, root), fs)# a package fetched, from where the lock says it comes fromdef part.fetch(  +rs: List<&2, F.Reply>,  +src: L.Src,  fs: List<&2, K.Item>,  +here: String,  +hub: String,  +hash: String) -> Part:  part.fetch.heard(F.heard(rs, fetch.ask(src, here, hub, hash)), fs, L.src.root(src))# a tree under BEND_LIB that passes is kept; any other is fetcheddef part.kept(  ok: Bool,  +rs: List<&2, F.Reply>,  +src: L.Src,  fs: List<&2, K.Item>,  +here: String,  +hub: String,  +hash: String) -> Part:  match ok:    case True{}:      Have{}    case False{}:      part.fetch(rs, src, fs, here, hub, hash)# the tree under BEND_LIB, read: checked the way a fetched one is, and# fetched when it is not theredef part.found(  tree: Tree,  +rs: List<&2, F.Reply>,  +src: L.Src,  +fs: List<&2, K.Item>,  +here: String,  +hub: String,  +hash: String) -> Part:  match tree:    case Tree{_r, files}:      part.kept(good(fs, hash, cand(fs, "", files)), rs, src, fs, here, hub, hash)    case Gone{_why}:      part.fetch(rs, src, fs, here, hub, hash)# the tree under BEND_LIB, once it is readdef part.laid(  got: F.Heard,  +rs: List<&2, F.Reply>,  +src: L.Src,  +fs: List<&2, K.Item>,  +here: String,  +hub: String,  +hash: String) -> Part:  match got:    case F.Open{}:      AskLaid{}    case F.Heard{answer}:      part.found(tree.of(answer, ""), rs, src, fs, here, hub, hash)# where one package standsdef part(  +rs: List<&2, F.Reply>,  +src: L.Src,  +fs: List<&2, K.Item>,  +here: String,  +hub: String,  +hash: String) -> Part:  part.laid(F.heard(rs, F.Laid{hash}), rs, src, fs, here, hub, hash)# ---------------------------------------------------------------------------# the checkout around a git package# why a checkout may not be laid for the narHash it weighed to, "" when it# may: the lock records a narHash and the checkout weighed to itdef nar.why(  none: Bool,  same: Bool,  +hash: String,  +want: String,  +got: String,  +url: String,  +rev: String) -> String:  match none:    case True{}:      "ez: " ++ hash ++ ": the lock records no narHash for " ++ url ++ " at " ++ rev ++        ", so its checkout cannot be checked; run `ez lock --upgrade` to pin it"    case False{}:      match same:        case True{}:          ""        case False{}:          "ez: " ++ hash ++ ": " ++ url ++ " at " ++ rev ++ " has narHash " ++ got ++            ", and the lock records " ++ want# why a package stands without a weighed checkoutdef unweighed(+hash: String) -> String:  "ez: " ++ hash ++ ": its checkout was not weighed"# what a checkout question was answered with, against the lock's narHashdef weighed.up(answer: Up.Answer, +hash: String, +nar: String, +url: String, +rev: String) -> String:  match answer:    case Up.Cloned{+got, _fs}:      nar.why(String.is_empty(nar), String.eq(got, nar), hash, nar, got, url, rev)    case _:      unweighed(hash)def weighed.answer(answer: F.Answer, +hash: String, +nar: String, +url: String, +rev: String) -> String:  match answer:    case F.Said{a}:      weighed.up(a, hash, nar, url, rev)    case F.Read{_a}:      unweighed(hash)    case F.Held{_got}:      unweighed(hash)def weighed.heard(got: F.Heard, +hash: String, +nar: String, +url: String, +rev: String) -> String:  match got:    case F.Heard{a}:      weighed.answer(a, hash, nar, url, rev)    case F.Open{}:      unweighed(hash)def weighed.src(src: L.Src, +rs: List<&2, F.Reply>, +here: String, +hash: String) -> String:  match src:    case L.Hub{}:      ""    case L.Git{+url, +rev, _entry, _root, nar, _tag}:      weighed.heard(F.heard(rs, F.Git{hash, Up.Clone{Path.anchor(here, url), rev}}),        hash, nar, url, rev)# why the checkout a package of the lock was fetched as may not be laid, ""# when it may. A hub package has no checkout. A git package's checkout, the# one its fetch question was answered with, must weigh to the narHash the# lock records, so what is laid comes from the tree the lock pins and not# only from files that happen to match their sums.def weighed(+ss: List<&2, T.Sect>, +rs: List<&2, F.Reply>, +here: String, +hash: String) -> String:  weighed.src(L.pack.src(L.pack_of(ss, hash)), rs, here, hash)def weigh.got(ok: Bool, +why: String, ls: List<&2, W.Source>) -> Part:  match ok:    case True{}:      Got{ls}    case False{}:      No{why}# files that arrived stand only when the checkout they came from weighed to# the lock's narHash. A tree kept from BEND_LIB was not fetched, and is# checked file by file as before.def weigh.part(+why: String, part: Part) -> Part:  match part:    case AskLaid{}:      AskLaid{}    case AskFetch{}:      AskFetch{}    case Have{}:      Have{}    case Got{ls}:      weigh.got(String.is_empty(why), why, ls)    case No{w}:      No{w}# one package of the lock, and where it standstype Pt is Data:  Pt{hash: String, src: L.Src, files: List<&2, K.Item>, part: Part}# every package of the lock, and where each standsdef parts(  +ss: List<&2, T.Sect>,  +rs: List<&2, F.Reply>,  +here: String,  +hub: String,  hs: List<&2, String>) -> List<&2, Pt>:  match hs:    case []:      []    case +h <> t:      +pk = L.pack_of(ss, h)      +src = L.pack.src(pk)      +fs = L.pack.files(pk)      Pt{h, src, fs, weigh.part(weighed(ss, rs, here, h), part(rs, src, fs, here, hub, h))}        <> parts(ss, rs, here, hub, t)# ---------------------------------------------------------------------------# the names# where one name of the lock stands: its names file still to read, a file# that already holds the lock's hash, a file to write, with the hash the one# there held ("" when there was none), or why the name refuses the fetchtype Nm is Data:  NmAsk{nv: String}  NmKeep{}  NmPut{nv: String, hash: String, was: String}  NmStop{why: String}# a names file that was there, against the lock's hash. bend trims the file# before it reads the hash, and so does this.def name.held(same: Bool, +nv: String, +hash: String, +was: String) -> Nm:  match same:    case True{}:      NmKeep{}    case False{}:      NmPut{nv, hash, was}# what BEND_LIB holds for a namedef name.got(got: Maybe<&2, String>, +nv: String, +hash: String) -> Nm:  match got:    case None{}:      NmPut{nv, hash, ""}    case Some{text}:      +was = String.trim(text)      name.held(String.eq(was, hash), nv, hash, was)# a names file, once it is readdef name.answer(answer: F.Answer, +nv: String, +hash: String) -> Nm:  match answer:    case F.Held{got}:      name.got(got, nv, hash)    case _:      NmStop{"ez: " ++ nv ++ ": its names file was not read"}def name.heard(got: F.Heard, +nv: String, +hash: String) -> Nm:  match got:    case F.Open{}:      NmAsk{nv}    case F.Heard{a}:      name.answer(a, nv, hash)# a name bend would read back is looked for under BEND_LIB; any other stops# the fetch, since the file laid for it would not be the one bend readsdef name.valid(ok: Bool, +rs: List<&2, F.Reply>, +nv: String, +hash: String) -> Nm:  match ok:    case True{}:      name.heard(F.heard(rs, F.Named{nv}), nv, hash)    case False{}:      NmStop{"ez: the lock names " ++ nv ++ " as " ++ hash ++        ", which bend would not read back; run ez lock"}# where one name of the lock standsdef name.part(+rs: List<&2, F.Reply>, name: L.Name) -> Nm:  L.Name{+nv, +hash} = name  name.valid(L.name.valid(L.Name{nv, hash}), rs, nv, hash)# where every name of the lock standsdef name.parts(+rs: List<&2, F.Reply>, ns: List<&2, L.Name>) -> List<&2, Nm>:  match ns:    case []:      []    case h <> t:      name.part(rs, h) <> name.parts(rs, t)# how a rewritten names file is said to have been rewrittendef name.rewrote(+nv: String, +hash: String, +was: String) -> String:  "ez: BEND_LIB/names/" ++ nv ++ " named " ++ was ++ ", and the lock names " ++ hash ++    "; rewrote it"# a names file written, said first when it held another hashdef name.put.at(none: Bool, +nv: String, +hash: String, was: String, rest: List<&2, P.Effect>) -> List<&2, P.Effect>:  match none:    case True{}:      P.Name{nv, hash} <> rest    case False{}:      P.Say{name.rewrote(nv, hash, was)} <> P.Name{nv, hash} <> restdef name.put(+nv: String, +hash: String, +was: String, rest: List<&2, P.Effect>) -> List<&2, P.Effect>:  name.put.at(String.is_empty(was), nv, hash, was, rest)# every names file a fetch writes, ahead of what followsdef names.lay(ms: List<&2, Nm>, rest: List<&2, P.Effect>) -> List<&2, P.Effect>:  match ms:    case []:      rest    case m <> t:      match m:        case NmPut{nv, hash, was}:          name.put(nv, hash, was, names.lay(t, rest))        case _:          names.lay(t, rest)# how the names end the fetch: the first that refuses, or `o`, how the# packages ended it, which comes firstdef names.outcome(ms: List<&2, Nm>) -> P.Outcome:  match ms:    case []:      P.Success{}    case m <> t:      match m:        case NmStop{why}:          P.Refused{why}        case NmAsk{nv}:          P.Refused{"ez: " ++ nv ++ " was never looked for under BEND_LIB"}        case _:          names.outcome(t)# the packages' end, or, when they succeed, the names'def outcome.then(first: P.Outcome, then: P.Outcome) -> P.Outcome:  match first:    case P.Success{}:      then    case P.Refused{why}:      P.Refused{why}# the names still to read under BEND_LIBdef names.asks(ms: List<&2, Nm>) -> List<&2, F.Ask>:  match ms:    case []:      []    case m <> t:      match m:        case NmAsk{nv}:          F.Named{nv} <> names.asks(t)        case _:          names.asks(t)# every name the lock records, and where each standsdef name.parts.of(+ss: List<&2, T.Sect>, +world: F.World) -> List<&2, Nm>:  name.parts(F.replies(world), L.names(ss))# ---------------------------------------------------------------------------# the verdict# what one package comes to: nothing to do, a tree to lay, or a refusaltype Verdict is Data:  Keep{}  Put{hash: String, files: List<&2, W.Source>}  Stop{why: String}# the files that arrived, laid when they passdef judge.got(ok: Bool, fs: List<&2, K.Item>, +hash: String, +ls: List<&2, W.Source>) -> Verdict:  match ok:    case True{}:      Put{hash, ls}    case False{}:      Stop{bad.why(fs, hash, ls)}# what one package comes to, where it standsdef judge(+fs: List<&2, K.Item>, +hash: String, part: Part) -> Verdict:  match part:    case AskLaid{}:      Stop{"ez: " ++ hash ++ " was never looked for under BEND_LIB"}    case AskFetch{}:      Stop{"ez: " ++ hash ++ " was never fetched"}    case Have{}:      Keep{}    case Got{+ls}:      judge.got(good(fs, hash, ls), fs, hash, ls)    case No{why}:      Stop{why}# what every package comes todef judged(ps: List<&2, Pt>) -> List<&2, Verdict>:  match ps:    case []:      []    case Pt{h, _src, fs, p} <> t:      judge(fs, h, p) <> judged(t)# a tree laid under BEND_LIB: the files the lock names, then their manifest,# so a tree that has one is a tree that finisheddef lay(+hash: String, +ls: List<&2, W.Source>) -> P.Effect:  P.Lay{P.Cache{}, hash, List.append(&2, W.Source, ls, [W.Source{"manifest", manifest(ls)}])}# one verdict's effect, ahead of the restdef effect.v(verdict: Verdict, rest: List<&2, P.Effect>) -> List<&2, P.Effect>:  match verdict:    case Keep{}:      rest    case Put{hash, ls}:      lay(hash, ls) <> rest    case Stop{_why}:      rest# every verdict's effectdef effects.of(vs: List<&2, Verdict>) -> List<&2, P.Effect>:  match vs:    case []:      []    case v <> t:      effect.v(v, effects.of(t))# how the command ends: the first refusal, or successdef outcome.of(vs: List<&2, Verdict>) -> P.Outcome:  match vs:    case []:      P.Success{}    case v <> t:      match v:        case Stop{why}:          P.Refused{why}        case _:          outcome.of(t)# a plan that refuses has no effect at all; its reason is how it endsdef seal(es: List<&2, P.Effect>, outcome: P.Outcome) -> P.Plan:  match outcome:    case P.Success{}:      P.Plan{es, P.Success{}}    case P.Refused{why}:      P.Plan{[], P.Refused{why}}# the plan once every package and every name stands somewhere: the names# files, then the trees, and how the command ends. Each verdict is made once# and read by both halves.def parts.plan(ps: List<&2, Pt>, +ms: List<&2, Nm>) -> P.Plan:  +vs = judged(ps)  seal(names.lay(ms, effects.of(vs)), outcome.then(outcome.of(vs), names.outcome(ms)))# ---------------------------------------------------------------------------# the plan# every package of the lock on a World that has one, and where each standsdef parts.of(+ss: List<&2, T.Sect>, +world: F.World) -> List<&2, Pt>:  parts(ss, F.replies(world), F.here(world), L.lock.hub(ss), L.hashes(ss))# why a fetch refuses before anything is asked: no ez.toml, or no lockdef need.no() -> String:  "ez: no " ++ F.toml() ++ " here; ez fetch fills BEND_LIB for a project's lock, " ++    "and ez init makes one"def need.lock() -> String:  "ez: no " ++ F.lockfile() ++ " here; ez fetch reads the lock and resolves " ++    "nothing, and ez lock writes it"# the plan once the ledger and the lock are known to be theredef plan.lock(got: Maybe<&2, String>, +world: F.World) -> P.Plan:  match got:    case None{}:      P.Plan{[], P.Refused{need.lock()}}    case Some{_text}:      +ss = F.sects(world)      parts.plan(parts.of(ss, world), name.parts.of(ss, world))# the plan: a missing ledger, then a missing lock, refused before the restdef plan.led(led: Bool, got: Maybe<&2, String>, +world: F.World) -> P.Plan:  match led:    case False{}:      P.Plan{[], P.Refused{need.no()}}    case True{}:      plan.lock(got, world)# `ez fetch` over a Worlddef plan(+world: F.World) -> P.Plan:  plan.led(F.ledger(world), F.lock(world), world)# whether a plan ends in a refusaldef refuses.plan(pl: P.Plan) -> Bool:  P.Plan{_es, outcome} = pl  match outcome:    case P.Success{}:      False{}    case P.Refused{_why}:      True{}# whether `ez fetch` refuses on a Worlddef refuses(+world: F.World) -> Bool:  refuses.plan(plan(world))# ---------------------------------------------------------------------------# what is still to ask# one package's open question, ahead of the restdef ask.pt(pt: Pt, +here: String, +hub: String, rest: List<&2, F.Ask>) -> List<&2, F.Ask>:  Pt{+h, src, _fs, p} = pt  match p:    case AskLaid{}:      F.Laid{h} <> rest    case AskFetch{}:      fetch.ask(src, here, hub, h) <> rest    case _:      rest# every open questiondef asks.of(ps: List<&2, Pt>, +here: String, +hub: String) -> List<&2, F.Ask>:  match ps:    case []:      []    case p <> t:      ask.pt(p, here, hub, asks.of(t, here, hub))# the open questions once the ledger and the lock are known to be theredef wants.lock(got: Maybe<&2, String>, +world: F.World) -> List<&2, F.Ask>:  match got:    case None{}:      []    case Some{_text}:      +ss = F.sects(world)      List.append(&2, F.Ask, asks.of(parts.of(ss, world), F.here(world), L.lock.hub(ss)),        names.asks(name.parts.of(ss, world)))def wants.led(led: Bool, got: Maybe<&2, String>, +world: F.World) -> List<&2, F.Ask>:  match led:    case False{}:      []    case True{}:      wants.lock(got, world)# the questions a World still leaves open: none when the fetch refuses before# it asks, and otherwise one for each package that does not yet stand# anywheredef wants(+world: F.World) -> List<&2, F.Ask>:  wants.led(F.ledger(world), F.lock(world), world)# the questions still open, or the plan once there are nonetype Step is Data:  Asking{asks: List<&2, F.Ask>}  Run{plan: P.Plan}def step.asks(asks: List<&2, F.Ask>, ps: List<&2, Pt>, ms: List<&2, Nm>) -> Step:  match asks:    case []:      Run{parts.plan(ps, ms)}    case h <> t:      Asking{h <> t}def step.lock(got: Maybe<&2, String>, +world: F.World) -> Step:  match got:    case None{}:      Run{P.Plan{[], P.Refused{need.lock()}}}    case Some{_text}:      +ss = F.sects(world)      +ps = parts.of(ss, world)      +ms = name.parts.of(ss, world)      step.asks(List.append(&2, F.Ask, asks.of(ps, F.here(world), L.lock.hub(ss)), names.asks(ms)),        ps, ms)def step.led(led: Bool, got: Maybe<&2, String>, +world: F.World) -> Step:  match led:    case False{}:      Run{P.Plan{[], P.Refused{need.no()}}}    case True{}:      step.lock(got, world)# what the interpreter does next on a World. Every package is placed once per# round, and both the questions and the plan are read from that.def step(+world: F.World) -> Step:  step.led(F.ledger(world), F.lock(world), world)# ---------------------------------------------------------------------------# law vocabulary# the effects of a plandef effects(pl: P.Plan) -> List<&2, P.Effect>:  P.Plan{es, _outcome} = pl  es# every file of a tree but the last, which a `Lay` writes as the manifestdef front(fs: List<&2, W.Source>) -> List<&2, W.Source>:  match fs:    case []:      []    case h <> t:      match t:        case []:          []        case h2 <> t2:          h <> front(h2 <> t2)# the files the lock records under a hashdef locked(+ss: List<&2, T.Sect>, +hash: String) -> List<&2, K.Item>:  L.pack.files(L.pack_of(ss, hash))# whether an effect that lays a tree lays the files the lock records under# its hash, each with a text whose digest is the lock's sum for itdef lays.one(+ss: List<&2, T.Sect>, effect: P.Effect) -> Bool:  match effect:    case P.Lay{_place, hash, fs}:      fits(locked(ss, hash), front(fs))    case _:      True{}# whether every tree a list of effects lays is the one the lock recordsdef lays.locked(+ss: List<&2, T.Sect>, es: List<&2, P.Effect>) -> Bool:  match es:    case []:      True{}    case h <> t:      +rest = lays.locked(ss, t)      Bool.and(lays.one(ss, h), rest)# the source a checkout is asked of, "" for any other questiondef clone.url(ask: Up.Ask) -> String:  match ask:    case Up.Clone{url, _rev}:      url    case _:      ""# the package a question is aboutdef ask.hash(ask: F.Ask) -> String:  match ask:    case F.Laid{hash}:      hash    case F.Git{hash, _q}:      hash    case F.Hub{q}:      W.ask.hash(q)    case F.Named{nv}:      nv# whether a git question asks for the source the lock records for its# package, taken from the project: the lock's url anchored at `here`def ask.anchored(+here: String, src: L.Src, ask: F.Ask) -> Bool:  match ask:    case F.Git{_hash, q}:      String.eq(clone.url(q), Path.anchor(here, L.src.url(src)))    case _:      True{}# whether every question doesdef asks.anchored(+here: String, +ss: List<&2, T.Sect>, as: List<&2, F.Ask>) -> Bool:  match as:    case []:      True{}    case +h <> t:      +rest = asks.anchored(here, ss, t)      Bool.and(ask.anchored(here, L.pack.src(L.pack_of(ss, ask.hash(h))), h), rest)# whether an effect that lays a tree lays a package whose checkout, when it# has one, weighed to the narHash the lock recordsdef weighed.lay(  +ss: List<&2, T.Sect>,  +rs: List<&2, F.Reply>,  +here: String,  effect: P.Effect) -> Bool:  match effect:    case P.Lay{_place, hash, _fs}:      String.is_empty(weighed(ss, rs, here, hash))    case _:      True{}# whether every tree a list of effects lays doesdef lays.weighed(  +ss: List<&2, T.Sect>,  +rs: List<&2, F.Reply>,  +here: String,  es: List<&2, P.Effect>) -> Bool:  match es:    case []:      True{}    case h <> t:      +rest = lays.weighed(ss, rs, here, t)      Bool.and(weighed.lay(ss, rs, here, h), rest)