Project P167

Backup planner

An order system's changes hour by hour against a one-hour recovery point objective: a nightly backup that has never failed meets it for 1 hour of 24, and every archive carries the key that encrypts it.

2 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-19

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

An order system's changes hour by hour, a recovery point objective of one hour, three backup schedules, and what one archive holds.

  • main.eml - the menu, the system, a failure at each hour, what an archive holds, and a schedule of your own
  • backup.eml - the changes in each hour, the objective, the schedules, the age of the last backup before a failure and what it loses, and the archive's contents

How each part works:

  • 300 changes an hour at night, 4,200 in office hours, 51,100 a day. A failure at h:00 loses everything written since the last backup before it; the objective is to lose at most one hour of work.
  • As in the corpus case the-backup-ran-nightly-and-the-change-was-hourly, a nightly backup at 02:00 that has succeeded 400 nights in a row is within the objective for 1 of the 24 failure hours, and a failure just before it loses the whole day, 51,100 changes. Every 6 hours meets it for 4 hours of 24 and loses up to 25,200; only an incremental every hour meets it for all 24. The success count says the archive was made; the schedule alone says how old it is when it is needed.
  • As in the-backup-was-encrypted-and-the-key-was-in-the-backup, the archive is a snapshot of the whole host, and the key it is encrypted with is a file on that host: with 90 archives and 3 copies of each off site, the key is in 270 places, each next to the data it protects.

What is checked: menu choices 1 to 5; a first backup hour from 0 to 23 and a backup every 1 to 24 hours. An empty answer cancels.

Sessions: sessions/basic.in gives: - the system, a failure at each hour, and the archive; - your own schedules: from 02:00 every 5 hours, from 00:00 every hour, and once a day at 09:00.

sessions/bad-input.in gives: - menu choices 0 and x; - first hours: a cancel, 24 and x; then intervals with a cancel, 0, 25 and x; - from 23:00 every 7 hours, and from 03:00 every 12.

Built on the verified corpus cases the-backup-ran-nightly-and-the-change-was-hourly (a nightly backup, 400 successes, and a one-hour recovery point objective) and the-backup-was-encrypted-and-the-key-was-in-the-backup (encrypted host snapshots that hold the key material the host needs to start).

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal
== Backup planner ==
Backups that succeed every night, and what a failure still loses.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 0
Pick a number from 1 to 5.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> x
Pick a number from 1 to 5.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 4
first backup hour (0 to 23)> 
Cancelled.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 4
first backup hour (0 to 23)> 24
Type a whole number from 0 to 23.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 4
first backup hour (0 to 23)> x
Type a whole number from 0 to 23.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 4
first backup hour (0 to 23)> 3
every how many hours (1 to 24)> 
Cancelled.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 4
first backup hour (0 to 23)> 3
every how many hours (1 to 24)> 0
Type a whole number from 1 to 24.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 4
first backup hour (0 to 23)> 3
every how many hours (1 to 24)> 25
Type a whole number from 1 to 24.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 4
first backup hour (0 to 23)> 3
every how many hours (1 to 24)> x
Type a whole number from 1 to 24.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 4
first backup hour (0 to 23)> 23
every how many hours (1 to 24)> 7
Backups at 23:00, 06:00, 13:00 and 20:00.
This schedule: within the objective for 4 of 24 failure hours; the worst, at 20:00, loses 24,000 changes, 7 hours of work.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 4
first backup hour (0 to 23)> 3
every how many hours (1 to 24)> 12
Backups at 03:00 and 15:00.
This schedule: within the objective for 2 of 24 failure hours; the worst, at 15:00, loses 30,400 changes, 12 hours of work.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 5
Bye.
What was typed (27 lines)
0
x
4

4
24
4
x
4
3

4
3
0
4
3
25
4
3
x
4
23
7
4
3
12
5

basic

