<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P158-bug-triage/ | updated: 2026-10-18 -->

# P158 Bug triage

Two findings with one shared cause patched where each was seen, one report with two causes patched to its own inputs, and a backlog that outruns its fixes worked in arrival order and worst first.

EML-P project `projects/bug-triage` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/bug-triage` runs it and `eml project verify projects/bug-triage` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: two-findings-one-defect (https://efficientnewlanguage.org/cases/371-two-findings-one-defect/), one-report-two-mechanisms (https://efficientnewlanguage.org/cases/361-one-report-two-mechanisms/), finding-outruns-fixing (https://efficientnewlanguage.org/cases/360-finding-outruns-fixing/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
3

3
3
3
x
2
4
```

Screen:

```text
== Bug triage ==
Two findings with one cause, one report with two, and a backlog that outruns its fixes.

1) two findings  2) one report  3) the backlog  4) quit
choice> 0
Pick a number from 1 to 4.

1) two findings  2) one report  3) the backlog  4) quit
choice> x
Pick a number from 1 to 4.

1) two findings  2) one report  3) the backlog  4) quit
choice> 3
fixes each period (1 or 2)> 
Cancelled.

1) two findings  2) one report  3) the backlog  4) quit
choice> 3
fixes each period (1 or 2)> 3
Type 1 or 2.

1) two findings  2) one report  3) the backlog  4) quit
choice> 3
fixes each period (1 or 2)> x
Type 1 or 2.

1) two findings  2) one report  3) the backlog  4) quit
choice> 2
One report, three wrong balances:
     -4 shows    4, should be -4
    150 shows   99, should be 150
     -7 shows    7, should be -7
Over the 12 inputs -9, -4, 0, 3, 42, 99, 100, 150, 260, -7, -1, 77:
  fix                   wrong
  none                  7   -9, -4, 100, 150, 260, -7, -1
  the reported inputs   4   -9, 100, 260, -1
  keep the sign         3   100, 150, 260
  no clamp at 99        4   -9, -4, -7, -1
  both                  0   none
Fitted to the report, the report passes and 4 failures remain; each mechanism run alone accounts for its share:
  the sign dropped:      -9, -1
  past 99 clamped:       100, 260
The report could not show that two of its rows came from one cause and the third from another.

1) two findings  2) one report  3) the backlog  4) quit
choice> 4
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
2
3
1
3
2
4
```

Screen:

```text
== Bug triage ==
Two findings with one cause, one report with two, and a backlog that outruns its fixes.

1) two findings  2) one report  3) the backlog  4) quit
choice> 1
Two findings, filed a week apart: the weekly report starts a day late; reminders come a day late.
Each candidate cause alone, and the screens it makes wrong on the days 3, 10, 17, 24 and 31:
  the date parser:      report, reminder, audit
  the report's code:    report
  the reminder's code:  reminder
Only the parser explains both findings, and it predicts a third nobody filed: the audit log.
Days right of 5, as the fixes go in:
                              report  reminder  audit
  as shipped                       0         0      0
  patched where seen               5         5      0
  parser fixed                     5         5      5
  parser fixed, patches left       0         0      5
Patched where seen, both findings close and the audit log stays wrong; fix the parser later and the two patches make their screens wrong again.

1) two findings  2) one report  3) the backlog  4) quit
choice> 2
One report, three wrong balances:
     -4 shows    4, should be -4
    150 shows   99, should be 150
     -7 shows    7, should be -7
Over the 12 inputs -9, -4, 0, 3, 42, 99, 100, 150, 260, -7, -1, 77:
  fix                   wrong
  none                  7   -9, -4, 100, 150, 260, -7, -1
  the reported inputs   4   -9, 100, 260, -1
  keep the sign         3   100, 150, 260
  no clamp at 99        4   -9, -4, -7, -1
  both                  0   none
Fitted to the report, the report passes and 4 failures remain; each mechanism run alone accounts for its share:
  the sign dropped:      -9, -1
  past 99 clamped:       100, 260
The report could not show that two of its rows came from one cause and the third from another.

1) two findings  2) one report  3) the backlog  4) quit
choice> 3
fixes each period (1 or 2)> 1
Two findings arrive in each of periods 1 to 5; 1 fixed a period; the harm each does every period it waits is summed over 6 periods.
  in arrival order:  harm 127; still open: f7, f8, f9, f10
  worst first:       harm  71; still open: f1, f3, f5, f9
The same number fixed each period, and the order changes the harm by 56: once findings outrun fixes, the order is not a preference.

1) two findings  2) one report  3) the backlog  4) quit
choice> 3
fixes each period (1 or 2)> 2
Two findings arrive in each of periods 1 to 5; 2 fixed a period; the harm each does every period it waits is summed over 6 periods.
  in arrival order:  harm  48; still open: none
  worst first:       harm  48; still open: none
The order changes nothing here.
With fixes keeping up, the queue empties either way.

1) two findings  2) one report  3) the backlog  4) quit
choice> 4
Bye.
```

