# agenda — what you have taken on, what you owe, and what you may not do. # # This module supplies the REASONING. You supply the facts: what you are # working towards (`agenda.Goal`), what you have undertaken to do by a date # (`agenda.Commitment`), what you owe someone by a date (`agenda.Obligation`), # and what you may not do (`agenda.Prohibition`). Import this module and the # standing rules and the queries below apply to whatever you have loaded. # # import agenda # # Nothing here acts, reminds or chooses. The store keeps no clock, so every # question that depends on the time takes the time as its argument: you ask # what is overdue AT a moment, and you get the same answer every time you ask # about that moment. What the rules conclude withdraws itself when a fact it # rests on is forgotten. class Goal: # What the goal is, for reading. The rules below do not need it: they take # every name a commitment serves as a goal, recorded or not, and # `unrecorded()` below is how you find a misspelt one. text: string class Commitment: # Something you have undertaken to do. `due` is when it is due; moving it # is `update`, which keeps the date it replaces, and `previous_dues()` # reads those back. `done_at` is when it was done, and a commitment with # none is still open. `serves` names the goal it is for, if any. action: string due: datetime serves: string done_at: datetime class Obligation: # Something you owe `owed_to`, due by `due`. Read the same way as a # commitment. The difference is that you did not choose it. action: string due: datetime owed_to: string done_at: datetime class Prohibition: # An action you may not take. Write it with a validity window to say when # it applies; written without one, it always applies. action: string # ── Standing rules ─────────────────────────────────────────────────────────── # # A rule holds continuously: its conclusion is in force exactly while its # condition holds, and withdraws itself when the condition stops holding. You # never write these; the engine maintains them. @rule def commitment_forbidden(): # A commitment to an action a prohibition covers. It holds for as long as # the prohibition does. for c in Commitment, p in Prohibition: if c.action == p.action: c.forbidden = True @rule def obligation_forbidden(): # An obligation to an action a prohibition covers: what you owe and what # you may not do disagree. The module names it and leaves the choice to you. for o in Obligation, p in Prohibition: if o.action == p.action: o.forbidden = True @rule def commitment_late(): for c in Commitment: if c.done_at > c.due: c.late = True @rule def obligation_late(): for o in Obligation: if o.done_at > o.due: o.late = True @rule def goal_pursued(): for c in Commitment, g in Goal: if c.serves == g: g.pursued = True @rule def goal_met(): # Every commitment serving the goal is done. for g in Goal: if g.pursued == True and none(c in Commitment: c.serves == g and absent(c.done_at)): g.met = True # ── Standing constraints ───────────────────────────────────────────────────── # # `checkup()` reports each of these that does not hold now, naming what # breaches it: an open item whose action is forbidden at present. Neither # refuses a write. @constraint def nothing_forbidden_committed(): for c in Commitment: if c.forbidden == True and absent(c.done_at): violated @constraint def nothing_forbidden_owed(): for o in Obligation: if o.forbidden == True and absent(o.done_at): violated # ── Queries ────────────────────────────────────────────────────────────────── # # A query that answers commitments and obligations names each as # `Commitment.` or `Obligation.`, which is also how # `previous_dues()` takes one. def overdue(at: datetime) -> list: # Every commitment and obligation due before `at` and not done by then. # One whose due date or completion two sources disagree on is in # `unsettled(at)` instead. out = [] cs = list_subjects(Commitment.due, at) for c in cs: try: d = current(Commitment.c.due, at) f = current(Commitment.c.done_at, at) match d.value: case datetime as due: if due < at: match f: case active_claim: match f.value: case datetime as done: if done > at: out = out + ["Commitment." + c] case default: pass case empty: out = out + ["Commitment." + c] case default: pass except refuse: pass os = list_subjects(Obligation.due, at) for o in os: try: d = current(Obligation.o.due, at) f = current(Obligation.o.done_at, at) match d.value: case datetime as due: if due < at: match f: case active_claim: match f.value: case datetime as done: if done > at: out = out + ["Obligation." + o] case default: pass case empty: out = out + ["Obligation." + o] case default: pass except refuse: pass return sorted(out) def due_within(at: datetime, span: duration) -> list: # Every commitment and obligation not done by `at` and due from `at` until # `span` after it — what is coming up. out = [] until = at + span cs = list_subjects(Commitment.due, at) for c in cs: try: d = current(Commitment.c.due, at) f = current(Commitment.c.done_at, at) match d.value: case datetime as due: if due >= at and due < until: match f: case active_claim: match f.value: case datetime as done: if done > at: out = out + ["Commitment." + c] case default: pass case empty: out = out + ["Commitment." + c] case default: pass except refuse: pass os = list_subjects(Obligation.due, at) for o in os: try: d = current(Obligation.o.due, at) f = current(Obligation.o.done_at, at) match d.value: case datetime as due: if due >= at and due < until: match f: case active_claim: match f.value: case datetime as done: if done > at: out = out + ["Obligation." + o] case default: pass case empty: out = out + ["Obligation." + o] case default: pass except refuse: pass return sorted(out) def unsettled(at: datetime) -> list: # Every commitment and obligation whose due date or completion two sources # give differently at `at`. It has no one answer to place in time, so the # two queries above leave it out and this one names it. out = [] cs = list_subjects(Commitment, at) for c in cs: try: d = current(Commitment.c.due, at) f = current(Commitment.c.done_at, at) except refuse: out = out + ["Commitment." + c] os = list_subjects(Obligation, at) for o in os: try: d = current(Obligation.o.due, at) f = current(Obligation.o.done_at, at) except refuse: out = out + ["Obligation." + o] return sorted(out) def forbidden(at: datetime) -> list: # Every commitment and obligation open at `at` whose action a prohibition # covers at `at` — what you may not do at that moment, whenever it is due. out = [] cs = list_subjects(Commitment.forbidden, at) for c in cs: try: f = current(Commitment.c.done_at, at) match f: case active_claim: match f.value: case datetime as done: if done > at: out = out + ["Commitment." + c] case default: pass case empty: out = out + ["Commitment." + c] except refuse: pass os = list_subjects(Obligation.forbidden, at) for o in os: try: f = current(Obligation.o.done_at, at) match f: case active_claim: match f.value: case datetime as done: if done > at: out = out + ["Obligation." + o] case default: pass case empty: out = out + ["Obligation." + o] except refuse: pass return sorted(out) def blocked() -> list: # Every open commitment and obligation whose action a prohibition covers at # the moment it is due: it cannot be done on time without doing what you # may not. out = [] cs = list_subjects(Commitment.due) for c in cs: try: d = current(Commitment.c.due) f = current(Commitment.c.done_at) match f: case active_claim: pass case empty: match d.value: case datetime as due: b = current(Commitment.c.forbidden, due) match b: case active_claim: out = out + ["Commitment." + c] case empty: pass case default: pass except refuse: pass os = list_subjects(Obligation.due) for o in os: try: d = current(Obligation.o.due) f = current(Obligation.o.done_at) match f: case active_claim: pass case empty: match d.value: case datetime as due: b = current(Obligation.o.forbidden, due) match b: case active_claim: out = out + ["Obligation." + o] case empty: pass case default: pass except refuse: pass return sorted(out) def late() -> list: # Every commitment and obligation done after it was due. out = [] cs = subjects(Commitment.late, True) for c in cs: out = out + ["Commitment." + c] os = subjects(Obligation.late, True) for o in os: out = out + ["Obligation." + o] return sorted(out) def met() -> list: # Every goal whose commitments are all done. out = [] gs = subjects(Goal.met, True) for g in gs: out = out + [g] return sorted(out) def serving(g: string) -> list: # Every commitment that serves goal `g`. out = [] cs = subjects(Commitment.serves, g) for c in cs: out = out + ["Commitment." + c] return sorted(out) def previous_dues(item: string) -> list: # Every due date `item` had before the one it has now, oldest first: one # for each time it was moved with `update`. An item whose due date two # sources disagree on has no one date now, and answers none. out = [] i = item.find(".") kind = item[:i] s = item[i + 1:] try: if kind == "Commitment": ds = get(Commitment.s.due) now = current(Commitment.s.due) match now: case active_claim: for d in ds: if d.claim_id != now.claim_id: out = out + [str(d.value)] case empty: pass if kind == "Obligation": ds = get(Obligation.s.due) now = current(Obligation.s.due) match now: case active_claim: for d in ds: if d.claim_id != now.claim_id: out = out + [str(d.value)] case empty: pass except refuse: pass return out def unrecorded() -> list: # Every goal a commitment serves that no goal carries — usually a # misspelling. It reads what each commitment serves now. names = [] served = list_values(Commitment.serves) for c in served: names = names + [str(c.value)] out = [] for n in names: r = current(Goal.n.text) match r: case active_claim: pass case empty: if n not in out: out = out + [n] return sorted(out)