interpreter: byte-equal
== Backup planner ==
Backups that succeed every night, and what a failure still loses.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 1
An order system, hour by hour: 300 changes an hour from 00:00 to 07:00, 2,000 to 09:00, 4,200 to 18:00, 1,500 to 22:00 and 600 to midnight - 51,100 a day.
The recovery point objective: lose at most 1 hour of work.
The backups: nightly at 02:00, which has succeeded 400 nights in a row; every 6 hours; and nightly with an incremental every hour.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 2
A failure at each hour, the changes lost since the last backup, and their age (ok: within the objective):
  failure  nightly at 02:00   every 6 hours      nightly + hourly
  00:00     50,500 22h          7,200  6h            600  1h ok
  01:00     50,800 23h            300  1h ok         300  1h ok
  02:00     51,100 24h            600  2h            300  1h ok
  03:00        300  1h ok         900  3h            300  1h ok
  04:00        600  2h          1,200  4h            300  1h ok
  05:00        900  3h          1,500  5h            300  1h ok
  06:00      1,200  4h          1,800  6h            300  1h ok
  07:00      1,500  5h            300  1h ok         300  1h ok
  08:00      3,500  6h          2,300  2h          2,000  1h ok
  09:00      5,500  7h          4,300  3h          2,000  1h ok
  10:00      9,700  8h          8,500  4h          4,200  1h ok
  11:00     13,900  9h         12,700  5h          4,200  1h ok
  12:00     18,100 10h         16,900  6h          4,200  1h ok
  13:00     22,300 11h          4,200  1h ok       4,200  1h ok
  14:00     26,500 12h          8,400  2h          4,200  1h ok
  15:00     30,700 13h         12,600  3h          4,200  1h ok
  16:00     34,900 14h         16,800  4h          4,200  1h ok
  17:00     39,100 15h         21,000  5h          4,200  1h ok
  18:00     43,300 16h         25,200  6h          4,200  1h ok
  19:00     44,800 17h          1,500  1h ok       1,500  1h ok
  20:00     46,300 18h          3,000  2h          1,500  1h ok
  21:00     47,800 19h          4,500  3h          1,500  1h ok
  22:00     49,300 20h          6,000  4h          1,500  1h ok
  23:00     49,900 21h          6,600  5h            600  1h ok
nightly at 02:00: within the objective for 1 of 24 failure hours; the worst, at 02:00, loses 51,100 changes, 24 hours of work.
every 6 hours: within the objective for 4 of 24 failure hours; the worst, at 18:00, loses 25,200 changes, 6 hours of work.
nightly + hourly: every failure hour within the objective; the most a failure loses is 4,200 changes.
400 nights of success say the archive was made. How old it is when it is needed is set by the schedule alone.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 3
What one archive holds - a snapshot of the whole application host:
  /srv/orders/db          the order database                       48,000 MB
  /etc/orders/app.conf    the application's settings                    1 MB
  /etc/orders/backup.key  the key the archive is encrypted with         1 MB
90 archives are kept, each with 3 copies off site: the key is in 270 places, each next to the data it protects. Anyone who holds one archive can open it.
With /etc/orders/backup.key left out of the snapshot, a restore asks the key service for the key: no copy in any archive, and one more step in the drill.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 4
first backup hour (0 to 23)> 2
every how many hours (1 to 24)> 5
Backups at 02:00, 07:00, 12:00, 17:00 and 22:00.
This schedule: within the objective for 5 of 24 failure hours; the worst, at 17:00, loses 21,000 changes, 5 hours of work.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 4
first backup hour (0 to 23)> 0
every how many hours (1 to 24)> 1
Backups at 00:00, 01:00, 02:00, 03:00, 04:00, 05:00, 06:00, 07:00, 08:00, 09:00, 10:00, 11:00, 12:00, 13:00, 14:00, 15:00, 16:00, 17:00, 18:00, 19:00, 20:00, 21:00, 22:00 and 23:00.
This schedule: every failure hour within the objective; the most a failure loses is 4,200 changes.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 4
first backup hour (0 to 23)> 9
every how many hours (1 to 24)> 24
Backups at 09:00.
This schedule: within the objective for 1 of 24 failure hours; the worst, at 09:00, loses 51,100 changes, 24 hours of work.

1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit
choice> 5
Bye.
What was typed (13 lines)
1
2
3
4
2
5
4
0
1
4
9
24
5

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

eml
# P167 backup planner: an order system's changes hour by hour against a
# one-hour recovery point objective - the system, a failure at each hour
# under three schedules, what one archive holds, and a schedule of your own.
import backup