## Modules

### main.eml

```eml
# P158 bug triage: two findings with one cause, one report with two causes,
# and a backlog that grows faster than it is fixed.
import triage

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 listed(xs):
    "" => out
    for x in xs:
        if out == "":
            str(x) => out
        else:
            out + ", " + str(x) => out
    if out == "":
        return "none"
    return out

def wrong_screens(parser_bug, report_bug, reminder_bug):
    [] => out
    for d in triage.days():
        if triage.report(d, parser_bug, report_bug) != d % 7:
            out + ["report"] => out
        if triage.reminder(d, parser_bug, reminder_bug) != d % 7:
            out + ["reminder"] => out
        if triage.audit(d, parser_bug) != d % 7:
            out + ["audit"] => out
    [] => names
    for s in ["report", "reminder", "audit"]:
        for x in out:
            if x == s:
                False => have
                for n in names:
                    if n == s:
                        True => have
                if not have:
                    names + [s] => names
    return names

def two_findings():
    "Two findings, filed a week apart: the weekly report starts a day late; reminders come a day late." ^0
    "Each candidate cause alone, and the screens it makes wrong on the days 3, 10, 17, 24 and 31:" ^0
    ("  the date parser:      " + listed(wrong_screens(True, False, False))) ^0
    ("  the report's code:    " + listed(wrong_screens(False, True, False))) ^0
    ("  the reminder's code:  " + listed(wrong_screens(False, False, True))) ^0
    "Only the parser explains both findings, and it predicts a third nobody filed: the audit log." ^0
    "Days right of 5, as the fixes go in:" ^0
    ("  " + pad("", 28) + "report  reminder  audit") ^0
    for st in triage.states():
        ("  " + pad(st[0], 28) + lpad(str(triage.right_days("report", st[1], st[2])), 6) + lpad(str(triage.right_days("reminder", st[1], st[2])), 10) + lpad(str(triage.right_days("audit", st[1], st[2])), 7)) ^0
    "Patched where seen, both findings close and the audit log stays wrong; fix the parser later and the two patches make their screens wrong again." ^0

def one_report():
    "One report, three wrong balances:" ^0
    for r in triage.reported():
        ("  " + lpad(str(r), 5) + " shows " + lpad(triage.render(r, True, True), 4) + ", should be " + triage.truth(r)) ^0
    ("Over the " + str(len(triage.inputs())) + " inputs " + listed(triage.inputs()) + ":") ^0
    ("  " + pad("fix", 22) + "wrong") ^0
    for fx in ["none", "fitted", "sign", "clamp", "both"]:
        [] => bad
        for v in triage.inputs():
            if triage.shown(v, fx) != triage.truth(v):
                bad + [v] => bad
        if fx == "fitted":
            "the reported inputs" => name
        elif fx == "sign":
            "keep the sign" => name
        elif fx == "clamp":
            "no clamp at 99" => name
        else:
            fx => name
        ("  " + pad(name, 22) + pad(str(len(bad)), 4) + listed(bad)) ^0
    [] => s
    [] => c
    0 => left
    for v in triage.inputs():
        if triage.shown(v, "fitted") != triage.truth(v):
            left + 1 => left
            if triage.render(v, True, False) != triage.truth(v):
                s + [v] => s
            if triage.render(v, False, True) != triage.truth(v):
                c + [v] => c
    ("Fitted to the report, the report passes and " + str(left) + " failures remain; each mechanism run alone accounts for its share:") ^0
    ("  the sign dropped:      " + listed(s)) ^0
    ("  past 99 clamped:       " + listed(c)) ^0
    "The report could not show that two of its rows came from one cause and the third from another." ^0

def backlog():
    plain(input("fixes each period (1 or 2)> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    if t != "1" and t != "2":
        "Type 1 or 2." ^0
        return
    int(t) => cap
    ("Two findings arrive in each of periods 1 to 5; " + t + " fixed a period; the harm each does every period it waits is summed over 6 periods.") ^0
    triage.work("fifo", cap, 6) => a
    triage.work("harm", cap, 6) => b
    ("  in arrival order:  harm " + lpad(str(a[0]), 3) + "; still open: " + listed(a[2])) ^0
    ("  worst first:       harm " + lpad(str(b[0]), 3) + "; still open: " + listed(b[2])) ^0
    if a[0] != b[0]:
        ("The same number fixed each period, and the order changes the harm by " + str(a[0] - b[0]) + ": once findings outrun fixes, the order is not a preference.") ^0
    else:
        "The order changes nothing here." ^0
    if len(a[2]) == 0 and len(b[2]) == 0:
        "With fixes keeping up, the queue empties either way." ^0

"== Bug triage ==" ^0
"Two findings with one cause, one report with two, and a backlog that outruns its fixes." ^0
True => running
while running:
    "" ^0
    "1) two findings  2) one report  3) the backlog  4) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        two_findings()
    elif choice == "2":
        one_report()
    elif choice == "3":
        backlog()
    elif choice == "4":
        False => running
    else:
        "Pick a number from 1 to 4." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import triage

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 listed(xs):
    out = ""
    for x in xs:
        if out == "":
            out = str(x)
        else:
            out = out + ", " + str(x)
    if out == "":
        return "none"
    return out

def wrong_screens(parser_bug, report_bug, reminder_bug):
    out = []
    for d in triage.days():
        if triage.report(d, parser_bug, report_bug) != d % 7:
            out = out + ["report"]
        if triage.reminder(d, parser_bug, reminder_bug) != d % 7:
            out = out + ["reminder"]
        if triage.audit(d, parser_bug) != d % 7:
            out = out + ["audit"]
    names = []
    for s in ["report", "reminder", "audit"]:
        for x in out:
            if x == s:
                have = False
                for n in names:
                    if n == s:
                        have = True
                if not have:
                    names = names + [s]
    return names

def two_findings():
    print("Two findings, filed a week apart: the weekly report starts a day late; reminders come a day late.")
    print("Each candidate cause alone, and the screens it makes wrong on the days 3, 10, 17, 24 and 31:")
    print("  the date parser:      " + listed(wrong_screens(True, False, False)))
    print("  the report's code:    " + listed(wrong_screens(False, True, False)))
    print("  the reminder's code:  " + listed(wrong_screens(False, False, True)))
    print("Only the parser explains both findings, and it predicts a third nobody filed: the audit log.")
    print("Days right of 5, as the fixes go in:")
    print("  " + pad("", 28) + "report  reminder  audit")
    for st in triage.states():
        print("  " + pad(st[0], 28) + lpad(str(triage.right_days("report", st[1], st[2])), 6) + lpad(str(triage.right_days("reminder", st[1], st[2])), 10) + lpad(str(triage.right_days("audit", st[1], st[2])), 7))
    print("Patched where seen, both findings close and the audit log stays wrong; fix the parser later and the two patches make their screens wrong again.")

def one_report():
    print("One report, three wrong balances:")
    for r in triage.reported():
        print("  " + lpad(str(r), 5) + " shows " + lpad(triage.render(r, True, True), 4) + ", should be " + triage.truth(r))
    print("Over the " + str(len(triage.inputs())) + " inputs " + listed(triage.inputs()) + ":")
    print("  " + pad("fix", 22) + "wrong")
    for fx in ["none", "fitted", "sign", "clamp", "both"]:
        bad = []
        for v in triage.inputs():
            if triage.shown(v, fx) != triage.truth(v):
                bad = bad + [v]
        if fx == "fitted":
            name = "the reported inputs"
        elif fx == "sign":
            name = "keep the sign"
        elif fx == "clamp":
            name = "no clamp at 99"
        else:
            name = fx
        print("  " + pad(name, 22) + pad(str(len(bad)), 4) + listed(bad))
    s = []
    c = []
    left = 0
    for v in triage.inputs():
        if triage.shown(v, "fitted") != triage.truth(v):
            left = left + 1
            if triage.render(v, True, False) != triage.truth(v):
                s = s + [v]
            if triage.render(v, False, True) != triage.truth(v):
                c = c + [v]
    print("Fitted to the report, the report passes and " + str(left) + " failures remain; each mechanism run alone accounts for its share:")
    print("  the sign dropped:      " + listed(s))
    print("  past 99 clamped:       " + listed(c))
    print("The report could not show that two of its rows came from one cause and the third from another.")

def backlog():
    t = plain(input("fixes each period (1 or 2)> "))
    if t == "":
        print("Cancelled.")
        return
    if t != "1" and t != "2":
        print("Type 1 or 2.")
        return
    cap = int(t)
    print("Two findings arrive in each of periods 1 to 5; " + t + " fixed a period; the harm each does every period it waits is summed over 6 periods.")
    a = triage.work("fifo", cap, 6)
    b = triage.work("harm", cap, 6)
    print("  in arrival order:  harm " + lpad(str(a[0]), 3) + "; still open: " + listed(a[2]))
    print("  worst first:       harm " + lpad(str(b[0]), 3) + "; still open: " + listed(b[2]))
    if a[0] != b[0]:
        print("The same number fixed each period, and the order changes the harm by " + str(a[0] - b[0]) + ": once findings outrun fixes, the order is not a preference.")
    else:
        print("The order changes nothing here.")
    if len(a[2]) == 0 and len(b[2]) == 0:
        print("With fixes keeping up, the queue empties either way.")

print("== Bug triage ==")
print("Two findings with one cause, one report with two, and a backlog that outruns its fixes.")
running = True
while running:
    print("")
    print("1) two findings  2) one report  3) the backlog  4) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        two_findings()
    elif choice == "2":
        one_report()
    elif choice == "3":
        backlog()
    elif choice == "4":
        running = False
    else:
        print("Pick a number from 1 to 4.")
print("Bye.")
```