def plain(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def pad(s, n):
    s => out
    while len(out) < n:
        out + " " => out
    return out

def lpad(s, n):
    s => out
    while len(out) < n:
        " " + out => out
    return out

def digits(t):
    for c in t:
        if c < "0" or c > "9":
            return False
    return len(t) > 0

def commas(n):
    str(n) => s
    "" => out
    0 => k
    len(s) => i
    while i > 0:
        if k == 3:
            "," + out => out
            0 => k
        s[i - 1] + out => out
        k + 1 => k
        i - 1 => i
    return out

def clock(h):
    if h < 10:
        return "0" + str(h) + ":00"
    return str(h) + ":00"

def clocks(hours):
    "" => out
    0 => i
    while i < len(hours):
        if i > 0 and i == len(hours) - 1:
            out + " and " => out
        elif i > 0:
            out + ", " => out
        out + clock(hours[i]) => out
        i + 1 => i
    return out

def hours_word(n):
    if n == 1:
        return "1 hour"
    return str(n) + " hours"

def show_system():
    ("An order system, hour by hour: 300 changes an hour from 00:00 to 07:00, 2,000 to 09:00, 4,200 to 18:00, 1,500 to 22:00 and 600 to midnight - " + commas(backup.per_day()) + " a day.") ^0
    ("The recovery point objective: lose at most " + hours_word(backup.rpo()) + " of work.") ^0
    "The backups: nightly at 02:00, which has succeeded 400 nights in a row; every 6 hours; and nightly with an incremental every hour." ^0

def summary(sc):
    # [failure hours within the objective, the worst hour, its loss, its age]
    0 => within
    0 => worst
    0 => h
    while h < 24:
        if backup.age(sc, h) <= backup.rpo():
            within + 1 => within
        if backup.lost(sc, h) > backup.lost(sc, worst):
            h => worst
        h + 1 => h
    return [within, worst, backup.lost(sc, worst), backup.age(sc, worst)]

def verdict(name, sc):
    summary(sc) => s
    if s[0] == 24:
        return name + ": every failure hour within the objective; the most a failure loses is " + commas(s[2]) + " changes."
    return name + ": within the objective for " + str(s[0]) + " of 24 failure hours; the worst, at " + clock(s[1]) + ", loses " + commas(s[2]) + " changes, " + hours_word(s[3]) + " of work."

def cell(sc, h):
    backup.age(sc, h) => a
    lpad(commas(backup.lost(sc, h)), 7) + lpad(str(a) + "h", 4) => out
    if a <= backup.rpo():
        out + " ok" => out
    else:
        out + "   " => out
    return out

def show_failures():
    "A failure at each hour, the changes lost since the last backup, and their age (ok: within the objective):" ^0
    backup.schedules() => scs
    ("  " + pad("failure", 9) + pad(scs[0][0], 19) + pad(scs[1][0], 19) + scs[2][0]) ^0
    0 => h
    while h < 24:
        ("  " + pad(clock(h), 9) + pad(cell(scs[0][1], h), 19) + pad(cell(scs[1][1], h), 19) + cell(scs[2][1], h)) ^0
        h + 1 => h
    for sc in scs:
        verdict(sc[0], sc[1]) ^0
    "400 nights of success say the archive was made. How old it is when it is needed is set by the schedule alone." ^0

def show_archive():
    "What one archive holds - a snapshot of the whole application host:" ^0
    for f in backup.archive():
        ("  " + pad(f[0], 24) + pad(f[1], 40) + lpad(commas(f[2]) + " MB", 10)) ^0
    backup.kept() => k
    (str(k[0]) + " archives are kept, each with " + str(k[1]) + " copies off site: the key is in " + str(k[0] * k[1]) + " places, each next to the data it protects. Anyone who holds one archive can open it.") ^0
    "With /etc/orders/backup.key left out of the snapshot, a restore asks the key service for the key: no copy in any archive, and one more step in the drill." ^0

def own():
    plain(input("first backup hour (0 to 23)> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    if len(t) > 2 or not digits(t) or int(t) > 23:
        "Type a whole number from 0 to 23." ^0
        return
    plain(input("every how many hours (1 to 24)> ")) => n
    if n == "":
        "Cancelled." ^0
        return
    if len(n) > 2 or not digits(n) or int(n) < 1 or int(n) > 24:
        "Type a whole number from 1 to 24." ^0
        return
    backup.every(int(t), int(n)) => sc
    ("Backups at " + clocks(sc) + ".") ^0
    verdict("This schedule", sc) ^0

"== Backup planner ==" ^0
"Backups that succeed every night, and what a failure still loses." ^0
True => running
while running:
    "" ^0
    "1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_system()
    elif choice == "2":
        show_failures()
    elif choice == "3":
        show_archive()
    elif choice == "4":
        own()
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
Python projection (main.py)
import backup

def plain(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def pad(s, n):
    out = s
    while len(out) < n:
        out = out + " "
    return out

def lpad(s, n):
    out = s
    while len(out) < n:
        out = " " + out
    return out

def digits(t):
    for c in t:
        if c < "0" or c > "9":
            return False
    return len(t) > 0

def commas(n):
    s = str(n)
    out = ""
    k = 0
    i = len(s)
    while i > 0:
        if k == 3:
            out = "," + out
            k = 0
        out = s[i - 1] + out
        k = k + 1
        i = i - 1
    return out

def clock(h):
    if h < 10:
        return "0" + str(h) + ":00"
    return str(h) + ":00"

def clocks(hours):
    out = ""
    i = 0
    while i < len(hours):
        if i > 0 and i == len(hours) - 1:
            out = out + " and "
        elif i > 0:
            out = out + ", "
        out = out + clock(hours[i])
        i = i + 1
    return out

def hours_word(n):
    if n == 1:
        return "1 hour"
    return str(n) + " hours"

def show_system():
    print("An order system, hour by hour: 300 changes an hour from 00:00 to 07:00, 2,000 to 09:00, 4,200 to 18:00, 1,500 to 22:00 and 600 to midnight - " + commas(backup.per_day()) + " a day.")
    print("The recovery point objective: lose at most " + hours_word(backup.rpo()) + " of work.")
    print("The backups: nightly at 02:00, which has succeeded 400 nights in a row; every 6 hours; and nightly with an incremental every hour.")

def summary(sc):
    within = 0
    worst = 0
    h = 0
    while h < 24:
        if backup.age(sc, h) <= backup.rpo():
            within = within + 1
        if backup.lost(sc, h) > backup.lost(sc, worst):
            worst = h
        h = h + 1
    return [within, worst, backup.lost(sc, worst), backup.age(sc, worst)]

def verdict(name, sc):
    s = summary(sc)
    if s[0] == 24:
        return name + ": every failure hour within the objective; the most a failure loses is " + commas(s[2]) + " changes."
    return name + ": within the objective for " + str(s[0]) + " of 24 failure hours; the worst, at " + clock(s[1]) + ", loses " + commas(s[2]) + " changes, " + hours_word(s[3]) + " of work."

def cell(sc, h):
    a = backup.age(sc, h)
    out = lpad(commas(backup.lost(sc, h)), 7) + lpad(str(a) + "h", 4)
    if a <= backup.rpo():
        out = out + " ok"
    else:
        out = out + "   "
    return out

def show_failures():
    print("A failure at each hour, the changes lost since the last backup, and their age (ok: within the objective):")
    scs = backup.schedules()
    print("  " + pad("failure", 9) + pad(scs[0][0], 19) + pad(scs[1][0], 19) + scs[2][0])
    h = 0
    while h < 24:
        print("  " + pad(clock(h), 9) + pad(cell(scs[0][1], h), 19) + pad(cell(scs[1][1], h), 19) + cell(scs[2][1], h))
        h = h + 1
    for sc in scs:
        print(verdict(sc[0], sc[1]))
    print("400 nights of success say the archive was made. How old it is when it is needed is set by the schedule alone.")

def show_archive():
    print("What one archive holds - a snapshot of the whole application host:")
    for f in backup.archive():
        print("  " + pad(f[0], 24) + pad(f[1], 40) + lpad(commas(f[2]) + " MB", 10))
    k = backup.kept()
    print(str(k[0]) + " archives are kept, each with " + str(k[1]) + " copies off site: the key is in " + str(k[0] * k[1]) + " places, each next to the data it protects. Anyone who holds one archive can open it.")
    print("With /etc/orders/backup.key left out of the snapshot, a restore asks the key service for the key: no copy in any archive, and one more step in the drill.")

def own():
    t = plain(input("first backup hour (0 to 23)> "))
    if t == "":
        print("Cancelled.")
        return
    if len(t) > 2 or not digits(t) or int(t) > 23:
        print("Type a whole number from 0 to 23.")
        return
    n = plain(input("every how many hours (1 to 24)> "))
    if n == "":
        print("Cancelled.")
        return
    if len(n) > 2 or not digits(n) or int(n) < 1 or int(n) > 24:
        print("Type a whole number from 1 to 24.")
        return
    sc = backup.every(int(t), int(n))
    print("Backups at " + clocks(sc) + ".")
    print(verdict("This schedule", sc))

print("== Backup planner ==")
print("Backups that succeed every night, and what a failure still loses.")
running = True
while running:
    print("")
    print("1) the system  2) a failure at each hour  3) what an archive holds  4) a schedule of your own  5) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_system()
    elif choice == "2":
        show_failures()
    elif choice == "3":
        show_archive()
    elif choice == "4":
        own()
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")

backup.eml

eml
# P167 backup planner - an order system's changes hour by hour, a recovery
# point objective of one hour, and three backup schedules. As in the corpus
# case the-backup-ran-nightly-and-the-change-was-hourly, a backup that has
# succeeded every night says the artifact was made; how old it is when it is
# needed is set by the schedule alone. And as in
# the-backup-was-encrypted-and-the-key-was-in-the-backup, an archive that is a
# snapshot of the whole host carries the key that encrypts it.

def rpo():
    # hours of work the business can afford to lose
    return 1

def changes(h):
    # orders and updates written in hour h (0 to 23)
    if h < 7:
        return 300
    if h < 9:
        return 2000
    if h < 18:
        return 4200
    if h < 22:
        return 1500
    return 600

def schedules():
    # [name, the hours a backup runs]
    return [["nightly at 02:00", [2]],
            ["every 6 hours", [0, 6, 12, 18]],
            ["nightly + hourly", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]]]

def every(start, n):
    # backups every n hours from start, the same hours each day
    [] => out
    0 => k
    while k < 24:
        (start + k) % 24 => h
        False => dup
        for x in out:
            if x == h:
                True => dup
        if not dup:
            out + [h] => out
        k + n => k
    return out

def age(hours, h):
    # hours since the last backup before a failure at h:00 (1 to 24)
    1 => a
    while a <= 24:
        (h - a + 24) % 24 => b
        for x in hours:
            if x == b:
                return a
        a + 1 => a
    return 24

def lost(hours, h):
    # changes written since that backup
    age(hours, h) => a
    0 => t
    1 => k
    while k <= a:
        t + changes((h - k + 24) % 24) => t
        k + 1 => k
    return t

def per_day():
    0 => t
    0 => h
    while h < 24:
        t + changes(h) => t
        h + 1 => h
    return t

def archive():
    # [path, what it is, size in MB] in a snapshot of the application host
    return [["/srv/orders/db", "the order database", 48000],
            ["/etc/orders/app.conf", "the application's settings", 1],
            ["/etc/orders/backup.key", "the key the archive is encrypted with", 1]]

def kept():
    # archives kept, and the copies of each held off site
    return [90, 3]
Python projection (backup.py)
def rpo():
    return 1

def changes(h):
    if h < 7:
        return 300
    if h < 9:
        return 2000
    if h < 18:
        return 4200
    if h < 22:
        return 1500
    return 600

def schedules():
    return [["nightly at 02:00", [2]], ["every 6 hours", [0, 6, 12, 18]], ["nightly + hourly", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]]]

def every(start, n):
    out = []
    k = 0
    while k < 24:
        h = (start + k) % 24
        dup = False
        for x in out:
            if x == h:
                dup = True
        if not dup:
            out = out + [h]
        k = k + n
    return out

def age(hours, h):
    a = 1
    while a <= 24:
        b = (h - a + 24) % 24
        for x in hours:
            if x == b:
                return a
        a = a + 1
    return 24

def lost(hours, h):
    a = age(hours, h)
    t = 0
    k = 1
    while k <= a:
        t = t + changes((h - k + 24) % 24)
        k = k + 1
    return t

def per_day():
    t = 0
    h = 0
    while h < 24:
        t = t + changes(h)
        h = h + 1
    return t

def archive():
    return [["/srv/orders/db", "the order database", 48000], ["/etc/orders/app.conf", "the application's settings", 1], ["/etc/orders/backup.key", "the key the archive is encrypted with", 1]]

def kept():
    return [90, 3]

Built on these corpus cases

ΣEML 2026

Efficient New Language — a high-density semantic overlay for humans and AI agents.

This site’s demos run on the real EML toolchain, in your browser.

© 2026 EveMissLab (一言諾科技有限公司) / Neo.K. All rights reserved.