### triage.eml

```eml
# P158 bug triage - three ways a bug report misleads the people who fix it.
# As in the corpus case two-findings-one-defect, two findings on two
# screens, a report a day late and a reminder a day late, come from one
# shared date parser; patched where each was seen, both go quiet, a third
# consumer stays wrong, and fixing the parser later breaks the two patches.
# As in the corpus case one-report-two-mechanisms, one report of three wrong
# outputs has two causes underneath, and a patch fitted to the reported
# inputs fixes the report and neither cause. As in the corpus case
# finding-outruns-fixing, once findings arrive faster than they are fixed
# the queue never empties, and the order it is worked in decides the harm.

def days():
    return [3, 10, 17, 24, 31]

def weekday(day, parser_bug):
    if parser_bug:
        return (day + 1) % 7
    return day % 7

def report(day, parser_bug, patched):
    # the first weekday of the weekly report; the local patch takes a day off
    weekday(day, parser_bug) => w
    if patched:
        return (w + 6) % 7
    return w

def reminder(day, parser_bug, patched):
    weekday(day, parser_bug) => w
    if patched:
        return (w + 6) % 7
    return w

def audit(day, parser_bug):
    return weekday(day, parser_bug)

def states():
    # [name, parser bug, local patches]
    return [["as shipped", True, False], ["patched where seen", True, True], ["parser fixed", False, False], ["parser fixed, patches left", False, True]]

def right_days(screen, parser_bug, patched):
    0 => n
    for d in days():
        if screen == "report":
            report(d, parser_bug, patched) => v
        elif screen == "reminder":
            reminder(d, parser_bug, patched) => v
        else:
            audit(d, parser_bug) => v
        if v == d % 7:
            n + 1 => n
    return n

def truth(v):
    if v < 0:
        return "-" + str(0 - v)
    return str(v)

def render(v, sign_bug, clamp_bug):
    # the balance as shown: the sign dropped, and anything past 99 clamped
    v => x
    if sign_bug and x < 0:
        0 - x => x
    if clamp_bug and x > 99:
        99 => x
    if x < 0:
        return "-" + str(0 - x)
    return str(x)

def reported():
    return [0 - 4, 150, 0 - 7]

def inputs():
    return [0 - 9, 0 - 4, 0, 3, 42, 99, 100, 150, 260, 0 - 7, 0 - 1, 77]

def shown(v, fix):
    # the balance under a fix: none, fitted (the reported inputs special
    # cased), sign, clamp, or both
    if fix == "fitted":
        for r in reported():
            if r == v:
                return truth(v)
        return render(v, True, True)
    return render(v, fix != "sign" and fix != "both", fix != "clamp" and fix != "both")

def findings():
    # [name, period it arrives, harm each period it stays unfixed]
    return [["f1", 1, 1], ["f2", 1, 9], ["f3", 2, 2], ["f4", 2, 8], ["f5", 3, 1], ["f6", 3, 7], ["f7", 4, 3], ["f8", 4, 6], ["f9", 5, 1], ["f10", 5, 10]]

def work(policy, capacity, periods):
    # [harm in all, fixed in order, still waiting]; each period the arrivals
    # join, every waiting finding does its harm, then capacity are fixed
    [] => waiting
    [] => fixed
    0 => harm
    1 => p
    while p <= periods:
        for f in findings():
            if f[1] == p:
                waiting + [f] => waiting
        for f in waiting:
            harm + f[2] => harm
        0 => k
        while k < capacity and len(waiting) > 0:
            0 => pick
            if policy == "harm":
                0 => i
                for f in waiting:
                    if f[2] > waiting[pick][2]:
                        i => pick
                    i + 1 => i
            fixed + [waiting[pick][0]] => fixed
            [] => rest
            0 => i
            for f in waiting:
                if i != pick:
                    rest + [f] => rest
                i + 1 => i
            rest => waiting
            k + 1 => k
        p + 1 => p
    [] => names
    for f in waiting:
        names + [f[0]] => names
    return [harm, fixed, names]
```

Python projection of triage.eml:

```python
def days():
    return [3, 10, 17, 24, 31]

def weekday(day, parser_bug):
    if parser_bug:
        return (day + 1) % 7
    return day % 7

def report(day, parser_bug, patched):
    w = weekday(day, parser_bug)
    if patched:
        return (w + 6) % 7
    return w

def reminder(day, parser_bug, patched):
    w = weekday(day, parser_bug)
    if patched:
        return (w + 6) % 7
    return w

def audit(day, parser_bug):
    return weekday(day, parser_bug)

def states():
    return [["as shipped", True, False], ["patched where seen", True, True], ["parser fixed", False, False], ["parser fixed, patches left", False, True]]

def right_days(screen, parser_bug, patched):
    n = 0
    for d in days():
        if screen == "report":
            v = report(d, parser_bug, patched)
        elif screen == "reminder":
            v = reminder(d, parser_bug, patched)
        else:
            v = audit(d, parser_bug)
        if v == d % 7:
            n = n + 1
    return n

def truth(v):
    if v < 0:
        return "-" + str(0 - v)
    return str(v)

def render(v, sign_bug, clamp_bug):
    x = v
    if sign_bug and x < 0:
        x = 0 - x
    if clamp_bug and x > 99:
        x = 99
    if x < 0:
        return "-" + str(0 - x)
    return str(x)

def reported():
    return [0 - 4, 150, 0 - 7]

def inputs():
    return [0 - 9, 0 - 4, 0, 3, 42, 99, 100, 150, 260, 0 - 7, 0 - 1, 77]

def shown(v, fix):
    if fix == "fitted":
        for r in reported():
            if r == v:
                return truth(v)
        return render(v, True, True)
    return render(v, fix != "sign" and fix != "both", fix != "clamp" and fix != "both")

def findings():
    return [["f1", 1, 1], ["f2", 1, 9], ["f3", 2, 2], ["f4", 2, 8], ["f5", 3, 1], ["f6", 3, 7], ["f7", 4, 3], ["f8", 4, 6], ["f9", 5, 1], ["f10", 5, 10]]

def work(policy, capacity, periods):
    waiting = []
    fixed = []
    harm = 0
    p = 1
    while p <= periods:
        for f in findings():
            if f[1] == p:
                waiting = waiting + [f]
        for f in waiting:
            harm = harm + f[2]
        k = 0
        while k < capacity and len(waiting) > 0:
            pick = 0
            if policy == "harm":
                i = 0
                for f in waiting:
                    if f[2] > waiting[pick][2]:
                        pick = i
                    i = i + 1
            fixed = fixed + [waiting[pick][0]]
            rest = []
            i = 0
            for f in waiting:
                if i != pick:
                    rest = rest + [f]
                i = i + 1
            waiting = rest
            k = k + 1
        p = p + 1
    names = []
    for f in waiting:
        names = names + [f[0]]
    return [harm, fixed, names]
```

## README

# P158 - Bug triage

Three ways a bug report misleads the people who fix it: two findings with
one cause, one report with two, and a backlog that grows faster than it is
fixed.

- `main.eml` - the menu, the two findings, the one report, and the backlog
  worked two ways
- `triage.eml` - the calendar screens and the date parser they share, the
  states as fixes go in, the balance as shown and the fixes for it, the
  findings in the backlog, and working them

How each part works:

- As in the corpus case `two-findings-one-defect`, the weekly report
  starts a day late and reminders come a day late. Each candidate cause is
  run alone: the shared date parser makes the report, the reminder and the
  audit log wrong; each screen's own code only itself. Only the parser
  explains both findings, and it predicts a third nobody filed. Patched
  where each was seen, both findings close and the audit log stays wrong;
  fix the parser later and the two patches make their screens wrong again.
- As in the corpus case `one-report-two-mechanisms`, one report of three
  wrong balances - -4, 150, -7 - has two causes: the sign dropped and
  anything past 99 clamped. Special-casing the reported inputs makes the
  report pass and leaves 4 of 12 inputs wrong, each explained by one
  mechanism run alone; fixing both mechanisms leaves none.
- As in the corpus case `finding-outruns-fixing`, two findings arrive in
  each of five periods. Fixing one a period, the queue never empties, and
  working the worst first instead of in arrival order cuts the harm from
  127 to 71 with the same fixes; fixing two a period, the queue empties and
  the order changes nothing.

What is checked: menu choices 1 to 4; fixes each period 1 or 2. An empty
answer cancels.

Sessions: `sessions/basic.in` gives:
- the two findings and the one report;
- the backlog at one fix a period and at two.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- fixes each period: a cancel, 3 and x; then the one report.

Built on the verified corpus cases `two-findings-one-defect` (two symptoms
patched locally over one shared cause), `one-report-two-mechanisms` (a
patch fitted to the reported inputs) and `finding-outruns-fixing` (the
same fixing capacity in arrival order and by harm).
